2026-08-03 17:31:08 +08:00
/**
2026-08-03 17:48:41 +08:00
* Deterministic real-process proofs for runner classification: the real local
2026-08-04 12:04:48 +08:00
* provider and sandbox bash executor exercise direct runner-spawn failures
2026-08-03 17:48:41 +08:00
* and a POSIX fake Landlock launcher that prints its notice before exec.
2026-08-03 17:31:08 +08:00
*/
import { mkdtemp , rm , writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach , describe , expect , it } from 'vitest'
2026-08-10 22:04:06 +08:00
import { Context } from '@deepseek-ai/cordis'
2026-08-06 14:41:17 +08:00
import { LAUNCHER_FAILURE_EXIT } from '@deepseek-ai/node-addon-landlock-run'
2026-08-04 16:55:11 +08:00
import { SANDBOX_UNAVAILABLE , SandboxUnavailableError } from '@deepseek-ai/dsh-sandbox'
2026-08-03 17:31:08 +08:00
import { LocalSandboxProvider } from '@deepseek-ai/dsh-sandbox-local'
import { SandboxPolicyService } from '@deepseek-ai/dsh-sandbox-policy'
import { SandboxBashExecutor } from '@deepseek-ai/dsh-bash-sandbox'
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
2026-08-04 13:37:33 +08:00
const NOTICE = 'landlock-run: partial enforcement (older Landlock ABI)'
const FATAL_PREFIX = 'landlock-run: '
const FATAL = ` ${ FATAL_PREFIX } landlock ruleset error: Invalid argument `
2026-08-03 17:31:08 +08:00
const contexts : Context [ ] = [ ]
const tempDirs : string [ ] = [ ]
afterEach ( async ( ) = > {
await Promise . all ( contexts . splice ( 0 ) . map ( ctx = > ctx . fiber . dispose ( ) ) )
await Promise . all ( tempDirs . splice ( 0 ) . map ( dir = > rm ( dir , { recursive : true , force : true } ) ) )
} )
/** Write a fake native launcher that reports partial enforcement, then execs or fails. */
2026-08-04 12:04:48 +08:00
async function fakeLauncher ( fatalExit? : number ) : Promise < string > {
2026-08-03 17:31:08 +08:00
const dir = await mkdtemp ( join ( tmpdir ( ) , 'dsh-partial-landlock-' ) )
tempDirs . push ( dir )
const launcher = join ( dir , 'landlock-run' )
2026-08-04 12:04:48 +08:00
const fatalBranch = fatalExit === undefined ? '' : ` printf '%s \\ n' ' ${ FATAL } ' >&2 \ nexit ${ fatalExit } \ n `
2026-08-03 17:31:08 +08:00
await writeFile ( launcher , ` #!/bin/sh
while [ " $ #" -gt 0 ]; do
case " $ 1" in
--ro|--rw) shift 2 ;;
--) shift; break ;;
2026-08-04 13:37:33 +08:00
*) printf '%s \\ n' ' ${ FATAL_PREFIX } usage error: unexpected fake argument' >&2; exit ${ LAUNCHER_FAILURE_EXIT } ;;
2026-08-03 17:31:08 +08:00
esac
done
2026-08-04 13:37:33 +08:00
printf '%s \\ n' ' ${ NOTICE } ' >&2
2026-08-03 17:31:08 +08:00
${ fatalBranch } exec " $ @"
` , { mode : 0o755 } )
return launcher
}
2026-08-04 12:04:48 +08:00
async function setup ( fatalExit? : number ) : Promise < SandboxBashExecutor > {
2026-08-03 17:31:08 +08:00
const ctx = new Context ( )
contexts . push ( ctx )
await ctx . plugin ( LocalSandboxProvider , { } )
const sandbox = ctx . sandbox as LocalSandboxProvider
sandbox . internals = {
platform : 'linux' ,
probeBwrap : ( ) = > false ,
probeLandlock : ( ) = > 'partial' ,
2026-08-04 12:04:48 +08:00
landlockLauncher : await fakeLauncher ( fatalExit ) ,
2026-08-03 17:31:08 +08:00
}
await ctx . plugin ( SandboxPolicyService , { mode : 'read-only' , workspaceRoot : process.cwd ( ) } )
await ctx . plugin ( LocalSubprocessService )
await ctx . plugin ( SandboxBashExecutor , { cwd : process.cwd ( ) , timeoutMs : 5_000 } )
return ctx . bash as SandboxBashExecutor
}
2026-08-04 12:04:48 +08:00
async function setupConfiguredRunner ( runner : string ) : Promise < SandboxBashExecutor > {
const ctx = new Context ( )
contexts . push ( ctx )
await ctx . plugin ( LocalSandboxProvider , {
runnerCommand : [ runner ] ,
runnerFailureSignatures : [ 'configured-runner: fatal' ] ,
} )
await ctx . plugin ( SandboxPolicyService , { mode : 'read-only' , workspaceRoot : process.cwd ( ) } )
await ctx . plugin ( LocalSubprocessService )
await ctx . plugin ( SandboxBashExecutor , { cwd : process.cwd ( ) , timeoutMs : 5_000 } )
return ctx . bash as SandboxBashExecutor
}
2026-08-03 17:31:08 +08:00
describe ( 'partial Landlock runner-failure classification' , ( ) = > {
2026-08-04 16:06:54 +08:00
it . each ( [ 'missing' , 'unexecutable' , 'missing-interpreter' ] as const ) ( 'classifies a %s configured runner through the direct spawn error channel' , async ( kind ) = > {
2026-08-04 12:04:48 +08:00
const dir = await mkdtemp ( join ( tmpdir ( ) , 'dsh-unusable-sandbox-runner-' ) )
2026-08-03 17:48:41 +08:00
tempDirs . push ( dir )
2026-08-04 12:04:48 +08:00
const runner = join ( dir , ` ${ kind } -runner ` )
if ( kind === 'unexecutable' ) await writeFile ( runner , '#!/bin/sh\nexit 0\n' , { mode : 0o644 } )
2026-08-04 16:06:54 +08:00
if ( kind === 'missing-interpreter' ) {
await writeFile ( runner , '#!/dsh-definitely-missing-sandbox-interpreter\nexit 0\n' , { mode : 0o755 } )
}
2026-08-04 12:04:48 +08:00
const bash = await setupConfiguredRunner ( runner )
2026-08-03 17:48:41 +08:00
2026-08-04 12:04:48 +08:00
const error = await bash . run ( bash . resolve ( { command : 'true' } ) ) . catch ( ( value : unknown ) = > value )
2026-08-03 17:48:41 +08:00
expect ( error ) . toMatchObject ( { name : 'SandboxUnavailableError' , code : SANDBOX_UNAVAILABLE } )
expect ( error ) . toBeInstanceOf ( Error )
2026-08-04 12:04:48 +08:00
expect ( ( error as Error ) . message ) . toContain ( runner )
const task = bash . start ( bash . resolve ( { command : 'true' } ) )
await task . done
expect ( task . status ) . toBe ( 'killed' )
expect ( task . readOutput ( ) . delta ) . toContain ( ` spawn failed: Error: spawn ${ runner } ` )
expect ( task . sandbox ) . toEqual ( {
mode : 'read-only' ,
denied : false ,
enforcement : 'full' ,
runnerFailed : true ,
} )
const accounting = ( bash as unknown as { processFacts : Map < unknown , unknown > } ) . processFacts
expect ( accounting . size ) . toBe ( 0 )
2026-08-03 17:48:41 +08:00
} )
2026-08-04 17:19:19 +08:00
it . each ( [ 'bare-name' , 'relative' ] as const ) (
'classifies a %s runner whose shebang interpreter is missing' ,
async ( form ) = > {
const dir = await mkdtemp ( join ( tmpdir ( ) , 'dsh-argv-form-sandbox-runner-' ) )
tempDirs . push ( dir )
const filename = 'missing-interpreter-runner'
const runner = form === 'bare-name' ? filename : ` ./ ${ filename } `
await writeFile ( join ( dir , filename ) , '#!/dsh-definitely-missing-sandbox-interpreter\nexit 0\n' , { mode : 0o755 } )
const bash = await setupConfiguredRunner ( runner )
const request = form === 'bare-name'
? { command : 'true' , env : { PATH : dir } }
: { command : 'true' , workdir : dir }
const error = await bash . run ( bash . resolve ( request ) ) . catch ( ( value : unknown ) = > value )
expect ( error ) . toMatchObject ( { name : 'SandboxUnavailableError' , code : SANDBOX_UNAVAILABLE } )
expect ( error ) . toBeInstanceOf ( Error )
// Empirically, Darwin and Linux Node 24 preserve the passed bare/relative
// argv[0] in this spawn error rather than resolving it to an absolute path.
expect ( ( error as Error ) . message ) . toContain ( ` spawn ${ runner } ENOENT ` )
const task = bash . start ( bash . resolve ( request ) )
await task . done
expect ( task . status ) . toBe ( 'killed' )
expect ( task . readOutput ( ) . delta ) . toContain ( ` spawn failed: Error: spawn ${ runner } ENOENT ` )
expect ( task . sandbox ) . toEqual ( {
mode : 'read-only' ,
denied : false ,
enforcement : 'full' ,
runnerFailed : true ,
} )
} ,
)
2026-08-04 16:55:11 +08:00
2026-08-04 17:10:42 +08:00
it ( 'keeps a real malformed executable ordinary across no-shebang spawn behavior' , async ( ) = > {
2026-08-04 16:55:11 +08:00
const dir = await mkdtemp ( join ( tmpdir ( ) , 'dsh-malformed-sandbox-runner-' ) )
tempDirs . push ( dir )
const runner = join ( dir , 'malformed-runner' )
await writeFile ( runner , 'not a native executable or shebang script\n' , { mode : 0o755 } )
const bash = await setupConfiguredRunner ( runner )
2026-08-04 17:10:42 +08:00
const request = { command : 'true' }
2026-08-04 16:55:11 +08:00
2026-08-04 17:10:42 +08:00
// Node/libuv may expose execve's ENOEXEC directly (Darwin) or retry a
// no-shebang executable through /bin/sh (Linux). Neither path supplies the
2026-08-09 15:35:02 +08:00
// ENOENT/EACCES with the exact failed executable path required for runner attribution.
2026-08-04 17:10:42 +08:00
const foreground = await bash . run ( bash . resolve ( request ) ) . catch ( ( value : unknown ) = > value )
2026-08-04 16:55:11 +08:00
expect ( foreground ) . not . toBeInstanceOf ( SandboxUnavailableError )
2026-08-04 17:10:42 +08:00
if ( foreground instanceof Error ) {
expect ( foreground ) . toMatchObject ( { code : 'ENOEXEC' , syscall : 'spawn' } )
expect ( ( foreground as { path? : unknown } ) . path ) . toBeUndefined ( )
let background : unknown
try {
bash . start ( bash . resolve ( request ) )
} catch ( error ) {
background = error
}
expect ( background ) . toMatchObject ( { code : 'ENOEXEC' , syscall : 'spawn' } )
expect ( ( background as { path? : unknown } ) . path ) . toBeUndefined ( )
expect ( background ) . not . toBeInstanceOf ( SandboxUnavailableError )
} else {
expect ( foreground ) . toMatchObject ( {
exitCode : 127 ,
signal : null ,
sandbox : { mode : 'read-only' , denied : false , enforcement : 'full' } ,
} )
2026-08-04 17:45:12 +08:00
expect ( ( foreground as { stderr : { text : string } } ) . stderr . text . length ) . toBeGreaterThan ( 0 )
2026-08-04 17:10:42 +08:00
const background = bash . start ( bash . resolve ( request ) )
await background . done
expect ( background . status ) . toBe ( 'completed' )
expect ( background . exitCode ) . toBe ( 127 )
expect ( background . signal ) . toBeNull ( )
expect ( background . sandbox ) . toEqual ( { mode : 'read-only' , denied : false , enforcement : 'full' } )
const output = background . readOutput ( ) . delta
2026-08-04 17:45:12 +08:00
expect ( output . startsWith ( '[stderr]\n' ) ) . toBe ( true )
expect ( output . length ) . toBeGreaterThan ( '[stderr]\n' . length )
2026-08-04 17:10:42 +08:00
expect ( output ) . not . toContain ( 'spawn failed:' )
2026-08-04 16:55:11 +08:00
}
2026-08-04 17:10:42 +08:00
2026-08-04 16:55:11 +08:00
const accounting = ( bash as unknown as { processFacts : Map < unknown , unknown > } ) . processFacts
expect ( accounting . size ) . toBe ( 0 )
} )
2026-08-04 12:04:48 +08:00
it . each ( [ 0 , 1 , 2 , LAUNCHER_FAILURE_EXIT ] ) (
'keeps child exit %i ordinary when the partial-enforcement notice is the only runner line' ,
async ( exitCode ) = > {
const bash = await setup ( )
const result = await bash . run ( bash . resolve ( { command : ` exit ${ exitCode } ` } ) )
2026-08-03 17:31:08 +08:00
expect ( result . exitCode ) . toBe ( exitCode )
2026-08-04 13:37:33 +08:00
expect ( result . stderr . text ) . toBe ( ` ${ NOTICE } \ n ` )
2026-08-03 17:31:08 +08:00
expect ( result . sandbox ) . toEqual ( { mode : 'read-only' , denied : false , enforcement : 'partial' } )
2026-08-04 12:04:48 +08:00
} ,
)
it . each ( [ 126 , 127 ] ) ( 'keeps a successfully launched Landlock child exit %i as an ordinary outcome' , async ( exitCode ) = > {
const bash = await setup ( )
const result = await bash . run ( bash . resolve ( { command : ` exit ${ exitCode } ` } ) )
expect ( result . exitCode ) . toBe ( exitCode )
2026-08-04 13:37:33 +08:00
expect ( result . stderr . text ) . toBe ( ` ${ NOTICE } \ n ` )
2026-08-04 12:04:48 +08:00
expect ( result . sandbox ) . toEqual ( { mode : 'read-only' , denied : false , enforcement : 'partial' } )
} )
it . each ( [ 1 , 2 ] ) ( 'keeps a Landlock fatal line at exit %i as insufficient runner-failure evidence' , async ( exitCode ) = > {
const bash = await setup ( exitCode )
const result = await bash . run ( bash . resolve ( { command : 'true' } ) )
expect ( result . exitCode ) . toBe ( exitCode )
2026-08-04 13:37:33 +08:00
expect ( result . stderr . text ) . toBe ( ` ${ NOTICE } \ n ${ FATAL } \ n ` )
2026-08-04 12:04:48 +08:00
expect ( result . sandbox ) . toEqual ( { mode : 'read-only' , denied : false , enforcement : 'partial' } )
2026-08-03 17:31:08 +08:00
} )
it ( 'reports the fatal line after the notice as SANDBOX_UNAVAILABLE detail' , async ( ) = > {
2026-08-04 12:04:48 +08:00
const bash = await setup ( LAUNCHER_FAILURE_EXIT )
2026-08-03 17:31:08 +08:00
const error = await bash . run ( bash . resolve ( { command : 'true' } ) ) . catch ( ( value : unknown ) = > value )
expect ( error ) . toMatchObject ( { name : 'SandboxUnavailableError' , code : SANDBOX_UNAVAILABLE } )
expect ( error ) . toBeInstanceOf ( Error )
expect ( ( error as Error ) . message ) . toContain ( ` Runner failure: ${ FATAL } ` )
2026-08-04 13:37:33 +08:00
expect ( ( error as Error ) . message ) . not . toContain ( NOTICE )
2026-08-03 17:31:08 +08:00
} )
it ( 'classifies a notice plus child Permission denied as a denial, not runner failure' , async ( ) = > {
const bash = await setup ( )
const result = await bash . run ( bash . resolve ( { command : 'printf "%s\\n" "child: Permission denied" >&2; exit 1' } ) )
2026-08-04 13:37:33 +08:00
expect ( result . stderr . text ) . toBe ( ` ${ NOTICE } \ nchild: Permission denied \ n ` )
2026-08-03 17:31:08 +08:00
expect ( result . sandbox ) . toEqual ( { mode : 'read-only' , denied : true , enforcement : 'partial' } )
} )
it ( 'applies the same evidence rule to notice-only background exits' , async ( ) = > {
const bash = await setup ( )
2026-08-04 12:04:48 +08:00
for ( const command of [ 'exit 1' , 'exit 2' , ` exit ${ LAUNCHER_FAILURE_EXIT } ` ] ) {
2026-08-03 17:31:08 +08:00
const task = bash . start ( bash . resolve ( { command } ) )
await task . done
expect ( task . sandbox ) . toEqual ( { mode : 'read-only' , denied : false , enforcement : 'partial' } )
2026-08-04 13:37:33 +08:00
expect ( task . readOutput ( ) . delta ) . toContain ( NOTICE )
2026-08-03 17:31:08 +08:00
}
} )
it ( 'classifies a background notice plus child Permission denied as denial' , async ( ) = > {
const bash = await setup ( )
const task = bash . start ( bash . resolve ( { command : 'printf "%s\\n" "child: Permission denied" >&2; exit 1' } ) )
await task . done
expect ( task . sandbox ) . toEqual ( { mode : 'read-only' , denied : true , enforcement : 'partial' } )
2026-08-04 13:37:33 +08:00
expect ( task . readOutput ( ) . delta ) . toContain ( NOTICE )
2026-08-03 17:31:08 +08:00
} )
it ( 'makes a background fatal line outrank denial text after the notice' , async ( ) = > {
2026-08-04 12:04:48 +08:00
const bash = await setup ( LAUNCHER_FAILURE_EXIT )
2026-08-03 17:31:08 +08:00
const task = bash . start ( bash . resolve ( { command : 'true' } ) )
await task . done
expect ( task . sandbox ) . toEqual ( {
mode : 'read-only' ,
denied : false ,
enforcement : 'partial' ,
runnerFailed : true ,
} )
const output = task . readOutput ( ) . delta
2026-08-04 13:37:33 +08:00
expect ( output ) . toContain ( NOTICE )
2026-08-03 17:31:08 +08:00
expect ( output ) . toContain ( FATAL )
} )
} )