Skip to content

v4.2.0 - The Show Must Go On

Latest

Choose a tag to compare

@LeChatP LeChatP released this 27 Oct 22:07
Immutable release. Only release title and notes can be modified.
e21fcd6

Bugs are still being fixed and the work is not over. I decided to set it as 4.2.0 as long there are many new features.

Password retries is finally available! and configurable with "password": {"tries": 1..255, "delay": "hh:mm:ss"} and build-time defaults!

The variables set by PAM modules (pam_env, pam_systemd) reach the command through the env filter of the task!

chsr is really up-to-date. Meaning that now it does really manages everything in the current policy. chsr is way stricter in its inputs. deny unknown fields, non octal umask, and variable names or patterns that the policy would not accept. It only writes the files that changed. Also regex parsing is now fixed.

xtask no more installs dependencies by itself. It checks the packages that building or running RootAsRole needs and prints the install command; doctor lists the missing ones. This comes from a better control and transparency of installer preferences. So now, users can use the compiler they wants (we recommend clang), decides to install manpages or not.

A policy with a signed duration (like -00:00:01) or a fourth field (00:00:00:00) is now refused.

^Z suspends dosr like a shell job, where before the command was killed and its unsaved work lost. When dosr has to stop a command, it sends SIGHUP, SIGTERM, then SIGKILL 2 s later.

Two dosr in one pipeline does not collide.

About performance. Since dosr reimplemented the PAM side, it introduces a lot of overhead. dosr is still high-performance. We do process a task in 2.3µs (no glob, no regex). To check overhead of modifications, we now have performance unit tests.

And many other bugs were fixed all along.

Just to understand how good is going RaR, this new version adds a total of 118 unit tests. Among these :

  • 29 ensures security
  • 44 functional bugs
  • 36 new features
  • 7 perf and checks
  • 2 migration to 4.2.0

Everything was verified by myself and checked two times. Claude is doing good work in general. And makes my project way more easily maintainable than before. My Claude subscription is mostly over, but I think that after that, I'm able to continue the work with the good direction it started. But of course, not as fast as Claude.

The show must go on!

⚠️ Spoiler : a very small amount of fixes ...
  • fix: dosr no longer loses a backchannel message that arrives in parts (5eef904)
    • fix: the runner and the monitor read their non-blocking socket with read_exact: when a frame arrived in two parts, its length was consumed then dropped on WouldBlock, the next read took the payload as a length, and dosr failed after the command ran with"invalid backchannel message size: 0 bytes" or an rkyv error, then exited 1; with a terminal on a machine with one CPU, it happened on nearly every command (since c524ead, 4.0.2)
    • fix: the backchannel keeps the bytes of the frame being read until it is complete, and reads no further than that frame, so that poll still reports the next one
    • fix: a frame is written at once, and a full non-blocking socket is waited for instead of cutting the frame
    • fix: the monitor ignores WouldBlock on its backchannel instead of leaving its event loop
    • tests: a frame split at every byte, written byte by byte, two frames in the socket, and a frame sent on a full socket
  • tests: a stdio terminal that is not dosr's own is not taken as the user's terminal (324c50f)
    • tests: a pty follower that is not the controlling terminal is a terminal, but is_terminal_for_pgrp refuses it, so the runnergives it to the command as it is instead of proxying it or setting it to raw mode (sudo-rs #1377, #1380)
  • fix: a stopped command suspends dosr like a shell job instead of being killed (70c8209)
    • fix: when the command was stopped (^Z, SIGSTOP, SIGTTIN, SIGTTOU), the monitor reported an error, dosr exited, and the monitor then killed the stopped command with SIGKILL: ^Z on dosr vim lost the editor and its unsaved work
    • feat: the monitor reports the stop with its signal, and keeps the process group that had the pty; dosr gives the terminal back in cooked mode and stops its job with the same signal, so that the shell sees a stopped job
    • feat: once dosr is continued (fg or bg), it sets the terminal up again, and the monitor resumes the command with the pty in its foreground when dosr is in the foreground of the user's terminal, in the background otherwise
    • fix: a command stopped by SIGTTIN or SIGTTOU while dosr is in the foreground is resumed at once, whether the terminal was already raw or not (sudo-rs #1598)
    • tests: the suspend messages round trip, and a signal action changed while suspending is put back
  • fix: two dosr of one pipeline no longer read their passwords from the terminal at the same time (ae0b705)
    • fix: with dosr a | dosr b, both prompts read /dev/tty at once, each one got part of the keystrokes and both authentications failed; dosr now holds an exclusive flock on its terminal while it authenticates, and the other one waits, then checks its timestamp once it gets the lock (sudo-rs #1650, #1651)
    • fix: no lock with -S (the password comes from stdin) or when authentication is skipped; without a terminal, or when the lockcannot be taken, dosr goes on without it
    • tests: the lock is exclusive between two opens of the terminal and released on drop, and there is none without a terminal
  • fix: dosr stops a command like sudo, giving it time to clean up before SIGKILL (14bb6d7)
    • fix: when dosr has to stop a command itself (SIGALRM, the monitor done while the command runs, no job to suspend), it sent SIGKILL at once, so a package manager or a database could not remove its lock or finish a write
    • fix: terminate_process sends SIGHUP, SIGTERM, then SIGKILL two seconds later, to the command's process group when the monitor is done or the suspension fails, to the command alone for SIGALRM, like sudo's terminate_command
    • fix: terminate_process refuses a pid of 0 or less, which would signal dosr's own group or every process
    • tests: a command that handles the signals cleans up and exits, one that ignores them is killed after the delay
  • chore: bump to 4.2.0 (3d4d19d)
    • chore: the next release is 4.2.0, since it adds the password retries (a new policy option)
    • feat: migration edge from 4.1.0 to 4.2.0, without any change for now; every released version needs one, otherwise its documents cannot be migrated
    • tests: a 4.1.0 document goes up to 4.2.0 and back down unchanged, but for its version
  • fix: a duration of the policy is refused when a field has a sign or more fields follow (3460db4)
    • fix: "-00:00:01" was read as one second, since "-00" parses as 0 and the negative check missed it; "+00:00:01" and "00:05:00:junk" were accepted too, the fourth field ignored
    • fix: a duration is exactly three fields of digits, hh:mm:ss
    • tests: signs on a zero field, a plus sign, extra, empty and missing fields are refused
  • feat: dosr asks for the password up to 3 times, waiting 5 seconds after a wrong one (b1092a1)
    • feat: a wrong password (PAM_AUTH_ERR) is asked again, after a delay, with "Sorry, try again."; any other PAM error, such as the end of the input or an unknown user, stops at once, without delay
    • feat: the tries and the delay are build-time defaults, RAR_PASSWORD_TRIES (3) and RAR_PASSWORD_DELAY (00:00:05), and a policy option at every level, "password": {"tries": 3, "delay": "00:00:05"}, each field inherited on its own
    • feat: tries is 1 to 255, 0 is refused: it would never let anyone in, or never stop asking (sudo-rs #1309); a build value outof range stops the build
    • feat: the downgrade to 4.1.0 removes the password option, which 4.1.0 does not know
    • tests: the option's format and defaults, its CBOR round trip, the inheritance of each field, the migrations, and the retriesuntil success, up to the last try, and stopping on another error
  • fix: dosr refuses to go on when a PAM module changed the user (4fa0dac)
    • fix: a PAM module may change PAM_USER during the transaction (pam_get_item(3)), and dosr went on with the session of anotheruser than the one it authenticated; it now checks PAM_USER after the account management, with or without password, and after opening the session, and refuses when it changed or cannot be read
    • tests: the comparison of the PAM user; the PAM tests of start_session pass the checks
  • feat: the variables that PAM modules set reach the command, through the env filter of the policy (8600b27)
    • feat: dosr reads the PAM environment once the session is open (pam_getenvlist(3), set by pam_env or pam_systemd) and adds the variables that the env options of the task keep, as for the variables of the user: keep and check lists in delete mode, delete and check lists in keep mode, never a shell function; pam_env may read a file that the user writes (~/.pam_environment with user_readenv=1)
    • feat: a variable that the command already has is left as it is: the kept variables of the user, those set by dosr (PATH, HOME, USER, RAR_, SUDO_) and the set list take precedence
    • feat: a PAM variable that is not UTF-8, or has no name, is left out
    • chore: rootasrole-core gets EnvFilter, the filter of ResolvedEnv owned so that it outlives the policy; both use the same rule
    • tests: the owned filter decides as the policy's, the precedence, the filter of a task with and without dosr -E, and the PAM environment read from a transaction
  • tests: end-to-end checks of the PAM environment and of the password retries (f31f6bd)
    • tests: the variables that pam_env sets reach the command through the env filter of the task, and never replace PATH nor a variable of the user that the task keeps; pam_env is added to the PAM service of dosr for the check, then the service is restored, since Debian's common-* files do not run it
    • tests: with -S, three wrong passwords are refused, two wrong ones then the right one are accepted after two "Sorry, try again."; the timestamp cookie and the pam_faillock counter (Arch) are cleared around the check
  • fix: a PPID timestamp cookie is bound to the session of dosr too (82c3191)
    • fix: with the ppid scope (and the tty scope without a terminal), a cookie served any process of the same parent, even in another session after setsid(2); the record now keeps the session id, like sudo; only the id, since the session leader may be gone while the session lives on
    • fix: the cookies of 4.1.0 have no session and are no longer reused: the password is asked once after the upgrade; after a downgrade, 4.1.0 reads the new cookies and ignores the session
    • tests: a cookie from another session is refused, the record holds the session of dosr, a 4.1.0 cookie is not reused, 4.1.0 reads a new cookie
  • fix: dosr and chsr no longer panic when their output is a closed pipe (6826331)
    • fix: Rust ignores SIGPIPE, so a write to a pipe that nobody reads (dosr --help | true, dosr -i | head) fails with EPIPE, andprintln! and eprintln! panic on it; dosr ended with exit code 101, and a message of a PAM module printed from the conversation, a callback of libpam, would abort the process
    • fix: rootasrole-core exports println_ignore_io_error!, print_ignore_io_error! and eprintln_ignore_io_error!, which ignore write errors; dosr and chsr use them outside the tests
    • chore: outside the tests, the crate roots of rootasrole-core, dosr and chsr shadow println!, print!, eprintln! and eprint! with a compile_error!, so that a new use does not build
    • tests: dosr --help and --version with stdout on a closed pipe exit 0 without a panic
  • fix: the PATH of a command never names its working directory (10d703e)
    • fix: a relative directory of the path option (".", "", "bin") was kept in every mode; it is the working directory of the command, which the user chooses: it is now kept only in keep-unsafe mode, like the relative directories of the user, and logged whenleft out
    • fix: with no directory left (delete mode with "add": []), dosr set PATH to the empty string, which shells and execvp(3) search as the working directory; the command now has no PATH, and its own default applies; the set list of the policy can still give one
    • fix: a PATH set by PAM never fills an absent PATH
    • tests: relative directories of the policy and of the user, the empty one included, in each mode; no PATH without directories, even when the user's PATH is kept by name or PAM sets one; a task without any directory
  • fix: chsr creates policy files readable and writable by their owner only (841605c)
    • fix: the policy files that chsr opened for writing were created with the mode of the umask, 0644 with the usual 022: the policy was readable by every user, and a umask of 000 made it writable by them, so that dosr refused it; they are now created 0600, like the files of the installers
    • tests: a new root file or policy file is never more than 0600; saving rewrites the file in place and keeps its owner and mode
  • fix: dosr says why it lacks its capabilities instead of a bare Permission denied (afb9905)
    • fix: when dosr runs without the capabilities it needs (setpcap, setuid, and dac_read_search or dac_override), reading the policy failed and the user only saw "Permission denied", as if the policy refused the command; dosr now prints why after the error,and logs it
    • fix: three causes are told apart: no_new_privs (the file capabilities are ignored: a service with NoNewPrivileges=yes, a container, setpriv, or a command that dosr runs with "root": "user", the usual nested session), a bounding set that lacks them (CapabilityBoundingSet=, a container, a command run with a strict bounding set), or a dosr file without capabilities or on a nosuid file system
    • tests: each cause, and one DAC capability being enough
  • fix: chsr prints its usage without the policy and no longer panics on startup errors (9816099)
    • fix: chsr created its temporary folder and read the policy before parsing its arguments, both with expect: without the privileges to read the policy, even chsr --help ended in a panic; --help and a usage error are now handled first, without the policy
    • fix: a failure to create the temporary folder or to read the policy is an error message on stderr and in the log, with a failure exit code
    • fix: a non-UTF-8 argument is refused instead of panicking in std::env::args()
    • tests: which arguments need the policy; chsr --help without privileges prints the usage and exits 0
  • chore: remove the upstream packaging leftovers (2224114)
    • chore: packages are made by Debian (debcargo-conf) and the AUR (dosr), not upstream (decided on 2026-10-19): xtask deploy, which could not build the deb and rpm packages anymore since their metadata left Cargo.toml, is removed with its feature
    • chore: resources/arch/PKGBUILD and rootasrole.install, resources/rh/postinst.sh and prerm.sh, and the Vagrantfiles of tests/capable are removed; the PAM service files of resources/arch and resources/rh stay, since cargo xtask install and uninstall use them
  • tests: remove a redundant clone that clippy --workspace refuses (105be6a)
    • tests: json! serializes an interpolated value by reference, so the commands of test_commands_without_arguments_up_and_down need no clone; only clippy on the whole workspace lints the tests of rootasrole-core
  • fix: chsr creates its temporary folder in /tmp, whatever TMPDIR says (3091822)
    • fix: a policy in keep mode, or dosr -E where allowed, passes the user's TMPDIR to chsr: its 0700 folder was then created in a directory that the user may own, where the user can rename it and put another one in its place; /tmp is sticky (decided by the maintainer on 2026-10-23)
  • fix: a failed lookup of an actor disables the roles with a separation of duties (09d0ea1)
    • fix: an actor written by name whose lookup failed (a directory service that does not answer, a group whose members take morethan the 1 MiB that nix allows for getgrnam_r) was taken as a name that nobody has: the user did not hold the role, and the SSD plugin let the user take a role that excludes it
    • fix: dosr now tells a failed lookup from an unknown name: the actor still never matches, the failure is logged, and every role of the policy file that has an ssd is disabled, since its separation of duties cannot be checked (decided by the maintainer on2026-10-23); SSD only names roles of the same file
    • feat: rootasrole-core gets DUserType::try_fetch_id and DGroupType::try_fetch_id, fetch_id keeps its meaning
    • tests: a failed lookup never matches and is reported, even with another matching actor; the roles with ssd are disabled whatever role the failed lookup belongs to, the others are not, through the hierarchy too
  • tests: end-to-end check of a failed group lookup under SSD (84e06b3)
    • tests: e2e.sh adds a role with an ssd and a role held through a group written by name; once the group has 120000 members, more than the 1 MiB of the lookups of dosr, getgrnam_r fails with ERANGE and the role with the ssd is refused; /etc/group and /etc/gshadow are restored, at the end of the check and in the cleanup
  • fix: chsr refuses a duration too long instead of writing a wrong one or panicking (b343c8c)
    • fix: chsr options timeout set --duration read a field that does not fit as 0 and ended in a panic when adding the fields overflowed: 153722867280912930:59:59 gave 00:59:59; a duration is now computed with checked arithmetic, and one that does not fit isan error
    • tests: the forms ss, mm:ss and hh:mm:ss, and durations too long, alone and through the timeout command
  • feat: chsr edits the password option at each level (52b215b)
    • feat: chsr [role R [task T]] options password set [--tries N] [--delay hh:mm:ss] and unset [--tries] [--delay], and options show password
    • feat: set changes the fields given and keeps the others; unset removes the fields named, then the option once nothing is left in it; the fields that this version does not know are kept
    • feat: tries is 1 to 255, a delay that does not fit is refused, and set without a value is an error
    • tests: each level, field by field, the unknown fields, the refused values, the usage, and the saved file read back
  • fix: chsr writes only what a command sets in the options of a role or task (5b4d5ed)
    • fix: a role or task without options got Opt::default(), which holds every built-in default (env and path lists, bounding, authentication, timeout, password...): chsr role R options root user wrote them all at the role, where its lists replaced those of the upper levels; the options are now created empty, at their level
    • fix: whitelist, blacklist, checklist and setlist of path and env worked on a temporary when the level had no path or env option: chsr reported a success and saved nothing; they now edit the option, and removing from a list that the level does not definechanges nothing
    • tests: the options of a new role and task, and list edits on a level without the option, read back from the saved file
  • fix: chsr options timeout set and unset keep the fields they do not name (72a7a78)
    • fix: set and unset started from an empty timeout: set --type tty dropped the duration and the max usage, unset --duration dropped the type, the max usage and the unknown fields, and left an empty object
    • fix: set changes the fields given, unset removes the fields named, then the option once nothing is left in it; set without avalue is an error
    • tests: chsr options timeout, field by field, with an unknown field and a level without the option, read back from the saved file
  • fix: chsr accepts the variable names and patterns of the policy, and no others (34a299d)
    • fix: the grammar of chsr required two characters (o env whitelist add A was refused), refused the * of patterns (LC_* could not be added), but accepted - and non-ASCII letters, which EnvKey refuses: o env whitelist add MY-VAR panicked; a key now followsis_valid_env_pattern, and is built with EnvKey::new, an error instead of a panic
    • fix: the names of the set list are variable names, a * is refused there as the policy does
    • fix: a refused name says what is expected instead of "unknown rule"
    • tests: one letter, patterns, and the names that the policy refuses
  • fix: chsr shows the option asked for at every level, and turns its panics into errors (a5c6c09)
    • fix: options show only filtered at the task level: at the global and role levels it printed every option, the whole policy for the global level; it now shows the resolved options of the level, or the one asked for
    • fix: options show can name authentication (auth), execinfo (info), umask (mask), workdir and password; cmd and cred, which are not options, panicked in an unreachable! and are refused
    • fix: chsr role R task T purge on a task that does not exist panicked; it and the other unreachable! and expect of chsr outside the tests are errors
    • tests: every option name at every level, missing roles and tasks, and the purges that the grammar accepts
  • fix: a timeout without a duration is read, instead of making the whole policy unreadable (08ce3d7)
    • fix: STimeout.duration has a deserialize_with but no default, so serde required it although it is an Option: a policy with "timeout": {"type": "tty"} could not be read, neither by chsr nor by dosr, which refused every request; chsr itself wrote such timeouts (options timeout set --type on a level without a timeout)
    • tests: every field of the timeout may be left out; the chsr tests check that the policy they save parses again
  • fix: chsr keeps the unknown fields of path, workdir, setuid and storage when it saves (512c189)
    • fix: SPathOptions, SWorkdirSet, SSetuidSet, SettingsContent and RemoteStorageSettings had no extra_fields, unlike the other objects of the policy: a field written by a newer version disappeared at the next chsr command; they keep them now, the serializer of SSetuidSet in JSON and CBOR
    • tests: every unknown field of a policy survives a chsr save; the sets and the storage settings round-trip, CBOR included; cred.capabilities and cred.setgid still refuse unknown fields
  • fix: chsr purges a path or workdir list back to inheritance, and keep-only keeps the other env fields (ead473d)
    • fix: purging a path or workdir list wrote an empty list, which also removes the inherited one (for the sub list, the protections of the upper levels), while purging an env list made it inherited again; every purge now makes the level stop defining the list, an explicit empty list stays a choice of the policy file
    • fix: env keep-only and delete-only replaced the whole env option of the level, dropping its check and set lists, override and unknown fields; they set the behavior and its list, as their usage says
    • fix: a command that removes from options that a level does not have leaves no empty options behind, and revoke warns about every actor that is not in the role
    • tests: purges at a level with and without the option, keep-only then delete-only
  • fix: chsr saves the groups and the user given to cred set, and refuses a umask that is not octal (50cb99a)
    • fix: cred set --setgid root,adm wrote "setgid": [], the groups were looked for among the direct children of the parse and they are nested in a combination
    • fix: cred set -u root wrote "setuid": "", the user was taken after the 9 characters of "--setuid ", whatever form was used
    • fix: options umask 999 set the built-in default umask, a value that is not octal fell back to the default; a umask is octal,000 to 777, or an error
    • tests: the simple edits of every level read back from the saved file, the forms of --setuid and --setgid, the umask
  • fix: chsr edits one file of a policy directory, and writes only the files that changed (00db348)
    • fix: with a policy directory (the layout of the Debian package and of cargo xtask install), chsr only read and wrote the policy embedded in the root file, which dosr ignores: chsr list printed {} and chsr role r add went nowhere useful
    • feat: a command edits the file named with chsr file (its path, or its name in the directory), else default.json, the only policy file, or the policy of the root file when there is no other; with several files and no default.json, chsr asks for one instead of guessing
    • feat: chsr list shows every file of the policy by its path, chsr file list lists the files
    • fix: saving rewrote every file, the unchanged ones in the typed form of the actors; a file is now written only when its datachanged since it was loaded or saved
    • tests: a directory with default.json and another file written in short form, by name and by path, a file that is not one of the policy, and a directory without default.json
  • tests: end-to-end check of chsr on the policy directory of the installation (eeed93c)
    • tests: e2e.sh creates and deletes a role with chsr: it lands in default.json, the root file and the other policy file keep their content byte for byte, and chsr list shows the files of the directory
  • fix: chsr reads the setuid set like dosr, refusing the fields it does not know (466fa4d)
    • fix: dosr refuses unknown fields in the three credential sets (setuid, setgid, capabilities), so that a typo there cannot leave a privilege granted; rootasrole-core refused them for setgid and capabilities, but b4b5a37 made it keep them for setuid, so chsr could save a policy that dosr refuses; the setuid set refuses them again, every other object keeps them
    • tests: the three credential sets refuse a typo, and a setuid set round-trips in CBOR
  • fix: cargo xtask install deploys the PAM service of dosr instead of an empty file (69ceb2b)
    • fix: the PAM service files were read at run time from ../../resources, a path relative to the source file, not to the working directory: from the project root it named /home/resources, and the privileged installer, root without effective capabilities, could not search a home directory of mode 0700 (Permission denied on Debian)
    • fix: /etc/pam.d/dosr was created before its content was read, so the failure left an empty PAM service that the next installation kept; the services are now built into xtask with include_str!, an empty file is replaced, and uninstall compares with the built-in services
  • tests: the Vagrant provisioning builds and installs RootAsRole again (63a351d)
    • tests: provision.sh ran its scripts with sudo -i, which escapes the newlines of the command: the login shell joined the lines as continuations and only sourced the cargo environment, so cargo xtask install never ran and the VM kept whatever dosr it had;the scripts run with sudo -H and set -e now
    • tests: the chsr --help integration test skips when chsr is not built (cargo integration-test only enables finder), CARGO_BIN_EXE_chsr is set anyway
  • fix: cargo xtask install installs a copy of root, not the build artifact itself (9576a0a)
    • fix: dosr and chsr were installed by renaming target//dosr to /usr/bin/dosr: the installed binary was the file thatthe user had built, still linked as target//deps/dosr-; until the installer changed its owner the user owned the file that was about to get every capability, and afterwards any chown -R of the build tree (vagrant rsync does one) gave it back to the user and removed its capabilities
    • fix: each binary is copied into a new file of root beside its destination, then renamed over it, so that a running dosr is never overwritten
    • tests: e2e.sh checks that the installed binaries belong to root, with one link, and that dosr has its capabilities; the check of a failed group lookup edits the current /etc/group, not the copy taken before the group was added
  • fix: dosr -r no longer lifts the separation of duties (79334f4)
    • fix: with -r, the roles of another name were skipped while the policy was read, actors included: the SSD plugin did not see the roles that the user holds, so dosr -r dev granted dev to a member of auditor although dev excludes auditor; without -r the request was refused
    • fix: every role is read now, and -r applies when the role is chosen; the hierarchy plugin also gets the parents again, so dosr -r senior gives the tasks of its parents
    • tests: -r chooses among the roles that allow the command, refuses a role that the user does not hold, keeps the separation of duties and the tasks of the parents
  • feat: chsr edits the separation of duties, the parents, the purpose, env override and hashed commands (bb231cc)
    • feat: chsr role R ssd|parents add|del|set R2,R3 and ssd|parents purge, a list of role names in the role; a role cannot name itself, a role that does not exist yet is told
    • feat: chsr role R task T purpose set and purpose unset
    • feat: chsr options env override allow|deny|unset, whether dosr -E may change the env behavior, at every level
    • feat: cmd whitelist|blacklist add --sha224|--sha256|--sha384|--sha512 [--read-only] [--immutable] , for the hashchecker plugin, with a warning that it is deprecated; the hash is checked, a malformed one is an error, never part of the command; del removes a command in either form, and add refuses a command line that is already listed in either form
    • tests: each command read back from the saved file; ssd del and purpose unset do not delete the role or the task, which the same keywords do
  • fix: cargo xtask install writes in the root file whether the policy is immutable (3c6111c)
    • fix: set_immutable assigned the value to a copy (if let Some(mut immutable) = settings.immutable), and the template has no storage settings: the root file never said whether the policy is immutable, and dosr and chsr took the build default, true, even on a file system without immutable flags
    • tests: set_immutable writes true and false in the storage settings of the template
  • tests: the SSD check of e2e.sh no longer hides the check of a planted policy file (fdbbbe0)
    • tests: the role of the SSD check allowed /usr/bin/id -u as the user, the least privileged task for the command that planted_policy_is_refused runs: dosr chose it with or without the planted file, and the check of the immutable flag could not tell; the SSD check runs id -g
  • fix: cargo xtask --dry-run changes nothing, and uninstall needs --apply (dd1f1c7)
    • fix: --dry-run was parsed but never applied, nothing called set_dry_run: a dry run of install elevated through the real privilege escalator (dosr) and ran the installation; run() applies it now
    • fix: in a dry run, the commands that change something (package manager, build, privilege escalation, manual pages) are printed instead of run, by run_checked and status_checked; the queries still run
    • fix: install, configure and uninstall print what they would change (the copy of the binaries, their owner, mode and capabilities, the PAM service, the policy) and stop, also when xtask runs as root and does not elevate
    • fix: uninstall removed the binaries and the PAM service without --apply, which its help says is required; without it, it prints what it would remove
    • tests: dry runs of install, install -bi, dependencies -i and configure, with a program that leaves a mark as the privilege escalation, never run it, and the same program does leave its mark when it runs; uninstall applies only with --apply and outside adry run
  • fix: a dry run of the dependencies does not say that they were installed (4c6e766)
  • feat: xtask checks the packages to build and to run RootAsRole, it no longer installs them (9f7555b)
    • feat: build and install -b stop before building when a build package is missing, with the packages and the command that installs them; install without -b checks the runtime packages; a dry run only warns, an OS that is not detected is not checked
    • feat: the check only reads the package database of the detected OS: dpkg-query (status installed), rpm -q --whatprovides, pacman -T
    • feat: doctor lists the missing packages to run and to build
    • chore: the dependencies command is removed, it ran the package manager as root (apt-get update, pacman -Syu --noconfirm, dnfupdate -y); install -i is hidden, does nothing and warns, so install -bip keeps working
    • fix: deps.json without clang and openssl, which nothing uses (no bindgen, clang-sys nor openssl in Cargo.lock), with gcc andpkg-config, and libseccomp2 instead of libseccomp-dev as a runtime package of Debian
    • tests: the provision script of the VMs and the workflows install the packages, then run install -bp
  • chore: prefer clang over gcc (e132e04)
  • fix: chsr keeps a regular expression on the arguments as typed, so that dosr matches it (4db9aee)
    • fix: chsr split the command with shell_words and joined it back, which quoted each word of a regular expression: '/usr/bin/tail ^(-n [0-9]+|-f) /var/log/caddy/[^/ ]+$' became /usr/bin/tail '^(-n' '[0-9]+|-f)' '/var/log/caddy/[^/' ']+$', which dosr nevermatched (it strips only the outer quotes); every regular expression of several words written with chsr was refused, in 4.1.0 too, and the backslashes were lost
    • fix: a regular expression on the arguments is now written as typed, after its path; any other command is still normalized asa shell splits it, and del finds a command in the same form
    • chore: rootasrole-core gets database::command, split_command and regex_arguments, the rule that tells a regular expression, used by dosr to match and by chsr to write
    • tests: chsr writes the regular expression of the book as typed, whether it comes as one argument or several, with its backslashes, and the rule of core reads it as one; the test fails with the former splitting
  • fix: dosr -i shows None for a task without capabilities (8bb8374)
    • fix: "capabilities": "none" or [] gives an empty set, which dosr -i printed as nothing after "With capabilities:"; an empty set now shows None, like a task that does not set them
    • tests: the display of no capabilities, an empty set, all of them and a list
  • fix: dosr -- ends the options of dosr, like sudo (ba91615)
    • fix: every word starting with - was taken as an option of dosr, so dosr -- refused the command line with "Unknown option: --"; scripts write -- before a command that could start with -
    • feat: -- ends the options: the next word is the command, even when it starts with -, and the rest are its arguments; with nothing after it, there is no command, as without it
    • tests: -- after options, before a command starting with -, before a plain command, and at the end
  • fix: dosr logs under the name dosr instead of sr (e44bdf3)
    • fix: dosr opened syslog as "sr", the name of RootAsRole 3, so its logs were read with journalctl -t sr; they are now under "dosr", next to those of chsr; a log filter on sr must be changed
    • fix: the error about the missing capabilities of dosr said "to run sr"
    • chore: rootasrole-core names its log function subscribe; subsribe stays as a deprecated alias for the users of 4.1
  • fix: the help of dosr and chsr lists what they accept (4267132)
    • fix: dosr --help had neither -S nor -K, and its usage now shows that -- may end the options
    • fix: chsr's help offered inherit for root, bounding and authentication, and display for execinfo, which the grammar refuses:execinfo takes show; the main commands now list options, file, convert, upgrade and editor
    • fix: two typos in the messages of chsr editor ("confguration", "[Y]ontinue")
  • fix: chsr editor edits the file of the policy that the other commands edit (e6b83c6)
    • fix: without a path, chsr editor opened the root file, while chsr role and the other commands edit default.json of a policy directory: with the layout of the packages, the roles were not in the file it opened; it now opens the same file, the one named, default.json, the only file, or the policy of the root file
    • fix: chsr editor gave the path of the policy file where the temporary folder was expected, so the copy for the editorcould not be created; the copy is always made in the temporary folder that Landlock allows
    • fix: a path that is not a file of the policy was read, edited and never saved; it is now refused like with chsr file; the path of the root file opens the root file
    • chore: --type of chsr editor is ignored with a warning, a file of the policy keeps its format; rootasrole-core gets FileSettings::root_path
    • tests: the file opened without a path, by name, by path, the root file, and the refusal of a file outside the policy
  • fix: chsr is confined by Landlock even when an editor or one of its files is missing (9762f7e)
    • fix: chsr opened every editor of RAR_CHSR_EDITOR_PATH, /usr/share/vim, /etc/vimrc and the other paths of the editor with PathFd::new()?, so one missing path made the whole ruleset fail, and chsr ran without Landlock with only a warning in syslog; Debianhas no /etc/vimrc, so chsr was never confined there, and a system without vim neither
    • fix: a missing optional path is now left out, which only allows less; the root file, the policy directory and the temporary folder stay required
    • tests: a missing path gives no rule, an existing one does; in the Debian VM without vim, chsr convert --from /etc/hostname is refused with Permission denied, where chsr read it before
  • fix: chsr file says why a file of the policy directory cannot be loaded (81d8663)
    • fix: a file of the directory that chsr could not load (written by hand without its version, for example) was answered "is not a file of the policy", like a file that does not exist; chsr file and chsr editor now give the path and the reason
    • tests: a file without version, named by its name and by its path, and a file that does not exist
  • fix: chsr prints the error of a failed command on its standard error (e5df878)
    • fix: when a command failed, chsr only logged the error, which goes to syslog alone in a release build: the terminal showed nothing, and the exit status was the only sign; it now prints chsr: cannot edit the policy: , like its other errors
  • tests: end-to-end checks of the fixes found while writing the dosr and chsr chapters (7341273)
    • tests: a regular expression of several words written with chsr is matched by dosr, the executions are logged under dosr, chsr is confined by Landlock without vim, -- ends the options, dosr -i shows None for a task without capabilities
    • tests: the policy file of e2e.sh has the version of the root file, without which chsr does not load it
  • feat: xtask builds with clang or gcc, and without pandoc (565f54c)
    • feat: a requirement of deps.json can be several packages of which one is enough: the C compiler that links the binaries is clang or gcc; a missing group is named whole, and its first package is the one to install
    • feat: optional packages, found by their command in PATH whatever installed them: pandoc only builds the manual pages, so without it xtask warns, with the command that installs it, and builds and installs everything else; doctor lists it
    • fix: the manual pages are built only with pandoc and gzip, the pages of a former build are removed first so they are never installed instead, and the installation says that none was built instead of failing to copy them
    • chore: command_exists moves from doctor to util
    • tests: one package of a group is enough, a failed query is not a missing package, the message of a group and of an optional package, every target accepts clang or gcc and has pandoc as optional; in the Debian VM without pandoc and without the clang package, cargo xtask install -bp sudo builds with gcc, installs without manual pages and exits 0
  • fix: a task that changes the user gives the command the groups of the target user (d0d28a0)
    • fix: a task with setuid and without setgid ran the command as the target user with every group of the caller, which neither of them has alone; the command now gets the groups of the target user, its primary group first and then the groups that list it, as a login gives them (getgrouplist, like sudo-rs User::from_libc and set_target_user)
    • fix: a setgid selector with "default": "all" and without fallback kept the groups of the caller without -g; it now gives thegroups of the target user too
    • feat: "from": "caller" or "target" in a setgid selector says whose groups the command gets when the task does not force themand the user does not choose any with -g; with from, the none default no longer requires -g; a task that keeps the user keeps the groups of the caller
    • fix: a lookup of the groups that fails refuses the command, it never falls back on the groups of the caller; the groups are read once the task is chosen, so the choice is the same, and dosr -i and the log show them
    • chore: no migration rewrites the tasks, the new default gives less (decided by the maintainer); the downgrade to 4.1.0 removes from, and a selector left with nothing else, so that 4.1.0 keeps the groups of the caller
    • tests: the groups of root for a setuid root, none for from: caller, without setuid or with forced groups, a failed lookup refused; from in the selector with and without -g and fallback; from in JSON and CBOR; the downgrade with the long and the short keys
  • feat: chsr cred set --setgid-from caller|target (85d6078)
    • feat: --setgid-from writes from in the setgid selector of the task, or a selector with only from when the task has none; it cannot go with --setgid, which forces the groups; the groups forced before are replaced, with a warning
    • feat: cred unset --setgid-from removes from, and a selector left with nothing else
    • fix: a command with only --setgid-from was taken for task add or del, which answered Invalid action
    • chore: the help of cred lists --setuid, --setgid, --caps and --setgid-from
    • tests: set on a task without setgid and on a selector, the refusal with --setgid, unset, and unset without from
  • tests: end-to-end checks of the groups of a task that changes the user (2669ef3)
    • tests: a task with setuid nobody and without setgid gives the groups of nobody, not those of the caller; with "from": "caller", those of the caller
  • fix: a task whose user or groups do not exist is left out instead of run with what is left (68ea667)
    • fix: a setuid user that nobody has (a typo, a removed account) was dropped, and the command ran as the caller with the capabilities of the task; a setgid group that does not exist was removed from the list, and with none left, the command kept the primary group of the caller
    • fix: the credentials of a task are resolved before it becomes the best task; a user, a group or an empty list of groups thatdoes not resolve leaves the task out, like a task that does not match, so that it tells nothing more before the authentication, and the log names the task and what is missing
    • tests: the two most precise tasks, one with a missing user and one with a missing group, lose to a task that accepts any arguments, and without it none is chosen; the test fails with the former conversion
  • tests: end-to-end check of a task whose group does not exist (b2cf239)
    • tests: dosr refuses the task, and its log names the task and the missing group
  • fix: a regular expression on the arguments applies to the whole arguments, whatever its alternations (bfb4c05)
    • fix: ^start|stop$ is ^start or stop$ for the regex crate, so a rule allowed any arguments starting with start or ending withstop; dosr now compiles ^(?:body)$, so that it allows start or stop alone; a group, a character class and an escaped bar keep their meaning, and an escaped final dollar sign stays in the body
    • chore: rootasrole-core gets regex_body, anchored_regex and has_top_level_alternation in database::command, which tells a baroutside of any group and class, escapes included
    • feat: the downgrade to 4.1.0 writes a top-level alternation ^(?:start|stop)$, in lists and selectors and with the short keys, so that 4.1.0 allows no more than 4.2.0; the upgrade leaves the rules as written, since they now allow less (decided by the maintainer)
    • tests: ^start|stop$ allows start and stop, not start anything nor please stop, and the test fails without the anchoring; groups, classes and escapes; the body and its anchored form; the downgrade and the upgrade
  • tests: end-to-end check of a top-level alternation on the arguments (bde245d)
    • tests: ^one|two$ allows echo one, and refuses echo one more and echo say two
  • tests: the final policy of the book is read by rootasrole-core and decides as the chapter says (27c1d5c)
    • tests: the policy under "The complete WebOps policy" of book/src/configuration.md is migrated and read by rootasrole-core, then dosr decides with it, the current user in place of the team except sys-admin: the logs are read with tail, less and journalctl, the path traversal and the pager of systemctl are refused, caddy-service manages caddy and nothing else, alice's escape with install is refused, web-push publishes where www-data exists, and the tasks of sys-admin are not held
    • tests: skipped when the book is not in the source tree (the crate and the Debian sources leave it out), so until the docs are merged into dev; checked against the current page, and the test fails when the rule of tail lets a path traversal through
  • fix: the PAM services that cargo xtask install deploys end with a newline (dd9ac92)
    • fix: the services of Red Hat, openSUSE and Arch had no final newline, so a line appended to /etc/pam.d/dosr was glued to thelast one (session include system-authsession optional pam_env.so …): the line was lost, and the include broken; found by the e2echeck of pam_env on Arch
    • tests: every PAM service built into xtask ends with a newline
  • tests: the Arch machine of tests/vagrant uses the generic/arch box (4e2ce86)
    • tests: archlinux/archlinux publishes no box for libvirt anymore; generic/arch does, and the Vagrantfile says how to upgrade it the first time, since vagrant installs rsync without upgrading the system and breaks it
  • feat: the tty option says who owns the terminal of the command, the caller by default (0c67f5e)
    • feat: "options": {"tty": {"owner": "caller"|"target"}} at every level, inherited like the others; caller, the default, keepsthe behaviour of 4.1.0: the terminal stays with the user who runs dosr, so the processes of the target account cannot open it, but a program that reopens its terminal by its name (pinentry through GPG_TTY) fails; target gives it to the user that the commandruns as, like sudo and sudo-rs since 0.2.11, at the cost of letting the other processes of that account open it while the command runs (#51)
    • feat: with target, and only for a command run as another user, dosr changes the owner of the pty with fchown on its descriptor, keeping its group and mode, with CAP_CHOWN raised for that call only; sudo-rs changes it by its path (src/exec/use_pty/parent.rs, get_pty); a failure stops before the command starts
    • feat: rootasrole-exec gets PtyFollower::set_owner and runner::run_with_pty_setup, which lets the caller prepare the pty before the command starts; run and run_with_pty keep their signature; a failed setup gives the caller back its signal mask
    • feat: the downgrade to 4.1.0 removes the tty option, which 4.1.0 does not know: the terminal stays with the caller
    • fix: a doc comment of path_only in the migrations was above another function
    • tests: the option's format, CBOR round trip, default and inheritance, its migrations, the owner chosen by dosr, set_owner, and a failed setup that stops before the command and restores the signal mask
  • feat: chsr edits the tty option at each level (c7f4e79)
    • feat: chsr [role R [task T]] options tty owner target|caller|unset, and options show tty
    • feat: unset removes the owner, then the option once nothing is left in it; the fields that this version does not know are kept
    • feat: the help lists the tty option, and a wrong command shows its usage; t stays the alias of timeout
    • tests: each level, the unknown fields, the refused values, timeout through its alias, the usage, and the saved file read back
  • tests: end-to-end checks of the owner of the terminal of a command run as another user (d35f82c)
    • tests: a task run as nobody through script sees a terminal of the caller that it cannot reopen by its name, and with "tty": {"owner": "target"} a terminal of nobody that it reopens, as pinentry does through GPG_TTY
    • tests: script gets no input, otherwise it copies into the terminal what is left on the input of the provisioner
    • tests: 24 checks pass on the Debian and Arch machines
  • feat: the default owner of the terminal is the build variable RAR_TTY_OWNER (3b8624a)
    • feat: RAR_TTY_OWNER in the [env] of .cargo/config.toml, read with env! like the defaults of the other options; caller upstream, so the default exposes nothing more than 4.1.0, and a build that wants the behaviour of sudo sets target
    • feat: TtyOwner::const_parse; a value that is neither caller nor target stops the build; TtyOwner::default() is the build-time value
    • chore: the Debian copies of [env] need RAR_TTY_OWNER, like RAR_PASSWORD_TRIES and RAR_PASSWORD_DELAY
    • tests: the shipped value is caller, the policy and dosr fall back to it
  • docs: rewriting the file configuration completely. (5e4eb6a)
  • docs: update usage (b8c3c45)
  • docs: reorganisation (54af4ba)
  • docs: update .gitignore to ignore hidden folders except two (9748f36)
  • docs: 4.2.0 news (acb8ab3)
  • tests: the read-logs task of the book is expected only where its groups exist (00604ca)
    • tests: the GitHub Actions container (xd009642/tarpaulin, Debian without systemd) has no systemd-journal group, so dosr leaves read-logs out, as a task whose group does not exist (68ea667), and test_the_policy_of_the_book failed on its first decision; the groups are read from the policy of the book, and without them the task must be left out
    • tests: checked with the page of the docs branch as is, and with systemd-journal replaced by a group that does not exist, where the previous version of the test fails like the CI
  • fix: dosr --help lists -S and -K once (30cd12d)
    • fix: the merge of docs into dev kept both help entries of -S and -K, the ones of docs: update usage (b8c3c45) and the ones of 4267132; the entries of 4267132 stay, since they say what dosr does: -K removes the cookies before authentication, not after it
    • tests: each option of dosr is listed once in its help; the test fails on the help of 6859639
  • tests: performance indicators of the finder, ignored by default (bb450d0)
    • tests: cargo test --release --all-features --lib -- --ignored --nocapture finder::tests::perf prints the time of a decision with a policy of 1 and of 10 000 tasks, where only the last task allows the command (the policies of perf/confgen), and the cost of one task; no time is asserted, the machine decides
    • tests: a command given by its path, with root named in each task (CBOR and JSON), root given by its id, the command searchedin the PATH, and commands written as patterns; one indicator at a time, each in its own folder
    • tests: on 4.2.0 (VM, release), one task costs 35 µs with root named (351 ms for 10 000 tasks), 12.8 µs with root by id, 76 µs for a command in the PATH (764 ms), 38 µs with patterns; sudo costs about 3 µs per rule on the same machine
  • perf: a path of the policy without pattern characters is not compiled as a glob (7c16d6b)
    • perf: match_single_path compiled every path of the policy that differed from the command into a globset matcher, about 10 µseach, for each command of each task: a path without *, ?, [, { or \ only matches the path written the same way, which the comparison before already found
    • perf: \ still makes a pattern, since globset reads it as an escape, so these paths keep their meaning
    • tests: the characters that make a pattern, an escaped *, and paths that differ by a suffix
    • perf: one task costs 23 µs instead of 35 µs with root named, 2.6 µs instead of 12.8 µs with root by id (the indicators of finder::tests::perf)
  • perf: the finder looks up each user and group name once (d488939)
    • perf: the score of each task checks whether its target is root, so a policy that names root (or any user or group) looked the name up for each task, through NSS: with systemd-userdb (group: files [SUCCESS=merge] systemd on Arch), about 20 µs per task
    • perf: DUserType and DGroupType keep the ids that a thread looked up, a name that nobody has included; a failed lookup is notkept, so that the separation of duties still sees it (09d0ea1)
    • tests: a name is looked up once, a name that nobody has is kept, an id is never looked up
    • perf: with the previous commit, one task costs 3.1 µs instead of 35 µs with root named (31 ms instead of 351 ms for 10 000 tasks), 13 µs instead of 76 µs for a command in the PATH, 16 µs instead of 38 µs with patterns
  • fix: a task that skips the authentication gives no timestamp cookie (f473694)
    • fix: start_session updated the cookie after every session, and update_cookie creates one when none is valid: a task with authentication skip gave a cookie, so a later task that requires the password, with the same type of timeout (ppid by default), ran without it, from the same parent; software in the account of the user could run the first task to get past the second; v4.1.0 does the same
    • fix: timeout::record_authentication creates or extends the cookie only when the authentication was not skipped, like sudo-rs, which records an authentication only after it (src/sudo/pipeline.rs, auth_and_update_record_file)
    • perf: with authentication skip, dosr no longer locks, reads and rewrites the cookie file
    • tests: a skipped authentication neither creates nor extends a cookie, and the test fails without the fix; tests/vagrant/e2e.sh: a task that requires the password, run from the same shell after a task that skips it, refuses wrong passwords
  • perf: dosr keeps the groups of the caller as ids (7973e2a)
    • perf: make_cred looked up the name of every group of the caller, through NSS, at each run: with systemd-userdb (group: files[SUCCESS=merge] systemd on Arch), about 0.5 ms per group, 2.2 ms for a user of 4 groups; the decision compares the gids of the policy with the gids of the caller (finder/de/roles.rs), and the logs and -i show the groups of the command, so the names were never used
    • perf: measured with a timing probe on the test VM (median of 200 runs, one task, authentication skip): the credentials take 0.1 ms instead of 2.2 ms; with the previous commit, a run takes 10.1 ms instead of 13.1 ms with 4.2.0 (hyperfine, 300 runs)
    • tests: make_cred keeps the ids of the groups, never looked up
  • perf: the finder compares the commands of a task first, and resolves its PATH only when needed (5fd8958)
    • perf: the options of each task (its environment entries, -E, the working directory, -i), its score and the notification of the plugins were computed before its commands were compared, for every task of the policy; most tasks do not allow the command, and a task that does not returned false either way, so they are now computed for the tasks that allow it
    • perf: the PATH of a task (an IndexSet per task) is only resolved when the command is typed by its name, or when a rule of the task resolves the command itself (**, the all default, a complex command for a plugin); the task that wins gets its PATH as before
    • perf: BestTaskSettingsFound is sent for the tasks that allow the command, which no plugin listens to
    • perf: one task costs 2.1 µs instead of 3.1 µs with root named, 1.5 µs instead of 2.7 µs with root by id (finder::tests::perf), with the next commit
  • perf: a command of the policy that is only a path is not split (3a92935)
    • perf: every command of every task went through shell_words::split and was copied into a vector of strings; a command withoutspace, quote or backslash is a path alone, matched as it is: like a command line of one word, it allows the command without arguments
    • perf: with the previous commit, one task costs 2.1 µs instead of 3.1 µs with root named (finder::tests::perf)
  • perf: a decision searches a command in each PATH once (573f9d5)
    • perf: a command typed by its name was searched in the PATH for each command of each task, with a stat per directory, though most tasks share their PATH; the searches are kept while find_best_exec_settings runs, by PATH and command name, and dropped at its end, so that another decision, or a direct call (tests, fuzzing, plugins), sees the files as they are then; the candidates andtheir order are the same
    • perf: one task costs 3.0 µs instead of 13 µs for a command typed by its name (finder::tests::perf)
  • perf: a pattern whose literal start differs from the command is not compiled (675369d)
    • perf: each pattern of the policy was compiled into a globset matcher (about 10 µs) for each command typed; the characters before the first special one (*, ?, [, {, ) are matched as they are, so a command path that does not start with them cannot match,and the pattern is not compiled
    • tests: the literal start of patterns, and a pattern whose start differs
    • perf: one task costs 2.0 µs instead of 13 µs with patterns that start differently (finder::tests::perf); a pattern that starts like the command, such as /usr/bin/*, is still compiled
  • docs: update performance evaluation details (742b7f3)
  • chore: update nonstick2 to version 0.1.5 for security fixes (d09e8da)
  • fix: the users and groups of the policy are looked up once per decision, not once per thread (793893f)
    • fix: d488939 kept the id of each name that a thread looked up for the whole life of the thread; dosr only lives for one decision, but rootasrole-core is a library: a program that lives longer (a plugin, a daemon) would keep a user deleted since, whose uid may now be another user's, and would never see a user created after its first lookup
    • fix: NameLookupCache::start keeps the names while it lives, like the PATH searches (573f9d5); find_best_exec_settings startsit, and outside a decision try_fetch_id looks each name up again; a failed lookup is still never kept
    • tests: the names are kept during a decision, and none outside it or after it
    • perf: unchanged for dosr, one task costs 1.9 µs with root named (finder::tests::perf)
  • tests: a command of one word matches like its shell splitting did in 4.1.0 (996ab91)
    • tests: 3a92935 matches a command of the policy without space, quote or backslash as a path, without shell_words::split; for paths, patterns, **, relative names and words with #, and for commands with and without arguments, the result and the path found are those of match_command_line on the split words
    • tests: a word that starts with # is a comment for the shell, so it allowed nothing in 4.1.0; it allows nothing either way, since the command of the user is resolved to an absolute path before it is compared