Coverage Report

Created: 2026-11-12 06:13

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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