Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions .github/workflows/native-musl.yml
Original file line number Diff line number Diff line change
@@ -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
10 changes: 7 additions & 3 deletions .github/workflows/native-unix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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
6 changes: 4 additions & 2 deletions plugins/codex-security/native/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 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.

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.
Expand Down
3 changes: 2 additions & 1 deletion plugins/codex-security/native/binding.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 3 additions & 1 deletion plugins/codex-security/native/build.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down Expand Up @@ -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`,
Expand Down Expand Up @@ -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.`);
52 changes: 45 additions & 7 deletions plugins/codex-security/native/check.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -52,15 +53,51 @@ 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]!);
// 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" ||
bytes[4] !== 2 ||
bytes[5] !== 1 ||
bytes.readUInt16LE(18) !== architecture.machine ||
!dependencies.includes(`libc.musl-${architecture.name}.so.1`) ||
requiresGlibc
) {
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",
Expand Down Expand Up @@ -98,6 +135,7 @@ if (
JSON.stringify({
platform: process.platform,
arch: process.arch,
libc,
nodeApi: 8,
floor,
bytes: bytes.length,
Expand Down
12 changes: 12 additions & 0 deletions plugins/codex-security/native/platform.mts
Original file line number Diff line number Diff line change
@@ -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}`}`;
Loading