From 7b4ce0ab3dc33dfd1d8720719ac9a05b046b308b Mon Sep 17 00:00:00 2001 From: beatzaplenty Date: Mon, 27 Jul 2026 21:52:39 +1000 Subject: [PATCH] fix(server): prevent zfs-init-tank from wiping pool on udev race zpool create -f was called if `zpool import -d /dev/disk/by-id` failed, which could happen due to a race with systemd-udev-settle. The disk would then be visible by the time zpool create ran, silently destroying all data on an otherwise-intact pool. Fix: locate the data disk first, retry the import directly against it as a fallback, then check zdb -l for existing ZFS label metadata before concluding the disk is blank. Remove -f so zpool create refuses rather than overwrites if a pool is present. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01ULXzafSDwGhmFGnn3LtDSQ --- modules/build-types/server.nix | 35 +++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/modules/build-types/server.nix b/modules/build-types/server.nix index 53bc190..5455065 100644 --- a/modules/build-types/server.nix +++ b/modules/build-types/server.nix @@ -44,14 +44,8 @@ in exit 0 fi - # Pool exists on a device but not yet imported — let the standard - # zfs-import-${poolName}.service handle it normally. - if zpool import -d /dev/disk/by-id -N "${poolName}" 2>/dev/null; then - exit 0 - fi - - # No pool found at all. Create it on the Proxmox data disk (scsi1), - # which appears as /dev/disk/by-id/scsi-*drive-scsi1 inside the VM. + # Locate the data disk first — used for both the fallback import + # attempt and, only if the disk is genuinely blank, pool creation. DATA_DISK="" for candidate in /dev/disk/by-id/scsi-*drive-scsi1; do [[ "$candidate" == *-part* ]] && continue @@ -63,8 +57,31 @@ in exit 1 fi + # Try importing via the by-id symlink directory first (normal path), + # then fall back to scanning the disk directly. The two-step exists + # because of a udev race: systemd-udev-settle.service can clear before + # /dev/disk/by-id/ entries are fully populated, causing the first + # import to fail even when the pool is intact on the disk. + if zpool import -d /dev/disk/by-id -N "${poolName}" 2>/dev/null; then + exit 0 + fi + if zpool import -d "$DATA_DISK" -N "${poolName}" 2>/dev/null; then + exit 0 + fi + + # Both import attempts failed. Before creating a new pool, verify the + # disk is genuinely blank — if ZFS label metadata is present the import + # failed for some other reason and we must not clobber existing data. + if zdb -l "$DATA_DISK" 2>/dev/null | grep -q "name: '${poolName}'"; then + echo "zfs-init-${poolName}: $DATA_DISK has ZFS pool '${poolName}' metadata but import failed — refusing to overwrite existing data. Run 'zpool import -d $DATA_DISK ${poolName}' manually to investigate." >&2 + exit 1 + fi + + # Disk is genuinely blank: create the pool. -f is intentionally + # omitted so that if we somehow reach this point with an existing pool + # on the disk, zpool refuses rather than silently destroying data. echo "zfs-init-${poolName}: creating pool on $DATA_DISK" - zpool create -f "${poolName}" "$DATA_DISK" + zpool create "${poolName}" "$DATA_DISK" ${lib.concatMapStrings (ds: '' zfs create "${poolName}/${ds}" '') poolDatasets}