The scenic route
to Userspace ZFS
– and elsewhere

Rob Norris
TrueNAS

Hello!

  • 🇦🇺 Melbourne, Australia
  • Bunurong Country
  • OpenZFS developer 2022
  • TrueNAS since 2026
  • Previously:
    • sysadmin/devops/SRE, system integrator, web programmer, project manager, team leader, ...
    • chat systems, games, graphics and visuals, hardware & embedded, ...
The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

Userspace
ZFS?

OpenZFS
<on|in|for>
<your thing here>

OpenZFS for ...

  • Linux
  • FreeBSD
The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

OpenZFS for ...

  • Linux
  • FreeBSD
  • macOS
  • Windows
  • illumos
  • NetBSD
The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

OpenZFS for ...

  • Linux
  • FreeBSD
  • macOS
  • Windows
  • illumos
  • NetBSD
  • Haiku
  • AROS
  • Redox
  • Plan 9
  • Hurd
  • FreeDOS
  • TempleOS
The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

OpenZFS for ...

  • Linux
  • FreeBSD
  • macOS
  • Windows
  • illumos
  • NetBSD
  • Haiku
  • AROS
  • Redox
  • Plan 9
  • Hurd
  • FreeDOS
  • TempleOS
  • FUSE
The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

OpenZFS for ...

  • Linux
  • FreeBSD
  • macOS
  • Windows
  • illumos
  • NetBSD
  • Haiku
  • AROS
  • Redox
  • Plan 9
  • Hurd
  • FreeDOS
  • TempleOS
  • FUSE
  • SQLite
  • PostgreSQL
  • MariaDB
The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

OpenZFS for ...

  • Linux
  • FreeBSD
  • macOS
  • Windows
  • illumos
  • NetBSD
  • Haiku
  • AROS
  • Redox
  • Plan 9
  • Hurd
  • FreeDOS
  • TempleOS
  • FUSE
  • SQLite
  • PostgreSQL
  • MariaDB
  • Lucene
  • Prometheus
  • Cyrus
The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

OpenZFS for ...

  • Linux
  • FreeBSD
  • macOS
  • Windows
  • illumos
  • NetBSD
  • Haiku
  • AROS
  • Redox
  • Plan 9
  • Hurd
  • FreeDOS
  • TempleOS
  • FUSE
  • SQLite
  • PostgreSQL
  • MariaDB
  • Lucene
  • Prometheus
  • Cyrus
  • Web servers
The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

OpenZFS for ...

  • Linux
  • FreeBSD
  • macOS
  • Windows
  • illumos
  • NetBSD
  • Haiku
  • AROS
  • Redox
  • Plan 9
  • Hurd
  • FreeDOS
  • TempleOS
  • FUSE
  • SQLite
  • PostgreSQL
  • MariaDB
  • Lucene
  • Prometheus
  • Cyrus
  • Web servers
  • virtio-fs
  • 9pfs
The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

OpenZFS for ...

  • Linux
  • FreeBSD
  • macOS
  • Windows
  • illumos
  • NetBSD
  • Haiku
  • AROS
  • Redox
  • Plan 9
  • Hurd
  • FreeDOS
  • TempleOS
  • FUSE
  • SQLite
  • PostgreSQL
  • MariaDB
  • Lucene
  • Prometheus
  • Cyrus
  • Web servers
  • virtio-fs
  • 9pfs
  • UEFI
  • bootloaders
The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

OpenZFS for ...

  • Linux
  • FreeBSD
  • macOS
  • Windows
  • illumos
  • NetBSD
  • Haiku
  • AROS
  • Redox
  • Plan 9
  • Hurd
  • FreeDOS
  • TempleOS
  • FUSE
  • SQLite
  • PostgreSQL
  • MariaDB
  • Lucene
  • Prometheus
  • Cyrus
  • Web servers
  • virtio-fs
  • 9pfs
  • UEFI
  • bootloaders
  • NFS
  • SMB
  • iSCSI
  • S3
The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

OpenZFS for ...

  • Linux
  • FreeBSD
  • macOS
  • Windows
  • illumos
  • NetBSD
  • Haiku
  • AROS
  • Redox
  • Plan 9
  • Hurd
  • FreeDOS
  • TempleOS
  • FUSE
  • SQLite
  • PostgreSQL
  • MariaDB
  • Lucene
  • Prometheus
  • Cyrus
  • Web servers
  • virtio-fs
  • 9pfs
  • UEFI
  • bootloaders
  • NFS
  • SMB
  • iSCSI
  • S3
  • tools
The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

OpenZFS: The Last Word In ...

File
Systems

Relational
Databases

Object
Storage

Network
Attached
Storage

Block
Storage

Key-value
Stores

Graph
Databases

Archive
Storage

Distributed
Storage

Three views of OpenZFS

XXX draw it properly!

 data plane
 [ZPL, ZVOL]
     |
    ZFS -- control plane [ioctl, libzfs_core, zfs/zpool]
     |
 host services
 [vdev_disk, SPL]
The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

I did some things

Did I do
them right?

ZTS: ZFS test suite

XXX draw it properly!

     /--------- ZTS
     |           |
     v           |
 data plane      |
 [ZPL, ZVOL]     |
     |           v
    ZFS -- control plane [ioctl, libzfs_core, zfs/zpool]
     |
 host services
 [vdev_disk, SPL]
The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

ZTS: ZFS test suite

XXX draw it properly!

         ?<---- ZTS
                 |
                 |
                 v
                 ?

    ZFS



The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

Unit
testing

unit: a unit testing framework

$ make unit T=cityhash
  UNITTEST tests/unit/test_cityhash
Running test suite with seed 0x58e12557...
cityhash.known                       [ OK    ] [ 0.00000472 / 0.00000286 CPU ]
cityhash.specialized                 [ OK    ] [ 0.00000257 / 0.00000205 CPU ]
cityhash.distinct                    [ OK    ] [ 0.00000260 / 0.00000204 CPU ]
3 of 3 (100%) tests successful, 0 (0%) test skipped.
The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

unit: a unit testing framework

/*
 * cityhash maps one to four uint64_t words to a single uint64_t hash.  The
 * output is fixed by the algorithm, so we can verify exact results.
 */
static MunitResult
test_cityhash_known(const MunitParameter params[], void *data)
{
	unit_eq(cityhash1(0), 0x7087061603e53293ULL);
	unit_eq(cityhash1(0x0123456789abcdefULL), 0x4d72820d4fcae8ffULL);
	unit_eq(cityhash2(1, 2), 0x8f1c7927f8b5dff2ULL);
	unit_eq(cityhash3(1, 2, 3), 0x4f6fe08120ecb540ULL);
	unit_eq(cityhash4(0x1111111111111111ULL, 0x2222222222222222ULL,
	    0x3333333333333333ULL, 0x4444444444444444ULL),
	    0xa6370a2070fdfd12ULL);

	return (MUNIT_OK);
}
The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

unit: a unit testing framework

$ make unit T=zap
  UNITTEST tests/unit/test_zap
Running test suite with seed 0x762a0910...
zap.mock_microzap_sanity             [ OK    ] [ 0.00002346 / 0.00002133 CPU ]
zap.mock_fatzap_sanity               [ OK    ] [ 0.00009360 / 0.00009260 CPU ]
zap.zap_basic
  type=micro                         [ OK    ] [ 0.00004231 / 0.00004149 CPU ]
  type=fat                           [ OK    ] [ 0.00022919 / 0.00022771 CPU ]
zap.zap_add
  type=micro                         [ OK    ] [ 0.00006083 / 0.00006056 CPU ]
  type=fat                           [ OK    ] [ 0.00016361 / 0.00016304 CPU ]
...
zap.norm_remove
  type=micro                         [ OK    ] [ 0.00001613 / 0.00001608 CPU ]
  type=fat                           [ OK    ] [ 0.00004699 / 0.00004694 CPU ]
59 of 59 (100%) tests successful, 0 (0%) test skipped.
The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

unit: a unit testing framework

