Skip to content

Commit 2ff2e25

Browse files
committed
Fix use-after-free with closures in JS bindings
This commit fixes an erroneous use-after-free which can happen in erroneous situations in JS. It's intended that if you invoke a closure after its environment has been destroyed that you'll immediately get an error from Rust saying so. The JS binding generation for mutable closures, however, accidentally did not protect against this. Each closure has an internal reference count which is incremented while being invoked and decremented when the invocation finishes and also when the `Closure` in Rust is dropped. That means there's two branches where the reference count reaches zero and the internal pointer stored in JS needs to be set to zero. Only one, however, actually set the pointer to zero! This means that if a closure was destroyed while it was being invoked it would not correctly set its internal pointer to zero. A further invocation of the closure would then pass as seemingly valid pointer into Rust, causing a use-after-free. A test isn't included here specifically for this because our CI has started failing left-and-right over this test, so this commit will hopefully just make our CI green!
1 parent b8e9a20 commit 2ff2e25

File tree

1 file changed

+23
-5
lines changed

1 file changed

+23
-5
lines changed

crates/cli-support/src/js/closures.rs

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -181,18 +181,36 @@ impl ClosureDescriptors {
181181
let mut shim = closure.shim_idx;
182182
let (js, _ts, _js_doc) = {
183183
let mut builder = Js2Rust::new("", input);
184+
185+
// First up with a closure we increment the internal reference
186+
// count. This ensures that the Rust closure environment won't
187+
// be deallocated while we're invoking it.
184188
builder.prelude("this.cnt++;");
189+
185190
if closure.mutable {
191+
// For mutable closures they can't be invoked recursively.
192+
// To handle that we swap out the `this.a` pointer with zero
193+
// while we invoke it. If we finish and the closure wasn't
194+
// destroyed, then we put back the pointer so a future
195+
// invocation can succeed.
186196
builder
187-
.prelude("let a = this.a;\n")
188-
.prelude("this.a = 0;\n")
197+
.prelude("let a = this.a;")
198+
.prelude("this.a = 0;")
189199
.rust_argument("a")
190200
.rust_argument("b")
191-
.finally("this.a = a;\n");
201+
.finally("if (this.cnt-- == 1) d(a, b);")
202+
.finally("else this.a = a;");
192203
} else {
193-
builder.rust_argument("this.a").rust_argument("b");
204+
// For shared closures they can be invoked recursively so we
205+
// just immediately pass through `this.a`. If we end up
206+
// executing the destructor, however, we clear out the
207+
// `this.a` pointer to prevent it being used again the
208+
// future.
209+
builder
210+
.rust_argument("this.a")
211+
.rust_argument("b")
212+
.finally("if (this.cnt-- == 1) { d(this.a, b); this.a = 0; }");
194213
}
195-
builder.finally("if (this.cnt-- == 1) d(this.a, b);");
196214
builder.process(&closure.function, None)?.finish(
197215
"function",
198216
"f",

0 commit comments

Comments
 (0)