Skip to content

fix: stop the Search view answering for a half-typed query - #6

Merged
m4sh-wacker merged 1 commit into
mainfrom
fix/search-stale-results
Oct 22, 2026
Merged

m4sh-wacker merged 1 commit into
mainfrom
fix/search-stale-results

Conversation

@m4sh-wacker

Copy link
Copy Markdown
Collaborator

What was wrong

Open the Search view, type flag{ one key at a time, and sometimes the report said 2 flags found: the real one, plus a long run of the hex input labelled "Flag-shaped, with a prefix this competition or a well-known one uses", and a "Read it backwards" flag hint. Setting the query in one go always gave the right answer. Caught while recording a demo.

Two things went wrong together.

1. A second pass read the search box mid-typing (src/hooks/useAnalysis.ts).
Opening the Search view starts the deeper pass right away (setActivity). The debounce effect in useAnalysis also listed view as a dependency, so 220 ms later it started a second pass, reading the search box at whatever point the typing had reached. Typing does not start a pass on purpose, so that second pass had the last word. If the box held f at that moment, the report answered for f.

The token in runCtf already stops an older pass overwriting a newer one, so the responses were not arriving out of order. The trouble was a duplicate pass that sampled a half-typed query.

Reproduced on the live site by scripting the page: when the first key lands 160 ms after the view opens, the result is "2 flags found" every time, because the timer reads f. At 60 ms or 400 ms it is "1 flag found", because the timer reads fl or the empty box.

2. Any text counted as a declared flag format (src/engine/ctf/flags.ts).
findFlags took any non-empty format as the competition's prefix, and its fallback for formats without braces searched for that text anywhere, mid-word included (f[^\s]{2,256}). f is a hex digit, so it matched the middle of the hex input at depth 0. The reversal check calls the same function, which is where the extra "Read it backwards" hint came from. Only f breaks, because fl, fla and flag never occur in hex.

The fix

  • Switching views no longer schedules a pass. The debounce now lives in watchInput(), a plain store subscription the hook starts, so it can be tested without a DOM. Only input and auto-run changes schedule a re-run. The view is read when the input changes, not when the timer fires, so opening the view inside the 220 ms window does not bring the second pass back.
  • Opening the Search view checks which input the running pass is over. It used to skip whenever any pass was running. Now it skips only if the running pass is over the same input and encoding. The removed delayed pass was the only thing covering "reopen Search while a pass over an older input is still running", so this keeps that case working.
  • runCtf drops a result whose input was replaced while it ran. Before, a pass could finish after the input was cleared or changed and still land.
  • A declared format must look like a flag prefix: a letter followed by 1–23 of [A-Za-z0-9_-], with an optional opening brace. That is the same prefix grammar the flag shape already uses. The brace is ignored when comparing, so typing myctf{ now promotes myctf{…} the way myctf already did. The fallback search is anchored to the start of a word, like the shape is.

Tests

  • src/engine/ctf.test.ts: three findFlags cases (brace-typed format, lone letter, match in the middle of a word). An end-to-end hints() run over the demo's input with '', f, fl, flag and flag{ in the box must find exactly the one flag every time.
  • src/hooks/useAnalysis.test.ts (new): the real store and engine, with setTimeout faked. It replays the demo's timing: open the view, first key at 160 ms, then one every 90 ms. It failed before the fix with passes run for ['', 'f']. Two more tests guard the cases above: an in-flight pass over an older input, and an input replaced while a pass runs. The second was confirmed red with the check disabled.

npm run lint, npm run typecheck, npm test and npm run build pass. Checked the built app in a browser with the same scripted timing (key at 160 ms after the view opens): it now shows 1 flag found, where the live site shows 2.

Opening the Search view started the deeper pass, and the debounce in
useAnalysis started a second one 220 ms later because it re-ran on view
changes. That second pass read the search box mid-typing, so typing
"flag{" a key at a time could leave the report answering for "f". And
findFlags took "f" as a declared flag format and found it in the middle
of the hex input.

- Only input and auto-run changes schedule a re-run (watchInput, now a
  plain store subscription so the timing is testable).
- Opening the view starts a pass unless the running one is over the same
  input; runCtf drops a result whose input was replaced while it ran.
- A declared format must look like a flag prefix (optional brace), and
  the fallback search only matches where a word starts.
@m4sh-wacker
m4sh-wacker merged commit b04a919 into main Oct 22, 2026
6 checks passed
@m4sh-wacker
m4sh-wacker deleted the fix/search-stale-results branch October 22, 2026 10:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant