Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
ea6892f
ignore macos hidden files, dsstore and such
christiano-developer May 31, 2026
8c46326
basic Go HTTP server with health check endpoint
christiano-developer May 31, 2026
11c7b80
define API request and response models
christiano-developer May 31, 2026
70ed8d3
completed intial repo structure
christiano-developer May 31, 2026
7114306
completion of the post/run endpoint, unit tests implimented
christiano-developer May 31, 2026
d64dab2
Ai: resolve dynamic linker and gcc headers issues in nsjail sandbox
christiano-developer May 31, 2026
a63db5b
docs: add Stage 1 documentation and AI logs
christiano-developer May 31, 2026
139a20b
feat: implement Stage 2 endpoints, support all 7 languages, and add i…
christiano-developer Jun 1, 2026
b944e15
docs: update API, architecture, language registry, and security refer…
christiano-developer Jun 1, 2026
32081a3
completion of stage 2
christiano-developer Jun 1, 2026
c5568fc
feat: implement Stage 3 bounded concurrency, priority queueing, and l…
christiano-developer Jun 1, 2026
e5bd0c4
docs: add concurrency load benchmarks and testing script
christiano-developer Jun 1, 2026
485d52e
feat: add multi-language options to load tester
christiano-developer Jun 1, 2026
10deb7a
docs: document multi-language benchmark results
christiano-developer Jun 1, 2026
f1e63f9
feat: add mixed mode with random override clamping to load tester
christiano-developer Jun 1, 2026
fcb5bbf
feat: implement process-unique UID sandboxing, container security har…
christiano-developer Jun 1, 2026
4ea9bb7
security: enforce test case size limits and startup orphan sweep
christiano-developer Jun 1, 2026
7117566
docs: document request size validations and startup orphan sweeps
christiano-developer Jun 1, 2026
e649775
feat: configurable concurrency, loud startup validation, and custom s…
christiano-developer Jun 1, 2026
ae88b2b
docs: update README with framework justification, environment configu…
christiano-developer Jun 1, 2026
678c4e9
feat: complete Stage 3 - update benchmarks with fresh performance met…
christiano-developer Jun 1, 2026
e55a8eb
feat: add php, kotlin, and lisp languages
christiano-developer Jun 12, 2026
93a877d
docs: log AI usage for php, kotlin, lisp plug-and-play work
christiano-developer Jun 12, 2026
ce38576
feat: intial memhog loadtest, and run 1
christiano-developer Jun 12, 2026
259409f
log: memhog 8 concurrent boxes(nsjail) run 2
christiano-developer Jun 12, 2026
0530ad2
log: memhog 8 concurrent boxes(nsjail) run 2
christiano-developer Jun 12, 2026
3dc2a1f
loadtest runs with memhog java
christiano-developer Jun 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

# Test binary, built with `go test -c`
*.test

third_party/
# Code coverage profiles and other test artifacts
*.out
coverage.*
Expand All @@ -30,3 +30,11 @@ go.work.sum
# Editor/IDE
# .idea/
# .vscode/

