-
Notifications
You must be signed in to change notification settings - Fork 269
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
Disallow invalid pointers in arrays and tuples #226
Changes from all commits
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 |
---|---|---|
|
@@ -19,6 +19,7 @@ | |
) | ||
from eth_abi.exceptions import ( | ||
InsufficientDataBytes, | ||
InvalidPointer, | ||
NonEmptyPaddingBytes, | ||
) | ||
from eth_abi.utils.numeric import ( | ||
|
@@ -78,13 +79,13 @@ def __init__(self, *args, **kwargs): | |
self._frames = [] | ||
self._total_offset = 0 | ||
|
||
def seek_in_frame(self, pos, *args, **kwargs): | ||
def seek_in_frame(self, pos: int, *args: Any, **kwargs: Any) -> None: | ||
""" | ||
Seeks relative to the total offset of the current contextual frames. | ||
""" | ||
self.seek(self._total_offset + pos, *args, **kwargs) | ||
|
||
def push_frame(self, offset): | ||
def push_frame(self, offset: int) -> None: | ||
""" | ||
Pushes a new contextual frame onto the stack with the given offset and a | ||
return position at the current cursor position then seeks to the new | ||
|
@@ -131,6 +132,13 @@ def __call__(self, stream: ContextFramesBytesIO) -> Any: | |
|
||
|
||
class HeadTailDecoder(BaseDecoder): | ||
""" | ||
Decoder for a dynamic element of a dynamic container (a dynamic array, or a sized | ||
array or tuple that contains dynamic elements). A dynamic element consists of a | ||
pointer, aka offset, which is located in the head section of the encoded container, | ||
and the actual value, which is located in the tail section of the encoding. | ||
""" | ||
|
||
is_dynamic = True | ||
|
||
tail_decoder = None | ||
|
@@ -141,13 +149,18 @@ def validate(self): | |
if self.tail_decoder is None: | ||
raise ValueError("No `tail_decoder` set") | ||
|
||
def decode(self, stream): | ||
def decode(self, stream: ContextFramesBytesIO) -> Any: | ||
# Decode the offset and move the stream cursor forward 32 bytes | ||
start_pos = decode_uint_256(stream) | ||
|
||
# Jump ahead to the start of the value | ||
stream.push_frame(start_pos) | ||
|
||
# assertion check for mypy | ||
if self.tail_decoder is None: | ||
raise AssertionError("`tail_decoder` is None") | ||
# Decode the value | ||
value = self.tail_decoder(stream) | ||
# Return the cursor | ||
stream.pop_frame() | ||
|
||
return value | ||
|
@@ -172,8 +185,43 @@ def validate(self): | |
if self.decoders is None: | ||
raise ValueError("No `decoders` set") | ||
|
||
def validate_pointers(self, stream: ContextFramesBytesIO) -> None: | ||
""" | ||
Verify that all pointers point to a valid location in the stream. | ||
""" | ||
current_location = stream.tell() | ||
len_of_head = sum( | ||
decoder.array_size if hasattr(decoder, "array_size") else 1 | ||
for decoder in self.decoders | ||
) | ||
end_of_offsets = current_location + 32 * len_of_head | ||
total_stream_length = len(stream.getbuffer()) | ||
for decoder in self.decoders: | ||
if isinstance(decoder, HeadTailDecoder): | ||
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. Nit: It would be nice to share this logic across decoders, maybe this could become a utility function that could take the 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. Nit heard and politely declined. There is enough required difference in how tuples and arrays are checked that any logic extraction have a lot of |
||
# the next 32 bytes are a pointer | ||
offset = decode_uint_256(stream) | ||
indicated_idx = current_location + offset | ||
if ( | ||
indicated_idx < end_of_offsets | ||
or indicated_idx >= total_stream_length | ||
): | ||
# the pointer is indicating its data is located either within the | ||
# offsets section of the stream or beyond the end of the stream, | ||
# both of which are invalid | ||
raise InvalidPointer( | ||
"Invalid pointer in tuple at location " | ||
f"{stream.tell() - 32} in payload" | ||
) | ||
else: | ||
# the next 32 bytes are not a pointer, so progress the stream per | ||
# the decoder | ||
decoder(stream) | ||
# return the stream to its original location for actual decoding | ||
stream.seek(current_location) | ||
|
||
@to_tuple # type: ignore[misc] # untyped decorator | ||
def decode(self, stream: ContextFramesBytesIO) -> Generator[Any, None, None]: | ||
self.validate_pointers(stream) | ||
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. I could use more context here. Could this be called in the loop below and maybe allow removal of the inner decoder loops inside 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. There is no way to know how long the head section of a dynamic tuple will be until you have stepped through each decoder - if the decoder is for a dynamic type, it will be 32 bytes every time (because it's a pointer), but if it's for a non-dynamic array, there will be a single decoder for multiple chunks of 32 bytes. I think it would be possible to take the logic from The validation needs to be in the tuple and array decoders, because only they have the context for how long they are. A 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. I see now what the difference means, assuming there may never be more than a few decoders at a time I don't have any concerns. |
||
for decoder in self.decoders: | ||
yield decoder(stream) | ||
|
||
|
@@ -248,6 +296,30 @@ def from_type_str(cls, abi_type, registry): | |
# If array dimension is dynamic | ||
return DynamicArrayDecoder(item_decoder=item_decoder) | ||
|
||
def validate_pointers(self, stream: ContextFramesBytesIO, array_size: int) -> None: | ||
""" | ||
Verify that all pointers point to a valid location in the stream. | ||
""" | ||
if isinstance(self.item_decoder, HeadTailDecoder): | ||
current_location = stream.tell() | ||
end_of_offsets = current_location + 32 * array_size | ||
total_stream_length = len(stream.getbuffer()) | ||
for _ in range(array_size): | ||
offset = decode_uint_256(stream) | ||
indicated_idx = current_location + offset | ||
if ( | ||
indicated_idx < end_of_offsets | ||
or indicated_idx >= total_stream_length | ||
): | ||
# the pointer is indicating its data is located either within the | ||
# offsets section of the stream or beyond the end of the stream, | ||
# both of which are invalid | ||
raise InvalidPointer( | ||
"Invalid pointer in array at location " | ||
f"{stream.tell() - 32} in payload" | ||
) | ||
stream.seek(current_location) | ||
|
||
|
||
class SizedArrayDecoder(BaseArrayDecoder): | ||
array_size = None | ||
|
@@ -261,6 +333,8 @@ def __init__(self, **kwargs): | |
def decode(self, stream): | ||
if self.item_decoder is None: | ||
raise AssertionError("`item_decoder` is None") | ||
|
||
self.validate_pointers(stream, self.array_size) | ||
for _ in range(self.array_size): | ||
yield self.item_decoder(stream) | ||
|
||
|
@@ -275,6 +349,8 @@ def decode(self, stream): | |
stream.push_frame(32) | ||
if self.item_decoder is None: | ||
raise AssertionError("`item_decoder` is None") | ||
|
||
self.validate_pointers(stream, array_size) | ||
for _ in range(array_size): | ||
yield self.item_decoder(stream) | ||
stream.pop_frame() | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
During decoding, verify all pointers in arrays and tuples point to a valid location in the payload |
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.
💯