/* zap_add: add new items. */
static MunitResult
test_zap_add(const MunitParameter params[], void *data)
{
	dnode_t *dn = mock_zap_create_params(params, "type");
	dmu_tx_t *tx = (dmu_tx_t *)mock_tx_create();

	/* A key added can be found by that name. */
	uint64_t va = 1, var = 0;
	unit_ok(zap_add_by_dnode(dn, "a", sizeof (uint64_t), 1, &va, tx));
	unit_ok(zap_lookup_by_dnode(dn, "a", sizeof (uint64_t), 1, &var));
	unit_eq(var, 1);

	mock_tx_destroy((mock_dmu_tx_t *)tx);
	unit_true(mock_zap_is_params(dn, params, "type"));
	mock_zap_destroy(dn);

	return (MUNIT_OK);
}

unit: a unit testing framework

$ make unit-coverage T=zap
Coverage: test_zap       | By line         | By branch       | By function
                         | Rate% Total Hit | Rate% Total Hit | Rate% Total Hit
module/zfs/u8_textprep.c | 42.0%   802 337 | 33.7%   510 172 | 50.0%    12   6
module/zfs/zap.c         | 53.5%   583 312 | 46.7%   214 100 | 40.0%    80  32
module/zfs/zap_fat.c     | 47.5%   665 316 | 30.3%   446 135 | 62.2%    37  23
module/zfs/zap_impl.c    | 77.2%   232 179 | 55.5%   146  81 | 84.0%    25  21
module/zfs/zap_leaf.c    | 72.7%   466 339 | 57.9%   216 125 | 82.6%    23  19
module/zfs/zap_micro.c   | 75.6%   238 180 | 52.1%   142  74 | 92.9%    14  13
The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

unit: a unit testing framework

The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

unit: a unit testing framework

$ make unit-coverage-html T=zap
coverage results:
    file:///home/robn/code/zfs-extra/tests/unit/test_zap_coverage/index.html
The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

unit: a unit testing framework

unit: a unit testing framework

In repo:

  • backtrace btree cityhash fletcher namecheck nvpair sha2 timer zap zfs_file

AI nonsense waiting for me to review:

  • aggsum avl multilist objlist range_tree ratelimit refcount rlock rrwlock sa unique zrlock

Work in progress:

  • nvpair_stress zalgo zap
The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

unit: a unit testing framework

