Skip to content

Fast fail array decoding#132

Merged
Ryang-21 merged 8 commits intomasterfrom
fast-fail-array-decode
Feb 24, 2026
Merged

Fast fail array decoding#132
Ryang-21 merged 8 commits intomasterfrom
fast-fail-array-decode

Conversation

@Ryang-21
Copy link
Contributor

What

Decoding a malformed array can be fast failed when the array length is greater than the remaining bytes to be decoded. This prevents unnecessary consumption of memory.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This pull request attempts to add fast-fail protection for array decoding to prevent unnecessary memory consumption when decoding malformed XDR arrays. It adds a remainingBytes() method to XdrReader and uses it to check if there are enough bytes available before attempting to decode array elements.

Changes:

  • Added remainingBytes() method to XdrReader to check unread bytes
  • Added fast-fail check in Array.read() to compare array length with remaining bytes
  • Added fast-fail check in VarArray.read() to compare array length with remaining bytes
  • Added tests for the new fast-fail behavior
  • Updated CHANGELOG (with extensive formatting changes)

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/serialization/xdr-reader.js Adds new remainingBytes() method to return unread bytes
src/array.js Adds fast-fail check before decoding array elements
src/var-array.js Adds fast-fail check after reading array length
test/unit/array_test.js Adds test for fast-fail behavior and updates existing test expectations
test/unit/var-array_test.js Adds test for fast-fail behavior
CHANGELOG.md Documents the change (with significant formatting modifications)

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

src/array.js Outdated
Comment on lines +15 to +20
if (this._length > reader.remainingBytes()) {
throw new XdrReaderError(
`insufficient bytes to decode Array of length ${this._length}`
);
}

Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

The fast-fail check compares array length (number of elements) with remaining bytes, which are incompatible units. This check will only catch cases where the number of elements exceeds the number of bytes, but XDR elements typically consume multiple bytes (e.g., Int consumes 4 bytes).

For example, an Array of 2 Int elements requires 8 bytes, but this check would pass if 4 bytes remain (since 2 ≤ 4), leading to a later "read outside boundary" error instead of the intended fast-fail behavior.

This check is too weak to provide the intended protection. Consider either:

  1. Checking if the child type has a known fixed size and multiplying accordingly
  2. Removing this check if the child size cannot be reliably determined
  3. Documenting that this check only provides minimal protection for egregious cases
Suggested change
if (this._length > reader.remainingBytes()) {
throw new XdrReaderError(
`insufficient bytes to decode Array of length ${this._length}`
);
}
// Fast-fail only when the child type has a known fixed byte size.
let elementSize = null;
if (typeof this._childType?.getFixedSize === 'function') {
const size = this._childType.getFixedSize();
if (typeof size === 'number' && size >= 0) {
elementSize = size;
}
} else if (typeof this._childType?.fixedSize === 'number' && this._childType.fixedSize >= 0) {
elementSize = this._childType.fixedSize;
}
if (elementSize !== null) {
const requiredBytes = elementSize * this._length;
if (requiredBytes > reader.remainingBytes()) {
throw new XdrReaderError(
`insufficient bytes to decode Array of length ${this._length}`
);
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +30 to +50
it('fast-fails before decoding child elements when remaining bytes are insufficient', function () {
let calls = 0;
class FixedSizeChild {
static read() {
calls += 1;
return 0;
}

static write() {}

static isValid() {
return true;
}
}

const fixed = new XDR.Array(FixedSizeChild, 2);
const reader = new XdrReader([0x00, 0x00, 0x00, 0x01]);

expect(() => fixed.read(reader)).to.throw(/insufficient bytes/i);
expect(calls).to.eql(0);
});
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

The test expects a fast-fail error when decoding an Array of 2 elements with only 4 bytes available. However, the fast-fail check compares 2 > 4 (array length vs bytes), which is false, so no error will be thrown. The test will fail because:

  1. No exception is thrown (line 48 expectation fails)
  2. The mock FixedSizeChild.read() is called twice, setting calls=2 instead of the expected 0 (line 49 expectation fails)

This test appears to be based on an incorrect understanding of how the fast-fail check works. The check only triggers when array length exceeds remaining bytes as numbers, not when insufficient bytes exist for the actual data.

Copilot uses AI. Check for mistakes.
Comment on lines +22 to +26
if (length > reader.remainingBytes()) {
throw new XdrReaderError(
`insufficient bytes to decode VarArray of length ${length}`
);
}
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

The fast-fail check compares array length (number of elements) with remaining bytes, which are incompatible units. While this check works for the current test case (0 bytes remaining), it provides insufficient protection in general.

For example, if 6 bytes remain after reading the length prefix and the array declares 2 Int elements, the check evaluates 2 > 6 (false), so no error is thrown. However, 2 Int elements require 8 bytes, causing a later "read outside boundary" error instead of an immediate fast-fail.

The check should either:

  1. Account for the known fixed size of child elements when applicable
  2. Be documented as providing only minimal protection for egregious cases where element count exceeds byte count

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

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

This file looks like it has changed and been reformatted which seems unrelated to the issue.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hm prettier must have done that I can revert the fmting changes

@Ryang-21 Ryang-21 linked an issue Feb 17, 2026 that may be closed by this pull request
* Remaining unread bytes in the source buffer
* @return {Number}
*/
remainingBytes() {
Copy link
Contributor

Choose a reason for hiding this comment

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

this could be a get property since there are no side effects, just an idea though

@Ryang-21 Ryang-21 merged commit 03a7c07 into master Feb 24, 2026
3 checks passed
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.

Improve code hardiness when decoding

5 participants