You are not logged in.
SessionsLoadFromFile breaks all session lookups due to fSessionCounterMin mismatch after restart
When using SessionsSaveToFile/SessionsLoadFromFile (or Shutdown with a state file) for session persistence across restarts, all session lookups via LockedSessionFind fail after the server restarts.
Root cause:
- In TRestServer.Create, fSessionCounterMin := Random32(1 shl 20) + 10 (a new random value on every startup, up to ~1,048,585)
- SessionsLoadFromFile restores fSessionCounter from the file, but fSessionCounterMin stays at the new random value
- LockedSessionFind checks: if (aSessionID <= fSessionCounterMin) or (aSessionID > cardinal(fSessionCounter)) then exit;
- For any small deployment where saved fSessionCounter < new fSessionCounterMin, ALL sessions (loaded and newly created) are silently rejected
This makes SessionsSaveToFile/SessionsLoadFromFile effectively broken for any server with fewer than ~1 million total logins (which is most deployments).
Proposed fix in SessionsLoadFromFile, after restoring fSessionCounter:
fSessionCounter := PCardinal(R.P)^;
fSessionCounterMin := 0; // allow all loaded session IDs to pass bounds check
Setting fSessionCounterMin=0 is safe because session IDs 0 and 1 (CONST_AUTHENTICATION_NOT_USED/NOT_STARTED) are rejected by the caller before LockedSessionFind is ever reached.
Regression from mORMot1: the old TSQLRestServerDB.Shutdown(filename) + SessionsLoadFromFile pattern worked reliably in mORMot1 because fSessionCounterMin was not randomized.
Offline
Nice finding.
The regression tests were indeed not enough to identify it, because it did reuse the same fSessionCounterMin.
Should be fixed by https://github.com/synopse/mORMot2/commit/b323ab692
Offline
Arnaud, you're amazing as always.
I barely finished my coffee before we received the fix.
My sincere respect and gratitude!
Added...
Checked, fix works!
Last edited by vs (2026-03-22 14:24:39)
Offline
Hi Arnaud, back in this thread.
Another issue found on another production implementation.
SessionsSaveToFile/LoadFromFile truncates 64-bit User.IDValue to 32-bit
TAuthSession.SaveTo and CreateFrom use WriteVarUInt32/VarUInt32 for fUser.IDValue and fUser.GroupRights.IDValue:
// SaveTo (line 5066-5068):
W.WriteVarUInt32(fUser.IDValue);
W.WriteVarUInt32(fUser.GroupRights.IDValue);
// CreateFrom (line 5083, 5086):
fUser.IDValue := Read.VarUInt32;
fUser.GroupRights.IDValue := Read.VarUInt32;
IDValue is TID = Int64, but VarUInt32 handles only 32-bit values. When using TSynUniqueIdentifierGenerator (63-bit IDs), the upper
bits are silently lost.
Reproduction:
1. Use TSynUniqueIdentifierGenerator for TAuthUser/TAuthGroup tables
2. Login → session created with 63-bit User.ID (e.g. 3810583231573688321)
3. Stop server → SessionsSaveToFile truncates to low 32 bits (2164424705)
4. Start server → SessionsLoadFromFile restores truncated ID → user not found in DB → session is useless
Fix: replace WriteVarUInt32/VarUInt32 with WriteVarUInt64/VarUInt64 for both IDValue fields. This changes the .ses binary format
(old files would need to be discarded on upgrade).
Offline
In fact, such 64-bit IDs were never supported because they could not be encoded as pointers on 32-bit apps.
If you use published properties of type TOrm, the ID should fit into a PtrInt, which is only 32-bit on 32-bit apps.
So it was by design.
Offline
Thank you Arnaud for the explanation.
That makes perfect sense — GroupRights stored as pointer(IDValue) limits IDs to PtrInt size.
We were assigning TSynUniqueIdentifierGenerator to all tables including TAuthUser/TAuthGroup, which produced 63-bit IDs
incompatible with this design.
Our fix: skip auth tables in SetIDGenerator loop:
if vClass.InheritsFrom(TAuthUser) or vClass.InheritsFrom(TAuthGroup) then
continue;
Auth tables now use default auto-increment, domain tables keep TSynUniqueIdentifier. Problem solved.
Perhaps worth a note in the SetIDGenerator documentation that it should not be used with TAuthUser/TAuthGroup descendants?
Offline