Guard against 64-bit overflow in ComputePitch - #733
Guard against 64-bit overflow in ComputePitch#733Roland Shum (ShumWengSang) wants to merge 1 commit into
Conversation
ComputePitch computes the row pitch and slice pitch in 64-bit arithmetic, but only validated the result on 32-bit platforms where size_t forces a UINT32_MAX bound. On 64-bit platforms sufficiently large dimensions could wrap the pitch/slice multiplies, yielding a slice pitch inconsistent with the row pitch and scanline count. Callers use the slice pitch to size allocations and to bounds-check input, so a wrapped value produces an undersized buffer. Route every multiply through a checked helper and fail with HRESULT_E_ARITHMETIC_OVERFLOW rather than truncating. No magnitude cap is introduced: slice pitches above UINT32_MAX remain valid on 64-bit, so large-but-representable surfaces (e.g. 16384x16384 R32G32B32A32_FLOAT, whose slice pitch is exactly 2^32) are unaffected.
|
The function uses 64-bit integer math already to deal with overflow detection. If there are specific input values that overflow, please provide some examples so we can verify any fix/change here. I think the majority of this PR is unnecessary. The one change I do believe may be needed is an initial bounds-check on the size width/height values coming in for 64-bit builds (for 32-bit builds is already going to be bounded by UINT32_MAX). In practice, most of the calling code has already done the bounds check but it's reasonable to add it here since ComputePitch is a public-facing API. IOW, the only change I think is needed here is: |
Chuck Walbourn (walbourn)
left a comment
There was a problem hiding this comment.
Most of this is not needed. You are free to submit a revision that only adds the initial bounds check to make sure the values aren't exceeding 32-bit to begin with.
Guard against 64-bit overflow in
ComputePitchSummary
ComputePitchperforms its row-pitch and slice-pitch computations in 64-bit arithmetic, but only validates the result on 32-bit platforms:On 32-bit targets that check is load-bearing, because
size_tcannot represent the result. On 64-bit targets there is no validation at all — thestatic_assertis a compile-time assertion, not a runtime guard. Sufficiently large dimensions therefore wrap thepitch * heightmultiply modulo 2^64 andComputePitchreturnsS_OKwith a slice pitch that is much smaller than the row pitch.That breaks an invariant the rest of the library depends on:
Callers rely on the slice pitch both to size allocations and to bounds-check input, while the copy loops are driven by the row pitch and scanline count. When the two disagree, the size check validates against a value that no longer describes the copy that follows:
DetermineImageArray(DirectXTexImage.cpp:66) accumulatesslicePitchintopixelSizeScratchImage::Initialize(DirectXTexImage.cpp:370) allocatespixelSizeCopyImage(DirectXTexDDS.cpp:1555) gates onif (pixelSize > size)DirectXTexDDS.cpp:1654) then copiesrowPitchbytes per rowReachable from any loader that accepts dimensions larger than the D3D limits — e.g. the DDS loader under
DDS_FLAGS_ALLOW_LARGE_FILES, which bypasses the 16384 dimension cap inDecodeDDSHeader(DirectXTexDDS.cpp:649-665).texconv,texassembleandtexdiagall set that flag unconditionally for.ddsinput (texconv.cpp:2078,texassemble.cpp:1415/1442/1465,texdiag.cpp:550).Change
Every multiply in
ComputePitchnow goes through a small checked helper that records overflow in a local flag; the flag is tested once, next to the existing 32-bit check, and returnsHRESULT_E_ARITHMETIC_OVERFLOW.MSVC has no
__builtin_mul_overflow, so it takes the division-based path; clang-cl, clang and GCC take the intrinsic.<intsafe.h>was deliberately not used since the library also builds for WSL/Linux and macOS. This is not on a hot path —ComputePitchis called once per subresource, not per pixel.This detects overflow; it does not impose a magnitude cap. That distinction matters. Simply removing the
_M_IX86gate so the existing> UINT32_MAXcheck applies everywhere would also stop the wrap, but it would reject slice pitches above 4 GB on 64-bit — which are legal and reachable well inside D3D limits. A16384 x 16384R32G32B32A32_FLOATsurface (the D3D12 maximum 2D dimension, requiring no special flags) has a slice pitch of exactly2^32— one byte overUINT32_MAX. Capping would break loading it on x64. The check added here leaves it working.Validation
Built
x64Release via CMake, plus a standalone/W4compile and aclang-cl -Wall -Wextracompile to exercise both helper branches. No new diagnostics.To confirm the change is purely additive,
ComputePitchwas swept over 107,712 combinations — 17 formats covering every branch of theswitch(BC, packed, planar, and the default bpp path), 24 dimensions each for width and height (powers of two, off-by-ones,16384/16385,0x80000000,0xFFFFFFFF, and values chosen to wrap), and all 11CP_FLAGS— with the results diffed against the unpatched build:S_OK→HRESULT_E_ARITHMETIC_OVERFLOWS_OKEvery case that previously succeeded without overflowing returns bit-identical values. The only behavioural change is that inputs which used to silently wrap now fail cleanly. Spot checks:
R32G32B32A32_FLOAT16384×16384S_OK262144 / 4294967296NV1265536×65536S_OK65536 / 6442450944BC7_UNORM16384×16384S_OK65536 / 268435456S_OK4295098370 / 4HRESULT_E_ARITHMETIC_OVERFLOWNotes for reviewers
CP_FLAGS_LIMIT_4GBdoes not help here. The check inDetermineImageArray(DirectXTexImage.cpp:127) teststotalPixelSize, which is the sum of already-wrapped slice pitches. It runs downstream of the wrap and cannot observe it.DetermineImageArraylooks similar but is already defended.SetupImageArray(DirectXTexImage.cpp:197-201) walkspixels += slicePitchand fails as soon as the running pointer passespEndBits, which catches a wrapped total incrementally. No change made there.slicePitch == rowPitch * ComputeScanlines(...)assertion inCopyImagewas considered and rejected. The invariant holds for every format except underCP_FLAGS_BAD_DXTN_TAILS, which computesheight >> 2whereComputeScanlinesreturnsmax(1, (height + 3) / 4); those differ for any height that is not a multiple of 4, so the assertion would fire on legitimate legacy DXTn content.+ 3u,+ 32767u,height + ((height + 1) >> 1)) could in principle wrap, but only forsize_tinputs at or near 2^64, which no image format can express and no loader can produce — dimensions are at most 32-bit on every input path. Flagging in case you'd prefer an explicit input bound instead.walbourn/directxtextest, since that is where the suite lives; happy to open a companion PR adding the overflow cases to theComputePitchunit tests.fuzzloadersalready setsDDS_FLAGS_ALLOW_LARGE_FILES | DDS_FLAGS_PERMISSIVE(fuzzloaders.cpp:59), so the affected configuration has been under coverage-guided fuzzing for years. The wrap opens no new edge — a wrapped slice pitch follows exactly the same path as a legitimately small surface — so there is no coverage signal to hill-climb on, and unsigned wrap is well-defined so ASan cannot see it. The interesting value is not in either dimension field but in their product, which mutation is unlikely to land on by chance.