v1.1.7: deep security & reliability audit — 33 bugs fixed
CI / test (push) Has been cancelled
CI / verify (push) Has been cancelled
Clients CI / build-server (push) Has been cancelled
Clients CI / test-python (push) Has been cancelled
Clients CI / test-javascript (push) Has been cancelled
Clients CI / test-nim (push) Has been cancelled
Clients CI / test-rust (push) Has been cancelled

Critical (5):
- Reject empty JWT secret when authEnabled (server.nim)
- Fix 2PC marking uncontacted participants as prepared/committed (disttxn.nim)
- Fix Raft commit index calculation for even-sized clusters (raft.nim)
- Fix REP/DISTTXN protocol auth bypass (server.nim)
- Fix HTTP backup/restore path traversal (httpserver.nim)

High (11):
- Fix WAL write race with flush (lsm.nim)
- Fix MVCC savepoint/rollback deep-copy writeSet (mvcc.nim)
- Fix table mutation during deadlock iteration (mvcc.nim)
- Fix LIMIT 0 returning all rows (executor.nim)
- Fix COUNT(col) counting NULL values — 3 locations (executor.nim)
- Fix EXISTS subquery lowering missing subqueryPlan (executor.nim)
- Fix Raft appendEntries/applyCommitted array vs logical index (raft.nim)
- Fix timing attacks on constantTimeCompare and SCRAM (auth.nim, scram.nim)
- Fix B-tree leaf merge phantom separator key (btree.nim)
- Fix SSL verifyPeer not applied to newContext (ssl.nim)
- Fix sharding connectWithTimeout missing SO_ERROR check (sharding.nim)
- Fix sync replication returning success on partial ack (replication.nim)
- Fix WebSocket JWT expiration not validated (websocket.nim)

Medium (13):
- Fix writeSSTable partial file → tmp + atomic rename (lsm.nim)
- Fix multi-CTE table loss (executor.nim)
- Fix nl_to_sql DML restricted to superuser (executor.nim)
- Fix unbounded plan cache — max 10000 (adaptive.nim)
- Fix migration lock crash persistence — timestamp + stale detection (executor.nim)
- Fix admin panel auth (httpserver.nim)
- Fix MVCC unbounded txn tracking — prune in compactVersions (mvcc.nim)
- Fix connection pool maxLifetime check (pool.nim)
- Fix JWT JSON parser backslash escapes (auth.nim)
- Fix substr(s, start) returning single char (udf.nim)
- Fix loadSSTable minimum file-size check (lsm.nim)
- Fix compaction mmap leak (compaction.nim)
- Fix JSON injection in hybrid_search_filtered (executor.nim)

Low (4):
- Raft loadState logs error instead of silent discard
- Replication healthCheck double-close fixed
- Lexer readIdent double column counting fixed
- WebSocket frame 32-bit overflow guard

