Skip to main content

tracexec_core/
account.rs

1//! Account database helpers.
2//!
3//! Static glibc binaries cannot safely use NSS-backed account lookups such as
4//! `getpwuid_r(3)`. In that build mode we read the local account files
5//! directly; other builds keep the usual libc/NSS behavior.
6
7#[cfg(any(test, all(target_env = "gnu", target_feature = "crt-static")))]
8use std::ffi::CString;
9use std::path::PathBuf;
10#[cfg(any(test, all(target_env = "gnu", target_feature = "crt-static")))]
11use std::{
12  os::unix::ffi::OsStrExt,
13  path::Path,
14};
15
16#[cfg(any(test, all(target_env = "gnu", target_feature = "crt-static")))]
17use nix::errno::Errno;
18use nix::unistd::{
19  Gid,
20  Group,
21  Uid,
22  User,
23};
24
25#[cfg(all(target_env = "gnu", target_feature = "crt-static"))]
26const ETC_PASSWD: &str = "/etc/passwd";
27#[cfg(all(target_env = "gnu", target_feature = "crt-static"))]
28const ETC_GROUP: &str = "/etc/group";
29
30#[cfg(not(all(target_env = "gnu", target_feature = "crt-static")))]
31pub fn user_from_uid(uid: Uid) -> nix::Result<Option<User>> {
32  User::from_uid(uid)
33}
34
35#[cfg(all(target_env = "gnu", target_feature = "crt-static"))]
36pub fn user_from_uid(uid: Uid) -> nix::Result<Option<User>> {
37  find_user_in_passwd(&read_account_file(ETC_PASSWD)?, |user| user.uid == uid)
38}
39
40#[cfg(not(all(target_env = "gnu", target_feature = "crt-static")))]
41pub fn user_from_name(name: &str) -> nix::Result<Option<User>> {
42  User::from_name(name)
43}
44
45#[cfg(all(target_env = "gnu", target_feature = "crt-static"))]
46pub fn user_from_name(name: &str) -> nix::Result<Option<User>> {
47  find_user_in_passwd(&read_account_file(ETC_PASSWD)?, |user| user.name == name)
48}
49
50#[cfg(not(all(target_env = "gnu", target_feature = "crt-static")))]
51pub fn group_from_gid(gid: Gid) -> nix::Result<Option<Group>> {
52  Group::from_gid(gid)
53}
54
55#[cfg(all(target_env = "gnu", target_feature = "crt-static"))]
56pub fn group_from_gid(gid: Gid) -> nix::Result<Option<Group>> {
57  find_group_in_group(&read_account_file(ETC_GROUP)?, |group| group.gid == gid)
58}
59
60pub fn current_shell() -> nix::Result<Option<PathBuf>> {
61  user_from_uid(nix::unistd::getuid()).map(|user| user.map(|user| user.shell))
62}
63
64/// Parse `/etc/group` content to find supplementary group IDs for a user.
65///
66/// Returns a deduplicated list of GIDs including the primary GID.
67#[cfg(any(test, all(target_env = "gnu", target_feature = "crt-static")))]
68pub fn parse_supplementary_gids(
69  etc_group_content: &str,
70  username: &str,
71  primary_gid: Gid,
72) -> Vec<Gid> {
73  let mut gids = vec![primary_gid];
74  for line in etc_group_content.lines() {
75    let Some(group) = parse_group_line(line) else {
76      continue;
77    };
78    if group.mem.iter().any(|member| member == username) && !gids.contains(&group.gid) {
79      gids.push(group.gid);
80    }
81  }
82  gids
83}
84
85#[cfg(all(target_env = "gnu", target_feature = "crt-static"))]
86pub fn supplementary_gids(username: &str, primary_gid: Gid) -> nix::Result<Vec<Gid>> {
87  Ok(parse_supplementary_gids(
88    &read_account_file(ETC_GROUP)?,
89    username,
90    primary_gid,
91  ))
92}
93
94#[cfg(all(target_env = "gnu", target_feature = "crt-static"))]
95fn read_account_file(path: &str) -> nix::Result<String> {
96  std::fs::read_to_string(path).map_err(io_error_to_errno)
97}
98
99#[cfg(all(target_env = "gnu", target_feature = "crt-static"))]
100fn io_error_to_errno(err: std::io::Error) -> Errno {
101  err.raw_os_error().map_or(Errno::EIO, Errno::from_raw)
102}
103
104#[cfg(all(target_env = "gnu", target_feature = "crt-static"))]
105fn find_user_in_passwd(
106  content: &str,
107  predicate: impl Fn(&User) -> bool,
108) -> nix::Result<Option<User>> {
109  for line in content.lines() {
110    let Some(user) = parse_passwd_line(line)? else {
111      continue;
112    };
113    if predicate(&user) {
114      return Ok(Some(user));
115    }
116  }
117  Ok(None)
118}
119
120#[cfg(all(target_env = "gnu", target_feature = "crt-static"))]
121fn find_group_in_group(
122  content: &str,
123  predicate: impl Fn(&Group) -> bool,
124) -> nix::Result<Option<Group>> {
125  for line in content.lines() {
126    let Some(group) = parse_group_line(line) else {
127      continue;
128    };
129    if predicate(&group) {
130      return Ok(Some(group));
131    }
132  }
133  Ok(None)
134}
135
136#[cfg(any(test, all(target_env = "gnu", target_feature = "crt-static")))]
137fn parse_passwd_line(line: &str) -> nix::Result<Option<User>> {
138  let line = line.trim();
139  if line.is_empty() || line.starts_with('#') {
140    return Ok(None);
141  }
142
143  let mut fields = line.split(':');
144  let Some(name) = fields.next() else {
145    return Ok(None);
146  };
147  let Some(passwd) = fields.next() else {
148    return Ok(None);
149  };
150  let Some(uid) = fields.next().and_then(|uid| uid.parse::<u32>().ok()) else {
151    return Ok(None);
152  };
153  let Some(gid) = fields.next().and_then(|gid| gid.parse::<u32>().ok()) else {
154    return Ok(None);
155  };
156  let Some(gecos) = fields.next() else {
157    return Ok(None);
158  };
159  let Some(dir) = fields.next() else {
160    return Ok(None);
161  };
162  let Some(shell) = fields.next() else {
163    return Ok(None);
164  };
165  if fields.next().is_some() {
166    return Ok(None);
167  }
168
169  Ok(Some(User {
170    name: name.to_owned(),
171    passwd: cstring(passwd)?,
172    uid: Uid::from_raw(uid),
173    gid: Gid::from_raw(gid),
174    gecos: cstring(gecos)?,
175    dir: pathbuf_from_bytes(dir),
176    shell: pathbuf_from_bytes(shell),
177  }))
178}
179
180#[cfg(any(test, all(target_env = "gnu", target_feature = "crt-static")))]
181fn parse_group_line(line: &str) -> Option<Group> {
182  let line = line.trim();
183  if line.is_empty() || line.starts_with('#') {
184    return None;
185  }
186
187  let mut fields = line.split(':');
188  let name = fields.next()?;
189  let passwd = fields.next()?;
190  let gid = fields.next()?.parse::<u32>().ok()?;
191  let members = fields.next().unwrap_or("");
192  if fields.next().is_some() {
193    return None;
194  }
195
196  Some(Group {
197    name: name.to_owned(),
198    passwd: CString::new(passwd).ok()?,
199    gid: Gid::from_raw(gid),
200    mem: members
201      .split(',')
202      .map(str::trim)
203      .filter(|member| !member.is_empty())
204      .map(str::to_owned)
205      .collect(),
206  })
207}
208
209#[cfg(any(test, all(target_env = "gnu", target_feature = "crt-static")))]
210fn cstring(s: &str) -> nix::Result<CString> {
211  CString::new(s).map_err(|_| Errno::EINVAL)
212}
213
214#[cfg(any(test, all(target_env = "gnu", target_feature = "crt-static")))]
215fn pathbuf_from_bytes(s: &str) -> PathBuf {
216  Path::new(std::ffi::OsStr::from_bytes(s.as_bytes())).to_path_buf()
217}
218
219#[cfg(test)]
220mod tests {
221  use test_that::prelude::*;
222
223  use super::*;
224
225  #[test]
226  fn test_parse_passwd_line_basic() {
227    let user = parse_passwd_line("alice:x:1000:100:Alice:/home/alice:/bin/bash")
228      .unwrap()
229      .unwrap();
230    assert_eq!(user.name, "alice");
231    assert_eq!(user.uid, Uid::from_raw(1000));
232    assert_eq!(user.gid, Gid::from_raw(100));
233    assert_eq!(user.shell, PathBuf::from("/bin/bash"));
234  }
235
236  #[test]
237  fn test_parse_passwd_line_skips_malformed_lines() {
238    assert_that!(parse_passwd_line("# comment").unwrap(), none());
239    assert_that!(parse_passwd_line("missing:fields").unwrap(), none());
240    assert_that!(
241      parse_passwd_line("alice:x:not-a-uid:100:Alice:/home/alice:/bin/bash").unwrap(),
242      none()
243    );
244    assert_that!(
245      parse_passwd_line("alice:x:1000:100:Alice:/home/alice:/bin/bash:extra").unwrap(),
246      none()
247    );
248  }
249
250  #[test]
251  fn test_parse_supplementary_gids_basic() {
252    let content =
253      "root:x:0:\ndaemon:x:1:\nusers:x:100:alice,bob\ndocker:x:999:alice\nwheel:x:10:bob\n";
254    let gids = parse_supplementary_gids(content, "alice", Gid::from_raw(1000));
255    assert_eq!(
256      gids,
257      vec![Gid::from_raw(1000), Gid::from_raw(100), Gid::from_raw(999)]
258    );
259  }
260
261  #[test]
262  fn test_parse_supplementary_gids_primary_gid_deduped() {
263    let content = "users:x:1000:alice\n";
264    let gids = parse_supplementary_gids(content, "alice", Gid::from_raw(1000));
265    assert_eq!(gids, vec![Gid::from_raw(1000)]);
266  }
267
268  #[test]
269  fn test_parse_supplementary_gids_no_members() {
270    let content = "root:x:0:\nusers:x:100:\n";
271    let gids = parse_supplementary_gids(content, "alice", Gid::from_raw(1000));
272    assert_eq!(gids, vec![Gid::from_raw(1000)]);
273  }
274
275  #[test]
276  fn test_parse_supplementary_gids_skips_malformed_lines() {
277    let content = "root:x:0:\nmalformed_line\n:x:abc:alice\nusers:x:100:alice\n";
278    let gids = parse_supplementary_gids(content, "alice", Gid::from_raw(1000));
279    assert_eq!(gids, vec![Gid::from_raw(1000), Gid::from_raw(100)]);
280  }
281
282  #[test]
283  fn test_parse_supplementary_gids_skips_comments_and_empty() {
284    let content = "# this is a comment\n\nusers:x:100:alice\n";
285    let gids = parse_supplementary_gids(content, "alice", Gid::from_raw(1000));
286    assert_eq!(gids, vec![Gid::from_raw(1000), Gid::from_raw(100)]);
287  }
288
289  #[test]
290  fn test_parse_supplementary_gids_no_partial_match() {
291    let content = "group1:x:100:alice2,malice\ngroup2:x:200:alice\n";
292    let gids = parse_supplementary_gids(content, "alice", Gid::from_raw(1000));
293    assert_eq!(gids, vec![Gid::from_raw(1000), Gid::from_raw(200)]);
294  }
295
296  #[test]
297  fn test_parse_supplementary_gids_whitespace_in_members() {
298    let content = "group1:x:100: alice , bob \n";
299    let gids = parse_supplementary_gids(content, "alice", Gid::from_raw(1000));
300    assert_eq!(gids, vec![Gid::from_raw(1000), Gid::from_raw(100)]);
301  }
302
303  #[test]
304  fn test_parse_supplementary_gids_no_user_list_field() {
305    let content = "nogroup:x:65534\n";
306    let gids = parse_supplementary_gids(content, "alice", Gid::from_raw(1000));
307    assert_eq!(gids, vec![Gid::from_raw(1000)]);
308  }
309}