Linux kernel mirror (for testing) git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel os linux

staging: comedi: fix circular locking dependency in comedi_mmap()

Mmapping a comedi data buffer with lockdep checking enabled produced the
following kernel debug messages:

======================================================
[ INFO: possible circular locking dependency detected ]
3.5.0-rc3-ija1+ #9 Tainted: G C
-------------------------------------------------------
comedi_test/4160 is trying to acquire lock:
(&dev->mutex#2){+.+.+.}, at: [<ffffffffa00313f4>] comedi_mmap+0x57/0x1d9 [comedi]

but task is already holding lock:
(&mm->mmap_sem){++++++}, at: [<ffffffff810c96fe>] vm_mmap_pgoff+0x41/0x76

which lock already depends on the new lock.

the existing dependency chain (in reverse order) is:

-> #1 (&mm->mmap_sem){++++++}:
[<ffffffff8106d0e8>] lock_acquire+0x97/0x105
[<ffffffff810ce3bc>] might_fault+0x6d/0x90
[<ffffffffa0031ffb>] do_devinfo_ioctl.isra.7+0x11e/0x14c [comedi]
[<ffffffffa003227f>] comedi_unlocked_ioctl+0x256/0xe48 [comedi]
[<ffffffff810f7fcd>] vfs_ioctl+0x18/0x34
[<ffffffff810f87fd>] do_vfs_ioctl+0x382/0x43c
[<ffffffff810f88f9>] sys_ioctl+0x42/0x65
[<ffffffff81415c62>] system_call_fastpath+0x16/0x1b

-> #0 (&dev->mutex#2){+.+.+.}:
[<ffffffff8106c528>] __lock_acquire+0x101d/0x1591
[<ffffffff8106d0e8>] lock_acquire+0x97/0x105
[<ffffffff8140c894>] mutex_lock_nested+0x46/0x2a4
[<ffffffffa00313f4>] comedi_mmap+0x57/0x1d9 [comedi]
[<ffffffff810d5816>] mmap_region+0x281/0x492
[<ffffffff810d5c92>] do_mmap_pgoff+0x26b/0x2a7
[<ffffffff810c971a>] vm_mmap_pgoff+0x5d/0x76
[<ffffffff810d493f>] sys_mmap_pgoff+0xc7/0x10d
[<ffffffff81004d36>] sys_mmap+0x16/0x20
[<ffffffff81415c62>] system_call_fastpath+0x16/0x1b

other info that might help us debug this:

Possible unsafe locking scenario:

CPU0 CPU1
---- ----
lock(&mm->mmap_sem);
lock(&dev->mutex#2);
lock(&mm->mmap_sem);
lock(&dev->mutex#2);

*** DEADLOCK ***

To avoid the circular dependency, just try to get the lock in
`comedi_mmap()` instead of blocking. Since the comedi device's main mutex
is heavily used, do a down-read of its `attach_lock` rwsemaphore
instead. Trying to down-read `attach_lock` should only fail if
some task has down-write locked it, and that is only done while the
comedi device is being attached to or detached from a low-level hardware
device.

Unfortunately, acquiring the `attach_lock` doesn't prevent another
task replacing the comedi data buffer we are trying to mmap. The
details of the buffer are held in a `struct comedi_buf_map` and pointed
to by `s->async->buf_map` where `s` is the comedi subdevice whose buffer
we are trying to map. The `struct comedi_buf_map` is already reference
counted with a `struct kref`, so we can stop it being freed prematurely.

Modify `comedi_mmap()` to call new function
`comedi_buf_map_from_subdev_get()` to read the subdevice's current
buffer map pointer and increment its reference instead of accessing
`async->buf_map` directly. Call `comedi_buf_map_put()` to decrement the
reference once the buffer map structure has been dealt with. (Note that
`comedi_buf_map_put()` does nothing if passed a NULL pointer.)

`comedi_buf_map_from_subdev_get()` checks the subdevice's buffer map
pointer has been set and the buffer map has been initialized enough for
`comedi_mmap()` to deal with it (specifically, check the `n_pages`
member has been set to a non-zero value). If all is well, the buffer
map's reference is incremented and a pointer to it is returned. The
comedi subdevice's spin-lock is used to protect the checks. Also use
the spin-lock in `__comedi_buf_alloc()` and `__comedi_buf_free()` to
protect changes to the subdevice's buffer map structure pointer and the
buffer map structure's `n_pages` member. (This checking of `n_pages` is
a bit clunky and I [Ian Abbott] plan to deal with it in the future.)

Signed-off-by: Ian Abbott <abbotti@mev.co.uk>
Cc: <stable@vger.kernel.org> # 3.14.x, 3.15.x
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>

authored by

Ian Abbott and committed by
Greg Kroah-Hartman
b34aa86f 2c33d7cc

+51 -6
+35 -2
drivers/staging/comedi/comedi_buf.c
··· 61 61 struct comedi_subdevice *s) 62 62 { 63 63 struct comedi_async *async = s->async; 64 + struct comedi_buf_map *bm; 65 + unsigned long flags; 64 66 65 67 if (async->prealloc_buf) { 66 68 vunmap(async->prealloc_buf); ··· 70 68 async->prealloc_bufsz = 0; 71 69 } 72 70 73 - comedi_buf_map_put(async->buf_map); 71 + spin_lock_irqsave(&s->spin_lock, flags); 72 + bm = async->buf_map; 74 73 async->buf_map = NULL; 74 + spin_unlock_irqrestore(&s->spin_lock, flags); 75 + comedi_buf_map_put(bm); 75 76 } 76 77 77 78 static void __comedi_buf_alloc(struct comedi_device *dev, ··· 85 80 struct page **pages = NULL; 86 81 struct comedi_buf_map *bm; 87 82 struct comedi_buf_page *buf; 83 + unsigned long flags; 88 84 unsigned i; 89 85 90 86 if (!IS_ENABLED(CONFIG_HAS_DMA) && s->async_dma_dir != DMA_NONE) { ··· 98 92 if (!bm) 99 93 return; 100 94 101 - async->buf_map = bm; 102 95 kref_init(&bm->refcount); 96 + spin_lock_irqsave(&s->spin_lock, flags); 97 + async->buf_map = bm; 98 + spin_unlock_irqrestore(&s->spin_lock, flags); 103 99 bm->dma_dir = s->async_dma_dir; 104 100 if (bm->dma_dir != DMA_NONE) 105 101 /* Need ref to hardware device to free buffer later. */ ··· 135 127 136 128 pages[i] = virt_to_page(buf->virt_addr); 137 129 } 130 + spin_lock_irqsave(&s->spin_lock, flags); 138 131 bm->n_pages = i; 132 + spin_unlock_irqrestore(&s->spin_lock, flags); 139 133 140 134 /* vmap the prealloc_buf if all the pages were allocated */ 141 135 if (i == n_pages) ··· 158 148 if (bm) 159 149 return kref_put(&bm->refcount, comedi_buf_map_kref_release); 160 150 return 1; 151 + } 152 + 153 + /* returns s->async->buf_map and increments its kref refcount */ 154 + struct comedi_buf_map * 155 + comedi_buf_map_from_subdev_get(struct comedi_subdevice *s) 156 + { 157 + struct comedi_async *async = s->async; 158 + struct comedi_buf_map *bm = NULL; 159 + unsigned long flags; 160 + 161 + if (!async) 162 + return NULL; 163 + 164 + spin_lock_irqsave(&s->spin_lock, flags); 165 + bm = async->buf_map; 166 + /* only want it if buffer pages allocated */ 167 + if (bm && bm->n_pages) 168 + comedi_buf_map_get(bm); 169 + else 170 + bm = NULL; 171 + spin_unlock_irqrestore(&s->spin_lock, flags); 172 + 173 + return bm; 161 174 } 162 175 163 176 bool comedi_buf_is_mmapped(struct comedi_async *async)
+14 -4
drivers/staging/comedi/comedi_fops.c
··· 1926 1926 struct comedi_device *dev = file->private_data; 1927 1927 struct comedi_subdevice *s; 1928 1928 struct comedi_async *async; 1929 - struct comedi_buf_map *bm; 1929 + struct comedi_buf_map *bm = NULL; 1930 1930 unsigned long start = vma->vm_start; 1931 1931 unsigned long size; 1932 1932 int n_pages; 1933 1933 int i; 1934 1934 int retval; 1935 1935 1936 - mutex_lock(&dev->mutex); 1936 + /* 1937 + * 'trylock' avoids circular dependency with current->mm->mmap_sem 1938 + * and down-reading &dev->attach_lock should normally succeed without 1939 + * contention unless the device is in the process of being attached 1940 + * or detached. 1941 + */ 1942 + if (!down_read_trylock(&dev->attach_lock)) 1943 + return -EAGAIN; 1937 1944 1938 1945 if (!dev->attached) { 1939 1946 dev_dbg(dev->class_dev, "no driver attached\n"); ··· 1980 1973 } 1981 1974 1982 1975 n_pages = size >> PAGE_SHIFT; 1983 - bm = async->buf_map; 1976 + 1977 + /* get reference to current buf map (if any) */ 1978 + bm = comedi_buf_map_from_subdev_get(s); 1984 1979 if (!bm || n_pages > bm->n_pages) { 1985 1980 retval = -EINVAL; 1986 1981 goto done; ··· 2006 1997 2007 1998 retval = 0; 2008 1999 done: 2009 - mutex_unlock(&dev->mutex); 2000 + up_read(&dev->attach_lock); 2001 + comedi_buf_map_put(bm); /* put reference to buf map - okay if NULL */ 2010 2002 return retval; 2011 2003 } 2012 2004
+2
drivers/staging/comedi/comedi_internal.h
··· 19 19 bool comedi_buf_is_mmapped(struct comedi_async *async); 20 20 void comedi_buf_map_get(struct comedi_buf_map *bm); 21 21 int comedi_buf_map_put(struct comedi_buf_map *bm); 22 + struct comedi_buf_map *comedi_buf_map_from_subdev_get( 23 + struct comedi_subdevice *s); 22 24 unsigned int comedi_buf_write_n_allocated(struct comedi_async *async); 23 25 void comedi_device_cancel_all(struct comedi_device *dev); 24 26