From 153295b64c8f0636c673f119e189f100f9bf3741 Mon Sep 17 00:00:00 2001 From: chrg6 Date: Fri, 13 Nov 2026 17:40:00 -0500 Subject: [PATCH] Check cfg_meta's result before reading a row's default in default_number and gfx_cfg_window_size. cfg.c default_number, which cfg_int and cfg_double use for a set value that does not parse, read cfg_meta(name)->def without a check, so a key without a row whose value was set and unparsable crashed there (the verifier's probe: a segmentation fault inside default_number). gfx_present.c gfx_cfg_window_size did the same for WINDOW_SIZE. Neither is reachable today: every unguarded value read has a row (tests/cfg_rows_check.py holds it so), the three guarded ones use cfg_bool, and WINDOW_SIZE has a row. default_number gives 0 for a key without a row; gfx_cfg_window_size falls back to 1920x1080 when the row or its default is missing. Taking the device profile's default there (1280x800 on a Steam Deck) would change what players get and is left for approval. Reproduced before the change with the new case in tests/cfg_parse_check.py (cfg_int and cfg_double of an unset row's key set to "abc" crashed the program); with the change both give 0 and the check passes, as do the cfg rows checks and the compile checks. Co-Authored-By: Claude Opus 5.5 --- runtime/cfg.c | 6 ++++-- runtime/gfx_present.c | 6 ++++-- tests/cfg_parse_check.c | 6 ++++++ tests/cfg_parse_check.py | 5 ++++- 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/runtime/cfg.c b/runtime/cfg.c index 7ede5417..1f835953 100644 --- a/runtime/cfg.c +++ b/runtime/cfg.c @@ -985,9 +985,11 @@ static const char *cfg_memo_get(const char *name, int kind) { const char *cfg_get(const char *name) { return cfg_memo_get(name, 0); } const char *cfg_value(const char *name) { return cfg_memo_get(name, 1); } -// The row's default as a number, for a set value that does not parse. +// The row's default as a number, for a set value that does not parse; 0 for a key without a row, whose set value +// cfg_value hands out without one (tests/cfg_rows_check.py keeps such reads guarded). static double default_number(const char *name) { - return strtod(cfg_meta(name)->def, NULL); + const CfgMeta *m = cfg_meta(name); + return m ? strtod(m->def, NULL) : 0; } double cfg_double(const char *name) { diff --git a/runtime/gfx_present.c b/runtime/gfx_present.c index 1aac8f2d..2db2407a 100644 --- a/runtime/gfx_present.c +++ b/runtime/gfx_present.c @@ -179,8 +179,10 @@ int gfx_cfg_fullscreen(void) { return cfg_bool("NX2NAVI_FULLSCREEN"); } void gfx_cfg_window_size(int *w, int *h) { int x = 0, y = 0; - if (sscanf(cfg_value("NX2NAVI_WINDOW_SIZE"), "%dx%d", &x, &y) != 2 || x < 640 || y < 360 || x > 7680 || y > 4320) - sscanf(cfg_meta("WINDOW_SIZE")->def, "%dx%d", &x, &y); // unparsable or out of range: the default + if (sscanf(cfg_value("NX2NAVI_WINDOW_SIZE"), "%dx%d", &x, &y) != 2 || x < 640 || y < 360 || x > 7680 || y > 4320) { + const CfgMeta *m = cfg_meta("WINDOW_SIZE"); // unparsable or out of range: the default + if (!m || sscanf(m->def, "%dx%d", &x, &y) != 2) x = 1920, y = 1080; + } *w = x; *h = y; } diff --git a/tests/cfg_parse_check.c b/tests/cfg_parse_check.c index 5014aaa9..bbb010c3 100644 --- a/tests/cfg_parse_check.c +++ b/tests/cfg_parse_check.c @@ -3,6 +3,7 @@ // cfg.c logged ("log: ...") and what the case reads back ("got: ..."). // load cfg_load on the file; then cfg_init's check of the environment (the variables the script set) // full cfg_load on the file, then two cfg_set of keys the file does not have +// norow cfg_int and cfg_double of a key without a row whose value (the environment's) does not parse #include #include #include @@ -80,5 +81,10 @@ int main(int argc, char **argv) { printf("got: vk_present %s\n", cfg_get("NX2NAVI_VK_PRESENT") ? cfg_get("NX2NAVI_VK_PRESENT") : "(unset)"); return 0; } + if (!strcmp(argv[1], "norow")) { + if (!cfg_load(ini)) return printf("cfg_load failed\n"), 2; + printf("got: cfg_int %d cfg_double %g\n", cfg_int("NX2NAVI_NO_ROW_CHECK"), cfg_double("NX2NAVI_NO_ROW_CHECK")); + return 0; + } return 2; } diff --git a/tests/cfg_parse_check.py b/tests/cfg_parse_check.py index ebce8019..0cfa221e 100644 --- a/tests/cfg_parse_check.py +++ b/tests/cfg_parse_check.py @@ -5,7 +5,8 @@ for what it links with. The values stay as written; what is checked is what the word that is none of the menu's choices, and a ';' or '#' comment kept in a value, each named with its line; none for a file whose values fit, nor for a list with numbers in it (the menu's presets); at most 16 lines, then a count; - a full settings table (cfg.c CFG_MAX_KEYS): one line at load naming the first key not read, one at the first menu - change not kept. + change not kept; +- a number read of a key without a row whose set value does not parse gives 0 (cfg.c default_number), not a crash. No game files. @@ -129,6 +130,8 @@ def main(): 'a file with 601 keys: one line at load from line 513 on', out) check(rc == 0 and len(set_lines) == 1 and 'VK_PRESENT is not kept' in set_lines[0], 'two menu changes past a full table: one line', out) + rc, out = run(exe, env, 'norow', CLEAN, work, {'NX2NAVI_NO_ROW_CHECK': 'abc'}) + check(rc == 0 and 'got: cfg_int 0 cfg_double 0' in out, 'cfg_int of a set, unparsable key without a row: 0', out) print(f'cfg_parse_check: {"FAILED" if bad else "ok"}') return 1 if bad else 0