diff --git a/docs/native-runtime/README.md b/docs/native-runtime/README.md index 188f971..2b27db7 100644 --- a/docs/native-runtime/README.md +++ b/docs/native-runtime/README.md @@ -177,6 +177,109 @@ and Node pins remain unchanged. Dawn, DevTools, ICU and Ink checkouts are presen V8 and WebRTC dependency checkouts are still absent. Free disk at this checkpoint was246.2GiB. This is a resumable preparation checkpoint, not a completed build. +A subsequent bounded resume has checked out WebRTC at +`6f37672d358475cd17544121a12494da454d85fb` and V8 at +`fedd021b538607860f9c56e5c193ad8dcd4f2bb1`, each verified against its own Git +root. A read-only snapshot found all nineteen required patch targets present +and tracked-clean, with no patch imports yet. The post-import verifier's real +negative control rejected all nineteen unpatched targets as expected. Ignored +evidence: `artifacts/native-build/patch-readiness.json` and +`patch-import-negative-control.json`. At this checkpoint synchronization is +still active on VK-GL-CTS and WebGPU CTS; this was not full dependency completion, +patch applicability, compilation or native acceptance. + +That same resume subsequently terminated with exit1 (`stopped:false`): Windows +antivirus rejected CIPD package `chromium/third_party/updater/chrome_win_x86`, +instance `NuXBEfKu8082d55WUbs6_3P0puTPCI4A1Df4BnQ1Z40C`, during fetch/install. +This is a measured security-tool rejection, not a timeout or a completed sync. +No antivirus exception, disabling, bypass or package retry was performed. +Compilation remains unverified. The existing Git sources can be independently +rechecked for the targeted Python patch import, which neither installs that +package nor proves complete dependency readiness. + +The subsequent official patch import used normal host Python after the isolated +embedded interpreter failed before entering patch code. The official import +then terminated with exit1: the excluded `fontconfig` directory existed without +its own Git root, so its target fell back to Chromium during parallel import. +Its Git-Am invocation failed against Chromium's active import and overwrote +Chromium's upstream patch references. Chromium has272 commits relative to the +saved pristine baseline and no remaining Git-Am directory, but its overwritten +references count only268 commits. This discrepancy is a real failed gate, not +an accepted successful import. Preserve the original log and verification +failure; verify the complete ordered series against the saved baseline before +considering an explicit, evidence-backed reference repair. No reset, abort, +patch replay or automatic reference repair has been performed at this checkpoint. + +The ordered-series verification subsequently proved all272 Chromium patches and +all eighteen other required repositories. Each of the thirteen modified parent +Gitlinks was checked against its registered, independently verified child's +actual HEAD, own Git root and stage0 mode160000 entry; arbitrary source changes +remain rejected. Only the two corrupted upstream references remained invalid. +An explicit atomic compare-and-swap repair then restored both references to +`2c592105bbcd9490a9894df48d0fe59b2c512651`, requiring their observed old value +`619d23e60466caaa2b5f35e16f15a255b5c09ffa`. Chromium HEAD stayed +`b33e21917a1ce686ca6134585cdaca88eaadf9f6`. A first transaction failed due to CRLF +stdin; a synthetic/no-op protocol check proved literal LF bytes necessary, and +the corrected start/prepare/commit transaction exited0. Original failures and +both transaction journals remain retained. No patch replay, source reset or +dependency staging occurred. The fresh post-repair verifier then passed all +nineteen required targets with the thirteen explicitly matched Gitlinks, and +`git apply --check` for the backport exited0 against Electron-patched Chromium. +Evidence: `artifacts/native-build/patch-import-verification-post-cas.json` and +the retained recovery journal. This proves combined source applicability, not +a compiled or runtime-verified fix. + +The verified backport was then applied with `git apply --index` and committed as +`6f8b80fef13bfdc9d9c70df567ebcad722d70467`, parent +`b33e21917a1ce686ca6134585cdaca88eaadf9f6`, in the ignored native source checkout. +Exactly the sixteen expected files were staged; no dependency Gitlinks were +included. Thirteen structural source assertions passed, the index is clean and +both official upstream references remain unchanged. The single-commit mailbox +`artifacts/native-build/backport-applied.patch` has SHA256 +`070ece6ffbd5723aa8bb0c7e94f855c931be2b2277a6e69b5908cabb5881343b`; +`backport-applied-journal.json` records provenance and exact files. This source +checkpoint remains **uncompiled and runtime-unverified**. The shipped project +runtime is unchanged; source assertions cannot substitute for the native gates. + +### Patch readiness before compilation + +ATL/MFC is required for the native toolchain, not for the Python patch import +or a combined-source text check. After dependency synchronization is terminal, +verify each required Windows repository's own Git root, pinned revision and clean +working tree; `git -C rev-parse HEAD` alone can accidentally resolve +the parent Chromium repository when a dependency has not yet checked out. + +The pinned official DEPS hook invokes the following action from +`artifacts/native-build/electron44`: + +```powershell +python3 src/electron/script/apply_all_patches.py src/electron/patches/config.json +git -C src apply --check C:/Users/karl/Safetynet/docs/native-runtime/chromium-152-webrtc-binder-backport.patch +``` + +This identifies the upstream action; **do not rerun its unfiltered twenty-target +config on Windows** after the measured ancestor collision. For a fresh checkout, +first run `scripts/native-patch-preflight.mjs`, explicitly verify the Linux-only +fontconfig exclusion, and use a reviewed config containing only the nineteen +required targets with the same official Python importer. Never replay imports +over an already patched tree. The hook does not invoke compilation. +Its exit code is insufficient evidence: missing repositories are skipped with +warnings. Verify imports and upstream references for each required target in +`patches/config.json`. In this pinned checkout, nineteen targets are required: +despite their names, `squirrel.mac` and `ReactiveObjC` are synchronized under +`process_deps`, without a macOS condition. Only `fontconfig` is excluded by its +Chromium `checkout_linux` condition. Do not infer exclusions from directory names. +Save each repository's pre-import HEAD. Both the legacy and checkout-specific +upstream references must resolve to that commit, and it must be an ancestor of +the resulting HEAD. Compare imported commit counts and ordered subjects with +the repository's `.patches` list. Reference existence alone is insufficient: +the importer writes references before `git am`, which can subsequently fail. +Do not blindly rerun the hook after partial failure or reset conflicting hunks. +Seven existing Chromium patches touch backport paths, so the earlier pristine +check does not prove compatibility with Electron's patches. Full `runhooks`, GN, +compilation and native transport acceptance remain separate gates. The backport +addresses denial stability; it does not provide WebRTC content inspection. + ### Missing ATL/MFC: explicit administrator step Inspection found the existing installer at diff --git a/scripts/native-patch-preflight.mjs b/scripts/native-patch-preflight.mjs new file mode 100644 index 0000000..9165167 --- /dev/null +++ b/scripts/native-patch-preflight.mjs @@ -0,0 +1,87 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import {execFile} from 'node:child_process'; +import {promisify} from 'node:util'; +import {fileURLToPath} from 'node:url'; + +const execute=promisify(execFile); +const FONTCONFIG='src/third_party/fontconfig/src'; +function inside(root,target){const relative=path.relative(root,target);return relative===''||(!relative.startsWith(`..${path.sep}`)&&relative!=='..'&&!path.isAbsolute(relative));} +async function contained(root,target){ + const resolved=await fs.realpath(target); + if(!inside(root,resolved))throw Error('path-outside-checkout'); + return resolved; +} +async function nearestContained(root,target){ + if(!inside(root,target))throw Error('path-outside-checkout'); + let current=target; + while(true){try{return await contained(root,current);}catch(error){if(error.code!=='ENOENT')throw error;const parent=path.dirname(current);if(parent===current)throw error;current=parent;}} +} +async function git(repo,args){ + const {stdout}=await execute('git',['--no-optional-locks','-C',repo,...args],{env:{...process.env,GIT_OPTIONAL_LOCKS:'0'},timeout:120000,maxBuffer:8*1024*1024,windowsHide:true}); + return stdout.trim(); +} + +// Read-only prerequisite check. In particular, an existing directory is not +// proof of an independent checkout: Git silently searches ancestor repositories. +export async function nativePatchPreflight({checkoutRoot,configPath,excludeWindowsFontconfig=false,platform=process.platform}){ + const result={scope:'Read-only patch target preflight; not patch import or build proof.',passed:false,required:[],excluded:[],errors:[]}; + try{ + const root=await fs.realpath(checkoutRoot);result.checkoutRoot=root; + const config=await contained(root,path.resolve(configPath)); + const targets=JSON.parse(await fs.readFile(config,'utf8')); + if(!Array.isArray(targets)||targets.length===0||targets.length>64)throw Error('invalid-patch-config'); + const seen=new Set(); + for(const target of targets){ + const row={repo:target?.repo,patchDir:target?.patch_dir,errors:[]}; + try{ + if(!target||typeof target.repo!=='string'||typeof target.patch_dir!=='string'||!target.repo||!target.patch_dir||path.isAbsolute(target.repo)||path.isAbsolute(target.patch_dir)||seen.has(target.repo))throw Error('invalid-patch-target'); + seen.add(target.repo); + const repo=path.resolve(root,target.repo),patchDir=path.resolve(root,target.patch_dir); + await nearestContained(root,repo); + const patches=await contained(root,patchDir),list=await contained(root,path.join(patches,'.patches')); + // Electron's official reader keeps every list line, including blanks. + const text=await fs.readFile(list,'utf8');const names=text.split(/\r?\n/);if(names.at(-1)==='')names.pop(); + if(!names.length||names.some(name=>!name||path.isAbsolute(name)))throw Error('invalid-patch-list'); + for(const name of names){const file=await contained(root,path.resolve(patches,name));if(!inside(patches,file))throw Error('patch-outside-patch-directory');} + row.patchCount=names.length; + if(target.repo===FONTCONFIG&&excludeWindowsFontconfig){ + if(platform!=='win32')throw Error('fontconfig-exclusion-requires-windows'); + const deps=await fs.readFile(await contained(root,path.join(root,'src/DEPS')),'utf8'); + if(!/['"]src\/third_party\/fontconfig\/src['"]\s*:\s*\{[^}]*['"]condition['"]\s*:\s*['"]checkout_linux['"]/s.test(deps))throw Error('fontconfig-exclusion-condition-unverified'); + row.reason='Explicit Windows exclusion: Chromium DEPS checkout_linux';result.excluded.push(row);continue; + } + const own=await contained(root,repo); + try{await contained(root,path.join(own,'.git'));}catch(error){if(error.code==='ENOENT')throw Error('missing-own-git-marker',{cause:error});throw error;} + const top=await git(own,['rev-parse','--show-toplevel']); + if(await fs.realpath(top)!==own)throw Error('ancestor-git-root'); + const gitDir=await git(own,['rev-parse','--absolute-git-dir']);await contained(root,gitDir); + const am=path.resolve(own,await git(own,['rev-parse','--git-path','rebase-apply'])); + await nearestContained(root,am); + try{await fs.stat(am);throw Error('git-am-in-progress');}catch(error){if(error.code!=='ENOENT')throw error;} + row.head=await git(own,['rev-parse','--verify','HEAD']); + row.trackedClean=(await git(own,['status','--porcelain=v1','--untracked-files=no']))===''; + if(!row.trackedClean)throw Error('tracked-worktree-not-clean'); + }catch(error){row.errors.push(error.code==='ENOENT'?'missing-required-path':error.message||'preflight-failed');} + result.required.push(row); + } + result.errors=result.required.filter(row=>row.errors.length).map(row=>({repo:row.repo,errors:row.errors})); + result.passed=result.errors.length===0; + }catch(error){result.errors.push({errors:[error.code==='ENOENT'?'missing-required-path':error.message||'preflight-failed']});} + result.requiredCount=result.required.length;result.excludedCount=result.excluded.length; + return result; +} + +if(process.argv[1]&&path.resolve(process.argv[1])===fileURLToPath(import.meta.url)){ + const args=process.argv.slice(2);const allowed=new Set(['--checkout-root','--config','--exclude-windows-fontconfig']);let checkoutRoot,configPath,excludeWindowsFontconfig=false; + try{ + for(let i=0;ifs.rm(root,{recursive:true,force:true})); + const config=path.join(root,'config.json'),patchDir=path.join(root,'patches');await fs.mkdir(patchDir);await fs.writeFile(path.join(patchDir,'.patches'),'one.patch\n');await fs.writeFile(path.join(patchDir,'one.patch'),'synthetic'); + await fs.writeFile(config,JSON.stringify([{repo:'src',patch_dir:'patches'}])); + return {root,config,options:{checkoutRoot:root,configPath:config}}; +} +async function init(repo){await fs.mkdir(repo,{recursive:true});await exec('git',['init',repo]);await fs.writeFile(path.join(repo,'tracked.txt'),'baseline');await exec('git',['-C',repo,'add','tracked.txt']);await exec('git',['-C',repo,'-c','user.name=Fixture','-c','user.email=fixture@example.invalid','-c','commit.gpgsign=false','commit','-m','baseline']);} + +test('accepts an independent clean checkout and leaves HEAD unchanged',async t=>{ + const f=await fixture(t),repo=path.join(f.root,'src');await init(repo);const before=(await exec('git',['-C',repo,'rev-parse','HEAD'])).stdout.trim();const r=await nativePatchPreflight(f.options);assert.equal(r.passed,true);assert.equal(r.required[0].head,before);assert.equal(r.required[0].patchCount,1);assert.equal((await exec('git',['-C',repo,'rev-parse','HEAD'])).stdout.trim(),before); +}); +test('rejects existing child directory whose Git resolves to ancestor',async t=>{ + const f=await fixture(t);await init(f.root);await fs.mkdir(path.join(f.root,'src'));const r=await nativePatchPreflight(f.options);assert.equal(r.passed,false);assert.deepEqual(r.errors[0].errors,['missing-own-git-marker']); +}); +test('missing required target fails even when patch directory exists',async t=>{ + const f=await fixture(t);const r=await nativePatchPreflight(f.options);assert.equal(r.passed,false);assert.equal(r.requiredCount,1);assert.deepEqual(r.errors[0].errors,['missing-required-path']); +}); +test('rejects changed tracked files',async t=>{ + const f=await fixture(t);await init(path.join(f.root,'src'));await fs.writeFile(path.join(f.root,'src/tracked.txt'),'changed');const r=await nativePatchPreflight(f.options);assert.equal(r.passed,false);assert.deepEqual(r.errors[0].errors,['tracked-worktree-not-clean']); +}); +test('rejects residual git-am state despite clean tracked files',async t=>{ + const f=await fixture(t),repo=path.join(f.root,'src');await init(repo);await fs.mkdir(path.join(repo,'.git/rebase-apply'));const r=await nativePatchPreflight(f.options);assert.equal(r.passed,false);assert.deepEqual(r.errors[0].errors,['git-am-in-progress']);assert.equal((await exec('git',['-C',repo,'status','--porcelain=v1','--untracked-files=no'])).stdout.trim(),''); +}); +test('rejects checkout symlink or junction escaping supplied root',async t=>{ + const f=await fixture(t),outside=await fs.mkdtemp(path.join(os.tmpdir(),'safetynet-preflight-outside-'));t.after(()=>fs.rm(outside,{recursive:true,force:true}));await init(outside);await fs.symlink(outside,path.join(f.root,'src'),process.platform==='win32'?'junction':'dir');const r=await nativePatchPreflight(f.options);assert.equal(r.passed,false);assert.deepEqual(r.errors[0].errors,['path-outside-checkout']); +}); +test('rejects escaping config and missing listed patch',async t=>{ + const f=await fixture(t);await init(path.join(f.root,'src'));await fs.rm(path.join(f.root,'patches/one.patch'));assert.equal((await nativePatchPreflight(f.options)).passed,false);const r=await nativePatchPreflight({...f.options,configPath:path.join(f.root,'../outside.json')});assert.equal(r.passed,false); +}); +test('fontconfig exclusion is opt-in, Windows-only, exact path and DEPS condition checked',async t=>{ + const f=await fixture(t);await init(path.join(f.root,'src'));await fs.mkdir(path.join(f.root,'src/third_party/fontconfig/src'),{recursive:true});await fs.writeFile(path.join(f.root,'src/DEPS'),"deps = {'src/third_party/fontconfig/src': {'url':'fixture','condition':'checkout_linux'}}");await fs.writeFile(f.config,JSON.stringify([{repo:'src/third_party/fontconfig/src',patch_dir:'patches'}])); + assert.equal((await nativePatchPreflight(f.options)).passed,false); + const options={...f.options,excludeWindowsFontconfig:true,platform:'win32'};const good=await nativePatchPreflight(options);assert.equal(good.passed,true);assert.equal(good.excludedCount,1);assert.equal(good.requiredCount,0); + assert.equal((await nativePatchPreflight({...options,platform:'linux'})).passed,false); + await fs.writeFile(path.join(f.root,'src/DEPS'),"deps={'src/third_party/fontconfig/src':{'condition':'checkout_win'}}");assert.equal((await nativePatchPreflight(options)).passed,false); +}); +test('rejects listed patch traversal outside its patch directory',async t=>{ + const f=await fixture(t);await init(path.join(f.root,'src'));await fs.writeFile(path.join(f.root,'other.patch'),'synthetic');await fs.writeFile(path.join(f.root,'patches/.patches'),'../other.patch\n');const r=await nativePatchPreflight(f.options);assert.equal(r.passed,false);assert.deepEqual(r.errors[0].errors,['patch-outside-patch-directory']); +}); +test('CLI requires explicit roots and returns failing status',async()=>{ + await assert.rejects(exec(process.execPath,['scripts/native-patch-preflight.mjs']),error=>error.code===1&&JSON.parse(error.stdout).passed===false); +});