Skip to content

fix(uprobe): align perf lifecycle with Linux semantics - #2205

Closed
fslongjin wants to merge 4 commits into
DragonOS-Community:masterfrom
fslongjin:feat/uprobe-uretprobe
Closed

fix(uprobe): align perf lifecycle with Linux semantics#2205
fslongjin wants to merge 4 commits into
DragonOS-Community:masterfrom
fslongjin:feat/uprobe-uretprobe

Conversation

@fslongjin

Copy link
Copy Markdown
Member

Summary

  • align the uprobe perf ABI with Linux for PMU discovery, attribute validation, task and CPU selection, path resolution, BPF attachment, unsupported options, and file descriptor behavior
  • give definitions, consumers, sites, and XOL execution clear ownership and locking rules, including safe enable, disable, concurrent teardown, and immutable callback snapshots
  • reconcile probes across memory mapping changes, fork, exec, faults, signals, and task exit while revalidating VMA identity and preserving precise installation errors
  • add regression coverage for lifecycle transitions, invalid attributes, file aliases, relative paths, tmpfs-backed probes, multiple consumers, and unsupported operations

Validation

  • make kernel
  • cargo test -p uprobe (9 passed)
  • dunitest host build
  • DragonOS QEMU: uprobe_test (17 passed)
  • DragonOS QEMU regressions: process_signal_fork_test (10 passed), mmap_truncate_cow_test (2 passed), general_protection_signal_test (1 passed), and perf_bpf_mmap_test (2 passed)

Context

This continues the uprobe work from #2163 and addresses its lifecycle, concurrency, compatibility, and regression gaps at their source.

sparkzky and others added 4 commits August 16, 2026 17:40
…#2150 phase 1)

Implement userspace breakpoint probes (uprobe), phase 1 of issue DragonOS-Community#2150,
enabling agentsight to instrument SSL_read/SSL_write entry points.

The design is XOL-based rather than reusing kprobe's kernel-buffer
single-step (impossible at CPL=3). Key pieces:

