/src/wasm3/source/m3_host.h
Line | Count | Source |
1 | | // |
2 | | // m3_host.h |
3 | | // |
4 | | // What the engine asks of the system it is hosted on. |
5 | | // |
6 | | // Everything here is something only the operating system can answer: how much native |
7 | | // stack this thread still has, how to get at a file's bytes and replace a file whole, |
8 | | // and - where the build |
9 | | // asks for guarded memories - how to reserve address space, commit part of it, and |
10 | | // turn the fault from a read past the committed part back into a Wasm trap. |
11 | | // |
12 | | // Exactly one implementation is ever built: |
13 | | // |
14 | | // m3_host_win32.h VirtualAlloc/VirtualQuery, file mappings, and structured |
15 | | // exception handling |
16 | | // m3_host_posix.h mmap/mprotect, file mappings, and a SIGSEGV handler |
17 | | // m3_host_none.h a system with none of that, which is most of platforms/: |
18 | | // it reads a file rather than mapping one, and says it cannot |
19 | | // measure a stack |
20 | | // |
21 | | // All three are headers, and m3_core.c includes whichever one it is built for: a new |
22 | | // .c would have to be added to every source list that names its sources one by one, |
23 | | // and most of those are build scripts outside this repository. Unlike the WASI host |
24 | | // split, the callers here are in other translation units, so these are real |
25 | | // definitions rather than static ones - this header is the only declaration of them |
26 | | // there is. |
27 | | // |
28 | | // Every call is allowed to fail, and failing is not an error: m3_HostStackBase |
29 | | // answers NULL on a system that cannot be asked, and the engine falls back to the |
30 | | // compile-time budget. Only the reservation calls report failure the caller has to |
31 | | // act on, and only a build with d_m3GuardedMemory has any. |
32 | | // |
33 | | |
34 | | #ifndef m3_host_h |
35 | | #define m3_host_h |
36 | | |
37 | | #include "m3_core.h" |
38 | | |
39 | | d_m3BeginExternC |
40 | | |
41 | | // The lowest address the calling thread's stack can reach, or NULL when this |
42 | | // build cannot ask (see d_m3HasThreadStackProbe). What sits at the very bottom |
43 | | // is the system's business - a guard page usually - so the answer bounds the |
44 | | // stack rather than describing what is safe to touch; d_m3NativeStackMargin is |
45 | | // what keeps the difference. |
46 | | void* m3_HostStackBase (void); |
47 | | |
48 | | // Suspension interrupt handlers (Ctrl+Z / SIGTSTP / SIGINT / Console Ctrl) |
49 | | void m3_HostInstallInterruptHandler (IM3Runtime io_runtime); |
50 | | void m3_HostRemoveInterruptHandler (void); |
51 | | |
52 | | // The wall clock, in milliseconds since the Unix epoch, UTC. 0 when there is no |
53 | | // clock to ask. Only ever written down - a snapshot says when it was taken - so |
54 | | // nothing depends on it being right. |
55 | | u64 m3_HostTimeMs (void); |
56 | | |
57 | | // Blocks the calling thread for at least the given time. A host with no clock to |
58 | | // wait on returns at once, as though the time had passed. |
59 | | void m3_HostSleepNs (u64 i_ns); |
60 | | |
61 | | |
62 | | // A file's bytes and how they were come by. |
63 | | // |
64 | | // 'mapped' is the one that says who gives the bytes back, and it is not the same |
65 | | // question as whether there is a handle: a Win32 mapping keeps nothing open, because |
66 | | // the view holds the section which holds the file object and the share mode with it, |
67 | | // while POSIX has to hold the descriptor its lock lives on. So a mapped file may have |
68 | | // no handle, and d_m3HostNoHandle is what that looks like. |
69 | 0 | #define d_m3HostNoHandle ((intptr_t) -1) |
70 | | |
71 | | typedef struct M3HostFile { |
72 | | void* data; |
73 | | size_t size; |
74 | | intptr_t handle; // the host layer's, and d_m3HostNoHandle when there is none |
75 | | bool mapped; |
76 | | } M3HostFile; |
77 | | |
78 | | // The module's bytes, however this system can best get at them: mapped where it can |
79 | | // map, read onto the heap where it cannot - a system with no mapping at all, a |
80 | | // filesystem that will not, or something that is not a file, which is what a pipe |
81 | | // from a process substitution is. The caller is told which only by 'mapped', and |
82 | | // needs to know only because m3_HostUnmapFile is what gives the bytes back either |
83 | | // way. |
84 | | // |
85 | | // i_maxBytes refuses a file larger than the caller is willing to take, before |
86 | | // anything is allocated for it; 0 accepts any size the system will hand over. False |
87 | | // is that, or the file not being readable at all, and o_file is written on success |
88 | | // only. |
89 | | bool m3_HostMapFile (const char* i_path, size_t i_maxBytes, M3HostFile* o_file); |
90 | | |
91 | | // Give the bytes back, releasing the mapping and its lock, or the heap block |
92 | | void m3_HostUnmapFile (M3HostFile* io_file); |
93 | | |
94 | | // Move the file at i_from over i_to, replacing whatever is there. Where the system |
95 | | // can, in one step: a reader of i_to sees the old file or the new one, never one |
96 | | // half written, and a writer that dies part way leaves i_to as it was. False when |
97 | | // the move failed, with i_from left where it is. |
98 | | bool m3_HostReplaceFile (const char* i_from, const char* i_to); |
99 | | |
100 | | // The reading half on its own: what each m3_HostMapFile falls back to, and the whole |
101 | | // of what a system with no mapping does. The same on every system, so it is here |
102 | | // rather than in any of them - and inline, so that a translation unit which never |
103 | | // calls it is not the one that drags stdio's FILE machinery into the link. |
104 | | static inline |
105 | | bool m3_HostReadFile (const char* i_path, size_t i_maxBytes, M3HostFile* o_file) |
106 | 0 | { |
107 | 0 | FILE* f = fopen(i_path, "rb"); |
108 | 0 | if (f == NULL) { |
109 | 0 | return false; |
110 | 0 | } |
111 | | |
112 | | // The size is taken before anything is allocated, so that a file too big to be |
113 | | // wanted is refused rather than read |
114 | 0 | fseek(f, 0, SEEK_END); |
115 | 0 | long tell = ftell(f); |
116 | 0 | fseek(f, 0, SEEK_SET); |
117 | |
|
118 | 0 | if (tell < 0 or (i_maxBytes and (size_t) tell > i_maxBytes)) { |
119 | 0 | fclose(f); |
120 | 0 | return false; |
121 | 0 | } |
122 | | |
123 | 0 | size_t size = (size_t)tell; |
124 | 0 | void* data = m3_Malloc("Host File", size ? size : 1); |
125 | |
|
126 | 0 | if (data == NULL) { |
127 | 0 | fclose(f); |
128 | 0 | return false; |
129 | 0 | } |
130 | | |
131 | 0 | bool read = (fread(data, 1, size, f) == size); |
132 | 0 | fclose(f); |
133 | |
|
134 | 0 | if (not read) { |
135 | 0 | m3_Free(data); |
136 | 0 | return false; |
137 | 0 | } |
138 | | |
139 | 0 | o_file->data = data; |
140 | 0 | o_file->size = size; |
141 | 0 | o_file->handle = d_m3HostNoHandle; |
142 | 0 | o_file->mapped = false; |
143 | | return true; |
144 | 0 | } Unexecuted instantiation: m3_core.c:m3_HostReadFile Unexecuted instantiation: m3_env.c:m3_HostReadFile Unexecuted instantiation: m3_compile.c:m3_HostReadFile |
145 | | |
146 | | // A module is parsed in place and keeps pointing into the mapped bytes for as long |
147 | | // as it is loaded, so this is where a module's own size stops being paid for twice: |
148 | | // what is mapped stays in the page cache, shared with anything else running the same |
149 | | // file, and is only ever read as far as the module is walked. |
150 | | // |
151 | | // What is mapped is the file itself, so a file that changes underneath the mapping |
152 | | // changes under a running module. Each system is asked to prevent that as far as it |
153 | | // can, and the two can do very different amounts: |
154 | | // |
155 | | // Win32 a share mode the kernel enforces. Another process opening the file for |
156 | | // writing is refused outright while the mapping lives, so this is a real |
157 | | // guarantee. Other readers - another wasm3 on the same module - are fine. |
158 | | // POSIX a shared advisory lock, which stops only a writer that asks for a lock |
159 | | // of its own. Nothing stops a plain open-and-write, because POSIX has no |
160 | | // portable mandatory locking: Linux removed what it had in 5.15, and |
161 | | // O_EXLOCK is a BSD extension that is advisory as well. |
162 | | // |
163 | | // So on POSIX this narrows the window rather than closing it. |
164 | | |
165 | | #if d_m3GuardedMemory |
166 | | |
167 | | # if M3_SIZEOF_PTR < 8 |
168 | | # error "d_m3GuardedMemory needs a 64-bit address space" |
169 | | # endif |
170 | | # if d_m3FixedHeap |
171 | | # error "d_m3GuardedMemory and d_m3FixedHeap are two different places for a linear memory to come from" |
172 | | # endif |
173 | | # if defined(_WIN32) && !defined(_MSC_VER) |
174 | | # error "d_m3GuardedMemory on Windows needs __try/__except - build with MSVC or clang-cl" |
175 | | # endif |
176 | | |
177 | | // The system's allocation granularity: what a reservation's size and a commit's |
178 | | // bounds are rounded to. Never zero. |
179 | | size_t m3_HostPageSize (void); |
180 | | |
181 | | // Reserve i_bytes of address space without backing any of it. Reading or writing it |
182 | | // faults until m3_HostCommit says otherwise, which is the whole point: the fault is |
183 | | // what a Wasm access past the end of its memory turns into. NULL if the system will |
184 | | // not. |
185 | | void* m3_HostReserve (size_t i_bytes); |
186 | | |
187 | | // Back [i_address, i_address + i_bytes) with pages, which read as zero. The range |
188 | | // has to be inside a reservation from m3_HostReserve; committing what is already |
189 | | // committed is not an error and changes nothing. |
190 | | bool m3_HostCommit (void* i_address, size_t i_bytes); |
191 | | |
192 | | // Undo m3_HostCommit for [i_address, i_address + i_bytes), giving the pages back to |
193 | | // the system and leaving the range reserved and unreachable again. What was written |
194 | | // there is gone: committing it once more reads as zero. |
195 | | bool m3_HostDecommit (void* i_address, size_t i_bytes); |
196 | | |
197 | | // Give back a whole reservation. i_bytes is what was reserved. |
198 | | void m3_HostRelease (void* i_address, size_t i_bytes); |
199 | | |
200 | | // Run i_body(i_context) with a fault inside [i_guardLow, i_guardLow + i_guardBytes) |
201 | | // caught and reported rather than killing the process: false means the body touched |
202 | | // that range and did not finish. Any other fault is left to whatever would have |
203 | | // handled it, so an engine bug still looks like an engine bug. |
204 | | // |
205 | | // Nests, so a host function calling back into Wasm gets a region of its own, and is |
206 | | // per-thread. What the body was doing when it faulted is abandoned where it stands - |
207 | | // the caller has to be able to carry on without it, which for the interpreter means |
208 | | // the Wasm stack and the memory, both of which are just data. |
209 | | bool m3_HostProtectedCall (void (*i_body)(void*), void* i_context, |
210 | | void* i_guardLow, size_t i_guardBytes); |
211 | | |
212 | | // Whether m3_HostProtectedCall can actually catch that fault, which is not a given: |
213 | | // a sanitizer takes the signal exclusively and a process is free to install whatever |
214 | | // it likes afterwards. Memory operations carry no bounds check in a guarded build, so |
215 | | // a false here means there is nothing underneath them and no Wasm may run. |
216 | | // |
217 | | // Puts the catching in place where it is not there yet, so this is also the call that |
218 | | // installs it. Settled once, when it goes in: something that takes the fault away |
219 | | // afterwards is not noticed, which is the bargain any handler in a process makes. |
220 | | bool m3_HostGuardsActive (void); |
221 | | |
222 | | #endif // d_m3GuardedMemory |
223 | | |
224 | | d_m3EndExternC |
225 | | |
226 | | #endif // m3_host_h |