Skip to content

feat(sandbox): emit OCSF process launch and exit events with parent pid - #5143

Open
vineethsai7 wants to merge 2 commits into
NVIDIA:mainfrom
vineethsai7:feat/4268-v1
Open

vineethsai7 wants to merge 2 commits into
NVIDIA:mainfrom
vineethsai7:feat/4268-v1

Conversation

@vineethsai7

@vineethsai7 vineethsai7 commented Nov 2, 2026 •

Copy link
Copy Markdown

Summary

Add opt-in OCSF launch and exit records for the Linux sandbox entrypoint and processes started directly by sandbox exec or SSH. The records carry PID, parent PID, and a stable process instance ID without argv or environment values.

Related Issue

Part of #4268. The issue is still state:triage-needed; this draft proposes the first slice and welcomes maintainer feedback on the design and scope.

#5144 builds on this PR and adds watched file open observations.

Changes

  • Register process_activity_enabled as a boolean sandbox setting, defaulting to false, and capture it when the sandbox starts.
  • Capture /proc/<pid>/stat field 22 before reaping a directly started process. Encode process.uid as pid:start_ticks; include the parent instance ID and epoch start time when available.
  • Add optional process identity fields to the existing boundary start, exec, and takeover snapshot messages. The supervisor emits Process Activity events through its existing OCSF log path, including the gateway WatchSandbox stream and optional JSONL output. The boundary also writes them to its container log.
  • Encode process start times as protobuf Timestamp fields while keeping millisecond values in the internal process identity and OCSF object.
  • Retain identity for exit, report exit code or signal, observe exec exits even when SSH consumes only the output stream, and suppress duplicate supervisor exit records on repeated waits.
  • Document the setting, event shape, and current coverage.

Design

The boundary already owns process creation and wait status, so it can capture PID and start tick before a short-lived process disappears. The supervisor already sends OCSF events to WatchSandbox. Optional identity fields connect those two existing paths and let a replacement supervisor retain an adopted process's identity. This avoids a new telemetry service.

Periodic /proc polling would miss short-lived processes and exec replacements. A kernel event source could cover descendants and exec replacements, but it would add a separate privilege and delivery path. This PR keeps that larger design open for maintainer review.

Examples

Enable the setting before starting a sandbox, then read the gateway stream:

openshell settings set --global --key process_activity_enabled --value true
openshell logs my-sandbox --source sandbox

Example shorthand records:

OCSF PROC:LAUNCH [INFO] /bin/sh(57) [ppid:1] [start:1793620000120] [process_uid:57:123456] [parent_uid:1:34]
OCSF PROC:TERMINATE [INFO] /bin/sh(57) [ppid:1] [start:1793620000120] [process_uid:57:123456] [parent_uid:1:34] [exit:0]

WatchSandbox carries the shorthand log line. When supervisor OCSF JSONL
export is enabled, the corresponding JSON includes the same identity in
process:

{"class_uid":1007,"activity_name":"Launch","process":{"name":"/bin/sh","pid":57,"uid":"57:123456","created_time":1793620000120,"parent_process":{"name":"unknown","pid":1,"uid":"1:34"}}}

Compatibility and limits

The setting is off by default. The existing entrypoint launch event remains when it is off. New protobuf fields are optional, so older boundary implementations continue to work and simply supply no identity. Existing OCSF fields remain unchanged; uid and created_time are additive. No argv, command line, or environment is sent across the protocol or added to these records.

Events are best effort through the existing bounded log pipeline. This slice observes only processes directly started by the boundary, including piped and PTY sessions. It does not claim a complete workload process tree.
Signal exits carry unmapped.signal in JSON and [signal:N] in shorthand.

Follow-ups

  • Observe workload forks, descendants, and in-place exec replacements, with explicit coverage for the Linux compute drivers.
  • Join process instance IDs to network events and expose counted source and pipeline drops, so consumers can distinguish a quiet sandbox from an incomplete audit stream.
  • Add driver-level tests for lifecycle coverage and telemetry loss when an approved test environment is available.

Review follow-up

Review found a P2 protocol compatibility issue: the new public process start times were scalar milliseconds, contrary to the repository's public time-field rule. They now use protobuf Timestamp, with tested millisecond conversion and rejection of fractional milliseconds. The setting remains off by default; identity fields are optional, and neither arguments nor environment values enter these records. Launch and exit use the same captured process instance. The direct-child coverage and best-effort delivery limits remain documented above.

Release Notes

Linux sandboxes can opt in to process launch and exit records in the sandbox log stream with process_activity_enabled=true. The setting is captured at sandbox startup.

Testing

  • cargo fmt --all -- --check on the final head
  • cargo clippy --all-targets -- -D warnings for the six touched crates
  • Serial cargo test for the six touched crates on the final head, including new OCSF and boundary lifecycle tests. The parallel suite had one tracing-capture test failure; that test passes alone and in the serial suite.
  • cargo test -p openshell-server --lib public_time_fields_use_well_known_types -- --test-threads=1 on the final head (with the build box's LIBRARY_PATH set for libz3); the regression failed before the Timestamp fix and passes now.
  • Docs navigation, Fern check (0 errors; warnings for an untouched configuration page, skipped redirect authentication, and theme contrast), Markdown lint (0 errors), and Buf lint
  • E2E tests added/updated (follow-up; no driver test host used)

Checklist

  • Follows Conventional Commits
  • Commits are signed off (DCO)
  • Architecture docs updated (if applicable)

Manual verification

Result: PASS — The Linux boundary emitted the new opt-in launch and exit records for its directly started entrypoint and exec child, with stable PID, parent PID, process UID, and start time. The gateway log path was not exercised.

Environment

  • Ubuntu 24.04.5 ARM64 stack host (dc-ubuntu-eh), Linux 7.0, one disposable local account with a private HOME. No service, container runtime, or default port was changed.
  • Before: upstream main at be5e49499 (the PR's merge base). After: PR head 155c68ac14. Both test binaries were built on the build box with Rust/Cargo 1.95.0 and a Zig 0.14 ARM64 GNU linker, using dcb up-os-4268 and nice -n 15 cargo build --release --tests -p openshell-sandbox --target aarch64-unknown-linux-gnu -q.
  • The existing control_restart_replays_running_lifecycle_exactly_once test was temporarily instrumented in both checkouts to capture OCSF tracing events, print only selected process fields and the test PID, and wait for the terminated entrypoint. No product code was changed. The instrumentation was removed and the worktree restored to a clean PR head after the runs.

Steps

  1. Under the disposable account, run each commit's ARM64 test binary with --exact boundary_server::linux::tests::control_restart_replays_running_lifecycle_exactly_once --nocapture. The test starts a real /bin/sleep entrypoint, runs a short-lived /bin/sh through boundary exec, consumes its output, waits twice for its status, then terminates and waits for the entrypoint. The PR test sets process_activity_enabled=true; main has no such setting.
  2. Compare captured OCSF class 1007 records for those two direct children. Both runs passed: 1 passed; 0 failed.
  3. On the PR binary, run process_audit::tests:: on the same host. Its three tests passed, including the short-lived child identity check after reaping.

Before: upstream main be5e49499

The test process PID was 374963. The only matching process record was the existing entrypoint launch; there was no /bin/sh record and no termination record:

Launch /bin/sleep  pid=374971  parent_pid=null  uid=null  created_time=null

After: PR head 155c68ac14

The test process PID was 374643. Both children named that exact PID as parent. Launch and exit retained each child's PID, UID, parent UID, and start time:

Launch    /bin/sleep  pid=374651  parent_pid=374643  uid=374651:5054766  parent_uid=374643:5054740  created_time=1793681047660
Launch    /bin/sh     pid=374656  parent_pid=374643  uid=374656:5054767  parent_uid=374643:5054740  created_time=1793681047670
Terminate /bin/sh     pid=374656  parent_pid=374643  uid=374656:5054767  parent_uid=374643:5054740  created_time=1793681047670  exit_code=0
Terminate /bin/sleep  pid=374651  parent_pid=374643  uid=374651:5054766  parent_uid=374643:5054740  created_time=1793681047660  signal=9

The captured process.cmd_line field was absent in all four PR records. Repeated waits for the exec child produced one exit record.

Not covered and cleanup

  • No gateway WatchSandbox stream, supervisor JSONL export, container shorthand log, settings CLI/default path, SSH or PTY child, or supervisor takeover was run. This host has no Docker or Podman; unprivileged user namespaces are denied, and its unprivileged port floor is 1024 while the full boundary qualification requires 0. The test used the boundary's in-process harness and set the PR option in its agent spec.
  • An attempted ARM64 musl test build stopped at untouched libc ABI compile sites in child_seccomp.rs and network_broker.rs; no musl binary was run. The ARM64 GNU binaries above built and ran successfully.
  • The disposable account, HOME, binaries, and logs were deleted. Readback found no process with its UID, no account or group, no HOME, and no transfer files. The dev worktree is clean at 155c68ac14; no commit or GitHub push was made.

Signed-off-by: Vineeth Sai Narajala <vnarajal@cisco.com>
@copy-pr-bot

copy-pr-bot Bot commented Nov 2, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

Signed-off-by: Vineeth Sai Narajala <vnarajal@cisco.com>
@vineethsai7
vineethsai7 marked this pull request as ready for review November 3, 2026 05:20
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 3, 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-11-03T05:26:50.760139Z 155c68a Draft marked ready
ℹ️ 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: 155c68ac14

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

pub fn terminate_event(&self, status: BoundaryExitStatus) -> OcsfEvent {
let mut builder = ProcessActivityBuilder::new(openshell_ocsf::ctx::ctx())
.activity(ActivityId::Close)
.severity(SeverityId::Informational)

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 Mark timeout-killed process exits as Critical

When the canonical process exceeds timeout_secs, SpawnedAgent::wait sends TERM/KILL and then calls this method with the resulting signaled status, but the builder always labels the termination Informational; operators therefore cannot distinguish the repository-defined Critical timeout kill from an ordinary lifecycle exit. Carry the timeout outcome into the emitted runtime and supervisor records and use SeverityId::Critical for that path.

AGENTS.md reference: AGENTS.md:L120-L128

Useful? React with 👍 / 👎.

// separately so the exit audit record is still emitted.
let observed = process.clone();
tokio::spawn(async move {
let _ = observed.wait().await;

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 Reuse the exec stream for exit auditing

When process activity is enabled for long-lived exec or SSH sessions, this background wait() opens a second Exchange stream for each process while the exec's I/O stream from call_stream_idempotent is already held. The protocol caps the shared HTTP/2 connection at 128 streams (BOUNDARY_MAX_CONCURRENT_STREAMS) and the runtime allows 64 retained execs, so 64 audited execs alone consume all 128 slots before mediation or control traffic; subsequent exec, signal, resize, or network requests can stall until one exits. Emit from or share the existing exit-status path rather than retaining another stream per process.

Useful? React with 👍 / 👎.

This branch has not been deployed

No deployments
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