Skip to content

x86/arm64: allow partial PTRACE_GETREGSET NT_PRSTATUS reads#12899

Open
ivg wants to merge 1 commit intogoogle:masterfrom
ivg:fix-ptrace-getregset-partial-read
Open

x86/arm64: allow partial PTRACE_GETREGSET NT_PRSTATUS reads#12899
ivg wants to merge 1 commit intogoogle:masterfrom
ivg:fix-ptrace-getregset-partial-read

Conversation

@ivg
Copy link
Copy Markdown

@ivg ivg commented Apr 8, 2026

The Linux kernel's ptrace_regset() (kernel/ptrace.c) writes min(iov_len, actual_size) bytes and returns success for any non-zero buffer; it never rejects a PTRACE_GETREGSET request solely because the buffer is smaller than the full register set.

gVisor's PtraceGetRegSet previously returned EFAULT when maxlen was less than ptraceRegistersSize (216 bytes for x86_64). This broke any tool that passes a smaller buffer, most notably libunwind whose _UPT_access_reg() allocates gregset_t (184 bytes) rather than struct user_regs_struct (216 bytes) for the PTRACE_GETREGSET call. The result was that unw_init_remote() failed with UNW_EBADREG ("bad register number"), making ptrace-based stack unwinding completely non-functional inside gVisor containers. The corresponding fix to libunwind: libunwind/libunwind#977

Fix: when maxlen is in the range (0, ptraceRegistersSize), serialize the full register set to a temporary buffer and write only maxlen bytes to dst, matching the kernel's truncation semantics. The fast path (maxlen

= ptraceRegistersSize) is unchanged.

@google-cla
Copy link
Copy Markdown

google-cla Bot commented Apr 8, 2026

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

Copy link
Copy Markdown
Collaborator

@EtiennePerot EtiennePerot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a syscall test in test/syscalls to exercise this and ensure Linux behaves the same way?

@ayushr2
Copy link
Copy Markdown
Collaborator

ayushr2 commented Apr 8, 2026

See test/syscalls/linux/ptrace.cc

@ivg ivg force-pushed the fix-ptrace-getregset-partial-read branch from c772307 to 663db51 Compare April 8, 2026 23:04
@ivg
Copy link
Copy Markdown
Author

ivg commented Apr 8, 2026

Can you add a syscall test in test/syscalls to exercise this and ensure Linux behaves the same way?

The test is added, and here's the permalink to the kernel implementation:

kiov->iov_len = min(kiov->iov_len, (__kernel_size_t) (regset->n * regset->size));

The kernel clamps iov_len down to the actual register set size, then proceeds with copy_regset_to_user — no size check, no EFAULT.

@ivg ivg requested a review from EtiennePerot April 8, 2026 23:09
Comment thread test/syscalls/linux/ptrace.cc Outdated
@ayushr2
Copy link
Copy Markdown
Collaborator

ayushr2 commented Apr 22, 2026

The newly added test is failing: https://buildkite.com/gvisor/pipeline/builds/41656

[ RUN      ] PtraceTest.GetRegSetPartialRead
test/syscalls/linux/ptrace.cc:1444: Failure
Value of: ptrace(PTRACE_GETREGSET, child_pid, 1, &partial_iov)
Expected: not -1 (success)
  Actual: -1 (of type long), with errno PosixError(errno=14 Bad address)

[  FAILED  ] PtraceTest.GetRegSetPartialRead (4 ms)
[----------] 3 tests from PtraceTest (138 ms total)

[----------] Global test environment tear-down
[==========] 3 tests from 1 test suite ran. (139 ms total)
[  PASSED  ] 2 tests.
[  FAILED  ] 1 test, listed below:
[  FAILED  ] PtraceTest.GetRegSetPartialRead

copybara-service Bot pushed a commit that referenced this pull request Apr 22, 2026
The Linux kernel's ptrace_regset() (kernel/ptrace.c) writes min(iov_len, actual_size) bytes and returns success for any non-zero buffer; it never rejects a PTRACE_GETREGSET request solely because the buffer is smaller than the full register set.

gVisor's PtraceGetRegSet previously returned EFAULT when maxlen was less than ptraceRegistersSize (216 bytes for x86_64).  This broke any tool that passes a smaller buffer, most notably libunwind whose _UPT_access_reg() allocates gregset_t (184 bytes) rather than struct user_regs_struct (216 bytes) for the PTRACE_GETREGSET call.  The result was that unw_init_remote() failed with UNW_EBADREG ("bad register number"), making ptrace-based stack unwinding completely non-functional inside gVisor containers. The corresponding fix to libunwind: libunwind/libunwind#977

