Skip to content

ctypes: packed structures passed or returned by value are corrupted on x86-64 #158411

Description

@matthiasgoergens

A structure whose _pack_ leaves a field under-aligned can be passed to foreign functions incorrectly on x86-64 System V platforms. The callee sees wrong field values, and the arguments after the structure are shifted, which crashes when one of them is a pointer:

// foo.c:  cc -shared -fPIC -o foo.so foo.c
#include <stdint.h>
#include <string.h>
#pragma pack(2)
typedef struct __attribute__((ms_struct)) { uint16_t a; uint32_t b; } Foo;
#pragma pack()
void foo_dump(Foo foo, unsigned char *out) { memcpy(out, &foo, sizeof foo); }

(ms_struct matches _layout_ = "ms", which 3.14 requires for _pack_ on non-Windows; for these fields it does not change the layout.)

import ctypes

class Foo(ctypes.Structure):
    _layout_ = "ms"
    _pack_ = 2
    _fields_ = [("a", ctypes.c_uint16), ("b", ctypes.c_uint32)]

lib = ctypes.CDLL("./foo.so")
lib.foo_dump.argtypes = [Foo, ctypes.c_void_p]
buf = (ctypes.c_ubyte * ctypes.sizeof(Foo))()
lib.foo_dump(Foo(), buf)   # segmentation fault

b sits at offset 2, so under the x86-64 System V ABI the structure has an unaligned field and is passed in memory, and out goes in %rdi. ctypes describes the structure to libffi by its size, alignment and field types, but not the field offsets, and libffi's classification disagrees with the compiler's: in gdb, libffi has passed the structure's bytes in %rdi (all zero here) and out in %rsi, and the callee writes through %rdi. With non-zero field values that is a write to an arbitrary address. Returning such a structure by value goes wrong too, through the corresponding mismatch in the return convention (a structure returned in memory is written through a hidden pointer passed in %rdi).

To see how far this goes, I checked every two- and three-field structure made of uint8_t, uint16_t, uint32_t, uint64_t, float and double, with no _pack_ and with _pack_ 1, 2, 4 and 8, both as an argument (the C side returns a hash of the field bytes it received) and as a return value, each case in its own process. A case counts as wrong if the field bytes differ; most of these do not crash:

platform C compiler libffi cases wrong
x86-64 Linux, CPython 3.14.7 clang 22.1.8 3.8.0 2520 528
x86-64 Linux, CPython 3.14.7 GCC 16.2.1 3.8.0 2520 528
arm64 macOS 27.2, CPython 3.14.7 Apple clang 1700.6.3.2 system 2520 0

On x86-64 all 528 wrong cases, and none of the others, are structures of at most 16 bytes with a field under-aligned by _pack_, with both compilers and in both directions. The sweep did not include nested structures, arrays, _align_ or other field types. All the arm64 cases were correct. The crash also reproduces on 3.13 and on main. I have not tested Windows.

Armin Rigo noticed this in 2012, in a comment on gh-60780 ("The same misbehavior occurs if the structures are packed"), but that issue is about bit fields, and nothing since has dealt with packing. The checks added in gh-60780 and gh-60779 (bpo-16576, bpo-16575) covered bit fields and unions only; the union check was disabled after it rejected software that worked, and the bit-field check with it as a precaution. The documentation warns against passing unions and structures with bit fields by value, but does not mention _pack_, and nothing stops the call.

gh-66469 fixed a similar mismatch for arrays inside structures by describing them to libffi more precisely. I don't see the same fix here: libffi's structure description has no field offsets, and describing the under-aligned field as bytes gives the right offsets but still lets libffi pass the structure in registers, where the ABI requires memory (declaring b as c_uint8 * 4 crashes the same way). So at the least the documentation should mention _pack_ next to the existing warning. ctypes might also raise TypeError for these structures on x86-64 System V, since it knows their layout; the sweep suggests a condition (at most 16 bytes, a field under-aligned), but it would need wider coverage (nested structures, arrays, _align_) before a check could be trusted not to reject calls that work. The reproducer, the sweep and its raw results are on a branch of my fork.

Activity

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

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions