Skip to content

plist: Rust implementation of lib/plist.c - #32

Open
bherrera wants to merge 2 commits into
claude/sonnet-5/feature/sortfrom
claude/sonnet-5/feature/plist
Open

bherrera wants to merge 2 commits into
claude/sonnet-5/feature/sortfrom
claude/sonnet-5/feature/plist

Conversation

@bherrera

@bherrera bherrera commented Sep 21, 2026

Copy link
Copy Markdown

plist: Rust implementation of lib/plist.c

Stacked on #31 (hlist); retarget to linux-rust when its parent lands. Companion harness PR: misttech/linux-rust#53.

Replaces lib/plist.c in place: plist_add(), plist_del() and plist_requeue() with the same names and signatures. lib/plist.c has no EXPORT_SYMBOL, so lib/plist_ffi.c has no export lines, and vmlinux.symvers of a CONFIG_MODULES=y build is identical for both (sorted, 6,315 lines). include/ does not change. plist.o stays in lib-y.

Commits:

  • rust: check-cfg CONFIG_DEBUG_PLIST for the ports
  • lib: plist: add the Rust implementation

The three <linux/list.h> operations that change a list stay in C behind single-call wrappers, so CONFIG_LIST_HARDENED and CONFIG_DEBUG_LIST keep their checks; reads are Relaxed loads. CONFIG_DEBUG_PLIST is kept, including the boot-time plist_test(), which is in Rust with its module_init() in the ffi file.

A bug in plist_requeue() in the C, kept by the port

The differential test found that the C reference alone fails the sorted-order invariant. After plist_del(), the shortcut follows iter->prio_list.next, which wraps to the first node of the head when the node's group is the last, and the node is inserted in front of higher priorities. Priorities 2, 3, 3, requeueing the first 3, gives 3, 2, 3, and the plist is then corrupt (plist_first() is not a leader).

  • It takes the first node of the last group, a group of two or more, and two or more groups: 155 of 1,215 small states (five nodes, three priorities, every node requeued).
  • An independent review reproduced it with a standalone C program built from the verbatim functions, and confirmed plist_test() cannot reach it: it requeues only the node it has just added, which is last in its group (0 hits over 20,000 seeds).
  • mm/swapfile.c is the one caller. swap_alloc_slow() requeues every device it walks, and default swap priorities are equal, so a higher-priority device plus two or more default ones can hit it. That is from reading the code and reproducing the operations; it has not been triggered on a kernel.
  • The port does not fix it: a port is a faithful replacement, and a change of behaviour is proposed against lib/plist.c first. The harness skips that requeue in its runs, and compares it in misuse (C, Rust and mixed must agree on the wrong order and memory); a mutant that fixes it fails.

Decisions for you

  • Licence. lib/plist.rs and lib/plist_ffi.c are GPL-2.0-or-later, as plist.c is: the Rust reproduces its header, authors and comments, and narrowing an "or later" grant needs the authors' consent. CLAUDE.md says all code is GPL-2.0-only. Proposed edit to CLAUDE.md, not made here: "GPL-2.0-only, or the licence of the C file it replaces". The other ports say GPL-2.0 where their C says GPL-2.0-only; that is a separate sweep.
  • No typed API. The intrusive-container skill puts plist after a typed list_head, which does not exist. 43 unsafe blocks in 710 lines is the cost of a raw-pointer port: every read is a dereference. The typed layer waits for list_head.

Not done

Benchmarks (plist_add() is on the futex wait path), arm64, anything at 32 bits, the hardened list checks in the harness, and the warning path of CONFIG_DEBUG_PLIST (plist_check_head() runs on every operation of the boot test and finds nothing; nothing corrupts a list to make it warn). A WARN_ON or BUG() reports lib/plist_ffi.c, not lib/plist.c.

Review

An independent review-rust-port said CHANGES REQUIRED for the first version, on four points, all fixed: an extern function declared -> ! made objtool report plist_requeue() falls through to next function (now c_plist_bug_on(bool), as kfifo does); plist.o had moved from lib-y to obj-y; four FFI calls sat in blocks tagged U3 (each is now in a block of its own, U1); and a clippy warning with CONFIG_DEBUG_PLIST=y. Its semantic comparison found no difference from plist.c.

