Skip to content

CI: two Linux x64 lanes with the stack protector, _FORTIFY_SOURCE, stack clash protection and zeroed locals - #865

Merged
dylan-conway merged 2 commits into
mainfrom
claude/hardened-prebuilt
Oct 21, 2026
Merged

dylan-conway merged 2 commits into
mainfrom
claude/hardened-prebuilt

Conversation

@dylan-conway

@dylan-conway dylan-conway commented Oct 21, 2026 •

Copy link
Copy Markdown
Member

What this does

Adds two prebuilt WebKits, Linux x64 glibc only. Each is an existing lane with ICU and WebKit compiled with these flags added, and nothing else changed:

-fstack-protector-strong -D_FORTIFY_SOURCE=2 -fstack-clash-protection -ftrivial-auto-var-init=zero -ftrivial-auto-var-init-max-size=4096
new lane is this lane plus the flags JavaScriptCore tests
bun-webkit-linux-amd64-hardened bun-webkit-linux-amd64 no
bun-webkit-linux-amd64-lto-hardened bun-webkit-linux-amd64-lto yes
  • The flags are HARDENING in lanes.mjs, passed to Dockerfile as HARDENING_FLAGS. It is empty for every other lane.
  • They go in CFLAGS and CXXFLAGS, so ICU gets them too.
  • None of them changes a type's layout or a calling convention, so the ABI is that of the lane each is made from.
  • _FORTIFY_SOURCE is 2 because the lanes build against glibc 2.31, which has no level 3.
  • After the link, the build fails unless jsc imports __stack_chk_fail and at least one checked libc function (__memcpy_chk and the like). It reads jsc and not the archives because an LTO lane's archives are bitcode, which does not name __stack_chk_fail yet.
  • The -lto-hardened lane runs the JavaScriptCore tests, because nothing else runs these builds.

What stays the same

lanes.mjs --json gives the same settings for the 45 existing lanes as on main, apart from the new empty HARDENING_FLAGS on the Linux glibc ones. The toolchain image tags are the same, so no image is rebuilt.

Testing

With clang 23.1.1, for x86_64-unknown-linux-gnu:

  • All five flags are accepted under -Werror -Wunused-command-line-argument.
  • Each shows up in the IR (sspstrong, "probe-stack"="inline-asm", a memset or a store of zero), so it is there before LTO runs.
  • A 4096 byte array is zeroed. A 16384 byte one is not, and its frame is probed a page at a time.

The check on jsc was run on a small program built four ways. It passes with all the flags and fails without the stack protector, without _FORTIFY_SOURCE, and without both.

The lanes themselves are built and tested by this pull request's CI.

@dylan-conway dylan-conway changed the title CI: Linux glibc lanes with the stack protector, _FORTIFY_SOURCE, stack clash protection and zeroed locals CI: two Linux x64 lanes with the stack protector, _FORTIFY_SOURCE, stack clash protection and zeroed locals Oct 21, 2026
@coderabbitai

coderabbitai Bot commented Oct 21, 2026

Copy link
Copy Markdown

Review in Change Stack →

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I reviewed this PR and didn't find any bugs. Since it adds two lanes to every main push and PR and makes the release wait on them, a human should still weigh that cost and the choice of flags.

What was reviewed:

  • lanes.mjs: the two variants, the amd64-only lane list, the lto-hardened test entry and the HARDENING_FLAGS arg; imageRef drops global ARGs that base does not declare, so the toolchain image tags are indeed unchanged.
  • Dockerfile: the flags are appended to the lane-stage CFLAGS/CXXFLAGS after the arch stage sets them, so ICU and WebKit both get them; the post-link check's regex matches __stack_chk_fail and the __*_chk fortify imports, and an empty match list still fails the lane rather than passing it; the toolchain is clang 23, which accepts all five flags.
  • Checked Source/cmake for an existing -fstack-protector/_FORTIFY_SOURCE setting that would conflict — none. The _FORTIFY_SOURCE #warning in unoptimized CMake probes and the SIGABRT-on-overflow behaviour were examined and are the intended effect of the flags, not defects.
Extended reasoning...

The change touches only CI configuration: lanes.mjs (a HARDENING flag string, two new Linux x64 glibc variants, one added to the tested set), the Dockerfile (a lane-only ARG appended to CFLAGS/CXXFLAGS and a post-link symbol check), and the matching CLAUDE.md prose. It touches no runtime code and no auth, crypto or input-handling surface; the hardening flags are compile-time and do not change ABI. The code matches the PR description and the repository's stated conventions (lanes defined only in lanes.mjs, base stage untouched, image hash unchanged). Deferring rather than approving because the inherited CODEOWNERS entry covers /.github, and because the change adds two 60-90 minute builds plus a test leg that every main push and PR now waits on and that gate the release, which is a project decision rather than a correctness question.

@github-actions

Copy link
Copy Markdown

Preview build of ac133d7: autobuild-preview-pr-865-ac133d7b

