Skip to content

fix(serializer): throw on writes exceeding SigmaByteWriter capacity - #226

Open
Kshot3000 wants to merge 1 commit into
fleet-sdk:masterfrom
Kshot3000:fix/writer-capacity-overflow
Open

Kshot3000 wants to merge 1 commit into
fleet-sdk:masterfrom
Kshot3000:fix/writer-capacity-overflow

Conversation

@Kshot3000

Copy link
Copy Markdown

Summary

SigmaByteWriter fails open when a write exceeds its fixed buffer capacity:

  • write(byte) assigns through a bare Uint8Array index. An out-of-bounds assignment is silently ignored by the platform, but #cursor++ still advances past the end — so the byte vanishes and the cursor claims it was written.
  • writeBits(bits) has the same flaw: bits past the capacity are computed against undefined and dropped while the cursor advances.
  • toBytes() then returns subarray(0, cursor), which clamps to the real buffer length, hiding the corruption entirely. The caller gets a silently truncated serialization that parses as a different value (or fails downstream with an unrelated error).

Every in-repo caller sizes its writer with an exact estimate — ErgoTree's computed total size, the box serializer's 4096-byte maximum, SConstant's guessed size, even the test suite's new SigmaByteWriter(tv.hex.length) exact-length writers — so an overflow can only mean the estimate was wrong, and wrong estimates were being converted into corrupt output instead of an error.

Fix

  • write() throws a RangeError naming the position and the buffer capacity when the cursor is already at the end, checked before writing, so a failed write leaves the cursor and the written bytes untouched.
  • writeBits() performs the same check upfront for the ceil(bits / 8) bytes it needs.
  • writeBytes() is deliberately unchanged: it delegates to Uint8Array.set, which already throws a RangeError on overflow without copying anything (pinned by a regression test here).
  • Everything layered on these primitives (writeBool, the VLQ writers behind writeUInt/writeBigUInt, the zig-zag integer writers, writeHex, writeChecksum) now fails closed through them.

This is the write-side mirror of the SigmaByteReader out-of-bounds fix (out-of-bounds reads now throw instead of inventing data): the codec fails closed on length mismatches in both directions. One design note: the reference Scala writer appends to a growable buffer, but Fleet's writers are deliberately fixed-capacity with exact up-front estimates, so failing loudly on an exceeded estimate is the behavior consistent with this codebase rather than silently growing the buffer and masking the estimation bug.

Deliberate non-change: the byte value passed to write() is still not range-checked (e.g. 256 wraps to 0 via the typed array, as before) — the typed writers are the checked layer, as with the earlier range fixes in this series.

Tests

Tests first: on pristine master, the new write, VLQ-overflow, and writeBits tests fail (nothing is thrown; bytes are silently dropped), while the writeBytes test already passes via Uint8Array.set. With the fix:

  • 4 new tests in sigmaByteWriter.spec.ts: single-byte overflow (with cursor/content untouched after the throw), byte-array overflow plus exact-fit success, multi-byte VLQ overflow through both writeUInt and writeBigUInt, and bit overflow plus an exact-fit 8-bit write.
  • Writer spec: 11/11. Full monorepo suite: 1136/1136 across 66 files — proving no in-repo serialization path relies on the silent-drop behavior or under-sizes its writer.
  • Biome check clean on the changed files.

Changeset included: @fleet-sdk/serializer, patch.

— @Kshot9000

SigmaByteWriter.write() used a bare Uint8Array index assignment, so a
write past the fixed buffer capacity was silently dropped while the
cursor advanced past the end; writeBits() had the same flaw. Callers
size buffers with exact estimates (ErgoTree total size, box size, exact
VLQ test lengths), so an overflow means the estimate was wrong — and
the writer returned a silently truncated serialization instead of
failing. toBytes() then clamped the over-advanced cursor, hiding the
corruption entirely.

write() and writeBits() now throw a RangeError naming the position and
the buffer capacity, checked before any byte or bit is written so a
failed write leaves the cursor and content untouched. writeBytes()
already failed closed via Uint8Array.set and is unchanged.

This mirrors the SigmaByteReader out-of-bounds fix: the codec fails
closed on length mismatches in both directions.
@changeset-bot

changeset-bot Bot commented Oct 8, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f3c3c27

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@fleet-sdk/serializer Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant