From 9fda83eacc60f19de6daec42d90072d6a57035f4 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Fri, 11 Sep 2026 18:08:53 +0000 Subject: [PATCH] fix(cso): pass native release gates Co-Authored-By: OpenAI Codex --- .github/docker/Dockerfile.ci | 9 +++- lib/cso/launcher.c | 3 ++ lib/cso/publish-lock.c | 5 ++- lib/cso/verification.ts | 2 +- lib/cso/watchdog.c | 10 ++++- scripts/build-cso.sh | 7 +++- scripts/test-free-shards.ts | 24 +++++++++++ test/cso-distribution.test.ts | 18 ++++++++ test/cso-docker-integration.test.ts | 2 +- test/cso-image-provisioning.test.ts | 4 +- test/cso-snapshot-disappearance-race.test.ts | 13 +++++- test/cso-snapshot-state.test.ts | 8 ++-- test/cso-windows-launcher.test.ts | 1 + test/provider-model-defaults.test.ts | 44 ++++++++++++-------- test/test-free-shards.test.ts | 15 +++++++ 15 files changed, 134 insertions(+), 31 deletions(-) diff --git a/.github/docker/Dockerfile.ci b/.github/docker/Dockerfile.ci index 39247f4a1..e2da7a89e 100644 --- a/.github/docker/Dockerfile.ci +++ b/.github/docker/Dockerfile.ci @@ -34,11 +34,18 @@ RUN printf 'Acquire::Retries "5";\nAcquire::http::Timeout "30";\nAcquire::https: # poppler-utils: make-pdf's e2e gates hard-require pdftotext/pdffonts/pdfinfo in CI. RUN for i in 1 2 3; do \ apt-get update && apt-get install -y --no-install-recommends \ - git curl unzip xz-utils ca-certificates jq bc gpg python3 file poppler-utils && break || \ + git curl unzip xz-utils ca-certificates jq bc gpg python3 file poppler-utils gcc libc6-dev && break || \ (echo "apt retry $i/3 after failure"; sleep 10); \ done \ && rm -rf /var/lib/apt/lists/* +# Direct builds produce the trusted CSO launcher and watchdog. Check the exact +# static-C capability here so the cached eval image cannot reach a slice without it. +RUN printf 'int main(void) { return 0; }\n' > /tmp/gstack-cso-cc-probe.c \ + && cc -std=c11 -static /tmp/gstack-cso-cc-probe.c -o /tmp/gstack-cso-cc-probe \ + && /tmp/gstack-cso-cc-probe \ + && rm -f /tmp/gstack-cso-cc-probe.c /tmp/gstack-cso-cc-probe + # GitHub CLI RUN curl --retry 5 --retry-delay 5 --retry-connrefused -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \ | gpg --dearmor -o /usr/share/keyrings/githubcli-archive-keyring.gpg \ diff --git a/lib/cso/launcher.c b/lib/cso/launcher.c index e2f30171f..57ebdaeae 100644 --- a/lib/cso/launcher.c +++ b/lib/cso/launcher.c @@ -1,6 +1,9 @@ /* Minimal trusted CSO launcher. Linux builds are static so LD_PRELOAD cannot * execute before the environment is replaced. macOS builds are signed with * the hardened runtime by build/setup before use. */ +#ifdef __APPLE__ +#define _DARWIN_C_SOURCE 1 +#endif #define _POSIX_C_SOURCE 200809L #define _XOPEN_SOURCE 700 #include diff --git a/lib/cso/publish-lock.c b/lib/cso/publish-lock.c index 32f5b036e..cea9f78f7 100644 --- a/lib/cso/publish-lock.c +++ b/lib/cso/publish-lock.c @@ -68,7 +68,7 @@ int wmain(int argc, wchar_t **argv) { if (!job || !SetInformationJobObject(job, JobObjectExtendedLimitInformation, &limits, (DWORD)sizeof(limits))) return 69; STARTUPINFOW startup = {0}; PROCESS_INFORMATION child = {0}; startup.cb = (DWORD)sizeof(startup); if (!CreateProcessW(argv[2], command, NULL, NULL, TRUE, CREATE_SUSPENDED, NULL, NULL, &startup, &child)) { - fputs("gstack-cso: publication command could not start\n", stderr); return 69; + fwprintf(stderr, L"gstack-cso: publication command could not start (Windows error %lu)\n", (unsigned long)GetLastError()); return 69; } if (!AssignProcessToJobObject(job, child.hProcess) || ResumeThread(child.hThread) == (DWORD)-1) { TerminateProcess(child.hProcess, 69); CloseHandle(child.hThread); CloseHandle(child.hProcess); return 69; @@ -80,6 +80,9 @@ int wmain(int argc, wchar_t **argv) { return (int)code; } #else +#ifdef __APPLE__ +#define _DARWIN_C_SOURCE 1 +#endif #define _POSIX_C_SOURCE 200809L #include #include diff --git a/lib/cso/verification.ts b/lib/cso/verification.ts index 3659a1f7e..a4a7107dd 100644 --- a/lib/cso/verification.ts +++ b/lib/cso/verification.ts @@ -30,7 +30,7 @@ export class VerificationAttemptError extends CsoError { constructor(public causeError:CsoError,public attempt:FailedVerificationAttempt){super(causeError.code,`${causeError.message}; before-phase evidence retained as attempt ${attempt.id}`);this.name='VerificationAttemptError';} } async function attemptGuard(runDir:string,work:string,watchdog:string,deadline:number):Promise<()=>Promise>{ - const control=secureDirectory(join(runDir,'supervision',work.slice(work.lastIndexOf('/')+1)));secureDirectory(work);const ready=join(control,'attempt.ready'),terminal=join(control,'attempt.terminal'),stopped=join(control,'attempt.stopped'); + const control=secureDirectory(join(runDir,'supervision',basename(work)));secureDirectory(work);const ready=join(control,'attempt.ready'),terminal=join(control,'attempt.terminal'),stopped=join(control,'attempt.stopped'); const child=spawn(watchdog,['--attempt-owner',String(process.pid),'--deadline',String(Math.ceil(deadline/1000)),'--control-dir',control,'--work-root',work,'--run-root',runDir],{cwd:control,env:{PATH:'/usr/bin:/bin'},detached:true,stdio:'ignore'});let failed=false;child.once('error',()=>{failed=true;});child.unref();for(let i=0;i<100&&!failed&&!fs.existsSync(ready);i++)await new Promise(resolve=>setTimeout(resolve,10));let alive=false;try{if(child.pid){process.kill(child.pid,0);alive=true;}}catch{}if(failed||!alive||!fs.existsSync(ready)){try{if(child.pid)process.kill(child.pid,'SIGKILL');}catch{}fs.rmSync(work,{recursive:true,force:true});throw new CsoError('ISOLATION_FAILED','Attempt execution-copy watchdog failed its startup handshake');} return async()=>{fs.rmSync(work,{recursive:true,force:true});fs.writeFileSync(terminal,'normal cleanup complete\n',{mode:0o600,flag:'wx'});for(let i=0;i<100&&!fs.existsSync(stopped);i++)await new Promise(resolve=>setTimeout(resolve,10));if(!fs.existsSync(stopped))throw new CsoError('ISOLATION_FAILED','Attempt watchdog did not acknowledge execution-copy cleanup');}; } diff --git a/lib/cso/watchdog.c b/lib/cso/watchdog.c index 4b9c01066..629314482 100644 --- a/lib/cso/watchdog.c +++ b/lib/cso/watchdog.c @@ -1,4 +1,7 @@ /* Independent CSO lease watchdog. It accepts only exact journaled container IDs. */ +#ifdef __APPLE__ +#define _DARWIN_C_SOURCE 1 +#endif #define _XOPEN_SOURCE 700 #include #include @@ -136,7 +139,12 @@ static void marker(const char *run_dir,const char *name,const char *value){ if(snprintf(path,sizeof path,"%s/%s",run_dir,name)>=(int)sizeof path)return; if(snprintf(tmp,sizeof tmp,"%s/%s.tmp.%ld",run_dir,name,(long)getpid())>=(int)sizeof tmp)return; int fd=open(tmp,O_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOW,0600);if(fd<0)return; - (void)write(fd,value,strlen(value));(void)fsync(fd);close(fd);(void)rename(tmp,path); + size_t remaining=strlen(value);const char *cursor=value;int failed=0; + while(remaining){ssize_t written=write(fd,cursor,remaining);if(written>0){cursor+=written;remaining-=(size_t)written;continue;}if(written<0&&errno==EINTR)continue;failed=1;break;} + if(!failed&&fsync(fd))failed=1; + if(close(fd))failed=1; + if(!failed&&!rename(tmp,path))return; + (void)unlink(tmp); } static int descendant(const char *root,const char *path){size_t n=strlen(root);return n>1&&!strncmp(root,path,n)&&path[n]=='/';} static int remove_tree(const char *path){ diff --git a/scripts/build-cso.sh b/scripts/build-cso.sh index d95c7e637..90355b4ae 100644 --- a/scripts/build-cso.sh +++ b/scripts/build-cso.sh @@ -283,5 +283,10 @@ cso_valid_generation "$CSO_STAGE_GENERATION"||{ echo 'Staged CSO generation mani "$CSO_STAGE_LAUNCHER" --version >/dev/null cso_checkpoint validated -case "${BASH:-}" in /*) CSO_PUBLISH_SHELL=$BASH;; *) echo 'CSO publication requires an absolute Bash executable.' >&2;exit 1;;esac +if [ -n "$CSO_EXE" ];then + [ -f /usr/bin/bash.exe ] || { echo 'CSO publication requires the Git Bash executable.' >&2;exit 1; } + CSO_PUBLISH_SHELL="$(cygpath -aw /usr/bin/bash.exe)" +else + case "${BASH:-}" in /*) CSO_PUBLISH_SHELL=$BASH;; *) echo 'CSO publication requires an absolute Bash executable.' >&2;exit 1;;esac +fi exec "$CSO_STAGE_LOCKER" "$CSO_BUILD_ROOT/bin" "$CSO_PUBLISH_SHELL" "$CSO_BUILD_ROOT/scripts/build-cso.sh" __publish_locked "$CSO_STAGE" diff --git a/scripts/test-free-shards.ts b/scripts/test-free-shards.ts index 9f5daeb0f..75b9b2b99 100755 --- a/scripts/test-free-shards.ts +++ b/scripts/test-free-shards.ts @@ -252,6 +252,30 @@ export const KNOWN_WINDOWS_INCOMPATIBLE: Array<{ file: string; reason: string }> file: 'browse/test/security-audit-r2.test.ts', reason: 'symlink-attack fixtures (evil-link) need Developer Mode CI runners lack; expect(toThrow) fires unhandled on Windows', }, + // CSO comprehensive execution is qualified only for Linux containers behind + // the POSIX watchdog and Unix-domain registry broker. Keep the portable + // static/parser contracts in the Windows lane while leaving these exact + // containment suites to the Linux and macOS gates. + { + file: 'test/cso-preparation-adversarial.test.ts', + reason: 'exercises POSIX prepared-tree and archive-cache containment for qualified Linux Docker execution, which Windows does not admit', + }, + { + file: 'test/cso-preparation-container.test.ts', + reason: 'asserts POSIX permission and symlink semantics for inert exports consumed by qualified Linux Docker execution', + }, + { + file: 'test/cso-preparation-executor.test.ts', + reason: 'executes the Linux Docker acquisition path and its Unix-domain registry broker; comprehensive execution is unavailable on Windows', + }, + { + file: 'test/cso-verification-cleanup.test.ts', + reason: 'spawns the POSIX detached watchdog used by contained repair verification, which Windows intentionally leaves unavailable', + }, + { + file: 'test/cso-witness.test.ts', + reason: 'tests the contained repair witness with POSIX private-directory and compiled-helper assumptions; comprehensive execution is unavailable on Windows', + }, ]; // Force-include overrides: files a WINDOWS_FRAGILE_PATTERNS regex excludes for diff --git a/test/cso-distribution.test.ts b/test/cso-distribution.test.ts index 47d2825c5..0180404bd 100644 --- a/test/cso-distribution.test.ts +++ b/test/cso-distribution.test.ts @@ -87,6 +87,24 @@ function reviewedInputs(): any { } describe('CSO build and distribution wiring', () => { + test('POSIX helpers expose Darwin no-follow flags before system headers', () => { + for (const relative of ['lib/cso/launcher.c', 'lib/cso/watchdog.c', 'lib/cso/publish-lock.c']) { + const source = readFileSync(join(ROOT, relative), 'utf8'); + const darwinFeature = source.indexOf('#define _DARWIN_C_SOURCE 1'); + const fileFlags = source.indexOf('#include '); + expect(darwinFeature, relative).toBeGreaterThanOrEqual(0); + expect(fileFlags, relative).toBeGreaterThan(darwinFeature); + expect(source, relative).toContain('O_NOFOLLOW'); + } + }); + + test('the cached eval image proves the static C toolchain required by direct builds', () => { + const dockerfile = readFileSync(join(ROOT, '.github/docker/Dockerfile.ci'), 'utf8'); + expect(dockerfile).toMatch(/\bgcc libc6-dev\b/); + expect(dockerfile).toContain('cc -std=c11 -static /tmp/gstack-cso-cc-probe.c'); + expect(dockerfile).toContain('/tmp/gstack-cso-cc-probe'); + }); + test.skipIf(process.platform === 'win32')('real build script supplies all startup-hardening flags and compiles its watchdog', () => { const dir = buildFixture(), log = join(dir, 'compiler.log'); const r = fakeBuild(dir); diff --git a/test/cso-docker-integration.test.ts b/test/cso-docker-integration.test.ts index 25bae5b78..961744247 100644 --- a/test/cso-docker-integration.test.ts +++ b/test/cso-docker-integration.test.ts @@ -5,7 +5,7 @@ const enabled=process.env.GSTACK_CSO_DOCKER_TESTS==='1',suite=enabled?describe:d function exec(file:string,args:string[],env:Record={}){const r=spawnSync(file,args,{encoding:'utf8',env:{PATH:'/usr/local/bin:/usr/bin:/bin',HOME:root,GSTACK_HOME:state,DOCKER_HOST:'unix:///var/run/docker.sock',...env},timeout:120_000});if(r.status!==0)throw new Error(`${file} failed: ${r.stderr}`);return r.stdout.trim();} beforeAll(()=>{if(!enabled)return;root=fs.mkdtempSync(path.join(os.tmpdir(),'cso-docker-'));state=path.join(root,'state');process.env.GSTACK_HOME=state;watchdog=path.resolve(import.meta.dir,'../bin/gstack-cso-watchdog');if(!fs.existsSync(watchdog))throw new Error('GSTACK_CSO_DOCKER_TESTS=1 requires the compiled watchdog'); const source=`#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nstatic int blocked4(const char *ip,int port){int s=socket(AF_INET,SOCK_STREAM,0);struct sockaddr_in a={.sin_family=AF_INET,.sin_port=htons(port)};inet_pton(AF_INET,ip,&a.sin_addr);int r=connect(s,(void*)&a,sizeof a);close(s);return r!=0;}\nstatic int blocked6(void){int s=socket(AF_INET6,SOCK_STREAM,0);struct sockaddr_in6 a={.sin6_family=AF_INET6,.sin6_port=htons(443)};inet_pton(AF_INET6,"2606:4700:4700::1111",&a.sin6_addr);int r=connect(s,(void*)&a,sizeof a);close(s);return r!=0;}\nint main(int argc,char**argv){if(strstr(argv[0],"entrypoint")){if(argc<2||argv[1][0]!='/')return 64;execv(argv[1],argv+1);return 69;}if(strstr(argv[0],"sleep")){for(;;)sleep(3600);}if(strstr(argv[0],"reader")){char a[16]={0},b[16]={0};FILE*x=fopen("/source/source.txt","r"),*y=fopen("/policy/verification.json","r");if(!x||!y||!fgets(a,sizeof a,x)||!fgets(b,sizeof b,y))return 12;fclose(x);fclose(y);if(strcmp(a,"source\\n")||strcmp(b,"policy\\n"))return 13;puts("INPUTS_OK");return 0;}if(strstr(argv[0],"server")){int s=socket(AF_INET,SOCK_STREAM,0),c;struct sockaddr_in a={.sin_family=AF_INET,.sin_port=htons(34567),.sin_addr.s_addr=htonl(INADDR_LOOPBACK)};int one=1;setsockopt(s,SOL_SOCKET,SO_REUSEADDR,&one,sizeof one);if(bind(s,(void*)&a,sizeof a)||listen(s,2))return 2;for(;;){c=accept(s,0,0);if(c>=0){write(c,"ok",2);close(c);}}}int fd=open("/should-not-write",O_WRONLY|O_CREAT,0600);if(fd>=0)return 10;FILE*f=fopen("/proc/self/status","r");char line[256];int caps=1,nnp=0;while(f&&fgets(line,sizeof line,f)){if(!strncmp(line,"CapEff:",7))caps=strtoull(line+7,0,16)!=0;if(!strncmp(line,"NoNewPrivs:",11))nnp=atoi(line+11);}if(f)fclose(f);int s=socket(AF_INET,SOCK_STREAM,0);struct sockaddr_in a={.sin_family=AF_INET,.sin_port=htons(34567),.sin_addr.s_addr=htonl(INADDR_LOOPBACK)};int loop=connect(s,(void*)&a,sizeof a)==0;close(s);struct addrinfo*h=0;int dns=getaddrinfo("example.com",0,0,&h);if(h)freeaddrinfo(h);if(getuid()==0||caps||!nnp||!loop||!blocked4("8.8.8.8",443)||!blocked4("169.254.169.254",80)||!blocked6()||dns==0)return 11;puts("CONTAINMENT_OK");return 0;}\n`; - fs.writeFileSync(path.join(root,'probe.c'),source);exec('/usr/bin/cc',['-static','-O2',path.join(root,'probe.c'),'-o',path.join(root,'probe')]);exec('/usr/bin/cc',['-static','-O2',path.join(import.meta.dir,'fixtures/cso-http-smoke.c'),'-o',path.join(root,'http-server')]);fs.writeFileSync(path.join(root,'Dockerfile'),'FROM scratch\nCOPY probe /opt/cso/entrypoint\nCOPY probe /bin/sleep\nCOPY probe /server\nCOPY probe /client\nCOPY probe /reader\nCOPY probe /policy/.keep\nCOPY probe /source/.keep\nCOPY http-server /http-server\nUSER 10001:10001\nENTRYPOINT ["/opt/cso/entrypoint"]\n');const tag=`gstack-cso-fixture:${process.pid}`;exec('/usr/bin/docker',['--host','unix:///var/run/docker.sock','build','--network=none','--tag',tag,root]);image=exec('/usr/bin/docker',['--host','unix:///var/run/docker.sock','image','inspect','--format','{{.Id}}',tag]);fs.writeFileSync(path.join(root,'VolumeDockerfile'),`FROM ${image}\nVOLUME ["/unbounded"]\n`);const volumeTag=`gstack-cso-volume-fixture:${process.pid}`;exec('/usr/bin/docker',['--host','unix:///var/run/docker.sock','build','--network=none','--file',path.join(root,'VolumeDockerfile'),'--tag',volumeTag,root]);volumeImage=exec('/usr/bin/docker',['--host','unix:///var/run/docker.sock','image','inspect','--format','{{.Id}}',volumeTag]);}); + fs.writeFileSync(path.join(root,'probe.c'),source);exec('/usr/bin/cc',['-static','-O2',path.join(root,'probe.c'),'-o',path.join(root,'probe')]);exec('/usr/bin/cc',['-static','-O2',path.join(import.meta.dir,'fixtures/cso-http-smoke.c'),'-o',path.join(root,'http-server')]);fs.writeFileSync(path.join(root,'Dockerfile'),'FROM scratch\nCOPY probe /opt/cso/entrypoint\nCOPY probe /bin/sleep\nCOPY probe /server\nCOPY probe /client\nCOPY probe /reader\nCOPY probe /policy/.keep\nCOPY probe /source/.keep\nCOPY http-server /http-server\nUSER 10001:10001\nENTRYPOINT ["/opt/cso/entrypoint"]\n');const tag=`gstack-cso-fixture:${process.pid}`;exec('/usr/bin/docker',['--host','unix:///var/run/docker.sock','build','--network=none','--tag',tag,root]);image=exec('/usr/bin/docker',['--host','unix:///var/run/docker.sock','image','inspect','--format','{{.Id}}',tag]);fs.writeFileSync(path.join(root,'VolumeDockerfile'),`FROM ${tag}\nVOLUME ["/unbounded"]\n`);const volumeTag=`gstack-cso-volume-fixture:${process.pid}`;exec('/usr/bin/docker',['--host','unix:///var/run/docker.sock','build','--network=none','--file',path.join(root,'VolumeDockerfile'),'--tag',volumeTag,root]);volumeImage=exec('/usr/bin/docker',['--host','unix:///var/run/docker.sock','image','inspect','--format','{{.Id}}',volumeTag]);}); afterAll(()=>{if(root){for(const candidate of [volumeImage,image])try{if(candidate)spawnSync('/usr/bin/docker',['--host','unix:///var/run/docker.sock','image','rm','--force',candidate],{stdio:'ignore',timeout:30_000});}catch{}fs.rmSync(root,{recursive:true,force:true});}delete process.env.GSTACK_HOME;}); suite('CSO Docker containment integration',()=>{ diff --git a/test/cso-image-provisioning.test.ts b/test/cso-image-provisioning.test.ts index 07324ee96..caf33ac00 100644 --- a/test/cso-image-provisioning.test.ts +++ b/test/cso-image-provisioning.test.ts @@ -143,7 +143,7 @@ describe('trusted CSO image provisioning',()=>{ await expect(openLocalCatalogImageSession({HOME:os.tmpdir(),DOCKER_HOST:'tcp://127.0.0.1:2375'})).rejects.toThrow('Remote TCP'); }); - test('doctor and setup command contracts share exact local availability without doctor downloads',async()=>{ + test.skipIf(process.platform==='win32')('doctor and setup command contracts share exact local availability without doctor downloads',async()=>{ const repo=nodeRepo(),runtimeCatalog=completeRuntimeCatalogFixture('doctor-cli-fixture'),scanners=scannerCatalog(),fake=session({present:()=>false}),dependencies:CsoCliDependencies={ runtimeCatalog,scannerCatalog:scanners,catalogImageSession:async()=>fake.value,watchdogPath:()=>'/trusted/watchdog', }; @@ -163,7 +163,7 @@ describe('trusted CSO image provisioning',()=>{ await expect(dispatchCsoCommand('provision-images',['--per-image-seconds','301'],dependencies)).rejects.toThrow('5..300'); }); - test('doctor includes the required PostgreSQL sidecar in Rails readiness without pulling',async()=>{ + test.skipIf(process.platform==='win32')('doctor includes the required PostgreSQL sidecar in Rails readiness without pulling',async()=>{ const repo=railsPostgresRepo(),runtimeCatalog=completeRuntimeCatalogFixture('doctor-rails-postgresql-fixture'),fake=session({present:entry=>!entry.id.includes('postgresql')}),dependencies:CsoCliDependencies={ runtimeCatalog,scannerCatalog:scannerCatalog(),catalogImageSession:async()=>fake.value,watchdogPath:()=>'/trusted/watchdog', }; diff --git a/test/cso-snapshot-disappearance-race.test.ts b/test/cso-snapshot-disappearance-race.test.ts index 62a95926e..fdd51412d 100644 --- a/test/cso-snapshot-disappearance-race.test.ts +++ b/test/cso-snapshot-disappearance-race.test.ts @@ -8,11 +8,20 @@ import { capture } from '../lib/cso/snapshot'; const roots:string[]=[]; afterEach(()=>{for(const root of roots.splice(0))fs.rmSync(root,{recursive:true,force:true});}); +function trustedGit():string{ + const executable=Bun.which('git'); + if(!executable)throw new Error('git is required for the snapshot disappearance fixture'); + return executable; +} +function gitEnvironment(home:string):NodeJS.ProcessEnv{ + return{...process.env,HOME:home,GIT_CONFIG_NOSYSTEM:'1',GIT_CONFIG_GLOBAL:process.platform==='win32'?'NUL':'/dev/null',GIT_TERMINAL_PROMPT:'0'}; +} + function fixture(){ const root=fs.mkdtempSync(path.join(os.tmpdir(),'cso-snapshot-disappearance-'));roots.push(root); const repo=path.join(root,'repo'),runDir=path.join(root,'run'); fs.mkdirSync(repo);fs.mkdirSync(runDir,{mode:0o700}); - const git=(...args:string[])=>{const result=spawnSync('/usr/bin/git',['-C',repo,...args],{encoding:'utf8',env:{HOME:root,PATH:'/usr/bin:/bin'},timeout:30_000});if(result.status)throw new Error(result.stderr);return result.stdout.trim();}; + const git=(...args:string[])=>{const result=spawnSync(trustedGit(),['-C',repo,...args],{encoding:'utf8',env:gitEnvironment(root),timeout:30_000});if(result.status)throw new Error(result.stderr);return result.stdout.trim();}; git('init','-q');git('config','user.email','fixture@example.test');git('config','user.name','Fixture'); const tracked=path.join(repo,'tracked.txt');fs.writeFileSync(tracked,'security-relevant source\n');git('add','tracked.txt');git('commit','-qm','base'); return{repo,runDir,tracked,parked:path.join(repo,'.tracked.txt.parked')}; @@ -49,7 +58,7 @@ describe('CSO snapshot source-disappearance races',()=>{ }); test.skipIf(process.platform==='win32')('binds the audited repository root while source is copied',async()=>{ const root=fs.mkdtempSync(path.join(os.tmpdir(),'cso-snapshot-root-swap-'));roots.push(root);const repo=path.join(root,'repo'),parked=path.join(root,'parked'),decoy=path.join(root,'decoy'),gitDir=path.join(root,'gitdir'),runDir=path.join(root,'run'),tracked=path.join(repo,'tracked.txt');fs.mkdirSync(repo);fs.mkdirSync(runDir,{mode:0o700}); - const run=(...args:string[])=>{const result=spawnSync('/usr/bin/git',args,{encoding:'utf8',env:{HOME:root,PATH:'/usr/bin:/bin'},timeout:30_000});if(result.status)throw new Error(result.stderr);}; + const run=(...args:string[])=>{const result=spawnSync(trustedGit(),args,{encoding:'utf8',env:gitEnvironment(root),timeout:30_000});if(result.status)throw new Error(result.stderr);}; run('init','-q',`--separate-git-dir=${gitDir}`,repo);run('-C',repo,'config','user.email','fixture@example.test');run('-C',repo,'config','user.name','Fixture');fs.writeFileSync(tracked,'secure original source\n');run('-C',repo,'add','tracked.txt');run('-C',repo,'commit','-qm','base');fs.mkdirSync(decoy);fs.writeFileSync(path.join(decoy,'.git'),`gitdir: ${gitDir}\n`);fs.writeFileSync(path.join(decoy,'tracked.txt'),'vulnerable decoy source\n'); const lstat=fs.lstatSync;let seen=0,injected=false,failure:unknown; const patched=spyOn(fs,'lstatSync').mockImplementation(((candidate:any,options?:any)=>{if(path.resolve(String(candidate))===tracked&&++seen===2){injected=true;fs.renameSync(repo,parked);fs.renameSync(decoy,repo);}return options===undefined?lstat(candidate):lstat(candidate,options);}) as typeof fs.lstatSync); diff --git a/test/cso-snapshot-state.test.ts b/test/cso-snapshot-state.test.ts index 6e09ac097..88bc437f1 100644 --- a/test/cso-snapshot-state.test.ts +++ b/test/cso-snapshot-state.test.ts @@ -126,14 +126,14 @@ describe('private state and process output',()=>{ expect(injected).toBe(true);expect(probes).toBe(532);expect(entered).toBe(false);expect(caught).toMatchObject({code:'INSUFFICIENT_CAPACITY'});expect(fs.existsSync(temporary)).toBe(false);expect(fs.readdirSync(leases)).toEqual([]); }); test('a settled publication remains bound to the originally observed inode',()=>{ - const dir=tmp(),token='4'.repeat(32),publisherPid=2147483647,target=path.join(dir,`${token}.json`),temporary=`${target}.tmp.${publisherPid}.abcdef12`,owner=JSON.stringify({pid:publisherPid,token,createdAt:0})+'\n'; + const dir=tmp(),token='4'.repeat(32),publisherPid=2147483647,target=path.join(dir,`${token}.json`),temporary=`${target}.tmp.${publisherPid}.abcdef12`,parked=`${target}.replaced`,owner=JSON.stringify({pid:publisherPid,token,createdAt:0})+'\n'; fs.writeFileSync(temporary,owner,{mode:0o600,flag:'wx'});fs.linkSync(temporary,target);const original=fs.statSync(target).ino,lstat=fs.lstatSync;let targetReads=0,replaced=false,caught:unknown; const reader=spyOn(fs,'lstatSync').mockImplementation(((file:any,options?:any)=>{ - if(String(file)===target&&++targetReads===2){fs.unlinkSync(target);fs.unlinkSync(temporary);fs.writeFileSync(target,owner,{mode:0o600,flag:'wx'});replaced=true;} + if(String(file)===target&&++targetReads===2){fs.unlinkSync(target);fs.renameSync(temporary,parked);fs.writeFileSync(target,owner,{mode:0o600,flag:'wx'});replaced=true;} return options===undefined?lstat(file):lstat(file,options); }) as typeof fs.lstatSync); try{discardAtomicNoReplaceTemp(temporary,publisherPid,{label:'Test publication',maxBytes:4096,validate:(value,pid)=>{expect((value as any).token).toBe(token);expect(pid).toBe(publisherPid);}});}catch(error){caught=error;}finally{reader.mockRestore();} - expect(replaced).toBe(true);expect(caught).toMatchObject({code:'UNSAFE_PATH'});expect(fs.statSync(target).ino).not.toBe(original); + expect(replaced).toBe(true);expect(caught).toMatchObject({code:'UNSAFE_PATH'});expect(fs.statSync(parked).ino).toBe(original);expect(fs.statSync(target).ino).not.toBe(original); }); test('a link transition on an open inode cannot launder a replacement pathname',()=>{ const dir=tmp();expect(withLock(dir,()=>1)).toBe(1); @@ -165,7 +165,7 @@ describe('private state and process output',()=>{ test('post-link verification failure cleans every recorded lease phase',()=>{const dir=tmp();expect(withLock(dir,()=>1)).toBe(1);const leases=path.join(dir,'.mutation-lock-leases'),fakeToken='f'.repeat(32),fakeTemp=path.join(leases,`${fakeToken}.json.tmp.${process.pid}.deadbeef`),link=fs.linkSync,injectedOwner=JSON.stringify({pid:process.pid,token:fakeToken,createdAt:Date.now()})+'\n';let injected=false,caught:unknown;const publisher=spyOn(fs,'linkSync').mockImplementation(((source:any,target:any)=>{const result=link(source,target);if(!injected&&String(target).includes('.active.')){injected=true;fs.writeFileSync(fakeTemp,injectedOwner,{mode:0o600,flag:'wx'});}return result;}) as typeof fs.linkSync);try{withLock(dir,()=>1);}catch(error){caught=error;}finally{publisher.mockRestore();}expect(injected).toBe(true);expect(caught).toMatchObject({code:'INSUFFICIENT_CAPACITY'});expect(fs.readdirSync(leases)).toEqual([path.basename(fakeTemp)]);fs.unlinkSync(fakeTemp);expect(withLock(dir,()=>2)).toBe(2);expect(fs.readdirSync(leases)).toEqual([]);}); test('an async callback retains its lease until settlement without blocking its own event loop',async()=>{const dir=tmp();let release!:()=>void;const pending=new Promise(resolve=>{release=resolve;}),outer=withLock(dir,async()=>{await pending;return 7;}),started=Date.now();expect(()=>withLock(dir,()=>99)).toThrow('Another operation in this helper');expect(Date.now()-started).toBeLessThan(200);release();expect(await outer).toBe(7);expect(withLock(dir,()=>9)).toBe(9);}); test('a synchronous exact-release failure is attempted only once',()=>{const dir=tmp(),lstat=fs.lstatSync;let observed=0,reader:any;try{expect(()=>withLock(dir,()=>{const leases=path.join(dir,'.mutation-lock-leases'),names=fs.readdirSync(leases),candidate=path.join(leases,names.find(name=>name.endsWith('.json'))!),active=path.join(leases,names.find(name=>name.includes('.active.'))!),value=fs.readFileSync(active);fs.unlinkSync(active);fs.writeFileSync(active,value,{mode:0o600,flag:'wx'});reader=spyOn(fs,'lstatSync').mockImplementation(((file:any,options?:any)=>{if(String(file)===candidate)observed++;return options===undefined?lstat(file):lstat(file,options);}) as typeof fs.lstatSync);})).toThrow('active phase changed before cleanup');expect(observed).toBe(1);}finally{reader?.mockRestore();}}); - test('exact release rejects active or decision inode substitution and overrides callback success or failure',()=>{for(const phase of ['active','decision'] as const){const dir=tmp();let replacement='';expect(()=>withLock(dir,()=>{const leases=path.join(dir,'.mutation-lock-leases'),name=fs.readdirSync(leases).find(value=>phase==='active'?value.includes('.active.'):value.endsWith('.decision'))!,lease=path.join(leases,name),value=fs.readFileSync(lease);fs.unlinkSync(lease);fs.writeFileSync(lease,value,{mode:0o600,flag:'wx'});replacement=lease;if(phase==='active')throw new Error('callback failed');return 1;})).toThrow(/changed before (cleanup|exact release)/);expect(fs.existsSync(replacement)).toBe(true);fs.rmSync(dir,{recursive:true,force:true});}}); + test('exact release rejects active or decision inode substitution and overrides callback success or failure',()=>{for(const phase of ['active','decision'] as const){const dir=tmp();let replacement='',parked='';expect(()=>withLock(dir,()=>{const leases=path.join(dir,'.mutation-lock-leases'),name=fs.readdirSync(leases).find(value=>phase==='active'?value.includes('.active.'):value.endsWith('.decision'))!,lease=path.join(leases,name),value=fs.readFileSync(lease);parked=`${lease}.replaced`;fs.renameSync(lease,parked);fs.writeFileSync(lease,value,{mode:0o600,flag:'wx'});replacement=lease;if(phase==='active')throw new Error('callback failed');return 1;})).toThrow(/changed before (cleanup|exact release)/);expect(fs.existsSync(replacement)).toBe(true);expect(fs.existsSync(parked)).toBe(true);expect(fs.statSync(replacement).ino).not.toBe(fs.statSync(parked).ino);fs.rmSync(dir,{recursive:true,force:true});}}); test('concurrent stale-lock recovery never admits overlapping report writers',async()=>{const dir=tmp(),lock=path.join(dir,'.mutation-lock'),script=path.join(dir,'racer.ts');fs.mkdirSync(lock);fs.writeFileSync(path.join(lock,'owner.json'),JSON.stringify({pid:2147483647,token:'stale',createdAt:0,expiresAt:0}));fs.writeFileSync(script,`import fs from 'node:fs';import path from 'node:path';import {withLock} from ${JSON.stringify(path.resolve(import.meta.dir,'../lib/cso/state.ts'))};const dir=process.env.RACE_DIR!;try{await withLock(dir,async()=>{const active=path.join(dir,'active');try{fs.writeFileSync(active,String(process.pid),{flag:'wx'});}catch{fs.appendFileSync(path.join(dir,'overlap'),'yes\\n');}await Bun.sleep(50);try{if(fs.readFileSync(active,'utf8')===String(process.pid))fs.unlinkSync(active);}catch{}});}catch{}`);const children=Array.from({length:8},()=>spawn(process.execPath,[script],{env:{...process.env,RACE_DIR:dir},stdio:'ignore'}));await Promise.all(children.map(child=>new Promise(resolve=>child.on('close',()=>resolve()))));expect(fs.existsSync(path.join(dir,'overlap'))).toBe(false);}); test('concurrent recovery of one dead hard-link publication has a winner and no raw race errors',async()=>{ const dir=tmp(),barrier=path.join(dir,'barrier'),script=path.join(dir,'recovery-racer.ts');fs.mkdirSync(barrier);expect(withLock(dir,()=>1)).toBe(1); diff --git a/test/cso-windows-launcher.test.ts b/test/cso-windows-launcher.test.ts index 27a6d0889..c7a9a2852 100644 --- a/test/cso-windows-launcher.test.ts +++ b/test/cso-windows-launcher.test.ts @@ -84,6 +84,7 @@ describe('CSO native Windows build contract', () => { expect(build).toContain('-LockOutputPath "$(cygpath -w "$CSO_STAGE_LOCKER")"'); expect(build).toContain('-CoreSha256 "$CSO_CORE_SHA256"'); expect(build).toContain('-GitExePath "$(cygpath -aw "$CSO_WINDOWS_GIT")"'); + expect(build).toContain('CSO_PUBLISH_SHELL="$(cygpath -aw /usr/bin/bash.exe)"'); expect(build).not.toContain('launcher-windows.ts'); expect(fs.existsSync(path.join(ROOT, 'lib/cso/launcher-windows.ts'))).toBe(false); expect(msvc).toContain('Launch-VsDevShell.ps1'); diff --git a/test/provider-model-defaults.test.ts b/test/provider-model-defaults.test.ts index a99f6f130..a957d1486 100644 --- a/test/provider-model-defaults.test.ts +++ b/test/provider-model-defaults.test.ts @@ -127,33 +127,38 @@ describe('CSO producer provider policies', () => { }); test('passes only selected provider auth plus safe execution inputs and strips Docker credentials', () => { + const fixtureRoot = join(tmpdir(), 'gstack-provider-environment-policy'); + const state = join(fixtureRoot, 'state'); + const source = join(fixtureRoot, 'source'); + const helpers = join(fixtureRoot, 'helpers'); + const work = join(fixtureRoot, 'work'); const sourceEnv = { PATH: '/usr/bin', HOME: '/home/eval', LANG: 'C.UTF-8', LC_ALL: 'C.UTF-8', TZ: 'UTC', - GSTACK_HOME: '/state', GSTACK_SESSION_KIND: 'spawned', GSTACK_HEADLESS: '1', + GSTACK_HOME: state, GSTACK_SESSION_KIND: 'spawned', GSTACK_HEADLESS: '1', OPENAI_API_KEY: 'openai-auth', ANTHROPIC_API_KEY: 'anthropic-auth', CLAUDE_CODE_OAUTH_TOKEN: 'claude-auth', GEMINI_API_KEY: 'gemini-auth', GOOGLE_CLOUD_PROJECT: 'project', CSO_EVAL_PAID: '1', AWS_SECRET_ACCESS_KEY: 'unrelated', DOCKER_HOST: 'tcp://remote.example:2376', DOCKER_CONFIG: '/credentials', DOCKER_CERT_PATH: '/certs', }; - const safe = { PATH: '/usr/bin', HOME: '/home/eval', LANG: 'C.UTF-8', LC_ALL: 'C.UTF-8', TZ: 'UTC', GSTACK_HOME: '/state', GSTACK_SESSION_KIND: 'spawned', GSTACK_HEADLESS: '1' }; + const safe = { PATH: '/usr/bin', HOME: '/home/eval', LANG: 'C.UTF-8', LC_ALL: 'C.UTF-8', TZ: 'UTC', GSTACK_HOME: state, GSTACK_SESSION_KIND: 'spawned', GSTACK_HEADLESS: '1' }; expect(csoProducerChildEnvironment('gpt', sourceEnv)).toEqual({ ...safe, OPENAI_API_KEY: 'openai-auth' }); expect(csoProducerChildEnvironment('claude', sourceEnv)).toEqual({ ...safe, ANTHROPIC_API_KEY: 'anthropic-auth', CLAUDE_CODE_OAUTH_TOKEN: 'claude-auth' }); expect(csoProducerChildEnvironment('gemini', sourceEnv)).toEqual({ ...safe, GEMINI_API_KEY: 'gemini-auth', GOOGLE_CLOUD_PROJECT: 'project' }); - const helper = helperAt('/helpers'); - const producer = policy('/state', '/source', helper); - const common = { prompt: '', workdir: '/work', timeoutMs: 1, csoProducer: producer }; - const codexPaths = codexProducerPaths('/state'); + const helper = helperAt(helpers); + const producer = policy(state, source, helper); + const common = { prompt: '', workdir: work, timeoutMs: 1, csoProducer: producer }; + const codexPaths = codexProducerPaths(state); expect(codexExecEnvironment(common, sourceEnv)).toEqual({ - ...safe, HOME: codexPaths.home, GSTACK_HOME: '/state/cso-home', CODEX_HOME: codexPaths.home, OPENAI_API_KEY: 'openai-auth', + ...safe, HOME: codexPaths.home, GSTACK_HOME: join(state, 'cso-home'), CODEX_HOME: codexPaths.home, OPENAI_API_KEY: 'openai-auth', }); - const claudePaths = claudeProducerPaths('/state'); + const claudePaths = claudeProducerPaths(state); expect(claudeExecEnvironment(common, sourceEnv)).toEqual({ - ...safe, HOME: claudePaths.home, GSTACK_HOME: '/state/cso-home', + ...safe, HOME: claudePaths.home, GSTACK_HOME: join(state, 'cso-home'), ANTHROPIC_API_KEY: 'anthropic-auth', CLAUDE_CODE_OAUTH_TOKEN: 'claude-auth', CLAUDE_CODE_SUBPROCESS_ENV_SCRUB: '1', }); - const geminiPaths = geminiProducerPaths('/state'); + const geminiPaths = geminiProducerPaths(state); expect(geminiExecEnvironment(common, sourceEnv)).toEqual({ - ...safe, HOME: geminiPaths.home, GSTACK_HOME: '/state/cso-home', + ...safe, HOME: geminiPaths.home, GSTACK_HOME: join(state, 'cso-home'), GEMINI_API_KEY: 'gemini-auth', GOOGLE_API_KEY: 'gemini-auth', GOOGLE_CLOUD_PROJECT: 'project', GEMINI_SANDBOX: 'false', GEMINI_TELEMETRY_ENABLED: 'false', GEMINI_TELEMETRY_LOG_PROMPTS: 'false', GEMINI_CLI_TRUST_WORKSPACE: 'true', GEMINI_SYSTEM_MD: 'false', GEMINI_WRITE_SYSTEM_MD: 'false', @@ -169,12 +174,17 @@ describe('CSO producer provider policies', () => { }); test('preserves default provider environments and rejects producer escape-hatch arguments', () => { - const source = { PATH: '/bin', CSO_EVAL_PAID: '1', UNRELATED_SECRET: 'kept-by-default', GEMINI_API_KEY: 'key' }; - const common = { prompt: '', workdir: '/work', timeoutMs: 1 }; - const producer = policy('/state', '/source', helperAt('/helpers')); - expect(codexExecEnvironment(common, source)).toBe(source); - expect(claudeExecEnvironment(common, source)).toEqual({ ...source, GSTACK_HEADLESS: '1' }); - expect(geminiExecEnvironment(common, source)).toEqual({ ...source, GOOGLE_API_KEY: 'key' }); + const fixtureRoot = join(tmpdir(), 'gstack-provider-default-policy'); + const state = join(fixtureRoot, 'state'); + const source = join(fixtureRoot, 'source'); + const helpers = join(fixtureRoot, 'helpers'); + const work = join(fixtureRoot, 'work'); + const sourceEnv = { PATH: '/bin', CSO_EVAL_PAID: '1', UNRELATED_SECRET: 'kept-by-default', GEMINI_API_KEY: 'key' }; + const common = { prompt: '', workdir: work, timeoutMs: 1 }; + const producer = policy(state, source, helperAt(helpers)); + expect(codexExecEnvironment(common, sourceEnv)).toBe(sourceEnv); + expect(claudeExecEnvironment(common, sourceEnv)).toEqual({ ...sourceEnv, GSTACK_HEADLESS: '1' }); + expect(geminiExecEnvironment(common, sourceEnv)).toEqual({ ...sourceEnv, GOOGLE_API_KEY: 'key' }); for (const invoke of [ () => codexExecArgs({ ...common, csoProducer: producer, extraArgs: ['--unsafe'] }, 'model'), () => claudeExecArgs({ ...common, csoProducer: producer, extraArgs: ['--unsafe'] }, 'model'), diff --git a/test/test-free-shards.test.ts b/test/test-free-shards.test.ts index c807c9b0e..03ad1ea2d 100644 --- a/test/test-free-shards.test.ts +++ b/test/test-free-shards.test.ts @@ -119,6 +119,21 @@ describe('test-free-shards: Windows curation', () => { expect(reason.length).toBeGreaterThan(0); } }); + + test('excludes CSO containment suites while retaining portable image provisioning coverage', () => { + const containmentOnly = [ + 'test/cso-preparation-adversarial.test.ts', + 'test/cso-preparation-container.test.ts', + 'test/cso-preparation-executor.test.ts', + 'test/cso-verification-cleanup.test.ts', + 'test/cso-witness.test.ts', + ]; + const portable = 'test/cso-image-provisioning.test.ts'; + const result = curateWindowsSafe([...containmentOnly, portable], ROOT); + expect(result.safe).toEqual([portable]); + expect(result.excluded.map(({ file }) => file).sort()).toEqual(containmentOnly.sort()); + for (const { reason } of result.excluded) expect(reason).toMatch(/Linux|POSIX|Windows/); + }); }); describe('test-free-shards: sharding', () => {