I looked into this issue today, and wanted to make a post to record the findings and the gory details of the search that Claude did for me.
The issue I was investigating was that i originally created a hot spare for one of my 2TB RAIDZ1 pools by specifying a disk/by-id path for it, and what is different about this one is that this is a partition and not a full disk.
It worked fine but when i look at zpool status the disk specification just showed sdd2, and that set my alarm bells
ringing because a bad thing could theoretically happen if:
/dev/sddHere is the transcript:
Claude Sonnet 4.6
● Agent(Research ZFS partition vs whole-disk by-id behavior)
⎿ Prompt:
Research and confirm whether ZFS on Linux (OpenZFS) has a known limitation where whole disks get stored/displayed with stable by-id names in zpool status, but partitions get stored/displayed with raw device names (like sdd2) even when added via /dev/disk/by-id/ paths.
Specifically I want to know:
1. Is it confirmed that ZFS treats whole disks differently from partitions when resolving/storing device paths?
2. Does ZFS do a reverse lookup to find by-id names for whole disks but not for partitions?
3. Is vdev_id.conf the recommended solution for giving partitions stable names in ZFS?
4. Any other relevant details about this behavior.
Search OpenZFS documentation, GitHub issues, mailing lists, forums (like Reddit r/zfs, ArchWiki, etc). Report what you find with sources.
⎿ Bash(find . -name "*.md" -o -name "*.txt" -o -name "*.sh" | head -20 && ls)
⎿ Bash(curl -s "https://openzfs.github.io/openzfs-docs/man/8/zpool.8.html" 2>/dev/null | grep -i "by-id\|partition\|disk\|vdev_id\|udev\|symlink" | head -40)
⎿
⎿ Web Search("OpenZFS partition vs whole disk by-id path storage zpool status vdev names")
⎿ Web Search("ZFS whole disk vs partition udev symlink reverse lookup by-id devname")
⎿ Fetch(url: "https://utcc.utoronto.ca/~cks/space/blog/linux/ZFSOnLinuxDiskNames", prompt: "Extract all details about how ZFS on Linux names disks, especially differences between whole disks and partitions, by-id paths, and reverse lookup behavior")
⎿ Web Search("OpenZFS zfs_vdev_udev_post_change partition "by-id" stored path whole disk different treatment source code")
⎿ Fetch(url: "https://github.com/openzfs/zfs/blob/master/cmd/zpool/zpool_vdev.c", prompt: "Find and extract all code or comments that describe how ZFS handles whole disk paths differently from partition paths, especially anything about by-id lookup, wholedisk property, udev symlink resolution, or how device paths are stored vs displayed")
⎿ Web Search("OpenZFS "wholedisk" property partition path stored raw devname "by-id" not shown zpool status")
⎿ Fetch(url: "https://github.com/openzfs/zfs/issues/14559", prompt: "Extract all relevant technical details about how ZFS handles whole disk vs partition device paths, especially differences in by-id path storage/display, and any mention of the wholedisk property affecting path resolution")
⎿ Web Search("OpenZFS partition added "by-id" shows as "sdd2" zpool status whole disk shows by-id name")
⎿ Fetch(url: "https://github.com/openzfs/zfs/issues/2844", prompt: "Extract all technical details about how ZFS handles whole disk vs partition device paths, especially differences in path storage/display, and any mention of by-id paths not being shown for partitions")
⎿ Web Search("OpenZFS zpool_vdev.c "wholedisk" udev lookup "by-id" partition path not resolved")
⎿ Web Search("ZFS Linux partition "by-id" path lost shows raw device name zpool status reddit r/zfs")
⎿ Fetch(url: "https://openzfs.github.io/openzfs-docs/man/master/8/vdev_id.8.html", prompt: "Extract all details about vdev_id.conf purpose, how it handles partitions vs whole disks, and whether it is recommended for giving partitions stable names in ZFS")
⎿ Fetch(url: "https://github.com/openzfs/zfs/issues/2944", prompt: "Extract all technical details about why ZFS reverts to /dev/sdx names from by-id paths, whether this affects partitions differently from whole disks, and any explanation of the underlying mechanism")
⎿ Web Search("OpenZFS "whole_disk" flag "by-id" lookup udev symlink path resolution partition not resolved stable name")
⎿ Fetch(url: "https://wiki.archlinux.org/title/ZFS", prompt: "Extract all details about how ZFS handles whole disk vs partition device naming, by-id paths, vdev_id.conf recommendations, and any differences in path storage/display between whole disks and partitions in zpool status")
⎿ Web Search("site:reddit.com r/zfs partition "by-id" shows sdX raw name whole disk shows by-id path zpool status")
⎿ Web Search("OpenZFS zpool status partition vdev shown as raw device name not by-id path unlike whole disk")
⎿ Fetch(url: "https://openzfs.github.io/openzfs-docs/Project%20and%20Community/FAQ.html", prompt: "Extract all sections about device naming, by-id paths, whole disk vs partition handling, and how zpool status displays device names")
⎿ Fetch(url: "https://github.com/openzfs/zfs/discussions/14864", prompt: "Extract all technical details about inconsistent vdev naming between whole disks and partitions, especially whether partitions added via by-id paths show as raw device names (like sdd2) in zpool status while whole disks show by-id names")
⎿ Fetch(url: "https://github.com/openzfs/zfs/issues/13170", prompt: "Extract all details about how persistent device names are lost for cache/partition devices after export/import, and whether this is different behavior from whole disks")
⎿ Web Search("OpenZFS libzfs vdev path lookup udev "whole_disk" "ZPOOL_CONFIG_WHOLE_DISK" by-id symlink find partition")
⎿ Fetch(url: "https://github.com/openzfs/zfs/blob/master/cmd/zpool/zpool_vdev.c", prompt: "Find the specific code that handles path resolution for whole disk vs partition vdevs - specifically look for any code that does a udev reverse lookup or by-id name resolution for whole disks that does not apply to partitions. Look for the make_leaf_vdev function, zfs_dev_is_whole_disk, and any udev path lookup logic.")
⎿ Fetch(url: "https://fossies.org/linux/zfs/cmd/zpool/zpool_vdev.c", prompt: "Find the exact code that handles udev path lookup or by-id name resolution for whole disks. Specifically look for: 1) any function that searches /dev/disk/by-id for a device path 2) any code where whole_disk=1 triggers a different path lookup 3) the make_leaf_vdev function and its handling of path storage 4) any comment explaining why
⎿ Error: Request failed with status code 403
⎿ Bash(curl -s "https://raw.githubusercontent.com/openzfs/zfs/master/cmd/zpool/zpool_vdev.c" | grep -n "by-id\|by_id\|udev\|whole_disk\|WHOLE_DISK\|partition\|udev_path\|UDISK\|devid" | head -60)
⎿ Bash(curl -s "https://raw.githubusercontent.com/openzfs/zfs/master/cmd/zpool/zpool_vdev.c" | sed -n '280,410p')
⎿ Bash(curl -s "https://raw.githubusercontent.com/openzfs/zfs/master/cmd/zpool/zpool_vdev.c" | sed -n '140,200p')
⎿ Bash(curl -s "https://raw.githubusercontent.com/openzfs/zfs/master/cmd/zpool/zpool_vdev.c" | sed -n '1090,1180p')
⎿ Bash(curl -s "https://raw.githubusercontent.com/openzfs/zfs/master/lib/libzfs/libzfs_pool.c" | grep -n "by-id\|by_id\|udev\|whole_disk\|WHOLE_DISK\|update_vdev_config_dev_strs\|udev_encode_string\|devid" | head -50)
⎿ Bash(curl -s "https://raw.githubusercontent.com/openzfs/zfs/master/lib/libzfs/libzfs_pool.c" | sed -n '4600,4700p')
⎿ Bash(curl -s "https://raw.githubusercontent.com/openzfs/zfs/master/lib/libzfs/libzfs_pool.c" | grep -n "update_vdev_config_dev_strs\|zfs_strip_path\|zfs_strip_partition" | head -20)
⎿ Bash(curl -s "https://raw.githubusercontent.com/openzfs/zfs/master/lib/libzfs/libzfs_util.c" | grep -n "update_vdev_config_dev_strs\|devid\|by-id\|udev\|UDISK" | head -30)
⎿ Bash(curl -s "https://raw.githubusercontent.com/openzfs/zfs/master/lib/libzfs/libzfs_pool.c" | grep -n "update_vdev_config_dev_strs" | head -10)
⎿ Bash(curl -s "https://raw.githubusercontent.com/openzfs/zfs/master/cmd/zpool/zpool_vdev.c" | grep -n "update_vdev_config_dev_strs" | head -10)
⎿ Bash(grep -r "update_vdev_config_dev_strs" /usr/src/ 2>/dev/null | head -5
⎿ Bash(find /usr/src/zfs-2.4.1 -name "*.c" | xargs grep -l "update_vdev_config_dev_strs" 2>/dev/null)
⎿ Error: Exit code 123
⎿ Bash(curl -s "https://raw.githubusercontent.com/openzfs/zfs/master/lib/libzutil/os/linux/zutil_import_os.c" | grep -n "update_vdev_config_dev_strs\|by-id\|udev\|UDISK\|devid\|whole_disk\|WHOLE_DISK" | head -40)
⎿ Bash(curl -s "https://raw.githubusercontent.com/openzfs/zfs/master/lib/libzutil/os/linux/zutil_import_os.c" | grep -n "update_vdev_config_dev_strs" | head -10)
⎿ Bash(curl -s "https://raw.githubusercontent.com/openzfs/zfs/master/lib/libzutil/os/linux/zutil_import_os.c" | sed -n '840,960p')
⎿ Bash(curl -s "https://raw.githubusercontent.com/openzfs/zfs/master/lib/libzutil/os/linux/zutil_import_os.c" | grep -n "encode_device_strings" | head -10)
⎿ Bash(curl -s "https://raw.githubusercontent.com/openzfs/zfs/master/lib/libzutil/os/linux/zutil_import_os.c" | sed -n '696,790p')
⎿ Bash(curl -s "https://raw.githubusercontent.com/openzfs/zfs/master/lib/libzutil/os/linux/zutil_import_os.c" | sed -n '155,230p')
⎿ Web Search("OpenZFS vdev_id.conf partition alias support "part" by-vdev stable name recommendation")
⎿ Fetch(url: "https://openzfs.github.io/openzfs-docs/man/master/8/vdev_id.8.html", prompt: "Extract all content about how vdev_id.conf handles partitions, whether it supports partition aliases, the ALIAS section, and any recommendation about using it for partition stable names")
⎿ Fetch(url: "https://github.com/openzfs/zfs/issues/3859", prompt: "Extract all technical details about why partition aliases don't work in vdev_id.conf, what the root cause is (udev DEVTYPE matching disk vs partition), and what workarounds or fixes are proposed")
⎿ Fetch(url: "https://openzfs.github.io/openzfs-docs/man/master/5/vdev_id.conf.5.html", prompt: "Extract all content about aliases for partitions, the ALIAS keyword, and whether vdev_id.conf supports giving stable names to partitions vs whole disks")
⎿ Bash(curl -s "https://raw.githubusercontent.com/openzfs/zfs/master/udev/rules.d/69-vdev.rules.in" 2>/dev/null || curl -s "https://raw.githubusercontent.com/openzfs/zfs/master/udev/rules.d/69-vdev.rules" 2>/dev/null)
⎿ Bash(# Check git blame/log to see when partition support was added to 69-vdev.rules
⎿ Bash(curl -s "https://raw.githubusercontent.com/openzfs/zfs/master/lib/libzutil/os/linux/zutil_import_os.c" | sed -n '386,465p')
⎿ Fetch(url: "https://manpages.debian.org/testing/zfsutils-linux/vdev_id.conf.5.en.html", prompt: "Extract the full ALIAS section documentation and any notes about partition support, especially whether aliases work for both whole disks and partitions")
Response: I now have comprehensive source-level evidence. Here is a thorough answer to all four questions.
Findings: ZFS Whole Disk vs. Partition Path Handling on Linux (OpenZFS)
All four of your questions can be answered definitively, with confirmation from the OpenZFS source code itself.
Yes, confirmed. The distinction is fundamental and baked into the design. When a device is added to a pool, make_leaf_vdev() in cmd/zpool/zpool_vdev.c calls zfs_dev_is_whole_disk() and records the result as ZPOOL_CONFIG_WHOLE_DISK (a uint64 flag) in the vdev's NVList config. This flag drives completely different downstream behavior throughout the codebase.
The relevant comment from the source at line 1094-1097:
/*
* Update the path to refer to the partition. The presence of
* the 'whole_disk' field indicates to the CLI that we should
* chop off the partition number when displaying the device in
* future output.
*/
When wholedisk=1, after ZFS creates the EFI partition automatically (e.g., sdd → sdd1), it stores the partition path internally but the CLI strips the partition suffix back off for display. For partitions you added manually (e.g., sdd2), wholedisk=0, and the path is stored and displayed as-is.
Yes, confirmed, and this is the exact mechanism behind the behavior you described. The function responsible is encode_device_strings() in lib/libzutil/os/linux/zutil_import_os.c. It is called from update_vdev_config_dev_strs(), which runs at pool create, add, attach, and import time. The critical gating check (lines 743-744):
/*
* Only whole disks require extra device strings
*/
if (!wholedisk && !udev_mpath_whole_disk(dev))
goto no_dev;
For whole disks, encode_device_strings() uses libudev to query the device node and calls zfs_device_get_devid(), which walks udev's devlinks list to find the by-id symlink (preferring the bus-based ata-, wwn-, nvme- style names). This devid string is then stored in the vdev config label as ZPOOL_CONFIG_DEVID.
For partitions, this entire block is skipped. No devid string is stored, so no by-id name is associated with the vdev. That is why:
The devid is also used during pool import scanning (zutil_import_os.c lines 177-208) to construct a preferred /dev/disk/by-id/
Yes, it is the official mechanism, and it now works for partitions (after a fix landed in 2016). However, there is a historical gotcha worth knowing:
The original bug (Issue #3859): Prior to OpenZFS commit dabb6f4f (PR #5266, merged October 2016), the udev rule 69-vdev.rules only triggered the vdev_id script for DEVTYPE=="disk". Partitions have DEVTYPE=="partition" and were silently ignored, so aliases in vdev_id.conf simply had no effect for partition-based pools.
The fix: The current 69-vdev.rules.in now has both lines:
ENV{DEVTYPE}=="disk", IMPORT{program}="@udevdir@/vdev_id -d %k"
ENV{DEVTYPE}=="partition", IMPORT{program}="@udevdir@/vdev_id -d %k"
And the symlink rule for partitions appends -part%n:
KERNEL=="*[0-9]", ENV{SUBSYSTEM}=="block", ENV{DEVTYPE}=="partition", ENV{ID_VDEV}=="?*", SYMLINK+="$env{ID_VDEV_PATH}-part%n"
How to use it: In /etc/zfs/vdev_id.conf, define an alias pointing to the by-id base name (without the -part2 suffix); udev will append the partition number automatically. Then create the pool using the /dev/disk/by-vdev/
Why ZFS was designed this way: The devid / by-id lookup was built for the "whole disk" use case because ZFS owns the entire disk including the label and can reliably store and retrieve that devid. For partitions, ZFS doesn't control the rest of the disk structure and cannot make the same assumptions.
The ZPOOL_CONFIG_PATH is stored as given at creation time. A comment in make_leaf_vdev() at line 294 says explicitly: "we are careful to store the given path as ZPOOL_CONFIG_PATH to ensure we can leverage udev's persistent device labels." So if you give it a by-id path for a partition at creation time, it stores that by-id path. The problem is that on re-import without a devid in the label, ZFS may fall back to the raw kernel device name if the original by-id symlink is not the first match during import scanning.
The zpool status -L flag resolves all symlinks to their real /dev/sdX paths, while the default view shows stored names. The -P flag shows full paths instead of just the last component.
Issue #13170 (open since 2022) documents the specific behavior that cache and log devices (which are often partitions) lose their persistent names after pool export/import, reverting to raw kernel names — and there is currently no good way to fix this without re-adding the device, confirming this is a known, unfixed rough edge for non-whole-disk vdevs.
The autoreplace feature (Issue #14559) also only works with by-path (not by-id) names, and only for whole disks, because it requires the replacement device to appear at the same path as the original — another area where partitions are second-class citizens.
Sources:
● Confirmed — your instinct was exactly right, and the behavior is well-documented in the OpenZFS source.
The core reason: In lib/libzutil/os/linux/zutil_import_os.c, the function that does the by-id reverse lookup (encode_device_strings()) has an explicit guard:
/*
* Only whole disks require extra device strings
*/
if (!wholedisk && !udev_mpath_whole_disk(dev))
goto no_dev;
Partitions skip this entirely. So no devid is stored in the vdev label, no by-id name is associated, and on re-import ZFS falls back to the raw kernel device name (sdd2).
Whole disks get a full udev reverse lookup to find the ata-/wwn- style name and store it permanently. Partitions don't — they're second-class citizens by design.
vdev_id.conf is the official fix. It does work for partitions (a bug where it was silently ignored for partitions was fixed in 2016 via openzfs/zfs#5266). The setup:
Add an alias in /etc/zfs/vdev_id.conf:
alias spare0 /dev/disk/by-id/ata-WDC_WD80EDBZ-<redacted>
udev will auto-create /dev/disk/by-vdev/spare0-part2
Remove and re-add the spare using that path:
sudo zpool remove tank sdd2
sudo zpool add tank spare /dev/disk/by-vdev/spare0-part2
Also worth knowing: openzfs/zfs#13170 (open since 2022) documents this exact problem for cache/log/spare partition vdevs losing their names after export/import — it's a known unfixed rough edge.
✻ Sautéed for 5m 24s
I created this blog post because I wanted to comment in a zfs github issue (I'll also say, this issue I credit to Claude who found it for me) but I did not want to pollute the discussion with a huge AI agent wall of text. I hope it helps. I'm not going to do such a large quantity of research manually, and you can even see that I opted to have this done by Sonnet rather than Opus just to save usage consumption. I am impressed with the depth to which it did research.
The above is not an ideal dump of the session but I think it's enough to illustrate the point. All I did was maximize the terminal width and copy it straight out of the terminal where this was running in claude code. This way I can get a significant amount of the lines of the tool call traces, but the long ones of those are still truncated. I also had to do some surgery on the formatting because it's not all clean markdown: There are commands and stuff in the tool call traces that were not marked as code for markdown purposes so I had to wrap it in a big code block, etc.
I did look into things like the /export function built in to claude code but that actually completely makes no attempt
to render clean markdown either (and also completely omits the tool call details).