Skip to content

ReadTable fails on Go 1.26 PIE binaries that have a separate .gopclntab section #329

@jiridanek

Description

@jiridanek

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions