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

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