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.
Wire(LinuxHardwareI2C) defaults its fd to0(stdin), I2C ops beforebegin()block on stdin or run against the wrong fdSummary
LinuxHardwareI2C::i2c_fileis initialized to0, which is stdin. UntilWire.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
Wireinstance (extern LinuxHardwareI2C Wire;) makes this reachable from any sketch that touches I2C before callingbegin(), or after a failedopen(), sincei2c_fileis 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)(inread())cores/portduino/linux/LinuxHardwareI2C.cpp:108,bytes_read = ::read(i2c_file, buffer, length);(inreadBytes())cores/portduino/linux/LinuxHardwareI2C.cpp:119,ioctl(i2c_file, FIONREAD, &numBytes);(inavailable())Steps to reproduce
A sketch that reads from
Wirebefore opening it:read().</dev/null:read()returns EOF immediately, so the bug is masked, which is likely why it has gone unnoticed. (available()likewise doesn't hang:FIONREADon 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, andioctl/::writerun 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 whatLinuxSerialalready does forserial_port(cores/portduino/linux/LinuxSerial.h:13,int serial_port = -1;). Syscalls then fail withEBADFinstead of hitting 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
-1default resolved a deterministic hang there with no other changes.