1use std::fs::{File, OpenOptions};
9use std::io;
10use std::path::{Path, PathBuf};
11use std::time::{SystemTime, UNIX_EPOCH};
12
13pub const SPOOL_DIR_ENV: &str = "RUNNING_PROCESS_PROBE_SPOOL_DIR";
15pub const REPORT_DIR_ENV: &str = "RUNNING_PROCESS_PROBE_CRASH_DIR";
17
18pub(crate) const MAGIC: &[u8; 8] = b"RPCRASH1";
19pub(crate) const VERSION: u32 = 2;
20pub const RECORD_SIZE: usize = 16 * 1024;
22pub(crate) const HEADER_SIZE: usize = 128;
23pub(crate) const TEXT_SIZE: usize = 64;
24pub(crate) const TEXT_FIELDS: usize = 4;
25pub(crate) const MODULE_OFFSET: usize = HEADER_SIZE + TEXT_SIZE * TEXT_FIELDS;
26pub(crate) const MAX_MODULES: usize = 16;
27pub(crate) const MODULE_SIZE: usize = 128;
28pub(crate) const THREAD_OFFSET: usize = MODULE_OFFSET + MAX_MODULES * MODULE_SIZE;
29pub(crate) const MAX_THREADS: usize = 32;
30pub(crate) const MAX_FRAMES: usize = 16;
31pub(crate) const FRAME_SIZE: usize = 16;
32pub(crate) const THREAD_SIZE: usize = 16 + MAX_FRAMES * FRAME_SIZE;
33pub(crate) const RAW_OFFSET: usize = THREAD_OFFSET + MAX_THREADS * THREAD_SIZE;
34pub(crate) const CWD_SIZE: usize = 1024;
35pub(crate) const CWD_OFFSET: usize = RECORD_SIZE - CWD_SIZE;
36pub(crate) const MAX_RAW_CONTEXT: usize = CWD_OFFSET - RAW_OFFSET;
37const V1_MAX_RAW_CONTEXT: usize = RECORD_SIZE - RAW_OFFSET;
38
39const OFF_VERSION: usize = 8;
40const OFF_RECORD_SIZE: usize = 12;
41const OFF_PID: usize = 16;
42const OFF_TID: usize = 24;
43const OFF_FAULT_CODE: usize = 32;
44const OFF_FAULT_ADDRESS: usize = 40;
45const OFF_UNIX_MS: usize = 48;
46const OFF_THREAD_COUNT: usize = 56;
47const OFF_RAW_LEN: usize = 60;
48const OFF_FLAGS: usize = 64;
49const OFF_MODULE_COUNT: usize = 68;
50const OFF_CREATION_TIME_MS: usize = 72;
51const FLAG_TRUNCATED_THREADS: u32 = 1;
52const FLAG_TRUNCATED_CONTEXT: u32 = 2;
53const FLAG_TRUNCATED_MODULES: u32 = 4;
54const KNOWN_FLAGS: u32 = FLAG_TRUNCATED_THREADS | FLAG_TRUNCATED_CONTEXT | FLAG_TRUNCATED_MODULES;
55
56#[derive(Clone, Debug, PartialEq, Eq)]
58pub struct CrashMetadata {
59 pub app_class: String,
61 pub app_name: String,
63 pub app_version: String,
65 pub instance_name: String,
67 pub creation_time_ms: u64,
69 pub cwd: String,
71}
72
73#[derive(Clone, Debug, PartialEq, Eq)]
75pub struct CrashModule {
76 pub identity: String,
78}
79
80#[derive(Clone, Debug, PartialEq, Eq)]
82pub struct CrashFrame {
83 pub module_index: Option<u32>,
85 pub relative_address: u64,
87}
88
89#[derive(Clone, Debug, PartialEq, Eq)]
91pub struct CrashThread {
92 pub os_tid: u64,
94 pub frames: Vec<CrashFrame>,
96}
97
98#[derive(Clone, Debug, PartialEq, Eq)]
100pub struct RawCrashReport {
101 pub pid: u32,
103 pub tid: u64,
105 pub fault_code: i64,
107 pub fault_address: u64,
109 pub crash_unix_ms: u64,
111 pub metadata: CrashMetadata,
113 pub modules: Vec<CrashModule>,
115 pub threads: Vec<CrashThread>,
117 pub raw_context: Vec<u8>,
119 pub truncated: bool,
121}
122
123pub fn spool_dir() -> PathBuf {
125 std::env::var_os(SPOOL_DIR_ENV)
126 .map(PathBuf::from)
127 .unwrap_or_else(|| default_owner_root().join("probe-spool"))
128}
129
130pub fn report_dir() -> PathBuf {
132 std::env::var_os(REPORT_DIR_ENV)
133 .map(PathBuf::from)
134 .unwrap_or_else(|| default_owner_root().join("probe-crashes"))
135}
136
137#[cfg(unix)]
138fn default_owner_root() -> PathBuf {
139 if let Some(runtime) = std::env::var_os("XDG_RUNTIME_DIR") {
140 let runtime = PathBuf::from(runtime);
141 if runtime.is_absolute() {
142 return runtime.join("running-process");
143 }
144 }
145 let uid = unsafe { libc::geteuid() };
147 std::env::temp_dir().join(format!("running-process-{uid}"))
148}
149
150#[cfg(not(unix))]
151fn default_owner_root() -> PathBuf {
152 std::env::temp_dir().join("running-process")
153}
154
155pub(crate) fn create_sink(
156 metadata: &CrashMetadata,
157) -> io::Result<(File, PathBuf, [u8; RECORD_SIZE])> {
158 let dir = spool_dir();
159 create_private_dir(&dir)?;
160 let now = unix_ms();
161 let path = dir.join(format!(
162 "pending-{}-{now}-{:016x}.rpcrash",
163 std::process::id(),
164 random_suffix()
165 ));
166
167 let mut options = OpenOptions::new();
168 options.create_new(true).write(true);
169 #[cfg(unix)]
170 {
171 use std::os::unix::fs::OpenOptionsExt as _;
172 options.mode(0o600);
173 }
174 let file = options.open(&path)?;
175 let mut record = [0u8; RECORD_SIZE];
176 initialize(&mut record, metadata);
177 Ok((file, path, record))
178}
179
180fn random_suffix() -> u64 {
181 let mut bytes = [0u8; 8];
182 if getrandom::fill(&mut bytes).is_ok() {
183 u64::from_le_bytes(bytes)
184 } else {
185 unix_ms() ^ u64::from(std::process::id())
186 }
187}
188
189pub fn create_private_dir(path: &Path) -> io::Result<()> {
191 #[cfg(unix)]
192 {
193 use std::os::unix::fs::{DirBuilderExt as _, MetadataExt as _};
194 let mut builder = std::fs::DirBuilder::new();
195 builder.recursive(true).mode(0o700);
196 builder.create(path)?;
197 let metadata = std::fs::symlink_metadata(path)?;
198 let uid = unsafe { libc::geteuid() };
200 if metadata.file_type().is_symlink()
201 || !metadata.is_dir()
202 || metadata.uid() != uid
203 || metadata.mode() & 0o077 != 0
204 {
205 return Err(io::Error::new(
206 io::ErrorKind::PermissionDenied,
207 "crash directory must be a non-symlink owned by this user with mode 0700",
208 ));
209 }
210 }
211 #[cfg(not(unix))]
212 std::fs::create_dir_all(path)?;
213 Ok(())
214}
215
216pub(crate) fn initialize(record: &mut [u8; RECORD_SIZE], metadata: &CrashMetadata) {
217 record.fill(0);
218 record[..8].copy_from_slice(MAGIC);
219 put_u32(record, OFF_VERSION, VERSION);
220 put_u32(record, OFF_RECORD_SIZE, RECORD_SIZE as u32);
221 put_u32(record, OFF_PID, std::process::id());
222 put_metadata(record, metadata);
223}
224
225pub(crate) fn put_metadata(record: &mut [u8; RECORD_SIZE], metadata: &CrashMetadata) {
226 record[HEADER_SIZE..MODULE_OFFSET].fill(0);
227 record[CWD_OFFSET..].fill(0);
228 put_text(record, HEADER_SIZE, &metadata.app_class);
229 put_text(record, HEADER_SIZE + TEXT_SIZE, &metadata.app_name);
230 put_text(record, HEADER_SIZE + TEXT_SIZE * 2, &metadata.app_version);
231 put_text(record, HEADER_SIZE + TEXT_SIZE * 3, &metadata.instance_name);
232 put_u64(record, OFF_CREATION_TIME_MS, metadata.creation_time_ms);
233 put_text_sized(record, CWD_OFFSET, CWD_SIZE, &metadata.cwd);
234}
235
236pub(crate) fn put_sample(
237 record: &mut [u8; RECORD_SIZE],
238 modules: &[CrashModule],
239 threads: &[CrashThread],
240) {
241 record[MODULE_OFFSET..RAW_OFFSET].fill(0);
242 put_u32(record, OFF_FLAGS, 0);
243 let module_count = modules.len().min(MAX_MODULES);
244 put_u32(record, OFF_MODULE_COUNT, module_count as u32);
245 if modules.len() > MAX_MODULES {
246 set_flag(record, FLAG_TRUNCATED_MODULES);
247 }
248 for (index, module) in modules.iter().take(MAX_MODULES).enumerate() {
249 let raw = module.identity.as_bytes();
250 if raw.len() >= MODULE_SIZE {
251 set_flag(record, FLAG_TRUNCATED_MODULES);
252 }
253 put_text_sized(
254 record,
255 MODULE_OFFSET + index * MODULE_SIZE,
256 MODULE_SIZE,
257 &module.identity,
258 );
259 }
260
261 let count = threads.len().min(MAX_THREADS);
262 put_u32(record, OFF_THREAD_COUNT, count as u32);
263 if threads.len() > MAX_THREADS {
264 set_flag(record, FLAG_TRUNCATED_THREADS);
265 }
266 for (index, thread) in threads.iter().take(MAX_THREADS).enumerate() {
267 let offset = THREAD_OFFSET + index * THREAD_SIZE;
268 put_u64(record, offset, thread.os_tid);
269 let frame_count = thread
270 .frames
271 .iter()
272 .filter(|frame| {
273 frame
274 .module_index
275 .is_none_or(|module| (module as usize) < module_count)
276 })
277 .count()
278 .min(MAX_FRAMES);
279 put_u32(record, offset + 8, frame_count as u32);
280 if thread.frames.len() > frame_count {
281 set_flag(record, FLAG_TRUNCATED_THREADS);
282 }
283 for (frame_index, frame) in thread
284 .frames
285 .iter()
286 .filter(|frame| {
287 frame
288 .module_index
289 .is_none_or(|module| (module as usize) < module_count)
290 })
291 .take(MAX_FRAMES)
292 .enumerate()
293 {
294 let frame_offset = offset + 16 + frame_index * FRAME_SIZE;
295 let module_index = frame.module_index.unwrap_or(u32::MAX);
296 put_u32(record, frame_offset, module_index);
297 put_u64(record, frame_offset + 8, frame.relative_address);
298 }
299 }
300}
301
302pub(crate) unsafe fn put_crash(
310 record: *mut u8,
311 tid: u64,
312 fault_code: i64,
313 fault_address: u64,
314 raw_context: *const u8,
315 raw_len: usize,
316) {
317 let bytes = unsafe { std::slice::from_raw_parts_mut(record, RECORD_SIZE) };
319 put_u64(bytes, OFF_TID, tid);
320 put_i64(bytes, OFF_FAULT_CODE, fault_code);
321 put_u64(bytes, OFF_FAULT_ADDRESS, fault_address);
322 put_u64(bytes, OFF_UNIX_MS, unix_ms_signal_safe());
323 let copied = raw_len.min(MAX_RAW_CONTEXT);
324 put_u32(bytes, OFF_RAW_LEN, copied as u32);
325 if raw_len > MAX_RAW_CONTEXT {
326 set_flag(bytes, FLAG_TRUNCATED_CONTEXT);
327 }
328 if copied != 0 {
329 unsafe {
331 std::ptr::copy_nonoverlapping(raw_context, bytes.as_mut_ptr().add(RAW_OFFSET), copied);
332 }
333 }
334}
335
336pub fn parse(bytes: &[u8]) -> io::Result<RawCrashReport> {
338 if bytes.len() != RECORD_SIZE || &bytes[..8] != MAGIC {
339 return Err(io::Error::new(
340 io::ErrorKind::InvalidData,
341 "incomplete or invalid crash record",
342 ));
343 }
344 let version = get_u32(bytes, OFF_VERSION);
345 if (version != 1 && version != VERSION)
346 || get_u32(bytes, OFF_RECORD_SIZE) as usize != RECORD_SIZE
347 {
348 return Err(io::Error::new(
349 io::ErrorKind::InvalidData,
350 "unsupported crash record version",
351 ));
352 }
353 let flags = get_u32(bytes, OFF_FLAGS);
354 let module_count = get_u32(bytes, OFF_MODULE_COUNT) as usize;
355 let thread_count = get_u32(bytes, OFF_THREAD_COUNT) as usize;
356 let raw_len = get_u32(bytes, OFF_RAW_LEN) as usize;
357 let max_raw_context = if version == 1 {
358 V1_MAX_RAW_CONTEXT
359 } else {
360 MAX_RAW_CONTEXT
361 };
362 if flags & !KNOWN_FLAGS != 0
363 || module_count > MAX_MODULES
364 || thread_count > MAX_THREADS
365 || raw_len > max_raw_context
366 {
367 return Err(io::Error::new(
368 io::ErrorKind::InvalidData,
369 "out-of-range crash record field",
370 ));
371 }
372 let modules = (0..module_count)
373 .map(|index| CrashModule {
374 identity: get_text_sized(bytes, MODULE_OFFSET + index * MODULE_SIZE, MODULE_SIZE),
375 })
376 .collect::<Vec<_>>();
377 let mut threads = Vec::with_capacity(thread_count);
378 for index in 0..thread_count {
379 let offset = THREAD_OFFSET + index * THREAD_SIZE;
380 let frame_count = get_u32(bytes, offset + 8) as usize;
381 if frame_count > MAX_FRAMES {
382 return Err(io::Error::new(
383 io::ErrorKind::InvalidData,
384 "out-of-range crash frame count",
385 ));
386 }
387 let mut frames = Vec::with_capacity(frame_count);
388 for frame_index in 0..frame_count {
389 let frame_offset = offset + 16 + frame_index * FRAME_SIZE;
390 let raw_module = get_u32(bytes, frame_offset);
391 let module_index = if raw_module == u32::MAX {
392 None
393 } else if (raw_module as usize) < module_count {
394 Some(raw_module)
395 } else {
396 return Err(io::Error::new(
397 io::ErrorKind::InvalidData,
398 "crash frame references an unknown module",
399 ));
400 };
401 frames.push(CrashFrame {
402 module_index,
403 relative_address: get_u64(bytes, frame_offset + 8),
404 });
405 }
406 threads.push(CrashThread {
407 os_tid: get_u64(bytes, offset),
408 frames,
409 });
410 }
411 Ok(RawCrashReport {
412 pid: get_u32(bytes, OFF_PID),
413 tid: get_u64(bytes, OFF_TID),
414 fault_code: get_i64(bytes, OFF_FAULT_CODE),
415 fault_address: get_u64(bytes, OFF_FAULT_ADDRESS),
416 crash_unix_ms: get_u64(bytes, OFF_UNIX_MS),
417 metadata: CrashMetadata {
418 app_class: get_text(bytes, HEADER_SIZE),
419 app_name: get_text(bytes, HEADER_SIZE + TEXT_SIZE),
420 app_version: get_text(bytes, HEADER_SIZE + TEXT_SIZE * 2),
421 instance_name: get_text(bytes, HEADER_SIZE + TEXT_SIZE * 3),
422 creation_time_ms: if version == 1 {
423 0
424 } else {
425 get_u64(bytes, OFF_CREATION_TIME_MS)
426 },
427 cwd: if version == 1 {
428 String::new()
429 } else {
430 get_text_sized(bytes, CWD_OFFSET, CWD_SIZE)
431 },
432 },
433 modules,
434 threads,
435 raw_context: bytes[RAW_OFFSET..RAW_OFFSET + raw_len].to_vec(),
436 truncated: flags != 0,
437 })
438}
439
440pub fn encode(report: &RawCrashReport) -> [u8; RECORD_SIZE] {
445 let mut bytes = [0; RECORD_SIZE];
446 initialize(&mut bytes, &report.metadata);
447 put_sample(&mut bytes, &report.modules, &report.threads);
448 unsafe {
450 put_crash(
451 bytes.as_mut_ptr(),
452 report.tid,
453 report.fault_code,
454 report.fault_address,
455 report.raw_context.as_ptr(),
456 report.raw_context.len(),
457 );
458 }
459 put_u32(&mut bytes, OFF_PID, report.pid);
461 put_u64(&mut bytes, OFF_UNIX_MS, report.crash_unix_ms);
462 bytes
463}
464
465fn unix_ms() -> u64 {
466 SystemTime::now()
467 .duration_since(UNIX_EPOCH)
468 .map(|value| value.as_millis() as u64)
469 .unwrap_or(0)
470}
471
472#[cfg(unix)]
473fn unix_ms_signal_safe() -> u64 {
474 let mut ts = libc::timespec {
475 tv_sec: 0,
476 tv_nsec: 0,
477 };
478 if unsafe { libc::clock_gettime(libc::CLOCK_REALTIME, &raw mut ts) } == 0 {
481 (ts.tv_sec as u64)
482 .saturating_mul(1_000)
483 .saturating_add((ts.tv_nsec as u64) / 1_000_000)
484 } else {
485 0
486 }
487}
488
489#[cfg(windows)]
490fn unix_ms_signal_safe() -> u64 {
491 use windows_sys::Win32::Foundation::FILETIME;
492 use windows_sys::Win32::System::SystemInformation::GetSystemTimeAsFileTime;
493 let mut ft = FILETIME {
494 dwLowDateTime: 0,
495 dwHighDateTime: 0,
496 };
497 unsafe { GetSystemTimeAsFileTime(&raw mut ft) };
499 let ticks = (u64::from(ft.dwHighDateTime) << 32) | u64::from(ft.dwLowDateTime);
500 ticks.saturating_sub(116_444_736_000_000_000) / 10_000
501}
502
503fn put_text(bytes: &mut [u8], offset: usize, value: &str) {
504 put_text_sized(bytes, offset, TEXT_SIZE, value);
505}
506
507fn put_text_sized(bytes: &mut [u8], offset: usize, size: usize, value: &str) {
508 let raw = value.as_bytes();
509 let length = raw.len().min(size - 1);
510 bytes[offset..offset + length].copy_from_slice(&raw[..length]);
511}
512
513fn get_text(bytes: &[u8], offset: usize) -> String {
514 get_text_sized(bytes, offset, TEXT_SIZE)
515}
516
517fn get_text_sized(bytes: &[u8], offset: usize, size: usize) -> String {
518 let slice = &bytes[offset..offset + size];
519 let end = slice.iter().position(|byte| *byte == 0).unwrap_or(size);
520 String::from_utf8_lossy(&slice[..end]).into_owned()
521}
522
523fn set_flag(bytes: &mut [u8], flag: u32) {
524 put_u32(bytes, OFF_FLAGS, get_u32(bytes, OFF_FLAGS) | flag);
525}
526
527fn put_u32(bytes: &mut [u8], offset: usize, value: u32) {
528 bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
529}
530
531fn get_u32(bytes: &[u8], offset: usize) -> u32 {
532 u32::from_le_bytes(bytes[offset..offset + 4].try_into().expect("fixed range"))
533}
534
535fn put_u64(bytes: &mut [u8], offset: usize, value: u64) {
536 bytes[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
537}
538
539fn get_u64(bytes: &[u8], offset: usize) -> u64 {
540 u64::from_le_bytes(bytes[offset..offset + 8].try_into().expect("fixed range"))
541}
542
543fn put_i64(bytes: &mut [u8], offset: usize, value: i64) {
544 bytes[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
545}
546
547fn get_i64(bytes: &[u8], offset: usize) -> i64 {
548 i64::from_le_bytes(bytes[offset..offset + 8].try_into().expect("fixed range"))
549}
550
551#[cfg(test)]
552mod tests {
553 use super::*;
554
555 #[test]
556 fn fixed_record_round_trips_identity_threads_and_context() {
557 let metadata = CrashMetadata {
558 app_class: "compiler".into(),
559 app_name: "worker".into(),
560 app_version: "4.6.4".into(),
561 instance_name: "west".into(),
562 creation_time_ms: 1234,
563 cwd: "/work".into(),
564 };
565 let mut bytes = [0; RECORD_SIZE];
566 initialize(&mut bytes, &metadata);
567 let modules = vec![CrashModule {
568 identity: "/app/worker".into(),
569 }];
570 put_sample(
571 &mut bytes,
572 &modules,
573 &[CrashThread {
574 os_tid: 42,
575 frames: vec![
576 CrashFrame {
577 module_index: Some(0),
578 relative_address: 0x1234,
579 },
580 CrashFrame {
581 module_index: Some(0),
582 relative_address: 0x5678,
583 },
584 ],
585 }],
586 );
587 let raw = [1u8, 2, 3, 4];
588 unsafe {
590 put_crash(bytes.as_mut_ptr(), 42, 11, 0xdead, raw.as_ptr(), raw.len());
591 }
592 let report = parse(&bytes).expect("parse");
593 assert_eq!(report.metadata, metadata);
594 assert_eq!(report.tid, 42);
595 assert_eq!(report.fault_code, 11);
596 assert_eq!(report.fault_address, 0xdead);
597 assert_eq!(report.modules, modules);
598 assert_eq!(report.threads[0].frames[0].relative_address, 0x1234);
599 assert_eq!(report.raw_context, raw);
600 }
601
602 #[test]
603 fn version_one_records_remain_readable_without_new_tags() {
604 let metadata = CrashMetadata {
605 app_class: "legacy".into(),
606 app_name: "worker".into(),
607 app_version: "1".into(),
608 instance_name: String::new(),
609 creation_time_ms: 123,
610 cwd: "/new-layout".into(),
611 };
612 let mut bytes = [0; RECORD_SIZE];
613 initialize(&mut bytes, &metadata);
614 put_u32(&mut bytes, OFF_VERSION, 1);
615 let parsed = parse(&bytes).unwrap();
616 assert_eq!(parsed.metadata.app_class, "legacy");
617 assert_eq!(parsed.metadata.creation_time_ms, 0);
618 assert!(parsed.metadata.cwd.is_empty());
619 }
620
621 #[test]
622 fn partial_record_is_refused() {
623 assert!(parse(&[0; 100]).is_err());
624 }
625
626 #[test]
627 fn excess_threads_and_frames_are_explicitly_truncated() {
628 let metadata = CrashMetadata {
629 app_class: "a".into(),
630 app_name: "b".into(),
631 app_version: "c".into(),
632 instance_name: String::new(),
633 creation_time_ms: 1,
634 cwd: "/test".into(),
635 };
636 let mut bytes = [0; RECORD_SIZE];
637 initialize(&mut bytes, &metadata);
638 let threads = (0..MAX_THREADS + 1)
639 .map(|tid| CrashThread {
640 os_tid: tid as u64,
641 frames: vec![
642 CrashFrame {
643 module_index: None,
644 relative_address: 1,
645 };
646 MAX_FRAMES + 1
647 ],
648 })
649 .collect::<Vec<_>>();
650 put_sample(&mut bytes, &[], &threads);
651 let parsed = parse(&bytes).unwrap();
652 assert!(parsed.truncated);
653 assert_eq!(parsed.threads.len(), MAX_THREADS);
654 assert_eq!(parsed.threads[0].frames.len(), MAX_FRAMES);
655 }
656
657 #[test]
658 fn impossible_counts_and_unknown_flags_are_rejected() {
659 let metadata = CrashMetadata {
660 app_class: "a".into(),
661 app_name: "b".into(),
662 app_version: "c".into(),
663 instance_name: String::new(),
664 creation_time_ms: 1,
665 cwd: "/test".into(),
666 };
667 let mut bytes = [0; RECORD_SIZE];
668 initialize(&mut bytes, &metadata);
669 put_u32(&mut bytes, OFF_THREAD_COUNT, (MAX_THREADS + 1) as u32);
670 assert!(parse(&bytes).is_err());
671
672 initialize(&mut bytes, &metadata);
673 put_u32(&mut bytes, OFF_FLAGS, 1 << 31);
674 assert!(parse(&bytes).is_err());
675 }
676
677 #[cfg(unix)]
678 #[test]
679 fn permissive_or_symlinked_spool_directories_are_rejected() {
680 use std::os::unix::fs::{symlink, PermissionsExt as _};
681
682 let root = tempfile::tempdir().unwrap();
683 let permissive = root.path().join("permissive");
684 std::fs::create_dir(&permissive).unwrap();
685 std::fs::set_permissions(&permissive, std::fs::Permissions::from_mode(0o755)).unwrap();
686 assert!(create_private_dir(&permissive).is_err());
687
688 let private = root.path().join("private");
689 std::fs::create_dir(&private).unwrap();
690 std::fs::set_permissions(&private, std::fs::Permissions::from_mode(0o700)).unwrap();
691 let linked = root.path().join("linked");
692 symlink(&private, &linked).unwrap();
693 assert!(create_private_dir(&linked).is_err());
694 }
695}