[GH-ISSUE #8780] [Docs] Process capture: same-group children of the target get SIGHUP when the target exits, and neither the docs nor the Evidence say so #7542

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/8780

Before submitting

  • I searched the existing issues for a related documentation report (SIGHUP, process capture hangup, PTY session leader, capture process group child, capture descendant signal). Related: #8032 / PR #8043, about cleanup that kills a detached (setsid) descendant. That case is different, because there REA's cleanup sends the signal. I first mentioned this observation in a comment on #8043.
  • I removed credentials, tokens, private paths, and sensitive target data.

Documentation location

docs/process-capture.md, the cleanup and settlement paragraphs ("Settlement reports whether the sampled process group quiesced or whether cleanup was needed or unverifiable; sampling cannot prove that every short-lived or detached descendant was seen.") and the capture_process_scenario tool description.

Problem

capture_process_scenario starts the target as the session leader of the capture PTY. When the target exits, the kernel sends SIGHUP to the PTY's foreground process group. A child that the target forked without setsid or setpgid, and that is still running, receives that SIGHUP. By default it dies a few milliseconds after the target exits.

This isn't REA cleanup, and cleanup_outcome: "not_required" is accurate. But the capture then shows different behavior from running the program in an interactive terminal or without a terminal, where the child keeps running. Nothing in the result points to it:

  • settlement is quiesced with cleanup_outcome: "not_required".
  • process_samples is empty. The child ended before any sample was taken.
  • No residual unknown or limitation mentions the hangup.

A caller who captures a program that forks a background worker sees a clean exit with no descendants. They can't tell that the worker was started and then hung up by the capture setup.

Expected content

A short note in docs/process-capture.md, and possibly a residual unknown in the Evidence, saying:

  • the target runs as the session leader of the capture PTY,
  • when it exits, still-running processes in its foreground process group receive SIGHUP from the terminal, as with script -c <program> or ssh -t host <program>,
  • children that should outlive the target must leave the group, for example with setsid or by ignoring SIGHUP.

This is a proposed clarification. Whether REA should instead keep the group alive, for example by running the target under a session-leader wrapper, is a design choice I'm not asking for.

Supporting evidence

I ran a self-written probe in a Linux x64 container (--network none), Node 24.22.1, on rea-agents@6.8.0 and main at 4b961e371. The parent exits at once. Its child stays in the parent's process group, ticks for 5 s, and logs any catchable signal. Signals are blocked across fork, so an early signal is still logged.

/* grpchild.c */
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
static FILE *log_file;
static void on_sig(int s) { fprintf(log_file, "signal %d\n", s); fflush(log_file); _exit(0); }
int main(void) {
    sigset_t all, old;
    sigfillset(&all);
    sigprocmask(SIG_BLOCK, &all, &old);
    log_file = fopen("grpchild.log", "w");
    fputs("start\n", log_file); fflush(log_file);
    if (fork() == 0) {
        for (int s = 1; s < 32; s++) if (s != SIGKILL && s != SIGSTOP) signal(s, on_sig);
        fprintf(log_file, "child pid=%d pgid=%d\n", getpid(), getpgrp()); fflush(log_file);
        sigprocmask(SIG_SETMASK, &old, NULL);
        for (int i = 0; i < 10; i++) { usleep(500000); fprintf(log_file, "tick %d\n", i); fflush(log_file); }
        fputs("finished\n", log_file); fclose(log_file);
        return 0;
    }
    printf("parent-exit\n");
    return 0;
}
How the probe runs Child's log
capture_process_scenario {"executable":"/tmp/grpchild","working_directory":"/tmp/pcg","timeout_ms":8000}, 6.8.0 and main start, child pid=… pgid=<parent pid>, signal 1. Evidence: exit code 0, settlement.cleanup_outcome: "not_required", process_samples: [], no intervention.
Directly, no terminal ticks 0 to 9, finished
script -qec "sh -c '/tmp/grpchild; sleep 7'", where the shell is the PTY session leader ticks 0 to 9, finished
script -qec /tmp/grpchild, where the probe is the PTY session leader signal 1, same as the capture

The last row shows the SIGHUP comes from the terminal, not from REA.

Additional context

I didn't test macOS. The hangup comes from POSIX session semantics, so it probably happens there too.

Originally created by @Rinkia on GitHub (Nov 3, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/8780 ### Before submitting - [x] I searched the existing issues for a related documentation report (SIGHUP, process capture hangup, PTY session leader, capture process group child, capture descendant signal). Related: #8032 / PR #8043, about cleanup that kills a detached (`setsid`) descendant. That case is different, because there REA's cleanup sends the signal. I first mentioned this observation in a comment on #8043. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Documentation location `docs/process-capture.md`, the cleanup and settlement paragraphs ("Settlement reports whether the sampled process group quiesced or whether cleanup was needed or unverifiable; sampling cannot prove that every short-lived or detached descendant was seen.") and the `capture_process_scenario` tool description. ### Problem `capture_process_scenario` starts the target as the session leader of the capture PTY. When the target exits, the kernel sends SIGHUP to the PTY's foreground process group. A child that the target forked without `setsid` or `setpgid`, and that is still running, receives that SIGHUP. By default it dies a few milliseconds after the target exits. This isn't REA cleanup, and `cleanup_outcome: "not_required"` is accurate. But the capture then shows different behavior from running the program in an interactive terminal or without a terminal, where the child keeps running. Nothing in the result points to it: - `settlement` is `quiesced` with `cleanup_outcome: "not_required"`. - `process_samples` is empty. The child ended before any sample was taken. - No residual unknown or limitation mentions the hangup. A caller who captures a program that forks a background worker sees a clean exit with no descendants. They can't tell that the worker was started and then hung up by the capture setup. ### Expected content A short note in `docs/process-capture.md`, and possibly a residual unknown in the Evidence, saying: - the target runs as the session leader of the capture PTY, - when it exits, still-running processes in its foreground process group receive SIGHUP from the terminal, as with `script -c <program>` or `ssh -t host <program>`, - children that should outlive the target must leave the group, for example with `setsid` or by ignoring SIGHUP. This is a proposed clarification. Whether REA should instead keep the group alive, for example by running the target under a session-leader wrapper, is a design choice I'm not asking for. ### Supporting evidence I ran a self-written probe in a Linux x64 container (`--network none`), Node 24.22.1, on `rea-agents@6.8.0` and `main` at `4b961e371`. The parent exits at once. Its child stays in the parent's process group, ticks for 5 s, and logs any catchable signal. Signals are blocked across `fork`, so an early signal is still logged. ```c /* grpchild.c */ #include <signal.h> #include <stdio.h> #include <unistd.h> static FILE *log_file; static void on_sig(int s) { fprintf(log_file, "signal %d\n", s); fflush(log_file); _exit(0); } int main(void) { sigset_t all, old; sigfillset(&all); sigprocmask(SIG_BLOCK, &all, &old); log_file = fopen("grpchild.log", "w"); fputs("start\n", log_file); fflush(log_file); if (fork() == 0) { for (int s = 1; s < 32; s++) if (s != SIGKILL && s != SIGSTOP) signal(s, on_sig); fprintf(log_file, "child pid=%d pgid=%d\n", getpid(), getpgrp()); fflush(log_file); sigprocmask(SIG_SETMASK, &old, NULL); for (int i = 0; i < 10; i++) { usleep(500000); fprintf(log_file, "tick %d\n", i); fflush(log_file); } fputs("finished\n", log_file); fclose(log_file); return 0; } printf("parent-exit\n"); return 0; } ``` | How the probe runs | Child's log | |---|---| | `capture_process_scenario` `{"executable":"/tmp/grpchild","working_directory":"/tmp/pcg","timeout_ms":8000}`, 6.8.0 and `main` | `start`, `child pid=… pgid=<parent pid>`, `signal 1`. Evidence: `exit` code 0, `settlement.cleanup_outcome: "not_required"`, `process_samples: []`, no intervention. | | Directly, no terminal | ticks 0 to 9, `finished` | | `script -qec "sh -c '/tmp/grpchild; sleep 7'"`, where the shell is the PTY session leader | ticks 0 to 9, `finished` | | `script -qec /tmp/grpchild`, where the probe is the PTY session leader | `signal 1`, same as the capture | The last row shows the SIGHUP comes from the terminal, not from REA. ### Additional context I didn't test macOS. The hangup comes from POSIX session semantics, so it probably happens there too.
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#7542
No description provided.