Skip to content

LibGpiodV2: unchecked null from gpiod_edge_event_buffer_get_event aborts the process (gpiod_edge_event_copy: Assertion 'event' failed) #2600

Description

@kai-melchior

A long-running service that watches a single input pin via
RegisterCallbackForPinValueChangedEvent is killed several times a day by a native abort()
raised inside libgpiod:

Garden.Pi: edge-event.c:41: gpiod_edge_event_copy: Assertion `event' failed.
systemd[1]: garden.service: Main process exited, code=killed, status=6/ABRT
systemd[1]: garden.service: Failed with result 'signal'.

Because this is a native assertion, the process dies outright — it cannot be caught or handled
from managed code, so there is no way for an application to survive it.

Notably, this happens without any real edge events occurring. In one two-hour window the
application logged zero pin changes on the only watched line, yet the process still aborted. So
this is not a matter of a bouncing input producing a flood of events; the crash occurs on a
wakeup that delivers no event.

Analysis

EdgeEventBuffer.GetEvent passes the pointer returned by gpiod_edge_event_buffer_get_event
directly into gpiod_edge_event_copy without checking it for null:

src/System.Device.Gpio/Interop/Unix/libgpiod/V2/Proxies/EdgeEventBuffer.cs

public EdgeEvent GetEvent(ulong index)
{
    return CallLibgpiod(() =>
    {
        using EdgeEventNotFreeable edgeEventHandle = LibgpiodV2.gpiod_edge_event_buffer_get_event(Handle, index);
        // no null check here
        EdgeEventSafeHandle edgeEventCopyHandle = LibgpiodV2.gpiod_edge_event_copy(edgeEventHandle);
        return new EdgeEvent(edgeEventCopyHandle);
    });
}

gpiod_edge_event_buffer_get_event returns NULL when the requested index is not populated in
the buffer, and gpiod_edge_event_copy starts with assert(event) — so a null pointer here is
an immediate abort() rather than a recoverable error.

The caller is the observer loop in
src/System.Device.Gpio/System/Device/Gpio/Drivers/LibGpiodV2EventObserver.cs:

int waitResult = request.WaitEdgeEventsRespectfully(WaitEdgeEventsTimeout);
// ... timeout / interrupt handling ...
int numberOfReadEvents = request.ReadEdgeEvents(edgeEventBuffer);

for (int i = 0; i < numberOfReadEvents; i++)
{
    EdgeEvent edgeEvent = edgeEventBuffer.GetEvent((ulong)i);
    HandleEdgeEvent(edgeEvent);
}

The loop bound comes from gpiod_line_request_read_edge_events (via LineRequest.ReadEdgeEvents,
called with edgeEventBuffer.Capacity, which is 10 by default from
LibGpiodProxyFactory.CreateEdgeEventBuffer). The observed crash implies that the count returned
there can exceed the number of events actually retrievable from the buffer, at which point
GetEvent receives null and the process aborts.

Note also that this runs on a background task whose catch only logs to Console.WriteLine — but
that is moot here, since a native assertion never surfaces as a managed exception.

Expected behaviour

A null event should not be able to terminate the host process. Either:

  1. EdgeEventBuffer.GetEvent null-checks the handle and throws a GpiodException (or returns
    null and lets the observer skip the entry), and/or
  2. the observer loop bounds its iteration by gpiod_edge_event_buffer_get_num_events (already
    exposed as EdgeEventBuffer.GetNumEvents()) rather than by the value returned from
    ReadEdgeEvents.

(1) alone is enough to turn a process-killing abort into a handleable error, and looks like the
minimal safe fix.

Environment

Hardware Raspberry Pi 2 Model B Rev 1.1 (armv7l)
OS Raspbian GNU/Linux 13 (trixie)
libgpiod 2.2.1-2+rpi1+deb13u1 (libgpiod3, libgpiod.so.3.1.1)
.NET 10.0, self-contained linux-arm
System.Device.Gpio 4.2.0 (latest release)
Driver LibGpiodV2Driver (auto-selected)

Reproduction

  1. On a Debian trixie / libgpiod 2.2.x system, open one pin as input and register an edge
    callback:
GpioController controller = new();
controller.OpenPin(21, PinMode.Input);
controller.RegisterCallbackForPinValueChangedEvent(
    21,
    PinEventTypes.Rising | PinEventTypes.Falling,
    (sender, args) => Console.WriteLine($"pin {args.PinNumber} -> {args.ChangeType}"));
  1. Leave the process running. No input activity is required.
  2. Within minutes to hours the process aborts with the assertion above.

gpioinfo for the watched line while running:

line  21: "GPIO21"  input bias=disabled edges=both consumer="C#-LibGpiodV2Driver-<pid>"

Impact

There is no application-side workaround: the abort cannot be caught, and 4.2.0 is the newest
release. The only options are to stop using edge-event watching entirely (poll instead), or to
pin the V1 driver — which on trixie needs libgpiod2 alongside libgpiod3 and has its own open
crash reports. We have currently had to disable input reading altogether.

Activity

  1. pgrawehr commented on Aug 27, 2026

    @pgrawehr
    Contributor

    @kai-melchior You mention some kind of wakeup. Does the Raspberry Pi enter some kind of sleep state before this happens? I have seen instances where I waited very long for a line but nothing bad happened.

  2. kai-melchior commented on Sep 7, 2026

    @kai-melchior
    Author

    @pgrawehr No sleep state. The Pi was only idle and waiting for incoming changes

  3. raffaeler commented on Sep 7, 2026

    @raffaeler
    Contributor

    @kai-melchior In this case I do not understand how could the handle become invalid.
    If the machine is always awake and the process does not close the handle, it should remain valid.
    What am I missing?

  4. pgrawehr commented on Sep 17, 2026

    @pgrawehr
    Contributor

    [Triage] I will try to reproduce this when I find time. Still confuses me why the handle becomes invalid just while waiting.

  5. wolfgang-knobloch commented on Sep 23, 2026

    @wolfgang-knobloch

    We hit the same abort on an armv7l gateway (self-contained linux-arm, libgpiod 2.x / libgpiod.so.3, System.Device.Gpio 3.2.0). I believe the root cause is an ABI mismatch in the V2 P/Invoke declaration on 32-bit ARM, not an invalid handle.

    libgpiod declares:

    struct gpiod_edge_event *
    gpiod_edge_event_buffer_get_event(struct gpiod_edge_event_buffer *buffer, unsigned long index);

    Interop.libgpiod.cs (V2) declares:

    public static extern EdgeEventNotFreeable gpiod_edge_event_buffer_get_event(EdgeEventBufferSafeHandle buffer, ulong index);

    C unsigned long is 32-bit on ARM32 (ILP32), but ulong is always 64-bit. Under AAPCS a 64-bit argument must go into an even register pair, so the runtime passes buffer in r0 and index in r2:r3, skipping r1. libgpiod reads its 32-bit index from r1, which contains whatever was left there. That index is almost always >= num_events, so gpiod_edge_event_buffer_get_event returns NULL and gpiod_edge_event_copy hits assert(event).

    This fits the report:

    • It only affects 32-bit ARM (the reporter's Pi 2 is armv7l, and ours is too). On arm64 and x64, unsigned long is 64-bit, so the declaration is correct.
    • "No pin changes logged": the first real edge event crashes in GetEvent before any callback can run, so nothing is ever logged.
    • The handle is valid; only the index is wrong.

    Suggested fix: use nuint for C unsigned long / size_t parameters and return values in the V2 binding. That covers gpiod_edge_event_buffer_get_event (index) and the unsigned long returns of gpiod_edge_event_get_global_seqno / gpiod_edge_event_get_line_seqno, which currently return a garbage upper half on ARM32. size_t parameters such as gpiod_edge_event_buffer_new(capacity) and gpiod_line_request_read_edge_events(max_events) are declared as int, which works on 32-bit but should also be nuint for correctness on 64-bit. A null check in EdgeEventBuffer.GetEvent, as proposed above, is still a good safety net.

  6. pgrawehr commented on Oct 1, 2026

    @pgrawehr
    Contributor

    @wolfgang-knobloch Please review #2610.

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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions