ptrace,signal: group stop / ptrace updates, take#2
Subsystems (5)
PATCH 1/1 ptrace,signal: group stop / ptrace updates, take#2
Hello,
This is yet another posting of ptrace and group stop interaction updates. Patches themselves remain mostly unchanged from the previous posting[1] other than additions of Acked-by's and rebasing on top of the current mainline.
The biggest change is that we now, hopefully, have an agreeable plan as laid out by the proposal for ptrace improvements[2] and the discussion which followed. Naturally, not everyone is ecstatic about it but I think there's enough level of agreement to proceed.
This patchset contains the following ten patches.
0001-signal-fix-SIGCONT-notification-code.patch 0002-ptrace-remove-the-extra-wake_up_state-from-ptrace_de.patch 0003-signal-remove-superflous-try_to_freeze-loop-in-do_si.patch 0004-ptrace-kill-tracehook_notify_jctl.patch 0005-ptrace-add-why-to-ptrace_stop.patch 0006-signal-fix-premature-completion-of-group-stop-when-i.patch 0007-signal-use-GROUP_STOP_PENDING-to-stop-once-for-a-sin.patch 0008-ptrace-participate-in-group-stop-from-ptrace_stop-if.patch 0009-ptrace-make-do_signal_stop-use-ptrace_stop-if-the-ta.patch 0010-ptrace-clean-transitions-between-TASK_STOPPED-and-TR.patch
In light of the proposal, this patchset can be separated into two parts. The first part, patches 0001 through 0006, tightens various loose ends and prepare for further changes. Except for 0004, every patch is acked by Oleg and/or Roland. 0004 doesn't cause any behavior change and removes a tracehook for future changes.
The second part, 0007-0010, fixes group stop participation accounting and make tracees always stop in TASK_TRACED instead of TASK_STOPPED. IOW, it implements "P1. Always TASK_TRACED while ptraced" of the proposal.
Roland, Oleg, Andrew, if there's no objection, I'd like to set up a git tree to route further developments in the area. We're going to see a lot of patches and it's simply much more efficient to have a dedicated stable git branch. A lot of changes which got folded into the posted patches after Oleg's review should have been separate commits rather than growing list of starred items in the patch descriptions.
I don't really care who controls the tree and don't intend to commit any patch without Oleg's Acked-by so it would make most sense for Oleg to control the branch but, if Oleg dosnn't want to, I'd be happy to do it. No matter who does it, let's _PLEASE_ establish a stable tree.
This patchset is available in the following git branch.
git://git.kernel.org/pub/scm/linux/kernel/git/tj/misc.git ptrace-review
HEAD is 31578b14d460dcac2946eff35c358ab1bebf2d7d. git.korg takes some time to sync so if it shows older commit, please try again after a while. diffstat follows.
fs/exec.c | 1 include/linux/sched.h | 11 ++ include/linux/tracehook.h | 27 ----- kernel/ptrace.c | 51 ++++++++-- kernel/signal.c | 226 ++++++++++++++++++++++++++++++++++------------ 5 files changed, 225 insertions(+), 91 deletions(-)
Thanks.
--
tejun
[1] http://thread.gmane.org/gmane.linux.kernel/1093410
[2] http://thread.gmane.org/gmane.linux.kernel/1107045
PATCH 1/10 signal: fix SIGCONT notification code
After a task receives SIGCONT, its parent is notified via SIGCHLD with its siginfo describing what the notified event is. If SIGCONT is received while the child process is stopped, the code should be CLD_CONTINUED. If SIGCONT is recieved while the child process is in the process of being stopped, it should be CLD_STOPPED. Which code to use is determined in prepare_signal() and recorded in signal->flags using SIGNAL_CLD_CONTINUED|STOP flags.
get_signal_deliver() should test these flags and then notify accoringly; however, it incorrectly tested SIGNAL_STOP_CONTINUED instead of SIGNAL_CLD_CONTINUED, thus incorrectly notifying CLD_CONTINUED if the signal is delivered before the task is wait(2)ed and CLD_STOPPED if the state was fetched already.
Fix it by testing SIGNAL_CLD_CONTINUED. While at it, uncompress the ?: test into if/else clause for better readability.
--- kernel/signal.c | 9 +++++++-- 1 files changed, 7 insertions(+), 2 deletions(-)
kernel/signal.c +7 -2
@@ -1853,8 +1853,13 @@ relock:* the CLD_ si_code into SIGNAL_CLD_MASK bits. */ if (unlikely (signal->flags &SIGNAL_CLD_MASK )) { -int why = (signal->flags &SIGNAL_STOP_CONTINUED ) - ?CLD_CONTINUED :CLD_STOPPED ; +int why; + +if (signal->flags &SIGNAL_CLD_CONTINUED ) + why =CLD_CONTINUED ; +else + why =CLD_STOPPED ; + signal->flags &= ~SIGNAL_CLD_MASK ; why =tracehook_notify_jctl (why,CLD_CONTINUED );
Signature · 1 line
1.7.1
PATCH 2/10 ptrace: remove the extra wake_up_state() from ptrace_detach()
In reply to Tejun HeoThis wake_up_state() has a turbulent history. This is a remnant from ancient ptrace implementation and patently wrong. Commit 95a3540d (ptrace_detach: the wrong wakeup breaks the ERESTARTxxx logic) removed it but the change was reverted later by commit edaba2c5 (ptrace:
revert "ptrace_detach: the wrong wakeup breaks the ERESTARTxxx logic") citing compatibility breakage and general brokeness of the whole group stop / ptrace interaction. Then, recently, it got converted from wake_up_process() to wake_up_state() to make it less dangerous.
Digging through the mailing archives, the compatibility breakage doesn't seem to be critical in the sense that the behavior isn't well defined or reliable to begin with and it seems to have been agreed to remove the wakeup with proper cleanup of the whole thing.
Now that the group stop and its interaction with ptrace are being cleaned up, it's high time to finally kill this silliness.
--- kernel/ptrace.c | 2 -- 1 files changed, 0 insertions(+), 2 deletions(-)
kernel/ptrace.c +0 -2
@@ -312,8 +312,6 @@ static int ptrace_detach(struct task_struct *child, unsigned int data)if (child->ptrace) { child->exit_code = data; dead =__ptrace_detach (current, child); -if (!child->exit_state) -wake_up_state (child,TASK_TRACED |TASK_STOPPED ); }write_unlock_irq (&tasklist_lock);
Signature · 1 line
1.7.1
PATCH 3/10 signal: remove superflous try_to_freeze() loop in do_signal_stop()
In reply to Tejun Heodo_signal_stop() is used only by get_signal_to_deliver() and after a successful signal stop, it always calls try_to_freeze(), so the try_to_freeze() loop around schedule() in do_signal_stop() is superflous and confusing. Remove it.
--- kernel/signal.c | 4 +--- 1 files changed, 1 insertions(+), 3 deletions(-)
kernel/signal.c +1 -3
@@ -1781,9 +1781,7 @@ static int do_signal_stop(int signr) }/* Now we don't run again until woken by SIGCONT or SIGKILL */ -do { -schedule (); - }while (try_to_freeze ()); +schedule ();tracehook_finish_jctl (); current->exit_code =0 ;
Signature · 1 line
1.7.1
PATCH 4/10 ptrace: kill tracehook_notify_jctl()
In reply to Tejun Heotracehook_notify_jctl() aids in determining whether and what to report to the parent when a task is stopped or continued. The function also adds an extra requirement that siglock may be released across it, which is currently unused and quite difficult to satisfy in well-defined manner.
As job control and the notifications are about to receive major overhaul, remove the tracehook and open code it. If ever necessary, let's factor it out after the overhaul.
* Oleg spotted incorrect CLD_CONTINUED/STOPPED selection when ptraced. Fixed.
--- include/linux/tracehook.h | 27 --------------------------- kernel/signal.c | 34 ++++++++++++++-------------------- 2 files changed, 14 insertions(+), 47 deletions(-)
include/linux/tracehook.h +0 -27
@@ -469,33 +469,6 @@ static inline int tracehook_get_signal(struct task_struct *task, }/** -* tracehook_notify_jctl - report about job control stop/continue -* @notify: zero, %CLD_STOPPED or %CLD_CONTINUED -* @why: %CLD_STOPPED or %CLD_CONTINUED -* -* This is called when we might call do_notify_parent_cldstop(). -* -* @notify is zero if we would not ordinarily send a %SIGCHLD, -* or is the %CLD_STOPPED or %CLD_CONTINUED .si_code for %SIGCHLD. -* -* @why is %CLD_STOPPED when about to stop for job control; -* we are already in %TASK_STOPPED state, about to call schedule(). -* It might also be that we have just exited (check %PF_EXITING), -* but need to report that a group-wide stop is complete. -* -* @why is %CLD_CONTINUED when waking up after job control stop and -* ready to make a delayed @notify report. -* -* Return the %CLD_* value for %SIGCHLD, or zero to generate no signal. -* -* Called with the siglock held. -*/ -static inline int tracehook_notify_jctl (int notify,int why) -{ -return notify ?: (current->ptrace &PT_PTRACED ) ? why :0 ; -} - -/** * tracehook_finish_jctl - report about return from job control stop * * This is called by do_signal_stop() after wakeup.
kernel/signal.c +14 -20
@@ -1727,7 +1727,7 @@ void ptrace_notify(int exit_code)static int do_signal_stop (int signr) {struct signal_struct *sig = current->signal; -int notify; +int notify =0 ;if (!sig->group_stop_count) {struct task_struct *t;
@@ -1759,19 +1759,16 @@ static int do_signal_stop(int signr)* a group stop in progress and we are the last to stop, report * to the parent. When ptraced, every thread reports itself. */ - notify = sig->group_stop_count ==1 ?CLD_STOPPED :0 ; - notify =tracehook_notify_jctl (notify,CLD_STOPPED ); -/* -* tracehook_notify_jctl() can drop and reacquire siglock, so -* we keep ->group_stop_count != 0 before the call. If SIGCONT -* or SIGKILL comes in between ->group_stop_count == 0. -*/ -if (sig->group_stop_count) { -if (!--sig->group_stop_count) - sig->flags =SIGNAL_STOP_STOPPED ; - current->exit_code = sig->group_exit_code; -__set_current_state (TASK_STOPPED ); +if (!--sig->group_stop_count) { + sig->flags =SIGNAL_STOP_STOPPED ; + notify =CLD_STOPPED ; } +if (task_ptrace (current)) + notify =CLD_STOPPED ; + + current->exit_code = sig->group_exit_code; +__set_current_state (TASK_STOPPED ); +spin_unlock_irq (¤t->sighand->siglock);if (notify) {
@@ -1860,14 +1857,11 @@ relock: signal->flags &= ~SIGNAL_CLD_MASK ; - why =tracehook_notify_jctl (why,CLD_CONTINUED );spin_unlock_irq (&sighand->siglock); -if (why) { -read_lock (&tasklist_lock); -do_notify_parent_cldstop (current->group_leader, why); -read_unlock (&tasklist_lock); - } +read_lock (&tasklist_lock); +do_notify_parent_cldstop (current->group_leader, why); +read_unlock (&tasklist_lock); goto relock; }
@@ -2034,7 +2028,7 @@ void exit_signals(struct task_struct *tsk)if (unlikely (tsk->signal->group_stop_count) && !--tsk->signal->group_stop_count) { tsk->signal->flags =SIGNAL_STOP_STOPPED ; - group_stop =tracehook_notify_jctl (CLD_STOPPED ,CLD_STOPPED ); + group_stop =CLD_STOPPED ; } out:spin_unlock_irq (&tsk->sighand->siglock);
Signature · 1 line
1.7.1
PATCH 5/10 ptrace: add @why to ptrace_stop()
In reply to Tejun HeoTo prepare for cleanup of the interaction between group stop and ptrace, add @why to ptrace_stop(). Existing users are updated such that there is no behavior change.
--- kernel/signal.c | 8 ++++---- 1 files changed, 4 insertions(+), 4 deletions(-)
kernel/signal.c +4 -4
@@ -1617,7 +1617,7 @@ static int sigkill_pending(struct task_struct *tsk)* If we actually decide not to stop at all because the tracer * is gone, we keep current->exit_code unless clear_code. */ -static void ptrace_stop (int exit_code,int clear_code,siginfo_t *info) +static void ptrace_stop (int exit_code,int why,int clear_code,siginfo_t *info) __releases(¤t->sighand->siglock) __acquires(¤t->sighand->siglock) {
@@ -1655,7 +1655,7 @@ static void ptrace_stop(int exit_code, int clear_code, siginfo_t *info)spin_unlock_irq (¤t->sighand->siglock);read_lock (&tasklist_lock);if (may_ptrace_stop ()) { -do_notify_parent_cldstop (current,CLD_TRAPPED ); +do_notify_parent_cldstop (current, why);/* * Don't want to allow preemption here, because * sys_ptrace() needs this task to be inactive.
@@ -1714,7 +1714,7 @@ void ptrace_notify(int exit_code)/* Let the debugger run. */ spin_lock_irq (¤t->sighand->siglock); -ptrace_stop (exit_code,1 , &info); +ptrace_stop (exit_code,CLD_TRAPPED ,1 , &info);spin_unlock_irq (¤t->sighand->siglock); }
@@ -1795,7 +1795,7 @@ static int ptrace_signal(int signr, siginfo_t *info,ptrace_signal_deliver (regs, cookie);/* Let the debugger run. */ -ptrace_stop (signr,0 , info); +ptrace_stop (signr,CLD_TRAPPED ,0 , info);/* We're back. Did the debugger cancel the sig? */ signr = current->exit_code;
Signature · 1 line
1.7.1
PATCH 6/10 signal: fix premature completion of group stop when interfered by ptrace
In reply to Tejun Heotask->signal->group_stop_count is used to track the progress of group stop. It's initialized to the number of tasks which need to stop for group stop to finish and each stopping or trapping task decrements. However, each task doesn't keep track of whether it decremented the counter or not and if woken up before the group stop is complete and stops again, it can decrement the counter multiple times.
Please consider the following example code.
static void *worker(void *arg)
{
while (1) ;
return NULL;}
int main(void)
{
pthread_t thread;
pid_t pid;
int i;
pid = fork();
if (!pid) {
for (i = 0; i < 5; i++)
pthread_create(&thread, NULL, worker, NULL);
while (1) ;
return 0;
}
ptrace(PTRACE_ATTACH, pid, NULL, NULL);
while (1) {
waitid(P_PID, pid, NULL, WSTOPPED);
ptrace(PTRACE_SINGLESTEP, pid, NULL, (void *)(long)SIGSTOP);
}
return 0;}
The child creates five threads and the parent continuously traps the first thread and whenever the child gets a signal, SIGSTOP is delivered. If an external process sends SIGSTOP to the child, all other threads in the process should reliably stop. However, due to the above bug, the first thread will often end up consuming group_stop_count multiple times and SIGSTOP often ends up stopping none or part of the other four threads.
This patch adds a new field task->group_stop which is protected by siglock and uses GROUP_STOP_CONSUME flag to track which task is still to consume group_stop_count to fix this bug.
task_clear_group_stop_pending() and task_participate_group_stop() are added to help manipulating group stop states. As ptrace_stop() now also uses task_participate_group_stop(), it will set SIGNAL_STOP_STOPPED if it completes a group stop.
There still are many issues regarding the interaction between group stop and ptrace. Patches to address them will follow.
- Oleg spotted duplicate GROUP_STOP_CONSUME. Dropped.
--- include/linux/sched.h | 6 ++++ kernel/signal.c | 62 ++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 60 insertions(+), 8 deletions(-)
include/linux/sched.h +6 -0
@@ -1261,6 +1261,7 @@ struct task_struct {int exit_state;int exit_code, exit_signal;int pdeath_signal;/* The signal sent when the parent dies */ +unsigned int group_stop;/* GROUP_STOP_*, siglock protected */ /* ??? */ unsigned int personality; unsigned did_exec:1;
@@ -1772,6 +1773,11 @@ extern void thread_group_times(struct task_struct *p, cputime_t *ut, cputime_t *#define tsk_used_math (p) ((p)->flags & PF_USED_MATH)#define used_math () tsk_used_math(current) +/* +* task->group_stop flags +*/ +#define GROUP_STOP_CONSUME (1 << 17)/* consume group stop count */ +#ifdef CONFIG_PREEMPT_RCU #define RCU_READ_UNLOCK_BLOCKED (1 << 0)/* blocked while in RCU read-side. */
kernel/signal.c +54 -8
108 lines not on this page · raw message
Signature · 1 line
1.7.1
PATCH 7/10 signal: use GROUP_STOP_PENDING to stop once for a single group stop
In reply to Tejun HeoCurrently task->signal->group_stop_count is used to decide whether to stop for group stop. However, if there is a task in the group which is taking a long time to stop, other tasks which are continued by ptrace would repeatedly stop for the same group stop until the group stop is complete.
Conversely, if a ptraced task is in TASK_TRACED state, the debugger won't get notified of group stops which is inconsistent compared to the ptraced task in any other state.
This patch introduces GROUP_STOP_PENDING which tracks whether a task is yet to stop for the group stop in progress. The flag is set when a group stop starts and cleared when the task stops the first time for the group stop, and consulted whenever whether the task should participate in a group stop needs to be determined. Note that now tasks in TASK_TRACED also participate in group stop.
This results in the following behavior changes.
* For a single group stop, a ptracer would see at most one stop reported.
* A ptracee in TASK_TRACED now also participates in group stop and the tracer would get the notification. However, as a ptraced task could be in TASK_STOPPED state or any ptrace trap could consume group stop, the notification may still be missing. These will be addressed with further patches.
* A ptracee may start a group stop while one is still in progress if the tracer let it continue with stop signal delivery. Group stop code handles this correctly.
Oleg:
* Spotted that a task might skip signal check even when its GROUP_STOP_PENDING is set. Fixed by updating recalc_sigpending_tsk() to check GROUP_STOP_PENDING instead of group_stop_count.
* Pointed out that task->group_stop should be cleared whenever task->signal->group_stop_count is cleared. Fixed accordingly.
* Pointed out the behavior inconsistency between TASK_TRACED and RUNNING and the last behavior change.
--- fs/exec.c | 1 + include/linux/sched.h | 3 +++ kernel/signal.c | 36 +++++++++++++++++++++--------------- 3 files changed, 25 insertions(+), 15 deletions(-)
fs/exec.c +1 -0
@@ -1653,6 +1653,7 @@ static int zap_process(struct task_struct *start, int exit_code) t = start; do { + task_clear_group_stop_pending(t); if (t != current && t->mm) { sigaddset(&t->pending.signal, SIGKILL); signal_wake_up(t, 1);
include/linux/sched.h +3 -0
@@ -1776,8 +1776,11 @@ extern void thread_group_times(struct task_struct *p, cputime_t *ut, cputime_t */* * task->group_stop flags */ +#define GROUP_STOP_PENDING (1 << 16)/* task should stop for group stop */ #define GROUP_STOP_CONSUME (1 << 17)/* consume group stop count */ +extern void task_clear_group_stop_pending (struct task_struct *task); +#ifdef CONFIG_PREEMPT_RCU #define RCU_READ_UNLOCK_BLOCKED (1 << 0)/* blocked while in RCU read-side. */
kernel/signal.c +21 -15
@@ -124,7 +124,7 @@ static inline int has_pending_signals(sigset_t *signal, sigset_t *blocked)static int recalc_sigpending_tsk (struct task_struct *t) { -if (t->signal->group_stop_count >0 || +if ((t->group_stop &GROUP_STOP_PENDING ) ||PENDING (&t->pending, &t->blocked) ||PENDING (&t->signal->shared_pending, &t->blocked)) {set_tsk_thread_flag (t,TIF_SIGPENDING );
@@ -232,19 +232,19 @@ static inline void print_dropped_signal(int sig)* CONTEXT: * Must be called with @task->sighand->siglock held. */ -static void task_clear_group_stop_pending (struct task_struct *task) +void task_clear_group_stop_pending (struct task_struct *task) { - task->group_stop &= ~GROUP_STOP_CONSUME ; + task->group_stop &= ~(GROUP_STOP_PENDING |GROUP_STOP_CONSUME ); }/** * task_participate_group_stop - participate in a group stop * @task: task participating in a group stop * -* @task is participating in a group stop. Group stop states are cleared -* and the group stop count is consumed if %GROUP_STOP_CONSUME was set. If -* the consumption completes the group stop, the appropriate %SIGNAL_* -* flags are set. +* @task has GROUP_STOP_PENDING set and is participating in a group stop. +* Group stop states are cleared and the group stop count is consumed if +* %GROUP_STOP_CONSUME was set. If the consumption completes the group +* stop, the appropriate %SIGNAL_* flags are set. * * CONTEXT: * Must be called with @task->sighand->siglock held.
@@ -254,6 +254,8 @@ static bool task_participate_group_stop(struct task_struct *task)struct signal_struct *sig = task->signal;bool consume = task->group_stop &GROUP_STOP_CONSUME ; +WARN_ON_ONCE (!(task->group_stop &GROUP_STOP_PENDING )); +task_clear_group_stop_pending (task);if (!consume)
@@ -765,6 +767,9 @@ static int prepare_signal(int sig, struct task_struct *p, int from_ancestor_ns) t = p; do { unsigned int state; + + task_clear_group_stop_pending(t); + rm_from_queue(SIG_KERNEL_STOP_MASK, &t->pending); /* * If there is a handler for SIGCONT, we must make
@@ -906,6 +911,7 @@ static void complete_signal(int sig, struct task_struct *p, int group) signal->group_stop_count =0 ; t = p; do { + task_clear_group_stop_pending(t); sigaddset(&t->pending.signal, SIGKILL); signal_wake_up(t, 1); } while_each_thread(p, t);
@@ -1139,6 +1145,7 @@ int zap_other_threads(struct task_struct *p) p->signal->group_stop_count =0 ; while_each_thread(p, t) { +task_clear_group_stop_pending (t); count++;/* Don't bother with already dead threads */
@@ -1690,7 +1697,7 @@ static void ptrace_stop(int exit_code, int why, int clear_code, siginfo_t *info)* If there is a group stop in progress, * we must participate in the bookkeeping. */ -if (current->signal->group_stop_count >0 ) +if (current->group_stop &GROUP_STOP_PENDING )task_participate_group_stop (current); current->last_siginfo = info;
@@ -1775,8 +1782,8 @@ static int do_signal_stop(int signr)struct signal_struct *sig = current->signal;int notify =0 ; -if (!sig->group_stop_count) { -unsigned int gstop =GROUP_STOP_CONSUME ; +if (!(current->group_stop &GROUP_STOP_PENDING )) { +unsigned int gstop =GROUP_STOP_PENDING |GROUP_STOP_CONSUME ;struct task_struct *t; if (!likely(sig->flags & SIGNAL_STOP_DEQUEUED) ||
@@ -1796,8 +1803,7 @@ static int do_signal_stop(int signr)* stop is always done with the siglock held, * so this check has no races. */ -if (!(t->flags &PF_EXITING ) && - !task_is_stopped_or_traced (t)) { +if (!(t->flags &PF_EXITING ) && !task_is_stopped (t)) { t->group_stop = gstop; sig->group_stop_count++;signal_wake_up (t,0 );
@@ -1926,8 +1932,8 @@ relock:if (unlikely (signr !=0 )) ka = return_ka;else { -if (unlikely (signal->group_stop_count >0 ) && -do_signal_stop (0 )) +if (unlikely (current->group_stop & +GROUP_STOP_PENDING ) &&do_signal_stop (0 )) goto relock; signr = dequeue_signal(current, ¤t->blocked,
@@ -2073,7 +2079,7 @@ void exit_signals(struct task_struct *tsk)if (!signal_pending (t) && !(t->flags &PF_EXITING ))recalc_sigpending_and_wake (t); -if (unlikely (tsk->signal->group_stop_count) && +if (unlikely (tsk->group_stop &GROUP_STOP_PENDING ) &&task_participate_group_stop (tsk)) group_stop =CLD_STOPPED ; out:
Signature · 1 line
1.7.1
PATCH 8/10 ptrace: participate in group stop from ptrace_stop() iff the task is trapping for group stop
In reply to Tejun HeoCurrently, ptrace_stop() unconditionally participates in group stop bookkeeping. This is unnecessary and inaccurate. Make it only participate if the task is trapping for group stop - ie. if @why is CLD_STOPPED. As ptrace_stop() currently is not used when trapping for group stop, this equals to disabling group stop participation from ptrace_stop().
A visible behavior change is increased likelihood of delayed group stop completion if the thread group contains one or more ptraced tasks.
This is to preapre for further cleanup of the interaction between group stop and ptrace.
--- kernel/signal.c | 9 ++++++--- 1 files changed, 6 insertions(+), 3 deletions(-)
kernel/signal.c +6 -3
@@ -1694,10 +1694,13 @@ static void ptrace_stop(int exit_code, int why, int clear_code, siginfo_t *info) }/* -* If there is a group stop in progress, -* we must participate in the bookkeeping. +* If @why is CLD_STOPPED, we're trapping to participate in a group +* stop. Do the bookkeeping. Note that if SIGCONT was delievered +* while siglock was released for the arch hook, PENDING could be +* clear now. We act as if SIGCONT is received after TASK_TRACED +* is entered - ignore it. */ -if (current->group_stop &GROUP_STOP_PENDING ) +if (why ==CLD_STOPPED && (current->group_stop &GROUP_STOP_PENDING ))task_participate_group_stop (current); current->last_siginfo = info;
Signature · 1 line
1.7.1
PATCH 9/10 ptrace: make do_signal_stop() use ptrace_stop() if the task is being ptraced
In reply to Tejun HeoA ptraced task would still stop at do_signal_stop() when it's stopping for stop signals and do_signal_stop() behaves the same whether the task is ptraced or not. However, in addition to stopping, ptrace_stop() also does ptrace specific stuff like calling architecture specific callbacks, so this behavior makes the code more fragile and difficult to understand.
This patch makes do_signal_stop() test whether the task is ptraced and use ptrace_stop() if so. This renders tracehook_notify_jctl() rather pointless as the ptrace notification is now handled by ptrace_stop() regardless of the return value from the tracehook. It probably is a good idea to update it.
This doesn't solve the whole problem as tasks already in stopped state would stay in the regular stop when ptrace attached. That part will be handled by the next patch.
Oleg pointed out that this makes a userland-visible change. Before, SIGCONT would be able to wake up a task in group stop even if the task is ptraced if the tracer hasn't issued another ptrace command afterwards (as the next ptrace commands transitions the state into TASK_TRACED which ignores SIGCONT wakeups). With this and the next patch, SIGCONT may race with the transition into TASK_TRACED and is ignored if the tracee already entered TASK_TRACED.
Another userland visible change of this and the next patch is that the ptracee's state would now be TASK_TRACED where it used to be TASK_STOPPED, which is visible via fs/proc.
--- kernel/signal.c | 43 +++++++++++++++++++++++++------------------ 1 files changed, 25 insertions(+), 18 deletions(-)
kernel/signal.c +25 -18
@@ -1783,7 +1783,6 @@ void ptrace_notify(int exit_code)static int do_signal_stop (int signr) {struct signal_struct *sig = current->signal; -int notify =0 ;if (!(current->group_stop &GROUP_STOP_PENDING )) {unsigned int gstop =GROUP_STOP_PENDING |GROUP_STOP_CONSUME ;
@@ -1813,29 +1812,37 @@ static int do_signal_stop(int signr) }else task_clear_group_stop_pending (t); } -/* -* If there are no other threads in the group, or if there is -* a group stop in progress and we are the last to stop, report -* to the parent. When ptraced, every thread reports itself. -*/ -if (task_participate_group_stop (current)) - notify =CLD_STOPPED ; -if (task_ptrace (current)) - notify =CLD_STOPPED ; current->exit_code = sig->group_exit_code;__set_current_state (TASK_STOPPED ); -spin_unlock_irq (¤t->sighand->siglock); +if (likely (!task_ptrace (current))) { +int notify =0 ; -if (notify) { -read_lock (&tasklist_lock); -do_notify_parent_cldstop (current, notify); -read_unlock (&tasklist_lock); - } +/* +* If there are no other threads in the group, or if there +* is a group stop in progress and we are the last to stop, +* report to the parent. +*/ +if (task_participate_group_stop (current)) + notify =CLD_STOPPED ; -/* Now we don't run again until woken by SIGCONT or SIGKILL */ -schedule (); +spin_unlock_irq (¤t->sighand->siglock); + +if (notify) { +read_lock (&tasklist_lock); +do_notify_parent_cldstop (current, notify); +read_unlock (&tasklist_lock); + } + +/* Now we don't run again until woken by SIGCONT or SIGKILL */ +schedule (); + +spin_lock_irq (¤t->sighand->siglock); + }else +ptrace_stop (current->exit_code,CLD_STOPPED ,0 ,NULL ); + +spin_unlock_irq (¤t->sighand->siglock);tracehook_finish_jctl (); current->exit_code =0 ;
Signature · 1 line
1.7.1
PATCH 10/10 ptrace: clean transitions between TASK_STOPPED and TRACED
In reply to Tejun HeoCurrently, if the task is STOPPED on ptrace attach, it's left alone and the state is silently changed to TRACED on the next ptrace call. The behavior breaks the assumption that arch_ptrace_stop() is called before any task is poked by ptrace and is ugly in that a task manipulates the state of another task directly.
With GROUP_STOP_PENDING, the transitions between TASK_STOPPED and TRACED can be made clean. The tracer can use the flag to tell the tracee to retry stop on attach and detach. On retry, the tracee will enter the desired state in the correct way. The lower 16bits of task->group_stop is used to remember the signal number which caused the last group stop. This is used while retrying for ptrace attach as the original group_exit_code could have been consumed with wait(2) by then.
As the real parent may wait(2) and consume the group_exit_code anytime, the group_exit_code needs to be saved separately so that it can be used when switching from regular sleep to ptrace_stop(). This is recorded in the lower 16bits of task->group_stop.
If a task is already stopped and there's no intervening SIGCONT, a ptrace request immediately following a successful PTRACE_ATTACH should always succeed even if the tracer doesn't wait(2) for attach completion; however, with this change, the tracee might still be TASK_RUNNING trying to enter TASK_TRACED which would cause the following request to fail with -ESRCH.
This intermediate state is hidden from the ptracer by setting GROUP_STOP_TRAPPING on attach and making ptrace_check_attach() wait for it to clear on its signal->wait_chldexit. Completing the transition or getting killed clears TRAPPING and wakes up the tracer.
Note that the STOPPED -> RUNNING -> TRACED transition is still visible to other threads which are in the same group as the ptracer and the reverse transition is visible to all. Please read the comments for details.
Oleg:
* Spotted a race condition where a task may retry group stop without proper bookkeeping. Fixed by redoing bookkeeping on retry.
* Spotted that the transition is visible to userland in several different ways. Most are fixed with GROUP_STOP_TRAPPING. Unhandled corner case is documented.
* Pointed out not setting GROUP_STOP_SIGMASK on an already stopped task would result in more consistent behavior.
* Pointed out that calling ptrace_stop() from do_signal_stop() in TASK_STOPPED can race with group stop start logic and then confuse the TRAPPING wait in ptrace_check_attach(). ptrace_stop() is now called with TASK_RUNNING.
* Suggested using signal->wait_chldexit instead of bit wait.
* Spotted a race condition between TRACED transition and clearing of TRAPPING.
--- include/linux/sched.h | 2 + kernel/ptrace.c | 49 +++++++++++++++++++++++++++--- kernel/signal.c | 79 +++++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 112 insertions(+), 18 deletions(-)
include/linux/sched.h +2 -0
@@ -1776,8 +1776,10 @@ extern void thread_group_times(struct task_struct *p, cputime_t *ut, cputime_t */* * task->group_stop flags */ +#define GROUP_STOP_SIGMASK 0xffff/* signr of the last group stop */ #define GROUP_STOP_PENDING (1 << 16)/* task should stop for group stop */ #define GROUP_STOP_CONSUME (1 << 17)/* consume group stop count */ +#define GROUP_STOP_TRAPPING (1 << 18)/* switching from STOPPED to TRACED */ extern void task_clear_group_stop_pending (struct task_struct *task);
kernel/ptrace.c +44 -5
76 lines not on this page · raw message
kernel/signal.c +66 -13
130 lines not on this page · raw message
Signature · 1 line
1.7.1
Oleg Nesterov
maintainerQuoted 20 lines · Tejun Heo
This patchset contains the following ten patches.
0001-signal-fix-SIGCONT-notification-code.patch 0002-ptrace-remove-the-extra-wake_up_state-from-ptrace_de.patch 0003-signal-remove-superflous-try_to_freeze-loop-in-do_si.patch 0004-ptrace-kill-tracehook_notify_jctl.patch 0005-ptrace-add-why-to-ptrace_stop.patch 0006-signal-fix-premature-completion-of-group-stop-when-i.patch 0007-signal-use-GROUP_STOP_PENDING-to-stop-once-for-a-sin.patch 0008-ptrace-participate-in-group-stop-from-ptrace_stop-if.patch 0009-ptrace-make-do_signal_stop-use-ptrace_stop-if-the-ta.patch 0010-ptrace-clean-transitions-between-TASK_STOPPED-and-TR.patch
In light of the proposal, this patchset can be separated into two parts. The first part, patches 0001 through 0006, tightens various loose ends and prepare for further changes. Except for 0004, every patch is acked by Oleg and/or Roland. 0004 doesn't cause any behavior change and removes a tracehook for future changes.
Quoted 4 lines
The second part, 0007-0010, fixes group stop participation accounting and make tracees always stop in
TASK_TRACEDinstead ofTASK_STOPPED. IOW, it implements "P1. AlwaysTASK_TRACEDwhile ptraced" of the proposal.
Looks like, we already discussed these changes? I mean, at first glance the only difference (compared to the previous series) is that 10/10 simplifies the usage of task_clear_group_stop_trapping().
In this case please free to add my reviewed-by or acked-by.
Quoted 4 lines
Roland, Oleg, Andrew, if there's no objection, I'd like to set up a git tree to route further developments in the area. We're going to see a lot of patches and it's simply much more efficient to have a dedicated stable git branch.
Agreed.
Quoted 8 lines
I don't really care who controls the tree and don't intend to commit any patch without Oleg's Acked-by so it would make most sense for Oleg to control the branch but, if Oleg dosnn't want to, I'd be happy to do it. No matter who does it, let's _PLEASE_ establish a stable tree.
This patchset is available in the following git branch.
git://git.kernel.org/pub/scm/linux/kernel/git/tj/misc.gitptrace-review
Thanks!
As for "who controls" I do not care too. But I think I should help you as much as I can. I'll contact you in a few days to discuss this, but let me repeat once again: whatever you prefer is fine for me.
Oleg.
Hey, Oleg.
Quoted 8 lines · Oleg Nesterov
Quoted 4 lines
The second part, 0007-0010, fixes group stop participation accounting and make tracees always stop in
TASK_TRACEDinstead ofTASK_STOPPED. IOW, it implements "P1. AlwaysTASK_TRACEDwhile ptraced" of the proposal.Looks like, we already discussed these changes? I mean, at first glance the only difference (compared to the previous series) is that 10/10 simplifies the usage of
task_clear_group_stop_trapping().
Yeah, nothing really changed other than incorporating what you suggested on the last posting.
Quoted 1 line
In this case please free to add my reviewed-by or acked-by.
Alright, will add Acked-by's.
Quoted 3 lines
As for "who controls" I do not care too. But I think I should help you as much as I can. I'll contact you in a few days to discuss this, but let me repeat once again: whatever you prefer is fine for me.
Great, I would need a lot of your help; otherwise, I would be breaking things left and right. :-)
Thank you.
Signature · 1 line
tejun