1 use super::REALLOC_AND_FREE;
2 use anyhow::Result;
3 use wasmtime::component::{Component, Linker};
4 use wasmtime::{Engine, Store, StoreContextMut, Trap, TrapCode};
5 
6 const UTF16_TAG: u32 = 1 << 31;
7 
8 // Special cases that this tries to test:
9 //
10 // * utf8 -> utf8
11 //    * various code point sizes
12 //
13 // * utf8 -> utf16 - the adapter here will make a pessimistic allocation that's
14 //   twice the size of the utf8 encoding for the utf16 destination
15 //    * utf16 byte size is twice the utf8 size
16 //    * utf16 byte size is less than twice the utf8 size
17 //
18 // * utf8 -> latin1+utf16 - attempts to convert to latin1 then falls back to a
19 //   pessimistic utf16 allocation that's downsized if necessary
20 //    * utf8 fits exactly in latin1
21 //    * utf8 fits latin1 but is bigger byte-wise
22 //    * utf8 is not latin1 and fits utf16 allocation precisely (NOT POSSIBLE)
23 //    * utf8 is not latin1 and utf16 is smaller than allocation
24 //
25 // * utf16 -> utf8 - this starts with an optimistic size and then reallocates to
26 //   a pessimistic size, interesting cases are:
27 //    * utf8 size is 0.5x the utf16 byte size (perfect fit in initial alloc)
28 //    * utf8 size is 1.5x the utf16 byte size (perfect fit in larger alloc)
29 //    * utf8 size is 0.5x-1.5x the utf16 size (larger alloc is downsized)
30 //
31 // * utf16 -> utf16
32 //    * various code point sizes
33 //
34 // * utf16 -> latin1+utf16 - attempts to convert to latin1 then falls back to a
35 //   pessimistic utf16 allocation that's downsized if necessary
36 //    * utf16 fits exactly in latin1
37 //    * utf16 fits latin1 but is bigger byte-wise (NOT POSSIBLE)
38 //    * utf16 is not latin1 and fits utf16 allocation precisely
39 //    * utf16 is not latin1 and utf16 is smaller than allocation (NOT POSSIBLE)
40 //
41 // * compact-utf16 -> utf8 dynamically determines between one of
42 //    * latin1 -> utf8
43 //      * latin1 size matches utf8 size
44 //      * latin1 is smaller than utf8 size
45 //    * utf16 -> utf8
46 //      * covered above
47 //
48 // * compact-utf16 -> utf16 dynamically determines between one of
49 //    * latin1 -> utf16 - latin1 size always matches utf16
50 //      * test various code points
51 //    * utf16 -> utf16
52 //      * covered above
53 //
54 // * compact-utf16 -> compact-utf16 dynamically determines between one of
55 //    * latin1 -> latin1
56 //      * not much interesting here
57 //    * utf16 -> compact-utf16-to-compact-probably-utf16
58 //      * utf16 actually fits within latin1
59 //      * otherwise not more interesting than utf16 -> utf16
60 //
61 const STRINGS: &[&str] = &[
62     "",
63     // 1 byte in utf8, 2 bytes in utf16
64     "x",
65     "hello this is a particularly long string yes it is it keeps going",
66     // 35 bytes in utf8, 23 units in utf16, 23 bytes in latin1
67     "à á â ã ä å æ ç è é ê ë",
68     // 47 bytes in utf8, 31 units in utf16
69     "Ξ Ο Π Ρ Σ Τ Υ Φ Χ Ψ Ω Ϊ Ϋ ά έ ή",
70     // 24 bytes in utf8, 8 units in utf16
71     "STUVWXYZ",
72     // 16 bytes in utf8, 8 units in utf16
73     "ËÌÍÎÏÐÑÒ",
74     // 4 bytes in utf8, 1 unit in utf16
75     "\u{10000}",
76     // latin1-compatible prefix followed by utf8/16-requiring suffix
77     //
78     // 24 bytes in utf8, 13 units in utf16, first 8 usvs are latin1-compatible
79     "à ascii VWXYZ",
80 ];
81 
82 static ENCODINGS: [&str; 3] = ["utf8", "utf16", "latin1+utf16"];
83 
84 #[test]
85 fn roundtrip() -> Result<()> {
86     for debug in [true, false] {
87         let mut config = component_test_util::config();
88         config.debug_adapter_modules(debug);
89         let engine = Engine::new(&config)?;
90         for src in ENCODINGS {
91             for dst in ENCODINGS {
92                 test_roundtrip(&engine, src, dst)?;
93             }
94         }
95     }
96     Ok(())
97 }
98 
99 fn test_roundtrip(engine: &Engine, src: &str, dst: &str) -> Result<()> {
100     println!("src={src} dst={dst}");
101 
102     let mk_echo = |name: &str, encoding: &str| {
103         format!(
104             r#"
105 (component {name}
106     (import "echo" (func $echo (param string) (result string)))
107     (core instance $libc (instantiate $libc))
108     (core func $echo (canon lower (func $echo)
109         (memory $libc "memory")
110         (realloc (func $libc "realloc"))
111         string-encoding={encoding}
112     ))
113     (core instance $echo (instantiate $echo
114         (with "libc" (instance $libc))
115         (with "" (instance (export "echo" (func $echo))))
116     ))
117     (func (export "echo") (param string) (result string)
118         (canon lift
119             (core func $echo "echo")
120             (memory $libc "memory")
121             (realloc (func $libc "realloc"))
122             string-encoding={encoding}
123         )
124     )
125 )
126             "#
127         )
128     };
129 
130     let src = mk_echo("$src", src);
131     let dst = mk_echo("$dst", dst);
132     let component = format!(
133         r#"
134 (component
135     (import "host" (func $host (param string) (result string)))
136 
137     (core module $libc
138         (memory (export "memory") 1)
139         {REALLOC_AND_FREE}
140     )
141     (core module $echo
142         (import "" "echo" (func $echo (param i32 i32 i32)))
143         (import "libc" "memory" (memory 0))
144         (import "libc" "realloc" (func $realloc (param i32 i32 i32 i32) (result i32)))
145 
146         (func (export "echo") (param i32 i32) (result i32)
147             (local $retptr i32)
148             (local.set $retptr
149                 (call $realloc
150                     (i32.const 0)
151                     (i32.const 0)
152                     (i32.const 4)
153                     (i32.const 8)))
154             (call $echo
155                 (local.get 0)
156                 (local.get 1)
157                 (local.get $retptr))
158             local.get $retptr
159         )
160     )
161 
162     {src}
163     {dst}
164 
165     (instance $dst (instantiate $dst (with "echo" (func $host))))
166     (instance $src (instantiate $src (with "echo" (func $dst "echo"))))
167     (export "echo" (func $src "echo"))
168 )
169 "#
170     );
171     let component = Component::new(engine, &component)?;
172     let mut store = Store::new(engine, String::new());
173     let mut linker = Linker::new(engine);
174     linker
175         .root()
176         .func_wrap("host", |store: StoreContextMut<String>, arg: String| {
177             assert_eq!(*store.data(), arg);
178             Ok(arg)
179         })?;
180     let instance = linker.instantiate(&mut store, &component)?;
181     let func = instance.get_typed_func::<(String,), String, _>(&mut store, "echo")?;
182 
183     for string in STRINGS {
184         println!("testing string {string:?}");
185         *store.data_mut() = string.to_string();
186         let ret = func.call(&mut store, (string.to_string(),))?;
187         assert_eq!(ret, *string);
188         func.post_return(&mut store)?;
189     }
190     Ok(())
191 }
192 
193 #[test]
194 fn ptr_out_of_bounds() -> Result<()> {
195     let engine = component_test_util::engine();
196     for src in ENCODINGS {
197         for dst in ENCODINGS {
198             test_ptr_out_of_bounds(&engine, src, dst)?;
199         }
200     }
201     Ok(())
202 }
203 
204 fn test_ptr_out_of_bounds(engine: &Engine, src: &str, dst: &str) -> Result<()> {
205     let test = |len: u32| -> Result<()> {
206         let component = format!(
207             r#"
208 (component
209   (component $c
210     (core module $m
211       (func (export "") (param i32 i32))
212       (func (export "realloc") (param i32 i32 i32 i32) (result i32) i32.const 0)
213       (memory (export "memory") 1)
214     )
215     (core instance $m (instantiate $m))
216     (func (export "") (param string)
217       (canon lift (core func $m "") (realloc (func $m "realloc")) (memory $m "memory")
218         string-encoding={dst})
219     )
220   )
221 
222   (component $c2
223     (import "" (func $f (param string)))
224     (core module $libc
225       (memory (export "memory") 1)
226     )
227     (core instance $libc (instantiate $libc))
228     (core func $f (canon lower (func $f) string-encoding={src} (memory $libc "memory")))
229     (core module $m
230       (import "" "" (func $f (param i32 i32)))
231 
232       (func $start (call $f (i32.const 0x8000_0000) (i32.const {len})))
233       (start $start)
234     )
235     (core instance (instantiate $m (with "" (instance (export "" (func $f))))))
236   )
237 
238   (instance $c (instantiate $c))
239   (instance $c2 (instantiate $c2 (with "" (func $c ""))))
240 )
241 "#
242         );
243         let component = Component::new(engine, &component)?;
244         let mut store = Store::new(engine, ());
245         let trap = Linker::new(engine)
246             .instantiate(&mut store, &component)
247             .err()
248             .unwrap()
249             .downcast::<Trap>()?;
250         assert_eq!(trap.trap_code(), Some(TrapCode::UnreachableCodeReached));
251         Ok(())
252     };
253 
254     test(0)?;
255     test(1)?;
256 
257     Ok(())
258 }
259 
260 // Test that even if the ptr+len calculation overflows then a trap still
261 // happens.
262 #[test]
263 fn ptr_overflow() -> Result<()> {
264     let engine = component_test_util::engine();
265     for src in ENCODINGS {
266         for dst in ENCODINGS {
267             test_ptr_overflow(&engine, src, dst)?;
268         }
269     }
270     Ok(())
271 }
272 
273 fn test_ptr_overflow(engine: &Engine, src: &str, dst: &str) -> Result<()> {
274     let component = format!(
275         r#"
276 (component
277   (component $c
278     (core module $m
279       (func (export "") (param i32 i32))
280       (func (export "realloc") (param i32 i32 i32 i32) (result i32) i32.const 0)
281       (memory (export "memory") 1)
282     )
283     (core instance $m (instantiate $m))
284     (func (export "") (param string)
285       (canon lift (core func $m "") (realloc (func $m "realloc")) (memory $m "memory")
286         string-encoding={dst})
287     )
288   )
289 
290   (component $c2
291     (import "" (func $f (param string)))
292     (core module $libc
293       (memory (export "memory") 1)
294     )
295     (core instance $libc (instantiate $libc))
296     (core func $f (canon lower (func $f) string-encoding={src} (memory $libc "memory")))
297     (core module $m
298       (import "" "" (func $f (param i32 i32)))
299 
300       (func (export "f") (param i32) (call $f (i32.const 1000) (local.get 0)))
301     )
302     (core instance $m (instantiate $m (with "" (instance (export "" (func $f))))))
303     (func (export "f") (param u32) (canon lift (core func $m "f")))
304   )
305 
306   (instance $c (instantiate $c))
307   (instance $c2 (instantiate $c2 (with "" (func $c ""))))
308   (export "f" (func $c2 "f"))
309 )
310 "#
311     );
312 
313     let component = Component::new(engine, &component)?;
314     let mut store = Store::new(engine, ());
315 
316     let mut test_overflow = |size: u32| -> Result<()> {
317         println!("src={src} dst={dst} size={size:#x}");
318         let instance = Linker::new(engine).instantiate(&mut store, &component)?;
319         let func = instance.get_typed_func::<(u32,), (), _>(&mut store, "f")?;
320         let trap = func
321             .call(&mut store, (size,))
322             .unwrap_err()
323             .downcast::<Trap>()?;
324         assert_eq!(trap.trap_code(), Some(TrapCode::UnreachableCodeReached));
325         Ok(())
326     };
327 
328     let max = 1 << 31;
329 
330     match src {
331         "utf8" => {
332             // This exceeds MAX_STRING_BYTE_LENGTH
333             test_overflow(max)?;
334 
335             if dst == "utf16" {
336                 // exceeds MAX_STRING_BYTE_LENGTH when multiplied
337                 test_overflow(max / 2)?;
338 
339                 // Technically this fails on the first string, not the second.
340                 // Ideally this would test the overflow check on the second
341                 // string though.
342                 test_overflow(max / 2 - 100)?;
343             } else {
344                 // This will point into unmapped memory
345                 test_overflow(max - 100)?;
346             }
347         }
348 
349         "utf16" => {
350             test_overflow(max / 2)?;
351             test_overflow(max / 2 - 100)?;
352         }
353 
354         "latin1+utf16" => {
355             test_overflow((max / 2) | UTF16_TAG)?;
356             // tag a utf16 string with the max length and it should overflow.
357             test_overflow((max / 2 - 100) | UTF16_TAG)?;
358         }
359 
360         _ => unreachable!(),
361     }
362 
363     Ok(())
364 }
365 
366 // Test that that the pointer returned from `realloc` is bounds-checked.
367 #[test]
368 fn realloc_oob() -> Result<()> {
369     let engine = component_test_util::engine();
370     for src in ENCODINGS {
371         for dst in ENCODINGS {
372             test_realloc_oob(&engine, src, dst)?;
373         }
374     }
375     Ok(())
376 }
377 
378 fn test_realloc_oob(engine: &Engine, src: &str, dst: &str) -> Result<()> {
379     let component = format!(
380         r#"
381 (component
382   (component $c
383     (core module $m
384       (func (export "") (param i32 i32))
385       (func (export "realloc") (param i32 i32 i32 i32) (result i32) i32.const 100_000)
386       (memory (export "memory") 1)
387     )
388     (core instance $m (instantiate $m))
389     (func (export "") (param string)
390       (canon lift (core func $m "") (realloc (func $m "realloc")) (memory $m "memory")
391         string-encoding={dst})
392     )
393   )
394 
395   (component $c2
396     (import "" (func $f (param string)))
397     (core module $libc
398       (memory (export "memory") 1)
399     )
400     (core instance $libc (instantiate $libc))
401     (core func $f (canon lower (func $f) string-encoding={src} (memory $libc "memory")))
402     (core module $m
403       (import "" "" (func $f (param i32 i32)))
404 
405       (func (export "f") (call $f (i32.const 1000) (i32.const 10)))
406     )
407     (core instance $m (instantiate $m (with "" (instance (export "" (func $f))))))
408     (func (export "f") (canon lift (core func $m "f")))
409   )
410 
411   (instance $c (instantiate $c))
412   (instance $c2 (instantiate $c2 (with "" (func $c ""))))
413   (export "f" (func $c2 "f"))
414 )
415 "#
416     );
417 
418     let component = Component::new(engine, &component)?;
419     let mut store = Store::new(engine, ());
420 
421     let instance = Linker::new(engine).instantiate(&mut store, &component)?;
422     let func = instance.get_typed_func::<(), (), _>(&mut store, "f")?;
423     let trap = func.call(&mut store, ()).unwrap_err().downcast::<Trap>()?;
424     assert_eq!(trap.trap_code(), Some(TrapCode::UnreachableCodeReached));
425     Ok(())
426 }
427 
428 // Test that that the pointer returned from `realloc` is bounds-checked.
429 #[test]
430 fn raw_string_encodings() -> Result<()> {
431     let engine = component_test_util::engine();
432     test_invalid_string_encoding(&engine, "utf8", "utf8", &[0xff], 1)?;
433     let array = b"valid string until \xffthen valid again";
434     test_invalid_string_encoding(&engine, "utf8", "utf8", array, array.len() as u32)?;
435     test_invalid_string_encoding(&engine, "utf8", "utf16", array, array.len() as u32)?;
436     let array = b"symbol \xce\xa3 until \xffthen valid";
437     test_invalid_string_encoding(&engine, "utf8", "utf8", array, array.len() as u32)?;
438     test_invalid_string_encoding(&engine, "utf8", "utf16", array, array.len() as u32)?;
439     test_invalid_string_encoding(&engine, "utf8", "latin1+utf16", array, array.len() as u32)?;
440     test_invalid_string_encoding(&engine, "utf16", "utf8", &[0x01, 0xd8], 1)?;
441     test_invalid_string_encoding(&engine, "utf16", "utf16", &[0x01, 0xd8], 1)?;
442     test_invalid_string_encoding(
443         &engine,
444         "utf16",
445         "latin1+utf16",
446         &[0xff, 0xff, 0x01, 0xd8],
447         2,
448     )?;
449     test_invalid_string_encoding(
450         &engine,
451         "latin1+utf16",
452         "utf8",
453         &[0x01, 0xd8],
454         1 | UTF16_TAG,
455     )?;
456     test_invalid_string_encoding(
457         &engine,
458         "latin1+utf16",
459         "utf16",
460         &[0x01, 0xd8],
461         1 | UTF16_TAG,
462     )?;
463     test_invalid_string_encoding(
464         &engine,
465         "latin1+utf16",
466         "utf16",
467         &[0xff, 0xff, 0x01, 0xd8],
468         2 | UTF16_TAG,
469     )?;
470     test_invalid_string_encoding(
471         &engine,
472         "latin1+utf16",
473         "latin1+utf16",
474         &[0xab, 0x00, 0xff, 0xff, 0x01, 0xd8],
475         3 | UTF16_TAG,
476     )?;
477 
478     // This latin1+utf16 string should get compressed to latin1 across the
479     // boundary.
480     test_valid_string_encoding(
481         &engine,
482         "latin1+utf16",
483         "latin1+utf16",
484         &[0xab, 0x00, 0xff, 0x00],
485         2 | UTF16_TAG,
486     )?;
487     Ok(())
488 }
489 
490 fn test_invalid_string_encoding(
491     engine: &Engine,
492     src: &str,
493     dst: &str,
494     bytes: &[u8],
495     len: u32,
496 ) -> Result<()> {
497     let trap = test_raw_when_encoded(engine, src, dst, bytes, len)?.unwrap();
498     let src = src.replace("latin1+", "");
499     assert!(
500         trap.to_string()
501             .contains(&format!("invalid {src} encoding")),
502         "bad error: {}",
503         trap,
504     );
505     Ok(())
506 }
507 
508 fn test_valid_string_encoding(
509     engine: &Engine,
510     src: &str,
511     dst: &str,
512     bytes: &[u8],
513     len: u32,
514 ) -> Result<()> {
515     let err = test_raw_when_encoded(engine, src, dst, bytes, len)?;
516     assert!(err.is_none());
517     Ok(())
518 }
519 
520 fn test_raw_when_encoded(
521     engine: &Engine,
522     src: &str,
523     dst: &str,
524     bytes: &[u8],
525     len: u32,
526 ) -> Result<Option<Trap>> {
527     let component = format!(
528         r#"
529 (component
530   (component $c
531     (core module $m
532       (func (export "") (param i32 i32))
533       (func (export "realloc") (param i32 i32 i32 i32) (result i32) i32.const 0)
534       (memory (export "memory") 1)
535     )
536     (core instance $m (instantiate $m))
537     (func (export "") (param string)
538       (canon lift (core func $m "") (realloc (func $m "realloc")) (memory $m "memory")
539         string-encoding={dst})
540     )
541   )
542 
543   (component $c2
544     (import "" (func $f (param string)))
545     (core module $libc
546       (memory (export "memory") 1)
547       (func (export "realloc") (param i32 i32 i32 i32) (result i32) i32.const 0)
548     )
549     (core instance $libc (instantiate $libc))
550     (core func $f (canon lower (func $f) string-encoding={src} (memory $libc "memory")))
551     (core module $m
552       (import "" "" (func $f (param i32 i32)))
553 
554       (func (export "f") (param i32 i32 i32) (call $f (local.get 0) (local.get 2)))
555     )
556     (core instance $m (instantiate $m (with "" (instance (export "" (func $f))))))
557     (func (export "f") (param (list u8)) (param u32) (canon lift (core func $m "f")
558         (memory $libc "memory")
559         (realloc (func $libc "realloc"))))
560   )
561 
562   (instance $c (instantiate $c))
563   (instance $c2 (instantiate $c2 (with "" (func $c ""))))
564   (export "f" (func $c2 "f"))
565 )
566 "#
567     );
568 
569     let component = Component::new(engine, &component)?;
570     let mut store = Store::new(engine, ());
571 
572     let instance = Linker::new(engine).instantiate(&mut store, &component)?;
573     let func = instance.get_typed_func::<(&[u8], u32), (), _>(&mut store, "f")?;
574     match func.call(&mut store, (bytes, len)) {
575         Ok(_) => Ok(None),
576         Err(e) => Ok(Some(e.downcast()?)),
577     }
578 }
579