Skip to content

Commit 5e4a581

Browse files
authored
Fix virtual to binary addr conversion for processes that have an uncommon virtual memory mapping (#1637)
Summary: Fix virtual to binary addr conversion for processes that have an uncommon virtual memory mapping Our previous virtual to binary address conversion logic assumed that the first offset within `/proc/$PID/maps` was the correct offset to apply for PIE binaries. There are certain cases, such as when an unlimited stack ulimit is applied, where this assumption doesn't hold true (see the linked issue before for more details). This change adjusts our conversion logic to take into account the correct `/proc/$PID/maps` entry so address conversion works in all known cases. Relevant Issues: #1630 Type of change: /kind bug Test Plan: Verified the following: - [x] New test verifies the status quo case as well as the situation reported in #1630 - [x] Verified `perf_profiler_bpf_test` passes when the perf profiler uses the ELF symbolizer --------- Signed-off-by: Dom Del Nano <ddelnano@pixielabs.ai>
1 parent 26cf6ea commit 5e4a581

File tree

7 files changed

+260
-9
lines changed

7 files changed

+260
-9
lines changed

src/common/testing/test_utils/container_runner.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ class ContainerRunner {
6666
*/
6767
void Wait(bool close_pipe = true);
6868

69+
/**
70+
* Returns the exit status of the container.
71+
*/
72+
int GetStatus() { return podman_.GetStatus(); }
73+
6974
/**
7075
* Returns the stdout of the container. Needs to be combined with the full output from Run
7176
* to ensure the entire result is present.

src/stirling/obj_tools/BUILD.bazel

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,19 @@ pl_cc_test(
6969
],
7070
)
7171

72+
pl_cc_test(
73+
name = "address_converter_test",
74+
srcs = ["address_converter_test.cc"],
75+
tags = [
76+
"requires_bpf",
77+
],
78+
deps = [
79+
":cc_library",
80+
"//src/stirling/obj_tools/testdata/containers:vaddr_convert_self_func_container",
81+
"//src/stirling/testing:cc_library",
82+
],
83+
)
84+
7285
pl_cc_test(
7386
name = "dwarf_reader_test",
7487
srcs = ["dwarf_reader_test.cc"],

src/stirling/obj_tools/address_converter.cc

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,20 @@
1717
*/
1818

1919
#include <memory>
20+
#include <string>
2021
#include <vector>
2122

23+
#include "src/common/fs/fs_wrapper.h"
2224
#include "src/common/system/proc_parser.h"
25+
#include "src/common/system/proc_pid_path.h"
2326
#include "src/stirling/obj_tools/address_converter.h"
2427

2528
namespace px {
2629
namespace stirling {
2730
namespace obj_tools {
2831

32+
using ProcSMaps = system::ProcParser::ProcessSMaps;
33+
2934
uint64_t ElfAddressConverter::VirtualAddrToBinaryAddr(uint64_t virtual_addr) const {
3035
return virtual_addr + virtual_to_binary_addr_offset_;
3136
}
@@ -34,22 +39,31 @@ uint64_t ElfAddressConverter::BinaryAddrToVirtualAddr(uint64_t binary_addr) cons
3439
return binary_addr - virtual_to_binary_addr_offset_;
3540
}
3641

42+
StatusOr<uint32_t> GetProcMapsIndexForBinary(const std::string& binary_path,
43+
const std::vector<ProcSMaps>& map_entries) {
44+
for (const auto& [idx, entry] : Enumerate(map_entries)) {
45+
if (absl::EndsWith(binary_path, entry.pathname)) {
46+
return idx;
47+
}
48+
}
49+
return error::NotFound("");
50+
}
51+
3752
/**
3853
* The calculated offset is used to convert between virtual addresses (eg. the address you
3954
* would get from a function pointer) and "binary" addresses (i.e. the address that `nm` would
4055
* display for a given function).
4156
*
4257
* This conversion is non-trivial and requires information from both the ELF file of the binary in
43-
* question, as well as the /proc/PID/maps file for the PID of the process in question.
58+
* question, as well as the /proc/$PID/maps file for the PID of the process in question.
4459
*
4560
* For non-PIE executables, this conversion is trivial as the virtual addresses in the ELF file are
4661
* used directly when loading.
4762
*
48-
* However, for PIE, the loaded virtual address can be whatever. So to calculate the offset we look
49-
* at the first loadable segment in the ELF file and compare it to the first entry in the
50-
* /proc/PID/maps file to see how the loader changed the virtual address. This works because the
51-
* loader guarantees that the relative offsets of the different segments remain the same, regardless
52-
* of where in virtual address space it ends up putting the segment.
63+
* However, for PIE, the loaded virtual address can be whatever. To calculate the offset we must
64+
* find the /proc/$PID/maps entry that corresponds to the given process's executable (entry that
65+
* matches /proc/$PID/exe) and use that entry's virtual memory offset to find the binary
66+
* address.
5367
*
5468
**/
5569
StatusOr<std::unique_ptr<ElfAddressConverter>> ElfAddressConverter::Create(ElfReader* elf_reader,
@@ -64,16 +78,29 @@ StatusOr<std::unique_ptr<ElfAddressConverter>> ElfAddressConverter::Create(ElfRe
6478
}
6579
system::ProcParser parser;
6680
std::vector<system::ProcParser::ProcessSMaps> map_entries;
67-
// This is a little inefficient as we only need the first entry.
6881
PX_RETURN_IF_ERROR(parser.ParseProcPIDMaps(pid, &map_entries));
6982
if (map_entries.size() < 1) {
7083
return Status(
7184
statuspb::INTERNAL,
7285
absl::Substitute("ElfAddressConverter::Create: Failed to parse /proc/$0/maps", pid));
7386
}
74-
const auto mapped_virt_addr = map_entries[0].vmem_start;
87+
88+
PX_ASSIGN_OR_RETURN(const auto proc_exe, parser.GetExePath(pid));
89+
// Prior to pixie#1630, we believed that a process's VMA space would always have its executable
90+
// at the first (lowest) virtual memory address. This isn't always the case, however, if we fail
91+
// to match against a /proc/$PID/maps entry, default to the first one.
92+
auto map_entry = map_entries[0];
93+
auto idx_status = GetProcMapsIndexForBinary(proc_exe, map_entries);
94+
if (idx_status.ok()) {
95+
map_entry = map_entries[idx_status.ConsumeValueOrDie()];
96+
} else {
97+
LOG(WARNING) << absl::Substitute(
98+
"Failed to find match for $0 in /proc/$1/maps. Defaulting to the first entry",
99+
proc_exe.string(), pid);
100+
}
101+
const auto mapped_virt_addr = map_entry.vmem_start;
75102
uint64_t mapped_offset;
76-
if (!absl::SimpleHexAtoi(map_entries[0].offset, &mapped_offset)) {
103+
if (!absl::SimpleHexAtoi(map_entry.offset, &mapped_offset)) {
77104
return Status(statuspb::INTERNAL,
78105
absl::Substitute(
79106
"ElfAddressConverter::Create: Failed to parse offset in /proc/$0/maps", pid));
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright 2018- The Pixie Authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
*/
18+
19+
#include "src/stirling/obj_tools/address_converter.h"
20+
21+
#include "src/stirling/obj_tools/testdata/containers/vaddr_convert_self_func_container.h"
22+
#include "src/stirling/testing/common.h"
23+
24+
namespace px {
25+
namespace stirling {
26+
namespace obj_tools {
27+
28+
TEST(ElfAddressConverterTest, VirtualAddrToBinaryAddr) {
29+
VaddrConvertSelfFuncContainer container;
30+
ASSERT_OK(container.Run());
31+
32+
int status = -1;
33+
testing::Timeout t(std::chrono::minutes{1});
34+
while (status == -1 && !t.TimedOut()) {
35+
status = container.GetStatus();
36+
std::this_thread::sleep_for(std::chrono::milliseconds{200});
37+
}
38+
EXPECT_EQ(0, status);
39+
}
40+
41+
TEST(ElfAddressConverterTest, VirtualAddrToBinaryAddrForReorderedVirtualMemoryMappings) {
42+
// Setting an unlimited stack size ulimit causes the VMAs of a process to be reordered and
43+
// caused a previous crash (as described in https://github.com/pixie-io/pixie/issues/1630).
44+
VaddrConvertSelfFuncContainer container;
45+
ASSERT_OK(container.Run(std::chrono::seconds{5}, {"--ulimit=stack=-1"}));
46+
47+
int status = -1;
48+
testing::Timeout t(std::chrono::minutes{1});
49+
while (status == -1 && !t.TimedOut()) {
50+
status = container.GetStatus();
51+
std::this_thread::sleep_for(std::chrono::milliseconds{200});
52+
}
53+
EXPECT_EQ(0, status);
54+
}
55+
56+
} // namespace obj_tools
57+
} // namespace stirling
58+
} // namespace px
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
#
2+
# http://www.apache.org/licenses/LICENSE-2.0
3+
#
4+
# Unless required by applicable law or agreed to in writing, software
5+
# distributed under the License is distributed on an "AS IS" BASIS,
6+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
7+
# See the License for the specific language governing permissions and
8+
# limitations under the License.
9+
#
10+
# SPDX-License-Identifier: Apache-2.0
11+
12+
load("@io_bazel_rules_docker//cc:image.bzl", "cc_image")
13+
load("//bazel:pl_build_system.bzl", "pl_cc_binary", "pl_cc_test_library")
14+
15+
package(default_visibility = ["//src/stirling/obj_tools:__subpackages__"])
16+
17+
pl_cc_binary(
18+
name = "vaddr_convert_self_func",
19+
srcs = ["vaddr_convert_self_func.cc"],
20+
deps = [
21+
"//src/stirling/obj_tools:cc_library",
22+
],
23+
)
24+
25+
cc_image(
26+
name = "vaddr_convert_self_func_image",
27+
base = "//:pl_cc_base_debug_image",
28+
binary = ":vaddr_convert_self_func",
29+
)
30+
31+
pl_cc_test_library(
32+
name = "vaddr_convert_self_func_container",
33+
srcs = glob(
34+
["*.cc"],
35+
exclude = [
36+
"vaddr_convert_self_func.cc",
37+
],
38+
),
39+
hdrs = glob(
40+
[
41+
"*.h",
42+
],
43+
),
44+
data = [
45+
":vaddr_convert_self_func_image.tar",
46+
],
47+
deps = ["//src/common/testing/test_utils:cc_library"],
48+
)
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* Copyright 2018- The Pixie Authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
*/
18+
19+
#include "src/stirling/obj_tools/address_converter.h"
20+
#include "src/stirling/utils/proc_path_tools.h"
21+
22+
// Using extern C to avoid name mangling since ElfReader must be able to address this
23+
// by its symbol name.
24+
extern "C" {
25+
void TestFunc() {}
26+
27+
} // extern "C"
28+
29+
using px::stirling::GetSelfPath;
30+
using px::stirling::obj_tools::ElfAddressConverter;
31+
using px::stirling::obj_tools::ElfReader;
32+
using px::stirling::obj_tools::SymbolMatchType;
33+
34+
// This utility performs an assertion that the ElfAddressConverter::VirtualAddrToBinaryAddr function
35+
// returns the correct (consistent with ElfReader and `nm` cli output) binary address for a given
36+
// function. This is used to test our address conversion logic when a PIE binary is memory mapped
37+
// differently from the common scenario (where the process's ELF segments are mapped at the
38+
// lowest VMA), such as when an unlimited stack size ulimit is set on a process.
39+
int main() {
40+
LOG(INFO) << "Running";
41+
42+
// This sleep is required otherwise when it is run inside a container (via ContainerRunner) we
43+
// will fail to detect the child process's pid during the test case that uses this.
44+
sleep(5);
45+
46+
std::filesystem::path self_path = GetSelfPath().ValueOrDie();
47+
PX_ASSIGN_OR(auto elf_reader, ElfReader::Create(self_path.string()), return -1);
48+
PX_ASSIGN_OR(std::vector<ElfReader::SymbolInfo> syms,
49+
elf_reader->ListFuncSymbols("TestFunc", SymbolMatchType::kSubstr));
50+
51+
PX_ASSIGN_OR(auto converter, ElfAddressConverter::Create(elf_reader.get(), getpid()), return -1);
52+
auto symbol_addr = converter->VirtualAddrToBinaryAddr(reinterpret_cast<uint64_t>(&TestFunc));
53+
54+
auto expected_addr = syms[0].address;
55+
if (symbol_addr != expected_addr) {
56+
LOG(ERROR) << absl::Substitute(
57+
"Expected ElfAddressConverter address=$0 to match binary address=$1", symbol_addr,
58+
expected_addr);
59+
return -1;
60+
}
61+
return 0;
62+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright 2018- The Pixie Authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
*/
18+
19+
#pragma once
20+
21+
#include "src/common/base/base.h"
22+
#include "src/common/testing/test_environment.h"
23+
#include "src/common/testing/test_utils/container_runner.h"
24+
25+
using px::ContainerRunner;
26+
27+
class VaddrConvertSelfFuncContainer : public ContainerRunner {
28+
public:
29+
VaddrConvertSelfFuncContainer()
30+
: ContainerRunner(px::testing::BazelRunfilePath(kBazelImageTar), kInstanceNamePrefix,
31+
kReadyMessage) {}
32+
33+
private:
34+
static constexpr std::string_view kBazelImageTar =
35+
"src/stirling/obj_tools/testdata/containers/vaddr_convert_self_func_image.tar";
36+
static constexpr std::string_view kInstanceNamePrefix = "vaddr_convert_self_func_container";
37+
static constexpr std::string_view kReadyMessage = "Running";
38+
};

0 commit comments

Comments
 (0)