From be00e6d6e8970c58c91d7b28ca6aa776aaeaf162 Mon Sep 17 00:00:00 2001 From: Toys0125 Date: Thu, 25 Jun 2026 19:35:45 -0500 Subject: [PATCH 01/11] Have the last HMDActive Time set if hmdGaze is true. --- .../Device Management/EyeTracking/BasisEyeTrackingManager.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Basis/Packages/com.basis.framework/Device Management/EyeTracking/BasisEyeTrackingManager.cs b/Basis/Packages/com.basis.framework/Device Management/EyeTracking/BasisEyeTrackingManager.cs index 2486daa05e..b91de57acf 100644 --- a/Basis/Packages/com.basis.framework/Device Management/EyeTracking/BasisEyeTrackingManager.cs +++ b/Basis/Packages/com.basis.framework/Device Management/EyeTracking/BasisEyeTrackingManager.cs @@ -105,6 +105,10 @@ public static void Simulate() } bool hmdGaze = hmdHas && (hmdData.HasWorldRay || hmdData.HasPerEyeAngles); + if (hmdGaze) + { + _lastHmdActiveTime = Time.unscaledTime; + } bool oscGaze = oscHas && (oscData.HasWorldRay || oscData.HasPerEyeAngles); BasisEyeTrackingData merged = default; From d228a574cfcd943b258ae0e86a949d774df1e1ed Mon Sep 17 00:00:00 2001 From: Toys0125 Date: Thu, 25 Jun 2026 19:37:53 -0500 Subject: [PATCH 02/11] Extra packet validation. --- .../com.basis.framework/Networking/Sync/BasisSyncReceiver.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Basis/Packages/com.basis.framework/Networking/Sync/BasisSyncReceiver.cs b/Basis/Packages/com.basis.framework/Networking/Sync/BasisSyncReceiver.cs index 70a9ec162c..bcb24ac8b9 100644 --- a/Basis/Packages/com.basis.framework/Networking/Sync/BasisSyncReceiver.cs +++ b/Basis/Packages/com.basis.framework/Networking/Sync/BasisSyncReceiver.cs @@ -94,7 +94,7 @@ public void Configure(bool extrapolate, double maxExtrapSeconds, bool teleport, public void OnPacket(byte[] payload, int length) { - if (payload == null || length < BasisSyncCodec.HeaderSize) return; + if (payload == null || length < BasisSyncCodec.HeaderSize || length > payload.Length || length > _maxPacket) return; // Drop corrupted packets before they touch the sequence/baseline, so a corrupted sequence number // can't poison the high-water-mark and a corrupted value is never applied. if (_verifyChecksum && !BasisSyncCodec.VerifyChecksum(payload, length)) return; From aa14c058b28f3c9c007d76159b617b969fd1f7fe Mon Sep 17 00:00:00 2001 From: Toys0125 Date: Thu, 25 Jun 2026 19:40:35 -0500 Subject: [PATCH 03/11] * Buffer lenght validation. * Confirm fields exist before applying. * Fixed sized bounds checked before read. * Safe Variant Reads. * BasisSyncCodec.Deserialize() rejects malformed packets safely. --- .../Networking/Sync/BasisSyncCodec.cs | 185 ++++++++++++++---- 1 file changed, 150 insertions(+), 35 deletions(-) diff --git a/Basis/Packages/com.basis.framework/Networking/Sync/BasisSyncCodec.cs b/Basis/Packages/com.basis.framework/Networking/Sync/BasisSyncCodec.cs index b39bf02b10..86dca77dff 100644 --- a/Basis/Packages/com.basis.framework/Networking/Sync/BasisSyncCodec.cs +++ b/Basis/Packages/com.basis.framework/Networking/Sync/BasisSyncCodec.cs @@ -103,7 +103,7 @@ public static int Serialize(BasisSyncSchema schema, BasisSyncValues values, bool /// True if the trailing 2-byte Fletcher-16 matches the packet body. Call before trusting a packet's sequence/values. public static bool VerifyChecksum(byte[] buf, int length) { - if (buf == null || length < HeaderSize + ChecksumSize) return false; + if (buf == null || length < HeaderSize + ChecksumSize || length > buf.Length) return false; int payloadLen = length - ChecksumSize; ushort expected = Fletcher16(buf, payloadLen); ushort actual = (ushort)(buf[payloadLen] | (buf[payloadLen + 1] << 8)); @@ -126,7 +126,7 @@ public static bool Deserialize(BasisSyncSchema schema, byte[] buf, int length, B seq = 0; keyframe = false; intervalMs = 0; - if (buf == null || length < HeaderSize) return false; + if (buf == null || length < HeaderSize || length > buf.Length) return false; int o = 0; seq = buf[o++]; @@ -134,23 +134,44 @@ public static bool Deserialize(BasisSyncSchema schema, byte[] buf, int length, B keyframe = (flags & FlagKeyframe) != 0; intervalMs = (ushort)(buf[o] | (buf[o + 1] << 8)); o += 2; + int fieldCount = schema.FieldCount; if (keyframe) { + var verify = new BitReader(buf, o, length); + for (int fi = 0; fi < fieldCount; fi++) + { + if (!SkipField(schema, schema.GetField(fi), ref verify)) return false; + } + var r = new BitReader(buf, o, length); - for (int fi = 0; fi < schema.FieldCount; fi++) - DecodeField(schema, schema.GetField(fi), baseline, ref r); + for (int fi = 0; fi < fieldCount; fi++) + { + if (!DecodeField(schema, schema.GetField(fi), baseline, ref r)) return false; + } } else { int maskStart = o; o += schema.DirtyMaskBytes; if (o > length) return false; + + var verify = new BitReader(buf, o, length); + for (int fi = 0; fi < fieldCount; fi++) + { + if ((buf[maskStart + (fi >> 3)] & (1 << (fi & 7))) != 0) + { + if (!SkipField(schema, schema.GetField(fi), ref verify)) return false; + } + } + var r = new BitReader(buf, o, length); - for (int fi = 0; fi < schema.FieldCount; fi++) + for (int fi = 0; fi < fieldCount; fi++) { if ((buf[maskStart + (fi >> 3)] & (1 << (fi & 7))) != 0) - DecodeField(schema, schema.GetField(fi), baseline, ref r); + { + if (!DecodeField(schema, schema.GetField(fi), baseline, ref r)) return false; + } } } return true; @@ -187,7 +208,7 @@ private static void EncodeField(BasisSyncSchema schema, in BasisSyncField f, Bas } } - private static void DecodeField(BasisSyncSchema schema, in BasisSyncField f, BasisSyncValues v, ref BitReader r) + private static bool DecodeField(BasisSyncSchema schema, in BasisSyncField f, BasisSyncValues v, ref BitReader r) { switch (f.Pool) { @@ -195,12 +216,13 @@ private static void DecodeField(BasisSyncSchema schema, in BasisSyncField f, Bas for (int c = 0; c < f.ContComponents; c++) { int ci = f.Offset + c; - v.Cont[ci] = DecodeCont(ref r, schema.QuantMode(ci), schema.QuantMin(ci), schema.QuantMax(ci), schema.QuantBits(ci)); + if (!TryDecodeCont(ref r, schema.QuantMode(ci), schema.QuantMin(ci), schema.QuantMax(ci), schema.QuantBits(ci), out float value)) return false; + v.Cont[ci] = value; } break; case BasisSyncPool.Rotation: { - uint packed = r.ReadBits(32); + if (!r.TryReadBits(32, out uint packed)) return false; UnityEngine.Quaternion uq = BasisCompression.QuaternionCompressor.DecompressQuaternion(packed); v.Rot[f.Offset] = new quaternion(uq.x, uq.y, uq.z, uq.w); break; @@ -208,14 +230,66 @@ private static void DecodeField(BasisSyncSchema schema, in BasisSyncField f, Bas default: switch (f.Type) { - case BasisSyncFieldType.Bool: v.Disc[f.Offset] = r.ReadBits(1) != 0 ? 1 : 0; break; - case BasisSyncFieldType.Byte: v.Disc[f.Offset] = (int)r.ReadBits(8); break; - case BasisSyncFieldType.UShort: v.Disc[f.Offset] = (int)r.ReadBits(16); break; - case BasisSyncFieldType.UInt: v.Disc[f.Offset] = (int)ReadVarUInt(ref r); break; - default: v.Disc[f.Offset] = ReadVarInt(ref r); break; + case BasisSyncFieldType.Bool: + { + if (!r.TryReadBits(1, out uint value)) return false; + v.Disc[f.Offset] = value != 0 ? 1 : 0; + break; + } + case BasisSyncFieldType.Byte: + { + if (!r.TryReadBits(8, out uint value)) return false; + v.Disc[f.Offset] = (int)value; + break; + } + case BasisSyncFieldType.UShort: + { + if (!r.TryReadBits(16, out uint value)) return false; + v.Disc[f.Offset] = (int)value; + break; + } + case BasisSyncFieldType.UInt: + { + if (!TryReadVarUInt(ref r, out uint value)) return false; + v.Disc[f.Offset] = (int)value; + break; + } + default: + { + if (!TryReadVarInt(ref r, out int value)) return false; + v.Disc[f.Offset] = value; + break; + } } break; } + return true; + } + + private static bool SkipField(BasisSyncSchema schema, in BasisSyncField f, ref BitReader r) + { + switch (f.Pool) + { + case BasisSyncPool.Continuous: + for (int c = 0; c < f.ContComponents; c++) + { + if (!r.SkipBits(ContBits(schema, f.Offset + c))) return false; + } + return true; + case BasisSyncPool.Rotation: + return r.SkipBits(32); + default: + switch (f.Type) + { + case BasisSyncFieldType.Bool: return r.SkipBits(1); + case BasisSyncFieldType.Byte: return r.SkipBits(8); + case BasisSyncFieldType.UShort: return r.SkipBits(16); + case BasisSyncFieldType.UInt: + default: + uint ignored; + return TryReadVarUInt(ref r, out ignored); + } + } } private static void EncodeCont(ref BitWriter w, float value, BasisQuantMode mode, float min, float max, int bits) @@ -241,30 +315,42 @@ private static void EncodeCont(ref BitWriter w, float value, BasisQuantMode mode } } - private static float DecodeCont(ref BitReader r, BasisQuantMode mode, float min, float max, int bits) + private static bool TryDecodeCont(ref BitReader r, BasisQuantMode mode, float min, float max, int bits, out float value) { + value = 0f; switch (mode) { case BasisQuantMode.Half: - return math.f16tof32(r.ReadBits(16)); + { + if (!r.TryReadBits(16, out uint raw)) return false; + value = math.f16tof32(raw); + return true; + } case BasisQuantMode.Ranged: { int b = ClampBits(bits); uint maxQ = (1u << b) - 1u; - uint q = r.ReadBits(b); - return min + (q / (float)maxQ) * (max - min); + if (!r.TryReadBits(b, out uint q)) return false; + value = min + (q / (float)maxQ) * (max - min); + return true; } default: - return BitsToFloat(r.ReadBits(32)); + { + if (!r.TryReadBits(32, out uint raw)) return false; + value = BitsToFloat(raw); + return true; + } } } private static void WriteVarInt(ref BitWriter w, int value) => WriteVarUInt(ref w, (uint)((value << 1) ^ (value >> 31))); - private static int ReadVarInt(ref BitReader r) + private static bool TryReadVarInt(ref BitReader r, out int value) { - uint zig = ReadVarUInt(ref r); - return (int)(zig >> 1) ^ -(int)(zig & 1); + value = 0; + if (!TryReadVarUInt(ref r, out uint zig)) return false; + value = (int)(zig >> 1) ^ -(int)(zig & 1); + return true; } private static void WriteVarUInt(ref BitWriter w, uint v) @@ -277,18 +363,20 @@ private static void WriteVarUInt(ref BitWriter w, uint v) w.WriteBits(v, 8); } - private static uint ReadVarUInt(ref BitReader r) + private static bool TryReadVarUInt(ref BitReader r, out uint value) { - uint val = 0; + value = 0; int shift = 0; - while (shift <= 35) + for (int count = 0; count < 5; count++) { - byte b = (byte)r.ReadBits(8); - val |= (uint)(b & 0x7F) << shift; - if ((b & 0x80) == 0) break; + if (!r.TryReadBits(8, out uint raw)) return false; + byte b = (byte)raw; + if (count == 4 && (b & 0xF0) != 0) return false; + value |= (uint)(b & 0x7F) << shift; + if ((b & 0x80) == 0) return true; shift += 7; } - return val; + return false; } private static unsafe uint FloatToBits(float v) => *(uint*)&v; @@ -326,7 +414,7 @@ public void WriteBits(uint value, int bits) public int ByteLength => _byte + (_bit > 0 ? 1 : 0); } - /// LSB-first bit reader; returns 0 once the byte limit is exhausted (corrupt/truncated safe). + /// LSB-first bit reader with explicit bounds checks for corrupt/truncated packets. private struct BitReader { private readonly byte[] _buf; @@ -342,15 +430,42 @@ public BitReader(byte[] buf, int startByte, int limitBytes) _limit = limitBytes < buf.Length ? limitBytes : buf.Length; } - public uint ReadBits(int bits) + public bool TryReadBits(int bits, out uint value) { - uint v = 0; + value = 0; + if (!CanReadBits(bits)) return false; + for (int i = 0; i < bits; i++) { - if (_byte < _limit && ((_buf[_byte] >> _bit) & 1) != 0) v |= 1u << i; - if (++_bit == 8) { _bit = 0; _byte++; } + if (((_buf[_byte] >> _bit) & 1) != 0) value |= 1u << i; + AdvanceBit(); + } + return true; + } + + public bool SkipBits(int bits) + { + if (!CanReadBits(bits)) return false; + int total = _bit + bits; + _byte += total >> 3; + _bit = total & 7; + return true; + } + + private bool CanReadBits(int bits) + { + if (bits < 0 || bits > 32) return false; + int available = ((_limit - _byte) << 3) - _bit; + return available >= bits; + } + + private void AdvanceBit() + { + if (++_bit == 8) + { + _bit = 0; + _byte++; } - return v; } } } From c9fc01698eced7d94507b5573f84a62f15836d0c Mon Sep 17 00:00:00 2001 From: Toys0125 Date: Thu, 25 Jun 2026 19:41:26 -0500 Subject: [PATCH 04/11] Add atomic lock to prevent multiple thread data reads and writes to Basis Batabase.. --- .../BasisNetworking/BasisDatabase.cs | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/Basis/Packages/com.basis.server/BasisNetworkServer/BasisNetworking/BasisDatabase.cs b/Basis/Packages/com.basis.server/BasisNetworkServer/BasisNetworking/BasisDatabase.cs index 0184584442..c99d2acb07 100644 --- a/Basis/Packages/com.basis.server/BasisNetworkServer/BasisNetworking/BasisDatabase.cs +++ b/Basis/Packages/com.basis.server/BasisNetworkServer/BasisNetworking/BasisDatabase.cs @@ -29,6 +29,7 @@ public BasisData(string name, ConcurrentDictionary jsonPayload) public static class BasisPersistentDatabase { private static readonly ConcurrentDictionary _dataByName = new(); + private static readonly object _dataLock = new(); private static readonly object _fileLock = new(); private static string _filePath = "basis_data.json"; @@ -72,18 +73,22 @@ public static bool AddOrUpdate(BasisData item) BNL.LogError("Payload exceeds maximum entry count. (basis database)"); return false; } - if (!_dataByName.ContainsKey(item.Name) && _dataByName.Count >= BasisNetworkServer.Security.BasisResourceLimitManager.MaxDatabaseEntries) + lock (_dataLock) { - BNL.LogError("Database entry limit reached; rejecting new entry. (basis database)"); - return false; - } - _dataByName.AddOrUpdate(item.Name, - addValueFactory: _ => item, - updateValueFactory: (_, existing) => + if (_dataByName.TryGetValue(item.Name, out BasisData existing)) { existing.JsonPayload = new ConcurrentDictionary(item.JsonPayload); - return existing; - }); + } + else + { + if (_dataByName.Count >= BasisNetworkServer.Security.BasisResourceLimitManager.MaxDatabaseEntries) + { + BNL.LogError("Database entry limit reached; rejecting new entry. (basis database)"); + return false; + } + _dataByName[item.Name] = item; + } + } MarkDirty(); return true; From e88ba0dec9df520121a188c2ae13b2b6f69923e9 Mon Sep 17 00:00:00 2001 From: Toys0125 Date: Thu, 25 Jun 2026 19:56:15 -0500 Subject: [PATCH 05/11] Add back original GUID and local int into BasisSyncCodec. --- .../Networking/Object Sync/BasisObjectSyncNetworking.cs.meta | 2 +- .../Networking/Object Sync/BasisPickupSyncNetworking.cs.meta | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Basis/Packages/com.basis.framework/Networking/Object Sync/BasisObjectSyncNetworking.cs.meta b/Basis/Packages/com.basis.framework/Networking/Object Sync/BasisObjectSyncNetworking.cs.meta index 54a80b5182..6ac0283725 100644 --- a/Basis/Packages/com.basis.framework/Networking/Object Sync/BasisObjectSyncNetworking.cs.meta +++ b/Basis/Packages/com.basis.framework/Networking/Object Sync/BasisObjectSyncNetworking.cs.meta @@ -1,2 +1,2 @@ fileFormatVersion: 2 -guid: 52a9ea38cfa7aca4ebec1aec993bbb64 \ No newline at end of file +guid: d2aa9116619ed954aa8e19d984b58ada \ No newline at end of file diff --git a/Basis/Packages/com.basis.framework/Networking/Object Sync/BasisPickupSyncNetworking.cs.meta b/Basis/Packages/com.basis.framework/Networking/Object Sync/BasisPickupSyncNetworking.cs.meta index 6ac0283725..54a80b5182 100644 --- a/Basis/Packages/com.basis.framework/Networking/Object Sync/BasisPickupSyncNetworking.cs.meta +++ b/Basis/Packages/com.basis.framework/Networking/Object Sync/BasisPickupSyncNetworking.cs.meta @@ -1,2 +1,2 @@ fileFormatVersion: 2 -guid: d2aa9116619ed954aa8e19d984b58ada \ No newline at end of file +guid: 52a9ea38cfa7aca4ebec1aec993bbb64 \ No newline at end of file From da0d0c5fb71bc65bddbf70616dc0428c3485c6ea Mon Sep 17 00:00:00 2001 From: Toys0125 Date: Thu, 25 Jun 2026 20:07:33 -0500 Subject: [PATCH 06/11] Check if null before applying int JsonPayload. --- .../BasisNetworkServer/BasisNetworking/BasisDatabase.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Basis/Packages/com.basis.server/BasisNetworkServer/BasisNetworking/BasisDatabase.cs b/Basis/Packages/com.basis.server/BasisNetworkServer/BasisNetworking/BasisDatabase.cs index c99d2acb07..30564f6f26 100644 --- a/Basis/Packages/com.basis.server/BasisNetworkServer/BasisNetworking/BasisDatabase.cs +++ b/Basis/Packages/com.basis.server/BasisNetworkServer/BasisNetworking/BasisDatabase.cs @@ -77,7 +77,9 @@ public static bool AddOrUpdate(BasisData item) { if (_dataByName.TryGetValue(item.Name, out BasisData existing)) { - existing.JsonPayload = new ConcurrentDictionary(item.JsonPayload); + existing.JsonPayload = item.JsonPayload != null + ? new ConcurrentDictionary(item.JsonPayload) + : new ConcurrentDictionary(); } else { From 1e32a6414090969427904937ea929ad8854182e5 Mon Sep 17 00:00:00 2001 From: Toys0125 Date: Fri, 26 Jun 2026 14:11:49 -0500 Subject: [PATCH 07/11] Remove function not needed. --- .../Networking/Sync/BasisSyncCodec.cs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/Basis/Packages/com.basis.framework/Networking/Sync/BasisSyncCodec.cs b/Basis/Packages/com.basis.framework/Networking/Sync/BasisSyncCodec.cs index 86dca77dff..ca1302c7cf 100644 --- a/Basis/Packages/com.basis.framework/Networking/Sync/BasisSyncCodec.cs +++ b/Basis/Packages/com.basis.framework/Networking/Sync/BasisSyncCodec.cs @@ -438,7 +438,12 @@ public bool TryReadBits(int bits, out uint value) for (int i = 0; i < bits; i++) { if (((_buf[_byte] >> _bit) & 1) != 0) value |= 1u << i; - AdvanceBit(); + //Advance Bit + if (++_bit == 8) + { + _bit = 0; + _byte++; + } } return true; } @@ -458,15 +463,6 @@ private bool CanReadBits(int bits) int available = ((_limit - _byte) << 3) - _bit; return available >= bits; } - - private void AdvanceBit() - { - if (++_bit == 8) - { - _bit = 0; - _byte++; - } - } } } } From a9fd918d5d02a4fb981541cac0b83bb9c803c3b3 Mon Sep 17 00:00:00 2001 From: Toys0125 Date: Fri, 26 Jun 2026 14:33:38 -0500 Subject: [PATCH 08/11] Inline if check. --- .../Device Management/EyeTracking/BasisEyeTrackingManager.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Basis/Packages/com.basis.framework/Device Management/EyeTracking/BasisEyeTrackingManager.cs b/Basis/Packages/com.basis.framework/Device Management/EyeTracking/BasisEyeTrackingManager.cs index b91de57acf..f14101019b 100644 --- a/Basis/Packages/com.basis.framework/Device Management/EyeTracking/BasisEyeTrackingManager.cs +++ b/Basis/Packages/com.basis.framework/Device Management/EyeTracking/BasisEyeTrackingManager.cs @@ -105,10 +105,7 @@ public static void Simulate() } bool hmdGaze = hmdHas && (hmdData.HasWorldRay || hmdData.HasPerEyeAngles); - if (hmdGaze) - { - _lastHmdActiveTime = Time.unscaledTime; - } + if (hmdGaze) _lastHmdActiveTime = Time.unscaledTime; bool oscGaze = oscHas && (oscData.HasWorldRay || oscData.HasPerEyeAngles); BasisEyeTrackingData merged = default; From 84f3b9c9335944d8c8f38aefa99e3854709ef845 Mon Sep 17 00:00:00 2001 From: Toys0125 Date: Fri, 26 Jun 2026 15:04:59 -0500 Subject: [PATCH 09/11] Using TryGetValue is only one call instead of two. --- .../BasisNetworking/BasisDatabase.cs | 64 ++++++++++++------- .../BasisNetworking/BasisDatabase.cs | 45 ++++++++----- 2 files changed, 69 insertions(+), 40 deletions(-) diff --git a/Basis Server/BasisNetworkServer/BasisNetworking/BasisDatabase.cs b/Basis Server/BasisNetworkServer/BasisNetworking/BasisDatabase.cs index 0184584442..82046e277e 100644 --- a/Basis Server/BasisNetworkServer/BasisNetworking/BasisDatabase.cs +++ b/Basis Server/BasisNetworkServer/BasisNetworking/BasisDatabase.cs @@ -29,6 +29,7 @@ public BasisData(string name, ConcurrentDictionary jsonPayload) public static class BasisPersistentDatabase { private static readonly ConcurrentDictionary _dataByName = new(); + private static readonly object _dataLock = new(); private static readonly object _fileLock = new(); private static string _filePath = "basis_data.json"; @@ -72,18 +73,24 @@ public static bool AddOrUpdate(BasisData item) BNL.LogError("Payload exceeds maximum entry count. (basis database)"); return false; } - if (!_dataByName.ContainsKey(item.Name) && _dataByName.Count >= BasisNetworkServer.Security.BasisResourceLimitManager.MaxDatabaseEntries) + lock (_dataLock) { - BNL.LogError("Database entry limit reached; rejecting new entry. (basis database)"); - return false; - } - _dataByName.AddOrUpdate(item.Name, - addValueFactory: _ => item, - updateValueFactory: (_, existing) => + if (_dataByName.TryGetValue(item.Name, out BasisData existing)) { - existing.JsonPayload = new ConcurrentDictionary(item.JsonPayload); - return existing; - }); + existing.JsonPayload = item.JsonPayload != null + ? new ConcurrentDictionary(item.JsonPayload) + : new ConcurrentDictionary(); + } + else + { + if (_dataByName.Count >= BasisNetworkServer.Security.BasisResourceLimitManager.MaxDatabaseEntries) + { + BNL.LogError("Database entry limit reached; rejecting new entry. (basis database)"); + return false; + } + _dataByName[item.Name] = item; + } + } MarkDirty(); return true; @@ -103,7 +110,11 @@ public static bool GetByName(string name, out BasisData BasisData) public static bool Remove(string name) { if (string.IsNullOrWhiteSpace(name)) return false; - var result = _dataByName.TryRemove(name, out _); + bool result; + lock (_dataLock) + { + result = _dataByName.TryRemove(name, out _); + } if (result) MarkDirty(); return result; } @@ -120,16 +131,19 @@ public static void Save() { try { - var list = GetAll().ToList(); - - using var stream = new FileStream(_filePath, FileMode.Create, FileAccess.Write, FileShare.None); - var serializer = new DataContractJsonSerializer(typeof(List), new DataContractJsonSerializerSettings + lock (_dataLock) { - UseSimpleDictionaryFormat = true - }); + var list = _dataByName.Values.ToList(); + + using var stream = new FileStream(_filePath, FileMode.Create, FileAccess.Write, FileShare.None); + var serializer = new DataContractJsonSerializer(typeof(List), new DataContractJsonSerializerSettings + { + UseSimpleDictionaryFormat = true + }); - serializer.WriteObject(stream, list); - _isDirty = false; + serializer.WriteObject(stream, list); + _isDirty = false; + } } catch (IOException ex) { @@ -155,11 +169,15 @@ public static void Load() if (serializer.ReadObject(stream) is List loadedData) { - _dataByName.Clear(); - foreach (var item in loadedData) + lock (_dataLock) { - if (!string.IsNullOrWhiteSpace(item.Name)) - _dataByName[item.Name] = item; + _dataByName.Clear(); + foreach (var item in loadedData) + { + if (!string.IsNullOrWhiteSpace(item.Name)) + _dataByName[item.Name] = item; + } + _isDirty = false; } } } diff --git a/Basis/Packages/com.basis.server/BasisNetworkServer/BasisNetworking/BasisDatabase.cs b/Basis/Packages/com.basis.server/BasisNetworkServer/BasisNetworking/BasisDatabase.cs index 30564f6f26..27553d924d 100644 --- a/Basis/Packages/com.basis.server/BasisNetworkServer/BasisNetworking/BasisDatabase.cs +++ b/Basis/Packages/com.basis.server/BasisNetworkServer/BasisNetworking/BasisDatabase.cs @@ -84,10 +84,10 @@ public static bool AddOrUpdate(BasisData item) else { if (_dataByName.Count >= BasisNetworkServer.Security.BasisResourceLimitManager.MaxDatabaseEntries) - { - BNL.LogError("Database entry limit reached; rejecting new entry. (basis database)"); - return false; - } + { + BNL.LogError("Database entry limit reached; rejecting new entry. (basis database)"); + return false; + } _dataByName[item.Name] = item; } } @@ -110,7 +110,11 @@ public static bool GetByName(string name, out BasisData BasisData) public static bool Remove(string name) { if (string.IsNullOrWhiteSpace(name)) return false; - var result = _dataByName.TryRemove(name, out _); + bool result; + lock (_dataLock) + { + result = _dataByName.TryRemove(name, out _); + } if (result) MarkDirty(); return result; } @@ -127,16 +131,19 @@ public static void Save() { try { - var list = GetAll().ToList(); - - using var stream = new FileStream(_filePath, FileMode.Create, FileAccess.Write, FileShare.None); - var serializer = new DataContractJsonSerializer(typeof(List), new DataContractJsonSerializerSettings + lock (_dataLock) { - UseSimpleDictionaryFormat = true - }); + var list = _dataByName.Values.ToList(); - serializer.WriteObject(stream, list); - _isDirty = false; + using var stream = new FileStream(_filePath, FileMode.Create, FileAccess.Write, FileShare.None); + var serializer = new DataContractJsonSerializer(typeof(List), new DataContractJsonSerializerSettings + { + UseSimpleDictionaryFormat = true + }); + + serializer.WriteObject(stream, list); + _isDirty = false; + } } catch (IOException ex) { @@ -162,11 +169,15 @@ public static void Load() if (serializer.ReadObject(stream) is List loadedData) { - _dataByName.Clear(); - foreach (var item in loadedData) + lock (_dataLock) { - if (!string.IsNullOrWhiteSpace(item.Name)) - _dataByName[item.Name] = item; + _dataByName.Clear(); + foreach (var item in loadedData) + { + if (!string.IsNullOrWhiteSpace(item.Name)) + _dataByName[item.Name] = item; + } + _isDirty = false; } } } From 45eddd8b45655aa5aab730cdfcc1f188d8c4a9dd Mon Sep 17 00:00:00 2001 From: Toys0125 Date: Fri, 26 Jun 2026 15:11:44 -0500 Subject: [PATCH 10/11] Add comment about double verify. --- .../com.basis.framework/Networking/Sync/BasisSyncCodec.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Basis/Packages/com.basis.framework/Networking/Sync/BasisSyncCodec.cs b/Basis/Packages/com.basis.framework/Networking/Sync/BasisSyncCodec.cs index ca1302c7cf..590a312384 100644 --- a/Basis/Packages/com.basis.framework/Networking/Sync/BasisSyncCodec.cs +++ b/Basis/Packages/com.basis.framework/Networking/Sync/BasisSyncCodec.cs @@ -138,6 +138,7 @@ public static bool Deserialize(BasisSyncSchema schema, byte[] buf, int length, B if (keyframe) { + // Preflight the packet so rejected/truncated data never partially mutates the baseline. var verify = new BitReader(buf, o, length); for (int fi = 0; fi < fieldCount; fi++) { @@ -156,6 +157,7 @@ public static bool Deserialize(BasisSyncSchema schema, byte[] buf, int length, B o += schema.DirtyMaskBytes; if (o > length) return false; + // Preflight the packet so rejected/truncated data never partially mutates the baseline. var verify = new BitReader(buf, o, length); for (int fi = 0; fi < fieldCount; fi++) { From f60ff3d16103eb85e70560dfcad079b0d52d301f Mon Sep 17 00:00:00 2001 From: Toys0125 Date: Fri, 26 Jun 2026 20:01:30 -0500 Subject: [PATCH 11/11] Use Threading.Lock when available. --- .../BasisNetworkServer/BasisNetworking/BasisDatabase.cs | 5 +++++ .../BasisNetworkServer/BasisNetworking/BasisDatabase.cs | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/Basis Server/BasisNetworkServer/BasisNetworking/BasisDatabase.cs b/Basis Server/BasisNetworkServer/BasisNetworking/BasisDatabase.cs index 82046e277e..d194ddacc7 100644 --- a/Basis Server/BasisNetworkServer/BasisNetworking/BasisDatabase.cs +++ b/Basis Server/BasisNetworkServer/BasisNetworking/BasisDatabase.cs @@ -29,8 +29,13 @@ public BasisData(string name, ConcurrentDictionary jsonPayload) public static class BasisPersistentDatabase { private static readonly ConcurrentDictionary _dataByName = new(); +#if NET9_0_OR_GREATER + private static readonly Lock _dataLock = new(); + private static readonly Lock _fileLock = new(); +#else private static readonly object _dataLock = new(); private static readonly object _fileLock = new(); +#endif private static string _filePath = "basis_data.json"; private static volatile bool _isDirty = false; diff --git a/Basis/Packages/com.basis.server/BasisNetworkServer/BasisNetworking/BasisDatabase.cs b/Basis/Packages/com.basis.server/BasisNetworkServer/BasisNetworking/BasisDatabase.cs index 27553d924d..309e91de6a 100644 --- a/Basis/Packages/com.basis.server/BasisNetworkServer/BasisNetworking/BasisDatabase.cs +++ b/Basis/Packages/com.basis.server/BasisNetworkServer/BasisNetworking/BasisDatabase.cs @@ -29,8 +29,13 @@ public BasisData(string name, ConcurrentDictionary jsonPayload) public static class BasisPersistentDatabase { private static readonly ConcurrentDictionary _dataByName = new(); +#if NET9_0_OR_GREATER + private static readonly Lock _dataLock = new(); + private static readonly Lock _fileLock = new(); +#else private static readonly object _dataLock = new(); private static readonly object _fileLock = new(); +#endif private static string _filePath = "basis_data.json"; private static volatile bool _isDirty = false;