Repository navigation
Make cfread_string_len read no more than fits in the buffer - #7863
dustinspace217 wants to merge 4 commits into
Conversation
The char* version only checked the stored length with an Assertion, so in release builds a string longer than the buffer was read past its end, and a negative length wrote before it. It now reads at most buffer_size - 1 characters, skips the rest so the next read stays aligned, always terminates the buffer, and logs when characters are dropped. The SCP_string version clamps a negative or past-the-end length to what the file still holds, so a corrupt length can't cause a huge allocation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The char* version now reads up to the full buffer, so a string that fills it exactly without a terminator is caught rather than cut silently. Both versions clamp the stored length to what the file still holds and check the read limit before reading, since cfseek does not; inside a limited pilot section a corrupt length, negative or too long, throws cfile::max_read_length so the loader can drop the section. A cut string, a corrupt length or a failed read gets a Warning, and a failed read no longer leaves the file position off by the bytes it did read. A buffer size of 0 skips the string without writing to the buffer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Made some fixes after an outside review raised some issues:
|
| static void string_len_warning(const SCP_string& what, CFILE* file) | ||
| { | ||
| Warning(LOCATION, "cfread_string_len: %s in %s", what.c_str(), cf_get_filename(file)); | ||
| } |
There was a problem hiding this comment.
This function just adds noise. Write the Warning inline in the places that call it.
Also, at the calling sites, do not concatenate strings with std::to_string calls. Instead, use printf-style format arguments within the Warning itself.
| if (read_failed) { | ||
| to_read = 0; | ||
| } | ||
| bool cut = to_read == buffer_size && memchr(buf, 0, buffer_size) == nullptr; |
There was a problem hiding this comment.
putting parentheses around the sub-expressions would be good for readability
| } | ||
|
|
||
| int to_read = std::min(stored, buffer_size); | ||
| bool read_failed = to_read > 0 && cfread(buf, to_read, 1, file) != 1; |
There was a problem hiding this comment.
putting parentheses around the sub-expressions would be good for readability
| cfread(&str[0], len, 1, file); | ||
| str.resize(stored); | ||
|
|
||
| bool read_failed = stored > 0 && cfread(str.data(), stored, 1, file) != 1; |
There was a problem hiding this comment.
putting parentheses around the sub-expressions would be good for readability
Both overloads now read the length, work out what is stored, read the body and warn through the same three steps. A length that does not fit the file throws inside a limited section even when the section ends at the end of the file, a zero-size buffer still warns about a corrupt length, and a cut-off length field warns (or throws inside a limited section) instead of reading as an empty string.
|
Thanks for both passes. Two commits:
Not taken: sharing |
What?
cfread_string_lennow never reads more characters than fit in the buffer, and keeps the file position correct when it has to cut a string short.Why?
The
char*version's only length check was anAssertion, which is compiled out of release builds. A POF or pilot file with a string longer than the buffer was read past the end of it, which can crash release builds. A negative stored length wrote before the buffer.How?
cfile::max_read_lengthbefore anything is read, so the pilot loaders still drop the section.char*version keeps at mostbuffer_size - 1characters, skips the rest so the next read lines up, and always terminates the buffer. A buffer size of 0 skips the string without writing to the buffer.Warning(debug builds; release builds keep FSO's convention of counting warnings only). A failed read seeks to the end of the string so later reads stay aligned.Testing?
New
CFileTestcases use in-memory files:SCP_stringcases.Each test also checks whether a warning was raised. Three of the original cases fail on the old code, and I broke each new rule on purpose and checked that a test failed. Release and Debug unit tests pass, and clang-tidy (CI's check) is clean.
I also checked it in game, on nightly 26.1.0-20261013 (b37cfda) with MediaVPs 5.0.2, SM1-01, in a mod holding only the two test models. A copy of a POF with one submodel's property string stretched to 8,000 bytes crashes a release build of the nightly in
read_model_file_no_subsys(a debug build stops on the old Assertion). With this change the release build loads it with the same radius, bounding box, submodels and vertex counts as the unmodified file and the mission runs normally, and the debug build logscfread_string_len: a string of 8000 characters was cut to fit a buffer of 256.Anything Else?
Suggested by Goober5000 as the first step toward per-texture POF properties. I used Claude Code to write the fix and tests and to run the checks; I reviewed the code and this text.