Re: [PATCH 6/9] signal: Always call do_notify_parent_cldstop with siglock held

From: Eric W. Biederman
Date: Thu Apr 28 2022 - 16:49:46 EST


"Eric W. Biederman" <ebiederm@xxxxxxxxxxxx> writes:

> Peter Zijlstra <peterz@xxxxxxxxxxxxx> writes:
>
>> On Wed, Apr 27, 2022 at 09:47:10AM -0500, Eric W. Biederman wrote:
>>
>>> Hmm. If we have the following process tree.
>>>
>>> A
>>> \
>>> B
>>> \
>>> C
>>>
>>> Process A, B, and C are all in the same process group.
>>> Process A and B are setup to receive SIGCHILD when
>>> their process stops.
>>>
>>> Process C traces process A.
>>>
>>> When a sigstop is delivered to the group we can have:
>>>
>>> Process B takes siglock(B) siglock(A) to notify the real_parent
>>> Process C takes siglock(C) siglock(B) to notify the real_parent
>>> Process A takes siglock(A) siglock(C) to notify the tracer
>>>
>>> If they all take their local lock at the same time there is
>>> a deadlock.
>>>
>>> I don't think the restriction that you can never ptrace anyone
>>> up the process tree is going to fly. So it looks like I am back to the
>>> drawing board for this one.
>>
>> I've not had time to fully appreciate the nested locking here, but if it
>> is possible to rework things to always take both locks at the same time,
>> then it would be possible to impose an arbitrary lock order on things
>> and break the cycle that way.
>>
>> That is, simply order the locks by their heap address or something:
>>
>> static void double_siglock_irq(struct sighand *sh1, struct sighand2 *sh2)
>> {
>> if (sh1 > sh2)
>> swap(sh1, sh2)
>>
>> spin_lock_irq(&sh1->siglock);
>> spin_lock_nested(&sh2->siglock, SINGLE_DEPTH_NESTING);
>> }
>
> You know it might be. Especially given that the existing code is
> already dropping siglock and grabbing tasklist_lock.
>
> It would take a potentially triple lock function to lock
> the task it's real_parent and it's tracer (aka parent).
>
> That makes this possible to consider is that notifying the ``parents''
> is a fundamental part of the operation so we know we are going to
> need the lock so we can move it up.
>
> Throw in a pinch of lock_task_sighand and the triple lock function
> gets quite interesting.
>
> It is certainly worth trying, and I will.

To my surprise it doesn't look too bad. The locking simplifications and
not using a lock as big as tasklist_lock probably make it even worth
doing.

I need to sleep on it and look at everything again. In the
meantime here is my function that comes in with siglock held,
possibly drops it, and grabs the other two locks all in
order.

static void lock_parents_siglocks(bool lock_tracer)
__releases(&current->sighand->siglock)
__acquires(&current->sighand->siglock)
__acquires(&current->real_parent->sighand->siglock)
__acquires(&current->parent->sighand->siglock)
{
struct task_struct *me = current;
struct sighand_struct *m_sighand = me->sighand;

lockdep_assert_held(&m_sighand->siglock);

rcu_read_lock();
for (;;) {
struct task_struct *parent, *tracer;
struct sighand_struct *p_sighand, *t_sighand, *s1, *s2, *s3;

parent = me->real_parent;
tracer = lock_tracer? me->parent : parent;

p_sighand = rcu_dereference(parent->sighand);
t_sighand = rcu_dereference(tracer->sighand);

/* Sort the sighands so that s1 >= s2 >= s3 */
s1 = m_sighand;
s2 = p_sighand;
s3 = t_sighand;
if (s1 > s2)
swap(s1, s2);
if (s1 > s3)
swap(s1, s3);
if (s2 > s3)
swap(s2, s3);

if (s1 != m_sighand) {
spin_unlock(&m_sighand->siglock);
spin_lock(&s1->siglock);
}

if (s1 != s2)
spin_lock_nested(&s2->siglock, SIGLOCK_LOCK_SECOND);
if (s2 != s3)
spin_lock_nested(&s3->siglock, SIGLOCK_LOCK_THIRD);

if (likely((me->real_parent == parent) &&
(me->parent == tracer) &&
(parent->sighand == p_sighand) &&
(tracer->sighand == t_sighand))) {
break;
}
spin_unlock(&p_sighand->siglock);
if (t_sighand != p_sighand)
spin_unlock(&t_sighand->siglock);
continue;
}
rcu_read_unlock();
}

Eric