Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix ExtractGolangInterface to handle go 1.20 binaries and later #2093

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions src/stirling/obj_tools/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ pl_cc_test(
"//src/stirling/obj_tools/testdata/go:test_binaries",
"//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",
],
deps = [
":cc_library",
Expand Down
26 changes: 19 additions & 7 deletions src/stirling/obj_tools/go_syms.cc
Original file line number Diff line number Diff line change
Expand Up @@ -144,16 +144,28 @@ StatusOr<std::string> ReadGoBuildVersion(ElfReader* elf_reader) {
return ReadGoString(elf_reader, ptr_size, ptr_addr, read_ptr);
}

// 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 = {
"go:itab.", // Prefix used by Go 1.20 binaries and later.
"go.itab.", // Prefix used by Go 1.19 binaries and earlier.
};

StatusOr<absl::flat_hash_map<std::string, std::vector<IntfImplTypeInfo>>> ExtractGolangInterfaces(
ElfReader* elf_reader) {
absl::flat_hash_map<std::string, std::vector<IntfImplTypeInfo>> interface_types;

// All itable objects in the symbols are prefixed with this string.
const std::string_view kITablePrefix("go.itab.");

PX_ASSIGN_OR_RETURN(std::vector<ElfReader::SymbolInfo> itable_symbols,
elf_reader->SearchSymbols(kITablePrefix, SymbolMatchType::kPrefix,
/*symbol_type*/ ELFIO::STT_OBJECT));
std::vector<ElfReader::SymbolInfo> itable_symbols;
std::string_view iTablePrefix;
for (const auto& prefix : kITablePrefixes) {
PX_ASSIGN_OR_RETURN(itable_symbols,
elf_reader->SearchSymbols(prefix, SymbolMatchType::kPrefix,
/*symbol_type*/ ELFIO::STT_OBJECT));
if (!itable_symbols.empty()) {
iTablePrefix = prefix;
break;
}
}

for (const auto& sym : itable_symbols) {
// Expected format is:
Expand All @@ -166,7 +178,7 @@ StatusOr<absl::flat_hash_map<std::string, std::vector<IntfImplTypeInfo>>> Extrac

std::string_view interface_name = sym_split[1];
std::string_view type = sym_split[0];
type.remove_prefix(kITablePrefix.size());
type.remove_prefix(iTablePrefix.size());

IntfImplTypeInfo info;

Expand Down
109 changes: 83 additions & 26 deletions src/stirling/obj_tools/go_syms_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
#include "src/stirling/obj_tools/go_syms.h"

#include <memory>
#include <tuple>
#include <utility>

#include "src/common/testing/testing.h"

Expand All @@ -27,7 +29,9 @@ namespace stirling {
namespace obj_tools {

using ::testing::Field;
using ::testing::Matcher;
using ::testing::StrEq;
using ::testing::UnorderedElementsAre;

constexpr std::string_view kTestGoLittleEndiani386BinaryPath =
"src/stirling/obj_tools/testdata/go/test_go1_13_i386_binary";
Expand All @@ -37,6 +41,8 @@ constexpr std::string_view kTestGoLittleEndianBinaryPath =

constexpr std::string_view kTestGoBinaryPath =
"src/stirling/obj_tools/testdata/go/test_go_1_19_binary";
constexpr std::string_view kTestGo1_21BinaryPath =
"src/stirling/obj_tools/testdata/go/test_go_1_21_binary";

// 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.
Expand Down Expand Up @@ -68,44 +74,95 @@ TEST(IsGoExecutableTest, WorkingOnBasicGoBinary) {
EXPECT_TRUE(IsGoExecutable(elf_reader.get()));
}

TEST(ElfGolangItableTest, ExtractInterfaceTypes) {
const std::string kPath = px::testing::BazelRunfilePath(kTestGoBinaryPath);
class ElfGolangItableTest
: public ::testing::TestWithParam<std::tuple<
std::string,
Matcher<const std::vector<std::pair<std::string, std::vector<IntfImplTypeInfo>>>>>> {};

INSTANTIATE_TEST_SUITE_P(
ElfGolangItableTestSuite, ElfGolangItableTest,
::testing::Values(
std::make_tuple(
kTestGo1_21BinaryPath,
UnorderedElementsAre(
Pair("fmt.State",
UnorderedElementsAre(Field(&IntfImplTypeInfo::type_name, "*fmt.pp"))),
Pair("internal/bisect.Writer",
UnorderedElementsAre(Field(&IntfImplTypeInfo::type_name,
"*internal/godebug.runtimeStderr"))),
Pair("internal/reflectlite.Type",
UnorderedElementsAre(Field(&IntfImplTypeInfo::type_name,
"internal/reflectlite.rtype"))),
Pair("error",
UnorderedElementsAre(
Field(&IntfImplTypeInfo::type_name, "main.IntStruct"),
Field(&IntfImplTypeInfo::type_name, "*errors.errorString"),
Field(&IntfImplTypeInfo::type_name, "syscall.Errno"),
Field(&IntfImplTypeInfo::type_name, "*io/fs.PathError"),
Field(&IntfImplTypeInfo::type_name, "runtime.errorString"),
Field(&IntfImplTypeInfo::type_name, "internal/poll.errNetClosing"),
Field(&IntfImplTypeInfo::type_name,
"*internal/poll.DeadlineExceededError"),
Field(&IntfImplTypeInfo::type_name, "*internal/bisect.parseError"))),
Pair("io.Writer",
UnorderedElementsAre(Field(&IntfImplTypeInfo::type_name, "*os.File"))),
Pair("sort.Interface", UnorderedElementsAre(Field(&IntfImplTypeInfo::type_name,
"*internal/fmtsort.SortedMap"))),
Pair("reflect.Type",
UnorderedElementsAre(Field(&IntfImplTypeInfo::type_name, "*reflect.rtype"))),
Pair("math/rand.Source64", UnorderedElementsAre(Field(&IntfImplTypeInfo::type_name,
"*math/rand.fastSource"))),
Pair("math/rand.Source", UnorderedElementsAre(Field(&IntfImplTypeInfo::type_name,
"*math/rand.lockedSource"),
Field(&IntfImplTypeInfo::type_name,
"*math/rand.fastSource"))))),
std::make_tuple(
kTestGoBinaryPath,
UnorderedElementsAre(
Pair("error",
UnorderedElementsAre(
Field(&IntfImplTypeInfo::type_name, "main.IntStruct"),
Field(&IntfImplTypeInfo::type_name, "*errors.errorString"),
Field(&IntfImplTypeInfo::type_name, "*io/fs.PathError"),
Field(&IntfImplTypeInfo::type_name,
"*internal/poll.DeadlineExceededError"),
Field(&IntfImplTypeInfo::type_name, "internal/poll.errNetClosing"),
Field(&IntfImplTypeInfo::type_name, "runtime.errorString"),
Field(&IntfImplTypeInfo::type_name, "syscall.Errno"))),
Pair("sort.Interface", UnorderedElementsAre(Field(&IntfImplTypeInfo::type_name,
"*internal/fmtsort.SortedMap"))),
Pair("math/rand.Source", UnorderedElementsAre(Field(&IntfImplTypeInfo::type_name,
"*math/rand.lockedSource"))),
Pair("io.Writer",
UnorderedElementsAre(Field(&IntfImplTypeInfo::type_name, "*os.File"))),
Pair("internal/reflectlite.Type",
UnorderedElementsAre(Field(&IntfImplTypeInfo::type_name,
"*internal/reflectlite.rtype"))),
Pair("reflect.Type",
UnorderedElementsAre(Field(&IntfImplTypeInfo::type_name, "*reflect.rtype"))),
Pair("fmt.State",
UnorderedElementsAre(Field(&IntfImplTypeInfo::type_name, "*fmt.pp")))))));

TEST_P(ElfGolangItableTest, ExtractInterfaceTypes) {
const std::string kPath = px::testing::BazelRunfilePath(std::get<0>(GetParam()));

ASSERT_OK_AND_ASSIGN(std::unique_ptr<ElfReader> elf_reader, ElfReader::Create(kPath));
ASSERT_OK_AND_ASSIGN(const auto interfaces, ExtractGolangInterfaces(elf_reader.get()));

ASSERT_OK_AND_ASSIGN(const auto interfaces_map, ExtractGolangInterfaces(elf_reader.get()));
std::vector<std::pair<std::string, std::vector<IntfImplTypeInfo>>> interfaces(
interfaces_map.begin(), interfaces_map.end());

// Check for `bazel coverage` so we can bypass the final checks.
// Note that we still get accurate coverage metrics, because this only skips the final check.
// Ideally, we'd get bazel to deterministically build test_go_binary,
// but it's not easy to tell bazel to use a different config for just one target.

#ifdef PL_COVERAGE
LOG(INFO) << "Whoa...`bazel coverage` is messaging with test_go_binary. Shame on you bazel. "
"Ending this test early.";
return;
#else
EXPECT_THAT(
interfaces,
UnorderedElementsAre(
Pair("error",
UnorderedElementsAre(
Field(&IntfImplTypeInfo::type_name, "main.IntStruct"),
Field(&IntfImplTypeInfo::type_name, "*errors.errorString"),
Field(&IntfImplTypeInfo::type_name, "*io/fs.PathError"),
Field(&IntfImplTypeInfo::type_name, "*internal/poll.DeadlineExceededError"),
Field(&IntfImplTypeInfo::type_name, "internal/poll.errNetClosing"),
Field(&IntfImplTypeInfo::type_name, "runtime.errorString"),
Field(&IntfImplTypeInfo::type_name, "syscall.Errno"))),
Pair("sort.Interface", UnorderedElementsAre(Field(&IntfImplTypeInfo::type_name,
"*internal/fmtsort.SortedMap"))),
Pair("math/rand.Source", UnorderedElementsAre(Field(&IntfImplTypeInfo::type_name,
"*math/rand.lockedSource"))),
Pair("io.Writer", UnorderedElementsAre(Field(&IntfImplTypeInfo::type_name, "*os.File"))),
Pair("internal/reflectlite.Type",
UnorderedElementsAre(
Field(&IntfImplTypeInfo::type_name, "*internal/reflectlite.rtype"))),
Pair("reflect.Type",
UnorderedElementsAre(Field(&IntfImplTypeInfo::type_name, "*reflect.rtype"))),
Pair("fmt.State", UnorderedElementsAre(Field(&IntfImplTypeInfo::type_name, "*fmt.pp")))));
EXPECT_THAT(interfaces, std::get<1>(GetParam()));
#endif
}

Expand Down
Loading