Skip to content

Commit 9f0dc1c

Browse files
committed
Parse module information from Go binaries .buildinfo
Signed-off-by: Dom Del Nano <ddelnano@gmail.com>
1 parent 301198f commit 9f0dc1c

8 files changed

Lines changed: 259 additions & 22 deletions

File tree

src/stirling/obj_tools/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ pl_cc_library(
3636
deps = [
3737
"//:llvm",
3838
"//src/common/fs:cc_library",
39+
"//src/common/json:cc_library",
3940
"//src/common/system:cc_library",
4041
"//src/shared/types/typespb/wrapper:cc_library",
4142
"//src/stirling/utils:cc_library",
@@ -134,6 +135,7 @@ pl_cc_test(
134135
name = "go_syms_test",
135136
srcs = ["go_syms_test.cc"],
136137
data = [
138+
"//src/stirling/obj_tools/testdata/go:offsetgen",
137139
"//src/stirling/obj_tools/testdata/go:test_binaries",
138140
"//src/stirling/obj_tools/testdata/go:test_go_1_17_binary",
139141
"//src/stirling/obj_tools/testdata/go:test_go_1_19_binary",

src/stirling/obj_tools/elf_reader.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ class ElfReader {
167167
* ElfAddressConverter::VirtualAddrToBinaryAddr is a more appropriate utility to use.
168168
*
169169
* Certain use cases may require this function, such as cases where the Go toolchain
170-
* embeds virtual addresses within a binary and must be parsed (See ReadGoBuildVersion and
170+
* embeds virtual addresses within a binary and must be parsed (See ReadGoBuildInfo and
171171
* ReadGoString in go_syms.cc).
172172
*/
173173
StatusOr<uint64_t> VirtualAddrToBinaryAddr(uint64_t virtual_addr);

src/stirling/obj_tools/go_syms.cc

Lines changed: 112 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@
1919
#include "src/stirling/obj_tools/go_syms.h"
2020
#include "src/stirling/utils/binary_decoder.h"
2121

22-
#include <utility>
23-
2422
namespace px {
2523
namespace stirling {
2624
namespace obj_tools {
@@ -70,10 +68,98 @@ StatusOr<std::string> ReadGoString(ElfReader* elf_reader, uint64_t ptr_size, uin
7068
return std::string(go_version_bytecode);
7169
}
7270

71+
StatusOr<std::string> ExtractSemVer(const std::string& input) {
72+
size_t go_pos = input.find("go"); // Find "go"
73+
if (go_pos == std::string::npos) {
74+
LOG(ERROR) << "Prefix 'go' not found in input.";
75+
return error::NotFound("Prefix 'go' not found in input.");
76+
}
77+
78+
size_t start = go_pos + 2; // Move past "go"
79+
size_t end = input.find(" ", start); // Find space delimiter after version
80+
if (end == std::string::npos) {
81+
end = input.size(); // If no space, take the rest of the string
82+
}
83+
84+
return input.substr(start, end - start);
85+
}
86+
87+
// This is modeled after go's runtime/debug package
88+
// https://github.com/golang/go/blob/93fb2c90740aef00553c9ce6a7cd4578c2469675/src/runtime/debug/mod.go#L158
89+
StatusOr<BuildInfo> ReadModInfo(const std::string& mod) {
90+
BuildInfo build_info;
91+
Module* last_module = nullptr;
92+
93+
for (std::string_view line : absl::StrSplit(mod, '\n')) {
94+
if (absl::StartsWith(line, "path\t")) {
95+
build_info.path = line.substr(5);
96+
} else if (absl::StartsWith(line, "mod\t")) {
97+
std::vector<std::string_view> mod_parts = absl::StrSplit(line.substr(4), '\t');
98+
99+
// The sum is optional, so each line must have either 2 or 3 parts.
100+
auto size = mod_parts.size();
101+
if (size != 2 && size != 3) {
102+
return error::InvalidArgument(absl::Substitute("Invalid mod line format: $0", line));
103+
}
104+
build_info.main.path = mod_parts[0];
105+
build_info.main.version = mod_parts[1];
106+
if (size == 3) {
107+
build_info.main.sum = mod_parts[2];
108+
}
109+
build_info.main.sum = mod_parts[2];
110+
VLOG(2) << absl::Substitute("mod.path=$0, mod.version=$1, mod.sum=$2", build_info.main.path,
111+
build_info.main.version, build_info.main.sum);
112+
last_module = &build_info.main;
113+
} else if (absl::StartsWith(line, "dep\t")) {
114+
std::vector<std::string_view> dep_parts = absl::StrSplit(line.substr(4), '\t');
115+
116+
// The sum is optional, so each line must have either 2 or 3 parts.
117+
auto size = dep_parts.size();
118+
if (size != 2 && size != 3) {
119+
return error::InvalidArgument(absl::Substitute("Invalid dep line format: $0", line));
120+
}
121+
Module dep;
122+
dep.path = dep_parts[0];
123+
dep.version = dep_parts[1];
124+
if (size == 3) {
125+
dep.sum = dep_parts[2];
126+
}
127+
128+
build_info.deps.push_back(std::move(dep));
129+
last_module = &build_info.deps.back();
130+
131+
VLOG(2) << absl::Substitute("dep.path=$0, dep.version=$1, dep.sum=$2", dep.path, dep.version,
132+
dep.sum);
133+
134+
} else if (absl::StartsWith(line, "=>\t")) {
135+
if (last_module == nullptr) {
136+
return error::InvalidArgument(
137+
"Unexpected module replacement line with no preceding module.");
138+
}
139+
std::vector<std::string_view> replace_parts = absl::StrSplit(line.substr(3), '\t');
140+
141+
if (replace_parts.size() != 3) {
142+
return error::InvalidArgument(
143+
absl::Substitute("Invalid module replacement line format: $0", line));
144+
}
145+
146+
std::unique_ptr<Module> replacement = std::make_unique<Module>();
147+
replacement->path = replace_parts[0];
148+
replacement->version = replace_parts[1];
149+
replacement->sum = replace_parts[2];
150+
last_module->replace = std::move(replacement);
151+
}
152+
// TODO(ddelnano): Handle the build flags line in the future
153+
// (https://github.com/golang/go/blob/93fb2c90740aef00553c9ce6a7cd4578c2469675/src/runtime/debug/mod.go#L171).
154+
// This is omitted for now since it doesn't help with Go uprobes.
155+
}
156+
return build_info;
157+
}
158+
73159
// Reads the buildinfo header embedded in the .go.buildinfo ELF section in order to determine the go
74160
// toolchain version. This function emulates what the go version cli performs as seen
75161
// https://github.com/golang/go/blob/cb7a091d729eab75ccfdaeba5a0605f05addf422/src/debug/buildinfo/buildinfo.go#L151-L221
76-
StatusOr<std::string> ReadGoBuildVersion(ElfReader* elf_reader) {
162+
StatusOr<std::pair<std::string, BuildInfo>> ReadGoBuildInfo(ElfReader* elf_reader) {
77163
PX_ASSIGN_OR_RETURN(ELFIO::section * section, elf_reader->SectionWithName(kGoBuildInfoSection));
78164
int offset = section->get_offset();
79165
PX_ASSIGN_OR_RETURN(std::string_view buildInfoByteCode,
@@ -85,6 +171,7 @@ StatusOr<std::string> ReadGoBuildVersion(ElfReader* elf_reader) {
85171
PX_ASSIGN_OR_RETURN(uint8_t ptr_size, binary_decoder.ExtractBEInt<uint8_t>());
86172
PX_ASSIGN_OR_RETURN(uint8_t endianness, binary_decoder.ExtractBEInt<uint8_t>());
87173

174+
BuildInfo build_info;
88175
// If the endianness has its second bit set, then the go version immediately follows the 32 bit
89176
// header specified by the varint encoded string data
90177
if ((endianness & 0x2) != 0) {
@@ -93,7 +180,16 @@ StatusOr<std::string> ReadGoBuildVersion(ElfReader* elf_reader) {
93180

94181
PX_ASSIGN_OR_RETURN(uint64_t size, binary_decoder.ExtractUVarInt());
95182
PX_ASSIGN_OR_RETURN(std::string_view go_version, binary_decoder.ExtractString(size));
96-
return std::string(go_version);
183+
184+
PX_ASSIGN_OR_RETURN(uint64_t mod_size, binary_decoder.ExtractUVarInt());
185+
PX_ASSIGN_OR_RETURN(std::string_view mod, binary_decoder.ExtractString(mod_size));
186+
if (mod_size >= 33 && mod.at(mod_size - 17) == '\n') {
187+
mod.remove_prefix(16);
188+
PX_ASSIGN_OR_RETURN(build_info, ReadModInfo(std::string(mod)));
189+
}
190+
191+
PX_ASSIGN_OR_RETURN(auto s, ExtractSemVer(std::string(go_version)));
192+
return std::make_pair(s, std::move(build_info));
97193
}
98194

99195
read_ptr_func_t read_ptr;
@@ -141,7 +237,18 @@ StatusOr<std::string> ReadGoBuildVersion(ElfReader* elf_reader) {
141237
PX_ASSIGN_OR_RETURN(uint64_t ptr_addr,
142238
elf_reader->VirtualAddrToBinaryAddr(read_ptr(runtime_version_vaddr)));
143239

144-
return ReadGoString(elf_reader, ptr_size, ptr_addr, read_ptr);
240+
PX_ASSIGN_OR_RETURN(auto version, ReadGoString(elf_reader, ptr_size, ptr_addr, read_ptr));
241+
242+
PX_ASSIGN_OR_RETURN(uint64_t mod_ptr_addr, elf_reader->VirtualAddrToBinaryAddr(
243+
read_ptr(runtime_version_vaddr) + ptr_size));
244+
auto mod_status = ReadGoString(elf_reader, ptr_size, mod_ptr_addr, read_ptr);
245+
if (mod_status.ok()) {
246+
std::string mod = mod_status.ValueOrDie();
247+
} else {
248+
LOG(WARNING) << "Failed to read mod status";
249+
}
250+
PX_ASSIGN_OR_RETURN(auto s, ExtractSemVer(std::string(version)));
251+
return std::make_pair(s, std::move(build_info));
145252
}
146253

147254
// Prefixes used to search for itable symbols in the binary. Follows the format:

src/stirling/obj_tools/go_syms.h

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@
1818

1919
#pragma once
2020

21+
#include <memory>
2122
#include <string>
2223
#include <string_view>
24+
#include <utility>
2325
#include <vector>
2426

2527
#include <absl/container/flat_hash_map.h>
@@ -33,11 +35,24 @@ namespace obj_tools {
3335
// Returns true if the executable is built by Golang.
3436
bool IsGoExecutable(ElfReader* elf_reader);
3537

36-
// Returns the build version of a Golang executable. The executable is read through the input
37-
// elf_reader.
38-
// TODO(yzhao): We'll use this to determine the corresponding Golang executable's TLS data
39-
// structures and their offsets.
40-
StatusOr<std::string> ReadGoBuildVersion(ElfReader* elf_reader);
38+
struct Module {
39+
std::string path;
40+
std::string version;
41+
std::string sum;
42+
std::unique_ptr<Module> replace = nullptr;
43+
};
44+
45+
struct BuildInfo {
46+
std::string path;
47+
Module main;
48+
std::vector<Module> deps;
49+
std::vector<std::pair<std::string, std::string>> settings;
50+
};
51+
52+
StatusOr<BuildInfo> ReadModInfo(const std::string& mod);
53+
// Returns the build version and buildinfo of a Golang executable. The executable is read through
54+
// the input elf_reader.
55+
StatusOr<std::pair<std::string, BuildInfo>> ReadGoBuildInfo(ElfReader* elf_reader);
4156

4257
// Describes a Golang type that implement an interface.
4358
struct IntfImplTypeInfo {

src/stirling/obj_tools/go_syms_test.cc

Lines changed: 121 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ constexpr std::string_view kTestGoLittleEndiani386BinaryPath =
3939
constexpr std::string_view kTestGoLittleEndianBinaryPath =
4040
"src/stirling/obj_tools/testdata/go/test_go_1_17_binary";
4141

42+
constexpr std::string_view kTestGoWithModulesBinaryPath =
43+
"src/stirling/obj_tools/testdata/go/test_buildinfo_with_mods";
44+
4245
constexpr std::string_view kTestGoBinaryPath =
4346
"src/stirling/obj_tools/testdata/go/test_go_1_19_binary";
4447
constexpr std::string_view kTestGo1_21BinaryPath =
@@ -47,25 +50,134 @@ constexpr std::string_view kTestGo1_21BinaryPath =
4750
// The "endian agnostic" case refers to where the Go version data is varint encoded
4851
// directly within the buildinfo header. See the following reference for more details.
4952
// https://github.com/golang/go/blob/1dbbafc70fd3e2c284469ab3e0936c1bb56129f6/src/debug/buildinfo/buildinfo.go#L184C16-L184C16
50-
TEST(ReadGoBuildVersionTest, BuildinfoEndianAgnostic) {
53+
TEST(ReadGoBuildInfoTest, BuildinfoEndianAgnostic) {
5154
const std::string kPath = px::testing::BazelRunfilePath(kTestGoBinaryPath);
5255
ASSERT_OK_AND_ASSIGN(std::unique_ptr<ElfReader> elf_reader, ElfReader::Create(kPath));
53-
ASSERT_OK_AND_ASSIGN(std::string version, ReadGoBuildVersion(elf_reader.get()));
54-
EXPECT_THAT(version, StrEq("go1.19.13"));
56+
ASSERT_OK_AND_ASSIGN(auto pair, ReadGoBuildInfo(elf_reader.get()));
57+
auto version = pair.first;
58+
EXPECT_THAT(version, StrEq("1.19.13"));
5559
}
5660

57-
TEST(ReadGoBuildVersionTest, BuildinfoLittleEndian) {
61+
TEST(ReadGoBuildInfoTest, BuildinfoLittleEndian) {
5862
const std::string kPath = px::testing::BazelRunfilePath(kTestGoLittleEndianBinaryPath);
5963
ASSERT_OK_AND_ASSIGN(std::unique_ptr<ElfReader> elf_reader, ElfReader::Create(kPath));
60-
ASSERT_OK_AND_ASSIGN(std::string version, ReadGoBuildVersion(elf_reader.get()));
61-
EXPECT_THAT(version, StrEq("go1.17.13"));
64+
ASSERT_OK_AND_ASSIGN(auto pair, ReadGoBuildInfo(elf_reader.get()));
65+
auto version = pair.first;
66+
EXPECT_THAT(version, StrEq("1.17.13"));
67+
}
68+
69+
// These tests are modeled off of upstream's
70+
// https://github.com/golang/go/blob/93fb2c90740aef00553c9ce6a7cd4578c2469675/src/runtime/debug/mod_test.go#L23
71+
TEST(ReadGoBuildInfoTest, BuildinfoPackageBuiltOutsideModule) {
72+
const std::string kBinContent =
73+
"path\trsc.io/fortune\n"
74+
"mod\trsc.io/fortune\tv1.0.0";
75+
auto build_info_s = ReadModInfo(kBinContent);
76+
EXPECT_OK(build_info_s);
77+
78+
auto build_info = build_info_s.ConsumeValueOrDie();
79+
EXPECT_EQ(build_info.path, "rsc.io/fortune");
80+
EXPECT_EQ(build_info.main.path, "rsc.io/fortune");
81+
EXPECT_EQ(build_info.main.version, "v1.0.0");
82+
}
83+
84+
TEST(ReadGoBuildInfoTest, BuildinfoPackageBuiltStdlib) {
85+
const std::string kBinContent = "path\tcmd/test2json";
86+
auto build_info_s = ReadModInfo(kBinContent);
87+
EXPECT_OK(build_info_s);
88+
auto build_info = build_info_s.ConsumeValueOrDie();
89+
EXPECT_EQ(build_info.path, "cmd/test2json");
90+
}
91+
92+
TEST(ReadGoBuildInfoTest, BuildinfoPackageBuiltInsideModule) {
93+
const std::string kBinContent =
94+
"go\t1.18\n"
95+
"path\texample.com/m\n"
96+
"mod\texample.com/m\t(devel)\n"
97+
"build\t-compiler=gc";
98+
auto build_info_s = ReadModInfo(kBinContent);
99+
EXPECT_OK(build_info_s);
100+
101+
auto build_info = build_info_s.ConsumeValueOrDie();
102+
EXPECT_EQ(build_info.path, "example.com/m");
103+
EXPECT_EQ(build_info.main.path, "example.com/m");
104+
EXPECT_EQ(build_info.main.version, "(devel)");
105+
EXPECT_EQ(build_info.main.replace, nullptr);
106+
EXPECT_EQ(build_info.deps.size(), 0);
107+
}
108+
109+
TEST(ReadGoBuildInfoTest, BuildinfoWithModules) {
110+
const std::string kPath = px::testing::BazelRunfilePath(kTestGoWithModulesBinaryPath);
111+
ASSERT_OK_AND_ASSIGN(std::unique_ptr<ElfReader> elf_reader, ElfReader::Create(kPath));
112+
ASSERT_OK_AND_ASSIGN(auto pair, ReadGoBuildInfo(elf_reader.get()));
113+
auto version = pair.first;
114+
EXPECT_THAT(version, StrEq("1.23.0"));
115+
116+
auto& build_info = pair.second;
117+
// Validate main module path.
118+
EXPECT_THAT(build_info.path,
119+
StrEq("go.opentelemetry.io/auto/internal/tools/inspect/cmd/offsetgen"));
120+
121+
// Validate main module metadata.
122+
EXPECT_THAT(build_info.main.path, StrEq("go.opentelemetry.io/auto/internal/tools"));
123+
EXPECT_THAT(build_info.main.version, StrEq("(devel)"));
124+
EXPECT_EQ(build_info.main.replace, nullptr);
125+
126+
// Validate module dependencies.
127+
EXPECT_THAT(build_info.deps,
128+
UnorderedElementsAre(
129+
Field(&Module::path, StrEq("github.com/Masterminds/semver/v3")),
130+
Field(&Module::path, StrEq("github.com/cilium/ebpf")),
131+
Field(&Module::path, StrEq("github.com/distribution/reference")),
132+
Field(&Module::path, StrEq("github.com/docker/docker")),
133+
Field(&Module::path, StrEq("github.com/docker/go-connections")),
134+
Field(&Module::path, StrEq("github.com/docker/go-units")),
135+
Field(&Module::path, StrEq("github.com/felixge/httpsnoop")),
136+
Field(&Module::path, StrEq("github.com/go-logr/logr")),
137+
Field(&Module::path, StrEq("github.com/go-logr/stdr")),
138+
Field(&Module::path, StrEq("github.com/gogo/protobuf")),
139+
Field(&Module::path, StrEq("github.com/moby/docker-image-spec")),
140+
Field(&Module::path, StrEq("github.com/opencontainers/go-digest")),
141+
Field(&Module::path, StrEq("github.com/opencontainers/image-spec")),
142+
Field(&Module::path, StrEq("github.com/pkg/errors")),
143+
Field(&Module::path, StrEq("go.opentelemetry.io/auto")),
144+
Field(&Module::path, StrEq("go.opentelemetry.io/auto/sdk")),
145+
Field(&Module::path, StrEq("go.opentelemetry.io/collector/pdata")),
146+
Field(&Module::path,
147+
StrEq("go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp")),
148+
Field(&Module::path, StrEq("go.opentelemetry.io/otel")),
149+
Field(&Module::path, StrEq("go.opentelemetry.io/otel/metric")),
150+
Field(&Module::path, StrEq("go.opentelemetry.io/otel/trace")),
151+
Field(&Module::path, StrEq("go.uber.org/multierr")),
152+
Field(&Module::path, StrEq("golang.org/x/arch")),
153+
Field(&Module::path, StrEq("golang.org/x/net")),
154+
Field(&Module::path, StrEq("golang.org/x/sync")),
155+
Field(&Module::path, StrEq("golang.org/x/sys")),
156+
Field(&Module::path, StrEq("golang.org/x/text")),
157+
Field(&Module::path, StrEq("google.golang.org/genproto/googleapis/rpc")),
158+
Field(&Module::path, StrEq("google.golang.org/grpc")),
159+
Field(&Module::path, StrEq("google.golang.org/protobuf"))));
160+
161+
// Validate replaced modules.
162+
EXPECT_THAT(build_info.deps,
163+
Contains(AllOf(Field(&Module::path, StrEq("go.opentelemetry.io/auto")),
164+
Field(&Module::replace,
165+
Pointee(AllOf(Field(&Module::path, StrEq("../../")),
166+
Field(&Module::version, StrEq("(devel)"))))))));
167+
168+
EXPECT_THAT(build_info.deps,
169+
Contains(AllOf(Field(&Module::path, StrEq("go.opentelemetry.io/auto/sdk")),
170+
Field(&Module::replace,
171+
Pointee(AllOf(Field(&Module::path, StrEq("../../sdk")),
172+
Field(&Module::version, StrEq("(devel)"))))))));
62173
}
63174

64-
TEST(ReadGoBuildVersionTest, BuildinfoLittleEndiani386) {
175+
TEST(ReadGoBuildInfoTest, BuildinfoLittleEndiani386) {
65176
const std::string kPath = px::testing::BazelRunfilePath(kTestGoLittleEndiani386BinaryPath);
66177
ASSERT_OK_AND_ASSIGN(std::unique_ptr<ElfReader> elf_reader, ElfReader::Create(kPath));
67-
ASSERT_OK_AND_ASSIGN(std::string version, ReadGoBuildVersion(elf_reader.get()));
68-
EXPECT_THAT(version, StrEq("go1.13.15"));
178+
ASSERT_OK_AND_ASSIGN(auto pair, ReadGoBuildInfo(elf_reader.get()));
179+
auto version = pair.first;
180+
EXPECT_THAT(version, StrEq("1.13.15"));
69181
}
70182

71183
TEST(IsGoExecutableTest, WorkingOnBasicGoBinary) {

src/stirling/obj_tools/testdata/go/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ filegroup(
7575
# (https://github.com/golang/go/blob/1dbbafc70fd3e2c284469ab3e0936c1bb56129f6/src/debug/buildinfo/buildinfo.go#L189-L190)
7676
# and so it cannot be tested without compiling against an older Go version.
7777
"test_go_1_17_binary",
78+
"test_buildinfo_with_mods",
7879
":test_go_1_18_binary",
7980
":test_go_1_19_binary",
8081
":test_go_1_20_binary",
21.4 MB
Binary file not shown.

src/stirling/source_connectors/socket_tracer/uprobe_symaddrs.cc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -468,8 +468,8 @@ Status PopulateHTTP2DebugSymbols(DwarfReader* dwarf_reader, std::string_view ven
468468

469469
Status PopulateGoTLSDebugSymbols(ElfReader* elf_reader, DwarfReader* dwarf_reader,
470470
struct go_tls_symaddrs_t* symaddrs) {
471-
PX_ASSIGN_OR_RETURN(std::string build_version, ReadGoBuildVersion(elf_reader));
472-
PX_ASSIGN_OR_RETURN(SemVer go_version, GetSemVer(build_version, false));
471+
PX_ASSIGN_OR_RETURN(auto build_info, ReadGoBuildInfo(elf_reader));
472+
PX_ASSIGN_OR_RETURN(SemVer go_version, GetSemVer(build_info.first, false));
473473
std::string retval0_arg = "~r1";
474474
std::string retval1_arg = "~r2";
475475

0 commit comments

Comments
 (0)