Fix 32-bit truncation in the flagset u64 round-trip

Mission flags are saved to the mission file as the full 64-bit flagset value, but three issues prevented anything larger than 32 bits from being saved as intended:

 * `stuff_flagset` parsed the value with `stuff_long`, where `atol` clamps anything above `LONG_MAX`
 * `flagset::from_u64` cast the incoming value to `unsigned long`, truncating bits 32-63
 * `flagset::to_u64` used `bitset::to_ulong` where it should use `bitset::to_ullong`

Fix these issues and add `static_assert` in the flagset functions to defend against future issues.  Also use the proper UINT64_T_ARG where it should be used.

This was first triggered by `Limited_support_rearm_pool`, the first mission flag to occupy bit 31.  Only mission flags are affected; the other flagset serialization paths were audited and are safe.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
Goober5000andClaude Opus 4.7 committed 2026-08-30 16:46:11 -04:00
1 parent 68e9a30463
commit ba7aa70794
7 files changed
+96 -17

No files matched your search

+10 -2
View File
@@ -201,9 +201,17 @@ class flagset {
bool any_set() const { return values.any(); }
bool none_set() const { return values.none(); }
void from_u64(std::uint64_t num) { values = (unsigned long) num; }
void from_u64(std::uint64_t num)
{
static_assert(SIZE <= 64, "from_u64 can only be used with flagsets of up to 64 flags");
values = std::bitset<SIZE>(num);
}
std::uint64_t to_u64() const { return (std::uint64_t) values.to_ulong(); }
std::uint64_t to_u64() const
{
static_assert(SIZE <= 64, "to_u64 can only be used with flagsets of up to 64 flags");
return static_cast<std::uint64_t>(values.to_ullong());
}
size_t hash() const { return std::hash<std::bitset<SIZE>>()(values); }
};
+1 -1
View File
@@ -62,7 +62,7 @@ bool gr_opengl_openxr_test_capabilities() {
}
if (requirements.minApiVersionSupported > XR_MAKE_VERSION(GLVersion.major, GLVersion.minor, 0)) {
mprintf(("System doesn't meet OpenXR graphics requirements (min %" PRIu64 ", available %" PRIu64 ")!\n", requirements.minApiVersionSupported, static_cast<XrVersion>(XR_MAKE_VERSION(GLVersion.major, GLVersion.minor, 0))));
mprintf(("System doesn't meet OpenXR graphics requirements (min " UINT64_T_ARG ", available " UINT64_T_ARG ")!\n", requirements.minApiVersionSupported, static_cast<XrVersion>(XR_MAKE_VERSION(GLVersion.major, GLVersion.minor, 0))));
return false;
}
+1 -1
View File
@@ -208,7 +208,7 @@ bool compareDevices(const PhysicalDeviceValues& left, const PhysicalDeviceValues
void printPhysicalDevice(const PhysicalDeviceValues& values)
{
mprintf((" Found %s (%d) of type %s. API version %d.%d.%d, Driver version %d.%d.%d. Scored as %" PRIu64 "\n",
mprintf((" Found %s (%d) of type %s. API version %d.%d.%d, Driver version %d.%d.%d. Scored as " UINT64_T_ARG "\n",
values.properties.deviceName.data(),
values.properties.deviceID,
to_string(values.properties.deviceType).c_str(),
+1 -1
View File
@@ -2566,7 +2566,7 @@ int Fred_mission_save::save_mission_info()
fout("\n+Flags:");
}
fout(" %" PRIu64, The_mission.flags.to_u64());
fout(" " UINT64_T_ARG, The_mission.flags.to_u64());
// maybe write out Nebula values
if (The_mission.flags[Mission::Mission_Flags::Fullneb]) {
+73
View File
@@ -2989,6 +2989,79 @@ int stuff_long(long *l, bool optional)
return retval;
}
// Stuff an unsigned 64-bit value pointed at by Mp.
// Advances past integer characters.
int stuff_uint64(std::uint64_t *l, bool optional)
{
char *str_start = Mp;
// since strtoull ignores white space anyway, might as well make it explicit
ignore_white_space();
// this is a bit cumbersome
size_t span;
if (*Mp == '+')
{
span = strspn(Mp + 1, "0123456789");
// account for the sign symbol, but not if it's the only valid character
if (span > 0)
++span;
}
else
span = strspn(Mp, "0123456789");
// don't call strtoull unless we found digits, because it will happily parse
// (and wrap around) a negative number
std::uint64_t result = (span > 0) ? strtoull(Mp, nullptr, 10) : 0;
bool success = false, comma = false;
int retval = 0;
// no number found?
if (span == 0)
{
if (!optional)
error_display(1, "Expected unsigned integer, found [%.32s].\n", next_tokens());
}
else
{
*l = result;
success = true;
}
if (success)
Mp += span;
// if an unexpected character is part of the number, warn about it
if (success && unexpected_numeric_char(*Mp))
{
error_display(0, "Expected unsigned integer, found [%.32s].\n", next_tokens(true));
// Rather than back up to str_start, do what retail did and continue
// merrily parsing along at the next character. (Optional numbers
// will still back up to str_start - c.f. a few lines down.)
if (optional)
success = false;
}
if (check_first_non_grayspace_char(Mp, ',', &Mp))
comma = true;
if (optional && !success)
Mp = str_start;
if (success)
{
retval = 2;
diag_printf("Stuffed uint64: " UINT64_T_ARG "\n", *l);
}
else if (optional)
retval = comma ? 1 : 0;
else
skip_token();
return retval;
}
int stuff_float_optional(float *f)
{
return stuff_float(f, true);
+7 -9
View File
@@ -164,6 +164,7 @@ extern bool check_first_non_grayspace_char(const char *str, char ch_to_look_for,
extern int stuff_float(float *f, bool optional = false);
extern int stuff_int(int *i, bool optional = false);
extern int stuff_long(long *l, bool optional = false);
extern int stuff_uint64(std::uint64_t *l, bool optional = false);
extern void stuff_ubyte(ubyte *i);
extern int stuff_int_optional(int *i);
extern int stuff_float_optional(float *f);
@@ -246,17 +247,14 @@ void parse_string_flag_list_special(Flagset& dest, const special_flag_def_list_n
}
template<class T>
void stuff_flagset(T *dest) {
long l = 0;
stuff_long(&l);
void stuff_flagset(T *dest)
{
std::uint64_t val = 0;
stuff_uint64(&val);
if (l < 0) {
error_display(0, "Expected flagset value but got negative value %lu!\n", l);
l = 0;
}
dest->from_u64((std::uint64_t) l);
dest->from_u64(val);
diag_printf("Stuffed flagset: %" PRIu64 "\n", dest->to_u64());
diag_printf("Stuffed flagset: " UINT64_T_ARG "\n", dest->to_u64());
}
extern size_t stuff_int_list(int *ilp, size_t max_ints, ParseLookupType lookup_type = ParseLookupType::RAW_INTEGER_TYPE, bool warn_on_lookup_failure = true);
+3 -3
View File
@@ -2383,15 +2383,15 @@ void game_show_framerate()
MEMORYSTATUSEX mem_stats;
mem_stats.dwLength = sizeof(mem_stats);
if (GlobalMemoryStatusEx(&mem_stats)) {
sprintf(mem_buffer, "Physical Free: %" PRIu64 " / %" PRIu64 " Meg", mem_stats.ullAvailPhys / 1024 / 1024, mem_stats.ullTotalPhys / 1024 / 1024);
sprintf(mem_buffer, "Physical Free: " UINT64_T_ARG " / " UINT64_T_ARG " Meg", mem_stats.ullAvailPhys / 1024 / 1024, mem_stats.ullTotalPhys / 1024 / 1024);
gr_string(sx, sy, mem_buffer.c_str(), GR_RESIZE_NONE);
sy += line_height;
sprintf(mem_buffer, "Pagefile Free: %" PRIu64 " / %" PRIu64 " Meg", mem_stats.ullAvailPageFile / 1024 / 1024, mem_stats.ullTotalPageFile / 1024 / 1024);
sprintf(mem_buffer, "Pagefile Free: " UINT64_T_ARG " / " UINT64_T_ARG " Meg", mem_stats.ullAvailPageFile / 1024 / 1024, mem_stats.ullTotalPageFile / 1024 / 1024);
gr_string(sx, sy, mem_buffer.c_str(), GR_RESIZE_NONE);
sy += line_height;
sprintf(mem_buffer, "Virtual Free: %" PRIu64 " / %" PRIu64 " Meg", mem_stats.ullAvailVirtual / 1024 / 1024, mem_stats.ullTotalVirtual / 1024 / 1024);
sprintf(mem_buffer, "Virtual Free: " UINT64_T_ARG " / " UINT64_T_ARG " Meg", mem_stats.ullAvailVirtual / 1024 / 1024, mem_stats.ullTotalVirtual / 1024 / 1024);
gr_string(sx, sy, mem_buffer.c_str(), GR_RESIZE_NONE);
}
}