Skip to content

*: Reject malformed variable-length integers#2090

Merged
pjbgf merged 1 commit into
go-git:mainfrom
hiddeco:validate-variable-length-integers
May 7, 2026
Merged

*: Reject malformed variable-length integers#2090
pjbgf merged 1 commit into
go-git:mainfrom
hiddeco:validate-variable-length-integers

Conversation

@hiddeco

@hiddeco hiddeco commented May 7, 2026

Copy link
Copy Markdown
Member

Each delta operation's declared size is validated against the size still remaining in the target buffer, not the total target size. The previous check against the total accepted a sequence of operations whose cumulative output would exceed the declared target, leaving the unsigned remaining counter unable to reach zero and the loop free to keep writing past the declared target until the delta payload was consumed.

Three sibling code paths shared the flaw and are updated together: patchDelta (used by PatchDelta and ApplyDelta), ReaderFromDelta, and patchDeltaWriter (used by the packfile parser).

The loop structure is rewritten as for remainingTargetSz > 0 so the invariant is explicit, and a post-loop check rejects deltas that leave bytes unconsumed -- matching the data != top sanity check in patch-delta.c1. Empty-target deltas (header-only, no operations) are now accepted, also matching upstream.

Several incidental defects on the same call paths are fixed: patchDelta's copy-from-src failure used break, which only exited the switch and let the loop keep consuming delta bytes; patchDeltaWriter returned (0, ZeroHash, nil) on invalid input, silently reporting success; and ReaderFromDelta's copy-from-src failure closed the writer without an error, silently truncating the consumer stream. Each now propagates ErrInvalidDelta (or ErrDeltaCmd) consistently.

packfile.getMemoryObject was re-inflating delta payloads into the buffer the scanner had already populated, appending a duplicate copy that previously went unnoticed because the loop silently ignored anything past the target. The fix matches the cached-content pattern already used in Scanner.WriteObject.

@hiddeco hiddeco force-pushed the validate-variable-length-integers branch from 5fd6b38 to 315abf5 Compare May 7, 2026 18:56
// start of some binary data and returns the decoded number, the rest
// of the bytes, and an error if the encoded value does not fit in a
// uint.
func DecodeLEB128(input []byte) (uint, []byte, error) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Signature change, but I believe this is still acceptable for v6?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This package was actually introduced in v6, so never release exposed on a stable release.

@hiddeco hiddeco force-pushed the validate-variable-length-integers branch from 315abf5 to 6c32091 Compare May 7, 2026 19:03
@pjbgf pjbgf added the breaking label May 7, 2026
Comment thread plumbing/format/packfile/patch_delta.go Outdated
@hiddeco hiddeco force-pushed the validate-variable-length-integers branch from 6c32091 to 7439d40 Compare May 7, 2026 21:13
Variable-length integer decoders accepted continuation chains long
enough to push the running shift past the bit-width of their
accumulator, producing out-of-range values that propagated into
buffer allocation hints and on-disk size fields.

Mirror the bound from canonical Git's `unpack_object_header_buffer`[1]:
reject any continuation byte that would shift past the type's
capacity. The same rule applies to `VariableLengthSize`, `DecodeLEB128`,
`DecodeLEB128FromReader` and `ReadVariableWidthInt`; `DecodeLEB128` now
returns an error so callers surface invalid input rather than
silently zeroing the result.

Add defense-in-depth at every call site that allocates from a parsed
length: cap `bytes.Buffer.Grow` hints in `Parser.parentReader` and
`patchDeltaWriter` at 1 GiB / `maxPatchPreemptionSize`, cap
`parserCache.Reset` at a 4 Mi-entry hint so a hostile `ObjectsQty`
cannot request a multi-gigabyte slice, validate `OFS-delta`
back-references in the streaming and mmap paths (must be `> 0` and
`<= o.offset`), and reject `mmap.fsobject` sizes whose top bit is
set rather than truncating into a negative `int64`.

Cover the new behavior with unit, parser-level regression and fuzz
tests (`FuzzVariableLengthSize`, `FuzzDecodeLEB128`, `FuzzParser`).

[1]: https://github.com/git/git/blob/v2.54.0/packfile.c#L1135-L1158

Assisted-by: Claude Opus 4.7
Signed-off-by: Hidde Beydals <hidde@hhh.computer>
@hiddeco hiddeco force-pushed the validate-variable-length-integers branch from 7439d40 to 5b89cb5 Compare May 7, 2026 21:24

@pjbgf pjbgf left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hiddeco thanks for working on this. 🙇

hiddeco added a commit to hiddeco/go-git that referenced this pull request May 7, 2026
Variable-length integer decoders accepted continuation chains long
enough to push the running shift past the bit-width of their
accumulator, producing out-of-range values that propagated into
buffer allocation hints and on-disk size fields.

Mirror the bound from canonical Git's `unpack_object_header_buffer`[1]:
reject any continuation byte that would shift past the type's
capacity. The same rule applies to `Scanner.readLength`, the
package-local `decodeLEB128`/`decodeLEB128ByteReader`, and
`ReadVariableWidthInt`; `decodeLEB128` now returns an error so
callers surface invalid input rather than silently zeroing the
result.

Add defense-in-depth at every call site that allocates from a
parsed length: cap `bytes.Buffer.Grow` hints in the parser at
1 GiB, clamp the slice and maps initialized from `Parser.count`
at a 4 Mi-entry hint so a hostile object count cannot request a
multi-gigabyte allocation, and validate `OFS-delta` back-references
in the scanner (must be `> 0` and `<= h.Offset`).

Cover the new behavior with unit, parser-level regression and
fuzz tests (`FuzzReadLength`, `FuzzDecodeLEB128`,
`FuzzDecodeLEB128ByteReader`, `FuzzParser`).

[1]: https://github.com/git/git/blob/v2.54.0/packfile.c#L1135-L1158

Backport of go-git#2090.

Assisted-by: Claude Opus 4.7
Signed-off-by: Hidde Beydals <hidde@hhh.computer>
@pjbgf pjbgf merged commit ab78ec5 into go-git:main May 7, 2026
15 of 17 checks passed
@hiddeco hiddeco deleted the validate-variable-length-integers branch May 7, 2026 22:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants