Skip to content

shadow-core: commit several files together, and convert the last six tools - #279

Merged
pierre-warnier merged 1 commit into
mainfrom
refactor/249-multi-file-commit
Sep 4, 2026
Merged

shadow-core: commit several files together, and convert the last six tools#279
pierre-warnier merged 1 commit into
mainfrom
refactor/249-multi-file-commit

Conversation

@pierre-warnier

Copy link
Copy Markdown
Collaborator

The rest of item 2 of #249. No tool holds a FileLock directly any more.

Files that have to agree

/etc/group and /etc/gshadow are the clearest case: 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. That 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 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/gshadow that
-G does not touch.

The last six tools

groupmod, useradd, usermod, userdel, pwck and grpck.

userdel loses a hand-rolled line filter that had its own idea of which lines
were 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 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.

Verification

Each tool was exercised against a prefix tree and its output compared field by
field with what it produced before:

  • useradd creating an account with a user group, supplementary groups, aging
    from login.defs, a subuid range, and a home with the skeleton; plus both
    refusal paths leaving every file untouched.
  • usermod renaming an account across passwd, shadow, group and gshadow, then
    replacing a supplementary group list.
  • groupmod moving a GID and following it into passwd, and putting -p into
    the group file when there is no gshadow.
  • userdel removing an account from six files, including unlinking a subid
    file whose last row it held.
  • grpck -s sorting group and gshadow together, keeping each comment with its
    group, and writing nothing on a second run.
make check              exit 0
GNU comparison          38 passed, 0 unexpected
Deployment suite        215 passed, 0 failed

…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.
@pierre-warnier
pierre-warnier merged commit d34ab6f into main Sep 4, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant