Two small defects in the same crate, both reachable from the iCaptcha origin, which the crate's own
header treats as untrusted. Filing together since they share a fix surface and a test file.
1. interactive_prompt blocks forever on open-but-silent stdin
The docstring at lib.rs:353-355 states the contract: "Returns None when stdin isn't a usable
interactive source (e.g. an agent), so the caller surfaces a clear 'couldn't auto-solve' error
instead." The implementation does not honor it. lib.rs:366-368 is a bare read with no TTY test and
no timeout:
match stdin().read_line(&mut line) { Ok(0) | Err(_) => None,
Ok(0) is EOF only. An open pipe with a live writer and no data returns neither, so it hangs. There is
no TTY facility anywhere in the workspace: grep -rn "is-terminal\|atty\|IsTerminal" over crates/
returns nothing.
Measured, three stdin shapes against the read step compiled verbatim:
stdin = /dev/null (EOF) -> returned None, exit 0
stdin = open pipe, no data -> exit 124 (timed out, blocked)
stdin = pipe with data -> returned Some("42"), exit 0
/dev/null, the typical CI shape, is fine. The bad case is an agent, wrapper script, or supervisor
handing the child an inherited pipe it keeps open. Under gl this also pins a tokio blocking thread
(crates/gl/src/http.rs:200-202).
Reachable because nothing enforces that the service's reply is one of SOLVABLE_TYPES (lib.rs:179):
an unexpected type, or an unparseable prompt inside a known one, makes solvers::solve return None
and drops straight to this branch (lib.rs:265-267), with solver: None on the production path.
Fix: check for a TTY before prompting, and return None when there is not one, which is what the
docstring already promises.
2. i64 overflow in the deterministic solvers
Plain i64 arithmetic with no checked_/saturating_/wrapping_ anywhere in solvers.rs:
:32-34 (acc += n, acc -= n), :131-136 (n[1] - n[0], last + d), :180-183 (isqrt_exact,
where cand * cand overflows for v near i64::MAX), plus :48, :53, :83, :104, :107, and
:159 (v.abs() at i64::MIN).
The workspace release profile (Cargo.toml:59-61) sets lto and strip but not overflow-checks, so
release wraps and debug panics. Both halves confirmed by running a standalone copy both ways:
DEBUG PANIC arithmetic "What is 9223372036854775807 + 1?"
DEBUG PANIC sequence "... 1, 4, 9223372036854775807, ?"
RELEASE OK arithmetic -> Some("-9223372036854775808")
RELEASE OK algebra -> Some("0")
Every panicking input is well-formed for the parser: each literal is in range and parses fine, and the
overflow happens during evaluation, so no malformed-input guard stands in the way. Source is
challenge.prompt (lib.rs:234), reaching solvers::solve at lib.rs:265.
Consequences are mild. In release a wrapped answer just fails grading; in debug the panic happens
inside spawn_blocking and gl reports "iCaptcha solver task panicked". Worth fixing because it is
cheap and the function's contract is already to return None when a prompt cannot be handled
(solvers.rs:10-11), so overflow should join that path via checked_*.
Not filed: the PoW iteration cap
For the record, since it came up in the same pass and I checked it: pow::solve burning its full
iteration cap on an unsolvable difficulty is not worth an issue. The bound already exists
(pow.rs:31, MAX_ITERS = 1 << 26) with a comment explaining the choice, and an early exit at an
"impossible" threshold buys nothing, since a difficulty of 28 is equally unsolvable in practice and
sits below any such threshold. Measured worst case is 121s of one core in a release build, which is
bounded, client-side, and self-inflicted. The module header's "well under a second" framing is
optimistic and could use a correcting comment; that is the whole of it.
Two small defects in the same crate, both reachable from the iCaptcha origin, which the crate's own
header treats as untrusted. Filing together since they share a fix surface and a test file.
1. interactive_prompt blocks forever on open-but-silent stdin
The docstring at
lib.rs:353-355states the contract: "Returns None when stdin isn't a usableinteractive source (e.g. an agent), so the caller surfaces a clear 'couldn't auto-solve' error
instead." The implementation does not honor it.
lib.rs:366-368is a bare read with no TTY test andno timeout:
Ok(0)is EOF only. An open pipe with a live writer and no data returns neither, so it hangs. There isno TTY facility anywhere in the workspace:
grep -rn "is-terminal\|atty\|IsTerminal"overcrates/returns nothing.
Measured, three stdin shapes against the read step compiled verbatim:
/dev/null, the typical CI shape, is fine. The bad case is an agent, wrapper script, or supervisorhanding the child an inherited pipe it keeps open. Under
glthis also pins a tokio blocking thread(
crates/gl/src/http.rs:200-202).Reachable because nothing enforces that the service's reply is one of
SOLVABLE_TYPES(lib.rs:179):an unexpected
type, or an unparseable prompt inside a known one, makessolvers::solvereturnNoneand drops straight to this branch (
lib.rs:265-267), withsolver: Noneon the production path.Fix: check for a TTY before prompting, and return
Nonewhen there is not one, which is what thedocstring already promises.
2. i64 overflow in the deterministic solvers
Plain
i64arithmetic with nochecked_/saturating_/wrapping_anywhere insolvers.rs::32-34(acc += n,acc -= n),:131-136(n[1] - n[0],last + d),:180-183(isqrt_exact,where
cand * candoverflows forvneari64::MAX), plus:48,:53,:83,:104,:107, and:159(v.abs()ati64::MIN).The workspace release profile (
Cargo.toml:59-61) setsltoandstripbut notoverflow-checks, sorelease wraps and debug panics. Both halves confirmed by running a standalone copy both ways:
Every panicking input is well-formed for the parser: each literal is in range and parses fine, and the
overflow happens during evaluation, so no malformed-input guard stands in the way. Source is
challenge.prompt(lib.rs:234), reachingsolvers::solveatlib.rs:265.Consequences are mild. In release a wrapped answer just fails grading; in debug the panic happens
inside
spawn_blockingandglreports "iCaptcha solver task panicked". Worth fixing because it ischeap and the function's contract is already to return
Nonewhen a prompt cannot be handled(
solvers.rs:10-11), so overflow should join that path viachecked_*.Not filed: the PoW iteration cap
For the record, since it came up in the same pass and I checked it:
pow::solveburning its fulliteration cap on an unsolvable difficulty is not worth an issue. The bound already exists
(
pow.rs:31,MAX_ITERS = 1 << 26) with a comment explaining the choice, and an early exit at an"impossible" threshold buys nothing, since a difficulty of 28 is equally unsolvable in practice and
sits below any such threshold. Measured worst case is 121s of one core in a release build, which is
bounded, client-side, and self-inflicted. The module header's "well under a second" framing is
optimistic and could use a correcting comment; that is the whole of it.