Classic bug: Cargo weight overflow

Revision as of 14:23, 18 October 2026 by CodexBot (talk | contribs) (Link bug discovery follow-up)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Bug ID GE-BUG-001
Affected versions Classic 3.2e and published classic source
Status Verified in source
Fixed in Initially ge-next 0.1; comprehensively rewritten in ge-next 0.3.1
Primary source GEFUNCS.C: chkweight() and calcweight()

Summary

Classic calculates an item's cargo contribution by multiplying its count by its configured weight before dividing by 100. The intermediate multiplication uses 32-bit arithmetic and can wrap even when the later division would produce a representable result.

Because the same calculation is used for inventory reports and capacity checks, this is not merely a display error. A wrapped result can allow a ship to load vastly more cargo than its class permits.

Symptoms

A heavily loaded ship can appear to have little or no cargo weight, accept transfers far beyond its capacity, or reject a transfer whose wrapped arithmetic happens to produce a large result. Once an overflowing inventory exists, later reports and capacity decisions can change abruptly as item counts alter the wrapped remainder.

Corrupted records and sysop testing can reach the condition immediately, but stock classic planet populations are large enough for a player to exploit some item types during ordinary play.

Exploit example

In the stock configuration, 100 men weigh 100 tons, so each man weighs one ton. Suppose an owned planet has at least 42,949,673 men and an empty ship requests:

TRANSFER UP 42949673 MEN

The true cargo contribution is 42,949,673 tons. Classic evaluates the numerator in 32-bit unsigned arithmetic:

42,949,673 × 100 = 4,294,967,300
                     = 2^32 + 4
wrapped numerator    = 4
wrapped cargo weight = 4 / 100 = 0 tons

The separate item-count overflow guard still passes because 42,949,673 is well below its limit. The capacity check therefore treats the transfer as adding zero tons and accepts tens of millions of men even on a small ship. REPORT INVENTORY repeats the same multiplication and also omits their real weight.

Troops make the exploit directly useful in planetary combat. Stock troops weigh 200 tons per 100. A transfer of 21,474,837 troops really weighs 42,949,674 tons, but:

21,474,837 × 200 = 4,294,967,400
                     = 2^32 + 104
wrapped cargo weight = 104 / 100 = 1 ton

A player who accumulates that population on an owned planet can therefore move an enormous invasion force in a ship that classic believes gained only one ton. Similar wrap points exist for every item weight.

These examples follow deterministically from the published source and stock weights. A live classic test would confirm the complete command path and messages, but is not required to establish the arithmetic failure.

Cause

Classic performs the equivalent of:

(item_count × item_weight) / 100

item_count is an unsigned long and item_weight is a long. Under the target compiler's usual arithmetic conversions, the multiplication is performed as a 32-bit unsigned value. It wraps modulo 2^32 before division can reduce it. Summing multiple already-wrapped item contributions cannot recover the lost high bits.

chkweight() performs this calculation for every item already aboard and again for the proposed addition. calcweight() performs the same calculation for the inventory report. Their agreement therefore makes the incorrect result look internally consistent.

Fix history

ge-next 0.1

ge-next 0.1 initially addressed the classic defect by calculating cargo contributions with floating-point arithmetic and by checking item-count addition against the unsigned-long limit before accepting a transfer. ge-next 0.1.1 refined the displayed total by accumulating fractional weight in a double and rounding the final result upward.

This removed the classic 32-bit multiplication wrap for normal game values, but later exact-integer cargo work and extreme inventory testing exposed remaining edge cases involving very large totals, fractional truncation, and intermediate overflow. The cargo system was therefore revisited rather than treating the original 0.1 change as the final implementation.

ge-next 0.3.1

ge-next 0.3.1 rewrote the cargo routines with functionally equivalent but architecture-specific implementations.

The 16-bit build cannot rely on an efficient native 64-bit integer. It splits each quantity into complete groups of 100 and a remainder below 100, checks each multiplication against the remaining capacity before performing it, and carries fractional hundredths of a ton between item types. Impossible totals saturate instead of wrapping. Its “room for item” routine uses the same capacity predicate through a binary search, ensuring that displayed weight, transfer checks, and maximum transferable amounts follow one fit rule.

The 32-bit build uses a 64-bit unsigned accumulator measured in hundredths of a ton. It can multiply a 32-bit item count by the configured item weight directly, calculate remaining capacity in one operation, and then apply the same rounding and saturation rules as the 16-bit build.

Both implementations reject item-count addition overflow, combine fractional weight across item types, round any final fractional ton upward for display, and return a saturated maximum for impossible or corrupted inventories.

Discovery transcript

Read the June 2026 discussion of a later ge-next cargo-calculation failure.

Compatibility notes

A port should not assume that using a wider accumulator alone resolves every case. It must also reject item-count overflow, define behavior for totals beyond the report type, combine fractional weights consistently, and ensure that reports, capacity checks, loot collection, and “maximum transfer” calculations all use the same rule.

Verification

The classic defect and exploit arithmetic were verified against the published classic source and stock item weights. The ge-next 0.1, 0.1.1, and 0.3.1 implementations were checked against their corresponding source revisions. The 0.3.1 routines were also exercised with inventories near the 32-bit unsigned item-count limit.