Skip to content

About

DragonFly BSD's HAMMER2 file system as a Linux kernel module, built the way the FreeBSD, NetBSD and OpenBSD ports are: the core carried unchanged, an OS shim beneath it

Topics

Resources

Contributing

Stars

2 stars

Watchers

0 watching

Forks

Repository files navigation

Linux HAMMER2

CI License: BSD-3-Clause Status Kernel

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_DATA and SEEK_HOLE so a sparse file copies and archives as one, FIEMAP so filefrag reads the layout, and a freeze fsfreeze can 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.md has 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 in doc/ROOTFS_MILESTONES.md
  • measured: small-file create and delete rates, mixed read/write with mmap and fsync storms, and an export served by nfsd, by million-tree.sh, storm.sh and nfsd-export.sh; a sustained soak is the same load over H2_STORM_CYCLES cycles, each checked and reclaimed on its own, and the readings are in doc/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.

Where it stands

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.

Why a kernel module, when hammer2-fuse exists

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.

Requirements

  • 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 version script/test-syntax.sh pins as KERNEL_REF and 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 the create operation's signature. A build against 7.2 failed on exactly those on 2026-09-05, recorded in doc/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.md records 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_INFLATE and CONFIG_XXHASH, built in or as modules: the codecs HAMMER2 compresses and decompresses with and the digest in every blockref. Distribution kernels carry all five; a defconfig leaves the LZ4 compressor out, and the build names the missing one

  • CONFIG_CRC32 and CONFIG_CRYPTO_LIB_SHA256, for the crc32c and SHA-256 the module imports

  • userland from hammer2-utils, which already supports Linux

Build

    $ 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.

Install

    $ cd linux_hammer2
    $ make install

Uninstall

    $ cd linux_hammer2
    $ make uninstall

Test

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.

Documentation

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

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.

Credit

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.

License

BSD-3-Clause, matching DragonFly and the BSD ports, and listed in the Linux kernel's own LICENSES/preferred/. See COPYRIGHT.

About

DragonFly BSD's HAMMER2 file system as a Linux kernel module, built the way the FreeBSD, NetBSD and OpenBSD ports are: the core carried unchanged, an OS shim beneath it

Topics

Resources

Contributing

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages