Skip to content

BA2008 is skipped for Go Windows internal-linker binaries because the PE linker version is 3.0 #1176

@qmuntal

Description

@qmuntal

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions