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 | } | ||