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:
EdgeEventBuffer.GetEvent null-checks the handle and throws a GpiodException (or returns
null and lets the observer skip the entry), and/or
- 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
- 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}"));
- Leave the process running. No input activity is required.
- 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.
A long-running service that watches a single input pin via
RegisterCallbackForPinValueChangedEventis killed several times a day by a nativeabort()raised inside libgpiod:
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.GetEventpasses the pointer returned bygpiod_edge_event_buffer_get_eventdirectly into
gpiod_edge_event_copywithout checking it for null:src/System.Device.Gpio/Interop/Unix/libgpiod/V2/Proxies/EdgeEventBuffer.csgpiod_edge_event_buffer_get_eventreturnsNULLwhen the requested index is not populated inthe buffer, and
gpiod_edge_event_copystarts withassert(event)— so a null pointer here isan immediate
abort()rather than a recoverable error.The caller is the observer loop in
src/System.Device.Gpio/System/Device/Gpio/Drivers/LibGpiodV2EventObserver.cs:The loop bound comes from
gpiod_line_request_read_edge_events(viaLineRequest.ReadEdgeEvents,called with
edgeEventBuffer.Capacity, which is 10 by default fromLibGpiodProxyFactory.CreateEdgeEventBuffer). The observed crash implies that the count returnedthere can exceed the number of events actually retrievable from the buffer, at which point
GetEventreceives null and the process aborts.Note also that this runs on a background task whose
catchonly logs toConsole.WriteLine— butthat 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:
EdgeEventBuffer.GetEventnull-checks the handle and throws aGpiodException(or returnsnull and lets the observer skip the entry), and/or
gpiod_edge_event_buffer_get_num_events(alreadyexposed as
EdgeEventBuffer.GetNumEvents()) rather than by the value returned fromReadEdgeEvents.(1) alone is enough to turn a process-killing abort into a handleable error, and looks like the
minimal safe fix.
Environment
libgpiod3,libgpiod.so.3.1.1)linux-armLibGpiodV2Driver(auto-selected)Reproduction
callback:
gpioinfofor the watched line while running: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
libgpiod2alongsidelibgpiod3and has its own opencrash reports. We have currently had to disable input reading altogether.