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

NVIDIA / nvrc / 20926017676

12 Jan 2026 04:01PM UTC coverage: 88.809% (-1.3%) from 90.078%
20926017676

Pull #102

github

web-flow
Merge fea7bdc09 into 8a99e167e
Pull Request #102: hardened_std: eliminate thread::sleep from production code

10 of 40 new or added lines in 3 files covered. (25.0%)

4 existing lines in 1 file now uncovered.

1849 of 2082 relevant lines covered (88.81%)

11.59 hits per line

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

74.72
/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
/// 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.
NEW
31
pub fn exit(code: i32) -> ! {
×
32
    // SAFETY: _exit() is always safe and never returns
NEW
33
    unsafe { libc::_exit(code) }
×
34
}
35

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

51
    if production_allowed {
29✔
52
        return true;
8✔
53
    }
21✔
54

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

66
/// Command builder with security restrictions
67
pub struct Command {
68
    path: &'static str,
69
    args: [Option<&'static str>; 16], // Max 16 args
70
    arg_count: usize,
71
    stdout_fd: Option<c_int>,
72
    stderr_fd: Option<c_int>,
73
}
74

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

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

107
    /// Configure stdout redirection.
108
    pub fn stdout(&mut self, cfg: Stdio) -> &mut Self {
2✔
109
        self.stdout_fd = cfg.as_fd();
2✔
110
        self
2✔
111
    }
2✔
112

113
    /// Configure stderr redirection.
114
    pub fn stderr(&mut self, cfg: Stdio) -> &mut Self {
×
115
        self.stderr_fd = cfg.as_fd();
×
116
        self
×
117
    }
×
118

119
    /// Spawn the command as a child process.
120
    pub fn spawn(&mut self) -> Result<Child> {
9✔
121
        // SAFETY: fork() is safe here because we're in a controlled init environment
122
        let pid = unsafe { libc::fork() };
9✔
123
        if pid < 0 {
9✔
124
            return Err(last_os_error());
×
125
        }
9✔
126

127
        if pid == 0 {
9✔
128
            // Child process - setup stdio and exec
129
            self.setup_stdio();
×
130
            let _ = self.do_exec();
×
131
            // If exec fails, exit child
132
            unsafe { libc::_exit(1) };
×
133
        }
9✔
134

135
        // Parent process
136
        Ok(Child { pid })
9✔
137
    }
9✔
138

139
    /// Execute the command, blocking until completion.
140
    pub fn status(&mut self) -> Result<ExitStatus> {
6✔
141
        let mut child = self.spawn()?;
6✔
142
        child.wait()
6✔
143
    }
6✔
144

145
    /// Replace current process with the command (exec).
146
    /// Never returns on success - only returns Error on failure.
147
    pub fn exec(&mut self) -> Error {
×
148
        self.setup_stdio();
×
149
        match self.do_exec() {
×
150
            Ok(_) => unreachable!("exec should never return Ok"),
×
151
            Err(e) => e,
×
152
        }
153
    }
×
154

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

180
    /// Execute the command with execv.
181
    /// Uses absolute paths (no PATH search) for security - all binaries are whitelisted
182
    /// with full paths. Converts Rust strings to null-terminated C strings for execv.
183
    fn do_exec(&self) -> Result<()> {
×
184
        use alloc::ffi::CString;
185
        use alloc::vec::Vec;
186

187
        let c_path = CString::new(self.path).map_err(|_| {
×
188
            Error::InvalidInput(alloc::string::String::from("Path contains null byte"))
×
189
        })?;
×
190

191
        let mut c_args: Vec<CString> = Vec::new();
×
192
        for i in 0..self.arg_count {
×
193
            if let Some(arg) = self.args[i] {
×
194
                let c_arg = CString::new(arg).map_err(|_| {
×
195
                    Error::InvalidInput(alloc::string::String::from("Arg contains null byte"))
×
196
                })?;
×
197
                c_args.push(c_arg);
×
198
            }
×
199
        }
200

201
        // Build argv: [path, args..., NULL]
202
        let mut argv: Vec<*const c_char> = Vec::new();
×
203
        argv.push(c_path.as_ptr());
×
204
        for c_arg in &c_args {
×
205
            argv.push(c_arg.as_ptr());
×
206
        }
×
207
        argv.push(core::ptr::null());
×
208

209
        // SAFETY: execv is safe here - we're replacing the process
210
        unsafe {
×
211
            libc::execv(c_path.as_ptr(), argv.as_ptr());
×
212
        }
×
213

214
        // If we get here, exec failed
215
        Err(last_os_error())
×
216
    }
×
217
}
218

219
/// Child process handle
220
pub struct Child {
221
    pid: c_int,
222
}
223

224
impl Child {
225
    /// Check if child has exited without blocking.
226
    /// Returns Some(ExitStatus) if exited, None if still running.
227
    pub fn try_wait(&mut self) -> Result<Option<ExitStatus>> {
1✔
228
        let mut status: c_int = 0;
1✔
229
        // SAFETY: waitpid with WNOHANG is safe
230
        let ret = unsafe { libc::waitpid(self.pid, &mut status, libc::WNOHANG) };
1✔
231

232
        if ret < 0 {
1✔
233
            return Err(last_os_error());
×
234
        }
1✔
235

236
        if ret == 0 {
1✔
237
            // Still running
238
            return Ok(None);
1✔
239
        }
×
240

241
        Ok(Some(ExitStatus { status }))
×
242
    }
1✔
243

244
    /// Wait for child to exit, blocking until it does.
245
    pub fn wait(&mut self) -> Result<ExitStatus> {
9✔
246
        let mut status: c_int = 0;
9✔
247
        // SAFETY: waitpid is safe
248
        let ret = unsafe { libc::waitpid(self.pid, &mut status, 0) };
9✔
249

250
        if ret < 0 {
9✔
251
            return Err(last_os_error());
×
252
        }
9✔
253

254
        Ok(ExitStatus { status })
9✔
255
    }
9✔
256

257
    /// Send SIGKILL to the child process.
258
    pub fn kill(&mut self) -> Result<()> {
1✔
259
        // SAFETY: kill syscall is safe
260
        let ret = unsafe { libc::kill(self.pid, libc::SIGKILL) };
1✔
261
        if ret < 0 {
1✔
262
            return Err(last_os_error());
×
263
        }
1✔
264
        Ok(())
1✔
265
    }
1✔
266
}
267

268
/// Process exit status
269
pub struct ExitStatus {
270
    status: c_int,
271
}
272

273
impl ExitStatus {
274
    /// Returns true if the process exited successfully (code 0).
275
    pub fn success(&self) -> bool {
9✔
276
        libc::WIFEXITED(self.status) && libc::WEXITSTATUS(self.status) == 0
9✔
277
    }
9✔
278

279
    /// Get the exit code if the process exited normally.
280
    #[cfg(test)]
281
    pub fn code(&self) -> Option<i32> {
1✔
282
        if libc::WIFEXITED(self.status) {
1✔
283
            Some(libc::WEXITSTATUS(self.status))
1✔
284
        } else {
285
            None
×
286
        }
287
    }
1✔
288
}
289

290
/// Standard I/O configuration
291
pub enum Stdio {
292
    /// Redirect to /dev/null
293
    Null,
294
    /// Inherit from parent
295
    Inherit,
296
    /// Create a pipe (not implemented - requires pipe2 syscall)
297
    Piped,
298
    /// Use specific file descriptor
299
    Fd(c_int),
300
}
301

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

328
    /// Create Stdio from hardened_std::fs::File
329
    pub fn from(file: crate::fs::File) -> Self {
1✔
330
        Stdio::Fd(file.into_raw_fd())
1✔
331
    }
1✔
332
}
333

334
#[cfg(test)]
335
mod tests {
336
    use super::*;
337

338
    // ==================== Binary whitelist tests ====================
339

340
    #[test]
341
    fn test_allowed_production_binaries() {
1✔
342
        // All production binaries should be allowed
343
        assert!(is_binary_allowed("/usr/bin/nvidia-smi"));
1✔
344
        assert!(is_binary_allowed("/usr/bin/nvidia-ctk"));
1✔
345
        assert!(is_binary_allowed("/usr/sbin/modprobe"));
1✔
346
        assert!(is_binary_allowed("/usr/bin/nvidia-persistenced"));
1✔
347
        assert!(is_binary_allowed("/usr/bin/nv-hostengine"));
1✔
348
        assert!(is_binary_allowed("/usr/bin/dcgm-exporter"));
1✔
349
        assert!(is_binary_allowed("/usr/bin/nv-fabricmanager"));
1✔
350
        assert!(is_binary_allowed("/usr/bin/kata-agent"));
1✔
351
    }
1✔
352

353
    #[test]
354
    fn test_allowed_test_binaries() {
1✔
355
        // Test binaries only allowed in test builds
356
        assert!(is_binary_allowed("/bin/true"));
1✔
357
        assert!(is_binary_allowed("/bin/false"));
1✔
358
        assert!(is_binary_allowed("/bin/sleep"));
1✔
359
        assert!(is_binary_allowed("/bin/sh"));
1✔
360
    }
1✔
361

362
    #[test]
363
    fn test_disallowed_binaries() {
1✔
364
        assert!(!is_binary_allowed("/bin/bash"));
1✔
365
        assert!(!is_binary_allowed("/usr/bin/wget"));
1✔
366
        assert!(!is_binary_allowed("/usr/bin/curl"));
1✔
367
        assert!(!is_binary_allowed("nvidia-smi")); // Must be absolute path
1✔
368
        assert!(!is_binary_allowed(""));
1✔
369
    }
1✔
370

371
    // ==================== Command creation tests ====================
372

373
    #[test]
374
    fn test_command_new_allowed() {
1✔
375
        let cmd = Command::new("/bin/true");
1✔
376
        assert!(cmd.is_ok());
1✔
377
    }
1✔
378

379
    #[test]
380
    fn test_command_new_disallowed() {
1✔
381
        let cmd = Command::new("/bin/bash");
1✔
382
        assert!(matches!(cmd, Err(Error::BinaryNotAllowed)));
1✔
383
    }
1✔
384

385
    // ==================== Command execution tests ====================
386

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

394
    #[test]
395
    fn test_command_status_failure() {
1✔
396
        let mut cmd = Command::new("/bin/false").unwrap();
1✔
397
        let status = cmd.status().unwrap();
1✔
398
        assert!(!status.success());
1✔
399
    }
1✔
400

401
    #[test]
402
    fn test_command_with_args() {
1✔
403
        let mut cmd = Command::new("/bin/sh").unwrap();
1✔
404
        cmd.args(&["-c", "exit 0"]).unwrap();
1✔
405
        let status = cmd.status().unwrap();
1✔
406
        assert!(status.success());
1✔
407

408
        let mut cmd = Command::new("/bin/sh").unwrap();
1✔
409
        cmd.args(&["-c", "exit 42"]).unwrap();
1✔
410
        let status = cmd.status().unwrap();
1✔
411
        assert!(!status.success());
1✔
412
        assert_eq!(status.code(), Some(42));
1✔
413
    }
1✔
414

415
    // ==================== Child process tests ====================
416

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

425
    #[test]
426
    fn test_try_wait() {
1✔
427
        let mut cmd = Command::new("/bin/sleep").unwrap();
1✔
428
        cmd.args(&["1"]).unwrap();
1✔
429
        let mut child = cmd.spawn().unwrap();
1✔
430

431
        // Should be None initially (still running)
432
        let result = child.try_wait().unwrap();
1✔
433
        assert!(result.is_none());
1✔
434

435
        // Wait for it to finish
436
        let status = child.wait().unwrap();
1✔
437
        assert!(status.success());
1✔
438
    }
1✔
439

440
    #[test]
441
    fn test_kill() {
1✔
442
        let mut cmd = Command::new("/bin/sleep").unwrap();
1✔
443
        cmd.args(&["10"]).unwrap();
1✔
444
        let mut child = cmd.spawn().unwrap();
1✔
445

446
        // Kill it
447
        child.kill().unwrap();
1✔
448

449
        // Wait should return (killed status)
450
        let status = child.wait().unwrap();
1✔
451
        assert!(!status.success());
1✔
452
    }
1✔
453

454
    // ==================== Stdio tests ====================
455

456
    #[test]
457
    fn test_stdio_null() {
1✔
458
        let mut cmd = Command::new("/bin/sh").unwrap();
1✔
459
        cmd.args(&["-c", "echo test"]).unwrap();
1✔
460
        cmd.stdout(Stdio::Null);
1✔
461
        let status = cmd.status().unwrap();
1✔
462
        assert!(status.success());
1✔
463
    }
1✔
464

465
    #[test]
466
    fn test_max_args_exceeded() {
1✔
467
        let mut cmd = Command::new("/bin/true").unwrap();
1✔
468
        // Try to add 17 args (exceeds max of 16)
469
        let many_args: [&'static str; 17] = [
1✔
470
            "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16",
1✔
471
            "17",
1✔
472
        ];
1✔
473
        let result = cmd.args(&many_args);
1✔
474
        assert!(result.is_err());
1✔
475
    }
1✔
476

477
    #[test]
478
    fn test_stdio_from_file() {
1✔
479
        use crate::fs::OpenOptions;
480

481
        // Open /dev/null as a file and use it for stdio
482
        let file = OpenOptions::new().write(true).open("/dev/null").unwrap();
1✔
483
        let stdio = Stdio::from(file);
1✔
484

485
        let mut cmd = Command::new("/bin/sh").unwrap();
1✔
486
        cmd.args(&["-c", "echo test"]).unwrap();
1✔
487
        cmd.stdout(stdio);
1✔
488
        let status = cmd.status().unwrap();
1✔
489
        assert!(status.success());
1✔
490
    }
1✔
491
}
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