-
Notifications
You must be signed in to change notification settings - Fork 56
Consume the Windows Learning Mode trace API: FFI loader + capture lifecycle #661
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Richie Gomez (richiemsft)
merged 14 commits into
main
from
user/saulg/consume-learning-mode-api
Jul 27, 2026
Merged
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
dea582b
PR3(pr3-ffi): add learning_mode_windows runtime FFI loader + capabiliβ¦
richiemsft 67ceaf4
PR3(pr3-ffi): add lm_probe example; validated on GE_CURRENT x64 VM
richiemsft c147e22
learning_mode_windows: add 2-phase security-environment FFI
richiemsft 970028f
learning_mode_windows: add CaptureSession RAII lifecycle
richiemsft 69707a3
learning_mode_windows: add lm_capture end-to-end validation example
richiemsft 6d64d41
Address PR #661 review: lossless ETL path + fail-fast environment handle
richiemsft 2c61156
Remove internal OS build references
richiemsft 2d981b4
Harden Learning Mode failure handling
richiemsft 695cd40
Address Learning Mode teardown review feedback
richiemsft ac18b01
Clarify Learning Mode API surfaces
richiemsft ae7ea9a
Validate Learning Mode output paths
richiemsft d909fe3
Expose security environment launch function type
richiemsft 5c2aec9
Preserve security environment ownership on close failure
richiemsft 85c2945
Reuse lossless Windows string conversion
richiemsft File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| [package] | ||
| name = "learning_mode_windows" | ||
|
richiemsft marked this conversation as resolved.
|
||
| version.workspace = true | ||
| edition.workspace = true | ||
| license.workspace = true | ||
|
|
||
| [dependencies] | ||
| thiserror = { workspace = true } | ||
|
|
||
| [target.'cfg(target_os = "windows")'.dependencies] | ||
| wxc_common = { workspace = true } | ||
| windows = { workspace = true } | ||
| windows-core = { workspace = true } | ||
|
|
||
| [target.'cfg(target_os = "windows")'.dev-dependencies] | ||
| sandbox_spec = { workspace = true } | ||
| flatbuffers = { workspace = true } | ||
232 changes: 232 additions & 0 deletions
232
src/backends/learning_mode/windows/examples/lm_capture.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,232 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| //! End-to-end validation for the Learning Mode capture lifecycle, independent of the | ||
| //! MXC runner and the `captureDenials` config. | ||
| //! | ||
| //! It drives the full 2-phase sequence against a real child process: | ||
| //! | ||
| //! 1. build a minimal FlatBuffer sandbox spec with the `permissiveLearningMode` | ||
| //! capability (the token the OS learning-mode path recognises), | ||
| //! 2. [`CaptureSession::begin`] β create the security environment + start the trace, | ||
| //! 3. launch `cmd.exe` inside the environment via | ||
| //! `CreateProcessAsUserInsideSecurityEnvironment`, | ||
| //! 4. wait for it to exit, | ||
| //! 5. [`CaptureSession::finish`] β seal the ETL to a temp path + close the environment, | ||
| //! 6. assert the ETL file was produced (non-empty). | ||
| //! | ||
| //! Run on a feature-enabled Windows build (elevated): | ||
| //! | ||
| //! ```text | ||
| //! cargo run -p learning_mode_windows --example lm_capture | ||
| //! ``` | ||
| //! | ||
| //! Exit codes: `0` = ETL produced; `2` = API unavailable / off-feature build; `1` = a | ||
| //! step failed. | ||
|
|
||
| #[cfg(not(target_os = "windows"))] | ||
| fn main() { | ||
| eprintln!("lm_capture is Windows-only"); | ||
| std::process::exit(2); | ||
| } | ||
|
|
||
| #[cfg(target_os = "windows")] | ||
| fn main() { | ||
| std::process::exit(windows_impl::run()); | ||
| } | ||
|
|
||
| #[cfg(target_os = "windows")] | ||
| mod windows_impl { | ||
| use std::path::PathBuf; | ||
|
|
||
| use flatbuffers::FlatBufferBuilder; | ||
| use learning_mode_windows::{ | ||
| CaptureSession, LearningModeApi, SecurityEnvironmentApi, | ||
| PROCESS_SECURITY_ENVIRONMENT_FLAG_NONE, | ||
| }; | ||
| use sandbox_spec::base_container_layout::{ | ||
| finish_sandbox_spec_buffer, SandboxSpec, SandboxSpecArgs, | ||
| }; | ||
| use windows::Win32::Foundation::{CloseHandle, HANDLE, WAIT_FAILED, WAIT_OBJECT_0}; | ||
| use windows::Win32::System::Threading::{ | ||
| GetExitCodeProcess, WaitForSingleObject, INFINITE, PROCESS_INFORMATION, STARTUPINFOW, | ||
| }; | ||
|
|
||
| /// Matches the schema version BaseContainer embeds in every spec payload. | ||
| const SANDBOX_SPEC_VERSION: &str = "0.1.0"; | ||
|
|
||
| /// Build a minimal FlatBuffer `SandboxSpec` carrying the learning-mode capability. | ||
| fn build_sandbox_spec() -> Vec<u8> { | ||
| let mut builder = FlatBufferBuilder::with_capacity(256); | ||
| let version = builder.create_string(SANDBOX_SPEC_VERSION); | ||
| // `permissiveLearningMode` is the capability the SandboxEngine functest uses to | ||
| // exercise the learning-mode trace; it reliably drives recorded events. | ||
| let capabilities = builder.create_string("permissiveLearningMode"); | ||
| let spec = SandboxSpec::create( | ||
| &mut builder, | ||
| &SandboxSpecArgs { | ||
| version: Some(version), | ||
| app_container: true, | ||
| capabilities: Some(capabilities), | ||
| ..Default::default() | ||
| }, | ||
| ); | ||
| finish_sandbox_spec_buffer(&mut builder, spec); | ||
| builder.finished_data().to_vec() | ||
| } | ||
|
|
||
| /// Null-terminated, mutable UTF-16 command line for the child. | ||
| fn wide_command_line() -> Vec<u16> { | ||
| let cmd = r#"cmd.exe /c echo Hello from the learning-mode sandbox & whoami"#; | ||
| cmd.encode_utf16().chain(std::iter::once(0)).collect() | ||
| } | ||
|
|
||
| fn etl_output_path() -> PathBuf { | ||
| std::env::temp_dir().join(format!("lm_capture_{}.etl", std::process::id())) | ||
| } | ||
|
|
||
| pub fn run() -> i32 { | ||
| let secenv_api = match SecurityEnvironmentApi::load() { | ||
| Ok(api) => api, | ||
| Err(e) => { | ||
| eprintln!("SecurityEnvironmentApi::load failed (off-feature build?): {e}"); | ||
| return 2; | ||
| } | ||
| }; | ||
| let learning_mode_api = match LearningModeApi::load() { | ||
| Ok(api) => api, | ||
| Err(e) => { | ||
| eprintln!("LearningModeApi::load failed (off-feature build?): {e}"); | ||
| return 2; | ||
| } | ||
| }; | ||
|
|
||
| let spec = build_sandbox_spec(); | ||
| println!("built sandbox spec: {} bytes", spec.len()); | ||
|
|
||
| let session = match CaptureSession::begin( | ||
| secenv_api, | ||
| learning_mode_api, | ||
| &spec, | ||
| PROCESS_SECURITY_ENVIRONMENT_FLAG_NONE, | ||
| ) { | ||
| Ok(session) => session, | ||
| Err(e) => { | ||
| eprintln!("CaptureSession::begin failed: {e}"); | ||
| return 1; | ||
| } | ||
| }; | ||
| println!("CaptureSession::begin OK β environment + trace live"); | ||
|
|
||
| let exit_code = match launch_and_wait(&secenv_api, session.environment()) { | ||
| Ok(code) => { | ||
| println!("child exited with code {code}"); | ||
| code | ||
| } | ||
| Err(e) => { | ||
| eprintln!("launch failed: {e}"); | ||
| // `session` drops here β trace discarded + environment closed. | ||
| return 1; | ||
| } | ||
| }; | ||
| let _ = exit_code; | ||
|
|
||
| let etl_path = etl_output_path(); | ||
| if let Err(e) = session.finish(Some(&etl_path)) { | ||
| eprintln!("CaptureSession::finish failed: {e}"); | ||
| return 1; | ||
| } | ||
| println!("CaptureSession::finish OK β trace sealed, environment closed"); | ||
|
|
||
| match std::fs::metadata(&etl_path) { | ||
| Ok(meta) => { | ||
| println!( | ||
| "ETL produced: {} ({} bytes)", | ||
| etl_path.display(), | ||
| meta.len() | ||
| ); | ||
| if meta.len() == 0 { | ||
| eprintln!("ETL validation failed: file is empty"); | ||
| 1 | ||
| } else { | ||
| 0 | ||
| } | ||
| } | ||
| Err(e) => { | ||
| eprintln!("expected ETL at {} but none found: {e}", etl_path.display()); | ||
| 1 | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// Launch the child inside `environment` and wait for it to exit, returning its exit | ||
| /// code. | ||
| fn launch_and_wait( | ||
|
richiemsft marked this conversation as resolved.
|
||
| secenv_api: &SecurityEnvironmentApi, | ||
| environment: HANDLE, | ||
| ) -> Result<u32, String> { | ||
| let launch = secenv_api.launch_fn(); | ||
| let mut cmd = wide_command_line(); | ||
|
|
||
| // SAFETY: a zeroed STARTUPINFOW with only `cb` set is valid; the child inherits | ||
| // the caller's console for stdio (no STARTF_USESTDHANDLES). | ||
| let mut startup_info: STARTUPINFOW = unsafe { std::mem::zeroed() }; | ||
| startup_info.cb = u32::try_from(std::mem::size_of::<STARTUPINFOW>()) | ||
| .map_err(|_| "STARTUPINFOW size overflow".to_string())?; | ||
| let mut process_information: PROCESS_INFORMATION = unsafe { std::mem::zeroed() }; | ||
|
|
||
| // SAFETY: `launch` was resolved from processmodel.dll and matches the declared C | ||
| // signature. `cmd` is a mutable, null-terminated UTF-16 buffer; `startup_info` | ||
| // and `process_information` are valid; `environment` is the live handle from the | ||
| // session. `lpEnvironment` is null, so CREATE_UNICODE_ENVIRONMENT is not needed. | ||
| let ok = unsafe { | ||
| launch( | ||
| HANDLE(std::ptr::null_mut()), // userToken: caller context | ||
| std::ptr::null(), // applicationName (from command line) | ||
| cmd.as_mut_ptr(), // commandLine | ||
| 0, // creationFlags | ||
| std::ptr::null(), // environment | ||
| std::ptr::null(), // currentDirectory | ||
| &startup_info, | ||
| environment, | ||
| &mut process_information, | ||
| ) | ||
| }; | ||
| if ok == 0 { | ||
| // SAFETY: reads the calling thread's last-error slot. | ||
| let err = unsafe { windows::Win32::Foundation::GetLastError() }; | ||
| return Err(format!( | ||
| "CreateProcessAsUserInsideSecurityEnvironment failed (GetLastError = {})", | ||
| err.0 | ||
| )); | ||
| } | ||
|
|
||
| // SAFETY: `hProcess` is a valid process handle returned by the launch. | ||
| let wait = unsafe { WaitForSingleObject(process_information.hProcess, INFINITE) }; | ||
| let result = if wait == WAIT_OBJECT_0 { | ||
| let mut exit_code: u32 = 0; | ||
| // SAFETY: `hProcess` is valid and the process has signalled exit. | ||
| unsafe { GetExitCodeProcess(process_information.hProcess, &mut exit_code) } | ||
| .map(|()| exit_code) | ||
| .map_err(|e| format!("GetExitCodeProcess failed: {e}")) | ||
| } else if wait == WAIT_FAILED { | ||
| // SAFETY: reads the last-error value set by WaitForSingleObject. | ||
| let err = unsafe { windows::Win32::Foundation::GetLastError() }; | ||
| Err(format!( | ||
| "WaitForSingleObject failed (GetLastError = {})", | ||
| err.0 | ||
| )) | ||
| } else { | ||
| Err(format!( | ||
| "WaitForSingleObject returned unexpected status: {wait:?}" | ||
| )) | ||
| }; | ||
|
|
||
| // SAFETY: both handles were returned by the launch and are not used again. | ||
| unsafe { | ||
| let _ = CloseHandle(process_information.hThread); | ||
| let _ = CloseHandle(process_information.hProcess); | ||
| } | ||
| result | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| //! Manual validation probe for the Learning Mode trace + security-environment API. | ||
| //! | ||
| //! Prints whether `processmodel.dll` on this machine exposes the Learning Mode trace | ||
| //! exports (`StartLearningModeTrace` / `StopLearningModeTrace`) and the 2-phase | ||
| //! security-environment exports (`CreateProcessSecurityEnvironment` / | ||
| //! `CreateProcessAsUserInsideSecurityEnvironment` / `CloseProcessSecurityEnvironment`), | ||
| //! reporting the exact resolved name for each (plain vs `Experimental_`). Intended to | ||
| //! be run on a feature-enabled Windows build to confirm the runtime FFI resolves | ||
| //! against the real API. | ||
| //! | ||
| //! ```text | ||
| //! cargo run -p learning_mode_windows --example lm_probe | ||
| //! ``` | ||
|
|
||
| fn main() { | ||
| std::process::exit(run_probe()); | ||
| } | ||
|
|
||
| #[cfg(target_os = "windows")] | ||
| fn run_probe() -> i32 { | ||
| let learning_mode_available = learning_mode_windows::is_learning_mode_api_available(); | ||
| println!("is_learning_mode_api_available = {learning_mode_available}"); | ||
|
|
||
| match learning_mode_windows::LearningModeApi::load() { | ||
| Ok(api) => println!("LearningModeApi::load = OK ({api:?})"), | ||
| Err(e) => println!("LearningModeApi::load = ERR ({e})"), | ||
| } | ||
|
|
||
| let secenv_available = learning_mode_windows::is_security_environment_api_available(); | ||
| println!("is_security_environment_api_available = {secenv_available}"); | ||
|
|
||
| let report = learning_mode_windows::probe_security_environment_exports(); | ||
| println!(" create export = {:?}", report.create); | ||
| println!(" launch export = {:?}", report.launch); | ||
| println!(" close export = {:?}", report.close); | ||
|
|
||
| match learning_mode_windows::SecurityEnvironmentApi::load() { | ||
| Ok(api) => println!("SecurityEnvironmentApi::load = OK ({api:?})"), | ||
| Err(e) => println!("SecurityEnvironmentApi::load = ERR ({e})"), | ||
| } | ||
|
|
||
| if learning_mode_available && secenv_available { | ||
| 0 | ||
| } else { | ||
| 2 | ||
| } | ||
| } | ||
|
|
||
| #[cfg(not(target_os = "windows"))] | ||
| fn run_probe() -> i32 { | ||
| println!("is_learning_mode_api_available = false"); | ||
| 2 | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.