Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
44 changes: 43 additions & 1 deletion 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 @@ -161,7 +196,14 @@ StatusOr<BuildInfo> ReadModInfo(const std::string& mod) {
// 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));
auto build_info_section_s = elf_reader->SectionWithName(kGoBuildInfoSection);

if (!build_info_section_s.ok()) {
// If the section is not found, it means that the binary is either not a Go binary or it was
// built with an older version of Go that does not include the .go.buildinfo section.
return ReadBuildVersion(elf_reader);
}
auto section = build_info_section_s.ConsumeValueOrDie();
int offset = section->get_offset();
PX_ASSIGN_OR_RETURN(std::string_view buildInfoByteCode,
elf_reader->BinaryByteCode<char>(offset, 64 * 1024));
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.