• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

NVIDIA / nvrc / 20920149983

12 Jan 2026 12:55PM UTC coverage: 89.12% (-0.6%) from 89.763%
20920149983

push

github

web-flow
Merge pull request #100 from zvonkok/process

hardened_std: implement Command::* functions

186 of 248 new or added lines in 2 files covered. (75.0%)

2 existing lines in 1 file now uncovered.

1712 of 1921 relevant lines covered (89.12%)

12.42 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

75.29
/hardened_std/src/process.rs
1
// SPDX-License-Identifier: Apache-2.0
2
// Copyright (c) NVIDIA CORPORATION
3

4
//! Process execution with security-hardened restrictions
5
//!
6
//! **Security Model:**
7
//! - Only whitelisted binaries can be executed (compile-time enforcement)
8
//! - All arguments must be &'static str (no dynamic strings) to prevent injection
9
//!   attacks from runtime data - only compile-time constants are accepted
10
//! - Maximum security for ephemeral VM init process
11
//!
12
//! **Allowed binaries (production):**
13
//! - /usr/bin/nvidia-smi - GPU configuration
14
//! - /usr/bin/nvidia-ctk - Container toolkit
15
//! - /usr/sbin/modprobe - Kernel module loading
16
//! - /usr/bin/nvidia-persistenced - GPU persistence daemon
17
//! - /usr/bin/nv-hostengine - DCGM host engine
18
//! - /usr/bin/dcgm-exporter - DCGM metrics exporter
19
//! - /usr/bin/nv-fabricmanager - NVLink fabric manager
20
//! - /usr/bin/kata-agent - Kata runtime agent
21
//!
22
//! **Test binaries (cfg(test) only):**
23
//! - /bin/true, /bin/false, /bin/sleep, /bin/sh - For unit tests
24

25
use crate::{last_os_error, Error, Result};
26
use core::ffi::{c_char, c_int};
27

28
/// Check if binary is in the allowed list
29
fn is_binary_allowed(path: &str) -> bool {
29✔
30
    // Production binaries - always allowed
31
    let production_allowed = matches!(
29✔
32
        path,
29✔
33
        "/usr/bin/nvidia-smi"
29✔
34
            | "/usr/bin/nvidia-ctk"
28✔
35
            | "/usr/sbin/modprobe"
27✔
36
            | "/usr/bin/nvidia-persistenced"
26✔
37
            | "/usr/bin/nv-hostengine"
25✔
38
            | "/usr/bin/dcgm-exporter"
24✔
39
            | "/usr/bin/nv-fabricmanager"
23✔
40
            | "/usr/bin/kata-agent"
22✔
41
    );
42

43
    if production_allowed {
29✔
44
        return true;
8✔
45
    }
21✔
46

47
    // Test binaries - only allowed in test builds
48
    #[cfg(test)]
49
    {
50
        matches!(path, "/bin/true" | "/bin/false" | "/bin/sleep" | "/bin/sh")
21✔
51
    }
52
    #[cfg(not(test))]
53
    {
NEW
54
        false
×
55
    }
56
}
29✔
57

58
/// Command builder with security restrictions
59
pub struct Command {
60
    path: &'static str,
61
    args: [Option<&'static str>; 16], // Max 16 args
62
    arg_count: usize,
63
    stdout_fd: Option<c_int>,
64
    stderr_fd: Option<c_int>,
65
}
66

67
impl Command {
68
    /// Create a new Command for the given binary path.
69
    /// Returns BinaryNotAllowed error if path is not in the whitelist.
70
    pub fn new(path: &'static str) -> Result<Self> {
12✔
71
        if !is_binary_allowed(path) {
12✔
72
            return Err(Error::BinaryNotAllowed);
1✔
73
        }
11✔
74
        Ok(Self {
11✔
75
            path,
11✔
76
            args: [None; 16],
11✔
77
            arg_count: 0,
11✔
78
            stdout_fd: None,
11✔
79
            stderr_fd: None,
11✔
80
        })
11✔
81
    }
12✔
82

83
    /// Add arguments to the command.
84
    /// Arguments must be &'static str for security (no dynamic strings).
85
    /// Maximum 16 arguments supported.
86
    pub fn args(&mut self, args: &[&'static str]) -> Result<&mut Self> {
7✔
87
        if self.arg_count + args.len() > 16 {
7✔
88
            return Err(Error::InvalidInput(alloc::string::String::from(
1✔
89
                "Too many arguments (max 16)",
1✔
90
            )));
1✔
91
        }
6✔
92
        for &arg in args {
16✔
93
            self.args[self.arg_count] = Some(arg);
10✔
94
            self.arg_count += 1;
10✔
95
        }
10✔
96
        Ok(self)
6✔
97
    }
7✔
98

99
    /// Configure stdout redirection.
100
    pub fn stdout(&mut self, cfg: Stdio) -> &mut Self {
2✔
101
        self.stdout_fd = cfg.as_fd();
2✔
102
        self
2✔
103
    }
2✔
104

105
    /// Configure stderr redirection.
106
    pub fn stderr(&mut self, cfg: Stdio) -> &mut Self {
×
NEW
107
        self.stderr_fd = cfg.as_fd();
×
NEW
108
        self
×
UNCOV
109
    }
×
110

111
    /// Spawn the command as a child process.
112
    pub fn spawn(&mut self) -> Result<Child> {
9✔
113
        // SAFETY: fork() is safe here because we're in a controlled init environment
114
        let pid = unsafe { libc::fork() };
9✔
115
        if pid < 0 {
9✔
NEW
116
            return Err(last_os_error());
×
117
        }
9✔
118

119
        if pid == 0 {
9✔
120
            // Child process - setup stdio and exec
NEW
121
            self.setup_stdio();
×
NEW
122
            let _ = self.do_exec();
×
123
            // If exec fails, exit child
NEW
124
            unsafe { libc::_exit(1) };
×
125
        }
9✔
126

127
        // Parent process
128
        Ok(Child { pid })
9✔
129
    }
9✔
130

131
    /// Execute the command, blocking until completion.
132
    pub fn status(&mut self) -> Result<ExitStatus> {
6✔
133
        let mut child = self.spawn()?;
6✔
134
        child.wait()
6✔
135
    }
6✔
136

137
    /// Replace current process with the command (exec).
138
    /// Never returns on success - only returns Error on failure.
NEW
139
    pub fn exec(&mut self) -> Error {
×
NEW
140
        self.setup_stdio();
×
NEW
141
        match self.do_exec() {
×
NEW
142
            Ok(_) => unreachable!("exec should never return Ok"),
×
NEW
143
            Err(e) => e,
×
144
        }
NEW
145
    }
×
146

147
    /// Setup stdio redirections for child process.
148
    /// Closes original fds after dup2 to prevent leaks.
NEW
149
    fn setup_stdio(&self) {
×
150
        unsafe {
NEW
151
            if let Some(fd) = self.stdout_fd {
×
NEW
152
                if libc::dup2(fd, libc::STDOUT_FILENO) == -1 {
×
NEW
153
                    libc::_exit(1);
×
NEW
154
                }
×
155
                // Close original fd after dup2 (unless it's a standard fd)
NEW
156
                if fd > libc::STDERR_FILENO {
×
NEW
157
                    libc::close(fd);
×
NEW
158
                }
×
NEW
159
            }
×
NEW
160
            if let Some(fd) = self.stderr_fd {
×
NEW
161
                if libc::dup2(fd, libc::STDERR_FILENO) == -1 {
×
NEW
162
                    libc::_exit(1);
×
NEW
163
                }
×
164
                // Close original fd after dup2 (unless it's a standard fd)
NEW
165
                if fd > libc::STDERR_FILENO {
×
NEW
166
                    libc::close(fd);
×
NEW
167
                }
×
NEW
168
            }
×
169
        }
NEW
170
    }
×
171

172
    /// Execute the command with execv.
173
    /// Uses absolute paths (no PATH search) for security - all binaries are whitelisted
174
    /// with full paths. Converts Rust strings to null-terminated C strings for execv.
NEW
175
    fn do_exec(&self) -> Result<()> {
×
176
        use alloc::ffi::CString;
177
        use alloc::vec::Vec;
178

NEW
179
        let c_path = CString::new(self.path).map_err(|_| {
×
NEW
180
            Error::InvalidInput(alloc::string::String::from("Path contains null byte"))
×
NEW
181
        })?;
×
182

NEW
183
        let mut c_args: Vec<CString> = Vec::new();
×
NEW
184
        for i in 0..self.arg_count {
×
NEW
185
            if let Some(arg) = self.args[i] {
×
NEW
186
                let c_arg = CString::new(arg).map_err(|_| {
×
NEW
187
                    Error::InvalidInput(alloc::string::String::from("Arg contains null byte"))
×
NEW
188
                })?;
×
NEW
189
                c_args.push(c_arg);
×
NEW
190
            }
×
191
        }
192

193
        // Build argv: [path, args..., NULL]
NEW
194
        let mut argv: Vec<*const c_char> = Vec::new();
×
NEW
195
        argv.push(c_path.as_ptr());
×
NEW
196
        for c_arg in &c_args {
×
NEW
197
            argv.push(c_arg.as_ptr());
×
NEW
198
        }
×
NEW
199
        argv.push(core::ptr::null());
×
200

201
        // SAFETY: execv is safe here - we're replacing the process
NEW
202
        unsafe {
×
NEW
203
            libc::execv(c_path.as_ptr(), argv.as_ptr());
×
NEW
204
        }
×
205

206
        // If we get here, exec failed
NEW
207
        Err(last_os_error())
×
UNCOV
208
    }
×
209
}
210

211
/// Child process handle
212
pub struct Child {
213
    pid: c_int,
214
}
215

216
impl Child {
217
    /// Check if child has exited without blocking.
218
    /// Returns Some(ExitStatus) if exited, None if still running.
219
    pub fn try_wait(&mut self) -> Result<Option<ExitStatus>> {
1✔
220
        let mut status: c_int = 0;
1✔
221
        // SAFETY: waitpid with WNOHANG is safe
222
        let ret = unsafe { libc::waitpid(self.pid, &mut status, libc::WNOHANG) };
1✔
223

224
        if ret < 0 {
1✔
NEW
225
            return Err(last_os_error());
×
226
        }
1✔
227

228
        if ret == 0 {
1✔
229
            // Still running
230
            return Ok(None);
1✔
NEW
231
        }
×
232

NEW
233
        Ok(Some(ExitStatus { status }))
×
234
    }
1✔
235

236
    /// Wait for child to exit, blocking until it does.
237
    pub fn wait(&mut self) -> Result<ExitStatus> {
9✔
238
        let mut status: c_int = 0;
9✔
239
        // SAFETY: waitpid is safe
240
        let ret = unsafe { libc::waitpid(self.pid, &mut status, 0) };
9✔
241

242
        if ret < 0 {
9✔
NEW
243
            return Err(last_os_error());
×
244
        }
9✔
245

246
        Ok(ExitStatus { status })
9✔
247
    }
9✔
248

249
    /// Send SIGKILL to the child process.
250
    pub fn kill(&mut self) -> Result<()> {
1✔
251
        // SAFETY: kill syscall is safe
252
        let ret = unsafe { libc::kill(self.pid, libc::SIGKILL) };
1✔
253
        if ret < 0 {
1✔
NEW
254
            return Err(last_os_error());
×
255
        }
1✔
256
        Ok(())
1✔
257
    }
1✔
258
}
259

260
/// Process exit status
261
pub struct ExitStatus {
262
    status: c_int,
263
}
264

265
impl ExitStatus {
266
    /// Returns true if the process exited successfully (code 0).
267
    pub fn success(&self) -> bool {
9✔
268
        libc::WIFEXITED(self.status) && libc::WEXITSTATUS(self.status) == 0
9✔
269
    }
9✔
270

271
    /// Get the exit code if the process exited normally.
272
    #[cfg(test)]
273
    pub fn code(&self) -> Option<i32> {
1✔
274
        if libc::WIFEXITED(self.status) {
1✔
275
            Some(libc::WEXITSTATUS(self.status))
1✔
276
        } else {
NEW
277
            None
×
278
        }
279
    }
1✔
280
}
281

282
/// Standard I/O configuration
283
pub enum Stdio {
284
    /// Redirect to /dev/null
285
    Null,
286
    /// Inherit from parent
287
    Inherit,
288
    /// Create a pipe (not implemented - requires pipe2 syscall)
289
    Piped,
290
    /// Use specific file descriptor
291
    Fd(c_int),
292
}
293

294
impl Stdio {
295
    /// Convert to file descriptor option
296
    fn as_fd(&self) -> Option<c_int> {
2✔
297
        match self {
2✔
298
            Stdio::Fd(fd) => Some(*fd),
1✔
299
            Stdio::Null => {
300
                // Open /dev/null with O_CLOEXEC to prevent fd leak to child processes
301
                let null = b"/dev/null\0";
1✔
302
                // SAFETY: open is safe, path is null-terminated
303
                let fd = unsafe {
1✔
304
                    libc::open(
1✔
305
                        null.as_ptr() as *const c_char,
1✔
306
                        libc::O_RDWR | libc::O_CLOEXEC,
1✔
307
                    )
308
                };
309
                if fd >= 0 {
1✔
310
                    Some(fd)
1✔
311
                } else {
NEW
312
                    None
×
313
                }
314
            }
NEW
315
            Stdio::Inherit => None,
×
NEW
316
            Stdio::Piped => None, // TODO: implement if needed
×
317
        }
318
    }
2✔
319

320
    /// Create Stdio from hardened_std::fs::File
321
    pub fn from(file: crate::fs::File) -> Self {
1✔
322
        Stdio::Fd(file.into_raw_fd())
1✔
323
    }
1✔
324
}
325

326
#[cfg(test)]
327
mod tests {
328
    use super::*;
329

330
    // ==================== Binary whitelist tests ====================
331

332
    #[test]
333
    fn test_allowed_production_binaries() {
1✔
334
        // All production binaries should be allowed
335
        assert!(is_binary_allowed("/usr/bin/nvidia-smi"));
1✔
336
        assert!(is_binary_allowed("/usr/bin/nvidia-ctk"));
1✔
337
        assert!(is_binary_allowed("/usr/sbin/modprobe"));
1✔
338
        assert!(is_binary_allowed("/usr/bin/nvidia-persistenced"));
1✔
339
        assert!(is_binary_allowed("/usr/bin/nv-hostengine"));
1✔
340
        assert!(is_binary_allowed("/usr/bin/dcgm-exporter"));
1✔
341
        assert!(is_binary_allowed("/usr/bin/nv-fabricmanager"));
1✔
342
        assert!(is_binary_allowed("/usr/bin/kata-agent"));
1✔
343
    }
1✔
344

345
    #[test]
346
    fn test_allowed_test_binaries() {
1✔
347
        // Test binaries only allowed in test builds
348
        assert!(is_binary_allowed("/bin/true"));
1✔
349
        assert!(is_binary_allowed("/bin/false"));
1✔
350
        assert!(is_binary_allowed("/bin/sleep"));
1✔
351
        assert!(is_binary_allowed("/bin/sh"));
1✔
352
    }
1✔
353

354
    #[test]
355
    fn test_disallowed_binaries() {
1✔
356
        assert!(!is_binary_allowed("/bin/bash"));
1✔
357
        assert!(!is_binary_allowed("/usr/bin/wget"));
1✔
358
        assert!(!is_binary_allowed("/usr/bin/curl"));
1✔
359
        assert!(!is_binary_allowed("nvidia-smi")); // Must be absolute path
1✔
360
        assert!(!is_binary_allowed(""));
1✔
361
    }
1✔
362

363
    // ==================== Command creation tests ====================
364

365
    #[test]
366
    fn test_command_new_allowed() {
1✔
367
        let cmd = Command::new("/bin/true");
1✔
368
        assert!(cmd.is_ok());
1✔
369
    }
1✔
370

371
    #[test]
372
    fn test_command_new_disallowed() {
1✔
373
        let cmd = Command::new("/bin/bash");
1✔
374
        assert!(matches!(cmd, Err(Error::BinaryNotAllowed)));
1✔
375
    }
1✔
376

377
    // ==================== Command execution tests ====================
378

379
    #[test]
380
    fn test_command_status_success() {
1✔
381
        let mut cmd = Command::new("/bin/true").unwrap();
1✔
382
        let status = cmd.status().unwrap();
1✔
383
        assert!(status.success());
1✔
384
    }
1✔
385

386
    #[test]
387
    fn test_command_status_failure() {
1✔
388
        let mut cmd = Command::new("/bin/false").unwrap();
1✔
389
        let status = cmd.status().unwrap();
1✔
390
        assert!(!status.success());
1✔
391
    }
1✔
392

393
    #[test]
394
    fn test_command_with_args() {
1✔
395
        let mut cmd = Command::new("/bin/sh").unwrap();
1✔
396
        cmd.args(&["-c", "exit 0"]).unwrap();
1✔
397
        let status = cmd.status().unwrap();
1✔
398
        assert!(status.success());
1✔
399

400
        let mut cmd = Command::new("/bin/sh").unwrap();
1✔
401
        cmd.args(&["-c", "exit 42"]).unwrap();
1✔
402
        let status = cmd.status().unwrap();
1✔
403
        assert!(!status.success());
1✔
404
        assert_eq!(status.code(), Some(42));
1✔
405
    }
1✔
406

407
    // ==================== Child process tests ====================
408

409
    #[test]
410
    fn test_spawn_and_wait() {
1✔
411
        let mut cmd = Command::new("/bin/true").unwrap();
1✔
412
        let mut child = cmd.spawn().unwrap();
1✔
413
        let status = child.wait().unwrap();
1✔
414
        assert!(status.success());
1✔
415
    }
1✔
416

417
    #[test]
418
    fn test_try_wait() {
1✔
419
        let mut cmd = Command::new("/bin/sleep").unwrap();
1✔
420
        cmd.args(&["1"]).unwrap();
1✔
421
        let mut child = cmd.spawn().unwrap();
1✔
422

423
        // Should be None initially (still running)
424
        let result = child.try_wait().unwrap();
1✔
425
        assert!(result.is_none());
1✔
426

427
        // Wait for it to finish
428
        let status = child.wait().unwrap();
1✔
429
        assert!(status.success());
1✔
430
    }
1✔
431

432
    #[test]
433
    fn test_kill() {
1✔
434
        let mut cmd = Command::new("/bin/sleep").unwrap();
1✔
435
        cmd.args(&["10"]).unwrap();
1✔
436
        let mut child = cmd.spawn().unwrap();
1✔
437

438
        // Kill it
439
        child.kill().unwrap();
1✔
440

441
        // Wait should return (killed status)
442
        let status = child.wait().unwrap();
1✔
443
        assert!(!status.success());
1✔
444
    }
1✔
445

446
    // ==================== Stdio tests ====================
447

448
    #[test]
449
    fn test_stdio_null() {
1✔
450
        let mut cmd = Command::new("/bin/sh").unwrap();
1✔
451
        cmd.args(&["-c", "echo test"]).unwrap();
1✔
452
        cmd.stdout(Stdio::Null);
1✔
453
        let status = cmd.status().unwrap();
1✔
454
        assert!(status.success());
1✔
455
    }
1✔
456

457
    #[test]
458
    fn test_max_args_exceeded() {
1✔
459
        let mut cmd = Command::new("/bin/true").unwrap();
1✔
460
        // Try to add 17 args (exceeds max of 16)
461
        let many_args: [&'static str; 17] = [
1✔
462
            "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16",
1✔
463
            "17",
1✔
464
        ];
1✔
465
        let result = cmd.args(&many_args);
1✔
466
        assert!(result.is_err());
1✔
467
    }
1✔
468

469
    #[test]
470
    fn test_stdio_from_file() {
1✔
471
        use crate::fs::OpenOptions;
472

473
        // Open /dev/null as a file and use it for stdio
474
        let file = OpenOptions::new().write(true).open("/dev/null").unwrap();
1✔
475
        let stdio = Stdio::from(file);
1✔
476

477
        let mut cmd = Command::new("/bin/sh").unwrap();
1✔
478
        cmd.args(&["-c", "echo test"]).unwrap();
1✔
479
        cmd.stdout(stdio);
1✔
480
        let status = cmd.status().unwrap();
1✔
481
        assert!(status.success());
1✔
482
    }
1✔
483
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc