@@ -20,6 +20,7 @@ const FSTAB_ANACONDA_STAMP: &str = "Created by anaconda";
2020pub ( crate ) const BOOTC_EDITED_STAMP : & str = "Updated by bootc-fstab-edit.service" ;
2121const TRANSIENT_RELABEL_UNIT : & str = "bootc-early-overlay-relabel.service" ;
2222const SYSINIT_TARGET : & str = "sysinit.target" ;
23+ const SHADOW_SYNC_UNIT : & str = "bootc-sysusers-shadow-sync.service" ;
2324
2425/// Called when the root is read-only composefs to reconcile /etc/fstab
2526#[ context( "bootc generator" ) ]
@@ -146,6 +147,11 @@ pub(crate) fn generator(root: &Dir, unit_dir: &Dir) -> Result<()> {
146147
147148 unit_enablement_impl ( sysroot, unit_dir) ?;
148149
150+ // The shadow sync runs on all ostree boots; shadow_sync_generator_impl
151+ // has its own guard that checks whether /etc/shadow exists.
152+ let updated = shadow_sync_generator_impl ( root, unit_dir) ?;
153+ tracing:: trace!( "Enabled shadow sync: {updated}" ) ;
154+
149155 // Only run for overlayfs roots (composefs mounts an overlay, regular or transient).
150156 let st = rustix:: fs:: fstatfs ( root. as_fd ( ) ) ?;
151157 if st. f_type != libc:: OVERLAYFS_SUPER_MAGIC {
@@ -166,6 +172,31 @@ pub(crate) fn generator(root: &Dir, unit_dir: &Dir) -> Result<()> {
166172 Ok ( ( ) )
167173}
168174
175+ /// Enable the statically-installed shadow sync unit by symlinking it into
176+ /// `sysinit.target.wants/` in the generator output directory.
177+ ///
178+ /// The unit file itself lives at `/usr/lib/systemd/system/bootc-sysusers-shadow-sync.service`
179+ /// and is shipped with bootc; the generator only performs conditional enablement.
180+ ///
181+ /// We check existence of `/etc/shadow` rather than writability because systemd
182+ /// sandboxes generators in a private read-only mount namespace (see
183+ /// `systemd.generator(7)`), so any writability check would always fail even
184+ /// though `/etc` will be on its own writable mount by the time the service
185+ /// actually runs. The caller already gates on the ostree-booted marker,
186+ /// which guarantees we are on a bootc system where `/etc` is writable at
187+ /// service-run time.
188+ #[ context( "shadow sync generator" ) ]
189+ pub ( crate ) fn shadow_sync_generator_impl ( root : & Dir , unit_dir : & Dir ) -> Result < bool > {
190+ if !root. try_exists ( "etc/shadow" ) ? {
191+ tracing:: trace!( "/etc/shadow not found, skipping shadow sync" ) ;
192+ return Ok ( false ) ;
193+ }
194+
195+ tracing:: debug!( "/etc/shadow found, enabling {SHADOW_SYNC_UNIT}" ) ;
196+ enable_unit ( unit_dir, SHADOW_SYNC_UNIT , "sysinit.target" ) ?;
197+ Ok ( true )
198+ }
199+
169200/// Parse /etc/fstab and check if the root mount is out of sync with the composefs
170201/// state, and if so, fix it.
171202fn generate_fstab_editor ( unit_dir : & Dir ) -> Result < ( ) > {
@@ -417,5 +448,51 @@ UUID=341c4712-54e8-4839-8020-d94073b1dc8b /boot xfs defaul
417448
418449 Ok ( ( ) )
419450 }
451+
452+ #[ test]
453+ fn test_shadow_sync_no_shadow ( ) -> Result < ( ) > {
454+ // No /etc/shadow => should not enable (boot detection is caller's job)
455+ let tempdir = fixture ( ) ?;
456+ let unit_dir = & tempdir. open_dir ( "run/systemd/system" ) ?;
457+ let generated = shadow_sync_generator_impl ( & tempdir, unit_dir) ?;
458+ assert ! ( !generated) ;
459+ assert_eq ! ( unit_dir. entries( ) ?. count( ) , 0 ) ;
460+ Ok ( ( ) )
461+ }
462+
463+ #[ test]
464+ fn test_shadow_sync_enables_when_shadow_present ( ) -> Result < ( ) > {
465+ // /etc/shadow present => enables the static unit
466+ let tempdir = fixture ( ) ?;
467+ tempdir. atomic_write ( "etc/shadow" , "root:*:18912:0:99999:7:::\n " ) ?;
468+ let unit_dir = & tempdir. open_dir ( "run/systemd/system" ) ?;
469+ let generated = shadow_sync_generator_impl ( & tempdir, unit_dir) ?;
470+ assert ! ( generated) ;
471+ // The generator creates a symlink in sysinit.target.wants/; check the
472+ // directory entry exists (symlink_contents creates an absolute-path symlink
473+ // that cap-std won't follow in a tempdir, so we check metadata directly).
474+ let wants = unit_dir. open_dir ( "sysinit.target.wants" ) ?;
475+ let meta = wants. symlink_metadata ( SHADOW_SYNC_UNIT ) ?;
476+ assert ! ( meta. is_symlink( ) , "expected symlink for {SHADOW_SYNC_UNIT}" ) ;
477+ Ok ( ( ) )
478+ }
479+
480+ /// Verify that generator() enables the shadow sync unit on traditional
481+ /// ostree boots (tmpfs root, OSTREE_BOOTED marker present).
482+ #[ test]
483+ fn test_generator_shadow_sync_on_non_composefs ( ) -> Result < ( ) > {
484+ let tempdir = fixture ( ) ?;
485+ tempdir. atomic_write ( OSTREE_BOOTED , "" ) ?;
486+ tempdir. atomic_write ( "etc/shadow" , "root:*:18912:0:99999:7:::\n " ) ?;
487+ let unit_dir = & tempdir. open_dir ( "run/systemd/system" ) ?;
488+ generator ( & tempdir, unit_dir) ?;
489+ let wants = unit_dir. open_dir ( "sysinit.target.wants" ) ?;
490+ let meta = wants. symlink_metadata ( SHADOW_SYNC_UNIT ) ?;
491+ assert ! (
492+ meta. is_symlink( ) ,
493+ "shadow sync unit must be enabled on non-composefs ostree systems"
494+ ) ;
495+ Ok ( ( ) )
496+ }
420497 }
421498}
0 commit comments