Root Cause Analysis
1. Involved Files, Functions, Structs, and Fields
- Files:
- Functions:
- Structs and Fields:
2. Detailed Technical Mechanism
The block layer tracks a monotonically increasing 64-bit disk sequence number (disk->diskseq) to distinguish between different media instances of a block device (e.g. across media changes on removable drives or loop devices). Whenever a media change event occurs, inc_diskseq() allocates a new global sequence number and stores it into disk->diskseq:
void inc_diskseq(struct gendisk *disk)
{
disk->diskseq = atomic64_inc_return(&diskseq);
}
When notifying user space of device events, the kernel generates a uevent via kobject_uevent_env(), which invokes the block device's class callback block_uevent():
static int block_uevent(const struct device *dev, struct kobj_uevent_env *env)
{
const struct gendisk *disk = dev_to_disk(dev);
return add_uevent_var(env, "DISKSEQ=%llu", disk->diskseq);
}
In standard event polling (disk_check_events() in block/disk-events.c), inc_diskseq() is correctly called before disk_event_uevent():
if (events & DISK_EVENT_MEDIA_CHANGE)
inc_diskseq(disk);
if (disk->event_flags & DISK_EVENT_FLAG_UEVENT)
disk_event_uevent(disk, events);
This ordering guarantees that:
disk->diskseq is updated to the new sequence number before the uevent is prepared and broadcast.
- The uevent sent to user space contains the updated
DISKSEQ value.
However, in disk_force_media_change() (block/disk-events.c), which is invoked when tearing down or reconfiguring a device (such as __loop_clr_fd() when releasing a loop device), the ordering of operations is inverted:
void disk_force_media_change(struct gendisk *disk)
{
disk_event_uevent(disk, DISK_EVENT_MEDIA_CHANGE);
inc_diskseq(disk);
bdev_mark_dead(disk->part0, true);
}
3. How the Data Race and Stale Sequence Number Manifest
- Task 659 (CPU 0): A loop device is cleared in
__loop_clr_fd(), which calls disk_force_media_change(lo->lo_disk).
disk_force_media_change() first calls disk_event_uevent(disk, DISK_EVENT_MEDIA_CHANGE). Inside kobject_uevent_env(), block_uevent() is executed and reads disk->diskseq, capturing the old sequence number (e.g., 0x99b).
- The netlink uevent announcing
DISK_EVENT_MEDIA_CHANGE is transmitted with DISKSEQ=0x99b.
- Task 2978 (CPU 1,
udevd): Upon receiving the media change uevent, udevd immediately reads the device's sysfs /sys/devices/.../uevent attribute to refresh device information. Handling sysfs_kf_seq_show() -> uevent_show() -> dev_uevent() -> block_uevent() causes CPU 1 to read disk->diskseq.
- Simultaneously, CPU 0 returns from
disk_event_uevent() and executes line 299: inc_diskseq(disk). This performs an unsynchronized 8-byte write to disk->diskseq (0x99b -> 0x99c).
Because disk_event_uevent() signals user space before updating diskseq, user space is deterministically woken up to query sysfs concurrently with the write in inc_diskseq(). Additionally, accesses to disk->diskseq are unprotected plain loads/stores without atomic/concurrency accessors (e.g., READ_ONCE/WRITE_ONCE), leading to the reported KCSAN data race.