blueStackRoot makes Magisk Delta (Kitsune) the sole, trace-free root on BlueStacks 5 / MSI App
Player — from one file, fully automatically. Releases are grouped by the BlueStacks version they target.
Fixes #24: the pipeline died on the first step
with Invalid length for a Base-64 char array or string (or earlier The compression mode specified in the GZip header is unknown) — then still printed a green "Magisk pipeline finished". Root cause: Windows
Defender reads the embedded Magisk APK + setuid su as HackTool/PUA and strips those bytes out of the
on-disk .cmd, so FromBase64String/GZipStream choke on the damaged payload. The old AV exclusion ran
after the menu and never covered the temp work dir, so it protected nothing at the moment it mattered.
- 🛡️ AV exclusion first, and wider. New
:av_setupadds the Defender exclusions before any payload is decoded or written — the folder, the DataDir, and%TEMP%\bsr_work(where the decoded APK /su/ debugfs land). Held for the whole session, removed on clean exit; the old mid-operation:av_exclude/:av_unexcludechurn is gone. - 🔎 Payload integrity → actionable error.
Get-BlockBytesvalidates every embedded block (marker, base64 length, decode); the bootstrapsuis additionally checked by gzip magic and SHA-256. A damaged block now throws "antivirus most likely removed part of this file … add a Defender exclusion, then re-download blueStackRoot.cmd from Releases into the same folder" instead of a raw .NET exception. - 🧪 Post-write quarantine check. After the APK /
su/ debugfs are written to%TEMP%\bsr_work,Assert-Extractedconfirms they still exist and match size — catching AV that quarantines the extracted files mid-run (not just the.cmd), even when a path exclusion is in place but the verdict is behavioural. - 🏷️ Names your antivirus. On any payload-damage error the message reports the actual installed AV
(via
root/SecurityCenter2); Defender is excluded automatically, third-party products can't be, so the guidance points you at the right exclusion list. (Full "AV-proof for every scanner" is not possible — the first scan happens before this file can run — but this covers Defender end-to-end and every other case gracefully.) - ✅ No more false "finished". The batch captures the PowerShell exit code and prints the green success
line only when the pipeline actually succeeds; on failure it prints red guidance.
bsr_magisk.ps1nowexit 0s explicitly on success so%errorlevel%is trustworthy. - 📦
.gitattributespinsblueStackRoot.cmdas binary (-text -diff) so a clone can't EOL-normalize and shift the payload offsets.
Fixes #18: PREP/DATA succeed but the instance
loops on device offline forever on a slow-booting (multi-minute) low-end PC, even though the guest boots
fine. The per-instance adb transport, opened during early boot, wedges in offline (TCP socket up, adb
handshake never finalized) and a plain connect no-ops on it (already connected), so Boot-And-Wait
polled getprop over a dead transport until timeout. On a fast PC the offline window is a few seconds and
clears on its own — which is why it never reproduced there.
- 🩹 Self-healing transport. A candidate that isn't
deviceis nowdisconnect+connected to force a fresh handshake instead of re-using the wedged socket — the deterministic form of the manualkill-server; connectthat recovered it by hand.AdbShellRetry/AdbTryheal on a dropped transport too. - 📜 Host-side boot signal. A per-instance
Player.log [Ready]line is used as an adb-independent "guest booted" signal (immune to the offline-transport race) for liveness and timeout shaping. - 🧭 Liveness, not WMI-only. Relaunch fires only when nothing says the instance is alive (conf adb port
not listening AND no
Player.logactivity AND no process), so a null/blocked WMI command-line read no longer spamsretrying launch. - ⏱️ Reasonable waits. Fail fast (~90 s) when nothing is alive at all (not a slow boot); cap the
post-
[Ready]wait; otherwise tolerate multi-minute boots. - 🐞 New
debug.cmd— a standalone, read-only diagnostic that launches the instance, tests the disconnect+reconnect heal live, and writes one redactedbsr_debug_*.logso a single attached log pinpoints the failure mode. The productionblueStackRoot.cmdkeeps terminal logging only (writes no files). - 🧪 New unit tests for
Parse-AdbState+ thePlayer.logready/alive parsers (Magisk 256, engine 29, resolver 49, patch-equivalence 24; embedded blocks in sync). No Magisk APK, su, debugfs, or disk-payload changes — only the host-side adb wait/boot logic. Confirmed on the original reporter's low-end 2013 PC. - Re-embedded the updated orchestrator into
blueStackRoot.cmd.
Pure performance + test-coverage release. No rooting-behavior, Magisk APK, su, debugfs, or disk-payload changes — identical inputs produce identical outputs, proven byte-for-byte against the original algorithm.
- ⚡ Anti-tamper patch: whole-file scan ~20 s → ~0.25 s (~80×); the patch step drops from ~23 s to under 2 s.
The patch only ever flips 2 bytes, yet it used to read, scan, and rewrite the entire ~27 MB
HD-Player.exe. The 6× whole-file anchor-string scan + the.textCALL;TEST;JZscan ran byte-by-byte in interpreted PowerShell (~177 M iterations); they now use native[Array]::IndexOfto jump straight to each candidate byte (measured on a real 27 MB binary: 20,159 ms → 248 ms). - 💾 Writes 2 bytes, not 27 MB. The validated
90 90is now written in place via aFileStreamseek instead of rewriting the whole file — same output, a fraction of the disk I/O. - 📦 Reads the 21 MB single-file
.cmdonce, not 3×. The orchestrator memoizes the self-read used to extract the embedded debugfs / su / APK blobs. - 🧰 Zero new dependencies. Everything rides on the in-box Windows PowerShell / .NET runtime the tool already uses — nothing to install, nothing from BlueStacks.
- 🧪 Proven, not promised. New
tests\Run-Patch-Equivalence.ps1keeps a frozen copy of the original algorithm as an oracle and asserts the optimized engine produces byte-identical results across synthetic PEs and the realHD-Player.exe(same 1675 candidates, same validated site, same patched bytes — including a full-scale re-patch) and prints the before/after timing. Wired into CI next to the engine, resolver, and Magisk suites (all green). - 🔎 Same update-proof matching as before — dynamic PE parse + the stable
CALL;TEST;JZidiom +"Verified/Failed the disk integrity!"anchor validation. Only the iteration changed, never what is matched, so version-proofness is unchanged. - Re-embedded the optimized engine + orchestrator into
blueStackRoot.cmd.
Fixes the reproduced DATA-stage failure where progress output showed candidates=System.Object[] and adb
was called with a malformed serial like 127.0.0.1:5555 5557, producing error: unknown host service.
Get-AdbPortCandidatesnow returns a flat string list instead of one nested PowerShell array object.Boot-And-Waitnow tries candidate ports separately (127.0.0.1:5555, then127.0.0.1:5557, etc.) instead of accidentally joining them into one invalid serial.- Resolver tests now assert the candidate list shape, and CI now runs a heavier pure-unit suite for the
Magisk orchestrator covering candidate-port matrices, log redaction, adb server-port selection,
HD-Player instance matching, adb retry classifiers, and stray
suparsing. - Re-embedded the updated orchestrator into
blueStackRoot.cmd. No Magisk APK, su, debugfs, disk payload, or rooting-pipeline changes beyond the adb candidate-list fix.
Masks Windows/macOS user-profile directories in runtime and helper output so logs show
C:\Users\xxxxx\... or /Users/xxxxx/... instead of the local account name.
- Redacts normal runtime logging in
bsr_magisk.ps1andbsr_engine.ps1. - Redacts top-level failure messages so PowerShell exceptions do not print raw workspace paths.
- Redacts the
.cmdmenu path display (DataDir,Install,debugfs) and the.bstk not foundpath. - Removes hardcoded local
C:\Users\...paths from older test/dev probe scripts and derives them from the repo location instead. - Adds resolver tests for Windows, slash-style Windows, and Unix-style user-profile redaction.
- Re-embedded the updated runtime scripts into
blueStackRoot.cmd. No Magisk APK, su, debugfs, or disk payload changes.
Follow-up to the v12 reports where PREP succeeded but DATA failed with
instance 'Rvc64' did not boot / become adb-reachable within 300 s. The issue discussion showed ADB
works once the Android 11 instance is manually opened (127.0.0.1:5555 device), so the remaining weak
spot was host-side launch/wait behavior rather than the offline image prep or Magisk payload.
- 🚀 Boot wait now keeps the host launch alive.
Boot-And-Waitlogs progress every ~30s, retries theHD-Player --instance ...launch if no player process is present, and extends the wait only when the target instance is visibly alive but ADB is still not ready. The liveness check is scoped to the requested Android 11 instance, so another running BlueStacks instance cannot mask an Rvc64 launch miss. Normal boots do not get slower; dead launches still fail on the original timeout. - 🧯 Inherited shared adb port 5037 is ignored. A stale
ANDROID_ADB_SERVER_PORT=5037no longer pulls the tool back onto the shared SDK/HD-Adb conflict port. Private overrides such as15040are still honored. - ⏱️ Shutdown handling is less racy.
Kill-BlueStackswaits only as long as needed, up to 20s, for BlueStacks-owned processes to exit. This avoids a stale listener briefly holding5555and forcing the next Android 11 launch onto an alternate live port. - 🔎 Failure output is actionable. Timeout errors now include the HD-Adb server port, candidate device ports, HD-Player process count, and the last connect/getprop result, so future logs show whether launch, bind, or boot completion is the failing stage.
- 🧪 Validated on Android 11 / Rvc64 only. Live boot tested with a foreign listener on 5037; Rvc64 still
booted through the private HD-Adb server and reached
sys.boot_completed=1. Parser checks, resolver tests, synthetic tests, and embedded-sync checks all pass. No Magisk APK or disk payload changes.
Fixes Magisk reporting "Abnormal State — a su binary not from Magisk has been detected" on an instance
whose shared master Root.vhd still carried a classic/engine su at /system/xbin/su (the old
non-Magisk root method — e.g. left behind by the legacy live-E2E harness). The Magisk pipeline scrubbed
its own bootstrap su but never removed a pre-existing /system/xbin/su, and Verify only swept for the
bootstrap su's hash, so a competing su passed silently. (Git shows no commit ever changed su handling;
the v11 adb fix simply let the pipeline complete, so Magisk finally booted and flagged the leftover.)
- 🧹 Prep + Clean now scrub
/system/xbin/su(anddaemonsu) from the master, so Magisk's own su (/system/bin/su→ magisk,/sbin/su→ magisk) is the only one. Re-running Clean repairs an already-rooted instance — verified live: the stray su was removed and the instance returned to a clean state. - 🔎 Verify now FAILS on ANY competing su. It enumerates every su in the standard PATH dirs and flags
anything that isn't a symlink to magisk (new pure, unit-tested
Find-StraySu). No more silent PASS with a foreign su present. - 🧪 Live-E2E rewritten + de-footgunned.
tests/Run-Live-E2E.ps1used to root via the engine's legacy classic-su path (-Action AdbRoot) — which installs a competing/system/xbin/suand is not what the tool ships. It now drives the realbsr_magisk.ps1 -Action Autopipeline, asserts VERIFY PASS + no competing su (across a reboot), uses the correct packageio.github.huskydg.magisk, and reverts via Magisk Undo. - ✅ Tests:
Run-Resolve-Tests.ps1+7Find-StraySucases (38 total);Run-Tests.ps1(28) andCheck-Embedded-Sync.ps1green; re-embedded intoblueStackRoot.cmd.
Fixes a report where a fully-booted instance (Home visible, Magisk installed) still failed with
instance '<name>' did not boot / become adb-reachable within 300 s. Root cause was not the instance:
a system adb of a different version on the host (Android SDK platform-tools v1.0.41) and
BlueStacks' bundled HD-Adb v1.0.36 were killing each other's adb server on the shared default port
5037 ("adb server version doesn't match this client; killing…"), so the tool's getprop sys.boot_completed
calls failed forever and Boot-And-Wait timed out.
- 🛡️ Version-conflict immunity. The tool now pins BlueStacks' HD-Adb onto its own private adb server
port and only ever uses
HD-Adb.exe(never a systemadb.exe). A foreign-version adb on 5037 can no longer touch our server. Proven on this machine: with a v41 server deliberately running on 5037, HD-Adbgetpropon the private port succeeded 30/30; on the shared 5037 port it failed 0/12 with the exact reporter error; the fullBoot-And-Waitthen booted the instance end-to-end despite the v41 competitor. - 🔌 Free-port selection (no new collisions). The private port is chosen free from
15037–15057: if something already holds15037before the run — a non-adb app or a foreign-version adb — the tool steps to the next free port instead of colliding with it (and reuses its own HD-Adb server if one is already up). An explicitANDROID_ADB_SERVER_PORTalways wins. Verified live: a non-adb listener on 15037 → tool picks 15038; its own server on 15037 → reused. The private server is released when the tool exits (kill-serverin afinally), so nothing of ours lingers on the port. - 🎯 Port detection hardened.
Get-AdbPortCandidatesnow also consults the actually-bound listening port (Get-NetTCPConnection, band 5550-5900), merged after thebluestacks.confstatus.adb_port/adb_portvalues (which stay authoritative). This rescues the boot wait when the conf is stale — verified live: conf saidstatus.adb_port=5646while the instance was really on 5645, and the live scan found 5645 on 20/20 runs. - 🧪 Tests.
tests/Run-Resolve-Tests.ps1gains a deterministic seam + 3 new cases for the conf+live merge/dedup order (25 checks);Run-Tests.ps1(28) andCheck-Embedded-Sync.ps1still pass. Re-embedded intoblueStackRoot.cmd(engine + orchestrator back in sync). - 📄 No change to the rooting pipeline, the embedded Magisk APK, or any on-disk format — purely
host-side adb plumbing in
tools/bsr_magisk.ps1(+ a one-line mirror intools/bsr_engine.ps1).
Swaps the bundled Magisk for a custom build of Kitsune Mask v31 (still 31.0-kitsune, versionCode
31000) that fixes a long-standing table mismatch. Kitsune's DenyList UI/CLI wrote app entries to the
hidelist table, but external ptrace-Zygisk modules (ReZygisk / NeoZygisk) read the denylist table —
so toggling an app in Magisk's own hide list had no effect under those modules, and you had to run
magisk --sqlite "INSERT INTO denylist ..." by hand.
- 🩹 The patch (3 lines). In
native/src/core/deny/utils.cppthe deny module's default table is nowdenylistinstead ofhidelist(SuList mode still usessulist, which those modules also read). Built fromJordan231111/KitsuneMagisk@25fa2159f, a fork of1q23lyc45/KitsuneMagisk. Result: the in-app DenyList toggle now actually hides root for ReZygisk/NeoZygisk-protected apps — no SQLite editing needed. - 📦 New embedded APK. SHA-256
fac319d2de262fcfff1684e13e1a5c61c486d2a773a7a8ffcfdbfe6f763a7fd4(12,574,128 bytes — same size as the stock v31 APK, since"hidelist"→"denylist"is byte-length-neutral). Verified rebuilt, not re-signed:lib/x86_64/libmagisk64.soandlib/x86/libmagisk32.soboth differ from the stock APK. - 🔁 No blueStackRoot pipeline change. Package id (
io.github.huskydg.magisk) and the APK'slib/$ABI+assets/*.shlayout are unchanged, so the version-agnostic extract/install/undo path roots exactly as before. - 🛠️ Repo kept in sync + transparent. Re-spliced with
tools/reembed-apk.ps1(byte-level, SHA-256 round-trip); refreshedtools/magisk_databin/viatools/extract-databin.ps1; updated the embedded-APK hash asserted bytests/Check-Embedded-Sync.ps1; replaced the referenceWorking Example & Fix/MagiskMyStableBuild.apk; README "Is this safe?" now states plainly that the APK is a custom build and links the source/diff. (Live E2EVERIFY PASSto be re-confirmed on an instance.)
Re-bundles Kitsune Mask v31 using the version-tagged 31.0-kitsune (versionCode 31000) build,
replacing the commit-named build (magisk -c → 2ef8f002, versionCode 29999) shipped in v8. Same
1q23lyc45/KitsuneMagisk fork and Magisk version — only the APK bytes differ.
- 📦 New embedded APK. SHA-256
f554c9643a527cda4910e1a044a2bfabd5f034f456587bc995895092dfe9b933(12,574,128 bytes);blueStackRoot.cmdgrows ~5 KB of base64 text accordingly. - 🔁 No pipeline logic changes. The package id (
io.github.huskydg.magisk) and the APK's internallib/$ABI+assets/*.shlayout are unchanged, so the version-agnostic extract/install/undo path roots exactly as before. - 🛠️ Repo kept in sync. Re-spliced with
tools/reembed-apk.ps1(byte-level, SHA-256 round-trip verified); refreshedtools/magisk_databin/viatools/extract-databin.ps1; updated the embedded-APK hash asserted bytests/Check-Embedded-Sync.ps1; the bundled reference APK is nowWorking Example & Fix/MagiskMyStableBuild.apk. (E2EVERIFY PASSto be re-confirmed on a live instance.)
Updates the bundled Magisk to Kitsune Mask v31 (the 1q23lyc45/KitsuneMagisk fork; magisk -c →
2ef8f002, versionCode 29999), replacing Magisk Delta v27.2-kitsune-4 (HuskyDG).
- 📦 New embedded APK. SHA-256
e01648059a412fd9946a99801260dfde81c99def2512f161657faf404a280e05(12,570,028 bytes); the.cmdshrinks ~261 KB of base64 text accordingly (the new APK is ~200 KB smaller). - 🔁 No pipeline logic changes. The package id is unchanged (
io.github.huskydg.magisk) and the APK's internallib/$ABI+assets/*.shlayout is identical, so the version-agnostic extract/install/undo path roots exactly as before. (E2EVERIFY PASSshould be re-confirmed on a live instance.) - 🛠️ New tooling.
tools/reembed-apk.ps1swaps the embedded APK at the byte level and verifies it round-trips by SHA-256;tools/extract-databin.ps1refreshes thetools/magisk_databin/reference set;tests/Check-Embedded-Sync.ps1now also asserts the embedded APK's SHA-256 (CI-enforced).
A ground-up rewrite: the old junction / integrity-bypass method is gone. One self-contained
blueStackRoot.cmd now roots the latest BlueStacks with real Magisk — no downgrade, no traces.
Proven end-to-end (VERIFY PASS) on Android 9 / 11 / 13: su → uid=0, the Magisk app shows
Installed, zero bootstrap-su left behind.
Highlights
- 📦 One file, Magisk included. The genuine Magisk Delta (Kitsune) v27.2-kitsune-4 APK is now
embedded inside the
.cmd— nothing else to download (older releases shipped Magisk separately). - 🆕 Roots the latest BlueStacks (5.22.169) without downgrading — a one-byte HD-Player patch clears the "Android system illegally tampered with" shutdown.
- 🔑 The breakthrough: Magisk only comes alive once
/data/adb/magiskis populated — the step the GUI Install to System silently leaves empty. Filling it from the APK is what makes Magisk actually work. - 🧹 Magisk as the sole root, no traces: every bootstrap/engine su is erased and stock files restored, so the app's "Abnormal State" warning never appears.
- 🪟 Multi-instance: run a rooted and an unrooted instance side by side (per-instance gate on the shared master disk).
- ⚡ Fast, width-aware menu with no hardcoded paths or ports (resolved from the registry + each
instance's
bluestacks.conf).
Menu: 1/2/3 root Android 9 / 11 / 13 · 4/5/6 undo each · 7 full host scrub · 8 custom folder ·
0 exit.
Each option roots one instance — the one of that Android type you opened most recently (resolved from
Player.log). So launch the exact instance you want first; with several clones of the same type it targets the last one you opened, not all of them.
Removed: Android 7 (32-bit — can't run the 64-bit binaries) and the entire legacy junction / DiskRW
method.
How it works under the hood (technical)
- Offline
Root.vhdedit via an embeddeddebugfswrites Magisk's/systempayload + a hijackedbootanim.rcstraight into the VHD — no ext4 driver, noDiskRW, no ramdisk. - Version-proof patch: the HD-Player byte-scan follows RIP-relative
LEAs to the disk-integrity anchor strings, so it locks onto the right call site across versions (74 5B → 90 90). - Transient bootstrap su (
bsr_su, delivered with aCAP_FSETIDbind-mount trick) writes the root-owned/data/adb/magisk, then is 100% removed. - Per-instance gate: the shared master
Root.vhdstaysReadonly;bootanim.rcruns Magisk only on instances carrying/data/adb/.bsr_root, so unrooted instances stay clean and boot alongside rooted ones. - Robust adb: per-instance port from
bluestacks.conf(re-read live), pinned127.0.0.1:<port>transport, reconnect/retry across boot — never a hardcoded 5555. - Tested & RE'd: the offline write is unit-tested byte-for-byte, CI runs real Windows tests, and the
closed-source
BstkRooter.exeis reproduced byte-exact inrecovered/.
Full write-up: docs/BLUESTACKS_ROOTING_DEEP_DIVE.md ·
runbook: docs/RUNBOOK.md.
bluestacks5.21_Major and earlier use the legacy junction / integrity-bypass method and support only that
BlueStacks era and below. On 5.22.169, use v7 above.