/src/node/test/fuzzers/fuzz_strings.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  |  | /*  | 
16  |  |  * A fuzzer focused on C string -> Javascript String using N-API.  | 
17  |  |  * Extended to cover UTF-16, external strings, optimized property keys,  | 
18  |  |  * length-query getter paths, napi_coerce_to_string, and exception draining.  | 
19  |  |  */  | 
20  |  |  | 
21  |  | #include <cstdint>  | 
22  |  | #include <cstdlib>  | 
23  |  | #include <cstring>  | 
24  |  | #include <string>  | 
25  |  |  | 
26  |  | #include "js_native_api.h"  | 
27  |  | #include "js_native_api_v8.h"  | 
28  |  | #include "node.h"  | 
29  |  | #include "node_internals.h"  | 
30  |  | #include "node_api_internals.h"  | 
31  |  | #include "env-inl.h"  | 
32  |  | #include "util-inl.h"  | 
33  |  | #include "v8.h"  | 
34  |  |  | 
35  |  | #include "fuzz_common.h"  // IsolateScope + RunInEnvironment  | 
36  |  |  | 
37  |  | // --- Helpers ---------------------------------------------------------------  | 
38  |  |  | 
39  |  | // Drain (and ignore) any pending exception on the env to avoid fatal V8 scopes  | 
40  | 538k  | static inline void DrainLastException(napi_env env) { | 
41  | 538k  |   if (!env) return;  | 
42  | 538k  |   bool pending = false;  | 
43  | 538k  |   if (napi_is_exception_pending(env, &pending) == napi_ok && pending) { | 
44  | 21.1k  |     napi_value exc = nullptr;  | 
45  | 21.1k  |     (void)napi_get_and_clear_last_exception(env, &exc);  | 
46  | 21.1k  |   }  | 
47  | 538k  | }  | 
48  |  |  | 
49  |  | // Optional: same deleter you had (used for external strings)  | 
50  | 26.9k  | static void free_string(node_api_nogc_env /*env*/, void* data, void* /*hint*/) { | 
51  | 26.9k  |   std::free(data);  | 
52  | 26.9k  | }  | 
53  |  |  | 
54  |  | // Static used only to receive env from addon init; reset every run.  | 
55  |  | static napi_env g_addon_env = nullptr;  | 
56  |  |  | 
57  |  | // Non-capturing addon init to receive napi_env  | 
58  | 13.4k  | static napi_value CaptureEnvInit(napi_env env, napi_value exports) { | 
59  | 13.4k  |   g_addon_env = env;  | 
60  | 13.4k  |   return exports;  | 
61  | 13.4k  | }  | 
62  |  |  | 
63  | 22.0k  | extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { | 
64  | 22.0k  |   const char* bytes = reinterpret_cast<const char*>(data);  | 
65  | 22.0k  |   std::string s(bytes, size);  | 
66  |  |  | 
67  | 22.0k  |   fuzz::IsolateScope iso;  | 
68  | 22.0k  |   if (!iso.ok()) return 0;  | 
69  |  |  | 
70  |  |   // Fresh Context + Environment for this input  | 
71  | 22.0k  |   fuzz::RunInEnvironment(iso.isolate(),  | 
72  | 22.0k  |     [&](node::Environment* /*env*/, v8::Local<v8::Context> context) { | 
73  | 13.4k  |       g_addon_env = nullptr;  // reset before registering  | 
74  |  |  | 
75  |  |       // Create module/exports objects and register a dummy addon to obtain napi_env.  | 
76  | 13.4k  |       v8::Isolate* isolate = v8::Isolate::GetCurrent();  | 
77  | 13.4k  |       v8::Local<v8::Object> module_obj  = v8::Object::New(isolate);  | 
78  | 13.4k  |       v8::Local<v8::Object> exports_obj = v8::Object::New(isolate);  | 
79  |  |  | 
80  | 13.4k  |       napi_module_register_by_symbol(  | 
81  | 13.4k  |           exports_obj, module_obj, context, &CaptureEnvInit, NAPI_VERSION);  | 
82  |  |  | 
83  | 13.4k  |       napi_env addon_env = g_addon_env;  | 
84  | 13.4k  |       if (addon_env == nullptr) { | 
85  |  |         // Couldn’t get an env; bail out gracefully.  | 
86  | 0  |         return;  | 
87  | 0  |       }  | 
88  |  |  | 
89  |  |       // ---- Original N-API string ops (augmented below) ----  | 
90  | 13.4k  |       size_t copied1 = 0, copied2 = 0;  | 
91  | 13.4k  |       bool copied3 = false;  | 
92  | 13.4k  |       napi_value output1{}, output2{}, output3{}, output4{}, output5{}, output6{}, | 
93  | 13.4k  |                  output7{}, output8{}, output9{}, output10{}, output11{}, | 
94  | 13.4k  |                  output12{}; | 
95  |  |  | 
96  |  |       // Allocate temp buffers (respecting size)  | 
97  | 13.4k  |       char* buf1 = static_cast<char*>(std::malloc(size ? size : 1));  | 
98  | 13.4k  |       char* buf2 = static_cast<char*>(std::malloc(size ? size : 1));  | 
99  | 13.4k  |       if (!buf1 || !buf2) { | 
100  | 0  |         std::free(buf1); std::free(buf2);  | 
101  | 0  |         return;  | 
102  | 0  |       }  | 
103  |  |  | 
104  |  |       // create/get UTF-8  | 
105  | 13.4k  |       (void) napi_create_string_utf8(addon_env, s.data(), size, &output1);  | 
106  | 13.4k  |       DrainLastException(addon_env);  | 
107  | 13.4k  |       (void) napi_get_value_string_utf8(addon_env, output1, buf1, size, &copied1);  | 
108  | 13.4k  |       DrainLastException(addon_env);  | 
109  |  |  | 
110  |  |       // create/get Latin-1  | 
111  | 13.4k  |       (void) napi_create_string_latin1(addon_env, s.data(), size, &output2);  | 
112  | 13.4k  |       DrainLastException(addon_env);  | 
113  | 13.4k  |       (void) napi_get_value_string_latin1(addon_env, output2, buf2, size, &copied2);  | 
114  | 13.4k  |       DrainLastException(addon_env);  | 
115  |  |  | 
116  |  |       // symbol.for  | 
117  | 13.4k  |       (void) node_api_symbol_for(addon_env, s.data(), size, &output4);  | 
118  | 13.4k  |       DrainLastException(addon_env);  | 
119  |  |  | 
120  |  |       // Property ops using raw fuzz bytes for the property name  | 
121  | 13.4k  |       (void) napi_set_named_property(addon_env, output1, s.c_str(), output2);  | 
122  | 13.4k  |       DrainLastException(addon_env);  | 
123  | 13.4k  |       (void) napi_get_named_property(addon_env, output1, s.c_str(), &output6);  | 
124  | 13.4k  |       DrainLastException(addon_env);  | 
125  | 13.4k  |       (void) napi_has_named_property(addon_env, output1, s.c_str(), &copied3);  | 
126  | 13.4k  |       DrainLastException(addon_env);  | 
127  |  |  | 
128  | 13.4k  |       (void) napi_get_property_names(addon_env, output1, &output7);  | 
129  | 13.4k  |       DrainLastException(addon_env);  | 
130  | 13.4k  |       (void) napi_has_property(addon_env, output1, output2, &copied3);  | 
131  | 13.4k  |       DrainLastException(addon_env);  | 
132  | 13.4k  |       (void) napi_get_property(addon_env, output1, output2, &output8);  | 
133  | 13.4k  |       DrainLastException(addon_env);  | 
134  | 13.4k  |       (void) napi_delete_property(addon_env, output1, output2, &copied3);  | 
135  | 13.4k  |       DrainLastException(addon_env);  | 
136  | 13.4k  |       (void) napi_has_own_property(addon_env, output1, output2, &copied3);  | 
137  | 13.4k  |       DrainLastException(addon_env);  | 
138  |  |  | 
139  | 13.4k  |       (void) napi_create_type_error(addon_env, output1, output2, &output9);  | 
140  | 13.4k  |       DrainLastException(addon_env);  | 
141  | 13.4k  |       (void) napi_create_range_error(addon_env, output1, output2, &output10);  | 
142  | 13.4k  |       DrainLastException(addon_env);  | 
143  | 13.4k  |       (void) node_api_create_syntax_error(addon_env, output1, output2, &output11);  | 
144  | 13.4k  |       DrainLastException(addon_env);  | 
145  |  |  | 
146  | 13.4k  |       (void) napi_run_script(addon_env, output2, &output12);  | 
147  | 13.4k  |       DrainLastException(addon_env);  | 
148  |  |  | 
149  |  |       // -----------------------------------------------------------------  | 
150  |  |       // Length-query getter paths for UTF-8 / Latin-1  | 
151  | 13.4k  |       size_t needed_utf8 = 0;  | 
152  | 13.4k  |       (void) napi_get_value_string_utf8(addon_env, output1, nullptr, 0, &needed_utf8);  | 
153  | 13.4k  |       DrainLastException(addon_env);  | 
154  | 13.4k  |       if (needed_utf8 > 0 && needed_utf8 < (1ull << 20)) { // cap alloc | 
155  | 13.3k  |         char* dyn_utf8 = static_cast<char*>(std::malloc(needed_utf8 + 1));  | 
156  | 13.3k  |         if (dyn_utf8) { | 
157  | 13.3k  |           size_t got = 0;  | 
158  | 13.3k  |           (void) napi_get_value_string_utf8(addon_env, output1, dyn_utf8, needed_utf8 + 1, &got);  | 
159  | 13.3k  |           DrainLastException(addon_env);  | 
160  | 13.3k  |           std::free(dyn_utf8);  | 
161  | 13.3k  |         }  | 
162  | 13.3k  |       }  | 
163  |  |  | 
164  | 13.4k  |       size_t needed_latin1 = 0;  | 
165  | 13.4k  |       (void) napi_get_value_string_latin1(addon_env, output2, nullptr, 0, &needed_latin1);  | 
166  | 13.4k  |       DrainLastException(addon_env);  | 
167  | 13.4k  |       if (needed_latin1 > 0 && needed_latin1 < (1ull << 20)) { | 
168  | 13.4k  |         char* dyn_latin1 = static_cast<char*>(std::malloc(needed_latin1 + 1));  | 
169  | 13.4k  |         if (dyn_latin1) { | 
170  | 13.4k  |           size_t got = 0;  | 
171  | 13.4k  |           (void) napi_get_value_string_latin1(addon_env, output2, dyn_latin1, needed_latin1 + 1, &got);  | 
172  | 13.4k  |           DrainLastException(addon_env);  | 
173  | 13.4k  |           std::free(dyn_latin1);  | 
174  | 13.4k  |         }  | 
175  | 13.4k  |       }  | 
176  |  |  | 
177  |  |       // UTF-16 create/get and length-query path  | 
178  |  |       // Build a UTF-16LE buffer from fuzz bytes (pair up bytes; if odd, drop last byte)  | 
179  | 13.4k  |       size_t u16_len = size / 2;  | 
180  | 13.4k  |       if (u16_len == 0) u16_len = 1; // ensure non-zero for coverage  | 
181  | 13.4k  |       char16_t* u16_in = static_cast<char16_t*>(std::malloc(sizeof(char16_t) * u16_len));  | 
182  | 13.4k  |       if (u16_in) { | 
183  | 125M  |         for (size_t i = 0; i < u16_len; ++i) { | 
184  | 125M  |           uint16_t lo = (2*i < size) ? static_cast<uint8_t>(data[2*i]) : 0;  | 
185  | 125M  |           uint16_t hi = (2*i + 1 < size) ? static_cast<uint8_t>(data[2*i + 1]) : 0;  | 
186  | 125M  |           u16_in[i] = static_cast<char16_t>((hi << 8) | lo);  | 
187  | 125M  |         }  | 
188  | 13.4k  |         napi_value out_u16{}; | 
189  | 13.4k  |         (void) napi_create_string_utf16(addon_env, u16_in, u16_len, &out_u16);  | 
190  | 13.4k  |         DrainLastException(addon_env);  | 
191  |  |  | 
192  |  |         // Length query first  | 
193  | 13.4k  |         size_t needed_u16 = 0;  | 
194  | 13.4k  |         (void) napi_get_value_string_utf16(addon_env, out_u16, nullptr, 0, &needed_u16);  | 
195  | 13.4k  |         DrainLastException(addon_env);  | 
196  |  |  | 
197  |  |         // Then allocate and fetch  | 
198  | 13.4k  |         if (needed_u16 == 0) needed_u16 = 1;  | 
199  | 13.4k  |         char16_t* u16_out = static_cast<char16_t*>(std::malloc(sizeof(char16_t) * (needed_u16 + 1)));  | 
200  | 13.4k  |         if (u16_out) { | 
201  | 13.4k  |           size_t got_u16 = 0;  | 
202  | 13.4k  |           (void) napi_get_value_string_utf16(addon_env, out_u16, u16_out, needed_u16 + 1, &got_u16);  | 
203  | 13.4k  |           DrainLastException(addon_env);  | 
204  | 13.4k  |           std::free(u16_out);  | 
205  | 13.4k  |         }  | 
206  |  |  | 
207  |  |         // Use the UTF-16 string in property ops too  | 
208  | 13.4k  |         (void) napi_set_property(addon_env, out_u16, output2 /*key string*/, out_u16);  | 
209  | 13.4k  |         (void) napi_has_property(addon_env, out_u16, output2, &copied3);  | 
210  | 13.4k  |         DrainLastException(addon_env);  | 
211  |  |  | 
212  | 13.4k  |         std::free(u16_in);  | 
213  | 13.4k  |       }  | 
214  |  |  | 
215  |  |       // Optimized property-key creators (UTF-8 / Latin-1 / UTF-16)  | 
216  | 13.4k  |       napi_value key_u8{}, key_l1{}, key_u16{}; | 
217  | 13.4k  |       (void) node_api_create_property_key_utf8(addon_env, s.data(), size, &key_u8);  | 
218  | 13.4k  |       (void) node_api_create_property_key_latin1(addon_env, s.data(), size, &key_l1);  | 
219  | 13.4k  |       DrainLastException(addon_env);  | 
220  |  |  | 
221  |  |       // Build a short u16 key buffer (re-use first few code units of previous conversion)  | 
222  | 13.4k  |       size_t key16_len = (size / 2) ? (size / 2) : 1;  | 
223  | 13.4k  |       char16_t* key16_buf = static_cast<char16_t*>(std::malloc(sizeof(char16_t) * key16_len));  | 
224  | 13.4k  |       if (key16_buf) { | 
225  | 125M  |         for (size_t i = 0; i < key16_len; ++i) { | 
226  | 125M  |           uint16_t lo = (2*i < size) ? static_cast<uint8_t>(data[2*i]) : 0;  | 
227  | 125M  |           uint16_t hi = (2*i + 1 < size) ? static_cast<uint8_t>(data[2*i + 1]) : 0;  | 
228  | 125M  |           key16_buf[i] = static_cast<char16_t>((hi << 8) | lo);  | 
229  | 125M  |         }  | 
230  | 13.4k  |         (void) node_api_create_property_key_utf16(addon_env, key16_buf, key16_len, &key_u16);  | 
231  | 13.4k  |         DrainLastException(addon_env);  | 
232  | 13.4k  |       }  | 
233  |  |  | 
234  |  |       // Try using these keys on a string receiver (boxed in JS)  | 
235  | 13.4k  |       if (key_u8)  { | 
236  | 13.4k  |         napi_value tmp{}; | 
237  | 13.4k  |         (void) napi_set_property(addon_env, output1, key_u8, output2);  | 
238  | 13.4k  |         DrainLastException(addon_env);  | 
239  | 13.4k  |         (void) napi_get_property(addon_env, output1, key_u8, &tmp);  | 
240  | 13.4k  |         DrainLastException(addon_env);  | 
241  | 13.4k  |       }  | 
242  | 13.4k  |       if (key_l1)  { | 
243  | 13.4k  |         napi_value tmp{}; | 
244  | 13.4k  |         (void) napi_set_property(addon_env, output1, key_l1, output1);  | 
245  | 13.4k  |         DrainLastException(addon_env);  | 
246  | 13.4k  |         (void) napi_get_property(addon_env, output1, key_l1, &tmp);  | 
247  | 13.4k  |         DrainLastException(addon_env);  | 
248  | 13.4k  |       }  | 
249  | 13.4k  |       if (key_u16) { | 
250  | 13.4k  |         napi_value tmp{}; | 
251  | 13.4k  |         (void) napi_set_property(addon_env, output1, key_u16, output4);  | 
252  | 13.4k  |         DrainLastException(addon_env);  | 
253  | 13.4k  |         (void) napi_get_property(addon_env, output1, key_u16, &tmp);  | 
254  | 13.4k  |         DrainLastException(addon_env);  | 
255  | 13.4k  |       }  | 
256  |  |  | 
257  | 13.4k  |       std::free(key16_buf);  | 
258  |  |  | 
259  |  |       // External strings (Latin-1 and UTF-16) with finalizers.  | 
260  |  |       // For external-string APIs: if 'copied' comes back true, free immediately  | 
261  |  |       // (engine made a copy and will NOT call the finalizer). If false, the GC  | 
262  |  |       // will call 'free_string' later, so don't free here.  | 
263  |  |  | 
264  |  |       // External Latin-1  | 
265  | 13.4k  |       { | 
266  | 13.4k  |         bool copied = false;  | 
267  | 13.4k  |         size_t ext_len = size ? size : 1;  | 
268  | 13.4k  |         char* ext_l1 = static_cast<char*>(std::malloc(ext_len));  | 
269  | 13.4k  |         if (ext_l1) { | 
270  | 13.4k  |           if (size) { | 
271  | 13.4k  |             std::memcpy(ext_l1, s.data(), size);  | 
272  | 13.4k  |           } else { | 
273  | 0  |             ext_l1[0] = '\0';  // deterministic content when input is empty  | 
274  | 0  |           }  | 
275  |  |  | 
276  | 13.4k  |           napi_value ext_l1_val{}; | 
277  | 13.4k  |           (void) node_api_create_external_string_latin1(  | 
278  | 13.4k  |               addon_env, ext_l1, ext_len, free_string, nullptr, &ext_l1_val, &copied);  | 
279  | 13.4k  |           DrainLastException(addon_env);  | 
280  |  |  | 
281  |  |           // Touch it a bit  | 
282  | 13.4k  |           (void) napi_coerce_to_string(addon_env, ext_l1_val, &output3);  | 
283  | 13.4k  |           (void) napi_has_property(addon_env, ext_l1_val, key_u8 ? key_u8 : output2, &copied3);  | 
284  | 13.4k  |           DrainLastException(addon_env);  | 
285  |  |  | 
286  | 13.4k  |           if (copied) { | 
287  |  |             // Engine copied data; finalizer won't run. Free now.  | 
288  | 0  |             std::free(ext_l1);  | 
289  | 0  |           }  | 
290  | 13.4k  |         }  | 
291  | 13.4k  |       }  | 
292  |  |  | 
293  |  |       // External UTF-16  | 
294  | 13.4k  |       { | 
295  | 13.4k  |         bool copied_ext16 = false;  | 
296  | 13.4k  |         size_t ext16_len = (size / 2) ? (size / 2) : 1;  | 
297  | 13.4k  |         char16_t* ext_u16 = static_cast<char16_t*>(std::malloc(sizeof(char16_t) * ext16_len));  | 
298  | 13.4k  |         if (ext_u16) { | 
299  | 125M  |           for (size_t i = 0; i < ext16_len; ++i) { | 
300  | 125M  |             uint16_t lo = (2*i < size) ? static_cast<uint8_t>(data[2*i]) : 0;  | 
301  | 125M  |             uint16_t hi = (2*i + 1 < size) ? static_cast<uint8_t>(data[2*i + 1]) : 0;  | 
302  | 125M  |             ext_u16[i] = static_cast<char16_t>((hi << 8) | lo);  | 
303  | 125M  |           }  | 
304  |  |  | 
305  | 13.4k  |           napi_value ext_u16_val{}; | 
306  | 13.4k  |           (void) node_api_create_external_string_utf16(  | 
307  | 13.4k  |               addon_env, ext_u16, ext16_len, free_string, nullptr, &ext_u16_val, &copied_ext16);  | 
308  | 13.4k  |           DrainLastException(addon_env);  | 
309  |  |  | 
310  |  |           // Exercise getter path on it  | 
311  | 13.4k  |           size_t need16 = 0;  | 
312  | 13.4k  |           (void) napi_get_value_string_utf16(addon_env, ext_u16_val, nullptr, 0, &need16);  | 
313  | 13.4k  |           DrainLastException(addon_env);  | 
314  | 13.4k  |           if (need16 == 0) need16 = 1;  | 
315  | 13.4k  |           char16_t* tmp16 = static_cast<char16_t*>(std::malloc(sizeof(char16_t) * (need16 + 1)));  | 
316  | 13.4k  |           if (tmp16) { | 
317  | 13.4k  |             size_t got16 = 0;  | 
318  | 13.4k  |             (void) napi_get_value_string_utf16(addon_env, ext_u16_val, tmp16, need16 + 1, &got16);  | 
319  | 13.4k  |             DrainLastException(addon_env);  | 
320  | 13.4k  |             std::free(tmp16);  | 
321  | 13.4k  |           }  | 
322  |  |  | 
323  | 13.4k  |           if (copied_ext16) { | 
324  | 0  |             std::free(ext_u16);  | 
325  | 0  |           }  | 
326  | 13.4k  |         }  | 
327  | 13.4k  |       }  | 
328  |  |  | 
329  | 13.4k  |       { | 
330  | 13.4k  |         napi_value coerced{}; | 
331  | 13.4k  |         (void) napi_coerce_to_string(addon_env, output4 /* Symbol.for(...) */, &coerced);  | 
332  | 13.4k  |         (void) napi_coerce_to_string(addon_env, output7 /* property names array */, &coerced);  | 
333  | 13.4k  |         (void) napi_coerce_to_string(addon_env, output9 /* TypeError object */, &coerced);  | 
334  | 13.4k  |         (void) napi_coerce_to_string(addon_env, output12 /* result of run_script */, &coerced);  | 
335  | 13.4k  |         DrainLastException(addon_env);  | 
336  | 13.4k  |       }  | 
337  |  |  | 
338  |  |       // Clean up original temp buffers  | 
339  | 13.4k  |       std::free(buf1);  | 
340  | 13.4k  |       std::free(buf2);  | 
341  |  |  | 
342  |  |       // Final safeguard: ensure no exception is left pending before we return.  | 
343  | 13.4k  |       DrainLastException(addon_env);  | 
344  | 13.4k  |       g_addon_env = nullptr;  // avoid leakage across inputs  | 
345  | 13.4k  |     },  | 
346  | 22.0k  |     /*opts=*/fuzz::EnvRunOptions{ | 
347  | 22.0k  |         node::EnvironmentFlags::kDefaultFlags,  | 
348  | 22.0k  |         /*print_js_to_stdout=*/false,  | 
349  | 22.0k  |         /*max_pumps=*/4  // give microtasks/callbacks a chance, still bounded  | 
350  | 22.0k  |     });  | 
351  |  |  | 
352  | 22.0k  |   return 0;  | 
353  | 22.0k  | }  |