Initialize module and dependencies

This commit is contained in:
dwrz
2026-02-13 14:59:42 +00:00
commit 0740968bca
390 changed files with 131652 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
This code is a copied and slightly modified subset of go/src/debug/buildinfo.
It contains logic for parsing Go binary files for the purpose of extracting
module dependency and symbol table information.
Logic added by vulncheck is located in files with "additions_" prefix.
Within the originally named files, changed or added logic is annotated with
a comment starting with "Addition:".

View File

@@ -0,0 +1,257 @@
// Copyright 2022 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build go1.18
// +build go1.18
package buildinfo
// This file adds to buildinfo the functionality for extracting the PCLN table.
import (
"debug/elf"
"debug/macho"
"debug/pe"
"encoding/binary"
"errors"
"fmt"
"io"
)
// ErrNoSymbols represents non-existence of symbol
// table in binaries supported by buildinfo.
var ErrNoSymbols = errors.New("no symbol section")
// SymbolInfo is derived from cmd/internal/objfile/elf.go:symbols, symbolData.
func (x *elfExe) SymbolInfo(name string) (uint64, uint64, io.ReaderAt, error) {
sym, err := x.lookupSymbol(name)
if err != nil || sym == nil {
if errors.Is(err, elf.ErrNoSymbols) {
return 0, 0, nil, ErrNoSymbols
}
return 0, 0, nil, fmt.Errorf("no symbol %q", name)
}
prog := x.progContaining(sym.Value)
if prog == nil {
return 0, 0, nil, fmt.Errorf("no Prog containing value %d for %q", sym.Value, name)
}
return sym.Value, prog.Vaddr, prog.ReaderAt, nil
}
func (x *elfExe) lookupSymbol(name string) (*elf.Symbol, error) {
x.symbolsOnce.Do(func() {
syms, err := x.f.Symbols()
if err != nil {
x.symbolsErr = err
return
}
x.symbols = make(map[string]*elf.Symbol, len(syms))
for _, s := range syms {
s := s // make a copy to prevent aliasing
x.symbols[s.Name] = &s
}
})
if x.symbolsErr != nil {
return nil, x.symbolsErr
}
return x.symbols[name], nil
}
func (x *elfExe) progContaining(addr uint64) *elf.Prog {
for _, p := range x.f.Progs {
if addr >= p.Vaddr && addr < p.Vaddr+p.Filesz {
return p
}
}
return nil
}
const go12magic = 0xfffffffb
const go116magic = 0xfffffffa
// PCLNTab is derived from cmd/internal/objfile/elf.go:pcln.
func (x *elfExe) PCLNTab() ([]byte, uint64) {
var offset uint64
text := x.f.Section(".text")
if text != nil {
offset = text.Offset
}
pclntab := x.f.Section(".gopclntab")
if pclntab == nil {
// Addition: this code is added to support some form of stripping.
pclntab = x.f.Section(".data.rel.ro.gopclntab")
if pclntab == nil {
pclntab = x.f.Section(".data.rel.ro")
if pclntab == nil {
return nil, 0
}
// Possibly the PCLN table has been stuck in the .data.rel.ro section, but without
// its own section header. We can search for for the start by looking for the four
// byte magic and the go magic.
b, err := pclntab.Data()
if err != nil {
return nil, 0
}
// TODO(rolandshoemaker): I'm not sure if the 16 byte increment during the search is
// actually correct. During testing it worked, but that may be because I got lucky
// with the binary I was using, and we need to do four byte jumps to exhaustively
// search the section?
for i := 0; i < len(b); i += 16 {
if len(b)-i > 16 && b[i+4] == 0 && b[i+5] == 0 &&
(b[i+6] == 1 || b[i+6] == 2 || b[i+6] == 4) &&
(b[i+7] == 4 || b[i+7] == 8) {
// Also check for the go magic
leMagic := binary.LittleEndian.Uint32(b[i:])
beMagic := binary.BigEndian.Uint32(b[i:])
switch {
case leMagic == go12magic:
fallthrough
case beMagic == go12magic:
fallthrough
case leMagic == go116magic:
fallthrough
case beMagic == go116magic:
return b[i:], offset
}
}
}
}
}
b, err := pclntab.Data()
if err != nil {
return nil, 0
}
return b, offset
}
// SymbolInfo is derived from cmd/internal/objfile/pe.go:findPESymbol, loadPETable.
func (x *peExe) SymbolInfo(name string) (uint64, uint64, io.ReaderAt, error) {
sym, err := x.lookupSymbol(name)
if err != nil {
return 0, 0, nil, err
}
if sym == nil {
return 0, 0, nil, fmt.Errorf("no symbol %q", name)
}
sect := x.f.Sections[sym.SectionNumber-1]
// In PE, the symbol's value is the offset from the section start.
return uint64(sym.Value), 0, sect.ReaderAt, nil
}
func (x *peExe) lookupSymbol(name string) (*pe.Symbol, error) {
x.symbolsOnce.Do(func() {
x.symbols = make(map[string]*pe.Symbol, len(x.f.Symbols))
if len(x.f.Symbols) == 0 {
x.symbolsErr = ErrNoSymbols
return
}
for _, s := range x.f.Symbols {
x.symbols[s.Name] = s
}
})
if x.symbolsErr != nil {
return nil, x.symbolsErr
}
return x.symbols[name], nil
}
// PCLNTab is derived from cmd/internal/objfile/pe.go:pcln.
// Assumes that the underlying symbol table exists, otherwise
// it might panic.
func (x *peExe) PCLNTab() ([]byte, uint64) {
var textOffset uint64
for _, section := range x.f.Sections {
if section.Name == ".text" {
textOffset = uint64(section.Offset)
break
}
}
var start, end int64
var section int
if s, _ := x.lookupSymbol("runtime.pclntab"); s != nil {
start = int64(s.Value)
section = int(s.SectionNumber - 1)
}
if s, _ := x.lookupSymbol("runtime.epclntab"); s != nil {
end = int64(s.Value)
}
if start == 0 || end == 0 {
return nil, 0
}
offset := int64(x.f.Sections[section].Offset) + start
size := end - start
pclntab := make([]byte, size)
if _, err := x.r.ReadAt(pclntab, offset); err != nil {
return nil, 0
}
return pclntab, textOffset
}
// SymbolInfo is derived from cmd/internal/objfile/macho.go:symbols.
func (x *machoExe) SymbolInfo(name string) (uint64, uint64, io.ReaderAt, error) {
sym, err := x.lookupSymbol(name)
if err != nil {
return 0, 0, nil, err
}
if sym == nil {
return 0, 0, nil, fmt.Errorf("no symbol %q", name)
}
seg := x.segmentContaining(sym.Value)
if seg == nil {
return 0, 0, nil, fmt.Errorf("no Segment containing value %d for %q", sym.Value, name)
}
return sym.Value, seg.Addr, seg.ReaderAt, nil
}
func (x *machoExe) lookupSymbol(name string) (*macho.Symbol, error) {
const mustExistSymbol = "runtime.main"
x.symbolsOnce.Do(func() {
x.symbols = make(map[string]*macho.Symbol, len(x.f.Symtab.Syms))
for _, s := range x.f.Symtab.Syms {
s := s // make a copy to prevent aliasing
x.symbols[s.Name] = &s
}
// In the presence of stripping, the symbol table for darwin
// binaries will not be empty, but the program symbols will
// be missing.
if _, ok := x.symbols[mustExistSymbol]; !ok {
x.symbolsErr = ErrNoSymbols
}
})
if x.symbolsErr != nil {
return nil, x.symbolsErr
}
return x.symbols[name], nil
}
func (x *machoExe) segmentContaining(addr uint64) *macho.Segment {
for _, load := range x.f.Loads {
seg, ok := load.(*macho.Segment)
if ok && seg.Addr <= addr && addr <= seg.Addr+seg.Filesz-1 && seg.Name != "__PAGEZERO" {
return seg
}
}
return nil
}
// SymbolInfo is derived from cmd/internal/objfile/macho.go:pcln.
func (x *machoExe) PCLNTab() ([]byte, uint64) {
var textOffset uint64
text := x.f.Section("__text")
if text != nil {
textOffset = uint64(text.Offset)
}
pclntab := x.f.Section("__gopclntab")
if pclntab == nil {
return nil, 0
}
b, err := pclntab.Data()
if err != nil {
return nil, 0
}
return b, textOffset
}

