Embedded Linux ioctl Compatibility: Keep 32-Bit Applications Working on a 64-Bit Kernel

Keep the request number, payload layout and compat dispatch path consistent, then send the same semantic test vectors from native and 32-bit applications. Fixed-width fields help, but they do not by themselves prove that a private driver ioctl works across different application ABIs.

This guide uses upstream Linux v6.12 ioctl rules. A compile-only x86 fixture was checked with GCC 14.2.0 in both -m32 and -m64 modes. No driver command was issued, no 32-bit program was executed and no embedded board was tested. Other architectures and vendor kernels need their own compiled layout and runtime checks.

Inventory the shipped contract before changing it

For each command, record its numeric value, direction, payload size, every field’s width and offset, and whether the outer argument is a pointer or scalar. Include old application binaries and vendor utilities, not just the latest source header. The ABI belongs to already shipped consumers as well as to the current driver.

The v6.12 ioctl guidance recommends the standard request macros and explains that their data-type argument encodes the type’s size. Passing sizeof(type) instead of the type encodes the size of size_t, which is a different quantity. Compare the final numeric command in both builds rather than checking that the macro name looks identical.

Do not silently change an existing payload and retain its command number. Where the contract must evolve, add a deliberately assigned new command and preserve the old interpretation, possibly through an explicit translation wrapper. A version field that makes one command mean incompatible layouts complicates old-client support.

Remove width and alignment ambiguities

Native long, unsigned long and pointers can change width between 32-bit and 64-bit applications. Even fixed-width 64-bit integers can have different natural alignment: the kernel document specifically contrasts i386 with x86-64. Compare offsets and tail padding, not only the sum of member widths.

The v6.12 UAPI type definitions provide __aligned_u64 for consistent eight-byte alignment. A hypothetical new payload can use two such fields followed by two 32-bit fields:

struct sample_request {
    __aligned_u64 cookie;
    __aligned_u64 user_address;
    __u32 length;
    __u32 reserved;
};

This is a layout illustration, not an allocated public ioctl number or complete driver. Its expected offsets are zero, eight, sixteen and twenty bytes, with a total of twenty-four. Define reserved-field handling and supported lengths as part of the actual interface. Avoid implementation-defined bitfields and enums in the external layout.

An integer representation of a userspace address is still an untrusted address. Userspace converts through uintptr_t when forming it; the kernel uses the appropriate user-pointer conversion and access routines. Validate lengths and ownership independently. Matching structure layouts does not validate the pointed-to memory or authorize a device operation.

Three ioctl compatibility gates cover request identity, a fixed 24-byte payload layout and compat dispatch, with a separate limitation on compile-only evidence.
Original ioctl compatibility model; offsets belong to the illustrative payload and are not a measured device interface.

Choose compat dispatch by argument meaning

A driver or subsystem supporting a 32-bit application on a 64-bit kernel needs the corresponding compat handling. When every argument is a pointer to a compatible structure, compat_ptr_ioctl can be appropriate. It is not a universal replacement for reviewing each command’s argument contract.

The compat callback receives an unsigned-long argument whose meaning depends on the command. For a scalar argument, applying compat_ptr is incorrect. This distinction is particularly visible on s390’s 31-bit userspace pointer representation, where pointer conversion clears a bit that a scalar may legitimately contain.

Legacy incompatible structures need explicit translation between their layouts and an internal representation. Copy user input once into a bounded kernel-owned object, validate it and translate fields deliberately. Keep conversion code separate from the device operation so native and compat requests can share checked internal behavior.

For backports, note the version boundary: before Linux 5.5, compat fallback conventions used ENOIOCTLCMD differently. This article’s v6.12 procedure must not be applied mechanically to an older vendor branch without reading its dispatch path.

Make compile-time layout checks reproducible

The supplied fixture uses compiler-provided integer types, explicit eight-byte alignment and the generic ioctl macros. Both x86 compile modes passed static assertions for the illustrated twenty-four-byte payload and its offsets. The intentionally native-width comparison structure was twenty-four bytes in the 64-bit mode and twelve in the 32-bit mode.

Those results establish only the fixture’s compiler layouts. They do not prove that target headers, toolchain options or a real driver use the same definitions. Repeat equivalent assertions against the actual exported UAPI header with each supported target compiler and archive the command, compiler identity and object inspection.

Check both standalone structures and nesting within larger payloads. A structure can appear compatible alone while a containing structure introduces alignment differences. Review packing options and generated language bindings as additional ABI participants; adding a packed attribute is not a universal portability repair.

Run identical semantic vectors through both clients

Vector Required comparison
Ordinary supported request Same command meaning, response fields and documented result
High bits in a 64-bit cookie No narrowing through a native-width intermediate
Zero and maximum permitted lengths Same validation and bounded access behavior
Reserved or unsupported values Consistent rejection without device side effects
Invalid user address Controlled failure through the proper user-access path
Unknown command Documented unsupported-command behavior

Use a recoverable laboratory device or a reviewed non-hardware fixture. Do not probe arbitrary command numbers against production hardware. Compare responses field by field and check that returned reserved bytes are initialized. A kernel copy that leaks uninitialized padding is a defect even if both applications appear to work.

For pointer-bearing requests, use buffers valid in each process and compare content rather than expecting identical address values. Exercise the actual 32-bit executable on the supported 64-bit kernel; compiling an object with -m32 cannot substitute for compat dispatch execution.

Close the compatibility decision

If command numbers differ, fix the definition or provide an explicit legacy dispatch mapping. If layouts differ, translate or redesign a new command. If only one runtime path fails, inspect compat dispatch and pointer/scalar treatment before changing device logic.

Release with the shared UAPI header, layout assertions, native/compat request vectors and exact supported kernel/toolchain matrix. Preserve the old-client test even after all new applications become 64-bit. Compatibility is established by the maintained external contract and its observed behavior, not by matching C member names.

Similar Posts