Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions config-zh.toml
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,15 @@ backtrace_depth = 128
# 默认值:false
force_perf_event_array = false

# 使用可睡眠的 `uprobe.s` 程序替代普通 `uprobe` 程序。
# 这是显式开启的实验性选项:需要 Linux 5.18+;开启后,固定长度的用户态
# 内存读取可使用 bpf_copy_from_user_task(),从而允许处理用户页缺失。
# 需要 NUL 终止语义的字符串读取仍使用 bpf_probe_read_user_str()。
# sleepable uprobe 必须使用 RingBuf,不能与 force_perf_event_array 同时开启。
# 较深的 DWARF bt 在内核允许时使用 tail call;否则限制为五帧 inline 回溯并打印警告。
# 默认值:false
sleepable_uprobe = false

# 源代码路径配置
# 当 DWARF 调试信息中包含的编译时路径与运行时路径不同时,
# 使用这些设置帮助 ghostscope 定位实际的源文件。
Expand Down
10 changes: 10 additions & 0 deletions config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,16 @@ backtrace_depth = 128
# Default: false
force_perf_event_array = false

# Use sleepable `uprobe.s` programs instead of regular `uprobe` programs.
# This is opt-in because it requires Linux 5.18+ and can let fixed-size user
# memory reads use bpf_copy_from_user_task(), which may fault in user pages.
# NUL-terminated string reads keep bpf_probe_read_user_str() semantics.
# Sleepable uprobes require RingBuf; do not combine with force_perf_event_array.
# Long DWARF bt uses tail calls when supported; otherwise it is limited to five
# inline frames and GhostScope prints a warning.
# Default: false
sleepable_uprobe = false

# Source code path configuration
# When DWARF debug info contains compilation-time paths that differ from runtime paths,
# use these settings to help ghostscope locate the actual source files.
Expand Down
51 changes: 51 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,10 @@ ghostscope --source-panel # Show source panel
# WARNING: Testing purposes only. Forces PerfEventArray even on kernels >= 5.8
ghostscope --force-perf-event-array

# Opt in to sleepable uprobes for this invocation. This overrides
# sleepable_uprobe=false in the config file.
ghostscope --sleepable-uprobe

