Skip to content

Commit 19eeb69

Browse files
committed
Improve get_page_protections implementation and handling of /proc/self/maps. Cache mappings and better handle possible read tears.
1 parent 91ce5ce commit 19eeb69

File tree

1 file changed

+125
-32
lines changed

1 file changed

+125
-32
lines changed

src/from_current.cpp

Lines changed: 125 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
#include <cpptrace/cpptrace.hpp>
22
#include <cpptrace/from_current.hpp>
33

4+
#include <cstdint>
45
#include <exception>
56
#include <system_error>
67
#include <typeinfo>
78

89
#include "platform/platform.hpp"
10+
#include "utils/error.hpp"
911
#include "utils/microfmt.hpp"
1012
#include "utils/utils.hpp"
1113
#include "logging.hpp"
@@ -253,44 +255,135 @@ namespace detail {
253255
return perms;
254256
}
255257
#else
256-
int get_page_protections(void* page) {
257-
auto page_addr = reinterpret_cast<uintptr_t>(page);
258+
// Code for reading /proc/self/maps
259+
// Unfortunately this is the canonical and only way to get memory permissions on linux
260+
// It comes with some surprising behaviors. Because it's a pseudo-file and maps could update at any time, reads of
261+
// the file can tear. The surprising observable behavior here is overlapping ranges:
262+
// - https://unix.stackexchange.com/questions/704987/overlapping-address-ranges-in-proc-maps
263+
// - https://stackoverflow.com/questions/59737950/what-is-the-correct-way-to-get-a-consistent-snapshot-of-proc-pid-smaps
264+
// Additional info:
265+
// Note: reading /proc/PID/maps or /proc/PID/smaps is inherently racy (consistent
266+
// output can be achieved only in the single read call).
267+
// This typically manifests when doing partial reads of these files while the
268+
// memory map is being modified. Despite the races, we do provide the following
269+
// guarantees:
270+
//
271+
// 1) The mapped addresses never go backwards, which implies no two
272+
// regions will ever overlap.
273+
// 2) If there is something at a given vaddr during the entirety of the
274+
// life of the smaps/maps walk, there will be some output for it.
275+
//
276+
// https://www.kernel.org/doc/Documentation/filesystems/proc.txt
277+
// Ideally we could do everything as a single read() call but I don't think that's practical, especially given that
278+
// the kernel has limited buffers internally. While we shouldn't be modifying mapped memory while reading
279+
// /proc/self/maps here, it's theoretically possible that we could allocate and that could go to the OS for more
280+
// pages.
281+
// While reading this is inherently racy, as far as I can tell tears don't happen within a line but they can happen
282+
// between lines.
283+
// The code that writes /proc/pid/maps:
284+
// - https://github.com/torvalds/linux/blob/3d0ebc36b0b3e8486ceb6e08e8ae173aaa6d1221/fs/proc/task_mmu.c#L304-L365
285+
286+
struct address_range {
287+
uintptr_t low;
288+
uintptr_t high;
289+
int perms;
290+
bool operator<(const address_range& other) const {
291+
return low < other.low;
292+
}
293+
};
294+
295+
// returns nullopt on eof
296+
optional<address_range> read_map_entry(std::ifstream& stream) {
297+
uintptr_t start;
298+
uintptr_t stop;
299+
stream>>start;
300+
stream.ignore(1); // dash
301+
stream>>stop;
302+
if(stream.eof()) {
303+
return nullopt;
304+
}
305+
if(stream.fail()) {
306+
throw std::runtime_error("Failure reading /proc/self/maps");
307+
}
308+
stream.ignore(1); // space
309+
char r, w, x; // there's a private/shared flag after these but we don't need it
310+
stream>>r>>w>>x;
311+
if(stream.fail() || stream.eof()) {
312+
throw std::runtime_error("Failure reading /proc/self/maps");
313+
}
314+
int perms = 0;
315+
if(r == 'r') {
316+
perms |= PROT_READ;
317+
}
318+
if(w == 'w') {
319+
perms |= PROT_WRITE;
320+
}
321+
if(x == 'x') {
322+
perms |= PROT_EXEC;
323+
}
324+
stream.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
325+
return address_range{start, stop, perms};
326+
}
327+
328+
// returns a vector or nullopt if a tear is detected
329+
optional<std::vector<address_range>> try_load_mapped_region_info() {
258330
std::ifstream stream("/proc/self/maps");
259331
stream>>std::hex;
260-
while(!stream.eof()) {
261-
uintptr_t start;
262-
uintptr_t stop;
263-
stream>>start;
264-
stream.ignore(1); // dash
265-
stream>>stop;
266-
if(stream.eof()) {
267-
break;
332+
std::vector<address_range> ranges;
333+
while(auto entry = read_map_entry(stream)) {
334+
const auto& range = entry.unwrap();
335+
VERIFY(range.low <= range.high);
336+
if(!ranges.empty()) {
337+
const auto& last_range = ranges.back();
338+
if(range.low < last_range.high) {
339+
return nullopt;
340+
}
268341
}
269-
if(stream.fail()) {
270-
throw std::runtime_error("Failure reading /proc/self/maps");
342+
ranges.push_back(range);
343+
}
344+
return ranges;
345+
}
346+
347+
// we can allocate during try_load_mapped_region_info, in theory that could cause a tear
348+
optional<std::vector<address_range>> try_load_mapped_region_info_with_retries(int n) {
349+
VERIFY(n > 0);
350+
for(int i = 0; i < n; i++) {
351+
if(auto info = try_load_mapped_region_info()) {
352+
return info;
271353
}
272-
if(page_addr >= start && page_addr < stop) {
273-
stream.ignore(1); // space
274-
char r, w, x; // there's a private/shared flag after these but we don't need it
275-
stream>>r>>w>>x;
276-
if(stream.fail() || stream.eof()) {
277-
throw std::runtime_error("Failure reading /proc/self/maps");
278-
}
279-
int perms = 0;
280-
if(r == 'r') {
281-
perms |= PROT_READ;
282-
}
283-
if(w == 'w') {
284-
perms |= PROT_WRITE;
285-
}
286-
if(x == 'x') {
287-
perms |= PROT_EXEC;
288-
}
289-
return perms;
354+
}
355+
throw internal_error("Couldn't successfully load /proc/self/maps after {} retries", n);
356+
}
357+
358+
const std::vector<address_range>& load_mapped_region_info() {
359+
static std::vector<address_range> regions;
360+
static bool has_loaded = false;
361+
if(!has_loaded) {
362+
has_loaded = true;
363+
if(auto info = try_load_mapped_region_info_with_retries(2)) {
364+
regions = std::move(info).unwrap();
365+
}
366+
}
367+
return regions;
368+
}
369+
370+
int get_page_protections(void* page) {
371+
const auto& mapped_region_info = load_mapped_region_info();
372+
auto it = first_less_than_or_equal(
373+
mapped_region_info.begin(),
374+
mapped_region_info.end(),
375+
reinterpret_cast<uintptr_t>(page),
376+
[](uintptr_t a, const address_range& b) {
377+
return a < b.low;
290378
}
291-
stream.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
379+
);
380+
if(it == mapped_region_info.end()) {
381+
throw internal_error(
382+
"Failed to find mapping for {>16:0h} in /proc/self/maps",
383+
reinterpret_cast<uintptr_t>(page)
384+
);
292385
}
293-
throw std::runtime_error("Failed to find mapping with page in /proc/self/maps");
386+
return it->perms;
294387
}
295388
#endif
296389
void mprotect_page(void* page, int page_size, int protections) {

0 commit comments

Comments
 (0)