Compare commits

..

No commits in common. "master" and "master" have entirely different histories.

6 changed files with 88 additions and 146 deletions

View file

@ -1,17 +1,11 @@
CFLAGS = -flto -Wall -Wextra -Werror -Wl,-z,now CFLAGS = -Wall -Wextra -Werror -Wl,-z,now
CFLAGS_RELEASE = ${CFLAGS} -O3 -s -D_FORTIFY_SOURCE=2 CFLAGS_RELEASE = ${CFLAGS} -O2 -s -D_FORTIFY_SOURCE=2
CFLAGS_DEBUG = ${CFLAGS} -O0 -g -fsanitize=undefined CFLAGS_DEBUG = ${CFLAGS} -O0 -g -fsanitize=undefined
CFLAGS_STATIC = ${CFLAGS_RELEASE} -static-pie CFLAGS_STATIC = ${CFLAGS_RELEASE} -static-pie
LIBS = -lcrypt LIBS = -lcrypt
CC = cc CC = gcc
PREFIX ?= /usr/local all: rdo.c
BINDIR = ${PREFIX}/bin
MANDIR = ${PREFIX}/share/man
all: rdo
rdo: rdo.c
${CC} ${CFLAGS_RELEASE} rdo.c -o rdo ${LIBS} ${CC} ${CFLAGS_RELEASE} rdo.c -o rdo ${LIBS}
static: rdo.c static: rdo.c
@ -20,29 +14,17 @@ static: rdo.c
debug: rdo.c debug: rdo.c
${CC} ${CFLAGS_DEBUG} rdo.c -o rdo ${LIBS} ${CC} ${CFLAGS_DEBUG} rdo.c -o rdo ${LIBS}
install: all install: rdo
mkdir -p ${DESTDIR}${BINDIR} cp rdo ${DESTDIR}/usr/local/bin/rdo
cp rdo ${DESTDIR}${BINDIR}/rdo chown 0:0 ${DESTDIR}/usr/local/bin/rdo
chmod 4755 ${DESTDIR}${BINDIR}/rdo chmod 755 ${DESTDIR}/usr/local/bin/rdo
mkdir -p ${DESTDIR}${MANDIR}/man1 chmod u+s ${DESTDIR}/usr/local/bin/rdo
cp rdo.1 ${DESTDIR}${MANDIR}/man1/rdo.1 cp rdo_sample.conf ${DESTDIR}/etc/rdo.conf
chmod 644 ${DESTDIR}${MANDIR}/man1/rdo.1 chmod 600 ${DESTDIR}/etc/rdo.conf
mkdir -p ${DESTDIR}${MANDIR}/man5
cp rdo.conf.5 ${DESTDIR}${MANDIR}/man5/rdo.conf.5
chmod 644 ${DESTDIR}${MANDIR}/man5/rdo.conf.5
mkdir -p ${DESTDIR}/etc
@if [ -f ${DESTDIR}/etc/rdo.conf ]; then \
echo "Skipping existing configuration file: ${DESTDIR}/etc/rdo.conf"; \
else \
cp rdo_sample.conf ${DESTDIR}/etc/rdo.conf; \
chmod 644 ${DESTDIR}/etc/rdo.conf; \
fi
uninstall: uninstall:
rm -f ${DESTDIR}${BINDIR}/rdo rm /usr/local/bin/rdo
rm -f ${DESTDIR}${MANDIR}/man1/rdo.1 rm /etc/rdo.conf
rm -f ${DESTDIR}${MANDIR}/man5/rdo.conf.5
clean: clean:
rm rdo rm rdo

View file

@ -1,16 +1,18 @@
# RootDO # RootDO [![AUR](https://img.shields.io/aur/version/rdo.svg)](https://aur.archlinux.org/packages/rdo/)
This project aims to be a very slim alternative to both sudo and doas. This project aims to be a very slim alternative to both sudo and doas.
### Installation ### Installation
If you are on Arch Linux, you can download the package via the [AUR](https://aur.archlinux.org/packages/rdo/).
You can clone and build rdo with the following set of commands: You can clone and build rdo with the following set of commands:
```sh ```sh
git clone https://mrrp.sx7n8.tech/soccera/rdo.git git clone https://codeberg.org/sw1tchbl4d3/rdo
cd rdo cd rdo
make make
rdo make install sudo make install
``` ```
After that, you'll have to configure rdo to allow you to use it. After that, you'll have to configure rdo to allow you to use it.
@ -20,7 +22,7 @@ Then you're good to go!
To uninstall: To uninstall:
```sh ```sh
rdo make uninstall sudo make uninstall
``` ```
### Usage ### Usage
@ -35,8 +37,6 @@ Or, to get the password from stdin:
rdo - [command] rdo - [command]
``` ```
`rdo` will ask for your password and grant you a session, on successful authentication you will be able to use `rdo` for the time specified in `session_ttl`. You have 3 attempts to enter the correct password.
The configuration file has the following variables: The configuration file has the following variables:
``` ```
group=wheel group=wheel
@ -45,9 +45,34 @@ session_ttl=5
``` ```
- `group`: The group of users that is allowed to execute rdo. - `group`: The group of users that is allowed to execute rdo.
- `wrong_pw_sleep`: The amount of milliseconds to sleep at a wrong password attempt. Must be a positive integer. Set to 0 to disable. Defaults to 1000. - `wrong_pw_sleep`: The amount of milliseconds to sleep at a wrong password attempt. Must be a positive integer. Set to 0 to disable.
- `session_ttl`: The amount of minutes a session lasts. Must be a positive integer. Set to 0 to disable. Defaults to 5. - `session_ttl`: The amount of minutes a session lasts. Must be a positive integer. Set to 0 to disable.
### License ### Benchmarks
This project is licensed under the GNU General Public License v3.0. See the `LICENSE` file for the full license text. The benchmark: Execute `whoami` (GNU coreutils 9.1) 10000 times.
Yes, this is a silly benchmark. Yes, the performance gain in real world application is close to nothing.
But it's fun!
|Program|Time|
--- | ---
sudo 1.19.11 | 46.85s
doas 6.8.2 | 32.57s
rdo 1.4.2 | 13.37s
Baseline | 7.95s
> Baseline here is how long it took without any wrapper to make it root.
These benchmarks were done on a `Intel i5 7200U` processor, on a Debian 12 Docker container.
`sudo` and `doas` were pulled from the Debian repos, `rdo` was compiled locally.
All configs were kept as default, except allow the `wheel` group on both + enable `persist` on doas.
The benchmark can be executed through a Docker container by running:
```
make bench-build bench-run
```

View file

@ -1 +0,0 @@
masters = gentoo

67
rdo.1
View file

@ -1,67 +0,0 @@
.TH RDO 1 "August 2025" "rdo 1.4.3" "User Commands"
.SH NAME
rdo \- execute commands as the superuser
.SH SYNOPSIS
.B rdo
[\fB-\fP]
\fIcommand\fP [\fIargs ...\fP]
.SH DESCRIPTION
The
.B rdo
utility allows a user to run a command as the superuser.
.B rdo
authenticates the user by asking for their password.
Once authenticated,
.B rdo
can optionally cache the successful authentication for a configurable duration.
.PP
The security policy is configured in the
.I /etc/rdo.conf
file. This file determines which users are permitted to use
.BR rdo .
.SH OPTIONS
.TP
.B \-
Read the password from standard input instead of the terminal.
.SH EXIT STATUS
The
.B rdo
utility exits with one of the following values:
.TP
\fB0\fP
The usage message was printed and
.B rdo
exited.
.TP
\fB1\fP
An error occurred.
.PP
Otherwise, the exit status is that of the command executed.
.SH FILES
.TP
.I /etc/rdo.conf
The
.B rdo
configuration file.
.SH EXAMPLES
Run the
.I id
command as the superuser:
.IP
.EX
$ rdo id -u
.EE
.PP
Run a shell as the superuser:
.IP
.EX
$ rdo /bin/sh
.EE
.SH SEE ALSO
.BR doas (1),
.BR sudo (8),
.BR rdo.conf (5)
.SH AUTHOR
The
.B rdo
project was created by sw1tchbl4d3 and was heavily modified by coast and soccera.

48
rdo.c
View file

@ -5,31 +5,41 @@
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <errno.h>
#ifdef __linux__ #ifdef __linux__
#include <shadow.h> #include <shadow.h>
#endif #endif
#include "readpassphrase.h" #include "readpassphrase.h"
#include "sessions.h" #include "sessions.h"
#define VERSION "1.4.3" #define VERSION "1.4.3"
char* getpwhash(struct passwd* pw) { char* getpwhash(struct passwd* pw) {
if (pw->pw_passwd[0] != 'x') if (pw->pw_passwd[0] != 'x')
return pw->pw_passwd; return pw->pw_passwd;
#ifdef __linux__ #ifdef __linux__
struct spwd* pw_entry = getspnam(pw->pw_name); struct spwd* pw_entry = getspnam(pw->pw_name);
if (!pw_entry || !pw_entry->sp_pwdp) if (!pw_entry || !pw_entry->sp_pwdp)
err(1, "Could not get shadow entry"); err(1, "Could not get shadow entry");
return pw_entry->sp_pwdp; return pw_entry->sp_pwdp;
#endif #endif
errx(1, "Could not get hashed password entry"); errx(1, "Could not get hashed password entry");
} }
void getconf(FILE* fp, const char* entry, char* result, size_t len_result) { void getconf(FILE* fp, const char* entry, char* result, size_t len_result) {
char* line = NULL; char* line = NULL;
size_t len = 0; size_t len = 0;
size_t entry_len = strlen(entry); size_t entry_len = strlen(entry);
fseek(fp, 0, SEEK_SET); fseek(fp, 0, SEEK_SET);
while (getline(&line, &len, fp) != -1) { while (getline(&line, &len, fp) != -1) {
if (strncmp(line, entry, entry_len) == 0 && (line[entry_len] == '=')) { if (strncmp(line, entry, entry_len) == 0 &&
(line[entry_len] == '=')) {
char* value = line + entry_len + 1; char* value = line + entry_len + 1;
value[strcspn(value, "\n")] = 0; value[strcspn(value, "\n")] = 0;
strncpy(result, value, len_result); strncpy(result, value, len_result);
@ -38,45 +48,62 @@ void getconf(FILE* fp, const char* entry, char* result, size_t len_result) {
return; return;
} }
} }
free(line); free(line);
errx(1, "Could not get '%s' entry in config", entry); errx(1, "Could not get '%s' entry in config", entry);
} }
void runprog(char** program_argv) { void runprog(char** program_argv) {
if (setuid(0) < 0) if (setuid(0) < 0)
err(1, "Could not setuid"); err(1, "Could not setuid");
if (setgid(0) < 0) if (setgid(0) < 0)
err(1, "Could not setgid"); err(1, "Could not setgid");
putenv("HOME=/root"); putenv("HOME=/root");
// NOTE: this does not return when no error occurred.
execvp(program_argv[0], program_argv); execvp(program_argv[0], program_argv);
err(1, "%s", program_argv[0]); err(1, "%s", program_argv[0]);
} }
int main(int argc, char** argv) { int main(int argc, char** argv) {
char groupname[64], wrong_pw_sleep[64], session_ttl[64], password[128]; char groupname[64], wrong_pw_sleep[64], session_ttl[64], password[128];
int sleep_us, tries, ts_ttl; int sleep_us, tries, ts_ttl;
int read_pw_from_stdin = 0; int read_pw_from_stdin = 0;
if (argc > 1) if (argc > 1)
read_pw_from_stdin = strcmp(argv[1], "-") == 0; read_pw_from_stdin = strcmp(argv[1], "-") == 0;
if (argc == 1 || (read_pw_from_stdin && argc == 2)) { if (argc == 1 || (read_pw_from_stdin && argc == 2)) {
printf("RootDO version: %s\n\n", VERSION); printf("RootDO version: %s\n\n", VERSION);
printf("Usage: %s [command]\n", argv[0]); printf("Usage: %s [command]\n", argv[0]);
return 0; return 0;
} }
if (geteuid() != 0) if (geteuid() != 0)
errx(1, "The rdo binary needs to be installed as SUID."); errx(1, "The rdo binary needs to be installed as SUID.");
int ruid = getuid(); int ruid = getuid();
if (ruid == 0) if (ruid == 0)
runprog(&argv[read_pw_from_stdin+1]); runprog(&argv[read_pw_from_stdin+1]);
FILE* fp = fopen("/etc/rdo.conf", "r"); FILE* fp = fopen("/etc/rdo.conf", "r");
if (!fp) if (!fp)
err(1, "Could not open /etc/rdo.conf"); err(1, "Could not open /etc/rdo.conf");
getconf(fp, "group", groupname, sizeof(groupname)); getconf(fp, "group", groupname, sizeof(groupname));
getconf(fp, "wrong_pw_sleep", wrong_pw_sleep, sizeof(wrong_pw_sleep)); getconf(fp, "wrong_pw_sleep", wrong_pw_sleep, sizeof(wrong_pw_sleep));
getconf(fp, "session_ttl", session_ttl, sizeof(session_ttl)); getconf(fp, "session_ttl", session_ttl, sizeof(session_ttl));
sleep_us = atoi(wrong_pw_sleep) * 1000; sleep_us = atoi(wrong_pw_sleep) * 1000;
ts_ttl = atoi(session_ttl) * 60; ts_ttl = atoi(session_ttl) * 60;
fclose(fp); fclose(fp);
if (getsession(ts_ttl) == 0 && !read_pw_from_stdin) if (getsession(ts_ttl) == 0 && !read_pw_from_stdin)
runprog(&argv[1]); runprog(&argv[1]);
struct passwd* pw = getpwuid(ruid); struct passwd* pw = getpwuid(ruid);
if (!pw) { if (!pw) {
if (errno == 0) if (errno == 0)
@ -84,40 +111,46 @@ int main(int argc, char** argv) {
else else
err(1, "Could not get user info"); err(1, "Could not get user info");
} }
struct group* current_group_entry = getgrent(); struct group* current_group_entry = getgrent();
while (current_group_entry) { while (current_group_entry) {
if (strcmp(current_group_entry->gr_name, groupname) == 0) if (strcmp(current_group_entry->gr_name, groupname) == 0)
break; break;
current_group_entry = getgrent(); current_group_entry = getgrent();
} }
if (!current_group_entry) if (!current_group_entry)
errx(1, "The group '%s' does not exist.", groupname); errx(1, "The group '%s' does not exist.", groupname);
char* current_member = current_group_entry->gr_mem[0]; char* current_member = current_group_entry->gr_mem[0];
for (int i = 1; current_member; i++) { for (int i = 1; current_member; i++) {
if (strcmp(current_member, pw->pw_name) == 0) if (strcmp(current_member, pw->pw_name) == 0)
break; break;
current_member = current_group_entry->gr_mem[i]; current_member = current_group_entry->gr_mem[i];
} }
if (!current_member) if (!current_member)
errx(1, "You are not allowed to execute rdo."); errx(1, "You are not allowed to execute rdo.");
char* user_hashed_pw = getpwhash(pw); char* user_hashed_pw = getpwhash(pw);
tries = 0; tries = 0;
while (tries < 3) { while (tries < 3) {
char hostname[64]; if (!readpassphrase("(rdo) Password: ", password, sizeof(password), read_pw_from_stdin))
gethostname(hostname, sizeof(hostname));
char prompt[256];
snprintf(prompt, sizeof(prompt), "[rdo: (%s@%s) Password]: ", pw->pw_name, hostname);
if (!readpassphrase(prompt, password, sizeof(password), read_pw_from_stdin))
err(1, "Could not get passphrase"); err(1, "Could not get passphrase");
char* given_hashed_pw = crypt(password, user_hashed_pw); char* given_hashed_pw = crypt(password, user_hashed_pw);
memset(password, 0, sizeof(password)); memset(password, 0, sizeof(password));
if (!given_hashed_pw) if (!given_hashed_pw)
errx(1, "Could not hash password, does your user have a password?"); errx(1, "Could not hash password, does your user have a password?");
if (strcmp(given_hashed_pw, user_hashed_pw) == 0) { if (strcmp(given_hashed_pw, user_hashed_pw) == 0) {
if (!read_pw_from_stdin) if (!read_pw_from_stdin)
setsession(ts_ttl); setsession(ts_ttl);
runprog(&argv[read_pw_from_stdin+1]); runprog(&argv[read_pw_from_stdin+1]);
} }
usleep(sleep_us); usleep(sleep_us);
fprintf(stderr, "Wrong password.\n"); fprintf(stderr, "Wrong password.\n");
tries++; tries++;
@ -125,4 +158,3 @@ int main(int argc, char** argv) {
errx(1, "Too many wrong password attempts."); errx(1, "Too many wrong password attempts.");
return 1; return 1;
} }

View file

@ -1,29 +0,0 @@
.TH RDO.CONF 5 "August 2025" "rdo 1.4.3" "File Formats"
.SH NAME
rdo.conf \- configuration file for rdo
.SH DESCRIPTION
The
.B rdo
utility reads the
.I /etc/rdo.conf
file for its configuration.
.PP
The file consists of
.I variable=value
pairs. Comments are not supported. Leading and trailing whitespace is ignored.
.SH VARIABLES
.TP
.B group=\fIgroup\fP
Specifies the group whose members are allowed to run
.BR rdo .
This is a mandatory variable.
.TP
.B wrong_pw_sleep=\fImilliseconds\fP
The amount of time in milliseconds to wait after a wrong password attempt before prompting again. If not set, the default is 1000. Set to 0 to disable.
.TP
.B session_ttl=\fIminutes\fP
The time to live in minutes for a cached authentication. If a user successfully authenticates, they can run
.B rdo
without a password for this duration. If not set, the default is 5. Set to 0 to disable session caching.
.SH SEE ALSO
.BR rdo (1)