Repository navigation
crypto/tls, crypto/x509: binary size increases sharply starting with Go 1.24 #81132
Description
Go version
Reproduced with:
- go1.23.2
- go1.24.13
- go1.26.7
- go1.27.0
Host: linux/amd64
Targets tested:
GOOS=linux GOARCH=amd64 CGO_ENABLED=0GOOS=js GOARCH=wasm
For Go 1.24.13, 1.26.7, and 1.27.0, go env GOFIPS140 reports off.
No third-party dependencies are required.
What did you do?
I noticed a large binary-size increase when upgrading a Go application from Go 1.23 to Go 1.24. The standard-library part of the increase can be reproduced with a blank import of crypto/tls:
package main
import _ "crypto/tls"
func main() {}For comparison, I tested four programs: empty, blank-import crypto/tls, blank-import crypto/x509, and blank-import net/http.
Reproduction script:
ROOT="$(mktemp -d)"
cd "$ROOT"
cat > go.mod <<'EOM'
module go-crypto-size-repro
go 1.23
EOM
mkdir empty tls x509 http
cat > empty/main.go <<'EOM'
package main
func main() {}
EOM
cat > tls/main.go <<'EOM'
package main
import _ "crypto/tls"
func main() {}
EOM
cat > x509/main.go <<'EOM'
package main
import _ "crypto/x509"
func main() {}
EOM
cat > http/main.go <<'EOM'
package main
import _ "net/http"
func main() {}
EOM
for TARGET in linux/amd64 js/wasm; do
GOOS="${TARGET%/*}"
GOARCH="${TARGET#*/}"
echo
echo "=== $GOOS/$GOARCH ==="
printf '%-12s %12s %12s %12s %12s\n' VERSION empty tls x509 http
for TC in go1.23.2 go1.24.13 go1.26.7 go1.27.0; do
sizes=()
for P in empty tls x509 http; do
GOOS="$GOOS" GOARCH="$GOARCH" CGO_ENABLED=0 \
GOTOOLCHAIN="$TC" \
go build -trimpath -ldflags='-s -w' \
-o "$P-$TC-$GOOS-$GOARCH" "./$P"
sizes+=("$(wc -c < "$P-$TC-$GOOS-$GOARCH")")
done
printf '%-12s %12d %12d %12d %12d\n' \
"$TC" "${sizes[0]}" "${sizes[1]}" "${sizes[2]}" "${sizes[3]}"
done
doneWhat did you see happen?
linux/amd64
Stripped binaries, CGO_ENABLED=0:
| Version | empty | crypto/tls | crypto/x509 | net/http |
|---|---|---|---|---|
| go1.23.2 | 979,096 | 1,671,320 | 1,626,264 | 2,723,992 |
| go1.24.13 | 1,007,800 | 2,384,056 | 2,293,944 | 3,231,928 |
| go1.26.7 | 1,200,290 | 2,654,370 | 2,416,802 | 3,494,050 |
| go1.27.0 | 1,216,672 | 2,699,424 | 2,474,144 | 3,539,104 |
Go 1.23.2 -> Go 1.24.13:
| Program | Change |
|---|---|
| empty | +28,704 bytes (+2.93%) |
| crypto/tls | +712,736 bytes (+42.65%) |
| crypto/x509 | +667,680 bytes (+41.06%) |
| net/http | +507,936 bytes (+18.65%) |
Subtracting the empty-program size, the incremental package cost is:
| Package | Go 1.23.2 | Go 1.24.13 | Change |
|---|---|---|---|
| crypto/tls | 692,224 | 1,376,256 | +98.82% |
| crypto/x509 | 647,168 | 1,286,144 | +98.73% |
| net/http | 1,744,896 | 2,224,128 | +27.46% |
js/wasm
| Version | empty | crypto/tls | crypto/x509 | net/http |
|---|---|---|---|---|
| go1.23.2 | 1,562,137 | 2,504,144 | 2,455,891 | 3,262,592 |
| go1.24.13 | 1,567,379 | 3,758,954 | 3,623,080 | 4,168,698 |
| go1.26.7 | 1,800,136 | 4,383,725 | 3,880,366 | 4,692,491 |
| go1.27.0 | 1,855,363 | 4,484,580 | 3,990,654 | 4,797,695 |
Go 1.23.2 -> Go 1.24.13:
| Program | Change |
|---|---|
| empty | +5,242 bytes (+0.34%) |
| crypto/tls | +1,254,810 bytes (+50.11%) |
| crypto/x509 | +1,167,189 bytes (+47.53%) |
| net/http | +906,106 bytes (+27.77%) |
Subtracting the empty-program size:
| Package | Go 1.23.2 | Go 1.24.13 | Change |
|---|---|---|---|
| crypto/tls | 942,007 | 2,191,575 | +132.65% |
| crypto/x509 | 893,754 | 2,055,701 | +130.01% |
| net/http | 1,700,455 | 2,601,319 | +52.98% |
So the discontinuity is not WASM-only, although it is particularly costly on WASM. It begins at the Go 1.23 -> Go 1.24 boundary and persists through Go 1.27.
What did you expect to see?
Some binary-size change between releases is expected, especially given the cryptographic changes in Go 1.24.
However, I did not expect the incremental cost of merely importing crypto/tls or crypto/x509 to approximately double on linux/amd64 and more than double on js/wasm while GOFIPS140=off.
Go 1.24 introduced the Go Cryptographic Module / FIPS 140-3 machinery. In the FIPS design discussion (#70123), the expected binary-size effect was described as small. Is all of the additional crypto implementation/type/init graph required when FIPS mode is off, or is some of it being retained unnecessarily?
Additional investigation
This was originally found in a larger js/wasm application. With otherwise identical source, a minimal document/application path grew approximately:
| Version | Size |
|---|---|
| go1.23.12 | 6.89 MB |
| go1.24.6 | 13.64 MB |
| go1.26.3 | 14.10 MB |
| go1.27.0 | 15.10 MB |
Linker -dumpdep investigation showed:
- no
<ReflectMethod>retention in either Go 1.23 or Go 1.24; - a large increase in reachable symbols and
UsedInIfacetypes; - much of the additional Go 1.24 graph in
net/http,crypto/tls,crypto/x509, andcrypto/internal/fips140; - compiled application package archive sizes did not show a corresponding increase, suggesting that much of the increase appears at final-link reachability.
The small blank-import examples above are intended to isolate the standard-library component from that larger application.
Related issues
This appears distinct from #81067. That issue is a Go 1.27 WASM size regression associated with encoding/json / jsonv2; GOEXPERIMENT=nojsonv2 reportedly restores its previous size.
It also appears distinct from #78321, which concerned a 32 MiB FIPS DRBG BSS allocation increasing WASM initial linear memory in Go 1.26 and was fixed by statically excluding that code.
Also related, #62202 especially @aclements comment.
This issue concerns binary size, begins at the Go 1.23 -> Go 1.24 boundary, affects both linux/amd64 and js/wasm, and reproduces directly with crypto/tls or crypto/x509.
cc @FiloSottile
Activity
dexhunter commentedon Aug 26, 2026
Would a focused investigation into avoidable final-link reachability be useful here? I can start from the four minimal programs in the report and compare Go 1.23.2, 1.24.13, and current master on linux/amd64 and js/wasm. I would use stripped binary bytes as the primary metric and keep FIPS-off and FIPS-enabled behavior as correctness controls.
Before I build anything, should the scope be reducing the FIPS-related reachable graph when GOFIPS140=off, or is the Go 1.24 size increase an expected tradeoff that should be documented instead?
atdiar commentedon Aug 28, 2026
Hi Aiden, Hello @dexhunter,
Regarding you first question, I think it would help. (if you can reproduce).
As for your second question, not quite sure, I am trying to understand what changed exactly. If it is FIPS related, it should probably be documented, at least, before figuring out if there is a way to mitigate.
If it ends up not being FIPS-related somehow, we can then figure out whether there is anything that could be done.
I did try to inspect if there wasn't something getting in the way of DCE given the changes in 1.24 but could not find anything salient. Maybe you will have better luck.
FiloSottile commentedon Aug 28, 2026
I had a bot do a deeper bisect, and then looked at the top contributors.
The main insight is that FIPS 140-3
init()self-tests made empty imports bring in a good chunk of the program, but that did not change the size of a program that actually uses the package.Here's the size of a program that does
tls.Client(...).Handshake(). You can see the growth is not nearly as dramatic.Linux/amd64,
CGO_ENABLED=0,-trimpath -ldflags='-s -w':Here are the top contributors to a program that just imports
crypto/x509, and how much they add to a program that does a TLS handshake.The X.509 policy validation is actual new code. The RSA move does add 12 KB of self-test key, which is annoying and might be worth trading some FIPS 104-3 mode-only init CPU time against.
This investigation turned up a couple easy wins, that should reduce binary size by some 40 KB. I'll mail them soon.
We can also drastically reduce the empty import cost of e8d9561 by removing the
var anyPolicyOID = mustNewOIDFromInts()call from init time.In general though, I don't think importing but not using a package is particularly common or something we need to care about?
gopherbot commentedon Aug 28, 2026
Change https://go.dev/cl/824126 mentions this issue:
crypto/rsa: reduce size of FIPS 140-3 CASTgopherbot commentedon Aug 28, 2026
Change https://go.dev/cl/824125 mentions this issue:
crypto/ecdh,crypto/ecdsa: reduce binary size by avoiding body duplicationgopherbot commentedon Aug 28, 2026
Change https://go.dev/cl/824124 mentions this issue:
crypto/x509: avoid linking fmt on empty package importsatdiar commentedon Aug 29, 2026
Thanks @FiloSottile . Yes the blank import is just for the reproducer.
The issue was that the original wasm program was transitively importing those by importing net/http, x/net etc...
The quasi doubling of binary size is unexpected.
I will investigate further, I didn't think it was FIPS related and you are showing that this is not the main reason for what I observe.
But seems the change was adjacent to the introduction of FIPS.
prattmic commentedon Sep 4, 2026
cc @golang/security
FiloSottile commentedon Sep 4, 2026
To be clear, the FIPS 140-3 mode support does make some code reachable from a blank import that would normally be linked only by a program that actually uses the package. However, I am not convinced that near-empty imports are a common case.
To look at more real-world cases, we'd need the actual program, or a more realistic reproducer.
atdiar commentedon Sep 4, 2026
I understand but I think there might have been init changes that might interact with the linker phase and subsequently interact unfavorably with DCE. That can happen at any point within the import graph, not just a blank import.
For us, it happens when importing net/http for instance.
Can't really post the actual code yet but here is a more realistic reproducer:
-trimpath -ldflags='-s -w', CGO_ENABLED=0, stripped bytes on my machine:import _ "net/http", linux/amd64import _ "net/http", js/wasmThe real program is isomorphic: same code runs on the client (browser via wasm) and the server (server-side rendering).
On 1.23 the program costs zero bytes over the blank import: Size, load, and
the HTTP client are all dead code. On 1.24 the same lines cost +2,347,008
bytes on amd64 and +5,476,390 on js/wasm, more than doubling the binary. Two
controls on 1.24: with
var sink *Store(no interface conversion) and withthe method renamed to
ItemCount() int, the program is within ~600 bytes ofthe blank import on both targets.
Where the retention might come from, from -dumpdep on these builds (with the help of a little llm):
The init()-registered CASTs and the check package make hash-interface
method calls (
Size() int,BlockSize() int, ...) reachable from init;that's what lights up same-signature methods of unrelated types, as above.
var x = sync.OnceFunc/OnceValue(...)package vars (the lazy self-testsand the ecdsa curve constructors) build their closures at init, so the
closures' whole callee graphs are retained on import while the pre-1.24 shape
(a sync.Once inside the function) was invisible to a blank import ?
nistec's init decodes the 88 KB P-256 table; since inittasks reference
imported packages' inittasks, any live init that transitively imports
nistec keeps it.
My real-world case: my wasm app's framework went from 6.9 to 13.6 MB.
Explanation:
The net/http import is entirely internal to the framework: main.go never touches it. By importing the framework, a client transitively links net/http, and with it crypto/tls and the whole 1.24 init graph, even if they don't use it (it is used by the framework for fetch requests, but arguably a client may remain local and not issue any).
That is how it ends up inflating every app via the combination of FIPS init changes and of coarse DCE wrt method signatures but we can't change the latter yet).
One more data point:
fips140.Supported()rejects GOARCH=wasm, so onjs/wasm the self-test/check graph can never run: it's retained purely by
init reachability.
I'll leave what to do about all this to you(don't want to touch crypto code, code owners should probably be better suited to take on this), happy to run measurements on any candidate fix however.
crypto/x509: avoid linking fmt on empty package imports
crypto/ecdh,crypto/ecdsa: reduce binary size by avoiding body duplica…