* shadow-core: add a shared terminal helper
Reading a line and reading a password were open-coded in pam.rs, and
every prompt outside a PAM conversation had no helper at all.
tty.rs holds all of it: prompt_line, read_password and read_stdin_line,
with the EchoGuard that restores the terminal. Ctrl-C at a password
prompt used to leave echo disabled -- the signal terminates the process
without unwinding, so the guard's destructor never ran. The read now
blocks SIGINT, SIGQUIT and SIGTSTP for its duration, which is what
readpassphrase(3) does, and restores the mask on the way out.
The prompt prefers /dev/tty and falls back to stderr and stdin when
there is no controlling terminal (ENXIO/ENODEV), the case that broke
password changes under su -c.
* chfn: prompt for fields, and check values before asking for a password
chfn(1) with no field option prompts for each field with its current
value; ours refused with 'no flags specified'. It now prompts, and an
empty answer keeps the field.
Three fixes come with it:
- CHFN_RESTRICT is enforced on the values, not on which options were
passed. Interactive input went through unrestricted, and a field the
caller may not change is now not prompted for at all.
- Values are validated before the PAM conversation, so a rejected
value no longer costs the caller a password prompt.
- mutate_passwd no longer calls setuid(0). euid 0 is all the lock and
the atomic write need; raising the real uid made caller_is_root(),
which is deliberately real-uid based, answer true for everyone.
The five sub-fields are now one Changes value with the collect,
validate, permission and apply steps on it, which is what let uumain
shrink rather than grow.
* chsh: prompt for the shell, and stop leaking path existence
Three changes to the self-service path.
chsh(1) with no -s prompts for the new shell with the current one in
brackets; ours refused with 'no shell specified'. Pressing ENTER, or
repeating the current value, is now a successful no-op.
An empty shell is accepted. passwd(5) gives the field no meaning of its
own and login falls back to /bin/sh, so -s '' selects the system default
rather than naming a program that does not exist.
The order of the two checks on a shell now matters and is documented.
chsh is setuid-root, so Path::exists answers with root's view of the
filesystem: testing existence first made the tool an oracle for paths
the caller cannot stat, one probe at a time --
chsh -s /root/.ssh/id_ed25519
answering 'is not listed in /etc/shells' for a file that exists and
'does not exist' for one that does not. For a non-root caller the
membership test now runs first and discloses nothing, /etc/shells being
world-readable. Root still bypasses the list but not existence.
Validation also moved ahead of the PAM conversation, so a refused shell
no longer costs a password prompt, and mutate_passwd no longer calls
setuid(0): euid 0 is enough, and raising the real uid made
caller_is_root() answer true for every caller.
* docs: rewrite the chfn and chsh man pages; cover self-service end to end
The man pages documented neither interactive mode, nor CHFN_RESTRICT,
nor the restricted-account rule, nor what an empty shell field means,
and chfn's still said a field option was mandatory. Both now carry
permissions, value restrictions, configuration, files and worked
examples, and chsh explains why the two shell checks run in the order
they do.
The deployment suite gains eighteen assertions against the real setuid
binaries: interactive chfn and chsh through PAM, an empty answer as a
no-op, CHFN_RESTRICT enforced on interactive input, the empty shell
field, and the oracle itself -- an existing and a missing path under
/root must give a non-root caller the same answer, with no password
prompt before it.
4.2 KiB
chsh(1) - change login shell
NAME
chsh - change login shell
SYNOPSIS
chsh [options] [LOGIN]
DESCRIPTION
The chsh command changes a user's login shell: the program login(1) starts after a successful authentication, held in the last field of the account's /etc/passwd record (see passwd(5)).
With no LOGIN, chsh operates on the account of the user running it.
If no -s option is given, chsh prompts for the new shell, showing the current one in brackets. Pressing ENTER, or repeating the current value, makes no change and exits successfully.
An empty shell field is not an error: login falls back to /bin/sh when the field is empty, so an empty value selects the system default. Because an empty answer at the prompt means "keep what I have", the default is selected with -s '' rather than interactively.
PERMISSIONS
A normal user may only change the login shell of their own account; the superuser may change it for any account.
chsh is normally installed setuid-root, so it establishes who is asking before it writes: a non-root caller must authenticate through the chsh PAM service (typically /etc/pam.d/chsh, where pam_rootok admits root and the system authentication stack prompts everyone else). Authentication happens after the new shell has been checked and accepted, so a rejected shell never costs the caller a password prompt.
A build without PAM support cannot verify the caller, so it refuses every non-root invocation rather than applying an unauthenticated change.
Restricted accounts
An account whose current login shell is not listed in /etc/shells is restricted (see shells(5)) and may not change its shell at all. Without this rule, an account deliberately confined to a shell kept out of /etc/shells, such as /bin/rbash, could simply set itself a normal one and escape the confinement. An account whose shell field is empty is not restricted: the empty field is the system default, not a confinement.
An account with no /etc/passwd record, or whose record cannot be read, is treated as restricted.
OPTIONS
- -l, --list-shells
- Print the shells listed in /etc/shells, one per line, and exit.
- -R, --root CHROOT_DIR
- Apply changes in the CHROOT_DIR directory. Only the superuser may use this option: it points a setuid-root program at files of the caller's choosing.
- -s, --shell SHELL
- Set the login shell to SHELL. See VALUE RESTRICTIONS below.
- --help
- Print a usage summary and exit.
- --version
- Print the version and exit.
VALUE RESTRICTIONS
A non-empty shell must be an absolute path and must exist.
A non-root caller must additionally name a shell listed in /etc/shells. If /etc/shells is missing or contains no entries, only /bin/sh is implicitly valid. The superuser is not restricted to the list, but the file must still exist.
The two tests run in that order for a reason. chsh is setuid-root, so its existence check answers with root's view of the filesystem. Were existence tested first, a distinct "does not exist" reply would turn the program into an oracle for paths the caller cannot otherwise stat, one probe at a time:
$ chsh -s /root/.ssh/id_ed25519
Testing the /etc/shells membership first means every unlisted path gets the same answer whether or not it exists, and that answer discloses nothing: /etc/shells is world-readable.
EXIT STATUS
- 0
- Success, including an interactive run answered with ENTER.
- 1
- Permission denied, a restricted account, an invalid or unlisted shell, a missing account, or a failure to read or write /etc/passwd.
FILES
- /etc/passwd
- User account information.
- /etc/shells
- List of valid login shells.
- /etc/pam.d/chsh
- PAM service used to authenticate a non-root caller.
EXAMPLES
List the shells you may choose from:
$ chsh -l
/bin/sh
/bin/bash
/usr/bin/zsh
Change your own shell without being prompted:
$ chsh -s /usr/bin/zsh
Change it interactively:
$ chsh
Changing the login shell for ada
Enter the new value, or press ENTER for the default
Login Shell [/bin/bash]: /usr/bin/zsh
Give a service account the system default shell, as root:
# chsh -s '' backup
SEE ALSO
chfn(1), login(1), passwd(5), shells(5)