1use std::collections::HashSet;
17use std::io::Write;
18
19use anyhow::{Context, Result};
20use cap_std_ext::cap_std;
21use cap_std_ext::cap_std::fs::Dir;
22use cap_std_ext::dirext::CapStdExtDirExt;
23use fn_error_context::context;
24use ostree_ext::ostree;
25
26fn load_usernames(root: &Dir, path: &str) -> Result<HashSet<String>> {
31 use bootc_sysusers::nameservice::passwd::parse_passwd_content;
32 let mut names = HashSet::new();
33 if let Some(f) = root
34 .open_optional(path)
35 .with_context(|| format!("Opening {path}"))?
36 {
37 let entries = parse_passwd_content(std::io::BufReader::new(f))
38 .with_context(|| format!("Parsing {path}"))?;
39 names.extend(entries.into_iter().map(|e| e.name));
40 }
41 Ok(names)
42}
43
44fn load_groupnames(root: &Dir, path: &str) -> Result<HashSet<String>> {
47 use bootc_sysusers::nameservice::group::parse_group_content;
48 let mut names = HashSet::new();
49 if let Some(f) = root
50 .open_optional(path)
51 .with_context(|| format!("Opening {path}"))?
52 {
53 let entries = parse_group_content(std::io::BufReader::new(f))
54 .with_context(|| format!("Parsing {path}"))?;
55 names.extend(entries.into_iter().map(|e| e.name));
56 }
57 Ok(names)
58}
59
60#[derive(Debug, Default)]
64struct RemovedEntries {
65 orphaned: Vec<String>,
66 duplicates: Vec<String>,
67}
68
69impl RemovedEntries {
70 fn is_empty(&self) -> bool {
71 self.orphaned.is_empty() && self.duplicates.is_empty()
72 }
73}
74
75fn filter_shadow_file<T>(
88 root: &Dir,
89 path: &str,
90 valid_names: &HashSet<String>,
91 name_fn: impl Fn(&T) -> &str,
92 serialize_fn: impl Fn(&T, &mut Vec<u8>) -> Result<()>,
93 parse_fn: impl Fn(std::io::BufReader<cap_std::fs::File>) -> Result<Vec<T>>,
94 sepolicy: Option<&ostree::SePolicy>,
95) -> Result<Option<RemovedEntries>> {
96 let Some(f) = root
97 .open_optional(path)
98 .with_context(|| format!("Opening {path}"))?
99 else {
100 return Ok(None);
101 };
102 let meta = f.metadata().with_context(|| format!("Stat {path}"))?;
103 use cap_std::fs::MetadataExt as _;
104 let mode = rustix::fs::Mode::from_raw_mode(meta.mode());
105 let entries = parse_fn(std::io::BufReader::new(f))?;
106
107 let mut seen = HashSet::new();
108 let mut removed = RemovedEntries::default();
109
110 let filtered: Vec<T> = entries
111 .into_iter()
112 .filter(|e| {
113 let name = name_fn(e);
114 if !valid_names.contains(name) {
115 removed.orphaned.push(name.to_string());
116 return false;
117 }
118 if !seen.insert(name.to_string()) {
119 removed.duplicates.push(name.to_string());
120 return false;
121 }
122 true
123 })
124 .collect();
125
126 if removed.is_empty() {
127 return Ok(Some(removed));
128 }
129
130 let mut buf = Vec::new();
131 for entry in &filtered {
132 serialize_fn(entry, &mut buf)?;
133 }
134 crate::lsm::atomic_replace_labeled(root, path, mode, sepolicy, |w| {
135 w.write_all(&buf).map_err(Into::into)
136 })
137 .with_context(|| format!("Rewriting {path}"))?;
138
139 Ok(Some(removed))
140}
141
142struct PwdLock {
151 locked: bool,
152}
153
154impl PwdLock {
155 fn acquire_for_root(root: &Dir) -> Result<Self> {
159 #[allow(unsafe_code)]
160 unsafe extern "C" {
161 fn lckpwdf() -> libc::c_int;
162 }
163 let root_meta = root.dir_metadata()?;
166 let real_root_meta = std::fs::metadata("/")?;
167 use cap_std_ext::cap_primitives::fs::MetadataExt as CapMetadataExt;
168 use std::os::unix::fs::MetadataExt;
169 if root_meta.dev() != real_root_meta.dev() || root_meta.ino() != real_root_meta.ino() {
170 tracing::trace!("skipping lckpwdf: not operating on real root");
171 return Ok(PwdLock { locked: false });
172 }
173 #[allow(unsafe_code)]
175 let r = unsafe { lckpwdf() };
176 if r != 0 {
177 anyhow::bail!("lckpwdf() failed: could not acquire /etc/.pwd.lock");
178 }
179 Ok(PwdLock { locked: true })
180 }
181}
182
183impl Drop for PwdLock {
184 fn drop(&mut self) {
185 if !self.locked {
186 return;
187 }
188 #[allow(unsafe_code)]
189 unsafe extern "C" {
190 fn ulckpwdf() -> libc::c_int;
191 }
192 #[allow(unsafe_code)]
193 let _ = unsafe { ulckpwdf() };
194 }
195}
196
197#[context("Fixing orphaned/duplicate entries in /etc/shadow and /etc/gshadow")]
216pub(crate) fn run(root: &Dir) -> Result<()> {
217 use bootc_sysusers::nameservice::gshadow::{GshadowEntry, parse_gshadow_content};
218 use bootc_sysusers::nameservice::shadow::{ShadowEntry, parse_shadow_content};
219
220 let _lock = PwdLock::acquire_for_root(root)?;
224
225 let sepolicy = crate::lsm::new_sepolicy_at(root)?;
230 let sepolicy = sepolicy.as_ref();
231
232 let mut valid_users = load_usernames(root, "etc/passwd")?;
235 valid_users.extend(load_usernames(root, "usr/lib/passwd")?);
236
237 let mut valid_groups = load_groupnames(root, "etc/group")?;
241 valid_groups.extend(load_groupnames(root, "usr/lib/group")?);
242
243 if valid_users.is_empty() {
246 tracing::debug!("No /etc/passwd or /usr/lib/passwd found, skipping shadow fixup");
247 return Ok(());
248 }
249
250 if let Some(removed) = filter_shadow_file(
251 root,
252 "etc/shadow",
253 &valid_users,
254 |e: &ShadowEntry| e.namp.as_str(),
255 |e, buf| e.to_writer(buf),
256 parse_shadow_content,
257 sepolicy,
258 )? {
259 if !removed.is_empty() {
260 tracing::info!(
261 "etc/shadow: removed {} orphaned ({}), {} duplicate ({}) entries",
262 removed.orphaned.len(),
263 removed.orphaned.join(", "),
264 removed.duplicates.len(),
265 removed.duplicates.join(", "),
266 );
267 }
268 }
269
270 if !valid_groups.is_empty() {
273 if let Some(removed) = filter_shadow_file(
274 root,
275 "etc/gshadow",
276 &valid_groups,
277 |e: &GshadowEntry| e.name.as_str(),
278 |e, buf| e.to_writer(buf),
279 parse_gshadow_content,
280 sepolicy,
281 )? {
282 if !removed.is_empty() {
283 tracing::info!(
284 "etc/gshadow: removed {} orphaned ({}), {} duplicate ({}) entries",
285 removed.orphaned.len(),
286 removed.orphaned.join(", "),
287 removed.duplicates.len(),
288 removed.duplicates.join(", "),
289 );
290 }
291 }
292 }
293
294 Ok(())
295}
296
297#[cfg(test)]
300mod tests {
301 use anyhow::Result;
302 use cap_std_ext::cap_std;
303 use cap_std_ext::dirext::CapStdExtDirExt;
304
305 use super::*;
306
307 fn setup_etc(td: &cap_std::fs::Dir) -> Result<()> {
308 td.create_dir_all("etc")?;
309 Ok(())
310 }
311
312 #[test]
313 fn test_fixup_shadow_no_orphans() -> Result<()> {
314 let td = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
315 setup_etc(&td)?;
316 td.atomic_write(
317 "etc/passwd",
318 "root:x:0:0:root:/root:/bin/bash\ndaemon:x:1:1::/usr/sbin:/usr/sbin/nologin\n",
319 )?;
320 td.atomic_write(
321 "etc/shadow",
322 "root:*:18912:0:99999:7:::\ndaemon:*:18474:0:99999:7:::\n",
323 )?;
324 td.atomic_write("etc/group", "root:x:0:\ndaemon:x:1:\n")?;
325 td.atomic_write("etc/gshadow", "root:*::\ndaemon:*::\n")?;
326
327 run(&td)?;
328
329 assert_eq!(
330 td.read_to_string("etc/shadow")?,
331 "root:*:18912:0:99999:7:::\ndaemon:*:18474:0:99999:7:::\n"
332 );
333 assert_eq!(td.read_to_string("etc/gshadow")?, "root:*::\ndaemon:*::\n");
334 Ok(())
335 }
336
337 #[test]
338 fn test_fixup_shadow_orphaned_entry() -> Result<()> {
339 let td = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
340 setup_etc(&td)?;
341 td.atomic_write("etc/passwd", "root:x:0:0:root:/root:/bin/bash\n")?;
342 td.atomic_write(
344 "etc/shadow",
345 "root:*:18912:0:99999:7:::\nplocate:!!:::::::\n",
346 )?;
347 td.atomic_write("etc/group", "root:x:0:\n")?;
348 td.atomic_write("etc/gshadow", "root:*::\nplocate:!::\n")?;
349
350 run(&td)?;
351
352 assert_eq!(
354 td.read_to_string("etc/shadow")?,
355 "root:*:18912:0:99999:7:::\n"
356 );
357 assert_eq!(td.read_to_string("etc/gshadow")?, "root:*::\n");
358 Ok(())
359 }
360
361 #[test]
362 fn test_fixup_shadow_nss_altfiles_group_gshadow_kept() -> Result<()> {
363 let td = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
369 td.create_dir_all("etc")?;
370 td.create_dir_all("usr/lib")?;
371 td.atomic_write("etc/passwd", "root:x:0:0:root:/root:/bin/bash\n")?;
372 td.atomic_write("etc/shadow", "root:*:18912:0:99999:7:::\n")?;
373 td.atomic_write("etc/group", "root:x:0:\n")?;
374 td.atomic_write("usr/lib/group", "plocate:x:999:\n")?;
376 td.atomic_write("etc/gshadow", "root:*::\nplocate:!::\n")?;
377
378 run(&td)?;
379
380 assert_eq!(td.read_to_string("etc/gshadow")?, "root:*::\nplocate:!::\n");
382 Ok(())
383 }
384
385 #[test]
386 fn test_fixup_shadow_nss_altfiles_group_gshadow_removed() -> Result<()> {
387 let td = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
391 td.create_dir_all("etc")?;
392 td.create_dir_all("usr/lib")?;
393 td.atomic_write("etc/passwd", "root:x:0:0:root:/root:/bin/bash\n")?;
394 td.atomic_write("etc/shadow", "root:*:18912:0:99999:7:::\n")?;
395 td.atomic_write("etc/group", "root:x:0:\n")?;
396 td.atomic_write("etc/gshadow", "root:*::\nplocate:!::\n")?;
398
399 run(&td)?;
400
401 assert_eq!(td.read_to_string("etc/gshadow")?, "root:*::\n");
403 Ok(())
404 }
405
406 #[test]
407 fn test_fixup_shadow_nss_altfiles_passwd_user_kept() -> Result<()> {
408 let td = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
411 td.create_dir_all("etc")?;
412 td.create_dir_all("usr/lib")?;
413 td.atomic_write("etc/passwd", "root:x:0:0:root:/root:/bin/bash\n")?;
415 td.atomic_write("usr/lib/passwd", "bin:x:1:1:bin:/bin:/sbin/nologin\n")?;
416 td.atomic_write("etc/shadow", "root:*:18912:0:99999:7:::\nbin:!!:::::::\n")?;
418 td.atomic_write("etc/group", "root:x:0:\nbin:x:1:\n")?;
419 td.atomic_write("etc/gshadow", "root:*::\nbin:*::\n")?;
420
421 run(&td)?;
422
423 assert_eq!(
425 td.read_to_string("etc/shadow")?,
426 "root:*:18912:0:99999:7:::\nbin:!!:::::::\n"
427 );
428 Ok(())
429 }
430
431 #[test]
432 fn test_fixup_shadow_duplicate_removed() -> Result<()> {
433 let td = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
434 setup_etc(&td)?;
435 td.atomic_write("etc/passwd", "root:x:0:0:root:/root:/bin/bash\n")?;
436 td.atomic_write(
438 "etc/shadow",
439 "root:*:18912:0:99999:7:::\nroot:*:18912:0:99999:7:::\n",
440 )?;
441 td.atomic_write("etc/group", "root:x:0:\n")?;
442 td.atomic_write("etc/gshadow", "root:*::\n")?;
443
444 run(&td)?;
445
446 assert_eq!(
448 td.read_to_string("etc/shadow")?,
449 "root:*:18912:0:99999:7:::\n"
450 );
451 Ok(())
452 }
453
454 #[test]
455 fn test_fixup_shadow_no_passwd_skips() -> Result<()> {
456 let td = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
458 setup_etc(&td)?;
459 td.atomic_write(
460 "etc/shadow",
461 "root:*:18912:0:99999:7:::\nplocate:!!:::::::\n",
462 )?;
463 td.atomic_write("etc/gshadow", "root:*::\nplocate:!::\n")?;
464
465 run(&td)?;
466
467 assert_eq!(
469 td.read_to_string("etc/shadow")?,
470 "root:*:18912:0:99999:7:::\nplocate:!!:::::::\n"
471 );
472 Ok(())
473 }
474}