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.

ci/container/container_test.go +31

@@ -59,10 +59,16 @@ func TestRunArgsAreClosed(t *testing.T) {
59 59 }
60 60 for _, unwanted := range []string{
61 61 "--privileged",
62 "--cap-add",
63 "--device",
64 "--pid host",
65 "--ipc host",
62 66 "--network host",
63 67 "/var/run/docker.sock",
68 "/run/podman/podman.sock",
64 69 "SSH_AUTH_SOCK",
65 70 "/home/runner",
71 "/etc/",
66 72 } {
67 73 if strings.Contains(args, unwanted) {
68 74 t.Fatalf("container arguments contain %q:\n%s", unwanted, args)
@@ -124,3 +130,28 @@ func TestImageTagFollowsTheDigest(t *testing.T) {
124 130 t.Fatalf("tag kept the whole digest: %q", a)
125 131 }
126 132 }
133
134 // A host path becomes a `--volume` field, and a colon, NUL or newline in one would
135 // be read as an extra field or split the argument. The runner builds these paths,
136 // so the check is a guarantee rather than a discovery.
137 func TestInvalidVolumePathsAreRefused(t *testing.T) {
138 t.Parallel()
139 for _, spec := range []Spec{
140 {Work: "/work/job:ro"},
141 {Work: "/work/job\x00"},
142 {Work: "/work/job\n"},
143 {Work: "/work/job", Mounts: []Mount{{Host: "/cache:x", Path: "/cache"}}},
144 {Work: "/work/job", Mounts: []Mount{{Host: "/cache", Path: "/cache\r"}}},
145 {Work: ""},
146 } {
147 if err := ValidatePaths(spec); err == nil {
148 t.Fatalf("ValidatePaths(%+v) = nil", spec)
149 }
150 }
151 if err := ValidatePaths(Spec{
152 Work: "/work/job",
153 Mounts: []Mount{{Host: "/var/cache/vole/demo", Path: "/cache"}},
154 }); err != nil {
155 t.Fatalf("a valid spec was refused: %v", err)
156 }
157 }