forc_test/
ecal.rs

1use fuel_vm::{
2    interpreter::EcalHandler,
3    prelude::{Interpreter, RegId},
4};
5
6// ssize_t write(int fd, const void buf[.count], size_t count);
7pub const WRITE_SYSCALL: u64 = 1000;
8pub const FFLUSH_SYSCALL: u64 = 1001;
9
10#[derive(Debug, Clone)]
11pub enum Syscall {
12    Write { fd: u64, bytes: Vec<u8> },
13    Fflush { fd: u64 },
14    Unknown { ra: u64, rb: u64, rc: u64, rd: u64 },
15}
16
17impl Syscall {
18    pub fn apply(&self) {
19        use std::io::Write;
20        use std::os::fd::FromRawFd;
21        match self {
22            Syscall::Write { fd, bytes } => {
23                let s = std::str::from_utf8(bytes.as_slice()).unwrap();
24
25                let mut f = unsafe { std::fs::File::from_raw_fd(*fd as i32) };
26                write!(&mut f, "{s}").unwrap();
27
28                // Don't close the fd
29                std::mem::forget(f);
30            }
31            Syscall::Fflush { fd } => {
32                let mut f = unsafe { std::fs::File::from_raw_fd(*fd as i32) };
33                let _ = f.flush();
34
35                // Don't close the fd
36                std::mem::forget(f);
37            }
38            Syscall::Unknown { ra, rb, rc, rd } => {
39                println!("Unknown ecal: {ra} {rb} {rc} {rd}");
40            }
41        }
42    }
43}
44
45/// Handle VM `ecal` as syscalls.
46///
47/// The application of the syscalls can be turned off,
48/// guaranteeing total isolation from the outside world.
49///
50/// Capture of the syscalls can be turned on, allowing
51/// its application even after the VM is not running anymore.
52///
53/// Supported syscalls:
54/// 1000 - write(fd: u64, buf: raw_ptr, count: u64) -> u64
55#[derive(Debug, Clone)]
56pub struct EcalSyscallHandler {
57    pub apply: bool,
58    pub capture: bool,
59    pub captured: Vec<Syscall>,
60}
61
62impl Default for EcalSyscallHandler {
63    fn default() -> Self {
64        Self::only_capturing()
65    }
66}
67
68impl EcalSyscallHandler {
69    pub fn only_capturing() -> Self {
70        Self {
71            apply: false,
72            capture: true,
73            captured: vec![],
74        }
75    }
76
77    pub fn only_applying() -> Self {
78        Self {
79            apply: true,
80            capture: false,
81            captured: vec![],
82        }
83    }
84
85    pub fn clear(&mut self) {
86        self.captured.clear();
87    }
88}
89
90impl EcalHandler for EcalSyscallHandler {
91    fn ecal<M, S, Tx, V>(
92        vm: &mut Interpreter<M, S, Tx, Self, V>,
93        a: RegId,
94        b: RegId,
95        c: RegId,
96        d: RegId,
97    ) -> fuel_vm::error::SimpleResult<()>
98    where
99        M: fuel_vm::prelude::Memory,
100    {
101        let regs = vm.registers();
102        let syscall = match regs[a.to_u8() as usize] {
103            WRITE_SYSCALL => {
104                let fd = regs[b.to_u8() as usize];
105                let addr = regs[c.to_u8() as usize];
106                let count = regs[d.to_u8() as usize];
107                let bytes = vm.memory().read(addr, count).unwrap().to_vec();
108                Syscall::Write { fd, bytes }
109            }
110            FFLUSH_SYSCALL => {
111                let fd = regs[b.to_u8() as usize];
112                Syscall::Fflush { fd }
113            }
114            _ => {
115                let ra = regs[a.to_u8() as usize];
116                let rb = regs[b.to_u8() as usize];
117                let rc = regs[c.to_u8() as usize];
118                let rd = regs[d.to_u8() as usize];
119                Syscall::Unknown { ra, rb, rc, rd }
120            }
121        };
122
123        let s = vm.ecal_state_mut();
124
125        if s.apply {
126            syscall.apply();
127        }
128
129        if s.capture {
130            s.captured.push(syscall);
131        }
132
133        Ok(())
134    }
135}
136
137#[test]
138fn ok_capture_ecals() {
139    use fuel_vm::fuel_asm::op::*;
140    use fuel_vm::prelude::*;
141    let vm: Interpreter<MemoryInstance, MemoryStorage, Script, EcalSyscallHandler> = <_>::default();
142
143    let test_input = "Hello, WriteSyscall!";
144    let script_data: Vec<u8> = test_input.bytes().collect();
145    let script = vec![
146        movi(0x20, WRITE_SYSCALL as u32),
147        gtf_args(0x10, 0x00, GTFArgs::ScriptData),
148        movi(0x21, script_data.len().try_into().unwrap()),
149        ecal(0x20, 0x1, 0x10, 0x21),
150        ret(RegId::ONE),
151    ]
152    .into_iter()
153    .collect();
154
155    // Execute transaction
156    let mut client = MemoryClient::from_txtor(vm.into());
157    let tx = TransactionBuilder::script(script, script_data)
158        .script_gas_limit(1_000_000)
159        .add_fee_input()
160        .finalize()
161        .into_checked(Default::default(), &ConsensusParameters::standard())
162        .expect("failed to generate a checked tx");
163    let _ = client.transact(tx);
164
165    // Verify
166    let t: Transactor<MemoryInstance, MemoryStorage, Script, EcalSyscallHandler> = client.into();
167    let syscalls = t.interpreter().ecal_state().captured.clone();
168
169    assert_eq!(syscalls.len(), 1);
170    assert!(
171        matches!(&syscalls[0], Syscall::Write { fd: 1, bytes } if std::str::from_utf8(bytes).unwrap() == test_input)
172    );
173}