@@ -21,9 +21,11 @@ local DM_DIFF = 2
2121--[[
2222 Depth-stacked key scratch. A nested Map.write (Map<K, Map<K2, V>>) would
2323 clobber a single shared array mid-iteration; the stack gives each frame
24- its own slot. Mirrors Shared.acquireScratch / releaseScratch.
24+ its own slot. _keyTails parallels _keyStacks because `#keys` after
25+ `keys[count] = k` is undefined when count < the prior count.
2526]]
2627local _keyStacks : { { any } } = {}
28+ local _keyTails : { number } = {}
2729local _keyDepth = 0
2830
2931-- Private ----------------------------------------------------------------
@@ -73,23 +75,23 @@ local function sortKeys(keys: { any }, canSortDirect: boolean): ()
7375 end
7476end
7577
76- --[[
77- Acquire a scratch keys array, populate it from `value`, nil any leftover
78- tail entries from a prior reuse, then sort. Returns the keys array and
79- the populated count. Caller MUST releaseKeys() after consuming the keys.
80- ]]
8178local function collectAndSortKeys (value : { [any ]: any }, canSortDirect : boolean ): ({ any }, number )
8279 local keys = acquireKeys ()
80+ local prevTail = _keyTails [_keyDepth ] or 0
8381 local count = 0
8482 for k in value do
8583 count += 1
8684 keys [count ] = k
8785 end
88- local prevTail = # keys
89- for i = count + 1 , prevTail do
90- keys [i ] = nil
86+ if count < prevTail then
87+ for i = count + 1 , prevTail do
88+ keys [i ] = nil
89+ end
90+ end
91+ _keyTails [_keyDepth ] = count
92+ if count > 1 then
93+ sortKeys (keys , canSortDirect )
9194 end
92- sortKeys (keys , canSortDirect )
9395 return keys , count
9496end
9597
@@ -163,6 +165,11 @@ function Map.map(
163165 if payloadBytes > bufLen (src ) - (pos + lenBytes ) then
164166 Log .error (`payload {payloadBytes }B exceeds remaining buffer` )
165167 end
168+ elseif count > 0 then
169+ -- Variable-size: each pair is at least 2 bytes (key + value).
170+ if count * 2 > bufLen (src ) - (pos + lenBytes ) then
171+ Log .error (`map count {count } exceeds remaining buffer` )
172+ end
166173 end
167174
168175 local result : { [any ]: any } = {}
@@ -419,14 +426,19 @@ function Map.deltaMap(
419426 local result : { [any ]: any } = table.clone (cached )
420427 local total = 1
421428
429+ local cachedSize = 0
430+ for _ in cached do
431+ cachedSize += 1
432+ end
433+
422434 local removeCount , rcLen = varintRead (src , pos + total )
423435 if rcLen == 0 then
424436 Log .error ("truncated deltaMap remove count" )
425437 end
426- -- Both counts bounded by maxCount; otherwise a malicious DIFF
427- -- could spin the loop arbitrarily long before erroring on
428- -- buffer overrun.
429438 enforceMaxCount (removeCount , maxCount , "removeCount" )
439+ if removeCount > cachedSize then
440+ Log .error (`deltaMap removeCount {removeCount } exceeds cached size {cachedSize }` )
441+ end
430442 total += rcLen
431443 for _ = 1 , removeCount do
432444 local k , kc = keyCodec .read (src , pos + total , refs )
@@ -442,6 +454,10 @@ function Map.deltaMap(
442454 Log .error ("truncated deltaMap change count" )
443455 end
444456 enforceMaxCount (changeCount , maxCount , "changeCount" )
457+ -- Joint bound: post-DIFF size can't exceed maxCount either.
458+ if maxCount and (cachedSize - removeCount + changeCount ) > maxCount then
459+ Log .error (`deltaMap DIFF would grow map past maxCount {maxCount }` )
460+ end
445461 total += ccLen
446462 for _ = 1 , changeCount do
447463 local k , kc = keyCodec .read (src , pos + total , refs )
0 commit comments