vole
Clone
Read only, no credentials:
https://rodent.software/git/vole.git
Write over SSH with a registered key:
git clone ssh://git@0.0.0.0:2222/vole.git
A free and open-source forge that lives in your Git repo.
Bound what a CI job can consume
6bfc42949be4d40b3b74de8cc17c6d903cfe531e · Keenan Nemetz · 12d ago · parent f5456470
Bound what a CI job can consume A job runs untrusted build code, and everything it produces is either stored by the instance or kept on the runner. None of those channels had a bound of its own. The attempt log was a strings.Builder: the whole run in memory, then written to a temp file, then read twice by the LFS transfer (once to hash, once to send). It is now a spool file on disk from the first byte, capped at 32 MiB, uploaded once at the end as an ordinary LFS object. A capped log reports the whole write as taken so the producing process is not killed, and gets a line saying it was truncated. Step offsets are computed from the spool's length, which never exceeds the cap, so a truncated log still has valid ranges. Nothing is streamed to the server: a log's content-addressed id cannot be known before its last byte, so streaming would only buy a second protocol. A run may publish at most 256 artifact files totalling 4 GiB. The workflow limit is on declarations, and a declaration is a pattern that can match a directory tree, so the bound on what it expands to has to be where the tree is walked. Contributor caches are now namespaced per account, not one shared "contrib" namespace: one contributor's untrusted branch must not be able to seed another contributor's cache. A contributor keeps their cache across their own branches. Container volume paths are checked before the argument list is built: a colon is the field separator, and a NUL or newline would split the argument, so a path carrying one is refused rather than passed to the runtime. RunArgs is unchanged; the refusal is in Start, and the closed-set test now also pins --cap-add, --device, --pid/--ipc host and the podman socket. Image builds run through the configured runtime, so a rootless Podman runner builds rootless. The definition is pinned by digest already; the docs now say how the build gets its network and recommend pinning the base image in from: by digest for a reproducible build. Tests: the spool keeps what fits, truncates and says so, and removes its file; artifact count and byte caps (the byte cap uses a sparse file so it does not write 4 GiB); contributor caches are separated by account while one account's branches share; volume paths with a colon, NUL or newline are refused.
docs/runner.md +35
| @@ -55,6 +55,18 @@ environment variable cannot loosen it: an explicit | |||
| 55 | 55 | `GIT_SSH_COMMAND=... StrictHostKeyChecking=no` is overridden by the runner's own | |
| 56 | 56 | options, which come first. | |
| 57 | 57 | ||
| 58 | ## Images and image builds | ||
| 59 | |||
| 60 | An `image-file` definition is built by the configured runtime, so a rootless | ||
| 61 | Podman runner builds rootless. The build runs with the runtime's own network | ||
| 62 | (bridge by default), because the base image and any `run:` commands need to fetch | ||
| 63 | packages; if builds should have no network, preload the base image and set the | ||
| 64 | runtime's default network to none on that host. The definition is pinned by its | ||
| 65 | digest — the runner receives the definition text and digest with the job, tags the | ||
| 66 | built image by that digest, and reuses it — so a definition that changes after a | ||
| 67 | job is queued is not the one that was reviewed. Pin the base image in `from:` by | ||
| 68 | digest (`image@sha256:...`) when you need the build itself to be reproducible. | ||
| 69 | |||
| 58 | 70 | ## What a job may name | |
| 59 | 71 | ||
| 60 | 72 | The server validates a workflow when it is pushed, and the runner validates the | |
| @@ -67,6 +79,29 @@ absolute path, a `:` or NUL in a value used to build a container argument, and a | |||
| 67 | 79 | commit id that is not a commit id are all refused at the runner, not only at the | |
| 68 | 80 | server. | |
| 69 | 81 | ||
| 82 | ## What a job may consume | ||
| 83 | |||
| 84 | A job runs untrusted build code, and everything it produces is stored by the | ||
| 85 | instance or kept on the runner, so each of those channels has a bound: | ||
| 86 | |||
| 87 | - **Logs.** One attempt's log is capped at 32 MiB. Past that the runner keeps the | ||
| 88 | first 32 MiB and appends a line saying it was truncated, so a build that loops | ||
| 89 | printing cannot fill the runner's disk or the instance's object store. The log | ||
| 90 | is written straight to a file on the runner and uploaded once at the end; it is | ||
| 91 | not held in memory and nothing is streamed to the server. | ||
| 92 | - **Artifacts.** A workflow declares at most 32 patterns, and a run may publish at | ||
| 93 | most 256 files totalling 4 GiB. The expanded set is checked before anything is | ||
| 94 | uploaded; a pattern that matches a directory tree is the usual way to hit this. | ||
| 95 | - **Cache.** `--cache-size` (5 GiB by default) caps the cache root, enforced by | ||
| 96 | evicting whole namespaces. Each repository gets its own namespace, and each | ||
| 97 | contributor account gets its own inside a repository, so one contributor's | ||
| 98 | branch cannot seed the cache another contributor's branch builds from. A | ||
| 99 | protected branch never shares a cache with a contributor branch. | ||
| 100 | - **Container.** `--container-memory`, `--container-cpus` and `--container-pids` | ||
| 101 | pass the runtime's own limits; `--container-network none` (the default) means a | ||
| 102 | step with no network. The runtime's storage driver, not Vole, is what bounds the | ||
| 103 | container's writable layer, so size it on the host. | ||
| 104 | |||
| 70 | 105 | ## Images and image builds | |
| 71 | 106 | ||
| 72 | 107 | Every job names its image in the workflow. A registry reference is used as | |