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

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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions