ecall.gno
4.08 Kb · 135 lines
1package riscv
2
3import "gno.land/p/moul/x/vm/vmkit/v0"
4
5// The syscall table: the entire interface between a guest and the chain.
6//
7// Register-passing, in the RISC-V Linux convention a compiler already knows:
8// a7 (x17) selects the call, a0..a2 (x10..x12) carry arguments, a0 receives the
9// result. A guest written against these needs no gno-specific runtime, only the
10// three lines of inline assembly every no_std program already has for ecall.
11//
12// Deliberately small. Every call here maps onto something [vmkit.Host] already
13// offers, because the Host is the audited surface and a syscall that reaches
14// past it would be a second, unreviewed one.
15const (
16 sysExit = 93 // a0 = status. The Linux number, so a guest can reuse its libc shim.
17 sysWrite = 64 // a0 = fd (ignored), a1 = buf, a2 = len -> Host.Output
18 sysRead = 63 // a0 = fd (ignored), a1 = buf, a2 = len <- Host.Input
19 sysHeight = 90 // -> block height
20 sysNow = 91 // -> block time, Unix seconds
21 sysCaller = 92 // a0 = buf, a1 = len; writes the caller address, returns bytes written
22 sysLog = 94 // a0 = buf, a1 = len -> Host.Log
23 sysEmit = 95 // a0 = type buf, a1 = type len, a2 = body buf, a3 = body len
24)
25
26// maxSyscallBytes bounds one call's buffer. A guest asking to write a gigabyte
27// is a guest that found the host's allocator, not one with something to say.
28const maxSyscallBytes = 64 * 1024
29
30// ecall dispatches a syscall. It returns the machine status and whether the
31// machine should stop: only exit stops it, everything else returns to the guest
32// with a0 set.
33func (m *Machine) ecall(h vmkit.Host) (vmkit.Status, bool) {
34 switch m.reg[17] { // a7
35 case sysExit:
36 // The guest's exit status is not the machine's: a program that returns
37 // 1 has halted, it has not trapped. Trapping is for what the machine
38 // could not do, and a chain should not confuse "your code said no"
39 // with "the VM broke".
40 return vmkit.Halted, true
41
42 case sysWrite:
43 buf, ok := m.readGuest(m.reg[11], m.reg[12])
44 if !ok {
45 m.reg[10] = ^uint32(0)
46 return vmkit.Running, false
47 }
48 h.Output(buf)
49 m.outLen += len(buf)
50 m.reg[10] = uint32(len(buf))
51
52 case sysRead:
53 in := h.Input()
54 n := int(m.reg[12])
55 if n > len(in) {
56 n = len(in)
57 }
58 if n > maxSyscallBytes {
59 n = maxSyscallBytes
60 }
61 if !m.writeGuest(m.reg[11], in[:n]) {
62 m.reg[10] = ^uint32(0)
63 return vmkit.Running, false
64 }
65 m.reg[10] = uint32(n)
66
67 case sysHeight:
68 m.reg[10] = uint32(h.Height())
69
70 case sysNow:
71 m.reg[10] = uint32(h.Now())
72
73 case sysCaller:
74 addr := []byte(h.Caller().String())
75 n := int(m.reg[11])
76 if n > len(addr) {
77 n = len(addr)
78 }
79 if !m.writeGuest(m.reg[10], addr[:n]) {
80 m.reg[10] = ^uint32(0)
81 return vmkit.Running, false
82 }
83 m.reg[10] = uint32(n)
84
85 case sysLog:
86 buf, ok := m.readGuest(m.reg[10], m.reg[11])
87 if !ok {
88 m.reg[10] = ^uint32(0)
89 return vmkit.Running, false
90 }
91 h.Log(string(buf))
92 m.reg[10] = uint32(len(buf))
93
94 case sysEmit:
95 typ, ok1 := m.readGuest(m.reg[10], m.reg[11])
96 body, ok2 := m.readGuest(m.reg[12], m.reg[13])
97 if !ok1 || !ok2 {
98 m.reg[10] = ^uint32(0)
99 return vmkit.Running, false
100 }
101 h.Emit(string(typ), "body", string(body))
102 m.reg[10] = 0
103
104 default:
105 // An unknown syscall is a trap, not a silent zero. A guest compiled
106 // against a newer table must fail loudly on an older host rather than
107 // read a success it did not get.
108 m.trap = "unknown syscall"
109 return vmkit.Trapped, true
110 }
111 return vmkit.Running, false
112}
113
114// readGuest copies a buffer out of guest memory, refusing anything out of range
115// or larger than maxSyscallBytes.
116func (m *Machine) readGuest(addr, length uint32) ([]byte, bool) {
117 if length > maxSyscallBytes || !m.mem.inRange(addr, int(length)) {
118 return nil, false
119 }
120 out := make([]byte, length)
121 copy(out, m.mem.b[addr:addr+length])
122 return out, true
123}
124
125// writeGuest copies into guest memory, marking the pages it touched.
126func (m *Machine) writeGuest(addr uint32, b []byte) bool {
127 if len(b) > maxSyscallBytes || !m.mem.inRange(addr, len(b)) {
128 return false
129 }
130 copy(m.mem.b[addr:], b)
131 for i := 0; i < len(b); i++ {
132 m.mem.dirty[(uint64(addr)+uint64(i))/PageSize] = true
133 }
134 return true
135}