Hi,
I've been writing my first RPM spec[0] for one of my projects[1]. After
running rpmbuild, I something peculiar: the resulting binaries were
dynamically-linked according to the "file" command, even though I took
care to never import anything that could enable CGO. The same thing
happened to other non-CGO binaries in the Fedora repos, such as fzf and
shfmt. I managed to isolate the cause to the %gocompilerflags macro
setting -buildmode=pie.
[0]
https://git.sr.ht/~seirdy/fedora-specs/tree/master/item/golang-sr-seirdy-...
[1]
https://sr.ht/~seirdy/MOAC
The %gobuild macro sets -buildmode=PIE as of F26[2]. However,
-buildmode=PIE doesn't necessarily improve the security of a Go binary.
Russel Cox and Ian Lance Taylor share some information in the
golang-nuts list [3].
[2]
https://fedoraproject.org/wiki/Changes/golang-buildmode-pie
[3]
https://groups.google.com/g/golang-nuts/c/Jd9tlNc6jUE/m/qp2oyfEEfjQJ
Although the full thread is worth a read, Ian's messages towards the end
were the most relevant.
The view of the Go authors is that PIE makes sense for C/C++ toolchains,
but it isn't as relevant for the Go runtime. The Go runtime uses a fixed
memory address regardless of the build mode. What buildmode=pie does is
introduce position-independent code to make the binary look like a PIE
and satisfy the "PIE-only" requirement of many operating systems. It's a
box-ticker but doesn't actually serve as an exploit mitigation.
PIE is useful when a binary links in C code with CGO, but for Go
packages that make no use of CGO (shfmt, fzf, etc), PIE doesn't offer
tangible security benefits.
What buildmode=pie does accomplish for otherwise-non-CGO bins is the
introduction of additional complexity. Pure-Go bins now need a C
toolchain and are dynlinked against /lib64/ld-linux-%{arch}.so.2. If
anything, this introduces additional attack surface.
One of the main advantages of Linux is the relative stability of its
kernel ABI; we should make use of that by allowing binaries to be
independent of the system libc when reasonable.
Proposal:
Introduce macros for building Go packages without CGO. Unlike the
existing %gobuildflags and %gocompilerflags macros, they should:
- set "-linkmode internal" in -ldflags
- set the variable CGO_ENABLED=0
- set -buildmode=default
- omit extldflags (since no C code and internal linking means no C
toolchain)
This should produce statically-linked Go binaries independent of the
system libc/ld.
A more speculative follow-up idea I had:
Packages could define whether they will need CGO and why: for example,
Caddy would need CGO to use the OS networking stacks (e.g. for DNS),
Hugo would need CGO for building with SASS support via libsass, etc.
Simply importing something like net/http enables CGO by default, but
this is not strictly necessary; while such features would be useful to
Caddy, a hypothetical tiny Go program that simply needs to perform a
basic HTTP request doesn't need the advanced CGO-only net/http package.
--
/Seirdy