dotnet / dotnet/iot

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

Open
#2,600 3 comments 1 reaction 2 assignees Claimed by @krwq View on GitHub
untriaged
Dominant language
C#
Stars
2.4k
Forks
630
Avg merge
11d 3h
Merged PRs (30d)
2

Description

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`

```csharp
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`:

```csharp
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:

```csharp
GpioController controller = new();
controller.OpenPin(21, PinMode.Input);
controller.RegisterCallbackForPinValueChangedEvent(
21,
PinEventTypes.Rising | PinEventTypes.Falling,
(sender, args) => Console.WriteLine($"pin {args.PinNumber} -> {args.ChangeType}"));
```

2. Leave the process running. No input activity is required.
3. 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-"
```

## 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.

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.