Skip to content

tincand: attribute external SIGTERM/SIGINT senders, fix shutdown hang#178

Merged
quad341 merged 4 commits into
mainfrom
fix/tincan-97mlk.6-signal-attribution
Jul 5, 2026
Merged

tincand: attribute external SIGTERM/SIGINT senders, fix shutdown hang#178
quad341 merged 4 commits into
mainfrom
fix/tincan-97mlk.6-signal-attribution

Conversation

@quad341

@quad341 quad341 commented Jul 5, 2026

Copy link
Copy Markdown
Owner

What this changes

tincand previously handled SIGINT/SIGTERM with a plain signal.signal()
handler that logged only "SIGTERM received — shutting down" and quit the
GLib main loop. When the daemon received an external SIGTERM (e.g. a stray
systemctl stop or another process sending the signal) there was no way
to tell who sent it, and — in the case this was built to fix — the
shutdown path could hang entirely.

Signal handling now blocks SIGINT/SIGTERM process-wide via
signal.pthread_sigmask() as the second statement in main(), before any
thread is spawned, and a dedicated daemon thread consumes the signal with
signal.sigwaitinfo(). Unlike a signal.signal() handler, sigwaitinfo()
exposes the sender's pid/uid, so the shutdown log now includes a bounded
parent-process chain (e.g. pid=1234 cmdline='systemctl stop tincand' <- pid=1 cmdline='systemd') for attribution.

Why one PR

N/A — single-bead fix, one cohesive change (signal handling + its tests).

Review notes

  • packaging/systemd/tincand.service is intentionally untouched. The
    service file's own ExecStopPost attribution attempt was a dead end;
    the in-process sigwaitinfo approach replaces it entirely.
  • The attribution string is bounded (max_depth=4 ancestors, 200 chars per
    process, 1000 chars overall — roughly 1KB) specifically to avoid
    recreating the original bug: an unbounded WARNING log line filling a
    piped stdout/stderr and blocking the logging call itself, which is what
    caused the shutdown hang in the first place. loop.quit() runs in a
    finally around the attribution logging so shutdown always completes
    even if that logging path raises.
  • /proc reads (_proc_cmdline, _proc_ppid) are best-effort and
    tolerate the sender process already being gone by the time we read its
    entry — they fall back to <unavailable> rather than raising.

Test plan

  • Full suite: pytest tests/ -q — 2463 passed, 1 skipped, 0 failed
  • Targeted: pytest tests/tincand/test_main_signal_attribution.py tests/tincand/test_main.py tests/tincand/test_main_args.py -q — 60 passed
  • Live process-level check: sent a real SIGTERM to a throwaway process with attribution logging forced to raise — shutdown still completed (recreates the original hang's failure mode, not just the mocked unit test)
  • Release gate: release-gates/tincand-signal-attribution-97mlk.6-gate.md

quad341 and others added 4 commits July 5, 2026 10:50
…an-97mlk.6)

Block SIGINT/SIGTERM process-wide in main() via pthread_sigmask, then
attribute and consume them in a dedicated waiter thread via sigwaitinfo,
which (unlike signal.signal()) exposes si_pid/si_uid. Logs the sender
pid/uid and a bounded /proc parent-chain at WARNING before triggering
the existing loop.quit() shutdown.

The parent-chain lookup bounds depth (8 -> 4) and truncates each cmdline
(200 chars) and the overall string (1000 chars): an earlier unbounded
version produced a WARNING line large enough to fill a piped stderr that
nothing was draining, deadlocking the waiter thread inside logging's
stream.write() and hanging shutdown entirely (root-caused via py-spy;
was intermittently failing all 9 tests in test_dbus_client_live.py).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
…k.6.1)

Covers _block_shutdown_signals, _proc_cmdline, _proc_ppid,
_describe_signal_sender, _signal_waiter, _start_signal_waiter per the
tincan-97mlk.6.1 acceptance criteria. Verified against the real
implementation on builder/tincan-97mlk.6 (16 passed, 1 xfailed pending
a reviewer call on quit()'s exception-resilience); fails to collect on
this branch until tincan-97mlk.6 merges, which is expected.
…g (tincan-97mlk.6.2)

Addresses reviewer REQUEST-CHANGES on tincan-97mlk.6: _signal_waiter called
loop.quit() unconditionally after the WARNING attribution log with no
try/except, so an exception in that path (e.g. a malicious cmdline value
tripping _proc_ppid's strict decode) skipped quit() and hung daemon shutdown
entirely — the same class of bug tincan-97mlk.6 was meant to fix. Moves
loop.quit() into a finally so it always runs. Also aligns _proc_ppid's
/proc/<pid>/status read with _proc_cmdline's existing errors="replace"
pattern instead of a strict decode.

Un-xfails test_quit_still_called_if_warning_log_raises (tincan-97mlk.6.1)
now that it passes, and loosens two read_text mocks in the same file to
accept the errors= kwarg the real call now passes.

adr_check: skipped — actual CLI not installed
@quad341
quad341 merged commit 7f34ad6 into main Jul 5, 2026
1 check passed
@quad341
quad341 deleted the fix/tincan-97mlk.6-signal-attribution branch July 5, 2026 19:14
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