subc_os/launch_nonce.rs
1//! The launch nonce: the secret the daemon gives each module it spawns, which
2//! the module presents to be admitted as itself.
3//!
4//! On macOS and Linux the daemon hands it over through a pipe rather than
5//! the environment, because any process of the same user can read another
6//! process's initial environment (`ps eww`, `sysctl KERN_PROCARGS2`). The
7//! daemon side is [`LaunchNonceHandoff`]: a pipe that already holds the nonce,
8//! whose read end becomes descriptor [`LAUNCH_NONCE_FD`] in the child. The
9//! module side is [`launch_nonce`]: it reads that descriptor once, closes it,
10//! and caches the value for the life of the process.
11//!
12//! Windows uses a one-time named pipe authenticated to the direct child's PID.
13//! The daemon still sets the Windows environment copy for readers that have
14//! not adopted the pipe. `SUBC_LAUNCH_NONCE_PIPE_FALLBACK=env` explicitly permits
15//! environment startup when delivery fails; unmarked readers remain fail-closed.
16
17use std::{
18 ffi::OsString,
19 fmt,
20 sync::{atomic::AtomicUsize, OnceLock},
21};
22
23/// The descriptor number the pipe's read end has in the child.
24pub const LAUNCH_NONCE_FD: i32 = 3;
25
26/// Names the descriptor holding the nonce, as `<fd>:<inode>`. The inode names
27/// the pipe itself, so the reader can tell it from an unrelated descriptor
28/// that happens to have the same number. A process a module spawns inherits
29/// this variable but not the pipe, and without the inode it would read and
30/// close whatever that process has at the number.
31pub const LAUNCH_NONCE_FD_ENV: &str = "SUBC_LAUNCH_NONCE_FD";
32
33/// Names the one-time Windows pipe. Its name is public, not a credential.
34pub const LAUNCH_NONCE_PIPE_ENV: &str = "SUBC_LAUNCH_NONCE_PIPE";
35
36/// Explicit Windows rollout permission to use the environment copy if pipe
37/// delivery fails. The daemon sets this to `env` while environment-only modules
38/// migrate to pipe reads; it must be removed with the environment copy.
39pub const LAUNCH_NONCE_PIPE_FALLBACK_ENV: &str = "SUBC_LAUNCH_NONCE_PIPE_FALLBACK";
40
41/// The environment copy of the nonce, kept only while modules move to the
42/// descriptor. Same name as `subc_protocol::SUBC_LAUNCH_NONCE_ENV`; this crate
43/// does not depend on subc-protocol, so it states the name itself.
44pub const LAUNCH_NONCE_ENV: &str = "SUBC_LAUNCH_NONCE";
45
46/// Where a process got its launch nonce from.
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48#[non_exhaustive]
49pub enum LaunchNonceSource {
50 /// The inherited descriptor named by [`LAUNCH_NONCE_FD_ENV`].
51 Fd,
52 /// The PID-authenticated Windows pipe named by [`LAUNCH_NONCE_PIPE_ENV`].
53 Pipe,
54 /// The environment variable [`LAUNCH_NONCE_ENV`].
55 Env,
56}
57
58impl LaunchNonceSource {
59 /// The name modules report in their provenance.
60 pub fn as_str(self) -> &'static str {
61 match self {
62 Self::Fd => "fd",
63 Self::Pipe => "pipe",
64 Self::Env => "env",
65 }
66 }
67}
68
69/// The nonce this process was launched with, and where it came from.
70/// `Debug` never prints the value.
71#[derive(Clone, PartialEq, Eq)]
72pub struct LaunchNonce {
73 value: String,
74 source: LaunchNonceSource,
75}
76
77impl LaunchNonce {
78 pub fn value(&self) -> &str {
79 &self.value
80 }
81
82 pub fn source(&self) -> LaunchNonceSource {
83 self.source
84 }
85}
86
87impl fmt::Debug for LaunchNonce {
88 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89 f.debug_struct("LaunchNonce")
90 .field(
91 "value",
92 &format_args!("<{} bytes redacted>", self.value.len()),
93 )
94 .field("source", &self.source)
95 .finish()
96 }
97}
98
99/// Why a named launch-nonce handoff gave no nonce.
100///
101/// These errors never fall back unless a Windows daemon explicitly permits
102/// an environment copy via [`LAUNCH_NONCE_PIPE_FALLBACK_ENV`].
103/// A named descriptor or pipe
104/// that cannot be read means the handoff went wrong, or that this process
105/// inherited the variable from a module without inheriting the pipe; reading
106/// the environment instead would hide a failed handoff or admit a descendant
107/// using its parent's environment credential.
108#[derive(Debug, Clone, PartialEq, Eq)]
109#[non_exhaustive]
110pub enum LaunchNonceError {
111 /// The variable is not `<fd>:<inode>`.
112 Malformed { value: String },
113 /// Nothing is open at that number: the variable was inherited without
114 /// the descriptor, which is what a process spawned by a module sees.
115 NotOpen { fd: i32, errno: i32 },
116 /// The descriptor is open but is not a pipe. It was left alone.
117 NotAPipe { fd: i32 },
118 /// The descriptor is a pipe, but not the one named. It was left alone.
119 WrongPipe {
120 fd: i32,
121 expected_inode: u64,
122 found_inode: u64,
123 },
124 /// The named pipe holds no bytes. It was left open and unread.
125 Empty { fd: i32 },
126 /// Reading the named pipe failed.
127 Unreadable { fd: i32, errno: Option<i32> },
128 /// The named pipe held bytes that are not UTF-8.
129 NotUtf8 { fd: i32 },
130 /// The named Windows pipe could not be opened within the busy retry window.
131 PipeNotOpen { errno: i32 },
132 /// The named Windows pipe held no bytes.
133 PipeEmpty,
134 /// Reading the Windows pipe failed or exceeded its deadline.
135 PipeUnreadable { errno: Option<i32> },
136 /// The Windows pipe held bytes that are not UTF-8.
137 PipeNotUtf8,
138}
139
140impl fmt::Display for LaunchNonceError {
141 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142 match self {
143 Self::PipeNotOpen { errno } => write!(f, "{LAUNCH_NONCE_PIPE_ENV} names a pipe that could not be opened (errno {errno})"),
144 Self::PipeEmpty => write!(f, "the launch nonce named pipe is empty"),
145 Self::PipeUnreadable { errno } => write!(f, "could not read the launch nonce from the named pipe (errno {errno:?})"),
146 Self::PipeNotUtf8 => write!(f, "the launch nonce named pipe held bytes that are not UTF-8"),
147 Self::Malformed { value } => write!(
148 f,
149 "{LAUNCH_NONCE_FD_ENV}={value:?} is not <fd>:<inode>"
150 ),
151 Self::NotOpen { fd, errno } => write!(
152 f,
153 "{LAUNCH_NONCE_FD_ENV} names descriptor {fd}, which is not open (errno {errno}); \
154 a process spawned by a module inherits the variable but not the descriptor"
155 ),
156 Self::NotAPipe { fd } => write!(
157 f,
158 "{LAUNCH_NONCE_FD_ENV} names descriptor {fd}, which is not a pipe; left it untouched"
159 ),
160 Self::WrongPipe {
161 fd,
162 expected_inode,
163 found_inode,
164 } => write!(
165 f,
166 "{LAUNCH_NONCE_FD_ENV} names descriptor {fd} with inode {expected_inode}, but it has \
167 inode {found_inode}; left it untouched"
168 ),
169 Self::Empty { fd } => write!(
170 f,
171 "the launch nonce pipe at descriptor {fd} is empty; left it untouched"
172 ),
173 Self::Unreadable { fd, errno } => write!(
174 f,
175 "could not read the launch nonce from descriptor {fd} (errno {errno:?})"
176 ),
177 Self::NotUtf8 { fd } => write!(
178 f,
179 "the launch nonce pipe at descriptor {fd} held bytes that are not UTF-8"
180 ),
181 }
182 }
183}
184
185impl std::error::Error for LaunchNonceError {}
186
187type Cached = Result<Option<LaunchNonce>, LaunchNonceError>;
188
189/// A launch nonce read at most once. The process has one, behind
190/// [`launch_nonce`]; tests make their own with a stand-in for the
191/// environment.
192pub(crate) struct LaunchNonceCell {
193 value: OnceLock<Cached>,
194 descriptor_reads: AtomicUsize,
195}
196
197impl LaunchNonceCell {
198 pub(crate) const fn new() -> Self {
199 Self {
200 value: OnceLock::new(),
201 descriptor_reads: AtomicUsize::new(0),
202 }
203 }
204
205 /// The cached result, reading it first if no caller has yet. Concurrent
206 /// first callers wait for the one that reads, so nobody reads twice.
207 pub(crate) fn get(&self, lookup: impl FnMut(&str) -> Option<OsString>) -> Cached {
208 self.value
209 .get_or_init(|| read_launch_nonce(lookup, &self.descriptor_reads))
210 .clone()
211 }
212
213 /// How many times this cell has taken a descriptor. At most one.
214 #[cfg(all(test, unix))]
215 pub(crate) fn descriptor_reads(&self) -> usize {
216 self.descriptor_reads
217 .load(std::sync::atomic::Ordering::SeqCst)
218 }
219}
220
221static PROCESS_NONCE: LaunchNonceCell = LaunchNonceCell::new();
222
223/// This process's launch nonce and where it came from.
224///
225/// The first call decides, and every later call returns the same answer
226/// without touching the descriptor or the environment again. So every reader
227/// in a process must come through here: after the first read closes the
228/// descriptor, its number is the next one the process hands out, and a second
229/// independent reader would read and close some unrelated socket or file.
230///
231/// - When [`LAUNCH_NONCE_FD_ENV`] is set (macOS and Linux), the descriptor it
232/// names is taken only if it is a pipe with the named inode and holds
233/// bytes; it is then read to end of file and closed. Anything else is a
234/// [`LaunchNonceError`] that leaves the descriptor as it was and never
235/// falls back to the environment.
236/// - On Windows, when [`LAUNCH_NONCE_PIPE_ENV`] is set, the named pipe is opened
237/// read-only at identification impersonation level and read to EOF with a
238/// bounded deadline. Without explicit fallback permission, errors are cached
239/// and never fall back. When [`LAUNCH_NONCE_PIPE_FALLBACK_ENV`] is `env` and
240/// a nonempty environment copy exists, failed or incomplete delivery instead
241/// caches that copy and its `env` source. A matching complete read stays `pipe`.
242/// - Otherwise the value of [`LAUNCH_NONCE_ENV`] is used.
243/// - `Ok(None)` means no handoff is named (or the environment copy is empty): the
244/// process was not started by the daemon.
245///
246/// It never changes the environment. Removing either variable would break
247/// any other reader in the process still on the environment copy, and
248/// changing the environment of a multi-threaded process can race readers in
249/// libraries whose environment access cannot be synchronized by this accessor.
250///
251/// Call it before the process spawns anything. Until the first read the
252/// descriptor is inheritable (it has to be, to survive the daemon's exec),
253/// so a child spawned earlier could inherit and consume the module's secret.
254pub fn launch_nonce() -> Result<Option<LaunchNonce>, LaunchNonceError> {
255 PROCESS_NONCE.get(|key| std::env::var_os(key))
256}
257
258fn read_launch_nonce(
259 mut lookup: impl FnMut(&str) -> Option<OsString>,
260 descriptor_reads: &AtomicUsize,
261) -> Cached {
262 #[cfg(windows)]
263 if let Some(value) = lookup(LAUNCH_NONCE_PIPE_ENV) {
264 let fallback = if lookup(LAUNCH_NONCE_PIPE_FALLBACK_ENV).as_deref()
265 == Some(std::ffi::OsStr::new("env"))
266 {
267 environment_nonce(lookup(LAUNCH_NONCE_ENV))
268 } else {
269 None
270 };
271 return pipe_or_permitted_env(windows::read_pipe(&value), fallback);
272 }
273 #[cfg(unix)]
274 if let Some(value) = lookup(LAUNCH_NONCE_FD_ENV) {
275 return unix::read_descriptor(&value, descriptor_reads);
276 }
277 #[cfg(not(unix))]
278 let _ = descriptor_reads;
279 Ok(environment_nonce(lookup(LAUNCH_NONCE_ENV)))
280}
281
282fn environment_nonce(value: Option<OsString>) -> Option<LaunchNonce> {
283 value
284 .and_then(|value| value.into_string().ok())
285 .filter(|value| !value.is_empty())
286 .map(|value| LaunchNonce {
287 value,
288 source: LaunchNonceSource::Env,
289 })
290}
291
292#[cfg(any(windows, test))]
293fn pipe_or_permitted_env(pipe: Cached, fallback: Option<LaunchNonce>) -> Cached {
294 let Some(fallback) = fallback else {
295 return pipe;
296 };
297 match &pipe {
298 Ok(Some(nonce)) if nonce.value() == fallback.value() => pipe,
299 // The daemon supplies the same nonce through the pipe and
300 // SUBC_LAUNCH_NONCE until every module reads the pipe. With explicit
301 // permission to use the copy, reject even valid UTF-8 pipe bytes that
302 // differ from it: a truncated secret would otherwise fail registration.
303 _ => Ok(Some(fallback)),
304 }
305}
306
307#[cfg(unix)]
308pub use unix::LaunchNonceHandoff;
309
310#[cfg(windows)]
311pub use windows::{LaunchNoncePipeDelivery, LaunchNoncePipeHandoff};
312
313#[cfg(windows)]
314#[allow(unsafe_code)]
315mod windows;
316
317#[cfg(unix)]
318mod unix {
319 use std::{
320 ffi::OsStr,
321 fs::File,
322 io::{self, Read, Write},
323 os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd},
324 sync::atomic::{AtomicUsize, Ordering},
325 };
326
327 use super::{Cached, LaunchNonce, LaunchNonceError, LaunchNonceSource, LAUNCH_NONCE_FD};
328
329 /// The daemon half of the handoff, prepared before the spawn: a pipe that
330 /// already holds the nonce, its write end closed, and its inode.
331 ///
332 /// A module uses the same type to hand its own nonce to a helper process
333 /// that must connect as the module: never in argv, the environment or a
334 /// file, all of which another same-user process can read.
335 ///
336 /// ```no_run
337 /// # fn helper(nonce: &str) -> std::io::Result<()> {
338 /// use subc_os::launch_nonce::{LaunchNonceHandoff, LAUNCH_NONCE_FD_ENV};
339 ///
340 /// let mut command = std::process::Command::new("helper");
341 /// let handoff = LaunchNonceHandoff::new(nonce)?;
342 /// command.env(LAUNCH_NONCE_FD_ENV, handoff.fd_env_value());
343 /// // After every other pre-exec step the command has.
344 /// handoff.install_last(&mut command);
345 /// command.spawn()?;
346 /// # Ok(()) }
347 /// ```
348 #[derive(Debug)]
349 pub struct LaunchNonceHandoff {
350 read_end: OwnedFd,
351 inode: u64,
352 target: RawFd,
353 }
354
355 impl LaunchNonceHandoff {
356 /// Make the pipe, write `nonce` into it and close the write end, so a
357 /// reader gets exactly `nonce` and then end of file.
358 ///
359 /// Both ends are created close-on-exec (`std::io::pipe` does that),
360 /// so the read end reaches no child until [`Self::install_last`] puts
361 /// it into one. The nonce must fit in the pipe buffer, or the write
362 /// blocks with no reader: POSIX guarantees 512 bytes and macOS and
363 /// Linux give 16 KiB or more, and a daemon nonce is 64 characters.
364 pub fn new(nonce: &str) -> io::Result<Self> {
365 let (reader, mut writer) = io::pipe()?;
366 writer.write_all(nonce.as_bytes())?;
367 drop(writer);
368 let mut read_end = OwnedFd::from(reader);
369 // std configures the child's stdio before pre-exec callbacks. If a
370 // standard descriptor was closed in the parent, pipe() can use its
371 // number, which stdio setup would overwrite before the handoff runs.
372 // Move it out of that range now, keeping the parent copy close-on-exec.
373 if read_end.as_raw_fd() < LAUNCH_NONCE_FD {
374 // SAFETY: duplicates an owned descriptor; no memory is passed.
375 #[allow(unsafe_code)]
376 let copy = unsafe {
377 libc::fcntl(read_end.as_raw_fd(), libc::F_DUPFD_CLOEXEC, LAUNCH_NONCE_FD)
378 };
379 if copy == -1 {
380 return Err(io::Error::last_os_error());
381 }
382 // SAFETY: the successful fcntl returned a new descriptor owned here.
383 #[allow(unsafe_code)]
384 {
385 read_end = unsafe { OwnedFd::from_raw_fd(copy) };
386 }
387 }
388 let inode = fstat(read_end.as_raw_fd())?.st_ino as u64;
389 Ok(Self {
390 read_end,
391 inode,
392 target: LAUNCH_NONCE_FD,
393 })
394 }
395
396 /// The value to give the child as
397 /// [`LAUNCH_NONCE_FD_ENV`](super::LAUNCH_NONCE_FD_ENV):
398 /// `3:<inode of this pipe>`.
399 pub fn fd_env_value(&self) -> String {
400 format!("{}:{}", self.target, self.inode)
401 }
402
403 /// Arrange for the read end to be descriptor 3 in the process
404 /// `command` spawns, and in no other process. For a tokio `Command`,
405 /// pass `command.as_std_mut()`.
406 ///
407 /// It must be the LAST pre-exec step registered. The standard library
408 /// runs pre-exec steps in registration order, after its own stdio
409 /// setup, and this one closes whatever the child had at descriptor 3.
410 /// A step that runs later and writes through a descriptor it captured
411 /// (the Linux cgroup placement writes to `cgroup.procs`) would find
412 /// the pipe there instead if that descriptor had number 3.
413 ///
414 /// Registering any pre-exec step makes the standard library fork and
415 /// exec instead of using `posix_spawn`, on macOS as on Linux.
416 pub fn install_last(self, command: &mut std::process::Command) {
417 use std::os::unix::process::CommandExt;
418 // SAFETY: the closure runs between fork and exec in a copy of a
419 // possibly multi-threaded process, where only async-signal-safe
420 // calls are sound. `install_in_child` makes only dup2 and fcntl
421 // calls on a descriptor opened before the fork, and allocates
422 // nothing (see the_pre_exec_step_does_not_allocate).
423 #[allow(unsafe_code)]
424 unsafe {
425 command.pre_exec(move || self.install_in_child());
426 }
427 }
428
429 /// Put the read end at the target number without close-on-exec. Runs
430 /// in the forked child: only dup2 and fcntl, and errors built from
431 /// errno, which does not allocate.
432 ///
433 /// When the read end already has the target number, `dup2` would do
434 /// nothing and leave close-on-exec set, so exec would close the
435 /// descriptor; that case clears the flag instead.
436 pub(crate) fn install_in_child(&self) -> io::Result<()> {
437 let source = self.read_end.as_raw_fd();
438 if source == self.target {
439 // SAFETY: fcntl on a descriptor this struct owns; no memory is passed.
440 #[allow(unsafe_code)]
441 let flags = unsafe { libc::fcntl(source, libc::F_GETFD) };
442 if flags == -1 {
443 return Err(io::Error::last_os_error());
444 }
445 // SAFETY: as above.
446 #[allow(unsafe_code)]
447 let set = unsafe { libc::fcntl(source, libc::F_SETFD, flags & !libc::FD_CLOEXEC) };
448 if set == -1 {
449 return Err(io::Error::last_os_error());
450 }
451 return Ok(());
452 }
453 // SAFETY: dup2 takes two integers and touches no memory. It closes
454 // whatever the child had at the target, which is why this step
455 // must run after every other one (see `install_last`). The new
456 // descriptor does not carry close-on-exec, so it survives exec.
457 #[allow(unsafe_code)]
458 if unsafe { libc::dup2(source, self.target) } == -1 {
459 return Err(io::Error::last_os_error());
460 }
461 Ok(())
462 }
463
464 /// A handoff whose read end is placed at `target` instead of 3, so
465 /// tests can exercise the step without claiming descriptor 3 in the
466 /// test process itself.
467 #[cfg(test)]
468 pub(crate) fn with_target(mut self, target: RawFd) -> Self {
469 self.target = target;
470 self
471 }
472
473 #[cfg(test)]
474 pub(crate) fn read_end_fd(&self) -> RawFd {
475 self.read_end.as_raw_fd()
476 }
477
478 #[cfg(test)]
479 pub(crate) fn inode(&self) -> u64 {
480 self.inode
481 }
482 }
483
484 pub(super) fn read_descriptor(value: &OsStr, descriptor_reads: &AtomicUsize) -> Cached {
485 let text = value.to_string_lossy();
486 let malformed = || LaunchNonceError::Malformed {
487 value: text.to_string(),
488 };
489 let (fd_text, inode_text) = text.split_once(':').ok_or_else(malformed)?;
490 let fd: RawFd = fd_text.parse().map_err(|_| malformed())?;
491 let expected_inode: u64 = inode_text.parse().map_err(|_| malformed())?;
492 if fd < 0 {
493 return Err(malformed());
494 }
495
496 // Check what the descriptor is before taking ownership of it. Reading
497 // and closing a descriptor that belongs to other code in this process
498 // would break that code, and a process that inherited the variable
499 // without the descriptor may well have something else at this number.
500 let stat = fstat(fd).map_err(|error| LaunchNonceError::NotOpen {
501 fd,
502 errno: error.raw_os_error().unwrap_or(0),
503 })?;
504 if stat.st_mode & libc::S_IFMT != libc::S_IFIFO {
505 return Err(LaunchNonceError::NotAPipe { fd });
506 }
507 let found_inode = stat.st_ino as u64;
508 if found_inode != expected_inode {
509 return Err(LaunchNonceError::WrongPipe {
510 fd,
511 expected_inode,
512 found_inode,
513 });
514 }
515 // Ask how many bytes are waiting rather than reading to find out, so
516 // an empty pipe is refused without being consumed or closed.
517 let mut waiting: libc::c_int = 0;
518 // SAFETY: FIONREAD writes one int through the pointer, which points
519 // at a live local of that type.
520 #[allow(unsafe_code)]
521 if unsafe { libc::ioctl(fd, libc::FIONREAD, &mut waiting) } == -1 {
522 return Err(LaunchNonceError::Unreadable {
523 fd,
524 errno: io::Error::last_os_error().raw_os_error(),
525 });
526 }
527 if waiting <= 0 {
528 return Err(LaunchNonceError::Empty { fd });
529 }
530
531 descriptor_reads.fetch_add(1, Ordering::SeqCst);
532 // SAFETY: the descriptor is open and is the pipe the daemon named by
533 // inode, so it was handed to this process for this read. Nothing else
534 // in the process reads it: every reader goes through the one cached
535 // accessor, which reaches this line at most once.
536 #[allow(unsafe_code)]
537 let mut file = File::from(unsafe { OwnedFd::from_raw_fd(fd) });
538 let mut bytes = Vec::with_capacity(64);
539 let read = file.read_to_end(&mut bytes);
540 drop(file);
541 read.map_err(|error| LaunchNonceError::Unreadable {
542 fd,
543 errno: error.raw_os_error(),
544 })?;
545 let value = String::from_utf8(bytes).map_err(|_| LaunchNonceError::NotUtf8 { fd })?;
546 Ok(Some(LaunchNonce {
547 value,
548 source: LaunchNonceSource::Fd,
549 }))
550 }
551
552 pub(super) fn fstat(fd: RawFd) -> io::Result<libc::stat> {
553 let mut stat = std::mem::MaybeUninit::<libc::stat>::uninit();
554 // SAFETY: fstat writes one `struct stat` into the buffer, which is
555 // exactly that size, and writes nothing when it fails.
556 #[allow(unsafe_code)]
557 if unsafe { libc::fstat(fd, stat.as_mut_ptr()) } == -1 {
558 return Err(io::Error::last_os_error());
559 }
560 // SAFETY: fstat succeeded, so it filled the buffer.
561 #[allow(unsafe_code)]
562 Ok(unsafe { stat.assume_init() })
563 }
564}
565
566#[cfg(test)]
567pub(crate) mod tests;
568
569#[cfg(test)]
570mod pipe_rollout_tests {
571 use super::*;
572
573 fn nonce(value: &str, source: LaunchNonceSource) -> LaunchNonce {
574 LaunchNonce {
575 value: value.to_owned(),
576 source,
577 }
578 }
579
580 #[test]
581 fn phase_one_pipe_failure_uses_the_permitted_environment_source() {
582 let result = pipe_or_permitted_env(
583 Err(LaunchNonceError::PipeEmpty),
584 Some(nonce("env-secret", LaunchNonceSource::Env)),
585 );
586 let value = result.unwrap().unwrap();
587 assert_eq!(value.value(), "env-secret");
588 assert_eq!(value.source(), LaunchNonceSource::Env);
589 }
590
591 #[test]
592 fn phase_one_partial_pipe_nonce_uses_the_permitted_environment_source() {
593 let result = pipe_or_permitted_env(
594 Ok(Some(nonce("partial", LaunchNonceSource::Pipe))),
595 Some(nonce("env-secret", LaunchNonceSource::Env)),
596 );
597 let value = result.unwrap().unwrap();
598 assert_eq!(value.value(), "env-secret");
599 assert_eq!(value.source(), LaunchNonceSource::Env);
600 }
601
602 #[test]
603 fn phase_one_complete_pipe_nonce_keeps_the_pipe_source() {
604 let result = pipe_or_permitted_env(
605 Ok(Some(nonce("same-secret", LaunchNonceSource::Pipe))),
606 Some(nonce("same-secret", LaunchNonceSource::Env)),
607 );
608 let value = result.unwrap().unwrap();
609 assert_eq!(value.value(), "same-secret");
610 assert_eq!(value.source(), LaunchNonceSource::Pipe);
611 }
612
613 #[test]
614 fn strict_pipe_failure_without_permission_stays_an_error() {
615 assert_eq!(
616 pipe_or_permitted_env(Err(LaunchNonceError::PipeEmpty), None),
617 Err(LaunchNonceError::PipeEmpty)
618 );
619 }
620}