Skip to content

fix: store LongBits words unsigned - #212

Merged
achingbrain merged 1 commit into
ipfs:mainfrom
SgtPooki:fix/longbits-unsigned-words
Oct 15, 2026
Merged

achingbrain merged 1 commit into
ipfs:mainfrom
SgtPooki:fix/longbits-unsigned-words

Conversation

@SgtPooki

@SgtPooki SgtPooki commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

LongBits stores its low word signed (this.lo = lo | 0). For a 64-bit value with hi == 0 and lo in [2^31, 2^32) the word is negative, so writeVarint64 writes one byte into the 5 bytes length() reserved. The other 4 bytes are whatever was in the allocUnsafe slab.

  • affected: uint64, int64
  • not affected: sint64, fixed64, sfixed64

Repro

# protons-runtime 8.0.1 (and 5.0.4 through main)
node --zero-fill-buffers --input-type=module -e 'import {writer} from "protons-runtime"; const w = writer(); w.uint64(2147483648n); console.log(Buffer.from(w.finish()).toString("hex"))'
# 0000000000

# protobufjs 8.8.1, same value
node -e 'const {Writer} = require("protobufjs/minimal"); console.log(Buffer.from(Writer.create().uint64(2147483648).finish()).toString("hex"))'
# 8080808008

# protons-runtime 5.0.3 (last release wrapping protobufjs), same command as the first
# 8080808008

Without --zero-fill-buffers the 4 bytes are Buffer pool leftovers, so the same node encodes differently depending on what ran before it. Browser builds of uint8arrays zero-fill, so there it is always the 0000000000 form.

# same UnixFS node, 200 times, with some allocations in between
node --input-type=module -e 'import {UnixFS} from "ipfs-unixfs"; import * as dagPB from "@ipld/dag-pb"; import {sha256} from "multiformats/hashes/sha2"; import {CID} from "multiformats/cid"; const cids = new Set(); for (let i = 0; i < 200; i++) { for (let j = 0; j < 50; j++) Buffer.allocUnsafe(1 + (i * 7 + j * 13) % 500).fill(i + j); const bytes = dagPB.encode({Data: new UnixFS({type:"file",blockSizes:[2147483648n]}).marshal(), Links: []}); cids.add(CID.createV1(dagPB.code, await sha256.digest(bytes)).toString()) } console.log(cids.size, "distinct CIDs for the same node")'
# 25 distinct CIDs for the same node

Impact

UnixFS filesize and blocksizes are uint64. A file node is invalid protobuf when its filesize or any blocksizes entry is in [2^31, 2^32). That is every multi-block file of 2 GiB to 4 GiB, plus larger files with a subtree in that range.

Where it came from

  • d101804 (fix: port protobuf reader/writer to ts #60, "fix: port protobuf reader/writer to ts", 2023-10-13), released as protons-runtime 5.0.4 the same day
  • protobufjs stores both words with >>> 0 (src/util/longbits.js#L23-L29 at protobufjs-v7.2.5, the line 5.0.3 wrapped); the port changed them to | 0
  • protons-runtime: 5.0.3 and earlier correct, 5.0.4 through 8.0.1 and main broken (bisected, then each major checked from npm)
  • ipfs-unixfs: 11.0.0 through 11.1.3 (^5.0.0) break on any install after 2023-10-13; 11.1.4 and later (^5.4.0, ^5.5.0, ^6.0.1, ^7.0.0, ^7.1.1, ^8.0.1) have never resolved to a working runtime; a lockfile pinned to 5.0.3 is fine

Change

Restore >>> 0 on both words. Regression test at the window edges for uint64 and int64, red on main, green here.

Checked 13,483 encodings across all five 64-bit codecs before and after: the 520 in-window cases change to the correct bytes, nothing else moves. INT64_MIN is mis-encoded on int64/sint64/sfixed64 both before and after; separate overflow in fromBigInt, not touched here.

fixes #213

LongBits stores lo as lo | 0. For values with hi == 0 and lo in
[2^31, 2^32) that is negative, so writeVarint64 writes one byte into the
5 bytes length() reserved and leaves 4 bytes of the allocUnsafe slab
unwritten. uint64 and int64 are affected. sint64 and fixed64 are not.

protobufjs stores both words with >>> 0. The port in d101804 changed
them to | 0. Restore >>> 0 and add a regression test at the window edges.
@SgtPooki
SgtPooki force-pushed the fix/longbits-unsigned-words branch from 42af6a1 to 8282f66 Compare October 15, 2026 12:42
@SgtPooki
SgtPooki marked this pull request as ready for review October 15, 2026 12:47
@SgtPooki

Copy link
Copy Markdown
Member Author

@wemeetagain @achingbrain can we get this in ASAP? two-line fix with test included. every 2 to 4 GiB file written through ipfs-unixfs since 2023-10-13 has a root node Go can't read.

@SgtPooki

Copy link
Copy Markdown
Member Author

other callers of uint64 in the libp2p tree that hit the same window (read from the generated codecs, not run against go):

  • ipns ttl is nanoseconds, so any ttl between 2.15s and 4.29s encodes corrupt. helia's default is 1h so nobody has hit it, but a short ttl would produce a record go-ipns can't parse.
  • @libp2p/circuit-relay-v2 reservation expire is unix seconds. values cross 2^31 on 2038-01-19, after which every js relay reservation is malformed until this lands. same for the data limit if anyone sets it to 2 to 4 GiB.

so this isn't only a unixfs problem.

@hugomrdias hugomrdias 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.

LGTM

@achingbrain
achingbrain merged commit 815bfb7 into ipfs:main Oct 15, 2026
37 checks passed
github-actions Bot pushed a commit that referenced this pull request Oct 15, 2026
## [protons-runtime-v8.0.2](protons-runtime-8.0.1...protons-runtime-8.0.2) (2026-10-15)

### Bug Fixes

* store LongBits words unsigned ([#212](#212)) ([815bfb7](815bfb7)), closes [#213](#213)
hugomrdias added a commit to filecoin-project/agents that referenced this pull request Oct 15, 2026
…files

protons-runtime 5.0.4 through 8.0.1 encodes uint64 values in [2^31, 2^32)
as one varint byte plus four bytes of stale Buffer pool memory
(ipfs/protons#212). packDirectory wrote a corrupt file-root node for any
2–4 GiB file in a folder: the root CID changed between runs and extractCar
failed with "invalid wire type".

No fixed release is on npm, so patch protons-runtime 6.0.2 (importer) and
8.0.1 (exporter) with the upstream one-line fix. Add a regression test that
builds a 3 GiB file root from fake leaves, and document that folders stored
with filecoin 0.5.0 or earlier holding such a file must be stored again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@achingbrain

Copy link
Copy Markdown
Member

Looks like publishing to npm failed here.

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.

uint64 values in [2^31, 2^32) encode as one byte plus uninitialised memory (LongBits stores lo as signed)

3 participants