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

NVIDIA / nvrc / 20937448875

12 Jan 2026 10:37PM UTC coverage: 90.015%. First build
20937448875

Pull #121

github

web-flow
Merge 4d2fc5781 into f13bb81bd
Pull Request #121: Execute hardened

140 of 154 new or added lines in 6 files covered. (90.91%)

1821 of 2023 relevant lines covered (90.01%)

16.72 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
//! - /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 (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 {
157✔
38
    // Production binaries - always allowed
39
    let production_allowed = matches!(
157✔
40
        path,
157✔
41
        "/usr/bin/nvidia-smi"
157✔
42
            | "/usr/bin/nvidia-ctk"
148✔
43
            | "/usr/sbin/modprobe"
143✔
44
            | "/usr/bin/nvidia-persistenced"
138✔
45
            | "/usr/bin/nv-hostengine"
137✔
46
            | "/usr/bin/dcgm-exporter"
136✔
47
            | "/usr/bin/nv-fabricmanager"
135✔
48
            | "/usr/bin/kata-agent"
134✔
49
    );
50

51
    if production_allowed {
157✔
52
        return true;
24✔
53
    }
133✔
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")
133✔
59
    }
60
    #[cfg(not(debug_assertions))]
61
    {
62
        false
63
    }
64
}
157✔
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 {
141✔
81
        Self {
141✔
82
            path,
141✔
83
            args: alloc::vec::Vec::new(),
141✔
84
            stdout_fd: None,
141✔
85
            stderr_fd: None,
141✔
86
        }
141✔
87
    }
141✔
88

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

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

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

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

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

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

134
        if pid == 0 {
123✔
135
            // Child process - setup stdio and exec
136
            self.setup_stdio();
×
137
            let _ = self.do_exec();
×
138
            // If exec fails, exit child
139
            unsafe { libc::_exit(1) };
×
140
        }
123✔
141

142
        // Parent process
143
        Ok(Child { pid })
123✔
144
    }
140✔
145

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

265
        Ok(ExitStatus { status })
72✔
266
    }
72✔
267

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

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

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

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

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

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

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

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

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

360
    // ==================== Binary whitelist tests ====================
361

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

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

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

393
    // ==================== Command creation tests ====================
394

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

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

409
    // ==================== Command execution tests ====================
410

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

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

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

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

439
    // ==================== Child process tests ====================
440

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

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

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

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

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

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

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

478
    // ==================== Stdio tests ====================
479

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

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

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

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

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

© 2026 Coveralls, Inc