Skip to content

crypto/internal/fips140/drbg: 32 MiB BSS reservation makes binaries unrunnable on low-memory Linux #81505

Open

Description

@alexpalyan

Go version

go version go1.27.1 darwin/arm64

Output of go env in your module/workspace:

GOARCH='arm'
GOARM='5'
GOOS='linux'
GOHOSTARCH='arm64'
GOHOSTOS='darwin'
CGO_ENABLED='0'
GOFIPS140='off'
GOFLAGS=''
GOEXPERIMENT=''
GODEBUG=''
GOTOOLCHAIN='go1.27.1'
GOVERSION='go1.27.1'
GOROOT='/opt/homebrew/Cellar/go/1.27.1/libexec'
GOTOOLDIR='/opt/homebrew/Cellar/go/1.27.1/libexec/pkg/tool/darwin_arm64'
GOMOD='/Users/user/src/sms-relay/go.mod'
GOPATH='/Users/user/go'
GOMODCACHE='/Users/user/go/pkg/mod'
GOCACHE='/Users/user/Library/Caches/go-build'
GOPROXY='https://proxy.golang.org,direct'
GOSUMDB='sum.golang.org'
GOWORK='off'

What did you do?

Built a program whose only notable import is crypto/cipher:

package main

import (
	"crypto/cipher"
	"fmt"
)

func main() {
	var s cipher.Stream
	fmt.Println("cipher ok", s == nil)
}
GOOS=linux GOARCH=arm GOARM=5 CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" .

then copied the binary to the device and ran it.

What did you see happen?

The kernel refuses the exec and the process is SIGKILLed before main() — no
output, no panic, no core, exit status 137.

oom_score_adj=-1000 does not help, which is what first ruled out the OOM killer.
Flipping the commit accounting and back, with the same binary each time, isolates the
mechanism exactly:

overcommit_memory=0   ./prog   -> exit 137, "Killed", no output
overcommit_memory=1   ./prog   -> "cipher ok true", exit 0
overcommit_memory=0   ./prog   -> exit 137, "Killed"          (control)

The writable PT_LOAD segment across toolchains shows where it comes from:

toolchain BSS (memsz)
go1.22.12 223 496
go1.23.12 94 824
go1.24.9 104 664
go1.25.3 175 176
go1.26.1 33 733 432
go1.27.1 33 733 760

go tool nm -size -sort size names the cause:

  1dbc68   33554432 B crypto/internal/fips140/drbg.memory

(Build without -ldflags "-s -w" or the symbol table is stripped and nm prints
nothing. The address varies per build; the size does not.) The same command on
go1.25.3 finds no such symbol, which is what makes 1.25 the last good release.

Exactly 32 MiB, statically reserved. The variable is declared in
crypto/internal/fips140/drbg/entropy_fips140.go, and its size comes from
crypto/internal/entropy/v1.0.0/entropy.go:

// ScratchBuffer is a large buffer that will be written to using atomics, to
// generate noise from memory access timings. Its contents do not matter.
type ScratchBuffer [1 << 25]byte

Anything reaching this package pulls it in — crypto/aes, crypto/sha256,
crypto/x509, crypto/tls. GOFIPS140=off does not remove it, and that is the
default setting.

The failure is silent and happens only on the target, so a cross-compiled binary looks
perfectly healthy on the build host.

The buffer size is the whole problem

Confirmed by changing that one line and nothing else. Building the real application
(a TLS + WebSocket client) with ScratchBuffer reduced to [1 << 20]byte, applied
through go build -overlay so the toolchain itself is untouched:

BSS on the device
go1.27.1, stock 33 733 760 SIGKILL before main()
go1.27.1, buffer at 1 MiB 1 230 960 runs; TLS 1.3 handshake, stable connection

So nothing else in Go 1.26/1.27 blocks this target — it is this reservation alone.

What did you expect to see?

A binary that runs, as it did through go1.25.

The declaration's own comment states the assumption that does not hold here:

It should end up in the .noptrbss section, and become backed by physical pages at
first use. This ensures that programs that do not use the FIPS 140-3 module do not
incur any memory use or initialization penalties.

The pages are indeed never touched. But the commit charge is taken at exec, and on
a system whose CommitLimit is smaller than the reservation the program never starts
— so the penalty is paid by exactly the programs the comment promises to spare.


Additional context

Prior art — this is not the first report of this variable

#78321 reported the same 32 MiB variable
on Wasm, was labelled release-blocker, and was fixed in
CL 758361 by excluding the code with a !wasm build tag.
That reporter's framing was "the BSS memory is usually pretty cheap on regular
platforms, but it can be pretty costly on Wasm". This is the same shape of problem on
a platform where it is also not cheap.

The build tag added by that fix carries a comment that anticipates this report almost
exactly:

(Could also exclude other platforms that does not support FIPS 140-3 mode, but as
the BSS variable doesn't cost much, don't bother.)

On a Linux target with a CommitLimit below 32 MiB it costs everything — the binary
does not start at all. A build tag per platform is also not the right fix here, since
FIPS 140-3 mode is nominally supported on linux/arm; lazy allocation on first use
would cover both cases, as would sizing the buffer from GOMAXPROCS rather than a
fixed maximum.

Related but distinct: #77235 and
#81132 concern on-disk binary size rather
than the commit charge that blocks exec.

This target is within Go's stated support policy

Worth stating, since "old embedded kernel" invites dismissal. Go's minimum supported
Linux kernel is 3.2, raised from 2.6.32 in Go 1.24 by the accepted proposal
#67001. This device runs 3.4.110,
comfortably inside that, and linux/arm with GOARM=5 is a first-class port.

Related

#81506 covers a separate defect on this same device (crypto/internal/sysrand and
syscall 384). Together they mean there is no unmodified toolchain that is both usable
and correct here: 1.24+ aborts on the first random read, and 1.23 silently corrupts
memory.

The two also interact when reproducing: because a stock 1.26+ binary cannot exec at
all on this device, the abort in #81506 only becomes visible once the reservation
reported here is worked around.

Activity

  1. randall77 commented on Sep 13, 2026

    @randall77
    Contributor

    Tell us more about the Linux you are running.

  2. added
    WaitingForInfoIssue is not actionable because of missing required information, which needs to be provided.
    on Sep 13, 2026
  3. thepudds commented on Sep 13, 2026

    @thepudds
    Member

    FWIW, Filippo has a possibly related comment in
    #81506 (comment) that suggests this might not be a standard Linux kernel:

    What I think you are saying is that you are running a Go program on a custom kernel that squatted on the getrandom syscall number for something else, and would want Go to do behavior-based detection of that?

  4. alexpalyan commented on Sep 13, 2026

    @alexpalyan
    Author

    The original device is a ZTE MF79U LTE modem, SoC ZX297520V3, ARMv7, booted with mem=62M:

    Linux version 3.4.110 (SCM@ZTE) (gcc version 4.7.2 (Buildroot 2013.02)) #2 Tue Jun 1 12:45:22 CST 2021

    MemTotal:  55084 kB
    SwapTotal:     0 kB
    MemFree:   ~8036 kB
    vm.overcommit_memory = 0
    

    I also reproduced this away from that hardware, on Alpine's stock linux-lts 6.12.81 for armv7 in qemu (-m 64, MemTotal 44356 kB, no swap), same two-line program, GOARCH=arm GOARM=7:

     BSS:  go1.25.3  133,184        go1.27.1  33,690,144
    
     overcommit_memory=2, overcommit_kbytes=27540   go1.25.3 exit 0   go1.27.1 exit 139 (before main)
     overcommit_memory=1                            go1.25.3 exit 0   go1.27.1 exit 0
     overcommit_memory=2 again (control)                              go1.27.1 exit 139
    

    Both binaries do start on 6.12 with the default overcommit_memory=0. That is expected: Linux 5.2 replaced that heuristic with a bare pages > totalram_pages() + total_swap_pages (8c7829b04c52), and the modem's 3.4 kernel is on the old side of it, which is why defaults are enough to fail there.

  5. removed
    WaitingForInfoIssue is not actionable because of missing required information, which needs to be provided.
    on Sep 13, 2026
  6. alexpalyan commented on Sep 14, 2026

    @alexpalyan
    Author

    One additional data point: I reproduced the same behavior on vanilla Linux 3.4.113 from kernel.org, with no special sysctl configuration.

    QEMU ARM (versatile_defconfig), 64 MB RAM, no swap, overcommit_memory=0 and overcommit_ratio=50 (defaults). Same two-line program, built with GOARCH=arm GOARM=5:

    MemFree     go1.25.3    go1.27.1
    52364 kB    exit=0      exit=0
    44156 kB    exit=0      exit=0
    35948 kB    exit=0      exit=0
    31844 kB    exit=0      exit=0
    27752 kB    exit=0      exit=137
    23648 kB    exit=0      exit=137
    

    Go 1.25.3 starts at every tested memory level. Go 1.27.1 stops starting once free memory falls below its ~32 MiB BSS reservation.

    The exit=137 is the same failure mode I observed on the original device.

  7. cherrymui commented on Sep 18, 2026

    @cherrymui
    Member

    cc @FiloSottile @golang/security

  8. added
    NeedsInvestigationSomeone must examine and confirm this is a valid issue and not a duplicate of an existing one.
    on Sep 18, 2026
  9. added this to the Go1.28 milestone on Sep 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    NeedsInvestigationSomeone must examine and confirm this is a valid issue and not a duplicate of an existing one.

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions