Skip to content

Commit 6575033

Browse files
authored
Reject out-of-range NaN payloads and bare type-index value types (#2563)
* wast: reject out-of-range NaN payloads A NaN literal's payload `n` must satisfy `1 <= n < 2^signif_bits` (52 bits for f64, 23 for f32). The parser masked the payload to the significand width instead of range-checking it, so an over-long payload such as `nan:0xffffffffffffff` (56 bits for f64) was silently truncated and accepted rather than rejected as the reference interpreter does. Range-check the payload against the significand width instead of masking. The spec testsuite only exercised the power-of-two boundary `nan:0x80_0000`, which the old masking rejected by coincidence (it masks to 0, hitting the "payload is zero => infinity" check), so out-of-range payloads that mask to a nonzero value went unnoticed. Regression tests cover both the invalid cases and the adjacent valid boundaries. * wasmparser: reject bare type index as a value type The short form of a reference type (without a `0x63`/`0x64` prefix) is only an abbreviation for `ref null <abstract-heap-type>`. The decoder instead parsed a full heap type there, which additionally accepts a bare non-negative type index, so `0x00` was wrongly decoded as the value type `(ref null 0)`. Parse an abstract heap type directly in the short-form path (handling the `0x65` prefix for `shared` variants) so a bare index is rejected at decode time. This also covers the over-long-LEB variant (`ff 00`). In value-type position the error surfaces as `invalid value type`; in a reference-type-only position (e.g. a table element type) as `malformed reference type`. The legitimate `0x63 <idx>` / `0x64 <idx>` concrete reftypes and the `shared` abstract shorthands (`0x65 ...`) are unaffected.
1 parent c799bb8 commit 6575033

9 files changed

Lines changed: 218 additions & 10 deletions

File tree

crates/wasmparser/src/readers/core/types.rs

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1822,17 +1822,25 @@ impl<'a> FromReader<'a> for RefType {
18221822
}
18231823
0x62 => Err(crate::Error::new("unexpected exact type", pos)),
18241824
_ => {
1825-
// Reclassify errors as invalid reference types here because
1826-
// that's the "root" of what was being parsed rather than
1827-
// heap types.
1828-
let hty = reader.read().map_err(|mut e| {
1825+
// The short form of a reference type (without a `0x63`/`0x64`
1826+
// prefix byte) is only an abbreviation for `ref null <ht>` where
1827+
// `<ht>` is an *abstract* heap type; a bare (non-negative) type
1828+
// index is not a value type. Parse an abstract heap type
1829+
// directly, handling the `0x65` prefix for `shared` variants,
1830+
// rather than parsing a full heap type and then rejecting the
1831+
// concrete/exact cases that a heap type additionally allows.
1832+
let shared = reader.peek()? == 0x65;
1833+
if shared {
1834+
reader.read_u8()?;
1835+
}
1836+
let ty = reader.read().map_err(|mut e| {
18291837
if let ErrorKind::InvalidHeapType = e.kind() {
18301838
e.set_message("malformed reference type");
18311839
}
18321840
e
18331841
})?;
1834-
RefType::new(true, hty)
1835-
.ok_or_else(|| crate::Error::new("type index too large", pos))
1842+
// `RefType::new` is infallible for abstract heap types.
1843+
Ok(RefType::new(true, HeapType::Abstract { shared, ty }).unwrap())
18361844
}
18371845
}
18381846
}

crates/wast/src/token.rs

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -465,15 +465,17 @@ macro_rules! float {
465465
Some(val) => $int::from_str_radix(val,16).ok()?,
466466
None => 1 << (signif_bits - 1),
467467
};
468-
// If the significand is zero then this is actually infinity
469-
// so we fail to parse it.
470-
if signif & signif_mask == 0 {
468+
// The payload `n` must satisfy `1 <= n < 2^signif_bits`. A
469+
// payload of zero would actually be infinity, and a payload
470+
// with any bits set outside the significand is out of range
471+
// rather than something to silently mask away.
472+
if signif == 0 || signif & !signif_mask != 0 {
471473
return None;
472474
}
473475
return Some(
474476
(neg_bit << neg_offset) |
475477
(exp_bits << exp_offset) |
476-
(signif & signif_mask)
478+
signif
477479
);
478480
}
479481

@@ -766,4 +768,37 @@ mod tests {
766768
Some(0x26800000)
767769
);
768770
}
771+
772+
#[test]
773+
fn nan_payload_range() {
774+
fn nan(val: &'static str) -> crate::lexer::Float<'static> {
775+
crate::lexer::Float::Nan {
776+
val: Some(val.into()),
777+
negative: false,
778+
}
779+
}
780+
781+
// f32 has a 23-bit significand, so `0x7fffff` is the maximum valid NaN
782+
// payload; `0x800000` and beyond are out of range and must not be
783+
// silently masked. A zero payload would be infinity, not a NaN.
784+
assert_eq!(super::strtof(&nan("1")), Some(0x7f800001));
785+
assert_eq!(super::strtof(&nan("7fffff")), Some(0x7fffffff));
786+
assert_eq!(super::strtof(&nan("800000")), None);
787+
assert_eq!(super::strtof(&nan("ffffff")), None);
788+
assert_eq!(super::strtof(&nan("0")), None);
789+
790+
// f64 has a 52-bit significand, so `0xf_ffff_ffff_ffff` is the maximum
791+
// valid NaN payload; a 14-hex-digit (56-bit) payload is out of range.
792+
assert_eq!(super::strtod(&nan("1")), Some(0x7ff0000000000001));
793+
assert_eq!(
794+
super::strtod(&nan("8000000000000")),
795+
Some(0x7ff8000000000000)
796+
);
797+
assert_eq!(
798+
super::strtod(&nan("fffffffffffff")),
799+
Some(0x7fffffffffffffff)
800+
);
801+
assert_eq!(super::strtod(&nan("ffffffffffffff")), None);
802+
assert_eq!(super::strtod(&nan("0")), None);
803+
}
769804
}

tests/cli/bad-nan.wast

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,26 @@
1111
(assert_malformed
1212
(module quote "(func (result f32) (f32.const nan:0xffffffff0))")
1313
"constant out of range")
14+
15+
;; The payload `n` of a NaN literal must satisfy `1 <= n < 2^significand`, where
16+
;; the significand is 52 bits for f64 and 23 bits for f32. Payloads that overflow
17+
;; the significand must be rejected rather than silently masked.
18+
19+
;; f64: `0xf_ffff_ffff_ffff` (13 hex digits) is the maximum valid payload; one bit
20+
;; more is out of range.
21+
(module (func (result f64) f64.const nan:0xfffffffffffff))
22+
(assert_malformed
23+
(module quote "(func (result f64) (f64.const nan:0xffffffffffffff))")
24+
"constant out of range")
25+
26+
;; f32: `0x7fffff` (23 bits) is the maximum valid payload; `0x800000` is out of
27+
;; range.
28+
(module (func (result f32) f32.const nan:0x7fffff))
29+
(assert_malformed
30+
(module quote "(func (result f32) (f32.const nan:0x800000))")
31+
"constant out of range")
32+
33+
;; A zero payload is not a valid NaN (it would be an infinity).
34+
(assert_malformed
35+
(module quote "(func (result f64) (f64.const nan:0x0))")
36+
"constant out of range")

tests/cli/bad-reftype.wast

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
;; RUN: wast --assert default --snapshot tests/snapshots %
2+
3+
;; A value type is `numtype | vectype | reftype`, and a reference type is either
4+
;; an abstract heap type shorthand byte or the two-byte `0x64 heaptype` (`ref ht`)
5+
;; / `0x63 heaptype` (`ref null ht`) forms. There is no bare type-index value
6+
;; type, so a leading byte of `0x00` (a non-negative sLEB) is malformed.
7+
8+
;; A bare index used as a value type. In value-type position the error is
9+
;; reclassified as "invalid value type".
10+
(assert_malformed
11+
(module binary
12+
"\00asm" "\01\00\00\00" ;; module header
13+
14+
"\01\08\02" ;; type section, 2 types
15+
"\60\00\00" ;; type 0: (func)
16+
"\60\01" ;; type 1: (func (param ...)), 1 param
17+
"\00" ;; param 0: bare type index 0x00 (invalid value type)
18+
"\00" ;; 0 results
19+
)
20+
"invalid value type")
21+
22+
;; The same bug with an over-long LEB encoding of the bare index (`\ff\00` = 127).
23+
;; The reftype discriminator is a single byte, so this must be rejected at decode
24+
;; time rather than re-interpreted as a type index.
25+
(assert_malformed
26+
(module binary
27+
"\00asm" "\01\00\00\00" ;; module header
28+
29+
"\01\09\02" ;; type section, 2 types
30+
"\60\00\00" ;; type 0: (func)
31+
"\60\01" ;; type 1: (func (param ...)), 1 param
32+
"\ff\00" ;; param 0: bare index 0x00 with an over-long LEB
33+
"\00" ;; 0 results
34+
)
35+
"invalid value type")
36+
37+
;; The same bare index in a reference-type-only position (a table's element type)
38+
;; is reported as a malformed reference type.
39+
(assert_malformed
40+
(module binary
41+
"\00asm" "\01\00\00\00" ;; module header
42+
43+
"\04\04\01" ;; table section, 1 table
44+
"\00" ;; element type: bare index 0x00 (malformed reftype)
45+
"\00\00" ;; limits: no max, minimum 0
46+
)
47+
"malformed reference type")
48+
49+
;; Boundary: the legitimate concrete reftypes `0x63 <idx>` (`ref null $t`) and
50+
;; `0x64 <idx>` (`ref $t`), where the index is an sLEB after the prefix byte, must
51+
;; still be accepted.
52+
(module binary
53+
"\00asm" "\01\00\00\00" ;; module header
54+
55+
"\01\0b\02" ;; type section, 2 types
56+
"\60\00\00" ;; type 0: (func)
57+
"\60\02" ;; type 1: (func (param ...)), 2 params, 0 results
58+
"\63\00" ;; param 0: (ref null 0)
59+
"\64\00" ;; param 1: (ref 0)
60+
"\00" ;; 0 results
61+
)

tests/snapshots/cli/bad-nan.wast.json

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,39 @@
2121
"filename": "bad-nan.2.wat",
2222
"module_type": "text",
2323
"text": "constant out of range"
24+
},
25+
{
26+
"type": "module",
27+
"line": 21,
28+
"filename": "bad-nan.3.wasm",
29+
"module_type": "binary"
30+
},
31+
{
32+
"type": "assert_malformed",
33+
"line": 23,
34+
"filename": "bad-nan.4.wat",
35+
"module_type": "text",
36+
"text": "constant out of range"
37+
},
38+
{
39+
"type": "module",
40+
"line": 28,
41+
"filename": "bad-nan.5.wasm",
42+
"module_type": "binary"
43+
},
44+
{
45+
"type": "assert_malformed",
46+
"line": 30,
47+
"filename": "bad-nan.6.wat",
48+
"module_type": "text",
49+
"text": "constant out of range"
50+
},
51+
{
52+
"type": "assert_malformed",
53+
"line": 35,
54+
"filename": "bad-nan.7.wat",
55+
"module_type": "text",
56+
"text": "constant out of range"
2457
}
2558
]
2659
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
(module
2+
(type (;0;) (func (result f64)))
3+
(func (;0;) (type 0) (result f64)
4+
f64.const nan:0xfffffffffffff (;=NaN;)
5+
)
6+
)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
(module
2+
(type (;0;) (func (result f32)))
3+
(func (;0;) (type 0) (result f32)
4+
f32.const nan:0x7fffff (;=NaN;)
5+
)
6+
)
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"source_filename": "tests/cli/bad-reftype.wast",
3+
"commands": [
4+
{
5+
"type": "assert_malformed",
6+
"line": 11,
7+
"filename": "bad-reftype.0.wasm",
8+
"module_type": "binary",
9+
"text": "invalid value type"
10+
},
11+
{
12+
"type": "assert_malformed",
13+
"line": 26,
14+
"filename": "bad-reftype.1.wasm",
15+
"module_type": "binary",
16+
"text": "invalid value type"
17+
},
18+
{
19+
"type": "assert_malformed",
20+
"line": 40,
21+
"filename": "bad-reftype.2.wasm",
22+
"module_type": "binary",
23+
"text": "malformed reference type"
24+
},
25+
{
26+
"type": "module",
27+
"line": 52,
28+
"filename": "bad-reftype.3.wasm",
29+
"module_type": "binary"
30+
}
31+
]
32+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
(module
2+
(type (;0;) (func))
3+
(type (;1;) (func (param (ref null 0) (ref 0))))
4+
)

0 commit comments

Comments
 (0)