# macOS system files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
__MACOSX/
25 changes: 22 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@ RUN git clone --depth 1 --branch ${NSJAIL_VERSION} https://github.com/google/nsj
# ---- Builder / dev image (Go + linters + nsjail) ----
FROM golang:${GO_VERSION}-${DEBIAN_VERSION} AS builder
RUN apt-get update && apt-get install -y --no-install-recommends \
libnl-route-3-200 libprotobuf32 \
&& rm -rf /var/lib/apt/lists/*
libnl-route-3-200 libprotobuf32 python3 \
default-jdk nodejs iverilog uidmap \
&& rm -rf /var/lib/apt/lists/* \
&& echo "root:100000:1000000000" > /etc/subuid \
&& echo "root:100000:1000000000" > /etc/subgid
COPY --from=nsjail-builder /usr/local/bin/nsjail /usr/local/bin/nsjail
RUN go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
WORKDIR /src
Expand All @@ -30,10 +33,26 @@ RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/goboxd ./cmd/gobox

# ---- Runtime image ----
FROM debian:${DEBIAN_VERSION}-slim AS runtime
ARG KOTLIN_VERSION=2.1.10
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates libnl-route-3-200 libprotobuf32 \
&& rm -rf /var/lib/apt/lists/*
python3 gcc libc6-dev php sbcl \
g++ default-jdk nodejs iverilog uidmap \
curl unzip \
# Install the Kotlin command-line compiler (depends on the JDK above)
&& curl -fsSL -o /tmp/kotlin.zip \
"https://github.com/JetBrains/kotlin/releases/download/v${KOTLIN_VERSION}/kotlin-compiler-${KOTLIN_VERSION}.zip" \
&& unzip -q /tmp/kotlin.zip -d /opt \
&& ln -sf /opt/kotlinc/bin/kotlinc /usr/local/bin/kotlinc \
&& ln -sf /opt/kotlinc/bin/kotlin /usr/local/bin/kotlin \
&& rm -f /tmp/kotlin.zip \
&& apt-get purge -y --auto-remove unzip \
&& rm -rf /var/lib/apt/lists/* \
&& echo "root:100000:1000000000" > /etc/subuid \
&& echo "root:100000:1000000000" > /etc/subgid
COPY --from=nsjail-builder /usr/local/bin/nsjail /usr/local/bin/nsjail
COPY --from=builder /out/goboxd /usr/local/bin/goboxd
COPY configs/ /configs/
ENV LANGUAGE_CONFIG=/configs/languages/languages.yaml
EXPOSE 8080
ENTRYPOINT ["/usr/local/bin/goboxd"]
13 changes: 11 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
.PHONY: build run test integration lint
.PHONY: build run test integration load lint

COMPOSE ?= docker compose
TOOLS := $(COMPOSE) --profile tools run --rm tools

# Load test knobs (override on the CLI, e.g. `make load LANG=mixed C=6 N=60`)
LANG ?= py3
C ?= 10
N ?= 100
URL ?= http://localhost:8080/run

build:
$(COMPOSE) build goboxd

Expand All @@ -13,7 +19,10 @@ test:
$(TOOLS) go test ./...

integration:
$(TOOLS) go test -tags=integration ./tests/...
$(TOOLS) env LANGUAGE_CONFIG=/src/configs/languages/languages.yaml go test -tags=integration ./tests/...

load:
go run scripts/loadtest.go -c $(C) -n $(N) -url $(URL) -lang $(LANG)

lint:
$(TOOLS) golangci-lint run ./...
99 changes: 53 additions & 46 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,70 +1,77 @@
<div align="center">

# goboxd

**A Go HTTP service for executing untrusted code in isolated sandboxes.**
goboxd (Go Sandbox Daemon) is a Go HTTP service that compiles and executes untrusted code inside an isolated `nsjail` sandbox and returns per-test results.

---

[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](LICENSE)
[![Go](https://img.shields.io/badge/Go-1.23-00ADD8.svg?logo=go&logoColor=white)](https://go.dev)
[![Docker](https://img.shields.io/badge/Docker-Required-2496ED.svg?logo=docker&logoColor=white)](https://www.docker.com)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/thesouldev/goboxd/pulls)
## HTTP Framework Choice

</div>
We use the Go standard library's `net/http` (with the enhanced `ServeMux` introduced in Go 1.22) for all endpoint routing. This design avoids introducing third-party web framework dependencies and guarantees excellent performance with zero external overhead.

---

## Overview
## Features

goboxd is an HTTP service written in Go that compiles and runs untrusted code inside isolated sandboxes and returns the result. Optional test cases can be supplied to assert behaviour against expected output. It is built for safe execution of code across many languages, with strict isolation, bounded concurrency, and a plug and play language registry.
* **Nested Sandboxing:** Dual-layer containment using Docker and `nsjail` (utilizing user/UTS/PID/IPC namespaces, tmpfs mounts, process caps, and disabled networking).
* **Plug-and-Play Languages:** Declarative runtime toolchain specs loaded dynamically via YAML, featuring custom smoke check configuration commands.
* **SJF Concurrency Queue:** Min-heap request scheduling sorted by execution cost, integrated with starvation prevention (wait-time aging) and graceful server shutdown.
* **Load-Adaptive Resource Clamping:** Monitors request rate over a sliding window, dynamically scaling down CPU and memory limits to protect the host under heavy load.
* **Process-Unique UID Isolation:** Maps concurrent execution tasks to process-unique unprivileged UIDs, preventing sibling process and workspace directory collisions.
* **Structured telemetry:** Exposes real-time queue states, active jobs, disk capacity, and registry metadata under `/info`.

## Features
---

- Plug and play language registry driven by YAML
- Process isolation using Linux namespaces and cgroups
- Bounded concurrency with request queuing
- Fully containerised for local development and deployment
- Per request resource limits for time, memory, and processes
- Liveness and readiness probes for orchestration
## Documentation

## Getting started
Extended documentation is located in the `docs/` directory:

### Prerequisites
* **[API Reference](docs/api.md)** — Payload details for `/run`, `/healthz`, `/readyz`, and `/info`.
* **[System Architecture](docs/architecture.md)** — Concurrency scheduling, priority queueing, and lifecycle flows.
* **[Language Registry](docs/languages.md)** — Configuration schema for registering compilers and runtimes.
* **[Security Model](docs/security.md)** — Explaining namespace isolation, UID mapping, and resource limits.
* **[Load Testing](docs/loadtest/runs/README.md)** — MemoryHog breaking-point benchmark: methodology, how to run it, and the per-run results/plots (best config: `CONCURRENCY_LIMIT=8`, breaking point 5 rps @ 2 vCPU / 2 GB).

- Docker with Compose v2
---

## Configuration

No Go toolchain or system dependencies are required on the host. Everything runs in containers.
The service can be configured using the following environment variables:

### Installation
| Variable | Description | Default |
| :--- | :--- | :--- |
| `CONCURRENCY_LIMIT` | Maximum concurrent sandboxes executing at once. | `runtime.NumCPU()` |
| `MAX_QUEUE_SIZE` | Maximum pending requests allowed in the priority queue. | `500` |
| `LANGUAGE_CONFIG` | Path to the registered languages YAML file. | `configs/languages/languages.yaml` |

```sh
git clone https://github.com/thesouldev/goboxd.git
cd goboxd
make build
```
---

### Usage
## Getting Started

```sh
make run # start the service on :8080
make test # run unit tests
make integration # run end to end tests
make lint # run static analysis
```
### Prerequisites

## Project structure
* Docker (with compose v2)

```
.
├── cmd/goboxd/ binary entry point
├── internal/ private application packages
├── docs/ api, languages, security, benchmarks, architecture
└── tests/ integration tests
```
### Booting the Server

## Contributing
1. **Build the container image:**
```bash
make build
```

2. **Run the HTTP service (listens on port 8080):**
```bash
make run
```

---

Contributions are welcome. Open an issue to discuss substantial changes before sending a pull request.
## Command Reference

## License
Every common operation has a dedicated Makefile target:

This project is distributed under the GNU General Public License v3.0. See [LICENSE](LICENSE) for the full text.
* `make build` — Builds the goboxd runtime image.
* `make run` — Spins up the goboxd daemon on port 8080.
* `make test` — Runs unit and configuration validation tests.
* `make integration` — Runs end-to-end sandbox execution tests inside Docker.
* `make load` — Launches a local performance load test against a running server.
* `make lint` — Runs static analysis and code checks (`golangci-lint`).
Empty file removed cmd/goboxd/.gitkeep
Empty file.
130 changes: 130 additions & 0 deletions cmd/goboxd/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// cmd/goboxd/main.go
package main

import (
"context"
"fmt"
"log/slog"
"net/http"
"os"
"os/signal"
"runtime"
"strconv"
"syscall"
"time"

"github.com/thesouldev/goboxd/internal/executor"
"github.com/thesouldev/goboxd/internal/handler"
"github.com/thesouldev/goboxd/internal/languages"
"github.com/thesouldev/goboxd/internal/worker"
)

func main() {
// Configure global structured JSON logger
slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, nil)))

// Clean up stale orphan directories from previous runs/crashes
if err := executor.SweepOrphans(5 * time.Minute); err != nil {
slog.Warn("failed to sweep orphan directories at startup", "err", err)
} else {
slog.Info("startup orphan directory sweep completed")
}

// Load language registry
configPath := envOrDefault("LANGUAGE_CONFIG", "configs/languages/languages.yaml")
registry, err := languages.Load(configPath)
if err != nil {
slog.Error("failed to load language config", "err", err)
os.Exit(1)
}
slog.Info("language registry loaded")

// Determine concurrency limit (default to CPU cores)
concurrencyLimit := runtime.NumCPU()
if envVal := os.Getenv("CONCURRENCY_LIMIT"); envVal != "" {
if val, err := strconv.Atoi(envVal); err == nil && val > 0 {
concurrencyLimit = val
}
} else if envVal := os.Getenv("MAX_CONCURRENT_JOBS"); envVal != "" {
if val, err := strconv.Atoi(envVal); err == nil && val > 0 {
concurrencyLimit = val
}
}

// Determine max queue size (default to 500)
maxQueueSize := 500
if envVal := os.Getenv("MAX_QUEUE_SIZE"); envVal != "" {
if val, err := strconv.Atoi(envVal); err == nil && val >= 0 {
maxQueueSize = val
}
}

slog.Info("initializing concurrency pool",
"concurrency_limit", concurrencyLimit,
"max_queue_size", maxQueueSize,
)

// Instantiate stats and pool
stats := &handler.ServerStats{}
pool := worker.NewConcurrencyPool(concurrencyLimit, maxQueueSize)

mux := http.NewServeMux()

// GET /healthz
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"status":"ok"}`)
})

// GET /readyz
readyHandler := handler.NewReadyHandler(registry)
mux.Handle("GET /readyz", readyHandler)

// GET /info
mux.Handle("GET /info", handler.NewInfoHandler(registry, stats, readyHandler, pool))

// POST /run
mux.Handle("POST /run", &handler.RunHandler{
Registry: registry,
Stats: stats,
Pool: pool,
})

srv := &http.Server{
Addr: ":8080",
Handler: mux,
}

// Graceful shutdown channel
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)

go func() {
slog.Info("server starting", "addr", srv.Addr)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
slog.Error("server failed", "err", err)
os.Exit(1)
}
}()

<-stop
slog.Info("shutting down server gracefully...")

// 15 seconds window to drain in-flight requests
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()

if err := srv.Shutdown(ctx); err != nil {
slog.Error("graceful shutdown failed", "err", err)
} else {
slog.Info("server stopped cleanly")
}
}

func envOrDefault(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}
Loading