diff --git a/crates/wasmparser/src/readers/core/types.rs b/crates/wasmparser/src/readers/core/types.rs index 9079c7701b..a935d4e54b 100644 --- a/crates/wasmparser/src/readers/core/types.rs +++ b/crates/wasmparser/src/readers/core/types.rs @@ -1822,17 +1822,25 @@ impl<'a> FromReader<'a> for RefType { } 0x62 => Err(crate::Error::new("unexpected exact type", pos)), _ => { - // Reclassify errors as invalid reference types here because - // that's the "root" of what was being parsed rather than - // heap types. - let hty = reader.read().map_err(|mut e| { + // The short form of a reference type (without a `0x63`/`0x64` + // prefix byte) is only an abbreviation for `ref null ` where + // `` is an *abstract* heap type; a bare (non-negative) type + // index is not a value type. Parse an abstract heap type + // directly, handling the `0x65` prefix for `shared` variants, + // rather than parsing a full heap type and then rejecting the + // concrete/exact cases that a heap type additionally allows. + let shared = reader.peek()? == 0x65; + if shared { + reader.read_u8()?; + } + let ty = reader.read().map_err(|mut e| { if let ErrorKind::InvalidHeapType = e.kind() { e.set_message("malformed reference type"); } e })?; - RefType::new(true, hty) - .ok_or_else(|| crate::Error::new("type index too large", pos)) + // `RefType::new` is infallible for abstract heap types. + Ok(RefType::new(true, HeapType::Abstract { shared, ty }).unwrap()) } } } diff --git a/crates/wast/src/token.rs b/crates/wast/src/token.rs index be6c6ac658..097df77d17 100644 --- a/crates/wast/src/token.rs +++ b/crates/wast/src/token.rs @@ -465,15 +465,17 @@ macro_rules! float { Some(val) => $int::from_str_radix(val,16).ok()?, None => 1 << (signif_bits - 1), }; - // If the significand is zero then this is actually infinity - // so we fail to parse it. - if signif & signif_mask == 0 { + // The payload `n` must satisfy `1 <= n < 2^signif_bits`. A + // payload of zero would actually be infinity, and a payload + // with any bits set outside the significand is out of range + // rather than something to silently mask away. + if signif == 0 || signif & !signif_mask != 0 { return None; } return Some( (neg_bit << neg_offset) | (exp_bits << exp_offset) | - (signif & signif_mask) + signif ); } @@ -766,4 +768,37 @@ mod tests { Some(0x26800000) ); } + + #[test] + fn nan_payload_range() { + fn nan(val: &'static str) -> crate::lexer::Float<'static> { + crate::lexer::Float::Nan { + val: Some(val.into()), + negative: false, + } + } + + // f32 has a 23-bit significand, so `0x7fffff` is the maximum valid NaN + // payload; `0x800000` and beyond are out of range and must not be + // silently masked. A zero payload would be infinity, not a NaN. + assert_eq!(super::strtof(&nan("1")), Some(0x7f800001)); + assert_eq!(super::strtof(&nan("7fffff")), Some(0x7fffffff)); + assert_eq!(super::strtof(&nan("800000")), None); + assert_eq!(super::strtof(&nan("ffffff")), None); + assert_eq!(super::strtof(&nan("0")), None); + + // f64 has a 52-bit significand, so `0xf_ffff_ffff_ffff` is the maximum + // valid NaN payload; a 14-hex-digit (56-bit) payload is out of range. + assert_eq!(super::strtod(&nan("1")), Some(0x7ff0000000000001)); + assert_eq!( + super::strtod(&nan("8000000000000")), + Some(0x7ff8000000000000) + ); + assert_eq!( + super::strtod(&nan("fffffffffffff")), + Some(0x7fffffffffffffff) + ); + assert_eq!(super::strtod(&nan("ffffffffffffff")), None); + assert_eq!(super::strtod(&nan("0")), None); + } } diff --git a/tests/cli/bad-nan.wast b/tests/cli/bad-nan.wast index 2bb5233ebf..796e48918b 100644 --- a/tests/cli/bad-nan.wast +++ b/tests/cli/bad-nan.wast @@ -11,3 +11,26 @@ (assert_malformed (module quote "(func (result f32) (f32.const nan:0xffffffff0))") "constant out of range") + +;; The payload `n` of a NaN literal must satisfy `1 <= n < 2^significand`, where +;; the significand is 52 bits for f64 and 23 bits for f32. Payloads that overflow +;; the significand must be rejected rather than silently masked. + +;; f64: `0xf_ffff_ffff_ffff` (13 hex digits) is the maximum valid payload; one bit +;; more is out of range. +(module (func (result f64) f64.const nan:0xfffffffffffff)) +(assert_malformed + (module quote "(func (result f64) (f64.const nan:0xffffffffffffff))") + "constant out of range") + +;; f32: `0x7fffff` (23 bits) is the maximum valid payload; `0x800000` is out of +;; range. +(module (func (result f32) f32.const nan:0x7fffff)) +(assert_malformed + (module quote "(func (result f32) (f32.const nan:0x800000))") + "constant out of range") + +;; A zero payload is not a valid NaN (it would be an infinity). +(assert_malformed + (module quote "(func (result f64) (f64.const nan:0x0))") + "constant out of range") diff --git a/tests/cli/bad-reftype.wast b/tests/cli/bad-reftype.wast new file mode 100644 index 0000000000..3b2b6d7bdb --- /dev/null +++ b/tests/cli/bad-reftype.wast @@ -0,0 +1,61 @@ +;; RUN: wast --assert default --snapshot tests/snapshots % + +;; A value type is `numtype | vectype | reftype`, and a reference type is either +;; an abstract heap type shorthand byte or the two-byte `0x64 heaptype` (`ref ht`) +;; / `0x63 heaptype` (`ref null ht`) forms. There is no bare type-index value +;; type, so a leading byte of `0x00` (a non-negative sLEB) is malformed. + +;; A bare index used as a value type. In value-type position the error is +;; reclassified as "invalid value type". +(assert_malformed + (module binary + "\00asm" "\01\00\00\00" ;; module header + + "\01\08\02" ;; type section, 2 types + "\60\00\00" ;; type 0: (func) + "\60\01" ;; type 1: (func (param ...)), 1 param + "\00" ;; param 0: bare type index 0x00 (invalid value type) + "\00" ;; 0 results + ) + "invalid value type") + +;; The same bug with an over-long LEB encoding of the bare index (`\ff\00` = 127). +;; The reftype discriminator is a single byte, so this must be rejected at decode +;; time rather than re-interpreted as a type index. +(assert_malformed + (module binary + "\00asm" "\01\00\00\00" ;; module header + + "\01\09\02" ;; type section, 2 types + "\60\00\00" ;; type 0: (func) + "\60\01" ;; type 1: (func (param ...)), 1 param + "\ff\00" ;; param 0: bare index 0x00 with an over-long LEB + "\00" ;; 0 results + ) + "invalid value type") + +;; The same bare index in a reference-type-only position (a table's element type) +;; is reported as a malformed reference type. +(assert_malformed + (module binary + "\00asm" "\01\00\00\00" ;; module header + + "\04\04\01" ;; table section, 1 table + "\00" ;; element type: bare index 0x00 (malformed reftype) + "\00\00" ;; limits: no max, minimum 0 + ) + "malformed reference type") + +;; Boundary: the legitimate concrete reftypes `0x63 ` (`ref null $t`) and +;; `0x64 ` (`ref $t`), where the index is an sLEB after the prefix byte, must +;; still be accepted. +(module binary + "\00asm" "\01\00\00\00" ;; module header + + "\01\0b\02" ;; type section, 2 types + "\60\00\00" ;; type 0: (func) + "\60\02" ;; type 1: (func (param ...)), 2 params, 0 results + "\63\00" ;; param 0: (ref null 0) + "\64\00" ;; param 1: (ref 0) + "\00" ;; 0 results +) diff --git a/tests/snapshots/cli/bad-nan.wast.json b/tests/snapshots/cli/bad-nan.wast.json index 8de7d96047..349f31098d 100644 --- a/tests/snapshots/cli/bad-nan.wast.json +++ b/tests/snapshots/cli/bad-nan.wast.json @@ -21,6 +21,39 @@ "filename": "bad-nan.2.wat", "module_type": "text", "text": "constant out of range" + }, + { + "type": "module", + "line": 21, + "filename": "bad-nan.3.wasm", + "module_type": "binary" + }, + { + "type": "assert_malformed", + "line": 23, + "filename": "bad-nan.4.wat", + "module_type": "text", + "text": "constant out of range" + }, + { + "type": "module", + "line": 28, + "filename": "bad-nan.5.wasm", + "module_type": "binary" + }, + { + "type": "assert_malformed", + "line": 30, + "filename": "bad-nan.6.wat", + "module_type": "text", + "text": "constant out of range" + }, + { + "type": "assert_malformed", + "line": 35, + "filename": "bad-nan.7.wat", + "module_type": "text", + "text": "constant out of range" } ] } \ No newline at end of file diff --git a/tests/snapshots/cli/bad-nan.wast/3.print b/tests/snapshots/cli/bad-nan.wast/3.print new file mode 100644 index 0000000000..e1cdd5c403 --- /dev/null +++ b/tests/snapshots/cli/bad-nan.wast/3.print @@ -0,0 +1,6 @@ +(module + (type (;0;) (func (result f64))) + (func (;0;) (type 0) (result f64) + f64.const nan:0xfffffffffffff (;=NaN;) + ) +) diff --git a/tests/snapshots/cli/bad-nan.wast/5.print b/tests/snapshots/cli/bad-nan.wast/5.print new file mode 100644 index 0000000000..80f3789d42 --- /dev/null +++ b/tests/snapshots/cli/bad-nan.wast/5.print @@ -0,0 +1,6 @@ +(module + (type (;0;) (func (result f32))) + (func (;0;) (type 0) (result f32) + f32.const nan:0x7fffff (;=NaN;) + ) +) diff --git a/tests/snapshots/cli/bad-reftype.wast.json b/tests/snapshots/cli/bad-reftype.wast.json new file mode 100644 index 0000000000..f6d7f5066a --- /dev/null +++ b/tests/snapshots/cli/bad-reftype.wast.json @@ -0,0 +1,32 @@ +{ + "source_filename": "tests/cli/bad-reftype.wast", + "commands": [ + { + "type": "assert_malformed", + "line": 11, + "filename": "bad-reftype.0.wasm", + "module_type": "binary", + "text": "invalid value type" + }, + { + "type": "assert_malformed", + "line": 26, + "filename": "bad-reftype.1.wasm", + "module_type": "binary", + "text": "invalid value type" + }, + { + "type": "assert_malformed", + "line": 40, + "filename": "bad-reftype.2.wasm", + "module_type": "binary", + "text": "malformed reference type" + }, + { + "type": "module", + "line": 52, + "filename": "bad-reftype.3.wasm", + "module_type": "binary" + } + ] +} \ No newline at end of file diff --git a/tests/snapshots/cli/bad-reftype.wast/3.print b/tests/snapshots/cli/bad-reftype.wast/3.print new file mode 100644 index 0000000000..e67abd1b5f --- /dev/null +++ b/tests/snapshots/cli/bad-reftype.wast/3.print @@ -0,0 +1,4 @@ +(module + (type (;0;) (func)) + (type (;1;) (func (param (ref null 0) (ref 0)))) +)