Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions MANIFEST.md
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,7 @@ repository.
| proc-macro-crate | 3.5.0 | MIT OR Apache-2.0 |
| proc-macro2 | 1.0.106 | MIT OR Apache-2.0 |
| proc-macro2-diagnostics | 0.10.1 | MIT/Apache-2.0 |
| proptest | 1.11.0 | MIT OR Apache-2.0 |
| pulldown-cmark | 0.12.2 | MIT |
| quick-xml | 0.39.4 | MIT |
| quinn | 0.11.11 | MIT OR Apache-2.0 |
Expand All @@ -367,6 +368,7 @@ repository.
| rand_chacha | 0.9.0 | MIT OR Apache-2.0 |
| rand_core | 0.6.4 | MIT OR Apache-2.0 |
| rand_core | 0.9.5 | MIT OR Apache-2.0 |
| rand_xorshift | 0.4.0 | MIT OR Apache-2.0 |
| ratatui | 0.30.2 | MIT |
| ratatui-core | 0.1.2 | MIT |
| ratatui-crossterm | 0.1.2 | MIT |
Expand Down Expand Up @@ -515,6 +517,7 @@ repository.
| typenum | 1.20.1 | MIT OR Apache-2.0 |
| ucd-trie | 0.1.7 | MIT OR Apache-2.0 |
| uds_windows | 1.2.1 | MIT |
| unarray | 0.1.4 | MIT OR Apache-2.0 |
| unicase | 2.9.0 | MIT OR Apache-2.0 |
| unicode-ident | 1.0.24 | (MIT OR Apache-2.0) AND Unicode-3.0 |
| unicode-linebreak | 0.1.5 | Apache-2.0 |
Expand Down
58 changes: 58 additions & 0 deletions apps/rocm/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33986,6 +33986,64 @@ ID_LIKE="suse opensuse"
assert!(err.to_string().contains("protected system location"));
}

/// The gate above is the last thing between a registry entry and
/// `fs::remove_dir_all`, and the path it is handed is text somebody else
/// wrote. One folder has many spellings, so the refusal has to survive all
/// of them — a trailing separator, a doubled one, a `.`, or a `..` that
/// walks back out of the user's home directory. Each of these names `/etc`.
#[test]
#[cfg(unix)]
fn ensure_runtime_install_root_rejects_every_spelling_of_a_protected_path() {
let home = rocm_core::runtime_home_dir().expect("a home directory");
let home = home.display().to_string();
let spellings = [
"/etc/".to_owned(),
"//etc".to_owned(),
"/./etc".to_owned(),
"/etc/.".to_owned(),
format!("{home}/../../etc"),
];

let accepted: Vec<String> = spellings
.into_iter()
.filter(|text| ensure_runtime_install_root_is_safe_to_remove(Path::new(text)).is_ok())
.collect();

assert!(
accepted.is_empty(),
"accepted as safe to recursively delete: {accepted:?}"
);
}

/// The same question — "is this a system location?" — also keeps the local
/// assistant from installing into one, and there the folder arrives as
/// free-form JSON from a model rather than from a file the user wrote. A
/// trailing separator is exactly the sort of thing generated text carries,
/// so the refusal has to survive every spelling here too.
#[test]
#[cfg(unix)]
fn the_assistant_cannot_name_a_system_install_folder_by_respelling_it() {
let home = rocm_core::runtime_home_dir().expect("a home directory");
let home = home.display().to_string();
let spellings = [
"/usr/".to_owned(),
"/opt/".to_owned(),
"//usr".to_owned(),
"/./opt".to_owned(),
format!("{home}/../../usr"),
];

let accepted: Vec<String> = spellings
.into_iter()
.filter(|text| !chat_install_prefix_is_system(Path::new(text)))
.collect();

assert!(
accepted.is_empty(),
"accepted as a user install folder: {accepted:?}"
);
}

#[test]
fn plan_runtime_uninstall_does_not_mutate() -> Result<()> {
let (root, paths) = test_paths("runtime-uninstall-plan-dry-run");
Expand Down
22 changes: 22 additions & 0 deletions apps/rocmd/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6233,6 +6233,28 @@ mod tests {
);
}

/// The test above only ever hands `system_prefix_requires_ack` an
/// already-canonical path, so it cannot catch the bug this crate's fix
/// addresses: a `..`-respelled prefix that escapes `$HOME` used to compare
/// equal to a path still inside it (`Path::ancestors()` treats `..` as an
/// ordinary component), so acknowledgement was never required. Drive the
/// same check with a prefix built by walking `..` out of the real home
/// directory, which is exactly the shape the original bug let through.
#[test]
#[cfg(unix)]
fn install_sdk_rejects_system_prefix_reached_by_escaping_home() {
let home = rocm_core::runtime_home_dir().expect("a home directory");
let escaped_prefix = format!("{}/../../usr", home.display());

let arguments =
serde_json::Map::from_iter([("prefix".to_owned(), Value::String(escaped_prefix))]);
let error = build_install_sdk_args(&arguments, false).unwrap_err();
assert!(
error.to_string().contains("allow_system_prefix=true"),
"{error:#}"
);
}

/// The `install_sdk` MCP tool spawns `rocm` with null stdin, so a real
/// install over an active default managed runtime would hit the approval
/// gate's non-interactive refusal and bail asking for a flag no MCP caller
Expand Down
10 changes: 10 additions & 0 deletions crates/rocm-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,15 @@ sysinfo.workspace = true
toml = "0.8"
ureq = { version = "2.12", features = ["native-certs"] }

[dev-dependencies]
# Property-based tests for the path comparison that decides whether a folder
# may be recursively deleted. The contract — "a path that resolves inside a
# protected system root is never reported unprotected" — has to hold for EVERY
# spelling of that path, which is exactly the shape an example test misses: the
# pre-existing example used `/etc/rocm-cli-test-runtime`, the one shape that
# happened to work, while `/etc/` and `$HOME/../../etc` sailed through.
# Test-only, so it adds nothing to any shipped binary.
proptest = { version = "1", default-features = false, features = ["std"] }

[target.'cfg(target_os = "windows")'.dependencies]
windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_Security", "Win32_System_Registry", "Win32_System_SystemInformation", "Win32_System_Threading"] }
11 changes: 11 additions & 0 deletions crates/rocm-core/proptest-regressions/runtime.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Seeds for failure cases proptest has generated in the past. It is
# automatically read and these particular cases re-run before any
# novel cases are generated.
#
# It is recommended to check this file in to source control so that
# everyone who runs the test benefits from these saved cases.
cc 44838f606145dd05b27407447aa76a8a775e5a0d7a547bd898e4fb6488871799 # shrinks to text = "/bin/"
cc 5dc93bbc0c3b8a6fda0a1f14eba8364422f99adb63c0a4c3df90a55c903fd631 # shrinks to text = "/bin/"
cc a25d13c5221f176eaf8f3b0647babe58fe2984d7f0d3190b1a1b1fdc2e74d273 # shrinks to path = "/opt/", base = "/opt"
cc aa4f3b9ad94cb072d17df2d8c448929748d0a76fad6f04a0a76463f97ada9d60 # shrinks to text = "/bin/..\\tmp/.."
cc 00244038f28f97b7497323fe552197d8efe1e5006c07a42a7e8bbd83ba7f956a # shrinks to text = "/bin/..\\tmp"
Loading
Loading