@@ -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