Skip to content

Commit 8fb9770

Browse files
committed
ye
1 parent d2997d4 commit 8fb9770

27 files changed

Lines changed: 987 additions & 125 deletions

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Schemas, packets, queries, groups, validation, rate limiting. Every send batches
1616
Wally — add to your `wally.toml`:
1717

1818
```toml
19-
Lync = "axp3cter/lync@2.3.2"
19+
Lync = "axp3cter/lync@2.3.3"
2020
```
2121

2222
npm (roblox-ts):

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@axpecter/lync",
3-
"version": "2.3.2",
3+
"version": "2.3.3",
44
"description": "Buffer networking for Roblox. Delta compression, XOR framing, built-in security",
55
"main": "src/init.luau",
66
"types": "src/index.d.ts",

src/api/Packet.luau

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -152,15 +152,13 @@ function Packet.define<T>(
152152
local payload = scratch.buff
153153
local payloadLen = scratch.cursor
154154
local server = Transport.server()
155+
local getChannel = if isUnreliable
156+
then server.getUnreliableChannel
157+
else server.getReliableChannel
155158
-- Channel.writeBatchEncoded is hot-swapped by enableStats; re-read per call.
156159
local writeBatchEncoded = Channel.writeBatchEncoded
157160
for i = 1, count do
158-
writeBatchEncoded(
159-
server.getChannel(players[i], isUnreliable),
160-
reg,
161-
payload,
162-
payloadLen
163-
)
161+
writeBatchEncoded(getChannel(players[i]), reg, payload, payloadLen)
164162
end
165163
Shared.releaseScratch()
166164
bumpFires()

src/api/Query.luau

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,10 @@ local function writeQueryTracked(
4444
): ()
4545
if Channel.statsEnabled() then
4646
local before = ch.cursor
47-
Channel.writeQuery(ch, reg.id, corrId, codec, data)
47+
Channel.writeQuery(ch, reg.id, corrId, codec, data, reg.name)
4848
reg.bytesSent += ch.cursor - before
4949
else
50-
Channel.writeQuery(ch, reg.id, corrId, codec, data)
50+
Channel.writeQuery(ch, reg.id, corrId, codec, data, reg.name)
5151
end
5252
end
5353

src/api/Signal.luau

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,16 @@ local _freeThread: thread? = nil
1717

1818
-- Private ----------------------------------------------------------------
1919

20+
-- pcall wrapper: a handler error must not consume the recycled thread, otherwise
21+
-- every subsequent fire pays a coroutine.create until process restart.
2022
local function passer(fn: (...any) -> (), ...): ()
2123
local thread = _freeThread
2224
_freeThread = nil
23-
fn(...)
25+
local ok, err = pcall(fn, ...)
2426
_freeThread = thread
27+
if not ok then
28+
task.spawn(error, err)
29+
end
2530
end
2631

2732
local function yielder(): ()

src/codec/Base.luau

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ local Types = require(script.Parent.Parent.Types)
77

88
-- Constants --------------------------------------------------------------
99

10-
local DEFAULT_MAX = 262144
10+
local DEFAULT_MAX = 1048576
1111
local INITIAL_BUF = 1024
1212

1313
-- State ------------------------------------------------------------------

src/codec/composite/Array.luau

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ local function makeBoolArray(
5353
write = function(ch: Types.ChannelState, value: { boolean }): ()
5454
local len = #value
5555
varintWrite(ch, len)
56-
packBoolArray(ch, value, len)
56+
packBoolArray(ch, value, len, if len > 0 then boolBytes(len) else 0)
5757
end,
5858

5959
read = function(src: buffer, pos: number, _refs: { Instance }?): ({ boolean }, number)
@@ -69,7 +69,7 @@ local function makeBoolArray(
6969
Log.error(`payload {byteCount}B exceeds remaining buffer`)
7070
end
7171
local result = table.create(len, false)
72-
unpackBoolArray(src, dataStart, len, result)
72+
unpackBoolArray(src, dataStart, len, result, byteCount)
7373
return result, lenBytes + byteCount
7474
end,
7575
}
@@ -157,6 +157,11 @@ local function makeGenericArray(
157157
if len == 0 then
158158
return {}, lenBytes
159159
end
160+
-- Every element occupies at least 1 byte; len > remaining is
161+
-- unambiguously corrupt and would OOM table.create otherwise.
162+
if len > bufLen(src) - (pos + lenBytes) then
163+
Log.error(`array length {len} exceeds remaining buffer`)
164+
end
160165

161166
local result = table.create(len)
162167
local total = lenBytes
@@ -415,22 +420,28 @@ function Array.deltaArray(
415420
if cLen == 0 then
416421
Log.error("truncated deltaArray change count")
417422
end
418-
-- changeCount is also bounded by newLen: every index appears at most once.
419423
if changeCount > newLen then
420424
Log.error(`deltaArray changeCount {changeCount} exceeds newLen {newLen}`)
421425
end
422426
total += cLen
423427

428+
-- The writer marks every appended index (cachedLen+1 .. newLen)
429+
-- as changed; the reader must enforce this so the wire can't
430+
-- ship a holey baseline that breaks #result downstream.
431+
local appendedNeeded = if newLen > cachedLen then newLen - cachedLen else 0
432+
local appendedSeen = 0
433+
424434
for _ = 1, changeCount do
425435
local idx, iLen = varintRead(src, pos + total)
426436
if iLen == 0 then
427437
Log.error("truncated deltaArray index")
428438
end
429-
-- Reject out-of-range idx so the wire can't poison the cache with
430-
-- arbitrary keys that survive truncation.
431439
if idx < 1 or idx > newLen then
432440
Log.error(`deltaArray index {idx} out of range [1, {newLen}]`)
433441
end
442+
if idx > cachedLen then
443+
appendedSeen += 1
444+
end
434445
total += iLen
435446
local v, vc = element.read(src, pos + total, refs)
436447
if vc == 0 then
@@ -440,6 +451,12 @@ function Array.deltaArray(
440451
result[idx] = v
441452
end
442453

454+
if appendedSeen ~= appendedNeeded then
455+
Log.error(
456+
`deltaArray PATCH grew length to {newLen} but only {appendedSeen}/{appendedNeeded} appended indices supplied`
457+
)
458+
end
459+
443460
for i = newLen + 1, cachedLen do
444461
result[i] = nil
445462
end

src/codec/composite/Map.luau

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -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
]]
2627
local _keyStacks: { { any } } = {}
28+
local _keyTails: { number } = {}
2729
local _keyDepth = 0
2830

2931
-- Private ----------------------------------------------------------------
@@ -73,23 +75,23 @@ local function sortKeys(keys: { any }, canSortDirect: boolean): ()
7375
end
7476
end
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-
]]
8178
local 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
9496
end
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

Comments
 (0)