Re: [RFC PATCH 1/5] rseq: uapi: add rseq operation definitions

From: Dmitry Vyukov

Date: Sat Aug 29 2026 - 18:34:50 EST


On Fri, 28 Aug 2026 at 17:33, <odion@xxxxxxxxxxxx> wrote:
>
> From: Olivier Dion <odion@xxxxxxxxxxxx>
>
> Introduce userspace ABI for rseq operations: a per-thread list of
> operations the kernel applies on return to user space.
>
> The main motivation for this work is to encourage TCMalloc to migrate to
> RSEQ v2 [0] and use the glibc RSEQ region.
>
> Indeed, TCMalloc relies on the behavior of RSEQ v1, that reset the
> cpu_id bits in the RSEQ shared region, to invalidate a per-cpu pointer
> cached in a TLS. This hack requires TCMalloc users to use a glibc
> tunable to disable RSEQ registration for threads so that TCMalloc can
> register its own region, overlapping the TLS cache.
>
> Overall, this puts the users in a situation of choosing between a fast
> sched_getcpu() and TCMalloc, plus some downsides such as requiring a
> initial-exec model for the cache TLS in a shared-library and not being
> compatible with the glibc
>
> RSEQ operations aim at solving this by offering operations that are
> executed by the kernel, on behalf of a task after it is scheduled,
> before returning to userspace. Operations are per-task and registered
> throught prctl. They are opt-in and only introduce overhead on tasks
> that register them.
>
> Add enum rseq_op_type and the rseq_op_node / rseq_op_reset /
> rseq_op_reset_with_stride structures, the RSEQ_CS_FLAG_RSEQ_OP_*
> availability/enabled flags, the RSEQ_OP_LIST_LIMIT walk bound, and the
> PR_RSEQ_OP prctl with its REGISTER/UNREGISTER sub-commands.
>
> The operation list is a circular doubly-linked list anchored by a
> kernel-owned sentinel embedded in struct rseq.
>
> [0] Documentation/userspace-api/rseq.rst (Optimized RSEQ v2)

Hi Olivier,

This is very cool, thanks for working on this.

> Link: https://lore.kernel.org/lkml/20260428221058.149538293@xxxxxxxxxx
> Signed-off-by: Olivier Dion <odion@xxxxxxxxxxxx>
> ---
> include/uapi/linux/prctl.h | 12 +++++
> include/uapi/linux/rseq.h | 104 ++++++++++++++++++++++++++++++++++---
> 2 files changed, 108 insertions(+), 8 deletions(-)
>
> diff --git a/include/uapi/linux/prctl.h b/include/uapi/linux/prctl.h
> index b6ec6f693719..4cb6356a271a 100644
> --- a/include/uapi/linux/prctl.h
> +++ b/include/uapi/linux/prctl.h
> @@ -396,6 +396,18 @@ struct prctl_mm_map {
> */
> # define PR_RSEQ_SLICE_EXT_ENABLE 0x01
>
> +/*
> + * RSEQ operation registration.
> + *
> + * arg3 is the user address of a struct rseq_op_node embedded in one of the
> + * rseq operation structures (see uapi/linux/rseq.h). Registering the first
> + * operation enables rseq operation processing for the thread; unregistering
> + * the last one disables it.
> + */
> +#define PR_RSEQ_OP 82
> +# define PR_RSEQ_OP_REGISTER 1
> +# define PR_RSEQ_OP_UNREGISTER 2
> +
> /*
> * Get or set the control flow integrity (CFI) configuration for the
> * current thread.
> diff --git a/include/uapi/linux/rseq.h b/include/uapi/linux/rseq.h
> index ca6fe1f9d05e..b664d1991c48 100644
> --- a/include/uapi/linux/rseq.h
> +++ b/include/uapi/linux/rseq.h
> @@ -13,6 +13,11 @@
> #include <linux/types.h>
> #include <asm/byteorder.h>
>
> +/*
> + * Maximum number of nodes walked in the rseq operation list.
> + */
> +#define RSEQ_OP_LIST_LIMIT 2048

Does this belong to the user header? Kernel and userspace are not
necessary built with the same headers, and we don't necessary want to
set this const in stone. I think it may be more flexible to make this
impl detail and just return ENOSPC.

> enum rseq_cpu_id_state {
> RSEQ_CPU_ID_UNINITIALIZED = -1,
> RSEQ_CPU_ID_REGISTRATION_FAILED = -2,
> @@ -33,6 +38,8 @@ enum rseq_cs_flags_bit {
> /* User read only feature flags */
> RSEQ_CS_FLAG_SLICE_EXT_AVAILABLE_BIT = 4,
> RSEQ_CS_FLAG_SLICE_EXT_ENABLED_BIT = 5,
> + RSEQ_CS_FLAG_RSEQ_OP_AVAILABLE_BIT = 6,
> + RSEQ_CS_FLAG_RSEQ_OP_ENABLED_BIT = 7,
> };
>
> enum rseq_cs_flags {
> @@ -47,6 +54,10 @@ enum rseq_cs_flags {
> (1U << RSEQ_CS_FLAG_SLICE_EXT_AVAILABLE_BIT),
> RSEQ_CS_FLAG_SLICE_EXT_ENABLED =
> (1U << RSEQ_CS_FLAG_SLICE_EXT_ENABLED_BIT),
> + RSEQ_CS_FLAG_RSEQ_OP_AVAILABLE =
> + (1U << RSEQ_CS_FLAG_RSEQ_OP_AVAILABLE_BIT),
> + RSEQ_CS_FLAG_RSEQ_OP_ENABLED =
> + (1U << RSEQ_CS_FLAG_RSEQ_OP_ENABLED_BIT),
> };
>
> /*
> @@ -86,6 +97,77 @@ struct rseq_slice_ctrl {
> };
> };
>
> +/*
> + * enum rseq_op_type - Type of an rseq operation
> + * @RSEQ_OP_RESET: Plain reset. Uses struct rseq_op_reset.
> + * @RSEQ_OP_RESET_WITH_STRIDE_CPUID: Reset indexed by the current CPU ID.
> + * Uses struct rseq_op_reset_with_stride.
> + * @RSEQ_OP_RESET_WITH_STRIDE_MMCID: Reset indexed by the current MM CID.
> + * Uses struct rseq_op_reset_with_stride.

Do you have use-cases for these STRIDE ops?
I can think of some, but they would require executing ops when a task
is descheduled, rather than when it's scheduled in.

> + */
> +enum rseq_op_type {
> + RSEQ_OP_RESET,
> + RSEQ_OP_RESET_WITH_STRIDE_CPUID,
> + RSEQ_OP_RESET_WITH_STRIDE_MMCID,
> + RSEQ_OP_NR,
> +};
> +
> +/*
> + * struct rseq_op_node - Common header linking an rseq operation into the list
> + * @next: Address of the next node. Owned by the kernel.
> + * @prev: Address of the previous node. Owned by the kernel.
> + * @type: Operation type. See enum rseq_op_type.
> + * @reserved: Must be zero on registration.
> + *
> + * User space allocates the node, sets @type and zeroes @next, @prev and
> + * @reserved before passing it to prctl(PR_RSEQ_OP, PR_RSEQ_OP_REGISTER, node).
> + * The kernel owns @next and @prev for the lifetime of the registration and
> + * links the node into a circular doubly-linked list anchored by an internal
> + * sentinel in struct rseq. User space must not touch @next or @prev while the
> + * node is registered.

Have you considered keeping copies of these structs in the kernel?
That (1) is more consistent with most of the other kernel APIs, (2)
will make executing ops faster since we don't need to copy_from_user
all the time, (3) will make code much simpler since we won't need to
re-verify all data, (4) will make the code more secure since
user-space won't be able to mess with these structs, (5) will simplify
the contract and documentation.
There are robust lists that keep lists in user-space, but these are
supposed to be concurrently modified by userspace. But these rseq ops
are generally not expected to be added/removed often (and the current
contract does not allow that anyway).

> + */
> +struct rseq_op_node {
> + __u64 next;
> + __u64 prev;
> + struct {
> + __u8 type; /* enum rseq_op_type */
> + __u8 reserved[7];
> + };
> +};
> +
> +/*
> + * struct rseq_op_reset - Reset one word to a value on return to user space
> + * @node: Operation list node.
> + * @src: Address of the source word, or 0 to reset @dst to zero.

Is "or 0 to reset" part implemented in the series?

It would be useful to see an op that contains a const that is written,
rather than an address. Address invovles additional indirection +
copy_from_user.

And the contract for concurrent changes of the value pointed by src
from user-space are not documented (atomicity?), w/o that having the
address is not very useful.


> + * @dst: Address of the destination word.
> + * @len: Word length in bytes. Must be 4 or 8 (8 is 64-bit only).
> + */
> +struct rseq_op_reset {
> + struct rseq_op_node node;
> + __u64 src;
> + __u64 dst;
> + __u32 len;
> +};
> +
> +/*
> + * struct rseq_op_reset_with_stride - Reset one word in a strided array
> + * @node: Operation list node.
> + * @src: Address of the source word, or 0 to reset the slot to zero.
> + * @dst: Base address of the strided destination array.
> + * @dst_stride: Stride in bytes between consecutive array slots.
> + * @len: Word length in bytes. Must be 4 or 8 (8 is 64-bit only).
> + *
> + * The destination slot is @dst + @dst_stride * index, where index is the
> + * current CPU ID or MM CID depending on the operation type.
> + */
> +struct rseq_op_reset_with_stride {
> + struct rseq_op_node node;
> + __u64 src;
> + __u64 dst;
> + __u64 dst_stride;
> + __u32 len;
> +};
> +
> /*
> * The original size and alignment of the allocation for struct rseq is
> * 32 bytes.
> @@ -191,15 +273,21 @@ struct rseq {
> struct rseq_slice_ctrl slice_ctrl;
>
> /*
> - * Before rseq became extensible, its original size was 32 bytes even
> - * though the active rseq area was only 20 bytes.
> - * Exposing a 32 bytes feature size would make life needlessly painful
> - * for userspace. Therefore, add a reserved byte after byte 32
> - * to bump the rseq feature size from 32 to 33.
> - * The next field to be added to the rseq area will be larger
> - * than one byte, and will replace this reserved byte.
> + * Sentinel of the circular doubly-linked list of rseq operations
> + * registered via prctl(PR_RSEQ_OP, ...). Fully owned and maintained by
> + * the kernel: it is initialized to point to itself on registration and
> + * user space must never read or write it directly.
> + *
> + * The kernel only use next and prev from rseq_op_list. The rest of the
> + * bytes are reserved for later usage and should be zeroed.
> */

Do we really need the union and the op_used/reserved? Isn't it easier
to say that everything needs to be 0'ed? What would userspace do with
op_used? It's not possible to skip initialization of these fields when
using both memset, struct initialization syntax, and relying on 0 init
of global data.

> - __u8 __reserved;
> + union {
> + struct rseq_op_node rseq_op_list;
> + struct {
> + __u64 op_used[2];
> + __u64 reserved;
> + };
> + };
>
> /*
> * Flexible array member at end of structure, after last feature field.
> --
> 2.54.0
>