[GH-ISSUE #8779] [Bug] When a Rizin debug command hits its deadline while the target runs, cleanup can't detach, and Rizin and the target outlive the MCP server #7541

Open
opened 2026-11-03 12:24:58 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @Rinkia on GitHub (Nov 3, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/8779

Before submitting

  • I searched the open and closed issues for a related report (rizin deadline, rizin timeout cleanup, rizin dc timeout, deadlineExpired, rizin detach not verified, rizin cleanup_incomplete, REA_DEBUGGER_COMMAND_DEADLINE_MS). Related but different: #5896 (closed; cleanup after a limit or deadline was blocked by the session's own protocolError, tested with simulated streams), #8642 / PR #8647 (close fails because a target that reads stdin consumes REA's commands; my targets don't read stdin), #8023 / PR #8091 (the GDB counterpart, see below).
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Debugging (Rizin debug sessions)

Observed behavior

In a persistent Rizin debug session, dc on a target that keeps running past the command deadline ends in a session that can't be cleaned up while the target runs:

  • The command returns cleanup_incomplete one second after the deadline: "The owned Rizin process could not be confirmed stopped: Owned Rizin debugger inferior detach was not verified within 1000 ms. This release has no runtime override."
  • Each later rizin_debug_command and close_rizin_debug_session repeats the same error, and each takes another 1000 ms. rizin_debug_session_status reports state: "closed".
  • When the MCP server exits, either killed or after its stdin closes, Rizin and the running target are reparented to PID 1 and keep running. With the probe below they stay until the target ends on its own (120 s). A target that never ends would keep both processes indefinitely.

The cause looks like this. Cleanup detaches by sending olj, e dbg.exitkills and o- <fd> to Rizin. Rizin is still blocked inside dc and doesn't read them until the target stops. Nothing interrupts the running target first, so detach waits until it times out. The same happens with a shell command, !sleep 10 with a 3000 ms deadline. There, cleanup recovers once the sleep ends: a later close after 18 s returned clean on one run and an ESRCH read error on the other.

With the default deadline (120000 ms), any dc that waits more than two minutes for a breakpoint that isn't hit ends the same way.

Two checks with Rizin directly, outside REA, both in Rizin 0.9.1 with -0 -d and stdin a pipe:

  • SIGINT to Rizin doesn't break dc. Rizin keeps waiting.
  • SIGSTOP to the inferior does break it. Rizin reports signal 19 aka SIGSTOP received and answers the next command (dr rip).
  • SIGKILL to Rizin during dc leaves the target running and untraced (TracerPid: 0), because dbg.exitkills=false.

For comparison, GDB on main behaves the same with run past a 3000 ms deadline: cleanup_incomplete ("GDB MI state is not synchronized; inferior state is unknown"), and GDB plus the target are left behind. With PR #8091 at 001d8032c the same GDB sequence closes the session and leaves no process. I didn't find a Rizin counterpart to that change.

Reproduction

I ran this in a Linux x64 container (--network none, --cap-add SYS_PTRACE) on Ubuntu 24.04, with the Rizin 0.9.1 static release and main at 4b961e371. Probe:

/* wait.c: gcc -g -O0 -o /p/wait wait.c */
#include <stdio.h>
#include <unistd.h>
int main(void) { for (int i = 0; i < 120; i++) { printf("tick %d\n", i); fflush(stdout); sleep(1); } return 0; }

Start the MCP server with REA_DEBUGGER_COMMAND_DEADLINE_MS=3000 to make it quick. The default 120000 ms gives the same result with a longer target. Then:

start_rizin_debug_session {"path":"/p/wait","inferior_tty":"/dev/pts/0"}
rizin_debug_command "dc"       -> cleanup_incomplete after ~4.0 s (3 s deadline + 1 s detach)
rizin_debug_command "dr rip"   -> cleanup_incomplete after 1.0 s
rizin_debug_session_status     -> state "closed"
close_rizin_debug_session      -> cleanup_incomplete after 1.0 s
(end the MCP server)
ps                             -> rizin -N -0 -d ... /p/wait (PPID 1) and /p/wait still running

Same result in 2 of 2 runs, and with a graceful server shutdown (stdin closed, 15 s wait).

Expected behavior

A deadline on a running target should leave the session in a state REA can clean up. Either:

  • interrupt the target so Rizin returns to its prompt, then detach as usual, or
  • once detach isn't possible, stop Rizin as #8091 does for GDB. The target then keeps running, as the close description already allows.

At least, Rizin and the target shouldn't outlive the server without a trace.

Impact and scope

This affects persistent Rizin debug sessions over MCP whenever a command blocks past the deadline: dc waiting for a breakpoint that isn't hit, or a long ! shell command. It doesn't affect commands that finish. I tested Linux x64 only.

Side note: docs/reverse-engineering-tools.md says Rizin and GDB sessions have no runtime override for the per-command deadline or the 16 MiB command-output cap. On main, REA_DEBUGGER_COMMAND_DEADLINE_MS=3000 and REA_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES=100000 both take effect. The output-cap limitation even names the setting.

REA version

main at 4b961e371

Runtime and platform

Node 24.22.1; Ubuntu 24.04 x86-64 container; Rizin 0.9.1 (static release)

Target and artifact context

Self-written ELF x86-64 probe (above)

Interface

MCP

Earliest failure stage

Cleanup, cancellation, or lifecycle

Originally created by @Rinkia on GitHub (Nov 3, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/8779 ### Before submitting - [x] I searched the open and closed issues for a related report (rizin deadline, rizin timeout cleanup, rizin dc timeout, deadlineExpired, rizin detach not verified, rizin cleanup_incomplete, REA_DEBUGGER_COMMAND_DEADLINE_MS). Related but different: #5896 (closed; cleanup after a limit or deadline was blocked by the session's own `protocolError`, tested with simulated streams), #8642 / PR #8647 (close fails because a target that reads stdin consumes REA's commands; my targets don't read stdin), #8023 / PR #8091 (the GDB counterpart, see below). - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Debugging (Rizin debug sessions) ### Observed behavior In a persistent Rizin debug session, `dc` on a target that keeps running past the command deadline ends in a session that can't be cleaned up while the target runs: - The command returns `cleanup_incomplete` one second after the deadline: "The owned Rizin process could not be confirmed stopped: Owned Rizin debugger inferior detach was not verified within 1000 ms. This release has no runtime override." - Each later `rizin_debug_command` and `close_rizin_debug_session` repeats the same error, and each takes another 1000 ms. `rizin_debug_session_status` reports `state: "closed"`. - When the MCP server exits, either killed or after its stdin closes, Rizin and the running target are reparented to PID 1 and keep running. With the probe below they stay until the target ends on its own (120 s). A target that never ends would keep both processes indefinitely. The cause looks like this. Cleanup detaches by sending `olj`, `e dbg.exitkills` and `o- <fd>` to Rizin. Rizin is still blocked inside `dc` and doesn't read them until the target stops. Nothing interrupts the running target first, so detach waits until it times out. The same happens with a shell command, `!sleep 10` with a 3000 ms deadline. There, cleanup recovers once the sleep ends: a later close after 18 s returned clean on one run and an `ESRCH` read error on the other. With the default deadline (120000 ms), any `dc` that waits more than two minutes for a breakpoint that isn't hit ends the same way. Two checks with Rizin directly, outside REA, both in Rizin 0.9.1 with `-0 -d` and stdin a pipe: - SIGINT to Rizin doesn't break `dc`. Rizin keeps waiting. - SIGSTOP to the inferior does break it. Rizin reports `signal 19 aka SIGSTOP received` and answers the next command (`dr rip`). - SIGKILL to Rizin during `dc` leaves the target running and untraced (`TracerPid: 0`), because `dbg.exitkills=false`. For comparison, GDB on `main` behaves the same with `run` past a 3000 ms deadline: `cleanup_incomplete` ("GDB MI state is not synchronized; inferior state is unknown"), and GDB plus the target are left behind. With PR #8091 at `001d8032c` the same GDB sequence closes the session and leaves no process. I didn't find a Rizin counterpart to that change. ### Reproduction I ran this in a Linux x64 container (`--network none`, `--cap-add SYS_PTRACE`) on Ubuntu 24.04, with the Rizin 0.9.1 static release and `main` at `4b961e371`. Probe: ```c /* wait.c: gcc -g -O0 -o /p/wait wait.c */ #include <stdio.h> #include <unistd.h> int main(void) { for (int i = 0; i < 120; i++) { printf("tick %d\n", i); fflush(stdout); sleep(1); } return 0; } ``` Start the MCP server with `REA_DEBUGGER_COMMAND_DEADLINE_MS=3000` to make it quick. The default 120000 ms gives the same result with a longer target. Then: ``` start_rizin_debug_session {"path":"/p/wait","inferior_tty":"/dev/pts/0"} rizin_debug_command "dc" -> cleanup_incomplete after ~4.0 s (3 s deadline + 1 s detach) rizin_debug_command "dr rip" -> cleanup_incomplete after 1.0 s rizin_debug_session_status -> state "closed" close_rizin_debug_session -> cleanup_incomplete after 1.0 s (end the MCP server) ps -> rizin -N -0 -d ... /p/wait (PPID 1) and /p/wait still running ``` Same result in 2 of 2 runs, and with a graceful server shutdown (stdin closed, 15 s wait). ### Expected behavior A deadline on a running target should leave the session in a state REA can clean up. Either: - interrupt the target so Rizin returns to its prompt, then detach as usual, or - once detach isn't possible, stop Rizin as #8091 does for GDB. The target then keeps running, as the close description already allows. At least, Rizin and the target shouldn't outlive the server without a trace. ### Impact and scope This affects persistent Rizin debug sessions over MCP whenever a command blocks past the deadline: `dc` waiting for a breakpoint that isn't hit, or a long `!` shell command. It doesn't affect commands that finish. I tested Linux x64 only. Side note: `docs/reverse-engineering-tools.md` says Rizin and GDB sessions have no runtime override for the per-command deadline or the 16 MiB command-output cap. On `main`, `REA_DEBUGGER_COMMAND_DEADLINE_MS=3000` and `REA_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES=100000` both take effect. The output-cap limitation even names the setting. ### REA version `main` at `4b961e371` ### Runtime and platform Node 24.22.1; Ubuntu 24.04 x86-64 container; Rizin 0.9.1 (static release) ### Target and artifact context Self-written ELF x86-64 probe (above) ### Interface MCP ### Earliest failure stage Cleanup, cancellation, or lifecycle
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#7541
No description provided.