Skip to content

fix(procfs): align seq read semantics, re-parent every thread, resolve /proc/<nr> by tid - #2285

Open
fslongjin wants to merge 4 commits into
DragonOS-Community:masterfrom
fslongjin:codex/procfs-task-semantics-2283
Open

fslongjin wants to merge 4 commits into
DragonOS-Community:masterfrom
fslongjin:codex/procfs-task-semantics-2283

Conversation

@fslongjin

@fslongjin fslongjin commented Sep 15, 2026

Copy link
Copy Markdown
Member

Fixes #2283.

What was wrong

Three behaviours of /proc disagreed with Linux 6.6:

  1. Every read_at() re-rendered the record and then sliced it by file position, so a second read() could return the tail of a newer, longer render instead of hitting EOF. Linux seq_read_iter() keeps one buffer per fd and only re-enters the handler when that buffer runs dry.
  2. reparent only rewrote the thread group leader, so /proc/<pid>/task/<tid>/status and /proc/<pid>/status reported different Ppid for the same thread group.
  3. /proc/<nr> was resolved through the TGID chain only, so a non-leader tid had no directory, unlike proc_pid_lookup().

Fixes

1. One seq driver for the whole procfs read path (procfs/utils.rs)

ProcfsSeq (rendered bytes + resume cursor + "record ended" flag, mirroring struct seq_file) plus proc_read_seq(), which reproduces seq_read_iter()/seq_lseek():

  • offset == 0 rewinds (Linux resets m->index/m->count on every ki_pos == 0 read);
  • offset == fd position continues from the buffer, without re-entering the record source — this is what makes EOF stay EOF while content grows;
  • any other offset re-renders and skips, and a seek past the end parks the fd so the next read reports EOF instead of rendering again;
  • errors follow seq_read_iter(): bytes already copied out are returned, and only a slice that produced nothing reports the error.

proc_read_snapshot() is a 12-line wrapper over the same driver for single_open()-style files, so there is one implementation of the semantics, not two. Files that Linux serves with a plain ->read (oom_score_adj, /proc/<pid>/cmdline, sys/*) keep streaming; the driver refuses a read that arrives without the state ProcFile::open() installed instead of silently falling back to the per-read rendering this PR removes.

2. /proc/<pid>/maps renders one slice at a time (procfs/pid/maps.rs)

The first version of this PR froze the whole mapping table per fd. A PR reviewer pointed out that this is an unprivileged memory DoS: a process can create many VMAs, open many /proc/self/maps fds (default RLIMIT_NOFILE is 1048576) and read one byte from each, so kernel heap grows as fds x mappings. Linux freezes one buffer, not the record (m->size = PAGE_SIZE, and the fill loop stops as soon as it has at least what the reader asked for).

So maps now streams: render_maps_slice() walks the mapping table from a cursor that is looked up the way find_vma() does (find_nearest(): the mapping covering the address, else the first one above it, which is what vma_iter_init() + vma_next() return) and stops at the slice budget; the fd holds at most one page plus the one line that crossed the bound. A mapping added after the first read shows up later in the stream, and open() pins the address space under exec_update_read() so a continuation read never re-resolves the target. The descriptor is pinned, not the memory: like proc_mem_open() (mmgrab() then mmput(), "but do not pin its memory"), each slice re-checks the user count through try_acquire(), so execve()/exit still tears the mappings down and the stream ends there instead of walking a dead table.

3. /proc/<pid>/{mounts,mountinfo,mountstats} pin their view at open (procfs/mount/view.rs)

The reviewer's second point: the task reference was dropped and the target re-resolved on the first read, so a setns(), unshare() or chroot() after open() changed what the fd reported. MountView::capture() now takes mnt_ns + root in open() and stores them in the file's private data, exactly like mounts_open_common() stores p->ns/p->root; read_at() renders from that view only.

4. Thread-group re-parent and /proc/<nr> lookup

reparent_one_task_locked() now walks the whole thread group the way forget_original_parent() does, and /proc/<nr> is resolved through the PID chain (proc_pid_lookup()) while directory listing keeps using the TGID chain (next_tgid()), with non-leader entries filtered out of the per-directory cache so listing cannot leak directories that lookup created.

Review follow-up (dc0164bff)

The third review round found four places where a node reported another task's state, or buffered more than it serves:

a. /proc/net/arp streams per slice and pins its namespace (procfs/net/arp.rs, net/neighbor/mod.rs)

It went through proc_read_snapshot(), so every open fd held a copy of the whole neighbour table and N descriptors multiplied an unbounded table by N. render_arp_slice() now renders through proc_read_seq() with the same one-page budget maps uses, and emits the header only on the first slice. The old code also re-sampled ProcessManager::current_netns() on every read, so a setns() between two reads could join the header of one namespace to the entries of another; open() now pins the namespace in the fd's private data, which is what seq_open_net() does with get_proc_net(inode). neighbor::get_arp_entries() takes that namespace from its caller instead of resolving the current one itself.

b. Hidden tids resolve their own task (pid/mod.rs, pid/fd.rs, pid/fdinfo.rs, pid/cgroup.rs, mount/inode/pid_mount.rs)

/proc/<tid>/fd, /proc/<tid>/fdinfo, the mounts family and /proc/<tid>/cgroup resolved thread_group_leader(). A thread that took a private files table (close_range(CLOSE_RANGE_UNSHARE)) or a private fs_struct (unshare(CLONE_FS), which CLONE_NEWNS implies) was therefore reported through the leader. They now use ProcPidTarget::task(), i.e. Linux get_proc_task(inode) through PIDTYPE_PID: the leader for /proc/<tgid>, the named thread for /proc/<tid>. The existence gate for fd/fdinfo moved to the same lookup, so it can no longer fall back to an empty directory for a thread that is still alive.

c. /proc/<nr>/stat takes CPU time from the same view as its fault counters (pid/stat.rs)

Fields 14/15 came from pcb.cputime() (the named thread, and for /proc/<tgid> the leader alone) while 10/12 aggregated the thread group. Both now come from the one usage view the node's StatScope selects, mirroring do_task_stat() where whole picks thread_group_cputime_adjusted() together with the group counters. A hidden tid keeps the group view, because /proc/<nr>/stat is proc_tgid_stat() (whole = 1) whatever nr names; /proc/<pid>/task/<tid>/stat keeps the per-thread one.

d. The mounts open path reports EINVAL for a task that is gone, as mounts_open_common() does (ret = -EINVAL); it reported ESRCH before.

Known bounds (deliberate, not hidden)

  • The mount family still snapshots its record per fd. Making it incremental needs a resumable cursor over the mount list; collecting the list once per slice would be O(n^2), so the per-fd footprint stays bounded by the mount-namespace size instead of the mount count times open fds being bounded by one page.
  • /proc/<pid>/maps still has no ptrace gate (proc_mem_open() uses PTRACE_MODE_READ on Linux). Pre-existing, tracked separately.
  • The init user namespace's own /proc/<pid>/{uid,gid}_map read deadlocks on a SpinLock; also pre-existing and unchanged by this PR.
  • DragonOS has no /proc/<pid>/net, so /proc/net/arp pins the net namespace of the task that opened it. Linux's /proc/net is a symlink to self/net, which resolves through the group leader; the two differ only for a thread that took a private net namespace, and closing that gap means adding /proc/<pid>/net rather than changing this file.

Build follow-up (0f94b8e70)

Build riscv64 and Build loongarch64 failed on dc0164bff with

error[E0275]: overflow evaluating the requirement
`alloc::sync::Weak<filesystem::page_cache::PageCache>: Sync`

at kernel/src/filesystem/page_cache/writeback.rs:4393, while x86_64 still passed. That file is untouched by this branch. The kernel's object graph is one strongly-connected component, so proving Send/Sync for a type such as File walks an obligation chain of roughly 130 levels through it, and master already resolves at a depth of 121-128 on riscv64, so rustc's default budget of 128 was nearly exhausted. Adding only the ProcfsFilePrivateData.net_ns field from part (a) on top of the previous commit reproduces that exact diagnostic, which is what this branch's new edge tipped over.

The budget is load-bearing rather than cosmetic: the generated code measurably differs between budgets (the kernel crate's .text grows by ~2.6% from 128 to 256), and 129 - just enough to stop reporting the error - still produces the same object code as the default 128. The fix therefore sets #![recursion_limit = "256"] in kernel/src/lib.rs, the value rustc suggests for this diagnostic and about twice the measured depth, rather than dropping an edge from the type graph or giving up the per-file namespace pin that seq_open_net() requires.

Verified with the CI flags: riscv64 and loongarch64 cargo build --release and x86_64 make kernel all succeed, and FMT_CHECK=1 make fmt is clean. Because x86_64 code generation is affected, the x86_64 kernel's behaviour is covered by the existing Dunitest and Integration Test jobs rather than argued from the attribute being "build time only".

Tests

normal/procfs_task_semantics (22 cases) plus 11 existing procfs suites, run in the guest (QEMU/KVM, same rootfs):

  • procfs_task_semantics 22/22 PASSED, including the new MapsStreamsMappingsAddedAfterFirstRead, MapsLargeReadReassemblesTheSameRecord, MountInfoKeepsTheRootPinnedAtOpen, MapsStreamKeepsCopiedBytesWhenTargetDies, MapsStreamKeepsTheAddressSpaceOpenedOn, SeekPastEndStaysEofAndRewindReRenders, TidFdSubtreeUsesTheThreadsFilesTable, TidMountsUsesTheThreadsRoot, TidStatReportsThreadGroupUsage and ArpChunkedReadReassemblesTheSameRecord;
  • the thread-addressed cases use real private state: close_range(CLOSE_RANGE_UNSHARE) for a private files table, unshare(CLONE_FS) plus chroot() for a private root, and a probe thread that burns user time so the per-thread and thread-group stat views cannot coincide;
  • the other 11 procfs suites are unchanged from the baseline, proc_thread_accounting_test still fails for an environment reason (the probe rootfs has no /sys/fs/cgroup/cgroup.subtree_control), on the baseline as well;
  • negative controls, measured in the same guest: restoring the whole-table maps render and the read-time mount resolution makes 5 of the round-2 cases fail, and reverting the three round-3 fixes (the leader lookups in fd/fdinfo/mounts and pcb.cputime() for fields 14/15) fails exactly TidFdSubtreeUsesTheThreadsFilesTable, TidMountsUsesTheThreadsRoot and TidStatReportsThreadGroupUsage, with 19 of 22 passing. The new cases are therefore falsifiable rather than tautological.

make fmt and make -C kernel fmt FMT_CHECK=--check pass; make kernel passes with no new warnings.

Signed-off-by: longjin longjin@dragonos.org

… /proc/<nr> by tid

/proc exposed three behaviours that disagree with Linux 6.6.

1. Every read_at() re-rendered the record and then sliced it with the file
   position, so a second read() could hand back the tail of a longer render
   instead of staying at EOF. Serve the seq-style files the way
   seq_read_iter() does: render once per fd, replay that snapshot, and
   re-render only on rewind or when the position no longer matches the
   continuation point. A failed render drops the continuation point, as a
   failed traverse() resets the buffer. Files Linux serves through a plain
   ->read (oom_score_adj, /proc/<pid>/cmdline, sys/*) keep streaming on
   purpose.

2. Re-parenting rewrote only the group leader, so
   /proc/<pid>/task/<tid>/status reported a different Ppid than
   /proc/<pid>/status for the same thread group. Walk the whole group the
   way forget_original_parent() does.

3. /proc/<nr> resolved through the TGID link only, so a non-leader tid had no
   directory. Resolve lookups through the PID link (proc_pid_lookup()) while
   directory listing keeps using the TGID link (next_tgid()), and drop
   non-leader entries from the per-directory cache so the listing cannot leak
   a tid that lookup created.

Add normal/procfs_task_semantics to the dunitest whitelist. Five of its
twelve cases fail on the pre-fix kernel; the rest guard the directions that
must not change.

Signed-off-by: longjin <longjin@dragonos.org>
@fslongjin

Copy link
Copy Markdown
Member Author

@codex review

@github-actions github-actions Bot added the Bug fix A bug is fixed in this pull request label Sep 15, 2026
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 15, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-16T04:15:50.325632Z 0f94b8e Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 580e98b961

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread kernel/src/filesystem/procfs/pid/maps.rs Outdated
Comment thread kernel/src/filesystem/procfs/mount/inode/pid_mount.rs Outdated
…er fd

The first version of this fix froze the whole record of every seq-style
procfs file per fd. Two review comments on this PR showed where that is
too coarse:

- /proc/<pid>/maps buffered a copy of the whole mapping table in every
  open fd, so a process that creates many VMAs can multiply that buffer
  by RLIMIT_NOFILE (1048576 by default). Linux freezes one buffer, not
  the record, so maps now renders one slice at a time from a cursor that
  is looked up the way find_vma() does (find_nearest(): the mapping
  covering the address, else the first one above it, as vma_iter_init()
  plus vma_next() return), and the fd holds at most one page plus the
  single line that crossed the bound.
- /proc/<pid>/{mounts,mountinfo,mountstats} resolved the target again on
  the first read, so a setns(), unshare() or chroot() after open()
  changed what the fd reported. MountView::capture() now pins mnt_ns and
  the root at open time, the way mounts_open_common() stores p->ns and
  p->root, and read_at() renders from that view only.

The shared driver was aligned with seq_read_iter() in two more places: a
read at offset 0 always rewinds, because Linux resets m->index/m->count
on every ki_pos == 0 read, and a read that arrives without the state
ProcFile::open() installed is refused with EINVAL instead of silently
falling back to the per-read rendering this fix removes.

Tests: normal/procfs_task_semantics grows to 18 cases, including
MapsStreamsMappingsAddedAfterFirstRead, MapsLargeReadReassemblesTheSameRecord,
MountInfoKeepsTheRootPinnedAtOpen, MapsStreamKeepsCopiedBytesWhenTargetDies
and MapsStreamKeepsTheAddressSpaceOpenedOn. Restoring the whole-table maps
render, or the read-time mount resolution, makes 5 of the 18 fail, so they
are falsifiable rather than tautological. Guest run (QEMU/KVM): 18/18, and
the 11 existing procfs suites are unchanged from the baseline
(proc_thread_accounting_test still fails for a missing /sys/fs/cgroup in
the probe rootfs, on the baseline as well).

Signed-off-by: longjin <longjin@dragonos.org>
@fslongjin fslongjin changed the title fix(procfs): freeze per-fd snapshots, re-parent every thread, resolve /proc/<nr> by tid fix(procfs): align seq read semantics, re-parent every thread, resolve /proc/<nr> by tid Sep 15, 2026
@fslongjin

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 906f6fc400

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread kernel/src/filesystem/procfs/net/arp.rs Outdated
Comment thread kernel/src/filesystem/procfs/root.rs
Comment thread kernel/src/filesystem/procfs/mount/inode/pid_mount.rs Outdated
Comment thread kernel/src/filesystem/procfs/root.rs
… slice

Review of DragonOS-Community#2285 found four places where a procfs node reported another task's
state, or buffered more than the file it serves has to:

- /proc/net/arp went through proc_read_snapshot(), so every open fd held a copy
  of the whole neighbour table: N descriptors multiplied an unbounded table by
  N. It re-sampled the caller's network namespace on every read as well. The
  file now renders one seq slice at a time through proc_read_seq() and pins the
  namespace at open(), the way seq_open_net() stores it in the seq private data.
- /proc/<tid>/fd, /proc/<tid>/fdinfo, the mounts family and /proc/<tid>/cgroup
  resolved the thread group leader rather than the task the node names, so a
  thread that took a private files table (close_range(CLOSE_RANGE_UNSHARE)) or
  its own fs_struct (unshare(CLONE_FS)) was reported through the leader. They
  now resolve ProcPidTarget::task(), which is Linux get_proc_task(inode): the
  leader for /proc/<tgid>, the thread for /proc/<tid>.
- /proc/<nr>/stat took CPU time (fields 14/15) from the per-thread accounting
  while its fault counters (10/12) aggregated the thread group. Both now come
  from the one usage view do_task_stat() selects with whole=1, so the two
  directories of a task group cannot disagree.
- The mounts open path reported ESRCH for a task that is gone;
  mounts_open_common() leaves EINVAL there.

The dunitest suite grows to 22 cases: it pins the thread-scoped fd and mounts
subtrees and the thread-group stat view against threads holding private state,
plus a sliced-read reassembly case for /proc/net/arp. The file now owns a single
probe-thread helper instead of two.

Signed-off-by: longjin <longjin@dragonos.org>
@fslongjin

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Can't wait for the next one!

Reviewed commit: dc0164bff5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

…trait graph

The kernel's object graph is one strongly-connected component, so proving
`Send`/`Sync` for a type such as `File` walks an obligation chain of roughly 130
levels through it. That chain is just past rustc's default budget of 128: once
`/proc/net/arp` began pinning its network namespace per file (the
`seq_open_net()` behaviour asked for in review), the riscv64 and loongarch64
builds failed with

    error[E0275]: overflow evaluating the requirement
    `alloc::sync::Weak<filesystem::page_cache::PageCache>: Sync`

at `kernel/src/filesystem/page_cache/writeback.rs:4393`, while x86_64 still
passed. `writeback.rs` is untouched by this branch, so the report is about the
solver's budget, not about a type that is actually unsound.

Adding only the new `ProcfsFilePrivateData.net_ns` field on top of the previous
commit reproduces that exact diagnostic, so this branch's new edge is what
consumed the last of the margin; master already resolves at a depth of 121-128
on riscv64. The budget is load-bearing rather than cosmetic as well: the
generated code measurably differs between budgets (the kernel crate's `.text`
grows by about 2.6% from 128 to 256), so it cannot simply be ignored.

Set the limit to 256, the value rustc suggests for this diagnostic and about
twice the measured depth, rather than dropping an edge from the type graph or
giving up the per-file namespace pin that `seq_open_net()` requires.

Verified: riscv64 and loongarch64 `cargo build --release` with the CI flags and
x86_64 `make kernel` all succeed, and `FMT_CHECK=1 make fmt` is clean.

Signed-off-by: longjin <longjin@dragonos.org>
@fslongjin

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0f94b8e70d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +84 to +86
proc_read_snapshot(offset, len, buf, &mut data, move || {
render_mount_file(&view, self.kind)
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 将挂载表改为有界增量输出

当具有挂载权限的容器进程创建接近 mount-max(默认 100,000)个挂载点,并打开大量 /proc/self/{mounts,mountinfo,mountstats} fd 后各读取一个字节时,proc_read_snapshot() 会为每个 fd 渲染并长期保留整张挂载表;结合默认可达 1,048,576 的 RLIMIT_NOFILE,这会形成 O(fd 数量 × 挂载数量) 的全局内核堆放大。Linux 的 seq_file 只缓存当前输出块,因此这些文件也应使用带挂载游标的有界增量渲染,而不是整表快照。

AGENTS.md reference: AGENTS.md:L9-L11

Useful? React with 👍 / 👎.

Comment on lines +49 to +53
let ns = task.nsproxy().mnt_ns.clone();
let root = task
.try_fs_struct()
.ok_or(SystemError::ENOENT)?
.root()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 原子捕获挂载命名空间与根目录

当目标线程在另一个进程打开其 mountinfo 的同时执行 setns(CLONE_NEWNS)unshare(CLONE_NEWNS) 时,这两个独立读取可能捕获不一致的视图:PreparedNamespaceInstall::commit() 先在 nsproxy.rs:426-427 发布新 fs_struct,再于 430 行发布新 nsproxy,因此这里可组合出旧挂载命名空间和新根目录。随后渲染的路径和拓扑来自不同命名空间;应像 Linux 的 mounts_open_common() 一样,在共同的任务状态锁下同时固定二者。

AGENTS.md reference: AGENTS.md:L9-L11

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Bug fix A bug is fixed in this pull request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG REPORT] three procfs/task semantics gaps: non-snapshot reads, stale thread Ppid, task/ ESRCH after leader exit

1 participant