Skip to content

Support NetBSD, OpenBSD and DragonFly - #403

Merged
folbricht merged 5 commits into
masterfrom
bsd-support
Sep 2, 2026
Merged

folbricht merged 5 commits into
masterfrom
bsd-support

Conversation

@folbricht

Copy link
Copy Markdown
Owner

FUSE turned out to be the only thing keeping desync off these three platforms. Everything else in the tree already builds for them through the existing platform abstractions — ioctl_nonlinux.go, preallocate_other.go, blocksize.go and friends absorbed all three without a single change.

Excluding FUSE

The go-fuse bindings cover linux, darwin and freebsd only, and there's no near-term prospect of more: upstream's Stance on BSD support is closed, DragonFly's own FUSE driver only got writes working in March 2024, OpenBSD's fuse(4) doesn't support partial reads and writes, and NetBSD has no FUSE device at all — it emulates one over PUFFS.

So mount-index.go, mount-sparse.go and cmd/desync/mount-index.go are excluded there, and the three platforms register the same stub Windows already used. That stub is no longer windows-only, and it gained a Use string, so desync mount-index now reports why it's unavailable instead of failing with "unknown command".

Device nodes take the atomic path

All three have mknodat(2), and x/sys types the device argument as int — exactly as on Linux. So localfs_mknod_linux.go becomes localfs_mknod_mknodat.go and they share it, rather than falling into Darwin's resolve-then-mknod fallback. FreeBSD stays separate only because it types the same argument uint64.

Extended attributes are the one real gap

pkg/xattr builds its BSD implementation for freebsd || netbsd only. OpenBSD and DragonFly compile in xattr_unsupported.go, whose functions return nil and do nothing — so tar records no xattrs and untar discards them, with no error either way. That's honest on OpenBSD, whose kernel has no extended attributes at all; DragonFly has extattr the library just doesn't wire up. Called out in the platform table because it's silent.

File permissions needed nothing. FilemodeToStatMode is built from the S_IF*/S_IS* constants, whose values are identical across all of these.

Verification

Locally, go vet ./... (which typechecks the test files too) passes on netbsd/{amd64,arm64,386}, openbsd/{amd64,arm64,riscv64} and dragonfly/amd64, with linux, darwin, freebsd and windows still green. goreleaser build --snapshot produces the five new binaries and nothing unexpected.

What that does not cover is runtime behaviour, which is why this adds a VM job per platform next to the existing FreeBSD one. Those jobs are the part of this PR I'd watch: the Go toolchain comes from each system's package repo, those lag, and go.mod sets a minimum — so each job echoes go version to make "package too old" readable straight off the log. If a platform can't get a new enough Go, the honest fix is to drop its VM job and downgrade its README row to "Builds, untested" rather than claim support the CI doesn't back.

Release artifacts

netbsd and openbsd on amd64 and arm64, dragonfly on amd64 — its only Go port. goreleaser drops nonexistent combinations by itself, so the new ignore entries are only for targets that would otherwise build and ship (32-bit ARM and RISC-V), following the existing freebsd/arm precedent.

FUSE was the only thing keeping desync off these three platforms. The
go-fuse bindings cover linux, darwin and freebsd; everything else in the
tree already builds for the remaining BSDs through the existing
platform abstractions, without a single change.

So exclude the three mount files there and register the same "not
available on this platform" stub Windows already used. That stub is now
shared rather than windows-only, and gained a Use string so that
`desync mount-index` reports why it's unavailable instead of failing
with "unknown command".

Device node creation gets to use the atomic path rather than Darwin's
resolve-then-mknod fallback: all three have mknodat(2), typed with the
device number as an int exactly as on Linux, so they can share that
file. FreeBSD stays separate only because it types the argument uint64.

Extended attributes are the one real gap. pkg/xattr builds its BSD
implementation for freebsd and netbsd only; on OpenBSD and DragonFly it
compiles in stubs that return nil and do nothing, so tar records no
xattrs and untar discards them without an error. OpenBSD has no
extended attributes to begin with, so that is honest there. Noted in
the platform table since it is silent.

File permissions need nothing: FilemodeToStatMode is built from the
S_IF*/S_IS* constants, which are identical across all of these.

CI grows a VM job per platform alongside the FreeBSD one, and the
cross-compile smoke test now covers every BSD release target. Both echo
`go version`, because the toolchain comes from each system's package
repo and "too old for go.mod" is the failure worth reading off a log.
Release builds add netbsd and openbsd on amd64 and arm64, and dragonfly
on amd64, which is its only Go port.
Two things the VM jobs caught that cross-compiling could not.

NetBSD couldn't find the toolchain at all: pkgsrc installs under
/usr/pkg, which isn't on the default PATH the way FreeBSD's
/usr/local/bin is, so `go` was missing despite pkg_add succeeding.

DragonFly can't round-trip device nodes. mknod reports success, but the
node reads back with an Rdev of NODEV (0xffffffff) for every major/minor
pair the test tries, 0:0 included. TestMkdev passes there, so the
encoding desync computes is right and it's the trip through the kernel
that loses it. Skip the round-trip test there and say so in the platform
table; nothing else in the suite is affected.

Worth noting the DragonFly package is Go 1.21, well under what go.mod
asks for, and the toolchain download picked up 1.25.0 for
dragonfly/amd64 by itself. So the `go version` echo earned its place on
the first run.
Two follow-ups from the second CI round.

The NetBSD PATH fix was aimed at the wrong cause. pkgsrc's `go` is a
meta-package that installs no files of its own, and the versioned
package it depends on lands as /usr/pkg/bin/go126 and
/usr/pkg/go126/bin/go. Nothing anywhere provides an unversioned `go`,
so no amount of PATH pointing at /usr/pkg/bin would have found it.
Glob for the versioned directory instead, without hard-coding a number
pkgsrc will bump. NetBSD 10.1 and 11.0 both currently carry 1.26.5.

Build with -buildvcs=false in the VMs: git exits 128 on the tree rsynced
into the guest, which failed the DragonFly build on version stamping
alone after its tests had all passed. Release builds stamp through
goreleaser's ldflags, so nothing is lost. Applied to all four VM jobs,
since FreeBSD and OpenBSD passing it so far looks incidental rather than
guaranteed.
Reading extended attributes treated every error as fatal, so a walk over
a filesystem that simply doesn't implement them failed on its first
entry with "operation not supported" instead of reporting a tree with no
xattrs. NetBSD is where this shows up - its extattr support is per
filesystem and tmpfs has none, so `desync tar` couldn't read anything
under /tmp at all - but it isn't NetBSD-specific: the same is true of
vfat on Linux.

Tell the two cases apart and carry on when support is simply absent. A
real failure to read attributes still surfaces. Note that ENOTSUP and
EOPNOTSUPP are the same value on Linux but not on the BSDs, so both have
to be tested.

The write path is deliberately left alone: dropping attributes an
archive actually carries is a loss worth an error, while finding none to
read is not.
NetBSD's mknodat(2) is declared to take a uint32_t where every other BSD
takes a dev_t, and the device number doesn't survive the call: nodes come
back with an rdev of 0, so 1:3 read back as 0:0 and only 0:0 appeared to
work. Its plain mknod(2) is the versioned __mknod50 and does carry the
full dev_t, so route NetBSD through the same path Darwin already uses.

The other two stay where they are. OpenBSD and DragonFly both declare
mknodat with a dev_t, and OpenBSD passes the round-trip test as it
stands. DragonFly's nodes come back as NODEV rather than 0, which is a
different thing and not something the call convention explains, so that
one keeps its skip.
@folbricht
folbricht merged commit 40c1f28 into master Sep 2, 2026
9 checks passed
@folbricht
folbricht deleted the bsd-support branch September 2, 2026 10:03
folbricht added a commit that referenced this pull request Sep 2, 2026
CreateDevice passed on whatever error mknod returned and never checked
the result, so a filesystem that accepts a device number without
recording it produced a node pointing at the wrong device, silently and
with a successful exit.

Two platforms are known to do exactly that. DragonFly's mknod stores
NODEV. NetBSD's did the same through mknodat until #403 switched it to
mknod. Neither reported anything: what gave NetBSD away was a test that
happens to run as root in a VM job, and nothing would have given away a
filesystem that behaved the same way under Linux or macOS, because that
test never runs there - GitHub's hosted runners aren't root.

So read the node back and compare. Whether a device number survives
depends on the kernel and on the filesystem underneath rather than on
the operating system, so this isn't something a build tag can express.
An extraction that stops and says so beats a device node pointing
somewhere else.

On DragonFly untar now refuses device entries instead of writing wrong
ones, which is the intended trade and is noted in the platform table.

Verified as root on Linux, where the round-trip test had never actually
run before: it passes, and a deliberate mismatch is caught with the
expected message.
@matshch matshch mentioned this pull request Sep 3, 2026
7 of 13 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

⌂ 换网址