Skip to content

gpioioctl.GPIOLine.Read() silently reconfigures output pins as input with PullUp #75

@acanewby

Description

@acanewby

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:

  1. If drvGPIO.gpioMemory is populated (via /dev/gpiomem or /dev/mem): reads the GPIO level register directly — non-destructive.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions