From 9deceef17ffa2cc062b8fce8c5785b4303cf2ad4 Mon Sep 17 00:00:00 2001 From: Kyle Brown Date: Thu, 3 Sep 2026 00:30:43 +0000 Subject: [PATCH 1/2] build(plugin): add Python-free musl native artifacts --- .github/workflows/native-musl.yml | 112 +++++++++++++++++++++ .github/workflows/native-unix.yml | 10 +- plugins/codex-security/native/README.md | 6 +- plugins/codex-security/native/binding.mts | 3 +- plugins/codex-security/native/build.mts | 4 +- plugins/codex-security/native/check.mts | 47 +++++++-- plugins/codex-security/native/platform.mts | 12 +++ 7 files changed, 179 insertions(+), 15 deletions(-) create mode 100644 .github/workflows/native-musl.yml create mode 100644 plugins/codex-security/native/platform.mts diff --git a/.github/workflows/native-musl.yml b/.github/workflows/native-musl.yml new file mode 100644 index 000000000..98d241d4a --- /dev/null +++ b/.github/workflows/native-musl.yml @@ -0,0 +1,112 @@ +name: native-musl + +on: + push: + branches: [main] + pull_request: + types: [opened, reopened, synchronize] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + primitives: + name: primitives / linux-${{ matrix.arch }}-musl + runs-on: ${{ matrix.runner }} + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + include: + - runner: ubuntu-24.04 + arch: x64 + rust-image: rust@sha256:83b16fd204613a557527e81ef4f87fd513229187c49635e3c7f96b08dfa43c33 + node22-image: node@sha256:133cdce957f50f47236d6d926592fb1db7a120ac3c33191e611b60dfab63e324 + node20-image: node@sha256:2ffec31a58e85fbcd575c544a3584f6f4d128779e6b856153a04366b8dd01bb0 + - runner: ubuntu-24.04-arm + arch: arm64 + rust-image: rust@sha256:ec97f172f73f5ec381a238dad6ddd99e0e9c14fd3709a05beb41ccdf50eb666a + node22-image: node@sha256:9090fa64b73b14ab658d907e7916afcb1633e4bdd5134f9b1be4a21a6c56f0b8 + node20-image: node@sha256:9a3cb11e78b5082ce7d22bb5113ed5bb7ac4c20195c05f3981e765e21e31c29c + defaults: + run: + shell: bash + working-directory: plugins/codex-security/native + env: + RUST_IMAGE: ${{ matrix.rust-image }} + NODE22_IMAGE: ${{ matrix.node22-image }} + NODE20_IMAGE: ${{ matrix.node20-image }} + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Set up pnpm + uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 + with: + package_json_file: package.json + cache: true + cache_dependency_path: sdk/typescript/pnpm-lock.yaml + - name: Set up Node.js for TypeScript tools + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + with: + node-version: "22.13.0" + - name: Compile TypeScript tools + working-directory: . + run: | + pnpm --dir sdk/typescript install --frozen-lockfile + pnpm --dir sdk/typescript run build:ci + - name: Fetch locked Rust dependencies + run: | + rustup toolchain install 1.97.1 --profile minimal --component rustfmt --component clippy + cargo fetch --locked + - name: Assemble pinned musl compiler image + run: | + docker build --network none \ + --build-arg RUST_IMAGE="$RUST_IMAGE" \ + --build-arg NODE_IMAGE="$NODE22_IMAGE" \ + --tag codex-security-native-musl - <<'DOCKERFILE' + ARG RUST_IMAGE + ARG NODE_IMAGE + FROM ${NODE_IMAGE} AS node-runtime + FROM ${RUST_IMAGE} + COPY --from=node-runtime /usr/local/bin/node /usr/local/bin/node + DOCKERFILE + - name: Build and check musl artifact + run: | + mkdir -p "$RUNNER_TEMP/native-musl-cargo" + docker run --rm --network none --user "$(id -u):$(id -g)" \ + --volume "$PWD:/source" \ + --volume "$RUNNER_TEMP/native-musl-cargo:/cargo" \ + --volume "${CARGO_HOME:-$HOME/.cargo}/registry:/cargo/registry:ro" \ + --env CARGO_HOME=/cargo \ + --env CARGO_NET_OFFLINE=true \ + --env RUSTUP_TOOLCHAIN=1.97.1 \ + --workdir /source codex-security-native-musl /bin/sh -euc ' + test -z "$(command -v python || true)" + test -z "$(command -v python3 || true)" + node build.mjs + node check.mjs + ' + - name: Verify the same artifact with Node.js 22 and 20 + run: | + for native_image in "$NODE22_IMAGE" "$NODE20_IMAGE"; do + docker run --rm --network none --user "$(id -u):$(id -g)" \ + --volume "$PWD:/source:ro" \ + --workdir /source "$native_image" /bin/sh -euc ' + test -z "$(command -v python || true)" + test -z "$(command -v python3 || true)" + PATH= /usr/local/bin/node proof.mjs + ' + done + - name: Upload verified native artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: native-linux-${{ matrix.arch }}-musl-${{ github.sha }} + path: plugins/codex-security/native/dist/linux-${{ matrix.arch }}-musl/unix.node + if-no-files-found: error + retention-days: 7 diff --git a/.github/workflows/native-unix.yml b/.github/workflows/native-unix.yml index f75495b0e..fef17c62b 100644 --- a/.github/workflows/native-unix.yml +++ b/.github/workflows/native-unix.yml @@ -15,7 +15,7 @@ concurrency: jobs: primitives: - name: primitives / ${{ matrix.platform }}-${{ matrix.arch }} + name: primitives / ${{ matrix.artifact }} runs-on: ${{ matrix.runner }} timeout-minutes: 30 strategy: @@ -25,17 +25,21 @@ jobs: - runner: ubuntu-24.04 platform: linux arch: x64 + artifact: linux-x64-gnu image: quay.io/pypa/manylinux_2_28_x86_64@sha256:0536c364004fa2a3c5041120b6fe35d84fc5bfe31f04c6a6304f13eac4a67b63 - runner: ubuntu-24.04-arm platform: linux arch: arm64 + artifact: linux-arm64-gnu image: quay.io/pypa/manylinux_2_28_aarch64@sha256:1676a4f178dc6cf8a1d1e3b7e1d71fca891a466ebde72a33880ccda8af3383d8 - runner: macos-15-intel platform: darwin arch: x64 + artifact: darwin-x64 - runner: macos-15 platform: darwin arch: arm64 + artifact: darwin-arm64 defaults: run: shell: bash @@ -121,7 +125,7 @@ jobs: - name: Upload verified native artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: native-${{ matrix.platform }}-${{ matrix.arch }}-${{ github.sha }} - path: plugins/codex-security/native/dist/${{ matrix.platform }}-${{ matrix.arch }}/unix.node + name: native-${{ matrix.artifact }}-${{ github.sha }} + path: plugins/codex-security/native/dist/${{ matrix.artifact }}/unix.node if-no-files-found: error retention-days: 7 diff --git a/plugins/codex-security/native/README.md b/plugins/codex-security/native/README.md index 216e3c3df..b793d4c95 100644 --- a/plugins/codex-security/native/README.md +++ b/plugins/codex-security/native/README.md @@ -23,16 +23,18 @@ The proof runs without Python. It checks directory replacement, byte paths, unre node plugins/codex-security/native/proof.mjs python3 plugins/codex-security/scripts ``` -Build outputs stay under ignored `target` and `dist` directories. Source, Cargo registry, and compiler paths are remapped before compilation; actual payload bytes are checked for private paths. Before an artifact is uploaded, run: +Build outputs stay under ignored `target` and `dist` directories. Linux output directories include the C runtime: `linux-x64-gnu`, `linux-arm64-gnu`, `linux-x64-musl`, and `linux-arm64-musl`. The dependency-free `platform.mts` helper distinguishes glibc from musl using the Node diagnostic report header, without a subprocess. macOS and Windows retain their platform and architecture directories. Source, Cargo registry, and compiler paths are remapped before compilation; actual payload bytes are checked for private paths. Before an artifact is uploaded, run: ```sh node plugins/codex-security/native/check.mjs ``` -Linux artifacts must import no glibc version newer than 2.28. macOS artifacts must declare a deployment target of 11.0 or earlier. A build from a newer Linux workstation can pass the behavioral proof and still fail this distribution check. +GNU Linux artifacts must import no glibc version newer than 2.28. Musl artifacts must be ELF images for the current architecture, depend on that architecture's musl library, and import no glibc symbols. Musl has no glibc-style symbol version floor, so its runtime compatibility also requires the load proofs below. macOS artifacts must declare a deployment target of 11.0 or earlier. A build from a newer GNU Linux workstation can pass the behavioral proof and still fail this distribution check. The `native-unix` workflow builds Linux artifacts in digest-pinned manylinux 2.28 images. It mounts the pinned Rust toolchain and fetched Cargo registry, builds offline, and blocks Python commands during compilation. macOS builds set `MACOSX_DEPLOYMENT_TARGET=11.0`. CI verifies separate x64 and arm64 artifacts on both platforms using Node 20.0.0 and 22.13.0. These artifacts are inputs to the later universal-package gate. +The `native-musl` workflow uses native x64 and arm64 Ubuntu workers with digest-pinned Rust 1.97.1 Alpine compiler images. Musl builds disable static CRT linkage so Node can load the shared library. After the ELF and private-path checks, each unchanged artifact runs the full proof in pinned Node 20.0.0 Alpine 3.17 and Node 22.13.0 Alpine 3.21 images, with musl 1.2.3 and 1.2.5 respectively. Compilation uses the locked registry offline; runtime containers mount only the source and artifact read-only. Python is absent, and proof processes receive an empty `PATH`. + Windows uses `windows-binding.mts` and the same Rust crate. `WindowsHandle` owns a non-inheritable Win32 handle; explicit `close()` and garbage collection release it. Handles never cross into Node's CRT descriptor table. Paths and returned names are UTF-16LE buffers without a NUL terminator, preserving lone surrogates. Volume identities and file positions are decimal strings; file IDs retain all 128 bits in a buffer. The binding exposes synchronous file and directory creation, attributes and reparse tags, identity and final/opened names, read/write/seek/size/EOF/flush, exact-handle rename and deletion, and byte-range locking. Calls return numeric Windows errors. Buffer ranges, path encoding, and 64-bit arguments are checked before FFI calls. Overlapped handles are unsupported because pending operations could retain native buffers beyond the call. Path authorization, ancestor traversal, and reparse-point policy remain the caller's responsibility. diff --git a/plugins/codex-security/native/binding.mts b/plugins/codex-security/native/binding.mts index 4dac94153..bd6f40526 100644 --- a/plugins/codex-security/native/binding.mts +++ b/plugins/codex-security/native/binding.mts @@ -2,9 +2,10 @@ import { createRequire } from "node:module"; import { readSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; +import { nativeTarget } from "./platform.mjs"; export const root = dirname(fileURLToPath(import.meta.url)); -export const output = join(root, "dist", `${process.platform}-${process.arch}`); +export const output = join(root, "dist", nativeTarget); export const binaryPath = join( output, process.platform === "win32" ? "windows.node" : "unix.node", diff --git a/plugins/codex-security/native/build.mts b/plugins/codex-security/native/build.mts index ba14ef327..4b4b61663 100644 --- a/plugins/codex-security/native/build.mts +++ b/plugins/codex-security/native/build.mts @@ -4,6 +4,7 @@ import { homedir } from "node:os"; import { join, resolve } from "node:path"; import { binaryPath, output, root } from "./binding.mjs"; import { checkPrivatePaths } from "./check.mjs"; +import { libc, nativeTarget } from "./platform.mjs"; let windowsTarget: string | undefined; if (process.platform === "win32") { @@ -32,6 +33,7 @@ const inheritedFlags = const flags = [ ...inheritedFlags, ...(windowsTarget === undefined ? [] : ["-C", "target-feature=+crt-static"]), + ...(libc === "musl" ? ["-C", "target-feature=-crt-static"] : []), `--remap-path-prefix=${root}=codex-security-native`, `--remap-path-prefix=${cargoHome}=cargo`, `--remap-path-prefix=${sysroot}=rust-toolchain`, @@ -61,4 +63,4 @@ const library = join( checkPrivatePaths(readFileSync(library), [root, cargoHome, sysroot, target]); mkdirSync(output, { recursive: true }); copyFileSync(library, binaryPath); -console.log(`Built ${process.platform}-${process.arch} Node-API 8 primitives.`); +console.log(`Built ${nativeTarget} Node-API 8 primitives.`); diff --git a/plugins/codex-security/native/check.mts b/plugins/codex-security/native/check.mts index acc949121..c695366e8 100644 --- a/plugins/codex-security/native/check.mts +++ b/plugins/codex-security/native/check.mts @@ -4,6 +4,7 @@ import { readFileSync } from "node:fs"; import { resolve } from "node:path"; import { pathToFileURL } from "node:url"; import { binaryPath } from "./binding.mjs"; +import { libc } from "./platform.mjs"; export function checkPrivatePaths( bytes: Buffer, @@ -52,15 +53,44 @@ if ( const required = [...versions.matchAll(/\bGLIBC_(\d+(?:\.\d+)*)/gu)].map( (match) => match[1]!, ); - if ( - required.length === 0 || - required.some((version) => versionAfter(version, "2.28")) - ) { - throw new Error( - "Native payload requires glibc newer than 2.28, or has no inspectable glibc requirements.", - ); + if (libc === "musl") { + const architecture = + process.arch === "x64" + ? { machine: 62, name: "x86_64" } + : process.arch === "arm64" + ? { machine: 183, name: "aarch64" } + : undefined; + const dynamic = execFileSync("readelf", ["--dynamic", binaryPath], { + encoding: "utf8", + }); + const dependencies = [ + ...dynamic.matchAll(/\(NEEDED\)[^\n]*\[([^\]]+)\]/gu), + ].map((match) => match[1]!); + if ( + architecture === undefined || + bytes.toString("latin1", 0, 4) !== "\x7fELF" || + bytes[4] !== 2 || + bytes[5] !== 1 || + bytes.readUInt16LE(18) !== architecture.machine || + !dependencies.includes(`libc.musl-${architecture.name}.so.1`) || + /\bGLIBC_/u.test(versions) + ) { + throw new Error( + "Native payload is not a musl ELF image for this architecture, or imports glibc.", + ); + } + floor = "musl; Node 20 and 22 load proofs required"; + } else { + if ( + required.length === 0 || + required.some((version) => versionAfter(version, "2.28")) + ) { + throw new Error( + "Native payload requires glibc newer than 2.28, or has no inspectable glibc requirements.", + ); + } + floor = "glibc 2.28"; } - floor = "glibc 2.28"; } else if (process.platform === "darwin") { const commands = execFileSync("otool", ["-l", binaryPath], { encoding: "utf8", @@ -98,6 +128,7 @@ if ( JSON.stringify({ platform: process.platform, arch: process.arch, + libc, nodeApi: 8, floor, bytes: bytes.length, diff --git a/plugins/codex-security/native/platform.mts b/plugins/codex-security/native/platform.mts new file mode 100644 index 000000000..7b5902b55 --- /dev/null +++ b/plugins/codex-security/native/platform.mts @@ -0,0 +1,12 @@ +export const libc = + process.platform !== "linux" + ? undefined + : ( + process.report.getReport() as { + header: { glibcVersionRuntime?: string }; + } + ).header.glibcVersionRuntime === undefined + ? "musl" + : "gnu"; + +export const nativeTarget = `${process.platform}-${process.arch}${libc === undefined ? "" : `-${libc}`}`; From e2cff42204bc5d2112e1ce5c40c44980be2c08c0 Mon Sep 17 00:00:00 2001 From: Kyle Brown Date: Thu, 3 Sep 2026 01:00:10 +0000 Subject: [PATCH 2/2] fix(plugin): attribute musl version checks to their libraries --- plugins/codex-security/native/README.md | 2 +- plugins/codex-security/native/check.mts | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/plugins/codex-security/native/README.md b/plugins/codex-security/native/README.md index b793d4c95..50b5a01e9 100644 --- a/plugins/codex-security/native/README.md +++ b/plugins/codex-security/native/README.md @@ -29,7 +29,7 @@ Build outputs stay under ignored `target` and `dist` directories. Linux output d node plugins/codex-security/native/check.mjs ``` -GNU Linux artifacts must import no glibc version newer than 2.28. Musl artifacts must be ELF images for the current architecture, depend on that architecture's musl library, and import no glibc symbols. Musl has no glibc-style symbol version floor, so its runtime compatibility also requires the load proofs below. macOS artifacts must declare a deployment target of 11.0 or earlier. A build from a newer GNU Linux workstation can pass the behavioral proof and still fail this distribution check. +GNU Linux artifacts must import no glibc version newer than 2.28. Musl artifacts must be ELF images for the current architecture, depend on that architecture's musl library, and have no version requirements from glibc. GCC's own `GLIBC_2.0` compatibility exports are attributed to `libgcc_s.so.1`, not the C library. Musl has no glibc-style symbol version floor, so its runtime compatibility also requires the load proofs below. macOS artifacts must declare a deployment target of 11.0 or earlier. A build from a newer GNU Linux workstation can pass the behavioral proof and still fail this distribution check. The `native-unix` workflow builds Linux artifacts in digest-pinned manylinux 2.28 images. It mounts the pinned Rust toolchain and fetched Cargo registry, builds offline, and blocks Python commands during compilation. macOS builds set `MACOSX_DEPLOYMENT_TARGET=11.0`. CI verifies separate x64 and arm64 artifacts on both platforms using Node 20.0.0 and 22.13.0. These artifacts are inputs to the later universal-package gate. diff --git a/plugins/codex-security/native/check.mts b/plugins/codex-security/native/check.mts index c695366e8..d63c6b3e1 100644 --- a/plugins/codex-security/native/check.mts +++ b/plugins/codex-security/native/check.mts @@ -66,6 +66,13 @@ if ( const dependencies = [ ...dynamic.matchAll(/\(NEEDED\)[^\n]*\[([^\]]+)\]/gu), ].map((match) => match[1]!); + // libgcc uses GLIBC_2.0 for its own compatibility exports on aarch64. + const requiresGlibc = [ + ...versions.matchAll(/\bFile:\s+(\S+)([\s\S]*?)(?=\bFile:|$)/gu), + ].some( + (match) => + match[1] !== "libgcc_s.so.1" && /\bName:\s+GLIBC_/u.test(match[2]!), + ); if ( architecture === undefined || bytes.toString("latin1", 0, 4) !== "\x7fELF" || @@ -73,7 +80,7 @@ if ( bytes[5] !== 1 || bytes.readUInt16LE(18) !== architecture.machine || !dependencies.includes(`libc.musl-${architecture.name}.so.1`) || - /\bGLIBC_/u.test(versions) + requiresGlibc ) { throw new Error( "Native payload is not a musl ELF image for this architecture, or imports glibc.",