diff --git a/src/stirling/obj_tools/go_syms.cc b/src/stirling/obj_tools/go_syms.cc index 6d71e95758d..1788a318f69 100644 --- a/src/stirling/obj_tools/go_syms.cc +++ b/src/stirling/obj_tools/go_syms.cc @@ -68,6 +68,41 @@ StatusOr 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> 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(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("go"), 2)) { + str.erase(0, 2); // Remove "go" from the beginning + } + return std::make_pair(std::string(reinterpret_cast(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 ExtractSemVer(const std::string& input) { @@ -161,7 +196,14 @@ StatusOr 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> 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(offset, 64 * 1024)); diff --git a/src/stirling/obj_tools/go_syms.h b/src/stirling/obj_tools/go_syms.h index bdec9c81cd3..5bfb15e1ca7 100644 --- a/src/stirling/obj_tools/go_syms.h +++ b/src/stirling/obj_tools/go_syms.h @@ -54,6 +54,13 @@ StatusOr ReadModInfo(const std::string& mod); // the input elf_reader. StatusOr> 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> ReadBuildVersion(ElfReader* elf_reader); + // Describes a Golang type that implement an interface. struct IntfImplTypeInfo { // The name of the type that implements a given interface. diff --git a/src/stirling/obj_tools/go_syms_test.cc b/src/stirling/obj_tools/go_syms_test.cc index ef82ee31399..e086033b850 100644 --- a/src/stirling/obj_tools/go_syms_test.cc +++ b/src/stirling/obj_tools/go_syms_test.cc @@ -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"; @@ -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 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 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 elf_reader, ElfReader::Create(kPath)); diff --git a/src/stirling/obj_tools/testdata/go/BUILD.bazel b/src/stirling/obj_tools/testdata/go/BUILD.bazel index 0a5d26505f4..8ec81482acc 100644 --- a/src/stirling/obj_tools/testdata/go/BUILD.bazel +++ b/src/stirling/obj_tools/testdata/go/BUILD.bazel @@ -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 diff --git a/src/stirling/obj_tools/testdata/go/test_go_1_11_binary b/src/stirling/obj_tools/testdata/go/test_go_1_11_binary new file mode 100755 index 00000000000..1fde1cce56d Binary files /dev/null and b/src/stirling/obj_tools/testdata/go/test_go_1_11_binary differ