Summary
check-payload scan local fails with "could not find magic number" on Go 1.26-built PIE binaries because ReadTable in internal/golang/goscan.go unconditionally redirects to .data.rel.ro when it sees -buildmode=pie. Go 1.26 changed the ELF layout for PIE builds: it now emits .gopclntab as a separate section instead of embedding the pclntab in .data.rel.ro (which was the Go 1.25 and earlier behavior).
This will affect all OCP components once the RHEL 9 Go toolchain moves to 1.26, not just the images where we first observed it.
Root Cause
The section lookup logic in internal/golang/goscan.go overrides the section label when the binary is PIE:
sectionLabel := ".gopclntab"
for _, bs := range bi.Settings {
if bs.Key == "-buildmode" && bs.Value == "pie" {
sectionLabel = ".data.rel.ro" // <-- skips .gopclntab entirely
break
}
}
In Go <= 1.25, PIE binaries embedded the pclntab inside .data.rel.ro, so this was correct. In Go 1.26, the linker restores .gopclntab as a standalone section for PIE builds. The pclntab magic is no longer in .data.rel.ro.
Evidence (binary comparison)
We extracted /usr/bin/skopeo from two s390x RHEL 9 container images built in the same CI run:
| Binary |
Go version |
.gopclntab section |
Magic in .data.rel.ro |
check-payload result |
skopeo 1.22.2-2.el9 (from appstream) |
go1.26.1 (Red Hat 1.26.1-1.el9) |
Present (magic ff ff ff f1 at offset 0) |
Not present |
FAIL |
skopeo 1.18.1-5.el9_6 (from appstream-eus) |
go1.25.8 (Red Hat 1.25.8-1.el9_6) |
Absent |
Present (at offset 3278560) |
PASS |
Both binaries are PIE, dynamically linked, stripped, s390x (big-endian). The only difference is the Go toolchain version used to build the RPM.
ELF section listing for the Go 1.26 binary:
[17] .gopclntab PROGBITS 0000000000f60540 f60540 8f04e1 00 A 0 0 16
[23] .data.rel.ro PROGBITS 000000000185ab00 1859b00 2eb418 00 WA 0 0 32
[28] .go.fipsinfo PROGBITS 0000000001b7ce20 01b7be20 ...
[29] .go.module PROGBITS 0000000001b7cea0 01b7bea0 ...
Note the new .go.fipsinfo and .go.module sections — consistent with Go 1.26's FIPS-related linker changes.
Suggested Fix
Always check .gopclntab first, regardless of build mode. Fall back to .data.rel.ro only if .gopclntab is absent:
section := exe.Section(".gopclntab")
if section == nil {
section = exe.Section(".data.rel.ro")
if section == nil {
return nil, fmt.Errorf("could not read section .gopclntab from %s ", fileName)
}
}
This is backward-compatible: Go <= 1.25 PIE binaries don't have .gopclntab, so the fallback to .data.rel.ro still applies.
Related
Reproduction
# Pull an s390x image with skopeo built by Go 1.26
podman pull --platform linux/s390x registry.access.redhat.com/ubi9/ubi-minimal:latest
# Install skopeo 1.22.2 (from appstream, built with go1.26.1)
# Mount the image and run check-payload scan local --path <mount>
# Observe: "could not find magic number in .../usr/bin/skopeo"
cc @rphillips @smith-xyz
Summary
check-payload scan localfails with "could not find magic number" on Go 1.26-built PIE binaries becauseReadTableininternal/golang/goscan.gounconditionally redirects to.data.rel.rowhen it sees-buildmode=pie. Go 1.26 changed the ELF layout for PIE builds: it now emits.gopclntabas a separate section instead of embedding the pclntab in.data.rel.ro(which was the Go 1.25 and earlier behavior).This will affect all OCP components once the RHEL 9 Go toolchain moves to 1.26, not just the images where we first observed it.
Root Cause
The section lookup logic in
internal/golang/goscan.gooverrides the section label when the binary is PIE:In Go <= 1.25, PIE binaries embedded the pclntab inside
.data.rel.ro, so this was correct. In Go 1.26, the linker restores.gopclntabas a standalone section for PIE builds. The pclntab magic is no longer in.data.rel.ro.Evidence (binary comparison)
We extracted
/usr/bin/skopeofrom two s390x RHEL 9 container images built in the same CI run:.gopclntabsection.data.rel.roappstream)ff ff ff f1at offset 0)appstream-eus)Both binaries are PIE, dynamically linked, stripped, s390x (big-endian). The only difference is the Go toolchain version used to build the RPM.
ELF section listing for the Go 1.26 binary:
Note the new
.go.fipsinfoand.go.modulesections — consistent with Go 1.26's FIPS-related linker changes.Suggested Fix
Always check
.gopclntabfirst, regardless of build mode. Fall back to.data.rel.roonly if.gopclntabis absent:This is backward-compatible: Go <= 1.25 PIE binaries don't have
.gopclntab, so the fallback to.data.rel.rostill applies.Related
golang-1.26.1-1.el9) was recently rolled out toappstream; EUS repos still have Go 1.25.8#forum-ocp-fipsdiscussion on Go 1.26 native FIPS support (GOFIPS140=auto): https://redhat-internal.slack.com/archives/C05U13J3LLS/p1772567553738529Reproduction
cc @rphillips @smith-xyz