Skip to content

Commit

Permalink
Add tests that cover recursion with self re-entrance. (#1459)
Browse files Browse the repository at this point in the history
### What

Add tests that cover recursion with self re-entrance.

This is kind of an esoteric/edge case scenario, so I'm covering it in
the 'hostile' test suite combined with exercising the deep call stacks.

### Why

Improving test coverage.

### Known limitations

N/A
  • Loading branch information
dmkozh authored Sep 13, 2024
1 parent 75b7821 commit f9a5197
Show file tree
Hide file tree
Showing 11 changed files with 27,265 additions and 9,362 deletions.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

111 changes: 96 additions & 15 deletions soroban-env-host/src/test/hostile_opt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ fn run_deep_host_stack_test(
contract_call_depth: u32,
serialization_depth: u32,
deserialization_depth: u32,
use_self_reentrance: bool,
function_name: &'static str,
) -> Result<Val, HostError> {
let host = ObservedHost::new(function_name, Host::test_host_with_recording_footprint());
Expand All @@ -45,27 +46,50 @@ fn run_deep_host_stack_test(
// cost optimizations and contract implementation optimizations.
host.as_budget().reset_unlimited().unwrap();

// Contract instance setup:
// Contract `0` doesn't perform auth and is only used for the
// root `call` function.
// Contracts `1..CONTRACT_CALL_DEPTH - 2` recursively call
// the next contract in the sequence.
// Contract `CONTRACT_CALL_DEPTH - 1` serializes nested XDR value
// of depth `SERIALIZATION_DEPTH`.
for _ in 0..contract_call_depth {
if use_self_reentrance {
// With self-reentrance we can use a single contract instance that will
// get `call` function to be called first, then a number of nested
// `__check_auth` reentrant recursive calls.
contracts.push(
Address::try_from_val(
&*host,
&host.register_test_contract_wasm(RECURSIVE_ACCOUNT_CONTRACT),
)
.unwrap(),
);
} else {
// Contract instance setup:
// Contract `0` doesn't perform auth and is only used for the
// root `call` function.
// Contracts `1..CONTRACT_CALL_DEPTH - 2` recursively call
// the next contract in the sequence.
// Contract `CONTRACT_CALL_DEPTH - 1` serializes nested XDR value
// of depth `SERIALIZATION_DEPTH`.
for _ in 0..contract_call_depth {
contracts.push(
Address::try_from_val(
&*host,
&host.register_test_contract_wasm(RECURSIVE_ACCOUNT_CONTRACT),
)
.unwrap(),
);
}
}

let mut auth_entries = vec![];
for i in 1..contract_call_depth as usize {
let signature = if i < (contract_call_depth - 1) as usize {
RecursiveAccountSignature::RedirectAddress(contracts[i + 1].clone())
// Even with self-reentrance we still need to provide a separate
// auth tree (just single node here) per `require_auth`. The reason
// is that an auth tree can not be used while being validated, i.e.
// self-reentrant `require_auth` can't use the auth entry that's
// currently being validated by `__check_auth`.
let redirect_contract = if use_self_reentrance {
&contracts[0]
} else {
&contracts[i + 1]
};
RecursiveAccountSignature::RedirectAddress(redirect_contract.clone())
} else {
if deserialization_depth > 0 {
assert_eq!(serialization_depth, 0);
Expand All @@ -80,16 +104,26 @@ fn run_deep_host_stack_test(
}
};
let signature_val: Val = signature.try_into_val(&*host).unwrap();
let authorizer_contract = if use_self_reentrance {
&contracts[0]
} else {
&contracts[i]
};
let credentials = SorobanAddressCredentials {
address: contracts[i].to_sc_address().unwrap(),
nonce: 0,
address: authorizer_contract.to_sc_address().unwrap(),
nonce: i as i64,
signature_expiration_ledger: 1000,
signature: signature_val.try_into_val(&*host).unwrap(),
};
let function_name = if i == 1 { "call" } else { "__check_auth" };
let authorized_contract = if use_self_reentrance {
&contracts[0]
} else {
&contracts[i - 1]
};
let root_invocation = SorobanAuthorizedInvocation {
function: SorobanAuthorizedFunction::ContractFn(InvokeContractArgs {
contract_address: contracts[i - 1].to_sc_address().unwrap(),
contract_address: authorized_contract.to_sc_address().unwrap(),
function_name: function_name.try_into().unwrap(),
args: Default::default(),
}),
Expand All @@ -110,12 +144,17 @@ fn run_deep_host_stack_test(
}

host.set_authorization_entries(auth_entries).unwrap();
let first_redirect_contract = if use_self_reentrance {
&contracts[0]
} else {
&contracts[1]
};
// Note, that we only are interested in the end result of the test;
// the setup shouldn't fail and doesn't need to be verified.
host.call(
contracts[0].as_object(),
Symbol::try_from_small_str("call").unwrap(),
test_vec![&*host, contracts[1]].into(),
test_vec![&*host, first_redirect_contract].into(),
)
}

Expand All @@ -128,14 +167,40 @@ fn test_deep_stack_call_succeeds_near_limit() {
DEFAULT_HOST_DEPTH_LIMIT,
DEFAULT_HOST_DEPTH_LIMIT - 1,
0,
false,
function_name!(),
);
assert!(res.is_ok());
}

#[test]
fn test_deep_stack_with_self_reentry_call_succeeds_near_limit() {
// The serialized object has depth of `serialization_depth + 1`,
// thus the maximum serializable value needs
// `serialization_depth == DEFAULT_HOST_DEPTH_LIMIT - 1`.
let res = run_deep_host_stack_test(
DEFAULT_HOST_DEPTH_LIMIT,
DEFAULT_HOST_DEPTH_LIMIT - 1,
0,
true,
function_name!(),
);
assert!(res.is_ok());
}

#[test]
fn test_deep_stack_call_fails_when_contract_call_depth_exceeded() {
let res = run_deep_host_stack_test(DEFAULT_HOST_DEPTH_LIMIT + 1, 0, 0, function_name!());
let res = run_deep_host_stack_test(DEFAULT_HOST_DEPTH_LIMIT + 1, 0, 0, false, function_name!());
assert!(res.is_err());
let err = res.err().unwrap().error;
// We shouldn't run out of budget here, so the error would just
// be decorated as auth error.
assert!(err.is_type(ScErrorType::Auth));
}

#[test]
fn test_deep_stack_call_with_self_reentry_fails_when_contract_call_depth_exceeded() {
let res = run_deep_host_stack_test(DEFAULT_HOST_DEPTH_LIMIT + 1, 0, 0, false, function_name!());
assert!(res.is_err());
let err = res.err().unwrap().error;
// We shouldn't run out of budget here, so the error would just
Expand All @@ -145,7 +210,7 @@ fn test_deep_stack_call_fails_when_contract_call_depth_exceeded() {

#[test]
fn test_deep_stack_call_fails_when_serialization_depth_exceeded() {
let res = run_deep_host_stack_test(2, DEFAULT_HOST_DEPTH_LIMIT, 0, function_name!());
let res = run_deep_host_stack_test(2, DEFAULT_HOST_DEPTH_LIMIT, 0, false, function_name!());
assert!(res.is_err());
let err = res.err().unwrap().error;
// We shouldn't run out of budget here, so the error would just
Expand All @@ -159,6 +224,19 @@ fn test_deep_stack_call_succeeds_near_limit_with_xdr_deserialization() {
DEFAULT_HOST_DEPTH_LIMIT,
0,
DEFAULT_HOST_DEPTH_LIMIT - 2,
false,
function_name!(),
);
assert!(res.is_ok());
}

#[test]
fn test_deep_stack_call_with_self_reentry_succeeds_near_limit_with_xdr_deserialization() {
let res = run_deep_host_stack_test(
DEFAULT_HOST_DEPTH_LIMIT,
0,
DEFAULT_HOST_DEPTH_LIMIT - 2,
true,
function_name!(),
);
assert!(res.is_ok());
Expand All @@ -170,6 +248,7 @@ fn test_deep_stack_call_fails_xdr_deserialization_exceeding_host_object_depth()
DEFAULT_HOST_DEPTH_LIMIT,
0,
DEFAULT_HOST_DEPTH_LIMIT - 1,
false,
function_name!(),
);
assert!(res.is_err());
Expand All @@ -185,6 +264,7 @@ fn test_deep_stack_call_fails_with_deep_xdr_deserialization() {
DEFAULT_HOST_DEPTH_LIMIT,
0,
DEFAULT_XDR_RW_LIMITS.depth,
false,
function_name!(),
);
assert!(res.is_err());
Expand All @@ -200,6 +280,7 @@ fn test_deep_stack_call_fails_with_too_deep_xdr_deserialization() {
DEFAULT_HOST_DEPTH_LIMIT,
0,
DEFAULT_XDR_RW_LIMITS.depth * 2,
false,
function_name!(),
);
assert!(res.is_err());
Expand Down

0 comments on commit f9a5197

Please sign in to comment.