Skip to content

[T6] Zone loader spins forever instead of erroring when a fastfile fails to inflate #82

Closed

Description

@schindlershadow

What happened?

On ARM64 Android (Snapdragon 8 Gen 2) running the client under Wine/Proton arm64ec with
FEX as the x86→ARM64 translator, T6MP hangs forever at Loading fastfile code_pre_gfx_mp
— no error, no crash, one thread pinned at 100% CPU indefinitely.

I traced it to the guest instruction level, and there are two separate issues. The first one
is platform-independent and is the reason this presents as a silent freeze rather than an
error message.

1. The inflate error path is an infinite spin loop (affects all platforms)

The zone chunk loader does roughly:

inflateInit2_(&strm, -15, ...)      ; raw deflate
call <decrypt/deblock>
call inflate                        ; inflate(&strm, Z_FULL_FLUSH)
cmp  eax, 1
je   done                           ; Z_STREAM_END -> fine
test eax, eax
jne  <back to the cmp>              ; eax is NEVER reloaded -> spins forever

If inflate() returns anything other than Z_OK or Z_STREAM_END, the loop re-tests the
same stale return value forever. So any corrupt or unreadable fastfile becomes a
permanent silent hang instead of hitting the existing
Fastfile for zone '...' is corrupt or unreadable. error. That would apply on Windows too,
and it makes this class of problem very hard for users to self-diagnose.

2. Signed/encrypted zones are misdetected under x86→ARM translation

The loader treats the signed retail zone as if it were an unsigned one: it reads a chunk
size at offset 0x0C and begins inflating at 0x10, instead of skipping the 0x138
signed header (TAff0100 + version + PHEEBs71 + flags + zone name[32] + RSA signature[256])
and running the Salsa20 pass first. inflate() is therefore handed still-encrypted bytes.

Evidence, read out of the live process at the hang:

  • z_stream.msg = "invalid distance too far back"
  • total_in = 3, total_out = 0
  • next_in pointed at file offset 0x13 — i.e. inside the PHEEBs71 tag
  • the 512 KB input buffer was byte-identical to the on-disk .ff, header and payload —
    so the decryption pass had not modified it at all

I have reported the underlying translation problem to FEX as
FEX-Emu/FEX#5966. I am not asking you to support FEX or Android —
issue 2 is arguably theirs. Issue 1 is the one I think is worth fixing on your side,
since it turns every fastfile read failure into an unexplained freeze.

A useful corroborating detail: T4 (WaW) works perfectly on the exact same device, same
client build, same translator.
WaW zones are unencrypted (IWffu100), so they never take
the decrypt path — which isolates the failure to the signed/encrypted zone path rather than
to zone loading in general.

Expected result

inflate() failing should raise the normal COM_ERROR (e.g. "Fastfile for zone '...' is
corrupt or unreadable.") instead of looping on a stale return value forever. Even just
breaking out of that loop on a negative return would turn a mystery freeze into an
actionable message.

Steps to reproduce

  1. BO2 (TU17) with Plutonium r5316 on ARM64 Android, running the client under Wine/Proton
    arm64ec with FEX (libwow64fex.dll) as the 32-bit x86 translator.
  2. Launch T6 Multiplayer from the launcher.
  3. The console window prints Loading fastfile code_pre_gfx_mp and then hangs forever with
    one thread at 100% CPU. It never errors and never times out.
  4. For contrast, launch T4 Multiplayer on the same setup — it loads and runs normally.

On Windows, issue 1 alone should be reproducible by feeding the loader any fastfile that
fails to inflate: expect a hang rather than the corrupt-fastfile error.

Affected game(s)

BO2 Multiplayer (T6MP) — the hang. The spin loop itself is in shared zone-loading code and
would affect any title.

Affected component(s)

Client (game)

Activity

  1. schindlershadow commented on Sep 16, 2026

    @schindlershadow
    Author

    Additional confirmation: the spin reproduces on a second, completely independent x86→ARM stack. I think that's worth separating from the platform question, because it makes the client-side half of this clearly reproducible rather than "one weird emulator".

    • Originally reported on: Snapdragon 8 Gen 2 / Android, FEX under a proton-arm64ec container.
    • Now also on: the same device and the same Plutonium install, but Winlator 11.2 — Box64 0.4.4 with wine 10 WoW64.

    Behaviour is identical. plutonium-bootstrapper-win32.exe starts normally, logs in, binds DW, prints --- Cmd Initialization Complete ---, and then:

    Loading fastfile code_pre_gfx_mp
    

    …and never prints another line. The Database thread sits at ~100% of a core with /proc/<tid>/syscall = running on every sample (i.e. never in a syscall) and read_bytes flat — so it isn't reading, blocking or retrying, it's spinning in userspace. I left it 70+ minutes. Same result with Box64's default tuning and with its most conservative settings (exact flags, no block merging, strict memory ordering, no call/ret optimisation).

    To be clear about scope: the reason the fastfile fails to inflate on these devices is an emulation problem and not yours — the decrypt step is Themida-virtualised and doesn't execute under translation, so zlib is handed still-encrypted bytes. I'm not asking you to support that.

    The part that is squarely in T6's court is the error path. When inflate() returns a failure, the loop re-tests a stale register instead of re-reading the result:

    call  inflate
    cmp   eax,1
    je    done
    test  eax,eax
    jne   $-7        ; eax is never reloaded -> spins forever

    That's platform-independent. Any Windows user who hits a corrupt, truncated or partially-downloaded zone gets an unkillable frozen client with nothing written to console.log, instead of an error naming the zone. In my case the z_stream had already recorded a perfectly good diagnosis — msg was invalid distance too far back — it just never reaches the user.

    A bounded retry, or simply surfacing the zlib error and aborting the load, would turn a silent hang into a self-explanatory failure.

    Happy to run anything else on the device if it's useful.

  2. ineedbots commented on Sep 28, 2026

    @ineedbots
    Collaborator

    This is due to Valve's CEG 2.0 encrypted/protected code sections, which was a novel DRM solution for BO2. Valve's obfuscation techniques are most likely not being emulated accurately. This isn't anything to do themida or even plutonium related, but the vanilla's game's DRM and/or x86 emulator you are using.

    DB_AuthLoad_Init is the very FIRST CEG encrypted code section you encounter, and failure to call this function results in fastfiles failing to load due to the RSA sig not being consumed from the stream on the fastfile, cause in stream reads to be offset.

    Do note that the latest steam EXEs released this month no longer have CEG applied to them, as CEG is no longer being offered by valve anymore.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    component: clientIssue related to the game clientgame: T6Issue related to T6mode: MPIssue only affecting Multiplayer

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions