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

NVIDIA / nvrc / 20939107918

12 Jan 2026 11:49PM UTC coverage: 89.224%. First build
20939107918

Pull #121

github

web-flow
Merge 50734dc0b into f13bb81bd
Pull Request #121: hardened_std: Implement execute hardened

156 of 171 new or added lines in 6 files covered. (91.23%)

1805 of 2023 relevant lines covered (89.22%)

16.52 hits per line

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

76.73
/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 (runtime enforcement at Command::new)
8
//! - Binary paths must be &'static str (compile-time constants) - no dynamic paths
9
//! - Arguments can be dynamic &str - validated but not restricted to static strings
10
//! - Maximum security for ephemeral VM init process
11
//!
12
//! **Allowed binaries (production):**
13
//! - /bin/nvidia-smi - GPU configuration
14
//! - /bin/nvidia-ctk - Container toolkit
15
//! - /sbin/modprobe - Kernel module loading
16
//! - /bin/nvidia-persistenced - GPU persistence daemon
17
//! - /bin/nv-hostengine - DCGM host engine
18
//! - /bin/dcgm-exporter - DCGM metrics exporter
19
//! - /bin/nv-fabricmanager - NVLink fabric manager
20
//! - /bin/kata-agent - Kata runtime agent
21
//!
22
//! **Test binaries (debug builds 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
/// Terminate the process with the given exit code.
29
/// This is a thin wrapper around libc::_exit() - it never returns.
30
/// Use this instead of std::process::exit() for no_std compatibility.
31
pub fn exit(code: i32) -> ! {
×
32
    // SAFETY: _exit() is always safe and never returns
33
    unsafe { libc::_exit(code) }
×
34
}
35

36
/// Check if binary is in the allowed list
37
fn is_binary_allowed(path: &str) -> bool {
155✔
38
    // Production binaries - always allowed
39
    let production_allowed = matches!(
155✔
40
        path,
155✔
41
        "/bin/nvidia-smi"
155✔
42
            | "/bin/nvidia-ctk"
146✔
43
            | "/sbin/modprobe"
141✔
44
            | "/bin/nvidia-persistenced"
136✔
45
            | "/bin/nv-hostengine"
135✔
46
            | "/bin/dcgm-exporter"
134✔
47
            | "/bin/nv-fabricmanager"
133✔
48
            | "/bin/kata-agent"
132✔
49
    );
50

51
    if production_allowed {
155✔
52
        return true;
24✔
53
    }
131✔
54

55
    // Test binaries - only allowed in debug builds (never in release)
56
    #[cfg(debug_assertions)]
57
    {
58
        matches!(path, "/bin/true" | "/bin/false" | "/bin/sleep" | "/bin/sh")
131✔
59
    }
60
    #[cfg(not(debug_assertions))]
61
    {
62
        false
63
    }
64
}
155✔
65

66
/// Maximum number of arguments allowed
67
const MAX_ARGS: usize = 32;
68

69
/// Command builder with security restrictions
70
pub struct Command {
71
    path: &'static str,
72
    args: alloc::vec::Vec<alloc::string::String>,
73
    stdout_fd: Option<c_int>,
74
    stderr_fd: Option<c_int>,
75
}
76

77
impl Command {
78
    /// Create a new Command for the given binary path.
79
    /// Binary whitelist is checked at spawn/status/exec time, not here.
80
    pub fn new(path: &'static str) -> Self {
139✔
81
        Self {
139✔
82
            path,
139✔
83
            args: alloc::vec::Vec::new(),
139✔
84
            stdout_fd: None,
139✔
85
            stderr_fd: None,
139✔
86
        }
139✔
87
    }
139✔
88

89
    /// Check if the binary is allowed before execution.
90
    fn check_allowed(&self) -> Result<()> {
138✔
91
        if !is_binary_allowed(self.path) {
138✔
92
            return Err(Error::BinaryNotAllowed);
17✔
93
        }
121✔
94
        Ok(())
121✔
95
    }
138✔
96

97
    /// Add arguments to the command.
98
    /// Maximum 32 arguments supported.
99
    pub fn args(&mut self, args: &[&str]) -> Result<&mut Self> {
124✔
100
        if self.args.len() + args.len() > MAX_ARGS {
124✔
101
            return Err(Error::InvalidInput(alloc::string::String::from(
1✔
102
                "Too many arguments (max 32)",
1✔
103
            )));
1✔
104
        }
123✔
105
        for &arg in args {
309✔
106
            self.args.push(alloc::string::String::from(arg));
186✔
107
        }
186✔
108
        Ok(self)
123✔
109
    }
124✔
110

111
    /// Configure stdout redirection.
112
    pub fn stdout(&mut self, cfg: Stdio) -> &mut Self {
117✔
113
        self.stdout_fd = cfg.as_fd();
117✔
114
        self
117✔
115
    }
117✔
116

117
    /// Configure stderr redirection.
118
    pub fn stderr(&mut self, cfg: Stdio) -> &mut Self {
115✔
119
        self.stderr_fd = cfg.as_fd();
115✔
120
        self
115✔
121
    }
115✔
122

123
    /// Spawn the command as a child process.
124
    pub fn spawn(&mut self) -> Result<Child> {
138✔
125
        // Check whitelist before forking
126
        self.check_allowed()?;
138✔
127

128
        // SAFETY: fork() is safe here because we're in a controlled init environment
129
        let pid = unsafe { libc::fork() };
121✔
130
        if pid < 0 {
121✔
131
            return Err(last_os_error());
×
132
        }
121✔
133

134
        if pid == 0 {
121✔
135
            // Child process - setup stdio and exec
136
            self.setup_stdio();
×
137
            let _ = self.do_exec();
×
138
            // If exec fails, exit with 127 (standard "command not found" exit code)
139
            // This distinguishes exec failures from the spawned command returning 1
NEW
140
            unsafe { libc::_exit(127) };
×
141
        }
121✔
142

143
        // Parent process
144
        Ok(Child { pid })
121✔
145
    }
138✔
146

147
    /// Execute the command, blocking until completion.
148
    pub fn status(&mut self) -> Result<ExitStatus> {
57✔
149
        let mut child = self.spawn()?;
57✔
150
        child.wait()
49✔
151
    }
57✔
152

153
    /// Replace current process with the command (exec).
154
    /// Never returns on success - only returns Error on failure.
155
    pub fn exec(&mut self) -> Error {
×
156
        // Check whitelist before exec
NEW
157
        if let Err(e) = self.check_allowed() {
×
NEW
158
            return e;
×
NEW
159
        }
×
160

161
        self.setup_stdio();
×
162
        match self.do_exec() {
×
163
            Ok(_) => unreachable!("exec should never return Ok"),
×
164
            Err(e) => e,
×
165
        }
166
    }
×
167

168
    /// Setup stdio redirections for child process.
169
    /// Closes original fds after dup2 to prevent leaks.
170
    fn setup_stdio(&self) {
×
171
        unsafe {
172
            if let Some(fd) = self.stdout_fd {
×
173
                if libc::dup2(fd, libc::STDOUT_FILENO) == -1 {
×
174
                    libc::_exit(1);
×
175
                }
×
176
                // Close original fd after dup2 (unless it's a standard fd)
177
                if fd > libc::STDERR_FILENO {
×
178
                    libc::close(fd);
×
179
                }
×
180
            }
×
181
            if let Some(fd) = self.stderr_fd {
×
182
                if libc::dup2(fd, libc::STDERR_FILENO) == -1 {
×
183
                    libc::_exit(1);
×
184
                }
×
185
                // Close original fd after dup2 (unless it's a standard fd)
186
                if fd > libc::STDERR_FILENO {
×
187
                    libc::close(fd);
×
188
                }
×
189
            }
×
190
        }
191
    }
×
192

193
    /// Execute the command with execv.
194
    /// Uses absolute paths (no PATH search) for security - all binaries are whitelisted
195
    /// with full paths. Converts Rust strings to null-terminated C strings for execv.
196
    fn do_exec(&self) -> Result<()> {
×
197
        use alloc::ffi::CString;
198
        use alloc::vec::Vec;
199

200
        let c_path = CString::new(self.path).map_err(|_| {
×
201
            Error::InvalidInput(alloc::string::String::from("Path contains null byte"))
×
202
        })?;
×
203

204
        let mut c_args: Vec<CString> = Vec::new();
×
NEW
205
        for arg in &self.args {
×
NEW
206
            let c_arg = CString::new(arg.as_str()).map_err(|_| {
×
NEW
207
                Error::InvalidInput(alloc::string::String::from("Arg contains null byte"))
×
NEW
208
            })?;
×
NEW
209
            c_args.push(c_arg);
×
210
        }
211

212
        // Build argv: [path, args..., NULL]
213
        let mut argv: Vec<*const c_char> = Vec::new();
×
214
        argv.push(c_path.as_ptr());
×
215
        for c_arg in &c_args {
×
216
            argv.push(c_arg.as_ptr());
×
217
        }
×
218
        argv.push(core::ptr::null());
×
219

220
        // SAFETY: execv is safe here - we're replacing the process
221
        unsafe {
×
222
            libc::execv(c_path.as_ptr(), argv.as_ptr());
×
223
        }
×
224

225
        // If we get here, exec failed
226
        Err(last_os_error())
×
227
    }
×
228
}
229

230
/// Child process handle
231
#[derive(Debug)]
232
pub struct Child {
233
    pid: c_int,
234
}
235

236
impl Child {
237
    /// Check if child has exited without blocking.
238
    /// Returns Some(ExitStatus) if exited, None if still running.
239
    pub fn try_wait(&mut self) -> Result<Option<ExitStatus>> {
27✔
240
        let mut status: c_int = 0;
27✔
241
        // SAFETY: waitpid with WNOHANG is safe
242
        let ret = unsafe { libc::waitpid(self.pid, &mut status, libc::WNOHANG) };
27✔
243

244
        if ret < 0 {
27✔
245
            return Err(last_os_error());
×
246
        }
27✔
247

248
        if ret == 0 {
27✔
249
            // Still running
250
            return Ok(None);
19✔
251
        }
8✔
252

253
        Ok(Some(ExitStatus { status }))
8✔
254
    }
27✔
255

256
    /// Wait for child to exit, blocking until it does.
257
    pub fn wait(&mut self) -> Result<ExitStatus> {
70✔
258
        let mut status: c_int = 0;
70✔
259
        // SAFETY: waitpid is safe
260
        let ret = unsafe { libc::waitpid(self.pid, &mut status, 0) };
70✔
261

262
        if ret < 0 {
70✔
263
            return Err(last_os_error());
×
264
        }
70✔
265

266
        Ok(ExitStatus { status })
70✔
267
    }
70✔
268

269
    /// Send SIGKILL to the child process.
270
    pub fn kill(&mut self) -> Result<()> {
3✔
271
        // SAFETY: kill syscall is safe
272
        let ret = unsafe { libc::kill(self.pid, libc::SIGKILL) };
3✔
273
        if ret < 0 {
3✔
274
            return Err(last_os_error());
×
275
        }
3✔
276
        Ok(())
3✔
277
    }
3✔
278
}
279

280
/// Process exit status
281
pub struct ExitStatus {
282
    status: c_int,
283
}
284

285
impl ExitStatus {
286
    /// Returns true if the process exited successfully (code 0).
287
    pub fn success(&self) -> bool {
62✔
288
        libc::WIFEXITED(self.status) && libc::WEXITSTATUS(self.status) == 0
62✔
289
    }
62✔
290

291
    /// Get the exit code if the process exited normally.
292
    pub fn code(&self) -> Option<i32> {
1✔
293
        if libc::WIFEXITED(self.status) {
1✔
294
            Some(libc::WEXITSTATUS(self.status))
1✔
295
        } else {
296
            None
×
297
        }
298
    }
1✔
299
}
300

301
impl core::fmt::Display for ExitStatus {
302
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
24✔
303
        if libc::WIFEXITED(self.status) {
24✔
304
            write!(f, "exit status: {}", libc::WEXITSTATUS(self.status))
24✔
NEW
305
        } else if libc::WIFSIGNALED(self.status) {
×
NEW
306
            write!(f, "signal: {}", libc::WTERMSIG(self.status))
×
307
        } else {
NEW
308
            write!(f, "unknown status: {}", self.status)
×
309
        }
310
    }
24✔
311
}
312

313
/// Standard I/O configuration
314
pub enum Stdio {
315
    /// Redirect to /dev/null
316
    Null,
317
    /// Inherit from parent
318
    Inherit,
319
    /// Create a pipe (not implemented - requires pipe2 syscall)
320
    Piped,
321
    /// Use specific file descriptor
322
    Fd(c_int),
323
}
324

325
impl Stdio {
326
    /// Convert to file descriptor option
327
    fn as_fd(&self) -> Option<c_int> {
232✔
328
        match self {
232✔
329
            Stdio::Fd(fd) => Some(*fd),
231✔
330
            Stdio::Null => {
331
                // Open /dev/null with O_CLOEXEC to prevent fd leak to child processes
332
                let null = b"/dev/null\0";
1✔
333
                // SAFETY: open is safe, path is null-terminated
334
                let fd = unsafe {
1✔
335
                    libc::open(
1✔
336
                        null.as_ptr() as *const c_char,
1✔
337
                        libc::O_RDWR | libc::O_CLOEXEC,
1✔
338
                    )
339
                };
340
                if fd >= 0 {
1✔
341
                    Some(fd)
1✔
342
                } else {
343
                    None
×
344
                }
345
            }
346
            Stdio::Inherit => None,
×
347
            Stdio::Piped => None, // TODO: implement if needed
×
348
        }
349
    }
232✔
350

351
    /// Create Stdio from hardened_std::fs::File
352
    pub fn from(file: crate::fs::File) -> Self {
231✔
353
        Stdio::Fd(file.into_raw_fd())
231✔
354
    }
231✔
355
}
356

