shadow-core: commit several files together, and convert the last six tools - #279
Merged
Merged
Conversation
…tools group and gshadow have to agree: a group present in one and absent from the other is a broken system, and every tool that touches a group touches both. Committing them one at a time leaves a window in which they disagree, and a failure in the second makes that permanent. commit_all validates every file before writing any, which closes the failure this actually hits -- a value that would corrupt a record is rejected while nothing has been touched, instead of after the first file is already on disk. A genuine I/O error partway through can still leave the set half applied; there is no journal, and a rollback that can itself fail would not be an improvement. All the locks are held until the last write finishes, so no other process sees the intermediate state. A commit that would write the same bytes now writes nothing. A rewrite is not free: it replaces the inode and moves the mtime, and for an unchanged empty file it failed outright, since the atomic writer refuses to produce a zero-length file. Every tool used to carry its own 'did anything change' flag to work around that; those are gone. Found by usermod's tests, whose fixture has an empty gshadow that -G does not touch. groupmod, useradd, usermod, userdel, pwck and grpck are converted, and there is no longer a single FileLock::acquire left in any tool. userdel loses its hand-rolled line filter, which had its own idea of which lines were comments next to the parser that already knows; one consequence is that a comment above a removed account now moves to the end of the file rather than staying in place, which is what every other tool already did. Exit codes are preserved, including the two pwck keeps for a failed write: 6 'can not sort' and 5 'can not update the files' stay distinct by matching on the file the error names. Verified against a prefix tree for each tool: useradd creating an account with a user group, supplementary groups, aging and subid; usermod renaming across four files and replacing a group list; groupmod moving a GID and following it into passwd; userdel removing an account from six files; grpck sorting group and gshadow together, keeping each comment with its group, and writing nothing on a second run.
This was referenced Sep 4, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The rest of item 2 of #249. No tool holds a
FileLockdirectly any more.Files that have to agree
/etc/groupand/etc/gshadoware the clearest case: a group present in oneand absent from the other is a broken system, and every tool that touches a
group touches both. Committing them one at a time leaves a window in which they
disagree, and a failure in the second makes that permanent.
commit_allvalidates every file before writing any. That closes thefailure this actually hits: a value that would corrupt a record is rejected
while nothing has been touched, instead of after the first file is already on
disk. A genuine I/O error partway through the writes can still leave the set
half applied — there is no journal, and a rollback that can itself fail would
not be an improvement — but every lock is held until the last write finishes,
so no other process sees the intermediate state.
A commit that changes nothing writes nothing
A rewrite is not free: it replaces the inode and moves the mtime. For an
unchanged empty file it failed outright, because the atomic writer refuses
to produce a zero-length file. Every tool used to carry its own "did anything
change" flag to work around that; those are gone.
Found by
usermod's own tests, whose fixture has an empty/etc/gshadowthat-Gdoes not touch.The last six tools
groupmod,useradd,usermod,userdel,pwckandgrpck.userdelloses a hand-rolled line filter that had its own idea of which lineswere comments, standing next to the parser that already knows. One consequence
is worth stating: a comment above a removed account now moves to the end of the
file rather than staying in place. It is still preserved, and this is what
every other tool already did.
Exit codes are preserved, including the two
pwckkeeps for a failed write:6 can not sort and 5 can not update the files stay distinct by matching on
the file the error names.
Verification
Each tool was exercised against a prefix tree and its output compared field by
field with what it produced before:
useraddcreating an account with a user group, supplementary groups, agingfrom login.defs, a subuid range, and a home with the skeleton; plus both
refusal paths leaving every file untouched.
usermodrenaming an account across passwd, shadow, group and gshadow, thenreplacing a supplementary group list.
groupmodmoving a GID and following it into passwd, and putting-pintothe group file when there is no gshadow.
userdelremoving an account from six files, including unlinking a subidfile whose last row it held.
grpck -ssorting group and gshadow together, keeping each comment with itsgroup, and writing nothing on a second run.