Skip to content

pillar: libzfs ASSERT in namespace_reload() aborts zedbox via cgo, device reboots with no panic trail #6299

Description

@eriknordmark

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):

  1. Audit every go-libzfs caller in pillar and put them behind one shared lock,
    rather than a lock private to zfsmanager's context.
  2. 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.
  3. 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".

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions