doos 1.0.0: images, size cap, subuid ids, working apt in host mode

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-23 21:41:28 +02:00
co-authored by Claude Sonnet 5.5
parent fdb96c7b1c
commit d446c29372
24 changed files with 1709 additions and 142 deletions
+122 -25
View File
@@ -16,6 +16,11 @@
#include <cstring>
#include <poll.h>
#include <chrono>
#include "cgroup.hpp"
#include "spec.hpp"
#include "util.hpp"
@@ -26,8 +31,10 @@ namespace doos {
namespace {
volatile sig_atomic_t g_forwardTo = 0;
volatile sig_atomic_t g_terminating = 0;
void forwardSignal(int sig) {
g_terminating = 1;
if (g_forwardTo > 0) ::kill(g_forwardTo, sig);
}
@@ -95,17 +102,24 @@ void bindHostDevice(const fs::path& root, const std::string& dev) {
doMount(("/dev/" + dev).c_str(), target, nullptr, MS_BIND);
}
void setupRoot(const Container& c) {
fs::path root = c.rootfs();
doMount(nullptr, "/", nullptr, MS_REC | MS_PRIVATE);
doMount(root.c_str(), root.string(), nullptr, MS_BIND | MS_REC);
// apt drops to its _apt user for downloads; with one mappable id that user does not exist.
void writeAptSandboxOverride(const Container& c, const fs::path& lowerEtc) {
std::error_code ec;
if (c.ids() != IdMode::Single || !fs::is_directory(lowerEtc / "apt", ec)) return;
fs::path dir = c.etcDir() / "apt/apt.conf.d";
fsutil::mkdirs(dir);
fsutil::writeFile(dir / "99doos-sandbox", "APT::Sandbox::User \"root\";\n");
}
// resolv.conf lives in the /etc upperdir so it matches the network mode on every start.
fs::path resolv = c.layer("/etc") / "upper" / "resolv.conf";
void writeResolvConf(const Container& c) {
fs::path resolv = c.etcDir() / "resolv.conf";
fsutil::mkdirs(resolv.parent_path());
std::error_code ec;
fs::remove(resolv, ec);
fsutil::writeFile(resolv, Network::resolvConf(c.net()));
}
void mountHostLayers(const Container& c, const fs::path& root) {
for (auto d : spec::kOverlayDirs) {
std::error_code e;
if (fs::is_directory(std::string(d), e)) mountOverlay(c, root, d);
@@ -125,6 +139,32 @@ void setupRoot(const Container& c) {
}
}
for (auto d : spec::kPrivateDirs) doMount(c.privateDir(std::string(d)).c_str(), sub(root, d), nullptr, MS_BIND);
// overlays that live inside a private dir must be mounted after it
for (auto d : spec::kVarOverlayDirs) {
std::error_code e;
if (fs::is_directory(std::string(d), e)) mountOverlay(c, root, d);
}
}
void mountImageRoot(const Container& c, const fs::path& root, const fs::path& imageRoot) {
std::string opts = "lowerdir=" + imageRoot.string() + ",upperdir=" + (c.layer("/root") / "upper").string() +
",workdir=" + (c.layer("/root") / "work").string() + ",userxattr";
doMount("overlay", root.string(), "overlay", MS_NODEV | MS_NOSUID, opts);
for (auto d : spec::kEmptyDirs) fsutil::mkdirs(sub(root, d));
}
void setupRoot(const Container& c, const fs::path& imageRoot) {
fs::path root = c.rootfs();
doMount(nullptr, "/", nullptr, MS_REC | MS_PRIVATE);
writeResolvConf(c);
if (c.imageMode()) {
writeAptSandboxOverride(c, imageRoot / "etc");
mountImageRoot(c, root, imageRoot);
} else {
writeAptSandboxOverride(c, "/etc");
doMount(root.c_str(), root.string(), nullptr, MS_BIND | MS_REC);
mountHostLayers(c, root);
}
// tmpfs mounts
doMount("tmpfs", sub(root, "/tmp"), "tmpfs", MS_NOSUID | MS_NODEV, "mode=1777,size=" + std::string(spec::kTmpSize));
@@ -179,7 +219,9 @@ void applyLimitsAndEnv(const Container& c) {
::setenv("HOSTNAME", c.name().c_str(), 1);
::setenv("container", "doos", 1);
::setenv("PS1", ("(doos:" + c.name() + ") \\u@\\h:\\w\\$ ").c_str(), 1);
if (!langv.empty()) ::setenv("LANG", langv.c_str(), 1);
// The host's locale usually does not exist inside an image; C.UTF-8 is built into glibc and musl.
if (c.imageMode()) ::setenv("LANG", "C.UTF-8", 1);
else if (!langv.empty()) ::setenv("LANG", langv.c_str(), 1);
}
[[noreturn]] void execCommand(std::vector<std::string> cmd) {
@@ -202,6 +244,7 @@ struct ChildCtx {
const Container* container;
const std::vector<std::string>* command;
int syncFd;
fs::path imageRoot;
int lockFd; // inherited copy must be closed or the flock outlives unlock()
};
@@ -216,7 +259,7 @@ int containerInit(void* arg) {
::close(ctx->syncFd);
const Container& c = *ctx->container;
setupRoot(c);
setupRoot(c, ctx->imageRoot);
if (::sethostname(c.name().c_str(), c.name().size()) != 0) throw SysError("sethostname");
if (Network::isolated(c.net())) bringLoopbackUp();
applyLimitsAndEnv(c);
@@ -248,17 +291,6 @@ int containerInit(void* arg) {
}
}
void writeMap(pid_t pid, const char* file, const std::string& content) {
std::string path = "/proc/" + std::to_string(pid) + "/" + file;
int fd = ::open(path.c_str(), O_WRONLY | O_CLOEXEC);
if (fd < 0) throw SysError("open " + path);
Fd guard(fd);
if (::write(fd, content.data(), content.size()) != static_cast<ssize_t>(content.size())) {
throw SysError("write " + path +
" (user namespaces blocked? see apparmor/doos and docs/RESEARCH.md)");
}
}
} // namespace
int Sandbox::run() {
@@ -273,6 +305,14 @@ int Sandbox::run() {
int Sandbox::start(FileLock& lock) {
NetMode net = container_.net();
fs::path imageRoot;
if (container_.imageMode()) {
Store store = Store::fromEnvironment();
imageRoot = store.images().rootfs(container_.image());
if (!fs::is_directory(imageRoot)) {
throw std::runtime_error("image '" + container_.image() + "' is missing; add it again with: doos image add");
}
}
int flags = CLONE_NEWUSER | CLONE_NEWNS | CLONE_NEWPID | CLONE_NEWUTS | CLONE_NEWIPC | SIGCHLD;
if (Network::isolated(net)) flags |= CLONE_NEWNET;
@@ -281,7 +321,7 @@ int Sandbox::start(FileLock& lock) {
Fd syncRead(sync[0]), syncWrite(sync[1]);
std::vector<char> stack(1 << 20);
ChildCtx ctx{&container_, &command_, syncRead.get(), lock.fd()};
ChildCtx ctx{&container_, &command_, syncRead.get(), imageRoot, lock.fd()};
pid_t child = ::clone(containerInit, stack.data() + stack.size(), flags, &ctx);
if (child < 0) throw SysError("clone (user namespaces available?)");
@@ -297,13 +337,16 @@ int Sandbox::start(FileLock& lock) {
}
} reaper{child};
writeMap(child, "setgroups", "deny");
writeMap(child, "uid_map", "0 " + std::to_string(::geteuid()) + " 1\n");
writeMap(child, "gid_map", "0 " + std::to_string(::getegid()) + " 1\n");
IdMap::apply(child, container_.ids());
SlirpSession slirp;
if (net == NetMode::Slirp) slirp.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());
container_.recordPid(child);
struct PidGuard {
const Container& c;
@@ -324,12 +367,66 @@ int Sandbox::start(FileLock& lock) {
::sigaction(SIGQUIT, &ign, nullptr);
installForwarding(child, {SIGTERM, SIGHUP});
int status = watch(child, cgroup.get());
reaper.armed = false;
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) {
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();
for (;;) {
if (pidfd.valid()) {
pollfd p{pidfd.get(), POLLIN, 0};
int r = ::poll(&p, 1, spec::kSizeCheckIntervalMs);
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);
}
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;
std::size_t skipped = 0;
std::uint64_t used = container_.usedBytes(skipped);
const char* name = container_.name().c_str();
if (used >= cap) {
if (cgroup && !cgroup->frozen()) {
cgroup->freeze(true);
std::fprintf(stderr, "doos: '%s' reached its size cap (%s of %s): FROZEN. Free space with 'doos attach %s -- ...'; it thaws below %d%%.\n",
name, formatSize(used).c_str(), formatSize(cap).c_str(), name, spec::kSizeThawPercent);
} else if (!cgroup && !warned) {
std::fprintf(stderr, "doos: '%s' is over its size cap (%s of %s), cannot freeze\n", name,
formatSize(used).c_str(), formatSize(cap).c_str());
warned = true;
}
} else if (used * 100 >= cap * spec::kSizeWarnPercent) {
if (!warned) {
std::fprintf(stderr, "doos: '%s' uses %s of its %s size cap\n", name, formatSize(used).c_str(),
formatSize(cap).c_str());
warned = true;
}
} else {
warned = false;
}
if (cgroup && cgroup->frozen() && used * 100 < cap * spec::kSizeThawPercent) {
cgroup->freeze(false);
std::fprintf(stderr, "doos: '%s' thawed (%s used)\n", name, formatSize(used).c_str());
}
}
int status = 0;
while (::waitpid(child, &status, 0) < 0) {
if (errno != EINTR) throw SysError("waitpid");
}
reaper.armed = false;
return exitCodeOf(status);
return status;
}
int Sandbox::join(pid_t initPid, FileLock& lock) {