]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
loop-util: handle EAGAIN on LOOP_SET_STATUS64
authorLennart Poettering <lennart@poettering.net>
Wed, 26 Aug 2020 20:42:26 +0000 (22:42 +0200)
committerLennart Poettering <lennart@poettering.net>
Thu, 22 Oct 2020 12:58:27 +0000 (14:58 +0200)
Since
https://github.com/torvalds/linux/commit/5db470e229e22b7eda6e23b5566e532c96fb5bc3 (i.e. kernel 5.0)
changing the .lo_offset field via LOOP_SET_STATUS64 might result in
EAGAIN. Let's handle that.

Fixes: #16858
src/shared/loop-util.c

index 4843cbcaabc40467ce3d4924e2c8d0f5b6b9e7dd..156a41dd3a6e92e21359acadcb0a94f4d2ca65e4 100644 (file)
@@ -34,6 +34,7 @@ static void cleanup_clear_loop_close(int *fd) {
 }
 
 static int loop_configure(int fd, const struct loop_config *c) {
+        _cleanup_close_ int lock_fd = -1;
         int r;
 
         assert(fd >= 0);
@@ -84,15 +85,46 @@ static int loop_configure(int fd, const struct loop_config *c) {
                         return 0;
 
                 /* Otherwise, undo the attachment and use the old APIs */
-                (void) ioctl(fd, LOOP_CLR_FD);
+                if (ioctl(fd, LOOP_CLR_FD) < 0)
+                        return -errno;
         }
 
+        /* Since kernel commit 5db470e229e22b7eda6e23b5566e532c96fb5bc3 (kernel v5.0) the LOOP_SET_STATUS64
+         * ioctl can return EAGAIN in case we change the lo_offset field, if someone else is accessing the
+         * block device while we try to reconfigure it. This is a pretty common case, since udev might
+         * instantly start probing the device as soon as we attach an fd to it. Hence handle it in two ways:
+         * first, let's take the BSD lock that that ensures that udev will not step in between the point in
+         * time where we attach the fd and where we reconfigure the device. Secondly, let's wait 50ms on
+         * EAGAIN and retry. The former should be an efficient mechanism to avoid we have to wait 50ms
+         * needlessly if we are just racing against udev. The latter is protection against all other cases,
+         * i.e. peers that do not take the BSD lock.
+         *
+         * We take the BSD lock on a second, separately opened fd for the device. udev after all watches for
+         * close() events (specifically IN_CLOSE_WRITE) on block devices to reprobe them, hence by having a
+         * separate fd we will later close() we can ensure we trigger udev after everything is done. If we'd
+         * lock our own fd instead and keep it open for a long time udev would possibly never run on it
+         * again, even though the fd is unlocked, simply because we never close() it. It also has the nice
+         * benefit we can use the _cleanup_close_ logic to automatically release the lock, after we are
+         * done. */
+        lock_fd = fd_reopen(fd, O_RDWR|O_CLOEXEC|O_NONBLOCK|O_NOCTTY);
+        if (lock_fd < 0)
+                return lock_fd;
+
+        if (flock(lock_fd, LOCK_EX) < 0)
+                return -errno;
+
         if (ioctl(fd, LOOP_SET_FD, c->fd) < 0)
                 return -errno;
 
-        if (ioctl(fd, LOOP_SET_STATUS64, &c->info) < 0) {
-                r = -errno;
-                goto fail;
+        for (unsigned n_attempts = 0;;) {
+                if (ioctl(fd, LOOP_SET_STATUS64, &c->info) >= 0)
+                        break;
+                if (errno != EAGAIN || ++n_attempts >= 64) {
+                        r = log_debug_errno(errno, "Failed to configure loopback device: %m");
+                        goto fail;
+                }
+
+                (void) usleep(50 * USEC_PER_MSEC);
         }
 
         return 0;