Skip to main content

xchecker_runner/
wsl.rs

1use crate::error::RunnerError;
2use std::ffi::OsStr;
3use std::process::Stdio;
4use std::time::Duration;
5
6use super::{CommandSpec, ProcessOutput, ProcessRunner};
7
8// ============================================================================
9// WslRunner - Secure WSL Process Execution
10// ============================================================================
11
12/// WSL process runner for Windows.
13///
14/// `WslRunner` provides secure process execution via WSL using argv-style APIs only.
15/// It wraps commands with `wsl.exe --exec` to execute them in a WSL distribution
16/// without shell interpretation.
17///
18/// # Security
19///
20/// `WslRunner` enforces the following security properties:
21/// - Uses `wsl.exe --exec` with argv-style argument passing only
22/// - Arguments are passed as discrete `OsString` elements via `CommandSpec`
23/// - NO shell string concatenation of user data
24/// - NO `sh -c` or shell string evaluation
25/// - Arguments are validated/normalized at trust boundaries
26///
27/// # Platform Support
28///
29/// `WslRunner` is only functional on Windows. On non-Windows platforms,
30/// it will return an error indicating WSL is not available.
31///
32/// # Example
33///
34/// ```rust,no_run
35/// use xchecker_utils::runner::{WslRunner, ProcessRunner, CommandSpec};
36/// use std::time::Duration;
37///
38/// let runner = WslRunner::new();
39/// let cmd = CommandSpec::new("echo")
40///     .arg("hello")
41///     .arg("world");
42///
43/// // On Windows with WSL, this executes: wsl.exe --exec echo hello world
44/// let output = runner.run(&cmd, Duration::from_secs(30)).unwrap();
45/// ```
46#[derive(Debug, Clone, Default)]
47pub struct WslRunner {
48    /// Optional specific WSL distro to use (e.g., "Ubuntu-22.04")
49    pub distro: Option<String>,
50}
51
52impl WslRunner {
53    /// Create a new `WslRunner` using the default WSL distribution.
54    ///
55    /// # Example
56    ///
57    /// ```rust
58    /// use xchecker_utils::runner::WslRunner;
59    ///
60    /// let runner = WslRunner::new();
61    /// ```
62    #[must_use]
63    pub const fn new() -> Self {
64        Self { distro: None }
65    }
66
67    /// Create a new `WslRunner` targeting a specific WSL distribution.
68    ///
69    /// # Arguments
70    ///
71    /// * `distro` - The name of the WSL distribution (e.g., "Ubuntu-22.04")
72    ///
73    /// # Example
74    ///
75    /// ```rust
76    /// use xchecker_utils::runner::WslRunner;
77    ///
78    /// let runner = WslRunner::with_distro("Ubuntu-22.04");
79    /// ```
80    #[must_use]
81    pub fn with_distro(distro: impl Into<String>) -> Self {
82        Self {
83            distro: Some(distro.into()),
84        }
85    }
86
87    /// Validate an argument for WSL execution.
88    ///
89    /// This function validates arguments at trust boundaries to ensure they
90    /// don't contain characters that could cause issues in WSL execution.
91    ///
92    /// # Security
93    ///
94    /// While `wsl.exe --exec` uses argv-style execution (no shell), we still
95    /// validate arguments to:
96    /// - Reject null bytes (which could truncate arguments)
97    /// - Ensure arguments are valid UTF-8 or valid OS strings
98    ///
99    /// # Arguments
100    ///
101    /// * `arg` - The argument to validate
102    ///
103    /// # Returns
104    ///
105    /// * `Ok(())` - The argument is valid
106    /// * `Err(RunnerError)` - The argument contains invalid characters
107    fn validate_argument(arg: &OsStr) -> Result<(), RunnerError> {
108        // Check for null bytes which could truncate arguments
109        let arg_bytes = arg.as_encoded_bytes();
110        if arg_bytes.contains(&0) {
111            return Err(RunnerError::WslExecutionFailed {
112                reason: "Argument contains null byte which is not allowed".to_string(),
113            });
114        }
115        Ok(())
116    }
117
118    /// Build a `CommandSpec` for WSL execution.
119    ///
120    /// This method transforms the input `CommandSpec` into a WSL-wrapped command
121    /// using `wsl.exe --exec` with argv-style argument passing.
122    ///
123    /// # Security
124    ///
125    /// - Uses `--exec` flag which bypasses shell interpretation
126    /// - Arguments are passed as discrete elements, not concatenated strings
127    /// - All arguments are validated before being added to the command
128    ///
129    /// # Arguments
130    ///
131    /// * `cmd` - The original command specification to wrap
132    ///
133    /// # Returns
134    ///
135    /// * `Ok(CommandSpec)` - The WSL-wrapped command specification
136    /// * `Err(RunnerError)` - An argument failed validation
137    fn build_wsl_command(&self, cmd: &CommandSpec) -> Result<CommandSpec, RunnerError> {
138        // Validate all arguments at the trust boundary
139        Self::validate_argument(&cmd.program)?;
140        for arg in &cmd.args {
141            Self::validate_argument(arg)?;
142        }
143
144        // Build the WSL command using argv-style APIs only
145        // Command structure: wsl.exe [-d <distro>] --exec <program> <args...>
146        let mut wsl_cmd = CommandSpec::new("wsl");
147
148        // Add distro specification if provided (using discrete args, not string concat)
149        if let Some(ref distro) = self.distro {
150            wsl_cmd = wsl_cmd.arg("-d").arg(distro);
151        }
152
153        // Add --exec flag to bypass shell interpretation
154        // This is critical for security - it ensures arguments are passed directly
155        // to the target program without shell evaluation
156        wsl_cmd = wsl_cmd.arg("--exec");
157
158        // Add the original program as a discrete argument
159        wsl_cmd = wsl_cmd.arg(&cmd.program);
160
161        // Add all original arguments as discrete elements
162        // NO string concatenation occurs here - each arg is a separate element
163        for arg in &cmd.args {
164            wsl_cmd = wsl_cmd.arg(arg);
165        }
166
167        // Preserve working directory if specified
168        if let Some(ref cwd) = cmd.cwd {
169            wsl_cmd = wsl_cmd.cwd(cwd);
170        }
171
172        // Preserve environment variables if specified
173        if let Some(ref env) = cmd.env {
174            for (key, value) in env {
175                wsl_cmd = wsl_cmd.env(key, value);
176            }
177        }
178
179        Ok(wsl_cmd)
180    }
181}
182
183impl ProcessRunner for WslRunner {
184    /// Execute a command via WSL using argv-style APIs.
185    ///
186    /// This implementation:
187    /// - Wraps the command with `wsl.exe --exec` (no shell)
188    /// - Uses `Command::new().args()` only (no shell string evaluation)
189    /// - Validates arguments at trust boundaries
190    /// - Handles timeout via thread-based waiting
191    /// - Captures stdout and stderr
192    ///
193    /// # Arguments
194    ///
195    /// * `cmd` - The command specification to execute
196    /// * `timeout` - Maximum duration to wait for the process to complete
197    ///
198    /// # Returns
199    ///
200    /// * `Ok(ProcessOutput)` - The process completed (possibly with non-zero exit code)
201    /// * `Err(RunnerError::Timeout)` - The process timed out
202    /// * `Err(RunnerError::WslExecutionFailed)` - Failed to spawn or wait for process
203    /// * `Err(RunnerError::WslNotAvailable)` - WSL is not available (non-Windows platform)
204    ///
205    /// # Security
206    ///
207    /// This method builds a WSL command using `build_wsl_command()` which:
208    /// - Uses `--exec` to bypass shell interpretation
209    /// - Passes arguments as discrete elements via `CommandSpec`
210    /// - Validates all arguments at trust boundaries
211    /// - NO shell string concatenation of user data occurs
212    fn run(&self, cmd: &CommandSpec, timeout: Duration) -> Result<ProcessOutput, RunnerError> {
213        // WSL is only available on Windows
214        if !cfg!(target_os = "windows") {
215            return Err(RunnerError::WslNotAvailable {
216                reason: "WSL is only available on Windows".to_string(),
217            });
218        }
219
220        use std::sync::mpsc;
221        use std::thread;
222
223        // Build the WSL-wrapped command using argv-style APIs
224        let wsl_cmd = self.build_wsl_command(cmd)?;
225
226        // Convert to std::process::Command using argv-style APIs only
227        let mut command = wsl_cmd.to_command();
228        command
229            .stdin(Stdio::null())
230            .stdout(Stdio::piped())
231            .stderr(Stdio::piped());
232
233        // Spawn the process
234        let child = command
235            .spawn()
236            .map_err(|e| RunnerError::WslExecutionFailed {
237                reason: format!(
238                    "Failed to spawn WSL process for '{}': {}",
239                    cmd.program.to_string_lossy(),
240                    e
241                ),
242            })?;
243
244        // Get the child's PID for potential termination
245        let child_id = child.id();
246
247        // Create a channel for the result
248        let (tx, rx) = mpsc::channel();
249
250        // Spawn a thread to wait for the process
251        let handle = thread::spawn(move || {
252            let output = child.wait_with_output();
253            let _ = tx.send(output);
254        });
255
256        // Wait for the result with timeout
257        match rx.recv_timeout(timeout) {
258            Ok(output_result) => {
259                // Process completed within timeout
260                let _ = handle.join();
261
262                let output = output_result.map_err(|e| RunnerError::WslExecutionFailed {
263                    reason: format!("Failed to wait for WSL process: {e}"),
264                })?;
265
266                Ok(ProcessOutput::new(
267                    output.stdout,
268                    output.stderr,
269                    output.status.code(),
270                    false,
271                ))
272            }
273            Err(mpsc::RecvTimeoutError::Timeout) => {
274                // Timeout occurred - attempt to terminate the process
275                Self::terminate_wsl_process(child_id);
276
277                // Wait for the thread to finish (it should complete after termination)
278                let _ = handle.join();
279
280                Err(RunnerError::Timeout {
281                    timeout_seconds: timeout.as_secs(),
282                })
283            }
284            Err(mpsc::RecvTimeoutError::Disconnected) => {
285                // Thread panicked or channel was closed unexpectedly
286                Err(RunnerError::WslExecutionFailed {
287                    reason: "WSL process monitoring thread terminated unexpectedly".to_string(),
288                })
289            }
290        }
291    }
292}
293
294impl WslRunner {
295    /// Terminate a WSL process by its PID.
296    ///
297    /// On Windows, uses TerminateProcess to kill the wsl.exe process,
298    /// which will also terminate the child process in WSL.
299    fn terminate_wsl_process(pid: u32) {
300        #[cfg(windows)]
301        {
302            use windows::Win32::Foundation::CloseHandle;
303            use windows::Win32::System::Threading::{
304                OpenProcess, PROCESS_TERMINATE, TerminateProcess,
305            };
306
307            unsafe {
308                if let Ok(handle) = OpenProcess(PROCESS_TERMINATE, false, pid) {
309                    let _ = TerminateProcess(handle, 1);
310                    let _ = CloseHandle(handle);
311                }
312            }
313        }
314
315        #[cfg(not(windows))]
316        {
317            // No-op on non-Windows platforms
318            let _ = pid;
319        }
320    }
321}
322
323#[cfg(test)]
324mod tests {
325    use super::*;
326    use proptest::prelude::*;
327    use std::ffi::OsString;
328    use std::path::PathBuf;
329
330    // ============================================================================
331    // WslRunner Tests (FR-SEC-17, FR-SEC-18)
332    // ============================================================================
333
334    #[test]
335    fn test_wsl_runner_new() {
336        let runner = WslRunner::new();
337        assert!(runner.distro.is_none());
338    }
339
340    #[test]
341    fn test_wsl_runner_with_distro() {
342        let runner = WslRunner::with_distro("Ubuntu-22.04");
343        assert_eq!(runner.distro, Some("Ubuntu-22.04".to_string()));
344    }
345
346    #[test]
347    fn test_wsl_runner_default() {
348        let runner = WslRunner::default();
349        assert!(runner.distro.is_none());
350    }
351
352    #[test]
353    fn test_wsl_runner_clone() {
354        let runner = WslRunner::with_distro("Ubuntu");
355        let cloned = runner.clone();
356        assert_eq!(cloned.distro, runner.distro);
357    }
358
359    #[test]
360    fn test_wsl_runner_implements_process_runner() {
361        // Verify WslRunner implements ProcessRunner trait
362        fn assert_process_runner<T: ProcessRunner>(_: &T) {}
363
364        let runner = WslRunner::new();
365        assert_process_runner(&runner);
366    }
367
368    #[test]
369    fn test_wsl_runner_build_command_basic() {
370        let runner = WslRunner::new();
371        let cmd = CommandSpec::new("echo").arg("hello").arg("world");
372
373        let wsl_cmd = runner.build_wsl_command(&cmd).unwrap();
374
375        // Should be: wsl --exec echo hello world
376        assert_eq!(wsl_cmd.program, OsString::from("wsl"));
377        assert_eq!(wsl_cmd.args.len(), 4);
378        assert_eq!(wsl_cmd.args[0], OsString::from("--exec"));
379        assert_eq!(wsl_cmd.args[1], OsString::from("echo"));
380        assert_eq!(wsl_cmd.args[2], OsString::from("hello"));
381        assert_eq!(wsl_cmd.args[3], OsString::from("world"));
382    }
383
384    #[test]
385    fn test_wsl_runner_build_command_with_distro() {
386        let runner = WslRunner::with_distro("Ubuntu-22.04");
387        let cmd = CommandSpec::new("echo").arg("test");
388
389        let wsl_cmd = runner.build_wsl_command(&cmd).unwrap();
390
391        // Should be: wsl -d Ubuntu-22.04 --exec echo test
392        assert_eq!(wsl_cmd.program, OsString::from("wsl"));
393        assert_eq!(wsl_cmd.args.len(), 5);
394        assert_eq!(wsl_cmd.args[0], OsString::from("-d"));
395        assert_eq!(wsl_cmd.args[1], OsString::from("Ubuntu-22.04"));
396        assert_eq!(wsl_cmd.args[2], OsString::from("--exec"));
397        assert_eq!(wsl_cmd.args[3], OsString::from("echo"));
398        assert_eq!(wsl_cmd.args[4], OsString::from("test"));
399    }
400
401    #[test]
402    fn test_wsl_runner_build_command_preserves_cwd() {
403        let runner = WslRunner::new();
404        let cmd = CommandSpec::new("ls").cwd("/home/user");
405
406        let wsl_cmd = runner.build_wsl_command(&cmd).unwrap();
407
408        assert_eq!(wsl_cmd.cwd, Some(PathBuf::from("/home/user")));
409    }
410
411    #[test]
412    fn test_wsl_runner_build_command_preserves_env() {
413        let runner = WslRunner::new();
414        let cmd = CommandSpec::new("env").env("MY_VAR", "my_value");
415
416        let wsl_cmd = runner.build_wsl_command(&cmd).unwrap();
417
418        let env = wsl_cmd.env.as_ref().unwrap();
419        assert_eq!(
420            env.get(&OsString::from("MY_VAR")),
421            Some(&OsString::from("my_value"))
422        );
423    }
424
425    #[test]
426    fn test_wsl_runner_build_command_shell_metacharacters_preserved() {
427        // Verify that shell metacharacters are preserved as discrete arguments
428        // This is critical for security - no shell injection
429        let runner = WslRunner::new();
430        let cmd = CommandSpec::new("echo")
431            .arg("$(whoami)")
432            .arg("`id`")
433            .arg("${HOME}")
434            .arg("$PATH")
435            .arg("arg;with;semicolons")
436            .arg("arg|with|pipes")
437            .arg("arg&with&ampersands");
438
439        let wsl_cmd = runner.build_wsl_command(&cmd).unwrap();
440
441        // All arguments should be preserved literally as discrete elements
442        // wsl --exec echo $(whoami) `id` ${HOME} $PATH arg;with;semicolons arg|with|pipes arg&with&ampersands
443        assert_eq!(wsl_cmd.args[2], OsString::from("$(whoami)"));
444        assert_eq!(wsl_cmd.args[3], OsString::from("`id`"));
445        assert_eq!(wsl_cmd.args[4], OsString::from("${HOME}"));
446        assert_eq!(wsl_cmd.args[5], OsString::from("$PATH"));
447        assert_eq!(wsl_cmd.args[6], OsString::from("arg;with;semicolons"));
448        assert_eq!(wsl_cmd.args[7], OsString::from("arg|with|pipes"));
449        assert_eq!(wsl_cmd.args[8], OsString::from("arg&with&ampersands"));
450    }
451
452    #[test]
453    fn test_wsl_runner_validate_argument_rejects_null_bytes() {
454        // Arguments with null bytes should be rejected
455        let arg_with_null = OsString::from("hello\0world");
456        let result = WslRunner::validate_argument(&arg_with_null);
457
458        assert!(result.is_err());
459        match result {
460            Err(RunnerError::WslExecutionFailed { reason }) => {
461                assert!(reason.contains("null byte"));
462            }
463            _ => panic!("Expected WslExecutionFailed error"),
464        }
465    }
466
467    #[test]
468    fn test_wsl_runner_validate_argument_accepts_valid_args() {
469        // Valid arguments should be accepted
470        let valid_args = [
471            "simple",
472            "with spaces",
473            "with-dashes",
474            "with_underscores",
475            "with.dots",
476            "/path/to/file",
477            "C:\\Windows\\Path",
478            "unicode: 日本語",
479            "emoji: 🎉",
480            "--flag=value",
481            "-v",
482            "$(not-expanded)",
483            "`backticks`",
484            "${variable}",
485        ];
486
487        for arg in valid_args {
488            let os_arg = OsString::from(arg);
489            let result = WslRunner::validate_argument(&os_arg);
490            assert!(result.is_ok(), "Argument '{}' should be valid", arg);
491        }
492    }
493
494    #[test]
495    fn test_wsl_runner_build_command_rejects_null_in_program() {
496        let runner = WslRunner::new();
497        let cmd = CommandSpec::new("echo\0bad");
498
499        let result = runner.build_wsl_command(&cmd);
500
501        assert!(result.is_err());
502        match result {
503            Err(RunnerError::WslExecutionFailed { reason }) => {
504                assert!(reason.contains("null byte"));
505            }
506            _ => panic!("Expected WslExecutionFailed error"),
507        }
508    }
509
510    #[test]
511    fn test_wsl_runner_build_command_rejects_null_in_args() {
512        let runner = WslRunner::new();
513        let cmd = CommandSpec::new("echo")
514            .arg("valid")
515            .arg("has\0null")
516            .arg("also valid");
517
518        let result = runner.build_wsl_command(&cmd);
519
520        assert!(result.is_err());
521        match result {
522            Err(RunnerError::WslExecutionFailed { reason }) => {
523                assert!(reason.contains("null byte"));
524            }
525            _ => panic!("Expected WslExecutionFailed error"),
526        }
527    }
528
529    #[cfg(not(target_os = "windows"))]
530    #[test]
531    fn test_wsl_runner_returns_error_on_non_windows() {
532        // On non-Windows platforms, WslRunner should return an error
533        let runner = WslRunner::new();
534        let cmd = CommandSpec::new("echo").arg("test");
535
536        let result = runner.run(&cmd, Duration::from_secs(10));
537
538        assert!(result.is_err());
539        match result {
540            Err(RunnerError::WslNotAvailable { reason }) => {
541                assert!(reason.contains("only available on Windows"));
542            }
543            _ => panic!("Expected WslNotAvailable error"),
544        }
545    }
546
547    #[test]
548    fn test_wsl_runner_no_string_concatenation() {
549        // This test verifies that arguments are passed as discrete elements
550        // and no string concatenation occurs
551        let runner = WslRunner::with_distro("TestDistro");
552        let cmd = CommandSpec::new("program")
553            .arg("arg1")
554            .arg("arg2 with spaces")
555            .arg("arg3;semicolon");
556
557        let wsl_cmd = runner.build_wsl_command(&cmd).unwrap();
558
559        // Verify each argument is a discrete element
560        // The command should be: wsl -d TestDistro --exec program arg1 "arg2 with spaces" "arg3;semicolon"
561        // But stored as discrete OsString elements, not concatenated
562
563        // Count total args: -d, TestDistro, --exec, program, arg1, arg2 with spaces, arg3;semicolon
564        assert_eq!(wsl_cmd.args.len(), 7);
565
566        // Each argument should be exactly what we passed, not concatenated
567        assert_eq!(wsl_cmd.args[4], OsString::from("arg1"));
568        assert_eq!(wsl_cmd.args[5], OsString::from("arg2 with spaces"));
569        assert_eq!(wsl_cmd.args[6], OsString::from("arg3;semicolon"));
570    }
571
572    #[test]
573    fn test_wsl_runner_command_construction() {
574        // This test verifies that WslRunner correctly wraps commands with --exec
575        // to bypass shell interpretation.
576
577        let runner = WslRunner::new();
578        let cmd = CommandSpec::new("echo").arg("hello").arg("world");
579
580        let wsl_cmd = runner
581            .build_wsl_command(&cmd)
582            .expect("Failed to build WSL command");
583
584        // Verify program is wsl
585        assert_eq!(wsl_cmd.program, OsString::from("wsl"));
586
587        // Verify arguments structure: --exec echo hello world
588        let args: Vec<String> = wsl_cmd
589            .args
590            .iter()
591            .map(|s| s.to_string_lossy().to_string())
592            .collect();
593
594        assert_eq!(args[0], "--exec");
595        assert_eq!(args[1], "echo");
596        assert_eq!(args[2], "hello");
597        assert_eq!(args[3], "world");
598
599        // Verify no shell wrapping (sh -c, etc)
600        for arg in &args {
601            assert!(!arg.contains("sh -c"));
602            assert!(!arg.contains("cmd /C"));
603        }
604    }
605
606    #[test]
607    fn test_wsl_runner_with_distro_command_construction() {
608        let runner = WslRunner::with_distro("Ubuntu-22.04");
609        let cmd = CommandSpec::new("ls").arg("-la");
610
611        let wsl_cmd = runner
612            .build_wsl_command(&cmd)
613            .expect("Failed to build WSL command");
614
615        let args: Vec<String> = wsl_cmd
616            .args
617            .iter()
618            .map(|s| s.to_string_lossy().to_string())
619            .collect();
620
621        // Verify structure: -d Ubuntu-22.04 --exec ls -la
622        assert_eq!(args[0], "-d");
623        assert_eq!(args[1], "Ubuntu-22.04");
624        assert_eq!(args[2], "--exec");
625        assert_eq!(args[3], "ls");
626        assert_eq!(args[4], "-la");
627    }
628
629    #[test]
630    fn test_wsl_runner_argument_validation() {
631        // Verify that arguments with null bytes are rejected
632        let runner = WslRunner::new();
633
634        // Create a string with a null byte
635        let cmd = CommandSpec::new("echo").arg("hello\0world");
636
637        let result = runner.build_wsl_command(&cmd);
638        assert!(result.is_err());
639
640        if let Err(RunnerError::WslExecutionFailed { reason }) = result {
641            assert!(reason.contains("null byte"));
642        } else {
643            panic!("Expected WslExecutionFailed error");
644        }
645    }
646
647    proptest! {
648        #![proptest_config(ProptestConfig::with_cases(100))]
649        #[test]
650        fn test_wsl_runner_safety_property(
651            program in any::<String>(),
652            args in prop::collection::vec(any::<String>(), 0..10),
653            distro in prop::option::of(any::<String>())
654        ) {
655            // Property 17: WSL runner safety
656            // Validates: Requirements FR-SEC-4
657
658            let mut runner = WslRunner::new();
659            if let Some(ref d) = distro {
660                runner = WslRunner::with_distro(d.clone());
661            }
662
663            let mut cmd = CommandSpec::new(&program);
664            for arg in &args {
665                cmd = cmd.arg(arg);
666            }
667
668            let result = runner.build_wsl_command(&cmd);
669
670            // Check for null bytes in inputs
671            let has_null = program.contains('\0') || args.iter().any(|a| a.contains('\0'));
672
673            if has_null {
674                // Must fail if null bytes are present
675                prop_assert!(result.is_err());
676            } else {
677                // Must succeed if no null bytes
678                prop_assert!(result.is_ok());
679                let wsl_cmd = result.unwrap();
680
681                // Verify structure
682                prop_assert_eq!(wsl_cmd.program, OsString::from("wsl"));
683
684                let mut expected_args_len = 1; // --exec
685                let mut arg_idx = 0;
686
687                // Check distro args
688                if let Some(ref d) = runner.distro {
689                    prop_assert_eq!(&wsl_cmd.args[arg_idx], &OsString::from("-d"));
690                    prop_assert_eq!(&wsl_cmd.args[arg_idx+1], &OsString::from(d));
691                    arg_idx += 2;
692                    expected_args_len += 2;
693                }
694
695                // Check --exec
696                prop_assert_eq!(&wsl_cmd.args[arg_idx], &OsString::from("--exec"));
697                arg_idx += 1;
698
699                // Check program
700                prop_assert_eq!(&wsl_cmd.args[arg_idx], &OsString::from(&program));
701                arg_idx += 1;
702                expected_args_len += 1;
703
704                // Check args
705                for (i, arg) in args.iter().enumerate() {
706                    prop_assert_eq!(&wsl_cmd.args[arg_idx + i], &OsString::from(arg));
707                }
708                expected_args_len += args.len();
709
710                prop_assert_eq!(wsl_cmd.args.len(), expected_args_len);
711            }
712        }
713    }
714}