diff --git a/runtime/libia2/memory_maps.c b/runtime/libia2/memory_maps.c index 5db29c522c..4e0c81e75e 100644 --- a/runtime/libia2/memory_maps.c +++ b/runtime/libia2/memory_maps.c @@ -166,17 +166,16 @@ static void label_memory_map(FILE *log, uintptr_t start_addr) {} // so we need to free what `getline` allocated with `__real_free`. typeof(IA2_IGNORE(free)) __real_free; -void ia2_log_memory_maps(FILE *log) { - FILE *maps = fopen("/proc/self/maps", "r"); - assert(maps); - - // Skip dev and inode. - fprintf(log, " start addr-end addr perms offset path\n"); +void ia2_memory_map_foreach(FILE *maps_file, + void (*each_cb)(const char *line, ssize_t line_len, int path_index, uintptr_t start_addr, uintptr_t end_addr, char perms[static 4], uintptr_t offset, void *context), + void (*error_cb)(const char *line, void *context), + void *context) { + assert(maps_file); char *line = NULL; size_t line_cap = 0; while (true) { - const ssize_t line_len = getline(&line, &line_cap, maps); + const ssize_t line_len = getline(&line, &line_cap, maps_file); if (line_len == -1) { break; } @@ -198,27 +197,46 @@ void ia2_log_memory_maps(FILE *log) { const int vars_matched = sscanf(line, "%lx-%lx %4c %lx %x:%x %lu %n", &start_addr, &end_addr, perms, &offset, &dev_major, &dev_minor, &inode, &path_index); const int expected_vars_matched = 7; // Note that "%n" doesn't count as a matched var. if (vars_matched != expected_vars_matched) { - fprintf(log, "%s\n", line); - fprintf(stderr, "error parsing /proc/self/maps line (matched %d vars instead of %d): %s\n", + error_cb(line, context); + fprintf(stderr, "error parsing /proc//maps line (matched %d vars instead of %d): %s\n", vars_matched, expected_vars_matched, line); continue; } - const char *path = line + path_index; + each_cb(line, line_len, path_index, start_addr, end_addr, perms, offset, context); + } - // Skip dev and inode. - fprintf(log, "%08lx-%08lx %.4s %08lx ", start_addr, end_addr, perms, offset); + __real_free(line); +} - const size_t path_len = (size_t)line_len - path_index - 1; - if (path_len != 0) { - fprintf(log, "%s", path); - } else { - // No path, try to identify it. - label_memory_map(log, start_addr); - } +static void log_memory_map_error(const char *line, void *context) { + FILE *log = (FILE *)context; + fprintf(log, "%s\n", line); +} - fprintf(log, "\n"); +static void log_memory_map_entry(const char *line, ssize_t line_len, int path_index, uintptr_t start_addr, uintptr_t end_addr, char perms[static 4], uintptr_t offset, void *context) { + FILE *log = (FILE *)context; + // Skip dev and inode. + fprintf(log, "%08lx-%08lx %.4s %08lx ", start_addr, end_addr, perms, offset); + + const char *path = line + path_index; + + const size_t path_len = (size_t)line_len - path_index - 1; + if (path_len != 0) { + fprintf(log, "%s", path); + } else { + // No path, try to identify it. + label_memory_map(log, start_addr); } - __real_free(line); - fclose(maps); + fprintf(log, "\n"); +} + +void ia2_log_memory_maps(FILE *log) { + FILE *maps_file = fopen("/proc/self/maps", "r"); + assert(maps_file); + + // Skip dev and inode. + fprintf(log, " start addr-end addr perms offset path\n"); + ia2_memory_map_foreach(maps_file, log_memory_map_entry, log_memory_map_error, (void *)log); + fclose(maps_file); } diff --git a/runtime/tracer/memory-map/src/lib.rs b/runtime/tracer/memory-map/src/lib.rs index 61db1f4a01..bec66ef43d 100644 --- a/runtime/tracer/memory-map/src/lib.rs +++ b/runtime/tracer/memory-map/src/lib.rs @@ -44,6 +44,16 @@ impl fmt::Debug for Range { } impl Range { + pub fn from_bounds(start: usize, end: usize) -> Option { + if start < end { + Some(Range { + start, + len: end - start, + }) + } else { + None + } + } pub fn end(&self) -> usize { self.start + self.len } @@ -59,6 +69,25 @@ impl Range { self.start = self.start & !0xFFF; self.len = end_round_up - self.start; } + + pub fn overlap(&self, other: &Range) -> Option { + let start = self.start.max(other.start); + let end = self.end().min(other.end()); + Self::from_bounds(start, end) + } + + /// Remove the other range from this one. + /// If they do not overlap, returns None. + /// Otherwise, returns a pair of any prefix remaining from this range and any suffix remaining from this range. + pub fn subtract(&self, other: &Range) -> Option<(Option, Option)> { + match self.overlap(other) { + None => None, + Some(overlap) => Some(( + Self::from_bounds(self.start, overlap.start), + Self::from_bounds(self.end(), overlap.end()), + )), + } + } } /// The state of a contiguous region of memory @@ -178,45 +207,48 @@ impl MemoryMap { }) } - /* removes exactly the specified range from an existing region, leaving any other parts of that region mapped */ - fn split_out_region(&mut self, mut subrange: Range) -> Option { + /* removes exactly the specified range from all existing regions, leaving any other parts of that region mapped */ + fn split_out_region(&mut self, mut subrange: Range) -> Result { subrange.round_to_4k(); - // return None if the subrange does not overlap or is not fully contained - let r = match self.find_overlapping_region(subrange) { - Some(r) => r, - None => { - #[cfg(debug)] - printerrln!("{:?} has no overlap", subrange); - return None; + let mut removed_state = None; + let mut count = 0; + while let Some(r) = self.find_overlapping_region(subrange) { + // remove the range containing the subrange; this should not fail + let found = self.remove_region(r.range).is_some(); + assert!(found); + + // re-add any prefix range + let before = Range { + start: r.range.start, + len: subrange.start - r.range.start, + }; + self.add_region(before, r.state); + + // re-add any suffix range + let after = Range { + start: subrange.end(), + len: r.range.end() - subrange.end(), + }; + self.add_region(after, r.state); + + if count == 0 { + removed_state = Some(r.state); } - }; - if !r.range.subsumes(subrange) { + count += 1; + } + /*if !r.range.subsumes(subrange) { #[cfg(debug)] printerrln!("{:?} does not subsume {:?}", r.range, subrange); return None; - } - - // remove the range containing the subrange; this should not fail - let found = self.remove_region(r.range).is_some(); - assert!(found); + }*/ - // re-add any prefix range - let before = Range { - start: r.range.start, - len: subrange.start - r.range.start, - }; - self.add_region(before, r.state); - - // re-add any suffix range - let after = Range { - start: subrange.end(), - len: r.range.end() - subrange.end(), - }; - self.add_region(after, r.state); - - // return the split-out range - Some(r.state) + // return the split-out range's state, or the count ranges if not exactly 1 + if count == 1 { + Ok(removed_state.unwrap()) + } else { + Err(count) + } } pub fn split_region( @@ -225,7 +257,7 @@ impl MemoryMap { owner_pkey: u8, prot: u32, ) -> Option { - let state = self.split_out_region(subrange)?; + let state = self.split_out_region(subrange).ok()?; // add the new split-off range let new_state = State { @@ -361,7 +393,7 @@ pub extern "C" fn memory_map_region_get_owner_pkey(map: &MemoryMap, needle: Rang #[no_mangle] pub extern "C" fn memory_map_unmap_region(map: &mut MemoryMap, needle: Range) -> bool { - map.split_out_region(needle).is_some() + map.split_out_region(needle).is_ok() } #[no_mangle] @@ -398,7 +430,7 @@ pub extern "C" fn memory_map_pkey_mprotect_region( range: Range, pkey: u8, ) -> bool { - if let Some(mut state) = map.split_out_region(range) { + if let Some(mut state) = map.split_out_region(range).ok() { /* forbid pkey_mprotect of owned by another compartment other than 0 */ if state.owner_pkey != pkey && state.owner_pkey != 0 { printerrln!( @@ -429,7 +461,7 @@ pub extern "C" fn memory_map_pkey_mprotect_region( #[no_mangle] pub extern "C" fn memory_map_mprotect_region(map: &mut MemoryMap, range: Range, prot: u32) -> bool { - if let Some(mut state) = map.split_out_region(range) { + if let Some(mut state) = map.split_out_region(range).ok() { if state.mprotected == false { state.mprotected = true; state.prot = prot; diff --git a/runtime/tracer/mmap_event.c b/runtime/tracer/mmap_event.c index 5cb672980a..9594fb4a46 100644 --- a/runtime/tracer/mmap_event.c +++ b/runtime/tracer/mmap_event.c @@ -29,6 +29,8 @@ enum mmap_event event_from_syscall(uint64_t syscall_nr) { case __NR_execve: case __NR_execveat: return EVENT_EXEC; + case __NR_exit: + return EVENT_EXIT; default: return EVENT_NONE; } diff --git a/runtime/tracer/mmap_event.h b/runtime/tracer/mmap_event.h index 4a4530a5be..e66d9d6168 100644 --- a/runtime/tracer/mmap_event.h +++ b/runtime/tracer/mmap_event.h @@ -59,6 +59,7 @@ enum mmap_event { EVENT_PKEY_MPROTECT, EVENT_CLONE, EVENT_EXEC, + EVENT_EXIT, EVENT_NONE, }; @@ -71,6 +72,7 @@ static const char *event_names[] = { "PKEY_MPROTECT", "CLONE", "EXEC", + "EXIT", "NONE", }; @@ -96,6 +98,8 @@ static inline const struct range *event_target_range(enum mmap_event event, cons return NULL; case EVENT_EXEC: return NULL; + case EVENT_EXIT: + return NULL; case EVENT_NONE: return NULL; break; diff --git a/runtime/tracer/track_memory_map.c b/runtime/tracer/track_memory_map.c index 59ae1e9f8b..2c2a80b740 100644 --- a/runtime/tracer/track_memory_map.c +++ b/runtime/tracer/track_memory_map.c @@ -1,4 +1,5 @@ #define _GNU_SOURCE +#include #include #include #include @@ -112,6 +113,10 @@ static bool is_op_permitted(struct memory_map *map, int event, return true; break; } + case EVENT_EXIT: { + return true; + break; + } case EVENT_NONE: return true; break; @@ -186,6 +191,11 @@ static bool update_memory_map(struct memory_map *map, int event, return true; break; } + case EVENT_EXIT: { + printf("clearing memory map on exit\n"); + return true; + break; + } case EVENT_NONE: return true; break; @@ -618,20 +628,22 @@ static void return_syscall_eperm(pid_t pid) { debug_forbid("wrote -eperm to rax\n"); } -struct memory_map_for_processes { +/* a memory map that tracks multiple threads under the same process */ +struct memory_map_for_process { struct memory_map *map; pid_t *pids; size_t n_pids; }; +/* the set of memory maps for a family of traced processes */ struct memory_maps { - struct memory_map_for_processes *maps_for_processes; + struct memory_map_for_process *maps_for_processes; size_t n_maps; }; -static struct memory_map_for_processes *find_memory_map(struct memory_maps *maps, pid_t pid) { +static struct memory_map_for_process *find_memory_map(struct memory_maps *maps, pid_t pid) { for (int i = 0; i < maps->n_maps; i++) { - struct memory_map_for_processes *map_for_procs = &maps->maps_for_processes[i]; + struct memory_map_for_process *map_for_procs = &maps->maps_for_processes[i]; for (int j = 0; j < map_for_procs->n_pids; j++) { if (map_for_procs->pids[j] == pid) { return map_for_procs; @@ -641,7 +653,7 @@ static struct memory_map_for_processes *find_memory_map(struct memory_maps *maps return NULL; } -static bool remove_pid(struct memory_map_for_processes *map_for_procs, pid_t pid) { +static bool remove_pid(struct memory_map_for_process *map_for_procs, pid_t pid) { for (int j = 0; j < map_for_procs->n_pids; j++) { if (map_for_procs->pids[j] == pid) { // swap last into its place and decrement count @@ -653,9 +665,9 @@ static bool remove_pid(struct memory_map_for_processes *map_for_procs, pid_t pid return false; } -static bool remove_map(struct memory_maps *maps, struct memory_map_for_processes *map_to_remove) { +static bool remove_map(struct memory_maps *maps, struct memory_map_for_process *map_to_remove) { for (int i = 0; i < maps->n_maps; i++) { - struct memory_map_for_processes *map_for_procs = &maps->maps_for_processes[i]; + struct memory_map_for_process *map_for_procs = &maps->maps_for_processes[i]; if (map_for_procs == map_to_remove) { // swap last into its place and decrement count maps->maps_for_processes[i] = maps->maps_for_processes[maps->n_maps - 1]; @@ -666,15 +678,15 @@ static bool remove_map(struct memory_maps *maps, struct memory_map_for_processes return false; } -static struct memory_map_for_processes for_processes_new(struct memory_map *map, pid_t pid) { +static struct memory_map_for_process for_process_new(struct memory_map *map, pid_t pid) { pid_t *pids = malloc(sizeof(pid_t)); pids[0] = pid; - struct memory_map_for_processes for_processes = {.map = map, .pids = pids, .n_pids = 1}; + struct memory_map_for_process for_processes = {.map = map, .pids = pids, .n_pids = 1}; return for_processes; } -static enum control_flow handle_process_exit(struct memory_maps *maps, pid_t waited_pid) { - struct memory_map_for_processes *map_for_procs = find_memory_map(maps, waited_pid); +static enum control_flow handle_thread_exit(struct memory_maps *maps, pid_t waited_pid) { + struct memory_map_for_process *map_for_procs = find_memory_map(maps, waited_pid); if (!map_for_procs) { fprintf(stderr, "exited: could not find memory map for process %d\n", waited_pid); return RETURN_FALSE; @@ -683,6 +695,7 @@ static enum control_flow handle_process_exit(struct memory_maps *maps, pid_t wai fprintf(stderr, "could not remove pid %d from memory map\n", waited_pid); return RETURN_FALSE; } + // exit of the last thread in the process if (map_for_procs->n_pids == 0) { if (!remove_map(maps, map_for_procs)) { fprintf(stderr, "could not remove memory map for pid %d\n", waited_pid); @@ -696,6 +709,82 @@ static enum control_flow handle_process_exit(struct memory_maps *maps, pid_t wai return CONTINUE; } +void ia2_memory_map_foreach(FILE *maps_file, + void (*each_cb)(const char *line, ssize_t line_len, int path_index, uintptr_t start_addr, uintptr_t end_addr, char perms[static 4], uintptr_t offset, void *context), + void (*error_cb)(const char *line, void *context), + void *context) { + assert(maps_file); + + char *line = NULL; + size_t line_cap = 0; + while (true) { + const ssize_t line_len = getline(&line, &line_cap, maps_file); + if (line_len == -1) { + break; + } + + // Remove trailing newline. + if (line_len > 0 && line[line_len - 1] == '\n') { + line[line_len - 1] = 0; + } + + // Parse `/proc/self/maps` line. + uintptr_t start_addr = 0; + uintptr_t end_addr = 0; + char perms[4] = {0}; + uintptr_t offset = 0; + unsigned int dev_major = 0; + unsigned int dev_minor = 0; + ino_t inode = 0; + int path_index = 0; + const int vars_matched = sscanf(line, "%lx-%lx %4c %lx %x:%x %lu %n", &start_addr, &end_addr, perms, &offset, &dev_major, &dev_minor, &inode, &path_index); + const int expected_vars_matched = 7; // Note that "%n" doesn't count as a matched var. + if (vars_matched != expected_vars_matched) { + error_cb(line, context); + fprintf(stderr, "error parsing /proc//maps line (matched %d vars instead of %d): %s\n", + vars_matched, expected_vars_matched, line); + continue; + } + each_cb(line, line_len, path_index, start_addr, end_addr, perms, offset, context); + } + + free(line); +} + +static void fail_loading_memory_map(const char *line, void *context) { + struct memory_map **map = (struct memory_map **)context; + fprintf(stderr, "failed to load initial memory map at line:\n%s\n", line); + if (*map) { + memory_map_destroy(*map); + } + *map = NULL; +} + +static void add_memory_map_entry(const char *line, ssize_t line_len, int path_index, uintptr_t start_addr, uintptr_t end_addr, char perms[static 4], uintptr_t offset, void *context) { + struct memory_map **map = (struct memory_map **)context; + // if we have already failed, bail + if (!*map) + return; + + struct range new_range; + new_range.start = start_addr; + new_range.len = end_addr - start_addr; + uint32_t prot = (perms[0] == 'r' ? PROT_READ : 0) | (perms[1] == 'w' ? PROT_WRITE : 0) | (perms[2] == 'x' ? PROT_EXEC : 0); + uint8_t owner_pkey = 0; // initial mappings belong to the initial compartment + memory_map_add_region(*map, new_range, owner_pkey, prot); +} + +struct memory_map *load_initial_memory_map(pid_t pid) { + char *filename = NULL; + asprintf(&filename, "/proc/%d/maps", pid); + FILE *maps_file = fopen(filename, "r"); + assert(maps_file); + + struct memory_map *map = memory_map_new(); + ia2_memory_map_foreach(maps_file, add_memory_map_entry, fail_loading_memory_map, &map); + return map; +} + /* track the inferior process' memory map. returns true if the inferior exits, false on trace error. @@ -703,20 +792,33 @@ returns true if the inferior exits, false on trace error. if true is returned, the inferior's exit status will be stored to *exit_status_out if not NULL. */ bool track_memory_map(pid_t pid, int *exit_status_out, enum trace_mode mode) { - struct memory_map *map = memory_map_new(); - struct memory_map_for_processes *for_processes = malloc(sizeof(struct memory_map_for_processes)); - *for_processes = for_processes_new(map, pid); + /* load initial memory map before starting tracing loop */ + pid_t waited_pid; + /* pause the process first to avoid race */ + enum wait_trap_result wait_result = wait_for_next_trap(-1, &waited_pid, exit_status_out); + + struct memory_map *map = load_initial_memory_map(waited_pid); + if (!map) { + fprintf(stderr, "failed to load initial memory map for pid %d, exiting", pid); + return false; + } + struct memory_map_for_process *for_process = malloc(sizeof(struct memory_map_for_process)); + *for_process = for_process_new(map, pid); struct memory_maps maps = { - .maps_for_processes = for_processes, + .maps_for_processes = for_process, .n_maps = 1, }; + /* jump into tracing loop */ + goto with_wait_result; + enum __ptrace_request continue_request = mode == TRACE_MODE_PTRACE_SYSCALL ? PTRACE_SYSCALL : PTRACE_CONT; while (true) { /* wait for the process to get signalled */ - pid_t waited_pid = pid; - enum wait_trap_result wait_result = wait_for_next_trap(-1, &waited_pid, exit_status_out); + waited_pid = pid; + wait_result = wait_for_next_trap(-1, &waited_pid, exit_status_out); + with_wait_result: switch (wait_result) { /* we need to handle events relating to process lifetime upfront: these include clone()/fork()/exec() and sigchld */ @@ -780,7 +882,7 @@ bool track_memory_map(pid_t pid, int *exit_status_out, enum trace_mode mode) { } debug_proc("should track child pid %d\n", cloned_pid); - struct memory_map_for_processes *map_for_procs = find_memory_map(&maps, waited_pid); + struct memory_map_for_process *map_for_procs = find_memory_map(&maps, waited_pid); map_for_procs->n_pids++; map_for_procs->pids = realloc(map_for_procs->pids, map_for_procs->n_pids * sizeof(pid_t)); map_for_procs->pids[map_for_procs->n_pids - 1] = cloned_pid; @@ -795,17 +897,17 @@ bool track_memory_map(pid_t pid, int *exit_status_out, enum trace_mode mode) { } debug_proc("should track forked child pid %d\n", cloned_pid); - struct memory_map_for_processes *map_for_procs = find_memory_map(&maps, waited_pid); + struct memory_map_for_process *map_for_procs = find_memory_map(&maps, waited_pid); struct memory_map *cloned = memory_map_clone(map_for_procs->map); remove_pid(map_for_procs, cloned_pid); maps.n_maps++; - maps.maps_for_processes = realloc(maps.maps_for_processes, maps.n_maps * sizeof(struct memory_map_for_processes)); - maps.maps_for_processes[maps.n_maps - 1] = for_processes_new(cloned, cloned_pid); + maps.maps_for_processes = realloc(maps.maps_for_processes, maps.n_maps * sizeof(struct memory_map_for_process)); + maps.maps_for_processes[maps.n_maps - 1] = for_process_new(cloned, cloned_pid); break; } case WAIT_EXEC: { - struct memory_map_for_processes *map_for_procs = find_memory_map(&maps, waited_pid); + struct memory_map_for_process *map_for_procs = find_memory_map(&maps, waited_pid); if (!map_for_procs) { fprintf(stderr, "exec: could not find memory map for process %d\n", waited_pid); return false; @@ -816,19 +918,19 @@ bool track_memory_map(pid_t pid, int *exit_status_out, enum trace_mode mode) { } case WAIT_SIGNALED: { fprintf(stderr, "process received fatal signal (syscall entry)\n"); - enum control_flow cf = handle_process_exit(&maps, waited_pid); + enum control_flow cf = handle_thread_exit(&maps, waited_pid); return false; } case WAIT_EXITED: { debug_exit("pid %d exited (syscall entry)\n", waited_pid); - propagate(handle_process_exit(&maps, waited_pid)); + propagate(handle_thread_exit(&maps, waited_pid)); // in any case, this process is gone, so wait for a new one continue; } } - struct memory_map_for_processes *map_for_procs = find_memory_map(&maps, waited_pid); + struct memory_map_for_process *map_for_procs = find_memory_map(&maps, waited_pid); if (!map_for_procs) { fprintf(stderr, "could not find memory map for process %d\n", waited_pid); return false; @@ -940,7 +1042,7 @@ bool track_memory_map(pid_t pid, int *exit_status_out, enum trace_mode mode) { } printf("should track child pid %d\n", cloned_pid); - struct memory_map_for_processes *map_for_procs = find_memory_map(&maps, waited_pid); + struct memory_map_for_process *map_for_procs = find_memory_map(&maps, waited_pid); map_for_procs->n_pids++; map_for_procs->pids = realloc(map_for_procs->pids, map_for_procs->n_pids * sizeof(pid_t)); map_for_procs->pids[map_for_procs->n_pids - 1] = cloned_pid; @@ -949,13 +1051,13 @@ bool track_memory_map(pid_t pid, int *exit_status_out, enum trace_mode mode) { } case WAIT_EXITED: debug_exit("pid %d exited (syscall exit)\n", waited_pid); - propagate(handle_process_exit(&maps, waited_pid)); + propagate(handle_thread_exit(&maps, waited_pid)); // in any case, this process is gone, so wait for a new one continue; case WAIT_EXEC: fprintf(stderr, "unexpected PTRACE_O_TRACEEXEC stop at syscall exit\n"); - struct memory_map_for_processes *map_for_procs = find_memory_map(&maps, waited_pid); + struct memory_map_for_process *map_for_procs = find_memory_map(&maps, waited_pid); if (!map_for_procs) { fprintf(stderr, "exec: could not find memory map for process %d\n", waited_pid); return false; diff --git a/tests/memory_maps/CMakeLists.txt b/tests/memory_maps/CMakeLists.txt index 61bd6b11b5..ecae940fd4 100644 --- a/tests/memory_maps/CMakeLists.txt +++ b/tests/memory_maps/CMakeLists.txt @@ -22,5 +22,4 @@ define_test( LIBS 2 3 4 NEEDS_LD_WRAP CRITERION_TEST - WITHOUT_SANDBOX # tracer forbids reading `/proc/self/maps` )