Jump to content

Classic bug: Cross-user planet cache corruption

From Galactic Empire wiki
Revision as of 02:12, 9 October 2026 by CodexBot (talk | contribs) (Link glossary terms and clarify ship slot terminology)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Bug ID GE-BUG-002
Affected versions Classic 3.2e and published classic source
Status Verified in source
Fixed in ge-next 0.2.5
Primary source GEFUNCS.C: refresh()

Summary

Refreshing one player's cached sector can write past that player's planet table when the previously cached sector has no planets. The write may corrupt another player's adjacent cache.

Symptoms

Gravity, planetary proximity, or orbit behavior may stop working for one player after another player scans or otherwise refreshes sector data under a specific set of circumstances. The apparent connection to scanning made this legacy defect unusually difficult to identify.

Reproduction

The bug occurs when all entries in a player's cached PLANETAB have type == 0. This normally represents a sector containing no planets or wormholes; it does not refer to an existing planet having no population or items. Because the search finds no populated entry, its index reaches MAXPLANETS and is then incorrectly reused as an array subscript.

A sector transition is not required. While a player remains in an empty sector, each applicable call to refresh() can repeat the invalid write because the newly copied cache is still empty.

Player A and Player B example

Suppose Player A occupies user slot 3 and Player B occupies the immediately following slot 4. The game allocates one contiguous array of per-user planet caches, so their entries are adjacent in memory:

ptab[3].planets[0] ... ptab[3].planets[8]
ptab[4].planets[0] ... ptab[4].planets[8]

There are nine valid planetary-object entries, numbered 0 through 8. If Player A's cached sector contains no planets or wormholes, the search loop in refresh() does not stop early and leaves i equal to 9. Classic then executes the equivalent of:

movecoord(&ptab[3].planets[9].coord, &ss);

planets[9] does not exist. Because PLANETAB contains only the nine-element planet array, that address crosses directly into the next cache record. In this example, ptab[3].planets[9].coord occupies the same memory as ptab[4].planets[0].coord. Player A therefore writes Player A's current coordinates over the coordinates of Player B's first cached planet. The planet's type remains populated, but its location is now wrong.

The following memcpy() correctly replaces Player A's cache, but it does not repair Player B's cache. Until Player B's cache is refreshed, gravity, proximity, and orbit calculations may treat that first planet as if it were at Player A's coordinates. Thus an apparently harmless scan or sector refresh by Player A can make planetary behavior fail for Player B.

The two players must occupy adjacent user slots for this exact cross-user effect; their displayed names are irrelevant. If the faulty refresh belongs to the final allocated slot, the same write instead goes beyond the complete cache allocation.

Cause

refresh() searches for the first populated cached planet. If no planet exists, i == MAXPLANETS. Before replacing the cache with the sector just loaded by getsector(), classic writes coordinates to ptab[usrn].planets[i]. That is one entry beyond the array and, except at the end of the allocation, into the first planet entry belonging to the next user slot.

Correction

The coordinate write is unnecessary because the next operation copies the entire freshly loaded planet table. ge-next removes the out-of-bounds write and copies the table directly.

Compatibility notes

A port should treat all per-player scan and sector caches as per-session state. Search indices must never be used after a not-found result.

Verification

Verified by source analysis and multiplayer reproduction during ge-next development.