Skip to content

Commit 442b5f3

Browse files
committed
feat(policy): accept numeric UIDs in sandbox process identity validation
Allow run_as_user and run_as_group to be either the literal 'sandbox' or a numeric UID/GID within [1000, 2_000_000_000]. This removes the hard dependency on a baked-in 'sandbox' user in container images, enabling compute drivers to inject resolved UIDs at sandbox creation. Phase 1 of #1959. Signed-off-by: Seth Jennings <sjenning@redhat.com>
1 parent 45060f4 commit 442b5f3

1 file changed

Lines changed: 207 additions & 5 deletions

File tree

  • crates/openshell-policy/src

crates/openshell-policy/src/lib.rs

Lines changed: 207 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -917,6 +917,41 @@ fn from_proto(policy: &SandboxPolicy) -> PolicyFile {
917917
}
918918
}
919919

920+
// ---------------------------------------------------------------------------
921+
// Sandbox UID/GID constants
922+
// ---------------------------------------------------------------------------
923+
924+
/// Minimum accepted UID for sandbox process identity.
925+
/// UIDs below this are reserved for system users and are rejected.
926+
pub const MIN_SANDBOX_UID: u32 = 1000;
927+
928+
/// Maximum accepted UID for sandbox process identity.
929+
/// UIDs above this exceed typical OS limits and are rejected.
930+
pub const MAX_SANDBOX_UID: u32 = 2_000_000_000;
931+
932+
/// The literal string value accepted as a valid sandbox user/group name.
933+
const SANDBOX_NAME: &str = "sandbox";
934+
935+
/// Validate whether a process identity field value is acceptable.
936+
///
937+
/// Accepts either the literal `"sandbox"` or a numeric UID/GID parsed as
938+
/// `u32` within the range `[MIN_SANDBOX_UID, MAX_SANDBOX_UID]`.
939+
///
940+
/// Rejects:
941+
/// - The empty string (callers should use `ensure_sandbox_process_identity`
942+
/// to fill defaults before validation)
943+
/// - UID 0 or values below `MIN_SANDBOX_UID`
944+
/// - Values above `MAX_SANDBOX_UID`
945+
/// - Non-numeric strings other than `"sandbox"` (e.g. `"root"`, `"nobody"`)
946+
pub fn is_valid_sandbox_identity(value: &str) -> bool {
947+
if value == SANDBOX_NAME {
948+
return true;
949+
}
950+
value.parse::<u32>().is_ok_and(|uid| {
951+
(MIN_SANDBOX_UID..=MAX_SANDBOX_UID).contains(&uid)
952+
})
953+
}
954+
920955
// ---------------------------------------------------------------------------
921956
// Public API
922957
// ---------------------------------------------------------------------------
@@ -1090,7 +1125,10 @@ impl fmt::Display for PolicyViolation {
10901125
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10911126
match self {
10921127
Self::InvalidProcessIdentity { field, value } => {
1093-
write!(f, "{field} must be 'sandbox', got '{value}'")
1128+
write!(
1129+
f,
1130+
"{field} must be 'sandbox' or a numeric UID/GID in range [{MIN_SANDBOX_UID}, {MAX_SANDBOX_UID}], got '{value}'"
1131+
)
10941132
}
10951133
Self::PathTraversal { path } => {
10961134
write!(f, "path contains '..' traversal component: {path}")
@@ -1168,17 +1206,18 @@ pub fn validate_sandbox_policy(
11681206
) -> std::result::Result<(), Vec<PolicyViolation>> {
11691207
let mut violations = Vec::new();
11701208

1171-
// Check process identity — must be "sandbox".
1209+
// Check process identity — must be "sandbox" or a numeric UID/GID
1210+
// within the acceptable sandbox range.
11721211
// `ensure_sandbox_process_identity` should be called before this to
1173-
// fill in defaults; anything other than "sandbox" is rejected.
1212+
// fill in defaults; any invalid value is rejected.
11741213
if let Some(ref process) = policy.process {
1175-
if process.run_as_user != "sandbox" {
1214+
if !is_valid_sandbox_identity(&process.run_as_user) {
11761215
violations.push(PolicyViolation::InvalidProcessIdentity {
11771216
field: "run_as_user",
11781217
value: process.run_as_user.clone(),
11791218
});
11801219
}
1181-
if process.run_as_group != "sandbox" {
1220+
if !is_valid_sandbox_identity(&process.run_as_group) {
11821221
violations.push(PolicyViolation::InvalidProcessIdentity {
11831222
field: "run_as_group",
11841223
value: process.run_as_group.clone(),
@@ -2031,6 +2070,169 @@ network_policies:
20312070
assert!(s.contains("sandbox"));
20322071
}
20332072

2073+
// ---- is_valid_sandbox_identity tests ----
2074+
2075+
#[test]
2076+
fn valid_identity_accepts_sandbox() {
2077+
assert!(is_valid_sandbox_identity("sandbox"));
2078+
}
2079+
2080+
#[test]
2081+
fn valid_identity_accepts_numeric_uid_in_range() {
2082+
assert!(is_valid_sandbox_identity("1000"));
2083+
assert!(is_valid_sandbox_identity("50000"));
2084+
assert!(is_valid_sandbox_identity("1000660000"));
2085+
}
2086+
2087+
#[test]
2088+
fn valid_identity_accepts_boundary_uids() {
2089+
assert!(is_valid_sandbox_identity(&MIN_SANDBOX_UID.to_string()));
2090+
assert!(is_valid_sandbox_identity(&MAX_SANDBOX_UID.to_string()));
2091+
}
2092+
2093+
#[test]
2094+
fn valid_identity_rejects_zero() {
2095+
assert!(!is_valid_sandbox_identity("0"));
2096+
}
2097+
2098+
#[test]
2099+
fn valid_identity_rejects_system_uids_below_min() {
2100+
assert!(!is_valid_sandbox_identity("999"));
2101+
assert!(!is_valid_sandbox_identity("100"));
2102+
assert!(!is_valid_sandbox_identity("1"));
2103+
}
2104+
2105+
#[test]
2106+
fn valid_identity_rejects_uid_above_max() {
2107+
assert!(!is_valid_sandbox_identity(&MAX_SANDBOX_UID.saturating_add(1).to_string()));
2108+
}
2109+
2110+
#[test]
2111+
fn valid_identity_rejects_non_numeric_names() {
2112+
assert!(!is_valid_sandbox_identity("root"));
2113+
assert!(!is_valid_sandbox_identity("nobody"));
2114+
assert!(!is_valid_sandbox_identity("user"));
2115+
}
2116+
2117+
#[test]
2118+
fn valid_identity_rejects_empty_string() {
2119+
assert!(!is_valid_sandbox_identity(""));
2120+
}
2121+
2122+
// ---- Policy validation with numeric UIDs ----
2123+
2124+
#[test]
2125+
fn validate_accepts_numeric_uid_in_range() {
2126+
let policy = SandboxPolicy {
2127+
version: 1,
2128+
process: Some(ProcessPolicy {
2129+
run_as_user: "1000".into(),
2130+
run_as_group: "5000".into(),
2131+
}),
2132+
filesystem: None,
2133+
landlock: None,
2134+
network_policies: HashMap::new(),
2135+
};
2136+
assert!(validate_sandbox_policy(&policy).is_ok());
2137+
}
2138+
2139+
#[test]
2140+
fn validate_accepts_boundary_uids() {
2141+
let policy = SandboxPolicy {
2142+
version: 1,
2143+
process: Some(ProcessPolicy {
2144+
run_as_user: MIN_SANDBOX_UID.to_string(),
2145+
run_as_group: MAX_SANDBOX_UID.to_string(),
2146+
}),
2147+
filesystem: None,
2148+
landlock: None,
2149+
network_policies: HashMap::new(),
2150+
};
2151+
assert!(validate_sandbox_policy(&policy).is_ok());
2152+
}
2153+
2154+
#[test]
2155+
fn validate_rejects_uid_out_of_range_low() {
2156+
let mut policy = restrictive_default_policy();
2157+
policy.process = Some(ProcessPolicy {
2158+
run_as_user: "500".into(),
2159+
run_as_group: "sandbox".into(),
2160+
});
2161+
let violations = validate_sandbox_policy(&policy).unwrap_err();
2162+
assert!(violations.iter().any(|v| matches!(
2163+
v,
2164+
PolicyViolation::InvalidProcessIdentity { field: "run_as_user", .. }
2165+
)));
2166+
}
2167+
2168+
#[test]
2169+
fn validate_rejects_uid_out_of_range_high() {
2170+
let mut policy = restrictive_default_policy();
2171+
policy.process = Some(ProcessPolicy {
2172+
run_as_user: (MAX_SANDBOX_UID + 1).to_string(),
2173+
run_as_group: "sandbox".into(),
2174+
});
2175+
let violations = validate_sandbox_policy(&policy).unwrap_err();
2176+
assert!(violations.iter().any(|v| matches!(
2177+
v,
2178+
PolicyViolation::InvalidProcessIdentity { field: "run_as_user", .. }
2179+
)));
2180+
}
2181+
2182+
#[test]
2183+
fn validate_rejects_root_string() {
2184+
let mut policy = restrictive_default_policy();
2185+
policy.process = Some(ProcessPolicy {
2186+
run_as_user: "root".into(),
2187+
run_as_group: "sandbox".into(),
2188+
});
2189+
let violations = validate_sandbox_policy(&policy).unwrap_err();
2190+
assert!(violations.iter().any(|v| matches!(
2191+
v,
2192+
PolicyViolation::InvalidProcessIdentity { field: "run_as_user", .. }
2193+
)));
2194+
}
2195+
2196+
#[test]
2197+
fn validate_rejects_nobody_string() {
2198+
let mut policy = restrictive_default_policy();
2199+
policy.process = Some(ProcessPolicy {
2200+
run_as_user: "nobody".into(),
2201+
run_as_group: "nogroup".into(),
2202+
});
2203+
let violations = validate_sandbox_policy(&policy).unwrap_err();
2204+
assert_eq!(violations.len(), 2);
2205+
}
2206+
2207+
#[test]
2208+
fn validate_accepts_mixed_sandbox_name_and_uid() {
2209+
// run_as_user as "sandbox" name, run_as_group as numeric UID
2210+
let policy = SandboxPolicy {
2211+
version: 1,
2212+
process: Some(ProcessPolicy {
2213+
run_as_user: "sandbox".into(),
2214+
run_as_group: "1000".into(),
2215+
}),
2216+
filesystem: None,
2217+
landlock: None,
2218+
network_policies: HashMap::new(),
2219+
};
2220+
assert!(validate_sandbox_policy(&policy).is_ok());
2221+
}
2222+
2223+
#[test]
2224+
fn policy_violation_display_includes_range() {
2225+
let v = PolicyViolation::InvalidProcessIdentity {
2226+
field: "run_as_user",
2227+
value: "root".into(),
2228+
};
2229+
let s = format!("{v}");
2230+
assert!(s.contains("sandbox"));
2231+
assert!(s.contains(&MIN_SANDBOX_UID.to_string()));
2232+
assert!(s.contains(&MAX_SANDBOX_UID.to_string()));
2233+
assert!(s.contains("root"));
2234+
}
2235+
20342236
// ---- Multi-port and host wildcard tests ----
20352237

20362238
#[test]

0 commit comments

Comments
 (0)