@@ -327,13 +327,57 @@ async def _exec_internal(
327327 if cmd == ["test" , "-x" , helper_path ]:
328328 return ExecResult (stdout = b"" , stderr = b"" , exit_code = 0 )
329329 if cmd and cmd [0 ] == helper_path :
330+ for_write = cmd [3 ]
330331 candidate = self ._host_path (cmd [2 ]).resolve (strict = False )
331- roots = [self ._host_path (root ).resolve (strict = False ) for root in [cmd [1 ], * cmd [3 :]]]
332- for root in roots :
332+ workspace_root = self ._host_path (cmd [1 ]).resolve (strict = False )
333+ try :
334+ candidate .relative_to (workspace_root )
335+ except ValueError :
336+ pass
337+ else :
338+ return ExecResult (
339+ stdout = str (self ._container_path (candidate )).encode ("utf-8" ),
340+ stderr = b"" ,
341+ exit_code = 0 ,
342+ )
343+
344+ best_root : Path | None = None
345+ best_original = ""
346+ best_read_only = False
347+ grant_args = cmd [4 :]
348+ assert len (grant_args ) % 2 == 0
349+ for original_root , read_only_text in zip (
350+ grant_args [::2 ],
351+ grant_args [1 ::2 ],
352+ strict = False ,
353+ ):
354+ root = self ._host_path (original_root ).resolve (strict = False )
355+ if root == root .parent :
356+ return ExecResult (
357+ stdout = b"" ,
358+ stderr = (
359+ f"extra path grant must not resolve to filesystem root: { original_root } "
360+ ).encode (),
361+ exit_code = 113 ,
362+ )
333363 try :
334364 candidate .relative_to (root )
335365 except ValueError :
336366 continue
367+ if best_root is None or len (root .parts ) > len (best_root .parts ):
368+ best_root = root
369+ best_original = original_root
370+ best_read_only = read_only_text == "1"
371+ if best_root is not None :
372+ if for_write == "1" and best_read_only :
373+ return ExecResult (
374+ stdout = b"" ,
375+ stderr = (
376+ f"read-only extra path grant: { best_original } \n "
377+ f"resolved path: { self ._container_path (candidate )} \n "
378+ ).encode (),
379+ exit_code = 114 ,
380+ )
337381 return ExecResult (
338382 stdout = str (self ._container_path (candidate )).encode ("utf-8" ),
339383 stderr = b"" ,
@@ -1006,14 +1050,56 @@ async def test_docker_write_rejects_workspace_symlink_to_read_only_extra_path_gr
10061050 ),
10071051 )
10081052
1009- with pytest .raises (InvalidManifestPathError ) as exc_info :
1053+ with pytest .raises (WorkspaceArchiveWriteError ) as exc_info :
10101054 await session .write (Path ("tmp-link/result.txt" ), io .BytesIO (b"scratch output" ))
10111055
1012- assert str (exc_info .value ) == "manifest path must not escape root: tmp-link/result.txt"
1056+ assert str (exc_info .value ) == "failed to write archive for path: /workspace/tmp-link/result.txt"
1057+ assert exc_info .value .context == {
1058+ "path" : "/workspace/tmp-link/result.txt" ,
1059+ "reason" : "read_only_extra_path_grant" ,
1060+ "grant_path" : "/tmp" ,
1061+ "resolved_path" : "/tmp/result.txt" ,
1062+ }
1063+
1064+
1065+ @pytest .mark .asyncio
1066+ async def test_docker_write_rejects_workspace_symlink_to_nested_read_only_extra_path_grant (
1067+ tmp_path : Path ,
1068+ ) -> None :
1069+ host_root = tmp_path / "container"
1070+ workspace = host_root / "workspace"
1071+ extra_root = host_root / "tmp"
1072+ protected_root = extra_root / "protected"
1073+ workspace .mkdir (parents = True )
1074+ protected_root .mkdir (parents = True )
1075+ (workspace / "tmp-link" ).symlink_to (extra_root , target_is_directory = True )
1076+
1077+ session = _HostBackedDockerSession (
1078+ host_root = host_root ,
1079+ manifest = Manifest (
1080+ root = "/workspace" ,
1081+ extra_path_grants = (
1082+ SandboxPathGrant (path = "/tmp" ),
1083+ SandboxPathGrant (path = "/tmp/protected" , read_only = True ),
1084+ ),
1085+ ),
1086+ )
1087+
1088+ with pytest .raises (WorkspaceArchiveWriteError ) as exc_info :
1089+ await session .write (
1090+ Path ("tmp-link/protected/result.txt" ),
1091+ io .BytesIO (b"scratch output" ),
1092+ )
1093+
1094+ assert (
1095+ str (exc_info .value )
1096+ == "failed to write archive for path: /workspace/tmp-link/protected/result.txt"
1097+ )
10131098 assert exc_info .value .context == {
1014- "rel" : "tmp-link/result.txt" ,
1015- "reason" : "escape_root" ,
1016- "resolved_path" : "workspace escape" ,
1099+ "path" : "/workspace/tmp-link/protected/result.txt" ,
1100+ "reason" : "read_only_extra_path_grant" ,
1101+ "grant_path" : "/tmp/protected" ,
1102+ "resolved_path" : "/tmp/protected/result.txt" ,
10171103 }
10181104
10191105
0 commit comments