Fix: when maxlen is in the range (0, ptraceRegistersSize), serialize the full register set to a temporary buffer and write only maxlen bytes to dst, matching the kernel's truncation semantics.  The fast path (maxlen
>= ptraceRegistersSize) is unchanged.
FUTURE_COPYBARA_INTEGRATE_REVIEW=#12899 from ivg:fix-ptrace-getregset-partial-read 7eead62
PiperOrigin-RevId: 900253310
copybara-service Bot pushed a commit that referenced this pull request Apr 22, 2026
The Linux kernel's ptrace_regset() (kernel/ptrace.c) writes min(iov_len, actual_size) bytes and returns success for any non-zero buffer; it never rejects a PTRACE_GETREGSET request solely because the buffer is smaller than the full register set.

gVisor's PtraceGetRegSet previously returned EFAULT when maxlen was less than ptraceRegistersSize (216 bytes for x86_64).  This broke any tool that passes a smaller buffer, most notably libunwind whose _UPT_access_reg() allocates gregset_t (184 bytes) rather than struct user_regs_struct (216 bytes) for the PTRACE_GETREGSET call.  The result was that unw_init_remote() failed with UNW_EBADREG ("bad register number"), making ptrace-based stack unwinding completely non-functional inside gVisor containers. The corresponding fix to libunwind: libunwind/libunwind#977

Fix: when maxlen is in the range (0, ptraceRegistersSize), serialize the full register set to a temporary buffer and write only maxlen bytes to dst, matching the kernel's truncation semantics.  The fast path (maxlen
>= ptraceRegistersSize) is unchanged.
FUTURE_COPYBARA_INTEGRATE_REVIEW=#12899 from ivg:fix-ptrace-getregset-partial-read 7eead62
PiperOrigin-RevId: 900253310
@ivg ivg force-pushed the fix-ptrace-getregset-partial-read branch from 7eead62 to 75f9fd4 Compare April 22, 2026 19:32
@ivg
Copy link
Copy Markdown
Author

ivg commented Apr 22, 2026

Addressed, the implementation is now much cleaner and the tests pass locally on my machine.

copybara-service Bot pushed a commit that referenced this pull request Apr 22, 2026
The Linux kernel's ptrace_regset() (kernel/ptrace.c) writes min(iov_len, actual_size) bytes and returns success for any non-zero buffer; it never rejects a PTRACE_GETREGSET request solely because the buffer is smaller than the full register set.

gVisor's PtraceGetRegSet previously returned EFAULT when maxlen was less than ptraceRegistersSize (216 bytes for x86_64).  This broke any tool that passes a smaller buffer, most notably libunwind whose _UPT_access_reg() allocates gregset_t (184 bytes) rather than struct user_regs_struct (216 bytes) for the PTRACE_GETREGSET call.  The result was that unw_init_remote() failed with UNW_EBADREG ("bad register number"), making ptrace-based stack unwinding completely non-functional inside gVisor containers. The corresponding fix to libunwind: libunwind/libunwind#977

Fix: when maxlen is in the range (0, ptraceRegistersSize), serialize the full register set to a temporary buffer and write only maxlen bytes to dst, matching the kernel's truncation semantics.  The fast path (maxlen
>= ptraceRegistersSize) is unchanged.
FUTURE_COPYBARA_INTEGRATE_REVIEW=#12899 from ivg:fix-ptrace-getregset-partial-read 75f9fd4
PiperOrigin-RevId: 900253310
copybara-service Bot pushed a commit that referenced this pull request Apr 22, 2026
The Linux kernel's ptrace_regset() (kernel/ptrace.c) writes min(iov_len, actual_size) bytes and returns success for any non-zero buffer; it never rejects a PTRACE_GETREGSET request solely because the buffer is smaller than the full register set.

gVisor's PtraceGetRegSet previously returned EFAULT when maxlen was less than ptraceRegistersSize (216 bytes for x86_64).  This broke any tool that passes a smaller buffer, most notably libunwind whose _UPT_access_reg() allocates gregset_t (184 bytes) rather than struct user_regs_struct (216 bytes) for the PTRACE_GETREGSET call.  The result was that unw_init_remote() failed with UNW_EBADREG ("bad register number"), making ptrace-based stack unwinding completely non-functional inside gVisor containers. The corresponding fix to libunwind: libunwind/libunwind#977

Fix: when maxlen is in the range (0, ptraceRegistersSize), serialize the full register set to a temporary buffer and write only maxlen bytes to dst, matching the kernel's truncation semantics.  The fast path (maxlen
>= ptraceRegistersSize) is unchanged.
FUTURE_COPYBARA_INTEGRATE_REVIEW=#12899 from ivg:fix-ptrace-getregset-partial-read 75f9fd4
PiperOrigin-RevId: 900253310
copybara-service Bot pushed a commit that referenced this pull request Apr 22, 2026
The Linux kernel's ptrace_regset() (kernel/ptrace.c) writes min(iov_len, actual_size) bytes and returns success for any non-zero buffer; it never rejects a PTRACE_GETREGSET request solely because the buffer is smaller than the full register set.

