From 1e847f29035acd2465a121046a1d4c389d289382 Mon Sep 17 00:00:00 2001 From: qiyinxi Date: Sat, 7 Nov 2026 18:35:31 +0100 Subject: [PATCH 1/2] =?UTF-8?q?=E5=AE=A2=E4=BD=93=E9=A9=B1=E5=8A=A8?= =?UTF-8?q?=E5=BA=93=E8=A1=A5=E4=B8=81=EF=BC=9A=E4=BF=AE=E6=89=B9=E9=87=8F?= =?UTF-8?q?=E6=8F=8F=E8=BF=B0=E7=AC=A6=E6=9B=B4=E6=96=B0=E9=87=8C=E6=A8=A1?= =?UTF-8?q?=E6=9D=BF=E8=B7=A8=E7=BB=91=E5=AE=9A=E5=86=99=E9=94=99=E6=A0=BC?= =?UTF-8?q?=E5=AD=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 镜像自带的 libvulkan_enc.so 里,doEmulatedDescriptorBufferInfoWriteFromTemplate 和 doEmulatedDescriptorBufferViewWriteFromTemplate 跨到下一个绑定以后,用 table[currBinding][dstArrayElement + i] 写,应为回卷后的 arrOffset(同文件写图像的那个函数是对的)。 一个模板条目连写几个相邻绑定时(World of Airports 的 Unity GPU 蒙皮:descriptorCount=4 写第 0–3 号), 后面几个绑定没写上、反而越界写,计算着色器拿着没写过的存储缓冲描述符派发,宿主显卡卡死, 模拟器报 VK_ERROR_DEVICE_LOST 退出。只在 VulkanBatchedDescriptorSetUpdate 开着时走这条路。 vkfix 加第 5 步,两处等长替换(0x1FED62 14 字节、0x1FEE2C 4 字节),下标改为「下一个 arrOffset 减一」。补丁库 sha256 9e5ba745…;宿主缓存名换成 libvulkan_enc.khrfix-bindlock2-dstpl1.so, 不和 AUTO-MAS 那份(d5258af6…,没有这处修复)互相覆盖。 测试:unicorn 实跑两个函数(原版跨绑定写错、越界;改后 4 种写法全对);单测核补丁点字节、 长度、范围和不重叠,真库比对 AUTO-MAS 那份加上两处补丁点;实机在 flow-b 开着批量更新进 因斯布鲁克,修复前 4 次 4 退,修复后不退。 Refs ModAVD/ModAVD#131 --- internal/emu/vulkanfix.go | 2 +- internal/emu/vulkanfix_test.go | 2 +- internal/vkfix/vkfix.go | 40 +++++++++++++++++---- internal/vkfix/vkfix_test.go | 63 +++++++++++++++++++++++++++++++++- 4 files changed, 98 insertions(+), 9 deletions(-) diff --git a/internal/emu/vulkanfix.go b/internal/emu/vulkanfix.go index 9fb954f..2de1fa5 100644 --- a/internal/emu/vulkanfix.go +++ b/internal/emu/vulkanfix.go @@ -441,7 +441,7 @@ func vulkanFixStatus(status string) (kind, rest string) { return "", "" } -// patchedVulkanLibrary is the host cache \guest-patches\libvulkan_enc.khrfix-bindlock2.so, made when missing or +// patchedVulkanLibrary is the host cache \guest-patches\, made when missing or // bad: pull the guest's own library, check its hash, patch, check the result's hash (vkfix.Patch), write it. "" // when any step fails (logged, nothing mounted). Host I/O errors (disk full, a virus scanner holding the file) are // warnings too. diff --git a/internal/emu/vulkanfix_test.go b/internal/emu/vulkanfix_test.go index b2f72c1..13e4b94 100644 --- a/internal/emu/vulkanfix_test.go +++ b/internal/emu/vulkanfix_test.go @@ -419,7 +419,7 @@ func TestMountVulkanFix(t *testing.T) { func TestVulkanFixConstants(t *testing.T) { // what vulkanfix.sh is called with and what it expects (u= in the script) if vkfix.UploadPath != "/data/local/tmp/vulkanfix-upload.so" || vkfix.GuestPath != "/vendor/lib64/libvulkan_enc.so" || - vkfix.CacheName != "libvulkan_enc.khrfix-bindlock2.so" { + vkfix.CacheName != "libvulkan_enc.khrfix-bindlock2-dstpl1.so" { t.Error("vkfix constants changed") } } diff --git a/internal/vkfix/vkfix.go b/internal/vkfix/vkfix.go index 2dba70f..3eaefac 100644 --- a/internal/vkfix/vkfix.go +++ b/internal/vkfix/vkfix.go @@ -1,7 +1,7 @@ -// Package vkfix builds the patched guest Vulkan encoder (bindlock2, includes khrfix) from the image's own -// /vendor/lib64/libvulkan_enc.so on the user's machine; the patched file is never distributed. +// Package vkfix builds the patched guest Vulkan encoder (bindlock2, includes khrfix, plus the template rollover fix) +// from the image's own /vendor/lib64/libvulkan_enc.so on the user's machine; the patched file is never distributed. // -// Byte-for-byte port of the companion Vulkan fix implementation: khrfix jumps for +// Steps 1 and 2 are a byte-for-byte port of the companion Vulkan fix implementation: khrfix jumps for // vkUpdateDescriptorSetWithTemplateKHR, and lock wrappers around // ResourceTracker::on_vkBindImageMemory(2) placed in the freed dynCheck function body. // @@ -15,6 +15,12 @@ // drop the lock and, on the marker, call the encoder with the original exit's arguments. The lock is never held // across a host round trip (version 1 did, and deadlocked against VulkanQueueSubmitWithCommands' global sequence // number in 3 of 10 cold boots). +// 3. template rollover (11-07, World of Airports device lost; not in the companion implementation): with batched +// descriptor set updates, doEmulatedDescriptor{BufferInfo,BufferView}WriteFromTemplate index the table with +// dstArrayElement + i after rolling over into the next binding, instead of the rolled-over arrOffset. An entry +// spanning bindings leaves them unwritten and writes past their arrays (the ImageInfo variant is right); a +// compute dispatch then uses never-written storage buffer descriptors and the host GPU hangs (VK_ERROR_DEVICE_LOST). +// Two same-length edits make both use the register that holds the next arrOffset, minus one. package vkfix import ( @@ -31,14 +37,29 @@ import ( const ( // The image's library (API 34 google_apis x86_64) and the patched one. OrigSHA256 = "898c0bbc38077e4b19d112f6c66e2970f1e015fc440fae091974b8f06e40fefb" - FixSHA256 = "d5258af6244679fd5ce50245b7dcc1f645d971a04f7e64fc9872fcfb9faa7ecb" + FixSHA256 = "9e5ba7452b7d834da575704b19bfa36bb2a4cf4e302fca8737629841e3ae736a" // Where the guest library lives, and the upload path vulkanfix.sh expects (u= in the script). GuestPath = "/vendor/lib64/libvulkan_enc.so" UploadPath = "/data/local/tmp/vulkanfix-upload.so" - // File name of the cached patched library in \guest-patches. - CacheName = "libvulkan_enc.khrfix-bindlock2.so" + // File name of the cached patched library in \guest-patches. Not the companion's khrfix-bindlock2 name: its + // library (sha256 d5258af6…) lacks the template rollover fix, and the two would overwrite each other's cache. + CacheName = "libvulkan_enc.khrfix-bindlock2-dstpl1.so" ) +// templateSites are the template rollover fix (3. above). In both loops the register written by the rollover cmov +// holds the next arrOffset (1 after a rollover), so it minus one is the element to write. +var templateSites = []struct { + va int + orig, fix []byte + name string +}{ + // lea rsi,[r13*2+0]; add rsi,r13; mov r14d,edx -> lea rsi,[r13+r13*2]; lea r14d,[rbx-1]; nopw 0(rax,rax) + {0x1FED62, mustHex("4a8d346d00000000 4c01ee 4189d6"), mustHex("4b8d746d00 448d73ff 0f1f440000"), + "doEmulatedDescriptorBufferInfoWriteFromTemplate"}, + // lea r14d,[rcx+r10] -> lea r14d,[rdx-1] + {0x1FEE2C, mustHex("468d3411"), mustHex("448d72ff"), "doEmulatedDescriptorBufferViewWriteFromTemplate"}, +} + // Virtual addresses, from bindlock.py (see the meaning there). const ( textDelta = 0x1000 // .text: vaddr 0x1fcee0, file offset 0x1fbee0 @@ -259,6 +280,13 @@ func Patch(original []byte) ([]byte, error) { } } + // 5. the template rollover fix + for _, s := range templateSites { + if err := put(s.va, s.fix, s.orig, s.name); err != nil { + return nil, err + } + } + if digest := SHA256Hex(data); digest != FixSHA256 { return nil, errs.Newf(errs.Unsupported, "改完的库 sha256 %s 不是预期值", digest) } diff --git a/internal/vkfix/vkfix_test.go b/internal/vkfix/vkfix_test.go index ff4f450..6e43e03 100644 --- a/internal/vkfix/vkfix_test.go +++ b/internal/vkfix/vkfix_test.go @@ -29,8 +29,56 @@ const ( goldenJmpKhr = "e9ebe9ffff" goldenJmpDyn = "e9cb200200" goldenExitMagic = "b83253414d5b415c415d415e415f5dc3" + + // the companion's khrfix + bindlock2 library (no template rollover fix) + companionFixSHA256 = "d5258af6244679fd5ce50245b7dcc1f645d971a04f7e64fc9872fcfb9faa7ecb" ) +// The template rollover sites (checked by running both functions under unicorn, original and patched, 11-07): +// same length as the original, inside .text, not overlapping each other or any other patched range. +func TestTemplateSites(t *testing.T) { + golden := map[int][2]string{ + 0x1FED62: {"4a8d346d000000004c01ee4189d6", "4b8d746d00448d73ff0f1f440000"}, + 0x1FEE2C: {"468d3411", "448d72ff"}, + } + if len(templateSites) != len(golden) { + t.Fatalf("%d template sites, want %d", len(templateSites), len(golden)) + } + others := [][2]int{ + {khrEntry, khrEntry + 5}, {khrDyn, khrDynEnd}, {exitBind, exitBind + len(exitBindOrig)}, + {exitBind2, exitBind2 + len(exitBind2Orig)}, {fwdBind, fwdBind + 5}, {fwdBind2, fwdBind2 + 5}, + {fwdBind2KHR, fwdBind2KHR + 5}, + } + for _, s := range templateSites { + g, ok := golden[s.va] + if !ok { + t.Errorf("%s: unexpected site 0x%x", s.name, s.va) + continue + } + if hex.EncodeToString(s.orig) != g[0] || hex.EncodeToString(s.fix) != g[1] { + t.Errorf("%s (0x%x): %x -> %x, want %s -> %s", s.name, s.va, s.orig, s.fix, g[0], g[1]) + } + if len(s.orig) != len(s.fix) { + t.Errorf("%s: %d bytes replaced by %d", s.name, len(s.orig), len(s.fix)) + } + if s.va < 0x1fcee0 || s.va+len(s.fix) > 0x1fcee0+0x1e951b { + t.Errorf("%s: 0x%x outside .text", s.name, s.va) + } + for _, o := range append(others, func() (r [][2]int) { + for _, u := range templateSites { + if u.va != s.va { + r = append(r, [2]int{u.va, u.va + len(u.fix)}) + } + } + return + }()...) { + if s.va < o[1] && o[0] < s.va+len(s.fix) { + t.Errorf("%s (0x%x) overlaps 0x%x-0x%x", s.name, s.va, o[0], o[1]) + } + } + } +} + func readOrSkip(t *testing.T, path string) []byte { t.Helper() b, err := os.ReadFile(path) @@ -57,8 +105,21 @@ func TestPatchRealLibrary(t *testing.T) { if SHA256Hex(got) != FixSHA256 { t.Fatalf("patched sha256 %s, want %s", SHA256Hex(got), FixSHA256) } + // the companion's library has steps 1 and 2 only: equal to ours once the template sites are applied to it + if SHA256Hex(want) != companionFixSHA256 { + t.Fatalf("%s sha256 %s, want %s", fixLib, SHA256Hex(want), companionFixSHA256) + } + for _, s := range templateSites { + copy(want[s.va-textDelta:], s.fix) + } if !bytes.Equal(got, want) { - t.Fatalf("patched library differs from %s", fixLib) + t.Fatalf("patched library differs from %s plus the template sites", fixLib) + } + for _, s := range templateSites { + off := s.va - textDelta + if !bytes.Equal(keep[off:off+len(s.orig)], s.orig) || !bytes.Equal(got[off:off+len(s.fix)], s.fix) { + t.Errorf("%s (0x%x): original %x, patched %x", s.name, s.va, keep[off:off+len(s.orig)], got[off:off+len(s.fix)]) + } } if !bytes.Equal(orig, keep) { t.Fatal("Patch modified its input") From 45fad4b337858d16e6852014e527ab3acd48416d Mon Sep 17 00:00:00 2001 From: qiyinxi Date: Sat, 7 Nov 2026 16:46:10 +0100 Subject: [PATCH 2/2] =?UTF-8?q?ARM=20=E8=BD=AC=E8=AF=91=E5=B1=82=E8=A1=A5?= =?UTF-8?q?=E4=B8=81=EF=BC=9A=E5=BC=80=E6=9C=BA=E6=97=B6=E6=8A=8A=20HINT?= =?UTF-8?q?=20=E8=A7=A3=E7=A0=81=E8=A1=A5=E4=B8=81=E5=92=8C=E5=AE=A2?= =?UTF-8?q?=E4=BD=93=E9=A9=B1=E5=8A=A8=E5=BA=93=E8=A1=A5=E4=B8=81=E4=B8=80?= =?UTF-8?q?=E8=B5=B7=E6=8C=82=E4=B8=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit World of Airports 3.7.3(Unity 6000.3)在 libndk_translation.so 里撞上 SEVL(0xd50320bf)抛 SIGILL, 启动约 2 秒闪退。Google 的转译层 0.2.3 把 HINT 空间里的 WFE、WFI、SEV、SEVL 当成未定义指令, 按 ARM 规范它们可以按 NOP 执行。 - 新增 ndkfix:在本机从镜像自带的 /system/lib64/libndk_translation.so(a2086fbc…)生成补丁版, 三套解码器(重优化器、轻转译器、解释器)各改一处 6 字节(jbe rel32 → 6 字节 NOP), 其余未定义编码照旧 SIGILL。build-id、动态符号、GOT、数据区都不动,执行桥的门控和 fork 保护照常。 补丁库 sha256 3efa8977…,只在本机生成,不分发。 - 新增 guestpatch,把客体驱动库补丁和转译层补丁做成同一种 profile;客体脚本 libfix.sh 在一次 root 运行里先只读检查全部、有要上传的就一个都不挂,再逐个 bind mount、核哈希,只要新挂上了 任何一个就 setprop ctl.restart zygote 一次。原来的 vulkanfix 编排改成 guestfix。 - 选补丁按实例自己的配置(inst.Config),缺 abi.type 时按 x86_64 处理,不再重读 config.ini。 测试:ndkfix 真库逐字节比对、补丁点 golden、错误输入;libfix.sh 用 Git 的 sh 加假根目录跑 (两组都挂只 setprop 一次、有上传就不挂、镜像不对、目标不存在、缓存坏、挂载失败、已挂); guestfix 用假客体跑多 profile 的 launched / owed / adopted。变异检查:去掉输入哈希检查、少改一处 补丁点、去掉「有上传就不挂」、每组各 setprop、owed 时不过闸,单测都会失败。实机在 flow-b 冷启动, 两个补丁一次挂上、zygote 只重启一次,WoA 不再出 0xd50320bf,走到登录界面和机场。 Refs ModAVD/ModAVD#131 Refs ModAVD/ModAVD#130 --- README.en.md | 6 +- README.md | 6 +- internal/cli/commands.go | 2 +- internal/cli/format.go | 4 +- internal/cli/format_test.go | 6 +- internal/emu/afterboot.go | 23 +- internal/emu/bridge.go | 2 +- internal/emu/guestfix.go | 667 ++++++++++++++++++++++++ internal/emu/guestfix_test.go | 669 +++++++++++++++++++++++++ internal/emu/start.go | 35 +- internal/emu/start_test.go | 6 +- internal/emu/types.go | 7 +- internal/emu/vulkanfix.go | 533 -------------------- internal/emu/vulkanfix_test.go | 462 ----------------- internal/guest/guest.go | 4 +- internal/guest/guest_test.go | 10 +- internal/guest/libfix_sh_test.go | 352 +++++++++++++ internal/guest/scripts/libfix.sh | 115 +++++ internal/guest/scripts/vulkanfix.sh | 45 -- internal/guestpatch/guestpatch.go | 66 +++ internal/guestpatch/guestpatch_test.go | 56 +++ internal/mcp/mcp_test.go | 2 +- internal/mcp/tools.go | 8 +- internal/ndkfix/ndkfix.go | 165 ++++++ internal/ndkfix/ndkfix_test.go | 267 ++++++++++ internal/ui/web/dev/mock.js | 4 +- internal/ui/web/js/panels.js | 2 +- internal/vkfix/profile.go | 27 - 28 files changed, 2423 insertions(+), 1128 deletions(-) create mode 100644 internal/emu/guestfix.go create mode 100644 internal/emu/guestfix_test.go delete mode 100644 internal/emu/vulkanfix.go delete mode 100644 internal/emu/vulkanfix_test.go create mode 100644 internal/guest/libfix_sh_test.go create mode 100644 internal/guest/scripts/libfix.sh delete mode 100644 internal/guest/scripts/vulkanfix.sh create mode 100644 internal/guestpatch/guestpatch.go create mode 100644 internal/guestpatch/guestpatch_test.go create mode 100644 internal/ndkfix/ndkfix.go create mode 100644 internal/ndkfix/ndkfix_test.go delete mode 100644 internal/vkfix/profile.go diff --git a/README.en.md b/README.en.md index c9e19b5..8900907 100644 --- a/README.en.md +++ b/README.en.md @@ -47,7 +47,7 @@ Both programs share the implementation under `internal/`: | `emu` | Start, post-boot tuning, first-boot initialization, execution bridge, shutdown, status, computer check | `manager.py`, `precheck.py` | | `adb` | Private adb server, re-registration, emulator console | `host.py` | | `win` | Processes, command lines, ports, launching detached from the process tree, file locks | `host.py`, `windows/process.py` | -| `vkfix`, `apk`, `guest` | Guest Vulkan driver patch, APK manifest, scripts pushed into the guest | `vulkanfix.py`, `res/avd/guest/` | +| `vkfix`, `ndkfix`, `guestpatch`, `apk`, `guest` | Guest Vulkan driver patch, ARM translation layer patch (WFE / WFI / SEV / SEVL as NOP) and the list of both, APK manifest, scripts pushed into the guest | `vulkanfix.py`, `res/avd/guest/` | | `cli`, `mcp`, `ui` | The CLI, MCP, and the manager's HTTP server and pages | — | `internal/guest/scripts/*.sh` and the scripts under AUTO-MAS `res/avd/guest/` are the same files; change both together. @@ -159,7 +159,7 @@ On Apple Silicon macOS, downloads and imports use Android 15 Google Play `arm64- - Shut down every instance of the root folder before you change it. Do not add a root folder that sets `portBase` to AUTO-MAS. - The emulator, the adb server and logcat are all launched through an intermediate process that exits immediately, so they are not in modavd's process tree: if the caller ends modavd (Ctrl+C, the MCP client disconnects), the emulator is not taken down with it. - A separate `tools\modavd.exe` process writes logcat to `logs\logcat-mas_-