357
#[cfg(test)]
358
mod tests {
359
    use super::*;
360

361
    // ==================== Binary whitelist tests ====================
362

363
    #[test]
364
    fn test_allowed_production_binaries() {
1✔
365
        // All production binaries should be allowed
366
        assert!(is_binary_allowed("/bin/nvidia-smi"));
1✔
367
        assert!(is_binary_allowed("/bin/nvidia-ctk"));
1✔
368
        assert!(is_binary_allowed("/sbin/modprobe"));
1✔
369
        assert!(is_binary_allowed("/bin/nvidia-persistenced"));
1✔
370
        assert!(is_binary_allowed("/bin/nv-hostengine"));
1✔
371
        assert!(is_binary_allowed("/bin/dcgm-exporter"));
1✔
372
        assert!(is_binary_allowed("/bin/nv-fabricmanager"));
1✔
373
        assert!(is_binary_allowed("/bin/kata-agent"));
1✔
374
    }
1✔
375

376
    #[test]
377
    fn test_allowed_test_binaries() {
1✔
378
        // Test binaries only allowed in test builds
379
        assert!(is_binary_allowed("/bin/true"));
1✔
380
        assert!(is_binary_allowed("/bin/false"));
1✔
381
        assert!(is_binary_allowed("/bin/sleep"));
1✔
382
        assert!(is_binary_allowed("/bin/sh"));
1✔
383
    }
1✔
384

385
    #[test]
386
    fn test_disallowed_binaries() {
1✔
387
        assert!(!is_binary_allowed("/bin/bash"));
1✔
388
        assert!(!is_binary_allowed("/usr/bin/wget"));
1✔
389
        assert!(!is_binary_allowed("/usr/bin/curl"));
1✔
390
        assert!(!is_binary_allowed("nvidia-smi")); // Must be absolute path
1✔
391
        assert!(!is_binary_allowed(""));
1✔
392
    }
1✔
393

394
    // ==================== Command creation tests ====================
395

396
    #[test]
397
    fn test_command_new_allowed() {
1✔
398
        // new() is infallible, whitelist checked at spawn time
399
        let mut cmd = Command::new("/bin/true");
1✔
400
        assert!(cmd.spawn().is_ok());
1✔
401
    }
1✔
402

403
    #[test]
404
    fn test_command_new_disallowed() {
1✔
405
        // new() succeeds, but spawn() fails for disallowed binary
406
        let mut cmd = Command::new("/bin/bash");
1✔
407
        assert!(matches!(cmd.spawn(), Err(Error::BinaryNotAllowed)));
1✔
408
    }
1✔
409

410
    // ==================== Command execution tests ====================
411

412
    #[test]
413
    fn test_command_status_success() {
1✔
414
        let mut cmd = Command::new("/bin/true");
1✔
415
        let status = cmd.status().unwrap();
1✔
416
        assert!(status.success());
1✔
417
    }
1✔
418

419
    #[test]
420
    fn test_command_status_failure() {
1✔
421
        let mut cmd = Command::new("/bin/false");
1✔
422
        let status = cmd.status().unwrap();
1✔
423
        assert!(!status.success());
1✔
424
    }
1✔
425

426
    #[test]
427
    fn test_command_with_args() {
1✔
428
        let mut cmd = Command::new("/bin/sh");
1✔
429
        cmd.args(&["-c", "exit 0"]).unwrap();
1✔
430
        let status = cmd.status().unwrap();
1✔
431
        assert!(status.success());
1✔
432

433
        let mut cmd = Command::new("/bin/sh");
1✔
434
        cmd.args(&["-c", "exit 42"]).unwrap();
1✔
435
        let status = cmd.status().unwrap();
1✔
436
        assert!(!status.success());
1✔
437
        assert_eq!(status.code(), Some(42));
1✔
438
    }
1✔
439

440
    // ==================== Child process tests ====================
441

442
    #[test]
443
    fn test_spawn_and_wait() {
1✔
444
        let mut cmd = Command::new("/bin/true");
1✔
445
        let mut child = cmd.spawn().unwrap();
1✔
446
        let status = child.wait().unwrap();
1✔
447
        assert!(status.success());
1✔
448
    }
1✔
449

450
    #[test]
451
    fn test_try_wait() {
1✔
452
        let mut cmd = Command::new("/bin/sleep");
1✔
453
        cmd.args(&["1"]).unwrap();
1✔
454
        let mut child = cmd.spawn().unwrap();
1✔
455

456
        // Should be None initially (still running)
457
        let result = child.try_wait().unwrap();
1✔
458
        assert!(result.is_none());
1✔
459

460
        // Wait for it to finish
461
        let status = child.wait().unwrap();
1✔
462
        assert!(status.success());
1✔
463
    }
1✔
464

465
    #[test]
466
    fn test_kill() {
1✔
467
        let mut cmd = Command::new("/bin/sleep");
1✔
468
        cmd.args(&["10"]).unwrap();
1✔
469
        let mut child = cmd.spawn().unwrap();
1✔
470

471
        // Kill it
472
        child.kill().unwrap();
1✔
473

474
        // Wait should return (killed status)
475
        let status = child.wait().unwrap();
1✔
476
        assert!(!status.success());
1✔
477
    }
1✔
478

479
    // ==================== Stdio tests ====================
480

481
    #[test]
482
    fn test_stdio_null() {
1✔
483
        let mut cmd = Command::new("/bin/sh");
1✔
484
        cmd.args(&["-c", "echo test"]).unwrap();
1✔
485
        cmd.stdout(Stdio::Null);
1✔
486
        let status = cmd.status().unwrap();
1✔
487
        assert!(status.success());
1✔
488
    }
1✔
489

490
    #[test]
491
    fn test_max_args_exceeded() {
1✔
492
        let mut cmd = Command::new("/bin/true");
1✔
493
        // Try to add 33 args (exceeds max of 32)
494
        let many_args: [&str; 33] = [
1✔
495
            "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16",
1✔
496
            "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30",
1✔
497
            "31", "32", "33",
1✔
498
        ];
1✔
499
        let result = cmd.args(&many_args);
1✔
500
        assert!(result.is_err());
1✔
501
    }
1✔
502

503
    #[test]
504
    fn test_stdio_from_file() {
1✔
505
        use crate::fs::OpenOptions;
506

507
        // Open /dev/null as a file and use it for stdio
508
        let file = OpenOptions::new().write(true).open("/dev/null").unwrap();
1✔
509
        let stdio = Stdio::from(file);
1✔
510

511
        let mut cmd = Command::new("/bin/sh");
1✔
512
        cmd.args(&["-c", "echo test"]).unwrap();
1✔
513
        cmd.stdout(stdio);
1✔
514
        let status = cmd.status().unwrap();
1✔
515
        assert!(status.success());
1✔
516
    }
1✔
517
}
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