From 23fbe5a23eeec7b4fd0629fd1e1fad5fab47e551 Mon Sep 17 00:00:00 2001 From: Daan De Meyer Date: Tue, 31 Mar 2026 12:38:05 +0200 Subject: [PATCH] loop: add regression test for partscan double-scan race Add a stress test that detects spurious partition removal events when setting up a loop device with partscan enabled. The kernel bug was that disk_force_media_change() set GD_NEED_PART_SCAN, causing udev's device open to trigger a partition scan racing with the explicit scan from loop_reread_partitions(). The second scan would drop and re-add all partitions, making partition devices briefly disappear. The test monitors kernel uevents while repeatedly setting up and tearing down a loop device with partscan. Each cycle should produce exactly one add and one remove uevent for the partition device. Extra events indicate the double-scan race was triggered. Link: https://lore.kernel.org/linux-block/20260330081819.652890-1-daan@amutable.com/T/#u Signed-off-by: Daan De Meyer Co-Authored-By: Shinichiro Kawasaki --- common/rc | 16 ++++++++++ tests/loop/012 | 77 ++++++++++++++++++++++++++++++++++++++++++++++ tests/loop/012.out | 2 ++ 3 files changed, 95 insertions(+) create mode 100755 tests/loop/012 create mode 100644 tests/loop/012.out diff --git a/common/rc b/common/rc index 0a8caca0..28fc2ad6 100644 --- a/common/rc +++ b/common/rc @@ -603,6 +603,22 @@ _systemd_start_udevd() { return 0 } +_have_systemd_ver() { + local required_ver=${1} + local ver + + if ! command -v systemctl &>/dev/null; then + SKIP_REASONS+=("systemd is not available") + return 1 + fi + + ver=$(systemctl --version | head -1 | sed 's/systemd //;s/[^0-9].*//') + if ((ver < required_ver)); then + SKIP_REASONS+=("systemd version is older than ${required_ver}") + return 1 + fi +} + # Run the given command as NORMAL_USER _run_user() { su "$NORMAL_USER" -c "$1" diff --git a/tests/loop/012 b/tests/loop/012 new file mode 100755 index 00000000..b156997f --- /dev/null +++ b/tests/loop/012 @@ -0,0 +1,77 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-3.0+ +# Copyright (C) 2026 Daan De Meyer +# +# Regression test for a race between udev and loop_reread_partitions(). +# +# When LOOP_CONFIGURE is called with LO_FLAGS_PARTSCAN, +# disk_force_media_change() used to set GD_NEED_PART_SCAN before the +# uevent was sent. When udev opened the device in response, +# blkdev_get_whole() would trigger a partition scan, and then +# loop_reread_partitions() would scan again. The second scan drops all +# partitions from the first scan before re-adding them, causing +# partition devices to briefly disappear. +# +# Verify that setting up a loop device with partscan does not produce +# spurious partition add/remove events. + +. tests/loop/rc + +DESCRIPTION="check for spurious partition removal when partscan is enabled" +TIMED=1 + +requires() { + _have_program sfdisk + _have_systemd_ver 259 +} + +test() { + echo "Running ${TEST_NAME}" + + truncate -s 3MiB "$TMPDIR/img" + sfdisk "$TMPDIR/img" >"$FULL" 2>&1 <<-EOF + label: gpt + size=1MiB + EOF + + local dev + dev="$(losetup -f)" + + # Monitor kernel uevents for partition block devices. + udevadm monitor --kernel -s block/partition >"$TMPDIR/uevents" 2>&1 & + local mon_pid=$! + # Give the monitor time to set up its netlink socket. + sleep 0.5 + + local iterations=0 + SECONDS=0 + while ((SECONDS < "${TIMEOUT:-5}")); do + if ! losetup -P "$dev" "$TMPDIR/img" 2>>"$FULL"; then + continue + fi + losetup -d "$dev" 2>>"$FULL" + ((iterations++)) + done + + sleep 0.5 + kill "$mon_pid" + wait "$mon_pid" 2>/dev/null + + # Each setup+teardown cycle should produce exactly one add and one + # remove kernel uevent for the partition device. If the race + # triggers, a second partition scan produces an extra remove+add + # pair, inflating the counts beyond the number of iterations. + local name="${dev##*/}" + local adds removes + adds=$(grep -c "^KERNEL\[.*\] add.*${name}p" "$TMPDIR/uevents") + removes=$(grep -c "^KERNEL\[.*\] remove.*${name}p" "$TMPDIR/uevents") + + if ((adds > iterations)); then + echo "Fail: $iterations iterations but $adds add events (expected $iterations)" + fi + if ((removes > iterations)); then + echo "Fail: $iterations iterations but $removes remove events (expected $iterations)" + fi + + echo "Test complete" +} diff --git a/tests/loop/012.out b/tests/loop/012.out new file mode 100644 index 00000000..30dbd1bd --- /dev/null +++ b/tests/loop/012.out @@ -0,0 +1,2 @@ +Running loop/012 +Test complete