View File

@@ -0,0 +1,160 @@
// Copyright 2021 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build go1.18
// +build go1.18
package buildinfo
// Code in this package is dervied from src/cmd/go/internal/version/version.go
// and cmd/go/internal/version/exe.go.
import (
"debug/buildinfo"
"errors"
"fmt"
"net/url"
"os"
"runtime/debug"
"strings"
"golang.org/x/tools/go/packages"
"golang.org/x/vuln/internal/gosym"
"golang.org/x/vuln/internal/goversion"
)
func debugModulesToPackagesModules(debugModules []*debug.Module) []*packages.Module {
packagesModules := make([]*packages.Module, len(debugModules))
for i, mod := range debugModules {
packagesModules[i] = &packages.Module{
Path: mod.Path,
Version: mod.Version,
}
if mod.Replace != nil {
packagesModules[i].Replace = &packages.Module{
Path: mod.Replace.Path,
Version: mod.Replace.Version,
}
}
}
return packagesModules
}
type Symbol struct {
Pkg string `json:"pkg,omitempty"`
Name string `json:"name,omitempty"`
}
// ExtractPackagesAndSymbols extracts symbols, packages, modules from
// Go binary file as well as bin's metadata.
//
// If the symbol table is not available, such as in the case of stripped
// binaries, returns module and binary info but without the symbol info.
func ExtractPackagesAndSymbols(file string) ([]*packages.Module, []Symbol, *debug.BuildInfo, error) {
bin, err := os.Open(file)
if err != nil {
return nil, nil, nil, err
}
defer bin.Close()
bi, err := buildinfo.Read(bin)
if err != nil {
// It could be that bin is an ancient Go binary.
v, err := goversion.ReadExe(file)
if err != nil {
return nil, nil, nil, err
}
bi := &debug.BuildInfo{
GoVersion: v.Release,
Main: debug.Module{Path: v.ModuleInfo},
}
// We cannot analyze symbol tables of ancient binaries.
return nil, nil, bi, nil
}
funcSymName := gosym.FuncSymName(bi.GoVersion)
if funcSymName == "" {
return nil, nil, nil, fmt.Errorf("binary built using unsupported Go version: %q", bi.GoVersion)
}
x, err := openExe(bin)
if err != nil {
return nil, nil, nil, err
}
value, base, r, err := x.SymbolInfo(funcSymName)
if err != nil {
if errors.Is(err, ErrNoSymbols) {
// bin is stripped, so return just module info and metadata.
return debugModulesToPackagesModules(bi.Deps), nil, bi, nil
}
return nil, nil, nil, fmt.Errorf("reading %v: %v", funcSymName, err)
}
pclntab, textOffset := x.PCLNTab()
if pclntab == nil {
// If we have build information, but not PCLN table, fall
// back to much higher granularity vulnerability checking.
return debugModulesToPackagesModules(bi.Deps), nil, bi, nil
}
lineTab := gosym.NewLineTable(pclntab, textOffset)
if lineTab == nil {
return nil, nil, nil, errors.New("invalid line table")
}
tab, err := gosym.NewTable(nil, lineTab)
if err != nil {
return nil, nil, nil, err
}
pkgSyms := make(map[Symbol]bool)
for _, f := range tab.Funcs {
if f.Func == nil {
continue
}
pkgName, symName, err := parseName(f.Func.Sym)
if err != nil {
return nil, nil, nil, err
}
pkgSyms[Symbol{pkgName, symName}] = true
// Collect symbols that were inlined in f.
it, err := lineTab.InlineTree(&f, value, base, r)
if err != nil {
return nil, nil, nil, fmt.Errorf("InlineTree: %v", err)
}
for _, ic := range it {
pkgName, symName, err := parseName(&gosym.Sym{Name: ic.Name})
if err != nil {
return nil, nil, nil, err
}
pkgSyms[Symbol{pkgName, symName}] = true
}
}
var syms []Symbol
for ps := range pkgSyms {
syms = append(syms, ps)
}
return debugModulesToPackagesModules(bi.Deps), syms, bi, nil
}
func parseName(s *gosym.Sym) (pkg, sym string, err error) {
symName := s.BaseName()
if r := s.ReceiverName(); r != "" {
if strings.HasPrefix(r, "(*") {
r = strings.Trim(r, "(*)")
}
symName = fmt.Sprintf("%s.%s", r, symName)
}
pkgName := s.PackageName()
if pkgName != "" {
pkgName, err = url.PathUnescape(pkgName)
if err != nil {
return "", "", err
}
}
return pkgName, symName, nil
}

