Add tool to display Secure Execution system information

Add 'pvinfo' a tool to display the information of enabled flags, and
print the debug information in Yaml and Human format.

Reviewed-by: Steffen Eiden <seiden@linux.ibm.com>
Signed-off-by: Serena Santosh <serena@linux.ibm.com>
Signed-off-by: Ann Mariya Jojo <annjojo@linux.ibm.com>
Signed-off-by: Annu Sharma <annu09@linux.ibm.com>
Signed-off-by: Steffen Eiden <seiden@linux.ibm.com>
This commit is contained in:
Annu Sharma
2025-12-03 16:31:04 +00:00
committed by Steffen Eiden
parent c0d8034962
commit ce82010827
33 changed files with 1626 additions and 2 deletions

14
rust/Cargo.lock generated
View File

@@ -652,6 +652,20 @@ dependencies = [
"utils",
]
[[package]]
name = "pvinfo"
version = "0.12.0"
dependencies = [
"anyhow",
"clap",
"clap_complete",
"s390_pv_core",
"serde",
"serde_yaml",
"tempfile",
"utils",
]
[[package]]
name = "pvsecret"
version = "0.12.0"

View File

@@ -6,6 +6,7 @@ members = [
"pvapconfig",
"pvattest",
"pvimg",
"pvinfo",
"pvsecret",
"utils",
]

View File

@@ -28,9 +28,9 @@ ifneq (${HAVE_LIBCURL},0)
PV_TARGETS := pvsecret pvattest pvimg
ifeq ($(HOST_ARCH),s390x)
PV_TARGETS += pvapconfig
PV_TARGETS += pvapconfig pvinfo
else
BUILD_TARGETS += skip-pvapconfig
BUILD_TARGETS += skip-pvapconfig skip-pvinfo
endif #HOSTARCH
PV_BUILD_TARGETS := $(PV_TARGETS)
@@ -75,6 +75,9 @@ skip-build:
skip-pv-build:
echo " SKIP rust-pv-tools due to unresolved dependencies"
skip-pvinfo:
echo " SKIP pvinfo due to unsupported architecture (s390x only)"
skip-pvapconfig:
echo " SKIP pvapconfig due to unsupported architecture (s390x only)"

25
rust/pvinfo/Cargo.toml Normal file
View File

@@ -0,0 +1,25 @@
[package]
name = "pvinfo"
version = "0.12.0"
edition.workspace = true
license.workspace = true
rust-version.workspace = true
[lints]
workspace = true
[dependencies]
anyhow = "1"
clap = { version = "4.5", features = ["derive"] }
serde = { version = "1.0.217", features = ["derive"] }
serde_yaml = "0.9.21"
pv_core = { path = "../pv_core", package = "s390_pv_core" }
utils = { path = "../utils" }
[dev-dependencies]
tempfile = "3.15.0"
[build-dependencies]
clap = { version = "4.5", features = ["derive"] }
clap_complete = "4.5"

26
rust/pvinfo/build.rs Normal file
View File

@@ -0,0 +1,26 @@
// SPDX-License-Identifier: MIT
//
// Copyright IBM Corp. 2024
// it under the terms of the MIT license. See LICENSE for details.
#![allow(missing_docs)]
use clap::CommandFactory;
use clap_complete::{generate_to, Shell};
use std::env;
use std::io::Error;
include!("src/cli.rs");
fn main() -> Result<(), Error> {
let outdir = env::var_os("OUT_DIR").unwrap();
let crate_name = env!("CARGO_PKG_NAME");
let mut cmd = CliOptions::command();
for &shell in Shell::value_variants() {
generate_to(shell, &mut cmd, crate_name, &outdir)?;
}
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=src/cli.rs");
println!("cargo:rerun-if-changed=../utils/src/cli.rs");
Ok(())
}

230
rust/pvinfo/src/cli.rs Normal file
View File

@@ -0,0 +1,230 @@
// SPDX-License-Identifier: MIT
//
// Copyright IBM Corp. 2025
use clap::{Parser, Subcommand, ValueEnum};
/// Output format for pvinfo results
#[derive(Copy, Clone, Debug, ValueEnum, Default)]
pub enum OutputFormat {
#[default]
/// Human-readable format
Human,
/// YAML format
Yaml,
}
/// Query information about IBM Secure Execution system status
///
/// The PV Info Tool queries and displays information provided by the
/// Ultravisor about IBM Secure Execution host and guest systems.
/// It can show the Secure Execution (SE) status, installed facilities,
/// supported features, supported flags, and limits.
///
/// By default, running `pvinfo` without arguments prints all available
/// information in human-readable format.
/// Specific flags allow focused queries (e.g., only SE status, only limits).
/// Output can also be formatted as YAML.
#[derive(Parser, Debug)]
#[command(author)]
pub struct CliOptions {
/// Display the Secure Execution status of the system
///
/// Prints whether this system is running as an SE-host,
/// SE-guest, or none.
#[arg(long)]
pub se_status: bool,
/// Show installed Ultravisor calls
///
/// Lists all facilities supported by the Ultravisor.
#[arg(long)]
pub facilities: bool,
/// Show Ultravisor feature indications
///
/// Lists the feature bits reported by the Ultravisor that describe
/// available Secure Execution functionality.
#[arg(long)]
pub feature_indications: bool,
/// Show supported plaintext attestation flags
///
/// Prints the list of flags that can be used in attestation requests.
#[arg(long)]
pub supported_plaintext_attestation_flags: bool,
/// Show supported SE header versions
///
/// Prints the list of header versions supported.
#[arg(long)]
pub supported_se_header_versions: bool,
/// Show supported secret types
///
/// Lists the types of secrets.
#[arg(long)]
pub supported_secret_types: bool,
/// Show supported plaintext control flags
///
/// Lists control flags that can be set .
#[arg(long)]
pub supported_plaintext_control_flags: bool,
/// Show supported attestation request versions
///
/// Lists the versions of attestation request .
#[arg(long)]
pub supported_attestation_request_versions: bool,
/// Show supported add-secret request versions
///
/// Lists the versions of add-secret request supported.
#[arg(long)]
pub supported_add_secret_request_versions: bool,
/// Show supported plaintext add-secret flags
///
/// Lists control flags available for plaintext Add-secret requests.
#[arg(long)]
pub supported_plaintext_add_secret_flags: bool,
/// Show Secure Execution limits
///
/// Prints limits such as the maximum number of CPUs,
/// maximum guests, maximum retrievable secrets, etc.
#[arg(long)]
pub limits: bool,
/// Print version information and exit
#[arg(long)]
pub version: bool,
/// Select the output format
///
/// By default, output is human-readable text.
/// Use `--format yaml` to produce YAML output instead.
#[arg(long, value_enum, default_value_t)]
pub format: OutputFormat,
#[command(subcommand)]
pub command: Option<Commands>,
}
/// Additional commands to query supported flags
#[derive(Subcommand, Debug)]
pub enum Commands {
/// Query supported flags grouped by category
///
/// Provides detailed information about supported secret types,
/// attestation flags, and header versions.
/// By default, all categories are shown if no specific options are set.
SupportedFlags {
/// Show supported secret-related flags
#[arg(long)]
secret: bool,
/// Show supported attestation-related flags
#[arg(long)]
attestation: bool,
/// Show supported header-related flags
#[arg(long)]
header: bool,
},
}
impl CliOptions {
/// Returns true if any query flags or subcommands were provided.
pub fn any_flags_set(&self) -> bool {
self.se_status
|| self.facilities
|| self.feature_indications
|| self.supported_plaintext_attestation_flags
|| self.supported_se_header_versions
|| self.supported_secret_types
|| self.supported_plaintext_control_flags
|| self.supported_attestation_request_versions
|| self.supported_add_secret_request_versions
|| self.supported_plaintext_add_secret_flags
|| self.limits
|| self.command.is_some()
}
/// Sets all flags to true if no flags or subcommands are set.
pub fn post_process(&mut self) {
if !self.any_flags_set() {
self.se_status = true;
self.facilities = true;
self.feature_indications = true;
self.supported_plaintext_attestation_flags = true;
self.supported_se_header_versions = true;
self.supported_secret_types = true;
self.supported_plaintext_control_flags = true;
self.supported_attestation_request_versions = true;
self.supported_add_secret_request_versions = true;
self.supported_plaintext_add_secret_flags = true;
self.limits = true;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
// Helper function to create a base CLI with all flags false
fn base_cli() -> CliOptions {
CliOptions {
se_status: false,
facilities: false,
feature_indications: false,
supported_plaintext_attestation_flags: false,
supported_se_header_versions: false,
supported_secret_types: false,
supported_plaintext_control_flags: false,
supported_attestation_request_versions: false,
supported_add_secret_request_versions: false,
supported_plaintext_add_secret_flags: false,
limits: false,
version: false,
command: None,
format: OutputFormat::Human,
}
}
#[test]
fn test_any_flags_set_false_when_all_false() {
let cli = base_cli();
assert!(!cli.any_flags_set());
}
#[test]
fn test_any_flags_set_true_when_single_flag_true() {
let mut cli = base_cli();
cli.se_status = true;
assert!(cli.any_flags_set());
}
#[test]
fn test_any_flags_set_true_when_command_set() {
let mut cli = base_cli();
cli.command = Some(Commands::SupportedFlags {
secret: true,
attestation: true,
header: true,
});
assert!(cli.any_flags_set());
}
#[test]
fn test_any_flags_set_true_when_multiple_flags_true() {
let mut cli = base_cli();
cli.se_status = true;
cli.limits = true;
cli.feature_indications = true;
assert!(cli.any_flags_set());
}
}

View File

@@ -0,0 +1,43 @@
// SPDX-License-Identifier: MIT
//
// Copyright IBM Corp. 2025
//! Constants used throughout the PV Info Tool
// Base directories
#[cfg(test)]
pub const BASE_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/assets");
#[cfg(not(test))]
pub const BASE_DIR: &str = "/sys/firmware/uv";
pub const QUERY_DIR: &str = "query";
// File names
pub const FACILITIES: &str = "facilities";
pub const FEATURE_INDICATIONS: &str = "feature_indications";
pub const PROT_VIRT_GUEST: &str = "prot_virt_guest";
pub const PROT_VIRT_HOST: &str = "prot_virt_host";
pub const MAX_ADDRESS: &str = "max_address";
pub const MAX_ASSOC_SECRETS: &str = "max_assoc_secrets";
pub const MAX_CPUS: &str = "max_cpus";
pub const MAX_GUESTS: &str = "max_guests";
pub const MAX_RETR_SECRETS: &str = "max_retr_secrets";
pub const MAX_SECRETS: &str = "max_secrets";
pub const SUPP_ADD_SECRET_PCF: &str = "supp_add_secret_pcf";
pub const SUPP_ADD_SECRET_REQ_VER: &str = "supp_add_secret_req_ver";
pub const SUPP_ATT_PFLAGS: &str = "supp_att_pflags";
pub const SUPP_ATT_REQ_HDR_VER: &str = "supp_att_req_hdr_ver";
pub const SUPP_SE_HDR_PCF: &str = "supp_se_hdr_pcf";
pub const SUPP_SE_HDR_VER: &str = "supp_se_hdr_ver";
pub const SUPP_SECRET_TYPES: &str = "supp_secret_types";
// Limits
pub const LIMITS: [(&str, &str); 6] = [
(MAX_ADDRESS, "Maximal Address for a SE-Guest"),
(MAX_ASSOC_SECRETS, "Maximal number of associated secrets"),
(MAX_CPUS, "Maximal number of CPUs in one SE-Guest"),
(MAX_GUESTS, "Maximal number of SE-Guests"),
(MAX_RETR_SECRETS, "Maximal number of retrievable secrets"),
(MAX_SECRETS, "Maximal number of secrets in the system"),
];

190
rust/pvinfo/src/handlers.rs Normal file
View File

@@ -0,0 +1,190 @@
// SPDX-License-Identifier: MIT
//
// Copyright IBM Corp. 2025
//! Handlers for processing CLI flags and subcommands
use crate::constants::*;
use crate::io_utils::{collect_bit_messages, collect_version_flags, read_hex_from_file};
use crate::strings::*;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
// Represents metadata for a supported content file.
struct Content {
file: &'static str,
label: &'static str,
desc_content: Option<&'static str>,
}
impl Content {
//Reads a hex file, then prints either:
// bit messages or version flags
fn handle_flag(&self, writer: &mut dyn Write, query_dir: &Path) -> io::Result<String> {
let hex = match read_hex_from_file(query_dir.join(self.file), self.label) {
Some(h) => h,
None => {
return Ok(String::new());
}
};
let mut output = String::new();
let header = format!("\n{}:", self.label);
writeln!(writer, "{header}")?;
output.push_str(&header);
match self.desc_content {
Some(desc) => {
for msg in collect_bit_messages(hex, desc) {
writeln!(writer, "{msg}")?;
output.push_str(&format!("\n{msg}"));
}
}
None => {
for v in collect_version_flags(hex) {
writeln!(writer, "{v}")?;
output.push_str(&format!("\n{v}"));
}
}
}
Ok(output)
}
}
// Define constants for each supported flag group
const SUPP_SECRET_C: Content = Content {
file: SUPP_SECRET_TYPES,
label: "Supported Secret types",
desc_content: Some(SUPP_SECRET_TYPES_DESC),
};
const SUPP_ADD_SECRET_REQ_C: Content = Content {
file: SUPP_ADD_SECRET_REQ_VER,
label: "Supported Add Secret Request Versions",
desc_content: None,
};
const SUPP_ADD_SECRET_PCF_C: Content = Content {
file: SUPP_ADD_SECRET_PCF,
label: "Supported Plaintext Add Secret Flags",
desc_content: Some(SUPP_ADD_SECRET_PCF_DESC),
};
const SUPP_ATT_PFLAGS_C: Content = Content {
file: SUPP_ATT_PFLAGS,
label: "Supported Plaintext Attestation Flags",
desc_content: Some(SUPP_ATT_PFLAGS_DESC),
};
const SUPP_ATT_REQ_HDR_VER_C: Content = Content {
file: SUPP_ATT_REQ_HDR_VER,
label: "Supported Attestation Request Versions",
desc_content: None,
};
const SUPP_SE_HDR_VER_C: Content = Content {
file: SUPP_SE_HDR_VER,
label: "Supported SE Header Versions",
desc_content: None,
};
const SUPP_SE_HDR_PCF_C: Content = Content {
file: SUPP_SE_HDR_PCF,
label: "Supported Plaintext Control Flags",
desc_content: Some(SUPP_SE_HDR_PCF_DESC),
};
// Main entry point for handling the supported-flags subcommand.
pub fn handle_supported_flags(
writer: &mut dyn Write,
secret: bool,
attestation: bool,
header: bool,
query_dir: PathBuf,
) -> io::Result<Vec<String>> {
let mut results = Vec::new();
if secret {
results.push(SUPP_SECRET_C.handle_flag(writer, &query_dir)?);
results.push(SUPP_ADD_SECRET_REQ_C.handle_flag(writer, &query_dir)?);
results.push(SUPP_ADD_SECRET_PCF_C.handle_flag(writer, &query_dir)?);
}
if attestation {
results.push(SUPP_ATT_PFLAGS_C.handle_flag(writer, &query_dir)?);
results.push(SUPP_ATT_REQ_HDR_VER_C.handle_flag(writer, &query_dir)?);
}
if header {
results.push(SUPP_SE_HDR_VER_C.handle_flag(writer, &query_dir)?);
results.push(SUPP_SE_HDR_PCF_C.handle_flag(writer, &query_dir)?);
}
if results.is_empty() {
let mut all_results = handle_supported_flags(writer, true, true, true, query_dir)?;
results.append(&mut all_results);
}
Ok(results)
}
// Unit Tests for handlers
#[cfg(test)]
mod test {
use super::*;
use std::fs;
use tempfile::tempdir;
// Verifies that handle_supported_flags correctly processes all
#[test]
fn test_handle_supported_flags_with_buffer() {
let dir = tempdir().unwrap();
// Creating files
fs::write(dir.path().join(SUPP_SECRET_TYPES), "1").unwrap();
fs::write(dir.path().join(SUPP_ADD_SECRET_REQ_VER), "1").unwrap();
fs::write(dir.path().join(SUPP_ADD_SECRET_PCF), "1").unwrap();
fs::write(dir.path().join(SUPP_ATT_PFLAGS), "1").unwrap();
fs::write(dir.path().join(SUPP_ATT_REQ_HDR_VER), "1").unwrap();
fs::write(dir.path().join(SUPP_SE_HDR_VER), "1").unwrap();
fs::write(dir.path().join(SUPP_SE_HDR_PCF), "1").unwrap();
// Capture output into buffer
let mut buffer = Vec::new();
let results =
handle_supported_flags(&mut buffer, true, true, true, dir.path().to_path_buf())
.unwrap();
// Convert buffer to string
let printed = String::from_utf8(buffer).unwrap();
let normalized: String = printed
.lines()
.map(|line| line.trim())
.filter(|line| !line.is_empty())
.collect::<Vec<_>>()
.join(" ");
let expected = [
"Supported Secret types:",
"Supported Add Secret Request Versions:",
"Supported Plaintext Add Secret Flags:",
"Supported Plaintext Attestation Flags:",
"Supported Attestation Request Versions:",
"Supported SE Header Versions:",
"Supported Plaintext Control Flags:",
];
for label in &expected {
assert!(
normalized.contains(label),
"{label} missing in printed output!\nNormalized buffer:\n{normalized}"
);
assert!(
results.iter().any(|r| r.contains(label)),
"{label} missing in results Vec!"
);
}
}
}

320
rust/pvinfo/src/io_utils.rs Normal file
View File

@@ -0,0 +1,320 @@
// SPDX-License-Identifier: MIT
//
// Copyright IBM Corp. 2025
//! I/O utilities for the PV Info Tool
use crate::constants::*;
use anyhow::{Context, Result};
use pv_core::misc::{read_file, read_file_string, try_parse_u64};
use pv_core::misc::{Flags, Msb0Flags64};
use std::collections::HashMap;
use std::path::Path;
use std::str;
// Verify that the Ultravisor directory exists
pub fn check_uv_exists() -> Result<()> {
let uv_path = Path::new(BASE_DIR);
if !uv_path.exists() {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"Ultravisor directory missing",
))
.context("This system is neither a SE-host or SE-guest");
}
Ok(())
}
// File readers
pub fn read_hex_from_file<P: AsRef<Path>>(path: P, context: &str) -> Option<u64> {
// Add precise context for which file failed
let content_str = read_file_string(path.as_ref(), context).ok()?;
let first_line = content_str.lines().next()?.trim();
if first_line.is_empty() {
return None;
}
match try_parse_u64(first_line, context) {
Ok(val) if val != 0 => Some(val),
Ok(_) => None, // treat "0x0" as absent
Err(e) => panic!(
"Failed to parse hex value in {} ({}): {}",
path.as_ref().display(),
context,
e
),
}
}
pub fn read_integer_from_file<P: AsRef<Path>>(path: P, context: &str) -> Option<u64> {
let content_str = read_file_string(path.as_ref(), context).ok()?;
let first_line = content_str.lines().next()?.trim();
if first_line.is_empty() {
return None;
}
match first_line.parse::<u64>() {
Ok(val) => Some(val),
Err(e) => panic!(
"Failed to parse integer in {} ({}): {}",
path.as_ref().display(),
context,
e
),
}
}
pub fn read_bool_from_file<P: AsRef<Path>>(path: P, context: &str) -> bool {
let content = read_file(path.as_ref(), context).unwrap_or_else(|e| {
panic!(
"Failed to read {} ({}): {}",
path.as_ref().display(),
context,
e
)
});
let content_str = str::from_utf8(&content)
.unwrap_or_else(|_| panic!("Invalid UTF-8 in {} ({})", path.as_ref().display(), context));
match content_str.lines().next().unwrap_or("").trim() {
"1" => true,
"0" => false,
other => panic!(
"Invalid boolean value in {} ({}): {}",
path.as_ref().display(),
context,
other
),
}
}
// Collectors
pub fn collect_bit_messages(hex_value: u64, desc_content: &str) -> Vec<String> {
let flags = Msb0Flags64::from(hex_value);
let mut messages = Vec::new();
for (line_index, line) in desc_content.lines().enumerate() {
if flags.is_set(line_index as u8) {
if line.to_lowercase().contains("reserved") {
messages.push(format!("{line} Bit-{line_index}"));
} else {
messages.push(line.to_string());
}
}
}
if messages.is_empty() {
messages.push("No matching messages. But these bits are ON:".into());
for bit in 0..64 {
if flags.is_set(bit) {
messages.push(format!("- Bit {bit} is ON"));
}
}
}
messages
}
pub fn collect_version_flags(hex_value: u64) -> Vec<String> {
let flags = Msb0Flags64::from(hex_value);
let mut versions = Vec::new();
for bit in 0..64 {
if flags.is_set(bit as u8) {
let version = (bit + 1) * 0x100;
versions.push(format!("version {version:x} hex is supported"));
}
}
versions
}
pub fn collect_limits(query_dir: &Path) -> HashMap<String, u64> {
let mut map = HashMap::new();
for (file, desc) in LIMITS {
if let Some(val) = read_integer_from_file(query_dir.join(file), file) {
map.insert(desc.to_string(), val);
}
}
map
}
#[cfg(test)]
mod test {
//! Unit Tests for io_utils
use super::*;
use std::io::{self, Write};
use std::path::Path;
use tempfile::{tempdir, NamedTempFile};
// Tests check_uv_exists() by observing the real BASE_DIR at test runtime
// if BASE_DIR exists on the machine running the tests, check_uv_exists() must return Ok
// otherwise, it must return Err(anyhow::Error) whose cause is an io::ErrorKind::NotFound,
// and whose message also contains the added context string
#[test]
fn test_check_uv_exists_behaviour_matches_base_dir() {
let base_dir = BASE_DIR;
let uv_path = Path::new(base_dir);
let res = check_uv_exists();
if uv_path.exists() {
assert!(
res.is_ok(),
"BASE_DIR exists but check_uv_exists returned Err: {res:?}"
);
} else {
assert!(
res.is_err(),
"BASE_DIR missing but check_uv_exists returned Ok"
);
if let Err(e) = res {
// downcast anyhow::Error into std::io::Error
if let Some(io_err) = e.downcast_ref::<io::Error>() {
assert_eq!(io_err.kind(), io::ErrorKind::NotFound);
} else {
panic!("Expected std::io::Error inside anyhow::Error, got: {e:?}");
}
// ensure both the base error and context message appear
let msg = format!("{e}");
assert!(msg.contains("Ultravisor directory missing"));
assert!(msg.contains("This system is neither a SE-host or SE-guest"));
}
}
}
// read_hex_from_file tests
#[test]
fn test_read_hex_from_file_valid() {
let mut file = NamedTempFile::new().unwrap();
writeln!(file, "0x1a2b").unwrap();
let got = read_hex_from_file(file.path(), "test");
assert_eq!(got, Some(0x1a2b));
}
#[test]
fn test_read_hex_from_file_zero_is_none() {
let mut file = NamedTempFile::new().unwrap();
writeln!(file, "0x0").unwrap();
assert_eq!(read_hex_from_file(file.path(), "test"), None);
}
#[test]
#[should_panic(expected = "Failed to parse hex value")]
fn test_read_hex_from_file_invalid_panics() {
let mut file = NamedTempFile::new().unwrap();
writeln!(file, "not_hex").unwrap();
// should panic now
let _ = read_hex_from_file(file.path(), "test");
}
#[test]
fn test_read_hex_from_file_missing_returns_none() {
// missing file -> None
let dir = tempdir().unwrap();
let missing = dir.path().join("no_such_file");
assert_eq!(read_hex_from_file(missing, "test"), None);
}
// read_integer_from_file tests
#[test]
fn test_read_integer_from_file_valid() {
let mut file = NamedTempFile::new().unwrap();
writeln!(file, "12345").unwrap();
assert_eq!(read_integer_from_file(file.path(), "test"), Some(12345));
}
#[test]
fn test_read_integer_from_file_empty_returns_none() {
let mut file = NamedTempFile::new().unwrap();
writeln!(file).unwrap();
assert_eq!(read_integer_from_file(file.path(), "test"), None);
}
#[test]
#[should_panic(expected = "Failed to parse integer")]
fn test_read_integer_from_file_invalid_panics() {
let mut file2 = NamedTempFile::new().unwrap();
writeln!(file2, "abc").unwrap();
// should panic now
let _ = read_integer_from_file(file2.path(), "test");
}
// read_bool_from_file tests
#[test]
fn test_read_bool_from_file_true() {
let mut file = NamedTempFile::new().unwrap();
writeln!(file, "1").unwrap();
assert!(read_bool_from_file(file.path(), "test"));
}
#[test]
fn test_read_bool_from_file_false() {
let mut file = NamedTempFile::new().unwrap();
writeln!(file, "0").unwrap();
assert!(!read_bool_from_file(file.path(), "test"));
}
#[test]
#[should_panic(expected = "Invalid boolean value")]
fn test_read_bool_from_file_invalid_value_panics() {
let mut file = NamedTempFile::new().unwrap();
writeln!(file, "yes").unwrap();
// should panic with clear error
let _ = read_bool_from_file(file.path(), "test");
}
// collect_bit_messages tests
#[test]
fn test_collect_bit_messages_matches_and_reserved() {
// lines: index 0 -> bit 63, index 1 -> bit 62, index 2 -> bit 61
let desc = "First Feature\nreserved for future use\nThird Feature";
// set bit for line 0 and line 2
let hex = (1u64 << 63) | (1u64 << 61);
let messages = collect_bit_messages(hex, desc);
// Expect "First Feature" and "Third Feature" (and the reserved line should get the " Bit-<index>" if matched)
assert!(messages.iter().any(|m| m == "First Feature"));
assert!(messages.iter().any(|m| m == "Third Feature"));
// reserved wasn't set here; now test reserved specifically below
}
#[test]
fn test_collect_bit_messages_reserved_line() {
let desc = "one\nReserved entry\nthree";
// set the bit corresponding to line index 1 -> bit 62
let hex = 1u64 << (63 - 1);
let messages = collect_bit_messages(hex, desc);
assert_eq!(messages.len(), 1);
assert_eq!(messages[0], "Reserved entry Bit-1");
}
#[test]
fn test_collect_bit_messages_fallback_lists_bits() {
let desc = "only one line";
// pick a bit that does not map to line 0 (63) so fallback triggers; e.g. bit 5
let hex = 1u64 << 5;
let messages = collect_bit_messages(hex, desc);
assert!(!messages.is_empty());
assert!(messages[0].starts_with("No matching messages"));
assert!(messages.iter().any(|m| m.contains("Bit 5")));
}
// collect_version_flags tests
#[test]
fn test_collect_version_flags_two_bits() {
// top two bits set => versions 0x100 and 0x200 should be present
let hex = (1u64 << 63) | (1u64 << 62);
let versions = collect_version_flags(hex);
assert!(versions.contains(&"version 100 hex is supported".to_string()));
assert!(versions.contains(&"version 200 hex is supported".to_string()));
}
}

