Attackathon _ Fuel Network 32872 - [Smart Contract - High] Incorrect load_store_to_memcopy optimizat
Submitted on Fri Jul 05 2024 18:27:38 GMT-0400 (Atlantic Standard Time) by @anatomist for Attackathon | Fuel Network
Report ID: #32872
Report type: Smart Contract
Report severity: High
Target: https://github.com/FuelLabs/sway/tree/7b56ec734d4a4fda550313d448f7f20dba818b59
Impacts:
Direct theft of any user funds, whether at-rest or in-motion, other than unclaimed yield
Incorrect sway optimization leading to incorrect bytecode
Description
Brief/Intro
load_store_to_memcopy
does not consider escaped symbols, and can result in incorrect optimization results.
Vulnerability Details
The load_store_to_memcopy
optimization transforms load_val = Load(src_val_ptr)
and Store(dst_val_ptr, load_val)
ir pairs into a single MemCopyVal(dst_val_ptr, src_val_ptr)
ir. To do this correctly, load_store_to_memcopy
must check if data pointed to by src_val_ptr
has been modified between the Load
and Store
. If it is modified, then replacing Store
between pointers will be different from loading before src_val_ptr
content is modified and storing the original value into dst_val_ptr
, and the code should not be optimized. The check is done by is_clobbered
, which looks for Store
to any gep_referred_symbols
from the src_ptr
.
However, the check is only done to the function itself, and can not catch writes to src_ptr
by other functions. And because load_store_to_memcopy
is not skipped for escaped_symbols
, it might incorrectly transform ir.
This is not the end, the bug is even more complicated because the compiler backend can not load non copy types from pointers, so even if we want to skip load_store_to_memcopy
for escaped_symbols
, we can't because it will cause compilation to fail, this will make it difficult to patch the bug.
Impact Details
Storing incorrect values to local variable will cause incorrect execution results. The exact impact is hard to estimate because it depends on how the affected contract is written, but loss of funds or bricking of contracts are both possible.
References
https://github.com/FuelLabs/sway/blob/e1b1c2bee73e0ba825e07736cefa6c0abd079595/sway-ir/src/optimize/memcpyopt.rs#L796
https://github.com/FuelLabs/sway/blob/e1b1c2bee73e0ba825e07736cefa6c0abd079595/sway-ir/src/optimize/memcpyopt.rs#L739
Proof of concept
Proof of Concept
Tests are run on sway commit acded67b3ec77ce1753356ad46f7ae17290f2ee0
.
The test compiles to the initial ir then goes through several optimizations. We also show the ir related to let b = a[1].get(side_effect(a, &mut idx));
. The code is expected to return 4, but returns 6 instead because Store
is replaced by a MemCopyVal
incorrectly.
The initial ir loads a[1]
to v38
, then calls side_effect
with a
to get the second index, and finally uses it to get b
. Because a[1]
is loaded before side_effect
is called, it should not be affected by it, and the get
will fetch value from S{3, 4}
.
initial ir
Right before memcpyopt
, the ir is still the same as initial ir except a local variable __tmp_arg4
is used to store the loaded a[1]
, and get
takes a pointer to __tmp_arg4
instead of the value. The code should still fetch value from S{3, 4}
.
ir before memcpyopt
After memcpyopt
, the store
to __tmp_arg4
is replaced with mem_copy_val
from &a[1]
, and the loaded a[1]
is discarded. a[1]
is fetched after side_effect
is called, and its value will be affected. The get
will fetch value from S{5, 6}
created by side_effect
ir after memcpyopt
If we remove the #[inline(never)]
attribute from side_effect
, the code will fail to compile because the changes to a
is inlined between the Load
and Store
, is_clobbered
sees those changes and gives up replacing the Store
. The backend then sees a Load
trying to load a struct, which is not a copy type, and returns an error.
We omit writing a dapp to show loss of funds caused by this bug, because the fuel team said we only need to show the incorrect compilation with our PoC in the changelog walkthrough earlier.
Last updated