|
1 | 1 | #include <cpptrace/cpptrace.hpp>
|
2 | 2 | #include <cpptrace/from_current.hpp>
|
3 | 3 |
|
| 4 | +#include <cstdint> |
4 | 5 | #include <exception>
|
5 | 6 | #include <system_error>
|
6 | 7 | #include <typeinfo>
|
7 | 8 |
|
8 | 9 | #include "platform/platform.hpp"
|
| 10 | +#include "utils/error.hpp" |
9 | 11 | #include "utils/microfmt.hpp"
|
10 | 12 | #include "utils/utils.hpp"
|
11 | 13 | #include "logging.hpp"
|
@@ -253,44 +255,135 @@ namespace detail {
|
253 | 255 | return perms;
|
254 | 256 | }
|
255 | 257 | #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() { |
258 | 330 | std::ifstream stream("/proc/self/maps");
|
259 | 331 | 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 | + } |
268 | 341 | }
|
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; |
271 | 353 | }
|
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; |
290 | 378 | }
|
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 | + ); |
292 | 385 | }
|
293 |
| - throw std::runtime_error("Failed to find mapping with page in /proc/self/maps"); |
| 386 | + return it->perms; |
294 | 387 | }
|
295 | 388 | #endif
|
296 | 389 | void mprotect_page(void* page, int page_size, int protections) {
|
|
0 commit comments