Redis XAUTOCLAIM bounds COUNT before multiplying allocation sizes
Redis 7.0.5 fixes an integer overflow in XAUTOCLAIM's COUNT handling. The corrected limit covers both the deleted-ID allocation and the command's scan-attempt budget.
Accepting a positive integer is not enough validation when a command multiplies that integer to allocate memory. Redis's XAUTOCLAIM uses COUNT both for a result buffer and for a larger scan-attempt budget. Redis 7.0.5 limits the input before those calculations, fixing CVE-2022-35951.
The 7.0.5 release notes are dated September 21. The maintainer advisory, published September 22, describes an integer overflow followed by a heap overflow and possible remote code execution. It credits Xion, SeungHyun Lee, of KAIST GoN. This is a distinct failure from the deleted-entry response accounting repaired in July.
I value the way Redis exposes consumer-group ownership explicitly. Its recovery commands are still ordinary commands receiving untrusted arguments, and their memory calculations need to remain valid for values far outside a reasonable application's normal requests.
A requested result count becomes two calculations
The change from 7.0.4 to 7.0.5 shows the relevant validation. Previously, parsing accepted a count between one and LONG_MAX. The command later multiplied that count by the size of a streamID to allocate the array used for deleted IDs. It also multiplied the count by ten to derive the number of scan attempts.
The input could fit in the parsed integer type while a derived calculation did not. That is the core distinction. A parser proving that a value is representable has not proved that every multiplication involving it remains representable. The command needs a bound that accounts for what it will do with the value afterward.
The allocation is needed because XAUTOCLAIM can remove obsolete pending records whose messages no longer exist in the stream. Those deleted IDs appear in a separate reply element. A caller asking for reassignment can therefore cause Redis to accumulate cleanup results as well as return live messages.
The scan budget has a different purpose. Pending entries may be ineligible for transfer because they have not been idle long enough. Redis allows more attempts than requested results so it can encounter such entries without assuming every examined record becomes a result. That budget is a multiplication too, even though it does not directly specify a buffer size.
The new maximum accounts for both multipliers
The fix introduces a named attempt factor with the value ten. It computes the maximum accepted count by dividing LONG_MAX by the larger of that factor and sizeof(streamID). The parser then enforces that derived upper bound instead of accepting the full positive long range.
This is a conservative bound shared by the two calculations. A value below it can be multiplied by either quantity without exceeding the chosen limit. The validation makes the relationship between input range and later arithmetic explicit in one place.
The following C excerpt summarizes the bound present in the tagged implementation. It describes validation logic, not a count value that an application should use.
const unsigned attempts_factor = 10;
long max_count = LONG_MAX / max(sizeof(streamID), attempts_factor);
Dividing before multiplying matters. Computing a large product first and then comparing the result would already have performed the operation whose overflow needs prevention. The maximum is instead derived from the available range and the known factors before the untrusted count reaches either multiplication.
The patch also uses the named factor when calculating scan attempts. That keeps the validation and use visibly connected. If the attempt factor were written independently in different places, a future change to one could leave the bound inconsistent with the calculation it is meant to protect.
Allocation failure becomes a command error
The 7.0.5 stream source also switches the deleted-ID allocation to ztrymalloc. If allocation fails, the command returns an error explaining that transient memory could not be allocated and the count is too high. It returns before constructing the normal reply and entering the claim loop.
This handles a separate boundary from arithmetic overflow. A request can have a representable allocation size and still ask for more memory than the process can obtain. Range validation proves that the requested size calculation is meaningful. The fallible allocation path handles inability to satisfy that meaningful request.
Moving the allocation earlier also gives the command a clean point to refuse the operation. It has checked the stream and group, but it has not started assembling the normal three-part response or traversing pending entries for the claim. The memory error can therefore be reported as a command failure at that stage.
The distinction is useful for client behavior. A rejected count is an argument validation failure. A failed transient allocation is a resource failure after valid parsing. Neither should be interpreted as a reply saying that no pending work exists. The command did not complete its ordinary traversal and ownership processing.
July's fix protected a different invariant
The July 7.0.4 change added a decrement when a deleted pending entry is reported. That made deleted results consume the same remaining count as live results. It prevented traversal from recording more deleted IDs than the allocated count allowed through that particular path.
The September fix addresses the calculation that establishes the allocation and attempt budget in the first place. Both defects concern COUNT, but they violate different relationships. One allowed a loop branch to exceed an intended budget. The other allowed the arithmetic defining that budget to overflow.
Keeping those explanations separate helps interpret the affected versions. Having the earlier decrement does not supply the newer input bound. Redis 7.0.5 contains the additional validation and allocation handling described here. The advisory identifies that version as patched for this vulnerability.
This also illustrates why a security update should be described by its actual change. A statement that the command was fixed once cannot establish that every use of its input has been checked. The relevant question for this issue is whether the server enforces the derived maximum before it calculates buffer size and scan work.
The implementation parses optional arguments before reporting a missing stream or group. Its source comments explain that syntax errors should take precedence. The new maximum is enforced during that parsing phase, so an invalid count can be refused without entering the claim operation. The key lookup and group checks do not substitute for argument validation.
The default count remains 100 in the tagged source. A caller that omits COUNT still follows that default path, while an explicit value has to pass the range check. This preserves the ordinary interface for applications using small batches. The defect lies in accepting values whose derived calculations are unsafe, not in requiring every application to supply a new option or adopt a different consumer-group model.
The regression test rejects an out-of-range request
The patch adds a test that passes an out-of-range count and expects a count-related error. That is a direct check of the new parser boundary. It does not rely on successfully allocating a huge buffer or reproducing memory corruption to establish that the vulnerable path is closed for that input.
The test belongs beside the existing stream consumer-group tests because the command still needs to preserve ordinary claim behavior. The fix changes which counts are valid and how allocation failure is reported. It does not remove ownership reassignment, the cursor, or the deleted-ID reply element.
An application should continue selecting counts appropriate to its response size and processing loop. The new maximum is an implementation safety bound, not a recommended batch size. A value can be below that bound and still request an impractical amount of transient memory, which is why allocation failure handling remains necessary.
I prefer the two explicit refusal points in this repair. Invalid derived arithmetic is prevented at argument parsing, and unavailable transient memory produces a command error before claim processing begins. Both keep implementation limits separate from the application's normal interpretation of a successful claim reply.
The release notes classify 7.0.5 as a security update and describe the overflow's possible impact. The implementation makes the repair assessable. COUNT has a maximum derived from both uses, the scan factor is shared with that calculation, and the deleted-ID buffer has a checked allocation path. Those changes preserve the stream recovery operation while bounding the arithmetic and memory on which it depends.