From fe0cf30baf80622732f134712ee80fad9c3ad22b Mon Sep 17 00:00:00 2001 From: Andrew White Date: Thu, 23 Jul 2026 22:26:29 -0500 Subject: [PATCH 1/5] fix(supervisor-process): use slice patterns when rewriting passwd/group lines rewrite_passwd_at and rewrite_group_at indexed into a Vec of fields with fields[N] after checking fields.len(). Use slice patterns so a malformed line falls through to the no-op branch instead of risking a panic if the guard is ever changed. Add regression tests for malformed sandbox entries. Signed-off-by: Andrew White --- .../src/process.rs | 35 +++++++++++++++---- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/crates/openshell-supervisor-process/src/process.rs b/crates/openshell-supervisor-process/src/process.rs index 322dcadd86..3733c7c7e3 100644 --- a/crates/openshell-supervisor-process/src/process.rs +++ b/crates/openshell-supervisor-process/src/process.rs @@ -1101,11 +1101,8 @@ fn rewrite_passwd_at(path: &Path, uid: &str, gid: &str) -> Result<()> { if line.starts_with("sandbox:") { found = true; let fields: Vec<&str> = line.split(':').collect(); - if fields.len() >= 7 { - format!( - "{}:{}:{}:{}:{}:{}:{}", - fields[0], fields[1], uid, gid, fields[4], fields[5], fields[6] - ) + if let [name, pass, _, _, gecos, home, shell, ..] = fields.as_slice() { + format!("{name}:{pass}:{uid}:{gid}:{gecos}:{home}:{shell}") } else { line.to_string() } @@ -1139,8 +1136,8 @@ fn rewrite_group_at(path: &Path, gid: &str) -> Result<()> { if line.starts_with("sandbox:") { found = true; let fields: Vec<&str> = line.split(':').collect(); - if fields.len() >= 4 { - format!("{}:{}:{}:{}", fields[0], fields[1], gid, fields[3]) + if let [name, pass, _, members, ..] = fields.as_slice() { + format!("{name}:{pass}:{gid}:{members}") } else { line.to_string() } @@ -2302,6 +2299,30 @@ mod tests { assert!(content.contains("sandbox:x:6000:")); } + #[cfg(unix)] + #[test] + fn rewrite_passwd_leaves_malformed_entry_unchanged() { + let dir = tempfile::tempdir().unwrap(); + let passwd = dir.path().join("passwd"); + // Only 3 fields — slice pattern should fall through instead of panic. + std::fs::write(&passwd, "sandbox:x:1000\n").unwrap(); + rewrite_passwd_at(&passwd, "5000", "6000").unwrap(); + let content = std::fs::read_to_string(&passwd).unwrap(); + assert!(content.contains("sandbox:x:1000")); + } + + #[cfg(unix)] + #[test] + fn rewrite_group_leaves_malformed_entry_unchanged() { + let dir = tempfile::tempdir().unwrap(); + let group = dir.path().join("group"); + // Only 2 fields — slice pattern should fall through instead of panic. + std::fs::write(&group, "sandbox:x\n").unwrap(); + rewrite_group_at(&group, "6000").unwrap(); + let content = std::fs::read_to_string(&group).unwrap(); + assert!(content.contains("sandbox:x")); + } + #[cfg(unix)] #[test] fn rewrite_passwd_preserves_other_entries() { From 69fc420aa2915ff55e1cbf5343d8c4eec537f3f3 Mon Sep 17 00:00:00 2001 From: Andrew White Date: Thu, 23 Jul 2026 22:26:29 -0500 Subject: [PATCH 2/5] fix(bootstrap): avoid byte-index slice after @ in SSH destination extract_host_from_ssh_destination sliced dest[at_pos + 1..] after finding '@'. '@' is ASCII so this is currently safe, but it is a latent panic surface if the split logic changes. Use get() instead and add a multi-byte hostname test. Signed-off-by: Andrew White --- crates/openshell-bootstrap/src/metadata.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/openshell-bootstrap/src/metadata.rs b/crates/openshell-bootstrap/src/metadata.rs index fe4c277bd2..8190fac829 100644 --- a/crates/openshell-bootstrap/src/metadata.rs +++ b/crates/openshell-bootstrap/src/metadata.rs @@ -142,7 +142,8 @@ pub fn extract_host_from_ssh_destination(destination: &str) -> String { // Handle user@host format dest.find('@') - .map_or_else(|| dest.to_string(), |at_pos| dest[at_pos + 1..].to_string()) + .and_then(|at_pos| dest.get(at_pos + 1..)) + .map_or_else(|| dest.to_string(), ToString::to_string) } /// Resolve an SSH host alias to the actual hostname or IP address. @@ -457,6 +458,11 @@ mod tests { ); } + #[test] + fn extract_host_user_at_multi_byte_hostname() { + assert_eq!(extract_host_from_ssh_destination("user@mülti"), "mülti"); + } + #[test] fn metadata_roundtrip() { let meta = GatewayMetadata { From 63766d65e66243c8f0b29f31df0ed9495c7ff92f Mon Sep 17 00:00:00 2001 From: Andrew White Date: Thu, 23 Jul 2026 22:26:29 -0500 Subject: [PATCH 3/5] fix(bootstrap): avoid byte-index slice in .dockerignore glob matcher glob_match sliced path[idx + 1..] after matching '/'. '/' is ASCII so this is currently safe, but it is a latent UTF-8 panic surface. Use get() instead. Signed-off-by: Andrew White --- crates/openshell-bootstrap/src/build.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/openshell-bootstrap/src/build.rs b/crates/openshell-bootstrap/src/build.rs index efa040c34a..f0e03fcd89 100644 --- a/crates/openshell-bootstrap/src/build.rs +++ b/crates/openshell-bootstrap/src/build.rs @@ -398,7 +398,9 @@ fn glob_match(pattern: &str, path: &str, is_dir: bool) -> bool { return true; } for (idx, _) in path.match_indices('/') { - if glob_match(rest, &path[idx + 1..], is_dir) { + if let Some(suffix) = path.get(idx + 1..) + && glob_match(rest, suffix, is_dir) + { return true; } } @@ -592,6 +594,7 @@ mod tests { assert!(glob_match("**/*.log", "a/b/c.log", false)); assert!(glob_match("**/*.log", "c.log", false)); assert!(!glob_match("**/*.log", "c.txt", false)); + assert!(glob_match("**/föö.log", "a/b/föö.log", false)); } #[test] From fcd87efab04a3ea00e715f7bd5d348fdbf494e45 Mon Sep 17 00:00:00 2001 From: Andrew White Date: Thu, 23 Jul 2026 22:26:29 -0500 Subject: [PATCH 4/5] fix(router): avoid literal byte offset when stripping /v1 prefix build_backend_url sliced &path[3..] after verifying the /v1 prefix. Use strip_prefix() so the code stays correct if the prefix length ever changes. Signed-off-by: Andrew White --- crates/openshell-router/src/backend.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/openshell-router/src/backend.rs b/crates/openshell-router/src/backend.rs index 9fbd538c06..be908055ee 100644 --- a/crates/openshell-router/src/backend.rs +++ b/crates/openshell-router/src/backend.rs @@ -824,8 +824,12 @@ fn build_backend_url(endpoint: &str, path: &str) -> String { let base_path = &base[path_start..]; base_path.starts_with("/v1/") || base_path.ends_with("/v1") }); - if base_path_has_v1_edge_segment && (path == "/v1" || path.starts_with("/v1/")) { - return format!("{base}{}", &path[3..]); + if base_path_has_v1_edge_segment + && let Some(rest) = path + .strip_prefix("/v1") + .filter(|rest| rest.is_empty() || rest.starts_with('/')) + { + return format!("{base}{rest}"); } format!("{base}{path}") From 7326f0fbf70c11e4095ab73aa4f75bc496110d00 Mon Sep 17 00:00:00 2001 From: Andrew White Date: Thu, 23 Jul 2026 22:26:29 -0500 Subject: [PATCH 5/5] fix(supervisor-network): avoid byte-index slice in inference path matching The Bedrock-style /*/ path matcher sliced rest[slash_at + 1..] after finding '/'. '/' is ASCII so this is currently safe, but it is a latent UTF-8 panic surface. Use get() instead. Signed-off-by: Andrew White --- crates/openshell-supervisor-network/src/l7/inference.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/openshell-supervisor-network/src/l7/inference.rs b/crates/openshell-supervisor-network/src/l7/inference.rs index e06dffd324..5aefab9e93 100644 --- a/crates/openshell-supervisor-network/src/l7/inference.rs +++ b/crates/openshell-supervisor-network/src/l7/inference.rs @@ -179,7 +179,10 @@ pub fn detect_inference_pattern<'a>( let Some(slash_at) = rest.find('/') else { return false; }; - return slash_at > 0 && rest[slash_at + 1..] == *after; + let Some(after_segment) = rest.get(slash_at + 1..) else { + return false; + }; + return slash_at > 0 && after_segment == after; } path_only == p.path_glob