diff --git a/Makefile b/Makefile index 04b2536..caf0d99 100644 --- a/Makefile +++ b/Makefile @@ -72,11 +72,13 @@ ifneq (,$(filter $(I),admin ci)) docker run -it --rm --hostname $(I) -v $(VOLUME_MOUNT):/src -v /var/run/docker.sock:/var/run/docker.sock -v $(HOME)/.aws:/var/riju/.aws -v $(HOME)/.docker:/var/riju/.docker -v $(HOME)/.ssh:/var/riju/.ssh -v $(HOME)/.terraform.d:/var/riju/.terraform.d -e AWS_REGION -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e DOCKER_USERNAME -e DOCKER_PASSWORD -e DEPLOY_SSH_PRIVATE_KEY -e DOCKER_REPO -e S3_BUCKET -e DOMAIN -e VOLUME_MOUNT=$(VOLUME_MOUNT) $(SHELL_PORTS) $(SHELL_ENV) --network host riju:$(I) $(BASH_CMD) else ifeq ($(I),app) docker run -it --rm --hostname $(I) $(SHELL_PORTS) $(SHELL_ENV) riju:$(I) $(BASH_CMD) -else ifneq (,$(filter $(I),runtime lang)) +else ifneq (,$(filter $(I),base lang)) ifeq ($(I),lang) @: $${L} endif docker run -it --rm --hostname $(I) -v $(VOLUME_MOUNT):/src --label riju-install-target=yes $(SHELL_PORTS) $(SHELL_ENV) riju:$(LANG_TAG) $(BASH_CMD) +else ifeq ($(I),runtime) + docker run -it --rm --hostname $(I) -v $(VOLUME_MOUNT):/src -v /var/run/docker.sock:/var/run/docker.sock $(SHELL_PORTS) $(SHELL_ENV) riju:$(I) $(BASH_CMD) else docker run -it --rm --hostname $(I) -v $(VOLUME_MOUNT):/src $(SHELL_PORTS) $(SHELL_ENV) riju:$(I) $(BASH_CMD) endif diff --git a/backend/api.js b/backend/api.js index ddc16ad..86cdb0c 100644 --- a/backend/api.js +++ b/backend/api.js @@ -6,12 +6,10 @@ import pty from "node-pty"; import pQueue from "p-queue"; const PQueue = pQueue.default; import rpc from "vscode-jsonrpc"; -import { v4 as getUUID } from "uuid"; import { langs } from "./langs.js"; -import { borrowUser } from "./users.js"; import * as util from "./util.js"; -import { bash } from "./util.js"; +import { bash, getUUID } from "./util.js"; const allSessions = new Set(); @@ -24,16 +22,8 @@ export class Session { return langs[this.lang]; } - get uid() { - return this.uidInfo.uid; - } - - returnUser = async () => { - this.uidInfo && (await this.uidInfo.returnUser()); - }; - get context() { - return { uid: this.uid, uuid: this.uuid }; + return { uuid: this.uuid, lang: this.lang }; } log = (msg) => this.logPrimitive(`[${this.uuid}] ${msg}`); @@ -43,7 +33,7 @@ export class Session { this.uuid = getUUID(); this.lang = lang; this.tearingDown = false; - this.uidInfo = null; + this.container = null; this.term = null; this.lsp = null; this.daemon = null; @@ -57,24 +47,48 @@ export class Session { return await util.run(args, this.log, options); }; - privilegedSetup = () => util.privilegedSetup(this.context); - privilegedSpawn = (args) => util.privilegedSpawn(this.context, args); - privilegedUseradd = () => util.privilegedUseradd(this.uid); - privilegedTeardown = () => util.privilegedTeardown(this.context); + privilegedSession = () => util.privilegedSession(this.context); + privilegedWait = () => util.privilegedWait(this.context); + privilegedExec = (args) => util.privilegedExec(this.context, args); setup = async () => { try { allSessions.add(this); - const { uid, returnUser } = await borrowUser(); - this.uidInfo = { uid, returnUser }; - this.log(`Borrowed uid ${this.uid}`); - await this.run(this.privilegedSetup()); + const containerArgs = this.privilegedSession(); + const containerProc = spawn(containerArgs[0], containerArgs.slice(1)); + this.container = { + proc: containerProc, + }; + for (const stream of [containerProc.stdout, containerProc.stderr]) { + stream.on("data", (data) => + this.send({ + event: "serviceLog", + service: "container", + output: data.toString("utf8"), + }) + ); + containerProc.on("close", (code, signal) => + this.send({ + event: "serviceFailed", + service: "container", + error: `Exited with status ${signal || code}`, + }) + ); + containerProc.on("error", (err) => + this.send({ + event: "serviceFailed", + service: "container", + error: `${err}`, + }) + ); + } + await this.run(this.privilegedWait(this.context)); if (this.config.setup) { - await this.run(this.privilegedSpawn(bash(this.config.setup))); + await this.run(this.privilegedExec(bash(this.config.setup))); } await this.runCode(); if (this.config.daemon) { - const daemonArgs = this.privilegedSpawn(bash(this.config.daemon)); + const daemonArgs = this.privilegedExec(bash(this.config.daemon)); const daemonProc = spawn(daemonArgs[0], daemonArgs.slice(1)); this.daemon = { proc: daemonProc, @@ -105,9 +119,9 @@ export class Session { } if (this.config.lsp) { if (this.config.lsp.setup) { - await this.run(this.privilegedSpawn(bash(this.config.lsp.setup))); + await this.run(this.privilegedExec(bash(this.config.lsp.setup))); } - const lspArgs = this.privilegedSpawn(bash(this.config.lsp.start)); + const lspArgs = this.privilegedExec(bash(this.config.lsp.start)); const lspProc = spawn(lspArgs[0], lspArgs.slice(1)); this.lsp = { proc: lspProc, @@ -252,7 +266,7 @@ export class Session { writeCode = async (code) => { if (this.config.main.includes("/")) { await this.run( - this.privilegedSpawn([ + this.privilegedExec([ "mkdir", "-p", path.dirname(`${this.homedir}/${this.config.main}`), @@ -260,7 +274,7 @@ export class Session { ); } await this.run( - this.privilegedSpawn([ + this.privilegedExec([ "sh", "-c", `cat > ${path.resolve(this.homedir, this.config.main)}`, @@ -283,7 +297,7 @@ export class Session { } = this.config; if (this.term) { const pid = this.term.pty.pid; - const args = this.privilegedSpawn( + const args = this.privilegedExec( bash(`kill -SIGTERM ${pid}; sleep 1; kill -SIGKILL ${pid}`) ); spawn(args[0], args.slice(1)); @@ -310,7 +324,7 @@ export class Session { code += suffix + "\n"; } await this.writeCode(code); - const termArgs = this.privilegedSpawn(bash(cmdline)); + const termArgs = this.privilegedExec(bash(cmdline)); const term = { pty: pty.spawn(termArgs[0], termArgs.slice(1), { name: "xterm-color", @@ -349,14 +363,14 @@ export class Session { } if (this.formatter) { const pid = this.formatter.proc.pid; - const args = this.privilegedSpawn( + const args = this.privilegedExec( bash(`kill -SIGTERM ${pid}; sleep 1; kill -SIGKILL ${pid}`) ); spawn(args[0], args.slice(1)); this.formatter.live = false; this.formatter = null; } - const args = this.privilegedSpawn(bash(this.config.format.run)); + const args = this.privilegedExec(bash(this.config.format.run)); const formatter = { proc: spawn(args[0], args.slice(1)), live: true, @@ -409,7 +423,7 @@ export class Session { }; ensure = async (cmd) => { - const code = await this.run(this.privilegedSpawn(bash(cmd)), { + const code = await this.run(this.privilegedExec(bash(cmd)), { check: false, }); this.send({ event: "ensured", code }); @@ -422,11 +436,15 @@ export class Session { } this.log(`Tearing down session`); this.tearingDown = true; - allSessions.delete(this); - if (this.uidInfo) { - await this.run(this.privilegedTeardown()); - await this.returnUser(); + if (this.container) { + // SIGTERM should be sufficient as the command running in the + // foreground is just 'tail -f /dev/null' which won't try to + // block signals. Killing the foreground process (i.e. pid1) + // should cause the Docker runtime to bring everything else + // down in flames. + this.container.proc.kill(); } + allSessions.delete(this); this.ws.terminate(); } catch (err) { this.log(`Error during teardown`); diff --git a/backend/sandbox.js b/backend/sandbox.js index 4dddd9b..a89d79e 100644 --- a/backend/sandbox.js +++ b/backend/sandbox.js @@ -3,9 +3,9 @@ import { promises as fs } from "fs"; import process from "process"; import { quote } from "shell-quote"; -import { v4 as getUUID } from "uuid"; -import { borrowUser } from "./users.js"; +import { getUUID } from "./util.js"; + import { privilegedSetup, privilegedSpawn, @@ -29,9 +29,8 @@ async function main() { die("environment variable unset: $L"); } const uuid = getUUID(); - const { uid, returnUser } = await borrowUser(log); - await run(privilegedSetup({ uid, uuid }), log); - const args = privilegedSpawn({ uid, uuid }, [ + await run(privilegedSetup({ uuid }), log); + const args = privilegedSpawn({ uuid }, [ "bash", "-c", `exec env L='${lang}' bash --rcfile <(cat <<< ${quote([sandboxScript])})`, @@ -43,7 +42,7 @@ async function main() { proc.on("error", reject); proc.on("close", resolve); }); - await run(privilegedTeardown({ uid, uuid }), log); + await run(privilegedTeardown({ uuid }), log); await returnUser(); } diff --git a/backend/test-runner.js b/backend/test-runner.js index a6de2ce..86a55bc 100644 --- a/backend/test-runner.js +++ b/backend/test-runner.js @@ -5,10 +5,10 @@ import _ from "lodash"; import pQueue from "p-queue"; const PQueue = pQueue.default; import stripAnsi from "strip-ansi"; -import { v4 as getUUID } from "uuid"; import * as api from "./api.js"; import { langsPromise } from "./langs.js"; +import { getUUID } from "./util.js"; let langs = {}; diff --git a/backend/users.js b/backend/users.js deleted file mode 100644 index 2f9af6f..0000000 --- a/backend/users.js +++ /dev/null @@ -1,115 +0,0 @@ -import { spawn } from "child_process"; -import { promises as fs } from "fs"; -import os from "os"; - -import AsyncLock from "async-lock"; -import _ from "lodash"; -import parsePasswd from "parse-passwd"; - -import { asBool, privilegedUseradd, run, uuidRegexp } from "./util.js"; - -// Keep in sync with system/src/riju-system-privileged.c -export const MIN_UID = 2000; -export const MAX_UID = 65000; - -function validUID(uid) { - return uid >= MIN_UID && uid < MAX_UID; -} - -const CUR_UID = os.userInfo().uid; -const ASSUME_SINGLE_PROCESS = asBool( - process.env.RIJU_ASSUME_SINGLE_PROCESS, - false -); - -let initialized = false; -let nextUserToCreate = null; -let locallyBorrowedUsers = new Set(); -let availableUsers = new Set(); -let lock = new AsyncLock(); - -async function getCreatedUsers() { - return new Set( - parsePasswd(await fs.readFile("/etc/passwd", "utf-8")) - .map(({ uid }) => parseInt(uid)) - .filter((uid) => !isNaN(uid) && validUID(uid)) - ); -} - -async function getActiveUsers() { - let dirents; - try { - dirents = await fs.readdir("/tmp/riju"); - } catch (err) { - if (err.code === "ENOENT") { - return new Set(); - } - throw err; - } - return new Set( - ( - await Promise.all( - dirents - .filter((name) => name.match(uuidRegexp)) - .map((name) => fs.stat(`/tmp/riju/${name}`)) - ) - ) - .map(({ uid }) => uid) - .filter(validUID) - ); -} - -async function createUser(log) { - if (nextUserToCreate >= MAX_UID) { - throw new Error("too many users"); - } - const uid = nextUserToCreate; - await run(privilegedUseradd(uid), log); - nextUserToCreate += 1; - return uid; -} - -export async function borrowUser(log) { - return await lock.acquire("key", async () => { - if (!initialized || !ASSUME_SINGLE_PROCESS) { - const createdUsers = await getCreatedUsers(); - const activeUsers = await getActiveUsers(); - if (createdUsers.size > 0) { - nextUserToCreate = _.max([...createdUsers]) + 1; - } else { - nextUserToCreate = MIN_UID; - } - // If there are new users created, we want to make them - // available (unless they are already active). Similarly, if - // there are users that have become inactive, we want to make - // them available (unless they are already borrowed locally). - for (const user of createdUsers) { - if (!activeUsers.has(user) && !locallyBorrowedUsers.has(user)) { - availableUsers.add(user); - } - } - // If there are users that have become active, we want to make - // them unavailable. - for (const user of activeUsers) { - availableUsers.delete(user); - } - initialized = true; - } - if (availableUsers.size === 0) { - availableUsers.add(await createUser(log)); - } - // https://stackoverflow.com/a/32539929/3538165 - const user = availableUsers.values().next().value; - locallyBorrowedUsers.add(user); - availableUsers.delete(user); - return { - uid: user, - returnUser: async () => { - await lock.acquire("key", () => { - locallyBorrowedUsers.delete(user); - availableUsers.add(user); - }); - }, - }; - }); -} diff --git a/backend/util.js b/backend/util.js index 69643c3..ce29b8a 100644 --- a/backend/util.js +++ b/backend/util.js @@ -3,55 +3,12 @@ import os from "os"; import process from "process"; import { quote } from "shell-quote"; - -import { MIN_UID, MAX_UID } from "./users.js"; +import { v4 as getUUIDOrig } from "uuid"; export const rijuSystemPrivileged = "system/out/riju-system-privileged"; -const rubyVersion = (() => { - try { - return spawnSync("ruby", ["-e", "puts RUBY_VERSION"]) - .stdout.toString() - .trim(); - } catch (err) { - return null; - } -})(); - -function getEnv({ uid, uuid }) { - const cwd = `/tmp/riju/${uuid}`; - const path = [ - rubyVersion && `${cwd}/.gem/ruby/${rubyVersion}/bin`, - `${cwd}/.local/bin`, - `${cwd}/node_modules/.bin`, - `/usr/local/sbin`, - `/usr/local/bin`, - `/usr/sbin`, - `/usr/bin`, - `/bin`, - ].filter((x) => x); - const username = - uid >= MIN_UID && uid < MAX_UID ? `riju${uid}` : os.userInfo().username; - return { - HOME: cwd, - HOSTNAME: "riju", - LANG: "C.UTF-8", - LC_ALL: "C.UTF-8", - LOGNAME: username, - PATH: path.join(":"), - PWD: cwd, - SHELL: "/usr/bin/bash", - TERM: "xterm-256color", - TMPDIR: `${cwd}`, - USER: username, - USERNAME: username, - }; -} - -function getEnvString(ctx) { - return Object.entries(getEnv(ctx)) - .map(([key, val]) => `${key}=${quote([val])}`) - .join(" "); +export function getUUID() { + return getUUIDOrig().replace(/-/g, ""); } export async function run(args, log, options) { @@ -87,30 +44,16 @@ export async function run(args, log, options) { }); } -export function privilegedUseradd(uid) { - return [rijuSystemPrivileged, "useradd", `${uid}`]; +export function privilegedSession({ uuid, lang }) { + return [rijuSystemPrivileged, "session", uuid, lang]; } -export function privilegedSetup({ uid, uuid }) { - return [rijuSystemPrivileged, "setup", `${uid}`, uuid]; +export function privilegedWait({ uuid }) { + return [rijuSystemPrivileged, "wait", uuid]; } -export function privilegedSpawn(ctx, args) { - const { uid, uuid } = ctx; - return [ - rijuSystemPrivileged, - "spawn", - `${uid}`, - uuid, - "sh", - "-c", - `exec env -i ${getEnvString(ctx)} "$@"`, - "--", - ].concat(args); -} - -export function privilegedTeardown({ uid, uuid }) { - return [rijuSystemPrivileged, "teardown", `${uid}`, uuid]; +export function privilegedExec({ uuid }, args) { + return [rijuSystemPrivileged, "exec", uuid].concat(args); } export function bash(cmdline) { @@ -130,9 +73,6 @@ export const log = { error: console.error, }; -// https://gist.github.com/bugventure/f71337e3927c34132b9a -export const uuidRegexp = /^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$/; - export function asBool(value, def) { if (def === undefined) { throw new Error("asBool needs an explicit default value"); diff --git a/docker/app/Dockerfile b/docker/app/Dockerfile index 9d27f1d..cd36ed1 100644 --- a/docker/app/Dockerfile +++ b/docker/app/Dockerfile @@ -22,17 +22,10 @@ COPY lib ./lib/ COPY backend ./backend/ COPY langs ./langs/ -FROM ubuntu:rolling +FROM riju:runtime -COPY docker/app/install.bash /tmp/ -RUN /tmp/install.bash - -COPY docker/shared/my_init /usr/local/sbin/ ENTRYPOINT ["/usr/local/sbin/my_init", "--quiet", "--"] - RUN useradd -p '!' -m -l -s /usr/bin/bash riju - -WORKDIR /src COPY --chown=riju:riju --from=build /src ./ RUN chown root:riju system/out/*-privileged && chmod a=,g=rx,u=rwxs system/out/*-privileged diff --git a/docker/app/install.bash b/docker/app/install.bash deleted file mode 100755 index d23776a..0000000 --- a/docker/app/install.bash +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env bash - -set -euxo pipefail - -export DEBIAN_FRONTEND=noninteractive - -apt-get update -apt-get dist-upgrade -y - -apt-get install -y curl gnupg lsb-release - -curl -fsSL https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add - -curl -fsSL https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - - -ubuntu_ver="$(lsb_release -rs)" -ubuntu_name="$(lsb_release -cs)" - -node_repo="$(curl -sS https://deb.nodesource.com/setup_current.x | grep NODEREPO= | grep -Eo 'node_[0-9]+\.x' | head -n1)" - -tee -a /etc/apt/sources.list.d/custom.list >/dev/null </dev/null </dev/null </dev/null <<"EOF" +%sudo ALL=(ALL:ALL) NOPASSWD: ALL +EOF + +popd +rm -rf /tmp/riju-work + +rm "$0" diff --git a/docker/lang/Dockerfile b/docker/lang/Dockerfile index e7504df..f38b4f8 100644 --- a/docker/lang/Dockerfile +++ b/docker/lang/Dockerfile @@ -1,4 +1,4 @@ -FROM riju:runtime +FROM riju:base ARG LANG diff --git a/docker/packaging/install.bash b/docker/packaging/install.bash index 6d47b02..1b08ef3 100755 --- a/docker/packaging/install.bash +++ b/docker/packaging/install.bash @@ -2,8 +2,8 @@ set -euxo pipefail -# See install.bash for the runtime image for much of the same, but -# with more comments. +# See install.bash for the base image for much of the same, but with +# more comments. mkdir /tmp/riju-work pushd /tmp/riju-work diff --git a/docker/runtime/Dockerfile b/docker/runtime/Dockerfile index 76e0872..62acade 100644 --- a/docker/runtime/Dockerfile +++ b/docker/runtime/Dockerfile @@ -3,9 +3,10 @@ FROM ubuntu:rolling COPY docker/runtime/install.bash /tmp/ RUN /tmp/install.bash -WORKDIR /src COPY docker/shared/my_init docker/runtime/pid1.bash /usr/local/sbin/ ENTRYPOINT ["/usr/local/sbin/my_init", "--quiet", "--", "/usr/local/sbin/pid1.bash"] + +WORKDIR /src CMD ["bash"] EXPOSE 6119 EXPOSE 6120 diff --git a/docker/runtime/install.bash b/docker/runtime/install.bash index b988373..87da6ad 100755 --- a/docker/runtime/install.bash +++ b/docker/runtime/install.bash @@ -11,120 +11,32 @@ pushd /tmp/riju-work export DEBIAN_FRONTEND=noninteractive -dpkg --add-architecture i386 - apt-get update (yes || true) | unminimize apt-get install -y curl gnupg lsb-release wget -# Ceylon -wget https://cacerts.digicert.com/DigiCertTLSRSASHA2562020CA1.crt.pem -O /usr/local/share/ca-certificates/DigiCertTLSRSASHA2562020CA1.crt +curl -fsSL https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add - +curl -fsSL https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - +curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - -# D -wget https://letsencrypt.org/certs/lets-encrypt-r3.pem -O /usr/local/share/ca-certificates/lets-encrypt-r3.crt - -update-ca-certificates - -ubuntu_ver="$(lsb_release -rs)" ubuntu_name="$(lsb_release -cs)" -cran_repo="$(curl -fsSL https://cran.r-project.org/bin/linux/ubuntu/ | grep -Eo 'cran[0-9]+' | head -n1)" -node_repo="$(curl -fsSL https://deb.nodesource.com/setup_current.x | grep NODEREPO= | grep -Eo 'node_[0-9]+\.x' | head -n1)" - -# .NET -wget "https://packages.microsoft.com/config/ubuntu/${ubuntu_ver}/packages-microsoft-prod.deb" -apt-get install ./packages-microsoft-prod.deb - -# Ceylon -curl -fsSL https://downloads.ceylon-lang.org/apt/ceylon-debian-repo.gpg.key | apt-key add - - -# Crystal -curl -fsSL https://keybase.io/crystal/pgp_keys.asc | apt-key add - - -# Dart -curl -fsSL https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - - -# Hack -apt-key adv --keyserver keyserver.ubuntu.com --recv-keys B4112585D386EB94 - -# MongoDB -curl -fsSL https://www.mongodb.org/static/pgp/server-4.4.asc | apt-key add - - -# Node.js -curl -fsSL https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add - - -# R -apt-key adv --keyserver keyserver.ubuntu.com --recv-keys E298A3A825C0D65DFD57CBB651716619E084DAB9 - -# Yarn -curl -fsSL https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - +node_repo="$(curl -sS https://deb.nodesource.com/setup_current.x | grep NODEREPO= | grep -Eo 'node_[0-9]+\.x' | head -n1)" tee -a /etc/apt/sources.list.d/custom.list >/dev/null </dev/null </dev/null <<"EOF" %sudo ALL=(ALL:ALL) NOPASSWD: ALL EOF -mkdir -p /opt/riju/langs -touch /opt/riju/langs/.keep - popd rm -rf /tmp/riju-work diff --git a/system/compile.bash b/system/compile.bash index e7c1cae..3907274 100755 --- a/system/compile.bash +++ b/system/compile.bash @@ -19,9 +19,7 @@ for src in system/src/*.c; do out="${out/.c}" verbosely clang -Wall -Wextra -Werror -std=c11 "${src}" -o "${out}" if [[ "${out}" == *-privileged ]]; then - if getent group riju >/dev/null; then - sudo chown root:riju "${out}" - fi - sudo chmod a=,g=rx,u=rwxs "${out}" + verbosely sudo chown root:riju "${out}" + verbosely sudo chmod a=,g=rx,u=rwxs "${out}" fi done diff --git a/system/src/riju-system-privileged.c b/system/src/riju-system-privileged.c index b6dfd6a..44a0f6b 100644 --- a/system/src/riju-system-privileged.c +++ b/system/src/riju-system-privileged.c @@ -1,19 +1,15 @@ #define _GNU_SOURCE #include #include +#include #include #include #include #include #include +#include #include -// Keep in sync with backend/src/users.ts -const int MIN_UID = 2000; -const int MAX_UID = 65000; - -int privileged; - void __attribute__ ((noreturn)) die(char *msg) { fprintf(stderr, "%s\n", msg); @@ -23,155 +19,136 @@ void __attribute__ ((noreturn)) die(char *msg) void die_with_usage() { die("usage:\n" - " riju-system-privileged useradd UID\n" - " riju-system-privileged setup UID UUID\n" - " riju-system-privileged spawn UID UUID CMDLINE...\n" - " riju-system-privileged teardown UID UUID"); -} - -int parseUID(char *str) -{ - if (!privileged) - return -1; - char *endptr; - long uid = strtol(str, &endptr, 10); - if (!*str || *endptr) - die("uid must be an integer"); - if (uid < MIN_UID || uid >= MAX_UID) - die("uid is out of range"); - return uid; + " riju-system-privileged session UUID LANG\n" + " riju-system-privileged wait UUID\n" + " riju-system-privileged exec UUID CMDLINE..."); } char *parseUUID(char *uuid) { - if (!*uuid) + if (strnlen(uuid, 33) != 32) die("illegal uuid"); for (char *ptr = uuid; *ptr; ++ptr) - if (!((*ptr >= 'a' && *ptr <= 'z') || (*ptr >= '0' && *ptr <= '9') || *ptr == '-')) + if (!((*ptr >= 'a' && *ptr <= 'z') || (*ptr >= '0' && *ptr <= '9'))) die("illegal uuid"); return uuid; } -void useradd(int uid) -{ - if (!privileged) - die("useradd not allowed without root privileges"); - char *cmdline; - if (asprintf(&cmdline, "groupadd -g %1$d riju%1$d", uid) < 0) - die("asprintf failed"); - int status = system(cmdline); - if (status != 0) - die("groupadd failed"); - if (asprintf(&cmdline, "useradd -M -N -l -r -u %1$d -g %1$d -p '!' -s /usr/bin/bash riju%1$d", uid) < 0) - die("asprintf failed"); - status = system(cmdline); - if (status != 0) - die("useradd failed"); +char *parseLang(char *lang) { + size_t len = strnlen(lang, 65); + if (len == 0 || len > 64) + die("illegal language name"); + return lang; } -void spawn(int uid, char *uuid, char **cmdline) +void session(char *uuid, char *lang) { - char *cwd; - if (asprintf(&cwd, "/tmp/riju/%s", uuid) < 0) + char *image, *container; + if (asprintf(&image, "riju:lang-%s", lang) < 0) die("asprintf failed"); - if (chdir(cwd) < 0) - die("chdir failed"); - if (privileged) { - if (setgid(uid) < 0) - die("setgid failed"); - if (setgroups(0, NULL) < 0) - die("setgroups failed"); - if (setuid(uid) < 0) - die("setuid failed"); - } - umask(077); - execvp(cmdline[0], cmdline); + if (asprintf(&container, "riju-session-%s", uuid) < 0) + die("asprintf failed"); + char *argv[] = { + "docker", + "run", + "--rm", + "-e", "HOME=/home/riju", + "-e", "HOSTNAME=riju", + "-e", "LANG=C.UTF-8", + "-e", "LC_ALL=C.UTF-8", + "-e", "LOGNAME=riju", + "-e", "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/bin", + "-e", "PWD=/home/riju/src", + "-e", "SHELL=/usr/bin/bash", + "-e", "TERM=xterm-256color", + "-e", "TMPDIR=/tmp", + "-e", "USER=riju", + "-e", "USERNAME=riju", + "--hostname", "riju", + "--name", container, + image, "tail", "-f", "/dev/null", NULL, + }; + execvp(argv[0], argv); die("execvp failed"); } -void setup(int uid, char *uuid) +void wait_alarm(int signum) { - char *cmdline; - if (asprintf(&cmdline, privileged - ? "install -d -o riju%1$d -g riju%1$d -m 700 /tmp/riju/%2$s" - : "install -d -m 700 /tmp/riju/%2$s", uid, uuid) < 0) - die("asprintf failed"); - int status = system(cmdline); - if (status != 0) - die("install failed"); + (void)signum; + die("container did not come up within 1 second"); } -void teardown(int uid, char *uuid) +void wait(char *uuid) { char *cmdline; - int status; - char *users; - if (uid >= MIN_UID && uid < MAX_UID) { - if (asprintf(&users, "%d", uid) < 0) - die("asprintf failed"); - } else { - cmdline = "getent passwd | grep -Eo '^riju[0-9]{4}' | paste -s -d, - | tr -d '\n'"; - FILE *fp = popen(cmdline, "r"); - if (fp == NULL) - die("popen failed"); - static char buf[(MAX_UID - MIN_UID) * 9]; - if (fgets(buf, sizeof(buf), fp) == NULL) { - if (feof(fp)) - users = NULL; - else { - die("fgets failed"); - } - } else - users = buf; - } - if (users != NULL) { - if (asprintf(&cmdline, "while pkill -9 --uid %1$s; do sleep 0.01; done", users) < 0) - die("asprintf failed"); - status = system(cmdline); - if (status != 0 && status != 256) - die("pkill failed"); - } - if (asprintf(&cmdline, "rm -rf /tmp/riju/%s", uuid) < 0) + if (asprintf(&cmdline, "docker inspect riju-session-%s", uuid) < 0) die("asprintf failed"); - status = system(cmdline); - if (status != 0) - die("rm failed"); + struct timespec ts; + ts.tv_sec = 0; + ts.tv_nsec = 1000 * 1000 * 10; + signal(SIGALRM, wait_alarm); + alarm(1); + while (1) { + FILE *proc = popen(cmdline, "r"); + if (proc == NULL) + die("popen failed"); + int status = pclose(proc); + if (status < 0) + die("pclose failed"); + if (WEXITSTATUS(status) == 0) + break; + int rv = nanosleep(&ts, NULL); + if (rv != 0 && rv != EINTR) + die("nanosleep failed"); + } +} + +void exec(char *uuid, int argc, char **cmdline) +{ + char *container; + if (asprintf(&container, "riju-session-%s", uuid) < 0) + die("asprintf failed"); + char *argvPrefix[] = { + "docker", + "exec", + "-it", + container, + }; + char **argv = malloc(sizeof(argvPrefix) + (argc + 1) * sizeof(char *)); + if (argv == NULL) + die("malloc failed"); + memcpy(argv, argvPrefix, sizeof(argvPrefix)); + memcpy((void *)argv + sizeof(argvPrefix), cmdline, argc * sizeof(char *)); + argv[sizeof(argvPrefix) + argc * sizeof(char *)] = NULL; + execvp(argv[0], argv); + die("execvp failed"); } int main(int argc, char **argv) { - int code = setuid(0); - if (code != 0 && code != -EPERM) + if (setuid(0) != 0) die("setuid failed"); - privileged = code == 0; if (argc < 2) die_with_usage(); - if (!strcmp(argv[1], "useradd")) { + if (!strcmp(argv[1], "session")) { + if (argc != 4) + die_with_usage(); + char *uuid = parseUUID(argv[2]); + char *lang = parseLang(argv[3]); + session(uuid, lang); + return 0; + } + if (!strcmp(argv[1], "wait")) { if (argc != 3) die_with_usage(); - useradd(parseUID(argv[2])); + char *uuid = parseUUID(argv[2]); + wait(uuid); return 0; } - if (!strcmp(argv[1], "spawn")) { - if (argc < 5) + if (!strcmp(argv[1], "exec")) { + if (argc < 4) die_with_usage(); - spawn(parseUID(argv[2]), parseUUID(argv[3]), &argv[4]); - return 0; - } - if (!strcmp(argv[1], "setup")) { - if (argc != 4) - die_with_usage(); - int uid = parseUID(argv[2]); - char *uuid = parseUUID(argv[3]); - setup(uid, uuid); - return 0; - } - if (!strcmp(argv[1], "teardown")) { - if (argc != 4) - die_with_usage(); - int uid = strcmp(argv[2], "*") ? parseUID(argv[2]) : -1; - char *uuid = strcmp(argv[3], "*") ? parseUUID(argv[3]) : "*"; - teardown(uid, uuid); + exec(parseUUID(argv[2]), argc, &argv[3]); return 0; } die_with_usage(); diff --git a/tools/build-lang-image.js b/tools/build-lang-image.js index 3e5ab36..248dd91 100644 --- a/tools/build-lang-image.js +++ b/tools/build-lang-image.js @@ -28,10 +28,7 @@ async function main() { const hash = await hashDockerfile( "lang", { - "riju:runtime": await getLocalImageLabel( - "riju:runtime", - "riju.image-hash" - ), + "riju:base": await getLocalImageLabel("riju:base", "riju.image-hash"), }, { salt: { @@ -52,7 +49,7 @@ async function main() { try { if (debug) { await runCommand( - `docker run -it --rm -e LANG=${lang} -w /tmp/riju-work --network host riju:runtime` + `docker run -it --rm -e LANG=${lang} -w /tmp/riju-work --network host base:runtime` ); } else { await runCommand( diff --git a/tools/hash-dockerfile.js b/tools/hash-dockerfile.js index bef6be8..8aa6dfa 100644 --- a/tools/hash-dockerfile.js +++ b/tools/hash-dockerfile.js @@ -12,7 +12,7 @@ import _ from "lodash"; import { getLocalImageDigest, getLocalImageLabel } from "./docker-util.js"; import { runCommand } from "./util.js"; -// Given a string like "runtime" that identifies the relevant +// Given a string like "base" that identifies the relevant // Dockerfile, read it from disk and parse it into a list of commands. async function parseDockerfile(name) { const contents = await fs.readFile(`docker/${name}/Dockerfile`, "utf-8"); diff --git a/tools/plan-publish.js b/tools/plan-publish.js index fa389dc..635e46e 100644 --- a/tools/plan-publish.js +++ b/tools/plan-publish.js @@ -149,9 +149,7 @@ async function planDebianPackages(opts) { } clauses.push(`make installs L=${lang}`); clauses.push("make test"); - await runCommand( - `make shell I=runtime CMD="${clauses.join(" && ")}"` - ); + await runCommand(`make shell I=base CMD="${clauses.join(" && ")}"`); } await runCommand(`make upload L=${lang} T=${type}`); }, @@ -186,12 +184,12 @@ async function computePlan() { "ubuntu:rolling": await getLocalImageDigest("ubuntu:rolling"), }; const packaging = await planDockerImage("packaging", dependentHashes); - const runtime = await planDockerImage("runtime", dependentHashes); + const base = await planDockerImage("base", dependentHashes); const packages = await planDebianPackages({ - deps: [packaging.id, runtime.id], + deps: [packaging.id, base.id], }); const composite = await planDockerImage("composite", dependentHashes, { - deps: [runtime.id, ...packages.map(({ id }) => id)], + deps: [base.id, ...packages.map(({ id }) => id)], hashOpts: { salt: { packageHashes: packages.map(({ desired }) => desired).sort(), @@ -202,7 +200,7 @@ async function computePlan() { const app = await planDockerImage("app", dependentHashes, { deps: [composite.id, compile.id], }); - return [packaging, runtime, ...packages, composite, compile, app]; + return [packaging, base, ...packages, composite, compile, app]; } function printTable(data, headers) {