From 0774e1d04bfcfb069bf7598234c8ef9bacd8600f Mon Sep 17 00:00:00 2001 From: Kir Kolyshkin Date: Thu, 1 Oct 2026 21:58:44 -0700 Subject: [PATCH 1/2] Makefile: always use urfave_cli_no_template build tag Using text/template disables linker dead code elimination of exported methods for the whole program (see golang/go#72895). urfave/cli v3.14.0 added urfave_cli_no_template build tag, which makes it render help without text/template, producing the same output. Since runc does not use custom help templates, we can always use it. Together with the previous commits, this makes runc binary about 2 MB (15%) smaller (13.3 MB -> 11.3 MB on linux/amd64). Signed-off-by: Kir Kolyshkin --- CHANGELOG.md | 4 ++++ Makefile | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db1cd1924..82ce8f6f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Switched to opencontainers/cgroups v0.1.0, which no longer uses the high-level cilium/ebpf API to manage cgroup v2 device rules. As a result, the runc binary shrunk by about 1 MiB (7.5%) on amd64. (#5403) +- The runc binary is now built with the `urfave_cli_no_template` build tag, + which, together with removing the reflect-based logging of CRIU options, + re-enables the linker's dead code elimination of exported methods. As a + result, the runc binary shrunk by about 2 MB (15%) on amd64. (#5486, #5506) - The `cpuAffinity` and NUMA `memoryPolicy` settings are no longer limited to 1024 CPUs/nodes, as runc now uses a dynamically-sized CPU mask. (#5343) - A container configuration which asks for a read-only rootfs diff --git a/Makefile b/Makefile index 4b3c47d48..1043251e2 100644 --- a/Makefile +++ b/Makefile @@ -45,7 +45,7 @@ ifneq (,$(filter $(GOARCH),386 amd64 arm arm64 loong64 ppc64le riscv64 s390x)) endif endif GO_BUILD := $(GO) build $(TRIMPATH) $(GO_BUILDMODE) \ - $(EXTRA_FLAGS) -tags "$(BUILDTAGS)" \ + $(EXTRA_FLAGS) -tags "$(BUILDTAGS) urfave_cli_no_template" \ -ldflags "$(LDFLAGS_COMMON) $(EXTRA_LDFLAGS)" GO_BUILDMODE_STATIC := @@ -61,7 +61,7 @@ ifneq (,$(filter $(GOARCH),arm64 amd64)) endif # Enable static PIE binaries on supported platforms. GO_BUILD_STATIC := $(GO) build $(TRIMPATH) $(GO_BUILDMODE_STATIC) \ - $(EXTRA_FLAGS) -tags "$(BUILDTAGS) netgo osusergo" \ + $(EXTRA_FLAGS) -tags "$(BUILDTAGS) urfave_cli_no_template netgo osusergo" \ -ldflags "$(LDFLAGS_COMMON) $(LDFLAGS_STATIC) $(EXTRA_LDFLAGS)" GPG_KEYID ?= cyphar@cyphar.com From 1a187e899b0999765cb2dbed7693b9e8b9de25cb Mon Sep 17 00:00:00 2001 From: Kir Kolyshkin Date: Thu, 1 Oct 2026 22:04:32 -0700 Subject: [PATCH 2/2] Makefile: add verify-dce target, use it in CI Calling reflect.Value.Method or reflect.Value.MethodByName with a non-constant argument (directly, or indirectly via e.g. text/template) disables the linker's dead code elimination of exported methods, noticeably increasing the binary size. Add verify-dce target to make sure this does not happen again, and run it from CI. To make sure the check itself works, also run it against a small program which disables DCE, and expect it to fail. Signed-off-by: Kir Kolyshkin --- .github/workflows/validate.yml | 2 ++ Makefile | 14 ++++++++++++++ tests/dce-canary/main.go | 18 ++++++++++++++++++ 3 files changed, 34 insertions(+) create mode 100644 tests/dce-canary/main.go diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index f26fff687..e9e858646 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -91,6 +91,8 @@ jobs: run: make BUILDTAGS="" - name: compile with runc_nocriu build tag run: make RUNC_BUILDTAGS="runc_nocriu" + - name: verify linker dead code elimination is not disabled + run: make verify-dce codespell: runs-on: ubuntu-24.04 diff --git a/Makefile b/Makefile index 1043251e2..d293fd44e 100644 --- a/Makefile +++ b/Makefile @@ -249,6 +249,20 @@ verify-dependencies: vendor || (echo -e "git status:\n $$(git status -- go.mod go.sum vendor/)\nerror: vendor/, go.mod and/or go.sum not up to date. Run \"make vendor\" to update"; exit 1) \ && echo "all vendor files are up to date." +# Check that nothing disables the linker's dead code elimination of +# exported methods, which noticeably increases the binary size. This is +# caused by calling reflect.Value.Method or reflect.Value.MethodByName +# with a non-constant argument, including indirectly (e.g. text/template). +# The check itself is verified using tests/dce-canary, which disables DCE. +.PHONY: verify-dce +verify-dce: + $(GO) build -o /dev/null -ldflags=-dumpdep ./tests/dce-canary 2>&1 \ + | grep -q '' \ + || { echo "error: verify-dce self-check failed (tests/dce-canary not detected)"; exit 1; } + set -o pipefail; $(GO_BUILD) -o /dev/null -ldflags=-dumpdep . 2>&1 \ + | { ! grep -q '' \ + || { echo "error: linker dead code elimination is disabled; use github.com/aarzilli/whydeadcode to find out why"; exit 1; }; } + .PHONY: validate-keyring validate-keyring: script/keyring_validate.sh diff --git a/tests/dce-canary/main.go b/tests/dce-canary/main.go new file mode 100644 index 000000000..407d8c052 --- /dev/null +++ b/tests/dce-canary/main.go @@ -0,0 +1,18 @@ +// dce-canary is a positive control for "make verify-dce". It calls +// reflect.Value.MethodByName with a non-constant argument, which makes +// the linker disable dead code elimination of exported methods, so +// "verify-dce" must detect it. If it does not, the check is broken. +package main + +import ( + "os" + "reflect" +) + +type t struct{} + +func (t) Foo() {} + +func main() { + reflect.ValueOf(t{}).MethodByName(os.Args[0]) +}