Kill criteria

  1. Layout: not hit. The three structures are the same in every configuration: BTF for 64 bits, clang for i386, every field asserted.
  2. loom: not applicable; no shared state, a caller's lock.
  3. unsafe at call sites: not applicable; no typed API. unsafe budget: U1 13, U3 30.

Verification

Kernel CONFIG_RUST_KERNEL=n and =y build, with clippy and without objtool warnings, also with CONFIG_DEBUG_PLIST=y; a C and a Rust kernel with CONFIG_DEBUG_PLIST=y boot and run plist_test() without a BUG. The harness (linux-rust) runs the C reference, the Rust and a per-operation mix on the same memory (1,889,568 edge sequences and 10 seeds x 100,000 operations), the misuse cases, layout at 64 and 32 bits, and 26 mutants (25 caught, the survivor changes only an ordering on a load).

Assisted-by: LLM [Claude Code]

🤖 Generated with Claude Code

bherrera and others added 2 commits September 21, 2026 16:16
The main.rs crate that holds the ports is built with the kernel's
rustc_cfg and checks the cfgs it uses. A port of lib/plist.c reads
CONFIG_DEBUG_PLIST, which changes plist_check_head() and adds a boot-
time test, so every use is an unexpected_cfgs warning until the cfg is
declared.

Add it next to the others. A cfg that is declared and never tested is
not a warning, so nothing changes until the port lands.

Assisted-by: LLM [Claude Code]
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Port the three functions lib/plist.c defines: plist_add(), plist_del()
and plist_requeue(). lib/plist.rs implements them with the declarations
of include/linux/plist.h, and CONFIG_RUST_KERNEL builds it instead of
lib/plist.c. The header and its inlines do not change. lib/plist.c has
no EXPORT_SYMBOL, so lib/plist_ffi.c has no export lines, and
vmlinux.symvers of a CONFIG_MODULES=y build is the same for both,
sorted. plist.o stays in lib-y. The licence is the C file's, GPL-2.0-or-
later.

plist_head and plist_node sit on struct list_head. The port mirrors the
three, whose layout is the same in every configuration, with assertions
for 64 bits (BTF) and 32 bits (DWARF of clang for i386). The three
operations of <linux/list.h> that change a list, list_add(),
list_add_tail() and list_del_init(), stay in C behind single-call
wrappers, so CONFIG_LIST_HARDENED and CONFIG_DEBUG_LIST keep their
checks. The reads are Relaxed loads of atomic fields, which is what a
plain C read of a list that a lock protects is, so plist_add() and
plist_del() make one call into C for each list change and none for a
read. BUG_ON() is a wrapper that takes the condition, since objtool
cannot know that an extern function does not return.

CONFIG_DEBUG_PLIST is kept, as a cfg: the list walk of
plist_check_head() and the boot-time plist_test(), which is in Rust with
its module_init() in the ffi file. The boot test differs from the C in
that its 241 nodes are not __initdata, and a WARN or BUG reports the
wrapper's file and line.

The differential test in linux-rust found a bug in plist_requeue(),
which the port keeps. After plist_del() it follows the prio_list to the
next priority's first node, but when the node's group is the last, the
prio_list wraps to the first node of the head, and the node is inserted
in front of higher priorities: priorities 2, 3, 3, requeueing the first
3, gives 3, 2, 3. It takes the first node of the last group, a group of
two or more, and two or more groups. mm/swapfile.c is the one caller.
plist_test() does not reach it, since it requeues only the node it has
just added. A fix is a change of behaviour to propose against
lib/plist.c, not part of a port.

Both CONFIG_RUST_KERNEL=n and =y build, the =y build with CLIPPY=1
without a warning in lib/plist.rs and without an objtool warning, and a
kernel of each with CONFIG_DEBUG_PLIST=y boots and runs plist_test()
without a BUG. No typed API is added: that waits for one over list_head.
unsafe budget: U1 13, U3 30.

Assisted-by: LLM [Claude Code]
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@bherrera
bherrera force-pushed the claude/sonnet-5/feature/plist branch from dee0a2c to 99fe08f Compare September 21, 2026 19:17
@bherrera
bherrera changed the base branch from claude/sonnet-5/feature/hlist to claude/sonnet-5/feature/sort September 21, 2026 19:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant