/src/node/test/fuzzers/fuzz_common.cc
Line  | Count  | Source  | 
1  |  | // Copyright 2025 Google LLC  | 
2  |  | //  | 
3  |  | // Licensed under the Apache License, Version 2.0 (the "License");  | 
4  |  | // you may not use this file except in compliance with the License.  | 
5  |  | // You may obtain a copy of the License at  | 
6  |  | //  | 
7  |  | //      http://www.apache.org/licenses/LICENSE-2.0  | 
8  |  | //  | 
9  |  | // Unless required by applicable law or agreed to in writing, software  | 
10  |  | // distributed under the License is distributed on an "AS IS" BASIS,  | 
11  |  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  | 
12  |  | // See the License for the specific language governing permissions and  | 
13  |  | // limitations under the License.  | 
14  |  |  | 
15  |  | #include "fuzz_common.h"  | 
16  |  |  | 
17  |  | #include <cstdlib>  | 
18  |  | #include <string>  | 
19  |  | #include <vector>  | 
20  |  |  | 
21  |  | #include "uv.h"  | 
22  |  | #include "v8.h"  | 
23  |  |  | 
24  |  | #include "node.h"  | 
25  |  | #include "node_platform.h"  | 
26  |  | #include "env-inl.h"  | 
27  |  | #include "util-inl.h"  | 
28  |  |  | 
29  |  | // cppgc platform init/shutdown like cctest does  | 
30  |  | #include "cppgc/platform.h"  | 
31  |  |  | 
32  |  | #if defined(__GLIBC__)  | 
33  |  | #include <malloc.h>  // malloc_trim  | 
34  |  | #endif  | 
35  |  |  | 
36  |  | namespace fuzz { | 
37  |  | namespace { | 
38  |  |  | 
39  |  | // -------- Process-wide, persistent Node/V8 state (single Environment) --------  | 
40  |  | std::unique_ptr<node::NodePlatform> g_platform;  | 
41  |  | uv_loop_t                            g_persist_loop;  | 
42  |  |  | 
43  |  | using ABAUnique =  | 
44  |  |     std::unique_ptr<node::ArrayBufferAllocator, decltype(&node::FreeArrayBufferAllocator)>;  | 
45  |  |  | 
46  |  | ABAUnique                            g_persist_allocator{ nullptr, &node::FreeArrayBufferAllocator }; | 
47  |  |  | 
48  |  | v8::Isolate*                         g_iso = nullptr;  | 
49  |  | v8::Global<v8::Context>              g_ctx;  | 
50  |  | node::IsolateData*                   g_iso_data = nullptr;  | 
51  |  | node::Environment*                   g_env = nullptr;  | 
52  |  |  | 
53  |  | // Pre-compiled JS function: (a:ArrayBuffer, b:ArrayBuffer) => Buffer.compare(...)  | 
54  |  | v8::Global<v8::Function>             g_bufcmp_fn;  | 
55  |  |  | 
56  |  | // Helper: run platform tasks + libuv + microtasks once.  | 
57  |  | // Returns true if any progress was made.  | 
58  |  | // NOTE: Callers must have entered the isolate (Isolate::Scope) before calling.  | 
59  |  | static inline bool OnePump(v8::Isolate* isolate,  | 
60  |  |                            node::NodePlatform* platform,  | 
61  | 22.1k  |                            uv_loop_t* loop) { | 
62  | 22.1k  |   bool progressed = false;  | 
63  | 22.1k  |   platform->DrainTasks(isolate);  | 
64  | 22.1k  |   progressed |= (uv_run(loop, UV_RUN_NOWAIT) != 0);  | 
65  | 22.1k  |   isolate->PerformMicrotaskCheckpoint();  | 
66  | 22.1k  |   return progressed;  | 
67  | 22.1k  | }  | 
68  |  |  | 
69  |  | // Drain up to max_spins or until the loop is idle.  | 
70  |  | // Enters the isolate to satisfy V8 invariants while touching microtasks/heap.  | 
71  |  | static inline void DrainUntilIdle(v8::Isolate* isolate,  | 
72  |  |                                   node::NodePlatform* platform,  | 
73  |  |                                   uv_loop_t* loop,  | 
74  | 22.1k  |                                   int max_spins = 256) { | 
75  | 22.1k  |   v8::Isolate::Scope iso_scope(isolate);  | 
76  | 22.1k  |   v8::HandleScope hs(isolate);  | 
77  | 22.1k  |   for (int i = 0; i < max_spins; ++i) { | 
78  | 22.1k  |     const bool progressed = OnePump(isolate, platform, loop);  | 
79  | 22.1k  |     if (!progressed && !uv_loop_alive(loop)) break;  | 
80  | 22.1k  |   }  | 
81  | 22.1k  | }  | 
82  |  |  | 
83  |  | // Build (once) a comparator that DOES NOT copy: Buffer.from(ArrayBuffer) shares memory.  | 
84  | 35  | static void BuildBufCmpOnce() { | 
85  | 35  |   if (!g_bufcmp_fn.IsEmpty()) return;  | 
86  |  |  | 
87  | 35  |   v8::Isolate::Scope iso_scope(g_iso);  | 
88  | 35  |   v8::HandleScope hs(g_iso);  | 
89  | 35  |   v8::Local<v8::Context> ctx = g_ctx.Get(g_iso);  | 
90  | 35  |   v8::Context::Scope cs(ctx);  | 
91  |  |  | 
92  |  |   // Share the underlying ArrayBuffer; avoid Uint8Array -> Buffer copy.  | 
93  |  |   // Docs: Buffer.from(arrayBuffer[, byteOffset[, length]]) shares memory.  | 
94  | 35  |   constexpr const char* kSrc = R"JS(  | 
95  | 35  |     (function(a, b) { | 
96  | 35  |       const bufa = Buffer.from(a);  | 
97  | 35  |       const bufb = Buffer.from(b);  | 
98  | 35  |       return Buffer.compare(bufa, bufb);  | 
99  | 35  |     })  | 
100  | 35  |   )JS";  | 
101  |  |  | 
102  | 35  |   v8::Local<v8::String> src;  | 
103  | 35  |   if (!v8::String::NewFromUtf8(g_iso, kSrc, v8::NewStringType::kNormal).ToLocal(&src)) return;  | 
104  | 35  |   v8::Local<v8::Script> script;  | 
105  | 35  |   if (!v8::Script::Compile(ctx, src).ToLocal(&script)) return;  | 
106  | 35  |   v8::Local<v8::Value> fn_val;  | 
107  | 35  |   if (!script->Run(ctx).ToLocal(&fn_val)) return;  | 
108  | 35  |   g_bufcmp_fn.Reset(g_iso, fn_val.As<v8::Function>());  | 
109  | 35  | }  | 
110  |  |  | 
111  | 35  | void GlobalShutdown() { | 
112  | 35  |   if (g_env != nullptr) { | 
113  | 35  |     v8::Isolate::Scope iso_scope(g_iso);  | 
114  | 35  |     v8::HandleScope hs(g_iso);  | 
115  | 35  |     v8::Local<v8::Context> ctx = g_ctx.Get(g_iso);  | 
116  | 35  |     v8::Context::Scope cs(ctx);  | 
117  |  |  | 
118  | 35  |     node::RunAtExit(g_env);  | 
119  | 35  |     node::Stop(g_env);  | 
120  | 35  |     DrainUntilIdle(g_iso, g_platform.get(), &g_persist_loop);  | 
121  |  |  | 
122  | 35  |     node::FreeEnvironment(g_env);  | 
123  | 35  |     g_env = nullptr;  | 
124  | 35  |   }  | 
125  |  |  | 
126  | 35  |   if (g_iso_data != nullptr) { | 
127  | 35  |     node::FreeIsolateData(g_iso_data);  | 
128  | 35  |     g_iso_data = nullptr;  | 
129  | 35  |   }  | 
130  |  |  | 
131  | 35  |   g_bufcmp_fn.Reset();  | 
132  | 35  |   g_ctx.Reset();  | 
133  |  |  | 
134  | 35  |   if (g_iso != nullptr) { | 
135  |  |     // Dispose the isolate via the platform so its per-isolate queues are freed.  | 
136  | 35  |     g_platform->DisposeIsolate(g_iso);  | 
137  | 35  |     g_iso = nullptr;  | 
138  | 35  |   }  | 
139  |  |  | 
140  |  |   // Close the persistent libuv loop.  | 
141  | 35  |   uv_loop_close(&g_persist_loop);  | 
142  |  |  | 
143  |  |   // Bring down cppgc + V8 + platform (order matters).  | 
144  | 35  |   g_platform->Shutdown();  | 
145  | 35  |   cppgc::ShutdownProcess();  | 
146  | 35  |   v8::V8::Dispose();  | 
147  | 35  |   v8::V8::DisposePlatform();  | 
148  |  |  | 
149  | 35  |   g_platform.reset();  | 
150  | 35  |   g_persist_allocator.reset();  | 
151  | 35  | }  | 
152  |  |  | 
153  |  | // Set up the persistent Environment once.  | 
154  | 44.1k  | static void InitializePersistentEnvOnce() { | 
155  | 44.1k  |   if (g_env != nullptr) return;  // already initialized  | 
156  |  |  | 
157  | 35  |   uv_os_unsetenv("NODE_OPTIONS"); | 
158  |  |  | 
159  |  |   // Small, fast platform with no tracing.  | 
160  | 35  |   static constexpr int kV8ThreadPoolSize = 1;  | 
161  | 35  |   g_platform = std::make_unique<node::NodePlatform>(kV8ThreadPoolSize, /*tracing_controller=*/nullptr);  | 
162  | 35  |   v8::V8::InitializePlatform(g_platform.get());  | 
163  |  |  | 
164  |  |   // Parse Node/V8 flags BEFORE V8::Initialize() to avoid "IsFrozen()" asserts.  | 
165  | 35  |   std::vector<std::string> node_argv{ "fuzz_env" }; | 
166  | 35  |   (void) node::InitializeOncePerProcess(  | 
167  | 35  |       node_argv,  | 
168  | 35  |       node::ProcessInitializationFlags::kLegacyInitializeNodeWithArgsBehavior);  | 
169  |  |  | 
170  |  |   // Initialize cppgc + V8  | 
171  | 35  |   cppgc::InitializeProcess(g_platform->GetPageAllocator());  | 
172  | 35  |   v8::V8::Initialize();  | 
173  |  |  | 
174  |  |   // Persistent libuv loop for this Environment.  | 
175  | 35  |   (void)uv_loop_init(&g_persist_loop);  | 
176  |  |  | 
177  |  |   // Process-wide allocator for this persistent isolate.  | 
178  | 35  |   g_persist_allocator.reset(node::CreateArrayBufferAllocator());  | 
179  |  |  | 
180  |  |   // Create isolate, context, and Node Environment.  | 
181  | 35  |   g_iso = node::NewIsolate(g_persist_allocator.get(), &g_persist_loop, g_platform.get());  | 
182  | 35  |   { | 
183  | 35  |     v8::Isolate::Scope iso_scope(g_iso);  | 
184  | 35  |     v8::HandleScope hs(g_iso);  | 
185  |  |  | 
186  | 35  |     v8::Local<v8::Context> ctx = node::NewContext(g_iso);  | 
187  | 35  |     g_ctx.Reset(g_iso, ctx);  | 
188  | 35  |     v8::Context::Scope cs(ctx);  | 
189  |  |  | 
190  | 35  |     g_iso_data = node::CreateIsolateData(g_iso, &g_persist_loop, g_platform.get());  | 
191  |  |  | 
192  | 35  |     std::vector<std::string> args{ "node" }; | 
193  | 35  |     std::vector<std::string> exec_args;  | 
194  | 35  |     node::EnvironmentFlags::Flags flags = node::EnvironmentFlags::kDefaultFlags;  | 
195  |  |  | 
196  | 35  |     g_env = node::CreateEnvironment(g_iso_data, ctx, args, exec_args, flags);  | 
197  |  |  | 
198  |  |     // Bootstrap Node (no entry script).  | 
199  | 35  |     node::LoadEnvironment(g_env, const_cast<char*>("")); | 
200  |  |  | 
201  |  |     // Build and cache the comparator function.  | 
202  | 35  |     BuildBufCmpOnce();  | 
203  | 35  |   }  | 
204  |  |  | 
205  |  |   // Ensure we tear everything down at process exit.  | 
206  | 35  |   std::atexit(&GlobalShutdown);  | 
207  | 35  | }  | 
208  |  |  | 
209  |  | }  // namespace  | 
210  |  |  | 
211  |  | // ------------------------ IsolateScope (lightweight façade) --------------------  | 
212  |  |  | 
213  | 22.0k  | IsolateScope::IsolateScope() { | 
214  |  |   // Ensure persistent env/isolate exist, then "enter" the isolate so code that  | 
215  |  |   // expects an entered isolate continues to work. This does NOT own the isolate.  | 
216  | 22.0k  |   InitializePersistentEnvOnce();  | 
217  | 22.0k  |   isolate_ = g_iso;  | 
218  | 22.0k  |   if (isolate_) isolate_->Enter();  | 
219  | 22.0k  | }  | 
220  |  |  | 
221  | 22.0k  | IsolateScope::~IsolateScope() { | 
222  | 22.0k  |   if (!isolate_) return;  | 
223  |  |   // Leave the isolate; do NOT dispose it (persistent env owns it).  | 
224  | 22.0k  |   isolate_->Exit();  | 
225  | 22.0k  |   isolate_ = nullptr;  | 
226  |  |  | 
227  | 22.0k  | #if defined(__GLIBC__)  | 
228  |  |   // Keep RSS in check during long runs.  | 
229  | 22.0k  |   malloc_trim(0);  | 
230  | 22.0k  | #endif  | 
231  | 22.0k  | }  | 
232  |  |  | 
233  |  | // ----------------------- Public helpers (persistent env) -----------------------  | 
234  |  |  | 
235  |  | void RunEnvString(v8::Isolate* /*unused*/,  | 
236  |  |                   const char* env_js,  | 
237  | 8.61k  |                   const EnvRunOptions& /*opts*/) { | 
238  | 8.61k  |   InitializePersistentEnvOnce();  | 
239  |  |  | 
240  | 8.61k  |   v8::Isolate::Scope iso_scope(g_iso);  | 
241  | 8.61k  |   v8::HandleScope hs(g_iso);  | 
242  | 8.61k  |   v8::Local<v8::Context> ctx = g_ctx.Get(g_iso);  | 
243  | 8.61k  |   v8::Context::Scope cs(ctx);  | 
244  |  |  | 
245  | 8.61k  |   if (!env_js) env_js = "";  | 
246  |  |  | 
247  | 8.61k  |   v8::TryCatch tc(g_iso);  | 
248  | 8.61k  |   v8::Local<v8::String> src;  | 
249  | 8.61k  |   if (v8::String::NewFromUtf8(g_iso, env_js, v8::NewStringType::kNormal).ToLocal(&src)) { | 
250  | 8.61k  |     v8::Local<v8::Script> script;  | 
251  | 8.61k  |     if (v8::Script::Compile(ctx, src).ToLocal(&script)) { | 
252  | 0  |       (void)script->Run(ctx);  | 
253  | 0  |     }  | 
254  | 8.61k  |   }  | 
255  |  |  | 
256  |  |   // Keep the job stateless: drain until idle; do not Stop()/FreeEnvironment().  | 
257  | 8.61k  |   DrainUntilIdle(g_iso, g_platform.get(), &g_persist_loop);  | 
258  | 8.61k  | }  | 
259  |  |  | 
260  |  | void RunInEnvironment(v8::Isolate* /*unused*/,  | 
261  |  |                       EnvCallback cb,  | 
262  | 13.4k  |                       const EnvRunOptions& /*opts*/) { | 
263  | 13.4k  |   InitializePersistentEnvOnce();  | 
264  |  |  | 
265  | 13.4k  |   v8::Isolate::Scope iso_scope(g_iso);  | 
266  | 13.4k  |   v8::HandleScope hs(g_iso);  | 
267  | 13.4k  |   v8::Local<v8::Context> ctx = g_ctx.Get(g_iso);  | 
268  | 13.4k  |   v8::Context::Scope cs(ctx);  | 
269  |  |  | 
270  | 13.4k  |   cb(g_env, ctx);  | 
271  | 13.4k  |   DrainUntilIdle(g_iso, g_platform.get(), &g_persist_loop);  | 
272  | 13.4k  | }  | 
273  |  |  | 
274  |  | }  // namespace fuzz  |