Further reading:

  • docs: tests/unit/README.md
  • existing tests: tests/unit/*.c
  • extensions and helpers: tests/unit/unit.h
  • mock DMU: tests/unit/mock_dmu.h
  • µnit: https://nemequ.github.io/munit/
The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

Host
Services

Solaris
Porting
Layer

 Solaris 
Porting
Layer

 Solaris 
 Porting 
Layer

 Solaris 
 Porting 
 Layer 

Some

Stuff

Serious

Shared

Surprising

Standard

Substrate

Soapbox

System

Secure

Service

Software

Sanskrit

Peculiar

Portable

Plumbing

Probability

Provisioning

Primitives

Parity

Portability

Platform

Percolation

Patching

Performance

Profanity

Pasting

Particle

Laboratory

Laughter

Liaison

Lore

Library

Love

Layer

libspl:
SPL for
userspace POSIX

libspl: everything in its place

  • moved sys/zfs_context.h → lib/libspl/include/*.h
    • mutex rwlock condvar thread tsd taskq kmem kmem_cache list kstat procfs_list cred
  • removed userspace variants
    • umem → kmem, tpool → taskq, uu_list → list, uu_avl → avl
The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

libspl: a thread by any other name

typedef {
	pthread_mutex_t		m_lock;
	pthread_t		m_owner;
} kmutex_t;

static inline void mutex_enter(kmutex_t *mp)
{
	pthread_mutex_lock(&mp);
	mp->m_owner = pthread_self();
}

static inline void mutex_exit(kmutex_t *mp)
{
	mp->m_owner = NULL;
	pthread_mutex_lock(&mp);
}

#define	MUTEX_HELD(mp)	pthread_equal((mp)->m_owner, pthread_self())
The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

libspl: a thread by any other name

typedef {
	pthread_mutex_t		m_lock;
	kthread_t		*m_owner;
} kmutex_t;

static inline void mutex_enter(kmutex_t *mp)
{
	pthread_mutex_lock(&mp);
	mp->m_owner = curthread;
}

static inline void mutex_exit(kmutex_t *mp)
{
	mp->m_owner = NULL;
	pthread_mutex_lock(&mp);
}

#define	MUTEX_HELD(mp)	((mp)->m_owner == curthread)
The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

libspl: a thread by any other name

typedef	pthread_t	kthread_t;

#define	curthread	((void *)(uintptr_t)pthread_self())
The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

libspl: a thread by any other name

commit 2473094d34192e733a8ed8efc155e94000c719a8
Author: Rob Norris <[email protected]>
Date:   Tue Oct 20 15:49:26 2026 +1100

    libspl/thread: rewrite for kthread semantics and to hide pthread

    The libspl kthread implementation was previously a simple wrapper around
    POSIX threads. This was subtly wrong in a handful of ways, mostly in
    that a pthread does not have exactly the same lifetime as a kthread, and
    pthread_t neither represents exactly the same thing as kthread_t*, nor
    is it guaranteed to be the same size and shape.

    This commit properly implements the kthread semantics and API on top of
    pthreads, while largely hiding the pthread use inside as an
    implementation detail.

    Sponsored-by: TrueNAS
    Reviewed-by: Kamil Monicz <[email protected]>
    Reviewed-by: Brian Behlendorf <[email protected]>
    Signed-off-by: Rob Norris <[email protected]>
    Closes 19508

 lib/libspl/include/sys/thread.h |  20 ++-
 lib/libspl/thread.c             | 364 ++++++++++++++++++++++++++++++++++------
 2 files changed, 325 insertions(+), 59 deletions(-)

Build your
own platform

“Hey computer, do a math for me”

  • Ciphers: AES-CCM AES-GCM
  • MACs: HMAC-SHA512
  • Block checksums: Fletcher2 Fletcher4 SHA256 SHA512 Skein Edon-R BLAKE3
    • (and byteswapped variants)
  • Key derivation: HKDF-SHA512 PBKDF2-HMAC-SHA1
  • Compression: LZJB gzip ZLE LZ4 ZSTD
  • RAID-Z:
    • generation: gen_p gen_pq gen_pqr
    • reconstruction: rec_p rec_q rec_r rec_pq rec_pr rec_qr rec_pqr
  • PRNG: ???
  • ... ???
The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

What we want

  • uniform interface
  • off-the-shelf implementations that we can import into the source tree
  • host/platform-provided implementations that we can access
  • implementation variants to support progressively more powerful CPU extensions
  • runtime selection of specific implementation by operator
  • internal microbenchmarks of available implementations and selection of “best”
  • runtime self-test/verification
  • stats to report current selection, performance, etc
The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

What we have: "all-or-nothing" crypto

Two implementation suites:

  • ICP, the “Illumos Crypto Port”
    • implements all the crypto primitives, for multiple architectures
    • mostly imported from OpenSSL
  • FreeBSD in-kernel crypto
    • we use it
The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

What we have: "all-or-nothing" crypto

Three implementation suites:

  • ICP, the “Illumos Crypto Port”
    • implements all the crypto primitives, for multiple architectures
    • mostly imported from OpenSSL
  • FreeBSD in-kernel crypto
    • we use it
  • Linux in-kernel crypto
    • only when ZFS compiled into the kernel
The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

What we have: runtime implementation selection

$ cat /sys/module/zfs/parameters/...

icp_aes_impl          cycle [fastest] generic x86_64 aesni
icp_gcm_impl          cycle [fastest] avx avx2-vaes generic pclmulqdq
zfs_blake3_impl       cycle [fastest] generic sse2 sse41 avx2
zfs_fletcher_4_impl   [fastest] scalar superscalar superscalar4 sse2 ssse3 avx2
zfs_sha256_impl       cycle [fastest] generic x64 ssse3 avx avx2 shani
zfs_sha512_impl       cycle [fastest] generic x64 avx avx2
zfs_vdev_raidz_impl   cycle [fastest] original scalar sse2 ssse3 avx2
The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

What we have: microbenchmark stats

$ cat /proc/spl/kstat/zfs/fletcher_4_bench

implementation   native         byteswap
scalar           6266711888     5870987609
superscalar      8970116748     7301501974
superscalar4     4954699210     5141905824
sse2             10455595606    6429781496
ssse3            10462533176    10380321880
avx2             17324589882    15808130194
fastest          avx2           avx2
The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

What we have: microbenchmark stats

$ cat /proc/spl/kstat/zfs/vdev_raidz_bench

implementation   gen_p           gen_pq          gen_pqr         rec_p           rec_q
original         842793295       891179207       427445690       3373512385      579765224
scalar           1419630887      1243528934      512368264       3179783052      496270257
sse2             2699525778      2867292418      1419757750      5372653553      2766079845
ssse3            2809510255      2868830879      1430530974      5374000897      3113288613
avx2             6832394366      2724168111      2444051661      7859478561      172317477
fastest          avx2            ssse3           avx2            avx2            ssse3

implementation   rec_r           rec_pq          rec_pr          rec_qr          rec_pqr
original         40677974        250708138       30814919        40672809        29737555
scalar           728794699       431367107       498750004       312414540       227531597
sse2             886963427       551017606       1331050817      790245408       546343220
ssse3            2152506556      2131895260      1720700356      1198861850      961819932
avx2             1663043506      3536269342      3030121108      2244232880      1774876944
fastest          ssse3           avx2            avx2            avx2            avx2
The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

What we have (ish): FIPS module

  • FIPS: US certification that all the crypto things are buzzword compliant
  • Needs to be re-certified for each new version
  • Workaround: certify only the crypto module, let the main program consume it

  • (AIUI, YMMV, IANAL, 🇦🇺 )
The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

What we have (ish): FIPS module

  • wolfCrypt: FIPS-certified crypto module for Linux:
    • kernel module (libwolfssl.ko)
    • userspace library (libwolfssl.so)
The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

It's dangerous
to go alone

zalgo

zalgo: registry for algorithm implementations

$ ./zdb -b lucy
libzpool: NOTICE: zalgo: registered 'icp' (ICP HMAC-SHA512) for mac:HMAC-SHA512 [selected]
libzpool: NOTICE: zalgo: registered 'icp' (ICP AES-CCM) for cipher:AES-CCM [selected]
libzpool: NOTICE: zalgo: registered 'icp' (ICP AES-GCM) for cipher:AES-GCM [selected]
libzpool: NOTICE: zalgo: registered 'openssl' (OpenSSL HMAC-SHA512) for mac:HMAC-SHA512
libzpool: NOTICE: zalgo: registered 'openssl' (OpenSSL AES-CCM) for cipher:AES-CCM
libzpool: NOTICE: zalgo: registered 'openssl' (OpenSSL AES-GCM) for cipher:AES-GCM
libzpool: NOTICE: zalgo: registered 'scalar' (OpenZFS fletcher4 [generic]) for checksum:fletcher4 [selected]
libzpool: NOTICE: zalgo: registered 'scalar' (OpenZFS fletcher4 [generic]) for checksum:fletcher4-byteswap [selected]
libzpool: NOTICE: zalgo: registered 'superscalar' (OpenZFS fletcher4 [2x]) for checksum:fletcher4
libzpool: NOTICE: zalgo: registered 'superscalar' (OpenZFS fletcher4 [2x]) for checksum:fletcher4-byteswap
libzpool: NOTICE: zalgo: registered 'superscalar4' (OpenZFS fletcher4 [4x]) for checksum:fletcher4
libzpool: NOTICE: zalgo: registered 'superscalar4' (OpenZFS fletcher4 [4x]) for checksum:fletcher4-byteswap
libzpool: NOTICE: zalgo: registered 'sse2' (OpenZFS fletcher4 [SSE2]) for checksum:fletcher4
libzpool: NOTICE: zalgo: registered 'sse2' (OpenZFS fletcher4 [SSE2]) for checksum:fletcher4-byteswap
libzpool: NOTICE: zalgo: registered 'ssse3' (OpenZFS fletcher4 [SSSE3]) for checksum:fletcher4
libzpool: NOTICE: zalgo: registered 'ssse3' (OpenZFS fletcher4 [SSSE3]) for checksum:fletcher4-byteswap
libzpool: NOTICE: zalgo: registered 'avx2' (OpenZFS fletcher4 [AVX2]) for checksum:fletcher4
libzpool: NOTICE: zalgo: registered 'avx2' (OpenZFS fletcher4 [AVX2]) for checksum:fletcher4-byteswap
libzpool: NOTICE: zalgo: selected 'avx2' (OpenZFS fletcher4 [AVX2]) for checksum:fletcher4 [best] [from 'scalar', 3.081x]
libzpool: NOTICE: zalgo: selected 'avx2' (OpenZFS fletcher4 [AVX2]) for checksum:fletcher4-byteswap [best] [from 'scalar', 2.196x]
libzpool: NOTICE: zalgo: selected 'openssl' (OpenSSL AES-CCM) for cipher:AES-CCM [best] [from 'icp', 2.113x]

Traversing all blocks to verify nothing leaked ...
The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

zalgo: registry for algorithm implementations

[    5.298227] NOTICE: zalgo: registered 'icp' (ICP HMAC-SHA512) for mac:HMAC-SHA512 [selected]
[    5.298430] NOTICE: zalgo: registered 'icp' (ICP AES-CCM) for cipher:AES-CCM [selected]
[    5.298514] NOTICE: zalgo: registered 'icp' (ICP AES-GCM) for cipher:AES-GCM [selected]
[    5.298567] NOTICE: zalgo: registered 'scalar' (OpenZFS fletcher4 [generic]) for checksum:fletcher4 [selected]
[    5.298651] NOTICE: zalgo: registered 'scalar' (OpenZFS fletcher4 [generic]) for checksum:fletcher4-byteswap [selected]
[    5.298745] NOTICE: zalgo: registered 'superscalar' (OpenZFS fletcher4 [2x]) for checksum:fletcher4
[    5.298842] NOTICE: zalgo: registered 'superscalar' (OpenZFS fletcher4 [2x]) for checksum:fletcher4-byteswap
[    5.298946] NOTICE: zalgo: registered 'superscalar4' (OpenZFS fletcher4 [4x]) for checksum:fletcher4
[    5.299035] NOTICE: zalgo: registered 'superscalar4' (OpenZFS fletcher4 [4x]) for checksum:fletcher4-byteswap
[    5.299133] NOTICE: zalgo: registered 'sse2' (OpenZFS fletcher4 [SSE2]) for checksum:fletcher4
[    5.299222] NOTICE: zalgo: registered 'sse2' (OpenZFS fletcher4 [SSE2]) for checksum:fletcher4-byteswap
[    5.299312] NOTICE: zalgo: registered 'ssse3' (OpenZFS fletcher4 [SSSE3]) for checksum:fletcher4
[    5.299401] NOTICE: zalgo: registered 'ssse3' (OpenZFS fletcher4 [SSSE3]) for checksum:fletcher4-byteswap
[    5.299458] NOTICE: zalgo: registered 'avx2' (OpenZFS fletcher4 [AVX2]) for checksum:fletcher4
[    5.299547] NOTICE: zalgo: registered 'avx2' (OpenZFS fletcher4 [AVX2]) for checksum:fletcher4-byteswap
[    5.359744] NOTICE: zalgo: selected 'avx2' (OpenZFS fletcher4 [AVX2]) for checksum:fletcher4 [best] [from 'scalar', 2.192x]
[    5.420066] NOTICE: zalgo: selected 'avx2' (OpenZFS fletcher4 [AVX2]) for checksum:fletcher4-byteswap [best] [from 'scalar', 2.195x]
[    5.631453] ZFS: Loaded module v2.4.99-1.1435.g165db919ba (DEBUG mode), ZFS pool version 5000, ZFS filesystem version 5
The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

zalgo: registry for algorithm implementations

zfs: NOTICE: zalgo: registered 'scalar' (OpenZFS fletcher4 [generic]) for checksum:fletcher4 [selected]
zfs: NOTICE: zalgo: registered 'scalar' (OpenZFS fletcher4 [generic]) for checksum:fletcher4-byteswap [selected]
zfs: NOTICE: zalgo: registered 'superscalar' (OpenZFS fletcher4 [2x]) for checksum:fletcher4
zfs: NOTICE: zalgo: registered 'superscalar' (OpenZFS fletcher4 [2x]) for checksum:fletcher4-byteswap
zfs: NOTICE: zalgo: registered 'superscalar4' (OpenZFS fletcher4 [4x]) for checksum:fletcher4
zfs: NOTICE: zalgo: registered 'superscalar4' (OpenZFS fletcher4 [4x]) for checksum:fletcher4-byteswap
zfs: NOTICE: zalgo: registered 'sse2' (OpenZFS fletcher4 [SSE2]) for checksum:fletcher4
zfs: NOTICE: zalgo: registered 'sse2' (OpenZFS fletcher4 [SSE2]) for checksum:fletcher4-byteswap
zfs: NOTICE: zalgo: registered 'ssse3' (OpenZFS fletcher4 [SSSE3]) for checksum:fletcher4
zfs: NOTICE: zalgo: registered 'ssse3' (OpenZFS fletcher4 [SSSE3]) for checksum:fletcher4-byteswap
zfs: NOTICE: zalgo: registered 'avx2' (OpenZFS fletcher4 [AVX2]) for checksum:fletcher4
zfs: NOTICE: zalgo: registered 'avx2' (OpenZFS fletcher4 [AVX2]) for checksum:fletcher4-byteswap
zfs: NOTICE: zalgo: registered 'freebsd' (FreeBSD AES-CCM) for cipher:AES-CCM [selected]
zfs: NOTICE: zalgo: registered 'freebsd' (FreeBSD AES-GCM) for cipher:AES-GCM [selected]
zfs: NOTICE: zalgo: registered 'icp' (ICP HMAC-SHA512) for mac:HMAC-SHA512 [selected]
zfs: NOTICE: zalgo: registered 'icp' (ICP AES-CCM) for cipher:AES-CCM
zfs: NOTICE: zalgo: registered 'icp' (ICP AES-GCM) for cipher:AES-GCM
zfs: NOTICE: zalgo: selected 'avx2' (OpenZFS fletcher4 [AVX2]) for checksum:fletcher4 [best] [from 'scalar', 4.139x]
zfs: NOTICE: zalgo: selected 'avx2' (OpenZFS fletcher4 [AVX2]) for checksum:fletcher4-byteswap [best] [from 'scalar', 4.117x]
ZFS filesystem version: 5
ZFS storage pool version: features support (5000)
The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

zalgo: registry for algorithm implementations

$ cat /proc/spl/kstat/zfs/zalgo
TYPE      SUBTYPE               ID                    SCORE   FLAGS
mac       HMAC-SHA512           icp                   0       [selected]
checksum  fletcher4             scalar                372
checksum  fletcher4             superscalar           527
checksum  fletcher4             superscalar4          273
checksum  fletcher4             sse2                  627
checksum  fletcher4             ssse3                 626
checksum  fletcher4             avx2                  1042    [best] [selected]
checksum  fletcher4-byteswap    scalar                349
checksum  fletcher4-byteswap    superscalar           434
checksum  fletcher4-byteswap    superscalar4          297
checksum  fletcher4-byteswap    sse2                  384
checksum  fletcher4-byteswap    ssse3                 616
checksum  fletcher4-byteswap    avx2                  974     [best] [selected]
cipher    AES-CCM               icp                   4       [best] [selected]
cipher    AES-GCM               icp                   349     [best] [selected]
The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

zalgo: registry for algorithm implementations

$ sysctl kstat.zfs.misc.zalgo
kstat.zfs.misc.zalgo:
TYPE      SUBTYPE               ID                    SCORE   FLAGS
mac       HMAC-SHA512           icp                   0       [selected]
checksum  fletcher4             scalar                690
checksum  fletcher4             superscalar           900
checksum  fletcher4             superscalar4          873
checksum  fletcher4             sse2                  1625
checksum  fletcher4             ssse3                 1629
checksum  fletcher4             avx2                  3137    [best] [selected]
checksum  fletcher4-byteswap    scalar                634
checksum  fletcher4-byteswap    superscalar           937
checksum  fletcher4-byteswap    superscalar4          827
checksum  fletcher4-byteswap    sse2                  906
checksum  fletcher4-byteswap    ssse3                 1525
checksum  fletcher4-byteswap    avx2                  2828    [best] [selected]
cipher    AES-CCM               freebsd               29      [best] [selected]
cipher    AES-CCM               icp                   13
cipher    AES-GCM               freebsd               332     [best] [selected]
cipher    AES-GCM               icp                   8

zalgo: registry for algorithm implementations

Further reading:

The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

Control
Plane

“ZFS, do a barrel roll”

int fd = open("/dev/zfs", O_RDWR);
zfs_cmd_t cmd = { ... };
ioctl(fd, ZFS_IOC_BARREL_ROLL, &cmd);
The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

“ZFS, do a barrel roll”

/*
 * zfs ioctl command structure
 */

/*
 * Note: this struct must have the same layout in 32-bit and 64-bit, so
 * that 32-bit processes (like /sbin/zfs) can pass it to the 64-bit
 * kernel.  Therefore, we add padding to it so that no "hidden" padding
 * is automatically added on 64-bit (but not on 32-bit).
 */
typedef struct zfs_cmd {
	char		zc_name[MAXPATHLEN];	/* name of pool or dataset */
	uint64_t	zc_nvlist_src;		/* really (char *) */
	uint64_t	zc_nvlist_src_size;
	uint64_t	zc_nvlist_dst;		/* really (char *) */
	uint64_t	zc_nvlist_dst_size;
	boolean_t	zc_nvlist_dst_filled;	/* put an nvlist in dst? */
	int		zc_pad2;

	/*
	 * The following members are for legacy ioctls which haven't been
	 * converted to the new method.
	 */
	uint64_t	zc_history;		/* really (char *) */
	char		zc_value[MAXPATHLEN * 2];
	char		zc_string[MAXNAMELEN];
	uint64_t	zc_guid;
	uint64_t	zc_nvlist_conf;		/* really (char *) */
	uint64_t	zc_nvlist_conf_size;
	uint64_t	zc_cookie;
	uint64_t	zc_objset_type;
	uint64_t	zc_perm_action;
	uint64_t	zc_history_len;
	uint64_t	zc_history_offset;
	uint64_t	zc_obj;
	uint64_t	zc_iflags;		/* internal to zfs(7fs) */
	zfs_share_t	zc_share;
	dmu_objset_stats_t zc_objset_stats;
	struct drr_begin zc_begin_record;
	/*
	 * zinject_record_t grew past its original size, which would push out
	 * the size of zfs_cmd_t. To adjust for this, we allow it to use the
	 * space after it, since those fields aren't used with ZFS_IOC_INJECT.
	 */
	union {
		zinject_record_t zc_inject_record;
		struct {
			char		zc_pad1[sizeof (zinject_record_t) - 16];
			uint32_t	zc_defer_destroy;
			uint32_t	zc_flags;
			uint64_t	zc_action_handle;
		};
	};

	int		zc_cleanup_fd;
	uint8_t		zc_simple;
	uint8_t		zc_pad[3];		/* alignment */
	uint64_t	zc_sendobj;
	uint64_t	zc_fromobj;
	uint64_t	zc_createtxg;
	zfs_stat_t	zc_stat;
	uint64_t	zc_zoneid;
} zfs_cmd_t;

/*
 * zfs_cmd_t (and by extension, it's member structs) must always be the same
 * size. Changing it will break compatibility between the kernel module and the
 * userspace tools.
 *
 * This test is convoluted because MAXPATHLEN and MAXNAMELEN can vary across
 * platforms. We include them directly here, which means it won't trip if those
 * ever change, but if that happens we likely have other things to worry about.
 */
#define	_expected_zfs_cmd_size	((MAXPATHLEN*3)+MAXNAMELEN+1200)
_Static_assert(sizeof (zfs_cmd_t) == _expected_zfs_cmd_size,
	"zfs_cmd_t has wrong size");
#undef	_expected_zfs_cmd_size

“ZFS, do a barrel roll”

uint64_t count = 5;

















The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

“ZFS, do a barrel roll”

uint64_t count = 5;

nvlist_t *req = fnvlist_alloc();
nvlist_t *resp = fnvlist_alloc();

fnvlist_add_unit64(req, "count", count);












The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

“ZFS, do a barrel roll”

uint64_t count = 5;

nvlist_t *req = fnvlist_alloc();
nvlist_t *resp = fnvlist_alloc();

fnvlist_add_unit64(req, "count", count);

int fd = open("/dev/zfs", O_RDWR);
zfs_cmd_t cmd = { .zc_nvlist_src = req, .zc_nvlist_dst = resp };
ioctl(fd, ZFS_IOC_BARREL_ROLL, &cmd);








The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

“ZFS, do a barrel roll”

uint64_t count = 5;

nvlist_t *req = fnvlist_alloc();
nvlist_t *resp = fnvlist_alloc();

fnvlist_add_unit64(req, "count", count);

int fd = open("/dev/zfs", O_RDWR);
zfs_cmd_t cmd = { .zc_nvlist_src = req, .zc_nvlist_dst = resp };
ioctl(fd, ZFS_IOC_BARREL_ROLL, &cmd);

uint64_t nsuccess, nfailed;
fnvlist_lookup_uint64(resp, "succeeded", &nsuccess);
fnvlist_lookup_uint64(resp, "failed", &nfailed);

printf("ZFS tried %u barrel rolls, "
    "stuck the landing on %u, fell down on %u\n",
    count, nsuccess, nfail);
The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

“ZFS, do a barrel roll”

uint64_t count = 5;

nvlist_t *req = fnvlist_alloc();
nvlist_t *resp = fnvlist_alloc();

fnvlist_add_unit64(req, "count", count);





uint64_t nsuccess, nfailed;
fnvlist_lookup_uint64(resp, "succeeded", &nsuccess);
fnvlist_lookup_uint64(resp, "failed", &nfailed);

printf("ZFS tried %u barrel rolls, "
    "stuck the landing on %u, fell down on %u\n",
    count, nsuccess, nfail);







int fd = open("/dev/zfs", O_RDWR);
zfs_cmd_t cmd = { .zc_nvlist_src = req, .zc_nvlist_dst = resp };
ioctl(fd, ZFS_IOC_BARREL_ROLL, &cmd);








The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

“ZFS, do a barrel roll”

uint64_t count = 5;

nvlist_t *req = fnvlist_alloc();
nvlist_t *resp = fnvlist_alloc();

fnvlist_add_unit64(req, "count", count);





uint64_t nsuccess, nfailed;
fnvlist_lookup_uint64(resp, "succeeded", &nsuccess);
fnvlist_lookup_uint64(resp, "failed", &nfailed);

printf("ZFS tried %u barrel rolls, "
    "stuck the landing on %u, fell down on %u\n",
    count, nsuccess, nfail);







char *req_json = nvlist_to_json(req);
char *resp_json = post("http://server3.corp/zfs/barrel_roll", req_json);
nvlist_from_json(&resp, resp_json);








The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

“ZFS, do a barrel roll”

import zfs
succeeded, failed = zfs.barrel_roll(5)
The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

Control schema

static void
zfs_ioctl_init(void)
{
	zfs_ioctl_register("create", ZFS_IOC_CREATE,
	    zfs_ioc_create, zfs_secpolicy_create_clone, DATASET_NAME,
	    POOL_CHECK_SUSPENDED | POOL_CHECK_READONLY, B_TRUE, B_TRUE,
	    zfs_keys_create, ARRAY_SIZE(zfs_keys_create));

	zfs_ioctl_register("snapshot", ZFS_IOC_SNAPSHOT,
	    zfs_ioc_snapshot, zfs_secpolicy_snapshot, POOL_NAME,
	    POOL_CHECK_SUSPENDED | POOL_CHECK_READONLY, B_TRUE, B_TRUE,
	    zfs_keys_snapshot, ARRAY_SIZE(zfs_keys_snapshot));

	zfs_ioctl_register("send", ZFS_IOC_SEND_NEW,
	    zfs_ioc_send_new, zfs_secpolicy_send_new, DATASET_NAME,
	    POOL_CHECK_SUSPENDED, B_FALSE, B_FALSE,
	    zfs_keys_send_new, ARRAY_SIZE(zfs_keys_send_new));
The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

Control schema

static const zfs_ioc_key_t zfs_keys_create[] = {
	{"type",	DATA_TYPE_INT32,	0},
	{"props",	DATA_TYPE_NVLIST,	ZK_OPTIONAL},
	{"hidden_args",	DATA_TYPE_NVLIST,	ZK_OPTIONAL},
};
The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

Control schema?

static int
zfs_ioc_create(const char *fsname, nvlist_t *innvl, nvlist_t *outnvl)
{
	type = (dmu_objset_type_t)fnvlist_lookup_int32(innvl, "type");
	(void) nvlist_lookup_nvlist(innvl, "props", &nvprops);
	(void) nvlist_lookup_nvlist(innvl, ZPOOL_HIDDEN_ARGS, &hidden_args);










The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

Control schema?

static int
zfs_ioc_create(const char *fsname, nvlist_t *innvl, nvlist_t *outnvl)
{
	type = (dmu_objset_type_t)fnvlist_lookup_int32(innvl, "type");
	(void) nvlist_lookup_nvlist(innvl, "props", &nvprops);
	(void) nvlist_lookup_nvlist(innvl, ZPOOL_HIDDEN_ARGS, &hidden_args);










static int
zfs_ioc_create(const char *fsname, nvlist_t *innvl, nvlist_t *outnvl)
{




	if (type == DMU_OST_ZVOL) {
		if (nvprops == NULL)
			return (SET_ERROR(EINVAL));
		if (nvlist_lookup_uint64(nvprops,
		    zfs_prop_to_name(ZFS_PROP_VOLSIZE), &volsize) != 0)
			return (SET_ERROR(EINVAL));



The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

Control schema?

static int
zfs_ioc_create(const char *fsname, nvlist_t *innvl, nvlist_t *outnvl)
{
	type = (dmu_objset_type_t)fnvlist_lookup_int32(innvl, "type");
	(void) nvlist_lookup_nvlist(innvl, "props", &nvprops);
	(void) nvlist_lookup_nvlist(innvl, ZPOOL_HIDDEN_ARGS, &hidden_args);










static int
zfs_ioc_create(const char *fsname, nvlist_t *innvl, nvlist_t *outnvl)
{




	if (type == DMU_OST_ZVOL) {
		if (nvprops == NULL)
			return (SET_ERROR(EINVAL));
		if (nvlist_lookup_uint64(nvprops,
		    zfs_prop_to_name(ZFS_PROP_VOLSIZE), &volsize) != 0)
			return (SET_ERROR(EINVAL));



static int
zfs_ioc_create(const char *fsname, nvlist_t *innvl, nvlist_t *outnvl)
{











	error = dsl_crypto_params_create_nvlist(DCP_CMD_NONE, nvprops,
	    hidden_args, &dcp);
static int
zfs_ioc_create(const char *fsname, nvlist_t *innvl, nvlist_t *outnvl)
{











	error = dsl_crypto_params_create_nvlist(DCP_CMD_NONE, nvprops,
	    hidden_args, &dcp);
The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

nvlist marshalling

static const zfs_ioc_key_t zfs_keys_pool_scrub[] = {
	{"scan_type",		DATA_TYPE_UINT64,	0},
	{"scan_command",	DATA_TYPE_UINT64,	0},
	{"scan_flags",		DATA_TYPE_UINT64,	ZK_OPTIONAL},
	{"scan_date_start",	DATA_TYPE_UINT64,	ZK_OPTIONAL},
	{"scan_date_end",	DATA_TYPE_UINT64,	ZK_OPTIONAL},
};

static int
zfs_ioc_pool_scrub(const char *poolname, nvlist_t *innvl, nvlist_t *outnvl)
{
	uint64_t scan_type, scan_cmd, scan_flags;
	uint64_t date_start, date_end;

	if (nvlist_lookup_uint64(innvl, "scan_type", &scan_type) != 0)
		return (SET_ERROR(EINVAL));
	if (nvlist_lookup_uint64(innvl, "scan_command", &scan_cmd) != 0)
		return (SET_ERROR(EINVAL));

	if (nvlist_lookup_uint64(innvl, "scan_flags", &scan_flags) != 0)
		scan_flags = 0;

	if (nvlist_lookup_uint64(innvl, "scan_date_start", &date_start) != 0)
		date_start = 0;
	if (nvlist_lookup_uint64(innvl, "scan_date_end", &date_end) != 0)
		date_end = 0;

	if ((error = zfs_scan_ioc_validate(scan_type, scan_cmd, scan_flags,
	    date_start, date_end)) != 0)
		return (error);

nvlist marshalling

NVM_SCHEMA(zfs_ioc_pool_scrub_args_t,
        ("scan_type",           NVM_SCALAR(UINT64),     type,   NVM_REQUIRED),
        ("scan_command",        NVM_SCALAR(UINT64),     command, NVM_REQUIRED),
        ("scan_flags",          NVM_SCALAR(UINT64),     flags,  NVM_OPTIONAL),
        ("scan_date_start",     NVM_SCALAR(UINT64),     start,  NVM_OPTIONAL),
        ("scan_date_end",       NVM_SCALAR(UINT64),     end,    NVM_OPTIONAL)
);

static int
zfs_ioc_pool_scrub(const char *poolname, nvlist *innvl, nvlist_t *outnvl)
{
	zfs_ioc_pool_scrub_args_t in;
	if ((error = NVM_UNMARSHAL(innvl, zfs_ioc_pool_scrub_args_t, &in)) != 0)
                return (error);

        if ((error = zfs_scan_ioc_validate(in->type, in->command, in->flags,
            in->start, in->end)) != 0)
                return (error);

nvlist marshalling

NVM_SCHEMA(zfs_ioc_pool_scrub_args_t,
        ("scan_type",           NVM_SCALAR(UINT64),     type,   NVM_REQUIRED),
        ("scan_command",        NVM_SCALAR(UINT64),     command, NVM_REQUIRED), 
        ("scan_flags",          NVM_SCALAR(UINT64),     flags,  NVM_OPTIONAL),
        ("scan_date_start",     NVM_SCALAR(UINT64),     start,  NVM_OPTIONAL),
        ("scan_date_end",       NVM_SCALAR(UINT64),     end,    NVM_OPTIONAL)
);

typedef struct {
	uint64_t	type;
	uint64_t	command;
	uint64_t	flags;		boolean_t	has_flags;
	uint64_t	start;		boolean_t	has_start;
	uint64_t	end;		boolean_t	has_end;
} zfs_ioc_pool_scrub_args_t;
The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

nvlist marshalling

NVM_SCHEMA(everything_t,
	"u64",		NVM_SCALAR(UINT64),	 u64,		NVM_REQUIRED),
	"i32_maybe",	NVM_SCALAR(INT32),	 i32_maybe,	NVM_OPTIONAL),
	"u8_maybe_5",	NVM_SCALAR(UINT8),	 u8_maybe_5,	NVM_DEFAULT(5)),
);

typedef struct {
	uint64_t	u64;

	/* optional, if !has_i32_maybe, defaults to 0 */
	uint64_t	i32_maybe;	boolean_t	has_i32_maybe;

	/* optional, if !has_u8_maybe_5, defaults to 5 */
	uint8_t		u8_maybe_5;	boolean_t	has_u8_maybe_5;
} everything_t;
The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

nvlist marshalling

NVM_SCHEMA(everything_t,
	"name",		NVM_SCALAR(STRING),	name,		NVM_REQUIRED),
	"props",	NVM_SCALAR(NVLIST),	props,		NVM_REQUIRED),
);

typedef struct {
	const char *name;
	nvlist_t *props;
} everything_t;
The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

nvlist marshalling

NVM_SCHEMA(everything_t,
	"some_numbers",	NVM_ARRAY(UINT64),	 some_numbers,	NVM_REQUIRED),

	"ten_numbers",	NVM_ARRAY_N(UINT64, 10), ten_numbers,	NVM_REQUIRED),
);

typedef struct {
	uint64_t *some_numbers;		uint_t nelem_some_numbers;

	uint64_t ten_numbers[10];
} everything_t;
The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

nvlist marshalling

NVM_SCHEMA(everything_t,
	"activate",	NVM_FLAG,		activate);
);

typedef struct {
	boolean_t activate;	/* B_TRUE if present, B_FALSE if missing */
} everything_t;
The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

nvlist marshalling

NVM_SCHEMA(everything_t,
	"names_with_ids",   NVM_MAP(UINT64), names_with_ids, NVM_REQUIRED),
	"capabilities",     NVM_SET,         caps,           NVM_REQUIRED),
);

typedef struct { const char *name; uint64_t value; } nvm_pair_uint64_t;

typedef struct {
	nvm_pair_uint64_t *names_with_ids;	uint_t nelem_names_with_ids;
	const char        **caps;		uint_t nelem_caps;
} everything_t;
The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

nvlist marshalling

NVM_SCHEMA(person_t,
	"name",		NVM_SCALAR(STRING),	name,	NVM_REQUIRED,
	"rating",	NVM_SCALAR(INT32),	rating,	NVM_OPTIONAL),
);

typedef struct {
	const char *name;
	int32	    rating;	boolean_t has_rating;
} person_t;

NVM_SCHEMA(party_t,
	"host",    NVM_STRUCT(person_t),        host,     NVM_REQUIRED),
	"guests",  NVM_STRUCT_ARRAY(person_t),  helpers,  NVM_REQUIRED),
);

