Repository navigation
fix(resource-lifetime): report use-after-release from the ownership engine - #270
Open
SizzleUnrlsd wants to merge 6 commits into
Open
SizzleUnrlsd wants to merge 6 commits into
SizzleUnrlsd wants to merge 6 commits into
Conversation
A Use event states that the resource a location holds must be live there. It leaves the state unchanged, so summaries and the cross-TU cache do not move. releasedUses() lists the uses whose resource may be released and is not uncertain, or is released on every path where it exists. An incomplete solve lists nothing.
…ngine The rule counted any read or write of a variable after a release as a use, so `p = NULL` after free(p) was reported and `*q` through a copy was missed. The collector now emits Use for memory accesses through a pointer, pointer arguments the callee likely dereferences, and returned pointers. Copies, writes into the slot, comparisons and the released argument are not uses. The diagnostic comes from the engine run that already reports leaks. The counter path for this rule is removed. 05_null_deref.c: the two use-after-release expectations, on `p = NULL` and on the null dereference after it, become a negative assertion. The null dereference stays pinned. Part of #104.
…n delete The second `delete p` of an int stays a double release, now pinned at its exact location, 7:5. Its use-after-release came from the null test that delete emits, which reads only the pointer value. A delete that runs a destructor still reports both (see delete-twice-with-destructor.cpp).
Copying a released pointer into a global, an out parameter or a heap object, returning it, or passing it to unknown code marked the resource Escaped, so a later access through the pointer was no longer reported. A new owner does not make freed memory live again: these hand-overs now change live resources only. Summaries change for released inputs, so the resource summary cache moves to v6. Summaries also list the arguments that a Use in the function may reach (usedArgs, kept in memory only), for the collector to read.
…esses The counters reported these uses after a release; the engine missed them: - A call to a defined function that reads or writes through the argument, on some path or through further calls. The call is now a use when the callee's summary lists the argument. A call through a pointer, or to a function without a summary, is a use, like a call to a declaration. An argument that a callee releases only through unknown code is still a use. - p = p + 1, p++ or q = &p->field. The copy now carries the resource of the pointer the address is computed from, so *p and *q are judged. Computing the address is not a use.
…he argument A callee that releases its argument was never a use, even when it also accesses it, so `free(p); write_and_free(p);` reported only the double release. The counters reported the use-after-release as well. The call is now a use of a released argument when the callee's summary lists a Use of it, on the same path as the release or on another one. A model release or a wrapper that only releases stays no use. The summary cache now stores usedArgs, so a callee in another file gives the same result when its summary comes from the cache. A cache file without them is a miss and is rebuilt.
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.
Part of #104 (the
UseAfterReleasehalf;DoubleReleasestays on the counters).Summary
Use-after-release is now reported by the ownership engine, which tracks resources apart from the variables that hold them. Before this change, it came from per-variable counters: after a release, any read or write of the variable that held the resource counted as a use. As a result:
Now
p = NULLis not reported and*q = 42is.Changes
Engine.
Useevent means "the resourcesrcholds must be live here". It does not change the state.releasedUses()lists a use when the resource may be released and is not uncertain. It also lists a use when the resource is released on every path where it exists: a certain release came last, so earlier uncertainty does not matter. Nothing is listed when the solve is incomplete.Usein the function may reach (usedArgs). The summary cache stores them, so a callee in another file gives the same result when its summary comes from the cache. A v6 file without them is a cache miss and is rebuilt.Collector. These operations emit
Use:load,store, atomics,memset/memcpy/memmove);These operations do not emit
Use:p = NULL,p = malloc(..));deleteemits;p + 1,&p->field);An address computed from a pointer carries that pointer's resource when it is copied (
p = p + 1,q = &p->field).Diagnostic.
ResourceLifetime.UseAfterReleaseis reported from the run that already producesMissingRelease. The message and severity are unchanged. The legacy counter path for this rule is removed.DoubleRelease,IncompleteInterproc,ReleasedHandleEscapesandMissingDestructorReleaseare untouched.Known limits
**pp) is not followed, as before.if (c) { free(p); p = NULL; } if (p) *p = 1;is still reported, as before.p = p + 1on a live handle that is never released, the leak warning reads "not released in this function" instead of "overwritten while still owned", becausepstill points into the allocation.free(p); *p = 1;) reports nothing about that use in its own body; its callers see the use at the call. The counters did not report it either: they needed an acquisition in the same function.Review follow-up
A review compared
db7fb25with the first version of this PR. It found use-after-release reports that the counters made and the engine missed:p = p + 1.A wider probe found the same gap for:
p++and&t->b;Commits 4 and 5 fix these. Each case has a fixture with a live witness that reports nothing.
A second review found one more case: a callee that uses the argument and also releases it (
*p = 42; free(p);, or a release on one path and a write on the other). The release hid the use, sofree(p); write_and_free(p);reported only the double release. Commit 6 fixes it, in one file and across files. A callee that only releases the argument is still no use.Tests
testUses: a use leaves the state unchanged; an alias after a release is reported and a cleared owner is not; reacquisition; a loop; a release on one path only; uncertainty; an unknown call before and after a certain release; a use on an edge; an unreachable block; an incomplete solve.testUses, hand-overs of a released resource: a global, an out parameter, a resource released on one path only, a return, a call to unknown code.testUsedArgs: a use through a copy on one path, an unreachable use, a use of the function's own resource, andsameSummary.p = p + 1;usedArgscomputed from real IR.test/resource-lifetime/use-after-release-alias.c. It covers the alias cases. It also covers the cases that are not uses: a cleared owner, reassignment from a modeled or an unmodeled callee (the DoubleRelease and UseAfterRelease: derive them from the ownership engine #104 acceptance case), and a comparison. Finally, it covers an external call after a release, and a certain release after an external call.test/resource-lifetime/delete-twice-with-destructor.cpp. A seconddeletethat runs a destructor stays a use-after-release as well as a double release.test/resource-lifetime/use-after-release-through-callee.c. It covers calls that write, read, write on one path, pass the pointer to unknown code or to another local function, or go through a pointer. It also covers callees that write then release, or release on one path and write on the other. A callee that only keeps the pointer, or only releases it, is not a use. Live witnesses call the releasing callees too: no use-after-release, only the possible leak the one-path release leaves.check_ownership_cross_tuinrun_test.py, withcross-tu-use-then-release-def.cand-use.c. The same releasing callees are in another file, with the same live witnesses. The use-after-release must be reported at the two calls and nowhere else, without and with Z3, on three runs that share a summary cache; the third reads every summary from it.test/resource-lifetime/use-after-release-escape-offset.c. It covers a global, an out parameter, a heap object, unknown code,p + 1,p++and&t->b. The use is reported at the access, not at the copy or the address computation.test/security/null-dereference/05_null_deref.c: the two use-after-release expectations, onp = NULLand on*pafter it, are replaced by a negative assertion. The null dereference stays pinned.test/resource-lifetime/new-double-delete.cpp, in its own commit: the use-after-release expectation is replaced by a negative assertion. The double release is now pinned at its exact location, 7:5. It was pinned at line 6, which only passed because of the runner's line tolerance.Diagnostics before and after
This compares
db7fb25with this PR diagnostic by diagnostic (file, function, line, column, rule) on 86 sources. They are every fixture with a resource model, the review's reproducers and a probe file of 15 cases.p = NULL, uses of a slot reassigned after the release, and a comparison after a release;05_null_deref.clines 37 and 38;new-double-delete.cpp:7:12(see below);global = p,*out = p,h->q = p,p = p + 1,p++,&t->b); the access that follows is still reported;*qafterq = p; free(p));*bafterb = &t->b,*qafterq = p + 1; free(p),*pafterq = p + 0; free(q)).p = p + 1, in two reproducers (see Known limits).DoubleRelease,03_use_after_free.c:21and18_use_after_free_advanced.c:37.Commit 6 was compared with the previous head (
bb5f1e7) the same way, on the same sources plus a file with the second review's cases. Nothing is removed or changed. The 13 added diagnostics are all in the new cases: 5 use-after-release at the call, the 7 double releases those cases contain, and the possible leak of the live witness whose callee releases on one path only.new-double-delete.cppstill reports the double release at 7:5. The extra use-after-release at 7:12 came from the null test thatdeleteemits, and it is gone. That expectation is changed in its own commit.delete-twice-with-destructor.cppstill reports both diagnostics, at 15:5 and 15:12.Results
These results come from a Release build with Z3, in the CI's Linux image (x86_64, LLVM 20.1.8), with 4 CPUs.
ctest2/2 andrun_test.py --jobs=4 --no-cache2760/2760, default and Z3 passes. That is 14 checks more than commit 5 (2746).ctest2/2 and 2746/2746.ctest2/2 andrun_test.py2714/2714.usedArgsleft out of the cache, which only the third cached run catches.Out of scope
A double release through an alias (
q = p; free(p); free(q);) is still not reported. Only the incomplete-analysis warning appears onq. That belongs to theDoubleReleasehalf of #104, where the reproducer is posted.