SUDO_PLUGIN(5)
โจ NAME โจ
sudo_plugin โ the Sudo Plugin API ๐ (this one's from the File Formats Manual, section 5, so it's basically the official rulebook ๐)
๐ธ DESCRIPTION ๐ธ
Okay so, starting with version 1.8, sudo supports a plugin API for policy and session logging ๐
. Plugins can be compiled as dynamic shared objects (the default on systems that support them) or compiled statically into the sudo binary itself ๐ง. By default, the sudoers plugin handles audit, security policy and I/O logging, and it's giving everything you need โจ. But through the plugin API, sudo can be set up to use alternate plugins made by third parties ๐ซถ. The plugins you want get listed in sudo.conf(5), so go check there too ๐.
The API is versioned with a major and minor number ๐ข. The minor number goes up when additions are made, and the major number goes up when there are incompatible changes ๐. A plugin should check the version passed to it and make sure the major version matches, bestie, because otherwise things get messy ๐.
The plugin API is defined in the <sudo_plugin.h> header file ๐.
๐ฆ Policy plugin API ๐ฆ
A policy plugin has to declare and populate a struct policy_plugin in the global scope ๐. This struct holds pointers to the functions that actually do the sudo policy checks, so it's kind of the main event ๐. The symbol name should be specified in sudo.conf(5) along with a path to the plugin, so sudo knows where to find it and can load it ๐ฅ.
struct policy_plugin {
#define SUDO_POLICY_PLUGIN 1
unsigned int type; /* always SUDO_POLICY_PLUGIN */
unsigned int version; /* always SUDO_API_VERSION */
int (*open)(unsigned int version, sudo_conv_t conversation,
sudo_printf_t sudo_plugin_printf, char * const settings[],
char * const user_info[], char * const user_env[],
char * const plugin_options[], const char **errstr);
void (*close)(int exit_status, int error);
int (*show_version)(int verbose);
int (*check_policy)(int argc, char * const argv[],
char *env_add[], char **command_info[],
char **argv_out[], char **user_env_out[], const char **errstr);
int (*list)(int argc, char * const argv[], int verbose,
const char *user, const char **errstr);
int (*validate)(const char **errstr);
void (*invalidate)(int rmcred);
int (*init_session)(struct passwd *pwd, char **user_env[],
const char **errstr);
void (*register_hooks)(int version,
int (*register_hook)(struct sudo_hook *hook));
void (*deregister_hooks)(int version,
int (*deregister_hook)(struct sudo_hook *hook));
struct sudo_plugin_event * (*event_alloc)(void);
};
A struct policy_plugin has the following fields ๐:
typeโ This should always be set toSUDO_POLICY_PLUGIN๐.versionโ This should be set toSUDO_API_VERSIONโจ. That's how sudo figures out which API version the plugin was built against, so don't skip it ๐.openโ The entry point that sets everything up ๐. Its prototype is:
int (*open)(unsigned int version, sudo_conv_t conversation,
sudo_printf_t sudo_plugin_printf, char * const settings[],
char * const user_info[], char * const user_env[],
char * const plugin_options[], const char **errstr);
It returns 1 on success, 0 on failure, -1 if a general error happened, or -2 if there was a usage error. For that last one, sudo prints a usage message before it exits ๐
. If something goes wrong, the plugin can optionally call the conversation() or sudo_plugin_printf() function with SUDO_CONF_ERROR_MSG to show the user some extra error info ๐ซถ.
The function arguments are as follows:
versionโ The version sudo passes in lets the plugin figure out the major and minor version numbers of the plugin API that sudo supports ๐ข.conversationโ A pointer to theconversation()function, which the plugin can use to talk to the user (see โConversation APIโ for details) ๐ฌ. It returns0on success and-1on failure.sudo_plugin_printfโ A pointer to aprintf()-style function for showing informational or error messages (see โConversation APIโ for details) ๐ฃ. It returns the number of characters printed on success and-1on failure.settingsโ A vector of user-supplied sudo settings, written asname=valuestrings ๐. The vector ends with a NULL pointer. These settings match options the user passed when running sudo, so they only show up when that option was specified on the command line ๐.
When parsing settings, the plugin should split on the first equal sign (=), since the name never includes one but the value might ๐ฅ.
These are the values sudo may set:
bsdauth_type=stringโ The authentication type, if specified with the-aoption, to use on systems where BSD authentication is supported ๐.closefrom=numberโ If it's specified, the user asked (with the-Coption) for sudo to close all file descriptors with a value of number or higher ๐ช. The plugin can optionally pass this value, or a different one, back in thecommand_infolist ๐.cmnd_chroot=stringโ The root directory (seechroot(2)) to run the command in, as the user specified with the-Roption ๐ณ. The plugin can ignore or restrict the user's ability to pick a new root directory. Only available starting with API version 1.16 โณ.cmnd_cwd=stringโ The working directory to run the command in, as the user specified with the-Doption ๐. The plugin can ignore or restrict the user's ability to pick a new working directory. Only available starting with API version 1.16 โณ.debug_flags=stringโ A debug file path name, followed by a space and a comma-separated list of debug flags that match the plugin'sDebugentry insudo.conf(5), if there is one ๐. The flags get passed to the plugin exactly as they appear insudo.conf(5). The syntax sudo and the sudoers plugin use issubsystem@priority, but a plugin is free to use a different format as long as it doesn't include a comma (,) ๐ . Before sudo 1.8.12, there was no way to specify plugin-specificdebug_flags, so the value was always the same as the one used by the sudo front-end, and it didn't include a path name, just the flags themselves ๐ฐ๏ธ. As of version 1.7 of the plugin interface, sudo only passesdebug_flagsifsudo.conf(5)contains a plugin-specificDebugentry ๐ .ignore_ticket=boolโ Set to true if the user specified the-koption along with a command, which means they want sudo to ignore any cached authentication credentials ๐. (Sudo may also setimplied_shellto true. This lets sudo with no arguments be used similarly tosu(1). If the plugin doesn't support this usage, it may return a value of-2from thecheck_policy()function, which makes sudo print a usage message and exit ๐.)implied_shell=boolโ If the user doesn't specify a program on the command line, sudo passes the plugin the path to the user's shell and setsimplied_shell๐.intercept_ptrace=boolโ Tells you whether the system supports intercept mode usingptrace(2)๐. Right now this is only true for Linux systems that supportseccomp(2)filtering and the โtrapโ action. Other systems use a dynamic shared object to implement intercept. Only available starting with API version 1.19 โณ.intercept_setid=boolโ Tells you whether the system supports running set-user-ID and set-group-ID binaries in intercept mode ๐จ. This is currently only true for Linux systems that supportseccomp(2)filtering and the โtrapโ action. On systems that use a dynamic shared object to implement intercept, the dynamic linker (ld.soor the equivalent) disables preloading of shared objects when executing a set-user-ID or set-group-ID binary. That turns off intercept mode for that program and any other programs it runs ๐ฌ. The policy plugin may refuse to run a set-user-ID or set-group-ID binary in intercept mode to avoid this. Only available starting with API version 1.19 โณ.login_class=stringโ The BSD login class to use when setting resource limits and nice value, if specified with the-coption ๐.login_shell=boolโ Set to true if the user specified the-ioption, meaning they want to run a login shell ๐.max_groups=intโ The maximum number of groups a user may belong to ๐ฏ. This only shows up if there's a matching setting insudo.conf(5)โ๏ธ.network_addrs=listโ A space-separated list of IP network addresses and netmasks, in the form โaddr/netmaskโ, e.g., โ192.168.1.2/255.255.255.0โ ๐. The address and netmask pairs can be IPv4 or IPv6, depending on what the operating system supports. If the address contains a colon (:), it's IPv6, otherwise it's IPv4 ๐ง .noninteractive=boolโ Set to true if the user specified the-noption, which means sudo should run in non-interactive mode ๐คซ. The plugin may reject a command run in non-interactive mode if it needs user interaction ๐ โโ๏ธ.
plugin_dir=string: okay so this is the default plugin directory the sudo front-end uses ๐ It's set at compile time, so it might not match the directory the running plugin was actually loaded from ๐ A plugin can use it to find its support files โจplugin_path=string: this is the path name of the plugin sudo loaded ๐ฆ It'll be fully-qualified unless the plugin was statically compiled into sudo, so no cap, it's giving precise ๐preserve_environment=bool: set to true if the user passed the-Eoption, which means they want to keep their environment ๐ธpreserve_groups=bool: set to true if the user passed the-Poption. That means they want to keep the group vector as it is, instead of setting it based on the runas user ๐progname=string: the command name sudo was run as, usually โsudoโ or โsudoeditโ ๐prompt=string: the prompt to show when asking for a password, if one was set with the-poption ๐ซถremote_host=string: the name of the remote host to run the command on, if the user set it with the-hoption ๐ Running stuff on a remote host is meant to be handled by a helper program that gets run in place of the user's command. The sudo front-end can only run commands on the local host, so this is a whole vibe check on the plugin's side ๐ง Only available starting with API version 1.4.run_shell=bool: set to true if the user passed the-soption, meaning they want to run a shell ๐โจrunas_group=string: the group name or group-ID to run the command as, if the user set it with the-goption ๐runas_user=string: the user name or user-ID to run the command as, if the user set it with the-uoption ๐selinux_role=string: the SELinux role to use when running the command, if the-roption was used ๐ฅselinux_type=string: the SELinux type to use when running the command, if the-toption was used ๐ฅset_home=bool: set to true if the user passed the-Hoption. When that's true, theHOMEenvironment variable gets set to the target user's home directory ๐กsudoedit=bool: set to true when the-eoption is used, or when sudo was invoked as sudoedit ๐ The plugin is supposed to swap an editor intoargvinside thecheck_policy()function. If the plugin doesn't support sudoedit, it should return-2to signal a usage error instead. For more info, check thecheck_policy()section ๐timeout=string: the command timeout the user set with the-Toption โณ Not every plugin supports command timeouts, and policy might restrict whether the user can set one at all. The format of the timeout string is up to each plugin ๐ฆupdate_ticket=bool: set to false if the user passed the-Noption, which means they don't want any cached authentication credentials updated ๐ โโ๏ธ Only available starting with API version 1.20.
Additional settings may be added in the future, so the plugin should quietly ignore any settings it doesn't recognize ๐ซง
โจ user_info โจ
user_info is a vector holding info about the user running the command, written as โname=valueโ strings ๐ The vector ends with a NULL pointer, so you know when to stop ๐
When you're parsing user_info, split on the first equal sign (โ=โ) only ๐ช The name never includes one, but the value might, so splitting on the first one keeps everything in the right place ๐
sudo sets the following values:
cols=int: the number of columns the user's terminal supports ๐บ If there's no terminal device, the default is 80 ๐ธcwd=string: the user's current working directory ๐egid=gid_t: the effective group-ID of the user invoking sudo ๐ชชeuid=uid_t: the effective user-ID of the user invoking sudo ๐ชชgid=gid_t: the real group-ID of the user invoking sudo ๐groups=list: the user's supplementary group list, formatted as a string of comma-separated group-IDs ๐ฏโโ๏ธhost=string: the local machine's hostname, as returned by thegethostname(2)system call ๐ฅ๏ธlines=int: the number of lines the user's terminal supports ๐ If there's no terminal device, the default is 24 โจpgid=int: the ID of the process group that the running sudo process belongs to ๐ Only available starting with API version 1.2.pid=int: the process ID of the running sudo process ๐งท Only available starting with API version 1.2.ppid=int: the parent process ID of the running sudo process ๐ช Only available starting with API version 1.2.rlimit_as=soft,hard: the maximum size the process's address space can grow to, in bytes, if the OS supports it ๐ง The soft and hard limits are separated by a comma. โinfinityโ means there's no limit. Only available starting with API version 1.16.rlimit_core=soft,hard: the largest core dump file that can be created, in bytes ๐ฅ The soft and hard limits are separated by a comma. โinfinityโ means no limit. Only available starting with API version 1.16.rlimit_cpu=soft,hard: the maximum CPU time the process can use, in seconds โฑ๏ธ The soft and hard limits are separated by a comma. โinfinityโ means no limit. Only available starting with API version 1.16.rlimit_data=soft,hard: the maximum size of the process's data segment, in bytes ๐๏ธ The soft and hard limits are separated by a comma. โinfinityโ means no limit. Only available starting with API version 1.16.rlimit_fsize=soft,hard: the largest file the process can create, in bytes ๐ The soft and hard limits are separated by a comma. โinfinityโ means no limit. Only available starting with API version 1.16.rlimit_locks=soft,hard: the maximum number of locks the process can establish, if the OS supports it ๐ The soft and hard limits are separated by a comma. โinfinityโ means no limit. Only available starting with API version 1.16.rlimit_memlock=soft,hard: the maximum amount the process can lock in memory, in bytes, if the OS supports it ๐ง The soft and hard limits are separated by a comma. โinfinityโ means no limit. Only available starting with API version 1.16.rlimit_nofile=soft,hard: the maximum number of files the process can have open at once ๐ The soft and hard limits are separated by a comma. โinfinityโ means no limit. Only available starting with API version 1.16.rlimit_nproc=soft,hard: the maximum number of processes the user can run at the same time ๐โโ๏ธ The soft and hard limits are separated by a comma. โinfinityโ means no limit. Only available starting with API version 1.16.rlimit_rss=soft,hard: the maximum size the process's resident set size can grow to, in bytes ๐ง The soft and hard limits are separated by a comma. โinfinityโ means no limit. Only available starting with API version 1.16.rlimit_stack=soft,hard: the maximum size the process's stack can grow to, in bytes ๐ฅ The soft and hard limits are separated by a comma. โinfinityโ means no limit. Only available starting with API version 1.16.sid=int: the session ID of the running sudo process, or 0 if sudo isn't part of a POSIX job control session ๐ซง Only available starting with API version 1.2.tcpgid=int: the ID of the foreground process group tied to the terminal device that the sudo process is attached to, or 0 if there's no terminal ๐ฅ๏ธ Only available starting with API version 1.2.tty=string: Where the user's terminal device lives, if they have one. This entry only shows up when the session is attached to a terminal device. ๐ttydev=dev_t: The number of the user's terminal device, if there is one, formatted as a long long value. Same deal: it's only there when a terminal is attached to the session. Only available starting with API version 1.22. ๐ขuid=uid_t: The real user-ID of the person invoking sudo. ๐ชชumask=octal: The invoking user's file creation mask. Only available starting with API version 1.10. ๐ญuser=string: The name of the person invoking sudo. ๐โโ๏ธuser_env: The user's environment as a NULL-terminated vector ofname=valuestrings. โจ
When parsing user_env, the plugin should split on the first equal sign (=), because the name will never contain one but the value might. Trust me, values can get messy. ๐
plugin_options: Any non-comment strings right after the plugin path get passed to the plugin as arguments. They're split on whitespace and handed over as a NULL-terminated array of strings. If no arguments were given,plugin_optionsis the NULL pointer. ๐งฉ
The plugin_options parameter is only available starting with API version 1.2. A plugin must check the API version the sudo front-end specifies before touching plugin_options, or it may crash. ๐
errstr: Ifopen()returns something other than 1, the plugin can store a message describing the failure inerrstr. The sudo front-end then passes that message along to any registered audit plugins. The string inerrstrhas to stay valid until the plugin'sclose()function gets called. ๐
The errstr parameter is only available starting with API version 1.15. A plugin must check the API version the sudo front-end specifies before using errstr, or it may crash. โ ๏ธ
โจ close โจ
void (*close)(int exit_status, int error);
The close() function is called when sudo is wrapping up, shortly before it exits. Starting with API version 1.15, close() gets called whether or not a command actually ran, so plugins can clean up even when nothing got executed. ๐งน
You can't tell whether a command ran just from the arguments passed to close(). If you need to know, the plugin has to keep track of whether check_policy() returned successfully. ๐ต๏ธโโ๏ธ
The function arguments go like this:
exit_status: The command's exit status, as returned by the wait(2) system call, or zero if no command was run. The value ofexit_statusis undefined iferroris non-zero. ๐ฆerror: If the command couldn't be executed, this is set to the value oferrnoset by the execve(2) system call. The plugin is responsible for showing error info through theconversation()orsudo_plugin_printf()function. If the command ran successfully,erroris zero. ๐ โโ๏ธ
If no close() function is defined, no I/O logging plugins are loaded, and neither the timeout nor use_pty options are set in the command_info list, the sudo front-end may run the command directly instead of as a child process. ๐โโ๏ธ๐จ
๐ show_version ๐
int (*show_version)(int verbose);
The show_version() function gets called by sudo when the user passes the -V option. The plugin can display its version info to the user via conversation() or sudo_plugin_printf() using SUDO_CONV_INFO_MSG. If the user asks for detailed version info, the verbose flag will be non-zero. ๐ฃ
It returns 1 on success, 0 on failure, -1 if a general error occurred, or -2 if there was a usage error. The return value is currently ignored, though, so don't stress about it too much. ๐คทโโ๏ธ
๐ check_policy ๐
int (*check_policy)(int argc, char * const argv[], char *env_add[],
char **command_info[], char **argv_out[], char **user_env_out[],
const char **errstr);
The check_policy() function is how sudo figures out whether the user is actually allowed to run the requested commands. ๐
If the sudoedit option was enabled in the settings array passed to open(), the user has asked for sudoedit mode. Sudoedit is a way to edit one or more files where the editor runs with the user's own credentials instead of elevated privileges. Sudo pulls this off by making user-writable temporary copies of the files, then overwriting the originals with those copies once editing is done. ๐โก๏ธ๐
If the plugin supports sudoedit, it must set sudoedit=true in the command_info list. The plugin is in charge of picking the editor, possibly from an environment variable like EDITOR, and that editor goes in argv_out (environment variables may include command line options). The files being edited should get copied from argv to argv_out, separated from the editor and its arguments by a -- element. Sudo removes that -- before running the editor. The plugin can also set sudoedit_nfiles in the command_info list to the number of files being edited. Only the sudo front-end uses this, and only starting with API version 1.21. ๐๏ธ
The check_policy() function returns 1 if the command is allowed, 0 if it's not, -1 for a general error, or -2 for a usage error. It also returns -2 if sudoedit was requested but the plugin doesn't support it. In that last case, sudo prints a usage message before it exits. ๐ซ
If an error happens, the plugin can optionally call conversation() or sudo_plugin_printf() with SUDO_CONF_ERROR_MSG to show the user more detail. ๐
The function arguments are:
argc: The number of elements inargv, not counting the final NULL pointer. ๐ขargv: The argument vector describing the command the user wants to run, in the same form you'd pass to the execve(2) system call. The vector ends with a NULL pointer. ๐งพenv_add: Extra environment variables the user specified on the command line, as a NULL-terminated vector ofname=valuestrings. The plugin can reject the command if one or more variables aren't allowed to be set, or it can quietly ignore them. ๐ฑ
When parsing env_add, the plugin should split on the first equal sign (=), since the name will never contain one but the value might. ๐
command_info: Info about the command being run, asname=valuestrings. Sudo uses these to set up the execution environment. The plugin is responsible for creating and filling this vector, which must end with a NULL pointer. These are the values sudo recognizes: ๐apparmor_profile=string: The AppArmor profile to switch to when executing the command. Only available starting with API version 1.19. ๐ก๏ธchroot=string: The root directory to use when running the command. ๐ณclosefrom=number: If specified, sudo will close all file descriptors with a value ofnumberor higher. ๐ชcommand=string: The fully qualified path to the command to be executed. ๐บ๏ธcwd=string: The working directory to change to when executing the command. If sudo can't change to that directory, the command won't run unlesscwd_optionalis also set (see below). ๐cwd_optional=bool: If set, failing to change to the new working directory is treated as a non-fatal error. This does nothing unlesscwdis also set. ๐คexec_background=bool: By default, sudo runs a command as the foreground process as long as sudo itself is in the foreground. Whenexec_backgroundis enabled and the command runs in a pseudo-terminal (because of I/O logging or theuse_ptysetting), the command runs as a background process instead. If it tries to read from the controlling terminal (or change terminal settings), it gets suspended with the SIGTTIN signal (or SIGTTOU for terminal settings). If that happens while sudo is in the foreground, the command gets the controlling terminal and resumes in the foreground with no user action needed. The upside of starting in the background is that sudo doesn't have to read from the terminal unless the command asks for it. Otherwise, all terminal input has to be passed along to the command, whether it wanted it or not, since the kernel buffers terminals and there's no way to tell if the command really wants the input. This is different from historic sudo behavior, and from what happens when the command isn't running in a pseudo-terminal. ๐งโโ๏ธ๐
For this to work seamlessly, the operating system has to support automatic restarting of system calls. Not all operating systems do this by default, and even the ones that do can have bugs, bestie ๐. For example, macOS fails to restart the tcgetattr() and tcsetattr() system calls (this is a bug in macOS ๐). Also, this behavior depends on the command stopping with the SIGTTIN or SIGTTOU signals, so programs that catch those signals and suspend themselves with a different signal (usually SIGTOP) won't get automatically foregrounded. Some versions of the linux su(1) command act like this ๐ญ. Because of all that, a plugin should not set exec_background unless the administrator has explicitly enabled it, and there should be a way to enable or disable it on a per-command basis ๐ฅ.
This setting has no effect unless I/O logging is enabled or use_pty is enabled.
execfd=numberโ If you set this, sudo will use thefexecve(2)system call to run the command instead ofexecve(2)โจ. The number you give has to point to an open file descriptor, no exceptions ๐ .intercept=boolโ If this is set, sudo will catch any attempt to run a subsequent command and do a policy check through the policy plugin'scheck_policy()function to see whether that command is allowed ๐. It can help stop shell escapes on supported platforms, but it comes with a bunch of limitations, so check out Preventing shell escapes insudoers(5)for the tea โ. Only available starting with API version 1.18 ๐ธ.intercept_verify=boolโ If this is set, sudo will try to verify that a command running in intercept mode has the expected path name, command line arguments, and environment ๐ง. This does nothing unlessuse_ptraceis also enabled. Only available starting with API version 1.20 ๐ฆ.iolog_compress=boolโ Set this to true if the I/O logging plugins (if you have any) should compress the log data ๐๏ธ. It's just a hint, though, so the I/O logging plugin is free to ignore it ๐คทโโ๏ธ.iolog_group=stringโ The group that will own newly created I/O log files and directories ๐ซถ. Again, just a hint that the I/O logging plugin can ignore.iolog_mode=octalโ The file permission mode to use when creating I/O log files and directories ๐. Hint-level energy, the I/O logging plugin may skip it.iolog_user=stringโ The user that will own newly created I/O log files and directories ๐. Also just a hint that the I/O logging plugin might ignore.iolog_path=stringโ The fully qualified path to the file or directory where the I/O log gets stored ๐. It's a hint, so the I/O logging plugin can ignore it. And if no I/O logging plugin is loaded, this setting does absolutely nothing ๐.iolog_stdin=boolโ Set this to true if the I/O logging plugins (if any) should log standard input when it isn't connected to a terminal device ๐ฅ. Hint, not a guarantee ๐.iolog_stdout=boolโ Set this to true if the I/O logging plugins (if any) should log standard output when it isn't connected to a terminal device ๐ค. Hint vibes only ๐.iolog_stderr=boolโ Set this to true if the I/O logging plugins (if any) should log standard error when it isn't connected to a terminal device ๐จ. Also just a hint ๐ซ .iolog_ttyin=boolโ Set this to true if the I/O logging plugins (if any) should log all terminal input โจ๏ธ. This only covers input the user actually typed, not stuff coming from a pipe or redirected from a file ๐ โโ๏ธ. Hint, and the plugin may ignore it.iolog_ttyout=boolโ Set this to true if the I/O logging plugins (if any) should log all terminal output ๐ฅ๏ธ. This only covers output going to the screen, not output going to a pipe or file ๐. Hint, which the plugin may ignore.login_class=stringโ The BSD login class to use when setting resource limits and the nice value ๐. This one is optional, and it's only set on systems that support login classes ๐ .nice=intโ The nice value (priority) to use when running the command โญ. If you specify it, it overrides the priority that comes withlogin_classon BSD systems ๐.log_subcmds=boolโ If this is set, sudo will call the audit plugin'saccept()function to log when the command runs a subsequent command, as long as the system supports it ๐. Ifinterceptis also specified,log_subcmdsgets ignored, so no double dipping ๐. See Preventing shell escapes insudoers(5)for more info. Only available starting with API version 1.18 ๐.noexec=boolโ If this is set, the command isn't allowed to run other programs ๐ซ. Super strict, no cap ๐.preserve_fds=listโ A comma-separated list of file descriptors that should stick around, no matter what theclosefromsetting says ๐ง. Only available starting with API version 1.5 ๐ฆ.preserve_groups=boolโ If this is set, sudo keeps the user's group vector instead of rebuilding it fromrunas_user๐ฏโโ๏ธ. Main character energy for the group list ๐.rlimit_as=soft,hardโ The maximum size the process's address space is allowed to grow to (in bytes), if the operating system supports it ๐. The soft and hard limits are separated by a comma, and if you only give one value, both the hard and soft limits get set ๐ซถ.infinitymeans there's no limit at all โพ๏ธ,userkeeps the invoking user's resource limit as is ๐, anddefaultuses the target user's default resource limit on systems that allow per-user resource limits to be configured ๐ธ. Only available starting with API version 1.18 โจ.rlimit_core=soft,hardโ The largest core dump file that may be created (in bytes) ๐ฅ. Same comma rules: one value sets both soft and hard limits ๐.infinitymeans no limit,userpreserves the invoking user's resource limit, anddefaultuses the target user's default resource limit on systems that allow per-user resource limits ๐ . Only available starting with API version 1.18 ๐ฆ.rlimit_cpu=soft,hardโ The maximum amount of CPU time the process may use (in seconds) โฑ๏ธ. Soft and hard are separated by a comma, and one value sets both ๐ซถ.infinityis no limit โพ๏ธ,userkeeps the invoking user's resource limit ๐, anddefaultuses the target user's default resource limit on systems that allow per-user resource limits to be configured ๐. Only available starting with API version 1.18 ๐.rlimit_data=soft,hardโ The maximum size of the data segment for the process (in bytes) ๐๏ธ. Soft and hard limits are separated by a comma, and a single value sets both ๐.infinitymeans no limit,userpreserves the invoking user's resource limit, anddefaultuses the target user's default resource limit on systems that allow per-user resource limits to be configured ๐. Only available starting with API version 1.18 โจ.rlimit_fsize=soft,hardโ The largest file the process is allowed to create (in bytes) ๐. Comma-separated soft and hard limits, and one value sets both ๐.infinityis no limit โพ๏ธ,userkeeps the invoking user's resource limit ๐ซถ, anddefaultuses the target user's default resource limit on systems that allow per-user resource limits to be configured ๐ธ. Only available starting with API version 1.18 ๐.rlimit_locks=soft,hardโ ๐ The max number of locks the process can grab, if the OS supports it. The soft and hard limits go in a comma-separated pair. Pass just one value and it sets both.infinitymeans no limit at all,userkeeps the invoking user's resource limit as it is, anddefaultuses the target user's default resource limit on systems that let you configure per-user limits. Only available starting with API version 1.18.rlimit_memlock=soft,hardโ ๐ง The max amount of memory the process can lock, in bytes, if the OS supports it. Soft and hard limits are comma-separated, and a single value sets both.infinitymeans no limit,userpreserves the invoking user's limit, anddefaultuses the target user's default on systems with per-user limits. Only available starting with API version 1.18.rlimit_nofile=soft,hardโ ๐ The max number of files the process can have open. Soft and hard limits are comma-separated, and one value sets both.infinitymeans no limit,userkeeps the invoking user's limit, anddefaultuses the target user's default on systems that allow per-user limits. Only available starting with API version 1.18.rlimit_nproc=soft,hardโ ๐โโ๏ธ The max number of processes the user can run at the same time. Soft and hard limits are comma-separated, and one value sets both.infinitymeans no limit,userkeeps the invoking user's limit, anddefaultuses the target user's default where per-user limits can be configured. Only available starting with API version 1.18.rlimit_rss=soft,hardโ ๐ The biggest size the process's resident set size can grow to, in bytes. Soft and hard limits are comma-separated, and one value sets both.infinitymeans no limit,userkeeps the invoking user's limit, anddefaultuses the target user's default on systems with per-user limits. Only available starting with API version 1.18.rlimit_stack=soft,hardโ ๐ฅ The biggest size the process's stack can grow to, in bytes. Soft and hard limits are comma-separated, and one value sets both.infinitymeans no limit,userkeeps the invoking user's limit, anddefaultuses the target user's default on systems with per-user limits. Only available starting with API version 1.18.runas_egid=gidโ ๐ The effective group-ID the command runs as. If you don't set it, the value ofrunas_gidgets used instead.runas_euid=uidโ ๐ The effective user-ID the command runs as. If you don't set it, the value ofrunas_uidgets used instead.runas_gid=gidโ ๐ซถ The group-ID the command runs as.runas_group=stringโ ๐ท๏ธ The name of the group the command runs as, if it's different from therunas_user's default group. This is just for auditing, though. The sudo front-end usesrunas_egidandrunas_gidwhen it actually executes the command.runas_groups=listโ ๐งบ The supplementary group vector for the command, written as a comma-separated list of group-IDs. Ifpreserve_groupsis set, this one gets ignored.runas_uid=uidโ ๐ซถ The user-ID the command runs as.runas_user=stringโ ๐ท๏ธ The name of the user the command runs as. It should matchrunas_euid(orrunas_uidifrunas_euidisn't set). Again, this is only for auditing. The sudo front-end actually usesrunas_euidandrunas_uidwhen executing the command.selinux_role=stringโ ๐ก๏ธ The SELinux role to use when executing the command.selinux_type=stringโ ๐ก๏ธ The SELinux type to use when executing the command.set_utmp=boolโ ๐ Creates a utmp (or utmpx) entry when a pseudo-terminal gets allocated. By default the new entry is a copy of the user's existing utmp entry (if they have one), with the tty, time, type, and pid fields updated.sudoedit=boolโ โ๏ธ Set this to true when you're in sudoedit mode. The plugin can turn on sudoedit mode even if sudo wasn't invoked as sudoedit. That lets the plugin do command substitution and quietly switch on sudoedit whenever someone tries to run an editor.sudoedit_checkdir=boolโ ๐ง Set this to false to turn off directory writability checks in sudoedit. By default, sudoedit 1.8.16 and higher checks every directory component of the path being edited for writability by the invoking user. Symbolic links won't be followed inside writable directories, and sudoedit will refuse to edit a file that lives in a writable directory. These restrictions aren't enforced when sudoedit runs as root. Thesudoedit_checkdiroption can be set to false to skip this check. Only available starting with API version 1.8.sudoedit_follow=boolโ ๐ Set this to true to let sudoedit edit files that are symbolic links. By default, sudoedit 1.8.15 and higher refuses to open a symbolic link. Thesudoedit_followoption brings back the older behavior and lets sudoedit open symbolic links. Only available starting with API version 1.8.sudoedit_nfiles=numberโ ๐ข The number of files the user is going to edit. If it's present, the sudo front-end uses it to figure out which elements of theargv_outvector are the files to be edited. Theโ--โelement has to sit directly before the first file to be edited. Ifsudoedit_nfilesisn't specified, the sudo front-end looks at where theโ--โelement is to find where the file list starts. Only available starting with API version 1.21.timeout=intโ โฐ The command timeout. If it's non-zero, the command gets killed once the timeout runs out.umask=octalโ ๐ The file creation mask to use when executing the command. On some systems PAM or login.conf can override this value, unless theumask_overrideoption is also set.umask_override=boolโ ๐ช Forces the value set by theumaskoption to override any umask that PAM or login.conf sets.use_ptrace=boolโ ๐ If set, sudo usesptrace(2)to run intercept mode, as long as the system supports it. This setting does nothing unlessinterceptis also set. Only available starting with API version 1.19.use_pty=boolโ ๐ฅ๏ธ Allocates a pseudo-terminal for the command to run in, no matter whether I/O logging is on or off. By default, sudo only uses a pseudo-terminal when an I/O log plugin is loaded.utmp_user=stringโ ๐งพ The user name to put in a new utmp (or utmpx) entry whenset_utmpis enabled. This lets you set the user field in the utmp entry to the user the command runs as, instead of the invoking user. If it isn't set, sudo bases the new entry on the invoking user's existing entry.
Unsupported values get ignored, so don't stress if you slip one in ๐
argv_outโ ๐ฏ The NULL-terminated argument vector to pass to theexecve(2)system call when executing the command. The plugin is responsible for allocating and populating this vector.
okay so picking up right where the user_env_out bit left off ๐
user_env_outโ this is the NULL-terminated environment vector used when running the command. The plugin is in charge of allocating it and filling it in ๐ธerrstrโ ifcheck_policy()returns something other than1, the plugin can leave a message about the failure or error inerrstr. The sudo front-end then hands that to any registered audit plugins ๐ฆ The string has to stay valid until the plugin'sclose()function gets called, so don't let it vanish on me ๐ญ
The errstr parameter only exists starting with API version 1.15. Before you touch it, check the API version the sudo front-end gives you. Skip that check and you could crash ๐
๐ง list
int (*list)(int argc, char * const argv[], int verbose,
const char *user, const char **errstr);
this is how you list the privileges available to the invoking user โจ It returns 1 on success, 0 on failure, and -1 on error. If something goes wrong, the plugin can optionally call conversation() or sudo_plugin_printf() with SUDO_CONF_ERROR_MSG to show the user some extra error info ๐
The privileges themselves should get printed through conversation() or sudo_plugin_printf() using SUDO_CONV_INFO_MSG ๐
The function arguments go like this:
argcโ how many elements are inargv, not counting the final NULL pointer ๐argvโ if it's not NULL, this is an argument vector describing a command the user wants to check against the policy. It has the same form you'd pass to theexecve(2)system call. If the policy allows the command, the fully-qualified path to it should show up along with any command line arguments ๐ฅverboseโ a flag that says whether to list in verbose mode or not ๐userโ the name of a different user to list privileges for, but only if the policy lets you do that. If it's NULL, the plugin should list the privileges of the invoking user ๐errstrโ iflist()returns something other than1, the plugin can store a message about the failure or error here. The sudo front-end passes it along to any registered audit plugins. It has to stay valid until the plugin'sclose()function is called โจ
Same deal as before: errstr is only available starting with API version 1.15, so check the API version the front-end specifies before using it or you might crash ๐ญ
๐ฆ validate
int (*validate)(const char **errstr);
the validate() function runs when sudo gets called with the -v option. For policy plugins like sudoers that cache authentication credentials, this is where the credentials get validated and cached ๐ซถ
The validate() function should be NULL if the plugin doesn't support credential caching.
It returns 1 on success, 0 on failure, and -1 on error. On error, the plugin can optionally call conversation() or sudo_plugin_printf() with SUDO_CONF_ERROR_MSG to show the user more error info ๐
The function arguments:
errstrโ ifvalidate()returns something other than1, the plugin can store a message about the failure or error here. The sudo front-end passes it to any registered audit plugins. It has to stay valid until the plugin'sclose()function is called ๐
errstr only exists starting with API version 1.15. Check the API version from the front-end first, or it could crash ๐
๐ invalidate
void (*invalidate)(int rmcred);
the invalidate() function runs when sudo is called with -k or -K. For policy plugins like sudoers that cache credentials, this one invalidates them ๐ง If the rmcred flag is non-zero, the plugin can go one step further and actually remove the credentials instead of just invalidating them ๐๏ธ
The invalidate() function should be NULL if the plugin doesn't support credential caching.
๐ init_session
int (*init_session)(struct passwd *pwd, char **user_env[],
const char **errstr);
init_session() gets called before sudo sets up the execution environment for the command. It runs in the parent sudo process before any user-ID or group-ID changes happen โจ This is handy for session setup that command_info doesn't cover, like opening the PAM session ๐ The close() function can tear down whatever session init_session() opened ๐งน
It returns 1 on success, 0 on failure, and -1 on error. On error, the plugin can optionally call conversation() or sudo_plugin_printf() with SUDO_CONF_ERROR_MSG to present more error info to the user ๐
The function arguments:
pwdโ if the user-ID the command will run as is in the password database,pwddescribes that user. Otherwise it's NULL ๐user_env_outโ the NULL-terminated environment vector to use when running the command. This is the same thing that goes back to the front-end through the Policy Plugin'suser_env_outparameter. Ifinit_session()needs to change the user environment, it should update the pointer stored inuser_env_out. The expected use case is merging the PAM environment (if there is one) with the contents ofuser_env_out๐ธ This parameter is only available starting with API version 1.2, so check the API version from the front-end before using it or you might crash ๐ญerrstrโ ifinit_session()returns something other than1, the plugin can store a message about the failure or error inerrstr. The front-end passes it to any registered audit plugins. The string has to remain valid until the plugin'sclose()function is called ๐ฆ
errstr only exists starting with API version 1.15, so verify the API version before using it ๐
๐ช register_hooks
void (*register_hooks)(int version,
int (*register_hook)(struct sudo_hook *hook));
the sudo front-end calls register_hooks() to register whatever hooks the plugin needs ๐ If the plugin doesn't support hooks, register_hooks should be set to the NULL pointer.
The version argument tells you which version of the hooks API the sudo front-end supports ๐
The register_hook() function is what you use to register any supported hooks the plugin needs. It returns 0 on success, 1 if the hook type isn't supported, and -1 if the major version in struct sudo_hook doesn't match the front-end's major hook API version ๐
โโ๏ธ
register_hooks()is only available starting with API version 1.2. If the sudo front-end is older than API version 1.2,register_hooks()won't get called at all.
See the โHook function APIโ section below for more info about hooks โจ
๐งฝ deregister_hooks
void (*deregister_hooks)(int version,
int (*deregister_hook)(struct sudo_hook *hook));
the sudo front-end calls deregister_hooks() to deregister any hooks the plugin has registered ๐งน If the plugin doesn't support hooks, deregister_hooks should be set to the NULL pointer.
The version argument describes the version of the hooks API the sudo front-end supports ๐
The deregister_hook() function should be used to deregister any hooks that were set up by register_hook(). If the plugin tries to deregister a hook the front-end doesn't support, deregister_hook() returns an error ๐
See the โHook function APIโ section below for more info about hooks โจ
deregister_hooks()is only available starting with API version 1.2. If the sudo front-end is older than API version 1.2,deregister_hooks()won't get called.
๐ช event_alloc
struct sudo_plugin_event * (*event_alloc)(void);
the event_alloc() function is used to allocate a struct sudo_plugin_event, which gives you access to the main sudo event loop ๐ Unlike the other fields, the event_alloc pointer is filled in by the sudo front-end, not by the plugin ๐โโ๏ธ
See the โEvent APIโ section below for more info about events โจ
event_alloc()is only available starting with API version 1.15. If the sudo front-end doesn't support API version 1.15 or higher,event_allocwon't be set.
๐ Policy Plugin Version Macros
These are the API version macros, so you know exactly which version you're working with ๐ง
/* Plugin API version major/minor. */
#define SUDO_API_VERSION_MAJOR 1
#define SUDO_API_VERSION_MINOR 13
#define SUDO_API_MKVERSION(x, y) ((x << 16) | y)
#define SUDO_API_VERSION SUDO_API_MKVERSION(SUDO_API_VERSION_MAJOR,\
SUDO_API_VERSION_MINOR)
/* Getters and setters for API version */
#define SUDO_API_VERSION_GET_MAJOR(v) ((v) >> 16)
#define SUDO_API_VERSION_GET_MINOR(v) ((v) & 0xffff)
#define SUDO_API_VERSION_SET_MAJOR(vp, n) do { \
*(vp) = (*(vp) & 0x0000ffff) | ((n) << 16); \
} while(0)
#define SUDO_API_VERSION_SET_MINOR(vp, n) do { \
*(vp) = (*(vp) & 0xffff0000) | (n); \
} while(0)
โจ I/O plugin API โจ
struct io_plugin {
#define SUDO_IO_PLUGIN 2
unsigned int type; /* always SUDO_IO_PLUGIN */
unsigned int version; /* always SUDO_API_VERSION */
int (*open)(unsigned int version, sudo_conv_t conversation,
sudo_printf_t sudo_plugin_printf, char * const settings[],
char * const user_info[], char * const command_info[],
int argc, char * const argv[], char * const user_env[],
char * const plugin_options[], const char **errstr);
void (*close)(int exit_status, int error); /* wait status or error */
int (*show_version)(int verbose);
int (*log_ttyin)(const char *buf, unsigned int len,
const char **errstr);
int (*log_ttyout)(const char *buf, unsigned int len,
const char **errstr);
int (*log_stdin)(const char *buf, unsigned int len,
const char **errstr);
int (*log_stdout)(const char *buf, unsigned int len,
const char **errstr);
int (*log_stderr)(const char *buf, unsigned int len,
const char **errstr);
void (*register_hooks)(int version,
int (*register_hook)(struct sudo_hook *hook));
void (*deregister_hooks)(int version,
int (*deregister_hook)(struct sudo_hook *hook));
int (*change_winsize)(unsigned int lines, unsigned int cols,
const char **errstr);
int (*log_suspend)(int signo, const char **errstr);
struct sudo_plugin_event * (*event_alloc)(void);
};
Okay so when an I/O plugin gets loaded, sudo runs the command inside a pseudo-terminal ๐ปโจ This is what makes it possible to log the input and output from the user's session, bestie ๐ If any of standard input, standard output, or standard error don't correspond to a tty, sudo opens a pipe to capture the I/O for logging before passing it on ๐ฆ
The log_ttyin() function gets the raw user input straight from the terminal device ๐ณ This includes input even when echo is disabled, like when a password is being read ๐ The log_ttyout() function gets output from the pseudo-terminal that's suitable for replaying the user's session later on ๐ฌ The log_stdin(), log_stdout(), and log_stderr() functions are only called if standard input, standard output, or standard error respectively correspond to something other than a tty ๐ธ
Any of the logging functions can be set to the NULL pointer if no logging is to be performed ๐
If the open function returns 0, no I/O will be sent to the plugin ๐ซถ
If a logging function returns an error (-1), the running command gets terminated and all of the plugin's logging functions get disabled ๐ญ Other I/O logging plugins will still receive any remaining input or output that hasn't been processed yet.
If an input logging function rejects the data by returning 0, the command gets terminated and the data is not passed to the command ๐ It'll still be sent to any other I/O logging plugins though. If an output logging function rejects the data by returning 0, the command gets terminated and the data is not written to the terminal ๐ Again, it'll still be sent to any other I/O logging plugins.
A struct audit_plugin has the following fields ๐
typeโ Thetypefield should always be set toSUDO_IO_PLUGIN.versionโ Theversionfield should be set toSUDO_API_VERSION. This lets sudo figure out which API version the plugin was built against ๐ง
โจ open โจ
int (*open)(unsigned int version, sudo_conv_t conversation,
sudo_printf_t sudo_plugin_printf, char * const settings[],
char * const user_info[], char * const command_info[],
int argc, char * const argv[], char * const user_env[],
char * const plugin_options[]);
The open() function runs before log_ttyin(), log_ttyout(), log_stdin(), log_stdout(), log_stderr(), log_suspend(), change_winsize(), or show_version() get called ๐ It's only called if the version is being requested or if the policy plugin's check_policy() function has returned successfully. It returns 1 on success, 0 on failure, -1 if a general error occurred, or -2 if there was a usage error. In that last case, sudo prints a usage message before it exits ๐ If an error happens, the plugin may optionally call the conversation() or sudo_plugin_printf() function with SUDO_CONF_ERROR_MSG to show the user some extra error info ๐ฅ
Here are the function arguments, lowkey the main event:
versionโ The version sudo passes in lets the plugin figure out the major and minor version number of the plugin API sudo supports ๐ขconversationโ A pointer to theconversation()function. The plugin can use it to show version info fromshow_version()(more on that below) ๐ It can also be used to display extra error messages to the user. Theconversation()function returns 0 on success and -1 on failure.sudo_plugin_printfโ A pointer to aprintf()-style function that theshow_version()function can use to display version info (seeshow_versionbelow). Thesudo_plugin_printf()function can also be used to show extra error messages to the user. It returns the number of characters printed on success and -1 on failure โจsettingsโ A vector of user-supplied sudo settings in the form of โname=valueโ strings. The vector is terminated by a NULL pointer. These settings match options the user specified when running sudo, so they'll only be there when the matching option was given on the command line ๐
When parsing settings, the plugin should split on the first equal sign (=), since the name field will never include one itself but the value might ๐ง
Check out the โPolicy plugin APIโ section for a list of every possible setting ๐
user_infoโ A vector of info about the user running the command, in the form of โname=valueโ strings. The vector is terminated by a NULL pointer ๐ซถ
When parsing user_info, split on the first equal sign (=), since the name field will never include one itself but the value might.
See the โPolicy plugin APIโ section for a list of all possible strings.
command_infoโ A vector of info describing the command being run, in the form of โname=valueโ strings. The vector is terminated by a NULL pointer ๐
When parsing command_info, split on the first equal sign (=), since the name field will never include one itself but the value might.
See the โPolicy plugin APIโ section for a list of all possible strings.
argcโ The number of elements inargv, not counting the final NULL pointer. It can be zero, like when sudo is called with the-Voption ๐argvโ If non-NULL, this is an argument vector describing a command the user wants to run, in the same form that would be passed to theexecve(2)system call ๐ฆuser_envโ The user's environment, in the form of a NULL-terminated vector of โname=valueโ strings ๐ธ
When parsing user_env, split on the first equal sign (=), since the name field will never include one itself but the value might.
plugin_optionsโ Any (non-comment) strings immediately after the plugin path are treated as arguments to the plugin. These arguments get split on a white space boundary and passed to the plugin as a NULL-terminated array of strings. If no arguments were specified,plugin_optionswill be the NULL pointer ๐ง
The plugin_options parameter is only available starting with API version 1.2. A plugin must check the API version specified by the sudo front-end before using plugin_options. Failure to do so may result in a crash ๐
errstrโ If theopen()function returns a value other than 1, the plugin may store a message describing the failure or error inerrstr. The sudo front-end will then pass this value to any registered audit plugins ๐ฅ The string stored inerrstrmust stay valid until the plugin'sclose()function is called.
The errstr parameter is only available starting with API version 1.15. A plugin must check the API version specified by the sudo front-end before using errstr. Failure to do so may result in a crash ๐ญ
โจ close โจ
void (*close)(int exit_status, int error);
The close() function is called when sudo is finished, shortly before it exits ๐
Here are the function arguments:
exit_statusโ The command's exit status, as returned by thewait(2)system call, or zero if no command was run. The value ofexit_statusis undefined iferroris non-zero โ ๏ธerrorโ If the command couldn't be executed, this is set to the value oferrnoset by theexecve(2)system call. If the command ran successfully, the value oferroris zero โจ
โจ show_version โจ
int (*show_version)(int verbose);
The show_version() function is called by sudo when the user specifies the -V option ๐ The plugin can display its version info to the user via the conversation() or sudo_plugin_printf() function, using SUDO_CONV_INFO_MSG ๐
If the user asks for detailed version info, the verbose flag will be non-zero ๐
It returns 1 on success, 0 on failure, -1 if a general error occurred, or -2 if there was a usage error. Heads up, though: the return value is currently ignored ๐
โจ log_ttyin โจ
int (*log_ttyin)(const char *buf, unsigned int len,
const char **errstr);
๐ log_ttyin ๐
int (*log_ttyin)(const char *buf, unsigned int len,
const char **errstr);
okay so log_ttyin() gets called every time data can be read from the user, but before it gets passed to the running command ๐ This lets the plugin reject the data if it wants to (like if the input has banned content ๐
โโ๏ธ). It returns 1 if the data is good to go to the command, 0 if the data gets rejected (which will terminate the running command ๐), or -1 if something went wrong.
The function arguments are as follows:
bufโ the buffer holding the user's input ๐lenโ the length ofbufin bytes ๐errstrโ iflog_ttyin()returns anything other than1, the plugin can store a message explaining the failure or error here ๐ The sudo front-end then hands that message to any registered audit plugins. The string stored inerrstrhas to stay valid until the plugin'sclose()function gets called, so don't pull the rug out from under it ๐ซถ
The errstr parameter is only available starting with API version 1.15. A plugin has to check the API version the sudo front-end reports before touching errstr.
โ ๏ธ bestie, seriously, check the API version first. Skipping that step may result in a crash ๐ญ
๐ฅ๏ธ log_ttyout ๐ฅ๏ธ
int (*log_ttyout)(const char *buf, unsigned int len,
const char **errstr);
so log_ttyout() is the mirror of the one above ๐ช It gets called whenever data can be read from the command, but before it's written to the user's terminal. Same deal: the plugin can reject the output if it chooses to (say, the output has banned content ๐ซ). It returns 1 if the data should be passed to the user, 0 if the data is rejected (which will terminate the running command ๐ฅ), or -1 if an error occurred.
The function arguments are as follows:
bufโ the buffer holding the command's output ๐งพlenโ the length ofbufin bytes ๐errstrโ iflog_ttyout()returns something other than1, the plugin may store a message describing the failure or error inerrstr๐ The sudo front-end passes that along to any registered audit plugins. The string has to stay valid until the plugin'sclose()function is called ๐ซถ
The errstr parameter only exists starting with API version 1.15, so check the API version given by the sudo front-end before using it.
โ ๏ธ same energy as before: skipping the API version check may result in a crash ๐
๐ฅ log_stdin ๐ฅ
int (*log_stdin)(const char *buf, unsigned int len,
const char **errstr);
the log_stdin() function is only used if standard input doesn't correspond to a tty device ๐ง When it's in play, it gets called whenever data can be read from standard input but before it's passed to the running command. The plugin can reject data if it wants to (you know, for banned content and whatnot ๐
โโ๏ธ). It returns 1 if the data should be passed to the command, 0 if the data is rejected (which will terminate the running command ๐), or -1 if an error occurred.
The function arguments are as follows:
bufโ the buffer holding the user input ๐lenโ the length ofbufin bytes ๐errstrโ iflog_stdin()returns a value other than1, the plugin may store a message describing the failure or error here ๐ The sudo front-end passes it to any registered audit plugins. The string stored inerrstrmust remain valid until the plugin'sclose()function is called ๐ซถ
The errstr parameter is only available starting with API version 1.15. A plugin must check the API version specified by the sudo front-end before using it, or it may crash ๐ญ
๐ค log_stdout ๐ค
int (*log_stdout)(const char *buf, unsigned int len,
const char **errstr);
so log_stdout() is only used when standard output doesn't correspond to a tty device ๐ฏ It gets called whenever data can be read from the command, but before it's written to standard output. The plugin can reject the data if it chooses to (like if the output contains banned content ๐ซ). It returns 1 if the data should be passed to the user, 0 if the data is rejected (which will terminate the running command ๐ฅ), or -1 if an error occurred.
The function arguments are as follows:
bufโ the buffer holding the command output ๐งพlenโ the length ofbufin bytes ๐errstrโ iflog_stdout()returns something other than1, the plugin may store a message describing the failure or error inerrstr๐ The sudo front-end hands that to any registered audit plugins. The string has to stay valid until the plugin'sclose()function is called ๐ซถ
The errstr parameter is only available starting with API version 1.15. Check the API version the sudo front-end specifies before using errstr, because failing to do so may result in a crash ๐
๐จ log_stderr ๐จ
int (*log_stderr)(const char *buf, unsigned int len,
const char **errstr);
the log_stderr() function is only used when standard error doesn't correspond to a tty device ๐ It's called whenever data can be read from the command, but before it's written to standard error. Same as always, the plugin can reject the data if it wants (for instance if the output has banned content ๐ซ). It returns 1 if the data should be passed to the user, 0 if the data is rejected (which will terminate the running command ๐ฅ), or -1 if an error occurred.
The function arguments are as follows:
bufโ the buffer holding the command output ๐งพlenโ the length ofbufin bytes ๐errstrโ iflog_stderr()returns something other than1, the plugin may store a message describing the failure or error inerrstr๐ The sudo front-end passes that to any registered audit plugins. The string stored there must remain valid until the plugin'sclose()function is called ๐ซถ
The errstr parameter is only available starting with API version 1.15. A plugin must check the API version specified by the sudo front-end before using errstr. Failure to do so may result in a crash ๐ญ
๐ช register_hooks ๐ช
see the โPolicy plugin APIโ section for the full tea on register_hooks() ๐
๐ช deregister_hooks ๐ช
same vibe here: check the โPolicy plugin APIโ section for a description of deregister_hooks() ๐
๐ change_winsize ๐
int (*change_winsize)(unsigned int lines, unsigned int cols,
const char **errstr);
so change_winsize() gets called whenever the terminal's window size changes from the initial values listed in the user_info list ๐ช It returns -1 if an error occurred, and in that case no further calls to change_winsize() will be made,
The function arguments are as follows:
linesโ the number of lines (rows) in the re-sized terminal ๐colsโ the number of columns in the re-sized terminal ๐errstrโ ifchange_winsize()returns a value other than1, the plugin may store a message describing the failure or error inerrstr๐ The sudo front-end hands that to any registered audit plugins. The string has to stay valid until the plugin'sclose()function is called ๐ซถ
The errstr parameter is only available starting with API version 1.15. A plugin must check the API version specified by the sudo front-end before using it. Failure to do so may result in a crash ๐
โธ๏ธ log_suspend โธ๏ธ
int (*log_suspend)(int signo, const char **errstr);
the log_suspend() function gets called whenever a command is suspended or resumed โฏ๏ธ Logging this is what lets you skip the chunk of time when the command was suspended during session playback, which is honestly so smart ๐ง It returns -1 if an error occurred, and in that case no further calls to log_suspend() will be made,
The function arguments are as follows:
signoโ the signal that caused the command to be suspended, orSIGCONTif the command was resumed ๐errstrโ iflog_suspend()returns a value other than1, the plugin may store a message describing the failure or error inerrstr๐ The sudo front-end passes it to any registered audit plugins. The string stored there must remain valid until the plugin'sclose()function is called ๐ซถ
The errstr parameter is only available starting with API version 1.15. A plugin must check the API version specified by the sudo front-end before using errstr, or it may crash ๐ญ
๐ event_alloc ๐
struct sudo_plugin_event * (*event_alloc)(void);
the event_alloc() function is used to allocate a struct sudo_plugin_event, which gives you access to the main sudo event loop ๐ Unlike the other fields, the event_alloc() pointer is filled in by the sudo front-end, not by the plugin, so hands off ๐
See the โEvent APIโ section below for more information about events ๐ฆ
The event_alloc() function is only available starting with API version 1.15. If the sudo front-end doesn't support API version 1.15 or higher, event_alloc() will not be set.
๐งฉ I/O Plugin Version Macros ๐งฉ
same as the โPolicy plugin APIโ ๐
๐ Audit plugin API ๐
okay now the audit side, and these close function status types are giving very specific vibes ๐ฎ
/* Audit plugin close function status types. */
#define SUDO_PLUGIN_NO_STATUS 0
#define SUDO_PLUGIN_WAIT_STATUS 1
#define SUDO_PLUGIN_EXEC_ERROR 2
#define SUDO_PLUGIN_SUDO_ERROR 3
#define SUDO_AUDIT_PLUGIN 3
struct audit_plugin {
unsigned int type; /* always SUDO_AUDIT_PLUGIN */
unsigned int version; /* always SUDO_API_VERSION */
int (*open)(unsigned int version, sudo_conv_t conversation,
sudo_printf_t sudo_plugin_printf, char * const settings[],
char * const user_info[], int submit_optind,
char * const submit_argv[], char * const submit_envp[],
char * const plugin_options[], const char **errstr);
void (*close)(int status_type, int status);
int (*accept)(const char *plugin_name,
unsigned int plugin_type, char * const command_info[],
char * const run_argv[], char * const run_envp[],
const char **errstr);
int (*reject)(const char *plugin_name, unsigned int plugin_type,
const char *audit_msg, char * const command_info[],
const char **errstr);
int (*error)(const char *plugin_name, unsigned int plugin_type,
const char *audit_msg, char * const command_info[],
const char **errstr);
int (*show_version)(int verbose);
void (*register_hooks)(int version,
int (*register_hook)(struct sudo_hook *hook));
void (*deregister_hooks)(int version,
int (*deregister_hook)(struct sudo_hook *hook));
struct sudo_plugin_event * (*event_alloc)(void);
}
okay so an audit plugin is basically your sudo diary ๐โจ it logs every successful AND unsuccessful attempt to run sudo, and it does this totally independently of the policy or any I/O plugins ๐
you can stack up multiple audit plugins in sudo.conf(5) if you're feeling extra ๐ซถ
A struct audit_plugin has the following fields ๐
type
The type field should always be set to SUDO_AUDIT_PLUGIN ๐
version
The version field should be set to SUDO_API_VERSION ๐ฆ
This lets sudo figure out which API version the plugin was built against ๐ฅ
open
int (*open)(unsigned int version, sudo_conv_t conversation,
sudo_printf_t sudo_plugin_printf, char * const settings[],
char * const user_info[], int submit_optind,
char * const submit_argv[], char * const submit_envp[],
char * const plugin_options[], const char **errstr);
the audit open() function runs before any other sudo plugin API functions, which is the whole point ๐ that way it can catch failures in the other plugins too ๐ญ it returns 1 on success, 0 on failure, -1 if a general error happened, or -2 if there was a usage error. for that last one, sudo prints a usage message before it exits ๐ if something goes wrong, the plugin can optionally call the conversation() or plugin_printf() function with SUDO_CONF_ERROR_MSG to show the user some extra error info ๐ธ
the function arguments are as follows:
versionโ sudo passes this in so the plugin knows the major and minor version number of the plugin API that sudo supports โจconversationโ a pointer to theconversation()function. theshow_version()function can use it to display version info (seeshow_version()below), and it can also be used to show extra error messages to the user ๐ฌconversation()returns0on success and-1on failure.plugin_printfโ a pointer to aprintf()-style function thatshow_version()can use for version info (see show_version below), and that can also show extra error messages to the user ๐จ๏ธplugin_printf()returns the number of characters printed on success and-1on failure.settingsโ a vector of user-supplied sudo settings, written as โname=valueโ strings ๐ง the vector is terminated by a NULL pointer. these settings match the options the user typed when running sudo, so they only show up when that option was actually on the command line.
when parsing settings, the plugin should split on the first equal sign (โ=โ) ๐ช the name never includes one itself, but the value might!
see the โPolicy plugin APIโ section for a list of all possible settings ๐
user_infoโ a vector of info about the user running the command, in the form of โname=valueโ strings ๐ชช it's terminated by a NULL pointer.
when parsing user_info, split on the first equal sign (โ=โ) ๐ same deal as before: the name never has one, but the value might.
see the โPolicy plugin APIโ section for a list of all possible strings ๐
submit_optindโ the index intosubmit_argvthat points at the first entry that isn't a command line option ๐งญ ifsubmit_argvonly has options in it (which can happen with the-lor-voptions), thensubmit_argv[submit_optind]evaluates to the NULL pointer ๐submit_argvโ the argument vector sudo was invoked with, options and all ๐ you can usesubmit_optindto find where the options end.submit_envpโ the invoking user's environment, as a NULL-terminated vector of โname=valueโ strings ๐
when parsing submit_envp, split on the first equal sign (โ=โ) since the name field will never include one itself but the value might.
plugin_optionsโ any (non-comment) strings sitting right after the plugin path get treated as arguments for the plugin ๐ง they're split on white space boundaries and passed along as a NULL-terminated array of strings. if no arguments were given,plugin_optionsis the NULL pointer.errstrโ ifopen()returns something other than1, the plugin can store a message describing the failure or error here ๐ the sudo front-end then passes that message along to any registered audit plugins. the string stored inerrstrhas to stay valid until the plugin'sclose()function gets called, so don't pull the rug out from under it ๐ญ
close
void (*close)(int status_type, int status);
the close() function gets called when sudo is finished, shortly before it exits ๐
the function arguments are as follows:
status_typeโ the type of status being passed. it's one ofSUDO_PLUGIN_NO_STATUS,SUDO_PLUGIN_WAIT_STATUS,SUDO_PLUGIN_EXEC_ERRORorSUDO_PLUGIN_SUDO_ERROR๐ฅstatusโ what this holds depends onstatus_type๐ค it's either ignored, or it's the command's exit status as returned by thewait(2)system call, or it's the value oferrnoset by theexecve(2)system call, or it's the value oferrnofrom an error in the sudo front-end.
accept
int (*accept)(const char *plugin_name, unsigned int plugin_type,
char * const command_info[], char * const run_argv[],
char * const run_envp[], const char **errstr);
the accept() function gets called when a command or action is accepted by a policy or approval plugin โ
the function arguments are as follows:
plugin_nameโ the name of the plugin that accepted the command, or"sudo"if it was the sudo front-end ๐ซถplugin_typeโ the type of plugin that accepted the command. it's currently eitherSUDO_POLICY_PLUGIN,SUDO_POLICY_APPROVAL, orSUDO_FRONT_END๐accept()gets called multiple times, once for each policy or approval plugin that succeeds and once for the sudo front-end. when it's called on behalf of the sudo front-end,command_infomight include info from an I/O logging plugin too ๐
typically an audit plugin only cares about the accept status from the sudo front-end or from the policy and approval plugins, not both ๐
โโ๏ธ it's possible for the policy plugin to accept a command that an approval plugin later rejects, and in that case the audit plugin's accept() and reject() functions will both get called ๐ญ
command_infoโ an optional vector of info describing the command being run, in the form of โname=valueโ strings ๐ it's terminated by a NULL pointer.
when parsing command_info, split on the first equal sign (โ=โ) since the name never includes one itself but the value might.
see the โPolicy plugin APIโ section for a list of all possible strings ๐
run_argvโ a NULL-terminated argument vector describing the command that will be run, in the same form you'd pass to theexecve(2)system call ๐โโ๏ธrun_envpโ the environment the command will be run with, as a NULL-terminated vector of โname=valueโ strings ๐ธ
when parsing run_envp, split on the first equal sign (โ=โ) since the name field will never include one itself but the value might.
errstrโ ifaccept()returns something other than1, the plugin can store a message describing the failure or error here ๐ the sudo front-end passes this value along to any registered audit plugins. the string has to stay valid until the plugin'sclose()function is called ๐ซถ
reject
int (*reject)(const char *plugin_name, unsigned int plugin_type,
const char *audit_msg, char * const command_info[],
const char **errstr);
the reject() function gets called when a command or action is rejected by a plugin ๐
โโ๏ธ the function arguments are as follows:
plugin_nameโ the name of the plugin that rejected the command ๐plugin_typeโ the kind of plugin that said "nope" to the command ๐ โโ๏ธ. Right now it's eitherSUDO_POLICY_PLUGIN,SUDO_APPROVAL_PLUGIN, orSUDO_IO_PLUGIN.
Quick reminder that reject() is different from accept() ๐
: reject() is not called on behalf of the sudo front-end, so don't go looking for it there.
audit_msgโ an optional string explaining why the plugin rejected the command ๐. If the plugin didn't give a reason,audit_msgis the NULL pointer ๐.command_infoโ an optional vector of info about the command being run, written asโname=valueโstrings ๐งพ. The vector ends with a NULL pointer.
When you're parsing command_info, split on the first equal sign (โ=โ) ๐ช. The name never has one in it, but the value might, so splitting on the first one keeps everything intact โจ.
Check the โPolicy plugin APIโ section for the full list of possible strings ๐.
errstrโ ifreject()returns something other than1, the plugin can leave a message about the failure or error inerrstr๐. The sudo front-end then hands that message to any registered audit plugins ๐. The string stored inerrstrhas to stay valid until the plugin'sclose()function gets called, so don't free it early, bestie ๐ซถ.
error
int (*error)(const char *plugin_name, unsigned int plugin_type,
const char *audit_msg, char * const command_info[],
const char **errstr);
The error() function gets called when a plugin or the sudo front-end returns an error ๐ญ. Here's what it gets:
plugin_nameโ the name of the plugin that caused the error, orโsudoโif it was the sudo front-end ๐ซฃ.plugin_typeโ the type of plugin that caused the error, orSUDO_FRONT_ENDif it was the sudo front-end ๐ง.audit_msgโ an optional string describing the plugin error ๐ฌ. If the plugin didn't provide a description,audit_msgis the NULL pointer ๐.command_infoโ an optional vector of info about the command being run, inโname=valueโform ๐งพ. It's terminated by a NULL pointer.
Same deal as before: when parsing it, split on the first โ=โ ๐ช, because the value might have one but the name never will ๐.
See the โPolicy plugin APIโ section for every possible string ๐.
errstrโ iferror()returns something other than1, the plugin may store a message about the failure or error inerrstr๐. The sudo front-end passes that along to any registered audit plugins, and the string has to stay valid until the plugin'sclose()function is called ๐ธ.
show_version
int (*show_version)(int verbose);
show_version() gets called by sudo when the user passes the -V option ๐. The plugin can show its version info to the user through the conversation() or plugin_printf() function, using SUDO_CONV_INFO_MSG โจ. If the user asks for the detailed version info, the verbose flag gets set ๐.
It returns 1 on success, 0 on failure, -1 if a general error happened, or -2 if there was a usage error ๐. Heads up: the return value is currently ignored, so don't stress about it too much ๐
.
register_hooks
See the โPolicy plugin APIโ section for a description of register_hooks() ๐ช.
deregister_hooks
See the โPolicy plugin APIโ section for a description of deregister_hooks() ๐ช.
event_alloc
struct sudo_plugin_event * (*event_alloc)(void);
event_alloc() is how you allocate a struct sudo_plugin_event, which gives you access to the main sudo event loop ๐. Unlike the other fields, the event_alloc pointer gets filled in by the sudo front-end, not by your plugin ๐
โโ๏ธ.
Check out the โEvent APIโ section below for more on events ๐.
โ ๏ธevent_alloc()only exists starting with API version1.17. If the sudo front-end doesn't support API version1.17or higher,event_alloc()won't be set at all. Lowkey a whole vibe check moment ๐ฌ.
๐ฆ Approval plugin API ๐ฆ
struct approval_plugin {
#define SUDO_APPROVAL_PLUGIN 4
unsigned int type; /* always SUDO_APPROVAL_PLUGIN */
unsigned int version; /* always SUDO_API_VERSION */
int (*open)(unsigned int version, sudo_conv_t conversation,
sudo_printf_t sudo_plugin_printf, char * const settings[],
char * const user_info[], int submit_optind,
char * const submit_argv[], char * const submit_envp[],
char * const plugin_options[], const char **errstr);
void (*close)(void);
int (*check)(char * const command_info[], char * const run_argv[],
char * const run_envp[], const char **errstr);
int (*show_version)(int verbose);
};
An approval plugin is the extra bouncer ๐ that can add more constraints after the policy plugin has already accepted a command โจ. Unlike the other plugin types, it does not stay open until the command finishes ๐
โโ๏ธ. It gets opened right before a call to check() or show_version(), then closed shortly after ๐ช. Audit plugin functions have to be called before the plugin is closed, so keep that in mind ๐. You can specify multiple approval plugins in sudo.conf(5) ๐งฉ.
A struct approval_plugin has the following fields ๐:
typeโ should always be set toSUDO_APPROVAL_PLUGIN๐ท๏ธ.versionโ should be set toSUDO_API_VERSION๐ข. This lets sudo figure out which API version the plugin was built against ๐ง .
open
int (*open)(unsigned int version, sudo_conv_t conversation,
sudo_printf_t sudo_plugin_printf, char * const settings[],
char * const user_info[], int submit_optind,
char * const submit_argv[], char * const submit_envp[],
char * const plugin_options[], const char **errstr);
The approval open() function runs right before the plugin's check() or show_version() functions ๐โโ๏ธ. It only gets called if the version is being requested, or if the policy plugin's check_policy() function has returned successfully โ
.
It returns 1 on success, 0 on failure, -1 if a general error happened, or -2 if there was a usage error ๐ซ. In that last case, sudo prints a usage message before it exits ๐ฃ. If something goes wrong, the plugin can optionally call the conversation() or plugin_printf() function with SUDO_CONF_ERROR_MSG to show the user more error info ๐โโ๏ธ.
Here are the function arguments ๐:
versionโ the version sudo passes in lets the plugin figure out the major and minor version number of the plugin API sudo supports ๐ข.conversationโ a pointer to theconversation()function, which the plugin can use to chat with the user ๐ฌ. See the โConversation APIโ for details. It returns0on success and-1on failure.plugin_printfโ a pointer to aprintf()-style function for showing informational or error messages ๐ข. See the โConversation APIโ for details. It returns the number of characters printed on success and-1on failure.settingsโ a vector of user-supplied sudo settings, written asโname=valueโstrings โ๏ธ. It's terminated by a NULL pointer. These settings match the options the user passed when running sudo, so they only show up when the matching option was given on the command line ๐ฏ.
When parsing settings, split on the first โ=โ ๐ช, since the name never has one but the value might ๐.
Check the โPolicy plugin APIโ section for every possible setting ๐.
user_infoโ a vector of info about the user running the command, asโname=valueโstrings ๐ชช. It's terminated by a NULL pointer.
When parsing user_info, split on the first โ=โ ๐ช, same as always โจ.
See the โPolicy plugin APIโ section for every possible string ๐.
submit_optindโ the index intosubmit_argvthat points to the first entry that isn't a command line option ๐. Ifsubmit_argvonly has options in it (which can happen with the-lor-voptions), thensubmit_argv[submit_optind]evaluates to the NULL pointer ๐ซ .submit_argvโ the argument vector sudo was invoked with, including all the command line options ๐งพ. You can usesubmit_optindto find where the options end ๐ฏ.submit_envpโ the invoking user's environment, as aNULL-terminated vector ofโname=valueโstrings ๐ฟ.
When parsing submit_envp, split on the first โ=โ ๐ช. The name never has one, but the value might ๐
.
plugin_optionsโ any (non-comment) strings that show up right after the plugin path get treated as arguments for the plugin ๐ง. They're split on white space and passed to the plugin as aNULL-terminated array of strings ๐. If no arguments were specified,plugin_optionsis the NULL pointer ๐.errstrโ ifopen()returns something other than1, the plugin can store a message about the failure or error inerrstr๐. The sudo front-end passes this to any registered audit plugins ๐. The string has to stay valid until the plugin'sclose()function is called ๐ซถ.
close
void (*close)(void);
okay so close() is the last thing that happens after the approval plugin's check() or show_version() has been called ๐
It takes no arguments, and it's basically your plugin's cleanup crew ๐งน It's typically where you free up any memory objects the plugin allocated. If your plugin has nothing to clean up, you can set close() to the NULL pointer, no drama ๐ซถ
โจ check โจ
int (*check)(char * const command_info[], char * const run_argv[],
char * const run_envp[], const char **errstr);
bestie, the approval check() function runs after the policy plugin's check_policy() function and before any I/O logging plugins ๐ฅ If you've got multiple approval plugins loaded, they ALL have to succeed for the command to be allowed, no exceptions ๐ญ It returns 1 on success, 0 on failure, -1 if a general error happened, or -2 if there was a usage error. For that last one, sudo prints a usage message before it exits. If something goes wrong, the plugin can optionally call conversation() or plugin_printf() with SUDO_CONF_ERROR_MSG to give the user some extra error info ๐
Here's what the function arguments look like:
command_infoโ a vector describing the command being run, written as "name=value" strings. The vector ends with a NULL pointer. When you parse it, split on the first equal sign (=), because the name will never have one but the value might ๐ Check out the "Policy plugin API" section for a list of every possible string.run_argvโ a NULL-terminated argument vector for the command that's about to run. It's in the same form you'd pass to theexecve(2)system call ๐งrun_envpโ the environment the command will run with, as a NULL-terminated vector of "name=value" strings. Same deal here: split on the first=when parsing, since the value is the part that might have more equal signs ๐errstrโ ifopen()returned something other than1, the plugin can store a message describing the failure in here. The sudo front-end then hands that message to any registered audit plugins ๐ Just make sure the string stays valid until the plugin'sclose()function gets called, or it's giving dangling pointer energy ๐ฌ
โจ show_version โจ
int (*show_version)(int verbose);
the show_version() function gets called by sudo whenever the user passes the -V option ๐ The plugin can show its version info to the user through conversation() or plugin_printf() using SUDO_CONV_INFO_MSG. If the user wants the detailed version info, the verbose flag gets set โจ
It returns 1 on success, 0 on failure, -1 if a general error happened, or -2 if there was a usage error. Honestly though, the return value is currently ignored, so it's mostly vibes ๐คทโโ๏ธ
๐ก๏ธ Signal handlers ๐ก๏ธ
okay so the sudo front-end installs default signal handlers to catch the common signals while the plugin functions are running ๐ซง Before the command gets executed, these signals are trapped by default:
SIGALRMSIGHUPSIGINTSIGPIPESIGQUITSIGTERMSIGTSTPSIGUSR1SIGUSR2
If a fatal signal shows up before the command is executed, sudo calls the plugin's close() function with an exit status of 128 plus the value of the signal that was received ๐ This is super useful for consistent logging of commands that got killed by a signal, for plugins that log that kind of info in their close() function. The one exception is SIGPIPE, which gets ignored until the command is executed ๐
A plugin is allowed to temporarily install its own signal handlers, but it must restore the original handler before the plugin function returns. no skipping this step, bestie โ ๏ธ
๐ช Hook function API ๐ช
beginning with plugin API version 1.2, you can install hooks for certain functions the sudo front-end calls ๐ฆ
Right now, the only supported hooks are about environment variables. Hooks let you intercept attempts to get, set, or remove environment variables, so those changes get reflected in the version of the environment that's used to run a command ๐ A future version of the API will support hooking internal sudo front-end functions too, so stay tuned ๐
๐งธ Hook structure ๐งธ
Hooks in sudo are described by this structure:
typedef int (*sudo_hook_fn_t)();
struct sudo_hook {
unsigned int hook_version;
unsigned int hook_type;
sudo_hook_fn_t hook_fn;
void *closure;
};
A struct sudo_hook has the following fields:
hook_versionโ this should be set toSUDO_HOOK_VERSION.hook_typeโ this can be one of the following supported hook types:SUDO_HOOK_SETENVโ this is for the C librarysetenv(3)function. Any registered hooks run before the C library implementation. Thehook_fnfield should be a function matching this typedef:
typedef int (*sudo_hook_fn_setenv_t)(const char *name,
const char *value, int overwrite, void *closure);
if the registered hook doesn't match the typedef, the results are unspecified ๐ตโ๐ซ
SUDO_HOOK_UNSETENVโ this is for the C libraryunsetenv(3)function. Any registered hooks run before the C library implementation. Thehook_fnfield should be a function matching this typedef:
typedef int (*sudo_hook_fn_unsetenv_t)(const char *name,
void *closure);
SUDO_HOOK_GETENVโ this is for the C librarygetenv(3)function. Any registered hooks run before the C library implementation. Thehook_fnfield should be a function matching this typedef:
typedef int (*sudo_hook_fn_getenv_t)(const char *name,
char **value, void *closure);
if the registered hook doesn't match the typedef, the results are unspecified ๐ตโ๐ซ
SUDO_HOOK_PUTENVโ this is for the C libraryputenv(3)function. Any registered hooks run before the C library implementation. Thehook_fnfield should be a function matching this typedef:
typedef int (*sudo_hook_fn_putenv_t)(char *string,
void *closure);
if the registered hook doesn't match the typedef, the results are unspecified ๐ตโ๐ซ
โจ hook_fn โจ
sudo_hook_fn_t hook_fn;
the hook_fn field should be set to your plugin's hook implementation ๐ The actual function arguments depend on the hook_type (see hook_type above). In every case, the closure field of struct sudo_hook gets passed as the last function parameter. You can use this to pass whatever arbitrary data you need into your plugin's hook implementation ๐
The function return value can be one of the following:
SUDO_HOOK_RET_ERRORโ the hook function ran into an error โSUDO_HOOK_RET_NEXTโ the hook finished without error, so move on to the next hook (including the system implementation, if that applies). For example, agetenv(3)hook might returnSUDO_HOOK_RET_NEXTif the variable wasn't found in the private copy of the environment ๐SUDO_HOOK_RET_STOPโ the hook finished without error, and processing of hooks stops for this invocation. You can use this to replace the system implementation. For example, a setenv hook that works on a private copy of the environment but leavesenvironunchanged ๐ช
โ ๏ธ Care must be taken when hooking C library functions, because it is very easy to create an infinite loop ๐ญ For example, agetenv(3)hook that calls thesnprintf(3)function may create a loop if thesnprintf(3)implementation callsgetenv(3)to check the locale. To prevent this, you may wish to use a static variable in the hook function to guard against nested calls. For example:
static int in_progress = 0; /* avoid recursion */
if (in_progress)
return SUDO_HOOK_RET_NEXT;
in_progress = 1;
...
in_progress = 0;
return SUDO_HOOK_RET_STOP;
โจ Hook API Version Macros โจ
/* Hook API version major/minor */
#define SUDO_HOOK_VERSION_MAJOR 1
#define SUDO_HOOK_VERSION_MINOR 0
#define SUDO_HOOK_VERSION SUDO_API_MKVERSION(SUDO_HOOK_VERSION_MAJOR,\
SUDO_HOOK_VERSION_MINOR)
for getters and setters, see the "Policy plugin API" ๐ซถ
๐ Event API ๐
when sudo runs a command, it uses an event loop to handle signals and I/O ๐ Events can be triggered by time, by a file or socket descriptor becoming ready, or by receipt of a signal. Starting with API version 1.15, a plugin can join this event loop by calling the event_alloc() function ๐
๐งฉ Event structure ๐งฉ
Events are described by the following structures:
typedef void (*sudo_plugin_ev_callback_t)(int fd, int what, void *closure);
struct sudo_plugin_event {
int (*set)(struct sudo_plugin_event *pev, int fd, int events,
sudo_plugin_ev_callback_t callback, void *closure);
int (*add)(struct sudo_plugin_event *pev, struct timespec *timeout);
int (*del)(struct sudo_plugin_event *pev);
int (*pending)(struct sudo_plugin_event *pev, int events,
struct timespec *ts);
int (*fd)(struct sudo_plugin_event *pev);
void (*setbase)(struct sudo_plugin_event *pev, void *base);
void (*loopbreak)(struct sudo_plugin_event *pev);
void (*free)(struct sudo_plugin_event *pev);
};
A struct sudo_plugin_event contains the following function pointers:
โจ set โจ
int (*set)(struct sudo_plugin_event *pev, int fd, int events,
sudo_plugin_ev_callback_t callback, void *closure);
the set() function takes the following arguments:
struct sudo_plugin_event *pevโ a pointer to thestruct sudo_plugin_eventitself ๐ชfdโ the file or socket descriptor for I/O-based events, or the signal number for signal events. For time-based events,fdmust be-1โฐ
okay so the events field is where you decide what actually sets off your callback ๐ these are the triggers:
SUDO_PLUGIN_EV_TIMEOUTโ the callback runs once the timeout you set runs out โฐSUDO_PLUGIN_EV_READโ the callback runs when the file descriptor is readable ๐SUDO_PLUGIN_EV_WRITEโ the callback runs when the file descriptor is writable โ๏ธSUDO_PLUGIN_EV_PERSISTโ the event sticks around and stays enabled until you explicitly delete it ๐ฆSUDO_PLUGIN_EV_SIGNALโ the callback runs when the signal you specified shows up ๐ฅ
bestie, SUDO_PLUGIN_EV_PERSIST can be ORed with any of the event types ๐
you can also OR SUDO_PLUGIN_EV_READ and SUDO_PLUGIN_EV_WRITE together so the callback fires when a descriptor is ready for reading or writing โจ every other event value is mutually exclusive, so don't try to mix those ๐
โจ sudo_plugin_ev_callback_t callback โจ
typedef void (*sudo_plugin_ev_callback_t)(int fd, int what,
void *closure);
This is the function that gets called when an event is triggered ๐ the callback() function gets run with these arguments:
fdโ the file or socket descriptor for I/O-based events, or the signal number for signal events ๐งwhatโ the event type that set the callback off ๐ธ for events with multiple types (likeSUDO_PLUGIN_EV_READandSUDO_PLUGIN_EV_WRITE) or a timeout attached,whatis how you figure out why it ran ๐ต๏ธโโ๏ธclosureโ the generic pointer you passed in to theset()function ๐
โจ closure โจ
closure is a generic pointer that gets handed to your callback function ๐
The set() function returns 1 on success and -1 if an error happens ๐ฅบ
โจ add โจ
int (*add)(struct sudo_plugin_event *pev, struct timespec *timeout);
The add() function drops the event pev into sudo's event loop ๐ฆ the event has to be initialized with set() first, no skipping that step! If timeout isn't NULL, it should be a (relative) timeout, and the event fires after that time if the main criteria haven't been met โฐ this is super handy for an I/O timeout, where the event fires if a descriptor isn't ready in time โจ and if the event is already in the loop, its timeout gets updated to the new value, if there is one ๐
add() returns 1 on success and -1 if an error occurred ๐ซถ
โจ del โจ
int (*del)(struct sudo_plugin_event *pev);
del() removes the event pev from sudo's event loop ๐ the good news? Deleted events can be added back with add() whenever you want ๐
del() returns 1 on success and -1 if an error occurred ๐
โจ pending โจ
int (*pending)(struct sudo_plugin_event *pev, int events,
struct timespec *ts);
pending() lets you check whether one or more events are waiting to happen ๐ the events argument says which ones to look for, and the set() function has the full list of valid event types ๐ if SUDO_PLUGIN_EV_TIMEOUT is in events, the event has a timeout attached, and ts isn't NULL, then ts gets filled in with the time that's left โณ
โจ fd โจ
int (*fd)(struct sudo_plugin_event *pev);
fd() gives you back the descriptor or signal number tied to the event pev ๐ง
โจ setbase โจ
void (*setbase)(struct sudo_plugin_event *pev, void *base);
setbase() sets the underlying event base for pev to whatever value you pass ๐ธ this is how you can move an event made with event_alloc() over to a new event loop that sudo's event subsystem allocated ๐ if base is NULL, then pev's event base goes back to the default, which is sudo's main event loop ๐ heads up: using this function means you have to link your plugin with the sudo_util library ๐ and it's very unlikely you'll need it outside of the sudoers plugin ๐
โจ loopbreak โจ
void (*loopbreak)(struct sudo_plugin_event *pev);
loopbreak() makes sudo's event loop exit right away, and the command that's running gets terminated ๐ use with care, bestie!
โจ free โจ
void (*free)(struct sudo_plugin_event *pev);
free() removes the event pev from the event loop and frees the memory that goes with it ๐งน
๐ Remote command execution ๐
okay so the sudo front-end does not support running remote commands ๐
โโ๏ธ BUT starting with sudo 1.8.8, you can use the -h option to name a remote host, and that gets passed along to the policy plugin ๐ a plugin can also accept a runas_user in the form of "user@hostname," which works with older versions of sudo too ๐
the plan for remote commands is to run a "helper" program ๐ง the policy plugin sets up the execution environment so the sudo front-end runs the helper, and the helper then connects to the remote host and runs the command ๐ซถ
for example, the policy plugin could use ssh to do remote command execution โจ the helper program would be in charge of running ssh with the right options so it can use a private key or certificate the remote host will accept, then run a program on that host that sets up the execution environment ๐
โ ๏ธ remote sudoedit has to be handled by the policy plugin, NOT sudo itself, because the front-end has no idea a remote command is happening ๐ญ this might get addressed in a future revision of the plugin API ๐ค
๐ฌ Conversation API ๐ฌ
if your plugin needs to talk to the user, it can do that with the conversation() function ๐ฃ๏ธ a plugin should never try to read straight from standard input or the user's terminal, because neither one is guaranteed to exist ๐
โโ๏ธ the caller has to put a trailing newline in msg if you want one printed ๐
there's also a printf()-style function for showing informational or error messages to the user ๐ it's usually way easier for simple messages where you don't need any user input ๐
โจ Conversation function structures โจ
the conversation() function takes pointers to these structures as arguments ๐
struct sudo_conv_message {
#define SUDO_CONV_PROMPT_ECHO_OFF 0x0001 /* do not echo user input */
#define SUDO_CONV_PROMPT_ECHO_ON 0x0002 /* echo user input */
#define SUDO_CONV_ERROR_MSG 0x0003 /* error message */
#define SUDO_CONV_INFO_MSG 0x0004 /* informational message */
#define SUDO_CONV_PROMPT_MASK 0x0005 /* mask user input */
#define SUDO_CONV_PROMPT_ECHO_OK 0x1000 /* flag: allow echo if no tty */
#define SUDO_CONV_PREFER_TTY 0x2000 /* flag: use tty if possible */
int msg_type;
int timeout;
const char *msg;
};
#define SUDO_CONV_REPL_MAX 1023
struct sudo_conv_reply {
char *reply;
};
typedef int (*sudo_conv_callback_fn_t)(int signo, void *closure);
struct sudo_conv_callback {
unsigned int version;
void *closure;
sudo_conv_callback_fn_t on_suspend;
sudo_conv_callback_fn_t on_resume;
};
the conversation() and printf()-style function pointers get passed into your plugin's open() function when the plugin is initialized ๐ these type definitions can be used when you declare open():
typedef int (*sudo_conv_t)(int num_msgs,
const struct sudo_conv_message msgs[],
struct sudo_conv_reply replies[], struct sudo_conv_callback *callback);
typedef int (*sudo_printf_t)(int msg_type, const char * restrict fmt, ...);
to use conversation(), your plugin needs to pass an array of struct sudo_conv_message and an array of struct sudo_conv_reply ๐ซถ there has to be one of each for every message in the conversation, so both arrays need the same number of elements ๐ข every struct sudo_conv_reply must have its reply member set to NULL ๐ the struct sudo_conv_callback pointer is optional, and if it isn't NULL, it should hold function pointers that get called when sudo is suspended and/or resumed during conversation input ๐ the on_suspend and on_resume functions get called with the signal that caused the suspend and the closure pointer from the struct sudo_conv_callback ๐ they should return 0 on success and -1 on error, and if there's an error, the conversation ends and conversation() returns -1 ๐ the whole point is letting your plugin release resources like locks that shouldn't be held while suspended, then grab them again once the process resumes ๐ and these functions are not actually invoked from within a signal handler, so chill ๐
the msg_type has to be set to one of these values:
SUDO_CONV_PROMPT_ECHO_OFFโ prompts the user for input with echo turned off, usually for passwords ๐ the reply goes into therepliesarray and it will never beNULLSUDO_CONV_PROMPT_ECHO_ONโ prompts the user for input with echo turned on ๐ค the reply goes into therepliesarray and it will never beNULLSUDO_CONV_ERROR_MSGโ displays an error message ๐จ it gets written to standard error, unlessSUDO_CONV_PREFER_TTYis set, in which case it goes to the user's terminal if that's possibleSUDO_CONV_INFO_MSGโ displays a message โจ it gets written to standard output, unlessSUDO_CONV_PREFER_TTYis set, in which case it goes to the user's terminal if possibleSUDO_CONV_PROMPT_MASKโ prompts the user for input but shows an asterisk for each character they type ๐ the reply goes into therepliesarray and it will never beNULL๐ซฃ this is a nice way to give visual feedback while someone types sensitive info that shouldn't be displayed
on top of those values, you can also set these flag bits:
SUDO_CONV_PROMPT_ECHO_OKโ lets input get read even when echo can't be disabled, for theSUDO_CONV_PROMPT_ECHO_OFForSUDO_CONV_PROMPT_MASKmessage types ๐ by default, sudo refuses to read input if echo can't be disabled for those types, so this flag is the override ๐
Okay bestie, this part is where the plugin stuff gets spicy ๐ ๐ซถ
When a message goes out through SUDO_CONV_ERROR_MSG or SUDO_CONV_INFO_MSG, you can use the SUDO_CONV_PREFER_TTY flag to ask sudo to send it to the user's terminal. If there's no terminal, it falls back to standard error or standard output, depending on which of those two message types you used. Input always goes to the terminal when it can, so this flag only changes where output lands ๐ฅ๏ธโจ
The timeout is how many seconds the prompt waits for input before giving up. Set it to 0 and it waits forever, no cap ๐ฐ๏ธ๐
The plugin is in charge of freeing the reply buffer inside each struct sudo_conv_reply, as long as it isn't NULL. SUDO_CONV_REPL_MAX is the biggest the reply buffer can be, not counting the trailing NUL character. Realistically, that's the longest password sudo will accept ๐๐
The printf()-style function runs on the same engine as conversation(), but for msg_type it only takes SUDO_CONV_INFO_MSG and SUDO_CONV_ERROR_MSG. It's way more convenient when you don't need a reply from the user, and it supports the usual printf() escape sequences ๐ธ
If you want to see conversation() in action, check out the sample plugin ๐ฆ
โจ Plugin invocation order โจ
Starting in sudo 1.9.0, the plugin open() and close() functions run in this exact order, so pay attention bestie ๐ง
- audit open
- policy open
- approval open
- approval close
- I/O log open
- command runs
- command exits
- I/O log close
- policy close
- audit close
- sudo exits
Before sudo 1.9.0, the I/O log close() function ran after the policy close() function. Old school, but still a vibe ๐ฐ๏ธ
โจ Sudoers group plugin API โจ
The sudoers plugin has its own plugin interface for non-Unix group lookups. Basically, it lets sudo ask a group source that isn't the standard Unix group database. Two sample group plugins come bundled with sudo, group_file and system_group, and you can read all about them in sudoers(5). Third-party group plugins exist too, like a QAS AD plugin from Quest Software ๐ข๐
A group plugin has to declare and fill in a struct sudoers_group_plugin in the global scope. This struct holds pointers to the functions that handle plugin initialization, cleanup, and group lookup ๐
struct sudoers_group_plugin {
unsigned int version;
int (*init)(int version, sudo_printf_t sudo_plugin_printf,
char *const argv[]);
void (*cleanup)(void);
int (*query)(const char *user, const char *group,
const struct passwd *pwd);
};
A struct sudoers_group_plugin has these fields:
versionโ This should be set toGROUP_API_VERSION. That way sudoers can tell which API version the group plugin was built against ๐initโ This is called after sudoers is parsed but before any policy checks. It returns1on success,0on failure (or if the plugin isn't configured), and-1if a error occurred. If something goes wrong, the plugin can call theplugin_printf()function withSUDO_CONF_ERROR_MSGto show the user more error info ๐จ
int (*init)(int version, sudo_printf_t sudo_plugin_printf,
char *const argv[]);
versionโ The version sudoers passes in lets the plugin figure out the major and minor version number of the group plugin API that sudoers supports.plugin_printfโ A pointer to a printf()-style function you can use to show informational or error messages to the user. It returns the number of characters printed on success and -1 on failure.argvโ A NULL-terminated array of arguments generated from thegroup_pluginoption in sudoers. If no arguments were given,argvwill beNULL.
cleanupโ This runs when sudoers is done with its group checks. The plugin should free any memory it allocated and close any open file handles ๐งน
void (*cleanup)();
queryโ This asks the group plugin whetheruseris a member ofgroup๐ซง
int (*query)(const char *user, const char *group,
const struct passwd *pwd);
userโ The name of the user being looked up in the external group database.groupโ The name of the group being queried.pwdโ The password database entry foruser, if there is one. Ifuserisn't in the password database,pwdwill beNULL.
Group API Version Macros
/* Sudoers group plugin version major/minor */
#define GROUP_API_VERSION_MAJOR 1
#define GROUP_API_VERSION_MINOR 0
#define GROUP_API_VERSION ((GROUP_API_VERSION_MAJOR << 16) | \
GROUP_API_VERSION_MINOR)
For getters and setters see the โPolicy plugin APIโ. ๐
โจ PLUGIN API CHANGELOG โจ
Here's the full history of every revision made to the Sudo Plugin API. Main character energy ๐๐
- Version 1.0 โ Initial API version. ๐ฃ
- Version 1.1 (sudo 1.8.0) โ The I/O logging plugin's
open()function was changed to take thecommand_infolist as an argument. - Version 1.2 (sudo 1.8.5) โ Several things changed:
- The Policy and I/O logging plugins'
open()functions now get a list of plugin parameters, if any are specified in sudo.conf(5). - A simple hooks API was introduced so plugins can hook into the system's environment handling functions.
- The
init_session()Policy plugin function now gets a pointer to the user environment, which it can update as needed. This can be used to merge in environment variables stored in the PAM handle before a command runs.
- The Policy and I/O logging plugins'
- Version 1.3 (sudo 1.8.7) โ Several things changed:
- Support for the
exec_backgroundentry was added to thecommand_infolist. - The
max_groupsandplugin_direntries were added to the settings list. - The
version()andclose()functions are now optional. Before this, a missingversion()orclose()function would crash things ๐ If no policy pluginclose()function is defined, the sudo front-end provides a defaultclose()that displays a warning if the command couldn't be executed. - The sudo front-end now installs default signal handlers to trap common signals while the plugin functions run.
- Support for the
- Version 1.4 (sudo 1.8.8) โ The
remote_hostentry was added to the settings list. - Version 1.5 (sudo 1.8.9) โ The
preserve_fdsentry was added to thecommand_infolist. - Version 1.6 (sudo 1.8.11) โ Two behavior changes for I/O logging plugins:
- When an I/O logging plugin returns an error (
-1), the sudo front-end used to do nothing iflog_ttyin(),log_ttyout(),log_stdin(),log_stdout(), orlog_stderr()returned an error. That's changed now. - When an I/O logging plugin returns
0, output from the command used to still show up on the terminal even if an output logging function returned0. That's changed too.
- When an I/O logging plugin returns an error (
- Version 1.7 (sudo 1.8.12) โ Two things changed:
- The
plugin_pathentry was added to the settings list. - The
debug_flagsentry now starts with a debug file path name, and it may occur multiple times if there are multiple plugin-specific Debug lines in the sudo.conf(5) file.
- The
- Version 1.8 (sudo 1.8.15) โ The
sudoedit_checkdirandsudoedit_followentries were added to thecommand_infolist. The default value ofsudoedit_checkdirwas changed to true in sudo 1.8.16. The sudoconversation()function now takes a pointer to astruct sudo_conv_callbackas its fourth argument, and thesudo_conv_tdefinition has been updated to match. A plugin has to say it supports plugin API version 1.8 or higher to get a conversation function pointer that supports this argument. - Version 1.9 (sudo 1.8.16) โ The
execfdentry was added to thecommand_infolist. - Version 1.10 (sudo 1.8.19) โ The
umaskentry was added to theuser_infolist. Theiolog_group,iolog_mode, andiolog_userentries were added to thecommand_infolist. - Version 1.11 (sudo 1.8.20) โ The
timeoutentry was added to the settings list. - Version 1.12 (sudo 1.8.21) โ The
change_winsize()function was added tostruct io_plugin. - Version 1.13 (sudo 1.8.26) โ The
log_suspend()function was added tostruct io_plugin. - Version 1.14 (sudo 1.8.29) โ The
umask_overrideentry was added to thecommand_infolist. - Version 1.15 (sudo 1.9.0) โ A big one! Several things changed:
- The
cwd_optionalentry was added to thecommand_infolist. - The
event_alloc()function was added tostruct policy_pluginandstruct io_plugin. - The
errstrargument was added to the policy and I/O plugin functions, so a plugin function can use it to return an error string. The audit plugin can use this string to report failure or error conditions set by the other plugins. - The
close()function is now called whether or not a command was actually executed. This means plugins can do cleanup even when no command ran. SUDO_CONV_REPL_MAXwent up from 255 to 1023 bytes.- Support for audit and approval plugins was added.
- The
- Version 1.16 (sudo 1.9.3) โ Initial resource limit values were added to the
user_infolist. Thecmnd_chrootandcmnd_cwdentries were added to the settings list. - Version 1.17 (sudo 1.9.4) โ The
event_alloc()function was added tostruct audit_pluginandstruct approval_plugin. - Version 1.18 (sudo 1.9.9) โ The policy can now set resource limit values in the
command_infolist. Theinterceptandlog_subcmdsentries were added to thecommand_infolist. - Version 1.19 (sudo 1.9.11) โ The
intercept_ptraceandintercept_setidentries were added to the settings list. Theapparmor_profileanduse_ptraceentries were added to thecommand_infolist. - Version 1.20 (sudo 1.9.12) โ The
update_ticketentry was added to the settings list. Theintercept_verifyentry was added to thecommand_infolist. - Version 1.21 (sudo 1.9.13) โ The
sudoedit_nfilesentry was added to thecommand_infolist. - Version 1.22 (sudo 1.9.16) โ The
ttydeventry was added to theuser_infolist.
โจ SEE ALSO โจ
sudo.conf(5), sudoers(5), sudo(8) ๐๐
โจ AUTHORS โจ
okay so sudo has been around for SO long, and tons of people have helped build it ๐ซถ but this version was mostly written by:
Todd C. Miller ๐
for the full list of everyone who's contributed, check the CONTRIBUTORS.md file in the sudo distribution, or go to https://www.sudo.ws/about/contributors/ ๐ธ it's giving community energy fr
๐ BUGS ๐
bestie, if you think you've found a bug in sudo_plugin ๐ you've got a few options. you can file a report in the sudo bug database at https://bugzilla.sudo.ws/, or open an issue at https://github.com/sudo-project/sudo/issues. if email is more your thing, you can send messages to the sudo-workers mailing list, https://www.sudo.ws/mailman/listinfo/sudo-workers (that one is public), or privately to <sudo@sudo.ws> ๐
๐จ SECURITY, PLEASE READ ๐จ do NOT report security vulnerabilities through public GitHub issues, Bugzilla, or the mailing lists!! instead, email them directly to<Todd.Miller@sudo.ws>. if you want extra privacy, you can encrypt your message with PGP, using the key athttps://www.sudo.ws/dist/PGPKEYS๐๐
๐ SUPPORT ๐
there's limited free support available through the sudo-users mailing list ๐ to subscribe or search the archives, head to https://www.sudo.ws/mailman/listinfo/sudo-users โจ
โ๏ธ DISCLAIMER โ๏ธ
okay the legal stuff, but make it cute ๐
sudo is provided "AS IS", and any express or implied warranties are disclaimed. that includes, but isn't limited to, the implied warranties of merchantability and fitness for a particular purpose. for the full details, check the LICENSE.md file that comes with sudo, or https://www.sudo.ws/about/license/ ๐ง
Sudo 1.9.17p2 ๐ August 14, 2024 ๐ SUDO_PLUGIN(5) ๐ฆ that's a wrap, bestie!! ๐โจ
๐ฆ where this page comes from
- alpine 3.24: sudo-doc 1.9.17_p2-r2 ยท custom ISC
- arch rolling: sudo 1.9.17.p2-6 ยท custom
- fedora 43: sudo-devel 1.9.17-7.p2.fc43 ยท ISC
- fedora 43: sudo-devel 1.9.17-7.p2.fc43 ยท ISC
- fedora 44: sudo-devel 1.9.17-10.p2.fc44 ยท ISC
- fedora 44: sudo-devel 1.9.17-10.p2.fc44 ยท ISC
- fedora 44: sudo-devel 1.9.17-7.p2.fc44 ยท ISC
- fedora 44: sudo-devel 1.9.17-7.p2.fc44 ยท ISC
- nixos 26.05: sudo 1.9.17p2
- nixpkgs unstable: sudo 1.9.17p2
- ubuntu questing: sudo 1.9.17p2-1ubuntu1
- ubuntu questing: sudo 1.9.17p2-1ubuntu1.1 ยท ISC
- ubuntu resolute: sudo 1.9.17p2-1ubuntu3 ยท ISC
- ubuntu resolute: sudo 1.9.17p2-1ubuntu3.2 ยท ISC
- ubuntu stonking: sudo 1.9.17p2-7ubuntu5 ยท ISC
- ๐ the page's license: ISC
- Copyright (c) 2009-2024 Todd C. Miller <Todd.Miller@sudo.ws>
- ๐ girlified by anthropic/claude-haiku-5.5 on : a language model's rewrite of the original, under its terms