I recently discovered that sudo actually has a plugin system. In my head this is just bonkers; a program like sudo should not have a plugin interface. So I just had to try it out. 😁 It turned out that there aren’t really any great resources online on how to actually do that – probably with good reason – so I decided to just write one myself.
How Sudo Works
On Linux (or actually any Unixoid system for that matter) sudo is a command-line application that you can use to elevate the privileges of the current user for a given command. A classic example of when you would use it is if you wanted to install a new package on your system: the package manager probably needs to modify the system in a way a normal user is not allowed to. Let’s say I want to install iotop on my Fedora machine; I can run sudo dnf install iotop as my normal user, and sudo will execute dnf for me with root permissions.
To achieve this, sudo is actually what’s called a setuid binary. Setuid is a file permission flag that tells the kernel to always execute the file with the permissions of its owner – the owner being root in the case of sudo.

sudo binary on my MacOS system; The “s” in the permission bits section indicates that the setuid flag is set.Once the process starts, it will do the following:
- It evaluates its command-line arguments.
- It looks up the user and the command in its configuration.
- It determines if the user is allowed to run the given command and whether they should be challenged for a password.
- It might talk to an authorization provider like PAM.
- It might drop permissions to a given target user.
- And lastly, it executes the target program (or shell).
At least this is how it appears. That’s, however, not 100% correct. In reality sudo itself is actually kind of dumb. It just handles command-line arguments and the permission stuff. The actual logic of how the user and user-command are validated is done by a so-called security policy plugin. In theory there could be multiple such plugins. However, the default one is called sudoers.
Now, if you are familiar with sudo, you might actually recognize that name. That’s because the config file that contains all important “sudo” settings is called /etc/sudoers. As you might have already guessed, that file is just for sudoers settings. sudo itself has its own config file, usually in /etc/sudo.conf. There you can specify the plugins that should be loaded.
Security in Sudo
From a security perspective, sudo is actually kind of bad. In fact, while I was working on this blog post, another 9.3 vulnerability for local privilege escalation dropped. Let’s take a look at some (design) decisions that might contribute to that:
- Let’s start with the elephant in the room: the plugin system. Plugins require additional public interfaces, which in turn create more attack surface. Additionally, each plugin itself could contain security issues. In the case of
sudo, there are only two official plugins anyway:sudoersandsudo_plugin_python– the latter is just a wrapper to expose the plugin interface to Python scripts. I thinksudoerscould have just been integrated intosudoitself, removing the need for plugins altogether.
By the way, I’m not saying that this plugin interface is not useful. There are plugins likesudo_pairthat have legitimate use cases. But in terms of security, it’s at the very least suboptimal. - Easter eggs. Don’t get me wrong: I’m a sucker for easter eggs in software. And while I think the
insultsflag is hilarious, I’m not sure if I like that they put it into such a security-focused application. - The sheer size of the project. At the time of writing, running
clocon thesudorepository returns 289,678 lines of code. To put this into perspective, the complete Unix 6 system + userland, including a C compiler, is about one-third of that. And it’s not likesudo‘s problem domain is inherently complex.doas, an OpenBSD program that essentially does the same thing, clocks in at only 3,167 lines (I used the Arch port to measure this because I didn’t want to deal with OpenBSD’s source repo.). The thing is, 100 times the size means 100 times more opportunity for bugs. When it comes to security, smaller is usually better. - Most of the source code is written in C. There is actually a project called
sudo-rsaiming to rebuildsudoin Rust. Getting rid of a whole class of security issues is no doubt a good move. I’m just not sure if rethinking the problem might have been a better move instead of just reimplementing the existing solution. - Utilizing setuid for stuff like this might actually not be that great of an idea. Using setuid as the method to gain root permissions requires that all checks are actually executed by a process started by the user. There are other solutions for this problem that defer the permission check and the execution of the target program to a service process running as root. The actual binary then just communicates with that service and therefore doesn’t need to run as root. One implementation of this is systemd’s run0.
Building a Plugin
Before we get into the weeds, let’s get one important thing out of the way:
No, you should not do this!
Do not try this on any machine that you actually need to function!
Good. Now let’s get on with writing our plugin.
There are actually multiple different types of plugins in sudo (and one plugin can use any number of the underlying APIs):
- Policy plugins will be called when
sudoneeds to check if the given user can execute the given command.sudoersis an example of a policy plugin. - Approval plugins are called after a policy plugin has already checked and approved the request. Approval plugins can apply further restrictions to who can execute which command.
- I/O plugins can be used to process or log the input and output of the user session.
- Audit plugins are called for (un)successful authentication attempts and can also be used for logging.
- Hook functions allow for more fine-grained hooks into different parts of
sudo. - The Event API allows you to hook into the internal event loop of
sudo.
Okay, so now that we know what we can do, we just need an idea. I actually got inspired by @5225225@furry.engineer over on the fediverse: the original idea was a bot that posts “this incident will be reported” on your account when you mistype your password. That’s actually a fantastic idea! Let’s do that. And let’s call it… I don’t know; “sudo-shame” sounds good.
Unfortunately, we can’t access the standard “this incident will be reported” message directly, as it is generated by the sudoers plugin. What we can do, however, is register an audit plugin that will get called if the sudo call fails for any reason. Then we can post any arbitrary text wherever we want.
As mentioned earlier, there are actually two different ways of registering an plugin. At first glance, using the sudo_plugin_python wrapper seems easier. However, that would require that sudo was built with the corresponding non-default --enable-python flag, and I’d prefer we don’t make that assumption. That just leaves the C API (or rather the shared object API – the programming language really is not that relevant as long as the ABI matches).
Okay, so let’s start with a skeleton for an audit plugin:
#include <sudo_plugin.h>
// Called when the plugin is opened.
// Interesting arguments are:
// - sudo_plugin_printf can be used for outputs
// - user_info contains stuff like username, ...
// - plugin_options contain the arugments of the plugin
static int plugin_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
) {
return 1;
}
// Called when the plugin is closed.
static void plugin_close(int status_type, int status) {
}
// Called when sudo accepts the request.
static int plugin_accept(
const char *plugin_name,
unsigned int plugin_type,
char * const command_info[],
char * const run_argv[],
char * const run_envp[],
const char **errstr
) {
return 1;
}
// Called when sudo rejects the request.
// This is where we can put our custom code.
// command_info contains stuff like the path of the
// target command.
static int plugin_reject(
const char *plugin_name,
unsigned int plugin_type,
const char *audit_msg,
char * const command_info[],
const char **errstr
) {
return 1;
}
// Called when any plugin creates an error.
static int plugin_error(
const char *plugin_name,
unsigned int plugin_type,
const char *audit_msg,
char * const command_info[],
const char **errstr
) {
return 1;
}
// Called when the -V flag is specified.
static int show_version(int verbose) {
return 1;
}
// The name of this variable is the exported symbol name
// that we can use to load the plugin.
struct audit_plugin shame = {
.type = SUDO_AUDIT_PLUGIN,
.version = SUDO_API_VERSION,
.open = plugin_open,
.close = plugin_close,
.accept = plugin_accept,
.reject = plugin_reject,
.error = plugin_error,
.show_version = show_version,
// The following fields are not relevant for
// our application.
.register_hooks = NULL,
.deregister_hooks = NULL,
.event_alloc = NULL,
};
Code language: C++ (cpp)
The highlighted section at the end defines the symbol that sudo can use to find the plugin in the shared object (shame in this case; this has to be specified later in the sudo.conf file). It also defines which API we are using (SUDO_AUDIT_PLUGIN), as well as the different hooks we can use.
Also note the sudo_plugin_printf argument in the plugin_open function at the top; this is what we are supposed to use for outputs. Inputs can be handled using the conversation API, but we don’t need this anyway.
Okay, so now we have a hook (plugin_reject) that’s called when the authentication request fails. The next question is, how do we call our fedi server of choice? I mean, the easiest solution would probably be to just use libcurl and do the call directly in the plugin. However, this doesn’t seem very clean to me; we’d need to statically link libcurl in the shared object – which is ugly – and I don’t really want to deal with JSON handling in C.
The solution that I chose was to have the plugin execute a shell script instead. We can get the name of the script from the plugin arguments to make it configurable. While we are at it, let’s also give the script some information about the sudo call, like the username, the hostname of the system, and the command that’s executed.
It’s probably a good idea to redirect stdout of the script to the sudo_plugin_printf function so we don’t accidentally write into any streams we are not supposed to. Another thing we can (and should) do is to drop permissions and execute the script as a non-root user instead.
static void invoke_script() {
// arguments for the script call
char* args[] = {
script,
username,
hostname,
command,
NULL
};
// environment of the script call
char* env[] = {
NULL
};
// pipe for stdout, so we can utilize the
// sudo_plugin_printf function
int pipefd[2];
pipe(pipefd);
pid_t child = fork();
if (child == 0) {
// replace stdout with pipe
close(pipefd[0]);
dup2(pipefd[1], STDOUT_FILENO);
close(pipefd[1]);
// drop permissions to uid/gid
// -> need to be set beforehand
if (setuid(uid) < 0) {
printf("setuid: %s\n", strerror(errno));
}
if (setgid(gid) < 0) {
printf("setgid: %s\n", strerror(errno));
}
execve(script, args, env);
exit(1);
} else {
close(pipefd[1]);
char buffer[1024];
ssize_t bytes_read;
// copy output to plugin_printf
while ((bytes_read = read(pipefd[0], buffer, sizeof(buffer) - 1)) > 0) {
buffer[bytes_read] = '\0';
plugin_printf(SUDO_CONV_INFO_MSG, "%s", buffer);
}
close(pipefd[0]);
waitpid(child, NULL, 1);
}
}
Code language: C++ (cpp)
Okay, looking good. That should be everything necessary on the C side. The script is pretty straight forward by comparison, so I won’t bother putting it here. If you’re interested, please check out the git repository – I actually wrote a few different scripts; you can find them in the adapters directory.
The next step is to configure sudo to actually load our plugin. So, let’s put the following in our /etc/sudo.conf:
# shared object name in
# /usr/libexec/sudo uid for script
# plugin symbol | script path | gid for script
# | | | | |
# v v v v v
Plugin shame shame.so /usr/local/lib/sudo-shame/mastodon.sh 1002 1002
Code language: PHP (php)
The arguments after the name of the shared object are given to the plugin in the plugin_options parameter. So, in plugin_open we need to parse these arguments accordingly.
Great, now we just have to try out if this actually works. For, I think, obvious reasons, I decided to try it in a Docker container – I am moderately confident that it won’t break anything, but I’m not going to risk it. ^^


Woohoo, it works! Nice. 😁
Conclusion
My first blog post in over 7 months. I should probably write more about all my little side projects. I think the problem is that a lot of them are in a state where I kinda consider them finished enough to be no longer interesting for me, but not finished enough to write a blog post about them. Even with this one, I was sitting on the almost-done implementation for, like, 4 months.
Anyway, for me this was a really rather fun project. I learned a lot about sudo (and also I enjoy working with C 😅). Hope you had some fun too.
If you want to see more C content, check out my post 7 Beautiful SegFaults in C.
See you next time,
Sigma

