First of all, thank you for periph.io — it's an excellent project and has been a pleasure to work with.
We've found a behaviour in gpioioctl.GPIOLine.Read() that can cause unintended hardware side effects. When Read() is called on a GPIO line that is currently configured as output (e.g. by a Device Tree overlay), it silently reconfigures the line as input with gpio.PullUp:
// gpioioctl/gpio.go:172
func (line *GPIOLine) Read() gpio.Level {
if line.direction != LineInput {
err := line.In(gpio.PullUp, gpio.NoEdge) // <-- reconfigures pin
...
}
...
}
The pull-up drives the pin high, overriding whatever state the DT overlay or previous software had established.
How we hit this
Our application runs as a non-root systemd service user on a Raspberry Pi 4B. The bcm283x driver's Pin.Read() method has two paths:
- If
drvGPIO.gpioMemory is populated (via /dev/gpiomem or /dev/mem): reads the GPIO level register directly — non-destructive.
- If
drvGPIO.gpioMemory is nil: falls through to p.ioctlPin.Read() — reconfigures pin direction and pull.
On Ubuntu 24.04, /dev/gpiomem had incorrect permissions (root:root rw------- instead of root:gpio rw-rw----) due to a udev rule that matches SUBSYSTEM=="bcm2835-gpiomem" while the kernel presents the device as SUBSYSTEM=="gpiomem" (no bcm2835 prefix). This caused pmem.MapGPIO() to fail, gpioMemory to remain nil, and all Read() calls to go through the ioctl path — driving output pins high.
Any application controlling external hardware via GPIO output pins (relays, motors, LEDs, power switching) could be affected if the memory-mapped path fails for any reason.
Expected behaviour
Read() should return the current pin state without changing pin direction or pull configuration. If the pin is configured as output, reading it should return the current output level.
Suggested fix
In gpioioctl.GPIOLine.Read(), when the line is not currently configured as input, either:
- Use
GPIO_V2_LINE_GET_VALUES_IOCTL directly without reconfiguring the line direction, or
- Read the value in the current direction (output lines have a readable value in the chardev interface), or
- At minimum, use
gpio.PullNoChange instead of gpio.PullUp to avoid driving the pin
Environment
- periph.io/x/host/v3 v3.8.5
- periph.io/x/conn/v3 v3.7.2
- Raspberry Pi 4B, Ubuntu 24.04, ARM64
- GPIO pins configured as outputs by Device Tree overlay
Workaround
Ship a udev rule ensuring /dev/gpiomem is accessible to the service user so that pmem.MapGPIO() succeeds and Read() uses the non-destructive memory-mapped path:
SUBSYSTEM=="*gpiomem*", KERNEL=="gpiomem", GROUP="gpio", MODE="0660"
The wildcard subsystem match covers both the legacy bcm2835-gpiomem and current gpiomem kernel subsystem names.
First of all, thank you for periph.io — it's an excellent project and has been a pleasure to work with.
We've found a behaviour in
gpioioctl.GPIOLine.Read()that can cause unintended hardware side effects. WhenRead()is called on a GPIO line that is currently configured as output (e.g. by a Device Tree overlay), it silently reconfigures the line as input withgpio.PullUp:The pull-up drives the pin high, overriding whatever state the DT overlay or previous software had established.
How we hit this
Our application runs as a non-root systemd service user on a Raspberry Pi 4B. The
bcm283xdriver'sPin.Read()method has two paths:drvGPIO.gpioMemoryis populated (via/dev/gpiomemor/dev/mem): reads the GPIO level register directly — non-destructive.drvGPIO.gpioMemoryis nil: falls through top.ioctlPin.Read()— reconfigures pin direction and pull.On Ubuntu 24.04,
/dev/gpiomemhad incorrect permissions (root:root rw-------instead ofroot:gpio rw-rw----) due to a udev rule that matchesSUBSYSTEM=="bcm2835-gpiomem"while the kernel presents the device asSUBSYSTEM=="gpiomem"(no bcm2835 prefix). This causedpmem.MapGPIO()to fail,gpioMemoryto remain nil, and allRead()calls to go through the ioctl path — driving output pins high.Any application controlling external hardware via GPIO output pins (relays, motors, LEDs, power switching) could be affected if the memory-mapped path fails for any reason.
Expected behaviour
Read()should return the current pin state without changing pin direction or pull configuration. If the pin is configured as output, reading it should return the current output level.Suggested fix
In
gpioioctl.GPIOLine.Read(), when the line is not currently configured as input, either:GPIO_V2_LINE_GET_VALUES_IOCTLdirectly without reconfiguring the line direction, orgpio.PullNoChangeinstead ofgpio.PullUpto avoid driving the pinEnvironment
Workaround
Ship a udev rule ensuring
/dev/gpiomemis accessible to the service user so thatpmem.MapGPIO()succeeds andRead()uses the non-destructive memory-mapped path:The wildcard subsystem match covers both the legacy
bcm2835-gpiomemand currentgpiomemkernel subsystem names.