Project

General

Profile

Actions

Bug #2161

closed

Heap buffer overflow in worklist_load() via unbounded wl_length from save file -- overwrites struct city fields including pointers (CVSS 8.8)

Added by Marko Lindqvist 12 days ago. Updated 1 day ago.

Status:
Closed
Priority:
Normal
Category:
Server
Target version:
Start date:
08/24/2026
Due date:
% Done:

0%

Estimated time:

Description

Reported by Tristan.

========================================================================
Heap Buffer Overflow in worklist_load() via Crafted Save File ========================================================================
CVSS 3.1: 8.8 (AV:L/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H)
CWE: CWE-122 (Heap-based Buffer Overflow)
Auth: None (open save file)
Version: commit 815c1f4 (master, 2026-07-24)

File: server/savegame/savegame3.c, worklist_load(), lines 966-1006
server/savegame/savegame2.c, worklist_load(), lines 790-830

Root Cause:
worklist_load() reads wl_length from the save file and uses it as
a loop bound without validating against MAX_LEN_WORKLIST (64). The
loop writes struct universal entries directly to pwl->entries[i],
overflowing when i >= 64.

pwl->length = secfile_lookup_int_default(file, 0,       // line 982
"%s.wl_length", path_str);
// NO check: pwl->length <= MAX_LEN_WORKLIST
for (i = 0; i < pwl->length; i++) {                     // line 985
kind = secfile_lookup_str(file, "%s.wl_kind%d", path_str, i);
name = secfile_lookup_str_default(file, "-", "%s.wl_value%d",
path_str, i);
pwl->entries[i] = universal_by_rule_name(kind, name); // line
992: OOB write
}

The wlist_max_length parameter is only used for a padding loop
(line 1002), NOT to cap pwl->length.

Heap Layout (struct city, common/city.h:399+):
struct worklist worklist; // entries64 -- THE OVERFLOW SOURCE
bv_city_options city_options; // overwritten first
enum city_wl_cancel_behavior wlcb;
struct unit_list *units_supported; // POINTER -- corruptible for RCE
int *counter_values; // POINTER -- corruptible

When wl_length > 64, struct universal values (enum kind + union
value with pointers) overwrite city_options, wlcb, units_supported,
and counter_values. Corrupting units_supported leads to controlled
dereference when the server iterates supported units.

Contrast with safe pattern:
worklist_append() in common/worklist.c:147 correctly checks
next_index >= MAX_LEN_WORKLIST. But worklist_load() bypasses
worklist_append() entirely.

Impact:
Heap corruption via crafted save file. A save file with
wl_length=200 overwrites 136 entries past the buffer, corrupting
pointers. Arbitrary code execution via controlled pointer
dereference. Affects single-player and multiplayer (shared saves).
Both savegame v2 and v3 formats affected. All platforms.


Files


Related issues 2 (1 open1 closed)

Blocks Tasks #2165: Release 3.2.6ClosedMarko Lindqvist08/25/2026

Actions
Blocks Tasks #2116: S3_3-alpha3NewMarko Lindqvist08/04/2026

Actions
Actions #1

Updated by Marko Lindqvist 12 days ago

Actions #4

Updated by Alina Lenk 10 days ago

For all branches except S2_6: AIUI the wlist_max_length is just for reading/ignoring all the remaining (empty) entries in the table for worklists that are shorter than the longest one. So, limiting it to MAX_LEN_WORKLIST can produce a lot of unused entry warnings.
For avoiding the vulnerability, we can limit the worklist length directly to MAX_LEN_WORKLIST as in S2_6; this would also make the code more obviously correct. We can leave wlist_max_length uncapped.
Of course, if a worklist is longer than (uncapped) wlist_max_length that might still be cause for a warning? That can only happen in main, S3_3 and S3_2 where wlist_max_length is read from the file; it shouldn't happen in S3_1 and S3_0 where it's computed as the maximum of the individual lengths.

I don't consider that a blocking issue if we want to get this out ASAP. Additional warnings on an already incorrect save file are barely a problem, but obvious correctness could be worth it.

Aside from that, it all looks good.

Actions #5

Updated by Marko Lindqvist 10 days ago

Hmm... Should it be "if (pwl->length > MAX_LEN_WORKLIST) { log error; pwl->length = MAX_LEN_WORKLIST; } else if (pwl->length > wlist_max_length) { log different error; }" ?

Actions #6

Updated by Alina Lenk 10 days ago

Could even drop the else; just two separate ifs, for the case where wlist_max_length is itself too big. Though they'd have to be the other way 'round since one of them changes the length. Of course, then you'd get two errors for a single overlong worklist in an otherwise intact save file... Probably fine either way.

Actions #7

Updated by Marko Lindqvist 10 days ago

Now you start to sound like you'd want the semantics of my original patch. ;-)

Actions #8

Updated by Alina Lenk 10 days ago

Well no; the original patch caps wlist_max_length at MAX_LEN_WORKLIST, so the case where it's still greater than that by the time we load a worklist would be irrelevant :P

But ultimately, the original patch does look correct; it just produces more unused entry warnings in the hypothetical case where someone recompiles the game with an increased MAX_LEN_WORKLIST, makes an overlong worklist, and then tries to load that in the regular version. So we are, as ever, just doing perfectionism ^^

Actions #9

Updated by Marko Lindqvist 10 days ago

Anyway, I think it's enough to get just (1) "over MAX_WORKLIST_LENGTH" error also when there are also (2) "over wlist_max_length" AND (3) "wlist_max_length over MAX_WORKLIST_LENGTH" error situations simultaneously. It's not uncommon that one parsing error hides another. You know that things are going wrong from one error message already.

Actions #10

Updated by Alina Lenk 10 days ago

Makes sense. +1 for the else if option, then.

Actions #12

Updated by Marko Lindqvist 10 days ago

Next iteration of patches ready for review.

Actions #13

Updated by Marko Lindqvist 10 days ago

Actions #14

Updated by Marko Lindqvist 10 days ago

Actions #15

Updated by Alina Lenk 9 days ago

Looks good to me.

Actions #16

Updated by Marko Lindqvist 1 day ago

  • Private changed from Yes to No
Actions #17

Updated by Marko Lindqvist 1 day ago

  • Status changed from In Review to Closed
Actions

Also available in: Atom PDF