Move ELF build-id reader into a separate library.
Bug: 328702178
Change-Id: I188a8d20d22e67e4f0c7e3441e3781fff369c828
diff --git a/elf/Android.bp b/elf/Android.bp
new file mode 100644
index 0000000..6450be1
--- /dev/null
+++ b/elf/Android.bp
@@ -0,0 +1,28 @@
+// Copyright 2016 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+bootstrap_go_package {
+ name: "soong-elf",
+ pkgPath: "android/soong/elf",
+ srcs: [
+ "elf.go",
+ ],
+ testSrcs: [
+ "elf_test.go",
+ ],
+}
diff --git a/elf/elf.go b/elf/elf.go
new file mode 100644
index 0000000..e84a8ae
--- /dev/null
+++ b/elf/elf.go
@@ -0,0 +1,118 @@
+// Copyright 2022 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package elf
+
+import (
+ "debug/elf"
+ "encoding/binary"
+ "encoding/hex"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+)
+
+const gnuBuildID = "GNU\x00"
+
+// Identifier extracts the elf build ID from an elf file. If allowMissing is true it returns
+// an empty identifier if the file exists but the build ID note does not.
+func Identifier(filename string, allowMissing bool) (string, error) {
+ f, err := os.Open(filename)
+ if err != nil {
+ return "", fmt.Errorf("failed to open %s: %w", filename, err)
+ }
+ defer f.Close()
+
+ return elfIdentifierFromReaderAt(f, filename, allowMissing)
+}
+
+// elfIdentifierFromReaderAt extracts the elf build ID from a ReaderAt. If allowMissing is true it
+// returns an empty identifier if the file exists but the build ID note does not.
+func elfIdentifierFromReaderAt(r io.ReaderAt, filename string, allowMissing bool) (string, error) {
+ f, err := elf.NewFile(r)
+ if err != nil {
+ if allowMissing {
+ if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
+ return "", nil
+ }
+ if _, ok := err.(*elf.FormatError); ok {
+ // The file was not an elf file.
+ return "", nil
+ }
+ }
+ return "", fmt.Errorf("failed to parse elf file %s: %w", filename, err)
+ }
+ defer f.Close()
+
+ buildIDNote := f.Section(".note.gnu.build-id")
+ if buildIDNote == nil {
+ if allowMissing {
+ return "", nil
+ }
+ return "", fmt.Errorf("failed to find .note.gnu.build-id in %s", filename)
+ }
+
+ buildIDs, err := readNote(buildIDNote.Open(), f.ByteOrder)
+ if err != nil {
+ return "", fmt.Errorf("failed to read .note.gnu.build-id: %w", err)
+ }
+
+ for name, desc := range buildIDs {
+ if name == gnuBuildID {
+ return hex.EncodeToString(desc), nil
+ }
+ }
+
+ return "", nil
+}
+
+// readNote reads the contents of a note section, returning it as a map from name to descriptor.
+func readNote(note io.Reader, byteOrder binary.ByteOrder) (map[string][]byte, error) {
+ var noteHeader struct {
+ Namesz uint32
+ Descsz uint32
+ Type uint32
+ }
+
+ notes := make(map[string][]byte)
+ for {
+ err := binary.Read(note, byteOrder, ¬eHeader)
+ if err != nil {
+ if err == io.EOF {
+ return notes, nil
+ }
+ return nil, fmt.Errorf("failed to read note header: %w", err)
+ }
+
+ nameBuf := make([]byte, align4(noteHeader.Namesz))
+ err = binary.Read(note, byteOrder, &nameBuf)
+ if err != nil {
+ return nil, fmt.Errorf("failed to read note name: %w", err)
+ }
+ name := string(nameBuf[:noteHeader.Namesz])
+
+ descBuf := make([]byte, align4(noteHeader.Descsz))
+ err = binary.Read(note, byteOrder, &descBuf)
+ if err != nil {
+ return nil, fmt.Errorf("failed to read note desc: %w", err)
+ }
+ notes[name] = descBuf[:noteHeader.Descsz]
+ }
+}
+
+// align4 rounds the input up to the next multiple of 4.
+func align4(i uint32) uint32 {
+ return (i + 3) &^ 3
+}
diff --git a/elf/elf_test.go b/elf/elf_test.go
new file mode 100644
index 0000000..a220770
--- /dev/null
+++ b/elf/elf_test.go
@@ -0,0 +1,152 @@
+// Copyright 2022 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package elf
+
+import (
+ "bytes"
+ "debug/elf"
+ "encoding/binary"
+ "reflect"
+ "testing"
+)
+
+func Test_elfIdentifierFromReaderAt_BadElfFile(t *testing.T) {
+ tests := []struct {
+ name string
+ contents string
+ }{
+ {
+ name: "empty",
+ contents: "",
+ },
+ {
+ name: "text",
+ contents: "#!/bin/bash\necho foobar",
+ },
+ {
+ name: "empty elf",
+ contents: emptyElfFile(),
+ },
+ {
+ name: "short section header",
+ contents: shortSectionHeaderElfFile(),
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ buf := bytes.NewReader([]byte(tt.contents))
+ _, err := elfIdentifierFromReaderAt(buf, "<>", false)
+ if err == nil {
+ t.Errorf("expected error reading bad elf file without allowMissing")
+ }
+ _, err = elfIdentifierFromReaderAt(buf, "<>", true)
+ if err != nil {
+ t.Errorf("expected no error reading bad elf file with allowMissing, got %q", err.Error())
+ }
+ })
+ }
+}
+
+func Test_readNote(t *testing.T) {
+ note := []byte{
+ 0x04, 0x00, 0x00, 0x00,
+ 0x10, 0x00, 0x00, 0x00,
+ 0x03, 0x00, 0x00, 0x00,
+ 0x47, 0x4e, 0x55, 0x00,
+ 0xca, 0xaf, 0x44, 0xd2, 0x82, 0x78, 0x68, 0xfe, 0xc0, 0x90, 0xa3, 0x43, 0x85, 0x36, 0x6c, 0xc7,
+ }
+
+ descs, err := readNote(bytes.NewBuffer(note), binary.LittleEndian)
+ if err != nil {
+ t.Fatalf("unexpected error in readNote: %s", err)
+ }
+
+ expectedDescs := map[string][]byte{
+ "GNU\x00": []byte{0xca, 0xaf, 0x44, 0xd2, 0x82, 0x78, 0x68, 0xfe, 0xc0, 0x90, 0xa3, 0x43, 0x85, 0x36, 0x6c, 0xc7},
+ }
+
+ if !reflect.DeepEqual(descs, expectedDescs) {
+ t.Errorf("incorrect return, want %#v got %#v", expectedDescs, descs)
+ }
+}
+
+// emptyElfFile returns an elf file header with no program headers or sections.
+func emptyElfFile() string {
+ ident := [elf.EI_NIDENT]byte{}
+ identBuf := bytes.NewBuffer(ident[0:0:elf.EI_NIDENT])
+ binary.Write(identBuf, binary.LittleEndian, []byte("\x7fELF"))
+ binary.Write(identBuf, binary.LittleEndian, elf.ELFCLASS64)
+ binary.Write(identBuf, binary.LittleEndian, elf.ELFDATA2LSB)
+ binary.Write(identBuf, binary.LittleEndian, elf.EV_CURRENT)
+ binary.Write(identBuf, binary.LittleEndian, elf.ELFOSABI_LINUX)
+ binary.Write(identBuf, binary.LittleEndian, make([]byte, 8))
+
+ header := elf.Header64{
+ Ident: ident,
+ Type: uint16(elf.ET_EXEC),
+ Machine: uint16(elf.EM_X86_64),
+ Version: uint32(elf.EV_CURRENT),
+ Entry: 0,
+ Phoff: uint64(binary.Size(elf.Header64{})),
+ Shoff: uint64(binary.Size(elf.Header64{})),
+ Flags: 0,
+ Ehsize: uint16(binary.Size(elf.Header64{})),
+ Phentsize: 0x38,
+ Phnum: 0,
+ Shentsize: 0x40,
+ Shnum: 0,
+ Shstrndx: 0,
+ }
+
+ buf := &bytes.Buffer{}
+ binary.Write(buf, binary.LittleEndian, header)
+ return buf.String()
+}
+
+// shortSectionHeader returns an elf file header with a section header that extends past the end of
+// the file.
+func shortSectionHeaderElfFile() string {
+ ident := [elf.EI_NIDENT]byte{}
+ identBuf := bytes.NewBuffer(ident[0:0:elf.EI_NIDENT])
+ binary.Write(identBuf, binary.LittleEndian, []byte("\x7fELF"))
+ binary.Write(identBuf, binary.LittleEndian, elf.ELFCLASS64)
+ binary.Write(identBuf, binary.LittleEndian, elf.ELFDATA2LSB)
+ binary.Write(identBuf, binary.LittleEndian, elf.EV_CURRENT)
+ binary.Write(identBuf, binary.LittleEndian, elf.ELFOSABI_LINUX)
+ binary.Write(identBuf, binary.LittleEndian, make([]byte, 8))
+
+ header := elf.Header64{
+ Ident: ident,
+ Type: uint16(elf.ET_EXEC),
+ Machine: uint16(elf.EM_X86_64),
+ Version: uint32(elf.EV_CURRENT),
+ Entry: 0,
+ Phoff: uint64(binary.Size(elf.Header64{})),
+ Shoff: uint64(binary.Size(elf.Header64{})),
+ Flags: 0,
+ Ehsize: uint16(binary.Size(elf.Header64{})),
+ Phentsize: 0x38,
+ Phnum: 0,
+ Shentsize: 0x40,
+ Shnum: 1,
+ Shstrndx: 0,
+ }
+
+ buf := &bytes.Buffer{}
+ binary.Write(buf, binary.LittleEndian, header)
+ binary.Write(buf, binary.LittleEndian, []byte{0})
+ return buf.String()
+}