Guard native patch targets and document verified WebRTC backport preparation
This commit is contained in:
1 parent
09bf616d98
commit
7925633527
3 files changed
+241
No files matched your search
@@ -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 <directory> 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
|
||||
|
||||
@@ -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;i<args.length;i++){
|
||||
if(!allowed.has(args[i]))throw Error('invalid-cli-argument');
|
||||
if(args[i]==='--exclude-windows-fontconfig'){excludeWindowsFontconfig=true;continue;}
|
||||
const value=args[++i];if(!value||value.startsWith('--'))throw Error('missing-cli-value');
|
||||
if(args[i-1]==='--checkout-root')checkoutRoot=value;else configPath=value;
|
||||
}
|
||||
if(!checkoutRoot||!configPath)throw Error('checkout-root-and-config-required');
|
||||
const result=await nativePatchPreflight({checkoutRoot,configPath,excludeWindowsFontconfig});console.log(JSON.stringify(result,null,2));process.exitCode=result.passed?0:1;
|
||||
}catch(error){console.log(JSON.stringify({passed:false,errors:[error.message]}));process.exitCode=1;}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import {execFile} from 'node:child_process';
|
||||
import {promisify} from 'node:util';
|
||||
import {nativePatchPreflight} from '../scripts/native-patch-preflight.mjs';
|
||||
const exec=promisify(execFile);
|
||||
async function fixture(t){
|
||||
const root=await fs.mkdtemp(path.join(os.tmpdir(),'safetynet-native-preflight-'));t.after(()=>fs.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','[email protected]','-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);
|
||||
});
|
||||
Reference in new issue
Block a user