Repository navigation
Screen concurrent C for race candidates - #33
Merged
Merged
Conversation
PARSE_PROBE_BUDGET was inserted between the first and second lines of EXTERNAL_COMMAND_BUDGET's doc comment, so the sentence about an external command that does real work described the parse probe instead, and the constant it belonged to was left with a dangling fragment. Both now carry their own text, and PARSE_PROBE_BUDGET uses the Duration alias the rest of the module does.
analysis.rs had reached 6,181 lines, past the 5,664 that motivated the checkgaps extraction, and its tool_router impl block pins the tool methods in one place, so the free functions above it are the only part that can move. verdicts.rs takes the band that reads a property or a goal and answers one question about what its recorded status means. That is distinct from status.rs next door, which normalizes how a row spells a verdict: this module reads the spelling status.rs produces. The summarizers that shape the check payload stayed behind, so the new module depends on nothing above it and no constant had to be published to feed it. analysis.rs is 5,660 lines. Nine items are pub(crate) because callers in analysis.rs and checkgaps.rs reach them; the rest stayed private.
The transport owns a socket and not a child, so it can only say "connection closed": it cannot tell an out-of-memory kill from a kernel fatal from a prover that took the process down with it. A mid-session death reached the log as an unexplained disconnect, and the one flake this suite has seen could only be called an infrastructure symptom. The respawn path has the session state, so it reports the pid, whether the process is still there, and the tail of both logs, the way the spawn path already does through startup_failure_tail. Liveness is read with kill(pid, 0) rather than waited for, because the child is owned elsewhere.
The pin was measured and written down on 2026-09-04 and applied in neither the workflow nor the runner, while the comment above each command argued for the default it had rejected, citing a measurement taken when the suite held 89 tests. It holds 157 now. Seventeen days of runs took the risk the pin exists to remove. Both commands pass --test-threads=4 and both comments say why. A guard reads the count out of each file and fails when they disagree or when either drops the flag, because the repository's answer to a fact that must agree in two places is a test, not a third document.
The lint fired the day a guard was separated from its test attribute and became an ordinary function: the suite went on passing, the test count went up, and the only signal was a warning nothing fails on. As a deny it covers every orphan shape, an attribute that moved, a helper whose last caller went, a test renamed out of a path module. What it cannot cover is a doc comment that attaches to the wrong item, because both items still exist, and that stays a review question.
analyze_concurrency reads the loaded project's sources as text and
reports concurrent events, lexical lock order, and pairs of conflicting
accesses it cannot order. Level 0: every claim is syntax, and there is
no status meaning "not a race", because the strongest thing a lexical
lockset supports is that two accesses name one lock, which is evidence
rather than protection.
The defects this went through were one cause, not twenty. The unit of C
is a statement and the unit of a line scanner is a line, so comments and
literals are blanked before anything reads the text, with columns kept
so the actions on a line still order by position; accesses are per token
occurrence rather than per line; braces are positioned actions in the
same ordered stream as locks, which is what makes "} else {" fall out
rather than be special-cased; and leaving a block intersects locksets
rather than restoring the enclosing one, so a lock released inside a
block stays released.
Thread attribution walks a call graph from each entry, so a global
touched in a helper belongs to the threads that reach it. Whether the
program is concurrent at all is read off pthread_create tokens rather
than resolved entries, because an argument list that wraps resolves to
nothing and calling that program single threaded is the same false
verdict in a new place.
The scan carries an absolute deadline it checks itself, in both the
emission and the pairing phase, because a blocking task cannot be
cancelled and the timeout around it bounds only the caller's wait. A
scan that stops early says so, and every count it reports is a floor.
Fifty-four tests pin this, each named for the wrong answer it refuses.
The screener re-derives from text what Frama-C has already computed, and its default mode is the one where the AST is parsed and resident. The research report measures what the plug-in serializes today, and names the five things it does not: CFG edges, a resolved thread entry, a resolved mutex, loop ancestry, and the access base, which is the key the pairing groups zones by. The architecture document specifies one whole-program request and a must-lockset computed in Rust as a greatest fixpoint, and it refuses to inherit the keep-list: every defect found in this module lives in code the move would otherwise carry across untouched.
All three reported a program with a race in it as clean, which is the answer this pass exists not to give, and all three came out of one review. An initializer may hold a call. is_declaration tested the whole statement for a parenthesis, to keep prototypes out of the global set, so "int limit = SEC(5);" declared nothing and every access to limit was invisible. The test now reads the declarator. A loop may carry its body beside its head. The in-loop mark was hung on the following line alone, so "for (...) pthread_create(...);" spawned a pool the pass called a thread that runs once, and four threads writing one global produced no candidate. The budget did not cover reading. It started at the emission pass, so a caller naming a fifo or a character device blocked in read_to_string forever, and a blocking task cannot be cancelled. Reading is now inside the deadline and refuses anything that is not a regular file, which is reported rather than waited on.
report_lost_process read the session's poisoned flag as well as the transport's. A reload whose sources moved underneath it sets the first with the connection intact, and so does a reload that returned an error, so an ordinary failure announced a lost Frama-C, reported a live pid, and attached log tails that explain nothing. Only the transport's own flag means the stream died, so only that is read.
max_events and max_candidates are clamped to 100,000 and 20,000, and default to 10,000 and 2,000, none of which the parameter documentation said. A caller reading the schema sees an unbounded integer, passes 200,000, and silently gets the cap. The playbook listed three reasons an enumeration can be incomplete and there are four: the scan's own time budget also stops it, mid-file or mid-pairing, and scan_complete reports that one on its own.
KEYWORDS holds 25 entries and the research report called it a 24-word list, twice. The getContractFrontier registration is at line 148, not 158, which is inside getContractContext. The architecture document promised five resolutions above a table of six, the sixth being the entry sid the fixpoint starts from. A document arguing that counts must be measured rather than asserted is the worst place to assert one.
The comment claimed the deny covers every orphan shape. It covers a private item nothing reaches. A pub item in this library is never reported, because the unit tests link the crate from outside and everything they reach is published, and a doc comment attached to the wrong item is invisible to it either way.
The guard matched one line at a time, so a command rewrapped over two, by a trailing backslash or inside a YAML block, put the flag on a line that no longer named the suite. The count fell to one and a gate that promises to refuse only a disagreement went red over layout. Continuations are joined first and the flag is looked for across the lines a command spans. Measured against a rewrapped workflow, which now reads 4, and against a drifted one, which still fails.
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.
Adds
analyze_concurrency, a Level-0 screen over the loaded project's C sources that reports concurrent events, lexical lock order, and pairs of conflicting accesses it cannot order. Everything it claims is syntax, so it reports candidates and never verdicts: there is deliberately no status meaning "not a race", because the strongest thing a lexical lockset supports is that two accesses name one lock, which is evidence rather than protection. The series also carries the supporting changes that came out of building it, each revertible on its own: the verdict predicates move out ofanalysis.rs(6,181 lines to 5,660), the stdio suite's thread count is pinned where it actually runs,dead_codeis denied rather than warned, and a Frama-C that dies mid-session now reports its pid, liveness and log tails instead of only "connection closed".Verified with
scripts/run-gates.shat the tip: 18 of 18 gates,RUNNER_EXIT=0, including 648 unit tests, 157 stdio tests at the pinned four threads, and the tutorial corpus at 28/28 under-wp-cache none, so those proofs are computed rather than replayed. Every commit builds undercargo check --all-targets, checked by checking each one out, so the series bisects. Three of the fixes were mutation-tested rather than trusted: reverting the trylock handling, the lockset intersection on block exit, and readingthreads_detectedoff spawn tokens each makes its own regression fail, and the file was restored byte-identical afterwards.Deliberately left out.
docs/design/concurrency-ast/specifies re-founding this tool on the Frama-C AST, which answers by construction what the scanner answers by heuristic, but that is a design only and none of it is implemented here. Two measured cleanups are also deferred to it: interning the strings anEventrepeats, and memoizing thread attribution per function. One maintenance hazard is recorded and not fixed: the "a transport went" predicate is spelled twice inserver.rs, once negated.Summary by cubic
Adds
analyze_concurrency, a Level-0 screen over the loaded project's C sources that reports concurrent events, lexical lock order, and pairs of conflicting accesses it cannot order. Everything it claims is syntax, so it reports candidates and never verdicts: there is deliberately no status meaning "not a race", because the strongest thing a lexical lockset supports is that two accesses name one lock, which is evidence rather than protection.Screening behavior
} else {falls out rather than being special-cased.pthread_createtokens rather than resolved entries, since a wrapped argument list resolves to nothing.Supporting changes
analysis.rsintoverdicts.rs; the stdio suite's thread count is pinned to four, with a guard that the workflow and runner agree;dead_codeis denied rather than warned.Verified at the tip: 18 of 18 gates pass, including 648 unit tests, 157 stdio tests, and the tutorial corpus at 28/28 under
-wp-cache none, so those proofs are computed rather than replayed.Written for commit c585f53. Summary will update on new commits.