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
|
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.
d023e70 to
97d46fb
Compare
|
The overflow check in #if defined(_M_IX86) || defined(_M_ARM) || defined(_M_HYBRID_X86_ARM64)
if (pitch > UINT32_MAX || slice > UINT32_MAX)
return HRESULT_E_ARITHMETIC_OVERFLOW;
#else
static_assert(sizeof(size_t) == 8, "Not a 64-bit platform!");
#endifOn x64 nothing inspects the result at runtime. Measured on x64,
I've added your bounds check and it's a good one — it makes every row-pitch computation provably safe, so I dropped 16 of the 34 changed lines. But it doesn't catch any of the rows above: both dimensions are already The last two rows are why I didn't simply enable the existing Swept 107,712 format/size/flag combinations against an unpatched build: all 98,230 previously-succeeding cases return byte-identical values, and the only change is 1,562 cases that used to truncate now returning the overflow HRESULT. Builds clean on MSVC /W4 x64 and x86, and clang-cl -Wall -Wextra. |
97d46fb to
550e28f
Compare
|
Both applied in 550e28f. Intrinsics — added, and confirmed via preprocessor output which branch each target actually takes:
You were right that One deviation from your snippet: I left Helper on both pitch and slice — agreed, and done. My reasoning for restricting it to slice was that the input bound makes the pitch computations provably safe (max bpp 128, so pitch stays under 2^37), but you're right that this is a distinction the next reader has to re-derive, and it quietly breaks if the bound is ever relaxed. Uniform is better. No behaviour change from either edit — the sweep is identical to the previous revision, and against unpatched The 8 red checks are unrelated to this change: 7 are an MSVC internal compiler error (C1001) building openjph for x86 under vcpkg, and 1 is a DNS failure fetching the openjph tarball. The same 7 appear on #515. Non-vcpkg x86 and all x64 configurations pass. |
| uint64_t slice = 0; | ||
| bool overflow = false; | ||
|
|
||
| // No image format expresses a dimension beyond 32 bits, and bounding them here keeps |
There was a problem hiding this comment.
Let's just remove the early-out bound check and we can rely on proper overflow detection.
There was a problem hiding this comment.
It might also make it easier to test the failure points below without this guard :)
| const size_t nbh = height >> 2; | ||
| pitch = std::max<uint64_t>(1u, uint64_t(nbw) * 8u); | ||
| slice = std::max<uint64_t>(1u, pitch * uint64_t(nbh)); | ||
| pitch = std::max<uint64_t>(1u, MulOverflow(uint64_t(nbw), 8u, overflow)); |
There was a problem hiding this comment.
Since this is now a function call, we can remove the extra casting here to simplify the code.
ComputePitch computes row and slice pitch in 64-bit arithmetic, but the check on the result is compiled out on 64-bit targets, where only a static_assert remains. The multiplies can therefore wrap modulo 2^64 and the function returns S_OK with 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 yields an undersized buffer. Route the pitch and slice multiplies through a checked helper and fail with HRESULT_E_ARITHMETIC_OVERFLOW rather than truncating. The helper uses __builtin_mul_overflow on GCC/Clang, __umulh on ARM64/ARM64EC, _umul128 on x64, and a portable division check elsewhere. No magnitude cap is introduced: slice pitches above UINT32_MAX remain valid on 64-bit, so large-but-representable surfaces such as 16384x16384 R32G32B32A32_FLOAT (slice pitch exactly 2^32) are unaffected.
550e28f to
635e90b
Compare
Guard against 64-bit overflow in
ComputePitchRevised per review feedback: adopts the suggested input bound, which lets the change shrink to the slice computation only.
The defect
ComputePitchcomputes row and slice pitch in 64-bit arithmetic, but the validation of the result is compiled out on 64-bit targets:On 32-bit that check is load-bearing and correct. On 64-bit nothing remains but a
static_assert, soslice = pitch * <height-derived multiplier>can wrap modulo 2^64 and the function returnsS_OKwith a slice pitch smaller than the row pitch.That breaks the identity the rest of the library relies on:
Callers use the slice pitch to size allocations and to bounds-check input, while the copy loops are driven by the row pitch and the scanline count. When the two disagree, the size check no longer describes the copy that follows (
DirectXTexImage.cpp:66,:370;DirectXTexDDS.cpp:1555,:1654).Concrete example
No special flags, both dimensions at exactly
UINT32_MAX:nbw = (0xFFFFFFFF + 3) / 4 = 2^30,pitch = 2^30 * 16 = 2^34,slice = 2^34 * 2^30 = 2^64→ wraps to 0. A 16 GB row pitch reported with a zero slice pitch.This matters because dimensions are not always bounded by the caller:
DecodeDDSHeaderdeliberately skips the 16384 cap underDDS_FLAGS_ALLOW_LARGE_FILES(DirectXTexDDS.cpp:649-665), andtexconv,texassembleandtexdiagall set that flag unconditionally for.ddsinput (texconv.cpp:2078,texassemble.cpp:1415/1442/1465,texdiag.cpp:550).The change
1. Bound the inputs (as suggested in review):
No image format expresses a dimension beyond 32 bits. With this in place every row-pitch computation is provably safe — max
bppis 128, so the largest pitch (PAGE4K) stays under 2^37 — and the incidental additions (+ 3,+ 32767,height + ((height + 1) >> 1)) can no longer wrap either.2. Check the multiplies. Bounding the inputs is not sufficient on its own: with
width, height <= UINT32_MAXthe row pitch can still reach ~2^36 and the multiplier ~2^33, soslice = pitch * multiplierreaches ~2^69 and wraps. Every pitch and slice multiply therefore goes through a checked helper, and the flag is tested once alongside the existing 32-bit check:Confirmed by preprocessor output that x64 selects
_umul128, ARM64/ARM64EC selects__umulh, and x86 falls to the portable division check (no 64-bit widening intrinsic there).intrin.harrives via DirectXMath, so no new include is needed.<intsafe.h>was avoided since this also builds for WSL/Linux and macOS.This detects overflow; it does not impose a magnitude cap. Slice pitches above
UINT32_MAXremain valid on 64-bit, so a16384 x 16384R32G32B32A32_FLOATsurface — the D3D12 maximum 2D dimension, requiring no special flags, whose slice pitch is exactly 2^32 — continues to work.Validation
Compiles clean with no warnings: MSVC
/W4x64, MSVC/W4x86, and clang-cl-Wall -Wextra. Full x64 Release library builds via CMake.ComputePitchwas swept over 107,712 combinations — 17 formats covering every branch of theswitch, 24 widths x 24 heights (powers of two, off-by-ones,16384/16385,0x80000000,0xFFFFFFFF), and all 11CP_FLAGS— with results compared against an unpatched build. Wrap detection uses the identity above (CP_FLAGS_BAD_DXTN_TAILSexcluded, since it intentionally usesheight >> 2whereComputeScanlinesusesmax(1, (height + 3) / 4)).mainThe middle row is why the slice checks are retained: bounding the inputs alone changes nothing measurable, because every dimension involved is already
<= UINT32_MAX— the overflow is in the product, not the operands.Behavioural diff against unpatched, same 107,712 vectors:
S_OK->HRESULT_E_ARITHMETIC_OVERFLOWS_OKEvery previously-succeeding non-overflowing case returns bit-identical values. Spot checks:
width > UINT32_MAXE_INVALIDARG(new bound)BC7_UNORM0xFFFFFFFFx0xFFFFFFFFHRESULT_E_ARITHMETIC_OVERFLOWR32G32B32A32_FLOAT16384x16384S_OK, 262144 / 4294967296 — unchangedNV1265536x65536S_OK, 65536 / 6442450944 — unchangedR8G8B8A8_UNORM4096x4096S_OK, 16384 / 67108864 — unchangedNotes
CP_FLAGS_LIMIT_4GBdoes not cover this.DetermineImageArray(DirectXTexImage.cpp:127) teststotalPixelSize, which is the sum of already-wrapped slice pitches — it runs downstream of the wrap and cannot observe it.DetermineImageArrayis already defended.SetupImageArray(:197-201) walkspixels += slicePitchand fails once the running pointer passespEndBits, catching a wrapped total incrementally. No change made there.slicePitch == rowPitch * ComputeScanlines(...)assertion inCopyImagewas considered and rejected — the identity does not hold underCP_FLAGS_BAD_DXTN_TAILS, so it would fire on legitimate legacy DXTn content.