Skip to content

Size the SIOCGIFMTU buffer for a whole struct ifreq #63

Closed

Description

@cao-xiao-hao

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 past len is 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 None

Worth 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 5b7da892 as vendored in litegrip-studio at 41888cf; 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.c at
    v3.14.0; the repro re-run on the second machine under 3.10.12

Activity

  1. added
    bugSomething isn't working
    status: needs-triageNot looked at yet - a maintainer still has to agree on priority and kind
    on Oct 14, 2026
  2. added a commit that references this issue on Oct 14, 2026
    ad605f6
  3. added a commit that references this issue on Oct 14, 2026
    9180117
  4. github-actions commented on Oct 14, 2026

    @github-actions

    🎉 This issue has been resolved 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingreleasedShipped in a releasestatus: needs-triageNot looked at yet - a maintainer still has to agree on priority and kind

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions