Repository navigation
crypto/internal/fips140/drbg: 32 MiB BSS reservation makes binaries unrunnable on low-memory Linux #81505
Description
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]byteAnything 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
randall77 commentedon Sep 13, 2026
Tell us more about the Linux you are running.
gabyhelp commentedon Sep 13, 2026
Related Issues
(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)
thepudds commentedon Sep 13, 2026
FWIW, Filippo has a possibly related comment in
#81506 (comment) that suggests this might not be a standard Linux kernel:
alexpalyan commentedon Sep 13, 2026
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
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:
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.
alexpalyan commentedon Sep 14, 2026
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:
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.
cherrymui commentedon Sep 18, 2026
cc @FiloSottile @golang/security