fix: paginate bugs_quicksearch over the bugs a client may see - #18
Merged
Conversation
Bugzilla applied limit/offset and the guard filtered what came back, so a hidden bug left a hole exactly where it sat: the page returned short while the next offset still had results. That gap says "a bug exists here that you may not see" — and because quicksearch matches summary text, it is a probe rather than a hint. Append a word to the query, see whether the hole survives, and the hidden bug's title comes out one word at a time. Uniform denials (I2) and silent filtering (I3) are worth nothing while the window itself reports what was removed. Apply pagination after the guard instead. Guard::quicksearch_window scans the upstream result from row 0 in chunks, classifies each chunk, and keeps going until enough visible bugs have accumulated to fill the window the client asked for. A hidden bug now costs a row of scanning and nothing else: it cannot shorten a page or contradict a later offset. Scanning from row 0 every time is not laziness. A visible-space offset cannot be mapped onto an upstream offset without classifying everything before it, and how many bugs that is, is exactly what must not leak. The scan target is rounded up to whole chunks rather than stopping the instant the window fills. Stopping exactly would make the request count a function of both the client's limit and the hidden-bug density, and those can be separated: binary-search limit against the clock and each block's exact hidden count falls out. Quantised, every limit within a block buys the same work, so the clock reports at most "this block was not entirely visible". The common case is unaffected — an unfiltered chunk already fills the target in one request. Bounded so a hostile query cannot spin: 1000 addressable, 2000 upstream rows scanned, so at most ten sequential requests. Hitting either bound truncates the page, which is indistinguishable from reaching the end of the results — it hides more, never less. The slice start is clamped to the slice end, not merely to the length. The window stops at 1000 but the scan overshoots it whenever a chunk yields fewer visible bugs than it holds, which happens only when bugs WERE hidden — so an unclamped start panicked precisely when something had been hidden past the window, answering the question this function exists to refuse and taking the session with it. Rows are deduped on the id the server reported, since relevance ordering is not stable between calls, and a row without a readable id is dropped rather than guessed at (I4). Dedup handles a row that moves forward between chunks; one that moves backwards past an already scanned offset is missed entirely and appears on no page. That hides more rather than less, and is noted where it could otherwise read as a guarantee. Search failures now return a bare "Search failed"; the upstream text goes to the log, since it can name a bug and say whether it exists.
plusky
force-pushed
the
fix/search-pagination-window
branch
from
July 27, 2026 19:45
8ffc346 to
5a75017
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The oracle
Bugzilla applied
limit/offset, then the guard filtered what came back. Soa hidden bug left a hole exactly where it sat: the page returned short
while the next offset still had results.
That gap says "a bug exists here that you may not see". And because
quicksearch matches summary text, it is a probe, not a hint:
Iterate a dictionary and the hidden bug's title comes out one word at a time.
Uniform denials (I2) and silent filtering (I3) are worth nothing while the
window itself reports what was removed — which makes this the finding that
actually defeats the embargo rules the policy exists to enforce.
The fix
Pagination is applied after the guard.
Guard::quicksearch_windowscansthe upstream result from row 0 in 200-row chunks, classifies each chunk, and
continues until enough visible bugs have accumulated to fill the requested
window. A hidden bug now costs a row of scanning and nothing else — it cannot
shorten a page or contradict a later offset.
Scanning from row 0 every time is not laziness: a visible-space offset cannot
be mapped onto an upstream offset without classifying everything before it,
and how many bugs that is, is exactly what must not leak.
Bounds and what they cost
offset+limit)Truncation is indistinguishable from reaching the end of the results, so the
failure direction hides more rather than less. I chose these over the 10,000
rows / 50 requests the design proposed, because this deployment already showed
it drops connections under load — a search costing 50 upstream round trips is
not a good trade for pagination depth almost nobody uses.
Rows are deduped on the server-reported id (relevance ordering proved unstable
between calls against the live instance), and a row without a readable id is
dropped rather than guessed at (I4).
Residual, stated plainly
The number of upstream requests grows with how many bugs were hidden in
the scanned prefix, so a client with a stopwatch can infer that a query's
results contain hidden bugs somewhere. That is a long way from
reconstructing a title, and the count steps only every 200 rows. Removing it
entirely would mean a fixed-size scan on every search, charging every query
for the worst case.
Verification
147 tests (+7). I checked the tests actually detect the old behaviour by
reintroducing it: three fail immediately. Two others did not discriminate
— one placed its hidden bugs past the window where they could not matter — so
I sharpened them; that one now interleaves hidden bugs before and inside the
window and asserts the page is still full.
Pinned: pages stay full while visible bugs remain, consecutive pages tile the
visible sequence disjointly and gaplessly, a hidden bug never shortens a page,
exhaustion and scan-truncation look alike, a zero limit touches upstream not at
all, and the objects returned are the ones that were classified.
Search failures now return a bare
Search failed; the upstream text is loggedserver-side only, since it can name a bug and say whether it exists.