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