forked from mirrors/linux
		
	gpiolib: protect the GPIO device against being dropped while in use by user-space
While any of the GPIO cdev syscalls is in progress, the kernel can call gpiochip_remove() (for instance, when a USB GPIO expander is disconnected) which will set gdev->chip to NULL after which any subsequent access will cause a crash. To avoid that: use an RW-semaphore in which the syscalls take it for reading (so that we don't needlessly prohibit the user-space from calling syscalls simultaneously) while gpiochip_remove() takes it for writing so that it can only happen once all syscalls return. Fixes:d7c51b47ac("gpio: userspace ABI for reading/writing GPIO lines") Fixes:3c0d9c635a("gpiolib: cdev: support GPIO_V2_GET_LINE_IOCTL and GPIO_V2_LINE_GET_VALUES_IOCTL") Fixes:aad955842d("gpiolib: cdev: support GPIO_V2_GET_LINEINFO_IOCTL and GPIO_V2_GET_LINEINFO_WATCH_IOCTL") Fixes:a54756cb24("gpiolib: cdev: support GPIO_V2_LINE_SET_CONFIG_IOCTL") Fixes:7b8e00d981("gpiolib: cdev: support GPIO_V2_LINE_SET_VALUES_IOCTL") Signed-off-by: Bartosz Golaszewski <bartosz.golaszewski@linaro.org> [Nick: fixed a build failure with CDEV_V1 disabled] Co-authored-by: Nick Hainke <vincent@systemli.org> Reviewed-by: Kent Gibson <warthog618@gmail.com> Reviewed-by: Andy Shevchenko <andriy.shevchenko@linux.intel.com> Reviewed-by: Linus Walleij <linus.walleij@linaro.org>
This commit is contained in:
		
							parent
							
								
									533aae7c94
								
							
						
					
					
						commit
						bdbbae241a
					
				
					 3 changed files with 161 additions and 25 deletions
				
			
		|  | @ -55,6 +55,50 @@ static_assert(IS_ALIGNED(sizeof(struct gpio_v2_line_values), 8)); | ||||||
|  * interface to gpiolib GPIOs via ioctl()s. |  * interface to gpiolib GPIOs via ioctl()s. | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
|  | typedef __poll_t (*poll_fn)(struct file *, struct poll_table_struct *); | ||||||
|  | typedef long (*ioctl_fn)(struct file *, unsigned int, unsigned long); | ||||||
|  | typedef ssize_t (*read_fn)(struct file *, char __user *, | ||||||
|  | 			   size_t count, loff_t *); | ||||||
|  | 
 | ||||||
|  | static __poll_t call_poll_locked(struct file *file, | ||||||
|  | 				 struct poll_table_struct *wait, | ||||||
|  | 				 struct gpio_device *gdev, poll_fn func) | ||||||
|  | { | ||||||
|  | 	__poll_t ret; | ||||||
|  | 
 | ||||||
|  | 	down_read(&gdev->sem); | ||||||
|  | 	ret = func(file, wait); | ||||||
|  | 	up_read(&gdev->sem); | ||||||
|  | 
 | ||||||
|  | 	return ret; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static long call_ioctl_locked(struct file *file, unsigned int cmd, | ||||||
|  | 			      unsigned long arg, struct gpio_device *gdev, | ||||||
|  | 			      ioctl_fn func) | ||||||
|  | { | ||||||
|  | 	long ret; | ||||||
|  | 
 | ||||||
|  | 	down_read(&gdev->sem); | ||||||
|  | 	ret = func(file, cmd, arg); | ||||||
|  | 	up_read(&gdev->sem); | ||||||
|  | 
 | ||||||
|  | 	return ret; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static ssize_t call_read_locked(struct file *file, char __user *buf, | ||||||
|  | 				size_t count, loff_t *f_ps, | ||||||
|  | 				struct gpio_device *gdev, read_fn func) | ||||||
|  | { | ||||||
|  | 	ssize_t ret; | ||||||
|  | 
 | ||||||
|  | 	down_read(&gdev->sem); | ||||||
|  | 	ret = func(file, buf, count, f_ps); | ||||||
|  | 	up_read(&gdev->sem); | ||||||
|  | 
 | ||||||
|  | 	return ret; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| /*
 | /*
 | ||||||
|  * GPIO line handle management |  * GPIO line handle management | ||||||
|  */ |  */ | ||||||
|  | @ -191,8 +235,8 @@ static long linehandle_set_config(struct linehandle_state *lh, | ||||||
| 	return 0; | 	return 0; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static long linehandle_ioctl(struct file *file, unsigned int cmd, | static long linehandle_ioctl_unlocked(struct file *file, unsigned int cmd, | ||||||
| 			     unsigned long arg) | 				      unsigned long arg) | ||||||
| { | { | ||||||
| 	struct linehandle_state *lh = file->private_data; | 	struct linehandle_state *lh = file->private_data; | ||||||
| 	void __user *ip = (void __user *)arg; | 	void __user *ip = (void __user *)arg; | ||||||
|  | @ -250,6 +294,15 @@ static long linehandle_ioctl(struct file *file, unsigned int cmd, | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | static long linehandle_ioctl(struct file *file, unsigned int cmd, | ||||||
|  | 			     unsigned long arg) | ||||||
|  | { | ||||||
|  | 	struct linehandle_state *lh = file->private_data; | ||||||
|  | 
 | ||||||
|  | 	return call_ioctl_locked(file, cmd, arg, lh->gdev, | ||||||
|  | 				 linehandle_ioctl_unlocked); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| #ifdef CONFIG_COMPAT | #ifdef CONFIG_COMPAT | ||||||
| static long linehandle_ioctl_compat(struct file *file, unsigned int cmd, | static long linehandle_ioctl_compat(struct file *file, unsigned int cmd, | ||||||
| 				    unsigned long arg) | 				    unsigned long arg) | ||||||
|  | @ -1381,8 +1434,8 @@ static long linereq_set_config(struct linereq *lr, void __user *ip) | ||||||
| 	return ret; | 	return ret; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static long linereq_ioctl(struct file *file, unsigned int cmd, | static long linereq_ioctl_unlocked(struct file *file, unsigned int cmd, | ||||||
| 			  unsigned long arg) | 				   unsigned long arg) | ||||||
| { | { | ||||||
| 	struct linereq *lr = file->private_data; | 	struct linereq *lr = file->private_data; | ||||||
| 	void __user *ip = (void __user *)arg; | 	void __user *ip = (void __user *)arg; | ||||||
|  | @ -1402,6 +1455,15 @@ static long linereq_ioctl(struct file *file, unsigned int cmd, | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | static long linereq_ioctl(struct file *file, unsigned int cmd, | ||||||
|  | 			  unsigned long arg) | ||||||
|  | { | ||||||
|  | 	struct linereq *lr = file->private_data; | ||||||
|  | 
 | ||||||
|  | 	return call_ioctl_locked(file, cmd, arg, lr->gdev, | ||||||
|  | 				 linereq_ioctl_unlocked); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| #ifdef CONFIG_COMPAT | #ifdef CONFIG_COMPAT | ||||||
| static long linereq_ioctl_compat(struct file *file, unsigned int cmd, | static long linereq_ioctl_compat(struct file *file, unsigned int cmd, | ||||||
| 				 unsigned long arg) | 				 unsigned long arg) | ||||||
|  | @ -1410,8 +1472,8 @@ static long linereq_ioctl_compat(struct file *file, unsigned int cmd, | ||||||
| } | } | ||||||
| #endif | #endif | ||||||
| 
 | 
 | ||||||
| static __poll_t linereq_poll(struct file *file, | static __poll_t linereq_poll_unlocked(struct file *file, | ||||||
| 			    struct poll_table_struct *wait) | 				      struct poll_table_struct *wait) | ||||||
| { | { | ||||||
| 	struct linereq *lr = file->private_data; | 	struct linereq *lr = file->private_data; | ||||||
| 	__poll_t events = 0; | 	__poll_t events = 0; | ||||||
|  | @ -1428,10 +1490,16 @@ static __poll_t linereq_poll(struct file *file, | ||||||
| 	return events; | 	return events; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static ssize_t linereq_read(struct file *file, | static __poll_t linereq_poll(struct file *file, | ||||||
| 			    char __user *buf, | 			     struct poll_table_struct *wait) | ||||||
| 			    size_t count, | { | ||||||
| 			    loff_t *f_ps) | 	struct linereq *lr = file->private_data; | ||||||
|  | 
 | ||||||
|  | 	return call_poll_locked(file, wait, lr->gdev, linereq_poll_unlocked); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static ssize_t linereq_read_unlocked(struct file *file, char __user *buf, | ||||||
|  | 				     size_t count, loff_t *f_ps) | ||||||
| { | { | ||||||
| 	struct linereq *lr = file->private_data; | 	struct linereq *lr = file->private_data; | ||||||
| 	struct gpio_v2_line_event le; | 	struct gpio_v2_line_event le; | ||||||
|  | @ -1485,6 +1553,15 @@ static ssize_t linereq_read(struct file *file, | ||||||
| 	return bytes_read; | 	return bytes_read; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | static ssize_t linereq_read(struct file *file, char __user *buf, | ||||||
|  | 			    size_t count, loff_t *f_ps) | ||||||
|  | { | ||||||
|  | 	struct linereq *lr = file->private_data; | ||||||
|  | 
 | ||||||
|  | 	return call_read_locked(file, buf, count, f_ps, lr->gdev, | ||||||
|  | 				linereq_read_unlocked); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| static void linereq_free(struct linereq *lr) | static void linereq_free(struct linereq *lr) | ||||||
| { | { | ||||||
| 	unsigned int i; | 	unsigned int i; | ||||||
|  | @ -1722,8 +1799,8 @@ struct lineevent_state { | ||||||
| 	(GPIOEVENT_REQUEST_RISING_EDGE | \ | 	(GPIOEVENT_REQUEST_RISING_EDGE | \ | ||||||
| 	GPIOEVENT_REQUEST_FALLING_EDGE) | 	GPIOEVENT_REQUEST_FALLING_EDGE) | ||||||
| 
 | 
 | ||||||
| static __poll_t lineevent_poll(struct file *file, | static __poll_t lineevent_poll_unlocked(struct file *file, | ||||||
| 			       struct poll_table_struct *wait) | 					struct poll_table_struct *wait) | ||||||
| { | { | ||||||
| 	struct lineevent_state *le = file->private_data; | 	struct lineevent_state *le = file->private_data; | ||||||
| 	__poll_t events = 0; | 	__poll_t events = 0; | ||||||
|  | @ -1739,15 +1816,21 @@ static __poll_t lineevent_poll(struct file *file, | ||||||
| 	return events; | 	return events; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | static __poll_t lineevent_poll(struct file *file, | ||||||
|  | 			       struct poll_table_struct *wait) | ||||||
|  | { | ||||||
|  | 	struct lineevent_state *le = file->private_data; | ||||||
|  | 
 | ||||||
|  | 	return call_poll_locked(file, wait, le->gdev, lineevent_poll_unlocked); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| struct compat_gpioeevent_data { | struct compat_gpioeevent_data { | ||||||
| 	compat_u64	timestamp; | 	compat_u64	timestamp; | ||||||
| 	u32		id; | 	u32		id; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| static ssize_t lineevent_read(struct file *file, | static ssize_t lineevent_read_unlocked(struct file *file, char __user *buf, | ||||||
| 			      char __user *buf, | 				       size_t count, loff_t *f_ps) | ||||||
| 			      size_t count, |  | ||||||
| 			      loff_t *f_ps) |  | ||||||
| { | { | ||||||
| 	struct lineevent_state *le = file->private_data; | 	struct lineevent_state *le = file->private_data; | ||||||
| 	struct gpioevent_data ge; | 	struct gpioevent_data ge; | ||||||
|  | @ -1815,6 +1898,15 @@ static ssize_t lineevent_read(struct file *file, | ||||||
| 	return bytes_read; | 	return bytes_read; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | static ssize_t lineevent_read(struct file *file, char __user *buf, | ||||||
|  | 			      size_t count, loff_t *f_ps) | ||||||
|  | { | ||||||
|  | 	struct lineevent_state *le = file->private_data; | ||||||
|  | 
 | ||||||
|  | 	return call_read_locked(file, buf, count, f_ps, le->gdev, | ||||||
|  | 				lineevent_read_unlocked); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| static void lineevent_free(struct lineevent_state *le) | static void lineevent_free(struct lineevent_state *le) | ||||||
| { | { | ||||||
| 	if (le->irq) | 	if (le->irq) | ||||||
|  | @ -1832,8 +1924,8 @@ static int lineevent_release(struct inode *inode, struct file *file) | ||||||
| 	return 0; | 	return 0; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static long lineevent_ioctl(struct file *file, unsigned int cmd, | static long lineevent_ioctl_unlocked(struct file *file, unsigned int cmd, | ||||||
| 			    unsigned long arg) | 				     unsigned long arg) | ||||||
| { | { | ||||||
| 	struct lineevent_state *le = file->private_data; | 	struct lineevent_state *le = file->private_data; | ||||||
| 	void __user *ip = (void __user *)arg; | 	void __user *ip = (void __user *)arg; | ||||||
|  | @ -1864,6 +1956,15 @@ static long lineevent_ioctl(struct file *file, unsigned int cmd, | ||||||
| 	return -EINVAL; | 	return -EINVAL; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | static long lineevent_ioctl(struct file *file, unsigned int cmd, | ||||||
|  | 			    unsigned long arg) | ||||||
|  | { | ||||||
|  | 	struct lineevent_state *le = file->private_data; | ||||||
|  | 
 | ||||||
|  | 	return call_ioctl_locked(file, cmd, arg, le->gdev, | ||||||
|  | 				 lineevent_ioctl_unlocked); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| #ifdef CONFIG_COMPAT | #ifdef CONFIG_COMPAT | ||||||
| static long lineevent_ioctl_compat(struct file *file, unsigned int cmd, | static long lineevent_ioctl_compat(struct file *file, unsigned int cmd, | ||||||
| 				   unsigned long arg) | 				   unsigned long arg) | ||||||
|  | @ -2422,8 +2523,8 @@ static int lineinfo_changed_notify(struct notifier_block *nb, | ||||||
| 	return NOTIFY_OK; | 	return NOTIFY_OK; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static __poll_t lineinfo_watch_poll(struct file *file, | static __poll_t lineinfo_watch_poll_unlocked(struct file *file, | ||||||
| 				    struct poll_table_struct *pollt) | 					     struct poll_table_struct *pollt) | ||||||
| { | { | ||||||
| 	struct gpio_chardev_data *cdev = file->private_data; | 	struct gpio_chardev_data *cdev = file->private_data; | ||||||
| 	__poll_t events = 0; | 	__poll_t events = 0; | ||||||
|  | @ -2440,8 +2541,17 @@ static __poll_t lineinfo_watch_poll(struct file *file, | ||||||
| 	return events; | 	return events; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static ssize_t lineinfo_watch_read(struct file *file, char __user *buf, | static __poll_t lineinfo_watch_poll(struct file *file, | ||||||
| 				   size_t count, loff_t *off) | 				    struct poll_table_struct *pollt) | ||||||
|  | { | ||||||
|  | 	struct gpio_chardev_data *cdev = file->private_data; | ||||||
|  | 
 | ||||||
|  | 	return call_poll_locked(file, pollt, cdev->gdev, | ||||||
|  | 				lineinfo_watch_poll_unlocked); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static ssize_t lineinfo_watch_read_unlocked(struct file *file, char __user *buf, | ||||||
|  | 					    size_t count, loff_t *off) | ||||||
| { | { | ||||||
| 	struct gpio_chardev_data *cdev = file->private_data; | 	struct gpio_chardev_data *cdev = file->private_data; | ||||||
| 	struct gpio_v2_line_info_changed event; | 	struct gpio_v2_line_info_changed event; | ||||||
|  | @ -2519,6 +2629,15 @@ static ssize_t lineinfo_watch_read(struct file *file, char __user *buf, | ||||||
| 	return bytes_read; | 	return bytes_read; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | static ssize_t lineinfo_watch_read(struct file *file, char __user *buf, | ||||||
|  | 				   size_t count, loff_t *off) | ||||||
|  | { | ||||||
|  | 	struct gpio_chardev_data *cdev = file->private_data; | ||||||
|  | 
 | ||||||
|  | 	return call_read_locked(file, buf, count, off, cdev->gdev, | ||||||
|  | 				lineinfo_watch_read_unlocked); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| /**
 | /**
 | ||||||
|  * gpio_chrdev_open() - open the chardev for ioctl operations |  * gpio_chrdev_open() - open the chardev for ioctl operations | ||||||
|  * @inode: inode for this chardev |  * @inode: inode for this chardev | ||||||
|  | @ -2532,13 +2651,17 @@ static int gpio_chrdev_open(struct inode *inode, struct file *file) | ||||||
| 	struct gpio_chardev_data *cdev; | 	struct gpio_chardev_data *cdev; | ||||||
| 	int ret = -ENOMEM; | 	int ret = -ENOMEM; | ||||||
| 
 | 
 | ||||||
|  | 	down_read(&gdev->sem); | ||||||
|  | 
 | ||||||
| 	/* Fail on open if the backing gpiochip is gone */ | 	/* Fail on open if the backing gpiochip is gone */ | ||||||
| 	if (!gdev->chip) | 	if (!gdev->chip) { | ||||||
| 		return -ENODEV; | 		ret = -ENODEV; | ||||||
|  | 		goto out_unlock; | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	cdev = kzalloc(sizeof(*cdev), GFP_KERNEL); | 	cdev = kzalloc(sizeof(*cdev), GFP_KERNEL); | ||||||
| 	if (!cdev) | 	if (!cdev) | ||||||
| 		return -ENOMEM; | 		goto out_unlock; | ||||||
| 
 | 
 | ||||||
| 	cdev->watched_lines = bitmap_zalloc(gdev->chip->ngpio, GFP_KERNEL); | 	cdev->watched_lines = bitmap_zalloc(gdev->chip->ngpio, GFP_KERNEL); | ||||||
| 	if (!cdev->watched_lines) | 	if (!cdev->watched_lines) | ||||||
|  | @ -2561,6 +2684,8 @@ static int gpio_chrdev_open(struct inode *inode, struct file *file) | ||||||
| 	if (ret) | 	if (ret) | ||||||
| 		goto out_unregister_notifier; | 		goto out_unregister_notifier; | ||||||
| 
 | 
 | ||||||
|  | 	up_read(&gdev->sem); | ||||||
|  | 
 | ||||||
| 	return ret; | 	return ret; | ||||||
| 
 | 
 | ||||||
| out_unregister_notifier: | out_unregister_notifier: | ||||||
|  | @ -2570,6 +2695,8 @@ static int gpio_chrdev_open(struct inode *inode, struct file *file) | ||||||
| 	bitmap_free(cdev->watched_lines); | 	bitmap_free(cdev->watched_lines); | ||||||
| out_free_cdev: | out_free_cdev: | ||||||
| 	kfree(cdev); | 	kfree(cdev); | ||||||
|  | out_unlock: | ||||||
|  | 	up_read(&gdev->sem); | ||||||
| 	return ret; | 	return ret; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -790,6 +790,7 @@ int gpiochip_add_data_with_key(struct gpio_chip *gc, void *data, | ||||||
| 	spin_unlock_irqrestore(&gpio_lock, flags); | 	spin_unlock_irqrestore(&gpio_lock, flags); | ||||||
| 
 | 
 | ||||||
| 	BLOCKING_INIT_NOTIFIER_HEAD(&gdev->notifier); | 	BLOCKING_INIT_NOTIFIER_HEAD(&gdev->notifier); | ||||||
|  | 	init_rwsem(&gdev->sem); | ||||||
| 
 | 
 | ||||||
| #ifdef CONFIG_PINCTRL | #ifdef CONFIG_PINCTRL | ||||||
| 	INIT_LIST_HEAD(&gdev->pin_ranges); | 	INIT_LIST_HEAD(&gdev->pin_ranges); | ||||||
|  | @ -924,6 +925,8 @@ void gpiochip_remove(struct gpio_chip *gc) | ||||||
| 	unsigned long	flags; | 	unsigned long	flags; | ||||||
| 	unsigned int	i; | 	unsigned int	i; | ||||||
| 
 | 
 | ||||||
|  | 	down_write(&gdev->sem); | ||||||
|  | 
 | ||||||
| 	/* FIXME: should the legacy sysfs handling be moved to gpio_device? */ | 	/* FIXME: should the legacy sysfs handling be moved to gpio_device? */ | ||||||
| 	gpiochip_sysfs_unregister(gdev); | 	gpiochip_sysfs_unregister(gdev); | ||||||
| 	gpiochip_free_hogs(gc); | 	gpiochip_free_hogs(gc); | ||||||
|  | @ -958,6 +961,7 @@ void gpiochip_remove(struct gpio_chip *gc) | ||||||
| 	 * gone. | 	 * gone. | ||||||
| 	 */ | 	 */ | ||||||
| 	gcdev_unregister(gdev); | 	gcdev_unregister(gdev); | ||||||
|  | 	up_write(&gdev->sem); | ||||||
| 	put_device(&gdev->dev); | 	put_device(&gdev->dev); | ||||||
| } | } | ||||||
| EXPORT_SYMBOL_GPL(gpiochip_remove); | EXPORT_SYMBOL_GPL(gpiochip_remove); | ||||||
|  |  | ||||||
|  | @ -15,6 +15,7 @@ | ||||||
| #include <linux/device.h> | #include <linux/device.h> | ||||||
| #include <linux/module.h> | #include <linux/module.h> | ||||||
| #include <linux/cdev.h> | #include <linux/cdev.h> | ||||||
|  | #include <linux/rwsem.h> | ||||||
| 
 | 
 | ||||||
| #define GPIOCHIP_NAME	"gpiochip" | #define GPIOCHIP_NAME	"gpiochip" | ||||||
| 
 | 
 | ||||||
|  | @ -39,6 +40,9 @@ | ||||||
|  * @list: links gpio_device:s together for traversal |  * @list: links gpio_device:s together for traversal | ||||||
|  * @notifier: used to notify subscribers about lines being requested, released |  * @notifier: used to notify subscribers about lines being requested, released | ||||||
|  *            or reconfigured |  *            or reconfigured | ||||||
|  |  * @sem: protects the structure from a NULL-pointer dereference of @chip by | ||||||
|  |  *       user-space operations when the device gets unregistered during | ||||||
|  |  *       a hot-unplug event | ||||||
|  * @pin_ranges: range of pins served by the GPIO driver |  * @pin_ranges: range of pins served by the GPIO driver | ||||||
|  * |  * | ||||||
|  * This state container holds most of the runtime variable data |  * This state container holds most of the runtime variable data | ||||||
|  | @ -60,6 +64,7 @@ struct gpio_device { | ||||||
| 	void			*data; | 	void			*data; | ||||||
| 	struct list_head        list; | 	struct list_head        list; | ||||||
| 	struct blocking_notifier_head notifier; | 	struct blocking_notifier_head notifier; | ||||||
|  | 	struct rw_semaphore	sem; | ||||||
| 
 | 
 | ||||||
| #ifdef CONFIG_PINCTRL | #ifdef CONFIG_PINCTRL | ||||||
| 	/*
 | 	/*
 | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue
	
	 Bartosz Golaszewski
						Bartosz Golaszewski