typedef struct {
	person_t	*host;
	person_t	**guests;	uint_t nelem_guests;
} party_t;
The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route
NVM_SCHEMA(zfs_ioc_pool_scrub_args_t,
        ("scan_type",           NVM_SCALAR(UINT64),     type,   NVM_REQUIRED),
        ("scan_command",        NVM_SCALAR(UINT64),     command, NVM_REQUIRED), 
        ("scan_flags",          NVM_SCALAR(UINT64),     flags,  NVM_OPTIONAL),
        ("scan_date_start",     NVM_SCALAR(UINT64),     start,  NVM_OPTIONAL),
        ("scan_date_end",       NVM_SCALAR(UINT64),     end,    NVM_OPTIONAL)
);
The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

nvlist marshalling

https://github.com/robn/zfs/commits/nv-marshal/

  • marshal layer: done, including heavy documentation
  • tests: done
  • ioctls: proof of concept only, because all of that deep nvpair stuff
The scenic route to Userspace ZFS - and elsewhererobn.au/openzfs-scenic-route

Maybe we
can have
nice things

SOUND CHECK

Hi everyone, I'm Rob. I think most of you know me, or at least know of my from longwinding rambling and/or annoying opinions on the bug tracker, but just in case... I'm based in Melbourne, Australia, which is why I'm barely awake while you're just getting ready for your after-lunch nap _yesterday_. I live and work on the traditional lands of the Boonwurrung people. I've worked full-time on OpenZFS since 2022, and at TrueNAS since 2026. For the 30+ years before that, I've done all sorts, up and down the stack, so I've seen a bit of everything, which is useful!

So we love to talk about "userspace ZFS", and I get asked about it almost every week in the user calls (which you should attend, btw). And I always say, "well everything I do is working towards it, sorta". Because, I'm not interested in userspace ZFS _as such_.

I'm far more interested in working towards an OpenZFS that can used on or in or with anything that has a need to store things. To me "userspace" is mostly just a placeholder for "not in the kernel".

Or at least, outside of these two kernels.

Or maybe outside of these kernels or operating systems, that already have a ZFS of some sort, either downstream or adjacent. Though bringing them all into the fold would be nice.

There's also a cottage industry of other operating systems that would be interesting to put ZFS on. But still, these are all kind of the same: ZFS as the filesystem for regular applications.

Incidentally, that's where I put FUSE. It's not uninteresting, and there are some useful things it could enable, but ultimately it's more of the same. Because there's a lot more storage out there that is not especially interested in the OS-supplied filesystem, beyond the fact that it's there by default.

The "big three" SQL databases all have interesting and rich backend storage APIs. What would it look like for ZFS to be connected directly to those, rather than as we do now, with careful tuning required to minimise the filesystem overheads?

There's other kinds of databases around. Lucene stores search indexes, basically massive tables of terms -> documents. Prometheus is a time-series database for service metrics & monitoring, storing, querying and performing computations on millions of raw numbers - think of it like a native store for spreadsheets. Cyrus is an IMAP mail server. I ran it for millions of users for over a decade. Surprising fact about IMAP: its data and access control model rival the POSIX filesystem for richness and complexity. Cyrus in particular works very hard to get the filesystem out of the way, and in doing so has flushed out serious bugs in Linux, ext4 and OpenZFS.

What about a web server? What if your webroot was a dataset, and your "publish" was to just push a replication stream into it?

Ok, I said I wasn't interested in "more of the same", but these are filesystem options for VMs. If you're running a VM host but don't actually need to see the host filesystem there, why can't you just run a virtio-fs "server" on the host, with ZFS inside it, talking to the raw disk. Why route it all through the host filesystem and kernel when actually we just want to expose raw memory regions into the VM space?

Not unrelated, why not ZFS in the earliest parts of computer startup?

