doos 1.1.0: limits, hardening, pasta, signatures, Debian image

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-23 21:59:26 +02:00
co-authored by Claude Sonnet 5.5
parent d446c29372
commit 530582e826
24 changed files with 1182 additions and 127 deletions
+45 -10
View File
@@ -17,10 +17,12 @@
#include <cstring>
#include <poll.h>
#include <algorithm>
#include <chrono>
#include "cgroup.hpp"
#include "hardening.hpp"
#include "spec.hpp"
#include "util.hpp"
@@ -200,12 +202,16 @@ void applyLimitsAndEnv(const Container& c) {
if (::prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0) throw SysError("no_new_privs");
rlimit core{0, 0};
::setrlimit(RLIMIT_CORE, &core);
// No single file can outgrow the size cap, which bounds the overshoot between two usage scans.
rlimit fsize{c.sizeCap(), c.sizeCap()};
::setrlimit(RLIMIT_FSIZE, &fsize);
rlimit nofile;
if (::getrlimit(RLIMIT_NOFILE, &nofile) == 0) {
if (nofile.rlim_max > spec::kMaxOpenFiles) nofile.rlim_max = spec::kMaxOpenFiles;
if (nofile.rlim_cur > nofile.rlim_max) nofile.rlim_cur = nofile.rlim_max;
::setrlimit(RLIMIT_NOFILE, &nofile);
}
if (!c.relaxed()) Hardening::apply();
const char* term = std::getenv("TERM");
std::string termv = term && *term ? term : "xterm";
const char* lang = std::getenv("LANG");
@@ -340,12 +346,20 @@ int Sandbox::start(FileLock& lock) {
IdMap::apply(child, container_.ids());
SlirpSession slirp;
PastaSession pasta;
if (net == NetMode::Slirp) slirp.start(child);
if (net == NetMode::Pasta) pasta.start(child);
std::string cgroupWhy;
std::unique_ptr<Cgroup> cgroup = Cgroup::create(container_.name(), cgroupWhy);
if (cgroup) cgroup->add(child);
else std::fprintf(stderr, "doos: size cap only warns, cannot freeze: %s\n", cgroupWhy.c_str());
Limits limits{container_.memoryCap(), container_.pidsCap(), container_.milliCpus()};
std::string cgroupWhy, notEnforced;
std::unique_ptr<Cgroup> cgroup = Cgroup::create(container_.name(), limits, cgroupWhy, notEnforced);
if (cgroup) {
cgroup->addMain(child);
if (!notEnforced.empty()) std::fprintf(stderr, "doos: limits not enforced (controller missing): %s\n", notEnforced.c_str());
} else {
std::fprintf(stderr, "doos: memory, pids and cpu limits are not enforced and the size cap only warns: %s\n",
cgroupWhy.c_str());
}
container_.recordPid(child);
struct PidGuard {
@@ -369,34 +383,50 @@ int Sandbox::start(FileLock& lock) {
int status = watch(child, cgroup.get());
reaper.armed = false;
if (cgroup && cgroup->oomKills() > 0) {
std::fprintf(stderr, "doos: '%s' was killed by its memory limit (%s); raise it with --memory\n",
container_.name().c_str(), formatSize(container_.memoryCap()).c_str());
}
if (cgroup && cgroup->pidFailures() > 0) {
std::fprintf(stderr, "doos: '%s' hit its process limit (%u) %llu times; raise it with --pids\n",
container_.name().c_str(), container_.pidsCap(),
static_cast<unsigned long long>(cgroup->pidFailures()));
}
return exitCodeOf(status);
}
// Waits for the container's init while enforcing the size cap: warn at kSizeWarnPercent, freeze at the cap,
// thaw again once usage drops below kSizeThawPercent. Nothing is ever deleted by this loop.
int Sandbox::watch(pid_t child, Cgroup* cgroup) {
using Clock = std::chrono::steady_clock;
long fd = ::syscall(SYS_pidfd_open, child, 0);
Fd pidfd(static_cast<int>(fd));
const std::uint64_t cap = container_.sizeCap();
bool warned = false;
auto lastScan = std::chrono::steady_clock::now();
auto nextScan = Clock::now() + std::chrono::milliseconds(spec::kSizeCheckMinIntervalMs);
for (;;) {
auto left = std::chrono::duration_cast<std::chrono::milliseconds>(nextScan - Clock::now()).count();
int timeout = static_cast<int>(std::max<long>(left, 50));
if (pidfd.valid()) {
pollfd p{pidfd.get(), POLLIN, 0};
int r = ::poll(&p, 1, spec::kSizeCheckIntervalMs);
int r = ::poll(&p, 1, timeout);
if (r > 0) break;
if (r < 0 && errno != EINTR) break;
} else {
int st = 0;
if (::waitpid(child, &st, WNOHANG) == child) return st;
::poll(nullptr, 0, 1000);
::poll(nullptr, 0, std::min(timeout, 1000));
}
if (g_terminating && cgroup && cgroup->frozen()) cgroup->freeze(false); // a frozen init cannot see SIGTERM
auto now = std::chrono::steady_clock::now();
if (now - lastScan < std::chrono::milliseconds(spec::kSizeCheckIntervalMs)) continue;
lastScan = now;
if (Clock::now() < nextScan) continue;
auto began = Clock::now();
std::size_t skipped = 0;
std::uint64_t used = container_.usedBytes(skipped);
// The scan may use at most 1/kSizeScanDutyFactor of the time: big trees are scanned less often.
auto took = Clock::now() - began;
nextScan = Clock::now() + std::max(std::chrono::milliseconds(spec::kSizeCheckMinIntervalMs),
std::chrono::duration_cast<std::chrono::milliseconds>(took) * spec::kSizeScanDutyFactor);
const char* name = container_.name().c_str();
if (used >= cap) {
if (cgroup && !cgroup->frozen()) {
@@ -444,10 +474,15 @@ int Sandbox::join(pid_t initPid, FileLock& lock) {
if (!rootFd.valid()) throw SysError("open container root");
lock.unlock();
// Later attaches live in the `shell` leaf: same limits as the container, but never frozen.
std::unique_ptr<Cgroup> cgroup = Cgroup::open(container_.name());
pid_t child = ::fork();
if (child < 0) throw SysError("fork");
if (child == 0) {
try {
if (cgroup) {
try { cgroup->addShell(::getpid()); } catch (const std::exception&) { /* limits only; keep going */ }
}
for (int i = 0; i < 6; ++i) {
if (fds[i].valid() && ::setns(fds[i].get(), kinds[i]) != 0) {
throw SysError(std::string("setns ") + names[i]);