Container Environments

June 24, 2026 ยท View on GitHub

This document explains how GhostScope behaves in container environments, which scenarios matter, and what the current implementation limits are.

Topic 1: Container PID Semantics for -p and -t

PID namespaces are the core source of complexity in container environments, so this section explains them once and then distinguishes how -p and -t use those same PID views differently.

This document assumes a local CLI workflow: GhostScope runs in the same environment where you actually invoke GhostScope. The deployment-scope discussion for running GhostScope itself inside a container is collected later in Topic 3.

The Most Important User Rule

For ghostscope -p <PID>, the rule is simple:

Enter the PID as seen in the same environment where you run ghostscope -p, for example from ps, top, or pgrep.

In other words:

  • If you run ghostscope -p on the host, enter the PID you see on the host.
  • If you run ghostscope -p inside a container, enter the PID you see inside that container.

Users should not manually convert between "host PID" and "container PID". GhostScope is responsible for translating the user input into the internal PID meanings it needs.

For -t, there is no PID input from the user, but the same current environment still defines GhostScope's /proc view. That means the environment where GhostScope runs still determines which proc_pid is authoritative for /proc/<pid>/maps, proc_module_offsets, and runtime lifecycle cleanup.

Why Containers Make This More Complicated

PID namespaces allow the same process to have multiple PIDs at the same time.

Typical example:

  • The host sees a process as 81234
  • A container sees the same process as 17

Both are correct. They are just different views of the same process.

GhostScope depends on two kinds of information:

  • userspace /proc/<pid>/...
  • kernel events and eBPF PID filtering

These two sources do not always speak the same PID language, so container scenarios require extra PID mapping logic.

PID Terms Used in This Document

For clarity, this document uses the following names:

  • input_pid The PID entered by the user when running ghostscope -p, meaning the PID visible in that command's current environment.
  • proc_pid The PID visible in GhostScope's current userspace /proc view, and the PID that can be used to read /proc/<pid>/maps.
  • host_pid The PID of the same target process in the host / initial PID namespace. Traditional bpf_get_current_pid_tgid() uses this PID view as well.
  • container_pid The PID of the same target process in the innermost / target PID namespace view that GhostScope can resolve, usually the tail of NSpid when that mapping is explicit. In host-only or shared-PID cases it often collapses to the same numeric value as host_pid, so it is not always a distinct extra PID.
  • pid_filter This term is only relevant in -p mode. After the user enters ghostscope -p <PID>, GhostScope resolves the internal PID views it needs and then installs an eBPF-side filter that keeps runtime events associated with that original input_pid. The purpose of pid_filter is to make sure GhostScope still filters the process the user intended, even when container PID namespaces mean the userspace-visible PID and the kernel-side PID view are not expressed the same way. In simpler cases this behaves like host-view TGID filtering; in namespace-aware cases it behaves more like "the target PID in a specific PID namespace".
  • event_pid The PID carried by runtime kernel events. GhostScope has a process-lifecycle monitoring pipeline that listens for exec, fork, and exit events, and this is the PID that pipeline sees first. In the current implementation, event_pid is populated from bpf_get_current_pid_tgid() >> 32, so it reflects the TGID in the host / initial PID namespace view. When the event comes from a particular target process, it will usually align with that process's host_pid, but it cannot directly replace proc_pid for accessing the current /proc view or for cleaning caches keyed by proc_pid.
  • proc_offsets_runtime_pid An implementation-only term for the PID value that the eBPF side obtains first when it is about to look up proc_module_offsets. This is not a new stable user-facing PID view. Depending on the path, it may line up with host_pid, container_pid, or a PID carried by runtime events. GhostScope may still need to normalize it before the map lookup can succeed.
  • proc_offsets_lookup_pid An implementation-only term for the PID component that is finally used to look up proc_module_offsets. In the intended successful path, this should align with proc_pid, because proc_module_offsets is populated from /proc/<proc_pid>/maps in GhostScope's current userspace environment.

For the current implementation, the proc_module_offsets path is easiest to understand as:

proc_offsets_runtime_pid -> pid_aliases -> proc_offsets_lookup_pid (= usually proc_pid)

