Skip to content

Restore all of the stack that Task::os_exec writes - #4109

Open
Keno wants to merge 1 commit into
rr-debugger:masterfrom
ChronitonAI:os-exec-restore-args
Open

Keno wants to merge 1 commit into
rr-debugger:masterfrom
ChronitonAI:os-exec-restore-args

Conversation

@Keno

@Keno Keno commented Oct 6, 2026 •

Copy link
Copy Markdown
Member

[Encountered, debugged and patch by AI]

To replay an execve(), rr makes the task exec rr_exec_stub with Task::os_exec, which writes the arguments for that exec at the start of the page that the task's stack pointer is in: three words of the tracee's size (an argv array whose terminating NULL also serves as the empty envp) followed by the stub's file name. When the task shares its address space with another process, e.g. a vfork child, os_exec saves that memory first and restores it after the exec, since the other process still uses it. But it saved and restored only filename.size() + 1 + 2 * sizeof(size_t) bytes, with rr's own size_t. When that is the tracee's word size (a 64-bit tracee under a 64-bit rr, or a 32-bit tracee under a 32-bit build of rr), that is one word less than it writes, so the last word's worth of what it wrote, the end of the stub's file name, stayed in the memory the task shares with the other process. Recording doesn't write there, so the memory of the vfork parent's stack differs in replay. The parent usually doesn't read it again (it's below its stack pointer), but --checksum notices:

Divergence in contents of memory segment after 'SYSCALL: vfork':
0x7ffd581d7000-0x7ffd581fa000 rw-p ... (recorded checksum:...;
replaying checksum:...)

This regressed in 7f3bb73 ("Set up an empty argv[0] string when we exec the rr stub, to avoid a kernel warning."), which added two words to what os_exec writes but only one to what it saves.

So save three 8-byte words and the file name, which covers what os_exec writes for every tracee word size.

To replay an execve(), rr makes the task exec rr_exec_stub with
Task::os_exec, which writes the arguments for that exec at the start
of the page that the task's stack pointer is in: three words of the
tracee's size (an argv array whose terminating NULL also serves as the
empty envp) followed by the stub's file name. When the task shares its
address space with another process, e.g. a vfork child, os_exec saves
that memory first and restores it after the exec, since the other
process still uses it. But it saved and restored only
filename.size() + 1 + 2 * sizeof(size_t) bytes, with rr's own size_t.
When that is the tracee's word size (a 64-bit tracee under a 64-bit
rr, or a 32-bit tracee under a 32-bit build of rr), that is one word
less than it writes, so the last word's worth of what it wrote, the end
of the stub's file name, stayed in the memory the task shares with the
other process. Recording doesn't write there, so the memory of the
vfork parent's stack differs in replay. The parent usually doesn't
read it again (it's below its stack pointer), but --checksum notices:

  Divergence in contents of memory segment after 'SYSCALL: vfork':
  0x7ffd581d7000-0x7ffd581fa000 rw-p ... (recorded checksum:...;
  replaying checksum:...)

This regressed in 7f3bb73 ("Set up an empty argv[0] string when we
exec the rr stub, to avoid a kernel warning."), which added two words
to what os_exec writes but only one to what it saves.

So save three 8-byte words and the file name, which covers what
os_exec writes for every tracee word size.

Test: vfork_checksum records and replays the vfork test, whose vfork
child execs, with --checksum=on-all-events.

Results on x86_64 (Linux 7.0, glibc 2.31), with a 64-bit build of rr:
- Without this change, the test fails in every run of the 64-bit
  variant without the syscall buffer (20 of 20). The other variants
  pass: a 32-bit tracee's three 4-byte words fit in the two 8-byte
  words that os_exec saved, and with the syscall buffer the child makes
  the syscall on its own stack in its scratch memory, which nothing else
  uses.
- With this change, it passes in 50 of 50 runs in each of the four
  variants, as do vfork, vfork_done, vfork_exec, vfork_flush and
  vfork_shared.
- The full test suite shows no new failures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant