Skip to content

fix: size the SIOCGIFMTU buffer for a whole struct ifreq - #64

Merged
cao-xiao-hao merged 1 commit into
mainfrom
fix/size-the-ifreq-buffer
Oct 14, 2026
Merged

cao-xiao-hao merged 1 commit into
mainfrom
fix/size-the-ifreq-buffer

Conversation

@cao-xiao-hao

@cao-xiao-hao cao-xiao-hao commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Summary

connect() failed on Python 3.14 against every real gripper. _iface_mtu() handed
fcntl.ioctl() an 18-byte buffer for SIOCGIFMTU, while the kernel writes back a
whole struct ifreq (40 bytes on 64-bit Linux). Since cpython c2eaeee3 the
ioctl() wrapper puts a guard byte after the argument and raises SystemError: buffer overflow when that write lands past it — a SystemError, not an OSError,
so it escaped _iface_mtu()'s except OSError and killed the connect. The
simulated backends never call _iface_mtu(), and 3.13 and earlier accepted the
short buffer silently, which is why a self-check passed while real hardware could
not be reached.

No breaking change: a private helper, same signature, same return, and a missing MTU
already meant "fall back to the requested mode". Upstream's callers see nothing but
a connect that works again on 3.14.

Closes #63.

Changes

  • src/litegrip/can/transport.py — pad the ifreq to _IFREQ_SIZE (40 bytes,
    sizeof(struct ifreq) on 64-bit Linux, 32 on 32-bit) before the ioctl, and read
    the mtu out of the first 18 bytes of the reply. The reply is exactly as long as the
    argument on every version, so it has to be sliced back down.
  • tests/test_iface_mtu.py — new. Pins the buffer length with a stubbed ioctl
    (fails on any interpreter without the fix), pins None when the ioctl fails, and
    reads every interface's MTU back through the real ioctl, comparing against
    /sys/class/net/<iface>/mtu.
  • .github/workflows/ci.yml — the suite now runs twice in the same job: on the
    runner's own python3 (3.10) and on 3.14. A matrix is the obvious way to spell
    two interpreters, and it is wrong here: main requires a status check named
    exactly test, while a matrix names its checks test (3.10) / test (3.14). The
    first push of this PR did use a matrix, and gh pr merge answered the base branch policy prohibits the merge — the required check would never report again, for this
    PR or any later one. The second interpreter is a step instead, and it prints its
    version before running so the log says which interpreter the results are from.

Testing

On this machine, 3.10.12 (there is no 3.14 here; the second run is what CI is for),
from the repository root:

$ python3 -m unittest discover -s tests -t tests -v
...
Ran 440 tests in 9.680s

OK (skipped=12)

The 12 skips are test_zenoh_link.py, which needs eclipse-zenoh installed.

The new test against the unfixed helper — what the suite says without this PR:

$ python3 -m unittest discover -s tests -t tests -p "test_iface_mtu.py" -v
ERROR: test_buffer_holds_a_whole_ifreq (test_iface_mtu.TestIfreqBufferSize)
  File "src/litegrip/can/transport.py", line 50, in _iface_mtu
    return struct.unpack("16sH", res)[1]
struct.error: unpack requires a buffer of 18 bytes

Ran 3 tests in 0.001s

FAILED (errors=1)

On CI's 3.14.8 run, all three test_iface_mtu cases pass with this change. The
real-ioctl test passes on 3.10 either way — the regression is invisible there, which
is exactly what the 3.14 run covers; on 3.14 that path raises the SystemError from
#63 instead of returning a value.

Also re-ran the reporter's standalone repro on 3.10.12: the 18-byte argument returns
18 bytes with the mtu in place, the 40-byte one returns 40 bytes with the same mtu —
so padding the call changes nothing about what the transport reads on the versions it
already worked on.

Issues

Closes #63

@cao-xiao-hao

Copy link
Copy Markdown
Contributor Author

One note on the 3.14 leg, recorded because it is new coverage rather than new code: the first run of this PR failed on test_teleop.ReadinessHandshakeTest.test_slave_withdraws_ready_when_the_leader_goes_quiet (3.14.8) while 3.10 passed on the same runner, and the re-run of that same commit passed on both. So that test is flaky under the new leg, not broken by this change — every test_iface_mtu case passed on 3.14 on the first attempt.

The failure was the first assertion, "the follower announced ready", with watchdog_s=0.05. Locally (3.10.12) the follower announces ready 0.7 ms after start(), a ~70x margin against that 50 ms watchdog, so the flake is not a slow-machine effect and the cause is not yet known; the follow-up belongs in its own issue.

Python 3.14 put a guard byte after the argument fcntl.ioctl() is handed
and raises SystemError when the kernel writes past it.  SIOCGIFMTU
writes back a whole struct ifreq — 40 bytes on 64-bit Linux — so the
18-byte buffer _iface_mtu() passed tripped the guard: connect() died
with "buffer overflow" on every real gripper, while the simulated paths
never reach _iface_mtu() and stayed green.

Pad the buffer to sizeof(struct ifreq), and slice the mtu back out of
the front of the reply — the reply is as long as the argument on every
version.

The suite could not have caught this, because 3.10 and 3.12 accept the
short buffer silently, so the unit tests now run on 3.14 as well.

Closes #63
@cao-xiao-hao
cao-xiao-hao force-pushed the fix/size-the-ifreq-buffer branch from 442c712 to ad605f6 Compare October 14, 2026 04:36
@cao-xiao-hao
cao-xiao-hao merged commit 9180117 into main Oct 14, 2026
1 check passed
@cao-xiao-hao
cao-xiao-hao deleted the fix/size-the-ifreq-buffer branch October 14, 2026 04:39
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 1.0.2 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

released Shipped in a release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Size the SIOCGIFMTU buffer for a whole struct ifreq

1 participant