Summary
BinSkim skips BA2008 (EnableControlFlowGuard) for Go 1.26 Windows binaries produced by the internal linker because the PE header advertises linker version 3.0. The result is reported as NotApplicable instead of a meaningful CFG result.
The same minimal Go program built with external linking is evaluated by BA2008 and reported as an error, which suggests BinSkim is gating the rule primarily on the PE linker version heuristic (>= 14.0) rather than on the actual CFG-related metadata present in the image.
Why this looks like an incompatibility
For Go-produced Windows binaries, the internal linker stamps:
MajorLinkerVersion: 3
MinorLinkerVersion: 0
LoadConfigTableRVA: 0x0
BinSkim then reports BA2008 as:
image was compiled with a toolset version (3.0) that is not sufficiently recent (14.0 or newer) to provide relevant settings
That means BA2008 is never evaluated for the internal-linker binary, even though the PE image is otherwise analyzable and BinSkim successfully evaluates other PE hardening checks on the same file.
Minimal repro
1. Create a tiny Go program
package main
func main() {
println("Hello, World!")
}
2. Build with the Go internal linker
go version
$env:GOOS = 'windows'
$env:GOARCH = 'amd64'
go build -o hello-internal.exe .\hello.go
3. Inspect PE metadata
Using llvm-readobj:
llvm-readobj --file-headers --coff-load-config .\hello-internal.exe
Observed relevant fields:
MajorLinkerVersion: 3
MinorLinkerVersion: 0
LoadConfigTableRVA: 0x0
4. Run BinSkim with NotApplicable results included
BinSkim.exe analyze --kind "Pass;Fail;NotApplicable" --ignorePdbLoadError --output hello-internal.sarif .\hello-internal.exe
Observed BA2008 result:
notapplicable BA2008: 'hello-internal.exe' was not evaluated for check 'EnableControlFlowGuard' as the analysis is not relevant based on observed metadata: image was compiled with a toolset version (3.0) that is not sufficiently recent (14.0 or newer) to provide relevant settings.
Comparison case
5. Build the same program with external linking
go build -ldflags "-linkmode external" -o hello-extlink.exe .\hello.go
6. Inspect PE metadata again
llvm-readobj --file-headers --coff-load-config .\hello-extlink.exe
Observed relevant fields:
MajorLinkerVersion: 14
LoadConfigTableRVA: <non-zero>
GuardFlags: CF_INSTRUMENTED
GuardCFFunctionTable: 0x0
GuardCFFunctionCount: 0
7. Run BinSkim on the external-linker binary
BinSkim.exe analyze --kind "Pass;Fail;NotApplicable" .\hello-extlink.exe
Observed BA2008 result:
error BA2008: 'hello-extlink.exe' does not enable the control flow guard (CFG) mitigation.
Expected behavior
For Go internal-linker PE binaries, BA2008 should ideally be based on the actual PE / load-config / guard metadata, rather than being suppressed solely because the linker version field is 3.0.
Even if the final answer is still “CFG is not enabled”, that should be surfaced as a meaningful result instead of NotApplicable, because today the heuristic makes Go internal-linker Windows binaries look as though BA2008 is irrelevant rather than unsupported or failing.
Actual behavior
BA2008 is skipped as NotApplicable for Go internal-linker Windows binaries due to the stamped toolset version (3.0), while the same source built with external linking is evaluated and produces a BA2008 error.
Notes
I observed the same internal-linker behavior on both:
windows/amd64
windows/arm64
So this does not appear to be architecture-specific.
Summary
BinSkim skips BA2008 (EnableControlFlowGuard) for Go 1.26 Windows binaries produced by the internal linker because the PE header advertises linker version 3.0. The result is reported as NotApplicable instead of a meaningful CFG result.
The same minimal Go program built with external linking is evaluated by BA2008 and reported as an error, which suggests BinSkim is gating the rule primarily on the PE linker version heuristic (
>= 14.0) rather than on the actual CFG-related metadata present in the image.Why this looks like an incompatibility
For Go-produced Windows binaries, the internal linker stamps:
MajorLinkerVersion: 3MinorLinkerVersion: 0LoadConfigTableRVA: 0x0BinSkim then reports BA2008 as:
That means BA2008 is never evaluated for the internal-linker binary, even though the PE image is otherwise analyzable and BinSkim successfully evaluates other PE hardening checks on the same file.
Minimal repro
1. Create a tiny Go program
2. Build with the Go internal linker
3. Inspect PE metadata
Using
llvm-readobj:Observed relevant fields:
4. Run BinSkim with NotApplicable results included
Observed BA2008 result:
Comparison case
5. Build the same program with external linking
6. Inspect PE metadata again
Observed relevant fields:
7. Run BinSkim on the external-linker binary
Observed BA2008 result:
Expected behavior
For Go internal-linker PE binaries, BA2008 should ideally be based on the actual PE / load-config / guard metadata, rather than being suppressed solely because the linker version field is
3.0.Even if the final answer is still “CFG is not enabled”, that should be surfaced as a meaningful result instead of NotApplicable, because today the heuristic makes Go internal-linker Windows binaries look as though BA2008 is irrelevant rather than unsupported or failing.
Actual behavior
BA2008 is skipped as NotApplicable for Go internal-linker Windows binaries due to the stamped toolset version (
3.0), while the same source built with external linking is evaluated and produces a BA2008 error.Notes
I observed the same internal-linker behavior on both:
windows/amd64windows/arm64So this does not appear to be architecture-specific.