[Util] Fix 32-bit wrap of util.string.format placeholder-count check (#24685)
## Summary
Compute `util.string.format`'s expected-operand count in 64-bit so a
near-`UINT_MAX` placeholder index cannot wrap the check and slip past
verification into an out-of-bounds access.
## Context
`util.string.format` interpolates its operands into `{N}`-style
placeholders. Its verifier — and `StringFormatOp::fold` — determine how
many operands the op should have from the highest placeholder index in
the format string, computed as `maxArgIndex + 1`. `maxArgIndex` was a
32-bit `unsigned`.
## Problem
A format string with an explicit placeholder index of `UINT_MAX` makes
`maxArgIndex + 1` overflow to `0`, so the verifier expects `0` operands
and accepts the op even though the placeholder references argument
`4294967295`. `fold` (and the verifier's own indexing) then read
`argStrings[4294967295]` on an empty list — an out-of-bounds read. For
example, before this change the following op passed verification:
```mlir
%0 = util.string.format "{4294967295}"() : () -> !util.buffer
```
## Fix
Cast `maxArgIndex` to `uint64_t` so `maxArgIndex + 1` no longer wraps; an
out-of-range placeholder is now rejected at verification. The same
computation exists in two copies of `parseFormatString` (the verifier in
`UtilOps.cpp` and the folder in `UtilOpFolders.cpp`); both are updated.
`string_format_index_overflow` in `op_verification.mlir` checks the op
above is now rejected with a diagnostic instead of passing verification.
---
Disclosure: this contribution was authored with an AI coding assistant
(Claude) and reviewed before submission.
Signed-off-by: Eylon Krause <eylon1909@gmail.com>diff --git a/compiler/src/iree/compiler/Dialect/Util/IR/UtilOpFolders.cpp b/compiler/src/iree/compiler/Dialect/Util/IR/UtilOpFolders.cpp
index 09297de..7b57903 100644
--- a/compiler/src/iree/compiler/Dialect/Util/IR/UtilOpFolders.cpp
+++ b/compiler/src/iree/compiler/Dialect/Util/IR/UtilOpFolders.cpp
@@ -1446,8 +1446,14 @@
return failure();
}
- unsigned expectedArgCount =
- hasAnyArg ? (hasExplicit ? (maxArgIndex + 1) : nextSequentialIndex) : 0;
+ // Compute in 64-bit: an explicit placeholder can be as large as UINT_MAX, so
+ // `maxArgIndex + 1` would wrap to 0 in 32-bit and spuriously match an empty
+ // argument list, letting an out-of-range placeholder through to the indexing
+ // below.
+ uint64_t expectedArgCount =
+ hasAnyArg ? (hasExplicit ? (static_cast<uint64_t>(maxArgIndex) + 1)
+ : nextSequentialIndex)
+ : 0;
if (expectedArgCount != argCount) {
return failure();
}
diff --git a/compiler/src/iree/compiler/Dialect/Util/IR/UtilOps.cpp b/compiler/src/iree/compiler/Dialect/Util/IR/UtilOps.cpp
index 7c31bd7..536d253 100644
--- a/compiler/src/iree/compiler/Dialect/Util/IR/UtilOps.cpp
+++ b/compiler/src/iree/compiler/Dialect/Util/IR/UtilOps.cpp
@@ -2918,8 +2918,13 @@
return failure();
}
- unsigned expectedArgCount =
- hasAnyArg ? (hasExplicit ? (maxArgIndex + 1) : nextSequentialIndex) : 0;
+ // Compute in 64-bit: an explicit placeholder can be as large as UINT_MAX, so
+ // `maxArgIndex + 1` would wrap to 0 in 32-bit and spuriously match an empty
+ // argument list, wrongly accepting an out-of-range placeholder.
+ uint64_t expectedArgCount =
+ hasAnyArg ? (hasExplicit ? (static_cast<uint64_t>(maxArgIndex) + 1)
+ : nextSequentialIndex)
+ : 0;
if (expectedArgCount != argCount) {
return failure();
}
diff --git a/compiler/src/iree/compiler/Dialect/Util/IR/test/op_verification.mlir b/compiler/src/iree/compiler/Dialect/Util/IR/test/op_verification.mlir
index 69a3cf1..1a2a7b5 100644
--- a/compiler/src/iree/compiler/Dialect/Util/IR/test/op_verification.mlir
+++ b/compiler/src/iree/compiler/Dialect/Util/IR/test/op_verification.mlir
@@ -71,3 +71,13 @@
%0 = "arith.constant"() <{value = 0 : i32}> : () -> i32
"util.return"(%0) : (i32) -> ()
}) : () -> ()
+
+// -----
+
+util.func public @string_format_index_overflow() -> !util.buffer {
+ // A near-UINT_MAX explicit placeholder index must not wrap the argument-count
+ // check and slip past verification with too few operands.
+ // expected-error @+1 {{format string placeholder count does not match the 0 provided argument(s)}}
+ %0 = util.string.format "{4294967295}"() : () -> !util.buffer
+ util.return %0 : !util.buffer
+}