Describe the bug
On a ZFS-persist EVE-k device, zedbox was killed by abort() raised from an
assertion inside libzfs, reached through cgo from zfsmanager's periodic
storage-metrics collection. Losing zedbox takes every pillar agent with it, the
watchdog then finds /run/zedbox.pid pointing at a dead process, and the device
reboots — reporting BootReasonWatchdogPid.
The failure leaves none of the usual evidence a pillar crash leaves, which is
what makes it worth a dedicated issue: a C ASSERT raises SIGABRT before any Go
panic machinery runs, so there is no panic: line, no level=fatal, an empty
/persist/newlog/panicStacks, and an empty /persist/reboot-stack. Searching for
those (the natural first sweep) returns clean and the reboot looks unexplained.
The evidence is on stderr, which newlog carries under source: "pillar".
Recent release
Seen once on 0.0.0-rucoder_kube-init-go-620171de-k-amd64, a fork build. The
relevant code is unmodified with respect to master (checked at
38e378177): the call path, the locking, and the go-libzfs pin
(github.com/andrewd-zededa/go-libzfs v0.0.0-20251014191158-755aaeb96a92) are
identical, so master is expected to be equally exposed. Not yet reproduced on a
release build.
Duplicate
Searched the issue list for namespace_reload, libzfs, zfsIterLock,
PoolOpenAll, SIGABRT zfs — no existing issue.
How to reproduce
No deterministic reproducer. Observed once in ~4 days of continuous
test traffic on one device; the same assertion appears in zero of the four other
device-log windows captured from that run.
- EVE-OS version:
0.0.0-rucoder_kube-init-go-620171de-k-amd64 (EVE-k, k3s +
KubeVirt + Longhorn), /persist on ZFS
- Architecture: amd64
- Conditions: 1-core box under sustained app create/destroy load, load average
around 8 when idle and observed above 27 under test
Expected behavior
zedbox does not die because a metrics collector called into libzfs. Worst case a
metrics cycle fails and logs an error.
Error message
Device time 2026-08-10T02:31:10 UTC, newlog source: "pillar":
ASSERT at lib/libzfs/libzfs_config.c:190:namespace_reload()
uu_avl_find(hdl->libzfs_ns_avl, cn, NULL, &where) == NULL
PID: 4280 COMM: zedbox
TID: 6341 NAME: zedbox
SIGABRT: abort
PC=0x7f07114e1c2f m=13 sigcode=18446744073709551610
signal arrived during cgo execution
goroutine 4490 gp=0xc002b81500 m=13 mp=0xc0005f4808 [syscall]:
runtime.cgocall(0x59455e5, 0xc002d9b720)
/usr/local/go/src/runtime/cgocall.go:167 +0x4b
github.com/andrewd-zededa/go-libzfs._Cfunc_zpool_list_openall()
_cgo_gotypes.go:1753 +0x45
github.com/andrewd-zededa/go-libzfs.PoolOpenAll()
/pillar/vendor/github.com/andrewd-zededa/go-libzfs/zpool.go:465 +0x34
github.com/lf-edge/eve/pkg/pillar/cmd/zfsmanager.collectAndPublishStorageMetrics(0xc000c2cc80)
/pillar/cmd/zfsmanager/zfsstoragemetrics.go:39 +0xbc
github.com/lf-edge/eve/pkg/pillar/cmd/zfsmanager.storageMetricsPublisher(0xc000c2cc80)
/pillar/cmd/zfsmanager/zfsstoragemetrics.go:30 +0x56
github.com/lf-edge/eve/pkg/pillar/cmd/zfsmanager.Run.gowrap4()
/pillar/cmd/zfsmanager/zfsmanager.go:213 +0x25
The last log record from pid 4280 is collectAndPublishStorageMetrics start at
02:31:10.552 — the abort is 144 ms later, inside the call it had just announced.
The rest follows mechanically:
02:32:02+ watchdog pinging process 4280 (/run/zedbox.pid) gave errno = 3 = 'No such process'
02:32:12 watchdog Retry timed-out at 61 seconds for /run/zedbox.pid → runs the repair binary
02:33:12 watchdog repair child 30942 timed out
02:33:12 watchdog shutting down the system because of error 255 = 'unconditional reboot requested'
(Note for anyone correlating: the timestamp in the reported reboot reason is when
the repair binary was launched, ~60 s before the machine actually went down.)
Additional context
Why the assertion may be firing — libzfs handle use is only partially
serialized. libzfs is not thread-safe, and the namespace_reload() AVL
assertion is the classic symptom of one handle being used from more than one
thread. In pillar the serialization covers only part of the callers (line numbers
at 38e378177):
zfsIterLock is declared at pkg/pillar/cmd/zfsmanager/zfsmanager.go:58 and
taken at exactly two call sites, both inside zfsmanager —
zfsstoragemetrics.go:37 and zfsstoragestatus.go:37.
pkg/pillar/zfs/zfs.go — the shared helper used from other agents —
contains no mutex at all and calls libzfs.DatasetOpen directly (e.g.
GetDatasetUsageStat).
pkg/pillar/cmd/zfsmanager/handlediskconfig.go calls libzfs.PoolOpen
without taking the lock either.
pkg/pillar/vault/handler_zfs.go is a further go-libzfs user.
Concretely, volumemgr reaches libzfs on every metrics cycle: on a ZFS-persist
device diskmetrics.PersistUsageStat routes /persist to
zfs.GetDatasetUsageStat(types.PersistDataset), from
createOrUpdateDiskMetrics. So at least two goroutines in the one zedbox process
call libzfs with no lock in common.
This is a hypothesis, not a demonstrated interleaving. Only one goroutine is
inside libzfs in the abort traceback, and volumemgr's metrics pass had logged its
last path 1.0 s earlier rather than concurrently. The assertion fires when
namespace_reload rebuilds a namespace AVL that is already inconsistent, so a
corrupting access that had already returned is compatible with the evidence — but
so is a fault with no concurrency in it. The missing lock is offered as the
leading suspect and as an audit worth doing on its own merits.
Impact. A single unlucky metrics cycle costs a full device reboot with no
diagnostic trail, and every application on the node goes down with it. In this
run it also cost two test verdicts as collateral.
Suggested next steps (happy to be redirected):
- Audit every
go-libzfs caller in pillar and put them behind one shared lock,
rather than a lock private to zfsmanager's context.
- Consider whether
namespace_reload's assertion should be reachable from a
metrics path at all, or whether pool enumeration for metrics can use a cached
handle / a single owning goroutine.
- Independently of the cause: a C-level abort in a cgo dependency should not be
invisible. Nothing writes panicStacks on SIGABRT, so consider capturing
the stderr traceback the way Go panics are captured, so the next occurrence is
diagnosable without knowing to grep source: "pillar".
Describe the bug
On a ZFS-persist EVE-k device,
zedboxwas killed byabort()raised from anassertion inside libzfs, reached through cgo from zfsmanager's periodic
storage-metrics collection. Losing zedbox takes every pillar agent with it, the
watchdog then finds
/run/zedbox.pidpointing at a dead process, and the devicereboots — reporting
BootReasonWatchdogPid.The failure leaves none of the usual evidence a pillar crash leaves, which is
what makes it worth a dedicated issue: a C
ASSERTraisesSIGABRTbefore any Gopanic machinery runs, so there is no
panic:line, nolevel=fatal, an empty/persist/newlog/panicStacks, and an empty/persist/reboot-stack. Searching forthose (the natural first sweep) returns clean and the reboot looks unexplained.
The evidence is on stderr, which newlog carries under
source: "pillar".Recent release
Seen once on
0.0.0-rucoder_kube-init-go-620171de-k-amd64, a fork build. Therelevant code is unmodified with respect to
master(checked at38e378177): the call path, the locking, and thego-libzfspin(
github.com/andrewd-zededa/go-libzfs v0.0.0-20251014191158-755aaeb96a92) areidentical, so master is expected to be equally exposed. Not yet reproduced on a
release build.
Duplicate
Searched the issue list for
namespace_reload,libzfs,zfsIterLock,PoolOpenAll,SIGABRT zfs— no existing issue.How to reproduce
No deterministic reproducer. Observed once in ~4 days of continuous
test traffic on one device; the same assertion appears in zero of the four other
device-log windows captured from that run.
0.0.0-rucoder_kube-init-go-620171de-k-amd64(EVE-k, k3s +KubeVirt + Longhorn),
/persiston ZFSaround 8 when idle and observed above 27 under test
Expected behavior
zedbox does not die because a metrics collector called into libzfs. Worst case a
metrics cycle fails and logs an error.
Error message
Device time 2026-08-10T02:31:10 UTC, newlog
source: "pillar":The last log record from pid 4280 is
collectAndPublishStorageMetrics startat02:31:10.552 — the abort is 144 ms later, inside the call it had just announced.
The rest follows mechanically:
(Note for anyone correlating: the timestamp in the reported reboot reason is when
the repair binary was launched, ~60 s before the machine actually went down.)
Additional context
Why the assertion may be firing — libzfs handle use is only partially
serialized. libzfs is not thread-safe, and the
namespace_reload()AVLassertion is the classic symptom of one handle being used from more than one
thread. In pillar the serialization covers only part of the callers (line numbers
at
38e378177):zfsIterLockis declared atpkg/pillar/cmd/zfsmanager/zfsmanager.go:58andtaken at exactly two call sites, both inside zfsmanager —
zfsstoragemetrics.go:37andzfsstoragestatus.go:37.pkg/pillar/zfs/zfs.go— the shared helper used from other agents —contains no mutex at all and calls
libzfs.DatasetOpendirectly (e.g.GetDatasetUsageStat).pkg/pillar/cmd/zfsmanager/handlediskconfig.gocallslibzfs.PoolOpenwithout taking the lock either.
pkg/pillar/vault/handler_zfs.gois a furthergo-libzfsuser.Concretely, volumemgr reaches libzfs on every metrics cycle: on a ZFS-persist
device
diskmetrics.PersistUsageStatroutes/persisttozfs.GetDatasetUsageStat(types.PersistDataset), fromcreateOrUpdateDiskMetrics. So at least two goroutines in the one zedbox processcall libzfs with no lock in common.
This is a hypothesis, not a demonstrated interleaving. Only one goroutine is
inside libzfs in the abort traceback, and volumemgr's metrics pass had logged its
last path 1.0 s earlier rather than concurrently. The assertion fires when
namespace_reloadrebuilds a namespace AVL that is already inconsistent, so acorrupting access that had already returned is compatible with the evidence — but
so is a fault with no concurrency in it. The missing lock is offered as the
leading suspect and as an audit worth doing on its own merits.
Impact. A single unlucky metrics cycle costs a full device reboot with no
diagnostic trail, and every application on the node goes down with it. In this
run it also cost two test verdicts as collateral.
Suggested next steps (happy to be redirected):
go-libzfscaller in pillar and put them behind one shared lock,rather than a lock private to zfsmanager's context.
namespace_reload's assertion should be reachable from ametrics path at all, or whether pool enumeration for metrics can use a cached
handle / a single owning goroutine.
invisible. Nothing writes
panicStacksonSIGABRT, so consider capturingthe stderr traceback the way Go panics are captured, so the next occurrence is
diagnosable without knowing to grep
source: "pillar".