gVisor's PtraceGetRegSet previously returned EFAULT when maxlen was less than ptraceRegistersSize (216 bytes for x86_64).  This broke any tool that passes a smaller buffer, most notably libunwind whose _UPT_access_reg() allocates gregset_t (184 bytes) rather than struct user_regs_struct (216 bytes) for the PTRACE_GETREGSET call.  The result was that unw_init_remote() failed with UNW_EBADREG ("bad register number"), making ptrace-based stack unwinding completely non-functional inside gVisor containers. The corresponding fix to libunwind: libunwind/libunwind#977

Fix: when maxlen is in the range (0, ptraceRegistersSize), serialize the full register set to a temporary buffer and write only maxlen bytes to dst, matching the kernel's truncation semantics.  The fast path (maxlen
>= ptraceRegistersSize) is unchanged.
FUTURE_COPYBARA_INTEGRATE_REVIEW=#12899 from ivg:fix-ptrace-getregset-partial-read 75f9fd4
PiperOrigin-RevId: 900253310
@ayushr2
Copy link
Copy Markdown
Collaborator

ayushr2 commented Apr 22, 2026

Could you describe how you are running the test locally? It is still failing: https://buildkite.com/gvisor/pipeline/builds/41675

[ RUN      ] PtraceTest.GetRegSetPartialRead
test/syscalls/linux/ptrace.cc:1444: Failure
Value of: ptrace(PTRACE_GETREGSET, child_pid, 1, &partial_iov)
Expected: not -1 (success)
  Actual: -1 (of type long), with errno PosixError(errno=14 Bad address)

[  FAILED  ] PtraceTest.GetRegSetPartialRead (4 ms)
[----------] 3 tests from PtraceTest (139 ms total)

@ivg
Copy link
Copy Markdown
Author

ivg commented Apr 23, 2026

This is what I do:

bazelisk build //test/syscalls/linux:ptrace_test
bazel-bin/test/syscalls/linux/ptrace_test \
    --gtest_filter="PtraceTest.GetRegSet:PtraceTest.GetRegSetPartialRead"
   
[==========] Running 2 tests from 1 test suite.
[----------] Global test environment set-up.
[----------] 2 tests from PtraceTest
[ RUN      ] PtraceTest.GetRegSet
[       OK ] PtraceTest.GetRegSet (0 ms)
[ RUN      ] PtraceTest.GetRegSetPartialRead
[       OK ] PtraceTest.GetRegSetPartialRead (0 ms)
[----------] 2 tests from PtraceTest (0 ms total)

So it is run on native linux, I can't rebuild gvisor locally. It looks like it is failing on ARM64, which I didn't fix (and didn't even fix about it, sorry). Let me address it.

The Linux kernel's ptrace_regset() (kernel/ptrace.c) writes
min(iov_len, actual_size) bytes and returns success for any non-zero
buffer; it never rejects a PTRACE_GETREGSET request solely because the
buffer is smaller than the full register set.

gVisor's PtraceGetRegSet previously returned EFAULT when maxlen was
less than ptraceRegistersSize on both x86 and arm64.  This broke any
tool that passes a smaller buffer, most notably libunwind which passes
sizeof(gregset_t) = 184 bytes rather than sizeof(user_regs_struct) = 216
bytes for the PTRACE_GETREGSET call, causing unw_init_remote() to fail
with UNW_EBADREG and making ptrace-based stack unwinding non-functional
inside gVisor containers.

Fix PtraceGetRegSet on x86 and arm64 to write min(maxlen,
ptraceRegistersSize) bytes when maxlen is positive but smaller than the
full register set, matching Linux kernel semantics.  Add a syscall test
that exercises the partial read path and verifies the bytes written match
the corresponding bytes from a full register read.

Signed-off-by: Ivan Gotovchits <ivg@ieee.org>
@ivg ivg force-pushed the fix-ptrace-getregset-partial-read branch from 75f9fd4 to c071889 Compare April 23, 2026 02:58
@ivg ivg changed the title arch/x86: allow partial PTRACE_GETREGSET NT_PRSTATUS reads x86/arm64: allow partial PTRACE_GETREGSET NT_PRSTATUS reads Apr 23, 2026
@ivg
Copy link
Copy Markdown
Author

ivg commented Apr 23, 2026

updated the arm64 achitecture as well (and updated the title).

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants