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/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software:2222/vole.git

A free and open-source forge that lives in your Git repo.

Bound and stream the anonymous read path

4581690d1ae9d87c272428d101a0fbe3538d7f44 · Keenan Nemetz · 13d ago · parent e05f1c48

Bound and stream the anonymous read path

Every anonymous read path read a whole git object before deciding what to do
with it: the code browser truncated after allocating, the overview rendered the
README it had just read in full, and the site pool allocated the entire object
for every request. `max_blob_bytes` allows a 100 MiB blob, so one pushed file
made every concurrent request for it a 100 MiB allocation, with no account
needed to ask for it.

Read the header first instead. `core/git` gains `BlobSize`, `BlobSizes` (one
`cat-file --batch-check` for a whole tree) and `BlobStream`, which parses the
batch header before touching the payload and stops the process when a caller
wants only a prefix.

Then use it: `/raw` streams with the object's own `Content-Length`, the inline
viewer reads a one-MiB prefix and reports the file's real size, the README is
capped and named instead of rendered when it is too large, site blobs are capped
at 25 MiB with a 413, and the commit and tree objects read to resolve a site
path are capped separately.

Metadata gets the same treatment on both sides: 4 MiB per file, 2 MiB per body,
20,000 files and 64 MiB for `issues/` and `requests/` together. The loader asks
git for every size in one call and refuses the tree before reading it; the push
boundary refuses the same tree, so the author learns why instead of finding that
their issue list stopped loading. Counting is now per subtree, so the machine
written `runs/` tree no longer eats the issue file budget.

server/pages/catfile.go +28−7

@@ -56,9 +56,12 @@ type object struct {
56 56 Data []byte
57 57 }
58 58
59 // read resolves rev and returns its content. rev may be a ref name, which is how
60 // the pool answers "what commit is this branch on" without a second subprocess.
61 func (p *objectPool) read(rev string) (object, error) {
59 // read resolves rev and returns its content, refusing anything past max bytes.
60 // rev may be a ref name, which is how the pool answers "what commit is this
61 // branch on" without a second subprocess. A refused object is never allocated:
62 // its size is in the batch header, so the answer costs one header line and the
63 // process is dropped rather than drained.
64 func (p *objectPool) read(rev string, max int64) (object, error) {
62 65 if strings.ContainsAny(rev, "\n\r") {
63 66 return object{}, fmt.Errorf("pages: malformed revision %q", rev)
64 67 }
@@ -66,7 +69,7 @@ func (p *objectPool) read(rev string) (object, error) {
66 69 proc := p.procs[p.next%len(p.procs)]
67 70 p.next++
68 71 p.mu.Unlock()
69 return proc.read(rev)
72 return proc.read(rev, max)
70 73 }
71 74
72 75 func (p *objectPool) Close() {
@@ -75,7 +78,7 @@ func (p *objectPool) Close() {
75 78 }
76 79 }
77 80
78 func (proc *catFileProc) read(rev string) (object, error) {
81 func (proc *catFileProc) read(rev string, max int64) (object, error) {
79 82 proc.mu.Lock()
80 83 defer proc.mu.Unlock()
81 84 if proc.dead || proc.cmd == nil {
@@ -105,6 +108,13 @@ func (proc *catFileProc) read(rev string) (object, error) {
105 108 proc.dead = true
106 109 return object{}, fmt.Errorf("pages: bad object size in %q", line)
107 110 }
111 if max > 0 && size > max {
112 // The object is still pending on the stream. Killing the process is
113 // cheaper and safer than reading a blob we are not going to keep: the next
114 // read starts a fresh one.
115 proc.stop()
116 return object{}, fmt.Errorf("%w: %s is %d bytes, limit %d", errObjectTooLarge, rev, size, max)
117 }
108 118 data := make([]byte, size)
109 119 if _, err := io.ReadFull(proc.out, data); err != nil {
110 120 proc.dead = true
@@ -141,11 +151,22 @@ func (proc *catFileProc) start() error {
141 151 func (proc *catFileProc) close() {
142 152 proc.mu.Lock()
143 153 defer proc.mu.Unlock()
154 proc.stop()
155 }
156
157 // stop kills the process behind the stream, leaving it to restart lazily. The
158 // caller holds proc.mu.
159 func (proc *catFileProc) stop() {
144 160 if proc.cmd == nil {
161 proc.dead = true
145 162 return
146 163 }
147 proc.in.Close()
148 proc.cmd.Process.Kill()
164 if proc.in != nil {
165 proc.in.Close()
166 }
167 if proc.cmd.Process != nil {
168 proc.cmd.Process.Kill()
169 }
149 170 proc.cmd.Wait()
150 171 proc.cmd, proc.in, proc.out, proc.dead = nil, nil, nil, true
151 172 }