@dylan-conway
dylan-conway merged commit 7d0c8a7 into main Oct 21, 2026
55 checks passed
dylan-conway added a commit to oven-sh/bun that referenced this pull request Oct 21, 2026
…ORTIFY_SOURCE, stack clash protection and zeroed locals (#46317)

### What does this PR do?

Adds a build option, `--hardened=on`, for Linux x64 glibc release
builds. It is off by default and no profile or CI step turns it on.

With it on, everything in the binary is compiled with:

```
-fstack-protector-strong -D_FORTIFY_SOURCE=2 -fstack-clash-protection -ftrivial-auto-var-init=zero -ftrivial-auto-var-init-max-size=4096
```

| part of the binary | how it gets the flags |
| --- | --- |
| Bun's C and C++, and the vendored dependencies | `hardeningFlags` in
`flags.ts`, in place of `-fno-stack-protector` |
| JavaScriptCore, WTF, bmalloc and ICU | the `-hardened` WebKit
prebuilts of oven-sh/WebKit#865 |
| JavaScriptCore with `--webkit=local` | the same `hardeningFlags` |
| Rust | `-Zstack-protector=strong` |

Rust needs only that one. rustc's stack probes are already inline on
this target (`"stack-probes": {"kind": "inline"}` in the target spec).

| configuration | WebKit prebuilt |
| --- | --- |
| `--profile=release --hardened=on` |
`bun-webkit-linux-amd64-lto-hardened` |
| `--profile=release --hardened=on --lto=off` |
`bun-webkit-linux-amd64-hardened` |

Any other combination is a configure error, because there is no prebuilt
for it: another OS, arm64, musl, a debug build, `asan`, `assertions` or
`fuzzilli`.

### WebKit version

`WEBKIT_VERSION` moves from `d0b16ad0ce2b` to `7d0c8a7d0958`, the first
commit with the two prebuilts:

- oven-sh/WebKit#861
- oven-sh/WebKit#865

### How did you verify your code works?

CI builds no hardened configuration, so it says nothing about the option
itself.

- `build.ninja` is byte for byte the same with and without this change
for `release`, `debug`, `release-asan` and `release-assertions`.
- The two configurations above ask for the tarballs in the table.
- `--hardened=on` with `--profile=debug`, `--profile=release-asan`,
`--profile=release-assertions`, `--fuzzilli=on` or `--abi=musl` fails at
configure with the message and hint.
- In the hardened graph all five flags are on the same 1192 lines of
`build.ninja`, and `-fno-stack-protector` is on none.
- `-Zstack-protector=strong` is on 212 of the 270 Rust units. The other
58 are proc macros, build scripts and what those depend on, which run on
the host during the build.

`--profile=release --hardened=on`, which is LTO with
`bun-webkit-linux-amd64-lto-hardened` as CI built it. The tarball is the
one from oven-sh/WebKit#865's last run, whose source tree is the same as
the pin's.

- It builds and links with no failed step, Bun's own C++ under `-Werror`
included.
- Every binary check passes.
- The binary imports `__stack_chk_fail` and 20 checked libc functions
(`__memcpy_chk`, `__vsnprintf_chk`, `__longjmp_chk`, ...).
- Functions that call `__stack_chk_fail`, from a disassembly of
`bun-profile`. A build of this branch without the option has none in
92,682 functions.

| | functions | with a canary |
| --- | --- | --- |
| JavaScriptCore | 16,276 | 10,055 |
| WTF | 4,914 | 1,975 |
| ICU | 3,852 | 1,079 |
| Bun's C++ | 7,547 | 4,948 |
| Rust | 39,900 | 23,288 |
| the rest (C dependencies and others) | 20,433 | 6,974 |

- It runs a script with a hot loop, recursion until the stack overflows,
`Bun.serve`, `fetch`, `Intl`, a regular expression, `Bun.CryptoHasher`,
gzip and `WebAssembly.validate`.
- It passes `test/js/bun/util/inspect.test.js`,
`test/js/bun/ffi/cc.test.ts`, `test/js/bun/sqlite/sqlite.test.js`,
`test/js/node/zlib/zlib.test.js`, `test/js/web/fetch/fetch.test.ts`,
`test/js/bun/image/image-adversarial.test.ts`,
`test/js/web/workers/worker.test.ts` and
`test/js/bun/http/serve.test.ts`.

| | without the option | `--hardened=on` |
| --- | --- | --- |
| `bun`, bytes | 107,534,504 | 111,004,416 |
| `.text`, bytes | 81,936,053 | 85,404,741 |

`--profile=release --hardened=on --lto=off --webkit=local`, with
JavaScriptCore built from the branch of oven-sh/WebKit#865, also builds
with no failed step and passes every binary check, the script and the
same test files. All 715 of JavaScriptCore's compile commands have the
five flags.

Not done yet: nothing has been linked with
`bun-webkit-linux-amd64-hardened`, the prebuilt for `--lto=off`. Speed
has not been measured.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant