Skip to contentSkip to conversation

tasklist_lock/dcache_lock race bugfix

RFC PATCH In review Manfred Spraul 2 participants 3 messages Apr 6, 2003 – Apr 13, 2003

Subsystems (4)
lore
Share & tools
lore
#0 lore raw

RFC PATCH 1/1 tasklist_lock/dcache_lock race bugfix

__unhash_process acquires the dcache_lock while holding the tasklist_lock for writing. This can deadlock. While trying to fix that race, I found further races with proc_dentry. The attached patch (hopefully) fixes all these races.

Changes:
- fs/proc/base.c assumed that p->pid is reset to 0 during exit. This is not the case anymore. I now look at the count of the pid structure for PIDTYPE_PID.
- pid_delete_dentry and pid_revalidate removed. The implementation didn't work, and noone complained.
- only d_add the new proc entry if the task is still valid: d_add+d_drop is a race, if a lookup happens between both calls.
- add a new spinlock that protects proc_dentry.

What do you think? I don't like the new spinlock, but I couldn't find another solution.

--
Manfred

// $Header$
// Kernel Version:

//  VERSION = 2
//  PATCHLEVEL = 5
//  SUBLEVEL = 66
//  EXTRAVERSION =
include/linux/sched.h +2 -0
@@ -428,6 +428,8 @@
    	u32 self_exec_id;
 /* Protection of (de-)allocation: mm, files, fs, tty */
 	spinlock_t alloc_lock;
+/* Protection of proc_dentry: nesting proc_lock, dcache_lock, write_lock_irq(&tasklist_lock); */
+	spinlock_t proc_lock;
 /* context-switch lock */
 	spinlock_t switch_lock;
 
fs/proc/base.c +70 -29

141 lines not on this page · raw message

kernel/exit.c +13 -28

89 lines not on this page · raw message

include/linux/init_task.h +1 -0
@@ -101,6 +101,7 @@
 	.blocked	= {{0}},					\
 	 .posix_timers	 = LIST_HEAD_INIT(tsk.posix_timers),		   \
 	.alloc_lock	= SPIN_LOCK_UNLOCKED,				\
+	.proc_lock	= SPIN_LOCK_UNLOCKED,				\
 	.switch_lock	= SPIN_LOCK_UNLOCKED,				\
 	.journal_info	= NULL,						\
 }
fs/exec.c +8 -29
@@ -529,30 +529,6 @@
 	return 0;
 }
 
-static struct dentry *clean_proc_dentry(struct task_struct *p)
-{
-	struct dentry *proc_dentry = p->proc_dentry;
-
-	if (proc_dentry) {
-		spin_lock(&dcache_lock);
-		if (!d_unhashed(proc_dentry)) {
-			dget_locked(proc_dentry);
-			__d_drop(proc_dentry);
-		} else
-			proc_dentry = NULL;
-		spin_unlock(&dcache_lock);
-	}
-	return proc_dentry;
-}
-
-static inline void put_proc_dentry(struct dentry *dentry)
-{
-	if (dentry) {
-		shrink_dcache_parent(dentry);
-		dput(dentry);
-	}
-}
-
 /*
  * This function makes sure the current process has its own signal table,
  * so that flush_signal_handlers can later reset the handlers without
@@ -660,9 +636,11 @@
 		while (leader->state != TASK_ZOMBIE)
 			yield();
 
+		task_lock(leader);
+		task_lock(current);
+		proc_dentry1 = proc_pid_unhash(current);
+		proc_dentry2 = proc_pid_unhash(leader);
 		write_lock_irq(&tasklist_lock);
-		proc_dentry1 = clean_proc_dentry(current);
-		proc_dentry2 = clean_proc_dentry(leader);
 
 		if (leader->tgid != current->tgid)
 			BUG();
@@ -702,9 +680,10 @@
 		state = leader->state;
 
 		write_unlock_irq(&tasklist_lock);
-
-		put_proc_dentry(proc_dentry1);
-		put_proc_dentry(proc_dentry2);
+		task_unlock(current);
+		task_unlock(leader);
+		proc_pid_flush(proc_dentry1);
+		proc_pid_flush(proc_dentry2);
 
 		if (state != TASK_ZOMBIE)
 			BUG();
#1 lore raw
Quoted 6 lines · Manfred Spraul

__unhash_process acquires the dcache_lock while holding the tasklist_lock for writing. This can deadlock. While trying to fix that race, I found further races with proc_dentry.
The attached patch (hopefully) fixes all these races.

Quoted 9 lines

Changes:
- fs/proc/base.c assumed that p->pid is reset to 0 during exit. This is not the case anymore. I now look at the count of the pid structure for PIDTYPE_PID.
- pid_delete_dentry and pid_revalidate removed. The implementation didn't work, and noone complained.
- only d_add the new proc entry if the task is still valid: d_add+d_drop is a race, if a lookup happens between both calls.
- add a new spinlock that protects proc_dentry.

Did you send the correct patch?

kernel/exit.c: In function `release_task':
kernel/exit.c:59: warning: implicit declaration of function proc_pid_unhash' kernel/exit.c:59: warning: assignment makes pointer from integer without a cast kernel/exit.c:86: warning: implicit declaration of function proc_pid_flush' kernel/exit.c: In function `unhash_process':
kernel/exit.c:95: warning: proc_dentry' might be used uninitialized in this function fs/exec.c: In function de_thread':
fs/exec.c:641: warning: implicit declaration of function `proc_pid_unhash' fs/exec.c:641: warning: assignment makes pointer from integer without a cast fs/exec.c:642: warning: assignment makes pointer from integer without a cast fs/exec.c:685: warning: implicit declaration of function `proc_pid_flush'

and an oops at boot:

Program received signal SIGEMT, Emulation trap.
select_parent (parent=0x1) at fs/dcache.c:540
540 next = tmp->next;
(gdb) p tmp
No symbol "tmp" in current context.
(gdb) p next
$1 = (struct list_head *) 0x87f000fe
(gdb) bt

#0  select_parent (parent=0x1) at fs/dcache.c:540
#1  0xc01624e0 in shrink_dcache_parent (parent=0x1) at fs/dcache.c:590
#2  0xc017a8bd in proc_pid_flush (proc_dentry=0x1) at fs/proc/base.c:1129
#3  0xc0120a53 in unhash_process (p=0xc8db6660) at kernel/exit.c:102
#4  0xc03d188f in do_boot_cpu (apicid=0) at arch/i386/kernel/smpboot.c:805
#5  0xc03d1b53 in smp_boot_cpus (max_cpus=4) at arch/i386/kernel/smpboot.c:1040
#6  0xc03d1d58 in smp_prepare_cpus (max_cpus=4) at arch/i386/kernel/smpboot.c:1125
#7  0xc0105098 in init (unused=0x0) at init/main.c:563
#2 lore raw
Quoted 17 lines · Andrew Morton
Quoted 13 lines · Manfred Spraul

__unhash_process acquires the dcache_lock while holding the tasklist_lock for writing. This can deadlock. While trying to fix that race, I found further races with proc_dentry. The attached patch (hopefully) fixes all these races.

Changes:
- fs/proc/base.c assumed that p->pid is reset to 0 during exit. This is not the case anymore. I now look at the count of the pid structure for PIDTYPE_PID.
- pid_delete_dentry and pid_revalidate removed. The implementation didn't work, and noone complained.
- only d_add the new proc entry if the task is still valid: d_add+d_drop is a race, if a lookup happens between both calls.
- add a new spinlock that protects proc_dentry.

Quoted 3 lines

Did you send the correct patch?

Yes, but my diff script was incomplete :-(

Quoted 3 lines

kernel/exit.c: In function `release_task':
kernel/exit.c:59: warning: implicit declaration of function `proc_pid_unhash'

Missing updates to <linux/proc_fs.h>

Quoted 5 lines

kernel/exit.c:59: warning: assignment makes pointer from integer without a cast kernel/exit.c:86: warning: implicit declaration of function proc_pid_flush' kernel/exit.c: In function unhash_process':
kernel/exit.c:95: warning: `proc_dentry' might be used uninitialized in this function

And this one is a fatal error: proc_dentry remained uninitialized, and that crashes during smp boot.

New patch attached.
Changelog unchanged.

--
Manfred

// $Header$
// Kernel Version:

//  VERSION = 2
//  PATCHLEVEL = 5
//  SUBLEVEL = 67
//  EXTRAVERSION = -mm1
include/linux/sched.h +2 -0
@@ -431,6 +431,8 @@
    	u32 self_exec_id;
 /* Protection of (de-)allocation: mm, files, fs, tty */
 	spinlock_t alloc_lock;
+/* Protection of proc_dentry: nesting proc_lock, dcache_lock, write_lock_irq(&tasklist_lock); */
+	spinlock_t proc_lock;
 /* context-switch lock */
 	spinlock_t switch_lock;
 
fs/proc/base.c +70 -29

141 lines not on this page · raw message

kernel/exit.c +13 -27
@@ -21,6 +21,7 @@
 #include <linux/ptrace.h>
 #include <linux/profile.h>
 #include <linux/mount.h>
+#include <linux/proc_fs.h>
 
 #include <asm/uaccess.h>
 #include <asm/pgtable.h>
@@ -31,10 +32,8 @@
 
 int getrusage(struct task_struct *, int, struct rusage *);
 
-static struct dentry * __unhash_process(struct task_struct *p)
+static void __unhash_process(struct task_struct *p)
 {
-	struct dentry *proc_dentry;
-
 	nr_threads--;
 	detach_pid(p, PIDTYPE_PID);
 	detach_pid(p, PIDTYPE_TGID);
@@ -46,34 +45,25 @@
 	}
 
 	REMOVE_LINKS(p);
-	proc_dentry = p->proc_dentry;
-	if (unlikely(proc_dentry != NULL)) {
-		spin_lock(&dcache_lock);
-		if (!d_unhashed(proc_dentry)) {
-			dget_locked(proc_dentry);
-			__d_drop(proc_dentry);
-		} else
-			proc_dentry = NULL;
-		spin_unlock(&dcache_lock);
-	}
-	return proc_dentry;
 }
 
 void release_task(struct task_struct * p)
 {
-	struct dentry *proc_dentry;
 	task_t *leader;
+	struct dentry *proc_dentry;
  
 	BUG_ON(p->state < TASK_ZOMBIE);
  
 	atomic_dec(&p->user->processes);
+	spin_lock(&p->proc_lock);
+	proc_dentry = proc_pid_unhash(p);
 	write_lock_irq(&tasklist_lock);
 	if (unlikely(p->ptrace))
 		__ptrace_unlink(p);
 	BUG_ON(!list_empty(&p->ptrace_list) || !list_empty(&p->ptrace_children));
 	__exit_signal(p);
 	__exit_sighand(p);
-	proc_dentry = __unhash_process(p);
+	__unhash_process(p);
 
 	/*
 	 * If we are the last non-leader member of the thread
@@ -92,11 +82,8 @@
 	p->parent->cnswap += p->nswap + p->cnswap;
 	sched_exit(p);
 	write_unlock_irq(&tasklist_lock);
-
-	if (unlikely(proc_dentry != NULL)) {
-		shrink_dcache_parent(proc_dentry);
-		dput(proc_dentry);
-	}
+	spin_unlock(&p->proc_lock);
+	proc_pid_flush(proc_dentry);
 	release_thread(p);
 	put_task_struct(p);
 }
@@ -107,14 +94,13 @@
 {
 	struct dentry *proc_dentry;
 
+	spin_lock(&p->proc_lock);
+	proc_dentry = proc_pid_unhash(p);
 	write_lock_irq(&tasklist_lock);
-	proc_dentry = __unhash_process(p);
+	__unhash_process(p);
 	write_unlock_irq(&tasklist_lock);
-
-	if (unlikely(proc_dentry != NULL)) {
-		shrink_dcache_parent(proc_dentry);
-		dput(proc_dentry);
-	}
+	spin_unlock(&p->proc_lock);
+	proc_pid_flush(proc_dentry);
 }
 
 /*
include/linux/init_task.h +1 -0
@@ -101,6 +101,7 @@
 	.blocked	= {{0}},					\
 	 .posix_timers	 = LIST_HEAD_INIT(tsk.posix_timers),		   \
 	.alloc_lock	= SPIN_LOCK_UNLOCKED,				\
+	.proc_lock	= SPIN_LOCK_UNLOCKED,				\
 	.switch_lock	= SPIN_LOCK_UNLOCKED,				\
 	.journal_info	= NULL,						\
 }
fs/exec.c +8 -29
@@ -528,30 +528,6 @@
 	return 0;
 }
 
-static struct dentry *clean_proc_dentry(struct task_struct *p)
-{
-	struct dentry *proc_dentry = p->proc_dentry;
-
-	if (proc_dentry) {
-		spin_lock(&dcache_lock);
-		if (!d_unhashed(proc_dentry)) {
-			dget_locked(proc_dentry);
-			__d_drop(proc_dentry);
-		} else
-			proc_dentry = NULL;
-		spin_unlock(&dcache_lock);
-	}
-	return proc_dentry;
-}
-
-static inline void put_proc_dentry(struct dentry *dentry)
-{
-	if (dentry) {
-		shrink_dcache_parent(dentry);
-		dput(dentry);
-	}
-}
-
 /*
  * This function makes sure the current process has its own signal table,
  * so that flush_signal_handlers can later reset the handlers without
@@ -659,9 +635,11 @@
 		while (leader->state != TASK_ZOMBIE)
 			yield();
 
+		task_lock(leader);
+		task_lock(current);
+		proc_dentry1 = proc_pid_unhash(current);
+		proc_dentry2 = proc_pid_unhash(leader);
 		write_lock_irq(&tasklist_lock);
-		proc_dentry1 = clean_proc_dentry(current);
-		proc_dentry2 = clean_proc_dentry(leader);
 
 		if (leader->tgid != current->tgid)
 			BUG();
@@ -701,9 +679,10 @@
 		state = leader->state;
 
 		write_unlock_irq(&tasklist_lock);
-
-		put_proc_dentry(proc_dentry1);
-		put_proc_dentry(proc_dentry2);
+		task_unlock(current);
+		task_unlock(leader);
+		proc_pid_flush(proc_dentry1);
+		proc_pid_flush(proc_dentry2);
 
 		if (state != TASK_ZOMBIE)
 			BUG();
include/linux/proc_fs.h +2 -0
@@ -87,6 +87,8 @@
 extern void proc_misc_init(void);
 
 struct dentry *proc_pid_lookup(struct inode *dir, struct dentry * dentry);
+struct dentry *proc_pid_unhash(struct task_struct *p);
+void proc_pid_flush(struct dentry *proc_dentry);
 int proc_pid_readdir(struct file * filp, void * dirent, filldir_t filldir);
 
 extern struct proc_dir_entry *create_proc_entry(const char *name, mode_t mode,