Search code examples
pythoncioctl

Passing pointer to C struct to ioctl in Python


I am writing a Python script that needs to use the FS_IOC_ADD_ENCRYPTION_KEY ioctl.

This ioctl expects an argument of type (pointer to) fscrypt_add_key_arg, which has this definition in the Linux kernel header files:

struct fscrypt_add_key_arg {
    struct fscrypt_key_specifier key_spec;
    __u32 raw_size;
    __u32 key_id;
    __u32 __reserved[8];
    __u8 raw[];
};

#define FSCRYPT_KEY_SPEC_TYPE_DESCRIPTOR        1
#define FSCRYPT_KEY_SPEC_TYPE_IDENTIFIER        2
#define FSCRYPT_KEY_DESCRIPTOR_SIZE             8
#define FSCRYPT_KEY_IDENTIFIER_SIZE             16

struct fscrypt_key_specifier {
    __u32 type;     /* one of FSCRYPT_KEY_SPEC_TYPE_* */
    __u32 __reserved;
    union {
            __u8 __reserved[32]; /* reserve some extra space */
            __u8 descriptor[FSCRYPT_KEY_DESCRIPTOR_SIZE];
            __u8 identifier[FSCRYPT_KEY_IDENTIFIER_SIZE];
    } u;
};

This is my Python code:

import fcntl
import struct

FS_IOC_ADD_ENCRYPTION_KEY = 0xc0506617

policy_data.key_descriptor: str = get_key_descriptor()
policy_key: bytes = get_policy_key(policy_data)

fscrypt_key_specifier = struct.pack(
    'II16s',
    0x2,
    0,
    bytes.fromhex(policy_data.key_descriptor)
)

fscrypt_add_key_arg = struct.pack(
    f'{len(fscrypt_key_specifier)}sII8I{len(policy_key)}s',
    fscrypt_key_specifier,
    len(policy_key),
    0,
    0, 0, 0, 0, 0, 0, 0, 0,
    policy_key
)

fd = os.open('/mnt/external', os.O_RDONLY)
res = fcntl.ioctl(fd, FS_IOC_ADD_ENCRYPTION_KEY, fscrypt_add_key_arg)

print(res)

When I execute this code I get an OSError:

Traceback (most recent call last):
  File "/home/foo/fscryptdump/./main.py", line 101, in <module>
    res = fcntl.ioctl(fd, FS_IOC_ADD_ENCRYPTION_KEY, fscrypt_add_key_arg)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
OSError: [Errno 22] Invalid argument

I have double-checked the documentation of that ioctl and I think the values I am passing are correct, but there probably is a problem in the way they are packed.

How can I solve this?


Solution

  • Expanding my comment into an answer:

    Python struct module and C unions

    As far as I can determine, the struct module has no particular provision for C unions. Although I don't think that will interfere with your efforts in this particular case, it is a potential problem for using struct to pack or unpack structures that contain unions. Structure size and layout layout is affected by the size and alignment requirement of every member, including unions, so to use struct with structures containing unions, you need to manually account for not only the unions' own layouts, but also their effects on the containing structure's.

    unions are fixed-size data types large enough to accommodate their largest members. But they can have trailing padding, too, so a union can be strictly larger than its largest member. This is at the discretion of the C language implementation in use.

    Union size

    C unions are fixed-size data types, large enough to contain any one of their members. Thus, the one in ...

    struct fscrypt_key_specifier {
        __u32 type;     /* one of FSCRYPT_KEY_SPEC_TYPE_* */
        __u32 __reserved;
        union {
                __u8 __reserved[32]; /* reserve some extra space */
                __u8 descriptor[FSCRYPT_KEY_DESCRIPTOR_SIZE];
                __u8 identifier[FSCRYPT_KEY_IDENTIFIER_SIZE];
        } u;
    };
    

    ... is at least 32 bytes long, to be able to accommodate member __reserved. Therefore, as @TurePålsson observed first, format 'II16s' cannot be a correct format for packing a structure of this type. It will generate a representation that is at least 16 bytes too small, but possibly even more deficient if member u is in fact larger than 32 bytes.

    In practice, I anticipate from the code comment and on general programming principles, that neither FSCRYPT_KEY_DESCRIPTOR_SIZE nor FSCRYPT_KEY_IDENTIFIER_SIZE exceeds 32, the size of the __reserved member. I furthermore make an educated guess that a union containing three byte arrays, the largest containing 32 bytes, will not be laid out with any trailing padding, so that its size is exactly 32 bytes.

    Other considerations

    Standard vs. native data type sizes

    The kernel's __u32 and __u8 data types are explicit-size data types. The former does not necessarily match the host's native unsigned int type. This is exactly the situation that the struct module's "standard" data type sizes are meant to address, but the formats you are providing specify "native" data type sizes (by default). Probably this is a distinction without a difference, but it is conceivable that you, or someone who inherits your code, would run into a platform where the relevant native sizes do not match struct's standard sizes.

    To more properly match the C structure definition, then, you should use a format that specifies "standard" data sizes and native byte order. That is achieved by prefixing it with =. You also need to choose the format codes for the standard sizes that match the structure, but that does not require any change in this case.

    Union member size

    One way to approach writing the union is to use a format that expresses a member the full size of the union (32 bytes, probably). But you seem to have assumed that the size of the union member you are writing is 16 bytes. In that case, the format most precisely matching the structure, as you intend to write it, would use that size. In that case, however, you would need to account for the extra size of the union, which you should be able to do by expressing padding bytes in the format. This has at least the advantage that you can use the same format to unpack structures of this type, with the same union member used.

    Overall recommendation

    Use one of these formats for packing fscrypt_key_specifier:

    • '=II16s16x' (standard type sizes, native byte order, 16-byte union member, 16 bytes in the union after the end of the member). struct.pack() will zero-fill the padding. It will also truncate or pad the data as necessary to make it exactly 16 bytes. struct.unpack() will read 16 bytes for the member and provide a bytes exactly that long.

    • '=II32s' (standard type sizes, native byte order, 32-byte union member). struct.pack() will truncate or pad the data as necessary to make it exactly 32 bytes. struct.unpack() will read 32 bytes for the member and provide a bytes exactly that long.

    • Either of the above without the leading =. That will use native data type sizes (and byte order) for the first two members. Although either of these would probably work for you, they are not, strictly speaking, correctly matched to the target structure type.