Skip to content
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

fix[codegen]: fix _abi_decode buffer overflow #3925

Merged
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
2ffb67b
fix abi head validation
cyberthirst Apr 9, 2024
7bc3878
add test for abi decode overflow
cyberthirst Apr 9, 2024
7dfe669
add clamping of abi member type
cyberthirst Apr 12, 2024
50f9153
add abi decode test with incorrect member type size
cyberthirst Apr 12, 2024
c8ccd45
add cacheing of abi_ofst in getelemptr_abi
cyberthirst Apr 12, 2024
9274f29
lint and update comment
cyberthirst Apr 12, 2024
a7bb73a
refactor creation of bytes payload in tests
cyberthirst Apr 15, 2024
ab874ac
lint
cyberthirst Apr 15, 2024
867dd03
refactor bounds check to be generalized
cyberthirst Apr 15, 2024
8983f9e
refactor tests and improve naming
cyberthirst Apr 18, 2024
2095d29
add abi decode test
cyberthirst Apr 18, 2024
807c665
refactor decode test to use smallest possible buffer overflow
cyberthirst Apr 20, 2024
933baf3
fix variable reference
charles-cooper Apr 22, 2024
da0a59a
fix offset clamp
cyberthirst Apr 23, 2024
333c7bf
add comment for decode clamp
cyberthirst Apr 23, 2024
14f93e2
add abi decode test and lint
cyberthirst Apr 23, 2024
28d26b2
refactor outer tuple
charles-cooper Apr 23, 2024
bad1a97
small refactor
charles-cooper Apr 23, 2024
09e15e7
rename some variables, add notes
charles-cooper Apr 23, 2024
e0dbb39
wip: add runtime sz clamps to abi decode
cyberthirst Apr 25, 2024
37c4515
add abi bounds calculation
cyberthirst Apr 25, 2024
bb4939a
update decode tests to reflect runtime sz checks
cyberthirst Apr 25, 2024
5191860
lint
cyberthirst Apr 25, 2024
7adf734
Merge remote-tracking branch 'origin/master' into fix/builtins-abi-de…
cyberthirst Apr 25, 2024
435efb6
wip: move abi decode oob clamps to clamper funs
cyberthirst Apr 26, 2024
09918fd
relax static upper bound
cyberthirst Apr 29, 2024
f1c6783
Merge remote-tracking branch 'origin/master' into fix/builtins-abi-de…
cyberthirst Apr 29, 2024
889223a
Merge branch 'master' into fix/builtins-abi-decode-overflow
charles-cooper Apr 29, 2024
a5ba635
Merge branch 'master' into fix/builtins-abi-decode-overflow
charles-cooper May 10, 2024
87ca3e7
Lint and fix todo's
DanielSchiavini May 10, 2024
5851411
Merge issues
DanielSchiavini May 10, 2024
d9462a6
Refactor
DanielSchiavini May 10, 2024
39e0cc9
Merge pull request #1 from DanielSchiavini/fix/builtins-abi-decode-ov…
cyberthirst May 14, 2024
02c11ca
check arithmetic overflow within array clampers
cyberthirst May 14, 2024
49dc800
remove `lo` param, use safe_add instead
charles-cooper May 14, 2024
6f47c7f
thread hi from external call
charles-cooper May 14, 2024
79eca11
refactoring
charles-cooper May 14, 2024
5a8cc51
fix overflow check
charles-cooper May 14, 2024
e3d2458
Merge branch 'master' into fix/builtins-abi-decode-overflow
charles-cooper May 14, 2024
9592708
update decode tests to reflect the decoding refactor
cyberthirst May 15, 2024
67f1c1b
add extcall oob test for abi decode
cyberthirst May 15, 2024
fd8f0b1
Merge branch 'master' into fix/builtins-abi-decode-overflow
charles-cooper May 15, 2024
067bfae
fix some type errors
charles-cooper May 15, 2024
c872f2a
lint
charles-cooper May 15, 2024
a96c256
fix some bugs
charles-cooper May 15, 2024
7e3d6ae
add more evil abi decode tests
cyberthirst May 15, 2024
b7743b2
fix some logic
charles-cooper May 15, 2024
3d679fb
remove dead kwarg
charles-cooper May 15, 2024
cec1589
check arithmetic overflow
charles-cooper May 15, 2024
19ed864
refactor a check
charles-cooper May 15, 2024
dda0cb0
make a check stricter
charles-cooper May 15, 2024
06d76a7
slight cleanup
charles-cooper May 15, 2024
157c10e
remove unneeded arithmetic overflow check
charles-cooper May 15, 2024
b8ef8b8
bit of cleanup
charles-cooper May 15, 2024
32bda27
refactor a test
charles-cooper May 15, 2024
ce2412b
small changes
charles-cooper May 15, 2024
cb2b31d
cache returndatasize
charles-cooper May 15, 2024
2916d09
add even more evil abi decode tests
cyberthirst May 15, 2024
5bea8a2
lint
charles-cooper May 15, 2024
9f0a648
Merge branch 'master' into fix/builtins-abi-decode-overflow
charles-cooper May 15, 2024
6d75a0c
add even more evil abi decode tests
cyberthirst May 16, 2024
aa82e15
refactor payload construction
cyberthirst May 17, 2024
532bdb8
remove asserts for payload refactor equality
cyberthirst May 17, 2024
d5e370d
lint
cyberthirst May 17, 2024
70809e5
rename utility
cyberthirst May 17, 2024
8ee79ab
Merge branch 'master' into fix/builtins-abi-decode-overflow
cyberthirst May 17, 2024
2ac31d4
add test for escaping child payload to another section of parent buffer
cyberthirst May 17, 2024
389be3a
add more tests for decoding from returndata
cyberthirst May 19, 2024
890e20f
add test for zero head for dynarray
cyberthirst May 20, 2024
538cdaa
add test mergeing head and length fields
cyberthirst May 20, 2024
9ea386d
Merge branch 'master' into fix/builtins-abi-decode-overflow
charles-cooper May 25, 2024
0483f48
Merge branch 'master' into fix/builtins-abi-decode-overflow
charles-cooper May 27, 2024
e7d2351
Merge branch 'master' into fix/builtins-abi-decode-overflow
charles-cooper May 28, 2024
898a91d
Merge branch 'master' into fix/builtins-abi-decode-overflow
charles-cooper May 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions tests/functional/builtins/codegen/test_abi_decode.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from eth.codecs import abi

from vyper.exceptions import ArgumentException, StackTooDeep, StructureException
Fixed Show fixed Hide fixed
from vyper.utils import method_id

TEST_ADDR = "0x" + b"".join(chr(i).encode("utf-8") for i in range(20)).hex()

Expand Down Expand Up @@ -467,3 +468,77 @@
@pytest.mark.parametrize("bad_code,exception", FAIL_LIST)
def test_abi_decode_length_mismatch(get_contract, assert_compile_failed, bad_code, exception):
assert_compile_failed(lambda: get_contract(bad_code), exception)


def test_abi_decode_arithmetic_overflow(w3, tx_failed, get_contract):
# test based on GHSA-9p8r-4xp4-gw5w:
# https://github.com/vyperlang/vyper/security/advisories/GHSA-9p8r-4xp4-gw5w#advisory-comment-91841
# note: doesn't even reach the assert but reverts internally on the clamp in getelemptr
code = """
@external
def f(x: Bytes[32 * 3]):
a: Bytes[32] = b"foo"
y: Bytes[32 * 3] = x

decoded_y1: Bytes[32] = _abi_decode(y, Bytes[32])
a = b"bar"
decoded_y2: Bytes[32] = _abi_decode(y, Bytes[32])

assert decoded_y1 != decoded_y2
"""
c = get_contract(code)
data = method_id("f(bytes)")
data += (0x20).to_bytes(32, "big") # tuple head
data += (0x60).to_bytes(32, "big") # parent array length
# parent payload - this word will be considered as the head of the abi-encoded inner array
# and it will be added to base ptr leading to an arithmetic overflow
data += (2**256 - 0x60).to_bytes(32, "big")
cyberthirst marked this conversation as resolved.
Show resolved Hide resolved
with tx_failed():
w3.eth.send_transaction({"to": c.address, "data": data})



def test_abi_decode_oob_due_to_invalid_head(w3, tx_failed, get_contract):
code = """
@external
def f(x: Bytes[32 * 5]):
y: Bytes[32 * 5] = x
a: Bytes[32] = b"a"
Fixed Show fixed Hide fixed
decoded_y1: DynArray[uint256, 3] = _abi_decode(y, DynArray[uint256, 3])
a = b"aaaa"
decoded_y1 = _abi_decode(y, DynArray[uint256, 3])
"""
c = get_contract(code)
data = method_id("f(bytes)")
data += (0x20).to_bytes(32, "big") # tuple head
data += (0xA0).to_bytes(32, "big") # parent array length
# head should be 20 and thus the decoding func would decode 1 byte
# over the end of the input data
# _getelemptr_abi_helper will revert due to clamping
data += (0x21).to_bytes(32, "big") # invalid inner array head (1 byte over)
# we don't want to revert on invalid length, so set this to 0
# the first byte of payload will be considered as the length
data += (0x00).to_bytes(32, "big")
data += (0x01).to_bytes(1, "big") # will be considered as the length=1
data += (0x00).to_bytes(31, "big")
data += (0x03).to_bytes(32, "big") * 2
with tx_failed():
w3.eth.send_transaction({"to": c.address, "data": data})


def test_abi_decode_oob_due_to_invalid_head(tx_failed, get_contract):
code = """
@external
def bar() -> (uint256, uint256, uint256):
return (480, 0, 0)

interface A:
def bar() -> String[32]: nonpayable

@external
def foo():
x:String[32] = extcall A(self).bar()
"""
c = get_contract(code)
with tx_failed():
c.foo()
21 changes: 5 additions & 16 deletions vyper/builtins/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
add_ofst,
bytes_data_ptr,
calculate_type_for_external_return,
check_buffer_overflow_ir,
check_external_call,
clamp,
clamp2,
Expand Down Expand Up @@ -230,18 +231,6 @@ def build_IR(self, expr, context):
ADHOC_SLICE_NODE_MACROS = ["~calldata", "~selfcode", "~extcode"]


# make sure we don't overrun the source buffer, checking for overflow:
# valid inputs satisfy:
# `assert !(start+length > src_len || start+length < start`
def _make_slice_bounds_check(start, length, src_len):
with start.cache_when_complex("start") as (b1, start):
with add_ofst(start, length).cache_when_complex("end") as (b2, end):
arithmetic_overflow = ["lt", end, start]
buffer_oob = ["gt", end, src_len]
ok = ["iszero", ["or", arithmetic_overflow, buffer_oob]]
return b1.resolve(b2.resolve(["assert", ok]))


def _build_adhoc_slice_node(sub: IRnode, start: IRnode, length: IRnode, context: Context) -> IRnode:
assert length.is_literal, "typechecker failed"
assert isinstance(length.value, int) # mypy hint
Expand All @@ -254,7 +243,7 @@ def _build_adhoc_slice_node(sub: IRnode, start: IRnode, length: IRnode, context:
if sub.value == "~calldata":
node = [
"seq",
_make_slice_bounds_check(start, length, "calldatasize"),
check_buffer_overflow_ir(start, length, "calldatasize"),
["mstore", np, length],
["calldatacopy", np + 32, start, length],
np,
Expand All @@ -264,7 +253,7 @@ def _build_adhoc_slice_node(sub: IRnode, start: IRnode, length: IRnode, context:
elif sub.value == "~selfcode":
node = [
"seq",
_make_slice_bounds_check(start, length, "codesize"),
check_buffer_overflow_ir(start, length, "codesize"),
["mstore", np, length],
["codecopy", np + 32, start, length],
np,
Expand All @@ -279,7 +268,7 @@ def _build_adhoc_slice_node(sub: IRnode, start: IRnode, length: IRnode, context:
sub.args[0],
[
"seq",
_make_slice_bounds_check(start, length, ["extcodesize", "_extcode_address"]),
check_buffer_overflow_ir(start, length, ["extcodesize", "_extcode_address"]),
["mstore", np, length],
["extcodecopy", "_extcode_address", np + 32, start, length],
np,
Expand Down Expand Up @@ -452,7 +441,7 @@ def build_IR(self, expr, args, kwargs, context):

ret = [
"seq",
_make_slice_bounds_check(start, length, src_len),
check_buffer_overflow_ir(start, length, src_len),
do_copy,
["mstore", dst, length], # set length
dst, # return pointer to dst
Expand Down
28 changes: 26 additions & 2 deletions vyper/codegen/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -445,8 +445,9 @@ def _mul(x, y):


# Resolve pointer locations for ABI-encoded data
def _getelemptr_abi_helper(parent, member_t, ofst, clamp=True):
def _getelemptr_abi_helper(parent, member_t, ofst, clamp_=True):
cyberthirst marked this conversation as resolved.
Show resolved Hide resolved
member_abi_t = member_t.abi_type
parent_abi_t = parent.typ.abi_type

# ABI encoding has length word and then pretends length is not there
# e.g. [[1,2]] is encoded as 0x01 <len> 0x20 <inner array ofst> <encode(inner array)>
Expand All @@ -457,10 +458,21 @@ def _getelemptr_abi_helper(parent, member_t, ofst, clamp=True):
ofst_ir = add_ofst(parent, ofst)

if member_abi_t.is_dynamic():
abi_ofst = unwrap_location(ofst_ir)
# double dereference, according to ABI spec
# TODO optimize special case: first dynamic item
# offset is statically known.
ofst_ir = add_ofst(parent, unwrap_location(ofst_ir))
ofst_ir = add_ofst(parent, abi_ofst)

if parent.location == MEMORY: # TODO: replace with utility function
with abi_ofst.cache_when_complex("abi_ofst") as (b1, abi_ofst):
bound = parent_abi_t.size_bound()
ofst_ir = [
"seq",
check_buffer_overflow_ir(abi_ofst, member_abi_t.size_bound(), bound),
add_ofst(parent, abi_ofst),
]
ofst_ir = b1.resolve(ofst_ir)

return IRnode.from_list(
ofst_ir,
Expand Down Expand Up @@ -1230,3 +1242,15 @@ def clamp2(lo, arg, hi, signed):
LE = "sle" if signed else "le"
ret = ["seq", ["assert", ["and", [GE, arg, lo], [LE, arg, hi]]], arg]
return IRnode.from_list(b1.resolve(ret), typ=arg.typ)


# make sure we don't overrun the source buffer, checking for overflow:
# valid inputs satisfy:
# `assert !(start+length > src_len || start+length < start)`
def check_buffer_overflow_ir(start, length, src_len):
with start.cache_when_complex("start") as (b1, start):
with add_ofst(start, length).cache_when_complex("end") as (b2, end):
arithmetic_overflow = ["lt", end, start]
buffer_oob = ["gt", end, src_len]
ok = ["iszero", ["or", arithmetic_overflow, buffer_oob]]
return b1.resolve(b2.resolve(["assert", ok]))
Loading