Linux HAMMER2
A port of DragonFly BSD's HAMMER2 file system to the Linux kernel.
HAMMER2 is Matthew Dillon's copy-on-write filesystem for DragonFly BSD: snapshots, transparent compression, block-level deduplication, an integrity check on every block, and a design that has been in production use for a decade. Tomohiro Kusumi has since ported it to FreeBSD, NetBSD and OpenBSD, and written the Linux userland (hammer2-utils, hammer2-fuse). The kernel side on Linux is the piece that has been missing.
This is that piece, built the way his three ports are built: the DragonFly core is carried and kept readable as DragonFly's, with every port edit marked in place, and an OS shim makes it compile. Same file names, same shim split, same section order, so a fix found here is legible to the other three and can travel.
It mounts a HAMMER2 volume read-write, lists it, reads it, follows its symlinks, writes to it, takes snapshots of it, and refuses media whose checksums do not match. Files on it can be mapped and executed, so a volume can be a root filesystem, and one has booted as one: a static init on an otherwise empty volume, once, on a guest.
At a glance, on a kernel of 7.3 or newer:
- works: mount read-write by PFS label, read, write, mmap and exec,
snapshots and writes into them, a filesystem across two volumes,
PFS create, delete and list through
hammer2-utils, LZ4 and ZLIB media, deduplication, crash recovery to the same tree as the FreeBSD port,SEEK_DATAandSEEK_HOLEso a sparse file copies and archives as one,FIEMAPsofilefragreads the layout, and a freezefsfreezecan drive - works with a caution: a volume filled to capacity refuses the fill and
keeps every accepted file whole, after seven defects in that path were
found and fixed; read the paragraph on it below before trying it. A
real Nix closure of two hundred thousand files copies in and reads
back beside squashfs, erofs and ext4, identical on a 4 GiB guest with
four writers, the write path taking a folio smaller than its block
and the device side a buffer of its own when the page cache cannot
give a 64 KiB folio;
doc/IO_MODEL.mdhas both and the run - new and narrow: a SLAVE PFS on a second device of the same host is kept
in step with its MASTER by DragonFly's synchronization thread, and a
cluster of two MASTERs on two devices is written and read through the
quorum that requires both to agree, every file reading back what was
written; both on the debug and the sanitizer kernel. A master on
another host is read too: over the transport and message core carried
since P5 and P6, a slave's synchronization pass on one guest copies the
master's PFS on another through a remote element, from a snapshot the
master takes at the slave's request, and P7's five criteria read on
two hosts as well as on one (
doc/CLUSTER_PROTOCOL_DESIGN.md, stages 1 and 2). The link is plaintext with a peer admitted by a marker file until kernel TLS lands in P8's TLS stage, and P7 closes then. No run yet of a quorum that cannot be met, and no write through a PFS with a remote master, which stays read-only until P8's lease protocol - not yet: a package, and any write to media that is not a scratch image
- not built:
O_DIRECT, which is refused by decision. What a root filesystem needs and where each need stands is indoc/ROOTFS_MILESTONES.md - measured: small-file create and delete
rates, mixed read/write with mmap and fsync storms, and an export
served by
nfsd, bymillion-tree.sh,storm.shandnfsd-export.sh; a sustained soak is the same load overH2_STORM_CYCLEScycles, each checked and reclaimed on its own, and the readings are indoc/history/verification-record.md
Every write operation has been run on scratch media and read back by
DragonFly, in both directions of a round trip, and the crash matrix, a
writer killed, a kernel panicked, the power cut and a volume header
torn, leaves media that this port and the FreeBSD port both recover to
the same tree. find walks the whole tree and every file compares byte
for byte against the tree it was made from, at 511 bytes, at 512, at one
page, at one 64 KiB block and at 200 KB, on images written with LZ4 and
with ZLIB compression, and on media created and written by DragonFly
itself.
v1.0.0 and v1.0.4 are tagged, and the media it has written to are
scratch copies on a guest. To try a later point that has
been through the fleet, take the newest row of
CHANGELOG.md: every row pins the commits that delivered
it and names the runs that verified them, which is what a tag would say. See
doc/README.status.md for exactly what exists and
what has been verified, and doc/README.roadmap.md
for the order the rest lands in. There is no schedule attached to any of
it.
The version is a position on the roadmap, not a release: the first two numbers name the milestone reached and the third counts point releases inside it. The tree is at 1.0.72, and CHANGELOG.md pins every row to the commits that delivered it and the runs that verified it.
Be careful about filling a HAMMER2 volume to capacity. HAMMER2 needs free
space for the flush that commits a fill, and every tree this port
carries from keeps a reserve for it. The port carries that check, with
the page cache's dirty pages counted against the reserve, so a fill is
refused with every accepted file whole on the media. Seven defects in
that path were found and fixed on the way there. script/test-enospc.sh
fills a volume on every run and compares each file's hash with the
media, and doc/README.status.md has the account.
To try it, make an image, put it on a loop device and mount the PFS by
label. newfs_hammer2 is in hammer2-utils. A mount that names no PFS
asks for DATA, which newfs_hammer2 creates only when given no -L,
so the mount below names @TEST:
$ make && sudo make install
$ sudo modprobe hammer2
$ truncate -s 8G scratch.img
$ newfs_hammer2 -L TEST scratch.img
$ sudo losetup -f --show scratch.img
$ sudo mount -t hammer2 /dev/loop0@TEST /mnt
losetup prints the device it chose; /dev/loop0 above is that device.
modprobe rather than insmod, because the codecs and the digest may
be modules on your kernel and only modprobe loads them first.
Add -o ro to read without writing; mount -o remount,rw goes the
other way and runs the flush recovery on the transition, refusing only
when the device itself is write-protected, while a remote element is
registered on the PFS, or when that recovery fails. A volume DragonFly made
mounts the same way.
hammer2-utils drives the ioctls once a PFS is mounted:
$ sudo hammer2 -s /mnt pfs-list
$ sudo hammer2 -s /mnt snapshot /mnt before-upgrade
$ sudo hammer2 -s /mnt pfs-delete before-upgrade
Give -s <mount> before the subcommand, which is not optional here even
though it is on the BSDs. Without it the utility looks the mount up
through libfs, whose Linux build reports no mounted filesystems at
all, so anything that routes by mount reports the PFS as not found.
That is a defect in the utility rather than in this driver, and
doc/upstream/libfs-linux-get_mnt_info.md is the report against it. The kernel log carries every refusal by name, and a report of
anything the driver does that DragonFly would not, with the image if you
can share it, is the most useful thing a tester can send: the
issue tracker is
open, and doc/README.status.md says what has already been measured so
you can see whether your case is new.
If you have ported this filesystem before, or maintain one of the trees it comes from, the most useful thing you can do is tell us a port decision is wrong. They are listed with their reasoning in doc/README.porting.md, and several are load-bearing.
hammer2-fuse already reads HAMMER2 on Linux in userspace, and for inspecting a volume it is the easier answer. A kernel driver buys three things FUSE cannot: the page cache holds the filesystem's own buffers rather than copying through a userspace daemon, the volume can be a root filesystem, and the write path can order its flushes against the block layer directly, which is what a copy-on-write filesystem's crash consistency depends on.
Neither replaces the other. They are independent implementations, not two
front ends on one core: hammer2-fuse is Rust over libhammer2, this port
is C carried from DragonFly. That independence is what makes hammer2-fuse
a useful cross-check - a second reader sharing no code with this one, so
agreement between the two says something about the on-disk format rather
than about a shared bug.
-
Linux 7.3 or newer, enforced by an
#error. The floor is the 7.3 line by decision, not by accident: 7.2 is an ordinary stable series, and 7.3, which shipped on 2026-10-18, is expected to be the next longterm, so a floor at 7.2 would buy one cycle and charge for the migration twice. The kernel of record is one tree, the versionscript/test-syntax.shpins asKERNEL_REFand refuses to report a pass against anything else; the pin moved through 7.3's release candidates to the release, because running on the line and pinning the floor to it are the same decision here. There is no conditional compilation on the kernel version in the tree. Two things put it at 7.3, both from that release's VFS changes: the shared block device open that lets several PFSes on one device be mounted, and thecreateoperation's signature. A build against 7.2 failed on exactly those on 2026-09-05, recorded indoc/history/verification-record.md; no later 7.2 build is recorded. Older kernels are not blocked by anything deeper: measured from below, 6.18 needed two compatibility conditionals in September and seven in October, as the port took up what 6.19 to 7.3 added, and 6.15 is where the block layer first holds a 64 KiB folio. The ruling keeps the kernel of record as the floor, and backward compatibility, if it is ever provided, goes to 6.18 alone, the one longterm kernel in that range, revisited after 1.0.0;doc/README.roadmap.mdrecords the list -
kernel headers for the running kernel
-
CONFIG_TRANSPARENT_HUGEPAGE, without which the block layer caps the block size below HAMMER2's 64KB physical buffer -
CONFIG_LZ4_COMPRESS,CONFIG_LZ4_DECOMPRESS,CONFIG_ZLIB_DEFLATE,CONFIG_ZLIB_INFLATEandCONFIG_XXHASH, built in or as modules: the codecs HAMMER2 compresses and decompresses with and the digest in every blockref. Distribution kernels carry all five; adefconfigleaves the LZ4 compressor out, and the build names the missing one -
CONFIG_CRC32andCONFIG_CRYPTO_LIB_SHA256, for thecrc32cand SHA-256 the module imports -
userland from hammer2-utils, which already supports Linux
$ cd linux_hammer2
$ make
make builds against /lib/modules/$(uname -r)/build. Point KDIR at a
different tree when the running kernel is not the one you mean to build
for, which is the usual case on a development machine:
$ make KDIR=/path/to/linux-7.3/build
Three build knobs, the same three the FreeBSD and NetBSD ports carry:
HAMMER2_INVARIANTS turns on KKASSERT and KASSERTMSG,
HAMMER2_MALLOC turns on the allocation leak counters, and
HAMMER2_ATIME stores the access time a read stamps and reports it,
instead of returning the modification time in its place, which is what
DragonFly does and the default here does. Pass them on the make line.
$ cd linux_hammer2
$ make install
$ cd linux_hammer2
$ make uninstall
Eighteen gates. The compile gates need a toolchain and a kernel tree:
$ bash script/test-shim.sh # needs only a C compiler
$ bash script/test-ccms-lock.sh # needs only a C compiler
$ bash script/test-transport.sh # needs only a C compiler
$ bash script/test-syntax.sh # needs kernel headers and clang
$ bash script/test-checkpatch.sh # needs scripts/checkpatch.pl
The repository gates check the documentation against the tree. They are POSIX sh over grep, sed and git, so they run anywhere:
$ bash script/test-inventory.sh # the lists that claim to cover src/ and test/
$ bash script/test-citations.sh # every file:line citation in doc/
$ bash script/test-history.sh # every changelog row's commit hash
$ bash script/test-provenance.sh # every file under src/ has an origin row
$ bash script/test-absence.sh # every "X() is not carried" resolves against src/
$ bash script/test-doc-prose.sh # vale over every tracked document
$ bash script/test-report.sh # a committed kernel report is a whole capture
$ bash script/test-reverts.sh # a take-back of recent work carries its reason
test-provenance.sh is the one of those that asks a question this
repository cannot answer alone: a row claiming a byte-for-byte carry is
re-run with cmp against the origin clone, and where a row makes that
claim and the clone is not on the machine it reports COULD-NOT-RUN
rather than passing on a table that only agrees with itself.
One is neither. script/test-vectors-contract.sh freezes the output and
the constant spelling of the two files in test/ that a gate in another
repository compiles, so an edit that breaks that consumer fails here
rather than there:
$ bash script/test-vectors-contract.sh
And one parses the gates that declare #!/bin/sh with shells that are not
bash, because every gate here is normally run by bash and a bash-only
construct in such a script breaks only when something honors the shebang:
$ bash script/test-posix.sh
And one runs the selftests the analysis tools under script/ carry, which
nothing else ran, so the readings they produce are checked rather than
remembered:
$ bash script/test-tools.sh
Two need a guest, and exit 2 without one:
$ bash script/test-fixtures.sh # mounts every fixture on a guest and compares manifests
$ bash script/test-enospc.sh # fills a volume, checks what it kept and what each writer was told
No gate here is trusted on silence alone, because a check whose healthy
signature is silence cannot otherwise be told from a check that never ran.
How that is bought differs by gate, and they are not interchangeable:
test-shim.sh, test-ccms-lock.sh, test-transport.sh and test-syntax.sh
carry built-in controls that must fail on every run, test-checkpatch.sh compares against
a recorded deviation set rather than asking for silence, and each
repository gate asserts the population it searched before checking
anything, so a glob that matches nothing cannot report a clean run.
Exit 2 means the instrument could not run, which is not a verdict on the code. doc/README.testing.md has the detail, including what each gate cannot catch.
| CHANGELOG.md | every point release, pinned to its commit and its verifying gate |
| doc/README.status.md | what exists, what is verified, what is missing |
| doc/history/verification-record.md | every measurement in the order taken, with the instrument and what it found |
| doc/README.md | which document answers which question |
| doc/STATUS_SNAPSHOT.md | the short current status, one page with no history |
| doc/CAPABILITIES_MATRIX.md | the operator matrix: what each feature does and its limits |
| doc/USER_GUIDE.md | formatting, mounting, snapshots, dedup, compression, sync |
| doc/README.roadmap.md | the order, with the check that ends each step |
| doc/ARCHITECTURE.md | the layering, the shim boundary, the object and locking model |
| doc/IO_MODEL.md | the DIO layer, and which 64 KiB assumptions are format and which are ours |
| doc/CCMS_DESIGN.md | the P3 design note: what cache coherency was, and what completing it has to prove |
| doc/SYNC_DESIGN.md | the P7 design note: what remote synchronization is in the design, and what reaching a master on another host has to prove |
| doc/CLUSTER_PROTOCOL_DESIGN.md | the P8 design note, joint with P7's remote reach: the wire format, a master on another host as an element, the constraints the carried core puts on a quorum between masters, and the decisions that stop at the note |
| doc/README.porting.md | every port decision and why |
| doc/README.testing.md | what the gates prove and what they cannot |
| doc/README.capabilities.md | what the port declares it provides, each row on a measurement |
| doc/README.kernel-style.md | licensing, and the path to mainline |
| doc/README.maintenance.md | how the carried core is kept current, and how a defect goes back |
| doc/SECURITY_MODEL.md | what the port guarantees, and what the cluster link does not yet |
| doc/PACKAGING_NOTES.md | what a package or an initramfs needs to carry |
| doc/PERFORMANCE.md | dated rate readings, each with the run that holds it |
| doc/REPRODUCIBILITY.md | stock Linux plus QEMU to green gates |
| doc/RELEASE_POLICY.md | what the version number means, and which points are tagged |
| doc/UPSTREAM_ENGAGEMENT.md | how drafts for DragonFly and the BSD ports are kept and filed |
| doc/ROOTFS_MILESTONES.md | what a root filesystem needs, and where each need stands |
CONTRIBUTING.md. Questions, corrections and review of the port decisions are as welcome as patches, and more useful at this stage. The most useful test right now is the one above on a distribution kernel at 7.3 or newer, with the kernel log from the mount and from any refusal, since every run so far has been on one maintainer's guests.
Matthew Dillon designed and wrote HAMMER2 for DragonFly BSD. The format, the algorithms and the core carried here are his.
Tomohiro Kusumi wrote the FreeBSD, NetBSD and OpenBSD kernel ports and
the entire Linux userland. This port is his method applied a fourth time,
and it borrows more than the method: the repository layout, the
hammer2_os.h / hammer2_compat.h split and its section order, the make
knobs, the XXX convention at non-mechanical mappings, and the
<os>_hammer2 name. Where a port decision had a precedent in one of his
three trees, that precedent was followed and
doc/README.porting.md says which tree.
The work here is the Linux OS layer and the gates around it.
AI tools assisted with this port. Every line was reviewed, every claim in the documentation traces to a recorded run, and the judgment calls are the maintainer's.
BSD-3-Clause, matching DragonFly and the BSD ports, and listed in the
Linux kernel's own LICENSES/preferred/. See COPYRIGHT.