Compare commits

..

4 Commits

Author SHA1 Message Date
ba3e32917e
init: Support starting services as a separate user
Some checks failed
continuous-integration/drone/push Build is failing
2023-08-14 10:46:45 +02:00
cfb60fad25
init: Use pledge and support init --user 2023-08-14 10:46:28 +02:00
9954fc1658
libos: Add a pledge wrapper 2023-08-14 10:45:00 +02:00
a98df9e743
kernel: Return EACCES when trying to apply execpromises to a setuid program
Closes #41.
2023-08-14 09:50:52 +02:00
5 changed files with 178 additions and 15 deletions

View File

@ -4,9 +4,12 @@
#include <luna/Sort.h>
#include <luna/String.h>
#include <luna/Vector.h>
#include <os/ArgumentParser.h>
#include <os/Directory.h>
#include <os/File.h>
#include <os/Process.h>
#include <os/Security.h>
#include <pwd.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
@ -17,6 +20,8 @@
#include <sys/sysmacros.h>
#include <unistd.h>
static bool g_is_system = false;
FILE* g_init_log = nullptr;
// Request a successful exit from the system (for tests)
@ -40,6 +45,8 @@ struct Service
String standard_output;
String standard_error;
String standard_input;
Option<uid_t> user {};
Option<gid_t> group {};
bool wait { false };
Option<pid_t> pid {};
};
@ -65,6 +72,12 @@ static Result<void> service_child(const Service& service, SharedPtr<os::File> ou
if (error) dup2(error->fd(), STDERR_FILENO);
if (input) dup2(input->fd(), STDIN_FILENO);
if (service.user.has_value())
{
setgid(service.group.value());
setuid(service.user.value());
}
if (service.environment.is_empty()) { TRY(os::Process::exec(args[0].view(), args.slice(), false)); }
else
{
@ -214,6 +227,15 @@ static Result<void> load_service(const os::Path& path)
continue;
}
if (g_is_system && parts[0].view() == "User")
{
auto* pw = getpwnam(parts[1].chars());
if (!pw) continue;
service.user = pw->pw_uid;
service.group = pw->pw_gid;
continue;
}
if (parts[0].view() == "Wait")
{
if (parts[1].view() == "true" || parts[1].view().to_uint().value_or(0) == 1)
@ -247,9 +269,9 @@ static Result<void> load_service(const os::Path& path)
return {};
}
static Result<void> load_services()
static Result<void> load_services(StringView path)
{
auto dir = TRY(os::Directory::open("/etc/init"));
auto dir = TRY(os::Directory::open(path));
auto services = TRY(dir->list_names(os::Directory::Filter::ParentAndBase));
sort(services.begin(), services.end(), String::compare);
@ -259,9 +281,9 @@ static Result<void> load_services()
return {};
}
static Result<void> start_services()
static Result<void> start_services(StringView path)
{
TRY(load_services());
TRY(load_services(path));
for (auto& service : g_services)
{
do_log("[init] starting service %s\n", service.name.chars());
@ -301,7 +323,7 @@ static void mount_shmfs()
if (chmod("/dev/shm", 01777) < 0) exit(255);
}
int main()
Result<int> sysinit()
{
if (getpid() != 1)
{
@ -309,12 +331,16 @@ int main()
return 1;
}
g_is_system = true;
// Before this point, we don't even have an stdin, stdout and stderr. Set it up now so that child processes (and us)
// can print stuff.
stdin = fopen("/dev/console", "r");
stdout = fopen("/dev/console", "w");
stderr = fopen("/dev/console", "w");
TRY(os::Security::pledge("stdio rpath wpath cpath fattr host mount proc exec signal", nullptr));
mount_tmpfs();
mount_shmfs();
@ -330,7 +356,11 @@ int main()
if (signal(SIGTERM, sigterm_handler) == SIG_ERR) do_log("[init] failed to register handler for SIGTERM\n");
if (signal(SIGQUIT, sigquit_handler) == SIG_ERR) do_log("[init] failed to register handler for SIGQUIT\n");
start_services();
TRY(os::Security::pledge("stdio rpath wpath cpath proc exec", nullptr));
start_services("/etc/init");
TRY(os::Security::pledge("stdio rpath wpath proc exec", nullptr));
while (1)
{
@ -365,3 +395,66 @@ int main()
}
}
}
Result<int> user_init()
{
setpgid(0, 0);
g_init_log = fopen("/dev/uart0", "w");
check(g_init_log);
setlinebuf(g_init_log);
fcntl(fileno(g_init_log), F_SETFD, FD_CLOEXEC);
TRY(os::Security::pledge("stdio rpath wpath cpath proc exec", nullptr));
start_services("/etc/user");
TRY(os::Security::pledge("stdio rpath wpath proc exec", nullptr));
while (1)
{
int status;
auto rc = os::Process::wait(os::Process::ANY_CHILD, &status);
if (rc.has_error()) continue;
pid_t child = rc.release_value();
for (auto& service : g_services)
{
if (service.pid.has_value() && service.pid.value() == child)
{
if (WIFEXITED(status))
{
do_log("[init] service %s exited with status %d\n", service.name.chars(), WEXITSTATUS(status));
}
else
{
do_log("[init] service %s was terminated by signal %d\n", service.name.chars(), WTERMSIG(status));
}
if (service.restart)
{
do_log("[init] restarting service %s\n", service.name.chars());
start_service(service);
}
break;
}
}
}
}
Result<int> luna_main(int argc, char** argv)
{
bool user;
os::ArgumentParser parser;
parser.add_description("The init system for Luna.");
parser.add_system_program_info("init"_sv);
parser.add_switch_argument(user, 'u', "user"_sv, "initialize a user session instead of the system");
parser.parse(argc, argv);
if (user) return user_init();
return sysinit();
}

View File

@ -74,6 +74,11 @@ Result<u64> sys_execve(Registers* regs, SyscallArgs args)
kdbgln("exec: attempting to replace current image with %s", path.chars());
#endif
bool is_setuid = VFS::is_setuid(inode);
bool is_setgid = VFS::is_setgid(inode);
bool is_secure_environment = is_setgid || is_setuid;
if (is_secure_environment && current->execpromises >= 0) return err(EACCES);
auto loader = TRY(BinaryFormat::create_loader(inode));
#ifdef EXEC_DEBUG
@ -107,8 +112,8 @@ Result<u64> sys_execve(Registers* regs, SyscallArgs args)
if (descriptor->flags & O_CLOEXEC) { descriptor = {}; }
}
if (VFS::is_setuid(inode)) current->auth.euid = current->auth.suid = inode->metadata().uid;
if (VFS::is_setgid(inode)) current->auth.egid = current->auth.sgid = inode->metadata().gid;
if (is_setuid) current->auth.euid = current->auth.suid = inode->metadata().uid;
if (is_setgid) current->auth.egid = current->auth.sgid = inode->metadata().gid;
current->name = path.chars();
@ -160,8 +165,6 @@ Result<u64> sys_fork(Registers* regs, SyscallArgs)
thread->current_directory_path = move(current_directory_path);
thread->umask = current->umask;
thread->parent = current;
// TODO: Should promises be inherited across fork()? We're assuming yes, as they're already reset on exec (unless
// execpromises has been set). Couldn't find any suitable documentation from OpenBSD about this.
thread->promises = current->promises;
thread->execpromises = current->execpromises;
@ -171,11 +174,7 @@ Result<u64> sys_fork(Registers* regs, SyscallArgs)
memcpy(&thread->regs, regs, sizeof(*regs));
for (int i = 0; i < NSIG; i++)
{
auto sighandler = current->signal_handlers[i].sa_handler;
thread->signal_handlers[i] = { .sa_handler = sighandler, .sa_mask = 0, .sa_flags = 0 };
}
for (int i = 0; i < NSIG; i++) thread->signal_handlers[i] = current->signal_handlers[i];
thread->signal_mask = current->signal_mask;
thread->set_return(0);

View File

@ -13,6 +13,7 @@ set(SOURCES
src/Path.cpp
src/Mode.cpp
src/Prompt.cpp
src/Security.cpp
)
add_library(os ${SOURCES})

View File

@ -0,0 +1,48 @@
/**
* @file Security.h
* @author apio (cloudapio.eu)
* @brief Functions to restrict process operations.
*
* @copyright Copyright (c) 2023, the Luna authors.
*
*/
#pragma once
#include <luna/Result.h>
namespace os
{
namespace Security
{
/**
* @brief Restrict system operations.
*
* The pledge() call, borrowed from OpenBSD, is a simple function to sandbox a process effectively.
*
* Syscalls are divided into a number of categories ("promises"), the following are the ones implemented on
* Luna: stdio rpath wpath cpath fattr chown unix tty proc exec prot_exec id mount signal host error
*
* The way pledge() works is: the process "tells" the kernel which subset of functions it will use, and if it
* suddenly uses something it has not promised (probably because the process was hacked, using ROP or something
* else) the kernel kills the process immediately with an uncatchable SIGABRT. Alternatively, if the process has
* pledged the "error" promise, the call will fail with ENOSYS.
*
* Pledges are not inherited across exec, although one may specify another set of promises to apply on the next
* execve() call. Thus, pledge() is not a way to restrict untrusted programs (unless the "exec" pledge is
* removed), but more of a way to protect trusted local programs from vulnerabilities.
*
* One may call pledge() several times, but only to remove promises, not to add them.
*
* A typical call to pledge would look like this:
*
* TRY(os::Security::pledge("stdio rpath wpath unix proc", nullptr));
*
* @param promises The promises to apply immediately, separated by spaces. If empty, the process may only call
* _exit(2). If NULL, the promises are not changed.
* @param execpromises The promises to apply on the next call to execve(2), separated by spaces. If empty, the
* process may only call _exit(2). If NULL, the execpromises are not changed.
* @return Result<void> Whether the operation succeded.
*/
Result<void> pledge(const char* promises, const char* execpromises);
}
}

22
libos/src/Security.cpp Normal file
View File

@ -0,0 +1,22 @@
/**
* @file Security.cpp
* @author apio (cloudapio.eu)
* @brief Functions to restrict process operations.
*
* @copyright Copyright (c) 2023, the Luna authors.
*
*/
#include <errno.h>
#include <os/Security.h>
#include <unistd.h>
namespace os::Security
{
Result<void> pledge(const char* promises, const char* execpromises)
{
int rc = ::pledge(promises, execpromises);
if (rc < 0) return err(errno);
return {};
}
}