-
Notifications
You must be signed in to change notification settings - Fork 160
Expand file tree
/
Copy pathmod.rs
More file actions
1157 lines (1008 loc) · 39.8 KB
/
mod.rs
File metadata and controls
1157 lines (1008 loc) · 39.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// TODO: remove unused attribute when system is cleaned up
#[cfg(target_os = "linux")]
use std::str::FromStr;
use std::{
ffi::{CStr, c_int, c_long, c_uint},
fmt, fs, io,
mem::MaybeUninit,
ops,
os::unix,
path::PathBuf,
};
use crate::{
common::{Error, SudoPath, SudoString},
cutils::*,
};
use interface::{DeviceId, GroupId, ProcessId, UserId};
pub use libc::PATH_MAX;
use libc::STDERR_FILENO;
#[cfg(not(target_os = "macos"))]
use libc::{CLOSE_RANGE_CLOEXEC, EINVAL, ENOSYS};
use time::ProcessCreateTime;
use self::signal::SignalNumber;
pub(crate) mod audit;
// generalized traits for when we want to hide implementations
pub mod interface;
pub mod file;
pub mod time;
pub mod timestamp;
pub mod signal;
pub mod term;
pub mod wait;
#[cfg(not(any(target_os = "freebsd", target_os = "linux")))]
compile_error!("sudo-rs only works on Linux and FreeBSD");
pub(crate) fn _exit(status: c_int) -> ! {
// SAFETY: this function is safe to call
unsafe { libc::_exit(status) }
}
fn set_cloexec(fd: c_int) -> io::Result<()> {
// SAFETY: This only sets the CLOEXEC flag; it does not close or invalidate the fd.
unsafe { cerr(libc::fcntl(fd, libc::F_SETFD, libc::FD_CLOEXEC)) }.map(|_| ())
}
/// Attempts close_range(CLOSE_RANGE_CLOEXEC) for all fds >= lowfd.
/// Returns Ok(true) on success, Ok(false) if unsupported, Err on any other failure.
#[cfg(not(target_os = "macos"))]
fn try_close_range(lowfd: c_int) -> io::Result<bool> {
// SAFETY: this function is safe to call:
// - any errors while closing a specific fd will be effectively ignored
#[allow(clippy::diverging_sub_expression)]
let res = unsafe {
'a: {
#[cfg(not(target_os = "linux"))]
break 'a cerr(libc::close_range(
lowfd as c_uint,
c_uint::MAX,
CLOSE_RANGE_CLOEXEC as c_int,
));
// on Linux, close_range was only added in glibc 2.34, and is not
// part of musl, so we go perform a straight syscall instead
#[cfg(target_os = "linux")]
break 'a cerr(libc::syscall(
libc::SYS_close_range,
lowfd as c_uint,
c_uint::MAX,
CLOSE_RANGE_CLOEXEC as c_uint,
));
}
};
match res {
Ok(_) => Ok(true),
Err(e) if e.raw_os_error() == Some(ENOSYS) || e.raw_os_error() == Some(EINVAL) => Ok(false),
Err(e) => Err(e),
}
}
/// Attempts to set CLOEXEC on all fds >= lowfd via proc_pidinfo(PROC_PIDLISTFDS).
/// Returns Ok(true) on success, Ok(false) if proc_pidinfo is unavailable, Err on failure.
#[cfg(target_os = "macos")]
fn try_proc_pidinfo(lowfd: c_int) -> io::Result<bool> {
const WIGGLE_ROOM: i32 = 4;
let pid = unsafe { libc::getpid() };
// SAFETY: passing a null buffer with size 0 just queries the required buffer size.
let needed =
unsafe { libc::proc_pidinfo(pid, libc::PROC_PIDLISTFDS, 0, std::ptr::null_mut(), 0) };
// If needed is zero, there are no open files.
// If needed is less than zero, alternate methods should be considered
// Either way, we want to recover, so throw unavailable.
if needed <= 0 {
return Ok(false);
}
// Allocate with wiggle room for any extraneous fds opened between the two calls.
let cap = needed + (libc::PROC_PIDLISTFD_SIZE * WIGGLE_ROOM);
let count = cap as usize / std::mem::size_of::<libc::proc_fdinfo>();
let mut buf: Vec<libc::proc_fdinfo> = Vec::with_capacity(count);
// SAFETY: since proc_fdinfo is a plain C struct... we assume that proc_pidinfo fills the allocation safely.
let filled =
unsafe { libc::proc_pidinfo(pid, libc::PROC_PIDLISTFDS, 0, buf.as_mut_ptr().cast(), cap) };
if filled <= 0 {
return Ok(false);
}
let pidlistfd_size = std::mem::size_of::<libc::proc_fdinfo>();
let n = filled as usize / pidlistfd_size;
// SAFETY: proc_pidinfo wrote `n` valid entries into the buffer.
unsafe { buf.set_len(n) };
for info in &buf {
if info.proc_fd >= lowfd {
set_cloexec(info.proc_fd)?;
}
}
Ok(true)
}
/// Sets CLOEXEC on all fds >= lowfd by walking an fd directory
/// (/proc/self/fd on Linux/FreeBSD, /dev/fd on macOS).
fn cloexec_via_fd_dir(lowfd: c_int, path: &str) -> io::Result<()> {
for entry in fs::read_dir(path)? {
let file_name = entry?.file_name();
if file_name == "." || file_name == ".." {
continue;
}
let fd = file_name
.to_str()
.and_then(|s| s.parse::<c_int>().ok())
.ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidData,
"fd directory returned non-integer fd name",
)
})?;
if fd >= lowfd {
set_cloexec(fd)?;
}
}
Ok(())
}
/// Mark every file descriptor that is not one of the IO streams as CLOEXEC.
pub(crate) fn mark_fds_as_cloexec() -> io::Result<()> {
let lowfd = STDERR_FILENO + 1;
// The kernel doesn't support close_range or CLOSE_RANGE_CLOEXEC,
// so our fallback on all platforms to finding all open fds using /proc/self/fd.
#[cfg(not(target_os = "macos"))]
{
if try_close_range(lowfd)? {
return Ok(());
}
cloexec_via_fd_dir(lowfd, "/proc/self/fd")
}
#[cfg(target_os = "macos")]
{
if try_proc_pidinfo(lowfd)? {
return Ok(());
}
cloexec_via_fd_dir(lowfd, "/dev/fd")
}
}
pub(crate) enum ForkResult {
// Parent process branch with the child process' PID.
Parent(ProcessId),
// Child process branch.
Child,
}
/// Create a new process.
///
/// # Safety
///
/// Must not be called in multithreaded programs.
pub(crate) unsafe fn fork() -> io::Result<ForkResult> {
// FIXME add debug assertion that we are not currently using multiple threads.
// SAFETY: Calling async-signal-unsafe functions after fork is safe as the program is single
// threaded at this point according to the safety invariant of this function.
let pid = cerr(unsafe { libc::fork() })?;
if pid == 0 {
Ok(ForkResult::Child)
} else {
Ok(ForkResult::Parent(ProcessId::new(pid)))
}
}
/// Create a new process with extra precautions for usage in tests.
///
/// # Safety
///
/// In a multithreaded program, only async-signal-safe functions are guaranteed to work in the
/// child process until a call to `execve` or a similar function is done.
#[cfg(test)]
unsafe fn fork_for_test(child_func: impl FnOnce() -> std::convert::Infallible) -> ProcessId {
use std::io::Write;
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::process::exit;
// SAFETY: Not really safe, but this is test only code.
match unsafe { fork() }.unwrap() {
ForkResult::Child => {
// Make sure that panics in the child always abort the process.
let err = match catch_unwind(AssertUnwindSafe(child_func)) {
Ok(res) => match res {},
Err(err) => err,
};
let s = if let Some(s) = err.downcast_ref::<&str>() {
s
} else if let Some(s) = err.downcast_ref::<String>() {
s
} else {
"Box<dyn Any>"
};
let _ = writeln!(std::io::stderr(), "{s}");
exit(101);
}
ForkResult::Parent(pid) => pid,
}
}
pub fn setsid() -> io::Result<ProcessId> {
// SAFETY: this function is memory-safe to call
Ok(ProcessId::new(cerr(unsafe { libc::setsid() })?))
}
#[derive(Clone)]
#[cfg_attr(test, derive(PartialEq))]
pub struct Hostname {
inner: String,
}
impl fmt::Debug for Hostname {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("Hostname").field(&self.inner).finish()
}
}
impl fmt::Display for Hostname {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.inner)
}
}
impl ops::Deref for Hostname {
type Target = str;
fn deref(&self) -> &str {
&self.inner
}
}
impl Hostname {
#[cfg(test)]
pub fn fake(hostname: &str) -> Self {
Self {
inner: hostname.to_string(),
}
}
pub fn resolve() -> Self {
// see `man 2 gethostname`
const MAX_HOST_NAME_SIZE_ACCORDING_TO_SUSV2: c_long = 255;
// POSIX.1 systems limit hostnames to `HOST_NAME_MAX` bytes
// not including null-byte in the count
let max_hostname_size = sysconf(libc::_SC_HOST_NAME_MAX)
.unwrap_or(MAX_HOST_NAME_SIZE_ACCORDING_TO_SUSV2)
as usize;
let buffer_size = max_hostname_size + 1 /* null byte delimiter */ ;
let mut buf = vec![0; buffer_size];
// SAFETY: we are passing a valid pointer to gethostname
match cerr(unsafe { libc::gethostname(buf.as_mut_ptr(), buffer_size) }) {
Ok(_) => Self {
// SAFETY: gethostname succeeded, so `buf` will hold a null-terminated C string
inner: unsafe { string_from_ptr(buf.as_ptr()) },
},
// ENAMETOOLONG is returned when hostname is greater than `buffer_size`
Err(_) => {
// but we have chosen a `buffer_size` larger than `max_hostname_size` so no truncation error is possible
panic!("Unexpected error while retrieving hostname, this should not happen");
}
}
}
}
pub fn syslog(priority: c_int, facility: c_int, message: &CStr) {
const MSG: &CStr = c"%s";
// SAFETY:
// - "MSG" is a constant expression that is a null-terminated C string that represents "%s";
// this also means that to achieve safety we MUST pass one more argument to syslog that is a proper
// pointer to a null-terminated C string
// - message.as_ptr() is a pointer to a proper null-terminated C string (message being a &CStr)
// for more info: read the manpage for syslog(2)
unsafe {
libc::syslog(priority | facility, MSG.as_ptr(), message.as_ptr());
}
}
/// Makes sure that that the target is included in the groups, and is its first element
fn inject_group(target: GroupId, groups: &mut Vec<GroupId>) {
if let Some(index) = groups.iter().position(|id| id == &target) {
// make sure the requested group id is the first in the list (necessary on FreeBSD)
groups.swap(0, index)
} else {
// add target group to list of additional groups if not present
groups.insert(0, target);
}
}
/// Set the supplementary groups -- returns a c_int to mimic a libc function
fn set_supplementary_groups(groups: &[GroupId]) -> io::Result<()> {
// On FreeBSD, setgruops expects the size to be passed as a i32, so the below
// conversion protects a very extreme case of arithmetic conversion error
#[allow(irrefutable_let_patterns)]
#[allow(clippy::useless_conversion)]
let Ok(len) = groups.len().try_into() else {
return Err(io::Error::other("too many groups"));
};
// SAFETY: setgroups is passed a valid pointer to a chunk of memory of the correct size
// We can cast to gid_t because `GroupId` is marked as transparent
cerr(unsafe { libc::setgroups(len, groups.as_ptr().cast::<libc::gid_t>()) })?;
Ok(())
}
/// set target user and groups (uid, gid, additional groups) for a command
pub fn set_target_user(
cmd: &mut std::process::Command,
mut target_user: User,
target_group: Group,
) {
use std::os::unix::process::CommandExt;
inject_group(target_group.gid, &mut target_user.groups);
// we need to do this in a `pre_exec` call since the `groups` method in `process::Command` is unstable
// see https://github.com/rust-lang/rust/blob/a01b4cc9f375f1b95fa8195daeea938d3d9c4c34/library/std/src/sys/unix/process/process_unix.rs#L329-L352
// for the std implementation of the libc calls to `setgroups`, `setgid` and `setuid`
// SAFETY: Setuid, setgid and setgroups are async-signal-safe.
unsafe {
cmd.pre_exec(move || {
set_supplementary_groups(&target_user.groups)?;
// setgid and setuid set the real, effective and saved version of the gid and uid
// respectively rather than just the real gid and uid. The original sudo uses setresgid
// and setresuid instead with all three arguments equal, but as this does the same as
// setgid and setuid using the latter is fine too.
cerr(libc::setgid(target_group.gid.inner()))?;
cerr(libc::setuid(target_user.uid.inner()))?;
Ok(())
});
}
}
/// Send a signal to a process with the specified ID.
pub fn kill(pid: ProcessId, signal: SignalNumber) -> io::Result<()> {
// SAFETY: This function cannot cause UB even if `pid` is not a valid process ID or if
// `signal` is not a valid signal code.
cerr(unsafe { libc::kill(pid.inner(), signal) }).map(|_| ())
}
/// Send a signal to a process group with the specified ID.
pub fn killpg(pgid: ProcessId, signal: SignalNumber) -> io::Result<()> {
// SAFETY: This function cannot cause UB even if `pgid` is not a valid process ID or if
// `signal` is not a valid signal code.
cerr(unsafe { libc::killpg(pgid.inner(), signal) }).map(|_| ())
}
/// Get the process group ID of the current process.
pub fn getpgrp() -> ProcessId {
// SAFETY: This function is always safe to call
ProcessId::new(unsafe { libc::getpgrp() })
}
/// Get a process group ID.
pub fn getpgid(pid: ProcessId) -> io::Result<ProcessId> {
// SAFETY: This function cannot cause UB even if `pid` is not a valid process ID
Ok(ProcessId::new(cerr(unsafe { libc::getpgid(pid.inner()) })?))
}
/// Set a process group ID.
pub fn setpgid(pid: ProcessId, pgid: ProcessId) -> io::Result<()> {
// SAFETY: This function cannot cause UB even if `pid` or `pgid` are not a valid process IDs:
// https://pubs.opengroup.org/onlinepubs/007904975/functions/setpgid.html
cerr(unsafe { libc::setpgid(pid.inner(), pgid.inner()) }).map(|_| ())
}
pub fn chown<S: AsRef<CStr>>(
path: &S,
uid: impl Into<UserId>,
gid: impl Into<GroupId>,
) -> io::Result<()> {
let path = path.as_ref().as_ptr();
let uid = uid.into();
let gid = gid.into();
// SAFETY: path is a valid pointer to a null-terminated C string; chown cannot cause safety
// issues even if uid and/or gid would be invalid identifiers.
cerr(unsafe { libc::chown(path, uid.inner(), gid.inner()) }).map(|_| ())
}
#[derive(Debug, Clone, PartialEq)]
pub struct User {
pub uid: UserId,
pub gid: GroupId,
pub name: SudoString,
pub home: SudoPath,
pub shell: PathBuf,
pub groups: Vec<GroupId>,
}
impl User {
/// # Safety
/// This function expects `pwd` to be a result from a successful call to `getpwXXX_r`.
/// (It can cause UB if any of `pwd`'s pointed-to strings does not have a null-terminator.)
unsafe fn from_libc(pwd: &libc::passwd) -> Result<User, Error> {
let mut buf_len: c_int = 32;
let mut groups_buffer: Vec<libc::gid_t>;
while {
groups_buffer = vec![0; buf_len as usize];
// SAFETY: getgrouplist is passed valid pointers
// in particular `groups_buffer` is an array of `buf.len()` bytes, as required
let result = unsafe {
libc::getgrouplist(
pwd.pw_name,
pwd.pw_gid,
groups_buffer.as_mut_ptr(),
&mut buf_len,
)
};
result == -1
} {
if buf_len >= 65536 {
panic!("user has too many groups (> 65536), this should not happen");
}
buf_len *= 2;
}
groups_buffer.resize_with(buf_len as usize, || {
panic!("invalid groups count returned from getgrouplist, this should not happen")
});
// SAFETY: All pointers were initialized by a successful call to `getpwXXX_r` as per the
// safety invariant of this function.
unsafe {
Ok(User {
uid: UserId::new(pwd.pw_uid),
gid: GroupId::new(pwd.pw_gid),
name: SudoString::new(string_from_ptr(pwd.pw_name))?,
home: SudoPath::new(os_string_from_ptr(pwd.pw_dir).into())?,
shell: os_string_from_ptr(pwd.pw_shell).into(),
groups: groups_buffer
.iter()
.map(|id| GroupId::new(*id))
.collect::<Vec<_>>(),
})
}
}
pub fn from_uid(uid: UserId) -> Result<Option<User>, Error> {
let max_pw_size = sysconf(libc::_SC_GETPW_R_SIZE_MAX).unwrap_or(16_384);
let mut buf = vec![0; max_pw_size as usize];
let mut pwd = MaybeUninit::uninit();
let mut pwd_ptr = std::ptr::null_mut();
// SAFETY: getpwuid_r is passed valid (although partly uninitialized) pointers to memory,
// in particular `buf` points to an array of `buf.len()` bytes, as required.
// After this call, if `pwd_ptr` is not NULL, `*pwd_ptr` and `pwd` will be aliased;
// but we never dereference `pwd_ptr`.
cerr(unsafe {
libc::getpwuid_r(
uid.inner(),
pwd.as_mut_ptr(),
buf.as_mut_ptr(),
buf.len(),
&mut pwd_ptr,
)
})?;
if pwd_ptr.is_null() {
Ok(None)
} else {
// SAFETY: pwd_ptr was not null, and getpwuid_r succeeded, so we have assurances that
// the `pwd` structure was written to by getpwuid_r
let pwd = unsafe { pwd.assume_init() };
// SAFETY: `pwd` was obtained by a call to getpwXXX_r, as required.
unsafe { Self::from_libc(&pwd).map(Some) }
}
}
pub fn effective_uid() -> UserId {
// SAFETY: this function cannot cause memory safety issues
UserId::new(unsafe { libc::geteuid() })
}
pub fn effective_gid() -> GroupId {
// SAFETY: this function cannot cause memory safety issues
GroupId::new(unsafe { libc::getegid() })
}
pub fn real_uid() -> UserId {
// SAFETY: this function cannot cause memory safety issues
UserId::new(unsafe { libc::getuid() })
}
pub fn real_gid() -> GroupId {
// SAFETY: this function cannot cause memory safety issues
GroupId::new(unsafe { libc::getgid() })
}
pub fn real() -> Result<Option<User>, Error> {
Self::from_uid(Self::real_uid())
}
pub fn primary_group(&self) -> std::io::Result<Group> {
// Use from_gid_unchecked here to ensure that we can still resolve when
// the /etc/group entry for the primary group is missing.
Group::from_gid_unchecked(self.gid)
}
pub fn from_name(name_c: &CStr) -> Result<Option<User>, Error> {
let max_pw_size = sysconf(libc::_SC_GETPW_R_SIZE_MAX).unwrap_or(16_384);
let mut buf = vec![0; max_pw_size as usize];
let mut pwd = MaybeUninit::uninit();
let mut pwd_ptr = std::ptr::null_mut();
// SAFETY: analogous to getpwuid_r above
cerr(unsafe {
libc::getpwnam_r(
name_c.as_ptr(),
pwd.as_mut_ptr(),
buf.as_mut_ptr(),
buf.len(),
&mut pwd_ptr,
)
})?;
if pwd_ptr.is_null() {
Ok(None)
} else {
// SAFETY: pwd_ptr was not null, and getpwnam_r succeeded, so we have assurances that
// the `pwd` structure was written to by getpwnam_r
let pwd = unsafe { pwd.assume_init() };
// SAFETY: `pwd` was obtained by a call to getpwXXX_r, as required.
unsafe { Self::from_libc(&pwd).map(Some) }
}
}
}
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(PartialEq))]
pub struct Group {
pub gid: GroupId,
pub name: Option<String>,
}
impl Group {
/// # Safety
/// This function expects `grp` to be a result from a successful call to `getgrXXX_r`.
/// In particular the grp.gr_mem pointer is assumed to be non-null, and pointing to a
/// null-terminated list; the pointed-to strings are expected to be null-terminated.
unsafe fn from_libc(grp: &libc::group) -> Group {
// SAFETY: The name pointer is initialized by a successful call to `getgrXXX_r` as per the
// safety invariant of this function.
let name = unsafe { string_from_ptr(grp.gr_name) };
Group {
gid: GroupId::new(grp.gr_gid),
name: Some(name),
}
}
/// Lookup group for gid without returning an error when a /etc/group entry is missing.
fn from_gid_unchecked(gid: GroupId) -> std::io::Result<Group> {
let max_gr_size = sysconf(libc::_SC_GETGR_R_SIZE_MAX).unwrap_or(16_384);
let mut buf = vec![0; max_gr_size as usize];
let mut grp = MaybeUninit::uninit();
let mut grp_ptr = std::ptr::null_mut();
// SAFETY: analogous to getpwuid_r above
cerr(unsafe {
libc::getgrgid_r(
gid.inner(),
grp.as_mut_ptr(),
buf.as_mut_ptr(),
buf.len(),
&mut grp_ptr,
)
})?;
if grp_ptr.is_null() {
Ok(Group { gid, name: None })
} else {
// SAFETY: grp_ptr was not null, and getgrgid_r succeeded, so we have assurances that
// the `grp` structure was written to by getgrgid_r
let grp = unsafe { grp.assume_init() };
// SAFETY: `pwd` was obtained by a call to getgrXXX_r, as required.
Ok(unsafe { Group::from_libc(&grp) })
}
}
pub fn from_gid(gid: GroupId) -> std::io::Result<Option<Group>> {
let group = Self::from_gid_unchecked(gid)?;
if group.name.is_none() {
// No entry in /etc/group
Ok(None)
} else {
Ok(Some(group))
}
}
pub fn from_name(name_c: &CStr) -> std::io::Result<Option<Group>> {
let max_gr_size = sysconf(libc::_SC_GETGR_R_SIZE_MAX).unwrap_or(16_384);
let mut buf = vec![0; max_gr_size as usize];
let mut grp = MaybeUninit::uninit();
let mut grp_ptr = std::ptr::null_mut();
// SAFETY: analogous to getpwuid_r above
cerr(unsafe {
libc::getgrnam_r(
name_c.as_ptr(),
grp.as_mut_ptr(),
buf.as_mut_ptr(),
buf.len(),
&mut grp_ptr,
)
})?;
if grp_ptr.is_null() {
Ok(None)
} else {
// SAFETY: grp_ptr was not null, and getgrgid_r succeeded, so we have assurances that
// the `grp` structure was written to by getgrgid_r
let grp = unsafe { grp.assume_init() };
// SAFETY: `pwd` was obtained by a call to getgrXXX_r, as required.
Ok(Some(unsafe { Group::from_libc(&grp) }))
}
}
}
pub enum WithProcess {
Current,
Other(ProcessId),
}
impl WithProcess {
#[cfg(target_os = "linux")]
fn to_proc_string(&self) -> String {
match self {
WithProcess::Current => "self".into(),
WithProcess::Other(pid) => pid.to_string(),
}
}
}
#[derive(Debug, Clone)]
pub struct Process {
pub parent_pid: Option<ProcessId>,
pub session_id: ProcessId,
}
impl Process {
pub fn new() -> Process {
Process {
parent_pid: Self::parent_id(),
session_id: Self::session_id(),
}
}
/// Return the process identifier for the current process
pub fn process_id() -> ProcessId {
// NOTE libstd casts the `i32` that `libc::getpid` returns into `u32`
// here we cast it back into `i32` (`ProcessId`)
ProcessId::new(std::process::id() as i32)
}
/// Return the parent process identifier for the current process
pub fn parent_id() -> Option<ProcessId> {
// NOTE libstd casts the `i32` that `libc::getppid` returns into `u32`
// here we cast it back into `i32` (`ProcessId`)
let pid = ProcessId::new(unix::process::parent_id() as i32);
if !pid.is_valid() { None } else { Some(pid) }
}
/// Get the session id for the current process
pub fn session_id() -> ProcessId {
// SAFETY: this function is explicitly safe to call with argument 0,
// and more generally getsid will never cause memory safety issues.
ProcessId::new(unsafe { libc::getsid(0) })
}
/// Returns the device identifier of the TTY device that is currently
/// attached to the given process
#[cfg(target_os = "linux")]
pub fn tty_device_id(pid: WithProcess) -> std::io::Result<Option<DeviceId>> {
// device id of tty is displayed as a signed integer of 32 bits
let data: i32 = read_proc_stat(pid, 6 /* tty_nr */)?;
if data == 0 {
Ok(None)
} else {
// While the integer was displayed as signed in the proc stat file,
// we actually need to interpret the bits of that integer as an unsigned
// int. We convert via u32 because a direct conversion to DeviceId
// would use sign extension, which would result in a different bit
// representation
Ok(Some(DeviceId::new(data as u64)))
}
}
#[cfg(target_os = "freebsd")]
fn get_proc_info(pid: WithProcess) -> std::io::Result<libc::kinfo_proc> {
use std::ffi::c_void;
use std::ptr;
let mut ki_proc: Vec<libc::kinfo_proc> = Vec::with_capacity(1);
let pid = match pid {
WithProcess::Current => std::process::id() as i32,
WithProcess::Other(pid) => pid.inner(),
};
loop {
let mut size = ki_proc.capacity() * size_of::<libc::kinfo_proc>();
// SAFETY: KERN_PROC_PID only reads data into the ki_proc list. It
// does not write more than `size` bytes to the pointer.
match cerr(unsafe {
libc::sysctl(
[
libc::CTL_KERN,
libc::KERN_PROC,
libc::KERN_PROC_PID,
pid,
size_of::<libc::kinfo_proc>() as i32,
1,
]
.as_ptr(),
4,
ki_proc.as_mut_ptr().cast::<c_void>(),
&mut size,
ptr::null(),
0,
)
}) {
Ok(_) => {
assert!(size >= size_of::<libc::kinfo_proc>());
// SAFETY: The above sysctl has initialized at least `size` bytes. We have
// asserted that this is at least a single element.
unsafe {
ki_proc.set_len(1);
}
break;
}
Err(e) if e.raw_os_error() == Some(libc::ENOMEM) => {
// Vector not big enough. Grow it by 10% and try again.
ki_proc.reserve(ki_proc.capacity() + (ki_proc.capacity() + 9) / 10);
}
Err(e) => return Err(e),
}
}
Ok(ki_proc[0])
}
/// Returns the device identifier of the TTY device that is currently
/// attached to the given process
#[cfg(target_os = "freebsd")]
pub fn tty_device_id(pid: WithProcess) -> std::io::Result<Option<DeviceId>> {
let ki_proc = Self::get_proc_info(pid)?;
if ki_proc.ki_tdev == !0 {
Ok(None)
} else {
Ok(Some(DeviceId::new(ki_proc.ki_tdev)))
}
}
/// Get the process starting time of a specific process
#[cfg(target_os = "linux")]
pub fn starting_time(pid: WithProcess) -> io::Result<ProcessCreateTime> {
let process_start: u64 = read_proc_stat(pid, 21 /* start_time */)?;
// the startime field is stored in ticks since the system start, so we need to know how many
// ticks go into a second
let ticks_per_second = crate::cutils::sysconf(libc::_SC_CLK_TCK).ok_or_else(|| {
io::Error::other("Could not retrieve system config variable for ticks per second")
})? as u64;
// finally compute the system time at which the process was started
Ok(ProcessCreateTime::new(
(process_start / ticks_per_second) as i64,
((process_start % ticks_per_second) * (1_000_000_000 / ticks_per_second)) as i64,
))
}
/// Get the process starting time of a specific process
#[cfg(target_os = "freebsd")]
pub fn starting_time(pid: WithProcess) -> io::Result<ProcessCreateTime> {
let ki_proc = Self::get_proc_info(pid)?;
let ki_start = ki_proc.ki_start;
#[allow(clippy::useless_conversion)]
Ok(ProcessCreateTime::new(
i64::from(ki_start.tv_sec),
i64::from(ki_start.tv_usec) * 1000,
))
}
}
/// Read the n-th field (with 0-based indexing) from `/proc/<pid>/self`.
///
/// See ["Table 1-4: Contents of the stat fields" of "The /proc
/// Filesystem"][proc_stat_fields] in the Linux docs for all available fields.
///
/// IMPORTANT: the first two fields are not accessible with this routine.
///
/// [proc_stat_fields]: https://www.kernel.org/doc/html/latest/filesystems/proc.html#id10
#[cfg(target_os = "linux")]
fn read_proc_stat<T: FromStr>(pid: WithProcess, field_idx: isize) -> io::Result<T> {
// the first two fields are skipped by the code below, and we never need them,
// so no point in implementing code for it in this private function.
debug_assert!(field_idx >= 2);
// read from a specific pid file, or use `self` to refer to our own process
let pidref = pid.to_proc_string();
// read the data from the stat file for the process with the given pid
let path = PathBuf::from_iter(&["/proc", &pidref, "stat"]);
let proc_stat = std::fs::read(path)?;
// first get the part of the stat file past the second argument, we then reverse
// search for a ')' character and start the search for the starttime field from there on
let skip_past_second_arg = proc_stat.iter().rposition(|b| *b == b')').ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"Could not find position of 'comm' field in process stat",
)
})?;
let mut stat = &proc_stat[skip_past_second_arg..];
// we've now passed the first two fields, so we are at index 1, now we skip over
// fields until we arrive at the field we are searching for
let mut curr_field = 1;
while curr_field < field_idx && !stat.is_empty() {
if stat[0] == b' ' {
curr_field += 1;
}
stat = &stat[1..];
}
// The expected field cannot be in the file anymore when we are at EOF
if stat.is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"Stat file was not of the expected format",
));
}
// we've now arrived at the field we are looking for, we now check how
// long this field is by finding where the next space is
let mut idx = 0;
while stat[idx] != b' ' && idx < stat.len() {
idx += 1;
}
let field = &stat[0..idx];
// we first convert the data to a string slice, this should not fail with a normal /proc filesystem
let fielddata = std::str::from_utf8(field).map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidInput,
"Could not interpret byte slice as string",
)
})?;
// then we convert the string slice to whatever the requested type was
fielddata.parse().map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidInput,
"Could not interpret string as number",
)
})
}
pub fn escape_os_str_lossy(s: &std::ffi::OsStr) -> String {
s.to_string_lossy().escape_default().collect()
}
pub fn make_zeroed_sigaction() -> libc::sigaction {
// SAFETY: since sigaction is a C struct, all-zeroes is a valid representation
// We cannot use a "literal struct" initialization method since the exact representation
// of libc::sigaction is not fixed, see e.g. https://github.com/trifectatechfoundation/sudo-rs/issues/829
unsafe { std::mem::zeroed() }
}
#[cfg(all(test, target_os = "linux"))]
pub(crate) const ROOT_GROUP_NAME: &str = "root";
#[cfg(all(test, not(target_os = "linux")))]
pub(crate) const ROOT_GROUP_NAME: &str = "wheel";
#[allow(clippy::undocumented_unsafe_blocks)]
#[cfg(test)]
mod tests {
use std::{
ffi::c_char,
io::{self, Read, Write},
os::{
fd::{AsFd, AsRawFd},
unix::net::UnixStream,
},
process::exit,
};
use libc::SIGKILL;
use crate::system::interface::{GroupId, ProcessId, UserId};
use super::{
Group, ROOT_GROUP_NAME, User, WithProcess, fork_for_test, getpgrp, setpgid,
wait::{Wait, WaitOptions},
};
pub(super) fn tempfile() -> std::io::Result<std::fs::File> {
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("Failed to get system time")
.as_nanos();
let pid = std::process::id();
let filename = format!("sudo_rs_test_{pid}_{timestamp}");
let path = std::path::PathBuf::from("/tmp").join(filename);
std::fs::File::options()
.read(true)
.write(true)
.create_new(true)
.open(path)
}
#[test]
fn test_get_user_and_group_by_id() {
let fixed_users = &[
(UserId::ROOT, "root"),
(User::from_name(c"daemon").unwrap().unwrap().uid, "daemon"),
];
for &(id, name) in fixed_users {
let root = User::from_uid(id).unwrap().unwrap();
assert_eq!(root.uid, id);
assert_eq!(root.name, name);
}
let fixed_groups = &[
(GroupId::new(0), ROOT_GROUP_NAME),
(Group::from_name(c"daemon").unwrap().unwrap().gid, "daemon"),
];
for &(id, name) in fixed_groups {
let root = Group::from_gid(id).unwrap().unwrap();
assert_eq!(root.gid, id);
assert_eq!(root.name.unwrap(), name);
}
}
#[test]
fn miri_test_group_impl() {
use super::Group;
use std::ffi::CString;
fn test(name: &str, passwd: &str, gid: libc::gid_t, mem: &[&str]) {
assert_eq!(
{
let c_mem: Vec<CString> =
mem.iter().map(|&s| CString::new(s).unwrap()).collect();
let c_name = CString::new(name).unwrap();
let c_passwd = CString::new(passwd).unwrap();
unsafe {
Group::from_libc(&libc::group {
gr_name: c_name.as_ptr() as *mut _,