Repository navigation
test: separate Linux attribution causes and guard-gap models #157
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
4d1ba69
test: separate Linux attribution causes and guard-gap models
abruption 0b7b933
docs: align Linux research overview with measured results
abruption f409327
test: enforce pinned Linux research build provenance
abruption 25d1d83
test: isolate Linux research build consumers in verified snapshots
abruption c7ce2e4
docs: record snapshot-based Linux research validation
abruption b19b3a0
test: refuse patched builds in pinned Linux predictions
abruption 080f0d9
ci: rerun Linux research gates for dependency changes
abruption ea724d3
test: bind Linux research fingerprints and success evidence
abruption File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| name: Linux attribution research gates | ||
|
|
||
| on: | ||
| pull_request: | ||
| paths: | ||
| - '.github/workflows/linux-gates-research.yml' | ||
| - 'package.json' | ||
| - 'package-lock.json' | ||
| - 'experimental/126-linux-gates/**' | ||
| - 'experimental/126-guard-liveness/**' | ||
| workflow_dispatch: | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| research-models-and-kernel: | ||
| name: Models and synthetic Linux lock observations | ||
| runs-on: ubuntu-24.04 | ||
| timeout-minutes: 10 | ||
| steps: | ||
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
| with: | ||
| persist-credentials: false | ||
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 | ||
| with: | ||
| node-version: 24 | ||
| # The import-graph parser uses the existing TypeScript dev dependency. | ||
| # Skip lifecycle scripts: synthetic snapshot tests do not load the | ||
| # product or a Codex binary and establish no unlock eligibility. | ||
| - run: npm ci --ignore-scripts --no-audit --no-fund | ||
| - run: >- | ||
| node --test experimental/126-linux-gates/*/*.test.mjs | ||
| experimental/126-guard-liveness/f2-linux-nondumpable/build.test.mjs | ||
| experimental/126-guard-liveness/f2-linux-nondumpable/build-binding.test.mjs | ||
| experimental/126-guard-liveness/f2-linux-nondumpable/build-snapshot.test.mjs | ||
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| /* | ||
| * Synthetic native holder named `codex` (#126 research, Linux gates G1). | ||
| * | ||
| * SYNTHETIC. This is not Codex and says nothing about a deployed Codex | ||
| * binary. It exists only so that the product's executable/argv checks see a | ||
| * native image whose argv[0] basename is `codex`, with no arguments. | ||
| * | ||
| * Behaviour mirrors ../../126-guard-liveness/f2-linux-nondumpable/codex: | ||
| * refuse root and differing real/effective UIDs, set and verify the requested | ||
| * dumpable value with prctl, take `.coordination.lock` then the thread lock | ||
| * with bounded nonblocking polling, release coordination, print one ready | ||
| * JSON line, then hold until stdin EOF (exit 0) or the lifetime (exit 3). | ||
| * | ||
| * Environment: FIXTURE_LOCK_PATH, FIXTURE_DUMPABLE (0 or 1), | ||
| * FIXTURE_LIFETIME_SECONDS (1-120, default 20). | ||
| * | ||
| * It sends no signal to any process. Build: cc -std=c11 -O1 -Wall -Wextra. | ||
| */ | ||
| #define _GNU_SOURCE | ||
| #include <errno.h> | ||
| #include <fcntl.h> | ||
| #include <limits.h> | ||
| #include <poll.h> | ||
| #include <stdio.h> | ||
| #include <stdlib.h> | ||
| #include <string.h> | ||
| #include <sys/file.h> | ||
| #include <time.h> | ||
| #include <unistd.h> | ||
| #ifdef __linux__ | ||
| #include <sys/prctl.h> | ||
| #endif | ||
|
|
||
| static double now_s(void) { | ||
| struct timespec ts; | ||
| clock_gettime(CLOCK_MONOTONIC, &ts); | ||
| return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9; | ||
| } | ||
|
|
||
| static int fail(const char *what) { | ||
| fprintf(stderr, "codex fixture: %s: %s\n", what, errno ? strerror(errno) : "invalid"); | ||
| return 1; | ||
| } | ||
|
|
||
| static int acquire(int fd, double deadline) { | ||
| double until = now_s() + 2.0; | ||
| if (until > deadline) until = deadline; | ||
| for (;;) { | ||
| if (flock(fd, LOCK_EX | LOCK_NB) == 0) return 0; | ||
| if (errno != EWOULDBLOCK && errno != EAGAIN && errno != EINTR) return -1; | ||
| if (now_s() >= until) { errno = ETIMEDOUT; return -1; } | ||
| struct timespec pause = { 0, 10 * 1000 * 1000 }; | ||
| nanosleep(&pause, NULL); | ||
| } | ||
| } | ||
|
|
||
| static long parse_long(const char *text, long lo, long hi, long fallback) { | ||
| if (!text) return fallback; | ||
| char *end = NULL; | ||
| errno = 0; | ||
| long value = strtol(text, &end, 10); | ||
| if (errno || !end || *end || end == text || value < lo || value > hi) return LONG_MIN; | ||
| return value; | ||
| } | ||
|
|
||
| int main(void) { | ||
| #ifndef __linux__ | ||
| errno = 0; | ||
| return fail("Linux only"); | ||
| #else | ||
| if (getuid() == 0 || getuid() != geteuid()) { errno = 0; return fail("unprivileged same-UID process required"); } | ||
| const char *lock_path = getenv("FIXTURE_LOCK_PATH"); | ||
| long want = parse_long(getenv("FIXTURE_DUMPABLE"), 0, 1, LONG_MIN); | ||
| long lifetime = parse_long(getenv("FIXTURE_LIFETIME_SECONDS"), 1, 120, 20); | ||
| if (!lock_path || lock_path[0] != '/' || want == LONG_MIN || lifetime == LONG_MIN) { errno = 0; return fail("environment"); } | ||
| double deadline = now_s() + (double)lifetime; | ||
| if (prctl(PR_SET_DUMPABLE, (unsigned long)want, 0UL, 0UL, 0UL) != 0) return fail("prctl(PR_SET_DUMPABLE)"); | ||
| int dumpable = prctl(PR_GET_DUMPABLE, 0UL, 0UL, 0UL, 0UL); | ||
| if (dumpable < 0) return fail("prctl(PR_GET_DUMPABLE)"); | ||
| if (dumpable != (int)want) { errno = 0; return fail("dumpable mismatch"); } | ||
|
|
||
| char guard_path[PATH_MAX]; | ||
| const char *slash = strrchr(lock_path, '/'); | ||
| size_t dir_len = (size_t)(slash - lock_path); | ||
| if (dir_len == 0 || dir_len + sizeof("/.coordination.lock") > sizeof(guard_path)) { errno = 0; return fail("lock path"); } | ||
| memcpy(guard_path, lock_path, dir_len); | ||
| memcpy(guard_path + dir_len, "/.coordination.lock", sizeof("/.coordination.lock")); | ||
|
|
||
| int guard = open(guard_path, O_RDONLY | O_CLOEXEC | O_NOFOLLOW); | ||
| if (guard < 0) return fail("open coordination"); | ||
| if (acquire(guard, deadline) != 0) { int saved = errno; close(guard); errno = saved; return fail("acquire coordination"); } | ||
| int thread = open(lock_path, O_RDWR | O_CLOEXEC | O_NOFOLLOW); | ||
| if (thread < 0) { int saved = errno; close(guard); errno = saved; return fail("open thread lock"); } | ||
| if (acquire(thread, deadline) != 0) { int saved = errno; close(thread); close(guard); errno = saved; return fail("acquire thread lock"); } | ||
| flock(guard, LOCK_UN); | ||
| close(guard); | ||
| printf("{\"ready\":true,\"pid\":%ld,\"fd\":%d,\"dumpable\":%d,\"uid\":%ld,\"euid\":%ld,\"lifetime_seconds\":%ld,\"fixture\":\"native_c_synthetic\"}\n", | ||
| (long)getpid(), thread, dumpable, (long)getuid(), (long)geteuid(), lifetime); | ||
| fflush(stdout); | ||
|
|
||
| int eof = 0; | ||
| char buffer[4096]; | ||
| while (!eof) { | ||
| double left = deadline - now_s(); | ||
| if (left <= 0) break; | ||
| struct pollfd p = { STDIN_FILENO, POLLIN, 0 }; | ||
| int ready = poll(&p, 1, (int)(left * 1000.0) + 1); | ||
| if (ready < 0 && errno == EINTR) continue; | ||
| if (ready < 0) break; | ||
| if (ready == 0) continue; | ||
| ssize_t count = read(STDIN_FILENO, buffer, sizeof(buffer)); | ||
| if (count == 0) eof = 1; | ||
| else if (count < 0 && errno != EINTR && errno != EAGAIN) break; | ||
| } | ||
| close(thread); | ||
| /* Exit 0 on stdin EOF, 3 when the lifetime ended first or stdin failed. */ | ||
| return eof ? 0 : 3; | ||
| #endif | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| #!/usr/bin/env node | ||
| // Synthetic Node holder named `codex` (#126 research, Linux gates G1). | ||
| // | ||
| // SYNTHETIC, not Codex. It is started as `<node> <this file>`, the one | ||
| // interpreter form the product's argv grammar accepts for its own test | ||
| // fixture (basename(argv[0]) is node, basename(argv[1]) is codex). Node cannot | ||
| // call prctl, so this holder never changes dumpability; the runner infers the | ||
| // inherited value from /proc ownership and records it as inferred. | ||
| // | ||
| // Takes `.coordination.lock` then the thread lock with bounded nonblocking | ||
| // polling, releases coordination, prints one ready JSON line and holds until | ||
| // stdin EOF (exit 0) or FIXTURE_LIFETIME_SECONDS (exit 3). Sends no signal. | ||
| import fs from 'node:fs' | ||
| import { createRequire } from 'node:module' | ||
| import path from 'node:path' | ||
| import process from 'node:process' | ||
| import { clearTimeout, setTimeout } from 'node:timers' | ||
|
|
||
| if (process.platform !== 'linux' || process.getuid() === 0 || process.getuid() !== process.geteuid()) { | ||
| throw new Error('fixture requires an unprivileged same-user Linux process') | ||
| } | ||
| const require = createRequire(process.env.FIXTURE_REQUIRE_FROM) | ||
| const { flockSync } = require('fs-ext-extra-prebuilt') | ||
| const lifetime = Number(process.env.FIXTURE_LIFETIME_SECONDS ?? '20') | ||
| if (!Number.isInteger(lifetime) || lifetime < 1 || lifetime > 120) throw new Error('FIXTURE_LIFETIME_SECONDS must be 1-120') | ||
| const sleeper = new Int32Array(new SharedArrayBuffer(4)) | ||
|
|
||
| function acquire(fd) { | ||
| const until = Date.now() + 2000 | ||
| while (true) { | ||
| try { | ||
| flockSync(fd, 'exnb') | ||
| return | ||
| } catch (error) { | ||
| if (error.code !== 'EAGAIN' && error.code !== 'EWOULDBLOCK') throw error | ||
| } | ||
| if (Date.now() > until) throw new Error('fixture lock acquisition timed out') | ||
| Atomics.wait(sleeper, 0, 0, 10) | ||
| } | ||
| } | ||
|
|
||
| const lockPath = process.env.FIXTURE_LOCK_PATH | ||
| const flags = fs.constants.O_CLOEXEC | fs.constants.O_NOFOLLOW | ||
| const guard = fs.openSync(path.join(path.dirname(lockPath), '.coordination.lock'), fs.constants.O_RDONLY | flags) | ||
| acquire(guard) | ||
| const thread = fs.openSync(lockPath, fs.constants.O_RDWR | flags) | ||
| acquire(thread) | ||
| flockSync(guard, 'un') | ||
| fs.closeSync(guard) | ||
| process.stdout.write(JSON.stringify({ ready: true, pid: process.pid, fd: thread, dumpable: null, | ||
| uid: process.getuid(), euid: process.geteuid(), lifetime_seconds: lifetime, fixture: 'node_synthetic' }) + '\n') | ||
| const timer = setTimeout(() => { fs.closeSync(thread); process.exitCode = 3; process.stdin.destroy() }, lifetime * 1000) | ||
| process.stdin.on('end', () => { clearTimeout(timer); fs.closeSync(thread); process.exitCode = 0 }) | ||
| process.stdin.resume() |
48 changes: 48 additions & 0 deletions
48
experimental/126-linux-gates/g1-identity-visibility/argv-check.mjs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| #!/usr/bin/env node | ||
| // Cross-platform check of the G1 argv predictions against the product's | ||
| // pure `reconcileArguments` (#126 research). | ||
| // | ||
| // G1_BUILD=<build.mjs output> node argv-check.mjs | ||
| // | ||
| // It imports only the build's process.js (verified against its manifest | ||
| // first) and calls one pure function with each variant's expected kernel | ||
| // argv and an equal `ps -o args=` string. No process is inspected or | ||
| // signalled. This checks the argv part of the prediction only; real ps/proc | ||
| // behaviour is measured by run.mjs on Linux. | ||
| import { log } from 'node:console' | ||
| import fs from 'node:fs' | ||
| import path from 'node:path' | ||
| import process from 'node:process' | ||
| import { pathToFileURL } from 'node:url' | ||
| import { reachableModules, verifyBuild } from '../../126-guard-liveness/f2-linux-nondumpable/build.mjs' | ||
| import { assertPinnedUnpatchedBuild, expectedArgv, PRODUCT_ARGV_ERROR, VARIANT_ORDER, VARIANTS } from './variants.mjs' | ||
| import { createPinnedBuildSnapshot } from './build-input.mjs' | ||
|
|
||
| if (!process.env.G1_BUILD) throw new Error('G1_BUILD is required') | ||
| const build = fs.realpathSync(process.env.G1_BUILD) | ||
| const startup = verifyBuild(build) | ||
| if (!startup.ok) throw new Error(`build does not match its manifest: ${startup.faults.join(',')}`) | ||
| const snapshot = createPinnedBuildSnapshot(build, startup.manifestSha256) | ||
| const { verified } = snapshot | ||
| assertPinnedUnpatchedBuild(verified) | ||
| const dist = snapshot.dist | ||
| if (reachableModules(dist, ['process.js']).includes('unlock.js')) throw new Error('unlock.js reachable') | ||
| const { reconcileArguments } = await import(pathToFileURL(path.join(dist, 'process.js')).href) | ||
|
|
||
| // Example paths with the same shape as the run host's. | ||
| const paths = { scriptPath: '/srv/research/126-guard-liveness/f2-linux-nondumpable/codex', | ||
| nativePath: '/srv/scratch/codex-126-g1-x/native/codex', nodeExec: '/usr/bin/node', | ||
| nodeFixture: '/srv/research/126-linux-gates/fixtures/node/codex' } | ||
| const results = VARIANT_ORDER.map((variant) => { | ||
| const argv = expectedArgv(variant, paths) | ||
| const evidence = reconcileArguments(argv.join(' '), { status: 'present', argv }) | ||
| const expectedError = VARIANTS[variant].argvClean ? undefined : PRODUCT_ARGV_ERROR | ||
| return { variant, argv, evidence, ok: evidence.error === expectedError && evidence.isSharedService === false && | ||
| (expectedError ? evidence.arguments === null : evidence.arguments === argv.join(' ')) } | ||
| }) | ||
| // The product accepts only node/nodejs as an interpreter basename. | ||
| const renamedNode = reconcileArguments('/usr/bin/node24 /x/codex', { status: 'present', argv: ['/usr/bin/node24', '/x/codex'] }) | ||
| results.push({ variant: 'node_renamed_binary', evidence: renamedNode, ok: renamedNode.error === PRODUCT_ARGV_ERROR }) | ||
| const ok = results.every((r) => r.ok) | ||
| log(JSON.stringify({ build: { manifestSha256: verified.manifestSha256, commit: verified.source.commit }, ok, results }, null, 2)) | ||
| process.exitCode = ok ? 0 : 1 |
12 changes: 12 additions & 0 deletions
12
experimental/126-linux-gates/g1-identity-visibility/build-input.mjs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| // Keep the source pin and startup manifest binding on the copied bytes used | ||
| // by every G1 build-code consumer, rather than on a mutable input directory. | ||
| import path from 'node:path' | ||
| import { createBuildSnapshot } from '../../126-guard-liveness/f2-linux-nondumpable/build-snapshot.mjs' | ||
| import { assertPinnedUnpatchedBuild } from './variants.mjs' | ||
|
|
||
| export function createPinnedBuildSnapshot(build, expectedManifestSha256) { | ||
| const snapshot = createBuildSnapshot(build, expectedManifestSha256) | ||
| if (!snapshot.verified.ok) throw new Error(`G1_snapshot_unverified:${path.basename(snapshot.directory)}:${snapshot.verified.faults.join(',')}`) | ||
| assertPinnedUnpatchedBuild(snapshot.verified) | ||
| return snapshot | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.