Repository navigation
Make the String and StringBuilder models sound - #75
Closed
nils-loose wants to merge 5 commits into
Closed
nils-loose wants to merge 5 commits into
nils-loose wants to merge 5 commits into
Conversation
- indexOf(int ch[, from]) searches for the char (or the surrogate pair of a supplementary code point), not for its decimal digits; no code point, no match. - lastIndexOf(int ch[, from]) has no match for a ch that is no code point, also when ch is symbolic; the char is taken concretely (pinned by the caller). - startsWith(prefix, toffset) is false for an offset out of range, and its concrete result uses the offset. - replace uses str.replace_all instead of ten first-occurrence replaces; an empty or symbolic target (where Java and str.replace_all differ) is precision loss. - equals is true only for a String argument (false for null, not modelled for others); contentEquals and contains take a StringBuilder too. - getChars writes to dst from dstBegin, not from dstBegin + srcBegin. - toCharArray of "" returns an empty array; subSequence is substring. - new String(), new String(char[]) and new String(StringBuilder) are modelled; other constructors return a PlaceHolder (a missing invocation) instead of silently keeping the empty string. - Long and Short values convert to a String with their sign. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ring calls StringBuilder: the chars are a StringValue that is replaced on every change, never changed in place (it may be a String the program still holds). substring no longer truncates the builder (and takes end - start chars), replace replaces the range instead of the first occurrence of its contents, indexOf(str, from) and lastIndexOf use the String models, setLength is exact for any content. New models: append/insert of an Object, CharSequence, null, double and float, setCharAt, getChars, appendCodePoint, reverse of concrete chars, compareTo, subSequence, ensureCapacity/trimToSize; capacity is taken concretely. A builder with symbolic chars is symbolic now, so a method that is not modelled causes a context loss. After a change that is not modelled the chars are unknown: every method then returns a PlaceHolder until the builder is recovered from the heap, where it takes the concrete chars. (The consistency check of that recovery compared a String with a StringBuilder and always failed; it compares the chars now.) A StringBuilder from uninstrumented code gets a StringBuilderValue with its concrete chars. Index checks: String.charAt/substring/subSequence/getChars and the index methods of StringBuilder throw an IndexOutOfBoundsException for an index out of range, a branch inside the library that the trace did not have, so the solver never tried the other side. StringCallBranches gives these checks, in Java's order, and the visitor records them as branches (marked as in a callback, so the static pre-analysis walk holds its place). A call that threw at a failed check is explained by them and no longer counts as a context loss. Pins: where a model works on the concrete value of a symbolic int (the length in toCharArray, the indices of getChars, setLength's length, the char of lastIndexOf, char[] arguments' sizes and ranges), a branch x == concrete makes the solver try the other values in other runs, so the model is exact. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
compareTo, trim and lastIndexOf introduce UFs (commonPrefixLength, trimStart, trimEnd, lastIndexOf) that a trace constraint defines exactly for any arguments, so a branch over them is as exact as one over the inputs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
L0: each model, applied to symbolic strings and ints, admits only Java's result once the variables are fixed to a sample (for compareTo only that it admits Java's); the index checks hold iff Java does not throw. L2: a forked agent run records the index check of charAt on both sides, and no context loss when it throws. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…apacity str.replace_all made Z3 give up (incomplete theory seq) or hang on queries the ten nested first-occurrence replaces solved, so replace keeps that encoding and records precision loss: it is exact only up to ten occurrences, and not when the replacement contains the target. StringBuilder.capacity() is the constructor's (16, the given one, or the length + 16) until a change that may grow the builder; then it is taken concretely, as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nils-loose
added a commit
that referenced
this pull request
Oct 19, 2026
…tates to every branch Combining #72, #82 and #84: - #84 pruned a subtree below a repeated loop-head state by returning from the recursive dfs. In #72's iterative walk that return ended the whole walk, so the open branches of every later subtree were never offered, and an exhausted search could report SAFE. It now skips that subtree only (regression test added). - The pruned-subtree count is reset for each walk, as #82 walks the persistent tree instead of a copy. - The choice branches (#69/#76) and library-call branches (#75/#90) take the pending loop-head states too, so a state always goes with the branch right after it, which its position assumes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Author
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.
Since afbd1db, constraints over String inputs no longer count as precision loss, so every String model is trusted for SAFE. Several models were not exact (review of #65, B3). This PR makes them exact, or records precision loss or context loss where an exact model is impractical.
What changed
String models (
StringValue)indexOf(int ch[, from])searches for the char, or for the surrogate pair of a supplementary code point. It no longer searches for the decimal digits ("97"). A ch that is no code point gives -1.lastIndexOf(int ch[, from]): a ch that is no code point gives -1, also when ch is symbolic. The char is pinned (see below), because the window encoding with a symbolic char was too hard for Z3.startsWith(prefix, toffset)is false for an offset out of range, and its concrete result now uses the offset.replacekeeps the ten nested first-occurrence replaces and records precision loss. That encoding is inexact beyond ten occurrences, for an empty target, and when the replacement contains the target.str.replace_allwas tried: Z3 gave up or hung on queries the nested form solves (securibench/Basic5went from VIOLATION to timeout).equalsis true only for a String argument: false for null, not modelled for others. The old model made"ab".equals(sb)true and"null".equals(null)true.getCharswrites fromdstBegin, not fromdstBegin + srcBegin.toCharArray("")returns an empty array.subSequence,contentEquals,new String(),new String(char[])andnew String(StringBuilder). Other constructors return a PlaceHolder, which records a missing invocation and a context loss when the argument is symbolic. Before, they silently kept "".containsalso accepts a StringBuilder argument.str.from_intgives "" for a negative number.StringBuilder model (
StringBuilderValue)substringtruncated the builder and tookendchars instead ofend - start.replace(start, end, str)replaced the first occurrence of the range's contents instead of the range.indexOf(str, from)ignoredfromin its concrete result.setLengthwas exact only for the concrete length of the content.append/insertof an Object, a CharSequence, null, a double or a float (concrete only);setCharAt,getChars,appendCodePoint,compareTo,subSequence;reverse(concrete content only);ensureCapacity/trimToSize;capacity()while it is the constructor's value.isSymbolicwas always false.Branches inside library calls (
StringCallBranches, visitor)String.charAt,substring,subSequenceandgetChars, and the index methods of StringBuilder (charAt,setCharAt,deleteCharAt,delete,replace,insert,substring,setLength,getChars,appendCodePoint,append(CharSequence, int, int)).x == concretemakes the model exact; the solver tries other values in other runs.toCharArray, the indices ofgetChars, the length insetLength, the char oflastIndexOf, and the sizes and ranges of char[] arguments.DTOBuilder
commonPrefixLength,trimStart,trimEnd,lastIndexOf) no longer count as precision loss. Each one is defined exactly by a trace constraint on its arguments.regionMatches,valueOf(Object)andindexOf(String, int), against the Java spec. The tests cover them.Measured
baseline-results.jsonl. 568 runs:replaceorcapacity(the last changes) were re-run on the final jar.Tests
StringModelExactnessTest(L0, 658 cases): each model is applied to symbolic strings and ints. With the variables fixed to a sample, it must admit only Java's result. ForcompareTothe test checks only that Java's result is admitted, because Z3 does not show uniqueness through the UF within reasonable time. The index checks must hold iff Java does not throw.StringIndexCheckAgentSpec(L2, forked agent):charAtrecords its index check on both sides, and no context loss when it throws.testandagentTestsuites pass.Known limits
replacestays precision loss.reverseof symbolic content,codePointAtand similar methods, and StringBuffer stay unmodelled (context loss).compareTo/lastIndexOfUF encodings (Static pre-analysis pruning, with soundness fixes #65): some queries end UNKNOWN by timeout.charAt. The negations are trivially UNSAT, and no timeouts appeared in the measured set.Overlap with #71
StringValue: it declares the same four UFs as MODEL_FUNCTION and records precision loss forlastIndexOf(int)with a symbolic char.StringValue.EXACT_UFSplus a check in DTOBuilder does the same for the UFs. On merge, keep Decide precision loss by symbol kind instead of by variable name #71's registry and dropEXACT_UFS.lastIndexOf(int)this PR pins the char (exact) instead. With the pin, the precision loss Decide precision loss by symbol kind instead of by variable name #71 records for a symbolic char is no longer needed.recordSymbolicPrecisionLoss(), as Decide precision loss by symbol kind instead of by variable name #71 expects.🤖 Generated with Claude Code