- per-mm uprobe table guarded by an irqsave SpinLock (not the global
  KPROBE_MANAGER lock nor the mm RwSem; the #BP/#DB hit path is IRQ-off)
- breakpoint page install replicates do_wp_page private COW:
  copy_page_as_normal + single atomic set_entry + rmap attach/detach
  + flush_tlb_range. No transient empty PTE; each mm gets a private copy
  so writeback never persists 0xcc into the shared page-cache (.so)
- XOL: a per-mm user slot page executes the saved instruction copy with
  RIP-relative relocation (yaxpeax-x86), validated at registration time
- do_int3/do_debug gain is_from_user() dispatch. The #BP handler runs
  pre_handler + BPF (rip kept as the original probe address), then jumps
  rip to the pre-filled XOL slot, sets TF and NEED_UPROBE. The #DB handler
  recognizes XOL completion via NEED_UPROBE and restores rip; unconsumed
  user #BP is delivered as SIGTRAP(TRAP_BRKPT)
- perf: PERF_TYPE_MAX dispatches to uprobe when the name contains '/';
  UprobePerfEvent mirrors KprobePerfEvent and reuses BPF_PROG_TYPE_KPROBE

Delivered in four batches: uprobe crate (x86 instruction analysis), mm
integration (per-mm table / XOL / breakpoint page), exception dispatch,
and perf attach.

Verified: `make kernel` builds with 0 error / 0 warning; `cargo test -p
uprobe` passes 7/7. An independent reviewer confirmed the F1-F10 review
findings are satisfied with no kprobe/fork regression, and flagged two
bugs that are fixed: re-registering the same probe_vaddr no longer reads
0xcc as the original instruction, and a RIP-relative displacement overflow
now fails fast at registration instead of panicking at hit time.

Out of scope: uretprobe (phase 2) and the QEMU runtime integration test.

Refs: DragonOS-Community#2150

test(uprobe): add dunitest suite for uprobe breakpoint probes

Add suites/normal/uprobe.cc covering the userspace perf_event_open
uprobe path (issue DragonOS-Community#2150 phase 1):
- RegisterAndTriggerSurvivesHit: perf_event_open(type=PERF_TYPE_MAX,
  config1=path, config2=offset) on the current process, then call the
  probed function and assert it survives the #BP -> XOL -> #DB -> resume
  hit path and returns the correct value
- InvalidPathIsRejected / InvalidOffsetIsRejected: error inputs return
  negative errno

Target offset is resolved from /proc/self/maps (executable segment +
file pgoff), so the suite works regardless of PIE layout.

Compiles cleanly via `make build-suites`; the gtest framework runs (the
two negative cases pass on host Linux; the core trigger case is
DragonOS-specific and is validated at runtime under QEMU).

Refs: DragonOS-Community#2150

fix(uprobe): resolve CI failures - format check and cross-arch build

- Apply rustfmt to uprobe integration code (reorder modules, imports,
  line width) to pass format-check on all arches
- Add #[cfg(target_arch = "x86_64")] gates to uprobe integration points
  (exception/perf/mm-ucontext modules, AddressSpace fields, fork path,
  perf dispatch arm) so riscv64/loongarch64 build succeeds
- Non-x86_64 perf dispatch returns ENOSYS for uprobe paths
- Fix unused_mut on phys_addr in fork path for non-x86_64
- Thread 1: add ptrace access check (check_process_vm_access) before
  taking a target mm for cross-process uprobe, preventing unprivileged
  users from instrumenting arbitrary processes
- Thread 2: reject control-flow instructions (call/jmp/ret/jcc/loop/int/
  syscall) at registration time — XOL cannot safely single-step them
- Thread 3: read_user_insn_bytes now continues into the next page when
  the probe is near a page boundary, returning real bytes instead of
  zero-padding that could decode to a different instruction
- Thread 5: build_xol_slot fills trailing slot bytes with int3 (0xcc) so
  that a racy unregister during the XOL single-step window re-triggers
  #BP instead of executing zero-filled garbage
Security & correctness:
- R1: pid==-1 (system-wide) now requires CAP_SYS_PTRACE; pid<-1
  returns EINVAL. Per-pid path keeps check_process_vm_access.
- R4: user #DB not consumed by uprobe now falls through to the normal
  DebugException path (restores pre-PR master behavior for
  ptrace/hardware breakpoints/single-step).
- R5: original RFLAGS.TF is saved per-thread before XOL redirect and
  restored on completion (previously cleared unconditionally, silently
  disabling a program's own single-step mode).
- R6: old_instruction copy now covers the full decoded instruction
  across page boundaries (was limited to first-page remainder).
- R7: the breakpoint byte is restored only when the LAST consumer at
  that address unregisters (previously restored on every unregister,
  silently disabling remaining same-address consumers).
- R8: unregister writes the original byte on the CURRENT mapped page
  instead of remapping the registration-time page, preserving the
  program's own writes to other bytes of that page.
- R10: reject MOV SS (suppresses #DB) and POPF (overwrites RFLAGS/TF)
  at registration, in addition to control-flow instructions.
- R11a: honor perf_event_attr.disabled (event starts disabled).

Architecture (per-thread state, R2/R3/R12):
- ActiveXol per-thread state on the PCB: probe_vaddr, return_addr,
  orig_tf, xol_page_base. Saved at #BP before rip redirect, consumed
  at #DB: O(1) completion independent of uprobe_list (racy unregister
  between #BP and #DB no longer corrupts resume), abort path when rip
  is outside the XOL page (signal/fault diversion), callbacks run
  outside the per-mm spinlock.

Durable probe identity (R9):
- Global registry keyed by inode+offset with consumer ids. New file
  mappings (dlopen/mmap via file_mapping_with_file_ext) and fork get
  late-applied probes; exec starts with an empty table. Consumer close
  drops registry entries plus late handles (per-mm unregister via the
  existing UprobeHandle::Drop with R7/R8 semantics). fork inherits
  instances with privatized child breakpoint pages.

Verified: make kernel 0 error/0 warning; make fmt (clippy) clean;
cargo test -p uprobe 9/9; QEMU dunitest uprobe 6/6, exec_abi 6/6,
process_signal_fork 10/10.
Match the uprobe PMU ABI with Linux for event type discovery, perf_event_attr validation, PID and CPU selection, path resolution, unsupported options, BPF attachment, and file descriptor behavior.

Refactor definitions, consumers, sites, and XOL state so canonical file identity, enable and disable transitions, concurrent teardown, callback snapshots, and instruction restoration have explicit ownership and locking rules.

Reconcile probes across mmap, mremap, mprotect, madvise, fork, exec, signal, fault, and exit paths while revalidating VMA identity after page faults and preserving strict error reporting for eligible mappings.

Add dunitest coverage for lifecycle control, invalid attributes, aliasing, relative paths, tmpfs-backed probes, multiple consumers, and unsupported operations.

Signed-off-by: longjin <longjin@dragonos.org>
@github-actions github-actions Bot added the Bug fix A bug is fixed in this pull request label Aug 20, 2026
@fslongjin

Copy link
Copy Markdown
Member Author

@codex review

@fslongjin fslongjin closed this Aug 20, 2026

@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: f473f04bee

ℹ️ 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".

.first()
.is_some_and(|entry| Arc::ptr_eq(&entry.read().site, &site))
}) {
list.remove(&probe_vaddr);

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 在删除命中表项前排空旧断点陷阱

当另一 CPU 已经取到旧的 0xcc、但尚未进入 #BP handler 时,此处在恢复原字节后立即删除命中表项,会让随后到达的陷阱被误判为用户断点并投递 SIGTRAP,默认可直接终止被跟踪进程;普通 consumer 注销路径的表项删除也有同一窗口。应在删除前加入覆盖远端已取指窗口的同步/宽限期,或保留可识别旧命中的 tombstone,直到旧陷阱排空。

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

Useful? React with 👍 / 👎.

{
let guard = mm.xol_area.lock_irqsave();
if let Some(area) = guard.as_ref() {
return area.alloc_slot().map(Arc::new).ok_or(SystemError::ENOMEM);

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 让 XOL 区突破单页 256 探针上限

每个地址空间只创建一个 4 KiB XOL 页,而每个不同探针在整个注册期永久持有一个 16 字节 slot,因此第 257 个探针即使从未并发命中也会稳定返回 ENOMEM;包含数百个探针的常规 perf/BPF 会话会因此无法创建。应按 ActiveXol 生命周期临时租用 slot,或让 XOL 区按需扩展多页。

AGENTS.md reference: AGENTS.md:L9-L10

Useful? React with 👍 / 👎.

Comment on lines +663 to +665
let opcode = read_user_opcode(&inner.user_mapper.utable, probe_vaddr)?;
let (old_instruction, analysis) = consumer.definition.instruction();
if opcode != old_instruction[0] {

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 在 XOL 前校验完整的映射指令

若私有文件映射曾可写并修改了操作数或立即数,随后通过 mprotect 变为只读可执行,只比较首字节仍会接受首字节相同的已修改指令,但 XOL slot 随后执行的是文件页缓存中的完整原始指令。这会在探针启用期间静默改变程序计算或访存行为;应读取并比较 analysis.insn_len 个映射字节(包括跨页情况),而非只验证第一个字节。

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

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.

2 participants