On Mar 7, 2026, at 3:26 AM, Philip Guenther <guenther gmail ! com> wrote:
> Given what we know now, would it have been better if pledge() only
> took one argument and there was a separate pledgeexec() syscall that
> only ldd called? I'll answer with a fully qualified "maybe"
For whatever it is worth, I find the second argument to pledge to be
extremely useful. To give one example, I use it for sandboxing Rust
development and the associated building of dependencies from crates.io.
Without it, I would have to resort to using chroot, an approach both
inconvenient (due to having to maintain the chroot) and less secure (due
to the lack of syscall filtering). It seems like the prevailing opinion
is that the second argument to pledge was a mistake, but I really hope
it sticks around: It's useful for the same reasons bubblewrap is useful
on Linux.
Here's an approach to sandboxing Rust development that seems to work
very well for my purposes (and could easily be generalized):
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>
static _Noreturn void die_alloc(void);
int
main(int const argc, char **const argv)
{
if (argc != 2 || !argv[1]) {
fputs("usage: rbox DIR\n", stderr);
exit(1);
}
char *const home_path = getenv("HOME");
if (!home_path) {
fputs("error: HOME environment variable not set\n", stderr);
exit(1);
}
char *alloced_sandbox_path = realpath(argv[1], NULL);
if (!alloced_sandbox_path) {
fputs("error: could not resolve provided path\n", stderr);
exit(1);
}
if (chdir(alloced_sandbox_path)) {
fputs("error: could not chdir to provided path\n", stderr);
exit(1);
}
char *alloced_cargo_path;
if (asprintf(&alloced_cargo_path, "%s/.cargo", home_path) == -1) {
die_alloc();
}
char *alloced_tmp_path;
if (
asprintf(
&alloced_tmp_path,
"%s/target",
alloced_sandbox_path
) == -1
) {
die_alloc();
}
if ( unveil("/bin", "rx")
|| unveil("/etc/ssl/cert.pem", "r")
|| unveil("/usr", "r")
|| unveil("/usr/bin/cc", "rx")
|| unveil("/usr/bin/ld", "rx")
|| unveil("/usr/local", "rx")
|| unveil(alloced_cargo_path, "crwx")
|| unveil(alloced_sandbox_path, "crwx")
|| unveil(NULL, NULL)
) {
fputs("error: could not unveil required paths\n", stderr);
exit(1);
}
if (
pledge(
NULL,
"stdio rpath wpath cpath fattr inet "
"flock unix dns tty proc exec error"
)
) {
fputs("error: could not pledge\n", stderr);
exit(1);
}
if ( setenv("LD_LIBRARY_PATH", "/usr/local/lib", 1)
|| setenv("TMPDIR", alloced_tmp_path, 1)
) {
fputs("error: could not set environment\n", stderr);
exit(1);
}
free(alloced_sandbox_path);
free(alloced_cargo_path);
free(alloced_tmp_path);
puts("\033[92mEntering sandbox.\033[0m");
fflush(stdout);
pid_t const pid = fork();
if (pid == -1) {
fputs("error: could not fork\n", stderr);
exit(1);
}
if (!pid) {
if (execl("/bin/sh", "/bin/sh", NULL)) {
fputs("error: could not exec\n", stderr);
exit(1);
}
}
int status;
waitpid(pid, &status, 0);
puts("\033[93mLeaving sandbox.\033[0m");
if (WIFEXITED(status)) {
exit(WEXITSTATUS(status));
}
fputs("error: sandbox did not exit normally\n", stderr);
exit(1);
}
static _Noreturn void
die_alloc(void)
{
fputs("error: insufficient memory\n", stderr);
exit(1);
}