SUDO_PLUGIN(5)

๐Ÿ“ฆ
version 1.9.17_p2 ยท 5 versions
, in alpine 3.24, arch rolling, fedora 43, fedora 44, nixos 26.05, nixpkgs unstable, ubuntu questing, ubuntu resolute, ubuntu stonking

โœจ 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 to SUDO_POLICY_PLUGIN ๐Ÿ’–.
  • version โ€” This should be set to SUDO_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 the conversation() function, which the plugin can use to talk to the user (see โ€œConversation APIโ€ for details) ๐Ÿ’ฌ. It returns 0 on success and -1 on failure.
  • sudo_plugin_printf โ€” A pointer to a printf()-style function for showing informational or error messages (see โ€œConversation APIโ€ for details) ๐Ÿ“ฃ. It returns the number of characters printed on success and -1 on failure.
  • settings โ€” A vector of user-supplied sudo settings, written as name=value strings ๐ŸŽ€. 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 -a option, to use on systems where BSD authentication is supported ๐Ÿ”.
  • closefrom=number โ€” If it's specified, the user asked (with the -C option) 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 the command_info list ๐Ÿ’œ.
  • cmnd_chroot=string โ€” The root directory (see chroot(2)) to run the command in, as the user specified with the -R option ๐ŸŒณ. 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 -D option ๐Ÿ“. 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's Debug entry in sudo.conf(5), if there is one ๐Ÿ›. The flags get passed to the plugin exactly as they appear in sudo.conf(5). The syntax sudo and the sudoers plugin use is subsystem@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-specific debug_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 passes debug_flags if sudo.conf(5) contains a plugin-specific Debug entry ๐Ÿ’….
  • ignore_ticket=bool โ€” Set to true if the user specified the -k option along with a command, which means they want sudo to ignore any cached authentication credentials ๐Ÿ™ˆ. (Sudo may also set implied_shell to true. This lets sudo with no arguments be used similarly to su(1). If the plugin doesn't support this usage, it may return a value of -2 from the check_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 sets implied_shell ๐Ÿš.
  • intercept_ptrace=bool โ€” Tells you whether the system supports intercept mode using ptrace(2) ๐Ÿ”. Right now this is only true for Linux systems that support seccomp(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 support seccomp(2) filtering and the โ€œtrapโ€ action. On systems that use a dynamic shared object to implement intercept, the dynamic linker (ld.so or 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 -c option ๐ŸŽ€.
  • login_shell=bool โ€” Set to true if the user specified the -i option, 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 in sudo.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 -n option, 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 -E option, which means they want to keep their environment ๐ŸŒธ
  • preserve_groups=bool: set to true if the user passed the -P option. 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 -p option ๐Ÿซถ
  • remote_host=string: the name of the remote host to run the command on, if the user set it with the -h option ๐ŸŒˆ 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 -s option, 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 -g option ๐Ÿ“
  • runas_user=string: the user name or user-ID to run the command as, if the user set it with the -u option ๐Ÿ’–
  • selinux_role=string: the SELinux role to use when running the command, if the -r option was used ๐Ÿ”ฅ
  • selinux_type=string: the SELinux type to use when running the command, if the -t option was used ๐Ÿ”ฅ
  • set_home=bool: set to true if the user passed the -H option. When that's true, the HOME environment variable gets set to the target user's home directory ๐Ÿก
  • sudoedit=bool: set to true when the -e option is used, or when sudo was invoked as sudoedit ๐Ÿ“ The plugin is supposed to swap an editor into argv inside the check_policy() function. If the plugin doesn't support sudoedit, it should return -2 to signal a usage error instead. For more info, check the check_policy() section ๐Ÿ‘€
  • timeout=string: the command timeout the user set with the -T option โณ 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 -N option, 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 the gethostname(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 of name=value strings. โœจ

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_options is 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: If open() returns something other than 1, the plugin can store a message describing the failure in errstr. The sudo front-end then passes that message along to any registered audit plugins. The string in errstr has to stay valid until the plugin's close() 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 of exit_status is undefined if error is non-zero. ๐Ÿšฆ
  • error: If the command couldn't be executed, this is set to the value of errno set by the execve(2) system call. The plugin is responsible for showing error info through the conversation() or sudo_plugin_printf() function. If the command ran successfully, error is 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 in argv, 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 of name=value strings. 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, as name=value strings. 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 of number or 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 unless cwd_optional is 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 unless cwd is also set. ๐Ÿคž
    • exec_background=bool: By default, sudo runs a command as the foreground process as long as sudo itself is in the foreground. When exec_background is enabled and the command runs in a pseudo-terminal (because of I/O logging or the use_pty setting), 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 the fexecve(2) system call to run the command instead of execve(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's check_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 in sudoers(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 unless use_ptrace is 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 with login_class on BSD systems ๐Ÿ’–.
  • log_subcmds=bool โ€” If this is set, sudo will call the audit plugin's accept() function to log when the command runs a subsequent command, as long as the system supports it ๐Ÿ“. If intercept is also specified, log_subcmds gets ignored, so no double dipping ๐Ÿ™ƒ. See Preventing shell escapes in sudoers(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 the closefrom setting 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 from runas_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 ๐Ÿซถ. infinity means there's no limit at all โ™พ๏ธ, user keeps the invoking user's resource limit as is ๐Ÿ™Œ, and default uses 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 ๐ŸŽ€. infinity means no limit, user preserves the invoking user's resource limit, and default uses 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 ๐Ÿซถ. infinity is no limit โ™พ๏ธ, user keeps the invoking user's resource limit ๐Ÿ™Œ, and default uses 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 ๐Ÿ’œ. infinity means no limit, user preserves the invoking user's resource limit, and default uses 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 ๐ŸŽ€. infinity is no limit โ™พ๏ธ, user keeps the invoking user's resource limit ๐Ÿซถ, and default uses 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. infinity means no limit at all, user keeps the invoking user's resource limit as it is, and default uses 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. infinity means no limit, user preserves the invoking user's limit, and default uses 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. infinity means no limit, user keeps the invoking user's limit, and default uses 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. infinity means no limit, user keeps the invoking user's limit, and default uses 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. infinity means no limit, user keeps the invoking user's limit, and default uses 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. infinity means no limit, user keeps the invoking user's limit, and default uses 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 of runas_gid gets used instead.
  • runas_euid=uid โ€” ๐Ÿ‘‘ The effective user-ID the command runs as. If you don't set it, the value of runas_uid gets 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 the runas_user's default group. This is just for auditing, though. The sudo front-end uses runas_egid and runas_gid when it actually executes the command.
  • runas_groups=list โ€” ๐Ÿงบ The supplementary group vector for the command, written as a comma-separated list of group-IDs. If preserve_groups is 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 match runas_euid (or runas_uid if runas_euid isn't set). Again, this is only for auditing. The sudo front-end actually uses runas_euid and runas_uid when 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. The sudoedit_checkdir option 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. The sudoedit_follow option 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 the argv_out vector are the files to be edited. The โ€˜--โ€™ element has to sit directly before the first file to be edited. If sudoedit_nfiles isn'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 the umask_override option is also set.
  • umask_override=bool โ€” ๐Ÿ’ช Forces the value set by the umask option to override any umask that PAM or login.conf sets.
  • use_ptrace=bool โ€” ๐Ÿ” If set, sudo uses ptrace(2) to run intercept mode, as long as the system supports it. This setting does nothing unless intercept is 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 when set_utmp is 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 the execve(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 โ€” if check_policy() returns something other than 1, the plugin can leave a message about the failure or error in errstr. The sudo front-end then hands that to any registered audit plugins ๐Ÿฆ‹ The string has to stay valid until the plugin's close() 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 in argv, 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 the execve(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 โ€” if list() returns something other than 1, 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's close() 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 โ€” if validate() returns something other than 1, 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's close() 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, pwd describes 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's user_env_out parameter. If init_session() needs to change the user environment, it should update the pointer stored in user_env_out. The expected use case is merging the PAM environment (if there is one) with the contents of user_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 โ€” if init_session() returns something other than 1, the plugin can store a message about the failure or error in errstr. The front-end passes it to any registered audit plugins. The string has to remain valid until the plugin's close() 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_alloc won'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 โ€” The type field should always be set to SUDO_IO_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[], 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 the conversation() function. The plugin can use it to show version info from show_version() (more on that below) ๐Ÿ’– It can also be used to display extra error messages to the user. The conversation() function returns 0 on success and -1 on failure.
  • sudo_plugin_printf โ€” A pointer to a printf()-style function that the show_version() function can use to display version info (see show_version below). The sudo_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 in argv, not counting the final NULL pointer. It can be zero, like when sudo is called with the -V option ๐Ÿ’œ
  • 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 the execve(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_options will 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 the open() function returns a value other than 1, the plugin may store a message describing the failure or error in errstr. The sudo front-end will then pass this value to any registered audit plugins ๐Ÿ”ฅ The string stored in errstr must stay valid until the plugin's close() 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 the wait(2) system call, or zero if no command was run. The value of exit_status is undefined if error is non-zero โš ๏ธ
  • error โ€” If the command couldn't be executed, this is set to the value of errno set by the execve(2) system call. If the command ran successfully, the value of error is 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 of buf in bytes ๐Ÿ“
  • errstr โ€” if log_ttyin() returns anything other than 1, 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 in errstr has to stay valid until the plugin's close() 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 of buf in bytes ๐Ÿ“
  • errstr โ€” if log_ttyout() returns something other than 1, the plugin may store a message describing the failure or error in errstr ๐Ÿ“ The sudo front-end passes that along to any registered audit plugins. The string has to stay valid until the plugin's close() 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 of buf in bytes ๐Ÿ“
  • errstr โ€” if log_stdin() returns a value other than 1, 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 in errstr must remain valid until the plugin's close() 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 of buf in bytes ๐Ÿ“
  • errstr โ€” if log_stdout() returns something other than 1, the plugin may store a message describing the failure or error in errstr ๐Ÿ“ The sudo front-end hands that to any registered audit plugins. The string has to stay valid until the plugin's close() 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 of buf in bytes ๐Ÿ“
  • errstr โ€” if log_stderr() returns something other than 1, the plugin may store a message describing the failure or error in errstr ๐Ÿ“ The sudo front-end passes that to any registered audit plugins. The string stored there must remain valid until the plugin's close() 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 โ€” if change_winsize() returns a value other than 1, the plugin may store a message describing the failure or error in errstr ๐Ÿ“ The sudo front-end hands that to any registered audit plugins. The string has to stay valid until the plugin's close() 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, or SIGCONT if the command was resumed ๐Ÿ”
  • errstr โ€” if log_suspend() returns a value other than 1, the plugin may store a message describing the failure or error in errstr ๐Ÿ“ The sudo front-end passes it to any registered audit plugins. The string stored there must remain valid until the plugin's close() 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 the conversation() function. the show_version() function can use it to display version info (see show_version() below), and it can also be used to show extra error messages to the user ๐Ÿ’ฌ conversation() returns 0 on success and -1 on failure.
  • plugin_printf โ€” a pointer to a printf()-style function that show_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 -1 on 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 into submit_argv that points at the first entry that isn't a command line option ๐Ÿงญ if submit_argv only has options in it (which can happen with the -l or -v options), then submit_argv[submit_optind] evaluates to the NULL pointer ๐Ÿ’€
  • submit_argv โ€” the argument vector sudo was invoked with, options and all ๐ŸŽ€ you can use submit_optind to 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_options is the NULL pointer.
  • errstr โ€” if open() returns something other than 1, 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 in errstr has to stay valid until the plugin's close() 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 of SUDO_PLUGIN_NO_STATUS, SUDO_PLUGIN_WAIT_STATUS, SUDO_PLUGIN_EXEC_ERROR or SUDO_PLUGIN_SUDO_ERROR ๐Ÿ”ฅ
  • status โ€” what this holds depends on status_type ๐Ÿค“ it's either ignored, or it's the command's exit status as returned by the wait(2) system call, or it's the value of errno set by the execve(2) system call, or it's the value of errno from 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 either SUDO_POLICY_PLUGIN, SUDO_POLICY_APPROVAL, or SUDO_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_info might 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 the execve(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 โ€” if accept() returns something other than 1, 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's close() 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 either SUDO_POLICY_PLUGIN, SUDO_APPROVAL_PLUGIN, or SUDO_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_msg is 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 โ€” if reject() returns something other than 1, the plugin can leave a message about the failure or error in errstr ๐Ÿ’Œ. The sudo front-end then hands that message to any registered audit plugins ๐Ÿ‘€. The string stored in errstr has to stay valid until the plugin's close() 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, or SUDO_FRONT_END if it was the sudo front-end ๐Ÿงš.
  • audit_msg โ€” an optional string describing the plugin error ๐Ÿ’ฌ. If the plugin didn't provide a description, audit_msg is 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 โ€” if error() returns something other than 1, the plugin may store a message about the failure or error in errstr ๐Ÿ“. The sudo front-end passes that along to any registered audit plugins, and the string has to stay valid until the plugin's close() 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 version 1.17. If the sudo front-end doesn't support API version 1.17 or 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 to SUDO_APPROVAL_PLUGIN ๐Ÿท๏ธ.
  • version โ€” 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 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 the conversation() function, which the plugin can use to chat with the user ๐Ÿ’ฌ. See the โ€œConversation APIโ€ for details. It returns 0 on success and -1 on failure.
  • plugin_printf โ€” a pointer to a printf()-style function for showing informational or error messages ๐Ÿ“ข. See the โ€œConversation APIโ€ for details. It returns the number of characters printed on success and -1 on 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 into submit_argv that points to the first entry that isn't a command line option ๐Ÿ‘‰. If submit_argv only has options in it (which can happen with the -l or -v options), then submit_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 use submit_optind to 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 โ€˜=โ€™ ๐Ÿ”ช. 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 a NULL-terminated array of strings ๐Ÿ“. If no arguments were specified, plugin_options is the NULL pointer ๐Ÿ™ˆ.
  • errstr โ€” if open() returns something other than 1, the plugin can store a message about the failure or error in errstr ๐Ÿ’Œ. The sudo front-end passes this to any registered audit plugins ๐Ÿ‘€. The string has to stay valid until the plugin's close() 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 the execve(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 โ€” if open() returned something other than 1, 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's close() 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:

  • SIGALRM
  • SIGHUP
  • SIGINT
  • SIGPIPE
  • SIGQUIT
  • SIGTERM
  • SIGTSTP
  • SIGUSR1
  • SIGUSR2

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 to SUDO_HOOK_VERSION.
  • hook_type โ€” this can be one of the following supported hook types:
    • SUDO_HOOK_SETENV โ€” this is for the C library setenv(3) function. Any registered hooks run before the C library implementation. The hook_fn field 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 library unsetenv(3) function. Any registered hooks run before the C library implementation. The hook_fn field 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 library getenv(3) function. Any registered hooks run before the C library implementation. The hook_fn field 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 library putenv(3) function. Any registered hooks run before the C library implementation. The hook_fn field 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, a getenv(3) hook might return SUDO_HOOK_RET_NEXT if 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 leaves environ unchanged ๐Ÿช„
โš ๏ธ Care must be taken when hooking C library functions, because it is very easy to create an infinite loop ๐Ÿ˜ญ For example, a getenv(3) hook that calls the snprintf(3) function may create a loop if the snprintf(3) implementation calls getenv(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 the struct sudo_plugin_event itself ๐Ÿชž
  • fd โ€” the file or socket descriptor for I/O-based events, or the signal number for signal events. For time-based events, fd must 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 (like SUDO_PLUGIN_EV_READ and SUDO_PLUGIN_EV_WRITE) or a timeout attached, what is how you figure out why it ran ๐Ÿ•ต๏ธโ€โ™€๏ธ
  • closure โ€” the generic pointer you passed in to the set() 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 the replies array and it will never be NULL
  • SUDO_CONV_PROMPT_ECHO_ON โ€” prompts the user for input with echo turned on ๐ŸŽค the reply goes into the replies array and it will never be NULL
  • SUDO_CONV_ERROR_MSG โ€” displays an error message ๐Ÿšจ it gets written to standard error, unless SUDO_CONV_PREFER_TTY is set, in which case it goes to the user's terminal if that's possible
  • SUDO_CONV_INFO_MSG โ€” displays a message โœจ it gets written to standard output, unless SUDO_CONV_PREFER_TTY is set, in which case it goes to the user's terminal if possible
  • SUDO_CONV_PROMPT_MASK โ€” prompts the user for input but shows an asterisk for each character they type ๐Ÿ™ˆ the reply goes into the replies array and it will never be NULL ๐Ÿซฃ 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 the SUDO_CONV_PROMPT_ECHO_OFF or SUDO_CONV_PROMPT_MASK message 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 ๐Ÿงš

  1. audit open
  2. policy open
  3. approval open
  4. approval close
  5. I/O log open
  6. command runs
  7. command exits
  8. I/O log close
  9. policy close
  10. audit close
  11. 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 to GROUP_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 returns 1 on success, 0 on failure (or if the plugin isn't configured), and -1 if a error occurred. If something goes wrong, the plugin can call the plugin_printf() function with SUDO_CONF_ERROR_MSG to 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 the group_plugin option in sudoers. If no arguments were given, argv will be NULL.
  • 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 whether user is a member of group ๐Ÿซง
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 for user, if there is one. If user isn't in the password database, pwd will be NULL.

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 the command_info list 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.
  • Version 1.3 (sudo 1.8.7) โ€” Several things changed:
    • Support for the exec_background entry was added to the command_info list.
    • The max_groups and plugin_dir entries were added to the settings list.
    • The version() and close() functions are now optional. Before this, a missing version() or close() function would crash things ๐Ÿ’€ If no policy plugin close() function is defined, the sudo front-end provides a default close() 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.
  • Version 1.4 (sudo 1.8.8) โ€” The remote_host entry was added to the settings list.
  • Version 1.5 (sudo 1.8.9) โ€” The preserve_fds entry was added to the command_info list.
  • 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 if log_ttyin(), log_ttyout(), log_stdin(), log_stdout(), or log_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 returned 0. That's changed too.
  • Version 1.7 (sudo 1.8.12) โ€” Two things changed:
    • The plugin_path entry was added to the settings list.
    • The debug_flags entry 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.
  • Version 1.8 (sudo 1.8.15) โ€” The sudoedit_checkdir and sudoedit_follow entries were added to the command_info list. The default value of sudoedit_checkdir was changed to true in sudo 1.8.16. The sudo conversation() function now takes a pointer to a struct sudo_conv_callback as its fourth argument, and the sudo_conv_t definition 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 execfd entry was added to the command_info list.
  • Version 1.10 (sudo 1.8.19) โ€” The umask entry was added to the user_info list. The iolog_group, iolog_mode, and iolog_user entries were added to the command_info list.
  • Version 1.11 (sudo 1.8.20) โ€” The timeout entry was added to the settings list.
  • Version 1.12 (sudo 1.8.21) โ€” The change_winsize() function was added to struct io_plugin.
  • Version 1.13 (sudo 1.8.26) โ€” The log_suspend() function was added to struct io_plugin.
  • Version 1.14 (sudo 1.8.29) โ€” The umask_override entry was added to the command_info list.
  • Version 1.15 (sudo 1.9.0) โ€” A big one! Several things changed:
    • The cwd_optional entry was added to the command_info list.
    • The event_alloc() function was added to struct policy_plugin and struct io_plugin.
    • The errstr argument 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_MAX went up from 255 to 1023 bytes.
    • Support for audit and approval plugins was added.
  • Version 1.16 (sudo 1.9.3) โ€” Initial resource limit values were added to the user_info list. The cmnd_chroot and cmnd_cwd entries were added to the settings list.
  • Version 1.17 (sudo 1.9.4) โ€” The event_alloc() function was added to struct audit_plugin and struct approval_plugin.
  • Version 1.18 (sudo 1.9.9) โ€” The policy can now set resource limit values in the command_info list. The intercept and log_subcmds entries were added to the command_info list.
  • Version 1.19 (sudo 1.9.11) โ€” The intercept_ptrace and intercept_setid entries were added to the settings list. The apparmor_profile and use_ptrace entries were added to the command_info list.
  • Version 1.20 (sudo 1.9.12) โ€” The update_ticket entry was added to the settings list. The intercept_verify entry was added to the command_info list.
  • Version 1.21 (sudo 1.9.13) โ€” The sudoedit_nfiles entry was added to the command_info list.
  • Version 1.22 (sudo 1.9.16) โ€” The ttydev entry was added to the user_info list.

โœจ 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 at https://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!! ๐ŸŒˆโœจ