bazel-contrib / bazel-contrib/rules_go
cgo coverage links external test binaries with `--coverage` even when C coverage instrumentation is denied
- Dominant language
- Go
- Stars
- 1.5k
- Forks
- 760
- Avg merge
- 1d 11h
- Merged PRs (30d)
- 12
Description
### What version of rules_go are you using?
Reproduces on current `bazel-contrib/rules_go` `master`:
```text
9792f1c079a7602347e0fd966542b6d6b35a6c64
```
### What version of gazelle are you using?
Not involved. This reproduces with a handwritten `go_library` / `go_test`.
### What version of Bazel are you using?
```text
bazel 9.2.0
```
### Does this issue reproduce with the latest releases of all the above?
Yes. The `aquery` reproduction below was run against current `master` at `9792f1c079a7602347e0fd966542b6d6b35a6c64` via `local_path_override`.
### What operating system and processor architecture are you using?
macOS arm64 for the minimal `aquery` reproduction below.
### Any other potentially useful information about your toolchain?
This is about the interaction between cgo, Bazel coverage mode, and the C/C++ toolchain flags collected by rules_go.
The relevant source shape on `master` appears to be:
```starlark
# go/private/context.bzl
_COMPILER_OPTIONS_DENYLIST = dict({
# Don't compile generated cgo code with coverage. If we do an internal
# link, we may have undefined references to coverage functions.
"--coverage": None,
"-ftest-coverage": None,
"-fprofile-arcs": None,
"-fprofile-instr-generate": None,
"-fcoverage-mapping": None,
}, **COVERAGE_OPTIONS_DENYLIST)
```
but the final Go link action still does this unconditionally when Bazel coverage is enabled:
```starlark
# go/private/actions/link.bzl
if go.coverage_enabled:
extldflags.append("--coverage")
```
`go.coverage_enabled` comes from `ctx.configuration.coverage_enabled`, while whether a target is actually instrumented is tracked separately via `ctx.coverage_instrumented()`.
### What did you do?
Minimal repro:
```starlark
# MODULE.bazel
module(name = "cgocover_repro")
bazel_dep(name = "rules_go", version = "0.61.1")
local_path_override(module_name = "rules_go", path = "/path/to/rules_go_master")
go_sdk = use_extension("@rules_go//go:extensions.bzl", "go_sdk")
go_sdk.host()
```
```starlark
# BUILD.bazel
load("@rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "cgocover",
srcs = ["cgocover.go"],
cgo = True,
importpath = "example.com/cgocover",
visibility = ["//visibility:public"],
)
go_test(
name = "cgocover_test",
srcs = ["cgocover_test.go"],
embed = [":cgocover"],
)
```
```go
// cgocover.go
package cgocover
// #include
import "C"
func Alloc() {
p := C.malloc(8)
C.free(p)
}
```
```go
// cgocover_test.go
package cgocover
import "testing"
func TestAlloc(t *testing.T) {
Alloc()
}
```
Then inspect the link action under coverage:
```bash
bazel aquery --include_commandline \
--collect_code_coverage \
--instrumentation_filter=. \
'mnemonic("GoLink", deps(//:cgocover_test))'
```
I also tried excluding the target from instrumentation:
```bash
bazel aquery --include_commandline \
--collect_code_coverage \
--instrumentation_filter='-.*' \
'mnemonic("GoLink", deps(//:cgocover_test))'
```
### What did you expect to see?
The external linker flags for the cgo test binary should not contain `--coverage` unless the C/C++ objects are actually being compiled with GCC/gcov coverage instrumentation.
In particular, if rules_go intentionally denies C-side coverage options for generated cgo code, the final external link should not still request the gcov coverage runtime for those objects.
This would also match native Go behavior. On the same small cgo package, `go test -cover -x .` did not pass `--coverage` to the C compiler or to the final cgo test binary link. The final Go linker invocation was of the form:
```text
.../pkg/tool/.../link ... -extld=cc ... cgocover.test ...
```
with no `--coverage` token.
### What did you see instead?
The `GoLink` action for the cgo test binary contains `--coverage` inside `-extldflags`:
```text
Command Line: ... builder link ... -- \
-extld external/rules_cc++cc_configure_extension+local_config_cc/cc_wrapper.sh \
-linkmode external \
... \
-extldflags \
'-mmacosx-version-min=10.11 ... -lm --coverage'
```
This still happens with `--instrumentation_filter='-.*'`, i.e. when no Go package in the target should be instrumented. The global Bazel coverage configuration flips `go.coverage_enabled`, and `link.bzl` then appends `--coverage` without checking whether C-side coverage instrumentation is actually in use.
The C compilation side is inconsistent with that. The `GoCompilePkg` action for the cgo package receives Go coverage flags (`-cover_mode`, `-cover`, etc.), but the C compiler flags do not contain `--coverage`, `-ftest-coverage`, or `-fprofile-arcs`; those are filtered by `_COMPILER_OPTIONS_DENYLIST`.
So the current behavior is effectively:
1. Do not compile generated cgo C code with gcov coverage instrumentation.
2. Still pass `--coverage` to the external linker for the final cgo test binary.
For GCC, `--coverage` is documented as a compile-and-link coverage option: it is equivalent to `-fprofile-arcs -ftest-coverage` when compiling and `-lgcov` when linking. If the compile side is intentionally disabled, the link-side `--coverage` appears to only pull in the gcov/profiling runtime without corresponding C coverage data.
In a large Linux amd64 cgo test binary linked externally in non-PIE mode, that extra external-link coverage runtime was enough to change the link and fail with a startup-object relocation overflow:
```text
/usr/lib/gcc/x86_64-linux-gnu/10/crtbegin.o(.text+0x7): error: relocation overflow: reference to local symbol 5
collect2: error: ld returned 1 exit status
```
Removing the residual `extldflags.append("--coverage")` avoided that failure, without affecting Go coverage collection.
### Possible fix direction
Would it make sense to remove the unconditional `extldflags.append("--coverage")` from `go/private/actions/link.bzl`, or at least gate it on a condition that proves C/C++ coverage instrumentation is actually being used?
Since Go coverage is handled through the Go coverage machinery and the cgo C-side coverage flags are already denylisted, the unconditional external linker `--coverage` seems both inconsistent with native `go test -cover` and harmful for large external links.
---
*Drafted by gpt-5.5.*
Contributor guide
Research direction
Read go/private/context.bzl's compiler coverage denylist and go/private/actions/link.bzl's GoLink handling. Reproduce the issue with the provided cgo example and both aquery instrumentation filters, then compare the generated compile and link actions. Done means the external link no longer receives --coverage when C coverage instrumentation is denied, while Go coverage remains unaffected.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- build-system, testing
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 68/100