Integer Overflow in PlayerAnimation.cpp memcpy Size Calculations
Introduction
The animation/PlayerAnimation.cpp file handles importing and processing skeletal animation data—vertex positions, normals, tangents, and joint weights—from model files. At line 222, a critical flaw existed in how the code computed byte sizes for memcpy operations. The variable vCount, read directly from animation file data, was multiplied by sizeof(float) * 3 (or * 4 for tangents) without any overflow validation. On 32-bit platforms—or anywhere vCount is stored as a 32-bit integer—a carefully chosen value like 0x20000000 (536,870,912) would cause the multiplication to silently wrap around to a tiny value, resulting in a heap buffer overflow that could enable arbitrary code execution.
This is the kind of vulnerability that turns a harmless-looking animation file into a weapon.
The Vulnerability Explained
Let's look at the core issue. In the original code at line 212:
size_t vCount = va->size(), wCount = jData._weightList.size();
if (vCount != wCount)
{
// warning and return
}
After this check, vCount was used directly in memory operations like:
memcpy(dest, src, vCount * sizeof(float) * 3); // positions
memcpy(dest, src, vCount * sizeof(float) * 4); // tangents
The problem is arithmetic: sizeof(float) is 4 bytes. So vCount * 4 * 3 = vCount * 12. On a 32-bit platform where size_t is 32 bits, SIZE_MAX is 0xFFFFFFFF (4,294,967,295). If an attacker sets vCount = 0x20000000 (536,870,912):
0x20000000 * 12 = 0x180000000
This exceeds 32 bits. The result wraps to 0x80000000 on some platforms, or even smaller values depending on the exact multiplication chain. The memcpy then copies far fewer bytes than the destination buffer expects—or worse, if the allocation was based on the wrapped value while the source contains the full data, it writes past the buffer boundary.
Attack Scenario
- Attacker crafts a malicious
.osgbor animation file with a vertex array whose reported size is0x55555556(1,431,655,766). - Application loads the file through the
ozzanimation pipeline, which calls intoPlayerAnimation.cpp. va->size()returns the attacker-controlled value asvCount.- The multiplication
vCount * sizeof(float) * 4overflows:0x55555556 * 16 = 0x555555560, which truncates to0x55555560on 32-bit—or on 64-bit with a 32-bit intermediate cast, wraps differently. memcpyuses the incorrect size, writing beyond the heap buffer and corrupting adjacent memory structures.- Attacker achieves code execution by overwriting function pointers or vtable entries in adjacent heap objects.
A similar pattern existed at line 515 for tCount * sizeof(float) in the timepoints memcpy:
if (tCount > 0) memcpy(anim.timepoints_.data(), &timePoints[0], tCount * sizeof(float));
The Fix
The fix introduces explicit bounds checks that validate the multiplication cannot overflow before it's performed.
Change 1: Line 213 — Vertex/Weight Processing Guard
Before:
size_t vCount = va->size(), wCount = jData._weightList.size();
if (vCount != wCount)
After:
size_t vCount = va->size(), wCount = jData._weightList.size();
if (vCount > SIZE_MAX / (sizeof(float) * 4)) return; // guard: integer overflow in memcpy sizes
if (vCount != wCount)
The check vCount > SIZE_MAX / (sizeof(float) * 4) ensures that even the largest multiplication used downstream (vCount * sizeof(float) * 4 for tangent data) cannot overflow size_t. Since sizeof(float) * 4 = 16, this limits vCount to SIZE_MAX / 16—which is still over 1 billion vertices on a 64-bit system, far more than any legitimate animation file would contain. If the check fails, the function returns early, safely rejecting the malicious input.
Change 2: Line 515 — Timepoints memcpy Guard
Before:
if (tCount > 0) memcpy(anim.timepoints_.data(), &timePoints[0], tCount * sizeof(float));
After:
if (tCount > 0 && (size_t)tCount <= SIZE_MAX / sizeof(float))
memcpy(anim.timepoints_.data(), &timePoints[0], (size_t)tCount * sizeof(float));
Here, tCount is validated against SIZE_MAX / sizeof(float) before the multiplication occurs. The explicit (size_t) cast also ensures the multiplication happens at the full platform width, preventing intermediate 32-bit truncation.
Why Both Changes Matter
The PR notes that similar patterns exist at lines 81, 223, 227, 233, and 248. The two changes address the primary entry points where attacker-controlled sizes flow into memcpy. The sizeof(float) * 4 divisor in the first check is deliberately chosen as the largest multiplier used anywhere downstream, creating a single guard that protects all subsequent size calculations for that vCount value.
Prevention & Best Practices
1. Always validate before multiplying for memory operations:
// Safe pattern
if (count > SIZE_MAX / element_size) {
// reject input
return error;
}
memcpy(dst, src, count * element_size);
2. Use compiler overflow builtins when available:
size_t result;
if (__builtin_mul_overflow(vCount, sizeof(float) * 3, &result)) {
return; // overflow detected
}
memcpy(dst, src, result);
3. Treat file-derived sizes as untrusted input. Any value read from a file—vertex counts, array lengths, string sizes—must be validated against reasonable bounds before use in memory operations.
4. Consider std::span or container-based copies instead of raw memcpy, which can enforce bounds at the type level.
5. Enable AddressSanitizer in CI to catch heap overflows during testing with fuzzed inputs.
6. Reference: This vulnerability maps to CWE-190: Integer Overflow or Wraparound and is closely related to CWE-122: Heap-based Buffer Overflow.
Key Takeaways
vCount * sizeof(float) * 3is dangerous whenvCountcomes from file data—always checkvCount <= SIZE_MAX / (sizeof(float) * 3)first.- A single guard using the largest downstream multiplier (
* 4for tangents) protects all smaller multiplications (* 3for positions,* 1for weights) in the same function. - Animation/model file parsers are high-value attack surfaces—they process complex binary data and are often exposed to untrusted content (downloaded assets, user uploads, mod files).
- The
(size_t)cast ontCountin the second fix prevents intermediate 32-bit truncation whentCountis declared asintbut used in asize_tmultiplication. - Regression tests using adversarial values like
0x55555556andUINT32_MAX/12 + 1ensure the overflow detection logic is never accidentally removed.
Conclusion
This vulnerability demonstrates how a simple arithmetic operation—multiplying a vertex count by a constant—can become a critical security flaw when the input is attacker-controlled. The fix is elegant in its simplicity: a single comparison against SIZE_MAX before any multiplication occurs. For developers working with binary file formats, 3D assets, or any code that computes buffer sizes from external data, this pattern should be second nature. Validate the math before you trust the result.