package release import ( "encoding/binary" "errors" "os" "os/exec" "path/filepath" "testing" ) // buildStampedProgram builds, for goarch, a real program in a module named // github.com/carlosframework/platform whose internal/version package holds a // Build string variable, the shape carlos and cmd/console have, with // ldflags as given. func buildStampedProgram(t *testing.T, goarch, ldflags string) []byte { t.Helper() dir := t.TempDir() for name, body := range map[string]string{ "go.mod": "module github.com/carlosframework/platform\n\ngo 1.21\n", "internal/version/version.go": "package version\n\nvar Build = \"dev\"\n", "main.go": "package main\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/carlosframework/platform/internal/version\"\n)\n\nfunc main() { fmt.Println(version.Build) }\n", } { if err := os.MkdirAll(filepath.Dir(filepath.Join(dir, name)), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0o644); err != nil { t.Fatal(err) } } out := filepath.Join(dir, "bin") args := []string{"build", "-o", out} if ldflags != "" { args = append(args, "-ldflags", ldflags) } cmd := exec.Command("go", append(args, ".")...) cmd.Dir = dir cmd.Env = append(os.Environ(), "GOOS=linux", "GOARCH="+goarch, "CGO_ENABLED=0", "GOFLAGS=-buildvcs=false", "GOWORK=off", "GOPROXY=off") if output, err := cmd.CombinedOutput(); err != nil { t.Fatal(string(output), err) } b, err := os.ReadFile(out) if err != nil { t.Fatal(err) } return b } // ArtifactBuild reads the -X version.Build flag a real linux-arm64 build // records, for a program that has the version.Build variable it stamps. It // reads the recorded flag; it does not establish the linked value // (controller ruling F2: owed before production cohorts, trusted build // attestation tied to the artefact digest). func TestArtifactBuildReadsTheRecordedStampFlag(t *testing.T) { if testing.Short() { t.Skip("builds Go binaries") } for ldflags, want := range map[string]string{ "-s -w -X " + BuildStampFlag + "=v0.28.2": "v0.28.2", "-s -w -X " + BuildStampFlag + "=26065378": "26065378", // Unstripped: Go compresses its DWARF sections (SHF_COMPRESSED), which // the bounded pre-check refuses. Release and console builds use -s -w. "-X " + BuildStampFlag + "=v0.28.2": "", "": "", "-s -w": "", "-s -w -X " + BuildStampFlag + "=v1 -X " + BuildStampFlag + "=v2": "", "-s -w -X=" + BuildStampFlag + "=v0.28.2": "", "-s -w -X github.com/carlosframework/platform/internal/version.BuildX=v0.28.2": "", "-X github.com/carlosframework/platform/internal/other.Build=v0.28.2 -s -w": "", } { if got := ArtifactBuild(buildStampedProgram(t, "arm64", ldflags)); got != want { t.Errorf("ldflags %q: stamp %q, want %q", ldflags, got, want) } } // linux-amd64 is outside the bounded pre-check, so it has no stamp. if got := ArtifactBuild(buildStampedProgram(t, "amd64", "-s -w -X "+BuildStampFlag+"=v0.28.2")); got != "" { t.Fatal("stamp read from a non-arm64 binary", got) } if ArtifactBuild([]byte("not a binary")) != "" { t.Fatal("stamp from non-binary") } } // elfMutate returns a copy of a real linux-arm64 build with change applied // to it, given its section and program header offsets. func elfMutate(b []byte, change func(b []byte, shoff, phoff uint64)) []byte { c := append([]byte(nil), b...) change(c, binary.LittleEndian.Uint64(c[40:]), binary.LittleEndian.Uint64(c[32:])) return c } // elfShstrtabCompressed sets SHF_COMPRESSED on the section-name table of a // real linux-arm64 build: the shape whose decompression Go's ELF reader // performs during parsing, unbounded by the file size. func elfShstrtabCompressed(b []byte) []byte { return elfMutate(b, func(c []byte, shoff, _ uint64) { idx := uint64(binary.LittleEndian.Uint16(c[62:])) h := c[shoff+idx*64:] binary.LittleEndian.PutUint64(h[8:], binary.LittleEndian.Uint64(h[8:])|0x800) }) } // elfOversizedShnum claims 65000 section headers in a real build. func elfOversizedShnum(b []byte) []byte { return elfMutate(b, func(c []byte, _, _ uint64) { binary.LittleEndian.PutUint16(c[60:], 65000) }) } // elfRepeatedName appends a name table holding one name of size bytes // to a real build, points its section-name table at it and every section's // name at that one name: the shape debug/elf would copy once per section // (Astra's re-review of F1). func elfRepeatedName(b []byte, size int) []byte { c := append(append([]byte(nil), b...), make([]byte, size+1)...) for i := len(b); i < len(b)+size; i++ { c[i] = 'a' } le := binary.LittleEndian shoff, shnum, idx := le.Uint64(c[40:]), uint64(le.Uint16(c[60:])), uint64(le.Uint16(c[62:])) h := c[shoff+idx*64:] le.PutUint64(h[24:], uint64(len(b))) le.PutUint64(h[32:], uint64(size+1)) for i := uint64(0); i < shnum; i++ { le.PutUint32(c[shoff+i*64:], 0) } return c } func TestCheckELFBoundsRefusesUnboundedMetadata(t *testing.T) { if testing.Short() { t.Skip("builds Go binaries") } real := buildStampedProgram(t, "arm64", "-s -w -X "+BuildStampFlag+"=v0.28.2") if err := CheckELFBounds(real); err != nil { t.Fatal("a real stripped linux-arm64 build refused", err) } // A 256-byte name, repeated, is inside the caps: 256 x 256 bytes at most. if err := CheckELFBounds(elfRepeatedName(real, 256)); err != nil { t.Fatal("a name at the cap refused", err) } le := binary.LittleEndian for name, crafted := range map[string][]byte{ "compressed section-name table": elfShstrtabCompressed(real), "oversized shnum": elfOversizedShnum(real), "compressed data section": elfMutate(real, func(c []byte, shoff, _ uint64) { h := c[shoff+3*64:] le.PutUint64(h[8:], le.Uint64(h[8:])|0x800) }), "extended numbering": elfMutate(real, func(c []byte, _, _ uint64) { le.PutUint16(c[60:], 0) }), "shstrndx extended": elfMutate(real, func(c []byte, _, _ uint64) { le.PutUint16(c[62:], 0xffff) }), "shstrndx past table": elfMutate(real, func(c []byte, _, _ uint64) { le.PutUint16(c[62:], le.Uint16(c[60:])) }), "oversized phnum": elfMutate(real, func(c []byte, _, _ uint64) { le.PutUint16(c[56:], 65000) }), "section table outside": elfMutate(real, func(c []byte, _, _ uint64) { le.PutUint64(c[40:], uint64(len(c))) }), "section table wraps": elfMutate(real, func(c []byte, _, _ uint64) { le.PutUint64(c[40:], ^uint64(0)-10) }), "tables overlap": elfMutate(real, func(c []byte, _, phoff uint64) { le.PutUint64(c[40:], phoff) }), "table over the header": elfMutate(real, func(c []byte, _, _ uint64) { le.PutUint64(c[32:], 0) }), "wrong header entry size": elfMutate(real, func(c []byte, _, _ uint64) { le.PutUint16(c[58:], 40) }), "section past the end": elfMutate(real, func(c []byte, shoff, _ uint64) { h := c[shoff+1*64:] le.PutUint32(h[4:], 1) le.PutUint64(h[32:], uint64(len(c))+1) }), "segment past the end": elfMutate(real, func(c []byte, _, phoff uint64) { le.PutUint64(c[phoff+32:], uint64(len(c))+1) }), "overlapping sections": elfMutate(real, func(c []byte, shoff, _ uint64) { a, b := c[shoff+1*64:], c[shoff+2*64:] le.PutUint32(a[4:], 1) le.PutUint32(b[4:], 1) le.PutUint64(b[24:], le.Uint64(a[24:])) if le.Uint64(a[32:]) == 0 || le.Uint64(b[32:]) == 0 { le.PutUint64(a[32:], 16) le.PutUint64(b[32:], 16) } }), "repeated 32 MiB name": elfRepeatedName(real, 32<<20), "name table over 64 KiB": elfRepeatedName(real, 64<<10), "repeated 257-byte name": elfRepeatedName(real, 257), "name table not a strtab": elfMutate(real, func(c []byte, shoff, _ uint64) { le.PutUint32(c[shoff+uint64(le.Uint16(c[62:]))*64+4:], 1) }), "empty name table": elfMutate(real, func(c []byte, shoff, _ uint64) { le.PutUint64(c[shoff+uint64(le.Uint16(c[62:]))*64+32:], 0) }), "name offset outside the table": elfMutate(real, func(c []byte, shoff, _ uint64) { size := le.Uint64(c[shoff+uint64(le.Uint16(c[62:]))*64+32:]) le.PutUint32(c[shoff+64:], uint32(size)) }), "x86-64": elfMutate(real, func(c []byte, _, _ uint64) { le.PutUint16(c[18:], 62) }), "32-bit class": elfMutate(real, func(c []byte, _, _ uint64) { c[4] = 1 }), "big endian": elfMutate(real, func(c []byte, _, _ uint64) { c[5] = 2 }), "not ELF": append([]byte("MZ"), real[2:]...), "truncated": real[:63], "empty": nil, } { if err := CheckELFBounds(crafted); !errors.Is(err, ErrELFBounds) { t.Errorf("%s: %v", name, err) } if ArtifactBuild(crafted) != "" { t.Errorf("%s: stamp read", name) } } }