Skip to content

Rebuild Privacy around BlueStacks' own ad switches#63

Merged
RobThePCGuy merged 3 commits into
masterfrom
feature/privacy-ad-settings
Jul 24, 2026
Merged

Rebuild Privacy around BlueStacks' own ad switches#63
RobThePCGuy merged 3 commits into
masterfrom
feature/privacy-ad-settings

Conversation

@RobThePCGuy

Copy link
Copy Markdown
Owner

Why

The guest hosts block could never stop BlueStacks' advertising. Those ads are served by HD-Player.exe on Windows, not by the Android guest.

Evidence, measured live on 5.22.250.1015 with a fresh instance:

  • With the Android guest completely powered off, HD-Player still held open connections to googlesyndication, inmobi, rubiconproject and adnxs. There is no guest for a hosts file to affect.
  • Process attribution was unambiguous: HD-Player had 217 established remotes; BlueStacksAppplayerWeb, BstkSVC and BlueStacksServices had 0 each. The guest's own netstat showed ~19, all Google/GMS/cloud.bluestacks.com.
  • Applying the block changed nothing: 40 ad/tracker endpoints before, 40 after.

BlueStacks ships explicit switches for this in bluestacks.conf. Turning them off, same rig, same instance:

measurement guest hosts block config switches
ad/tracker endpoints 40 (no change) 0
unique remote IPs 151 → 133 151 → 14

What remained was purely Play, GMS, Firebase and fonts. The emulator stayed healthy.

What changed

New ad_settings.py, and the Privacy tab becomes a toggle for it.

Version resilience was the design constraint, since a hardcoded key list silently rots across builds. It discovers keys by concept pattern (programmatic_ads, send_*_stats, auto_upload) against whatever config is present, so a renamed or newly added switch is covered with no code change. On this build it found 23 switches where a hand-written list had 19, catching send_auto_record_stats, send_usage_state_stats and show_nowbux_rewards_red_dot_onboarding.

Three gates keep pattern matching safe, so a rename can never make it flip something harmful:

  1. must match a concept pattern,
  2. must not match an exclusion (root, adb, *_preference, and the inverted disable/skip keys),
  3. must be a boolean "0"/"1" value, which is what keeps ids like android_google_ad_id out of scope regardless of naming.

Originals are recorded per key. BlueStacks selectively reverts on start (bst.enable_* stick, most bst.feature.* are put back including four stats beacons), so status() reports that drift and the tab offers a re-apply, plus an optional read-only pin.

The hosts block stays as the in-guest tracker control, with its real defects fixed:

  • Gated on a patched engine. Editing the guest system image trips BlueStacks' tamper check and shuts the instance down mid-session. Root is not required, any modification does it. The Magisk tab already gated on this; Privacy did not, so the button shipped a boot-killer.
  • Subdomains listed explicitly. A hosts file has no wildcards, so doubleclick.net did nothing for cm.g.doubleclick.net or pagead2.googlesyndication.com, which is most real ad traffic.
  • rtbhouse.net dropped (it does not resolve) for rtbhouse.com and the esp.rtbhouse.com endpoint actually observed.
  • Docstring no longer claims it blocks the player's ads.

Verification

Live on a fresh BlueStacks + fresh Android 13 instance, not just unit tests:

claim result
ads stop ad-tech endpoints 40 → 0, IPs 151 → 15
restore is exact config byte-identical to pristine after remove()
pin holds the reverts 23/23 off, 0 reverted after a real BlueStacks start
pin does not break BlueStacks instance boots normally, root intact

247 tests green, ruff clean. New tests use the real observed key set as a fixture, including the near-misses that must not be touched (bst.enable_adb_access contains "ad"; android_google_ad_id is an id; *_preference controls whether the user's own toggle is visible; skipNowggLogin is inverted).

The guest hosts block could never stop BlueStacks' advertising. Those ads are
served by HD-Player.exe on Windows, not by the Android guest: a live capture on
5.22.250.1015 showed the player holding connections to googlesyndication,
inmobi, rubiconproject and adnxs while the guest was completely powered off.
Applying the block left the player's ad endpoints unchanged (40 before, 40
after). BlueStacks ships explicit switches for this in bluestacks.conf, and
turning them off measured 40 endpoints to 0 on the same rig, with only Play,
GMS, Firebase and fonts left.

Add ad_settings.py to drive those switches, and make the Privacy tab a toggle
for them. To survive version updates it discovers keys by concept pattern
(programmatic_ads, send_*_stats, auto_upload) instead of a fixed list, so a
renamed or newly added switch is still covered. It found 23 switches where a
hand-written list had 19. Three gates keep that safe: the key must match a
concept pattern, must not match an exclusion (root, adb, *_preference, and the
inverted disable/skip keys), and must be a boolean 0/1 value, which is what
keeps ids such as android_google_ad_id out of scope no matter how they are
named. Originals are recorded per key, verified live as a byte-identical
restore of the real config.