58
rust/pvinfo/src/main.rs Normal file
View File

@@ -0,0 +1,58 @@
// SPDX-License-Identifier: MIT
//
// Copyright IBM Corp. 2025
//! Main function for the PV Info Tool
use anyhow::Result;
use clap::Parser;
use std::io::{self, Write};
mod cli;
mod constants;
mod handlers;
mod io_utils;
mod pvinfo;
mod se_status;
mod strings;
use crate::cli::{CliOptions, Commands, OutputFormat};
use crate::constants::*;
use crate::handlers::handle_supported_flags;
use crate::io_utils::check_uv_exists;
use crate::pvinfo::PvInfo;
use std::path::PathBuf;
fn main() -> Result<()> {
// Parse CLI arguments and apply post-processing
let mut cli = CliOptions::parse();
cli.post_process();
let base_dir = PathBuf::from(BASE_DIR);
let query_dir = base_dir.join(QUERY_DIR);
check_uv_exists()?;
let mut stdout = io::stdout();
if cli.version {
utils::print_version!(2025);
return Ok(());
}
match &cli.command {
// Handle the supported-flags subcommand
Some(Commands::SupportedFlags {
secret,
attestation,
header,
}) => {
handle_supported_flags(&mut stdout, *secret, *attestation, *header, query_dir)?;
}
None => {
let data = PvInfo::read(&cli, &base_dir, &query_dir);
// Print output in the requested format
match cli.format {
OutputFormat::Human => data.write(&mut stdout)?,
OutputFormat::Yaml => write!(stdout, "{}", serde_yaml::to_string(&data).unwrap())?,
}
}
}
Ok(())
}

