Skip to content

Commit 0acfa22

Browse files
committed
fix(android): use-after-free in RAF choreographer frame callback
AChoreographer_postFrameCallback cannot be cancelled, but clear_callback() freed the Box handed to the choreographer while a frame callback was still queued. The next vsync then fired with a dangling pointer, corrupting whatever had reused the allocation (often the V8 heap), crashing either inside libcanvasnative.so during dispatchVsync or later in unrelated V8 code (HandleScope::Extend, StubCache::Set). Every canvas teardown queued the UAF; framework HMR reboots made it reliably fatal. A stop()/start() restart could also spawn a duplicate callback chain whose stale sibling would free the new chain's allocation via lock.data.take(). Make each posted frame callback the sole owner of its allocation: on every fire it either re-posts the same FrameChain box or drops it, and no other code path frees it. A generation counter retires stale chains after a restart, and clear_callback() now only detaches the JS-side closure, which still guarantees the callback target is never invoked after release.
1 parent 02fa830 commit 0acfa22

1 file changed

Lines changed: 75 additions & 54 deletions

File tree

crates/canvas-c/src/raf/android.rs

Lines changed: 75 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ struct RafInner {
1616
callback: RafCallback,
1717
use_deprecated: bool,
1818
is_prepared: bool,
19-
data: Option<*mut c_void>,
19+
// Incremented on every start(). Pending frame callbacks belonging to an
20+
// older generation drop themselves on their next fire instead of
21+
// re-posting, so a stop()/start() pair can never spawn duplicate chains.
22+
generation: u64,
2023
thread_id: Option<std::thread::ThreadId>,
2124
}
2225

@@ -26,57 +29,63 @@ pub struct Raf(
2629
Arc<parking_lot::Condvar>,
2730
);
2831

32+
// Payload handed to AChoreographer_postFrameCallback. A posted frame callback
33+
// cannot be cancelled, so the callback itself is the SOLE owner of this
34+
// allocation: on each fire it either re-posts the same box for the next frame
35+
// or drops it. No other code path may free it — freeing it from stop() or
36+
// clear_callback() while a callback is still queued is a guaranteed
37+
// use-after-free at the next vsync.
38+
struct FrameChain {
39+
raf: Raf,
40+
generation: u64,
41+
}
42+
2943
impl Raf {
3044
extern "C" fn callback(frame_time_nanos: c_long, data: *mut std::os::raw::c_void) {
3145
if data.is_null() {
3246
return;
3347
}
3448

35-
let data_ptr = data;
36-
let raf_ptr = data as *mut Raf;
37-
let raf_ref = unsafe { &*raf_ptr };
49+
// Take ownership for this invocation; re-posted below if still live.
50+
let chain = unsafe { Box::from_raw(data as *mut FrameChain) };
51+
let raf = &chain.raf;
3852

3953
{
40-
let mut lock = raf_ref.0.lock();
41-
if !lock.started {
42-
let stored = lock.data.take();
43-
drop(lock);
44-
if let Some(p) = stored {
45-
let _ = unsafe { Box::from_raw(p as *mut Raf) };
46-
}
54+
let lock = raf.0.lock();
55+
if !lock.started || lock.generation != chain.generation {
56+
// Chain is dead (stopped, or superseded by a newer start()).
57+
// Dropping `chain` releases the allocation.
4758
return;
4859
}
49-
raf_ref.1.fetch_add(1, Ordering::SeqCst);
60+
raf.1.fetch_add(1, Ordering::SeqCst);
5061
}
5162

5263
{
53-
let lock = raf_ref.0.lock();
64+
let lock = raf.0.lock();
5465
if let Some(callback) = lock.callback.as_ref() {
5566
callback(frame_time_nanos.into());
5667
}
5768
}
5869

59-
raf_ref.1.fetch_sub(1, Ordering::SeqCst);
70+
raf.1.fetch_sub(1, Ordering::SeqCst);
71+
raf.2.notify_all();
6072

61-
raf_ref.2.notify_all();
73+
let repost = {
74+
let lock = raf.0.lock();
75+
lock.started && lock.generation == chain.generation && lock.use_deprecated
76+
};
6277

63-
unsafe {
64-
let instance = AChoreographer_getInstance();
65-
let mut lock = raf_ref.0.lock();
66-
if lock.started {
67-
if lock.use_deprecated {
68-
AChoreographer_postFrameCallback(instance, Some(Raf::callback), data_ptr);
69-
} else {
70-
// post 64 variant if available
71-
}
72-
} else {
73-
let stored = lock.data.take();
74-
drop(lock);
75-
if let Some(p) = stored {
76-
let _ = Box::from_raw(p as *mut Raf);
77-
}
78+
if repost {
79+
unsafe {
80+
let instance = AChoreographer_getInstance();
81+
AChoreographer_postFrameCallback(
82+
instance,
83+
Some(Raf::callback),
84+
Box::into_raw(chain) as *mut c_void,
85+
);
7886
}
7987
}
88+
// else: `chain` drops here, ending this frame chain.
8089
}
8190

8291
pub fn new(callback: RafCallback) -> Self {
@@ -86,7 +95,7 @@ impl Raf {
8695
callback,
8796
is_prepared: false,
8897
use_deprecated: true, //*crate::API_LEVEL.get().unwrap_or(&-1) < 24,
89-
data: None,
98+
generation: 0,
9099
thread_id: None,
91100
})),
92101
Arc::new(AtomicUsize::new(0)),
@@ -95,27 +104,42 @@ impl Raf {
95104
}
96105

97106
pub fn start(&self) {
98-
let mut lock = self.0.lock();
99-
if !lock.is_prepared {
100-
unsafe {
101-
ndk::looper::ThreadLooper::prepare();
107+
let generation;
108+
{
109+
let mut lock = self.0.lock();
110+
if lock.started {
111+
return;
112+
}
113+
if !lock.is_prepared {
114+
unsafe {
115+
ndk::looper::ThreadLooper::prepare();
116+
}
117+
lock.is_prepared = true;
118+
}
119+
120+
lock.thread_id = Some(std::thread::current().id());
121+
lock.generation += 1;
122+
lock.started = true;
123+
generation = lock.generation;
124+
if !lock.use_deprecated {
125+
// #[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
126+
// AChoreographer_postFrameCallback64(...)
127+
return;
102128
}
103-
lock.is_prepared = true;
104129
}
105130

106-
lock.thread_id = Some(std::thread::current().id());
131+
let chain = Box::new(FrameChain {
132+
raf: self.clone(),
133+
generation,
134+
});
107135
unsafe {
108136
let instance = AChoreographer_getInstance();
109-
let data = Box::into_raw(Box::new(self.clone())) as *mut c_void;
110-
lock.data = Some(data);
111-
if lock.use_deprecated {
112-
AChoreographer_postFrameCallback(instance, Some(Raf::callback), data);
113-
} else {
114-
// #[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
115-
// AChoreographer_postFrameCallback64(instance, Some(Raf::callback), data.cast());
116-
}
137+
AChoreographer_postFrameCallback(
138+
instance,
139+
Some(Raf::callback),
140+
Box::into_raw(chain) as *mut c_void,
141+
);
117142
}
118-
lock.started = true;
119143
}
120144

121145
pub fn stop(&self) {
@@ -129,8 +153,8 @@ impl Raf {
129153
let lock = self.0.lock();
130154
if let Some(tid) = lock.thread_id {
131155
if tid == std::thread::current().id() {
132-
drop(lock);
133-
self.clear_callback();
156+
// Same thread as the choreographer callback: a callback
157+
// cannot be mid-flight right now, nothing to wait for.
134158
return true;
135159
}
136160
}
@@ -151,14 +175,11 @@ impl Raf {
151175
}
152176

153177
pub fn clear_callback(&self) {
178+
// Only detach the JS-side callback. The FrameChain allocation stays
179+
// alive until the already-queued choreographer callback fires, sees
180+
// started == false (or a stale generation), and drops it itself.
154181
let mut lock = self.0.lock();
155182
lock.callback = None;
156-
if self.1.load(Ordering::SeqCst) == 0 {
157-
if let Some(p) = lock.data.take() {
158-
drop(lock);
159-
let _ = unsafe { Box::from_raw(p as *mut Raf) };
160-
}
161-
}
162183
}
163184

164185
pub fn set_callback(&self, callback: RafCallback) {

0 commit comments

Comments
 (0)