Skip to content

Wire (LinuxHardwareI2C) defaults its fd to 0 (stdin), I2C ops before begin() block on stdin or run against the wrong fd #71

@l5yth

Description

@l5yth

Wire (LinuxHardwareI2C) defaults its fd to 0 (stdin), I2C ops before begin() block on stdin or run against the wrong fd

Summary

LinuxHardwareI2C::i2c_file is initialized to 0, which is stdin. Until Wire.begin() successfully opens an I2C device, every I2C syscall runs against file descriptor 0. In particular, Wire.read() / Wire.readBytes() issue a blocking ::read(0, …) on stdin: on an interactive terminal the program hangs indefinitely; other methods (beginTransmission, endTransmission, available) silently operate on stdin/stdout instead of an I2C bus.

The global Wire instance (extern LinuxHardwareI2C Wire;) makes this reachable from any sketch that touches I2C before calling begin(), or after a failed open(), since i2c_file is never reset.

Affected version

meshtastic/framework-portduino @ 32b7b2c (HEAD, 2026-05-01).

Location

  • cores/portduino/linux/LinuxHardwareI2C.h:23, int i2c_file = 0;
  • cores/portduino/linux/LinuxHardwareI2C.cpp:97, if (::read(i2c_file, &tmpBuf, 1) == -1) (in read())
  • cores/portduino/linux/LinuxHardwareI2C.cpp:108, bytes_read = ::read(i2c_file, buffer, length); (in readBytes())
  • cores/portduino/linux/LinuxHardwareI2C.cpp:119, ioctl(i2c_file, FIONREAD, &numBytes); (in available())

Steps to reproduce

A sketch that reads from Wire before opening it:

void setup() {
    // note: no Wire.begin(...)
    int b = Wire.read();   // -> ::read(0, ...), blocks on stdin
}
void loop() {}
  • Interactive terminal: hangs forever in read().
  • stdin redirected from a pipe/fifo with no data: also blocks.
  • CI / </dev/null: read() returns EOF immediately, so the bug is masked, which is likely why it has gone unnoticed. (available() likewise doesn't hang: FIONREAD on stdin returns 0.)

Expected

Before a successful begin(), I2C operations should fail fast (return an error / -1) rather than touch stdin/stdout or block.

Actual

With i2c_file == 0, read()/readBytes() block on stdin, and ioctl/::write run against fd 0.

Root cause

The default member initializer int i2c_file = 0; happens to be a valid (and special) fd. An unopened fd should be -1.

Suggested fix

Default to -1, matching what LinuxSerial already does for serial_port (cores/portduino/linux/LinuxSerial.h:13, int serial_port = -1;). Syscalls then fail with EBADF instead of hitting stdin/stdout:

- int i2c_file = 0;
+ int i2c_file = -1; // unopened: fail fast instead of operating on stdin/stdout

Optionally also guard the entry points (if (i2c_file < 0) return -1;) so callers get a clean error instead of a syscall failure.

Notes

Found while working on ArduLinux, a downstream continuation; the one-line -1 default resolved a deterministic hang there with no other changes.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions