From 17779c1e7412f400ffa64f68e405724b06e3fd09 Mon Sep 17 00:00:00 2001 From: Santo Shakil Date: Sat, 16 May 2026 03:25:31 +0600 Subject: [PATCH] fix(mobile): cronet thumbnail buffer overflow regression from #28439 (#28450) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The hybrid added in onReadCompleted reuses Cronet's ByteBuffer between reads to save a JNI wrap call when no grow is needed. That reuse breaks advance() — Cronet's position() is cumulative across reads, so the same K bytes get counted on every subsequent iteration. b.offset overshoots b.capacity, the reuse branch keeps firing on a now-empty buffer, and request.read() throws the original IllegalArgumentException again. Always pass a fresh wrap from wrapRemaining() so byteBuffer.position() reflects only this iteration's bytes. Same shape as the original PR had before the broken optimization was layered on top. --- .../immich/images/RemoteImagesImpl.kt | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/images/RemoteImagesImpl.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/images/RemoteImagesImpl.kt index d1651b7960..f7ebc349f6 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/images/RemoteImagesImpl.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/images/RemoteImagesImpl.kt @@ -265,18 +265,14 @@ private class CronetImageFetcher : ImageFetcher { byteBuffer: ByteBuffer ) { try { - val b = buffer!! - b.advance(byteBuffer.position()) - // Reuse the caller-supplied ByteBuffer as long as we don't need to grow. - // It already points at our native memory with position advanced past the - // written bytes — Cronet can keep writing into the remaining tail. - // Only when the buffer is full do we grow (which may realloc + move the - // native pointer) and need a fresh wrap. - val buf = if (b.offset == b.capacity) { - b.ensureHeadroom() - b.wrapRemaining() - } else { - byteBuffer + // Always pass a fresh wrap so byteBuffer.position() represents only the + // bytes Cronet wrote in this iteration. Reusing the caller-supplied + // ByteBuffer breaks advance(): Cronet's position keeps accumulating + // across reads, which would double-count previous iterations' bytes. + val buf = buffer!!.run { + advance(byteBuffer.position()) + ensureHeadroom() + wrapRemaining() } request.read(buf) } catch (e: Exception) {