All 448 tests passing, 0 failures. Bump version to 1.1.7.
This commit is contained in:
2026-05-29 14:17:41 +03:00
parent 37a8ed52ba
commit 42043f3946
27 changed files with 408 additions and 86 deletions
+23 -10
View File
@@ -444,10 +444,16 @@ proc migrationLockKey(): string = "_schema:migrations:_lock"
proc acquireMigrationLock(ctx: ExecutionContext): bool =
let lockKey = migrationLockKey()
let (locked, _) = ctx.db.get(lockKey)
let (locked, lockVal) = ctx.db.get(lockKey)
if locked:
return false
ctx.db.put(lockKey, cast[seq[byte]]("locked"))
# Check for stale lock (older than 1 hour)
let lockTime = try: parseInt(cast[string](lockVal)) except: 0
if lockTime > 0 and (epochTime().int64 - lockTime) > 3600:
# Stale lock — force release
ctx.db.delete(lockKey)
else:
return false
ctx.db.put(lockKey, cast[seq[byte]]($epochTime().int64))
return true
proc releaseMigrationLock(ctx: ExecutionContext) =
@@ -930,7 +936,7 @@ proc evalExpr*(expr: IRExpr, row: Row, ctx: ExecutionContext = nil): Value =
of vkNull:
return Value(kind: vkNull)
else:
# Heuristic type inference from string content for untyped fields
# Heuristic type coercion for arithmetic on string-stored fields
if s.len == 0: return Value(kind: vkString, strVal: s)
try:
return Value(kind: vkInt64, int64Val: parseInt(s))
@@ -1457,7 +1463,8 @@ proc evalExprOld*(expr: IRExpr, row: Table[string, string], ctx: ExecutionContex
let results = doHybridSearchFiltered(ctx, table, vecCol, textCol, queryText, queryVec, k, filterCol, filterVal)
var parts: seq[string] = @[]
for (id, score) in results:
parts.add("{\"id\":\"" & id & "\",\"score\":\"" & $score & "\"}")
let safeId = id.replace("\\", "\\\\").replace("\"", "\\\"")
parts.add("{\"id\":\"" & safeId & "\",\"score\":\"" & $score & "\"}")
return "[" & parts.join(",") & "]"
of "rerank":
if expr.irFuncArgs.len < 2: return "[]"
@@ -1550,7 +1557,8 @@ proc evalExprOld*(expr: IRExpr, row: Table[string, string], ctx: ExecutionContex
let isSafeQuery = sqlLower.startsWith("select") or sqlLower.startsWith("explain") or
sqlLower.startsWith("with")
let allowDml = ctx.sessionVars.getOrDefault("nl_to_sql.allow_dml", "false") == "true"
if not isSafeQuery and not allowDml:
let isSuperuser = ctx.sessionVars.getOrDefault("is_superuser", "false") == "true"
if not isSafeQuery and (not allowDml or not isSuperuser):
# For non-SELECT: only do syntax validation via tokenize+parse, no execution
let tokens = qlex.tokenize(sql)
let astNode = qpar.parse(tokens)
@@ -2642,6 +2650,7 @@ proc lowerExpr*(node: Node): IRExpr =
result.binRight = lowerExpr(node.inRight)
of nkExists:
result = IRExpr(kind: irekExists)
result.existsSubquery = lowerSelect(node.existsExpr)
of nkSubquery:
result = IRExpr(kind: irekSubquery)
result.subqueryPlan = lowerSelect(node.subQuery)
@@ -3134,7 +3143,7 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
var count = 0
for row in filteredRows:
let v = evalExpr(expr.aggArgs[0], row, ctx)
if valueToString(v).len > 0: count += 1
if v.kind != vkNull: count += 1
newRow[alias] = $count
of irSum:
var sum = 0.0
@@ -3239,8 +3248,10 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
let sourceRows = executePlan(ctx, plan.limitSource)
var start = int(plan.limitOffset)
if start > sourceRows.len: start = sourceRows.len
if plan.limitCount == 0:
return @[]
var endIdx = start + int(plan.limitCount)
if endIdx > sourceRows.len or plan.limitCount == 0:
if endIdx > sourceRows.len:
endIdx = sourceRows.len
return sourceRows[start..<endIdx]
@@ -3311,7 +3322,7 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
var count = 0
for row in filteredRows:
let v = evalExpr(aggExpr.aggArgs[0], row, ctx)
if valueToString(v).len > 0: count += 1
if v.kind != vkNull: count += 1
aggRow[aggKey] = $count
of irSum:
var sum = 0.0
@@ -3833,7 +3844,7 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
var count = 0
for row in matchingRows:
let v = evalExpr(plan.pivotAgg.aggArgs[0], row, ctx)
if valueToString(v).len > 0: count += 1
if v.kind != vkNull: count += 1
aggResult = $count
of irSum:
var sum = 0.0
@@ -4291,7 +4302,9 @@ proc executeQueryImpl(ctx: ExecutionContext, astNode: Node, params: seq[WireValu
else:
var inner = Node(kind: nkStatementList, stmts: @[])
inner.stmts.add(cteQuery)
let savedCte = ctx.cteTables
let cteRes = executeQueryImpl(ctx, inner)
ctx.cteTables = savedCte
var cteRows: seq[Row] = @[]
for row in cteRes.rows:
cteRows.add(row)