And then there's our classic "remote storage" options, and the newcomer. All of these have high-quality application servers with pluggable backend storage APIs. Why can't we just spin up a VM or container, give it the underlying block devices, run a conventional application server and hand those things out to the network with all the power of ZFS under there.

And then all the tooling we've talked about over the years: backup receivers, recovery tools, offline scrub/repair, etc. Now to be clear, I'm not saying any of these are good ideas or natural fits for ZFS. But I don't think we've explored the space nearly enough, and I think a big part of that is that the OpenZFS codebase is kinda difficult to work with in a bunch of ways.

And hey, it doesn't hurt to dream a little. There's a lot of things to store, and we have some very good storage tech. Maybe it shouldn't matter how you access it.

So here's an idealised or aspirational diagram of ZFS. It has three places where it connects to its host environment. But if you've spent much time in the code, you'll know that there's a bunch of special cases that don't quite fit neatly into one of these boxes, and even the ones that do have an obvious home often have weird bits entangled with something else. XXX second slide, same boxes but pulled apart with harder lines between The goal that I've set for myself is to clearly define these lines, by whatever means. Some of that is just moving things around in the source tree a bit, or removing unused code. Some of it fixing existing warts, or finishing wiring a concept all the way through the tree. For some its requiring developing entirely new subsystems. XXX third slide, no outer boxes at all, just the "portable core" Because this is what I actually want. I want there to be clearly defined "thing" that is the "core" of ZFS, that can be connected up to any storage system that has these three concepts - data, control, and services, so the developers of that system can present it to the users in the way that makes most sense for them

I've been working on this in some way for a few years, but this year I really focused on the goal and started working on the things we'd need. No time for everything, but here's a few of the more interesting ones.

So of course, if I'm doing things, how do I know I'm doing them right?

So you know ZTS. Big ball of shell scripts that poke at ZFS in various ways to make sure it does the right thing. For many kinds of change, this is enough, but for many other types of change, its actually quite complicated to exercise a change to something very core to ZFS, because its usually not exposed directly; instead, you have to take particular actions that you know will tickle the internals in particular way to produce some effect, and then look for the effect. This is integration testing, or functional testing, or black-box testing. It's very nice, and serves us well! But it can't test the fine details of the internals.

Also, kind of not helpful if we want to make sure the core itself works, without those connective pieces, or with ones that look radically different. So we need something that can test the core subsystems directly, often in isolation. You know this. It's called...

No new ideas around here, I swear!

So I wrote a unit testing framework, and it's now integrated into upstream. If you have a git clone on your laptop, you can type `make unit` right now and it'll run the tests we have. This is simplest suite we have, just to show you the output.

This is what a simple test looks like. You'll see "munit" in there; that's from the underlying test framework this is built on. Things like `unit_eq()` are all ours, and it's designed to be incredibly simple and obvious to use, and have lots of nice helpers and affordances for ZFS testing. Incidentally, thats the reason to do our own - most C unit testing frameworks are awful to use because they require you to write code that isn't much like anything else you do, or go through weird contortions to make your code testable, or other nonsense. I wanted tests to feel like ZFS code, and maybe even be fun to write!

A less trivial example is the ZAP test suite. This is calling directly into the ZAP APIs. Oh yeah, that's the other thing. If I want to test ZAPs, then I don't want to need all those other annoying things that it would usually require, like a real pool under there somewhere, or a whole working DMU, or whatever. Because I want to make sure the thing works in isolation, not based on behaviours of other subsystems.

And so, the framework has "mocks" of key objects and data structures. Because, internally the ZAP code just operates on a dnode, and actually, just getting its DMU buffer pointer and reading and writing into that memory. So, a "mock dnode" just has a chunk of memory under it, and a "mock tx" actually currently does nothing. But the types work, and so we can just test the code we're interested in and let the framework take care of the rest.

We get nice things too. Code coverage isn't everything, but it helps a lot for some kinds of work. The framework just takes care of it.

Actually my slide presenter doesn't easily show that it's also has nice colours, so here's a screenshot of the previous output. It's designed so you can get a very quick visual check on how "good" your coverage is.

Of course, it's just lcov under there, so if you like the HTML output...

You can have that too. I just really wanted a quick text output that you can see right there, rather than having to regenerate a whole HTML thing every time, which is particularly a pain if you're working on a remote machine.

So, right now we have a small handful of test suites. Only a couple are mine, most were done by Christos Longros in the first couple of weeks after the framework weren't in, and I'm very grateful to him for pushing it; there's some nice features in there (like type-aware comparisons!) that wouldn't be without him finding problems. Interestingly, LLMs are incredibly good at being told to write a comprehensive test suite for a specific subsystem. You tell them to under no circumstances modify the original code, and they really like the coverage data. So I've got a bunch that Claude has generated that are waiting for me to review, rewrite, document, etc. Still work, but 80% already done! And I've got some actual real ones, or extensions to existing ones, in progress. Of course only a couple of t

I wrote a big describing how it all works, but have a look. It's not hard, that's the point!

Moving on, lets look at host services.

Mostly, I'm talking about the SPL; the Solaris Porting Layer.

Except of course that it probably hasn't looked much like Solaris for a while.

And so it's not really about porting things, not exactly.

And layer, well, at least the tentacles are in pretty deep to call it that. So it's not a great name.

But that's ok, because the abbreviation has lots of room for us to rename it to something that better describes its function without having to do more than update some code comments.

But the name isn't really important. What we do need to do is pin down exactly what the API isand what the semantics are. And it turns out, filling out the details of `libspl`, the userspace/POSIX SPL library, has been a really good way to identify a lot of the moving parts.

The POSIX implementations of things used to live directly in `zfs_context.h`, behind a `_KERNEL` gate. So about a year ago Brian and I moved them. Beyond general code cleanliness, this also gave us a chance to see what the "core" services are, that every environment would have to implement in some way. We've also removed some userspace-specific variants of facilities provided by libspl. These are mostly used by libs and CLI tools, but the benefit is that we now don't have two unrelated and different implementations of the same thing, so it's less to maintain and less for a ZFS generalist to understand.

So an SPL mutex is a bit more than just a lock. It also tracks the current owner. Until last week, the implementation looked like this. As it turns out, this isn't actually legal POSIX, because the underlying layout of `pthread_t` is unknown. POSIX does not require it to fit in a register, and it is allowed to be a composite type, like a struct. So, we can't actually assign NULL to it, because it might not be a pointer. Turns out there isn't even a sentinal value for "no thread" (until C23 anyway), so there's nothing else we can usefully assign to it anyway.

So I thought, no big deal, I'll just do what we do in kernel code, and keep a kthread_t* pointer and use curthread for the owher, and the NULL pointer for "no owner". And that even kinda looks right, except...

Actually makes it worse. The cast to `uintptr_t` still won't work if its a struct, and even if it does fit, there's nothing that says that two `pthread_t`s are comparable by memory layout. Only `pthread_self()`.

And it turns out its even more complicated, because `pthread_t` doesn't have the same lifetime as `kthread_t`, so isn't a suitable proxy. I'm definitely not going into all of this now, but anyway, last week I rewrote SPL kthreads, and now ownership is tracked properly for owned resources. All of this is to say that the SPL isn't _just_ mapping the ZFS name for a thing to its nearest host-provided counterpart. There's also specific semantic requirements, most of which are undocumented. So writing those down, in code, comments and/or tests is something I'm slowly picking away at.

But there's another more complicated case of host services, where there might be more than one available, and we need to choose one.

ZFS has a lot of these. Anywhere we've got some data transform, in fact. And we have multiple implementations of many of them, if for no other reason than we may have dedicated hardware available that a different implementation can exploit.

It turns out, if you ignore the specific algorithm and even the type, and just look at the way we use them, we have the same basic requirements for all of these. They all have the same interface, at least within their type. We mostly aren't doing our own implementations, either getting them elsewhere or using something provided by the host. The operator may want to select which implementation is used, or we can test them and use the "best" one. And there's some way to find out what's available and what's selected.

For the crypto algorithms, we have two implementations we can use. There's the ICP, which is basically all the crypto stuff we ship ourselves. This is what we use for Linux and in userspace. In the FreeBSD kernel, we use whatever the kernel's crypto framework provides.

Actually, a couple of weeks ago we got an adapter for the Linux kernel's crypto framework. It's very very new though, and a little complicated to access, but it's very interesting indeed! Regardless, this is a weird situation. We ship multiple implementations, and we can access the host implementation, but what if, say, on FreeBSD, an ICP implementation could outperform a kernel one? Or what if we want to add a new algorithm, that isn't supported by a host or platform framework at all? Why can't we choose the best of what's available?

$ perl -E 'printf "%-22s%s\n", ($_ =~ m#/([^/]+)$#), do { local @ARGV = ($_); <> } for sort @ARGV' /sys/module/zfs/parameters/*_impl

We have some runtime implementation selectors, but not for everything. Also you'll notice that fletcher4 doesn't have a "cycle" option. This is actually all weird because the code that drives this selection is repeated in all these modules, usually slightly modified each time.

We also have some stats for the microbenchmarks that we run to choose the "best" version of each. Here's the fletcher4 stats, showing its result for the two fletcher4 variants. Note that "fastest" could produce a different result for each, but recall that there's only one selector available.

It's even more insane for raidz math. One selector for ten different largely-unrelated algorithms. Also this is all the stats we have for any of these options. Again, the stats code for each of these is very custom, for particular reason.

Theres a company called wolfSSL, that sell a FIPS-certified crypto module exactly like this. They also ship a huge patch to OpenZFS to make it use it. It rips out the entirety of the ICP, and replaces it with calls into wolfCrypt instead. This is very cool, and can put ZFS into more hands. It also means those people have to re-patch OpenZFS on every release. It is also a particular kind of punishment for the wolfSSL people, that this is pretty much the only way to do it.

There's more to it than what I've shown here, not least that it isn't just about crypto. This is compression, checksums, and every other thing where we might possibly want to update an implementation, or connect up to whatever the host environment provides. We cannot and should not try to do everything. We should make it easy to bring in all these interesting things.

So I wrote a new subsystem for ZFS, called zalgo. It's "ZFS algorithms" or that eldritch Unicode horror, your choice. There's a prototype in PR 19129 right now.

These are logs from the prototype. At startup, different implementations of things register with the hub. So we see three ICP algorithms being added, and then an OpenSSL provider registers for those as well (yeah, I wrote an OpenSSL provider for this, just to show it). It registers various fletcher4 implementations, both for `fletcher4` and `fletcher4-byteswap`. They are _different_ algorithms; there's no reason to lump them under the same selector and microbenchmarks. At the end, the microbenchmarks are run for each type and subtype. You'll see here that the AVX2 fletcher4 implementations beats "scalar" (just the default C version) by some margin, so it gets selected. Similarly, OpenSSL AES-CCM beats out ICP AES-CCM, and gets selected. ICP GCM wins, so nothing gets changed.

In the Linux kernel, it looks the same, except there's no OpenSSL provider there, so the ICP gets selected.

Here I ported the ICP to the FreeBSD kernel, and here we see them both loading. I've commented out the load for the FreeBSD HMAC-SHA512, which is why the ICP version is the only one. That was deliberate, for an interop test. Works great!

There's a nice kstat to show what's currently available, the relative performance on the microbenchmarks, and the current state of things. I should note, though this says "ICP", in a real thing it wouldn't be, it would just be the OpenZFS-supplied implementations registered directly (all the different instruction set extensions). Just hitting the ICP direct was much easier for a prototype.

Same kstat for FreeBSD.

Theres a lot more to this than what I've shown you, and if you're interested in this I'd love your feedback there.

Ok so, when you want to tell ZFS to do something, you run a `zfs` or `zpool` command, and eventually it ends up doing a call into the kernel. Basically, a syscall, but as we know in Unixlikes, the way we get those is to open a device, and send it a custom thing.

Oh yeah never mind that, that's just `zfs_cmd_t`, it's seen some shit.

So we wanna tell ZFS to do five barrel rolls. Which is a thing a filesystem can do.

We create an empty nvlist for the request and the response, and add our count parameter to it. If you haven't seen nvlists, they're basically an in-memory typed key-value object. Think Python dictionary.

Then we make the call. Pretty much what we saw before. We have to fill in a bit more zfs_cmd_t than this, but not much more. It really doesn't matter right now I swear.

And then we unpack the results and do something with them and see just how wicked and rad those barrel rolls were!

But what if we wanted to do this?

Ok, that's not hard to arrange exactly; you'd put a HTTP listener on that host, and it'd take the JSON off the wire, convert it back to an nvlist and do the ioctl. Except if you're doing web calls, you're not doing them in C.

Really, you're hoping to do something more like this. (and I don't really know much Python but that's ok, any Python heads thinking "well actually" right now, don't worry, because you're definitely right and this is for you) Because how do you know what the args and returns are here? How do you actually build this API. And then, how do you repeat this for every language and every enviroment. And you know the answer to this already too. It's documentation! Even better, documentation a machine can read and use to generate the API, that is, a schema.

So we already sort of have a data description for many (not all) of the ioctls. If you've looked in the ioctl code before, you've seen this. If you squint, this is just a table. The name, what to call, security config, scope, some rules, etc. With a tiny bit of regex, that's just JSON or TOML or whatever and might be the start of a schema.

We even have these mini schema shaped things that we use to validate the args coming in, that describe types and options and things. So we're sorta kinda there. But it's also kinda half-baked. Like, "props" there is an nvlist. But an nvlist also has keys and types, so what validates its contents?

But then we get into the ioctl implementation, and we have a ton of work to do. We unpack the args, basically restating the names, types, and how optional they are.

Further down, we actually discover more schema-type things. have hardcoded policy: if we're creating a zvol, the "props" arg is actually not optional, and it has to have a uint64 key called "volsize".

And then we just pass two whole nvlists which could have anything in it straight down into some other function somewhere else in the system. Who knows what it needs, or what it does. We do this a lot, and I do not like it at all. But also it's sucks for anything that might want to embed ZFS, that already has a control system. Do we really want it to create an nvlist in memory, just to pass it down to a function that is going to take it apart again? That's just a crappy function call.

So there's a lot here, and I've been working on it from multiple angles. But the schema stuff has been particular interesting because it would help libzfs a lot, or a libzfs replacement. So I wrote a marshalling layer. Which is basically like serialise/deserialise, but also presents it in a way that makes the most sense for the language? So this is the entry point for scrub, and its unpacking everything by hand, like before.

Instead, we can do this. This creates the struct with the named fields, and then this `NVM_UNMARSHAL` call will take the nvlist and unpack it into the object. There's an inverse, `NVM_MARSHAL`, which will go in the other direction. This is the simple case. I haven't wired it all the way through yet, mostly because of all those other functions that want to take a raw nvlist that need to be untangled first. But it can handle all the weird cases.

Instead, we can do this. This creates the struct with the named fields, and then this `NVM_UNMARSHAL` call will take the nvlist and unpack it into the object. There's an inverse, `NVM_MARSHAL`, which will go in the other direction. This is the simple case. I haven't wired it all the way through yet, mostly because of all those other functions that want to take a raw nvlist that need to be untangled first. But it can handle all the weird cases.

Lightning overview of what we can do here. It'll take any nvlist scalar type and make it a regular C scalar. You can say its required, or optional, or optional with a non-zero default.

nvlist treats strings and nvlist members as scalars, so we do too. It'll take any nvlist scalar type and make it a regular C scalar. You can say its required, or optional, or optional with a non-zero default. Incidentally, there's magic so that any allocations required during unmarshaling are charged to the source nvlist, so everything is cleaned up properly when you free the nvlist.

All the nvlist array types are handled too. There's two forms, one for arbitrary number of elements, and one for known or fixed. For the arbitrary length, it records the number of elements found. For the fixed version, it won't, and also it'll enforce it - unmarshaling will the array does not have exactly ten elements.

The weird "key with no value" boolean type is modeled as a flag.

Then we get into the weird and cool stuff. It's pretty common in nvlists to model a list of of things as a sub-nvlist with some arbitrary number of elements. We know this in other languages as a map or dictionary. So we can say, this is a map of uint64. Inside the nvlist, it will be an nvlist full of UINT64 pairs. On the C side, we make a simple "pair" struct, and you get a regular array of them. Similarly, we have a set, for the nvlist of those "wierd boolean" things, ie flags.

The best part. Recursive unmarshalling, enforcing the rules all the way.

So I think that's a pretty cool tool for us to use internally anyway, but going back to the start, it forces us to write down everything that goes into our nvlists. And once we have programattic access to it, we can advertise our own capabilities, or use it to generate a client library, or whatever else.