5.5.1 The Fd Model

LCS uses a hybrid syscall/ioctl interface, the same shape KACS uses within PKM.

Syscalls create file descriptors: opening a key, creating a key, beginning a transaction. There are three, numbered 1100 to 1102 in the PKM range. Ioctls operate on a descriptor that already exists — eighteen of them, all under type byte 'R'. close() releases both kinds of fd through the ordinary fd lifecycle.

5.5.1.1 Key fds #

A key fd is an anonymous inode created with O_CLOEXEC, holding:

FieldDescription
Source and key GUIDThe identity of the opened key.
Granted access maskComputed once by AccessCheck at open. Immutable.
Resolved pathAfter symlink resolution and CurrentUser\ rewriting.
Ancestor chainThe GUID at each path component from the hive root down. Captured during the open walk, used for subtree watch dispatch (§5.6.3).
Watch stateArmed or not, filter, subtree flag, pending event queue.

Key fds behave like any other fd: close(), close-on-exec, poll/epoll, and passing over Unix sockets with SCM_RIGHTS. That last one is what makes them capabilities (§5.4.1).

5.5.1.2 Open-time checking #

A caller names a desired access mask, and AccessCheck evaluates it against the key's Security Descriptor. All of it is granted or the open fails with EACCES. MAXIMUM_ALLOWED is the only way to ask for whatever is available.

The granted mask lives on the fd, and every subsequent ioctl is a bitmask test against it — not a fresh AccessCheck, and not a fresh read of the descriptor.

Opening relative to a parent fd skips both path parsing and AccessCheck for the parent portion. The caller already proved its access when it obtained the parent fd, and this is the ordinary way to walk a subtree.

5.5.1.3 Transaction fds #

A transaction fd is an anonymous inode holding a transaction id and, once bound, its source and hive. Transaction lifetime is fd lifetime: closing without committing aborts (§5.7.1).

5.5.1.4 Reserved fields #

Every syscall and ioctl argument structure uses natural C layout with fixed-width fields and explicit padding. Nothing is packed.

Fields named _pad, and anything else described as reserved, are ABI extension points. A caller must set them to zero, and a non-zero reserved or padding field fails the operation with EINVAL — before source dispatch, before transaction enlistment, before sequence allocation, and before any output is copied.

In the other direction, LCS zeroes every reserved and padding byte of an output structure or a watch event before copying it to userspace.

Flags fields carry only the bits defined for them. An unknown or reserved flag bit is EINVAL unless a specific field says otherwise.

The point of all this is that a future version can give a reserved field meaning without an older kernel having silently accepted a value it did not understand.

5.5.1.5 Strings #

Strings in ioctl structures are length-delimited, not null-terminated. Each is a (len, ptr) pair where len is a byte count and ptr is a u64 userspace address. LCS reads exactly len bytes. A terminator is neither required nor expected, and one included in the length is a null byte and therefore invalid.

Syscall paths are the exception: they arrive as null-terminated C strings and have the terminator stripped before validation (§5.2.8).

5.5.1.6 Variable-size output buffers #

Six ioctls return variable-size data — REG_IOC_QUERY_VALUE, QUERY_VALUES_BATCH, ENUM_VALUES, ENUM_SUBKEYS, QUERY_KEY_INFO and GET_SECURITY — and all six use one convention.

For each output buffer described by (length, pointer):

  • length 0 is a size probe, whether the pointer is null or not. The pointer is not dereferenced.
  • length greater than 0 requires a non-null pointer writable for that many bytes, or the ioctl returns EFAULT.

If any output buffer is too small the ioctl returns ERANGE and writes every required size it can determine, not just the first one that failed — so a caller with two undersized buffers learns both sizes from one call.

On ERANGE, output buffers are not partially filled; their contents are unspecified. Output scalar metadata is meaningful only on success, unless an ioctl explicitly documents a field as carrying a required size or count on ERANGE.

Input pointer faults return EFAULT, validated before source dispatch wherever that is possible.

Edit this page