Bandaid fix for the UAF in ConvertPromiseToArrayBufferInThreadPool. Approach -------- We cannot route the post through cancelable_task_tracker_ because the tracker is sequence-affine to control_task_runner_ but the post happens on the isolate thread (this is likely *why* the original code used base::ThreadPool::PostTask directly). Instead: 1. Track in-flight named-data fds in a member set guarded by the existing named_fd_lock_. 2. In TerminateAndDestroy (control sequence), close all in-flight fds BEFORE posting DeleteSelf. This unblocks read() while `this` is still valid. The ThreadPool worker proceeds to dereference control_task_runner_ on a still-valid object. DeleteSelf is two thread hops away (control → post → isolate thread → run), giving the unblocked worker plenty of time to finish. 3. As a belt-and-suspenders measure, set an atomic flag in TerminateAndDestroy and CHECK it after ReadFromFD returns. If the residual race is ever hit (worker is descheduled between read() return and CHECK while DeleteSelf catches up), the process crashes cleanly instead of corrupting heap. This converts an attacker-controlled vtable-hijack UAF into either correct behaviour (overwhelmingly likely) or a clean CHECK abort. This is a bandaid, not a complete fix. A complete fix needs the destructor to join the in-flight task, e.g. via a WaitableEvent the task signals on completion. --- a/android_webview/js_sandbox/service/js_sandbox_isolate.h +++ b/android_webview/js_sandbox/service/js_sandbox_isolate.h @@ -6,6 +6,7 @@ #define ANDROID_WEBVIEW_JS_SANDBOX_SERVICE_JS_SANDBOX_ISOLATE_H_ #include +#include #include #include #include @@ -232,6 +233,14 @@ class JsSandboxIsolate { base::Lock named_fd_lock_; std::unordered_map named_fd_ GUARDED_BY(named_fd_lock_); + // SECURITY: fds whose ConvertPromiseToArrayBufferInThreadPool task is in + // flight. TerminateAndDestroy closes these to unblock read() while `this` + // is still valid, preventing the task from dereferencing a freed object. + std::set inflight_named_data_fds_ GUARDED_BY(named_fd_lock_); + // SECURITY: Set by TerminateAndDestroy. ConvertPromiseToArrayBufferInThreadPool + // CHECKs this after read() returns. Converts the residual race into a clean + // crash. + std::atomic terminate_called_{false}; // Callbacks associated with evaluations which have begun but not yet // finished. More precisely, these are callbacks which have been passed to --- a/android_webview/js_sandbox/service/js_sandbox_isolate.cc +++ b/android_webview/js_sandbox/service/js_sandbox_isolate.cc @@ -459,6 +459,21 @@ void JsSandboxIsolate::CreateCancelableTaskTracker() { // Called from control sequence. void JsSandboxIsolate::TerminateAndDestroy() { + // SECURITY: ConvertPromiseToArrayBufferInThreadPool was posted via + // base::ThreadPool::PostTask (not cancelable_task_tracker_) and may be + // blocked in read() right now. Close those fds to unblock read() while + // `this` is still valid; DeleteSelf is two thread hops away, so the + // unblocked worker finishes before the object is freed in practice. + // The atomic flag + CHECK below catches the residual race. + terminate_called_.store(true, std::memory_order_release); + { + base::AutoLock hold(named_fd_lock_); + for (int fd : inflight_named_data_fds_) { + // ScopedFD in the task still owns the fd; we just close() the + // descriptor number to make read() return -1/EBADF or 0/EOF. + ::close(fd); + } + } // This will cancel all pending executions. cancelable_task_tracker_.reset(); isolate_holder_->isolate()->TerminateExecution(); @@ -525,9 +540,21 @@ void JsSandboxIsolate::ConvertPromiseToArrayBufferInThreadPool( std::unique_ptr> array_buffer, std::unique_ptr> resolver, void* inner_buffer) { - if (base::ReadFromFD(fd.get(), UNSAFE_TODO(base::span( + bool read_ok = base::ReadFromFD(fd.get(), UNSAFE_TODO(base::span( static_cast(inner_buffer), - base::checked_cast(length))))) { + base::checked_cast(length)))); + // SECURITY: If TerminateAndDestroy ran while we were in read(), `this` + // may already be freed (DeleteSelf runs concurrently on the isolate + // thread). Crash cleanly instead of corrupting heap. Under ASAN, the + // load of terminate_called_ itself will fault if the object was freed + // before we got here, which is also a clean detection. + CHECK(!terminate_called_.load(std::memory_order_acquire)) + << "JsSandboxIsolate destroyed while named-data read was in flight"; + { + base::AutoLock hold(named_fd_lock_); + inflight_named_data_fds_.erase(fd.get()); + } + if (read_ok) { control_task_runner_->PostTask( FROM_HERE, base::BindOnce( @@ -908,6 +935,10 @@ void JsSandboxIsolate::ConsumeNamedDataAsArrayBuffer(gin::Arguments* args) { std::unique_ptr> global_array_buffer( std::make_unique>( isolate, std::move(local_array_buffer))); + { + base::AutoLock hold(named_fd_lock_); + inflight_named_data_fds_.insert(fd.get()); + } base::ThreadPool::PostTask( FROM_HERE, {base::MayBlock()}, base::BindOnce(&JsSandboxIsolate::ConvertPromiseToArrayBufferInThreadPool,