Skip to content

feat(mxc): warn when the ETW consumer receives zero provider events - #3499

Open
pkhodade-NV wants to merge 4 commits into
windowsfrom
feat/mxc-etw-zero-events-watchdog
Open

pkhodade-NV wants to merge 4 commits into
windowsfrom
feat/mxc-etw-zero-events-watchdog

Conversation

@pkhodade-NV

Copy link
Copy Markdown
Collaborator

Summary

  • The ETW->OCSF audit pipeline is meant to decode OS "Sandboxing" ETW events during a real sandbox lifecycle into a durable OCSF JSONL audit log. In a real-world repro, EnableTraceEx2/StartTraceW both reported success, the consumer thread ran correctly, but zero OS-sourced events were ever decoded across two full, successful sandbox lifecycles -- with no error, warning, or diagnostic anywhere pointing at why.
  • Root cause (already traced): EnableTraceEx2 success only proves the request to enable the provider succeeded, not that the provider exists on this host/build or will ever actually fire. A provider-identity mismatch (or a non-firing provider) left the audit trail silently empty. This PR is specifically about detection and surfacing, not diagnosing the underlying GUID/provider question, which is an environment/OS-build question outside what static code changes can resolve.

Related Issue

No linked issue -- this is a localized observability fix (a missing diagnostic for an existing, real failure mode) to the MXC ETW consumer.

Changes

  • etw_consumer.rs: CaptureHealth gains an events_matched: AtomicU64 counter, incremented on the consumer thread for every raw event that made it through the callback's provider-GUID filter, regardless of whether TDH decode later succeeds. Exposed via a new EtwSession::events_received() accessor, alongside the existing is_capture_alive().
  • AttributionIndex gains a total_launches counter (never decremented by forget), so the watchdog can tell "real sandbox activity has happened" apart from "the index is currently non-empty" (which forget clears on every normal sandbox completion).
  • Added a zero-events watchdog on the consumer thread's existing 200ms poll loop: once total_launches() > 0 and a generous grace period (30s) elapses with events_matched still at zero, log a tracing::warn! with an actionable message and emit a Detection Finding [2004] (severity: High, is_alert: true) into the OCSF stream itself, so the gap is visible in the audit log the same way the driver's own internal events already are.
  • Gated on actual sandbox activity (not just session uptime), so a gateway configured with etw_audit=true that simply hasn't created any sandboxes yet never warns -- only "activity happened, nothing arrived" does.
  • The watchdog's decision (should_warn_zero_events) is extracted into a small pure function, so it's directly unit-testable without a real ETW session or elevation.

Testing

  • New tests: total_launches_survives_forget, zero_events_watchdog_stays_quiet_without_sandbox_activity, zero_events_watchdog_stays_quiet_once_any_event_matched, zero_events_watchdog_waits_out_the_grace_period, zero_events_watchdog_fires_once_grace_elapses.
  • Full crate suite passes: cargo test -p openshell-driver-mxc --target x86_64-pc-windows-msvc --lib.

Checklist

  • Tests added for the new behavior
  • No unrelated changes bundled in

Originally opened as GitLab MR !117 against our internal mirror; re-opened here against windows for upstream review. Rebased onto windows's independently-evolved etw_consumer.rs (queue-overload reporting, PID-reuse rewrite, and byte-tracking additions that landed there since this MR was authored) -- both sets of changes are preserved side by side.

EnableTraceEx2 succeeding only proves the request to enable the
Sandboxing provider succeeded, not that the provider exists on this
host/build or will ever fire. A provider-identity mismatch or a
non-firing provider left the ETW->OCSF audit trail silently empty
across real, successful sandbox lifecycles, with no error, warning,
or diagnostic anywhere.

Track raw provider-matched events received per session and add a
zero-events watchdog on the consumer thread: once real sandbox
activity has happened (register_launch called at least once) and a
grace period elapses with zero events matched, log a warning and
emit a Detection Finding [2004] naming the gap. Gated on actual
activity (not just session uptime) so an idle gateway with
etw_audit=true and no sandboxes created never warns.

Also exposes EtwSession::events_received() alongside the existing
is_capture_alive(), so a status/diagnostics surface can query capture
health directly, not just infer it from tracing output.

The watchdog's decision logic is extracted into a pure function
(should_warn_zero_events) so it's unit-testable without a real ETW
session or elevation.

Signed-off-by: Prashant Khodade <pkhodade@nvidia.com>
(cherry picked from commit 4fcfa716a808eaa91c6476e9c69366f91702b1fa)
@copy-pr-bot

copy-pr-bot Bot commented Sep 20, 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.

…ommit

warn_zero_events_received() referenced a nonexistent SESSION_NAME
constant (only SESSION_NAME_PREFIX exists), so the crate failed to
compile. Separately, the consumer thread's decode-or-log match on
Option<DecodedEtwEvent> tripped clippy::single_match_else under -D
warnings. Neither issue is specific to a platform or toolchain
version -- both reproduce on a clean checkout of this branch's tip.

Signed-off-by: Prashant Khodade <pkhodade@nvidia.com>
…ot PID

The Sandboxing provider's events are logged under two PIDs that are
never the driver's own wxc-exec PID: a short-lived launcher PID
(wxc-exec.exe exits within seconds even while the sandboxed workload
keeps running) and a shared, constant PID hosted by a system-wide OS
broker service across unrelated sandboxes. Anchoring attribution on
`register_launch`'s wxc_pid therefore left every event unattributable,
so the OCSF audit trail stayed empty despite the provider firing
correctly (confirmed against a raw logman/tracerpt capture running
alongside this consumer).

wxc-exec never reports its OS-generated `identity`/`__TlgCV__` back to
the driver, so there is nothing to pre-seed `by_identity` with at
registration time the way `by_pid` is pre-seeded. Track launches still
awaiting their first identity/CV in a `pending_launches` queue instead,
and bind opportunistically in `resolve()`: when an event carries a
never-seen identity/CV and has no `by_pid` registration at all (the
real-world shape of these events) and exactly one launch is pending,
it can only be that launch's burst. Zero or multiple pending launches
stay ambiguous and fall through to the existing unresolved-event
buffer/TTL path rather than guess -- misattributing an audit event to
the wrong sandbox_id is worse than dropping it. A PID registration
that does exist (even generation-mismatched) is treated as positive
evidence of an existing PID-reuse race and takes precedence over the
opportunistic path, preserving the existing generation-key guarantees.

Signed-off-by: Prashant Khodade <pkhodade@nvidia.com>
An event that ages out of the unresolved-event buffer unattributed is
a permanent audit-trail gap: the OS action it represents will never
appear in the OCSF log, and nothing retries it afterward. That was
only visible at --log-level debug, so an operator running with the
default level would never see it. Promote it to warn, matching the
severity already used for the zero-events watchdog's own gap warning.

Signed-off-by: Prashant Khodade <pkhodade@nvidia.com>
@pkhodade-NV pkhodade-NV self-assigned this Sep 21, 2026

@shailendra-nv shailendra-nv left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Requesting changes for audit integrity and operability.

The singleton pending-launch fallback can attribute unrelated system-wide Sandboxing-provider activity to an OpenShell sandbox, and the new per-record warnings can flood operator logs. The current README also states that records without generation-backed evidence remain unattributed, which no longer matches this implementation.

Verification evidence at commit 92d856f0e6e1104eb72eb04444d4fd6dfdc59b29:

  • PASS: mise run --skip-tools windows:check:arm64 (native ARM64 Windows workspace check; exit 0)
  • PASS: cargo fmt --all -- --check
  • PASS: git diff --check
  • Reviewed the complete current PR diff and the surrounding ETW attribution, buffering, tests, and MXC documentation.
  • Not run: a live MXC/ETW runtime scenario; the static behavior below is directly exercised by the added singleton-attribution unit test.

Please keep attribution fail-closed until there is authoritative sandbox-generation correlation, rate-limit or aggregate unattributed-event warnings, and update the MXC observability documentation for the final behavior (including the new mxc-etw-zero-events finding).

if self.pending_launches.len() != 1 {
return None;
}
self.pending_launches.pop_front().map(|(sid, _)| sid)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Before this branch, records without generation-backed evidence remained unattributed. Here, any fresh identity/CV from an unregistered or system-broker PID is assigned to the sole pending OpenShell launch. The surrounding comment correctly notes that unrelated non-OpenShell AppContainer or UAC activity shares this system-wide provider, so an event in the five-second window can cross-link an unrelated identity and emit subsequent events under the wrong sandbox_id. That corrupts the audit trail. Please keep this fail-closed until there is authoritative correlation, such as the driver-owned process or relay reporting the identity/CV; queue cardinality is not attribution evidence.

for p in drained {
if now.duration_since(p.at) > PENDING_TTL {
tracing::debug!(target: "mxc_etw", pid = p.ev.process_id, "dropping unattributed (aged out) {}", p.ev.summary());
tracing::warn!(target: "mxc_etw", pid = p.ev.process_id, "dropping unattributed (aged out) {}", p.ev.summary());

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This changes expected system-wide unattributed activity from debug-only to one default warning per dropped record, with no aggregation or rate limit. Bursts of unrelated AppContainer or UAC events can therefore flood operator logs indefinitely and repeat event summaries. Please aggregate or rate-limit these coverage-gap warnings and report counts, similar to OverloadReporter, rather than warning for every record.

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.

2 participants