From: Daan De Meyer Date: Mon, 30 Mar 2026 08:43:38 +0000 (+0000) Subject: loop-util: work around kernel loop driver partition scan race X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=d3cb7a4e0fc95eaa0fefe1b750e3a2612aeee97f;p=thirdparty%2Fsystemd.git loop-util: work around kernel loop driver partition scan race The kernel loop driver has a race condition in LOOP_CONFIGURE when LO_FLAGS_PARTSCAN is set: it sends a KOBJ_CHANGE uevent (with GD_NEED_PART_SCAN set) before calling loop_reread_partitions(). If udev opens the device in response to the uevent before loop_reread_partitions() runs, the kernel's blkdev_get_whole() sees GD_NEED_PART_SCAN and triggers a first partition scan. Then loop_reread_partitions() runs a second scan that drops all partitions from the first scan (via blk_drop_partitions()) before re-adding them. This causes partition devices to briefly disappear (plugged -> dead -> plugged), which breaks systemd units with BindsTo= on the partition device: systemd observes the dead transition, fails the dependent units with 'dependency', and does not retry when the device reappears. Work around this in loop_device_make_internal() by splitting the loop device setup into two steps: first LOOP_CONFIGURE without LO_FLAGS_PARTSCAN, then LOOP_SET_STATUS64 to enable partscan. This avoids the race because: 1. LOOP_CONFIGURE without partscan: disk_force_media_change() sets GD_NEED_PART_SCAN, but GD_SUPPRESS_PART_SCAN remains set. If udev opens the device, blkdev_get_whole() calls bdev_disk_changed() which clears GD_NEED_PART_SCAN, but blk_add_partitions() returns early because disk_has_partscan() is false — no partitions appear, the flag is drained harmlessly. 2. Between the two ioctls, we open and close the device to ensure GD_NEED_PART_SCAN is drained regardless of whether udev processed the uevent yet. 3. LOOP_SET_STATUS64 with LO_FLAGS_PARTSCAN: clears GD_SUPPRESS_PART_SCAN and calls loop_reread_partitions() for a single clean scan. Crucially, loop_set_status() does not call disk_force_media_change(), so GD_NEED_PART_SCAN is never set again. A proper kernel fix has been submitted: https://lore.kernel.org/linux-block/20260330081819.652890-1-daan@amutable.com/T/#u This workaround should be dropped once the fix is widely available. Co-developed-by: Claude Opus 4.6 --- diff --git a/src/shared/loop-util.c b/src/shared/loop-util.c index fbed79bb2ec..4a759f4a7e2 100644 --- a/src/shared/loop-util.c +++ b/src/shared/loop-util.c @@ -541,12 +541,26 @@ static int loop_device_make_internal( return r; } + /* Strip LO_FLAGS_PARTSCAN from LOOP_CONFIGURE and enable it afterwards via + * LOOP_SET_STATUS64 to work around a kernel race: LOOP_CONFIGURE sends a uevent with + * GD_NEED_PART_SCAN set before calling loop_reread_partitions(). If udev opens the device in + * response, blkdev_get_whole() triggers a first scan, then loop_reread_partitions() does a + * second scan that briefly drops all partitions. By configuring without partscan, + * GD_SUPPRESS_PART_SCAN stays set, making any concurrent open harmless. LOOP_SET_STATUS64 + * doesn't call disk_force_media_change() so it doesn't set GD_NEED_PART_SCAN. + * + * See: https://lore.kernel.org/linux-block/20260330081819.652890-1-daan@amutable.com/T/#u + * Drop this workaround once the kernel fix is widely available. */ + bool deferred_partscan = FLAGS_SET(loop_flags, LO_FLAGS_PARTSCAN); + config = (struct loop_config) { .fd = fd, .block_size = sector_size, .info = { /* Use the specified flags, but configure the read-only flag from the open flags, and force autoclear */ - .lo_flags = (loop_flags & ~LO_FLAGS_READ_ONLY) | ((open_flags & O_ACCMODE_STRICT) == O_RDONLY ? LO_FLAGS_READ_ONLY : 0) | LO_FLAGS_AUTOCLEAR, + .lo_flags = ((loop_flags & ~(LO_FLAGS_READ_ONLY|LO_FLAGS_PARTSCAN)) | + ((open_flags & O_ACCMODE_STRICT) == O_RDONLY ? LO_FLAGS_READ_ONLY : 0) | + LO_FLAGS_AUTOCLEAR), .lo_offset = offset, .lo_sizelimit = size == UINT64_MAX ? 0 : size, }, @@ -638,6 +652,24 @@ static int loop_device_make_internal( } } + if (deferred_partscan) { + /* Open+close to drain GD_NEED_PART_SCAN harmlessly (GD_SUPPRESS_PART_SCAN is still + * set so no partitions appear). Then enable partscan via LOOP_SET_STATUS64. */ + int tmp_fd = fd_reopen(d->fd, O_RDONLY|O_CLOEXEC|O_NONBLOCK); + if (tmp_fd < 0) + return log_debug_errno(tmp_fd, "Failed to reopen loop device to drain partscan flag: %m"); + safe_close(tmp_fd); + + struct loop_info64 info; + if (ioctl(d->fd, LOOP_GET_STATUS64, &info) < 0) + return log_debug_errno(errno, "Failed to get loop device status: %m"); + + info.lo_flags |= LO_FLAGS_PARTSCAN; + + if (ioctl(d->fd, LOOP_SET_STATUS64, &info) < 0) + return log_debug_errno(errno, "Failed to enable partscan on loop device: %m"); + } + d->backing_file = TAKE_PTR(backing_file); d->backing_inode = st.st_ino; d->backing_devno = st.st_dev;