Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

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