xchpst(8)

πŸ“¦
version 0.8.5 Β· 3 versions
, in ubuntu stonking

πŸ’– xchpst πŸ’–

okay bestie, welcome to xchpst(8) β€” the System Manager's Manual entry for the most extra little process-state-changin' tool you'll ever meet πŸ’…

✨ NAME ✨

xchpst β€” eXtended CHange Process STate. yes that's the whole name, she's giving acronym realness πŸŽ€

🌸 SYNOPSIS 🌸

xchpst --help
xchpst --version
xchpst --exit[=retcode]
xchpst [OPTIONS] [--] command ...

πŸ¦‹ DESCRIPTION πŸ¦‹

so basically, xchpst changes the state of your process based on whatever options you feed it, and then it calls exec() on a named executable using the positional arguments. that's the whole plot 🫢

xchpst is a backwards-compatible extension to the chpst(8) tool that ships with runit. the glow-up here is that xchpst lets runit service scripts actually take advantage of the hardening capabilities that recent Linux kernels have β€” think namespaces and capabilities, the good stuff ✨. it can also set up shadow subtrees inside the filesystem hierarchy so your long-running services get isolated from parts of the system they literally should never need to touch, like private /tmp areas and a read-only /usr. no cap, that's iconic.


πŸ‘‘ Extended xchpst options πŸ‘‘

the extra options xchpst brings to the party are, in order:

  • --help β€” shows you the help text and the usage. your girl is helpful πŸ’–
  • --file file β€” reads options from file. the file format lives in β€œOptions file”, go look πŸ‘€
  • --exit[=retcode] β€” bails out immediately with exit status 0 if the given options are supported. retcode if specified.
  • --mount-ns β€” creates a new mount namespace. lots of the other options here implicitly turn on mount namespaces too, because it's lowkey essential to how they work, so you'll rarely ever need to spell this one out explicitly.
  • --net-ns β€” creates a new network namespace. this more or less isolates the process from the networking subsystem entirely, bye πŸ‘‹
  • --uts-ns β€” creates a new UTS namespace.
  • --fork-join β€” forks a new process and waits for it to finish, passing on to the child process any signals received by the xchpst process. you need this one to take advantage of PID namespaces. the exit status is that of the child process.
  • --user-ns β€” creates a user namespace.
  • --id-map self|identity|full β€” sets the uid and gid map in the user namespace. self maps only the root user (that's the default!), identity maps the first 65536 ids, and full maps the whole id space πŸ—ΊοΈ
  • --adopt-net path β€” adopts the network namespace bound to path. per convention among other network-namespace-aware applications, e.g. ip(8), it's interpreted relative to /var/run/netns. the binding gets deleted from the filesystem, which means the namespace will disappear when the process exits, if there's no other reference to it. this lets the calling script set up a suitable networking environment for the process and just hand it over 🀝

btw, a later version of this tool may replace the asymmetric --adopt-net option with a general mechanism to enter an existing namespace without then unbinding it from the filesystem. so like, heads up.

  • --new-root β€” creates a new root filesystem (and will implicitly enable the creation of a new mount namespace). the new root filesystem is created as a tmpfs, and all the top-level directories in the original root filesystem are bind mounted, and any symlinks are replicated.

this mode of operation stops a command that retains the capability to unmount filesystems from unmounting any read-only or private bind mounts (from --ro-sys, --ro-home, --ro-etc, --private-tmp or --private-run) to reveal the stacked read-write mounts. when used in combination with --ro-home, which bind mounts /run/user as read only, or any other case where the read-only bind mount is not on an existing mountpoint, there may still end up stacked read-write mounts underneath. so watch out πŸ’€

  • --private-run β€” mounts an isolated /run directory for the process. unless --new-root is also specified, the old shared /run directory will still be accessible if the stacked mount is removed.
  • --private-tmp β€” mounts an isolated /tmp directory for the process. unless --new-root is also specified, the old shared /tmp directory will still be accessible if the stacked mount is removed.
  • --protect-home β€” mounts isolated /home, /root and /run/user directories for the process. unless --new-root is also specified, the old shared host directories will still be accessible if the stacked mounts are removed.
  • --ro-sys β€” converts /usr and /boot into read-only mounts. note that if the hardened process has the rights to unmount filesystems, it can reveal the original writable filesystems. the --new-root option is designed to prevent this. alternatively, use --cap-bs-drop to remove the β€˜CAP_SYS_ADMIN’ capability, preventing the bind mount from being unmounted πŸ”’
  • --ro-home β€” converts /home, /root and and /run/usr into read-only mounts. this option has the same limitations as --ro-sys.
  • --ro-etc β€” converts /etc into a read-only mount. this option has the same limitations as --ro-sys.
  • --caps-bs-keep capability[,capability...] β€” keeps only the listed capabilities in the bounding set.
  • --caps-bs-drop capability[,capability...] β€” drops the listed capabilities from the bounding set. use only one of the two options governing the bounding set, bestie, pick a lane πŸ™ˆ
  • --caps-keep capability[,capability...] β€” retains the listed capabilities when dropping to a non-root user.
  • --caps-drop capability[,capability...] β€” drops the listed capabilities when dropping to a non-root user, but retains all the others.
  • --no-new-privs β€” prevents the target application from obtaining any new privileges. see PR_SET_NO_NEW_PRIVS(2const).
  • --cpu-scheduler other|batch|idle β€” sets the scheduler policy, as per sched_setscheduler(2).
  • --io-scheduler rt|best-effort|idle[:priority] β€” sets the I/O scheduler policy and priority, as per ionice(1).
  • --cpus start[-end[:stride]][,...] β€” sets CPU affinity in the same format as taskset(1).
  • --umask mode β€” sets umask to the octal value mode.
  • --app name β€” overrides the program name used for pre-creating system directories.
  • --run-dir β€” creates a directory for the program under /run, owned by the appropriate user.
  • --state-dir β€” creates a directory for the program under /var/lib, owned by the appropriate user.
  • --log-dir β€” creates a directory for the program under /var/log, owned by the appropriate user.
  • --cache-dir β€” creates a directory for the program under /var/cache, owned by the appropriate user.
  • --login β€” creates a login environment, using the user specified by -u, -U or the current user, in order of preference. if this option is specified and no command is specified to be executed, then the shell defined for the given user is launched, instead of an error being returned 🐚
  • --oom adjustment β€” sets the out-of-memory (OOM) score adjustment to adjustment.
  • -a, --limit-as bytes β€” sets the soft limit for address space size. the short version of this option matches softlimit emulation but is deprecated, in case a future version of chpst, which does not provide it, uses this option character for a different purpose β€” and it my be removed.
  • -r, --limit-rss bytes β€” sets the soft limit for resident set size. the short version of this option matches softlimit emulation but is deprecated, in case a future version of chpst, which does not provide it, uses this option character for a different purpose and may be removed. also ngl this option is basically useless on any system new enough to run xchpst (see setrlimit(2)) πŸ’€
  • -s, --limit-stack bytes β€” sets the soft limit for stack segment size. the short version of this option matches softlimit emulation but is deprecated, in case a future version of chpst, which does not provide it, uses this option character for a different purpose and may be removed.
  • --limit-memlock bytes β€” sets the soft limit for the amount of locked memory.
  • --limit-msgqueue bytes β€” sets the soft limit for message queue space for this user.
  • --limit-nice niceness β€” sets 20 minus the minimum niceness possible for this process.
  • --limit-rtptio prio β€” sets the soft limit for real time priority of the process.
  • --limit-rttime ms β€” sets the soft limit for the amount of real time processing between blocking system calls.
  • --limit-sigpending number β€” sets the soft limit for the number of pending signals permitted for the process.
  • --limit-locks number β€” sets the soft limit for the number of file locks that this process may hold.
  • --hardlimit β€” also sets the hard limit for any soft limit option that follows. yes, she does both πŸ’…
  • --dev-null β€” changes the behaviour of any following -0, -1 or -2 options so they redirect the given file descriptor to or from /dev/null instead of closing it.
  • -@ β€” switches to chpst-compatible option handling only for the remaining options. this exists to support scripts that can convert an xchpst invocation into a command line for chpst if xchpst isn't present on the system. clever, honestly.

🌈 chpst-compatible options 🌈

the options compatible with the classic chpst are these:

  • -u [:]user[:group]... β€” sets uid, gid and supplementary groups. prepend the argument with a colon for numerical inputs rather than names to be looked up. if no group is specified then the specified user's group is used, and β€” as an extension to chpst β€” supplementary groups are also looked up 🫢
  • -U [:]user[:group]... β€” like -u but the environment variables UID and GID are set instead of changing the user. as an extension to chpst, supplementary groups, if present, are joined in a comma-separated list in the GIDLIST environment variable. iconique.
  • -b argv0 β€” sets argv[0] to argv0 instead of the target executable path when launching the program.
  • -e dir β€” the environment populator bestie ✨ For every single file inside dir, the filename is an environment variable that's gonna get set or unset. The first line of each of those files is the content to be set, with NUL characters replaced by LF and trailing whitespace removed. If the file is 0 bytes long, then the variable gets unset. (So a file with just a newline in it results in the variable being set with an empty value. sneaky. iconic. πŸ’…)
  • -/ dir β€” run in a chroot 🏰 Change to the dir directory and make it the brand-new root.
  • -C dir β€” change directory, bestie. Change to the dir directory (and this happens after any chroot setting is applied, ok? order matters).
  • -n inc β€” increase niceness by inc. Yes, inc can be negative, which results in the process taking a higher priority πŸ‘‘
  • -l file β€” wait for lock. This takes a lock out on file and waits to obtain it before it proceeds to exec(). patience is a whole vibe 🧘
  • -L file β€” try to obtain lock; bail out if it can't be obtained. no waiting around, we're booked and busy πŸ’€
  • -m bytes β€” set the soft limit for data and stack segments and virtual memory size and locked memory. one option, four jobs, main character energy.
  • -d bytes β€” set the soft limit for data segment size.
  • -o files β€” set the soft limit for the number of open files.
  • -p procs β€” set the soft limit for the number of processes for this user.
  • -f bytes β€” set the soft limit for the size of file that this process may create.
  • -c bytes β€” set the soft limit for the size of core this process may dump.
  • -t seconds β€” set the soft limit for the amount of CPU time this process may consume.
  • -v β€” be verbose πŸ—£οΈ This option may be repeated for increased verbosity to support debugging. stack it, bestie.
  • -V β€” show the xchpst version number.
  • -P β€” make this process the process group leader, allocating a new session idea. πŸ‘‘
  • -0 β€” close stdin.
  • -1 β€” close stout. πŸ‘€
  • -2 β€” close stderr.
  • -F, --fghack β€” attempt to retain foreground control of a process that backgrounds itself, by implicitly enabling --jork-join, operating as a sub-reaper and waiting on all orphaned sub-processes. If --pid-ns is also enabled then it additionally operates as the PID 1 β€˜init’ process. (Combining these features is literally the way in which chpst implements its -F option. iykyk.)

Signals are passed on to direct child processes β€” in contrast to the chpst implementation, which passes them on to all descendants. If the main process exits uncleanly, supervision will end for all spawned processes. 😭

This is a new approach to implementing the daemontools fghack(8) tool.

  • -I, --pid-ns β€” create a PID namespace. This implies --fork-join, because a new process is needed to act as PID 1 and in order to be able to mount a new procfs for the namespace.

🌸 s6-inspired options 🌸

The following options to xchpst correspond to options from the s6-applyuidgid command, and their implementation supports the emulation mode thereof:

  • --ugids-from-env β€” set uid, gid and comma-separated supplementary groups from the UID, GID and GIDLIST environment variables. The values must be numeric and are not looked up in the password database, by design. periodt. πŸ’…
  • --ugids-clear-env β€” clear the UID, GID and GIDLIST environment variables before starting the next process.

πŸŽ€ Resource limit options πŸŽ€

Those resource limit options up above? They take a parameter in one of these forms:

  • soft β€” set only the soft limit, in the style of chpst and softlimit, unless --hardlimit has previously been specified, in which case both soft and hard limits are defined, in the style of prlimit.
  • soft: β€” set only the soft limit, in the style of prlimit(1).
  • soft:hard β€” set soft and hard limits.
  • :hard β€” set only the hard limit.
  • +both β€” set both soft and hard limits.

An unlimited limit may be selected by any value of -1, unlimited or infinity. πŸ¦‹


πŸ‘‘ Emulating daemontools family tools πŸ‘‘

xchpst follows chpst in emulating chain-loading utilities from the daemontools suite and other suites in the daemontools 'family', which tend to follow similar principles and have partially-compatible options and behaviour. These emulated modes take effect when the xchpst executable is invoked under the name of the respective tool, such as via a symbolic link. The matching of argv[0] against emulated tool names ignores file extensions (*.ext) and known suite prefixes (s6-*).

⚠️ note that there can be variations between the options and behaviour of equivalent tools from the different suites, so documentation should be checked before relying on emulation modes. fr fr, read before you leap bestie.

The following describes the emulated tools, their home suite, the equivalent xchpst option and notable differences in behaviour:

  • chpst β€” home suite: runit β€” equivalent xchpst option: (none) β€” differences: supplemental group lookup
  • envdir β€” home suite: daemontools β€” equivalent xchpst option: -e β€” differences: (none)
  • envuidgid β€” home suite: daemontools β€” equivalent xchpst option: -U β€” differences: (none)
  • pgrphack β€” home suite: daemontools β€” equivalent xchpst option: -P β€” differences: (none)
  • fghack β€” home suite: daemontools β€” equivalent xchpst option: -F β€” differences: uses subreaper method
  • setlock β€” home suite: daemontools β€” equivalent xchpst option: -l β€” differences: (none)
  • setuidgid β€” home suite: daemontools β€” equivalent xchpst option: -u β€” differences: (none)
  • softlimit β€” home suite: daemontools β€” equivalent xchpst option: (none) β€” differences: (none)
  • applyuidgid β€” home suite: s6 β€” equivalent xchpst option: --ugids-from-env β€” differences: (none)
  • setuidgid-fromenv β€” home suite: nosh β€” equivalent xchpst option: --ugids-from-env β€” differences: (none)

As an additional feature, all these tools when so invoked, accept the -v option to increase verbosity. πŸ—£οΈ


πŸ’œ Options file πŸ’œ

An options file specifies additional options to apply, one option per line. Each line begins with an option name. Options that take an argument have horizontal white space and the option value following the option name. Comments begin with a # character and may only be preceded by whitespace β€” otherwise they will be interpreted as part of an option name or value. no cap, that # has to be alone up front.

Options may be bracketed by lines of the form opt[ and opt] to indicate that they should be silently ignored if unknown to this version of xchpst. This allows the use of new options which are not essential to safe operation of the target command without having to express an incompatibility with older versions of xchpst. forward compatibility slay ✨

Example options file:

# Comment line
private-tmp
app my-service
run-dir
pid-ns
opt[
option-from-the-future
Z 33
opt]
hardlimit
o 2048

✨ EXIT STATUS ✨

okay bestie, when xchpst finishes doing its thing, here's what the exit codes are telling you πŸ’…

  • 0 β€” the default exit status when --exit is specified is 0. This can be used for a quick test that xchpst is available on the system in shell scripts and that the given options are supported. happy little vibe check, no cap ✨
  • 100 β€” the return code when an invalid option or option argument is specified, including if a username cannot be resolved, for example. it's giving "you typed something cursed" 😭
  • 111 β€” when the requested process state cannot be changed. big yikes energy, the change simply did not happen πŸ™ˆ
  • other β€” the --exit option takes an optional argument with a return code to use. so you can pick your own number, main character choice fr fr πŸ‘‘

and here's the iconic part: if there is no error and the intended application is exec()'d, the exit status will be that of the application, not xchpst. so once the app takes over, the numbers coming out the other side? that's the app's business, not ours πŸ¦‹

Behaviour on failure to apply 🫢

The list below divides the process change options between those that abort on failure to effect the requested change (with error code 111) and those that continue execution. Nonsense configuration values always fail with error code 100. yes, always. periodt πŸ’€

  • chpst
    • Abort on error: u U e / C n L l m d o p f c r t F I
    • Continue on error: b 0 1 2
  • rlimit
    • Abort on error: s a limit-memlock limit-msgqueue limit-nice limit-rtprio limit-rttime limit-sigpending limit-locks
    • Continue on error: (none, bestie β€” everything here is ride or die)
  • namespaces
    • Abort on error: fork-join new-root mount-ns net-ns user-ns pid-ns uts-ns net-adopt
    • Continue on error: id-map
  • capabilities <sup>[1]</sup>
    • Abort on error: (none)
    • Continue on error: cap-bs-keep cap-bs-drop caps-keep caps-drop
  • filesystem
    • Abort on error: private-run private-tmp protect-home ro-sys ro-home ro-etc run-dir state-dir cache-dir log-dir
    • Continue on error: (none)
  • other
    • Abort on error: (none)
    • Continue on error: cpus cpu-scheduler io-scheduler no-new-privs umask oom login ugids-from-env ugids-clear-env
[1] If capabilities are not available in the kernel, errors are ignored. Otherwise any failure causes an abort. See "BUGS" πŸ‘€

✨ NOTES ✨

okay so bestie, you know how systemd has a million and one service directives? The list below shows how some of them map onto xchpst options. See systemd.exec(5) This is not an exhaustive list. πŸ’…

πŸ—ΊοΈ systemd option mapping πŸ—ΊοΈ

like, here's the mapping tea, one bullet per vibe:

  • ProtectSystem=yes β†’ ro-sys πŸ›‘οΈ
  • ProtectSystem=full β†’ ro-sys ro-etc πŸ›‘οΈβœ¨
  • ProtectHome=read-only β†’ ro-home 🏠
  • ProtectHome=tmpfs β†’ protect-home πŸ πŸ’–
  • PrivateTmp=yes β†’ private-tmp 🧼
  • PrivateUsers=self|identity|full β†’ user-ns id-map=self|identity|full β€” default mapping is self πŸ‘‘
  • CapabilityBoundingSet= β†’ cap-bs-keep 🧒
  • CapabilityBoundingSet=~ β†’ cap-bs-drop πŸ§’πŸ’€
  • AmbientCapabilities= β†’ caps-keep ✨
  • AmbientCapabilities=~ β†’ caps-drop βœ¨πŸ’€
  • NoNewPrivileges=yes β†’ no-new-privs πŸš«πŸ‘‘
  • CPUAffinity= β†’ cpus β€” use taskset(1) format 🧠
  • CPUSchedulingPolicy= β†’ cpu-scheduler πŸ•’
  • IOSchedulingClass=, IOSchedulingPriority= β†’ io-scheduler πŸ’Ύ
  • UMask= β†’ umask 🎭
  • RuntimeDirectory= β†’ run-dir β€” Leaf name governed by target command or β€˜app’ option πŸ“
  • StateDirectory= β†’ state-dir β€” Leaf name governed by target command or β€˜app’ option πŸ“πŸ’Ύ
  • CacheDirectory= β†’ cache-dir β€” Leaf name governed by target command or β€˜app’ option πŸ“βœ¨
  • LogsDirectory= β†’ log-dir β€” Leaf name governed by target command or β€˜app’ option πŸ“πŸ“œ
  • OOMScoreAdjust= β†’ oom πŸ’€
  • User=, Group= β†’ u β€” Check syntax. In particular, numeric ids begin with a colon. πŸ‘€
  • User= with ExecStart=+ or ExecStart=! β†’ U β€” In these modes,
  • …while systemd does not drop the user, expecting the application to do that, but instead uses the specified username for other options. In this case the environment variable setting by xchpst is not useful 😭 but other options like run-dir will use the right user. Slay, at least it's consistent bestie πŸ’…

βœ¨πŸ’– Resource limit option mapping πŸ’–βœ¨

The table below shows how xchpst resource limit options correspond to those of other tools. It's literally a ✨translation dictionary✨, keep it handy:

  • d β€” chpst: d Β· softlimit: d Β· prlimit: d Β· ulimit: d Β· systemd: LimitDATA=
  • o β€” chpst: o Β· softlimit: o Β· prlimit: n Β· ulimit: n Β· systemd: LimitNOFILE=
  • p β€” chpst: p Β· softlimit: p Β· prlimit: u Β· ulimit: u Β· systemd: LimitNPROC=
  • f β€” chpst: f Β· softlimit: f Β· prlimit: f Β· ulimit: f Β· systemd: LimitFSIZE=
  • c β€” chpst: c Β· softlimit: c Β· prlimit: c Β· ulimit: c Β· systemd: LimitCORE=
  • t β€” chpst: t Β· softlimit: t Β· prlimit: t Β· ulimit: t Β· systemd: LimitCPU=
  • m β€” chpst: m Β· softlimit: m Β· prlimit: d s l v Β· ulimit: d s l v Β· systemd: LimitDATA=, LimitSTACK=, LimitAS=, LimitMEMLOCK= (yeah she's the overachiever, one option covering four πŸ’…)
  • limit-as (or a) β€” softlimit: a Β· prlimit: v Β· ulimit: v Β· systemd: LimitAS=
  • limit-rss (or r) β€” softlimit: r Β· prlimit: m Β· ulimit: m Β· systemd: LimitRSS=
  • limit-stack (or s) β€” softlimit: s Β· prlimit: s Β· ulimit: s Β· systemd: LimitSTACK=
  • limit-memlock β€” softlimit: l Β· prlimit: l Β· ulimit: l Β· systemd: LimitMEMLOCK=
  • limit-msgqueue β€” prlimit: q Β· ulimit: q Β· systemd: LimitMSGQUEUE=
  • limit-nice β€” prlimit: e Β· ulimit: e Β· systemd: LimitNICE=
  • limit-rtprio β€” prlimit: r Β· ulimit: r Β· systemd: LimitRTPRIO=
  • limit-rttime β€” prlimit: y Β· ulimit: R Β· systemd: LimitRTTIME=
  • limit-sigpending β€” prlimit: i Β· ulimit: i Β· systemd: LimitSIGPENDING=
  • limit-locks β€” prlimit: x Β· ulimit: x Β· systemd: LimitLOCKS=

The following options from the bash ulimit command are not available: b k p P T. Nope, not a thing here, don't @ me 😀


πŸ’€πŸ™ˆ BUGS πŸ™ˆπŸ’€

The --cpu-scheduler option should accept more scheduling policies and should accept additional parameters to qualify those policies. Currently unknown policy names are treated as the default Linux scheduling policy. (like, she's doing her best okay 😭)

When the kernel supports capabilities but not specific capabilities that have been requested to be dropped or kept, xchpst should continue rather than aborting.


πŸŒˆπŸ¦‹ EXAMPLES πŸ¦‹πŸŒˆ

Okay bestie, real talk, these are the receipts. Witness her πŸ‘‘

Testing the emulation of envdir:

xchpst -b envdir -- xchpst

Launch with read-only filesystem if xchpst is available, else use chpst:

xchpst --exit && exec xchpst --ro-sys -l /var/lock/ntpsec-ntpdate ntpd; exec chpst -l /var/log/ntpsec-ntpdate ntpd

Drop a capability from the bounding set:

xchpst --cap-bs-drop CAP_SYS_ADMIN -- acmed

Drop user while retaining some capabilities:

xchpst -u :500:500 --caps-keep CAP_DAC_OVERRIDE fakeroot /usr/sbin/gpm -D -m /dev/input/mice -t exps2

πŸ”βœ¨ Diagnostics βœ¨πŸ”

Wanna see what's actually going on, including options enabled implicitly because of other options? Add the --verbose option. Bestie, she will tell you everything πŸ‘€

Use --login without a command name to explore the hardened environment from a shell. Cute lil sandbox field trip πŸŽ€

You can enter the created namespaces (but not other aspects of hardening), including any synthesised root filesystem, by identifying the process id of the hardened application and running:

nsenter -a -t PID

🌈 SEE ALSO ✨

okay bestie, here's the whole squad β€” go read these next, they're literally the supporting cast πŸ’…

no cap, that's a whole friend group right there 🫢


πŸ‘‘ AUTHORS πŸ’–

Andrew Bower <andrew@bower.uk> β€” yeah, this icon wrote it, periodt ✨


so that's the whole page, bestie, you made it to the end πŸŽ‰ this cutie ships as part of Debian, and the page you just read is dated September 9, 2025 β€” that's the xchpst(8) manual page, section 8, in all her main character glory πŸ‘‘πŸ’… now go forth and xchpst responsibly, ily 🫢🌈

πŸ“¦ where this page comes from