From 435274434cba58df92967652bdc1dd4334964f29 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Tue, 10 May 2022 14:46:56 +0200 Subject: fixup! Makefile: Fix rebuilding Go binaries in place to add GNU build ID --- Makefile | 10 ++-- tools/replace-buildid/main.go | 125 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+), 4 deletions(-) create mode 100644 tools/replace-buildid/main.go diff --git a/Makefile b/Makefile index 2b1068d2a..4df090598 100644 --- a/Makefile +++ b/Makefile @@ -499,14 +499,16 @@ ${DEPENDENCY_DIR}: | ${BUILD_DIR} ${BUILD_DIR}/bin/%: ${BUILD_DIR}/intermediate/% | ${BUILD_DIR}/bin @ # To compute a unique and deterministic value for GNU build-id, we use an - @ # intermediate binary which has a fixed build ID of "GITALYS_BUILD_ID", + @ # intermediate binary which has a fixed build ID of "TEMP_GITALY_BUILD_ID", @ # which we replace with a deterministic build ID derived from the Go build ID. @ # If we cannot extract a Go build-id, we punt and fallback to using a random 32-byte hex string. @ # This fallback is unique but non-deterministic, making it sufficient to avoid generating the @ # GNU build-id from the empty string and causing guaranteed collisions. ${Q}GO_BUILD_ID=$$(go tool buildid "$<" || openssl rand -hex 32) && \ GNU_BUILD_ID=$$(echo $$GO_BUILD_ID | sha1sum | cut -d' ' -f1) && \ - sed "s/GITALYS_BUILD_ID/$$GNU_BUILD_ID/" "$<" >"$@" + go run "${SOURCE_DIR}"/tools/replace-buildid \ + -input "$<" -input-build-id "54454D505F474954414C595F4255494C445F4944" \ + -output "$@" -output-build-id "$$GNU_BUILD_ID" ${BUILD_DIR}/intermediate/gitaly: GO_BUILD_TAGS = ${SERVER_BUILD_TAGS} ${BUILD_DIR}/intermediate/praefect: GO_BUILD_TAGS = ${SERVER_BUILD_TAGS} @@ -514,9 +516,9 @@ ${BUILD_DIR}/intermediate/gitaly-git2go-v14: GO_BUILD_TAGS = ${GIT2GO_BUILD_TAGS ${BUILD_DIR}/intermediate/gitaly-git2go-v14: libgit2 ${BUILD_DIR}/intermediate/%: .FORCE @ # We're building intermediate binaries first which contain a fixed build ID - @ # of "GITALYS_BUILD_ID". In the final binary we replace this build ID with + @ # of "TEMP_GITALY_BUILD_ID". In the final binary we replace this build ID with @ # the computed build ID for this binary. - ${Q}go build -o "$@" -ldflags '-B 0x474954414C59535F4255494C445F4944 ${GO_LDFLAGS}' -tags "${GO_BUILD_TAGS}" $(addprefix ${SOURCE_DIR}/cmd/,$(@F)) + ${Q}go build -o "$@" -ldflags '-B 0x54454D505F474954414C595F4255494C445F4944 ${GO_LDFLAGS}' -tags "${GO_BUILD_TAGS}" $(addprefix ${SOURCE_DIR}/cmd/,$(@F)) # This is a build hack to avoid excessive rebuilding of targets. Instead of # depending on the Makefile, we start to depend on tool versions as defined in diff --git a/tools/replace-buildid/main.go b/tools/replace-buildid/main.go new file mode 100644 index 000000000..8eb15efe4 --- /dev/null +++ b/tools/replace-buildid/main.go @@ -0,0 +1,125 @@ +// The `replace-buildid` tool is used to replace a build ID in an ELF binary with a new build ID. +// Note that this tool is extremely naive: it simply takes the old input ID as string, verifies +// that this ID is contained in the binary exactly once, and then replaces it. It has no knowledge +// about ELF binaries whatsoever. +// +// This tool is mainly used to replace our static GNU build ID we set in our Makefile with a +// derived build ID without having to build binaries twice. + +package main + +import ( + "bytes" + "encoding/hex" + "flag" + "fmt" + "io" + "os" + "path/filepath" +) + +func main() { + var inputPath, outputPath, inputBuildID, outputBuildID string + + flag.StringVar(&inputPath, "input", "", "path to the binary whose GNU build ID should be replaced") + flag.StringVar(&outputPath, "output", "", "path whether the resulting binary should be placed") + flag.StringVar(&inputBuildID, "input-build-id", "", "static build ID to replace") + flag.StringVar(&outputBuildID, "output-build-id", "", "new build ID to replace old value with") + flag.Parse() + + if err := replaceBuildID(inputPath, outputPath, inputBuildID, outputBuildID); err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + os.Exit(1) + } +} + +func replaceBuildID(inputPath, outputPath, inputBuildID, outputBuildID string) error { + if inputPath == "" { + return fmt.Errorf("missing input path") + } + if outputPath == "" { + return fmt.Errorf("missing output path") + } + if inputBuildID == "" { + return fmt.Errorf("missing output path") + } + if outputBuildID == "" { + return fmt.Errorf("missing output path") + } + if flag.NArg() > 0 { + return fmt.Errorf("extra arguments") + } + + inputBuildIDDecoded, err := hex.DecodeString(inputBuildID) + if err != nil { + return fmt.Errorf("decoding input build ID: %w", err) + } + + outputBuildIDDecoded, err := hex.DecodeString(outputBuildID) + if err != nil { + return fmt.Errorf("decoding output build ID: %w", err) + } + + if len(inputBuildIDDecoded) != len(outputBuildIDDecoded) { + return fmt.Errorf("input and output build IDs do not have the same length") + } + + data, err := readAndReplace(inputPath, inputBuildIDDecoded, outputBuildIDDecoded) + if err != nil { + return fmt.Errorf("could not replace build ID: %w", err) + } + + if err := writeBinary(outputPath, data); err != nil { + return fmt.Errorf("writing binary: %w", err) + } + + return nil +} + +func readAndReplace(binaryPath string, inputBuildID, outputBuildID []byte) ([]byte, error) { + inputFile, err := os.Open(binaryPath) + if err != nil { + return nil, fmt.Errorf("opening input file: %w", err) + } + defer inputFile.Close() + + data, err := io.ReadAll(inputFile) + if err != nil { + return nil, fmt.Errorf("reading input file: %w", err) + } + + if occurrences := bytes.Count(data, inputBuildID); occurrences != 1 { + return nil, fmt.Errorf("exactly one match for old build ID expected, got %d", occurrences) + } + + return bytes.ReplaceAll(data, inputBuildID, outputBuildID), nil +} + +func writeBinary(binaryPath string, contents []byte) error { + f, err := os.CreateTemp(filepath.Dir(binaryPath), filepath.Base(binaryPath)) + if err != nil { + return fmt.Errorf("could not create binary: %w", err) + } + defer func() { + os.Remove(f.Name()) + f.Close() + }() + + if err := f.Chmod(0o755); err != nil { + return fmt.Errorf("could not change permissions: %w", err) + } + + if _, err := io.Copy(f, bytes.NewReader(contents)); err != nil { + return fmt.Errorf("could not write binary: %w", err) + } + + if err := f.Close(); err != nil { + return fmt.Errorf("could not close binary: %w", err) + } + + if err := os.Rename(f.Name(), binaryPath); err != nil { + return fmt.Errorf("could not move binary into place: %w", err) + } + + return nil +} -- cgit v1.2.3