Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 77 additions & 21 deletions src/stirling/obj_tools/go_syms.cc
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,41 @@ StatusOr<std::string> ReadGoString(ElfReader* elf_reader, uint64_t ptr_size, uin
return std::string(go_version_bytecode);
}

// Reads the Go version from the runtime.buildVersion symbol in the binary's data section.
// This function serves as a fallback for older Go binaries (Go 1.11 and earlier) that don't
// have the .go.buildinfo section. It extracts the version string from the runtime.buildVersion
// symbol and strips the "go" prefix to return just the semantic version (e.g., "1.11.13").
// Note: This function does not work correctly with 32-bit binaries due to gostring structure
// size differences (see https://github.com/pixie-io/pixie/issues/1300).
// See https://github.com/pixie-io/pixie/issues/1318 for context.
StatusOr<std::pair<std::string, BuildInfo>> ReadBuildVersion(ElfReader* elf_reader) {
BuildInfo build_info;
PX_ASSIGN_OR_RETURN(ElfReader::SymbolInfo symbol,
elf_reader->SearchTheOnlySymbol(kGoBuildVersionSymbol));

// The address of this symbol points to a Golang string object.
// But the size is for the symbol table entry, not this string object.
symbol.size = sizeof(gostring);
PX_ASSIGN_OR_RETURN(utils::u8string version_code, elf_reader->SymbolByteCode(".data", symbol));

// We can't guarantee the alignment on version_string so we make a copy into an aligned address.
gostring version_string;
std::memcpy(&version_string, version_code.data(), sizeof(version_string));

ElfReader::SymbolInfo version_symbol;
version_symbol.address = reinterpret_cast<uint64_t>(version_string.ptr);
version_symbol.size = version_string.len;

PX_ASSIGN_OR_RETURN(utils::u8string str, elf_reader->SymbolByteCode(".data", version_symbol));
// Strip go prefix from the version string.
if (str.size() >= 2 &&
str.substr(0, 2) == utils::u8string(reinterpret_cast<const unsigned char*>("go"), 2)) {
str.erase(0, 2); // Remove "go" from the beginning
}
return std::make_pair(std::string(reinterpret_cast<const char*>(str.data()), str.size()),
std::move(build_info));
}

