also looking at this
fix(chrome): ship NSS's runtime providers and gate on a real render
#246
3 files
+114
-2
Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.
| @@ -26,14 +26,18 @@ async function run(name: string, args: string[], timeout = 30_000): Promise<stri | ||
| 26 | 26 | return output |
| 27 | 27 | } |
| 28 | 28 | |
| 29 | /** Run with nothing inherited but HOME and a PATH without the package, so only its own layout resolves libraries. */ | |
| 30 | async function runClean(name: string, args: string[], timeout = 60_000): Promise<string> { | |
| 29 | /** | |
| 30 | * Run with nothing inherited but HOME and a PATH without the package, so only | |
| 31 | * its own layout resolves libraries. LD_DEBUG=files makes the dynamic loader | |
| 32 | * report each library it initialises (on stderr) without changing a lookup. | |
| 33 | */ | |
| 34 | async function runClean(name: string, args: string[], timeout = 60_000): Promise<{ stdout: string, stderr: string }> { | |
| 31 | 35 | const stdout = join(artifacts, `${name}.stdout.log`) |
| 32 | 36 | const stderr = join(artifacts, `${name}.stderr.log`) |
| 33 | const child = Bun.spawn(args, { cwd: root, env: { HOME: root, PATH: '/usr/bin:/bin' }, stdout: Bun.file(stdout), stderr: Bun.file(stderr), signal: AbortSignal.timeout(timeout) }) | |
| 37 | const child = Bun.spawn(args, { cwd: root, env: { HOME: root, PATH: '/usr/bin:/bin', LD_DEBUG: 'files' }, stdout: Bun.file(stdout), stderr: Bun.file(stderr), signal: AbortSignal.timeout(timeout) }) | |
| 34 | 38 | const code = await child.exited |
| 35 | const output = await Bun.file(stdout).text() | |
| 36 | assert.equal(code, 0, `${name}: ${output}${await Bun.file(stderr).text()}`) | |
| 39 | const output = { stdout: await Bun.file(stdout).text(), stderr: await Bun.file(stderr).text() } | |
| 40 | assert.equal(code, 0, `${name}: ${output.stdout}${output.stderr.slice(-4000)}`) | |
| 37 | 41 | return output |
| 38 | 42 | } |
| 39 | 43 | |
| @@ -77,8 +81,22 @@ try { | ||
| 77 | 81 | const page = join(root, 'render-profile') |
| 78 | 82 | await mkdir(page) |
| 79 | 83 | const rendered = await runClean('chrome-render', [entry, '--headless=new', '--disable-gpu', '--no-first-run', `--user-data-dir=${page}`, '--dump-dom', 'data:text/html,<h1 id="pantry">rendered by the installed package</h1>']) |
| 80 | assert.match(rendered, /<h1 id="pantry">rendered by the installed package<\/h1>/, 'Chrome must render with its own libraries and its sandbox') | |
| 84 | assert.match(rendered.stdout, /<h1 id="pantry">rendered by the installed package<\/h1>/, 'Chrome must render with its own libraries and its sandbox') | |
| 81 | 85 | proof.rendered = true |
| 86 | ||
| 87 | // A render alone is not proof: a host NSS (GitHub's image ships one with its | |
| 88 | // own Chrome) answers NSS's dlopen fallback for a provider the package left | |
| 89 | // out, and the page renders anyway. Require the loader to have initialised | |
| 90 | // softokn and its crypto provider from the installed package itself. | |
| 91 | const initialised = [...rendered.stderr.matchAll(/calling init: (\S+)/g)].map(match => match[1]!) | |
| 92 | const nss = initialised.filter(path => /\/lib(?:softokn3|freeblpriv3)\.so$/.test(path)) | |
| 93 | proof.nssModules = [...new Set(nss)] | |
| 94 | for (const module of ['libsoftokn3.so', 'libfreeblpriv3.so']) { | |
| 95 | const loaded = nss.filter(path => path.endsWith(`/${module}`)) | |
| 96 | assert(loaded.length > 0, `Chrome never initialised ${module}`) | |
| 97 | for (const path of loaded) | |
| 98 | assert(path.startsWith(`${root}/pantry/`), `${module} came from ${path}, not the installed package`) | |
| 99 | } | |
| 82 | 100 | proof.passed = true |
| 83 | 101 | } |
| 84 | 102 | finally { |
| @@ -25,7 +25,10 @@ test('the Linux release gate renders a page with the installed package, not just | ||
| 25 | 25 | expect(gate).toContain('--dump-dom') |
| 26 | 26 | // A clean environment: only HOME and a PATH without the package, so an |
| 27 | 27 | // inherited LD_LIBRARY_PATH cannot supply the library the package lacks. |
| 28 | expect(gate).toContain(`env: { HOME: root, PATH: '/usr/bin:/bin' }`) | |
| 28 | expect(gate).toContain(`env: { HOME: root, PATH: '/usr/bin:/bin', LD_DEBUG: 'files' }`) | |
| 29 | // And a host NSS cannot stand in: the providers must load from the package. | |
| 30 | expect(gate).toContain('calling init:') | |
| 31 | expect(gate).toContain('not the installed package') | |
| 29 | 32 | // The sandbox stays on. |
| 30 | 33 | expect(gate).not.toContain("'--no-sandbox'") |
| 31 | 34 | expect(gate).toContain('userns,') |