BlueStacks selectively reverts keys on start: the bst.enable_* ones stick while
most bst.feature.* are put back, including four stats beacons. status() reports
that drift so the tab can offer a re-apply, and an optional read-only pin holds
them (verified: 23/23 still off after a real start, instance boots normally).

Also fix the hosts block itself, which stays as the in-guest tracker control:

- Gate it on a patched engine. Editing the guest system image trips BlueStacks'
  tamper check and shuts the instance down mid-session; root is not required,
  any modification does it. The Magisk tab already gated on this, Privacy did
  not.
- Enumerate subdomains explicitly. A hosts file has no wildcards, so an entry
  for doubleclick.net did nothing for cm.g.doubleclick.net or
  pagead2.googlesyndication.com, which is most real ad traffic.
- Drop rtbhouse.net, which does not resolve, for rtbhouse.com and the
  esp.rtbhouse.com endpoint actually seen.
- Stop the docstring claiming it blocks the player's ads.

247 tests green.
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b8f5b29f6d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread ad_settings.py
Comment on lines +250 to +254
for key in sorted(switches):
if config_handler.modify_config_file(config_path, key, OFF):
changed += 1

_write_state(config_path, originals)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Persist originals before changing any switch

If any per-key modify_config_file() call fails after an earlier key was changed, or if _write_state() itself fails, the operation exits without a sidecar even though the config is already partially or fully modified. A subsequent apply then records those new "0" values as the originals, so Restore can no longer recover the user's actual settings. Persist the original-value state atomically before the first config mutation, or update the config and state as one recoverable transaction.

Useful? React with 👍 / 👎.

Comment on lines +35 to +38
for data in self._window.instance_data.values():
path = data.get("config_path")
if path:
return path

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Apply the operation to every detected config

When more than one supported edition is installed, this returns the first config from instance_data, so only that edition is changed while the confirmation and status claim the operation covers every instance; registry_handler.get_all_bluestacks_installations() explicitly supports separate Normal, China, and MSI config paths. It also returns nothing when BlueStacks is installed but no instance survives the instance_data filtering, making this global feature unavailable even though the config exists. Derive and operate on all distinct config paths in window.installations instead.

Useful? React with 👍 / 👎.

Comment on lines +117 to +120
if checked:
ad_settings.lock(config_path)
else:
ad_settings.unlock(config_path)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve locks owned by root persistence

If bluestacks.conf was already read-only for root persistence when the ad switches were applied, status() presents that shared attribute as this checkbox's pin; unchecking it then unconditionally clears the attribute here. In that upgrade/classic-root scenario, using the Privacy control silently removes root's persistence protection and allows BlueStacks to reset the root keys. Track whether the Privacy feature acquired the shared lock, or refuse to release a lock owned by another feature.

Useful? React with 👍 / 👎.

Comment thread ad_settings.py
Comment on lines +181 to +185
try:
with open(_state_path(config_path), encoding="utf-8") as f:
state = json.load(f)
except (OSError, ValueError):
return None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep a restored pin visible and removable

After a user pins the config and chooses Restore, remove() deletes the sidecar but leaves the file read-only; this early return then prevents the lock state from reaching the page, which programmatically shows an unchecked box. Clicking that unchecked box only attempts to lock an already-locked file and immediately refreshes it back to unchecked, so the user cannot unpin the config through the UI and BlueStacks remains unable to save settings. Either release an ad-owned pin during restore or report the live lock independently of the sidecar.

Useful? React with 👍 / 👎.

Definition-list bullets in Features and Project Structure used a hyphen where a
colon belongs, so all 42 now read as labels. Fixes the comma splices left behind
by the earlier wording pass, pairing clauses with semicolons and expanding with
colons. Tightens a few run-ons, drops the exclamation in Contributing, and
rewrites the Modules-tab and companion-guide paragraphs that had run together.

Version range reads "5.20.x to 5.21.x" instead of using a dash.

No content changes.
Comment thread tests/test_telemetry_block.py Fixed
Comment thread tests/test_telemetry_block.py Fixed
A blocklist entry has to be its own exact hostname, so comparing with == rather
than `in` is the stricter check: a substring sitting inside some longer domain
can no longer satisfy the test. This also clears two high-severity CodeQL
incomplete-URL-sanitization alerts, which flag "domain" in x without being able
to tell tuple membership from a substring check on a URL.
@RobThePCGuy
RobThePCGuy merged commit 5b58a72 into master Jul 24, 2026
5 checks passed
@RobThePCGuy
RobThePCGuy deleted the feature/privacy-ad-settings branch July 24, 2026 00:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants