Fix heap-buffer-overflow in _ctmStreamReadSTRING and stop parsing on string read errors - #2
Merged
Merged
Conversation
…erflow A crafted .ctm file with a string length of 0xFFFFFFFF made malloc(len + 1) wrap in 32-bit to malloc(0); the subsequent read then wrote up to len attacker-controlled bytes past the 1-byte allocation. Reachable from the public ctmLoad() through the file-comment field (and later UV/attribute name fields) for all methods. Allocate with size_t so len + 1 cannot wrap, and require the read to return exactly len bytes, otherwise reject the file as CTM_BAD_FORMAT. A failed malloc now sets CTM_OUT_OF_MEMORY instead of being silently ignored. Reported by Kamal Sentassi, S9S Security Research (coordinated disclosure). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The size_t cast alone does not stop the wrap on a 32-bit size_t target (e.g. the Emscripten/wasm32 build), where (size_t)len + 1 still wraps to 0 for len == 0xFFFFFFFF. Reject that length outright before allocating. Callers previously ignored the error _ctmStreamReadSTRING sets: after a rejected string, ctmLoadCustom and the RAW/MG1/MG2 decoders kept reading, so mError could be overwritten and the file processed further on garbage. Return early after each string read (freeing the temp int buffers in MG2). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…g after a failure ctmLoadCustom now reads mError, so a stale error left by a previous call (without ctmGetError) would otherwise abort a valid load; reset it up front. Clear the mesh counts before returning on a rejected file comment so ctmGetInteger does not report file-supplied counts with NULL arrays. _ctmStreamReadSTRING returns immediately if an earlier read already failed, so the paired mName/mFileName reads do not proceed with a garbage length.
Grantim
approved these changes
Sep 8, 2026
Fedr
added a commit
that referenced
this pull request
Sep 8, 2026
#3) Follow-up to #2 from the same S9S disclosure: file-declared vertex/triangle counts drove malloc directly, so count * stride wrapped to an undersized buffer on 32-bit size_t (wasm32) and in the 32-bit CTMuint products of the float-map and stream.c temp buffers. Counts above UINT_MAX / 16 (largest stride) are now rejected with CTM_BAD_FORMAT; the float-map size is computed in size_t. Also frees the MG1 temporary indices buffer on a failed packed-int read.
Fedr
added a commit
that referenced
this pull request
Sep 8, 2026
…#4) Fork point (upstream v1.0.3), library-only scope, CMake build and export macro, ctmRearrangeTriangles, ctmSaveCustom progress/cancel callback, empty-mesh support, noexcept in the C++ wrapper, the #2/#3 loader hardening, and file-format compatibility notes. README.txt is kept verbatim as the upstream change log.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes a heap-buffer-overflow write reachable from the public
ctmLoad()on a single crafted.ctmfile (CWE-190 → CWE-787), reported privately by Kamal Sentassi of S9S Security Research under coordinated disclosure._ctmStreamReadSTRING()read a 32-bit length and calledmalloc(len + 1). Forlen = 0xFFFFFFFFthelen + 1wraps in 32-bit unsigned to0→malloc(0), after which up tolenattacker-controlled bytes were read into that ~0-byte buffer.Reachable via the file-comment string read in
ctmLoadCustom(right after the header, for RAW/MG1/MG2), and again via UV/attribute-map name fields.Fix
_ctmStreamReadSTRING(lib/stream.c)(size_t)len + 1so the+ 1cannot wrap.lenbytes; on a short read (truncated/crafted file) free the buffer, null it, and fail withCTM_BAD_FORMAT.CTM_OUT_OF_MEMORYonmallocfailure instead of silently ignoring it.len == 0xFFFFFFFFexplicitly, so the+ 1cannot wrap on a 32-bitsize_teither (wasm32).mName/mFileNameread does not proceed with a garbage length (_ctmStreamReadUINTignores short reads and its buffer is uninitialized).Callers (
openctm.c,compressRAW.c,compressMG1.c,compressMG2.c)mErrorafter every_ctmStreamReadSTRINGand stop decoding on failure (MG2 frees its temporaryintUVCoords/intAttribsfirst). Previously the callers ignored the error and kept parsing.ctmLoadCustomresetsmErrortoCTM_NONEup front. This PR adds the library's first reads ofmError, and onlyctmGetErrorclears it, so without the reset a context reused after an un-acknowledged failure would abort the next, valid load.ctmLoadCustomclears the mesh before returning on a rejected file comment, soctmGetIntegerdoes not report file-supplied counts alongside NULL arrays.Verification
ctmLoad()on the reporter's 44-bytecrash_min.ctm→heap-buffer-overflow WRITE, allocation atstream.c:138, overflow atstream.c:141, chainctmLoad → ctmLoadCustom (openctm.c:1236) → _ctmStreamReadSTRING.len = 0xFFFFFFFFand0xFFFFFFFE, and a truncated comment, all returnCTM_BAD_FORMATwith zero vertex/triangle counts and NULL arrays; a context reused after such a failure without callingctmGetErrorthen loads a valid file normally.ctmSave/ctmLoad.Follow-up (not in this PR)
Secondary, lower-severity items the reporter flagged: file-controlled vertex/triangle/UV/attrib counts drive
mallocdirectly (openctm.c:~1239/1245/1254), enabling large-allocation OOM, plus an error-path leak. To be addressed separately.