return -ENOMEM;
r = worker_lock_block_device(dev, &fd_lock);
+ if (r == -EAGAIN) {
+ /* So this is a block device and the device is locked currently via the BSD advisory locks —
+ * someone else is exclusively using it. This means we don't run our udev rules now, to not
+ * interfere. However we want to know when the device is unlocked again, and retrigger the
+ * device again then, so that the rules are run eventually. For that we use IN_CLOSE_WRITE
+ * inotify watches (which isn't exactly the same as waiting for the BSD locks to release, but
+ * not totally off, as long as unlock+close() is done together, as it usually is).
+ *
+ * (The user-facing side of this: https://systemd.io/BLOCK_DEVICE_LOCKING)
+ *
+ * There's a bit of a chicken and egg problem here for this however: inotify watching is
+ * supposed to be enabled via an option set via udev rules (OPTIONS+="watch"). If we skip the
+ * udev rules here however (as we just said we do), we would thus never see that specific
+ * udev rule, and thus never turn on inotify watching. But in order to catch up eventually
+ * and run them we we need the inotify watching: hence a classic chicken and egg problem.
+ *
+ * Our way out here: if we see the block device locked, unconditionally watch the device via
+ * inotify, regardless of any explicit request via OPTIONS+="watch". Thus, a device that is
+ * currently locked via the BSD file locks will be treated as if we ran a single udev rule
+ * only for it: the one that turns on inotify watching for it. If we eventually see the
+ * inotify IN_CLOSE_WRITE event, and then run the rules after all and we then realize that
+ * this wasn't actually requested (i.e. no OPTIONS+="watch" set) we'll simply turn off the
+ * watching again (see below). Effectively this means: inotify watching is now enabled either
+ * a) when the udev rules say so, or b) while the device is locked.
+ *
+ * Worst case scenario hence: in the (unlikely) case someone locked the device and we clash
+ * with that we might do inotify watching for a brief moment for a device where we actually
+ * weren't supposed to. But that shouldn't be too bad, in particular as BSD locks being taken
+ * on a block device is kinda an indication that the inotify logic is desired too, to some
+ * degree — they go hand-in-hand after all. */
+
+ log_device_debug(dev, "Block device is currently locked, installing watch to wait until the lock is released.");
+ (void) udev_watch_begin(dev);
+
+ /* Now the watch is installed, let's lock the device again, maybe in the meantime things changed */
+ r = worker_lock_block_device(dev, &fd_lock);
+ }
if (r < 0)
return r;
/* in case rtnl was initialized */
manager->rtnl = sd_netlink_ref(udev_event->rtnl);
- /* apply/restore inotify watch */
+ /* apply/restore/end inotify watch */
if (udev_event->inotify_watch) {
(void) udev_watch_begin(dev);
r = device_update_db(dev);
if (r < 0)
return log_device_debug_errno(dev, r, "Failed to update database under /run/udev/data/: %m");
- }
+ } else
+ (void) udev_watch_end(dev);
log_device_debug(dev, "Device (SEQNUM=%"PRIu64", ACTION=%s) processed",
seqnum, device_action_to_string(action));