Skip to content

build: link libutil statically on FreeBSD - #40532

Merged
dylan-conway merged 2 commits into
mainfrom
farm/1a59658d/freebsd-static-libutil
Aug 27, 2026
Merged

dylan-conway merged 2 commits into
mainfrom
farm/1a59658d/freebsd-static-libutil

Conversation

@robobun

@robobun robobun commented Aug 26, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • The bun-freebsd-x64 release binary fails to load on FreeBSD 15: ld-elf.so.1: Shared object "libutil.so.9" not found, required by "bun" (Bun 1.4 binary unable to running on FreeBSD #40530). readelf -d on the 1.4.0 release binary shows a NEEDED entry for libutil.so.9.
  • FreeBSD builds target the 14.3 sysroot (FREEBSD_VERSION_DEFAULT in scripts/build/config.ts) and link -lutil (scripts/build/bun.ts:99). FreeBSD 15.0 bumped libutil's soname to libutil.so.10 and ObsoleteFiles.inc removed libutil.so.9 from base, so a fresh 15.x install cannot satisfy the dependency.

Fix

  • Replace -lutil with -l:libutil.a on the FreeBSD link line. The static archive ships in base.txz (/usr/lib/libutil.a), so the sysroot already has it.
  • The only symbol bun imports from libutil is openpty (used by src/runtime/api/bun/Terminal.rs). With the static link it resolves at link time and the NEEDED entry disappears, so one binary runs on 14.x and 15.x.
  • Every other library on the FreeBSD link line (libc.so.7, libthr.so.3, libm.so.5, libkvm.so.7, libprocstat.so.1, libelf.so.2, libexecinfo.so.1, libcxxrt.so.1, libc++.so.1) kept its soname across the 15.0 bump, so libutil is the single blocker.
  • Verified: the generated FreeBSD build.ninja link edge now carries -l:libutil.a and no -lutil, and a FreeBSD-target link probe with lld shows the dynamic link produces NEEDED libutil.so.9 + undefined openpty while the static link produces no libutil NEEDED entry and a defined openpty.

Background

  • -l:name tells the linker to search for that exact file name, which forces the static archive even when libutil.so exists next to it. The build already uses this idiom for libatomic on Linux.
  • No runtime test can exercise this: it is a cross-link configuration for the FreeBSD target, and CI runners cannot execute FreeBSD binaries. Verification is at the ELF level (above).
Notes

Evidence for the soname bump, from freebsd-src:

  • releng/14.3 lib/libutil/Makefile: SHLIB_MAJOR= 9
  • releng/15.1 lib/libutil/Makefile: SHLIB_MAJOR= 10
  • releng/15.1 ObsoleteFiles.inc lists OLD_LIBS+=lib/libutil.so.9

Release binary check (bun-v1.4.0 bun-freebsd-x64.zip):

$ readelf -d bun | grep NEEDED
 libc.so.7 libthr.so.3 libm.so.5 libutil.so.9 libc++.so.1 libcxxrt.so.1

Only openpty in the dynamic symbol table matches libutil's API set, so static linking pulls in one small object.

Link probe (lld, --target=x86_64-unknown-freebsd14.3): an executable that references openpty linked with -lutil gets NEEDED libutil.so.9 and UND openpty; linked with -l:libutil.a it gets no libutil NEEDED entry and a defined openpty symbol.

Also checked: scripts/build typecheck failures (bunx tsc -p scripts/build/tsconfig.json) are identical with and without this change (14 pre-existing errors in ci.ts, jsonByteClass.ts, rust-lto-fix-cli.ts, xmlByteClass.ts). The native-FreeBSD build path is unaffected: /usr/lib/libutil.a is part of base there too.


no test proof · iteration 1 · build/CI scripts only; test-proof not applicable

FreeBSD 15 bumped libutil's soname from .so.9 to .so.10 and removed
libutil.so.9 from base. A binary built against the 14.3 sysroot with
-lutil fails to load on 15.x with:

  ld-elf.so.1: Shared object "libutil.so.9" not found, required by "bun"

The only symbol bun imports from libutil is openpty. Link the static
archive instead (-l:libutil.a, same idiom as libatomic on Linux) so the
NEEDED entry disappears and one binary runs on 14.x and 15.x. Every
other library on the FreeBSD link line kept its soname across the 15.0
bump.

Fixes #40530
@coderabbitai

coderabbitai Bot commented Aug 26, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

  • Run on-demand review

On-demand reviews are free for the next 25 days. After that, they cost $0.25 per reviewed file.

Or wait 13 minutes for your next included review.

View limit details

Limit details: You’ve used all 5 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9c2513e5-0a43-436e-ae17-4c2b172a46bf

📥 Commits

Reviewing files that changed from the base of the PR and between 06820dc and 95c70c1.

📒 Files selected for processing (1)
  • scripts/build/bun.ts

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

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM — one-token link-line change following an existing pattern.

What was reviewed:

  • -l:filename is already used in this function for libatomic.a (line 76), and the FreeBSD build links with lld (both cross and native since FreeBSD 13), which supports the syntax.
  • The sysroot extraction hint in config.ts pulls ./usr/lib from base.txz, which includes libutil.a, so the cross build has the archive.
  • Considered whether this belongs in workarounds.ts — it doesn't: there's no upstream fix to wait for, static linking is the permanent binary-compat decision, so no expectedToBeFixed predicate applies.
Extended reasoning...

Overview

This PR changes one argument on the FreeBSD link line in scripts/build/bun.ts::systemLibs(), replacing -lutil with -l:libutil.a, and updates the adjacent comment to explain why (libutil's soname bumped .so.9 → .so.10 between FreeBSD 14.x and 15.0, so a dynamic NEEDED libutil.so.9 entry from the 14.3 sysroot fails on 15.x; issue #40530). Only openpty is imported from libutil, so static linking pulls in a single small object.

Security risks

None. This is a build-system link-mode selection for a system library on FreeBSD. No user input is parsed, no network or auth surface is touched, and the linked code (openpty) is unchanged — only how the symbol is resolved at link time.

Level of scrutiny

Low. The change is a single flag substitution that follows an established idiom already present a few lines above in the same function (-l:libatomic.a on Linux). scripts/build/CLAUDE.md explicitly names systemLibs() as one of the intended per-platform branch points, and the change satisfies its "explicit over implicit — every decision traceable to a greppable line with a comment explaining why" goal. The -l: syntax is valid for lld, which is both the cross linker and FreeBSD's native system linker. The 14.3 sysroot is an extracted base.txz whose ./usr/lib is pulled in by the documented setup command in config.ts, and FreeBSD base ships libutil.a there.

Other factors

I weighed whether this should register a self-obsoleting entry in workarounds.ts per the build-system conventions. It should not: that registry is for temporary toolchain/OS bugs with an expectedToBeFixed predicate, whereas this is a permanent binary-compatibility decision with no future condition under which reverting to dynamic linking becomes desirable. No CODEOWNERS entry covers scripts/build/. The PR timeline shows no outstanding reviewer objections. The lack of an automated test is acceptable here — this is cross-link configuration for a target CI cannot execute, and the observable effect (absence of a NEEDED libutil.so.9 entry) was verified at the ELF level as described.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code review found no issues

No high-confidence issues detected in this change.

@dylan-conway

Copy link
Copy Markdown
Member

curious how much larger the binary gets

@dylan-conway
dylan-conway merged commit 2c0284e into main Aug 27, 2026
10 checks passed
@dylan-conway
dylan-conway deleted the farm/1a59658d/freebsd-static-libutil branch August 27, 2026 04:57
@robobun

robobun commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator Author

Effectively nothing. The link pulls two archive members: pty.o (openpty and forkpty, the only symbols bun references) and login_tty.o (forkpty calls it). Compiled from the releng/14.3 sources at -O2, pty.o is 483 bytes of text and login_tty.o is about 150 bytes, with zero data and bss. Everything they call (posix_openpt, grantpt, unlockpt, setsid, ioctl) stays dynamic in libc.so.7. So the binary grows by under 1 KB, which disappears into section alignment on a binary this size.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants