From 6976f59c8b5a777e1f7b38f3632822770666350e Mon Sep 17 00:00:00 2001 From: Alexander Sklar <22989529+asklar@users.noreply.github.com> Date: Thu, 30 Jul 2026 08:12:39 -0700 Subject: [PATCH 1/2] Add --wait-for-debugger flag for BaseContainer/AppContainer runners Hold the sandboxed child suspended (CREATE_SUSPENDED, no code run yet, not even loader init) instead of resuming immediately, so an external debugger can attach to the real sandboxed PID from outside the sandbox. wxc-exec polls CheckRemoteDebuggerPresent (no timeout) and, once a debugger attaches, clears only its own suspend increment, leaving the debugger's own attach-time freeze in place so a plain g is enough afterward. Windows-only; wired through the CLI flag, ExecutionRequest, and the BaseContainer/AppContainer suspend points. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8ed545b0-5021-4ad0-9a7e-0b5e9069b435 --- .../common/src/appcontainer_runner.rs | 18 ++- .../common/src/base_container_runner.rs | 111 +++++++++++++-- .../appcontainer/common/src/debugger_wait.rs | 134 ++++++++++++++++++ .../appcontainer/common/src/dispatcher.rs | 37 ++++- src/backends/appcontainer/common/src/lib.rs | 2 + src/core/wxc/src/main.rs | 29 ++++ src/core/wxc_common/src/config_parser.rs | 1 + src/core/wxc_common/src/models.rs | 17 +++ 8 files changed, 339 insertions(+), 10 deletions(-) create mode 100644 src/backends/appcontainer/common/src/debugger_wait.rs diff --git a/src/backends/appcontainer/common/src/appcontainer_runner.rs b/src/backends/appcontainer/common/src/appcontainer_runner.rs index 6149b301a..c1e5065b6 100644 --- a/src/backends/appcontainer/common/src/appcontainer_runner.rs +++ b/src/backends/appcontainer/common/src/appcontainer_runner.rs @@ -1339,7 +1339,23 @@ impl SandboxBackend for AppContainerScriptRunner { return Err(ScriptResponse::error(&e.to_string())); } }; - if let Err(e) = child.resume() { + + // `--wait-for-debugger`: block (no timeout) until an external + // debugger attaches to the real sandboxed PID, then clear only + // wxc-exec's own CREATE_SUSPENDED hold — see `debugger_wait` module + // docs for why this leaves the debugger's own attach-time freeze in + // place (so a plain `g` is enough, no `~0 m` needed). + if request.wait_for_debugger { + if let Err(e) = crate::debugger_wait::wait_for_debugger_then_resume( + child.process.get(), + child.thread.get(), + child.pid, + logger, + ) { + self.teardown(&mut prepared, request.lifecycle.preserve_policy, logger); + return Err(ScriptResponse::error(&e.to_string())); + } + } else if let Err(e) = child.resume() { self.teardown(&mut prepared, request.lifecycle.preserve_policy, logger); return Err(ScriptResponse::error(&e.to_string())); } diff --git a/src/backends/appcontainer/common/src/base_container_runner.rs b/src/backends/appcontainer/common/src/base_container_runner.rs index e80c5fe52..3e1492d77 100644 --- a/src/backends/appcontainer/common/src/base_container_runner.rs +++ b/src/backends/appcontainer/common/src/base_container_runner.rs @@ -1168,14 +1168,69 @@ impl BaseContainerRunner { } }; - // The child was created suspended; now that it is in the job object (so - // every descendant it spawns is captured), resume its main thread. If the - // create API ignored CREATE_SUSPENDED the thread is already running and - // this is a harmless no-op. - // SAFETY: `pi.hThread` is the just-created, still-owned main-thread - // handle; `ResumeThread` only adjusts its suspend count. - unsafe { - ResumeThread(pi.hThread); + // `--wait-for-debugger`: block (no timeout) until an external + // debugger attaches to the real sandboxed PID, then clear only + // wxc-exec's own CREATE_SUSPENDED hold — see `debugger_wait` module + // docs for why this leaves the debugger's own attach-time freeze in + // place (so a plain `g` is enough, no `~0 m` needed). Unreachable in + // practice: `validate()` rejects `--wait-for-debugger` for this + // backend before `spawn_base` runs (BaseContainer cannot guarantee + // CREATE_SUSPENDED is honored on every OS build). Handled here + // anyway so a direct/test caller that bypasses `validate()` still + // fails closed instead of silently violating the suspend guarantee. + if request.wait_for_debugger { + if let Err(e) = crate::debugger_wait::wait_for_debugger_then_resume( + pi.hProcess, + pi.hThread, + pi.dwProcessId, + logger, + ) { + let _ = writeln!( + logger, + "Error: --wait-for-debugger failed ({e}); the sandboxed child has \ + been terminated." + ); + // `wait_for_debugger_then_resume` already terminated the + // child on failure; reap it and tear down the same + // sandbox/proxy state the job-setup-failure path above does. + unsafe { + let _ = WaitForSingleObject(pi.hProcess, u32::MAX); + let _ = CloseHandle(pi.hProcess); + let _ = CloseHandle(pi.hThread); + } + if request.lifecycle.destroy_on_exit { + run_sandbox_cleanup( + &identity, + &sid_string, + request.policy.network_proxy.is_enabled(), + logger, + ); + sandbox_tracking::unregister_ctrl_c_cleanup(); + } + self.proxy_coordinator.stop(logger); + + const WAIT_FOR_DEBUGGER_FAILED_MSG: &str = + "--wait-for-debugger failed while waiting for a debugger to attach; \ + the sandboxed child was terminated."; + return Err(ScriptResponse { + exit_code: -1, + error_message: WAIT_FOR_DEBUGGER_FAILED_MSG.to_string(), + standard_err: WAIT_FOR_DEBUGGER_FAILED_MSG.to_string(), + extended_error: format!("wait_for_debugger_then_resume failed: {e}"), + failure_phase: FailurePhase::LaunchFailed, + ..Default::default() + }); + } + } else { + // The child was created suspended; now that it is in the job object (so + // every descendant it spawns is captured), resume its main thread. If the + // create API ignored CREATE_SUSPENDED the thread is already running and + // this is a harmless no-op. + // SAFETY: `pi.hThread` is the just-created, still-owned main-thread + // handle; `ResumeThread` only adjusts its suspend count. + unsafe { + ResumeThread(pi.hThread); + } } // Hand ownership to the caller via `BaseChild`, which performs @@ -1226,6 +1281,25 @@ struct BaseChild { impl SandboxBackend for BaseContainerRunner { fn validate(&self, request: &ExecutionRequest) -> Result<(), ScriptResponse> { + // `Experimental_CreateProcessInSandbox` is not guaranteed to honor + // CREATE_SUSPENDED on every OS build (see the creation-flags comment + // above), so this backend cannot guarantee the sandboxed process has + // run no code before a debugger attaches -- the core contract + // `--wait-for-debugger` promises. The dispatcher already routes + // `--wait-for-debugger` requests to an AppContainer tier + // automatically (its `CreateProcess*` contract always honors + // CREATE_SUSPENDED); this only fails closed for direct callers of + // this backend. + if request.wait_for_debugger { + return Err(ScriptResponse::error( + "--wait-for-debugger is not supported by the BaseContainer backend: \ + Experimental_CreateProcessInSandbox is not guaranteed to honor \ + CREATE_SUSPENDED on every OS build, so it cannot guarantee the \ + sandboxed process has run no code before a debugger attaches. Use \ + the AppContainer tier instead (the dispatcher does this \ + automatically for --wait-for-debugger).", + )); + } // deniedPaths reaches the OS via the SandboxSpec `fs_deny` field, honored // only when the OS advertises SANDBOX_CAP_FS_DENY. The dispatcher only // routes deny here when supported; fail closed for direct callers. @@ -1790,6 +1864,27 @@ mod tests { ); } + #[test] + fn validate_runner_rejects_wait_for_debugger() { + // BaseContainer's `Experimental_CreateProcessInSandbox` cannot + // guarantee CREATE_SUSPENDED is honored on every OS build, so this + // backend must fail closed for `--wait-for-debugger` rather than + // silently risk running code before a debugger attaches. The + // dispatcher already routes this flag to an AppContainer tier (see + // `dispatcher::tests::wait_for_debugger_skips_base_container_even_when_usable`); + // this covers a direct/test caller that bypasses the dispatcher. + let runner = BaseContainerRunner::new(); + let request = ExecutionRequest { + wait_for_debugger: true, + ..ExecutionRequest::default() + }; + + let err = runner + .validate(&request) + .expect_err("--wait-for-debugger must be rejected by the BaseContainer backend"); + assert!(err.error_message.contains("wait-for-debugger")); + } + #[test] fn validate_runner_rejects_allowed_hosts() { let runner = BaseContainerRunner::new(); diff --git a/src/backends/appcontainer/common/src/debugger_wait.rs b/src/backends/appcontainer/common/src/debugger_wait.rs new file mode 100644 index 000000000..6d582b270 --- /dev/null +++ b/src/backends/appcontainer/common/src/debugger_wait.rs @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! `--wait-for-debugger` support. +//! +//! The mechanism: the runners create the sandboxed child suspended and +//! assign it to the job object exactly as before, but instead of calling +//! `ResumeThread` immediately, they hand off to +//! [`wait_for_debugger_then_resume`], which: +//! +//! 1. Logs the PID and blocks — with **no timeout** — polling +//! `CheckRemoteDebuggerPresent` on the child's process handle. The main +//! thread has not run any code yet — not even its own loader +//! initialization, so no dependent DLL past the main image is mapped — +//! so nothing here races an operator who takes an arbitrarily long time +//! to attach. +//! 2. The instant a debugger attaches, calls `ResumeThread` exactly once. +//! +//! Why step 2 doesn't let the target run away before the operator sets +//! breakpoints: attaching a debugger (`DebugActiveProcess`) synthesizes an +//! initial batch of debug events (process/thread/module state) that stay +//! *outstanding* — and every thread stays frozen — until the debugger +//! itself acknowledges them via `ContinueDebugEvent`/`g`. That freeze is a +//! second, independent contribution to the same suspend-count field +//! `CREATE_SUSPENDED` used, which is why attaching used to leave the count +//! at 2 (our 1 + the debugger's 1) and required an operator-side `~0 m` +//! before `g`. By calling `ResumeThread` ourselves the moment attach is +//! detected, we cancel only *our own* contribution — the debugger's +//! independent freeze is still holding the thread — so all the operator +//! needs is a plain `g`. Every breakpoint set beforehand is still +//! necessarily a deferred/pending one (nothing but the main image has +//! loaded), so it resolves normally once the target actually starts running +//! after `g`. +//! +//! Once resumed this way, `wxc-exec` falls into its normal (already +//! unbounded when `script_timeout` is 0) wait for the child to exit — no +//! special-casing there. Ctrl-C during the attach-wait is handled by the +//! existing `sandbox_tracking` console-control handler (registered well +//! before this point), which runs on its own OS thread independent of this +//! poll loop. +//! +//! Failure handling: a `CheckRemoteDebuggerPresent` error (e.g. the process +//! handle going bad because the suspended child was killed externally while +//! we were polling) is a permanent condition, not "no debugger yet" — retrying +//! it forever would hang `wxc-exec` indefinitely. Likewise a failing +//! `ResumeThread` after attach must not be reported as success. Both cases +//! terminate the child and return an error, mirroring +//! `SpawnedChild::resume`'s fail-closed behavior; the caller only needs to +//! tear down its own sandbox resources and surface the error. + +use std::fmt::Write as _; +use std::time::Duration; + +use windows::Win32::Foundation::{GetLastError, HANDLE}; +use windows::Win32::System::Diagnostics::Debug::CheckRemoteDebuggerPresent; +use windows::Win32::System::Threading::{ResumeThread, TerminateProcess}; +use windows_core::BOOL; + +use wxc_common::error::WxcError; +use wxc_common::logger::Logger; + +/// How often to poll `CheckRemoteDebuggerPresent` while waiting. +const POLL_INTERVAL: Duration = Duration::from_millis(100); + +/// Block (no timeout) until a debugger attaches to `process`, then resume +/// `thread` exactly once — cancelling only the `CREATE_SUSPENDED` increment +/// `wxc-exec` itself added, leaving the debugger's own attach-time freeze in +/// place so the operator's plain `g` is what actually starts it running. +/// +/// On failure the child is terminated (fail closed) before returning +/// [`WxcError::Process`]; the caller only needs to tear down its own sandbox +/// resources and surface the error. +/// +/// # Safety +/// `process` and `thread` must be valid, still-open handles to the just +/// created, still-suspended child for the duration of this call; the caller +/// retains ownership (this function does not close either handle). +pub fn wait_for_debugger_then_resume( + process: HANDLE, + thread: HANDLE, + pid: u32, + logger: &mut Logger, +) -> Result<(), WxcError> { + let banner = format!("Process suspended, PID: {pid}. Attach a debugger to continue."); + let _ = writeln!(logger, "{banner}"); + eprintln!("{banner}"); + + loop { + let mut present = BOOL(0); + // SAFETY: `process` is a valid, open handle for the duration of this + // call (caller contract); `present` is a valid out-param on the stack. + let checked = unsafe { CheckRemoteDebuggerPresent(process, &mut present) }; + match checked { + Ok(()) if present.as_bool() => break, + Ok(()) => std::thread::sleep(POLL_INTERVAL), + Err(e) => { + // The handle went bad (e.g. the still-suspended child was + // terminated externally) -- this can never resolve on its + // own, so fail closed instead of polling forever. + // SAFETY: `process` is the caller's still-owned handle; + // terminating it before returning matches the fail-closed + // contract every other launch-failure path in the runners + // follows. + unsafe { + let _ = TerminateProcess(process, u32::MAX); + } + return Err(WxcError::Process(format!( + "CheckRemoteDebuggerPresent failed while waiting for a debugger to \ + attach to PID {pid}: {e}" + ))); + } + } + } + + // SAFETY: `thread` is the caller's still-owned, still-suspended main + // thread handle; ResumeThread only adjusts its suspend count. This + // cancels exactly the one increment wxc-exec added via CREATE_SUSPENDED + // — the debugger's own attach-time freeze (a separate contribution to + // the same counter) is unaffected and keeps the thread from actually + // running until the operator issues `g`. + let resumed = unsafe { ResumeThread(thread) }; + if resumed == u32::MAX { + let err = unsafe { GetLastError() }; + // SAFETY: same fail-closed contract as above. + unsafe { + let _ = TerminateProcess(process, u32::MAX); + } + return Err(WxcError::Process(format!( + "ResumeThread failed after debugger attach for PID {pid}: {err:?}" + ))); + } + let _ = writeln!(logger, "Process started, PID: {pid}."); + Ok(()) +} diff --git a/src/backends/appcontainer/common/src/dispatcher.rs b/src/backends/appcontainer/common/src/dispatcher.rs index 2db648327..74dcd0389 100644 --- a/src/backends/appcontainer/common/src/dispatcher.rs +++ b/src/backends/appcontainer/common/src/dispatcher.rs @@ -329,7 +329,19 @@ fn select_backend_with_fallback( ), DispatchError, > { - let decision = fallback_detector::detect(&request.policy, /*prefer_bc=*/ true)?; + // `--wait-for-debugger` needs a suspend point the Win32 `CreateProcess` + // contract actually guarantees. BaseContainer's + // `Experimental_CreateProcessInSandbox` may silently ignore + // CREATE_SUSPENDED on some OS builds (see `base_container_runner`'s + // creation-flags comment) -- which would let the sandboxed process start + // running before a debugger attaches, violating the feature's core + // guarantee. Skip Tier 1 whenever `wait_for_debugger` is requested so + // selection falls through to an AppContainer tier (2/3), whose + // `CreateProcess*` calls always honor CREATE_SUSPENDED per the documented + // Win32 contract. `BaseContainerRunner::validate` also rejects the flag + // directly, as defense-in-depth for callers that bypass this dispatcher. + let prefer_base_container = !request.wait_for_debugger; + let decision = fallback_detector::detect(&request.policy, prefer_base_container)?; let (backend, dacl_manager): (SelectedBackend, Option) = match decision.tier { IsolationTier::BaseContainer => { @@ -829,6 +841,29 @@ mod tests { assert!(matches!(d.tier, IsolationTier::AppContainerDacl)); } + #[test] + fn wait_for_debugger_skips_base_container_even_when_usable() { + // `--wait-for-debugger` must never land on BaseContainer: + // `Experimental_CreateProcessInSandbox` cannot guarantee + // CREATE_SUSPENDED is honored on every OS build, which would violate + // the flag's core "no code has run yet" guarantee. Force + // BaseContainer *usable* (bypassing real host capability) so this + // exercises the `prefer_base_container` gate itself, not host + // variance -- without the guard, Tier 1 would be selected (see + // `dispatch_t1_naturally_selected_when_bc_usable` / + // `select_backend_t1_builds_base_container_no_dacl`). + let _g = BcUsableGuard::set(true); + let mut req = test_request(empty_policy()); + req.wait_for_debugger = true; + let (backend, _dacl, tier, _warnings) = + select_backend_with_fallback(&req).expect("selection should succeed"); + assert!( + !matches!(tier, IsolationTier::BaseContainer), + "wait_for_debugger must never select BaseContainer, got {tier:?}" + ); + assert!(matches!(backend, SelectedBackend::AppContainer(_))); + } + // ------------------------------------------------------------------- // Streaming dispatch (`select_backend_with_fallback` / // `spawn_with_fallback`) — issue #643. diff --git a/src/backends/appcontainer/common/src/lib.rs b/src/backends/appcontainer/common/src/lib.rs index 361eb6790..aa619c11d 100644 --- a/src/backends/appcontainer/common/src/lib.rs +++ b/src/backends/appcontainer/common/src/lib.rs @@ -16,6 +16,8 @@ pub mod appcontainer_runner; #[cfg(target_os = "windows")] pub mod base_container_runner; #[cfg(target_os = "windows")] +pub mod debugger_wait; +#[cfg(target_os = "windows")] pub mod dispatcher; #[cfg(target_os = "windows")] pub mod fallback_detector; diff --git a/src/core/wxc/src/main.rs b/src/core/wxc/src/main.rs index aa9529df8..360f72b63 100644 --- a/src/core/wxc/src/main.rs +++ b/src/core/wxc/src/main.rs @@ -136,6 +136,31 @@ struct Cli { #[arg(long)] audit_verbose: bool, + /// Hold the sandboxed child suspended after creation — before it has + /// run any code, including its own loader initialization — instead of + /// resuming it immediately, so an external debugger can attach to the + /// *real* sandboxed process from outside the sandbox by PID, at its own + /// pace (no timeout: nothing races it, since nothing can run until a + /// debugger attaches). Unlike Image File Execution Options (IFEO), + /// which substitutes the debugger itself into the sandbox (so the + /// debugger process ends up sandboxed too and can fail for the same + /// reasons the target does), the target here stays the real process; + /// only the debugger you attach runs unsandboxed. + /// + /// Attaching a debugger adds its own +1 to the suspend count; the + /// instant wxc-exec detects the attach, it automatically clears its own + /// original CREATE_SUSPENDED increment (a single ResumeThread call), so + /// after setting breakpoints (necessarily deferred/pending ones, since + /// no dependent DLL past the main image has loaded yet), a plain `g` is + /// all you need — no `~0 m` ("Resume Thread") required. Windows-only; + /// only the AppContainer tier of the `processcontainer` backend + /// implements the suspend point (BaseContainer is skipped + /// automatically, since it cannot guarantee CREATE_SUSPENDED is honored + /// on every OS build). + #[cfg(target_os = "windows")] + #[arg(long = "wait-for-debugger")] + wait_for_debugger: bool, + /// Command to run inside the container, overriding `process.commandLine` /// from the policy. The command must follow a `--` separator so normal /// CLI flags remain usable after the config path. Examples: @@ -1019,6 +1044,10 @@ fn main() { request.experimental_enabled = cli.experimental; request.testing_features_enabled = cli.allow_testing_features; request.dry_run = cli.dry_run; + #[cfg(target_os = "windows")] + { + request.wait_for_debugger = cli.wait_for_debugger; + } // ── Telemetry init (experimental) ─────────────────────────────── let telemetry_active = if request.experimental_enabled { diff --git a/src/core/wxc_common/src/config_parser.rs b/src/core/wxc_common/src/config_parser.rs index 0cbefd500..cf1f8ded7 100644 --- a/src/core/wxc_common/src/config_parser.rs +++ b/src/core/wxc_common/src/config_parser.rs @@ -1193,6 +1193,7 @@ enforced; access denials are recorded for diagnostics.\n", testing_features_enabled: false, experimental, dry_run: false, + wait_for_debugger: false, }) } diff --git a/src/core/wxc_common/src/models.rs b/src/core/wxc_common/src/models.rs index 41ecbee61..9d6ffc78e 100644 --- a/src/core/wxc_common/src/models.rs +++ b/src/core/wxc_common/src/models.rs @@ -737,6 +737,23 @@ pub struct ExecutionRequest { /// Dry-run mode: validate config and runner setup then return success /// without executing the sandboxed process. pub dry_run: bool, + /// Set by `--wait-for-debugger`: hold the sandboxed child's main thread + /// suspended after creation (it stays at its `CREATE_SUSPENDED` suspend + /// count, having run no code at all, including its own loader + /// initialization) until an external, unsandboxed debugger attaches to + /// the real PID. The instant `wxc-exec` detects the attach it + /// automatically clears its own suspend increment (a single + /// `ResumeThread` call), leaving only the debugger's own attach-time + /// freeze in place — so after setting breakpoints (necessarily + /// deferred/pending ones, since no dependent DLL past the main image + /// has loaded), a plain `g` in the debugger is all that's needed; no + /// `~0 m` ("Resume Thread") required. `wxc-exec` then falls into its + /// normal (already-unbounded-by-default) wait for the child to exit. + /// `false` (default) is the unchanged, immediate-resume behavior. + /// Consumed by the AppContainer runner; the dispatcher never routes a + /// `wait_for_debugger` request to the BaseContainer runner, which + /// cannot guarantee `CREATE_SUSPENDED` is honored on every OS build. + pub wait_for_debugger: bool, } /// Distinguishes whether an error occurred during process creation (launch) From 082e3e8bfecbc9a8be655ecce237751980f96c8f Mon Sep 17 00:00:00 2001 From: Alexander Sklar <22989529+asklar@users.noreply.github.com> Date: Thu, 30 Jul 2026 08:12:39 -0700 Subject: [PATCH 2/2] docs: document --wait-for-debugger in README Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8ed545b0-5021-4ad0-9a7e-0b5e9069b435 --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index f14aa0efe..2b1cc9ed5 100644 --- a/README.md +++ b/README.md @@ -227,6 +227,16 @@ wxc-exec.exe --audit policy.json > **Warning:** `--audit` injects `permissiveLearningMode` — AppContainer restrictions are **not** enforced for the duration of the run. Use only for policy authoring. `learningModeLogging` and `permissiveLearningMode` are reserved internal capability names and are rejected in `processContainer.capabilities`; use `processContainer.learningMode: true` for deny-and-record mode or `--audit` for permissive mode. See [docs/learning-mode/capabilities.md](docs/learning-mode/capabilities.md) for the three learning-mode flows. +### Wait for Debugger + +`--wait-for-debugger` holds the sandboxed child suspended right after creation — before it has run any code, including its own loader initialization — instead of resuming it immediately. This lets an external, unsandboxed debugger attach to the *real* sandboxed process by PID at its own pace (there's no timeout, since nothing can run until a debugger attaches): + +```bash +wxc-exec.exe --wait-for-debugger config.json +``` + +Attach a debugger to the printed PID and set breakpoints (necessarily deferred/pending ones, since no dependent DLL past the main image has loaded yet). The instant `wxc-exec` detects the attach it automatically clears its own suspend hold, so a plain `g` is all you need afterward — no `~0 m` ("Resume Thread") required. Windows-only; only the AppContainer tier of the `processcontainer` backend implements the suspend point (BaseContainer is skipped automatically, since it cannot guarantee `CREATE_SUSPENDED` is honored on every OS build). + ## Telemetry (Experimental) MXC supports optional TraceLogging ETW telemetry for execution observability. When enabled, structured events (`MXC.Execution` and `MXC.Error`) are emitted to the local ETW subsystem via the Rust [`tracelogging`](https://crates.io/crates/tracelogging) crate. Every event includes common fields (Version, Channel, IsDebugging, `UTCReplace_AppSessionGuid`) as Part C custom event data.