Skip to content
File

Blob: src/workerd/io/tracked-wasm-instance.h

cpp227 lines
1// Copyright (c) 2017-2022 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4 
5#pragma once
6 
7#include <v8-forward.h>
8#include <v8-persistent-handle.h>
9 
10#include <kj/array.h>
11#include <kj/common.h>
12 
13#include <cstdint>
14 
15namespace workerd {
16 
17namespace jsg {
18class Lock;
19} // namespace jsg
20 
21// Byte size of each signal field in WASM linear memory (a single uint32).
22constexpr size_t WASM_SIGNAL_FIELD_BYTES = sizeof(uint32_t);
23 
24// Represents a single WASM module that has opted into receiving the "shut down" signal when CPU
25// time is nearly exhausted, or that wants its memory reclaimed when the instance is garbage
26// collected. The module must export at least one of "__instance_terminated" or "__instance_signal":
27//
28// "__instance_signal" — (optional) address of a uint32 in linear memory. When present,
29// the runtime writes SIGXCPU (24) here when CPU time is nearly
30// exhausted.
31// "__instance_terminated" — (optional) address of a uint32 in linear memory. The runtime
32// writes 1 here when the isolate is killed after exceeding its CPU
33// limit, informing the WASM module that it was forcefully terminated.
34//
35// Cleanup of entries relies on a weak reference to the WASM instance (instanceRef): when V8
36// garbage-collects the instance, the handle becomes empty and the GC prologue filter removes
37// the entry, releasing the strong reference to linear memory.
38struct TrackedWasmInstance {
39 // Owns a reference to the WASM module's linear memory. The underlying v8::BackingStore is kept
40 // alive via kj::Array's attach() mechanism, preventing V8 from garbage-collecting the memory
41 // while we still need to read/write signal addresses. This gets cleaned up in a V8 GC prologue
42 // hook where we atomically remove the entry from the signal list before releasing the memory.
43 //
44 // TODO: If a user were to grow a 64 bit linear memory >16GB, relocation will happen and this
45 // array will point to stale (but not free'd) memory. The impact is that the user will see a
46 // spike in memory usage and no longer receive the signal in that module instance. In practice this
47 // should almost never happen since they would hit a memory limit well before 16GB,
48 // and 64 bit WASM is currently used very infrequently anyways. Regardless, we should address this
49 // soon.
50 kj::Array<kj::byte> memory;
51 
52 // Offset into `memory` of the uint32 the runtime writes SIGXCPU (24) to (__instance_signal).
53 // When kj::none, the module did not export __instance_signal and will not receive the
54 // SIGXCPU shutdown warning.
55 kj::Maybe<uint32_t> signalByteOffset;
56 
57 // Offset into `memory` of the uint32 the runtime writes to (__instance_terminated).
58 // When kj::none, the module did not export __instance_terminated.
59 kj::Maybe<uint32_t> terminatedByteOffset;
60 
61 // Weak handle to the WASM instance. Set to weak via SetWeak() at registration time. When V8
62 // collects the instance, the handle becomes empty (IsEmpty() returns true). Checked in the GC
63 // prologue filter to clean up entries whose instance has been garbage-collected. Always set at
64 // registration time, so IsEmpty() unambiguously means the instance was collected.
65 //
66 // This field is never accessed from signal handlers — signal handlers only touch `memory` and
67 // `signalByteOffset`. The Global's destructor is safe to call during filter() (under isolate
68 // lock) or clear() (during isolate teardown with V8 still alive).
69 v8::Global<v8::Object> instanceRef;
70 
71 // Returns true if the entry should be kept in the list, false if it should be removed.
72 // An entry is removed when V8 garbage-collects the WASM instance, which resets the weak
73 // handle making IsEmpty() return true.
74 bool shouldRetain() const {
75 return !instanceRef.IsEmpty();
76 }
77};
78 
79// A linked list type which is signal-safe (for reading), but not thread safe - it can handle
80// same-thread concurrency and pre-emptive reads ONLY.
81// SAFETY: All mutations are must happen with the isolate lock held!
82template <typename T>
83class SignalSafeList {
84 public:
85 struct Node {
86 T value;
87 Node* next;
88 template <typename... Args>
89 explicit Node(Args&&... args): value(kj::fwd<Args>(args)...),
90 next(nullptr) {}
91 };
92 
93 SignalSafeList() {}
94 
95 ~SignalSafeList() noexcept(false) {
96 Node* node = __atomic_load_n(&head, __ATOMIC_RELAXED);
97 while (node != nullptr) {
98 Node* doomed = node;
99 node = __atomic_load_n(&doomed->next, __ATOMIC_RELAXED);
100 delete doomed;
101 }
102 }
103 
104 // Prepends a new node constructed from `args` at the front of the list.
105 // Returns a reference to the inserted value. The reference is stable (heap-allocated node)
106 // and valid until the node is removed from the list via filter() or clear().
107 template <typename... Args>
108 T& pushFront(Args&&... args) {
109 Node* node = new Node(kj::fwd<Args>(args)...);
110 __atomic_store_n(&node->next, __atomic_load_n(&head, __ATOMIC_RELAXED), __ATOMIC_RELAXED);
111 __atomic_store_n(&head, node, __ATOMIC_RELEASE);
112 return node->value;
113 }
114 
115 // Removes all nodes for which `predicate(node.value)` returns false
116 template <typename Predicate>
117 void filter(Predicate&& predicate) noexcept {
118 Node** prev = &head;
119 Node* current = __atomic_load_n(prev, __ATOMIC_RELAXED);
120 
121 while (current != nullptr) {
122 Node* next = __atomic_load_n(&current->next, __ATOMIC_RELAXED);
123 
124 if (predicate(current->value)) {
125 prev = &current->next;
126 } else {
127 // Splice out `current` by pointing its predecessor at `next`. Release ordering ensures a
128 // signal handler that loads *prev with acquire sees a fully consistent successor chain.
129 __atomic_store_n(prev, next, __ATOMIC_RELEASE);
130 delete current;
131 }
132 
133 current = next;
134 }
135 }
136 
137 // Removes all nodes from the list, destroying each one.
138 void clear() noexcept {
139 Node* current = __atomic_load_n(&head, __ATOMIC_RELAXED);
140 __atomic_store_n(&head, static_cast<Node*>(nullptr), __ATOMIC_RELEASE);
141 while (current != nullptr) {
142 Node* next = __atomic_load_n(&current->next, __ATOMIC_RELAXED);
143 delete current;
144 current = next;
145 }
146 }
147 
148 // Returns true if the list is empty. Signal safe.
149 bool isEmpty() const {
150 return __atomic_load_n(&head, __ATOMIC_ACQUIRE) == nullptr;
151 }
152 
153 // Traverses the list, calling `func(node.value)` for each node. Signal-safe (same-thread
154 // only), but not thread-safe — callers from a signal handler context should const_cast.
155 template <typename Func>
156 void iterate(Func&& func) {
157 Node* current = __atomic_load_n(&head, __ATOMIC_ACQUIRE);
158 while (current != nullptr) {
159 func(current->value);
160 current = __atomic_load_n(&current->next, __ATOMIC_ACQUIRE);
161 }
162 }
163 
164 private:
165 Node* head = nullptr;
166 
167 KJ_DISALLOW_COPY_AND_MOVE(SignalSafeList);
168};
169 
170// Encapsulates a SignalSafeList<TrackedWasmInstance> with operations that require the isolate
171// lock for mutation, and a read-only accessor for signal-handler use.
172//
173// The mutation methods are const and accept a jsg::Lock& to prove the caller holds the isolate
174// lock. Internally they const_cast the list, which is safe because the lock provides the
175// required synchronization. This design allows IsolateLimitEnforcer (whose methods are const
176// per KJ convention) to return a const reference without exposing mutable access to code that
177// does not hold the lock.
178class TrackedWasmInstanceList {
179 public:
180 // Registers a WASM module for receiving the "shut down" signal and/or terminated notification.
181 // At least one of signalOffset or terminatedOffset must be provided. Both are optional
182 // individually: when signalOffset is kj::none, the module will not receive SIGXCPU; when
183 // terminatedOffset is kj::none, the module will not receive the terminated notification.
184 // Cleanup always relies on the weak instanceRef becoming empty when V8 GCs the instance.
185 //
186 // Silently skips registration if any provided offset falls outside the module's linear memory.
187 //
188 // Returns a reference to the registered entry on success, or kj::none if registration was
189 // skipped. The caller uses the returned reference to set up the weak instance handle.
190 kj::Maybe<TrackedWasmInstance&> registerSignal(jsg::Lock&,
191 kj::Array<kj::byte> memory,
192 kj::Maybe<uint32_t> signalOffset,
193 kj::Maybe<uint32_t> terminatedOffset) const;
194 
195 // Filters out entries where the instance has been garbage-collected (instanceRef is empty).
196 // Call from a GC prologue hook to allow linear memory to be reclaimed.
197 void filter(jsg::Lock&) const;
198 
199 // Removes all entries unconditionally. Call before the V8 isolate is disposed, since each
200 // entry holds a shared_ptr<v8::BackingStore> (via the memory array) and a v8::Global whose
201 // destructors may access V8 state.
202 void clear(jsg::Lock&) const;
203 
204 void writeShutdownSignal() const;
205 
206 void clearShutdownSignal() const;
207 
208 void writeTerminatedSignal() const;
209 
210 // Returns the underlying signal-safe list for use by signal handlers and the CPU time limiter.
211 // The returned reference is const; signal-handler free functions use const_cast internally.
212 const SignalSafeList<TrackedWasmInstance>& signals() const {
213 return list;
214 }
215 
216 private:
217 SignalSafeList<TrackedWasmInstance> list;
218};
219 
220// The value written to the signal address when CPU time is nearly exhausted.
221// This is the UNIX signal number for SIGXCPU (24). Technically the number itself
222// is not standardized, but for most architectures it is 24 so that is what we're going with.
223// We're inventing WASM signals from scratch so we can do whatever we want.
224constexpr uint32_t WASM_SIGNAL_SIGXCPU = 24;
225 
226} // namespace workerd