@@ -1149,8 +1149,14 @@ const PROXY_BASELINE_READ_ONLY: &[&str] = &[
11491149] ;
11501150
11511151/// Minimum read-write paths required for a proxy-mode sandbox child process:
1152- /// user working directory and temporary files.
1153- const PROXY_BASELINE_READ_WRITE : & [ & str ] = & [ "/sandbox" , "/tmp" ] ;
1152+ /// user working directory, temporary files, and PTY devices.
1153+ ///
1154+ /// `/dev/ptmx` and `/dev/pts`: VS Code Remote-SSH launches its server under the
1155+ /// sandbox policy, and the server later allocates PTYs for the integrated
1156+ /// terminal via `node-pty`. Landlock blocks device-file opens unless they are
1157+ /// explicitly whitelisted, so PTY allocation fails with `EACCES` unless both
1158+ /// the PTY multiplexer and the slave PTY directory are writable.
1159+ const PROXY_BASELINE_READ_WRITE : & [ & str ] = & [ "/sandbox" , "/tmp" , "/dev/ptmx" , "/dev/pts" ] ;
11541160
11551161/// GPU read-only paths.
11561162///
@@ -1388,10 +1394,45 @@ mod baseline_tests {
13881394 }
13891395
13901396 #[ test]
1391- fn baseline_read_write_always_includes_sandbox_and_tmp ( ) {
1397+ fn baseline_read_write_includes_core_runtime_and_pty_paths ( ) {
13921398 let ( _ro, rw) = baseline_enrichment_paths ( ) ;
13931399 assert ! ( rw. contains( & "/sandbox" . to_string( ) ) ) ;
13941400 assert ! ( rw. contains( & "/tmp" . to_string( ) ) ) ;
1401+ assert ! ( rw. contains( & "/dev/ptmx" . to_string( ) ) ) ;
1402+ assert ! ( rw. contains( & "/dev/pts" . to_string( ) ) ) ;
1403+ }
1404+
1405+ #[ test]
1406+ fn enrich_proto_baseline_paths_adds_pty_paths_for_proxy_mode ( ) {
1407+ let mut policy = openshell_core:: proto:: SandboxPolicy :: default ( ) ;
1408+ policy. network_policies . insert (
1409+ "test" . to_string ( ) ,
1410+ openshell_core:: proto:: NetworkPolicyRule :: default ( ) ,
1411+ ) ;
1412+
1413+ let modified = enrich_proto_baseline_paths ( & mut policy) ;
1414+ assert ! ( modified, "proxy-mode policy should be enriched" ) ;
1415+
1416+ let fs = policy
1417+ . filesystem
1418+ . as_ref ( )
1419+ . expect ( "filesystem policy should be created during enrichment" ) ;
1420+ assert ! (
1421+ fs. read_write. iter( ) . any( |p| p == "/sandbox" ) ,
1422+ "proxy baseline should include /sandbox"
1423+ ) ;
1424+ assert ! (
1425+ fs. read_write. iter( ) . any( |p| p == "/tmp" ) ,
1426+ "proxy baseline should include /tmp"
1427+ ) ;
1428+ assert ! (
1429+ fs. read_write. iter( ) . any( |p| p == "/dev/ptmx" ) ,
1430+ "proxy baseline should include /dev/ptmx"
1431+ ) ;
1432+ assert ! (
1433+ fs. read_write. iter( ) . any( |p| p == "/dev/pts" ) ,
1434+ "proxy baseline should include /dev/pts"
1435+ ) ;
13951436 }
13961437
13971438 #[ test]
@@ -1406,6 +1447,15 @@ mod baseline_tests {
14061447 ) ;
14071448 }
14081449
1450+ #[ test]
1451+ fn runtime_device_paths_are_not_prepared_for_chown ( ) {
1452+ assert ! ( is_runtime_device_path( std:: path:: Path :: new( "/dev/ptmx" ) ) ) ;
1453+ assert ! ( is_runtime_device_path( std:: path:: Path :: new( "/dev/pts" ) ) ) ;
1454+ assert ! ( is_runtime_device_path( std:: path:: Path :: new( "/proc" ) ) ) ;
1455+ assert ! ( !is_runtime_device_path( std:: path:: Path :: new( "/sandbox" ) ) ) ;
1456+ assert ! ( !is_runtime_device_path( std:: path:: Path :: new( "/tmp" ) ) ) ;
1457+ }
1458+
14091459 #[ test]
14101460 fn no_duplicate_paths_in_baseline ( ) {
14111461 let ( ro, rw) = baseline_enrichment_paths ( ) ;
@@ -1750,11 +1800,25 @@ fn prepare_filesystem(policy: &SandboxPolicy) -> Result<()> {
17501800 // (e.g. /dev/null) are legitimate read_write entries and must be allowed.
17511801 if let Ok ( meta) = std:: fs:: symlink_metadata ( path) {
17521802 if meta. file_type ( ) . is_symlink ( ) {
1803+ if is_runtime_device_path ( path) {
1804+ debug ! (
1805+ path = %path. display( ) ,
1806+ "Skipping ownership change on runtime device symlink"
1807+ ) ;
1808+ continue ;
1809+ }
17531810 return Err ( miette:: miette!(
17541811 "read_write path '{}' is a symlink — refusing to chown (potential privilege escalation)" ,
17551812 path. display( )
17561813 ) ) ;
17571814 }
1815+ if is_runtime_device_path ( path) {
1816+ debug ! (
1817+ path = %path. display( ) ,
1818+ "Skipping ownership change on runtime device path"
1819+ ) ;
1820+ continue ;
1821+ }
17581822 } else {
17591823 debug ! ( path = %path. display( ) , "Creating read_write directory" ) ;
17601824 std:: fs:: create_dir_all ( path) . into_diagnostic ( ) ?;
@@ -1767,6 +1831,13 @@ fn prepare_filesystem(policy: &SandboxPolicy) -> Result<()> {
17671831 Ok ( ( ) )
17681832}
17691833
1834+ #[ cfg( unix) ]
1835+ fn is_runtime_device_path ( path : & std:: path:: Path ) -> bool {
1836+ path. starts_with ( std:: path:: Path :: new ( "/dev" ) )
1837+ || path. starts_with ( std:: path:: Path :: new ( "/proc" ) )
1838+ || path. starts_with ( std:: path:: Path :: new ( "/sys" ) )
1839+ }
1840+
17701841#[ cfg( not( unix) ) ]
17711842fn prepare_filesystem ( _policy : & SandboxPolicy ) -> Result < ( ) > {
17721843 Ok ( ( ) )
0 commit comments