@@ -414,16 +414,46 @@ impl FsBackend for RealFs {
414414 Ok ( ( ) )
415415 }
416416
417- /// THREAT[TM-ESC-003]: Symlink creation is blocked in RealFs to prevent
418- /// sandbox escape. Even though bashkit itself doesn't follow symlinks
419- /// (TM-ESC-002), any external process sharing the directory tree would
420- /// follow them, enabling reads/writes to arbitrary host paths.
421- async fn symlink ( & self , _target : & Path , _link : & Path ) -> Result < ( ) > {
422- Err ( IoError :: new (
423- ErrorKind :: PermissionDenied ,
424- "symlink creation is not allowed in RealFs (sandbox security)" ,
425- )
426- . into ( ) )
417+ /// THREAT[TM-ESC-003]: Symlink creation in RealFs is allowed only in
418+ /// ReadWrite mode. The OS resolves symlink targets on the host filesystem,
419+ /// so we must validate that the effective target stays within the mount
420+ /// root on disk. Absolute targets are rejected. Relative targets are
421+ /// normalized against the link's host-side parent directory.
422+ async fn symlink ( & self , target : & Path , link : & Path ) -> Result < ( ) > {
423+ self . check_writable ( ) ?;
424+ let real_link = self . resolve ( link) ?;
425+
426+ // Absolute targets always escape the mount root on disk
427+ if target. is_absolute ( ) {
428+ return Err ( IoError :: new (
429+ ErrorKind :: PermissionDenied ,
430+ "symlink with absolute target not allowed in RealFs (sandbox security)" ,
431+ )
432+ . into ( ) ) ;
433+ }
434+
435+ // Relative targets: resolve against the link's host-side parent
436+ // to verify the effective path stays within root
437+ let link_parent = real_link. parent ( ) . unwrap_or ( & self . root ) ;
438+ let effective = normalize_host_path ( & link_parent. join ( target) ) ;
439+ if !effective. starts_with ( & self . root ) {
440+ return Err ( IoError :: new (
441+ ErrorKind :: PermissionDenied ,
442+ "symlink target escapes realfs root (sandbox security)" ,
443+ )
444+ . into ( ) ) ;
445+ }
446+
447+ #[ cfg( unix) ]
448+ {
449+ tokio:: fs:: symlink ( target, & real_link) . await ?;
450+ }
451+ #[ cfg( not( unix) ) ]
452+ {
453+ let _ = target;
454+ tokio:: fs:: write ( & real_link, "" ) . await ?;
455+ }
456+ Ok ( ( ) )
427457 }
428458
429459 async fn read_link ( & self , path : & Path ) -> Result < PathBuf > {
0 commit comments