Skip to content

link: remove -z,now (BIND_NOW) from CC toolchain extldflags#4578

Merged
fmeum merged 8 commits intobazel-contrib:masterfrom
hunshcn:fix-bind-now-4377
Mar 31, 2026
Merged

link: remove -z,now (BIND_NOW) from CC toolchain extldflags#4578
fmeum merged 8 commits intobazel-contrib:masterfrom
hunshcn:fix-bind-now-4377

Conversation

@hunshcn
Copy link
Copy Markdown
Contributor

@hunshcn hunshcn commented Mar 21, 2026

Summary

Fixes #4377.

The CC toolchain (rules_cc) typically passes -Wl,-z,relro,-z,now to the linker for security hardening. However, -z,now forces all dynamic symbols to be resolved at load time (BIND_NOW), which breaks Go libraries that use dlopen/dlsym to load symbols at runtime.

For example, NVIDIA's go-nvml declares NVML C functions in its header file, which CGO compiles into undefined dynamic symbols. At runtime, go-nvml loads these symbols via dlopen("libnvidia-ml.so.1", RTLD_LAZY | RTLD_GLOBAL) inside nvml.Init(). With BIND_NOW, the dynamic linker fails immediately at program start:

symbol lookup error: undefined symbol: nvmlComputeInstanceDestroy

The same code works with go build because the standard Go toolchain does not pass -z,relro or -z,now to the linker.

Root Cause

rules_cc: unix_cc_configure.bzl    →  adds -Wl,-z,relro,-z,now to CC toolchain
rules_go: context.bzl              →  picks up as ld_executable_options
rules_go: mode.bzl                 →  extldflags_from_cc_toolchain() returns them
rules_go: link.bzl                 →  passes as -extldflags to Go linker

The resulting binary has FLAGS: BIND_NOW in its ELF dynamic section, while go build produces a binary without it.

Fix

Add -Wl,-z,relro,-z,now and -Wl,-z,now to _LINKER_OPTIONS_DENYLIST in context.bzl, consistent with how -Wl,--gc-sections and -pie are already filtered for the same reason (CC toolchain defaults incompatible with Go binaries).

Since go build does not pass -z,relro either, filtering the compound flag entirely aligns with go build behavior.

Test plan

  • Added bind_now_test.go that inspects the ELF dynamic section of a non-PIE cgo binary to verify DF_BIND_NOW / DF_1_NOW flags are not set (PIE excluded because the Go linker itself sets BIND_NOW for -buildmode=pie)
  • Verified with a minimal reproduction repository that the fix resolves the runtime symbol lookup error (the repo also includes a user-level workaround using gc_linkopts = ["-extldflags", "-Wl,-z,lazy"])

@hunshcn
Copy link
Copy Markdown
Contributor Author

hunshcn commented Mar 24, 2026

@fmeum @tyler-french PTAL

Comment thread tests/core/go_binary/bind_now_test.go Outdated
Comment thread go/private/context.bzl Outdated
hunshcn and others added 6 commits March 31, 2026 13:29
The CC toolchain (rules_cc) typically passes -Wl,-z,relro,-z,now to the
linker for security hardening. While -z,relro (partial RELRO) is safe,
-z,now forces all dynamic symbols to be resolved at load time.

This breaks Go libraries that use dlopen/dlsym to load symbols at
runtime (e.g., NVIDIA's go-nvml), because the dynamic linker tries to
resolve undefined symbols before dlopen has a chance to make them
available. The standard Go toolchain (go build) does not pass -z,now.

This is consistent with existing filtering of other CC toolchain flags
that are incompatible with Go binaries (-Wl,--gc-sections, -pie).

Fixes bazel-contrib#4377.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The Go linker itself sets BIND_NOW for -buildmode=pie, so the test
should only verify non-PIE cgo binaries.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Instead of a custom _remove_bind_now function in link.bzl, add the
flags to the existing _LINKER_OPTIONS_DENYLIST in context.bzl. This
is consistent with how -Wl,--gc-sections and -pie are already handled.

The Go toolchain (go build) does not pass -z,relro or -z,now either,
so filtering the compound flag entirely aligns with go build behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace magic numbers with debug/elf constants (elf.DT_FLAGS,
elf.DF_BIND_NOW, elf.DT_FLAGS_1, elf.DF_1_NOW) and add missing
defer e.Close() to match project conventions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@hunshcn hunshcn force-pushed the fix-bind-now-4377 branch from cb08aa0 to a8d33b5 Compare March 31, 2026 05:29
@hunshcn
Copy link
Copy Markdown
Contributor Author

hunshcn commented Mar 31, 2026

I have replaced the previous go_test-based bind_now_test.go (which hardcoded "BIND_NOW should not be set") with a go_bazel_test that builds the same cgo source with both bazel build and native go build, then asserts their BIND_NOW flags match. This covers both auto and pie link modes via table-driven subtests, making the test future-proof — if Go changes its default BIND_NOW behavior, the test adapts automatically.

@fmeum
Copy link
Copy Markdown
Member

fmeum commented Mar 31, 2026

I like the idea, but CI is red 🙃

@hunshcn
Copy link
Copy Markdown
Contributor Author

hunshcn commented Mar 31, 2026

fixed, only test on linux

Comment thread tests/core/go_binary/bind_now_test.go Outdated
Copy link
Copy Markdown
Member

@fmeum fmeum left a comment

Choose a reason for hiding this comment

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

Great test coverage!

@fmeum fmeum enabled auto-merge (squash) March 31, 2026 18:00
@fmeum fmeum merged commit 552451c into bazel-contrib:master Mar 31, 2026
1 check passed
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.

run failed with "undefined symbol" when using nvml-go

2 participants