Artifact f0b3627056d2ac8e87275546a23227a3c94107a656c76fe09f686ee59809fd44:
- File
internal/sync/sparse_clone_carry.go
— part of check-in
[624df80df2]
at
2026-10-28 13:40:20
on branch perf/cone-clone-resend
— fix(sync): bound a clone cookie's parse and its runs' expansion
A request carries a cookie of any length, and splitting one on ':' or '.' before any bound allocated a string header per separator: 8 MB of dots cost hundreds of megabytes. cloneCapFields now refuses a cookie past maxCloneCookieBytes (64 KiB, well past the longest the server writes) before it splits, and splits with SplitN, as parseCloneRuns does.
A forged cookie could also name one deep chain maxCloneRuns times. The server now rebuilds each head once and at most maxCloneRunRIDs rids in all, newest runs first, and the cookie keeps the newest maxCloneRuns runs rather than the oldest, since an old run may never be reached.
A SHA3 artifact refused to an older peer was recorded as sent, and so carried in a run across rounds; it is now left out as a withheld tree is.
TestConeCloneSurvivesRedeltify re-deltifies a forwarded version against an unreached in-cone blob mid-clone and checks the clone ends whole; it fails with a phantom if the run's checksum is not checked. (user: [email protected], size: 5570) [annotate] [blame] [check-ins using] [more...]
package sync import ( "encoding/binary" "hash/fnv" "strconv" "strings" "github.com/danmestas/go-libfossil/internal/content" libfossil "github.com/danmestas/go-libfossil/internal/fsltype" ) // maxCloneRuns bounds the delta chains a sparse walk's cookie carries from // round to round, and so the cookie's length. A walk item sends its version // with the chain of newer versions above it, so a run stands for one file's // history forwarded ahead of the check-ins that reach it: about one run per // in-cone file. A run dropped past the bound is only sent again. const maxCloneRuns = 1024 // maxCloneRunRIDs bounds the rids one request rebuilds from its cookie's // runs, each a delta-table lookup: about the versions of fossil-scm.org's // src a clone forwards at once, and far short of what a forged cookie could // name (maxCloneRuns runs of maxCloneChainWalk). const maxCloneRunRIDs = 1 << 16 // cloneRunRef is a run as a cookie names it: its head, the number of rids // its delta chain covers from the head, and a checksum of those rids. The // server rebuilds the run from its store and keeps it only when the checksum // agrees, so a store re-deltified since never passes off a base the client // was not sent. type cloneRunRef struct { head, n int sum uint64 } // runSum is the checksum a run's reference carries over its rids, in order. func runSum(rids []int) uint64 { h := fnv.New64a() var buf [8]byte for _, rid := range rids { binary.BigEndian.PutUint64(buf[:], uint64(rid)) h.Write(buf[:]) } return h.Sum64() } // parseCloneRuns reads a cookie's runs: "HEAD.N.SUM" joined by commas, SUM in // base 36. A list that is malformed or longer than maxCloneRuns carries // nothing, which only sends those bases again. func parseCloneRuns(v string) []cloneRunRef { if v == "" || strings.Count(v, ",") >= maxCloneRuns { return nil } parts := strings.Split(v, ",") runs := make([]cloneRunRef, 0, len(parts)) for _, part := range parts { fields := strings.SplitN(part, ".", 4) if len(fields) != 3 { return nil } head, herr := strconv.Atoi(fields[0]) n, nerr := strconv.Atoi(fields[1]) sum, serr := strconv.ParseUint(fields[2], 36, 64) if herr != nil || nerr != nil || serr != nil || head <= 0 || n <= 0 || n > maxCloneChainWalk { return nil } runs = append(runs, cloneRunRef{head: head, n: n, sum: sum}) } return runs } // formatCloneRuns is parseCloneRuns' inverse over runs' rids, keeping the // last maxCloneRuns: the newest, since an old run may never be reached. func formatCloneRuns(runs [][]int) string { runs = runs[max(len(runs)-maxCloneRuns, 0):] parts := make([]string, len(runs)) for i, run := range runs { if len(run) == 0 { panic("sync.formatCloneRuns: empty run") } parts[i] = strconv.Itoa(run[0]) + "." + strconv.Itoa(len(run)) + "." + strconv.FormatUint(runSum(run), 36) } return strings.Join(parts, ",") } // expandCloneRuns rebuilds the runs the request's cookie carried and counts // their rids as sent: each was on the wire in an earlier round of this clone. func (h *handler) expandCloneRuns(round *cloneBatch) error { if len(round.resume.runs) > maxCloneRuns { panic("sync.expandCloneRuns: cookie carried more runs than maxCloneRuns") } for _, ref := range expandable(round.resume.runs) { run, err := h.cloneRun(ref) if err != nil { return err } if run == nil { continue } for _, rid := range run { round.sent[rid] = true } round.runs = append(round.runs, run) } return nil } // expandable is the runs a request rebuilds: each head once, and no more // than maxCloneRunRIDs rids in all, the newest runs first. One left out is // only sent again. func expandable(refs []cloneRunRef) []cloneRunRef { out := make([]cloneRunRef, 0, len(refs)) heads := make(map[int]bool, len(refs)) total := 0 for i := len(refs) - 1; i >= 0; i-- { ref := refs[i] if heads[ref.head] || total+ref.n > maxCloneRunRIDs { continue } heads[ref.head] = true total += ref.n out = append(out, ref) } return out } // cloneRun follows ref's delta chain from its head, or returns nil when the // store no longer holds that chain as the cookie's checksum names it. func (h *handler) cloneRun(ref cloneRunRef) ([]int, error) { run := make([]int, 0, ref.n) for rid := ref.head; len(run) < ref.n; { if rid <= 0 || rid > h.cloneSnapMax { return nil, nil } run = append(run, rid) if len(run) == ref.n { break } src, err := content.DeltaSource(h.repo.DB(), libfossil.FslID(rid)) if err != nil { return nil, err } rid = int(src) } if runSum(run) != ref.sum { return nil, nil } return run, nil } // noteRun records the sources a send forwarded ahead of top as a run. rids // is the send's, source first, so reversed it follows the chain down from // top's own source. func (round *cloneBatch) noteRun(rids []int, top int) { run := make([]int, 0, len(rids)) for i := len(rids) - 1; i >= 0; i-- { if rids[i] != top { run = append(run, rids[i]) } } if len(run) > 0 { round.runs = append(round.runs, run) } } // carriedRuns is what the next round's cookie carries: each run less the // leading rids this round's walk reached, which no later walk needs skipped. // The walk reaches a file's versions oldest first, and so a run's from its // head. func (round *cloneBatch) carriedRuns() [][]int { out := make([][]int, 0, len(round.runs)) for _, run := range round.runs { for len(run) > 0 && round.reached[run[0]] { run = run[1:] } if len(run) > 0 { out = append(out, run) } } return out }