Jump to content

Classic bug: Negative missile charge

From Galactic Empire wiki
Revision as of 22:15, 11 October 2026 by CodexBot (talk | contribs) (Link first command references to command pages)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Bug ID GE-BUG-051
Affected versions Classic Galactic Empire 3.2e and Elwynor's 32-bit classic port
Status Verified in source; corrected in ge-next
Fixed in ge-next 0.2
Primary source cmd_missl() in GECMDS.C; incoming-missile handling in GEFUNCS.C

Summary

Classic GE accepts a negative charge for the MISSILE command. It checks whether the parsed signed number is zero or greater than 50,000, but never checks whether it is less than zero. It then converts the negative number to a 16-bit unsigned missile charge. A player can therefore launch an over-limit missile, or wrap a negative input to a very small or zero charge that the parser would otherwise reject.

This is a command-validation and projectile-payload bug. It is distinct from Classic bug: Flux subtraction underflow, which concerns the ship's stored flux pile becoming negative after an insufficiently guarded subtraction.

Cause

The classic launch routine parses the charge into a signed long, performs an incomplete range check, and then casts it to 16-bit unsigned:

eng_long = atol(margv[2]);
if (eng_long == 0 || eng_long > 50000L) {
    prfmsg(MISFMT);
    return;
}
energy = (unsigned)eng_long;
eng_flu = energy / misengfc;

On the 16-bit target, converting a negative integer to unsigned produces its value modulo 65,536. The valid-charge check applies to the signed input, whereas the flux cost, stored payload, and eventual impact use the converted value. Classic's incoming MISSILE.energy field is also 16-bit unsigned. Elwynor's 32-bit port retains the same missing lower-bound check and explicitly casts the value to USHORT, preserving this behavior rather than accidentally using a 32-bit unsigned charge.

Examples and consequences

With the stock MISENGFC of 100, integer division produces these results:

Entered charge Stored payload Launch flux cost Result
50000 50,000 500 Intended maximum
-1 65,535 655 Above the intended payload limit
-15536 50,000 500 Negative input duplicates the intended maximum
-65535 1 0 Almost powerless but free to launch
-65536 0 0 Zero-charge missile bypasses the explicit zero-input rejection

For example, MISSILE A -1 passes the signed check because -1 is neither zero nor greater than 50,000. If all ordinary launch conditions are met, the game subtracts one missile from inventory, deducts 655 flux with the stock ratio, and stores a 65,535-charge missile on the target. A directly entered 65535 would be rejected. The exact flux cost changes if a sysop changes MISENGFC; the stock MSG file describes a default of 10 in prose but actually supplies 100 in its option line.

At impact, classic uses the recorded charge in its damage calculation after applying the target ship's DAMF. Against an otherwise identical target and under the same random multiplier, a 65,535-charge missile has about 31% more nominal payload than the permitted 50,000 maximum. Actual hull damage and shield loss still depend on the target, shields, and random rolls; the target-side code also narrows its DAMF-adjusted charge back into an unsigned field for some subsequent effects. The exploit is therefore not a guaranteed fixed amount of extra damage, but it defeats the configured input ceiling.

A negative value that wraps to zero or one can still pass the launch checks, consume a missile from inventory, and occupy one of the target's three incoming-missile slots despite costing no flux at the stock ratio. It does not create free ammunition. The separate classic flux-availability check can also permit a costly launch that drives the ship's double energy pile below zero; that result is documented under Classic bug: Flux subtraction underflow.

Reproduction

  1. In classic 3.2e, use a missile-capable ship with ammunition, sufficient flux, and an eligible target in guidance range outside the neutral zone.
  2. Compare MIS A 65535, which fails the command's charge check, with MIS A -1, which passes that check and can launch.
  3. Inspect the target's incoming missile record or observe the launch's stock-ratio flux cost: the negative input is converted to 65,535, and the default cost is 655.
  4. For the zero-charge variant, use MIS A -65536; it passes the command's explicit rejection of a typed zero but stores a zero-charge incoming missile.

These outcomes follow directly from the published source. The example still requires the normal targeting, guidance, inventory, neutral-zone, and target-slot checks to pass.

Correction in ge-next

ge-next 0.2 changed the accepted charge range to 2,000–20,000, made an omitted charge default to 5,000, and replaced the ratio cost with the full requested charge. In cmd_missl(), it compares the parsed signed long against the lower and upper bounds before converting to an unsigned value. A negative argument is clamped to 2,000; it cannot wrap into an over-limit, free, or zero-charge payload. The same ordering is used in ge-next32. ge-arena uses that shared corrected command path.

For a classic-compatible port that does not adopt ge-next's balance changes, the minimal fix is to reject charges below 1 before narrowing them to the missile charge type. Keep the existing 50,000 maximum if classic balance is desired. Validate the whole numeric token as well if the port is intended to reject malformed strings rather than inherit atol()'s permissive parsing.

See also