@@ -32,6 +32,46 @@ const RESERVED_MOUNT_TARGETS: &[&str] = &[
3232 "/run/netns" ,
3333] ;
3434
35+ /// Container filesystem trees that image metadata must never turn into an
36+ /// agent-owned writable workspace.
37+ const PROTECTED_WORKSPACE_TREES : & [ & str ] = & [
38+ "/bin" ,
39+ "/boot" ,
40+ "/dev" ,
41+ "/etc" ,
42+ "/lib" ,
43+ "/lib64" ,
44+ "/proc" ,
45+ "/root" ,
46+ "/run" ,
47+ "/sbin" ,
48+ "/sys" ,
49+ "/usr/bin" ,
50+ "/usr/lib" ,
51+ "/usr/lib64" ,
52+ "/usr/local/bin" ,
53+ "/usr/local/lib" ,
54+ "/usr/local/lib64" ,
55+ "/usr/local/sbin" ,
56+ "/usr/local/share" ,
57+ "/usr/sbin" ,
58+ "/usr/share" ,
59+ "/var/log" ,
60+ ] ;
61+
62+ /// Broad container roots that are unsafe as workspaces themselves, while
63+ /// application-specific descendants remain valid.
64+ const PROTECTED_WORKSPACE_ROOTS : & [ & str ] = & [
65+ "/home" ,
66+ "/mnt" ,
67+ "/opt" ,
68+ "/srv" ,
69+ "/usr" ,
70+ "/usr/local" ,
71+ "/var" ,
72+ "/var/lib" ,
73+ ] ;
74+
3575/// Compatibility workspace used when an OCI image has no usable working
3676/// directory and by drivers whose workspace remains fixed.
3777pub const DEFAULT_WORKSPACE_ROOT : & str = "/sandbox" ;
@@ -139,8 +179,11 @@ pub fn resolve_oci_workspace_root(working_dir: &str) -> Result<String, String> {
139179 if working_dir. is_empty ( ) || working_dir == "/" {
140180 return Ok ( DEFAULT_WORKSPACE_ROOT . to_string ( ) ) ;
141181 }
142- if working_dir. as_bytes ( ) . contains ( & 0 ) {
143- return Err ( "OCI WorkingDir must not contain NUL bytes" . to_string ( ) ) ;
182+ if working_dir != working_dir. trim ( ) {
183+ return Err ( "OCI WorkingDir must not contain surrounding whitespace" . to_string ( ) ) ;
184+ }
185+ if working_dir. chars ( ) . any ( char:: is_control) {
186+ return Err ( "OCI WorkingDir must not contain control characters" . to_string ( ) ) ;
144187 }
145188 if !working_dir. starts_with ( '/' ) {
146189 return Err ( format ! (
@@ -159,13 +202,32 @@ pub fn resolve_oci_workspace_root(working_dir: &str) -> Result<String, String> {
159202 ) ) ;
160203 }
161204
162- Ok ( working_dir. trim_end_matches ( '/' ) . to_string ( ) )
205+ let workspace_root = working_dir. trim_end_matches ( '/' ) . to_string ( ) ;
206+ let workspace_path = Path :: new ( & workspace_root) ;
207+ if PROTECTED_WORKSPACE_ROOTS
208+ . iter ( )
209+ . any ( |protected| workspace_path == Path :: new ( protected) )
210+ || PROTECTED_WORKSPACE_TREES
211+ . iter ( )
212+ . any ( |protected| path_is_or_under ( workspace_path, Path :: new ( protected) ) )
213+ || RESERVED_MOUNT_TARGETS . iter ( ) . any ( |reserved| {
214+ let reserved = Path :: new ( reserved) ;
215+ path_is_or_under ( workspace_path, reserved) || path_is_or_under ( reserved, workspace_path)
216+ } )
217+ {
218+ return Err ( format ! (
219+ "OCI WorkingDir '{working_dir}' conflicts with a protected container path"
220+ ) ) ;
221+ }
222+
223+ Ok ( workspace_root)
163224}
164225
165- /// Reject a user-supplied mount that would replace the resolved workspace
166- /// root. Mounts below the workspace remain valid.
226+ /// Reject a user-supplied mount that would replace or contain the resolved
227+ /// workspace root. Mounts below the workspace remain valid.
167228pub fn validate_workspace_mount_target ( target : & str , workspace_root : & str ) -> Result < ( ) , String > {
168- if normalize_mount_target ( target) == workspace_root {
229+ let normalized_target = normalize_mount_target ( target) ;
230+ if path_is_or_under ( Path :: new ( workspace_root) , Path :: new ( & normalized_target) ) {
169231 return Err ( format ! (
170232 "mount target '{target}' is reserved for the OpenShell workspace"
171233 ) ) ;
@@ -202,6 +264,8 @@ mod tests {
202264 validate_workspace_mount_target ( "/sandbox/" , "/sandbox" ) . unwrap_err ( ) ;
203265 validate_workspace_mount_target ( "/workspace/" , "/sandbox" ) . unwrap ( ) ;
204266 validate_workspace_mount_target ( "/workspace/cache" , "/workspace" ) . unwrap ( ) ;
267+ validate_workspace_mount_target ( "/workspace" , "/workspace/project" ) . unwrap_err ( ) ;
268+ validate_workspace_mount_target ( "/workspace-other" , "/workspace/project" ) . unwrap ( ) ;
205269 }
206270
207271 #[ test]
@@ -227,6 +291,8 @@ mod tests {
227291 "/workspace/./project" ,
228292 "/workspace//project" ,
229293 "/workspace\0 project" ,
294+ "/workspace " ,
295+ "/workspace\n project" ,
230296 ] {
231297 assert ! (
232298 resolve_oci_workspace_root( invalid) . is_err( ) ,
@@ -235,6 +301,39 @@ mod tests {
235301 }
236302 }
237303
304+ #[ test]
305+ fn oci_workspace_root_rejects_protected_container_paths ( ) {
306+ for invalid in [
307+ "/etc" ,
308+ "/etc/project" ,
309+ "/usr" ,
310+ "/usr/bin/project" ,
311+ "/var" ,
312+ "/var/log/project" ,
313+ "/opt" ,
314+ "/opt/openshell/project" ,
315+ ] {
316+ assert ! (
317+ resolve_oci_workspace_root( invalid) . is_err( ) ,
318+ "expected protected workspace '{invalid}' to be rejected"
319+ ) ;
320+ }
321+
322+ for valid in [
323+ "/app" ,
324+ "/home/app" ,
325+ "/opt/app" ,
326+ "/usr/src/app" ,
327+ "/var/lib/app" ,
328+ ] {
329+ assert_eq ! (
330+ resolve_oci_workspace_root( valid) . unwrap( ) ,
331+ valid,
332+ "expected application workspace '{valid}' to remain valid"
333+ ) ;
334+ }
335+ }
336+
238337 #[ test]
239338 fn container_target_rejects_reserved_openshell_tls_legacy_path ( ) {
240339 let err = validate_container_mount_target ( "/etc/openshell-tls/client" ) . unwrap_err ( ) ;
0 commit comments