fix(vector): reject non-positive similar_to neighbor count - #9767
fix(vector): reject non-positive similar_to neighbor count#9767eharris128 wants to merge 2 commits into
Conversation
4ee5f87 to
b2c8c21
Compare
shiva-istari
left a comment
There was a problem hiding this comment.
Thanks @eharris128 for the fix. The worker/task.go guard looks right and the root-cause write-up is solid. Two asks on the test side:
-
Can you add an integration test? The new unit test covers addPathNode in isolation, but the actual user-facing contract a bad k returns a query error and the Alpha stays up only lives in the worker/task.go guard, and nothing exercises it end-to-end. Both k=0 and k=-1 parse fine through DQL, so this slots right into query/vector/vector_test.go next to TestSimilarToOptionsIntegration: fire similar_to(pred, 0, ...) and similar_to(pred, -1, ...), assert both return an error containing "number of neighbors", then run a valid similar_to afterwards and assert it succeeds. That last query is the important bit on the old code the first bad query kills the server, so it's what makes this a real crash-regression test.
-
Tighten the new regression test a bit. TestAddPathNodeNonPositiveMaxResults only asserts "no panic", but the search loop also depends on the state addPathNode leaves behind worth asserting slr.neighbors (and slr.path) look sane after the call too, so the test pins the behavior and not just the absence of a crash.
|
Thanks @shiva-istari — both good calls, addressed in 1. Integration test. Added 2. Tightened the unit test. No production code changed from the last review — both are test-only. |
|
(also my commit on the fork is still syncing it would appear) - in case you see my above comment before the commit renders here in the PR diff. |
80227ff to
3114ae3
Compare
The number-of-neighbors argument of a similar_to() vector query is parsed from the request with no lower-bound check (worker/task.go). A zero or negative value reaches the HNSW bottom-layer search on the default (no ef / distance_threshold) path, where addPathNode computes effectiveMaxLen = maxResults + filtered and then evaluates slr.neighbors[:effectiveMaxLen]. A negative value panics with a negative slice bound, and zero empties the slice and then dereferences slr.neighbors[0], so a single query with a neighbor count <= 0 panics the query goroutine and takes down the Alpha. Reject a non-positive neighbor count at the request boundary with a clear error, and harden the HNSW library as defense in depth: mirror the maxResults < 0 clamp that SearchWithOptions already applies into SearchWithPath, and clamp effectiveMaxLen plus guard the neighbors[0] dereference in addPathNode. Add a regression test for addPathNode with non-positive maxResults.
…hNode state Add an integration test (TestSimilarToNonPositiveNeighbors) that fires similar_to with k=0 and k=-1, asserts both return a 'number of neighbors' query error, and then runs a valid similar_to and asserts it succeeds. The trailing valid query is the real crash-regression check: on the pre-fix code the first non-positive query panics and crashes the Alpha. Tighten TestAddPathNodeNonPositiveMaxResults to also pin the post-call state (neighbors truncated to empty, path unchanged) instead of only asserting the absence of a panic.
3114ae3 to
f22f8e0
Compare
Description
A
similar_to()vector query takes a client-supplied number of neighbors. That argument is parsed inworker/task.gowithout a lower-bound check, so a zero or negative value flows into the HNSW search. On the default search path (noef/distance_thresholdoption),SearchWithPathpasses it through unclamped, andaddPathNode(tok/hnsw/search_layer.go) computeseffectiveMaxLen = maxResults + filteredand then evaluatesslr.neighbors[:effectiveMaxLen]. A negative count panics with a negative slice bound ([:-1]); a count of zero empties the slice and then dereferencesslr.neighbors[0]. Either way a single query with a neighbor count<= 0panics the query goroutine and crashes the Alpha. TheSearchWithOptionspath already guards against this with anif maxResults < 0 { maxResults = 0 }clamp; only the default path was left unprotected.This change:
worker/task.go) with a clear error, so an invalidsimilar_tocount returns a normal query error instead of crashing the server.SearchWithOptionsclamp intoSearchWithPathas defense in depth.effectiveMaxLenand guards theslr.neighbors[0]dereference inaddPathNode, so the bottom-layer search cannot panic on a non-positive limit regardless of caller.TestAddPathNodeNonPositiveMaxResults) that reproduces the panic on the old code and passes with the fix.Valid queries are unaffected: a normal positive neighbor count truncates exactly as before.
Checklist
fix:,feat:,chore:,ci:, etc.