Repository navigation
Releases: click0/zvolrescue
Release list
zvolrescue v0.9.10 — The catalogue closed
zvolrescue v0.9.10 — read-only extraction of zvols from ZFS pools that no longer import: labels, uberblocks, mirror/RAIDZ/dRAID reconstruction, every checksum and compression, encrypted datasets with a key.
0.x release. Validated in CI against OpenZFS 2.2 userland pools (
ztest+zdb) and on the environments listed in docs/REALWORLD-TESTS.md; treat the output as evidence to verify, not as a finished recovery.
Downloads
15 binaries: 5 programs, each built for the same three platforms. The name is always <program>-0.9.10-<platform>.
| Program | What it is for |
|---|---|
zvolrescue |
the pool, and a volume out of it: what the members hold, and the image of a zvol |
zvoltimeline |
the pool history the transaction groups still carry: when a dataset was made, destroyed, and whether its space is still held |
zvolcarve |
volumes no uberblock points at any more, found by scanning raw vdev space |
zvolfiles |
files out of filesystem datasets: the tree with its metadata, or a raw object dump when the metadata is gone |
zvolreport |
the evidence logs of the others as one document, hashed, signed and checkable by a third party |
| Platform | <platform> |
Notes |
|---|---|---|
| Linux x86_64 | x86_64-linux-musl |
static; runs on any distribution, and on a rescue medium |
| Linux aarch64 | aarch64-linux-musl |
static |
| FreeBSD 15.x amd64 | amd64-freebsd |
static (FreeBSD's own libc linked in); runs on any 15.x system. Built on 15.x, whose system-call ABI it uses; the real-kernel matrix also runs it on FreeBSD 14.5 and on the mfsBSD 14.2 rescue image, but on 14.x building from source stays the supported way: pkg install rust && cargo build --release |
So the carver for Linux x86_64 is zvolcarve-0.9.10-x86_64-linux-musl, and the FreeBSD main binary is zvolrescue-0.9.10-amd64-freebsd.
On Debian and Ubuntu, zvolrescue_0.9.10_amd64.deb and zvolrescue_0.9.10_arm64.deb install the same five static binaries with their manual pages: sudo apt install ./zvolrescue_0.9.10_amd64.deb, then man zvolrescue.
SHA256SUMS covers all 15 binaries and the 2 packages, and SHA256SUMS.sigstore.json is its signature — made by this release workflow at this tag, keylessly, through Sigstore:
sha256sum -c SHA256SUMS # or, on FreeBSD: sha256 -c SHA256SUMS
cosign verify-blob --bundle SHA256SUMS.sigstore.json \
--certificate-identity https://github.com/click0/zvolrescue/.github/workflows/release.yml@refs/tags/v0.9.10 \
--certificate-oidc-issuer https://token.actions.githubusercontent.com SHA256SUMS
gh attestation verify zvolrescue-0.9.10-x86_64-linux-musl --repo click0/zvolrescue # provenance of any one binaryNo installation: chmod +x and run. Every program opens the members read-only and never writes to the pool.
What is in v0.9.10
The catalogue closed.
SPEC §9.1's damage catalogue is covered end to end. The golden image is
published from the run that built it, nobody carrying 2.5 GB through a
browser, and the matrix runs against the published image, pinned by
its sums, on every push that asks and every Monday on its own. The
shapes the manifests named and no image could serve are placed: a
member replaced by an older copy of itself, from the round-6 copies the
release carries; a block's copy on the dRAID, found by content rather
than arithmetic; the dnode blocks, indirect blocks, data blocks and
crypto key object below a dataset's object set, from the oracle's
zdb -dddddd capture. The last two classes without a manifest have
theirs — partial rings with a forged txg, a partition re-created with
another size — and the one case this image could not serve runs now:
61 manifests, 61 pass, none n/a, no defects, inputs unchanged. The
forged txg found a gap, and it is fixed: list passes over a verified
TXG with nothing behind it the way dump passes over one that names a
dataset it cannot read, and dump's record says when it did.
Added
- The golden image is published from the run that built it.
publish-golden.ymltakes the number of arealworld.ymlrun, downloads
itsgolden-<tag>artifact where it already is, checks the sums and
that no run of the matrix fell outside its expected category, and
publishes the image inzvolrescue-testdata: the members into the
release<tag>(created as a draft, filled, checked, published — or
filled if the tag was made from the web first), the oracle into
oracle/<pool>/,IMAGE.mdand the matrix report into
reports/<tag>/, in one commit. Nobody carries the 2.5 GB artifact
through a browser. One secret,TESTDATA_TOKEN, a fine-grained token
for that repository with contents read and write. - The matrix runs against the published image, pinned. The
golden-publishedjob ofrealworld.ymlis the CI job SPEC §9.1
asks for: it fetches theimage-v1release ofzvolrescue-testdata,
checks every file against the sums this repository pins in
tests/golden/image-v1.SHA256SUMS(and against the oracle's own
record of the release), decompresses the members and their round-6
copies, reads every volume intact to the kernel's hash, and runs the
whole matrix with the oracle and manifests from that repository's
checkout, every expectation enforced — the held-out manifests too
withpublished-held-out(G4). The release files are kept in the
Actions cache between runs. It also runs every Monday on its own:
the image never changes, so a verdict that changes is the tool's
doing. The verdicts, counts, every n/a's reason and every
out-of-category run's detail come from one script now,
tests/golden/summarize-report.py, which both golden jobs share. - The
older-selfdamage pattern. SPEC §9.1's "member replaced by
an older copy of itself" was documented in the manifests and never
placed: no image had a member captured at two points of its life.
image-v1's release carries every member as it was after round 6 of
the fill, andrun-matrix.pynow writes that copy over the range
the manifest names (older-self:image-v1-round6; the copies beside
the members as<image>-round6, or--older DIR), refusing a source
of another image and reporting n/a where the copies are not there.
Five manifests inzvolrescue-testdatause it: one mirror half
stale, both halves, two raidz2 columns, one dRAID child, one stale
beside one missing. - Structure-targeted damage lands on a dRAID top too. Five of the
six n/a of every matrix run so far were the MOS and object-set
copies that live on the image's dRAID top-level vdev: the harness
maps a block pointer to members by arithmetic on mirrors and raidz,
and dRAID places columns through a permutation it refused to
reimplement. It finds them by content now — the block's bytes, read
from a copy on a mirror or put back together from a raidz's data
columns and checked against the pointer's fletcher4 or sha256, are
searched for on the dRAID's children; a column is placed only where
it is found at one row offset, on distinct children, and a parity
column that equals a data column (one data sector in a row, the rest
padding) is placed with it. Checked locally against the kernel-built
image: the five manifests pass, among them every copy of the MOS
destroyed and the pool read from an older uberblock. - Damage below the object set: a dnode block, an indirect block, one
data block of a named volume. The oracle ofimage-v1held
zdb -dddd— objects, not their block pointers — so a manifest could
aim at an object set and at nothing beneath it.oracle-capture.yml
runs OpenZFS's ownzdb -ddddddoffline over the published members
(checked against the pinned sums and the pool guid first, on a
userland at least the image's) and adds the capture to the oracle;
the builder keeps the same capture for every image from now on. The
harness reads it:<dataset>:dnode-block,<volume>:dataand
<volume>:indirectwithblockan index oron:<kind>(the first
block whose first copy lies on a mirror, raidz or dRAID top),copy
as before, andpieces = "one"for one piece of the first copy — a
mirror half, one raidz data column, one dRAID column — the loss every
geometry survives once. Eight manifests inzvolrescue-testdatause
them: a data block's one piece and every piece, an encrypted volume's
block, a dnode block and an indirect block by one copy and by every
copy, and a dnode block beside a missing member.
<dataset>:crypto-keyis located too — the MOS object the dataset's
DSL directory names ascom.datto:crypto_key_obj, the ZAP holding
its wrapped keys, every block of it (block = "all") — and two
manifests destroysecret-raw/disk's by one copy and by every copy,
so every shape the manifests' format names is placed now; a volume's
property ZAP onimage-v1is embedded in its dnode's block pointer,
so the dnode-block cases are its.tests/golden/fetch-image.shis
the one place the published image is fetched, pinned and
decompressed; the matrix job and the capture use it. - The last two classes of SPEC §9.1's catalogue, and no n/a left.
forged-txg:<bump>on alabel:…/ringsrange copies the ring's
newest verified uberblock into the slot ZFS would write that txg to,
with the txg bumped, the root pointer's birth to match and its
checksum words inverted, and seals the slot for its place: a TXG
every member's ring carries, that verifies, and that no block is
behind — what a sync leaves when its uberblocks reached the disks and
its MOS did not.grow:<bytes>makes the member longer by that much
of nothing: the partition re-created with another size, the rear
labels no longer where the length says.labels-forged-txg(every
ring forged three TXGs ahead, one member's rear rings gone besides)
andpartition-grownuse...
zvolrescue v0.9.9 — The golden image release
zvolrescue v0.9.9 — read-only extraction of zvols from ZFS pools that no longer import: labels, uberblocks, mirror/RAIDZ/dRAID reconstruction, every checksum and compression, encrypted datasets with a key.
0.x release. Validated in CI against OpenZFS 2.2 userland pools (
ztest+zdb) and on the environments listed in docs/REALWORLD-TESTS.md; treat the output as evidence to verify, not as a finished recovery.
Downloads
15 binaries: 5 programs, each built for the same three platforms. The name is always <program>-0.9.9-<platform>.
| Program | What it is for |
|---|---|
zvolrescue |
the pool, and a volume out of it: what the members hold, and the image of a zvol |
zvoltimeline |
the pool history the transaction groups still carry: when a dataset was made, destroyed, and whether its space is still held |
zvolcarve |
volumes no uberblock points at any more, found by scanning raw vdev space |
zvolfiles |
files out of filesystem datasets: the tree with its metadata, or a raw object dump when the metadata is gone |
zvolreport |
the evidence logs of the others as one document, hashed, signed and checkable by a third party |
| Platform | <platform> |
Notes |
|---|---|---|
| Linux x86_64 | x86_64-linux-musl |
static; runs on any distribution, and on a rescue medium |
| Linux aarch64 | aarch64-linux-musl |
static |
| FreeBSD 15.x amd64 | amd64-freebsd |
static (FreeBSD's own libc linked in); runs on any 15.x system. Built on 15.x, whose system-call ABI it uses; the real-kernel matrix also runs it on FreeBSD 14.5 and on the mfsBSD 14.2 rescue image, but on 14.x building from source stays the supported way: pkg install rust && cargo build --release |
So the carver for Linux x86_64 is zvolcarve-0.9.9-x86_64-linux-musl, and the FreeBSD main binary is zvolrescue-0.9.9-amd64-freebsd.
On Debian and Ubuntu, zvolrescue_0.9.9_amd64.deb and zvolrescue_0.9.9_arm64.deb install the same five static binaries with their manual pages: sudo apt install ./zvolrescue_0.9.9_amd64.deb, then man zvolrescue.
SHA256SUMS covers all 15 binaries and the 2 packages, and SHA256SUMS.sigstore.json is its signature — made by this release workflow at this tag, keylessly, through Sigstore:
sha256sum -c SHA256SUMS # or, on FreeBSD: sha256 -c SHA256SUMS
cosign verify-blob --bundle SHA256SUMS.sigstore.json \
--certificate-identity https://github.com/click0/zvolrescue/.github/workflows/release.yml@refs/tags/v0.9.9 \
--certificate-oidc-issuer https://token.actions.githubusercontent.com SHA256SUMS
gh attestation verify zvolrescue-0.9.9-x86_64-linux-musl --repo click0/zvolrescue # provenance of any one binaryNo installation: chmod +x and run. Every program opens the members read-only and never writes to the pool.
What is in v0.9.9
The golden image release.
The damage matrix of SPEC §9.1 runs in CI over an image a real kernel
built — mirror, raidz2 and dRAID top-level vdevs, every checksum and
compression, encryption, hundreds of TXGs — with every expectation
enforced: 41 manifests, 35 pass, 6 not applicable, no defects. That
image's first build found a pool feature the tool did not know,
OpenZFS 2.4's dynamic_gang_header, which it reads now. The
real-kernel matrix spans Ubuntu 26.04 and 24.04, Debian 13 and 12,
FreeBSD 15.1 and 14.5 and the mfsBSD 14.2 rescue image, and runs on a
machine of your own over ssh; dump passes over a TXG at which any of
what it opens is gone, not only the object set.
Added
- The real-kernel matrix runs on FreeBSD 15 and 14. The script
(tests/realworld/kernel-matrix.sh, renamed fromdebian-loop.sh)
now runs on FreeBSD with the base system's OpenZFS and md devices for
loop devices: B9's GPT fromgpart, F7's failing range agnopthat
refuses every read between two clean ones joined bygconcat, the
read-once check fromtruss, A3 asnobodythroughchroot -u.
realworld.ymlruns it in FreeBSD 15.1 and 14.5 VMs beside Ubuntu
and Debian 12, on demand. All four pass (REALWORLD-TESTS, Results);
on FreeBSD 15.1 (OpenZFS 2.4.2) every one of the 35 scenarios
applies, C12 included:listrefuses a kernel-expanded raidz with
exit 3 namingraidz_expansion. - Ubuntu 26.04 and Debian 13 on the matrix, beside 24.04 and 12.
The Linux jobs ofrealworld.ymlran only on the releases they were
written on in September; they are matrices now — theubuntu-26.04
andubuntu-24.04runners' own kernels, and Debian 13 and 12 VMs,
the cloud image checked against Debian'sSHA512SUMS. On Ubuntu
26.04 (OpenZFS 2.4.1, kernel 7.0) and Debian 13 (zfs-dkms 2.3.9,
kernel 6.12) all 35 scenarios apply and pass, C12 included — the
first Linux kernels on the matrix withraidz_expansion. The old
releases stay: 12's 2.1 is the oldest ZFS on the matrix and found
both of D2's defects. The workflow takes anonlyinput naming
which environments to run. - E3 on the rescue medium itself: mfsBSD.
tests/realworld/mfsbsd.sh
runs on mfsBSD — FreeBSD booted from an ISO into RAM,/bin/shand
the base system alone — where its kernel makes a mirror pool on two
raw disks and fills a volume, and the staticzvolrescuebuilt on
FreeBSD 15, copied in over ssh, scans both disks, lists the volume
and dumps it to the kernel's hash; the disks are checked unchanged
and the pool importable afterwards, andzvolreportruns over the
evidence. Themfsbsdjob ofrealworld.ymlboots
mfsbsd-14.2-RELEASE-amd64.iso, checked against its published
SHA2-256, under QEMU/KVM for it. The first run passed
(REALWORLD-TESTS, Results): the 15.1-built static binary runs on the
14.2 rescue image. The README and the release notes said "mfsBSD 15
included" of that binary; no mfsBSD 15 exists, and they now say what
was checked, and that 14.x stays build-from-source (SPEC N-06a). - Gang headers the size of a sector:
com.klarasystems:dynamic_gang_header
(OpenZFS 2.4) is read. The feature lets a gang header fill the
smallest allocation its top-level vdev makes — 4 KiB on anashift=12
mirror or raidz, a data row on a dRAID — instead of one 512-byte
sector, and it goes active on the first gang write that needed more
than three children; a fragmented pool on 2.4 (Ubuntu 26.04, FreeBSD
15.1) carries it. v0.9.8 did not know it and refused such a pool
whole, everydumpexit 3 — which is how the first golden image,
built with ganging forced, came out: 33 of 41 damage runs "refused".
The reader sizes the header aszio_gang_tree_assembledoes, the
least of the copies' minimum allocations, parses as many child
pointers as fit before the checksum tail, and — since a pool keeps the
512-byte headers it wrote before the feature went active, and the
golden image had hundreds of those and not one larger — tries the
old size when the vdev's does not verify, aszio_checksum_error
does. Fixtures write both kinds; four tests read them on a mirror and
a degraded raidz2 and see the same bytes refused cleanly when the
labels do not claim the feature. The feature joins the implemented
list.kernel-matrix.shC13 fills a mirror with ganging forced on a
2.4 kernel, sees the feature go active and the dump match. - The golden image is built in CI, and the whole damage matrix runs
over it (G1–G3). Thegoldenjob ofrealworld.ymlbuilds the
image of SPEC §9.1 withtests/golden/build-image.shon the
ubuntu-26.04runner's OpenZFS 2.4.1 — one pool with mirror, raidz2
and dRAID1 top-level vdevs, 12 volumes over every checksum and
compression, a clone, raw-key and passphrase encryption, snapshots, a
rename, a destroy, 5015 TXGs — reads every volume of it intact to
the kernel's hash, then runs the 41 manifests of
zvolrescue-testdataover it with every expectation enforced: a run
outside its expected category fails the job, where the ztest images
could only be counted. 35 pass, 6 n/a (structure-targeted damage is
not placed on a dRAID top, whose permutation the harness does not
map; one case asks for a third leaf of a two-way mirror), 0 defects,
inputs unchanged (REALWORLD-TESTS, Results). The members (zstd, with
SHA256SUMS), the oracle,IMAGE.mdand the report are uploaded as
the artifactgolden-image-v1, for publication asimage-v1in
zvolrescue-testdata. Its first build is howdynamic_gang_header
(above) was found. - The matrix on a machine of your own, over ssh.
realworld-ssh.ymlrunskernel-matrix.sh— ormfsbsd.sh, on a
rescue system — on a machine the runners cannot stand in for: a
distribution with no image here, an OpenZFS built from git, a box
with disks. It builds the static binaries for what the machine turns
out to be (Linux x86_64 or aarch64, FreeBSD amd64), copies them and
the script over, has the script run there as root and brings the
record back, as the VM jobs do. The machine is described by one
GitHub Environment's secrets alone — host, user, a key or a password,
the pinned host key, and anssh_configfor a bastion (ProxyJump
through a server or an OpenWrt router), with a Tailscale tailnet or
a self-hosted runner as the way into a network nothing else reaches.
On the machine the key runstests/realworld/ssh-gate.shas a forced
command: five requests (probe, put, run, record, clean) and no shell,
root for the run through one sudoers line that allows the gate
alone. REALWORLD-TESTS, "Running the matrix on your own machine over
ssh".
Fixed
dumppasses over a TXG at which a destroyed volume's dnodes are
gone, not only its object set. v0.9.8 made the search pass over a
TXG whose object set block no longer reads, but checked that block
alone. The blocks a destroy frees are reused in no particular order,
and on Debian 12's OpenZFS 2.1.11 and FreeBSD 15's 2.4...
zvolrescue v0.9.8 — The packaged release
zvolrescue v0.9.8 — read-only extraction of zvols from ZFS pools that no longer import: labels, uberblocks, mirror/RAIDZ/dRAID reconstruction, every checksum and compression, encrypted datasets with a key.
0.x release. Validated in CI against OpenZFS 2.2 userland pools (
ztest+zdb) and on the environments listed in docs/REALWORLD-TESTS.md; treat the output as evidence to verify, not as a finished recovery.
Downloads
15 binaries: 5 programs, each built for the same three platforms. The name is always <program>-0.9.8-<platform>.
| Program | What it is for |
|---|---|
zvolrescue |
the pool, and a volume out of it: what the members hold, and the image of a zvol |
zvoltimeline |
the pool history the transaction groups still carry: when a dataset was made, destroyed, and whether its space is still held |
zvolcarve |
volumes no uberblock points at any more, found by scanning raw vdev space |
zvolfiles |
files out of filesystem datasets: the tree with its metadata, or a raw object dump when the metadata is gone |
zvolreport |
the evidence logs of the others as one document, hashed, signed and checkable by a third party |
| Platform | <platform> |
Notes |
|---|---|---|
| Linux x86_64 | x86_64-linux-musl |
static; runs on any distribution, and on a rescue medium |
| Linux aarch64 | aarch64-linux-musl |
static |
| FreeBSD 15.x amd64 | amd64-freebsd |
static (FreeBSD's own libc linked in); runs on any 15.x system and on mfsBSD 15. Built on 15.x, whose system-call ABI it uses, so on 14.x build from source: pkg install rust && cargo build --release |
So the carver for Linux x86_64 is zvolcarve-0.9.8-x86_64-linux-musl, and the FreeBSD main binary is zvolrescue-0.9.8-amd64-freebsd.
On Debian and Ubuntu, zvolrescue_0.9.8_amd64.deb and zvolrescue_0.9.8_arm64.deb install the same five static binaries with their manual pages: sudo apt install ./zvolrescue_0.9.8_amd64.deb, then man zvolrescue.
SHA256SUMS covers all 15 binaries and the 2 packages, and SHA256SUMS.sigstore.json is its signature — made by this release workflow at this tag, keylessly, through Sigstore:
sha256sum -c SHA256SUMS # or, on FreeBSD: sha256 -c SHA256SUMS
cosign verify-blob --bundle SHA256SUMS.sigstore.json \
--certificate-identity https://github.com/click0/zvolrescue/.github/workflows/release.yml@refs/tags/v0.9.8 \
--certificate-oidc-issuer https://token.actions.githubusercontent.com SHA256SUMS
gh attestation verify zvolrescue-0.9.8-x86_64-linux-musl --repo click0/zvolrescue # provenance of any one binaryNo installation: chmod +x and run. Every program opens the members read-only and never writes to the pool.
What is in v0.9.8
The packaged release.
The tool has now been run on real kernels: the matrix's script passes
every applicable scenario on Ubuntu 24.04's in-tree OpenZFS and on
Debian 12's zfs-dkms, on pools the kernel made, every oracle from
OpenZFS — and those runs found what fixtures had not: a member
detached while it was out of the machine listed beside the survivor,
and dump stopping at a TXG whose object set a later write had
reused instead of taking an older one. Both are fixed below. The
FreeBSD release binary is static now, like the Linux ones, and the
one built on 15 runs on 14. And the packaging 1.0.0 was waiting for
is here: manual pages for every program, a FreeBSD port, a .deb and
an AUR PKGBUILD, each built and installed in CI on every push.
Added
- Manual pages, and packages that install them: a FreeBSD port, a
.deb, an AUR PKGBUILD (REALWORLD-TESTS E4).man/zvolrescue.1,
zvoltimeline.1,zvolreport.1,zvolcarve.1andzvolfiles.1in
mdoc, each kept true to its program bytests/man-check.sh, which
reads--helpof every subcommand and refuses a page missing an
option, carrying another version, or failingmandoc. Under
packaging/:deb/build.shmakeszvolrescue_<version>_<arch>.deb
of the five static binaries and the pages withdpkg-debalone, and
every release now ships one for amd64 and arm64, covered by
SHA256SUMS;freebsd/sysutils/zvolrescueis a port (USES=cargo,
its crate list generated fromCargo.lock, itsdistinfowritten by
a script that hashes the tarball and every crate the waymake makesumdoes);aur/holds the PKGBUILD and.SRCINFO, with a
script that points them at a new tag. CI builds and installs all
three on every push — the port on FreeBSD 15 through the ports
framework, the.debwithdpkg, the PKGBUILD withmakepkgin an
Arch container — and each install runs every program and finds every
page. Submission to the ports tree and to the AUR is still to do.
Fixed
- A member detached while it was out of the machine is set aside,
not listed beside the survivor.zpool detacherases the labels
of the device it detaches — but only of a device that is there. A
disk pulled from a mirror before the detach keeps its labels: the
same top-level guid (a detach hands the mirror's guid to the
survivor), an older txg, a leaf the survivor's configuration no
longer names. The assembler treated the two labels as the same
top-level vdev, took the newer tree, and then listed the old disk
underdevicesas though it belonged, with nothing instaleto
say otherwise. Found on a kernel-made pool in the first real-kernel
run of the matrix (REALWORLD-TESTS B8, Ubuntu 24.04 and Debian 12);
reproduced with a fixture, and such a label is now stale with the
reason: it names a leaf the top-level vdev's newest configuration no
longer has — detached or replaced. dumppasses over a TXG that names the dataset but has lost its
object set, and takes an older one. After azfs destroythe pool
goes on writing, and the freed object set block is among the first
to be reused; the uberblock ring may then name the dataset at a TXG
whose object set is gone while an older one still leads to it whole.
dumpstopped at the first TXG that named the dataset and reported
every copy failed its checksum(exit 3) without looking further.
Found in the Debian 12 run of the matrix (REALWORLD-TESTS D2, OpenZFS
2.1.11); reproduced with a fixture. Now such a TXG is said on stderr,
listed underunreadable_atin the record, and the search goes on to
the next older verified TXG; readable at none, the run says at which
TXG the dataset was named and why it does not read there.--txg
still means that TXG and no other.- The FreeBSD release binary is static, as the README always said
it was. Up to v0.9.7 theamd64-freebsdfiles were linked against
FreeBSD 15's baselibc.so.7,libthr.so.3andlibgcc_s.so.1—
fine on any 15.x system and on mfsBSD 15, which carry them, but not
the "static binaries with no runtime dependencies" the README
promised, andfilesaid so. The release workflow now builds the
FreeBSD binaries withcrt-static, and both it and the publish step
refuse a file thatfiledoes not call statically linked, on every
platform. The FreeBSD 15 CI job builds the same way on every push
and runs that binary on its real devices, so a crate that will not
link statically is caught there, not at release time. A static
build still carries the system-call ABI of the branch it was built
on: on 14.x, build from source as before. For a commit between
releases, or for a tag whose release still carries the dynamic file,
.github/workflows/freebsd-static.ymlbuilds the five programs the
same way on demand and uploads them with theirSHA256SUMS.
Documented
- The first real-kernel runs of the matrix, and what they taught the
script..github/workflows/realworld.ymlruns
tests/realworld/debian-loop.shon demand in two environments:
the Ubuntu runner's in-tree OpenZFS, and a Debian 12 cloud image
under QEMU/KVM withzfs-dkmsbuilt against Debian's kernel. The
first runs found the script's expectations wrong in four places and
the tool's in one (above):zpool createon a bare loop device
labels it but the partition node it then waits for never appears on
either kernel, so B9 writes the GPT itself; Debian 12 carries
OpenZFS 2.1.11, which has noblake3, so a property value this ZFS
lacks is noted rather than fatal; without a keydumprefuses an
encrypted volume up front (exit 1, naming the suite) instead of
writing zeros, which is the better behaviour and what C7 now says;
andzfs get -s localreportsvolsizeas local though the kernel
keeps it in the volume's own object, not the property ZAP, which
C11 now allows for alongsidevolblocksize. B4's corruption is
aimed at the volume's own blocks through their DVA, since 1 MiB at a
round offset landed in free space on OpenZFS 2.1. D1 accepts a
partial image when the export's own TXGs reused a block of the
volume destroyed just before it — compared byte for byte against a
copy taken before the destroy — and D2 the clean exit 3 thedump
fix above gives. With that, both environments pass every applicable
scenario (34 of the matrix; C12 needs OpenZFS 2.3), and the two rows
are in the Results table of REALWORLD-TESTS. The row the script
prints now names where the module came from (zfs-dkmsor the
in-tree module) and the module's version where it differs from
userland's. - The FreeBSD static binary built on 15 runs on 14 (REALWORLD-TESTS
E3). A CI job takes thecrt-staticbinary the FreeBSD 15 job
built, with the mirror fixture and the hash of the volume it read,
and runs version, scan and dump on FreeBSD 14.5-RELEASE: all pass,
the hash matches. The matrix row says so, with the caveat that this
is a full 14 VM and not mfsBSD itself.
Full history: CHANGELOG.md.
zvolrescue v0.9.7 — The interruptible release
zvolrescue v0.9.7 — read-only extraction of zvols from ZFS pools that no longer import: labels, uberblocks, mirror/RAIDZ/dRAID reconstruction, every checksum and compression, encrypted datasets with a key.
0.x release. Validated in CI against OpenZFS 2.2 userland pools (
ztest+zdb) and on the environments listed in docs/REALWORLD-TESTS.md; treat the output as evidence to verify, not as a finished recovery.
Downloads
15 binaries: 5 programs, each built for the same three platforms. The name is always <program>-0.9.7-<platform>.
| Program | What it is for |
|---|---|
zvolrescue |
the pool, and a volume out of it: what the members hold, and the image of a zvol |
zvoltimeline |
the pool history the transaction groups still carry: when a dataset was made, destroyed, and whether its space is still held |
zvolcarve |
volumes no uberblock points at any more, found by scanning raw vdev space |
zvolfiles |
files out of filesystem datasets: the tree with its metadata, or a raw object dump when the metadata is gone |
zvolreport |
the evidence logs of the others as one document, hashed, signed and checkable by a third party |
| Platform | <platform> |
Notes |
|---|---|---|
| Linux x86_64 | x86_64-linux-musl |
static; runs on any distribution, and on a rescue medium |
| Linux aarch64 | aarch64-linux-musl |
static |
| FreeBSD 15.x amd64 | amd64-freebsd |
built on 15.x. FreeBSD changes its ABI between major branches, so on 14.x build from source: pkg install rust && cargo build --release |
So the carver for Linux x86_64 is zvolcarve-0.9.7-x86_64-linux-musl, and the FreeBSD main binary is zvolrescue-0.9.7-amd64-freebsd.
SHA256SUMS covers all 15 of them, and SHA256SUMS.sigstore.json is its signature — made by this release workflow at this tag, keylessly, through Sigstore:
sha256sum -c SHA256SUMS # or, on FreeBSD: sha256 -c SHA256SUMS
cosign verify-blob --bundle SHA256SUMS.sigstore.json \
--certificate-identity https://github.com/click0/zvolrescue/.github/workflows/release.yml@refs/tags/v0.9.7 \
--certificate-oidc-issuer https://token.actions.githubusercontent.com SHA256SUMS
gh attestation verify zvolrescue-0.9.7-x86_64-linux-musl --repo click0/zvolrescue # provenance of any one binaryNo installation: chmod +x and run. Every program opens the members read-only and never writes to the pool.
What is in v0.9.7
The interruptible release.
A run can now be stopped and taken up where it stopped, in every tool
that runs long: SPEC §7 had promised exit 6 for an interrupted run
since 0.1 and nothing produced it — a SIGINT killed the run, and
--resume had whatever the checkpoint five seconds earlier had said.
zvolrescue dump, zvolcarve dump and zvolcarve scan end at the
next block or chunk with their state written; zvolfiles extract at
the next entry with the manifest saying what was written; zvolcarve dump gains the --resume its spec had promised, on the very state
file zvolrescue dump writes. And the first real-kernel environment
of the matrix is scripted: Debian with zfs-dkms and loop devices,
every oracle from OpenZFS alone, ready to run on a VM — which is the
step 1.0.0 waits on.
Added
- The real-world matrix on Debian with
zfs-dkmsand loop devices,
scripted (tests/realworld/debian-loop.sh). The cheapest
environment with a real kernel ZFS, and the first one REALWORLD-TESTS
should see run: pools the kernel makes on loop devices, which the
tool treats as the block devices they are (N-10). The script builds
every layout of B, every property of C, every recovery of D, the
dm-errormember of F7 and the read-only checks of A, takes every
oracle from OpenZFS alone (sha256sumof the zvol before export,
zdb -d,zdb -lu,zfs get -s local), and prints the row for the
Results table.raidz_expansion(C12) needs OpenZFS 2.3 and is
skipped on 2.2; dRAID is skipped wherezpoolbuilds none. Found
while writing it:dumphad no SIGINT handler (the next entry). The
matrix has a section describing what every scenario becomes on loop
devices and what a loop device cannot give (E1/E2, F1–F6, F8–F10
stay for a machine with drives). - A SIGINT ends
dumpwhere--resumecan take it up (F-32). The
signal used to kill the run: exit 130, the image cut wherever the
kernel happened to be, and--resumestarting from whatever the
checkpoint five seconds earlier had said — or from nothing, when the
run was younger than that. Now SIGINT and SIGTERM set a flag the
extraction reads before every block: the run ends at that block
boundary, writes its state naming the block it was about to read,
reportsinterrupted_atin the output and the evidence record, and
exits 6 as SPEC §7 has said since 0.1; a bulk-rrun stops there
instead of starting the next volume. A second signal is the default
action again, for a run that does not come round to the flag. The
five-second checkpoint stays, for a kill no handler sees. The flag
lives inzvol-commonon thesignal-hookcrate (nounsafeof
ours; the workspace still forbids it);extract_fromtakes it as an
optionalAtomicBool. Unit-tested inzfs-read, and CI sends a
real SIGINT to a 512 MiB dump once 128 MiB are on disk, then resumes
it to the same hash. - The companions stop the same way.
zvolcarve dumpgains the
--resumeCOMPANIONS §3.2 had promised, on the very state file
zvolrescue dumpwrites — the mechanics moved into
zvol_common::resume, so there is one extraction pipeline and not
two that agree by luck — and a SIGINT ends it at the next block with
the state written, exit 6.zvolcarve scanstops at the next chunk:
the state file says where each member got to, members not reached
keep what an earlier run reached, and--resumegoes on from there
(C-08 covered the candidate cap; now it covers the operator too).
zvolfiles extractstops at the next entry, or the next block of
the file being written: that file is recorded as failed with how
much of it was written, the manifest saysinterrupted, exit 6, and
--pathasks for the rest (Z-11). CI interrupts a real carve scan
and a real carve dump and resumes both.
Documented
- The open questions of §12 are closed (D-9…D-11). What the
repository already did is now a decision with its reasons: Linux CI
from phase 0 with FreeBSD beside it and both shipped (D-9);
raidz_expansiondeferred past 1.0.0, a pool with it active refused
with the reason, because a reflowed layout returns wrong bytes with
checksums that do not object and no such pool has been measured
(D-10); the library crates internal and unpublished until 1.0.0,
every crate markedpublish = falseso that no straycargo publishcan decide it first (D-11). With D-8 that leaves §12 with
nothing open, which was one of the three conditions §10 sets for
1.0.0.
Fixed
- The fixtures' dnode and objset blocks carry the object counts ZFS
keeps infill. v0.9.6 made the dense volume's indirect pointers
count the blocks beneath them; the level-0 blocks of dnode arrays
and every objset's root pointer still saidfill = 1. ZFS writes
the number of allocated dnodes in a dnode block, a multi-slot dnode
counted once, and the sum of those under an objset's meta dnode —
which is whatzdb -dprints as the object count. The fixture now
writes both, through two allocator calls that count what they
store, and a test reads the sample pool's MOS and the volume's
objset back and compares each pointer'sfillwith the dnodes it
finds in the block. This tool never readsfill; the cross-check
withzdbdoes. - A device that refuses a read while the members are being bound
stops the run there too (SPEC F-33, N-10). After every member's
labels are read,open_membersstill reads through the pool — a
removed vdev's mapping, a layout searched for, a leaf bound by
reading — past its own check for a stop. A refusal there was
recorded and swallowed, and the run went on into the command with a
stopped ledger, to end wherever it ended. Now a stop recorded by
those reads ends the open the way one during the labels does, and
the command's end of run reports it.zvolcarve dumprefusing to
write over evidence goes through that end of run as well, like its
other early returns.
Full history: CHANGELOG.md.
zvolrescue v0.9.6 — The review release
zvolrescue v0.9.6 — read-only extraction of zvols from ZFS pools that no longer import: labels, uberblocks, mirror/RAIDZ/dRAID reconstruction, every checksum and compression, encrypted datasets with a key.
0.x release. Validated in CI against OpenZFS 2.2 userland pools (
ztest+zdb) and on the environments listed in docs/REALWORLD-TESTS.md; treat the output as evidence to verify, not as a finished recovery.
Downloads
15 binaries: 5 programs, each built for the same three platforms. The name is always <program>-0.9.6-<platform>.
| Program | What it is for |
|---|---|
zvolrescue |
the pool, and a volume out of it: what the members hold, and the image of a zvol |
zvoltimeline |
the pool history the transaction groups still carry: when a dataset was made, destroyed, and whether its space is still held |
zvolcarve |
volumes no uberblock points at any more, found by scanning raw vdev space |
zvolfiles |
files out of filesystem datasets: the tree with its metadata, or a raw object dump when the metadata is gone |
zvolreport |
the evidence logs of the others as one document, hashed, signed and checkable by a third party |
| Platform | <platform> |
Notes |
|---|---|---|
| Linux x86_64 | x86_64-linux-musl |
static; runs on any distribution, and on a rescue medium |
| Linux aarch64 | aarch64-linux-musl |
static |
| FreeBSD 15.x amd64 | amd64-freebsd |
built on 15.x. FreeBSD changes its ABI between major branches, so on 14.x build from source: pkg install rust && cargo build --release |
So the carver for Linux x86_64 is zvolcarve-0.9.6-x86_64-linux-musl, and the FreeBSD main binary is zvolrescue-0.9.6-amd64-freebsd.
SHA256SUMS covers all 15 of them, and SHA256SUMS.sigstore.json is its signature — made by this release workflow at this tag, keylessly, through Sigstore:
sha256sum -c SHA256SUMS # or, on FreeBSD: sha256 -c SHA256SUMS
cosign verify-blob --bundle SHA256SUMS.sigstore.json \
--certificate-identity https://github.com/click0/zvolrescue/.github/workflows/release.yml@refs/tags/v0.9.6 \
--certificate-oidc-issuer https://token.actions.githubusercontent.com SHA256SUMS
gh attestation verify zvolrescue-0.9.6-x86_64-linux-musl --repo click0/zvolrescue # provenance of any one binaryNo installation: chmod +x and run. Every program opens the members read-only and never writes to the pool.
What is in v0.9.6
The review release.
Everything since v0.9.0 read again, with tests for what the reading
found. Two defects, neither of which had shipped a wrong byte but both
of which broke the medium policy's promise: a device that refused a
read could still be reached by any reader that went around the pool
reader, and a refusal on the way to the dataset came back as exit 3
with no incident on record when the run was a single volume. Both are
closed, in the io layer and at the end of every run, and pinned on a
real block device in CI. The fixtures stripe gang blocks and remapped
blocks onto raidz now, the dense fixture's indirect pointers count
what is beneath them, and the fuzzer starts from the inputs that once
crashed it.
Changed
- A device that refused a read is closed for the rest of the run, in
the io layer (SPEC N-10). The medium policy stopped the run at the
first refusal, and the pool reader refused every later read — but
only the pool reader. The io layer recorded the incident and went on
answering reads of the same device, so anything that read a member
without going through the pool reader (the label reads of a scan,
the zero-point search, carving) could still reach a disk that had
just refused, and a unit test pinned that a second read after the
stop goes through. Now the ledger knows which path the stopping
incident was on, and every source that is a device answers every
later read of that path with that incident before touching it —
memory hits included, so nothing masks a caller still trying. Other
devices and every image go on being read: the run is over, but the
report still wants their labels.--device-may-failis unchanged in
spirit: a skipped refusal closes nothing, the flag exists so that the
device's other addresses are still read, and the incident that stops
the run closes it like any stop. Four tests, each failing without
the gate: the second read after a stop is refused and the device not
touched; the same path through another handle is closed while
another device and an image on the same ledger read on; with
--device-may-failthe device stays open until the limit and is
closed by it; and a realFileSourcethat is a device refuses
before the read it would otherwise answer.
Fixed
- Every run ends the same way, whichever way it failed (SPEC F-33,
N-10). The fix below closed the early returns ofdumpandlist;
a review of the line found the same door open elsewhere.zvolfiles list,objectsandextractreturned a bare 3 when the MOS or the
dataset walk failed, with no incident on stderr and no record. And
when a device refused a label read, so that the members never
opened, every tool printed the incident and exited 7 but wrote no
evidence record at all. Now the ledger is made once per run, on the
pool spec, before anything is opened, and oneend_runcloses every
run — the report of the incidents, the stop as the exit code, the
record with them — including the runs that failed while opening,
choosing a pool or a transaction group, or before writing a byte:
dump,list,zvoltimeline, the threezvolfilescommands and
the fourzvolcarvecommands all go through it. A run that produced
nothing records that it produced nothing. Checked: incargo test,
adumpof a dataset that does not exist and alistat a
transaction group nobody has both end with their record; in CI on a
real block device,zvolfiles listandzvoltimelineon a member
whose label 0 refuses exit 7 with one incident on stderr and the
record saying so, and ascanof that member reports it refused,
with the incident, and opens no further member after the stop. - A device's refusal on the way to the dataset is exit 7 and an
incident on record, on a single volume too (SPEC F-33, N-10). The
medium policy of v0.9.1 reported a refused read after the run: every
incident on stderr, the stop as the exit code, both in the evidence
record. A single-volumedumpand alistnever got there when the
refusal landed on a metadata read — the MOS objset, the dataset tree,
the volume's dnode: "cannot read the pool" returned early with exit
3, noMEDIUM INCIDENTline, and no evidence record at all, while
the ledger held the incident and nobody read it. A bulk-rrun
reached the report, so the one-volume case was strictly worse. Now
every early failure ends the way every run ends. Checked in CI on a
real block device: the eight sectors of the MOS objset mapped to
device-mapper's error target,dumpandlistboth exit 7 with the
incident on stderr and in the record — the block's address taken
from the reader's own trace, so the test follows the fixture. - The dense fixture's indirect pointers count the blocks beneath
them. Every pointer the fixture wrote carriedfill = 1, the
indirect ones included; ZFS keeps the number of non-hole level-0
blocks under an indirect pointer there. This tool never readsfill,
so nothing it does changed, butzdbreads it and a fixture that
lies to the cross-check is worse than none. A test builds 2048
blocks under two levels and checks every pointer at every level.
Added
- The direct readers, tested against a closed device. The io-layer
gate (above) was shown withstracein CI; now a unit test builds a
member whose first sector refuses, runs the label scan on it as a
device, and asks what a scan asks next — the partition table, the
GEOM sector, the anchor search with leave — and finds every one
refused with the incident and the device read exactly once. And a
gang header'sasizeis asserted on a mirror and on raidz2, where
nothing in the read path would notice a wrong one. - The four inputs the fuzzer crashed on, committed as seeds.
v0.9.0's fuzz commit fixed four panics and kept the crashing inputs
only underfuzz/artifacts, which is ignored; CI had to rediscover
them inside its thirty seconds. They are underfuzz/seedsnow,
named for what they broke — the indirect mapping's address
arithmetic, a GPT entry's LBA arithmetic twice, a microzap shorter
than its header — beside the deterministic unit tests that already
guard each fix. - A healthy device asked for its zero point is refused the search,
with exit 5 — pinned (SPEC N-10). The labels verify, the pool is
reported, and--zero-pointstill getszero point: not searched,
surface_scan_refusedin the JSON and exit 5: the operator asked
for a surface search and did not get one, and a script must be told.
That was the behaviour; the only test covered a device whose labels
do not verify. Now the CI device job runs it on a real block device
with verified labels and shows throughstracethat the refusal
came before any read past the four labels. - Smaller guards from the same review. A read that only partly
overlaps a remembered window is never served from it, asserted
rather than assumed; a symlink to a device —/dev/mapper/NAME,
/dev/disk/by-id/…, the names an operator types — is refused a
surface scan like the device itself, byscanand byzvolcarve;
andscan --retriesis refused likedump --retries. - Gang blocks and remapped blocks on a degraded raidz, tested. The
defect v0.9.5 caught before its tag lived on the unverified read
path — the one a gang header, a block remapped by a vdev removal and
read_dvag...
zvolrescue v0.9.5 — The measured release
zvolrescue v0.9.5 — read-only extraction of zvols from ZFS pools that no longer import: labels, uberblocks, mirror/RAIDZ/dRAID reconstruction, every checksum and compression, encrypted datasets with a key.
0.x release. Validated in CI against OpenZFS 2.2 userland pools (
ztest+zdb) and on the environments listed in docs/REALWORLD-TESTS.md; treat the output as evidence to verify, not as a finished recovery.
Downloads
15 binaries: 5 programs, each built for the same three platforms. The name is always <program>-0.9.5-<platform>.
| Program | What it is for |
|---|---|
zvolrescue |
the pool, and a volume out of it: what the members hold, and the image of a zvol |
zvoltimeline |
the pool history the transaction groups still carry: when a dataset was made, destroyed, and whether its space is still held |
zvolcarve |
volumes no uberblock points at any more, found by scanning raw vdev space |
zvolfiles |
files out of filesystem datasets: the tree with its metadata, or a raw object dump when the metadata is gone |
zvolreport |
the evidence logs of the others as one document, hashed, signed and checkable by a third party |
| Platform | <platform> |
Notes |
|---|---|---|
| Linux x86_64 | x86_64-linux-musl |
static; runs on any distribution, and on a rescue medium |
| Linux aarch64 | aarch64-linux-musl |
static |
| FreeBSD 15.x amd64 | amd64-freebsd |
built on 15.x. FreeBSD changes its ABI between major branches, so on 14.x build from source: pkg install rust && cargo build --release |
So the carver for Linux x86_64 is zvolcarve-0.9.5-x86_64-linux-musl, and the FreeBSD main binary is zvolrescue-0.9.5-amd64-freebsd.
SHA256SUMS covers all 15 of them, and SHA256SUMS.sigstore.json is its signature — made by this release workflow at this tag, keylessly, through Sigstore:
sha256sum -c SHA256SUMS # or, on FreeBSD: sha256 -c SHA256SUMS
cosign verify-blob --bundle SHA256SUMS.sigstore.json \
--certificate-identity https://github.com/click0/zvolrescue/.github/workflows/release.yml@refs/tags/v0.9.5 \
--certificate-oidc-issuer https://token.actions.githubusercontent.com SHA256SUMS
gh attestation verify zvolrescue-0.9.5-x86_64-linux-musl --repo click0/zvolrescue # provenance of any one binaryNo installation: chmod +x and run. Every program opens the members read-only and never writes to the pool.
What is in v0.9.5
The measured release.
The 512 MiB memory bound and the throughput target had been asserted
and never measured, and zstd was decoded by a pure-Rust crate on the
strength of an assumption. Now every push builds dense 512 MiB volumes,
extracts them and prints the rate and the peak resident set; runs the
reader's own zstd decoder against libzstd on the same blocks; and dumps
one volume twice to check the bytes and the documents come back
identical. The first measurement found its own defect: raidz2 read an
uncompressed volume at a third of the mirror's rate, because every
stripe fetched its parity before anything said it was needed. A healthy
stripe now reads its data columns alone — and a review of the whole
range before the tag caught what that change had broken on the other
read path, a degraded raidz behind a gang or remapped pointer, which is
fixed here and never shipped. Deduplicated volumes are in the
fixtures.
Changed
- A healthy raidz stripe is read from its data columns alone. The
first measurements (below) showed an uncompressed volume on raidz2
extracted at a third of the mirror's rate. Two causes, both in the
striped read: every row fetched its parity columns before anyone
knew whether they were needed, and the stripe was assembled one byte
at a time through an iterator. Now the data columns are read first;
the parity is fetched only for a row that lost a column or, after
the checksum fails, for the combinatorial reconstruction — as
OpenZFS reads a healthy stripe, and as a device asks (SPEC N-10):
half the reads. Both read paths do it — the verified one and the
unverified one behindread_dva, gang headers and remapped blocks —
and each fetches the parity of a row that lost a column before it
reconstructs. The assembly copies whole columns. Measured on the same
volume: about 290 MB/s on raidz2 against about 310 on the mirror,
from 130; on a quiet machine raidz2 now extracts at the mirror's
rate, uncompressed, lz4 and gzip alike. Two tests: one counts the
reads of an extract on a four-wide raidz2 and finds none of the
parity columns among them; the other reads a pointer through the
unverified path with each one of the four members missing in turn,
and checks the reconstructed bytes against the pointer's checksum.
Added
- The zstd decoder, measured against libzstd (SPEC §12 Q3, D-8).
tests/zstd-bench.shcuts a fixture volume's bytes into 128 KiB
blocks, compresses them with thezstdcommand, gives each the
framing OpenZFS gives a zstd block and decodes the lot through the
reader's own decoder (zstd-bench, an example ofzfs-ondisk),
every block checked against its original; libzstd's built-in
benchmark runs on the same bytes. It runs on every push and prints
one table. Measured: about 400 MB/s forruzstdon one thread
against about 1 GB/s for libzstd on the fixture text at level 3 —
more than the media this tool reads deliver, soruzstdstays and
the question is closed as D-8. - A deduplicated volume in the fixtures (SPEC F-28).
mkfixture … dedupbuilds the sample pool withtank/vm/disk0deduplicated: its
data pointers carry the dedup bit and sha256 checksums, and
dedup=sha256,verifyis set among its properties. There is no
deduplication table on the pool and none is looked for — a dedup
pointer is a pointer — solist --propertiesnames the property
anddumpgives the plain volume's image, byte for byte. Both are
tests now, in the library and on the command line. - Memory and throughput, measured on every push (SPEC N-03, N-08,
N-05). Until now the 512 MiB bound and the throughput target were
asserted and never measured.mkfixture benchandbench-raidz2
build a dense volume — every block present under a real indirect
tree, 128 KiB blocks, of any size, with compression off, lz4 or gzip
(the fixture gained a compressingput) — and print the SHA-256 the
extracted image must have.tests/bench.shextracts six such volumes
of 512 MiB and reports, per run, the extraction rate, the rate of a
plain copy of as many bytes through the same page cache, their ratio,
and the peak resident set, whichdumpnow reports itself
(peak_rss_kib, from/proc/self/statuson Linux). It fails on a
wrong hash, on a resident set over 512 MiB, or on a rate under a
floor; the N-08 ratios are to a disk's read rate and wait for the
disks of REALWORLD-TESTS E1 and E2, which now say so. Measured here:
peak resident set about 70 MiB on every volume, and the reader alone
— output to tmpfs, so the disk is out of it — about 460 MB/s on one
thread, mirror or single member alike. Reproducibility (N-05) is
checked the same way: the same volume dumped, scanned and listed
twice gives identical images and identical documents onceseconds
andpeak_rss_kib, the two fields meant to vary, are set aside.
Full history: CHANGELOG.md.
zvolrescue v0.9.1 — This tool reads healthy media
zvolrescue v0.9.1 — read-only extraction of zvols from ZFS pools that no longer import: labels, uberblocks, mirror/RAIDZ/dRAID reconstruction, every checksum and compression, encrypted datasets with a key.
0.x release. Validated in CI against OpenZFS 2.2 userland pools (
ztest+zdb) and on the environments listed in docs/REALWORLD-TESTS.md; treat the output as evidence to verify, not as a finished recovery.
Downloads
15 binaries: 5 programs, each built for the same three platforms. The name is always <program>-0.9.1-<platform>.
| Program | What it is for |
|---|---|
zvolrescue |
the pool, and a volume out of it: what the members hold, and the image of a zvol |
zvoltimeline |
the pool history the transaction groups still carry: when a dataset was made, destroyed, and whether its space is still held |
zvolcarve |
volumes no uberblock points at any more, found by scanning raw vdev space |
zvolfiles |
files out of filesystem datasets: the tree with its metadata, or a raw object dump when the metadata is gone |
zvolreport |
the evidence logs of the others as one document, hashed, signed and checkable by a third party |
| Platform | <platform> |
Notes |
|---|---|---|
| Linux x86_64 | x86_64-linux-musl |
static; runs on any distribution, and on a rescue medium |
| Linux aarch64 | aarch64-linux-musl |
static |
| FreeBSD 15.x amd64 | amd64-freebsd |
built on 15.x. FreeBSD changes its ABI between major branches, so on 14.x build from source: pkg install rust && cargo build --release |
So the carver for Linux x86_64 is zvolcarve-0.9.1-x86_64-linux-musl, and the FreeBSD main binary is zvolrescue-0.9.1-amd64-freebsd.
SHA256SUMS covers all 15 of them, and SHA256SUMS.sigstore.json is its signature — made by this release workflow at this tag, keylessly, through Sigstore:
sha256sum -c SHA256SUMS # or, on FreeBSD: sha256 -c SHA256SUMS
cosign verify-blob --bundle SHA256SUMS.sigstore.json \
--certificate-identity https://github.com/click0/zvolrescue/.github/workflows/release.yml@refs/tags/v0.9.1 \
--certificate-oidc-issuer https://token.actions.githubusercontent.com SHA256SUMS
gh attestation verify zvolrescue-0.9.1-x86_64-linux-musl --repo click0/zvolrescue # provenance of any one binaryNo installation: chmod +x and run. Every program opens the members read-only and never writes to the pool.
What is in v0.9.1
This tool reads healthy media.
A data-recovery practitioner read v0.9.0 and saw the danger in it: a
sector a live disk refused was read again in pieces, then by sector,
then again with --retries, and a surface scan read a device end to
end. That is what a failing disk survives least. Physical and logical
recovery are separate trades: a disk with defects is imaged first, by a
tool that knows the kind of defect and when to stop, and this tool
works on the image and its map. So now: on a device the first refused
read stops the run with exit 7 and the incident, nothing is read twice
anywhere, surface scans refuse a block device, and --retries is gone.
Checked on real block devices in CI — device-mapper's error target on
Linux, gnop on FreeBSD — with strace proving no address read twice.
Changed
-
This tool reads healthy media (SPEC N-10, F-33 rewritten). A
data-recovery practitioner's reading of v0.9.0: re-reading a sector a
live disk refused — in pieces, then sector by sector, then again with
--retries— is what a failing disk survives least, and a surface
scan of one is worse. Physical and logical recovery are separate
trades: a disk with defects is imaged first, by a tool that knows the
kind of defect and when to stop, and the metadata is parsed from the
image. So: on a device, the first read it refuses stops the run
with a new exit code7. The incident — device, byte offset and LBA,
length,errno, time, and which volume and block — is on stderr, in
the JSON (stopped_by_medium) and in the evidence record
(incidents), and the message says what to do next: image the disk
with a map and continue on the image with--mapand--resume.
Nothing is read twice: no retry, no pieces, no sectors, and not the
other side of a mirror either — the operator is told first and
decides.--retriesis gone.--device-may-failis the one way past
the first incident: the refused range is skipped once — the mirror
heals it if it can, else zeros with the incident as the reason — and
the run stops anyway after eight. On an image with its map (F-72)
nothing changes: the map refuses ranges before they are read, and a
bad sector still costs only its sector. Checked in CI on a real block
device: a mirror member on a loop device with eight sectors mapped to
device-mapper'serrortarget stops the run with exit 7 and the LBA;
with--device-may-failthe mirror heals the block andstraceshows
no address on the device read twice; on FreeBSD,gnop -e 5makes
the first label read the incident. -
No address on a device is read twice, anywhere (SPEC N-10). The
metadata walk used to come back to the same MOS blocks over and over
— one block nineteen times in a small dump — and a scan read each
label twice, then its first and last sectors again for the partition
table and the GEOM check. The reader now keeps the metadata blocks it
has read (bounded: small blocks only, dropped whole past 64 MiB), each
label is read once, and a source answers any read that lies inside a
recent read from memory. A scan of a device is four reads, one per
label; a dump of a two-block volume off a mirror member is sixteen,
none overlapping. -
Surface scans refuse a block device (SPEC N-10). The search for
uberblock anchors when a member's labels are gone (F-61), the root
search when no uberblock survives (F-64), pointer self-consistency
(F-63) and carving (zvolcarve scan) answer a block device with exit
5and the reason, unless--surface-scan-on-deviceis given.scan
reports it aszero point: not searched(JSONsurface_scan_refused)
and exits 5 too. Image the disk, and search the image.
Full history: CHANGELOG.md.
zvolrescue v0.9.0 — Checked, fuzzed, interpreted, signed
zvolrescue v0.9.0 — read-only extraction of zvols from ZFS pools that no longer import: labels, uberblocks, mirror/RAIDZ/dRAID reconstruction, every checksum and compression, encrypted datasets with a key.
0.x release. Validated in CI against OpenZFS 2.2 userland pools (
ztest+zdb) and on the environments listed in docs/REALWORLD-TESTS.md; treat the output as evidence to verify, not as a finished recovery.
Downloads
15 binaries: 5 programs, each built for the same three platforms. The name is always <program>-0.9.0-<platform>.
| Program | What it is for |
|---|---|
zvolrescue |
the pool, and a volume out of it: what the members hold, and the image of a zvol |
zvoltimeline |
the pool history the transaction groups still carry: when a dataset was made, destroyed, and whether its space is still held |
zvolcarve |
volumes no uberblock points at any more, found by scanning raw vdev space |
zvolfiles |
files out of filesystem datasets: the tree with its metadata, or a raw object dump when the metadata is gone |
zvolreport |
the evidence logs of the others as one document, hashed, signed and checkable by a third party |
| Platform | <platform> |
Notes |
|---|---|---|
| Linux x86_64 | x86_64-linux-musl |
static; runs on any distribution, and on a rescue medium |
| Linux aarch64 | aarch64-linux-musl |
static |
| FreeBSD 15.x amd64 | amd64-freebsd |
built on 15.x. FreeBSD changes its ABI between major branches, so on 14.x build from source: pkg install rust && cargo build --release |
So the carver for Linux x86_64 is zvolcarve-0.9.0-x86_64-linux-musl, and the FreeBSD main binary is zvolrescue-0.9.0-amd64-freebsd.
SHA256SUMS covers all 15 of them, and SHA256SUMS.sigstore.json is its signature — made by this release workflow at this tag, keylessly, through Sigstore:
sha256sum -c SHA256SUMS # or, on FreeBSD: sha256 -c SHA256SUMS
cosign verify-blob --bundle SHA256SUMS.sigstore.json \
--certificate-identity https://github.com/click0/zvolrescue/.github/workflows/release.yml@refs/tags/v0.9.0 \
--certificate-oidc-issuer https://token.actions.githubusercontent.com SHA256SUMS
gh attestation verify zvolrescue-0.9.0-x86_64-linux-musl --repo click0/zvolrescue # provenance of any one binaryNo installation: chmod +x and run. Every program opens the members read-only and never writes to the pool.
What is in v0.9.0
Checked, fuzzed, interpreted, signed.
The audit release, the first of the three tags between v0.8.5 and 1.0.0
(SPEC §10). Nothing the tool reads or writes changes; what changes is
what is known about it on every push. The dependency graph is held to a
licence list and a ban list, and every binary's import table is read
and found to ask for no socket, no exec and no dlopen. Every parser is
fuzzed, and the four panics the fuzzer found on its first run are fixed.
The unit tests of the on-disk parsers run under Miri. And the release
files are signed, keylessly, by the workflow that built them.
Added
-
What the dependency graph carries, and what the binaries ask their
platform for, checked on every push (SPEC §8.3 (3), N-07). The tool
reads evidence and writes images; it has no business on a network and
never runs another program. The source said so, and nothing checked
it. Nowdeny.tomlholds the rule for the graph: every licence in the
tree is a permissive one on the list — the CDDL of the Edon-R port is
allowed on theedonrcrate alone, where OpenZFS's files are kept
apart — no advisory stands against a crate, and the crates that exist
to open a socket, run a program or load code are named and banned, so
a dependency that grows one is stopped on the branch it arrives on.
And every binary the workspace builds —zvolrescueand the four
companions — has a test that reads its ELF symbol tables and fails on
any ofsocket,connect,execve,fork,posix_spawn,dlopen
and their kin, whether imported from the C library on a dynamic build
or linked in on a static one. The reader is checked against a program
that does connect and does run a program, so a clean result is known
to mean something. A source grep forprocess::Commandand
std::netruns beside it. Today's binaries import none of it. -
The parsers are fuzzed on every push (SPEC §9). Everything in
zfs-ondiskthat reads bytes off a disk is now acargo fuzztarget
— a packed nvlist and the label configuration read out of it, a block
pointer and a gang header, a dnode and an objset header, the three ZAP
forms, an uberblock slot and avdev_phys, every bonus buffer (space
map, indirect mapping, DSL directory and dataset, bpobj, deadlist,
znode, system-attribute header), every decompressor, and a GPT, an
MBR and a GEOM sector — in both byte orders where the structure has
one. Each starts from a well-formed seed built with the crate's own
encoders and read back through the parser before it is committed
(fuzz/seeds, from the examplefuzz-seeds), and CI runs every
target for a bounded time under the address sanitizer on a nightly
toolchain, failing on a panic, an abort or an input that runs past
the time limit and keeping that input as an artifact. The workspace
itself stays on stable; the fuzz crate is outside it.The first run found four ways to make a parser panic, all on input no
pool writes and every one of them a crash on a damaged disk: an
indirect-mapping entry whose destination offset sat at the top of the
address space overflowed the addition inremap, and the sum of
entry sizes could overflow too; a GPT slot whose LBAs did not fit the
byte address space overflowed the multiplication, and one spanning
the whole space overflowed its length by one; and a block whose first
eight bytes said "microzap" and which ended before its header chunk
did was sliced past its end. Each now answers as damage — unmapped,
a skipped slot, a truncated block — with a unit test beside it. -
Release files are signed (SPEC N-07). Nobody holds a signing key:
the release workflow signsSHA256SUMS— which covers every binary —
with a short-lived certificate that Sigstore's Fulcio issues to that
workflow run against GitHub's OIDC token, and ships the signature,
the certificate and the transparency-log entry as one bundle beside
it,SHA256SUMS.sigstore.json. The certificate names this
repository's release workflow and the tag it ran for, and that is
whatcosign verify-blobchecks; the release notes carry the exact
line. The workflow verifies its own signature before publishing, so
a release whose signature does not verify is not published. Each
binary also gets a GitHub build-provenance attestation — the commit,
the workflow and the runner that built it — forgh attestation verify. Nothing changes forsha256sum -c. -
Miri on the on-disk parsers' unit tests, every push (SPEC §9).
The workspace forbids unsafe code, so what the interpreter catches is
in the dependencies the parsers lean on and in the standard library's
contract as the tests use it: an uninitialised read, an aliasing
violation, an out-of-bounds access the native build happened to
survive. The 125 tests ofzfs-ondiskthat run there pass, in five
and a half minutes on a laptop core. A handful that
loop over megabytes of pure arithmetic — noise fed to the carver and
the filesystem signatures, the space-map replay, every RAIDZ loss
pattern, the dRAID permutation maps, a 1 MiB fletcher — run a tenth of
their volume there, and thevdev_physround trip, three SHA-256
passes over 112 KiB that took six minutes interpreted, is skipped
there; each says so beside its loop, and every one runs in full on
the native build.
Full history: CHANGELOG.md.
zvolrescue v0.8.5 — What the disk never gave, and what the disk still says
zvolrescue v0.8.5 — read-only extraction of zvols from ZFS pools that no longer import: labels, uberblocks, mirror/RAIDZ/dRAID reconstruction, every checksum and compression, encrypted datasets with a key.
0.x release. Validated in CI against OpenZFS 2.2 userland pools (
ztest+zdb) and on the environments listed in docs/REALWORLD-TESTS.md; treat the output as evidence to verify, not as a finished recovery.
Downloads
15 binaries: 5 programs, each built for the same three platforms. The name is always <program>-0.8.5-<platform>.
| Program | What it is for |
|---|---|
zvolrescue |
the pool, and a volume out of it: what the members hold, and the image of a zvol |
zvoltimeline |
the pool history the transaction groups still carry: when a dataset was made, destroyed, and whether its space is still held |
zvolcarve |
volumes no uberblock points at any more, found by scanning raw vdev space |
zvolfiles |
files out of filesystem datasets: the tree with its metadata, or a raw object dump when the metadata is gone |
zvolreport |
the evidence logs of the others as one document, hashed, signed and checkable by a third party |
| Platform | <platform> |
Notes |
|---|---|---|
| Linux x86_64 | x86_64-linux-musl |
static; runs on any distribution, and on a rescue medium |
| Linux aarch64 | aarch64-linux-musl |
static |
| FreeBSD 15.x amd64 | amd64-freebsd |
built on 15.x. FreeBSD changes its ABI between major branches, so on 14.x build from source: pkg install rust && cargo build --release |
So the carver for Linux x86_64 is zvolcarve-0.8.5-x86_64-linux-musl, and the FreeBSD main binary is zvolrescue-0.8.5-amd64-freebsd.
SHA256SUMS covers all 15 of them:
sha256sum -c SHA256SUMS # or, on FreeBSD: sha256 -c SHA256SUMSNo installation: chmod +x and run. Every program opens the members read-only and never writes to the pool.
What is in v0.8.5
What the disk never gave, and what the disk still says.
An image's ddrescue map is honoured: sectors the imager could not read
are refused by name, not read as data, and a bad sector on any member
costs its sector rather than its block. A disk's own names — a GPT
label, a gptid, a glabel — tie a member whose labels are gone to the
leaf its siblings describe, and a member with no anchor at all is found
at the base their asize bounds. Three-way mirrors and pools of several
mirrors are fixtures now, and ashift 9 and 13 are cross-checked against
zdb.
Added
-
The imager's map (SPEC F-72), and retries per sector (F-33). An
image of a failing disk is made with GNUddrescue, and the mapfile
beside it is the only record of which bytes the disk actually gave.
Where it could not read, the image holds zeros that look like data,
and a block crossing them failed its checksum with nothing to say
why.--map MEMBER=MAPFILEreads the map — its own format, comments
and status line and hex positions — and every unfinished range is
refused before it is read, the way a disk refuses a bad sector, so
the block takes the F-33 path: kept as far as the imager read it,
zeros for the rest, and the reason names the imager.scanreports
the map per device: bytes never read, by status, and which of the
four labels the unread ranges touch. The mapfile is hashed into the
evidence record as an input. And--retries N(default 1) reads a
refused sector that many more times before giving it up, for the
marginal sector on a live disk that gives on the third try — an
image file pays nothing for it. Prompted by a Habr write-up of a
NAND recovery that read every failing page up to 128 times: the
lesson for a tool that starts from images is that the image's own
record of what was never read has to be honoured. -
A member with no anchor at all is found at the base its siblings
bound (SPEC F-62, the other half). F-62 could bind a label-less
member to a leaf by reading through it, but only at a base something
on the member had confirmed: a surviving uberblock ring, or a
partition table. Every label gone — rings included — and the member
somewhere inside a larger image with no table left, and the binding
tried base 0, read garbage and said nothing reads through it.The siblings' labels say how big a leaf of that top is (
asize, plus
the labels at both ends), which bounds where its vdev can begin. The
trial now tries 0 and every 1 MiB multiple that leaves room for such
a leaf — the first and last 512 of them on a disk large enough to
have more, since a member is at the front of its disk or its tail is
at the end — and the base that walks is the one the member is read
at. Confirmed by the checksums the walk verifies, never assumed, and
said out loud:read as leaf … with its vdev at byte N. The fixture
can now write a realasizefor it. -
Fixtures for a three-way mirror and for a pool of two mirrors.
Both shapes were read before — a mirror is any number of sides, and
a pool is any number of top-level vdevs — but onlyztestever
produced them, by chance: a side attached during its run, a vdev
added. Nowmkfixture mirror3builds the widened mirror and
mkfixture stripeda pool whose MOS is onmirror-0and whose
volume's data is onmirror-1, three leaves wide, so that a read
has to cross tops to finish. Every push checks: any one side of the
three-way mirror is a whole copy; the damage-matrix shape (one side's
labels gone, another absent, the third describing the pool) binds the
bare side by reading (F-62) and the volume comes out; the two-mirror
pool reads with one side of each; and with every member ofmirror-1
gone the MOS still lists the volume while its blocks are refused by
the name of the top nothing describes — zeros with the reason, exit 4
— because a member's label describes only its own top-level vdev,
which is how ZFS labels work and what makes that top unrecoverable
without--hints. -
A bad sector costs its sector, not its block (SPEC F-33). A disk
with one unreadable sector refuses the whole 8 KiB read that touches
it, and with no other copydumpwrote 8 KiB of zeros — 7.5 KiB of
which the disk would have given. The real-world matrix had carried
"read per sector on EIO — to implement" since it was written.A read a member refuses is now read again in 4 KiB pieces and, inside
a piece that still fails, sector by sector; what comes back is kept
and only the refused sectors are zeros. Each zeroed sector is its own
range inbad,blocks_salvagedcounts the blocks it happened to,
and the exit code is 4 as for any image with zeros in it. What was
kept is unverified — the checksum covers the whole block and part of
it is gone — and the range's reason says so, because a forensic
report that called those bytes verified would be lying.Only what can be kept in part is: an uncompressed, unencrypted block
on a disk or mirror. A compressed block cannot be decompressed in
part, ciphertext cannot be decrypted in part, and under raidz or
dRAID a partial column is reconstructed rather than kept, so those
are zeroed whole as before, with the reason naming which. A mirror
still heals from its other side first, and nothing is salvaged that
could be read whole; a read that fails once and then gives every
sector counts only if its checksum then agrees. Tested against a
source that refuses reads touching given ranges the way a disk with
bad sectors does; the read error a mirror reports also stopped
saying "no present member" about a member that is present and
failing. -
The names a disk gives itself tie a bare member to its leaf (SPEC
F-71). On FreeBSD a member is usually handed to ZFS as
/dev/gpt/NAME,/dev/gptid/…or/dev/label/NAME, and the pool's
labels record that string as the member'spath.scanalready read
the GPT partition name and did nothing with it; the partition's own
GUID andglabel's metadata were not read at all. Now every device
lists the names its disk carries — GPT name and GUID, and any GEOM
class in the last sector of the device or of a partition — and where
a pool has a vacant leaf whose recordedpathis one of those names,
the scan says which device that is and spells out the
--assume-memberthat binds it. The binding is still confirmed by
reading, exactly as F-62 does: the name says what the disk was
called, not what is on it. A name that merely contains a pool's name
(tank-sparebesidetank) is a hint and binds nothing.Two more things fall out of reading the GEOM sector. The metadata
records the size of the provider it sits on, and ZFS was given one
sector less than that and placed its rear labels against that end —
so a member whose size the sector carries across a 256 KiB boundary
now shows all four labels instead of three. And ageliprovider is named as
encrypted rather than reported as a disk with no ZFS on it.Checked on FreeBSD 15 in CI with the real tools:
gpart add -land
glabel labelonmddevices, the GUID read back equal to what
gpart listprints, and the volume out of the labelled member equal
to the volume out of the plain one. -
The ends of the ashift range, against
zdb. The fixtures were
already built at ashift 9 and 13 as well as 12 (CI step "Every
ashift"), but the cross-check against OpenZFS userland ran every
geometry at 12. Two moreztestpools — raidz2 at ashift 13 and a
mirror at 9 — now go through the same thirteen steps as the others,
so the stride that decides whether a RAIDZ column read lands on the
right sector is compared againstzdbat both ends and not only at
the middle. The real-world matrix rows say 9, 12 and 13 accordingly.
Full history: [CHANGELOG.md](https://github.com/click0/zvolrescue/blob/v0.8.5/CHANG...
zvolrescue v0.8.0 — A vdev that is gone, a dataset's own settings, and what a hash is for
zvolrescue v0.8.0 — read-only extraction of zvols from ZFS pools that no longer import: labels, uberblocks, mirror/RAIDZ/dRAID reconstruction, every checksum and compression, encrypted datasets with a key.
0.x release. Validated in CI against OpenZFS 2.2 userland pools (
ztest+zdb) and on the environments listed in docs/REALWORLD-TESTS.md; treat the output as evidence to verify, not as a finished recovery.
Downloads
15 binaries: 5 programs, each built for the same three platforms. The name is always <program>-0.8.0-<platform>.
| Program | What it is for |
|---|---|
zvolrescue |
the pool, and a volume out of it: what the members hold, and the image of a zvol |
zvoltimeline |
the pool history the transaction groups still carry: when a dataset was made, destroyed, and whether its space is still held |
zvolcarve |
volumes no uberblock points at any more, found by scanning raw vdev space |
zvolfiles |
files out of filesystem datasets: the tree with its metadata, or a raw object dump when the metadata is gone |
zvolreport |
the evidence logs of the others as one document, hashed, signed and checkable by a third party |
| Platform | <platform> |
Notes |
|---|---|---|
| Linux x86_64 | x86_64-linux-musl |
static; runs on any distribution, and on a rescue medium |
| Linux aarch64 | aarch64-linux-musl |
static |
| FreeBSD 15.x amd64 | amd64-freebsd |
built on 15.x. FreeBSD changes its ABI between major branches, so on 14.x build from source: pkg install rust && cargo build --release |
So the carver for Linux x86_64 is zvolcarve-0.8.0-x86_64-linux-musl, and the FreeBSD main binary is zvolrescue-0.8.0-amd64-freebsd.
SHA256SUMS covers all 15 of them:
sha256sum -c SHA256SUMS # or, on FreeBSD: sha256 -c SHA256SUMSNo installation: chmod +x and run. Every program opens the members read-only and never writes to the pool.
What is in v0.8.0
A vdev that is gone, a dataset's own settings, and what a hash is for.
A pool a top-level vdev was removed from now reads, through the mapping
the removal left in the MOS, and a feature this build cannot account for
stops the read by name instead of letting it guess. list --properties
shows what a dataset was written with, user properties included. dump --hash md5,sha1 takes the legacy digests in the same pass as SHA-256
and zvolreport verify checks every digest a record carries back.
zvolcarve zeropoint and zvolcarve roots give two more ways into a
pool nothing points into. And an image dump had to fill with zeros is
exit 4, not a success.
Added
-
dump --hash md5,sha1: the legacy digests, taken in the same pass
(SPEC F-53). An extracted image usually leaves for somewhere else,
and what is waiting there may be an acquisition log, a
case-management system or a hashset that only speaks MD5 or SHA-1.
Taking one afterwards costs another full read of a multi-terabyte
image, so the moment to take it is while the bytes are going past —
which is what the requirement said and what half of it had been doing
for SHA-256 alone.Neither is offered as a check on whether the image is right. Both
have practical collision attacks, which matters when someone may have
chosen the bytes and does not matter for lining an image up against a
record written before this tool touched it. That is what they are
for, and the module says so where someone might otherwise reach for
one as a verifier.What was taken goes into the evidence record, into the report, and
intozvolreport verify, which now checks every digest a record
carries rather than only its SHA-256 — one result per digest, with
the file read once for all of them. A hash that is written down and
never checked back is decoration.zvolcarve dumptakes the same flag: it writes images too.Checked against the tools a forensic toolchain would actually use:
sha256sum,sha1sumandmd5sumagree with all three on a real
32 MiB sparse image, the published RFC 1321 and FIPS 180 vectors are
unit tests, and a resumed extraction feeds the prefix into each
digest so that stopping and continuing comes to what one pass would
have. One byte changed in a finished image, and all three say so. -
list --properties: what a dataset has set, user properties
included (SPEC F-14). The properties ZAP was parsed as far as its
object number and never read. It holds what an operator holding a
dead pool most wants next after the dataset list: whatcompression
andchecksumthis volume was written with, whatrecordsizeit
used, and whatever the shop's own bookkeeping put in a user property.Only what was set here is on disk. A property nobody set is
inherited from an ancestor or is the pool's default, and neither is
written down anywhere — so this reports what the dataset holds and
says so, rather than computing an effective value out of a table of
defaults that no disk would confirm. The ancestors are in the same
listing, which is where an inherited value comes from.A number keeps its number unless the build can show its working.
checksumanddedupcarry the samezio_checksumenumeration a
block pointer carries, which this tool already decodes on every block
it reads and the cross-check compares againstzdb.compression
carries azio_compresscode in its low seven bits with azstd
level above them — and that split was measured, not remembered:
across the five pools the cross-check builds and two more with vdevs
removed, nineteen distinct values appear, and every one either fits
in seven bits (on, lzjb, zle, lz4) or has exactly16— zstd — in
them with 1..=19 or 100..=120 above. Nothing else produced a high
part at all. Everything else —copies,recordsize, every on/off
flag — prints the number the pool holds, because a name this build
cannot evidence would be worse than the number.Checked against
zdbon a realztestpool: twenty properties over
eight datasets, name for name and value for value, with an empty
diff. In CI a fixture volume carries two of ZFS's own properties
and a user property — which forces a fatzap, since a microzap entry
is one 64-bit integer and cannot hold a string at all — and the
listing is compared in both text and JSON. Asking is what separates
"none set" from "not asked": without the flag no properties are read
at all, and with it a dataset that set none shows an empty list. -
A pool a top-level vdev was removed from now reads (SPEC F-69).
zpool removedoes not free what was on a vdev: it copies those
blocks onto the vdevs that remain and leaves in its place an
indirectvdev holding nothing but a record of where each range of
its old address space went. The block pointers are never rewritten —
that would mean walking every pointer in the pool — so they go on
naming a vdev that no longer exists, and every one of them was
refused:DVA names unknown top-level vdev 3. It came up three times
in one afternoon onztestimages that happened to have had a vdev
removed during the run, which is what put it at the top of the list.That mapping is now read and the addresses translated. A range the
removal copied in pieces is several entries and is joined back
together; a piece that landed on a vdev itself removed later is
followed through that one too, which real pools do —ztestproduced
one with three removed vdevs where one maps onto another. Where the
destination is a mirror, each of its sides is still offered as a
separate copy, so redundancy survives the translation.The mapping comes from the pool's own account of itself, not from
the labels. No label describes a removed vdev: removing it is what
took its members away. What does describe it is the configuration
object in the MOS — and reading it there rather than anywhere else is
also what keeps an older transaction group honest. Asked for a txg
from before the removal, the configuration of that txg has no
indirect vdev in it, and nothing is translated, because at that point
nothing had been.scanasks rather than guesses. A removed vdev is still counted in
vdev_childrenand still has no member to find, so from the labels
alone it is a missing vdev — and a first version of this said exactly
that, calling a pool with every live member present "NOT readable" in
the summary line andreadable: falsein the JSON a script keys on.
Nowscanasks the pool's configuration object which undescribed
vdevs were removed, only when there is one to ask about and
device_removalis active (a healthy pool never opens its MOS for
this), and reports them as removed with the pool readable. When the
MOS cannot be reached it says so and leaves them missing, which they
may well be. The same answer reaches--assume-member, which no
longer counts a vdev that is gone on purpose against the leaf being
bound.com.delphix:device_removalmoves to the features this build honours
andcom.delphix:obsolete_countsdeliberately does not. The counts
are bookkeeping over the same mapping, and a pointer that still names
the removed vdev is live whatever they say — but no pool measured
against this build has had that feature active, and F-70's rule is
that nothing is honoured here on reasoning alone.Checked against
zdbon two realztestpools with vdevs removed
mid-run, one with three and one with four: the mapping objects match
entry for entry and byte for byte, and every object comes out — 343
of 343 and 279 of 279, with nothing refused. In CI a fixture whose
volume is addressed entirely on a removed vdev extracts to the same
sha256 as the same volume on a pool that never had one, and the same
blocks are refused by name when the mapping is not...