Skip to content

Route thread-directed signals to the target thread#153

Open
jserv wants to merge 1 commit into
mainfrom
tgkill
Open

Route thread-directed signals to the target thread#153
jserv wants to merge 1 commit into
mainfrom
tgkill

Conversation

@jserv

@jserv jserv commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

{tg,t}kill, rt_tgsigqueueinfo, and thus pthread_kill validated the target tid then queued the signal into the single process-wide pending set, so whichever thread reached a delivery point first consumed it and standard signals coalesced across threads. Go async preemption, pthread_cancel, and glibc setxid / musl __synccall all interrupted the wrong thread; interruptible blocking I/O made the gap load-bearing.

Model Linux's 2 pending sets. Extract signal_pending_t (pending bitmask, coalesced standard siginfo, per-signal RT queues); signal_state now holds one shared instance (signal->shared_pending, hit by kill) and each thread_entry_t holds its own private tpending (task->pending, hit by tgkill/tkill). signal_deliver drains the current thread's private set before the shared set, matching dequeue_signal; signal_pending, sigsuspend, rt_sigprocmask unblock, and rt_sigpending union the two.

Resolve the target under thread_lock and enqueue into its tpending under sig_lock then thread_lock so a concurrent exit cannot misroute; kill and rt_sigqueueinfo stay process-directed. Add tkill (syscall 130), which was ENOSYS. Both directed senders validate tgid/tid and treat sig 0 as an existence probe.

signalfd reads the caller's private set then the shared set with a per-entry source tag so peek and take consume from the same set; a queued directed signal wakes signalfd waiters (Linux wakes the shared sighand queue) and a blocking read re-waits on a false wake. The pending hint becomes shared.pending OR the union of every active thread's private set, recomputed on thread exit so a departed thread's unconsumed directed signal stops pinning the identity-syscall fast path off.

Close #152


Summary by cubic

Route thread-directed signals to the exact target thread and match Linux delivery semantics. Adds SYS_tkill, corrects tgkill/rt_tgsigqueueinfo routing, and hardens signalfd to handle per-thread vs shared pending sets.

  • New Features

    • Two pending sets: shared (process-directed) and per-thread (tpending). Delivery drains the thread’s private set before the shared set; signal_pending, rt_sigpending, rt_sigsuspend, and rt_sigprocmask union the two.
    • Add SYS_tkill (130). Route tgkill/rt_tgsigqueueinfo to the target thread with tgid validation; treat sig==0 as an existence probe. Keep kill/rt_sigqueueinfo process-directed and accept a thread TID as the PID; also honor sig==0.
    • signalfd: read private then shared with a per-entry source tag so take consumes from the same set; re-wait on false wakes; wake signalfd on directed signals so target readers unblock while non-targets see EAGAIN.
  • Bug Fixes

    • Fix thread-directed signals being handled by the wrong thread and stop standard-signal coalescing across threads.
    • Resolve target under thread_lock and enqueue into its private set under sig_lockthread_lock to avoid misrouting during concurrent thread exit.
    • Recompute the global pending hint as (shared.pending OR union of all threads’ private masks) and refresh on thread slot deactivation/exit so stale bits do not pin fast paths off.
    • Tests: add test-tgkill-directed and a signalfd case ensuring a directed signal wakes the target’s signalfd.

Written for commit 217bb85. Summary will update on new commits.

Review in cubic

@jserv jserv requested a review from Max042004 July 5, 2026 20:19
cubic-dev-ai[bot]

This comment was marked as resolved.

{tg,t}kill, rt_tgsigqueueinfo, and thus pthread_kill validated the target
tid then queued the signal into the single process-wide pending set, so
whichever thread reached a delivery point first consumed it and standard
signals coalesced across threads. Go async preemption, pthread_cancel,
and glibc setxid / musl __synccall all interrupted the wrong thread;
interruptible blocking I/O made the gap load-bearing.

Model Linux's 2 pending sets. Extract signal_pending_t (pending bitmask,
coalesced standard siginfo, per-signal RT queues); signal_state now holds
one shared instance (signal->shared_pending, hit by kill) and each
thread_entry_t holds its own private tpending (task->pending, hit by
tgkill/tkill). signal_deliver drains the current thread's private set
before the shared set, matching dequeue_signal; signal_pending,
sigsuspend, rt_sigprocmask unblock, and rt_sigpending union the two.

Resolve the target under thread_lock and enqueue into its tpending under
sig_lock then thread_lock so a concurrent exit cannot misroute; kill and
rt_sigqueueinfo stay process-directed. Add tkill (syscall 130), which was
ENOSYS. Both directed senders validate tgid/tid and treat sig 0 as an
existence probe.

signalfd reads the caller's private set then the shared set with a
per-entry source tag so peek and take consume from the same set; a queued
directed signal wakes signalfd waiters (Linux wakes the shared sighand
queue) and a blocking read re-waits on a false wake. The pending hint
becomes shared.pending OR the union of every active thread's private set,
recomputed on thread exit so a departed thread's unconsumed directed
signal stops pinning the identity-syscall fast path off.

Close #152
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.

tgkill discards the target thread: thread-directed signals are delivered process-wide

1 participant