@@ -755,4 +755,114 @@ describe('allowWrite glob suffix handling', () => {
755755 rmSync ( parentDir , { recursive : true , force : true } )
756756 }
757757 } )
758+
759+ // Regression: #190 reordered denyWrite after denyRead so .git/hooks ro-binds
760+ // survive a tmpfs over an ancestor. But denyWrite's --ro-bind <host> <host>
761+ // now lands after denyRead's --ro-bind /dev/null <host>, undoing the mask
762+ // when the same file is in both lists.
763+ it ( 'does not let denyWrite unmask a denyRead /dev/null bind (Linux)' , async ( ) => {
764+ if ( getPlatform ( ) !== 'linux' ) {
765+ return
766+ }
767+
768+ const parentDir = join ( tmpdir ( ) , `srt-test-unmask-${ Date . now ( ) } ` )
769+ const secret = join ( parentDir , 'secret.txt' )
770+ mkdirSync ( parentDir , { recursive : true } )
771+ writeFileSync ( secret , '' )
772+
773+ try {
774+ await SandboxManager . reset ( )
775+ await SandboxManager . initialize ( {
776+ network : { allowedDomains : [ ] , deniedDomains : [ ] } ,
777+ filesystem : {
778+ denyRead : [ secret ] ,
779+ allowWrite : [ parentDir ] ,
780+ denyWrite : [ secret ] ,
781+ } ,
782+ } )
783+
784+ const result = await SandboxManager . wrapWithSandbox ( command )
785+
786+ // The /dev/null mask is what we want; the host-file bind is what we don't.
787+ expect ( result ) . toContain ( `--ro-bind /dev/null ${ secret } ` )
788+ expect ( result ) . not . toContain ( `--ro-bind ${ secret } ${ secret } ` )
789+ } finally {
790+ await SandboxManager . reset ( )
791+ rmSync ( parentDir , { recursive : true , force : true } )
792+ }
793+ } )
794+
795+ // A file listed in denyRead should stay denied even if allowRead covers its
796+ // parent directory. Before this change, startsWith(allowPath + '/') matched
797+ // and the file-deny was silently skipped.
798+ it ( 'file-level denyRead survives a parent-directory allowRead (Linux)' , async ( ) => {
799+ if ( getPlatform ( ) !== 'linux' ) {
800+ return
801+ }
802+
803+ const parentDir = join ( tmpdir ( ) , `srt-test-file-deny-${ Date . now ( ) } ` )
804+ const secret = join ( parentDir , '.env' )
805+ mkdirSync ( parentDir , { recursive : true } )
806+ writeFileSync ( secret , '' )
807+
808+ try {
809+ await SandboxManager . reset ( )
810+ await SandboxManager . initialize ( {
811+ network : { allowedDomains : [ ] , deniedDomains : [ ] } ,
812+ filesystem : {
813+ denyRead : [ secret ] ,
814+ allowRead : [ parentDir ] ,
815+ allowWrite : [ parentDir ] ,
816+ denyWrite : [ ] ,
817+ } ,
818+ } )
819+
820+ const result = await SandboxManager . wrapWithSandbox ( command )
821+
822+ expect ( result ) . toContain ( `--ro-bind /dev/null ${ secret } ` )
823+ } finally {
824+ await SandboxManager . reset ( )
825+ rmSync ( parentDir , { recursive : true , force : true } )
826+ }
827+ } )
828+
829+ // denyRead entries are sorted shallow-first before mounting, so a file-deny
830+ // listed before its ancestor dir-deny still lands on top of the ancestor's
831+ // tmpfs + re-allow binds.
832+ it ( 'file-deny survives ancestor dir-deny listed after it in denyRead (Linux)' , async ( ) => {
833+ if ( getPlatform ( ) !== 'linux' ) {
834+ return
835+ }
836+
837+ const parentDir = join ( tmpdir ( ) , `srt-test-order-${ Date . now ( ) } ` )
838+ const projectDir = join ( parentDir , 'project' )
839+ const envFile = join ( projectDir , '.env' )
840+ mkdirSync ( projectDir , { recursive : true } )
841+ writeFileSync ( envFile , '' )
842+
843+ try {
844+ await SandboxManager . reset ( )
845+ await SandboxManager . initialize ( {
846+ network : { allowedDomains : [ ] , deniedDomains : [ ] } ,
847+ filesystem : {
848+ // File deliberately listed before the dir that contains it
849+ denyRead : [ envFile , parentDir ] ,
850+ allowRead : [ projectDir ] ,
851+ allowWrite : [ projectDir ] ,
852+ denyWrite : [ ] ,
853+ } ,
854+ } )
855+
856+ const result = await SandboxManager . wrapWithSandbox ( command )
857+
858+ // The /dev/null mask must come after the tmpfs + ro-bind in arg order.
859+ const tmpfsAt = result . indexOf ( `--tmpfs ${ parentDir } ` )
860+ const maskAt = result . indexOf ( `--ro-bind /dev/null ${ envFile } ` )
861+ expect ( tmpfsAt ) . toBeGreaterThan ( - 1 )
862+ expect ( maskAt ) . toBeGreaterThan ( tmpfsAt )
863+ } finally {
864+ await SandboxManager . reset ( )
865+ rmSync ( parentDir , { recursive : true , force : true } )
866+ }
867+ } )
758868} )
0 commit comments