From 3ac1f1e516dd2d1ada25fc6d592faf50b883f564 Mon Sep 17 00:00:00 2001 From: Dom Del Nano Date: Mon, 5 May 2025 14:50:25 +0000 Subject: [PATCH 1/4] Parse module information from Go binaries .buildinfo Signed-off-by: Dom Del Nano --- src/stirling/obj_tools/BUILD.bazel | 2 + src/stirling/obj_tools/elf_reader.h | 2 +- src/stirling/obj_tools/go_syms.cc | 116 +++++++++++++++- src/stirling/obj_tools/go_syms.h | 25 +++- src/stirling/obj_tools/go_syms_test.cc | 130 ++++++++++++++++-- .../obj_tools/testdata/go/BUILD.bazel | 3 + .../testdata/go/test_buildinfo_with_mods | Bin 0 -> 22402710 bytes .../socket_tracer/uprobe_symaddrs.cc | 4 +- 8 files changed, 260 insertions(+), 22 deletions(-) create mode 100755 src/stirling/obj_tools/testdata/go/test_buildinfo_with_mods diff --git a/src/stirling/obj_tools/BUILD.bazel b/src/stirling/obj_tools/BUILD.bazel index e49289e3a18..9aa3b8b0b29 100644 --- a/src/stirling/obj_tools/BUILD.bazel +++ b/src/stirling/obj_tools/BUILD.bazel @@ -36,6 +36,7 @@ pl_cc_library( deps = [ "//:llvm", "//src/common/fs:cc_library", + "//src/common/json:cc_library", "//src/common/system:cc_library", "//src/shared/types/typespb/wrapper:cc_library", "//src/stirling/utils:cc_library", @@ -135,6 +136,7 @@ pl_cc_test( srcs = ["go_syms_test.cc"], data = [ "//src/stirling/obj_tools/testdata/go:test_binaries", + "//src/stirling/obj_tools/testdata/go:test_buildinfo_with_mods", "//src/stirling/obj_tools/testdata/go:test_go_1_17_binary", "//src/stirling/obj_tools/testdata/go:test_go_1_19_binary", "//src/stirling/obj_tools/testdata/go:test_go_1_21_binary", diff --git a/src/stirling/obj_tools/elf_reader.h b/src/stirling/obj_tools/elf_reader.h index d492122a26e..6ebfe1cf618 100644 --- a/src/stirling/obj_tools/elf_reader.h +++ b/src/stirling/obj_tools/elf_reader.h @@ -167,7 +167,7 @@ class ElfReader { * ElfAddressConverter::VirtualAddrToBinaryAddr is a more appropriate utility to use. * * Certain use cases may require this function, such as cases where the Go toolchain - * embeds virtual addresses within a binary and must be parsed (See ReadGoBuildVersion and + * embeds virtual addresses within a binary and must be parsed (See ReadGoBuildInfo and * ReadGoString in go_syms.cc). */ StatusOr VirtualAddrToBinaryAddr(uint64_t virtual_addr); diff --git a/src/stirling/obj_tools/go_syms.cc b/src/stirling/obj_tools/go_syms.cc index b57275e9897..0f353192363 100644 --- a/src/stirling/obj_tools/go_syms.cc +++ b/src/stirling/obj_tools/go_syms.cc @@ -19,8 +19,6 @@ #include "src/stirling/obj_tools/go_syms.h" #include "src/stirling/utils/binary_decoder.h" -#include - namespace px { namespace stirling { namespace obj_tools { @@ -70,10 +68,97 @@ StatusOr ReadGoString(ElfReader* elf_reader, uint64_t ptr_size, uin return std::string(go_version_bytecode); } +StatusOr ExtractSemVer(const std::string& input) { + size_t go_pos = input.find("go"); // Find "go" + if (go_pos == std::string::npos) { + LOG(ERROR) << "Prefix 'go' not found in input."; + return error::NotFound("Prefix 'go' not found in input."); + } + + size_t start = go_pos + 2; // Move past "go" + size_t end = input.find(" ", start); // Find space delimiter after version + if (end == std::string::npos) { + end = input.size(); // If no space, take the rest of the string + } + + return input.substr(start, end - start); +} + +// This is modeled after go's runtime/debug package +// https://github.com/golang/go/blob/93fb2c90740aef00553c9ce6a7cd4578c2469675/src/runtime/debug/mod.go#L158 +StatusOr ReadModInfo(const std::string& mod) { + BuildInfo build_info; + Module* last_module = nullptr; + + for (std::string_view line : absl::StrSplit(mod, '\n')) { + if (absl::StartsWith(line, "path\t")) { + build_info.path = line.substr(5); + } else if (absl::StartsWith(line, "mod\t")) { + std::vector mod_parts = absl::StrSplit(line.substr(4), '\t'); + + // The sum is optional, so each line must have either 2 or 3 parts. + auto size = mod_parts.size(); + if (size != 2 && size != 3) { + return error::InvalidArgument(absl::Substitute("Invalid mod line format: $0", line)); + } + build_info.main.path = mod_parts[0]; + build_info.main.version = mod_parts[1]; + if (size == 3) { + build_info.main.sum = mod_parts[2]; + } + VLOG(2) << absl::Substitute("mod.path=$0, mod.version=$1, mod.sum=$2", build_info.main.path, + build_info.main.version, build_info.main.sum); + last_module = &build_info.main; + } else if (absl::StartsWith(line, "dep\t")) { + std::vector dep_parts = absl::StrSplit(line.substr(4), '\t'); + + // The sum is optional, so each line must have either 2 or 3 parts. + auto size = dep_parts.size(); + if (size != 2 && size != 3) { + return error::InvalidArgument(absl::Substitute("Invalid dep line format: $0", line)); + } + Module dep; + dep.path = dep_parts[0]; + dep.version = dep_parts[1]; + if (size == 3) { + dep.sum = dep_parts[2]; + } + + build_info.deps.push_back(std::move(dep)); + last_module = &build_info.deps.back(); + + VLOG(2) << absl::Substitute("dep.path=$0, dep.version=$1, dep.sum=$2", dep.path, dep.version, + dep.sum); + + } else if (absl::StartsWith(line, "=>\t")) { + if (last_module == nullptr) { + return error::InvalidArgument( + "Unexpected module replacement line with no preceding module."); + } + std::vector replace_parts = absl::StrSplit(line.substr(3), '\t'); + + if (replace_parts.size() != 3) { + return error::InvalidArgument( + absl::Substitute("Invalid module replacement line format: $0", line)); + } + + std::unique_ptr replacement = std::make_unique(); + replacement->path = replace_parts[0]; + replacement->version = replace_parts[1]; + replacement->sum = replace_parts[2]; + last_module->replace = std::move(replacement); + } + // TODO(ddelnano): Handle the build flags line in the future + // (https://github.com/golang/go/blob/93fb2c90740aef00553c9ce6a7cd4578c2469675/src/runtime/debug/mod.go#L171). + // This is omitted for now since it doesn't help with Go uprobes. + } + return build_info; +} + // 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 ReadGoBuildVersion(ElfReader* elf_reader) { +StatusOr> ReadGoBuildInfo(ElfReader* elf_reader) { PX_ASSIGN_OR_RETURN(ELFIO::section * section, elf_reader->SectionWithName(kGoBuildInfoSection)); int offset = section->get_offset(); PX_ASSIGN_OR_RETURN(std::string_view buildInfoByteCode, @@ -85,6 +170,7 @@ StatusOr ReadGoBuildVersion(ElfReader* elf_reader) { PX_ASSIGN_OR_RETURN(uint8_t ptr_size, binary_decoder.ExtractBEInt()); PX_ASSIGN_OR_RETURN(uint8_t endianness, binary_decoder.ExtractBEInt()); + BuildInfo build_info; // If the endianness has its second bit set, then the go version immediately follows the 32 bit // header specified by the varint encoded string data if ((endianness & 0x2) != 0) { @@ -93,7 +179,16 @@ StatusOr ReadGoBuildVersion(ElfReader* elf_reader) { PX_ASSIGN_OR_RETURN(uint64_t size, binary_decoder.ExtractUVarInt()); PX_ASSIGN_OR_RETURN(std::string_view go_version, binary_decoder.ExtractString(size)); - return std::string(go_version); + + PX_ASSIGN_OR_RETURN(uint64_t mod_size, binary_decoder.ExtractUVarInt()); + PX_ASSIGN_OR_RETURN(std::string_view mod, binary_decoder.ExtractString(mod_size)); + if (mod_size >= 33 && mod.at(mod_size - 17) == '\n') { + mod.remove_prefix(16); + PX_ASSIGN_OR_RETURN(build_info, ReadModInfo(std::string(mod))); + } + + PX_ASSIGN_OR_RETURN(auto s, ExtractSemVer(std::string(go_version))); + return std::make_pair(s, std::move(build_info)); } read_ptr_func_t read_ptr; @@ -141,7 +236,18 @@ StatusOr ReadGoBuildVersion(ElfReader* elf_reader) { PX_ASSIGN_OR_RETURN(uint64_t ptr_addr, elf_reader->VirtualAddrToBinaryAddr(read_ptr(runtime_version_vaddr))); - return ReadGoString(elf_reader, ptr_size, ptr_addr, read_ptr); + PX_ASSIGN_OR_RETURN(auto version, ReadGoString(elf_reader, ptr_size, ptr_addr, read_ptr)); + + PX_ASSIGN_OR_RETURN(uint64_t mod_ptr_addr, elf_reader->VirtualAddrToBinaryAddr( + read_ptr(runtime_version_vaddr) + ptr_size)); + auto mod_status = ReadGoString(elf_reader, ptr_size, mod_ptr_addr, read_ptr); + if (mod_status.ok()) { + std::string mod = mod_status.ValueOrDie(); + } else { + LOG(WARNING) << "Failed to read mod status"; + } + PX_ASSIGN_OR_RETURN(auto s, ExtractSemVer(std::string(version))); + return std::make_pair(s, std::move(build_info)); } // Prefixes used to search for itable symbols in the binary. Follows the format: diff --git a/src/stirling/obj_tools/go_syms.h b/src/stirling/obj_tools/go_syms.h index 56ffe858abd..bdec9c81cd3 100644 --- a/src/stirling/obj_tools/go_syms.h +++ b/src/stirling/obj_tools/go_syms.h @@ -18,8 +18,10 @@ #pragma once +#include #include #include +#include #include #include @@ -33,11 +35,24 @@ namespace obj_tools { // Returns true if the executable is built by Golang. bool IsGoExecutable(ElfReader* elf_reader); -// Returns the build version of a Golang executable. The executable is read through the input -// elf_reader. -// TODO(yzhao): We'll use this to determine the corresponding Golang executable's TLS data -// structures and their offsets. -StatusOr ReadGoBuildVersion(ElfReader* elf_reader); +struct Module { + std::string path; + std::string version; + std::string sum; + std::unique_ptr replace = nullptr; +}; + +struct BuildInfo { + std::string path; + Module main; + std::vector deps; + std::vector> settings; +}; + +StatusOr ReadModInfo(const std::string& mod); +// Returns the build version and buildinfo of a Golang executable. The executable is read through +// the input elf_reader. +StatusOr> ReadGoBuildInfo(ElfReader* elf_reader); // Describes a Golang type that implement an interface. struct IntfImplTypeInfo { diff --git a/src/stirling/obj_tools/go_syms_test.cc b/src/stirling/obj_tools/go_syms_test.cc index f44f1cebed3..1bfb416543a 100644 --- a/src/stirling/obj_tools/go_syms_test.cc +++ b/src/stirling/obj_tools/go_syms_test.cc @@ -39,6 +39,9 @@ constexpr std::string_view kTestGoLittleEndiani386BinaryPath = constexpr std::string_view kTestGoLittleEndianBinaryPath = "src/stirling/obj_tools/testdata/go/test_go_1_17_binary"; +constexpr std::string_view kTestGoWithModulesBinaryPath = + "src/stirling/obj_tools/testdata/go/test_buildinfo_with_mods"; + constexpr std::string_view kTestGoBinaryPath = "src/stirling/obj_tools/testdata/go/test_go_1_19_binary"; constexpr std::string_view kTestGo1_21BinaryPath = @@ -47,25 +50,134 @@ constexpr std::string_view kTestGo1_21BinaryPath = // The "endian agnostic" case refers to where the Go version data is varint encoded // directly within the buildinfo header. See the following reference for more details. // https://github.com/golang/go/blob/1dbbafc70fd3e2c284469ab3e0936c1bb56129f6/src/debug/buildinfo/buildinfo.go#L184C16-L184C16 -TEST(ReadGoBuildVersionTest, BuildinfoEndianAgnostic) { +TEST(ReadGoBuildInfoTest, BuildinfoEndianAgnostic) { const std::string kPath = px::testing::BazelRunfilePath(kTestGoBinaryPath); ASSERT_OK_AND_ASSIGN(std::unique_ptr elf_reader, ElfReader::Create(kPath)); - ASSERT_OK_AND_ASSIGN(std::string version, ReadGoBuildVersion(elf_reader.get())); - EXPECT_THAT(version, StrEq("go1.19.13")); + ASSERT_OK_AND_ASSIGN(auto pair, ReadGoBuildInfo(elf_reader.get())); + auto version = pair.first; + EXPECT_THAT(version, StrEq("1.19.13")); } -TEST(ReadGoBuildVersionTest, BuildinfoLittleEndian) { +TEST(ReadGoBuildInfoTest, BuildinfoLittleEndian) { const std::string kPath = px::testing::BazelRunfilePath(kTestGoLittleEndianBinaryPath); ASSERT_OK_AND_ASSIGN(std::unique_ptr elf_reader, ElfReader::Create(kPath)); - ASSERT_OK_AND_ASSIGN(std::string version, ReadGoBuildVersion(elf_reader.get())); - EXPECT_THAT(version, StrEq("go1.17.13")); + ASSERT_OK_AND_ASSIGN(auto pair, ReadGoBuildInfo(elf_reader.get())); + auto version = pair.first; + EXPECT_THAT(version, StrEq("1.17.13")); +} + +// These tests are modeled off of upstream's +// https://github.com/golang/go/blob/93fb2c90740aef00553c9ce6a7cd4578c2469675/src/runtime/debug/mod_test.go#L23 +TEST(ReadGoBuildInfoTest, BuildinfoPackageBuiltOutsideModule) { + const std::string kBinContent = + "path\trsc.io/fortune\n" + "mod\trsc.io/fortune\tv1.0.0"; + auto build_info_s = ReadModInfo(kBinContent); + EXPECT_OK(build_info_s); + + auto build_info = build_info_s.ConsumeValueOrDie(); + EXPECT_EQ(build_info.path, "rsc.io/fortune"); + EXPECT_EQ(build_info.main.path, "rsc.io/fortune"); + EXPECT_EQ(build_info.main.version, "v1.0.0"); +} + +TEST(ReadGoBuildInfoTest, BuildinfoPackageBuiltStdlib) { + const std::string kBinContent = "path\tcmd/test2json"; + auto build_info_s = ReadModInfo(kBinContent); + EXPECT_OK(build_info_s); + auto build_info = build_info_s.ConsumeValueOrDie(); + EXPECT_EQ(build_info.path, "cmd/test2json"); +} + +TEST(ReadGoBuildInfoTest, BuildinfoPackageBuiltInsideModule) { + const std::string kBinContent = + "go\t1.18\n" + "path\texample.com/m\n" + "mod\texample.com/m\t(devel)\n" + "build\t-compiler=gc"; + auto build_info_s = ReadModInfo(kBinContent); + EXPECT_OK(build_info_s); + + auto build_info = build_info_s.ConsumeValueOrDie(); + EXPECT_EQ(build_info.path, "example.com/m"); + EXPECT_EQ(build_info.main.path, "example.com/m"); + EXPECT_EQ(build_info.main.version, "(devel)"); + EXPECT_EQ(build_info.main.replace, nullptr); + EXPECT_EQ(build_info.deps.size(), 0); +} + +TEST(ReadGoBuildInfoTest, BuildinfoWithModules) { + const std::string kPath = px::testing::BazelRunfilePath(kTestGoWithModulesBinaryPath); + ASSERT_OK_AND_ASSIGN(std::unique_ptr elf_reader, ElfReader::Create(kPath)); + ASSERT_OK_AND_ASSIGN(auto pair, ReadGoBuildInfo(elf_reader.get())); + auto version = pair.first; + EXPECT_THAT(version, StrEq("1.23.0")); + + auto& build_info = pair.second; + // Validate main module path. + EXPECT_THAT(build_info.path, + StrEq("go.opentelemetry.io/auto/internal/tools/inspect/cmd/offsetgen")); + + // Validate main module metadata. + EXPECT_THAT(build_info.main.path, StrEq("go.opentelemetry.io/auto/internal/tools")); + EXPECT_THAT(build_info.main.version, StrEq("(devel)")); + EXPECT_EQ(build_info.main.replace, nullptr); + + // Validate module dependencies. + EXPECT_THAT(build_info.deps, + UnorderedElementsAre( + Field(&Module::path, StrEq("github.com/Masterminds/semver/v3")), + Field(&Module::path, StrEq("github.com/cilium/ebpf")), + Field(&Module::path, StrEq("github.com/distribution/reference")), + Field(&Module::path, StrEq("github.com/docker/docker")), + Field(&Module::path, StrEq("github.com/docker/go-connections")), + Field(&Module::path, StrEq("github.com/docker/go-units")), + Field(&Module::path, StrEq("github.com/felixge/httpsnoop")), + Field(&Module::path, StrEq("github.com/go-logr/logr")), + Field(&Module::path, StrEq("github.com/go-logr/stdr")), + Field(&Module::path, StrEq("github.com/gogo/protobuf")), + Field(&Module::path, StrEq("github.com/moby/docker-image-spec")), + Field(&Module::path, StrEq("github.com/opencontainers/go-digest")), + Field(&Module::path, StrEq("github.com/opencontainers/image-spec")), + Field(&Module::path, StrEq("github.com/pkg/errors")), + Field(&Module::path, StrEq("go.opentelemetry.io/auto")), + Field(&Module::path, StrEq("go.opentelemetry.io/auto/sdk")), + Field(&Module::path, StrEq("go.opentelemetry.io/collector/pdata")), + Field(&Module::path, + StrEq("go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp")), + Field(&Module::path, StrEq("go.opentelemetry.io/otel")), + Field(&Module::path, StrEq("go.opentelemetry.io/otel/metric")), + Field(&Module::path, StrEq("go.opentelemetry.io/otel/trace")), + Field(&Module::path, StrEq("go.uber.org/multierr")), + Field(&Module::path, StrEq("golang.org/x/arch")), + Field(&Module::path, StrEq("golang.org/x/net")), + Field(&Module::path, StrEq("golang.org/x/sync")), + Field(&Module::path, StrEq("golang.org/x/sys")), + Field(&Module::path, StrEq("golang.org/x/text")), + Field(&Module::path, StrEq("google.golang.org/genproto/googleapis/rpc")), + Field(&Module::path, StrEq("google.golang.org/grpc")), + Field(&Module::path, StrEq("google.golang.org/protobuf")))); + + // Validate replaced modules. + EXPECT_THAT(build_info.deps, + Contains(AllOf(Field(&Module::path, StrEq("go.opentelemetry.io/auto")), + Field(&Module::replace, + Pointee(AllOf(Field(&Module::path, StrEq("../../")), + Field(&Module::version, StrEq("(devel)")))))))); + + EXPECT_THAT(build_info.deps, + Contains(AllOf(Field(&Module::path, StrEq("go.opentelemetry.io/auto/sdk")), + Field(&Module::replace, + Pointee(AllOf(Field(&Module::path, StrEq("../../sdk")), + Field(&Module::version, StrEq("(devel)")))))))); } -TEST(ReadGoBuildVersionTest, BuildinfoLittleEndiani386) { +TEST(ReadGoBuildInfoTest, BuildinfoLittleEndiani386) { const std::string kPath = px::testing::BazelRunfilePath(kTestGoLittleEndiani386BinaryPath); ASSERT_OK_AND_ASSIGN(std::unique_ptr elf_reader, ElfReader::Create(kPath)); - ASSERT_OK_AND_ASSIGN(std::string version, ReadGoBuildVersion(elf_reader.get())); - EXPECT_THAT(version, StrEq("go1.13.15")); + ASSERT_OK_AND_ASSIGN(auto pair, ReadGoBuildInfo(elf_reader.get())); + auto version = pair.first; + EXPECT_THAT(version, StrEq("1.13.15")); } TEST(IsGoExecutableTest, WorkingOnBasicGoBinary) { diff --git a/src/stirling/obj_tools/testdata/go/BUILD.bazel b/src/stirling/obj_tools/testdata/go/BUILD.bazel index 95c4bae03ff..0a5d26505f4 100644 --- a/src/stirling/obj_tools/testdata/go/BUILD.bazel +++ b/src/stirling/obj_tools/testdata/go/BUILD.bazel @@ -75,6 +75,9 @@ filegroup( # (https://github.com/golang/go/blob/1dbbafc70fd3e2c284469ab3e0936c1bb56129f6/src/debug/buildinfo/buildinfo.go#L189-L190) # and so it cannot be tested without compiling against an older Go version. "test_go_1_17_binary", + # TODO(ddelnano): rules_go doesn't support populating .buildinfo with dependency information (https://github.com/bazel-contrib/rules_go/issues/3090). + # Once this is supported, test_buildinfo_with_mods should be replaced with a bazel built binary. + "test_buildinfo_with_mods", ":test_go_1_18_binary", ":test_go_1_19_binary", ":test_go_1_20_binary", diff --git a/src/stirling/obj_tools/testdata/go/test_buildinfo_with_mods b/src/stirling/obj_tools/testdata/go/test_buildinfo_with_mods new file mode 100755 index 0000000000000000000000000000000000000000..9af7f76a19b4a2da072bf13a6e2d87cf977e8a39 GIT binary patch literal 22402710 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