Repository navigation
Size the SIOCGIFMTU buffer for a whole struct ifreq #63
Description
Context
LiteGrip.connect() fails on Python 3.14 on every machine, for every backend that
goes through CanTransport.open():
litegrip.exceptions.ConnectError: 连接失败: buffer overflow
The gripper is fine and the CAN interface is fine. The failure is in
litegrip/can/transport.py::_iface_mtu(), which passes a buffer smaller than the
structure the kernel writes back:
ifr = struct.pack("16sH", iface.encode("utf-8")[:15], 0) # 18 bytes
res = fcntl.ioctl(sock.fileno(), _SIOCGIFMTU, ifr)SIOCGIFMTU writes back a whole struct ifreq, not just ifr_name + ifr_mtu.
sizeof(struct ifreq) is 40 bytes on 64-bit Linux and 32 on 32-bit, because
ifr_ifru is a union that has to be large enough for all of its members. The
buffer handed to the ioctl is 18 bytes. Line 49 at 351f80d, unchanged since the
report.
Reported against 5b7da892, observed as vendored in litegrip-studio at
41888cf.
Root cause
CPython commit c2eaeee3dc3306ca486b0377b07b1a957584b691 — "Try to detect a
buffer overflow in fcntl() and ioctl()" — copies the argument into a stack
buffer and writes a guard byte at buf + len. After the ioctl it compares the
guard; anything written past the end of the argument trips it:
memcpy(buf, view.buf, len);
memcpy(buf + len, guard, GUARDSZ);
ret = ioctl(fd, code, buf);
...
if (memcmp(buf + len, guard, GUARDSZ) != 0) {
PyErr_SetString(PyExc_SystemError, "buffer overflow");
return NULL;
}IOCTL_BUFSZ is still 1024, so the failure is not "the buffer was sized from the
argument" — the kernel's 40-byte write-back simply lands on the guard. Before
that commit the same code was harmless, which is why 3.13 and earlier return the
40 bytes without complaining. (gh-144206 / GH-144273 later reworded the
message to "Possible stack corruption in ioctl() due to buffer overflow. Provide
an argument of sufficient size as determined by the operation."; 3.14.6 still
emits the terse wording.)
Two consequences worth knowing:
- The buffer has to hold the whole
struct ifreq, not "enough for the fields
we read". One byte pastlenis enough to trip the check. - 40 bytes covers both widths: a 32-bit kernel writes 32 back into a 40-byte
buffer, and only the process's own width matters.
Fedora tracked the same class of breakage for other projects in
https://bugzilla.redhat.com/show_bug.cgi?id=2370944.
Note that fcntl.ioctl() returns exactly as many bytes as it was given, on every
version, so the fix has to slice the reply back down to the 18 bytes the format
string describes.
Expected
connect() reaches the gripper on 3.14 exactly as it does on 3.10 and 3.12. The
MTU read is a hint the transport uses to pick classic CAN vs CAN FD, so failing it
should at worst fall back to the requested mode, never abort the connect.
Actual
File "litegrip/gripper.py", line 435, in connect
self._can.connect()
File "litegrip/protocols/can_bus.py", line 72, in connect
self._transport.open()
File "litegrip/can/transport.py", line 131, in open
mtu = _iface_mtu(sock, self._channel)
File "litegrip/can/transport.py", line 49, in _iface_mtu
res = fcntl.ioctl(sock.fileno(), _SIOCGIFMTU, ifr)
SystemError: buffer overflow
Total loss of connectivity on 3.14 for every backend that goes through
CanTransport.open() — real hardware only. _iface_mtu() is the only ioctl
call site in the package, and the simulated paths never reach it, which is why a
self-check or a simulation passes while a real gripper cannot be reached.
_iface_mtu() catches OSError only, so the SystemError escapes to the caller,
and the operator sees nothing but the generic 连接失败 wrapper — the failure
reads as a wiring or adapter problem and sends people looking in the wrong place.
Reproduce
No gripper required — the ioctl is called before bind():
import fcntl, socket, struct, sys
print(sys.version.split()[0])
sock = socket.socket(socket.PF_CAN, socket.SOCK_RAW, 1) # CAN_RAW
base = struct.pack("16sH", b"can0", 0) # 18 bytes
print("len(base) =", len(base))
try:
fcntl.ioctl(sock.fileno(), 0x8921, base) # SIOCGIFMTU
print("as-is : OK")
except Exception as e:
print("as-is :", type(e).__name__, e)
try:
res = fcntl.ioctl(sock.fileno(), 0x8921, base + b"\x00" * 22)
print("padded to 40 : OK, mtu =", struct.unpack("16sH", res[:18])[1])
except Exception as e:
print("padded to 40 :", type(e).__name__, e)Measured on the reporting machine:
| Interpreter | ioctl(18B) |
ioctl(40B) |
|---|---|---|
| 3.14.6 | SystemError: buffer overflow |
OK, mtu = 16 |
| 3.12.12 | OK, mtu = 16 |
— |
| 3.10.12 | OK, mtu = 16 |
— |
Re-measured on a second machine (3.10.12, enp2s0, MTU 1500): the 18-byte
argument returns 18 bytes with mtu = 1500, the 40-byte one returns 40 bytes with
the same mtu = 1500. The reply is exactly as long as the argument on every
version, and the mtu sits in the same place either way — the bug is invisible on
the interpreter the suite is developed on, which is the whole reason it shipped.
With only the padding change applied at runtime, connect() returns True on
3.14.6, the motor id auto-detects (mst_id = 0x18), and get_state() reads back
normally (position_mm = 86.0, error_code = 0), so this is the only 3.14
blocker in the connect path.
Suggested fix
_IFREQ_SIZE = 40 # sizeof(struct ifreq); 32 on 32-bit Linux
def _iface_mtu(sock: socket.socket, iface: str) -> Optional[int]:
try:
ifr = struct.pack("16sH", iface.encode("utf-8")[:15], 0)
ifr = ifr.ljust(_IFREQ_SIZE, b"\x00")
res = fcntl.ioctl(sock.fileno(), _SIOCGIFMTU, ifr)
return struct.unpack("16sH", res[:18])[1]
except OSError:
return NoneWorth a test that runs under 3.14 and fails without this, given the failure
cannot be seen from the interpreter the suite runs on today.
Environment
- SDK
5b7da892as vendored inlitegrip-studioat41888cf; still present at
351f80d - Python 3.14.6 (miniconda), Linux 6.8.1, x86-64
can0: gs_usb adapter,bitrate 1000000,state UP,ERROR-ACTIVE- Console:
litegrip-studio --backend real→ 连接 fails every time;--backend sim
is unaffected - Root-cause mechanism above verified by reading
Modules/fcntlmodule.cat
v3.14.0; the repro re-run on the second machine under 3.10.12
Activity
fix: size the SIOCGIFMTU buffer for a whole struct ifreq
fix: size the SIOCGIFMTU buffer for a whole struct ifreq (#64)
github-actions commentedon Oct 14, 2026
🎉 This issue has been resolved in version 1.0.2 🎉
The release is available on GitHub release
Your semantic-release bot 📦🚀
ci: run the studio suite on Python 3.14 (#38)