From a99ce9ae4763a6a7a014c5cf43f6fd53ab3cffe8 Mon Sep 17 00:00:00 2001 From: Omar Brikaa Date: Fri, 13 Sep 2024 15:14:16 +0300 Subject: [PATCH 1/3] Remove nosocket, update security principles in docs --- api/src/nosocket/Makefile | 19 ------------ api/src/nosocket/nosocket.c | 62 ------------------------------------- readme.md | 20 ++++++------ 3 files changed, 10 insertions(+), 91 deletions(-) delete mode 100644 api/src/nosocket/Makefile delete mode 100644 api/src/nosocket/nosocket.c diff --git a/api/src/nosocket/Makefile b/api/src/nosocket/Makefile deleted file mode 100644 index a86a8f5..0000000 --- a/api/src/nosocket/Makefile +++ /dev/null @@ -1,19 +0,0 @@ -CC = gcc -CFLAGS = -O2 -Wall -lseccomp -TARGET = nosocket -BUILD_PATH = ./ -INSTALL_PATH = /usr/local/bin/ -SOURCE = nosocket.c - -all: $(TARGET) - -$(TARGET): $(SOURCE) - $(CC) $(BUILD_PATH)$(SOURCE) $(CFLAGS) -o $(TARGET) - -install: - mv $(TARGET) $(INSTALL_PATH) - -clean: - $(RM) $(TARGET) - $(RM) $(INSTALL_PATH)$(TARGET) - diff --git a/api/src/nosocket/nosocket.c b/api/src/nosocket/nosocket.c deleted file mode 100644 index 4efab88..0000000 --- a/api/src/nosocket/nosocket.c +++ /dev/null @@ -1,62 +0,0 @@ -/* -nosocket.c - -Disables access to the `socket` syscall and runs a program provided as the first -commandline argument. -*/ -#include -#include -#include -#include -#include - -int main(int argc, char *argv[]) -{ - // Disallow any new capabilities from being added - prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); - - // SCMP_ACT_ALLOW lets the filter have no effect on syscalls not matching a - // configured filter rule (allow all by default) - scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ALLOW); - if (!ctx) - { - fprintf(stderr, "Unable to initialize seccomp filter context\n"); - return 1; - } - - // Add 32 bit and 64 bit architectures to seccomp filter - int rc; - uint32_t arch[] = {SCMP_ARCH_X86_64, SCMP_ARCH_X86, SCMP_ARCH_X32}; - // We first remove the existing arch, otherwise our subsequent call to add - // it will fail - seccomp_arch_remove(ctx, seccomp_arch_native()); - for (int i = 0; i < sizeof(arch) / sizeof(arch[0]); i++) - { - rc = seccomp_arch_add(ctx, arch[i]); - if (rc != 0) - { - fprintf(stderr, "Unable to add arch: %d\n", arch[i]); - return 1; - } - } - - // Add a seccomp rule to the syscall blacklist - blacklist the socket syscall - if (seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EACCES), SCMP_SYS(socket), 0) < 0) - { - fprintf(stderr, "Unable to add seccomp rule to context\n"); - return 1; - } - -#ifdef DEBUG - seccomp_export_pfc(ctx, 0); -#endif - - if (argc < 2) - { - fprintf(stderr, "Usage %s: %s \n", argv[0], argv[0]); - return 1; - } - seccomp_load(ctx); - execvp(argv[1], argv + 1); - return 1; -} diff --git a/readme.md b/readme.md index 18e9e5a..2c19b5e 100644 --- a/readme.md +++ b/readme.md @@ -411,26 +411,26 @@ Content-Type: application/json # Principle of Operation -Piston uses Docker as the primary mechanism for sandboxing. There is an API within the container written in Node -which takes in execution requests and executees them within the container safely. -High level, the API writes any source code to a temporary directory in `/piston/jobs`. +Piston uses [Isolate](https://www.ucw.cz/moe/isolate.1.html) inside Docker as the primary mechanism for sandboxing. There is an API within the container written in Node +which takes in execution requests and executes them within the container safely. +High level, the API writes any source code and executes it inside an Isolate sandbox. The source file is either ran or compiled and ran (in the case of languages like c, c++, c#, go, etc.).
# Security -Docker provides a great deal of security out of the box in that it's separate from the system. -Piston takes additional steps to make it resistant to -various privilege escalation, denial-of-service, and resource saturation threats. These steps include: +Piston uses Isolate which makes use of Linux namespaces, chroot, multiple unprivileged users, and cgroup for sandboxing and resource limiting. Code execution submissions on Piston shall not be aware of each other, shall not affect each other and shall not affect the underlying host system. This is ensured through multiple steps including: -- Disabling outgoing network interaction +- Disabling outgoing network interaction by default - Capping max processes at 256 by default (resists `:(){ :|: &}:;`, `while True: os.fork()`, etc.) - Capping max files at 2048 (resists various file based attacks) - Cleaning up all temp space after each execution (resists out of drive space attacks) -- Running as a variety of unprivileged users -- Capping runtime execution at 3 seconds -- Capping stdout to 65536 characters (resists yes/no bombs and runaway output) +- Running each submission as a different unprivileged user +- Running each submission with its own isolated Linux namespaces +- Capping runtime execution at 3 seconds by default (CPU-time and wall-time) +- Capping the peak memory that all the submission's processes can use +- Capping stdout to 1024 characters by default (resists yes/no bombs and runaway output) - SIGKILLing misbehaving code
From ecdced9ee7104074cb05bd7aac156cafeaf31a69 Mon Sep 17 00:00:00 2001 From: Omar Brikaa Date: Fri, 13 Sep 2024 16:19:09 +0300 Subject: [PATCH 2/3] Add SIGKILL signal for output limits and timeout, add status for output limits --- api/src/job.js | 6 ++++-- readme.md | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/api/src/job.js b/api/src/job.js index e573843..881aa9f 100644 --- a/api/src/job.js +++ b/api/src/job.js @@ -205,6 +205,7 @@ class Job { this.runtime.output_max_size ) { message = 'stderr length exceeded'; + status = 'EL'; this.logger.info(message); try { process.kill(proc.pid, 'SIGABRT'); @@ -229,6 +230,7 @@ class Job { this.runtime.output_max_size ) { message = 'stdout length exceeded'; + status = 'OL'; this.logger.info(message); try { process.kill(proc.pid, 'SIGABRT'); @@ -287,7 +289,7 @@ class Job { message = message || value; break; case 'status': - status = value; + status = status || value; break; case 'time': cpu_time_stat = parse_float(value) * 1000; @@ -310,7 +312,7 @@ class Job { stdout, stderr, code, - signal, + signal: ['TO', 'OL', 'EL'].includes(status) ? 'SIGKILL' : signal, output, memory, message, diff --git a/readme.md b/readme.md index 2c19b5e..f1172d3 100644 --- a/readme.md +++ b/readme.md @@ -283,6 +283,8 @@ It also contains the `code` and `signal` which was returned from each process. I - `RE` for runtime error - `SG` for dying on a signal - `TO` for timeout (either via `timeout` or `cpu_time`) +- `OL` for stdout length exceeded +- `EL` for stderr length exceeded - `XX` for internal error ```json From c4afd97a38013f7062a51a13f32802c6266fb089 Mon Sep 17 00:00:00 2001 From: Omar Brikaa Date: Sun, 15 Sep 2024 20:48:45 +0300 Subject: [PATCH 3/3] Use pkgdir inside isolate sandbox to account for packages that have been built with a custom PREFIX closes #686 --- api/src/job.js | 8 ++++---- api/src/runtime.js | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/api/src/job.js b/api/src/job.js index 881aa9f..d46120b 100644 --- a/api/src/job.js +++ b/api/src/job.js @@ -157,7 +157,7 @@ class Job { '-c', '/box/submission', '-e', - `--dir=/runtime=${this.runtime.pkgdir}`, + `--dir=${this.runtime.pkgdir}`, `--dir=/etc:noexec`, `--processes=${this.runtime.max_process_count}`, `--open-files=${this.runtime.max_open_files}`, @@ -171,7 +171,7 @@ class Job { ...(config.disable_networking ? [] : ['--share-net']), '--', '/bin/bash', - file, + path.join(this.runtime.pkgdir, file), ...args, ], { @@ -365,7 +365,7 @@ class Job { emit_event_bus_stage('compile'); compile = await this.safe_call( box, - '/runtime/compile', + 'compile', code_files.map(x => x.name), this.timeouts.compile, this.cpu_times.compile, @@ -390,7 +390,7 @@ class Job { emit_event_bus_stage('run'); run = await this.safe_call( box, - '/runtime/run', + 'run', [code_files[0].name, ...this.args], this.timeouts.run, this.cpu_times.run, diff --git a/api/src/runtime.js b/api/src/runtime.js index 1d4a8fc..9a2adf4 100644 --- a/api/src/runtime.js +++ b/api/src/runtime.js @@ -185,7 +185,6 @@ class Runtime { .split('\n') .map(line => line.split('=', 2)) .forEach(([key, val]) => { - val = val.replace_all(this.pkgdir, '/runtime'); this._env_vars[key.trim()] = val.trim(); }); }