447
rust/pvinfo/src/pvinfo.rs Normal file
View File

@@ -0,0 +1,447 @@
// SPDX-License-Identifier: MIT
//
// Copyright IBM Corp. 2025
//! PV Info method implementation for the PV Info Tool
use crate::cli::CliOptions;
use crate::constants::*;
use crate::io_utils::{
collect_bit_messages, collect_limits, collect_version_flags, read_bool_from_file,
read_hex_from_file,
};
use crate::se_status::*;
use crate::strings::*;
use serde::Serialize;
use std::collections::HashMap;
use std::fmt;
use std::path::Path;
#[derive(Serialize)]
pub struct PvInfo {
pub se_status: Option<SeStatus>,
#[serde(skip_serializing_if = "Option::is_none")]
pub facilities: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub feature_indications: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limits: Option<HashMap<String, u64>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub supported_plaintext_control_flags: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub supported_se_header_versions: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub supported_plaintext_attestation_flags: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub supported_attestation_request_versions: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub supported_secret_types: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub supported_add_secret_request_versions: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub supported_plaintext_add_secret_flags: Option<Vec<String>>,
}
impl PvInfo {
pub fn read(cli: &CliOptions, base_dir: &Path, query_dir: &Path) -> Self {
// helper function
fn process_flag_file<F>(
enabled: bool,
query_dir: &Path,
file: &str,
collector: F,
) -> Option<Vec<String>>
where
F: Fn(u64) -> Vec<String>,
{
if !enabled {
return None;
}
read_hex_from_file(query_dir.join(file), file)
.map(collector)
.filter(|v| !v.is_empty())
}
// Read Secure Execution guest or host state
let virt_guest = read_bool_from_file(base_dir.join(PROT_VIRT_GUEST), PROT_VIRT_GUEST);
let virt_host = read_bool_from_file(base_dir.join(PROT_VIRT_HOST), PROT_VIRT_HOST);
let se_status = if cli.se_status {
Some(SeStatus::from_flags(virt_guest, virt_host))
} else {
None
};
// Collect all optional subsections depending on CLI flags
let facilities = process_flag_file(cli.facilities, query_dir, FACILITIES, |hex| {
collect_bit_messages(hex, FACILITIES_DESC)
});
let feature_indications = process_flag_file(
cli.feature_indications,
query_dir,
FEATURE_INDICATIONS,
|hex| collect_bit_messages(hex, FEATURE_INDICATIONS_DESC),
);
let limits = if cli.limits {
let map = collect_limits(query_dir);
if map.is_empty() {
None
} else {
Some(map)
}
} else {
None
};
let supported_plaintext_attestation_flags = process_flag_file(
cli.supported_plaintext_attestation_flags,
query_dir,
SUPP_ATT_PFLAGS,
|hex| collect_bit_messages(hex, SUPP_ATT_PFLAGS_DESC),
);
let supported_se_header_versions = process_flag_file(
cli.supported_se_header_versions,
query_dir,
SUPP_SE_HDR_VER,
collect_version_flags,
);
let supported_secret_types = process_flag_file(
cli.supported_secret_types,
query_dir,
SUPP_SECRET_TYPES,
|hex| collect_bit_messages(hex, SUPP_SECRET_TYPES_DESC),
);
let supported_plaintext_control_flags = process_flag_file(
cli.supported_plaintext_control_flags,
query_dir,
SUPP_SE_HDR_PCF,
|hex| collect_bit_messages(hex, SUPP_SE_HDR_PCF_DESC),
);
let supported_attestation_request_versions = process_flag_file(
cli.supported_attestation_request_versions,
query_dir,
SUPP_ATT_REQ_HDR_VER,
collect_version_flags,
);
let supported_add_secret_request_versions = process_flag_file(
cli.supported_add_secret_request_versions,
query_dir,
SUPP_ADD_SECRET_REQ_VER,
collect_version_flags,
);
let supported_plaintext_add_secret_flags = process_flag_file(
cli.supported_plaintext_add_secret_flags,
query_dir,
SUPP_ADD_SECRET_PCF,
|hex| collect_bit_messages(hex, SUPP_ADD_SECRET_PCF_DESC),
);
Self {
se_status,
facilities,
feature_indications,
limits,
supported_plaintext_control_flags,
supported_se_header_versions,
supported_plaintext_attestation_flags,
supported_attestation_request_versions,
supported_secret_types,
supported_add_secret_request_versions,
supported_plaintext_add_secret_flags,
}
}
//// Print the PvInfo data to the provided writer.
/// Prints SE status first (if present)
/// and then the rest of the sections.
pub fn write(&self, writer: &mut dyn std::io::Write) -> std::io::Result<()> {
if let Some(status) = &self.se_status {
writeln!(writer, "{status}")?;
}
write!(writer, "{self}")?;
Ok(())
}
}
// Implements human-readable output for PvInfo
impl fmt::Display for PvInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(fac) = &self.facilities {
writeln!(f, "\nFacilities: Installed Ultravisor Calls")?;
for entry in fac {
writeln!(f, "{entry}")?;
}
}
if let Some(fi) = &self.feature_indications {
writeln!(f, "\nFeature Indications: Ultravisor Features")?;
for entry in fi {
writeln!(f, "{entry}")?;
}
}
if let Some(lims) = &self.limits {
writeln!(f, "\nLimits:")?;
for (k, v) in lims {
writeln!(f, "{k} {v}")?;
}
}
if let Some(flags) = &self.supported_plaintext_attestation_flags {
writeln!(f, "\nSupported Plaintext Attestation Flags:")?;
for flag in flags {
writeln!(f, "{flag}")?;
}
}
if let Some(vers) = &self.supported_se_header_versions {
writeln!(f, "\nSupported SE Header Versions:")?;
for ver in vers {
writeln!(f, "{ver}")?;
}
}
if let Some(types) = &self.supported_secret_types {
writeln!(f, "\nSupported secret types:")?;
for ty in types {
writeln!(f, "{ty}")?;
}
}
if let Some(flags) = &self.supported_plaintext_control_flags {
writeln!(f, "\nSupported plaintext control flags:")?;
for flag in flags {
writeln!(f, "{flag}")?;
}
}
if let Some(vers) = &self.supported_attestation_request_versions {
writeln!(f, "\nSupported Attestation Request Versions:")?;
for ver in vers {
writeln!(f, "{ver}")?;
}
}
if let Some(vers) = &self.supported_add_secret_request_versions {
writeln!(f, "\nSupported Add Secret Request Versions:")?;
for ver in vers {
writeln!(f, "{ver}")?;
}
}
if let Some(flags) = &self.supported_plaintext_add_secret_flags {
writeln!(f, "\nSupported plaintext add secret flags:")?;
for flag in flags {
writeln!(f, "{flag}")?;
}
}
Ok(())
}
}
#[cfg(test)]
mod test {
//! Unit Tests for pvinfo_method
use super::*;
use crate::se_status::SeStatus;
use std::collections::HashMap;
// Test that Display for PvInfo produces a truly empty string
// when all optional fields are None/
#[test]
fn display_with_no_optional_fields_is_empty() {
// Create a PvInfo instance with only se_status set, everything else is None
let pv = PvInfo {
se_status: Some(SeStatus::from_flags(false, false)),
facilities: None,
feature_indications: None,
limits: None,
supported_plaintext_control_flags: None,
supported_se_header_versions: None,
supported_plaintext_attestation_flags: None,
supported_attestation_request_versions: None,
supported_secret_types: None,
supported_add_secret_request_versions: None,
supported_plaintext_add_secret_flags: None,
};
// Format PvInfo into a string
let out = format!("{pv}");
// Assert that when all optional fields are None, Display implementation outputs nothing
assert!(
out.trim().is_empty(),
"expected empty display, got: {out:?}"
);
}
// Test that Display for PvInfo containing all fields
// produces the correct headers and entries for each section.
// This verifies both section titles and specific values appear in the output
#[test]
fn display_with_all_fields_contains_expected_sections_and_entries() {
// Create a limits map with multiple entries
let mut limits = HashMap::new();
limits.insert("max_secrets".to_string(), 42_u64);
limits.insert("max_payload".to_string(), 1024_u64);
// Create a PvInfo instance with all fields populated
let pv = PvInfo {
se_status: Some(SeStatus::from_flags(true, false)),
facilities: Some(vec!["fac-a".into(), "fac-b".into()]),
feature_indications: Some(vec!["feat-x".into(), "feat-y".into()]),
limits: Some(limits),
supported_plaintext_control_flags: Some(vec!["pcf-1".into()]),
supported_se_header_versions: Some(vec!["v1".into(), "v2".into()]),
supported_plaintext_attestation_flags: Some(vec!["paf-1".into()]),
supported_attestation_request_versions: Some(vec!["arv-1".into()]),
supported_secret_types: Some(vec!["secret-type-foo".into()]),
supported_add_secret_request_versions: Some(vec!["asrv-1".into()]),
supported_plaintext_add_secret_flags: Some(vec!["pasf-1".into()]),
};
// Format PvInfo into a string
let out = format!("{pv}");
// Verify that all expected section headers appear in the formatted output
assert!(
out.contains("Facilities: Installed Ultravisor Calls"),
"missing facilities header: {out}"
);
assert!(
out.contains("Feature Indications: Ultravisor Features"),
"missing feature indications header: {out}"
);
assert!(out.contains("Limits:"), "missing Limits header: {out}");
assert!(
out.contains("Supported Plaintext Attestation Flags:"),
"missing attestation flags header: {out}"
);
assert!(
out.contains("Supported SE Header Versions:"),
"missing se header versions header: {out}"
);
assert!(
out.contains("Supported secret types:"),
"missing secret types header: {out}"
);
assert!(
out.contains("Supported plaintext control flags:"),
"missing plaintext control flags header: {out}"
);
assert!(
out.contains("Supported Attestation Request Versions:"),
"missing attestation request versions header: {out}"
);
assert!(
out.contains("Supported Add Secret Request Versions:"),
"missing add secret request versions header: {out}"
);
assert!(
out.contains("Supported plaintext add secret flags:"),
"missing plaintext add secret flags header: {out}"
);
// Verify that specific entries inside sections are correctly displayed
assert!(out.contains("fac-a"), "missing facility entry: {out}");
assert!(
out.contains("feat-x"),
"missing feature indication entry: {out}"
);
assert!(
out.contains("max_secrets 42"),
"missing limits key/value: {out}"
);
assert!(
out.contains("max_payload 1024"),
"missing limits key/value: {out}"
);
assert!(
out.contains("pcf-1"),
"missing plaintext control flag entry: {out}"
);
assert!(out.contains("v1"), "missing se header version entry: {out}");
assert!(
out.contains("secret-type-foo"),
"missing secret type entry: {out}"
);
assert!(
out.contains("pasf-1"),
"missing add secret flag entry: {out}"
);
}
// Test that serde serialization of `PvInfo` skips fields set to None
// Ensures optional values are omitted in the YAML output, but mandatory fields remain
#[test]
fn serde_serialization_skips_none_fields() {
// Create a PvInfo instance with some fields set, most left as None
let pv = PvInfo {
se_status: Some(SeStatus::from_flags(false, true)),
facilities: Some(vec!["one".into()]),
feature_indications: None,
limits: None,
supported_plaintext_control_flags: None,
supported_se_header_versions: None,
supported_plaintext_attestation_flags: None,
supported_attestation_request_versions: None,
supported_secret_types: None,
supported_add_secret_request_versions: None,
supported_plaintext_add_secret_flags: None,
};
// Serialize PvInfo into YAML
let v = serde_yaml::to_value(pv).expect("serialize to value");
// Verify YAML is a mapping (equivalent to an object in JSON)
let map = v.as_mapping().expect("expected YAML mapping");
// "facilities" should appear because it's Some(...)
assert!(
map.contains_key(serde_yaml::Value::from("facilities")),
"facilities should be serialized when Some"
);
// Optional fields that were None should not be serialized
assert!(
!map.contains_key(serde_yaml::Value::from("feature_indications")),
"feature_indications should be skipped when None"
);
assert!(
!map.contains_key(serde_yaml::Value::from("limits")),
"limits should be skipped when None"
);
assert!(
!map.contains_key(serde_yaml::Value::from("supported_plaintext_control_flags")),
"supported_plaintext_control_flags should be skipped when None"
);
// "se_status" should always be present
assert!(
map.contains_key(serde_yaml::Value::from("se_status")),
"se_status should be serialized"
);
}
}