View File

@@ -0,0 +1,221 @@
// Copyright 2021 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build go1.18
// +build go1.18
package buildinfo
// Addition: this file is a trimmed and slightly modified version of debug/buildinfo/buildinfo.go
import (
"bytes"
"debug/elf"
"debug/macho"
"debug/pe"
"fmt"
"sync"
// "internal/xcoff"
"io"
)
// Addition: modification of rawBuildInfo in the original file.
// openExe returns reader r as an exe.
func openExe(r io.ReaderAt) (exe, error) {
data := make([]byte, 16)
if _, err := r.ReadAt(data, 0); err != nil {
return nil, err
}
if bytes.HasPrefix(data, []byte("\x7FELF")) {
e, err := elf.NewFile(r)
if err != nil {
return nil, err
}
return &elfExe{f: e}, nil
}
if bytes.HasPrefix(data, []byte("MZ")) {
e, err := pe.NewFile(r)
if err != nil {
return nil, err
}
return &peExe{r: r, f: e}, nil
}
if bytes.HasPrefix(data, []byte("\xFE\xED\xFA")) || bytes.HasPrefix(data[1:], []byte("\xFA\xED\xFE")) {
e, err := macho.NewFile(r)
if err != nil {
return nil, err
}
return &machoExe{f: e}, nil
}
return nil, fmt.Errorf("unrecognized executable format")
}
type exe interface {
// ReadData reads and returns up to size byte starting at virtual address addr.
ReadData(addr, size uint64) ([]byte, error)
// DataStart returns the virtual address of the segment or section that
// should contain build information. This is either a specially named section
// or the first writable non-zero data segment.
DataStart() uint64
PCLNTab() ([]byte, uint64) // Addition: for constructing symbol table
SymbolInfo(name string) (uint64, uint64, io.ReaderAt, error) // Addition: for inlining purposes
}
// elfExe is the ELF implementation of the exe interface.
type elfExe struct {
f *elf.File
symbols map[string]*elf.Symbol // Addition: symbols in the binary
symbolsOnce sync.Once // Addition: for computing symbols
symbolsErr error // Addition: error for computing symbols
}
func (x *elfExe) ReadData(addr, size uint64) ([]byte, error) {
for _, prog := range x.f.Progs {
if prog.Vaddr <= addr && addr <= prog.Vaddr+prog.Filesz-1 {
n := prog.Vaddr + prog.Filesz - addr
if n > size {
n = size
}
data := make([]byte, n)
_, err := prog.ReadAt(data, int64(addr-prog.Vaddr))
if err != nil {
return nil, err
}
return data, nil
}
}
return nil, fmt.Errorf("address not mapped") // Addition: custom error
}
func (x *elfExe) DataStart() uint64 {
for _, s := range x.f.Sections {
if s.Name == ".go.buildinfo" {
return s.Addr
}
}
for _, p := range x.f.Progs {
if p.Type == elf.PT_LOAD && p.Flags&(elf.PF_X|elf.PF_W) == elf.PF_W {
return p.Vaddr
}
}
return 0
}
// peExe is the PE (Windows Portable Executable) implementation of the exe interface.
type peExe struct {
r io.ReaderAt
f *pe.File
symbols map[string]*pe.Symbol // Addition: symbols in the binary
symbolsOnce sync.Once // Addition: for computing symbols
symbolsErr error // Addition: error for computing symbols
}
func (x *peExe) imageBase() uint64 {
switch oh := x.f.OptionalHeader.(type) {
case *pe.OptionalHeader32:
return uint64(oh.ImageBase)
case *pe.OptionalHeader64:
return oh.ImageBase
}
return 0
}
func (x *peExe) ReadData(addr, size uint64) ([]byte, error) {
addr -= x.imageBase()
for _, sect := range x.f.Sections {
if uint64(sect.VirtualAddress) <= addr && addr <= uint64(sect.VirtualAddress+sect.Size-1) {
n := uint64(sect.VirtualAddress+sect.Size) - addr
if n > size {
n = size
}
data := make([]byte, n)
_, err := sect.ReadAt(data, int64(addr-uint64(sect.VirtualAddress)))
if err != nil {
return nil, err
}
return data, nil
}
}
return nil, fmt.Errorf("address not mapped") // Addition: custom error
}
func (x *peExe) DataStart() uint64 {
// Assume data is first writable section.
const (
IMAGE_SCN_CNT_CODE = 0x00000020
IMAGE_SCN_CNT_INITIALIZED_DATA = 0x00000040
IMAGE_SCN_CNT_UNINITIALIZED_DATA = 0x00000080
IMAGE_SCN_MEM_EXECUTE = 0x20000000
IMAGE_SCN_MEM_READ = 0x40000000
IMAGE_SCN_MEM_WRITE = 0x80000000
IMAGE_SCN_MEM_DISCARDABLE = 0x2000000
IMAGE_SCN_LNK_NRELOC_OVFL = 0x1000000
IMAGE_SCN_ALIGN_32BYTES = 0x600000
)
for _, sect := range x.f.Sections {
if sect.VirtualAddress != 0 && sect.Size != 0 &&
sect.Characteristics&^IMAGE_SCN_ALIGN_32BYTES == IMAGE_SCN_CNT_INITIALIZED_DATA|IMAGE_SCN_MEM_READ|IMAGE_SCN_MEM_WRITE {
return uint64(sect.VirtualAddress) + x.imageBase()
}
}
return 0
}
// machoExe is the Mach-O (Apple macOS/iOS) implementation of the exe interface.
type machoExe struct {
f *macho.File
symbols map[string]*macho.Symbol // Addition: symbols in the binary
symbolsOnce sync.Once // Addition: for computing symbols
symbolsErr error // Addition: error for computing symbols
}
func (x *machoExe) ReadData(addr, size uint64) ([]byte, error) {
for _, load := range x.f.Loads {
seg, ok := load.(*macho.Segment)
if !ok {
continue
}
if seg.Addr <= addr && addr <= seg.Addr+seg.Filesz-1 {
if seg.Name == "__PAGEZERO" {
continue
}
n := seg.Addr + seg.Filesz - addr
if n > size {
n = size
}
data := make([]byte, n)
_, err := seg.ReadAt(data, int64(addr-seg.Addr))
if err != nil {
return nil, err
}
return data, nil
}
}
return nil, fmt.Errorf("address not mapped") // Addition: custom error
}
func (x *machoExe) DataStart() uint64 {
// Look for section named "__go_buildinfo".
for _, sec := range x.f.Sections {
if sec.Name == "__go_buildinfo" {
return sec.Addr
}
}
// Try the first non-empty writable segment.
const RW = 3
for _, load := range x.f.Loads {
seg, ok := load.(*macho.Segment)
if ok && seg.Addr != 0 && seg.Filesz != 0 && seg.Prot == RW && seg.Maxprot == RW {
return seg.Addr
}
}
return 0
}