// Extracts the semantic version from a Go version string (e.g., "go1.20.3").
// This is how the version is formatted in the buildinfo header.
StatusOr<std::string> ExtractSemVer(const std::string& input) {
Expand Down Expand Up @@ -157,20 +192,32 @@ StatusOr<BuildInfo> ReadModInfo(const std::string& mod) {
return build_info;
}

// Macro that calls ReadBuildVersion on failure before returning
#define PX_ASSIGN_OR_RETURN_WITH_FALLBACK(lhs, rexpr, elf_reader_ptr) \
PX_ASSIGN_OR( \
lhs, rexpr, auto fallback_result = ReadBuildVersion(elf_reader_ptr); \
if (fallback_result.ok()) { \
return fallback_result.ConsumeValueOrDie(); \
} return __s__.status())

// Reads the buildinfo header embedded in the .go.buildinfo ELF section in order to determine the go
// toolchain version. This function emulates what the go version cli performs as seen
// https://github.com/golang/go/blob/cb7a091d729eab75ccfdaeba5a0605f05addf422/src/debug/buildinfo/buildinfo.go#L151-L221
StatusOr<std::pair<std::string, BuildInfo>> ReadGoBuildInfo(ElfReader* elf_reader) {
PX_ASSIGN_OR_RETURN(ELFIO::section * section, elf_reader->SectionWithName(kGoBuildInfoSection));
PX_ASSIGN_OR_RETURN_WITH_FALLBACK(ELFIO::section * section,
elf_reader->SectionWithName(kGoBuildInfoSection), elf_reader);
int offset = section->get_offset();
PX_ASSIGN_OR_RETURN(std::string_view buildInfoByteCode,
elf_reader->BinaryByteCode<char>(offset, 64 * 1024));
PX_ASSIGN_OR_RETURN_WITH_FALLBACK(std::string_view buildInfoByteCode,
elf_reader->BinaryByteCode<char>(offset, 64 * 1024),
elf_reader);

BinaryDecoder binary_decoder(buildInfoByteCode);

PX_CHECK_OK(binary_decoder.ExtractStringUntil(kGoBuildInfoMagic));
PX_ASSIGN_OR_RETURN(uint8_t ptr_size, binary_decoder.ExtractBEInt<uint8_t>());
PX_ASSIGN_OR_RETURN(uint8_t endianness, binary_decoder.ExtractBEInt<uint8_t>());
PX_ASSIGN_OR_RETURN_WITH_FALLBACK(uint8_t ptr_size, binary_decoder.ExtractBEInt<uint8_t>(),
elf_reader);
PX_ASSIGN_OR_RETURN_WITH_FALLBACK(uint8_t endianness, binary_decoder.ExtractBEInt<uint8_t>(),
elf_reader);

BuildInfo build_info;
std::string go_version;
Expand All @@ -181,11 +228,12 @@ StatusOr<std::pair<std::string, BuildInfo>> ReadGoBuildInfo(ElfReader* elf_reade
// Skip the remaining 16 bytes of buildinfo header
PX_CHECK_OK(binary_decoder.ExtractBufIgnore(16));

PX_ASSIGN_OR_RETURN(uint64_t size, binary_decoder.ExtractUVarInt());
PX_ASSIGN_OR_RETURN(go_version, binary_decoder.ExtractString(size));
PX_ASSIGN_OR_RETURN_WITH_FALLBACK(uint64_t size, binary_decoder.ExtractUVarInt(), elf_reader);
PX_ASSIGN_OR_RETURN_WITH_FALLBACK(go_version, binary_decoder.ExtractString(size), elf_reader);

PX_ASSIGN_OR_RETURN(uint64_t mod_size, binary_decoder.ExtractUVarInt());
PX_ASSIGN_OR_RETURN(mod_info, binary_decoder.ExtractString(mod_size));
PX_ASSIGN_OR_RETURN_WITH_FALLBACK(uint64_t mod_size, binary_decoder.ExtractUVarInt(),
elf_reader);
PX_ASSIGN_OR_RETURN_WITH_FALLBACK(mod_info, binary_decoder.ExtractString(mod_size), elf_reader);
} else {
read_ptr_func_t read_ptr;
switch (endianness) {
Expand Down Expand Up @@ -227,19 +275,25 @@ StatusOr<std::pair<std::string, BuildInfo>> ReadGoBuildInfo(ElfReader* elf_reade
}

// Reads the virtual address location of the runtime.buildVersion symbol.
PX_ASSIGN_OR_RETURN(auto runtime_version_vaddr,
binary_decoder.ExtractString<u8string_view::value_type>(ptr_size));
PX_ASSIGN_OR_RETURN(auto mod_info_vaddr,
binary_decoder.ExtractString<u8string_view::value_type>(ptr_size));
PX_ASSIGN_OR_RETURN(uint64_t ptr_addr,
elf_reader->VirtualAddrToBinaryAddr(read_ptr(runtime_version_vaddr)));

PX_ASSIGN_OR_RETURN(go_version, ReadGoString(elf_reader, ptr_size, ptr_addr, read_ptr));
PX_ASSIGN_OR_RETURN_WITH_FALLBACK(
auto runtime_version_vaddr,
binary_decoder.ExtractString<u8string_view::value_type>(ptr_size), elf_reader);
PX_ASSIGN_OR_RETURN_WITH_FALLBACK(
auto mod_info_vaddr, binary_decoder.ExtractString<u8string_view::value_type>(ptr_size),
elf_reader);
PX_ASSIGN_OR_RETURN_WITH_FALLBACK(
uint64_t ptr_addr, elf_reader->VirtualAddrToBinaryAddr(read_ptr(runtime_version_vaddr)),
elf_reader);

PX_ASSIGN_OR_RETURN_WITH_FALLBACK(
go_version, ReadGoString(elf_reader, ptr_size, ptr_addr, read_ptr), elf_reader);

auto mod_ptr_addr_s = elf_reader->VirtualAddrToBinaryAddr(read_ptr(mod_info_vaddr));
if (mod_ptr_addr_s.ok()) {
PX_ASSIGN_OR_RETURN(mod_info, ReadGoString(elf_reader, ptr_size,
mod_ptr_addr_s.ConsumeValueOrDie(), read_ptr));
PX_ASSIGN_OR_RETURN_WITH_FALLBACK(
mod_info,
ReadGoString(elf_reader, ptr_size, mod_ptr_addr_s.ConsumeValueOrDie(), read_ptr),
elf_reader);
}
}

Expand All @@ -251,13 +305,15 @@ StatusOr<std::pair<std::string, BuildInfo>> ReadGoBuildInfo(ElfReader* elf_reade
// https://github.com/golang/go/blob/cb7a091d729eab75ccfdaeba5a0605f05addf422/src/debug/buildinfo/buildinfo.go#L214-L215
if (mod_size >= 33 && mod_info.at(mod_size - 17) == '\n') {
mod_info.erase(0, 16);
PX_ASSIGN_OR_RETURN(build_info, ReadModInfo(mod_info));
PX_ASSIGN_OR_RETURN_WITH_FALLBACK(build_info, ReadModInfo(mod_info), elf_reader);
}
}
PX_ASSIGN_OR_RETURN(auto s, ExtractSemVer(go_version));
PX_ASSIGN_OR_RETURN_WITH_FALLBACK(auto s, ExtractSemVer(go_version), elf_reader);
return std::make_pair(s, std::move(build_info));
}

#undef PX_ASSIGN_OR_RETURN_WITH_FALLBACK

// Prefixes used to search for itable symbols in the binary. Follows the format:
// <prefix>.<type_name>,<interface_name>. i.e. go.itab.<type_name>,<interface_name>
constexpr std::array<std::string_view, 2> kITablePrefixes = {
Expand Down
7 changes: 7 additions & 0 deletions src/stirling/obj_tools/go_syms.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ StatusOr<BuildInfo> ReadModInfo(const std::string& mod);
// the input elf_reader.
StatusOr<std::pair<std::string, BuildInfo>> ReadGoBuildInfo(ElfReader* elf_reader);

// Returns the build version by reading the runtime.buildVersion symbol from a Golang executable.
// This is a fallback method for older Go binaries (Go 1.11 and earlier) that don't have the
// .go.buildinfo section. The version string has the "go" prefix stripped (e.g., "1.11.13").
// Note: This function does not work correctly with 32-bit binaries due to gostring structure size
// differences.
StatusOr<std::pair<std::string, BuildInfo>> ReadBuildVersion(ElfReader* elf_reader);

// Describes a Golang type that implement an interface.
struct IntfImplTypeInfo {
// The name of the type that implements a given interface.
Expand Down
21 changes: 21 additions & 0 deletions src/stirling/obj_tools/go_syms_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ using ::testing::UnorderedElementsAre;
constexpr std::string_view kTestGoLittleEndiani386BinaryPath =
"src/stirling/obj_tools/testdata/go/test_go1_13_i386_binary";

constexpr std::string_view kTestGo1_11BinaryPath =
"src/stirling/obj_tools/testdata/go/test_go_1_11_binary";

constexpr std::string_view kTestGoLittleEndianBinaryPath =
"src/stirling/obj_tools/testdata/go/test_go_1_17_binary";

Expand Down Expand Up @@ -186,6 +189,24 @@ TEST(ReadGoBuildInfoTest, BuildinfoLittleEndiani386) {
EXPECT_THAT(buildinfo.main.version, StrEq("(devel)"));
}

TEST(ReadGoBuildInfoTest, BuildVersionLittleEndiani386) {
const std::string kPath = px::testing::BazelRunfilePath(kTestGoLittleEndiani386BinaryPath);
ASSERT_OK_AND_ASSIGN(std::unique_ptr<ElfReader> elf_reader, ElfReader::Create(kPath));
auto result = ReadBuildVersion(elf_reader.get());

EXPECT_FALSE(result.ok());
EXPECT_THAT(result.msg(), ::testing::HasSubstr("Refusing to preallocate that much memory"));
}

TEST(ReadGoBuildInfoTest, BuildVersionGo1_11) {
const std::string kPath = px::testing::BazelRunfilePath(kTestGo1_11BinaryPath);
ASSERT_OK_AND_ASSIGN(std::unique_ptr<ElfReader> elf_reader, ElfReader::Create(kPath));
ASSERT_OK_AND_ASSIGN(auto pair, ReadBuildVersion(elf_reader.get()));

auto version = pair.first;
EXPECT_THAT(version, StrEq("1.11.13"));
}

TEST(IsGoExecutableTest, WorkingOnBasicGoBinary) {
const std::string kPath = px::testing::BazelRunfilePath(kTestGoBinaryPath);
ASSERT_OK_AND_ASSIGN(std::unique_ptr<ElfReader> elf_reader, ElfReader::Create(kPath));
Expand Down
3 changes: 3 additions & 0 deletions src/stirling/obj_tools/testdata/go/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ filegroup(
# These older 32 bit binaries have been the source of bugs, so this test case verifies we don't
# introduce a regression (https://github.com/pixie-io/pixie/issues/1300).
"test_go1_13_i386_binary",
# This binary was built with go 1.11. This ensures that ReadBuildVersion works for Go 1.11 and earlier
# binaries that don't have the .go.buildinfo section.
"test_go_1_11_binary",
# This binary was built with go 1.17. This ensures that the 64 bit little endian case buildinfo logic is tested.
# (https://github.com/golang/go/blob/1dbbafc70fd3e2c284469ab3e0936c1bb56129f6/src/debug/buildinfo/buildinfo.go#L192-L208).
# Newer versions of go generate the endian agnostic buildinfo header
Expand Down
Binary file not shown.