Here:

  • pid_aliases is an internal bridge map, not a general PID translation layer.
  • It exists only to help proc_module_offsets recover the /proc-side PID key that userspace used during prefill.
  • It does not redefine event_pid, and it does not replace the normal pid_filter semantics used for -p.

From a product perspective, users only need to care about input_pid. The other PID values are internal views that GhostScope maintains for /proc access, eBPF filtering, and cleanup.

One Important Fact About the Current Implementation

The current runtime environment detection in the code, which classifies the environment as container, host, or unknown, is about GhostScope itself, not about whether the target process runs in a container.

This means:

  • User-facing docs need to describe the rule from the user's perspective: enter the PID visible where you run ghostscope -p.
  • Implementation analysis still needs to care about which PID namespace GhostScope itself is in, because that affects /proc visibility, helper selection, and whether fallback behavior is safe.

So this document distinguishes between:

  • user-facing scenario semantics
  • implementation-level technical differences caused by GhostScope's own runtime environment

How -p and -t Use the Same PID Terms Differently

The scenario descriptions below are shared. The main difference is which parts of the PID model each mode actually depends on:

  • -p Starts from a user-supplied input_pid, resolves the target's PID views, and installs a pid_filter so eBPF events keep matching the process the user meant.
  • -t Does not have an input_pid. Instead, GhostScope relies on sysmon and runtime module-maintenance logic, so the hard problem becomes keeping host-view event_pid and current-/proc proc_pid aligned strongly enough for /proc/<pid>/maps, offset prefill, allowlists, and exit cleanup.

That is why the same scenario can be fine for -p but still limited for -t.

Scenario Matrix

The main scenarios can be described using two axes: where GhostScope runs, and where the target process runs.

Scenario 1: GhostScope on the Host, Target Also on the Host

This is the simplest case.

  • The user enters a host-visible PID.
  • input_pid, proc_pid, and host_pid are usually the same.
  • No extra PID-namespace mapping is involved.

Scenario 2: GhostScope on the Host, Target in a --pid=host Container

This is very close to scenario 1 because the container shares the same PID namespace as the host.

  • The user still enters a host-visible PID.
  • The PID seen inside the container matches the host PID.
  • input_pid, proc_pid, and host_pid are still usually the same.
  • Even if bpf_get_ns_current_pid_tgid is unavailable, GhostScope can still safely use host-view PID filtering as long as the target remains in the initial PID namespace.

The key point is that although the process is "inside a container", it still belongs to the host PID namespace from a PID-semantics perspective.

Scenario 3: GhostScope on the Host, Target in a Private PID-Namespace Container

This is the most important container PID scenario for the current implementation.

  • The user runs GhostScope on the host, so the input is the host-visible PID.
  • The container has another namespace-local PID for the same target.
  • The same target process has both a host_pid and a container-local PID.

Common observations in this case:

  • GhostScope's /proc access is closer to the host view.
  • Kernel events are also usually expressed using host / initial PID namespace PIDs.
  • If the script uses $pid/$tid, or if helper support is unavailable and GhostScope must consider fallback behavior, the difference between host PID and container-local PID becomes visible.
  • In this case, $input_pid still reflects the host-side -p input value, $host_pid still reflects the host PID view, and $pid/$tid are closer to the target PID namespace view.

This is the class of scenarios that the current implementation primarily tries to support and reason about.

Scenario 4: GhostScope in a --pid=host Container, Target on the Host or in Another Host-PID-Namespace Process

From a PID-semantics perspective, this is very similar to scenarios 1 and 2.

This scenario is listed mainly to show that GhostScope being inside a container does not automatically mean the PID language has changed.

  • GhostScope runs in a container, but the PID view it sees is still the host view.
  • The PID entered in that container shell is usually already the host PID.
  • The /proc view is still close to the host.

What changes here is mostly the answer to "does GhostScope itself look container-like?", not whether the PID values themselves need translation.

Scenario 5: GhostScope in a Private PID-Namespace Container, Target in the Same PID Namespace

Here, the user runs GhostScope inside the container, so the input is the container-visible PID.

This scenario is included because it explains what changes once GhostScope and the target share the same private PID namespace.

  • input_pid usually equals the container-visible proc_pid.
  • host_pid may differ.
  • If GhostScope needs to line up with kernel events or host-side PID semantics, explicit PID mapping is required.

From the user's perspective, the rule is still the same: enter the PID visible where you run the command. But GhostScope's internal implementation is much more complex here than in host-side scenarios.

Scenario 6: GhostScope in a Private PID-Namespace Container, Target in a Descendant / Nested Private PID Namespace

This is the "outer container -> child container" case that sits between scenarios 5 and 7.

The important difference from scenario 5 is that GhostScope and the target do not share the exact same PID namespace, but the target is still visible from GhostScope's current /proc view because the target lives in a descendant namespace.

  • The user still runs GhostScope in the outer container, so input_pid is the PID visible from that outer container.
  • proc_pid is still usually the same as that outer-container-visible PID.
  • The target may also have a different innermost PID in the child container, so container_pid can differ from both input_pid and proc_pid.
  • If GhostScope uses namespace-aware PID filtering here, the comparison target is no longer "the current /proc PID view"; it needs the target PID in the target's own innermost namespace.

This scenario is now a separately validated -p path: GhostScope still accepts the PID visible from the outer container's /proc, but namespace-aware filtering must compare against the target's innermost container_pid. If the helper path is unavailable and NSpid does not expose a trustworthy host mapping, GhostScope should still fail fast rather than guess.

Scenario 7: GhostScope in One Private PID-Namespace Container, Target Outside That PID Namespace

This is one of the most ambiguous and failure-prone cases.

In the current implementation, this scenario is not part of the supported path.

It is mainly useful to explain why GhostScope must not blindly guess PID mappings across namespaces.

Potential problems include:

  • The target PID is not visible at all in the current /proc view.
  • GhostScope may receive some kernel-view events, but cannot reliably map them back to the current namespace's /proc path.
  • If helpers are unavailable, fallback behavior may be unsafe.

In these cases, GhostScope should fail clearly and early rather than guessing mappings.

Scenario-to-PID and Support Reference Table

The prose above explains the shared semantics. The table below turns those same cases into a quick lookup for both -p and -t.

Here, "scenario" always means the relationship between two sides:

  • where GhostScope itself is running
  • where the observed target process is running
Scenarioinput_pidproc_pidhost_pidcontainer_pidpid_filterevent_pidproc_offsets_runtime_pidproc_module_offsets lookup key-p Support-t Support
1. Host -> hostHost-visible PID entered on the hostUsually the same as input_pidUsually the same as input_pid and proc_pidUsually the same as host_pid, because no nested PID namespace is involvedbpf_get_current_pid_tgid() >> 32 == host_pidbpf_get_current_pid_tgid() >> 32, usually the same as host_pidUsually the same as host_pid and proc_pid; aliasing is usually unnecessaryproc_pid in the host /proc viewSupportedSupported
2. Host -> --pid=host containerHost-visible PID entered on the hostUsually the same as input_pidUsually the same as input_pid and proc_pidUsually the same as host_pid, because the container shares the host PID namespacebpf_get_current_pid_tgid() >> 32 == host_pidbpf_get_current_pid_tgid() >> 32, usually the same as host_pidUsually the same as host_pid and proc_pid; aliasing is usually unnecessaryproc_pid in the host /proc viewSupportedSupported
3. Host -> private PID-namespace containerHost-visible PID entered on the hostUsually the same as input_pid in the host /proc viewUsually the same as input_pid and proc_pidTarget PID inside the inner namespace; may differ from host_pidbpf_get_current_pid_tgid() >> 32 == host_pidbpf_get_current_pid_tgid() >> 32, usually the same as host_pidUsually the same as host_pid; aliasing is usually unnecessary because /proc is already in the host viewproc_pid in the host /proc viewSupportedSupported
4. --pid=host container -> host / shared-PID targetPID entered inside the container shell, which is already host-visibleUsually the same as input_pidUsually the same as input_pid and proc_pidUsually the same as host_pidHelper supported: bpf_get_ns_current_pid_tgid(...).tgid == proc_pid; helper unsupported: bpf_get_current_pid_tgid() >> 32 == host_pidbpf_get_current_pid_tgid() >> 32, usually the same as host_pidHelper supported: namespace-local TGID, which is still usually proc_pid; helper unsupported: host-view TGIDproc_pid in the current shared /proc viewSupportedSupported
5. Private PID-namespace container -> same private PID namespaceContainer-visible PID entered inside that containerUsually the same as input_pid in the current container /proc viewUsually different from input_pid and proc_pid; corresponds to the first value in NSpidUsually the same as input_pid and proc_pid when GhostScope and the target share that namespaceHelper supported: bpf_get_ns_current_pid_tgid(...).tgid == proc_pid; helper unsupported: if NSpid exposes an explicit host mapping, bpf_get_current_pid_tgid() >> 32 == host_pid, otherwise fail fastbpf_get_current_pid_tgid() >> 32, usually aligned with host_pid rather than input_pidHelper supported: usually the same as proc_pid; helper unsupported: often starts from host_pid, so aliasing may be neededproc_pid in the current container /proc viewConditionally supportedConditionally supported
6. Private PID-namespace container -> descendant / nested private PID namespacePID entered inside the current container shell, meaning the PID visible from the current container's /proc view, not the child container-local PIDUsually the same as input_pid in the current container /proc viewUsually different from input_pid and proc_pid; corresponds to the first value in NSpidUsually different from input_pid and proc_pid; corresponds to the tail of NSpid, often the child container-local PIDHelper supported: bpf_get_ns_current_pid_tgid(...).tgid == container_pid; helper unsupported: if NSpid exposes an explicit host mapping, bpf_get_current_pid_tgid() >> 32 == host_pid, otherwise fail fastbpf_get_current_pid_tgid() >> 32, usually aligned with host_pidHelper supported: usually starts from container_pid; helper unsupported: usually starts from host_pid; either way it still has to normalize back to the outer-container proc_pidproc_pid in the outer container /proc view, not the child-container-local container_pidConditionally supportedConditionally supported
7. Private PID-namespace container -> target outside that PID namespaceOften not satisfiable because the target is not visible in the current /proc viewOften unavailable from the current /proc viewOften not stably resolvable from the current view; if it exists, it belongs to a PID view outside the current /proc namespaceUnreliable or not visible from the current namespaceNo stable comparison is installed; GhostScope should fail fastNo stable event_pid to proc_pid mapping can be assumedNo stable runtime-side PID can be normalized back to the current /proc viewUsually unavailable because the current /proc view cannot stably identify the targetUnsupportedUnsupported

Notes:

  • pid_filter only exists in -p mode. Its job is to keep eBPF-side runtime events associated with the original ghostscope -p <PID> input.
  • -p support in the table means the current implementation can reliably maintain the user-facing PID-input contract and eBPF-side PID filtering for that scenario.
  • -t support in the table means the current implementation can reliably maintain target-path lifecycle state for that scenario, including the relationship between event_pid, proc_pid, and proc_module_offsets.
  • container_pid here means "the tail of NSpid when GhostScope can resolve one". It is often not a distinct extra value in host-only or shared-PID cases.
  • When the table says bpf_get_current_pid_tgid() >> 32 == host_pid, GhostScope is comparing the current event's host-view TGID against the resolved target host_pid.
  • When the table says bpf_get_ns_current_pid_tgid(...).tgid == proc_pid, GhostScope is comparing the current event's TGID in the target PID namespace against the resolved target proc_pid.
  • event_pid always comes from bpf_get_current_pid_tgid() >> 32, so it stays aligned with host-view TGID semantics even when pid_filter uses namespace-aware matching.
  • proc_offsets_runtime_pid is the PID value that the eBPF side sees first for proc_module_offsets lookup, before any pid_aliases normalization. It may line up with host_pid, proc_pid, or container_pid, depending on helper usage and namespace layout.
  • proc_module_offsets is populated from /proc/<pid>/maps in GhostScope's current userspace environment, so its lookup key follows proc_offsets_lookup_pid, which in the intended successful path should be the same as proc_pid, not event_pid, and in nested-container cases not necessarily the innermost container_pid.
  • That map is used for runtime module rebasing, including global-variable and shared-library address resolution that depends on the current /proc/<pid>/maps layout.
  • pid_aliases is the bridge from proc_offsets_runtime_pid to proc_offsets_lookup_pid. It is not a general replacement for event_pid, host_pid, or pid_filter.
  • Conditionally supported means the current implementation can work, but correctness still depends on helper availability, explicit enough NSpid data, and stable runtime alignment between the kernel-event side and the current /proc view.
  • Scenario 6 has the tightest PID-view requirements. -p and backtrace -t now have nested validation coverage, while globals_target nested -t cases still skip explicitly and wait on a stable runtime-PID alias back to the outer-container proc_pid.
  • Values marked "usually" or "may differ" still depend on the actual /proc view, NSpid, and helper availability at runtime.

Current -p Decision Flow

The scenario matrix above is meant to explain semantics. The current implementation does not literally decide "this is scenario 1" or "this is scenario 3" first. Instead, it infers behavior from a sequence of signals.

1. First Verify That the User Input Matches the Contract

Depends on:

  • /proc/<input_pid> in the current environment

Precondition:

  • input_pid is already defined to mean "the PID visible where ghostscope -p is being run".

Result:

  • If /proc does not contain that PID at all, GhostScope fails immediately.
  • This means the input does not satisfy the -p contract. GhostScope does not try to guess some other PID across namespaces.

This removes obviously invalid inputs early, including scenario-7-like cases where the target is simply not visible in the current /proc view.

2. Check Whether GhostScope Itself Looks Like It Runs in a Container

Depends on:

  • /.dockerenv
  • /run/.containerenv
  • /proc/1/cgroup

Result:

  • Produces container-likely, host-likely, or unknown

This is about GhostScope itself, not the target process. It mainly influences later conservative behavior, for example whether GhostScope should fail earlier when helpers are unavailable.

container-likely should be understood as a risk signal:

  • It means GhostScope's current environment may involve PID-namespace view differences, so later PID inference must be more conservative.
  • It does not prove that the target process is in a container.
  • It does not prove whether input_pid is already equal to host_pid.

Even when GhostScope runs in the host PID view, the relationship is not a mechanical identity:

  • If GhostScope runs in the host PID view, the user-entered input_pid is usually the PID seen in the host's /proc.
  • But the target process may still run inside a private PID-namespace container, so the same target may still have a second container-local PID.

3. Read NSpid and PID-Namespace Information for the Target

Depends on:

  • NSpid in /proc/<input_pid>/status
  • /proc/<input_pid>/ns/pid

These pieces of data mean different things:

  • NSpid A PID chain showing the same process in multiple PID namespaces. For the scenarios GhostScope currently cares about, it is often enough to think of it as "the host-side PID and the container-side PID of the same process".
  • /proc/<pid>/ns/pid The PID namespace object that the process currently belongs to. GhostScope reads its dev and inode not to get another PID, but to uniquely identify which PID namespace this is.

Example:

  • If /proc/<pid>/status contains NSpid: 81234 17
  • then it can be approximated as:
    • 81234 in the host / initial PID namespace
    • 17 in an inner PID namespace

The important distinction is:

  • NSpid describes PID-number relationships for the same process across different views
  • /proc/<pid>/ns/pid describes which PID namespace object the process belongs to

Result:

  • proc_pid The PID used for /proc/<pid>/maps in the current /proc view. In the current implementation, this is usually the same as input_pid.
  • host_pid The first element of NSpid, corresponding to the host / initial PID namespace.
  • PID namespace inode / dev
  • Whether NSpid exposes an explicit mapping

These outputs are used for different purposes:

  • proc_pid For userspace access to /proc/<pid>/maps, /proc/<pid>/status, and similar files
  • host_pid To align with the PID view returned by traditional bpf_get_current_pid_tgid()
  • PID namespace dev/inode To pass into bpf_get_ns_current_pid_tgid(), telling eBPF to interpret the current task from that PID-namespace view
  • Whether NSpid is explicit To decide whether fallback to host-side PID filtering is still reliable when helpers are unavailable

The most important thing about this step is not "we found another PID", but that GhostScope uses it to answer two questions:

  • Is the PID seen in the current /proc view the same as the PID in the host / initial PID namespace?
  • If not, which PID-namespace view should eBPF use when interpreting the current task?

This is the key step for distinguishing scenarios:

  • If the target is in the initial PID namespace, the case is usually closer to scenarios 1, 2, or 4.
  • If the target is not in the initial PID namespace but is still visible in the current /proc, the case is closer to scenarios 3 or 5.
  • If NSpid is missing or incomplete, later fallback safety becomes much more sensitive.

4. Probe Whether the Kernel Supports the Namespace-Aware Helper

Depends on:

  • Whether the kernel supports bpf_get_ns_current_pid_tgid

Result:

  • If the helper is available, GhostScope can retrieve PID/TGID in the requested PID namespace and perform safer filtering.
  • If the helper is unavailable, GhostScope has to rely more on NSpid, current /proc visibility, and other namespace information.
  • Traditional bpf_get_current_pid_tgid() returns pid/tgid in the kernel's default PID view. In container terms, this can be approximated as the host / initial PID namespace view rather than container-local PID values.

This step decides whether GhostScope can perform namespace-aware PID filtering directly.

5. Choose a PID Filtering Strategy

Depends on:

  • GhostScope's own runtime-environment classification
  • Whether NSpid provides an explicit host mapping
  • target PID-namespace information
  • helper availability

Result:

  • In the current implementation, GhostScope effectively chooses between two filter forms: a host-view TGID filter and a namespace-aware TGID filter.
  • If the helper is available and GhostScope concludes that namespace-aware filtering is actually needed for this -p run, it uses the namespace-aware form.
  • If namespace-aware filtering is not considered necessary for the current case, GhostScope may still keep using host-view PID filtering even when the helper is available.
  • If the helper is unavailable, GhostScope considers falling back to host-view PID filtering.
  • If the target is still in the initial PID namespace, for example in a --pid=host case, then host-view PID filtering is still safe even without the helper.
  • GhostScope only fails fast when the environment looks container-like, the helper is unavailable, NSpid does not provide an explicit mapping, and the target is not in the initial PID namespace.

This is exactly where scenarios 2 and 3 tend to diverge:

  • A --pid=host container still looks container-like, but the target may remain in the initial PID namespace.
  • A private PID-namespace container usually needs more explicit namespace information or helper support.

6. Keep Kernel-Event PID and /proc PID Aligned at Runtime

Depends on:

  • event_pid from kernel events
  • proc_pid from the current /proc view

Result:

  • When inserting offsets, caching per-PID state, and cleaning up on exit, GhostScope must keep using the same PID-key semantics.
  • If it writes using proc_pid, then it must be able to recover that same proc_pid later during cleanup, otherwise stale cache or stale offset entries remain behind.

This step does not decide which scenario the run belongs to, but it determines whether the PID meaning inferred earlier stays consistent throughout runtime.

Common Misconceptions

Misconception 1: Users Should Always Enter the Host PID

No.

The correct rule is:

  • Enter the PID visible where you are running ghostscope -p.

Do not manually convert it into a host PID. Do not manually convert it into a container-local PID either.

Misconception 2: If Something Runs in a Container, Its PID Must Differ from the Host

No.

If the container uses --pid=host, then the container and the host already share the same PID namespace.

Misconception 3: If GhostScope Knows the Target Runs in a Container, It Can Always Infer the Right Mapping

No.

What actually determines whether the mapping is reliable is:

  • GhostScope's current PID-namespace view
  • whether the target is visible in the current /proc
  • whether the helper is available
  • whether NSpid provides enough explicit mapping information

-t Mode and sysmon

What sysmon Is

sysmon is GhostScope's runtime pipeline for tracking process lifecycle state.

It mainly listens for:

  • exec
  • fork
  • exit
  • map changes (mmap, mprotect, munmap, mremap) when runtime module refresh is enabled

In the current implementation, standalone -t starts this pipeline by default unless enable_sysmon_for_target = false is set in config. -p mode uses a narrower sysmon path for watched-PID map changes. A combined -t <path> -p <pid> run uses the -p process context rather than target-mode sysmon: -t scopes function/source/address target resolution to one module, while -p supplies the concrete process mappings and PID filter. sysmon mainly serves standalone -t mode, especially when GhostScope needs to keep module offsets, runtime backtrace modules, allowlists, and exit cleanup up to date after the target starts.

Which PID View sysmon Depends On

sysmon's kernel events come from tracepoints. The event_pid in those events is not read from the current /proc view. It is populated from bpf_get_current_pid_tgid() >> 32.

This means:

  • event_pid aligns with the TGID in the host / initial PID namespace view
  • in -t semantics, it aligns with the host / initial PID namespace PID language, not the current /proc view
  • it cannot directly replace proc_pid

But the userspace side of sysmon still needs proc_pid for:

  • reading /proc/<pid>/maps
  • prefilling module offsets
  • cleaning caches and pinned map entries keyed by proc_pid

So in -t mode, sysmon actually depends on two PID languages at the same time:

  • event_pid on the kernel-event side
  • proc_pid on the current /proc side

Why -t Becomes Problematic in Containers

If GhostScope and the target stay in the same PID namespace, or if the current /proc view can reliably map event_pid back to the same proc_pid, then the pipeline can still work.

But when GhostScope and the target do not share the same PID view, problems appear:

  • sysmon receives a host-view event_pid first
  • userspace can only access the current environment's proc_pid
  • those two values are not guaranteed to be the same

This directly affects:

  • whether GhostScope can reliably find the right /proc/<pid>/maps after exec / fork
  • whether offset insertion and exit cleanup still use the same PID key

So the core -t problem in cross-PID-namespace cases is not "did GhostScope receive events?" but:

  • even when events arrive, the relationship between event_pid and proc_pid may not be recoverable in a stable way

Once that mapping cannot be recovered, the sysmon lifecycle pipeline breaks.

Current Conclusion for -t

Today, -t is better understood in three broad categories:

  • host-aligned cases, where event_pid and the current /proc view already line up well enough for lifecycle maintenance; these are the clearly supported -t paths
  • cases where GhostScope and the target still share enough PID-view information to recover the right proc_pid, but correctness depends on helper availability and runtime mapping quality; these are the conditionally supported -t paths
  • cross-PID-namespace cases without a stable shared mapping source between runtime event_pid and the /proc-side proc_pid; these are not currently reliable enough to treat as supported -t paths

That is why:

  • event_pid can be aligned with the host / initial PID namespace view
  • but it cannot directly replace proc_pid
  • and -t cannot simply reuse the same PID semantics as -p in container-heavy environments

There is one nested case worth calling out explicitly:

  • In the "outer container -> child container" topology, backtrace -t is now part of supported container-e2e coverage instead of being skipped.
  • The hard part is still structural: sysmon may first learn a host-view event_pid or an innermost namespace TGID, while proc_module_offsets remains keyed by the outer container's /proc view (proc_pid), because that map is populated from /proc/<pid>/maps in GhostScope's current userspace environment.
  • The current implementation handles that by publishing offsets for the resolved runtime PID candidates and maintaining pid_aliases from runtime lookup PIDs back to the outer-container proc_pid.
  • Treat this as partial target-mode coverage: backtrace cases validate the alias path, but globals_target nested -t cases still skip until the global-variable path has the same stable mapping guarantee.

Current container e2e exercises host-to-container, same-container, and outer-container-to-child-container backtrace -t paths. The nested child-container path remains the highest-risk lifecycle-maintenance case, so it stays in the full container-e2e CI matrix.

Topic 2: WSL

GhostScope does not currently support WSL as a runtime target.

The problem is that WSL's PID semantics do not line up with GhostScope's assumptions:

  • bpf_get_current_pid_tgid() may report a PID/TGID that does not match the PID visible inside the WSL distro.
  • bpf_get_ns_current_pid_tgid() is also not a general fix for this on WSL.
  • In current WSL + Docker container-topology validation, GhostScope teardown also hit kernel perf cleanup hangs after timeout, with stacks including perf_event_detach_bpf_prog, perf_event_free_bpf_prog, and __fput.

So this is currently a platform limitation, not a normal container-mapping case that GhostScope can reliably work around.

Relevant background:

Topic 3: Deployment Scope When GhostScope Itself Runs in a Container

GhostScope does not currently plan to support "run GhostScope in a container and observe arbitrary processes on the machine" as a deployment mode.

The main reason is observability scope:

  • If GhostScope itself runs inside a container, it may simply be unable to see processes that live outside that container's PID namespace.
  • The main exception is when GhostScope runs in a --pid=host container, because that container shares the host PID namespace.

So today's container support should be understood more narrowly:

  • GhostScope can run on the host and observe target processes that happen to run inside containers on that host. This is the primary container story.
  • GhostScope can run inside a container and observe processes in that same container PID namespace.
  • Descendant / nested PID namespaces that remain visible from that container are still within the intended scope, and -p plus backtrace -t now have dedicated "outer container -> child container target" validation paths in the full container-e2e CI matrix.
  • Nested child-container -t remains partial coverage because runtime events can surface host-view or innermost namespace PIDs while proc_module_offsets is keyed by the outer container's /proc PID view; backtrace cases use resolved runtime PID candidates and targeted pid_aliases, while globals_target nested -t cases still skip.
  • GhostScope can run inside a --pid=host container and observe host-visible processes because the PID view is shared with the host.

Current Implementation Limitations Summary

Shared Runtime Limits

  • proc_module_offsets is always populated from GhostScope's current userspace /proc/<pid>/maps view, so its stable lookup key follows proc_pid, not event_pid, and in nested cases not necessarily the innermost container_pid.
  • In container PID-namespace environments, if the namespace-aware helper is unavailable, $pid/$tid in scripts may reflect host-namespace values rather than the PID visible inside the container.

-p-Specific Limits

  • In -p mode, GhostScope currently decides in this order: runtime environment detection -> NSpid parsing -> helper probe -> filter strategy selection.
  • The current implementation does not switch to namespace-aware PID filtering solely because helper bpf_get_ns_current_pid_tgid (id 120) is available.
  • Instead, in -p mode GhostScope currently chooses between host-view TGID filtering and namespace-aware TGID filtering based on runtime-environment classification, resolved PID mapping, and helper availability together.
  • If the helper is unavailable, GhostScope falls back to host PID mapping derived from NSpid, but only when that mapping is explicit enough to be trusted.
  • -p must refer to a PID visible in the current PID namespace. If the PID is not visible in the current /proc, GhostScope fails immediately rather than guessing across namespaces.
  • The current implementation is intentionally stricter in one more case: in a container-like environment, if the helper is unavailable and NSpid cannot provide an explicit host mapping, GhostScope fails instead of guessing, unless the target remains in the initial PID namespace.
  • For scenario 6, namespace-aware PID filtering must distinguish the current /proc PID view from the target's innermost container_pid; when that mapping cannot be established safely, GhostScope still fails fast.

-t-Specific Limits

  • -t depends on sysmon to maintain runtime process lifecycle and module-map state. sysmon's event_pid comes from bpf_get_current_pid_tgid() >> 32 and aligns with host-view PID semantics.
  • In cross-PID-namespace cases, -t must keep event_pid, proc_pid, and proc_module_offsets aligned strongly enough for /proc reads, offset insertion, runtime module refresh, and exit cleanup. That alignment is still the structural weak point.
  • Nested child-container -t coverage is partial in e2e. Backtrace cases run, but globals_target cases still skip because they need a stable mapping between runtime/event PID candidates and the outer container's proc_pid; proc_module_offsets is populated from the outer /proc/<pid>/maps view used for global-variable and shared-library rebasing.

These limitations do not change the user contract defined earlier. They describe what the current implementation can support reliably, and where it will deliberately refuse to guess.