# Standalone -t starts target-mode sysmon by default. When -t is combined with
# -p, GhostScope uses the -p watched-PID module-refresh path instead.
# WARNING: Attaches system-wide lifecycle tracepoints (exec/fork/exit) and may
Expand All @@ -194,6 +198,45 @@ ghostscope --force-perf-event-array
ghostscope --enable-sysmon-for-target
```

### Sleepable Uprobes

Sleepable uprobes are an opt-in `[ebpf]` configuration because they require a
Linux 5.18+ kernel and can change probe latency by allowing a user-memory page
fault to be serviced during a probe hit. The default remains the regular,
non-sleepable `uprobe` path.

```toml
[ebpf]
sleepable_uprobe = true
```

For a one-off run, use `--sleepable-uprobe`; it enables this setting even when
the config file has `sleepable_uprobe = false`. To keep regular uprobes, omit
the flag and leave the configuration at its default `false`.

Sleepable uprobes require RingBuf event output. Do not combine
`sleepable_uprobe = true` (or `--sleepable-uprobe`) with
`force_perf_event_array = true` (or `--force-perf-event-array`): the kernel
does not permit sleepable BPF programs to use `BPF_MAP_TYPE_PERF_EVENT_ARRAY`,
and GhostScope reports this configuration conflict before attaching.

When enabled, GhostScope emits `uprobe.s` programs. Fixed-size user-memory
reads use `bpf_copy_from_user_task()` so pages can be faulted in; NUL-terminated
string reads retain `bpf_probe_read_user_str()` because its early-NUL and
reported-length semantics differ. Startup fails clearly if the kernel does not
support the helper required by sleepable uprobe mode rather than silently
falling back.

Long DWARF `bt` traces normally use a tail-call step program. GhostScope probes
that sleepable tail-call path at startup; on kernels that do not allow a
sleepable program to use `BPF_MAP_TYPE_PROG_ARRAY`, it compiles the trace with
the inline limit of five frames instead and prints a warning. This preserves a
working sleepable trace rather than failing eBPF verification.

Enable this only when fault-capable reads are worth the added latency and kernel
requirement. Set `sleepable_uprobe = false` (the default) to use regular
uprobes again.

### BPFFS Maintenance

GhostScope uses bpffs because some runtime state must be shared across the userspace process layer, the loader, and the eBPF programs themselves. In practice, maps such as `proc_module_offsets` and `allowed_pids` are pinned into bpffs so later stages can reopen and reuse the same kernel maps by path instead of recreating them. GhostScope places these pins under a per-instance `pid-starttime` directory to avoid collisions between concurrent runs.
Expand Down Expand Up @@ -276,6 +319,7 @@ unusable index is reported in CLI/TUI startup status before falling back.
| `--source-panel` | | Show source panel | On |
| `--config <PATH>` | | Custom config file | Auto-detect |
| `--force-perf-event-array` | | Force PerfEventArray (testing) | Off |
| `--sleepable-uprobe` | | Enable sleepable uprobes for this run; overrides a `false` config value | Off |
| `--enable-sysmon-for-target` | | Re-enable target-mode sysmon for standalone `-t` when config disables it. Standalone `-t` enables target-mode sysmon by default; `-t -p` uses the `-p` watched-PID module-refresh path instead. | Off |
| `[BINARY] [ARGS...]` | | Launch target program with positional arguments | None |
| `--args <PROGRAM> [ARGS...]` | | Separate GhostScope options from target program arguments | None |
Expand Down Expand Up @@ -542,6 +586,13 @@ backtrace_unwind_rows_max_entries = 65536
# overhead compared to RingBuf and should only be used for compatibility testing.
force_perf_event_array = false # Default (auto-detect based on kernel version)

# Opt-in sleepable uprobes. Requires Linux 5.18+.
# Fixed-size user-memory reads use bpf_copy_from_user_task().
# NUL-terminated string reads retain bpf_probe_read_user_str() semantics.
# Long DWARF bt uses tail calls when the kernel allows them; otherwise it is
# limited to five inline frames and GhostScope prints a warning.
sleepable_uprobe = false # Default

# Start sysmon eBPF for standalone -t targets.
# Maintains ASLR offsets for late-start processes and runtime module refresh for
# processes that map the target or other backtrace modules later.
Expand Down
2 changes: 1 addition & 1 deletion docs/limitations.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ development time. JIT language support is an even more distant goal.
### 2. User-Memory Reads via `bpf_probe_read_user`
In traditional non-sleepable probe paths, helpers such as `bpf_probe_read_user` cannot resolve user-space page faults, so reads from a target virtual address may still fail if the page is not resident or otherwise faults on access.

This is no longer an absolute eBPF limitation. Linux now supports sleepable uprobes (`uprobe.s` / `uretprobe.s`), and sleepable programs can use helpers such as `bpf_copy_from_user_task()` for fault-capable user-memory reads. GhostScope currently emits regular `uprobe` programs, so this remains a practical limitation today, but it is better described as a soft implementation limitation rather than a fundamental design limit of eBPF.
This is no longer an absolute eBPF limitation. GhostScope can emit sleepable `uprobe.s` programs when `[ebpf].sleepable_uprobe = true`; fixed-size user-memory reads then use `bpf_copy_from_user_task()` for fault-capable reads. The option is disabled by default and requires Linux 5.18+ because servicing a fault can increase the traced thread's latency. NUL-terminated string reads retain `bpf_probe_read_user_str()` semantics, so sleepable mode does not guarantee that every user-memory access can fault in a page.

**References**:
- https://lists.iovisor.org/g/iovisor-dev/topic/accessing_user_memory_and/21386221
Expand Down
4 changes: 2 additions & 2 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ GhostScope is still evolving quickly. The milestones below are ordered from “s
- See [Container support and limits](container.md) and [Limitations](limitations.md#10-container--wsl-limitations-for--p-pid-mode).

## Uprobe enhancements
- Add support for sleepable uprobes (`uprobe.s` / `uretprobe.s`) so GhostScope can use sleepable helpers where appropriate, especially for more reliable user-memory reads.
- Sleepable entry uprobes (`uprobe.s`) are available as an opt-in `[ebpf].sleepable_uprobe` setting. Regular `uprobe` remains the default compatibility path.
- Add support for multi-attach uprobes (`uprobe.multi` / `uretprobe.multi`) to scale better when a script expands into many probe points.
- Keep compatibility fallbacks for kernels or libbpf/Aya paths that still require regular `uprobe` attachments.
- Continue improving compatibility diagnostics for kernels or Aya paths that only support regular `uprobe` attachments.

## Stack Unwinding
- DWARF-only `bt` / `backtrace` is now supported for compact CFI rows that can
Expand Down
45 changes: 45 additions & 0 deletions docs/zh/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,10 @@ ghostscope --source-panel # 显示源码面板
# 警告:仅用于测试目的。即使在内核 >= 5.8 上也强制使用 PerfEventArray
ghostscope --force-perf-event-array

# 为本次运行显式启用 sleepable uprobe;会覆盖配置文件中的
# sleepable_uprobe=false。
ghostscope --sleepable-uprobe

# 独立 -t 默认启动 target-mode sysmon。-t 与 -p 同时使用时,
# GhostScope 会改用 -p 的 watched-PID 模块刷新路径。
# 警告:该选项会全局附加生命周期 tracepoint(exec/fork/exit),也可能
Expand All @@ -194,6 +198,40 @@ ghostscope --force-perf-event-array
ghostscope --enable-sysmon-for-target
```

### Sleepable Uprobe

sleepable uprobe 是 `[ebpf]` 下显式开启的配置项:它要求 Linux 5.18+,并且
探针命中时允许为用户态内存处理缺页,因此可能增加目标线程的探针延迟。默认仍是
普通、不可睡眠的 `uprobe` 路径。

```toml
[ebpf]
sleepable_uprobe = true
```

如只需单次开启,可使用 `--sleepable-uprobe`;即使配置文件中设置了
`sleepable_uprobe = false`,该参数也会启用它。若要继续使用普通 uprobe,请不传
该参数,并保留默认配置 `false`。

sleepable uprobe 必须使用 RingBuf 输出。不要将 `sleepable_uprobe = true`(或
`--sleepable-uprobe`)与 `force_perf_event_array = true`(或
`--force-perf-event-array`)同时使用:内核不允许 sleepable BPF 程序使用
`BPF_MAP_TYPE_PERF_EVENT_ARRAY`,GhostScope 会在 attach 前明确报告该配置冲突。

开启后,GhostScope 会生成 `uprobe.s` 程序。固定长度的用户态内存读取会使用
`bpf_copy_from_user_task()`,使相关页面能够被 fault-in;需要 NUL 终止和返回实际
长度语义的字符串读取仍使用 `bpf_probe_read_user_str()`。如果内核不支持
sleepable uprobe 模式所需的 helper,启动会给出明确错误,而不会静默回退到普通
uprobe。

较深的 DWARF `bt` 通常使用 tail-call step program。GhostScope 会在启动时探测
sleepable tail-call 路径;如果内核不允许 sleepable 程序使用
`BPF_MAP_TYPE_PROG_ARRAY`,则会将该回溯编译为最多五帧的 inline 路径并打印警告。
这样 sleepable trace 仍可工作,而不会在 eBPF verifier 阶段失败。

只有在 fault-capable 读取的收益值得额外延迟和内核要求时才应开启;将
`sleepable_uprobe = false`(默认值)即可恢复普通 uprobe。

### BPFFS 维护

GhostScope 使用 bpffs,是因为有一部分运行时状态需要在用户态的 process 层、loader 以及 eBPF 程序之间共享。实际里像 `proc_module_offsets` 和 `allowed_pids` 这样的 map,会先 pin 到 bpffs,这样后续阶段就能按路径重新打开并复用同一个内核 map,而不是重复创建。GhostScope 又把这些 pin 放在按实例隔离的 `pid-starttime` 目录下,用来避免多个实例并发运行时互相冲突。
Expand Down Expand Up @@ -275,6 +313,7 @@ GhostScope 会使用该原生索引选择 CU,并按需调用 fast parser。否
| `--source-panel` | | 显示源码面板 | 开 |
| `--config <PATH>` | | 自定义配置文件 | 自动检测 |
| `--force-perf-event-array` | | 强制 PerfEventArray(测试) | 关 |
| `--sleepable-uprobe` | | 为本次运行启用 sleepable uprobe;覆盖配置中的 `false` | 关 |
| `--enable-sysmon-for-target` | | 当配置关闭 sysmon 时,重新为独立 `-t` 开启 target-mode sysmon。独立 `-t` 默认开启;`-t -p` 改用 `-p` 的 watched-PID 模块刷新路径。 | 关 |
| `[BINARY] [ARGS...]` | | 启动目标程序并传递位置参数 | 无 |
| `--args <PROGRAM> [ARGS...]` | | 分隔 GhostScope 选项和目标程序参数 | 无 |
Expand Down Expand Up @@ -533,6 +572,12 @@ backtrace_unwind_rows_max_entries = 65536
# 有性能开销,仅应用于兼容性测试。
force_perf_event_array = false # 默认(根据内核版本自动检测)

# 显式开启 sleepable uprobe;要求 Linux 5.18+。
# 固定长度用户态内存读取使用 bpf_copy_from_user_task()。
# 需要 NUL 终止语义的字符串读取仍使用 bpf_probe_read_user_str()。
# 较深的 DWARF bt 在内核允许时使用 tail call;否则限制为五帧 inline 回溯并打印警告。
sleepable_uprobe = false # 默认关闭

# 为独立 -t 目标启动 sysmon eBPF,
# 用于维护后续启动进程里的 ASLR 偏移,以及后续映射目标模块或其他
# backtrace 模块时的运行时模块刷新。
Expand Down
2 changes: 1 addition & 1 deletion docs/zh/limitations.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Hash 表条目中的嵌套 adapter 会在配置的递归深度、集合宽度和
### 2. 通过 `bpf_probe_read_user` 读取用户内存
在传统的非 sleepable probe 路径里,`bpf_probe_read_user` 这类 helper 仍然不能处理用户态缺页,因此当目标虚拟地址对应的页面尚未驻留,或访问时会触发 fault,读取就可能失败。

但这已经不是 eBPF 的绝对硬限制。Linux 已支持 sleepable uprobe(`uprobe.s` / `uretprobe.s`),而 sleepable 程序可以使用 `bpf_copy_from_user_task()` 之类的 helper 执行可睡眠的用户态内存读取。GhostScope 当前仍生成普通 `uprobe` 程序,所以现阶段它在实践中仍然是个限制;但更准确地说,这属于实现层面的软限制,而不是 eBPF 的根本设计上限
但这已经不是 eBPF 的绝对硬限制。将 `[ebpf].sleepable_uprobe` 设为 `true` 后,GhostScope 会生成可睡眠的 `uprobe.s` 程序;固定长度的用户态内存读取会使用 `bpf_copy_from_user_task()`,从而支持可处理缺页的读取。该选项默认关闭,并要求 Linux 5.18+,因为处理缺页可能增加被跟踪线程的延迟。需要 NUL 终止语义的字符串读取仍使用 `bpf_probe_read_user_str()`,所以 sleepable 模式并不保证每一种用户态内存访问都能 fault-in 页面

**参考**:
- https://lists.iovisor.org/g/iovisor-dev/topic/accessing_user_memory_and/21386221
Expand Down
4 changes: 2 additions & 2 deletions docs/zh/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ GhostScope 仍处在快速演进阶段,以下里程碑按照“优先修补基
- 详见[容器支持与限制](container.md)和[限制列表](limitations.md#10-容器-wsl-场景下--pid-pid-模式的软限制)。

## Uprobe 增强
- 支持 sleepable uprobe(`uprobe.s` / `uretprobe.s`),在合适场景下使用可睡眠 helper,尤其提升用户态内存读取的可靠性
- sleepable 入口 uprobe(`uprobe.s`)已通过 `[ebpf].sleepable_uprobe` 配置项提供;普通 `uprobe` 仍是默认的兼容路径
- 支持 multi-attach uprobe(`uprobe.multi` / `uretprobe.multi`),让脚本展开出大量探针点时仍能保持更好的扩展性。
- 对暂时只能走普通 `uprobe` 的内核或 libbpf/Aya 路径保留兼容性回退
- 继续改进只支持普通 `uprobe` 的内核或 Aya 路径的兼容性诊断

## 栈回溯(Stack Unwinding)
- 已支持 DWARF-only `bt` / `backtrace`,用于可安全降到 eBPF 执行的
Expand Down
99 changes: 99 additions & 0 deletions e2e-tests/tests/script_execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,105 @@ trace print_record {
Ok(())
}

#[tokio::test]
async fn test_sleepable_uprobe_cli_opt_in_overrides_config_and_reads_user_memory(
) -> anyhow::Result<()> {
init();
ensure_global_cleanup_registered();

let target = get_global_test_target_with_opt(OptimizationLevel::Debug).await?;
let script_content = r#"
trace process_record {
if (record.value > 0) {
print "SLEEPABLE_USER_READ";
}
}
"#;

let (exit_code, stdout, stderr) = common::runner::GhostscopeRunner::new()
.with_script(script_content)
.attach_to(&target)
.timeout_secs(5)
.enable_sysmon_for_target(false)
.with_config_content(
r#"
[ebpf]
sleepable_uprobe = false
"#,
)
.with_cli_args(["--sleepable-uprobe"])
.run()
.await?;

assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}");
assert!(
stdout.contains("SLEEPABLE_USER_READ"),
"sleepable uprobe did not read the record field. stdout={stdout} stderr={stderr}"
);
Ok(())
}

#[tokio::test]
async fn test_sleepable_uprobe_backtrace_uses_a_kernel_supported_depth() -> anyhow::Result<()> {
init();
ensure_global_cleanup_registered();

let target = get_global_test_target_with_opt(OptimizationLevel::Debug).await?;
let script_content = r#"
trace test_function {
print "before-sleepable-bt";
bt full;
print "after-sleepable-bt";
}
"#;

let (exit_code, stdout, stderr) = common::runner::GhostscopeRunner::new()
.with_script(script_content)
.attach_to(&target)
.timeout_secs(5)
.enable_sysmon_for_target(false)
.with_cli_args(["--sleepable-uprobe", "--backtrace-depth", "6"])
.run()
.await?;

assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}");
let backtrace = stdout
.find("backtrace:")
.ok_or_else(|| anyhow::anyhow!("missing backtrace header:\n{stdout}"))?;
assert!(
stdout[backtrace..].contains("(max 5)") || stdout[backtrace..].contains("(max 6)"),
"sleepable bt should preserve depth on kernels with sleepable tail calls or fall back to five inline frames:\n{stdout}"
);
assert!(
stdout.contains("after-sleepable-bt"),
"sleepable backtrace did not finish:\n{stdout}"
);
Ok(())
}

#[tokio::test]
async fn test_sleepable_uprobe_rejects_forced_perf_event_array() -> anyhow::Result<()> {
init();
ensure_global_cleanup_registered();

let target = get_global_test_target_with_opt(OptimizationLevel::Debug).await?;
let (exit_code, _stdout, stderr) = common::runner::GhostscopeRunner::new()
.with_script("trace process_record { print record.value; }")
.attach_to(&target)
.timeout_secs(5)
.enable_sysmon_for_target(false)
.with_cli_args(["--sleepable-uprobe", "--force-perf-event-array"])
.run()
.await?;

assert_ne!(exit_code, 0, "conflicting options unexpectedly succeeded");
assert!(
stderr.contains("sleepable BPF programs cannot use BPF_MAP_TYPE_PERF_EVENT_ARRAY"),
"missing conflict explanation: stderr={stderr}"
);
Ok(())
}

#[tokio::test]
async fn test_backtrace_outputs_dwarf_frames_between_prints() -> anyhow::Result<()> {
init();
Expand Down
Loading
Loading