Skip to main content

max / alloy

install: do not offer an empty slot as a target An unconnected nbd node and a card reader with no card in it both report type "disk" at zero bytes, so both rendered as pickable rows. Selecting one means confirming a wipe and then watching bootc fail against nothing. Found on fw13, where the nbd module put fifteen of them in the picker. The card reader is the case that reaches real hardware. Filtered on size rather than by name: the name varies with whatever presents the empty slot, and no real target reports zero.
Co-Authored-By
Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-20 17:17 UTC
Signed with PGP, not checked
Commit: 05a91de20c3a64d48a8c796153a797b31828ed4c
Parent: f099d9b
1 file changed, +28 insertions, -1 deletion
@@ -886,7 +886,13 @@
886 886 Ok(parsed
887 887 .blockdevices
888 888 .into_iter()
889 - .filter(|device| device.kind == "disk" && !is_virtual(&device.name))
889 + // A zero-byte disk is a slot, not a disk: an unconnected nbd node, or a
890 + // card reader with no card in it. lsblk types both as `disk` and both
891 + // would otherwise render as a pickable row, so the user can confirm a
892 + // wipe and only find out it was nothing partway through the install.
893 + // Filtered on size rather than by name because the name varies with
894 + // whatever presents the empty slot, and no real target reports zero.
895 + .filter(|device| device.kind == "disk" && !is_virtual(&device.name) && device.size > 0)
890 896 .map(|device| {
891 897 let mut mountpoints = Vec::new();
892 898 mountpoints_of(&device, &mut mountpoints);
@@ -1562,6 +1568,27 @@
1562 1568 );
1563 1569 }
1564 1570
1571 + // An empty slot reports `type: "disk"` at zero bytes: an unconnected nbd
1572 + // node, or a card reader with nothing in it. Found on fw13, where loading
1573 + // the nbd module put fifteen of them in the picker. Offering one means the
1574 + // user confirms a wipe and the install fails partway through against
1575 + // nothing.
1576 + #[test]
1577 + fn an_empty_slot_is_not_offered_as_a_target() {
1578 + let raw = r#"{"blockdevices":[
1579 + {"name": "nbd0", "path": "/dev/nbd0", "size": 0, "type": "disk",
1580 + "rm": false, "ro": false, "mountpoints": [null]},
1581 + {"name": "sdb", "path": "/dev/sdb", "size": 61530439680, "type": "disk",
1582 + "rm": true, "ro": false, "mountpoints": [null]}
1583 + ]}"#;
1584 + let names: Vec<String> = parse_disks(raw)
1585 + .unwrap()
1586 + .into_iter()
1587 + .map(|disk| disk.name)
1588 + .collect();
1589 + assert_eq!(names, ["sdb"], "a zero-byte slot was offered as a target");
1590 + }
1591 +
1565 1592 #[test]
1566 1593 fn real_disks_survive_with_their_identifying_details() {
1567 1594 let disks = disks();