Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Unverified Commit 1aa7a92d authored by Simon Chan's avatar Simon Chan
Browse files

fix(adb): read socket too slow can cause data loss

parent dce44ae9
Loading
Loading
Loading
Loading
+74 −80
Original line number Diff line number Diff line
@@ -104,6 +104,8 @@ export class AdbDaemonWebUsbConnection
        return this.#device;
    }

    #inEndpoint: USBEndpoint;

    #readable: ReadableStream<AdbPacketData>;
    get readable() {
        return this.#readable;
@@ -121,6 +123,7 @@ export class AdbDaemonWebUsbConnection
        usbManager: USB,
    ) {
        this.#device = device;
        this.#inEndpoint = inEndpoint;

        let closed = false;

@@ -154,26 +157,68 @@ export class AdbDaemonWebUsbConnection
        usbManager.addEventListener("disconnect", handleUsbDisconnect);

        this.#readable = duplex.wrapReadable(
            new ReadableStream<AdbPacketData>({
                async pull(controller) {
            new ReadableStream<AdbPacketData>(
                {
                    pull: async (controller) => {
                        const packet = await this.#transferIn();
                        if (packet) {
                            controller.enqueue(packet);
                        } else {
                            controller.close();
                        }
                    },
                },
                { highWaterMark: 0 },
            ),
        );

        const zeroMask = outEndpoint.packetSize - 1;
        this.#writable = pipeFrom(
            duplex.createWritable(
                new ConsumableWritableStream({
                    write: async (chunk) => {
                        try {
                            await device.raw.transferOut(
                                outEndpoint.endpointNumber,
                                chunk,
                            );

                            // In USB protocol, a not-full packet indicates the end of a transfer.
                            // If the payload size is a multiple of the packet size,
                            // we need to send an empty packet to indicate the end,
                            // so the OS will send it to the device immediately.
                            if (
                                zeroMask &&
                                (chunk.byteLength & zeroMask) === 0
                            ) {
                                await device.raw.transferOut(
                                    outEndpoint.endpointNumber,
                                    EMPTY_UINT8_ARRAY,
                                );
                            }
                        } catch (e) {
                            if (closed) {
                                return;
                            }
                            throw e;
                        }
                    },
                }),
            ),
            new AdbPacketSerializeStream(),
        );
    }

    async #transferIn(): Promise<AdbPacketData | undefined> {
        try {
            while (true) {
                            // The `length` argument in `transferIn` must not be smaller than what the device sent,
                            // otherwise it will return `babble` status without any data.
                // ADB daemon sends each packet in two parts, the 24-byte header and the payload.
                            const result = await device.raw.transferIn(
                                inEndpoint.endpointNumber,
                                24,
                const result = await this.#device.raw.transferIn(
                    this.#inEndpoint.endpointNumber,
                    this.#inEndpoint.packetSize,
                );

                            // Maximum payload size is 1MB, so reading 1MB data will always success,
                            // and always discards all lingering data.
                            // FIXME: Chrome on Windows doesn't support babble status. See the HACK below.
                            if (result.status === "babble") {
                                await device.raw.transferIn(
                                    inEndpoint.endpointNumber,
                                    1024 * 1024,
                                );
                if (result.data!.byteLength !== 24) {
                    continue;
                }

@@ -185,34 +230,22 @@ export class AdbDaemonWebUsbConnection
                const packet = AdbPacketHeader.deserialize(
                    stream,
                ) as AdbPacketHeader & { payload: Uint8Array };
                            if (packet.payloadLength !== 0) {
                                // HACK: Chrome on Windows doesn't support babble status,
                                // so maybe we are not actually reading an ADB packet header.
                                // Currently the maximum payload size is 1MB,
                                // so if the payload length is larger than that,
                                // try to discard the data and receive again.
                                // https://crbug.com/1314358
                                if (packet.payloadLength > 1024 * 1024) {
                                    await device.raw.transferIn(
                                        inEndpoint.endpointNumber,
                                        1024 * 1024,
                                    );

                if (packet.magic !== (packet.command ^ 0xffffffff)) {
                    continue;
                }

                                const result = await device.raw.transferIn(
                                    inEndpoint.endpointNumber,
                if (packet.payloadLength !== 0) {
                    const result = await this.#device.raw.transferIn(
                        this.#inEndpoint.endpointNumber,
                        packet.payloadLength,
                    );
                                packet.payload = new Uint8Array(
                                    result.data!.buffer,
                                );
                    packet.payload = new Uint8Array(result.data!.buffer);
                } else {
                    packet.payload = EMPTY_UINT8_ARRAY;
                }

                            controller.enqueue(packet);
                            return;
                return packet;
            }
        } catch (e) {
            // On Windows, disconnecting the device will cause `NetworkError` to be thrown,
@@ -227,7 +260,7 @@ export class AdbDaemonWebUsbConnection
                });

                if (closed) {
                                controller.close();
                    return undefined;
                } else {
                    throw e;
                }
@@ -235,45 +268,6 @@ export class AdbDaemonWebUsbConnection

            throw e;
        }
                },
            }),
        );

        const zeroMask = outEndpoint.packetSize - 1;
        this.#writable = pipeFrom(
            duplex.createWritable(
                new ConsumableWritableStream({
                    write: async (chunk) => {
                        try {
                            await device.raw.transferOut(
                                outEndpoint.endpointNumber,
                                chunk,
                            );

                            // In USB protocol, a not-full packet indicates the end of a transfer.
                            // If the payload size is a multiple of the packet size,
                            // we need to send an empty packet to indicate the end,
                            // so the OS will send it to the device immediately.
                            if (
                                zeroMask &&
                                (chunk.byteLength & zeroMask) === 0
                            ) {
                                await device.raw.transferOut(
                                    outEndpoint.endpointNumber,
                                    EMPTY_UINT8_ARRAY,
                                );
                            }
                        } catch (e) {
                            if (closed) {
                                return;
                            }
                            throw e;
                        }
                    },
                }),
            ),
            new AdbPacketSerializeStream(),
        );
    }
}

+13 −14
Original line number Diff line number Diff line
@@ -87,20 +87,8 @@ export class AdbPacketDispatcher implements Closeable {
                                await this.#handleClose(packet);
                                break;
                            case AdbCommand.Write:
                                if (this.#sockets.has(packet.arg1)) {
                                    await this.#sockets
                                        .get(packet.arg1)!
                                        .enqueue(packet.payload);
                                    await this.sendPacket(
                                        AdbCommand.OK,
                                        packet.arg1,
                                        packet.arg0,
                                    );
                                await this.#handleWrite(packet);
                                break;
                                }
                                throw new Error(
                                    `Unknown local socket id: ${packet.arg1}`,
                                );
                            case AdbCommand.Open:
                                await this.#handleOpen(packet);
                                break;
@@ -200,6 +188,17 @@ export class AdbPacketDispatcher implements Closeable {
        // the device may also respond with two `CLSE` packets.
    }

    async #handleWrite(packet: AdbPacketData) {
        const socket = this.#sockets.get(packet.arg1);
        if (!socket) {
            throw new Error(`Unknown local socket id: ${packet.arg1}`);
        }

        await socket.enqueue(packet.payload);
        await this.sendPacket(AdbCommand.OK, packet.arg1, packet.arg0);
        return;
    }

    addReverseTunnel(service: string, handler: AdbIncomingSocketHandler) {
        this.#incomingSocketHandlers.set(service, handler);
    }
+1 −6
Original line number Diff line number Diff line
@@ -108,12 +108,7 @@ export class AdbDaemonSocketController
                (controller) => {
                    this.#readableController = controller;
                },
                {
                    highWaterMark: options.highWaterMark ?? 16 * 1024,
                    size(chunk) {
                        return chunk.byteLength;
                    },
                },
                { highWaterMark: 0 },
            ),
        );

+20 −13
Original line number Diff line number Diff line
@@ -31,7 +31,6 @@ export class BufferedReadableStream implements AsyncExactReadable {
        if (done) {
            throw new ExactReadableEndedError();
        }
        this.#position += value.byteLength;
        return value;
    }

@@ -42,45 +41,51 @@ export class BufferedReadableStream implements AsyncExactReadable {
        if (initial) {
            result = new Uint8Array(length);
            result.set(initial);
            index = initial.byteLength;
            length -= initial.byteLength;
            index = initial.length;
            length -= initial.length;
        } else {
            const array = await this.#readSource();
            if (array.byteLength === length) {
            if (array.length === length) {
                this.#position += length;
                return array;
            }

            if (array.byteLength > length) {
            if (array.length > length) {
                this.#buffered = array;
                this.#bufferedOffset = length;
                this.#bufferedLength = array.byteLength - length;
                this.#bufferedLength = array.length - length;
                this.#position += length;
                return array.subarray(0, length);
            }

            result = new Uint8Array(length);
            result.set(array);
            index = array.byteLength;
            length -= array.byteLength;
            index = array.length;
            length -= array.length;
            this.#position += array.length;
        }

        while (length > 0) {
            const array = await this.#readSource();
            if (array.byteLength === length) {
            if (array.length === length) {
                result.set(array, index);
                this.#position += length;
                return result;
            }

            if (array.byteLength > length) {
            if (array.length > length) {
                this.#buffered = array;
                this.#bufferedOffset = length;
                this.#bufferedLength = array.byteLength - length;
                this.#bufferedLength = array.length - length;
                result.set(array.subarray(0, length), index);
                this.#position += length;
                return result;
            }

            result.set(array, index);
            index += array.byteLength;
            length -= array.byteLength;
            index += array.length;
            length -= array.length;
            this.#position += array.length;
        }

        return result;
@@ -101,12 +106,14 @@ export class BufferedReadableStream implements AsyncExactReadable {
                // don't use it until absolutely necessary
                this.#bufferedOffset += length;
                this.#bufferedLength -= length;
                this.#position += length;
                return array.subarray(offset, offset + length);
            }

            this.#buffered = undefined;
            this.#bufferedLength = 0;
            this.#bufferedOffset = 0;
            this.#position += array.length - offset;
            return this.#readAsync(length, array.subarray(offset));
        }

+21 −14
Original line number Diff line number Diff line
@@ -2,6 +2,7 @@ import { PromiseResolver } from "@yume-chan/async";
import type { ValueOrPromise } from "@yume-chan/struct";

import type {
    QueuingStrategy,
    ReadableStream,
    ReadableStreamDefaultController,
    WritableStreamDefaultWriter,
@@ -65,8 +66,12 @@ export class DuplexStreamFactory<R, W> {
        this.#options = options ?? {};
    }

    wrapReadable(readable: ReadableStream<R>): WrapReadableStream<R> {
        return new WrapReadableStream<R>({
    wrapReadable(
        readable: ReadableStream<R>,
        strategy?: QueuingStrategy<R>,
    ): WrapReadableStream<R> {
        return new WrapReadableStream<R>(
            {
                start: (controller) => {
                    this.#readableControllers.push(controller);
                    return readable;
@@ -79,7 +84,9 @@ export class DuplexStreamFactory<R, W> {
                    // stream end means the remote peer closed the connection first.
                    await this.dispose();
                },
        });
            },
            strategy,
        );
    }

    createWritable(stream: WritableStream<W>): WritableStream<W> {
Loading