Corrupting a ZFS File on Purpose

zdw2 pts0 comments

Corrupting a ZFS File on Purpose - oshogbo//vx

oshogbo//vx<br>Breaking your code

Corrupting a ZFS File on Purpose

June 5, 2026, 10:31 a.m.

Most of the time, the whole point of ZFS is that your data does not get corrupted.<br>But during development you sometimes need the opposite: a controlled, reproducible corruption, so you can watch self-healing kick in, see what a scrub reports, or just understand how a file maps onto the physical disk.<br>There is no better exercise than breaking one byte on purpose and seeing ZFS struggling.

The safe rule is simple: do this only on throwaway pools backed by throwaway files.<br>Pointing these commands at a real disk would be less of a lesson and more of a confession.

This is the story of doing exactly that on Linux, the lazy way and the educational way.

The lazy way

If you just want a corrupted file and you do not care how it happened, ZFS has a tool for that.<br>After creating a file on a ZFS filesystem, zinject will cause for data blocks to come back with a checksum error:

# zinject -t data -e checksum -a /tmp/zfs-blog-flow/single-mnt/file.bin<br>Added handler 1 with the following properties:<br>pool: zblog1<br>objset: 54<br>object: 3<br>type: 0<br>level: 0<br>range: all<br>dvas: 0x0

You can list the active handlers:

# zinject<br>ID POOL OBJSET OBJECT TYPE LVL DVAs RANGE<br>1 zblog1 54 3 - 0 0x00 all

And clear them again when you are done:

# zinject -c all<br>removed all registered handlers

# zinject<br>No handlers registered.<br>Run 'zinject -h' for usage information.

That is it.<br>zinject injects simulated corruption into a live pool<br>It is a great tool, heavily used in the ZFS test suite.

It is also completely unsatisfying if what you actually want is to understand where the bytes live.<br>For that, we have to do it by hand.

A pool made of files

I do not want to corrupt a real disk.<br>Not for moral reasons. I just don't have one lying around.<br>Yes, I could use a VM with a virtual drive, but plain files are simply easier for demonstrating the idea.<br>So the first step is to build pools out of plain files under /tmp/zfs-blog-flow.<br>Every "disk" is then a file I can open with dd and a hex editor, which is the entire trick.

$ mkdir -p /tmp/zfs-blog-flow/single-mnt /tmp/zfs-blog-flow/raidz-mnt<br>$ cd /tmp/zfs-blog-flow<br>$ truncate -s 512M single.img<br>$ truncate -s 512M r1.img<br>$ truncate -s 512M r2.img<br>$ truncate -s 512M r3.img<br>$ truncate -s 512M r4.img

From here on I work from inside /tmp/zfs-blog-flow, so the backing files are just single.img, r1.img, and so on.

I will build two pools, because they fail in different ways.<br>First a single-vdev pool, with no redundancy at all:

# zpool create -f -O atime=off \<br>-O mountpoint=/tmp/zfs-blog-flow/single-mnt \<br>zblog1 /tmp/zfs-blog-flow/single.img

And then a four-file RAIDZ2 pool, with parity:

# zpool create -f -O atime=off \<br>-O mountpoint=/tmp/zfs-blog-flow/raidz-mnt \<br>zblogR raidz2 \<br>/tmp/zfs-blog-flow/r1.img \<br>/tmp/zfs-blog-flow/r2.img \<br>/tmp/zfs-blog-flow/r3.img \<br>/tmp/zfs-blog-flow/r4.img

Both come up online:

# zpool status zblog1 zblogR<br>pool: zblog1<br>state: ONLINE<br>config:

NAME STATE READ WRITE CKSUM<br>zblog1 ONLINE 0 0 0<br>/tmp/zfs-blog-flow/single.img ONLINE 0 0 0

errors: No known data errors

pool: zblogR<br>state: ONLINE<br>config:

NAME STATE READ WRITE CKSUM<br>zblogR ONLINE 0 0 0<br>raidz2-0 ONLINE 0 0 0<br>/tmp/zfs-blog-flow/r1.img ONLINE 0 0 0<br>/tmp/zfs-blog-flow/r2.img ONLINE 0 0 0<br>/tmp/zfs-blog-flow/r3.img ONLINE 0 0 0<br>/tmp/zfs-blog-flow/r4.img ONLINE 0 0 0

errors: No known data errors

One habit is worth remembering, because file-backed pools are not where zpool looks by default.<br>To let an import find a pool sitting in a plain directory, point it at that directory:

$ zpool import -d .

We will need that later, when the pool is exported and we are corrupting its backing files behind its back.

Following one file down to the hardware

Start with the single-vdev pool.<br>Write a file with an easy-to-recognize pattern, and let us go find it:

$ yes 'SINGLE-ZFS-CORRUPTION-DEMO-BLOCK' | head -c 1M > single-mnt/file.bin<br>$ sync

Get its inode, size, and block usage:

$ stat -c 'path=%n inode=%i size=%s blocks512=%b' single-mnt/file.bin<br>path=single-mnt/file.bin inode=2 size=1048576 blocks512=21

A 1 MiB file, 21 sectors on disk.<br>Noted.

Now hand that object number to zdb and ask it to describe the object in detail:

# zdb -ddddd zblog1/ 2<br>Dataset zblog1 [ZPL], ID 54, cr_txg 1, 34K, 7 objects, rootbp<br>DVA[0]= [L0 DMU objset] fletcher4 lz4 ...<br>size=1000L/200P birth=14L/14P fill=7 cksum=...

Object lvl iblk dblk dsize dnsize lsize %full type<br>2 2 128K 128K 10K 512 1M 100.00 ZFS plain file<br>176 bonus System attributes<br>dnode flags: USED_BYTES USERUSED_ACCOUNTED USEROBJUSED_ACCOUNTED<br>dnode maxblkid: 7<br>path /file.bin<br>uid 0<br>gid 0<br>atime Thu Jun 4 23:46:26 2026<br>mtime Thu Jun 4 23:46:34 2026<br>ctime Thu Jun 4 23:46:34 2026<br>crtime Thu Jun 4 23:46:26 2026<br>gen 12<br>mode 100664<br>size 1048576<br>parent 34<br>links 1<br>pflags 840800000004<br>projid 0<br>Indirect blocks:<br>0 L1 0:1ba00:400 20000L/400P F=8 B=14/14...

file blog flow single pool online

Related Articles