MacOS Bluestacks Air support - #68
Open
AndnixSH wants to merge 5 commits into
Open
Conversation
Air needs a different rooting method than Windows, because the one this tool already has cannot work there. Windows BlueStacks ships a guest `su` that `bst.instance.<name>.enable_root_access` merely unlocks; Air ships no `su` at all -- verified against the live guest and offline against the image's ext4 (/system/bin, /system/xbin, /system/app, /system/priv-app and the ramdisk are all clean). The conf keys still exist and the player still resets bst.feature.rooting to "0" on every launch, but nothing consumes them, and Air's init even tries to import /init.superuser.rc and logs that it does not exist. Setting those flags on Air does nothing. So rooting Air means adding an `su`: convert Contents/img/Root.qcow2 with BlueStacks' own bundled qemu-img, inject a 223-byte statically-linked aarch64 su (mode 06755, uid 0) with debugfs through e2fsprogs' ?offset= syntax, run e2fsck, repack, and write it back. SELinux is disabled on Air, so a classic setuid binary is sufficient -- no policy patching. The binary is generated rather than vendored or downloaded (macos_su.py carries its full assembly listing), which keeps a GPLv3 payload out of the tree and avoids trusting a third party for the most security-sensitive bytes here. Two properties of the image being inside the app bundle drive the design: - Rooting is install-wide. Every Air instance boots the same image; a per-instance Root.qcow2 is ignored (tested), so the UI says so and the toggle collapses to one pass no matter how many instances are ticked. - Writing it needs macOS App Management, not root. The file is rw-rw-rw-, and App Management is granted to an *application*, so an elevated helper does not inherit it -- a root cp through osascript is refused where a direct write succeeds. The direct write is therefore tried first and elevation is only a fallback, so the normal flow shows no password prompt at all. Root and the in-guest hosts block share one image, so they share one state record. Per-feature state files would invalidate each other: both are validated by fingerprinting the image, and any edit rewrites it, so blocking trackers on a rooted install read as un-rooted and removing the block then "restored" over the root. One record, one fingerprint, re-stamped by whichever edit ran last. Undo restores the pristine backup byte-for-byte rather than editing the change back out. Root.qcow2 is a sealed bundle resource, so any edit invalidates the app's code signature; restoring identical bytes is the only thing that repairs it. The backup is kept until the last change is undone and refreshed only while the image is unmodified, so a BlueStacks update cannot leave a stale one that would "undo" to a previous build. Platform coupling elsewhere was contained rather than spread: detection still goes through registry_handler, which delegates to macos_locator; admin.py and root_persistence.py degrade to no-ops (the read-only conf lock cannot work on POSIX anyway, since BlueStacks saves by rename()); and the UI branches on the installation's air_mode flag rather than on the host platform. Modules, the R/W toggle and the Magisk buttons are hidden on Air -- it has no .bstk files and the Magisk installer drives Windows VHDs through bundled .exe tools. Verified end-to-end on a real install: `su -c id` returns uid=0(root), `which su` resolves via the guest PATH, a blocked domain resolves to 127.0.0.1 while a control domain does not, and undo leaves the image byte-identical with the code signature valid again. CI now runs the suite on macos-14 alongside windows-latest; pywin32 is marked Windows-only so pip install works on macOS.
The grid still rendered an R/W column reading "Off" and a "Manager app" column reading "-" on BlueStacks Air, and the hint under the Root group offered a choice between "Native Root" and "Manager Root". None of that is true there: Air has no R/W state to toggle and no Magisk path, and the Manager Root button is not even on screen. "Off" reads as a thing you have switched off rather than a thing that does not exist here, and naming a button that isn't there sends people hunting for it. Both columns are now dropped on Air rather than filled in, and the hint says what the one available action actually does -- including that a BlueStacks update replaces the image and takes root with it. Also rebuild the grid when the Air flag arrives after rows already exist, which only happens on a rescan that finds a different install; detection normally sets it before the first load. Caught by rendering the real UI rather than trusting the widget states.
On macOS the Instances and Privacy tabs showed grey panels behind every
QGroupBox that are not there on Windows. The cause is that QMacStyle paints
a group box's *frame* as a filled panel in Qt's lightGray (#d3d3d3), and the
generic `QWidget { background-color }` rule does not override it -- setting
`background-color: transparent` changes nothing, because it is the frame
being drawn. Only removing the border clears it, which then clips the group
title, so the frame is restated explicitly in the same flat rounded style
the buttons already use, with the title positioned into a top margin.
Fixing that exposed a second artefact on the same tabs: an *empty* checkbox
is drawn at #f4f4f4 against the light theme's #f3f3f3 page, so it is
invisible -- you could not see which instances were tickable until one
turned blue. Giving the unchecked indicator a border fixes it. The rule is
:unchecked-scoped on purpose: styling every state hands the box to the QSS
box model, which paints a flat rectangle and loses the checkmark glyph
(verified by rendering -- the checked box came out white and empty).
Both rules are appended only on macOS. Windows already renders these
correctly, it is this project's primary platform, and it cannot be verified
from here, so its stylesheet is left byte-for-byte unchanged and a test
asserts that.
Measured before and after by rendering the real macOS style: lightGray went
from ~15% of both tabs to 0%, in light and dark.
Detection only checked that BlueStacks.app existed, so the Intel/x86 macOS build would have been reported as Air. That does not fail loudly: it is a different product inside -- VirtualBox-based, VHDX disks, x86 Android guest -- so macos_root would convert an image it does not understand and write an *aarch64* su into an x86 guest, leaving the user with a modified system image and an su that dies with an exec-format error. Read the player binary's Mach-O architecture instead, handling universal binaries, and fail closed unless arm64 is present -- including when the header cannot be read at all. An Intel user gets "no installation found" plus a log line saying why, which is a better outcome than a silent edit. Documented in the README as a stated limitation rather than an omission, with its own row in the compatibility table.
Contributor
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
The Windows job failed on test_bundled_tool_requires_executability. The test asserted that a present-but-not-executable file is rejected, which only holds on POSIX: on Windows os.access(path, X_OK) is equivalent to R_OK, so every existing file reads as executable and the distinction cannot be expressed. Split the case in two. The positive half -- an executable tool is found -- still runs everywhere, and only the rejection half is skipped on Windows, with the reason recorded on the marker. This is a test-only defect. The production check is sound on both platforms: os.access is the right question to ask on macOS, where the bundle really can contain non-executable files, and it is a harmless no-op on Windows, which is also how adb_handler.find_adb has always behaved.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
I saw that someone had asked about a rooting method for BlueStacks Air. Luckily, I had a Mac Mini with an ARM processor, so I decided to try to implement it with an AI.
I did not add Magisk support since Kyubi only supports x86 and x86_64.
The x86 version of BlueStacks for MacOS is not currently supported because I don't have a Mac computer with an x86 processor. I will set up a Hackintosh on my gaming PC later.
Copied from AI:
BlueStacks Air needs a different rooting method: Unlike Windows BlueStacks, Bluestacks Air ships no guest
suforenable_root_accessto unlock (verified against the live guest and offline against the image's ext4). The player still resetsbst.feature.rootingto 0, but nothing consumes it - so the conf flip does nothing there.This injects one instead:
qemu-img(BlueStacks' own) convertsRoot.qcow2,debugfswrites a small statically-linked aarch64su(mode 06755, uid 0), then e2fsck + repack. SELinux is disabled on Air, so setuid is enough. The binary is generated, not vendored or downloaded.Windows is unchanged. Detection still goes through
registry_handler; the macOS locator is a delegate. Tests pin their platform branch instead of inheriting the host, and the macOS theme fix is scoped with a test asserting the Windows stylesheet is untouched. CI now runs macos-14 alongside windows-latest.Needs
brew install e2fsprogson macOS;pywin32is now marked Windows-only.Verified on a real install:
su -c id → uid=0(root), and undo restores the original image byte-for-byte.Known limits: root is install-wide (all instances share one image), a BlueStacks update removes it, and Modules/R-W/Magisk are hidden on Air. While rooted, the app bundle's code signature doesn't validate - inherent to modifying a sealed resource; undo repairs it.
Proof of concept: