Skip to content

Commit 97dd394

Browse files
authored
feat: add option to filter cycles transfer (#89)
* feat: add option to filter cycles transfer * fix name * cleanup * cleanup * fix * format * spelling * add a forbidden function * comment * comment
1 parent f2011bb commit 97dd394

File tree

2 files changed

+219
-2
lines changed

2 files changed

+219
-2
lines changed

src/bin/main.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,12 @@ enum SubCommand {
4242
},
4343
/// Limit resource usage
4444
Resource {
45-
/// Remove cycles_add system API call
45+
/// Remove `ic0.call_cycles_add[128]` system API calls
4646
#[clap(short, long)]
4747
remove_cycles_transfer: bool,
48+
/// Filter `ic0.call_cycles_add[128]` system API calls
49+
#[clap(short, long, conflicts_with_all = &["remove_cycles_transfer", "playground_backend_redirect"])]
50+
filter_cycles_transfer: bool,
4851
/// Allocate at most specified amount of memory pages for Wasm heap memory
4952
#[clap(short('m'), long)]
5053
limit_heap_memory_page: Option<u32>,
@@ -159,13 +162,15 @@ fn main() -> anyhow::Result<()> {
159162
}
160163
SubCommand::Resource {
161164
remove_cycles_transfer,
165+
filter_cycles_transfer,
162166
limit_heap_memory_page,
163167
limit_stable_memory_page,
164168
playground_backend_redirect,
165169
} => {
166170
use ic_wasm::limit_resource::{limit_resource, Config};
167171
let config = Config {
168172
remove_cycles_add: *remove_cycles_transfer,
173+
filter_cycles_add: *filter_cycles_transfer,
169174
limit_heap_memory_page: *limit_heap_memory_page,
170175
limit_stable_memory_page: *limit_stable_memory_page,
171176
playground_canister_id: *playground_backend_redirect,

src/limit_resource.rs

Lines changed: 213 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use walrus::*;
55

66
pub struct Config {
77
pub remove_cycles_add: bool,
8+
pub filter_cycles_add: bool,
89
pub limit_stable_memory_page: Option<u32>,
910
pub limit_heap_memory_page: Option<u32>,
1011
pub playground_canister_id: Option<candid::Principal>,
@@ -48,6 +49,25 @@ pub fn limit_resource(m: &mut Module, config: &Config) {
4849
make_cycles_burn128(m, &mut replacer, wasm64);
4950
}
5051

52+
if config.filter_cycles_add {
53+
// Create a private global set in every invokation of `ic0.call_new` to
54+
// - 0 if subsequent calls to `ic0.call_cycles_add[128]` are allowed;
55+
// - 1 if subsequent calls to `ic0.call_cycles_add[128]` should be filtered (i.e., turned into no-op).
56+
let global_id = m.globals.add_local(
57+
ValType::I32,
58+
true, // mutable
59+
false, // shared (not supported yet)
60+
ConstExpr::Value(Value::I32(0)),
61+
);
62+
// Instrument `ic0.call_cycles_add[128]` to respect the value of the private global.
63+
make_filter_cycles_add(m, &mut replacer, wasm64, global_id);
64+
make_filter_cycles_add128(m, &mut replacer, global_id);
65+
// Calls to `ic0.cycles_burn128` are always filtered (i.e., turned into no-op).
66+
make_cycles_burn128(m, &mut replacer, wasm64);
67+
// Instrument `ic0.call_new` to set the private global.
68+
make_filter_call_new(m, &mut replacer, wasm64, global_id);
69+
}
70+
5171
if let Some(limit) = config.limit_stable_memory_page {
5272
make_stable_grow(m, &mut replacer, wasm64, limit as i32);
5373
make_stable64_grow(m, &mut replacer, limit as i64);
@@ -126,6 +146,38 @@ fn make_cycles_add(m: &mut Module, replacer: &mut Replacer, wasm64: bool) {
126146
}
127147
}
128148

149+
fn make_filter_cycles_add(
150+
m: &mut Module,
151+
replacer: &mut Replacer,
152+
wasm64: bool,
153+
global_id: GlobalId,
154+
) {
155+
if let Some(old_cycles_add) = get_ic_func_id(m, "call_cycles_add") {
156+
if wasm64 {
157+
panic!("Wasm64 module should not call `call_cycles_add`");
158+
}
159+
let mut builder = FunctionBuilder::new(&mut m.types, &[ValType::I64], &[]);
160+
let amount = m.locals.add(ValType::I64);
161+
let mut func = builder.func_body();
162+
// Compare to zero
163+
func.global_get(global_id);
164+
func.i32_const(0);
165+
func.binop(BinaryOp::I32Ne);
166+
// If block
167+
func.if_else(
168+
None,
169+
|then| {
170+
then.local_get(amount).drop(); // no-op
171+
},
172+
|otherwise| {
173+
otherwise.local_get(amount).call(old_cycles_add); // call `ic0.call_cycles_add`
174+
},
175+
);
176+
let new_cycles_add = builder.finish(vec![amount], &mut m.funcs);
177+
replacer.add(old_cycles_add, new_cycles_add);
178+
}
179+
}
180+
129181
fn make_cycles_add128(m: &mut Module, replacer: &mut Replacer) {
130182
if let Some(old_cycles_add128) = get_ic_func_id(m, "call_cycles_add128") {
131183
let mut builder = FunctionBuilder::new(&mut m.types, &[ValType::I64, ValType::I64], &[]);
@@ -142,8 +194,36 @@ fn make_cycles_add128(m: &mut Module, replacer: &mut Replacer) {
142194
}
143195
}
144196

197+
fn make_filter_cycles_add128(m: &mut Module, replacer: &mut Replacer, global_id: GlobalId) {
198+
if let Some(old_cycles_add128) = get_ic_func_id(m, "call_cycles_add128") {
199+
let mut builder = FunctionBuilder::new(&mut m.types, &[ValType::I64, ValType::I64], &[]);
200+
let high = m.locals.add(ValType::I64);
201+
let low = m.locals.add(ValType::I64);
202+
let mut func = builder.func_body();
203+
// Compare to zero
204+
func.global_get(global_id);
205+
func.i32_const(0);
206+
func.binop(BinaryOp::I32Ne);
207+
// If block
208+
func.if_else(
209+
None,
210+
|then| {
211+
then.local_get(high).local_get(low).drop().drop(); // no-op
212+
},
213+
|otherwise| {
214+
otherwise
215+
.local_get(high)
216+
.local_get(low)
217+
.call(old_cycles_add128); // call `ic0.call_cycles_add128`
218+
},
219+
);
220+
let new_cycles_add128 = builder.finish(vec![high, low], &mut m.funcs);
221+
replacer.add(old_cycles_add128, new_cycles_add128);
222+
}
223+
}
224+
145225
fn make_cycles_burn128(m: &mut Module, replacer: &mut Replacer, wasm64: bool) {
146-
if let Some(older_cycles_burn128) = get_ic_func_id(m, "call_cycles_burn128") {
226+
if let Some(older_cycles_burn128) = get_ic_func_id(m, "cycles_burn128") {
147227
let dst_type = match wasm64 {
148228
true => ValType::I64,
149229
false => ValType::I32,
@@ -592,6 +672,138 @@ fn make_redirect_call_new(
592672
}
593673
}
594674

675+
fn make_filter_call_new(
676+
m: &mut Module,
677+
replacer: &mut Replacer,
678+
wasm64: bool,
679+
global_id: GlobalId,
680+
) {
681+
if let Some(old_call_new) = get_ic_func_id(m, "call_new") {
682+
let pointer_type = match wasm64 {
683+
true => ValType::I64,
684+
false => ValType::I32,
685+
};
686+
// Specify the same args as `call_new` so that WASM will correctly check mismatching args
687+
let callee_src = m.locals.add(pointer_type);
688+
let callee_size = m.locals.add(pointer_type);
689+
let name_src = m.locals.add(pointer_type);
690+
let name_size = m.locals.add(pointer_type);
691+
let arg5 = m.locals.add(pointer_type);
692+
let arg6 = m.locals.add(pointer_type);
693+
let arg7 = m.locals.add(pointer_type);
694+
let arg8 = m.locals.add(pointer_type);
695+
696+
let memory = m
697+
.get_memory_id()
698+
.expect("Canister Wasm module should have only one memory");
699+
700+
// Scratch variables
701+
let not_allowed_canister = m.locals.add(ValType::I32);
702+
let allow_cycles = m.locals.add(ValType::I32);
703+
704+
// Cycles transfer is only allowed if
705+
// - the callee is the management canister or `7hfb6-caaaa-aaaar-qadga-cai` (EVM RPC Canister);
706+
// - *and* the method name is *neither* `create_canister` *nor* `deposit_cycles`.
707+
let allowed_canisters = [
708+
Principal::from_slice(&[]),
709+
Principal::from_text("7hfb6-caaaa-aaaar-qadga-cai").unwrap(),
710+
];
711+
let forbidden_function_names = ["create_canister", "deposit_cycles"];
712+
713+
let mut builder = FunctionBuilder::new(
714+
&mut m.types,
715+
&[
716+
pointer_type,
717+
pointer_type,
718+
pointer_type,
719+
pointer_type,
720+
pointer_type,
721+
pointer_type,
722+
pointer_type,
723+
pointer_type,
724+
],
725+
&[],
726+
);
727+
728+
builder
729+
.func_body()
730+
.block(None, |checks| {
731+
let checks_id = checks.id();
732+
// Check if callee is an allowed canister
733+
checks
734+
.block(None, |id_check| {
735+
// no match (i.e., callee not in `allowed_canisters`) => `not_allowed_canister` set to 1
736+
check_list(
737+
memory,
738+
id_check,
739+
not_allowed_canister,
740+
callee_size,
741+
callee_src,
742+
None,
743+
&allowed_canisters
744+
.iter()
745+
.map(|p| p.as_slice())
746+
.collect::<Vec<_>>(),
747+
wasm64,
748+
);
749+
})
750+
.local_get(not_allowed_canister)
751+
.br_if(checks_id); // we already know that callee is not allowed => no need to check further
752+
753+
// Callee is an allowed canister => check if method name is not forbidden
754+
// no match (i.e., method name not in `forbidden_function_names`) => `allow_cycles` set to 1
755+
check_list(
756+
memory,
757+
checks,
758+
allow_cycles,
759+
name_size,
760+
name_src,
761+
None,
762+
&forbidden_function_names
763+
.iter()
764+
.map(|s| s.as_bytes())
765+
.collect::<Vec<_>>(),
766+
wasm64,
767+
);
768+
})
769+
.local_get(allow_cycles)
770+
.if_else(
771+
None,
772+
|block| {
773+
// set global to 0 => allow `ic0.call_cycles_add[128]`
774+
block.i32_const(0).global_set(global_id);
775+
},
776+
|block| {
777+
// set global to 1 => filter `ic0.call_cycles_add[128]`
778+
block.i32_const(1).global_set(global_id);
779+
},
780+
)
781+
.local_get(callee_src)
782+
.local_get(callee_size)
783+
.local_get(name_src)
784+
.local_get(name_size)
785+
.local_get(arg5)
786+
.local_get(arg6)
787+
.local_get(arg7)
788+
.local_get(arg8)
789+
.call(old_call_new);
790+
let new_call_new = builder.finish(
791+
vec![
792+
callee_src,
793+
callee_size,
794+
name_src,
795+
name_size,
796+
arg5,
797+
arg6,
798+
arg7,
799+
arg8,
800+
],
801+
&mut m.funcs,
802+
);
803+
replacer.add(old_call_new, new_call_new);
804+
}
805+
}
806+
595807
/// Get the FuncionId of a system API in ic0 import.
596808
///
597809
/// If stable_size or stable64_size is not imported, add them to the module.

0 commit comments

Comments
 (0)