From 2ce173dda02565d7630748f7b94abace9bdcf4c0 Mon Sep 17 00:00:00 2001 From: Shallow Date: Mon, 22 Jun 2026 22:15:22 +0800 Subject: [PATCH 01/13] Harden DEX obfuscation against malformed input --- daemon/src/main/jni/obfuscation.cpp | 273 ++++++++++++++++++++++++++-- 1 file changed, 253 insertions(+), 20 deletions(-) diff --git a/daemon/src/main/jni/obfuscation.cpp b/daemon/src/main/jni/obfuscation.cpp index d6ecc83fa..794f0f2f1 100644 --- a/daemon/src/main/jni/obfuscation.cpp +++ b/daemon/src/main/jni/obfuscation.cpp @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -37,6 +38,237 @@ jmethodID method_shared_memory_ctor = nullptr; } // anonymous namespace +static bool rangeFits(size_t file_size, dex::u4 offset, size_t byte_count) { + auto start = static_cast(offset); + return start <= file_size && byte_count <= file_size - start; +} + +static bool isAligned(dex::u4 value, dex::u4 alignment) { + return value % alignment == 0; +} + +static bool isStandardDexMagic(const dex::u1 *magic) { + return std::memcmp(magic, "dex\n", 4) == 0 && magic[4] >= '0' && magic[4] <= '9' && + magic[5] >= '0' && magic[5] <= '9' && magic[6] >= '0' && magic[6] <= '9' && + magic[7] == '\0'; +} + +static bool sectionFits(size_t file_size, dex::u4 offset, dex::u4 count, size_t item_size, + const char *name) { + if (count == 0) { + if (offset == 0) return true; + LOGW("Invalid DEX %s section: empty section has non-zero offset %u", name, offset); + return false; + } + if (offset == 0 || !isAligned(offset, 4)) { + LOGW("Invalid DEX %s section: offset=%u count=%u", name, offset, count); + return false; + } + auto start = static_cast(offset); + if (start > file_size || count > (file_size - start) / item_size) { + LOGW("Invalid DEX %s section: offset=%u count=%u item_size=%zu file_size=%zu", name, + offset, count, item_size, file_size); + return false; + } + return true; +} + +static bool dataRangeFits(const dex::Header *header, size_t file_size, dex::u4 offset, + size_t byte_count, const char *name, bool allow_zero) { + if (offset == 0) return allow_zero; + if (offset < header->data_off || !rangeFits(file_size, offset, byte_count)) { + LOGW("Invalid DEX %s offset: offset=%u data_off=%u size=%zu file_size=%zu", name, offset, + header->data_off, byte_count, file_size); + return false; + } + return true; +} + +static bool typeListFits(const dex::u1 *base, const dex::Header *header, size_t file_size, + dex::u4 offset, const char *name) { + if (offset == 0) return true; + if (!isAligned(offset, 4) || + !dataRangeFits(header, file_size, offset, sizeof(dex::TypeList), name, false)) { + return false; + } + + const auto *type_list = reinterpret_cast(base + offset); + auto start = static_cast(offset) + sizeof(dex::u4); + if (start > file_size || type_list->size > (file_size - start) / sizeof(dex::TypeItem)) { + LOGW("Invalid DEX %s type list: offset=%u count=%u file_size=%zu", name, offset, + type_list->size, file_size); + return false; + } + + for (dex::u4 i = 0; i < type_list->size; ++i) { + if (type_list->list[i].type_idx >= header->type_ids_size) { + LOGW("Invalid DEX %s type list item: type_idx=%u type_count=%u", name, + type_list->list[i].type_idx, header->type_ids_size); + return false; + } + } + return true; +} + +// Slicer's own structural checks compile out under NDEBUG, so validate the +// table ranges and indexed references that CreateFullIr() will touch first. +static bool isDexSafeForSlicer(const void *dex_data, size_t mapped_size) { + if (mapped_size < sizeof(dex::Header)) { + LOGW("Invalid DEX: mapped size %zu is smaller than header size %zu", mapped_size, + sizeof(dex::Header)); + return false; + } + + const auto *base = reinterpret_cast(dex_data); + const auto *header = reinterpret_cast(base); + if (!isStandardDexMagic(header->magic)) { + LOGW("Invalid DEX: unsupported magic"); + return false; + } + + auto file_size = static_cast(header->file_size); + if (file_size < sizeof(dex::Header) || file_size > mapped_size) { + LOGW("Invalid DEX: file_size=%zu mapped_size=%zu", file_size, mapped_size); + return false; + } + if (header->header_size != sizeof(dex::Header)) { + LOGW("Invalid DEX: unsupported header_size=%u", header->header_size); + return false; + } + if (header->endian_tag != dex::kEndianConstant) { + LOGW("Invalid DEX: unsupported endian tag 0x%x", header->endian_tag); + return false; + } + if (header->link_size != 0 || header->link_off != 0) { + LOGW("Invalid DEX: link section is not supported"); + return false; + } + if ((header->data_size != 0 && (header->data_off == 0 || !isAligned(header->data_off, 4))) || + !rangeFits(file_size, header->data_off, header->data_size)) { + LOGW("Invalid DEX data section: offset=%u size=%u file_size=%zu", header->data_off, + header->data_size, file_size); + return false; + } + if (header->type_ids_size >= 65536 || header->proto_ids_size >= 65536) { + LOGW("Invalid DEX: type_ids_size=%u proto_ids_size=%u", header->type_ids_size, + header->proto_ids_size); + return false; + } + + if (header->map_off == 0 || !isAligned(header->map_off, 4) || + header->map_off < header->data_off || !rangeFits(file_size, header->map_off, sizeof(dex::u4))) { + LOGW("Invalid DEX map section: offset=%u data_off=%u file_size=%zu", header->map_off, + header->data_off, file_size); + return false; + } + const auto *map_list = reinterpret_cast(base + header->map_off); + auto map_items_start = static_cast(header->map_off) + sizeof(dex::u4); + if (map_list->size == 0 || + map_items_start > file_size || + map_list->size > (file_size - map_items_start) / sizeof(dex::MapItem)) { + LOGW("Invalid DEX map list: offset=%u count=%u file_size=%zu", header->map_off, + map_list->size, file_size); + return false; + } + + if (!sectionFits(file_size, header->string_ids_off, header->string_ids_size, + sizeof(dex::StringId), "string_ids") || + !sectionFits(file_size, header->type_ids_off, header->type_ids_size, sizeof(dex::TypeId), + "type_ids") || + !sectionFits(file_size, header->proto_ids_off, header->proto_ids_size, + sizeof(dex::ProtoId), "proto_ids") || + !sectionFits(file_size, header->field_ids_off, header->field_ids_size, + sizeof(dex::FieldId), "field_ids") || + !sectionFits(file_size, header->method_ids_off, header->method_ids_size, + sizeof(dex::MethodId), "method_ids") || + !sectionFits(file_size, header->class_defs_off, header->class_defs_size, + sizeof(dex::ClassDef), "class_defs")) { + return false; + } + + const auto *string_ids = reinterpret_cast(base + header->string_ids_off); + for (dex::u4 i = 0; i < header->string_ids_size; ++i) { + if (!dataRangeFits(header, file_size, string_ids[i].string_data_off, sizeof(dex::u1), + "string_data", false)) { + return false; + } + } + + const auto *type_ids = reinterpret_cast(base + header->type_ids_off); + for (dex::u4 i = 0; i < header->type_ids_size; ++i) { + if (type_ids[i].descriptor_idx >= header->string_ids_size) { + LOGW("Invalid DEX type_id: descriptor_idx=%u string_count=%u", + type_ids[i].descriptor_idx, header->string_ids_size); + return false; + } + } + + const auto *proto_ids = reinterpret_cast(base + header->proto_ids_off); + for (dex::u4 i = 0; i < header->proto_ids_size; ++i) { + if (proto_ids[i].shorty_idx >= header->string_ids_size || + proto_ids[i].return_type_idx >= header->type_ids_size || + !typeListFits(base, header, file_size, proto_ids[i].parameters_off, + "proto parameters")) { + LOGW("Invalid DEX proto_id: shorty_idx=%u return_type_idx=%u", proto_ids[i].shorty_idx, + proto_ids[i].return_type_idx); + return false; + } + } + + const auto *field_ids = reinterpret_cast(base + header->field_ids_off); + for (dex::u4 i = 0; i < header->field_ids_size; ++i) { + if (field_ids[i].class_idx >= header->type_ids_size || + field_ids[i].type_idx >= header->type_ids_size || + field_ids[i].name_idx >= header->string_ids_size) { + LOGW("Invalid DEX field_id: class_idx=%u type_idx=%u name_idx=%u", + field_ids[i].class_idx, field_ids[i].type_idx, field_ids[i].name_idx); + return false; + } + } + + const auto *method_ids = reinterpret_cast(base + header->method_ids_off); + for (dex::u4 i = 0; i < header->method_ids_size; ++i) { + if (method_ids[i].class_idx >= header->type_ids_size || + method_ids[i].proto_idx >= header->proto_ids_size || + method_ids[i].name_idx >= header->string_ids_size) { + LOGW("Invalid DEX method_id: class_idx=%u proto_idx=%u name_idx=%u", + method_ids[i].class_idx, method_ids[i].proto_idx, method_ids[i].name_idx); + return false; + } + } + + const auto *class_defs = reinterpret_cast(base + header->class_defs_off); + for (dex::u4 i = 0; i < header->class_defs_size; ++i) { + const auto &class_def = class_defs[i]; + if (class_def.class_idx >= header->type_ids_size || + (class_def.superclass_idx != dex::kNoIndex && + class_def.superclass_idx >= header->type_ids_size) || + (class_def.source_file_idx != dex::kNoIndex && + class_def.source_file_idx >= header->string_ids_size) || + !typeListFits(base, header, file_size, class_def.interfaces_off, "class interfaces") || + !dataRangeFits(header, file_size, class_def.annotations_off, + sizeof(dex::AnnotationsDirectoryItem), + "class annotations", true) || + !dataRangeFits(header, file_size, class_def.class_data_off, sizeof(dex::u1), + "class data", true) || + !dataRangeFits(header, file_size, class_def.static_values_off, sizeof(dex::u1), + "static values", true)) { + LOGW("Invalid DEX class_def at index %u", i); + return false; + } + } + + return true; +} + +static jobject wrapSharedMemoryFd(JNIEnv *env, int fd) { + auto java_fd = + lsplant::JNI_NewObject(env, class_file_descriptor, method_file_descriptor_ctor, fd); + auto java_sm = + lsplant::JNI_NewObject(env, class_shared_memory, method_shared_memory_ctor, java_fd); + return java_sm.release(); +} + // Converts Dex signatures to Java format. // Trailing slashes are translated to dots, which correctly aligns with // Java's string matching expectations for package prefixes. @@ -201,7 +433,13 @@ Java_org_matrix_vector_daemon_utils_ObfuscationManager_obfuscateDex(JNIEnv *env, if (fd < 0) return nullptr; auto size = ASharedMemory_getSize(fd); - LOGV("obfuscateDex: fd=%d, size=%zu", fd, size); + if (size <= 0) { + LOGE("Invalid input dex shared memory size: %zd", static_cast(size)); + close(fd); + return nullptr; + } + auto mapped_size = static_cast(size); + LOGV("obfuscateDex: fd=%d, size=%zu", fd, mapped_size); // CRITICAL: We MUST use MAP_SHARED here, not MAP_PRIVATE. // 1. Android's SharedMemory is backed by ashmem or memfd. Mapping these as @@ -217,7 +455,7 @@ Java_org_matrix_vector_daemon_utils_ObfuscationManager_obfuscateDex(JNIEnv *env, // heap allocations. This is safe here because the Daemon owns the // lifecycle of this temporary buffer and the Java caller will discard // the un-obfuscated original anyway. - void *mem = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + void *mem = mmap(nullptr, mapped_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (mem == MAP_FAILED) { LOGE("Failed to map input dex"); close(fd); @@ -226,7 +464,7 @@ Java_org_matrix_vector_daemon_utils_ObfuscationManager_obfuscateDex(JNIEnv *env, bool needs_obfuscation = false; for (const auto &sig : signatures) { - if (memmem(mem, size, sig.first.c_str(), sig.first.length()) != nullptr) { + if (memmem(mem, mapped_size, sig.first.c_str(), sig.first.length()) != nullptr) { needs_obfuscation = true; break; } @@ -234,33 +472,28 @@ Java_org_matrix_vector_daemon_utils_ObfuscationManager_obfuscateDex(JNIEnv *env, if (!needs_obfuscation) { LOGV("No target signatures found in fd=%d, skipping slicer.", fd); - munmap(mem, size); + munmap(mem, mapped_size); + return wrapSharedMemoryFd(env, fd); + } - // Wrap the duplicated FD into Java objects and return instantly - auto java_fd = - lsplant::JNI_NewObject(env, class_file_descriptor, method_file_descriptor_ctor, fd); - auto java_sm = - lsplant::JNI_NewObject(env, class_shared_memory, method_shared_memory_ctor, java_fd); - return java_sm.release(); + if (!isDexSafeForSlicer(mem, mapped_size)) { + LOGW("Skipping DEX obfuscation for malformed input fd=%d", fd); + munmap(mem, mapped_size); + return wrapSharedMemoryFd(env, fd); } // Process the DEX and obtain a new file descriptor for the output - int new_fd = obfuscateDexBuffer(mem, size); + int new_fd = obfuscateDexBuffer(mem, mapped_size); // Safely unmap and close the input buffer mapping - munmap(mem, size); - close(fd); + munmap(mem, mapped_size); if (new_fd < 0) { LOGE("Obfuscation failed to create new dex buffer"); - return nullptr; + return wrapSharedMemoryFd(env, fd); } + close(fd); // Construct new SharedMemory object around the new_fd - auto java_fd = - lsplant::JNI_NewObject(env, class_file_descriptor, method_file_descriptor_ctor, new_fd); - auto java_sm = - lsplant::JNI_NewObject(env, class_shared_memory, method_shared_memory_ctor, java_fd); - - return java_sm.release(); + return wrapSharedMemoryFd(env, new_fd); } From 612b0b0350c33055f92bc0a6bad2e80c5a92bffb Mon Sep 17 00:00:00 2001 From: Shallow Date: Tue, 23 Jun 2026 01:12:54 +0800 Subject: [PATCH 02/13] Use validated DEX file size for slicer input --- daemon/src/main/jni/obfuscation.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/daemon/src/main/jni/obfuscation.cpp b/daemon/src/main/jni/obfuscation.cpp index 794f0f2f1..e69e18825 100644 --- a/daemon/src/main/jni/obfuscation.cpp +++ b/daemon/src/main/jni/obfuscation.cpp @@ -481,9 +481,11 @@ Java_org_matrix_vector_daemon_utils_ObfuscationManager_obfuscateDex(JNIEnv *env, munmap(mem, mapped_size); return wrapSharedMemoryFd(env, fd); } + auto dex_file_size = + static_cast(reinterpret_cast(mem)->file_size); // Process the DEX and obtain a new file descriptor for the output - int new_fd = obfuscateDexBuffer(mem, mapped_size); + int new_fd = obfuscateDexBuffer(mem, dex_file_size); // Safely unmap and close the input buffer mapping munmap(mem, mapped_size); From aa5a1c4e41ccde82e44566c8fbd373578b391d28 Mon Sep 17 00:00:00 2001 From: Shallow Date: Tue, 23 Jun 2026 01:39:26 +0800 Subject: [PATCH 03/13] Return original shared memory on obfuscation fallback --- daemon/src/main/jni/obfuscation.cpp | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/daemon/src/main/jni/obfuscation.cpp b/daemon/src/main/jni/obfuscation.cpp index e69e18825..c95166bc2 100644 --- a/daemon/src/main/jni/obfuscation.cpp +++ b/daemon/src/main/jni/obfuscation.cpp @@ -269,6 +269,11 @@ static jobject wrapSharedMemoryFd(JNIEnv *env, int fd) { return java_sm.release(); } +static jobject returnOriginalSharedMemory(jobject memory, int fd) { + if (fd >= 0) close(fd); + return memory; +} + // Converts Dex signatures to Java format. // Trailing slashes are translated to dots, which correctly aligns with // Java's string matching expectations for package prefixes. @@ -430,13 +435,15 @@ Java_org_matrix_vector_daemon_utils_ObfuscationManager_obfuscateDex(JNIEnv *env, ensureInitialized(env); int fd = ASharedMemory_dupFromJava(env, memory); - if (fd < 0) return nullptr; + if (fd < 0) { + LOGE("Failed to duplicate input dex shared memory"); + return memory; + } auto size = ASharedMemory_getSize(fd); if (size <= 0) { LOGE("Invalid input dex shared memory size: %zd", static_cast(size)); - close(fd); - return nullptr; + return returnOriginalSharedMemory(memory, fd); } auto mapped_size = static_cast(size); LOGV("obfuscateDex: fd=%d, size=%zu", fd, mapped_size); @@ -458,8 +465,7 @@ Java_org_matrix_vector_daemon_utils_ObfuscationManager_obfuscateDex(JNIEnv *env, void *mem = mmap(nullptr, mapped_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (mem == MAP_FAILED) { LOGE("Failed to map input dex"); - close(fd); - return nullptr; + return returnOriginalSharedMemory(memory, fd); } bool needs_obfuscation = false; @@ -473,13 +479,13 @@ Java_org_matrix_vector_daemon_utils_ObfuscationManager_obfuscateDex(JNIEnv *env, if (!needs_obfuscation) { LOGV("No target signatures found in fd=%d, skipping slicer.", fd); munmap(mem, mapped_size); - return wrapSharedMemoryFd(env, fd); + return returnOriginalSharedMemory(memory, fd); } if (!isDexSafeForSlicer(mem, mapped_size)) { LOGW("Skipping DEX obfuscation for malformed input fd=%d", fd); munmap(mem, mapped_size); - return wrapSharedMemoryFd(env, fd); + return returnOriginalSharedMemory(memory, fd); } auto dex_file_size = static_cast(reinterpret_cast(mem)->file_size); @@ -492,7 +498,7 @@ Java_org_matrix_vector_daemon_utils_ObfuscationManager_obfuscateDex(JNIEnv *env, if (new_fd < 0) { LOGE("Obfuscation failed to create new dex buffer"); - return wrapSharedMemoryFd(env, fd); + return returnOriginalSharedMemory(memory, fd); } close(fd); From b8f47d12ef50fca560f4bb373c72d1b7bcfd0ea3 Mon Sep 17 00:00:00 2001 From: Shallow Date: Sun, 28 Jun 2026 13:42:19 +0800 Subject: [PATCH 04/13] Validate DEX class data before obfuscation --- daemon/src/main/jni/obfuscation.cpp | 150 ++++++++++++++++++++++++++-- 1 file changed, 142 insertions(+), 8 deletions(-) diff --git a/daemon/src/main/jni/obfuscation.cpp b/daemon/src/main/jni/obfuscation.cpp index c95166bc2..c2a69eba0 100644 --- a/daemon/src/main/jni/obfuscation.cpp +++ b/daemon/src/main/jni/obfuscation.cpp @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -110,6 +111,140 @@ static bool typeListFits(const dex::u1 *base, const dex::Header *header, size_t return true; } +static bool readULeb128Checked(const dex::u1 **ptr, const dex::u1 *limit, dex::u4 *value, + const char *name) { + dex::u4 result = 0; + for (int i = 0; i < 5; ++i) { + if (*ptr >= limit) { + LOGW("Invalid DEX %s: truncated uleb128", name); + return false; + } + dex::u1 byte = *(*ptr)++; + if (i == 4 && (byte & 0xf0) != 0) { + LOGW("Invalid DEX %s: uleb128 overflow", name); + return false; + } + result |= static_cast(byte & 0x7f) << (i * 7); + if ((byte & 0x80) == 0) { + *value = result; + return true; + } + } + LOGW("Invalid DEX %s: unterminated uleb128", name); + return false; +} + +static bool applyMemberIndexDelta(dex::u4 delta, dex::u4 *base_index, dex::u4 count, + const char *name) { + if (*base_index != dex::kNoIndex && delta == 0) { + LOGW("Invalid DEX %s: repeated member index delta", name); + return false; + } + uint64_t index = delta; + if (*base_index != dex::kNoIndex) { + index += *base_index; + } + if (index >= count) { + LOGW("Invalid DEX %s: index=%llu count=%u", name, + static_cast(index), count); + return false; + } + *base_index = static_cast(index); + return true; +} + +static bool codeItemFits(const dex::u1 *base, const dex::Header *header, size_t file_size, + dex::u4 offset) { + if (offset == 0) return true; + if (!isAligned(offset, 4) || + !dataRangeFits(header, file_size, offset, offsetof(dex::Code, insns), "method code", + false)) { + return false; + } + const auto *code = reinterpret_cast(base + offset); + auto insns_start = static_cast(offset) + offsetof(dex::Code, insns); + if (insns_start > file_size || + code->insns_size > (file_size - insns_start) / sizeof(dex::u2)) { + LOGW("Invalid DEX method code: offset=%u insns_size=%u file_size=%zu", offset, + code->insns_size, file_size); + return false; + } + if (code->tries_size != 0) { + auto aligned_insns = (static_cast(code->insns_size) + 1) / 2 * 2; + auto tries_start = insns_start + aligned_insns * sizeof(dex::u2); + if (tries_start > file_size || + code->tries_size > (file_size - tries_start) / sizeof(dex::TryBlock)) { + LOGW("Invalid DEX method code tries: offset=%u tries_size=%u file_size=%zu", offset, + code->tries_size, file_size); + return false; + } + auto handlers_start = tries_start + static_cast(code->tries_size) * sizeof(dex::TryBlock); + if (handlers_start >= file_size) { + LOGW("Invalid DEX method code handlers: offset=%u file_size=%zu", offset, file_size); + return false; + } + } + return true; +} + +static bool classDataFits(const dex::u1 *base, const dex::Header *header, size_t file_size, + dex::u4 offset) { + if (offset == 0) return true; + if (!dataRangeFits(header, file_size, offset, sizeof(dex::u1), "class data", false)) { + return false; + } + + const dex::u1 *ptr = base + offset; + const dex::u1 *limit = base + file_size; + dex::u4 static_fields_count; + dex::u4 instance_fields_count; + dex::u4 direct_methods_count; + dex::u4 virtual_methods_count; + if (!readULeb128Checked(&ptr, limit, &static_fields_count, "class static_fields_count") || + !readULeb128Checked(&ptr, limit, &instance_fields_count, "class instance_fields_count") || + !readULeb128Checked(&ptr, limit, &direct_methods_count, "class direct_methods_count") || + !readULeb128Checked(&ptr, limit, &virtual_methods_count, "class virtual_methods_count")) { + return false; + } + + auto validate_fields = [&](dex::u4 count, const char *name) { + dex::u4 base_index = dex::kNoIndex; + for (dex::u4 i = 0; i < count; ++i) { + dex::u4 field_idx_delta; + dex::u4 access_flags; + if (!readULeb128Checked(&ptr, limit, &field_idx_delta, name) || + !applyMemberIndexDelta(field_idx_delta, &base_index, header->field_ids_size, name) || + !readULeb128Checked(&ptr, limit, &access_flags, name)) { + return false; + } + } + return true; + }; + + auto validate_methods = [&](dex::u4 count, const char *name) { + dex::u4 base_index = dex::kNoIndex; + for (dex::u4 i = 0; i < count; ++i) { + dex::u4 method_idx_delta; + dex::u4 access_flags; + dex::u4 code_off; + if (!readULeb128Checked(&ptr, limit, &method_idx_delta, name) || + !applyMemberIndexDelta(method_idx_delta, &base_index, header->method_ids_size, + name) || + !readULeb128Checked(&ptr, limit, &access_flags, name) || + !readULeb128Checked(&ptr, limit, &code_off, name) || + !codeItemFits(base, header, file_size, code_off)) { + return false; + } + } + return true; + }; + + return validate_fields(static_fields_count, "class static field") && + validate_fields(instance_fields_count, "class instance field") && + validate_methods(direct_methods_count, "class direct method") && + validate_methods(virtual_methods_count, "class virtual method"); +} + // Slicer's own structural checks compile out under NDEBUG, so validate the // table ranges and indexed references that CreateFullIr() will touch first. static bool isDexSafeForSlicer(const void *dex_data, size_t mapped_size) { @@ -245,14 +380,13 @@ static bool isDexSafeForSlicer(const void *dex_data, size_t mapped_size) { class_def.superclass_idx >= header->type_ids_size) || (class_def.source_file_idx != dex::kNoIndex && class_def.source_file_idx >= header->string_ids_size) || - !typeListFits(base, header, file_size, class_def.interfaces_off, "class interfaces") || - !dataRangeFits(header, file_size, class_def.annotations_off, - sizeof(dex::AnnotationsDirectoryItem), - "class annotations", true) || - !dataRangeFits(header, file_size, class_def.class_data_off, sizeof(dex::u1), - "class data", true) || - !dataRangeFits(header, file_size, class_def.static_values_off, sizeof(dex::u1), - "static values", true)) { + !typeListFits(base, header, file_size, class_def.interfaces_off, "class interfaces") || + !dataRangeFits(header, file_size, class_def.annotations_off, + sizeof(dex::AnnotationsDirectoryItem), + "class annotations", true) || + !classDataFits(base, header, file_size, class_def.class_data_off) || + !dataRangeFits(header, file_size, class_def.static_values_off, sizeof(dex::u1), + "static values", true)) { LOGW("Invalid DEX class_def at index %u", i); return false; } From f70bd527a3d9edc8a375c647b50a1b172e7cc271 Mon Sep 17 00:00:00 2001 From: Shallow Date: Thu, 2 Jul 2026 02:10:36 +0800 Subject: [PATCH 05/13] Validate DEX variable data before obfuscation --- daemon/src/main/jni/obfuscation.cpp | 498 +++++++++++++++++++++++++++- 1 file changed, 490 insertions(+), 8 deletions(-) diff --git a/daemon/src/main/jni/obfuscation.cpp b/daemon/src/main/jni/obfuscation.cpp index c2a69eba0..4572e71ef 100644 --- a/daemon/src/main/jni/obfuscation.cpp +++ b/daemon/src/main/jni/obfuscation.cpp @@ -134,6 +134,41 @@ static bool readULeb128Checked(const dex::u1 **ptr, const dex::u1 *limit, dex::u return false; } +static bool readSLeb128Checked(const dex::u1 **ptr, const dex::u1 *limit, dex::s4 *value, + const char *name) { + dex::u4 result = 0; + int shift = 0; + dex::u1 byte = 0; + for (int i = 0; i < 5; ++i) { + if (*ptr >= limit) { + LOGW("Invalid DEX %s: truncated sleb128", name); + return false; + } + byte = *(*ptr)++; + result |= static_cast(byte & 0x7f) << shift; + shift += 7; + if ((byte & 0x80) == 0) { + if (shift < 32 && (byte & 0x40) != 0) { + result |= ~0u << shift; + } + *value = static_cast(result); + return true; + } + } + LOGW("Invalid DEX %s: unterminated sleb128", name); + return false; +} + +static bool consumeBytes(const dex::u1 **ptr, const dex::u1 *limit, size_t byte_count, + const char *name) { + if (*ptr > limit || byte_count > static_cast(limit - *ptr)) { + LOGW("Invalid DEX %s: truncated encoded data", name); + return false; + } + *ptr += byte_count; + return true; +} + static bool applyMemberIndexDelta(dex::u4 delta, dex::u4 *base_index, dex::u4 count, const char *name) { if (*base_index != dex::kNoIndex && delta == 0) { @@ -153,6 +188,406 @@ static bool applyMemberIndexDelta(dex::u4 delta, dex::u4 *base_index, dex::u4 co return true; } +static bool stringDataFits(const dex::u1 *base, const dex::Header *header, size_t file_size, + dex::u4 offset) { + if (!dataRangeFits(header, file_size, offset, sizeof(dex::u1), "string_data", false)) { + return false; + } + + const dex::u1 *ptr = base + offset; + const dex::u1 *limit = base + file_size; + dex::u4 utf16_size; + if (!readULeb128Checked(&ptr, limit, &utf16_size, "string_data utf16_size")) { + return false; + } + if (ptr > limit || std::memchr(ptr, '\0', static_cast(limit - ptr)) == nullptr) { + LOGW("Invalid DEX string_data: missing MUTF-8 terminator at offset=%u", offset); + return false; + } + return true; +} + +static bool encodedValueFits(const dex::u1 **ptr, const dex::u1 *limit, + const dex::Header *header, int depth); + +static bool annotationFits(const dex::u1 **ptr, const dex::u1 *limit, + const dex::Header *header, int depth) { + dex::u4 type_index; + dex::u4 elements_count; + if (!readULeb128Checked(ptr, limit, &type_index, "annotation type") || + !readULeb128Checked(ptr, limit, &elements_count, "annotation elements")) { + return false; + } + if (type_index >= header->type_ids_size) { + LOGW("Invalid DEX annotation: type_idx=%u type_count=%u", type_index, + header->type_ids_size); + return false; + } + + for (dex::u4 i = 0; i < elements_count; ++i) { + dex::u4 name_index; + if (!readULeb128Checked(ptr, limit, &name_index, "annotation element name")) { + return false; + } + if (name_index >= header->string_ids_size) { + LOGW("Invalid DEX annotation element: name_idx=%u string_count=%u", name_index, + header->string_ids_size); + return false; + } + if (!encodedValueFits(ptr, limit, header, depth + 1)) { + return false; + } + } + return true; +} + +static bool encodedArrayFits(const dex::u1 **ptr, const dex::u1 *limit, + const dex::Header *header, int depth) { + dex::u4 count; + if (!readULeb128Checked(ptr, limit, &count, "encoded array size")) { + return false; + } + for (dex::u4 i = 0; i < count; ++i) { + if (!encodedValueFits(ptr, limit, header, depth + 1)) { + return false; + } + } + return true; +} + +static bool readEncodedIndexChecked(const dex::u1 **ptr, const dex::u1 *limit, dex::u1 arg, + dex::u4 count, const char *name) { + if (arg > 3) { + LOGW("Invalid DEX %s: encoded index uses %u bytes", name, arg + 1); + return false; + } + if (!consumeBytes(ptr, limit, static_cast(arg) + 1, name)) { + return false; + } + + const dex::u1 *value = *ptr - (static_cast(arg) + 1); + dex::u4 index = 0; + for (dex::u1 i = 0; i <= arg; ++i) { + index |= static_cast(value[i]) << (i * 8); + } + if (index >= count) { + LOGW("Invalid DEX %s: index=%u count=%u", name, index, count); + return false; + } + return true; +} + +static bool encodedValueFits(const dex::u1 **ptr, const dex::u1 *limit, + const dex::Header *header, int depth) { + if (depth > 32) { + LOGW("Invalid DEX encoded value: nesting is too deep"); + return false; + } + if (*ptr >= limit) { + LOGW("Invalid DEX encoded value: missing header"); + return false; + } + + dex::u1 encoded_header = *(*ptr)++; + dex::u1 type = encoded_header & dex::kEncodedValueTypeMask; + dex::u1 arg = encoded_header >> dex::kEncodedValueArgShift; + + switch (type) { + case dex::kEncodedByte: + if (arg != 0) { + LOGW("Invalid DEX encoded byte: arg=%u", arg); + return false; + } + return consumeBytes(ptr, limit, 1, "encoded byte"); + case dex::kEncodedShort: + case dex::kEncodedChar: + if (arg > 1) { + LOGW("Invalid DEX encoded 16-bit value: arg=%u", arg); + return false; + } + return consumeBytes(ptr, limit, static_cast(arg) + 1, "encoded 16-bit value"); + case dex::kEncodedInt: + case dex::kEncodedFloat: + if (arg > 3) { + LOGW("Invalid DEX encoded 32-bit value: arg=%u", arg); + return false; + } + return consumeBytes(ptr, limit, static_cast(arg) + 1, "encoded 32-bit value"); + case dex::kEncodedLong: + case dex::kEncodedDouble: + if (arg > 7) { + LOGW("Invalid DEX encoded 64-bit value: arg=%u", arg); + return false; + } + return consumeBytes(ptr, limit, static_cast(arg) + 1, "encoded 64-bit value"); + case dex::kEncodedString: + return readEncodedIndexChecked(ptr, limit, arg, header->string_ids_size, + "encoded string"); + case dex::kEncodedType: + return readEncodedIndexChecked(ptr, limit, arg, header->type_ids_size, + "encoded type"); + case dex::kEncodedField: + case dex::kEncodedEnum: + return readEncodedIndexChecked(ptr, limit, arg, header->field_ids_size, + "encoded field"); + case dex::kEncodedMethod: + return readEncodedIndexChecked(ptr, limit, arg, header->method_ids_size, + "encoded method"); + case dex::kEncodedArray: + return arg == 0 && encodedArrayFits(ptr, limit, header, depth + 1); + case dex::kEncodedAnnotation: + return arg == 0 && annotationFits(ptr, limit, header, depth + 1); + case dex::kEncodedNull: + if (arg != 0) { + LOGW("Invalid DEX encoded null: arg=%u", arg); + return false; + } + return true; + case dex::kEncodedBoolean: + if (arg > 1) { + LOGW("Invalid DEX encoded boolean: arg=%u", arg); + return false; + } + return true; + default: + LOGW("Invalid DEX encoded value type: 0x%x", type); + return false; + } +} + +static bool encodedArrayItemFits(const dex::u1 *base, const dex::Header *header, + size_t file_size, dex::u4 offset, const char *name) { + if (offset == 0) return true; + if (!dataRangeFits(header, file_size, offset, sizeof(dex::u1), name, false)) { + return false; + } + const dex::u1 *ptr = base + offset; + const dex::u1 *limit = base + file_size; + return encodedArrayFits(&ptr, limit, header, 0); +} + +static bool annotationItemFits(const dex::u1 *base, const dex::Header *header, size_t file_size, + dex::u4 offset) { + if (!dataRangeFits(header, file_size, offset, sizeof(dex::AnnotationItem), "annotation item", + false)) { + return false; + } + const dex::u1 *ptr = base + offset + offsetof(dex::AnnotationItem, annotation); + const dex::u1 *limit = base + file_size; + return annotationFits(&ptr, limit, header, 0); +} + +static bool annotationSetFits(const dex::u1 *base, const dex::Header *header, size_t file_size, + dex::u4 offset) { + if (offset == 0) return true; + if (!isAligned(offset, 4) || + !dataRangeFits(header, file_size, offset, sizeof(dex::AnnotationSetItem), + "annotation set", false)) { + return false; + } + const auto *set = reinterpret_cast(base + offset); + auto entries_start = static_cast(offset) + sizeof(dex::u4); + if (entries_start > file_size || set->size > (file_size - entries_start) / sizeof(dex::u4)) { + LOGW("Invalid DEX annotation set: offset=%u count=%u file_size=%zu", offset, set->size, + file_size); + return false; + } + for (dex::u4 i = 0; i < set->size; ++i) { + if (set->entries[i] == 0 || !annotationItemFits(base, header, file_size, set->entries[i])) { + LOGW("Invalid DEX annotation set entry at index %u", i); + return false; + } + } + return true; +} + +static bool annotationSetRefListFits(const dex::u1 *base, const dex::Header *header, + size_t file_size, dex::u4 offset) { + if (!isAligned(offset, 4) || + !dataRangeFits(header, file_size, offset, sizeof(dex::AnnotationSetRefList), + "annotation set ref list", false)) { + return false; + } + const auto *list = reinterpret_cast(base + offset); + auto entries_start = static_cast(offset) + sizeof(dex::u4); + if (entries_start > file_size || + list->size > (file_size - entries_start) / sizeof(dex::AnnotationSetRefItem)) { + LOGW("Invalid DEX annotation set ref list: offset=%u count=%u file_size=%zu", offset, + list->size, file_size); + return false; + } + for (dex::u4 i = 0; i < list->size; ++i) { + dex::u4 annotations_off = list->list[i].annotations_off; + if (annotations_off != 0 && !annotationSetFits(base, header, file_size, annotations_off)) { + LOGW("Invalid DEX annotation set ref entry at index %u", i); + return false; + } + } + return true; +} + +static bool annotationsDirectoryFits(const dex::u1 *base, const dex::Header *header, + size_t file_size, dex::u4 offset) { + if (offset == 0) return true; + if (!isAligned(offset, 4) || + !dataRangeFits(header, file_size, offset, sizeof(dex::AnnotationsDirectoryItem), + "class annotations", false)) { + return false; + } + const auto *directory = reinterpret_cast(base + offset); + auto fields_start = static_cast(offset) + sizeof(dex::AnnotationsDirectoryItem); + auto fields_bytes = static_cast(directory->fields_size) * + sizeof(dex::FieldAnnotationsItem); + auto methods_bytes = static_cast(directory->methods_size) * + sizeof(dex::MethodAnnotationsItem); + auto parameters_bytes = static_cast(directory->parameters_size) * + sizeof(dex::ParameterAnnotationsItem); + if (fields_start > file_size || fields_bytes > file_size - fields_start || + methods_bytes > file_size - fields_start - fields_bytes || + parameters_bytes > file_size - fields_start - fields_bytes - methods_bytes) { + LOGW("Invalid DEX annotations directory: offset=%u file_size=%zu", offset, file_size); + return false; + } + + if (directory->class_annotations_off != 0 && + !annotationSetFits(base, header, file_size, directory->class_annotations_off)) { + return false; + } + + const auto *field_annotations = + reinterpret_cast(base + fields_start); + for (dex::u4 i = 0; i < directory->fields_size; ++i) { + if (field_annotations[i].field_idx >= header->field_ids_size || + field_annotations[i].annotations_off == 0 || + !annotationSetFits(base, header, file_size, field_annotations[i].annotations_off)) { + LOGW("Invalid DEX field annotation at index %u", i); + return false; + } + } + + const auto *method_annotations = reinterpret_cast( + reinterpret_cast(field_annotations) + fields_bytes); + for (dex::u4 i = 0; i < directory->methods_size; ++i) { + if (method_annotations[i].method_idx >= header->method_ids_size || + method_annotations[i].annotations_off == 0 || + !annotationSetFits(base, header, file_size, method_annotations[i].annotations_off)) { + LOGW("Invalid DEX method annotation at index %u", i); + return false; + } + } + + const auto *param_annotations = reinterpret_cast( + reinterpret_cast(method_annotations) + methods_bytes); + for (dex::u4 i = 0; i < directory->parameters_size; ++i) { + if (param_annotations[i].method_idx >= header->method_ids_size || + param_annotations[i].annotations_off == 0 || + !annotationSetRefListFits(base, header, file_size, param_annotations[i].annotations_off)) { + LOGW("Invalid DEX parameter annotation at index %u", i); + return false; + } + } + + return true; +} + +static bool debugInfoIndexFits(dex::u4 index_plus_one, dex::u4 count, const char *name) { + if (index_plus_one == 0) return true; + dex::u4 index = index_plus_one - 1; + if (index >= count) { + LOGW("Invalid DEX debug info %s: index=%u count=%u", name, index, count); + return false; + } + return true; +} + +static bool debugInfoFits(const dex::u1 *base, const dex::Header *header, size_t file_size, + dex::u4 offset) { + if (offset == 0) return true; + if (!dataRangeFits(header, file_size, offset, sizeof(dex::u1), "debug info", false)) { + return false; + } + const dex::u1 *ptr = base + offset; + const dex::u1 *limit = base + file_size; + dex::u4 value; + if (!readULeb128Checked(&ptr, limit, &value, "debug line_start") || + !readULeb128Checked(&ptr, limit, &value, "debug parameters_size")) { + return false; + } + dex::u4 parameters_size = value; + for (dex::u4 i = 0; i < parameters_size; ++i) { + dex::u4 name_index; + if (!readULeb128Checked(&ptr, limit, &name_index, "debug parameter name") || + !debugInfoIndexFits(name_index, header->string_ids_size, "parameter name")) { + return false; + } + } + + while (true) { + if (ptr >= limit) { + LOGW("Invalid DEX debug info: missing end sequence"); + return false; + } + dex::u1 opcode = *ptr++; + switch (opcode) { + case dex::DBG_END_SEQUENCE: + return true; + case dex::DBG_ADVANCE_PC: + if (!readULeb128Checked(&ptr, limit, &value, "debug advance pc")) return false; + break; + case dex::DBG_ADVANCE_LINE: { + dex::s4 line_diff; + if (!readSLeb128Checked(&ptr, limit, &line_diff, "debug advance line")) { + return false; + } + break; + } + case dex::DBG_START_LOCAL: { + dex::u4 register_num; + dex::u4 name_index; + dex::u4 type_index; + if (!readULeb128Checked(&ptr, limit, ®ister_num, "debug local register") || + !readULeb128Checked(&ptr, limit, &name_index, "debug local name") || + !debugInfoIndexFits(name_index, header->string_ids_size, "local name") || + !readULeb128Checked(&ptr, limit, &type_index, "debug local type") || + !debugInfoIndexFits(type_index, header->type_ids_size, "local type")) { + return false; + } + break; + } + case dex::DBG_START_LOCAL_EXTENDED: { + dex::u4 register_num; + dex::u4 name_index; + dex::u4 type_index; + dex::u4 sig_index; + if (!readULeb128Checked(&ptr, limit, ®ister_num, "debug local register") || + !readULeb128Checked(&ptr, limit, &name_index, "debug local name") || + !debugInfoIndexFits(name_index, header->string_ids_size, "local name") || + !readULeb128Checked(&ptr, limit, &type_index, "debug local type") || + !debugInfoIndexFits(type_index, header->type_ids_size, "local type") || + !readULeb128Checked(&ptr, limit, &sig_index, "debug local signature") || + !debugInfoIndexFits(sig_index, header->string_ids_size, "local signature")) { + return false; + } + break; + } + case dex::DBG_END_LOCAL: + case dex::DBG_RESTART_LOCAL: + if (!readULeb128Checked(&ptr, limit, &value, "debug local register")) { + return false; + } + break; + case dex::DBG_SET_FILE: + if (!readULeb128Checked(&ptr, limit, &value, "debug source file") || + !debugInfoIndexFits(value, header->string_ids_size, "source file")) { + return false; + } + break; + default: + break; + } + } +} + static bool codeItemFits(const dex::u1 *base, const dex::Header *header, size_t file_size, dex::u4 offset) { if (offset == 0) return true; @@ -169,6 +604,9 @@ static bool codeItemFits(const dex::u1 *base, const dex::Header *header, size_t code->insns_size, file_size); return false; } + if (!debugInfoFits(base, header, file_size, code->debug_info_off)) { + return false; + } if (code->tries_size != 0) { auto aligned_insns = (static_cast(code->insns_size) + 1) / 2 * 2; auto tries_start = insns_start + aligned_insns * sizeof(dex::u2); @@ -183,6 +621,53 @@ static bool codeItemFits(const dex::u1 *base, const dex::Header *header, size_t LOGW("Invalid DEX method code handlers: offset=%u file_size=%zu", offset, file_size); return false; } + const auto *tries = reinterpret_cast(base + tries_start); + const dex::u1 *handlers = base + handlers_start; + const dex::u1 *ptr = handlers; + const dex::u1 *limit = base + file_size; + dex::u4 handlers_count; + if (!readULeb128Checked(&ptr, limit, &handlers_count, "catch handler list") || + handlers_count > code->tries_size) { + LOGW("Invalid DEX catch handler list: handlers=%u tries=%u", handlers_count, + code->tries_size); + return false; + } + for (dex::u4 handler_index = 0; handler_index < handlers_count; ++handler_index) { + dex::s4 catch_count; + if (!readSLeb128Checked(&ptr, limit, &catch_count, "catch handler size")) { + return false; + } + auto typed_catches = catch_count < 0 ? -static_cast(catch_count) + : static_cast(catch_count); + for (int64_t catch_index = 0; catch_index < typed_catches; ++catch_index) { + dex::u4 type_index; + dex::u4 address; + if (!readULeb128Checked(&ptr, limit, &type_index, "catch handler type") || + type_index >= header->type_ids_size || + !readULeb128Checked(&ptr, limit, &address, "catch handler address") || + address > code->insns_size) { + LOGW("Invalid DEX catch handler entry: type_idx=%u type_count=%u", type_index, + header->type_ids_size); + return false; + } + } + if (catch_count <= 0) { + dex::u4 catch_all_addr; + if (!readULeb128Checked(&ptr, limit, &catch_all_addr, "catch-all handler") || + catch_all_addr > code->insns_size) { + return false; + } + } + } + auto handlers_size = static_cast(ptr - handlers); + for (dex::u4 i = 0; i < code->tries_size; ++i) { + if (tries[i].start_addr > code->insns_size || + tries[i].insn_count > code->insns_size - tries[i].start_addr || + tries[i].handler_off >= handlers_size) { + LOGW("Invalid DEX try block at index %u", i); + return false; + } + } } return true; } @@ -323,8 +808,7 @@ static bool isDexSafeForSlicer(const void *dex_data, size_t mapped_size) { const auto *string_ids = reinterpret_cast(base + header->string_ids_off); for (dex::u4 i = 0; i < header->string_ids_size; ++i) { - if (!dataRangeFits(header, file_size, string_ids[i].string_data_off, sizeof(dex::u1), - "string_data", false)) { + if (!stringDataFits(base, header, file_size, string_ids[i].string_data_off)) { return false; } } @@ -378,15 +862,13 @@ static bool isDexSafeForSlicer(const void *dex_data, size_t mapped_size) { if (class_def.class_idx >= header->type_ids_size || (class_def.superclass_idx != dex::kNoIndex && class_def.superclass_idx >= header->type_ids_size) || - (class_def.source_file_idx != dex::kNoIndex && + (class_def.source_file_idx != dex::kNoIndex && class_def.source_file_idx >= header->string_ids_size) || !typeListFits(base, header, file_size, class_def.interfaces_off, "class interfaces") || - !dataRangeFits(header, file_size, class_def.annotations_off, - sizeof(dex::AnnotationsDirectoryItem), - "class annotations", true) || + !annotationsDirectoryFits(base, header, file_size, class_def.annotations_off) || !classDataFits(base, header, file_size, class_def.class_data_off) || - !dataRangeFits(header, file_size, class_def.static_values_off, sizeof(dex::u1), - "static values", true)) { + !encodedArrayItemFits(base, header, file_size, class_def.static_values_off, + "static values")) { LOGW("Invalid DEX class_def at index %u", i); return false; } From 481f548c1125d8633ca5fb3d26a2d5e2ebe9ec92 Mon Sep 17 00:00:00 2001 From: Shallow Date: Thu, 2 Jul 2026 11:10:47 +0800 Subject: [PATCH 06/13] Match obfuscation map to preload dex output --- .../matrix/vector/daemon/data/FileSystem.kt | 20 ++++++++++++++++--- .../vector/daemon/ipc/ApplicationService.kt | 6 +++++- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/FileSystem.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/FileSystem.kt index 5eeb7a1b9..a8e672b4e 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/FileSystem.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/FileSystem.kt @@ -39,6 +39,8 @@ import org.matrix.vector.daemon.utils.ObfuscationManager private const val TAG = "VectorFileSystem" object FileSystem { + private data class LoadedDex(val memory: SharedMemory, val obfuscated: Boolean) + val basePath: Path = Paths.get("/data/adb/lspd") val logDirPath: Path = basePath.resolve("log") val oldLogDirPath: Path = basePath.resolve("log.old") @@ -50,6 +52,8 @@ object FileSystem { val dbPath: File = configDirPath.resolve("modules_config.db").toFile() @Volatile private var preloadDex: SharedMemory? = null + @Volatile var isPreloadDexObfuscated: Boolean = false + private set private val formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME.withZone(ZoneId.systemDefault()) private val lockPath: Path = basePath.resolve("lock") @@ -143,21 +147,27 @@ object FileSystem { } /** Loads a single DEX file into SharedMemory, optionally applying obfuscation. */ - private fun readDex(inputStream: InputStream, obfuscate: Boolean): SharedMemory { + private fun readDexResult(inputStream: InputStream, obfuscate: Boolean): LoadedDex { var memory = SharedMemory.create(null, inputStream.available()) val byteBuffer = memory.mapReadWrite() Channels.newChannel(inputStream).read(byteBuffer) SharedMemory.unmap(byteBuffer) + var obfuscated = false if (obfuscate) { val newMemory = ObfuscationManager.obfuscateDex(memory) if (memory !== newMemory) { memory.close() memory = newMemory + obfuscated = true } } memory.setProtect(OsConstants.PROT_READ) - return memory + return LoadedDex(memory, obfuscated) + } + + private fun readDex(inputStream: InputStream, obfuscate: Boolean): SharedMemory { + return readDexResult(inputStream, obfuscate).memory } /** Parses the module APK, extracts init lists, and loads DEXes into SharedMemory. */ @@ -308,7 +318,11 @@ object FileSystem { fun getPreloadDex(obfuscate: Boolean): SharedMemory? { if (preloadDex == null) { runCatching { - FileInputStream("framework/lspd.dex").use { preloadDex = readDex(it, obfuscate) } + FileInputStream("framework/lspd.dex").use { + val loadedDex = readDexResult(it, obfuscate) + preloadDex = loadedDex.memory + isPreloadDexObfuscated = loadedDex.obfuscated + } } .onFailure { Log.e(TAG, "Failed to load framework dex", it) } } diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ApplicationService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ApplicationService.kt index fb4f817e8..35b7a79b3 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ApplicationService.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ApplicationService.kt @@ -53,7 +53,11 @@ object ApplicationService : ILSPApplicationService.Stub() { return true } OBFUSCATION_MAP_TRANSACTION_CODE -> { - val obfuscation = ConfigCache.state.isDexObfuscateEnabled + if (ConfigCache.state.isDexObfuscateEnabled) { + FileSystem.getPreloadDex(true) + } + val obfuscation = + ConfigCache.state.isDexObfuscateEnabled && FileSystem.isPreloadDexObfuscated val signatures = ObfuscationManager.getSignatures() reply?.writeNoException() reply?.writeInt(signatures.size * 2) From ea6531c971782169d3921e5557e84c09e6d4b3d7 Mon Sep 17 00:00:00 2001 From: Shallow Date: Thu, 2 Jul 2026 11:48:16 +0800 Subject: [PATCH 07/13] Keep dex input immutable during obfuscation --- daemon/src/main/jni/obfuscation.cpp | 37 +++++++++++++++-------------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/daemon/src/main/jni/obfuscation.cpp b/daemon/src/main/jni/obfuscation.cpp index 4572e71ef..071de339c 100644 --- a/daemon/src/main/jni/obfuscation.cpp +++ b/daemon/src/main/jni/obfuscation.cpp @@ -14,7 +14,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -1018,7 +1020,7 @@ Java_org_matrix_vector_daemon_utils_ObfuscationManager_getSignatures( return signatures_jni; } -static int obfuscateDexBuffer(const void *dex_data, size_t size) { +static int obfuscateDexBuffer(void *dex_data, size_t size) { dex::Reader reader{reinterpret_cast(dex_data), size}; reader.CreateFullIr(); auto ir = reader.GetIr(); @@ -1040,6 +1042,11 @@ static int obfuscateDexBuffer(const void *dex_data, size_t size) { // CreateImage calls allocator.Allocate() auto *image = writer.CreateImage(&allocator, &new_size); LOGD("writer.CreateImage returned: %p", image); + if (image == nullptr) { + auto output_fd = allocator.GetFd(); + if (output_fd >= 0) close(output_fd); + return -1; + } return allocator.GetFd(); } @@ -1064,21 +1071,7 @@ Java_org_matrix_vector_daemon_utils_ObfuscationManager_obfuscateDex(JNIEnv *env, auto mapped_size = static_cast(size); LOGV("obfuscateDex: fd=%d, size=%zu", fd, mapped_size); - // CRITICAL: We MUST use MAP_SHARED here, not MAP_PRIVATE. - // 1. Android's SharedMemory is backed by ashmem or memfd. Mapping these as - // MAP_PRIVATE creates a Copy-On-Write (COW) layer. In many Android kernel - // configurations, this COW layer does not correctly fault-in the initial - // contents from the shared source, resulting in the JNI side seeing - // unpopulated zero-pages. This causes slicer to fail immediately. - // 2. Using MAP_SHARED ensures we have direct access to the same physical - // pages populated by the Java layer. - // 3. ZERO-COPY MUTATION: Slicer's Intermediate Representation (IR) points - // directly into this mapped memory for string data. By mutating the - // buffer in-place, we update the IR's state without any additional - // heap allocations. This is safe here because the Daemon owns the - // lifecycle of this temporary buffer and the Java caller will discard - // the un-obfuscated original anyway. - void *mem = mmap(nullptr, mapped_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + void *mem = mmap(nullptr, mapped_size, PROT_READ, MAP_SHARED, fd, 0); if (mem == MAP_FAILED) { LOGE("Failed to map input dex"); return returnOriginalSharedMemory(memory, fd); @@ -1106,8 +1099,16 @@ Java_org_matrix_vector_daemon_utils_ObfuscationManager_obfuscateDex(JNIEnv *env, auto dex_file_size = static_cast(reinterpret_cast(mem)->file_size); - // Process the DEX and obtain a new file descriptor for the output - int new_fd = obfuscateDexBuffer(mem, dex_file_size); + auto mutable_dex = std::unique_ptr(new (std::nothrow) dex::u1[dex_file_size]); + if (mutable_dex == nullptr) { + LOGE("Failed to allocate private dex obfuscation buffer, size=%zu", dex_file_size); + munmap(mem, mapped_size); + return returnOriginalSharedMemory(memory, fd); + } + memcpy(mutable_dex.get(), mem, dex_file_size); + + // Slicer mutates string storage through the IR, so run it on a private copy. + int new_fd = obfuscateDexBuffer(mutable_dex.get(), dex_file_size); // Safely unmap and close the input buffer mapping munmap(mem, mapped_size); From 7f8fe1771d4f8d74e779335db569bb1e2ff61d7a Mon Sep 17 00:00:00 2001 From: Shallow Date: Thu, 2 Jul 2026 12:02:13 +0800 Subject: [PATCH 08/13] Validate dex bytecode references before slicing --- daemon/src/main/jni/obfuscation.cpp | 117 ++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/daemon/src/main/jni/obfuscation.cpp b/daemon/src/main/jni/obfuscation.cpp index 071de339c..19d653b80 100644 --- a/daemon/src/main/jni/obfuscation.cpp +++ b/daemon/src/main/jni/obfuscation.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -590,6 +591,119 @@ static bool debugInfoFits(const dex::u1 *base, const dex::Header *header, size_t } } +static bool instructionIndexFits(const dex::Header *header, dex::InstructionIndexType type, + dex::u4 index, dex::u4 index2, const char *opcode) { + switch (type) { + case dex::kIndexNone: + case dex::kIndexInlineMethod: + case dex::kIndexVtableOffset: + case dex::kIndexFieldOffset: + return true; + case dex::kIndexStringRef: + if (index < header->string_ids_size) return true; + break; + case dex::kIndexTypeRef: + if (index < header->type_ids_size) return true; + break; + case dex::kIndexFieldRef: + if (index < header->field_ids_size) return true; + break; + case dex::kIndexMethodRef: + if (index < header->method_ids_size) return true; + break; + case dex::kIndexProtoRef: + if (index < header->proto_ids_size) return true; + break; + case dex::kIndexMethodAndProtoRef: + if (index < header->method_ids_size && index2 < header->proto_ids_size) return true; + break; + default: + break; + } + + LOGW("Invalid DEX bytecode reference: opcode=%s index=%u index2=%u type=%u", + opcode, index, index2, type); + return false; +} + +static bool instructionsFit(const dex::Header *header, const dex::u2 *insns, + dex::u4 insns_size) { + dex::u4 cursor = 0; + while (cursor < insns_size) { + const dex::u2 *ptr = insns + cursor; + size_t remaining = static_cast(insns_size - cursor); + size_t width; + if (*ptr == dex::kPackedSwitchSignature) { + if (remaining < 2) { + LOGW("Invalid DEX packed-switch payload header: cursor=%u", cursor); + return false; + } + width = 4 + static_cast(ptr[1]) * 2; + } else if (*ptr == dex::kSparseSwitchSignature) { + if (remaining < 2) { + LOGW("Invalid DEX sparse-switch payload header: cursor=%u", cursor); + return false; + } + width = 2 + static_cast(ptr[1]) * 4; + } else if (*ptr == dex::kArrayDataSignature) { + if (remaining < 4) { + LOGW("Invalid DEX array-data payload header: cursor=%u", cursor); + return false; + } + dex::u4 length = ptr[2] | (static_cast(ptr[3]) << 16); + width = 4 + (static_cast(ptr[1]) * length + 1) / 2; + } else { + width = dex::GetWidthFromFormat(dex::GetFormatFromOpcode(dex::OpcodeFromBytecode(*ptr))); + } + if (width == 0 || width > insns_size - cursor) { + LOGW("Invalid DEX bytecode width: cursor=%u width=%zu insns_size=%u", + cursor, width, insns_size); + return false; + } + if (*ptr == dex::kPackedSwitchSignature || + *ptr == dex::kSparseSwitchSignature || + *ptr == dex::kArrayDataSignature) { + cursor += static_cast(width); + continue; + } + + auto opcode = dex::OpcodeFromBytecode(*ptr); + if ((dex::GetVerifyFlagsFromOpcode(opcode) & dex::kVerifyError) != 0) { + LOGW("Invalid DEX bytecode opcode: opcode=%s", dex::GetOpcodeName(opcode)); + return false; + } + + dex::u4 index = dex::kNoIndex; + dex::u4 index2 = dex::kNoIndex; + auto instruction = dex::DecodeInstruction(ptr); + switch (dex::GetFormatFromOpcode(instruction.opcode)) { + case dex::k20bc: + case dex::k21c: + case dex::k31c: + case dex::k35c: + case dex::k3rc: + index = instruction.vB; + break; + case dex::k45cc: + case dex::k4rcc: + index = instruction.vB; + index2 = instruction.arg[4]; + break; + case dex::k22c: + index = instruction.vC; + break; + default: + break; + } + if (!instructionIndexFits(header, dex::GetIndexTypeFromOpcode(instruction.opcode), + index, index2, dex::GetOpcodeName(instruction.opcode))) { + return false; + } + cursor += static_cast(width); + } + return cursor == insns_size; +} + static bool codeItemFits(const dex::u1 *base, const dex::Header *header, size_t file_size, dex::u4 offset) { if (offset == 0) return true; @@ -606,6 +720,9 @@ static bool codeItemFits(const dex::u1 *base, const dex::Header *header, size_t code->insns_size, file_size); return false; } + if (!instructionsFit(header, code->insns, code->insns_size)) { + return false; + } if (!debugInfoFits(base, header, file_size, code->debug_info_off)) { return false; } From c5a945cc2251792ef36f74715d23a74de2c75eb6 Mon Sep 17 00:00:00 2001 From: Shallow Date: Thu, 2 Jul 2026 12:15:43 +0800 Subject: [PATCH 09/13] Check dex preflight table size overflow --- daemon/src/main/jni/obfuscation.cpp | 61 ++++++++++++++++++++++++----- 1 file changed, 51 insertions(+), 10 deletions(-) diff --git a/daemon/src/main/jni/obfuscation.cpp b/daemon/src/main/jni/obfuscation.cpp index 19d653b80..da82c18de 100644 --- a/daemon/src/main/jni/obfuscation.cpp +++ b/daemon/src/main/jni/obfuscation.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -429,6 +430,9 @@ static bool annotationSetRefListFits(const dex::u1 *base, const dex::Header *hea return true; } +static bool checkedTableBytes(size_t start, dex::u4 count, size_t item_size, size_t file_size, + const char *name, size_t *bytes); + static bool annotationsDirectoryFits(const dex::u1 *base, const dex::Header *header, size_t file_size, dex::u4 offset) { if (offset == 0) return true; @@ -439,15 +443,26 @@ static bool annotationsDirectoryFits(const dex::u1 *base, const dex::Header *hea } const auto *directory = reinterpret_cast(base + offset); auto fields_start = static_cast(offset) + sizeof(dex::AnnotationsDirectoryItem); - auto fields_bytes = static_cast(directory->fields_size) * - sizeof(dex::FieldAnnotationsItem); - auto methods_bytes = static_cast(directory->methods_size) * - sizeof(dex::MethodAnnotationsItem); - auto parameters_bytes = static_cast(directory->parameters_size) * - sizeof(dex::ParameterAnnotationsItem); - if (fields_start > file_size || fields_bytes > file_size - fields_start || - methods_bytes > file_size - fields_start - fields_bytes || - parameters_bytes > file_size - fields_start - fields_bytes - methods_bytes) { + size_t fields_bytes; + if (!checkedTableBytes(fields_start, directory->fields_size, + sizeof(dex::FieldAnnotationsItem), file_size, "field annotations", + &fields_bytes)) { + LOGW("Invalid DEX annotations directory: offset=%u file_size=%zu", offset, file_size); + return false; + } + auto methods_start = fields_start + fields_bytes; + size_t methods_bytes; + if (!checkedTableBytes(methods_start, directory->methods_size, + sizeof(dex::MethodAnnotationsItem), file_size, "method annotations", + &methods_bytes)) { + LOGW("Invalid DEX annotations directory: offset=%u file_size=%zu", offset, file_size); + return false; + } + auto parameters_start = methods_start + methods_bytes; + size_t parameters_bytes; + if (!checkedTableBytes(parameters_start, directory->parameters_size, + sizeof(dex::ParameterAnnotationsItem), file_size, + "parameter annotations", ¶meters_bytes)) { LOGW("Invalid DEX annotations directory: offset=%u file_size=%zu", offset, file_size); return false; } @@ -503,6 +518,17 @@ static bool debugInfoIndexFits(dex::u4 index_plus_one, dex::u4 count, const char return true; } +static bool checkedTableBytes(size_t start, dex::u4 count, size_t item_size, size_t file_size, + const char *name, size_t *bytes) { + if (start > file_size || count > (file_size - start) / item_size) { + LOGW("Invalid DEX %s table: start=%zu count=%u item_size=%zu file_size=%zu", + name, start, count, item_size, file_size); + return false; + } + *bytes = static_cast(count) * item_size; + return true; +} + static bool debugInfoFits(const dex::u1 *base, const dex::Header *header, size_t file_size, dex::u4 offset) { if (offset == 0) return true; @@ -651,7 +677,22 @@ static bool instructionsFit(const dex::Header *header, const dex::u2 *insns, return false; } dex::u4 length = ptr[2] | (static_cast(ptr[3]) << 16); - width = 4 + (static_cast(ptr[1]) * length + 1) / 2; + auto element_width = static_cast(ptr[1]); + if (element_width == 0 || + (length != 0 && + element_width > (std::numeric_limits::max() - 1) / length)) { + LOGW("Invalid DEX array-data payload size: width=%zu length=%u", + element_width, length); + return false; + } + auto data_bytes = element_width * static_cast(length); + auto data_units = (data_bytes + 1) / 2; + if (data_units > remaining - 4) { + LOGW("Invalid DEX array-data payload bounds: cursor=%u units=%zu remaining=%zu", + cursor, data_units, remaining); + return false; + } + width = 4 + data_units; } else { width = dex::GetWidthFromFormat(dex::GetFormatFromOpcode(dex::OpcodeFromBytecode(*ptr))); } From ddbc04d930b4ce60a2fdf64f8c4576424661fffd Mon Sep 17 00:00:00 2001 From: Shallow Date: Sat, 4 Jul 2026 11:54:10 +0800 Subject: [PATCH 10/13] Track per-module dex obfuscation outcome --- .../kotlin/org/matrix/vector/daemon/data/FileSystem.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/FileSystem.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/FileSystem.kt index a8e672b4e..f210b46a4 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/FileSystem.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/FileSystem.kt @@ -179,6 +179,7 @@ object FileSystem { val preLoadedDexes = mutableListOf() val moduleClassNames = mutableListOf() val moduleLibraryNames = mutableListOf() + var moduleDexesObfuscated = obfuscate var isLegacy = false runCatching { @@ -245,7 +246,11 @@ object FileSystem { while (true) { val entryName = if (secondary == 1) "classes.dex" else "classes$secondary.dex" val dexEntry = zip.getEntry(entryName) ?: break - zip.getInputStream(dexEntry).use { preLoadedDexes.add(readDex(it, obfuscate)) } + zip.getInputStream(dexEntry).use { + val loadedDex = readDexResult(it, obfuscate) + preLoadedDexes.add(loadedDex.memory) + moduleDexesObfuscated = moduleDexesObfuscated && loadedDex.obfuscated + } secondary++ } } @@ -258,7 +263,7 @@ object FileSystem { if (preLoadedDexes.isEmpty()) return null // Apply obfuscation - if (obfuscate) { + if (moduleDexesObfuscated) { val signatures = ObfuscationManager.getSignatures() for (i in moduleClassNames.indices) { val s = moduleClassNames[i] From 888e725125082d5445c9911c6702616842be199e Mon Sep 17 00:00:00 2001 From: Shallow Date: Sat, 4 Jul 2026 11:54:15 +0800 Subject: [PATCH 11/13] Tighten dex preflight edges and fd ownership --- daemon/src/main/jni/obfuscation.cpp | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/daemon/src/main/jni/obfuscation.cpp b/daemon/src/main/jni/obfuscation.cpp index da82c18de..d47c5fda3 100644 --- a/daemon/src/main/jni/obfuscation.cpp +++ b/daemon/src/main/jni/obfuscation.cpp @@ -61,8 +61,10 @@ static bool isStandardDexMagic(const dex::u1 *magic) { static bool sectionFits(size_t file_size, dex::u4 offset, dex::u4 count, size_t item_size, const char *name) { if (count == 0) { - if (offset == 0) return true; - LOGW("Invalid DEX %s section: empty section has non-zero offset %u", name, offset); + // An empty section may legitimately carry a non-zero offset in DEX + // files produced by some toolchains; only flag a misaligned one. + if (offset == 0 || isAligned(offset, 4)) return true; + LOGW("Invalid DEX %s section: empty section has unaligned offset %u", name, offset); return false; } if (offset == 0 || !isAligned(offset, 4)) { @@ -892,7 +894,10 @@ static bool classDataFits(const dex::u1 *base, const dex::Header *header, size_t // Slicer's own structural checks compile out under NDEBUG, so validate the // table ranges and indexed references that CreateFullIr() will touch first. -static bool isDexSafeForSlicer(const void *dex_data, size_t mapped_size) { +// On success, *out_file_size receives the validated header file_size so the +// caller can feed slicer without re-reading the (potentially untrusted) header. +static bool isDexSafeForSlicer(const void *dex_data, size_t mapped_size, + size_t *out_file_size) { if (mapped_size < sizeof(dex::Header)) { LOGW("Invalid DEX: mapped size %zu is smaller than header size %zu", mapped_size, sizeof(dex::Header)); @@ -1034,14 +1039,25 @@ static bool isDexSafeForSlicer(const void *dex_data, size_t mapped_size) { } } + if (out_file_size != nullptr) *out_file_size = file_size; return true; } static jobject wrapSharedMemoryFd(JNIEnv *env, int fd) { auto java_fd = lsplant::JNI_NewObject(env, class_file_descriptor, method_file_descriptor_ctor, fd); + if (!java_fd) { + LOGE("Failed to construct FileDescriptor for fd=%d", fd); + close(fd); + return nullptr; + } auto java_sm = lsplant::JNI_NewObject(env, class_shared_memory, method_shared_memory_ctor, java_fd); + if (!java_sm) { + LOGE("Failed to construct SharedMemory for fd=%d", fd); + close(fd); + return nullptr; + } return java_sm.release(); } @@ -1249,13 +1265,12 @@ Java_org_matrix_vector_daemon_utils_ObfuscationManager_obfuscateDex(JNIEnv *env, return returnOriginalSharedMemory(memory, fd); } - if (!isDexSafeForSlicer(mem, mapped_size)) { + size_t dex_file_size = 0; + if (!isDexSafeForSlicer(mem, mapped_size, &dex_file_size)) { LOGW("Skipping DEX obfuscation for malformed input fd=%d", fd); munmap(mem, mapped_size); return returnOriginalSharedMemory(memory, fd); } - auto dex_file_size = - static_cast(reinterpret_cast(mem)->file_size); auto mutable_dex = std::unique_ptr(new (std::nothrow) dex::u1[dex_file_size]); if (mutable_dex == nullptr) { From 55c591f69e59b3aef0e377759ec6385150352d1f Mon Sep 17 00:00:00 2001 From: Shallow Date: Sat, 4 Jul 2026 12:08:20 +0800 Subject: [PATCH 12/13] Allow dex 038 callsite and methodhandle index types --- daemon/src/main/jni/obfuscation.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/daemon/src/main/jni/obfuscation.cpp b/daemon/src/main/jni/obfuscation.cpp index d47c5fda3..cbdd397a7 100644 --- a/daemon/src/main/jni/obfuscation.cpp +++ b/daemon/src/main/jni/obfuscation.cpp @@ -623,9 +623,19 @@ static bool instructionIndexFits(const dex::Header *header, dex::InstructionInde dex::u4 index, dex::u4 index2, const char *opcode) { switch (type) { case dex::kIndexNone: + case dex::kIndexVaries: case dex::kIndexInlineMethod: case dex::kIndexVtableOffset: case dex::kIndexFieldOffset: + // These index types do not reference the string/type/field/method + // tables that obfuscation rewrites, so slicer won't dereference an + // out-of-range entry through them. kIndexCallSiteRef and + // kIndexMethodHandleRef (DEX 038+) point at call-site/method-handle + // tables which slicer reads lazily and does not rewrite; rejecting + // them here would skip obfuscation for any module using + // invoke-custom or const-method-handle, defeating the hardening. + case dex::kIndexCallSiteRef: + case dex::kIndexMethodHandleRef: return true; case dex::kIndexStringRef: if (index < header->string_ids_size) return true; From 94af15a053300cc2e972ed64a7dce67675c72ebb Mon Sep 17 00:00:00 2001 From: Shallow Date: Sat, 4 Jul 2026 12:08:21 +0800 Subject: [PATCH 13/13] Keep class name rewrite when sibling dex skips obfuscation --- .../kotlin/org/matrix/vector/daemon/data/FileSystem.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/FileSystem.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/FileSystem.kt index f210b46a4..cd69333b8 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/FileSystem.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/FileSystem.kt @@ -179,7 +179,10 @@ object FileSystem { val preLoadedDexes = mutableListOf() val moduleClassNames = mutableListOf() val moduleLibraryNames = mutableListOf() - var moduleDexesObfuscated = obfuscate + // True when at least one module DEX had its signatures rewritten. Class-name + // rewriting must fire as long as any DEX was obfuscated, even if sibling + // DEXes without target signatures were returned unchanged. + var anyDexObfuscated = false var isLegacy = false runCatching { @@ -249,7 +252,7 @@ object FileSystem { zip.getInputStream(dexEntry).use { val loadedDex = readDexResult(it, obfuscate) preLoadedDexes.add(loadedDex.memory) - moduleDexesObfuscated = moduleDexesObfuscated && loadedDex.obfuscated + anyDexObfuscated = anyDexObfuscated || loadedDex.obfuscated } secondary++ } @@ -263,7 +266,7 @@ object FileSystem { if (preLoadedDexes.isEmpty()) return null // Apply obfuscation - if (moduleDexesObfuscated) { + if (anyDexObfuscated) { val signatures = ObfuscationManager.getSignatures() for (i in moduleClassNames.indices) { val s = moduleClassNames[i]