-
Notifications
You must be signed in to change notification settings - Fork 1.8k
AVRO-4295: [csharp] Bound allocation when decoding length-prefixed values and collections #3860
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 21 commits
2210a46
04308d0
7376905
4232c99
601f0ab
d200b5f
99cefe0
3671989
1af6c0a
d308962
cf8a687
20fb487
6350b1e
91609bd
345a803
ffecc24
8361138
98f8da0
b5d6c69
22b8cac
6a9cb20
ed310ad
1ed2417
c5e4306
57aae1f
43b262d
824bc73
331dc22
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -76,7 +76,22 @@ public long ReadLong() | |
| int shift = 7; | ||
| while ((b & 0x80) != 0) | ||
| { | ||
| // A 64-bit value uses at most 10 bytes (shifts 0..63); reject an | ||
| // overlong varint rather than silently wrapping to a wrong value. | ||
| if (shift >= 70) | ||
| { | ||
| throw new AvroException("Varint is too long"); | ||
| } | ||
|
|
||
| b = read(); | ||
| // The 10th byte (shift == 63) contributes only bit 63; any higher | ||
| // payload bit (b & 0x7E) would be silently dropped by << 63, so a | ||
| // valid encoding must have them clear. Reject otherwise. | ||
| if (shift == 63 && (b & 0x7E) != 0) | ||
| { | ||
| throw new AvroException("Invalid long encoding"); | ||
| } | ||
|
|
||
| n |= (b & 0x7FUL) << shift; | ||
| shift += 7; | ||
|
Comment on lines
86
to
96
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed — when
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Already handled — ReadLong rejects a 10th byte whose payload exceeds bit 63 via |
||
| } | ||
|
|
@@ -264,11 +279,73 @@ public void SkipFixed(int len) | |
| // Read p bytes into a new byte buffer | ||
| private byte[] read(long p) | ||
| { | ||
| byte[] buffer = new byte[p]; | ||
| if (p < 0) | ||
| { | ||
| throw new AvroException($"Can not read a negative number of bytes: {p}"); | ||
| } | ||
|
|
||
| if (p > MaxDotNetArrayLength) | ||
| { | ||
| // A .NET array cannot be larger than this; reject with a | ||
| // consistent AvroException rather than letting new byte[p] throw | ||
| // an OverflowException/OutOfMemoryException, mirroring the | ||
| // maximum-length guard in ReadString() (the message differs). | ||
| throw new AvroException($"Length {p} exceeds the maximum supported array length"); | ||
|
Comment on lines
+289
to
+293
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 1af6c0a: reworded the comment to say the guard 'mirrors' ReadString()'s max-length guard and notes the message differs, rather than claiming an exact match. |
||
| } | ||
|
|
||
| EnsureAvailableBytes(p); | ||
| // p has been bounded to <= MaxDotNetArrayLength above, so the cast to | ||
| // int (required for array allocation) cannot overflow. | ||
| byte[] buffer = new byte[(int)p]; | ||
| Read(buffer, 0, buffer.Length); | ||
|
iemejia marked this conversation as resolved.
iemejia marked this conversation as resolved.
|
||
| return buffer; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// When the underlying stream can report its length, verifies that at | ||
| /// least <paramref name="length"/> bytes remain before the caller | ||
| /// allocates a buffer of that size. This guards against an | ||
| /// out-of-memory attack from a malicious or truncated input that | ||
| /// declares a huge length prefix but carries little actual data. The | ||
| /// check is skipped for non-seekable streams, whose remaining length is | ||
| /// unknown. | ||
| /// </summary> | ||
| /// <param name="length">Number of bytes about to be read.</param> | ||
| internal void EnsureAvailableBytes(long length) | ||
| { | ||
| if (length > 0) | ||
| { | ||
| long remaining = RemainingBytes(); | ||
| if (remaining >= 0 && length > remaining) | ||
| { | ||
| throw new AvroException( | ||
| $"Cannot read {length} bytes, only {remaining} bytes remaining in the stream"); | ||
| } | ||
| } | ||
| } | ||
|
iemejia marked this conversation as resolved.
|
||
|
|
||
| /// <summary> | ||
| /// Returns the number of bytes still available to read from the | ||
| /// underlying stream when it is seekable, or -1 when that count is not | ||
| /// known (a non-seekable stream). Used to reject a declared length or a | ||
| /// collection block count that exceeds the data actually available | ||
| /// before allocating for it. | ||
| /// </summary> | ||
| /// <returns>The number of bytes remaining, or -1 if unknown.</returns> | ||
| public long RemainingBytes() | ||
| { | ||
| if (!stream.CanSeek) | ||
| { | ||
| return -1; | ||
| } | ||
|
|
||
| // Clamp to 0: if the stream was externally seeked past its end (or | ||
| // truncated), Position can exceed Length. Callers should only ever | ||
| // see -1 (unknown) or a non-negative count. | ||
| long remaining = stream.Length - stream.Position; | ||
| return remaining < 0 ? 0 : remaining; | ||
| } | ||
|
iemejia marked this conversation as resolved.
|
||
|
|
||
| private byte read() | ||
| { | ||
| int n = stream.ReadByte(); | ||
|
|
@@ -281,6 +358,14 @@ private long doReadItemCount() | |
| long result = ReadLong(); | ||
| if (result < 0) | ||
| { | ||
| // long.MinValue cannot be negated (it would overflow); reject it | ||
| // explicitly rather than propagating a wrapped negative or, under | ||
| // checked arithmetic, throwing an OverflowException. | ||
| if (result == long.MinValue) | ||
| { | ||
| throw new AvroException("Invalid negative block count: " + result); | ||
| } | ||
|
|
||
| ReadLong(); // Consume byte-count if present | ||
| result = -result; | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch — this is a real gap specific to C#'s architecture:
PreresolvingDatumReader<T>(base of SpecificDatumReader/GenericDatumReader) is a separate reader implementation from the DefaultReader/GenericReader path hardened in this PR, so it doesn't inherit these checks (unlike Java, where SpecificDatumReader extends the hardened GenericDatumReader). Hardening it means sharing the limit logic, threading element min-bytes into ReadArray/ReadMap, clamping EnsureSize preallocation, and adding tests on the specific-reader path — a distinct change with its own regression risk on a widely-used path. Filed as AVRO-4306 (https://issues.apache.org/jira/browse/AVRO-4306) to do it properly with dedicated tests rather than bolt it onto this PR late in review.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct — PreresolvingDatumReader (the GenericDatumReader/SpecificDatumReader path) is not hardened by this PR. Tracked as a dedicated follow-up: AVRO-4306 ("Harden PreresolvingDatumReader collection allocation"). This PR intentionally scopes the DefaultReader/GenericReader path; AVRO-4306 will apply the same MinBytesPerElement/EnsureCollectionAvailable + bounded prealloc/grow to the preresolving path and add tests over GenericDatumReader/SpecificDatumReader.