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:
+122
-25
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user