View File

@@ -0,0 +1,99 @@
// SPDX-License-Identifier: MIT
//
// Copyright IBM Corp. 2025
//! se-status definitions for the PV Info Tool
use serde::Serialize; // Trait for serializing data structures (e.g., YAML)
use std::fmt; // Provides the `Display` trait for pretty-printing
/// Enum representing Secure Execution status
#[derive(Serialize, PartialEq, Debug)] // Automatically implements `serde::Serialize`
pub enum SeStatus {
/// Running as a Secure Execution Guest
Guest,
/// Running as a Secure Execution Host
Host,
/// Secure Execution is not enabled
Unsecure,
/// Invalid state: Secure Execution is enabled as both Guest and Host
/// This state is impossible in practice
Invalid,
}
// Implement the `Display` trait so we can print human-readable strings instead of enum names.
impl fmt::Display for SeStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// Map each enum variant to a descriptive string
let s = match self {
SeStatus::Guest => "Secure Execution Guest",
SeStatus::Host => "Secure Execution Host",
SeStatus::Unsecure => "Secure Execution is not enabled",
SeStatus::Invalid => "Invalid state: both guest and host enabled (impossible)",
};
// Write the chosen string into the formatter
write!(f, "{s}")
}
}
// Associated functions (like static methods) for constructing SeStatus values.
impl SeStatus {
/// Construct a `SeStatus` from two boolean flags:
/// - `virt_guest`: true if running as a guest
/// - `virt_host`: true if running as a host
///
/// Returns the correct variant of `SeStatus`.
pub fn from_flags(virt_guest: bool, virt_host: bool) -> Self {
match (virt_guest, virt_host) {
(true, false) => SeStatus::Guest,
(false, true) => SeStatus::Host,
(false, false) => SeStatus::Unsecure,
(true, true) => SeStatus::Invalid,
}
}
}
#[cfg(test)]
mod test {
//! Unit Tests for se_status
use super::SeStatus;
// Display implementation tests
// Verifies that each enum variant of SeStatus is converted into the expected human-readable string
// via the Display trait implementation
#[test]
fn test_display_strings() {
assert_eq!(SeStatus::Guest.to_string(), "Secure Execution Guest");
assert_eq!(SeStatus::Host.to_string(), "Secure Execution Host");
assert_eq!(
SeStatus::Unsecure.to_string(),
"Secure Execution is not enabled"
);
assert_eq!(
SeStatus::Invalid.to_string(),
"Invalid state: both guest and host enabled (impossible)"
);
}
// from_flags() constructor tests
// Ensures that from_flags() correctly maps the
// combinations of (virt_guest, virt_host) booleans
// into the expected SeStatus variant
#[test]
fn test_from_flags_variants() {
assert_eq!(SeStatus::from_flags(true, false), SeStatus::Guest);
assert_eq!(SeStatus::from_flags(false, true), SeStatus::Host);
assert_eq!(SeStatus::from_flags(false, false), SeStatus::Unsecure);
assert_eq!(SeStatus::from_flags(true, true), SeStatus::Invalid);
}
}

145
rust/pvinfo/src/strings.rs Normal file
View File

@@ -0,0 +1,145 @@
// SPDX-License-Identifier: MIT
//
// Copyright IBM Corp. 2025
//! For processing descriptions
pub const FACILITIES_DESC: &str = r#"Query Ultravisor Information
Initialize Ultravisor
Create Secure Configuration
Destroy Secure Configuration
Create Secure CPU
Destroy Secure CPU
Convert to Secure Storage
Convert from Secure Storage
Set Shared Access
Remove Shared Access
Reserved
Set Secure Parameters
Destroy Secure Storage
Unpack Image
Verify Image
Perform CPU Reset
Perform Initial CPU Reset
Set CPU State
Prepare for Reset
Perform CPU Clear Reset
Unshare All
Pin Shared Storage
Unpin Shared Storage
Destroy Secure Configuration Fast
Initiate Configuration Dump
Dump Configuration Storage State
Dump CPU State
Complete Configuration Dump
Retrieve Attestation Measurement
Add Secret
List Secrets
Lock Secrets
Verify Large Frame
Retrieve Secret"#;
pub const FEATURE_INDICATIONS_DESC: &str = r#"Reserved
Adapter interrupt virtualization is supported
Reserved
Reserved
AP passthrough is supported
AP interpretion passthrough is supported"#;
pub const SUPP_ADD_SECRET_PCF_DESC: &str = r#"Disable dumping"#;
pub const SUPP_ATT_PFLAGS_DESC: &str = r#"Reserved
The attestation request contains an optional nonce
Adding the SHA-256 hash of the public host key to the additional data area for measurement.
Adding the SHA-256 hash of the public host key for the Attestation request header to the additional data area for measurement.
The Add-secret Request Stream Flag (ARSF) is a SHA-512 hash of successful add-secret tags (in order), plus a byte indicating store lock status.
Adding the 320-byte firmware attestation measurement (FWCF) to the additional data area."#;
pub const SUPP_SE_HDR_PCF_DESC: &str = r#"Reserved
Reserved
Reserved
Reserved
Reserved
Reserved
Reserved
Reserved
Reserved
Reserved
Reserved
Reserved
Reserved
Reserved
Reserved
Reserved
Reserved
Reserved
Reserved
Reserved
Reserved
Reserved
Reserved
Reserved
Reserved
Reserved
Reserved
Reserved
Reserved
Reserved
Reserved
Reserved
Reserved
Reserved
Dumping of the secure execution guest is allowed
The decrypt image ultravisor command does not decrypt the content of the specified 4K-byte block of storage. The page-list digest, the address-list digest, and the tweak-list digest are still verified.
Reserved
Reserved
Reserved
Reserved
Reserved
Reserved
Reserved
Reserved
Reserved
Reserved
Reserved
Reserved
Reserved
Reserved
Reserved
Reserved
Reserved
Reserved
Reserved
Reserved
PCKMO encrypt-DEA/TDEA-key functions allowed
PCKMO encrypt-AES-key functions allowed
PCKMO encrypt-ECC-key functions allowed
Reserved
Reserved
Reserved
Temporary backup-host-key use allowed
Reserved"#;
pub const SUPP_SECRET_TYPES_DESC: &str = r#"Reserved
Meta
AP-association
Plaintext
AES 128
AES 192
AES 256
AES 128 XTS
AES 256 XTS
HMAC SHA 256
HMAC SHA 512
Reserved
Reserved
Reserved
Reserved
Reserved
Reserved
ECDSA P256 private key
ECDSA P384 private key
ECDSA P521 private key
EdDSA Ed25529 private key
EdDSA Ed448 private key
Update-CCK"#;

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@
1

View File

@@ -0,0 +1 @@
1000

View File

@@ -0,0 +1 @@
1000

View File

@@ -0,0 +1,4 @@
ff1ffff000000000
0
0
0

View File

@@ -0,0 +1 @@
fc00000000000000

View File

@@ -0,0 +1 @@
800000000000

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@
257

View File

@@ -0,0 +1 @@
8388607

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@
300050e3

View File

@@ -0,0 +1 @@
8000000000000000

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@
1000