Pandora
Download
Technical documentation

How Pandora writes a flag

Everything on this page is measured on the developer’s machine, or read straight out of the source. Where a number was never measured, it says so instead of being estimated.

v1.0.0 Windows 10 / 11 · x64 Rust 2021 · eframe / egui

Architecture

Pandora is one binary with no plugin surface. The FastFlag modules were split out of an older, larger program; login, key authentication and the sealed-binary machinery were deliberately left behind. What remains depends on four things internally: memory access, the scorer, the engine, and the translation macros.

ModuleResponsibility
fflag_engineThe adaptive engine. Read-before-write, type contracts, transactional apply, audit trail. Unit-testable without Roblox running.
fflag_injectorThe live path: registry discovery, offset refresh, auto-apply and re-apply loops, 20 diagnostics.
flag_mlRelevance scorer over flag names. Pure Rust, no external crates.
advisor/*Bottleneck scoring, evidence corpus, lexicon, combos, and the A/B measurement harness.
flagdb, flaglistThe flag database (three sources merged) and the 72 built-in presets.
historyFull-text snapshots of the table, one file per revision.
ui, pages, appDesign system, screens, and the window shell that ties the editor to the injector.

Injection path

The engine is layered so that no code path can reach a write without passing the read that justifies it. Each layer only knows about the one below it.

 LayerWhat it settles
L0MemorySourceProcess memory as a trait — read, write, is-writable-data, module range. Replaced by a mock in tests.
L1Knowledge / OperationalA three-state model. Knowing a thing and being allowed to write it are separate questions.
L2judge_registryIdentifies the registry layout by accumulating independent evidence rather than matching one signature.
L3TypeContractPer-type memory contracts for bool, int, float and string, including what a slot must not contain.
L4apply_typedTransactional apply: write on change only, then read back to confirm.
L5AuditTrailWhy a write was refused, kept for later. --fflag-audit separates what was written from what applied.

The handle

The process is opened with mask 0x0438 — VM_OPERATION, VM_READ, VM_WRITE, QUERY_INFORMATION. No thread creation, no allocation, no page-protection change: the write is four to eight bytes over a value that already exists. Older code that asked for PROCESS_ALL_ACCESS and flipped pages to PAGE_EXECUTE_READWRITE was deleted — nothing called it, and it was the first shape a scanner looks for.

When offsets go stale

A Roblox update moves things. The status bar says so and the app re-derives offsets on a backoff of 30 s → 1 min → 2 min → 5 min. An apply that lands zero flags is itself the clearest evidence that offsets are stale, so it schedules a refresh and retries once it finishes.

What it refuses to do

Every crash ever observed in this project came from writing while not knowing. All three would have been stopped by a single read before the write, which is why that read is not optional.

  • Trusting a stale RVA table and stamping four bytes at mbase+off, over a live pointer.
  • Pouring raw bytes over a std::string object — death on the next access.
  • Writing a 4-byte int into a 1-byte bool slot, corrupting three neighbouring values.
A live pointer in an int slot means the slot is not an int

The type contract does not only describe what a slot should hold; it describes what it cannot hold. A value whose top 32 bits are set and which is 8-byte aligned looks like a pointer, and no FInt is ever going to be one.

Speed

“It takes forever to inject” is one lump on screen and four stages underneath. Same machine, same Roblox process (2.0 GB), timed from launch to the first flag landing:

StageBeforeAfter
Waiting for the daemon’s first patrol2,000 ms0 ms
Building the index3,013 ms880 ms
First apply2,981 ms130 ms
Total~8 s~1.0 s

The result never changed across the rewrite: 435 of 715 flags applied at every step, and the index name set stayed identical. --fflag-time and --fflag-ready are the rulers built for it.

Where the time went

  • The page cache cloned 4 KB for every 8-byte read. One index build was 4.26 GB of memcpy. The hottest read now allocates nothing.
  • Dead pages were re-asked of the kernel. 30,970 failed reads, 30% of all calls; a failed round trip costs what a successful one costs. Caching failures with the same TTL: 1,597.
  • The heap sweep was single-threaded. Split into 1 MB chunks: 1,379 ms → 895 → 551 → 448 at six threads. It stops at six because past that you gain almost nothing and take it from the game.
  • The missing-flag report was 80% of an apply. 11.5 million comparisons for a list nobody had changed. Sorted names make it a binary search; an unchanged list is not recomputed.
  • 36,821 of 41,039 names had exactly one candidate. With one candidate the answer is that candidate, read or no read: 168 ms → 31 ms.

Proving nothing was lost

Counting cannot prove a faster sweep lost nothing — drop one name, gain another, and the count is identical. So the name sets were compared whole:

Pandora.exe --fflag-scan-diff
full sweep   41042 names
fast sweep   41042 names
only in full     0 ← nothing lost

The older, looser candidate filter was compared the same way — the sorted name lists did not differ by a line, and the missing-flag report matched the previous build byte for byte.

Advisor

A community preset hands everyone the same 300 flags. But an integrated-graphics laptop stalls on the GPU, a four-core desktop on the CPU, an 8 GB machine on memory. The advisor scores those three, weights the flags that relieve the tight one, and prints the reasoning on screen — hide it and the user cannot decide whether to accept the result.

The universe, and what may enter it

Three sources are merged: 35,079 names actually present in this build’s memory, 22,758 from the public server settings table with official types and current defaults, and 3,194 values from 72 community presets. All 36,219 are walked; only what can be justified becomes a candidate.

AggressivenessEvidence admittedRow capRows here
ConservativeVerified only — 53 written by hand, each with an explainable mechanism20060
Normal+ proven by name — 499 whose direction follows mechanically600423
Aggressive+ community consensus — 90 touched by 6+ presets with 3/4 agreement1,600563
Maximum+ weak consensus and unreadable names — behaviour unknown733

Rows measured on the developer’s machine at 60% quality cost. Default is Normal.

Quality cost is a maximum, not a sum

How much visual quality to give up is not a technical question, so it is a slider from 0 to 100%. Cost is counted as the maximum per aspect: lowering shadow resolution after shadows are already off costs no further quality, and summing would price that pairing as double — which keeps the free finishing touches out. The same reasoning caps each aspect’s benefit; no matter how much telemetry you switch off, its ceiling is 1.2.

Measured, not asserted

--advise-measure alternates default and recommended values on a running client, reading CPU time from GetProcessTimes and GPU from the same counter Task Manager uses.

Pandora.exe --advise-measure 40 12 3 1
round 1  A CPU 2657 ms/s  GPU 32.9%   →  B CPU 2441 ms/s  GPU 31.9%
round 2  A CPU 2730 ms/s  GPU 32.9%   →  B CPU 2301 ms/s  GPU 31.6%
round 3  A CPU 2837 ms/s  GPU 32.6%   →  B CPU 2375 ms/s  GPU 32.1%
                                      CPU −13.4% (±4.6%p) · GPU −2.9%
  • Alternate, repeatedly. Measuring all of A then all of B bakes the scene’s own drift into the result.
  • Pick the process that is drawing. Four Roblox instances on one machine is normal; the first handle found was once idle at CPU 41 ms/s, GPU 0%.
  • Inject through the real path. A direct name lookup landed 0 of 240 flags, because the registry stores names without their prefix.
  • Pin the frame cap on both sides. Otherwise A and B run at different frame rates and the delta stops meaning anything.
Sixty rows beat seven hundred

At the same 60% quality cost: conservative, 60 rows, CPU 1651.7 → 1103.8 ms/s (−33.2%, spread ±1.5%p). Maximum, 736 rows, 1576.1 → 1211.8 (−23.1%, ±7.5%p). Twelve times the rows performed worse, because hundreds of telemetry flags do not make frames.

A round that cannot be measured is not a result

One run read A 55 → B 138, A 48 → B 39, A 47 → B 52, spread ±91.5%p. The game window said FPS 149.8 · Render 0.7 msec — an empty scene. Where there is nothing to reduce, nothing you inject changes anything. That is “cannot measure”, not “no effect”, and the screen says which.

FlagML

A small neural network scores how relevant a flag name is, and it is written by hand in Rust with no external crates — the whole model is a JSON file in your profile. It learns online: applying a flag is the training signal.

Input256-dim sparse vector — character n-gram hashing, plus 8 one-hot slots for the prefix (FFlag, DFInt, FLog…)
Hidden64 units, ReLU
Output1 unit, sigmoid
TrainingOnline SGD, learning rate 0.02, log capped at 5,000 entries
Storage%APPDATA%\PandoraSettings\fflagWeight\

Nothing leaves the machine. There is no server to talk to, and the scorer is a suggestion engine — it never decides on its own that a flag is safe to write.

Safety filter

Names touching addresses, tokens, login, payment, moderation, security or age are excluded from every recommendation — 2,296 of them. Community lists commonly overwrite a telemetry URL with "null"; this one does not. That is not a trade worth a few frames.

Word boundaries, not substrings

The filter first matched substrings. age ate Percentage, Storage and Manager; ban ate Bandwidth. A quarter of the universe — 6,387 names — disappeared silently. Matching whole words brought it to 1,711.

The advisor also refuses to recommend a name it cannot find in the live index, even when 48 presets contain it. A flag that no longer exists in this build is reported as unverified rather than injected quietly.

Interface runtime

“The window feels laggy” measured out to something other than drawing. The code picked the first top-level window the process owns, and winit makes several — eight top-level, four without an owner, two of those invisible helpers. When an invisible one was picked, IsWindowVisible returned 0, the app decided it was hidden, and pinned repaint at 400 ms. Three frames a second with the window in front of you. It now picks the largest visible window.

WhereWhat it did every frameBeforeAfter
PresetsWalked the folder and re-read every preset JSON to count rows7.80 ms0.36 ms
DatabaseSearched 36,000 names and cloned 300 results0.31 ms0.17 ms
SidebarSerialised the whole table to JSON for one asterisk on the save button0.12 ms0.06 ms
AnywhereA process snapshot every 2 s, a 10–13 ms stallspikenone

The repaint timer does not arrive when asked

request_repaint_after lands on winit’s 15.6 ms wait granularity: ask for 16 ms and you wake at 31 (32 fps); ask for 33 and you wake at 46 (21 fps, visibly steppy). Neither timeBeginPeriod(1) nor leaving the power-saving class moved it. So the app asks for 16 ms. Drawing at vsync costs 2.3% CPU for a slightly smoother gradient — not worth it; scrolling feels smooth because egui redraws on input, not because of this interval.

StateCPU
Idle, window in front0.7 – 1.9%
Pointer moving over it2.2%
Covered by another window0.26%
Minimised, hidden, or cloaked by DWM~0 (2.5 fps)

Losing focus is not a reason to slow down: Pandora is meant to sit beside Roblox, and a frozen status bar looks like a dead program.

Diagnostics

Every one of these runs without the window, so an experiment repeats quickly and leaves numbers behind rather than impressions.

Pandora.exe <flag>
--fflag-diag          attach, score the layout, parse flags.json (no write)
--fflag-apply         the above, then actually inject
--fflag-audit         read-only audit: what was written vs what applied
--fflag-verify 20     re-check after 20 s that the values held
--fflag-repair        diagnose entries that did not take
--fflag-time [n]      break one injection into attach / index / apply
--fflag-ready         launch to first-writable, measured
--fflag-regions       region kinds, sizes, candidate counts
--fflag-scan-diff     fast sweep vs full sweep, by name set
--advise 30 1         quality cost 30%, aggressiveness 1 (normal)
--advise-sweep 2      walk 0-100% quality cost at aggressiveness 2
--advise-measure 60 12 3 0   A/B: 60% cost, 12 s, 3 rounds, conservative
--music-diag <term>   search → url → stream → decode → device, timed

Files and network

Everything Pandora keeps lives under one folder. Deleting it resets the program.

%APPDATA%\PandoraSettings\
settings.json              window settings
clientsettings.json        server settings table, cached
pandora_lang.txt           chosen language
fflags\flags.json          the table itself
fflag_favorites\*.json     favorites
BuiltedFlag\<category>\    the 72 built-in presets
offset\fflagoffset\        offset table, version stamp, learned cache
fflagWeight\               FlagML weights and training log
history\<ms>.json          one file per revision, 120 kept
wallpaper\ · music\        background images, recent track titles only

Network

  • Roblox’s public settings table. Fetched only when you press refresh in the Database tab; otherwise the cache is used and nothing is requested.
  • Wallpaper search. wallhaven.cc, chosen because it is the only one that needs no API key — a key would have to be baked into the binary. Safety is enforced in the URL (SFW only, no people), so the server filters rather than the client.
  • yt-dlp, downloaded once from GitHub into tools\ for the music player. Audio is streamed into memory and never written to disk; only the title is remembered.

There is no analytics endpoint, no update ping, and no account of any kind.

Numbers on this page are measured on the developer’s machine, at the versions stated. Download Pandora · v1.0.0