From 6a0e3cad2648b694513d0e3a0c425193b08a93a0 Mon Sep 17 00:00:00 2001 From: Owen Carey <37121709+owenthcarey@users.noreply.github.com> Date: Wed, 21 Oct 2026 21:06:24 -0700 Subject: [PATCH 1/4] feat!: reuse freed memory only after a sweep and delete the analysis --- .github/ISSUE_TEMPLATE/bug_report.yml | 27 +- .github/ISSUE_TEMPLATE/feature_request.yml | 14 +- .github/PULL_REQUEST_TEMPLATE.md | 16 +- .github/workflows/ci.yml | 29 +- .github/workflows/corpus.yml | 9 +- AGENTS.md | 183 +- CONTRIBUTING.md | 73 +- README.md | 144 +- docs/README.md | 2 +- docs/annotations.md | 813 +--- docs/architecture.md | 1221 ++--- docs/astro.config.mjs | 10 +- docs/data/diagnostic-remedies.json | 19 - docs/development.md | 168 +- docs/examples/fixed.c | 15 - docs/examples/use-after-free.c | 15 - docs/package.json | 4 +- docs/pages/404.md | 3 +- docs/pages/contributing/overview.md | 10 +- docs/pages/contributing/website.md | 25 +- docs/pages/getting-started/first-check.md | 131 +- docs/pages/getting-started/installation.mdx | 14 +- docs/pages/getting-started/introduction.md | 38 +- docs/pages/guides/adoption.md | 47 +- docs/pages/guides/build-integration.md | 37 +- docs/pages/guides/ownership.md | 56 - docs/pages/guides/unsafe.md | 40 +- docs/pages/guides/whole-program.md | 39 - docs/pages/index.mdx | 2 +- docs/pages/reference/cli.md | 152 +- docs/pages/reference/compatibility.md | 41 +- docs/pages/reference/guarantees.md | 90 +- docs/pages/reference/troubleshooting.md | 92 +- docs/pages/rfcs.mdx | 6 +- docs/rfcs/0000-template.md | 76 +- docs/rfcs/0001-ownership-model.md | 8 +- docs/rfcs/0002-intraprocedural-checking.md | 9 +- docs/rfcs/0003-signature-inference.md | 10 +- docs/rfcs/0004-unsafe-boundaries.md | 14 +- docs/rfcs/0005-whole-program-analysis.md | 20 +- docs/rfcs/0006-precision.md | 22 +- docs/rfcs/0007-resource-lifecycle.md | 20 +- docs/rfcs/0008-pointer-validity.md | 16 +- docs/rfcs/0009-value-conditional-behaviour.md | 20 +- docs/rfcs/0010-shared-ownership.md | 24 +- docs/rfcs/0011-spatial-safety.md | 31 +- .../0012-spatial-safety-strings-and-fields.md | 24 +- docs/rfcs/0013-interprocedural-heap-state.md | 12 +- .../0014-pointer-identity-and-call-effects.md | 12 +- .../0015-array-and-container-ownership.md | 13 +- docs/rfcs/0016-compositional-call-checking.md | 14 +- ...-c-integer-semantics-and-spatial-safety.md | 17 +- docs/rfcs/0030-prove-or-trap.md | 10 +- docs/rfcs/0031-object-engine.md | 28 +- docs/rfcs/0035-guard-by-default.md | 10 + docs/rfcs/0036-deterministic-heap-reuse.md | 728 +++ docs/rfcs/README.md | 72 +- docs/roadmap.md | 173 +- docs/scripts/check-examples.py | 44 +- docs/scripts/content.test.mjs | 26 +- docs/scripts/prepare-content.mjs | 47 +- docs/scripts/watch-content.mjs | 4 +- docs/src/components/Head.astro | 2 +- docs/src/components/Home.astro | 80 +- docs/src/styles/custom.css | 14 +- docs/tests/site.spec.ts | 15 +- include/weavec/Analysis/Annotations.h | 234 - include/weavec/Analysis/AttributeReader.h | 81 - include/weavec/Analysis/BoundaryInvariants.h | 75 - .../weavec/Analysis/BypassedDeclarations.h | 38 - include/weavec/Analysis/ClangLocation.h | 33 - include/weavec/Analysis/Concurrency.h | 63 - include/weavec/Analysis/KindInference.h | 208 - include/weavec/Analysis/KindTable.h | 400 -- include/weavec/Analysis/LedgerAdapter.h | 320 -- include/weavec/Analysis/ObjectEngine.h | 61 - include/weavec/Analysis/ProgramDatabase.h | 207 - include/weavec/Analysis/SafetyEngine.h | 111 - include/weavec/Analysis/SiteCollector.h | 294 -- include/weavec/Analysis/SlotCollector.h | 152 - include/weavec/Analysis/SourceTerm.h | 93 - include/weavec/Analysis/UnitPipeline.h | 78 - include/weavec/Core/AnalysisStats.h | 59 - include/weavec/Core/Core.h | 31 - include/weavec/Core/Diagnostic.h | 228 - include/weavec/Core/Effects.h | 310 -- include/weavec/Core/FnSlots.h | 343 -- include/weavec/Core/Heap.h | 1030 ----- include/weavec/Core/Integer.h | 190 - include/weavec/Core/Ledger.h | 429 -- include/weavec/Core/LibrarySpec.h | 357 +- include/weavec/Core/Path.h | 204 - include/weavec/Core/Persistent.h | 260 -- include/weavec/Core/PointerKind.h | 220 - include/weavec/Core/Scc.h | 31 - include/weavec/Core/SourceLocation.h | 43 - include/weavec/Core/Zone.h | 182 - include/weavec/Frontend/AnalysisStats.h | 19 - include/weavec/Frontend/AnalysisSummary.h | 58 - include/weavec/Frontend/ClangDiagnosticSink.h | 37 - include/weavec/Frontend/DiagnosticControl.h | 151 - include/weavec/Frontend/Driver.h | 36 +- include/weavec/Frontend/FrontendAction.h | 130 - include/weavec/Frontend/InterfaceFacts.h | 154 - include/weavec/Frontend/ProgramAnalysis.h | 225 - include/weavec/Frontend/ProgramChecks.h | 102 - include/weavec/Frontend/UnsafeRegions.h | 4 + lib/Analysis/Annotations.cpp | 358 -- lib/Analysis/AttributeReader.cpp | 783 ---- lib/Analysis/BoundaryInvariants.cpp | 205 - lib/Analysis/BypassedDeclarations.cpp | 147 - lib/Analysis/CMakeLists.txt | 41 - lib/Analysis/ClangLocation.cpp | 37 - lib/Analysis/Concurrency.cpp | 256 -- lib/Analysis/Engine.h | 1284 ------ lib/Analysis/EngineAnnotations.cpp | 377 -- lib/Analysis/EngineCalls.cpp | 2554 ----------- lib/Analysis/EngineContexts.cpp | 281 -- lib/Analysis/EngineDecide.cpp | 2470 ----------- lib/Analysis/EngineExpr.cpp | 3671 --------------- lib/Analysis/EngineIntegers.h | 148 - lib/Analysis/EngineInvariants.cpp | 286 -- lib/Analysis/EngineKinds.cpp | 549 --- lib/Analysis/EngineLibrary.cpp | 624 --- lib/Analysis/EngineLifetimes.cpp | 1341 ------ lib/Analysis/EngineRun.cpp | 2539 ----------- lib/Analysis/EngineStrings.cpp | 475 -- lib/Analysis/EngineSummary.cpp | 3574 --------------- lib/Analysis/EngineUnit.cpp | 806 ---- lib/Analysis/KindInference.cpp | 1020 ----- lib/Analysis/KindInferenceCollect.cpp | 980 ----- lib/Analysis/KindInferenceDump.cpp | 159 - lib/Analysis/KindInferenceFields.cpp | 164 - lib/Analysis/KindInferenceImpl.h | 373 -- lib/Analysis/KindInferenceMustAccess.cpp | 1366 ------ lib/Analysis/KindTable.cpp | 272 -- lib/Analysis/LedgerAdapter.cpp | 566 --- lib/Analysis/ProgramDatabase.cpp | 191 - lib/Analysis/SafetyEngine.cpp | 15 - lib/Analysis/SiteCollector.cpp | 1737 -------- lib/Analysis/SlotCollector.cpp | 908 ---- lib/Analysis/SourceTerm.cpp | 126 - lib/Analysis/UnitPipeline.cpp | 140 - lib/CMakeLists.txt | 7 +- lib/Core/CMakeLists.txt | 22 +- lib/Core/Diagnostic.cpp | 33 - lib/Core/Effects.cpp | 601 --- lib/Core/FnSlots.cpp | 790 ---- lib/Core/Heap.cpp | 3919 ----------------- lib/Core/Integer.cpp | 793 ---- lib/Core/Ledger.cpp | 497 --- lib/Core/LibrarySpec.cpp | 1260 ++---- lib/Core/LibrarySpec.txt | 1791 ++------ lib/Core/Path.cpp | 84 - lib/Core/PointerKind.cpp | 377 -- lib/Core/Scc.cpp | 100 - lib/Core/SourceLocation.cpp | 24 - lib/Core/Zone.cpp | 751 ---- lib/Frontend/AnalysisStats.cpp | 116 - lib/Frontend/AnalysisSummary.cpp | 107 - lib/Frontend/CMakeLists.txt | 19 +- lib/Frontend/ClangDiagnosticSink.cpp | 74 - lib/Frontend/DiagnosticControl.cpp | 185 - lib/Frontend/Driver.cpp | 190 +- lib/Frontend/FrontendAction.cpp | 251 -- lib/Frontend/GuardFrames.cpp | 89 +- lib/Frontend/GuardInsert.cpp | 4 + lib/Frontend/GuardLibrary.cpp | 92 +- lib/Frontend/GuardPass.cpp | 38 +- lib/Frontend/GuardPassImpl.h | 2 + lib/Frontend/InterfaceFacts.cpp | 237 - lib/Frontend/ProgramAnalysis.cpp | 612 --- lib/Frontend/ProgramChecks.cpp | 405 -- lib/Frontend/UnsafeRegions.cpp | 8 +- resources/include/weavec.h | 177 +- runtime/CMakeLists.txt | 19 +- runtime/test/rt_test.c | 324 +- runtime/weavec_alloc.c | 424 +- runtime/weavec_asan.c | 151 + runtime/weavec_guard.c | 43 +- runtime/weavec_libc.c | 142 +- runtime/weavec_owner.c | 19 +- runtime/weavec_report.c | 149 +- runtime/weavec_rt.h | 131 +- runtime/weavec_sweep.c | 864 ++++ scripts/check-hygiene.py | 374 +- scripts/corpus-gate.py | 355 +- scripts/run-cases.py | 300 +- scripts/test_check_hygiene.py | 478 +- scripts/test_corpus_gate.py | 247 +- scripts/test_run_cases.py | 242 +- test/Analysis/analysis-stats.c | 15 - test/Analysis/rfc0002-borrows.c | 161 - test/Analysis/rfc0004-function-pointers.c | 94 - test/Analysis/rfc0004-posix.c | 115 - test/Analysis/rfc0004-raw.c | 115 - test/Analysis/rfc0004-unsafe-regions.c | 91 - test/Analysis/rfc0006-conditions.c | 105 - test/Analysis/rfc0006-outcomes.c | 151 - test/Analysis/rfc0006-written.c | 29 - test/Analysis/rfc0007-clean.c | 294 -- test/Analysis/rfc0007-families.c | 78 - test/Analysis/rfc0008-null.c | 225 - test/Analysis/rfc0008-replaced.c | 107 - test/Analysis/rfc0008-uninit.c | 86 - test/Analysis/rfc0009-arguments.c | 180 - test/Analysis/rfc0009-noreturn.c | 114 - test/Analysis/rfc0009-replaced.c | 92 - test/Analysis/rfc0009-scalars.c | 211 - test/Analysis/rfc0010-escapes.c | 124 - test/Analysis/rfc0010-outcomes.c | 145 - test/Analysis/rfc0010-refcount.c | 206 - test/Analysis/rfc0011-bounds.c | 311 -- test/Analysis/rfc0011-derived.c | 239 - test/Analysis/rfc0011-requirements.c | 218 - test/Analysis/rfc0012-assume.c | 61 - test/Analysis/rfc0012-relations.c | 73 - test/Analysis/rfc0012-strings.c | 171 - test/Analysis/rfc0013-heap.c | 63 - test/Analysis/rfc0015-limits.c | 54 - test/Analysis/rfc0016-clean.c | 45 - test/Analysis/rfc0017-integer-diagnostics.c | 36 - test/Analysis/rfc0017-target-and-unsafe.c | 46 - test/Analysis/rfc0030-assume.c | 30 - test/Analysis/rfc0030-budget.c | 35 - test/Analysis/rfc0030-certainty.c | 141 - test/Analysis/rfc0030-declared-kinds.c | 120 - test/Analysis/rfc0030-library.c | 60 - test/Analysis/rfc0030-literal-write.c | 24 - test/Analysis/rfc0030-owner-uniqueness.c | 71 - test/Analysis/rfc0030-reallocator-outcomes.c | 99 - .../Analysis/rfc0030-slot-candidate-classes.c | 61 - test/Analysis/rfc0030-sound-defaults.c | 55 - test/Annotations/Inputs/kinds-lstate.h | 17 - test/Annotations/Inputs/vendor-kinds.h | 9 - test/Annotations/Inputs/vendor.h | 8 - test/Annotations/kinds-alloc-size.c | 30 - test/Annotations/kinds-array-parameters.c | 33 - test/Annotations/kinds-count-attributes.c | 23 - test/Annotations/kinds-counted-by.c | 57 - test/Annotations/kinds-defaults.c | 48 - test/Annotations/kinds-ended-by.c | 29 - test/Annotations/kinds-fields.c | 55 - test/Annotations/kinds-fn-slots.c | 59 - test/Annotations/kinds-must-access.c | 131 - test/Annotations/kinds-nonnull-attributes.c | 29 - test/Annotations/kinds-nullability.c | 31 - test/Annotations/kinds-ownership-attributes.c | 26 - test/Annotations/kinds-precedence.c | 25 - test/Annotations/kinds-slots.c | 92 - test/Annotations/kinds-string.c | 30 - test/Annotations/ownership-annotations.c | 37 - test/Annotations/rfc0003-unknown-extern.c | 42 - test/Annotations/rfc0010-annotations.c | 105 - test/Annotations/rfc0011-sized-by.c | 66 - test/CMakeLists.txt | 2 +- test/Driver/Inputs/buggy-header.h | 10 - test/Driver/Inputs/image-globals-lib.c | 6 + test/Driver/Inputs/rfc0014-callback-client.c | 10 - test/Driver/Inputs/rfc0030-hook-state.c | 12 - test/Driver/Inputs/rfc0030-hook-state.h | 13 - test/Driver/Inputs/rfc0030-inspect.c | 4 - test/Driver/asan-compat-fortify.c | 6 + test/Driver/asan-compat.c | 24 + test/Driver/checks-none-identity.c | 23 +- test/Driver/compilation-database-p.c | 50 - test/Driver/diagnostics-format-sarif.c | 40 - test/Driver/dump-analysis.c | 65 - test/Driver/flags.c | 45 + test/Driver/fortify.c | 14 + test/Driver/headers-analysed.c | 14 - test/Driver/image-globals.c | 30 + test/Driver/leak-default.c | 23 - test/Driver/rfc0005-flags.c | 69 - test/Driver/rfc0007-flags.c | 33 - test/Driver/rfc0008-flags.c | 38 - test/Driver/rfc0014-callback-link.c | 13 - test/Driver/rfc0030-flags.c | 91 - test/Driver/rfc0030-link-declarations.c | 28 - test/Driver/rfc0030-link-slots.c | 36 - test/Driver/system-headers.c | 20 - test/Driver/version.c | 19 +- test/Guards/alloca-entry.c | 22 + test/Guards/intra-object.c | 23 + test/Guards/shared-frame.c | 33 + test/Guards/use-after-poison.c | 30 + test/Inputs/prelude.h | 5 +- test/README.md | 43 +- test/WholeProgram/Inputs/array15.c | 17 - test/WholeProgram/Inputs/array15.h | 11 - test/WholeProgram/Inputs/buffers.c | 23 - test/WholeProgram/Inputs/buffers.h | 21 - test/WholeProgram/Inputs/counted.c | 25 - test/WholeProgram/Inputs/counted.h | 14 - test/WholeProgram/Inputs/die.c | 22 - test/WholeProgram/Inputs/grow.c | 19 - test/WholeProgram/Inputs/handle.c | 17 - test/WholeProgram/Inputs/handle.h | 12 - test/WholeProgram/Inputs/handlers.c | 7 - test/WholeProgram/Inputs/heap13.c | 38 - test/WholeProgram/Inputs/heap13.h | 13 - test/WholeProgram/Inputs/node.c | 25 - test/WholeProgram/Inputs/node.h | 15 - test/WholeProgram/Inputs/rfc0016-callee.c | 9 - test/WholeProgram/Inputs/rfc0017-numeric.c | 18 - test/WholeProgram/Inputs/rfc0030-first.c | 3 - test/WholeProgram/Inputs/validity.c | 28 - test/WholeProgram/Inputs/validity.h | 26 - test/WholeProgram/Inputs/vec.c | 43 - test/WholeProgram/Inputs/vec.h | 29 - test/WholeProgram/program-summary.c | 20 - test/WholeProgram/retention.c | 42 - test/WholeProgram/rfc0005-boundary-once.c | 36 - test/WholeProgram/rfc0005-callbacks.c | 29 - test/WholeProgram/rfc0005-cross-unit.c | 59 - test/WholeProgram/rfc0005-dump.c | 44 - test/WholeProgram/rfc0006-outcomes.c | 61 - test/WholeProgram/rfc0007-families.c | 58 - test/WholeProgram/rfc0008-validity.c | 115 - test/WholeProgram/rfc0009-noreturn-units.c | 51 - test/WholeProgram/rfc0010-shares.c | 72 - test/WholeProgram/rfc0011-extents.c | 94 - test/WholeProgram/rfc0012-sized-fields.c | 55 - test/WholeProgram/rfc0013-heap.c | 37 - test/WholeProgram/rfc0013-lazy.c | 15 - test/WholeProgram/rfc0013-swap.c | 28 - test/WholeProgram/rfc0015-arrays.c | 48 - test/WholeProgram/rfc0016-composition.c | 27 - test/WholeProgram/rfc0017-numeric.c | 79 - test/cases/CMakeLists.txt | 4 +- test/cases/README.md | 118 +- test/cases/determinism/README.md | 24 + test/cases/determinism/asan-pool_bug.c | 42 + test/cases/determinism/asan-pool_ok.c | 45 + .../determinism/double-free-after-churn_bug.c | 19 + .../determinism/double-free-after-churn_ok.c | 19 + test/cases/determinism/escaping-oob_bug.c | 35 + test/cases/determinism/escaping-oob_ok.c | 37 + test/cases/determinism/field-strcpy_bug.c | 22 + test/cases/determinism/field-strcpy_ok.c | 22 + test/cases/determinism/global-jump_bug.c | 17 + test/cases/determinism/global-jump_ok.c | 15 + test/cases/determinism/heap-jump_bug.c | 19 + test/cases/determinism/heap-jump_ok.c | 16 + test/cases/determinism/shared-frame_ok.c | 28 + test/cases/determinism/uaf-after-churn_bug.c | 35 + test/cases/determinism/uaf-after-churn_ok.c | 36 + test/cases/determinism/uaf-spray_bug.c | 22 + test/cases/determinism/uaf-spray_ok.c | 25 + .../cases/determinism/use-after-longjmp_bug.c | 26 + test/cases/determinism/use-after-longjmp_ok.c | 25 + test/cases/determinism/use-after-return_bug.c | 26 + test/cases/determinism/use-after-return_ok.c | 25 + test/cases/engine/Analysis-branches.c | 20 - test/cases/engine/Analysis-clean.c | 40 - test/cases/engine/Analysis-double-free.c | 7 - test/cases/engine/Analysis-rfc0002-aliases.c | 81 - .../cases/engine/Analysis-rfc0002-lifetimes.c | 63 - test/cases/engine/Analysis-rfc0002-moves.c | 24 - test/cases/engine/Analysis-rfc0002-paths.c | 57 - test/cases/engine/Analysis-rfc0002-places.c | 53 - test/cases/engine/Analysis-rfc0002-realloc.c | 49 - test/cases/engine/Analysis-rfc0003-escapes.c | 63 - test/cases/engine/Analysis-rfc0003-globals.c | 31 - test/cases/engine/Analysis-rfc0003-libc.c | 53 - test/cases/engine/Analysis-rfc0003-paths.c | 73 - test/cases/engine/Analysis-rfc0003-wrappers.c | 93 - .../engine/Analysis-rfc0004-laundering.c | 73 - test/cases/engine/Analysis-rfc0006-elements.c | 89 - test/cases/engine/Analysis-rfc0006-nll.c | 83 - test/cases/engine/Analysis-rfc0007-leaks.c | 79 - .../engine/Analysis-rfc0007-owned-fields.c | 99 - test/cases/engine/Analysis-rfc0008-release.c | 72 - .../cases/engine/Analysis-rfc0011-lifetimes.c | 64 - .../engine/Analysis-rfc0012-sized-fields.c | 121 - test/cases/engine/Analysis-rfc0013-clean.c | 29 - test/cases/engine/Analysis-rfc0014-clean.c | 49 - .../Analysis-rfc0014-pointer-identity.c | 47 - test/cases/engine/Analysis-rfc0015-elements.c | 31 - test/cases/engine/Analysis-rfc0015-ranges.c | 24 - .../engine/Analysis-rfc0016-boundaries.c | 40 - .../engine/Analysis-rfc0016-composition.c | 28 - test/cases/engine/Analysis-use-after-free.c | 14 - .../engine/Annotations-invalid-annotation.c | 7 - .../Annotations-rfc0003-annotation-mismatch.c | 61 - test/cases/engine/Annotations-unsafe.c | 19 - test/cases/engine/Inputs/cycle-b.c | 12 - test/cases/engine/Inputs/heap.c | 15 - test/cases/engine/Inputs/heap.h | 7 - test/cases/engine/Inputs/heap13.c | 38 - test/cases/engine/Inputs/heap13.h | 13 - test/cases/engine/Inputs/prelude.h | 16 - .../cases/engine/WholeProgram-rfc0005-cycle.c | 18 - .../WholeProgram-rfc0013-input-identity.c | 29 - test/cases/evaluation/helper-alias.c | 12 - test/cases/evaluation/helper-good.c | 11 - test/cases/evaluation/helper-leak.c | 8 - test/cases/evaluation/helper-overflow.c | 2 +- test/cases/evaluation/inline-alias.c | 12 - test/cases/evaluation/inline-good.c | 12 - test/cases/evaluation/inline-leak.c | 9 - test/cases/evaluation/inline-overflow.c | 2 +- test/cases/evaluation/output-size.c | 2 +- test/cases/evaluation/product-known-miss.c | 2 +- test/cases/evaluation/rfc0014-callback-bug.c | 9 - test/cases/evaluation/rfc0014-callback-good.c | 8 - .../evaluation/rfc0014-copied-pointer-bug.c | 6 - .../evaluation/rfc0014-copied-pointer-good.c | 5 - .../evaluation/rfc0014-pointer-guard-bug.c | 7 - .../evaluation/rfc0014-pointer-guard-good.c | 6 - .../evaluation/rfc0014-whole-callback-bug.c | 9 - .../evaluation/rfc0014-whole-callback-good.c | 8 - .../evaluation/rfc0015-array-cleanup-bug.c | 8 - .../evaluation/rfc0015-array-cleanup-good.c | 7 - .../evaluation/rfc0015-array-compacted-bug.c | 9 - .../evaluation/rfc0015-array-compacted-good.c | 8 - .../cases/evaluation/rfc0015-array-copy-bug.c | 7 - .../evaluation/rfc0015-array-copy-good.c | 6 - .../cases/evaluation/rfc0015-array-fill-bug.c | 8 - .../evaluation/rfc0015-array-fill-good.c | 7 - .../evaluation/rfc0015-array-history-bug.c | 7 - .../evaluation/rfc0015-array-history-good.c | 6 - .../evaluation/rfc0015-array-index-bug.c | 7 - .../evaluation/rfc0015-array-index-good.c | 6 - .../rfc0015-array-initialization-bug.c | 2 +- .../rfc0015-array-initialization-good.c | 6 - .../evaluation/rfc0015-array-memmove-bug.c | 7 - .../evaluation/rfc0015-array-memmove-good.c | 6 - .../evaluation/rfc0015-array-record-bug.c | 8 - .../evaluation/rfc0015-array-record-good.c | 7 - .../evaluation/rfc0015-array-resized-bug.c | 9 - .../evaluation/rfc0015-array-resized-good.c | 8 - .../evaluation/rfc0015-array-returned-bug.c | 10 - .../evaluation/rfc0015-array-returned-good.c | 8 - .../evaluation/rfc0015-array-symbolic-bug.c | 7 - .../evaluation/rfc0015-array-symbolic-good.c | 6 - test/cases/evaluation/rfc0016-array-bad.c | 4 - test/cases/evaluation/rfc0016-array-good.c | 10 - test/cases/evaluation/rfc0016-callback-bad.c | 5 - test/cases/evaluation/rfc0016-callback-good.c | 6 - test/cases/evaluation/rfc0016-cross-bad.c | 5 - test/cases/evaluation/rfc0016-cross-good.c | 6 - test/cases/evaluation/rfc0016-double-bad.c | 4 - test/cases/evaluation/rfc0016-double-good.c | 5 - test/cases/evaluation/rfc0016-global-bad.c | 4 - test/cases/evaluation/rfc0016-global-good.c | 5 - test/cases/evaluation/rfc0016-guard-bad.c | 4 - test/cases/evaluation/rfc0016-guard-good.c | 5 - test/cases/evaluation/rfc0016-inline-bad.c | 3 - test/cases/evaluation/rfc0016-inline-good.c | 4 - test/cases/evaluation/rfc0016-interior-bad.c | 4 - test/cases/evaluation/rfc0016-interior-good.c | 5 - test/cases/evaluation/rfc0016-nested-bad.c | 5 - test/cases/evaluation/rfc0016-nested-good.c | 6 - test/cases/evaluation/rfc0016-order-bad.c | 4 - test/cases/evaluation/rfc0016-order-good.c | 5 - test/cases/evaluation/rfc0016-outputs-bad.c | 4 - test/cases/evaluation/rfc0016-outputs-good.c | 5 - .../evaluation/rfc0016-replacement-bad.c | 4 - .../evaluation/rfc0016-replacement-good.c | 5 - test/cases/evaluation/shared-child.c | 14 - test/cases/evaluation/snapshot-good.c | 10 - test/cases/evaluation/string-field.c | 2 +- test/cases/evaluation/symbolic-snapshot.c | 2 +- test/cases/evaluation/value-snapshot.c | 2 +- test/cases/evaluation/vla-known-miss.c | 2 +- test/cases/evaluation/whole-program-alias.c | 12 - test/cases/evaluation/whole-program-good.c | 11 - test/cases/evaluation/whole-program-leak.c | 8 - .../cases/evaluation/whole-program-overflow.c | 2 +- test/cases/pairs/bool-bug.c | 11 - test/cases/pairs/bool-good.c | 12 - test/cases/pairs/checked-builtin-bug.c | 13 - test/cases/pairs/checked-builtin-good.c | 19 - test/cases/pairs/fam-bug.c | 2 +- test/cases/pairs/fam-good.c | 14 - test/cases/pairs/helper-return-bug.c | 2 +- test/cases/pairs/helper-return-good.c | 12 - test/cases/pairs/memcpy-wrapped-zero-bug.c | 13 - test/cases/pairs/memcpy-wrapped-zero-good.c | 13 - test/cases/pairs/min-bound-bug.c | 2 +- test/cases/pairs/min-bound-good.c | 11 - test/cases/pairs/mixed-signed-bug.c | 10 - test/cases/pairs/mixed-signed-good.c | 11 - test/cases/pairs/narrowing-bug.c | 11 - test/cases/pairs/narrowing-good.c | 12 - test/cases/pairs/outparam-bug.c | 2 +- test/cases/pairs/outparam-good.c | 14 - test/cases/pairs/product-bug.c | 2 +- test/cases/pairs/product-good.c | 12 - test/cases/pairs/unsigned-wrap-bug.c | 2 +- test/cases/pairs/unsigned-wrap-good.c | 12 - test/cases/pairs/vla-snapshot-bug.c | 2 +- test/cases/pairs/vla-snapshot-good.c | 13 - test/cases/proofs/attached-payload-ignored.c | 7 - test/cases/proofs/attached-payload-leak.c | 7 - test/cases/proofs/reader-uninitialized.c | 9 - test/cases/proofs/recursive-offset-forward.c | 6 +- .../cases/recall/CWE-121/callee_writes_past.c | 4 +- test/cases/recall/CWE-121/constant_index.c | 2 +- test/cases/recall/CWE-121/fgets_too_long.c | 2 +- test/cases/recall/CWE-121/loop_off_by_one.c | 2 +- test/cases/recall/CWE-121/loop_wrong_bound.c | 2 +- test/cases/recall/CWE-121/memcpy_too_much.c | 2 +- test/cases/recall/CWE-121/memset_too_much.c | 4 +- test/cases/recall/CWE-121/pointer_walk.c | 2 +- test/cases/recall/CWE-121/strcpy_literal.c | 6 +- test/cases/recall/CWE-122/constant_index.c | 2 +- test/cases/recall/CWE-122/memcpy_too_much.c | 4 +- test/cases/recall/CWE-122/sized_field.c | 4 +- test/cases/recall/CWE-122/strcpy_strlen.c | 6 +- test/cases/recall/CWE-122/symbolic_index.c | 4 +- test/cases/recall/CWE-122/wrapper_alloc.c | 4 +- test/cases/recall/CWE-122/wrong_size_alloc.c | 4 +- .../recall/CWE-124/pointer_before_start.c | 4 +- .../cases/recall/CWE-124/write_before_start.c | 4 +- test/cases/recall/CWE-126/loop_reads_past.c | 4 +- test/cases/recall/CWE-126/read_past_end.c | 4 +- .../recall/CWE-126/strlen_unterminated.c | 4 +- test/cases/recall/CWE-127/read_before_start.c | 4 +- .../recall/CWE-170/strncpy_unterminated.c | 8 +- test/cases/recall/CWE-401/leak_in_callee.c | 28 - test/cases/recall/CWE-401/leak_on_return.c | 28 - test/cases/recall/CWE-401/leak_overwritten.c | 24 - test/cases/recall/CWE-415/double_free.c | 30 - test/cases/recall/CWE-415/double_free_alias.c | 34 - test/cases/recall/CWE-416/use_after_free.c | 28 - .../recall/CWE-416/use_after_free_alias.c | 45 - .../recall/CWE-457/uninitialized_pointer.c | 4 +- test/cases/recall/CWE-476/null_dereference.c | 7 +- test/cases/recall/CWE-476/null_from_callee.c | 36 - test/cases/repros/arrloop.c | 1 - test/cases/repros/loopcorr.c | 1 - test/cases/repros/luaalloc.c | 4 +- test/cases/semantics/Inputs/rfc0030.h | 24 - .../semantics/aliasing/cursor-into-data.c | 2 +- .../aliasing/owning-fields-distinct.c | 19 - .../semantics/aliasing/two-same-argument.c | 2 +- .../semantics/assume/assumed-after-site.c | 19 - test/cases/semantics/assume/contradicted.c | 13 - test/cases/semantics/assume/proven.c | 12 - test/cases/semantics/boundary/atexit-reader.c | 2 +- .../boundary/global-written-by-unknown-code.c | 30 - .../incomplete-summary-writes-arguments.c | 1 - .../semantics/boundary/remember-free-peek.c | 2 +- .../semantics/concurrency/handler-race.c | 2 +- .../concurrency/worker-freed-global.c | 2 +- .../confirm/const-shallow-callback.c | 1 - .../semantics/confirm/exclusive-out-params.c | 1 - .../confirm/exclusive-result-or-out-param.c | 1 - .../confirm/exclusive-switch-out-params.c | 1 - .../semantics/confirm/munmap-constant-pages.c | 1 - .../semantics/confirm/munmap-hint-address.c | 1 - .../semantics/confirm/munmap-partial-range.c | 1 - .../semantics/confirm/munmap-whole-twice.c | 21 - .../confirm/release-by-flag-stream.c | 1 - .../cases/semantics/confirm/release-by-flag.c | 1 - .../semantics/confirm/shared-out-params.c | 34 - .../confirm/unreachable-after-assert.c | 1 - .../semantics/dropin/fam-strlen-copy_bug.c | 2 +- .../dropin/fortify-inline-wrapper_bug.c | 2 +- .../semantics/dropin/fortify-memcpy_bug.c | 2 +- .../semantics/dropin/fortify-strcat_bug.c | 2 +- .../semantics/dropin/free-while-copied_ok.c | 1 - .../semantics/dropin/lowered-violation_bug.c | 2 +- .../semantics/dropin/lowered-violation_ok.c | 5 +- .../semantics/dropin/memset-callee_bug.c | 10 - .../cases/semantics/dropin/printf-freed_bug.c | 2 +- .../dropin/printf-pointer-freed_bug.c | 2 +- .../cases/semantics/dropin/raw-declared_bug.c | 8 - .../semantics/dropin/strdup-overflow_bug.c | 2 +- test/cases/semantics/dropin/strdup-uaf_bug.c | 2 +- .../dropin/trap-signals-blocked_bug.c | 2 +- .../semantics/dropin/void-cast-store_bug.c | 2 +- .../emission/lowered-out-of-bounds.c | 2 +- .../lowered-use-after-free-no-runtime.c | 2 +- .../emission/lowered-use-after-free.c | 2 +- .../semantics/emission/qsort-wrapping-size.c | 2 +- .../semantics/emission/span-difference_bug.c | 4 +- .../semantics/examples/assumption-checked.c | 14 - .../examples/assumption-contradicted.c | 16 - test/cases/semantics/examples/checked-index.c | 3 +- .../semantics/examples/proven-static-extent.c | 7 - .../semantics/examples/temporal-definite.c | 2 +- .../semantics/examples/temporal-possible.c | 2 +- .../semantics/examples/trusted-concurrency.c | 2 +- .../semantics/examples/trusted-system-api.c | 14 - .../cases/semantics/examples/trusted-unsafe.c | 16 - .../examples/unresolved-unknown-callee-link.c | 2 +- .../examples/unresolved-unknown-callee.c | 16 - .../examples/unresolved-unknown-extent.c | 12 - .../cases/semantics/examples/violation-null.c | 2 +- .../semantics/examples/violation-spatial.c | 2 +- .../extents/null-after-checked-access.c | 26 - .../semantics/extents/subarray-subscript.c | 2 +- test/cases/semantics/kinds/attr-counted-by.c | 2 +- test/cases/semantics/kinds/attr-ownership.c | 22 - .../semantics/kinds/cursor-through-char-pp.c | 2 +- .../kinds/declared-counted-by-field.c | 5 +- .../kinds/declared-counted-by-param.c | 5 +- .../cases/semantics/kinds/declared-ended-by.c | 18 - .../semantics/kinds/declared-invalid-name.c | 15 - .../kinds/declared-nonnull-definite.c | 10 - test/cases/semantics/kinds/declared-nonnull.c | 7 +- .../cases/semantics/kinds/declared-sized-by.c | 5 +- .../semantics/kinds/declared-static-array.c | 10 - test/cases/semantics/kinds/declared-string.c | 7 +- .../semantics/kinds/declared-vla-param.c | 2 +- .../semantics/kinds/relies-on-single-call.c | 2 +- .../semantics/kinds/static-cursor-argument.c | 2 +- test/cases/semantics/ledger/budget.c | 15 - test/cases/semantics/ledger/extern-contract.c | 21 - test/cases/semantics/ledger/inline-asm.c | 17 - .../semantics/ledger/no-site-unevaluated.c | 20 - .../semantics/ledger/nonnull-destructor.c | 21 - .../semantics/ledger/setjmp-assigned-after.c | 1 - test/cases/semantics/ledger/setjmp.c | 19 - .../semantics/ledger/system-api-borrow.c | 19 - .../semantics/ledger/unknown-then-free.c | 18 - .../library/fill-shared-unknown-object.c | 51 - .../semantics/library/library-filled-result.c | 37 - .../semantics/library/realloc-zero-definite.c | 16 - .../semantics/library/realloc-zero-possible.c | 4 +- .../library/release-at-unknown-offset.c | 1 - test/cases/semantics/library/releasers.c | 23 - .../semantics/library/stdout-closed-twice.c | 37 - test/cases/semantics/library/wrapper-lossy.c | 2 +- .../semantics/library/wrapper-param-keyed.c | 30 - .../objects/array-summary-cells_bug.c | 2 +- .../semantics/objects/byte-copy-struct_bug.c | 2 +- .../semantics/objects/materialise-head_bug.c | 2 +- .../cases/semantics/objects/owner-cycle_bug.c | 2 +- .../semantics/objects/recency-loop_bug.c | 2 +- .../objects/strong-update-stale_bug.c | 2 +- .../cases/semantics/objects/weak-update_bug.c | 2 +- .../semantics/runtime/access-difference_bug.c | 12 +- .../semantics/runtime/access-subscript_bug.c | 12 +- test/cases/semantics/runtime/access-sum_bug.c | 16 +- .../semantics/runtime/cache-shared_bug.c | 2 +- .../semantics/runtime/callback-table_bug.c | 2 +- .../semantics/runtime/cleanup-function_bug.c | 2 +- .../runtime/format-precision-dangling_bug.c | 2 +- .../runtime/global-static-local_bug.c | 2 +- .../cases/semantics/runtime/global-view_bug.c | 2 +- .../semantics/runtime/heap-index-field_bug.c | 2 +- .../semantics/runtime/libcall-bytes_bug.c | 2 +- .../runtime/libcall-copy-source_bug.c | 2 +- .../semantics/runtime/libcall-string_bug.c | 2 +- .../semantics/runtime/realloc-stale_bug.c | 2 +- .../semantics/runtime/release-interior_bug.c | 2 +- .../semantics/runtime/release-interior_ok.c | 1 - .../semantics/runtime/release-twice_bug.c | 2 +- .../semantics/runtime/release-twice_ok.c | 1 - .../semantics/runtime/stack-cursor_bug.c | 2 +- .../semantics/runtime/stack-underflow_bug.c | 2 +- .../semantics/runtime/stack-vla-again_bug.c | 2 +- .../semantics/runtime/term-signed-sum_bug.c | 2 +- .../semantics/runtime/uaf-container_bug.c | 2 +- .../semantics/runtime/uaf-container_ok.c | 1 - test/cases/semantics/slots/fp-memcpy.c | 4 +- test/cases/semantics/slots/hook-freed-twice.c | 29 - .../semantics/slots/open-slot-callback.c | 17 - .../temporal/flag-selected-release.c | 1 - .../temporal/free-then-guarded-wrapper.c | 28 - .../temporal/identity-guarded-share.c | 1 - .../possible-rewrite-stays-possible.c | 57 - .../temporal/unknown-callee-resets-frame.c | 52 - .../temporal/unknown-effect-reaches-frame.c | 47 - .../semantics/unsafe/definite-violations.c | 19 - .../semantics/unsafe/possible-temporal.c | 13 - .../semantics/unsafe/raw-outside-region.c | 17 - .../unsafe/trusted-refines-nothing.c | 12 - test/cases/soundness/01_uaf_field_alias_bug.c | 2 +- test/cases/soundness/02_uaf_global_bug.c | 2 +- .../soundness/02b_uaf_global_direct_bug.c | 2 +- .../02c_uaf_global_free_in_helper_bug.c | 2 +- .../soundness/02d_uaf_heap_field_helper_bug.c | 2 +- .../soundness/02e_uaf_heap_field_direct_bug.c | 2 +- .../soundness/02f_uaf_param_read_helper_bug.c | 2 +- .../02g_uaf_heap_field_copied_helper_bug.c | 2 +- .../cases/soundness/03_double_free_loop_bug.c | 2 +- test/cases/soundness/03_double_free_loop_ok.c | 1 - .../04_iter_invalidation_realloc_bug.c | 2 +- .../soundness/05_oob_while_offbyone_bug.c | 2 +- .../soundness/06_int_overflow_malloc_bug.c | 2 +- .../07b_sprintf_stack_overflow_bug.c | 2 +- test/cases/soundness/08_uninit_index_bug.c | 4 +- .../soundness/09_type_confusion_voidp_bug.c | 2 +- test/cases/soundness/10_union_punning_bug.c | 2 +- .../soundness/11_dangling_outparam_bug.c | 5 +- test/cases/soundness/12_local_in_global_bug.c | 5 +- test/cases/soundness/13_uaf_callback_bug.c | 2 +- .../soundness/13b_uaf_callback_registry_bug.c | 2 +- .../soundness/13b_uaf_callback_registry_ok.c | 1 - .../soundness/14_unknown_extern_frees_bug.c | 2 +- .../soundness/14b_unknown_extern_buffer_bug.c | 3 +- .../soundness/14c_unknown_extern_param_bug.c | 2 - .../cases/soundness/15_data_race_thread_bug.c | 2 +- test/cases/soundness/15b_signal_handler_bug.c | 2 +- test/cases/soundness/16_setjmp_longjmp_bug.c | 2 +- .../soundness/16b_longjmp_dead_frame_bug.c | 14 - test/cases/soundness/17_variadic_misuse_bug.c | 2 +- test/cases/soundness/18_format_string_bug.c | 6 +- test/cases/soundness/19_vla_size_bug.c | 2 +- .../soundness/20_ptr_arith_past_end_bug.c | 2 +- test/cases/soundness/20b_ptr_walk_bug.c | 2 +- test/cases/soundness/21_memcpy_overlap_bug.c | 2 +- test/cases/soundness/22_realloc_zero_bug.c | 12 - test/cases/soundness/22b_realloc_alias_bug.c | 2 +- test/cases/soundness/23_alloca_bug.c | 4 +- test/cases/soundness/24_flexible_array_bug.c | 2 +- test/cases/soundness/25_strtok_static_bug.c | 2 +- .../soundness/25b_string_literal_write_bug.c | 5 +- test/cases/soundness/25c_static_result_bug.c | 22 - test/cases/soundness/26_container_uaf_ok.c | 1 - .../soundness/27_struct_return_dangling_bug.c | 5 +- test/cases/soundness/28_free_non_heap_bug.c | 2 +- test/cases/soundness/29_negative_index_bug.c | 2 +- .../soundness/30_unterminated_strlen_bug.c | 2 +- .../soundness/31_truncation_memcpy_bug.c | 2 +- .../cases/soundness/32_uninit_heap_read_bug.c | 4 +- .../33_double_free_aliased_records_bug.c | 2 +- .../34_goto_cleanup_double_free_bug.c | 2 +- .../cases/soundness/35_uninit_ptr_field_bug.c | 2 +- test/cases/soundness/36_heap_oob_param_bug.c | 2 +- .../37_exported_api_no_callers_bug.c | 13 - test/cases/soundness/38_extern_impl.c | 2 +- .../cases/soundness/38_lying_annotation_bug.c | 3 +- test/cases/soundness/39_false_assume_bug.c | 10 - test/cases/soundness/40_struct_copy_uaf_bug.c | 2 +- .../soundness/41b_late_iteration_uaf_bug.c | 2 +- .../cases/soundness/41c_deep_call_chain_bug.c | 2 +- test/cases/soundness/41d_many_branches_bug.c | 2 +- .../soundness/42_byte_copied_pointer_bug.c | 2 +- .../soundness/43_list_free_then_next_bug.c | 2 +- .../soundness/44_vector_element_ptr_bug.c | 2 +- .../soundness/45_unsafe_region_hides_bug.c | 3 +- .../cases/soundness/46_param_null_deref_bug.c | 2 +- .../cases/soundness/47_uaf_via_qsort_cb_bug.c | 2 +- .../49_negative_index_via_helper_bug.c | 2 +- .../soundness/50_param_passthrough_uaf_bug.c | 2 +- test/cases/soundness/README.md | 29 +- .../alias-cursor-several-arrays_bug.c | 2 +- .../alias-global-element-const_bug.c | 2 +- .../alias-global-element-index_bug.c | 2 +- .../soundness/alias-null-box-field_bug.c | 2 +- .../cases/soundness/alias-oob-box-field_bug.c | 2 +- .../alias-oob-replaced-buffer-direct_bug.c | 2 +- .../alias-oob-replaced-buffer-kept_bug.c | 2 +- .../soundness/alias-oob-replaced-buffer_bug.c | 2 +- .../soundness/alias-record-element_bug.c | 2 +- .../alias-uaf-alias-before-store_bug.c | 2 +- .../soundness/alias-uaf-heap-box-field_bug.c | 2 +- .../soundness/alias-uaf-heap-cell-alias_bug.c | 2 +- .../cases/soundness/alias-uaf-heap-cell_bug.c | 2 +- .../soundness/alias-uaf-stack-box-field_bug.c | 2 +- .../soundness/alias-uaf-stack-cell_bug.c | 2 +- .../c01_global_realloc_between_bug.c | 2 +- .../soundness/c02_param_global_alias_bug.c | 2 +- .../soundness/c03_contradictory_assume_bug.c | 8 - .../soundness/c04_union_tag_confusion_bug.c | 2 +- test/cases/soundness/c05_realloc_shrink_bug.c | 2 +- test/cases/soundness/c06_fam_sizeof_bug.c | 2 +- test/cases/soundness/c07_fnptr_cast_bug.c | 2 +- test/cases/soundness/c08_recursive_free_bug.c | 2 +- .../soundness/c09_strncpy_unterminated_bug.c | 2 +- .../c10_double_free_two_globals_bug.c | 2 +- .../c10b_double_free_two_params_bug.c | 2 +- .../c10c_double_free_two_fields_bug.c | 2 +- .../soundness/c10d_uaf_two_globals_bug.c | 2 +- .../soundness/c10e_global_and_param_bug.c | 2 +- .../soundness/c11_loop_40_iterations_bug.c | 2 +- .../soundness/c12_uninit_struct_field_bug.c | 2 +- .../c13_errno_style_extern_global_bug.c | 3 +- .../soundness/c14_memcpy_ptr_partial_bug.c | 10 +- .../soundness/conditional-arm-in-parens_bug.c | 2 +- test/corpus/README.md | 136 +- test/corpus/expected.json | 168 +- test/corpus/injections/injections.json | 2 +- test/corpus/manifest.json | 23 +- test/corpus/triage.json | 331 +- test/lit.cfg.py | 7 - tools/CMakeLists.txt | 1 - tools/weavec/CMakeLists.txt | 9 - tools/weavec/main.cpp | 515 --- unittests/Analysis/AnnotationsTest.cpp | 231 - unittests/Analysis/ArrayOwnershipTest.cpp | 821 ---- unittests/Analysis/AttributeReaderTest.cpp | 368 -- unittests/Analysis/CMakeLists.txt | 26 - unittests/Analysis/DynamicExtentsTest.cpp | 403 -- unittests/Analysis/EngineDecisionsTest.cpp | 633 --- unittests/Analysis/GuardCompletenessTest.cpp | 139 - unittests/Analysis/HeapStateTest.cpp | 711 --- unittests/Analysis/IntegerSemanticsTest.cpp | 623 --- unittests/Analysis/KindInferenceTest.cpp | 658 --- unittests/Analysis/LedgerAdapterTest.cpp | 449 -- unittests/Analysis/LoopRequirementsTest.cpp | 270 -- unittests/Analysis/NumericInputsTest.cpp | 245 -- unittests/Analysis/PointerIdentityTest.cpp | 548 --- unittests/Analysis/PointerValidityTest.cpp | 592 --- unittests/Analysis/ProgramDatabaseTest.cpp | 213 - unittests/Analysis/ResourceLifecycleTest.cpp | 650 --- unittests/Analysis/SharedOwnershipTest.cpp | 134 - unittests/Analysis/SiteCollectorTest.cpp | 391 -- unittests/Analysis/SiteTestUtils.h | 173 - unittests/Analysis/SlotCollectorTest.cpp | 343 -- unittests/Analysis/SoundDefaultsTest.cpp | 436 -- unittests/Analysis/SpatialSafetyTest.cpp | 771 ---- unittests/Analysis/StringsAndFieldsTest.cpp | 406 -- unittests/Analysis/TestUtils.h | 372 -- unittests/Analysis/ValueConditionalTest.cpp | 429 -- unittests/CMakeLists.txt | 1 - unittests/Core/CMakeLists.txt | 11 +- unittests/Core/DiagnosticTest.cpp | 130 - unittests/Core/EffectsTest.cpp | 176 - unittests/Core/FnSlotsTest.cpp | 430 -- unittests/Core/HeapTest.cpp | 1199 ----- unittests/Core/IntegerTest.cpp | 664 --- unittests/Core/LedgerTest.cpp | 330 -- unittests/Core/LibrarySpecTest.cpp | 1811 +++----- unittests/Core/PointerKindTest.cpp | 204 - unittests/Frontend/AnalysisSummaryTest.cpp | 126 - unittests/Frontend/CMakeLists.txt | 10 +- unittests/Frontend/DiagnosticControlTest.cpp | 362 -- unittests/Frontend/InterfaceFactsTest.cpp | 101 - unittests/Frontend/ProgramAnalysisTest.cpp | 691 --- unittests/Frontend/ProgramChecksTest.cpp | 192 - unittests/Frontend/RetainedUnitTest.cpp | 103 - 828 files changed, 7763 insertions(+), 85518 deletions(-) delete mode 100644 docs/data/diagnostic-remedies.json delete mode 100644 docs/examples/fixed.c delete mode 100644 docs/examples/use-after-free.c delete mode 100644 docs/pages/guides/ownership.md delete mode 100644 docs/pages/guides/whole-program.md create mode 100644 docs/rfcs/0036-deterministic-heap-reuse.md delete mode 100644 include/weavec/Analysis/Annotations.h delete mode 100644 include/weavec/Analysis/AttributeReader.h delete mode 100644 include/weavec/Analysis/BoundaryInvariants.h delete mode 100644 include/weavec/Analysis/BypassedDeclarations.h delete mode 100644 include/weavec/Analysis/ClangLocation.h delete mode 100644 include/weavec/Analysis/Concurrency.h delete mode 100644 include/weavec/Analysis/KindInference.h delete mode 100644 include/weavec/Analysis/KindTable.h delete mode 100644 include/weavec/Analysis/LedgerAdapter.h delete mode 100644 include/weavec/Analysis/ObjectEngine.h delete mode 100644 include/weavec/Analysis/ProgramDatabase.h delete mode 100644 include/weavec/Analysis/SafetyEngine.h delete mode 100644 include/weavec/Analysis/SiteCollector.h delete mode 100644 include/weavec/Analysis/SlotCollector.h delete mode 100644 include/weavec/Analysis/SourceTerm.h delete mode 100644 include/weavec/Analysis/UnitPipeline.h delete mode 100644 include/weavec/Core/AnalysisStats.h delete mode 100644 include/weavec/Core/Core.h delete mode 100644 include/weavec/Core/Diagnostic.h delete mode 100644 include/weavec/Core/Effects.h delete mode 100644 include/weavec/Core/FnSlots.h delete mode 100644 include/weavec/Core/Heap.h delete mode 100644 include/weavec/Core/Integer.h delete mode 100644 include/weavec/Core/Ledger.h delete mode 100644 include/weavec/Core/Path.h delete mode 100644 include/weavec/Core/Persistent.h delete mode 100644 include/weavec/Core/PointerKind.h delete mode 100644 include/weavec/Core/Scc.h delete mode 100644 include/weavec/Core/SourceLocation.h delete mode 100644 include/weavec/Core/Zone.h delete mode 100644 include/weavec/Frontend/AnalysisStats.h delete mode 100644 include/weavec/Frontend/AnalysisSummary.h delete mode 100644 include/weavec/Frontend/ClangDiagnosticSink.h delete mode 100644 include/weavec/Frontend/DiagnosticControl.h delete mode 100644 include/weavec/Frontend/FrontendAction.h delete mode 100644 include/weavec/Frontend/InterfaceFacts.h delete mode 100644 include/weavec/Frontend/ProgramAnalysis.h delete mode 100644 include/weavec/Frontend/ProgramChecks.h delete mode 100644 lib/Analysis/Annotations.cpp delete mode 100644 lib/Analysis/AttributeReader.cpp delete mode 100644 lib/Analysis/BoundaryInvariants.cpp delete mode 100644 lib/Analysis/BypassedDeclarations.cpp delete mode 100644 lib/Analysis/CMakeLists.txt delete mode 100644 lib/Analysis/ClangLocation.cpp delete mode 100644 lib/Analysis/Concurrency.cpp delete mode 100644 lib/Analysis/Engine.h delete mode 100644 lib/Analysis/EngineAnnotations.cpp delete mode 100644 lib/Analysis/EngineCalls.cpp delete mode 100644 lib/Analysis/EngineContexts.cpp delete mode 100644 lib/Analysis/EngineDecide.cpp delete mode 100644 lib/Analysis/EngineExpr.cpp delete mode 100644 lib/Analysis/EngineIntegers.h delete mode 100644 lib/Analysis/EngineInvariants.cpp delete mode 100644 lib/Analysis/EngineKinds.cpp delete mode 100644 lib/Analysis/EngineLibrary.cpp delete mode 100644 lib/Analysis/EngineLifetimes.cpp delete mode 100644 lib/Analysis/EngineRun.cpp delete mode 100644 lib/Analysis/EngineStrings.cpp delete mode 100644 lib/Analysis/EngineSummary.cpp delete mode 100644 lib/Analysis/EngineUnit.cpp delete mode 100644 lib/Analysis/KindInference.cpp delete mode 100644 lib/Analysis/KindInferenceCollect.cpp delete mode 100644 lib/Analysis/KindInferenceDump.cpp delete mode 100644 lib/Analysis/KindInferenceFields.cpp delete mode 100644 lib/Analysis/KindInferenceImpl.h delete mode 100644 lib/Analysis/KindInferenceMustAccess.cpp delete mode 100644 lib/Analysis/KindTable.cpp delete mode 100644 lib/Analysis/LedgerAdapter.cpp delete mode 100644 lib/Analysis/ProgramDatabase.cpp delete mode 100644 lib/Analysis/SafetyEngine.cpp delete mode 100644 lib/Analysis/SiteCollector.cpp delete mode 100644 lib/Analysis/SlotCollector.cpp delete mode 100644 lib/Analysis/SourceTerm.cpp delete mode 100644 lib/Analysis/UnitPipeline.cpp delete mode 100644 lib/Core/Diagnostic.cpp delete mode 100644 lib/Core/Effects.cpp delete mode 100644 lib/Core/FnSlots.cpp delete mode 100644 lib/Core/Heap.cpp delete mode 100644 lib/Core/Integer.cpp delete mode 100644 lib/Core/Ledger.cpp delete mode 100644 lib/Core/Path.cpp delete mode 100644 lib/Core/PointerKind.cpp delete mode 100644 lib/Core/Scc.cpp delete mode 100644 lib/Core/SourceLocation.cpp delete mode 100644 lib/Core/Zone.cpp delete mode 100644 lib/Frontend/AnalysisStats.cpp delete mode 100644 lib/Frontend/AnalysisSummary.cpp delete mode 100644 lib/Frontend/ClangDiagnosticSink.cpp delete mode 100644 lib/Frontend/DiagnosticControl.cpp delete mode 100644 lib/Frontend/FrontendAction.cpp delete mode 100644 lib/Frontend/InterfaceFacts.cpp delete mode 100644 lib/Frontend/ProgramAnalysis.cpp delete mode 100644 lib/Frontend/ProgramChecks.cpp create mode 100644 runtime/weavec_asan.c create mode 100644 runtime/weavec_sweep.c delete mode 100644 test/Analysis/analysis-stats.c delete mode 100644 test/Analysis/rfc0002-borrows.c delete mode 100644 test/Analysis/rfc0004-function-pointers.c delete mode 100644 test/Analysis/rfc0004-posix.c delete mode 100644 test/Analysis/rfc0004-raw.c delete mode 100644 test/Analysis/rfc0004-unsafe-regions.c delete mode 100644 test/Analysis/rfc0006-conditions.c delete mode 100644 test/Analysis/rfc0006-outcomes.c delete mode 100644 test/Analysis/rfc0006-written.c delete mode 100644 test/Analysis/rfc0007-clean.c delete mode 100644 test/Analysis/rfc0007-families.c delete mode 100644 test/Analysis/rfc0008-null.c delete mode 100644 test/Analysis/rfc0008-replaced.c delete mode 100644 test/Analysis/rfc0008-uninit.c delete mode 100644 test/Analysis/rfc0009-arguments.c delete mode 100644 test/Analysis/rfc0009-noreturn.c delete mode 100644 test/Analysis/rfc0009-replaced.c delete mode 100644 test/Analysis/rfc0009-scalars.c delete mode 100644 test/Analysis/rfc0010-escapes.c delete mode 100644 test/Analysis/rfc0010-outcomes.c delete mode 100644 test/Analysis/rfc0010-refcount.c delete mode 100644 test/Analysis/rfc0011-bounds.c delete mode 100644 test/Analysis/rfc0011-derived.c delete mode 100644 test/Analysis/rfc0011-requirements.c delete mode 100644 test/Analysis/rfc0012-assume.c delete mode 100644 test/Analysis/rfc0012-relations.c delete mode 100644 test/Analysis/rfc0012-strings.c delete mode 100644 test/Analysis/rfc0013-heap.c delete mode 100644 test/Analysis/rfc0015-limits.c delete mode 100644 test/Analysis/rfc0016-clean.c delete mode 100644 test/Analysis/rfc0017-integer-diagnostics.c delete mode 100644 test/Analysis/rfc0017-target-and-unsafe.c delete mode 100644 test/Analysis/rfc0030-assume.c delete mode 100644 test/Analysis/rfc0030-budget.c delete mode 100644 test/Analysis/rfc0030-certainty.c delete mode 100644 test/Analysis/rfc0030-declared-kinds.c delete mode 100644 test/Analysis/rfc0030-library.c delete mode 100644 test/Analysis/rfc0030-literal-write.c delete mode 100644 test/Analysis/rfc0030-owner-uniqueness.c delete mode 100644 test/Analysis/rfc0030-reallocator-outcomes.c delete mode 100644 test/Analysis/rfc0030-slot-candidate-classes.c delete mode 100644 test/Analysis/rfc0030-sound-defaults.c delete mode 100644 test/Annotations/Inputs/kinds-lstate.h delete mode 100644 test/Annotations/Inputs/vendor-kinds.h delete mode 100644 test/Annotations/Inputs/vendor.h delete mode 100644 test/Annotations/kinds-alloc-size.c delete mode 100644 test/Annotations/kinds-array-parameters.c delete mode 100644 test/Annotations/kinds-count-attributes.c delete mode 100644 test/Annotations/kinds-counted-by.c delete mode 100644 test/Annotations/kinds-defaults.c delete mode 100644 test/Annotations/kinds-ended-by.c delete mode 100644 test/Annotations/kinds-fields.c delete mode 100644 test/Annotations/kinds-fn-slots.c delete mode 100644 test/Annotations/kinds-must-access.c delete mode 100644 test/Annotations/kinds-nonnull-attributes.c delete mode 100644 test/Annotations/kinds-nullability.c delete mode 100644 test/Annotations/kinds-ownership-attributes.c delete mode 100644 test/Annotations/kinds-precedence.c delete mode 100644 test/Annotations/kinds-slots.c delete mode 100644 test/Annotations/kinds-string.c delete mode 100644 test/Annotations/ownership-annotations.c delete mode 100644 test/Annotations/rfc0003-unknown-extern.c delete mode 100644 test/Annotations/rfc0010-annotations.c delete mode 100644 test/Annotations/rfc0011-sized-by.c delete mode 100644 test/Driver/Inputs/buggy-header.h create mode 100644 test/Driver/Inputs/image-globals-lib.c delete mode 100644 test/Driver/Inputs/rfc0014-callback-client.c delete mode 100644 test/Driver/Inputs/rfc0030-hook-state.c delete mode 100644 test/Driver/Inputs/rfc0030-hook-state.h delete mode 100644 test/Driver/Inputs/rfc0030-inspect.c create mode 100644 test/Driver/asan-compat-fortify.c create mode 100644 test/Driver/asan-compat.c delete mode 100644 test/Driver/compilation-database-p.c delete mode 100644 test/Driver/diagnostics-format-sarif.c delete mode 100644 test/Driver/dump-analysis.c create mode 100644 test/Driver/flags.c create mode 100644 test/Driver/fortify.c delete mode 100644 test/Driver/headers-analysed.c create mode 100644 test/Driver/image-globals.c delete mode 100644 test/Driver/leak-default.c delete mode 100644 test/Driver/rfc0005-flags.c delete mode 100644 test/Driver/rfc0007-flags.c delete mode 100644 test/Driver/rfc0008-flags.c delete mode 100644 test/Driver/rfc0014-callback-link.c delete mode 100644 test/Driver/rfc0030-flags.c delete mode 100644 test/Driver/rfc0030-link-declarations.c delete mode 100644 test/Driver/rfc0030-link-slots.c delete mode 100644 test/Driver/system-headers.c create mode 100644 test/Guards/alloca-entry.c create mode 100644 test/Guards/intra-object.c create mode 100644 test/Guards/shared-frame.c create mode 100644 test/Guards/use-after-poison.c delete mode 100644 test/WholeProgram/Inputs/array15.c delete mode 100644 test/WholeProgram/Inputs/array15.h delete mode 100644 test/WholeProgram/Inputs/buffers.c delete mode 100644 test/WholeProgram/Inputs/buffers.h delete mode 100644 test/WholeProgram/Inputs/counted.c delete mode 100644 test/WholeProgram/Inputs/counted.h delete mode 100644 test/WholeProgram/Inputs/die.c delete mode 100644 test/WholeProgram/Inputs/grow.c delete mode 100644 test/WholeProgram/Inputs/handle.c delete mode 100644 test/WholeProgram/Inputs/handle.h delete mode 100644 test/WholeProgram/Inputs/handlers.c delete mode 100644 test/WholeProgram/Inputs/heap13.c delete mode 100644 test/WholeProgram/Inputs/heap13.h delete mode 100644 test/WholeProgram/Inputs/node.c delete mode 100644 test/WholeProgram/Inputs/node.h delete mode 100644 test/WholeProgram/Inputs/rfc0016-callee.c delete mode 100644 test/WholeProgram/Inputs/rfc0017-numeric.c delete mode 100644 test/WholeProgram/Inputs/rfc0030-first.c delete mode 100644 test/WholeProgram/Inputs/validity.c delete mode 100644 test/WholeProgram/Inputs/validity.h delete mode 100644 test/WholeProgram/Inputs/vec.c delete mode 100644 test/WholeProgram/Inputs/vec.h delete mode 100644 test/WholeProgram/program-summary.c delete mode 100644 test/WholeProgram/retention.c delete mode 100644 test/WholeProgram/rfc0005-boundary-once.c delete mode 100644 test/WholeProgram/rfc0005-callbacks.c delete mode 100644 test/WholeProgram/rfc0005-cross-unit.c delete mode 100644 test/WholeProgram/rfc0005-dump.c delete mode 100644 test/WholeProgram/rfc0006-outcomes.c delete mode 100644 test/WholeProgram/rfc0007-families.c delete mode 100644 test/WholeProgram/rfc0008-validity.c delete mode 100644 test/WholeProgram/rfc0009-noreturn-units.c delete mode 100644 test/WholeProgram/rfc0010-shares.c delete mode 100644 test/WholeProgram/rfc0011-extents.c delete mode 100644 test/WholeProgram/rfc0012-sized-fields.c delete mode 100644 test/WholeProgram/rfc0013-heap.c delete mode 100644 test/WholeProgram/rfc0013-lazy.c delete mode 100644 test/WholeProgram/rfc0013-swap.c delete mode 100644 test/WholeProgram/rfc0015-arrays.c delete mode 100644 test/WholeProgram/rfc0016-composition.c delete mode 100644 test/WholeProgram/rfc0017-numeric.c create mode 100644 test/cases/determinism/README.md create mode 100644 test/cases/determinism/asan-pool_bug.c create mode 100644 test/cases/determinism/asan-pool_ok.c create mode 100644 test/cases/determinism/double-free-after-churn_bug.c create mode 100644 test/cases/determinism/double-free-after-churn_ok.c create mode 100644 test/cases/determinism/escaping-oob_bug.c create mode 100644 test/cases/determinism/escaping-oob_ok.c create mode 100644 test/cases/determinism/field-strcpy_bug.c create mode 100644 test/cases/determinism/field-strcpy_ok.c create mode 100644 test/cases/determinism/global-jump_bug.c create mode 100644 test/cases/determinism/global-jump_ok.c create mode 100644 test/cases/determinism/heap-jump_bug.c create mode 100644 test/cases/determinism/heap-jump_ok.c create mode 100644 test/cases/determinism/shared-frame_ok.c create mode 100644 test/cases/determinism/uaf-after-churn_bug.c create mode 100644 test/cases/determinism/uaf-after-churn_ok.c create mode 100644 test/cases/determinism/uaf-spray_bug.c create mode 100644 test/cases/determinism/uaf-spray_ok.c create mode 100644 test/cases/determinism/use-after-longjmp_bug.c create mode 100644 test/cases/determinism/use-after-longjmp_ok.c create mode 100644 test/cases/determinism/use-after-return_bug.c create mode 100644 test/cases/determinism/use-after-return_ok.c delete mode 100644 test/cases/engine/Analysis-branches.c delete mode 100644 test/cases/engine/Analysis-clean.c delete mode 100644 test/cases/engine/Analysis-double-free.c delete mode 100644 test/cases/engine/Analysis-rfc0002-aliases.c delete mode 100644 test/cases/engine/Analysis-rfc0002-lifetimes.c delete mode 100644 test/cases/engine/Analysis-rfc0002-moves.c delete mode 100644 test/cases/engine/Analysis-rfc0002-paths.c delete mode 100644 test/cases/engine/Analysis-rfc0002-places.c delete mode 100644 test/cases/engine/Analysis-rfc0002-realloc.c delete mode 100644 test/cases/engine/Analysis-rfc0003-escapes.c delete mode 100644 test/cases/engine/Analysis-rfc0003-globals.c delete mode 100644 test/cases/engine/Analysis-rfc0003-libc.c delete mode 100644 test/cases/engine/Analysis-rfc0003-paths.c delete mode 100644 test/cases/engine/Analysis-rfc0003-wrappers.c delete mode 100644 test/cases/engine/Analysis-rfc0004-laundering.c delete mode 100644 test/cases/engine/Analysis-rfc0006-elements.c delete mode 100644 test/cases/engine/Analysis-rfc0006-nll.c delete mode 100644 test/cases/engine/Analysis-rfc0007-leaks.c delete mode 100644 test/cases/engine/Analysis-rfc0007-owned-fields.c delete mode 100644 test/cases/engine/Analysis-rfc0008-release.c delete mode 100644 test/cases/engine/Analysis-rfc0011-lifetimes.c delete mode 100644 test/cases/engine/Analysis-rfc0012-sized-fields.c delete mode 100644 test/cases/engine/Analysis-rfc0013-clean.c delete mode 100644 test/cases/engine/Analysis-rfc0014-clean.c delete mode 100644 test/cases/engine/Analysis-rfc0014-pointer-identity.c delete mode 100644 test/cases/engine/Analysis-rfc0015-elements.c delete mode 100644 test/cases/engine/Analysis-rfc0015-ranges.c delete mode 100644 test/cases/engine/Analysis-rfc0016-boundaries.c delete mode 100644 test/cases/engine/Analysis-rfc0016-composition.c delete mode 100644 test/cases/engine/Analysis-use-after-free.c delete mode 100644 test/cases/engine/Annotations-invalid-annotation.c delete mode 100644 test/cases/engine/Annotations-rfc0003-annotation-mismatch.c delete mode 100644 test/cases/engine/Annotations-unsafe.c delete mode 100644 test/cases/engine/Inputs/cycle-b.c delete mode 100644 test/cases/engine/Inputs/heap.c delete mode 100644 test/cases/engine/Inputs/heap.h delete mode 100644 test/cases/engine/Inputs/heap13.c delete mode 100644 test/cases/engine/Inputs/heap13.h delete mode 100644 test/cases/engine/Inputs/prelude.h delete mode 100644 test/cases/engine/WholeProgram-rfc0005-cycle.c delete mode 100644 test/cases/engine/WholeProgram-rfc0013-input-identity.c delete mode 100644 test/cases/evaluation/helper-alias.c delete mode 100644 test/cases/evaluation/helper-good.c delete mode 100644 test/cases/evaluation/helper-leak.c delete mode 100644 test/cases/evaluation/inline-alias.c delete mode 100644 test/cases/evaluation/inline-good.c delete mode 100644 test/cases/evaluation/inline-leak.c delete mode 100644 test/cases/evaluation/rfc0014-callback-bug.c delete mode 100644 test/cases/evaluation/rfc0014-callback-good.c delete mode 100644 test/cases/evaluation/rfc0014-copied-pointer-bug.c delete mode 100644 test/cases/evaluation/rfc0014-copied-pointer-good.c delete mode 100644 test/cases/evaluation/rfc0014-pointer-guard-bug.c delete mode 100644 test/cases/evaluation/rfc0014-pointer-guard-good.c delete mode 100644 test/cases/evaluation/rfc0014-whole-callback-bug.c delete mode 100644 test/cases/evaluation/rfc0014-whole-callback-good.c delete mode 100644 test/cases/evaluation/rfc0015-array-cleanup-bug.c delete mode 100644 test/cases/evaluation/rfc0015-array-cleanup-good.c delete mode 100644 test/cases/evaluation/rfc0015-array-compacted-bug.c delete mode 100644 test/cases/evaluation/rfc0015-array-compacted-good.c delete mode 100644 test/cases/evaluation/rfc0015-array-copy-bug.c delete mode 100644 test/cases/evaluation/rfc0015-array-copy-good.c delete mode 100644 test/cases/evaluation/rfc0015-array-fill-bug.c delete mode 100644 test/cases/evaluation/rfc0015-array-fill-good.c delete mode 100644 test/cases/evaluation/rfc0015-array-history-bug.c delete mode 100644 test/cases/evaluation/rfc0015-array-history-good.c delete mode 100644 test/cases/evaluation/rfc0015-array-index-bug.c delete mode 100644 test/cases/evaluation/rfc0015-array-index-good.c delete mode 100644 test/cases/evaluation/rfc0015-array-initialization-good.c delete mode 100644 test/cases/evaluation/rfc0015-array-memmove-bug.c delete mode 100644 test/cases/evaluation/rfc0015-array-memmove-good.c delete mode 100644 test/cases/evaluation/rfc0015-array-record-bug.c delete mode 100644 test/cases/evaluation/rfc0015-array-record-good.c delete mode 100644 test/cases/evaluation/rfc0015-array-resized-bug.c delete mode 100644 test/cases/evaluation/rfc0015-array-resized-good.c delete mode 100644 test/cases/evaluation/rfc0015-array-returned-bug.c delete mode 100644 test/cases/evaluation/rfc0015-array-returned-good.c delete mode 100644 test/cases/evaluation/rfc0015-array-symbolic-bug.c delete mode 100644 test/cases/evaluation/rfc0015-array-symbolic-good.c delete mode 100644 test/cases/evaluation/rfc0016-array-bad.c delete mode 100644 test/cases/evaluation/rfc0016-array-good.c delete mode 100644 test/cases/evaluation/rfc0016-callback-bad.c delete mode 100644 test/cases/evaluation/rfc0016-callback-good.c delete mode 100644 test/cases/evaluation/rfc0016-cross-bad.c delete mode 100644 test/cases/evaluation/rfc0016-cross-good.c delete mode 100644 test/cases/evaluation/rfc0016-double-bad.c delete mode 100644 test/cases/evaluation/rfc0016-double-good.c delete mode 100644 test/cases/evaluation/rfc0016-global-bad.c delete mode 100644 test/cases/evaluation/rfc0016-global-good.c delete mode 100644 test/cases/evaluation/rfc0016-guard-bad.c delete mode 100644 test/cases/evaluation/rfc0016-guard-good.c delete mode 100644 test/cases/evaluation/rfc0016-inline-bad.c delete mode 100644 test/cases/evaluation/rfc0016-inline-good.c delete mode 100644 test/cases/evaluation/rfc0016-interior-bad.c delete mode 100644 test/cases/evaluation/rfc0016-interior-good.c delete mode 100644 test/cases/evaluation/rfc0016-nested-bad.c delete mode 100644 test/cases/evaluation/rfc0016-nested-good.c delete mode 100644 test/cases/evaluation/rfc0016-order-bad.c delete mode 100644 test/cases/evaluation/rfc0016-order-good.c delete mode 100644 test/cases/evaluation/rfc0016-outputs-bad.c delete mode 100644 test/cases/evaluation/rfc0016-outputs-good.c delete mode 100644 test/cases/evaluation/rfc0016-replacement-bad.c delete mode 100644 test/cases/evaluation/rfc0016-replacement-good.c delete mode 100644 test/cases/evaluation/shared-child.c delete mode 100644 test/cases/evaluation/snapshot-good.c delete mode 100644 test/cases/evaluation/whole-program-alias.c delete mode 100644 test/cases/evaluation/whole-program-good.c delete mode 100644 test/cases/evaluation/whole-program-leak.c delete mode 100644 test/cases/pairs/bool-bug.c delete mode 100644 test/cases/pairs/bool-good.c delete mode 100644 test/cases/pairs/checked-builtin-bug.c delete mode 100644 test/cases/pairs/checked-builtin-good.c delete mode 100644 test/cases/pairs/fam-good.c delete mode 100644 test/cases/pairs/helper-return-good.c delete mode 100644 test/cases/pairs/memcpy-wrapped-zero-bug.c delete mode 100644 test/cases/pairs/memcpy-wrapped-zero-good.c delete mode 100644 test/cases/pairs/min-bound-good.c delete mode 100644 test/cases/pairs/mixed-signed-bug.c delete mode 100644 test/cases/pairs/mixed-signed-good.c delete mode 100644 test/cases/pairs/narrowing-bug.c delete mode 100644 test/cases/pairs/narrowing-good.c delete mode 100644 test/cases/pairs/outparam-good.c delete mode 100644 test/cases/pairs/product-good.c delete mode 100644 test/cases/pairs/unsigned-wrap-good.c delete mode 100644 test/cases/pairs/vla-snapshot-good.c delete mode 100644 test/cases/proofs/attached-payload-ignored.c delete mode 100644 test/cases/proofs/attached-payload-leak.c delete mode 100644 test/cases/proofs/reader-uninitialized.c delete mode 100644 test/cases/recall/CWE-401/leak_in_callee.c delete mode 100644 test/cases/recall/CWE-401/leak_on_return.c delete mode 100644 test/cases/recall/CWE-401/leak_overwritten.c delete mode 100644 test/cases/recall/CWE-415/double_free.c delete mode 100644 test/cases/recall/CWE-415/double_free_alias.c delete mode 100644 test/cases/recall/CWE-416/use_after_free.c delete mode 100644 test/cases/recall/CWE-416/use_after_free_alias.c delete mode 100644 test/cases/recall/CWE-476/null_from_callee.c delete mode 100644 test/cases/semantics/Inputs/rfc0030.h delete mode 100644 test/cases/semantics/aliasing/owning-fields-distinct.c delete mode 100644 test/cases/semantics/assume/assumed-after-site.c delete mode 100644 test/cases/semantics/assume/contradicted.c delete mode 100644 test/cases/semantics/assume/proven.c delete mode 100644 test/cases/semantics/boundary/global-written-by-unknown-code.c delete mode 100644 test/cases/semantics/confirm/munmap-whole-twice.c delete mode 100644 test/cases/semantics/confirm/shared-out-params.c delete mode 100644 test/cases/semantics/dropin/memset-callee_bug.c delete mode 100644 test/cases/semantics/dropin/raw-declared_bug.c delete mode 100644 test/cases/semantics/examples/assumption-checked.c delete mode 100644 test/cases/semantics/examples/assumption-contradicted.c delete mode 100644 test/cases/semantics/examples/proven-static-extent.c delete mode 100644 test/cases/semantics/examples/trusted-system-api.c delete mode 100644 test/cases/semantics/examples/trusted-unsafe.c delete mode 100644 test/cases/semantics/examples/unresolved-unknown-callee.c delete mode 100644 test/cases/semantics/examples/unresolved-unknown-extent.c delete mode 100644 test/cases/semantics/extents/null-after-checked-access.c delete mode 100644 test/cases/semantics/kinds/attr-ownership.c delete mode 100644 test/cases/semantics/kinds/declared-ended-by.c delete mode 100644 test/cases/semantics/kinds/declared-invalid-name.c delete mode 100644 test/cases/semantics/kinds/declared-nonnull-definite.c delete mode 100644 test/cases/semantics/kinds/declared-static-array.c delete mode 100644 test/cases/semantics/ledger/budget.c delete mode 100644 test/cases/semantics/ledger/extern-contract.c delete mode 100644 test/cases/semantics/ledger/inline-asm.c delete mode 100644 test/cases/semantics/ledger/no-site-unevaluated.c delete mode 100644 test/cases/semantics/ledger/nonnull-destructor.c delete mode 100644 test/cases/semantics/ledger/setjmp.c delete mode 100644 test/cases/semantics/ledger/system-api-borrow.c delete mode 100644 test/cases/semantics/ledger/unknown-then-free.c delete mode 100644 test/cases/semantics/library/fill-shared-unknown-object.c delete mode 100644 test/cases/semantics/library/library-filled-result.c delete mode 100644 test/cases/semantics/library/realloc-zero-definite.c delete mode 100644 test/cases/semantics/library/releasers.c delete mode 100644 test/cases/semantics/library/stdout-closed-twice.c delete mode 100644 test/cases/semantics/library/wrapper-param-keyed.c delete mode 100644 test/cases/semantics/slots/hook-freed-twice.c delete mode 100644 test/cases/semantics/slots/open-slot-callback.c delete mode 100644 test/cases/semantics/temporal/free-then-guarded-wrapper.c delete mode 100644 test/cases/semantics/temporal/possible-rewrite-stays-possible.c delete mode 100644 test/cases/semantics/temporal/unknown-callee-resets-frame.c delete mode 100644 test/cases/semantics/temporal/unknown-effect-reaches-frame.c delete mode 100644 test/cases/semantics/unsafe/definite-violations.c delete mode 100644 test/cases/semantics/unsafe/possible-temporal.c delete mode 100644 test/cases/semantics/unsafe/raw-outside-region.c delete mode 100644 test/cases/semantics/unsafe/trusted-refines-nothing.c delete mode 100644 test/cases/soundness/14c_unknown_extern_param_bug.c delete mode 100644 test/cases/soundness/16b_longjmp_dead_frame_bug.c delete mode 100644 test/cases/soundness/22_realloc_zero_bug.c delete mode 100644 test/cases/soundness/25c_static_result_bug.c delete mode 100644 test/cases/soundness/37_exported_api_no_callers_bug.c delete mode 100644 test/cases/soundness/39_false_assume_bug.c delete mode 100644 test/cases/soundness/c03_contradictory_assume_bug.c delete mode 100644 tools/weavec/CMakeLists.txt delete mode 100644 tools/weavec/main.cpp delete mode 100644 unittests/Analysis/AnnotationsTest.cpp delete mode 100644 unittests/Analysis/ArrayOwnershipTest.cpp delete mode 100644 unittests/Analysis/AttributeReaderTest.cpp delete mode 100644 unittests/Analysis/CMakeLists.txt delete mode 100644 unittests/Analysis/DynamicExtentsTest.cpp delete mode 100644 unittests/Analysis/EngineDecisionsTest.cpp delete mode 100644 unittests/Analysis/GuardCompletenessTest.cpp delete mode 100644 unittests/Analysis/HeapStateTest.cpp delete mode 100644 unittests/Analysis/IntegerSemanticsTest.cpp delete mode 100644 unittests/Analysis/KindInferenceTest.cpp delete mode 100644 unittests/Analysis/LedgerAdapterTest.cpp delete mode 100644 unittests/Analysis/LoopRequirementsTest.cpp delete mode 100644 unittests/Analysis/NumericInputsTest.cpp delete mode 100644 unittests/Analysis/PointerIdentityTest.cpp delete mode 100644 unittests/Analysis/PointerValidityTest.cpp delete mode 100644 unittests/Analysis/ProgramDatabaseTest.cpp delete mode 100644 unittests/Analysis/ResourceLifecycleTest.cpp delete mode 100644 unittests/Analysis/SharedOwnershipTest.cpp delete mode 100644 unittests/Analysis/SiteCollectorTest.cpp delete mode 100644 unittests/Analysis/SiteTestUtils.h delete mode 100644 unittests/Analysis/SlotCollectorTest.cpp delete mode 100644 unittests/Analysis/SoundDefaultsTest.cpp delete mode 100644 unittests/Analysis/SpatialSafetyTest.cpp delete mode 100644 unittests/Analysis/StringsAndFieldsTest.cpp delete mode 100644 unittests/Analysis/TestUtils.h delete mode 100644 unittests/Analysis/ValueConditionalTest.cpp delete mode 100644 unittests/Core/DiagnosticTest.cpp delete mode 100644 unittests/Core/EffectsTest.cpp delete mode 100644 unittests/Core/FnSlotsTest.cpp delete mode 100644 unittests/Core/HeapTest.cpp delete mode 100644 unittests/Core/IntegerTest.cpp delete mode 100644 unittests/Core/LedgerTest.cpp delete mode 100644 unittests/Core/PointerKindTest.cpp delete mode 100644 unittests/Frontend/AnalysisSummaryTest.cpp delete mode 100644 unittests/Frontend/DiagnosticControlTest.cpp delete mode 100644 unittests/Frontend/InterfaceFactsTest.cpp delete mode 100644 unittests/Frontend/ProgramAnalysisTest.cpp delete mode 100644 unittests/Frontend/ProgramChecksTest.cpp delete mode 100644 unittests/Frontend/RetainedUnitTest.cpp diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 83620fc3..7ff1839d 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,5 +1,5 @@ name: Bug report -description: WeaveC crashed, rejected valid code, or accepted unsafe code +description: weavec-cc crashed, trapped on a correct program, or let a memory bug through title: "[bug]: " labels: [bug, needs-triage] body: @@ -8,10 +8,12 @@ body: attributes: label: Kind of bug options: - - False positive (safe code rejected) - - False negative (unsafe code accepted) - - Crash / assertion failure - - Wrong or unhelpful diagnostic + - False trap (a correct program stopped) + - Missed bug (an unsafe access ran) + - Crash / assertion failure in weavec-cc + - Wrong or unhelpful report + - Project fails to build with weavec-cc (drop-in) + - Performance (run time, build time, memory) - Build or installation problem - Other validations: @@ -22,8 +24,9 @@ body: attributes: label: Minimal reproducer description: > - The smallest C program that shows the problem, plus the exact `weavec` - command line. Please avoid system headers where possible. + The smallest C program that shows the problem, plus the exact + `weavec-cc` command line and how you ran the program. Please avoid + system headers where possible. render: c validations: required: true @@ -32,7 +35,10 @@ body: id: actual attributes: label: Actual behaviour - description: Full diagnostic output or crash backtrace. + description: > + The full report (`weavec: at ...`), compiler output or crash + backtrace. `WEAVEC_RT_BACKTRACE=1` adds the stack of the access that + stopped the program. render: text validations: required: true @@ -47,8 +53,9 @@ body: - type: input id: version attributes: - label: weavec --version - placeholder: "weavec version 0.1.0-dev (abc123def456), built with LLVM 23.1.0" + label: weavec-cc --version + description: The `weavec-cc version` lines at the end of `weavec-cc --version`. + placeholder: "weavec-cc version 0.16.0 (abc123def456), built with LLVM 23.1.0" validations: required: true diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 8d85d6ad..183639a8 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -1,5 +1,5 @@ name: Feature request -description: Propose an analysis, annotation, diagnostic, or tooling improvement +description: Propose a guard, runtime, driver, or tooling improvement title: "[feature]: " labels: [enhancement, needs-triage] body: @@ -16,8 +16,8 @@ body: attributes: label: Proposal description: > - How should WeaveC behave? Include example C code and the diagnostics or - inference results you would expect. + How should WeaveC behave? Include example C code and what you would + expect `weavec-cc` and the program it builds to do. validations: required: true @@ -31,8 +31,8 @@ body: attributes: label: Affected areas options: - - label: Core ownership / borrow / lifetime model (`lib/Core`) - - label: Inference or checkers (`lib/Analysis`) - - label: Clang integration or driver (`lib/Frontend`, `tools/`) - - label: Annotations (`weavec.h`) + - label: Guard passes (`lib/Frontend/Guard*.cpp`) + - label: Library table (`lib/Core/LibrarySpec.txt`) + - label: Runtime (`runtime/`) + - label: Driver (`lib/Frontend/Driver.cpp`, `tools/weavec-cc`) - label: Build, CI, or documentation diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 9eb0b7bd..904b7234 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -11,23 +11,23 @@ Small, focused PRs are much easier to review than large ones. ## Test plan - + -- [ ] Unit tests added/updated (`unittests/`) -- [ ] Integration tests added/updated (`test/`) -- [ ] `ninja check-weavec` passes locally +- [ ] Unit tests added/updated (`unittests/`, `runtime/test/rt_test.c`) +- [ ] Integration tests added/updated (`test/Guards`, `test/Driver`, `test/cases`) +- [ ] `ctest --preset dev` passes locally (a runtime change: on Darwin and Linux) ## Checklist - [ ] Code is formatted (`scripts/format.sh`) and passes clang-tidy - [ ] Public headers and behaviour changes are documented - [ ] Conventional Commit PR title and user-visible changes documented (changelog is generated) -- [ ] No new dependencies on Clang/LLVM introduced into `lib/Core` +- [ ] `scripts/check-hygiene.py` passes (no Clang/LLVM in `lib/Core`, runtime entry points declared in `weavec_rt.h`, line budgets) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 143257c9..c73fd70f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,8 +77,8 @@ jobs: - name: Linux / Clang / Debug + ASan/UBSan os: ubuntu-24.04 preset: ci-debug - # The test/cases suites analyse and build (guard passes and all) - # under ASan, without running the case programs. + # The test/cases suites build (guard passes and all) under ASan, + # without running the case programs. configure-args: -DWEAVEC_CASES_ARGS=--no-run - name: Linux / Clang / Release os: ubuntu-24.04 @@ -148,18 +148,10 @@ jobs: ulimit -c 0 ctest --preset "${{ matrix.preset }}" -j "$(getconf _NPROCESSORS_ONLN)" - - name: Documentation examples - env: - ASAN_OPTIONS: detect_leaks=0:allow_user_poisoning=0 - run: | - python3 docs/scripts/check-examples.py \ - --weavec "build/${{ matrix.preset }}/bin/weavec" \ - --weavec-cc "build/${{ matrix.preset }}/bin/weavec-cc" - - # RFC 0035 gate H1: no checked-mode or old-engine residue, no library - # names compared outside the LibrarySpec table, no corpus project names - # in lib/, the engine seam, and the engine, library and runtime line - # budgets. + # RFC 0036 gate H1: no library names compared outside the LibrarySpec + # table, no corpus project names in lib/, the layering of lib/Core and + # the runtime, the runtime's entry points against the passes', and the + # library and runtime line budgets. - name: Repository hygiene if: runner.os == 'Linux' && matrix.preset == 'ci-release' run: | @@ -167,18 +159,16 @@ jobs: python3 scripts/check-hygiene.py # RFC 0035 (section 11): the corpus gate's own tests, then --quick over - # pinned corpus configurations in test/corpus/: the weavec analysis - # against the triage in test/corpus/triage.json, and the enforcement - # ledgers of per-file weavec-cc compiles against the ratchet in + # pinned corpus configurations in test/corpus/: the enforcement ledgers + # of per-file weavec-cc compiles against the ratchet in # test/corpus/expected.json. The weekly Corpus workflow runs --full. - name: Pinned corpus if: matrix.preset == 'ci-release' run: | python3 scripts/test_corpus_gate.py python3 scripts/corpus-gate.py --quick \ - --weavec "build/${{ matrix.preset }}/bin/weavec" \ --weavec-cc "build/${{ matrix.preset }}/bin/weavec-cc" \ - --only log.c --only cJSON-program --only linenoise-program \ + --only log.c --only cJSON --only linenoise-program \ --only jansson --json corpus-gate-quick.json - name: Upload pinned corpus results @@ -192,7 +182,6 @@ jobs: - name: Smoke-test install run: | cmake --install "build/${{ matrix.preset }}" --prefix "$RUNNER_TEMP/weavec-install" - "$RUNNER_TEMP/weavec-install/bin/weavec" --version "$RUNNER_TEMP/weavec-install/bin/weavec-cc" --version printf 'int main(void) { return 0; }\n' > "$RUNNER_TEMP/hello.c" "$RUNNER_TEMP/weavec-install/bin/weavec-cc" "$RUNNER_TEMP/hello.c" -o "$RUNNER_TEMP/hello" diff --git a/.github/workflows/corpus.yml b/.github/workflows/corpus.yml index fd6308c5..48086b3d 100644 --- a/.github/workflows/corpus.yml +++ b/.github/workflows/corpus.yml @@ -1,10 +1,10 @@ name: Corpus # Runs the full corpus gate (RFC 0035, section 11) over the projects pinned -# by SHA in test/corpus/manifest.json, but the sealed sets: the weavec -# analysis and the per-file enforcement ledgers (checked against -# test/corpus/triage.json and test/corpus/expected.json), each project's -# build and test suite with CC=weavec-cc in trap mode (rerun in report mode, +# by SHA in test/corpus/manifest.json, but the sealed sets: the per-file +# enforcement ledgers (checked against test/corpus/expected.json), each +# project's build and test suite with CC=weavec-cc in trap mode (rerun in +# report mode, with the guard failures of test/corpus/triage.json allowed, # and built once more with the reference compiler for the build-time gate), # the injected bugs and the benchmarks (against the reference compiler and # ASan). The gate clones the projects into build/corpus. Pull requests run @@ -61,7 +61,6 @@ jobs: - name: Run corpus gate run: | python3 scripts/corpus-gate.py --full \ - --weavec build/ci-release/bin/weavec \ --weavec-cc build/ci-release/bin/weavec-cc \ --json corpus-gate-full.json diff --git a/AGENTS.md b/AGENTS.md index 3a42f775..fb79fc5e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,58 +5,49 @@ most and where to find the rest. ## Project in one paragraph -WeaveC is a Clang/LLVM-based drop-in C compiler and an ownership analysis -for C. `weavec-cc` guards every memory access of the C it compiles: LLVM -passes (`lib/Frontend/Guard*.cpp`) put a guard before every load, store, -atomic operation, memory intrinsic and library-call memory argument, remove -a guard only where a local rule proves it redundant, lay out stack frames -and globals with redzones, and expand each remaining guard into an inline -check of the runtime's shadow memory; the program traps at the first bad -access with a report. `runtime/` is the C runtime every enforcing link -carries: the shadow, the arena allocator with its quarantine, the guards' -slow paths, the checked library wrappers and the reports -(`libweavec_rt.a`), and the `malloc` family over the arena -(`libweavec_alloc.a`). The ownership and lifetime analysis is *advisory*: it -runs in the `weavec` tool and under `weavec-cc -fweavec-diagnose`, reports -diagnostics and a summary line, and never changes generated code. Three C++ -libraries: `weavec::Core` (the analysis's model, the analysis ledger, -pointer kinds, the abstract heap, the library table; **no Clang/LLVM -includes allowed**), `weavec::Analysis` (Clang AST → core facts: sites, -kinds, slots, the object engine behind the `SafetyEngine` seam in -`lib/Analysis/Engine*.cpp`; the only layer that includes both), +WeaveC is a Clang/LLVM-based drop-in C compiler that enforces memory safety +at run time. `weavec-cc` guards every memory access of the C it compiles: +LLVM passes (`lib/Frontend/Guard*.cpp`) put a guard before every load, +store, atomic operation, memory intrinsic and library-call memory argument, +remove a guard only where a local rule proves it redundant, lay out stack +frames (locals whose lifetimes never overlap share space) and globals with +redzones, check fortified calls against their object size, and expand each +remaining guard into an inline check of the runtime's shadow memory; the +program traps at the first bad access with a report. `runtime/` is the C +runtime every enforcing link carries: the shadow, the arena allocator whose +quarantine reuses a released block only after a *sweep* finds no pointer to +it, the guards' slow paths, the checked library wrappers, ASan's interface +and the reports (`libweavec_rt.a`), and the `malloc` family over the arena +(`libweavec_alloc.a`). Two C++ libraries: `weavec::Core` (only the library +table, `LibrarySpec`; **no Clang/LLVM includes allowed**) and `weavec::Frontend` (the guard passes, unsafe regions, the enforcement -ledger, the analysis action and diagnostics bridging, whole-program -analysis, the compiler driver). `tools/weavec` is a libTooling CLI; -`tools/weavec-cc` is Clang's driver with WeaveC inside. Full picture: -`docs/architecture.md`. Model semantics and the reasoning behind them: -`docs/rfcs/`. Read `0001-ownership-model.md` first, then -`0030-prove-or-trap.md` through `0034-fast-enforcement.md` for the -analysis (the model of facets, outcomes and reasons, the object engine, -the library table, the work budget), and then -`0035-guard-by-default.md`, the current design: it supersedes the -enforcement of RFCs 0030–0034 (inserted checks, require levels, unit -records and the link step, guards lowered from declared calls) and keeps -their analysis as advisory. Read its *Implementation amendments* too; they -override its body. RFC 0036 (planned) brings ownership back as contracts -checked at function entry. +ledger, the compiler driver). `tools/weavec-cc` is Clang's driver with +WeaveC inside; there is no other tool and no static analysis. Full +picture: `docs/architecture.md`. Design and the reasoning behind it: +`docs/rfcs/`. Read `0036-deterministic-heap-reuse.md`, the current design +(sweep-before-reuse, shared frame space, fortified sizes, ASan's contract; +it deleted the advisory ownership analysis, and its *Alternatives* record +why provenance bounds and frame blocks were measured and left out), and +then `0035-guard-by-default.md` for what RFC 0036 keeps: the guards, the +shadow, frames and globals, the library-call guards, the removal rules, the +modes and the ledger. Read each one's *Implementation amendments* too; they +override its body. RFCs 0001–0034 record the analysis and the enforcement +designs that came before. -## Before touching the model, the passes or the runtime +## Before touching the passes or the runtime -Design decisions for `Core`, the analysis (the engine behind -`SafetyEngine`: `ObjectEngine` in `lib/Analysis/Engine*.cpp` over the -domain in `include/weavec/Core/Heap.h`), its outcomes and reasons, -`LibrarySpec.txt`, `weavec.h`, diagnostic ids, what is guarded and which -rules remove a guard, the shadow encoding, frames and globals, the runtime -(`runtime/`), the report formats, the enforcement ledger, and the driver's +Design decisions for `LibrarySpec.txt`, `weavec.h`, what is guarded and +which rules remove a guard, the shadow encoding, frames and globals, the +runtime (`runtime/`: the allocator, the quarantine and the sweep, ASan's +interface), the report formats, the enforcement ledger, and the driver's flags are recorded as RFCs in `docs/rfcs/`. **Read the relevant RFC before -changing any of these**, and treat it as authoritative over comments in the -code. If the change you are about to make is not covered by an Accepted -RFC, or contradicts one, stop and write or amend an RFC first +changing any of these**, and treat it as authoritative over comments in +the code. If the change you are about to make is not covered by an +Accepted RFC, or contradicts one, stop and write or amend an RFC first (`docs/rfcs/README.md` explains when one is required and the process); do not encode a new design decision in code alone. Bug fixes that bring code in line with an RFC need no RFC. A rule that removes a guard must be local -to a function and monitored by `verify` mode (RFC 0035 §6); nothing the -analysis concludes may remove a guard. +to a function and monitored by `verify` mode (RFC 0035 §6). ## Build and test @@ -65,20 +56,21 @@ export WEAVEC_LLVM_PREFIX="$(brew --prefix llvm)" # or /usr/lib/llvm-23 cmake --preset dev && cmake --build --preset dev && ctest --preset dev ``` -- Unit tests: `build/dev/unittests/WeaveC{Core,Analysis,Frontend}Tests` - (`GuardPassTest` runs the passes on IR text). +- Unit tests: `build/dev/unittests/WeaveC{Core,Frontend}Tests` + (`LibrarySpecTest` checks every row of the library table; + `GuardPassTest` runs the passes on IR text). - Runtime test: `ctest --preset dev -R '^runtime$'` (`runtime/test/rt_test.c`, built as `weavec_rt_test`; test names as arguments run only those). - Integration tests: `lit -v build/dev/test` (FileCheck-based; see `test/README.md`). `test/Guards/` pins the passes' IR (`ir-*.c`, on - `-emit-llvm`) and the run-time reports. -- Test cases: `scripts/run-cases.py [--filter 'soundness/**']` (see + `-emit-llvm`) and the run-time reports; `test/Driver/` the driver. +- Test cases: `scripts/run-cases.py [--filter 'determinism/**']` (see `test/cases/README.md`); CTest runs them as `cases-`. `--asan` adds the ASan oracle; `--checks verify` fails on any `weavec.proven` report. - Corpus gate: `scripts/corpus-gate.py --quick` (see `test/corpus/README.md`). -- Documentation examples: `python3 docs/scripts/check-examples.py --weavec - build/dev/bin/weavec --weavec-cc build/dev/bin/weavec-cc`; the site: - `npm ci && npm test && npm run build` in `docs/`. +- Documentation examples: `python3 docs/scripts/check-examples.py + --weavec-cc build/dev/bin/weavec-cc`; the site: `npm ci && npm test && + npm run build` in `docs/`. - Hygiene gate: `scripts/check-hygiene.py`. - Format: `scripts/format.sh`; check: `scripts/check-format.sh`. @@ -86,15 +78,15 @@ cmake --preset dev && cmake --build --preset dev && ctest --preset dev 1. Never add `clang/` or `llvm/` includes under `include/weavec/Core` or `lib/Core`. -2. Every new diagnostic gets a stable id in `weavec::core::diag` - (`include/weavec/Core/Diagnostic.h`), an entry in `docs/annotations.md` - and `docs/data/diagnostic-remedies.json`, a unit test, and a lit test that - pins the exact message. A new run-time report kind is not a diagnostic: - it gets a `test/Guards/` test that pins its exact report line, and an - entry in the command-line reference's list of kinds. -3. Annotation spellings live in exactly two places that must agree: - `include/weavec/Analysis/Annotations.h` (`spelling::`) and - `resources/include/weavec.h`. +2. WeaveC has no compile-time diagnostics of its own. A new run-time report + kind gets a `test/Guards/` test that pins its exact report line, and an + entry in the command-line reference's list of kinds + (`docs/pages/reference/cli.md`); the case runner's list of kinds + (`scripts/run-cases.py`) must know it too. +3. `WEAVEC_UNSAFE` is the only annotation. Its spelling + (`annotate("weavec.unsafe")`) lives in exactly two places that must + agree: `UnsafeAnnotation` in `include/weavec/Frontend/UnsafeRegions.h` + and `resources/include/weavec.h`. 4. Warnings are errors in CI (`WEAVEC_WARNINGS_AS_ERRORS=ON`). Do not add suppressions without a comment. 5. Follow the existing file header block and naming (`CamelCase` types and @@ -105,62 +97,49 @@ cmake --preset dev && cmake --build --preset dev && ctest --preset dev do not edit it manually. 7. Do not commit generated files (`build/`, `compile_commands.json`, `docs/.generated/`). -8. Changes to `Core`, the analysis's rules, the guard passes, the runtime, - annotations or diagnostic ids reference the RFC that specifies them in +8. Changes to the library table, the guard passes, the runtime, the + annotation or the report kinds reference the RFC that specifies them in the PR. New tests are named by feature (`test/cases//…`, `test/Guards/.c`); existing `rfcNNNN-` names may stay. -9. Keep the engine seam and the library table clean (`scripts/check-hygiene.py`; - RFC 0031 §2): only the `Engine*.cpp` files include the engine's private - header `lib/Analysis/Engine.h`; the engine publishes only through - `LedgerAdapter` and never receives a `DiagnosticSink`; library behaviour - goes in `lib/Core/LibrarySpec.txt` (the guards read it too, through - `GuardLibrary.cpp`), never in `name == "…"` tests; nothing under `lib/` - names a corpus project; library code stays within its line budget, and - so does the runtime (`runtime/` without `runtime/test/`, counted apart). - The runtime is C and includes no WeaveC header but its own `weavec_rt.h`, - and the entry points it declares there must match the ones the passes - declare. +9. Keep the library table and the runtime clean (`scripts/check-hygiene.py`; + RFC 0036 gate H1): library behaviour goes in `lib/Core/LibrarySpec.txt` + (the guards read it through `GuardLibrary.cpp`), never in + `name == "…"` tests; nothing under `lib/` names a corpus project; the + library code (`lib/`, `include/`, `tools/`) stays within its line budget + (9,000), and so does the runtime (`runtime/` without `runtime/test/`, + counted apart: 6,000). The runtime is C and includes no WeaveC header + but its own `weavec_rt.h`, and every runtime entry point the passes + declare (and every `wrapper(NAME)` of the table) must be declared there + with the same signature. The runtime's `preserve_all` entry points + (`WEAVEC_RT_SLOW`) stay trivial wrappers that call non-inlined helpers: + Apple clang 21 miscompiles a `preserve_all` function that uses many + callee-saved registers (it saves some below the stack pointer). ## Where things are | Task | Look at | | ------------------------------------ | ------------------------------------------------------ | | Propose a design change | `docs/rfcs/README.md`, `docs/rfcs/0000-template.md` | -| Change what weavec-cc adds to a compile (zero-init, array bounds, locations, the passes) | `configureEnforcement` in `lib/Frontend/Driver.cpp`; `registerGuardPasses` in `lib/Frontend/GuardPass.cpp` (RFC 0035 §1, amendment 1) | +| Change what weavec-cc adds to a compile (zero-init, array bounds, locations, the passes) | `configureEnforcement` in `lib/Frontend/Driver.cpp`; `registerGuardPasses` in `lib/Frontend/GuardPass.cpp` (RFC 0035 §1, amendment 1; RFC 0036 §1) | +| Change fortification or ASan's contract (`_FORTIFY_SOURCE=3`, `__SANITIZE_ADDRESS__`, `-fno-weavec-asan-compat`) | `wantsFortification`, `applyAsanCompat` and `AsanOptionsReset` in `lib/Frontend/Driver.cpp` (RFC 0036 §1, §5); tests `test/Driver/fortify.c`, `asan-compat.c`, `asan-compat-fortify.c` | | Change which accesses get a guard | `lib/Frontend/GuardInsert.cpp` (`GuardInsert`, at `PipelineStartEP`: the markers `__weavec.guard`, `.range`, `.strlen`, `.disjoint`, `.scope`; RFC 0035 §2.1, amendments 1–2) | -| Change how a library call is guarded | `lib/Frontend/GuardLibrary.cpp` (row-driven guards, `wrapper(…)` redirection, `wide` rows, `%s`/`%n` arguments); the rows and their clauses in `lib/Core/LibrarySpec.txt` (one unit test per row in `unittests/Core/LibrarySpecTest.cpp`); the wrappers in `runtime/weavec_libc.c` (RFC 0035 §2.5, amendment 7) | +| Change how a library call is guarded, fortified limits included | `lib/Frontend/GuardLibrary.cpp` (row-driven guards, `wrapper(…)` redirection, `chk` object sizes and `limit` arguments, `wide` rows, `%s`/`%n` arguments); the rows and their clauses in `lib/Core/LibrarySpec.txt` (one unit test per row in `unittests/Core/LibrarySpecTest.cpp`); the wrappers, their limits and the number parsers in `runtime/weavec_libc.c` (RFC 0035 §2.5, amendment 7; RFC 0036 §6); tests `test/Guards/checked-calls.c`, `intra-object.c` | | Change the rules that remove guards | `GuardPrune` (`PeepholeEP`; `pruneGuards` in `lib/Frontend/GuardInsert.cpp`: rule 6.1 after inlining) and `GuardExpand` (`OptimizerLastEP`; `lib/Frontend/GuardExpand.cpp`: rules 6.1–6.3); the passes' entry points are in `GuardPass.cpp`; `lib/Frontend/GuardLoops.cpp` (`GuardLoop`, `VectorizerStartEP`: loop versioning, rule 6.4); each removal must stay monitored by `verify` mode | | Change the inline checks (fast paths, null bases) | `lib/Frontend/GuardPass.cpp`, `GuardPassImpl.h`; the shadow encoding in `runtime/weavec_rt.h` (RFC 0035 §2.2–§2.4, amendments 3–4); tests `test/Guards/ir-inline-check.c`, `null-base.c` | -| Change stack frames, scopes, dynamic allocas, non-local exits | `lib/Frontend/GuardFrames.cpp` (RFC 0035 §3, amendment 5); tests `test/Guards/ir-frames.c`, `stack-overflow.c`, `use-after-scope*.c`, `vla-overflow.c`, `longjmp.c`, `ir-noreturn.c` | -| Change global redzones and registration | `lib/Frontend/GuardGlobals.cpp`; `__weavec_rt_globals_register` in `runtime/weavec_guard.c` (RFC 0035 §4); tests `test/Guards/ir-globals.c`, `global-overflow.c` | +| Change stack frames, shared frame space, scopes, dynamic allocas, non-local exits | `lib/Frontend/GuardFrames.cpp` (RFC 0035 §3, amendment 5; RFC 0036 §2: `StackLifetime`); tests `test/Guards/ir-frames.c`, `shared-frame.c`, `stack-overflow.c`, `use-after-scope*.c`, `vla-overflow.c`, `alloca-entry.c`, `longjmp.c`, `ir-noreturn.c`; case `test/cases/determinism/shared-frame_ok.c` | +| Change global redzones and registration | `lib/Frontend/GuardGlobals.cpp`; `__weavec_rt_globals_register` in `runtime/weavec_guard.c`, which a shared image's copy of the runtime forwards to the process's owner (RFC 0035 §4, RFC 0036 §4); tests `test/Guards/ir-globals.c`, `global-overflow.c`, `test/Driver/image-globals.c` | | Change unsafe regions | `lib/Frontend/UnsafeRegions.cpp` (the AST collector; `no_sanitize("address")` functions too; RFC 0035 §2.6, amendment 8) | | Change the enforcement ledger or the summary line | `lib/Frontend/EnforcementLedger.cpp` (`weavec-ledger` version 3, `EnforcementLedgerVersion`; RFC 0035 §9, amendment 10); test `test/Guards/ledger.c` | -| Change the runtime (allocator, shadow, guards' slow paths, reports) | `runtime/weavec_alloc.c` (arena, quarantine, the heap's shadow), `weavec_malloc.c` (the `malloc` family), `weavec_guard.c` (slow paths, ranges, strings, stack and globals), `weavec_libc.c` (checked library calls, `mmap`), `weavec_owner.c` (one runtime per process on Darwin), `weavec_report.c` (report formats, `WEAVEC_RT_*` variables), `weavec_rt.h`; its C test `runtime/test/rt_test.c` (CTest `runtime`); cases in `test/cases/semantics/runtime/` | +| Change the allocator, the quarantine or when to sweep | `runtime/weavec_alloc.c` (arena, slot words, quarantine, huge blocks, the heap's shadow; the soft and hard budgets and the pacing, `overBudget`; recycling in address order; RFC 0036 §3), `weavec_malloc.c` (the `malloc` family); `rt_test` tests `pacing`, `sweep-*` | +| Change the sweep | `runtime/weavec_sweep.c` (stopping threads: Darwin `thread_suspend`, Linux signals; the roots: stacks, registers, writable mappings with dirty pages, the image table for the dyld shared cache; the conservative mark; RFC 0036 §3.4); `rt_test` tests `sweep-*`, `library-data-is-scanned` | +| Change ASan's interface | `runtime/weavec_asan.c` (RFC 0036 §5); test `test/Guards/use-after-poison.c`, case `test/cases/determinism/asan-pool_*.c` | +| Change the rest of the runtime (slow paths, reports) | `runtime/weavec_guard.c` (slow paths, ranges, strings, frames' helpers, globals, fortified-size failures), `weavec_owner.c` (one runtime per process on Darwin, the dispatch table, version 4), `weavec_report.c` (report formats, `WEAVEC_RT_*` variables), `weavec_rt.h`; its C test `runtime/test/rt_test.c` (CTest `runtime`); cases in `test/cases/semantics/runtime/` | | Change the runtime's link line and fallbacks | `lib/Frontend/Driver.cpp` (`addRuntimeLibraries`, `runtimeObstacle`, `allocatorDefinedBy`, `dropAllocatorIfDefined`); `test/Driver/runtime-*.c` | -| Change `weavec-cc` (driver, cc1 wrapping, flags) | `lib/Frontend/Driver.cpp` (`DriverOptions::consume`, `driverFlagsHelp`, `runCc1Job`), `include/weavec/Frontend/Driver.h`, `tools/weavec-cc/main.cpp` | -| Change the analysis under `-fweavec-diagnose` | `analysisOptions` in `lib/Frontend/Driver.cpp`; `lib/Frontend/FrontendAction.cpp` (the analysis action shared with `weavec`) | -| Change `-W` handling | `lib/Frontend/DiagnosticControl.cpp` | -| Add a checker rule | After an RFC, behind the seam: the object engine (`lib/Analysis/Engine*.cpp`, `ObjectEngine`; decisions in `EngineDecide.cpp`) publishes only through `LedgerAdapter` (`include/weavec/Analysis/LedgerAdapter.h`, `SafetyEngine.h`) | -| Change the abstract domain (objects, cells, symbols, zone, joins) | `include/weavec/Core/{Heap,Zone,Persistent}.h`, `lib/Core/Heap.cpp`, `lib/Core/Zone.cpp` (unit tests in `unittests/Core/HeapTest.cpp`) | -| Change analysis outcomes, reasons, the analysis summary line | `lib/Core/Ledger.cpp`, `lib/Analysis/LedgerAdapter.cpp` (defaults, rollup), `lib/Frontend/AnalysisSummary.cpp` | -| Change which sites exist | `lib/Analysis/SiteCollector.cpp` | -| Change pointer kinds / how annotations become kinds | `lib/Core/PointerKind.cpp`, `lib/Analysis/AttributeReader.cpp`, `lib/Analysis/KindInference*.cpp`; what the engine takes from them: `lib/Analysis/EngineKinds.cpp` | -| Evaluate an expression / map an lvalue to an address | `lib/Analysis/EngineExpr.cpp` (`Transfer::evaluate`, `Transfer::addressOf`) | -| Model a C library function for the analysis | `lib/Core/LibrarySpec.txt`; how the engine applies a row: `lib/Analysis/EngineCalls.cpp` (effects), `lib/Analysis/EngineLibrary.cpp` (argument requirements) | -| Change function-pointer slots | `lib/Core/FnSlots.cpp`, `lib/Analysis/SlotCollector.cpp` | -| Change how a callee's summary is found | `lib/Analysis/EngineCalls.cpp` (`CallApplier::applyDirect`), `UnitRun::summaryOf` in `lib/Analysis/EngineUnit.cpp` (RFC 0031 §5.4) | -| Change how a summary is derived or applied | `lib/Analysis/EngineSummary.cpp` (RFC 0031 §6), `include/weavec/Core/Effects.h` | -| Change the TU driver / call graph | `lib/Analysis/EngineUnit.cpp` (`UnitRun`), `lib/Analysis/UnitPipeline.cpp` | -| Change what a unit exports / the program database | `UnitRun::exports` in `lib/Analysis/EngineUnit.cpp`, `lib/Analysis/ProgramDatabase.cpp` (RFC 0005, RFC 0031 §7) | -| Change how summaries join, widen or renumber their globals | `lib/Core/Effects.cpp` (`joinEffects`, `widenEffects`, `renumberGlobals`; tests in `unittests/Core/EffectsTest.cpp`) | -| Change `weavec --whole-program` | `lib/Frontend/ProgramAnalysis.cpp` (the units as one program, in memory), `lib/Frontend/ProgramChecks.cpp` (declarations against definitions), `lib/Frontend/InterfaceFacts.cpp`, `tools/weavec/main.cpp` (RFC 0035 §8) | -| Debug what a guarded program did | the report line names the kind, the access and the address; `-fweavec-checks=report` prints each failing site once and goes on; `WEAVEC_RT_STATS=1 ./prog` prints the runtime's counters at exit; `-fweavec-ledger=out/ -fweavec-summary` shows which accesses kept their guard | -| Debug the passes | `weavec-cc -O2 -S -emit-llvm file.c -o -` (the expanded guards); `-mllvm -print-after-all` (the markers through the pipeline; the passes print as `weavec::frontend::GuardInsertPass` and so on); `-fweavec-checks=verify` (removed guards kept as monitors) | -| Debug what the analysis inferred | `weavec --dump-analysis file.c --`; `weavec --dump-kinds file.c --`; `weavec --whole-program --dump-analysis a.c b.c --`; `WEAVEC_ENGINE_DUMP=1` prints every summary on stderr, `=2` adds each run's exit states, `=3` each block's entry state instead (`lib/Analysis/EngineUnit.cpp`, `EngineSummary.cpp`, `EngineRun.cpp`) | +| Change `weavec-cc` (driver, cc1 wrapping, flags) | `lib/Frontend/Driver.cpp` (`DriverOptions::consume`, `driverFlagsHelp`, `runCc1Job`), `include/weavec/Frontend/Driver.h`, `tools/weavec-cc/main.cpp`; document flags in `docs/pages/reference/cli.md` | +| Debug what a guarded program did | the report line names the kind, the access and the address, and, for the heap, a second line the block; `WEAVEC_RT_BACKTRACE=1` prints the stack of the stopping access; `-fweavec-checks=report` prints each failing site once and goes on; `WEAVEC_RT_STATS=1 ./prog` prints the runtime's counters (sweeps and their pauses included) at exit; `-fweavec-ledger=out/ -fweavec-summary` shows which accesses kept their guard | +| Debug the passes | `weavec-cc -O2 -S -emit-llvm file.c -o -` (the expanded guards and the frame layout); `-mllvm -print-after-all` (the markers through the pipeline; the passes print as `weavec::frontend::GuardInsertPass` and so on); `-fweavec-checks=verify` (removed guards kept as monitors) | | Add or run a test case | `test/cases/README.md`, `scripts/run-cases.py` | | Measure on real code | `scripts/corpus-gate.py`, `test/corpus/` (README, manifest, expected ratchet, triage) | -| Change how diagnostics are rendered | `lib/Frontend/ClangDiagnosticSink.cpp` | -| Add a CLI flag | `tools/weavec/main.cpp`, `FrontendOptions`; driver flags in `Driver.h` and `Driver.cpp`; document them in `docs/pages/reference/cli.md` | -| Add an annotation | `Annotations.h`, `weavec.h`, `docs/annotations.md` | +| Add an annotation | Don't, without an RFC: `WEAVEC_UNSAFE` is the only one (`UnsafeRegions.h`, `weavec.h`, `docs/annotations.md`) | | Change build flags / warnings | `cmake/WeaveCWarnings.cmake`, `cmake/WeaveCLLVM.cmake` | | Add a CI job | `.github/workflows/ci.yml` | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 04cf68c2..d0c4f73c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,8 @@ covers building, testing and tooling in depth. - Be kind and constructive. We follow the [Code of Conduct](CODE_OF_CONDUCT.md). - Discuss significant changes in an issue before opening a large PR. Changes - to the ownership model, checker rules or annotations go through an RFC; see + to what WeaveC guards, the rules that remove guards, the runtime or the + annotation go through an RFC; see [Proposing larger changes](#proposing-larger-changes). - Keep PRs small and focused. Several small PRs are easier to review than one large one. @@ -32,9 +33,9 @@ covers building, testing and tooling in depth. cmake --preset dev && cmake --build --preset dev && ctest --preset dev ``` -5. Before pushing, run the sanitizer build at least once for analysis changes - (`cmake --workflow --preset ci-debug`) and make sure `scripts/check-format.sh` - passes. +5. Before pushing, run the sanitizer build at least once for changes to the + guard passes (`cmake --workflow --preset ci-debug`), run a runtime change + on both Darwin and Linux, and make sure `scripts/check-format.sh` passes. 6. Open a pull request. The template will ask for a summary, design notes and a test plan. CI must be green and one maintainer approval is required. @@ -49,14 +50,16 @@ links each milestone to the RFC that specifies it. ## Proposing larger changes -Anything that changes what WeaveC guarantees needs an RFC before code: adding -to or changing the ownership model in `lib/Core`, adding a checker rule or -changing what an existing rule accepts, adding or changing an annotation in -`weavec.h`, or adding a diagnostic id. Copy +Anything that changes what WeaveC guarantees needs an RFC before code: +changing what is guarded or how (frames, globals, the sweep, library-call +guards and the library table), adding or changing a rule that removes a +guard, changing the runtime's allocator, quarantine, sweep or shadow +encoding, adding a run-time report kind or changing a report's format, or +adding an annotation to `weavec.h`. Copy [`docs/rfcs/0000-template.md`](docs/rfcs/0000-template.md) to `docs/rfcs/NNNN-short-title.md` with the next unused number, fill it in -(the *Soundness*, *Diagnostics* and *Unresolved questions* sections are the -ones reviewers read first), and open a PR containing only the RFC. Discussion +(the *Soundness* and *Unresolved questions* sections are the ones reviewers +read first), and open a PR containing only the RFC. Discussion happens on that PR; once there is consensus the RFC is merged as **Accepted** and implementation proceeds in follow-up PRs that reference it. The full process and the index of existing RFCs are in @@ -73,33 +76,39 @@ with a few deliberate deviations, all enforced by `.clang-format` / `.clang-tidy | Element | Convention | Example | | ----------------------- | ---------------------- | ------------------------------- | | Namespaces | `lower_case` | `weavec::core` | -| Types | `CamelCase` | `HeapState`, `ObjectId` | -| Functions and methods | `camelBack` | `addressOf`, `toCoreLocation` | -| Variables and members | `camelBack` | `placeOf`, `tracker` | -| Enumerators | `CamelCase` | `OwnershipKind::Shared` | -| Compile-time constants | `CamelCase` | `diag::UseAfterFree`, `FrameAlign` | -| Macros | `UPPER_CASE` | `WEAVEC_OWNED` | -| Header guards | `WEAVEC__H` | `WEAVEC_CORE_HEAP_H` | +| Types | `CamelCase` | `ModuleContext`, `LedgerRow` | +| Functions and methods | `camelBack` | `layoutFrame`, `checkEscape` | +| Variables and members | `camelBack` | `tracked`, `ledger` | +| Enumerators | `CamelCase` | `LedgerRow::Outcome::Guarded` | +| Compile-time constants | `CamelCase` | `EnforcementLedgerVersion`, `LocalAlign` | +| Macros | `UPPER_CASE` | `WEAVEC_UNSAFE` | +| Header guards | `WEAVEC__H` | `WEAVEC_CORE_LIBRARYSPEC_H` | + +The runtime (`runtime/`) is C and follows the same naming, with its +internal functions prefixed `weavecRt` and its entry points +`__weavec_rt_`. Additional rules: -- **Layering.** `lib/Core` must never include Clang or LLVM headers. Only - `lib/Analysis` may depend on both Clang and Core. CI does not yet enforce - this mechanically, so reviewers will. -- **Diagnostics.** Every user-facing diagnostic has a stable identifier in - `weavec::core::diag`. Changing an identifier is a breaking change. +- **Layering.** `lib/Core` must never include Clang or LLVM headers + (`scripts/check-hygiene.py` checks it). The runtime includes no WeaveC + header but its own `runtime/weavec_rt.h`, and declares there every entry + point the passes call. +- **Reports.** A run-time report's kind and line format are user-visible: + scripts and the case runner match them, so changing one is a breaking + change. - **Comments** explain intent and invariants, not what the code does. Each file starts with the standard LLVM-style header block. - **Warnings** are errors in CI. Do not suppress warnings without a comment explaining why. -- **Tests.** New analysis or guard behaviour needs both a unit test (in - `unittests/`) exercising the core logic and an end-to-end test: a lit test - (in `test/`) when the exact diagnostic, IR or report text matters, or a - case under `test/cases/` whose markers pin what must be reported, where - the program must stop and what stays guarded (see `test/cases/README.md`). +- **Tests.** New guard behaviour needs both a unit test (in `unittests/`, + or `runtime/test/rt_test.c` for the runtime) exercising the core logic and + an end-to-end test: a lit test (in `test/`) when the exact IR or report + text matters, or a case under `test/cases/` whose markers pin where the + program must stop and what stays guarded (see `test/cases/README.md`). New tests are named by feature (`test/cases//…`, `test/Guards/.c`), not by RFC number; - existing `rfcNNNN-` names may stay. False-positive fixes need a regression + existing `rfcNNNN-` names may stay. False-trap fixes need a regression case under `test/cases/` (a `// CLEAN` file). ## Commit messages @@ -108,10 +117,10 @@ Use Conventional Commits for commits and squash-merge PR titles, with an optional area scope. Explain *why* in the body when it is not obvious: ``` -fix(analysis): treat realloc as a move of its first argument +fix(runtime): check what strtoumax read, not a terminator -realloc may free the original allocation, so subsequent uses of the old -pointer must be flagged the same way as after free(). +The number parsers read only the number they parse and the character that +stops it, so requiring a terminated string trapped on correct input. Fixes #42. ``` @@ -120,7 +129,7 @@ Use `feat` for new behavior, `fix` for bug fixes, and `perf` for performance improvements. These trigger releases. Use `!` or a `BREAKING CHANGE:` footer for incompatible changes. `docs`, `ci`, `build`, `test`, `refactor`, `style` and `chore` normally do not trigger a release. Optional scopes include `core`, -`analysis`, `frontend`, `driver` and `cmake`. +`frontend`, `runtime`, `driver` and `cmake`. Python Semantic Release generates `CHANGELOG.md` from the commit history; do not edit it manually. Document behavior and migration guidance in the diff --git a/README.md b/README.md index dcc49fd2..607522c2 100644 --- a/README.md +++ b/README.md @@ -2,33 +2,35 @@ # WeaveC -**Memory safety for existing C code: every access guarded at run time, ownership bugs reported before it runs.** +**Memory safety for existing C code: every access checked at run time, freed memory never handed back while a pointer to it remains.** [![CI](https://github.com/weavefoundry/weavec/actions/workflows/ci.yml/badge.svg)](https://github.com/weavefoundry/weavec/actions/workflows/ci.yml) [![Release](https://img.shields.io/github/v/release/weavefoundry/weavec?include_prereleases&sort=semver)](https://github.com/weavefoundry/weavec/releases) [![License](https://img.shields.io/badge/license-Apache--2.0%20WITH%20LLVM--exception-blue.svg)](LICENSE) -[Documentation](https://weavec.com) · [Getting started](https://weavec.com/getting-started/installation/) · [Diagnostic reference](https://weavec.com/reference/diagnostics/) · [Releases](https://github.com/weavefoundry/weavec/releases) +[Documentation](https://weavec.com) · [Getting started](https://weavec.com/getting-started/installation/) · [Safety guarantees](https://weavec.com/reference/guarantees/) · [Releases](https://github.com/weavefoundry/weavec/releases) -WeaveC is a drop-in C compiler that makes the C code you already have stop at its first bad memory access, and an ownership and borrowing analysis that reports memory bugs before the program runs. +WeaveC is a drop-in C compiler, `weavec-cc`, that makes the C code you already have stop at its first bad memory access. Before a load, a store or a C library call touches memory, a few inline instructions ask a small runtime linked into the program whether those bytes belong to a live object. The first access that touches a byte that does not stops the program with a report that names the access. -- **`weavec-cc` guards every memory access.** Before a load, a store or a C library call touches memory, a few inline instructions ask a small runtime linked into the program whether those bytes belong to a live object. An access past a heap block, a local or a global, a read of freed memory, or a use of a local after its scope has ended stops the program with a report that names the access. A guard is removed only where a simple local rule proves it redundant. -- **The analysis infers ownership.** It follows how pointers are allocated, shared, moved and released, and reports use after free, double free, dangling pointers and leaks, across files when asked. It is advisory: it never changes the compiled code, so a mistake in it can neither break a correct build nor remove a needed guard. +- **Every access is guarded.** An access past a heap block, a local or a global, a use of a local after its scope, an index past an array's declared bound and a bad `free` stop the program. A guard is removed only where a simple local rule proves it redundant. +- **Freed memory is reused only after a sweep.** A released heap block waits until a sweep of the program's stacks, registers, writable data and live heap finds no pointer to it. A use after free or a double free traps, however much the program allocated since. +- **Fortified sizes are kept.** A fortified library call never writes more than the size of the field it was given: a `strcpy` that would run from an 8-byte field into the next one traps. -WeaveC is built on Clang and LLVM, which provide parsing, code generation and platform support. WeaveC adds the guard passes, the runtime and the analysis, in C++ libraries kept separate from the Clang integration. +WeaveC is built on Clang and LLVM, which provide parsing, code generation and platform support. WeaveC adds LLVM passes that guard the code, and a runtime in C. > [!NOTE] -> WeaveC is early, v0.x software: flags, diagnostics, report formats and the ledger can change between minor versions. The design is [RFC 0035, *Guard by default*](docs/rfcs/0035-guard-by-default.md); [RFC 0036](docs/roadmap.md) will make inferred ownership contracts checked at function entry, so that they remove guards. The [roadmap](docs/roadmap.md) tracks progress. +> WeaveC is early, v0.x software: flags, report formats and the ledger can change between minor versions. The design is [RFC 0036, *Deterministic heap reuse*](docs/rfcs/0036-deterministic-heap-reuse.md), which builds on the guards of [RFC 0035](docs/rfcs/0035-guard-by-default.md). The [roadmap](docs/roadmap.md) tracks progress. ## Highlights - **A drop-in compiler.** `weavec-cc` is Clang's driver with WeaveC inside: point a build at it with `CC=weavec-cc` and it compiles as `clang` would. A correct program built with the default flags compiles, links and runs as it does with Clang, apart from run time; a false trap is a WeaveC bug. +- **Nothing to annotate.** Existing code builds unchanged. The only annotation, `WEAVEC_UNSAFE` from `weavec.h`, leaves a function or a block unguarded, for code that reads past an object on purpose. - **Precise reports.** A failed guard prints the kind of failure, the source location and the address, and, for the heap, where the address lies relative to its block. -- **No ABI changes.** Guards are inline checks of a shadow memory; pointer representation and calling conventions are unchanged, and objects from other compilers link in. -- **Inference first.** The analysis needs no annotations to start; annotations from `weavec.h` state contracts where inference needs help, and they expand to nothing on other compilers. -- **An auditable ledger.** `-fweavec-ledger` lists every access the compiler saw and whether it kept its guard, and why not. +- **No ABI changes.** Pointers keep their representation and calling conventions are unchanged; objects from other compilers link in. +- **Compatible with AddressSanitizer's contract.** Code compiled by `weavec-cc` sees `__SANITIZE_ADDRESS__` and `__has_feature(address_sanitizer)`, and the runtime implements ASan's poisoning interface, so code written to be ASan-clean is WeaveC-clean, and a pool that poisons its free space for ASan is checked by WeaveC too. +- **An auditable ledger.** `-fweavec-ledger` lists every access the compiler saw, whether it kept its guard, and why not. ## Quick look @@ -46,75 +48,33 @@ weavec: heap-buffer-overflow at vec.c:9:43: read of 4 bytes at 0xbd80000010 weavec: 0xbd80000010 is 0 bytes after the 16-byte heap object at 0xbd80000000 ``` -The program stops with `SIGTRAP` (or `SIGILL` on x86-64) at the access, instead of reading past the block. The same guards stop a read of a freed block (`heap-use-after-free`), an overflow of a local or a global (`stack-buffer-overflow`, `global-buffer-overflow`), a use of a local after its scope (`stack-use-after-scope`), an index past an array's declared bound (`index-out-of-bounds`) and a double or invalid `free`. +The program stops with `SIGTRAP` (or `SIGILL` on x86-64) at the access, instead of reading past the block. -The analysis finds bugs without running the program: +A stale pointer stays caught however much the program allocates after the `free`: ```c -#include - -struct node { int v; struct node *next; }; -static void node_free(struct node *n) { free(n); } // inferred: consumes n - -int example(struct node *n) { - struct node *m = n; - node_free(m); - return n->v; // error: freed on every path -} - -int maybe(struct node *n, int done) { - if (done) - node_free(n); - return n->v; // warning: freed on some paths -} - -int *escape(void) { - int x = 0; - return &x; // error: the pointer outlives x -} +struct session *s = malloc(sizeof *s); +s->on_close = bye; +free(s); +for (int i = 0; i < 4000000; i++) + free(malloc(sizeof *s)); // the allocator recycles millions of blocks of the same size +s->on_close(); // but never the one s still points to ``` ```console -$ weavec example.c -- -example.c:20:11: warning: address of stack memory associated with local variable 'x' returned [-Wreturn-stack-address] - 20 | return &x; // error: the pointer outlives x - | ^ -example.c:9:10: error: use of 'n' after it was freed [weavec::use-after-free] - 9 | return n->v; // error: freed on every path - | ^ -example.c:8:3: note: freed here (through 'm') - 8 | node_free(m); - | ^ -example.c:15:10: warning: use of 'n' after it may have been freed [weavec::use-after-free] - 15 | return n->v; // warning: freed on some paths - | ^ -example.c:14:5: note: freed here on some paths - 14 | node_free(n); - | ^ -example.c:20:10: error: returned pointer may outlive 'x', which it points to [weavec::lifetime-too-short] - 20 | return &x; // error: the pointer outlives x - | ^ -example.c:19:7: note: 'x' is declared here - 19 | int x = 0; - | ^ -weavec: example.c: 9 sites: 5 proven, 2 not proven, 2 violations, 0 trusted; 2 errors, 1 warning -2 warnings and 2 errors generated. -Error while processing example.c. +$ weavec-cc stale.c -o stale +$ ./stale +weavec: heap-use-after-free at stale.c:18:3: read of 8 bytes at 0xbc00000000 +weavec: 0xbc00000000 is inside a released heap block at 0xbc00000000 ``` -Definite bugs are errors; a use-after-free on only some paths is a warning. (The first warning is Clang's own.) The last line summarises the analysis of the file's memory operations (*sites*). `weavec-cc -fweavec-diagnose` prints the same findings as warnings during a build. +The same guards stop an overflow of a local or a global (`stack-buffer-overflow`, `global-buffer-overflow`), a use of a local after its scope (`stack-use-after-scope`), a fortified call that overruns a field (`intra-object-overflow`), an access to memory the program poisoned through ASan's interface (`use-after-poison`), an index past an array's declared bound (`index-out-of-bounds`), and a double or invalid `free`. ## How it works -The runtime keeps one *shadow* byte for every 16 bytes of memory, saying which of them belong to a live object, and replaces the program's allocator: every heap object sits in a slot with at least one byte after it that belongs to no object, and released blocks wait in a quarantine (16 MiB by default) before they are reused. `weavec-cc` lays out the locals a guard can reach in one frame with *redzones* between them, gives the file's globals redzones, and zero-initialises locals. +The runtime replaces the program's allocator and keeps one *shadow* byte for every 16 bytes of memory, saying which of them belong to a live object. Every heap object sits in a slot of an arena with at least one byte after it that belongs to no object. A released block waits in a quarantine until a *sweep*, which stops the program's threads and scans their stacks, registers, writable data and live heap conservatively, finds no pointer to it; only then is it reused. Sweeps are paced so that they take at most about a ninth of the run time; between them the quarantine grows, up to a ceiling. `weavec-cc` lays out the locals a guard can reach in one frame with *redzones* between them (locals whose lifetimes never overlap share space, as Clang's own frames do), gives the file's globals redzones, and zero-initialises locals. -Four LLVM passes then guard the code. The first puts a guard before every load, store, atomic operation and memory intrinsic, and before every memory argument of a call in the [library table](lib/Core/LibrarySpec.txt), before the optimiser runs, so the optimiser cannot exploit the bug before the guard sees it. The others remove the guards a local rule proves redundant (an access inside a local or a global at a known offset, an access a dominating guard on the same pointer already covered with nothing between that may free, nearby accesses merged into one guard, a loop whose whole range is checked once before it runs) and expand the rest into a three-instruction check of the shadow that calls the runtime only when a byte is not zero. Array indexes are also checked against their declared bounds. `-fweavec-checks=verify` keeps every removed guard as a monitor, so a wrong removal is caught in testing. - -The analysis runs separately, in the `weavec` tool or under `-fweavec-diagnose`: ownership inference, borrow and lifetime checking, null and bounds reasoning over an abstract heap, and function summaries that let a caller see what a callee does, across files with `weavec --whole-program`. - -### Annotations - -Where the analysis needs help, typically at public interfaces, annotations from `weavec.h` state the contract: `WEAVEC_OWNED`, `WEAVEC_BORROWED`, `WEAVEC_MUT`, `WEAVEC_RAW`, `WEAVEC_NULLABLE`, `WEAVEC_NONNULL`, `WEAVEC_COUNTED_BY(n)`, `WEAVEC_ENDED_BY(q)`, `WEAVEC_STRING`, `WEAVEC_ASSUME(e)` and the reference-counting forms. They are inputs to the analysis and expand to nothing on other compilers. One changes the compiled code: `WEAVEC_UNSAFE` on a function or block leaves its accesses unguarded, for code that reads past an object on purpose. See [docs/annotations.md](docs/annotations.md). +Four LLVM passes then guard the code. The first puts a guard before every load, store, atomic operation and memory intrinsic, and before every memory argument of a call in the [library table](lib/Core/LibrarySpec.txt), before the optimiser runs, so the optimiser cannot exploit a bug before the guard sees it. The others remove the guards a local rule proves redundant (an access inside a local or a global at a known offset, an access a dominating guard on the same pointer already covered with nothing between that may free, nearby accesses merged into one guard, a loop whose whole range is checked once before it runs) and expand the rest into a three-instruction check of the shadow that calls the runtime only when a byte is not zero. Array indexes are also checked against their declared bounds, and fortified library calls against the object size they carry. `-fweavec-checks=verify` keeps every removed guard as a monitor, so a wrong removal is caught in testing. ## Using it as the compiler @@ -125,7 +85,7 @@ make CC=weavec-cc cmake -S . -B build -DCMAKE_C_COMPILER=weavec-cc ``` -Every link adds the runtime (`libweavec_rt.a`) and its allocator (`libweavec_alloc.a`). On Darwin one runtime serves the whole process, and its allocator also serves the C library's own allocations (`strdup`, `getline`, `asprintf`). `weavec-cc` builds without the runtime, and without guards, under a sanitizer that replaces the allocator, with `-ffreestanding` or `-nostdlib`, and on targets other than 64-bit Darwin and Linux, and prints a note at the link. A program that defines `malloc` itself keeps its allocator, and its heap is untracked. +Every link adds the runtime (`libweavec_rt.a`) and its allocator (`libweavec_alloc.a`). On Darwin one runtime serves the whole process, and its allocator also serves the C library's own allocations (`strdup`, `getline`, `asprintf`). `weavec-cc` builds without the runtime, and without guards, under a sanitizer that replaces the allocator, with `-ffreestanding` or `-nostdlib`, and on targets other than 64-bit Darwin and Linux, and prints a note at the link. A program that defines `malloc` itself keeps its allocator, and its heap is untracked. On optimising glibc builds `weavec-cc` adds `-D_FORTIFY_SOURCE=3` unless the command line chooses a level. `-fweavec-checks=` selects what a failed guard does: @@ -143,32 +103,35 @@ Other useful flags and variables: | `-fno-weavec` | Compile as plain Clang. | | `-fno-weavec-zero-init` | Do not zero-initialise locals. | | `-fweavec-ledger=/`, `-fweavec-summary` | Write the enforcement ledger per unit; print `weavec: file.c: 812 accesses: 431 proven, 381 guarded, 0 unguarded`. | -| `-fweavec-diagnose` | Run the analysis and print its findings as warnings (`-Werror=weavec` makes them errors). | -| `WEAVEC_RT_STATS=1` | Print the runtime's counters when the program exits. | +| `-fno-weavec-asan-compat` | Do not compile as AddressSanitizer would for the source. | +| `WEAVEC_RT_STATS=1` | Print the runtime's counters (allocations, releases, sweeps and their pauses) when the program exits. | | `WEAVEC_RT_REPORT_LOG=` | Write reports to a file instead of stderr. | -| `WEAVEC_RT_QUARANTINE=` | Change the quarantine's size. | +| `WEAVEC_RT_BACKTRACE=1` | Print the stack after a report that stops the program. | +| `WEAVEC_RT_QUARANTINE=` | Change how much released memory waits before a sweep (16 MiB by default, and at least a quarter of the live heap). | +| `WEAVEC_RT_SWEEP_SIGNAL=` | On Linux, the signal that stops threads for a sweep (`SIGPWR` by default). | -`weavec-cc --help-weavec` lists every option; the [command-line reference](docs/pages/reference/cli.md) documents both tools in full. - -### Analysis without building - -The `weavec` tool analyses without producing binaries. `weavec file.c --` checks one file; `weavec --whole-program -p build/` analyses every source in a compilation database as one program, and `weavec --whole-program a.c b.c -- -Iinclude` the given sources. A definite finding is an error and the tool exits with status 1, so it can gate CI. +`weavec-cc --help-weavec` lists every option; the [command-line reference](docs/pages/reference/cli.md) documents them in full. ## What WeaveC guarantees -For a translation unit compiled by `weavec-cc` in an enforcing mode (`trap` or `verify`), with the runtime linked, every memory access outside a `WEAVEC_UNSAFE` region satisfies: +For a program whose every unit is compiled by `weavec-cc` in an enforcing mode (`trap` or `verify`), with the runtime linked, every memory access outside a `WEAVEC_UNSAFE` region satisfies: - **(A) Accesses.** A load, store, atomic operation or memory intrinsic executes only if every byte it touches is *addressable* when it runs, and so does each memory argument of a call to a function in the library table; otherwise the program traps first. - **(B) Bounds.** An index into an array whose bound its type declares (other than a trailing array of a struct) is within that bound, or the program traps. - **(R) Releases.** A release through the runtime's allocator releases null or the start of a live heap object, or the program stops. +- **(T) Temporal.** A byte of a released heap block is not addressable until a sweep has found no pointer to the block. An access through a pointer the program held at the release traps, and so does a second release. +- **(F) Fortified sizes.** A fortified library call writes no more than the object size it was given, or the program traps. +- **(P) Poisoning.** A byte the program poisoned through ASan's interface is not addressable until it unpoisons it. -A byte is not addressable when it lies after a heap object in its slot, in a released and quarantined block, in a redzone of a tracked local or global, in a local outside its scope, or in the lowest 64 KiB. Every other byte is addressable. So, deliberately, these pass: an access that jumps over a redzone into another live object; a use of a freed block after the quarantine recycled it; a use of a local after its function returned; an overflow from one field of a struct into the next by pointer arithmetic or a library call; accesses to memory the runtime does not track (other allocators, `mmap`, string literals); wide-string library arguments; and code that `weavec-cc` did not compile. The guarantee assumes that untracked memory is accessed correctly, that library functions access only what the library table says, that there are no data races between a guard and its access, and that the program does not write the runtime's shadow. The analysis is not part of it. +A byte is not addressable when it lies after a heap object in its slot, in a released block, in a redzone of a tracked local or global, in a local outside its scope, or in the lowest 64 KiB. So, deliberately, these pass: an access that jumps over a redzone into another live object; a use of a local after its function returned; a pointer jump from one field of a struct into the next by arithmetic; an access through a pointer the program hid from the sweep (stored as a transformed integer, XOR-linked, held only by the kernel); accesses to memory the runtime does not track (other allocators that do not poison for ASan, `mmap`, string literals); and code that `weavec-cc` did not compile. Jumps over a redzone and use after return are the next RFC's. The guarantee assumes that untracked memory is accessed correctly, that library functions access only what the library table says, that there are no data races between a guard and its access, and that the program does not write the runtime's shadow. -The full statement is in [the guarantees reference](docs/pages/reference/guarantees.md) and [RFC 0035, *Soundness*](docs/rfcs/0035-guard-by-default.md#soundness). +The full statement is in [the guarantees reference](docs/pages/reference/guarantees.md) and [RFC 0036, *Soundness*](docs/rfcs/0036-deterministic-heap-reuse.md#soundness). ## Status -RFC 0035 replaced the analysis-driven enforcement of RFCs 0030–0034 with guards on every access. Measured on its final tree (the RFC's *Measured gates* has the details): ten C projects WeaveC was never tuned on (git, tcpdump, libtiff, BearSSL, YARA, gawk, libarchive, Tcl, json-c, MicroPython) build with their own build systems and `CC=weavec-cc` and pass their test suites (json-c with its own option against a deliberate over-read, MicroPython with a one-line change that lets Clang see the `no_sanitize_address` on its stack scan, as ASan needs too); the default build stops more of a set of 64 blind bug programs than AddressSanitizer (60 against 58), with no false stop on their fixed twins; and every measured workload runs at or below ASan's cost, typically 1.3–2.3 times the reference compiler's (Lua 2.2x, zlib 1.3x, cJSON 1.8x, against ASan's 2.5x, 1.4x and 3.1x). A program starts about 1.2 ms later than without WeaveC. Interpreters and loops over pointers whose exit depends on the data pay the most: measure on your own workload. Ownership enforcement returns in RFC 0036, as contracts checked at function entry. +RFC 0035 put a guard on every access. Measured at v0.16.0, before RFC 0036: ten C projects WeaveC was never tuned on (git, tcpdump, libtiff, BearSSL, YARA, gawk, libarchive, Tcl, json-c, MicroPython) built with their own build systems and `CC=weavec-cc` and passed their test suites; CPython 3.13.7, SQLite 3.50.4 and GNU coreutils 9.5 built with configure results identical to the reference compiler's; and the measured workloads ran at or below AddressSanitizer's cost (SQLite's `speedtest1` 1.55× the reference against ASan's 1.81×, Lua 2.10× against 2.73×), with peak memory 1.2–1.5× (ASan 2.4–6×). + +RFC 0036 makes use after free and double free deterministic (reuse only after a sweep), keeps fortified sizes, honours ASan's contract and lets large frames share space between locals. Measured on darwin-arm64 (its [measured gates](docs/rfcs/0036-deterministic-heap-reuse.md#measured-gates)): CPython 3.13.7, SQLite 3.50.4 and coreutils 9.5 configure as with the reference compiler and pass their suites as it does, but for a deliberate null read in CPython's `test_faulthandler` and four coreutils tests that reach gnulib's word-at-a-time `rawmemchr` over-read, which ASan stops too; every corpus project builds and passes its tests; the corpus benchmarks run at a geometric mean of 1.6× the reference compiler (1.05× v0.16.0; QuickJS 2.4×, because it sends every object to `malloc` when it sees ASan's macros), with peak memory within 1.5× v0.16.0's; and a sweep over 256 MiB of live heap pauses the program for 33 ms. Interpreters and allocation-heavy programs pay the most: measure on your own workload. Linux is not yet measured for this design. ## Building @@ -206,20 +169,19 @@ cmake --build --preset release ctest --preset release cmake --install build/release --prefix "$HOME/.local" export PATH="$HOME/.local/bin:$PATH" -weavec --version weavec-cc --version ``` -Keep the LLVM/Clang installation used for the build available at run time: the compiler records LLVM's resource and executable paths at configure time. Prebuilt portable binaries and package-manager distribution are planned. The [release workflow](docs/development.md#releasing) describes how versions are selected and published. +The install puts `weavec-cc` in `bin/`, and `weavec.h` and the runtime archives under `lib/weavec/`. Keep the LLVM/Clang installation used for the build available at run time: the compiler records LLVM's resource and executable paths at configure time. Prebuilt portable binaries and package-manager distribution are planned. The [release workflow](docs/development.md#releasing) describes how versions are selected and published. ## Documentation | Topic | Where | | --- | --- | | Installation, first check, guides | [weavec.com](https://weavec.com) | -| Annotations and diagnostics | [docs/annotations.md](docs/annotations.md) | | Command-line reference | [docs/pages/reference/cli.md](docs/pages/reference/cli.md) | | Guarantees and scope | [docs/pages/reference/guarantees.md](docs/pages/reference/guarantees.md) | +| Unsafe regions | [docs/pages/guides/unsafe.md](docs/pages/guides/unsafe.md) | | Architecture | [docs/architecture.md](docs/architecture.md) | | Design decisions (RFCs) | [docs/rfcs/](docs/rfcs/) | | Developer guide | [docs/development.md](docs/development.md) | @@ -229,17 +191,13 @@ Keep the LLVM/Clang installation used for the build available at run time: the c ``` include/weavec/ Public C++ headers - Core/ The analysis's model: ownership lattice, pointer kinds, the abstract heap, - summaries, the analysis ledger, the library table, diagnostics (no Clang/LLVM) - Analysis/ Clang AST -> core facts: sites, kinds, slots, the object engine behind one seam - Frontend/ Clang and LLVM integration: the guard passes, unsafe regions, the enforcement - ledger, the analysis action, whole-program analysis, the compiler driver + Core/ The library table's interface (no Clang/LLVM) + Frontend/ The guard passes, unsafe regions, the enforcement ledger, the compiler driver lib/ Implementations, mirroring include/ (lib/Core/LibrarySpec.txt is the library table) -runtime/ The C runtime: the shadow memory, the allocator, guards' slow paths, checked - library calls, reports -tools/weavec/ The analysis tool (libTooling; --whole-program for a compilation database) +runtime/ The C runtime: the shadow memory, the arena allocator and its sweep, the guards' + slow paths, frames and globals, checked library calls, ASan's interface, reports tools/weavec-cc/ The drop-in compiler driver (Clang's driver with WeaveC inside) -resources/ weavec.h, the C-facing annotation header (installed to lib/weavec/include) +resources/ weavec.h, the C-facing header with WEAVEC_UNSAFE (installed to lib/weavec/include) unittests/ GoogleTest unit tests test/ lit + FileCheck integration tests (test/Guards pins the passes and the runtime) cases/ Executable C cases by feature, with markers, run by scripts/run-cases.py @@ -249,7 +207,7 @@ docs/ Architecture, RFCs (docs/rfcs/), roadmap, and the weavec.com s cmake/ Build-system modules ``` -The layering rule is strict: `Core` must not include anything from `clang/` or `llvm/`. `Analysis` is the only layer that knows Core and Clang's AST, and only the object engine behind the `SafetyEngine` seam (`lib/Analysis/Engine*.cpp`) sees its own internals. The runtime is C and includes no WeaveC header but its own. See [docs/architecture.md](docs/architecture.md). +`Core` must not include anything from `clang/` or `llvm/`. The runtime is C and includes no WeaveC header but its own. See [docs/architecture.md](docs/architecture.md). ## Contributing diff --git a/docs/README.md b/docs/README.md index ac798860..2129c6e8 100644 --- a/docs/README.md +++ b/docs/README.md @@ -32,7 +32,7 @@ content. Author new site-specific content in `pages/`. - [Developer guide](development.md) - [Architecture](architecture.md) -- [Annotations and diagnostics](annotations.md) +- [The `weavec.h` header](annotations.md) (`WEAVEC_UNSAFE`) - [RFCs](rfcs/README.md) - [Roadmap](roadmap.md) diff --git a/docs/annotations.md b/docs/annotations.md index 9fcae2a4..b5a0033b 100644 --- a/docs/annotations.md +++ b/docs/annotations.md @@ -1,777 +1,54 @@ -# Annotations reference - -WeaveC annotations live in the `weavec.h` header, which `weavec` and -`weavec-cc` put on the system include path automatically -(`#include `). Every macro expands -to `__attribute__((annotate("weavec.")))` under Clang and to nothing -under compilers without the `annotate` attribute, so annotated code stays -portable C. - -`weavec-cc` guards every memory access at run time whatever the annotations -say ([RFC 0035](rfcs/0035-guard-by-default.md)). `WEAVEC_UNSAFE` is the only -annotation that changes the generated code: the accesses in its function or -block are not guarded and its array indexes are not checked. Every other -annotation is an input to the advisory ownership and bounds analysis, which -runs in `weavec` and under `weavec-cc -fweavec-diagnose`, reports -diagnostics, and never changes the generated code. An annotation the -analysis relies on is not checked at run time: a `WEAVEC_COUNTED_BY(n)` -pointer is guarded against the object it points into, not against `n`. -RFC 0036 (planned) gives the ownership and extent annotations an -enforcement role, as contracts checked at run time. - -| Macro | Applies to | Meaning | -| ----------------- | ------------------------------ | ------------------------------------------------------------------------ | -| `WEAVEC_OWNED` | pointer parameters, returns, variables, fields | The pointer uniquely owns its referent and must release it exactly once. On a struct field: the field owns its referent whenever the struct is live, so freeing the struct without releasing, moving or nulling the field first is a `leak` ([RFC 0007](rfcs/0007-resource-lifecycle.md)). | -| `WEAVEC_BORROWED` | pointer parameters, returns, variables, fields | Shared, read-only borrow. The referent outlives the borrow. | -| `WEAVEC_MUT` | pointer parameters, returns, variables, fields | Exclusive, mutable borrow. | -| `WEAVEC_RAW` | pointer parameters, returns, variables, fields, function-pointer types | No guarantee at all: the checker tracks the pointer, and any dereference, release or transfer of ownership outside a `WEAVEC_UNSAFE` region is an `unsafe-operation`. The only source of raw pointers: a pointer converted from an integer is not raw ([RFC 0033](rfcs/0033-drop-in-by-default.md) §2). `weavec-cc` guards accesses through a raw pointer like any other outside an unsafe region. | -| `WEAVEC_UNSAFE` | function declarations, compound statements | An *unsafe region* ([RFC 0035](rfcs/0035-guard-by-default.md) §2.6): `weavec-cc` does not guard the memory accesses in it and does not check its array indexes. The analysis trusts its raw pointers and its spatial and null operations ([RFC 0030](rfcs/0030-prove-or-trap.md) §6.1). Temporal state is still tracked: possible temporal findings are warnings as elsewhere, and definite violations are still reported. Ownership still flows through it and out of it. The only annotation that changes the generated code. | -| `WEAVEC_NULLABLE` | pointer parameters, returns, variables, fields | The pointer may be null ([RFC 0008](rfcs/0008-pointer-validity.md)). On a parameter: the body must test it before dereferencing it, and callers may pass null. On a return type: callers must test the result. On a variable or field: every load is treated as maybe-null until it is tested. Says nothing about ownership; combine with `WEAVEC_OWNED`/`WEAVEC_BORROWED`/`WEAVEC_MUT` as needed. | -| `WEAVEC_NONNULL` | pointer parameters, returns, variables, fields | The pointer is never null ([RFC 0008](rfcs/0008-pointer-validity.md)). On a parameter: a definitely null argument is a `null-dereference`, even if the body never dereferences it; one that may be null is not reported. On a return type: callers need not test the result. On a variable or field: it is never reported as null. Says nothing about ownership. | -| `WEAVEC_RETAINS` | pointer parameters | The callee takes a reference on the argument's object ([RFC 0010](rfcs/0010-shared-ownership.md)): the caller's pointer gains a *share*, which the next copy of it carries away. On a declaration with no body whose result has the parameter's type and no ownership annotation, the result is a copy of the argument (the shape of `g_object_ref`). | -| `WEAVEC_RELEASES` | pointer parameters | The callee releases one reference ([RFC 0010](rfcs/0010-shared-ownership.md)): the argument's name is dead afterwards; other shares of the object are untouched. | -| `WEAVEC_REFCOUNT` | integer fields | The field is a reference count ([RFC 0010](rfcs/0010-shared-ownership.md)): a share taken through it and never released is a `leak` even when no function in the program releases through it. Inference recognises the field by its increments and decrements regardless. | -| `WEAVEC_OWNED_BY(f)` | pointer parameters and returns, next to `WEAVEC_OWNED` | The release family is `f` ([RFC 0010](rfcs/0010-shared-ownership.md)): `struct handle *handle_open(void) WEAVEC_OWNED WEAVEC_OWNED_BY(handle_close);` makes `free(handle_open())` a `mismatched-release`. Without `WEAVEC_OWNED` it is an `invalid-annotation`. | -| `WEAVEC_SIZED_BY(n)` | pointer parameters, pointer fields | A synonym of `WEAVEC_COUNTED_BY(n)` ([RFC 0030](rfcs/0030-prove-or-trap.md) §7.2): `n` counts elements (bytes for `void` and character pointees), unlike Clang's byte-counting `sized_by`. It keeps its [RFC 0011](rfcs/0011-spatial-safety.md) and [RFC 0012](rfcs/0012-spatial-safety-strings-and-fields.md) meaning (`void fill(char *WEAVEC_SIZED_BY(len) buf, size_t len);`, `struct buf { char *WEAVEC_SIZED_BY(cap) data; size_t cap; };`): inside the body the parameter has that extent, so `buf[len]` is `out-of-bounds`; a call with a smaller object is `out-of-bounds`; a store into the field of an object the count says is too small is an `annotation-mismatch`. New code should use `WEAVEC_COUNTED_BY`. On a non-pointer, or naming nothing or a non-integer: `invalid-annotation`. | -| `WEAVEC_COUNTED_BY(n)` | pointer parameters, pointer fields | At least `n` elements (bytes for `void` and character pointees) are accessible from the pointer: the kind `counted(n)` (`sized(n)`) of [RFC 0030](rfcs/0030-prove-or-trap.md) §7.2. `n` names a sibling parameter or field by name, in any position, so the count may follow the pointer (`void fill(char *WEAVEC_COUNTED_BY(len) buf, size_t len);`, `struct buf { char *WEAVEC_COUNTED_BY(cap) data; size_t cap; };`). The analysis checks the kind at every call and every store and proves accesses through the pointer against it; `weavec-cc` guards those accesses against the object the pointer points into, not against `n`. A name that resolves to nothing or to a non-integer is an `invalid-annotation`, and the kind is dropped. | -| `WEAVEC_ENDED_BY(q)` | pointer parameters, pointer fields | `[p, q)` lies in one object, `q` naming a sibling pointer parameter or field by name, in any position: the kind `ended-by(q)` ([RFC 0030](rfcs/0030-prove-or-trap.md) §7.2). `size_t sum(const int *WEAVEC_ENDED_BY(end) p, const int *end);`. A name that resolves to nothing or to a non-pointer is an `invalid-annotation`. | -| `WEAVEC_STRING` | pointer parameters, returns, fields | The pointer is NUL-terminated within its object (a zero element lies at or after it): the kind `nul-terminated` ([RFC 0030](rfcs/0030-prove-or-trap.md) §7.2). `size_t name_len(const char *WEAVEC_STRING name);`. On a non-pointer or a variable: `invalid-annotation`. | -| `WEAVEC_ASSUME(expr)` | statements | `expr` holds from here on, as if the code below were inside `if (expr)` ([RFC 0012](rfcs/0012-spatial-safety-strings-and-fields.md)): `WEAVEC_ASSUME(b->len < b->cap);` lets the checker prove `d[b->len]` in bounds on `malloc(b->cap)`. A statement to the analysis only ([RFC 0035](rfcs/0035-guard-by-default.md), amendment 11): when the analysis refutes `expr`, that is a `contradicted-assumption`; otherwise it assumes `expr` after it. Nothing checks it at run time, and no guard depends on it. `expr` must be side-effect free; it is evaluated under WeaveC and is an unevaluated operand (`sizeof`) under every other compiler. | -| `WEAVEC_ENABLED` | (macro, not an attribute) | `1` when the TU is being processed by `weavec` or `weavec-cc` (both define `__WEAVEC__`), else `0`. | - -Annotations on a function-pointer type describe whatever is called through it -([RFC 0004](rfcs/0004-unsafe-boundaries.md)): - -```c -typedef void (*dtor_t)(struct node *WEAVEC_OWNED); /* every call consumes */ -typedef WEAVEC_OWNED struct node *(*maker_t)(void); /* every call allocates */ -struct ops { - void (*drop)(struct node *WEAVEC_OWNED); /* fields too */ - void *(*WEAVEC_OWNED alloc)(size_t); /* result of alloc */ -}; -``` - -An annotation on the declarator of a function pointer (the typedef name, the -field or the parameter) describes the *result* of calls through it; the -annotations inside its parameter list describe the arguments. - -Without a type contract, a call through a function pointer uses the functions -the program stores into that pointer's *slot* (a field, global, parameter or -result; [RFC 0030](rfcs/0030-prove-or-trap.md) §9.3): one known target is -analysed as a direct call, several as the join of their summaries. A -same-type function elsewhere in the program does not provide an unknown -callback's behavior. A slot that can also receive values from outside the -program keeps its known targets for temporal facts, and the sites that -depend on them count as trusted; a slot with no known target is treated as -an unknown callee. `weavec --whole-program` solves the slots across units. - -## Placement - -Attributes bind to the declarator they precede, so put them immediately before -the name for parameters and variables: +# The weavec.h header + +`weavec.h` declares the one annotation WeaveC has: `WEAVEC_UNSAFE`. +`weavec-cc` puts the header on the system include path +(`#include `) and defines `__WEAVEC__` to `1` in every unit it +compiles with WeaveC enabled. The header is at version 1.0 +(`WEAVEC_H_VERSION_MAJOR`, `WEAVEC_H_VERSION_MINOR`). + +`WEAVEC_UNSAFE` expands to `__attribute__((annotate("weavec.unsafe")))` +under compilers that understand Clang's `annotate` attribute and to nothing +under the others, so code that uses it stays buildable with any C +toolchain. Copy the header into a project that must also build without +WeaveC installed. + +| Form | Placement | Meaning | +| ---- | --------- | ------- | +| `WEAVEC_UNSAFE void f(…) { … }` | before a function definition | The function's body is an *unsafe region*. | +| `WEAVEC_UNSAFE { … }` | before a compound statement | The block is an unsafe region. | + +In an unsafe region `weavec-cc` does not guard memory accesses and does not +check array indexes ([RFC 0035](rfcs/0035-guard-by-default.md) §2.6, +amendment 8). A region is decided by source location: code inlined into it +from elsewhere keeps its guards, and the runtime's allocator still validates +every `free` made in it. The enforcement ledger records each access in a +region as `unguarded` with the reason `unsafe`. ```c -struct buffer *WEAVEC_OWNED buffer_new(size_t n); /* return value */ -void buffer_free(struct buffer *WEAVEC_OWNED b); /* consumes b */ -size_t buffer_len(const struct buffer *WEAVEC_BORROWED b); /* borrows b */ -void buffer_push(struct buffer *WEAVEC_MUT b, int v); /* mutably */ +#include +#include -int *WEAVEC_OWNED p = malloc(sizeof *p); - -struct node *WEAVEC_BORROWED WEAVEC_NULLABLE find(int key); /* may return NULL */ -int node_value(const struct node *WEAVEC_BORROWED WEAVEC_NONNULL n); -``` - -For functions, `WEAVEC_UNSAFE` goes before the declaration: - -```c -WEAVEC_UNSAFE void poke_hardware(volatile uint32_t *reg) { *reg = 1; } -``` - -For blocks, before the opening brace: +WEAVEC_UNSAFE void write_register(uintptr_t address, uint32_t value) { + *(volatile uint32_t *)address = value; +} -```c -void reset_device(uintptr_t base) { +unsigned long first_word(const char *s) { + unsigned long word; WEAVEC_UNSAFE { - /* base is the device's mapped register block. */ - *(volatile uint32_t *)base = 1; + word = *(const unsigned long *)s; /* may read past the string on purpose */ } + return word; } ``` -In a `weavec-cc` build, the accesses whose source location lies inside the -region are not guarded and its array indexes are not checked -([RFC 0035](rfcs/0035-guard-by-default.md) §2.6). Code inlined into the -region keeps its own location, so a guarded function inlined there stays -guarded. A function its author keeps from AddressSanitizer -(`__attribute__((no_sanitize("address")))`, `no_sanitize_address` or -`disable_sanitizer_instrumentation`) is unguarded the same way; the -analysis treats it as ordinary code. The runtime's allocator still -validates every `free` in a region. - -For the analysis, an unsafe region is a boundary, not a hole -([RFC 0030](rfcs/0030-prove-or-trap.md) §6.1). It still analyses what happens -inside it: a `free` inside the block is a free as far as the code after it -is concerned, and the function's summary is still inferred. Inside the -region, spatial and null operations and raw pointers are trusted (counted as -trusted in the summary line). Nothing is suppressed: temporal findings are -reported as anywhere else, and definite violations are still reported. The -region is also the only place a `WEAVEC_RAW` pointer may be dereferenced, -released or handed to an owning parameter, and the place to assert what a -raw pointer really is: - -```c -struct node *WEAVEC_OWNED node_from_handle(uintptr_t h) { - WEAVEC_UNSAFE { return (struct node *)h; } /* asserts: this is owned */ -} - -void stash(struct node *WEAVEC_OWNED n) { - WEAVEC_UNSAFE { registry[key] = (uintptr_t)n; } /* ownership leaves the model */ -} -``` - -Outside an unsafe region the same statements are `unsafe-operation` errors, -but the asserted kind still holds afterwards, so a single error replaces a -cascade. See [RFC 0004](rfcs/0004-unsafe-boundaries.md), *Laundering*. - -### Where raw pointers come from - -- a declaration annotated `WEAVEC_RAW` (parameters, variables, fields, - results and function-pointer results), the only source of raw pointers - ([RFC 0033](rfcs/0033-drop-in-by-default.md) §2); -- a load through, or arithmetic on, a raw pointer (`raw->next` is raw too); -- the result of a callee whose body returns or stores a raw value, or whose - declaration says `WEAVEC_RAW`. - -Rawness is a *must* fact: a value that is raw on some paths only, or a -cell that holds raw and non-raw values, is not raw; its accesses are not -proven, never an error. A pointer converted from an integer -(`(struct node *)h`, `(void *)(uintptr_t)n`) is not raw either: it is a -pointer of unknown provenance, whose accesses are not proven. Inside an -unsafe region they are trusted. `weavec-cc` guards -all of them outside an unsafe region, as it does every access. - -Copying, comparing and converting a raw pointer back to an integer are fine -anywhere; passing it to a callee's `WEAVEC_RAW` parameter is fine too. Only a -dereference, a release, a move into an owning parameter, or a borrow by a -callee that reads or writes through it is a *raw operation*. - -## Diagnostics - -The diagnostics come from the advisory analysis -([RFC 0035](rfcs/0035-guard-by-default.md) §8), which runs in two places: - -- `weavec file.c -- ` (and `weavec --whole-program`) reports each - finding with the severity in the table below and exits with status 1 when - there is an error; -- `weavec-cc -fweavec-diagnose` analyses each unit as it compiles it and - reports every finding as a warning (`-Werror=weavec` makes them errors); - `leak` is off there unless `-Wweavec-leak` turns it on. - -Neither changes the generated code. Every WeaveC diagnostic ends with a -stable identifier in brackets, e.g. `[weavec::use-after-free]`. Severity -follows certainty ([RFC 0030](rfcs/0030-prove-or-trap.md) §3): a finding -that holds on every path is an error, and a temporal finding that holds on -some paths only is a warning with "may" wording. A null dereference or -out-of-bounds access that is only possible is not diagnosed: the access is -counted as not proven in the summary line. - -The reports a `weavec-cc` build prints when a guard fails at run time -(`weavec: heap-use-after-free at ...`) are not diagnostics and have no -identifiers ([RFC 0035](rfcs/0035-guard-by-default.md) §5.3). - -| Identifier | Severity | Emitted when | -| --------------------- | -------- | --------------------------------------------------------------------- | -| `use-after-free` | error / warning | A pointer (or any alias of it) is used after being passed to `free`. An error when the release happened on every path to the use; otherwise a warning with "may" wording ([RFC 0030](rfcs/0030-prove-or-trap.md) §3.1): `use of '

' after it may have been freed`, note `freed here on some paths`. Note: `freed here` / `freed here (through '')`. After a share release ([RFC 0010](rfcs/0010-shared-ownership.md)): `use of '

' after its reference was released` (`... may have been released`), note `reference released here`. A release by code WeaveC cannot see (an unknown callee, inline assembly) is never reported: the use counts as not proven ([RFC 0030](rfcs/0030-prove-or-trap.md) §5.1). | -| `double-free` | error / warning | A pointer (or any alias of it) is passed to `free` twice without reassignment: `'

' is freed twice`, or, when the first release happened on some paths only, `'

' may be freed twice` ([RFC 0030](rfcs/0030-prove-or-trap.md) §3.1). Note: `previously freed here [on some paths] [(through '')]`. Two share releases of one name ([RFC 0010](rfcs/0010-shared-ownership.md)): `'

' is released twice` (`may be released twice`), note `previously released here`. | -| `use-after-move` | error / warning | A pointer is used after being passed to a `WEAVEC_OWNED` parameter, to `realloc`, or to a function that moves it (on every path, or on the paths whose result the caller has not ruled out; [RFC 0006](rfcs/0006-precision.md)). Note: `moved here`. On some paths only: `use of '

' after it may have been moved`, a warning ([RFC 0030](rfcs/0030-prove-or-trap.md) §3.1). | -| `conflicting-borrow` | warning | An object is freed or moved while a live pointer into it exists: `cannot free '

' while it is borrowed`, `cannot move '

' while it is borrowed`. Note: `borrowed by '' here`. A pointer is *live* until its last use ([RFC 0006](rfcs/0006-precision.md)). Not reported for a pointer *derived* from `

` (`q = &p->f`, `q = p + 1`): that is a name for the same object, and its later use is a `use-after-free` ([RFC 0011](rfcs/0011-spatial-safety.md)). Always a warning: the release accesses nothing through the copy, and a later use of the copy is a `use-after-free` ([RFC 0033](rfcs/0033-drop-in-by-default.md) amendment 1). RFC 0030 removed `--exclusive-borrows`, the opt-in to RFC 0001's full exclusivity rules. | -| `lifetime-too-short` | error / warning | A pointer may outlive what it points to: `'

' may outlive '', which it points to` (stored into an outer scope, a global or through a parameter) or `returned pointer may outlive '', which it points to`. Notes: where `` is declared and where it goes out of scope. Reported at the store, but decided when `` dies ([RFC 0011](rfcs/0011-spatial-safety.md)): a store undone before then (`ls->fs = fs.prev`), or into a holder that is dead by then, is not reported. `alloca` storage belongs to the frame ([RFC 0030](rfcs/0030-prove-or-trap.md) §8.2), so returning it is reported here too. An error when the escape happens on every path, otherwise a warning ([RFC 0030](rfcs/0030-prove-or-trap.md) §3.4). | -| `unsafe-operation` | error | A raw operation outside a `WEAVEC_UNSAFE` region ([RFC 0004](rfcs/0004-unsafe-boundaries.md)): `dereference of raw pointer '

' outside an unsafe region`, `'' dereferences raw pointer '

' ...` (also `releases`, `takes ownership of`), `raw pointer '

' is assigned to '', which is declared WEAVEC_OWNED, outside an unsafe region` (any safe annotation), `raw pointer is returned from a function whose return type is annotated WEAVEC_OWNED outside an unsafe region`. [RFC 0030](rfcs/0030-prove-or-trap.md) §16 removed the `--strict-externs` forms (`unchecked call to '' outside an unsafe region`, `unchecked call through '' ...`) along with the flag; the uses after a call to an unknown callee count as not proven instead (§5.1). [RFC 0033](rfcs/0033-drop-in-by-default.md) §2 removed the forms for pointers converted from integers, which are no longer raw: their accesses are not proven, and only a pointer declared `WEAVEC_RAW` (or loaded through, derived from or handed out as one) is raw, on every path. Notes: why the pointer is raw (`'

' is raw: declared WEAVEC_RAW here`, `loaded through raw pointer '' here`, `handed out by '' here`, each optionally `(through '')`) and `move this operation into a WEAVEC_UNSAFE block or function, or assert the pointer's ownership first`. | -| `mismatched-release` | error / warning | A resource is released (or moved into a consuming parameter) by a function of another release family ([RFC 0007](rfcs/0007-resource-lifecycle.md)): `'

' is released with 'free' but must be released with 'fclose'`. Both names are family names, the canonical releaser of the allocator (`malloc`/`strdup`/`realloc` → `free`, `fopen` → `fclose`, `opendir` → `closedir`, ...), even when the release went through a wrapper defined in the program. Note: `allocated here`. | -| `leak` | warning | An owned resource is lost without being released, moved or stored where the caller can see it ([RFC 0007](rfcs/0007-resource-lifecycle.md)): `'

' is leaked` at the point its last holder goes out of reach (a `return`, a scope end, the statement after its last use); `'

' is leaked: it is overwritten without being released` at the assignment; `'->p' is leaked when '' is freed` (also `'*a' is leaked when 'a' is freed`) at the release of a container whose `WEAVEC_OWNED` field, or a field this function stored an owned value into, still owns something; `result of '' is leaked` at a discarded allocating call. Notes: `allocated here`, `'

' is declared WEAVEC_OWNED here` for a parameter or field, or `reference taken here` for a share retained by a count increment and dropped ([RFC 0010](rfcs/0010-shared-ownership.md); reported only through a *known count*: a field some function in the program releases through, or one annotated `WEAVEC_REFCOUNT`). Not reported: pointers handed to callees the checker cannot follow or cast to integers (they are *escaped*), resources kept by globals or `static` locals when the function returns, blocks that end in a `noreturn` or `exits` call, what is still held at a `return` from `main` ([RFC 0030](rfcs/0030-prove-or-trap.md) §8.4: the process is ending, so it is not a leak), fields of an object this function allocated, and the old block after a failed in-place `realloc`. A warning whatever the certainty ([RFC 0030](rfcs/0030-prove-or-trap.md), *Diagnostics*); `-Werror=weavec-leak` still promotes it. Off by default under `weavec-cc -fweavec-diagnose` ([RFC 0033](rfcs/0033-drop-in-by-default.md) §8): `-Wweavec-leak`, `-Werror=weavec-leak` or `-Wweavec` turns it on; `weavec` reports it by default. | -| `null-dereference` | error | A pointer that is null on every path reaching here is dereferenced ([RFC 0008](rfcs/0008-pointer-validity.md)): `dereference of '

', which is null`; or passed to a callee whose declaration or library entry requires it non-null: `'

', which is null, is passed to '', which dereferences it` (also `a null pointer is passed to '' ...`). The note says why: `'

' is assigned NULL here`, `'

' is declared WEAVEC_NULLABLE here` / `the result of '' is declared WEAVEC_NULLABLE here`; for a call, also `'' is declared here`. [RFC 0030](rfcs/0030-prove-or-trap.md) §3.2 removed the "may be null" forms: a dereference of a pointer that may be null is not proven and not reported, and an allocation's result used untested is `allocation-failure` (off by default). | -| `use-of-uninitialized`| error | A pointer variable, or a pointer field of a record variable, declared without an initialiser is read, dereferenced, copied or released before it is assigned ([RFC 0008](rfcs/0008-pointer-validity.md)): `use of '

' before it was initialized` (also `'.f'`). Note: `'

' is declared here`. Any assignment, a callee's store (`init(&p)`), a mutable borrow for a call, `memset` or a whole-object write initialises it; `static` and address-taken variables are not tracked. Reported only when it holds on every path ([RFC 0030](rfcs/0030-prove-or-trap.md), *Diagnostics*): a use that is uninitialised on some paths only is defined by zero-initialisation, which `weavec-cc` turns on and the analysis models (`weavec --no-zero-init` models `-fno-weavec-zero-init`), and produces no diagnostic. | -| `invalid-release` | error / warning | A releaser (or a consuming parameter) is handed a pointer that is not the start of a heap allocation ([RFC 0008](rfcs/0008-pointer-validity.md)): `'

' is released but points to '', which is not a heap object` (a stack or static variable, an array, a field of one; `'' is released but is not a heap object` when `

` is `` itself), `'

' is released but points to a string literal`, `'

' is released but points 4 elements past the start of its allocation` / `points to field 'in' of its allocation` / `does not point to the start of its allocation` (`p + 1`, `strchr(p, c)`, `p++`, `&o->in`; the offset is named when the checker knows it, [RFC 0011](rfcs/0011-spatial-safety.md)). Notes: `'' is declared here` / `allocated here`. When that holds on some paths only, a warning ([RFC 0030](rfcs/0030-prove-or-trap.md) §3.1): `'

' is released but may point to , which is not a heap object`, and for an interior pointer at an unknown offset `'

' is released but may not point to the start of its allocation`. A release the callee may not perform — one it makes only on some result class, under a guard on the arguments, or from a widened case ([RFC 0030](rfcs/0030-prove-or-trap.md) §3.4, §9.1) — makes the finding possible whatever the argument is, which is where the `may be released` forms come from: `a string literal may be released`, `'' may be released but is not a heap object`. | -| `out-of-bounds` | error | An access reaches past the object it is in, or before its start, on every value the facts allow, against an exact extent ([RFC 0011](rfcs/0011-spatial-safety.md); RFC 0030 §3.3: the `may be out of bounds` and `may access past the end` forms of the earlier RFCs are gone — those accesses are not proven, and produce no diagnostic). Direct accesses: `'

[]' is out of bounds: index of an object of bytes` (both constant; the index is spelled as written, with its folded value in parentheses when that differs), `'

[]' is out of bounds: '' is the number of elements of '

'` / `'' is at least '', the number of elements of '

'` / `'' is above '', ...` (the index related to the count by a condition), `'

[]' is out of bounds: it reaches bytes into '

', which has bytes` (an index the facts pin only through a relation), `'

[]' is out of bounds: index is before the start of '

'` (`it lies before the start of '

'` when no index is named). Library calls with a buffer and a length (`memcpy`, `memmove`, `memset`, `memcmp`, `fgets`, `snprintf`, `read`, `write`, `strncpy`, ...): `'memcpy' accesses 16 bytes of '

', which has 8 bytes` and the relational form `'memset' accesses 'm' bytes of 'p', which has 'n' bytes ('m' is above 'n')`. A callee's requirement at the call: `'put7' requires 8 bytes behind '

', which has 4 bytes`. Three further definite forms ([RFC 0030](rfcs/0030-prove-or-trap.md), *Diagnostics*): `write through '

', which points to a string literal`; `format string of '' reads arguments but are passed`; and `'' copies bytes between overlapping ranges of ''`, note `'' is declared here`. Notes: `'

' is allocated here` / `'

' is declared here` / `the object behind '

' is declared here`. Extents come from allocations (`malloc(n)`, `calloc(n, sz)`, `realloc(p, n)`, and every function in the program that returns one: `xmalloc(n)` returns `fresh extent=n`), from the declared size of a variable, an array or an array member, from string literals, from `WEAVEC_SIZED_BY` on a parameter, and from the count of a sized field ([RFC 0012](rfcs/0012-spatial-safety-strings-and-fields.md)), declared or inferred (`'b->data[b->cap]' is out of bounds: 'b->cap' is the number of elements of 'b->data'`, note `'b->data' is declared here`). Strings (RFC 0012): a copy that needs the length plus the terminator is checked like a length (`'strcpy' accesses 6 bytes of 'buf', which has 4 bytes`, `'strcpy' accesses 'strlen(s)' + 1 bytes of 'd', which has 'strlen(s)' bytes` on `malloc(strlen(s))`, `'strcat' accesses 5 bytes of 'buf', which has 4 bytes` counting what `buf` already holds, `'sprintf' accesses at least 5 bytes of 'buf', which has 4 bytes` from the format's minimum), and a terminator-seeking read (`strlen`, `strcpy`'s source, `strcat`'s, `puts`, `printf("%s")`) of an object the checker knows has no terminator (`strncpy` that filled it, `char a[4] = "abcd"`, `memset(a, 'x', sizeof a)`) is `'strlen' reads past the end of 'name', which is not NUL-terminated`, note `'name' is left without a terminator here`. Relations one step further (RFC 0012): `'buf[i]' is out of bounds: 'i' is at least 8 in an object of 8 bytes` under `i >= 8`. RFC 0017 also checks actual converted or wrapped allocation sizes, represented products, VLA dimensions and allocated flexible-array tails; a dimension violation can say `'' is out of bounds for its variable array dimension`, and a caller interval can say `'' requires '

' before its start`. Unknown indices, unknown sizes or offsets, unsupported field layouts and unknown string lengths do not by themselves produce this error or establish safety. | -| `invalid-integer-operation` | error | A definitely invalid supported integer operation ([RFC 0017](rfcs/0017-c-integer-semantics-and-spatial-safety.md)). Message: `invalid integer operation: `, at the source operation. Reasons: `signed integer overflow`, `division by zero`, `signed division overflow`, `invalid shift count`, `invalid signed left shift`, and `nonpositive variable array dimension`. Possibly invalid arithmetic is conservative, without a definite-error claim or using undefined behavior to discard a reachable path. | -| `annotation-mismatch` | error | A definition contradicts its own annotation: `'

' is annotated WEAVEC_BORROWED but is freed here` (also `WEAVEC_MUT`; also `moved`, `written through`; also `... but '

->f' is freed here` for a path under the parameter), `function returns a borrow but its return type is annotated WEAVEC_OWNED`, `function returns a fresh allocation but its return type is annotated WEAVEC_BORROWED` (or `WEAVEC_MUT`). Notes: `'

' is annotated here` / `annotated here`; `'' is a copy of '

'` when through an alias. Callers keep trusting the annotation. A store into a `WEAVEC_SIZED_BY(g)` field of an object smaller than `g` says ([RFC 0012](rfcs/0012-spatial-safety-strings-and-fields.md)): `'b->data' is declared WEAVEC_SIZED_BY(cap) but is given 4 bytes where 'b->cap' says 8` (at the store, or at the write to the count when that comes second; `says at least 8` under a lower bound, `says 'n' elements of 4 bytes` for a non-byte element), note `'b->data' is declared here`. An object of unknown size, a larger one, or null is not reported. `weavec --whole-program` adds a form of its own, which checks each declaration against the definition in another file ([RFC 0030](rfcs/0030-prove-or-trap.md) §13.2): `'' is declared here but its definition in '' ''`, note `defined here`. | -| `invalid-annotation` | warning | A `weavec.*` annotation WeaveC does not recognise (including `weavec.checked`, which [RFC 0030](rfcs/0030-prove-or-trap.md) removed), a `WEAVEC_COUNTED_BY(n)`, `WEAVEC_SIZED_BY(n)` or `WEAVEC_ENDED_BY(q)` whose name resolves to no sibling parameter or field or to one of the wrong type (`'n' in WEAVEC_COUNTED_BY does not name a parameter or field`, `'q' in WEAVEC_COUNTED_BY is not an integer parameter or field`, `'n' in WEAVEC_ENDED_BY is not a pointer parameter or field`; the kind is dropped), an extent macro or `WEAVEC_STRING` on a non-pointer (`'x' is declared WEAVEC_COUNTED_BY(n) but is not a pointer`) or on a variable, two declared kinds of one parameter or field that disagree (`conflicting kinds for 'p': ...`; the weaker is used, [RFC 0030](rfcs/0030-prove-or-trap.md) §7.2), `WEAVEC_NULLABLE` and `WEAVEC_NONNULL` on the same declaration, `WEAVEC_RETAINS` and `WEAVEC_RELEASES` on the same declaration, `WEAVEC_OWNED_BY(f)` without `WEAVEC_OWNED` ([RFC 0010](rfcs/0010-shared-ownership.md)), `WEAVEC_SIZED_BY(n)` on a non-pointer or naming no integer parameter (`'

' is declared WEAVEC_SIZED_BY(n) but is not a pointer` / `... but 'n' is not an integer parameter`, [RFC 0011](rfcs/0011-spatial-safety.md)), `WEAVEC_SIZED_BY(g)` on a field that is not a pointer or whose `g` is no integer field of the record (`field 'data' is declared WEAVEC_SIZED_BY(cap) but 'cap' is not an integer field of 'struct buf'` / `field 'n' is declared WEAVEC_SIZED_BY(cap) but is not a pointer`, once per unit, when the field is first used), or `weavec.assume` on any function but `weavec.h`'s (`'weavec.assume' is not an annotation for 'f'`, [RFC 0012](rfcs/0012-spatial-safety-strings-and-fields.md)). Reported on definitions. | -| `contradicted-assumption` | error | A `WEAVEC_ASSUME(e)` the analysis refutes ([RFC 0030](rfcs/0030-prove-or-trap.md) §6.2): `assumption '' is false here`, with the note `'' is here` at the source of the fact. Minimal trigger: `char b[4] = {0}; int i = 10; WEAVEC_ASSUME(i < 4); return b[i];`. An assumption the analysis neither proves nor refutes is not reported, and the analysis assumes `e` after it in every case. Nothing checks an assumption at run time ([RFC 0035](rfcs/0035-guard-by-default.md), amendment 11). | -| `allocation-failure` | warning | **Off by default**; `-Wweavec-allocation-failure` (or `-Wweavec`) enables it ([RFC 0030](rfcs/0030-prove-or-trap.md) §3.2, §8.4). The result of an allocating call is used without a null test: `the result of '' is used without a null test; it is null when allocation fails`, note `allocated here`. Minimal trigger: `char *p = malloc(8); p[0] = 1;`. In a `weavec-cc` build the dereference is guarded either way, so a failed allocation traps instead of writing through null. | - -Removed identifiers: [RFC 0030](rfcs/0030-prove-or-trap.md) removed `analysis-incomplete`, `annotation-required`, `checking-incomplete` and `checking-failed`, and a `-W` flag naming one is an error, `unknown WeaveC diagnostic '' (removed by RFC 0030)`; [RFC 0035](rfcs/0035-guard-by-default.md) removed `unresolved-operation` and `unchecked-operation`, and a `-W` flag naming one is an unknown WeaveC diagnostic. - -The identifiers are defined in `include/weavec/Core/Diagnostic.h` -(`weavec::core::diag`). Renaming one is a breaking change. The rules behind -them are specified by [RFC 0001](rfcs/0001-ownership-model.md) through -[RFC 0017](rfcs/0017-c-integer-semantics-and-spatial-safety.md), as amended -by [RFC 0030](rfcs/0030-prove-or-trap.md), which sets their severities and -adds the last three. [RFC 0035](rfcs/0035-guard-by-default.md) makes -them advisory: they do not change what a build guards. -[RFC 0009](rfcs/0009-value-conditional-behaviour.md) adds no diagnostic; it -shrinks the set of programs that trigger the existing ones. - -Annotation suggestions are not diagnostics: `weavec --dump-kinds` prints -the ones kind inference makes, beside the kinds it inferred. - - -## What the checker understands - -- **Ownership sources**: `malloc`, `calloc`, `realloc`, `strdup`, `strndup`, - `aligned_alloc`, `fopen`, `opendir`, `getaddrinfo`, `asprintf`, `getline`, - `mmap`, ... (the library table `lib/Core/LibrarySpec.txt`, - [RFC 0030](rfcs/0030-prove-or-trap.md) §8, which describes functions from - ``, ``, ``, ``, ``, - ``, ``, ``, ``, ``, - ``, ``, ``, ``, `` and friends, - with their aliases and fortified forms), any function whose return type - carries `WEAVEC_OWNED` or Clang's `malloc`/`ownership_returns` attribute, - and any function defined in the program whose body returns a fresh - allocation. -- **Releases and moves**: `free`, `fclose`, `closedir`, `freeaddrinfo`, - `munmap`, `regfree`, `dlclose`, ..., passing a pointer to a - `WEAVEC_OWNED` parameter, and passing it to a function defined in the - program whose body frees or moves that parameter (through any depth of - wrappers and through recursion). `realloc(p, n)` moves `p`; on the path - where the result is tested null (`if (!q)`, `q == NULL`, ...), `p` is - valid again. -- **Resource lifecycle** ([RFC 0007](rfcs/0007-resource-lifecycle.md)): - every ownership source above puts a resource on the function's books, - with the *release family* of its allocator (`free`, `fclose`, `closedir`, - `freeaddrinfo`, `munmap`, `pclose`, `regfree`, `dlclose`, ...; a - `WEAVEC_OWNED` parameter's family is unknown). It leaves the books when - it is released by a function of the same family, moved into an owning - parameter, returned, stored into caller-visible memory (`*out`, `b->data`, - a global), copied into a struct that is returned or stored, handed to a - callee the checker cannot follow, or cast to an integer. A resource still - on the books when its last holder dies is a `leak`; a release by another - family is a `mismatched-release`. Copies share one record (`q = p; ... - use(q);` reports `q` once), a callee summarised from its body that keeps - nothing is trusted not to retain the argument, and the null edge of a - test of the holder (`if (!p) return -1;`) owns nothing. A constructor - that reports failure through its result (`int make(char **out) { *out = - malloc(n); return *out != NULL; }`) is summarised with the classes on - which `*out` is null, so `if (!make(&s)) return;` is clean. -- **Outcome-conditional consumption** ([RFC 0006](rfcs/0006-precision.md)): - a callee that frees or moves its argument only on the paths that return - some class of value (`NULL` vs non-null, `0` vs positive vs negative) is - summarised per class, and a test of its result selects the class: after - `int rc = try_take(p);` the pointer `p` is gone where `rc == 0` and still - yours where `rc != 0`, if `try_take` frees only when it returns `0`. - Recognised tests: `x`, `!x`, `x == NULL`, `x != NULL`, `x == 0`, `x != 0` - and comparisons with an integer constant (`x < 0`, `x == -1`, `x >= 0`, - ...), on the call itself, on the variable its result was stored in, or - on an assignment inside the condition (`if ((rc = f(p)) < 0)`). Wrappers - (`char *q = realloc(p, n); if (!q) return NULL; return q;`) inherit the - conditional behaviour. Untested, a conditional free is a may-free and the - next use is reported. -- **Calls through function pointers**: the callee's signature is taken from - the annotations on the function-pointer type (typedef, field or parameter) - when it has any; otherwise from the functions the program stores into the - pointer's slot (`ops.drop = node_free;`, a callback argument such as - `qsort(a, n, sz, cmp)`; [RFC 0030](rfcs/0030-prove-or-trap.md) §9.3), so a - call through `ops->drop` frees what `node_free` frees. A slot with no - known target is treated as an unknown callee (below). -- **The program**: with `weavec file.c --` the program is that one file, - as it is for each unit `weavec-cc -fweavec-diagnose` compiles; with - `weavec --whole-program` it is every file analysed together, and a callee - defined in another file is checked from its body there (a definition wins over the library table, so a program - that defines its own `strdup` is checked against its own). A whole-struct - copy `b = a` copies the facts of every pointer field, so `b = a; - free(a.data); b.data[0]` is a use after free. -- **Effects through parameters**: a callee that frees `b->data`, writes - `*out`, or stores a fresh allocation into `*out` or a global has that - effect at the call site. A callee that releases a value and then - reinitialises the place (`free(b->data); b->data = NULL;`, `v->items = - realloc(v->items, n)`) is summarised as `freed,replaced` ([RFC - 0008](rfcs/0008-pointer-validity.md), *Replaced values*): the place - itself is usable afterwards, but every copy of the *old* value the caller - kept (`int *old = v->items; grow(v); old[0]`) is dead. Only what happens - on a path that returns counts (`free(g); exit(1);` is no effect), and a - callee that frees *an element* (`free(history[len])`) says so - (`freed,element`): which element is not the caller's to know, so calling - it twice is not a `double-free`. -- **Struct-by-value results** ([RFC 0008](rfcs/0008-pointer-validity.md)): - a function that returns a record hands the caller its pointer fields - (`stores{result.data = fresh(free)}`), so `struct buf b = make(); ...` - owns `b.data` and must release it. -- **Nullness** ([RFC 0008](rfcs/0008-pointer-validity.md)): the checker - knows when a pointer is null (`p = NULL`) or may be null (the result of - `malloc`, `strchr`, `fopen`, `getenv`, ... or of any function in the - program that can return null; a pointer compared with null whose null - edge merged back). Dereferencing a pointer that is null on every path - from a non-allocator source, or passing it to a callee whose body - dereferences the parameter without testing it, is a `null-dereference` - error; any other dereference that is not proven non-null is not proven - and not reported ([RFC 0030](rfcs/0030-prove-or-trap.md) §3.2). - Every test idiom clears the fact on the non-null edge (`if (!p) return;`, - `if (p && p->x)`, `p ? p->x : 0`, `while ((q = f()) != NULL)`, - `__builtin_expect`), for the pointer and its copies; a callee's outcome - does too (`if (!make(&p)) return -1; p[0]` is clean when `make` returns - `*out != NULL`). Pointers the checker knows nothing about (parameters, - loaded fields, results of unknown callees) are not assumed non-null: their - dereferences are not proven. `WEAVEC_NONNULL` moves the obligation to the - callers: a caller that passes null is reported. Summaries record which parameters a - function requires non-null (`requires{s}`) and the classes on which an - out-parameter is non-null (`notnull{*out}`). -- **Uninitialised pointers** ([RFC 0008](rfcs/0008-pointer-validity.md)): - `char *p;` and the pointer fields of `struct buf b;` hold nothing until - they are assigned; using them first on every path is a - `use-of-uninitialized` error. `weavec-cc` zero-initialises locals, and - the analysis models that, so a pointer that may be uninitialised is null - and its dereference is not proven; under `weavec --no-zero-init` (which - models `-fno-weavec-zero-init`), or when a jump bypasses the declaration, - it holds an unknown value ([RFC 0030](rfcs/0030-prove-or-trap.md) §11). -- **Invalid releases** ([RFC 0008](rfcs/0008-pointer-validity.md)): - `free` (or any releaser, or a consuming parameter) of a pointer to a - stack or static object, a string literal, or the middle of an allocation - (`p + 1`, the result of `strchr`) is an `invalid-release`. -- **Integer facts and guards** ([RFC - 0009](rfcs/0009-value-conditional-behaviour.md)): the checker knows the - class (`zero`, `positive`, `negative`) and, when it can, the exact value - of an integer local, parameter or field: from a constant assigned to it, - from the edges of `if (n == 0)`, `if (n > 0)`, `if (!c)`, `if (n != 3)` - and every comparison with an integer constant, and from the `case` - labels of a `switch`. A free, a move, a held resource or a null pointer - established while such a fact holds is remembered *under* it, and the - edge of a later test that contradicts the fact drops it: `if (c) free(p); - ... if (!c) use(p);`, `switch (n) { case 0: free(p); } ... switch (n) { - case 1: use(p); }` and `char *p = NULL; if (n > 0) p = malloc(n); if (n - <= 0) return -1; ... free(p);` are clean. A branch whose condition the facts contradict - (`int c = 0; if (c) free(p);`) is not taken. Reassigning the integer from - an unknown value forgets the fact and weakens every guard that named it; - supported arithmetic and computed tests also use typed ranges and bounded - expressions ([RFC 0017](rfcs/0017-c-integer-semantics-and-spatial-safety.md)). - Promotions, narrowing, `_Bool` conversion and mixed-sign comparisons follow - the target's C types. Full-width unsigned constants through 64 bits remain - representable: `unsigned x = -1` is `UINT_MAX`, and converting 256 to an - eight-bit `unsigned char` produces zero. A fact about a converted expression - is transferred to its source only when the transformation preserves it. - Unknown or possibly invalid arithmetic cannot justify pruning a path. -- **Argument-conditional summaries** ([RFC - 0009](rfcs/0009-value-conditional-behaviour.md)): a callee whose free, - move, store or returned value depends on a fact about its parameters is - summarised with a `when` guard (`param 1 freed(free) when param 3 zero`, - `store param 0 *.msg = copy param 2 when param 2 nonnull`, `return - fresh(free) when param 3 positive|negative`), on a parameter, a field or - dereference below one, or a global. At the call the guard is translated - to the arguments (a constant or `NULL` argument decides it on the spot; a - variable, a cast of one or `n * 8` is looked up in the caller's facts) - and the effect is applied only when it is not refuted: `l_alloc(ud, p, - 8, 0)` frees `p` and returns null, `l_alloc(ud, p, 8, 16)` does not free - and returns a fresh block, `b.noalloc = 1; release(&b); use(b.data)` is - clean when `release` frees only `if (!b->noalloc)`, and `gz_error(s, err, - NULL)` stores nothing. What survives translation stays attached to the - record in the caller, where a later test may still refute it. -- **Inferred `noreturn`** ([RFC - 0009](rfcs/0009-value-conditional-behaviour.md)): a function whose every - path ends in `abort`, `exit`, `longjmp`, a function declared `noreturn` - or `_Noreturn`, an infinite loop, or a call to another such function is - summarised `never-returns`, in the same file or across the program, and - a call to it ends the path: `if (bad) die("..."); use(p);` is analysed on - the good path only, nothing is leaked at the end of a block that is never - left, and code after the call is dead. A function that returns on *some* - path (`void check(int ok) { if (!ok) die(); }`) returns as far as the - checker knows, and a call through a function pointer never returns only - if every candidate never returns. No annotation is needed or added. -- **Shared ownership and reference counts** ([RFC - 0010](rfcs/0010-shared-ownership.md)): a resource on the books has a - number of *shares*. A function that increments an integer field of its - argument's object (`o->rc++`, `o->rc += 1`, `__atomic_fetch_add(&o->rc, - 1, m)`, `__sync_add_and_fetch(&o->rc, 1)`, in its own body or a callee's) - *retains* the argument, and the caller's pointer gains a share. A function - that frees the object when a decrement of that field reaches zero (`if - (--o->rc == 0) free(o)`, `o->rc-- == 1`, the atomic and `__sync_` forms, - or a helper such as `dec_and_test(&o->rc)` whose result says the count is - zero) *releases a share*: the argument's name is dead, and the object is - gone only when that was the holder's last share. A copy of a holder with - a surplus takes one share with it (`b = obj_ref(a); obj_unref(b); - use(a)` is clean), a copy of a holder with one share shares it (`b = a; - obj_unref(b); use(a)` is a `use-after-free`), and a plain `free` kills - every name. Releasing through a name this function never retained (a - parameter it was handed) is a discipline, not a bug: the name is dead - afterwards and nothing else changes. A share retained on a local and - dropped is a `leak` when the field is a *known count*; a share retained - on a parameter or global is the caller's (`increment param 0 *.rc` in the - summary). Libraries with no body in view are described with - `WEAVEC_RETAINS` / `WEAVEC_RELEASES`. -- **Per-outcome stores and facts** ([RFC - 0010](rfcs/0010-shared-ownership.md)): a callee that stores an argument - only on some outcome classes (`int bag_put(struct bag *b, char *s)` - returning `-1` when full) is summarised with `stored ` - lines, and on the edge where the caller rules those classes out the store - is retracted: `if (bag_put(b, s) < 0) free(s);` is clean, and dropping - `s` on that edge is a `leak`. Likewise what the callee left in the - caller's integer memory per class (`fact negative param 0 *.filled =0`) - is known to the caller on the edge it takes. -- **Stores out of sight** ([RFC 0010](rfcs/0010-shared-ownership.md)): a - callee that copies its argument into memory its summary cannot name (a - node it allocated and linked into the caller's table: `p->value = value; - t->first = p;`) is summarised `param 1 escaped`, and the caller treats the - argument as it treats any value a callee stored a copy of: it has a - second home, so it is not reported leaked and a share the call gave it is - not lost. Wrappers pass the effect on, including through `f(obj_ref(v))` - (a result that is `v` or null resolves to `v`). -- **Derived pointers** ([RFC 0011](rfcs/0011-spatial-safety.md)): a - pointer is an object and an *offset* into it. `&p->f`, `&p[3]`, `p + 4`, - `p->payload` (an array member decaying) and `(char *)p - - offsetof(struct outer, in)` are names for `p`'s object at a known offset, - so `free(container_of(i, struct outer, in))` frees what `i` was derived - from, a callee that does so is summarised `freed @-struct outer.in`, a - field pointer kept across `free(p)` is a `use-after-free` on its next use - (not a `conflicting-borrow` at the free), and `free(&o->in)` or - `free(p + 1)` names the offset in its `invalid-release`. Two field steps, - an element step below a field, or a step by a variable amount make the - offset unknown, which is still `p`'s object. -- **Extents and bounds** ([RFC 0011](rfcs/0011-spatial-safety.md)): an - allocation carries its size (`malloc(n)`: `n` bytes; `calloc(n, sz)`; - `realloc(p, n)`; a wrapper's result: `xmalloc(n)` is summarised `fresh - extent=n`), a variable and an array member their declared size, a string - literal its length, and a `WEAVEC_SIZED_BY(n)` parameter `n` elements. An - access `p[i]`, `*(p + i)`, `(p + i)->f`, or a library call's buffer and - length (`memcpy`, `memset`, `fgets`, `read`, `snprintf`, ...) is compared - with the extent: constant against constant outright; a symbolic index - through what the path knows of it — the same counter (`p[n]` on - `malloc(n)`), a relation learnt from a condition (`i < n`, `i <= n`, `i >= - n`, also through one copy `j = i`), or a constant bound (`i < 8`) — and - a pointer's own offset is added (`p = buf + 4; p[4]` on eight bytes). A - write to the index or the counter forgets what was known. An access that - is out of bounds for every value the facts allow, against an exact extent, - is an `out-of-bounds` error; one that is not proven is not reported - ([RFC 0030](rfcs/0030-prove-or-trap.md) §3.3, §7). A callee that - accesses more of a parameter than its type promises (`b[7]` on `char *b`, - `for (i = 0; i < n; i++) b[i]`, `for (i = 0; i < 8; i++) b[i]`, - `memset(b, 0, n)`) is summarised with a *requirement* on that parameter's - extent (`requires-extent{b: 8}`, `{b: n*4}`), checked at every call - against what the argument has and passed on through wrappers. RFC 0017 - retains representable numeric conditions such as `if (n > 4) b[4]`, the - first accessed byte, and bounded product or minimum expressions. An - unsupported condition leaves the access not proven; dropping it cannot - create an unconditional caller error. -- **Strings** ([RFC 0012](rfcs/0012-spatial-safety-strings-and-fields.md)): - beside its extent an object carries what is known of the string it - holds — its length (a constant, or `strlen(s)` as a place of its own, or - `n` when `n = strlen(s)`), or that it has *no* terminator — on the object - and every exact alias of it. A literal or initialiser sets the length; - `strcpy`, `stpcpy`, `strcat`, `sprintf`, `strdup`, `fgets` and `snprintf` - leave a known or at least terminated string; `strncpy` with a source at - least as long as the count, `memset` with a non-zero byte and `memcpy` - from an unterminated source over the whole object leave none; a NUL - store makes a string again; any other write forgets. The copies are - checked as lengths (`strcpy` needs `strlen(s) + 1`, `strcat` needs what - the destination holds plus the source plus one, `sprintf` at least the - format's literal bytes and one per conversion), the reads that seek a - terminator (`strlen`, `strcpy`'s source, `puts`, `%s`) are reported on - an object known to have none, and a copy that overflowed leaves the - string unknown so it is reported once. A source of unknown length is - not reported either way. -- **Sized fields** ([RFC 0012](rfcs/0012-spatial-safety-strings-and-fields.md)): - a pointer field whose object is counted by a sibling integer field. - Declared with `WEAVEC_SIZED_BY(cap)`, the field is loaded with `cap` - elements of extent everywhere and stores into it are checked against - `cap`. Undeclared, the pair is *inferred*: every store into the field - anywhere in the program is a witness (`v->items = malloc(n * sizeof - *v->items); v->cap = n;` says `(items, cap, 4)`) or a refutation (a store - of an object no sibling counts, such as a caller's pointer; a write to - the count with no store into the pointer, such as `v->n++`); the pair - holds when the witnesses agree on one count and nothing refutes it. A - unit is analysed once more when its own stores confirm a pair its - readers use, and the whole-program analysis (`weavec --whole-program`) - does the same across units, so - the inference needs no annotation and no particular order of files; - what it costs is one more analysis of the functions that read the field. -- **Offsets and lower bounds** ([RFC 0012](rfcs/0012-spatial-safety-strings-and-fields.md)): - a relation between two integers carries an offset (`i <= n - 1` is `i < - n`; `j = i + 1` makes `a[j]` under `i < n` one past), and a constant - lower bound is kept beside the upper (`i >= 8` decides `buf[i]` on eight - bytes outright). -- **Assumptions** ([RFC 0012](rfcs/0012-spatial-safety-strings-and-fields.md)): - `WEAVEC_ASSUME(expr)` applies `expr` as the true edge of `if (expr)` - would — a null test, a relation, a bound, a constant, an outcome of a - call. An assumption the facts contradict is a `contradicted-assumption` - error ([RFC 0030](rfcs/0030-prove-or-trap.md) §6.2); one they neither - prove nor refute is assumed. Nothing checks it at run time - ([RFC 0035](rfcs/0035-guard-by-default.md), amendment 11). -- **Result provenance**: a callee that returns one of its arguments or a - pointer into one (`strchr`, `next_of(n)`, `&n->v`) makes the result an - alias or a borrow of that argument in the caller, so freeing the argument - then using the result is reported. -- **Aliases**: `q = p` makes `q` and `p` names for the same object, so a free - through either is a free of both. Reassigning a pointer separates it. So - does a test ([RFC 0006](rfcs/0006-precision.md)): on the edge where `p - != q` holds the two are distinct (`if (l == sentinel) return; free(l); - use(sentinel);` is clean), and on the edge where `p == q` holds they are - the same object. `!=` separates only pointers that hold the *same value* - (`q = p`); `q = p + 1` points into the same object and stays an alias. -- **Borrows**: `&x`, `&s->f`, array decay and `&a[i]` create a loan held by - the pointer being assigned (mutable unless the pointer's pointee type is - `const`). Arguments to `WEAVEC_BORROWED`/`WEAVEC_MUT` parameters, and to - parameters that a defined callee reads or writes through, borrow for the - duration of the call. A loan ends when its holder is reassigned or is - *last used* ([RFC 0006](rfcs/0006-precision.md)): `int *a = &n->v; *a = - 1; free(n);` is fine, `free(n); *a = 1;` is not. Loans held through a - pointer (`h->view = &n->v`), by a global, or by a local whose address is - taken last until the holder is reassigned. Copying a pointer that holds - a loan into a longer-lived pointer is checked like creating the loan - there. Two live pointers into one object, or writing an object another - pointer views, are accepted; RFC 0030 removed the option that rejected - them as RFC 0001 does. -- **Places**: `p`, `s.f`, `p->f`, `*pp`, `p->a->b`, file-scope variables; - all elements of an array share one place, written `a[*]` in diagnostics - (`*a` for a pointer `a`). A free or move of an element remembers which - element was named ([RFC 0006](rfcs/0006-precision.md)): `free(a[i]); - a[i][0] = 0;` and `free(a[0]); free(a[0]);` are reported, while `for (i) - free(a[i]); free(a);`, `free(a[0]); use(a[1]);` and `free(a[i]); a[i] = - NULL;` are clean. Assigning, incrementing or taking the address of the - index variable makes the element unknown: it is then neither reported by - element accesses nor cleared by element writes. An access without a - subscript (`*a`, `free(a)` of the array's owner) matches every element. -- **Overwrites**: a callee that writes an object wholesale (`memcpy(root, - &tmp, sizeof *root)`, or a defined function that writes through the - parameter) makes every fact about the object's fields stale, so - `free(root->string); memcpy(root, ...); use(root->string);` is clean - ([RFC 0006](rfcs/0006-precision.md)). -- **Annotations are checked against their bodies**: a body that frees - a `WEAVEC_BORROWED` - parameter is an `annotation-mismatch`; the annotation still governs what - callers assume. -- **Pointer identity**: pointer arithmetic (`p + 1`, `p++`, `&a[i]`) and - casts between pointer types (`(char *)p`, `(void *)p`) name the same - object as the operand, so `free(p); use(p + 1)` is a use after free. Only - a round trip through an integer loses identity, and what comes back is a - pointer of unknown provenance, whose accesses are not proven. -- **Raw pointers and unsafe regions**: a raw pointer is tracked (copies, - comparisons and conversions to integers are fine) but dereferencing, - releasing or handing it to an owning parameter outside a `WEAVEC_UNSAFE` - region is an `unsafe-operation`. Unsafe regions are analysed like any - other code, so a `free` inside one is still a free afterwards; inside - them spatial and null operations are trusted, and nothing is suppressed - ([RFC 0030](rfcs/0030-prove-or-trap.md) §6.1). -- **Unknown callees**: a call to a function with no body in the program, no - library-table entry and no ownership contract, not declared in a C - library, POSIX or platform header, may release, keep or replace every - pointer argument without an ownership annotation, and everything - reachable through it ([RFC 0030](rfcs/0030-prove-or-trap.md) §5.1). Later - uses are not reported; their temporal facets are not proven. Its result - has unknown nullness and no ownership. A function declared in a platform - header but missing from the table borrows its arguments for the call, - and what depends on that is trusted (§5.2). In a per-file analysis, calls - into other files are unknown; `weavec --whole-program` applies their - definitions. - -`weavec --dump-kinds file.c -- ` prints, instead of analysing, each -unit's pointer kinds ([RFC 0030](rfcs/0030-prove-or-trap.md) §7): the declared -kind of every parameter, result, field and variable of the main file with its -precedence level, the inferred defaults, slot kinds (and the stores that -demoted them) and must-access requirements, the annotation problems and fix-it suggestions, the store groups -and §7.6 field candidates, and the unit's function-pointer slots (§9.3) with -the resolution of each indirect call. The format is unstable. - -`weavec --dump-analysis file.c -- ` is the object engine's dump -([RFC 0031](rfcs/0031-object-engine.md) §12): its states, with each -object's kind, extent and life, the symbols its cells hold (targets, -nullness, release records, raw origin) and the zone; with -`--whole-program` it ends with the program database (every exported -summary). The format is unstable. Summaries and the program database are -kept in memory; nothing is written to disk. - -### Constructors, returned fields and allocation-time sizes - -[RFC 0013](rfcs/0013-interprocedural-heap-state.md) requires no new -annotations. An inferred constructor describes the pointer fields of its -returned or published object, including owned children, aliases of arguments, -shared children and self-links. These facts also cross record returns and -translation units. Existing diagnostics apply to them: a child access can -be `out-of-bounds`, releasing its container can lose a child (`leak`), and -using a field after releasing its referent is `use-after-free`. Known null, -raw, borrowed, release-family and string facts retain their usual checks. - -For example, after a helper returns a box whose `data` was allocated with -`malloc(4)`, the caller's `box->data[4]` is out of bounds and -`free(box->data); free(box);` releases both resources. Two fields initialized -from the same allocation remain aliases; two constructor call sites create -independent objects. Final values govern outputs: a field reset to null does -not still hold its earlier allocation. A helper that fails without replacing -an output leaves the caller's incoming value and bounds intact. - -An allocation uses the size's value at allocation time. With -`size_t n = 4; char *p = malloc(n); n = 8;`, a non-null `p` still has four -bytes: the analysis names values, not variables, so the extent stays the -value `n` had when `malloc` ran, and a copy of it that is not reassigned -still relates to it. Reassigning the size cannot resize an older object to -a later count. - -A function's summary describes the cells of the objects it returns or -stores, and of the new objects those cells point to, up to three levels -deep ([RFC 0031](rfcs/0031-object-engine.md) §6.1, *Implementation -amendments*); a cell it does not describe carries no facts to its callers. -The original evaluation's bug and clean programs are kept as cases under -`test/cases/evaluation/` (see [test/cases](../test/cases/README.md)). - -## Related pointer arguments - -[RFC 0016](rfcs/0016-compositional-call-checking.md) checks known helpers under -relationships established by their callers. No annotation is needed for this -ordinary aliasing pattern: - -```c -static void release_then_write(char *a, char *b) { - free(a); - *b = 1; // use-after-free when called with (p, p) -} - -static void write_then_release(char *a, char *b) { - *b = 1; - free(a); // the same (p, p) relationship is safe here -} -``` - -The checker also distinguishes `reset(&p, &p)` from `reset(&p, &q)` when -both cells initially contain one allocation: replacing `*out` changes the -first cell, while a saved copy can still refer to the released value. A -global that points into an argument's object is part of the relationship, -and so are the call's constant integer arguments, so the helper is checked -on the branch the call takes. An ownership annotation on a known definition -does not skip its body checks. - -Errors name the operation in the helper, with the note "called here with -related pointer arguments" at the originating call. Facts describe values -on entry; subsequent writes still update or invalidate them. A call inside a -`WEAVEC_UNSAFE` region is checked the same way; the region does not -suppress these reports ([RFC 0030](rfcs/0030-prove-or-trap.md) §6.1). - -This checking is local to a translation unit: a helper is re-checked under -its caller's relationship when both are in the same unit, at most 16 -relationships per helper ([RFC 0031](rfcs/0031-object-engine.md) §6.6, -*Implementation amendments*). Other calls use the helper's summary, which -is derived for parameters that may point to the same object unless the -analysis can tell them apart; silence does not prove arbitrary pointers -disjoint, and the temporal facet of a use through a pointer that may alias -a released object is not proven. Summaries carry no call contexts, and a -helper specialised for the one -callback a call passes is not checked separately: a call through a callback -parameter uses every target the program passes to it. - -## C integers and dynamic bounds - -[RFC 0017](rfcs/0017-c-integer-semantics-and-spatial-safety.md) adds no -annotation spellings. `WEAVEC_SIZED_BY(n)` still counts elements (`void *` -counts bytes), and `WEAVEC_ASSUME` supplies an invariant that is proven, -refuted or assumed ([RFC 0030](rfcs/0030-prove-or-trap.md) §6.2). Both use the expressions' actual C types. Neither contract changes an -allocation's size. - -Integer interpretation now affects temporal checks as well as bounds. On an -eight-bit-char target, this narrowing cannot hide the use-after-free: - -```c -#include - -void example(unsigned n) { - if (n != 256) return; - unsigned char k = n; // k is zero - char *p = malloc(1); - if (!p) return; - free(p); - if (k == 0) *p = 1; // use-after-free -} -``` - -For supported types through 64 bits, promotions and storage conversions apply -to assignments, compound assignments, comparisons, switches, selected indices -and count adjustments. Unsigned arithmetic wraps modulo its width. Signed -arithmetic retains its validity rules, with wrapping arithmetic honored where -the compilation mode defines it. The checker reports definitely invalid -operations it visits; it is not a general integer lint for all expressions. - -Allocation extents use the value actually passed: `malloc((unsigned char)n)` -on that target receives zero when `n == 256`. A repeated symbolic byte product -can expose `p[rows * cols]` as one past `malloc(rows * cols)`, even when the -unsigned product wraps. The byte interval of an access is then calculated -mathematically, without wrapping its end back into the allocation. Supported -`__builtin_add_overflow`, `__builtin_sub_overflow` and -`__builtin_mul_overflow` calls retain their output and success/failure facts; -a matching nonzero-divisor and `SIZE_MAX / count` guard can establish that a -product fits. `calloc` and `reallocarray` use checked products: overflow cannot -create a small successful allocation, and failed `reallocarray` retains its -input object. - -Numeric returns and out-parameters travel through helpers (and, under -`weavec --whole-program`, across translation units) as integer ranges, or -relative to a value the caller passed, keyed by the result's class or a -parameter's zero test. Access requirements -retain lower bounds and numeric conditions. A supported zero-based unit-stride -loop with `i < n && i < cap` can require `min(n, cap)` elements; equivalent -explicit minimum expressions also compose. Early-exit and other unsupported -loops do not produce inferred must-requirements: a bound that the loop might -reach cannot be treated as storage every caller must provide. Reassigning a -size operand does not resize an earlier allocation or change a value already -stored. - -VLA dimensions are captured at declaration time, including supported nested -dimensions and subsequent `sizeof` of that array. A later write to the bound -variable does not change the existing array. A definitely nonpositive dimension -is `invalid-integer-operation`; an unrepresentable byte extent leaves the -access not proven. Flexible-array member bounds come from the backing allocation minus -the target's field offset, with the member's element size. A sibling count -cannot invent tail storage. Tail pointers preserve the enclosing allocation's -lifetime and release identity, while fixed-array subobjects retain their own -bounds. Inferred sized fields preserve the C type of count multiplication, -including its possible wrap. - -The analysis decides each access's spatial outcome -([RFC 0030](rfcs/0030-prove-or-trap.md) §2, as amended by -[RFC 0035](rfcs/0035-guard-by-default.md) §8), and the summary line of -`weavec` counts them: `proven` covers the full represented access, -including its lower bound; a `violation` is an access out of bounds for -every value the facts allow against an exact extent; *not proven* is an -access the analysis could not decide (an unknown extent or index, or a -bound it cannot express); `trusted` is an access in an unsafe region or -behind a contract the analysis takes on trust. A requirement a callee -places on its parameter is checked at its callers. Outcomes are -independent of diagnostic controls: changing a warning's severity does not -turn an access that is not proven into a proof. None of this changes the -build: `weavec-cc` guards every access against the object the runtime -knows, whatever its outcome. - -Ranges retain at most two intervals. Relations between values are kept -for at most 64 values per program point, and the rest keep only their -ranges; a pointer names at most eight objects, and an array object keeps at -most 32 elements selected by a variable index and four ranges of elements -per element position ([RFC 0031](rfcs/0031-object-engine.md) §4). Exceeding -a limit loses precision, never soundness; a construct the analysis does not -model leaves the affected facets not proven, and an arbitrary unknown -index alone is not proven, without a new bounds error. General -nonlinear inequalities, arbitrary induction/strides, integers wider than 64 -bits, unsupported union/type-punning and pointer-provenance operations, -unrestricted aliases, byte-encoded pointers, GC invariants and concurrency -remain outside the supported model. - -These rules add no run-time instrumentation: `weavec-cc` guards every -access whether or not the analysis proves it -([RFC 0035](rfcs/0035-guard-by-default.md)). - -## Controlling diagnostics - -`weavec` and `weavec-cc` accept Clang-style warning flags for WeaveC's ids. -They control the advisory analysis only, so in `weavec-cc` they matter -under `-fweavec-diagnose`; no flag here changes what a build guards. - -| Flag | Effect | -| ----------------------------- | ------------------------------------------------------------------------------------------------- | -| `-Wno-weavec-` | Disable the diagnostics of `` whose default severity in `weavec` is *warning*: all of `leak`, `invalid-annotation` and `allocation-failure`, and the possible (warning) findings of `use-after-free`, `double-free`, `use-after-move`, `conflicting-borrow`, `lifetime-too-short`, `mismatched-release` and `invalid-release`, whose definite findings stay errors ([RFC 0030](rfcs/0030-prove-or-trap.md) §3). Refused for an id that is always an error in `weavec` (`null-dereference`, `use-of-uninitialized`, `out-of-bounds`, `unsafe-operation`, `annotation-mismatch`, `invalid-integer-operation`, `contradicted-assumption`; lower those with `-Wno-error=`), and for a removed id: naming one in any `-W` flag is an error. | -| `-Wweavec-` | Re-enable it. `-Wweavec-allocation-failure` enables `allocation-failure`, which is off by default, and `-Wweavec-leak` enables `leak`, which is off by default under `weavec-cc -fweavec-diagnose`; `-Wweavec` enables every id. | -| `-Wno-error=weavec-` | Report an error as a warning, so that `weavec` exits with status 0 while a codebase works through the reports. | -| `-Werror=weavec-` | Report a warning as an error. | -| `-Wno-weavec`, `-Wno-error=weavec`, `-Werror=weavec` | The same for every WeaveC id (`-Wno-weavec` leaves the errors alone). Under `weavec-cc -fweavec-diagnose` every finding starts as a warning, and `-Werror=weavec` makes them errors. | - -The other flags of `weavec-cc` (`-fweavec-checks=`, `-f[no-]weavec-zero-init`, -`-fweavec-ledger=`, `-f[no-]weavec-summary`, `-fweavec-diagnose`, the -analysis budgets and dumps) are listed by `weavec-cc --help-weavec`. An -unknown `-fweavec-*` flag is an error, and a removed flag keeps no alias. - -## Compatibility with other annotation schemes - -`annotate` payloads that do not start with `weavec.` are ignored, so WeaveC -coexists with GSL (`gsl::owner`), Clang's own attributes, and project-specific -annotations. +A function kept from AddressSanitizer (`__attribute__((no_sanitize("address")))`, +`no_sanitize_address` or `disable_sanitizer_instrumentation`) is an unsafe +region too, with no source change. + +Every other annotation of earlier versions (`WEAVEC_OWNED`, +`WEAVEC_BORROWED`, `WEAVEC_COUNTED_BY`, `WEAVEC_ASSUME` and the rest) was +deleted with the ownership analysis that read it +([RFC 0036](rfcs/0036-deterministic-heap-reuse.md)); a program that still +uses one fails to compile until it stops. `annotate` payloads that do not +start with `weavec.` are ignored. The [unsafe boundaries +guide](pages/guides/unsafe.md) says when a region is the right tool. diff --git a/docs/architecture.md b/docs/architecture.md index 81e8a602..faea01ce 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,83 +1,69 @@ # Architecture -WeaveC is a drop-in C compiler that enforces memory safety at run time, and -an advisory ownership and memory-safety analysis for C, both built on Clang -and LLVM. [RFC 0035](rfcs/0035-guard-by-default.md), *Guard by default*, -defines the design; its *Implementation amendments* override its body. +WeaveC is a drop-in C compiler that enforces memory safety at run time, +built on Clang and LLVM. [RFC 0036](rfcs/0036-deterministic-heap-reuse.md), +*Deterministic heap reuse*, defines the design over the guards of +[RFC 0035](rfcs/0035-guard-by-default.md), *Guard by default*; each RFC's +*Implementation amendments* override its body. `weavec-cc` guards every memory access of the C code it compiles, in LLVM -IR, against the runtime's shadow memory, and removes a guard only where a -simple local rule proves it redundant. It lays out the stack objects a guard -can reach and the unit's globals with redzones, zero-initialises locals, -checks typed array indexes against their declared bounds, and links a -runtime whose allocator leaves at least one byte after every heap object -that no object owns and keeps released blocks in a quarantine. A bad access -traps with a report. The analysis of RFCs 0030 and 0031 (pointer kinds, -sites and facets, the object engine behind the `SafetyEngine` seam, -summaries, whole-program analysis) is advisory: it runs in the `weavec` tool -and under `weavec-cc -fweavec-diagnose`, reports diagnostics, and never -changes the generated code. Ownership enforcement is planned as checked -contracts in RFC 0036. - -The code is three C++ libraries, two thin command-line tools and a C -runtime. The arrows point from a layer to what it may depend on. +IR, against the runtime's shadow memory. It removes a guard only where a +simple local rule proves it redundant. The locals a guard can reach get +redzones in their frame (locals whose lifetimes never overlap share space), +globals get redzones, locals are zero-initialised, typed array indexes are +checked against their declared bounds and fortified library calls against +the object size they carry. The runtime's allocator leaves at least one +byte after every heap object that no object owns, and reuses a released +block only after a *sweep* of the program's memory finds no pointer to it. +A bad access traps with a report. + +The code is two C++ libraries, one thin command-line tool and a C runtime. +The arrows point from a layer to what it may depend on. ``` - ┌──────────────────────┐ ┌──────────────────────────┐ - │ tools/weavec │ │ tools/weavec-cc │ - │ (libTooling) │ │ (drop-in C compiler) │ - └──────────┬───────────┘ └────────────┬─────────────┘ - └───────────┬───────────────┘ - ▼ ┌──────────────────────────┐ - │ weavec::Frontend │ guard passes, enforcement - │ lib/Frontend │ ledger, unsafe regions, - │ │ analysis action, whole- - │ │ program analysis, driver + │ tools/weavec-cc │ + │ (drop-in C compiler) │ └────────────┬─────────────┘ ▼ ┌──────────────────────────┐ - │ weavec::Analysis │ kinds, sites, slots, the - │ lib/Analysis │ engine seam, the object - │ │ engine + │ weavec::Frontend │ guard passes, unsafe + │ lib/Frontend │ regions, enforcement + │ │ ledger, driver └──────┬───────────┬───────┘ ▼ ▼ ┌────────────────────┐ ┌────────────────────┐ │ weavec::Core │ │ Clang / LLVM │ │ lib/Core │ │ (external) │ - │ no Clang/LLVM │ └────────────────────┘ + │ the library table │ └────────────────────┘ + │ no Clang/LLVM │ └────────────────────┘ ``` -The guard passes in Frontend use LLVM and Core's library table directly and -nothing from Analysis. `runtime/` is C code that `weavec-cc` links into -every enforcing link (see *The runtime*). It depends on nothing above and -includes no WeaveC header but its own. The layering rule is strict: +`runtime/` is C code that `weavec-cc` links into every enforcing link (see +*The runtime*). It depends on nothing above. The layering rules are strict: - Core includes nothing from `clang/` or `llvm/`. It is the one library - built without their include paths. -- Analysis is the only layer that knows both Core and Clang's AST. -- Inside Analysis, only the `Engine*.cpp` files include the engine's - private header `lib/Analysis/Engine.h` (RFC 0031 §2, gate H2), so the - components on the near side of the engine seam (see *The engine seam*), - `SiteCollector`, `AttributeReader`, `KindInference`, `SlotCollector`, - `BoundaryInvariants` and `LedgerAdapter`, never see the engine's - internals. The rest of the code names the engine only as `ObjectEngine` - behind `SafetyEngine`. - -RFC 0035 is Accepted and implemented in stages; the [roadmap](roadmap.md) -says which have landed. This page describes the tree as it is. Earlier -designs (checked mode, checks and guards planned from the analysis, unit -records and the link step) are described in the RFCs that introduced and -retired them. + built without their include paths, and holds only the library table. +- The runtime includes no WeaveC header but its own + [`weavec_rt.h`](../runtime/weavec_rt.h), and the entry points it declares + there match the ones the passes declare (`scripts/check-hygiene.py` + checks that each one the passes name is declared). + +RFC 0036 is Accepted and being implemented; the [roadmap](roadmap.md) says +which stages have landed. This page describes the tree as it is. Earlier +designs (the advisory ownership analysis and the `weavec` tool, checked +mode, checks planned from the analysis, unit records and the link step) are +described in the RFCs that introduced and retired them. ## Compile pipeline `weavec-cc` compiles a C translation unit as Clang does, in-process, with these additions in an enforcing mode (`-fweavec-checks=trap`, the default, -`report` or `verify`; RFC 0035 §1 and amendment 1): +`report` or `verify`; RFC 0035 §1 and amendment 1, RFC 0036 §1): -1. **Zero-initialised locals.** `-ftrivial-auto-var-init=zero` +1. **Zero-initialised locals.** `-ftrivial-auto-var-init=zero`, for locals + up to 4 KiB unless the command line sets its own limit (`-fno-weavec-zero-init` turns it off). The runtime's allocator zero-fills every block. 2. **Array bounds.** `-fsanitize=array-bounds`, reported through the @@ -95,17 +81,32 @@ these additions in an enforcing mode (`-fweavec-checks=trap`, the default, 5. **No conditional compares on AArch64** (`-aarch64-enable-ccmp=false`; a later `-mllvm` option of the user's wins). The backend folded a guard's partial-granule test back into its fast path. -6. **The unsafe-region collector** (`UnsafeRegions`) runs over the AST +6. **Fortification** (§1, §6). On an optimising glibc build whose command + line neither defines nor undefines `_FORTIFY_SOURCE`, the driver adds + `-D_FORTIFY_SOURCE=3`; Darwin's SDK fortifies at level 2 by default. The + guards check the object size a fortified call carries. +7. **ASan's contract** (§1, §5). The unit is parsed with AddressSanitizer in its + language options, so `__has_feature(address_sanitizer)` is true, + `__SANITIZE_ADDRESS__` is defined and `no_sanitize("address")` is + accepted; at the end of the unit, before code generation, the option is + taken out again, so ASan's instrumentation never runs, and GuardInsert + removes the `sanitize_address` function attribute. Clang's + `_FORTIFY_SOURCE=0` predefine for ASan on Darwin is undone unless the + command line chose a level. Preprocessor-only jobs get the same macros. + `-fno-weavec-asan-compat` keeps the language options as the command line + set them, and a command line that asks for an address sanitizer itself + keeps its own. +8. **The unsafe-region collector** (`UnsafeRegions`) runs over the AST before code generation (see *Unsafe regions*). -7. **Four LLVM passes** (`registerGuardPasses` in +9. **Four LLVM passes** (`registerGuardPasses` in `lib/Frontend/GuardPass.cpp`), at every optimisation level: | Pass | Extension point | Files | Does | | --- | --- | --- | --- | - | `GuardInsert` | `PipelineStartEP` | `GuardInsert.cpp`, `GuardLibrary.cpp` | puts a marker before each access and each memory argument of a library call, redirects wrapped library calls, records the ledger rows | + | `GuardInsert` | `PipelineStartEP` | `GuardInsert.cpp`, `GuardLibrary.cpp` | puts a marker before each access and each memory argument of a library call, redirects the library calls whose wrappers write, records the ledger rows, drops `sanitize_address` | | `GuardPrune` | `PeepholeEP` | `GuardInsert.cpp` | after inlining, removes the markers rule 6.1 proves on a local or global at constant offsets, so that SROA still promotes those locals; manages the scope barriers | | `GuardLoop` | `VectorizerStartEP` | `GuardLoops.cpp` | rule 6.4: versions loops whose whole ranges one check covers | - | `GuardExpand` | `OptimizerLastEP` | `GuardExpand.cpp`, `GuardFrames.cpp`, `GuardGlobals.cpp` | rules 6.1–6.3, frame layout, global redzones, and each marker into its inline check | + | `GuardExpand` | `OptimizerLastEP` | `GuardExpand.cpp`, `GuardFrames.cpp`, `GuardGlobals.cpp`, `GuardLibrary.cpp` | rules 6.1–6.3; the frame layout, shared space and scopes; global redzones; the unpoisoning before `noreturn` calls; the late library redirections; each marker into its inline check | `GuardPass.cpp` and the private header `GuardPassImpl.h` hold what the passes share: the module's context, the markers, the runtime's entry @@ -113,7 +114,7 @@ these additions in an enforcing mode (`-fweavec-checks=trap`, the default, passes run on 64-bit targets only, and skip declarations, naked functions and the runtime's own functions. An `available_externally` body (a C99 inline definition) is guarded, since the inliner copies it - into the unit. + into the unit. Verify mode skips GuardLoop. Guards are inserted first and expanded last because the optimiser exploits undefined behaviour: an out-of-bounds read in a loop can make the loop @@ -134,35 +135,38 @@ it may hoist and remove them as it does loads. Beside each lifetime marker GuardInsert puts `__weavec.scope()`, which writes the runtime's memory, so that no marker is hoisted above the `lifetime.start` of a local declared in a loop; GuardPrune drops the barriers of locals SROA promoted and adds them -beside the lifetime markers inlining brought (amendment 2). - -What is collected (§2.1): a `load`, `store`, `atomicrmw` or `cmpxchg` is a -guard of the bytes of its type; `llvm.memcpy`, `llvm.memmove` and -`llvm.memset` a range for each memory operand; a masked load or store, a -gather or a scatter one access per lane under its mask bit; a call of a -library-table function a range or a string for each memory argument. An -access to a stack or global object at a constant offset inside it gets no -marker and no ledger row, so the object stays promotable to registers. -Inline assembly operands, accesses in a non-default address space and the -passes' own shadow accesses are not collected. +beside the lifetime markers inlining brought (RFC 0035 amendment 2). + +What is collected (RFC 0035 §2.1): a `load`, `store`, `atomicrmw` or +`cmpxchg` is a guard of the bytes of its type; `llvm.memcpy`, +`llvm.memmove` and `llvm.memset` a range for each memory operand; a masked +load or store, a gather or a scatter one access per lane under its mask +bit; a call of a library-table function a range or a string for each +memory argument. An access to a stack or global object at a constant +offset inside it gets no marker and no ledger row, so the object stays +promotable to registers. Inline assembly operands, accesses in a +non-default address space and the passes' own shadow accesses are not +collected. Nothing else changes: no AST is rewritten and code generation is not deferred. A unit compiled with `-fweavec-checks=none` or `-fno-weavec`, or as C++, Objective-C, OpenCL or CUDA, is compiled as Clang compiles it. -Under `-fweavec-diagnose` the analysis runs beside Clang's code generator -in the same job (see *The advisory analysis*). ## Guards and the shadow -The runtime keeps one shadow byte per 16-byte granule (§2.2 and amendment -3; the values are in [`weavec_rt.h`](../runtime/weavec_rt.h)): +The runtime keeps one shadow byte per 16-byte granule (RFC 0035 §2.2 and +amendment 3, RFC 0036 §5; the values are in +[`weavec_rt.h`](../runtime/weavec_rt.h)): | Value | Meaning | | --- | --- | | `0x00` | all 16 bytes addressable | | `0x01`–`0x0F` | the first k bytes addressable, the rest not | -| `0xF1`, `0xF2`, `0xF3` | a tracked frame's left, middle and right redzone | -| `0xF4` | a dynamic stack object's redzone (`alloca`, VLA) | +| `0xF1` | a frame's left redzone | +| `0xF2` | a redzone between a frame's objects | +| `0xF3` | a frame's right redzone | +| `0xF4` | a dynamic stack object's redzone | +| `0xF7` | poisoned by the program through ASan's interface | | `0xF8` | a tracked local out of scope | | `0xF9` | a tracked global's redzone | | `0xFA` | a heap slot after its object; also committed arena slots not yet handed out, and the granule before each class region | @@ -180,14 +184,14 @@ spuriously, and the slow path, which knows the window, lets it pass. When the reservation fails, `mask` is 0 and `base` points to zero bytes: every guard passes, and the runtime says so once on standard error. -**Fast paths** (amendment 4). A guard whose bytes lie in one granule is -three instructions: the shift and mask, the shadow load, and a branch on a -nonzero byte, weighted cold. The partial-granule test (whether the access -ends within the first k bytes) is on the slow side of the branch, before -the call. An access of up to 16 bytes that may cross a granule loads the -two shadow bytes it may touch at once; a range of constant length up to 113 -bytes loads eight with a constant mask. A nonzero byte only sends the -access to the exact check. Any other range calls +**Fast paths** (RFC 0035 amendment 4). A guard whose bytes lie in one +granule is three instructions: the shift and mask, the shadow load, and a +branch on a nonzero byte, weighted cold. The partial-granule test (whether +the access ends within the first k bytes) is on the slow side of the +branch, before the call. An access of up to 16 bytes that may cross a +granule loads the two shadow bytes it may touch at once; a range of +constant length up to 113 bytes loads eight with a constant mask. A nonzero +byte only sends the access to the exact check. Any other range calls `__weavec_rt_range(a, n, site)`, which scans the shadow. **The slow path.** `__weavec_rt_guard(a, w, site)` decides exactly (the @@ -195,15 +199,17 @@ window, partial granules, the poison kind) and names a failure by the shadow value that caused it (see *Reports*). `site` is a constant `__weavec_rt_site` record `{file, line, column, flags}` built from the instruction's location (the flags say write, report mode, a monitor, and -whether a null string is accepted), or null. - -**Null bases** (§2.4). An access whose address is `p + x`, with `x` not a -constant below 4,096 and `p` not known to be non-null, also tests `p`: a -null base takes the slow path, which reports a null dereference. A constant -offset below 4,096 from a null base faults on the null page, and a guard on -an address in the lowest 64 KiB fails as a null dereference through its -shadow, even when the optimiser moved the access to the other arm of a -`select`. +whether a null string is accepted), or null. The slow paths are +`preserve_all` on AArch64 and x86-64, so an inline guard costs the code +around it no spills. + +**Null bases** (RFC 0035 §2.4). An access whose address is `p + x`, with +`x` not a constant below 4,096 and `p` not known to be non-null, also tests +`p`: a null base takes the slow path, which reports a null dereference. A +constant offset below 4,096 from a null base faults on the null page, and a +guard on an address in the lowest 64 KiB fails as a null dereference +through its shadow, even when the optimiser moved the access to the other +arm of a `select`. ## Removing guards @@ -231,7 +237,7 @@ LLVM's `nofree` attribute, and is monitored by verify mode that may not return between them, become one guard of the hull at the first of them. Its slow path checks each access it covers, so a failure names the access that fails. -- **6.4 Loop ranges** (amendment 6). GuardLoop versions a loop with a known +- **6.4 Loop ranges** (RFC 0035 amendment 6). GuardLoop versions a loop with a known trip count, one exit block and nothing that may end a lifetime. Its preheader asks `__weavec_rt_range_ok(lo, n)` (no report) whether the whole range of each invariant or affine access over the trip is @@ -246,14 +252,14 @@ covered its bytes. ## Stack and global objects -**Which locals are tracked** (§3.1, amendment 5). A local (an `alloca`) is +**Which locals are tracked** (RFC 0035 §3.1, amendment 5). A local (an `alloca`) is tracked when a guard that remains may reach it: any of the objects a guarded pointer may be based on, through phis and selects, is that local, or its address escapes (stored, passed to a call other than a lifetime marker or a memory intrinsic on itself, returned, or converted to an integer). Other locals are left as they are. -**The frame** (`GuardFrames.cpp`, §3.2). The tracked static allocas of the +**The frame** (`GuardFrames.cpp`, RFC 0035 §3.2). The tracked static allocas of the entry block become offsets into one frame `alloca`, aligned to 32 bytes: a left redzone of 64 bytes, then each object at a 32-byte boundary followed by its redzone, at least 32 bytes and one eighth of the object up to @@ -266,13 +272,27 @@ it with `0xF8`, so a use after scope traps; a use after return is not caught. A function with a `musttail` call or `localescape` keeps its locals in place, without redzones. -**Dynamic objects** (§3.4). A tracked dynamic `alloca` (an `alloca` call, a +**Shared space** (RFC 0036 §2, amendment 8). In a frame that would be +larger than 512 bytes, two tracked locals may share bytes when their +lifetimes never overlap: LLVM's `StackLifetime` analysis, +in its *may be live* mode, over the lifetime markers that remain after the +optimiser (the analysis SafeStack uses). Each local is placed at the lowest +32-byte-aligned offset after the left redzone where its object and redzone +meet no other local's that may be live with it, so locals in the disjoint +arms of a `switch` take the space of one, as in Clang's own frames. A local +without lifetime markers (every local at `-O0`) is live throughout and +shares nothing. At entry a shared granule is `0xF8` (out of scope); a +lifetime start writes its object addressable, and its redzone `0xF2` again +when the redzone overlaps another local's object, so that an overflow into +it reports a `stack-buffer-overflow`. + +**Dynamic objects** (RFC 0035 §3.4). A tracked dynamic `alloca` (an `alloca` call, a VLA, or a static alloca outside the entry block) of `n` bytes allocates `round32(n) + 32` bytes, and its tail is poisoned `0xF4`. Before a stack restore and before each return, `__weavec_rt_alloca_unpoison(top, bottom)` clears the shadow of the dynamic objects made since. -**Non-local exits** (§3.5). Before each call of a function that does not +**Non-local exits** (RFC 0035 §3.5). Before each call of a function that does not return, other than the runtime's own and those that end the process (`abort`, `exit`, the assertion failures, `err` and the like), the pass calls `__weavec_rt_unpoison_stack()`, which clears the shadow from the @@ -283,7 +303,7 @@ abandons. A frame left by code not built by `weavec-cc` (an exception unwinding through C frames) keeps its poison until the stack is reused (assumption A5). -**Globals** (`GuardGlobals.cpp`, §4). A global the unit defines exactly is +**Globals** (`GuardGlobals.cpp`, RFC 0035 §4). A global the unit defines exactly is tracked unless it is thread-local, in a named section, in a comdat, in a non-default address space, a string literal or other mergeable constant (constant, `unnamed_addr` and local), `common`, appending, externally @@ -293,14 +313,17 @@ visibility that holds the original and a redzone (sized as a frame object's), aligned to 32 bytes. A module constructor of priority 1 calls `__weavec_rt_globals_register(array, n)` with `{address, size, size with redzone}` per global, which poisons each redzone -`0xF9`; a module destructor unregisters them and clears their shadow, so -`dlclose` leaves nothing behind. +`0xF9`. A shared library's or bundle's copy of the runtime sends the +registration to the process's owning runtime first (RFC 0036 §4), so its +globals are poisoned in the shadow its guards read. A module destructor +unregisters them and clears their shadow, so `dlclose` leaves nothing +behind. ## Library calls `GuardLibrary.cpp` guards a direct call of a function of the library table ([`LibrarySpec.txt`](../lib/Core/LibrarySpec.txt)) that the unit does not -define, before the call (§2.5, amendment 7): +define, before the call (RFC 0035 §2.5 and amendment 7, RFC 0036 §6): - Each pointer argument the row reads or writes is guarded over the bytes its term gives (`bytes(t)`, `count(t)` times the element size, one @@ -310,23 +333,31 @@ define, before the call (§2.5, amendment 7): site)` scans it and fails with `unterminated-string` when the terminator is not addressable. A `printf`- or `scanf`-family call with a constant format guards each `%s` argument as a string and each `%n` argument as an - `int`. Fortified forms (`chk(…)`) are guarded as their base row. + `int`. +- **Fortified forms** (`chk(…)`, RFC 0036 §6). A call of a fortified alias + (`__strcpy_chk(d, s, 8)`) whose object-size argument is not + `(size_t)-1` checks that it writes no more than that size: inline, or by + passing the size to its wrapper as `limit`. A write past the size within + addressable bytes reports `intra-object-overflow`. - A row's `wrapper(NAME)` clause replaces the call with - `__weavec_rt_NAME`, with the call's arguments and the site (first when - the call is variadic). The wrappers (`runtime/weavec_libc.c`) of - `strcpy`, `stpcpy`, `strcat`, `sprintf`, `vsprintf`, `snprintf` and - `vsnprintf` compute what the call writes before it writes, and the - `printf`-family ones check each `%s` and `%n` argument, through a - `va_list` too; those of `memchr`, `strchr`, `strcmp`, `strncmp`, - `strcasecmp` and `strncasecmp` check only the bytes the call read up to - where it stopped; `gets` checks each byte before it writes it; `mmap` - and `munmap` clear the shadow of what they map, so a mapping placed where - a tracked object's poison was left behind is addressable. A wrapper is - named after its row's function (the parser checks it). The calls that - write through no pointer are redirected late, by GuardExpand, so that the - optimiser can still fold `strcmp` of constants; GuardInsert guards only - the first byte of their string arguments. Such a call that reads exactly - a constant of at most 113 bytes keeps inline guards instead. + `__weavec_rt_NAME`, with the call's arguments, a `limit` when the row has + a fortified alias, and the site (first when the call is variadic). The + wrappers (`runtime/weavec_libc.c`) of `strcpy`, `stpcpy`, `strcat`, + `sprintf`, `vsprintf`, `snprintf`, `vsnprintf` and `gets` compute what + the call writes before it writes; the `printf`-family ones check each + `%s` and `%n` argument, through a `va_list` too; the `scanf` family checks + what each conversion wrote; those of `memchr`, `strchr`, `strcmp`, + `strncmp`, `strcasecmp` and `strncasecmp` check only the bytes the call + read up to where it stopped; the number parsers (`strtol` to `strtold`, + `strtoimax`, `strtoumax`, `atoi`, `atol`, `atoll`, `atof`) check after the + call the bytes the C library read (up to the end pointer and the + character that stopped it); `mmap` and `munmap` clear the shadow of what + they map. A wrapper is named after + its row's function (the parser checks it). The calls that write through + no pointer are redirected late, by GuardExpand, so that the optimiser can + still fold `strcmp` of constants; GuardInsert guards only the first byte + of their string arguments. Such a call that reads exactly a constant of + at most 113 bytes keeps inline guards instead. - A row whose result is a `value(t)` computed only from what it reads (`strlen`, `strnlen`) is replaced by the string guard that computes `t`. - A `wide` row scales its `count` terms by the module's `wchar_size`; its @@ -345,30 +376,33 @@ are guarded if `weavec-cc` built it (assumption A2). ## Unsafe regions `WEAVEC_UNSAFE` on a function definition or a compound statement makes its -source range unsafe (§2.6). `UnsafeRegions` collects the ranges by file -over the AST before code generation, only when the unit expanded the macro -or spelled the annotation, and also records as unsafe every function its -author keeps from AddressSanitizer: `no_sanitize("address")`, -`no_sanitize_address` and `disable_sanitizer_instrumentation` (amendment 8), -so a conservative collector's stack scan needs no source change. GuardInsert +source range unsafe (RFC 0035 §2.6). It is the only annotation `weavec.h` +defines; its spelling, `annotate("weavec.unsafe")`, is `UnsafeAnnotation` +in `include/weavec/Frontend/UnsafeRegions.h`. `UnsafeRegions` collects the +ranges by file over the AST before code generation, only when the unit +expanded the macro or spelled the annotation, and also records as unsafe +every function its author keeps from AddressSanitizer: +`no_sanitize("address")`, `no_sanitize_address` and +`disable_sanitizer_instrumentation` (RFC 0035 amendment 8), so a +conservative collector's stack scan needs no source change. GuardInsert leaves each access whose instruction's location lies in a region unguarded (it gets a ledger row with reason `unsafe` and no marker), and its `array-bounds` checks are removed. An access inlined from an unsafe function keeps the location of its source, and so stays unguarded. -`WEAVEC_UNSAFE` is the only annotation that changes generated code. ## The enforcement ledger GuardInsert records each access it collected, before any rule removes its guard, in an `EnforcementLedger` (`lib/Frontend/EnforcementLedger.cpp`, -§9 and amendment 10); GuardPrune and GuardExpand record what became of -each copy of its marker. A row has its function, file, line, column, -operation (`load`, `store`, `rmw`, `copy`, `set`, `lane`, `call:`), -bytes (0 when computed at run time), outcome and reason: +RFC 0035 §9 and amendment 10); GuardPrune and GuardExpand record what +became of each copy of its marker. A row has its function, file, line, +column, operation (`load`, `store`, `rmw`, `copy`, `set`, `lane`, +`disjoint`, `call:`), bytes (0 when computed at run time), outcome and +reason: | Outcome | Reasons | | --- | --- | -| `guarded` | `access`, `range`, `string`, `checked-call` | +| `guarded` | `access`, `range`, `string`, `checked-call`, `loop-range` | | `proven` | `in-bounds`, `dominated`, `merged`, `optimized` (the optimiser removed every copy of the access) | | `unguarded` | `unsafe` | @@ -378,34 +412,34 @@ bytes (0 when computed at run time), outcome and reason: `config` (`checks`, `zeroInit`), `summary` (`accesses`, `proven`, `guarded`, `unguarded`) and `rows`. A path ending in `/`, or an existing directory, receives `.ledger.json` per unit; a file receives one -ledger, and an invocation that would write more than one is an error. Files are written -through a temporary renamed into place. `-fweavec-summary` (the default -when a ledger is written) prints the summary line on standard error: +ledger, and an invocation that would write more than one is an error. Files +are written through a temporary renamed into place. `-fweavec-summary` (the +default when a ledger is written) prints the summary line on standard +error: ``` weavec: lookup.c: 3 accesses: 1 proven, 2 guarded, 0 unguarded ``` -The enforcement ledger is not the analysis's ledger: `guarded` and the -reasons above are words of the enforcing build only. - ## The runtime `runtime/` holds the only code WeaveC links into user programs. It is C, built for the host into two archives installed under `lib/weavec` next to `weavec.h`. [`weavec_rt.h`](../runtime/weavec_rt.h) is its internal -interface (the shadow encoding, the descriptors, the entry points), shared -by its sources and its test and not installed; compiled code reaches the -runtime only through the entry points the guard passes declare, the shadow -descriptor and the standard allocation functions. +interface (the shadow encoding, the slot words, the descriptors, the entry +points), shared by its sources and its test and not installed; compiled +code reaches the runtime only through the entry points the guard passes +declare, the shadow descriptor and the standard allocation functions. | Archive | Source | Holds | | --- | --- | --- | -| `libweavec_rt.a` | [`weavec_alloc.c`](../runtime/weavec_alloc.c) | the size-class arena allocator, its shadow writes, the quarantine and huge blocks; the shadow's reservation | -| | [`weavec_guard.c`](../runtime/weavec_guard.c) | the guards' slow paths (`__weavec_rt_guard`, `__weavec_rt_range`, `__weavec_rt_range_ok`, `__weavec_rt_strlen`, `__weavec_rt_overlap`, `__weavec_rt_null`), the stack's unpoisoning (`__weavec_rt_unpoison_stack`, `__weavec_rt_alloca_poison`, `__weavec_rt_alloca_unpoison`), global registration, and the weak `__ubsan_handle_out_of_bounds[_abort]` | -| | [`weavec_libc.c`](../runtime/weavec_libc.c) | the checked library wrappers `__weavec_rt_`, `mmap` and `munmap` included | -| | [`weavec_owner.c`](../runtime/weavec_owner.c) | the table of entry points `__weavec_rt_dispatch`, and on Darwin the choice of the copy that owns the process | -| | [`weavec_report.c`](../runtime/weavec_report.c) | reports, `__weavec_rt_fatal`, `__weavec_rt_trapping` and the counters | +| `libweavec_rt.a` | [`weavec_alloc.c`](../runtime/weavec_alloc.c) | the size-class arena allocator, its slot words and shadow writes, the quarantine and huge blocks, when to sweep, the allocator's side of the sweep; the shadow's reservation | +| | [`weavec_sweep.c`](../runtime/weavec_sweep.c) | the sweep: stopping the threads, the roots (the image table on Darwin), the conservative mark, recycling | +| | [`weavec_guard.c`](../runtime/weavec_guard.c) | the guards' slow paths (`__weavec_rt_guard`, `__weavec_rt_range`, `__weavec_rt_range_ok`, `__weavec_rt_strlen`, `__weavec_rt_overlap`, `__weavec_rt_null`, `__weavec_rt_limit_fail`), the frames' (`__weavec_rt_unpoison_stack`, `__weavec_rt_alloca_poison`, `_unpoison`), the globals' registration, and the weak `__ubsan_handle_out_of_bounds[_abort]` | +| | [`weavec_libc.c`](../runtime/weavec_libc.c) | the checked library wrappers `__weavec_rt_`, with their limits for fortified forms, the number parsers, `mmap` and `munmap` | +| | [`weavec_asan.c`](../runtime/weavec_asan.c) | ASan's and LeakSanitizer's interface over WeaveC's shadow | +| | [`weavec_owner.c`](../runtime/weavec_owner.c) | the table of entry points `__weavec_rt_dispatch` (version 4), and on Darwin the choice of the copy that owns the process | +| | [`weavec_report.c`](../runtime/weavec_report.c) | reports, `__weavec_rt_fatal`, `__weavec_rt_trapping`, the `WEAVEC_RT_*` settings and the counters | | `libweavec_alloc.a` | [`weavec_malloc.c`](../runtime/weavec_malloc.c) | the standard allocation functions over the arena, for the image the archive is linked into | ### The allocator @@ -416,23 +450,27 @@ followed by the metadata. The classes are 16 to 128 bytes in steps of 16, then four per doubling up to 2^30 bytes. A request takes a slot of the smallest class strictly larger than it, so at least one byte after every object belongs to no object. Each slot has one 32-bit word of metadata, -`size << 2 | state`; free lists and the quarantine queues live in the -metadata, never in freed memory. An allocation writes 0 over the object's -whole granules, k over a partial last granule and `0xFA` over the rest of -its slot (up to 64 KiB); a release writes `0xFD` over the slot. The shadow -of a large object is mapped afresh rather than written, so untouched large -blocks cost no memory. Every block is zero-filled. - -A released slot waits in a quarantine (16 MiB by default, -`WEAVEC_RT_QUARANTINE=`) before it is reused, so a use after free or -a double free traps while the block is in it; after reuse a stale pointer -sees the new object. A request no class can hold is mapped on its own as a -*huge block*, kept in a table, with at least one poisoned granule after it. -The allocator validates every release: a release of an interior pointer, of -a released or never-allocated slot, or of a stack or global object stops -the program through `__weavec_rt_fatal`. A pointer it does not own goes to -the next allocator (the pointer's own malloc zone on Darwin, `__libc_free` -and `__libc_realloc` or the next `free` and `realloc` in lookup order +`value << 2 | state`: a live slot's value is its object's size, a free or +released one's the next slot on its list. Free lists and +the quarantine queues live in the metadata, never in freed memory. An +allocation writes 0 over the object's whole granules, k over a partial last +granule and `0xFA` over the rest of its slot (up to 64 KiB); a release +writes `0xFD` over the slot. The shadow of a large object is mapped afresh +rather than written, so untouched large blocks cost no memory. Every block +is zero-filled. + +A released slot joins its class's *quarantine* and is never reused from +there: only a sweep moves it to the free list (RFC 0036 §3.2), in address +order, so that slots are reused in order. A request no class +can hold is mapped on its own as a *huge block*, kept in a table, with at +least one poisoned granule after it; at release its mapping is replaced by +an inaccessible reservation, unmapped only by a sweep that finds no pointer +to it. The allocator validates every release: a release of an interior +pointer, of a released or never-allocated slot, or of a stack or global +object stops the program through `__weavec_rt_fatal`; a release of a +quarantined block is a double free. A pointer it does not own goes to the +next allocator (the pointer's own malloc zone on Darwin, `__libc_free` and +`__libc_realloc` or the next `free` and `realloc` in lookup order elsewhere). `libweavec_alloc.a` defines `malloc`, `calloc`, `realloc` and `free` @@ -443,19 +481,82 @@ strongly; the rest of the family (`reallocarray`, `aligned_alloc`, over `malloc` replaces it and keeps the arena. On ELF the executable's definitions serve every shared library. +### When to sweep + +Let `gained` be the bytes the quarantine gained since the last sweep (what +the last sweep retained is not counted), `Q` the configured quarantine +(`WEAVEC_RT_QUARANTINE`, 16 MiB by default) and `live` the bytes of live +heap blocks. A release (or an allocation) sweeps before it returns +(RFC 0036 §3.3) when `gained` reaches the *hard* budget `max(2 Q, live / +2)`; when it reaches the *soft* budget `max(Q, live / 4)` and the last sweep +ended at least eight of its durations ago, so that sweeps take at most +about a ninth of the run time and the quarantine grows instead; or when a +class's region is exhausted. The clock is read once per sixteenth of the +soft budget released. + +### The sweep + +The sweep (RFC 0036 §3.4, `weavec_sweep.c`): + +1. **Stop.** Take the image table's lock, every allocator lock and the + sweep lock, then stop every other thread: on Darwin with `task_threads` + and `thread_suspend`, reading each thread's registers with + `thread_get_state`; on Linux by signalling each task of + `/proc/self/task` (`SIGPWR`, `SIGXCPU` when the program handles + `SIGPWR`, or `WEAVEC_RT_SWEEP_SIGNAL`), whose handler records its stack + pointer and waits. A thread that does not stop within a second ends the + sweep without recycling: safety never depends on a sweep running. +2. **Mark.** Scan conservatively, word by aligned word, for addresses of + quarantined slots and released huge blocks (interior addresses count): + - each thread's registers, and its stack from its stack pointer; + - every other readable, writable mapping of the process but the + runtime's own. On Darwin a mapping is asked first for its count of + dirty and swapped-out pages (one call), and skipped when it has none, + or when it is the kernel's reclaim ring; `mincore` then finds the pages + to scan. The dyld shared cache is not walked: its writable mappings + hold the data of every image in the cache. Instead the runtime keeps, + from dyld's add-image and remove-image callbacks, a table of the + writable segments of the images the process loaded from the cache, + and scans those; + - every live heap block and live huge block. + + Released blocks are not scanned. Marks go to a bitmap per class. +3. **Recycle** each quarantined slot without a mark to its class's free + list, and unmap each released huge block without one; marked blocks stay. +4. **Resume** the threads and release the locks. + +The sweep runs on the thread that asked for it and calls nothing that takes +a lock a stopped thread could hold: no allocation, no stdio, no dyld. With +little live heap a sweep takes about a millisecond on Darwin; it grows with +the live heap and the dirtied writable data. + +### ASan's interface + +`weavec_asan.c` (RFC 0036 §5) defines, with ASan's signatures and meaning, +`__asan_poison_memory_region` (whole granules inside the region become +`0xF7`, `use-after-poison`), `__asan_unpoison_memory_region`, +`__asan_address_is_poisoned`, `__asan_region_is_poisoned`, +`__asan_describe_address`, `__asan_handle_no_return` (which clears the +stack's shadow, as before a `noreturn` call), +`__asan_get_current_fake_stack`, `__asan_addr_is_in_fake_stack`, +`__sanitizer_print_stack_trace`, `__sanitizer_set_report_path`, +`__sanitizer_annotate_contiguous_container`, the fiber-switch +announcements, and LeakSanitizer's entry points. What ASan does that +WeaveC does not (leak checking, fake stacks) does nothing. + ### One runtime per process on Darwin Every image linked by `weavec-cc` carries the archives. On Darwin the copy whose `__weavec_rt_dispatch` `dlsym(RTLD_DEFAULT, …)` finds is the *owner*; every other copy takes the owner's arena and shadow descriptors and -forwards to it, so one arena, one shadow and one quarantine serve every -image (RFC 0033 §6.2). A table of another layout (another WeaveC version's -runtime) is not used. The arena is also registered as a malloc zone and -promoted to the process's default zone, so `malloc` from any image, the C -library's own (`strdup`, `getline`, `asprintf`) included, is served by the -arena; blocks the system's zones allocated before that go back to them. -Start-up, the shadow's and the arena's reservations included, costs about -1 ms. +forwards to it the allocation and the global registration entry points, so +one arena, one shadow and one quarantine serve every image (RFC 0033 §6.2, +RFC 0036 §4). A table +of another layout (another WeaveC version's runtime) is not used. The arena +is also registered as a malloc zone and promoted to the process's default +zone, so `malloc` from any image, the C library's own (`strdup`, +`getline`, `asprintf`) included, is served by the arena; blocks the +system's zones allocated before that go back to them. ### Reports @@ -466,14 +567,15 @@ A failed guard prints one line on standard error and traps (`SIGTRAP`, or weavec: at ::: of bytes at 0x ``` -with `` for a missing location. A heap address gets a second line -that places it (`… is bytes after the -byte heap object at …`, or -`… is inside a released heap block at …`). The kinds are +with `` for a missing location. For the heap a second line places the address: +`… is bytes after the -byte heap object at …`, or `… is inside a +released heap block at …`; for a fortified call, +`… is bytes after the -byte destination at …`. The kinds are `heap-buffer-overflow`, `heap-use-after-free`, `stack-buffer-overflow`, -`stack-use-after-scope`, `dynamic-stack-buffer-overflow`, -`global-buffer-overflow`, `null-dereference`, `unterminated-string`, -`overlapping-copy`, and `buffer-overflow` or `invalid-access` for poison -the runtime cannot attribute. An index check prints +`dynamic-stack-buffer-overflow`, `stack-use-after-scope`, +`global-buffer-overflow`, `intra-object-overflow`, `use-after-poison`, +`null-dereference`, `unterminated-string`, `overlapping-copy`, and +`invalid-access` for poison the runtime cannot attribute. An index check prints `weavec: index-out-of-bounds at ::: index `, and a monitor that fails in verify mode `weavec: weavec.proven: at …`. The allocator stops an invalid release with @@ -485,15 +587,19 @@ program may define) and without allocating. Before every trap, resets an ignored disposition, so the trap ends the program even when it blocked them (a handler the program installed still runs). In report mode (`-fweavec-checks=report`) each site reports once and the program goes on, -with no guarantee; `WEAVEC_RT_ABORT=1` makes report mode trap. -`WEAVEC_RT_REPORT_LOG=` appends reports to that file instead of -standard error (ignored in set-uid programs). `WEAVEC_RT_STATS=1` prints -the runtime's counters at exit as `weavec: runtime: ` -(allocations, releases, recycled slots, huge blocks, slow guards, range -guards, string guards, stack unpoisons); on Darwin only the owner prints, -counting every image's work. Run-time reports are not diagnostics and have -no ids. The case runner and the corpus gate attribute traps through this -output. +with no guarantee. The settings: + +| Variable | Effect | +| --- | --- | +| `WEAVEC_RT_ABORT=1` | report mode traps | +| `WEAVEC_RT_REPORT_LOG=` | append reports to that file instead of standard error (ignored in set-uid programs) | +| `WEAVEC_RT_BACKTRACE=1` | print the stack of the access that stops the program | +| `WEAVEC_RT_STATS=1` | print the counters at exit as `weavec: runtime: ` (allocations, releases, recycled slots, huge blocks, slow guards, range guards, string guards, stack unpoisons, sweeps), then the sweeps' total and longest pause, what the last one retained, and the peaks of the live slots and of the quarantine; on Darwin only the owner prints, counting every image's work | +| `WEAVEC_RT_QUARANTINE=` | the quarantine's least growth before a sweep (`Q`) | +| `WEAVEC_RT_SWEEP_SIGNAL=` | the signal number that stops threads for a sweep on Linux (default `SIGPWR`, or `SIGXCPU` when the program handles `SIGPWR`) | + +Reports have no ids: the kind and the line format are the interface. The +case runner and the corpus gate attribute traps through this output. ## Linking @@ -519,709 +625,106 @@ When an input of the link defines a symbol that `libweavec_alloc.a` defines strongly (`allocatorDefinedBy` reads the set from the archive itself), `dropAllocatorIfDefined` takes the archive and its `-u` off the line and prints a note: the program keeps its allocator, its heap is -untracked, guards pass on it, and releases are not validated. Stack and -global objects are still guarded. +untracked (unless it poisons it through ASan's interface), guards pass on +it, and releases are not validated. Stack and global objects are still +guarded. A Darwin link with `-flto` gets `-lto_library` naming the libLTO of the -LLVM WeaveC was built with, when the driver's default does not exist. There -is no link step: a link analyses nothing. - -## The advisory analysis - -The analysis reads the AST of a unit and decides, for each safety facet -(spatial, null, temporal) of each memory operation, or *site*, one outcome: -`proven`, `violation`, `unresolved` with a reason, or `trusted` with a -reason (RFC 0030 §2 as RFC 0035 §8 leaves it). It reports diagnostics: a -definite finding is an error and a possible temporal finding a warning. The -outcomes stay in memory and feed a summary line; nothing the analysis -concludes reaches the generated code. - -- **`weavec`** (`tools/weavec`) analyses each source of its command line or - compilation database (with `-p` and no source, every file of it), prints - the diagnostics, and exits 1 after an error. A compile command that asks - for `-fdiagnostics-format=sarif` is an error. It prints one summary line - per unit: - - ``` - weavec: example.c: 11 sites: 6 proven, 3 not proven, 2 violations, 0 trusted; 2 errors, 1 warning - ``` - - where *not proven* counts `unresolved` sites. -- **`weavec --whole-program`** analyses the files as one program - (`ProgramAnalysis`, RFC 0005): it discovers every unit's exports, orders - the units by strongly connected component, analyses acyclic units once - and cyclic groups to a fixpoint against a program database of the - summaries analysed so far, and publishes in the last round only. - `ProgramChecks` solves the program's function-pointer slots, propagates - the boundary rows, and verifies each declaration's annotations and kinds - against the defining unit (`annotation-mismatch`), from each unit's - `InterfaceFacts`. Everything stays in memory. It adds a program line: - `weavec: program : N sites in M units: …`, named after the unit that - defines `main`. A group of units that cannot be made to converge keeps - its widened summaries, with a note. -- **`weavec-cc -fweavec-diagnose`** runs the analysis beside Clang's code - generator in each compile job, per unit, with every finding a warning - (`-Werror=weavec` makes them errors) and `leak` off by default - (`-Wweavec-leak`). It prints no summary line, and the code is the same as - without it. `-fweavec-budget=` and `-fweavec-unit-budget=` bound its - work. - -## `weavec::Core` — the model - -`lib/Core` holds the model and depends only on the C++ standard library, so -it can be unit-tested without parsing code and reused by another frontend. -It never sees a `clang::VarDecl`, only an opaque `core::Handle` the -Analysis layer assigns, and never a `clang::SourceLocation`, only a -`core::SourceLocation` whose `opaque` field the frontend fills in. - -### The ledger and its companions - -| Header | Purpose | -| --- | --- | -| `Ledger.h` | Sites, facets and outcomes (RFC 0030 §2, RFC 0035 §8): `SiteKind`, `Facet`, `SiteOutcome` (`proven`, `violation`, `unresolved`, `trusted`), the closed reason lists, merging by rank, the defaults for undecided facets, `UnitLedger` and `Ledger` with requirement records and diagnostics, and the rollup behind the summary lines (`unitSummaryLine`, `programSummaryLine`). | -| `PointerKind.h` | The kind lattice (§7.1): `single`, `counted(e)`, `sized(e)`, `ended-by(q)`, `nul-terminated` or `unknown`, with nullability, a source (declared, inferred, default) and an `ExtentTerm` over a sibling parameter or field. Every kind is a lower bound. `ExtentClass` says whether an extent is exact, declared or a lower bound; only an exact extent can make an access a violation. | -| `LibrarySpec.h`, [`LibrarySpec.txt`](../lib/Core/LibrarySpec.txt) | The one declarative table of C library, POSIX, platform and builtin functions (§8), and its parser: per argument, the access, required length, nullability, ownership effect, release family, state slots and callback clause; per call, the result, disjointness, `exits`/`noreturn`, format arguments, fortified aliases, the `wrapper(NAME)` the guard passes redirect to, and `wide`. It also lists the platform headers of §5.2. CMake embeds the text. Both the guard passes and the analysis read it. | -| `FnSlots.h` | Function-pointer slots (§9.3): the constraints `f ∈ S`, `S ⊆ T` and `open(S)` over field, global, parameter, result and local slots, and the solver that computes each slot's targets and whether it is closed. | - -A site is one operation in one emitted function, identified by -`{function, ordinal, kind, location}`. Its kind is `deref`, `index`, -`ptr-arith`, `cast`, `int-to-ptr`, `lib-call`, `release`, `call` (a call or -a function exit), `assume` or `raw` (§2.1), and a facet exists only where it -has meaning. `assume` sites have a fourth facet, *assertion*. Records of one -facet merge by rank within one pass: `violation > unresolved > trusted > -proven`. The unresolved reasons are `undecided` (the facts neither prove -nor refute the facet), `unknown-extent`, `unknown-index`, `inexpressible`, -`may-released`, `may-moved`, `may-alias-released`, `may-invalid-release`, -`may-mismatched-release`, `may-dangle`, `may-conflict`, `unknown-callee`, -`callback`, `setjmp`, `budget`, `unanalysed`, `raw-cast`, `dangling-escape`, -`second-owner`, `no-zero-init` and `unconfirmed` (a definite finding the -replay did not confirm). The trust reasons are `unsafe`, `system-api`, -`library-spec`, `extern-contract`, `caller-contract`, `external-unit` and -`concurrency`. Adding a reason requires an RFC. - -### The abstract domain - -The object engine's domain (RFC 0031 §4) is in Core, so it is unit-tested -without Clang. Questions only the frontend can answer (whether two types may -name one object, the value of a cell this activation never wrote) go -through the `HeapOracle` interface the engine implements. - -| Header | Purpose | -| --- | --- | -| `Heap.h` | Symbols, objects, cells and states (§4.1–§4.9). A `Sym` names one runtime value; `SymInfo` is what is known of it: for an integer its C type, defining operation and linear form; for a pointer its points-to targets (at most 8 `(object, offset)` pairs, else "any object"), nullness with the `allocatorSource` bit, `ReleaseRecord`, share count, raw origin, owning-slot ancestors (D6) and pending result cases; for a function value a set of at most 32 functions. `ObjectTable` interns objects per function by their origin (`Local`, `Global`, `Literal`, `Function`, `HeapRecent`, `HeapOld`, `Entry`, `EntrySummary`, `Materialized`, `Focus`, `CallResult`, `Unknown`), so two states name an object the same way. `ObjectState` holds an object's cells (by `CellKey`: a byte offset, a selected element cell over an index symbol, or the summary cell of an element position), element segments, extent, life (`Live`, `Released`, `MayReleased`, `UnknownReleased`, `Ended`, `MayEnded`), release record, family and ownership, and string facts. `HeapState` is one program point: objects, symbols, the zone and the values of expressions carried between blocks. `Heap` is the operations: loads and stores (strong or weak), releases, the temporal and spatial verdicts, distinctness (D1–D6), join, widening and garbage collection. | -| `Zone.h` | The numeric domain (§4.4): closed difference bounds `x − y ≤ c` between integer symbols, with symbol 0 for the constant zero; relational bounds for at most 64 symbols per state, the rest keep bounds against constants only. Joins and widenings are paired: the heap decides which symbol of each side a result symbol stands for. | -| `Persistent.h` | `PMap`, the sorted vector shared by reference count and copied on the first write through a shared handle, so a state is copied along a CFG edge for one reference (§4.8). | -| `Path.h` | `SummaryPath`: a root (`param(i)`, `global(g)` or `result`) followed by dereference, field and index steps. Summaries, boundary facts and pointer kinds name places this way. An anonymous or positional member is spelled by its byte offset (`.#16`). | -| `Effects.h` | The summary, `FunctionEffects` (see *Summaries* below), with the join, the widening and the global renumbering the program database needs. | -| `Integer.h` | Target integers of 1 to 64 bits, modular ranges of at most two intervals, conversions, and the checked arithmetic of invalid operations (RFC 0017). | -| `Diagnostic.h` | `Diagnostic`, `Certainty`, the ids in `diag::` with their default severities, `FixItHint` and `DiagnosticSink`. | -| `SourceLocation.h`, `Scc.h`, `AnalysisStats.h` | Frontend-neutral positions with an `opaque` slot; Tarjan's strongly connected components for call and unit graphs; work counters, never part of a proof. | - -### Certainty - -Every diagnostic is *definite* or *possible* (RFC 0030 §3). The -`ReleaseRecord` of RFC 0030 §3.1 lives on values and objects rather than on -places. It is definite when it has `allPaths` (it holds on every path merged -since), is not `conditional` (from an effect that holds only on some outcome -classes or paths, or from a `lossy` one), is not of unknown origin (the -unknown-callee default or an open slot, never diagnosed) and is not -`aliasOnly` (made by a weak merge of values that elements or aliases do not -tell apart, never diagnosed). A pointer's `allocatorSource` bit keeps a null -allocation result an `unresolved(undecided)` null facet rather than a -`null-dereference` error. A release that conflicts with a stored borrow is -a `conflicting-borrow` error only when the cell holds the borrow on every -path and the release is unconditional. A join keeps a record present on one -side only with `allPaths` cleared, and joins the lives of an object released -on one side and live on the other into `MayReleased`. A correlated bug is -therefore a warning, not an error. - -### Summaries - -`FunctionEffects` (`Effects.h`) is what a function does to its *entry -heap*, the objects its parameters and the globals reach, named by -`SummaryPath`s, and what it returns (RFC 0031 §6.1). It holds whether the -function returns, whether the summary is incomplete (and why), path effects -(release with family and interior offset, move, the unknown-callee default, -escape, share up and down), stores into entry cells, result alternatives -(null, a fresh object of a family with an extent term over the parameters, -an entry path, static storage, an integer range, a pointer to the callee's -dead frame), non-null facts per result class (RFC 0030 §9.2), and the paths -it reads and writes. Effects, stores and results carry RFC 0030 §9.1's case, -result classes optionally narrowed by a parameter's zero test, with `may` -and `lossy` bits; an effect on array elements carries its element range -(§4.9). Summaries stay in memory; `toText` spells one for the dumps. -Pointer kinds are not part of the summary. - -## `weavec::Analysis` — the bridge - -`lib/Analysis` is the only library allowed to include both `weavec/Core/*` -and `clang/*`. Besides the engine (next section), it holds shared services -and the components around the engine seam. - -| Service | Role | -| --- | --- | -| `Annotations.h`, `ClangLocation.h` | Recognise WeaveC annotations on declarations, statements and function-pointer types, including a function's ownership signature; convert source locations both ways. | -| `ProgramDatabase.h` | What a unit exports (`UnitExports`: summaries with linkage and type keys, imports, indirect-call types, boundary rows, context requests) and the database of other units' exports, joined by name (`findEffects`) and by function type for indirect calls (`candidateEffects`), with globals renumbered by name (RFC 0031 §7). | -| `KindTable.h`, `SourceTerm.h` | The kind of every parameter, result and slot with its source (§7); a quantity spelled over C names at a site, with which the engine names the symbols of its messages (`'b->cap' bytes`). | -| `Concurrency.h`, `BypassedDeclarations.h` | The shared set G of RFC 0030 §5.3 (what threads and signal handlers reach); the locals a jump can bypass, so zero-initialisation does not reach them (§11). | - -Before the engine, these components read the AST and the `LibrarySpec`, with -no engine fact: - -- `AttributeReader` reads the declared kinds (§7.2): the `WEAVEC_*` extent, - string and nullability macros, Clang's `counted_by`, `sized_by`, - `alloc_size` and nullability attributes, and `[static N]` and VLA - parameters. `WEAVEC_*` annotations win over ecosystem attributes, which win - over the `LibrarySpec` entry and then system-header attributes; a finding - that rests only on the last is `trusted(system-api)`. -- `KindInference` fills the rest of the `KindTable` (§7.3–7.6): parameter - kinds with their `reliesOnSingle` flags, result kinds, slot kinds (a - greatest fixpoint that demotes `single` to `unknown` at any store that is - not Single-valid), the must-access requirements R1–R5, store groups, and - the counted-field candidates of §7.6 with their disqualifications - (`weavec --dump-kinds` prints them). The engine runs the Houdini rounds - over the candidates of the records the unit defines in its main file - (`EngineInvariants.cpp`, RFC 0031 *Implementation amendments*); a - surviving invariant gives its pointer field an exact extent. -- `SiteCollector` enumerates the sites of every emitted function, with their - ordinals and facets, into the `SiteIndex` and the unit's undecided rows - (§2.6). It runs after the kinds, because PtrArith and Cast sites exist - only in required positions. -- `SlotCollector` collects the unit's function-pointer constraints for - `FnSlots` (§9.3). - -After the engine, `BoundaryInvariants` checks at every call boundary and -function exit that no place reachable from a parameter or global may hold a -released or dangling pointer (`unresolved(dangling-escape)`), and that no -two owning places may hold the same object (`unresolved(second-owner)`). At -a call the reachable places are the objects the arguments point to and the -globals; at the exit that returns to the caller they are the places whose -storage has died, the result included, because a release there is already -in the summary. It then downgrades every temporal facet the unit *proved* -for a place of a broken class, which is the facet that relied on the entry -assumption the boundary broke (§9.4). Under `--whole-program` the other -units' rows come along, so the propagation is program-wide. -`LedgerAdapter`, `SafetyEngine` and `ObjectEngine` form the seam described -in *The engine seam*. - -`UnitPipeline` (`runUnitAnalysis`) runs the analysis of one unit. It builds -the kinds and the unit's slot solution, collects the sites, runs -`ObjectEngine` through an authoritative `LedgerAdapter` (a discarding one -for a silent fixpoint round), calls `finish`, and reports the diagnostics -in publication order. A discovery-only run returns the unit's exports -(`ObjectEngine::discover`) without analysing it. The §7.6 field candidates -it passes are empty. The Frontend calls it through -`analyzeTranslationUnit`. - -## The engine: the object engine - -`ObjectEngine` (`include/weavec/Analysis/ObjectEngine.h`) is the prover -behind the seam ([RFC 0031](rfcs/0031-object-engine.md)). Its facts live on -abstract objects and symbolic values: a pointer is a symbol with a -points-to set, memory maps object cells to symbols, and copying a value -copies its symbol, so every alias of a value sees every fact about it and -about the objects it points to. The engine's classes are declared in the -private header `lib/Analysis/Engine.h`: - -| File in `lib/Analysis` | Responsibility | -| --- | --- | -| `EngineUnit.cpp` | `ObjectEngine` and `UnitRun`: the unit's call graph, summary rounds, the authoritative pass, owning slots, imported summaries and exports (§3, §7). | -| `EngineRun.cpp` | `FunctionRun`: the always-add CFG, the entry state, the fixpoint with widening at loop heads, the final (publishing) pass, object interning, materialisation of the entry heap, and the replay that confirms definite findings (§3, §4; RFC 0034 §6). | -| `EngineExpr.cpp` | `Transfer`'s evaluation: rvalues to symbols, lvalues to addresses (`evaluate`, `addressOf`), loads, stores, casts, pointer arithmetic, integer operations and conversions (§5.1, RFC 0017). | -| `EngineCalls.cpp` | `CallApplier`: callee resolution and the effects of summaries, contracts, library rows, platform declarations and unknown callees; indirect calls; alias contexts (§5.4, §6.6). | -| `EngineSummary.cpp` | Summary derivation at the exits and instantiation at calls (§6.2, §6.3). | -| `EngineDecide.cpp` | `Decider`: the temporal, null and spatial facets of every site, releases, invalid releases and conflicting borrows (§5.2, §5.3, §5.5). | -| `EngineLibrary.cpp`, `EngineStrings.cpp` | The requirement records of `LibrarySpec` arguments (each buffer argument's need against what it points into), `disjoint` ranges and format calls; RFC 0012's string facts over objects. | -| `EngineKinds.cpp` | What the kinds give the engine: parameter extents and nullness at entry, the accesses a must-access requirement covers, and the requirement records at calls (RFC 0030 §7.2–§7.5). | -| `EngineContexts.cpp` | Contexts across units: the portable keys of the contexts a call asks of another unit's function, and the runs that serve the contexts other units asked (§7 *Amendment (cross-unit contexts)*). | -| `EngineInvariants.cpp` | Counted-field invariants: Houdini over `KindInference`'s candidates, and the functions that read a standing invariant's field analysed again with it (RFC 0030 §7.6, restored by RFC 0031). | -| `EngineLifetimes.cpp` | Frame storage whose lifetime ended, the boundary facts of calls and exits with their place classes (the class of a cell is the field that holds it, `fieldHolding`, so an element of an array is of its array's class), `WEAVEC_ASSUME`, raw-pointer laundering (§5.6, §5.7). | -| `EngineAnnotations.cpp` | `annotation-mismatch` of a definition against its own declaration (RFC 0003 reconciliation, RFC 0012 sized-field stores). | -| `EngineIntegers.h` | Target integer types of Clang types and bit-fields, the overflow builtins and operators (RFC 0017). | - -### The domain - -Every value is a symbol (§4.1): an integer, whose numeric facts live in the -zone; a pointer, with its targets, nullness, release record, share count, -raw origin and the owning slots it was derived from; a function value; or -unknown. An object (§4.2) has an origin that names it deterministically: a -local, a global, a literal, a function, the most recent or the older -allocations of a site (the recency abstraction), an entry object named by a -`SummaryPath` from a parameter, a global or a callee's result, a k-limited -entry summary, a callee's result, or the unknown object behind a raw -pointer. A singular object stands for one runtime object and takes strong -updates and definite releases; the others take weak ones. Cells are keyed -by byte offset from Clang's record layout; a variable index names a -*selected* cell over its index symbol, and what stores through indices the -engine cannot name wrote goes to the element position's summary cell. -Segments `[from, to) ↦ v` describe ranges of elements, so a cleanup loop -releases a range and a summary can say so (§4.9). Bytes that code the engine -does not see rewrote are *forgotten*: the whole object (`havocked`), a byte -range (a member copied over, a union a callee wrote), or a range forgotten on -some paths only, where an unwritten cell reads as its value otherwise merged -with an unknown one. A value also records the entry cells it was computed -from (`entryOrigins`), which the boundary propagation follows (RFC 0031 -*Implementation amendments*). - -Entry objects are materialised on first load, with the kind of the slot -they were loaded from (§4.6). An entry path in which one field repeats more -than twice, or longer than 6 steps, folds into an entry summary object. At -a join where a variable points to a different object on each side, the join -makes a *focus* object with the two as candidates; an entry or focus object -that a path released and no root reaches any more becomes a *dead copy*, -which keeps the release for the summary without overlapping the live object -of the next iteration (RFC 0031 *Implementation amendments*). Garbage -collection at every block end drops what no root (the locals, the -parameters' cells, the globals, the carried expression values, the result) -reaches; dropping an owned, unreleased, unescaped allocation is a leak. - -Two pointers may be equal unless one of the distinctness rules D1–D6 (§4.5) -separates their targets: incompatible types under C's effective-type rules, -two owning places, the owner forest (an object reached through an owning -step is distinct from the objects on its path), freshness, identity of two -singular objects the engine created, and derivation through owning slots. -The owner forest is the acyclicity clause RFC 0031 adds to assumption A3. - -### Transfer - -`FunctionRun` evaluates Clang's CFG built with `setAllAlwaysAdd()`, so -every subexpression is an element in evaluation order and `?:`, `&&`, `||` -and `,` need no special order; an expression's value is kept in the block's -memo, and values a later block reads travel in the state (§2). There is no -separate IR. Blocks are visited from a worklist in reverse post-order; a -loop head widens after two joins, jumping grown bounds to the program's -constants or the type's limits. A condition edge adds its constraint to the -successor's zone and prunes the edge when it is unsatisfiable (§4.4). -States are persistent maps, so a state is copied per edge for one -reference and a join costs the size of the difference (§4.8). - -Loads and stores go through addresses (§5.1): `x` is its object's cell, -`*e`, `e->f` and `e[i]` add the field's offset or the scaled index to the -pointer's targets, pointer arithmetic moves the offset term, and pointer -casts keep the symbol. An integer-to-pointer conversion makes a pointer of -unknown provenance to the unknown object, whose accesses are -`unresolved(raw-cast)` (RFC 0033 §2: only `WEAVEC_RAW` declarations make -raw values, and a merged value is raw only when every value merged is); a -pointer read back from reinterpreted bits is `raw-cast`; a construct the -engine does not evaluate yields unknown values and `unresolved(unanalysed)` -for its sites. `memcpy`, `memmove` and record assignments copy leaf by -leaf, every source cell read before any is written. A run whose work (the -sizes of the states it transfers and joins) exceeds the budget -(`--budget`, `-fweavec-budget`), whose entry states hold more than -1,000,000 symbols and objects, whose graph has more than 100,000 blocks, or -that visits one block more than its limit stops (RFC 0034 §7): its facets -take the defaults with reason `budget`, and its summary is incomplete. - -### Confirmation - -A definite finding of the authoritative run is a candidate -(`FunctionRun::confirmCandidates`, RFC 0034 §6). The function is replayed -in witness mode: a depth-first search along single paths with no joins -(at most 256 paths and 20,000 transfers), each block transferred with the -evidence its fixpoint entry state had, a loop left from its fixpoint state -after two turns, and an edge that refinement or a symbol's excluded -constants (`SymInfo::excluded`) empties ending the path. A candidate some -feasible path reaches, and that every such path decides alike, stays an -error; otherwise `LedgerAdapter::unconfirm` makes its facet -`unresolved(unconfirmed)` and its diagnostic a warning with the note -`not confirmed on a feasible path`. - -### Decisions - -After the fixpoint, the final pass transfers each reachable block once more -from its entry state and decides every site `SiteCollector` enumerated -there (§5.2), publishing through `LedgerAdapter`. The tables of RFC 0030 §3 -apply; their premises are read from the operand's value at the site. A -definite release record on the value is a violation and a possible one a -warning; a released target the value has no record of is -`unresolved(may-alias-released)`; a target an unknown callee reached, or a -pointer into the unknown object, is `unresolved(unknown-callee)`. Null -facets are proven, `unresolved(undecided)` or, for a definite null that is -not an allocation result, a violation. A spatial access is compared, in the -zone, with the extent of every target: in bounds for all targets is proven, -out of bounds for every value against an exact extent is a violation, an -undecided access against an exact or declared extent is -`unresolved(undecided)`, and one that only a lower-bound kind covers -`unresolved(unknown-extent)`. Diagnostics are reported once per site and -id, with RFC 0030's messages and notes taken from the records. Nothing is -suppressed in a `WEAVEC_UNSAFE` region, and in a function that calls -`setjmp` every temporal facet is `unresolved(setjmp)`. - -### Calls - -A call's own sites and its boundary facts are decided from the state before -its effects. `CallApplier::applyDirect` then resolves a direct callee -(§5.4): the summary of a definition in the unit, or under `--whole-program` -the program database's (`UnitRun::summaryOf`); a declared ownership -contract that covers every pointer argument; the declaration's ownership -annotations; the `LibrarySpec` row; a platform-header declaration, which -borrows its arguments under `trusted(system-api)`; else the unknown-callee -default of RFC 0030 §5.1. That default marks every object reachable from a -pointer argument through non-`const` pointees, every escaped object and -every object reachable from an externally visible global as -`UnknownReleased` and forgets their cells; a cell whose address is passed -(`&cmd`) holds a fresh value of unknown nullness afterwards. A library row's -`alloc` creates a recent heap object of the row's family with the extent the -row gives, and its argument requirements become requirement records; -lengths that may be zero make the null requirement `null-if-zero`. - -Indirect calls resolve through the flow-sensitive function value, then the -slot solution (RFC 0030 §9.3). Several targets are each applied to a copy -of the state and the results joined. An open slot without targets gets the -unknown-callee default with reason `callback`. At a call whose pointer -arguments (or globals) point into the same object, the callee is re-analysed -in an *alias context* with those entry objects unified and the constant -integer arguments bound, at most 16 contexts per callee and 3 deep (§6.6). -The context run's adapter only collects; each use-after-free, double free or -use-after-move it finds is reported with the note "called here with related -pointer arguments" and linked to the call's temporal facet. Under -`--whole-program` a call into another unit asks the defining unit for the -callee's context (`EngineContexts.cpp`, RFC 0031 §7 *Amendment (cross-unit -contexts)*), which runs it, exports its summary and reports what it finds; -a run that asked a context not yet served reports nothing until it has -been. - -### Summaries - -A summary is read off the exit states against the entry heap (§6.2). An -entry object that is released, moved or reached by an unknown callee gives -the effect for its path; a cell of an entry object or global that was -written gives a `store`, told apart from an unchanged cell by the entry -value each materialised symbol remembers; a result pointing to a new object -gives a fresh result, with its extent re-expressed over the parameters -when the zone relates it to one, and the new object's contents as stores -below `result`. An exit whose result carries pending cases is derived once -per result class. Effects on some exits only are keyed by the result -classes that separate them, by a parameter's zero test, or both, and are -otherwise possible; a release records what the state knew of the releasing -function's unmodified integer parameters (RFC 0031 *Implementation -amendments*). - -At a call the summary is instantiated (§6.3): each path is walked through -the caller's memory from the arguments and globals, materialising entry -objects as a load would, every store's place and value are read before any -is written, and each effect is applied to the objects the path reaches, -weakly when it reaches several. A fresh result becomes a heap object named -by the call site. A case keyed on the result becomes a *pending case* on the -result symbol, applied when a later test of the result selects its class; -cases the arguments already decide are applied at the call. An incomplete -summary adds the unknown-callee default to its known effects (RFC 0030 -§5.5). - -### The unit driver - -`UnitRun` drives a unit (§3). It computes the unit's owning slots (the -fields and globals some function releases a value loaded from, D2), builds -the call graph over the unit's definitions (direct calls, plus the targets -the slot solution gives indirect calls), and takes its strongly connected -components bottom up (`core::Scc`). A recursive component iterates its -members' summaries from empty through a discarding adapter, at most 8 -rounds; summaries that have not converged are marked incomplete. Then each -member gets one run: authoritative (`LedgerAdapter::beginFunction`) for a -function the unit reports, a summary run otherwise. Outside a cycle that run -also produces the function's summary, so a leaf function is analysed once. -`exports()` gives the summaries of the external and address-taken -definitions, with globals renumbered by portable name, and the unit's -imports and indirect-call types; `discover()` gives the same without -analysing anything. - -Under `--whole-program`, the same driver runs with the program database and -the program-wide slot solution (RFC 0030 §13.2 as RFC 0035 §8 keeps it). A -callee another unit defines is known by its imported summary, whose globals -are renumbered into the unit's; an effect through a global the importing -unit does not declare makes the summary incomplete there. An indirect call -that neither its value nor the slots resolve reaches every address-taken -function of its type, the unit's own and the database's joined candidate -summary; in a unit alone it keeps the unknown-callee default. - -`SafetyEngine::dump` is the hook of `--dump-analysis`: `ObjectEngine::dump` -re-runs a function and prints the entry state of every block, its objects -with their kind, name, extent and life, their cells with the symbols they -hold, and the zone. With `WEAVEC_ENGINE_DUMP` set, the engine prints every -summary it computes or imports to stderr; `WEAVEC_ENGINE_DUMP=2` adds each -run's exit states and `WEAVEC_ENGINE_DUMP=3` each run's block states. - -RFCs [0001](rfcs/0001-ownership-model.md) (model), -[0002](rfcs/0002-intraprocedural-checking.md) (dataflow), -[0003](rfcs/0003-signature-inference.md) (summaries), -[0004](rfcs/0004-unsafe-boundaries.md) (unsafe boundaries), -[0005](rfcs/0005-whole-program-analysis.md) (whole program), -[0006](rfcs/0006-precision.md) (precision), -[0007](rfcs/0007-resource-lifecycle.md) (resources), -[0008](rfcs/0008-pointer-validity.md) (validity) and -[0009](rfcs/0009-value-conditional-behaviour.md) (guards) specify the model. -RFC 0030 restates them and RFC 0031 restates over objects how their facts -are represented; RFC 0035 §8 keeps that analysis as an advisory one. - -## The engine seam - -Everything an engine produces flows through one interface (RFC 0030 §14), -which is how RFC 0031 replaced the engine by implementing `SafetyEngine` -without touching the ledger, kinds or library table. The types are in -`include/weavec/Analysis/SafetyEngine.h` and `LedgerAdapter.h`. - -`EngineInput` is everything an engine gets for one unit: the `ASTContext`, -the `SiteIndex`, the `KindTable` with what `KindInference` found besides it, -the `LibrarySpec`, the unit's slot constraints and their solution (local, -or program-wide under `--whole-program`), the `ProgramDatabase` of the other -units, the field candidates assumed at entry, and `EngineOptions` (the -function and unit budgets, zero-initialisation, strict aliasing, the -functions to report, the dump stream and statistics). `LedgerAdapter` is -the only channel back: - -| Method | Carries | -| --- | --- | -| `beginFunction` | the start of a function's authoritative pass; rows from earlier passes are discarded | -| `decide` | one outcome, with reason and detail, for one facet of a known site; records merge by rank | -| `decideAs` | the same, naming the site kind and boundary, for a statement that stands for several sites (a call that does not return, and its exit) | -| `suggest` | the annotation a row's fix-it offers; the first suggestion of a pass stands | -| `requirement` | one requirement record of a LibCall, Release or Call facet, kept with its own outcome | -| `report` | a diagnostic with its certainty, linked to its site and facet | -| `boundary`, `reliesOn` | the places reachable from parameters and globals that may hold released pointers or aliased owners, with place classes (an owning cycle is an aliased-owner fact with its `cycle` flag, RFC 0031 §8); the place class a proven temporal facet rests on | -| `overBudget` | a function that exceeded its budget | -| `storeVerdict` | a store group's verdict on a field-invariant candidate: holds, violated or unknown (kept for §7.6; the object engine publishes none) | -| `unconfirm` | a definite finding the replay did not confirm | -| `finish` | fills the defaults, applies the unsafe, `setjmp`, concurrency and boundary rules, and returns the unit's `core::Ledger` | - -An adapter is authoritative, discarding (summary and fixpoint rounds keep -nothing), collecting (alias-context runs decide no row and keep their -diagnostics for the caller), or a witness (the confirming replay: nothing is -published, every decision goes to an observer). A decision about a statement -`SiteCollector` did not enumerate is an internal error, and an -`unresolved(unanalysed)` row in a release build. - -`SafetyEngine` is what an engine implements: `analyzeUnit(input, out)`, -`exports()` for the program database, and `dump(function, os)` for -`--dump-analysis`. `ObjectEngine` implements it. - -Two rules keep the seam honest, and gate H2 (`scripts/check-hygiene.py`, -RFC 0031 §2) checks both: - -- The engine publishes nothing except through `LedgerAdapter`, diagnostics - included. It receives no `DiagnosticSink`. -- Only the `Engine*.cpp` files include `lib/Analysis/Engine.h`. - `SiteCollector`, `AttributeReader`, `KindInference`, `SlotCollector`, - `BoundaryInvariants` and `LedgerAdapter` itself never reach it. - -## Heap postconditions (RFC 0013) - -[RFC 0013](rfcs/0013-interprocedural-heap-state.md)'s constructors and -returned fields are ordinary summary content in the object engine. A result -pointing to an object the function created is a fresh result, and the -object's cells, down to the objects they point to, are stores below -`result`. A fresh value names which of the function's new objects it is, so -two fields initialised from one allocation stay aliases in the caller, and -two call sites create independent objects (§6.3). A store of a new object -keyed on a result class is weak, and the object is absent on the other -classes: a test of the result that selects them disowns it, so a -constructor whose failure path stored nothing leaks nothing (RFC 0031 -*Implementation amendments*). At a call the callee's stores are read before -any is written, so extraction (`p = *slot; *slot = NULL; return p`) stays -precise. Symbols are immutable, so an allocation's extent is the size's -value at allocation time whatever the size variable holds later. - -## Pointer identity and call effects (RFC 0014) - -A function value is a symbol holding a bounded set of at most 32 functions, -or unknown (§4.1), and an indirect call resolves against it before the slot -solution. Function pointers stored in fields and globals are resolved by -the slots of RFC 0030 §9.3, which replace RFC 0014's callback-global -fixpoint and its `callbackGlobals` export. A call through a closed slot with -one target is applied as a direct call, and with several targets as the -join of their summaries. An open slot with known targets gives their -temporal facts only, under `trusted(extern-contract)`; an open slot without -targets gets the unknown-callee default with reason `callback`. Every -indirect call has a null facet on its callee operand. A call through a -callback parameter takes the parameter's slot solution (RFC 0031 -*Unresolved questions*). - -Pointer arithmetic and pointer casts keep the symbol's targets, so -`free(p); use(p + 1)` is a use after free. `memcpy`, `memmove` and record -assignments copy pointer and integer leaves cell by cell; a pointer loaded -from a cell whose last store was of another type, or from a partial copy, -is `unresolved(raw-cast)` where it is used (§4.2). - -## Arrays and containers (RFC 0015) - -Array elements are cells of their object (RFC 0031 §4.9): a constant index -is a concrete cell, an affine index a selected cell over its index symbol -(at most 32 per object), and stores the engine cannot name go to the -element position's summary cell. Segments `[from, to) ↦ v` (at most 4 per -element position) describe ranges of elements, each holding its own value; -at a loop head the element cells an iteration changed fold into a segment -that grows with the induction variable, so `for (i = 0; i < n; i++) -free(a[i]);` releases `[0, n)`. A join that cannot keep a selected cell or -match a segment evicts it as a weak store to the elements it described. -Summaries carry element effects as ranges over constants or integer -parameters the body never assigns; a range that cannot be so expressed is -exported without one, as a possible effect on some elements. `realloc`'s -new block starts with the old block's cells and ranges below its size. - -## Compositional calls (RFC 0016) - -A summary is derived assuming distinct parameter objects except where -D1–D6 cannot separate them, where the entry objects are joined already and -the summary holds for aliased calls. Where a caller passes arguments that -point into one object, the callee is re-analysed in an alias context (§6.6; -see *Calls* above). A context run never decides the rows of the function it -analyses (§2.6); it keeps its diagnostics, each reported at the use in the -callee with a note naming the call and linked to that call's Call site. - -## Target integers and compositional bounds (RFC 0017) - -[RFC 0017](rfcs/0017-c-integer-semantics-and-spatial-safety.md) specifies -the target-integer model. Core represents integer types of 1 to 64 bits: -`IntegerValue` is an unsigned bit pattern, `IntegerRange` holds at most two -intervals, and transfers model unsigned wrap, signed validity and -conversions. An invalid operation supplies no invented value. The object -engine keeps each integer symbol's interval in its type's range and the -relations between symbols in the zone: `a = b + c` with a constant `c` -records `a − b = c`, other arithmetic computes intervals only, and a -comparison of an access against an extent with the same scale is a zone -query (§4.4). `EngineExpr.cpp` evaluates arithmetic, compound assignments -in their promoted type, conversions and the overflow builtins; -`invalid-integer-operation` is reported when the operands make the -operation invalid for every value (§5.10). A declaration or typedef of a -variable-length array captures its dimensions where it runs, and `sizeof` -and extents use the captured values; a subscript of a multi-dimensional -array is bounded by its own dimension (RFC 0031 *Implementation -amendments*). - -C values and byte intervals are distinct: `malloc(n * sizeof(T))` receives -the actual C product, and a byte size that may wrap `size_t` gives no -extent, so a wrapped product can establish a violation but never that an -access fits. The spatial verdict needs a lower and an upper bound to prove -an access. It maps onto the spatial facet (RFC 0030 §3.3): a violation -against an exact extent is an `out-of-bounds` error; an undecided access -against an exact or declared extent is `unresolved(undecided)`; an access -that only a lower-bound kind covers is `unresolved(unknown-extent)`. -Call-site findings come from the must-access requirements of §7.5. +LLVM WeaveC was built with, when the driver's default does not exist. + +## `weavec::Core` — the library table + +`lib/Core` depends only on the C++ standard library. It holds one +declarative table, [`LibrarySpec.txt`](../lib/Core/LibrarySpec.txt), and its +parser (`include/weavec/Core/LibrarySpec.h`, `lib/Core/LibrarySpec.cpp`): +per C library, POSIX and platform function, which pointer arguments the +call reads or writes and how many bytes, whether null is allowed, which +arguments it releases or reallocates, which must not overlap, how a format +reads its arguments, the result a `value(t)` computes, the runtime's +checked version (`wrapper(NAME)`), `wide` rows, and the fortified aliases +with their object-size argument (`chk`). CMake embeds the text; +`LibrarySpec::shipped()` parses it once on first use. Library behaviour +goes in the table, never in a comparison of a callee's name. ## `weavec::Frontend` — Clang and LLVM integration -`lib/Frontend` holds the guard passes and their ledger, adapts the analysis -to Clang's frontend machinery, runs the whole-program analysis, and drives -`weavec-cc`. - | Component | Role | | --- | --- | | `GuardPass` (`GuardPass.h`; `GuardPass.cpp`, `GuardPassImpl.h`, `GuardInsert.cpp`, `GuardLibrary.cpp`, `GuardLoops.cpp`, `GuardExpand.cpp`, `GuardFrames.cpp`, `GuardGlobals.cpp`) | The four passes of *Compile pipeline* and `registerGuardPasses`, which adds them through `CodeGenOptions::PassBuilderCallbacks` with the mode (trap, report, verify), the unit's unsafe regions and its enforcement ledger. | -| `EnforcementLedger` | The rows the passes record, their rollup, the `weavec-ledger` version 3 JSON writer and the enforcement summary line. | -| `UnsafeRegions` | The `WEAVEC_UNSAFE` and `no_sanitize` ranges of a unit, by file, and the AST consumer that collects them. | -| `FrontendAction` | `WeaveCAction`, an `ASTFrontendAction` for libTooling; `createWeaveCConsumer`, which `weavec-cc -fweavec-diagnose` runs beside Clang's code generator; and `analyzeTranslationUnit` and `analyzeRetainedUnit`, which run `UnitPipeline`. Every emitted function is analysed, including `static inline` functions from user headers (§5.6). | -| `AnalysisSummary` | The analysis's summary lines, with the `-W` flags applied to the counts. | -| `ProgramAnalysis` | The whole-program algorithm of RFC 0005 over an abstract `ProgramUnit`, for `weavec --whole-program`: discovery, the order by strongly connected component, fixpoints for cyclic groups, publication in the last round. | -| `InterfaceFacts`, `ProgramChecks` | A unit's interface facts (its definitions' kinds, its imports' declared kinds and ownership annotations, its slot constraints with local slots eliminated), and what `--whole-program` does with every unit's at once: `solveProgramSlots`, `programBoundaries`, `verifyDeclarations`. | -| `Driver` | `weavec-cc`: `DriverOptions` (WeaveC's flags, split from Clang's), the in-process `-cc1` jobs with the enforcement set-up and the wrapper action that adds the unsafe-region collector and the analysis, the runtime's link line (`addRuntimeLibraries`, `runtimeObstacle`, `allocatorDefinedBy`, `dropAllocatorIfDefined`), and `--help-weavec`. | -| `DiagnosticControl` | Applies the `-W` flags by each diagnostic's id and certainty. An error can be lowered but never disabled, and a flag naming a removed id is refused. `FilteringSink` drops what an earlier run already reported. | -| `ClangDiagnosticSink` | Forwards `core::Diagnostic`s, with notes and fix-its, to Clang's `DiagnosticsEngine`, so they render exactly like Clang's own. | -| `ResourceDir`, `AnalysisStats` | Locate `weavec.h`, the runtime archives, Clang's resource directory and `clang`; write the work counters of `--analysis-stats`. | - -## `tools/weavec` and `tools/weavec-cc` - -`weavec` is a libTooling application (see *The advisory analysis*): -`weavec file.c -- `, or `weavec -p build/ file.c` with a -compilation database. It injects `-isystem /include` and -`-D__WEAVEC__=1`, so user code can `#include `. `--budget` and -`--no-zero-init` model the analysis of a `weavec-cc -fweavec-diagnose` -build; `--dump-analysis`, `--dump-kinds` and `--analysis-stats` are -debugging aids. +| `EnforcementLedger` | The rows the passes record, their rollup, the `weavec-ledger` version 3 JSON writer and the summary line. | +| `UnsafeRegions` | The `WEAVEC_UNSAFE` and `no_sanitize` ranges of a unit, by file, the AST consumer that collects them, and `UnsafeAnnotation`. | +| `Driver` | `weavec-cc`: `DriverOptions` (WeaveC's flags, split from Clang's), the in-process `-cc1` jobs with the enforcement set-up (`configureEnforcement`), ASan's contract and fortification, the wrapper action that adds the unsafe-region collector, the runtime's link line (`addRuntimeLibraries`, `runtimeObstacle`, `allocatorDefinedBy`, `dropAllocatorIfDefined`), and `--help-weavec`. | +| `ResourceDir` | Locates `weavec.h`, the runtime archives, Clang's resource directory and `clang`. | + +## `tools/weavec-cc` `weavec-cc` is the drop-in compiler: `CC=weavec-cc make`. Its own flags, which `weavec-cc --help-weavec` lists, are `-f[no-]weavec`, `-fweavec-checks=trap|report|verify|none`, `-f[no-]weavec-zero-init`, -`-fweavec-ledger=`, `-f[no-]weavec-summary`, -`-f[no-]weavec-diagnose`, the analysis's budgets and debugging output -(`-fweavec-budget=`, `-fweavec-unit-budget=`, `-fweavec-dump-analysis`, -`-fweavec-analysis-stats=`) and the `-W…weavec…` flags; an unknown -`-fweavec-*` flag is an error, and everything else is Clang's. When WeaveC -is on, the driver defines `__WEAVEC__=1` and adds `weavec.h`'s directory -with `-isystem`. `--version` prints Clang's version block, then -`weavec-cc version …`. The command line is that of RFC 0035 §7. - -## Diagnostics contract - -Every diagnostic carries a stable id from `weavec::core::diag`, printed as -`[weavec::]`. Scripts and editors filter on ids, so renaming one is a -breaking change. Diagnostics come from the advisory analysis only: `weavec` -and `weavec-cc -fweavec-diagnose`. There are 18: - -| Ids | Default severity | -| --- | --- | -| `use-after-free`, `double-free`, `use-after-move`, `conflicting-borrow`, `lifetime-too-short`, `mismatched-release`, `invalid-release` | error when definite, warning when possible | -| `null-dereference`, `use-of-uninitialized`, `out-of-bounds` | error, reported only when definite | -| `unsafe-operation`, `annotation-mismatch`, `invalid-integer-operation`, `contradicted-assumption` | error | -| `leak`, `invalid-annotation` | warning (`leak` off by default under `-fweavec-diagnose`: `-Wweavec-leak`) | -| `allocation-failure` | warning, off by default (`-Wweavec-allocation-failure`) | - -`diag::defaultSeverity(id, certainty)` gives these severities; -`-fweavec-diagnose` then lowers every error to a warning. Possible null and -spatial findings are `unresolved(undecided)` rows rather than diagnostics. -`-Werror` in project flags does not promote WeaveC warnings; -`-Werror=weavec[-]` does. An error can be lowered with -`-Wno-error=weavec-` but not disabled. RFC 0030 removed -`analysis-incomplete`, `annotation-required`, `checking-incomplete` and -`checking-failed`, and a `-W` flag naming one of them is an error; RFC 0035 -removed `unresolved-operation` and `unchecked-operation` with the require -levels. Run-time reports are not diagnostics. +`-fweavec-ledger=`, `-f[no-]weavec-summary` and +`-f[no-]weavec-asan-compat`; an unknown `-fweavec-*` flag is an error, and +everything else is Clang's. When WeaveC is on, the driver defines +`__WEAVEC__=1` and adds `weavec.h`'s directory with `-isystem`. +`--version` prints Clang's version block, then `weavec-cc version …`. The +command line is that of RFC 0035 §7 as RFC 0036 §7 amends it. ## Tests and gates -- **Unit tests** (`unittests/`, GoogleTest) test each component alone. - `LibrarySpecTest.cpp` checks every library entry against an independent, - hand-written expectation table, and `HeapTest.cpp` builds the state a - violation of each domain invariant I1–I6 (RFC 0031 §4.7) would produce and - checks that the decision is not proven. The Analysis tests run - `ObjectEngine` over snippets (`unittests/Analysis/TestUtils.h`), and - `EffectsTest.cpp` checks the join and the widening of summaries. - `GuardPassTest.cpp` runs the guard passes on IR text; the other Frontend - tests cover the summary lines, the `-W` flags, interface facts and the - whole-program analysis. +- **Unit tests** (`unittests/`, GoogleTest). `LibrarySpecTest.cpp` + (`WeaveCCoreTests`) checks every library entry against an independent, + hand-written expectation table. `GuardPassTest.cpp` + (`WeaveCFrontendTests`) runs the guard passes on IR text, each removal + rule alone, and checks the IR and the ledger rows they leave. - **The runtime test** ([`runtime/test/rt_test.c`](../runtime/test/rt_test.c), CTest `runtime`) is one C program linked with `libweavec_rt.a` and `libweavec_alloc.a`, so its `malloc` is the arena's. It tests the shadow - and its encoding, the size classes, alignment, reallocation, the - quarantine, invalid releases (in child processes that must die), huge - blocks, foreign pointers, the guards, strings, the checked calls, globals, - dynamic allocas, the stack's unpoisoning, mappings, the array-bounds - handlers, threads and `fork`. Test names as arguments run only those. -- **Lit tests** (`test/Analysis`, `test/Annotations`, `test/Driver`, - `test/Guards`, `test/WholeProgram`) pin exact messages and behaviour. - `test/Guards` pins the passes' IR (FileCheck on `-emit-llvm`: `ir-*.c`) - and, by building and running programs, the run-time behaviour, the report - formats and the enforcement ledger; `test/Driver/runtime-*.c` pins the - runtime's link line and fallbacks. + and its encoding, the size classes, alignment, reallocation, the sweep + (recycling, retention, following the heap, stopping threads, pacing, + the C library's data through the image table), invalid releases (in + child processes that must die), huge blocks, foreign pointers, the + guards, strings, the checked calls, globals, dynamic allocas and the + stack's unpoisoning, ASan's interface, the number parsers and limits, + mappings, the array-bounds handlers, threads and `fork`. Test + names as arguments run only those. +- **Lit tests** (`test/Guards`, `test/Driver`) pin exact output and + behaviour. `test/Guards` pins the passes' IR (FileCheck on `-emit-llvm`: + `ir-*.c`, among them `ir-frames.c`, and `shared-frame.c`) and, by + building and running programs, the run-time + behaviour, each report line, and the enforcement ledger; + `test/Driver` pins the runtime's link line and fallbacks + (`runtime-*.c`), fortification (`fortify.c`), ASan's macros + (`asan-compat*.c`) and the globals of a dylib and a bundle + (`image-globals.c`). - **`test/cases`** is one tree of executable C cases by feature, with expectations as line-comment markers (see its [README](../test/cases/README.md)). - [`scripts/run-cases.py`](../scripts/run-cases.py) analyses each case with - `weavec` (`BUG`, `CLEAN`, `ALLOW`; `MISS: ` on a `BUG` line the - analysis is known not to report), builds it with `weavec-cc` (`GUARDED`, - `PROVEN`, `UNGUARDED` and `EXPECT-LEDGER` on the enforcement ledger), and - runs it, attributing every stop to a line and a kind (`TRAP: `, - `TRAP-AT: :`, and `STOP` in a `DETECT` case). `--asan` adds - an oracle built with `-fsanitize=address,array-bounds`; `--checks verify` + [`scripts/run-cases.py`](../scripts/run-cases.py) builds each case with + `weavec-cc` (`GUARDED`, `PROVEN`, `UNGUARDED` and `EXPECT-LEDGER` on the + enforcement ledger) and runs it, attributing every stop to a line and a + kind (`TRAP: `, `TRAP-AT: :`, and `STOP` in a `DETECT` + case). `test/cases/determinism` holds a bug per class RFC 0036 makes + deterministic, each with its correct twin (gate G1), and, as `XFAIL`, the + bugs it leaves to a later RFC. `--asan` adds an + oracle built with `-fsanitize=address,array-bounds`; `--checks verify` fails any `weavec.proven` report. CTest registers one `cases-` test per top-level directory, so `ctest -j` runs the suites in parallel. - **`test/corpus`** pins real projects, built and tested with `weavec-cc` by [`scripts/corpus-gate.py`](../scripts/corpus-gate.py) (`--quick` on every pull request); see its [README](../test/corpus/README.md). -RFC 0035's acceptance gates (drop-in G1–G3, detection G4–G6, cost G7–G9, +RFC 0036's acceptance gates (detection G1–G3, drop-in G4–G5, cost G6–G9, hygiene H1–H2) are measured with these tools. `scripts/check-hygiene.py` -implements gate H2 of RFCs 0030 and 0031: no retired names outside -`docs/rfcs/`, no libc name comparisons outside the library table, no corpus -project named under `lib/`, the seam rules, and the line budgets, the -runtime's own among them (RFC 0032 gate H1). +implements gate H1: no libc name comparisons outside the library table, no +corpus project named under `lib/`, Core free of Clang and LLVM, the +runtime's includes, its entry points declared in `weavec_rt.h`, and the +line budgets of the library (9,000) and of the runtime (6,000). [`scripts/codegen-identity.py`](../scripts/codegen-identity.py) checks that -objects built with `-fweavec-checks=none` are byte-identical to Clang's -(RFC 0030's gate G7). +objects built with `-fweavec-checks=none` are byte-identical to Clang's. ## Build structure -- The top-level `CMakeLists.txt` builds `lib/` (Core, Analysis, Frontend), - `tools/`, `runtime/` and, with `WEAVEC_BUILD_TESTS`, `unittests/` and - `test/`. +- The top-level `CMakeLists.txt` builds `lib/` (Core, Frontend), `tools/`, + `runtime/` and, with `WEAVEC_BUILD_TESTS`, `unittests/` and `test/`. - `cmake/WeaveCLLVM.cmake` finds LLVM and Clang and provides the `weavec::llvm` interface target and `weavec_link_llvm`/`weavec_link_clang`, which respect `LLVM_LINK_LLVM_DYLIB` and `CLANG_LINK_CLANG_DYLIB`. @@ -1236,5 +739,5 @@ objects built with `-fweavec-checks=none` are byte-identical to Clang's whatever the build type. With `WEAVEC_BUILD_TESTS` it also builds `weavec_rt_test`, which `test/CMakeLists.txt` registers as the CTest `runtime`. -- A CMake package config (`find_package(WeaveC)`) exports `weavec::Core`, - `weavec::Analysis` and `weavec::Frontend` to external tools. +- A CMake package config (`find_package(WeaveC)`) exports `weavec::Core` + and `weavec::Frontend` to external tools. diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index 8002bc89..ec8b41d5 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -14,7 +14,7 @@ export default defineConfig({ starlight({ title: 'WeaveC', description: - 'Run-time memory safety for existing C code, and an ownership analysis that reports bugs before it runs. Built on Clang and LLVM.', + 'Run-time memory safety for existing C code: a drop-in C compiler that guards every memory access. Built on Clang and LLVM.', logo: { src: './src/assets/mark.svg', replacesTitle: false }, favicon: '/favicon.svg', social: [ @@ -61,10 +61,8 @@ export default defineConfig({ { label: 'Use WeaveC', items: [ - item('Ownership & borrowing', 'guides/ownership'), item('Adopt incrementally', 'guides/adoption'), item('Build integration', 'guides/build-integration'), - item('Whole-program analysis', 'guides/whole-program'), item('Unsafe boundaries', 'guides/unsafe'), ], }, @@ -73,12 +71,8 @@ export default defineConfig({ collapsed: true, items: [ item('Command line', 'reference/cli'), - item('Annotations', 'reference/annotations'), - item('Annotation placement', 'reference/annotation-placement'), - item('Diagnostics', 'reference/diagnostics'), - item('Diagnostic controls', 'reference/diagnostic-controls'), item('Safety guarantees', 'reference/guarantees'), - item('Checker coverage', 'reference/checker-coverage'), + item('The weavec.h header', 'reference/annotations'), item('Compatibility', 'reference/compatibility'), item('Troubleshooting', 'reference/troubleshooting'), ], diff --git a/docs/data/diagnostic-remedies.json b/docs/data/diagnostic-remedies.json deleted file mode 100644 index c1bae7ff..00000000 --- a/docs/data/diagnostic-remedies.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "use-after-free": "Move the final read before cleanup, or extend the lifetime so cleanup happens after every user has finished. Follow the release note: freeing an alias invalidates the original pointer too. Assigning NULL to one variable does not repair other dangling aliases. An error means the object is released on every path to the use; a warning (\"may have been freed\") means some paths release it, so make the release and the use agree on every path. In a weavec-cc build a guard also stops such a use at run time while the freed block is still in the runtime's quarantine. See [your first check](/getting-started/first-check/).", - "double-free": "Give each allocation one clear cleanup path. After ownership transfer, let the new owner release it. On error paths, track whether ownership actually transferred before local cleanup. Reference-counted objects need one release per acquired share. In a default weavec-cc build the runtime's allocator also validates every free: a second release of a block stops the program at run time, as long as the block's storage has not been reused.", - "use-after-move": "Stop using the old owning pointer after transfer. Borrow the object when the callee only needs temporary access, or use a new value returned by the operation. With realloc, distinguish success from failure and avoid keeping aliases into the old storage after successful growth.", - "conflicting-borrow": "Delay release, transfer, or mutation until the borrow's final use, or shorten the borrow by moving its last use earlier. If the copy is never used after the release (a failure path that leaves it in a struct the caller discards), clear it or ignore the warning; a use of it would be reported as use-after-free.", - "lifetime-too-short": "Ensure the referent lives at least as long as every pointer that escapes. Return a value, use caller-provided storage, or allocate storage with an ownership contract instead of returning an address into a local stack frame.", - "unsafe-operation": "Inspect why the pointer or call is raw. Prefer a modeled or verified interface with an accurate contract. If the operation necessarily crosses the supported model, put the smallest reviewed operation inside a WEAVEC_UNSAFE boundary and document the assumptions. Unsafe code still affects ownership outside the boundary, and weavec-cc does not guard its accesses.", - "mismatched-release": "Pair the resource with its allocator's release family: malloc with free, fopen with fclose, and opendir with closedir. For a custom family, express the contract with WEAVEC_OWNED and WEAVEC_OWNED_BY and ensure wrapper bodies agree.", - "leak": "Release or transfer the owned resource on every applicable exit path. Before overwriting an owning pointer, dispose of the old resource. Free owned child allocations before their container. A shared cleanup label can make C error paths easier to audit.", - "null-dereference": "Check nullable results before their first use and keep that condition on every path reaching the dereference. The error means the pointer is null on every path from a non-allocator source. A pointer that may be null is not reported; in a weavec-cc build a guard stops a null dereference at run time. Propagate allocation or lookup failure to callers when appropriate. Use WEAVEC_NONNULL only for an interface whose callers can establish the requirement.", - "use-of-uninitialized": "Initialize the pointer or record field on every path before reading it. A declaration alone does not establish a value. The error means no path assigns it first. weavec-cc zero-initializes locals, so a pointer that may be uninitialized is null instead, and a guard stops its dereference at run time.", - "invalid-release": "Release the original allocation base with the correct releaser. Stack objects, string literals, and interior pointers are not independently freeable. Keep the owning base separate from traversal cursors. In a default weavec-cc build the runtime's allocator also validates every free: releasing an interior pointer, a pointer to a stack or global object, or a block that was already released stops the program at run time.", - "out-of-bounds": "Compare the access against the actual allocation size, including element size and space for string terminators. Array indices must be smaller than the element count. Establish and preserve bounds across calls; a capacity field does not enlarge the allocation it describes. The error means the access fails for every value the analysis allows against an exact extent; an access that only may overrun is not reported. In a weavec-cc build a guard stops an access that leaves its object at run time.", - "invalid-integer-operation": "Validate operands before the operation: divisors must be nonzero, shifts must have valid counts, signed results must be representable, and VLA dimensions must be positive. Check multiplication and conversion before using a result as an allocation size. See [integers and bounds](/guides/integers-and-bounds/).", - "annotation-mismatch": "Compare the annotation with the function body and its callers. Correct the implementation or the declared contract so they describe the same ownership, extent, and effects. Do not strengthen an annotation simply to silence a diagnostic.", - "invalid-annotation": "Check the macro spelling, placement, supported declaration kind, and named parameters or fields. WEAVEC_COUNTED_BY and WEAVEC_SIZED_BY must name a sibling integer parameter or field, WEAVEC_ENDED_BY a sibling pointer, and WEAVEC_OWNED_BY requires WEAVEC_OWNED. See [annotation placement](/reference/annotation-placement/).", - "contradicted-assumption": "The assumption is false on the path the note describes, so what the analysis concludes after it rests on a false premise. Fix the code that makes the value violate the assumption, or correct the assumption to what the code guarantees. Do not weaken an assumption only to silence the error. Nothing checks an assumption at run time: one the analysis cannot decide is simply assumed.", - "allocation-failure": "Test the allocation's result before its first use and handle the failure, for example by returning an error or calling an abort routine. The warning is off by default because a weavec-cc build already traps on the null dereference; enable it to find the places that should report the failure gracefully." -} diff --git a/docs/development.md b/docs/development.md index 66805b33..5dffdee0 100644 --- a/docs/development.md +++ b/docs/development.md @@ -72,8 +72,8 @@ Useful cache variables (`-D...` or in `CMakeUserPresets.json`): Build targets of note: -- `weavec` — the advisory analysis tool, in `build//bin/`. -- `weavec-cc` — the drop-in compiler driver, next to it. It needs Clang's +- `weavec-cc` — the drop-in compiler driver, in `build//bin/`. It + needs Clang's resource directory and (for jobs it does not run in-process, such as `-cc1as`) a `clang` binary; both are recorded at configure time from the LLVM install used to build, and `WEAVEC_CLANG` overrides the binary. It @@ -90,21 +90,14 @@ Build targets of note: ### Unit tests (`unittests/`) -GoogleTest, one binary per library (`WeaveCCoreTests`, `WeaveCAnalysisTests`, -`WeaveCFrontendTests`). Core tests exercise the model directly, including -the object engine's domain (`HeapTest.cpp`, one test per invariant I1–I6 of -RFC 0031 §4.7), the join and widening of summaries (`EffectsTest.cpp`) -and one test per row of the library table (`LibrarySpecTest.cpp`). -Analysis tests parse snippets with -`clang::tooling::buildASTFromCodeWithArgs`, run `ObjectEngine` over them and -collect diagnostics with `core::DiagnosticCollector`; `TestUtils.h` has -`analyze` and `analyzeInProgram` (a snippet checked against another unit's -interface), and the result gives each function's `core::FunctionEffects`. -Frontend tests run the whole-program analysis over in-memory units, the -diagnostic controls, and the guard passes: `GuardPassTest.cpp` runs -GuardInsert, GuardPrune and GuardExpand on IR text, each removal rule of -RFC 0035 §6 alone, and checks the ledger rows they leave. Run one with -`build/dev/unittests/WeaveCCoreTests --gtest_filter='Heap*'`. +GoogleTest, one binary per library (`WeaveCCoreTests`, +`WeaveCFrontendTests`). The Core tests check every row of the library table +against an independent, hand-written expectation table +(`LibrarySpecTest.cpp`). The Frontend tests run the guard passes on IR text +(`GuardPassTest.cpp`): GuardInsert, GuardPrune and GuardExpand, each +removal rule of RFC 0035 §6 alone, frames and global redzones, and the +ledger rows they leave. Run one with +`build/dev/unittests/WeaveCFrontendTests --gtest_filter='GuardExpand*'`. ### The runtime (`runtime/`) @@ -116,21 +109,30 @@ archives so that its `malloc` is the arena's: ```sh ctest --preset dev -R '^runtime$' # every test -build/dev/runtime/weavec_rt_test guards quarantine # only the named tests +build/dev/runtime/weavec_rt_test guards sweep-recycles # only the named tests ``` The tests are `shadow-ready`, `classes`, `shadow-encoding`, -`reuse-is-zero`, `alignment`, `realloc`, `quarantine`, `quarantine-holds`, -`invalid-releases`, `huge`, `foreign`, `guards`, `strings`, -`checked-calls`, `globals`, `allocas`, `unpoison-stack`, `mappings`, -`array-bounds`, `threads` and `fork`. A test that must stop the program (a -failed guard, an invalid release) runs in a child process, and the parent -checks how it died. The runtime has platform-specific halves (Darwin's -malloc zone and its one runtime per process, glibc's `__libc_free`, the -shadow window's size), so a change to it must pass on both Darwin and -Linux; CI runs the test in its Linux and macOS CTest jobs. The runtime -includes no WeaveC header but `runtime/weavec_rt.h`, and -`scripts/check-hygiene.py` holds it to its own line budget. +`reuse-is-zero`, `alignment`, `realloc`, `sweep-recycles`, +`quarantine-holds`, `sweep-follows-the-heap`, `sweep-stops-threads`, +`pacing`, `library-data-is-scanned`, `invalid-releases`, `huge`, +`foreign`, `guards`, `strings`, `checked-calls`, `globals`, `allocas`, +`unpoison-stack`, `asan-interface`, `parsers-and-limits`, `mappings`, +`array-bounds`, `threads` and `fork`. The sweep tests scrub the stack +they leave behind with a compiler barrier and use size classes no other +test touches: the scan is conservative, and a stale copy of an address in +an unused stack slot keeps its block. A +test that must stop the program (a failed guard, an invalid release) runs +in a child process, and the parent checks how it died. The runtime has +platform-specific halves (Darwin's malloc zone and its one runtime per +process, `thread_suspend` against Linux's signals for the sweep, glibc's +`__libc_free`, the shadow window's size), so a change to it must pass on +both Darwin and Linux; CI runs the test in its Linux and macOS CTest jobs. +The runtime includes no WeaveC header but `runtime/weavec_rt.h`, and +`scripts/check-hygiene.py` holds it to its own line budget. Keep its +`preserve_all` entry points (`WEAVEC_RT_SLOW`) trivial wrappers over +non-inlined helpers: Apple clang 21 saves some callee-saved registers below +the stack pointer in a `preserve_all` function that uses many of them. ### Integration tests (`test/`) @@ -139,14 +141,11 @@ with `lit -v build/dev/test/Guards/heap-overflow.c`. The suites: | Directory | What it pins | | --- | --- | -| `test/Guards/` | the guard passes and the runtime. `ir-*.c` run `%weavec_cc -O2 -S -emit-llvm` and FileCheck the IR: the inline checks, the frame and global layout, the guards a rule removes, the stack unpoisoning before a `noreturn` call. The others build and run a program and pin the exact report (`weavec: heap-buffer-overflow at …`), report mode, verify mode, the ledger and summary line, unsafe regions and `no_sanitize`, the checked library calls, array bounds, `longjmp`, `mmap` and zero-initialisation. | -| `test/Driver/` | `weavec-cc` and `weavec` on the command line: compile and link, the runtime on the link line and its fallback notes (`runtime-link.c`), one runtime per process on Darwin, `-fweavec-*` and `-W` flags, `-fweavec-diagnose`, versions. | -| `test/Analysis/` | the advisory analysis's diagnostics, through `%weavec`. | -| `test/Annotations/` | the `weavec.h` annotations and the pointer kinds they give (`--dump-kinds`). | -| `test/WholeProgram/` | several files through `%weavec --whole-program`, with shared sources in `test/WholeProgram/Inputs/`. | - -Every diagnostic change and every change to a run-time report needs a lit -test, because these pin the exact user-visible output. New tests are named +| `test/Guards/` | the guard passes and the runtime. `ir-*.c` run `%weavec_cc -O2 -S -emit-llvm` and FileCheck the IR: the inline checks, the frame layout and shared frame space (`ir-frames.c`, `shared-frame.c`), the unpoisoning before `noreturn` calls (`ir-noreturn.c`), the global layout, the guards a rule removes. The others build and run a program and pin the exact report (`weavec: heap-buffer-overflow at …`): heap, stack and global overflows, use after scope, intra-object overflow of fortified calls, use after poison, report mode, verify mode, the ledger and summary line, unsafe regions and `no_sanitize`, the checked library calls, array bounds, `longjmp`, `mmap` and zero-initialisation. | +| `test/Driver/` | `weavec-cc` on the command line: compile and link, the runtime on the link line and its fallback notes (`runtime-link.c`), one runtime per process on Darwin, the globals of a dylib and a bundle (`image-globals.c`), `-D_FORTIFY_SOURCE=3` (`fortify.c`), ASan's macros (`asan-compat*.c`), `-fweavec-*` flags, versions. | + +Every change to a run-time report needs a lit test, because these pin the +exact user-visible output. New tests are named by feature (`test/Guards/.c`, `test/Guards/ir-.c`); existing `rfcNNNN-` names stay. @@ -156,29 +155,32 @@ Executable C cases organised by feature, with their expectations in line-comment markers; [`test/cases/README.md`](../test/cases/README.md) has the grammar. For each case `scripts/run-cases.py`: -1. analyses it with `weavec` (`--whole-program` when it has several units) - and checks the diagnostics against `BUG: ` lines (`MISS: ` - on a `BUG` line marks one the analysis is known not to report), - `CLEAN` and `ALLOW: `; -2. builds it with `weavec-cc` (trap mode, or verify mode under +1. builds it with `weavec-cc` (trap mode, or verify mode under `--checks verify`) and checks the enforcement ledger against `GUARDED`, `PROVEN`, `UNGUARDED` and `EXPECT-LEDGER` markers; -3. runs it once per `RUN-INPUT` and requires every stop to be one a +2. runs it once per `RUN-INPUT` and requires every stop to be one a `TRAP: ` marker expects (a report kind such as `heap-buffer-overflow`, or `invalid-release`), and every `TRAP` to be hit; - `TRAP-AT: :` expects a stop in a unit several cases share. + `TRAP-AT: :` expects a stop in a unit several cases share; a + `CLEAN` case must not stop. A `DETECT` case is judged instead by whether its bug stops at or before a `STOP` line and its fixed twin runs clean (`--min-stops N` gates the - count). + count; `MISS` marks a `STOP` line as a known miss). + +`test/cases/determinism/` holds a bug per class RFC 0036 makes +deterministic (use after free and double free after churn, a fortified +field overflow, a pool poisoned through ASan's interface), each with its +correct twin (gate G1), and, as `XFAIL`, the bugs it leaves to a later RFC +(a jump into a neighbouring object, use after return). `--asan` adds the ASan oracle (each case built with `-fsanitize=address,array-bounds` by the reference compiler); `--checks -verify` fails any case that reports `weavec.proven`; `--no-run` analyses and -builds only; `--filter GLOB` selects cases. CTest registers one +verify` fails any case that reports `weavec.proven`; `--no-run` builds +only; `--filter GLOB` selects cases. CTest registers one `cases-` test per top-level directory, so `ctest --preset dev -L cases` runs them in parallel; the cache variable `WEAVEC_CASES_ARGS` passes -runner options (the ASan CI job uses `--no-run`). A false trap or false -positive fixed gets a `CLEAN` case; a new guard rule gets a bug case and its +runner options (the ASan CI job uses `--no-run`). A false trap fixed gets a +`CLEAN` case; a new guard rule gets a bug case and its correct twin under `test/cases/semantics//`. ### Corpus gate (`test/corpus/`) @@ -187,7 +189,7 @@ correct twin under `test/cases/semantics//`. `test/corpus/manifest.json` with `weavec-cc`, runs their test suites and benchmarks, and compares the results with the ratchet in `test/corpus/expected.json`; anything that needs a verdict (a trap in a -project's tests, an analysis finding) is recorded in +project's tests) is recorded in `test/corpus/triage.json`. `--quick` runs a few configurations on every pull request, and the weekly **Corpus** workflow runs the full gate. See [`test/corpus/README.md`](../test/corpus/README.md) for the configurations, @@ -198,10 +200,12 @@ benchmarks on an idle machine. `scripts/check-hygiene.py` checks the repository rules (no library names compared outside `lib/Core/LibrarySpec.txt`, no corpus project named under -`lib/`, the engine seam) and the line budgets of the object engine, of the -libraries (`lib/`, `include/` and `tools/`) and of the runtime (counted -apart, because it is linked into compiled programs, not into the -compiler); `--help` lists each check and its limit. It runs in the Linux +`lib/`, no Clang or LLVM header in Core, the runtime's includes, every +runtime entry point the passes name declared in `runtime/weavec_rt.h`) and +the line budgets of the libraries (`lib/`, `include/` and `tools/`, 9,000) +and of the runtime (6,000, counted apart, because it is linked into +compiled programs, not into the compiler); `--help` lists each check and +its limit. It runs in the Linux Release CI job. ### Sanitizers @@ -210,9 +214,9 @@ Release CI job. everything. This CI preset uses `-O1 -gline-tables-only` to keep the test suites within the job's runtime budget while retaining assertions, both sanitizers and source locations in stack traces. The `dev-asan` preset -keeps the unoptimized Debug build for interactive debugging. The analysis -and the guard passes are the most likely places for lifetime bugs of our -own, so run the CI preset before submitting changes to them. +keeps the unoptimized Debug build for interactive debugging. The guard +passes are the most likely place for lifetime bugs of our own, so run the +CI preset before submitting changes to them. ## Formatting and linting @@ -232,9 +236,10 @@ editor integration. - `weavec-cc -O2 -S -emit-llvm file.c -o -` prints the unit's IR after the guard passes: each surviving guard is an inline shadow check whose slow side calls `__weavec_rt_guard`, `__weavec_rt_range` or another - `__weavec_rt_*` entry point, and tracked locals live in one frame alloca - with redzones. Compare with `-fweavec-checks=none` to see the guards' - whole effect. + `__weavec_rt_*` entry point; the tracked locals are offsets into one + `weavec.frame` alloca whose shadow is written at entry and cleared at + each return. Compare with + `-fweavec-checks=none` to see the guards' whole effect. - `-mllvm -print-after-all` (narrowed with `-mllvm -filter-print-funcs=`) prints the IR after every pass, so the markers (`__weavec.guard`, `__weavec.range`, `__weavec.strlen`, @@ -257,12 +262,21 @@ editor integration. makes it trap at the first again. `WEAVEC_RT_REPORT_LOG=` appends each report to a file instead of standard error, for a test harness that keeps a passing test's output to itself. +- `WEAVEC_RT_BACKTRACE=1` prints the stack of the access that stops the + program after its report. - `WEAVEC_RT_STATS=1 ./prog` prints the runtime's counters on stderr at exit, one line each, `weavec: runtime: `: allocations, releases, recycled slots, huge blocks, slow guards, range guards, string - guards and stack unpoisons. The counters are not synchronised. - `WEAVEC_RT_QUARANTINE=` sets the quarantine budget (0 recycles a - released block at once). + guards, stack unpoisons and sweeps, then the sweeps' total and + longest pause, the blocks and bytes the last sweep retained, and the + most the live slots and the quarantine held. The + counters are not synchronised. +- `WEAVEC_RT_QUARANTINE=` sets how much the quarantine must gain + (and more than a quarter of the live heap) before a sweep is due (paced + to eight times the last sweep's duration; twice the value, or + half the live heap, sweeps at once); 16 MiB by default. `WEAVEC_RT_SWEEP_SIGNAL=` sets the signal number that stops + threads for a sweep on Linux (by default `SIGPWR`, or `SIGXCPU` when the + program handles `SIGPWR`). - `weavec-cc` runs its `-cc1` jobs in-process, so `lldb -- build/dev/bin/ weavec-cc -c file.c` stops in the passes directly; `weavec-cc -### file.c` prints the jobs Clang's driver planned and the link line with the @@ -279,34 +293,6 @@ editor integration. - For lit failures, `lit -a` prints the full command and output; the test's working files are under `build//test//Output/`. -## Debugging the analysis - -- `weavec file.c -- -Xclang -ast-dump` does *not* work (the tooling action - replaces Clang's); use `clang -fsyntax-only -Xclang -ast-dump file.c` - directly to inspect the AST. -- Run the tool under a debugger with `lldb -- build/dev/bin/weavec file.c --`. -- `weavec --dump-analysis file.c --` prints the inferred places, lifetimes, - exit state and summary of every analysed function. - `weavec --whole-program --dump-analysis a.c b.c --` names each unit in - analysis order and then prints the joined program database (every - exported summary) and the program's function-pointer slots. -- `weavec --dump-kinds file.c --` prints the unit's pointer kinds (declared - and inferred, with must-access requirements, slot demotions, store groups - and field candidates) and its function-pointer slots, without running the - engine. -- The object engine prints its own state to stderr when - `WEAVEC_ENGINE_DUMP` is set (`lib/Analysis/EngineUnit.cpp`, - `EngineSummary.cpp`, `EngineRun.cpp`): any value prints the summary of - every function the unit analyses and of every summary it imports from the - program database; `2` also prints each run's exit states, and `3` each - run's block entry states (objects, cells, symbols and the zone), as - `ObjectEngine::dump` does. `WEAVEC_ENGINE_TRACE` prints every block visit - of the fixpoint with the states it sends and the joins it makes. Both are - unstable and verbose; use them on a reduced case. -- `weavec --analysis-stats=` (`-fweavec-analysis-stats=` in - `weavec-cc -fweavec-diagnose`) writes the analysis's work statistics as - JSON, to find what spends a budget. - ## Releasing WeaveC publishes source releases to diff --git a/docs/examples/fixed.c b/docs/examples/fixed.c deleted file mode 100644 index d5b454b5..00000000 --- a/docs/examples/fixed.c +++ /dev/null @@ -1,15 +0,0 @@ -#include - -struct node { - int value; -}; - -static void node_free(struct node *n) { - free(n); -} - -int read_node(struct node *n) { - int value = n->value; - node_free(n); - return value; -} diff --git a/docs/examples/use-after-free.c b/docs/examples/use-after-free.c deleted file mode 100644 index a16c06be..00000000 --- a/docs/examples/use-after-free.c +++ /dev/null @@ -1,15 +0,0 @@ -#include - -struct node { - int value; -}; - -static void node_free(struct node *n) { - free(n); -} - -int read_node(struct node *n) { - struct node *alias = n; - node_free(alias); - return n->value; -} diff --git a/docs/package.json b/docs/package.json index 5917ff73..c713a177 100644 --- a/docs/package.json +++ b/docs/package.json @@ -14,8 +14,8 @@ "check": "npm run prepare:content && astro check", "test": "node --test scripts/*.test.mjs", "test:browser": "playwright test", - "format": "prettier --write \"{src,pages,scripts,tests}/**/*.{astro,css,mjs,ts,md,mdx}\" \"*.{mjs,json,ts}\" \"data/*.json\"", - "format:check": "prettier --check \"{src,pages,scripts,tests}/**/*.{astro,css,mjs,ts,md,mdx}\" \"*.{mjs,json,ts}\" \"data/*.json\"" + "format": "prettier --write \"{src,pages,scripts,tests}/**/*.{astro,css,mjs,ts,md,mdx}\" \"*.{mjs,json,ts}\"", + "format:check": "prettier --check \"{src,pages,scripts,tests}/**/*.{astro,css,mjs,ts,md,mdx}\" \"*.{mjs,json,ts}\"" }, "dependencies": { "@astrojs/markdown-satteri": "0.4.1", diff --git a/docs/pages/404.md b/docs/pages/404.md index 79300f85..ba053468 100644 --- a/docs/pages/404.md +++ b/docs/pages/404.md @@ -16,7 +16,8 @@ This address does not match a documentation page. Use the search button above, o - [Introduction](/getting-started/introduction/) - [Installation](/getting-started/installation/) -- [Diagnostic reference](/reference/diagnostics/) +- [Your first check](/getting-started/first-check/) +- [Safety guarantees](/reference/guarantees/) - [RFC library](/rfcs/) If you followed a link from this site, [report the broken link](https://github.com/weavefoundry/weavec/issues/new?template=bug_report.yml) with the page you came from. diff --git a/docs/pages/contributing/overview.md b/docs/pages/contributing/overview.md index 5d514e8b..98c6e7a7 100644 --- a/docs/pages/contributing/overview.md +++ b/docs/pages/contributing/overview.md @@ -3,7 +3,7 @@ title: Contribute to WeaveC description: Build the project, understand the architecture, propose design changes, and contribute tests or documentation. --- -WeaveC is developed in the [weavefoundry/weavec repository](https://github.com/weavefoundry/weavec). Contributions can improve the guards and the runtime, the advisory analysis and its diagnostics, test coverage, performance, or the documentation itself. +WeaveC is developed in the [weavefoundry/weavec repository](https://github.com/weavefoundry/weavec). Contributions can improve the guards and the runtime, drop-in compatibility, test coverage, performance, or the documentation itself. ## Set up the compiler @@ -15,18 +15,18 @@ cmake --build --preset dev ctest --preset dev ``` -The [architecture guide](/internals/architecture/) explains the three libraries and the runtime: Core owns the model and the library table, Analysis bridges Clang to Core behind the engine seam, Frontend holds the guard passes, the enforcement ledger, the compiler driver, whole-program analysis and diagnostics, and `runtime/` is the C runtime every enforcing link carries. +The [architecture guide](/internals/architecture/) explains the compiler and the runtime: Core holds the library table, Frontend the guard passes, unsafe regions, the enforcement ledger and the compiler driver, and `runtime/` is the C runtime every enforcing link carries. ## Read the design before changing it -Ownership semantics, checker rules, annotations, diagnostic identifiers, the guards and the rules that remove them, the runtime and its reports, and the ledger format are specified by RFCs. Read the relevant accepted RFC before changing those areas. Propose a new RFC for a new design decision; a bug fix that restores the specified behavior does not need one. +What is guarded and the rules that remove a guard, the shadow encoding, frames and globals, the runtime and its reports, the library table, `weavec.h`, the ledger format and the driver's flags are specified by RFCs. Read the relevant accepted RFC before changing those areas. Propose a new RFC for a new design decision; a bug fix that restores the specified behavior does not need one. -Start with the [RFC process](/rfcs/process/#process) and [RFC library](/rfcs/). Core must remain independent of Clang and LLVM headers. +Start with the [RFC process](/rfcs/process/#process) and [RFC library](/rfcs/). Core must remain independent of Clang and LLVM headers, and a rule that removes a guard must be local to a function and monitored by `verify` mode. ## Make a reviewable change - Add meaningful unit and integration coverage for changed behavior. Name new tests by feature (`test/cases//…`, `test/Guards/.c`); existing `rfcNNNN-` names may stay. -- Every new diagnostic needs a stable ID, an annotation-reference entry, resolution guidance in `docs/data/diagnostic-remedies.json`, a unit test, and a lit test pinning its exact message. A new or changed run-time report needs a lit test in `test/Guards/` pinning its exact text. +- A new or changed run-time report needs a lit test in `test/Guards/` pinning its exact text, and an entry in the command-line reference's list of report kinds. - Use Conventional Commit titles and update the relevant user guide. - Run formatting and required checks. Generated build outputs are not source files. - Leave `CHANGELOG.md` to semantic-release; do not edit it manually. diff --git a/docs/pages/contributing/website.md b/docs/pages/contributing/website.md index 0bbc4191..fbe30ed5 100644 --- a/docs/pages/contributing/website.md +++ b/docs/pages/contributing/website.md @@ -27,20 +27,19 @@ Pagefind creates its search index during the production build. Validate search i ## Where content lives -| Source | Purpose | -| ------------------------------------ | -------------------------------------------------------------- | -| `docs/pages/` | Authored onboarding, task guides, and website pages. | -| `docs/annotations.md` | Authoritative annotation and diagnostic reference. | -| `docs/rfcs/` | Authoritative design records and their statuses. | -| `docs/architecture.md` | Architecture source, split by section for the site. | -| `docs/data/diagnostic-remedies.json` | Practical resolution guidance for each diagnostic. | -| `docs/examples/` | Tutorial source files included directly in published examples. | -| `docs/src/` | Components, styles, and content configuration. | -| `docs/scripts/` | Content preparation and validation. | +| Source | Purpose | +| ---------------------- | -------------------------------------------------------------- | +| `docs/pages/` | Authored onboarding, task guides, and website pages. | +| `docs/annotations.md` | Reference for the `weavec.h` header and `WEAVEC_UNSAFE`. | +| `docs/rfcs/` | Authoritative design records and their statuses. | +| `docs/architecture.md` | Architecture source, split by section for the site. | +| `docs/examples/` | Tutorial source files included directly in published examples. | +| `docs/src/` | Components, styles, and content configuration. | +| `docs/scripts/` | Content preparation and validation. | `npm run prepare:content` creates `docs/.generated/`. Do not edit or commit that directory. The preparation step preserves the original guides, rewrites repository links to site routes, and gives generated pages edit links pointing to their real source. -When adding a top-level heading to a split source guide, update its mapping in `scripts/prepare-content.mjs`. An unmapped heading fails the build so content cannot silently disappear. RFC entries and diagnostic pages are discovered automatically; new diagnostic IDs require resolution guidance. +The architecture guide is split into one page per top-level heading, and RFC entries are discovered automatically. Other imported sources and their routes are listed in `scripts/prepare-content.mjs`. The RFC library reads status from each RFC's own metadata. Page dates use Git commit history. The version badge comes from the root CMake project version, and release notes come from the generated `CHANGELOG.md`. @@ -55,10 +54,10 @@ npm run test:browser The build checks TypeScript, every internal page and fragment link, local assets, canonical URLs, metadata, search output, and the 404 page. Browser tests exercise desktop and mobile navigation, search, RFC filtering, both themes, and accessibility. -Tutorial examples are also exercised against WeaveC by the compiler CI: `weavec` must report the use-after-free example and accept its fix; `weavec-cc` must record the lookup example's guarded load in the enforcement ledger and stop a negative index with an `index-out-of-bounds` report, and must stop the vector example's read past its heap block with a `heap-buffer-overflow` report. You can run them locally from the repository root; `weavec-cc` is taken from the same directory unless `--weavec-cc` names it: +The tutorial examples can be exercised against a built `weavec-cc`: it must record the lookup example's guarded load in the enforcement ledger and stop a negative index with an `index-out-of-bounds` report, and must stop the vector example's read past its heap block with a `heap-buffer-overflow` report. Run them from the repository root: ```sh -python3 docs/scripts/check-examples.py --weavec build/dev/bin/weavec --weavec-cc build/dev/bin/weavec-cc +python3 docs/scripts/check-examples.py --weavec-cc build/dev/bin/weavec-cc ``` ## GitHub Pages deployment diff --git a/docs/pages/getting-started/first-check.md b/docs/pages/getting-started/first-check.md index 9d28874b..f45a22ab 100644 --- a/docs/pages/getting-started/first-check.md +++ b/docs/pages/getting-started/first-check.md @@ -1,64 +1,29 @@ --- title: Your first check -description: Find a use-after-free through an alias with the analysis, fix it, then build programs whose bad accesses stop at run time with a precise report. +description: Build small C programs with weavec-cc, read the enforcement ledger, and watch guards stop an out-of-bounds index, a heap overflow, a use after free and a use after scope. --- -This tutorial starts with one C file. You need a working [WeaveC installation](/getting-started/installation/). +This tutorial builds a few small C programs with `weavec-cc` and reads what they print when they go wrong. You need a working [WeaveC installation](/getting-started/installation/). Work in an empty directory. -## Find a lifetime bug +## Build a program -Save this as `example.c`: - - - -Run the analysis. The `--` separator introduces flags for Clang: - -```sh -weavec example.c -- -std=c17 -``` - -The command fails with `[weavec::use-after-free]`: - -```text -example.c:14:10: error: use of 'n' after it was freed [weavec::use-after-free] - 14 | return n->value; - | ^ -example.c:13:3: note: freed here (through 'alias') - 13 | node_free(alias); - | ^ -weavec: example.c: 5 sites: 3 proven, 1 not proven, 1 violation, 0 trusted; 1 error, 0 warnings -``` - -There are two pointer variables, but they refer to the same object. `node_free(alias)` releases the object that `n` also names. WeaveC infers the helper's release behaviour from its body. The release happens on every path to the read, so this is a definite violation and an error. Had it happened on some paths only, the diagnostic would be a warning saying `n` _may_ have been freed. - -The last line summarises the analysis: every memory operation (_site_) it saw, and how many it proved safe, could not decide, or found violated. The exact counts depend on the WeaveC version. The analysis is advisory: it reports bugs before the program runs, and it never changes what `weavec-cc` compiles. - -## Fix the lifetime - -Read the value before releasing its storage: - - - -Replace the original contents with this version and rerun the command. The error is gone and the summary line reports no violation. The function still consumes its input; callers must not use their pointer after the call. - -## Watch an index check - -`weavec-cc` is a C compiler. Every memory access it compiles is _guarded_: before the access runs, a few inline instructions ask the runtime linked into the program whether the bytes it touches are addressable, and the program stops at the first one that is not. Save this as `lookup.c`: +`weavec-cc` is a C compiler. Every memory access it compiles is _guarded_: before the access runs, a few inline instructions check that the bytes it touches belong to a live object, and the program stops at the first one that does not. Save this as `lookup.c`: The test `i < 4` does not exclude a negative index. Build the program, writing the enforcement ledger into a directory: ```sh +mkdir ledger weavec-cc -std=c17 -fweavec-ledger=ledger/ -c lookup.c -o lookup.o weavec-cc lookup.o -o lookup ``` ```text -weavec: lookup.c: 3 accesses: 1 proven, 2 guarded, 0 unguarded +weavec: lookup.c: 4 accesses: 0 proven, 4 guarded, 0 unguarded ``` -The build succeeds as it would with Clang. The summary line counts the accesses the compiler saw: `proven` ones lost their guard to a local rule (or the optimiser removed the access), `guarded` ones kept it. The ledger `ledger/lookup.o.ledger.json` has one row per access: +The build succeeds as it would with Clang. The summary line counts the accesses the compiler saw: `proven` ones lost their guard to a local rule (or the optimiser removed the access), `guarded` ones kept it. The exact counts depend on the WeaveC version and the optimisation level. The ledger `ledger/lookup.o.ledger.json` has one row per access; this is the read of `table[i]`: ```json { @@ -72,6 +37,10 @@ The build succeeds as it would with Clang. The summary line counts the accesses } ``` +The reason `access` says that an inline check of the bytes the read touches stays in the program. + +## Watch an index check + Run the program. An index inside the table works as before; a negative one stops the program at the access instead of reading unrelated memory: ```console @@ -89,7 +58,7 @@ Often nothing in the code says how large an object is. Save this as `vec.c`: -Inside `get`, nothing says how many elements `v->data` has. The runtime knows: its allocator gave `calloc` a 16-byte block, and every byte after it is marked as belonging to no object. +Inside `get`, nothing says how many elements `v->data` has. The runtime knows: its allocator gave `calloc` a 16-byte block, and every byte after it belongs to no object. ```console $ weavec-cc -std=c17 vec.c -o vec @@ -100,12 +69,80 @@ weavec: heap-buffer-overflow at vec.c:9:43: read of 4 bytes at 0xbd80000010 weavec: 0xbd80000010 is 0 bytes after the 16-byte heap object at 0xbd80000000 ``` -The first line names the kind of failure, the access and its address; for the heap, the second places the address relative to its object. The same guards stop a read of a freed block while it waits in the runtime's quarantine (`heap-use-after-free`), an overflow of a local or a global into its redzone, a use of a local after its scope ends, and a `free` of anything but the start of a live block. +The first line names the kind of failure, the access and its address. The second places the address relative to its heap block. Addresses differ from run to run and between platforms. + +## Use a pointer after `free` + +A released block is not reused while a pointer to it remains. Save this as `stale.c`: + +```c title="stale.c" +#include +#include + +struct session { + void (*on_close)(void); +}; + +static void bye(void) { puts("bye"); } + +int main(void) { + struct session *s = malloc(sizeof *s); + if (!s) + return 1; + s->on_close = bye; + free(s); + for (int i = 0; i < 4000000; i++) + free(malloc(sizeof *s)); + s->on_close(); + return 0; +} +``` + +```console +$ weavec-cc -std=c17 stale.c -o stale +$ ./stale +weavec: heap-use-after-free at stale.c:18:3: read of 8 bytes at 0xbd80000000 +weavec: 0xbd80000000 is inside a released heap block at 0xbd80000000 +``` + +The loop releases millions of blocks of the same size, and the allocator reuses them: released blocks wait in a quarantine until a _sweep_, which stops the program, scans its stacks, registers, writable data and live heap for pointers, and frees for reuse every quarantined block that none points to. `s` still points to its block, so every sweep keeps that block, and the call through `s->on_close` traps. Without the call, `WEAVEC_RT_STATS=1 ./stale` prints the runtime's counters at exit, among them the sweeps and the blocks the last one kept. A second `free(s)` would stop the program in the same way, with `weavec: invalid release of 0x…: the block was already released`. + +## Use a local after its scope + +Save this as `scope.c`: + +```c title="scope.c" +#include + +int main(void) { + int *p; + { + int n = 42; + p = &n; + } + printf("%d\n", *p); + return 0; +} +``` + +```console +$ weavec-cc -std=c17 scope.c -o scope +$ ./scope +weavec: stack-use-after-scope at scope.c:9:18: read of 4 bytes at 0x16db82740 +``` + +`n`'s address is taken, so `n` gets redzones in the frame and its scope is tracked: the end of its block poisons its bytes, at every optimisation level. A use of a local after its function has returned is not caught; see [what is not caught](/reference/guarantees/#what-is-not-caught). + +## Investigate a failure + +- `-fweavec-checks=report` prints each failing site's report once and lets the program continue, to see every failure in one run. +- `WEAVEC_RT_BACKTRACE=1` prints the stack after a report that stops the program. +- `WEAVEC_RT_REPORT_LOG=` writes the reports to a file instead of stderr. -To see every failure while you investigate, build with `-fweavec-checks=report`: each failing site prints its report once and the program continues. [Safety guarantees](/reference/guarantees/) says exactly what is and is not caught. +The [troubleshooting](/reference/troubleshooting/) page explains each report kind and what usually causes it. ## Continue - [Read the safety guarantees](/reference/guarantees/) to see what a guarded build promises, and under which assumptions. -- [Integrate your build](/guides/build-integration/) with `CC=weavec-cc`, and run the analysis on the same sources. -- [Look up a diagnostic](/reference/diagnostics/) for its meaning and resolution. +- [Integrate your build](/guides/build-integration/) with `CC=weavec-cc`. +- [Adopt WeaveC incrementally](/guides/adoption/): run your tests under a guarded build and fix what traps. diff --git a/docs/pages/getting-started/installation.mdx b/docs/pages/getting-started/installation.mdx index 7572776f..814a8244 100644 --- a/docs/pages/getting-started/installation.mdx +++ b/docs/pages/getting-started/installation.mdx @@ -5,7 +5,7 @@ description: Build and install WeaveC from source on macOS or Linux using LLVM, import { Tabs, TabItem } from '@astrojs/starlight/components'; -WeaveC currently ships source releases. Build it against an LLVM/Clang development installation, and keep that LLVM installation available at runtime. The CI-tested platforms are macOS and Linux. The runtime that `weavec-cc` links into the programs it builds supports 64-bit macOS and Linux targets; for any other target `weavec-cc` builds without it. +WeaveC currently ships source releases. Build it against an LLVM/Clang development installation, and keep that LLVM installation available at run time: `weavec-cc` uses its Clang resources and linker. The CI-tested platforms are macOS and Linux. The runtime that `weavec-cc` links into the programs it builds supports 64-bit macOS and Linux targets; for any other target `weavec-cc` builds without it. ## 1. Install the toolchain @@ -73,15 +73,21 @@ cmake --install build/release --prefix "$HOME/.local" export PATH="$HOME/.local/bin:$PATH" ``` -The install puts `weavec` and `weavec-cc` in `bin/`, and `weavec.h` and the runtime archives (`libweavec_rt.a`, `libweavec_alloc.a`) under `lib/weavec/`. `weavec-cc` finds the archives relative to its own location, so move the prefix as a whole. Add the PATH export to your shell configuration to retain it in new terminals. Use the [developer guide](/contributing/development/) for debug builds, alternative LLVM installations, and build options. +The install puts `weavec-cc` in `bin/`, and `weavec.h` (under `lib/weavec/include/`) and the runtime archives (`libweavec_rt.a`, `libweavec_alloc.a`) under `lib/weavec/`. It also installs WeaveC's C++ libraries, their headers and a CMake package, which compiling C programs does not need. `weavec-cc` finds the archives relative to its own location, so move the prefix as a whole. Add the PATH export to your shell configuration to retain it in new terminals. Use the [developer guide](/contributing/development/) for debug builds, alternative LLVM installations, and build options. ## 4. Verify the installation ```sh -weavec --version weavec-cc --version ``` -Both commands should print their version. If Clang resources or shared libraries cannot be found, check [installation troubleshooting](/reference/troubleshooting/). +It prints Clang's version, then `weavec-cc version X.Y.Z` and the LLVM version it was built with. To check that the runtime links, compile and run an empty program: + +```sh +printf 'int main(void) { return 0; }\n' > empty.c +weavec-cc empty.c -o empty && ./empty && echo ok +``` + +It should print `ok` with no note about building without the WeaveC runtime. If Clang resources or shared libraries cannot be found, check [installation troubleshooting](/reference/troubleshooting/). **Next:** [Run your first check](/getting-started/first-check/). diff --git a/docs/pages/getting-started/introduction.md b/docs/pages/getting-started/introduction.md index ca4ae1ee..3784670a 100644 --- a/docs/pages/getting-started/introduction.md +++ b/docs/pages/getting-started/introduction.md @@ -1,44 +1,46 @@ --- title: Meet WeaveC -description: What WeaveC does, where it fits in a C toolchain, how its compiler guards every memory access at run time, and how its ownership analysis reports bugs before the program runs. +description: What WeaveC does, where it fits in a C toolchain, and how its compiler checks every memory access at run time and never hands back freed memory a pointer still reaches. --- -WeaveC makes existing C code memory-safe at run time, and tells you about its memory bugs before it runs. It is built on Clang and LLVM and has two parts: +WeaveC makes existing C code memory-safe at run time. It is `weavec-cc`, a drop-in C compiler built on Clang and LLVM, and a small runtime that every program it links carries. It compiles C as Clang does and _guards_ every memory access: before a load, a store or a library call touches memory, a few inline instructions check that the bytes it touches belong to a live object. The first one that does not stops the program with a report that names the access. -- **`weavec-cc`, a drop-in C compiler.** It compiles C as Clang does and _guards_ every memory access: before a load, a store or a library call touches memory, a few inline instructions ask a small runtime linked into the program whether those bytes are addressable. The first access that leaves its object, reads freed memory or uses a local after its scope ends stops the program with a report that names the access. A guard is removed only where a simple local rule proves it redundant. -- **An ownership and borrowing analysis.** It follows how pointers are allocated, shared, moved and released, infers what each function does to its arguments, and reports use after free, double free, dangling pointers and leaks with the source locations that explain them. It is advisory: it never changes the compiled code. +```sh +make CC=weavec-cc +``` -## Two ways to use it +There is nothing to annotate and no separate tool to run. Existing code builds unchanged, objects from other compilers link in, and pointers keep their representation. -| Tool | Use it when | Example | -| ----------- | ---------------------------------------------------------------------------- | ------------------------------ | -| `weavec-cc` | You want binaries that stop at the first bad memory access. | `make CC=weavec-cc` | -| `weavec` | You want the analysis's diagnostics without building, for example in review. | `weavec example.c -- -std=c17` | +## No reuse while a pointer remains -The analysis tool accepts a compilation database, so it uses the includes, defines and language flags of your build. `weavec-cc -fweavec-diagnose` runs the same analysis during a build and prints its findings as warnings. +A use after free is caught however much the program allocated after the `free`. A released heap block is reused only after a _sweep_ finds no pointer to it in the program's stacks, registers, writable data and live heap; a use after free or a double free through any pointer the program holds therefore traps. Sweeps are paced so that they take a small share of the run time. + +A fortified library call (`__strcpy_chk(d, s, 8)`, which Clang emits for a `strcpy` into an 8-byte field when the C library fortifies it) never writes more than that size: an overflow from the field into the next one traps as `intra-object-overflow`. ## What the compiler guards -The runtime replaces the program's allocator and keeps one _shadow_ byte for every 16 bytes of memory, saying which of them belong to a live object. `weavec-cc` gives the stack objects a guard can reach and the file's globals _redzones_, bytes no object owns, and zero-initialises locals. A guard then catches: +The runtime replaces the program's allocator and keeps one _shadow_ byte for every 16 bytes of memory, saying which of them belong to a live object. Heap objects, the locals a guard can reach and the file's globals are separated by _redzones_, bytes no object owns, and locals are zero-initialised. A guard then catches: -- an access past either end of a heap block, or past a local or a global into its redzone; -- a read or write of a freed heap block while it waits in the runtime's quarantine, and a double or invalid `free`; +- an access past either end of a heap block, a local or a global into its redzone; +- a read or write of a freed heap block, and a double or invalid `free`; - a use of a local after its scope has ended; - an index outside an array's declared bound, including one inside a struct; - a null dereference, and a library call (`memcpy`, `strcpy`, `printf("%s")`) that would read or write outside its arguments' objects. -It does not catch everything: an access that jumps over a redzone into another live object passes, a freed block is recognised only until its storage is reused, and code that WeaveC did not compile is not guarded. The [safety guarantees](/reference/guarantees/) page states exactly what holds and under which assumptions. The [enforcement ledger](/reference/cli/#ledger-and-summary-line) lists every access and whether it kept its guard. +Some things are deliberately out of reach: an index that jumps over a redzone into another live object, a use of a local after its function returned, a pointer walked by arithmetic from one field of a struct into the next, a pointer the program hides from the sweep (stored as a transformed integer, for example), memory the runtime does not track (another allocator, `mmap`), and code that WeaveC did not compile. The [safety guarantees](/reference/guarantees/) page states exactly what holds and under which assumptions. The [enforcement ledger](/reference/cli/#ledger-and-summary-line) lists every access and whether it kept its guard. + +## Built to drop in -## Start with inference +`weavec-cc` compiles as AddressSanitizer would for the source: the code sees `__SANITIZE_ADDRESS__` and `__has_feature(address_sanitizer)`, and the runtime implements ASan's poisoning interface. Code written to be ASan-clean, such as an allocator that poisons its free space or a scan that reads past an object only when it is not built for ASan, is WeaveC-clean, and a pool that poisons its free space for ASan is checked by WeaveC too. -The analysis needs no annotations to start. A helper that calls `free` consumes its argument; a constructor describes the memory it returns; callers are checked against those facts, across source files with `weavec --whole-program`. Where inference needs help, typically at public interfaces, annotations from `weavec.h` state the contract: ownership (`WEAVEC_OWNED`, `WEAVEC_BORROWED`), nullability and extents such as `WEAVEC_COUNTED_BY(n)`. They expand to nothing under other compilers. One annotation changes the compiled code: `WEAVEC_UNSAFE` leaves a function or block unguarded, for code that reads past an object on purpose. +Code that reads past an object on purpose and is not written for ASan can be marked with `WEAVEC_UNSAFE` from `weavec.h`, which leaves a function or a block unguarded. It is the only annotation, and it expands to nothing on other compilers. See [unsafe regions](/guides/unsafe/). ## Where to go next 1. [Install from source](/getting-started/installation/) on macOS or Linux. -2. [Run your first check](/getting-started/first-check/): fix a use-after-free, then watch an index check and a guard stop a program. +2. [Run your first check](/getting-started/first-check/): build small buggy programs and read their reports. 3. [Connect an existing build](/guides/build-integration/) and [adopt WeaveC incrementally](/guides/adoption/). ## Project status -WeaveC is early software. Source releases are available; portable prebuilt binaries and package-manager distribution are future work. Flags, diagnostics, report formats and the ledger can change between minor versions. The current design is [RFC 0035, _Guard by default_](/rfcs/0035-guard-by-default/); [RFC 0036](/project/roadmap/) will make the inferred ownership contracts checked at function entry, so that they remove guards. The [roadmap](/project/roadmap/) tracks progress. +WeaveC is early software. Source releases are available; portable prebuilt binaries and package-manager distribution are future work. Flags, report formats and the ledger can change between minor versions. The current design is [RFC 0036, _Deterministic heap reuse_](/rfcs/0036-deterministic-heap-reuse/), which builds on the guards of [RFC 0035, _Guard by default_](/rfcs/0035-guard-by-default/). The [roadmap](/project/roadmap/) tracks progress. diff --git a/docs/pages/guides/adoption.md b/docs/pages/guides/adoption.md index 03f0eaf9..8632e92f 100644 --- a/docs/pages/guides/adoption.md +++ b/docs/pages/guides/adoption.md @@ -1,6 +1,6 @@ --- title: Adopt WeaveC incrementally -description: Introduce WeaveC to an existing C project, run its tests under the guarded build, fix what traps, measure the cost, and add the advisory analysis to review and CI. +description: Introduce WeaveC to an existing C project, run its tests under the guarded build, read what traps, fix it, and measure the cost. --- Start with a component you understand: one with identifiable allocation and cleanup paths and a test suite that exercises them. @@ -15,47 +15,48 @@ cmake --build build-weavec ctest --test-dir build-weavec ``` -A correct program builds and passes as it does with Clang. Every memory access is guarded, so a test that trapped found a memory error at the access that made it. See [build integration](/guides/build-integration/) for Make and other build systems. +For Make and configure scripts, `make CC=weavec-cc` or `./configure CC=weavec-cc`. A correct program builds and passes as it does with Clang. Every memory access is guarded, so a test that trapped found a memory error at the access that made it. See [build integration](/guides/build-integration/) for the details of the link. -## 2. Read what traps +## 2. Survey what traps -Each trap prints the kind of failure, the source location of the access and its address; for the heap, a second line places the address in its block: +A first run may stop at the first of several problems. To see them all, build the tests with `-fweavec-checks=report`: each failing site prints its report once and the program goes on. If the test harness captures standard error, set `WEAVEC_RT_REPORT_LOG=` and the reports go to that file instead. -```text -weavec: heap-use-after-free at buffer.c:88:12: read of 8 bytes at 0x... -weavec: 0x... is inside a released heap block at 0x... +```sh +cmake -S . -B build-survey -G Ninja -DCMAKE_C_COMPILER=weavec-cc \ + -DCMAKE_C_FLAGS="-fweavec-checks=report" +cmake --build build-survey +WEAVEC_RT_REPORT_LOG=$PWD/reports.log ctest --test-dir build-survey ``` -To collect every failure in one run, build the tests with `-fweavec-checks=report`: each failing site prints once and the program goes on. If the test harness captures standard error, set `WEAVEC_RT_REPORT_LOG=` and the reports go to that file instead. The [command-line reference](/reference/cli/#check-modes) lists the report kinds. - -A trap is either a real bug or code that relies on undefined behaviour that happens to work: reading one element past an array, reading a word at a time past the end of a string's allocation, using the slack the system allocator leaves after a block (`malloc_usable_size` now returns the requested size), or indexing a two-dimensional array through its first row. Fix the code. If the over-read is intentional and harmless, put it in a small `WEAVEC_UNSAFE` region, or keep it in a function the project already excludes from AddressSanitizer with `no_sanitize("address")`: neither is guarded. See [unsafe boundaries](/guides/unsafe/). - -Locals are zero-initialised in the enforcing modes and the runtime's heap is zero-filled, so code that read uninitialised memory now reads zeros. +Keep the trapping build (`trap`, the default) for CI: in `report` mode the program goes on after a bad access, through memory it should not have touched. -## 3. Run the analysis +## 3. Read each report -The ownership analysis reports bugs the tests do not reach: a use after free on an error path, a double free, a dangling pointer returned from a function, a leak. Run it on the same sources, with the build's flags: +Each report names the kind of failure, the source location of the access and its address; a second line places the address relative to the object the pointer came from: -```sh -cmake -S . -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -weavec -p build src/buffer.c -weavec --whole-program -p build +```text +weavec: heap-use-after-free at buffer.c:88:12: read of 8 bytes at 0x... +weavec: 0x... is inside a released heap block at 0x... ``` -Errors are definite: the bug happens on every execution that reaches the line, and the analysis has found a feasible path that reaches it. Follow the reported allocation, alias, release or escape and fix the code. Warnings with "may" wording are temporal bugs on some paths only. Check a helper's inferred behaviour with `--dump-analysis` when its effect is surprising, and add [annotations](/reference/annotations/) where inference needs help, typically at public interfaces. +`WEAVEC_RT_BACKTRACE=1` adds the stack of the access that stopped the program. The [command-line reference](/reference/cli/#check-modes) lists the report kinds, and [troubleshooting](/reference/troubleshooting/) the common causes. -The analysis is advisory: it does not change what `weavec-cc` compiles, and a false finding does not stop a build. To see its findings during a build, add `-fweavec-diagnose`, which prints them as warnings. +A trap is either a real bug or code that relies on undefined behaviour that happens to work: reading one element past an array, reading a word at a time past the end of a string's allocation, using the slack the system allocator leaves after a block (`malloc_usable_size` returns the requested size), or an index computed from one object that lands in another. Fix the code. Code written to be clean under AddressSanitizer needs nothing more: `weavec-cc` compiles it as ASan would (`__SANITIZE_ADDRESS__` and `__has_feature(address_sanitizer)` are set), honours its `no_sanitize("address")` functions, and implements ASan's poisoning interface. If an over-read is intentional and harmless, and the project does not already keep it from ASan, put it in a small `WEAVEC_UNSAFE` region as a last resort: its accesses are not guarded. See [unsafe boundaries](/guides/unsafe/). + +Locals are zero-initialised in the enforcing modes and the runtime's heap is zero-filled, so code that read uninitialised memory now reads zeros. ## 4. Measure the cost -The program now runs on WeaveC's allocator, which holds freed blocks in a quarantine (16 MiB by default; `WEAVEC_RT_QUARANTINE=` changes it), and every access the local rules cannot prove runs a guard. Measure on your own workload before you ship a guarded build: interpreters and tight loops over pointers pay the most. `WEAVEC_RT_STATS=1` in the environment prints the runtime's counters when the program exits, and `-fweavec-ledger=/` with `-fweavec-summary` shows how many accesses kept their guard: +The program now runs on WeaveC's allocator, and every access the local rules cannot prove runs a guard. Measure on your own workload before you ship a guarded build: interpreters and tight loops over pointers pay the most. `WEAVEC_RT_STATS=1` in the environment prints the runtime's counters when the program exits, and `-fweavec-summary` (or a ledger, `-fweavec-ledger=/`) prints how many accesses of each file kept their guard: ```text weavec: buffer.c: 812 accesses: 431 proven, 381 guarded, 0 unguarded ``` -A hot loop over an array whose trip count is known runs without guards when its whole range is addressable. A component that cannot afford the guards can build with `-fweavec-checks=none`, which compiles it as Clang does and gives up its protection; units built with and without guards link together. +A freed block waits in a quarantine and is reused only after a _sweep_ (the program's threads stopped, its memory scanned) finds no pointer to it. A block that some stale pointer still names stays allocated until that pointer is overwritten, so a program that keeps dangling pointers keeps their memory, and a program with a large live heap pauses for each sweep. `WEAVEC_RT_STATS=1` reports the sweeps, their longest pause and the bytes they retained; `WEAVEC_RT_QUARANTINE=` sets how much released memory waits before one runs (16 MiB, or a quarter of the live heap if that is larger). On Linux the sweep stops threads with a signal (`SIGPWR`, or `SIGXCPU` when the program handles `SIGPWR`); `WEAVEC_RT_SWEEP_SIGNAL=` names another. + +A component that cannot afford the guards can build with `-fweavec-checks=none`, which compiles it as Clang does and gives up its protection; units built with and without guards link together. ## 5. Keep the result reproducible -Run the guarded build and its tests in CI, and let a trap fail the job. Add a `weavec --whole-program -p build` step, or build with `-fweavec-diagnose -Werror=weavec`, to fail the job on what the analysis reports; the second turns a false error into a failed build, so start with the first and review its findings. Keep the enforcement ledgers as build artifacts if you track how much of the code runs guarded. +Run the guarded build and its tests in CI, and let a trap fail the job. Set `WEAVEC_RT_REPORT_LOG` so the reports land in a file the job uploads. Keep the enforcement ledgers as build artifacts if you track how much of the code runs guarded, and review every new `WEAVEC_UNSAFE` region as you would any other unchecked code. diff --git a/docs/pages/guides/build-integration.md b/docs/pages/guides/build-integration.md index b2553cbd..607eafd8 100644 --- a/docs/pages/guides/build-integration.md +++ b/docs/pages/guides/build-integration.md @@ -1,9 +1,9 @@ --- title: Integrate your build -description: Use weavec-cc as the C compiler with Make or CMake, understand what changes in the compiled program and its link, run the analysis from a compilation database, and add both to CI. +description: Use weavec-cc as the C compiler with Make, CMake or a configure script, understand what changes in the compiled program and its link, and add the guarded build to CI. --- -`weavec-cc` replaces the C compiler in an existing build. The `weavec` analysis tool runs beside the build, from its compilation database. +`weavec-cc` replaces the C compiler in an existing build. Nothing else in the build changes. ## Use the compiler driver with Make @@ -29,7 +29,7 @@ cmake -S . -B build-weavec -G Ninja -DCMAKE_C_COMPILER=weavec-cc cmake --build build-weavec ``` -The driver forwards ordinary compiler options to Clang. WeaveC options use the `-fweavec-*` and `-W*weavec*` forms listed in the [command-line reference](/reference/cli/). +The driver forwards ordinary compiler options to Clang. WeaveC's own options use the `-fweavec-*` form listed in the [command-line reference](/reference/cli/); `weavec-cc --help-weavec` prints them. Configure scripts see Clang: `weavec-cc --version` prints Clang's version block first and `weavec-cc version …` after it, and a feature probe compiles as it would with Clang. On macOS a link with `-flto` uses the libLTO of the LLVM that WeaveC was built with. @@ -37,20 +37,21 @@ Configure scripts see Clang: `weavec-cc --version` prints Clang's version block By default `weavec-cc` compiles with `-fweavec-checks=trap`. Each C file is compiled as Clang compiles it, with these additions: -- every load, store, atomic operation and memory intrinsic, and every memory argument of a C library call in the library table, gets a _guard_: a few inline instructions that read the runtime's shadow memory and call the runtime only when a byte they read is not zero. A guard is removed where a local rule proves it redundant; -- the locals a guard can reach are laid out in one frame with redzones, and the file's globals get redzones, which a constructor registers with the runtime; -- array indexes are checked against the bound the array's type declares (`-fsanitize=array-bounds`, reported through the runtime); -- locals are zero-initialised (`-ftrivial-auto-var-init=zero`); +- every load, store, atomic operation and memory intrinsic, and every memory argument of a C library call in the library table, gets a _guard_: a few inline instructions that read the runtime's shadow memory and call the runtime only when a byte they read is not zero. A guard is removed only where a local rule proves it redundant; +- the locals a guard can reach, or whose address escapes, are laid out in one frame with redzones between them, and locals whose lifetimes never overlap share space as Clang's own frames do. The file's globals get redzones, which a constructor registers with the runtime; +- array indexes are checked against the bound the array's type declares (`-fsanitize=array-bounds`, reported through the runtime), and a fortified library call (`__strcpy_chk` and the like) is checked against the object size it carries. On glibc targets an optimising compile that does not set `_FORTIFY_SOURCE` gets `-D_FORTIFY_SOURCE=3`; +- the file is parsed as AddressSanitizer would parse it: `__SANITIZE_ADDRESS__` is defined and `__has_feature(address_sanitizer)` is true, so code written to be ASan-clean takes the same paths. `-fno-weavec-asan-compat` turns this off; +- locals are zero-initialised (`-ftrivial-auto-var-init=zero`; `-fno-weavec-zero-init` turns it off), and `__WEAVEC__` is defined; - source locations are kept for the reports, without emitting debug information when you asked for none. -There is no change to pointer representation or the ABI. `-fweavec-checks=report` prints failed guards instead of trapping; `-fweavec-checks=none` compiles the same code as Clang would and links nothing. C++ and Objective-C sources are compiled as Clang compiles them. +There is no change to pointer representation or the ABI. `-fweavec-checks=report` prints failed guards instead of trapping; `-fweavec-checks=none` (or `-fno-weavec`) compiles the same code as Clang would and links nothing. C++ and Objective-C sources are compiled as Clang compiles them. Every link in the `trap`, `report` and `verify` modes adds two archives before the system libraries: -- `libweavec_rt.a`, the runtime: the shadow memory, the arena allocator's heap with its quarantine of freed blocks, the guards' slow paths, the checked library wrappers and the reports; +- `libweavec_rt.a`, the runtime: the shadow memory, the arena allocator, the quarantine of released blocks and the sweep that recycles them, the guards' slow paths, the checked library wrappers, AddressSanitizer's poisoning interface and the reports; - `libweavec_alloc.a`, which defines `malloc`, `calloc`, `realloc`, `free` and the other standard allocation functions for the image, over the runtime's arena. -On Linux the executable's definitions serve every shared library in the process. On Darwin one runtime serves the process: the first copy the dynamic loader finds owns the arena and the shadow, and the runtimes of other images built by `weavec-cc` (shared libraries, plug-ins) forward to it. Its malloc zone becomes the process's default zone, so the C library's own allocations (`strdup`, `getline`, `asprintf` and the like) are tracked heap blocks too. +On Linux the executable's definitions serve every shared library in the process. On Darwin one runtime serves the process: the first copy the dynamic loader finds owns the arena and the shadow, and the runtimes of other images built by `weavec-cc` (shared libraries, bundles, plug-ins) forward to it, so their globals' redzones live in the process's one shadow. Its malloc zone becomes the process's default zone, so the C library's own allocations (`strdup`, `getline`, `asprintf` and the like) are tracked heap blocks too. Link through `weavec-cc`; a link by another driver or by `ld` directly fails with undefined `__weavec_rt_` symbols. The build compiles without guards and links without the runtime when the command line has a sanitizer that replaces the allocator (`-fsanitize=address`, `hwaddress`, `memory`, `thread`, `leak`, `kernel-address`), `-ffreestanding`, `-nostdlib`, `-nodefaultlibs` or `-nolibc`, or a target other than 64-bit Darwin or Linux. The link says so: @@ -64,19 +65,9 @@ A program that defines `malloc`, `calloc`, `realloc` or `free` itself keeps its weavec-cc: note: 'alloc.o' defines the allocator, so the WeaveC runtime's is not linked: the heap is untracked, guards pass on it and releases are not validated ``` -Objects compiled by `weavec-cc` and objects from another compiler link together. The other objects' code is not guarded; their calls into guarded code, and the memory they allocate through `malloc`, are covered as usual. The [command-line reference](/reference/cli/#the-runtime) lists the environment variables, and [safety guarantees](/reference/guarantees/) says what a guard promises. - -## Analyze a compilation database +An allocator that poisons its free space through ASan's interface (`ASAN_POISON_MEMORY_REGION`) gets the same protection under `weavec-cc`: an access to poisoned bytes reports `use-after-poison`. -For CMake projects using Ninja or Makefiles: - -```sh -cmake -S . -B build -G Ninja -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -weavec -p build src/main.c -weavec --whole-program -p build -``` - -`compile_commands.json` supplies each source file's compilation flags. The whole-program command selects all sources in the database when no source is named. Keep the database current when the build configuration changes. `weavec` builds nothing; it prints the analysis's diagnostics and a summary line per file. To get the same findings during a `weavec-cc` build, as warnings, add `-fweavec-diagnose`. +Objects compiled by `weavec-cc` and objects from another compiler link together. The other objects' code is not guarded; their calls into guarded code, and the memory they allocate through `malloc`, are covered as usual. The [command-line reference](/reference/cli/#the-runtime) lists the environment variables, and [safety guarantees](/reference/guarantees/) says what a guard promises. ## Add checks to CI @@ -89,4 +80,4 @@ cmake --build build-weavec ctest --test-dir build-weavec ``` -Let a nonzero exit status fail the job: an error from the build, or a trap in a test. If tests capture standard error, set `WEAVEC_RT_REPORT_LOG=` so the reports land in a file you can upload. Run `weavec --whole-program -p build` as a separate step to report what the analysis finds; its errors fail the step. Tests run slower under the guarded build than under a plain one; budget for it. +Let a nonzero exit status fail the job: an error from the build, or a trap in a test. If tests capture standard error, set `WEAVEC_RT_REPORT_LOG=` so the reports land in a file you can upload. Tests run slower under the guarded build than under a plain one; budget for it. diff --git a/docs/pages/guides/ownership.md b/docs/pages/guides/ownership.md deleted file mode 100644 index bd1b3dd3..00000000 --- a/docs/pages/guides/ownership.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -title: Ownership and borrowing -description: Learn how WeaveC's advisory analysis models owning pointers, borrows, moves and lifetimes in C, and how its findings relate to the run-time guards. ---- - -An allocation needs someone responsible for releasing it. Other code may use it temporarily, but those uses must finish while the object is still alive. Ownership and borrowing give names to that discipline. - -WeaveC's ownership analysis is advisory. `weavec` runs it and reports what it finds, and `weavec-cc -fweavec-diagnose` runs it beside the compiler; neither changes the generated code. What stops a use after free in a build is the run-time guards `weavec-cc` puts on every memory access, which work without any annotation. - -## Own, borrow, or cross a boundary - -| Pointer kind | Meaning | Annotation | -| -------------- | ------------------------------------------------------------------------- | ----------------- | -| Owned | Responsible for the referent's resource lifetime. | `WEAVEC_OWNED` | -| Shared borrow | Temporary, read-only access to a live referent. | `WEAVEC_BORROWED` | -| Mutable borrow | Temporary mutable access to a live referent. | `WEAVEC_MUT` | -| Raw | Outside the safe pointer contract; operations require an unsafe boundary. | `WEAVEC_RAW` | - -Inference determines kinds and effects from ordinary C whenever possible. Annotations make an interface's expectations explicit to the analysis. Include `weavec.h` to use them. - -```c -#include - -struct buffer *WEAVEC_OWNED buffer_new(unsigned capacity); -void buffer_free(struct buffer *WEAVEC_OWNED buffer); -unsigned buffer_size(const struct buffer *WEAVEC_BORROWED buffer); -void buffer_clear(struct buffer *WEAVEC_MUT buffer); -``` - -These declarations express the intended public interface. The analysis checks callers against them and each definition against its own annotations, and `weavec --whole-program` also checks each declaration against the definition in another file. - -## Copying a pointer does not copy the object - -If `alias = owner`, both pointers name the same allocation. Releasing it through either pointer invalidates both. The analysis follows that identity through supported assignments, fields, calls, and control flow. - -Moving ownership changes who may release or use the resource. After a consuming call, the caller cannot continue using the old owner as if the transfer had not happened. - -## A borrow has a lifetime - -A borrow must end before its referent is destroyed. WeaveC uses the pointer's last use when determining whether the borrow is live; the end of the lexical scope is not always the end of the borrow. - -WeaveC reports release, move and reallocation conflicts with live borrows (`conflicting-borrow`). Two live pointers into one object, or a write to an object another pointer views, are accepted. - -## Findings and guards - -In `weavec`, a finding that holds on every path is an error (`use-after-free`, `double-free`, `use-after-move`, ...) and one that holds on some paths only is a warning; under `weavec-cc -fweavec-diagnose` every finding is a warning, and `-Werror=weavec` makes them errors. Run `weavec` in CI, or build with `-fweavec-diagnose -Werror=weavec`, to fail on them. See [diagnostics](/reference/diagnostics/). - -The analysis is not what makes a build safe. A `weavec-cc` build guards every access, whatever the analysis concluded and whatever the annotations say: - -- a use of a heap object after its `free` traps while the released block is in the runtime's quarantine (16 MiB of released blocks by default); once the block is reused, a stale pointer sees the new object; -- every `free` is validated by the runtime's allocator, so a double free or the release of an interior or non-heap pointer stops the program; -- a use of a local after its scope ends traps; a use after the function returns does not. - -The analysis also reports what the guards cannot catch: a leak, a mismatched release family, a use after free once the block has been recycled. See [safety guarantees](/reference/guarantees/). - -RFC 0036 (planned; see the [roadmap](/project/roadmap/)) gives ownership an enforcement role: contracts checked at function entry. For the model's precise semantics, read [RFC 0001](/rfcs/0001-ownership-model/) as amended by [RFC 0035](/rfcs/0035-guard-by-default/). For syntax, use the [annotation reference](/reference/annotations/). diff --git a/docs/pages/guides/unsafe.md b/docs/pages/guides/unsafe.md index 314cfa06..e842fe41 100644 --- a/docs/pages/guides/unsafe.md +++ b/docs/pages/guides/unsafe.md @@ -1,15 +1,15 @@ --- title: Make unsafe boundaries explicit -description: Use narrow WEAVEC_UNSAFE regions for code that must run unguarded or handle raw pointers, and understand what the analysis still checks there. +description: Use narrow WEAVEC_UNSAFE regions for correct code that must run unguarded, and keep track of what runs without a guard. --- -`weavec-cc` guards every memory access of the code it compiles. A few correct programs touch memory a guard rejects on purpose, and some operations depend on knowledge the analysis cannot establish: a hardware address, an allocator's own bookkeeping, a representation crossing. `WEAVEC_UNSAFE` marks that code at a small, reviewable boundary. +`weavec-cc` guards every memory access of the code it compiles. A few correct programs touch memory a guard rejects on purpose: a word-at-a-time scan, an allocator's bookkeeping, a hardware register. `WEAVEC_UNSAFE` marks that code at a small, reviewable boundary. ## What a region changes in the build -`WEAVEC_UNSAFE` is the only annotation that changes the generated code. In a `weavec-cc` build, the accesses whose source location lies in the function or block are not guarded, and its array indexes are not checked. Code inlined into the region keeps its own location: a guarded helper inlined there stays guarded. The runtime's allocator still validates every `free` made in a region. +`WEAVEC_UNSAFE` is WeaveC's only annotation. It comes from `weavec.h`, which `weavec-cc` puts on the include path, and it marks a function (placed before its definition) or a block (placed before a compound statement). In a `weavec-cc` build, the accesses whose source location lies in the function or block are not guarded, and its array indexes are not checked. Code inlined into the region keeps its own location: a guarded helper inlined there stays guarded. The runtime's allocator still validates every `free` made in a region. See [the weavec.h header](/reference/annotations/) for both forms. -A function its author keeps from AddressSanitizer is unguarded the same way, with no source change: `__attribute__((no_sanitize("address")))`, `no_sanitize_address` and `disable_sanitizer_instrumentation` each make its body an unguarded region. The analysis treats such a function as ordinary code; only `WEAVEC_UNSAFE` also changes what the analysis trusts. +A function its author keeps from AddressSanitizer is unguarded the same way, with no source change: `__attribute__((no_sanitize("address")))`, `no_sanitize_address` and `disable_sanitizer_instrumentation` each make its body an unguarded region. Code that keeps a scan from ASan with `#if` on `__SANITIZE_ADDRESS__` or `__has_feature(address_sanitizer)` takes its ASan path under `weavec-cc` too, and memory the program poisons through ASan's interface (`ASAN_POISON_MEMORY_REGION`) traps when guarded code touches it. ## When to use one @@ -17,9 +17,10 @@ Use a region for code that is correct but reads or writes memory a guard rejects - a deliberate over-read, such as a word-at-a-time string scan or hash that reads past the end of an allocation within the same aligned word; - an allocator's internals, which use the slack after a block or the headers between blocks; -- a conservative garbage collector's scan of the stack or of memory it does not own. +- a conservative garbage collector's scan of the stack or of memory it does not own; +- an access to a fixed hardware address. -Under AddressSanitizer these need `no_sanitize("address")` for the same reason; code that already has it needs nothing more. A region is not a way to silence a report you have not understood: when the code was not written to read past an object, a failing guard is usually a real bug. +Under AddressSanitizer these need `no_sanitize("address")` for the same reason; code that already has it needs nothing more. A region is the last resort, not a way to silence a report you have not understood: when the code was not written to read past an object, a failing guard is usually a real bug. ```c #include @@ -32,29 +33,14 @@ WEAVEC_UNSAFE void write_register(uintptr_t address, uint32_t value) { This function's write is not guarded. Its caller must ensure that the address refers to the intended mapped register and that the write is valid; the annotation does not establish those facts. -## Raw pointers - -Pointers annotated `WEAVEC_RAW`, and values loaded through, derived from or handed out as raw pointers, have no safe ownership guarantee. Copying or comparing raw pointers is allowed. Dereferencing, releasing or transferring them into an owning contract outside an unsafe region is an `unsafe-operation` error in `weavec`. A value is raw only when it is raw on every path; one that is raw on some paths only is treated like a pointer of unknown origin. - -A pointer converted from an integer (`(struct node *)h`, `(void *)(uintptr_t)n`) is not raw: it is a pointer of unknown provenance. The analysis leaves its accesses not proven and never reports them; a `weavec-cc` build guards them like any other access outside a region. - -## What the analysis still checks in a region - -Inside a `WEAVEC_UNSAFE` function or block, `weavec` and `weavec-cc -fweavec-diagnose`: - -- permit raw operations, and trust them; -- trust spatial and null operations: a possible overrun or null dereference there is never reported; -- track temporal state exactly as outside the region: a use after a definite free is still reported, and a possible one is a warning; -- still report a definite spatial or null violation, such as a constant index past the end of an array. - -Nothing inside a region is suppressed, and what the region does to the surrounding code is visible: freeing an object inside it invalidates pointers used afterwards. - ## Keep the boundary narrow -Every access in a region runs without a guard, so a bug there is not caught at run time. Keep each region to the operation that needs it, prefer an interface that states the actual ownership and lifetime expectations, and document why each operation is valid and who maintains that assumption when the code changes. The trusted sites are counted in the summary line of `weavec`, and the enforcement ledger of a build (`-fweavec-ledger=`) lists each unguarded access with the reason `unsafe`. See [safety guarantees](/reference/guarantees/). +Every access in a region runs without a guard, so a bug there is not caught at run time, and a pointer the region corrupts can make a later guarded access fail far from the cause. Keep each region to the operation that needs it, prefer a block around the one statement to a whole function, and document why each operation is valid and who maintains that assumption when the code changes. -## Assumptions are not checked at run time +The accesses a region leaves unguarded are counted. `weavec-cc -fweavec-summary` prints a line per file whose last count is the unguarded accesses, and the enforcement ledger of a build (`-fweavec-ledger=`) lists each one with the outcome `unguarded` and the reason `unsafe`: -`WEAVEC_ASSUME(expr)` tells the analysis a fact it cannot derive. When the analysis refutes `expr`, that is a `contradicted-assumption`; otherwise it assumes `expr` from there on. Nothing checks an assumption at run time, and no guard depends on one, in a region or outside it: an assumption affects only what the analysis reports. +```text +weavec: scan.c: 214 accesses: 96 proven, 117 guarded, 1 unguarded +``` -See [annotation placement](/reference/annotation-placement/) and [RFC 0004](/rfcs/0004-unsafe-boundaries/), as amended by [RFC 0030](/rfcs/0030-prove-or-trap/) and [RFC 0035](/rfcs/0035-guard-by-default/), for the precise contract. +Review a change to that count as you would any other unchecked code. See [safety guarantees](/reference/guarantees/) for what a region removes from the guarantee, and [RFC 0035](/rfcs/0035-guard-by-default/) §2.6 for the precise contract. diff --git a/docs/pages/guides/whole-program.md b/docs/pages/guides/whole-program.md deleted file mode 100644 index 6db4dcbb..00000000 --- a/docs/pages/guides/whole-program.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -title: Analyze the whole program -description: Report bugs that span C translation units with weavec --whole-program, what it checks across files, and how the analysis treats code it cannot see. ---- - -A function's implementation may live in another source file. Whole-program analysis lets a caller use the effects inferred from that definition, so a use after free whose `free` is in another file is reported before the program runs. Like all of WeaveC's analysis it is advisory: a guarded build stops such a bug at run time whether or not the analysis saw it. - -## Analyze source files together - -```sh -weavec --whole-program main.c buffer.c -- -std=c17 -Iinclude -``` - -For files with different compiler flags, use their compilation database: - -```sh -weavec --whole-program -p build -``` - -Each translation unit exports what its functions do to their arguments (their _summaries_). WeaveC combines the available definitions and revisits callers as their callees' effects become known, so a wrapper that releases a resource invalidates aliases in a different file. Across the files it also: - -- solves which functions each function pointer can hold across the program; -- checks every declaration against its definition in another file: a declaration that says `WEAVEC_BORROWED` for a parameter the definition frees is an `annotation-mismatch` error. - -Everything stays in memory; nothing is written next to the objects. The tool prints a summary line per file and one for the program. A group of files that call each other in a cycle and whose summaries do not settle keeps its widened summaries, with a note: - -```text -weavec: note: the whole-program analysis of 'a.c', 'b.c' did not converge (its widened summaries are used) -``` - -`weavec-cc -fweavec-diagnose` analyses each file on its own as it compiles it; it does not analyse the program when it links. Run `weavec --whole-program` in CI to report the bugs that span files. - -## Understand unknown code - -A call to a function WeaveC cannot see (no definition in the program, no library-table entry, no ownership annotation, and not declared in a C library, POSIX or platform header) is an _unknown callee_. The analysis assumes it may release, keep or replace every pointer argument that has no ownership annotation. Later uses of those pointers are not reported, and they are not proven. Annotating the callee's declaration (`WEAVEC_BORROWED` on a parameter it neither keeps nor frees) tells the analysis what it does. In a per-file run, calls into other files are unknown; `weavec --whole-program` replaces them with the definitions' summaries. - -Functions declared in platform headers that the library table does not describe are assumed to borrow their arguments; the analysis trusts them (`system-api`). Calls through a function pointer use the targets the program stores into it; a pointer with no known target is treated like an unknown callee. - -None of this affects the compiled program: every access is guarded whatever the analysis knows. The design is specified by [RFC 0005](/rfcs/0005-whole-program-analysis/), [RFC 0031](/rfcs/0031-object-engine/) and [RFC 0035](/rfcs/0035-guard-by-default/) §8. diff --git a/docs/pages/index.mdx b/docs/pages/index.mdx index 16da5a72..11bab519 100644 --- a/docs/pages/index.mdx +++ b/docs/pages/index.mdx @@ -1,6 +1,6 @@ --- title: C, with more certainty. -description: A drop-in C compiler that stops your existing C code at its first bad memory access, and an ownership analysis that reports memory bugs before the program runs. WeaveC is built on Clang and LLVM. +description: A drop-in C compiler that stops your existing C code at its first bad memory access, and never reuses freed memory a pointer still reaches. WeaveC is built on Clang and LLVM. template: splash hero: title: C, with more certainty. diff --git a/docs/pages/reference/cli.md b/docs/pages/reference/cli.md index 1bc944cc..d22f4418 100644 --- a/docs/pages/reference/cli.md +++ b/docs/pages/reference/cli.md @@ -1,40 +1,47 @@ --- title: Command-line reference -description: weavec-cc and weavec options for guard modes, zero-initialisation, the runtime and its environment variables, the enforcement ledger, the advisory analysis, budgets and diagnostic controls. +description: weavec-cc options for guard modes, zero-initialisation, AddressSanitizer compatibility, fortification, the runtime and its environment variables, the enforcement ledger and its summary line, and the options that were removed. --- -WeaveC has two tools. `weavec-cc` is Clang's compiler driver with WeaveC inside: it guards every memory access of the C code it compiles and links the runtime that the guards ask. `weavec` is a libTooling analysis tool: it runs the ownership and lifetime analysis and prints its diagnostics without producing objects. The options below are specified by [RFC 0035](/rfcs/0035-guard-by-default/) §7 and §8; `weavec-cc --help-weavec` and `weavec --help` list the ones your build accepts. +WeaveC is one tool, `weavec-cc`: Clang's compiler driver with WeaveC inside. It guards every memory access of the C code it compiles and links the runtime that the guards call. The options below are specified by [RFC 0035](/rfcs/0035-guard-by-default/) §7 as amended by [RFC 0036](/rfcs/0036-deterministic-heap-reuse/) §1, §6 and §7; `weavec-cc --help-weavec` lists the ones your build accepts. ## Invocation ```sh weavec-cc [clang options] [weavec-cc options] source.c -o program -weavec [options] source.c -- [clang options] -weavec [options] -p build [source.c ...] -weavec --whole-program [options] -p build -weavec --whole-program [options] a.c b.c -- [clang options] +make CC=weavec-cc ``` -`weavec-cc` accepts every Clang driver option. For `weavec`, place its options **before** `--` and the language standard, include paths, defines and target flags **after** it when you are not using a compilation database. With `-p build` and no source named, `--whole-program` analyses every source in `build/compile_commands.json`. +`weavec-cc` accepts every Clang driver option, so a build system uses it as it would use `cc`. ## weavec-cc -| Flag | Default | Meaning | -| ---------------------------------------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `-fweavec` / `-fno-weavec` | on | Guard memory accesses. Off compiles as plain Clang. | -| `-fweavec-checks=trap\|report\|verify\|none` | `trap` | What a failed guard does; see [check modes](#check-modes). | -| `-fweavec-zero-init` / `-fno-weavec-zero-init` | on unless checks are `none` | Zero-initialise locals of up to 4 KiB (`-ftrivial-auto-var-init=zero`; a `-ftrivial-auto-var-init-max-size` on the command line sets another limit). The runtime's allocator zero-fills every block regardless. | -| `-fweavec-ledger=` | unset | Write the [enforcement ledger](#ledger-and-summary-line). A value ending in `/`, or naming an existing directory, receives `.ledger.json` per unit; a file receives one ledger, and naming one for an invocation that compiles several C files is an error. | -| `-fweavec-summary` / `-fno-weavec-summary` | off, on when a ledger is written | Print the unit's summary line on stderr. | -| `-fweavec-diagnose` / `-fno-weavec-diagnose` | off | Also run the [advisory analysis](#the-advisory-analysis) on each unit and print its findings as warnings. | -| `-fweavec-budget=`, `-fweavec-unit-budget=` | see [budgets](#budgets) | The analysis's work budgets per function and per unit, under `-fweavec-diagnose`. | -| `-fweavec-dump-analysis`, `-fweavec-analysis-stats=` | off | Debugging output of the analysis: the inferred facts (unstable format), and work statistics as JSON. | -| `--help-weavec` | | List the WeaveC flags and exit. | +| Flag | Default | Meaning | +| -------------------------------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `-fweavec` / `-fno-weavec` | on | Guard memory accesses. Off compiles as plain Clang. | +| `-fweavec-checks=trap\|report\|verify\|none` | `trap` | What a failed guard does; see [check modes](#check-modes). | +| `-fweavec-zero-init` / `-fno-weavec-zero-init` | on unless checks are `none` | Zero-initialise locals of up to 4 KiB (`-ftrivial-auto-var-init=zero`; a `-ftrivial-auto-var-init-max-size` on the command line sets another limit). The runtime's allocator zero-fills every block regardless. | +| `-fweavec-asan-compat` / `-fno-weavec-asan-compat` | on | Compile and preprocess C as AddressSanitizer would for the source; see [AddressSanitizer compatibility](#addresssanitizer-compatibility). | +| `-fweavec-ledger=` | unset | Write the [enforcement ledger](#ledger-and-summary-line). A value ending in `/`, or naming an existing directory, receives `.ledger.json` per unit; a file receives one ledger, and naming one for an invocation that compiles several C files is an error. | +| `-fweavec-summary` / `-fno-weavec-summary` | off, on when a ledger is written | Print the unit's summary line on stderr. | +| `--help-weavec` | | List the WeaveC flags and exit. | An unknown `-fweavec-*` flag is an error. Every other flag is Clang's. When WeaveC is on, the driver defines `__WEAVEC__=1` and puts the directory of `weavec.h` on the system include path. `weavec-cc --version` prints Clang's version block first, then `weavec-cc version ()`, so a configure script that reads the first line sees Clang. On macOS a link with `-flto` uses the libLTO of the LLVM that WeaveC was built with. C++, Objective-C, OpenCL and CUDA sources are compiled as Clang compiles them, without guards; so is everything under `-fno-weavec` or `-fweavec-checks=none`. +### Fortification + +On glibc targets, an optimising compile in an enforcing mode whose command line neither defines nor undefines `_FORTIFY_SOURCE` gets `-D_FORTIFY_SOURCE=3`, so that the C library's headers pass each string and `printf`-family call the size of the object or field it writes; the guards check that size ([guarantee (F)](/reference/guarantees/#the-guarantee)). A command line that sets its own level, `-D_FORTIFY_SOURCE=0` included, keeps it. Darwin's SDK fortifies optimising compiles at level 2 by itself. + +### AddressSanitizer compatibility + +By default, in the enforcing modes, `weavec-cc` parses each C unit as AddressSanitizer would: `__SANITIZE_ADDRESS__` is defined, `__has_feature(address_sanitizer)` is true and `no_sanitize("address")` is accepted. AddressSanitizer's own instrumentation never runs. Code written to be clean under ASan, such as a pool allocator that poisons its free space with `ASAN_POISON_MEMORY_REGION` or a scan that reads past an object only when ASan is off, is then clean under WeaveC. On Darwin, where Clang turns fortification off under ASan, the SDK's fortification stays on. + +The runtime implements ASan's poisoning interface with ASan's meaning: `__asan_poison_memory_region` (whole 16-byte granules inside the region become not addressable, and an access to them reports `use-after-poison`), `__asan_unpoison_memory_region`, `__asan_address_is_poisoned`, `__asan_region_is_poisoned` and `__asan_describe_address`. It accepts and ignores `__asan_handle_no_return`, the fake-stack queries (there is no fake stack), `__sanitizer_print_stack_trace`, `__sanitizer_set_report_path`, `__sanitizer_annotate_contiguous_container`, the fiber-switch hooks, and LeakSanitizer's `__lsan_*` entry points (there is no leak checking). + +`-fno-weavec-asan-compat` keeps the language options as the command line set them, for code whose ASan paths call a sanitizer interface the runtime does not implement (a link error) or behave differently in a way the program does not want. A unit built with a sanitizer of its own (`-fsanitize=address` and the like) is [built without the runtime](#the-runtime). + ## Check modes | `-fweavec-checks=` | A failed guard or index check | Guards removed by a local rule | Zero-init default | Runtime | Guarantee | @@ -44,20 +51,38 @@ C++, Objective-C, OpenCL and CUDA sources are compiled as Clang compiles them, w | `verify` | prints a report and traps | kept as monitors | on | linked | yes | | `none` | nothing is guarded | n/a | off | not linked | no | -A trap ends the program with `SIGTRAP`, or `SIGILL` on x86-64. Before it traps, the runtime unblocks both signals in the failing thread and resets a disposition the program set to ignore them, so the program ends even with the signals blocked; a handler the program installed still runs. The report is one line on stderr, two for the heap: +A trap ends the program with `SIGTRAP`, or `SIGILL` on x86-64. Before it traps, the runtime unblocks both signals in the failing thread and resets a disposition the program set to ignore them, so the program ends even with the signals blocked; a handler the program installed still runs. The report is one line on stderr, and a second line places the address relative to its object when the runtime knows it: ```text weavec: heap-buffer-overflow at vec.c:9:43: read of 4 bytes at 0xbd80000010 weavec: 0xbd80000010 is 0 bytes after the 16-byte heap object at 0xbd80000000 ``` -The kinds are `heap-buffer-overflow`, `heap-use-after-free`, `stack-buffer-overflow`, `dynamic-stack-buffer-overflow` (an `alloca` or a variable-length array), `stack-use-after-scope`, `global-buffer-overflow`, `null-dereference`, `unterminated-string` (a string argument of a library call with no terminator in its object), `overlapping-copy`, and `index-out-of-bounds` (`weavec: index-out-of-bounds at lookup.c:8:12: index -1`). An invalid `free` is stopped by the allocator, which knows no source location: `weavec: invalid release of 0x…: the block was already released` (or `not the start of its block`, `no block is allocated there`). +The first line names the kind, the source location of the access (`` when the unit has none), whether it reads or writes, its width and its address; an access with no known width reads `access at 0x…`. The kinds are: + +| Kind | The access | Second line | +| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | +| `heap-buffer-overflow` | reaches the bytes after a heap object in its slot | `… is bytes after the -byte heap object at …` | +| `heap-use-after-free` | reaches a released heap block | `… is inside a released heap block at …` | +| `stack-buffer-overflow` | reaches a redzone of a local | | +| `dynamic-stack-buffer-overflow` | reaches the redzone of an `alloca` block or a variable-length array | | +| `stack-use-after-scope` | reaches a local whose block has ended | | +| `global-buffer-overflow` | reaches the redzone of a global the unit defines | | +| `intra-object-overflow` | a fortified call writes past the size of the field or object its `_chk` form names | `… is bytes inside the -byte destination at …` | +| `use-after-poison` | reaches bytes the program poisoned through ASan's interface | | +| `null-dereference` | is through a null pointer, or reaches the lowest 64 KiB | | +| `unterminated-string` | a string argument of a library call has no terminator in its object | | +| `overlapping-copy` | the operands of `memcpy` or another call that requires disjoint operands overlap | | +| `index-out-of-bounds` | an index into an array whose bound the type declares is outside it: `weavec: index-out-of-bounds at lookup.c:8:12: index -1` | | +| `invalid-access` | is to bytes that are not addressable, of no kind the runtime can name | | + +An invalid `free` is stopped by the allocator, which knows no source location: `weavec: invalid release of 0x…: the block was already released` (or `not the start of its block`, `not the start of a live block`, `no block is allocated there`, `not a heap block`). In report mode each failing site (kind, file, line and column) prints once and the program continues, with no guarantee; an invalid release still stops it, since the allocator cannot go on from one. Verify mode is the monitor of the removal rules: every guard a local rule removed is emitted too, and its failure prints `weavec: weavec.proven: at …` before the trap, which means WeaveC removed a guard it needed. Verify mode does not version loops. ## The runtime -Every link in the `trap`, `report` and `verify` modes adds `libweavec_alloc.a` (forced in with `-u malloc`) and `libweavec_rt.a` before the first library on the line, and `-lpthread` on Linux. The first defines `malloc`, `calloc`, `realloc`, `free` and the other standard allocation functions for the image, over the runtime's arena; the second holds the shadow memory, the guards' slow paths, the checked library wrappers and the reports. Link through `weavec-cc` so that it can add them; a link by another driver fails with undefined `__weavec_rt_` symbols. On Darwin one runtime serves the process, and the C library's own allocations (`strdup`, `getline`, `asprintf`) come from its arena too. [Safety guarantees](/reference/guarantees/) states what the guards promise. +Every link in the `trap`, `report` and `verify` modes adds `libweavec_alloc.a` (forced in with `-u malloc`) and `libweavec_rt.a` before the first library on the line, and `-lpthread` on Linux. The first defines `malloc`, `calloc`, `realloc`, `free` and the other standard allocation functions for the image, over the runtime's arena; the second holds the shadow memory, the sweep, the guards' slow paths, the checked library wrappers, ASan's interface and the reports. Link through `weavec-cc` so that it can add them; a link by another driver fails with undefined `__weavec_rt_` symbols. On Darwin one runtime serves the process, and the C library's own allocations (`strdup`, `getline`, `asprintf`) come from its arena too. [Safety guarantees](/reference/guarantees/) states what the guards promise. `weavec-cc` compiles without guards and links without the runtime, as if `-fweavec-checks=none` had been given, when the command line has `-ffreestanding`, `-nostdlib`, `-nodefaultlibs` or `-nolibc`, a sanitizer that replaces the allocator (`-fsanitize=address`, `hwaddress`, `memory`, `thread`, `leak` or `kernel-address`), or a target other than 64-bit Darwin or Linux. A link prints one of: @@ -76,14 +101,25 @@ weavec-cc: note: 'alloc.o' defines the allocator, so the WeaveC runtime's is not A program's own definition of another allocation function (`posix_memalign`, `reallocarray` and the like) over `malloc` replaces the runtime's and keeps the arena. A `free` that reaches the runtime's allocator from code another compiler built is still validated. -| Environment variable | Meaning | -| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `WEAVEC_RT_QUARANTINE=` | How many bytes of released heap blocks are held before the oldest are reused. Default 16 MiB (16777216); `0` reuses blocks at once. | -| `WEAVEC_RT_ABORT=1` | In report mode, trap at the first failure as trap mode does. | -| `WEAVEC_RT_REPORT_LOG=` | Append every report line to this file instead of printing it on stderr, so a test that captures stderr sees only the program's own output. Ignored by set-user-ID and set-group-ID programs. | -| `WEAVEC_RT_STATS=1` | At exit, print the runtime's counters on stderr, one per line, as `weavec: runtime: `. On Darwin only the runtime that owns the process prints, and its counters count every image's work. | +A released block is reused only after a _sweep_ finds no pointer to it ([guarantee (T)](/reference/guarantees/#the-guarantee)). A sweep stops every other thread of the process, scans the threads' stacks and registers, the process's writable data and every live heap block for words that point into released blocks, and returns the blocks nothing points to to the free lists; blocks something still points to stay released until a later sweep. It runs when the bytes released since the last sweep exceed `WEAVEC_RT_QUARANTINE` and a quarter of the live heap (the soft budget) and the last sweep ended at least eight of its durations ago, so that sweeps take at most about a ninth of the run time; when they exceed twice `WEAVEC_RT_QUARANTINE` and half the live heap (the hard budget), at once; and when a size class runs out of space. Between sweeps the released memory waits. On Darwin it stops threads with `thread_suspend`; on Linux it signals each thread. + +| Environment variable | Meaning | +| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `WEAVEC_RT_QUARANTINE=` | The bytes of released blocks, gathered since the last sweep, that may start a sweep once they also exceed a quarter of the live heap; twice as many, and half the live heap, start one at once. Default 16 MiB (16777216). A smaller value sweeps more often and holds less memory; no value lets a block be reused without a sweep. | +| `WEAVEC_RT_SWEEP_SIGNAL=` | Linux only: the signal number the sweep stops threads with. By default `SIGPWR`, or `SIGXCPU` when the program handles `SIGPWR`. | +| `WEAVEC_RT_ABORT=1` | In report mode, trap at the first failure as trap mode does. | +| `WEAVEC_RT_BACKTRACE=1` | After the report of an access that stops the program, print its call stack on stderr. | +| `WEAVEC_RT_REPORT_LOG=` | Append every report line to this file instead of printing it on stderr, so a test that captures stderr sees only the program's own output. Ignored by set-user-ID and set-group-ID programs. | +| `WEAVEC_RT_STATS=1` | At exit, print the runtime's counters on stderr, one per line, as `weavec: runtime: `. On Darwin only the runtime that owns the process prints, and its counters count every image's work. | + +The counters of `WEAVEC_RT_STATS=1` are allocations, releases, recycled slots (released slots a sweep returned for reuse), huge blocks, slow guards (the guards whose inline check sent them to the runtime), range guards, string guards, stack unpoisons (clearings of the stack's shadow before a call that does not return) and sweeps. Two last lines give the sweeps' total and longest pause and what the last sweep had to keep, and the most the live heap's slots and the released blocks waiting for a sweep held: -The counters of `WEAVEC_RT_STATS=1` are allocations, releases, recycled slots, huge blocks, slow guards (the guards whose inline check sent them to the runtime), range guards, string guards and stack unpoisons (the clearing of the stack's shadow before a call that does not return). They are not synchronised between threads. +```text +weavec: runtime: sweeps took 48 ms (longest 7 ms); the last retained 0 blocks (0 bytes) +weavec: runtime: at most 86 MiB of live slots and 82 MiB of released ones waiting for a sweep +``` + +The counters are not synchronised between threads. ## Ledger and summary line @@ -93,7 +129,7 @@ The enforcement ledger records what the guard passes decided for each access of { "schema": "weavec-ledger", "version": 3, - "producer": { "name": "weavec-cc", "version": "0.15.0" }, + "producer": { "name": "weavec-cc", "version": "0.16.0" }, "units": [ { "source": "lookup.c", @@ -146,60 +182,8 @@ make CC=weavec-cc CFLAGS="-O2 -fweavec-ledger=build/ledger/" Every ledger is written to a temporary file and renamed into place. A link writes no ledger. -## The advisory analysis - -The analysis infers ownership, borrows, lifetimes, nullness and extents, and reports what it finds. It never changes the code `weavec-cc` generates. It runs in two places: - -- **`weavec`**, which builds nothing: a definite finding (one that holds on every path, confirmed on a feasible path) is an error and the tool exits with status 1; a possible one is a warning. -- **`weavec-cc -fweavec-diagnose`**, per unit as it compiles: every finding is a warning, so a correct program always builds. `-Werror=weavec` makes them errors. `leak` is off by default here; `-Wweavec-leak` turns it on. - -`weavec` ends each unit with a summary line, and `--whole-program` adds one for the program, named after the unit that defines `main`: - -```text -weavec: example.c: 11 sites: 6 proven, 3 not proven, 2 violations, 0 trusted; 2 errors, 1 warning -weavec: program app: 40 sites in 2 units: 31 proven, 9 not proven, 0 violations, 0 trusted; 0 errors, 0 warnings -``` - -Each memory operation (_site_) is counted once, by its worst safety facet: a violation, then not proven, trusted (it rests on a named assumption, such as a `WEAVEC_UNSAFE` region or a platform function), proven. Functions over budget are appended (`; 1 function over budget ()`). The counts describe the analysis, not the build: a `weavec-cc` build guards the accesses the analysis proved as well as the others. - -## weavec - -| Option | Meaning | -| ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | -| `--whole-program` | Analyse the given sources, or every source of the compilation database, as one program, so calls into other files use their definitions. | -| `-p ` | Read `compile_commands.json` from `` or a parent directory. | -| `--budget=` | As `-fweavec-budget`. | -| `--no-zero-init` | Model a build with `-fno-weavec-zero-init`. | -| `--dump-analysis` | Print the inferred places, lifetimes, exit states and summaries of every analysed function (unstable format). | -| `--dump-kinds` | Print each unit's pointer kinds, must-access requirements, store groups, field candidates and function-pointer slots instead of analysing (unstable). | -| `--analysis-stats=` | Write analysis work statistics as JSON. | -| `--extra-arg`, `--extra-arg-before` | Add a compiler argument to every command. | -| `-W…weavec…` | The [diagnostic controls](#diagnostic-controls), before `--`. | - -A compile command with `-fdiagnostics-format=sarif` is an error in `weavec`. - -## Budgets - -Each analysis run counts its _work_: over its block transfers and joins, the size of each state it transfers or joins (its symbols and objects), so a run over large states costs what it costs ([RFC 0034](/rfcs/0034-fast-enforcement/) §7). A function over the budget stops early: the facets it has not decided are not proven, callers apply the unknown-callee effects to its arguments, and the summary line names it. The count is deterministic and independent of timing. - -The default is 20,000,000 per run (Lua's `luaV_execute` takes about 10,000,000). A run is also over budget when the states it keeps at its blocks' entries hold more than 1,000,000 symbols and objects together, or when its function's graph has more than 100,000 blocks, which bounds the analysis's memory. - -A second budget bounds a whole unit, which runs its functions many times (summary rounds, alias contexts, the final run): `-fweavec-unit-budget=` work over every run, by default 400 per site of the unit and at least 20,000,000. Each run may take at most eight times its fair share of what is left, and at least 100,000, so a few large functions cannot spend what the others need. A function cut short this way is over budget like any other. `0` means unlimited. `weavec` uses the default unit budget. - -## Diagnostic controls - -These apply to `weavec` (before `--`) and to `weavec-cc -fweavec-diagnose`. - -| Flag | Behaviour | -| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `-Wno-weavec-` | Disable the warnings of ``: always-warning ids and the possible (warning) findings of temporal ids. | -| `-Wweavec-` | Enable the warnings of ``. `allocation-failure` is off by default, and so is `leak` under `-fweavec-diagnose` (not in `weavec`); `-Wweavec-`, `-Werror=weavec-` or `-Wweavec` enables them. | -| `-Werror=weavec[-]` | Promote WeaveC warnings, or those of one id, to errors. | -| `-Wno-error=weavec[-]` | Lower the errors of ``, or of every id, to warnings. | -| `-Wweavec`, `-Wno-weavec` | Enable or disable every WeaveC warning. | - -Errors cannot be disabled, only lowered. A flag naming a removed identifier is an error. See the [diagnostic reference](/reference/diagnostics/) for the identifiers and [diagnostic controls](/reference/diagnostic-controls/) for details. Run-time reports are not diagnostics: these flags do not affect them. - ## Removed options -RFC 0035 removed these, with no aliases (each is now an unknown-flag error): `-fno-weavec-runtime` and `-fweavec-runtime` (use `-fweavec-checks=none`); `-f[no-]weavec-stack-objects` and `-f[no-]weavec-global-objects`; `-fweavec-require=` and `WEAVEC_REQUIRE_SAFE` with the require levels; `-fweavec-link=`, `-fweavec-link-budget=` and the `.weavec` unit records; `-fweavec-ledger-format=` (SARIF); `-fweavec-print-prelude`; and, in `weavec`, `--ledger`, `--ledger-format`, `--require`, `--no-runtime` and `--dump-record`. Earlier RFCs removed the checked-mode options of RFCs 0018–0029, `-fweavec-exclusive-borrows`, `-fweavec-strict`, `-fweavec-analysis-cache=`, `-fweavec-analyze-headers` and `-fweavec-report-unannotated`, with their `weavec` spellings. +RFC 0036 deleted the advisory ownership and lifetime analysis, with no aliases. The `weavec` tool is gone with all of its options (`--whole-program`, `--dump-analysis`, `--dump-kinds`, `--budget=` and the rest), and so are these `weavec-cc` flags, each now an `unknown WeaveC flag` error: `-fweavec-diagnose` and `-fno-weavec-diagnose`, `-fweavec-budget=`, `-fweavec-unit-budget=`, `-fweavec-dump-analysis` and `-fweavec-analysis-stats=`. The diagnostic controls (`-Wweavec`, `-Wweavec-`, `-Wno-weavec-`, `-Werror=weavec[-]`, `-Wno-error=weavec[-]`) are no longer WeaveC's: they reach Clang, which warns that each is an unknown warning option (`-Wunknown-warning-option`, an error under `-Werror`). `weavec.h` keeps only `WEAVEC_UNSAFE`; a program that uses one of its other annotation macros no longer compiles until it stops using it. + +RFC 0035 removed these, also with no aliases (each is an unknown-flag error): `-fno-weavec-runtime` and `-fweavec-runtime` (use `-fweavec-checks=none`); `-f[no-]weavec-stack-objects` and `-f[no-]weavec-global-objects`; `-fweavec-require=` and `WEAVEC_REQUIRE_SAFE` with the require levels; `-fweavec-link=`, `-fweavec-link-budget=` and the `.weavec` unit records; `-fweavec-ledger-format=` (SARIF); and `-fweavec-print-prelude`. Earlier RFCs removed the checked-mode options of RFCs 0018–0029, `-fweavec-exclusive-borrows`, `-fweavec-strict`, `-fweavec-analysis-cache=`, `-fweavec-analyze-headers` and `-fweavec-report-unannotated`. diff --git a/docs/pages/reference/compatibility.md b/docs/pages/reference/compatibility.md index 03c264ba..37b542be 100644 --- a/docs/pages/reference/compatibility.md +++ b/docs/pages/reference/compatibility.md @@ -1,19 +1,15 @@ --- title: Compatibility -description: Supported toolchains and platforms, what weavec-cc changes in compiled code (the allocator, redzones, zero-initialised locals, the shadow reservation, traps on deliberate over-reads), sanitizers, external code, and version changes. +description: Supported toolchains and platforms, what weavec-cc changes in compiled code (the allocator and its sweep, frames and redzones, fortification, AddressSanitizer's macros, zero-initialised locals, the shadow reservation, traps on deliberate over-reads), sanitizers, external code, and version changes. --- ## C and Clang WeaveC uses Clang for C parsing, semantic analysis, target layout, optimisation and code generation. Pass the language standard and target flags used by your project. `weavec-cc` guards the C units it compiles; C++, Objective-C, OpenCL and CUDA sources are compiled as Clang compiles them, without guards, and link with the guarded C code as usual. -The advisory analysis (`weavec`, `weavec-cc -fweavec-diagnose`) reads C. Clang accepting a construct does not mean the analysis can prove anything about it; what it cannot decide counts as not proven and never changes the generated code. Use [checker coverage](/reference/checker-coverage/) to see what the analysis understands. +## weavec.h -## Annotations remain portable - -Annotations live in `weavec.h`. Under Clang they use the `annotate` attribute; under compilers without that attribute they expand away. Check [annotation placement](/reference/annotation-placement/) for syntax and [compatibility with other annotation schemes](/reference/annotation-compatibility/) for current integration boundaries. The analysis also reads Clang's own `counted_by`, `sized_by`, `alloc_size`, `nonnull` and ownership attributes. - -`WEAVEC_UNSAFE` is the only annotation that changes the generated code: the accesses in its function or block are not guarded and its array indexes are not checked. The others are read by the advisory analysis only. `WEAVEC_ASSUME(expr)` is a statement to the analysis; it is not checked at run time. Under WeaveC its expression is evaluated and discarded, and under other compilers it is not evaluated at all, so it must be free of side effects. +`weavec.h` (version 1.0, `WEAVEC_H_VERSION_MAJOR` and `WEAVEC_H_VERSION_MINOR`) provides one macro, `WEAVEC_UNSAFE`, which marks a function or a block whose accesses are not guarded and whose array indexes are not checked. Under Clang it uses the `annotate` attribute; under compilers without that attribute it expands to nothing, so the code stays buildable with any C toolchain. `weavec-cc` puts the header's directory on the system include path; a project that also builds with other compilers can copy the header into its tree. A function marked `no_sanitize("address")` is an unsafe region too, without the header. ## What changes in compiled code @@ -21,12 +17,18 @@ With the default `-fweavec-checks=trap`, `weavec-cc` compiles each C unit as Cla A guarded compile also turns on some of Clang's own options: `-fsanitize=array-bounds` for typed array indexes, reported through WeaveC's runtime (unless the command line already asks for a sanitizer); source locations for the reports, without emitting debug information when there is no `-g`; lifetime markers at every optimisation level, so that a use after scope is caught at `-O0` too; and, on AArch64, no conditional compares in the backend. Object code is larger and compiles take longer than with Clang alone. +### Preprocessor macros and fortification + +In the enforcing modes a C unit sees what it would see under AddressSanitizer: `__SANITIZE_ADDRESS__` is defined and `__has_feature(address_sanitizer)` is true, so a project's ASan-specific code (poisoning in a pool allocator, a scan kept from ASan) is compiled in, and the runtime implements ASan's poisoning interface for it. `__WEAVEC__` is defined whenever WeaveC is on. `-fno-weavec-asan-compat` leaves the ASan macros undefined. See [AddressSanitizer compatibility](/reference/cli/#addresssanitizer-compatibility). + +On glibc targets an optimising compile whose command line does not set `_FORTIFY_SOURCE` gets `-D_FORTIFY_SOURCE=3`, so string and `printf`-family calls carry the size of the object or field they write, which the guards check. Darwin's SDK fortifies optimising compiles at level 2, and keeps doing so under WeaveC although Clang turns it off under ASan. + ### The allocator With the runtime, the image's `malloc`, `calloc`, `realloc`, `free`, `reallocarray`, `aligned_alloc`, `posix_memalign`, `valloc`, `free_sized`, `free_aligned_sized` and the platform's `memalign`, `pvalloc`, `malloc_usable_size`, `malloc_size`, `malloc_good_size` and `reallocf` are WeaveC's. What a program can observe: - Every block is zero-filled, and its usable size is its requested size: `malloc_usable_size` and `malloc_size` return what was asked for, and the bytes after it are not addressable. `malloc(0)` and `realloc(p, 0)` return a block of zero bytes; `realloc(p, 0)` does not free `p` and return null. -- A freed block is not reused until it leaves the quarantine (16 MiB by default; `WEAVEC_RT_QUARANTINE=`), so a program that frees and allocates heavily can hold more memory than the same program on the system allocator. +- A freed block is not reused until a _sweep_ finds no pointer to it in the program's stacks, registers, writable data and live heap. A sweep is due when the bytes released since the last one exceed 16 MiB (`WEAVEC_RT_QUARANTINE=`) and a quarter of the live heap, and runs once the last sweep ended at least eight of its durations ago (at once past twice that budget and half the live heap). It stops every thread of the process for a time that grows with the live heap; on Linux it does so with a signal, `SIGPWR` by default (`WEAVEC_RT_SWEEP_SIGNAL=` names another). A program that frees and allocates heavily can hold more memory than on the system allocator, and a block that a stale pointer still points to stays allocated as long as the pointer does. - An invalid `free` or `realloc` (a double free, an interior, stack or global pointer) stops the program, in whichever object of the image it happens. - On ELF targets the executable's allocator serves every shared library in the process, the C library included. On Darwin one runtime serves the process (the copy the dynamic loader finds first; the others forward to it), and its malloc zone becomes the default zone, so the system libraries' own allocations (`strdup`, `getline`, `asprintf`) come from it too. Blocks the system allocated before that are handed back to the zone that owns them. - Speed, fragmentation and address layout are those of WeaveC's allocator, not the system's. Programs that depend on the system allocator's identity (a `malloc_zone_t` of their own, allocator introspection, `mallopt`) see WeaveC's allocator. @@ -35,9 +37,9 @@ A program that defines `malloc`, `calloc`, `realloc` or `free` itself keeps its ### Frames and globals -Each local that a guard can reach, `alloca` blocks and variable-length arrays included, is followed by a redzone of at least 32 bytes (more for large objects) and aligned to 32 bytes, so stack frames are larger: deeply recursive code may need a larger stack. Locals proven to need no tracking keep Clang's layout. +Each local that a guard can reach or whose address escapes is laid out in one frame with redzones between the locals, and `alloca` blocks and variable-length arrays get a redzone after them; in a frame larger than 512 bytes, locals whose lifetimes never overlap (the arms of a `switch`) share space, as in Clang's own frames, so the frame grows by the redzones rather than by the locals. Deep recursion through such locals reaches a lower depth than with Clang, as it does under AddressSanitizer. Before a call that does not return (`longjmp`, `exit`'s kin aside), the stack's shadow below the caller is cleared, so `setjmp` and `longjmp` work as usual. -Each global or static variable a unit defines is followed by a redzone and aligned to 32 bytes, so the data sections grow and their layout changes. `sizeof` and the variable's own bytes are unchanged, but code that relies on two globals being adjacent (walking from one into the next) traps. Thread-local variables, variables in a named section, string literals, common symbols and a few other kinds are left as they are, so linker sets placed in a named section keep working. +Each global or static variable a unit defines is followed by a redzone and aligned to 32 bytes, so the data sections grow and their layout changes. `sizeof` and the variable's own bytes are unchanged, but code that relies on two globals being adjacent (walking from one into the next) traps. Thread-local variables, variables in a named section, string literals, common symbols and a few other kinds are left as they are, so linker sets placed in a named section keep working. The globals of shared libraries and bundles built by `weavec-cc` are protected as the executable's are. ### Zero-initialisation @@ -49,15 +51,15 @@ At start-up the runtime reserves address space, without committing memory: the s ### Code that can trap -A program that reads past an object on purpose traps, as it does under AddressSanitizer: a word-at-a-time string scan, a hash that reads whole words of a key, a conservative collector's stack scan, or code that uses the slack after a heap block's requested size. So does an index past an inner bound of a multidimensional array (`int m[4][4]; m[0][k]` with `k ≥ 4`). Keep such code in a narrow `WEAVEC_UNSAFE` region; a function already marked `no_sanitize("address")` (or `no_sanitize_address`, `disable_sanitizer_instrumentation`) is left unguarded without a change. The [guarantees reference](/reference/guarantees/#false-traps) lists what is accepted. +A program that reads past an object on purpose traps, as it does under AddressSanitizer: a word-at-a-time string scan, a hash that reads whole words of a key, or code that uses the slack after a heap block's requested size. So does an index past an inner bound of a multidimensional array (`int m[4][4]; m[0][k]` with `k ≥ 4`). Code that keeps such reads from ASan behind `__SANITIZE_ADDRESS__` or `__has_feature(address_sanitizer)`, or in a function marked `no_sanitize("address")` (or `no_sanitize_address`, `disable_sanitizer_instrumentation`), needs no change; other code needs a narrow `WEAVEC_UNSAFE` region. The [guarantees reference](/reference/guarantees/#false-traps) lists what is accepted. ### Run-time cost -A guarded program runs slower than a plain Clang build and uses more memory. [RFC 0035's acceptance gates](/rfcs/0035-guard-by-default/#acceptance-gates) set the targets: each workload at most twice the reference compiler's run time, or AddressSanitizer's ratio where that is larger, 1.8 times on average, and a build within 1.5 times the reference compiler's CPU time. These are targets; the RFC records the measurements when its gates are run. See [troubleshooting](/reference/troubleshooting/#the-program-is-slower-or-uses-more-memory) for what to do when a program is too slow. +A guarded program runs slower than a plain Clang build and uses more memory. [RFC 0036's acceptance gates](/rfcs/0036-deterministic-heap-reuse/#acceptance-gates) set the targets against the previous release (v0.16.0): the corpus benchmarks' geometric mean at most 1.1 times its run time and no benchmark above 1.25 times; peak memory at most 1.5 times its, or 64 MiB more; the longest sweep pause at most 150 ms with 256 MiB of live heap; and a build at most 1.1 times as long as its. The RFC's measured gates record the results. See [troubleshooting](/reference/troubleshooting/#the-program-is-slower-or-uses-more-memory) for what to do when a program is too slow or holds too much memory. ### Sanitizers -The runtime is not used with a sanitizer that replaces the allocator (`-fsanitize=address`, `hwaddress`, `memory`, `thread`, `leak`, `kernel-address`), with `-ffreestanding`, `-nostdlib`, `-nodefaultlibs` or `-nolibc`, or on targets other than 64-bit Darwin and Linux. `weavec-cc` then compiles every unit without guards, as with `-fweavec-checks=none`, and the link prints `weavec-cc: note: building without the WeaveC runtime (): memory accesses are not guarded`. Other sanitizers, such as `-fsanitize=undefined`, work alongside the guards; a program that links UBSan's runtime reports array-bounds failures through UBSan's handlers rather than WeaveC's. +The runtime is not used with a sanitizer that replaces the allocator (`-fsanitize=address`, `hwaddress`, `memory`, `thread`, `leak`, `kernel-address`), with `-ffreestanding`, `-nostdlib`, `-nodefaultlibs` or `-nolibc`, or on targets other than 64-bit Darwin and Linux. `weavec-cc` then compiles every unit without guards, as with `-fweavec-checks=none`, and the link prints `weavec-cc: note: building without the WeaveC runtime (): memory accesses are not guarded`. Other sanitizers, such as `-fsanitize=undefined`, work alongside the guards; a program that links UBSan's runtime reports array-bounds failures through UBSan's handlers rather than WeaveC's. LeakSanitizer's interface is accepted and does nothing: WeaveC does not check for leaks. ## LLVM versions and platforms @@ -69,12 +71,17 @@ Current releases are source archives. Portable binary packages and package-manag ## External code and object files -Only code that `weavec-cc` compiles is guarded. Code built by another compiler, precompiled static and shared libraries, and assembly make their accesses unchecked; their stack frames and globals are untracked, and an access into them from guarded code passes. Their heap allocations still come from WeaveC's allocator when the program links the runtime, so a guarded access to a block they allocated is checked against its size and a release of it is validated. Link inputs need no WeaveC metadata: objects from any compiler link as they are. +Only code that `weavec-cc` compiles is guarded. Code built by another compiler, precompiled static and shared libraries, and assembly make their accesses unchecked; their stack frames and globals are untracked, and an access into them from guarded code passes. Their heap allocations still come from WeaveC's allocator when the program links the runtime, so a guarded access to a block they allocated is checked, a release of it is validated, and the sweep scans their memory for pointers like the rest of the program's. Link inputs need no WeaveC metadata: objects from any compiler link as they are. The library table (`lib/Core/LibrarySpec.txt`) describes the C library, POSIX and platform functions WeaveC models: allocation, release, string, stream and runtime operations. A call to one of them has its memory arguments guarded over the bytes the row says it accesses, or is replaced by a checked wrapper in the runtime. Each row states what every supported C library (Darwin's and glibc) accepts, not the strictest reading of a standard: where one of them documents accepting a null pointer (`gettimeofday(NULL, &tz)`, `setgroups(0, NULL)`), the row allows it. A call to a function outside the table is not checked at the call; its own accesses are guarded if WeaveC built it. Wide-character string arguments are not scanned. -The advisory analysis sees only the source it is given. Functions it cannot see are treated conservatively as possibly freeing or keeping their pointer arguments; analyse the program's files together with `weavec --whole-program` to see across them. - ## Version changes -WeaveC is in early 0.x development. Flags, diagnostics, run-time reports and the ledger schema can change between minor versions. [RFC 0035](/rfcs/0035-guard-by-default/) replaced the earlier enforcement: the enforcement ledger is now `weavec-ledger` version 3, `weavec.h` is at version 0.10, objects carry no WeaveC records, and objects compiled by an earlier version call runtime entry points this version no longer has, so rebuild them. Its removed options are errors rather than aliases; see [removed options](/reference/cli/#removed-options). Check the [release notes](/project/releases/) before upgrading. +WeaveC is in early 0.x development. Flags, run-time reports and the ledger schema can change between minor versions. [RFC 0036](/rfcs/0036-deterministic-heap-reuse/) changed the model of [RFC 0035](/rfcs/0035-guard-by-default/): released memory is reused only after a sweep, fortified calls are checked against their object size, code is compiled as AddressSanitizer would compile it, and locals whose lifetimes never overlap share frame space. With it: + +- The advisory ownership and lifetime analysis is gone: the `weavec` tool, `weavec-cc -fweavec-diagnose`, whole-program analysis, every diagnostic and its identifier. WeaveC reports nothing before the program runs; its guarantees are those of the guarded program. +- `weavec.h` is version 1.0 and provides only `WEAVEC_UNSAFE`. A program that uses one of the other annotation macros fails to compile, with `weavec-cc` and other compilers alike, until it stops using it. +- The runtime's entry points and the table its copies share on Darwin changed, so objects compiled by an earlier `weavec-cc` must be rebuilt: they call entry points this runtime no longer has, and on Darwin a shared image built by an earlier version keeps a runtime, heap and shadow of its own. +- The report kind `buffer-overflow` is no longer reported (a partial granule the program poisoned is `use-after-poison`); `intra-object-overflow` and `use-after-poison` are new. + +The removed `-fweavec-*` options are errors rather than aliases; see [removed options](/reference/cli/#removed-options). Check the [release notes](/project/releases/) before upgrading. diff --git a/docs/pages/reference/guarantees.md b/docs/pages/reference/guarantees.md index 49352548..580d89f1 100644 --- a/docs/pages/reference/guarantees.md +++ b/docs/pages/reference/guarantees.md @@ -1,17 +1,20 @@ --- title: Safety guarantees and scope -description: What a translation unit compiled by weavec-cc is guaranteed at run time, which bytes the runtime knows are not addressable, the assumptions behind it, what is and is not caught, and what the advisory analysis adds. +description: What a program compiled by weavec-cc is guaranteed at run time (accesses, bounds, releases, reuse only after a sweep, fortified sizes, poisoning), which memory the runtime tracks, the assumptions behind it, and what is and is not caught. --- -`weavec-cc` guards every memory access of the C code it compiles and removes a guard only where a simple local rule proves it redundant. The guarantee below is a property of the compiled program and its runtime; nothing the ownership analysis concludes is part of it. It is specified by [RFC 0035, _Soundness_](/rfcs/0035-guard-by-default/#soundness), with its _Implementation amendments_, which are authoritative; this page restates them. +`weavec-cc` guards every memory access of the C code it compiles and removes a guard only where a simple local rule proves it redundant. The guarantee below is a property of the compiled program and its runtime. It is specified by [RFC 0036, _Soundness_](/rfcs/0036-deterministic-heap-reuse/#soundness), which keeps (A), (B) and (R) of [RFC 0035](/rfcs/0035-guard-by-default/#soundness) and adds (T), (F) and (P); the RFCs' _Implementation amendments_ are authoritative, and this page restates them. ## The guarantee -For a translation unit compiled by `weavec-cc` in an enforcing mode (the default `-fweavec-checks=trap`, or `verify`; `report` only with `WEAVEC_RT_ABORT=1`), with the runtime linked, every memory access the unit's code makes outside a `WEAVEC_UNSAFE` region satisfies: +For a program whose every unit is compiled by `weavec-cc` in an enforcing mode (the default `-fweavec-checks=trap`, or `verify`; `report` only with `WEAVEC_RT_ABORT=1`), with the runtime linked, every memory access outside a `WEAVEC_UNSAFE` region satisfies: - **(A) Accesses.** A load, store, atomic operation or memory intrinsic (`memcpy`, `memmove`, `memset`) of _w_ bytes at address _a_ executes only if every byte of [_a_, _a_ + _w_) is _addressable_ when it runs; otherwise the program traps before the access. The same holds for each memory argument of a call to a function in the [library table](https://github.com/weavefoundry/weavec/blob/main/lib/Core/LibrarySpec.txt), over the bytes the table says the call accesses, computed from the arguments, or, for a string, up to its terminator. - **(B) Bounds.** An index `a[i]` into an array whose bound its type declares traps unless 0 ≤ `i` < _n_, or `i` ≤ _n_ where only the address is taken. A trailing array of a struct, which may be flexible, is excluded. - **(R) Releases.** A release through the runtime's allocator (`free`, `realloc` and the rest of the family) releases null or the start of a live heap object; anything else stops the program. +- **(T) Temporal.** A byte of a released heap block is not addressable until a _sweep_ has found no pointer to the block in the program's stacks, registers, writable data and live heap. An access through a pointer the program held when the block was released therefore traps, however much it allocated since, and so does a second release of the block. +- **(F) Fortified sizes.** A fortified library call (`__strcpy_chk(d, s, 8)`, and the other `_chk` forms) writes no more than the size its `__builtin_object_size` argument states, or the program traps (`intra-object-overflow`). +- **(P) Poisoning.** A byte the program poisoned through AddressSanitizer's interface is not addressable until the program unpoisons it. A function excluded from AddressSanitizer (`__attribute__((no_sanitize("address")))`, `no_sanitize_address`, `disable_sanitizer_instrumentation`) is treated as an unsafe region. Code inlined from an unsafe region stays unguarded wherever it lands. @@ -19,52 +22,60 @@ A function excluded from AddressSanitizer (`__attribute__((no_sanitize("address" A byte is addressable unless the runtime knows it is not: -| Bytes | Not addressable | -| ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | -| A heap object's slot in the runtime's allocator | the bytes after the object's requested size; the whole slot while the object is released and quarantined | -| A local whose address a guard can reach (a _tracked_ local) | its redzones, always; the local itself before its scope begins and after it ends | -| A global the unit defines (a _tracked_ global) | its redzone | -| Low memory | the lowest 64 KiB | +| Bytes | Not addressable | +| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | +| A heap object's slot in the runtime's allocator | the bytes after the object's requested size; the whole slot once the object is released, until a sweep finds no pointer to it | +| A tracked local (in its function's frame) | its redzones, always; the local before its scope begins and after it ends | +| An `alloca` block or a variable-length array | its redzone | +| A global the unit defines (a _tracked_ global) | its redzone | +| Bytes the program poisoned through ASan's interface | until the program unpoisons them | +| Low memory | the lowest 64 KiB | -Every other byte is addressable: memory the runtime does not track (other allocators, `mmap`, string literals, locals no guard can reach, frames and globals of code built without WeaveC) and the bytes of live tracked objects. +Every other byte is addressable: memory the runtime does not track (other allocators, `mmap`, string literals, locals no guard can reach, frames and globals of code built without WeaveC), the bytes of live tracked objects, and a local's bytes after its function returned. -The runtime keeps one _shadow_ byte for every 16 bytes, saying how many of them are addressable or why none are. Its allocator puts every object in a slot of the smallest size class strictly larger than the object, so at least one byte after every heap object belongs to no object, and holds released blocks in a quarantine (16 MiB by default, `WEAVEC_RT_QUARANTINE=`) before it reuses them. `weavec-cc` lays out the tracked locals of a function in one frame, each at a 32-byte boundary followed by a redzone of at least 32 bytes (more for large objects: an eighth of the object, up to 4 KiB), after a 64-byte left zone, and gives each tracked global a redzone of the same size. +The runtime keeps one _shadow_ byte for every 16 bytes, saying how many of them are addressable or why none are. Its allocator puts every object in a slot of the smallest size class strictly larger than the object, so at least one byte after every heap object belongs to no object. A released block joins a _quarantine_ and is never reused from there: only a sweep, which stops the program's threads and scans its memory conservatively for pointers, moves the blocks it found no pointer to back to the free lists. + +The locals a guard can reach (the _tracked_ locals: an access to them is guarded, or their address escapes) are laid out in one frame with a 64-byte redzone before the first and at least 32 bytes after each; in a frame that would be larger than 512 bytes, locals whose lifetimes never overlap (the arms of a `switch`) may share bytes, as Clang's own frames share them. Each `alloca` block and variable-length array gets a redzone after it. Each tracked global gets a redzone after it and a 32-byte alignment. ## What is caught -- **Heap overflow.** An access that leaves a heap block forwards traps at its first byte past the object: `heap-buffer-overflow`, with a second line that places the address relative to its block. -- **Heap use after free and double free.** An access to a released block traps while the block is in the quarantine (`heap-use-after-free`); a second `free`, a `free` of an interior, stack or global pointer, and a `realloc` of a released block stop the program (`weavec: invalid release of …`). The allocator validates every release in the image, from WeaveC's code or any other. -- **Stack and global overflow.** An access that leaves a tracked local or global into its redzone traps (`stack-buffer-overflow`, `global-buffer-overflow`); so does one past an `alloca` block or a variable-length array (`dynamic-stack-buffer-overflow`). -- **Use after scope.** An access to a local after its block has ended traps (`stack-use-after-scope`), at every optimisation level. -- **Index overflow inside an object.** By (B), `s.buf[i]` with `i` past `buf`'s declared bound traps (`index-out-of-bounds`) even when the bytes it would touch belong to the next field. +- **Heap overflow** that touches a byte past the block (`heap-buffer-overflow`), with a second line that places the address relative to the block. An access that jumps over the slot's tail into another live block lands on that block's bytes and passes. +- **Heap use after free and double free.** An access to a released block through any pointer the program still holds traps (`heap-use-after-free`), however much it allocated since, because the block is not reused while a pointer to it exists. A second `free`, a `free` of an interior, stack or global pointer, and a `realloc` of a released block stop the program (`weavec: invalid release of …`). The allocator validates every release in the image, from WeaveC's code or any other. +- **Stack and global overflow** into a redzone: an access that leaves a tracked local (`stack-buffer-overflow`), an `alloca` block or a variable-length array (`dynamic-stack-buffer-overflow`), or a tracked global (`global-buffer-overflow`) and touches its redzone. +- **Use after scope.** An access to a local after its block has ended traps (`stack-use-after-scope`), at every optimisation level, unless it lands in bytes that a local whose scope began later now shares (only in a frame larger than 512 bytes). +- **Overflow inside an object**, by an index into a declared array (B): `s.buf[i]` with `i` past `buf`'s declared bound traps (`index-out-of-bounds`) even when the bytes it would touch belong to the next field; and by a fortified string or `printf`-family call into a field (F): `strcpy(u->name, s)` into an 8-byte field that the C library's headers fortify traps (`intra-object-overflow`). - **Null dereference.** An access at a small offset from null faults on the null page, or traps on the shadow of the lowest 64 KiB; one at a variable or large offset from a pointer not known to be non-null tests the pointer first (`null-dereference`). -- **Library calls.** A call in the library table whose memory arguments would be read or written outside their objects traps before the call: `memcpy(dst, src, n)` with `n` past either object, a `%s` argument of a `printf`-family call with a literal format that has no terminator in its object (`unterminated-string`), fortified forms (`__builtin___memcpy_chk`) as their plain forms. `strcpy`, `stpcpy`, `strcat`, `sprintf`, `vsprintf`, `snprintf` and `vsnprintf` go through the runtime's checked versions, which compute what the call writes before it writes and check the `printf`-family `%s` and `%n` arguments, through a `va_list` too; `gets` through one that checks each byte before it writes it; `memchr`, `strchr`, `strcmp`, `strncmp`, `strcasecmp` and `strncasecmp` through versions that check the bytes the call read up to where it stopped; the `scanf` family through versions that check, after the call, what each conversion that ran wrote. Overlapping operands of `memcpy` and of the other calls whose table row says they must not overlap trap as `overlapping-copy`. +- **Library calls.** A call in the library table whose memory arguments would be read or written outside their objects traps before the call: `memcpy(dst, src, n)` with `n` past either object, a `%s` argument of a `printf`-family call with a literal format that has no terminator in its object (`unterminated-string`), fortified forms (`__builtin___memcpy_chk`) as their plain forms. `strcpy`, `stpcpy`, `strcat`, `sprintf`, `vsprintf`, `snprintf` and `vsnprintf` go through the runtime's checked versions, which compute what the call writes before it writes and check the `printf`-family `%s` and `%n` arguments, through a `va_list` too; `gets` through one that checks each byte before it writes it; `memchr`, `strchr`, `strcmp`, `strncmp`, `strcasecmp` and `strncasecmp` through versions that check the bytes the call read up to where it stopped; the `scanf` family through versions that check, after the call, what each conversion that ran wrote; the `strto*` and `ato*` number parsers through versions that check the bytes the parse read. Overlapping operands of `memcpy` and of the other calls whose table row says they must not overlap trap as `overlapping-copy`. +- **Poisoned memory.** Bytes a program poisons through AddressSanitizer's interface (`ASAN_POISON_MEMORY_REGION`, as a pool allocator does for its free space) trap when a guarded access reaches them (`use-after-poison`). A program that sends its objects to `malloc` when it sees `__SANITIZE_ADDRESS__` (QuickJS does) gets its objects tracked one by one. On Darwin the runtime's allocator also serves the C library's own allocations (`strdup`, `getline`, `asprintf`), so their blocks are tracked too. ## What is not caught -- **An access that jumps over a redzone.** An access that skips past a heap slot's tail into another live object, reaches backwards into the previous slot's object, or jumps over a local's or a global's redzone into another object, passes: only the bytes accessed are looked at, not the object the pointer came from. -- **A use after free after the quarantine.** Once a released block's storage is reused, a stale pointer sees the new object. -- **Use after return.** A frame's shadow is cleared when its function returns, because frames of code built without WeaveC reuse the stack. -- **Overflow inside an object by pointer arithmetic or a library call.** A cursor walked from one field into the next, or a `strcpy` into a field bounded only by its type, stays inside the object; only indexes into declared arrays are checked against the type (B). -- **Untracked memory.** Memory from another allocator, `mmap`, string literals, and objects of code built without WeaveC have no redzones and no shadow; an access into them passes (A1). A write into a string literal passes its guard (the hardware usually faults on it). +- **A jump over a redzone.** An index or pointer arithmetic that skips past a redzone or a slot's tail into another live object touches only addressable bytes and passes, as under AddressSanitizer. Bounds from each pointer's provenance are left to a later RFC (RFC 0036 measured a first design at twice the run time). +- **Use after return.** A local's bytes are addressable again once its function returns; an access through a pointer to it, or to a local of a frame `longjmp` abandoned, passes. Left to a later RFC. +- **A pointer jump inside one object by arithmetic.** A cursor walked from one field of a struct into the next stays inside its object; only indexes into declared arrays (B) and fortified calls (F) are checked against a field. +- **Hidden pointers.** A pointer the program stores only in a form the sweep does not recognise (XOR-linked, transformed, written to a file, held only by the kernel) does not keep its block from reuse (A6); a use of the block through it after a sweep reuses the block is not caught. +- **The program's own poisoning of a local or a global at a constant offset.** An access to a local or a global at a constant offset inside it needs no guard, so it is not checked against bytes the program poisoned there; accesses at a computed offset are. +- **Untracked memory.** Memory from another allocator (unless the program poisons it through ASan's interface), `mmap`, string literals, and objects of code built without WeaveC have no redzones; an access into them passes (A1). A write into a string literal passes its guard (the hardware usually faults on it). - **Wide-character strings** passed to library functions (`wcslen`, `wcscpy`) are not scanned for their terminator; their counted arguments are guarded. - **Functions outside the library table** are not checked at the call; their own accesses are guarded only if `weavec-cc` built them (A2). - **Uninitialised pointers** in memory other than locals and the runtime's heap (which are zero) point anywhere; accesses through them are guarded, and one into untracked memory passes. - **Data races.** A release by another thread between a guard and its access is not caught (A3). - **Anything in a `WEAVEC_UNSAFE` region** or an AddressSanitizer-excluded function, and **any code not compiled by `weavec-cc`**: other compilers, precompiled libraries, C++ and Objective-C sources. -- **Leaks**, integer overflow as such, type confusion within an object, and inline assembly. Leaks are reported only by the [advisory analysis](#the-advisory-analysis). +- **Leaks**, integer overflow as such, type confusion within an object, and inline assembly. WeaveC reports nothing before the program runs; a static analyser (such as the Clang static analyzer) finds leaks. ## Assumptions -| | Assumption | -| ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **A1** | An access to bytes the runtime does not track (memory from another allocator, `mmap`, string literals, code built without WeaveC) is to a live object it stays inside. | -| **A2** | A function in the library table accesses only what its row says. A function outside it is not checked at the call. | -| **A3** | No other thread releases memory between a guard and its access: the guard and the access are not atomic together. | -| **A4** | The program does not write the runtime's shadow or metadata except through guarded accesses. | -| **A5** | A frame left by a non-local jump from code without WeaveC (an exception unwinding through C frames) may keep its redzones until a WeaveC frame next uses that stack, and a guard that meets one can trap. A frame left by `longjmp`, `siglongjmp`, `pthread_exit` or another `noreturn` call from WeaveC-built code has its redzones cleared first. | +| | Assumption | +| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **A1** | An access to bytes the runtime does not track (memory from another allocator that the program does not poison through ASan's interface, `mmap`, string literals, code built without WeaveC) is to a live object it stays inside. | +| **A2** | A function in the library table accesses only what its row says. A function outside it is not checked at the call. | +| **A3** | No other thread releases memory between a guard and its access: the guard and the access are not atomic together. | +| **A4** | The program does not write the runtime's shadow or metadata except through guarded accesses. | +| **A5** | A frame left by `longjmp`, `pthread_exit` or another call that does not return leaves no poison behind: the stack's shadow below the caller is cleared before the call. A frame left by an exception unwinding through C frames keeps its poison until the stack is reused. | +| **A6** | A pointer to a heap block is kept, while the program may still use it, as a whole aligned pointer in memory the sweep scans (any readable and writable mapping) or in a register. | +| **A7** | On Linux every thread accepts the sweep's signal within a second. A thread that blocks it holds up the reuse of released memory (nothing is reused until it accepts), never safety. | No guard is removed because of what another function does, so nothing is assumed about callers, callees or the rest of the heap. The rules that remove guards are local to a function (an access inside a local or global at a known offset, an access a dominating guard on the same pointer covered with nothing between that may free, guards merged in a block, a loop whose whole range was found addressable before it ran), and `-fweavec-checks=verify` monitors them: it keeps every removed guard as a monitor that reports `weavec.proven` if it would have failed. @@ -74,23 +85,10 @@ A correct program does not trap. A program that breaks C's rules in a way that u - reading a word at a time past the end of a string or a buffer (a fast `strlen`, a hash over a key's last word); - using a block's tail beyond its requested size (`malloc_usable_size` returns the requested size); -- indexing a multidimensional array past an inner bound (`int m[4][4]; m[0][k]` with `k` ≥ 4), which (B) checks; -- scanning the stack conservatively, as a garbage collector does, which reads redzones. +- indexing a multidimensional array past an inner bound (`int m[4][4]; m[0][k]` with `k` ≥ 4), which (B) checks. -Such code needs a fix or a `WEAVEC_UNSAFE` region; a function the project already excludes from AddressSanitizer needs nothing. A program that depends on the system allocator's identity (a `malloc_zone_t` of its own, allocator introspection, `mallopt`) sees WeaveC's allocator instead. Exceptions unwinding through C frames are covered by A5. +`weavec-cc` compiles C as AddressSanitizer would (`__SANITIZE_ADDRESS__` is defined and `__has_feature(address_sanitizer)` is true), so code that keeps such reads from ASan behind those tests, or in a function marked `no_sanitize("address")`, needs no change. Other code needs a fix or a `WEAVEC_UNSAFE` region. A program that depends on the system allocator's identity (a `malloc_zone_t` of its own, allocator introspection, `mallopt`) sees WeaveC's allocator instead. ## Without the runtime -Nothing is enforced without the runtime except (B). `weavec-cc` builds without it, and its compiles without guards, under `-fweavec-checks=none`, `-ffreestanding`, `-nostdlib` and its kin, a sanitizer that replaces the allocator (`-fsanitize=address` and the like), and on targets other than 64-bit Darwin and Linux; the link says so. A program that defines `malloc` itself keeps its allocator: its guards stay, but its heap is untracked, guards pass on it and releases are not validated. See [the runtime](/reference/cli/#the-runtime). - -## The advisory analysis - -The ownership and lifetime analysis is not part of the guarantee: it removes no guard and adds none. It reports bugs before the program runs, in the `weavec` tool (errors for definite findings) and under `weavec-cc -fweavec-diagnose` (warnings): - -- use of a released or moved object, a double release, releasing non-heap or interior storage, and releasing through the wrong allocator family; -- dereferencing a pointer that is null on every path; -- an access past an object of exact extent for every value the facts allow, including library calls with a known required length, overlapping copies and writes into string literals; -- a returned or stored pointer that outlives its object; -- leaks, a contradicted `WEAVEC_ASSUME`, and a declaration whose annotation contradicts its definition, across translation units under `weavec --whole-program`. - -A definite finding is an error only once the analysis has found a feasible path that reaches it; a finding on some paths only is a warning with "may" wording. A build fails on them only if you ask: run `weavec` in CI, or build with `-fweavec-diagnose -Werror=weavec`. The analysis can be wrong in both directions; a guard is what stops a bug at run time. +Nothing is enforced without the runtime. `weavec-cc` builds without it, and compiles without guards and without array-index checks, under `-fweavec-checks=none`, `-ffreestanding`, `-nostdlib` and its kin, a sanitizer that replaces the allocator (`-fsanitize=address` and the like), and on targets other than 64-bit Darwin and Linux; the link says so. A program that defines `malloc` itself keeps its allocator: its guards stay, but its heap is untracked, guards pass on it and releases are not validated. See [the runtime](/reference/cli/#the-runtime). diff --git a/docs/pages/reference/troubleshooting.md b/docs/pages/reference/troubleshooting.md index af76287f..532a8574 100644 --- a/docs/pages/reference/troubleshooting.md +++ b/docs/pages/reference/troubleshooting.md @@ -1,6 +1,6 @@ --- title: Troubleshooting -description: Resolve installation problems, read a run-time report, find the bug behind a heap, stack or global report or an invalid release, handle code that reads past objects on purpose, understand the link notes, and deal with slow programs and the advisory analysis. +description: Resolve installation problems, read each run-time report and find the bug behind it, handle code that reads past objects on purpose or was written for AddressSanitizer, understand the link notes, and deal with memory held by the quarantine, slow programs and the sweep's signal. --- ## CMake cannot find LLVM or Clang @@ -25,42 +25,49 @@ weavec-cc: error: cannot find the WeaveC runtime (libweavec_rt.a, libweavec_allo Every link of a guarded program adds the runtime's two archives, which are installed in `lib/weavec/` (beside the `include/` directory that holds `weavec.h`). The driver looks for them in `../lib/weavec/` from its own executable, or in the directory `WEAVEC_RESOURCE_DIR` names. Install WeaveC with `cmake --install`, or run `weavec-cc` from the build tree, which has the same layout. `-fweavec-checks=none` builds without guards and without the runtime. +## The link fails with undefined `__weavec_rt_` or `__asan_` symbols + +Undefined `__weavec_rt_` symbols mean the link did not go through `weavec-cc`, or an object was compiled by an earlier version of `weavec-cc` whose runtime entry points this version no longer has: link with `weavec-cc` and rebuild every object with the current version. An undefined `__asan_` or `__sanitizer_` symbol that the runtime does not provide comes from code that took its AddressSanitizer path; build that unit with `-fno-weavec-asan-compat` (see [AddressSanitizer compatibility](/reference/cli/#addresssanitizer-compatibility)). + ## A program built with weavec-cc traps -A trap (`SIGTRAP`, or `SIGILL` on x86-64; a shell shows exit status 133 or 132) means a guard found an access to memory that is not addressable, and the program stopped before making it. The runtime prints one line on standard error first: +A trap (`SIGTRAP`, or `SIGILL` on x86-64; a shell shows exit status 133 or 132) means a guard found an access the program must not make, and the program stopped before making it. The runtime prints one line on standard error first, and a second that places the address in its object when it can: ```text -weavec: heap-buffer-overflow at vec.c:6:43: read of 4 bytes at 0xbe00000010 -weavec: 0xbe00000010 is 0 bytes after the 16-byte heap object at 0xbe00000000 +weavec: heap-buffer-overflow at vec.c:9:43: read of 4 bytes at 0xbd80000010 +weavec: 0xbd80000010 is 0 bytes after the 16-byte heap object at 0xbd80000000 ``` -The line names the kind of bad access, the source location of the access (`` when the unit has no location for it), whether it reads or writes, its width and its address. A report on the heap adds a second line that places the address relative to the heap object it hit. An index check has its own form, `weavec: index-out-of-bounds at lookup.c:8:12: index -1`, and an access with no known width reads `access at 0x…`. +The first line names the kind of bad access, the source location of the access (`` when the unit has no location for it), whether it reads or writes, its width and its address. An index check has its own form, `weavec: index-out-of-bounds at lookup.c:8:12: index -1`, and an access with no known width reads `access at 0x…`. The [command-line reference](/reference/cli/#check-modes) lists every kind and its second line. + +To see how the program reached the access, run it with `WEAVEC_RT_BACKTRACE=1`, which prints the call stack after the report, or under a debugger, which stops at the trap. Compile with `-g` for the most useful stacks. -To see every failing site in one run, rebuild with `-fweavec-checks=report` and rerun: each failure is printed once per site and the program goes on (what it does after an invalid access is not defined). `WEAVEC_RT_ABORT=1` makes a report-mode build trap at the first failure again. `WEAVEC_RT_REPORT_LOG=` appends the reports to that file instead of standard error, which helps when a test harness hides a passing test's output; a set-user-ID program ignores it. Run the program under a debugger to see the call stack at the trap. +To see every failing site in one run, rebuild with `-fweavec-checks=report` and rerun: each failure is printed once per site and the program goes on (what it does after an invalid access is not defined). `WEAVEC_RT_ABORT=1` makes a report-mode build trap at the first failure again. `WEAVEC_RT_REPORT_LOG=` appends the reports to that file instead of standard error, which helps when a test harness hides a passing test's output; a set-user-ID program ignores it. -A trap is almost always a real bug. The exceptions are programs that read past an object on purpose, described [below](#code-that-reads-past-an-object-on-purpose), and an exception unwound through C frames by code built without WeaveC, which can leave a frame's redzones behind for a later guard to meet (see [assumptions](/reference/guarantees/#assumptions)). See [false traps](/reference/guarantees/#false-traps) for what the guarantee accepts. +A trap is almost always a real bug. The exceptions are programs that read past an object on purpose, described [below](#code-that-reads-past-an-object-on-purpose). See [false traps](/reference/guarantees/#false-traps) for what the guarantee accepts. ## Heap reports -- `heap-buffer-overflow`: the access leaves a heap block past the size the program asked for. The second line says how far past the end (`is 0 bytes after the 16-byte heap object`) the address is. Look for an index or length that exceeds the allocation, an allocation sized in elements where bytes were meant (or the reverse), or a missing byte for a string's terminator. The runtime's allocator gives each block exactly its requested size: slack a system allocator rounds up to is not addressable, and `malloc_usable_size` returns the requested size. -- `heap-use-after-free`: the access is to a block that was freed and is still in the quarantine (`is inside a released heap block`). Look for a pointer kept across a `free`, or across a `realloc` that moved the block. Once the quarantine (16 MiB of released blocks by default) recycles a block, a stale pointer into it sees the new object and is not caught; a larger `WEAVEC_RT_QUARANTINE=` keeps blocks longer. +- `heap-buffer-overflow`: the access reached the bytes after a heap block's object in its slot. The second line says where (`is 16 bytes after the 64-byte heap object`). An index that jumps further, over the slot's tail into a neighbouring live block, is not caught. Look for an index or length that exceeds the allocation, an allocation sized in elements where bytes were meant (or the reverse), or a missing byte for a string's terminator. The runtime's allocator gives each block exactly its requested size: slack a system allocator rounds up to is not addressable, and `malloc_usable_size` returns the requested size. +- `heap-use-after-free`: the access is to a block that was freed (`is inside a released heap block at …`). Look for a pointer kept across a `free`, or across a `realloc` that moved the block. A released block is not reused while the program holds a pointer to it, so this is reported however long ago the block was freed and however much the program allocated since. ## Stack and global reports -- `stack-buffer-overflow`: the access left a local into its redzone. Every local a guard can reach is followed by at least 32 bytes no object owns. -- `dynamic-stack-buffer-overflow`: the same for an `alloca` block or a variable-length array. -- `stack-use-after-scope`: the access is to a local whose block has ended, through a pointer that outlived it. This is caught at every optimisation level. A use after the function returned is not caught. +- `stack-buffer-overflow`: the access left a local into the redzone after it (or, for a negative index, before it). `dynamic-stack-buffer-overflow` is the same for an `alloca` block or a variable-length array. +- `stack-use-after-scope`: the access is to a local whose block has ended, through a pointer that outlived it. This is caught at every optimisation level. A use of a local after its function returned is not caught. - `global-buffer-overflow`: the access left a global or static variable the unit defines into its redzone. -An access that jumps over a redzone into the next object passes, as it would under AddressSanitizer. Within a struct, an index into a declared array field is checked against its bound (`index-out-of-bounds`); a pointer walk past a field is not. +Within a struct, an index into a declared array field is checked against its bound (`index-out-of-bounds`), and a fortified string call into a field against the field's size (`intra-object-overflow`); a pointer walk past a field is not. ## Other reports - `index-out-of-bounds`: `a[i]` into an array whose bound the type declares, with `i` outside it. A multidimensional array indexed past an inner bound (`m[0][k]` with `k` at least the inner size) is reported too; index the flattened storage through a pointer instead. +- `intra-object-overflow`: a fortified library call (`__strcpy_chk`, `__memcpy_chk`, `__sprintf_chk` and the like, which the C library's headers substitute for `strcpy`, `memcpy` and `sprintf` when they know the destination's size) would write more than that size, usually a field of a struct: `weavec: 0x… is 0 bytes inside the 8-byte destination at 0x…`. The bytes past the field belong to the same object, so only the field's size stops the write. Use a bounded copy (`snprintf`, or `memcpy` with a checked length) into the field. +- `use-after-poison`: the access is to bytes the program poisoned through AddressSanitizer's interface (`ASAN_POISON_MEMORY_REGION`), typically free space in a pool or arena allocator, or an object the allocator handed back. It is the bug ASan reports under the same name. - `null-dereference`: the access is through a null pointer at an offset the guard could not rule out, or to the lowest 64 KiB of the address space. A string argument that is null where the library function requires a string is reported the same way. -- `unterminated-string`: a library call that reads a string (`strlen`, `strcpy`, a `%s` conversion) ran off the end of addressable memory without finding the terminator. +- `unterminated-string`: a library call that reads a string (`strlen`, `strcpy`, a `%s` conversion) ran off the end of its object without finding the terminator. - `overlapping-copy`: the source and destination of `memcpy` or another function that requires disjoint operands overlap; use `memmove`. -- `buffer-overflow` and `invalid-access`: the address is not addressable, but the runtime cannot say which kind of object it belongs to. +- `invalid-access`: the address is not addressable, but the runtime cannot say which kind of object it belongs to. - `weavec.proven: `: only in a `-fweavec-checks=verify` build. A guard that a local rule removed would have failed, so the rule was wrong. Please [report it](#report-an-issue) with the source. ## The program stops with "weavec: invalid release" @@ -71,18 +78,18 @@ weavec: invalid release of 0xbd00000000: the block was already released The runtime's allocator received a `free` or `realloc` it cannot honour, and stopped the program. This line has no source location, because the allocator validates the release, not a guard. The reasons are: -| Reason | Usual cause | -| ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -| `the block was already released` | a double free | -| `not the start of its block`, `not the start of a live block` | a free of an interior pointer | -| `no block is allocated there` | a free of a pointer into the heap that no block starts at, or of a stale pointer after the block was recycled | -| `not a heap block` | a free of a stack, global or other pointer that no allocator owns (on Linux the C library's allocator rejects such a pointer itself) | +| Reason | Usual cause | +| ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | +| `the block was already released` | a double free | +| `not the start of its block`, `not the start of a live block` | a free of an interior pointer | +| `no block is allocated there` | a free of a pointer into the heap that no block starts at | +| `not a heap block` | a free of a global or other pointer that no allocator owns (on Linux the C library's allocator rejects such a pointer itself) | -Run the program under a debugger to find the caller of `free`. A program that defines its own allocator does not get this check (see [below](#the-link-prints-defines-the-allocator)). +A released block is not reused while the program holds a pointer to it, so a second `free` through a pointer the program kept is always reported as `the block was already released`. Run the program with `WEAVEC_RT_BACKTRACE=1` or under a debugger to find the caller of `free`. A program that defines its own allocator does not get this check (see [below](#the-link-prints-defines-the-allocator)). ## Code that reads past an object on purpose -Some correct-looking code reads past the end of an object deliberately: string functions that load a word at a time, hash functions that read whole words of a key (json-c's `hashlittle`), conservative garbage collectors that scan the stack. It traps under WeaveC as it does under AddressSanitizer. Use the project's own switch if it has one (json-c's `PRECISE_MEMORY_ACCESS`), or keep the code out of the guards: +Some correct-looking code reads past the end of an object deliberately: string functions that load a word at a time, hash functions that read whole words of a key (json-c's `hashlittle`). It traps under WeaveC as it does under AddressSanitizer. Code that already avoids such reads under ASan, by testing `__SANITIZE_ADDRESS__` or `__has_feature(address_sanitizer)` or by marking the function `__attribute__((no_sanitize("address")))`, needs no change: `weavec-cc` compiles it as ASan would. Otherwise use the project's own switch if it has one (json-c's `PRECISE_MEMORY_ACCESS`), or keep the code out of the guards: ```c #include @@ -92,7 +99,11 @@ WEAVEC_UNSAFE static unsigned long hash_words(const unsigned long *p, size_t n) } ``` -Accesses in a `WEAVEC_UNSAFE` function or block are not guarded and its array indexes are not checked. A function marked `__attribute__((no_sanitize("address")))`, `no_sanitize_address` or `disable_sanitizer_instrumentation`, as code written for AddressSanitizer already is, is treated the same way, so such code needs no change. Keep the region to the code that needs it; see [unsafe boundaries](/guides/unsafe/). +Accesses in a `WEAVEC_UNSAFE` function or block are not guarded and its array indexes are not checked. A function marked `__attribute__((no_sanitize("address")))`, `no_sanitize_address` or `disable_sanitizer_instrumentation` is treated the same way. Keep the region to the code that needs it; see [unsafe boundaries](/guides/unsafe/). + +## Code written for AddressSanitizer behaves differently + +`weavec-cc` defines `__SANITIZE_ADDRESS__` and makes `__has_feature(address_sanitizer)` true, so a project's ASan paths are compiled in: a pool allocator poisons its free space, and a use of it traps as `use-after-poison`, which is the intent. When an ASan path is not wanted (it calls a sanitizer interface the runtime does not provide, or changes behaviour in a way the program should not see), build with `-fno-weavec-asan-compat`: the macros are left undefined and the code is compiled as for a plain build, still guarded. Code that reads past an object on purpose only when ASan is off then traps again, and needs a `WEAVEC_UNSAFE` region. ## The link prints "building without the WeaveC runtime" @@ -108,7 +119,7 @@ The command line has something the runtime cannot work with: a sanitizer that re weavec-cc: note: 'alloc.o' defines the allocator, so the WeaveC runtime's is not linked: the heap is untracked, guards pass on it and releases are not validated ``` -An object or archive member on the link line defines `malloc`, `calloc`, `realloc` or `free`, so the program keeps its own allocator. Guards on locals and globals still work; accesses to the heap pass, and no heap overflow, use after free or invalid free is caught. To have the heap tracked, build without the program's allocator if it has a switch for that. +An object or archive member on the link line defines `malloc`, `calloc`, `realloc` or `free`, so the program keeps its own allocator. Guards on locals and globals still work; accesses to the heap pass, and no heap overflow, use after free or invalid free is caught. To have the heap tracked, build without the program's allocator if it has a switch for that. An allocator that poisons its free space through ASan's interface gets `use-after-poison` reports for it. ## The program warns that the shadow memory could not be reserved @@ -120,37 +131,24 @@ At start-up the runtime reserves address space for its shadow and its arena, wit ## The program is slower or uses more memory -A guarded program reads the runtime's shadow memory (one byte per 16 bytes of the program's memory) before each access the guard passes could not prove safe, gives tracked locals and globals redzones, and holds freed blocks in a quarantine before reusing them. [RFC 0035's acceptance gates](/rfcs/0035-guard-by-default/#acceptance-gates) set targets for that cost: at most twice the run time of a plain Clang build, or AddressSanitizer's ratio where that is larger, and 1.8 times on average. They are targets; the RFC records the measurements when its gates are run. +A guarded program reads the runtime's shadow memory (one byte per 16 bytes of the program's memory) before each access the guard passes could not prove safe, writes the shadow of each local frame with redzones on entry and return, and holds released blocks until a sweep finds no pointer to them. [RFC 0036's acceptance gates](/rfcs/0036-deterministic-heap-reuse/#acceptance-gates) set targets for that cost against the previous release (v0.16.0): a geometric mean of the corpus benchmarks at most 1.1 times its run time and no benchmark above 1.25 times, peak memory at most 1.5 times its or 64 MiB more, and a sweep pause of at most 150 ms with 256 MiB of live heap; the RFC's measured gates record the results. -- Run with `WEAVEC_RT_STATS=1` to see the runtime's counters at exit: how many guards reached the runtime's slow path (`slow guards`), how many range and string guards ran, and how much the allocator did. +- Run with `WEAVEC_RT_STATS=1` to see the runtime's counters at exit: how many guards reached the runtime's slow path (`slow guards`), how many range and string guards ran, how many sweeps ran and how long they paused the program, and how many blocks the last sweep had to keep. - Write the ledger (`-fweavec-ledger=/ -fweavec-summary`) and look at the guarded share: the summary line (`812 accesses: 431 proven, 381 guarded, 0 unguarded`) and the rows say which accesses in a hot function kept their guard. A loop with a known trip count of at least 16 iterations, one exit and nothing that may end a lifetime runs a copy without the guards of its invariant and affine accesses once one range check passes; other loops keep their guards. -- Set `WEAVEC_RT_QUARANTINE=` to shrink the quarantine if memory is the problem. A smaller quarantine catches fewer uses of freed blocks; `0` reuses blocks at once. +- **Memory held by the quarantine.** A released block stays allocated until a sweep finds no pointer to it, and a block some stale word still points to (a dangling pointer the program never clears, or an integer that happens to look like its address) stays allocated as long as that word does. The last line of `WEAVEC_RT_STATS=1` says how many blocks and bytes the last sweep kept. Clearing pointers to freed memory (`free(p); p = NULL;`) lets their blocks be reused. `WEAVEC_RT_QUARANTINE=` sets how many bytes may be released before a sweep is due (16 MiB by default, and at least a quarter of the live heap); sweeps are then paced to take at most about a ninth of the run time, and the quarantine may grow to twice that value, or half the live heap, before one runs regardless. A smaller value holds less memory and sweeps more often. It never makes a block reusable without a sweep, so it does not weaken the guarantee. +- **Sweep pauses.** A sweep stops every thread for a time that grows with the live heap and the program's writable data. A larger `WEAVEC_RT_QUARANTINE` sweeps less often, at the cost of memory. - As a last resort, put a reviewed hot function in a `WEAVEC_UNSAFE` region, or build a unit that cannot pay with `-fweavec-checks=none`. Both give up the guarantee for that code. -## Headers or macros are missing during analysis +## A program that uses SIGPWR on Linux -Use a current `compile_commands.json` with `-p build`, or pass your actual include directories and defines after `--`: +On Linux the sweep stops the program's other threads with a signal, `SIGPWR` by default. When the program has installed its own `SIGPWR` handler by the time of the first sweep, the runtime uses `SIGXCPU` instead; if the program handles both, a process with more than one thread never reuses released memory. A program that installs a handler for the runtime's signal after the first sweep, or that handles both, names another signal for the runtime: ```sh -weavec src/main.c -- -std=c17 -Iinclude -DPROJECT_FEATURE=1 +WEAVEC_RT_SWEEP_SIGNAL=40 ./program ``` -Resolve Clang parse errors before interpreting analysis results. - -## weavec reports errors but the build succeeds - -The ownership and lifetime analysis is advisory: it never changes the generated code, and `weavec-cc` does not run it by default. The `weavec` tool reports a definite finding as an error and exits with status 1; the build, which relies on its guards, is unaffected. To fail a build on the analysis's findings, run `weavec` in CI, or compile with `-fweavec-diagnose -Werror=weavec`: `-fweavec-diagnose` prints every finding as a warning, and `-Werror=weavec` makes them errors. See [the advisory analysis](/reference/cli/#the-advisory-analysis). - -## A helper's behaviour is not visible to the analysis - -Analyse its source with the caller using `weavec --whole-program`, which checks each call against the callee's definition in another file. `weavec-cc -fweavec-diagnose` analyses one unit at a time. An unannotated declaration alone does not describe an unavailable function: the analysis assumes it may free, keep or replace its pointer arguments, and the sites that depend on them count as not proven in the summary line. - -For callbacks, ensure the actual targets are stored somewhere the program can see. A function-pointer type alone does not say which function a call reaches. - -## The analysis is slow - -Each function's analysis has a work budget, `--budget=` in `weavec` and `-fweavec-budget=` with `-fweavec-diagnose` (default 20,000,000; `0` for no limit), and each unit's analysis has one over all of its functions (by default 400 per site, at least 20,000,000; `-fweavec-unit-budget=` with `-fweavec-diagnose`). The sites of a function whose analysis ran out of budget count as not proven. Lower the budgets for faster runs on very large units; see [budgets](/reference/cli/#budgets). `--analysis-stats=` writes where the work went. The budgets do not affect the guards. +The runtime takes that signal over, so it must be one the program neither handles nor blocks in any thread. A thread that blocks it, or does not respond within a second, ends that sweep without recycling anything: memory is held longer, but no released block is reused. Darwin stops threads without a signal. ## Report an issue -Include the WeaveC and LLVM versions, platform, exact command, a minimal source example, and the complete diagnostic or run-time report. Remove private source and environment details you do not intend to publish. Open a [bug report](https://github.com/weavefoundry/weavec/issues/new?template=bug_report.yml). +Include the WeaveC and LLVM versions, platform, exact command, a minimal source example, and the complete run-time report (with `WEAVEC_RT_BACKTRACE=1` if the stack matters). Remove private source and environment details you do not intend to publish. Open a [bug report](https://github.com/weavefoundry/weavec/issues/new?template=bug_report.yml). diff --git a/docs/pages/rfcs.mdx b/docs/pages/rfcs.mdx index 6c121c51..b41f72d4 100644 --- a/docs/pages/rfcs.mdx +++ b/docs/pages/rfcs.mdx @@ -1,14 +1,14 @@ --- title: The RFC library -description: Browse WeaveC's design records for ownership, borrowing, whole-program analysis, and proven or runtime-checked memory safety. +description: Browse WeaveC's design records, from the ownership analysis it began with to the guards and runtime that make C memory safe at run time. editUrl: https://github.com/weavefoundry/weavec/edit/main/docs/rfcs/README.md --- import RfcLibrary from '../src/components/RfcLibrary.astro'; -Every major model decision has a written record: the problem, the proposed semantics, the tradeoffs, and the evidence required. These RFCs are the authoritative design references for WeaveC. +Every major design decision has a written record: the problem, the proposed semantics, the tradeoffs, and the evidence required. These RFCs are the authoritative design references for WeaveC. -**Accepted** records specify the design. **Implemented** records have completed their defined implementation and test criteria. **Superseded** records are kept as history; the record that replaced them says what changed. [RFC 0030](/rfcs/0030-prove-or-trap/) defines the current model and guarantee. A status is not a guarantee about arbitrary C programs; consult each RFC's scope and validation. +**Accepted** records specify the design. **Implemented** records have completed their defined implementation and test criteria. **Superseded** records are kept as history; the record that replaced them says what changed. [RFC 0035](/rfcs/0035-guard-by-default/) defines the guards and [RFC 0036](/rfcs/0036-deterministic-heap-reuse/) the current guarantee. Earlier records that specify the ownership analysis RFC 0036 deleted are kept as history. A status is not a guarantee about arbitrary C programs; consult each RFC's scope and validation. diff --git a/docs/rfcs/0000-template.md b/docs/rfcs/0000-template.md index 697e0257..0e2a4a8f 100644 --- a/docs/rfcs/0000-template.md +++ b/docs/rfcs/0000-template.md @@ -20,8 +20,8 @@ not delete them, so readers can see the question was considered. ## Motivation @@ -29,20 +29,20 @@ the cost of not doing this? ## Detailed design @@ -51,13 +51,15 @@ fewer false positives, say so here in plain terms. Enough detail that someone familiar with the codebase could implement it without re-deriving the decisions. Cover, as applicable: -- Changes to `weavec::Core` (new facts, lattice changes, new state carried - through the dataflow). Remember Core may not depend on Clang/LLVM. -- Changes to `weavec::Analysis` (which AST nodes / CFG elements produce which - core events; new recognised functions; new options). -- Changes to `weavec::Frontend` or the `weavec` driver, if any. +- Changes to the guard passes (`lib/Frontend/Guard*.cpp`): which accesses + get a guard, which rules remove one (local to a function, and monitored + by `verify` mode), what the inline check does. +- Changes to the runtime (`runtime/`): the shadow, the arena, frame blocks, + the quarantine and the sweep, the checked library wrappers, the reports. +- Changes to the library table (`lib/Core/LibrarySpec.txt`), the + enforcement ledger or the `weavec-cc` driver, if any. - Interaction with existing RFCs; anything they say that stops being true. -- Performance expectations (per-function cost, per-TU cost) and how they will +- Performance expectations (run time, memory, build time) and how they will be measured. --> @@ -66,9 +68,9 @@ without re-deriving the decisions. Cover, as applicable: @@ -76,26 +78,27 @@ Write "None." if the user-facing header is untouched. ## Diagnostics ## Drawbacks ## Alternatives @@ -112,12 +115,11 @@ WeaveC has no reference implementation to match, so this section is where we borrow judgement. Cite specifics (paper section, source file, RFC number) from, as relevant: -- Rust: the borrow checker, NLL (RFC 2094), Polonius, `Box`/`&`/`&mut`. -- Cyclone, Checked C, and other safe-C dialects. -- Clang: `-Wdangling`, `[[clang::lifetimebound]]`, the lifetime-safety - analysis, the static analyzer's `MallocChecker`. -- The C++ Core Guidelines lifetime profile and GSL `owner`. -- Classic static analyses (escape analysis, region inference, typestate). +- AddressSanitizer, HWASan and Arm MTE: shadow memory, redzones, tags. +- Low-fat pointers, SoftBound and CHERI: bounds from provenance. +- MarkUs, MineSweeper and the Boehm–Demers–Weiser collector: reuse after + a conservative scan. +- `_FORTIFY_SOURCE`, Checked C, Cyclone and other safe-C dialects. Say what each got right, what it got wrong for C, and what we take from it. --> diff --git a/docs/rfcs/0001-ownership-model.md b/docs/rfcs/0001-ownership-model.md index 2652e5d7..702d5f95 100644 --- a/docs/rfcs/0001-ownership-model.md +++ b/docs/rfcs/0001-ownership-model.md @@ -1,10 +1,14 @@ # RFC 0001: Ownership, borrowing and lifetimes for C -- **Status**: Accepted +- **Status**: Superseded - **Authors**: WeaveC authors - **Created**: 2026-09-01 - **Tracking issue**: TBD -- **Supersedes / superseded by**: — +- **Supersedes / superseded by**: Superseded by RFC 0036. + +> Superseded by [RFC 0036](0036-deterministic-heap-reuse.md), which +> deletes the advisory analysis this RFC specified. The RFC was Accepted +> until then; it is kept as a record of the design. > The guarantee statement in *Soundness* is replaced by > [RFC 0030](0030-prove-or-trap.md#soundness), *Soundness*. The model this diff --git a/docs/rfcs/0002-intraprocedural-checking.md b/docs/rfcs/0002-intraprocedural-checking.md index 2ea50a48..614c76e4 100644 --- a/docs/rfcs/0002-intraprocedural-checking.md +++ b/docs/rfcs/0002-intraprocedural-checking.md @@ -1,12 +1,17 @@ # RFC 0002: Sound intra-procedural checking -- **Status**: Implemented +- **Status**: Superseded - **Authors**: WeaveC authors - **Created**: 2026-09-01 - **Accepted**: 2026-09-01 - **Implemented**: 2026-09-01 - **Tracking issue**: TBD -- **Supersedes / superseded by**: — (implements phase 1 of RFC 0001) +- **Supersedes / superseded by**: Superseded by RFC 0036 (implements phase 1 + of RFC 0001). + +> Superseded by [RFC 0036](0036-deterministic-heap-reuse.md), which +> deletes the advisory analysis this RFC specified. The RFC was Implemented +> until then; it is kept as a record of the design. ## Implementation notes diff --git a/docs/rfcs/0003-signature-inference.md b/docs/rfcs/0003-signature-inference.md index 359ec42c..bcffe888 100644 --- a/docs/rfcs/0003-signature-inference.md +++ b/docs/rfcs/0003-signature-inference.md @@ -1,13 +1,17 @@ # RFC 0003: Signature inference -- **Status**: Implemented +- **Status**: Superseded - **Authors**: WeaveC authors - **Created**: 2026-09-01 - **Accepted**: 2026-09-01 - **Implemented**: 2026-09-01 - **Tracking issue**: TBD -- **Supersedes / superseded by**: — (implements phase 2 and the first half of - phase 3 of RFC 0001) +- **Supersedes / superseded by**: Superseded by RFC 0036 (implements phase 2 + and the first half of phase 3 of RFC 0001). + +> Superseded by [RFC 0036](0036-deterministic-heap-reuse.md), which +> deletes the advisory analysis this RFC specified. The RFC was Implemented +> until then; it is kept as a record of the design. ## Implementation notes diff --git a/docs/rfcs/0004-unsafe-boundaries.md b/docs/rfcs/0004-unsafe-boundaries.md index b39769d1..3778bf4b 100644 --- a/docs/rfcs/0004-unsafe-boundaries.md +++ b/docs/rfcs/0004-unsafe-boundaries.md @@ -1,18 +1,22 @@ # RFC 0004: Unsafe boundaries: raw pointers, unsafe regions and indirect calls -- **Status**: Implemented +- **Status**: Superseded - **Authors**: WeaveC authors - **Created**: 2026-09-01 - **Accepted**: 2026-09-01 - **Implemented**: 2026-09-01 - **Tracking issue**: TBD -- **Supersedes / superseded by**: supersedes RFC 0001 *Unsafe* and RFC 0002 - *Unsafe interaction* / *Places* (opaque expressions) / *Events* (pointer - arithmetic); supersedes the `--strict-externs` row of RFC 0003 - *Diagnostics*; implements the remaining item of RFC 0001 *Soundness* +- **Supersedes / superseded by**: Superseded by RFC 0036; supersedes RFC 0001 + *Unsafe* and RFC 0002 *Unsafe interaction* / *Places* (opaque expressions) / + *Events* (pointer arithmetic); supersedes the `--strict-externs` row of RFC + 0003 *Diagnostics*; implements the remaining item of RFC 0001 *Soundness* (`unsafe-operation`) and resolves RFC 0001's open question *Function pointers and callbacks*. +> Superseded by [RFC 0036](0036-deterministic-heap-reuse.md), which +> deletes the advisory analysis this RFC specified. The RFC was Implemented +> until then; it is kept as a record of the design. + ## Implementation notes The design below landed as written except where this section says diff --git a/docs/rfcs/0005-whole-program-analysis.md b/docs/rfcs/0005-whole-program-analysis.md index 16b7d9f3..c8cf0cb7 100644 --- a/docs/rfcs/0005-whole-program-analysis.md +++ b/docs/rfcs/0005-whole-program-analysis.md @@ -1,18 +1,22 @@ # RFC 0005: Whole-program analysis: cross-translation-unit summaries and the compiler driver -- **Status**: Implemented +- **Status**: Superseded - **Authors**: WeaveC authors - **Created**: 2026-09-02 - **Accepted**: 2026-09-02 - **Implemented**: 2026-09-02 - **Tracking issue**: TBD -- **Supersedes / superseded by**: supersedes RFC 0003 *`annotation-required`, - revised* item 1 (the boundary moves from the translation unit to the - program) and the *Cross-TU inference* row of RFC 0003 *Soundness*; - resolves RFC 0001's open question *Inference across translation units*; - narrows RFC 0004 *Callbacks registered from another translation unit* to - code outside the program; records the struct-copy rule RFC 0002 left - implicit. +- **Supersedes / superseded by**: Superseded by RFC 0036; supersedes RFC 0003 + *`annotation-required`, revised* item 1 (the boundary moves from the + translation unit to the program) and the *Cross-TU inference* row of RFC + 0003 *Soundness*; resolves RFC 0001's open question *Inference across + translation units*; narrows RFC 0004 *Callbacks registered from another + translation unit* to code outside the program; records the struct-copy rule + RFC 0002 left implicit. + +> Superseded by [RFC 0036](0036-deterministic-heap-reuse.md), which +> deletes the advisory analysis this RFC specified. The RFC was Implemented +> until then; it is kept as a record of the design. ## Implementation notes diff --git a/docs/rfcs/0006-precision.md b/docs/rfcs/0006-precision.md index 85df43e0..2821ed09 100644 --- a/docs/rfcs/0006-precision.md +++ b/docs/rfcs/0006-precision.md @@ -1,19 +1,23 @@ # RFC 0006: Precision: non-lexical loans, condition facts, element places and outcome-conditional summaries -- **Status**: Accepted +- **Status**: Superseded - **Authors**: WeaveC authors - **Created**: 2026-09-02 - **Accepted**: 2026-09-02 - **Tracking issue**: TBD -- **Supersedes / superseded by**: supersedes RFC 0002 *Loans and lifetimes* - (loans now end at the holder's last use), the *Borrow rules* paragraph of - RFC 0001 *The model* as the default behaviour (exclusive borrowing becomes - opt-in), RFC 0002 *`realloc` and the null edge* (subsumed by - outcome-conditional summaries), the RFC 0002 rule that `a[i]` and `a[j]` +- **Supersedes / superseded by**: Superseded by RFC 0036; supersedes RFC 0002 + *Loans and lifetimes* (loans now end at the holder's last use), the *Borrow + rules* paragraph of RFC 0001 *The model* as the default behaviour (exclusive + borrowing becomes opt-in), RFC 0002 *`realloc` and the null edge* (subsumed + by outcome-conditional summaries), the RFC 0002 rule that `a[i]` and `a[j]` are indistinguishable for moves (element witnesses), and the RFC 0003 - summary text format version 1 (bumped to 2). Resolves the two follow-ups - the roadmap lists under Milestone 2 (*may-moves for `a[*]` element - places*, *pointer-equality guards*). + summary text format version 1 (bumped to 2). Resolves the two follow-ups the + roadmap lists under Milestone 2 (*may-moves for `a[*]` element places*, + *pointer-equality guards*). + +> Superseded by [RFC 0036](0036-deterministic-heap-reuse.md), which +> deletes the advisory analysis this RFC specified. The RFC was Accepted +> until then; it is kept as a record of the design. ## Summary diff --git a/docs/rfcs/0007-resource-lifecycle.md b/docs/rfcs/0007-resource-lifecycle.md index 0ee39f9e..723128ee 100644 --- a/docs/rfcs/0007-resource-lifecycle.md +++ b/docs/rfcs/0007-resource-lifecycle.md @@ -1,18 +1,22 @@ # RFC 0007: Resource lifecycle: leaks, release families and owned fields -- **Status**: Accepted +- **Status**: Superseded - **Authors**: WeaveC authors - **Created**: 2026-09-02 - **Accepted**: 2026-09-02 - **Tracking issue**: TBD -- **Supersedes / superseded by**: supersedes the RFC 0003 rule that a - callee's consumption *below* a consumed argument is "the callee's - business" at the call site (*Applying a summary at a call*, step 1); the - RFC 0004 decision that `WEAVEC_OWNED` on a field is recorded but not +- **Supersedes / superseded by**: Superseded by RFC 0036; supersedes the RFC + 0003 rule that a callee's consumption *below* a consumed argument is "the + callee's business" at the call site (*Applying a summary at a call*, step + 1); the RFC 0004 decision that `WEAVEC_OWNED` on a field is recorded but not consumed (*Unresolved questions*, last row); and the RFC 0003 summary text - format version 2 (bumped to 3). Resolves the RFC 0002 and RFC 0006 - *Future work* items *Leak reporting*, *Leak detection and release - families* and *Owned fields*, and the RFC 0004 item *Allocator families*. + format version 2 (bumped to 3). Resolves the RFC 0002 and RFC 0006 *Future + work* items *Leak reporting*, *Leak detection and release families* and + *Owned fields*, and the RFC 0004 item *Allocator families*. + +> Superseded by [RFC 0036](0036-deterministic-heap-reuse.md), which +> deletes the advisory analysis this RFC specified. The RFC was Accepted +> until then; it is kept as a record of the design. ## Summary diff --git a/docs/rfcs/0008-pointer-validity.md b/docs/rfcs/0008-pointer-validity.md index 718a8c8b..c28d2262 100644 --- a/docs/rfcs/0008-pointer-validity.md +++ b/docs/rfcs/0008-pointer-validity.md @@ -1,17 +1,21 @@ # RFC 0008: Pointer validity: null dereferences, uninitialised pointers, invalid releases and replaced values -- **Status**: Accepted +- **Status**: Superseded - **Authors**: WeaveC authors - **Created**: 2026-09-03 - **Accepted**: 2026-09-03 - **Tracking issue**: TBD -- **Supersedes / superseded by**: amends the RFC 0003 rule on *where - effects are read from* (*Deriving a summary*, third bullet: frees and - moves below a dereference are read from the exit state alone); extends - the RFC 0003 summary vocabulary (`replaced`, `requires`, `notnull`, the - `result` root) and the RFC 0006/0007 outcome facts (`nonNullOn`); +- **Supersedes / superseded by**: Superseded by RFC 0036; amends the RFC 0003 + rule on *where effects are read from* (*Deriving a summary*, third bullet: + frees and moves below a dereference are read from the exit state alone); + extends the RFC 0003 summary vocabulary (`replaced`, `requires`, `notnull`, + the `result` root) and the RFC 0006/0007 outcome facts (`nonNullOn`); resolves the RFC 0007 *Future work* item "Nullability (a later RFC)". +> Superseded by [RFC 0036](0036-deterministic-heap-reuse.md), which +> deletes the advisory analysis this RFC specified. The RFC was Accepted +> until then; it is kept as a record of the design. + ## Summary WeaveC answers "who owns this pointer and for how long". It does not yet diff --git a/docs/rfcs/0009-value-conditional-behaviour.md b/docs/rfcs/0009-value-conditional-behaviour.md index 2358a4a2..4c657f8d 100644 --- a/docs/rfcs/0009-value-conditional-behaviour.md +++ b/docs/rfcs/0009-value-conditional-behaviour.md @@ -1,17 +1,21 @@ # RFC 0009: Value-conditional behaviour: scalar facts, guarded effects and inferred `noreturn` -- **Status**: Accepted +- **Status**: Superseded - **Authors**: WeaveC authors - **Created**: 2026-09-03 - **Accepted**: 2026-09-03 - **Tracking issue**: TBD -- **Supersedes / superseded by**: extends the RFC 0006 *condition facts* - from pointer tests and tests of call results to tests of integer places; - extends the RFC 0003 summary vocabulary with `when` guards on effects, - stores and returns and with `never-returns`; supersedes the RFC 0006 and - RFC 0007 *Accepted false positives* entries "Merge-point conservatism" - and the RFC 0008 entry "Tested-then-merged" in its unannotated-`noreturn` - form. +- **Supersedes / superseded by**: Superseded by RFC 0036; extends the RFC 0006 + *condition facts* from pointer tests and tests of call results to tests of + integer places; extends the RFC 0003 summary vocabulary with `when` guards + on effects, stores and returns and with `never-returns`; supersedes the RFC + 0006 and RFC 0007 *Accepted false positives* entries "Merge-point + conservatism" and the RFC 0008 entry "Tested-then-merged" in its + unannotated-`noreturn` form. + +> Superseded by [RFC 0036](0036-deterministic-heap-reuse.md), which +> deletes the advisory analysis this RFC specified. The RFC was Accepted +> until then; it is kept as a record of the design. ## Summary diff --git a/docs/rfcs/0010-shared-ownership.md b/docs/rfcs/0010-shared-ownership.md index 8cf240c8..e47f8616 100644 --- a/docs/rfcs/0010-shared-ownership.md +++ b/docs/rfcs/0010-shared-ownership.md @@ -1,20 +1,24 @@ # RFC 0010: Shared ownership: reference counts, ownership by outcome and per-outcome facts -- **Status**: Accepted +- **Status**: Superseded - **Authors**: WeaveC authors - **Created**: 2026-09-04 - **Accepted**: 2026-09-04 - **Tracking issue**: TBD -- **Supersedes / superseded by**: amends the RFC 0001 rule that every - owned resource has exactly one owner (*Ownership kinds*) by adding - *shares*; amends RFC 0002's alias relation (an edge now records whether - its ends hold the same share); amends RFC 0007's leak rule for a holder - that took a reference; resolves the RFC 0007 *Unresolved questions* items - *Reference counting* and *Family annotations* and its *Future work* item - *Per-outcome stores*; resolves the RFC 0009 *Future work* item *Facts +- **Supersedes / superseded by**: Superseded by RFC 0036; amends the RFC 0001 + rule that every owned resource has exactly one owner (*Ownership kinds*) by + adding *shares*; amends RFC 0002's alias relation (an edge now records + whether its ends hold the same share); amends RFC 0007's leak rule for a + holder that took a reference; resolves the RFC 0007 *Unresolved questions* + items *Reference counting* and *Family annotations* and its *Future work* + item *Per-outcome stores*; resolves the RFC 0009 *Future work* item *Facts about call results kept as scalars* for integer memory the callee writes. - Bumps the summary text format (RFC 0003) and the sidecar format (RFC - 0005) from 5 to 6. + Bumps the summary text format (RFC 0003) and the sidecar format (RFC 0005) + from 5 to 6. + +> Superseded by [RFC 0036](0036-deterministic-heap-reuse.md), which +> deletes the advisory analysis this RFC specified. The RFC was Accepted +> until then; it is kept as a record of the design. ## Summary diff --git a/docs/rfcs/0011-spatial-safety.md b/docs/rfcs/0011-spatial-safety.md index c02fb8b2..6bf5a577 100644 --- a/docs/rfcs/0011-spatial-safety.md +++ b/docs/rfcs/0011-spatial-safety.md @@ -1,23 +1,26 @@ # RFC 0011: Spatial safety: derived pointers, extents and bounds -- **Status**: Accepted +- **Status**: Superseded - **Authors**: WeaveC authors - **Created**: 2026-09-04 - **Accepted**: 2026-09-04 - **Tracking issue**: TBD -- **Supersedes / superseded by**: lifts the RFC 0001 exclusion of *bounds* - from the model (*Deliberately not caught*); replaces the boolean - *interior* attribute of RFC 0006's alias edges, RFC 0007's resource - records, RFC 0003's `copy` sources and the analysis layer's value - origins with a *pointer offset*; moves RFC 0002's `lifetime-too-short` - check from the store that creates the borrow to the death of the - borrowed object (RFC 0006's *Loans end at the last use of their holder*, - applied to the other end of the loan); amends RFC 0004's *Pointer - identity* so that taking the address of a field or element of an object - reached through a pointer derives a pointer to the *same object*; adds - a widening step to RFC 0005's whole-program fixpoint (as amended by RFC - 0010). Bumps the summary text format (RFC 0003) and the sidecar format - (RFC 0005) from 6 to 7. +- **Supersedes / superseded by**: Superseded by RFC 0036; lifts the RFC 0001 + exclusion of *bounds* from the model (*Deliberately not caught*); replaces + the boolean *interior* attribute of RFC 0006's alias edges, RFC 0007's + resource records, RFC 0003's `copy` sources and the analysis layer's value + origins with a *pointer offset*; moves RFC 0002's `lifetime-too-short` check + from the store that creates the borrow to the death of the borrowed object + (RFC 0006's *Loans end at the last use of their holder*, applied to the + other end of the loan); amends RFC 0004's *Pointer identity* so that taking + the address of a field or element of an object reached through a pointer + derives a pointer to the *same object*; adds a widening step to RFC 0005's + whole-program fixpoint (as amended by RFC 0010). Bumps the summary text + format (RFC 0003) and the sidecar format (RFC 0005) from 6 to 7. + +> Superseded by [RFC 0036](0036-deterministic-heap-reuse.md), which +> deletes the advisory analysis this RFC specified. The RFC was Accepted +> until then; it is kept as a record of the design. ## Summary diff --git a/docs/rfcs/0012-spatial-safety-strings-and-fields.md b/docs/rfcs/0012-spatial-safety-strings-and-fields.md index f20b83ad..85a97abd 100644 --- a/docs/rfcs/0012-spatial-safety-strings-and-fields.md +++ b/docs/rfcs/0012-spatial-safety-strings-and-fields.md @@ -1,19 +1,23 @@ # RFC 0012: Spatial safety II: strings, sized fields, offset relations and assumptions -- **Status**: Accepted +- **Status**: Superseded - **Authors**: WeaveC authors - **Created**: 2026-09-05 - **Accepted**: 2026-09-05 - **Tracking issue**: TBD -- **Supersedes / superseded by**: extends RFC 0011's *Spatial records* with - string facts, its *Relations* with constant offsets and lower bounds, its - *Bounds checks* with the string functions of the library table, and its - *Annotation surface* with `WEAVEC_SIZED_BY` on fields and - `WEAVEC_ASSUME`; strikes *null-terminated string extents*, *flexible - array members* and the `i < n - 1` case of *an interval domain* from - RFC 0011's *Future work*, and *flexible array members* from its *Bugs - deliberately not caught*. Bumps the sidecar format (RFC 0005) from 7 to - 8; the summary text format (RFC 0003) stays at 7. +- **Supersedes / superseded by**: Superseded by RFC 0036; extends RFC 0011's + *Spatial records* with string facts, its *Relations* with constant offsets + and lower bounds, its *Bounds checks* with the string functions of the + library table, and its *Annotation surface* with `WEAVEC_SIZED_BY` on fields + and `WEAVEC_ASSUME`; strikes *null-terminated string extents*, *flexible + array members* and the `i < n - 1` case of *an interval domain* from RFC + 0011's *Future work*, and *flexible array members* from its *Bugs + deliberately not caught*. Bumps the sidecar format (RFC 0005) from 7 to 8; + the summary text format (RFC 0003) stays at 7. + +> Superseded by [RFC 0036](0036-deterministic-heap-reuse.md), which +> deletes the advisory analysis this RFC specified. The RFC was Accepted +> until then; it is kept as a record of the design. ## Summary diff --git a/docs/rfcs/0013-interprocedural-heap-state.md b/docs/rfcs/0013-interprocedural-heap-state.md index ccd6ca54..b529ecb7 100644 --- a/docs/rfcs/0013-interprocedural-heap-state.md +++ b/docs/rfcs/0013-interprocedural-heap-state.md @@ -1,12 +1,16 @@ # RFC 0013: Interprocedural heap state and value identity -- **Status**: Accepted +- **Status**: Superseded - **Authors**: WeaveC authors - **Created**: 2026-09-06 - **Tracking issue**: TBD -- **Supersedes / superseded by**: Amends RFC 0003's summaries, RFC 0007's - escaped resources, RFC 0008's replaced values and result objects, RFC 0011's - extents, and RFC 0012's string facts. +- **Supersedes / superseded by**: Superseded by RFC 0036; amends RFC 0003's + summaries, RFC 0007's escaped resources, RFC 0008's replaced values and + result objects, RFC 0011's extents, and RFC 0012's string facts. + +> Superseded by [RFC 0036](0036-deterministic-heap-reuse.md), which +> deletes the advisory analysis this RFC specified. The RFC was Accepted +> until then; it is kept as a record of the design. Implementation is authorized by the project owner's request to draft this RFC first and then implement the recommended milestone end to end. This diff --git a/docs/rfcs/0014-pointer-identity-and-call-effects.md b/docs/rfcs/0014-pointer-identity-and-call-effects.md index 5d96a661..c8d067f8 100644 --- a/docs/rfcs/0014-pointer-identity-and-call-effects.md +++ b/docs/rfcs/0014-pointer-identity-and-call-effects.md @@ -1,12 +1,16 @@ # RFC 0014: Pointer identity and precise call effects -- **Status**: Implemented +- **Status**: Superseded - **Authors**: WeaveC authors - **Created**: 2026-09-06 - **Tracking issue**: TBD -- **Supersedes / superseded by**: Amends RFC 0004's indirect callees, - RFC 0005's exported summaries, RFC 0006's written objects, RFC 0009's - guards, and RFC 0013's heap identities and projection. +- **Supersedes / superseded by**: Superseded by RFC 0036; amends RFC 0004's + indirect callees, RFC 0005's exported summaries, RFC 0006's written objects, + RFC 0009's guards, and RFC 0013's heap identities and projection. + +> Superseded by [RFC 0036](0036-deterministic-heap-reuse.md), which +> deletes the advisory analysis this RFC specified. The RFC was Implemented +> until then; it is kept as a record of the design. ## Summary diff --git a/docs/rfcs/0015-array-and-container-ownership.md b/docs/rfcs/0015-array-and-container-ownership.md index 70553411..5b8f046f 100644 --- a/docs/rfcs/0015-array-and-container-ownership.md +++ b/docs/rfcs/0015-array-and-container-ownership.md @@ -1,12 +1,17 @@ # RFC 0015: Array elements, range operations, and container ownership -- **Status**: Implemented +- **Status**: Superseded - **Authors**: WeaveC authors - **Created**: 2026-09-07 - **Tracking issue**: TBD -- **Supersedes / superseded by**: Amends RFC 0002's array places, RFC 0006's - element witnesses, RFC 0008's element consumption, RFC 0013's heap - descriptions, and RFC 0014's complete memory copies and coverage. +- **Supersedes / superseded by**: Superseded by RFC 0036; amends RFC 0002's + array places, RFC 0006's element witnesses, RFC 0008's element consumption, + RFC 0013's heap descriptions, and RFC 0014's complete memory copies and + coverage. + +> Superseded by [RFC 0036](0036-deterministic-heap-reuse.md), which +> deletes the advisory analysis this RFC specified. The RFC was Implemented +> until then; it is kept as a record of the design. ## Summary diff --git a/docs/rfcs/0016-compositional-call-checking.md b/docs/rfcs/0016-compositional-call-checking.md index 45c41633..179426b6 100644 --- a/docs/rfcs/0016-compositional-call-checking.md +++ b/docs/rfcs/0016-compositional-call-checking.md @@ -1,13 +1,17 @@ # RFC 0016: Compositional call checking under caller alias relationships -- **Status**: Implemented +- **Status**: Superseded - **Authors**: WeaveC authors - **Created**: 2026-09-07 - **Tracking issue**: TBD -- **Supersedes / superseded by**: Amends RFC 0003's call summaries, RFC 0005's - program dependencies and sidecars, RFC 0009's heap-fact branch pruning, - RFC 0013's input/output identities, - RFC 0014's bounded specialization, and RFC 0015's selected-element interfaces. +- **Supersedes / superseded by**: Superseded by RFC 0036; amends RFC 0003's + call summaries, RFC 0005's program dependencies and sidecars, RFC 0009's + heap-fact branch pruning, RFC 0013's input/output identities, RFC 0014's + bounded specialization, and RFC 0015's selected-element interfaces. + +> Superseded by [RFC 0036](0036-deterministic-heap-reuse.md), which +> deletes the advisory analysis this RFC specified. The RFC was Implemented +> until then; it is kept as a record of the design. ## Summary diff --git a/docs/rfcs/0017-c-integer-semantics-and-spatial-safety.md b/docs/rfcs/0017-c-integer-semantics-and-spatial-safety.md index 5d0940a2..e6e17640 100644 --- a/docs/rfcs/0017-c-integer-semantics-and-spatial-safety.md +++ b/docs/rfcs/0017-c-integer-semantics-and-spatial-safety.md @@ -1,14 +1,19 @@ # RFC 0017: C integer semantics and compositional spatial safety -- **Status**: Implemented +- **Status**: Superseded - **Authors**: WeaveC authors - **Created**: 2026-09-07 - **Tracking issue**: TBD -- **Supersedes / superseded by**: Amends RFC 0009's mathematical-integer - assumptions and scalar guards, RFC 0011's affine extents and requirements, - RFC 0012's relations and counted fields, RFC 0013's value snapshots, - RFC 0015's selectors and range interfaces, and RFC 0016's numeric context - facts. RFC 0005 continues to specify whole-program orchestration. +- **Supersedes / superseded by**: Superseded by RFC 0036; amends RFC 0009's + mathematical-integer assumptions and scalar guards, RFC 0011's affine + extents and requirements, RFC 0012's relations and counted fields, RFC + 0013's value snapshots, RFC 0015's selectors and range interfaces, and RFC + 0016's numeric context facts. RFC 0005 continues to specify whole-program + orchestration. + +> Superseded by [RFC 0036](0036-deterministic-heap-reuse.md), which +> deletes the advisory analysis this RFC specified. The RFC was Implemented +> until then; it is kept as a record of the design. ## Summary diff --git a/docs/rfcs/0030-prove-or-trap.md b/docs/rfcs/0030-prove-or-trap.md index 4feb018d..37277292 100644 --- a/docs/rfcs/0030-prove-or-trap.md +++ b/docs/rfcs/0030-prove-or-trap.md @@ -1,6 +1,6 @@ # RFC 0030: Prove or trap — one safety semantics with compiler-enforced checks -- **Status**: Implemented (as amended by [RFC 0031](0031-object-engine.md), which replaced its engine, took over its gates G10, G14 and G15, and amended G10's limit to 65; and by [RFC 0032](0032-runtime-enforcement.md), which adds the outcome `guarded`, a runtime in every enforcing build, guards for temporal facets, the require level `guarded` and ledger version 2. Where this text says "RFC 0032" for records in object sections, baselines and archives, it means the adoption RFC, which is now planned as RFC 0033) +- **Status**: Implemented (as amended by [RFC 0031](0031-object-engine.md), which replaced its engine, took over its gates G10, G14 and G15, and amended G10's limit to 65; and by [RFC 0032](0032-runtime-enforcement.md), which adds the outcome `guarded`, a runtime in every enforcing build, guards for temporal facets, the require level `guarded` and ledger version 2. Where this text says "RFC 0032" for records in object sections, baselines and archives, it means the adoption RFC, which is now planned as RFC 0033. Its analysis (§2–§9) is superseded by [RFC 0036](0036-deterministic-heap-reuse.md), which deletes it) - **Authors**: WeaveC authors - **Created**: 2026-09-18 - **Tracking issue**: TBD @@ -11,6 +11,14 @@ `analysis-incomplete`, give unknown callees borrow-only effects, let `WEAVEC_UNSAFE` suppress diagnostics, trust `WEAVEC_ASSUME`, or state that WeaveC adds no runtime instrumentation (§19 lists each amendment). + Its enforcement is superseded by RFC 0035 and its analysis (§2–§9) by + RFC 0036. + +> [RFC 0035](0035-guard-by-default.md) superseded this RFC's enforcement +> and kept its analysis as advisory; [RFC 0036](0036-deterministic-heap-reuse.md) +> deletes that analysis (§2–§9: the `weavec` tool, its diagnostics and +> every annotation but `WEAVEC_UNSAFE`). The RFC keeps its status as the +> record of what was implemented. This RFC was drafted before implementation. On 2026-09-18 the project owner accepted every recommendation of the milestone analysis. The owner diff --git a/docs/rfcs/0031-object-engine.md b/docs/rfcs/0031-object-engine.md index e6fbbe47..e8339d0d 100644 --- a/docs/rfcs/0031-object-engine.md +++ b/docs/rfcs/0031-object-engine.md @@ -1,18 +1,26 @@ # RFC 0031: The object engine — a sound heap abstraction behind the engine seam -- **Status**: Implemented (as amended by [RFC 0032](0032-runtime-enforcement.md): unit record format 29 becomes 30, the place class of array-element cells at a boundary (§4.9) is the one RFC 0032's first amendment states, and the part of gate G6 that runtime extents and liveness close is closed there) +- **Status**: Superseded - **Authors**: WeaveC authors - **Created**: 2026-09-28 - **Tracking issue**: TBD -- **Supersedes / superseded by**: Replaces the engine of RFC 0030 - (`FunctionDataflow`, RFC 0030 §15) and the summary format of RFC 0030 §9.1 - and §13.1 (summary format 29 becomes 30, unit record format 28 becomes 29). - Amends RFC 0030's assumption A3 (*Soundness*, below), its §3 certainty - rules (restated over objects in §5), its §9.4 boundary facts (§5.6), its - §14 seam (§8), and takes over its open gates G10, G14 and G15. Amends RFCs - 0002–0017 wherever they describe how `FunctionDataflow` represents a fact; - what those RFCs decide about outcomes and diagnostics stays in force as - RFC 0030 restated it. +- **Supersedes / superseded by**: Superseded by RFC 0036; replaces the engine + of RFC 0030 (`FunctionDataflow`, RFC 0030 §15) and the summary format of RFC + 0030 §9.1 and §13.1 (summary format 29 becomes 30, unit record format 28 + becomes 29). Amends RFC 0030's assumption A3 (*Soundness*, below), its §3 + certainty rules (restated over objects in §5), its §9.4 boundary facts + (§5.6), its §14 seam (§8), and takes over its open gates G10, G14 and G15. + Amends RFCs 0002–0017 wherever they describe how `FunctionDataflow` + represents a fact; what those RFCs decide about outcomes and diagnostics + stays in force as RFC 0030 restated it. + +> Superseded by [RFC 0036](0036-deterministic-heap-reuse.md), which +> deletes the advisory analysis this RFC specified. The RFC was Implemented +> until then (as amended by [RFC 0032](0032-runtime-enforcement.md): unit +> record format 29 becomes 30, the place class of array-element cells at a +> boundary (§4.9) is the one RFC 0032's first amendment states, and the part +> of gate G6 that runtime extents and liveness close is closed there); it is +> kept as a record of the design. This RFC was drafted before implementation. On 2026-09-28 the project owner accepted the recommendation of the milestone analysis (replace the engine diff --git a/docs/rfcs/0035-guard-by-default.md b/docs/rfcs/0035-guard-by-default.md index c1d4d392..fc6fa556 100644 --- a/docs/rfcs/0035-guard-by-default.md +++ b/docs/rfcs/0035-guard-by-default.md @@ -16,6 +16,16 @@ the shadow encoding, the slow path, stack objects, library-call guards) and §6 (confirmed errors as build failures). The analysis those RFCs define (RFCs 0030 §2–§9, 0031) stays, as an advisory analysis (§8). + Superseded in part by RFC 0036: §3.2's layout (amended), §5.1's + quarantine, §8 and the analysis parts of §7, §10 and §11. + +> [RFC 0036](0036-deterministic-heap-reuse.md) supersedes this RFC where +> they differ: a released block is reused only after a sweep finds no +> pointer to it (§5.1's quarantine), locals whose scopes never overlap share +> frame space (§3.2), and the advisory analysis is deleted (§8, and the +> analysis parts of §7, §10 and §11). The guards, the shadow, frames and +> globals, the library-call guards, the removal rules, the modes and the +> ledger stay, as RFC 0036 amends them. This RFC was drafted before implementation. On 2026-10-06 the project owner accepted the recommendation of the milestone analysis (invert the diff --git a/docs/rfcs/0036-deterministic-heap-reuse.md b/docs/rfcs/0036-deterministic-heap-reuse.md new file mode 100644 index 00000000..1ecbb908 --- /dev/null +++ b/docs/rfcs/0036-deterministic-heap-reuse.md @@ -0,0 +1,728 @@ +# RFC 0036: Deterministic heap reuse + +- **Status**: Accepted +- **Authors**: WeaveC authors +- **Created**: 2026-10-07 (revised 2026-10-08) +- **Tracking issue**: TBD +- **Supersedes / superseded by**: Supersedes RFC 0035 §5.1's quarantine + (released blocks reused after a byte budget), §8 (the advisory analysis) + and the analysis parts of RFC 0035 §7, §10 and §11; the analysis of RFCs + 0001–0017, 0030 §2–§9 and 0031, which is deleted; RFC 0035's *Future + work* entry for RFC 0036 (ownership as checked contracts at function + entry), which is withdrawn. RFC 0035's guards, shadow, frames, globals, + library-call guards, removal rules, modes and ledger stay, amended below. + +This RFC was drafted before implementation. On 2026-10-07 the project +owner accepted the recommendation of a milestone analysis and authorized +drafting this RFC and implementing it in one large change, with breaking +changes and no compatibility layers. + +**History of this RFC.** Its first draft (2026-10-07, *Deterministic memory +safety*) also made spatial safety and use after return deterministic: +every access checked against the object of the pointer it was computed +from, computed pointers checked when they leave their function, and the +locals a guard can reach moved into *frame blocks* allocated from the +arena. That draft was implemented end to end on the branch +`rfc0036-deterministic-safety` and stopped at its cost gate: the corpus +benchmarks ran at 3.08 times the reference compiler (geometric mean), +twice v0.16.0's 1.53, QuickJS at 10.8. A measurement of each mechanism +(*Alternatives*) found that reuse after a sweep, the part that makes use +after free deterministic, is cheap once two fixable costs are removed, and +that provenance bounds and frame blocks are not, at any design point the +branch tried. On 2026-10-08 the owner accepted the recommendation to ship +the cheap part with the deletions and the drop-in fixes, and to leave +spatial and use-after-return determinism to a later RFC with a cost budget +as its first requirement. This revision is that scope; the first draft's +measurements are kept below. + +## Summary + +RFC 0035 guards every memory access against a shadow memory that says +which bytes belong to a live object. Measured on v0.16.0, the guards stop +the bugs AddressSanitizer stops, at or below its cost, and untuned +projects build and pass. But a use after free is stopped only while the +freed block waits in a 16 MiB quarantine: a program that allocates enough +after the release gets the block back for a new object, and the stale +pointer then reads and writes the new owner's data without a report. A +double free after reuse frees the new owner's block. + +This RFC: + +1. **Reuses a released heap block only after a sweep.** A released block + waits in the quarantine until a *sweep* (the program's threads stopped; + their stacks and registers, the process's writable data and every live + heap block scanned conservatively) finds no pointer to it. A use after + free or a double free through any pointer the program still holds traps, + however much it allocated since. Sweeps are paced so that they take a + bounded share of the run time. +2. **Checks a fortified call's object size.** `__strcpy_chk(d, s, 8)`, which + Clang emits for `strcpy` into an 8-byte field when the C library + fortifies, carries the field's size; the guards check it, where v0.16.0 + dropped it and lost an overflow inside a struct that plain Clang stops. +3. **Honours ASan's contract.** Code compiled by `weavec-cc` sees + `__SANITIZE_ADDRESS__` and `__has_feature(address_sanitizer)`, and the + runtime implements ASan's poisoning interface, so that code written to be + ASan-clean (a pool that poisons its free space, an interpreter that sends + every object to `malloc` under ASan, a scan that reads past an object on + purpose under `#if !ASAN`) is WeaveC-clean and gets the same detection. +4. **Shares stack space between locals whose scopes never overlap**, as + Clang does: v0.16.0 gave each local a guard can reach its own redzoned + slot, which grew frames 5–25 times and overflowed deep recursion. +5. **Fixes what drop-in found**: globals of shared libraries and bundles get + their redzones in the process's shadow; the `strto*` and `ato*` families + no longer require a terminator they never read. +6. **Deletes the advisory analysis**: the `weavec` tool, + `-fweavec-diagnose`, `weavec --whole-program`, `lib/Analysis`, all of + `lib/Core` but the library table, every annotation but `WEAVEC_UNSAFE`, + their diagnostics, tests and documentation. Measured on untuned code it + reported no bug worth fixing in 69 findings and found none of the bugs + the guards miss. + +## Motivation + +Measured on main at `f654555` (v0.16.0), release build, darwin-arm64 +(Apple M3), with the reference `clang` 23.1.0 and ASan +(`-O2 -fsanitize=address`). + +### What the guards miss + +Small programs, each a bug with an attacker-controlled index or a stale +pointer (the cases of `test/cases/determinism` pin them): + +| Bug | v0.16.0 | ASan | This RFC | +| --- | --- | --- | --- | +| Use after free after ~28 MiB of live allocations reclaimed the block (a function pointer called through it) | SIGSEGV later, no report | `heap-use-after-free` | `heap-use-after-free` | +| Double free after the block was reused | frees the new owner's block | passes | `double-free` | +| `strcpy(u->name, long)` into an 8-byte field followed by `int admin` | passes; `admin` overwritten | passes | `intra-object-overflow` | +| The same with plain `clang -O2` (Darwin fortifies it to `__strcpy_chk(d, s, 8)`) | | traps | | +| A global overflow inside a dylib or bundle | passes (the same code in the executable traps) | `global-buffer-overflow` | `global-buffer-overflow` | +| A pool that poisons its free space through ASan's interface, used after release | passes | `use-after-poison` | `use-after-poison` | +| Heap index from `a` that lands in a neighbouring live block `b` | passes; `b` corrupted | passes | passes (later RFC) | +| Read through a `char *` from one global into the next | passes | passes | passes (later RFC) | +| Use after return (`return &local`) | passes | `stack-use-after-return` (with its fake stack) | passes (later RFC) | + +The fortified call and the dylib global are WeaveC bugs, not design limits: +the guard pass redirects `__strcpy_chk` to its own checked `strcpy` and +drops the field size, and a shared image registers its globals in a shadow +of its own (below). + +### Drop-in on large untuned programs + +CPython 3.13.7, SQLite 3.50.4 (shell, `speedtest1`, Tcl `testfixture`) and +GNU coreutils 9.5, none tuned for: configure results identical to the +reference compiler's; builds 1.17–1.68× the reference's time (ASan +≈1.5–2.2×). Their suites pass but for: + +- **Globals of shared images are unprotected on Darwin.** The constructor + that registers a unit's globals (priority 1) runs before the copy of the + runtime in that image has found the process's owner, so it reserves a + shadow and an arena of its own (about 320 GiB of address space per + image), poisons the redzones there, and then switches to the owner's + shadow, where they are gone. Past six extension modules CPython prints + `the heap lies outside the shadow's window` once per module, which + alone fails eight of its test files. +- **Frames grow 5–25×.** Every escaping local gets its own 32-byte-aligned + slot and redzone in one frame, so locals in disjoint `switch` arms no + longer share stack space (`obj2ast_expr` 176 → 4,352 bytes, + `_PyEval_EvalFrameDefault` 480 → 2,240). CPython segfaults at a C + recursion depth where the reference raises `RecursionError` + (`test_ast`, `test_functools`). +- **`strtoumax` traps on correct input** (`tr`): the `strto*` and `ato*` + rows require a terminated string, but the functions read only the + number they parse and the character that stops it. + +Run time stays below ASan's: `speedtest1` 1.55× (ASan 1.81×), CPython +micro-benchmarks 1.54× (4.7×), Lua 2.10× instructions (2.73×); peak memory +1.2–1.5× (ASan 2.4–6×). + +### What the analysis contributes + +The advisory analysis is 33,230 lines in `lib/Analysis`, 13,698 in +`lib/Core` and 1,987 in `lib/Frontend` (85% of the production C++), with +16,322 lines of unit tests and 7,138 of lit tests. On four untuned +projects (libpcap, c-ares, nginx, libssh2; 314 units) `weavec` reported 0 +errors and 69 warnings; every one was read: 0 actionable, 2 true but +harmless, 5 unclear, 62 false (lists and loops taken as one object, +allocation only on success not modelled, callbacks taken to free, +ownership stored into containers). `--whole-program` took c-ares from 1 to +169 warnings and libssh2 from 2 to 70 (20 sampled: all false). On the 109 +bug programs of `test/cases/detection{,-blind}` the guards stop 104, the +Clang static analyzer 36 and `weavec` 27, none of the five the guards +miss; its only findings no dynamic check can make are leaks, which the +Clang static analyzer also reports. The guard passes use none of it but +the library table. + +The roadmap's next step, RFC 0036 as *ownership as checked contracts*, +would put that analysis back into what the build enforces (a contract it +infers removes a guard), which is what RFCs 0030–0034 did and RFC 0035 +undid. This RFC withdraws it. + +## Soundness + +### The guarantee + +For a program whose every unit is compiled by `weavec-cc` in an enforcing +mode (`trap` or `verify`; `report` with `WEAVEC_RT_ABORT=1`), with the +runtime linked, every memory access outside a `WEAVEC_UNSAFE` region +satisfies (A), (B) and (R) of RFC 0035, and: + +- **(T) Temporal.** A byte of a released heap block is not addressable + until a sweep has found no pointer to the block in the program's stacks, + registers, writable data and live heap. An access through a pointer the + program held when the block was released therefore traps, however much + the program allocated since; a release of a block already released + traps. +- **(F) Fortified sizes.** A fortified library call writes no more than + the size its `__builtin_object_size` argument states, or the program + traps. +- **(P) Poisoning.** A byte the program poisoned through ASan's interface + is not addressable until it unpoisons it. + +**What the guarantee means for the bug classes:** + +- **Use after free, double free** of the heap: trap through any pointer + the program holds in memory or registers. A pointer the program hides + (stored as an integer it transforms, XOR-linked, written to a file or + held only by the kernel) does not keep its block from reuse. +- **Heap, stack and global overflow**: as RFC 0035 — a contiguous overflow + traps; an index that jumps over a redzone into another live object + passes. +- **Use after scope**: traps, as before. **Use after return**: passes, as + before. +- **Overflow inside an object**: by an index into a declared array (B), or + by a fortified string or `printf` call into a field (F); not by pointer + arithmetic. +- **Memory of a custom allocator**: tracked when the allocator poisons its + free space through ASan's interface (P), or sends its objects to `malloc` + when it sees `__SANITIZE_ADDRESS__`; otherwise untracked (A1). + +### Assumptions + +RFC 0035's A1–A5 stay, with these changes: + +- **A1** (untracked memory) also covers memory the program allocates with + its own allocator and does not poison through ASan's interface. +- **A6 — visible pointers.** A pointer to a heap block is stored, while + the program may still use it, as a whole aligned pointer in memory the + sweep scans (any readable and writable mapping), or in a register. +- **A7 — the runtime sees the threads.** On Linux, every thread accepts + the sweep's signal (§3.4) within a second; a thread that blocks it holds + up recycling (nothing is reused until it accepts), never safety. + +### Where this is more sound than RFC 0035 + +The use after free, the double free, the fortified field overflow, the +dylib global and the poisoned pool of the motivation's table trap. The +quarantine is no longer a budget a program can exhaust. + +### Where this is less sound than RFC 0035 + +Nowhere. A sweep that cannot stop every thread, or finds a thread it did +not stop, recycles nothing. + +### Bugs deliberately not caught + +Pointer jumps over a redzone into another live object, by index or by +arithmetic (a later RFC); use after return (a later RFC); a pointer jump +inside one object by arithmetic; uses of a block through a pointer the +program hid (A6); data races between a guard and its access (A3); accesses +to untracked memory (A1); anything in a `WEAVEC_UNSAFE` region or in code +`weavec-cc` did not compile. + +### Accepted false traps + +- **ASan's own accepted false traps** stay accepted: a scan that reads + past an object on purpose traps unless the code keeps it from ASan (now + honoured, §5) or marks it `WEAVEC_UNSAFE`. +- **Code paths taken under ASan.** A program that behaves differently when + it sees `__SANITIZE_ADDRESS__` behaves as it does under ASan; one that + calls a sanitizer interface the runtime does not implement fails to link + (`-fno-weavec-asan-compat` builds it as v0.16.0 did). + +### Cost + +A sweep stops the program for a time that grows with its live heap and +its writable data; pacing (§3.3) keeps the share of the run time sweeps +take bounded, and the quarantine grows between sweeps instead, up to a +ceiling. A fortified call costs one compare. *Acceptance gates* bound run +time, memory and pauses. + +## Detailed design + +### 1. The pipeline + +As RFC 0035 §1 and its amendment 1 (GuardInsert, GuardPrune, GuardLoop, +GuardExpand), with these additions: + +1. **Fortification.** On glibc targets, when the command line neither + defines nor undefines `_FORTIFY_SOURCE` and optimises, `weavec-cc` + defines `_FORTIFY_SOURCE=3`. (Darwin's SDK fortifies at level 2 by + default when optimising.) +2. **ASan's contract** (§5): the unit is parsed with AddressSanitizer + enabled in its language options, so that + `__has_feature(address_sanitizer)` is true, `__SANITIZE_ADDRESS__` is + defined and `no_sanitize("address")` is accepted; before the backend + runs, AddressSanitizer is disabled again so its instrumentation never + runs, and GuardInsert removes the `sanitize_address` function attribute. + Clang predefines `_FORTIFY_SOURCE=0` under ASan on Darwin; the driver + undoes that. `-fno-weavec-asan-compat` keeps the language options as the + command line set them. + +### 2. Stack frames + +RFC 0035 §3 stays (one frame `alloca` with redzones, scopes written from +lifetime markers, dynamic allocas, the unpoisoning before `noreturn` +calls), with one change to §3.2: + +**Shared space.** Two tracked static allocas may share bytes of the frame +when their lifetimes never overlap: LLVM's `StackLifetime` analysis (the +one SafeStack uses), in its *may be live* mode, over the lifetime markers +that remain after the optimiser. An alloca without lifetime markers is live +for the whole function and shares with none. The layout places the +allocas in decreasing order of size, each at the lowest 32-byte-aligned +offset after the left zone where its *extent* (the object and its +redzone, as RFC 0035 §3.2 sizes them) overlaps the extent of no alloca +already placed that is live at the same time as it. The frame is the +largest end of an extent, rounded to 32. + +Shadow: at entry the frame's shadow is written as before, each granule +taking the value of the alloca live at entry that covers it (addressable +or partial for its object, `0xF2` for its redzone), else `0xF8` when an +alloca's object covers it, else `0xF2`. A lifetime start of a shared alloca +writes its object's granules addressable (as before) and its redzone's +granules `0xF2`; a lifetime end writes `0xF8` over its object, as before. +Two allocas that share bytes are never live together, so the last write +of a granule is always the live alloca's. At `-O0` (no lifetime markers) +nothing shares, as in v0.16.0. + +### 3. The heap + +#### 3.1 The slot word + +Unchanged from RFC 0035 §5.1: `size << 2 | state`. + +#### 3.2 Release + +`free` and `realloc` validate as before (R); a released slot's shadow +becomes `0xFD` and it joins its class's *quarantine*. It is never reused +from there: only a sweep moves it to the free list. A huge block's mapping +is replaced by an inaccessible reservation at release, as before, and +unmapped only by a sweep that finds no pointer to it. + +#### 3.3 When a sweep runs + +Let `q` be the quarantine's bytes, `live` the bytes of live heap blocks, +`Q` the configured quarantine (`WEAVEC_RT_QUARANTINE`, 16 MiB by default), +`S = max(Q, live / 4)` the *soft* budget and `H = max(16 Q, live / 2)` the +*hard* budget. A release (or an allocation) sweeps before it returns when + +- `q ≥ H`; or +- `q ≥ S`, and the time since the last sweep ended is at least eight times + that sweep's duration (so sweeping takes at most about a ninth of the + run time while the program frees faster than sweeps recycle); or +- a class's region is exhausted. + +Until the sweep, allocations take fresh slots. The pacing is checked only +when the quarantine has grown by a sixteenth of `S` since the last check. + +#### 3.4 The sweep + +1. **Stop.** Take the sweep lock and every allocator lock (as `fork` + does), then stop every other thread: on Darwin with `task_threads` and + `thread_suspend`, reading each thread's registers with + `thread_get_state`; on Linux by signalling each task of + `/proc/self/task` (`SIGPWR`, or `WEAVEC_RT_SWEEP_SIGNAL`), whose handler + records its stack pointer (the kernel saved its registers on its stack) + and waits. A thread that does not stop within a second ends the sweep + without recycling (A7). +2. **Mark.** Scan conservatively, word by aligned word, for addresses of + quarantined slots and released huge blocks (interior addresses count): + - each thread's registers and stack from its stack pointer (less the + ABI's red zone) to the end of the mapping that holds it; + - every other readable, writable mapping of the process but the + runtime's own (the shadow, the arena and its metadata, the mark + bitmaps, the tables) and the stacks; on Darwin, only the pages that + are anonymous or modified (`mincore`), and of the dyld shared cache + only the writable segments of the images the process loaded (a table + the runtime keeps from dyld's add-image callback), not the cache's + whole writable mapping; + - the bytes of every live heap block and live huge block. + + Released blocks are not scanned (a pointer in a released block is + reachable only through a use after free). Marks go to a bitmap per + class. +3. **Sweep.** Each quarantined slot without a mark moves to its class's + free list; marked slots stay. Released huge blocks without a mark are + unmapped. +4. **Resume** the threads and release the locks. + +The sweep calls nothing that takes a lock another thread could hold while +stopped (no allocation, no stdio, no dyld). `WEAVEC_RT_STATS=1` adds the +number of sweeps, their total and longest pause, and the blocks and bytes +retained by the last one. + +### 4. Globals + +RFC 0035 §4 stays. `__weavec_rt_globals_register` and `_unregister` first +find the process's owner (on Darwin), so that a shared image's globals are +poisoned in the owner's shadow; until the owner is resolved, a copy of the +runtime reserves nothing. + +### 5. ASan's contract + +The runtime defines, with ASan's signatures and meaning: +`__asan_poison_memory_region` (whole granules inside the region become +`0xF7`), `__asan_unpoison_memory_region`, `__asan_address_is_poisoned`, +`__asan_region_is_poisoned`, `__asan_describe_address`, +`__asan_handle_no_return` (clears the stack's shadow, as before a +`noreturn` call), `__asan_get_current_fake_stack` and +`__asan_addr_is_in_fake_stack` (none), `__sanitizer_print_stack_trace`, +`__sanitizer_set_report_path`, `__sanitizer_annotate_contiguous_container`, +`__sanitizer_start_switch_fiber`, `__sanitizer_finish_switch_fiber`, and +LeakSanitizer's `__lsan_ignore_object`, `__lsan_disable`, `__lsan_enable`, +`__lsan_register_root_region`, `__lsan_unregister_root_region`, +`__lsan_do_leak_check` and `__lsan_do_recoverable_leak_check` (no leak +checking: they do nothing). A function marked `no_sanitize("address")` +stays an unsafe region (RFC 0035 amendment 8). A poisoned byte reports +`use-after-poison`. + +### 6. Checked library calls + +- **Fortified forms.** A call of a `chk` alias whose object-size argument + is not the constant `(size_t)-1` checks (F): an inline guard checks that + its byte count is at most the size; a redirected call passes the size to + its wrapper (every writing wrapper takes a `limit` before its site), which + checks what it writes against it. A failure within addressable bytes + reports `intra-object-overflow`. +- **Number parsing.** `strtol`, `strtoll`, `strtoul`, `strtoull`, + `strtoimax`, `strtoumax`, `strtod`, `strtof`, `strtold`, `atoi`, `atol`, + `atoll` and `atof` are redirected to wrappers that call the C library and + then check the bytes it read: up to the end pointer and the character + that stopped it, or, when nothing was converted, the leading white space + and sign and the character after them (ASan's rule). +- **The library table** keeps only what the guards read (access, bytes, + count, str, null, release and realloc, wrapper, wide, variadic, format, + disjoint, value, chk); the analysis's clauses go with the analysis. + +### 7. Modes and options + +RFC 0035 §7 stays but: + +- Deleted, with no aliases: `-fweavec-diagnose`, `-fweavec-budget=`, + `-fweavec-unit-budget=`, `-fweavec-dump-analysis`, + `-fweavec-analysis-stats=`, and the `-W…weavec…` flags (they reach Clang + as unknown warning options). +- New: `-fno-weavec-asan-compat` (§1). +- Zero-initialisation keeps its 4 KiB limit: a stale pointer in an + uninitialised local is a pointer the sweep sees, so it cannot reach a + reused block (A6). + +### 8. Deletions + +- **Tools and libraries**: `tools/weavec`; `lib/Analysis` and + `include/weavec/Analysis` (`Annotations.h`'s `WEAVEC_UNSAFE` spelling + moves to `UnsafeRegions`); `lib/Core` and `include/weavec/Core` but the + library table (`LibrarySpec`), trimmed to what the guards read; + `lib/Frontend`'s `FrontendAction`, `ProgramAnalysis`, `ProgramChecks`, + `InterfaceFacts`, `AnalysisSummary`, `AnalysisStats`, + `ClangDiagnosticSink` and `DiagnosticControl`; the driver's analysis + flags. +- **The header**: `weavec.h` keeps `WEAVEC_UNSAFE` (and its version + macros); every other annotation is deleted. +- **Tests**: `test/Analysis`, `test/WholeProgram`, the analysis parts of + `test/Annotations`, `unittests/Analysis`, the `unittests/Core` tests but + `LibrarySpecTest`, the `unittests/Frontend` tests of deleted files; the + case runner's analysis (`BUG`, `MISS` on a `BUG`, `NEUTRALISED`, `ALLOW`, + `TOOL`); the case suites that pin only the analysis; the corpus gate's + analysis findings, triage entries and whole-program configs; the + documentation examples of `weavec`. +- **Documentation**: `docs/annotations.md`'s annotations but + `WEAVEC_UNSAFE`, `docs/data/diagnostic-remedies.json`, the diagnostic + reference, the guides of ownership, whole-program analysis and + annotations, and the internals pages of the analysis. +- **Runtime**: the byte-budget recycling of the quarantine. + +The RFCs are kept; RFCs 0001–0017 and 0031 are marked Superseded by this +RFC, and RFC 0030 keeps Implemented with its analysis superseded. + +### 9. Tests + +- **Cases.** `test/cases/determinism/` holds a case per row of the + motivation's table, each with a correct twin: a heap-spray use after + free, a use after free and a double free after churn, the field + `strcpy`, a pool poisoned through ASan's interface, deep recursion + through escaping locals in disjoint scopes (no stop at the reference's + depth), and the three bugs this RFC leaves to a later one as expected + misses (`MISS`). `BUG` lines of the remaining suites become `TRAP` where + the default build stops on them; analysis-only cases are deleted. +- **Lit.** `test/Guards/` pins the IR of the shared frame layout and each + new report line; `test/Driver/` the fortification define, ASan's macros, + and a dylib's and a bundle's global overflow. +- **Runtime test.** `rt_test.c` covers the sweep (single and + multi-threaded, a retained block, a recycled one, pacing), the image + table, the ASan interface and the number parsers. + +## Annotation surface + +`weavec.h` keeps `WEAVEC_UNSAFE` (function and block forms) and its +version macros, and moves to version 1.0; every other macro is deleted +(`WEAVEC_OWNED`, `WEAVEC_BORROWED`, `WEAVEC_MUT`, `WEAVEC_RAW`, +`WEAVEC_NULLABLE`, `WEAVEC_NONNULL`, `WEAVEC_COUNTED_BY`, +`WEAVEC_ENDED_BY`, `WEAVEC_STRING`, `WEAVEC_ASSUME`, the reference-counting +forms and the rest). A program that uses one fails to compile with +`weavec-cc` and other compilers alike until it stops; the macros expanded +to nothing elsewhere. + +## Diagnostics + +Every `weavec::core::diag` id is deleted with the analysis. New run-time +report kinds (not diagnostics; RFC 0035 §5.3): `intra-object-overflow`, +`use-after-poison`. + +## Implementation plan + +The stages land on `rfc0036-deterministic-safety`, on top of the first +draft's implementation, and ship as one change: + +- **S0 The RFC**: this revision. +- **S1 Remove the first draft's spatial and frame mechanisms**: + provenance bounds, escape checks and the escape registry, pointer slots, + frame blocks, frame regions and capture logs, the `setjmp` hook, the + globals table and `__weavec_rt_bounds`; RFC 0035's native frames back. +- **S2 Shared frame space** (§2). +- **S3 The sweep's cost**: pacing (§3.3) and the image table (§3.4). +- **S4 Cases, lit and runtime tests** for the revised scope. +- **S5 Fresh iterations**: the motivation's projects, the corpus, the + gates, the documentation; Linux through CI. + +## Acceptance gates + +Binaries are the `release` preset's on the final tree; the reference is +`$WEAVEC_LLVM_PREFIX/bin/clang`; v0.16.0 is built from its tag on the same +machine; darwin-arm64 unless named; ratios of best-of-three user CPU on an +idle machine. + +**Detection** + +- **G1. Determinism.** Every `_bug` case of `test/cases/determinism` that + this RFC catches stops with its report kind and its twin runs clean, in + trap and verify mode, on darwin-arm64 and linux-x86_64. +- **G2. No loss.** The detection, detection-blind and soundness suites stop + at least as many bugs as v0.16.0, with no twin stopping. +- **G3. Monitors.** No `weavec.proven` report in verify mode on any case. + +**Drop-in** + +- **G4. Untuned programs.** CPython 3.13.7 (`test_ast`, `test_functools` + and the motivation's subset), SQLite 3.50.4 (`veryquick`) and coreutils + 9.5 (`make check`) build with `CC=weavec-cc` and pass as the reference + does, but for tests that misuse memory on purpose and that the project + skips under ASan; CPython's extension modules' globals are protected. +- **G5. No regression.** The corpus configs build and pass their suites + (`scripts/corpus-gate.py --full`). + +**Cost** + +- **G6. Run.** The corpus benchmarks' geometric mean at most 1.10× + v0.16.0's, and no benchmark above 1.25× its v0.16.0 ratio. +- **G7. Memory.** Peak RSS of each benchmark at most 1.5× v0.16.0's or 64 + MiB above it, whichever is larger. +- **G8. Pauses.** On a benchmark with 256 MiB of live heap, the longest + sweep at most 150 ms. +- **G9. Build.** No project's build more than 1.1× v0.16.0's. + +**Hygiene** + +- **H1.** `scripts/check-hygiene.py` passes; the library (C++ under + `lib/`) is at most 9,000 lines. +- **H2.** The documentation describes the change (README, guarantees, CLI, + architecture, roadmap, AGENTS.md); `npm test && npm run build` in + `docs/`; this RFC is marked Implemented. + +## Implementation amendments + +These record where the implementation departs from the design above. They +override the body. + +1. **The budgets (§3.3).** `q` is the bytes the quarantine gained since the + last sweep: the blocks the last sweep retained (a stale word still points + to them) are not counted, or a program that keeps dangling pointers would + sweep on every release. The hard budget is `max(2 Q, live / 2)`, not + `max(16 Q, live / 2)`: with the larger floor pacing let libgit2's + quarantine reach 116 MiB beside 86 MiB of live heap, and its peak memory + 1.8 times v0.16.0's; with the smaller one 82 MiB, 1.5 times, for 3% more + time. `WEAVEC_RT_STATS=1` prints the peaks of both. +2. **The shared layout (§2).** The allocas are placed in RFC 0035's order + (larger alignments first, then source order), not by decreasing size, + so that a frame whose locals all live throughout is laid out as before. + A lifetime start writes its local's redzone again only when that redzone + overlaps another local's object; elsewhere the redzone is never + written over. The shadow of a granule two locals share is `0xF8` at + entry. +3. **The roots on Darwin (§3.4).** Walking the dyld shared cache's writable + mappings page by page cost 12–25 ms per sweep whatever the heap (cJSON: + 93 sweeps, 1.1 s of 3.2 s), and so did two mappings outside it: the + kernel's reclaim ring of the system allocator (64 MiB, tag + `VM_MEMORY_VM_RECLAIM`, never a program pointer) and the stack below its + stack pointer. The sweep now asks each mapping for its counts of dirty + and swapped-out pages (one call per mapping) and skips a mapping with + none and the reclaim ring; scans a stack from its stack pointer without + asking which of its pages are resident; and scans the writable segments + of the cached images the process loaded, from dyld's add-image and + remove-image callbacks, instead of the cache's mappings (3.4 MB of + segments against about 200 MB). A sweep with a small heap takes about a + millisecond. +4. **Reuse in address order (§3.4).** A sweep rebuilds each class's free + list and quarantine by walking its slots from the last, so that slots + are reused, and their words read, in address order. Reuse in the + quarantine's order after a sweep scattered allocations over the region + (cJSON's allocation path missed the cache). +5. **Released large slots (§3.2).** A released slot of at least 64 KiB + gives its whole pages back at once: on Darwin by mapping fresh pages over + them, since `MADV_FREE` leaves them resident until the system needs + memory, and the slot now waits for a sweep while the program takes fresh + ones. Without it brotli's peak memory was 2.2 times v0.16.0's. +6. **Small shadow writes (§3).** An allocation and a release write a small + object's few shadow bytes with stores rather than a call of `memset`. +7. **The sweep tests.** A conservative scan keeps a block that a stale copy + of its address in an unused stack slot points to. The runtime test's + sweep cases scrub the stack below them with a compiler barrier (the + optimiser removed the stores to a dead array, and with them the scrub), + and use size classes no other test touches. +8. **Shared space only in large frames (§2; *Where this is less sound + than RFC 0035*).** Sharing has a cost the body missed: a use after scope + that lands in bytes a later local now owns passes. In verify mode, where + every local a marker names is tracked, a detection case's temporary + (`int tmp[4]`) shared its bytes with the next block's `int other[16]`, + and the read through the stale pointer passed (ASan keeps such locals + apart for the same reason). Locals now share bytes only in a frame that + would be larger than 512 bytes without sharing: below that the redzones + cost little, and every local keeps its own bytes as in RFC 0035. So RFC + 0036 is less sound than RFC 0035 in one place: in such a large frame, a + use after scope that reaches a later local's bytes passes. + +## Measured gates + +Measured 2026-10-09 on the branch (darwin-arm64, Apple M3, `release` +preset, reference `clang` 23.1.0; v0.16.0 built from its tag on the same +machine). The RFC stays **Accepted**: linux-x86_64 is not measured (the +local Docker daemon does not answer; CI on the pushed branch measures it), +and G6 holds for every benchmark but QuickJS, whose excess is the cost of +the ASan contract (below). + +| Gate | Result | +| --- | --- | +| G1 Determinism | **Pass on darwin-arm64**: the seven cases of `test/cases/determinism` this RFC catches stop with their kinds and every twin runs clean, in trap and verify mode; the four left to a later RFC are `XFAIL`. linux-x86_64 **not measured**. | +| G2 No loss | **Pass**: the detection and detection-blind suites stop 105 of 109 bugs (v0.16.0: 104; the first draft: 106), and the eight detection cases of `semantics/` all stop; no twin stops; every case passes but 11 `XFAIL` (533 pass). | +| G3 Monitors | **Pass**: no `weavec.proven` report on any case in verify mode. | +| G4 Untuned programs | **Pass**, with one finding shared with ASan. CPython 3.13.7: `pyconfig.h` identical to the reference's; no warning at build or run (v0.16.0 printed 70 for extension modules); `test_ast`, `test_functools`, `test_regrtest` and the 21-file subset pass (6,364 tests); the full suite fails, run file by file, the same files as the reference build on this machine but `test_faulthandler`, whose `test_read_null` reads null on purpose and gets WeaveC's report. SQLite 3.50.4 `veryquick`: 24 errors in 329,057 tests, the same five files as the reference (Tcl 8.5); `capi3c` passes (SQLite skips its deliberate misuse under ASan). coreutils 9.5: `config.h` identical; `make check` 471 pass against the reference's 475: `tr` passes (the number parsers), and `help-version`, `csplit`, `shuf` and `tty-eof` stop at gnulib's word-at-a-time `rawmemchr` over-read, which ASan stops too (verified). | +| G5 No regression | **Pass**: every corpus config builds and passes its suite in trap and report mode, with no trap (`scripts/corpus-gate.py --full`); the injections stop 33 of 39, the same six misses as v0.16.0; jansson's four triaged guard failures are gone (it takes its exact path under ASan's macros). | +| G6 Run | **Geometric mean passes**: 1.05 times v0.16.0's (1.61 against 1.54 the reference; best of two runs of three). Every benchmark is within 1.25 times its v0.16.0 ratio but QuickJS, at 1.31 (2.37 against 1.80): under `__SANITIZE_ADDRESS__` QuickJS sends every object to `malloc` (8.3 million allocations instead of 76,000), so its objects are tracked one by one. Built the same way (`-D__SANITIZE_ADDRESS__` for v0.16.0), v0.16.0 takes 3.2 s and this tree 2.72 s; with `-fno-weavec-asan-compat` it is 1.06 times v0.16.0's. The others: cJSON 1.04, zlib 0.97, Lua 0.97, LMDB 0.95, Janet 1.05, brotli 1.13, xz 0.95, libdeflate 1.00, zlib-ng 0.99, curl 0.96, cmark 1.18, libgit2 1.17 (the hard budget of amendment 1 cost cmark, libgit2 and brotli 5–10 points). | +| G7 Memory | **Pass** on every benchmark (peak RSS, MiB, against v0.16.0's): cJSON 89 (71), zlib 3 (3), Lua 144 (176), QuickJS 202 (142; limit 214), LMDB 214 (210), Janet 59 (47), brotli 162 (441), xz 98 (98), libdeflate 105 (105), zlib-ng 3 (3), curl 32 (31), cmark 365 (473), libgit2 170 (117; limit 181). `scripts/corpus-gate.py --bench` now records each build's peak RSS. | +| G8 Pauses | **Pass**: 33 ms longest sweep with 256 MiB of live heap (7 sweeps). | +| G9 Build | **Pass**: compile CPU of the corpus gate's units 0.86 times v0.16.0's in total; no project above 1.07 times (curl), the worst unit 1.42 times (llhttp.c, 0.46 s). | +| H1 | **Pass** (`scripts/check-hygiene.py`; library 7,200 lines, runtime 5,000). | +| H2 | **Pass** (README, guarantees, CLI, compatibility, troubleshooting, architecture, roadmap, AGENTS.md; `npm test`, `npm run build`). | + +## Drawbacks + +- **WeaveC no longer reports bugs before the program runs.** Teams that + want static findings use the Clang static analyzer or another tool. +- **Pauses.** A sweep stops every thread; a large live heap pays for each + one. Concurrent marking is future work. +- **Memory.** Between sweeps the quarantine may grow to the hard budget + (§3.3); a block some stale word points to stays allocated until the word + changes. +- **A signal on Linux.** The sweep uses one signal; a program that uses + `SIGPWR` itself sets `WEAVEC_RT_SWEEP_SIGNAL`. +- **The ASan contract** can take code paths written for ASan, with their + cost (QuickJS sends every object to `malloc`), and can call sanitizer + interfaces the runtime does not implement (a link error, which + `-fno-weavec-asan-compat` avoids). + +## Alternatives + +### The first draft: provenance bounds and frame blocks + +Measured 2026-10-08 on the first draft's tree with each mechanism switched +off in turn (the corpus's 13 benchmarks, ratio to the reference; the +switched-off frames are native frames without redzones): + +| | v0.16.0 | none of them | frame blocks only | bounds only | first draft | +| --- | --- | --- | --- | --- | --- | +| cJSON | 1.89 | 2.49 | 2.66 | 2.67 | 2.81 | +| zlib | 1.26 | 1.27 | 1.28 | 1.83 | 1.83 | +| Lua | 2.25 | 2.36 | 2.89 | 4.03 | 5.00 | +| QuickJS | 1.78 | 3.01 | 8.89 | 5.68 | 10.79 | +| LMDB | 1.23 | 1.28 | 1.87 | 2.23 | 2.71 | +| Janet | 1.36 | 1.41 | 2.33 | 2.26 | 3.27 | +| brotli | 1.89 | 2.33 | 2.34 | 3.30 | 3.37 | +| xz | 1.30 | 1.28 | 1.33 | 2.10 | 2.06 | +| libdeflate | 1.32 | 1.32 | 1.33 | 1.56 | 1.56 | +| zlib-ng | 1.36 | 1.36 | 1.38 | 1.90 | 1.91 | +| curl | 1.82 | 1.79 | 3.02 | 2.50 | 3.56 | +| cmark | 1.69 | 2.55 | 3.93 | 3.62 | 4.84 | +| libgit2 | 1.19 | 2.11 | 2.23 | 2.63 | 2.56 | +| **geometric mean** | **1.53** | **1.80** | **2.32** | **2.62** | **3.08** | + +Bounds and escape checks cost about 46% on their own (a lookup of a root's +object is about 25 instructions and six loads, and an interpreter's loop +has many roots no loop hoists; Lua's `luaV_execute` grew from 19,600 to +64,000 instructions), frame blocks about 29% (a runtime call at entry and +exit of every function with a local a guard reaches, and QuickJS's frames +kept by pointers in their callers' frames wait for sweeps). The rest (1.53 +to 1.80) was two costs this revision removes: each sweep's fixed cost of +12–25 ms, most of it `mincore` over the dyld shared cache's whole writable +mapping (cJSON: 93 sweeps, 1.1 s of its 3.2 s; with the quarantine at +1 GiB it matched v0.16.0), and sweeps that were not paced against the run +time. The branch had tried cheaper bounds (run-time span checks: no gain; +slot-granular bounds without the slot word: 10% on Lua) before this +measurement. Meeting the first draft's cost gate (1.25× v0.16.0) would +need about two thirds of both costs gone, with no design in hand that +does it; a later RFC starts from that budget. + +### Others + +- **Ownership contracts at function entry** (the roadmap's RFC 0036): + removes guards on the strength of inferred facts, which would have to be + right where the analysis measured 62 false findings in 69. +- **A larger quarantine** (ASan's 256 MiB): makes the use after free of the + motivation pass later instead of never. +- **Fat pointers** (CHERI's model, SoftBound's): exact bounds that survive + integer round trips, at the price of the ABI. +- **Tags** (HWASan, MTE): probabilistic for far jumps without hardware, + and Darwin user space has neither top-byte tags nor MTE on the reference + machine. +- **ASan's macros off by default**: QuickJS then keeps its own small-block + allocator, whose objects the guards cannot tell apart; this RFC prefers + the detection and measures its cost (G6). + +## Prior art + +- **MarkUs** (Ainsworth and Jones, S&P 2020) and **MineSweeper** (Erdős et + al., ASPLOS 2022): quarantine released memory until a conservative mark + finds no pointer to it. +- **AddressSanitizer**: the shadow, the poisoning interface, stack slot + reuse by lifetime (`-fsanitize-address-use-after-scope` with Clang's + stack colouring). +- **`_FORTIFY_SOURCE`**: subobject sizes for string functions. +- **Boehm–Demers–Weiser GC**: stopping threads by signal, conservative + root scanning, finding roots in loaded images' data segments. +- **SafeStack**: `StackLifetime`-based sharing of a frame's objects. + +## Unresolved questions + +- **The quarantine's budgets and the pacing factor** (`live / 4`, + `live / 2`, eight times the last sweep) trade memory for time; the gates + measure them. + +## Future work + +- Spatial determinism (an access checked against its pointer's object) + and use after return, under a cost budget measured before the design: + low-fat pointers with power-of-two classes, bounds only at escapes, or + hardware tags. +- Concurrent marking, so that a sweep does not stop the program for its + whole duration. +- Hardware memory tagging (Arm MTE, Apple MIE) where the processor has it. +- Adoption: relocatable installs, packages, a vendored `weavec.h`. diff --git a/docs/rfcs/README.md b/docs/rfcs/README.md index 99583340..497d5701 100644 --- a/docs/rfcs/README.md +++ b/docs/rfcs/README.md @@ -1,24 +1,27 @@ # WeaveC RFCs -Design changes to WeaveC's ownership model, checker rules or annotation -surface go through a lightweight RFC. The RFC is the durable record of *why* -the model is the way it is; the code and tests record *what* it does. +Design changes to what WeaveC guards, how its runtime enforces it and what +it guarantees go through a lightweight RFC. The RFC is the durable record +of *why* the design is the way it is; the code and tests record *what* it +does. ## When an RFC is required Write an RFC when a change would: -- add to, remove from or change the meaning of anything in `weavec::Core` - (the lattice, places, loans, lifetimes, move tracking); -- add a checker rule or change what an existing rule accepts or rejects - (`lib/Analysis/Dataflow.cpp` and its successors); +- change which accesses get a guard, or add or change a rule that removes + one (`lib/Frontend/Guard*.cpp`); +- change the shadow encoding, frame layout, global redzones, the + quarantine or the sweep; +- change the runtime's checked library wrappers, its report kinds or their + formats, or the library table (`lib/Core/LibrarySpec.txt`); +- change the enforcement ledger's format or the driver's `-fweavec-*` flags; - add or change an annotation in `resources/include/weavec.h`; -- add a diagnostic id or change the meaning of an existing one; -- change what WeaveC guarantees, including trading soundness for precision. +- change what WeaveC guarantees, including trading soundness for cost. -Do **not** write an RFC for driver plumbing, CLI flags, diagnostics -rendering, build or CI changes, documentation, or bug fixes that bring the -implementation in line with an already-accepted RFC. Those go through normal +Do **not** write an RFC for driver plumbing, build or CI changes, +documentation, or bug fixes that bring the implementation in line with an +already-accepted RFC. Those go through normal pull requests; release-worthy changes appear in the generated `CHANGELOG.md`. ## Process @@ -30,8 +33,8 @@ pull requests; release-worthy changes appear in the generated `CHANGELOG.md`. Implementation proceeds in follow-up PRs that reference the RFC number. 3. When every item in the RFC's *Detailed design* and *Diagnostics* sections has landed with tests, a PR flips the status to **Implemented**. Lit tests - that pin RFC behaviour should carry the number in their filename - (`test/Analysis/rfc0002-*.c`) so the mapping stays greppable. + that pin RFC behaviour are named by feature (`test/Guards/.c`, + `test/cases//…`), and the pull request names the RFC. 4. An RFC that is abandoned is marked **Withdrawn**; one replaced by a later RFC is marked **Superseded** with a link. RFCs are never deleted or renumbered. @@ -47,23 +50,23 @@ decision is a new RFC that supersedes the relevant section. | RFC | Title | Status | | ---------------------------------------- | ---------------------------------- | -------- | -| [0001](0001-ownership-model.md) | Ownership, borrowing and lifetimes | Accepted | -| [0002](0002-intraprocedural-checking.md) | Sound intra-procedural checking | Implemented | -| [0003](0003-signature-inference.md) | Signature inference | Implemented | -| [0004](0004-unsafe-boundaries.md) | Unsafe boundaries: raw pointers, unsafe regions and indirect calls | Implemented | -| [0005](0005-whole-program-analysis.md) | Whole-program analysis: cross-TU summaries and the compiler driver | Implemented | -| [0006](0006-precision.md) | Precision: non-lexical loans, condition facts, element places and outcome-conditional summaries | Implemented | -| [0007](0007-resource-lifecycle.md) | Resource lifecycle: leaks, release families and owned fields | Accepted | -| [0008](0008-pointer-validity.md) | Pointer validity: null dereferences, uninitialised pointers, invalid releases and replaced values | Accepted | -| [0009](0009-value-conditional-behaviour.md) | Value-conditional behaviour: scalar facts, guarded effects and inferred `noreturn` | Accepted | -| [0010](0010-shared-ownership.md) | Shared ownership: reference counts, ownership by outcome and per-outcome facts | Accepted | -| [0011](0011-spatial-safety.md) | Spatial safety: derived pointers, extents and bounds | Accepted | -| [0012](0012-spatial-safety-strings-and-fields.md) | Spatial safety II: strings, sized fields, offset relations and assumptions | Accepted | -| [0013](0013-interprocedural-heap-state.md) | Interprocedural heap state and value identity | Accepted | -| [0014](0014-pointer-identity-and-call-effects.md) | Pointer identity and precise call effects | Implemented | -| [0015](0015-array-and-container-ownership.md) | Array elements, range operations, and container ownership | Implemented | -| [0016](0016-compositional-call-checking.md) | Compositional call checking under caller alias relationships | Implemented | -| [0017](0017-c-integer-semantics-and-spatial-safety.md) | C integer semantics and compositional spatial safety | Implemented | +| [0001](0001-ownership-model.md) | Ownership, borrowing and lifetimes | Superseded | +| [0002](0002-intraprocedural-checking.md) | Sound intra-procedural checking | Superseded | +| [0003](0003-signature-inference.md) | Signature inference | Superseded | +| [0004](0004-unsafe-boundaries.md) | Unsafe boundaries: raw pointers, unsafe regions and indirect calls | Superseded | +| [0005](0005-whole-program-analysis.md) | Whole-program analysis: cross-TU summaries and the compiler driver | Superseded | +| [0006](0006-precision.md) | Precision: non-lexical loans, condition facts, element places and outcome-conditional summaries | Superseded | +| [0007](0007-resource-lifecycle.md) | Resource lifecycle: leaks, release families and owned fields | Superseded | +| [0008](0008-pointer-validity.md) | Pointer validity: null dereferences, uninitialised pointers, invalid releases and replaced values | Superseded | +| [0009](0009-value-conditional-behaviour.md) | Value-conditional behaviour: scalar facts, guarded effects and inferred `noreturn` | Superseded | +| [0010](0010-shared-ownership.md) | Shared ownership: reference counts, ownership by outcome and per-outcome facts | Superseded | +| [0011](0011-spatial-safety.md) | Spatial safety: derived pointers, extents and bounds | Superseded | +| [0012](0012-spatial-safety-strings-and-fields.md) | Spatial safety II: strings, sized fields, offset relations and assumptions | Superseded | +| [0013](0013-interprocedural-heap-state.md) | Interprocedural heap state and value identity | Superseded | +| [0014](0014-pointer-identity-and-call-effects.md) | Pointer identity and precise call effects | Superseded | +| [0015](0015-array-and-container-ownership.md) | Array elements, range operations, and container ownership | Superseded | +| [0016](0016-compositional-call-checking.md) | Compositional call checking under caller alias relationships | Superseded | +| [0017](0017-c-integer-semantics-and-spatial-safety.md) | C integer semantics and compositional spatial safety | Superseded | | [0018](0018-checked-code-and-safety-contracts.md) | Compositional safety contracts and checked code | Superseded | | [0019](0019-practical-checked-memory-contracts.md) | Practical checked memory contracts for buffers and heap objects | Superseded | | [0020](0020-scalable-modular-checked-analysis.md) | Scalable modular checked analysis | Superseded | @@ -76,11 +79,12 @@ decision is a new RFC that supersedes the relevant section. | [0027](0027-recursive-object-ownership.md) | Recursive object ownership and complete cleanup contracts | Superseded | | [0028](0028-opaque-objects-and-library-state.md) | Inferred contracts for opaque objects and private library state | Superseded | | [0029](0029-compositional-recursive-workflows.md) | Compositional invariants for recursive C workflows | Superseded | -| [0030](0030-prove-or-trap.md) | Prove or trap: one safety semantics with compiler-enforced checks | Implemented | -| [0031](0031-object-engine.md) | The object engine: a sound heap abstraction behind the engine seam | Implemented | +| [0030](0030-prove-or-trap.md) | Prove or trap: one safety semantics with compiler-enforced checks | Implemented (analysis superseded by [0036](0036-deterministic-heap-reuse.md)) | +| [0031](0031-object-engine.md) | The object engine: a sound heap abstraction behind the engine seam | Superseded | | [0032](0032-runtime-enforcement.md) | Runtime enforcement: an object table, guarded facets and a temporal backstop | Implemented | | [0033](0033-drop-in-by-default.md) | Drop-in by default: no false stops on code WeaveC was never tuned on | Implemented | | [0034](0034-fast-enforcement.md) | Fast enforcement, confirmed errors and a bounded analysis | Implemented | -| [0035](0035-guard-by-default.md) | Guard by default: every access guarded in the backend, the analysis advisory | Implemented | +| [0035](0035-guard-by-default.md) | Guard by default: every access guarded in the backend, the analysis advisory | Implemented (superseded in part by [0036](0036-deterministic-heap-reuse.md)) | +| [0036](0036-deterministic-heap-reuse.md) | Deterministic heap reuse: reuse after a sweep, the analysis deleted | Accepted | The [roadmap](../roadmap.md) links each milestone to the RFCs that define it. diff --git a/docs/roadmap.md b/docs/roadmap.md index 4fde98e9..64d53048 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -4,17 +4,64 @@ Rough ordering of the major pieces of work. This page is the one-screen view; the design detail and the reasoning live in the [RFCs](rfcs/README.md) each milestone links to. Everything here is subject to change as we learn. -The design today is [RFC 0035](rfcs/0035-guard-by-default.md). The -milestones before it are summarised under *History*; their RFCs keep the +The design today is [RFC 0036](rfcs/0036-deterministic-heap-reuse.md) +over the guards of [RFC 0035](rfcs/0035-guard-by-default.md). The +milestones before it are summarised under _History_; their RFCs keep the detail, and the per-RFC validation records of the milestones before RFC 0030 remain in the repository history at tag `v0.10.0`. +## Now: RFC 0036 — Deterministic heap reuse + +Design: [RFC 0036 — Deterministic heap reuse](rfcs/0036-deterministic-heap-reuse.md) +(Accepted; being implemented on `rfc0036-deterministic-safety`, and +Implemented once its _Acceptance gates_ pass on the final tree). A released +heap block is reused only after a _sweep_ (the program's threads stopped, +its stacks, registers, writable data and live heap scanned conservatively) +finds no pointer to it, so a use after free or a double free traps however +much memory the program allocated since; sweeps are paced to a small share +of the run time. A fortified call's object size is checked; code written +for ASan sees ASan's macros and interface; locals whose lifetimes never +overlap share frame space; the globals of shared libraries and bundles are +registered with the process's runtime; the number parsers no longer need a +terminator they never read. Deleted: the advisory analysis (the `weavec` +tool, `-fweavec-diagnose`, `weavec --whole-program`, `lib/Analysis`, all of +`lib/Core` but the library table), every annotation but `WEAVEC_UNSAFE`, +and their diagnostics. + +The RFC's first draft also made spatial safety and use after return +deterministic (bounds from each pointer's provenance, locals in frame +blocks from the arena). It was implemented on the same branch and measured +at twice v0.16.0's run time (geometric mean 3.1× the reference compiler, +QuickJS 10.8×); the revision keeps the cheap part and records the +measurement of each mechanism in its _Alternatives_. The stages: + +- [x] **S0 The RFC**: the revision. +- [x] **S1 Remove the first draft's spatial and frame mechanisms**: + provenance bounds, escapes and the escape registry, pointer slots, + frame blocks, the `setjmp` hook, the globals table; RFC 0035's native + frames back. +- [x] **S2 Shared frame space** (`StackLifetime`). +- [x] **S3 The sweep's cost**: pacing, the image table, address-order + reuse. +- [x] **S4 Cases, lit and runtime tests** for the revised scope. +- [x] **S5 Fresh iterations** on darwin-arm64: the motivation's projects, + the corpus, the gates (recorded in the RFC's _Measured gates_), the + documentation. +- [ ] **Linux** through CI, then the RFC is Implemented (QuickJS's run + time, the cost of the ASan contract, is carried forward). + +RFC 0035's plan for RFC 0036, ownership as contracts checked at function +entry, is withdrawn: it would remove guards on the strength of inferred +facts, which would have to be right where the analysis measured 62 false +findings in 69 (RFC 0036 _Alternatives_). + ## Done: RFC 0035 — Guard by default Design: [RFC 0035 — Guard by default](rfcs/0035-guard-by-default.md) -(Implemented; its *Measured gates* record the final tree, with the run +(Implemented; its _Measured gates_ record the final tree, with the run time of interpreters (Lua, cJSON) and the build time of some units carried -forward). +forward; RFC 0036 supersedes its quarantine's reuse after a byte budget +and its advisory analysis, and amends its frame layout). `weavec-cc` guards every memory access in LLVM IR against the runtime's shadow memory, and removes a guard only where a local rule proves it redundant (in bounds of a local or a global, covered by a dominating guard, @@ -22,92 +69,72 @@ merged in a block, a loop whose whole range is checked once); the passes lay out stack frames and globals with redzones, so use after scope traps; the shadow says what is addressable, so untracked memory is fast; Clang's `array-bounds` checks keep typed indexes in bounds; library calls are -guarded from the library table or redirected to checked versions. The -analysis is advisory (`weavec`, `weavec --whole-program`, -`weavec-cc -fweavec-diagnose`) and changes no generated code. Deleted: the -checks inserted through Sema, deferred CodeGen, the check planner and the -prelude, unit records and the link step, the require levels, and the -analysis ledger. The stages landed on `rfc0035-guard-by-default` and ship -as one change; the RFC's *Implementation amendments* record the decisions: - -- [x] **S0 Tests first**: `fresh35` and `sealed35`, the wrong proofs and - false stops as cases, the CVE and blind sets. -- [x] **S1 The runtime**: the shadow encoding and its window, the - allocator's shadow, the entry points. -- [x] **S2 The passes**: accesses, guards, library calls, unsafe regions, - array bounds, the enforcement ledger. -- [x] **S3 Frames and globals.** -- [x] **S4 Removing guards**, loop ranges included. -- [x] **S5 The driver** and the deletion of the emission path and records. -- [x] **S6 The advisory analysis.** -- [x] **S7 Tests, the runner, the corpus gate, CI.** -- [x] **S8 Fresh iterations, gates, the sealed run, documentation.** - -## Next: RFC 0036 — Ownership as checked contracts - -- Inferred function contracts (a borrowed parameter is live and has `n` - bytes, a callee does not release it) checked once at entry, so the - body's guards go; contracts proven at every caller drop the entry check. -- Ownership and extent annotations as contracts. - -## Planned: RFC 0037 — Adoption - -- Fingerprinted baselines and reasoned suppressions. -- `weavec.toml`, with path scoping and API overlays. -- `weavec suggest --apply` for the analysis's fix-its. -- A vendored `weavec.h`. -- Relocatable installs. +guarded from the library table or redirected to checked versions. Deleted: +the checks inserted through Sema, deferred CodeGen, the check planner and +the prelude, unit records and the link step, the require levels, and the +analysis ledger. -## Later +## Next + +RFC 0036's _Future work_: -- Deterministic heap temporal safety: freed memory reused only after a - sweep finds no pointer to it. -- Use after return, with a fake stack for frames whose locals escape. +- Spatial determinism (an access checked against its pointer's object) and + use after return, under a cost budget measured before the design. +- Concurrent marking, so that a sweep does not stop the program for its + whole duration. - Hardware memory tagging (Arm MTE, Apple MIE) where the processor has it. +- Adoption: relocatable installs, packages, a vendored `weavec.h`. + +## Later + - Concurrency safety beyond assumption A3 (data races between a guard and its access). - C++ and Objective-C, which pass through to Clang unchanged today. -- Reading `lifetimebound`, `noescape` and `lifetime_capture_by`. ## History -The milestones that built the model and the analysis: - -| Milestone | Design | Scope | RFC status | -| --- | --- | --- | --- | -| 0 Scaffolding | [RFC 0001](rfcs/0001-ownership-model.md) | Layered libraries, pipeline, annotation header, the ownership model | Accepted; guarantee replaced by RFC 0030, then RFC 0035 | -| 1 Intra-procedural checking | [RFC 0002](rfcs/0002-intraprocedural-checking.md) | CFG dataflow, alias relation, moves, loans, lifetimes | Implemented; severities amended by RFC 0030 | -| 2 Signature inference and annotations | [RFC 0003](rfcs/0003-signature-inference.md), [RFC 0004](rfcs/0004-unsafe-boundaries.md) | Summaries, reconciliation, raw pointers, unsafe regions, indirect calls | Implemented; unsafe regions amended by RFCs 0030 and 0035 | -| 3–4 Compiler driver and whole program | [RFC 0005](rfcs/0005-whole-program-analysis.md) | `weavec-cc`, `--whole-program` | Implemented; records and the link step deleted by RFC 0035 | -| 5 Precision | [RFC 0006](rfcs/0006-precision.md) | Non-lexical loans, condition facts, element places, outcome-conditional summaries | Accepted | -| 6 Resource lifecycle | [RFC 0007](rfcs/0007-resource-lifecycle.md) | Leaks, release families, owned fields | Accepted; leak rules amended by RFC 0030 | -| 7 Pointer validity | [RFC 0008](rfcs/0008-pointer-validity.md) | Null dereferences, uninitialised pointers, invalid releases, replaced values | Accepted; severities amended by RFC 0030 | -| 8 Value-conditional behaviour | [RFC 0009](rfcs/0009-value-conditional-behaviour.md) | Scalar facts, guarded effects, inferred `noreturn` | Accepted | -| 9 Shared ownership | [RFC 0010](rfcs/0010-shared-ownership.md) | Reference counts, ownership by outcome | Accepted | -| 10–11 Spatial safety | [RFC 0011](rfcs/0011-spatial-safety.md), [RFC 0012](rfcs/0012-spatial-safety-strings-and-fields.md) | Derived pointers, extents, strings, sized fields, assumptions | Accepted | -| 12 Interprocedural heap state | [RFC 0013](rfcs/0013-interprocedural-heap-state.md) | Heap postconditions, value snapshots | Accepted | -| 13 Pointer identity and call effects | [RFC 0014](rfcs/0014-pointer-identity-and-call-effects.md) | Function values, callback specialisation, pointer-copy identity | Implemented; callback globals replaced by RFC 0030 slots | -| 14 Arrays and containers | [RFC 0015](rfcs/0015-array-and-container-ownership.md) | Element selectors, ranges, container ownership | Implemented | -| 15 Compositional call checking | [RFC 0016](rfcs/0016-compositional-call-checking.md) | Call contexts under caller alias relationships | Implemented | -| 16 C integers and spatial checking | [RFC 0017](rfcs/0017-c-integer-semantics-and-spatial-safety.md) | Target integer semantics, typed expressions, VLAs, flexible arrays | Implemented | -| 17–27 Checked mode | [RFC 0018](rfcs/0018-checked-code-and-safety-contracts.md)–[RFC 0029](rfcs/0029-compositional-recursive-workflows.md) | Opt-in checked functions with safety contracts | Superseded by RFC 0030; the mode is deleted | -| 28 Prove or trap | [RFC 0030](rfcs/0030-prove-or-trap.md) | One outcome per safety facet, the library table, pointer kinds, function-pointer slots | Implemented; its enforcement (inserted checks, require levels, records) superseded by RFC 0035 | -| 29 The object engine | [RFC 0031](rfcs/0031-object-engine.md) | Facts on abstract objects and symbolic values, so every alias sees every fact | Implemented; the analysis behind `weavec` | -| 30 Runtime enforcement | [RFC 0032](rfcs/0032-runtime-enforcement.md) | The runtime's allocator and quarantine, guards | Implemented; guards and registration superseded by RFC 0035 | -| 31 Drop-in by default | [RFC 0033](rfcs/0033-drop-in-by-default.md) | Correct untuned projects build and run; one runtime per process | Implemented; link modes and records superseded by RFC 0035 | -| 32 Fast enforcement | [RFC 0034](rfcs/0034-fast-enforcement.md) | Shadow memory, confirmed errors, the work budget | Implemented; guard lowering superseded by RFC 0035 | +The milestones that built the model, the analysis and the guards. RFC 0036 +deletes the analysis and marks the RFCs that specified it (0001–0017, 0031) Superseded; they stay as the record of what was built and measured. + +| Milestone | Design | Scope | RFC status | +| ------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | +| 0 Scaffolding | [RFC 0001](rfcs/0001-ownership-model.md) | Layered libraries, pipeline, annotation header, the ownership model | Superseded by RFC 0036; guarantee replaced by RFC 0030, then RFC 0035 | +| 1 Intra-procedural checking | [RFC 0002](rfcs/0002-intraprocedural-checking.md) | CFG dataflow, alias relation, moves, loans, lifetimes | Superseded by RFC 0036 | +| 2 Signature inference and annotations | [RFC 0003](rfcs/0003-signature-inference.md), [RFC 0004](rfcs/0004-unsafe-boundaries.md) | Summaries, reconciliation, raw pointers, unsafe regions, indirect calls | Superseded by RFC 0036; unsafe regions live on in RFC 0035 | +| 3–4 Compiler driver and whole program | [RFC 0005](rfcs/0005-whole-program-analysis.md) | `weavec-cc`, `--whole-program` | Superseded by RFC 0036; `weavec-cc` lives on, records and the link step deleted by RFC 0035 | +| 5 Precision | [RFC 0006](rfcs/0006-precision.md) | Non-lexical loans, condition facts, element places, outcome-conditional summaries | Superseded by RFC 0036 | +| 6 Resource lifecycle | [RFC 0007](rfcs/0007-resource-lifecycle.md) | Leaks, release families, owned fields | Superseded by RFC 0036 | +| 7 Pointer validity | [RFC 0008](rfcs/0008-pointer-validity.md) | Null dereferences, uninitialised pointers, invalid releases, replaced values | Superseded by RFC 0036 | +| 8 Value-conditional behaviour | [RFC 0009](rfcs/0009-value-conditional-behaviour.md) | Scalar facts, guarded effects, inferred `noreturn` | Superseded by RFC 0036 | +| 9 Shared ownership | [RFC 0010](rfcs/0010-shared-ownership.md) | Reference counts, ownership by outcome | Superseded by RFC 0036 | +| 10–11 Spatial safety | [RFC 0011](rfcs/0011-spatial-safety.md), [RFC 0012](rfcs/0012-spatial-safety-strings-and-fields.md) | Derived pointers, extents, strings, sized fields, assumptions | Superseded by RFC 0036 | +| 12 Interprocedural heap state | [RFC 0013](rfcs/0013-interprocedural-heap-state.md) | Heap postconditions, value snapshots | Superseded by RFC 0036 | +| 13 Pointer identity and call effects | [RFC 0014](rfcs/0014-pointer-identity-and-call-effects.md) | Function values, callback specialisation, pointer-copy identity | Superseded by RFC 0036 | +| 14 Arrays and containers | [RFC 0015](rfcs/0015-array-and-container-ownership.md) | Element selectors, ranges, container ownership | Superseded by RFC 0036 | +| 15 Compositional call checking | [RFC 0016](rfcs/0016-compositional-call-checking.md) | Call contexts under caller alias relationships | Superseded by RFC 0036 | +| 16 C integers and spatial checking | [RFC 0017](rfcs/0017-c-integer-semantics-and-spatial-safety.md) | Target integer semantics, typed expressions, VLAs, flexible arrays | Superseded by RFC 0036 | +| 17–27 Checked mode | [RFC 0018](rfcs/0018-checked-code-and-safety-contracts.md)–[RFC 0029](rfcs/0029-compositional-recursive-workflows.md) | Opt-in checked functions with safety contracts | Superseded by RFC 0030; the mode is deleted | +| 28 Prove or trap | [RFC 0030](rfcs/0030-prove-or-trap.md) | One outcome per safety facet, the library table, pointer kinds, function-pointer slots | Implemented; its enforcement superseded by RFC 0035 and its analysis by RFC 0036; the library table lives on | +| 29 The object engine | [RFC 0031](rfcs/0031-object-engine.md) | Facts on abstract objects and symbolic values, so every alias sees every fact | Superseded by RFC 0036, which deletes the analysis and `weavec` | +| 30 Runtime enforcement | [RFC 0032](rfcs/0032-runtime-enforcement.md) | The runtime's allocator and quarantine, guards | Implemented; guards and registration superseded by RFC 0035, the quarantine's reuse by RFC 0036 | +| 31 Drop-in by default | [RFC 0033](rfcs/0033-drop-in-by-default.md) | Correct untuned projects build and run; one runtime per process | Implemented; link modes and records superseded by RFC 0035 | +| 32 Fast enforcement | [RFC 0034](rfcs/0034-fast-enforcement.md) | Shadow memory, confirmed errors, the work budget | Implemented; guard lowering superseded by RFC 0035 | +| 33 Guard by default | [RFC 0035](rfcs/0035-guard-by-default.md) | Every access guarded in LLVM IR, local removal rules, redzones, the advisory analysis | Implemented; the quarantine's budget and the analysis superseded by RFC 0036, the frame layout amended | RFCs 0030–0034 made the analysis decide what the build enforced. Measured after RFC 0034, the runtime did the catching and the analysis caused the false stops, the missed bugs (wrong proofs removed needed guards) and most of the build cost, which is why RFC 0035 guards every access and makes the -analysis advisory. +analysis advisory. Measured after RFC 0035, the advisory analysis reported +no bug worth fixing on untuned code and found none of the bugs the guards +miss, while a use after free passed once the quarantine had recycled its +block, which is why RFC 0036 deletes the analysis and reuses released +memory only after a sweep finds no pointer to it. ## Ongoing - Corpus testing against real C projects pinned by SHA (`scripts/corpus-gate.py` over `test/corpus/`: a ratchet in - `test/corpus/expected.json` and a verdict for every finding in + `test/corpus/expected.json` and a verdict for every guard failure in `test/corpus/triage.json`); `--quick` on every pull request, `--full` weekly. -- Fuzzing the analyzer with generated C. -- Windows support once the analysis stabilises. +- Windows support. diff --git a/docs/scripts/check-examples.py b/docs/scripts/check-examples.py index 061dcf5f..0a692c99 100755 --- a/docs/scripts/check-examples.py +++ b/docs/scripts/check-examples.py @@ -1,13 +1,12 @@ #!/usr/bin/env python3 -"""Exercise the published tutorial examples against built WeaveC binaries. +"""Exercise the published tutorial examples against a built weavec-cc. -`weavec` must report the use-after-free in use-after-free.c and accept fixed.c -(RFC 0035 section 8: the advisory analysis). `weavec-cc` must compile +Every example in docs/examples must compile. `weavec-cc` must compile lookup.c, record the guarded load of `table[i]` in the enforcement ledger -(`weavec-ledger` version 3, RFC 0035 section 9), link it, run it, and stop a -negative index with an `index-out-of-bounds` report; and it must build vec.c -and stop a read past its heap block with a `heap-buffer-overflow` report -(RFC 0035 section 5.3; the default `-fweavec-checks=trap`). +(`weavec-ledger` version 3, RFC 0035 section 9), link it, run it, and stop +a negative index with an `index-out-of-bounds` report; and it must build +vec.c and stop a read past its heap block with a `heap-buffer-overflow` +report (RFC 0035 section 5.3; the default `-fweavec-checks=trap`). """ import argparse @@ -19,13 +18,9 @@ import tempfile parser = argparse.ArgumentParser(description=__doc__) -parser.add_argument("--weavec", required=True, type=Path, help="the weavec analysis tool") -parser.add_argument( - "--weavec-cc", type=Path, help="the weavec-cc compiler (default: next to --weavec)" -) +parser.add_argument("--weavec-cc", required=True, type=Path, help="the weavec-cc compiler") args = parser.parse_args() -weavec = args.weavec.resolve() -weavec_cc = (args.weavec_cc or weavec.with_name("weavec-cc")).resolve() +weavec_cc = args.weavec_cc.resolve() examples = Path(__file__).resolve().parent.parent / "examples" traps = {-signal.SIGTRAP, -signal.SIGILL} @@ -37,15 +32,6 @@ def run(command, **options): ) -def analyse(source): - return run([weavec, source, "--", "-std=c17"]) - - -def analysis_summary(stderr, name): - """The analysis's summary line (RFC 0035 section 8) for source `name`.""" - return re.search(rf"^weavec: (\S*/)?{re.escape(name)}: [\d,]+ sites?: ", stderr, re.M) - - def enforcement_summary(stderr, name): """The enforcement summary line (RFC 0035 section 9) for source `name`.""" pattern = rf"^weavec: (\S*/)?{re.escape(name)}: \d+ accesses: \d+ proven, \d+ guarded, \d+ unguarded$" @@ -59,14 +45,12 @@ def expect_trap(result, kind, name, line, detail): assert re.search(pattern, result.stderr, re.M), "unexpected report:\n" + result.stderr -bad = analyse(examples / "use-after-free.c") -assert bad.returncode != 0 and "[weavec::use-after-free]" in bad.stderr, bad.stderr -good = analyse(examples / "fixed.c") -assert good.returncode == 0, good.stderr -assert analysis_summary(good.stderr, "fixed.c"), "no summary line:\n" + good.stderr - with tempfile.TemporaryDirectory() as temp: temp = Path(temp) + for source in sorted(examples.glob("*.c")): + compiled = run([weavec_cc, "-std=c17", "-c", source, "-o", temp / (source.stem + ".o")]) + assert compiled.returncode == 0, f"{source.name}:\n{compiled.stderr}" + ledgers = temp / "ledger" ledgers.mkdir() obj = temp / "lookup.o" @@ -99,5 +83,5 @@ def expect_trap(result, kind, name, line, detail): assert re.search(r"^weavec: 0x[0-9a-f]+ is 0 bytes after the 16-byte heap object at 0x[0-9a-f]+$", past.stderr, re.M), "no heap object line:\n" + past.stderr -print("Documentation examples passed: use-after-free, fixed lifetime, the guarded " - "load in the ledger, and the index and heap reports.") +print("Documentation examples passed: every example compiles, the guarded load " + "in the ledger, and the index and heap reports.") diff --git a/docs/scripts/content.test.mjs b/docs/scripts/content.test.mjs index b0b0db45..1730b9bc 100644 --- a/docs/scripts/content.test.mjs +++ b/docs/scripts/content.test.mjs @@ -3,18 +3,18 @@ import assert from 'node:assert/strict'; import { sections, rewriteUrl, rewriteMarkdown, rfcStatus } from './content.mjs'; const routes = { - 'docs/annotations.md': '/reference/annotations/', - 'docs/annotations.md#placement': '/reference/annotation-placement/', + 'docs/architecture.md': '/internals/architecture/', + 'docs/architecture.md#the-runtime': '/internals/the-runtime/', 'docs/rfcs/0030-prove-or-trap.md': '/rfcs/0030-prove-or-trap/', }; test('cross-file anchors follow split sections', () => { assert.equal( - rewriteUrl('../annotations.md#placement', 'docs/rfcs/README.md', routes), - '/reference/annotation-placement/#placement', + rewriteUrl('../architecture.md#the-runtime', 'docs/rfcs/README.md', routes), + '/internals/the-runtime/#the-runtime', ); assert.equal( - rewriteUrl('#placement', 'docs/annotations.md', routes), - '/reference/annotation-placement/#placement', + rewriteUrl('#the-runtime', 'docs/architecture.md', routes), + '/internals/the-runtime/#the-runtime', ); assert.equal( rewriteUrl('0030-prove-or-trap.md#soundness', 'docs/rfcs/0018-checked.md', routes), @@ -23,17 +23,17 @@ test('cross-file anchors follow split sections', () => { }); test('code examples and external URLs are unchanged; reference links are rewritten', () => { const markdown = - '[guide][g]\n\n[g]: annotations.md#placement\n\n`[code](annotations.md)`\n\n```md\n[code](annotations.md)\n```\n\n[external](https://example.com/)'; - const result = rewriteMarkdown(markdown, 'docs/architecture.md', routes); - assert.ok(result.includes('[g]: /reference/annotation-placement/#placement')); - assert.ok(result.includes('`[code](annotations.md)`')); - assert.ok(result.includes('```md\n[code](annotations.md)\n```')); + '[guide][g]\n\n[g]: architecture.md#the-runtime\n\n`[code](architecture.md)`\n\n```md\n[code](architecture.md)\n```\n\n[external](https://example.com/)'; + const result = rewriteMarkdown(markdown, 'docs/roadmap.md', routes); + assert.ok(result.includes('[g]: /internals/the-runtime/#the-runtime')); + assert.ok(result.includes('`[code](architecture.md)`')); + assert.ok(result.includes('```md\n[code](architecture.md)\n```')); assert.ok(result.includes('[external](https://example.com/)')); }); test('repository files link to their source and cannot escape the repository', () => { assert.equal( - rewriteUrl('../lib/Core/Place.cpp', 'docs/architecture.md', routes), - 'https://github.com/weavefoundry/weavec/blob/main/lib/Core/Place.cpp', + rewriteUrl('../runtime/weavec_rt.h', 'docs/architecture.md', routes), + 'https://github.com/weavefoundry/weavec/blob/main/runtime/weavec_rt.h', ); assert.throws(() => rewriteUrl('../../private.txt', 'docs/file.md', routes), /leaves repository/); }); diff --git a/docs/scripts/prepare-content.mjs b/docs/scripts/prepare-content.mjs index 76e2dbdd..5c2cef42 100644 --- a/docs/scripts/prepare-content.mjs +++ b/docs/scripts/prepare-content.mjs @@ -90,20 +90,7 @@ const stale = new Set( (await readdir(output, { recursive: true })).filter((name) => /\.(md|mdx|json)$/.test(name)), ); -await split( - 'docs/annotations.md', - { - Placement: 'reference/annotation-placement', - Diagnostics: 'reference/diagnostics', - 'What the checker understands': 'reference/checker-coverage', - 'Related pointer arguments': 'guides/related-pointers', - 'C integers and dynamic bounds': 'guides/integers-and-bounds', - 'Controlling diagnostics': 'reference/diagnostic-controls', - 'Compatibility with other annotation schemes': 'reference/annotation-compatibility', - }, - 'reference/annotations', - 'Annotations', -); +await whole('docs/annotations.md', 'reference/annotations'); await whole('docs/development.md', 'contributing/development'); await whole('docs/roadmap.md', 'project/roadmap'); @@ -134,32 +121,6 @@ for (const filename of (await readdir(path.join(docs, 'rfcs'))).sort()) { }); } -// Each diagnostic keeps its exact source description and gains a focused remedy. -const diagnosticPage = pages.find((page) => page.route === 'reference/diagnostics'); -const remedies = JSON.parse(await read('docs/data/diagnostic-remedies.json')); -const diagnosticRows = diagnosticPage.body - .split('\n') - .filter((line) => /^\| `[^`]+`\s*\|/.test(line)); -const diagnosticIndex = []; -for (const line of diagnosticRows) { - const match = line.match(/^\| `([^`]+)`\s*\|\s*([^|]*?)\s*\|\s*(.*?)\s*\|$/); - if (!match) throw new Error(`Cannot parse diagnostic: ${line}`); - const [, id, severity, description] = match; - const remedy = remedies[id]; - if (!remedy) throw new Error(`Add documentation guidance for ${id}`); - add( - 'docs/annotations.md', - `reference/diagnostics/${id}`, - id, - `Default severity: **${severity}** · Identifier: \`weavec::${id}\`\n\n## What it means\n\n${description}\n\n## How to resolve it\n\n${remedy}\n\n## Related reference\n\n[Diagnostic controls](/reference/diagnostic-controls/) · [Guarantees](/reference/guarantees/)`, - ); - diagnosticIndex.push(`| [\`${id}\`](/reference/diagnostics/${id}/) | ${severity} |`); -} -diagnosticPage.body = - '\n\nEvery WeaveC diagnostic ends with a stable identifier such as `[weavec::use-after-free]`. Open an entry for its exact meaning, conditions, and resolution guidance.\n\n| Diagnostic | Default severity |\n| --- | --- |\n' + - diagnosticIndex.join('\n') + - '\n\nThese diagnostics come from the advisory analysis: the `weavec` tool, and `weavec-cc -fweavec-diagnose`, which prints every finding as a warning. In `weavec`, severity follows certainty: a finding that holds on every path is an error, and a temporal finding that holds on some paths only is a warning. A null dereference or out-of-bounds access that is only possible is not reported; the guards of a `weavec-cc` build stop it at run time if it happens. Run-time reports are not diagnostics and have no identifier; see [check modes](/reference/cli/#check-modes). Start with the first diagnostic and follow its source notes. See [diagnostic controls](/reference/diagnostic-controls/) and the [safety guarantees](/reference/guarantees/).'; - try { await whole('CHANGELOG.md', 'project/releases'); pages.at(-1).title = 'Release notes'; @@ -225,7 +186,7 @@ for (const page of pages) { content = rewriteMarkdown(raw, page.source, routes); } else { const lastUpdated = commitDate(page.source); - const description = `${page.title} — WeaveC documentation for run-time memory safety and ownership analysis in C.`; + const description = `${page.title} — WeaveC documentation: run-time memory safety for existing C code.`; const frontmatter = { title: page.title, description, @@ -258,6 +219,4 @@ await writeChanged( path.join(docs, 'public/social-card.png'), await sharp(path.join(docs, 'public/social-card.svg')).png().toBuffer(), ); -console.log( - `Prepared ${pages.length} pages, ${rfcs.length} RFCs, and ${diagnosticRows.length} diagnostic entries for WeaveC ${version}.`, -); +console.log(`Prepared ${pages.length} pages and ${rfcs.length} RFCs for WeaveC ${version}.`); diff --git a/docs/scripts/watch-content.mjs b/docs/scripts/watch-content.mjs index 363f9987..b8898b1d 100644 --- a/docs/scripts/watch-content.mjs +++ b/docs/scripts/watch-content.mjs @@ -10,9 +10,7 @@ export function watchContent() { hooks: { 'astro:server:setup': ({ server, logger }) => { const docs = fileURLToPath(new URL('../', import.meta.url)); - const directories = ['pages', 'rfcs', 'examples', 'data'].map((name) => - path.join(docs, name), - ); + const directories = ['pages', 'rfcs', 'examples'].map((name) => path.join(docs, name)); const files = readdirSync(docs) .filter((name) => name.endsWith('.md')) .map((name) => path.join(docs, name)); diff --git a/docs/src/components/Head.astro b/docs/src/components/Head.astro index c725dea3..97e9c0c2 100644 --- a/docs/src/components/Head.astro +++ b/docs/src/components/Head.astro @@ -18,7 +18,7 @@ if (Astro.locals.starlightRoute.entry.id === '404') { diff --git a/docs/src/components/Home.astro b/docs/src/components/Home.astro index 499e334d..25956e53 100644 --- a/docs/src/components/Home.astro +++ b/docs/src/components/Home.astro @@ -1,26 +1,26 @@ --- import { Code } from '@astrojs/starlight/components'; import project from '../../.generated/project.json'; -import example from '../../examples/use-after-free.c?raw'; +import example from '../../examples/vec.c?raw'; const paths = [ { number: '01', title: 'Run your first check', - description: 'Install WeaveC, find a lifetime bug, and understand the fix.', + description: 'Install WeaveC, build a program, and watch a bad access stop with a report.', href: '/getting-started/first-check/', label: 'GET STARTED', }, { number: '02', title: 'Bring your codebase', - description: 'Use your compilation database or build with weavec-cc.', + description: 'Build with CC=weavec-cc under Make, CMake or a configure script.', href: '/guides/build-integration/', label: 'INTEGRATE', }, { number: '03', title: 'Adopt it', - description: 'Run your tests under guarded builds, fix what traps, and add the analysis to CI.', + description: 'Run your tests under the guarded build, fix what traps, and measure the cost.', href: '/guides/adoption/', label: 'GO DEEPER', }, @@ -36,8 +36,8 @@ const paths = [ >

C, with more
certainty.

- Memory safety for the C you already write. WeaveC guards every memory access at run time, - stops at the first bad one, and reports ownership bugs before the program runs. + Memory safety for the C you already write. WeaveC is a drop-in C compiler that guards every + memory access and stops the program at the first bad one, with a precise report.

-
+
- FOLLOW THE OWNERSHIP C17 + STOP AT THE FIRST BAD ACCESS C17
- -
+ +

use of 'n' after it was freed

↳ freed here, through 'alias'TRAP./vec 4 +

weavec: heap-buffer-overflow at vec.c:9:43: read of 4 bytes

↳ 0 bytes after the 16-byte heap object
- Ownership inferred. No annotations needed for this example. + No source changes. Built with weavec-cc, as Clang builds it.
- YOUR C SOURCEINFER OWNERSHIPYOUR C SOURCEGUARD EVERY ACCESSSTOP AT THE FIRST BAD ONEGUARD EVERY ACCESSCLANG + LLVM + >CLANG + LLVM
@@ -85,7 +78,7 @@ const paths = [ Start small. Build confidence.

- From one source file to a whole program.
Choose the next step for your project. + From one source file to a whole build.
Choose the next step for your project.

@@ -112,35 +105,36 @@ const paths = [
- A MODEL YOU CAN REASON ABOUT

- The intent is already
in your code. + A GUARANTEE YOU CAN REASON ABOUT

+ Same code.
Same build.

- Allocators, cleanup functions, aliases, and control flow tell a story. WeaveC follows it - across functions and source files. -

Understand ownership and borrowing Read the safety guarantees
01
-

Infer first

- Function bodies reveal what they free, store, borrow, and return. Add annotations where - an interface needs a contract. +

Guard every access

+ Loads, stores, memory intrinsics and library calls are checked, however far an index + jumps, and however long ago a block was freed.

02

Keep your toolchain

- Analyze C with libTooling or use weavec-cc in a normal build. Clang handles parsing and - code generation. + weavec-cc is Clang's driver: configure scripts, Make and CMake see a C compiler, and + code written to be clean under AddressSanitizer is clean under WeaveC.

03

Know the boundary

- Every access is guarded unless a local rule proves it redundant, and the ledger lists - each one. WEAVEC_UNSAFE marks the code you choose to leave unguarded. + A guard is removed only where a local rule proves it redundant, and the ledger lists + each access. WEAVEC_UNSAFE marks the code you choose to leave unguarded.

@@ -152,17 +146,17 @@ const paths = [ Keep the reference close.

Early software. Explicit guarantees. The guarantee covers every guarded access, - under five named assumptions; the scope page lists what it does not catch. Read the scope and limits →

diff --git a/docs/src/styles/custom.css b/docs/src/styles/custom.css index 335cba09..1f8a3e39 100644 --- a/docs/src/styles/custom.css +++ b/docs/src/styles/custom.css @@ -344,7 +344,7 @@ h4, margin: 0; font-size: 0.75rem; } -.diagnostic-output { +.report-output { background: #291c1d; border: 1px solid #703d40; border-radius: 5px; @@ -353,27 +353,23 @@ h4, color: #f4cbca; font: 0.7rem/1.8 var(--sl-font-mono); } -.diagnostic-output > div { +.report-output > div { display: flex; flex-wrap: wrap; align-items: center; gap: 0.7rem; } -.diagnostic-output a { - color: #f4cbca; - text-decoration: none; -} -.diagnostic-output p { +.report-output p { margin: 0.4rem 0 0.1rem; } -.error-marker { +.trap-marker { font-size: 0.56rem; background: #683738; padding: 0.1rem 0.4rem; border-radius: 3px; color: #ffddda; } -.diagnostic-note { +.report-note { color: #d1aaa6; font-size: 0.65rem; } diff --git a/docs/tests/site.spec.ts b/docs/tests/site.spec.ts index 4ce76094..fbdcb598 100644 --- a/docs/tests/site.spec.ts +++ b/docs/tests/site.spec.ts @@ -7,7 +7,7 @@ test('homepage, onboarding, and both themes are accessible without horizontal ov for (const route of [ '/', '/getting-started/installation/', - '/reference/diagnostics/use-after-free/', + '/guides/build-integration/', '/reference/cli/', ]) { await page.goto(route); @@ -40,11 +40,11 @@ test('homepage, onboarding, and both themes are accessible without horizontal ov } }); -test('search finds a diagnostic and opens a result', async ({ page }) => { +test('search finds a report kind and opens a result', async ({ page }) => { await page.goto('/'); await page.locator('button[data-open-modal]').click(); const search = page.locator('.pagefind-ui__search-input'); - await search.fill('use-after-free'); + await search.fill('heap-use-after-free'); const results = page.locator('.pagefind-ui__result-link'); await expect(results.first()).toBeVisible(); await results.first().click(); @@ -60,8 +60,8 @@ test('RFC library filters by title, number, status, and empty state', async ({ p await page.locator('#rfc-query').fill('no-such-design'); await expect(page.locator('#rfc-empty')).toBeVisible(); await page.locator('#rfc-query').fill(''); - await page.locator('#rfc-status').selectOption('Accepted'); - await expect(page.locator('.rfc-list li:visible').first()).toContainText('Accepted'); + await page.locator('#rfc-status').selectOption('Superseded'); + await expect(page.locator('.rfc-list li:visible').first()).toContainText('Superseded'); const result = await new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa']).analyze(); expect(result.violations).toEqual([]); }); @@ -71,8 +71,11 @@ test('documentation navigation and copy controls work', async ({ page, isMobile, const copy = page.locator('.expressive-code .copy button').first(); await expect(copy).toBeVisible(); await context.grantPermissions(['clipboard-read', 'clipboard-write']); + const firstLine = (await page.locator('.expressive-code pre').first().innerText()) + .trim() + .split('\n')[0]; await copy.click(); - expect(await page.evaluate(() => navigator.clipboard.readText())).toContain('node_free(alias)'); + expect(await page.evaluate(() => navigator.clipboard.readText())).toContain(firstLine); if (isMobile) await page.getByRole('button', { name: 'Menu', exact: true }).click(); await page .locator('#starlight__sidebar a') diff --git a/include/weavec/Analysis/Annotations.h b/include/weavec/Analysis/Annotations.h deleted file mode 100644 index 968ed894..00000000 --- a/include/weavec/Analysis/Annotations.h +++ /dev/null @@ -1,234 +0,0 @@ -//===- Annotations.h - WeaveC source annotations ---------------*- C++ -*-===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// WeaveC annotations are spelled in C as -// `__attribute__((annotate("weavec.X")))` via the macros in -// resources/include/weavec.h. This header recognises them on Clang declarations -// and statements. -// -//===----------------------------------------------------------------------===// - -#ifndef WEAVEC_ANALYSIS_ANNOTATIONS_H -#define WEAVEC_ANALYSIS_ANNOTATIONS_H - -#include "clang/AST/Attr.h" -#include "clang/AST/Decl.h" -#include "clang/AST/Stmt.h" -#include "clang/AST/Type.h" - -#include "llvm/ADT/StringRef.h" - -#include -#include -#include -#include - -namespace weavec::analysis { - -/// Annotations understood by WeaveC. -enum class Annotation : std::uint8_t { - /// `weavec.owned` -- the pointer uniquely owns its referent. - Owned, - /// `weavec.borrowed` -- shared, read-only borrow. - Borrowed, - /// `weavec.mut_borrowed` -- exclusive, mutable borrow. - MutBorrowed, - /// `weavec.raw` -- no ownership guarantee; dereferencing or releasing the - /// pointer requires an unsafe region (RFC 0004). - Raw, - /// `weavec.unsafe` -- the function body or block is an unsafe region. - Unsafe, - /// `weavec.nullable` -- the pointer may be null (RFC 0008). - Nullable, - /// `weavec.nonnull` -- the pointer is never null (RFC 0008). - NonNull, - /// `weavec.retains` -- the callee takes a reference on the argument's - /// object: the caller's place gains a share (RFC 0010). - Retains, - /// `weavec.releases` -- the callee releases one reference: the argument's - /// name is dead afterwards, other shares are untouched (RFC 0010). - Releases, - /// `weavec.refcount` -- the integer field is a reference count (RFC 0010). - Refcount, - /// `weavec.family.` -- the release family of a `WEAVEC_OWNED` pointer - /// (RFC 0010, closing RFC 0007's open item). The family is carried in - /// `AnnotationSet::family`. - Family, - /// `weavec.sized_by.` -- the pointer parameter has at least `n` - /// elements behind it, `n` another parameter of the same function (RFC - /// 0011). The name is carried in `AnnotationSet::sizedBy`. RFC 0030 §7.2 - /// makes it a synonym of `CountedBy`. - SizedBy, - /// `weavec.counted_by.` -- at least `n` elements (bytes for `void` and - /// character pointees) are accessible; `n` names a sibling parameter or - /// field (RFC 0030 §7.2). The name is carried in - /// `AnnotationSet::countedBy`. - CountedBy, - /// `weavec.ended_by.` -- `[p, q)` lies in one object; `q` names a - /// sibling pointer parameter or field (RFC 0030 §7.2). The name is - /// carried in `AnnotationSet::endedBy`. - EndedBy, - /// `weavec.string` -- the pointer is NUL-terminated within its object - /// (RFC 0030 §7.2). - String, - /// `weavec.assume` -- the function is `weavec.h`'s `weavec_assume_`: a - /// call to it states that its argument holds (RFC 0012). - Assume, - /// A `weavec.`-prefixed annotation WeaveC does not recognise. - Invalid, -}; - -/// Spellings shared with the user-facing `weavec.h` header. -namespace spelling { -inline constexpr llvm::StringLiteral Prefix = "weavec."; -inline constexpr llvm::StringLiteral Owned = "weavec.owned"; -inline constexpr llvm::StringLiteral Borrowed = "weavec.borrowed"; -inline constexpr llvm::StringLiteral MutBorrowed = "weavec.mut_borrowed"; -inline constexpr llvm::StringLiteral Raw = "weavec.raw"; -inline constexpr llvm::StringLiteral Unsafe = "weavec.unsafe"; -inline constexpr llvm::StringLiteral Nullable = "weavec.nullable"; -inline constexpr llvm::StringLiteral NonNull = "weavec.nonnull"; -inline constexpr llvm::StringLiteral Retains = "weavec.retains"; -inline constexpr llvm::StringLiteral Releases = "weavec.releases"; -inline constexpr llvm::StringLiteral Refcount = "weavec.refcount"; -/// `weavec.family.`: the prefix, followed by the family name. -inline constexpr llvm::StringLiteral FamilyPrefix = "weavec.family."; -/// `weavec.sized_by.`: the prefix, followed by the parameter name. -inline constexpr llvm::StringLiteral SizedByPrefix = "weavec.sized_by."; -/// RFC 0030 §7.2, `WEAVEC_COUNTED_BY(n)`: the prefix, followed by the name. -inline constexpr llvm::StringLiteral CountedByPrefix = "weavec.counted_by."; -/// RFC 0030 §7.2, `WEAVEC_ENDED_BY(q)`: the prefix, followed by the name. -inline constexpr llvm::StringLiteral EndedByPrefix = "weavec.ended_by."; -/// RFC 0030 §7.2, `WEAVEC_STRING`. -inline constexpr llvm::StringLiteral String = "weavec.string"; -/// RFC 0012: the annotation on `weavec_assume_`, `WEAVEC_ASSUME`'s callee. -inline constexpr llvm::StringLiteral Assume = "weavec.assume"; -} // namespace spelling - -/// Parses an `annotate` payload. Returns `std::nullopt` for annotations that -/// do not belong to WeaveC (no `weavec.` prefix) and `Annotation::Invalid` -/// for unknown WeaveC annotations. A `weavec.family.` payload with an -/// empty or malformed `` is `Invalid`. -[[nodiscard]] std::optional parseAnnotation(llvm::StringRef text); - -/// The set of WeaveC annotations attached to a declaration. -struct AnnotationSet { - bool owned = false; - bool borrowed = false; - bool mutBorrowed = false; - bool raw = false; - bool unsafe = false; - bool nullable = false; - bool nonNull = false; - /// RFC 0010. - bool retains = false; - bool releases = false; - bool refcount = false; - /// RFC 0012: `WEAVEC_ASSUME`'s callee. - bool assume = false; - bool invalid = false; - /// RFC 0010: the release family `WEAVEC_OWNED_BY(f)` names; empty when - /// there is none. Two different families on one declaration are - /// `invalid`. - std::string family; - /// RFC 0011: the parameter `WEAVEC_SIZED_BY(n)` names; empty when there - /// is none. Two different names on one declaration are `invalid`. - std::string sizedBy; - /// RFC 0030 §7.2: the name `WEAVEC_COUNTED_BY(n)` names; empty when there - /// is none. Two different names on one declaration are `invalid`. - std::string countedBy; - /// RFC 0030 §7.2: the name `WEAVEC_ENDED_BY(q)` names; empty when there - /// is none. Two different names on one declaration are `invalid`. - std::string endedBy; - /// RFC 0030 §7.2: `WEAVEC_STRING`. - bool string = false; - /// RFC 0030 §7.2: the callee releases the argument (`ownership_takes`, an - /// ecosystem attribute; no WeaveC spelling). - bool frees = false; - - [[nodiscard]] bool any() const noexcept { - return owned || borrowed || mutBorrowed || raw || unsafe || nullable || - nonNull || retains || releases || refcount || assume || invalid || - string || frees || !family.empty() || !sizedBy.empty() || - !countedBy.empty() || !endedBy.empty(); - } - /// True if the set declares a pointer kind's shape (RFC 0030 §7.2). - [[nodiscard]] bool extent() const noexcept { - return string || !sizedBy.empty() || !countedBy.empty() || !endedBy.empty(); - } - /// True if the set says something about ownership (`owned`, `borrowed`, - /// `mutBorrowed`, `raw`, `retains` or `releases`), as opposed to - /// `unsafe`/`invalid`. - [[nodiscard]] bool ownership() const noexcept { - return owned || borrowed || mutBorrowed || raw || retains || releases || - frees; - } - /// True if a raw pointer may be *asserted* into the declared ownership - /// (RFC 0004, *Laundering*): owned, borrowed or mutably borrowed. - [[nodiscard]] bool safeOwnership() const noexcept { - return owned || mutBorrowed || borrowed; - } - - /// Merges `other` into this set. - void merge(const AnnotationSet &other); -}; - -/// Collects WeaveC annotations from `decl`. -[[nodiscard]] AnnotationSet getAnnotations(const clang::Decl &decl); - -/// Annotations on a function's signature, collected over every -/// redeclaration so a prototype in a header annotates the definition in the -/// source file. -struct SignatureAnnotations { - AnnotationSet result; - std::vector params; - bool unsafe = false; - - /// True if the result or any parameter carries an ownership annotation - /// (`WEAVEC_OWNED`, `WEAVEC_BORROWED`, `WEAVEC_MUT` or `WEAVEC_RAW`). - [[nodiscard]] bool anyOwnership() const noexcept; -}; - -/// The signature annotations of `function` (RFC 0003), with RFC 0030 §7.2's -/// `malloc` and `ownership_*` attributes outside system headers read as -/// ownership contracts where no WeaveC annotation states one. -[[nodiscard]] SignatureAnnotations -collectAnnotations(const clang::FunctionDecl &function); - -/// Returns true if `stmt` is an attributed statement carrying `weavec.unsafe`. -[[nodiscard]] bool isUnsafeBlock(const clang::Stmt &stmt); - -/// The macro spelling for the ownership annotation in `set`, if exactly one -/// of `WEAVEC_OWNED`, `WEAVEC_BORROWED`, `WEAVEC_MUT`, `WEAVEC_RAW` applies -/// (the first in that order wins otherwise); null if none does. -[[nodiscard]] const char *macroSpelling(const AnnotationSet &set) noexcept; - -/// Ownership annotations written on a function *type* (RFC 0004, *Signatures -/// for function pointers*): those on the parameters of the prototype in a -/// declarator of function-pointer type, and, as the result's annotations, -/// those on the declarator itself and on any `typedef` passed through on -/// the way to the prototype. -struct FunctionTypeAnnotations { - AnnotationSet result; - std::vector params; - /// The prototype the annotations were read from, or null if the type is - /// not a (pointer to) prototyped function. - const clang::FunctionProtoType *prototype = nullptr; - - [[nodiscard]] bool anyOwnership() const noexcept; -}; - -/// Reads the function-type annotations reachable from `decl`'s written type. -/// `decl` is the variable, parameter, field or typedef the callee expression -/// of an indirect call names. -[[nodiscard]] FunctionTypeAnnotations -collectFunctionTypeAnnotations(const clang::Decl &decl); - -} // namespace weavec::analysis - -#endif // WEAVEC_ANALYSIS_ANNOTATIONS_H diff --git a/include/weavec/Analysis/AttributeReader.h b/include/weavec/Analysis/AttributeReader.h deleted file mode 100644 index cdedce8c..00000000 --- a/include/weavec/Analysis/AttributeReader.h +++ /dev/null @@ -1,81 +0,0 @@ -//===- AttributeReader.h - Declared pointer kinds (RFC 0030) ----*- C++ -*-===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// RFC 0030 §7.2: the kinds programmers declare, from every source of the -// table: -// -// WEAVEC_COUNTED_BY(n), counted(n); sized(n) for void and character -// WEAVEC_SIZED_BY(n) pointees (synonyms; parameters and fields) -// WEAVEC_ENDED_BY(q) ended-by(q) (parameters and fields) -// WEAVEC_STRING nul-terminated (parameters, fields, results) -// WEAVEC_NONNULL/_NULLABLE nullability (parameter, field, result, -// variable) -// counted_by, sized_by, counted/sized(n) on fields (and parameters -// ..._or_null where Clang accepts them), nonnull unless -// _or_null -// alloc_size(i[, j]) result sized(param i [* param j]) -// nonnull, returns_nonnull, nullability -// _Nonnull, _Nullable -// T p[static N] counted(N) nonnull -// T p[n] (a VLA parameter) counted(n) nullable -// -// The `WEAVEC_*` extent macros are resolved by name: the argument names any -// sibling parameter (in any position) or sibling field, an integer for a -// count and a pointer for an end. A name that resolves to nothing, or to -// the wrong type, is a `KindProblem` (an `invalid-annotation` warning) and -// the kind is dropped. A constant `T p[N]` without `static` is no -// requirement; it yields a `KindSuggestion` (a fix-it adding `static`). -// -// The level of each source decides precedence (§7.2): WEAVEC_* annotations, -// then ecosystem attributes outside system headers, then the `LibrarySpec` -// entry, then attributes in system headers. Shape and nullability are -// resolved separately. A conflict within one level is recorded as a -// `KindProblem` and the weaker kind (their join) is used. -// -// The ownership attributes (`malloc`, `ownership_returns`, `ownership_takes`, -// `ownership_holds`) are contracts, not kinds: they become the function's -// `OwnershipContract`, which the unknown-callee rules (§5.1) read. -// -//===----------------------------------------------------------------------===// - -#ifndef WEAVEC_ANALYSIS_ATTRIBUTEREADER_H -#define WEAVEC_ANALYSIS_ATTRIBUTEREADER_H - -#include "weavec/Analysis/KindTable.h" -#include "weavec/Core/LibrarySpec.h" - -#include "clang/AST/ASTContext.h" -#include "clang/AST/Decl.h" - -namespace weavec::analysis { - -class AttributeReader { -public: - AttributeReader(clang::ASTContext &ctx, const core::LibrarySpec &spec) - : context(ctx), library(spec) {} - - /// Reads every function the unit defines or references, every field of a - /// record the unit declares, and every pointer variable, into a new table. - [[nodiscard]] KindTable read(); - - /// The declared kinds of `function`'s parameters and result, over all its - /// redeclarations, and whether a `LibrarySpec` entry governs it. - void readFunction(const clang::FunctionDecl &function, KindTable &table); - /// The declared kind of a pointer (or array) field. - void readField(const clang::FieldDecl &field, KindTable &table); - /// The declared nullability of a pointer variable. - void readVariable(const clang::VarDecl &variable, KindTable &table); - -private: - clang::ASTContext &context; - const core::LibrarySpec &library; -}; - -} // namespace weavec::analysis - -#endif // WEAVEC_ANALYSIS_ATTRIBUTEREADER_H diff --git a/include/weavec/Analysis/BoundaryInvariants.h b/include/weavec/Analysis/BoundaryInvariants.h deleted file mode 100644 index b23fd24c..00000000 --- a/include/weavec/Analysis/BoundaryInvariants.h +++ /dev/null @@ -1,75 +0,0 @@ -//===- BoundaryInvariants.h - The §9.4 boundary invariants ------*- C++ -*-===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// RFC 0030 §9.4: two invariants hold at every boundary — every Call site, -// every library call whose row hands an argument to a callback, and every -// function exit, a call that does not return included. -// -// - *Validity*: no place the other side of the boundary can reach holds a -// released pointer, or a pointer to storage whose lifetime has ended. -// A boundary that breaks it is `unresolved(dangling-escape)`. -// - *Owner uniqueness*: no two owning places it can reach hold the same -// object. A boundary that breaks it is `unresolved(second-owner)`. -// -// Both are assumed at function entry (A1 and A3), which is what makes -// `free(s->a); free(s->b)` proven inside a cleanup function and the -// temporal facets of loads from parameters sound. An assumption a boundary -// elsewhere breaks cannot prove anything, so each row names the *place -// class* it concerns — a global `g`, or a field path `.` — -// and every temporal facet the unit *proved* for a place of that class -// takes the boundary's reason instead. `LedgerAdapter::finish` applies both -// the rows and the propagation; `weavec --whole-program` (§13.2 step 5) -// supplies the other units' rows, so the propagation is program-wide there. -// -// This component runs after the engine and never includes `Engine.h` -// (gate H2). -// -//===----------------------------------------------------------------------===// - -#ifndef WEAVEC_ANALYSIS_BOUNDARYINVARIANTS_H -#define WEAVEC_ANALYSIS_BOUNDARYINVARIANTS_H - -#include "weavec/Analysis/LedgerAdapter.h" -#include "weavec/Analysis/ProgramDatabase.h" -#include "weavec/Analysis/SiteCollector.h" -#include "weavec/Core/Ledger.h" - -#include "llvm/ADT/ArrayRef.h" - -#include -#include -#include -#include - -namespace weavec::analysis { - -/// §9.4: what the invariants decided for one unit. -struct BoundaryVerdicts { - /// The boundaries whose invariant broke, and the propagation that - /// follows from them (`BoundaryDecision::propagated`). - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::vector decisions = {}; - /// The rows `UnitExports` carries for the program-wide propagation - /// (§13.1 `boundaries`, §13.2 step 5). - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::vector exported = {}; -}; - -/// §9.4: judges what the engine published at the unit's boundaries and -/// works out the propagation. `program` holds the other units' rows at -/// link, and is empty when a unit is compiled on its own. `relied` names, -/// by site, the place classes whose entry assumption the engine's proof of -/// its temporal facet rests on (`LedgerAdapter::reliesOn`). -[[nodiscard]] BoundaryVerdicts checkBoundaryInvariants( - const SiteIndex &sites, llvm::ArrayRef published, - llvm::ArrayRef program, - const std::map> &relied = {}); - -} // namespace weavec::analysis - -#endif // WEAVEC_ANALYSIS_BOUNDARYINVARIANTS_H diff --git a/include/weavec/Analysis/BypassedDeclarations.h b/include/weavec/Analysis/BypassedDeclarations.h deleted file mode 100644 index 3e68a5d0..00000000 --- a/include/weavec/Analysis/BypassedDeclarations.h +++ /dev/null @@ -1,38 +0,0 @@ -//===- BypassedDeclarations.h - Jumps past a declaration -------*- C++ -*-===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// RFC 0030 §11: `-ftrivial-auto-var-init=zero` initialises a local where its -// declaration runs. A `goto`, a computed `goto` or a `switch` that jumps past -// the declaration skips that initialisation too, so such a local holds -// whatever the stack held, which no null check can catch. Both readers of -// this fact share one answer: the A5 measurement counts the declarations -// (`lib/Frontend/ZeroInit.cpp`), and the engine gives a use of one the -// `no-zero-init` reason instead of a null check (§2.3). -// -//===----------------------------------------------------------------------===// - -#ifndef WEAVEC_ANALYSIS_BYPASSEDDECLARATIONS_H -#define WEAVEC_ANALYSIS_BYPASSEDDECLARATIONS_H - -#include "clang/AST/Decl.h" -#include "clang/AST/Stmt.h" - -#include - -namespace weavec::analysis { - -/// The locals declared in `body` that may hold a pointer and whose -/// declaration a jump within `body` can bypass, in the order the body -/// declares them. A jump from J to label L bypasses the declaration D when -/// L follows D within D's parent and J is not in that part of the tree. -[[nodiscard]] std::vector -bypassedDeclarations(const clang::Stmt &body); - -} // namespace weavec::analysis - -#endif // WEAVEC_ANALYSIS_BYPASSEDDECLARATIONS_H diff --git a/include/weavec/Analysis/ClangLocation.h b/include/weavec/Analysis/ClangLocation.h deleted file mode 100644 index c4478855..00000000 --- a/include/weavec/Analysis/ClangLocation.h +++ /dev/null @@ -1,33 +0,0 @@ -//===- ClangLocation.h - clang <-> core source location bridging -*- C++ -//-*-===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// - -#ifndef WEAVEC_ANALYSIS_CLANGLOCATION_H -#define WEAVEC_ANALYSIS_CLANGLOCATION_H - -#include "weavec/Core/SourceLocation.h" - -#include "clang/Basic/SourceLocation.h" -#include "clang/Basic/SourceManager.h" - -namespace weavec::analysis { - -/// Resolves `loc` (through macro expansions) into a core location. The raw -/// Clang encoding is preserved in `opaque` so the frontend can report at the -/// exact original position. -[[nodiscard]] core::SourceLocation -toCoreLocation(const clang::SourceManager &sm, clang::SourceLocation loc); - -/// Recovers the Clang location stored by `toCoreLocation`, or an invalid -/// location if the core location did not originate from Clang. -[[nodiscard]] clang::SourceLocation -toClangLocation(const core::SourceLocation &loc); - -} // namespace weavec::analysis - -#endif // WEAVEC_ANALYSIS_CLANGLOCATION_H diff --git a/include/weavec/Analysis/Concurrency.h b/include/weavec/Analysis/Concurrency.h deleted file mode 100644 index a1088e95..00000000 --- a/include/weavec/Analysis/Concurrency.h +++ /dev/null @@ -1,63 +0,0 @@ -//===- Concurrency.h - What entry points share (RFC 0030 §5.3) --*- C++ -*-===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// RFC 0030 §5.3: a function a `LibrarySpec` `entry` clause hands to the -// library (`pthread_create`, `signal`, `sigaction`'s `act`) runs as an entry -// point, concurrently with or in between the unit's other code. G is the set -// of places such code may share: the pointer-carrying globals any function -// reachable from an entry target reads, writes or releases (the call graph, -// and for an indirect call every address-taken function of its type until -// the slots of §9.3 are solved), together with what the argument passed to -// the target reaches (its variable, and the target's parameters). `at-exit` -// targets run after the main flow and share nothing. -// -// A facet whose pointer is loaded from a place rooted in G is not proven -// from flow facts: its temporal facet is `trusted(concurrency)`, and a null -// or spatial facet that flow facts proved is checked (the ledger adapter -// applies both). The roots are found in the syntax of the site's operand, so -// a pointer copied out of G into a local first is not covered. -// -//===----------------------------------------------------------------------===// - -#ifndef WEAVEC_ANALYSIS_CONCURRENCY_H -#define WEAVEC_ANALYSIS_CONCURRENCY_H - -#include "weavec/Core/LibrarySpec.h" - -#include "clang/AST/ASTContext.h" -#include "clang/AST/Decl.h" -#include "clang/AST/Expr.h" - -#include "llvm/ADT/DenseSet.h" - -namespace weavec::analysis { - -/// The roots of G: variables (globals, entry parameters, entry arguments) -/// whose places a thread or signal handler may share. -struct ConcurrencyShare { - llvm::DenseSet roots; - /// Every function reachable from an entry target. - llvm::DenseSet entryReachable; - - [[nodiscard]] bool empty() const noexcept { return roots.empty(); } -}; - -/// Computes G for the unit. -[[nodiscard]] ConcurrencyShare -collectConcurrencyShare(clang::ASTContext &context, - const core::LibrarySpec &library); - -/// Whether the pointer `operand` is loaded from a place rooted in G: its -/// lvalue path (through members, subscripts and dereferences) starts at a -/// root variable. -[[nodiscard]] bool rootedInShare(const clang::Expr &operand, - const ConcurrencyShare &share); - -} // namespace weavec::analysis - -#endif // WEAVEC_ANALYSIS_CONCURRENCY_H diff --git a/include/weavec/Analysis/KindInference.h b/include/weavec/Analysis/KindInference.h deleted file mode 100644 index a999613e..00000000 --- a/include/weavec/Analysis/KindInference.h +++ /dev/null @@ -1,208 +0,0 @@ -//===- KindInference.h - Inferred pointer kinds (RFC 0030) ------*- C++ -*-===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// RFC 0030 §7.3–§7.6, before the engine runs: everything `KindTable` holds -// beyond the declared kinds, from the syntax alone. No engine fact is used, -// so there is no circularity; the engine refines what is left (a static -// parameter's argument proven to have an element, the `reliesOnSingle` -// flags, the §7.6 rounds). -// -// - Parameter defaults (§7.3): a parameter of a `static` function whose -// address is never taken gets the join of its arguments at the direct -// calls, judged by the Single-valid rules; every other parameter is -// `single nullable` by A1, with `reliesOnSingle` set when some use of -// the parameter's value in the body may rest on that default (a -// syntactic over-approximation: every use but comparisons, conversions -// to integers, pointer arithmetic and non-zero subscripts). -// - `argv` of `main` (§7.3). -// - Results (§7.3): the join over the unit's `return`s for a function the -// unit defines, the A3 default for one it only declares. -// - Slot kinds (§7.3): every pointer field and pointer variable with -// static storage starts `single` and is demoted to `unknown`, as a -// greatest fixpoint, by a store whose value is not Single-valid and by -// the stores the syntax does not show: a store through `*q` (through a -// `void *` or character lvalue: of any type) into an address-taken slot, -// where every slot counts as address-taken once a pointer to pointers is -// made from anything but a fresh allocation; its address passed to a -// callee that may store through it, or converted to a pointer to -// non-pointers; a byte-wise write into its object (a zero fill or a copy -// from the same type excepted); a store to another member of its union; -// its object made from a byte buffer. Elements of arrays of pointers -// are no slots: a load from one is never Single-valid. -// - Must-access requirements R1–R5 (§7.5), from the function's CFG and -// post-dominator tree, with the `static`/exported enforcement. -// - The §7.6 candidates, with the disqualifications before round 1. -// -// Single-valid values (§7.3): null; `&object`, an array decaying to its -// first element and a string literal; an allocation whose `LibrarySpec` -// extent is at least the object width with constant operands; a parameter -// under its A1 default or its static join; a call result whose kind is -// `single` (A3 for functions outside the unit); a load from a slot that is -// still `single`. Two sound extensions: a local that is never incremented -// or address-taken is Single-valid when every value assigned to it is -// (locals carry the least width stored, so `void *m = malloc(sizeof(T))` -// then `(T *)m` is Single-valid), and a conversion is Single-valid when the -// source's width covers the object width of the new pointee (§7.4 rule 4). -// -// "Known to return" (§7.5) for a function the unit defines is a syntactic -// greatest fixpoint: every call in its body is known to return (a -// `LibrarySpec` row without `noreturn` or `exits`, a C library, POSIX or -// platform declaration without `noreturn`, a compiler builtin, or another -// such function of the unit). Indirect and unknown callees never are. -// -// Like every §14 component outside the engine, this one depends on no part -// of the engine (RFC 0031 §2). -// -//===----------------------------------------------------------------------===// - -#ifndef WEAVEC_ANALYSIS_KINDINFERENCE_H -#define WEAVEC_ANALYSIS_KINDINFERENCE_H - -#include "weavec/Analysis/KindTable.h" -#include "weavec/Analysis/LedgerAdapter.h" -#include "weavec/Analysis/SlotCollector.h" -#include "weavec/Core/FnSlots.h" -#include "weavec/Core/LibrarySpec.h" - -#include "clang/AST/ASTContext.h" -#include "clang/AST/Decl.h" -#include "clang/AST/Expr.h" - -#include "llvm/Support/raw_ostream.h" - -#include -#include -#include -#include - -namespace weavec::analysis { - -struct KindInferenceOptions { - /// §7.5 must-access requirements. - bool mustAccess = true; - /// §7.6 candidates (the designated first cut). - bool fieldCandidates = true; - /// Drop the candidates of a pointer and an integer field that no function - /// of the unit mentions together. Dropping a candidate only loses an - /// invariant, so this is sound; it keeps the engine's store verdicts - /// proportional to the fields the code relates. - bool pruneUnrelatedCandidates = true; - /// §9.3's slots for the unit and their solution: the result of an - /// indirect call through a closed slot is Single-valid when every target's - /// result is `single`, and through an open slot it is the A3 default. - /// Without them, indirect call results are never Single-valid. - const SlotCollection *slots = nullptr; - const core::SlotSolution *slotSolution = nullptr; -}; - -/// A §7.6 candidate with the declarations it names. -struct ResolvedCandidate { - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - FieldCandidate candidate = {}; - const clang::RecordDecl *record = nullptr; - const clang::FieldDecl *pointer = nullptr; - const clang::FieldDecl *count = nullptr; -}; - -/// §7.6: a struct whose candidates were all dropped before round 1. -struct DisqualifiedRecord { - const clang::RecordDecl *record = nullptr; - /// The record type key (`struct buf`). - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::string key = {}; - /// The first reason found, and where. - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::string reason = {}; - const clang::Stmt *where = nullptr; -}; - -class KindInferenceState; - -/// What `KindInference::infer` found besides the table. -class KindInferenceResult { -public: - KindInferenceResult(); - ~KindInferenceResult(); - KindInferenceResult(KindInferenceResult &&) noexcept; - KindInferenceResult &operator=(KindInferenceResult &&) noexcept; - KindInferenceResult(const KindInferenceResult &) = delete; - KindInferenceResult &operator=(const KindInferenceResult &) = delete; - - /// §7.6: the candidates assumed at entry in round 1. - [[nodiscard]] const std::vector & - resolvedCandidates() const noexcept; - [[nodiscard]] const std::vector & - disqualified() const noexcept; - - /// §7.3: whether `value` is Single-valid for a pointer to `pointee` under - /// the final slot kinds (the §7.3 Call-site row). - [[nodiscard]] bool isSingleValid(const clang::Expr &value, - clang::QualType pointee) const; - /// The same for argument `index` of `call`, against the parameter's - /// pointee (the argument's own pointee past the prototype). - [[nodiscard]] bool argumentIsSingleValid(const clang::CallExpr &call, - unsigned index) const; - -private: - friend class KindInference; - std::unique_ptr state; -}; - -/// Completes a unit's `KindTable`. -class KindInference { -public: - KindInference(clang::ASTContext &ctx, const core::LibrarySpec &spec, - KindInferenceOptions inferenceOptions = {}) - : context(ctx), library(spec), options(inferenceOptions) {} - - /// Fills `kinds`, which holds the declared kinds `AttributeReader` read, - /// with the defaults, inferred kinds, requirements and reliance flags of - /// every function the unit defines or calls and every slot, and returns - /// the rest. Declared shapes and nullabilities are kept; each entry's - /// `kind.source` and `extentClass` describe its shape. The result refers - /// to `kinds`, the AST and `KindInferenceOptions::slots`: keep them alive - /// while it is used. - [[nodiscard]] KindInferenceResult infer(KindTable &kinds); - -private: - clang::ASTContext &context; - const core::LibrarySpec &library; - KindInferenceOptions options; -}; - -/// RFC 0030 §1 step 2: a unit's kinds with everything they rest on, built -/// once per analysis of the unit before any site or engine fact. The members -/// refer to each other and to the AST, so the bundle is built in place and -/// kept, at one address, while the engine, the interface facts and the -/// dumps use it. -struct UnitKinds { - /// The declared kinds (`AttributeReader`) completed by `KindInference`. - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - KindTable table = {}; - /// §9.3's slots and their local solution (the engine consumes them from - /// S7 on; `KindInference` judges indirect call results with them). - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - SlotCollection slots = {}; - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - core::SlotSolution solution = {}; - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - KindInferenceResult inferred = {}; - - [[nodiscard]] static std::shared_ptr - build(clang::ASTContext &context, const core::LibrarySpec &library); -}; - -/// `weavec --dump-kinds`: every entry of `kinds`, the problems and -/// suggestions and the candidates, one per line in source order (a -/// debugging aid; the format is unstable). -void dumpKinds(const KindTable &kinds, const KindInferenceResult &inferred, - clang::ASTContext &context, llvm::raw_ostream &os); - -} // namespace weavec::analysis - -#endif // WEAVEC_ANALYSIS_KINDINFERENCE_H diff --git a/include/weavec/Analysis/KindTable.h b/include/weavec/Analysis/KindTable.h deleted file mode 100644 index 3fc8823a..00000000 --- a/include/weavec/Analysis/KindTable.h +++ /dev/null @@ -1,400 +0,0 @@ -//===- KindTable.h - Pointer kinds by declaration (RFC 0030) ----*- C++ -*-===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// RFC 0030 §7, §14: the kind of every parameter, result and slot (field or -// variable) the unit's analysis consults, with where it came from. Before -// the engine runs, `AttributeReader` fills in the declared kinds (§7.2); -// `KindInference` adds the defaults, the slot kinds, the must-access -// requirements and the `reliesOnSingle` flags (§7.3–§7.5). -// -// What an entry means depends on its position: -// -// - a parameter: `kind` is what the body may assume at entry (declared, -// the §7.3 join for a static function, or the A1 default) and, when -// declared, what every call must satisfy; `mustAccess` lists the §7.5 -// requirements, enforced as `enforcement` says; -// - a result: what a call's value guarantees (declared, inferred over the -// unit's `return`s, or the A3 default for a function outside the unit); -// - a field or a variable with static storage (a *slot*): what every load -// guarantees (declared, or the §7.3 greatest fixpoint), with the stores -// that demoted it. -// -// Only a declared shape makes a *required position* for `SiteCollector` -// (§7.4 rule 4): `hasDeclaredShape`, never `hasShape`, decides it, so the -// inferred entries leave the unit's sites unchanged. -// -// An entry keeps the §7.2 precedence level of its shape and of its -// nullability separately, because the two are resolved separately: -// `WEAVEC_SIZED_BY(n)` together with a `nonnull` attribute is -// `counted(param n) nonnull`. A kind that rests only on attributes in system -// headers (level 4) is never definite and never checked; facets resting on -// it are `trusted(system-api)`. -// -// Entries are keyed by canonical declaration; a function's entries merge -// every redeclaration. -// -//===----------------------------------------------------------------------===// - -#ifndef WEAVEC_ANALYSIS_KINDTABLE_H -#define WEAVEC_ANALYSIS_KINDTABLE_H - -#include "weavec/Core/Diagnostic.h" -#include "weavec/Core/LibrarySpec.h" -#include "weavec/Core/PointerKind.h" - -#include "clang/AST/Decl.h" -#include "clang/AST/Stmt.h" -#include "clang/Basic/SourceLocation.h" -#include "clang/Basic/SourceManager.h" - -#include "llvm/ADT/ArrayRef.h" -#include "llvm/ADT/DenseMap.h" -#include "llvm/ADT/DenseSet.h" -#include "llvm/ADT/SmallVector.h" - -#include -#include -#include -#include -#include -#include -#include - -namespace weavec::analysis { - -/// §7.2 precedence, strongest first. -enum class KindLevel : std::uint8_t { - /// 1. `WEAVEC_*` annotations, wherever they are written. - Annotation, - /// 2. Ecosystem attributes (`counted_by`, `alloc_size`, `nonnull`, - /// `_Nonnull`, `[static N]`, VLA parameters) outside system headers. - Ecosystem, - /// 3. The `LibrarySpec` entry. The calls an entry governs are LibCall and - /// Release sites, which read the entry itself, so the table records only - /// that the level exists (`KindTable::governedByLibrary`): it outranks - /// level 4. - LibrarySpec, - /// 4. Attributes in system headers. - SystemHeader, -}; -inline constexpr std::size_t KindLevelCount = 4; - -[[nodiscard]] std::string_view toString(KindLevel level) noexcept; - -/// §7.5: the rule an inferred requirement came from. -enum class MustAccessRule : std::uint8_t { - /// A must-access `*p`, `p->f` or `p[0]`: single nonnull. - R1, - /// `p[i + k]` in a canonical counted loop: counted(e + k) nonnull under - /// `c < e`. - R2, - /// `*x` in a canonical loop `x = p; x < q`: ended-by(q) nonnull under - /// `p < q`. - R3, - /// A canonical scan, or a must-call passing `p` where a `LibrarySpec` row - /// requires a string: nul-terminated. - R4, - /// An unguarded must-access `p[e]`: counted(e + 1) nonnull. - R5, -}; - -[[nodiscard]] std::string_view toString(MustAccessRule rule) noexcept; - -/// §7.5: the guard of a requirement, `lhs < rhs` or `lhs <= rhs`, over -/// constants and the function's parameters (a pointer comparison for R3). -/// The requirement holds only when the guard does: a loop that runs zero -/// times accesses nothing. -struct RequirementGuard { - enum class Relation : std::uint8_t { Less, LessEqual }; - - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - core::ExtentTerm lhs = {}; - Relation relation = Relation::Less; - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - core::ExtentTerm rhs = {}; - - /// ` < ` or ` <= `, each term spelled by - /// `ExtentTerm::toString`. - [[nodiscard]] std::string toString() const; - - friend bool operator==(const RequirementGuard &, - const RequirementGuard &) = default; -}; - -/// §7.5: one requirement inferred for a pointer parameter from the body: -/// `kind` (its shape, extent term and nullability; source `Inferred`) when -/// `guard` holds, or always without one. Requirements on one parameter -/// combine by conjunction. -struct MustAccessRequirement { - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - core::PointerKind kind = {}; - std::optional guard = std::nullopt; - MustAccessRule rule = MustAccessRule::R1; - /// The access (or, for R4, the call or loop condition) that gave it. - const clang::Stmt *access = nullptr; - /// Every must-access that gives this requirement, `access` first: the - /// accesses it covers in the body (§7.5 enforcement). - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::vector covers = {}; - - /// `[ -> ] ()`. - [[nodiscard]] std::string toString() const; - - friend bool operator==(const MustAccessRequirement &, - const MustAccessRequirement &) = default; -}; - -/// §7.5: how a function's inferred requirements are enforced. -enum class RequirementEnforcement : std::uint8_t { - /// A `static` function whose address is not taken: every direct call - /// gets a spatial or null facet for the argument, checked under the - /// guard; inside the body the requirement holds. - CallSites, - /// An exported or address-taken function: inside the body, accesses - /// covered only by an extent requirement beyond `single` are - /// `trusted(caller-contract)`, and their null facets stay checked. - CallerContract, -}; - -[[nodiscard]] std::string_view -toString(RequirementEnforcement enforcement) noexcept; - -/// §7.3, §13.1 `slotKinds.demotedBy`: a store, or a store the syntax does -/// not show, that made a slot `unknown`. -struct KindDemotion { - /// The assignment, initialiser, call or conversion. - const clang::Stmt *store = nullptr; - /// Why the stored value is not Single-valid, or which hidden store it is - /// ("pointer arithmetic", "a byte-wise write by 'memcpy'", ...). - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::string reason = {}; - - friend bool operator==(const KindDemotion &, const KindDemotion &) = default; -}; - -/// The kind of one parameter, result or slot. -struct KindEntry { - /// The shape, extent term, nullability, and the shape's source: - /// `Declared` for what `AttributeReader` reads, `Inferred` for the §7.3 - /// static joins, results and slot kinds, `Default` for the A1 parameter - /// and A3 result defaults. `unknown` when only the nullability was - /// declared and nothing was inferred. - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - core::PointerKind kind = {}; - /// §7.1: exact, declared or a lower bound; none for shapes without an - /// extent term (`unknown`, `nul-terminated`, `single`'s implicit one). - std::optional extentClass = std::nullopt; - /// The level the shape came from; none when no level gave one. - std::optional shapeLevel = std::nullopt; - /// The level the nullability came from; none when no level gave one. - std::optional nullabilityLevel = std::nullopt; - /// `alloc_size(i, j)`: a `sized` result is `param i * param j` bytes; the - /// extent term holds `param i` and this holds `j`. - std::optional extentFactor = std::nullopt; - /// §7.3: some access in the body, or some store of the parameter into a - /// slot, is proven only by the Single default. Set by `KindInference` - /// for parameters whose shape is the A1 default. - bool reliesOnSingle = false; - /// §7.5 (parameters): the inferred requirements, conjoined. - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::vector mustAccess = {}; - /// §7.5: how `mustAccess` is enforced; none when it is empty. - std::optional enforcement = std::nullopt; - /// §7.3: `argv` of `main`, never assigned: `counted(param 0 plus 1) - /// nonnull` at level 4, with each `argv[i]`, `i < argc`, nul-terminated - /// and nonnull and `argv[argc]` null. Facets resting on it are - /// `trusted(system-api)`. - bool mainArgv = false; - /// §7.3 (slots): the stores that demoted the slot to `unknown`, in source - /// order; empty for a slot that stayed `single` or has a declared kind. - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::vector demotedBy = {}; - - /// A shape other than `unknown`, whatever its source. - [[nodiscard]] bool hasShape() const noexcept; - /// A shape some declaration states (§7.2), not inferred or defaulted: the - /// entry is a *required position* for PtrArith and Cast sites (§7.4) and - /// gives a Call site a spatial facet. - [[nodiscard]] bool hasDeclaredShape() const noexcept; - /// The value is never null, whatever the source of that fact. - [[nodiscard]] bool isNonnull() const noexcept; - /// Some declaration says the value is never null (or requires that). - [[nodiscard]] bool declaresNonnull() const noexcept; - /// Some §7.5 requirement is enforced at every direct call. - [[nodiscard]] bool hasEnforcedRequirement() const noexcept; - /// The shape rests only on system-header attributes (level 4). - [[nodiscard]] bool shapeFromSystemHeader() const noexcept; - /// The nullability rests only on system-header attributes (level 4). - [[nodiscard]] bool nullabilityFromSystemHeader() const noexcept; - - friend bool operator==(const KindEntry &, const KindEntry &) = default; -}; - -/// A malformed or conflicting declaration `AttributeReader` found: an -/// `invalid-annotation` warning (RFC 0030, *Diagnostics*), which -/// `kindProblemDiagnostics` spells for the unit's sink. -struct KindProblem { - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - clang::SourceLocation location = {}; - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::string message = {}; -}; - -/// §7.2: `T p[N]` without `static` is no requirement, because C gives it -/// none; it only yields this suggestion, whose fix-it inserts `insert` at -/// `location` (`static ` after the `[`). -struct KindSuggestion { - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - clang::SourceLocation location = {}; - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::string message = {}; - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::string insert = {}; -}; - -/// §7.2 `malloc`, `ownership_returns(m)`, `ownership_takes(m, i)` and -/// `ownership_holds(m, i)`: the ownership contract a function's declarations -/// state. A contract, not a kind: the unknown-callee rules (§5.1) read it. -struct OwnershipContract { - /// The result is fresh, of this family: the heap's (`free`) for - /// `malloc`, `m` for `ownership_returns(m)`. - std::optional freshResult = std::nullopt; - struct Argument { - /// 0-based. - unsigned index = 0; - // NOLINTNEXTLINE(readability-redundant-member-init): designated init - std::string family = {}; - /// `ownership_holds`: retained; `ownership_takes`: released. - bool retains = false; - - friend bool operator==(const Argument &, const Argument &) = default; - }; - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::vector arguments = {}; - /// Level 2, or 4 when every attribute sits in a system header. - KindLevel level = KindLevel::Ecosystem; - - [[nodiscard]] bool empty() const noexcept { - return !freshResult && arguments.empty(); - } - friend bool operator==(const OwnershipContract &, - const OwnershipContract &) = default; -}; - -/// §7.5: a requirement of a parameter whose covered accesses include a -/// statement (`KindTable::covering`). -struct CoveringRequirement { - /// The canonical declaration of the function that has the parameter. - const clang::FunctionDecl *function = nullptr; - unsigned param = 0; - /// The index in the parameter's `KindEntry::mustAccess`. - unsigned requirement = 0; -}; - -/// Kinds by canonical declaration. -class KindTable { -public: - [[nodiscard]] const KindEntry *param(const clang::FunctionDecl &function, - unsigned index) const; - [[nodiscard]] const KindEntry * - result(const clang::FunctionDecl &function) const; - [[nodiscard]] const KindEntry *field(const clang::FieldDecl &field) const; - [[nodiscard]] const KindEntry *variable(const clang::VarDecl &variable) const; - /// A `LibrarySpec` entry governs the calls to `function` (level 3). - [[nodiscard]] bool - governedByLibrary(const clang::FunctionDecl &function) const; - - void setParam(const clang::FunctionDecl &function, unsigned index, - KindEntry entry); - void setResult(const clang::FunctionDecl &function, KindEntry entry); - void setField(const clang::FieldDecl &field, KindEntry entry); - void setVariable(const clang::VarDecl &variable, KindEntry entry); - void setGovernedByLibrary(const clang::FunctionDecl &function); - void addProblem(KindProblem problem); - - /// §7.2: the ownership contract `function`'s attributes state, or null. - [[nodiscard]] const OwnershipContract * - ownership(const clang::FunctionDecl &function) const; - void setOwnership(const clang::FunctionDecl &function, - OwnershipContract contract); - - /// §7.2: a fix-it that would declare a kind the code implies but C does - /// not require (`T p[N]` without `static`). - void addSuggestion(KindSuggestion suggestion); - [[nodiscard]] const std::vector & - suggestions() const noexcept { - return suggestionList; - } - - [[nodiscard]] const std::vector &problems() const noexcept { - return problemList; - } - /// §7.5: the requirements that cover `access` (one of their `covers`). - [[nodiscard]] llvm::ArrayRef - covering(const clang::Stmt &access) const; - - using ParamKey = std::pair; - /// Every entry, keyed by canonical declaration (unordered). - [[nodiscard]] const llvm::DenseMap & - paramEntries() const noexcept { - return params; - } - [[nodiscard]] const llvm::DenseMap & - resultEntries() const noexcept { - return results; - } - [[nodiscard]] const llvm::DenseMap & - fieldEntries() const noexcept { - return fields; - } - [[nodiscard]] const llvm::DenseMap & - variableEntries() const noexcept { - return variables; - } - [[nodiscard]] const llvm::DenseMap & - ownershipEntries() const noexcept { - return contracts; - } - -private: - llvm::DenseMap params; - llvm::DenseMap results; - llvm::DenseMap fields; - llvm::DenseMap variables; - llvm::DenseSet library; - llvm::DenseMap contracts; - std::vector problemList; - std::vector suggestionList; - /// `covering`, built on first use after the last `setParam`. - mutable llvm::DenseMap> - coveringIndex; - mutable bool coveringBuilt = false; -}; - -/// §8: the `LibrarySpec` row that governs calls to `function`: its name, a -/// `chk` alias or either behind `__builtin_` names a row that accepts the -/// declaration's signature, and the unit does not define the function. -[[nodiscard]] std::optional -governingLibraryEntry(const clang::FunctionDecl &function, - const core::LibrarySpec &library); - -/// The parameter classes of a declaration, as `LibrarySpec` rows state them. -/// None for a declaration without a prototype. -[[nodiscard]] std::optional -librarySignatureOf(const clang::FunctionDecl &function); - -/// The table's problems as `invalid-annotation` warnings, in source order, -/// for the unit's diagnostic sink. -[[nodiscard]] std::vector -kindProblemDiagnostics(const KindTable &table, const clang::SourceManager &sm); - -} // namespace weavec::analysis - -#endif // WEAVEC_ANALYSIS_KINDTABLE_H diff --git a/include/weavec/Analysis/LedgerAdapter.h b/include/weavec/Analysis/LedgerAdapter.h deleted file mode 100644 index 0d3d1021..00000000 --- a/include/weavec/Analysis/LedgerAdapter.h +++ /dev/null @@ -1,320 +0,0 @@ -//===- LedgerAdapter.h - The engine seam (RFC 0030) -------------*- C++ -*-===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// RFC 0030 §14: the only channel from an engine to the ledger. The engine -// publishes, per site and facet, decisions, requirements' decisions and -// boundary facts, and its diagnostics with their certainty; `finish` -// completes the unit ledger (RFC 0035 §8: in memory, for the summary line): -// -// 1. every undecided facet takes its §2.6 default (with reason `budget` in -// an over-budget function); IntToPtr sites are `unresolved(raw-cast)` -// (§7.4), facets resting only on system-header attributes -// `trusted(system-api)` (§5.2); -// 2. the ledger-side rules of `WEAVEC_UNSAFE` (§6.1), the concurrency -// rules (§5.3) and `setjmp` (§5.4); -// 3. the boundary invariants' decisions and their propagation (§9.4). -// -// Within one authoritative pass, records of one facet merge by rank (§2.5); -// `beginFunction` discards every row an earlier pass recorded for the -// function. Fixpoint and Houdini rounds publish into a discarding adapter, -// which keeps nothing, diagnostics included. -// -//===----------------------------------------------------------------------===// - -#ifndef WEAVEC_ANALYSIS_LEDGERADAPTER_H -#define WEAVEC_ANALYSIS_LEDGERADAPTER_H - -#include "weavec/Analysis/SiteCollector.h" -#include "weavec/Core/Diagnostic.h" -#include "weavec/Core/Ledger.h" -#include "weavec/Core/Path.h" - -#include "clang/AST/ASTContext.h" -#include "clang/AST/Decl.h" -#include "clang/AST/Stmt.h" - -#include "llvm/ADT/DenseSet.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace weavec::analysis { - -/// §9.4: what the engine knows at a boundary about the places reachable -/// from the function's parameters and globals. -struct BoundaryFacts { - /// A place that may hold a released pointer, or a pointer to storage whose - /// lifetime has ended (`dangling-escape`). - struct Dangling { - // NOLINTNEXTLINE(readability-redundant-member-init): designated init - core::SummaryPath place = {}; - /// Where the pointee was released or its storage ended. - // NOLINTNEXTLINE(readability-redundant-member-init): designated init - core::SourceLocation released = {}; - /// The place class for propagation: a global `g`, or a field path - /// `....`. - // NOLINTNEXTLINE(readability-redundant-member-init): designated init - std::string placeClass = {}; - /// How the place is spelled here, for the row's detail. - // NOLINTNEXTLINE(readability-redundant-member-init): designated init - std::string name = {}; - }; - /// Two owning places that may hold the same object (`second-owner`). - struct SharedOwners { - // NOLINTNEXTLINE(readability-redundant-member-init): designated init - core::SummaryPath first = {}; - // NOLINTNEXTLINE(readability-redundant-member-init): designated init - core::SummaryPath second = {}; - // NOLINTNEXTLINE(readability-redundant-member-init): designated init - std::string placeClass = {}; - /// The other place's class, when it differs. - // NOLINTNEXTLINE(readability-redundant-member-init): designated init - std::string otherClass = {}; - /// How the two places are spelled here, for the row's detail. - // NOLINTNEXTLINE(readability-redundant-member-init): designated init - std::string names = {}; - /// RFC 0031 §8: `first` owns an object its own object is reachable - /// from through owning cells (an owning cycle; `second` is `first`). - bool cycle = false; - }; - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::vector dangling = {}; - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::vector sharedOwners = {}; -}; - -/// §9.4: one decision of the boundary invariants: the temporal facet of -/// `site` takes `reason`. A decision the *propagation* made applies only to -/// a facet that would otherwise be proven, because only such a facet rests -/// on the entry assumption the boundary broke. -struct BoundaryDecision { - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - core::SiteId site = {}; - core::UnresolvedReason reason = core::UnresolvedReason::DanglingEscape; - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::string detail = {}; - bool propagated = false; -}; - -/// §7.6: a counted-field invariant candidate, `count(d) == f + c` or -/// `bytes(d) == f + c` with `c` 0 or 1. -struct FieldCandidate { - /// The record type key (`struct buf`). - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::string record = {}; - /// The pointer field `d` and the integer field `f`. - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::string pointer = {}; - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::string count = {}; - /// `bytes(d)` rather than `count(d)`. - bool bytes = false; - /// `c`. - std::int64_t offset = 0; - - friend auto operator<=>(const FieldCandidate &, - const FieldCandidate &) = default; -}; - -struct LedgerAdapterOptions { - /// The unit's main source as the user named it. - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::string source = {}; - /// §5.3: whether the site's pointer is loaded from a place shared with a - /// thread or signal entry point (rooted in G). - std::function concurrent = nullptr; -}; - -/// A boundary fact or store verdict as published, with its site. -struct PublishedBoundary { - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - core::SiteId site = {}; - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - BoundaryFacts facts = {}; -}; - -/// The only channel from an engine to the ledger (§14). -class LedgerAdapter { -public: - enum class Mode : std::uint8_t { - /// The pass whose rows and diagnostics stand. - Authoritative, - /// Fixpoint and Houdini rounds: everything is dropped. - Discarding, - /// Context-specialised runs (RFC 0016, §2.6): no row is decided, but - /// the diagnostics are kept, with their certainty, for the caller to - /// report at its call. - Collecting, - /// RFC 0034 §6.1: the replay of one function along single paths. The - /// unit's sites apply as in the authoritative pass, nothing is - /// published, and every decision goes to `observer`. - Witness, - }; - - LedgerAdapter(clang::ASTContext &ctx, const SiteIndex &siteIndex, - LedgerAdapterOptions adapterOptions = {}, - Mode adapterMode = Mode::Authoritative); - /// A discarding or collecting adapter, which needs no sites. - LedgerAdapter(clang::ASTContext &ctx, Mode adapterMode); - - /// Whether decisions, requirements and the other facts are dropped: every - /// mode but the authoritative one. - [[nodiscard]] bool isDiscarding() const noexcept { - return mode != Mode::Authoritative; - } - [[nodiscard]] Mode adapterMode() const noexcept { return mode; } - [[nodiscard]] const SiteIndex &siteIndex() const noexcept { return sites; } - /// Whether `facet` applies to the site `id` (§2.1). - [[nodiscard]] bool applies(core::SiteId id, core::Facet facet) const; - - /// Starts the authoritative pass over `function` and discards every row - /// and budget mark an earlier pass recorded for it (§2.5, §2.6). - void beginFunction(const clang::FunctionDecl &function); - /// One decision for one facet of a known site; may be called more than - /// once within a pass, and records merge by rank (§2.5). For a `return` - /// or a function body, the decision is about its exit. - void decide(const clang::Stmt &site, core::Facet facet, - core::SiteOutcome outcome, - std::optional unresolved = {}, - std::optional trusted = {}, - std::string detail = {}); - /// As `decide`, for the site of `stmt` of kind `kind` (and boundary): - /// when one statement stands for several sites, such as a call to a - /// function that does not return and its exit. - void decideAs(const clang::Stmt &site, core::SiteKind kind, - std::optional boundary, core::Facet facet, - const core::FacetDecision &decision); - /// A suggested annotation (§12.1 `fixit`) for `facet` of the site of - /// `stmt` of kind `kind`: §5.1's suggestion at an unknown callee's call. - /// The first suggestion of a pass stands. - void suggest(const clang::Stmt &site, core::SiteKind kind, - std::optional boundary, core::Facet facet, - core::FixItHint fixit); - /// One requirement's decision for a LibCall, Release or Call facet, - /// merged into the facet by rank (§2.5). - void requirement(const clang::Stmt &site, core::Facet facet, - const core::FacetDecision &decision); - /// A diagnostic with its certainty; `site` and `facet` link it to its row. - /// The diagnostic's own `certainty` is set to `certainty`. The same - /// diagnostic (id, location and message) is reported once per unit, as - /// the engine's passes may find it more than once. - void report(core::Diagnostic diagnostic, core::Certainty certainty, - const clang::Stmt *site = nullptr, - std::optional facet = {}); - /// Caller-visible places at a call or exit that may hold released - /// pointers or aliased owners (§9.4). - void boundary(const clang::Stmt &site, BoundaryFacts facts); - /// §9.4 as RFC 0031 amends it: the proven temporal facet of `site` rests - /// on the entry assumption of a place of `placeClass` (its pointer is a - /// value that place held at entry, or derived from one). - void reliesOn(core::SiteId site, std::string placeClass); - /// A function body exceeded its budget (§5.5). - void overBudget(const clang::FunctionDecl &function); - /// §9.4: what `BoundaryInvariants` made of `boundaries()`; `finish` - /// records the broken boundaries and applies the propagation, after the - /// defaults. - void boundaryDecisions(std::vector decisions); - - /// RFC 0034 §6.1: a definite error of the authoritative pass that the - /// replay did not confirm. Its facet's violation becomes - /// `unresolved(unconfirmed)`, and each of its diagnostics at the site a - /// warning with the note `not confirmed on a feasible path`. - void unconfirm(const clang::Stmt &site, core::Facet facet, - std::string_view id); - /// Witness mode: called with every decision of a facet of a site. - std::function - observer; - - /// Completes the unit's ledger; see the file comment. Call once, after - /// the engine is done. - [[nodiscard]] core::Ledger finish(); - - /// The diagnostics to report, in publication order. - [[nodiscard]] const std::vector & - diagnostics() const noexcept { - return emitted; - } - [[nodiscard]] const std::vector & - boundaries() const noexcept { - return boundaryList; - } - [[nodiscard]] const std::map> & - reliances() const noexcept { - return relied; - } - /// The unit's rows as published so far (tests, dumps). - [[nodiscard]] const core::UnitLedger &unitLedger() const noexcept { - return unit; - } - -private: - struct Orphan { - const clang::Stmt *stmt = nullptr; - std::uint32_t function = 0; - core::Facet facet = core::Facet::Temporal; - }; - - clang::ASTContext &context; - const SiteIndex &sites; - LedgerAdapterOptions options; - Mode mode; - core::UnitLedger unit; - const clang::FunctionDecl *current = nullptr; - llvm::DenseSet overBudgetFunctions; - std::vector emitted; - /// (id, file, line, column, message) of what `emitted` holds, with its - /// index among the ledger's diagnostics when it was published there. - std::map, - std::optional> - emittedKeys; - std::vector ledgerDiagnostics; - std::vector orphans; - std::vector boundaryList; - std::map> relied; - std::vector boundaryRows; - bool finished = false; - - /// The record of `facet` at `id`, or null when the facet does not apply. - core::FacetRecord *record(core::SiteId id, core::Facet facet); - void noteOrphan(const clang::Stmt &stmt, core::Facet facet); - void decideAt(std::optional id, const clang::Stmt &stmt, - core::Facet facet, const core::FacetDecision &decision); - /// Records a diagnostic, linked to a facet of a site when given. - /// Links the ledger diagnostic `index` to its site's facet (a violation - /// when it is a definite error). - void link(std::uint32_t index, const core::Diagnostic &diagnostic, - core::Certainty certainty, std::optional id, - std::optional facet); - void publish(core::Diagnostic diagnostic, core::Certainty certainty, - std::optional id, - std::optional facet); - /// The emitted function whose definition holds `loc`, or the current one. - [[nodiscard]] std::string - functionNameAt(const core::SourceLocation &loc) const; - void fillDefaults(); - void applyOverrides(); - /// §9.4: records the broken boundaries and their propagation. - void applyBoundaries(); - void appendOrphanRows(); -}; - -} // namespace weavec::analysis - -#endif // WEAVEC_ANALYSIS_LEDGERADAPTER_H diff --git a/include/weavec/Analysis/ObjectEngine.h b/include/weavec/Analysis/ObjectEngine.h deleted file mode 100644 index f2b40a24..00000000 --- a/include/weavec/Analysis/ObjectEngine.h +++ /dev/null @@ -1,61 +0,0 @@ -//===- ObjectEngine.h - The RFC 0031 engine behind the seam -----*- C++ -*-===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// RFC 0031: `SafetyEngine` implemented by the object engine, whose facts live -// on abstract objects and symbolic values (docs/rfcs/0031-object-engine.md). -// -//===----------------------------------------------------------------------===// - -#ifndef WEAVEC_ANALYSIS_OBJECTENGINE_H -#define WEAVEC_ANALYSIS_OBJECTENGINE_H - -#include "weavec/Analysis/ProgramDatabase.h" -#include "weavec/Analysis/SafetyEngine.h" - -#include "clang/AST/ASTContext.h" -#include "clang/AST/Decl.h" - -#include "llvm/Support/raw_ostream.h" - -#include - -namespace weavec::analysis { - -namespace engine { -class UnitRun; -} // namespace engine - -class ObjectEngine final : public SafetyEngine { -public: - ObjectEngine(); - ~ObjectEngine() override; - ObjectEngine(const ObjectEngine &) = delete; - ObjectEngine &operator=(const ObjectEngine &) = delete; - ObjectEngine(ObjectEngine &&) = delete; - ObjectEngine &operator=(ObjectEngine &&) = delete; - - void analyzeUnit(const EngineInput &input, LedgerAdapter &out) override; - [[nodiscard]] UnitExports exports() override; - void dump(const clang::FunctionDecl &function, - llvm::raw_ostream &os) override; - /// RFC 0005 discovery: the unit's definitions, imports and indirect types - /// with empty summaries, without analysing anything. - [[nodiscard]] static UnitExports discover(const EngineInput &input); - /// After `analyzeUnit`: the summary of a definition of the unit, or null. - [[nodiscard]] const core::FunctionEffects * - summaryOf(const clang::FunctionDecl &function) const; - -private: - std::unique_ptr input; - std::unique_ptr unit; - UnitExports exported; -}; - -} // namespace weavec::analysis - -#endif // WEAVEC_ANALYSIS_OBJECTENGINE_H diff --git a/include/weavec/Analysis/ProgramDatabase.h b/include/weavec/Analysis/ProgramDatabase.h deleted file mode 100644 index f684226c..00000000 --- a/include/weavec/Analysis/ProgramDatabase.h +++ /dev/null @@ -1,207 +0,0 @@ -//===- ProgramDatabase.h - Summaries across translation units --*- C++ -*-===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// What one translation unit exports to the rest of the program and the -// database that collects those exports (RFC 0005, *Programs, units and -// exports* and *The program database*; RFC 0031 §7). Summaries in exports -// and in the database name globals through a `GlobalNames` table, so they -// mean the same thing in every unit. -// -//===----------------------------------------------------------------------===// - -#ifndef WEAVEC_ANALYSIS_PROGRAMDATABASE_H -#define WEAVEC_ANALYSIS_PROGRAMDATABASE_H - -#include "weavec/Core/Effects.h" -#include "weavec/Core/FnSlots.h" -#include "weavec/Core/Ledger.h" - -#include "clang/AST/ASTContext.h" -#include "clang/AST/Type.h" - -#include "llvm/ADT/StringRef.h" -#include "llvm/Support/raw_ostream.h" - -#include -#include -#include -#include -#include -#include -#include - -namespace weavec::analysis { - -/// Names of the globals a set of summaries refers to, by id. -class GlobalNames { -public: - [[nodiscard]] std::uint32_t idFor(llvm::StringRef name); - [[nodiscard]] llvm::StringRef nameOf(std::uint32_t id) const; - [[nodiscard]] std::size_t size() const noexcept { return names.size(); } - - friend bool operator==(const GlobalNames &, const GlobalNames &) = default; - -private: - std::vector names; - std::map> ids; -}; - -/// One function a unit exports. -struct ExportedFunction { - /// The summary (RFC 0031 §6), with globals numbered by the unit's - /// `GlobalNames`. - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - core::FunctionEffects effects = {}; - /// `functionTypeKey` of the definition; empty if the type has no stable - /// spelling (an anonymous record is involved). - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::string typeKey = {}; - /// External linkage: callable by name from another unit. - bool external = true; - /// Its address is taken somewhere in the unit: reachable through a - /// pointer of its type from another unit. - bool addressTaken = false; - - friend bool operator==(const ExportedFunction &, - const ExportedFunction &) = default; -}; - -/// RFC 0030 §9.4, §13.1 `boundaries`: one `dangling-escape` or -/// `second-owner` row of a unit, with the place class it concerns, for -/// program-wide propagation (§13.2 step 5). -struct BoundaryRow { - /// The unit's main source; empty inside the unit's own record. - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::string unit = {}; - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::string function = {}; - /// The boundary's site ordinal within `function`. - std::uint32_t site = 0; - core::UnresolvedReason reason = core::UnresolvedReason::DanglingEscape; - /// A global `g`, or a field path `....`. - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::string placeClass = {}; - - friend auto operator<=>(const BoundaryRow &, const BoundaryRow &) = default; -}; - -/// RFC 0031 §6.6, §7 *Amendment (cross-unit contexts)*: a caller's request -/// that a function another unit defines be analysed in the context of a -/// call: the arguments it makes one object, the integers it knows (and the -/// integers their objects hold), and the callbacks it passes. -struct ContextRequest { - /// The callee's portable name (its own for external linkage, - /// `#` otherwise). - std::string callee; - /// The context, spelled by the engine (`contextKeyText`). - std::string key; - - friend auto operator<=>(const ContextRequest &, - const ContextRequest &) = default; -}; - -/// What a unit exports (RFC 0031 §7). -struct UnitExports { - /// The main source file, for messages and the dump. - std::string source; - /// Exported definitions by linkage name. - std::map functions; - /// Names the summaries above use for global roots. - GlobalNames globals; - /// External-linkage callees with no definition in the unit. - std::set imports; - /// Type keys of the unit's indirect calls. - std::set indirectTypes; - /// RFC 0030 §9.4: the unit's boundary rows, for the program-wide - /// propagation of §13.2 step 5. Filled after the engine, from what - /// `BoundaryInvariants` made of the boundaries the engine published. - std::vector boundaries; - - /// The contexts this unit's calls into other units asked for. - std::set contextRequests; - /// The summaries of this unit's functions in the contexts other units - /// asked for, globals numbered by `globals`. - std::map contextEffects; - /// True if the exported summaries are the same; the fixpoint test of RFC - /// 0005's whole-program algorithm. - [[nodiscard]] bool sameSummariesAs(const UnitExports &other) const; - /// The same, leaving out the contexts asked and served (RFC 0031 §7): - /// what a cyclic component's fixpoint iterates on. - [[nodiscard]] bool sameFunctionsAs(const UnitExports &other) const; -}; - -/// RFC 0030 §13.2: what `weavec --whole-program` knows about the whole -/// program beyond the summaries, for the engine's runs over its units: the -/// function-pointer slots solved over every unit (step 2, §9.3) and every -/// unit's boundary rows (step 5, §9.4). -struct ProgramFacts { - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - core::SlotSolution slots = {}; - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::vector boundaries = {}; -}; - -/// The canonical spelling of a function type that identifies indirect-call -/// candidates across units, or an empty string when the type involves an -/// anonymous record and so has no stable spelling. -[[nodiscard]] std::string functionTypeKey(clang::QualType type, - const clang::ASTContext &context); - -[[nodiscard]] std::string recordTypeKey(clang::QualType type, - const clang::ASTContext &context); - -/// The exports of every unit of a program except the one being analysed. -class ProgramDatabase { -public: - /// RFC 0030 §13.2: the program's solved slots and boundary rows, set by - /// the whole-program driver for every run it makes; null otherwise (and - /// never cleared by `clear`). Copies share it. - std::shared_ptr programFacts = nullptr; - - /// Adds a unit's exports. A name defined by more than one unit gets the - /// join of the definitions' summaries (RFC 0005, *Accepted false - /// positives*); the globals are renumbered into `globals()`. - void add(const UnitExports &unit); - void clear(); - [[nodiscard]] bool empty() const noexcept { return byName.empty(); } - - /// The joined summary of `name`'s external definitions, or of every - /// address-taken function of type `typeKey`, with globals numbered by - /// `globals()`; null if there is none. - [[nodiscard]] const core::FunctionEffects * - findEffects(llvm::StringRef name) const; - [[nodiscard]] const core::FunctionEffects * - candidateEffects(llvm::StringRef typeKey) const; - /// The summary of `request`'s callee in its context, when its unit ran - /// it; the contexts other units asked of `callee`. - [[nodiscard]] const core::FunctionEffects * - contextEffects(const ContextRequest &request) const; - /// Every context some unit asked for. - [[nodiscard]] const std::set &requests() const noexcept { - return requested; - } - - [[nodiscard]] const GlobalNames &globals() const noexcept { - return globalNames; - } - - /// Sorted names of every exported function, then every type key with - /// candidates, with their summaries (for `--dump-analysis`). - void dump(llvm::raw_ostream &os) const; - -private: - GlobalNames globalNames; - std::map> byName; - std::map> byType; - std::map byContext; - std::set requested; -}; - -} // namespace weavec::analysis - -#endif // WEAVEC_ANALYSIS_PROGRAMDATABASE_H diff --git a/include/weavec/Analysis/SafetyEngine.h b/include/weavec/Analysis/SafetyEngine.h deleted file mode 100644 index 8b6a725d..00000000 --- a/include/weavec/Analysis/SafetyEngine.h +++ /dev/null @@ -1,111 +0,0 @@ -//===- SafetyEngine.h - The engine interface (RFC 0030) ---------*- C++ -*-===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// RFC 0030 §14: what an engine gets for one unit (`EngineInput`) and what it -// implements (`SafetyEngine`). Everything an engine produces flows through -// `LedgerAdapter`, which let RFC 0031 replace the engine (today's is -// `ObjectEngine`) without touching the ledger, the kinds or the library -// table. -// -//===----------------------------------------------------------------------===// - -#ifndef WEAVEC_ANALYSIS_SAFETYENGINE_H -#define WEAVEC_ANALYSIS_SAFETYENGINE_H - -#include "weavec/Analysis/KindInference.h" -#include "weavec/Analysis/KindTable.h" -#include "weavec/Analysis/LedgerAdapter.h" -#include "weavec/Analysis/ProgramDatabase.h" -#include "weavec/Analysis/SiteCollector.h" -#include "weavec/Core/AnalysisStats.h" -#include "weavec/Core/FnSlots.h" -#include "weavec/Core/Ledger.h" -#include "weavec/Core/LibrarySpec.h" - -#include "clang/AST/ASTContext.h" -#include "clang/AST/Decl.h" - -#include "llvm/Support/raw_ostream.h" - -#include -#include -#include -#include - -namespace weavec::analysis { - -/// An engine's tunables (§14). -struct EngineOptions { - /// `--dump-analysis`: the engine's state per function (unstable format). - llvm::raw_ostream *dumpStream = nullptr; - /// RFC 0020: optional invocation-owned work accounting. - core::AnalysisStats *stats = nullptr; - /// §5.5: block transfers per function body; 0 is unlimited. - std::uint64_t budget = core::DefaultBudget; - /// RFC 0033 §9: block transfers summed over every run in the unit - /// (summary rounds, contexts, invariant rounds, the authoritative runs); - /// none means the default for the unit's size, 0 unlimited. Functions - /// run after it is spent take the over-budget defaults. - std::optional unitBudget = std::nullopt; - /// §11: locals and allocations are zero-initialised. - bool zeroInit = true; - /// §3.1: the unit follows C's effective-type rules (not - /// `-fno-strict-aliasing`). - bool strictAliasing = true; - /// The functions whose diagnostics are reported; empty reports every - /// emitted function, the ones `EngineInput::sites` holds (§2.6, §5.6). - std::function shouldReport = nullptr; -}; - -/// Everything an engine gets for one unit (§14). -struct EngineInput { - clang::ASTContext &context; - /// `SiteCollector`: statement to site, per function. - const SiteIndex &sites; - /// Declared (and, from S6, inferred) kinds (§7). - const KindTable &kinds; - const core::LibrarySpec &library; - /// The function-pointer slots' local or program-wide solution (§9.3). - const core::FnSlots &slots; - /// The other units' exports in `weavec --whole-program`; null otherwise. - const ProgramDatabase *database = nullptr; - /// What `KindInference` found besides the table (§7.3–§7.6): the - /// Single-valid judgements and the store groups; null without inference. - const KindInferenceResult *inferred = nullptr; - /// §9.3: the unit's slot constraints and their solution, kept alive with - /// the kinds (stage S7 consumes them); null without them. - const SlotCollection *slotCollection = nullptr; - const core::SlotSolution *slotSolution = nullptr; - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - EngineOptions options = {}; -}; - -/// What an engine implements (§14). -class SafetyEngine { -public: - virtual ~SafetyEngine(); - SafetyEngine(const SafetyEngine &) = delete; - SafetyEngine &operator=(const SafetyEngine &) = delete; - SafetyEngine(SafetyEngine &&) = delete; - SafetyEngine &operator=(SafetyEngine &&) = delete; - - /// Analyses every emitted function of the unit, publishing through `out`. - virtual void analyzeUnit(const EngineInput &input, LedgerAdapter &out) = 0; - /// Summaries and exported facts for the other units of the program. - [[nodiscard]] virtual UnitExports exports() = 0; - /// `--dump-analysis` text for one function (unstable format). - virtual void dump(const clang::FunctionDecl &function, - llvm::raw_ostream &os) = 0; - -protected: - SafetyEngine() = default; -}; - -} // namespace weavec::analysis - -#endif // WEAVEC_ANALYSIS_SAFETYENGINE_H diff --git a/include/weavec/Analysis/SiteCollector.h b/include/weavec/Analysis/SiteCollector.h deleted file mode 100644 index f3fc560d..00000000 --- a/include/weavec/Analysis/SiteCollector.h +++ /dev/null @@ -1,294 +0,0 @@ -//===- SiteCollector.h - Sites of emitted functions (RFC 0030) --*- C++ -*-===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// RFC 0030 §2.1, §2.6: before any dataflow runs, `SiteCollector` walks the -// body of every *emitted* function and enumerates its sites: one operation, -// its kind, its 0-based ordinal in source order, and the facets that apply -// to it. The result is the `SiteIndex` (statement to site, per function) -// and the unit's initial ledger rows, in which every facet is undecided. -// `LedgerAdapter::finish` later fills whatever the engine did not decide -// with the §2.6 defaults, so the ledger is complete by construction. -// -// Emitted functions are the definitions outside system headers whose body -// CodeGen may emit: those `ASTContext::DeclMustBeEmitted` accepts, used -// internal-linkage definitions, and C99 `inline` and `gnu_inline` -// definitions (`available_externally`). -// -// Site kinds, and what makes a site (§2.1): -// -// deref *p, p->f, p[0] (a pointer base, a zero constant index) -// index e[i] otherwise, *(p + i), *(p - i) -// ptr-arith p + i, p - i, ++p, p++, p += i, &p[i] flowing directly into -// a required position (§7.4) -// cast a widening pointer conversion flowing directly into a -// required position; a store into a slot with a declared kind -// int-to-ptr a conversion from an integer (not a null pointer constant); -// va_arg of a pointer type -// lib-call a call a LibrarySpec row governs, without a release effect -// release a call whose row releases or reallocates an argument -// call every other call (boundary call); every return, the end of -// the body when control can reach it, and every call that -// does not return (boundary exit) -// assume WEAVEC_ASSUME(e) -// raw a dereference, subscript or release through a pointer with -// a raw origin visible in the syntax: declared WEAVEC_RAW, -// converted from an integer, loaded through such a pointer, -// or returned by a function whose result is WEAVEC_RAW -// -// Exclusions: `&*p`, `&((T *)0)->f`, unevaluated operands (`sizeof` and -// `_Alignof` of anything but a variable-length array, unselected `_Generic` -// and `__builtin_choose_expr` branches, the operands of -// `__builtin_constant_p` and `__builtin_object_size`), and the bodies of -// blocks. Initialiser lists are walked in their semantic form, and sites are -// deduplicated by statement, so shared subtrees count once. Sites in -// constant expressions (static local initialisers, case labels) are -// decided as any other. -// -//===----------------------------------------------------------------------===// - -#ifndef WEAVEC_ANALYSIS_SITECOLLECTOR_H -#define WEAVEC_ANALYSIS_SITECOLLECTOR_H - -#include "weavec/Analysis/KindTable.h" -#include "weavec/Analysis/SlotCollector.h" -#include "weavec/Analysis/SourceTerm.h" -#include "weavec/Core/Ledger.h" -#include "weavec/Core/LibrarySpec.h" - -#include "clang/AST/ASTContext.h" -#include "clang/AST/Decl.h" -#include "clang/AST/Stmt.h" -#include "clang/Basic/SourceLocation.h" - -#include "llvm/ADT/ArrayRef.h" -#include "llvm/ADT/DenseMap.h" -#include "llvm/ADT/SmallVector.h" - -#include -#include -#include -#include - -namespace weavec::analysis { - -class KindInferenceResult; - -/// A requirement of a call-like site on one argument, from the callee's -/// declared kinds or its `LibrarySpec` row. -struct ArgumentNeed { - std::uint8_t argument = 0; - /// The null facet: the argument must not be null ... - bool nonnull = false; - /// ... unless a length is zero (`null-if-zero`, §8.3) ... - bool allowedIfZero = false; - /// ... namely this term over the call's arguments; none when the row's - /// term has no C spelling at the call. - std::optional unlessZero = std::nullopt; - /// The requirement rests only on system-header attributes (§7.2 level - /// 4): its facet is `trusted(system-api)`. - bool systemApi = false; - /// §7.5: the nullability of a static callee's inferred requirement, which - /// the engine decides with the requirement's other parts. - bool inferred = false; -}; - -/// What `SiteCollector` knows about one site before the engine runs. -struct SiteInfo { - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - core::SiteId id = {}; - core::SiteKind kind = core::SiteKind::Deref; - /// Call sites only. - std::optional boundary = std::nullopt; - /// The statement the site stands for: the dereference, subscript, member - /// access, arithmetic, conversion, store or call expression; the `return` - /// statement; or the function body (the end of the body). - const clang::Stmt *stmt = nullptr; - /// The pointer the null and spatial facets are about: the dereferenced - /// pointer, the subscript's base, the released argument or the callee - /// operand of an indirect call. Null when there is none. - const clang::Expr *operand = nullptr; - /// Index sites: the subscript. - const clang::Expr *index = nullptr; - /// Call-like sites: the direct callee. - const clang::FunctionDecl *callee = nullptr; - /// LibCall, Release (and a Raw release): the row that governs the call. - std::optional library = std::nullopt; - /// Call-like sites: the arguments with a null requirement. - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::vector arguments = {}; - /// Call sites: the arguments whose parameter has a declared shape (§7.2), - /// with the kind and the parameter's pointee. - struct DeclaredShape { - std::uint8_t argument = 0; - // NOLINTNEXTLINE(readability-redundant-member-init): designated init - core::PointerKind kind = {}; - // NOLINTNEXTLINE(readability-redundant-member-init): designated init - clang::QualType pointee = {}; - }; - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::vector declaredShapes = {}; - /// Call sites: the arguments of a parameter whose Single default the - /// callee relies on that are not Single-valid (the §7.3 Call-site row). - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::vector reliance = {}; - /// The expansion range of the site's source text. - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - clang::SourceLocation begin = {}; - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - clang::SourceLocation end = {}; - /// Inside a `WEAVEC_UNSAFE` function or block (§6.1). - bool inUnsafe = false; - /// The null facet rests only on system-header attributes (§5.2). - bool nullSystemApi = false; - /// The spatial facet rests only on system-header attributes (§5.2). - bool spatialSystemApi = false; - /// The spatial facet is proven by the types alone: a constant index - /// inside a non-flexible array, or a dereference of an object's address - /// (§5.3, §5.4 keep these proven). - bool provenByType = false; -}; - -/// Every site of the unit, by function and by statement. -class SiteIndex { -public: - struct FunctionSites { - const clang::FunctionDecl *decl = nullptr; - /// The function's index in `UnitLedger::functions`. - std::uint32_t index = 0; - /// `WEAVEC_UNSAFE` on the function (§6.1). - bool unsafe = false; - /// The function calls a returns-twice function (§5.4). - bool callsSetjmp = false; - /// The expansion range of the definition. - // NOLINTNEXTLINE(readability-redundant-member-init): designated init - clang::SourceLocation begin = {}; - // NOLINTNEXTLINE(readability-redundant-member-init): designated init - clang::SourceLocation end = {}; - /// By ordinal. - // NOLINTNEXTLINE(readability-redundant-member-init): designated init - std::vector sites = {}; - }; - - [[nodiscard]] const std::vector &functions() const noexcept { - return functionList; - } - /// The emitted function `decl` (any redeclaration), or null. - [[nodiscard]] const FunctionSites * - function(const clang::FunctionDecl &decl) const; - [[nodiscard]] const SiteInfo *info(core::SiteId id) const; - - /// Every site `stmt` stands for, in ordinal order: usually one; a call to - /// a function that does not return also has its exit site; a value - /// stored into a slot with a declared kind by an initialiser list may - /// also be a Cast site. - [[nodiscard]] llvm::ArrayRef - sitesOf(const clang::Stmt &stmt) const; - /// The first site of `stmt` that is not the exit of a call that does not - /// return; for a `return` statement or a function body, its exit site. - [[nodiscard]] std::optional find(const clang::Stmt &stmt) const; - /// The site of `stmt` of kind `kind` (and, for Call sites, boundary). - [[nodiscard]] std::optional - find(const clang::Stmt &stmt, core::SiteKind kind, - std::optional boundary = std::nullopt) const; - /// The exit site of a `return` statement, a function body or a call that - /// does not return. - [[nodiscard]] std::optional - findExit(const clang::Stmt &stmt) const; - - /// The innermost site of the function that contains `loc`, whose range - /// contains `loc` and to which `facet` applies (when given). - [[nodiscard]] std::optional - innermostAt(clang::SourceLocation loc, std::optional facet, - const clang::SourceManager &sm) const; - /// The emitted function whose definition contains `loc`, or null. - [[nodiscard]] const FunctionSites * - functionAt(clang::SourceLocation loc, const clang::SourceManager &sm) const; - - /// The unit's ledger rows before any decision: every site with the - /// facets that apply to it, all undecided. - [[nodiscard]] const std::vector & - ledgers() const noexcept { - return rows; - } - -private: - friend class SiteCollector; - std::vector functionList; - std::vector rows; - llvm::DenseMap> - byStmt; - llvm::DenseMap byFunction; -}; - -/// Enumerates the sites of every emitted function of a unit. -class SiteCollector { -public: - /// `inferred`, when given, supplies the Single-valid judgements of the - /// §7.3 Call-site rows. - SiteCollector(clang::ASTContext &ctx, const KindTable &kindTable, - const core::LibrarySpec &spec, - const KindInferenceResult *kindInference = nullptr, - const SlotCollection *unitSlots = nullptr, - const core::SlotSolution *slotSolution = nullptr) - : context(ctx), kinds(kindTable), library(spec), inferred(kindInference), - slots(unitSlots), solution(slotSolution) {} - - /// §2.6: CodeGen may emit `function`'s body in some form. `function` - /// must be a definition. - [[nodiscard]] bool isEmitted(const clang::FunctionDecl &function); - /// Every emitted function of the unit, in source order. - [[nodiscard]] std::vector emittedFunctions(); - /// The sites of every emitted function. - [[nodiscard]] SiteIndex collect(); - -private: - clang::ASTContext &context; - const KindTable &kinds; - const core::LibrarySpec &library; - const KindInferenceResult *inferred; - /// RFC 0030 §9.3: an indirect call whose slot is closed on one target a - /// `LibrarySpec` row governs gets that row's sites, as a direct call to - /// the target would. - const SlotCollection *slots; - const core::SlotSolution *solution; -}; - -/// §7.5: `term`, over the callee's parameters, as a C term over `call`'s -/// arguments; none when it names a parameter the call does not pass or a -/// pointer (a pointer difference is never a check term, §10.3). -[[nodiscard]] std::optional -argumentTerm(const core::ExtentTerm &term, const clang::CallExpr &call); -/// §7.5, §10.4: the guard `c < rhs` (`c <= rhs`) of a requirement at `call` -/// as the term `rhs - c` (`rhs - c + 1`), non-zero exactly when the guard -/// holds once the term helpers saturate it at zero. None when that is not -/// exact: a pointer guard, or a term adding to a signed argument (which the -/// helpers stop at zero first). -[[nodiscard]] std::optional guardTerm(const RequirementGuard &guard, - const clang::CallExpr &call); - -/// Whether `call` is to a function that does not return: declared -/// `noreturn` (or `_Noreturn`), through a `noreturn` function type, or a -/// `LibrarySpec` row that is `noreturn` or `exits`. -[[nodiscard]] bool callDoesNotReturn(const clang::CallExpr &call, - const core::LibrarySpec &library); - -/// §5.2: whether `function` is declared in a C library, POSIX or platform -/// header: a system header under the toolchain's resource directory, or one -/// the `LibrarySpec` header list names (on Darwin, every header of the SDK). -[[nodiscard]] bool isPlatformDeclaration(const clang::FunctionDecl &function, - const core::LibrarySpec &library, - const clang::SourceManager &sm); - -/// Whether `function` is a returns-twice function (§5.4): `returns_twice`, -/// a `LibrarySpec` row marked `returns-twice`, or `__builtin_setjmp`. -[[nodiscard]] bool isReturnsTwice(const clang::FunctionDecl &function, - const core::LibrarySpec &library); - -} // namespace weavec::analysis - -#endif // WEAVEC_ANALYSIS_SITECOLLECTOR_H diff --git a/include/weavec/Analysis/SlotCollector.h b/include/weavec/Analysis/SlotCollector.h deleted file mode 100644 index d8cf32a5..00000000 --- a/include/weavec/Analysis/SlotCollector.h +++ /dev/null @@ -1,152 +0,0 @@ -//===- SlotCollector.h - Function-pointer slots of a unit (RFC 0030) -*- C++ -//-*-===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// RFC 0030 §9.3: flow-insensitive, field-based constraints over every -// function-pointer value of a unit, for `core::FnSlots` to solve. -// -// - `f ∈ S`: a function designator stored, assigned, passed or returned -// into `S`, static initialisers included; -// - `S ⊆ T`: a copy of a value from slot `S` into `T`, through casts -// between function-pointer types; a direct call copies its arguments -// into `param g i` and `result g` into what receives it; an indirect -// call through `S` copies them into `call-param i S` and out of -// `call-result S` (the solver adds the dynamic constraints); -// - `open(S)`: `S` receives a value from outside the solved program: an -// integer or data-pointer conversion, a load the analysis cannot name, a -// `va_arg`, a byte-wise write, a store to another member of a union, or -// a slot whose address escapes. Parameters of exported functions and -// results of callees without a body are open by the solver's own rules. -// -// A function designator converted to a data pointer or an integer, or -// passed where no parameter receives it (a variadic argument), escapes: it -// becomes a member of `param 0`, so its parameters are open. -// -// Names: functions with external linkage keep their name; internal ones, -// `static` variables and the records a main file defines are qualified with -// the unit (`:l_alloc`, `static :hook`, `:struct ops`). -// Array elements share their array's slot. -// -// Closedness per TU (§9.3): a field of a main-file record is closed when no -// object of the record flows through a parameter or result of a function -// that may be called from outside, through a callee without a body, through -// an externally visible variable, or through a conversion from another -// pointer type; a `static` variable is closed while its address does not -// escape. Everything else follows the solver's seed rules. -// -// Like every §14 component outside the engine, this one depends on no part -// of the engine (RFC 0031 §2). -// -//===----------------------------------------------------------------------===// - -#ifndef WEAVEC_ANALYSIS_SLOTCOLLECTOR_H -#define WEAVEC_ANALYSIS_SLOTCOLLECTOR_H - -#include "weavec/Core/FnSlots.h" -#include "weavec/Core/LibrarySpec.h" - -#include "clang/AST/ASTContext.h" -#include "clang/AST/Decl.h" -#include "clang/AST/Expr.h" - -#include "llvm/ADT/DenseMap.h" -#include "llvm/ADT/StringRef.h" -#include "llvm/Support/raw_ostream.h" - -#include -#include -#include -#include -#include - -namespace weavec::analysis { - -struct SlotCollectorOptions { - /// The unit that qualifies internal names; empty for the main file's name - /// as the user gave it. - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::string unit = {}; -}; - -/// One unit's constraints, and how its code maps onto them. -class SlotCollection { -public: - /// Every constraint, local slots included. - [[nodiscard]] const core::FnSlots &constraints() const noexcept { - return all; - } - /// §9.3's closed-slot rules for a per-TU compile. - [[nodiscard]] const core::SlotRules &rules() const noexcept { - return unitRules; - } - /// Solves the unit's constraints under its rules. - [[nodiscard]] core::SlotSolution solve() const { - return all.solve(unitRules); - } - /// §13.1 `slots`: the constraints with local slots eliminated. - [[nodiscard]] core::FnSlots exported() const { return all.withoutLocals(); } - - /// The slot an indirect call calls through; none for a direct call. - [[nodiscard]] std::optional - calleeSlot(const clang::CallExpr &call) const; - /// Every indirect call of the unit with its callee slot, in source order. - [[nodiscard]] const std::vector< - std::pair> & - indirectCalls() const noexcept { - return calls; - } - /// The declaration a function name of the constraints stands for, or - /// null (another unit's function, or ``). - [[nodiscard]] const clang::FunctionDecl *function(llvm::StringRef name) const; - /// The unit that qualifies internal names. - [[nodiscard]] const std::string &unit() const noexcept { return unitName; } - -private: - friend class SlotCollector; - friend class SlotWalker; - core::FnSlots all; - core::SlotRules unitRules; - std::vector> calls; - llvm::DenseMap callIndex; - std::map> functions; - std::string unitName; -}; - -/// Builds the §9.3 constraints of a unit. -class SlotCollector { -public: - SlotCollector(clang::ASTContext &ctx, const core::LibrarySpec &spec, - SlotCollectorOptions collectorOptions = {}) - : context(ctx), library(spec), options(std::move(collectorOptions)) {} - - [[nodiscard]] SlotCollection collect(); - - /// The name `function` has in the constraints: its own, or - /// `:` with internal linkage. - [[nodiscard]] static std::string - functionName(const clang::FunctionDecl &function, llvm::StringRef unit); - /// The record type key of a field slot: `struct ops`, or - /// `:struct ops` for a record a main file defines. - [[nodiscard]] static std::string - recordSlotKey(const clang::RecordDecl &record, - const clang::ASTContext &context, llvm::StringRef unit); - -private: - clang::ASTContext &context; - const core::LibrarySpec &library; - SlotCollectorOptions options; -}; - -/// `weavec --dump-kinds`: the constraints, the indirect calls with their -/// resolution, and the open slots with their reasons (unstable format). -void dumpSlots(const SlotCollection &slots, const core::SlotSolution &solution, - clang::ASTContext &context, llvm::raw_ostream &os); - -} // namespace weavec::analysis - -#endif // WEAVEC_ANALYSIS_SLOTCOLLECTOR_H diff --git a/include/weavec/Analysis/SourceTerm.h b/include/weavec/Analysis/SourceTerm.h deleted file mode 100644 index e5ed9e9f..00000000 --- a/include/weavec/Analysis/SourceTerm.h +++ /dev/null @@ -1,93 +0,0 @@ -//===- SourceTerm.h - Quantities spelled over C at a site ------*- C++ -*-===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// A quantity as the program could spell it at a site: a constant, `sizeof`, -// a declaration with a member/dereference path, sums, differences and -// products, the length of a string, or a C expression of the program. The -// engine names the symbols of its messages with these (`'b->cap' bytes`), -// and `SiteCollector` spells a library row's terms over a call's arguments. -// -//===----------------------------------------------------------------------===// - -#ifndef WEAVEC_ANALYSIS_SOURCETERM_H -#define WEAVEC_ANALYSIS_SOURCETERM_H - -#include "clang/AST/Decl.h" -#include "clang/AST/Expr.h" -#include "clang/AST/Type.h" - -#include -#include -#include - -namespace weavec::analysis { - -/// A quantity spelled over C names at a site. -struct SourceTerm { - enum class Kind : std::uint8_t { - Constant, - /// A declaration and a member/dereference path below it. - Place, - Add, - Sub, - Mul, - /// `operands[0] / operands[1]`, the second a positive constant, rounding - /// down: an element count from a byte extent (§7.4). - Div, - /// The length of the string `operands[0]` points to. - StrLen, - /// A C expression of the program. - Expr, - }; - - /// One step below a place: a member (`.f`) or a dereference (`*`). - /// `p->f` is a dereference followed by a member. - struct Step { - enum class Kind : std::uint8_t { Member, Deref }; - - Kind kind = Kind::Member; - /// `Member` only. - // NOLINTNEXTLINE(readability-redundant-member-init): designated init - std::string field = {}; - - [[nodiscard]] static Step member(std::string name); - [[nodiscard]] static Step deref(); - - friend bool operator==(const Step &, const Step &) = default; - }; - - Kind kind = Kind::Constant; - std::int64_t constant = 0; - /// `Place`. - const clang::ValueDecl *decl = nullptr; - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::vector path = {}; - /// `Expr`. - const clang::Expr *expr = nullptr; - /// `Add`, `Sub`, `Mul`, `Div`: the two sides; `StrLen`: the pointer. - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::vector operands = {}; - - [[nodiscard]] static SourceTerm ofConstant(std::int64_t value); - [[nodiscard]] static SourceTerm ofPlace(const clang::ValueDecl &decl, - std::vector path = {}); - [[nodiscard]] static SourceTerm ofExpr(const clang::Expr &expr); - [[nodiscard]] static SourceTerm add(SourceTerm lhs, SourceTerm rhs); - [[nodiscard]] static SourceTerm sub(SourceTerm lhs, SourceTerm rhs); - [[nodiscard]] static SourceTerm mul(SourceTerm lhs, SourceTerm rhs); - /// `lhs / divisor`, rounding down; `divisor` must be positive. - [[nodiscard]] static SourceTerm div(SourceTerm lhs, std::int64_t divisor); - [[nodiscard]] static SourceTerm strLen(SourceTerm pointer); - - /// A debugging spelling: `42`, `n`, `s->len`, `(a + b)`, `(n / 4)`. - [[nodiscard]] std::string toString() const; -}; - -} // namespace weavec::analysis - -#endif // WEAVEC_ANALYSIS_SOURCETERM_H diff --git a/include/weavec/Analysis/UnitPipeline.h b/include/weavec/Analysis/UnitPipeline.h deleted file mode 100644 index 4c92077b..00000000 --- a/include/weavec/Analysis/UnitPipeline.h +++ /dev/null @@ -1,78 +0,0 @@ -//===- UnitPipeline.h - One unit's analysis (RFC 0030) ----------*- C++ -*-===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// RFC 0030 §1, steps 2 and 3, for one translation unit: -// -// - `AttributeReader` reads the declared kinds (§7.2), `SlotCollector` -// builds and solves the unit's slots (§9.3), and `KindInference` -// completes the kinds (§7.3–§7.6) (`UnitKinds::build`); the problems -// the declarations have are the first diagnostics; -// - `SiteCollector` enumerates the sites of every emitted function (§2.6); -// - the engine (`ObjectEngine`, RFC 0031) runs through `LedgerAdapter`, -// seeded by the kinds (§15 item 14); -// - `LedgerAdapter::finish` fills the defaults and applies the ledger-side -// rules; -// - the diagnostics are reported to the caller's sink in the order the -// engine produced them. -// -// `weavec` and `weavec-cc -fweavec-diagnose` reach this through the -// Frontend's `analyzeTranslationUnit` (lib/Frontend/FrontendAction.cpp); -// the result's `ledger` is what `weavec` summarises (RFC 0035 §8). -// -//===----------------------------------------------------------------------===// - -#ifndef WEAVEC_ANALYSIS_UNITPIPELINE_H -#define WEAVEC_ANALYSIS_UNITPIPELINE_H - -#include "weavec/Analysis/KindInference.h" -#include "weavec/Analysis/LedgerAdapter.h" -#include "weavec/Analysis/ProgramDatabase.h" -#include "weavec/Analysis/SafetyEngine.h" -#include "weavec/Core/Diagnostic.h" -#include "weavec/Core/Ledger.h" -#include "weavec/Core/LibrarySpec.h" - -#include "clang/AST/ASTContext.h" - -#include -#include - -namespace weavec::analysis { - -struct UnitPipelineOptions { - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - EngineOptions engine = {}; - /// Other units' exports (RFC 0005); null when the unit is the program. - const ProgramDatabase *database = nullptr; - /// Only the RFC 0005 discovery pass: no analysis, no ledger. - bool discoverOnly = false; - /// Build the ledger. A silent round of the whole-program fixpoint needs - /// only the engine's exports. - bool buildLedger = true; -}; - -struct UnitPipelineResult { - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - UnitExports exports = {}; - /// The unit's ledger; null after a discovery pass. - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::shared_ptr ledger = {}; - /// The kinds the analysis used (§7), for the interface facts; null after - /// a discovery pass. - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::shared_ptr kinds = {}; -}; - -/// Analyses one unit, reporting its diagnostics to `out`. -[[nodiscard]] UnitPipelineResult -runUnitAnalysis(clang::ASTContext &context, const UnitPipelineOptions &options, - core::DiagnosticSink &out); - -} // namespace weavec::analysis - -#endif // WEAVEC_ANALYSIS_UNITPIPELINE_H diff --git a/include/weavec/Core/AnalysisStats.h b/include/weavec/Core/AnalysisStats.h deleted file mode 100644 index c82769d6..00000000 --- a/include/weavec/Core/AnalysisStats.h +++ /dev/null @@ -1,59 +0,0 @@ -//===- AnalysisStats.h - Analysis work accounting (RFC 0020) ---*- C++ -*-===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -#ifndef WEAVEC_CORE_ANALYSISSTATS_H -#define WEAVEC_CORE_ANALYSISSTATS_H - -#include -#include -#include -#include -#include - -namespace weavec::core { - -/// Invocation-owned measurements. Never part of an abstract state or proof. -struct AnalysisStats { - std::map> counters; - std::map> nanoseconds; - void add(std::string_view name, std::uint64_t count = 1) { - counters[std::string(name)] += count; - } - /// Raises the counter `name` to `value` (a maximum). - void atLeast(std::string_view name, std::uint64_t value) { - std::uint64_t &counter = counters[std::string(name)]; - counter = counter < value ? value : counter; - } - [[nodiscard]] std::uint64_t count(std::string_view name) const { - const auto it = counters.find(name); - return it == counters.end() ? 0 : it->second; - } -}; - -class AnalysisTimer { -public: - AnalysisTimer(AnalysisStats *stats, std::string_view name) - : elapsed(stats ? &stats->nanoseconds[std::string(name)] : nullptr), - start(stats ? Clock::now() : Clock::time_point{}) {} - ~AnalysisTimer() { - if (elapsed) - *elapsed += static_cast( - std::chrono::duration_cast(Clock::now() - - start) - .count()); - } - AnalysisTimer(const AnalysisTimer &) = delete; - AnalysisTimer &operator=(const AnalysisTimer &) = delete; - -private: - using Clock = std::chrono::steady_clock; - std::uint64_t *elapsed; - Clock::time_point start; -}; - -} // namespace weavec::core -#endif // WEAVEC_CORE_ANALYSISSTATS_H diff --git a/include/weavec/Core/Core.h b/include/weavec/Core/Core.h deleted file mode 100644 index fc0eff68..00000000 --- a/include/weavec/Core/Core.h +++ /dev/null @@ -1,31 +0,0 @@ -//===- Core.h - Umbrella header for the WeaveC core model -----*- C++ -*-===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// The core model is independent of Clang and LLVM (RFC 0031 §2): the -// abstract domain the engine runs over (`Heap`, `Zone`), summaries -// (`Effects`), the ledger, pointer kinds, the library table, -// function-pointer slots and the diagnostics interface. Programs are named -// through opaque handles and `SourceLocation`s the Analysis layer produces -// from Clang's AST. -// -//===----------------------------------------------------------------------===// - -#ifndef WEAVEC_CORE_CORE_H -#define WEAVEC_CORE_CORE_H - -#include "weavec/Core/Diagnostic.h" // IWYU pragma: export -#include "weavec/Core/Effects.h" // IWYU pragma: export -#include "weavec/Core/FnSlots.h" // IWYU pragma: export -#include "weavec/Core/Heap.h" // IWYU pragma: export -#include "weavec/Core/Ledger.h" // IWYU pragma: export -#include "weavec/Core/LibrarySpec.h" // IWYU pragma: export -#include "weavec/Core/Path.h" // IWYU pragma: export -#include "weavec/Core/PointerKind.h" // IWYU pragma: export -#include "weavec/Core/Zone.h" // IWYU pragma: export - -#endif // WEAVEC_CORE_CORE_H diff --git a/include/weavec/Core/Diagnostic.h b/include/weavec/Core/Diagnostic.h deleted file mode 100644 index 050b2314..00000000 --- a/include/weavec/Core/Diagnostic.h +++ /dev/null @@ -1,228 +0,0 @@ -//===- Diagnostic.h - Frontend-neutral diagnostics -------------*- C++ -*-===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// The core model reports problems as `Diagnostic` values through a -// `DiagnosticSink`. The frontend layer forwards them to Clang's diagnostics -// engine; tests use `DiagnosticCollector` to inspect them directly. -// -//===----------------------------------------------------------------------===// - -#ifndef WEAVEC_CORE_DIAGNOSTIC_H -#define WEAVEC_CORE_DIAGNOSTIC_H - -#include "weavec/Core/SourceLocation.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace weavec::core { - -enum class Severity : std::uint8_t { - Note, - Warning, - Error, -}; - -/// RFC 0030 §3: every diagnostic is definite (a violation on every path the -/// analysis considers) or possible. The default severity of most ids -/// depends on it (`diag::defaultSeverity`). Its JSON spelling and parser are -/// in `Ledger.h`. -enum class Certainty : std::uint8_t { Definite, Possible }; - -/// Stable identifiers for every diagnostic WeaveC can emit. These are part of -/// the user-facing contract (they appear in output and can be used to filter), -/// so treat renames as breaking changes. -namespace diag { -inline constexpr std::string_view UseAfterFree = "use-after-free"; -inline constexpr std::string_view DoubleFree = "double-free"; -inline constexpr std::string_view UseAfterMove = "use-after-move"; -inline constexpr std::string_view ConflictingBorrow = "conflicting-borrow"; -inline constexpr std::string_view LifetimeTooShort = "lifetime-too-short"; -inline constexpr std::string_view UnsafeOperation = "unsafe-operation"; -inline constexpr std::string_view AnnotationMismatch = "annotation-mismatch"; -inline constexpr std::string_view InvalidAnnotation = "invalid-annotation"; -/// RFC 0007: an owned resource whose every holder went out of reach without -/// it being released, moved, stored, returned or handed to unknown code. -inline constexpr std::string_view Leak = "leak"; -/// RFC 0007: a resource released (or moved into a consuming parameter) by a -/// function of a different release family than the one that produced it. -inline constexpr std::string_view MismatchedRelease = "mismatched-release"; -/// RFC 0008: a dereference of a pointer that is null; or such a pointer -/// passed to a callee that dereferences its parameter. RFC 0030 §3.2: only -/// definite (a pointer that may be null is left to the guards). -inline constexpr std::string_view NullDereference = "null-dereference"; -/// RFC 0008: a read of a pointer variable or a pointer field of a local -/// record before any assignment reaches it. -inline constexpr std::string_view UseOfUninitialized = "use-of-uninitialized"; -/// RFC 0008: a release (or a move into an owning parameter) of a value that -/// is not the start of a heap allocation: borrowed storage, a string -/// literal, an interior pointer. -inline constexpr std::string_view InvalidRelease = "invalid-release"; -/// RFC 0011: an access through a pointer, or a library call over a buffer, -/// that reaches past the extent of the object the pointer points into on -/// every value the facts allow. RFC 0030 §3.3: only definite, against an -/// exact extent (the `may` forms are left to the guards). -inline constexpr std::string_view OutOfBounds = "out-of-bounds"; -/// RFC 0017: a definitely invalid operation in the target integer type. -inline constexpr std::string_view InvalidIntegerOperation = - "invalid-integer-operation"; -/// RFC 0030 §6.2: a `WEAVEC_ASSUME(e)` the analysis refutes. -inline constexpr std::string_view ContradictedAssumption = - "contradicted-assumption"; -/// RFC 0030 §3.2, §8.4: an allocation's result used without a null test. -/// The only id that is off by default (`-Wweavec-allocation-failure`). -inline constexpr std::string_view AllocationFailure = "allocation-failure"; - -/// Every id, for validating user input (`-Wweavec-`). -inline constexpr std::array All{ - UseAfterFree, DoubleFree, UseAfterMove, - ConflictingBorrow, LifetimeTooShort, UnsafeOperation, - AnnotationMismatch, InvalidAnnotation, Leak, - MismatchedRelease, NullDereference, UseOfUninitialized, - InvalidRelease, OutOfBounds, InvalidIntegerOperation, - ContradictedAssumption, AllocationFailure, -}; - -[[nodiscard]] constexpr bool isKnown(std::string_view id) noexcept { - return std::ranges::any_of( - All, [id](const std::string_view known) { return known == id; }); -} - -/// Ids an RFC removed (*Diagnostics*). A `-W` flag naming one is an error, -/// `unknown WeaveC diagnostic '' (removed by RFC 00NN)`; no alias is -/// kept. RFC 0030 removed checked mode's ids, `analysis-incomplete` -/// (unresolved facets) and `annotation-required`; RFC 0035 the require -/// levels' `unresolved-operation` and `unchecked-operation`, and -/// `unanalyzed-input` with the link step. -inline constexpr std::array Removed{ - std::string_view("analysis-incomplete"), - std::string_view("annotation-required"), - std::string_view("checking-incomplete"), - std::string_view("checking-failed"), - std::string_view("unresolved-operation"), - std::string_view("unchecked-operation"), - std::string_view("unanalyzed-input"), -}; - -[[nodiscard]] constexpr bool isRemoved(std::string_view id) noexcept { - return std::ranges::any_of( - Removed, [id](const std::string_view removed) { return removed == id; }); -} - -/// The RFC that removed `id`, one of `Removed`. -[[nodiscard]] constexpr std::string_view -removedBy(std::string_view id) noexcept { - return id == "unresolved-operation" || id == "unchecked-operation" || - id == "unanalyzed-input" - ? "RFC 0035" - : "RFC 0030"; -} - -/// The severity `id` has unless the user overrides it (RFC 0030, -/// *Diagnostics*): an error when definite and a warning when possible, -/// except that -/// - `leak`, `invalid-annotation` and `allocation-failure` are always -/// warnings, and -/// - `null-dereference`, `use-of-uninitialized` and `out-of-bounds` -/// (reported only when definite), `unsafe-operation`, -/// `annotation-mismatch`, `invalid-integer-operation` and -/// `contradicted-assumption` are always errors, as is an unknown id. -[[nodiscard]] constexpr Severity defaultSeverity(std::string_view id, - Certainty certainty) noexcept { - constexpr std::array AlwaysWarnings{ - Leak, - InvalidAnnotation, - AllocationFailure, - }; - constexpr std::array ByCertainty{ - UseAfterFree, DoubleFree, UseAfterMove, ConflictingBorrow, - LifetimeTooShort, MismatchedRelease, InvalidRelease, - }; - if (std::ranges::find(AlwaysWarnings, id) != AlwaysWarnings.end()) - return Severity::Warning; - if (certainty == Certainty::Possible && - std::ranges::find(ByCertainty, id) != ByCertainty.end()) - return Severity::Warning; - return Severity::Error; -} - -/// False only for `allocation-failure`, which is reported only under -/// `-Wweavec-allocation-failure` (or `-Wweavec`). -[[nodiscard]] constexpr bool isEnabledByDefault(std::string_view id) noexcept { - return id != AllocationFailure; -} -} // namespace diag - -/// A suggested source edit: insert `insertion` at `location`. Frontends -/// render it as their native fix-it (Clang's `FixItHint`). -struct FixItHint { - SourceLocation location; - std::string insertion; - - friend bool operator==(const FixItHint &, const FixItHint &) = default; -}; - -/// A single diagnostic, optionally accompanied by explanatory notes. -struct Diagnostic { - Severity severity = Severity::Error; - /// RFC 0030 §3. With `id`, it decides the default severity and whether - /// `-Wno-weavec[-]` can drop the diagnostic (`diag::defaultSeverity`). - Certainty certainty = Certainty::Definite; - /// One of the identifiers in `weavec::core::diag`. - std::string_view id; - std::string message; - SourceLocation location; - std::vector notes; - std::vector fixits; - - /// Fluent helper for attaching a note. - Diagnostic &addNote(std::string noteMessage, SourceLocation noteLocation); -}; - -/// Receives diagnostics produced by the analyses. -class DiagnosticSink { -public: - virtual ~DiagnosticSink() = default; - virtual void report(const Diagnostic &diagnostic) = 0; - -protected: - DiagnosticSink() = default; - DiagnosticSink(const DiagnosticSink &) = default; - DiagnosticSink(DiagnosticSink &&) = default; - DiagnosticSink &operator=(const DiagnosticSink &) = default; - DiagnosticSink &operator=(DiagnosticSink &&) = default; -}; - -/// A sink that stores diagnostics in memory; useful for tests and tooling. -class DiagnosticCollector final : public DiagnosticSink { -public: - void report(const Diagnostic &diagnostic) override; - - [[nodiscard]] const std::vector &diagnostics() const & noexcept { - return items; - } - [[nodiscard]] std::vector diagnostics() && noexcept { - return std::move(items); - } - [[nodiscard]] bool empty() const noexcept { return items.empty(); } - [[nodiscard]] std::size_t size() const noexcept { return items.size(); } - void clear() noexcept { items.clear(); } - -private: - std::vector items; -}; - -} // namespace weavec::core - -#endif // WEAVEC_CORE_DIAGNOSTIC_H diff --git a/include/weavec/Core/Effects.h b/include/weavec/Core/Effects.h deleted file mode 100644 index e81a2f36..00000000 --- a/include/weavec/Core/Effects.h +++ /dev/null @@ -1,310 +0,0 @@ -//===- Effects.h - Function summaries (RFC 0031) ----------------*- C++ -*-===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// RFC 0031 §6: what a function does to its entry heap (the objects its -// parameters and the globals reach, named by `SummaryPath`s) and what it -// returns, per result case. The object engine derives it at every exit and -// instantiates it at every call; the program database joins the summaries -// of several definitions and renumbers their globals between units (§7). -// -//===----------------------------------------------------------------------===// - -#ifndef WEAVEC_CORE_EFFECTS_H -#define WEAVEC_CORE_EFFECTS_H - -#include "weavec/Core/Integer.h" -#include "weavec/Core/Path.h" -#include "weavec/Core/SourceLocation.h" - -#include -#include -#include -#include -#include -#include -#include - -namespace weavec::core { - -/// A result class (RFC 0006): what a test of the result can select. -enum class ResultClass : std::uint8_t { - Null, - NonNull, - Zero, - Positive, - Negative -}; - -[[nodiscard]] std::string_view toString(ResultClass value) noexcept; - -/// RFC 0030 §9.1's case: the result classes under which an effect holds, -/// optionally narrowed by a parameter's zero test. An empty class list is -/// `always`. -struct EffectCase { - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::vector classes = {}; - /// `param N =0` (true) or `!=0` (false). - std::optional> paramZero = std::nullopt; - /// RFC 0014: pointer parameters N and M compare equal (true) or not - /// (false). - std::optional paramsEqual = std::nullopt; - /// The value at an entry path (`global0`, `param0*.buf`) was zero (true) - /// or not (false) at entry: a lazily initialised global or field. - std::optional> entryZero = std::nullopt; - - [[nodiscard]] bool always() const noexcept { - return classes.empty() && !paramZero && !paramsEqual && !entryZero; - } - [[nodiscard]] std::string toString() const; - friend bool operator==(const EffectCase &, const EffectCase &) = default; - friend auto operator<=>(const EffectCase &, const EffectCase &) = default; -}; - -/// `scale * + constant`, or a constant: an extent or value over -/// the interface. -struct PathTerm { - std::optional path = std::nullopt; - std::int64_t scale = 1; - std::int64_t constant = 0; - - [[nodiscard]] std::string toString() const; - friend bool operator==(const PathTerm &, const PathTerm &) = default; - friend auto operator<=>(const PathTerm &, const PathTerm &) = default; -}; - -/// RFC 0015 §5, RFC 0031 §4.2 *Amendment (arrays)*: the elements whose -/// indices lie in `[from, to)`, selected by the last index step of a path. -struct ElementRange { - PathTerm from = {}; - PathTerm to = {}; - - [[nodiscard]] std::string toString() const; - friend bool operator==(const ElementRange &, const ElementRange &) = default; - friend auto operator<=>(const ElementRange &, const ElementRange &) = default; -}; - -/// An effect on an entry object named by `path`. -struct PathEffect { - enum class Kind : std::uint8_t { - /// Released (`family`). - Release, - /// Ownership moved out (RFC 0002). - Move, - /// The unknown-callee default reached it (RFC 0030 §5.1). - Unknown, - /// Kept where others can reach it (a store into a global or the - /// caller's heap). - Escape, - /// RFC 0010: a reference released or added. - ShareDown, - ShareUp, - }; - Kind kind = Kind::Release; - SummaryPath path = {}; - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::string family = {}; - EffectCase when = {}; - /// Holds on some paths only (a possible effect). - bool may = false; - /// Made by dropping a conjunct it depended on (RFC 0030 §9.1). - bool lossy = false; - /// `Release`: the released pointer was this many bytes into the object - /// (non-zero for an interior release, RFC 0008). - std::int64_t offset = 0; - /// `Release`: the released pointer was some number of bytes into the - /// object, or before it, that the callee does not know (`s - hdr(s)`): - /// `offset` is then 0 and means nothing. - bool anyOffset = false; - /// The effect holds of every element in the range the path's last index - /// step selects (the objects those elements point to). - std::optional elements = std::nullopt; - - friend bool operator==(const PathEffect &, const PathEffect &) = default; - friend auto operator<=>(const PathEffect &, const PathEffect &) = default; -}; - -/// A value the function leaves in a cell of the entry heap, or returns. -struct ValueDesc { - enum class Kind : std::uint8_t { - Null, - /// A new object of `family` with `extent` bytes. - Fresh, - /// The value an entry path held at entry (plus `offset` bytes). - Path, - /// Static storage or a string literal. - Static, - /// An integer in `[lo, hi]`, or related to `path`. - Int, - /// RFC 0031 §5.7: a pointer to the callee's frame storage, whose - /// lifetime ended when the callee returned. - Dangling, - Unknown, - /// §7 *Amendment (cross-unit contexts)*: one of the functions - /// `functions` names (portable names), a callback handed out. - Function, - }; - // NOLINTBEGIN(readability-redundant-member-init): designated-init defaults - Kind kind = Kind::Unknown; - std::string family = {}; - std::optional extent = std::nullopt; - bool zeroed = false; - std::optional path = std::nullopt; - std::optional offset = std::nullopt; - std::optional lo = std::nullopt; - std::optional hi = std::nullopt; - /// `Int`: the values as integers of their C type, where `[lo, hi]` cannot - /// hold them (an unsigned 64-bit value above `INT64_MAX`). - std::optional range = std::nullopt; - /// May be null (a pointer) on this case. - bool maybeNull = false; - /// `Fresh`: which of the function's new objects, so two cells (or the - /// result and a cell) that hold the same one say so. - std::uint32_t object = 0; - /// `Fresh`: several objects of one family (a range of elements each - /// holding its own). - bool many = false; - /// `Function`: the functions, by portable name (sorted). - std::vector functions = {}; - /// `Fresh`: a pointer into the new object at an offset the summary cannot - /// spell (`offset` says a constant one); RFC 0017's flexible tails. - bool interior = false; - /// `Unknown`: a raw pointer (RFC 0004), which stays raw in the caller; - /// `rawSome`, raw through some of a call's functions (`SymInfo::rawSome`). - bool raw = false; - bool rawSome = false; - // NOLINTEND(readability-redundant-member-init) - - [[nodiscard]] std::string toString() const; - friend bool operator==(const ValueDesc &, const ValueDesc &) = default; - friend auto operator<=>(const ValueDesc &, const ValueDesc &) = default; -}; - -struct StoreEffect { - SummaryPath dest = {}; - ValueDesc value = {}; - EffectCase when = {}; - bool may = false; - /// Every element in the range `dest`'s last index step selects holds a - /// value `value` describes (each its own). - std::optional elements = std::nullopt; - /// RFC 0013 heap outputs: `dest` lies below a new object another store of - /// the summary leaves in the cell named by `dest`'s first `contents` - /// steps (the object's contents), so its dereferences from that cell on - /// read the values the summary stores, not the entry heap's (RFC 0031 - /// §6.3). - std::optional contents = std::nullopt; - /// RFC 0031 §6.3: `value` is a new object the callee made on every result - /// class but these, where it stored null instead: on them the object was - /// never made (`*out = malloc(n); return *out != NULL;`). - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::vector absentOn = {}; - /// An `unknown` value over the bytes `[first, second)` of the object - /// `dest` names, counted from where its pointer points: the callee - /// rewrote them (a member copied, a union written by code it could not - /// see) and left the object's other bytes as they were. Without it an - /// unknown store at an object's own path rewrites every byte. - std::optional> bytes = std::nullopt; - - friend bool operator==(const StoreEffect &, const StoreEffect &) = default; - friend auto operator<=>(const StoreEffect &, const StoreEffect &) = default; -}; - -/// RFC 0012 *String facts*, RFC 0031 §6.1 `string nul-within -/// `: at every exit where the object `path` names exists, a NUL lies -/// `nulWithin` bytes from where its pointer points and, with `nulFrom`, -/// none lies from `nulFrom` bytes up to it (the string there has exactly -/// that length). -struct StringEffect { - SummaryPath path = {}; - /// As `StoreEffect::contents`: the path lies below a new object stored in - /// the cell its first `contents` steps name. - std::optional contents = std::nullopt; - PathTerm nulWithin = {}; - std::optional nulFrom = std::nullopt; - - friend bool operator==(const StringEffect &, const StringEffect &) = default; - friend auto operator<=>(const StringEffect &, const StringEffect &) = default; -}; - -/// One alternative of the result, on the classes it has. -struct ResultEffect { - ValueDesc value = {}; - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::vector classes = {}; - /// Arises only when parameter N is zero (true) or non-zero (false): a - /// call whose argument is known to fail the test never gets it. - std::optional> paramZero = std::nullopt; - - friend bool operator==(const ResultEffect &, const ResultEffect &) = default; - friend auto operator<=>(const ResultEffect &, const ResultEffect &) = default; -}; - -/// A function's summary (RFC 0031 §6.1). -struct FunctionEffects { - enum class Returns : std::uint8_t { Always, May, Never }; - // NOLINTBEGIN(readability-redundant-member-init): designated-init defaults - Returns returns = Returns::Always; - /// Set when the summary may under-approximate the function (RFC 0030 - /// §5.5): callers add the unknown-callee default. - std::optional incomplete = std::nullopt; - std::vector effects = {}; - std::vector stores = {}; - std::vector results = {}; - std::vector strings = {}; - /// RFC 0030 §9.2: a result in the class implies the path is non-null. - std::map> nonNullOn = {}; - /// Entry paths the function reads or writes through (§6.6). - std::vector reads = {}; - std::vector writes = {}; - /// The function may run code the analysis does not see (an unknown - /// callee, or a summary that is incomplete or says this), which may write - /// any global, the caller's unit's included: a call forgets what the - /// caller's globals hold, as a call of unknown code does. - bool unknownGlobals = false; - // NOLINTEND(readability-redundant-member-init) - - [[nodiscard]] bool empty() const noexcept { - return effects.empty() && stores.empty() && results.empty() && - strings.empty() && !incomplete && !unknownGlobals; - } - friend bool operator==(const FunctionEffects &, - const FunctionEffects &) = default; -}; - -/// The summary as text lines, for dumps. -[[nodiscard]] std::string toText(const FunctionEffects &effects); - -/// A summary sound for a call that may reach either function: effects on -/// one side only become possible ones, results are the alternatives of -/// both, a non-null guarantee holds only where both give it. -[[nodiscard]] FunctionEffects joinEffects(const FunctionEffects &left, - const FunctionEffects &right); - -/// RFC 0031 §6.4: the widening of a recursive function's summary `previous` -/// by the next round's `next`: their join, where the integer results of one -/// case merge into one whose bounds that moved from `previous` are dropped, -/// so a component's rounds stop changing. -[[nodiscard]] FunctionEffects widenEffects(const FunctionEffects &previous, - const FunctionEffects &next); - -/// The id a global root has in another numbering, or none when that side -/// has no such global. -using GlobalRenumbering = - std::function(std::uint32_t)>; - -/// `effects` with every global root renumbered. On the other side a global -/// it does not have cannot be named: a store into one is dropped, a value -/// read from one is unknown, and an effect on an object reached through one -/// makes the summary incomplete (RFC 0030 §5.5), since the object may be -/// reachable there through other pointers. -[[nodiscard]] FunctionEffects renumberGlobals(const FunctionEffects &effects, - const GlobalRenumbering &map); - -} // namespace weavec::core - -#endif // WEAVEC_CORE_EFFECTS_H diff --git a/include/weavec/Core/FnSlots.h b/include/weavec/Core/FnSlots.h deleted file mode 100644 index a30ad3fd..00000000 --- a/include/weavec/Core/FnSlots.h +++ /dev/null @@ -1,343 +0,0 @@ -//===- FnSlots.h - Function-pointer slots (RFC 0030) -----------*- C++ -*-===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// RFC 0030 §9.3: flow-insensitive, field-based constraints over every -// function-pointer value, solved to a fixpoint over function names. The -// solution says which functions a call through a slot can reach and whether -// the slot is *closed* (every value it can hold is visible to the solver). -// -// Slot keys (§9.3): -// -// field array elements share the field -// global -// static : a TU-local static global -// param -// result -// local TU-private, eliminated before export -// -// The dynamic call constraints ("for an indirect call through S and each -// target f of S, the arguments flow into `param f ` and `result f` flows -// to the receiver") are expressed with two more keys, so that every -// constraint is a plain inclusion and the §13.1 export rows -// `{slot, targets, sources, open}` can carry them: -// -// call-param what calls through S pass as argument i -// call-result what calls through S return -// -// For each target `f` of `S`, the solver adds `call-param ⊆ param f -// ` and `result f ⊆ call-result `. -// -// Constraints are `f ∈ S`, `S ⊆ T` and `open(S)`. Function names are opaque -// strings; the collector qualifies internal-linkage functions (and the -// record type keys of structs defined in a main file) with their unit, as -// `static` globals are, so that names from different units never collide. -// -//===----------------------------------------------------------------------===// - -#ifndef WEAVEC_CORE_FNSLOTS_H -#define WEAVEC_CORE_FNSLOTS_H - -#include "weavec/Core/Ledger.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace weavec::core { - -enum class SlotKind : std::uint8_t { - Field, - Global, - Static, - Param, - Result, - Local, - CallParam, - CallResult, -}; - -/// A function name that no program defines: the target of a call through a -/// value from outside. Its result slot is open, and its parameter slots pass -/// their values to unknown code. -inline constexpr std::string_view UnknownFunction = ""; - -struct SlotKey { - SlotKind kind = SlotKind::Global; - /// Field: the record type key. Static: the unit. Param, Result, Local: the - /// function. CallParam, CallResult: the spelling of the callee slot. - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::string scope = {}; - /// Field: the field. Global, Static, Local: the variable. - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::string name = {}; - /// Param, CallParam: the 0-based parameter index. - std::uint32_t index = 0; - - [[nodiscard]] static SlotKey field(std::string record, std::string field); - [[nodiscard]] static SlotKey global(std::string name); - [[nodiscard]] static SlotKey staticGlobal(std::string unit, std::string name); - [[nodiscard]] static SlotKey param(std::string function, std::uint32_t index); - [[nodiscard]] static SlotKey result(std::string function); - [[nodiscard]] static SlotKey local(std::string function, std::string name); - [[nodiscard]] static SlotKey callParam(const SlotKey &callee, - std::uint32_t index); - [[nodiscard]] static SlotKey callResult(const SlotKey &callee); - - /// The spelling in the file comment. - [[nodiscard]] std::string toString() const; - [[nodiscard]] static std::optional parse(std::string_view text); - - /// The callee slot of a `call-param` or `call-result` key. - [[nodiscard]] std::optional callee() const; - /// `local` keys, and call keys through a local callee: TU-private. - [[nodiscard]] bool isLocal() const; - /// `:` of a `static` key, as `SlotRules::escapedStatics` - /// spells it. - [[nodiscard]] std::string staticName() const { return scope + ":" + name; } - - friend bool operator==(const SlotKey &, const SlotKey &) = default; - friend std::strong_ordering operator<=>(const SlotKey &, - const SlotKey &) = default; -}; - -/// One constraint: `function ∈ slot`, `from ⊆ slot`, or `open(slot)`. -struct SlotConstraint { - enum class Kind : std::uint8_t { Member, Subset, Open }; - - Kind kind = Kind::Member; - /// Member and Open: the slot. Subset: the destination. - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - SlotKey slot = {}; - /// Subset: the source. - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - SlotKey from = {}; - /// Member: the function. - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::string function = {}; - /// Open: where the outside value comes from, in words. - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::string detail = {}; - - [[nodiscard]] static SlotConstraint member(std::string function, - SlotKey slot); - [[nodiscard]] static SlotConstraint subset(SlotKey from, SlotKey to); - [[nodiscard]] static SlotConstraint open(SlotKey slot, std::string detail); - - friend bool operator==(const SlotConstraint &, - const SlotConstraint &) = default; - friend std::strong_ordering operator<=>(const SlotConstraint &, - const SlotConstraint &) = default; -}; - -/// §13.1 `slots`: one exported slot with the constraints that flow into it. -struct SlotRow { - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - SlotKey slot = {}; - /// `f ∈ slot`, sorted. - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::vector targets = {}; - /// `source ⊆ slot`, sorted. - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::vector sources = {}; - /// `open(slot)` with its detail (the least one, when there are several). - std::optional open = std::nullopt; - - friend bool operator==(const SlotRow &, const SlotRow &) = default; -}; - -enum class SlotScope : std::uint8_t { - /// A per-TU compile. - Unit, - /// Every unit of a program (`weavec --whole-program`). - Program, -}; - -/// The inputs of §9.3's closed-slot rules. -/// -/// Openness is seeded by these rules and by `open(S)` constraints, and -/// propagates along inclusions: a slot that can receive a value from an -/// open slot is open. Seeds: -/// -/// - `field`: open unless its record is in `confinedRecords`, or in a -/// closed-world program; -/// - `global`: open unless in a closed-world program; -/// - `static`: open when in `escapedStatics`; -/// - `param f i`: open when `f` is exported (unless in a closed-world -/// program), or when `f`'s address reaches an open position; -/// - `result f`: open when `f` has no body in the solved program. -/// -/// A function's address reaches an open position when it is a target of a -/// slot whose values code outside the solved program can see: a `field`, -/// `global` or `static` slot seeded open, `param g i` of a function `g` -/// without a body, `result g` of an exported `g` (unless in a closed-world -/// program), or `call-param i S` with `S` open. Local, call and the remaining -/// parameter and result slots are closed unless openness reaches them. -struct SlotRules { - SlotScope scope = SlotScope::Unit; - /// Functions with a body in the solved program: the unit, or every unit - /// of the program. - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::set defined = {}; - /// Externally visible functions. - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::set exported = {}; - /// Record type keys of structs defined in a main file (not a header) - /// whose objects never flow through parameters, results, unknown callees - /// or externally visible globals. Their fields are closed in both scopes. - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::set confinedRecords = {}; - /// `:` of `static` globals whose address escapes. - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::set escapedStatics = {}; - /// Program: it is an executable (some unit defines `main`). - bool executable = true; - - /// For a whole executable no code outside the solved program can store - /// into its slots or call its functions. - [[nodiscard]] bool closedWorld() const noexcept { - return scope == SlotScope::Program && executable; - } -}; - -/// Why a slot is open: the slot where the openness entered (`seed`) and why -/// that one is open. At an open indirect call, the call's temporal detail -/// names it (for example "values stored by 'json_set_alloc_funcs' parameter -/// 1"). -struct OpenSource { - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - SlotKey seed = {}; - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::string detail = {}; - - friend bool operator==(const OpenSource &, const OpenSource &) = default; -}; - -/// §9.3's four cases at an indirect call, plus a closed slot with no value -/// (a call through it can only be a call through null, which its `nonnull` -/// check traps). -enum class IndirectCallKind : std::uint8_t { - /// Closed, no target. - ClosedEmpty, - /// Closed, one target: analysed exactly as a direct call. - ClosedSingle, - /// Closed, several targets: the join of their summaries. - ClosedJoin, - /// Open with known targets: as closed for temporal facts only; the - /// temporal facet is `trusted(extern-contract)`. - OpenKnown, - /// Open without known targets: the unknown-callee default with reason - /// `callback`. - OpenUnknown, -}; - -[[nodiscard]] std::string_view toString(IndirectCallKind kind) noexcept; - -struct CallResolution { - IndirectCallKind kind = IndirectCallKind::ClosedEmpty; - /// Sorted. - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::vector targets = {}; - std::optional open = std::nullopt; - - friend bool operator==(const CallResolution &, - const CallResolution &) = default; -}; - -/// The temporal facet of a call through an open slot (§9.3): -/// `trusted(extern-contract)` with known targets, `unresolved(callback)` -/// without; the detail names the open source. None for closed slots, whose -/// calls are analysed through their targets. -[[nodiscard]] std::optional -openCallTemporalDecision(const CallResolution &resolution); - -/// The solved slots. -class SlotSolution { -public: - /// The functions `slot` may hold; empty for a slot the solver never saw. - [[nodiscard]] const std::set &targets(const SlotKey &slot) const; - [[nodiscard]] bool isOpen(const SlotKey &slot) const { - return openSource(slot).has_value(); - } - /// Why `slot` is open. Slots the solver never saw are judged by the seed - /// rules alone. - [[nodiscard]] std::optional openSource(const SlotKey &slot) const; - /// Whether `function`'s address reaches an open position, so that code - /// outside the solved program may call it. - [[nodiscard]] bool escapes(std::string_view function) const; - /// §9.3: how to treat an indirect call through `callee`. - [[nodiscard]] CallResolution resolveCall(const SlotKey &callee) const; - /// Every slot the solver saw, sorted. - [[nodiscard]] std::vector slots() const; - /// Worklist steps the solver took (for statistics and tests). - [[nodiscard]] std::size_t steps() const noexcept { return stepCount; } - -private: - friend class FnSlots; - - struct SlotState { - std::set targets; - std::optional open; - }; - - SlotRules rules; - std::map states; - std::set> escaped; - std::size_t stepCount = 0; -}; - -/// The constraints of one unit, or of a whole program. -class FnSlots { -public: - void add(SlotConstraint constraint); - void addMember(std::string function, SlotKey slot); - void addSubset(SlotKey from, SlotKey to); - void addOpen(SlotKey slot, std::string detail); - /// The union with `other`'s constraints (a whole program). - void merge(const FnSlots &other); - - /// Sorted and without duplicates. - [[nodiscard]] const std::set &constraints() const noexcept { - return all; - } - [[nodiscard]] bool empty() const noexcept { return all.empty(); } - - /// Solves the constraints under `rules`: set inclusion to a fixpoint over - /// function names, adding the dynamic call inclusions as targets appear. - [[nodiscard]] SlotSolution solve(const SlotRules &rules) const; - - /// The same constraints over non-local slots only (§9.3: "with local - /// slots eliminated"). Values that pass through locals are connected - /// directly: a local's sources flow to its sinks, a call through a local - /// becomes a call through each non-local slot the local reads (or a direct - /// call of each function stored in it), and a call through a local that - /// holds an outside value becomes a call of `UnknownFunction`. Solving the - /// result gives every non-local slot the same targets and openness as - /// solving the original. - [[nodiscard]] FnSlots withoutLocals() const; - - /// The §13.1 export rows: one per destination slot, sorted. - [[nodiscard]] std::vector rows() const; - [[nodiscard]] static FnSlots fromRows(std::span rows); - - friend bool operator==(const FnSlots &, const FnSlots &) = default; - -private: - std::set all; -}; - -} // namespace weavec::core - -#endif // WEAVEC_CORE_FNSLOTS_H diff --git a/include/weavec/Core/Heap.h b/include/weavec/Core/Heap.h deleted file mode 100644 index 576ff9c9..00000000 --- a/include/weavec/Core/Heap.h +++ /dev/null @@ -1,1030 +0,0 @@ -//===- Heap.h - The object engine's abstract heap ---------------*- C++ -*-===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// RFC 0031 §4: the abstract domain of the object engine. -// -// - A *symbol* (`Sym`) names one runtime value. Copying a value copies its -// symbol, so a fact on a symbol, or on the objects it points to, is seen -// through every variable, cell and expression that holds it. -// - An *abstract object* (`ObjectId`) stands for one runtime object when it -// is singular, or for several. Objects are interned per function in an -// `ObjectTable` by their origin, so two states name the same object the -// same way and joins match objects by identity. -// - A *cell* is `(object, key)`: a byte offset, or the summary cell of an -// element position. Memory maps cells to symbols. -// - Numbers live in a `Zone` over symbols. -// -// Everything here is independent of Clang: program entities are opaque -// `Handle`s the Analysis layer assigns, and questions only the frontend can -// answer (type compatibility, owning slots) go through `HeapOracle`. -// -//===----------------------------------------------------------------------===// - -#ifndef WEAVEC_CORE_HEAP_H -#define WEAVEC_CORE_HEAP_H - -#include "weavec/Core/Integer.h" -#include "weavec/Core/Path.h" -#include "weavec/Core/Persistent.h" -#include "weavec/Core/PointerKind.h" -#include "weavec/Core/SourceLocation.h" -#include "weavec/Core/Zone.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace weavec::core { - -/// An opaque frontend handle: a declaration, an expression, a type or a -/// site. Zero is none. -using Handle = std::uint64_t; -/// An abstract object, interned per function analysis. Zero is none. -using ObjectId = std::uint32_t; - -//===----------------------------------------------------------------------===// -// Objects -//===----------------------------------------------------------------------===// - -enum class ObjectKind : std::uint8_t { - /// A local variable, parameter's storage or compound literal. - Local, - Global, - /// A string literal (read-only). - Literal, - /// A function (the target of a function pointer). - Function, - /// The most recent allocation of a site (recency abstraction). - HeapRecent, - /// Every earlier allocation of a site. - HeapOld, - /// An object of the entry heap, named by its path (§4.6). - Entry, - /// The k-limited summary of the entry objects below a path. - EntrySummary, - /// A singular object focused out of a non-singular one (§4.6). - Materialized, - /// The object a variable points to at a join where its incoming paths - /// point to different objects (§4.6, *Amendment (S1)*): singular, and - /// possibly equal to each of its candidates. - Focus, - /// The result of a call the engine cannot see into, per call site. - CallResult, - /// What an unknown or raw pointer points to. - Unknown, -}; - -[[nodiscard]] std::string_view spell(ObjectKind kind) noexcept; - -/// What names an object deterministically. -struct ObjectKey { - ObjectKind kind = ObjectKind::Unknown; - /// The declaration, literal, site or function. - Handle handle = 0; - /// `Entry`, `EntrySummary`: the entry path. - SummaryPath path = {}; - /// `Materialized`: the object it was focused out of. Dead copies (§4.6 - /// garbage collection) of an object name it here with `dead` set. - ObjectId parent = 0; - /// `Focus`: the variable's object and cell offset. - std::int64_t cell = 0; - /// A copy kept only for the summary and for aliasing questions after the - /// object became unreachable (§4.6). - bool dead = false; - /// `Local`: `handle` is the expression that makes it (a compound literal, - /// a call's record result), not a variable's declaration. - bool expression = false; - - friend bool operator==(const ObjectKey &, const ObjectKey &) = default; - friend std::strong_ordering operator<=>(const ObjectKey &, - const ObjectKey &) = default; -}; - -/// What is known about an object independently of the program point. -struct ObjectInfo { - ObjectKey key = {}; - /// The object's type, as a frontend handle (zero when unknown or bytes). - Handle type = 0; - /// Whether the object stands for at most one runtime object. - bool singular = true; - /// `Entry`: the concrete cell whose entry value points to it (holder, - /// byte offset): it exists where that value is not null. - std::optional> heldIn = std::nullopt; - /// §4.5 D3: the object from whose owning slot this one was reached (for - /// `Entry`, `EntrySummary`, `Materialized` and `CallResult` objects). - ObjectId ownedFrom = 0; - /// §4.5 D2: loaded from an owning place. - bool fromOwningSlot = false; - /// For messages: the object as the program spells it (`p->next`, `buf`). - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::string name = {}; - /// Where it was created (allocation site, declaration). - SourceLocation created = {}; -}; - -struct HeapState; -struct SymInfo; -struct CellKey; - -/// Answers the questions about the program the domain cannot. -class HeapOracle { -public: - virtual ~HeapOracle(); - HeapOracle() = default; - HeapOracle(const HeapOracle &) = delete; - HeapOracle &operator=(const HeapOracle &) = delete; - HeapOracle(HeapOracle &&) = delete; - HeapOracle &operator=(HeapOracle &&) = delete; - - /// §4.5 D1: whether objects of these types may be the same object under - /// the unit's aliasing rules. Zero handles (unknown types) may alias - /// everything. - [[nodiscard]] virtual bool typesMayAlias(Handle first, - Handle second) const = 0; - /// Materialises the value of a cell of `object` this state never wrote, - /// as a load would (§4.6), writing it into the cell (except a summary - /// cell: its value is then that of an element no store reached). `hint` is - /// the value the cell holds on the other side of a join, for its type. - virtual Sym unwritten(HeapState &state, ObjectId object, CellKey key, - const SymInfo &hint) const = 0; -}; - -/// The objects of one function analysis, shared by all of its states. -class ObjectTable { -public: - /// The object named `key`, creating it with `info` on first use. - ObjectId intern(const ObjectKey &key, ObjectInfo info); - [[nodiscard]] std::optional lookup(const ObjectKey &key) const; - [[nodiscard]] const ObjectInfo &info(ObjectId id) const { - return objects.at(id - 1); - } - [[nodiscard]] std::size_t size() const noexcept { return objects.size(); } - /// Gives an untyped object (a `void *` allocation) its type on first - /// typed use. - void setTypeIfUnknown(ObjectId id, Handle type) { - ObjectInfo &object = objects.at(id - 1); - if (object.type == 0) - object.type = type; - } - /// The dead copy of `id` (§4.6). - ObjectId deadCopy(ObjectId id); - /// The object `id` is a dead copy of, or `id` itself. - [[nodiscard]] ObjectId liveVersion(ObjectId id) const; - -private: - std::vector objects; - std::map byKey; -}; - -//===----------------------------------------------------------------------===// -// Terms, targets and records -//===----------------------------------------------------------------------===// - -/// `scale * var + constant` bytes, or unknown (§4.3). -struct Term { - Sym var = ZeroSym; - std::int64_t scale = 0; - std::int64_t constant = 0; - bool known = true; - - [[nodiscard]] static Term of(std::int64_t constant) { - return Term{ - .var = ZeroSym, .scale = 0, .constant = constant, .known = true}; - } - [[nodiscard]] static Term ofSym(Sym var, std::int64_t scale = 1, - std::int64_t constant = 0) { - return Term{ - .var = var, .scale = scale, .constant = constant, .known = true}; - } - [[nodiscard]] static Term unknown() { - return Term{.var = ZeroSym, .scale = 0, .constant = 0, .known = false}; - } - [[nodiscard]] bool isConstant() const noexcept { - return known && (var == ZeroSym || scale == 0); - } - /// This term plus `other` when the sum is still one term. - [[nodiscard]] std::optional plus(const Term &other) const; - [[nodiscard]] Term plusConstant(std::int64_t delta) const; - - friend bool operator==(const Term &, const Term &) = default; - friend auto operator<=>(const Term &, const Term &) = default; -}; - -/// One object a pointer may point into, with its byte offset. -struct Target { - ObjectId object = 0; - Term offset = Term::of(0); - - friend bool operator==(const Target &, const Target &) = default; - friend auto operator<=>(const Target &, const Target &) = default; -}; - -enum class PointerNull : std::uint8_t { Null, NonNull, Maybe }; - -/// A byte offset (a *concrete* cell), the summary cell of an element -/// position (`offset` is then the offset within an element of `stride` -/// bytes), or a *selected* element cell: the cell at byte -/// `stride * index + offset` for the integer symbol `index` (§4.2 -/// *Amendment (arrays)*, RFC 0015 §1). Symbols are immutable, so a selected -/// key names one runtime cell for as long as the key exists. -struct CellKey { - std::int64_t offset = 0; - std::uint32_t stride = 0; - Sym index = ZeroSym; - - [[nodiscard]] bool isSummary() const noexcept { - return stride != 0 && index == ZeroSym; - } - [[nodiscard]] bool isSelected() const noexcept { return index != ZeroSym; } - [[nodiscard]] bool isConcrete() const noexcept { return stride == 0; } - /// A concrete or selected cell: one runtime cell. - /// The cell's byte offset as a term (unknown for a summary cell). - [[nodiscard]] Term byteTerm() const; - /// The summary key of the element position a selected cell is at. - [[nodiscard]] CellKey position() const; - /// The key of the cell at byte offset `offset`: concrete for a constant, - /// selected for an affine term, none otherwise. - [[nodiscard]] static std::optional at(const Term &offset); - - friend bool operator==(const CellKey &, const CellKey &) = default; - friend auto operator<=>(const CellKey &, const CellKey &) = default; -}; - -/// A zero test the path made of the value a cell held at entry (`if (!g)`, -/// `if (b->buf == NULL)`): what a summary case keys a lazy initialisation -/// by. -struct EntryTest { - ObjectId object = 0; - CellKey key = {}; - bool zero = true; - - friend bool operator==(const EntryTest &, const EntryTest &) = default; - friend auto operator<=>(const EntryTest &, const EntryTest &) = default; -}; - -/// RFC 0030 §3.1's `MoveRecord`, moved onto values and objects. -struct ReleaseRecord { - enum class Reason : std::uint8_t { - Freed, - Moved, - /// RFC 0010: the value's own reference was released. - ShareReleased, - /// The unknown-callee default (RFC 0030 §5.1). - UnknownCallee, - /// An open slot without targets (RFC 0030 §9.3). - Callback, - }; - // NOLINTBEGIN(readability-redundant-member-init): designated-init defaults - Reason reason = Reason::Freed; - SourceLocation where = {}; - /// The release family (RFC 0007), empty when unknown. - std::string family = {}; - /// The name the value was released through, for `freed here (through - /// 'm')`. - std::string via = {}; - bool allPaths = true; - bool conditional = false; - bool lossy = false; - /// Made by a weak cell's merge of values (§4.1): the value may be the - /// released one only because elements or aliases are not told apart. Such - /// a record never makes a diagnostic. - bool aliasOnly = false; - /// RFC 0030 §9.1: facts about the releasing function's unmodified integer - /// parameters on the path of the release: `(index, zero)`. - std::vector> paramGuard = {}; - /// RFC 0014: whether pairs of its unmodified pointer parameters compared - /// equal on the path of the release. - std::vector pairGuard = {}; - /// The zero tests of entry values on the path of the release (sorted). - std::vector entryGuard = {}; - /// RFC 0031 *Pending cases and exit splitting*: the locals, assigned only - /// where they are declared, that held a non-null pointer on the path of - /// the release (sorted handles): an exit returning one of them as null - /// did not release. - std::vector nonNullLocals = {}; - // NOLINTEND(readability-redundant-member-init) - - [[nodiscard]] bool unknownOrigin() const noexcept { - return reason == Reason::UnknownCallee || reason == Reason::Callback; - } - [[nodiscard]] bool definite() const noexcept { - return allPaths && !conditional && !unknownOrigin() && !aliasOnly; - } - friend bool operator==(const ReleaseRecord &, - const ReleaseRecord &) = default; -}; - -/// Joins two records of the same value or object (RFC 0030 §3.1 rules). -[[nodiscard]] ReleaseRecord joinRecords(const ReleaseRecord &left, - const ReleaseRecord &right); - -/// A result class of a call whose effects wait for a test (RFC 0030 §9.1). -struct PendingCase { - enum class Kind : std::uint8_t { - /// The subject was released (conditionally) at the call. - Release, - /// RFC 0030 §9.2: on these classes the subject is non-null. - NonNull, - /// RFC 0031 §6.3: on these classes the call did not store the new - /// objects the subject points to: they were never made. - Absent, - /// RFC 0017: on these classes the integer subject lies in `bound` (a - /// checked operation's result when it did not overflow). - Bound, - /// RFC 0031 §6.3: the call stored `stored` into the cells that now hold - /// `subject` (the join of the old and new values) on these classes, and - /// left `previous` there on the others. - Stored, - }; - Kind kind = Kind::Release; - /// The classes of the result under which the effect holds (`null`, - /// `nonnull`, `zero`, `positive`, `negative`). - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::vector classes = {}; - /// The symbol the effect applies to, and what happens to it. - Sym subject = ZeroSym; - ReleaseRecord record = {}; - /// `Release`: the effect's parameter test the call left open (the - /// argument, and whether it must be zero), tested again when the class - /// is selected. - std::optional> argumentZero = std::nullopt; - std::optional bound = std::nullopt; - /// `Stored`: the value stored, and the value the cells held before. - Sym stored = ZeroSym; - Sym previous = ZeroSym; - - friend bool operator==(const PendingCase &, const PendingCase &) = default; -}; - -/// A comparison a boolean symbol stands for, so a branch on it refines both -/// operands (§5.1). -struct Condition { - /// `And` and `Or`: `left` and `right` are truth values with conditions of - /// their own (a logical operator used as a value, `!(p && n)`). - enum class Op : std::uint8_t { Eq, Ne, Lt, Le, Gt, Ge, NonZero, And, Or }; - Op op = Op::NonZero; - Sym left = ZeroSym; - Sym right = ZeroSym; - /// `right` is the constant `constant` instead of a symbol. - bool rightIsConstant = false; - std::int64_t constant = 0; - /// The comparison is between the operands as unsigned values. - bool isUnsigned = false; - - friend bool operator==(const Condition &, const Condition &) = default; -}; - -//===----------------------------------------------------------------------===// -// Symbols -//===----------------------------------------------------------------------===// - -/// The C operation an integer symbol was computed by (§5.3): what a witness -/// spells when no C place holds the value, and, since symbols are immutable, -/// the key under which the same operation on the same operands is the same -/// value (§4.1). -struct SymDefinition { - IntegerOp op = IntegerOp::Add; - Sym left = ZeroSym; - /// The right operand, or none when it is the constant `constant`. - Sym right = ZeroSym; - std::optional constant = std::nullopt; - /// The C result equals the mathematical one for every value (no - /// wrap-around, RFC 0017), so a 64-bit term may spell it (RFC 0030 §7.4 - /// *Arithmetic*). - bool exact = false; - /// The left operand's value when it is a constant (the dividend of - /// `INT_MAX / m`), which a later state may no longer hold (RFC 0017 §3's - /// range guards). - std::optional leftValue = std::nullopt; - - [[nodiscard]] bool sameOperation(const SymDefinition &other) const noexcept { - return op == other.op && left == other.left && right == other.right && - constant == other.constant; - } - friend bool operator==(const SymDefinition &, - const SymDefinition &) = default; -}; - -/// Where a pointer value became null or possibly null (RFC 0008's notes). -struct NullOrigin { - enum class Reason : std::uint8_t { - /// `p = NULL`: "'p' is assigned NULL here". - Assigned, - /// The null edge of a test: "'p' may be null: it is compared with NULL - /// here". - Tested, - /// An allocation's result (`allocatorSource`): "allocated here"; - /// `detail` is the allocating function's name. - Allocated, - }; - Reason reason = Reason::Assigned; - SourceLocation where = {}; - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::string detail = {}; - - friend bool operator==(const NullOrigin &, const NullOrigin &) = default; -}; - -/// The most entry places a value records it was computed from. -inline constexpr std::size_t MaxEntryOrigins = 8; -/// The most constants a value records it is known not to equal (RFC 0034 -/// §6.2). -inline constexpr std::size_t MaxExcluded = 8; - -// (The fields stay in the groups they are documented in.) -// NOLINTNEXTLINE(clang-analyzer-optin.performance.Padding): documented order -struct SymInfo { - enum class Type : std::uint8_t { Unknown, Int, Pointer, Function }; - // NOLINTBEGIN(readability-redundant-member-init): designated-init defaults - Type type = Type::Unknown; - - // Integers. - /// The C type of an integer value (for ranges and wrap-around). - std::optional intType = std::nullopt; - /// For a boolean result of a comparison or test. - std::optional condition = std::nullopt; - /// A pointer converted to an integer: the pointer symbol behind it. - Sym pointerBehind = ZeroSym; - /// Known to be non-zero (a disequality the zone cannot hold). - bool nonZero = false; - /// RFC 0034 §6.2: the constants it is known not to equal besides what - /// `nonZero` says (sorted, at most `MaxExcluded`): the false edge of - /// `x == c` and the true edge of `x != c` add `c`, and an edge that - /// requires `x == c` for one of them is infeasible. - std::vector excluded = {}; - /// §4.4: the value's interval in its type (RFC 0017), where the zone's - /// 64-bit bounds cannot hold it (an unsigned 64-bit value above - /// `INT64_MAX`); none means the type's range, refined by the zone. - std::optional values = std::nullopt; - /// RFC 0017's range guard (`n <= INT_MAX / m`): this non-negative value - /// times the symbol is at most the bound, so their product cannot wrap. - std::optional> productAtMost = std::nullopt; - - // Pointers. - std::vector targets = {}; - /// "Any object": no points-to information. - bool top = false; - PointerNull null = PointerNull::Maybe; - bool allocatorSource = false; - /// RFC 0008, RFC 0031 §5.11: why the value may be null, for the notes of - /// null findings (never for a decision). - std::optional nullOrigin = std::nullopt; - std::optional release = std::nullopt; - /// RFC 0004, RFC 0033 §2: a raw pointer (declared `WEAVEC_RAW`, or loaded - /// through or handed out as one) and where it became one. Must: a merge - /// of a raw value and another is `rawSome`. - bool raw = false; - /// Raw only through some of the functions a call may reach (a hook whose - /// functions return raw and tracked pointers, RFC 0031 *Implementation - /// amendments*): no definite `unsafe-operation`, and nothing proven. - /// Joins keep it; only such a call makes it. - bool rawSome = false; - SourceLocation rawAt = {}; - /// How the raw origin arose, for the note at `rawAt` (RFC 0004). - enum class RawOrigin : std::uint8_t { Declared, Loaded, Returned }; - RawOrigin rawOrigin = RawOrigin::Declared; - /// `Loaded`: the raw pointer it was loaded through; `Returned`: the - /// callee that handed it out. - std::string rawFrom = {}; - /// The first place the raw value was read from, for `(through 'p')`. - std::string rawVia = {}; - /// RFC 0030 §2.3 `raw-cast`: the pointer was made by reinterpretation. - bool rawCast = false; - /// RFC 0010: references this value holds on a counted object. - std::optional shares = std::nullopt; - /// §4.5 D6: the symbols this value was derived from through owning slots, - /// nearest first (at most 8). - std::vector ancestors = {}; - /// Never assigned (RFC 0008): a local's value before any store. - bool uninit = false; - /// Never assigned on some path (RFC 0030 §11): garbage there unless - /// zero-initialisation ran. - bool mayUninit = false; - /// Null on some paths because a join took in a null pointer (not an - /// unassigned one) beside other values: the value a summary describes as - /// an entry path is then that path's value or null, where a value only - /// maybe-null at entry is the path's value alone. - bool nullJoined = false; - /// RFC 0011: made from another pointer by `&` on a place below it or by - /// arithmetic, a borrow of the object rather than a copy of its owner - /// (§5.5 *Conflicting borrows*). - bool derived = false; - /// Pending outcome cases keyed on this value's class (RFC 0030 §9.1). - std::vector pending = {}; - - // Functions. - std::vector functions = {}; - bool functionsKnown = false; - /// Functions of other units, by portable name (a callback a caller in - /// another unit passes, RFC 0031 §7 *cross-unit contexts*). - std::vector foreignFunctions = {}; - - /// The spelling the value was created under, for messages. - std::string name = {}; - /// The value's C type, as a frontend handle (zero when unknown). - Handle ctype = 0; - /// An integer equal to this term over another symbol (§4.4): `n * 4`. - std::optional linear = std::nullopt; - /// An unsigned integer equal to this term reduced modulo its type (a - /// product that may wrap, `n * sizeof *p`): never more than it. - std::optional unwrapped = std::nullopt; - /// The value a cell held at entry, materialised for it (§4.6): the - /// object and cell, so a summary tells a store from an unchanged cell. - std::optional> entryOf = std::nullopt; - /// The cells whose entry values this value was computed from (itself, - /// by pointer arithmetic, a cast, a merge), sorted: a proof about it - /// rests on those places' entry assumptions (RFC 0030 §9.4 as RFC 0031 - /// amends it). At most `MaxEntryOrigins`; a value from more rests on none - /// it could name, and says so by `entryOriginsLost`. - std::vector> entryOrigins = {}; - bool entryOriginsLost = false; - /// The operation that computed this integer, when one did (§5.3). - std::optional defined = std::nullopt; - // NOLINTEND(readability-redundant-member-init) - - friend bool operator==(const SymInfo &, const SymInfo &) = default; -}; - -//===----------------------------------------------------------------------===// -// Object states -//===----------------------------------------------------------------------===// - -/// RFC 0015 §5, §4.2 *Amendment (arrays)*: the elements whose indices lie in -/// `[from, to)` at the element position `position` (a summary key) each hold -/// a value described by `value`. Each element's value is its own runtime -/// value: a read copies `value`'s attributes into a fresh symbol, so a -/// release record on `value` that is definite says that every element in -/// the range was released. -struct Segment { - CellKey position = {}; - Term from = Term::of(0); - Term to = Term::of(0); - Sym value = ZeroSym; - /// RFC 0015 §4, §4.9 *Copies*: the range was copied from `source`, an - /// entry object no store had reached, `shift` bytes further on, so each - /// element holds the entry value of its own source element (the value a - /// load of that element reads). Holds while `value` is still `copied`, - /// the value the copy wrote: any later change of the range makes it a - /// plain range described by `value`. - ObjectId source = 0; - std::int64_t shift = 0; - Sym copied = ZeroSym; - - [[nodiscard]] bool isCopy() const noexcept { - return source != 0 && value == copied; - } - friend bool operator==(const Segment &, const Segment &) = default; -}; - -enum class Life : std::uint8_t { - Live, - /// Released on every path, with the record. - Released, - /// Released on some path, or weakly (a release through a pointer that may - /// point elsewhere). - MayReleased, - /// Reached by an unknown callee (RFC 0030 §5.1). - UnknownReleased, - /// Storage whose lifetime ended (a local out of scope). - Ended, - MayEnded, -}; - -/// An object's extent in bytes and how much it may be trusted (RFC 0030 -/// §7.1). -struct Extent { - Term bytes = Term::unknown(); - ExtentClass cls = ExtentClass::LowerBound; - /// `bytes` is this term reduced modulo the size type (an allocation of a - /// product that may wrap): never more than it, so an access past it is - /// past the object. - std::optional unwrapped = std::nullopt; - - friend bool operator==(const Extent &, const Extent &) = default; -}; - -struct ObjectState { - // NOLINTBEGIN(readability-redundant-member-init): designated-init defaults - PMap cells; - /// Element ranges, newest first: an element is described by its own cell - /// if it has one, else by the first segment that must contain it (§4.2 - /// *Amendment (arrays)*). - std::vector segments = {}; - /// The element size a variable index was used with on this object (RFC - /// 0015 array storage), zero when none: which cells are elements. - std::uint32_t stride = 0; - /// A store reached this object's cells in this activation (so a cell - /// holding an entry element's value may hold another element's, §4.9). - bool stored = false; - Life life = Life::Live; - std::optional record = std::nullopt; - /// The byte offset into the object of the pointer that released it, when - /// a constant (a release of `p + 1`, RFC 0008's invalid release). - std::optional releaseOffset = std::nullopt; - std::optional extent = std::nullopt; - /// RFC 0007: the allocation family, and whether this activation owns it. - std::string family = {}; - bool owned = false; - /// On this path the object was never made (its allocation failed, or the - /// store that would have made it did not happen), so it owns nothing - /// here; a join takes ownership from the paths on which it exists. - bool absent = false; - /// Cells that differ from their entry value on exactly the paths where - /// an entry test holds (`if (!g) g = malloc(…)` leaves `g` so), by key. - std::vector> storedIff = {}; - /// A join kept the object from the paths that made it only: it exists - /// where the symbol's zero test is `second` (`if (c) p = malloc(n);` - /// makes it where `c != 0`), which a later test of the symbol decides. - std::optional> existsIf = std::nullopt; - /// The same over an entry test: made on exactly the paths where the test - /// holds (`if (!g) g = malloc(…)` makes it where `g` was null at entry). - std::optional existsIfEntry = std::nullopt; - /// Its address was stored where a callee or another unit can reach it. - bool escaped = false; - bool readonly = false; - /// `Focus` objects: the objects it may be (never focus objects). - std::vector candidates = {}; - /// The symbol whose release released this object, for §4.5 D6. - Sym releasedBy = ZeroSym; - /// Every cell never written reads as zero (a zeroing allocation). - bool zeroed = false; - /// Cells never written read as uninitialised (a fresh local or a - /// non-zeroing allocation with zero-initialisation off). - bool uninitialised = false; - /// Cells were forgotten by an unknown call: an unwritten cell reads as a - /// fresh unknown value. - bool havocked = false; - /// The byte ranges `[first, second)` whose cells were forgotten (a member - /// rewritten by code the analysis does not see, a copy over part of the - /// object), sorted and disjoint: an unwritten cell there reads as a fresh - /// unknown value, as everywhere in a `havocked` object. - std::vector> forgotten = {}; - /// The byte ranges forgotten on some paths only (a callee that may have - /// rewritten them, a join with a path that forgot them): an unwritten - /// cell there reads as its value otherwise, merged with an unknown one. - std::vector> mayForgotten = {}; - /// RFC 0012 *String facts*: a NUL lies at this offset, so the string at - /// any offset up to it ends there at the latest ... - std::optional nulWithin = std::nullopt; - /// ... and no NUL lies from `nulFrom` up to it: the string at any offset - /// in between has exactly the length to it. - std::optional nulFrom = std::nullopt; - /// For the summary: some runtime object this object stood for was - /// released during the activation (kept when members are focused out and - /// collected, §4.6). - bool effectReleased = false; - bool effectMayReleased = false; - /// For messages: the last place the program stored a pointer to this - /// object in (`p`, `b->data`). - std::string holder = {}; - /// The statement that last used a pointer to this object (a frontend - /// handle), where a leak is reported. - Handle lastUse = 0; - // NOLINTEND(readability-redundant-member-init) - - /// Whether an unwritten cell at `key` reads as a fresh unknown value. - [[nodiscard]] bool forgets(const CellKey &key) const; - /// Whether an unwritten cell at `key` may read as an unknown value (and - /// otherwise as it would). - [[nodiscard]] bool mayForget(const CellKey &key) const; - /// Whether any of the object's bytes were, or may have been, forgotten. - [[nodiscard]] bool forgetsAny() const { - return havocked || !forgotten.empty() || !mayForgotten.empty(); - } - - friend bool operator==(const ObjectState &, const ObjectState &) = default; -}; - -//===----------------------------------------------------------------------===// -// The state -//===----------------------------------------------------------------------===// - -/// One program point's abstract state. -/// RFC 0014: two pointer values known to compare equal (or not); `first < -/// second`. -struct PointerFact { - Sym first = ZeroSym; - Sym second = ZeroSym; - bool equal = true; - - friend bool operator==(const PointerFact &, const PointerFact &) = default; - friend auto operator<=>(const PointerFact &, const PointerFact &) = default; -}; - -/// A load through a pointer to two objects of which each path has exactly -/// one (complementary existence): the value it read, while both cells -/// still hold what they held, so that a test of it holds for the next load. -struct MergedLoad { - ObjectId first = 0; - CellKey firstKey = {}; - Sym firstValue = ZeroSym; - ObjectId second = 0; - CellKey secondKey = {}; - Sym secondValue = ZeroSym; - Sym merged = ZeroSym; - - friend bool operator==(const MergedLoad &, const MergedLoad &) = default; -}; - -struct HeapState { - PMap objects; - PMap syms; - Zone zone; - /// Values of expressions evaluated in one block and used in another - /// (`?:`, `&&`, `||` operands and branch conditions). - PMap exprs; - /// The returned value, at exits. - Sym result = ZeroSym; - /// Pointer comparisons the path decided (sorted). - std::vector pointerFacts; - /// Zero tests of entry values the path decided (sorted, one per cell). - std::vector entryTests; - /// Values of loads through complementary objects (not kept by joins). - std::vector mergedLoads; - /// Pointers made by arithmetic from a maybe-null pointer, each with the - /// pointer the chain started from (not kept by joins): arithmetic on null - /// is undefined, so each is null exactly when that one is (`markNonNull`). - std::vector> nullFollows; - Sym nextSym = 1; - bool unreachable = false; - - friend bool operator==(const HeapState &, const HeapState &) = default; -}; - -/// The verdict on the temporal facet of an access through a value. -struct TemporalVerdict { - enum class Kind : std::uint8_t { - Proven, - /// Definitely released (or moved, or out of scope). - Violation, - /// Released on some path: a warning (`may-released`, `may-moved`). - MayReleased, - /// An object it may point to was released (`may-alias-released`). - MayAliasReleased, - /// An unknown callee or open slot may have released it. - UnknownCallee, - Callback, - /// Points to storage whose lifetime may have ended. - MayDangle, - }; - Kind kind = Kind::Proven; - /// The record the verdict rests on, for diagnostics and notes. - std::optional record = std::nullopt; - /// The object, for `ended` storage messages. - ObjectId object = 0; -}; - -/// The verdict on a spatial access `[offset, offset + width)`. -struct SpatialVerdict { - enum class Kind : std::uint8_t { - Proven, - Violation, - /// Undecided against an extent that may be compared against. - Checkable, - UnknownExtent, - UnknownIndex, - }; - Kind kind = Kind::Proven; - /// The extent the verdict was made against. - std::optional extent = std::nullopt; - /// For a violation: how far the access reaches, in bytes, when constant. - std::optional reach = std::nullopt; - /// For a violation: the end of the access as a term (for messages). - std::optional end = std::nullopt; - /// Accesses before the start. - bool beforeStart = false; -}; - -/// The domain operations of RFC 0031 §4 over one function's objects. -class Heap { -public: - Heap(ObjectTable &objects, const HeapOracle &oracle) - : table(objects), oracle(oracle) {} - - [[nodiscard]] ObjectTable &objects() noexcept { return table; } - [[nodiscard]] const ObjectTable &objects() const noexcept { return table; } - - // Symbols. - Sym fresh(HeapState &state, SymInfo info) const; - [[nodiscard]] const SymInfo &info(const HeapState &state, Sym sym) const; - SymInfo &infoMut(HeapState &state, Sym sym) const; - /// Refines a maybe-null pointer to non-null, with the pointers made from - /// it or from what it was made from by arithmetic (`nullFollows`). - void markNonNull(HeapState &state, Sym pointer) const; - /// An integer symbol with the constant `value`. - Sym constant(HeapState &state, std::int64_t value, - std::optional type = std::nullopt) const; - /// A pointer symbol to `targets`. - Sym pointer(HeapState &state, std::vector targets, PointerNull null, - std::string name = {}) const; - - // Objects. - ObjectState &object(HeapState &state, ObjectId id) const; - // NOLINTNEXTLINE(readability-convert-member-functions-to-static): Heap API - [[nodiscard]] const ObjectState *findObject(const HeapState &state, - ObjectId id) const { - return state.objects.find(id); - } - /// Adds `id` to the state if absent (fresh: live, no cells). - ObjectState &ensure(HeapState &state, ObjectId id) const; - - // Memory. - /// The symbol stored in `cell`, or none when never written (the caller - /// materialises the entry value, §4.6). - [[nodiscard]] std::optional read(const HeapState &state, ObjectId object, - CellKey key) const; - /// Stores `value` into `cell`: a strong update, or with `weak` a merge - /// with the old value into an alias-join symbol (§4.1). - void write(HeapState &state, ObjectId object, CellKey key, Sym value, - bool weak) const; - /// Forgets every cell of `object` in `[from, from + size)` (bytes), or all - /// cells when `size` is none: they read as unknown values afterwards, and - /// the object's string facts are dropped. - void forgetCells(HeapState &state, ObjectId object, std::int64_t from, - std::optional size) const; - /// `forgetCells` on some paths only: the cells in the bytes keep their - /// values, each merged by `unknownLike` with an unknown value of its kind, - /// and an unwritten cell there reads as unknown. - void weakenCells(HeapState &state, ObjectId object, std::int64_t from, - std::optional size, - const std::function &unknownLike) const; - /// A weak merge of two values into one alias-join symbol. - Sym mergeWeak(HeapState &state, Sym left, Sym right) const; - - // Elements (§4.2 *Amendment (arrays)*, RFC 0015). - /// The limits on selected cells and segments per object. - static constexpr std::size_t MaxSelectedCells = 32; - static constexpr std::size_t MaxSegmentsPerPosition = 4; - /// The value a load of `key` reads: the stored value, or the value the - /// element facts give an element cell (stored into the cell, so a second - /// load of the key reads the same symbol). A summary key reads "some - /// element" and stores nothing. - Sym load(HeapState &state, ObjectId object, CellKey key, - const SymInfo &hint) const; - /// The value of the element cell `key` (concrete or selected) that is not - /// in memory, without storing it: a cell that must be the same, else the - /// newest segment that must contain it, else the unwritten value and the - /// summary cell; cells and segments that may be it contribute their values - /// as possible ones. - Sym readElement(HeapState &state, ObjectId object, CellKey key, - const SymInfo &hint) const; - /// A fresh symbol with `value`'s attributes: another element's value. - Sym copyValue(HeapState &state, Sym value) const; - /// `left`, or a value an element that may be this one holds: like - /// `mergeWeak`, but a release record of `right` keeps its evidence (a - /// possible release, not `aliasOnly`). - Sym mergePossible(HeapState &state, Sym left, Sym right) const; - /// The join of two values that each describe every element of a range: a - /// release record definite on both stays definite. - Sym joinUniform(HeapState &state, Sym left, Sym right) const; - /// Removes the selected cell `key`; what it held becomes a weak write to - /// every element it may have been. - void evictCell(HeapState &state, ObjectId object, CellKey key) const; - /// Removes the segments `evict` marks (by index), newest first, in one - /// pass: each one's value becomes a weak write to the elements it - /// described. - void evictSegments(HeapState &state, ObjectId object, - const std::vector &evict) const; - /// Keeps each position's newest `MaxSegmentsPerPosition` segments of the - /// object, evicting the rest. - void trimSegments(HeapState &state, ObjectId object) const; - /// Moves the element cell `key` into the segments: it extends a segment - /// it is adjacent to, or becomes a one-element segment. False when the - /// cell is not an element of the object's stride. - bool foldCell(HeapState &state, ObjectId object, CellKey key) const; - /// RFC 0015 §5: a callee's range effect on the elements `[from, to)` at - /// `position`. Each element's value is released with `record`. - void releaseElements(HeapState &state, ObjectId object, CellKey position, - const Term &from, const Term &to, - const ReleaseRecord &record, const SymInfo &hint) const; - /// Each element in `[from, to)` at `position` is written a value - /// described by `value` (weakly with `weak`). - void writeElements(HeapState &state, ObjectId object, CellKey position, - const Term &from, const Term &to, Sym value, - bool weak) const; - /// RFC 0015 §4: the elements `[from, to)` at `position` are copied from - /// the entry object `source`, `shift` bytes further on, which no store - /// has reached: each holds its source element's entry value (a - /// `Segment::isCopy` range). `value` describes every element. - void copyElements(HeapState &state, ObjectId object, CellKey position, - const Term &from, const Term &to, Sym value, - ObjectId source, std::int64_t shift) const; - /// Whether two cells' byte offsets are equal (true), different (false), - /// or neither. - [[nodiscard]] std::optional - sameCell(const HeapState &state, const Term &first, const Term &second) const; - - // Distinctness (§4.5). - /// Whether the two objects may stand for the same runtime object; `state` - /// supplies focus objects' candidates. - [[nodiscard]] bool mayOverlap(const HeapState &state, ObjectId first, - ObjectId second) const; - /// Whether `object` is reached from `ancestor` through owning steps. - [[nodiscard]] bool ownedBelow(ObjectId object, ObjectId ancestor) const; - - // Queries. - [[nodiscard]] TemporalVerdict temporal(const HeapState &state, - Sym pointer) const; - [[nodiscard]] SpatialVerdict spatial(const HeapState &state, Sym pointer, - const Term &extraOffset, - std::int64_t width) const; - /// Whether `left <= right` holds for every value (true), for none - /// (false), or neither (none). - [[nodiscard]] std::optional - lessEqual(const HeapState &state, const Term &left, const Term &right) const; - /// RFC 0014: whether two pointer values compare equal, when the path - /// decided it. - [[nodiscard]] static std::optional pointersEqual(const HeapState &state, - Sym first, Sym second); - /// Records that two pointer values compare equal (or not); false when the - /// path already decided the opposite. - static bool assumePointersEqual(HeapState &state, Sym first, Sym second, - bool equal); - - // Releases. - /// Releases `pointer`'s value and targets (§5.5 *Effect*). `possibly`: - /// the call may release them (a range that may not cover the object, RFC - /// 0034 §6.3): no target becomes definitely released. - void release(HeapState &state, Sym pointer, const ReleaseRecord &record, - bool possibly = false) const; - - // Lattice. - /// The join of two states at the entry of block `block` (§4.8). At a - /// loop head (`loopHead`, `right` the back edge's state), element cells - /// the iteration changed are first folded into segments (§4.2 - /// *Amendment (arrays)*). A join inside one expression (the functions a - /// call may reach, each applied to a copy of one state) passes that - /// state's `nextSym` as `keepBelow`: a symbol both sides hold under it is - /// that state's value on both, and keeps its number, and every other - /// result is numbered from it on, so the values of the expression's other - /// operands, which the caller still holds, name no other value. - [[nodiscard]] HeapState join(const HeapState &left, const HeapState &right, - Handle block, bool loopHead = false, - Sym keepBelow = ZeroSym) const; - /// The widening of `previous` by `next` (both at one loop head). - [[nodiscard]] HeapState - widen(const HeapState &previous, const HeapState &next, Handle block, - const std::vector &thresholds) const; - /// Whether `a` and `b` are the same state up to how their symbols are - /// numbered (a join numbers its results in pairing order, so a loop head - /// that has settled can come back renumbered). Conservative: false when - /// a symbol it does not follow differs. - [[nodiscard]] bool equivalent(const HeapState &a, const HeapState &b) const; - /// Drops objects no root reaches (§4.6): released entry and materialised - /// objects become dead copies; unreachable heap objects are dropped and - /// reported to `leaked` when owned, unreleased and not escaped. - void collect(HeapState &state, const std::vector &roots, - const std::function &leaked) const; - - /// The symbols reachable from `roots` through memory and attributes. - [[nodiscard]] std::vector - reachableObjects(const HeapState &state, - const std::vector &roots) const; - - /// `--dump-analysis` text. - [[nodiscard]] std::string dump(const HeapState &state) const; - -private: - ObjectTable &table; - const HeapOracle &oracle; - - [[nodiscard]] SpatialVerdict spatialAt(const HeapState &state, Sym pointer, - const Term &extraOffset, - const Term &need) const; - /// A store through a summary key: every element at its position may now - /// hold `value`. - void writeSummary(HeapState &state, ObjectId object, CellKey key, - Sym value) const; - /// `sym` as a value read through a pointer `hint`: an integer or untyped - /// value becomes a raw pointer to any object (§4.2). - Sym asPointer(HeapState &state, Sym sym, const SymInfo &hint) const; - /// The value of "some element" at a summary key's position. - Sym anyElement(HeapState &state, ObjectId object, CellKey key, - const SymInfo &hint) const; - /// The value of an element in a range no cell describes: the unwritten - /// value and the summary cell, joined with every segment that may overlap - /// the range unless one must cover it. - Sym rangeValue(HeapState &state, ObjectId object, CellKey position, - const Term &from, const Term &to, const SymInfo &hint) const; - /// The value of the element at byte `offset` of a copied range - /// (`Segment::isCopy`): its source element's entry value. - Sym copiedElement(HeapState &state, const Segment &segment, - const Term &offset, const SymInfo &hint) const; - /// Keeps an object within `MaxSelectedCells` and - /// `MaxSegmentsPerPosition`, evicting the oldest. - void limitElements(HeapState &state, ObjectId object, CellKey keep) const; -}; - -} // namespace weavec::core - -#endif // WEAVEC_CORE_HEAP_H diff --git a/include/weavec/Core/Integer.h b/include/weavec/Core/Integer.h deleted file mode 100644 index 6a333778..00000000 --- a/include/weavec/Core/Integer.h +++ /dev/null @@ -1,190 +0,0 @@ -//===- Integer.h - Target integer values and bounded ranges ---*- C++ -*-===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// RFC 0017. Bit patterns belong to a source target type, never to the host's -// interpretation of an int64_t. Ranges are unions of at most two intervals -// in numeric order (signed order for signed types). -// -//===----------------------------------------------------------------------===// - -#ifndef WEAVEC_CORE_INTEGER_H -#define WEAVEC_CORE_INTEGER_H - -#include -#include -#include -#include -#include -#include -#include - -namespace weavec::core { - -struct IntegerType { - unsigned width = 32; - bool isSigned = true; - bool isBoolean = false; - - [[nodiscard]] bool valid() const noexcept; - [[nodiscard]] std::uint64_t mask() const noexcept; - [[nodiscard]] std::uint64_t signBit() const noexcept; - /// Bit pattern <-> numeric-order position (both are their own inverse). - [[nodiscard]] std::uint64_t rank(std::uint64_t bits) const noexcept; - [[nodiscard]] std::string toString() const; - - friend auto operator<=>(const IntegerType &, const IntegerType &) = default; -}; - -inline constexpr IntegerType BooleanType{ - .width = 1, .isSigned = false, .isBoolean = true}; - -struct IntegerValue { - IntegerType type; - std::uint64_t bits = 0; - - [[nodiscard]] static IntegerValue ofBits(IntegerType type, - std::uint64_t bits) noexcept; - [[nodiscard]] bool negative() const noexcept; - [[nodiscard]] std::uint64_t magnitude() const noexcept; - [[nodiscard]] std::optional signedValue() const noexcept; - [[nodiscard]] IntegerValue converted(IntegerType destination) const noexcept; - [[nodiscard]] std::string toString() const; - - friend auto operator<=>(const IntegerValue &, const IntegerValue &) = default; -}; - -enum class IntegerOp : std::uint8_t { - Add, - Subtract, - Multiply, - Divide, - Remainder, - ShiftLeft, - ShiftRight, - BitAnd, - BitOr, - BitXor, - Negate, - Complement, - LogicalNot, - Equal, - NotEqual, - Less, - LessEqual, - Greater, - GreaterEqual, - Minimum, - Maximum, -}; - -[[nodiscard]] bool isUnary(IntegerOp op) noexcept; -[[nodiscard]] bool isComparison(IntegerOp op) noexcept; -[[nodiscard]] IntegerOp negateComparison(IntegerOp op) noexcept; - -enum class IntegerError : std::uint8_t { - None, - SignedOverflow, - DivisionByZero, - SignedDivisionOverflow, - ShiftCount, - SignedLeftShift, - IncompatibleTypes, -}; - -[[nodiscard]] std::string_view toString(IntegerError error) noexcept; - -struct IntegerEvaluation { - std::optional value; - IntegerError error = IntegerError::None; -}; - -/// Operands have already undergone the source language's promotions. -/// Shift counts may have a different type. Comparisons yield BooleanType. -[[nodiscard]] IntegerEvaluation evaluateInteger(IntegerOp op, IntegerValue lhs, - IntegerValue rhs, - bool wrapSigned = false); - -struct IntegerInterval { - std::uint64_t lower = 0; - std::uint64_t upper = 0; - friend auto operator<=>(const IntegerInterval &, - const IntegerInterval &) = default; -}; - -inline constexpr std::size_t MaxIntegerIntervals = 2; - -class IntegerRange { -public: - IntegerType type; - - /// Empty is bottom (no represented value), not unknown. - IntegerRange() = default; - explicit IntegerRange(IntegerType type) : type(type) {} - [[nodiscard]] static IntegerRange full(IntegerType type); - [[nodiscard]] static IntegerRange singleton(IntegerValue value); - [[nodiscard]] static IntegerRange between(IntegerValue lower, - IntegerValue upper); - [[nodiscard]] static IntegerRange - fromRanks(IntegerType type, std::vector ranges); - - [[nodiscard]] bool empty() const noexcept { return intervals.empty(); } - [[nodiscard]] bool isFull() const noexcept; - [[nodiscard]] bool contains(IntegerValue value) const noexcept; - [[nodiscard]] std::optional constant() const noexcept; - [[nodiscard]] std::optional minimum() const noexcept; - [[nodiscard]] std::optional maximum() const noexcept; - [[nodiscard]] const std::vector &all() const noexcept { - return intervals; - } - [[nodiscard]] IntegerRange converted(IntegerType destination) const; - [[nodiscard]] IntegerRange intersect(const IntegerRange &other) const; - [[nodiscard]] IntegerRange united(const IntegerRange &other) const; - /// Values of this operand for which `this op rhs` can hold. - [[nodiscard]] IntegerRange satisfying(IntegerOp op, - const IntegerRange &rhs) const; - [[nodiscard]] std::string toString() const; - - friend auto operator<=>(const IntegerRange &, const IntegerRange &) = default; - -private: - std::vector intervals; -}; - -struct IntegerRangeEvaluation { - IntegerRange values; - bool mayBeInvalid = false; - bool alwaysInvalid = false; - IntegerError error = IntegerError::None; -}; - -/// GCC/Clang checked arithmetic first computes an infinite-precision integer, -/// then stores its conversion and reports whether the destination can hold it. -struct CheckedIntegerValue { - IntegerValue value; - bool overflow = false; -}; -struct CheckedIntegerRange { - IntegerRange values; - IntegerRange overflow; -}; -[[nodiscard]] std::optional -evaluateCheckedInteger(IntegerOp op, IntegerValue lhs, IntegerValue rhs, - IntegerType destination); -[[nodiscard]] CheckedIntegerRange -evaluateCheckedInteger(IntegerOp op, const IntegerRange &lhs, - const IntegerRange &rhs, IntegerType destination); - -/// Sound range transfer. Unsupported precision widens, never invents a fact. -[[nodiscard]] IntegerRangeEvaluation evaluateInteger(IntegerOp op, - const IntegerRange &lhs, - const IntegerRange &rhs, - bool wrapSigned = false); - -} // namespace weavec::core - -#endif // WEAVEC_CORE_INTEGER_H diff --git a/include/weavec/Core/Ledger.h b/include/weavec/Core/Ledger.h deleted file mode 100644 index 0b5d47bc..00000000 --- a/include/weavec/Core/Ledger.h +++ /dev/null @@ -1,429 +0,0 @@ -//===- Ledger.h - Per-site analysis outcomes (RFC 0030, 0035) --*- C++ -*-===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// RFC 0030 records one outcome for every safety facet (spatial, null, -// temporal) of every memory operation the analysis sees, and for the -// assertion facet of every `WEAVEC_ASSUME` (§2). RFC 0035 §8 makes the -// analysis advisory: the outcomes are `proven`, `violation`, `unresolved` -// and `trusted`, they stay in memory, and they feed the summary line. This -// header is the frontend-neutral model of that record: -// -// Ledger -> units -> functions -> sites -> facets -// -> diagnostics (a facet links to its diagnostic by index) -// -// with the closed reason lists (§2.3, §2.4), merging by rank (§2.5), the -// undecided defaults (§2.6), and the rollup behind the summary line. -// -//===----------------------------------------------------------------------===// - -#ifndef WEAVEC_CORE_LEDGER_H -#define WEAVEC_CORE_LEDGER_H - -#include "weavec/Core/Diagnostic.h" -#include "weavec/Core/SourceLocation.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace weavec::core { - -//===----------------------------------------------------------------------===// -// Closed vocabularies -//===----------------------------------------------------------------------===// - -/// §2.1: the operation a site stands for. -enum class SiteKind : std::uint8_t { - Deref, - Index, - PtrArith, - Cast, - IntToPtr, - LibCall, - Release, - Call, - Assume, - Raw, -}; - -/// §2.1: the obligations of a site. `Assertion` belongs to Assume sites only. -enum class Facet : std::uint8_t { Spatial, Null, Temporal, Assertion }; -inline constexpr std::size_t FacetCount = 4; -inline constexpr std::array AllFacets{ - Facet::Spatial, Facet::Null, Facet::Temporal, Facet::Assertion}; - -/// §2.2 as RFC 0035 §8 leaves it. Named `SiteOutcome` because -/// `core::Outcome` is the RFC 0006 result class. -enum class SiteOutcome : std::uint8_t { - Proven, - Violation, - Unresolved, - Trusted, -}; - -/// §2.3: why a facet is not proven. Closed: adding a reason requires an RFC. -enum class UnresolvedReason : std::uint8_t { - UnknownExtent, - UnknownIndex, - Inexpressible, - MayReleased, - MayMoved, - MayAliasReleased, - MayInvalidRelease, - MayMismatchedRelease, - MayDangle, - MayConflict, - UnknownCallee, - Callback, - Setjmp, - Budget, - Unanalysed, - RawCast, - DanglingEscape, - SecondOwner, - NoZeroInit, - /// RFC 0034 §6.1: a definite finding the replay did not confirm. - Unconfirmed, - /// RFC 0035 §8: the facts neither prove nor refute the facet (what RFC - /// 0030 called `checked`: a runtime check of a bound the code states - /// would decide it). - Undecided, -}; - -/// §2.4: the named trust assumption a trusted facet rests on. Closed. -enum class TrustReason : std::uint8_t { - Unsafe, - SystemApi, - LibrarySpec, - ExternContract, - CallerContract, - ExternalUnit, - Concurrency, -}; - -// §3 `Certainty` (definite or possible) is defined in `Diagnostic.h`, since -// `core::Diagnostic` carries it too. - -/// §2.1: a Call site is a call boundary or a function exit. -enum class Boundary : std::uint8_t { Call, Exit }; - -// Spellings, for dumps and tests. -[[nodiscard]] std::string_view toString(SiteKind kind) noexcept; -[[nodiscard]] std::string_view toString(Facet facet) noexcept; -[[nodiscard]] std::string_view toString(SiteOutcome outcome) noexcept; -[[nodiscard]] std::string_view toString(UnresolvedReason reason) noexcept; -[[nodiscard]] std::string_view toString(TrustReason reason) noexcept; - -//===----------------------------------------------------------------------===// -// Merging by rank (§2.5) -//===----------------------------------------------------------------------===// - -/// violation > unresolved > trusted > proven. -[[nodiscard]] constexpr int outcomeRank(SiteOutcome outcome) noexcept { - switch (outcome) { - case SiteOutcome::Proven: - return 0; - case SiteOutcome::Trusted: - return 1; - case SiteOutcome::Unresolved: - return 2; - case SiteOutcome::Violation: - return 3; - } - return 0; -} - -/// The higher-ranked of two outcomes. -[[nodiscard]] constexpr SiteOutcome maxByRank(SiteOutcome a, - SiteOutcome b) noexcept { - return outcomeRank(b) > outcomeRank(a) ? b : a; -} - -/// One record about one facet: an outcome and, for `unresolved` and -/// `trusted`, its reason, with a free-text detail. -struct FacetDecision { - SiteOutcome outcome = SiteOutcome::Proven; - /// Set exactly when `outcome` is `Unresolved`. - std::optional unresolved = std::nullopt; - /// Set exactly when `outcome` is `Trusted`. - std::optional trusted = std::nullopt; - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::string detail = {}; - - [[nodiscard]] static FacetDecision proven(); - [[nodiscard]] static FacetDecision violation(std::string detail = {}); - [[nodiscard]] static FacetDecision unresolvedFor(UnresolvedReason reason, - std::string detail = {}); - [[nodiscard]] static FacetDecision trustedFor(TrustReason reason, - std::string detail = {}); - - /// A reason is present exactly for the outcomes that carry one. - [[nodiscard]] bool isWellFormed() const noexcept; - /// The reason's spelling, or empty for outcomes without one. - [[nodiscard]] std::string_view reasonText() const noexcept; - - /// Whether `other` wins a merge against `this` (§2.5): a strictly higher - /// rank. Records of equal rank keep the earlier one, so the merge is - /// deterministic for a deterministic engine. - [[nodiscard]] bool losesTo(const FacetDecision &other) const noexcept; - - friend bool operator==(const FacetDecision &, - const FacetDecision &) = default; -}; - -/// One facet of one site: the merged decision and what else is known about -/// it. -struct FacetRecord { - /// False until the first record. `LedgerAdapter::finish` fills every - /// undecided facet with its default (§2.6, `defaultDecision`). - bool decided = false; - /// The merged outcome, reason and detail. - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - FacetDecision decision = {}; - /// A suggested annotation. - std::optional fixit = std::nullopt; - /// Index of the linked diagnostic in `Ledger::diagnostics`. - std::optional diagnostic = std::nullopt; - - [[nodiscard]] SiteOutcome outcome() const noexcept { - return decision.outcome; - } - /// Merges one record by rank (§2.5). - void decide(const FacetDecision &record); - /// Forgets every decision (a new authoritative pass, §2.6), keeping only - /// the fact that the facet applies. - void reset(); - - friend bool operator==(const FacetRecord &, const FacetRecord &) = default; -}; - -/// §2.6: the outcome of a facet no record decided: `unresolved(budget)` in -/// a function over its budget, `unresolved(unanalysed)` otherwise. -[[nodiscard]] FacetDecision defaultDecision(bool overBudget); - -//===----------------------------------------------------------------------===// -// Sites, functions, units -//===----------------------------------------------------------------------===// - -/// A site within one unit: the index of its function in -/// `UnitLedger::functions` and its ordinal there. -struct SiteId { - std::uint32_t function = 0; - std::uint32_t ordinal = 0; - - friend auto operator<=>(const SiteId &, const SiteId &) = default; -}; - -/// A site's `text` is at most this many bytes. -inline constexpr std::size_t MaxSiteTextBytes = 80; - -/// A site's `text`: `source` with every ASCII whitespace character removed, -/// truncated to `MaxSiteTextBytes` without splitting a UTF-8 sequence. -[[nodiscard]] std::string siteText(std::string_view source); - -/// §2.1: one operation in one emitted function. -struct Site { - /// 0-based position in source order within the function. - std::uint32_t ordinal = 0; - SiteKind kind = SiteKind::Deref; - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - SourceLocation location = {}; - /// Normalised by `siteText`. - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::string text = {}; - /// Call sites only. - std::optional boundary = std::nullopt; - /// The callee's name for call-like sites. - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::string callee = {}; - /// Indexed by `Facet`; empty for facets that do not apply (§2.1). - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::array, FacetCount> facets = {}; - - /// Makes `facet` apply to the site (idempotent) and returns its record. - FacetRecord &addFacet(Facet facet); - [[nodiscard]] FacetRecord *facet(Facet facet); - [[nodiscard]] const FacetRecord *facet(Facet facet) const; - [[nodiscard]] bool hasFacet(Facet facet) const { - return facets.at(static_cast(facet)).has_value(); - } - /// The site outcome of the summary line: the highest-ranked outcome among - /// its facets. A site without facets is vacuously proven. - [[nodiscard]] SiteOutcome outcome() const noexcept; - - friend bool operator==(const Site &, const Site &) = default; -}; - -/// One emitted function and its sites in source order. -struct FunctionLedger { - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::string name = {}; - /// The file that holds the definition, which differs from the unit's - /// source for a function defined in a header. Empty means the unit's - /// source. - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::string file = {}; - std::uint32_t line = 0; - bool overBudget = false; - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::vector sites = {}; - - /// The site with `ordinal`, or null. - [[nodiscard]] Site *site(std::uint32_t ordinal) noexcept; - [[nodiscard]] const Site *site(std::uint32_t ordinal) const noexcept; - /// `LedgerAdapter::beginFunction`: forgets every decision an earlier pass - /// recorded (§2.5), keeping the sites and the facets that apply. - void resetDecisions(); - - friend bool operator==(const FunctionLedger &, - const FunctionLedger &) = default; -}; - -/// One translation unit. -struct UnitLedger { - /// The main source as the user named it. - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::string source = {}; - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::vector functions = {}; - - [[nodiscard]] Site *site(SiteId id) noexcept; - [[nodiscard]] const Site *site(SiteId id) const noexcept; - - friend bool operator==(const UnitLedger &, const UnitLedger &) = default; -}; - -//===----------------------------------------------------------------------===// -// Diagnostics, the ledger -//===----------------------------------------------------------------------===// - -/// A note of a ledger diagnostic. -struct LedgerNote { - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::string message = {}; - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - SourceLocation location = {}; - - friend bool operator==(const LedgerNote &, const LedgerNote &) = default; -}; - -/// A diagnostic the analysis reported, with the site it is about. -struct LedgerDiagnostic { - /// One of the ids in `weavec::core::diag`. - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::string id = {}; - Severity severity = Severity::Error; - Certainty certainty = Certainty::Definite; - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::string message = {}; - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - SourceLocation location = {}; - /// The enclosing function; empty at file scope. - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::string function = {}; - /// The linked site's ordinal within `function`, and its facet. - std::optional site = std::nullopt; - std::optional facet = std::nullopt; - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::vector notes = {}; - - friend bool operator==(const LedgerDiagnostic &, - const LedgerDiagnostic &) = default; -}; - -/// The default of `-fweavec-budget` (§5.5, RFC 0034 §7.1): the work of one -/// function's run, the sizes of the states it transfers and joins summed. -inline constexpr std::uint64_t DefaultBudget = 20000000; - -/// One unit's ledger (a run of the analysis), or a program's (`weavec -/// --whole-program`: every unit's). -struct Ledger { - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::vector units = {}; - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::vector diagnostics = {}; - - friend bool operator==(const Ledger &, const Ledger &) = default; -}; - -/// Why a ledger is not complete (§2.6): an undecided facet, a decision -/// whose reason does not fit its outcome, a dangling diagnostic link, or -/// ordinals that are not 0, 1, 2, ... Empty when complete. -[[nodiscard]] std::vector -completenessProblems(const Ledger &ledger); - -/// Puts the diagnostics in source order (file, line and column, then id and -/// message; stable otherwise) and updates the facets' links to them. -void sortDiagnostics(Ledger &ledger); - -//===----------------------------------------------------------------------===// -// Rollup and the summary line (RFC 0035 §8) -//===----------------------------------------------------------------------===// - -/// Outcome counts. -struct OutcomeCounts { - std::uint64_t proven = 0; - std::uint64_t violation = 0; - std::uint64_t unresolved = 0; - std::uint64_t trusted = 0; - - void add(SiteOutcome outcome, std::uint64_t count = 1) noexcept; - [[nodiscard]] std::uint64_t total() const noexcept { - return proven + violation + unresolved + trusted; - } - friend bool operator==(const OutcomeCounts &, - const OutcomeCounts &) = default; -}; - -/// The rollup of a ledger. -struct LedgerSummary { - std::uint64_t sites = 0; - /// Sites by site outcome (the highest-ranked facet). - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - OutcomeCounts outcomes = {}; - std::uint64_t errors = 0; - std::uint64_t warnings = 0; - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::vector overBudget = {}; - - /// Adds every site of `unit` and its functions over budget. - void addUnit(const UnitLedger &unit); - /// Counts `diagnostic` under `errors` or `warnings`. - void addDiagnostic(const LedgerDiagnostic &diagnostic) noexcept; - - friend bool operator==(const LedgerSummary &, - const LedgerSummary &) = default; -}; - -/// The summary of the whole ledger: every unit and every diagnostic. -[[nodiscard]] LedgerSummary summarize(const Ledger &ledger); - -/// `4210` -> `4,210`. -[[nodiscard]] std::string formatThousands(std::uint64_t value); - -/// RFC 0035 §8, unit form: `weavec: cJSON.c: 4,210 sites: 3,050 proven, -/// 1,130 not proven, 0 violations, 30 trusted; 0 errors, 2 warnings`, with -/// `; 1 function over budget ()` appended when a function is over -/// budget. -[[nodiscard]] std::string unitSummaryLine(std::string_view source, - const LedgerSummary &summary); - -/// Program form: `weavec: program minigzip: 9,876 sites in 3 units: -/// ; 0 errors, 1 warning[; functions over budget (...)]`. -[[nodiscard]] std::string programSummaryLine(std::string_view program, - std::size_t units, - const LedgerSummary &summary); - -} // namespace weavec::core - -#endif // WEAVEC_CORE_LEDGER_H diff --git a/include/weavec/Core/LibrarySpec.h b/include/weavec/Core/LibrarySpec.h index 9eca7692..06ccec92 100644 --- a/include/weavec/Core/LibrarySpec.h +++ b/include/weavec/Core/LibrarySpec.h @@ -1,4 +1,4 @@ -//===- LibrarySpec.h - The declarative C library table ----------*- C++ -*-===// +//===- LibrarySpec.h - The C library table of the guards --------*- C++ -*-===// // // Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. // See LICENSE for license information. @@ -6,23 +6,22 @@ // //===----------------------------------------------------------------------===// // -// RFC 0030 §8: one declarative table, `lib/Core/LibrarySpec.txt`, states what -// every modelled C library, POSIX, platform and compiler-builtin function -// does to its arguments and result: accesses and their lengths, nullability, -// ownership effects and release families, hidden state slots, callbacks, -// format arguments and fortified aliases. It also carries the §5.2 header -// list that decides which system headers are the platform's own. +// RFC 0036 §6: one declarative table, `lib/Core/LibrarySpec.txt`, states what +// the guards need of each C library, POSIX and platform function: which +// pointer arguments the call reads or writes and how many bytes, whether +// null is allowed, which arguments must not overlap, how a format reads its +// arguments, the runtime's checked version that replaces the call, and the +// fortified aliases with their object-size argument. // // CMake embeds the text in the library; `LibrarySpec::shipped()` parses it // once on first use. `LibrarySpec::parse` parses any text in the same syntax -// (tests, experiments). The syntax, its semantics and the extensions to the -// RFC's §8.2 grammar are documented at the top of `LibrarySpec.txt`. +// (tests). The syntax and its meaning are documented at the top of +// `LibrarySpec.txt`. // -// Which calls a row governs (§8): a direct call whose callee's name is the -// row's name, `__builtin_`, or one of the row's `chk` aliases (with the -// alias's arguments remapped onto the row's), provided the program does not -// define a function of that name. That last condition is the engine's to -// check; this class only resolves names. +// Which calls a row governs: a direct call whose callee's name is the row's +// name, `__builtin_`, or one of the row's `chk` aliases (with the +// alias's arguments remapped onto the row's), provided the unit does not +// define a function of that name (the guard pass's check). // //===----------------------------------------------------------------------===// @@ -33,7 +32,6 @@ #include #include #include -#include #include #include #include @@ -41,47 +39,37 @@ namespace weavec::core { -/// A small expression over a call's arguments (§8.1): +/// A small expression over a call's arguments: /// -/// term ::= INT | 'a' N | 'strlen(a' N ')' | 'fmtlen(a' N ')' | MACRO -/// | term '*' term | term '+' term | term '-' INT +/// term ::= INT | 'a' N | 'strlen(a' N ')' | term '*' term +/// | term '+' term | term '-' INT | term '/' INT /// | 'min(' term ',' term ')' | '(' term ')' /// -/// `*` binds tighter than `+` and `-`; both associate to the left. -/// `strlen(aN)` counts the elements of argument N's pointee type before the -/// terminating zero element (`wcslen` for `wchar_t`); inside `min(t, …)` it -/// is the bounded length `strnlen(aN, t)` and requires no terminator within -/// `t` elements. `fmtlen(aN)` is the length the `printf` family would -/// produce for the format at argument N; it states a requirement and is -/// never evaluated as a check term. `MACRO` (an identifier that starts with -/// an upper-case letter or `_`, such as `BUFSIZ` or `L_tmpnam`) is the value -/// of that macro in the calling unit; where it is undefined the term is -/// unknown. Terms are mathematical integers: they never wrap. +/// `*` and `/` bind tighter than `+` and `-`; all associate to the left. +/// `strlen(aN)` counts the elements of argument N's string before its +/// terminator; inside `min(t, …)` it is the bounded length `strnlen(aN, t)` +/// and requires no terminator within `t` elements. Terms are mathematical +/// integers. struct LibTerm { enum class Kind : std::uint8_t { Constant, ///< `value` Argument, ///< `a` StringLength, ///< `strlen(a)` - FormatLength, ///< `fmtlen(a)` - Macro, ///< `macro` Product, ///< `operands[0] * operands[1]` Sum, ///< `operands[0] + operands[1]` Difference, ///< `operands[0] - value` Min, ///< `min(operands[0], operands[1])` - Quotient, ///< `operands[0] / value`, rounded down (RFC 0033 §3) + Quotient, ///< `operands[0] / value`, rounded down }; Kind kind = Kind::Constant; std::int64_t value = 0; std::uint8_t arg = 0; // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::string macro = {}; - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default std::vector operands = {}; [[nodiscard]] static LibTerm constant(std::int64_t value); [[nodiscard]] static LibTerm argument(unsigned index); [[nodiscard]] static LibTerm stringLength(unsigned index); - [[nodiscard]] static LibTerm formatLength(unsigned index); [[nodiscard]] static LibTerm product(LibTerm left, LibTerm right); [[nodiscard]] static LibTerm sum(LibTerm left, LibTerm right); [[nodiscard]] static LibTerm difference(LibTerm left, std::int64_t right); @@ -99,8 +87,6 @@ struct LibTerm { struct Values { std::function(unsigned)> argument; std::function(unsigned)> stringLength; - std::function(unsigned)> formatLength; - std::function(std::string_view)> macro; }; /// Evaluates the term, or `std::nullopt` when a leaf is unknown or the /// value leaves the range of `std::int64_t`. @@ -110,28 +96,6 @@ struct LibTerm { friend bool operator==(const LibTerm &, const LibTerm &) = default; }; -/// How a function-pointer argument is invoked (§5.3). -struct LibCallback { - enum class Kind : std::uint8_t { - /// Zero or more times before the call returns (`qsort`, `bsearch`). - Sync, - /// Later, as an entry point: a thread or a signal handler - /// (`pthread_create`, `signal`, `sigaction`). - Entry, - /// After the main flow ends (`atexit`, `at_quick_exit`, `on_exit`). - AtExit, - }; - Kind kind = Kind::Sync; - /// Sync: for each parameter of the target, the call argument its pointer - /// points into (`qsort`: `{0, 0}`); empty when the target receives only - /// pointers to the library's own storage (`nftw`). Entry: at most one - /// index, the argument passed to the target (`pthread_create`: `{3}`). - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::vector arguments = {}; - - friend bool operator==(const LibCallback &, const LibCallback &) = default; -}; - /// `disjoint(first, second, length)`: the first `length` bytes behind the /// two arguments must not overlap (`memcpy`, `strcpy`). struct LibDisjoint { @@ -142,50 +106,16 @@ struct LibDisjoint { friend bool operator==(const LibDisjoint &, const LibDisjoint &) = default; }; -/// RFC 0035 §2.5: `wrapper(name)`: the runtime's checked -/// `__weavec_rt_` replaces the call. It takes the call's arguments and -/// the site (first when the call is variadic, last otherwise) and checks -/// what the call reads and writes as it runs: the bytes the row's terms -/// could not name before the call (a formatted write's), or fewer than -/// they name (`memchr` reads up to the byte it finds). +/// `wrapper(name)`: the runtime's checked `__weavec_rt_` replaces the +/// call. It takes the call's arguments and the site (first when the call is +/// variadic, last otherwise) and checks what the call reads and writes as +/// it runs. struct LibWrapper { std::string name; friend bool operator==(const LibWrapper &, const LibWrapper &) = default; }; -/// `copies(dst, src, length)`: the call copies the first `length` bytes of -/// argument `src` to argument `dst` (`memcpy`, `strncpy`). -struct LibCopy { - std::uint8_t dst = 0; - std::uint8_t src = 0; - LibTerm length; - - friend bool operator==(const LibCopy &, const LibCopy &) = default; -}; - -/// `fills(dst, value, length)`: the call sets the first `length` bytes of -/// argument `dst` to `value` (`memset`, `bzero`). -struct LibFill { - std::uint8_t dst = 0; - LibTerm value; - LibTerm length; - - friend bool operator==(const LibFill &, const LibFill &) = default; -}; - -/// `writes-str(dst[, length])`: when the call succeeds, argument `dst` -/// holds a string of `length` characters, or of unknown length when absent -/// (`strcpy`, `fgets`). Terms read the arguments' values before the call; a -/// negative length means nothing is written (`snprintf(d, 0, …)`). -struct LibStringWrite { - std::uint8_t dst = 0; - std::optional length; - - friend bool operator==(const LibStringWrite &, - const LibStringWrite &) = default; -}; - /// The `printf(f, v)` / `scanf(f, v)` clause. struct LibFormat { enum class Kind : std::uint8_t { Printf, Scanf }; @@ -193,215 +123,81 @@ struct LibFormat { /// The format argument. std::uint8_t format = 0; /// The first variadic argument; for a `v…` function (`vprintf`), whose - /// entry is not variadic, the `va_list` argument. + /// row is not variadic, the `va_list` argument. std::uint8_t first = 0; - /// True when `first` is a `va_list` (the entry is not variadic): the + /// True when `first` is a `va_list` (the row is not variadic): the /// conversions cannot be matched against the arguments. bool vaList = false; friend bool operator==(const LibFormat &, const LibFormat &) = default; }; -/// The result of a call, or (`LibraryParam::out`) the value the call stores -/// through a pointer-to-pointer argument. -struct LibraryResult { - enum class Kind : std::uint8_t { - Void, ///< `void`, `noreturn` - Int, ///< `int`: any non-pointer result - Fresh, ///< `fresh(F)`: a new resource of family `family` - Static, ///< `static(S)`: a borrow of hidden state slot `state` - Arg, ///< `arg(N)`: argument `arg` itself - Interior, ///< `interior(N)`: a pointer into argument `arg`'s object - InteriorState, ///< `interior-state(S)`: into the value `S` retains - Unknown, ///< `ptr`: a pointer of unknown provenance - }; - Kind kind = Kind::Void; - /// Fresh: the release family. Arg: when non-empty, the result is instead - /// `fresh(family)` when argument `arg` is null (`realpath`, `getcwd`). - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::string family = {}; - /// Static, InteriorState: the hidden state slot. Arg: when non-empty, the - /// result is instead `static(state)` when argument `arg` is null - /// (`tmpnam`, `ctermid`). - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::string state = {}; - /// Arg, Interior: the argument. - std::uint8_t arg = 0; - /// Fresh, Static: accessible bytes. - std::optional extent; - enum class Null : std::uint8_t { - /// Never null. For `arg(N)`: exactly the argument, null only when the - /// argument is. - Never, - /// Null exactly when the call fails (allocation or lookup failure). - OnFailure, - /// May be null for any other reason (`strchr` found nothing). - May, - }; - Null null = Null::Never; - /// §11: a `fresh` result eligible for zero-initialisation lowering. - bool zeroInit = false; - /// `str`: the pointer is to a string terminated by a zero element. - bool string = false; - /// `replaces` (out values only): the value previously in the slot is - /// consumed as by `realloc` of the same family before the new one is - /// stored (`getline`). - bool replaces = false; - /// `value(t)` (Int): the result equals `t` (`strlen` is `strlen(a0)`). - std::optional value; - /// `offset(t)` (Interior): the result is `t` elements past the start of - /// argument `arg` (`stpcpy` is `strlen(a1)`). - std::optional offset; - /// `zero-filled` (Fresh): the library sets the whole extent to zero bytes - /// (`calloc`), whatever §11 lowering does. - bool zeroFilled = false; - - /// True for the pointer kinds (everything but Void and Int). - [[nodiscard]] bool isPointer() const noexcept; - friend bool operator==(const LibraryResult &, - const LibraryResult &) = default; -}; - -/// One parameter of a row (§8.1). +/// One parameter of a row. struct LibraryParam { enum class Type : std::uint8_t { Int, Pointer, Function, Other }; Type type = Type::Int; /// What the call does to the bytes behind a pointer. enum class Access : std::uint8_t { None, Read, Write, ReadWrite }; Access access = Access::None; - /// Required accessible bytes behind the pointer. Without `bytes`, `count` - /// or `string`, a pointer needs one element of its pointee type. + /// Bytes the call accesses behind the pointer. Without `bytes`, `count` + /// or `string`, its first byte is guarded. std::optional bytes; - /// Required accessible elements of the pointee type (`wmemcpy`). + /// `wchar_t` elements the call accesses (wide rows). std::optional count; - /// The pointee must hold a terminated string within its object. + /// The pointee holds a terminated string. bool string = false; enum class Null : std::uint8_t { Forbidden, Allowed, AllowedIfZero }; Null null = Null::Forbidden; /// AllowedIfZero: null is allowed when this term is zero. std::optional zeroTerm; - enum class Effect : std::uint8_t { - Borrow, ///< Used for the call only. - Release, ///< Released (`free`); `family` names the releaser. - Realloc, ///< Released on success, or kept, as `realloc` (§8.2). - Retain, ///< Stored in hidden state slot `state` for later `reads`. - Escape, ///< Kept by the library where no row can follow it. - Init, ///< The pointee acquires a resource of `family` (`regcomp`). - Fini, ///< The pointee's `family` resource ends; its storage stays - ///< valid and may be initialised again (`regfree`). - }; + /// `release`, `realloc`: the allocator validates the pointer. + enum class Effect : std::uint8_t { Borrow, Release, Realloc }; Effect effect = Effect::Borrow; - /// Release, Realloc, Init, Fini: the family. - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::string family = {}; - /// Retain: the hidden state slot. - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::string state = {}; - /// Function parameters, and pointer parameters whose pointee holds the - /// function pointers (`sigaction`'s `act`). - std::optional callback; - /// `out(…)`: the call stores this value through the pointer, which points - /// to a pointer slot (`getline`, `asprintf`, `strtol`'s end pointer). - std::optional out; - /// `range(t)` (Release only, RFC 0034 §6.3): the call releases the `t` - /// bytes from the pointer (`munmap`): the object only when they cover it - /// from its start; any other release of it is possible, and the pointer - /// may point anywhere in it. - std::optional range; friend bool operator==(const LibraryParam &, const LibraryParam &) = default; }; -/// A fortified alias (§8, "Which calls a row governs"). +/// A fortified alias. struct LibraryChk { /// `__builtin___memcpy_chk`, `__memcpy_chk`. std::string name; - /// For each alias argument, the row's argument index, or -1 (dropped). - /// Alias arguments beyond the list continue from the last mapped index - /// (variadic rows only). + /// For each alias argument, the row's argument index, or -1 (dropped: + /// a flag, or the object size). Alias arguments beyond the list continue + /// from the last mapped index (variadic rows only). // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default std::vector argumentOf = {}; + /// The alias argument that carries the size of the object the row + /// writes (`size`), or -1. + int sizeArgument = -1; friend bool operator==(const LibraryChk &, const LibraryChk &) = default; }; -/// The parameter classes of a declaration, to choose between rows of the -/// same name (`qsort_r` has a glibc and a BSD order) and to refuse a row for -/// a declaration that does not fit it. -struct LibSignature { - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::vector params = {}; - bool variadic = false; - bool pointerResult = false; - - friend bool operator==(const LibSignature &, const LibSignature &) = default; -}; - -/// One row of the table (§8.1). +/// One row of the table. struct LibraryEntry { std::string name; - /// The header that declares it, relative to its include directory; empty - /// for compiler builtins. - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::string header = {}; // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default std::vector chk = {}; // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default std::vector params = {}; bool variadic = false; - LibraryResult result; + /// The result: none, a non-pointer, or a pointer. + enum class Result : std::uint8_t { Void, Int, Pointer }; + Result result = Result::Void; + /// `value(t)` (Int): the result equals `t` (`strlen` is `strlen(a0)`). + std::optional value; // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default std::vector disjoint = {}; - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::vector copies = {}; - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::vector fills = {}; - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::vector writesString = {}; - /// State slots whose borrows end (`setenv` → `environ`). - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::vector invalidates = {}; - /// State slots read (`strtok(NULL, …)` → `strtok`). - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::vector reads = {}; - bool noreturn = false; - bool exits = false; - bool returnsTwice = false; /// `wide`: the row's strings end in a zero `wchar_t`, and `count` and /// `strlen(aN)` count `wchar_t` elements (`wcslen`). bool wide = false; std::optional format; - /// RFC 0035 §2.5: the runtime's checked version that replaces the call. std::optional wrapper; /// The line of `LibrarySpec.txt` the row starts on. unsigned line = 0; /// The parameter for argument `index`, or null for a variadic argument. [[nodiscard]] const LibraryParam *param(unsigned index) const noexcept; - /// The result or an `out` value is a fresh resource. - [[nodiscard]] bool allocates() const noexcept; - /// Some argument is released or reallocated: calls are Release sites. - [[nodiscard]] bool releases() const noexcept; - /// Some argument carries a callback clause (a §9.4 boundary). - [[nodiscard]] bool hasCallback() const noexcept; - /// RFC 0034 §6.3: the row gives pointer argument `index` no access - /// requirement and no effect (`mmap`'s address hint, `none` with neither - /// an extent, an effect, a callback nor an out value, and named by no - /// clause or callback): the call uses only its value. - [[nodiscard]] bool usesValueOnly(unsigned index) const noexcept; - /// Neither `noreturn` nor `exits` (§7.5 "known to return"). - [[nodiscard]] bool knownToReturn() const noexcept; - /// A fact rests on the row's own statement about hidden behaviour: a - /// `static`/`interior-state` result, `retain`/`reads`/`invalidates`, or - /// a callback clause. Such facets are `trusted(library-spec)` (§8.2). - [[nodiscard]] bool trustsLibrarySpec() const noexcept; - [[nodiscard]] LibSignature signature() const; - /// True if a declaration with `declared` classes may use this row: the - /// same arity and variadic-ness, the same pointer-ness of the result, and - /// each `int`, pointer or `fn` parameter of the same class (`other` - /// matches anything, as does a pointer declared where the row says - /// `other`, for `va_list`). - [[nodiscard]] bool accepts(const LibSignature &declared) const; friend bool operator==(const LibraryEntry &, const LibraryEntry &) = default; }; @@ -413,13 +209,15 @@ struct LibraryMatch { const LibraryChk *alias = nullptr; /// The row argument that call argument `callArgument` becomes, or -1 when - /// the alias drops it (an object-size or flag argument). + /// the alias drops it. [[nodiscard]] int rowArgument(unsigned callArgument) const noexcept; /// The call argument that carries row argument `rowArg`, or -1. [[nodiscard]] int callArgument(unsigned rowArg) const noexcept; - /// The row parameter for call argument `callArgument`, or null (dropped - /// or variadic). - [[nodiscard]] const LibraryParam *param(unsigned callArgument) const noexcept; + /// The call argument that carries the object size of a fortified alias, + /// or -1. + [[nodiscard]] int sizeArgument() const noexcept { + return alias != nullptr ? alias->sizeArgument : -1; + } }; /// The parsed table. @@ -440,63 +238,26 @@ class LibrarySpec { [[nodiscard]] const std::vector &entries() const noexcept { return rows; } - /// The §5.2 header list, in file order (`dir/*` patterns included). - [[nodiscard]] const std::vector &headers() const noexcept { - return headerList; - } /// The row named exactly `name` (the first, if it has overloads). [[nodiscard]] const LibraryEntry *find(std::string_view name) const; /// Resolves a callee name: the row's own name, a `chk` alias, or either - /// behind a `__builtin_` prefix. The first overload wins. + /// behind a `__builtin_` prefix. With `arguments`, the first overload + /// whose arity the call matches (a variadic row accepts more), else the + /// first. [[nodiscard]] std::optional - lookup(std::string_view callee) const; - /// As above, choosing the overload that `accepts(declared)`; no match - /// when none does. - [[nodiscard]] std::optional - lookup(std::string_view callee, const LibSignature &declared) const; - - /// §5.2: whether a system header, named relative to the include directory - /// that found it (`stdio.h`, `sys/ioctl.h`), is a C library, POSIX or - /// platform header. On Darwin every header of the SDK is - /// (`darwinSdk`). Headers under the toolchain's resource directory always - /// are; that is the caller's test, not this one. - [[nodiscard]] bool isPlatformHeader(std::string_view relativePath, - bool darwinSdk) const; + lookup(std::string_view callee, + std::optional arguments = std::nullopt) const; private: std::vector rows; - std::vector headerList; - /// The concrete names of `headerList`, and its `dir/*` patterns as - /// `dir/` prefixes. - std::set> headerNames; - std::vector headerPrefixes; std::multimap> byName; /// Alias name to (row index, index into the row's `chk`). std::map, std::less<>> byAlias; - - [[nodiscard]] std::optional - resolve(std::string_view callee, const LibSignature *declared) const; friend class LibrarySpecParser; }; -/// The heap family: `malloc`'s releaser (`free`). Release-family names are -/// the releasing function's name (RFC 0007); compare against this rather -/// than spelling the libc name. -inline constexpr std::string_view HeapFamily = "free"; -/// The family of `alloca` storage, released when the frame returns. -inline constexpr std::string_view StackFamily = "stack"; -/// The family of `FILE` streams from `fopen` and friends. -inline constexpr std::string_view StreamFamily = "fclose"; - -/// RFC 0030 §8.2: the number of variadic arguments a literal `printf` -/// (`kind` Printf) or `scanf` format reads, `*` widths and precisions -/// included; nothing for a format with positional (`%1$d`) conversions or a -/// malformed one. -[[nodiscard]] std::optional -formatArgumentCount(std::string_view format, LibFormat::Kind kind); - } // namespace weavec::core #endif // WEAVEC_CORE_LIBRARYSPEC_H diff --git a/include/weavec/Core/Path.h b/include/weavec/Core/Path.h deleted file mode 100644 index 4df043ce..00000000 --- a/include/weavec/Core/Path.h +++ /dev/null @@ -1,204 +0,0 @@ -//===- Path.h - Places relative to a function's interface -------*- C++ -*-===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// A *summary path* names a place a function can see from the outside: a -// parameter, a global or the result, followed by field, dereference and -// index steps (RFC 0003, RFC 0031 §1). -// -// root ::= param(i) | global(g) | result -// path ::= root ('*' | '.' field | '[' selector ']')* -// -// Summaries (`Effects.h`), boundary facts and pointer kinds name places this -// way. Roots are integers: the Analysis layer resolves them against a call's -// arguments or a unit's globals. -// -//===----------------------------------------------------------------------===// - -#ifndef WEAVEC_CORE_PATH_H -#define WEAVEC_CORE_PATH_H - -#include -#include -#include -#include -#include -#include -#include - -namespace weavec::core { - -/// `a == b` for the short names of path steps, compared in line: a call to -/// `memcmp` per name dominated the comparisons of paths. -[[nodiscard]] inline bool sameText(std::string_view a, - std::string_view b) noexcept { - if (a.size() != b.size()) - return false; - for (std::size_t i = 0; i < a.size(); ++i) - if (a[i] != b[i]) - return false; - return true; -} - -/// `a <=> b` as `std::string` orders them (bytes as unsigned characters, -/// then length), compared in line (see `sameText`). -[[nodiscard]] inline std::strong_ordering -compareText(std::string_view a, std::string_view b) noexcept { - const std::size_t common = a.size() < b.size() ? a.size() : b.size(); - for (std::size_t i = 0; i < common; ++i) - if (a[i] != b[i]) - return static_cast(a[i]) <=> - static_cast(b[i]); - return a.size() <=> b.size(); -} - -/// One step of a path. -enum class PathStep : std::uint8_t { - /// `parent.field` (or `parent->field` when the parent is a dereference). - Field, - /// `*parent`: the object the pointer stored in `parent` refers to. - Deref, - /// An element: every element (empty selector) or a named one. - Index, -}; - -/// What a summary path is rooted at. -enum class SummaryRoot : std::uint8_t { - /// The `index`-th parameter of the function. - Param, - /// A global variable, identified by an id interned per translation unit. - Global, - /// The returned value (RFCs 0008 and 0013). Record fields use `.field`; - /// pointer-result heap fields use `*.field`. `index` is always zero. - Result, -}; - -/// One step below a root, with its field name or selector. An anonymous -/// member is spelled by its index, `#2` (RFC 0031 §7). -struct PathElem { - PathStep step = PathStep::Field; - std::string field; - - friend bool operator==(const PathElem &a, const PathElem &b) noexcept { - return a.step == b.step && sameText(a.field, b.field); - } - friend std::strong_ordering operator<=>(const PathElem &a, - const PathElem &b) noexcept { - if (const auto order = a.step <=> b.step; std::is_neq(order)) - return order; - return compareText(a.field, b.field); - } -}; - -/// The steps of a path. -class PathSteps { -public: - using ConstIterator = std::vector::const_iterator; - PathSteps() = default; - PathSteps(std::initializer_list elements) : elements(elements) {} - - [[nodiscard]] std::size_t size() const noexcept { return elements.size(); } - [[nodiscard]] bool empty() const noexcept { return elements.empty(); } - [[nodiscard]] ConstIterator begin() const { return elements.begin(); } - [[nodiscard]] ConstIterator end() const { return elements.end(); } - [[nodiscard]] const PathElem *data() const { return elements.data(); } - [[nodiscard]] const PathElem &front() const { return elements.front(); } - [[nodiscard]] const PathElem &back() const { return elements.back(); } - [[nodiscard]] const PathElem &operator[](std::size_t index) const { - return elements[index]; - } - void pushBack(PathElem element) { elements.push_back(std::move(element)); } - void pushFront(PathElem element) { - elements.insert(elements.begin(), std::move(element)); - } - void popBack() { elements.pop_back(); } - void truncate(std::size_t size) { - if (size < elements.size()) - elements.resize(size); - } - void append(const PathSteps &other, std::size_t first = 0) { - if (first < other.elements.size()) - elements.insert(elements.end(), - other.elements.begin() + - static_cast(first), - other.elements.end()); - } - - friend bool operator==(const PathSteps &, const PathSteps &) = default; - friend std::strong_ordering operator<=>(const PathSteps &a, - const PathSteps &b) { - return std::lexicographical_compare_three_way( - a.elements.begin(), a.elements.end(), b.elements.begin(), - b.elements.end()); - } - -private: - std::vector elements; -}; - -/// RFC 0014: two pointer parameters that compare equal (or not). -struct ParamPairTest { - std::uint32_t first = 0; - std::uint32_t second = 0; - bool equal = true; - - friend bool operator==(const ParamPairTest &, - const ParamPairTest &) = default; - friend auto operator<=>(const ParamPairTest &, - const ParamPairTest &) = default; -}; - -/// A place relative to a function's interface: `param(0)`, `param(0)*`, -/// `param(0)*.data`, `global(3)`. -struct SummaryPath { - SummaryRoot root = SummaryRoot::Param; - std::uint32_t index = 0; - PathSteps steps; - - [[nodiscard]] static SummaryPath param(std::uint32_t index) { - return SummaryPath{.root = SummaryRoot::Param, .index = index, .steps = {}}; - } - [[nodiscard]] static SummaryPath global(std::uint32_t id) { - return SummaryPath{.root = SummaryRoot::Global, .index = id, .steps = {}}; - } - [[nodiscard]] static SummaryPath result() { - return SummaryPath{.root = SummaryRoot::Result, .index = 0, .steps = {}}; - } - - [[nodiscard]] SummaryPath deref() const; - [[nodiscard]] SummaryPath field(std::string_view name) const; - [[nodiscard]] SummaryPath indexed(std::string_view selector = {}) const; - - [[nodiscard]] bool isRoot() const noexcept { return steps.empty(); } - [[nodiscard]] bool isParam() const noexcept { - return root == SummaryRoot::Param; - } - [[nodiscard]] bool isGlobal() const noexcept { - return root == SummaryRoot::Global; - } - [[nodiscard]] bool isResult() const noexcept { - return root == SummaryRoot::Result; - } - /// True if `this` is a proper prefix of `other` (same root, fewer steps). - [[nodiscard]] bool isProperPrefixOf(const SummaryPath &other) const; - /// The root path (`param(i)` / `global(g)`). - [[nodiscard]] SummaryPath rootPath() const { - return SummaryPath{.root = root, .index = index, .steps = {}}; - } - - /// Spells the path given the display name of the root: `p`, `*p`, - /// `p->data`, `a[*]`. - [[nodiscard]] std::string toString(std::string_view rootName) const; - - friend bool operator==(const SummaryPath &, const SummaryPath &) = default; - friend std::strong_ordering operator<=>(const SummaryPath &, - const SummaryPath &) = default; -}; - -} // namespace weavec::core - -#endif // WEAVEC_CORE_PATH_H diff --git a/include/weavec/Core/Persistent.h b/include/weavec/Core/Persistent.h deleted file mode 100644 index 27cf4e43..00000000 --- a/include/weavec/Core/Persistent.h +++ /dev/null @@ -1,260 +0,0 @@ -//===- Persistent.h - Shared, copy-on-write sorted maps ---------*- C++ -*-===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// RFC 0031 §4.8: the object engine's states are copied along every CFG edge, -// so every map in a state is a `PMap`: a sorted vector shared by reference -// count and copied on the first write through a shared handle. A copy costs -// one reference; a write to an unshared map costs what a write to a sorted -// vector costs; a write to a shared one copies it once. -// -//===----------------------------------------------------------------------===// - -#ifndef WEAVEC_CORE_PERSISTENT_H -#define WEAVEC_CORE_PERSISTENT_H - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace weavec::core { - -/// A sorted map with value semantics whose copies share storage until one of -/// them is written. A large value (a symbol's or an object's description) -/// is held in its own shared box, so copying the map to write one entry -/// shares every other entry's value rather than copying it, and an insert -/// moves pointers rather than values. -template > -class PMap { - static constexpr bool Boxed = sizeof(Value) > 4 * sizeof(void *); - using Slot = std::conditional_t, Value>; - using Stored = std::pair; - using Storage = std::vector; - - static const Value &valueOf(const Slot &slot) { - if constexpr (Boxed) - return *slot; - else - // NOLINTNEXTLINE(bugprone-return-const-ref-from-parameter): a stored slot - return slot; - } - static Slot slotOf(Value value) { - if constexpr (Boxed) - return std::make_shared(std::move(value)); - else - return value; - } - -public: - /// Whether a value stays where it is while other entries are written - /// (a reference to it outlives inserts; not an erase of its own entry). - static constexpr bool StableValues = Boxed; - using Reference = std::pair; - - /// Iterates the entries in key order as (key, value) reference pairs. - class ConstIterator { - public: - // NOLINTBEGIN(readability-identifier-naming): the standard library's names - using iterator_category = std::forward_iterator_tag; - using value_type = std::pair; - using difference_type = std::ptrdiff_t; - using reference = Reference; - // NOLINTEND(readability-identifier-naming) - struct Arrow { - Reference entry; - const Reference *operator->() const { return &entry; } - }; - // NOLINTNEXTLINE(readability-identifier-naming): a standard library name - using pointer = Arrow; - - ConstIterator() = default; - explicit ConstIterator(Storage::const_iterator at) : at(at) {} - Reference operator*() const { return {at->first, valueOf(at->second)}; } - Arrow operator->() const { return Arrow{**this}; } - ConstIterator &operator++() { - ++at; - return *this; - } - ConstIterator operator++(int) { - ConstIterator old = *this; - ++at; - return old; - } - friend bool operator==(const ConstIterator &a, const ConstIterator &b) { - return a.at == b.at; - } - - private: - Storage::const_iterator at{}; - }; - - PMap() = default; - - /// The map of `entries`, which are sorted by key with no key twice. - static PMap fromSorted(std::vector> entries) { - PMap map; - if (entries.empty()) - return map; - map.data = std::make_shared(); - map.data->reserve(entries.size()); - for (auto &[key, value] : entries) - map.data->emplace_back(key, slotOf(std::move(value))); - return map; - } - - [[nodiscard]] std::size_t size() const noexcept { - return data ? data->size() : 0; - } - [[nodiscard]] bool empty() const noexcept { return size() == 0; } - [[nodiscard]] ConstIterator begin() const { - return ConstIterator(data ? data->cbegin() : emptyStorage().cbegin()); - } - [[nodiscard]] ConstIterator end() const { - return ConstIterator(data ? data->cend() : emptyStorage().cend()); - } - - /// The value at `key`, or null. - [[nodiscard]] const Value *find(const Key &key) const { - if (!data) - return nullptr; - auto it = lowerBound(*data, key); - if (it == data->end() || Less{}(key, it->first)) - return nullptr; - return &valueOf(it->second); - } - [[nodiscard]] bool contains(const Key &key) const { - return find(key) != nullptr; - } - /// The value at `key`, or `fallback`. - [[nodiscard]] Value get(const Key &key, Value fallback = Value{}) const { - const Value *value = find(key); - return value != nullptr ? *value : std::move(fallback); - } - - /// Sets `key` to `value`, copying shared storage first. - void set(const Key &key, Value value) { - Storage &storage = writable(); - auto it = lowerBoundMutable(storage, key); - if (it != storage.end() && !Less{}(key, it->first)) { - if constexpr (Boxed) { - if (it->second.use_count() == 1) { - *it->second = std::move(value); - return; - } - } - it->second = slotOf(std::move(value)); - } else { - storage.insert(it, Stored(key, slotOf(std::move(value)))); - } - } - /// A writable reference to the value at `key`, default-constructed when - /// absent. - Value &at(const Key &key) { - Storage &storage = writable(); - auto it = lowerBoundMutable(storage, key); - if (it == storage.end() || Less{}(key, it->first)) - it = storage.insert(it, Stored(key, slotOf(Value{}))); - if constexpr (Boxed) { - if (it->second.use_count() > 1) - it->second = std::make_shared(*it->second); - return *it->second; - } else { - return it->second; - } - } - /// Removes `key`; returns whether it was present. - bool erase(const Key &key) { - if (!find(key)) - return false; - Storage &storage = writable(); - storage.erase(lowerBoundMutable(storage, key)); - return true; - } - /// Removes every entry for which `drop(key, value)` holds. - template - void eraseIf(Predicate drop) { - if (!data) - return; - auto first = - std::find_if(data->begin(), data->end(), [&](const Stored &entry) { - return drop(entry.first, valueOf(entry.second)); - }); - if (first == data->end()) - return; - auto from = static_cast(first - data->begin()); - Storage &storage = writable(); - storage.erase( - std::remove_if(storage.begin() + static_cast(from), - storage.end(), - [&](const Stored &entry) { - return drop(entry.first, valueOf(entry.second)); - }), - storage.end()); - } - void clear() { data.reset(); } - - /// Whether the two maps share storage (a cheap equality test). - [[nodiscard]] bool sharesWith(const PMap &other) const noexcept { - return data == other.data; - } - - friend bool operator==(const PMap &a, const PMap &b) { - if (a.data == b.data) - return true; - if (a.size() != b.size()) - return false; - if (a.empty()) - return true; - return std::equal(a.data->begin(), a.data->end(), b.data->begin(), - [](const Stored &x, const Stored &y) { - if (Less{}(x.first, y.first) || - Less{}(y.first, x.first)) - return false; - if constexpr (Boxed) - if (x.second == y.second) - return true; - return valueOf(x.second) == valueOf(y.second); - }); - } - -private: - std::shared_ptr data; - - static const Storage &emptyStorage() { - static const Storage Empty; - return Empty; - } - static Storage::const_iterator lowerBound(const Storage &storage, - const Key &key) { - return std::lower_bound(storage.begin(), storage.end(), key, - [](const Stored &entry, const Key &probe) { - return Less{}(entry.first, probe); - }); - } - static Storage::iterator lowerBoundMutable(Storage &storage, const Key &key) { - return std::lower_bound(storage.begin(), storage.end(), key, - [](const Stored &entry, const Key &probe) { - return Less{}(entry.first, probe); - }); - } - Storage &writable() { - if (!data) - data = std::make_shared(); - else if (data.use_count() > 1) - data = std::make_shared(*data); - return *data; - } -}; - -} // namespace weavec::core - -#endif // WEAVEC_CORE_PERSISTENT_H diff --git a/include/weavec/Core/PointerKind.h b/include/weavec/Core/PointerKind.h deleted file mode 100644 index 893eb3da..00000000 --- a/include/weavec/Core/PointerKind.h +++ /dev/null @@ -1,220 +0,0 @@ -//===- PointerKind.h - The pointer-kind lattice (RFC 0030) -----*- C++ -*-===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// RFC 0030 §7.1: what a pointer value guarantees, or what a parameter -// requires, about the memory accessible from it. For a value `p` of type -// `T *` that is not null: -// -// single at least one `T` is accessible from `p` -// counted(e) at least `e` elements of `T` are accessible -// sized(e) at least `e` bytes are accessible -// ended-by(q) `p <= q`, and `[p, q)` lies in one object -// nul-terminated a zero element lies at or after `p` within its object -// unknown nothing -// -// followed by `nonnull` or `nullable`. Every kind is a lower bound: none -// claims that the object ends there. Whether a kind may be compared against -// by a check is its `ExtentClass`: exact extents and declared kinds may, -// lower bounds (Single, the defaults, inferred requirements) may not. -// -// Extent terms are `k * + c` or the constant `c`, where the path is a -// sibling parameter (`param `) or a sibling field (`.`) of the -// same object. They are spelled as in the RFC 0011 extent grammar: -// `` or ` scale plus `. -// -// The path is an `ExtentPath` rather than a `SummaryPath`: summaries carry -// kinds (summary format 27), so `Summary.h` depends on this header, and a -// summary path has no root for "a sibling field of the same object". -// -//===----------------------------------------------------------------------===// - -#ifndef WEAVEC_CORE_POINTERKIND_H -#define WEAVEC_CORE_POINTERKIND_H - -#include -#include -#include -#include -#include -#include -#include - -namespace weavec::core { - -enum class PointerShape : std::uint8_t { - Single, - Counted, - Sized, - EndedBy, - NulTerminated, - Unknown, -}; - -enum class Nullability : std::uint8_t { Nonnull, Nullable }; - -/// Where a kind came from (§7.2, §7.3, §7.5). -enum class KindSource : std::uint8_t { Declared, Inferred, Default }; - -[[nodiscard]] std::string_view toString(PointerShape shape) noexcept; -[[nodiscard]] std::string_view toString(Nullability nullability) noexcept; -[[nodiscard]] std::string_view toString(KindSource source) noexcept; -[[nodiscard]] std::optional -parseNullability(std::string_view text); -[[nodiscard]] std::optional parseKindSource(std::string_view text); - -/// Whether a shape carries an extent term (Counted, Sized, EndedBy). -[[nodiscard]] constexpr bool hasExtent(PointerShape shape) noexcept { - return shape == PointerShape::Counted || shape == PointerShape::Sized || - shape == PointerShape::EndedBy; -} - -/// The place an extent term reads: a sibling parameter or a sibling field -/// of the same object. -struct ExtentPath { - enum class Root : std::uint8_t { Param, Field }; - - Root root = Root::Param; - /// `Root::Param`: the parameter's 0-based index. - std::uint32_t param = 0; - /// `Root::Field`: the field's name. - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::string field = {}; - - [[nodiscard]] static ExtentPath ofParam(std::uint32_t index); - [[nodiscard]] static ExtentPath ofField(std::string name); - - /// `param ` or `.`. - [[nodiscard]] std::string toString() const; - - friend bool operator==(const ExtentPath &, const ExtentPath &) = default; - friend std::strong_ordering operator<=>(const ExtentPath &, - const ExtentPath &) = default; -}; - -/// `scale * path + offset`, or the constant `offset` when there is no path. -/// Counted: elements; Sized: bytes; EndedBy: the end pointer's path, with -/// `offset` elements past it (`EndedBy(q + 1)`, §7.5 R3). -struct ExtentTerm { - std::optional path = std::nullopt; - std::int64_t scale = 1; - std::int64_t offset = 0; - - [[nodiscard]] static ExtentTerm constant(std::int64_t value); - [[nodiscard]] static ExtentTerm of(ExtentPath path, std::int64_t scale = 1, - std::int64_t offset = 0); - - [[nodiscard]] bool isConstant() const noexcept { return !path.has_value(); } - - /// RFC 0011: ``, or ` scale plus `. - [[nodiscard]] std::string toString() const; - /// Accepts the RFC 0011 forms and, leniently, a bare `` (scale 1, - /// plus 0). - [[nodiscard]] static std::optional parse(std::string_view text); - - /// Constant terms compare by value alone; their scale is meaningless. - friend bool operator==(const ExtentTerm &a, const ExtentTerm &b) noexcept; -}; - -struct PointerKind { - PointerShape shape = PointerShape::Unknown; - /// Meaningful only when `hasExtent(shape)`. - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - ExtentTerm extent = {}; - Nullability nullability = Nullability::Nullable; - KindSource source = KindSource::Default; - - [[nodiscard]] static PointerKind - single(Nullability nullability = Nullability::Nullable, - KindSource source = KindSource::Default); - [[nodiscard]] static PointerKind - counted(ExtentTerm count, Nullability nullability = Nullability::Nullable, - KindSource source = KindSource::Default); - [[nodiscard]] static PointerKind - sized(ExtentTerm bytes, Nullability nullability = Nullability::Nullable, - KindSource source = KindSource::Default); - [[nodiscard]] static PointerKind - endedBy(ExtentPath end, std::int64_t offset = 0, - Nullability nullability = Nullability::Nullable, - KindSource source = KindSource::Default); - [[nodiscard]] static PointerKind - nulTerminated(Nullability nullability = Nullability::Nullable, - KindSource source = KindSource::Default); - [[nodiscard]] static PointerKind - unknown(Nullability nullability = Nullability::Nullable, - KindSource source = KindSource::Default); - - /// Same shape and, where the shape has one, the same extent term. - [[nodiscard]] bool sameShape(const PointerKind &other) const noexcept; - - /// §7.1: `single`, `counted()`, `sized()`, `ended-by()`, - /// `nul-terminated` or `unknown`, then ` nonnull` or ` nullable`. The - /// source is not part of the spelling. An `ended-by` with a non-trivial - /// scale or offset spells the whole term. - [[nodiscard]] std::string toString() const; - /// Parses the spelling of `toString`; the result has source `source`. - [[nodiscard]] static std::optional - parse(std::string_view text, KindSource source = KindSource::Default); - - /// The extent term is compared only for shapes that have one. - friend bool operator==(const PointerKind &a, const PointerKind &b) noexcept; -}; - -/// What the join may assume at a merge point. -struct JoinFacts { - /// Whether `e >= 1` is proven at the join, for `Single ⊔ Counted(e)`. - /// When empty, only constant terms are judged. - std::function provesAtLeastOne = nullptr; -}; - -/// `Nullable ⊔ Nonnull = Nullable`. -[[nodiscard]] constexpr Nullability join(Nullability a, - Nullability b) noexcept { - return a == Nullability::Nonnull && b == Nullability::Nonnull - ? Nullability::Nonnull - : Nullability::Nullable; -} - -/// The weaker of two sources (Default < Inferred < Declared), so a joined -/// guarantee is declared only when both sides were. -[[nodiscard]] KindSource join(KindSource a, KindSource b) noexcept; - -/// §7.1, the merge of guarantees at a CFG merge or across stores: -/// - `x ⊔ x = x`; -/// - different shapes or extent terms join to `Unknown`, except -/// `Single ⊔ Counted(e) = Single` when `e >= 1` is proven; -/// - `Nullable ⊔ Nonnull = Nullable`. -[[nodiscard]] PointerKind join(const PointerKind &a, const PointerKind &b, - const JoinFacts &facts = {}); - -/// §7.1: what a check may compare against. An *exact* extent (the type of -/// an array or VLA object, an allocation size in scope, a surviving §7.6 -/// invariant) and a *declared* kind may be check operands; a *lower bound* -/// (Single, the A1 and A3 defaults, a §7.5 requirement inferred inside the -/// body) discharges only the accesses it covers. Only an exact extent can -/// make an access a violation. -enum class ExtentClass : std::uint8_t { Exact, Declared, LowerBound }; - -[[nodiscard]] std::string_view toString(ExtentClass extentClass) noexcept; - -/// The class of a kind by its shape and source: none for shapes without an -/// extent (`unknown`, `nul-terminated`); `LowerBound` for `single` and for -/// inferred and default kinds; `Declared` for declared kinds with an extent. -/// `Exact` is never derived from a kind alone: `KindTable` assigns it to -/// surviving §7.6 invariants, and array and allocation extents are exact by -/// construction. -[[nodiscard]] std::optional -extentClassOf(const PointerKind &kind) noexcept; - -/// Exact extents and declared kinds may be compared against by a check. -[[nodiscard]] constexpr bool isCheckOperand(ExtentClass extentClass) noexcept { - return extentClass != ExtentClass::LowerBound; -} - -} // namespace weavec::core - -#endif // WEAVEC_CORE_POINTERKIND_H diff --git a/include/weavec/Core/Scc.h b/include/weavec/Core/Scc.h deleted file mode 100644 index 60b824f1..00000000 --- a/include/weavec/Core/Scc.h +++ /dev/null @@ -1,31 +0,0 @@ -//===- Scc.h - Strongly connected components -------------------*- C++ -*-===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// Tarjan's algorithm over a small adjacency list. Used to order functions -// callees-first within a translation unit (RFC 0003) and translation units -// dependencies-first within a program (RFC 0005). -// -//===----------------------------------------------------------------------===// - -#ifndef WEAVEC_CORE_SCC_H -#define WEAVEC_CORE_SCC_H - -#include - -namespace weavec::core { - -/// The strongly connected components of the directed graph `adjacency` -/// (node `i`'s successors are `adjacency[i]`), in reverse topological order -/// of the condensation: a component comes after every component it has an -/// edge to. Members within a component are sorted. -[[nodiscard]] std::vector> stronglyConnectedComponents( - const std::vector> &adjacency); - -} // namespace weavec::core - -#endif // WEAVEC_CORE_SCC_H diff --git a/include/weavec/Core/SourceLocation.h b/include/weavec/Core/SourceLocation.h deleted file mode 100644 index 95a58eee..00000000 --- a/include/weavec/Core/SourceLocation.h +++ /dev/null @@ -1,43 +0,0 @@ -//===- SourceLocation.h - Frontend-neutral source positions ----*- C++ -*-===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// - -#ifndef WEAVEC_CORE_SOURCELOCATION_H -#define WEAVEC_CORE_SOURCELOCATION_H - -#include -#include - -namespace weavec::core { - -/// A resolved source position, independent of any particular frontend. -/// -/// The Clang integration layer converts `clang::SourceLocation` into this -/// representation before handing facts to the core model, and maps back when -/// emitting diagnostics through Clang. -struct SourceLocation { - std::string file; - std::uint32_t line = 0; - std::uint32_t column = 0; - - /// An opaque frontend-specific handle (e.g. the raw encoding of a - /// `clang::SourceLocation`) that lets the frontend recover its native - /// location precisely. Zero means "none". - std::uint64_t opaque = 0; - - [[nodiscard]] bool isValid() const noexcept { return line != 0; } - - /// Formats as `file:line:column`. - [[nodiscard]] std::string toString() const; - - friend bool operator==(const SourceLocation &, - const SourceLocation &) = default; -}; - -} // namespace weavec::core - -#endif // WEAVEC_CORE_SOURCELOCATION_H diff --git a/include/weavec/Core/Zone.h b/include/weavec/Core/Zone.h deleted file mode 100644 index b278d624..00000000 --- a/include/weavec/Core/Zone.h +++ /dev/null @@ -1,182 +0,0 @@ -//===- Zone.h - Difference-bound constraints over symbols -------*- C++ -*-===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// RFC 0031 §4.4: the object engine's numeric domain. A `Zone` holds bounds -// `x - y <= c` between integer symbols, with the reserved symbol 0 standing -// for the constant zero, so `x <= c` is `x - 0 <= c` and `x >= c` is -// `0 - x <= -c`. The matrix is kept closed, so a query is a lookup; a -// bound between two symbols is stored only where it is tighter than what -// their bounds against zero imply (`x - y <= upper(x) - lower(y)`), which a -// query adds back (§4.4 *Amendment (sparse zone)*). Bounds are 64-bit; a -// bound that would not fit is dropped, which forgets and is therefore -// sound. -// -// Joins and widenings are *paired* (§4.8): the heap decides which symbol of -// each side a result symbol stands for, and the zone combines the two -// projections. A result symbol present on one side only keeps that side's -// bounds (the other side has no such value to constrain). -// -//===----------------------------------------------------------------------===// - -#ifndef WEAVEC_CORE_ZONE_H -#define WEAVEC_CORE_ZONE_H - -#include "weavec/Core/Persistent.h" - -#include -#include -#include -#include -#include -#include - -namespace weavec::core { - -/// A symbolic value (RFC 0031 §4.1). Zero is the constant zero in a `Zone` -/// and "no symbol" elsewhere. -using Sym = std::uint32_t; -inline constexpr Sym ZeroSym = 0; - -/// What a paired combination maps each result symbol to. -struct SymPair { - Sym result = ZeroSym; - /// The left and right symbols, or `ZeroSym` when the side has none. - Sym left = ZeroSym; - Sym right = ZeroSym; - bool hasLeft = false; - bool hasRight = false; -}; - -class Zone { -public: - /// The largest number of symbols with relational bounds (§4.4); symbols - /// beyond it keep their bounds against zero only. - static constexpr std::size_t MaxRelational = 64; - /// Bounds between two symbols from here up are not kept (§4.4 - /// *Amendment (zone cost)*). - static constexpr std::int64_t LooseRelation = - static_cast(std::uint64_t{1} << 31U); - - Zone() = default; - - [[nodiscard]] bool isBottom() const noexcept { return bottom; } - void setBottom() { - bottom = true; - rows.clear(); - degrees.clear(); - } - - /// The bound on `x - y`, if any. `bound(x, x)` is 0. - [[nodiscard]] std::optional bound(Sym x, Sym y) const; - [[nodiscard]] std::optional upper(Sym x) const { - return bound(x, ZeroSym); - } - [[nodiscard]] std::optional lower(Sym x) const { - auto b = bound(ZeroSym, x); - if (!b || *b == INT64_MIN) - return std::nullopt; - return -*b; - } - [[nodiscard]] std::optional constant(Sym x) const { - auto lo = lower(x); - auto hi = upper(x); - if (lo && hi && *lo == *hi) - return lo; - return std::nullopt; - } - - /// Adds `x - y <= c` and closes; the zone becomes bottom when the - /// constraints are unsatisfiable. Returns false then. - bool addLE(Sym x, Sym y, std::int64_t c); - /// `x == y + c`. - bool addEq(Sym x, Sym y, std::int64_t c) { - return addLE(x, y, c) && addLE(y, x, c == INT64_MIN ? INT64_MAX : -c); - } - /// `lo <= x <= hi`, each optional. - bool addRange(Sym x, std::optional lo, - std::optional hi); - /// Whether `x - y <= c` holds. - [[nodiscard]] bool entails(Sym x, Sym y, std::int64_t c) const; - /// Symbols with at least one bound. - [[nodiscard]] std::vector symbols() const; - /// Keeps only the symbols `keep` accepts. - template - void restrict(Keep keep) { - restrictTo(std::function(keep)); - } - - /// The join (or, with `widen`, the widening of `left` by `right`) of two - /// zones over the result symbols of `pairs`. `thresholds` are the - /// constants a widened bound may stop at before it is dropped. - [[nodiscard]] static Zone - combine(const Zone &left, const Zone &right, - const std::vector &pairs, bool widen, - const std::vector &thresholds); - /// A copy with every symbol renamed by `rename` (symbols it maps to - /// `ZeroSym`, other than zero itself, are dropped). - template - [[nodiscard]] Zone renamed(Rename rename) const { - Zone out; - if (bottom) { - out.setBottom(); - return out; - } - for (const auto &[x, row] : rows) { - Sym rx = x == ZeroSym ? ZeroSym : rename(x); - if (x != ZeroSym && rx == ZeroSym) - continue; - for (const auto &[y, c] : row) { - Sym ry = y == ZeroSym ? ZeroSym : rename(y); - if (y != ZeroSym && ry == ZeroSym) - continue; - out.setRaw(rx, ry, c); - } - } - return out; - } - - /// `x - y <= c` rows, for dumps: `a - b <= 3, a <= 7`. - [[nodiscard]] std::string - toString(const std::function &name) const; - - /// The same bounds (not the same stored ones: a stored bound its - /// symbols' bounds against zero now imply is the same as none). - friend bool operator==(const Zone &a, const Zone &b) { return a.equals(b); } - -private: - bool bottom = false; - /// rows[x][y] = c: `x - y <= c`, closed. - PMap> rows; - /// The number of relational bounds (neither side zero) each symbol is in, - /// kept as bounds come and go: the size limit's measure. - PMap degrees; - void noteAdded(Sym x, Sym y); - void noteRemoved(Sym x, Sym y); - - void setRaw(Sym x, Sym y, std::int64_t c); - /// Replaces the rows by `entries` (`x - y <= c`, no pair twice). - void assign(std::vector> entries); - /// The stored bound on `x - y`, without what zero implies. - [[nodiscard]] std::optional stored(Sym x, Sym y) const; - /// `upper(x) - lower(y)`, when both exist and it is tighter than - /// `LooseRelation`. - [[nodiscard]] std::optional implied(Sym x, Sym y) const; - void restrictTo(const std::function &keep); - [[nodiscard]] bool equals(const Zone &other) const; - void tighten(Sym x, Sym y, std::int64_t c); - void enforceLimit(); - /// `addLE`, keeping the size limit only when `limit` (a bulk operation - /// keeps it once, at its end). - bool addLimited(Sym x, Sym y, std::int64_t c, bool limit); - /// The closure of `zone` over `symbols`: every bound re-added. - static Zone closure(const Zone &zone, const std::vector &symbols); -}; - -} // namespace weavec::core - -#endif // WEAVEC_CORE_ZONE_H diff --git a/include/weavec/Frontend/AnalysisStats.h b/include/weavec/Frontend/AnalysisStats.h deleted file mode 100644 index 3ff65097..00000000 --- a/include/weavec/Frontend/AnalysisStats.h +++ /dev/null @@ -1,19 +0,0 @@ -//===- AnalysisStats.h - Invocation statistics output ---------*- C++ -*-===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -#ifndef WEAVEC_FRONTEND_ANALYSISSTATS_H -#define WEAVEC_FRONTEND_ANALYSISSTATS_H -#include "weavec/Core/AnalysisStats.h" - -#include - -namespace weavec::frontend { -bool writeAnalysisStats(std::string_view path, const core::AnalysisStats *stats, - bool final = true); -bool writeAtomicText(std::string_view path, std::string_view text); -} // namespace weavec::frontend -#endif // WEAVEC_FRONTEND_ANALYSISSTATS_H diff --git a/include/weavec/Frontend/AnalysisSummary.h b/include/weavec/Frontend/AnalysisSummary.h deleted file mode 100644 index 16f610a6..00000000 --- a/include/weavec/Frontend/AnalysisSummary.h +++ /dev/null @@ -1,58 +0,0 @@ -//===- AnalysisSummary.h - The analysis's summary lines --------*- C++ -*-===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// RFC 0035 §8: `weavec` prints one line on stderr for every unit it -// analyses, and `weavec --whole-program` one more for the program, from -// the sites the analysis decided and the diagnostics it reported: -// -// weavec: cJSON.c: 4,210 sites: 3,050 proven, 1,130 not proven, -// 0 violations, 30 trusted; 0 errors, 2 warnings -// -// The counts follow the `-W` flags, as the diagnostics shown do. -// -//===----------------------------------------------------------------------===// - -#ifndef WEAVEC_FRONTEND_ANALYSISSUMMARY_H -#define WEAVEC_FRONTEND_ANALYSISSUMMARY_H - -#include "weavec/Core/Ledger.h" -#include "weavec/Frontend/DiagnosticControl.h" - -#include "llvm/ADT/StringRef.h" -#include "llvm/Support/raw_ostream.h" - -#include - -namespace weavec::frontend { - -/// Applies the `-W` flags to the ledger's diagnostics, so the summary line -/// counts what was reported: severities are lowered or raised, disabled -/// diagnostics are dropped, and a facet linked to a dropped one loses its -/// link. -void applyDiagnosticControl(core::Ledger &ledger, - const DiagnosticControl &control); - -/// The name the summary line gives `source`: as given when relative, else -/// relative to `workingDirectory` (the process's when empty) when inside it. -[[nodiscard]] std::string summaryName(llvm::StringRef source, - llvm::StringRef workingDirectory); - -/// Prints the unit form of the summary line of `ledger` (one unit) to `os`, -/// with its diagnostics counted after `control`. -void printUnitSummary(core::Ledger ledger, llvm::StringRef name, - const DiagnosticControl &control, - llvm::raw_ostream &os = llvm::errs()); - -/// Prints the program form, for a ledger of every unit of the program. -void printProgramSummary(core::Ledger ledger, llvm::StringRef program, - const DiagnosticControl &control, - llvm::raw_ostream &os = llvm::errs()); - -} // namespace weavec::frontend - -#endif // WEAVEC_FRONTEND_ANALYSISSUMMARY_H diff --git a/include/weavec/Frontend/ClangDiagnosticSink.h b/include/weavec/Frontend/ClangDiagnosticSink.h deleted file mode 100644 index 7564af2e..00000000 --- a/include/weavec/Frontend/ClangDiagnosticSink.h +++ /dev/null @@ -1,37 +0,0 @@ -//===- ClangDiagnosticSink.h - Forward core diagnostics to Clang -*- C++ -//-*-===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// - -#ifndef WEAVEC_FRONTEND_CLANGDIAGNOSTICSINK_H -#define WEAVEC_FRONTEND_CLANGDIAGNOSTICSINK_H - -#include "weavec/Core/Diagnostic.h" - -#include "clang/Basic/Diagnostic.h" - -namespace weavec::frontend { - -/// Emits core diagnostics through Clang's `DiagnosticsEngine`, so they get -/// the same rendering (colours, caret, `-fdiagnostics-format`, ...) as the -/// compiler's own diagnostics. -class ClangDiagnosticSink final : public core::DiagnosticSink { -public: - explicit ClangDiagnosticSink(clang::DiagnosticsEngine &diags) - : engine(diags) {} - - void report(const core::Diagnostic &diagnostic) override; - -private: - clang::DiagnosticsEngine &engine; - - void emit(const core::Diagnostic &diagnostic, bool isNote); -}; - -} // namespace weavec::frontend - -#endif // WEAVEC_FRONTEND_CLANGDIAGNOSTICSINK_H diff --git a/include/weavec/Frontend/DiagnosticControl.h b/include/weavec/Frontend/DiagnosticControl.h deleted file mode 100644 index 6368ab15..00000000 --- a/include/weavec/Frontend/DiagnosticControl.h +++ /dev/null @@ -1,151 +0,0 @@ -//===- DiagnosticControl.h - -W flags for WeaveC diagnostics ---*- C++ -*-===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// Compiler-style control over WeaveC's diagnostics (RFC 0005, *Flags*): -// -// -Wweavec- re-enable a warning, or enable an id that is off -// by default (RFC 0030: `allocation-failure`) -// -Wno-weavec- disable the diagnostics of `id` whose default -// severity is a warning -// -Werror=weavec- make it an error (enabling an off-by-default id) -// -Wno-error=weavec- make an error a warning -// -Werror=weavec / -Wno-error=weavec / -Wweavec / -Wno-weavec -// the same for every WeaveC id; of these only -// -Wweavec enables the off-by-default ids -// -// An error cannot be disabled outright; it can be lowered to a warning, -// and then disabled. The default severity depends on the diagnostic's -// certainty (`core::diag::defaultSeverity`): `-Wno-weavec-use-after-free` -// drops the possible (warning) findings and leaves the definite errors, and -// is refused only for an id that is always an error. A flag naming a -// removed id is refused with `(removed by RFC 00NN)`. -// -// Also here: the key under which an emitted diagnostic is remembered so a -// unit `weavec --whole-program` runs again does not print it twice. -// -//===----------------------------------------------------------------------===// - -#ifndef WEAVEC_FRONTEND_DIAGNOSTICCONTROL_H -#define WEAVEC_FRONTEND_DIAGNOSTICCONTROL_H - -#include "weavec/Core/Diagnostic.h" - -#include "llvm/ADT/StringRef.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace weavec::frontend { - -/// Severity overrides for WeaveC diagnostics. -class DiagnosticControl { -public: - enum class Level : std::uint8_t { - Default, - Off, - Warning, - Error, - }; - - /// True if `flag` is one of the spellings above. When it is but the id is - /// unknown or removed, or the request is not allowed - /// (`-Wno-weavec-null-dereference`), the flag is still consumed and - /// `error` explains the problem. - bool parse(llvm::StringRef flag, std::string &error); - - /// True if `flag` looks like one of WeaveC's `-W` spellings. - [[nodiscard]] static bool isWeaveCFlag(llvm::StringRef flag); - - /// The diagnostic as it should be emitted, or `nullopt` to drop it. - [[nodiscard]] std::optional - apply(const core::Diagnostic &diagnostic) const; - - [[nodiscard]] Level levelFor(std::string_view id) const; - - /// RFC 0033 §8: makes `id` off unless a flag enables it (`-Wweavec-`, - /// `-Wweavec`), as `allocation-failure` is everywhere; `weavec-cc` does - /// this for `leak`. - void turnOffByDefault(std::string_view id) { offByDefault.emplace(id); } - - friend bool operator==(const DiagnosticControl &, - const DiagnosticControl &) = default; - -private: - /// Whether the flags turned `id` on, for an id that is off by default. - [[nodiscard]] bool enabledByFlags(std::string_view id) const; - - Level all = Level::Default; - std::map> perId; - /// The off-by-default ids: `-Wweavec` turns them all on and `-Wno-weavec` - /// all off, forgetting earlier per-id flags as they do for the levels; - /// `-Wweavec-`, `-Werror=weavec-` and `-Wno-weavec-` decide - /// the one they name. - bool enableAll = false; - std::map> enabledIds; - std::set> offByDefault; - /// The ids `-Wno-weavec-` disabled after they were lowered to - /// warnings: their errors are dropped too. - std::set> silenced; -}; - -/// Where a diagnostic was emitted, for deduplication between the runs of -/// one unit in `weavec --whole-program` (RFC 0005). -struct ReportedDiagnostic { - std::string id; - std::string file; - std::uint32_t line = 0; - std::uint32_t column = 0; - - [[nodiscard]] static ReportedDiagnostic of(const core::Diagnostic &d) { - return ReportedDiagnostic{.id = std::string(d.id), - .file = d.location.file, - .line = d.location.line, - .column = d.location.column}; - } - - friend bool operator==(const ReportedDiagnostic &, - const ReportedDiagnostic &) = default; - friend std::strong_ordering operator<=>(const ReportedDiagnostic &, - const ReportedDiagnostic &) = default; -}; - -/// A sink that applies a `DiagnosticControl`, drops diagnostics already in -/// `alreadyReported`, remembers what it forwarded, and forwards the rest. -class FilteringSink final : public core::DiagnosticSink { -public: - FilteringSink(core::DiagnosticSink &next, DiagnosticControl control, - const std::set *alreadyReported = nullptr) - : downstream(next), table(std::move(control)), skip(alreadyReported) {} - - void report(const core::Diagnostic &diagnostic) override; - - /// Keys of every diagnostic forwarded so far. - [[nodiscard]] const std::set &reported() const noexcept { - return forwarded; - } - [[nodiscard]] std::size_t errors() const noexcept { return errorCount; } - [[nodiscard]] std::size_t warnings() const noexcept { return warningCount; } - -private: - core::DiagnosticSink &downstream; - DiagnosticControl table; - const std::set *skip; - std::set forwarded; - std::size_t errorCount = 0; - std::size_t warningCount = 0; -}; - -} // namespace weavec::frontend - -#endif // WEAVEC_FRONTEND_DIAGNOSTICCONTROL_H diff --git a/include/weavec/Frontend/Driver.h b/include/weavec/Frontend/Driver.h index 665a6f05..57bf87c2 100644 --- a/include/weavec/Frontend/Driver.h +++ b/include/weavec/Frontend/Driver.h @@ -6,7 +6,8 @@ // //===----------------------------------------------------------------------===// // -// `weavec-cc` is Clang's driver with WeaveC inside (RFC 0035 §1, §7): +// `weavec-cc` is Clang's driver with WeaveC inside (RFC 0035 §1, §7, RFC 0036 +// §1, §7): // // weavec-cc -c foo.c -o foo.o compile foo.c with every memory access // guarded (the guard pass), locals @@ -14,23 +15,18 @@ // bounds-checked // weavec-cc foo.o bar.o -o prog link, with the WeaveC runtime // -// The `-fweavec-*` and `-W*weavec*` flags are WeaveC's; everything else is -// Clang's. Each `-cc1` job runs in this process. `-fweavec-diagnose` adds the -// advisory analysis (RFC 0035 §8), whose findings are warnings. +// The `-fweavec-*` flags are WeaveC's; everything else is Clang's. Each `-cc1` +// job runs in this process. // //===----------------------------------------------------------------------===// #ifndef WEAVEC_FRONTEND_DRIVER_H #define WEAVEC_FRONTEND_DRIVER_H -#include "weavec/Core/AnalysisStats.h" -#include "weavec/Frontend/DiagnosticControl.h" - #include "llvm/ADT/ArrayRef.h" #include "llvm/ADT/StringRef.h" #include -#include #include #include #include @@ -46,29 +42,14 @@ struct DriverOptions { Checks checks = Checks::Trap; /// `-f[no-]weavec-zero-init`; unset means on. std::optional zeroInit; - /// `-fweavec-diagnose`: run the advisory analysis. - bool diagnose = false; - /// `-fweavec-dump-analysis`, with `-fweavec-diagnose`. - bool dumpAnalysis = false; - /// `-fweavec-budget=`, `-fweavec-unit-budget=`: the analysis's - /// budgets (`-fweavec-diagnose`); unset means the defaults. - std::optional budget; - std::optional unitBudget; + /// `-f[no-]weavec-asan-compat` (RFC 0036 §1): parse with + /// AddressSanitizer's language options, so the unit sees ASan's macros. + bool asanCompat = true; /// `-fweavec-ledger=` (RFC 0035 §9). std::string ledger; /// `-f[no-]weavec-summary`; unset means on exactly when a ledger is /// written. std::optional summary; - /// `-fweavec-analysis-stats=`. - std::string analysisStatsPath; - std::shared_ptr stats; - /// The `-W` flags of the analysis's diagnostics; leaks are off unless - /// asked for. - DiagnosticControl control = [] { - DiagnosticControl leakOff; - leakOff.turnOffByDefault(core::diag::Leak); - return leakOff; - }(); /// The flags consumed, in order, for forwarding to `-cc1` jobs. std::vector spellings; @@ -95,8 +76,7 @@ struct DriverOptions { int runDriver(llvm::ArrayRef argv, void *mainAddress); /// Runs a `-cc1` command line (`argv` excludes `-cc1`): Clang's compiler -/// proper with WeaveC's guard pass, unsafe-region collector and, under -/// `-fweavec-diagnose`, analysis. +/// proper with WeaveC's guard passes and unsafe-region collector. int runCc1(llvm::ArrayRef argv, const char *argv0); } // namespace weavec::frontend diff --git a/include/weavec/Frontend/FrontendAction.h b/include/weavec/Frontend/FrontendAction.h deleted file mode 100644 index 3fffccc7..00000000 --- a/include/weavec/Frontend/FrontendAction.h +++ /dev/null @@ -1,130 +0,0 @@ -//===- FrontendAction.h - Clang frontend integration -----------*- C++ -*-===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// Glue between Clang's frontend/tooling machinery and the WeaveC analyses. -// `WeaveCAction` can be run by `clang::tooling::ClangTool`, embedded in a -// custom driver (`weavec-cc` multiplexes `createWeaveCConsumer` with -// Clang's own consumer), or (eventually) loaded as a plugin. -// -//===----------------------------------------------------------------------===// - -#ifndef WEAVEC_FRONTEND_FRONTENDACTION_H -#define WEAVEC_FRONTEND_FRONTENDACTION_H - -#include "weavec/Analysis/ProgramDatabase.h" -#include "weavec/Analysis/SafetyEngine.h" -#include "weavec/Core/Ledger.h" -#include "weavec/Frontend/DiagnosticControl.h" -#include "weavec/Frontend/InterfaceFacts.h" - -#include "clang/AST/ASTConsumer.h" -#include "clang/Frontend/ASTUnit.h" -#include "clang/Frontend/CompilerInstance.h" -#include "clang/Frontend/FrontendAction.h" -#include "clang/Tooling/Tooling.h" - -#include -#include -#include -#include -#include - -namespace weavec::frontend { - -/// What one run of the consumer over a unit produced (RFC 0005). -struct UnitResult { - analysis::UnitExports exports; - /// The diagnostics shown for the unit in this run. - std::set reported; - std::size_t errors = 0; - std::size_t warnings = 0; - /// Nothing was reported: the run asked a context `FrontendOptions::holdFor` - /// holds for. - bool held = false; - /// The unit's sites and their outcomes (RFC 0035 §8); null for a silent - /// or discovery run. - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::shared_ptr ledger = {}; - /// The unit's interface facts, under `FrontendOptions::collectInterface`; - /// after a discovery run only the function-pointer slots. - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::shared_ptr interface = {}; -}; - -struct FrontendOptions; -UnitResult analyzeTranslationUnit(clang::ASTContext &context, - clang::DiagnosticsEngine &diagnostics, - const FrontendOptions &options); -/// Runs against an ASTUnit with a fresh diagnostic consumer and counters. -UnitResult analyzeRetainedUnit(clang::ASTUnit &ast, - const FrontendOptions &options); - -/// User-configurable behaviour of the frontend action. -struct FrontendOptions { - /// The engine's options: the dump stream, statistics, the budget, - /// zero-initialisation and strict aliasing. - analysis::EngineOptions engine; - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::string analysisStatsPath = {}; - /// `-W` overrides applied before a diagnostic reaches Clang. - DiagnosticControl control; - /// Print the unit's summary line (`AnalysisSummary.h`) on stderr after a - /// reporting run: always in `weavec`, never under `-fweavec-diagnose`. - bool summary = false; - - // RFC 0005, whole-program analysis. Every pointer must outlive the run. - - /// Exports of the other units of the program; null when the unit is the - /// whole program. - const analysis::ProgramDatabase *database = nullptr; - /// Diagnostics not to show again (already shown by an earlier run). - const std::set *alreadyReported = nullptr; - /// Analyse but report nothing (a fixpoint round). - bool silent = false; - /// RFC 0031 §7 *Amendment (cross-unit contexts)*: when set, a run that - /// asks a context this accepts (one another unit is yet to serve) reports - /// nothing and publishes no ledger, and says so in `UnitResult::held`; - /// the run after the context is served reports. - std::function holdFor; - /// Collect the unit's definitions, imports and indirect types without - /// analysing anything; `onResult` receives exports with empty summaries. - bool discoverOnly = false; - /// Also collect the unit's interface facts for `weavec --whole-program` - /// (`UnitResult::interface`). - bool collectInterface = false; - /// Receives the unit's exports and reporting statistics. - std::function onResult; -}; - -/// The AST consumer that runs WeaveC over a translation unit, for embedding -/// in another action (`weavec-cc` runs it beside Clang's code generator). -std::unique_ptr -createWeaveCConsumer(clang::CompilerInstance &compiler, - const FrontendOptions &options); - -/// Runs WeaveC's analyses over each translation unit. -class WeaveCAction final : public clang::ASTFrontendAction { -public: - explicit WeaveCAction(FrontendOptions opts) : options(std::move(opts)) {} - -protected: - std::unique_ptr - CreateASTConsumer(clang::CompilerInstance &compiler, - llvm::StringRef inFile) override; - -private: - FrontendOptions options; -}; - -/// Creates a factory usable with `clang::tooling::ClangTool::run`. -std::unique_ptr -createWeaveCActionFactory(FrontendOptions options); - -} // namespace weavec::frontend - -#endif // WEAVEC_FRONTEND_FRONTENDACTION_H diff --git a/include/weavec/Frontend/InterfaceFacts.h b/include/weavec/Frontend/InterfaceFacts.h deleted file mode 100644 index e6bbc121..00000000 --- a/include/weavec/Frontend/InterfaceFacts.h +++ /dev/null @@ -1,154 +0,0 @@ -//===- InterfaceFacts.h - A unit's interface facts -------------*- C++ -*-===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// What `weavec --whole-program` checks across units beyond the summaries -// (RFC 0030 §13.2, RFC 0035 §8), from the components that run before the -// engine and depend on no engine fact: -// -// - `AttributeReader` and `KindInference` (§7.2–§7.5) give the kinds of -// the parameters of the unit's definitions and the declared kinds of its -// imports; -// - the ownership annotations of the imports' declarations (RFC 0003); -// - `SlotCollector` (§9.3) gives the function-pointer slot constraints -// with local slots eliminated and the unit's closed-slot rules. -// -// §9.4's boundary rows come from `BoundaryInvariants`, which runs after the -// engine, through `UnitExports`. The facts stay in memory: there are no -// unit records. -// -//===----------------------------------------------------------------------===// - -#ifndef WEAVEC_FRONTEND_INTERFACEFACTS_H -#define WEAVEC_FRONTEND_INTERFACEFACTS_H - -#include "weavec/Analysis/KindInference.h" -#include "weavec/Analysis/ProgramDatabase.h" -#include "weavec/Core/FnSlots.h" -#include "weavec/Core/LibrarySpec.h" -#include "weavec/Core/PointerKind.h" -#include "weavec/Core/SourceLocation.h" - -#include "clang/AST/ASTContext.h" - -#include -#include -#include -#include -#include -#include - -namespace weavec::frontend { - -/// A kind with its source, spelled ` `: `declared counted(param -/// 1 scale 1 plus 0) nonnull`, `default single nullable`. -[[nodiscard]] std::string spellKind(const core::PointerKind &kind); -[[nodiscard]] std::optional parseKind(std::string_view text); - -/// What the program check knows about one definition beyond its summary. -struct FunctionInterface { - /// Per parameter: `spellKind` of its kind, or none for a non-pointer. - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::vector> params = {}; - /// The definition, for the note `defined here`. - std::optional location = std::nullopt; - - friend bool operator==(const FunctionInterface &, - const FunctionInterface &) = default; -}; - -/// What the unit's declarations of an external function state. Ownership -/// annotations are spelled as the macros are (`WEAVEC_BORROWED`), kinds by -/// `PointerKind::toString`. -struct DeclaredParam { - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::string name = {}; - std::optional kind = std::nullopt; - std::optional ownership = std::nullopt; - - friend bool operator==(const DeclaredParam &, - const DeclaredParam &) = default; -}; -struct DeclaredInterface { - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::vector params = {}; - /// The declared result kind and ownership annotation. - std::optional result = std::nullopt; - std::optional ownership = std::nullopt; - - /// Whether any annotation or kind is declared at all. - [[nodiscard]] bool empty() const noexcept; - friend bool operator==(const DeclaredInterface &, - const DeclaredInterface &) = default; -}; - -struct ImportInterface { - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - DeclaredInterface declared = {}; - /// The declaration a contradiction is reported at (the one with - /// annotations). - std::optional location = std::nullopt; - - friend bool operator==(const ImportInterface &, - const ImportInterface &) = default; -}; - -/// The unit's function-pointer slot constraints with local slots -/// eliminated, and its inputs to §9.3's closed-slot rules (functions named -/// as `SlotCollector` names them). -struct SlotFacts { - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::vector rows = {}; - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::string unit = {}; - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::set defined = {}; - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::set exported = {}; - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::set confinedRecords = {}; - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::set escapedStatics = {}; - - friend bool operator==(const SlotFacts &, const SlotFacts &) = default; -}; - -/// Everything the program check verifies about a unit beyond its summaries. -struct InterfaceFacts { - /// By the name `UnitExports::functions` uses. - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::map functions = {}; - /// By the name `UnitExports::imports` uses. - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::map imports = {}; - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - SlotFacts slots = {}; - /// `BoundaryRow::unit` is empty here. - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::vector boundaries = {}; - - friend bool operator==(const InterfaceFacts &, - const InterfaceFacts &) = default; -}; - -struct FactsInput { - /// The unit's exports: which functions and imports to describe. - const analysis::UnitExports &exports; - /// The kinds the unit's analysis used; null to build them again. - const analysis::UnitKinds *kinds = nullptr; -}; - -/// The interface facts of the unit `context` holds. -[[nodiscard]] InterfaceFacts collectInterfaceFacts(clang::ASTContext &context, - const FactsInput &input); -/// Only the function-pointer slots, for a discovery run: what the -/// whole-program driver solves before any unit is analysed. -[[nodiscard]] InterfaceFacts collectSlotFacts(clang::ASTContext &context); - -} // namespace weavec::frontend - -#endif // WEAVEC_FRONTEND_INTERFACEFACTS_H diff --git a/include/weavec/Frontend/ProgramAnalysis.h b/include/weavec/Frontend/ProgramAnalysis.h deleted file mode 100644 index 06b07440..00000000 --- a/include/weavec/Frontend/ProgramAnalysis.h +++ /dev/null @@ -1,225 +0,0 @@ -//===- ProgramAnalysis.h - Whole-program driver ----------------*- C++ -*-===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// Analyses a program of several translation units so that callers see the -// summaries of callees defined in other units (RFC 0005, *The whole-program -// algorithm*): discover what each unit defines and imports, order the units -// dependencies-first by strongly connected component, analyse acyclic units -// once and cyclic groups to a fixpoint, each against a database of the -// exports analysed so far. -// -// The orchestrator does not know how a unit is parsed: `ProgramUnit` runs a -// frontend action over one unit, from a compilation database (`weavec -// --whole-program`) or from memory (the unit tests). -// -//===----------------------------------------------------------------------===// - -#ifndef WEAVEC_FRONTEND_PROGRAMANALYSIS_H -#define WEAVEC_FRONTEND_PROGRAMANALYSIS_H - -#include "weavec/Analysis/ProgramDatabase.h" -#include "weavec/Core/Ledger.h" -#include "weavec/Frontend/DiagnosticControl.h" -#include "weavec/Frontend/FrontendAction.h" -#include "weavec/Frontend/InterfaceFacts.h" - -#include "clang/Frontend/ASTUnit.h" -#include "clang/Tooling/Tooling.h" - -#include -#include -#include -#include -#include -#include - -namespace weavec::frontend { - -/// One translation unit of a program. -class ProgramUnit { -public: - virtual ~ProgramUnit() = default; - - /// The unit's name for messages, usually its main source file. - [[nodiscard]] virtual std::string name() const = 0; - - /// Parses the unit and runs the action `factory` creates over it. False - /// if the unit could not be processed (the reason has been printed). - /// Diagnostics the action emits do not make this false. - virtual bool run(clang::tooling::FrontendActionFactory &factory) = 0; - /// RFC 0020: production units retain their AST; synthetic units may use - /// the original action interface. - virtual bool analyze(const FrontendOptions &options) { - const auto factory = createWeaveCActionFactory(options); - return run(*factory); - } - /// Release its AST after analysis returns. The orchestrator - /// only requests this when no analysis dump is being written. - virtual bool releaseAST() { return false; } -}; - -/// The whole-program algorithm over an arbitrary set of units. -class ProgramAnalysis { -public: - struct Result { - /// Units that could not be parsed or re-run, by name. - std::vector failed; - /// Cyclic components that did not settle within `MaxRounds`, as lists - /// of unit names: their widened summaries stand (RFC 0033 §7). - std::vector> nonConverging; - /// WeaveC errors and warnings printed. - std::size_t errors = 0; - std::size_t warnings = 0; - - /// A group that did not converge keeps its widened summaries; a unit - /// that cannot be parsed or an error fails the analysis. - [[nodiscard]] bool ok() const noexcept { - return failed.empty() && errors == 0; - } - }; - - /// `opts.database`, `alreadyReported`, `silent`, - /// `discoverOnly` and `onResult` are managed by the analysis; the rest - /// apply to every unit. - explicit ProgramAnalysis(FrontendOptions opts); - - /// Adds a unit to analyse; `run` parses it once to discover its exports. - void addUnit(std::unique_ptr unit); - - [[nodiscard]] Result run(); - - /// After `run`: the exports of every unit, joined. - [[nodiscard]] const analysis::ProgramDatabase &database() const noexcept { - return settled; - } - - // RFC 0030 §13.2 (begin). - - /// `weavec --whole-program`: collect every unit's interface facts, keep - /// every unit's last ledger, and solve the program's slots from what - /// discovery collected (the program facts every run sees through its - /// database, `ProgramDatabase::programFacts`). The program is an - /// executable (its slots closed, §9.3) when some unit defines `main`. - void collectInterfaces(bool collect) { interfaces = collect; } - - /// After `run`, for the unit added `index`-th: the ledger of its last - /// reporting run, its interface facts and exports (null when it was not - /// analysed). - [[nodiscard]] const core::Ledger *ledgerOf(std::size_t index) const; - [[nodiscard]] const InterfaceFacts *interfaceOf(std::size_t index) const; - [[nodiscard]] const analysis::UnitExports *exportsOf(std::size_t index) const; - [[nodiscard]] std::size_t unitCount() const noexcept { return units.size(); } - [[nodiscard]] std::string unitName(std::size_t index) const; - - // RFC 0030 §13.2 (end). - - /// Upper bound on fixpoint rounds for a cyclic component. - static constexpr unsigned MaxRounds = 16; - /// RFC 0011, *Whole-program widening*: from this round on a member's new - /// exports are joined with its previous ones before comparison, so the - /// sequence is monotone in the finite summary lattice and settles. - static constexpr unsigned WidenAfter = 6; - /// The widening step: joins each of `exports`' function summaries with - /// the same function's summary in a member's `previous` exports (when - /// both number globals alike). - static void widen(analysis::UnitExports &exports, - const analysis::UnitExports &previous); - -private: - struct Unit { - std::unique_ptr unit; - // NOLINTNEXTLINE(readability-redundant-member-init) - std::optional exports = {}; - /// The diagnostics its runs have shown. - // NOLINTNEXTLINE(readability-redundant-member-init) - std::set reported = {}; - /// The last run was held for a context (`FrontendOptions::holdFor`): - /// the unit has not reported. - bool held = false; - /// RFC 0030 §13.2: the ledger of the last reporting run and the - /// interface facts collected (at discovery, then by the last reporting - /// run). - // NOLINTNEXTLINE(readability-redundant-member-init) - std::shared_ptr ledger = {}; - // NOLINTNEXTLINE(readability-redundant-member-init) - std::shared_ptr interface = {}; - }; - - FrontendOptions options; - std::vector units; - analysis::ProgramDatabase settled; - /// RFC 0031 §7: the context requests whose definers have run for them. - std::set attempted; - std::shared_ptr programFacts; - bool interfaces = false; - /// The units' summary lines, printed after the last run. - bool unitSummaries = false; - /// `weavec --whole-program`: the program facts from what discovery - /// collected. - void solveDiscoveredSlots(); - bool boundedRetention = false; - std::vector retainedUnits; - void touchRetainedUnit(ProgramUnit &unit); - void trimRetainedUnits(); - - /// Runs `unit` with `options` completed by `overrides`; the result of the - /// consumer, or `nullopt` if the unit could not be processed. - std::optional runUnit(ProgramUnit &unit, - const FrontendOptions &overrides); - [[nodiscard]] std::vector> unitGraph() const; - void analyzeAcyclic(unsigned index, Result &result); - void analyzeCyclic(const std::vector &component, Result &result); - void analyzeComponent(const std::vector &component, Result &result); - /// RFC 0031 §7 *Amendment (cross-unit contexts)*: runs the units that - /// define what other units asked contexts of, then the units the served - /// contexts change, until every request is served; then every unit still - /// held reports. - void serveContexts(Result &result); - /// Whether a unit this analysis runs defines `portable` (a function's - /// portable name). - [[nodiscard]] bool runsDefinitionOf(const std::string &portable) const; - /// The hold of a reporting run: a context asked of a unit this analysis - /// runs, not yet served. - [[nodiscard]] std::function - holdForUnserved() const; - /// Records what a reporting run of `unit` produced: its exports and the - /// diagnostics shown. - void settle(Unit &unit, UnitResult run) const; - /// `settled` plus the exports of a cyclic component's members. - [[nodiscard]] analysis::ProgramDatabase - databaseFor(const std::vector &members) const; -}; - -/// A unit parsed by `ClangTool` from a compilation database, as `weavec -/// --whole-program` does. -class CompilationDatabaseUnit final : public ProgramUnit { -public: - CompilationDatabaseUnit( - const clang::tooling::CompilationDatabase &compilations, - std::string sourcePath, - std::vector adjusters) - : compilations(compilations), source(std::move(sourcePath)), - adjusters(std::move(adjusters)) {} - - [[nodiscard]] std::string name() const override { return source; } - bool run(clang::tooling::FrontendActionFactory &factory) override; - bool analyze(const FrontendOptions &options) override; - bool releaseAST() override; - -private: - const clang::tooling::CompilationDatabase &compilations; - std::string source; - std::vector adjusters; - std::vector> asts; - bool attemptedParse = false; - bool multipleCommands = false; -}; - -} // namespace weavec::frontend - -#endif // WEAVEC_FRONTEND_PROGRAMANALYSIS_H diff --git a/include/weavec/Frontend/ProgramChecks.h b/include/weavec/Frontend/ProgramChecks.h deleted file mode 100644 index 242cdf68..00000000 --- a/include/weavec/Frontend/ProgramChecks.h +++ /dev/null @@ -1,102 +0,0 @@ -//===- ProgramChecks.h - What a whole program checks -----------*- C++ -*-===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// RFC 0030 §13.2 as RFC 0035 §8 keeps it: the parts of `weavec -// --whole-program` that work on every unit's interface facts at once. -// -// - `solveProgramSlots`: every member's function-pointer slot constraints -// solved together under the program's closed-slot rules (§9.3); -// - `programBoundaries`: every member's boundary rows, which reach the -// engine's runs through `analysis::ProgramFacts`; -// - `verifyDeclarations`: every import with declared annotations or kinds -// compared with the defining member's summary and kinds; a -// contradiction is an `annotation-mismatch` error at the declaration. -// -//===----------------------------------------------------------------------===// - -#ifndef WEAVEC_FRONTEND_PROGRAMCHECKS_H -#define WEAVEC_FRONTEND_PROGRAMCHECKS_H - -#include "weavec/Analysis/ProgramDatabase.h" -#include "weavec/Core/Diagnostic.h" -#include "weavec/Core/FnSlots.h" -#include "weavec/Frontend/InterfaceFacts.h" - -#include "llvm/ADT/StringRef.h" -#include "llvm/Support/raw_ostream.h" - -#include -#include -#include -#include - -namespace weavec::frontend { - -/// One unit of the program, as its last run left it. -struct ProgramMember { - /// The main source as the unit's compile command named it. - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::string source = {}; - /// The compile command's working directory: relative paths in the facts - /// are relative to it. - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::string cwd = {}; - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - analysis::UnitExports exports = {}; - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - InterfaceFacts facts = {}; -}; - -/// `path`, taken against `base` when relative, relative to `cwd` when -/// inside it, else absolute. -[[nodiscard]] std::string -displayPath(llvm::StringRef path, llvm::StringRef base, llvm::StringRef cwd); - -/// Every member's slots solved together (§9.3). The slots of an executable -/// (a program that defines `main`) are closed. -[[nodiscard]] core::SlotSolution -solveProgramSlots(std::span members, bool executable); - -/// Every member's boundary rows, each with its unit. -[[nodiscard]] std::vector -programBoundaries(std::span members); - -/// The program's slots, with their targets and whether they are closed, -/// for `--dump-analysis` in `weavec --whole-program` (unstable format). -void dumpProgramSlots(const core::SlotSolution &slots, llvm::raw_ostream &os); - -/// The `annotation-mismatch` errors at the declarations a definition -/// contradicts, each with the note `defined here`, in member and import -/// order. `cwd` is the working directory against which the messages name -/// files. -[[nodiscard]] std::vector -verifyDeclarations(std::span members, llvm::StringRef cwd); - -/// Prints the program's own diagnostics on stderr: one with a location as a -/// compile does (`file:line:column: error: ...` and the source line, the -/// file read on first use), the others after `: `. -class ProgramDiagnosticPrinter final : public core::DiagnosticSink { -public: - explicit ProgramDiagnosticPrinter(std::string prefix); - ~ProgramDiagnosticPrinter() override; - ProgramDiagnosticPrinter(const ProgramDiagnosticPrinter &) = delete; - ProgramDiagnosticPrinter & - operator=(const ProgramDiagnosticPrinter &) = delete; - ProgramDiagnosticPrinter(ProgramDiagnosticPrinter &&) = delete; - ProgramDiagnosticPrinter &operator=(ProgramDiagnosticPrinter &&) = delete; - - void report(const core::Diagnostic &diagnostic) override; - -private: - struct Engines; - std::unique_ptr engines; -}; - -} // namespace weavec::frontend - -#endif // WEAVEC_FRONTEND_PROGRAMCHECKS_H diff --git a/include/weavec/Frontend/UnsafeRegions.h b/include/weavec/Frontend/UnsafeRegions.h index 18149acd..6fe76ce0 100644 --- a/include/weavec/Frontend/UnsafeRegions.h +++ b/include/weavec/Frontend/UnsafeRegions.h @@ -32,6 +32,10 @@ class CompilerInstance; namespace weavec::frontend { +/// The annotation `WEAVEC_UNSAFE` expands to (`resources/include/weavec.h` +/// spells the same). +inline constexpr llvm::StringLiteral UnsafeAnnotation = "weavec.unsafe"; + /// The unsafe ranges of one unit, by file. class UnsafeRegions { public: diff --git a/lib/Analysis/Annotations.cpp b/lib/Analysis/Annotations.cpp deleted file mode 100644 index 3d988f99..00000000 --- a/lib/Analysis/Annotations.cpp +++ /dev/null @@ -1,358 +0,0 @@ -//===- Annotations.cpp - WeaveC source annotations ------------------------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// - -#include "weavec/Analysis/Annotations.h" - -#include "weavec/Core/LibrarySpec.h" - -#include "clang/AST/ASTContext.h" -#include "clang/AST/TypeLoc.h" -#include "clang/Basic/SourceManager.h" - -#include "llvm/ADT/STLExtras.h" -#include "llvm/ADT/StringExtras.h" -#include "llvm/Support/Casting.h" - -#include - -namespace weavec::analysis { - -std::optional parseAnnotation(llvm::StringRef text) { - if (!text.starts_with(spelling::Prefix)) - return std::nullopt; - if (text == spelling::Owned) - return Annotation::Owned; - if (text == spelling::Borrowed) - return Annotation::Borrowed; - if (text == spelling::MutBorrowed) - return Annotation::MutBorrowed; - if (text == spelling::Raw) - return Annotation::Raw; - if (text == spelling::Unsafe) - return Annotation::Unsafe; - if (text == spelling::Nullable) - return Annotation::Nullable; - if (text == spelling::NonNull) - return Annotation::NonNull; - if (text == spelling::Retains) - return Annotation::Retains; - if (text == spelling::Releases) - return Annotation::Releases; - if (text == spelling::Refcount) - return Annotation::Refcount; - if (text == spelling::Assume) - return Annotation::Assume; - if (text == spelling::String) - return Annotation::String; - if (text.starts_with(spelling::FamilyPrefix)) { - const llvm::StringRef family = - text.drop_front(spelling::FamilyPrefix.size()); - // A family is an identifier: what `SummaryIO` accepts in `freed(f)`. - const bool wellFormed = !family.empty() && llvm::all_of(family, [](char c) { - return llvm::isAlnum(c) || c == '_'; - }); - return wellFormed ? Annotation::Family : Annotation::Invalid; - } - // `weavec.sized_by.`, `weavec.counted_by.`, `weavec.ended_by.`: - // the name is an identifier (the macro stringises its argument). - for (const auto &[prefix, annotation] : - {std::pair{spelling::SizedByPrefix, Annotation::SizedBy}, - std::pair{spelling::CountedByPrefix, Annotation::CountedBy}, - std::pair{spelling::EndedByPrefix, Annotation::EndedBy}}) { - if (!text.starts_with(prefix)) - continue; - const llvm::StringRef name = text.drop_front(prefix.size()); - const bool wellFormed = - !name.empty() && !llvm::isDigit(name.front()) && - llvm::all_of(name, [](char c) { return llvm::isAlnum(c) || c == '_'; }); - return wellFormed ? annotation : Annotation::Invalid; - } - return Annotation::Invalid; -} - -/// The name after `prefix` in `text`, or empty for any other payload. -static llvm::StringRef nameAfter(llvm::StringRef text, llvm::StringRef prefix) { - if (!text.starts_with(prefix)) - return {}; - return text.drop_front(prefix.size()); -} - -static void apply(AnnotationSet &set, Annotation annotation) { - switch (annotation) { - case Annotation::Owned: - set.owned = true; - break; - case Annotation::Borrowed: - set.borrowed = true; - break; - case Annotation::MutBorrowed: - set.mutBorrowed = true; - break; - case Annotation::Raw: - set.raw = true; - break; - case Annotation::Unsafe: - set.unsafe = true; - break; - case Annotation::Nullable: - set.nullable = true; - break; - case Annotation::NonNull: - set.nonNull = true; - break; - case Annotation::Retains: - set.retains = true; - break; - case Annotation::Releases: - set.releases = true; - break; - case Annotation::Refcount: - set.refcount = true; - break; - case Annotation::Assume: - set.assume = true; - break; - case Annotation::String: - set.string = true; - break; - case Annotation::Family: - case Annotation::SizedBy: - case Annotation::CountedBy: - case Annotation::EndedBy: - // The name itself is applied by the caller, which has the payload. - break; - case Annotation::Invalid: - set.invalid = true; - break; - } -} - -/// Sets the family, or marks the set invalid when two different ones meet. -static void applyFamily(AnnotationSet &set, llvm::StringRef family) { - if (family.empty()) - return; - if (set.family.empty()) - set.family = family.str(); - else if (set.family != family) - set.invalid = true; -} - -/// Sets the name an extent annotation carries (`sizedBy`, `countedBy` or -/// `endedBy`), or marks the set invalid when two differ. -static void applyName(AnnotationSet &set, std::string &slot, - llvm::StringRef name) { - if (name.empty()) - return; - if (slot.empty()) - slot = name.str(); - else if (slot != name) - set.invalid = true; -} - -void AnnotationSet::merge(const AnnotationSet &other) { - owned = owned || other.owned; - borrowed = borrowed || other.borrowed; - mutBorrowed = mutBorrowed || other.mutBorrowed; - raw = raw || other.raw; - unsafe = unsafe || other.unsafe; - nullable = nullable || other.nullable; - nonNull = nonNull || other.nonNull; - retains = retains || other.retains; - releases = releases || other.releases; - refcount = refcount || other.refcount; - assume = assume || other.assume; - string = string || other.string; - frees = frees || other.frees; - invalid = invalid || other.invalid; - applyFamily(*this, other.family); - applyName(*this, sizedBy, other.sizedBy); - applyName(*this, countedBy, other.countedBy); - applyName(*this, endedBy, other.endedBy); -} - -AnnotationSet getAnnotations(const clang::Decl &decl) { - AnnotationSet set; - for (const auto *attr : decl.specific_attrs()) { - const auto parsed = parseAnnotation(attr->getAnnotation()); - if (!parsed) - continue; - apply(set, *parsed); - const llvm::StringRef text = attr->getAnnotation(); - if (*parsed == Annotation::Family) - applyFamily(set, nameAfter(text, spelling::FamilyPrefix)); - if (*parsed == Annotation::SizedBy) - applyName(set, set.sizedBy, nameAfter(text, spelling::SizedByPrefix)); - if (*parsed == Annotation::CountedBy) - applyName(set, set.countedBy, nameAfter(text, spelling::CountedByPrefix)); - if (*parsed == Annotation::EndedBy) - applyName(set, set.endedBy, nameAfter(text, spelling::EndedByPrefix)); - } - return set; -} - -bool isUnsafeBlock(const clang::Stmt &stmt) { - const auto *attributed = llvm::dyn_cast(&stmt); - if (attributed == nullptr) - return false; - return llvm::any_of(attributed->getAttrs(), [](const clang::Attr *attr) { - const auto *annotate = llvm::dyn_cast(attr); - return annotate != nullptr && - parseAnnotation(annotate->getAnnotation()) == Annotation::Unsafe; - }); -} - -const char *macroSpelling(const AnnotationSet &set) noexcept { - if (set.owned) - return "WEAVEC_OWNED"; - if (set.borrowed) - return "WEAVEC_BORROWED"; - if (set.mutBorrowed) - return "WEAVEC_MUT"; - if (set.raw) - return "WEAVEC_RAW"; - if (set.retains) - return "WEAVEC_RETAINS"; - if (set.releases) - return "WEAVEC_RELEASES"; - return nullptr; -} - -// -- Function-pointer types --------------------------------------------------- - -bool FunctionTypeAnnotations::anyOwnership() const noexcept { - return result.ownership() || llvm::any_of(params, [](const AnnotationSet &s) { - return s.ownership(); - }); -} - -/// Walks from a declarator's written type down to the prototype it names, -/// through pointers, parens, attributes and typedefs. Annotations on a -/// typedef passed through describe the function's result, since a typedef -/// of a function-pointer type has no other pointer to describe. -static void walkToPrototype(clang::TypeLoc loc, FunctionTypeAnnotations &out, - unsigned depth) { - // Typedef chains are finite, but be defensive against odd ASTs. - if (depth > 16) - return; - while (!loc.isNull()) { - if (const auto proto = loc.getAs()) { - out.prototype = proto.getTypePtr(); - out.params.assign(proto.getNumParams(), AnnotationSet{}); - for (unsigned i = 0; i < proto.getNumParams(); ++i) { - if (const clang::ParmVarDecl *param = proto.getParam(i)) - out.params[i].merge(getAnnotations(*param)); - } - return; - } - if (const auto typedefLoc = loc.getAs()) { - const clang::TypedefNameDecl *decl = typedefLoc.getTypePtr()->getDecl(); - if (decl == nullptr) - return; - out.result.merge(getAnnotations(*decl)); - const clang::TypeSourceInfo *info = decl->getTypeSourceInfo(); - if (info == nullptr) - return; - walkToPrototype(info->getTypeLoc(), out, depth + 1); - return; - } - // Every other wrapper (pointer, parens, attributes, qualifiers, arrays of - // callbacks, elaboration) has exactly one inner type location; a leaf - // yields a null one and ends the walk. - loc = loc.getNextTypeLoc(); - } -} - -FunctionTypeAnnotations -collectFunctionTypeAnnotations(const clang::Decl &decl) { - FunctionTypeAnnotations out; - const clang::TypeSourceInfo *info = nullptr; - if (const auto *declarator = llvm::dyn_cast(&decl)) { - info = declarator->getTypeSourceInfo(); - } else if (const auto *typedefDecl = - llvm::dyn_cast(&decl)) { - info = typedefDecl->getTypeSourceInfo(); - } - if (info == nullptr) - return out; - // Ownership annotations on the declarator itself describe the result. - out.result.merge(getAnnotations(decl)); - walkToPrototype(info->getTypeLoc(), out, 0); - if (out.prototype == nullptr) { - out.result = AnnotationSet{}; - out.params.clear(); - } - return out; -} - -bool SignatureAnnotations::anyOwnership() const noexcept { - return result.ownership() || llvm::any_of(params, [](const AnnotationSet &s) { - return s.ownership(); - }); -} - -/// RFC 0030 §7.2: `malloc` and the `ownership_*` attributes outside system -/// headers are ownership contracts: a fresh result of family `m`, and an -/// argument released (`ownership_takes`) or retained (`ownership_holds`). A -/// WeaveC annotation on the same position wins (precedence level 1). -static void applyOwnershipAttributes(const clang::FunctionDecl &redecl, - SignatureAnnotations &collected) { - const clang::SourceManager &sm = redecl.getASTContext().getSourceManager(); - if (sm.isInSystemHeader(redecl.getLocation())) - return; - const auto owns = [](const AnnotationSet &set) { - return set.owned || set.borrowed || set.mutBorrowed || set.raw || set.frees; - }; - if (redecl.hasAttr() && - redecl.getReturnType()->isPointerType() && !owns(collected.result)) { - collected.result.owned = true; - collected.result.family = std::string(core::HeapFamily); - } - for (const auto *attr : redecl.specific_attrs()) { - const std::string family = attr->getModule() != nullptr - ? attr->getModule()->getName().str() - : std::string(); - if (attr->getOwnKind() == clang::OwnershipAttr::Returns) { - if (!owns(collected.result)) { - collected.result.owned = true; - collected.result.family = family; - } - continue; - } - for (const clang::ParamIdx index : attr->args()) { - if (!index.isValid() || index.getASTIndex() >= collected.params.size()) - continue; - AnnotationSet ¶m = collected.params[index.getASTIndex()]; - if (owns(param) || param.retains || param.releases) - continue; - if (attr->getOwnKind() == clang::OwnershipAttr::Holds) { - param.retains = true; - } else { - param.frees = true; - param.family = family; - } - } - } -} - -SignatureAnnotations collectAnnotations(const clang::FunctionDecl &function) { - SignatureAnnotations collected; - collected.params.resize(function.getNumParams()); - for (const clang::FunctionDecl *redecl : function.redecls()) { - const AnnotationSet onFunction = getAnnotations(*redecl); - collected.result.merge(onFunction); - collected.unsafe = collected.unsafe || onFunction.unsafe; - for (unsigned i = 0; - i < redecl->getNumParams() && i < collected.params.size(); ++i) - collected.params[i].merge(getAnnotations(*redecl->getParamDecl(i))); - } - for (const clang::FunctionDecl *redecl : function.redecls()) - applyOwnershipAttributes(*redecl, collected); - return collected; -} - -} // namespace weavec::analysis diff --git a/lib/Analysis/AttributeReader.cpp b/lib/Analysis/AttributeReader.cpp deleted file mode 100644 index dce376bc..00000000 --- a/lib/Analysis/AttributeReader.cpp +++ /dev/null @@ -1,783 +0,0 @@ -//===- AttributeReader.cpp - Declared pointer kinds (RFC 0030) ------------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// - -#include "weavec/Analysis/AttributeReader.h" - -#include "weavec/Analysis/Annotations.h" - -#include "clang/AST/Attr.h" -#include "clang/AST/Expr.h" -#include "clang/AST/RecursiveASTVisitor.h" -#include "clang/AST/Type.h" -#include "clang/Basic/SourceManager.h" - -#include "llvm/ADT/DenseSet.h" -#include "llvm/ADT/STLExtras.h" -#include "llvm/Support/CheckedArithmetic.h" - -#include -#include -#include -#include -#include - -namespace weavec::analysis { - -namespace { - -/// `k * + c`, or the constant `c`. -struct LinearTerm { - std::optional path; - std::int64_t scale = 1; - std::int64_t offset = 0; -}; - -using PathOf = - std::function(const clang::ValueDecl &)>; - -/// What the name in a `WEAVEC_*` extent annotation resolves to (§7.2): a -/// sibling parameter or field, in any position. -struct Sibling { - core::ExtentPath path; - bool integer = false; - bool pointer = false; -}; -using SiblingResolver = std::function(llvm::StringRef)>; - -/// What one precedence level says about one position, over every -/// redeclaration. -struct LevelFacts { - std::optional shape; - std::optional nullability; - std::optional extentFactor; -}; - -/// Everything the levels say about one position (a parameter, a result, a -/// field or a variable), and its resolution by precedence (§7.2). -class PositionFacts { -public: - /// `declared` names the declaration in RFC 0008's contradiction message - /// (the function, for its result); it defaults to `name`. - PositionFacts(KindTable &kindTable, std::string name, - std::string declared = {}) - : table(kindTable), subject(std::move(name)), - declaredName(declared.empty() ? subject : std::move(declared)) {} - - void addShape(KindLevel level, core::PointerKind shape, - clang::SourceLocation at, - std::optional factor = std::nullopt) { - shape.nullability = core::Nullability::Nullable; - shape.source = core::KindSource::Declared; - LevelFacts &facts = levels.at(static_cast(level)); - if (!facts.shape) { - facts.shape = std::move(shape); - facts.extentFactor = factor; - return; - } - if (facts.shape->sameShape(shape) && facts.extentFactor == factor) - return; - table.addProblem(KindProblem{ - .location = at, - .message = "conflicting kinds for '" + subject + - "': " + facts.shape->toString() + " and " + shape.toString(), - }); - // §7.2: the weaker kind is used. - facts.shape = core::join(*facts.shape, shape); - facts.shape->source = core::KindSource::Declared; - facts.extentFactor = std::nullopt; - } - - void addNullability(KindLevel level, core::Nullability nullability, - clang::SourceLocation at) { - LevelFacts &facts = levels.at(static_cast(level)); - if (!facts.nullability) { - facts.nullability = nullability; - return; - } - if (*facts.nullability == nullability) - return; - // RFC 0008's wording for the two WeaveC annotations on one declaration. - table.addProblem(KindProblem{ - .location = at, - .message = level == KindLevel::Annotation - ? "'" + declaredName + - "' is declared both WEAVEC_NULLABLE and " - "WEAVEC_NONNULL" - : "conflicting kinds for '" + subject + - "': nonnull and nullable", - }); - facts.nullability = core::Nullability::Nullable; - } - - /// The entry, or none when no level said anything. With - /// `libraryGoverns`, a `LibrarySpec` entry (level 3) outranks the - /// system-header attributes (level 4), which are dropped. - [[nodiscard]] std::optional resolve(bool libraryGoverns) const { - KindEntry entry; - entry.kind = core::PointerKind::unknown(core::Nullability::Nullable, - core::KindSource::Declared); - for (std::size_t i = 0; i < KindLevelCount; ++i) { - const auto level = static_cast(i); - if (libraryGoverns && level == KindLevel::SystemHeader) - break; - const LevelFacts &facts = levels.at(i); - if (!entry.shapeLevel && facts.shape) { - const core::Nullability kept = entry.kind.nullability; - entry.kind = *facts.shape; - entry.kind.nullability = kept; - entry.shapeLevel = level; - entry.extentFactor = facts.extentFactor; - } - if (!entry.nullabilityLevel && facts.nullability) { - entry.kind.nullability = *facts.nullability; - entry.nullabilityLevel = level; - } - } - if (!entry.shapeLevel && !entry.nullabilityLevel) - return std::nullopt; - entry.kind.source = core::KindSource::Declared; - entry.extentClass = core::extentClassOf(entry.kind); - return entry; - } - -private: - std::array levels{}; - KindTable &table; - std::string subject; - std::string declaredName; -}; - -/// Every function, field and variable declaration of the unit. -class DeclarationVisitor - : public clang::RecursiveASTVisitor { -public: - DeclarationVisitor(AttributeReader &attributeReader, KindTable &kindTable) - : reader(attributeReader), table(kindTable) {} - - // RecursiveASTVisitor's CRTP hooks are found by name. - // NOLINTNEXTLINE(readability-identifier-naming,bugprone-derived-method-shadowing-base-method) - bool VisitFunctionDecl(clang::FunctionDecl *function) { - const clang::FunctionDecl *canonical = function->getCanonicalDecl(); - if (!seen.insert(canonical).second) - return true; - // A declaration nothing calls or defines has no site that could read - // its kinds. - if (!function->isReferenced() && !function->isDefined()) - return true; - reader.readFunction(*function, table); - return true; - } - - // NOLINTNEXTLINE(readability-identifier-naming,bugprone-derived-method-shadowing-base-method) - bool VisitFieldDecl(clang::FieldDecl *field) { - reader.readField(*field, table); - return true; - } - - // NOLINTNEXTLINE(readability-identifier-naming,bugprone-derived-method-shadowing-base-method) - bool VisitVarDecl(clang::VarDecl *variable) { - if (!llvm::isa(variable)) - reader.readVariable(*variable, table); - return true; - } - -private: - AttributeReader &reader; - KindTable &table; - llvm::DenseSet seen; -}; - -} // namespace - -/// The linear form of an extent expression over sibling parameters or -/// fields: constants, one path, `+`, `-` a constant, `*` a constant. -static std::optional linearTermOf(const clang::Expr *expr, - const clang::ASTContext &context, - const PathOf &pathOf, - unsigned depth = 0) { - if (expr == nullptr || depth > 8) - return std::nullopt; - expr = expr->IgnoreParenImpCasts(); - if (const auto value = expr->getIntegerConstantExpr(context)) { - if (const auto fits = value->tryExtValue()) - return LinearTerm{.path = std::nullopt, .scale = 1, .offset = *fits}; - return std::nullopt; - } - const clang::ValueDecl *named = nullptr; - if (const auto *ref = llvm::dyn_cast(expr)) - named = ref->getDecl(); - else if (const auto *member = llvm::dyn_cast(expr)) - named = member->getMemberDecl(); - if (named != nullptr) { - if (auto path = pathOf(*named)) - return LinearTerm{.path = std::move(path), .scale = 1, .offset = 0}; - return std::nullopt; - } - const auto *binary = llvm::dyn_cast(expr); - if (binary == nullptr) - return std::nullopt; - const auto lhs = linearTermOf(binary->getLHS(), context, pathOf, depth + 1); - const auto rhs = linearTermOf(binary->getRHS(), context, pathOf, depth + 1); - if (!lhs || !rhs) - return std::nullopt; - switch (binary->getOpcode()) { - case clang::BO_Add: { - if (lhs->path && rhs->path) - return std::nullopt; - const auto offset = llvm::checkedAdd(lhs->offset, rhs->offset); - if (!offset) - return std::nullopt; - const LinearTerm &withPath = lhs->path ? *lhs : *rhs; - return LinearTerm{ - .path = withPath.path, .scale = withPath.scale, .offset = *offset}; - } - case clang::BO_Sub: { - if (rhs->path) - return std::nullopt; - const auto offset = llvm::checkedSub(lhs->offset, rhs->offset); - if (!offset) - return std::nullopt; - return LinearTerm{ - .path = lhs->path, .scale = lhs->scale, .offset = *offset}; - } - case clang::BO_Mul: { - if (lhs->path && rhs->path) - return std::nullopt; - const LinearTerm &variable = lhs->path ? *lhs : *rhs; - const std::int64_t factor = lhs->path ? rhs->offset : lhs->offset; - const auto scale = llvm::checkedMul(variable.scale, factor); - const auto offset = llvm::checkedMul(variable.offset, factor); - if (!scale || !offset) - return std::nullopt; - return LinearTerm{ - .path = variable.path, .scale = *scale, .offset = *offset}; - } - default: - return std::nullopt; - } -} - -static core::ExtentTerm toExtentTerm(const LinearTerm &term) { - if (!term.path) - return core::ExtentTerm::constant(term.offset); - return core::ExtentTerm::of(*term.path, term.scale, term.offset); -} - -/// `void` and character pointees count bytes (§7.2, `WEAVEC_SIZED_BY`). -static bool countsBytes(clang::QualType pointer) { - if (!pointer->isPointerType()) - return false; - const clang::QualType pointee = pointer->getPointeeType(); - return pointee->isVoidType() || pointee->isCharType(); -} - -static KindLevel ecosystemLevel(const clang::Decl &decl, - const clang::SourceManager &sm) { - return sm.isInSystemHeader(decl.getLocation()) ? KindLevel::SystemHeader - : KindLevel::Ecosystem; -} - -/// `_Nonnull` / `_Nullable` on a written type. -static std::optional typeNullability(clang::QualType type) { - const auto nullability = type->getNullability(); - if (!nullability) - return std::nullopt; - switch (*nullability) { - case clang::NullabilityKind::NonNull: - return core::Nullability::Nonnull; - case clang::NullabilityKind::Nullable: - case clang::NullabilityKind::NullableResult: - return core::Nullability::Nullable; - case clang::NullabilityKind::Unspecified: - return std::nullopt; - } - return std::nullopt; -} - -/// `counted_by` and friends on a written type: the shape over `pathOf`, and -/// the nullability they imply. -static std::optional> -countAttributedKind(clang::QualType type, const clang::ASTContext &context, - const PathOf &pathOf) { - const auto *counted = type->getAs(); - if (counted == nullptr) - return std::nullopt; - const auto term = linearTermOf(counted->getCountExpr(), context, pathOf); - if (!term) - return std::nullopt; - const core::ExtentTerm extent = toExtentTerm(*term); - core::PointerKind kind = counted->isCountInBytes() - ? core::PointerKind::sized(extent) - : core::PointerKind::counted(extent); - return std::pair{std::move(kind), counted->isOrNull() - ? core::Nullability::Nullable - : core::Nullability::Nonnull}; -} - -/// The macro that spells an extent annotation, for messages. -static llvm::StringRef macroOf(Annotation annotation) { - switch (annotation) { - case Annotation::SizedBy: - return "WEAVEC_SIZED_BY"; - case Annotation::CountedBy: - return "WEAVEC_COUNTED_BY"; - case Annotation::EndedBy: - return "WEAVEC_ENDED_BY"; - default: - return "WEAVEC_STRING"; - } -} - -/// The name an extent annotation carries (empty for `weavec.string`). -static llvm::StringRef nameOf(Annotation annotation, llvm::StringRef text) { - switch (annotation) { - case Annotation::SizedBy: - return text.drop_front(spelling::SizedByPrefix.size()); - case Annotation::CountedBy: - return text.drop_front(spelling::CountedByPrefix.size()); - case Annotation::EndedBy: - return text.drop_front(spelling::EndedByPrefix.size()); - default: - return {}; - } -} - -namespace { -/// Where an annotation is written, for the §7.2 "applies to" column. -enum class Position : std::uint8_t { Parameter, Field, Result, Variable }; -} // namespace - -/// WEAVEC_* annotations on one declaration (level 1): nullability, and the -/// extent annotations (§7.2), whose names `resolve` looks up among the -/// siblings. A result takes only `WEAVEC_STRING`; a variable takes only -/// nullability. -static void readAnnotations(const clang::Decl &decl, clang::QualType type, - const SiblingResolver &resolve, - PositionFacts &facts, KindTable &table, - Position position) { - const AnnotationSet annotations = getAnnotations(decl); - const clang::SourceLocation at = decl.getLocation(); - const auto *named = llvm::dyn_cast(&decl); - const std::string subject = - position == Position::Result - ? "the result of '" + - (named != nullptr ? named->getNameAsString() : std::string()) + - "'" - : "'" + - (named != nullptr ? named->getNameAsString() : std::string()) + - "'"; - const auto problem = [&](std::string message) { - table.addProblem( - KindProblem{.location = at, .message = std::move(message)}); - }; - if (annotations.nonNull) - facts.addNullability(KindLevel::Annotation, core::Nullability::Nonnull, at); - if (annotations.nullable) - facts.addNullability(KindLevel::Annotation, core::Nullability::Nullable, - at); - if (!annotations.extent()) - return; - // Every extent annotation written on this declaration. An inherited one - // was read on the declaration that wrote it, where its name resolves - // against that declaration's parameters (§7.2: by name). - std::vector> written; - for (const auto *attr : decl.specific_attrs()) { - if (attr->isInherited()) - continue; - const llvm::StringRef text = attr->getAnnotation(); - const auto parsed = parseAnnotation(text); - if (parsed != Annotation::SizedBy && parsed != Annotation::CountedBy && - parsed != Annotation::EndedBy && parsed != Annotation::String) - continue; - std::pair entry{*parsed, nameOf(*parsed, text).str()}; - if (!llvm::is_contained(written, entry)) - written.push_back(std::move(entry)); - } - for (const auto &[annotation, name] : written) { - std::string spelled = macroOf(annotation).str(); - if (!name.empty()) - spelled += "(" + name + ")"; - if (!type->isPointerType()) { - std::string message = subject; - message += " is declared "; - message += spelled; - message += " but is not a pointer"; - problem(std::move(message)); - continue; - } - if (position == Position::Variable || - (position == Position::Result && annotation != Annotation::String)) { - std::string message = subject; - message += " is declared "; - message += spelled; - message += annotation == Annotation::String - ? " but only parameters, fields and results take it" - : " but only parameters and fields take it"; - problem(std::move(message)); - continue; - } - if (annotation == Annotation::String) { - facts.addShape(KindLevel::Annotation, core::PointerKind::nulTerminated(), - at); - continue; - } - const std::optional sibling = resolve(name); - if (!sibling) { - problem("'" + name + "' in " + macroOf(annotation).str() + - " does not name a parameter or field"); - continue; - } - if (annotation == Annotation::EndedBy) { - if (!sibling->pointer) { - problem("'" + name + - "' in WEAVEC_ENDED_BY is not a pointer parameter or field"); - continue; - } - facts.addShape(KindLevel::Annotation, - core::PointerKind::endedBy(sibling->path), at); - continue; - } - if (!sibling->integer) { - problem("'" + name + "' in " + macroOf(annotation).str() + - " is not an integer parameter or field"); - continue; - } - // §7.2: the two macros are synonyms, counting elements, and bytes for - // `void` and character pointees. - const core::ExtentTerm extent = core::ExtentTerm::of(sibling->path); - facts.addShape(KindLevel::Annotation, - countsBytes(type) ? core::PointerKind::sized(extent) - : core::PointerKind::counted(extent), - at); - } -} - -/// The sibling parameter of `function` named `wanted`, in any position. -static std::optional -siblingParameter(const clang::FunctionDecl &function, llvm::StringRef wanted) { - for (unsigned i = 0; i < function.getNumParams(); ++i) { - const clang::ParmVarDecl *param = function.getParamDecl(i); - if (param->getName() != wanted || wanted.empty()) - continue; - const clang::QualType type = param->getType(); - return Sibling{.path = core::ExtentPath::ofParam(i), - .integer = type->isIntegerType(), - .pointer = type->isPointerType()}; - } - return std::nullopt; -} - -/// §7.2: `T p[N]` without `static` is no requirement; it yields a fix-it -/// suggestion that inserts `static` after the `[`. -static void suggestStatic(const clang::ParmVarDecl ¶m, - const clang::ConstantArrayType &array, - const clang::ASTContext &context, KindTable &table) { - const clang::TypeSourceInfo *info = param.getTypeSourceInfo(); - if (info == nullptr || param.getName().empty()) - return; - const auto loc = info->getTypeLoc().getAsAdjusted(); - if (loc.isNull() || !loc.getLBracketLoc().isFileID()) - return; - const std::string count = std::to_string(array.getSize().getZExtValue()); - const std::string name = param.getNameAsString(); - table.addSuggestion(KindSuggestion{ - .location = loc.getLBracketLoc().getLocWithOffset(1), - .message = "'" + name + "' is declared with " + count + - " elements, which C does not require of callers; declare " - "it '" + - name + "[static " + count + "]' to require them", - .insert = "static ", - }); - (void)context; -} - -/// `T p[static N]` and VLA parameters `T p[n]` (level 2 or 4). With -/// `suggest`, a constant bound without `static` yields a suggestion. -static void readArrayParameter(const clang::ParmVarDecl ¶m, - const clang::ASTContext &context, - const PathOf &pathOf, KindLevel level, - PositionFacts &facts, KindTable &table, - bool suggest) { - const clang::ArrayType *array = - context.getAsArrayType(param.getOriginalType()); - if (array == nullptr) - return; - const bool isStatic = - array->getSizeModifier() == clang::ArraySizeModifier::Static; - std::optional count; - if (const auto *constant = llvm::dyn_cast(array)) { - // §7.2: a constant bound without `static` is no requirement, because C - // gives it none and code commonly passes fewer elements. - if (!isStatic) { - if (suggest && level != KindLevel::SystemHeader) - suggestStatic(param, *constant, context, table); - return; - } - if (const auto value = constant->getSize().tryZExtValue(); - value && *value <= static_cast( - std::numeric_limits::max())) - count = LinearTerm{.path = std::nullopt, - .scale = 1, - .offset = static_cast(*value)}; - } else if (const auto *variable = - llvm::dyn_cast(array)) { - if (array->getSizeModifier() == clang::ArraySizeModifier::Star) - return; - count = linearTermOf(variable->getSizeExpr(), context, pathOf); - } - const clang::SourceLocation at = param.getLocation(); - if (count) - facts.addShape(level, core::PointerKind::counted(toExtentTerm(*count)), at); - if (isStatic) - facts.addNullability(level, core::Nullability::Nonnull, at); -} - -/// §7.2: `malloc` and the `ownership_*` attributes of every declaration of -/// `function`: level 4 when all of them sit in system headers. -static OwnershipContract ownershipContract(const clang::FunctionDecl &function, - const clang::SourceManager &sm) { - OwnershipContract contract; - bool outsideSystem = false; - for (const clang::FunctionDecl *redecl : function.redecls()) { - const bool system = sm.isInSystemHeader(redecl->getLocation()); - if (redecl->hasAttr() && - redecl->getReturnType()->isPointerType()) { - contract.freshResult = - contract.freshResult.value_or(std::string(core::HeapFamily)); - outsideSystem = outsideSystem || !system; - } - for (const auto *attr : redecl->specific_attrs()) { - outsideSystem = outsideSystem || !system; - const std::string family = attr->getModule() != nullptr - ? attr->getModule()->getName().str() - : std::string(); - if (attr->getOwnKind() == clang::OwnershipAttr::Returns) { - contract.freshResult = family; - continue; - } - for (const clang::ParamIdx index : attr->args()) { - if (!index.isValid()) - continue; - OwnershipContract::Argument argument{ - .index = index.getASTIndex(), - .family = family, - .retains = attr->getOwnKind() == clang::OwnershipAttr::Holds}; - if (!llvm::is_contained(contract.arguments, argument)) - contract.arguments.push_back(std::move(argument)); - } - } - } - contract.level = - outsideSystem ? KindLevel::Ecosystem : KindLevel::SystemHeader; - return contract; -} - -void AttributeReader::readFunction(const clang::FunctionDecl &function, - KindTable &table) { - const clang::SourceManager &sm = context.getSourceManager(); - const clang::FunctionDecl &canonical = *function.getCanonicalDecl(); - const bool libraryGoverns = - governingLibraryEntry(canonical, library).has_value(); - if (libraryGoverns) - table.setGovernedByLibrary(canonical); - const unsigned count = canonical.getNumParams(); - const std::string name = canonical.getNameAsString(); - - std::vector params; - params.reserve(count); - for (unsigned i = 0; i < count; ++i) { - std::string paramName = canonical.getParamDecl(i)->getNameAsString(); - for (const clang::FunctionDecl *redecl : canonical.redecls()) - if (paramName.empty() && i < redecl->getNumParams()) - paramName = redecl->getParamDecl(i)->getNameAsString(); - params.emplace_back(table, paramName.empty() - ? "parameter " + std::to_string(i + 1) + - " of '" + name + "'" - : paramName); - } - PositionFacts result(table, "result of '" + name + "'", name); - // §7.2 suggestions go on the definition, or on the first declaration. - const clang::FunctionDecl *suggestOn = canonical.getDefinition(); - if (suggestOn == nullptr) - suggestOn = &canonical; - - for (const clang::FunctionDecl *redecl : canonical.redecls()) { - const KindLevel level = ecosystemLevel(*redecl, sm); - const PathOf paramPath = - [redecl]( - const clang::ValueDecl &decl) -> std::optional { - const auto *param = llvm::dyn_cast(&decl); - if (param == nullptr || !param->getType()->isIntegerType()) - return std::nullopt; - for (unsigned i = 0; i < redecl->getNumParams(); ++i) - if (redecl->getParamDecl(i) == param) - return core::ExtentPath::ofParam(i); - return std::nullopt; - }; - const SiblingResolver byName = - [redecl](llvm::StringRef wanted) -> std::optional { - return siblingParameter(*redecl, wanted); - }; - - // Function-level `nonnull` (all pointer parameters, or the listed ones). - std::vector nonnullParams(count, false); - for (const auto *attr : redecl->specific_attrs()) { - if (attr->args_size() == 0) { - nonnullParams.assign(count, true); - continue; - } - for (const clang::ParamIdx index : attr->args()) - if (index.isValid() && index.getASTIndex() < count) - nonnullParams[index.getASTIndex()] = true; - } - - const unsigned declared = std::min(count, redecl->getNumParams()); - for (unsigned i = 0; i < declared; ++i) { - const clang::ParmVarDecl ¶m = *redecl->getParamDecl(i); - PositionFacts &facts = params[i]; - const clang::QualType type = param.getType(); - readAnnotations(param, type, byName, facts, table, Position::Parameter); - const clang::SourceLocation at = param.getLocation(); - if (type->isPointerType() && - (nonnullParams[i] || param.hasAttr())) - facts.addNullability(level, core::Nullability::Nonnull, at); - if (const auto nullability = typeNullability(type)) - facts.addNullability(level, *nullability, at); - if (auto counted = countAttributedKind(type, context, paramPath)) { - facts.addShape(level, std::move(counted->first), at); - facts.addNullability(level, counted->second, at); - } - readArrayParameter(param, context, paramPath, level, facts, table, - /*suggest=*/redecl == suggestOn); - } - - // The result: nullability, and `WEAVEC_STRING` (§7.2). The function's - // own name is no sibling of its result. - const clang::SourceLocation at = redecl->getLocation(); - readAnnotations( - *redecl, redecl->getReturnType(), - [](llvm::StringRef) -> std::optional { return std::nullopt; }, - result, table, Position::Result); - if (redecl->getReturnType()->isPointerType()) { - if (redecl->hasAttr()) - result.addNullability(level, core::Nullability::Nonnull, at); - if (const auto nullability = typeNullability(redecl->getReturnType())) - result.addNullability(level, *nullability, at); - if (const auto *allocSize = redecl->getAttr()) { - const clang::ParamIdx size = allocSize->getElemSizeParam(); - const clang::ParamIdx elements = allocSize->getNumElemsParam(); - if (size.isValid() && size.getASTIndex() < count) { - std::optional factor; - if (elements.isValid() && elements.getASTIndex() < count) - factor = elements.getASTIndex(); - result.addShape(level, - core::PointerKind::sized(core::ExtentTerm::of( - core::ExtentPath::ofParam(size.getASTIndex()))), - at, factor); - } - } - } - } - - for (unsigned i = 0; i < count; ++i) - if (auto entry = params[i].resolve(libraryGoverns)) - table.setParam(canonical, i, std::move(*entry)); - if (auto entry = result.resolve(libraryGoverns)) - table.setResult(canonical, std::move(*entry)); - // A `LibrarySpec` row (level 3) outranks system-header contracts. - if (auto contract = ownershipContract(canonical, sm); - !contract.empty() && - (!libraryGoverns || contract.level != KindLevel::SystemHeader)) - table.setOwnership(canonical, std::move(contract)); -} - -void AttributeReader::readField(const clang::FieldDecl &field, - KindTable &table) { - const clang::QualType type = field.getType(); - if (!type->isPointerType() && !type->isArrayType()) { - // Only malformed annotations to report: an extent on a non-pointer. - if (const AnnotationSet set = getAnnotations(field); set.extent()) { - PositionFacts ignored(table, field.getNameAsString()); - readAnnotations( - field, type, - [](llvm::StringRef) -> std::optional { - return std::nullopt; - }, - ignored, table, Position::Field); - } - return; - } - const clang::RecordDecl *record = field.getParent(); - const SiblingResolver byName = - [record, &field](llvm::StringRef wanted) -> std::optional { - if (record == nullptr || wanted.empty()) - return std::nullopt; - for (const clang::FieldDecl *sibling : record->fields()) { - if (sibling == &field || sibling->getName() != wanted) - continue; - const clang::QualType type = sibling->getType(); - return Sibling{.path = core::ExtentPath::ofField(wanted.str()), - .integer = type->isIntegerType(), - .pointer = type->isPointerType()}; - } - return std::nullopt; - }; - const PathOf fieldPath = - [&field]( - const clang::ValueDecl &decl) -> std::optional { - const auto *sibling = llvm::dyn_cast(&decl); - if (sibling == nullptr || sibling == &field || - !sibling->getType()->isIntegerType()) - return std::nullopt; - return core::ExtentPath::ofField(sibling->getNameAsString()); - }; - PositionFacts facts(table, field.getNameAsString()); - readAnnotations(field, type, byName, facts, table, Position::Field); - const KindLevel level = ecosystemLevel(field, context.getSourceManager()); - const clang::SourceLocation at = field.getLocation(); - if (auto counted = countAttributedKind(type, context, fieldPath)) { - facts.addShape(level, std::move(counted->first), at); - // A counted flexible array member has no nullability. - if (type->isPointerType()) - facts.addNullability(level, counted->second, at); - } - if (type->isPointerType()) - if (const auto nullability = typeNullability(type)) - facts.addNullability(level, *nullability, at); - if (auto entry = facts.resolve(/*libraryGoverns=*/false)) - table.setField(field, std::move(*entry)); -} - -void AttributeReader::readVariable(const clang::VarDecl &variable, - KindTable &table) { - const clang::QualType type = variable.getType(); - const AnnotationSet annotations = getAnnotations(variable); - if (!type->isPointerType() && !annotations.extent()) - return; - PositionFacts facts(table, variable.getNameAsString()); - readAnnotations( - variable, type, - [](llvm::StringRef) -> std::optional { return std::nullopt; }, - facts, table, Position::Variable); - if (!type->isPointerType()) - return; - if (const auto nullability = typeNullability(type)) - facts.addNullability(ecosystemLevel(variable, context.getSourceManager()), - *nullability, variable.getLocation()); - if (auto entry = facts.resolve(/*libraryGoverns=*/false)) - table.setVariable(variable, std::move(*entry)); -} - -KindTable AttributeReader::read() { - KindTable table; - DeclarationVisitor visitor(*this, table); - visitor.TraverseDecl(context.getTranslationUnitDecl()); - return table; -} - -} // namespace weavec::analysis diff --git a/lib/Analysis/BoundaryInvariants.cpp b/lib/Analysis/BoundaryInvariants.cpp deleted file mode 100644 index c919a2a5..00000000 --- a/lib/Analysis/BoundaryInvariants.cpp +++ /dev/null @@ -1,205 +0,0 @@ -//===- BoundaryInvariants.cpp - The §9.4 boundary invariants --------------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// - -#include "weavec/Analysis/BoundaryInvariants.h" - -#include "clang/AST/Decl.h" -#include "clang/AST/Expr.h" - -#include "llvm/ADT/StringSet.h" - -#include -#include -#include -#include -#include - -using namespace clang; - -namespace weavec::analysis { - -/// §9.4: the class of the object a pointer of type `type` designates: the -/// key of the record it points to, or none for any other pointee. A field -/// class `struct s.buf` breaks this class too, because reaching the field -/// goes through the object. -static std::string objectClassOf(QualType type) { - const auto *pointer = type->getAs(); - if (pointer == nullptr) - return {}; - const RecordDecl *record = pointer->getPointeeType()->getAsRecordDecl(); - if (record == nullptr || record->getNameAsString().empty()) - return {}; - return record->getKindName().str() + " " + record->getNameAsString(); -} - -/// §9.4: the class of the place a site's operand was loaded from, spelled -/// as the engine spells it: the nearest enclosing field (`struct s.buf`), -/// else the name of the global the operand is rooted in. The operand is the -/// pointer the site's facets are about, so this is the place whose entry -/// assumption a proof of its temporal facet rests on. -static std::string classOfOperand(const Expr *operand) { - for (const Expr *at = operand; at != nullptr;) { - at = at->IgnoreParenImpCasts(); - if (const auto *member = dyn_cast(at)) { - const auto *field = dyn_cast(member->getMemberDecl()); - if (field == nullptr) - return {}; - const RecordDecl *record = field->getParent(); - const std::string name = record->getNameAsString(); - const std::string what = field->getNameAsString(); - if (name.empty() || what.empty()) - return {}; - std::string spelled = record->getKindName().str(); - spelled += ' '; - spelled += name; - spelled += '.'; - spelled += what; - return spelled; - } - if (const auto *reference = dyn_cast(at)) { - const auto *var = dyn_cast(reference->getDecl()); - if (var != nullptr && var->hasGlobalStorage()) - return var->getNameAsString(); - // A pointer held in a local or a parameter names no class of its own. - // What a load through it rests on is the invariant of the *object* it - // designates, which the record the boundary broke a field of names. - return objectClassOf(at->getType()); - } - // The pointer a subscript or a dereference reads is held one step up. - if (const auto *subscript = dyn_cast(at)) { - at = subscript->getBase(); - continue; - } - if (const auto *unary = dyn_cast(at); - unary != nullptr && unary->getOpcode() == UO_Deref) { - at = unary->getSubExpr(); - continue; - } - if (const auto *cast = dyn_cast(at)) { - at = cast->getSubExpr(); - continue; - } - return {}; - } - return {}; -} - -BoundaryVerdicts checkBoundaryInvariants( - const SiteIndex &sites, llvm::ArrayRef published, - llvm::ArrayRef program, - const std::map> &relied) { - BoundaryVerdicts verdicts; - // The classes whose entry assumption a boundary breaks, with the reason - // that broke them. A class broken both ways takes `dangling-escape`: a - // released pointer is the stronger statement. - std::set dangling; - std::set shared; - // (RFC 0031 amends §9.4 point 3: a broken field class no longer breaks - // the record that holds it. A value loaded from the field and kept in a - // local is found by where the engine says it came from, `relied`.) - const auto note = [&](core::UnresolvedReason reason, const std::string &of) { - if (of.empty()) - return; - std::set &into = - reason == core::UnresolvedReason::DanglingEscape ? dangling : shared; - into.insert(of); - }; - for (const BoundaryRow &row : program) - note(row.reason, row.placeClass); - - for (const PublishedBoundary &boundary : published) { - const core::SiteId at = boundary.site; - const std::string function = - at.function < sites.functions().size() && - sites.functions()[at.function].decl != nullptr - ? sites.functions()[at.function].decl->getNameAsString() - : std::string{}; - const auto exportRow = [&](core::UnresolvedReason reason, - const std::string &of) { - if (of.empty()) - return; - verdicts.exported.push_back(BoundaryRow{.unit = {}, - .function = function, - .site = at.ordinal, - .reason = reason, - .placeClass = of}); - }; - if (!boundary.facts.dangling.empty()) { - const BoundaryFacts::Dangling &first = boundary.facts.dangling.front(); - verdicts.decisions.push_back( - {.site = at, - .reason = core::UnresolvedReason::DanglingEscape, - .detail = "'" + first.name + - "' may hold a pointer to an object that is gone"}); - for (const BoundaryFacts::Dangling &row : boundary.facts.dangling) { - note(core::UnresolvedReason::DanglingEscape, row.placeClass); - exportRow(core::UnresolvedReason::DanglingEscape, row.placeClass); - } - } else if (!boundary.facts.sharedOwners.empty()) { - const BoundaryFacts::SharedOwners &first = - boundary.facts.sharedOwners.front(); - verdicts.decisions.push_back( - {.site = at, - .reason = core::UnresolvedReason::SecondOwner, - .detail = first.cycle - ? "'" + first.names + "' closes an owning cycle" - : "'" + first.names + "' may own the same object"}); - } - for (const BoundaryFacts::SharedOwners &row : boundary.facts.sharedOwners) { - note(core::UnresolvedReason::SecondOwner, row.placeClass); - note(core::UnresolvedReason::SecondOwner, row.otherClass); - exportRow(core::UnresolvedReason::SecondOwner, row.placeClass); - exportRow(core::UnresolvedReason::SecondOwner, row.otherClass); - } - } - std::ranges::sort(verdicts.exported); - const auto duplicates = std::ranges::unique(verdicts.exported); - verdicts.exported.erase(duplicates.begin(), duplicates.end()); - if (dangling.empty() && shared.empty()) - return verdicts; - - // *Propagation*. Every temporal facet the unit would otherwise prove for - // a place of a broken class takes the boundary's reason. The site's - // operand is the place the proof rests on; `LedgerAdapter` downgrades - // only the facets that are in fact proven. - for (const SiteIndex::FunctionSites &function : sites.functions()) - for (const SiteInfo &info : function.sites) { - if (info.operand == nullptr) - continue; - const core::SiteId id = info.id; - // The class the operand is spelled from, and those of the places the - // engine's proof took its value from. - std::vector classes; - if (std::string of = classOfOperand(info.operand); !of.empty()) - classes.push_back(std::move(of)); - if (auto it = relied.find(id); it != relied.end()) - classes.insert(classes.end(), it->second.begin(), it->second.end()); - const auto brokenBy = [&](const std::set &broken) { - for (const std::string &of : classes) - if (broken.contains(of)) - return of; - return std::string(); - }; - if (std::string of = brokenBy(dangling); !of.empty()) - verdicts.decisions.push_back( - {.site = id, - .reason = core::UnresolvedReason::DanglingEscape, - .detail = "a pointer held in '" + of + - "' may be gone where the object is handed on", - .propagated = true}); - else if (std::string owned = brokenBy(shared); !owned.empty()) - verdicts.decisions.push_back( - {.site = id, - .reason = core::UnresolvedReason::SecondOwner, - .detail = "'" + owned + "' may not be the only owner", - .propagated = true}); - } - return verdicts; -} - -} // namespace weavec::analysis diff --git a/lib/Analysis/BypassedDeclarations.cpp b/lib/Analysis/BypassedDeclarations.cpp deleted file mode 100644 index c09ca290..00000000 --- a/lib/Analysis/BypassedDeclarations.cpp +++ /dev/null @@ -1,147 +0,0 @@ -//===- BypassedDeclarations.cpp - Jumps past a declaration (RFC 0030) -----===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// - -#include "weavec/Analysis/BypassedDeclarations.h" - -#include "clang/AST/Expr.h" -#include "clang/AST/Type.h" - -#include "llvm/ADT/DenseMap.h" -#include "llvm/ADT/DenseSet.h" -#include "llvm/ADT/STLExtras.h" -#include "llvm/Support/Casting.h" - -#include -#include -#include - -namespace weavec::analysis { - -/// Whether an object of `type` can hold a pointer: -ftrivial-auto-var-init -/// does not initialise a bypassed one. -static bool mayHoldPointer(clang::QualType type, unsigned depth = 0) { - if (depth > 8 || type.isNull()) - return false; - type = type.getCanonicalType(); - if (type->isPointerType() || type->isBlockPointerType()) - return true; - if (const auto *array = type->getAsArrayTypeUnsafe()) - return mayHoldPointer(array->getElementType(), depth + 1); - if (const auto *record = type->getAsRecordDecl()) { - const clang::RecordDecl *definition = record->getDefinition(); - return definition != nullptr && - llvm::any_of(definition->fields(), - [depth](const clang::FieldDecl *field) { - return mayHoldPointer(field->getType(), depth + 1); - }); - } - return false; -} - -namespace { - -/// §11, A5: the pointer locals of one body whose declaration a `goto`, a -/// computed `goto` or a `switch` can jump past. -class BypassFinder { -public: - BypassFinder(const clang::Stmt &body, bool anyType) : anyType(anyType) { - number(&body, nullptr); - } - - [[nodiscard]] std::vector result() const { - std::vector bypassed; - for (const Declaration &declaration : declarations) { - const auto inRegion = [&](unsigned at) { - return at >= declaration.begin && at <= declaration.end; - }; - if (llvm::any_of(jumps, [&](const std::pair &jump) { - return inRegion(jump.second) && !inRegion(jump.first); - })) - llvm::append_range(bypassed, declaration.variables); - } - return bypassed; - } - -private: - struct Declaration { - /// The pre-order indices after the declaration within its parent. - unsigned begin = 0; - unsigned end = 0; - std::vector variables; - }; - - /// Locals of every type, not only those that may hold a pointer. - bool anyType = false; - unsigned next = 0; - llvm::DenseMap order; - llvm::DenseMap labels; - llvm::DenseSet addressTaken; - std::vector> gotos; - std::vector indirectGotos; - std::vector> jumps; - std::vector declarations; - - /// Numbers `node`'s subtree in pre-order; returns its last index. - unsigned number(const clang::Stmt *node, const clang::Stmt *parent) { - const unsigned at = next++; - order[node] = at; - if (const auto *label = llvm::dyn_cast(node)) - labels[label->getDecl()] = at; - else if (const auto *jump = llvm::dyn_cast(node)) - gotos.emplace_back(at, jump->getLabel()); - else if (llvm::isa(node)) - indirectGotos.push_back(at); - else if (const auto *address = llvm::dyn_cast(node)) - addressTaken.insert(address->getLabel()); - unsigned last = at; - std::vector> pending; - for (const clang::Stmt *child : node->children()) { - if (child == nullptr || llvm::isa(child)) - continue; - last = number(child, node); - if (const auto *statement = llvm::dyn_cast(child)) - pending.emplace_back(statement, last); - } - for (const auto &[statement, end] : pending) { - std::vector variables; - for (const clang::Decl *decl : statement->decls()) - if (const auto *variable = llvm::dyn_cast(decl); - variable != nullptr && variable->hasLocalStorage() && - !variable->getType()->isVariablyModifiedType() && - (anyType || mayHoldPointer(variable->getType()))) - variables.push_back(variable); - if (!variables.empty() && end < last) - declarations.push_back(Declaration{ - .begin = end + 1, .end = last, .variables = std::move(variables)}); - } - if (const auto *switchStmt = llvm::dyn_cast(node)) - for (const clang::SwitchCase *label = switchStmt->getSwitchCaseList(); - label != nullptr; label = label->getNextSwitchCase()) - if (const auto found = order.find(label); found != order.end()) - jumps.emplace_back(at, found->second); - if (parent == nullptr) { - for (const auto &[from, label] : gotos) - if (const auto target = labels.find(label); target != labels.end()) - jumps.emplace_back(from, target->second); - for (const unsigned from : indirectGotos) - for (const clang::LabelDecl *label : addressTaken) - if (const auto target = labels.find(label); target != labels.end()) - jumps.emplace_back(from, target->second); - } - return last; - } -}; - -} // namespace - -std::vector -bypassedDeclarations(const clang::Stmt &body) { - return BypassFinder(body, /*anyType=*/false).result(); -} - -} // namespace weavec::analysis diff --git a/lib/Analysis/CMakeLists.txt b/lib/Analysis/CMakeLists.txt deleted file mode 100644 index 93b50e17..00000000 --- a/lib/Analysis/CMakeLists.txt +++ /dev/null @@ -1,41 +0,0 @@ -# weavec::Analysis -- bridges Clang's AST to the core ownership model. -weavec_add_library( - Analysis - SOURCES Annotations.cpp - BypassedDeclarations.cpp - ClangLocation.cpp - ProgramDatabase.cpp - KindTable.cpp - AttributeReader.cpp - KindInference.cpp - KindInferenceCollect.cpp - KindInferenceDump.cpp - KindInferenceFields.cpp - KindInferenceMustAccess.cpp - SlotCollector.cpp - Concurrency.cpp - SiteCollector.cpp - SourceTerm.cpp - BoundaryInvariants.cpp - LedgerAdapter.cpp - SafetyEngine.cpp - # RFC 0031. - EngineAnnotations.cpp - EngineCalls.cpp - EngineContexts.cpp - EngineDecide.cpp - EngineExpr.cpp - EngineInvariants.cpp - EngineKinds.cpp - EngineLibrary.cpp - EngineLifetimes.cpp - EngineRun.cpp - EngineStrings.cpp - EngineSummary.cpp - EngineUnit.cpp - UnitPipeline.cpp - PUBLIC_DEPS weavec::Core - USES_LLVM) - -weavec_link_llvm(weavecAnalysis Support) -weavec_link_clang(weavecAnalysis clangAnalysis clangAST clangBasic clangLex) diff --git a/lib/Analysis/ClangLocation.cpp b/lib/Analysis/ClangLocation.cpp deleted file mode 100644 index f949a394..00000000 --- a/lib/Analysis/ClangLocation.cpp +++ /dev/null @@ -1,37 +0,0 @@ -//===- ClangLocation.cpp - clang <-> core source location bridging --------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// - -#include "weavec/Analysis/ClangLocation.h" - -namespace weavec::analysis { - -core::SourceLocation toCoreLocation(const clang::SourceManager &sm, - clang::SourceLocation loc) { - core::SourceLocation result; - if (loc.isInvalid()) - return result; - - const clang::SourceLocation expansion = sm.getExpansionLoc(loc); - const clang::PresumedLoc presumed = sm.getPresumedLoc(expansion); - if (presumed.isValid()) { - result.file = presumed.getFilename(); - result.line = presumed.getLine(); - result.column = presumed.getColumn(); - } - result.opaque = expansion.getRawEncoding(); - return result; -} - -clang::SourceLocation toClangLocation(const core::SourceLocation &loc) { - if (loc.opaque == 0) - return {}; - return clang::SourceLocation::getFromRawEncoding( - static_cast(loc.opaque)); -} - -} // namespace weavec::analysis diff --git a/lib/Analysis/Concurrency.cpp b/lib/Analysis/Concurrency.cpp deleted file mode 100644 index b6df5442..00000000 --- a/lib/Analysis/Concurrency.cpp +++ /dev/null @@ -1,256 +0,0 @@ -//===- Concurrency.cpp - What entry points share (RFC 0030 §5.3) ----------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// - -#include "weavec/Analysis/Concurrency.h" - -#include "weavec/Analysis/KindTable.h" -#include "weavec/Analysis/SiteCollector.h" - -#include "clang/AST/RecursiveASTVisitor.h" - -#include "llvm/ADT/STLExtras.h" - -#include - -using namespace clang; - -namespace weavec::analysis { - -/// Whether a value of `type` holds a pointer: itself, an element or a field. -static bool carriesPointer(QualType type, unsigned depth = 0) { - type = type.getCanonicalType(); - if (type->isPointerType()) - return true; - if (const auto *array = type->getAsArrayTypeUnsafe()) - return carriesPointer(array->getElementType(), depth); - if (const RecordDecl *record = type->getAsRecordDecl(); - record != nullptr && depth < 8 && record->getDefinition() != nullptr) - return llvm::any_of(record->getDefinition()->fields(), - [depth](const FieldDecl *field) { - return carriesPointer(field->getType(), depth + 1); - }); - return false; -} - -/// The variable an lvalue or pointer expression is rooted at, through -/// members, subscripts, dereferences and address-of. -static const VarDecl *rootVariable(const Expr *e) { - for (unsigned depth = 0; e != nullptr && depth < 64; ++depth) { - e = e->IgnoreParenCasts(); - if (const auto *ref = dyn_cast(e)) - return dyn_cast(ref->getDecl()); - if (const auto *member = dyn_cast(e)) - e = member->getBase(); - else if (const auto *subscript = dyn_cast(e)) - e = subscript->getBase(); - else if (const auto *unary = dyn_cast(e); - unary != nullptr && (unary->getOpcode() == UO_Deref || - unary->getOpcode() == UO_AddrOf)) - e = unary->getSubExpr(); - else - return nullptr; - } - return nullptr; -} - -namespace { -/// The entry targets of the unit and the variables passed to them. -class EntryCollector : public RecursiveASTVisitor { -public: - EntryCollector(ASTContext &ctx, const core::LibrarySpec &spec) - : context(ctx), library(spec) {} - - std::vector targets; - llvm::DenseSet passed; - llvm::DenseSet addressTaken; - bool unknownTarget = false; - - bool TraverseFunctionDecl(FunctionDecl *function) { // NOLINT - const FunctionDecl *outer = current; - current = function; - const bool result = RecursiveASTVisitor::TraverseFunctionDecl(function); - current = outer; - return result; - } - bool VisitDeclRefExpr(DeclRefExpr *ref) { // NOLINT - if (const auto *function = dyn_cast(ref->getDecl())) - addressTaken.insert(function->getCanonicalDecl()); - return true; - } - /// `&g`, `&g.f`, an array `g` decaying: external code may reach `g`. - bool VisitUnaryOperator(UnaryOperator *unary) { // NOLINT - if (unary->getOpcode() == UO_AddrOf) - if (const VarDecl *var = rootVariable(unary->getSubExpr())) - addressTakenVars.insert(var->getCanonicalDecl()); - return true; - } - bool VisitImplicitCastExpr(ImplicitCastExpr *cast) { // NOLINT - if (cast->getCastKind() == CK_ArrayToPointerDecay) - if (const VarDecl *var = rootVariable(cast->getSubExpr())) - addressTakenVars.insert(var->getCanonicalDecl()); - return true; - } - llvm::DenseSet addressTakenVars; - bool VisitCallExpr(CallExpr *call) { // NOLINT - const FunctionDecl *callee = call->getDirectCallee(); - const auto match = callee != nullptr - ? governingLibraryEntry(*callee, library) - : std::nullopt; - if (!match) - return true; - for (unsigned i = 0; i < call->getNumArgs(); ++i) { - const core::LibraryParam *param = match->param(i); - if (param == nullptr || !param->callback || - param->callback->kind != core::LibCallback::Kind::Entry) - continue; - if (param->type == core::LibraryParam::Type::Function) - addTarget(call->getArg(i)); - else - addStoredTargets(call->getArg(i)); - for (const std::uint8_t row : param->callback->arguments) - if (const int at = match->callArgument(row); - at >= 0 && static_cast(at) < call->getNumArgs()) - if (const VarDecl *var = - rootVariable(call->getArg(static_cast(at)))) - passed.insert(var->getCanonicalDecl()); - } - return true; - } - -private: - ASTContext &context; - const core::LibrarySpec &library; - const FunctionDecl *current = nullptr; - - void addTarget(const Expr *value) { - const Expr *e = value->IgnoreParenCasts(); - if (const auto *ref = dyn_cast(e)) - if (const auto *function = dyn_cast(ref->getDecl())) { - targets.push_back(function); - return; - } - // `SIG_DFL` (null) and `SIG_IGN` (an integer) are no function. - if (value->isNullPointerConstant(context, Expr::NPC_ValueDependentIsNull) || - isa(e)) - return; - unknownTarget = true; - } - - /// `sigaction(sig, &sa, old)`: the handlers stored into `sa` in this - /// function, or in its initialiser. - void addStoredTargets(const Expr *object) { - if (object->isNullPointerConstant(context, Expr::NPC_ValueDependentIsNull)) - return; - const VarDecl *var = rootVariable(object); - if (var == nullptr || current == nullptr || !current->hasBody()) { - unknownTarget = true; - return; - } - const auto designators = [this](const Stmt *stmt, - const auto &self) -> void { - if (stmt == nullptr) - return; - if (const auto *ref = dyn_cast(stmt); - ref != nullptr && isa(ref->getDecl())) - addTarget(ref); - for (const Stmt *child : stmt->children()) - self(child, self); - }; - designators(var->getInit(), designators); - const auto stores = [&](const Stmt *stmt, const auto &self) -> void { - if (stmt == nullptr) - return; - if (const auto *assign = dyn_cast(stmt); - assign != nullptr && assign->isAssignmentOp() && - rootVariable(assign->getLHS()) == var) - designators(assign->getRHS(), designators); - for (const Stmt *child : stmt->children()) - self(child, self); - }; - stores(current->getBody(), stores); - } -}; -} // namespace - -ConcurrencyShare collectConcurrencyShare(ASTContext &context, - const core::LibrarySpec &library) { - ConcurrencyShare share; - EntryCollector entries(context, library); - entries.TraverseDecl(context.getTranslationUnitDecl()); - if (entries.targets.empty() && !entries.unknownTarget) - return share; - for (const VarDecl *var : entries.passed) - share.roots.insert(var); - // Every function reachable from a target, and the globals they touch. - std::vector work; - for (const FunctionDecl *target : entries.targets) - if (const FunctionDecl *definition = target->getDefinition()) { - work.push_back(definition); - for (const ParmVarDecl *param : definition->parameters()) - if (carriesPointer(param->getType())) - share.roots.insert(param); - } - bool external = entries.unknownTarget; - const SourceManager &sm = context.getSourceManager(); - while (!work.empty()) { - const FunctionDecl *function = work.back(); - work.pop_back(); - if (!share.entryReachable.insert(function->getCanonicalDecl()).second) - continue; - const auto visit = [&](const Stmt *stmt, const auto &self) -> void { - if (stmt == nullptr) - return; - if (const auto *ref = dyn_cast(stmt)) { - if (const auto *var = dyn_cast(ref->getDecl()); - var != nullptr && var->hasGlobalStorage() && - carriesPointer(var->getType())) - share.roots.insert(var->getCanonicalDecl()); - } else if (const auto *call = dyn_cast(stmt)) { - const FunctionDecl *callee = call->getDirectCallee(); - if (callee == nullptr) { - // Until the slots of §9.3: any address-taken function. - for (const FunctionDecl *taken : entries.addressTaken) - if (const FunctionDecl *definition = taken->getDefinition()) - work.push_back(definition); - } else if (const FunctionDecl *definition = callee->getDefinition()) { - work.push_back(definition); - } else if (!governingLibraryEntry(*callee, library) && - !isPlatformDeclaration(*callee, library, sm)) { - external = true; - } - } - for (const Stmt *child : stmt->children()) - self(child, self); - }; - visit(function->getBody(), visit); - } - // Code WeaveC cannot see may reach every global of external linkage and - // every global whose address is taken. - if (external) { - const auto globals = [&](const Decl *decl, const auto &self) -> void { - if (const auto *var = dyn_cast(decl); - var != nullptr && var->hasGlobalStorage() && - carriesPointer(var->getType()) && - (var->hasExternalFormalLinkage() || - entries.addressTakenVars.contains(var->getCanonicalDecl()))) - share.roots.insert(var->getCanonicalDecl()); - if (const auto *nested = dyn_cast(decl)) - for (const Decl *child : nested->decls()) - self(child, self); - }; - globals(context.getTranslationUnitDecl(), globals); - } - return share; -} - -bool rootedInShare(const Expr &operand, const ConcurrencyShare &share) { - const VarDecl *root = rootVariable(&operand); - return root != nullptr && share.roots.contains(root->getCanonicalDecl()); -} - -} // namespace weavec::analysis diff --git a/lib/Analysis/Engine.h b/lib/Analysis/Engine.h deleted file mode 100644 index d9220547..00000000 --- a/lib/Analysis/Engine.h +++ /dev/null @@ -1,1284 +0,0 @@ -//===- Engine.h - The object engine (RFC 0031) ------------------*- C++ -*-===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// RFC 0031's engine behind the RFC 0030 seam. `ObjectEngine` runs a unit: -// the call graph bottom up, summaries (`core::FunctionEffects`) to a -// fixpoint per strongly connected component, then one authoritative pass -// per emitted function. `FunctionRun` analyses one function body over the -// always-add CFG with `core::HeapState` as the lattice (§4), and in its -// final pass decides every site `SiteCollector` enumerated (§5). -// -// Only `ObjectEngine.cpp` and the `Engine*.cpp` files include this header -// (hygiene gate H2). -// -//===----------------------------------------------------------------------===// - -#ifndef WEAVEC_LIB_ANALYSIS_ENGINE_H -#define WEAVEC_LIB_ANALYSIS_ENGINE_H - -#include "weavec/Analysis/Annotations.h" -#include "weavec/Analysis/KindInference.h" -#include "weavec/Analysis/LedgerAdapter.h" -#include "weavec/Analysis/ObjectEngine.h" -#include "weavec/Analysis/SafetyEngine.h" -#include "weavec/Analysis/SiteCollector.h" -#include "weavec/Analysis/SourceTerm.h" -#include "weavec/Core/Effects.h" -#include "weavec/Core/Heap.h" - -#include "clang/AST/ASTContext.h" -#include "clang/AST/Decl.h" -#include "clang/AST/Expr.h" -#include "clang/AST/ParentMap.h" -#include "clang/AST/Stmt.h" -#include "clang/Analysis/CFG.h" - -#include "llvm/ADT/BitVector.h" -#include "llvm/ADT/DenseMap.h" -#include "llvm/ADT/DenseSet.h" -#include "llvm/ADT/SparseBitVector.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace weavec::analysis::engine { - -/// Opaque handles for Clang entities (RFC 0031 §2). -// NOLINTBEGIN(cppcoreguidelines-pro-type-reinterpret-cast,performance-no-int-to-ptr): -// a handle is the address of the node it names. -[[nodiscard]] inline core::Handle handleOf(const void *pointer) noexcept { - return static_cast(reinterpret_cast(pointer)); -} -template -[[nodiscard]] inline const T *fromHandle(core::Handle handle) noexcept { - return reinterpret_cast(static_cast(handle)); -} -// NOLINTEND(cppcoreguidelines-pro-type-reinterpret-cast,performance-no-int-to-ptr) -/// The variable a `Local` or `Global` object is; null for any other, and -/// for a local an expression makes (`ObjectKey::expression`), whose handle -/// is no declaration. -[[nodiscard]] inline const clang::VarDecl * -variableOf(const core::ObjectInfo &info) noexcept { - if ((info.key.kind != core::ObjectKind::Local && - info.key.kind != core::ObjectKind::Global) || - info.key.expression) - return nullptr; - return llvm::dyn_cast_or_null( - fromHandle(info.key.handle)); -} -[[nodiscard]] core::Handle typeHandle(clang::QualType type) noexcept; -[[nodiscard]] clang::QualType typeOfHandle(core::Handle handle) noexcept; - -class UnitRun; - -/// RFC 0030 §9.1 `param N =0|!=0`: whether `value`, a parameter's or an -/// argument's, is zero (a null pointer) for certain, non-zero for certain, -/// or either. -[[nodiscard]] std::optional isZeroValue(const core::Heap &heap, - const core::HeapState &state, - core::Sym value); - -/// A C place as a diagnostic spells it (`b->cap`, where a check's text -/// says `(*b).cap`); any other term as the check does. -[[nodiscard]] std::string messageSpelling(const SourceTerm &term); - -/// `path[*]`: the elements below `path`. Unlike `SummaryPath::indexed`, -/// never collapsed onto a trailing dereference, so `a[*]` and `a[0]` -/// (`*a`) stay different paths (RFC 0031 §4.2 *Amendment (arrays)*). -[[nodiscard]] inline core::SummaryPath elementsOf(core::SummaryPath path) { - path.steps.pushBack( - core::PathElem{.step = core::PathStep::Index, .field = {}}); - return path; -} - -/// `path` extended to the scalar cell at byte `offset` of an object of -/// `type`: the member names down through nested records (`box.data`), an -/// anonymous member by its offset, and `#` where no member names -/// it. At offset zero of a scalar object `path` itself, unless `named` -/// (then `#0`). EngineSummary.cpp. -[[nodiscard]] core::SummaryPath cellPath(const clang::ASTContext &context, - clang::QualType type, - core::SummaryPath path, - std::int64_t offset, bool named); - -/// What one analysis of a function produced. -struct RunResult { - core::FunctionEffects effects; - bool overBudget = false; - /// Over the per-function budget, not just the run's share of the unit's. - bool spentBudget = false; - std::uint64_t transfers = 0; - /// RFC 0034 §7.1. - std::uint64_t work = 0; -}; - -/// A place where a value is stored or read: every object and offset an -/// lvalue may designate. -struct Address { - std::vector targets; - /// Any object (an unknown or raw pointer). - bool top = false; - /// The pointer the lvalue dereferences, when it dereferences one. - core::Sym base = core::ZeroSym; -}; - -/// The per-expression result of evaluation. -struct ExprResult { - core::Sym value = core::ZeroSym; - std::optional
address = std::nullopt; -}; - -/// RFC 0031 §6.6: which of a callee's entry objects a caller made the same -/// object. Each parameter maps to the lowest-numbered parameter it shares an -/// object with (itself when none) and its byte offset from it; each global -/// listed points into that parameter's object. -struct AliasContext { - std::vector> params; - std::vector> - globals; - /// Integer arguments the caller knows the value of. - std::vector> constants; - /// Globals whose value points into the object another global's value - /// points to (the first), at an offset from it. - std::vector< - std::tuple> - globalAliases; - /// Pointer cells of argument objects that point into one object: (param, - /// cell offset) holds the value of (rep param, rep cell offset), shifted. - struct CellAlias { - unsigned param; - std::int64_t cell; - unsigned repParam; - std::int64_t repCell; - std::int64_t shift; - friend auto operator<=>(const CellAlias &, const CellAlias &) = default; - }; - std::vector cellAliases; - /// §6.6 *Amendment (numeric contexts)*: integer cells of the objects - /// pointer arguments point to that the caller knows the value of, as - /// `(parameter, byte offset, value)`. - std::vector> cells; - /// The same for the object a pointer global the callee reads points to, - /// as `(global, byte offset, value)`. - std::vector> - globalCells; - /// §7 *Amendment (cross-unit contexts)*: the functions a function-pointer - /// argument holds, by portable name. - std::vector>> callbacks; - - /// No parameter shares another's object and no global points into one. - [[nodiscard]] bool trivial() const { - for (unsigned i = 0; i < params.size(); ++i) - if (params[i].first != i) - return false; - return globals.empty() && globalAliases.empty() && cellAliases.empty(); - } - friend bool operator<(const AliasContext &a, const AliasContext &b) { - return std::tie(a.params, a.globals, a.constants, a.globalAliases, - a.cellAliases, a.cells, a.globalCells, a.callbacks) < - std::tie(b.params, b.globals, b.constants, b.globalAliases, - b.cellAliases, b.cells, b.globalCells, b.callbacks); - } -}; - -/// §7 *Amendment (cross-unit contexts)*: a context's portable spelling (its -/// parameters' aliases, constants, integer cells and callbacks; not the -/// globals, which another unit numbers differently), and back. -[[nodiscard]] std::string contextKeyText( - const AliasContext &context, - const std::function &globalName); -[[nodiscard]] std::optional parseContextKey( - llvm::StringRef text, unsigned params, - const std::function &global); - -/// How one function body is analysed. -enum class RunMode : std::uint8_t { - /// A summary round: publishes into a discarding adapter. - Summary, - /// The authoritative pass (§3 step 3). - Authoritative, - /// An alias-context run: diagnostics only (§6.6). - Context, -}; - -/// The object an argument points into, as written (`buf` for `&buf[3]` -/// and `buf + 2`). -const clang::Expr &pointedObject(const clang::Expr &argument); - -class FunctionRun; - -/// RFC 0030 §9.4: the class of a cell for propagation: `struct s.f` for a -/// field, the global's name for a global's own storage; empty when neither. -std::string cellClass(const FunctionRun &run, core::ObjectId object, - core::CellKey key); - -/// Every object reachable from `start` through the state's memory, `start` -/// included. -std::vector reachableFrom(const core::Heap &heap, - const core::HeapState &state, - std::vector start); - -/// One analysis of one function body. -class FunctionRun final : public core::HeapOracle { -public: - FunctionRun(UnitRun &unitRun, const clang::FunctionDecl &fn, - LedgerAdapter &adapter, RunMode runMode, - const AliasContext *alias = nullptr, unsigned depth = 0); - ~FunctionRun() override; - FunctionRun(const FunctionRun &) = delete; - FunctionRun &operator=(const FunctionRun &) = delete; - FunctionRun(FunctionRun &&) = delete; - FunctionRun &operator=(FunctionRun &&) = delete; - - RunResult run(); - /// RFC 0030 §7.6 (RFC 0031 *Implementation amendments*): a checking run - /// refutes the standing counted-field invariants its writes break, at - /// its calls and exits. - bool checkingInvariants = false; - /// Refutes the standing invariants `object` breaks in `state`. - void checkInvariants(core::HeapState state, core::ObjectId object) const; - /// `--dump-analysis`: the states at block entries and the objects. - void dump(llvm::raw_ostream &os); - - // core::HeapOracle - [[nodiscard]] bool typesMayAlias(core::Handle first, - core::Handle second) const override; - core::Sym unwritten(core::HeapState &state, core::ObjectId object, - core::CellKey key, - const core::SymInfo &hint) const override; - -private: - friend class Transfer; - UnitRun &unit; - const clang::FunctionDecl &function; - const AliasContext *aliasContext; - unsigned contextDepth; - clang::ASTContext &context; - LedgerAdapter &out; - /// Where the run publishes: `out`, or the replay's witness adapter. - LedgerAdapter *publishTo; - RunMode mode; - mutable core::ObjectTable objects; - core::Heap heap; - std::unique_ptr cfg; - std::vector> entryStates; - std::vector visits; - /// Joins at each loop head (§12's cost bound). - std::vector joinsAt; - std::vector loopHead; - /// The back edges (block, loop head) of the depth-first order. - std::set> backEdges; - /// Each loop head's natural loop: the blocks that reach one of its back - /// edges without passing through it (the order of iteration, §12). - llvm::DenseMap loopBody; - /// Blocks in reverse post-order. - std::vector order; - /// Expressions whose value a later block reads (§2), and the ones each - /// block reads. - llvm::DenseSet crossBlock; - llvm::DenseMap> consumedBy; - /// Parameters the body never assigns (their cells hold the entry value). - std::set unmodifiedParams; - std::vector fixedLocals; - /// Liveness of locals after each CFG element (for leaks). - llvm::DenseMap localIndex; - llvm::DenseMap liveAfterStmt; - llvm::DenseMap liveBeforeStmt; - /// The locals a block may reference before they are assigned again, or - /// at all, from its entry on (by block id). - std::vector liveInBlock; - /// Where each block is (its first statement's, or its terminator's, - /// expansion location), and the compound statement each local is - /// declared in: a local stays while a block inside its scope may name it - /// in a check (`dropDeadLocals`). - std::vector blockLocation; - llvm::DenseMap localScope; - llvm::BitVector addressTaken; - /// Drops from `state`, entering block `block`, the locals no path from it - /// references whose address is not taken (§4.6): what they hold is dead. - void dropDeadLocals(core::HeapState &state, unsigned block) const; - /// The expression values a block's state carries for a later block - /// (`exprs`: cross-block operands, a conditional's arm values), by index, - /// and the ones some path from each block's entry reads before it - /// evaluates them again. - llvm::DenseMap carriedIndex; - std::vector> carriedLiveIn; - /// Drops from `state`, entering block `block`, the carried expression - /// values no path from it reads. - void dropDeadValues(core::HeapState &state, unsigned block) const; - /// Statement-level elements: whose parent is not an expression. - llvm::DenseSet statementLevel; - /// RFC 0017: declarations whose variably modified type the CFG does not - /// evaluate (a pointer to a variable-length array), by the first element - /// of their initializer: the dimensions are taken before it runs. - llvm::DenseMap> - vlaCaptureBefore; - llvm::DenseSet vlaCapturedEarly; - /// §6.6: a context this run needed was not run (its depth or count limit), - /// here or in a context it ran: the call that requested it is not proven. - bool contextIncomplete = false; - /// Calls with a spatial violation reported (RFC 0030 §7.5, or a store past - /// the caller's object): one finding per call. - llvm::DenseSet requirementViolated; - /// The block that evaluates each expression element. - llvm::DenseMap evaluatedIn; - /// The block being transferred. - unsigned currentBlock = ~0U; - /// The current block ends the program (a call that does not return). - bool currentBlockNoReturn = false; - /// The arms of conditional operators, to their operator. - llvm::DenseMap arms; - /// Exit states, for the summary. - std::vector exits; - std::uint64_t transfers = 0; - /// RFC 0034 §7.1: the run's work, and the states it keeps at entries. - std::uint64_t work = 0; - std::uint64_t retained = 0; - /// §12 `--analysis-stats`: the joins and materialised cells of the run. - std::uint64_t joins = 0; - mutable std::uint64_t materialisations = 0; - /// Stopped by the per-function budget (`RunResult::spentBudget`). - bool spentBudget = false; - bool overBudget = false; - bool publishing = false; - bool inFinalPass = false; - /// Objects already reported leaked in the publishing pass. - std::set reportedLeaks; - /// ... and in which blocks: a loss downstream of a reported one is the - /// same path's (RFC 0007: once per path). - std::vector> leakBlocks; - /// Whether the CFG block `to` is reachable from `from` (or is it). - [[nodiscard]] bool blockReaches(unsigned from, unsigned to) const; - /// RFC 0008: uninitialised values already reported where they were read - /// (a copy reports at the copy, once). - std::set reportedUninit; - /// Summary derivation: the new objects already described (§6.1). - std::set visitedFresh; - /// RFC 0007: entry objects a `WEAVEC_OWNED` field declares owned, with - /// the field's name and declaration (a leak's note). - std::map> - declaredOwner; - /// Diagnostics already reported, keyed by site and id. - std::set> reported; - /// RFC 0034 §6.1: the definite errors of the authoritative pass, and - /// what the replay saw of each: a path that reports it (`witnessed`), a - /// path that reaches its site and decides it otherwise (`countered`). - struct Candidate { - const clang::Stmt *site = nullptr; - core::Facet facet = core::Facet::Temporal; - std::string id; - core::SourceLocation where; - bool witnessed = false; - bool countered = false; - /// In the block being replayed: reported definitely, decided otherwise. - bool reportedHere = false; - bool otherwiseHere = false; - }; - std::vector candidates; - bool witnessing = false; - /// Replays the function along single paths and unconfirms the - /// candidates no path confirms. - void confirmCandidates(); - /// §4.1: the symbol an integer operation on two symbols last produced, so - /// the same operation on the same values finds the same value again - /// (checked against the state before use, EngineExpr.cpp). - /// Summary derivation (§6.2): the cells this run stored into, anywhere - /// (an integer cell's value alone does not tell a store from the entry - /// value). - std::set> writtenCells; - using OperationKey = std::tuple, core::Handle>; - std::map operations; - - // Object helpers (EngineRun.cpp). - mutable llvm::DenseMap localObjects; - const clang::Stmt *currentElement = nullptr; - mutable std::unique_ptr parentMap; - mutable std::optional> passedToCalls; - mutable std::optional> bypassed; - -public: - // Services for the transfer functions. - [[nodiscard]] core::Heap &domain() noexcept { return heap; } - [[nodiscard]] core::ObjectTable &table() const noexcept { return objects; } - [[nodiscard]] clang::ASTContext &ast() const noexcept { return context; } - [[nodiscard]] UnitRun &unitRun() const noexcept { return unit; } - [[nodiscard]] const clang::FunctionDecl &decl() const noexcept { - return function; - } - [[nodiscard]] bool isPublishing() const noexcept { return publishing; } - [[nodiscard]] unsigned depth() const noexcept { return contextDepth; } - [[nodiscard]] bool isUnmodifiedParam(unsigned index) const { - return unmodifiedParams.contains(index); - } - /// Pointer locals of the body's outermost block, assigned only where they - /// are declared: one value for the whole run. - [[nodiscard]] const std::vector & - fixedPointerLocals() const noexcept { - return fixedLocals; - } - [[nodiscard]] RunMode runMode() const noexcept { return mode; } - [[nodiscard]] LedgerAdapter &ledger() const noexcept { return *publishTo; } - /// The unit's sites, whatever adapter this run publishes into (a context - /// run collects into its own, §6.6). - [[nodiscard]] const SiteIndex &sites() const; - /// The function calls a returns-twice function (RFC 0030 §5.4). - [[nodiscard]] bool callsSetjmp() const; - [[nodiscard]] bool applies(core::SiteId id, core::Facet facet) const; - [[nodiscard]] bool isCrossBlock(const clang::Expr &expr) const { - return crossBlock.contains(&expr); - } - /// Whether `expr` is evaluated as an element of the current block (so a - /// carried value of it is from an earlier visit and stale). - [[nodiscard]] bool evaluatedHere(const clang::Expr &expr) const { - auto it = evaluatedIn.find(&expr); - return it != evaluatedIn.end() && it->second == currentBlock; - } - /// The conditional operator `expr` is an arm of, if any. - [[nodiscard]] const clang::Expr *armOf(const clang::Expr &expr) const { - auto it = arms.find(&expr); - return it == arms.end() ? nullptr : it->second; - } - [[nodiscard]] bool isStatementLevel(const clang::Stmt &stmt) const { - return statementLevel.contains(&stmt); - } - /// Records an exit state (a `return` or the end of the body). - void noteExit(const core::HeapState &state) { exits.push_back(state); } - - /// The object of a local, parameter or global variable. - core::ObjectId variableObject(const clang::VarDecl &var) const; - /// The object of a string literal or compound literal. - core::ObjectId literalObject(const clang::Expr &literal) const; - /// An allocation site's recent object (§4.2). - core::ObjectId allocationObject(const clang::Expr &site, - clang::QualType pointee, - const std::string &name, - const core::SummaryPath &path = {}) const; - /// RFC 0013: the caller's temporary a record-valued call returns into. - core::ObjectId recordResultObject(const clang::CallExpr &call) const; - /// The object a callee without a body returned a pointer to. - core::ObjectId callResultObject(const clang::Expr &site, - clang::QualType pointee, - const std::string &name) const; - /// The unknown object. - core::ObjectId unknownObject() const; - /// RFC 0030 §8.2: the library's hidden state slot `` (`strtok`'s - /// saved string, `getenv`'s environment), one object per slot. - core::ObjectId stateObject(const std::string &slot) const; - /// Whether `id` is a hidden state slot's object. - [[nodiscard]] bool isStateObject(core::ObjectId id) const; - /// The entry object below `parent`'s cell `key` (§4.6). - core::ObjectId childEntryObject(const core::HeapState &state, - core::ObjectId parent, core::CellKey key, - clang::QualType pointee, - const clang::FieldDecl *field) const; - /// A fresh value of C type `type` reached from an object nobody in this - /// activation wrote: an entry pointer, an unknown integer. - core::Sym entryValue(core::HeapState &state, clang::QualType type, - core::ObjectId parent, core::CellKey key, - const clang::FieldDecl *field) const; - /// §7.3–§7.5: a pointer parameter's entry extent and nullness from its - /// kind (EngineKinds.cpp); `values` are the integer parameters' symbols. - std::optional paramExtent(unsigned index, - const std::vector &values, - bool &nonnull) const; - /// The roots of the state for collection (§4.6). - [[nodiscard]] std::vector - roots(const core::HeapState &state) const; - - /// RFC 0007: owned allocations nothing live reaches after `at` are leaked. - /// `ExitEdge`: an edge that reaches the exit through blocks with no - /// statements, reported at the branch that takes it. `Scope`: the end of - /// a block with no statements, where only the locals whose lifetime ended - /// are dead. - enum class LeakPoint : std::uint8_t { - Statement, - BlockEnd, - Release, - Exit, - ExitEdge, - Scope - }; - void checkLeaks(core::HeapState &state, const clang::Stmt &at, - LeakPoint point, const std::string &released = {}); - /// Whether the local `var` may be read after `at`. - [[nodiscard]] bool liveAfter(const clang::Stmt &at, - const clang::VarDecl &var) const; - [[nodiscard]] bool liveBefore(const clang::Stmt &at, - const clang::VarDecl &var) const; - /// Reports a diagnostic once per (site, id) in the publishing pass. - void report(core::Diagnostic diagnostic, core::Certainty certainty, - const clang::Stmt *site, std::optional facet); - - // Lifetimes (EngineLifetimes.cpp, §5.7). - /// Where a pointer to frame storage was last stored into a cell, in the - /// publishing pass, and how the program spelled the cell there. - struct FrameStore { - const clang::Stmt *at = nullptr; - std::string holder; - }; - /// Storage this activation's frame owns: a local, a parameter, a compound - /// literal or temporary, an `alloca` block. - [[nodiscard]] bool isFrameObject(const core::HeapState &state, - core::ObjectId id) const; - /// How messages name frame storage (`x`, ``). - [[nodiscard]] std::string frameName(core::ObjectId id) const; - void noteFrameStore(core::ObjectId holder, core::CellKey key, - core::ObjectId frame, std::string holderSpelling); - [[nodiscard]] const FrameStore *frameStore(core::ObjectId holder, - core::CellKey key, - core::ObjectId frame) const; - /// §5.5: where a derived pointer (a borrow) was last stored into a cell. - void noteBorrowStore(core::ObjectId holder, core::CellKey key, - std::string holderSpelling); - [[nodiscard]] const FrameStore *borrowStore(core::ObjectId holder, - core::CellKey key) const; - /// The local variable (or parameter) an object is the storage of. - [[nodiscard]] const clang::VarDecl *localVariable(core::ObjectId id) const; - /// Whether the address of the local `var` is taken (or it is an - /// aggregate), so a pointer it holds may be read through another name. - [[nodiscard]] bool isAddressTaken(const clang::VarDecl &var) const; - /// The conditions `Transfer::refineCondition` is refining, outermost - /// first. - std::vector openConditions; - /// A call ran code the analysis does not see (`FunctionEffects:: - /// unknownGlobals`). - bool ranUnknownCode = false; - /// The CFG element being transferred. - void noteElement(const clang::Stmt &stmt) { currentElement = &stmt; } - /// RFC 0030 §11: whether `operand` reads a local whose declaration a - /// jump can bypass, so zero-initialisation does not reach it. - [[nodiscard]] bool isBypassed(const clang::Expr &operand) const; - /// RFC 0033 §1: whether `expr`'s value is discarded by a `(void)` cast. - [[nodiscard]] bool isDiscarded(const clang::Expr &expr) const; - /// `stmt`'s parent in the body (the unit's parent map costs too much). - [[nodiscard]] const clang::Stmt *parentOf(const clang::Stmt &stmt) const; - /// RFC 0033 §1: whether the function passes the address of `var` (or of - /// a member or an element of it) to a call. - [[nodiscard]] bool isPassedToCall(const clang::VarDecl &var) const; - /// RFC 0030 §6.1: whether `stmt` is inside a `WEAVEC_UNSAFE` block or - /// function. - [[nodiscard]] bool inUnsafeRegion(const clang::Stmt &stmt) const; - /// Frame objects a global held at a call (§5.7: handed to the callee - /// through the global, not left behind by mistake). - std::set exposedFrames; - /// RFC 0012: where the program last wrote an object's bytes wholesale (a - /// library call's copy, fill or string write, an array's initialiser), for - /// the note on a read of an object left without a terminator. Messages - /// only. - std::map byteWrites; - -private: - /// §5.7: frame stores by (holder, cell, frame object). - std::map, - FrameStore> - frameStores; - std::map, FrameStore> borrowStores; - -public: - /// Declaration-level annotation diagnostics (EngineAnnotations.cpp). - void validateAnnotations(); - /// The constants a bound growing at loop head `head` may stop at (§4.8): - /// those the loop's conditions test. - [[nodiscard]] std::vector - wideningThresholds(unsigned head) const; - -private: - void buildCfg(); - void initialState(core::HeapState &state); - bool transferBlock(const clang::CFGBlock &block, core::HeapState state, - std::vector> &outs); - void finalPass(); - /// The summary from the exit states (EngineSummary.cpp). - core::FunctionEffects deriveEffects(); - /// The cells of a fresh object as stores below `path` (§6.1). - template - void describeContents(const core::HeapState &state, core::ObjectId object, - const core::SummaryPath &path, - std::map &stores, - int depth, Describe &describe); - /// RFC 0015 §5: an entry object's ranges, selected cells and summary - /// cells at an exit as element releases and stores (EngineSummary.cpp). - using ElementKey = - std::pair>; - template - void describeElements(const core::HeapState &state, core::ObjectId id, - const core::ObjectState &contents, - const core::SummaryPath &objectPath, Describe &describe, - std::map &releases, - std::map &stores); - /// `term` over the values the integer parameters still hold (§6.2), when - /// the zone relates it to one of them. - [[nodiscard]] std::optional - parameterTerm(const core::HeapState &state, const core::Term &term) const; - /// Integer parameters the body never assigns or takes the address of. - mutable std::optional> unchangedParams; -}; - -/// One requirement on an argument of a call (RFC 0030 §7.5, §8). -struct ArgRequirement { - enum class Kind : std::uint8_t { Bytes, String }; - Kind kind = Kind::Bytes; - unsigned argument = 0; - /// The bytes needed behind the argument ... - core::Term need = core::Term::unknown(); - /// ... exactly, or a bound on them (a string length known only to be at - /// most, a format's least output): a bound proves or violates one way. - enum class Bound : std::uint8_t { Exact, AtMost, AtLeast }; - Bound bound = Bound::Exact; - /// §7.5: it binds only under a guard not known to hold, so a shortfall is - /// no violation. - bool guarded = false; - /// A shortfall against an exact extent is the call's violation. - bool enforced = false; - /// Proven or unresolved, never checked (§7.3 reliance). - bool rowOnly = false; - /// A `str` destination bounded by its member (§7.4). - bool memberBound = false; - bool writes = false; - /// A `printf`-family writer, checked through its bounded writer. - bool format = false; - /// An element of `main`'s argv: nul-terminated by the system. - bool argvElement = false; - /// A `%s` argument of a literal format: only a read past an object with - /// no terminator is decided (RFC 0012). - bool formatArgument = false; -}; - -/// The transfer functions over one state (§5): evaluation of the CFG -/// elements of one block, and the decisions of their sites in the -/// publishing pass. -class Transfer { -public: - Transfer(FunctionRun &run, core::HeapState &state); - - /// One CFG element. - void element(const clang::CFGElement &element); - /// The value of a branch condition evaluated in this block, if any. - [[nodiscard]] core::Sym conditionValue(const clang::Expr &condition); - /// Refines `state` for the edge on which `condition` is `truth`; returns - /// false when the edge is infeasible. - static bool refine(FunctionRun &run, core::HeapState &state, - core::Sym condition, bool truth); - static bool refineCondition(FunctionRun &run, core::HeapState &state, - core::Sym condition, bool truth); - /// Refines `state` for the edge of `switchStmt` to `successor` (the - /// default edge when `isDefault`), on which `value` of `type` matched its - /// case label or none of them (RFC 0017: labels converted to the - /// promoted type); false when the edge is infeasible. - static bool refineSwitchEdge(FunctionRun &run, core::HeapState &state, - core::Sym value, clang::QualType type, - const clang::SwitchStmt &switchStmt, - const clang::CFGBlock &successor, - bool isDefault); - /// Refines `state` to the paths on which `sym` is in `resultClass` and - /// applies the pending cases that decides; false when none are. - static bool selectClass(FunctionRun &run, core::HeapState &state, - core::Sym sym, core::ResultClass resultClass); - /// Applies the pending cases of `sym` its value already decides. - static void settlePending(FunctionRun &run, core::HeapState &state, - core::Sym sym); - /// RFC 0017: the dimensions of the variable-length arrays a declaration - /// (or typedef) of `type` spells, evaluated where it runs; a later - /// `sizeof` or extent uses these values, not the expressions' current - /// ones. - void captureVlas(clang::QualType type); - /// The element count a variable-length array type was declared with. - core::Sym vlaCount(const clang::VariableArrayType &vla); - /// The bytes an object of `type` takes, when its size is known or made of - /// captured dimensions. - std::optional bytesOf(clang::QualType type, const clang::Expr &at); - /// The block's end: exits and expression values later blocks read. - void finishBlock(const clang::CFGBlock &block); - - // Evaluation (EngineExpr.cpp). - ExprResult evaluate(const clang::Expr &expr); - core::Sym valueOf(const clang::Expr &expr); - Address addressOf(const clang::Expr &expr); - core::Sym load(const Address &address, clang::QualType type, - const clang::Expr *at); - void store(const Address &address, core::Sym value, clang::QualType type, - const clang::Expr *at, const std::string &holderName = {}); - core::Sym constant(std::int64_t value, clang::QualType type); - /// RFC 0017: an integer constant of `type` whatever its width, kept as - /// the symbol's interval when the zone's 64-bit bounds cannot hold it. - core::Sym constant(const llvm::APSInt &value, clang::QualType type); - core::Sym unknownValue(clang::QualType type); - core::Sym pointerTo(const Address &address, clang::QualType pointee, - std::string name); - core::Sym nullPointer(clang::QualType type); - [[nodiscard]] std::optional - integerType(clang::QualType type) const; - /// A definite store of a summary's `store` to the bytes `[start, end)` - /// of `target`'s object that lie outside it: an error at the call, which - /// counts the bytes behind the argument (at `pointer` in the object). - void storePastObject(const clang::CallExpr &call, - const core::StoreEffect &store, - const core::Target &target, const core::Term &pointer, - __int128 start, __int128 end); - /// A summary's integer (`desc`) as `value`'s interval, where its `[lo, hi]` - /// cannot say it. - void boundByRange(core::Sym value, clang::QualType type, - const core::ValueDesc &desc); - /// RFC 0017 §3: the size `left * right` of a checked-product allocation - /// (`calloc`, `reallocarray`): none when the product overflows `type` for - /// every value (the allocation fails); else the C product, which the - /// allocation's success makes the mathematical one. - std::optional checkedProduct(core::Sym left, core::Sym right, - clang::QualType type, - const clang::Expr &at); - /// `p + i * size` (or minus). - core::Sym pointerAdd(core::Sym pointer, core::Sym index, std::int64_t size, - bool subtract, const clang::Expr &at); - /// The byte width of `type`, if complete. - [[nodiscard]] std::optional sizeOf(clang::QualType type) const; - /// RFC 0015 §4: the scalar cells (pointer and integer leaves) of a value - /// of `type`, by offset; false when they are not all known (a union, a - /// bit-field, a large or incomplete member). - bool recordLeaves( - clang::QualType type, - std::vector> &out) const; - /// A record's value copied leaf by leaf from `from` to `to`, every leaf - /// read before any is written; false when its leaves are not known. - bool copyRecord(const Address &to, const Address &from, clang::QualType type); - /// Names the allocations a record's pointer members own after them. - void nameHeldByMembers(core::ObjectId object, clang::QualType type, - const std::string &prefix); - /// The spelling of an expression for messages (`q->a`, `buf`). - [[nodiscard]] std::string spell(const clang::Expr &expr) const; - /// The integer term a value stands for (its linear form). - [[nodiscard]] core::Term termOf(core::Sym sym) const; - - // Calls (EngineCalls.cpp). - core::Sym call(const clang::CallExpr &call); - /// RFC 0031 §6.6: re-analyses a callee whose arguments share objects, - /// and reports what it finds at the call. - void checkAliasContext(const clang::CallExpr &call, - const clang::FunctionDecl &callee, - const std::vector &args); - /// §6.6 *Amendment (numeric contexts)*: the summary of `callee`, a - /// function of the unit outside the caller's component, derived for this - /// call's context (the objects its arguments share, the integers it knows) - /// when `general` depends on them; none when the general one stands. - const core::FunctionEffects * - contextSummary(const clang::CallExpr &call, const clang::FunctionDecl &callee, - const std::vector &args, - const core::FunctionEffects &general); - /// §7 *Amendment (cross-unit contexts)*: for a call into another unit, - /// the callee's summary in the call's context when its unit ran it; - /// otherwise none, and the context is asked for. - const core::FunctionEffects * - remoteContext(const clang::CallExpr &call, const clang::FunctionDecl &callee, - const std::vector &args, - const core::FunctionEffects &general); - /// The same for a function of another unit known by its portable name, - /// over parameters of the types `shape`. - const core::FunctionEffects * - remoteContext(const clang::CallExpr &call, const std::string &callee, - const std::vector &shape, - const std::vector &args, - const core::FunctionEffects &general); - /// Applies a callee's summary at `call` (EngineSummary.cpp, §6.3). - /// Code the analysis does not see may write any global: each forgets - /// what it holds, and what it reaches may have been released by `record` - /// (the C library's own globals keep their values). - void forgetGlobals(const core::ReleaseRecord &record); - /// RFC 0030 §5.1 for one pointer argument `pointer` of `call`: code the - /// analysis does not see may write what it reaches (not the first object - /// when `constPointee`) and release what may be released. - void havocArgument(const clang::CallExpr &call, core::Sym pointer, - bool constPointee, bool callback); - core::Sym pathValue(core::Sym at, const core::ValueDesc &desc, - clang::QualType type); - core::Sym instantiate(const clang::CallExpr &call, - const core::FunctionEffects &effects, - const std::vector &args); - - // Decisions (EngineDecide.cpp). - void decideSites(const clang::Stmt &stmt); - /// What an access leaves known of its pointer (every pass). - void accessed(const clang::Stmt &stmt); - void decideExitSite(const clang::Stmt &stmt, bool isReturn); - /// A call's own sites, from the state before its effects. - void decideCall(const clang::CallExpr &call, - const std::vector &args, core::Sym calleeValue); - /// The requirement records of a call's arguments (EngineLibrary.cpp). - void decideArguments(const clang::CallExpr &call, const SiteInfo &site, - const std::vector &requirements, - const std::vector &args, bool library); - /// The kinds' requirements at a call (EngineKinds.cpp, §7.3–§7.5). - void decideCallKinds(const clang::CallExpr &call, const SiteInfo &site, - const std::vector &args); - /// An element of `main`'s argv. - [[nodiscard]] bool isArgvElement(const clang::Expr &pointer) const; - /// §7.5 covered accesses and the argv contract: a decision a kind - /// strengthens (EngineKinds.cpp). - [[nodiscard]] core::FacetDecision covered(const SiteInfo &site, - core::Facet facet, - core::FacetDecision decision) const; - /// §7.5 for a call's argument: the parameter's inferred requirement - /// covers what the call needs of it (EngineKinds.cpp). - [[nodiscard]] std::optional - coveredArgument(const clang::CallExpr &call, core::Sym pointer, - bool string) const; - /// A library call's requirement records (EngineLibrary.cpp). - void decideLibraryCall(const clang::CallExpr &call, const SiteInfo &site, - const std::vector &args); - /// RFC 0030 §9.3: the slot solution's answer for an indirect call. - [[nodiscard]] std::optional - slotResolution(const clang::CallExpr &call) const; - /// The functions `call` may reach: its direct callee, the functions its - /// callee value names, or a solved slot's (RFC 0030 §9.3). - [[nodiscard]] std::vector - callTargets(const clang::CallExpr &call, core::Sym calleeValue) const; - /// Whether a function `call` may reach reads or writes through its - /// argument `index` (the summary's `reads` and `writes`), or cannot say. - /// RFC 0033 §1: whether every function the call may reach has a - /// complete summary that neither reads, writes nor releases through - /// argument `index`. - [[nodiscard]] bool usesValueOnly(const clang::CallExpr &call, - core::Sym calleeValue, - const std::vector &args, - unsigned index) const; - [[nodiscard]] bool calleeTouches(const clang::CallExpr &call, - core::Sym calleeValue, unsigned index) const; - /// Whether the call's callee releases the object argument `index` points - /// to on every path (its summary), so a use of a released one there is a - /// double release. - [[nodiscard]] bool calleeReleases(const clang::CallExpr &call, - core::Sym calleeValue, - const std::vector &args, - unsigned index, - bool possibly = false) const; - /// The functions a resolution names. - [[nodiscard]] std::vector - slotTargets(const core::CallResolution &resolution) const; - /// RFC 0030 §9.1: what the state knows of the function's unmodified - /// integer parameters (zero or not), for a release's record. - [[nodiscard]] std::vector> paramGuard() const; - /// The fixed pointer locals that hold a non-null value here (sorted). - [[nodiscard]] std::vector nonNullLocals() const; - [[nodiscard]] std::vector pairGuard() const; - /// Whether `address` names two objects of which each path has exactly - /// one (an entry object and the object made where its pointer was null). - [[nodiscard]] bool complementary(const Address &address) const; - /// RFC 0014: whether the arguments a pair test names compare equal, when - /// the state decides it. - [[nodiscard]] std::optional - argumentsEqual(const std::vector &args, - const core::ParamPairTest &test) const; - /// §5.7: a pointer a callee left to its own frame storage: the object - /// may have ended, and a use of it is not proven (EngineLifetimes.cpp). - core::Sym danglingValue(const clang::CallExpr &call, clang::QualType type, - const core::SummaryPath &path); - /// RFC 0008: a callee's summary releases `value` (the caller's value at - /// `path`): a pointer to storage that is no heap object is an - /// `invalid-release` at the call (EngineLifetimes.cpp). - void calleeRelease(const clang::CallExpr &call, core::Sym value, - const core::SummaryPath &path, bool certain); - /// A callee's summary releases `value` (the caller's value at `path`, - /// below an argument or a global) that the caller already released: a - /// double release at the call. - /// §7 *Amendment (cross-unit contexts)*: a function value a summary - /// describes. - core::Sym functionValue(const core::ValueDesc &desc); - /// Whether an earlier release of `value` is one still pending on a null - /// result of the call that made the argument `path` goes through. - [[nodiscard]] bool - pendingOnNullArgument(const std::vector &args, core::Sym value, - const core::SummaryPath &path, - const core::SourceLocation &where) const; - // Strings (EngineStrings.cpp, RFC 0012 *String facts*). - enum class Byte : std::uint8_t { Zero, NonZero, Unknown }; - struct StringFacts { - enum class Length : std::uint8_t { Unknown, Exact, AtMost }; - Length length = Length::Unknown; - /// `strlen` of the pointer, exactly or at most. - core::Term term = core::Term::unknown(); - /// The offset in the object of the NUL that ends it. - core::Term nulAt = core::Term::unknown(); - /// No byte from the pointer to the object's constant end is NUL. - bool unterminated = false; - }; - /// A `printf`-family call's literal format (RFC 0030 §8.2). - struct FormatFacts { - bool literal = false; - std::optional reads = std::nullopt; - unsigned passed = 0; - /// The least output without its NUL, and whether it is exact. - std::int64_t lower = 0; - bool exact = false; - /// The call arguments `%s` reads to their terminator. - std::vector strings; - /// RFC 0033 §5: the call arguments a `%.Ns` reads at most N bytes of; - /// they need no terminator. - std::vector bounded; - }; - /// The byte a value stored as `width` bytes puts first in memory. - [[nodiscard]] Byte valueByte(core::Sym sym, - std::optional width) const; - [[nodiscard]] Byte byteAt(core::ObjectId id, std::int64_t offset) const; - [[nodiscard]] StringFacts stringFacts(core::Sym pointer) const; - /// `strlen(pointer)` after a call measured it: the known length, or a new - /// length symbol the object's string fact records. - core::Term measureString(core::Sym pointer, const clang::Expr *argument); - /// A row's `writes-str`: the string at `pointer` has `length` characters. - void noteStringWritten(core::Sym pointer, const core::Term &length); - /// A store of `value` (`width` bytes) at `offset` into `object`. - void stringStored(core::ObjectId id, const core::Term &offset, - std::int64_t width, core::Sym value); - [[nodiscard]] FormatFacts formatFacts(const clang::CallExpr &call, - const core::LibraryMatch &match) const; - - /// RFC 0030 §3.4, RFC 0031 §5.5: whether releasing `pointer` releases - /// the start of heap objects, with the message when it may not. - struct ReleaseCheck { - enum class Kind : std::uint8_t { - Proven, - UnknownIndex, - Possible, - Violation - }; - Kind kind = Kind::Proven; - std::string message; - std::string note; - core::SourceLocation noteAt = {}; - }; - /// `anywhere`: the release takes a range (`munmap`, RFC 0034 §6.3), so - /// a pointer into a heap object need not point to its start. - [[nodiscard]] ReleaseCheck releaseCheck(core::Sym pointer, - const std::string &subject, - const clang::Expr &operand, - const std::string &verb, - bool anywhere = false) const; - /// A path below a call's argument as the caller spells it (`b.data` for - /// `param0->data` with `&b`, `b->data` with `b`), when it can. - [[nodiscard]] std::optional - spellArgumentPath(const clang::CallExpr &call, - const core::SummaryPath &path) const; - /// RFC 0030 §5.3: the functions a callback argument's value may be; empty - /// when it is not known. - [[nodiscard]] std::vector - syncTargets(core::Sym function) const; - /// RFC 0031 §6.3: a callee's release through `valuePath` that the - /// caller's memory cannot follow; applies the unknown-callee default to - /// what the argument reaches and says so on the call. False when the path - /// does not start at a pointer argument. - bool lostView(const clang::CallExpr &call, const std::vector &args, - const core::SummaryPath &valuePath); - /// RFC 0008: `value`, read from `lvalue` at `address`, is a local - /// pointer no path assigned. - void uninitialisedRead(core::Sym value, const clang::Expr &lvalue, - const Address &address); - // Null findings (EngineDecide.cpp, RFC 0008, RFC 0030 §3.2). - /// Adds the note saying why `value` (spelled `name`) is null. - void addNullNote(core::Diagnostic &diagnostic, const core::SymInfo &value, - const std::string &name) const; - /// The `null-dereference` diagnostic for a null argument `arg` a callee - /// (spelled `callee`, declared at `declared` when valid) dereferences. - [[nodiscard]] core::Diagnostic - nullArgument(const clang::Expr &arg, const core::SymInfo &value, - const std::string &callee, - const clang::FunctionDecl *declared) const; - /// RFC 0030 §3.2: a use of an allocation's result that may be null (the - /// off-by-default `allocation-failure`), reported at `at`. - void allocationFailure(const core::SymInfo &value, const clang::Expr &at, - const clang::Stmt &site); - // Annotation mismatches (EngineAnnotations.cpp, RFC 0003, RFC 0012). - void reportMismatch(std::string message, clang::SourceLocation at, - std::string note, clang::SourceLocation noteAt, - std::string copy, const clang::Stmt &site, - std::optional facet = core::Facet::Temporal); - void checkConsumeAnnotation(core::Sym value, const clang::Expr &operand, - const clang::Stmt &site, bool moved); - void checkWriteAnnotation(const Address &address, const clang::Stmt &site); - void checkCallAnnotations(const clang::CallExpr &call, - const std::vector &args); - void checkReturnAnnotation(core::Sym value, const clang::Expr &returned); - void checkSizedFieldStore(const clang::MemberExpr &lhs, - const Address &address); - /// An amount of bytes for messages: `6 bytes`, `'strlen(s)' + 1 bytes`; - /// `own` prefers the name a value was made under to the place holding it. - [[nodiscard]] std::optional spellAmount(core::Term bytes, - bool own) const; - /// A C place holding `sym` at this point, for messages (§5.3); for an - /// `extent` (a have), also its defining operation. - [[nodiscard]] std::optional - nameOf(core::Sym sym, bool extent = false, int depth = 0) const; - - // Lifetimes, boundaries and assumptions (EngineLifetimes.cpp). - /// §5.7, §5.6: the exit of the function at `exit` (a `return` or the end - /// of the body): frame storage left where the caller can find it. - void exitLifetimes(const clang::Stmt &exit, const clang::ReturnStmt *ret); - /// §5.6: the boundary facts of a call, from the state before its effects. - void callBoundary(const clang::CallExpr &call, - const std::vector &args); - /// RFC 0030 §5.7: an `asm` statement applies the unknown-callee default - /// to its pointer operands. - void inlineAssembly(const clang::GCCAsmStmt &assembly); - /// The call a `cleanup` attribute makes when `var`'s scope ends. - void cleanupFunction(const clang::VarDecl &var); - /// What an unknown callee may do to the objects reachable from `start`; - /// with `mayOwn`, it may have taken over what they own. - void unknownEffect(const std::vector &start, - const core::ReleaseRecord &record, bool mayOwn); - /// A truth value (a comparison, a logical operator, `!`, a scalar) with a - /// condition refinement can decide, evaluated afresh here; none when the - /// expression is none of these. - std::optional truthValue(const clang::Expr &expr); - /// A comparison evaluated afresh from its operands' values here. - core::Sym comparison(const clang::BinaryOperator &op) { - return compare(op.getOpcode(), valueOf(*op.getLHS()), valueOf(*op.getRHS()), - op.getLHS()->getType()); - } - /// RFC 0030 §6.2: a `WEAVEC_ASSUME` call; false when `call` is not one. - bool assumption(const clang::CallExpr &call); - /// §5.2, §5.7: a use of `operand` whose value may point to storage whose - /// lifetime ended: `lifetime-too-short`, reported where the pointer was - /// stored. - /// RFC 0004 *Laundering*: a raw `value` stored into a place declared - /// with a safe kind (or returned from a function whose result is) asserts - /// that kind, which needs an unsafe region; the place holds a value that - /// is no longer raw. `target` names the place, or is empty for a return. - core::Sym launder(core::Sym value, const AnnotationSet &declared, - const std::string &target, const clang::Expr &source); - void danglingUse(const clang::Expr &operand, - const core::TemporalVerdict &verdict, - const clang::Stmt *site, bool definite); - - [[nodiscard]] core::HeapState &heapState() noexcept { return state; } - [[nodiscard]] FunctionRun &functionRun() noexcept { return run; } - [[nodiscard]] core::Heap &domain() noexcept { return heap; } - -private: - FunctionRun &run; - core::Heap &heap; - core::HeapState &state; - clang::ASTContext &context; - llvm::DenseMap memo; - /// The next element starts a statement. - bool statementStart = true; - - ExprResult evaluateUncached(const clang::Expr &expr); - core::Sym evaluateCast(const clang::CastExpr &cast); - core::Sym evaluateUnary(const clang::UnaryOperator &op); - core::Sym evaluateBinary(const clang::BinaryOperator &op); - core::Sym evaluateAssign(const clang::BinaryOperator &op); - core::Sym arithmetic(clang::BinaryOperatorKind kind, core::Sym left, - core::Sym right, clang::QualType type, - const clang::Expr &at); - /// RFC 0017: `value` of C type `from` converted to `to` (modular). - core::Sym convertInteger(core::Sym value, clang::QualType from, - clang::QualType to); - /// The same, to the integer type `target` (a bit-field's width) of a - /// value of C type `to`. - core::Sym convertInteger(core::Sym value, clang::QualType from, - const core::IntegerType &target, clang::QualType to); - /// RFC 0017: a bit-field holds its declared width. One whose bytes no - /// other member shares is a cell of its own; the others are not tracked - /// (their stores forget the bytes, their loads are unknown in the width). - [[nodiscard]] std::optional - bitFieldType(const clang::FieldDecl &field) const; - core::Sym loadBitField(const Address &address, clang::QualType type, - const clang::FieldDecl &field, const clang::Expr *at); - /// Stores `value` converted to the bit-field's width; returns the value - /// stored (the assignment's value). - core::Sym storeBitField(const Address &address, core::Sym value, - clang::QualType type, const clang::FieldDecl &field, - const clang::Expr *at); - /// RFC 0017: `__builtin_*_overflow(a, b, out)`: the value stored through - /// `out` and the overflow flag (the call's `result`, refined). - core::Sym checkedArithmetic(const clang::CallExpr &call, core::IntegerOp op, - const std::vector &args, - core::Sym result); - /// RFC 0017, RFC 0031 §5.10: an operation invalid for every value. - void reportInteger(core::IntegerError error, const clang::Expr &at); - core::Sym compare(clang::BinaryOperatorKind kind, core::Sym left, - core::Sym right, clang::QualType operandType); - void declare(const clang::VarDecl &var); - /// RFC 0007 *Owned fields*: at a release of `operand`, what the entry - /// container's `WEAVEC_OWNED` fields hold is owned (so leaked unless - /// released or moved first). - void declaredOwners(const clang::Expr &operand, const std::string &released); - void initialize(const Address &address, clang::QualType type, - const clang::Expr *init); - void zeroFill(const Address &address, clang::QualType type); - void lifetimeEnds(const clang::VarDecl &var); - void returned(const clang::ReturnStmt &ret); - void checkLeaks(core::Sym overwritten, const clang::Stmt &at); - /// §5.7: notes where a pointer to frame storage was stored into a cell - /// (EngineLifetimes.cpp). - void recordFrameStore(core::ObjectId holder, core::CellKey key, - core::Sym value, const clang::Expr *at, - const std::string &holderName); -}; - -/// Everything the unit's functions share: summaries, the owning slots, the -/// library, the kinds, the options. -class UnitRun { -public: - UnitRun(const EngineInput &engineInput, LedgerAdapter &adapter); - - void analyzeAll( - const std::function &shouldReport); - [[nodiscard]] UnitExports exports(); - void dump(const clang::FunctionDecl &function, llvm::raw_ostream &os); - - const EngineInput &input; - LedgerAdapter &authoritative; - /// A discarding adapter for summary rounds. - LedgerAdapter discarding; - /// Alias contexts already run, per callee (§6.6), and their count. - std::map> contextsRun; - /// RFC 0034 §6.1: what each context run found, for the replay of a - /// caller, which does not run a context twice. - std::map, - std::vector> - contextFindings; - /// §6.6 *Amendment (numeric contexts)*: the summaries derived per callee - /// and context (none: the run was over budget or incomplete). - std::map>> - contextSummaries; - /// The members of the recursive component being summarised, whose - /// summaries are not final. - std::set unsettled; - /// The block transfers each function's last run took (what a context - /// run of it may cost). - std::map transfersOf; - /// RFC 0034 §7.2: the work each function's last run took. - std::map workOf; - /// RFC 0033 §1: `onlyValueUses` by definition, parameter and depth. - std::map, bool> - valueOnly; - /// RFC 0033 §9, RFC 0034 §7.1: the unit's budget of work (0: - /// unlimited), what every run in it has spent, and the definitions whose - /// authoritative run is still to come. - std::uint64_t unitBudget = 0; - std::uint64_t unitWork = 0; - std::uint64_t functionsLeft = 1; - /// A run's share of the work left: eight fair shares (0 unlimited). - [[nodiscard]] std::uint64_t runShare() const { - static constexpr std::uint64_t MinRunShare = 100000; - const std::uint64_t left = unitBudget - std::min(unitBudget, unitWork); - return unitBudget == 0 - ? 0 - : std::max(MinRunShare, - 8 * left / std::max(1, functionsLeft)); - } - /// Summaries of the unit's definitions, by canonical declaration. - std::map summaries; - std::set incomplete; - /// Members of a recursive component whose summary round spent the - /// per-function budget (RFC 0033 *Implementation amendments*). - std::set exhausted; - /// §9.4, §4.5 D2: fields and globals some function releases a value - /// loaded from. - llvm::DenseSet owningSlots; - /// Over-budget functions. - std::set overBudget; - - [[nodiscard]] clang::ASTContext &context() const { return input.context; } - [[nodiscard]] const core::LibrarySpec &library() const { - return input.library; - } - /// The summary of `callee` from this unit or the program database. - [[nodiscard]] const core::FunctionEffects * - summaryOf(const clang::FunctionDecl &callee) const; - /// Whether `callee` has a body in this unit. - [[nodiscard]] bool hasBody(const clang::FunctionDecl &callee) const; - /// The summary id of a global variable, and back (RFC 0005 `global(g)`). - std::uint32_t globalId(const clang::VarDecl &var); - [[nodiscard]] const clang::VarDecl *globalDecl(std::uint32_t id) const; - [[nodiscard]] const std::vector &globals() const { - return globalList; - } - - /// The name another unit knows a global by: its own for external - /// linkage, `#` otherwise (which no other unit has). - [[nodiscard]] std::string portableName(const clang::VarDecl &var) const; - /// RFC 0005: the unit's functions of `typeKey` whose address is taken, - /// the candidates of an indirect call nothing else resolves in the - /// program. - [[nodiscard]] const std::vector & - localCandidates(const std::string &typeKey) const; - /// §4.6: a variable with static storage and internal linkage that the - /// unit only reads scalars of (its address is never taken, no store - /// names it): every cell holds its initializer's value. - [[nodiscard]] bool keepsInitializer(const clang::VarDecl &var) const; - - /// §7 *Amendment (cross-unit contexts)*: the name another unit knows a - /// function by, and the unit's function of that name. - [[nodiscard]] std::string portableName(const clang::FunctionDecl &fn) const; - [[nodiscard]] const clang::FunctionDecl * - functionNamed(const std::string &portable) const; - /// The unit's global of a portable name. - [[nodiscard]] const clang::VarDecl * - globalNamed(const std::string &portable) const; - /// A summary of the program database (`key` names it in the cache), in - /// this unit's global numbering. - [[nodiscard]] const core::FunctionEffects * - importEffects(const std::string &key, - const core::FunctionEffects &effects) const; - /// The contexts this unit's calls asked of other units' functions, and - /// the summaries of its own functions in the contexts others asked for. - std::set contextRequests; - std::map servedContexts; - /// Runs the contexts other units asked of this unit's functions: their - /// summaries, and (for aliased arguments) their diagnostics. - void serveContextRequests( - const std::function &shouldReport); - - /// RFC 0030 §7.6 (RFC 0031 *Implementation amendments*, "Counted-field - /// invariants"): the candidates standing, the kinds they give their - /// pointer fields while assumed, and those a checking run refuted. - std::vector standing; - std::map assumedFields; - std::map - assumedCandidate; - std::set refuted; - std::set witnessed; - /// The kind of `field`: its table entry, or an assumed invariant's when - /// the entry gives no extent. - [[nodiscard]] const KindEntry *fieldKind(const clang::FieldDecl &field) const; - /// Houdini over the candidates (checking runs of the functions that write - /// their fields), then the functions that read a standing invariant's - /// pointer field analysed again with it. - void inferInvariants( - const std::vector &definitions, - const std::function &shouldReport); - -private: - mutable std::optional> - functionsByName; - mutable std::optional> initializerOnly; - // Interned lazily, also while a const lookup imports a summary. - mutable std::vector globalList; - mutable llvm::DenseMap globalIds; - std::uint32_t internGlobal(const clang::VarDecl &var) const; - void computeOwningSlots(); - /// §7: summaries imported from the program database, by callee name, in - /// this unit's global numbering. - mutable std::map imported; - /// The unit's global variables by portable name. - mutable std::optional> - globalsByName; - mutable std::optional< - std::map>> - candidatesByType; -}; - -} // namespace weavec::analysis::engine - -#endif // WEAVEC_LIB_ANALYSIS_ENGINE_H diff --git a/lib/Analysis/EngineAnnotations.cpp b/lib/Analysis/EngineAnnotations.cpp deleted file mode 100644 index 49faf114..00000000 --- a/lib/Analysis/EngineAnnotations.cpp +++ /dev/null @@ -1,377 +0,0 @@ -//===- EngineAnnotations.cpp - Annotation mismatches (object engine) ------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// `annotation-mismatch` (RFC 0030 §3.4: always an error) in the object -// domain: a definition checked against its own declaration. -// -// - RFC 0003 *Reconciliation*: a `WEAVEC_BORROWED` or `WEAVEC_MUT` -// parameter's object released or moved, a `WEAVEC_BORROWED` one written -// through, and a result annotated `WEAVEC_OWNED` that is a borrow or -// `WEAVEC_BORROWED` that is a fresh allocation. Callers keep trusting the -// annotation; the definition is where the error is. -// - RFC 0012 *Sized fields*, "Stores": a pointer field declared -// `WEAVEC_SIZED_BY(n)` or `WEAVEC_COUNTED_BY(n)` given an object that the -// sibling count says is larger than it is exactly. -// -// A parameter's object is the entry object its value points to (`param(i)*`, -// RFC 0031 §4.6), whatever copy of the pointer reaches it. -// -//===----------------------------------------------------------------------===// - -#include "Engine.h" -#include "weavec/Analysis/Annotations.h" -#include "weavec/Analysis/ClangLocation.h" -#include "weavec/Analysis/KindTable.h" - -#include "clang/AST/RecordLayout.h" -#include "clang/Basic/SourceManager.h" - -using namespace clang; - -namespace weavec::analysis::engine { - -/// The parameter whose object `object` is (`param(i)*`), or the one it is -/// reached from through that object (`param(i)*.data*`, `below`). -static const ParmVarDecl *parameterOf(const FunctionDecl &function, - const core::ObjectInfo &info, - bool &below) { - below = false; - if (info.key.kind != core::ObjectKind::Entry || !info.key.path.isParam() || - info.key.path.steps.empty() || - info.key.path.steps.front().step != core::PathStep::Deref || - info.key.path.index >= function.getNumParams()) - return nullptr; - below = info.key.path.steps.size() > 1; - return function.getParamDecl(info.key.path.index); -} - -static std::string_view macroOf(const AnnotationSet &set) { - return set.borrowed ? "WEAVEC_BORROWED" : "WEAVEC_MUT"; -} - -void Transfer::reportMismatch(std::string message, SourceLocation at, - std::string note, SourceLocation noteAt, - std::string copy, const Stmt &site, - std::optional facet) { - core::Diagnostic diagnostic; - diagnostic.id = core::diag::AnnotationMismatch; - diagnostic.severity = core::Severity::Error; - diagnostic.message = std::move(message); - const SourceManager &sm = context.getSourceManager(); - diagnostic.location = toCoreLocation(sm, at); - if (noteAt.isValid()) - diagnostic.addNote(std::move(note), toCoreLocation(sm, noteAt)); - if (!copy.empty()) - diagnostic.addNote(std::move(copy), toCoreLocation(sm, at)); - run.report(std::move(diagnostic), core::Certainty::Definite, &site, facet); -} - -void Transfer::checkConsumeAnnotation(core::Sym value, const Expr &operand, - const Stmt &site, bool moved) { - if (!run.isPublishing()) - return; - const core::SymInfo &info = heap.info(state, value); - if (info.type != core::SymInfo::Type::Pointer || info.top || - info.targets.size() != 1 || info.null == core::PointerNull::Null) - return; - bool below = false; - const ParmVarDecl *param = - parameterOf(run.decl(), run.table().info(info.targets[0].object), below); - if (param == nullptr) - return; - SignatureAnnotations signature = collectAnnotations(run.decl()); - unsigned index = param->getFunctionScopeIndex(); - if (index >= signature.params.size()) - return; - const AnnotationSet &set = signature.params[index]; - if (!set.borrowed && !set.mutBorrowed) - return; - std::string name = param->getNameAsString(); - std::string verb = moved ? "moved" : "freed"; - std::string spelled = spell(operand); - if (below) { - // Releasing what the borrowed object owns mutates it. - if (!set.borrowed) - return; - reportMismatch("'" + name + "' is annotated " + std::string(macroOf(set)) + - " but '" + spelled + "' is " + verb + " here", - site.getBeginLoc(), "'" + name + "' is annotated here", - param->getLocation(), "", site); - return; - } - if (!info.targets[0].offset.isConstant() || - info.targets[0].offset.constant != 0) - return; - reportMismatch("'" + name + "' is annotated " + std::string(macroOf(set)) + - " but is " + verb + " here", - site.getBeginLoc(), "'" + name + "' is annotated here", - param->getLocation(), - spelled != name - ? "'" + spelled + "' is a copy of '" + name + "'" - : std::string(), - site); -} - -void Transfer::checkWriteAnnotation(const Address &address, const Stmt &site) { - if (!run.isPublishing() || address.top || address.targets.size() != 1) - return; - bool below = false; - const ParmVarDecl *param = parameterOf( - run.decl(), run.table().info(address.targets[0].object), below); - if (param == nullptr || below) - return; - SignatureAnnotations signature = collectAnnotations(run.decl()); - unsigned index = param->getFunctionScopeIndex(); - if (index >= signature.params.size() || !signature.params[index].borrowed) - return; - std::string name = param->getNameAsString(); - reportMismatch("'" + name + - "' is annotated WEAVEC_BORROWED but is written " - "through here", - site.getBeginLoc(), "'" + name + "' is annotated here", - param->getLocation(), "", site); -} - -void Transfer::checkCallAnnotations(const CallExpr &call, - const std::vector &args) { - const FunctionDecl *callee = call.getDirectCallee(); - if (callee == nullptr || !run.isPublishing()) - return; - SignatureAnnotations signature = collectAnnotations(*callee); - for (unsigned i = 0; - i < call.getNumArgs() && i < args.size() && i < signature.params.size(); - ++i) { - const AnnotationSet &set = signature.params[i]; - if (!call.getArg(i)->getType()->isPointerType()) - continue; - if (set.owned) { - checkConsumeAnnotation(args[i], *call.getArg(i), call, /*moved=*/true); - continue; - } - if (set.mutBorrowed) { - const core::SymInfo &value = heap.info(state, args[i]); - if (value.type != core::SymInfo::Type::Pointer || value.top) - continue; - Address address; - address.targets = value.targets; - checkWriteAnnotation(address, call); - } - } -} - -void Transfer::checkReturnAnnotation(core::Sym value, const Expr &returned) { - if (!run.isPublishing() || value == core::ZeroSym) - return; - SignatureAnnotations signature = collectAnnotations(run.decl()); - const AnnotationSet &result = signature.result; - bool promisesBorrow = result.borrowed || result.mutBorrowed; - if (!result.owned && !promisesBorrow) - return; - const core::SymInfo &info = heap.info(state, value); - if (info.type != core::SymInfo::Type::Pointer || info.top || - info.targets.empty() || info.null == core::PointerNull::Null) - return; - bool borrow = true; - bool fresh = true; - for (const core::Target &target : info.targets) { - const core::ObjectInfo &object = run.table().info(target.object); - const core::ObjectState *objectState = - heap.findObject(state, target.object); - // A borrow: the object of a parameter the caller keeps (`b`, - // `&b->data`), a global or a literal. - bool below = false; - const ParmVarDecl *param = parameterOf(run.decl(), object, below); - bool interior = param != nullptr && !below && - param->getFunctionScopeIndex() < signature.params.size() && - !signature.params[param->getFunctionScopeIndex()].owned; - borrow = - borrow && (interior || object.key.kind == core::ObjectKind::Global || - object.key.kind == core::ObjectKind::Literal); - fresh = fresh && - (object.key.kind == core::ObjectKind::HeapRecent || - object.key.kind == core::ObjectKind::HeapOld) && - objectState != nullptr && objectState->owned && - !objectState->escaped; - } - std::string message; - if (result.owned && borrow) - message = "function returns a borrow but its return type is annotated " - "WEAVEC_OWNED"; - else if (promisesBorrow && fresh) - message = std::string("function returns a fresh allocation but its return " - "type is annotated ") + - (result.borrowed ? "WEAVEC_BORROWED" : "WEAVEC_MUT"); - else - return; - reportMismatch(std::move(message), returned.getBeginLoc(), "annotated here", - run.decl().getLocation(), "", returned); -} - -//===----------------------------------------------------------------------===// -// Malformed annotations (RFC 0003, RFC 0010, RFC 0012) -//===----------------------------------------------------------------------===// - -void FunctionRun::validateAnnotations() { - const SourceManager &sm = context.getSourceManager(); - auto warn = [&](std::string message, SourceLocation at) { - core::Diagnostic diagnostic; - diagnostic.id = core::diag::InvalidAnnotation; - diagnostic.severity = core::Severity::Warning; - diagnostic.message = std::move(message); - diagnostic.location = toCoreLocation(sm, at); - out.report(std::move(diagnostic), core::Certainty::Possible); - }; - // `WEAVEC_OWNED_BY` needs `WEAVEC_OWNED`; retaining and releasing one - // argument contradict each other. - auto contradictions = [&](const NamedDecl &decl, const AnnotationSet &set) { - if (set.retains && set.releases) - warn("'" + decl.getNameAsString() + - "' is declared both WEAVEC_RETAINS and WEAVEC_RELEASES", - decl.getLocation()); - if (!set.family.empty() && !set.owned) - warn("'" + decl.getNameAsString() + "' is declared WEAVEC_OWNED_BY(" + - set.family + ") without WEAVEC_OWNED", - decl.getLocation()); - }; - const AnnotationSet annotations = getAnnotations(function); - if (annotations.invalid) - warn("unrecognised weavec annotation on '" + function.getNameAsString() + - "'", - function.getLocation()); - contradictions(function, annotations); - // `weavec.assume` belongs to the header's `weavec_assume_` alone. - if (annotations.assume && function.getName() != "weavec_assume_") - warn("'weavec.assume' is not an annotation for '" + - function.getNameAsString() + "'", - function.getLocation()); - for (const ParmVarDecl *param : function.parameters()) { - const AnnotationSet onParam = getAnnotations(*param); - contradictions(*param, onParam); - if (onParam.invalid) - warn("unrecognised weavec annotation on '" + param->getNameAsString() + - "'", - param->getLocation()); - } -} - -//===----------------------------------------------------------------------===// -// Sized fields (RFC 0012) -//===----------------------------------------------------------------------===// - -void Transfer::checkSizedFieldStore(const MemberExpr &lhs, - const Address &address) { - if (!run.isPublishing() || address.top || address.targets.size() != 1 || - !address.targets[0].offset.isConstant()) - return; - const auto *stored = dyn_cast(lhs.getMemberDecl()); - if (stored == nullptr) - return; - const RecordDecl *record = stored->getParent(); - if (record == nullptr || !record->isCompleteDefinition() || record->isUnion()) - return; - const KindTable &kinds = run.unitRun().input.kinds; - // The annotated pointer field and its count: the stored field is one. - const FieldDecl *pointerField = nullptr; - const FieldDecl *countField = nullptr; - const KindEntry *kind = nullptr; - for (const FieldDecl *field : record->fields()) { - const KindEntry *entry = kinds.field(*field); - if (entry == nullptr || !entry->hasDeclaredShape() || - entry->shapeFromSystemHeader() || - (entry->kind.shape != core::PointerShape::Sized && - entry->kind.shape != core::PointerShape::Counted) || - entry->kind.extent.isConstant() || - entry->kind.extent.path->root != core::ExtentPath::Root::Field) - continue; - const FieldDecl *count = nullptr; - for (const FieldDecl *sibling : record->fields()) - if (sibling->getName() == entry->kind.extent.path->field) - count = sibling; - if (count == nullptr || !count->getType()->isIntegerType()) - continue; - if (field == stored || count == stored) { - pointerField = field; - countField = count; - kind = entry; - break; - } - } - if (kind == nullptr) - return; - const ASTRecordLayout &layout = context.getASTRecordLayout(record); - auto offsetOf = [&](const FieldDecl &field) { - return static_cast( - layout.getFieldOffset(field.getFieldIndex()) / context.getCharWidth()); - }; - core::ObjectId object = address.targets[0].object; - std::int64_t base = address.targets[0].offset.constant - offsetOf(*stored); - auto pointer = heap.read( - state, object, core::CellKey{.offset = base + offsetOf(*pointerField)}); - auto count = heap.read(state, object, - core::CellKey{.offset = base + offsetOf(*countField)}); - if (!pointer || !count) - return; - const core::SymInfo &value = heap.info(state, *pointer); - if (value.type != core::SymInfo::Type::Pointer || value.top || - value.null == core::PointerNull::Null || value.targets.size() != 1 || - !(value.targets[0].offset == core::Term::of(0))) - return; - const core::ObjectState *target = - heap.findObject(state, value.targets[0].object); - if (target == nullptr || !target->extent || !target->extent->bytes.known || - target->extent->cls != core::ExtentClass::Exact) - return; - std::int64_t unit = 1; - if (kind->kind.shape == core::PointerShape::Counted) { - QualType pointee = pointerField->getType()->getPointeeType(); - if (pointee.isNull() || pointee->isIncompleteType() || - pointee->isVoidType()) - return; - unit = static_cast( - context.getTypeSizeInChars(pointee).getQuantity()); - } - core::Term says = termOf(*count); - if (!says.known) - return; - says.scale *= kind->kind.extent.scale; - says.constant = - (says.constant * kind->kind.extent.scale) + kind->kind.extent.offset; - core::Term need = says; - need.scale *= unit; - need.constant *= unit; - if (need.isConstant()) - need.scale = 0; - // Only a decided shortfall against an exact extent is a mismatch. - if (heap.lessEqual(state, need, target->extent->bytes) != false) - return; - std::string baseName = spell(*lhs.getBase()); - std::string arrow = lhs.isArrow() ? "->" : "."; - std::string fieldName = baseName + arrow + pointerField->getNameAsString(); - std::string countName = baseName + arrow + countField->getNameAsString(); - std::string saysText; - if (says.isConstant()) - saysText = std::to_string(says.constant); - else if (auto amount = spellAmount(says, /*own=*/false)) - saysText = amount->substr(0, amount->size() - std::string(" bytes").size()); - else - saysText = "more"; - if (unit != 1) - saysText += " elements of " + std::to_string(unit) + " bytes"; - std::string macro = kind->kind.shape == core::PointerShape::Sized - ? "WEAVEC_SIZED_BY" - : "WEAVEC_COUNTED_BY"; - reportMismatch( - "'" + fieldName + "' is declared " + macro + "(" + - countField->getNameAsString() + ") but is given " + - spellAmount(target->extent->bytes, false).value_or("fewer bytes") + - " where '" + countName + "' says " + saysText, - lhs.getBeginLoc(), "'" + fieldName + "' is declared here", - pointerField->getLocation(), "", lhs, std::nullopt); -} - -} // namespace weavec::analysis::engine diff --git a/lib/Analysis/EngineCalls.cpp b/lib/Analysis/EngineCalls.cpp deleted file mode 100644 index 83914ae4..00000000 --- a/lib/Analysis/EngineCalls.cpp +++ /dev/null @@ -1,2554 +0,0 @@ -//===- EngineCalls.cpp - Calls in the object engine -----------------------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// RFC 0031 §5.4: a call's callee is resolved in RFC 0030's order — a -// declared ownership contract, the unit's summary, the program database's, -// the library table's row, a platform declaration (borrow-only), and the -// unknown-callee default — and its effects are applied to the objects its -// arguments reach. The call's own sites are decided before its effects -// (EngineDecide.cpp), from the state the call sees. -// -//===----------------------------------------------------------------------===// - -#include "Engine.h" -#include "EngineIntegers.h" -#include "weavec/Analysis/Annotations.h" -#include "weavec/Analysis/ClangLocation.h" -#include "weavec/Analysis/KindTable.h" -#include "weavec/Analysis/SiteCollector.h" - -#include "clang/AST/RecordLayout.h" -#include "clang/AST/RecursiveASTVisitor.h" -#include "clang/Basic/Builtins.h" -#include "clang/Basic/SourceManager.h" -#include "clang/Basic/Version.h" - -#include -#include -#include - -using namespace clang; - -namespace weavec::analysis::engine { - -/// The parameters a context is over, by type: a callee's, or (for a target -/// this unit does not declare) the call's arguments'. -using ParamShape = std::vector; -static ParamShape shapeOf(const FunctionDecl &function) { - ParamShape shape; - for (const ParmVarDecl *param : function.parameters()) - shape.push_back(param->getType()); - return shape; -} -static ParamShape shapeOf(const CallExpr &call) { - ParamShape shape; - for (const Expr *arg : call.arguments()) - shape.push_back(arg->getType()); - return shape; -} - -namespace { -/// The row whose effects are being applied, for the terms that read the -/// call itself (`strlen`, `fmtlen`). -struct RowCall { - const CallExpr *call = nullptr; - const core::LibraryMatch *match = nullptr; -}; -} // namespace - -/// A library term over the call's arguments as a domain term, when it is -/// linear in one argument. -static core::Term termOfLibTerm(Transfer &transfer, const core::LibTerm &term, - const std::vector &args, - const RowCall &rowCall = {}) { - using Kind = core::LibTerm::Kind; - switch (term.kind) { - case Kind::Constant: - return core::Term::of(term.value); - case Kind::Argument: - if (term.arg < args.size() && args[term.arg] != core::ZeroSym) - return transfer.termOf(args[term.arg]); - return core::Term::unknown(); - case Kind::Sum: { - core::Term a = termOfLibTerm(transfer, term.operands[0], args, rowCall); - core::Term b = termOfLibTerm(transfer, term.operands[1], args, rowCall); - auto sum = a.plus(b); - return sum ? *sum : core::Term::unknown(); - } - case Kind::Difference: { - core::Term a = termOfLibTerm(transfer, term.operands[0], args, rowCall); - return a.plusConstant(-term.value); - } - case Kind::Product: { - core::Term a = termOfLibTerm(transfer, term.operands[0], args, rowCall); - core::Term b = termOfLibTerm(transfer, term.operands[1], args, rowCall); - if (!a.known || !b.known) - return core::Term::unknown(); - // (A product beyond 64 bits is no term: RFC 0017.) - std::int64_t product = 0; - if (a.isConstant() && b.isConstant()) - return __builtin_mul_overflow(a.constant, b.constant, &product) - ? core::Term::unknown() - : core::Term::of(product); - const core::Term &constant = a.isConstant() ? a : b; - const core::Term &other = a.isConstant() ? b : a; - std::int64_t scale = 0; - if (!constant.isConstant() || - __builtin_mul_overflow(other.scale, constant.constant, &scale) || - __builtin_mul_overflow(other.constant, constant.constant, &product)) - return core::Term::unknown(); - return core::Term::ofSym(other.var, scale, product); - } - case Kind::Min: { - core::Term a = termOfLibTerm(transfer, term.operands[0], args, rowCall); - core::Term b = termOfLibTerm(transfer, term.operands[1], args, rowCall); - if (a.isConstant() && b.isConstant()) - return core::Term::of(std::min(a.constant, b.constant)); - return core::Term::unknown(); - } - case Kind::Quotient: { - core::Term a = termOfLibTerm(transfer, term.operands[0], args, rowCall); - if (a.isConstant() && a.constant >= 0 && term.value > 0) - return core::Term::of(a.constant / term.value); - return core::Term::unknown(); - } - case Kind::StringLength: { - // RFC 0012 *Length places*: after a call that required the string (a - // `str` argument), its length is known or becomes a length symbol; - // otherwise only what the facts already say. - if (term.arg >= args.size() || args[term.arg] == core::ZeroSym) - return core::Term::unknown(); - bool required = rowCall.match != nullptr && - term.arg < rowCall.match->entry->params.size() && - rowCall.match->entry->params[term.arg].string; - if (required) { - const Expr *argument = nullptr; - int index = rowCall.match->callArgument(term.arg); - if (rowCall.call != nullptr && index >= 0 && - static_cast(index) < rowCall.call->getNumArgs()) - argument = rowCall.call->getArg(static_cast(index)); - return transfer.measureString(args[term.arg], argument); - } - Transfer::StringFacts facts = transfer.stringFacts(args[term.arg]); - return facts.length == Transfer::StringFacts::Length::Exact - ? facts.term - : core::Term::unknown(); - } - case Kind::FormatLength: { - if (rowCall.call == nullptr || rowCall.match == nullptr) - return core::Term::unknown(); - Transfer::FormatFacts format = - transfer.formatFacts(*rowCall.call, *rowCall.match); - return format.literal && format.exact ? core::Term::of(format.lower) - : core::Term::unknown(); - } - case Kind::Macro: - return core::Term::unknown(); - } - return core::Term::unknown(); -} - -std::vector reachableFrom(const core::Heap &heap, - const core::HeapState &state, - std::vector start) { - std::set seen(start.begin(), start.end()); - std::deque work(start.begin(), start.end()); - while (!work.empty()) { - core::ObjectId id = work.front(); - work.pop_front(); - const core::ObjectState *object = heap.findObject(state, id); - if (object == nullptr) - continue; - for (const auto &[key, sym] : object->cells) { - const core::SymInfo &value = heap.info(state, sym); - for (const core::Target &target : value.targets) - if (seen.insert(target.object).second) - work.push_back(target.object); - } - } - return {seen.begin(), seen.end()}; -} - -/// Whether objects of this kind can be released by a callee. -static bool releasable(core::ObjectKind kind) { - return kind == core::ObjectKind::HeapRecent || - kind == core::ObjectKind::HeapOld || kind == core::ObjectKind::Entry || - kind == core::ObjectKind::EntrySummary || - kind == core::ObjectKind::CallResult || - kind == core::ObjectKind::Materialized || - kind == core::ObjectKind::Focus || kind == core::ObjectKind::Unknown; -} - -namespace { -/// What one call does, applied to the transfer's state. -class CallApplier { -public: - CallApplier(Transfer &transfer, const CallExpr &call, - std::vector args) - : transfer(transfer), run(transfer.functionRun()), - heap(transfer.domain()), state(transfer.heapState()), call(call), - args(std::move(args)), context(run.ast()) {} - - core::Sym apply(const FunctionDecl *callee, core::Sym calleeValue); - -private: - Transfer &transfer; - FunctionRun &run; - core::Heap &heap; - core::HeapState &state; - const CallExpr &call; - - std::vector args; - ASTContext &context; - /// The library row being applied (its terms read the call). - RowCall rowCall; - - core::SourceLocation here() const { - return toCoreLocation(context.getSourceManager(), call.getBeginLoc()); - } - core::Sym result(); - core::Sym applyDirect(const FunctionDecl &callee); - core::Sym applyLibrary(const core::LibraryMatch &match); - core::Sym applyPlatform(const FunctionDecl &callee); - core::Sym applyUnknown(bool callback, const FunctionDecl *callee); - core::Sym applyContract(const FunctionDecl &callee, - const OwnershipContract &contract); - /// RFC 0003: a declaration's `WEAVEC_*` ownership annotations. - std::optional applyDeclared(const FunctionDecl &callee); - /// The same, from annotations already collected (a function-pointer - /// type's, §5.4 step 1). - core::Sym applyAnnotated(const std::vector ¶ms, - const AnnotationSet &result); - /// What a callee can reach besides its arguments escapes (§5.4). - void escapeVisible(); - /// `wrapped`: the allocation goes through the zero-initialisation - /// wrapper (a `zero-init` row), so its bytes are zero in that build; - /// otherwise, unless `zeroed` (a zeroing allocator), what the callee left - /// there is its own (a list `getaddrinfo` filled) and reads as unknown. - core::Sym freshAllocation(const std::string &family, - std::optional extent, bool zeroed, - bool maybeNull, bool wrapped); - /// `possibly`: the call may release it (RFC 0034 §6.3). - void releaseArgument(unsigned index, const std::string &family, - core::ReleaseRecord::Reason reason, - bool possibly = false); - /// RFC 0034 §6.3: a `range(t)` release covers argument `index`'s whole - /// object: it points to the start and `t` is at least the extent. - bool releasesWhole(unsigned index, const core::LibTerm &range); - void havocReachable(unsigned index, bool constPointee, bool callback = false); - /// RFC 0003: a mutable borrow may write what the argument reaches but - /// releases nothing. - void writeReachable(unsigned index); - /// RFC 0030 §2.3, §15 item 3: the copy made part of a pointer into a - /// value (`raw-cast`), which the call's temporal facet records. - void partialPointerCopy(); - /// RFC 0030 §5.3: a `sync` callback argument's targets run zero or more - /// times before the call returns: their effects on globals are possible - /// effects of the call; what they may do to the arguments they are handed - /// (or what an unknown target may do) is the unknown-callee default. - void syncCallback(unsigned row, const core::LibCallback &callback, - const std::vector &callArgs); - void copyCells(unsigned dst, unsigned src, const core::LibTerm &length, - const Expr *dstExpr, const Expr *srcExpr); - void fillCells(unsigned dst, const core::LibTerm &value, - const core::LibTerm &length); - core::Sym callResultValue(QualType type); - core::Sym rowResult(const core::LibraryResult &result, QualType type, - core::Sym realloced, const std::string &reallocFamily); - /// The call reaches the C library through the zero-initialisation - /// wrapper, which never asks `realloc` for zero bytes (RFC 0030 §11). - bool wrappedRealloc = false; -}; -} // namespace - -core::Sym CallApplier::callResultValue(QualType type) { - if (type->isVoidType()) - return transfer.unknownValue(type); - if (!type->isPointerType()) - return transfer.unknownValue(type); - QualType pointee = type->getPointeeType(); - if (pointee->isFunctionType()) - return transfer.unknownValue(type); - core::ObjectId object = - run.callResultObject(call, pointee, transfer.spell(call)); - core::ObjectState &objectState = heap.ensure(state, object); - if (!objectState.extent) - if (auto width = transfer.sizeOf(pointee)) - objectState.extent = core::Extent{.bytes = core::Term::of(*width), - .cls = core::ExtentClass::LowerBound}; - core::SymInfo info; - info.type = core::SymInfo::Type::Pointer; - info.targets = {core::Target{.object = object}}; - info.null = core::PointerNull::Maybe; - info.name = "the result of " + transfer.spell(*call.getCallee()); - info.ctype = typeHandle(type); - return heap.fresh(state, info); -} - -core::Sym CallApplier::freshAllocation(const std::string &family, - std::optional extent, - bool zeroed, bool maybeNull, - bool wrapped) { - QualType type = call.getType(); - QualType pointee = - type->isPointerType() ? type->getPointeeType() : QualType(); - core::ObjectId recent = - run.allocationObject(call, pointee, transfer.spell(call)); - // §4.2 recency: the previous allocation of this site becomes old. - if (const core::ObjectState *previous = heap.findObject(state, recent)) { - core::ObjectKey oldKey; - oldKey.kind = core::ObjectKind::HeapOld; - oldKey.handle = run.table().info(recent).key.handle; - core::ObjectInfo oldInfo = run.table().info(recent); - oldInfo.singular = false; - core::ObjectId old = run.table().intern(oldKey, oldInfo); - core::ObjectState moved = *previous; - if (const core::ObjectState *existing = heap.findObject(state, old)) { - // Merge into the old summary: every cell weakly. - core::ObjectState merged = *existing; - if (moved.life != merged.life) - merged.life = core::Life::MayReleased; - merged.owned = merged.owned && moved.owned; - merged.escaped = merged.escaped || moved.escaped; - state.objects.set(old, merged); - for (const auto &[key, sym] : moved.cells) { - if (auto have = heap.read(state, old, key)) - heap.write(state, old, key, heap.mergeWeak(state, *have, sym), false); - else - state.objects.at(old).cells.set(key, sym); - } - } else { - state.objects.set(old, moved); - } - // Every pointer into the recent object now points into the old one. - std::vector retarget; - for (const auto &[sym, info] : state.syms) - for (const core::Target &target : info.targets) - if (target.object == recent) { - retarget.push_back(sym); - break; - } - for (core::Sym sym : retarget) - for (core::Target &target : state.syms.at(sym).targets) - if (target.object == recent) - target.object = old; - state.objects.erase(recent); - } - core::ObjectState fresh; - fresh.family = family; - fresh.owned = true; - const bool lowered = wrapped && run.unitRun().input.options.zeroInit; - fresh.zeroed = zeroed || lowered; - // What C leaves undefined, even where zero-initialisation lowered the - // allocation to a zeroing one (RFC 0030 §11); what a callee filled in, - // unknown. - fresh.uninitialised = !zeroed && wrapped; - fresh.havocked = !zeroed && !wrapped; - if (extent && extent->known) - fresh.extent = - core::Extent{.bytes = *extent, .cls = core::ExtentClass::Exact}; - state.objects.set(recent, fresh); - core::SymInfo info; - info.type = core::SymInfo::Type::Pointer; - info.targets = {core::Target{.object = recent}}; - info.null = maybeNull ? core::PointerNull::Maybe : core::PointerNull::NonNull; - info.allocatorSource = maybeNull; - if (maybeNull) - info.nullOrigin = - core::NullOrigin{.reason = core::NullOrigin::Reason::Allocated, - .where = here(), - .detail = transfer.spell(*call.getCallee())}; - info.name = transfer.spell(call); - info.ctype = typeHandle(type); - return heap.fresh(state, info); -} - -bool CallApplier::releasesWhole(unsigned index, const core::LibTerm &range) { - const core::SymInfo &info = heap.info(state, args[index]); - if (info.top || info.targets.size() != 1) - return false; - const core::Target &target = info.targets[0]; - const core::ObjectState *object = heap.findObject(state, target.object); - if (object == nullptr || !object->extent || - heap.lessEqual(state, target.offset, core::Term::of(0)) != true || - heap.lessEqual(state, core::Term::of(0), target.offset) != true) - return false; - core::Term bytes = termOfLibTerm(transfer, range, args, rowCall); - return heap.lessEqual(state, object->extent->bytes, bytes) == true; -} - -void CallApplier::releaseArgument(unsigned index, const std::string &family, - core::ReleaseRecord::Reason reason, - bool possibly) { - if (index >= args.size()) - return; - core::Sym pointer = args[index]; - const core::SymInfo &info = heap.info(state, pointer); - if (info.type != core::SymInfo::Type::Pointer || - info.null == core::PointerNull::Null) - return; - core::ReleaseRecord record; - record.reason = reason; - record.where = here(); - record.family = family; - record.via = transfer.spell(*call.getArg(index)); - record.paramGuard = transfer.paramGuard(); - record.pairGuard = transfer.pairGuard(); - record.entryGuard = state.entryTests; - record.nonNullLocals = transfer.nonNullLocals(); - // A release after an unknown callee's: the record is replaced (RFC 0030 - // §3.1). - heap.release(state, pointer, record, possibly); -} - -void CallApplier::havocReachable(unsigned index, bool constPointee, - bool callback) { - if (index < args.size()) - transfer.havocArgument(call, args[index], constPointee, callback); -} - -void Transfer::havocArgument(const CallExpr &call, core::Sym pointer, - bool constPointee, bool callback) { - const core::SymInfo &info = heap.info(state, pointer); - if (info.type != core::SymInfo::Type::Pointer) - return; - std::vector start; - start.reserve(info.targets.size()); - for (const core::Target &target : info.targets) - start.push_back(target.object); - // RFC 0034 §6.3: C's `const` is shallow. The callee may still write - // through the pointers a `const` object holds, so what they reach is - // havocked as for any argument; only the object itself keeps its cells. - std::vector reached = reachableFrom(heap, state, start); - core::ReleaseRecord record; - record.reason = callback ? core::ReleaseRecord::Reason::Callback - : core::ReleaseRecord::Reason::UnknownCallee; - record.where = - toCoreLocation(run.ast().getSourceManager(), call.getBeginLoc()); - record.allPaths = false; - // (The code that may have released it, for the ledger's detail.) - if (const FunctionDecl *direct = call.getDirectCallee()) - record.via = direct->getNameAsString(); - for (core::ObjectId id : reached) { - if (!state.objects.contains(id)) - continue; - core::ObjectKind kind = run.table().info(id).key.kind; - core::ObjectState &object = state.objects.at(id); - if (!constPointee || std::ranges::find(start, id) == start.end()) { - object.cells = {}; - object.havocked = true; - object.nulWithin.reset(); - object.nulFrom.reset(); - } - if (releasable(kind) && object.life != core::Life::Released) { - object.life = core::Life::UnknownReleased; - object.record = record; - object.escaped = true; - } - } -} - -void CallApplier::syncCallback(unsigned row, const core::LibCallback &callback, - const std::vector &callArgs) { - const UnitRun &unit = run.unitRun(); - std::vector targets = transfer.syncTargets(args[row]); - bool handedArguments = targets.empty(); - core::FunctionEffects possible; - possible.returns = core::FunctionEffects::Returns::Always; - for (const FunctionDecl *target : targets) { - const FunctionDecl *canonical = target->getCanonicalDecl(); - const core::FunctionEffects *effects = unit.summaryOf(*canonical); - if (effects == nullptr) { - // Not summarised yet (a cycle's first round): no effect, as a direct - // call of it has; a function with no body here is unknown. - handedArguments = handedArguments || !unit.hasBody(*canonical); - continue; - } - if (effects->incomplete) - handedArguments = true; - for (core::PathEffect effect : effects->effects) { - if (effect.path.root != core::SummaryRoot::Global) { - handedArguments = true; - continue; - } - effect.may = true; - effect.when = core::EffectCase{}; - possible.effects.push_back(std::move(effect)); - } - for (core::StoreEffect store : effects->stores) { - if (store.dest.root != core::SummaryRoot::Global || - (store.value.kind == core::ValueDesc::Kind::Path && - store.value.path && - store.value.path->root != core::SummaryRoot::Global)) { - handedArguments = true; - continue; - } - store.may = true; - store.when = core::EffectCase{}; - possible.stores.push_back(std::move(store)); - } - } - if (!possible.empty()) - (void)transfer.instantiate(call, possible, callArgs); - if (!handedArguments) - return; - // What the targets may do with the pointers they are handed. - core::ReleaseRecord record; - record.reason = targets.empty() ? core::ReleaseRecord::Reason::Callback - : core::ReleaseRecord::Reason::UnknownCallee; - record.where = here(); - record.allPaths = false; - std::vector start; - for (std::uint8_t argument : callback.arguments) - if (argument < args.size() && args[argument] != core::ZeroSym) - for (const core::Target &target : - heap.info(state, args[argument]).targets) - start.push_back(target.object); - for (core::ObjectId id : reachableFrom(heap, state, start)) { - if (!state.objects.contains(id)) - continue; - core::ObjectState &object = state.objects.at(id); - object.cells = {}; - object.havocked = true; - object.nulWithin.reset(); - object.nulFrom.reset(); - if (releasable(run.table().info(id).key.kind) && - object.life == core::Life::Live) { - object.life = core::Life::UnknownReleased; - object.record = record; - object.escaped = true; - } - } -} - -void CallApplier::partialPointerCopy() { - if (!run.isPublishing()) - return; - for (core::SiteId id : run.sites().sitesOf(call)) { - const SiteInfo *info = run.sites().info(id); - if (info == nullptr || info->kind != core::SiteKind::LibCall || - !run.applies(id, core::Facet::Temporal)) - continue; - run.ledger().decideAs(call, info->kind, info->boundary, - core::Facet::Temporal, - core::FacetDecision::unresolvedFor( - core::UnresolvedReason::RawCast, - "unsupported memory copy of pointer-containing " - "storage")); - } -} - -void CallApplier::writeReachable(unsigned index) { - if (index >= args.size()) - return; - const core::SymInfo &info = heap.info(state, args[index]); - if (info.type != core::SymInfo::Type::Pointer) - return; - std::vector start; - start.reserve(info.targets.size()); - for (const core::Target &target : info.targets) - start.push_back(target.object); - for (core::ObjectId id : reachableFrom(heap, state, start)) { - if (!state.objects.contains(id) || - run.table().info(id).key.kind == core::ObjectKind::Focus) - continue; - core::ObjectState &object = state.objects.at(id); - if (object.readonly) - continue; - object.cells = {}; - object.segments.clear(); - object.havocked = true; - object.stored = true; - object.nulWithin.reset(); - object.nulFrom.reset(); - } -} - -/// RFC 0030 §7.2: the extent a callee's declared result kind gives its -/// result (`alloc_size(i[, j])`: argument `i` bytes, times argument `j`). -static std::optional -declaredResultExtent(const Transfer &transfer, const KindEntry &entry, - const CallExpr &call, const std::vector &args) { - if (!entry.hasShape() || entry.shapeFromSystemHeader() || - (entry.kind.shape != core::PointerShape::Sized && - entry.kind.shape != core::PointerShape::Counted)) - return std::nullopt; - QualType pointee = call.getType()->getPointeeType(); - std::int64_t unit = 1; - if (entry.kind.shape == core::PointerShape::Counted) { - auto size = transfer.sizeOf(pointee); - if (!size || *size <= 0) - return std::nullopt; - unit = *size; - } - const core::ExtentTerm &extent = entry.kind.extent; - core::Term bytes = core::Term::unknown(); - if (extent.isConstant()) { - bytes = core::Term::of(extent.offset * unit); - } else if (extent.path->root == core::ExtentPath::Root::Param && - extent.path->param < args.size() && - extent.path->param < call.getNumArgs()) { - bytes = transfer.termOf(args[extent.path->param]); - if (entry.extentFactor) { - if (*entry.extentFactor >= args.size()) - return std::nullopt; - core::Term factor = transfer.termOf(args[*entry.extentFactor]); - if (!factor.isConstant()) - return std::nullopt; - bytes.scale *= factor.constant; - bytes.constant *= factor.constant; - } - std::int64_t scale = extent.scale * unit; - bytes.scale *= scale; - bytes.constant = (bytes.constant * scale) + (extent.offset * unit); - if (bytes.isConstant()) - bytes.scale = 0; - } - if (!bytes.known) - return std::nullopt; - return core::Extent{ - .bytes = bytes, - .cls = entry.extentClass.value_or(core::ExtentClass::Declared)}; -} - -core::Sym CallApplier::applyUnknown(bool callback, const FunctionDecl *callee) { - escapeVisible(); - run.ranUnknownCode = true; - // RFC 0030 §5.1: per pointer argument without an ownership contract. - const OwnershipContract *contract = - callee != nullptr ? run.unitRun().input.kinds.ownership(*callee) - : nullptr; - for (unsigned i = 0; i < call.getNumArgs() && i < args.size(); ++i) { - QualType type = call.getArg(i)->getType(); - if (!type->isPointerType()) - continue; - bool covered = false; - if (contract != nullptr) - for (const auto &argument : contract->arguments) - if (argument.index == i) - covered = true; - if (covered) - continue; - havocReachable(i, type->getPointeeType().isConstQualified(), callback); - } - // Escaped objects and globals external code can reach. - core::ReleaseRecord record; - record.reason = callback ? core::ReleaseRecord::Reason::Callback - : core::ReleaseRecord::Reason::UnknownCallee; - record.where = here(); - record.allPaths = false; - if (callee != nullptr) - record.via = callee->getNameAsString(); - transfer.forgetGlobals(record); - core::Sym value = callResultValue(call.getType()); - // A declared result kind (§7.2) sizes the object the result points to. - if (callee != nullptr) - if (const KindEntry *entry = run.unitRun().input.kinds.result(*callee)) - if (auto extent = declaredResultExtent(transfer, *entry, call, args)) { - const core::SymInfo &info = heap.info(state, value); - if (info.type == core::SymInfo::Type::Pointer && - info.targets.size() == 1 && - state.objects.contains(info.targets[0].object)) - state.objects.at(info.targets[0].object).extent = *extent; - } - return value; -} - -void Transfer::forgetGlobals(const core::ReleaseRecord &record) { - run.ranUnknownCode = true; - std::vector globals; - for (const auto &[id, object] : state.objects) { - core::ObjectKind kind = run.table().info(id).key.kind; - if (kind == core::ObjectKind::Global) - globals.push_back(id); - } - // A local a global reaches escapes (the code may keep its address), and - // the code may write every local that escaped. - for (core::ObjectId id : reachableFrom(heap, state, globals)) - if (run.table().info(id).key.kind == core::ObjectKind::Local && - state.objects.contains(id)) - state.objects.at(id).escaped = true; - std::vector escaped; - for (const auto &[id, object] : state.objects) - if (object.escaped && - run.table().info(id).key.kind == core::ObjectKind::Local) - escaped.push_back(id); - for (core::ObjectId id : escaped) { - heap.forgetCells(state, id, 0, std::nullopt); - state.objects.at(id).havocked = true; - } - const SourceManager &sources = run.ast().getSourceManager(); - for (core::ObjectId id : globals) { - std::vector below = reachableFrom(heap, state, {id}); - for (core::ObjectId reached : below) { - if (reached == id || !state.objects.contains(reached)) - continue; - core::ObjectState &object = state.objects.at(reached); - if (releasable(run.table().info(reached).key.kind) && - object.life == core::Life::Live) { - object.life = core::Life::UnknownReleased; - object.record = record; - } - } - // (The C library's own globals, `stdout` and the like, keep their - // values: code the analysis does not see may close the stream, not - // make the name refer to another, RFC 0031 *Implementation - // amendments*.) - const auto *var = fromHandle(run.table().info(id).key.handle); - if (var != nullptr && - sources.isInSystemHeader(sources.getExpansionLoc(var->getLocation()))) - continue; - // (What it held is gone: a cell no store here reached reads as a new - // unknown value, not as its value at entry.) - core::ObjectState &object = state.objects.at(id); - object.cells = {}; - object.segments.clear(); - object.havocked = true; - object.zeroed = false; - object.nulWithin.reset(); - object.nulFrom.reset(); - } -} - -core::Sym CallApplier::applyPlatform(const FunctionDecl &callee) { - (void)callee; - // RFC 0030 §5.2: borrow-only; what the arguments reach may be written. - for (unsigned i = 0; i < call.getNumArgs() && i < args.size(); ++i) { - QualType type = call.getArg(i)->getType(); - if (!type->isPointerType() || type->getPointeeType().isConstQualified()) - continue; - const core::SymInfo &info = heap.info(state, args[i]); - for (const core::Target &target : info.targets) - if (state.objects.contains(target.object) && - run.table().info(target.object).key.kind != - core::ObjectKind::Literal) { - core::ObjectState &object = state.objects.at(target.object); - // Integer cells may change; pointers stay what they were, except - // behind a pointer to a pointer, which is an out-parameter the - // function may store a pointer into (RFC 0033 §1: - // `host_processor_info(..., (processor_info_array_t *)&info, ...)`). - const bool pointerOut = type->getPointeeType()->isPointerType(); - std::vector scalars; - for (const auto &[key, sym] : object.cells) - if (pointerOut || - heap.info(state, sym).type != core::SymInfo::Type::Pointer) - scalars.push_back(key); - for (const core::CellKey &key : scalars) - object.cells.erase(key); - // The callee may write any byte (RFC 0012). - if (pointerOut || !scalars.empty() || object.zeroed || - object.nulWithin) { - object.havocked = true; - object.nulWithin.reset(); - object.nulFrom.reset(); - } - } - } - return callResultValue(call.getType()); -} - -/// The element type a copy argument points to (`b` of `char *b[2]` or -/// `char **b`), when it is not `void` or a character type. -static QualType copiedElement(const ASTContext &context, const Expr *arg) { - if (arg == nullptr) - return {}; - QualType type = arg->IgnoreParenImpCasts()->getType().getCanonicalType(); - QualType element; - if (const auto *array = context.getAsArrayType(type)) - element = array->getElementType(); - else if (type->isPointerType()) - element = type->getPointeeType(); - if (element.isNull() || element->isVoidType() || element->isCharType() || - element->isIncompleteType()) - return {}; - return element; -} - -/// The offsets of the pointers of `type` that overlap `[from, to)` and do -/// not lie inside it. -static std::vector pointerOffsets(const ASTContext &context, - QualType type, - std::int64_t from, - std::int64_t to) { - std::vector out; - std::int64_t width = - context.getTypeSizeInChars(context.VoidPtrTy).getQuantity(); - auto visit = [&](auto &self, QualType t, std::int64_t base, int depth) { - if (depth > 4 || t.isNull() || t->isIncompleteType() || base >= to) - return; - t = t.getCanonicalType(); - std::int64_t size = context.getTypeSizeInChars(t).getQuantity(); - if (base + size <= from) - return; - if (t->isPointerType()) { - bool whole = base >= from && base + width <= to; - if (!whole) - out.push_back(base); - return; - } - if (const auto *array = context.getAsConstantArrayType(t)) { - QualType element = array->getElementType(); - std::int64_t step = context.getTypeSizeInChars(element).getQuantity(); - if (step <= 0) - return; - std::int64_t first = std::max(0, (from - base) / step); - auto count = static_cast(array->getSize().getZExtValue()); - for (std::int64_t i = first; i < count && base + (i * step) < to; ++i) - self(self, element, base + (i * step), depth + 1); - return; - } - if (const RecordDecl *record = t->getAsRecordDecl(); - record != nullptr && !record->isUnion() && - record->isCompleteDefinition()) { - const ASTRecordLayout &layout = context.getASTRecordLayout(record); - for (const FieldDecl *field : record->fields()) - if (!field->isBitField()) - self(self, field->getType(), - base + static_cast( - layout.getFieldOffset(field->getFieldIndex()) / - context.getCharWidth()), - depth + 1); - } - }; - visit(visit, type, 0, 0); - return out; -} - -void CallApplier::copyCells(unsigned dst, unsigned src, - const core::LibTerm &length, const Expr *dstExpr, - const Expr *srcExpr) { - if (dst >= args.size() || src >= args.size()) - return; - const core::SymInfo to = heap.info(state, args[dst]); - const core::SymInfo from = heap.info(state, args[src]); - core::Term bytes = termOfLibTerm(transfer, length, args, rowCall); - // RFC 0015 §4: a copy of whole elements copies their values, read before - // any is written (so an overlapping `memmove` is simultaneous). - QualType element = copiedElement(context, srcExpr); - if (element.isNull()) - element = copiedElement(context, dstExpr); - auto size = element.isNull() ? std::nullopt : transfer.sizeOf(element); - // (A destination that stands for several objects, the unknown object - // among them, keeps its cells' other values: only the range is forgotten.) - bool single = to.targets.size() == 1 && from.targets.size() == 1 && !to.top && - !from.top && to.targets[0].offset.isConstant() && - from.targets[0].offset.isConstant() && - run.table().info(to.targets[0].object).singular; - if (single && size && *size > 0 && bytes.known) { - // The elements the length must cover: all of a constant length, the - // lower bound of a symbolic one. - std::optional least; - if (bytes.isConstant()) { - least = bytes.constant; - } else if (bytes.scale > 0) { - if (auto lo = state.zone.lower(bytes.var)) - least = (*lo * bytes.scale) + bytes.constant; - } - std::int64_t count = least && *least > 0 ? *least / *size : 0; - bool exactLength = bytes.isConstant() && count <= 64; - // The bytes of a last element a constant length covers only in part. - std::int64_t partial = exactLength ? bytes.constant % *size : 0; - count = std::min(count, 64); - std::vector> leaves; - if (!transfer.recordLeaves(element, leaves)) - leaves.clear(); - if (!leaves.empty()) { - std::int64_t d = to.targets[0].offset.constant; - std::int64_t s = from.targets[0].offset.constant; - core::ObjectId target = to.targets[0].object; - core::ObjectId source = from.targets[0].object; - heap.ensure(state, target); - heap.ensure(state, source); - std::vector> copied; - for (std::int64_t e = 0; e < count; ++e) - for (const auto &[offset, type] : leaves) { - Address cell; - cell.targets = { - core::Target{.object = source, - .offset = core::Term::of(s + (e * *size) + offset)}}; - copied.emplace_back(d + (e * *size) + offset, type, - transfer.load(cell, type, nullptr)); - } - // A cell the length covers in part holds a value made of bytes of - // two values: unknown, and a pointer one is a reinterpretation (§4.2, - // RFC 0030 §2.3). - for (const auto &[offset, type] : leaves) { - if (offset >= partial) - continue; - std::int64_t width = transfer.sizeOf(type).value_or(*size); - std::int64_t at = d + (count * *size) + offset; - if (offset + width <= partial) { - Address cell; - cell.targets = {core::Target{ - .object = source, - .offset = core::Term::of(s + (count * *size) + offset)}}; - copied.emplace_back(at, type, transfer.load(cell, type, nullptr)); - continue; - } - core::Sym forged = transfer.unknownValue(type); - if (type->isPointerType()) { - heap.infoMut(state, forged).rawCast = true; - partialPointerCopy(); - } - copied.emplace_back(at, type, forged); - } - // Past what must be copied (a symbolic length): every element may - // now hold a value of any source element (weakly). - std::vector> tail; - if (!exactLength) - for (const auto &[offset, type] : leaves) { - core::SymInfo hint; - hint.type = type->isPointerType() ? core::SymInfo::Type::Pointer - : core::SymInfo::Type::Int; - hint.ctype = typeHandle(type); - core::CellKey position{.offset = - (((s + offset) % *size) + *size) % *size, - .stride = static_cast(*size), - .index = core::ZeroSym}; - tail.emplace_back(type, heap.load(state, source, position, hint)); - } - // From an entry object no store has reached, each element the length - // covers holds its own source element's entry value (§4.9 *Copies*): - // a copied range of the elements the length counts. - const core::ObjectState &sourceState = state.objects.at(source); - bool byElement = - !tail.empty() && target != source && d >= 0 && s >= 0 && - run.table().info(source).key.kind == core::ObjectKind::Entry && - run.table().info(target).singular && !sourceState.stored && - !sourceState.forgetsAny(); - core::Term elements = core::Term::unknown(); - if (byElement) { - if (bytes.isConstant()) { - elements = core::Term::of(bytes.constant / *size); - } else if (bytes.scale % *size == 0 && bytes.constant % *size == 0) { - elements = core::Term::ofSym(bytes.var, bytes.scale / *size, - bytes.constant / *size); - } else { - // A count the length's term does not spell: at least what the - // lower bound covers. - core::Sym counted = transfer.unknownValue(context.getSizeType()); - state.zone.addRange(counted, count, INT64_MAX / *size); - elements = core::Term::ofSym(counted); - } - } - // The range first: the elements copied one by one shadow it. - for (std::size_t i = 0; i < tail.size(); ++i) { - std::int64_t offset = leaves[i].first; - core::CellKey position{.offset = - (((d + offset) % *size) + *size) % *size, - .stride = static_cast(*size), - .index = core::ZeroSym}; - if (byElement) { - core::Term first = - core::Term::of((d + offset - position.offset) / *size); - auto last = first.plus(elements); - if (last) { - heap.copyElements(state, target, position, first, *last, - tail[i].second, source, s - d); - continue; - } - } - heap.write(state, target, position, tail[i].second, true); - } - for (const auto &[offset, type, value] : copied) { - Address cell; - cell.targets = { - core::Target{.object = target, .offset = core::Term::of(offset)}}; - transfer.store(cell, value, type, nullptr); - } - state.objects.at(target).uninitialised = false; - if (state.objects.at(target).stride == 0) - state.objects.at(target).stride = static_cast(*size); - return; - } - } - if (single && bytes.isConstant()) { - std::int64_t d = to.targets[0].offset.constant; - std::int64_t s = from.targets[0].offset.constant; - core::ObjectId source = from.targets[0].object; - core::ObjectId target = to.targets[0].object; - // A literal's bytes are cells to copy (RFC 0012: `strncpy` from one). - if (run.table().info(source).key.kind == core::ObjectKind::Literal && - bytes.constant > 0 && bytes.constant <= 64 && s >= 0) - for (std::int64_t at = s; at < s + bytes.constant; ++at) - if (!heap.read(state, source, core::CellKey{.offset = at})) - (void)run.unwritten( - state, source, core::CellKey{.offset = at}, - core::SymInfo{.type = core::SymInfo::Type::Int, - .ctype = typeHandle(context.CharTy)}); - // The source's pointers the copy splits, which a copy of its bytes - // must see to tell (RFC 0031 §4.2). - if (bytes.constant > 0 && bytes.constant <= 64 && s >= 0) - if (core::Handle handle = run.table().info(source).type; handle != 0) - for (std::int64_t at : pointerOffsets(context, typeOfHandle(handle), s, - s + bytes.constant)) - if (!heap.read(state, source, core::CellKey{.offset = at})) - (void)run.unwritten( - state, source, core::CellKey{.offset = at}, - core::SymInfo{.type = core::SymInfo::Type::Pointer, - .ctype = typeHandle(context.VoidPtrTy)}); - heap.ensure(state, target); - heap.forgetCells(state, target, d, bytes.constant); - if (const core::ObjectState *object = heap.findObject(state, source)) { - // RFC 0031 §4.2: a cell copied whole keeps its symbol; part of a - // pointer is no pointer (`raw-cast`). - std::vector> copied; - std::vector partial; - for (const auto &[key, sym] : object->cells) { - if (!key.isConcrete() || key.offset >= s + bytes.constant) - continue; - const core::SymInfo &cell = heap.info(state, sym); - std::int64_t width = - cell.type == core::SymInfo::Type::Pointer - ? static_cast( - context.getTypeSizeInChars(context.VoidPtrTy) - .getQuantity()) - : 1; - if (cell.type == core::SymInfo::Type::Int && cell.intType) - width = std::max(1, cell.intType->width / 8); - if (key.offset + width <= s) - continue; - if (key.offset >= s && key.offset + width <= s + bytes.constant) - copied.emplace_back(core::CellKey{.offset = key.offset - s + d}, sym); - else if (cell.type == core::SymInfo::Type::Pointer) - partial.push_back( - core::CellKey{.offset = std::max(key.offset, s) - s + d}); - } - for (const auto &[key, sym] : copied) - heap.write(state, target, key, sym, false); - for (const core::CellKey &key : partial) { - core::Sym raw = transfer.unknownValue(context.VoidPtrTy); - heap.infoMut(state, raw).rawCast = true; - heap.write(state, target, key, raw, false); - partialPointerCopy(); - } - } - state.objects.at(target).uninitialised = false; - return; - } - // An unknown range: the destination's cells are forgotten (a byte-wise - // copy of pointers is a reinterpretation, RFC 0030 §2.3). - for (const core::Target &target : to.targets) { - heap.ensure(state, target.object); - heap.forgetCells(state, target.object, 0, std::nullopt); - state.objects.at(target.object).havocked = true; - state.objects.at(target.object).uninitialised = false; - } -} - -void CallApplier::fillCells(unsigned dst, const core::LibTerm &value, - const core::LibTerm &length) { - if (dst >= args.size()) - return; - const core::SymInfo to = heap.info(state, args[dst]); - core::Term fill = termOfLibTerm(transfer, value, args, rowCall); - core::Term bytes = termOfLibTerm(transfer, length, args, rowCall); - for (const core::Target &target : to.targets) { - heap.ensure(state, target.object); - core::ObjectState &object = state.objects.at(target.object); - bool whole = target.offset.isConstant() && target.offset.constant == 0 && - bytes.known && object.extent && object.extent->bytes == bytes; - // (An object that stands for several, the unknown object among them, is - // filled only in part: its range is forgotten.) - const bool single = - to.targets.size() == 1 && run.table().info(target.object).singular; - if (fill.isConstant() && fill.constant == 0 && whole && single) { - object.cells = {}; - object.zeroed = true; - object.uninitialised = false; - object.havocked = false; - object.forgotten.clear(); - object.mayForgotten.clear(); - object.nulWithin.reset(); - object.nulFrom.reset(); - continue; - } - if (target.offset.isConstant() && bytes.isConstant()) - heap.forgetCells(state, target.object, target.offset.constant, - bytes.constant); - else - heap.forgetCells(state, target.object, 0, std::nullopt); - state.objects.at(target.object).uninitialised = false; - // A constant byte over a known range: the cells hold it (RFC 0012: - // `memset(a, 'x', sizeof a)` leaves no terminator). - if (fill.isConstant() && target.offset.isConstant() && bytes.isConstant() && - bytes.constant > 0 && bytes.constant <= 64 && single) { - core::Sym byte = transfer.constant( - static_cast(static_cast(fill.constant) & - 0xffU), - context.CharTy); - for (std::int64_t at = 0; at < bytes.constant; ++at) - heap.write(state, target.object, - core::CellKey{.offset = target.offset.constant + at}, byte, - false); - } - } -} - -core::Sym CallApplier::rowResult(const core::LibraryResult &result, - QualType type, core::Sym realloced, - const std::string &reallocFamily) { - core::Sym value = core::ZeroSym; - switch (result.kind) { - case core::LibraryResult::Kind::Fresh: { - std::optional extent; - // RFC 0017 §3: a product of two arguments is a checked product - // (`calloc`, `reallocarray`): one that overflows for every value makes - // the call fail, leaving a reallocated block as it was; otherwise the - // block has the C product's bytes, the mathematical ones on success. - const core::LibTerm *product = nullptr; - if (result.extent && result.extent->kind == core::LibTerm::Kind::Product && - result.extent->operands.size() == 2 && - result.extent->operands[0].kind == core::LibTerm::Kind::Argument && - result.extent->operands[1].kind == core::LibTerm::Kind::Argument && - result.extent->operands[0].arg < args.size() && - result.extent->operands[1].arg < args.size() && - args[result.extent->operands[0].arg] != core::ZeroSym && - args[result.extent->operands[1].arg] != core::ZeroSym) - product = &*result.extent; - if (product != nullptr) { - auto bytes = transfer.checkedProduct(args[product->operands[0].arg], - args[product->operands[1].arg], - context.getSizeType(), call); - if (!bytes) { - core::Sym failed = transfer.nullPointer(type); - heap.infoMut(state, failed).allocatorSource = true; - return failed; - } - extent = transfer.termOf(*bytes); - } else if (result.extent) { - extent = termOfLibTerm(transfer, *result.extent, args, rowCall); - } - value = freshAllocation( - result.family.empty() ? std::string(core::HeapFamily) : result.family, - extent, result.zeroFilled, - result.null != core::LibraryResult::Null::Never, result.zeroInit); - // A size that is a product which may wrap (`malloc(n * sizeof *p)`): - // the object ends at that product at the latest. - if (extent && extent->known && !extent->isConstant() && - extent->scale == 1 && extent->constant == 0) - if (const core::SymInfo *size = state.syms.find(extent->var); - size != nullptr && size->unwrapped) { - const core::SymInfo &fresh = heap.info(state, value); - if (fresh.targets.size() == 1 && - state.objects.contains(fresh.targets[0].object)) { - core::ObjectState &object = state.objects.at(fresh.targets[0].object); - if (object.extent) - object.extent->unwrapped = size->unwrapped; - } - } - // A duplicated string (`strdup`): its length is the extent's but one. - if (result.string && extent && extent->known) { - const core::SymInfo &fresh = heap.info(state, value); - if (fresh.targets.size() == 1 && - state.objects.contains(fresh.targets[0].object)) { - core::ObjectState &object = state.objects.at(fresh.targets[0].object); - object.nulWithin = extent->plusConstant(-1); - object.nulFrom = core::Term::of(0); - } - } - if (realloced != core::ZeroSym) { - // RFC 0015 §5: the new block holds the old block's values (those - // below its size); pointers into the old block do not follow. - const core::SymInfo &previous = heap.info(state, realloced); - const core::SymInfo &grown = heap.info(state, value); - if (previous.type == core::SymInfo::Type::Pointer && !previous.top && - previous.targets.size() == 1 && - previous.targets[0].offset.isConstant() && - previous.targets[0].offset.constant == 0 && - grown.targets.size() == 1) { - core::ObjectId from = previous.targets[0].object; - core::ObjectId into = grown.targets[0].object; - if (const core::ObjectState *contents = heap.findObject(state, from); - contents != nullptr && from != into) { - core::ObjectState copy = *contents; - core::ObjectState &target = state.objects.at(into); - std::optional limit; - if (target.extent && target.extent->bytes.isConstant()) - limit = target.extent->bytes.constant; - target.cells = {}; - for (const auto &[key, sym] : copy.cells) - if (!key.isConcrete() || !limit || key.offset < *limit) - target.cells.set(key, sym); - target.segments = copy.segments; - target.stride = copy.stride; - } - } - // realloc: the old block is released on success (and when the size - // is zero); until a test of the result says which, the release is - // conditional (RFC 0030 §9.1). - // RFC 0030 §8.2: the old block moves into the result on success, - // and is freed on failure when the size is zero; until a test of the - // result says which, the release is conditional (§9.1). - core::ReleaseRecord record; - record.reason = core::ReleaseRecord::Reason::Moved; - record.where = here(); - record.family = reallocFamily; - record.conditional = true; - const core::SymInfo &old = heap.info(state, realloced); - if (old.type == core::SymInfo::Type::Pointer && - old.null != core::PointerNull::Null) { - heap.release(state, realloced, record); - core::PendingCase onSuccess; - onSuccess.kind = core::PendingCase::Kind::Release; - onSuccess.classes = {"nonnull"}; - onSuccess.subject = realloced; - onSuccess.record = record; - onSuccess.record.conditional = false; - heap.infoMut(state, value).pending.push_back(onSuccess); - // The size: zero for certain, possibly, or never. (Never through - // the zero-initialisation wrapper, which asks for one byte instead: - // the build the analysis models fails without freeing.) - std::optional zero; - if (wrappedRealloc) { - zero = false; - } else if (result.extent) { - core::Term size = termOfLibTerm(transfer, *result.extent, args); - if (size.isConstant()) { - zero = size.constant == 0; - } else if (size.known && size.var != core::ZeroSym) { - // The bounds of `scale * var + constant`. - auto varLo = state.zone.lower(size.var); - auto varHi = state.zone.upper(size.var); - auto scaled = [&](const std::optional &bound) - -> std::optional { - std::int64_t product = 0; - std::int64_t sum = 0; - if (!bound || - __builtin_mul_overflow(size.scale, *bound, &product) || - __builtin_add_overflow(product, size.constant, &sum)) - return std::nullopt; - return sum; - }; - auto lo = size.scale >= 0 ? scaled(varLo) : scaled(varHi); - auto hi = size.scale >= 0 ? scaled(varHi) : scaled(varLo); - if ((lo && *lo > 0) || - (size.constant == 0 && heap.info(state, size.var).nonZero)) - zero = false; - else if (lo && hi && *lo == 0 && *hi == 0) - zero = true; - } - } - if (zero != false) { - core::PendingCase onFailure; - onFailure.kind = core::PendingCase::Kind::Release; - onFailure.classes = {"null"}; - onFailure.subject = realloced; - onFailure.record = std::move(record); - onFailure.record.reason = core::ReleaseRecord::Reason::Freed; - onFailure.record.conditional = zero != true; - heap.infoMut(state, value).pending.push_back(onFailure); - } - } - } - break; - } - case core::LibraryResult::Kind::Arg: - if (result.arg < args.size() && args[result.arg] != core::ZeroSym) { - value = args[result.arg]; - // The argument, or null when the call fails (`freopen`): the same - // pointer only on the non-null class. - const core::SymInfo &argument = heap.info(state, value); - if (result.null != core::LibraryResult::Null::Never && - argument.type == core::SymInfo::Type::Pointer && - argument.null != core::PointerNull::Null) { - core::SymInfo copy = argument; - copy.null = core::PointerNull::Maybe; - copy.pending.clear(); - copy.entryOf.reset(); - copy.condition.reset(); - copy.allocatorSource = false; - value = heap.fresh(state, copy); - } - } - break; - case core::LibraryResult::Kind::Interior: - if (result.arg < args.size() && args[result.arg] != core::ZeroSym) { - core::SymInfo info = heap.info(state, args[result.arg]); - info.entryOf.reset(); - info.pending.clear(); - info.condition.reset(); - info.linear.reset(); - info.release.reset(); - for (core::Target &target : info.targets) - target.offset = core::Term::unknown(); - info.null = result.null == core::LibraryResult::Null::Never - ? core::PointerNull::NonNull - : core::PointerNull::Maybe; - info.name = transfer.spell(call); - value = heap.fresh(state, info); - } - break; - case core::LibraryResult::Kind::Int: - case core::LibraryResult::Kind::Void: - if (result.value) { - core::Term term = termOfLibTerm(transfer, *result.value, args, rowCall); - // `strlen(s)` is the length symbol itself (RFC 0012 *Length - // places*), so two measurements of one string are one value. - if (term.known && !term.isConstant() && term.scale == 1 && - term.constant == 0 && - heap.info(state, term.var).type == core::SymInfo::Type::Int) { - value = term.var; - break; - } - value = transfer.unknownValue(type); - if (term.isConstant()) - state.zone.addRange(value, term.constant, term.constant); - break; - } - value = transfer.unknownValue(type); - break; - case core::LibraryResult::Kind::Static: - // RFC 0030 §8.2: a borrow of the slot's storage. - if (type->isPointerType() && !result.state.empty()) { - core::ObjectId slot = run.stateObject(result.state); - core::ObjectState &storage = heap.ensure(state, slot); - if (result.extent && !storage.extent) { - core::Term bytes = - termOfLibTerm(transfer, *result.extent, args, rowCall); - if (bytes.known) - storage.extent = - core::Extent{.bytes = bytes, .cls = core::ExtentClass::Declared}; - } - core::SymInfo info; - info.type = core::SymInfo::Type::Pointer; - info.targets = {core::Target{.object = slot}}; - info.null = result.null == core::LibraryResult::Null::Never - ? core::PointerNull::NonNull - : core::PointerNull::Maybe; - info.name = transfer.spell(call); - info.ctype = typeHandle(type); - value = heap.fresh(state, info); - break; - } - value = callResultValue(type); - break; - case core::LibraryResult::Kind::InteriorState: - // A pointer into the value the slot retains (`strtok`); one the - // program released is reported by `reads` and gives no object here. - if (type->isPointerType() && !result.state.empty()) - if (auto held = - heap.read(state, run.stateObject(result.state), core::CellKey{}); - held && - heap.info(state, *held).type == core::SymInfo::Type::Pointer && - heap.info(state, *held).null != core::PointerNull::Null && - heap.temporal(state, *held).kind == - core::TemporalVerdict::Kind::Proven) { - core::SymInfo info = heap.info(state, *held); - info.entryOf.reset(); - info.pending.clear(); - info.condition.reset(); - info.linear.reset(); - info.nullOrigin.reset(); - info.allocatorSource = false; - info.release.reset(); - for (core::Target &target : info.targets) - target.offset = core::Term::unknown(); - info.null = core::PointerNull::Maybe; - info.name = transfer.spell(call); - info.ctype = typeHandle(type); - value = heap.fresh(state, info); - break; - } - value = callResultValue(type); - break; - case core::LibraryResult::Kind::Unknown: - value = callResultValue(type); - if (result.null == core::LibraryResult::Null::Never && - heap.info(state, value).type == core::SymInfo::Type::Pointer) - heap.infoMut(state, value).null = core::PointerNull::NonNull; - break; - } - return value; -} - -core::Sym CallApplier::applyLibrary(const core::LibraryMatch &match) { - const core::LibraryEntry &entry = *match.entry; - // Arguments by row position. - std::vector rowArgs(entry.params.size(), core::ZeroSym); - for (unsigned i = 0; i < call.getNumArgs() && i < args.size(); ++i) { - int row = match.rowArgument(i); - if (row >= 0 && static_cast(row) < rowArgs.size()) - rowArgs[static_cast(row)] = args[i]; - } - std::swap(args, rowArgs); - rowCall = RowCall{.call = &call, .match = &match}; - auto callArgument = [&](unsigned position) { - int index = match.callArgument(position); - return index < 0 ? ~0U : static_cast(index); - }; - // `writes-str` lengths are over the values before the call (RFC 0030 §8). - std::vector written; - written.reserve(entry.writesString.size()); - for (const core::LibStringWrite &write : entry.writesString) - written.push_back( - write.length ? termOfLibTerm(transfer, *write.length, args, rowCall) - : core::Term::unknown()); - // Copies and fills first: they read the arguments before a release. - auto callExpr = [&](unsigned row) -> const Expr * { - unsigned index = callArgument(row); - return index < call.getNumArgs() ? call.getArg(index) : nullptr; - }; - // RFC 0012: the note on a later read of an object left unterminated. - auto noteByteWrite = [&](unsigned dst) { - if (dst < args.size()) - for (const core::Target &target : heap.info(state, args[dst]).targets) - run.byteWrites[target.object] = here(); - }; - for (const core::LibCopy © : entry.copies) { - copyCells(copy.dst, copy.src, copy.length, callExpr(copy.dst), - callExpr(copy.src)); - noteByteWrite(copy.dst); - } - for (const core::LibFill &fill : entry.fills) { - fillCells(fill.dst, fill.value, fill.length); - noteByteWrite(fill.dst); - } - for (const core::LibStringWrite &write : entry.writesString) - noteByteWrite(write.dst); - for (std::size_t i = 0; i < entry.writesString.size(); ++i) { - const core::LibStringWrite &write = entry.writesString[i]; - if (write.dst >= args.size()) - continue; - const core::SymInfo &info = heap.info(state, args[write.dst]); - std::vector targets; - targets.reserve(info.targets.size()); - for (const core::Target &target : info.targets) - targets.push_back(target.object); - for (const core::Target &target : info.targets) { - if (!state.objects.contains(target.object)) - continue; - // The bytes from the destination on (the ones before it stay). - const core::ObjectState &object = state.objects.at(target.object); - if (info.targets.size() == 1 && target.offset.isConstant() && - target.offset.constant >= 0 && object.extent && - object.extent->bytes.isConstant()) - heap.forgetCells(state, target.object, target.offset.constant, - object.extent->bytes.constant - - target.offset.constant); - else - heap.forgetCells(state, target.object, 0, std::nullopt); - state.objects.at(target.object).uninitialised = false; - } - // RFC 0012: a string of exactly that many characters, when known. - if (targets.size() == 1 && written[i].known) - transfer.noteStringWritten(args[write.dst], written[i]); - } - // RFC 0031 §4.2: the other bytes a `w` or `rw` argument's row writes - // hold what the library left there (`pthread_create`'s thread handle, - // `fread`'s records): unknown, never the values from before the call. - for (unsigned row = 0; row < entry.params.size() && row < args.size(); - ++row) { - const core::LibraryParam ¶m = entry.params[row]; - if (param.type != core::LibraryParam::Type::Pointer || - (param.access != core::LibraryParam::Access::Write && - param.access != core::LibraryParam::Access::ReadWrite) || - param.out || param.effect == core::LibraryParam::Effect::Release || - param.effect == core::LibraryParam::Effect::Realloc || - args[row] == core::ZeroSym) - continue; - bool precise = false; - for (const core::LibCopy © : entry.copies) - precise = precise || copy.dst == row; - for (const core::LibFill &fill : entry.fills) - precise = precise || fill.dst == row; - for (const core::LibStringWrite &write : entry.writesString) - precise = precise || write.dst == row; - if (precise) - continue; - const core::SymInfo info = heap.info(state, args[row]); - if (info.type != core::SymInfo::Type::Pointer || info.top) - continue; - // How many bytes from the pointer: the row's size, else one element. - std::optional bytes; - // One integer (`__builtin_mul_overflow`'s result, `pthread_create`'s - // handle): an unknown value in its cell. - QualType scalar; - if (param.bytes) { - core::Term term = termOfLibTerm(transfer, *param.bytes, args, rowCall); - if (term.isConstant()) - bytes = term.constant; - } else if (!param.count) { - if (const Expr *argument = callExpr(row)) { - QualType type = argument->IgnoreParenImpCasts()->getType(); - if (type->isPointerType()) { - bytes = transfer.sizeOf(type->getPointeeType()); - if (type->getPointeeType()->isIntegerType()) - scalar = type->getPointeeType(); - } - } - } - for (const core::Target &target : info.targets) { - if (!state.objects.contains(target.object) || - state.objects.at(target.object).readonly) - continue; - if (!scalar.isNull() && target.offset.isConstant()) { - Address cell; - cell.targets = {target}; - transfer.store(cell, transfer.unknownValue(scalar), scalar, nullptr); - } else if (bytes && target.offset.isConstant()) { - heap.forgetCells(state, target.object, target.offset.constant, bytes); - } else { - heap.forgetCells(state, target.object, 0, std::nullopt); - } - state.objects.at(target.object).uninitialised = false; - state.objects.at(target.object).stored = true; - } - } - core::Sym realloced = core::ZeroSym; - std::string reallocFamily; - for (unsigned row = 0; row < entry.params.size(); ++row) { - const core::LibraryParam ¶m = entry.params[row]; - if (row >= args.size()) - break; - switch (param.effect) { - case core::LibraryParam::Effect::Release: - // RFC 0034 §6.3, RFC 0033 §1's witness rule: a release of part of an - // object (`munmap` of a guard page) is no witness of the whole - // object's release, so one not known to cover it is possible. - releaseArgument(row, - param.family.empty() ? std::string(core::HeapFamily) - : param.family, - core::ReleaseRecord::Reason::Freed, - param.range && !releasesWhole(row, *param.range)); - break; - case core::LibraryParam::Effect::Realloc: - realloced = args[row]; - reallocFamily = - param.family.empty() ? std::string(core::HeapFamily) : param.family; - break; - case core::LibraryParam::Effect::Retain: - case core::LibraryParam::Effect::Escape: { - const core::SymInfo &info = heap.info(state, args[row]); - for (const core::Target &target : info.targets) - if (state.objects.contains(target.object)) - state.objects.at(target.object).escaped = true; - // RFC 0030 §8.2 `retain(S)`: the slot keeps the pointer for a later - // `reads(S)` (a null argument keeps the one it holds). - if (param.effect == core::LibraryParam::Effect::Retain && - !param.state.empty() && info.type == core::SymInfo::Type::Pointer && - info.null != core::PointerNull::Null) { - core::ObjectId slot = run.stateObject(param.state); - heap.ensure(state, slot); - core::Sym kept = args[row]; - if (info.null == core::PointerNull::Maybe) - if (auto held = heap.read(state, slot, core::CellKey{})) - kept = heap.mergeWeak(state, *held, kept); - heap.write(state, slot, core::CellKey{}, kept, false); - } - break; - } - case core::LibraryParam::Effect::Borrow: - case core::LibraryParam::Effect::Init: - case core::LibraryParam::Effect::Fini: - break; - } - // Out-parameters: a result stored through the argument. - if (param.out && args[row] != core::ZeroSym) { - const core::SymInfo &info = heap.info(state, args[row]); - if (info.type == core::SymInfo::Type::Pointer && !info.targets.empty()) { - Address address; - address.targets = info.targets; - QualType pointee = call.getArg(callArgument(row) < call.getNumArgs() - ? callArgument(row) - : 0) - ->IgnoreParenImpCasts() - ->getType(); - if (pointee->isPointerType()) - pointee = pointee->getPointeeType(); - // `replaces` (`getline`): the slot's previous value is consumed as - // by `realloc` of the same family: it moves into the new value. - if (param.out->replaces && pointee->isPointerType()) { - core::Sym old = transfer.load(address, pointee, nullptr); - const core::SymInfo &previous = heap.info(state, old); - if (previous.type == core::SymInfo::Type::Pointer && - previous.null != core::PointerNull::Null) { - core::ReleaseRecord record; - record.reason = core::ReleaseRecord::Reason::Moved; - record.where = here(); - record.family = param.out->family.empty() - ? std::string(core::HeapFamily) - : param.out->family; - heap.release(state, old, record); - } - } - core::Sym value = - rowResult(*param.out, pointee, core::ZeroSym, std::string()); - transfer.store(address, value, pointee, nullptr); - } - } - } - if (entry.noreturn || entry.exits) { - state.unreachable = true; - return transfer.unknownValue(call.getType()); - } - for (unsigned row = 0; row < entry.params.size() && row < args.size(); ++row) - if (const core::LibraryParam ¶m = entry.params[row]; - param.callback && param.callback->kind == core::LibCallback::Kind::Sync) - syncCallback(row, *param.callback, rowArgs); - // RFC 0030 §8.2 `invalidates(S)`: every borrow of the slot's storage may - // end here (the call's own result borrows it afresh). - for (const std::string &slot : entry.invalidates) { - core::ObjectId storage = run.stateObject(slot); - core::ReleaseRecord record; - record.reason = core::ReleaseRecord::Reason::Freed; - record.where = here(); - record.allPaths = false; - std::vector borrows; - for (const auto &[sym, info] : state.syms) - if (info.type == core::SymInfo::Type::Pointer && !info.release) - for (const core::Target &target : info.targets) - if (target.object == storage) { - borrows.push_back(sym); - break; - } - for (core::Sym sym : borrows) - heap.infoMut(state, sym).release = record; - } - wrappedRealloc = entry.result.zeroInit && reallocFamily == core::HeapFamily && - run.unitRun().input.options.zeroInit; - core::Sym value = - rowResult(entry.result, call.getType(), realloced, reallocFamily); - if (value == core::ZeroSym) - value = transfer.unknownValue(call.getType()); - std::swap(args, rowArgs); - return value; -} - -core::Sym CallApplier::applyContract(const FunctionDecl &callee, - const OwnershipContract &contract) { - (void)callee; - for (const auto &argument : contract.arguments) { - if (argument.retains) { - if (argument.index < args.size()) { - const core::SymInfo &info = heap.info(state, args[argument.index]); - for (const core::Target &target : info.targets) - if (state.objects.contains(target.object)) - state.objects.at(target.object).escaped = true; - } - } else { - releaseArgument(argument.index, - argument.family.empty() ? std::string(core::HeapFamily) - : argument.family, - core::ReleaseRecord::Reason::Freed); - } - } - if (contract.freshResult && call.getType()->isPointerType()) - return freshAllocation(contract.freshResult->empty() - ? std::string(core::HeapFamily) - : *contract.freshResult, - std::nullopt, false, true, false); - return callResultValue(call.getType()); -} - -void CallApplier::escapeVisible() { - std::vector start; - for (const auto &[id, object] : state.objects) { - core::ObjectKind kind = run.table().info(id).key.kind; - if (kind == core::ObjectKind::Global || kind == core::ObjectKind::Entry || - kind == core::ObjectKind::EntrySummary) - start.push_back(id); - } - for (core::ObjectId id : reachableFrom(heap, state, start)) { - if (!state.objects.contains(id)) - continue; - core::ObjectKind kind = run.table().info(id).key.kind; - if (kind == core::ObjectKind::HeapRecent || - kind == core::ObjectKind::HeapOld) - state.objects.at(id).escaped = true; - } -} - -/// The ownership annotations of parameter `index` over every declaration. -static AnnotationSet parameterAnnotations(const FunctionDecl &callee, - unsigned index) { - AnnotationSet all; - for (const FunctionDecl *redecl : callee.redecls()) { - if (index >= redecl->getNumParams()) - continue; - AnnotationSet set = getAnnotations(*redecl->getParamDecl(index)); - all.owned = all.owned || set.owned; - all.borrowed = all.borrowed || set.borrowed; - all.mutBorrowed = all.mutBorrowed || set.mutBorrowed; - all.raw = all.raw || set.raw; - all.retains = all.retains || set.retains; - all.releases = all.releases || set.releases; - all.frees = all.frees || set.frees; - if (!set.family.empty()) - all.family = set.family; - } - return all; -} - -std::optional -CallApplier::applyDeclared(const FunctionDecl &callee) { - bool any = false; - std::vector params; - for (unsigned i = 0; i < callee.getNumParams(); ++i) { - params.push_back(parameterAnnotations(callee, i)); - any = any || params.back().ownership(); - } - AnnotationSet result; - for (const FunctionDecl *redecl : callee.redecls()) { - AnnotationSet set = getAnnotations(*redecl); - result.owned = result.owned || set.owned; - result.borrowed = result.borrowed || set.borrowed; - if (!set.family.empty()) - result.family = set.family; - } - any = any || result.owned || result.borrowed; - if (!any) - return std::nullopt; - return applyAnnotated(params, result); -} - -core::Sym CallApplier::applyAnnotated(const std::vector ¶ms, - const AnnotationSet &result) { - for (unsigned i = 0; i < call.getNumArgs() && i < args.size(); ++i) { - if (!call.getArg(i)->getType()->isPointerType()) - continue; - if (i >= params.size()) { - havocReachable(i, false); - continue; - } - const AnnotationSet &set = params[i]; - std::string family = - set.family.empty() ? std::string(core::HeapFamily) : set.family; - if (set.owned || set.frees) { - releaseArgument(i, family, - set.frees ? core::ReleaseRecord::Reason::Freed - : core::ReleaseRecord::Reason::Moved); - } else if (set.releases) { - releaseArgument(i, family, core::ReleaseRecord::Reason::ShareReleased); - } else if (set.retains || set.raw) { - // RFC 0007 *Escape*, RFC 0004: kept by the callee, or handed over as - // a raw pointer the analysis does not follow. - const core::SymInfo &info = heap.info(state, args[i]); - std::vector targets; - targets.reserve(info.targets.size()); - for (const core::Target &target : info.targets) - targets.push_back(target.object); - for (core::ObjectId id : targets) - if (state.objects.contains(id)) - state.objects.at(id).escaped = true; - } else if (set.mutBorrowed) { - writeReachable(i); - } else if (!set.borrowed && !set.raw) { - havocReachable( - i, call.getArg(i)->getType()->getPointeeType().isConstQualified()); - } - } - if (result.owned && call.getType()->isPointerType()) - return freshAllocation(result.family.empty() ? std::string(core::HeapFamily) - : result.family, - std::nullopt, false, true, false); - return callResultValue(call.getType()); -} - -core::Sym CallApplier::applyDirect(const FunctionDecl &callee) { - const UnitRun &unit = run.unitRun(); - const FunctionDecl *canonical = callee.getCanonicalDecl(); - // A summary of the unit (or of the program in `weavec --whole-program`); - // for a call whose context it does not describe, the context's (§6.6). - if (const core::FunctionEffects *effects = unit.summaryOf(*canonical)) { - if (const core::FunctionEffects *specific = - transfer.contextSummary(call, callee, args, *effects)) - return transfer.instantiate(call, *specific, args); - if (const core::FunctionEffects *remote = - transfer.remoteContext(call, callee, args, *effects)) - return transfer.instantiate(call, *remote, args); - return transfer.instantiate(call, *effects, args); - } - if (unit.hasBody(*canonical)) - // A function of this component not yet summarised: bottom (the - // component's rounds reach the fixpoint from below, §3 step 2). - return transfer.unknownValue(call.getType()); - const OwnershipContract *contract = unit.input.kinds.ownership(callee); - bool allCovered = contract != nullptr && !contract->empty(); - if (allCovered) - for (unsigned i = 0; i < call.getNumArgs(); ++i) - if (call.getArg(i)->getType()->isPointerType()) { - bool covered = false; - for (const auto &argument : contract->arguments) - covered = covered || argument.index == i; - allCovered = allCovered && covered; - } - if (allCovered) - return applyContract(callee, *contract); - if (!governingLibraryEntry(callee, unit.library())) - if (auto declared = applyDeclared(callee)) - return *declared; - if (auto match = governingLibraryEntry(callee, unit.library())) - return applyLibrary(*match); - if (isPlatformDeclaration(callee, unit.library(), context.getSourceManager())) - return applyPlatform(callee); - if (contract != nullptr && !contract->empty()) - applyContract(callee, *contract); - return applyUnknown(false, &callee); -} - -std::optional -Transfer::slotResolution(const CallExpr &call) const { - const EngineInput &input = run.unitRun().input; - if (input.slotCollection == nullptr) - return std::nullopt; - const core::SlotSolution *solution = input.slotSolution; - if (input.database != nullptr && input.database->programFacts) - solution = &input.database->programFacts->slots; - if (solution == nullptr) - return std::nullopt; - auto slot = input.slotCollection->calleeSlot(call); - if (!slot) - return std::nullopt; - // Flow-sensitively (§9.3): a callee value that is still a parameter's - // entry value is whatever the callers passed, wherever it was stored - // since (`global = unknown; global(p)` calls `unknown`, not what the - // global's other stores hold). - if (auto it = memo.find(call.getCallee()); it != memo.end()) { - const unsigned entry = run.cfg ? run.cfg->getEntry().getBlockID() : ~0U; - if (it->second.value != core::ZeroSym && entry < run.entryStates.size() && - run.entryStates[entry]) - for (unsigned i = 0; i < run.decl().getNumParams(); ++i) { - auto held = heap.read(*run.entryStates[entry], - run.variableObject(*run.decl().getParamDecl(i)), - core::CellKey{}); - if (held && *held == it->second.value) { - slot = core::SlotKey::param( - SlotCollector::functionName(run.decl(), - input.slotCollection->unit()), - i); - break; - } - } - } - return solution->resolveCall(*slot); -} - -std::vector -Transfer::syncTargets(core::Sym function) const { - std::vector out; - const core::SymInfo &value = heap.info(state, function); - if (value.type != core::SymInfo::Type::Function || !value.functionsKnown) - return out; - for (core::Handle handle : value.functions) - if (const auto *target = fromHandle(handle)) - out.push_back(target); - return out; -} - -std::vector -Transfer::slotTargets(const core::CallResolution &resolution) const { - std::vector out; - const SlotCollection *slots = run.unitRun().input.slotCollection; - if (slots == nullptr) - return out; - for (const std::string &name : resolution.targets) - if (const FunctionDecl *fn = slots->function(name)) - out.push_back(fn); - return out; -} - -core::Sym CallApplier::apply(const FunctionDecl *callee, - core::Sym calleeValue) { - if (callee != nullptr) - return applyDirect(*callee); - const core::SymInfo &value = heap.info(state, calleeValue); - // §7 *Amendment (cross-unit contexts)*: functions of other units, by the - // summaries the program database has of them. - std::vector foreign; - bool foreignUnknown = false; - if (value.type == core::SymInfo::Type::Function && value.functionsKnown) { - const EngineInput &unitInput = transfer.functionRun().unitRun().input; - for (const std::string &name : value.foreignFunctions) { - const core::FunctionEffects *effects = - unitInput.database != nullptr ? unitInput.database->findEffects(name) - : nullptr; - if (effects == nullptr) - foreignUnknown = true; - else - foreign.push_back( - transfer.functionRun().unitRun().importEffects(name, *effects)); - } - } - if (foreignUnknown) - return applyUnknown(true, nullptr); - if (value.type == core::SymInfo::Type::Function && value.functionsKnown && - value.functions.size() == 1 && foreign.empty()) { - const auto *target = fromHandle(value.functions.front()); - if (target != nullptr) - return applyDirect(*target); - } - if (value.type == core::SymInfo::Type::Function && value.functionsKnown && - value.functions.empty() && foreign.size() == 1) - return transfer.instantiate(call, *foreign.front(), args); - std::vector targets; - if (value.type == core::SymInfo::Type::Function && value.functionsKnown) - for (core::Handle handle : value.functions) - if (const auto *target = fromHandle(handle)) - targets.push_back(target); - if (targets.empty() && foreign.empty()) - if (auto resolution = transfer.slotResolution(call); - resolution && resolution->kind != core::IndirectCallKind::OpenUnknown && - resolution->kind != core::IndirectCallKind::ClosedEmpty) { - targets = transfer.slotTargets(*resolution); - // Targets another unit defines (the program's solved slots name - // them): by their summaries, `unit:name` being `unit#name` there. - const SlotCollection *slots = - transfer.functionRun().unitRun().input.slotCollection; - const EngineInput &unitInput = transfer.functionRun().unitRun().input; - for (const std::string &name : resolution->targets) { - if (slots != nullptr && slots->function(name) != nullptr) - continue; - const core::FunctionEffects *effects = nullptr; - std::string portable = name; - if (unitInput.database != nullptr) { - effects = unitInput.database->findEffects(portable); - if (effects == nullptr) - if (std::size_t colon = name.rfind(':'); - colon != std::string::npos) { - portable = name.substr(0, colon) + "#" + name.substr(colon + 1); - effects = unitInput.database->findEffects(portable); - } - } - if (effects == nullptr) - return applyUnknown(true, nullptr); - const core::FunctionEffects *imported = - transfer.functionRun().unitRun().importEffects(portable, *effects); - // In the call's context when its unit serves it (an alias context - // for `callback(p, p)`). - if (const core::FunctionEffects *remote = transfer.remoteContext( - call, portable, shapeOf(call), args, *imported)) - imported = remote; - foreign.push_back(imported); - } - if (targets.empty() && foreign.size() == 1) - return transfer.instantiate(call, *foreign.front(), args); - } - // RFC 0005: in `weavec --whole-program` an indirect call nothing resolves - // may reach any address-taken function of its type, this unit's and - // (joined) the program's. - const core::FunctionEffects *programCandidates = nullptr; - const EngineInput &input = transfer.functionRun().unitRun().input; - if (targets.empty() && foreign.empty() && input.database != nullptr) { - QualType type = call.getCallee()->getType(); - if (type->isPointerType()) - type = type->getPointeeType(); - std::string key = functionTypeKey(type, context); - if (!key.empty()) { - targets = transfer.functionRun().unitRun().localCandidates(key); - programCandidates = input.database->candidateEffects(key); - } - } - if (targets.size() == 1 && programCandidates == nullptr && foreign.empty()) - return applyDirect(*targets.front()); - if (targets.empty() && programCandidates != nullptr) - return transfer.instantiate(call, *programCandidates, args); - if (!targets.empty() || !foreign.empty()) { - // Several targets: each on a copy, then the join (§5.4). - core::HeapState before = state; - std::optional joined; - core::Sym joinedResult = core::ZeroSym; - // (Pointer results raw from some targets and tracked from others.) - bool anyRaw = false; - bool anyTracked = false; - auto fold = [&](core::Sym result) { - if (result != core::ZeroSym && - heap.info(state, result).type == core::SymInfo::Type::Pointer && - heap.info(state, result).null != core::PointerNull::Null) - (heap.info(state, result).raw ? anyRaw : anyTracked) = true; - state.result = result; - if (!joined) - joined = state; - else - *joined = heap.join(*joined, state, handleOf(&call), - /*loopHead=*/false, before.nextSym); - }; - for (const FunctionDecl *target : targets) { - if (target == nullptr) - continue; - state = before; - fold(applyDirect(*target)); - } - for (const core::FunctionEffects *effects : foreign) { - state = before; - fold(transfer.instantiate(call, *effects, args)); - } - if (programCandidates != nullptr) { - state = before; - fold(transfer.instantiate(call, *programCandidates, args)); - } - if (joined) { - state = *joined; - joinedResult = state.result; - state.result = before.result; - if (anyRaw && anyTracked && joinedResult != core::ZeroSym && - heap.info(state, joinedResult).raw) - heap.infoMut(state, joinedResult).rawSome = true; - return joinedResult != core::ZeroSym - ? joinedResult - : transfer.unknownValue(call.getType()); - } - } - // RFC 0031 §5.4 step 1: the annotations of the function-pointer type the - // callee expression names (a typedef, a field, a parameter). - const Expr *named = call.getCallee()->IgnoreParenImpCasts(); - const Decl *declaration = nullptr; - if (const auto *ref = dyn_cast(named)) - declaration = ref->getDecl(); - else if (const auto *member = dyn_cast(named)) - declaration = member->getMemberDecl(); - if (declaration != nullptr) { - FunctionTypeAnnotations annotations = - collectFunctionTypeAnnotations(*declaration); - if (annotations.anyOwnership()) - return applyAnnotated(annotations.params, annotations.result); - } - return applyUnknown(true, nullptr); -} - -/// The single object and constant offset a pointer value points to: one -/// runtime object (§6.6: two pointers to an object that stands for several -/// need not be the same). -static std::optional> -singleTarget(core::Heap &heap, const core::HeapState &state, core::Sym sym) { - const core::SymInfo &info = heap.info(state, sym); - if (info.type != core::SymInfo::Type::Pointer || info.top || - info.targets.size() != 1 || !info.targets[0].offset.isConstant()) - return std::nullopt; - const core::ObjectInfo &object = heap.objects().info(info.targets[0].object); - if (!object.singular || object.key.kind == core::ObjectKind::Unknown) - return std::nullopt; - return std::make_pair(info.targets[0].object, - info.targets[0].offset.constant); -} - -/// §6.6: the context of a call to `definition`: which of its entry objects -/// the arguments make the same, and the integer arguments the caller knows. -static AliasContext contextOf(const FunctionRun &run, core::Heap &heap, - const core::HeapState &state, - const ParamShape &definition, - const std::vector &args) { - AliasContext alias; - auto count = static_cast(definition.size()); - std::vector>> objects( - count); - for (unsigned i = 0; i < count; ++i) { - alias.params.emplace_back(i, 0); - if (i >= args.size() || !definition[i]->isPointerType()) - continue; - objects[i] = singleTarget(heap, state, args[i]); - if (!objects[i]) - continue; - for (unsigned j = 0; j < i; ++j) - if (objects[j] && objects[j]->first == objects[i]->first) { - unsigned rep = alias.params[j].first; - std::int64_t base = objects[rep] ? objects[rep]->second : 0; - alias.params[i] = {rep, objects[i]->second - base}; - break; - } - } - // Globals whose value points into an argument's object, or into the - // object an earlier global's value points to. - std::map> - globalTargets; - for (const auto &[id, object] : state.objects) { - const core::ObjectInfo &info = run.table().info(id); - if (info.key.kind != core::ObjectKind::Global) - continue; - const auto *var = - dyn_cast_or_null(fromHandle(info.key.handle)); - if (var == nullptr || !var->getType()->isPointerType()) - continue; - const core::Sym *held = object.cells.find(core::CellKey{}); - if (held == nullptr) - continue; - auto target = singleTarget(heap, state, *held); - if (!target) - continue; - bool param = false; - for (unsigned i = 0; i < count; ++i) - if (objects[i] && objects[i]->first == target->first) { - unsigned rep = alias.params[i].first; - std::int64_t base = objects[rep] ? objects[rep]->second : 0; - alias.globals.emplace_back(var, rep, target->second - base); - param = true; - break; - } - if (param) - continue; - auto [first, inserted] = - globalTargets.try_emplace(target->first, var, target->second); - if (!inserted) - alias.globalAliases.emplace_back(var, first->second.first, - target->second - first->second.second); - } - // Pointer cells of the arguments' objects that point into one object - // (`s.a = s.b = p` passed as `&s`). - { - std::map>> - cellTargets; - for (unsigned i = 0; i < count; ++i) { - if (!objects[i] || alias.params[i].first != i) - continue; - const core::ObjectState *object = - heap.findObject(state, objects[i]->first); - if (object == nullptr) - continue; - for (const auto &[key, held] : object->cells) { - if (!key.isConcrete()) - continue; - auto target = singleTarget(heap, state, held); - if (!target || target->first == objects[i]->first) - continue; - std::int64_t cell = key.offset - objects[i]->second; - auto [first, inserted] = cellTargets.try_emplace( - target->first, i, std::make_pair(cell, target->second)); - if (!inserted) - alias.cellAliases.push_back(AliasContext::CellAlias{ - .param = i, - .cell = cell, - .repParam = first->second.first, - .repCell = first->second.second.first, - .shift = target->second - first->second.second.second}); - } - } - } - for (unsigned i = 0; i < count; ++i) { - std::optional value; - if (i < args.size() && definition[i]->isIntegerType()) { - value = state.zone.constant(args[i]); - // (An unsigned 64-bit value above `INT64_MAX`, by its bits.) - if (const core::SymInfo *info = state.syms.find(args[i]); - !value && info != nullptr && info->values) - if (auto constant = info->values->constant(); - constant && !constant->type.isSigned && constant->type.width == 64) - value = static_cast(constant->bits); - } - alias.constants.push_back(value); - } - return alias; -} - -void Transfer::checkAliasContext(const CallExpr &call, - const FunctionDecl &callee, - const std::vector &args) { - static constexpr unsigned MaxContextDepth = 3; - static constexpr std::size_t MaxContextsPerCallee = 16; - const FunctionDecl *definition = nullptr; - if (!callee.hasBody(definition) || definition == nullptr || - !run.unitRun().hasBody(callee)) - return; - AliasContext alias = contextOf(run, heap, state, shapeOf(*definition), args); - if (alias.trivial()) - return; - // A context the limits leave out: the call rests on what it would have - // shown (RFC 0030 §5.5, the shared context budget). - auto outOfBudget = [&] { - run.contextIncomplete = true; - if (run.isPublishing()) - run.ledger().decideAs( - call, core::SiteKind::Call, core::Boundary::Call, - core::Facet::Temporal, - core::FacetDecision::unresolvedFor(core::UnresolvedReason::Budget)); - }; - if (run.depth() >= MaxContextDepth) { - outOfBudget(); - return; - } - // RFC 0034 §7.2: no context run (diagnostics only) of an expensive callee. - static constexpr std::uint64_t MaxContextWork = 200000; - if (auto cost = run.unitRun().workOf.find(definition->getCanonicalDecl()); - cost == run.unitRun().workOf.end() || cost->second > MaxContextWork) { - outOfBudget(); - return; - } - std::set &done = - run.unitRun().contextsRun[definition->getCanonicalDecl()]; - core::SourceLocation at = - toCoreLocation(context.getSourceManager(), call.getBeginLoc()); - const auto reportFindings = [&](std::vector findings) { - for (core::Diagnostic &diagnostic : findings) { - core::Certainty certainty = diagnostic.certainty; - diagnostic.addNote("called here with related pointer arguments", at); - run.report(std::move(diagnostic), certainty, &call, - core::Facet::Temporal); - } - }; - if (done.contains(alias)) { - // (The replay of this caller sees what the first run of it saw.) - if (run.witnessing) - if (auto found = run.unitRun().contextFindings.find( - {definition->getCanonicalDecl(), alias}); - found != run.unitRun().contextFindings.end()) - reportFindings(found->second); - return; - } - if (done.size() >= MaxContextsPerCallee) { - outOfBudget(); - return; - } - done.insert(alias); - if (core::AnalysisStats *stats = run.unitRun().input.options.stats) - stats->add("alias_context_runs"); - LedgerAdapter collector(context, LedgerAdapter::Mode::Collecting); - FunctionRun contextRun(run.unitRun(), *definition, collector, - RunMode::Context, &done.find(alias).operator*(), - run.depth() + 1); - (void)contextRun.run(); - if (contextRun.contextIncomplete) - outOfBudget(); - std::vector findings; - for (const core::Diagnostic &diagnostic : collector.diagnostics()) - if (diagnostic.id == core::diag::UseAfterFree || - diagnostic.id == core::diag::DoubleFree || - diagnostic.id == core::diag::UseAfterMove) - findings.push_back(diagnostic); - run.unitRun().contextFindings[{definition->getCanonicalDecl(), alias}] = - findings; - reportFindings(std::move(findings)); -} - -/// §6.6 *Amendment (numeric contexts)*: whether what `effects` says of a -/// call depends on values a context can know: an integer it returns or -/// stores that is not one constant, an allocation whose size it does not -/// know, or an effect it may or may not have (not one keyed on the result, -/// which the caller's test of the result selects). -static bool dependsOnInputs(const core::FunctionEffects &effects) { - auto vague = [](const core::ValueDesc &value) { - switch (value.kind) { - case core::ValueDesc::Kind::Int: - return !value.lo || !value.hi || *value.lo != *value.hi; - case core::ValueDesc::Kind::Fresh: - return !value.extent || value.extent->path.has_value(); - case core::ValueDesc::Kind::Unknown: - return true; - default: - return false; - } - }; - // (Which of several results is returned.) - if (effects.results.size() > 1) - return true; - for (const core::ResultEffect &result : effects.results) - if (vague(result.value)) - return true; - for (const core::StoreEffect &store : effects.stores) - if (vague(store.value) || store.may) - return true; - return std::ranges::any_of( - effects.effects, [](const core::PathEffect &effect) { - return effect.may || effect.lossy || effect.when.paramZero; - }); -} - -/// §6.6 *Amendment (numeric contexts)*: the integers the objects of pointer -/// arguments hold (their first cells). -static void argumentCells(core::Heap &heap, const core::HeapState &state, - const ParamShape &definition, - const std::vector &args, - std::size_t perArgument, AliasContext &context) { - for (unsigned i = 0; i < definition.size() && i < args.size(); ++i) { - if (context.params[i].first != i || !definition[i]->isPointerType()) - continue; - auto target = singleTarget(heap, state, args[i]); - if (!target) - continue; - const core::ObjectState *object = heap.findObject(state, target->first); - if (object == nullptr) - continue; - std::size_t kept = 0; - for (const auto &[key, sym] : object->cells) { - if (!key.isConcrete() || key.offset < target->second) - continue; - const core::SymInfo *value = state.syms.find(sym); - auto number = state.zone.constant(sym); - if (value == nullptr || value->type != core::SymInfo::Type::Int || - !number || value->uninit) - continue; - context.cells.emplace_back(i, key.offset - target->second, *number); - if (++kept == perArgument) - break; - } - } -} - -const core::FunctionEffects * -Transfer::remoteContext(const CallExpr &call, const FunctionDecl &callee, - const std::vector &args, - const core::FunctionEffects &general) { - const UnitRun &unit = run.unitRun(); - if (unit.hasBody(callee) || !callee.isExternallyVisible() || - callee.getIdentifier() == nullptr || - call.getNumArgs() != callee.getNumParams()) - return nullptr; - return remoteContext(call, unit.portableName(callee), shapeOf(callee), args, - general); -} - -const core::FunctionEffects * -Transfer::remoteContext(const CallExpr &call, const std::string &callee, - const std::vector &shape, - const std::vector &args, - const core::FunctionEffects &general) { - (void)call; - // §7 *Amendment (cross-unit contexts)*: within the limits of a unit's own. - static constexpr unsigned MaxContextDepth = 2; - static constexpr std::size_t MaxRequestsPerCallee = 16; - static constexpr std::size_t MaxCellsPerArgument = 8; - UnitRun &unit = run.unitRun(); - const EngineInput &unitInput = unit.input; - if (unitInput.database == nullptr || run.depth() > MaxContextDepth || - general.incomplete || shape.size() != args.size()) - return nullptr; - AliasContext callContext = contextOf(run, heap, state, shape, args); - // (Another unit numbers the globals differently: those that point into an - // argument's object go by name; the rest are not carried.) - callContext.globalAliases.clear(); - callContext.cellAliases.clear(); - argumentCells(heap, state, shape, args, MaxCellsPerArgument, callContext); - // The callbacks the call passes, by portable name. - for (unsigned i = 0; i < shape.size() && i < args.size(); ++i) { - QualType type = shape[i]; - if (!type->isPointerType() || !type->getPointeeType()->isFunctionType()) - continue; - const core::SymInfo &value = heap.info(state, args[i]); - if (value.type != core::SymInfo::Type::Function || !value.functionsKnown || - (value.functions.empty() && value.foreignFunctions.empty())) - continue; - std::vector names = value.foreignFunctions; - for (core::Handle handle : value.functions) - if (const auto *fn = fromHandle(handle)) - names.push_back(unit.portableName(*fn)); - std::ranges::sort(names); - auto repeated = std::ranges::unique(names); - names.erase(repeated.begin(), repeated.end()); - callContext.callbacks.emplace_back(i, std::move(names)); - } - bool numeric = - !callContext.cells.empty() || - std::ranges::any_of(callContext.constants, - [](const auto &value) { return value.has_value(); }); - if (callContext.trivial() && callContext.callbacks.empty() && - !(numeric && dependsOnInputs(general))) - return nullptr; - ContextRequest request{ - .callee = callee, - .key = contextKeyText(callContext, [&](const VarDecl &var) { - return unit.portableName(var); - })}; - // Asked for as long as a call needs it (the definer serves what is asked). - std::size_t asked = 0; - for (const ContextRequest &other : unit.contextRequests) - asked += other.callee == request.callee ? 1 : 0; - if (asked < MaxRequestsPerCallee || unit.contextRequests.contains(request)) - unit.contextRequests.insert(request); - else - return nullptr; - if (const core::FunctionEffects *served = - unitInput.database->contextEffects(request)) - return unit.importEffects("context " + request.callee + " " + request.key, - *served); - return nullptr; -} - -const core::FunctionEffects * -Transfer::contextSummary(const CallExpr &call, const FunctionDecl &callee, - const std::vector &args, - const core::FunctionEffects &general) { - static constexpr unsigned MaxContextDepth = 2; - static constexpr std::size_t MaxContextsPerCallee = 8; - static constexpr std::size_t MaxCellsPerArgument = 8; - // (A context run costs what the callee's own run did: only small ones.) - static constexpr std::uint64_t MaxCalleeTransfers = 64; - UnitRun &unit = run.unitRun(); - const FunctionDecl *definition = nullptr; - auto cost = unit.transfersOf.find(callee.getCanonicalDecl()); - if (cost == unit.transfersOf.end() || cost->second > MaxCalleeTransfers || - run.depth() >= MaxContextDepth || general.incomplete || - !callee.hasBody(definition) || definition == nullptr || - !unit.hasBody(callee) || - unit.unsettled.contains(callee.getCanonicalDecl()) || - call.getNumArgs() != definition->getNumParams()) - return nullptr; - AliasContext callContext = - contextOf(run, heap, state, shapeOf(*definition), args); - argumentCells(heap, state, shapeOf(*definition), args, MaxCellsPerArgument, - callContext); - // And those of the objects the pointer globals the callee names point to. - { - class GlobalReads : public RecursiveASTVisitor { - public: - std::set globals; - // RecursiveASTVisitor's CRTP hooks are found by name. - // NOLINTNEXTLINE(readability-identifier-naming,bugprone-derived-method-shadowing-base-method) - bool VisitDeclRefExpr(DeclRefExpr *ref) { - if (const auto *var = dyn_cast(ref->getDecl()); - var != nullptr && var->hasGlobalStorage() && - var->getType()->isPointerType()) - globals.insert(var->getCanonicalDecl()); - return true; - } - } reads; - reads.TraverseStmt(definition->getBody()); - for (const VarDecl *var : reads.globals) { - const core::ObjectState *holder = - heap.findObject(state, run.variableObject(*var)); - const core::Sym *held = - holder != nullptr ? holder->cells.find(core::CellKey{}) : nullptr; - auto target = - held != nullptr ? singleTarget(heap, state, *held) : std::nullopt; - const core::ObjectState *object = - target ? heap.findObject(state, target->first) : nullptr; - if (object == nullptr) - continue; - std::size_t kept = 0; - for (const auto &[key, sym] : object->cells) { - if (!key.isConcrete() || key.offset < target->second) - continue; - const core::SymInfo *value = state.syms.find(sym); - auto number = state.zone.constant(sym); - if (value == nullptr || value->type != core::SymInfo::Type::Int || - !number || value->uninit) - continue; - callContext.globalCells.emplace_back(var, key.offset - target->second, - *number); - if (++kept == MaxCellsPerArgument) - break; - } - } - } - bool numeric = - !callContext.cells.empty() || !callContext.globalCells.empty() || - std::ranges::any_of(callContext.constants, - [](const auto &value) { return value.has_value(); }); - if ((!callContext.trivial() || numeric) && - (!callContext.trivial() || dependsOnInputs(general))) { - auto &known = unit.contextSummaries[definition->getCanonicalDecl()]; - auto found = known.find(callContext); - if (found == known.end()) { - if (known.size() >= MaxContextsPerCallee) - return nullptr; - found = known.emplace(callContext, std::nullopt).first; - if (core::AnalysisStats *stats = unit.input.options.stats) - stats->add("context_runs"); - FunctionRun contextRun(unit, *definition, unit.discarding, - RunMode::Summary, &found->first, run.depth() + 1); - RunResult result = contextRun.run(); - if (!result.overBudget && !result.effects.incomplete) - found->second = std::move(result.effects); - if (std::getenv("WEAVEC_ENGINE_DUMP") != nullptr && found->second) - llvm::errs() << "context summary " << definition->getNameAsString() - << "\n" - << core::toText(*found->second); - } - if (found->second) - return &*found->second; - } - return nullptr; -} - -core::Sym Transfer::call(const CallExpr &callExpr) { - // Builtins that only compute a value. - if (const FunctionDecl *direct = callExpr.getDirectCallee()) { - switch (direct->getBuiltinID()) { - case Builtin::BI__builtin_expect: - case Builtin::BI__builtin_expect_with_probability: - case Builtin::BI__builtin_assume_aligned: - if (callExpr.getNumArgs() > 0) - return valueOf(*callExpr.getArg(0)); - break; - case Builtin::BI__builtin_unreachable: - case Builtin::BI__builtin_trap: - case Builtin::BI__builtin_debugtrap: - case Builtin::BI__builtin_verbose_trap: - state.unreachable = true; - return unknownValue(callExpr.getType()); - case Builtin::BI__builtin_object_size: - case Builtin::BI__builtin_dynamic_object_size: - case Builtin::BI__builtin_constant_p: - case Builtin::BI__builtin_classify_type: - return unknownValue(callExpr.getType()); - // `va_start` and `va_copy` initialise the `va_list` they are given (by - // reference): its bytes are the implementation's, never uninitialised. - case Builtin::BI__builtin_va_start: -#if CLANG_VERSION_MAJOR >= 20 - case Builtin::BI__builtin_c23_va_start: -#endif - case Builtin::BI__va_start: - case Builtin::BI__builtin_va_copy: { - for (unsigned i = 1; i < callExpr.getNumArgs(); ++i) - (void)valueOf(*callExpr.getArg(i)); - if (callExpr.getNumArgs() > 0) { - const Expr *list = callExpr.getArg(0)->IgnoreParenImpCasts(); - if (list->isGLValue()) - for (const core::Target &target : addressOf(*list).targets) { - core::ObjectState &object = heap.ensure(state, target.object); - object.uninitialised = false; - heap.forgetCells(state, target.object, 0, std::nullopt); - } - } - return unknownValue(callExpr.getType()); - } - default: - break; - } - } - // RFC 0030 §6.2: `WEAVEC_ASSUME`. - if (assumption(callExpr)) - return unknownValue(callExpr.getType()); - std::vector args; - args.reserve(callExpr.getNumArgs()); - for (const Expr *arg : callExpr.arguments()) { - if (arg->getType()->isRecordType()) { - (void)evaluate(*arg); - args.push_back(core::ZeroSym); - } else { - args.push_back(valueOf(*arg)); - } - } - // RFC 0030 §7.6: an object handed to a callee meets the invariants. - if (run.checkingInvariants && run.inFinalPass) - for (core::Sym arg : args) - if (arg != core::ZeroSym) - for (const core::Target &target : heap.info(state, arg).targets) - if (target.offset == core::Term::of(0)) - run.checkInvariants(state, target.object); - core::Sym calleeValue = core::ZeroSym; - const FunctionDecl *direct = callExpr.getDirectCallee(); - if (direct == nullptr) - calleeValue = valueOf(*callExpr.getCallee()); - if (run.isPublishing()) { - decideCall(callExpr, args, calleeValue); - const FunctionDecl *target = direct; - if (target == nullptr) { - const core::SymInfo &value = heap.info(state, calleeValue); - if (value.type == core::SymInfo::Type::Function && value.functionsKnown && - value.functions.size() == 1) - target = fromHandle(value.functions.front()); - } - if (target != nullptr) - checkAliasContext(callExpr, *target, args); - callBoundary(callExpr, args); - } - CallApplier applier(*this, callExpr, args); - core::Sym result = applier.apply(direct, calleeValue); - if (result == core::ZeroSym) - result = unknownValue(callExpr.getType()); - // A callee declared not to return does not (C11 6.7.4: returning from - // one is undefined), whatever its summary could show. - if (direct != nullptr && direct->isNoReturn()) - state.unreachable = true; - // RFC 0017: the overflow-checking builtins' values (their rows give only - // the write through the result pointer). - if (auto op = checkedIntegerOp(callExpr); op && args.size() == 3) - result = checkedArithmetic(callExpr, *op, args, result); - return result; -} - -} // namespace weavec::analysis::engine diff --git a/lib/Analysis/EngineContexts.cpp b/lib/Analysis/EngineContexts.cpp deleted file mode 100644 index 6be30b86..00000000 --- a/lib/Analysis/EngineContexts.cpp +++ /dev/null @@ -1,281 +0,0 @@ -//===- EngineContexts.cpp - Contexts across units (RFC 0031 §7) -----------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// RFC 0031 §6.6, §7 *Amendment (cross-unit contexts)*: a call into another -// unit asks for the callee's summary in the call's context (the arguments -// it makes one object, the integers it knows, the callbacks it passes); -// the defining unit runs the context and exports its summary, and reports -// what an aliased context finds inside the callee. -// -//===----------------------------------------------------------------------===// - -#include "Engine.h" -#include "weavec/Analysis/ClangLocation.h" -#include "weavec/Core/Effects.h" - -#include "clang/Basic/SourceManager.h" - -#include "llvm/ADT/StringExtras.h" - -#include - -using namespace clang; - -namespace weavec::analysis::engine { - -/// A function's portable name with the characters the key uses escaped. -static std::string escapeName(llvm::StringRef name) { - std::string out; - for (char c : name) { - if (c == '%' || c == ' ' || c == ',' || c == ':' || c == '=' || c == ';') { - static constexpr const char *Hex = "0123456789ABCDEF"; - out += '%'; - const unsigned byte = static_cast(c); - out += Hex[(byte >> 4U) & 0xFU]; - out += Hex[byte & 0xFU]; - } else { - out += c; - } - } - return out; -} - -static std::optional unescapeName(llvm::StringRef text) { - std::string out; - for (std::size_t i = 0; i < text.size(); ++i) { - if (text[i] != '%') { - out += text[i]; - continue; - } - unsigned value = 0; - if (i + 2 >= text.size() || text.substr(i + 1, 2).getAsInteger(16, value)) - return std::nullopt; - out += static_cast(value); - i += 2; - } - return out; -} - -std::string -contextKeyText(const AliasContext &context, - const std::function &globalName) { - // `a=::;…`, `c=:;…`, - // `m=::;…`, `f=:,;…`, each - // section only when it says something. - std::string aliases; - for (unsigned i = 0; i < context.params.size(); ++i) - if (context.params[i].first != i) - aliases += std::to_string(i) + ":" + - std::to_string(context.params[i].first) + ":" + - std::to_string(context.params[i].second) + ";"; - std::string constants; - for (unsigned i = 0; i < context.constants.size(); ++i) - if (context.constants[i]) - constants += - std::to_string(i) + ":" + std::to_string(*context.constants[i]) + ";"; - std::string cells; - for (const auto &[param, offset, value] : context.cells) - cells += std::to_string(param) + ":" + std::to_string(offset) + ":" + - std::to_string(value) + ";"; - std::string callbacks; - for (const auto &[param, names] : context.callbacks) { - callbacks += std::to_string(param) + ":"; - for (std::size_t i = 0; i < names.size(); ++i) - callbacks += (i == 0 ? "" : ",") + escapeName(names[i]); - callbacks += ';'; - } - std::string globals; - for (const auto &[var, rep, offset] : context.globals) - globals += escapeName(globalName(*var)) + ":" + std::to_string(rep) + ":" + - std::to_string(offset) + ";"; - std::string out = "n=" + std::to_string(context.params.size()); - if (!aliases.empty()) - out += " a=" + aliases; - if (!constants.empty()) - out += " c=" + constants; - if (!cells.empty()) - out += " m=" + cells; - if (!callbacks.empty()) - out += " f=" + callbacks; - if (!globals.empty()) - out += " g=" + globals; - return out; -} - -std::optional parseContextKey( - llvm::StringRef text, unsigned params, - const std::function &global) { - AliasContext context; - for (unsigned i = 0; i < params; ++i) - context.params.emplace_back(i, 0); - context.constants.assign(params, std::nullopt); - llvm::SmallVector sections; - text.split(sections, ' ', -1, false); - auto number = [](llvm::StringRef part, auto &value) { - return !part.getAsInteger(10, value); - }; - for (llvm::StringRef section : sections) { - auto [name, body] = section.split('='); - llvm::SmallVector items; - body.split(items, ';', -1, false); - if (name == "n") { - unsigned count = 0; - if (!number(body, count) || count != params) - return std::nullopt; - continue; - } - for (llvm::StringRef item : items) { - if (name == "g") { - // `::`, the name first (escaped). - auto [spelled, rest] = item.split(':'); - auto [repText, offsetText] = rest.split(':'); - auto decoded = unescapeName(spelled); - unsigned rep = 0; - std::int64_t offset = 0; - if (!decoded || !number(repText, rep) || !number(offsetText, offset) || - rep >= params) - return std::nullopt; - // (A global the callee's unit does not see is no binding.) - if (const VarDecl *var = global(*decoded)) - context.globals.emplace_back(var, rep, offset); - continue; - } - llvm::SmallVector fields; - item.split(fields, ':', name == "f" ? 1 : -1, true); - unsigned param = 0; - if (fields.empty() || !number(fields[0], param) || param >= params) - return std::nullopt; - if (name == "a" && fields.size() == 3) { - unsigned rep = 0; - std::int64_t offset = 0; - if (!number(fields[1], rep) || !number(fields[2], offset) || - rep >= param) - return std::nullopt; - context.params[param] = {rep, offset}; - } else if (name == "c" && fields.size() == 2) { - std::int64_t value = 0; - if (!number(fields[1], value)) - return std::nullopt; - context.constants[param] = value; - } else if (name == "m" && fields.size() == 3) { - std::int64_t offset = 0; - std::int64_t value = 0; - if (!number(fields[1], offset) || !number(fields[2], value)) - return std::nullopt; - context.cells.emplace_back(param, offset, value); - } else if (name == "f" && fields.size() == 2) { - llvm::SmallVector names; - fields[1].split(names, ',', -1, false); - std::vector decoded; - for (llvm::StringRef each : names) { - auto unescaped = unescapeName(each); - if (!unescaped) - return std::nullopt; - decoded.push_back(std::move(*unescaped)); - } - context.callbacks.emplace_back(param, std::move(decoded)); - } else { - return std::nullopt; - } - } - } - return context; -} - -std::string UnitRun::portableName(const FunctionDecl &fn) const { - if (fn.isExternallyVisible()) - return fn.getNameAsString(); - const SourceManager &sm = context().getSourceManager(); - std::string source; - if (const auto entry = sm.getFileEntryRefForID(sm.getMainFileID())) - source = entry->getName().str(); - return source + "#" + fn.getNameAsString(); -} - -const VarDecl *UnitRun::globalNamed(const std::string &portable) const { - if (!globalsByName) { - globalsByName.emplace(); - for (const Decl *decl : context().getTranslationUnitDecl()->decls()) - if (const auto *var = dyn_cast(decl); - var != nullptr && var->hasGlobalStorage()) - globalsByName->try_emplace(portableName(*var), var->getCanonicalDecl()); - } - auto it = globalsByName->find(portable); - return it != globalsByName->end() ? it->second : nullptr; -} - -const FunctionDecl *UnitRun::functionNamed(const std::string &portable) const { - if (!functionsByName) { - functionsByName.emplace(); - for (const Decl *decl : context().getTranslationUnitDecl()->decls()) - if (const auto *fn = dyn_cast(decl); - fn != nullptr && fn->getIdentifier() != nullptr) { - // The definition when there is one, else a declaration. - auto [it, inserted] = - functionsByName->try_emplace(portableName(*fn), fn); - if (!inserted && fn->doesThisDeclarationHaveABody()) - it->second = fn; - } - } - auto it = functionsByName->find(portable); - return it != functionsByName->end() ? it->second : nullptr; -} - -void UnitRun::serveContextRequests( - const std::function &shouldReport) { - // A callee's contexts, at most as many as a unit's own (§6.6), and only - // of a callee whose own run was small, as a unit's own contexts - // (`Transfer::contextSummary`): a context run costs what that run did, - // twice (the caller keeps the callee's summary). - static constexpr std::size_t MaxContextsPerCallee = 16; - static constexpr std::uint64_t MaxCalleeTransfers = 64; - if (input.database == nullptr) - return; - std::map served; - for (const ContextRequest &request : input.database->requests()) { - const FunctionDecl *fn = functionNamed(request.callee); - if (fn == nullptr || !hasBody(*fn)) - continue; - if (auto cost = transfersOf.find(fn->getCanonicalDecl()); - cost != transfersOf.end() && cost->second > MaxCalleeTransfers) - continue; - if (++served[request.callee] > MaxContextsPerCallee) - continue; - std::optional parsed = parseContextKey( - request.key, fn->getNumParams(), - [&](const std::string &name) { return globalNamed(name); }); - if (!parsed) - continue; - // The summary run: what the caller's unit instantiates. - FunctionRun run(*this, *fn, discarding, RunMode::Summary, &*parsed, 1); - RunResult result = run.run(); - if (!result.overBudget && !result.effects.incomplete) - servedContexts[request] = std::move(result.effects); - // §6.6: a context that makes arguments one object reports what that - // finds inside the callee, here. - if (parsed->trivial() || !shouldReport(*fn)) - continue; - LedgerAdapter collector(context(), LedgerAdapter::Mode::Collecting); - FunctionRun checked(*this, *fn, collector, RunMode::Context, &*parsed, 1); - (void)checked.run(); - for (core::Diagnostic diagnostic : collector.diagnostics()) { - if (diagnostic.id != core::diag::UseAfterFree && - diagnostic.id != core::diag::DoubleFree && - diagnostic.id != core::diag::UseAfterMove) - continue; - core::Certainty certainty = diagnostic.certainty; - diagnostic.addNote( - "called from another unit with related pointer " - "arguments", - toCoreLocation(context().getSourceManager(), fn->getLocation())); - authoritative.report(std::move(diagnostic), certainty); - } - } -} - -} // namespace weavec::analysis::engine diff --git a/lib/Analysis/EngineDecide.cpp b/lib/Analysis/EngineDecide.cpp deleted file mode 100644 index df41c457..00000000 --- a/lib/Analysis/EngineDecide.cpp +++ /dev/null @@ -1,2470 +0,0 @@ -//===- EngineDecide.cpp - Site decisions in the object engine -------------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// RFC 0031 §5.2–§5.3: each facet of each site `SiteCollector` enumerated is -// decided from the operand's value at the site, by RFC 0030 §3's tables, -// with RFC 0030's messages; a message names the C places that hold the -// symbols its terms are over. -// -//===----------------------------------------------------------------------===// - -#include "Engine.h" -#include "weavec/Analysis/ClangLocation.h" -#include "weavec/Analysis/KindTable.h" -#include "weavec/Analysis/SiteCollector.h" - -#include "clang/AST/RecordLayout.h" -#include "clang/Basic/SourceManager.h" - -#include -#include -#include - -using namespace clang; - -namespace weavec::analysis::engine { - -static core::Diagnostic makeDiagnostic(std::string_view id, std::string message, - const ASTContext &context, - SourceLocation at, - core::Severity severity) { - core::Diagnostic diagnostic; - diagnostic.id = id; - diagnostic.message = std::move(message); - diagnostic.severity = severity; - diagnostic.location = toCoreLocation(context.getSourceManager(), at); - return diagnostic; -} - -static void addNote(core::Diagnostic &diagnostic, std::string message, - const core::SourceLocation &at) { - if (!at.isValid()) - return; - diagnostic.addNote(std::move(message), at); -} - -/// The object width of `type` (RFC 0030 §7.4): `sizeof`, or the offset of a -/// flexible trailing array member. -static std::optional objectWidth(const ASTContext &context, - QualType type) { - if (type.isNull() || type->isIncompleteType() || type->isVoidType() || - type->isFunctionType()) - return type.isNull() || !type->isVoidType() - ? std::nullopt - : std::optional(1); - auto size = - static_cast(context.getTypeSizeInChars(type).getQuantity()); - if (const RecordDecl *record = type->getAsRecordDecl(); - record != nullptr && !record->isUnion() && - record->isCompleteDefinition()) { - const FieldDecl *last = nullptr; - for (const FieldDecl *field : record->fields()) - last = field; - if (last != nullptr && (last->getType()->isIncompleteArrayType() || - context.getAsConstantArrayType(last->getType()))) { - bool flexible = last->getType()->isIncompleteArrayType(); - if (!flexible) - flexible = context.getLangOpts().getStrictFlexArraysLevel() == - LangOptions::StrictFlexArraysLevelKind::Default; - if (flexible) { - const ASTRecordLayout &layout = context.getASTRecordLayout(record); - return static_cast( - layout.getFieldOffset(last->getFieldIndex()) / - context.getCharWidth()); - } - } - } - return size; -} - -static std::string inQuotes(const std::string &name) { - return "'" + name + "'"; -} - -/// The members that lead from the start of a `type` object to the integer -/// or pointer at `offset` (`inner.cap`); none through arrays, unions and -/// bit-fields (§5.3). -static std::optional> -memberPath(const ASTContext &context, QualType type, std::int64_t offset) { - std::vector names; - type = type.getCanonicalType(); - for (int depth = 0; depth < 8; ++depth) { - const RecordDecl *record = type->getAsRecordDecl(); - if (record == nullptr || record->isUnion() || - !record->isCompleteDefinition()) - return std::nullopt; - const ASTRecordLayout &layout = context.getASTRecordLayout(record); - const FieldDecl *found = nullptr; - for (const FieldDecl *field : record->fields()) { - if (field->isBitField() || field->getType()->isIncompleteType() || - field->getName().empty()) - continue; - auto start = static_cast( - layout.getFieldOffset(field->getFieldIndex()) / - context.getCharWidth()); - auto size = static_cast( - context.getTypeSizeInChars(field->getType()).getQuantity()); - if (offset >= start && offset < start + size) { - found = field; - offset -= start; - break; - } - } - if (found == nullptr) - return std::nullopt; - names.push_back(found->getNameAsString()); - type = found->getType().getCanonicalType(); - if (type->isRecordType()) - continue; - if (offset == 0 && (type->isIntegerType() || type->isPointerType())) - return names; - return std::nullopt; - } - return std::nullopt; -} - -//===----------------------------------------------------------------------===// -// Source terms (§5.3) -//===----------------------------------------------------------------------===// - -std::string messageSpelling(const SourceTerm &term) { - if (term.kind != SourceTerm::Kind::Place || term.decl == nullptr) - return term.toString(); - std::string text = term.decl->getNameAsString(); - bool pendingDeref = false; - for (const SourceTerm::Step &step : term.path) { - if (step.kind == SourceTerm::Step::Kind::Deref) { - if (pendingDeref) - text.insert(text.begin(), '*'); - pendingDeref = true; - continue; - } - text += (pendingDeref ? "->" : ".") + step.field; - pendingDeref = false; - } - if (pendingDeref) - text = "*" + text; - return text; -} - -std::optional Transfer::nameOf(core::Sym sym, bool extent, - int depth) const { - if (sym == core::ZeroSym) - return std::nullopt; - if (auto c = state.zone.constant(sym)) - return SourceTerm::ofConstant(*c); - for (const auto &[id, object] : state.objects) { - const core::ObjectInfo &info = run.table().info(id); - if (info.key.kind != core::ObjectKind::Local && - info.key.kind != core::ObjectKind::Global) - continue; - if (object.life != core::Life::Live) - continue; - const VarDecl *var = variableOf(info); - if (var == nullptr || var->getType().isVolatileQualified()) - continue; - if (const core::Sym *held = object.cells.find(core::CellKey{}); - held != nullptr && *held == sym && !var->getType()->isRecordType() && - !var->getType()->isArrayType()) - return SourceTerm::ofPlace(*var); - } - // A member of a local struct (`s.cap`), or of the object a local pointer - // points to the start of (`b->cap`), whose cell holds the symbol. A read - // through a pointer not known to be non-null is checked by the term - // (RFC 0030 §10.3 rule 5). - for (const auto &[id, object] : state.objects) { - const core::ObjectInfo &holder = run.table().info(id); - if (holder.key.kind != core::ObjectKind::Local || - object.life != core::Life::Live) - continue; - const VarDecl *var = variableOf(holder); - if (var == nullptr || var->getType().isVolatileQualified() || - var->hasGlobalStorage()) - continue; - QualType type = var->getType().getCanonicalType(); - if (type->isRecordType()) { - for (const auto &[key, held] : object.cells) - if (held == sym && !key.isSummary()) - if (auto members = memberPath(context, type, key.offset)) { - std::vector path; - for (std::string &member : *members) - path.push_back(SourceTerm::Step::member(std::move(member))); - return SourceTerm::ofPlace(*var, std::move(path)); - } - continue; - } - if (!type->isPointerType()) - continue; - const core::Sym *pointer = object.cells.find(core::CellKey{}); - if (pointer == nullptr) - continue; - const core::SymInfo &value = heap.info(state, *pointer); - if (value.type != core::SymInfo::Type::Pointer || value.top || - value.targets.size() != 1 || !value.targets[0].offset.isConstant() || - value.targets[0].offset.constant != 0) - continue; - const core::ObjectState *target = - heap.findObject(state, value.targets[0].object); - if (target == nullptr || target->life != core::Life::Live) - continue; - for (const auto &[key, held] : target->cells) - if (held == sym && !key.isSummary()) - if (auto members = - memberPath(context, type->getPointeeType(), key.offset)) { - std::vector path{SourceTerm::Step::deref()}; - for (std::string &member : *members) - path.push_back(SourceTerm::Step::member(std::move(member))); - return SourceTerm::ofPlace(*var, std::move(path)); - } - } - const core::SymInfo &info = heap.info(state, sym); - if (info.linear && info.linear->var != sym && info.linear->known && - depth < 8) { - auto base = nameOf(info.linear->var, extent, depth + 1); - if (base) { - SourceTerm term = std::move(*base); - if (info.linear->scale != 1) - term = SourceTerm::mul(std::move(term), - SourceTerm::ofConstant(info.linear->scale)); - if (info.linear->constant > 0) - term = SourceTerm::add(std::move(term), - SourceTerm::ofConstant(info.linear->constant)); - else if (info.linear->constant < 0) - term = SourceTerm::sub(std::move(term), - SourceTerm::ofConstant(-info.linear->constant)); - return term; - } - } - // §5.3: the symbol's defining operation over nameable operands. A term - // is computed in 64 bits by the prelude's helpers, which saturate an - // extent that overflows to 0 (RFC 0030 §10.2), so a `size_t` sum or - // product that may wrap is still a sound extent; a narrower one is spelled - // only when it cannot wrap (§7.4 *Arithmetic*). - if (extent && info.defined && info.intType && depth < 8) { - const core::SymDefinition &definition = *info.defined; - bool wide = !info.intType->isSigned && info.intType->width == 64 && - (definition.op == core::IntegerOp::Add || - definition.op == core::IntegerOp::Subtract || - definition.op == core::IntegerOp::Multiply); - if (definition.exact || wide) { - auto left = nameOf(definition.left, extent, depth + 1); - std::optional right; - if (left) - right = - definition.constant - ? std::optional(SourceTerm::ofConstant(*definition.constant)) - : nameOf(definition.right, extent, depth + 1); - if (left && right) { - switch (definition.op) { - case core::IntegerOp::Add: - return SourceTerm::add(std::move(*left), std::move(*right)); - case core::IntegerOp::Subtract: - return SourceTerm::sub(std::move(*left), std::move(*right)); - case core::IntegerOp::Multiply: - return SourceTerm::mul(std::move(*left), std::move(*right)); - case core::IntegerOp::Divide: - if (right->kind == SourceTerm::Kind::Constant && right->constant > 0) - return SourceTerm::div(std::move(*left), right->constant); - break; - default: - break; - } - } - } - } - return std::nullopt; -} - -std::optional Transfer::spellAmount(core::Term bytes, - bool own) const { - if (!bytes.known) - return std::nullopt; - // A constant added to a named value reads as `'n' + 1` (in messages the - // sum is the mathematical one). - for (int depth = 0; depth < 8 && !bytes.isConstant(); ++depth) { - const core::SymInfo &info = heap.info(state, bytes.var); - if (!info.name.empty() || nameOf(bytes.var).has_value() || !info.defined || - !info.defined->constant || - (info.defined->op != core::IntegerOp::Add && - info.defined->op != core::IntegerOp::Subtract)) - break; - std::int64_t step = info.defined->op == core::IntegerOp::Add - ? *info.defined->constant - : -*info.defined->constant; - bytes = core::Term::ofSym(info.defined->left, bytes.scale, - bytes.constant + (bytes.scale * step)); - } - if (bytes.isConstant()) - return std::to_string(bytes.constant) + " bytes"; - // A need prefers the name its value was made under (`strlen(s)`), an - // extent the C place that holds it. - const std::string &made = heap.info(state, bytes.var).name; - std::string spelled = made; - if (!own || made.empty()) - if (auto name = nameOf(bytes.var, /*extent=*/true)) - spelled = messageSpelling(*name); - if (spelled.empty()) - return std::nullopt; - std::string text = "'" + spelled + "'"; - if (bytes.scale != 1) - text += " * " + std::to_string(bytes.scale); - if (bytes.constant > 0) - text += " + " + std::to_string(bytes.constant); - else if (bytes.constant < 0) - text += " - " + std::to_string(-bytes.constant); - return text + " bytes"; -} - -//===----------------------------------------------------------------------===// -// Accesses -//===----------------------------------------------------------------------===// - -namespace { -/// The decision helpers for one site. -class Decider { -public: - Decider(Transfer &transfer, const SiteInfo &site) - : transfer(transfer), run(transfer.functionRun()), - heap(transfer.domain()), state(transfer.heapState()), out(run.ledger()), - context(run.ast()), site(site) {} - - void access(); - void release(core::Sym pointer, const std::string &family); - void conflictingBorrow(core::Sym pointer); - void invalidRelease(core::Sym pointer); - void unknownCall(const CallExpr &call, bool callback); - void temporalOf(core::Sym pointer, const Expr &operand, bool isRelease); - /// RFC 0030 §8.2 `reads(S)`: the call uses the value slot `S` retains. - void readsState(const std::string &slot); - void nullOf(core::Sym pointer, const Expr &operand); - - bool applies(core::Facet facet) const { return run.applies(site.id, facet); } - void decide(core::Facet facet, core::FacetDecision decision) { - if (!run.isPublishing() || !applies(facet)) - return; - // RFC 0017 §5: a requirement's count bounds an index from above only; - // an index that may be negative lies before the object whatever the - // callers pass, so no requirement covers its spatial facet. - if ((site.kind == core::SiteKind::Deref || - site.kind == core::SiteKind::Index) && - (facet != core::Facet::Spatial || !mayBeNegativeIndex())) - decision = transfer.covered(site, facet, std::move(decision)); - out.decideAs(*site.stmt, site.kind, site.boundary, facet, decision); - } - void report(core::Diagnostic diagnostic, core::Certainty certainty, - core::Facet facet) { - run.report(std::move(diagnostic), certainty, site.stmt, facet); - } - -private: - Transfer &transfer; - FunctionRun &run; - core::Heap &heap; - core::HeapState &state; - LedgerAdapter &out; - ASTContext &context; - const SiteInfo &site; - - /// `beyond`: the index is above `INT64_MAX` for every value, so the - /// access lies past the end of any object (RFC 0017). - void spatialOf(core::Sym pointer, std::int64_t width, bool beyond = false); - bool writesLiteral(core::Sym pointer, const Expr &operand); - bool memberBound(std::int64_t width); - std::string indexText(const Expr &index); - /// The site's index may be negative. - bool mayBeNegativeIndex() { - if (site.index == nullptr || !site.index->getType()->isSignedIntegerType()) - return false; - auto lower = state.zone.lower(transfer.valueOf(*site.index)); - return !lower || *lower < 0; - } -}; -} // namespace - -void Decider::nullOf(core::Sym pointer, const Expr &operand) { - if (!applies(core::Facet::Null)) - return; - const core::SymInfo &value = heap.info(state, pointer); - std::string name = transfer.spell(operand); - if (value.uninit && value.null == core::PointerNull::Null) { - decide(core::Facet::Null, core::FacetDecision::violation()); - report(makeDiagnostic( - core::diag::UseOfUninitialized, - "use of " + inQuotes(name) + " before it was initialized", - context, operand.getBeginLoc(), core::Severity::Error), - core::Certainty::Definite, core::Facet::Null); - return; - } - switch (value.null) { - case core::PointerNull::NonNull: - decide(core::Facet::Null, core::FacetDecision::proven()); - return; - case core::PointerNull::Null: - if (!value.allocatorSource && !value.mayUninit) { - decide(core::Facet::Null, core::FacetDecision::violation()); - core::Diagnostic diagnostic = - makeDiagnostic(core::diag::NullDereference, - "dereference of " + inQuotes(name) + ", which is null", - context, operand.getBeginLoc(), core::Severity::Error); - transfer.addNullNote(diagnostic, value, name); - report(std::move(diagnostic), core::Certainty::Definite, - core::Facet::Null); - return; - } - decide(core::Facet::Null, core::FacetDecision::unresolvedFor( - core::UnresolvedReason::Undecided)); - if (applies(core::Facet::Null)) - transfer.allocationFailure(value, operand, *site.stmt); - return; - case core::PointerNull::Maybe: - // RFC 0030 §11: without zero-initialisation a path that never assigned - // the pointer leaves garbage, which a null check cannot catch; a - // declaration a jump bypasses is not zero-initialised either. - if ((value.uninit || value.mayUninit) && - (!run.unitRun().input.options.zeroInit || run.isBypassed(operand))) { - decide(core::Facet::Null, core::FacetDecision::unresolvedFor( - core::UnresolvedReason::NoZeroInit, - inQuotes(name) + " may be uninitialised")); - return; - } - if (site.nullSystemApi) { - decide(core::Facet::Null, - core::FacetDecision::trustedFor(core::TrustReason::SystemApi)); - return; - } - decide(core::Facet::Null, core::FacetDecision::unresolvedFor( - core::UnresolvedReason::Undecided)); - transfer.allocationFailure(value, operand, *site.stmt); - return; - } -} - -void Decider::readsState(const std::string &slot) { - if (!applies(core::Facet::Temporal)) - return; - auto held = heap.read(state, run.stateObject(slot), core::CellKey{}); - if (!held || heap.info(state, *held).type != core::SymInfo::Type::Pointer) - return; - core::TemporalVerdict verdict = heap.temporal(state, *held); - if (!verdict.record || - verdict.record->reason == core::ReleaseRecord::Reason::Moved || - (verdict.kind != core::TemporalVerdict::Kind::Violation && - verdict.kind != core::TemporalVerdict::Kind::MayReleased)) - return; - bool definite = verdict.kind == core::TemporalVerdict::Kind::Violation; - std::string name = "<" + slot + ">"; - decide(core::Facet::Temporal, definite - ? core::FacetDecision::violation() - : core::FacetDecision::unresolvedFor( - core::UnresolvedReason::MayReleased)); - core::Diagnostic diagnostic = makeDiagnostic( - core::diag::UseAfterFree, - "use of " + inQuotes(name) + - (definite ? " after it was freed" : " after it may have been freed"), - context, site.stmt->getBeginLoc(), - definite ? core::Severity::Error : core::Severity::Warning); - addNote(diagnostic, definite ? "freed here" : "freed here on some paths", - verdict.record->where); - report(std::move(diagnostic), - definite ? core::Certainty::Definite : core::Certainty::Possible, - core::Facet::Temporal); -} - -void Decider::temporalOf(core::Sym pointer, const Expr &operand, - bool isRelease) { - if (!applies(core::Facet::Temporal)) - return; - const core::SymInfo &value = heap.info(state, pointer); - if (value.rawCast && !isRelease) { - decide(core::Facet::Temporal, - core::FacetDecision::unresolvedFor(core::UnresolvedReason::RawCast)); - return; - } - core::TemporalVerdict verdict = heap.temporal(state, pointer); - std::string name = transfer.spell(operand); - using Kind = core::TemporalVerdict::Kind; - using Reason = core::ReleaseRecord::Reason; - auto verbFor = [](const core::ReleaseRecord &record) -> std::string { - switch (record.reason) { - case Reason::Moved: - return "moved"; - case Reason::ShareReleased: - return "reference was released"; - default: - return "freed"; - } - }; - auto idFor = [&](const core::ReleaseRecord &record) { - if (isRelease && record.reason != Reason::Moved) - return core::diag::DoubleFree; - return record.reason == Reason::Moved ? core::diag::UseAfterMove - : core::diag::UseAfterFree; - }; - auto noteFor = [&](const core::ReleaseRecord &record, bool some, - core::Diagnostic &diagnostic) { - std::string verb = record.reason == Reason::Moved ? "moved" : "freed"; - if (record.reason == Reason::ShareReleased) - verb = "reference released"; - if (isRelease) { - verb = record.reason == Reason::ShareReleased ? "released" : "freed"; - addNote(diagnostic, - "previously " + verb + " here" + (some ? " on some paths" : ""), - record.where); - return; - } - std::string text = verb + " here"; - if (some) - text += " on some paths"; - if (!record.via.empty() && record.via != name) - text += " (through " + inQuotes(record.via) + ")"; - addNote(diagnostic, text, record.where); - }; - switch (verdict.kind) { - case Kind::Proven: { - // A borrow of a library slot's storage lives as long as the row says - // (RFC 0030 §8.2). - const core::SymInfo &borrow = heap.info(state, pointer); - bool stateOnly = !borrow.top && !borrow.targets.empty(); - for (const core::Target &target : borrow.targets) - stateOnly = stateOnly && run.isStateObject(target.object); - decide(core::Facet::Temporal, - stateOnly - ? core::FacetDecision::trustedFor(core::TrustReason::LibrarySpec) - : core::FacetDecision::proven()); - // RFC 0031 amends RFC 0030 §9.4: the proof rests on the entry - // assumption of the places the pointer (or the value it was derived - // from) was loaded from at entry. - if (!stateOnly && run.isPublishing() && applies(core::Facet::Temporal)) { - std::vector origins{pointer}; - origins.insert(origins.end(), borrow.ancestors.begin(), - borrow.ancestors.end()); - for (core::Sym origin : origins) - if (const core::SymInfo *held = state.syms.find(origin)) - for (const auto &[object, key] : held->entryOrigins) - out.reliesOn(site.id, cellClass(run, object, key)); - } - return; - } - case Kind::Violation: { - decide(core::Facet::Temporal, core::FacetDecision::violation()); - if (!verdict.record) { - // Storage whose lifetime ended (§5.2, §5.7): reported where the - // pointer was stored. - transfer.danglingUse(operand, verdict, site.stmt, true); - return; - } - const core::ReleaseRecord &record = *verdict.record; - std::string message; - if (isRelease && record.reason == Reason::Moved) - message = "use of " + inQuotes(name) + " after it was moved"; - else if (isRelease) - message = - inQuotes(name) + " is " + - (record.reason == Reason::ShareReleased ? "released" : "freed") + - " twice"; - else if (record.reason == Reason::ShareReleased) - message = - "use of " + inQuotes(name) + " after its reference was released"; - else - message = "use of " + inQuotes(name) + " after it was " + verbFor(record); - core::Diagnostic diagnostic = makeDiagnostic( - idFor(record), message, context, - isRelease ? site.stmt->getBeginLoc() : operand.getBeginLoc(), - core::Severity::Error); - noteFor(record, false, diagnostic); - report(std::move(diagnostic), core::Certainty::Definite, - core::Facet::Temporal); - return; - } - case Kind::MayReleased: { - const core::ReleaseRecord record = - verdict.record.value_or(core::ReleaseRecord{}); - bool moved = record.reason == Reason::Moved; - decide(core::Facet::Temporal, - core::FacetDecision::unresolvedFor( - moved ? core::UnresolvedReason::MayMoved - : core::UnresolvedReason::MayReleased)); - std::string message; - if (isRelease && moved) - message = "use of " + inQuotes(name) + " after it may have been moved"; - else if (isRelease) - message = inQuotes(name) + " may be freed twice"; - else - message = "use of " + inQuotes(name) + " after it may have been " + - (moved ? "moved" : "freed"); - core::Diagnostic diagnostic = makeDiagnostic( - idFor(record), message, context, - isRelease ? site.stmt->getBeginLoc() : operand.getBeginLoc(), - core::Severity::Warning); - noteFor(record, true, diagnostic); - report(std::move(diagnostic), core::Certainty::Possible, - core::Facet::Temporal); - return; - } - case Kind::MayAliasReleased: - decide(core::Facet::Temporal, - core::FacetDecision::unresolvedFor( - core::UnresolvedReason::MayAliasReleased, - inQuotes(name) + " may point into an object released earlier")); - return; - case Kind::UnknownCallee: - // (The detail names the code that may have released it; at a call the - // callee's own suggestion is the detail.) - decide(core::Facet::Temporal, - core::FacetDecision::unresolvedFor( - core::UnresolvedReason::UnknownCallee, - verdict.record && !isa(site.stmt) ? verdict.record->via - : std::string())); - return; - case Kind::Callback: - decide(core::Facet::Temporal, core::FacetDecision::unresolvedFor( - core::UnresolvedReason::Callback)); - return; - case Kind::MayDangle: - decide(core::Facet::Temporal, core::FacetDecision::unresolvedFor( - core::UnresolvedReason::MayDangle)); - transfer.danglingUse(operand, verdict, site.stmt, false); - return; - } -} - -void Decider::spatialOf(core::Sym pointer, std::int64_t width, bool beyond) { - if (!applies(core::Facet::Spatial)) - return; - if (site.provenByType) { - decide(core::Facet::Spatial, core::FacetDecision::proven()); - return; - } - const core::SymInfo &value = heap.info(state, pointer); - if (value.rawCast) { - decide(core::Facet::Spatial, - core::FacetDecision::unresolvedFor(core::UnresolvedReason::RawCast)); - return; - } - core::SpatialVerdict verdict = - heap.spatial(state, pointer, core::Term::of(0), width); - using Kind = core::SpatialVerdict::Kind; - if (beyond && !value.top && !value.targets.empty()) { - verdict.kind = Kind::Violation; - verdict.beforeStart = false; - } - auto fallback = [&](core::UnresolvedReason reason) { - if (site.spatialSystemApi) { - decide(core::Facet::Spatial, - core::FacetDecision::trustedFor(core::TrustReason::SystemApi)); - return; - } - decide(core::Facet::Spatial, core::FacetDecision::unresolvedFor(reason)); - }; - switch (verdict.kind) { - case Kind::Proven: - decide(core::Facet::Spatial, core::FacetDecision::proven()); - return; - case Kind::Violation: { - decide(core::Facet::Spatial, core::FacetDecision::violation()); - std::string subject = inQuotes(transfer.spell(*cast(site.stmt))); - // The object: an array member's is the object it is a member of. - const Expr *objectExpr = - site.operand != nullptr ? site.operand->IgnoreParenImpCasts() : nullptr; - while (const auto *member = dyn_cast_or_null(objectExpr)) { - if (!member->getType()->isArrayType()) - break; - objectExpr = member->getBase()->IgnoreParenImpCasts(); - } - std::string object = objectExpr != nullptr - ? inQuotes(transfer.spell(*objectExpr)) - : "the object"; - std::string message; - // An amount of bytes as the program spells it: `5 bytes`, `'n' + 2 - // bytes`. - auto amount = [&](const core::Term &bytes) { - return transfer.spellAmount(bytes, /*own=*/false); - }; - std::optional extent = - verdict.extent ? amount(verdict.extent->bytes) : std::nullopt; - std::optional end = verdict.end; - // A member access reaches the end of its member, not of the whole - // record the check covers (`'r->id' ... reaches 12 bytes`). - if (const auto *member = dyn_cast(site.stmt); - member != nullptr && end && end->isConstant() && verdict.extent && - verdict.extent->bytes.isConstant()) - if (const auto *field = dyn_cast(member->getMemberDecl()); - field != nullptr && !field->isBitField() && - !field->getType()->isIncompleteType()) - if (auto fieldWidth = objectWidth(context, field->getType())) { - std::int64_t fieldEnd = - end->constant - width + - static_cast(context.getFieldOffset(field) / - context.getCharWidth()) + - *fieldWidth; - if (fieldEnd > verdict.extent->bytes.constant) - end = core::Term::of(fieldEnd); - } - std::optional reach = end ? amount(*end) : std::nullopt; - // The index as written: a subscript's, or `k` of `*(q + k)`. - const Expr *index = site.index; - bool negate = false; - if (index == nullptr) - if (const auto *subscript = dyn_cast(site.stmt)) - index = subscript->getIdx(); - if (index == nullptr && site.operand != nullptr) - if (const auto *sum = - dyn_cast(site.operand->IgnoreParenImpCasts()); - sum != nullptr && sum->isAdditiveOp() && - sum->getLHS()->getType()->isPointerType()) { - index = sum->getRHS(); - negate = sum->getOpcode() == BO_Sub; - } - core::Term indexTerm = index != nullptr - ? transfer.termOf(transfer.valueOf(*index)) - : core::Term::unknown(); - if (negate && indexTerm.isConstant()) - indexTerm = core::Term::of(-indexTerm.constant); - // `p[n]` against `n` elements, or an index the zone orders against the - // count. - std::string countClause; - // (The extent's bytes, or the product they are reduced from.) - auto countOf = [&](const core::Term &bytes) { - return bytes.known && !bytes.isConstant() && bytes.constant == 0 && - verdict.end->var == indexTerm.var && - verdict.end->scale == bytes.scale && - verdict.end->constant == verdict.end->scale; - }; - const core::Term *countTerm = nullptr; - if (verdict.extent && verdict.end && indexTerm.known && - !indexTerm.isConstant() && indexTerm.scale == 1 && - indexTerm.constant == 0) { - if (countOf(verdict.extent->bytes)) - countTerm = &verdict.extent->bytes; - else if (verdict.extent->unwrapped && countOf(*verdict.extent->unwrapped)) - countTerm = &*verdict.extent->unwrapped; - } - if (countTerm != nullptr) { - core::Sym count = countTerm->var; - // (Only values C places hold: a computed index reads better as the - // bytes it reaches.) - auto name = [&](core::Sym sym) -> std::string { - if (auto place = transfer.nameOf(sym)) - return messageSpelling(*place); - return heap.info(state, sym).name; - }; - std::string countName = name(count); - std::string indexName = name(indexTerm.var); - if (!countName.empty() && count == indexTerm.var) { - countClause = - inQuotes(countName) + " is the number of elements of " + object; - } else if (!countName.empty() && !indexName.empty()) { - std::string relation = " is at least "; - if (auto gap = state.zone.bound(count, indexTerm.var); gap && *gap < 0) - relation = " is above "; - countClause = inQuotes(indexName) + relation + inQuotes(countName) + - ", the number of elements of " + object; - } - } - if (verdict.beforeStart) - message = subject + " is out of bounds: " + - (index != nullptr ? "index " + indexText(*index) + " is" - : std::string("it lies")) + - " before the start of " + object; - else if (index != nullptr && (indexTerm.isConstant() || beyond) && extent) - message = subject + " is out of bounds: index " + indexText(*index) + - " of an object of " + *extent; - else if (!countClause.empty()) - message = subject + " is out of bounds: " + countClause; - else if (index != nullptr && indexTerm.known && !indexTerm.isConstant() && - indexTerm.scale == 1 && indexTerm.constant == 0 && extent && - verdict.extent && verdict.extent->bytes.isConstant() && - state.zone.lower(indexTerm.var) && transfer.nameOf(indexTerm.var)) - // RFC 0012: `'i' is at least 8 in an object of 8 bytes`. - message = subject + " is out of bounds: " + - inQuotes(messageSpelling(*transfer.nameOf(indexTerm.var))) + - " is at least " + - std::to_string(*state.zone.lower(indexTerm.var)) + - " in an object of " + *extent; - else if (reach && extent) - message = subject + " is out of bounds: it reaches " + *reach + " into " + - object + ", which has " + *extent; - else - message = subject + " is out of bounds of " + object + ", which has " + - extent.value_or("fewer bytes"); - core::Diagnostic diagnostic = - makeDiagnostic(core::diag::OutOfBounds, message, context, - site.stmt->getBeginLoc(), core::Severity::Error); - // Where the object came from. - const core::SymInfo &accessed = heap.info(state, pointer); - if (accessed.targets.size() == 1 && objectExpr != nullptr) { - const core::ObjectInfo &info = - run.table().info(accessed.targets[0].object); - std::string spelled = transfer.spell(*objectExpr); - if (info.key.kind == core::ObjectKind::Local || - info.key.kind == core::ObjectKind::Global) - addNote(diagnostic, - spelled == info.name - ? inQuotes(spelled) + " is declared here" - : "the object behind " + inQuotes(spelled) + - " is declared here", - info.created); - else if (info.key.kind == core::ObjectKind::HeapRecent || - info.key.kind == core::ObjectKind::HeapOld) - addNote(diagnostic, inQuotes(spelled) + " is allocated here", - info.created); - } - report(std::move(diagnostic), core::Certainty::Definite, - core::Facet::Spatial); - return; - } - case Kind::Checkable: - decide(core::Facet::Spatial, core::FacetDecision::unresolvedFor( - core::UnresolvedReason::Undecided)); - return; - case Kind::UnknownExtent: - fallback(core::UnresolvedReason::UnknownExtent); - return; - case Kind::UnknownIndex: - fallback(core::UnresolvedReason::UnknownIndex); - return; - } -} - -/// Whether the site's lvalue is stored to (`p[0] = c`, `++*p`). -static bool isWritten(const FunctionRun &run, const Stmt &lvalue) { - const Stmt *parent = run.parentOf(lvalue); - for (int depth = 0; depth < 4 && parent != nullptr; ++depth) { - if (isa(parent)) { - parent = run.parentOf(*parent); - continue; - } - if (const auto *binary = dyn_cast(parent)) - return binary->isAssignmentOp() && - binary->getLHS()->IgnoreParens() == &lvalue; - if (const auto *unary = dyn_cast(parent)) - return unary->isIncrementDecrementOp(); - return false; - } - return false; -} - -bool Decider::writesLiteral(core::Sym pointer, const Expr &operand) { - const core::SymInfo &value = heap.info(state, pointer); - if (value.top || value.targets.empty()) - return false; - bool any = false; - bool all = true; - for (const core::Target &target : value.targets) { - bool literal = - run.table().info(target.object).key.kind == core::ObjectKind::Literal; - any = any || literal; - all = all && literal; - } - if (!any || !isWritten(run, *site.stmt)) - return false; - if (!all) { - decide(core::Facet::Spatial, core::FacetDecision::unresolvedFor( - core::UnresolvedReason::UnknownExtent, - "it may point into a string literal")); - return true; - } - decide(core::Facet::Spatial, core::FacetDecision::violation()); - report(makeDiagnostic(core::diag::OutOfBounds, - "write through " + inQuotes(transfer.spell(operand)) + - ", which points to a string literal", - context, site.stmt->getBeginLoc(), - core::Severity::Error), - core::Certainty::Definite, core::Facet::Spatial); - return true; -} - -/// RFC 0004: where a raw pointer's raw origin is, for the note: `'n' is -/// raw: cast from an integer here (through 'p')`. -static std::string rawNote(const core::SymInfo &value, - const std::string &name) { - std::string origin; - switch (value.rawOrigin) { - case core::SymInfo::RawOrigin::Declared: - origin = "declared WEAVEC_RAW"; - break; - case core::SymInfo::RawOrigin::Loaded: - origin = value.rawFrom.empty() - ? std::string("loaded through a raw pointer") - : "loaded through raw pointer '" + value.rawFrom + "'"; - break; - case core::SymInfo::RawOrigin::Returned: - origin = value.rawFrom.empty() ? std::string("handed out by a callee") - : "handed out by '" + value.rawFrom + "'"; - break; - } - std::string subject = - name.empty() ? std::string("the pointer") : "'" + name + "'"; - std::string through; - if (!name.empty() && !value.rawVia.empty() && value.rawVia != name) - through = " (through '" + value.rawVia + "')"; - return subject + " is raw: " + origin + " here" + through; -} - -/// RFC 0004: what an unsafe operation outside a region should become. -static constexpr const char *UnsafeFixIt = - "move this operation into a WEAVEC_UNSAFE block or function, or assert " - "the pointer's ownership first"; - -void Decider::access() { - const auto *expr = dyn_cast(site.stmt); - if (expr == nullptr) - return; - const Expr *operand = site.operand; - core::Sym pointer = core::ZeroSym; - if (operand != nullptr && operand->getType()->isPointerType()) - pointer = transfer.valueOf(*operand); - // A pointer with an RFC 0004 raw origin (declared `WEAVEC_RAW`, loaded - // through a raw pointer, handed out as raw; RFC 0033 §2). - const bool raw = site.kind == core::SiteKind::Raw || - (pointer != core::ZeroSym && heap.info(state, pointer).raw); - // RFC 0030 §6.1: inside a region a raw access is trusted for every facet, - // the temporal one included, and so is one through a pointer converted - // from an integer (RFC 0033 §2: no longer raw, but no object either). - if (site.inUnsafe && (raw || (pointer != core::ZeroSym && - heap.info(state, pointer).rawCast))) { - for (core::Facet facet : - {core::Facet::Null, core::Facet::Spatial, core::Facet::Temporal}) - if (applies(facet)) - decide(facet, - core::FacetDecision::trustedFor(core::TrustReason::Unsafe)); - return; - } - // Raw through some of a call's functions only (RFC 0031 *Implementation - // amendments*): no error, and nothing about it proven. - if (raw && site.kind != core::SiteKind::Raw && pointer != core::ZeroSym && - heap.info(state, pointer).rawSome) { - nullOf(pointer, *operand); - for (core::Facet facet : {core::Facet::Spatial, core::Facet::Temporal}) - if (applies(facet)) - decide(facet, core::FacetDecision::unresolvedFor( - core::UnresolvedReason::RawCast)); - return; - } - // Outside a region it is an error (RFC 0004), on the spatial facet; its - // object is none the analysis tracks, so the temporal one is unresolved. - if (raw && run.isPublishing()) { - // (A conversion written in place is no name: `((T *)x)->v`.) - std::string spelled = - operand != nullptr && - !isa(operand->IgnoreParenImpCasts()) - ? transfer.spell(*operand) - : std::string(); - core::Diagnostic diagnostic = makeDiagnostic( - core::diag::UnsafeOperation, - "dereference of raw pointer " + - (spelled.empty() ? std::string() : inQuotes(spelled) + " ") + - "outside an unsafe region", - context, - operand != nullptr ? operand->getBeginLoc() : site.stmt->getBeginLoc(), - core::Severity::Error); - if (pointer != core::ZeroSym && heap.info(state, pointer).raw) - addNote(diagnostic, rawNote(heap.info(state, pointer), spelled), - heap.info(state, pointer).rawAt); - diagnostic.addNote(UnsafeFixIt, core::SourceLocation{}); - report(std::move(diagnostic), core::Certainty::Definite, - core::Facet::Spatial); - } - if (raw) { - if (pointer != core::ZeroSym) - nullOf(pointer, *operand); - if (applies(core::Facet::Temporal)) - decide(core::Facet::Temporal, core::FacetDecision::unresolvedFor( - core::UnresolvedReason::RawCast)); - return; - } - if (pointer != core::ZeroSym) { - nullOf(pointer, *operand); - temporalOf(pointer, *operand, false); - } - if (!applies(core::Facet::Spatial)) - return; - // A string literal has no writable byte (RFC 0030 *Diagnostics*). - if (pointer != core::ZeroSym && writesLiteral(pointer, *operand)) - return; - if (site.kind == core::SiteKind::Deref && pointer != core::ZeroSym) { - QualType pointee = operand->getType()->getPointeeType(); - auto width = objectWidth(context, pointee); - if (!width || *width <= 0) { - decide(core::Facet::Spatial, core::FacetDecision::unresolvedFor( - core::UnresolvedReason::UnknownExtent)); - return; - } - spatialOf(pointer, *width); - return; - } - // A variable-length array whose byte size may not be representable (or - // whose dimension may not be positive): C defines no storage, and the - // type's `sizeof` would be no bound to check against (RFC 0017). - if (site.operand != nullptr) - if (const auto *ref = - dyn_cast(site.operand->IgnoreParenImpCasts()); - ref != nullptr && ref->getType()->isVariablyModifiedType() && - ref->getType()->isArrayType() && - !transfer.bytesOf(ref->getType(), *ref)) { - // A dimension's own bound still shows a violation. - if (memberBound(transfer.sizeOf(expr->getType()).value_or(0))) - return; - decide(core::Facet::Spatial, core::FacetDecision::unresolvedFor( - core::UnresolvedReason::UnknownExtent)); - return; - } - // An index: the element's address. - Address address = transfer.addressOf(*expr); - core::SymInfo element; - element.type = core::SymInfo::Type::Pointer; - element.targets = address.targets; - element.top = address.top; - core::Sym at = heap.fresh(state, element); - auto width = transfer.sizeOf(expr->getType()); - if (!width || *width <= 0) { - // A row of a variable-length array: its own dimension still bounds it. - if (memberBound(0)) - return; - decide(core::Facet::Spatial, core::FacetDecision::unresolvedFor( - core::UnresolvedReason::Unanalysed)); - return; - } - // RFC 0017: an unsigned index above `INT64_MAX` for every value reaches - // past the end of any object (the zone's bounds cannot say so). - bool beyond = false; - if (site.index != nullptr) { - const core::SymInfo &index = - heap.info(state, transfer.valueOf(*site.index)); - beyond = index.values && !index.values->empty() && - !index.values->type.isSigned && - !index.values->minimum()->signedValue(); - } - if (!beyond && memberBound(*width)) - return; - spatialOf(at, *width, beyond); -} - -bool Decider::memberBound(std::int64_t width) { - // RFC 0030 §7.4: a direct subscript of a non-flexible array-typed lvalue - // uses the array's own bound, as C does (`r->name[8]` for `char - // name[8]`, `m[i][j]` needs `j < 4` for `int m[3][4]`). - if (site.index == nullptr || site.operand == nullptr) - return false; - const Expr *member = site.operand->IgnoreParenImpCasts(); - const ConstantArrayType *array = - context.getAsConstantArrayType(member->getType()); - // RFC 0017: a variable-length dimension, with the count its declaration - // captured. - const VariableArrayType *vla = - array == nullptr ? context.getAsVariableArrayType(member->getType()) - : nullptr; - if (array == nullptr && vla == nullptr) - return false; - const FieldDecl *field = nullptr; - if (const auto *access = dyn_cast(member)) { - field = dyn_cast(access->getMemberDecl()); - if (field == nullptr) - return false; - // A trailing array is flexible at the default level (§7.4). - const RecordDecl *record = field->getParent(); - const FieldDecl *last = nullptr; - for (const FieldDecl *each : record->fields()) - last = each; - if (record->isUnion() || - (last == field && context.getLangOpts().getStrictFlexArraysLevel() == - LangOptions::StrictFlexArraysLevelKind::Default)) - return false; - } else if (const auto *ref = dyn_cast(member)) { - // A variable whose rows are variable-length arrays: the outer - // dimension is the variable's own (a parameter is a pointer). A - // variable of fixed rows is bounded by its extent alone. - const ArrayType *element = - array != nullptr ? context.getAsArrayType(array->getElementType()) - : nullptr; - if (vla == nullptr && - (element == nullptr || !element->isVariableArrayType())) - return false; - if (!isa(ref->getDecl())) - return false; - } else if (!isa(member)) { - return false; - } - // Whether the array lies inside storage the analysis knows: the chain of - // subscripts leads to an array variable whose byte size is representable - // (every dimension positive, RFC 0017), to a member the outer access - // reached, or to a pointer whose rows have a constant size (its own site - // checks the whole row). Only then does the array's own bound prove an - // access; a violation of it is one regardless. - bool storageKnown = true; - { - const Expr *root = member; - while (const auto *subscript = dyn_cast(root)) - root = subscript->getBase()->IgnoreParenImpCasts(); - if (const auto *ref = dyn_cast(root); - ref != nullptr && ref->getType()->isArrayType()) - storageKnown = transfer.bytesOf(ref->getType(), *ref).has_value(); - else if (root->getType()->isPointerType()) - storageKnown = - !root->getType()->getPointeeType()->isVariablyModifiedType(); - } - core::Term index = transfer.termOf(transfer.valueOf(*site.index)); - if (!index.known) - return false; - std::int64_t count = 0; - core::Term bound = core::Term::unknown(); - if (array != nullptr) { - count = static_cast(array->getSize().getZExtValue()); - bound = core::Term::of(count); - } else { - core::Sym captured = transfer.vlaCount(*vla); - auto hi = state.zone.upper(captured); - // A dimension zero or negative for every value is the declaration's - // error (`invalid-integer-operation`), not the access's. - if (hi && *hi <= 0) { - decide(core::Facet::Spatial, core::FacetDecision::unresolvedFor( - core::UnresolvedReason::UnknownExtent)); - return true; - } - // One that may be is no storage to prove an access in, but an index - // past it is out of bounds whatever it is. - if (auto lo = state.zone.lower(captured); !lo || *lo < 1) - storageKnown = false; - bound = transfer.termOf(captured); - if (auto known = state.zone.constant(captured)) - count = *known; - } - std::optional lower = heap.lessEqual(state, core::Term::of(0), index); - std::optional upper = - bound.known ? heap.lessEqual(state, index.plusConstant(1), bound) - : std::nullopt; - std::string subject = inQuotes(transfer.spell(*cast(site.stmt))); - std::string spelled = transfer.spell(*member); - if (lower == false || upper == false) { - decide(core::Facet::Spatial, core::FacetDecision::violation()); - std::string where; - if (lower == false) - where = " is before the start of " + inQuotes(spelled); - else if (array == nullptr && !state.zone.constant(transfer.vlaCount(*vla))) - where = " is past the end of " + inQuotes(spelled); - else if (width > 0) - where = " of an object of " + std::to_string(count * width) + " bytes"; - else - where = " of an array of " + std::to_string(count) + " elements"; - core::Diagnostic diagnostic = makeDiagnostic( - core::diag::OutOfBounds, - subject + " is out of bounds: index " + indexText(*site.index) + where, - context, site.stmt->getBeginLoc(), core::Severity::Error); - if (field != nullptr) - addNote(diagnostic, inQuotes(spelled) + " is declared here", - toCoreLocation(context.getSourceManager(), field->getLocation())); - report(std::move(diagnostic), core::Certainty::Definite, - core::Facet::Spatial); - return true; - } - // The array lies inside its object by the access that reaches it (the - // member's `r->` dereference, the outer subscript's own site), so its - // bound decides. - // Not violated, over storage the analysis does not know: the object's - // extent decides, as for any access (a variable-length array's own site - // without a representable size is left unresolved there). - if (!storageKnown) - return false; - if (lower == true && upper == true) { - decide(core::Facet::Spatial, core::FacetDecision::proven()); - return true; - } - if (array == nullptr) { - // A variable-length array variable itself: its extent decides, with - // `sizeof v` to check against (§10.3 rule 4). - if (isa(member)) - return false; - // An inner dimension's captured count has no C spelling at the site to - // check against. - decide(core::Facet::Spatial, core::FacetDecision::unresolvedFor( - core::UnresolvedReason::UnknownIndex)); - return true; - } - decide(core::Facet::Spatial, - core::FacetDecision::unresolvedFor(core::UnresolvedReason::Undecided)); - return true; -} - -std::string Decider::indexText(const Expr &index) { - // The index as written, with its value when that is a folded constant - // the text does not show: `index 'data' (10)`. - std::string text = transfer.spell(index); - core::Sym sym = transfer.valueOf(index); - core::Term value = transfer.termOf(sym); - std::optional shown; - if (value.isConstant()) - shown = std::to_string(value.constant); - else if (const core::SymInfo &info = heap.info(state, sym); info.values) - // (A value beyond the zone's 64-bit bounds, RFC 0017.) - if (auto constant = info.values->constant()) - shown = constant->toString(); - if (shown && text != *shown) - return "'" + text + "' (" + *shown + ")"; - return text; -} - -void Decider::release(core::Sym pointer, const std::string &family) { - const Expr *operand = site.operand; - if (operand == nullptr) - return; - std::string name = transfer.spell(*operand); - const core::SymInfo value = heap.info(state, pointer); - // A pointer with an RFC 0004 raw origin (declared `WEAVEC_RAW`, from an - // integer, loaded through a raw pointer, handed out as raw). - const bool raw = site.kind == core::SiteKind::Raw || value.raw; - // RFC 0030 §6.1: inside a region a raw release is trusted for the - // facets of the pointer's memory; its temporal state is tracked as - // outside, so a definite second release is still an error (probe 45). - if (raw && site.inUnsafe) { - for (core::Facet facet : {core::Facet::Null, core::Facet::Spatial}) - if (applies(facet)) - decide(facet, - core::FacetDecision::trustedFor(core::TrustReason::Unsafe)); - temporalOf(pointer, *operand, true); - return; - } - temporalOf(pointer, *operand, true); - // Family (RFC 0007). - if (applies(core::Facet::Temporal) && value.targets.size() == 1) { - const core::ObjectState *object = - heap.findObject(state, value.targets[0].object); - if (object != nullptr && !object->family.empty() && !family.empty() && - object->family != family && object->life == core::Life::Live) { - decide(core::Facet::Temporal, core::FacetDecision::violation()); - core::Diagnostic diagnostic = makeDiagnostic( - core::diag::MismatchedRelease, - inQuotes(name) + " is released with " + inQuotes(family) + - " but must be released with " + inQuotes(object->family), - context, site.stmt->getBeginLoc(), core::Severity::Error); - addNote(diagnostic, "allocated here", - run.table().info(value.targets[0].object).created); - report(std::move(diagnostic), core::Certainty::Definite, - core::Facet::Temporal); - } - } - conflictingBorrow(pointer); - // Raw pointers (RFC 0004): a release asserts ownership, which needs an - // unsafe region (not for one raw through some of a call's functions). - if (raw && !value.rawSome && run.isPublishing()) { - std::string callee = "a function pointer"; - if (site.library) - callee = inQuotes(site.library->entry->name); - else if (site.callee != nullptr) - callee = inQuotes(site.callee->getNameAsString()); - core::Diagnostic diagnostic = - makeDiagnostic(core::diag::UnsafeOperation, - callee + " releases raw pointer " + inQuotes(name) + - " outside an unsafe region", - context, operand->getBeginLoc(), core::Severity::Error); - if (value.raw) - addNote(diagnostic, rawNote(value, name), value.rawAt); - diagnostic.addNote(UnsafeFixIt, core::SourceLocation{}); - report(std::move(diagnostic), core::Certainty::Definite, - core::Facet::Spatial); - } - invalidRelease(pointer); -} - -void Decider::conflictingBorrow(core::Sym pointer) { - // §5.5 *Conflicting borrows* (RFC 0002, RFC 0006): a pointer derived - // from a released target and held where it outlives the release — a - // cell reachable from a parameter, a global or an address-taken local. - if (!applies(core::Facet::Temporal) || !run.isPublishing()) - return; - const core::SymInfo value = heap.info(state, pointer); - if (value.type != core::SymInfo::Type::Pointer || value.top || - value.null == core::PointerNull::Null) - return; - std::set released; - for (const core::Target &target : value.targets) - released.insert(target.object); - std::vector roots; - for (const auto &[id, object] : state.objects) { - const core::ObjectInfo &info = run.table().info(id); - if (info.key.dead || released.contains(id)) - continue; - switch (info.key.kind) { - case core::ObjectKind::Global: - case core::ObjectKind::Entry: - case core::ObjectKind::EntrySummary: - roots.push_back(id); - break; - case core::ObjectKind::Local: - if (const VarDecl *var = run.localVariable(id); - var != nullptr && run.isAddressTaken(*var) && - !var->getType()->isArrayType() && !var->getType()->isRecordType()) - roots.push_back(id); - break; - default: - break; - } - } - std::set seen(roots.begin(), roots.end()); - std::vector work = roots; - while (!work.empty()) { - core::ObjectId id = work.back(); - work.pop_back(); - const core::ObjectState *object = heap.findObject(state, id); - if (object == nullptr || object->life != core::Life::Live) - continue; - for (const auto &[key, sym] : object->cells) { - const core::SymInfo &held = heap.info(state, sym); - if (held.type != core::SymInfo::Type::Pointer) - continue; - bool borrows = false; - for (const core::Target &target : held.targets) { - if (released.contains(target.object)) - borrows = true; - if (!released.contains(target.object) && - seen.insert(target.object).second) - work.push_back(target.object); - } - if (!borrows || !held.derived || sym == pointer) - continue; - // RFC 0033 §1: a release while a copy is held breaks no access by - // itself (an initialiser that frees a buffer whose copies it left in - // the caller's struct on a failure path); a later use through the copy - // is a use after free, reported there. Possible, never definite. - decide(core::Facet::Temporal, core::FacetDecision::unresolvedFor( - core::UnresolvedReason::MayConflict)); - std::string name = transfer.spell(*site.operand); - core::Diagnostic diagnostic = makeDiagnostic( - core::diag::ConflictingBorrow, - "cannot free " + inQuotes(name) + " while it is borrowed", context, - site.stmt->getBeginLoc(), core::Severity::Warning); - const FunctionRun::FrameStore *stored = run.borrowStore(id, key); - if (stored != nullptr && !stored->holder.empty()) - addNote(diagnostic, "borrowed by " + inQuotes(stored->holder) + " here", - toCoreLocation(context.getSourceManager(), - stored->at->getBeginLoc())); - report(std::move(diagnostic), core::Certainty::Possible, - core::Facet::Temporal); - return; - } - } -} - -/// The pointer a released operand was derived from, which the messages -/// name (RFC 0011): `p` for `p + 1` and `p - 2`, `o` for `&o->in`. -static const Expr &releasedBase(const Expr &operand) { - const Expr *e = operand.IgnoreParenCasts(); - while (true) { - if (const auto *binary = dyn_cast(e); - binary != nullptr && - (binary->getOpcode() == BO_Add || binary->getOpcode() == BO_Sub)) { - if (binary->getLHS()->getType()->isPointerType()) { - e = binary->getLHS()->IgnoreParenCasts(); - continue; - } - if (binary->getRHS()->getType()->isPointerType()) { - e = binary->getRHS()->IgnoreParenCasts(); - continue; - } - } - if (const auto *unary = dyn_cast(e); - unary != nullptr && unary->getOpcode() == UO_AddrOf) - if (const auto *member = - dyn_cast(unary->getSubExpr()->IgnoreParens()); - member != nullptr && member->isArrow()) { - e = member->getBase()->IgnoreParenCasts(); - continue; - } - return *e; - } -} - -void Decider::invalidRelease(core::Sym pointer) { - // Invalid releases (RFC 0008, RFC 0031 §5.5): not a heap object, or not - // its start. - if (!applies(core::Facet::Spatial) || site.operand == nullptr) - return; - const Expr *operand = site.operand; - std::string name = transfer.spell(releasedBase(*operand)); - const bool ranged = site.library.has_value() && - std::ranges::any_of(site.library->entry->params, - [](const core::LibraryParam ¶m) { - return param.range.has_value(); - }); - Transfer::ReleaseCheck check = - transfer.releaseCheck(pointer, name, *operand, "released", ranged); - switch (check.kind) { - case Transfer::ReleaseCheck::Kind::Proven: - decide(core::Facet::Spatial, core::FacetDecision::proven()); - return; - case Transfer::ReleaseCheck::Kind::UnknownIndex: - decide(core::Facet::Spatial, core::FacetDecision::unresolvedFor( - core::UnresolvedReason::UnknownIndex)); - return; - case Transfer::ReleaseCheck::Kind::Possible: - case Transfer::ReleaseCheck::Kind::Violation: { - bool definite = check.kind == Transfer::ReleaseCheck::Kind::Violation; - decide(core::Facet::Spatial, - definite ? core::FacetDecision::violation() - : core::FacetDecision::unresolvedFor( - core::UnresolvedReason::MayInvalidRelease)); - core::Diagnostic diagnostic = makeDiagnostic( - core::diag::InvalidRelease, check.message, context, - site.stmt->getBeginLoc(), - definite ? core::Severity::Error : core::Severity::Warning); - addNote(diagnostic, check.note, check.noteAt); - report(std::move(diagnostic), - definite ? core::Certainty::Definite : core::Certainty::Possible, - core::Facet::Spatial); - return; - } - } -} - -Transfer::ReleaseCheck Transfer::releaseCheck(core::Sym pointer, - const std::string &subject, - const Expr &operand, - const std::string &verb, - bool anywhere) const { - ReleaseCheck out; - const core::SymInfo &value = heap.info(state, pointer); - if (value.null == core::PointerNull::Null) - return out; - if (value.top || value.targets.empty()) { - out.kind = ReleaseCheck::Kind::UnknownIndex; - return out; - } - QualType pointee = operand.IgnoreParenImpCasts()->getType(); - if (pointee->isPointerType()) - pointee = pointee->getPointeeType(); - std::optional element; - if (!pointee.isNull() && !pointee->isVoidType() && - !pointee->isIncompleteType()) - element = static_cast( - context.getTypeSizeInChars(pointee).getQuantity()); - // `free(buf)`, `free(&x)`, `free("abc")`: the argument is the storage. - const Expr *stripped = operand.IgnoreParenCasts(); - bool storageItself = isa(stripped); - // `free(&b.len)`: the storage as written. - std::string storageSpelled; - // `free(&o->in)`: the field the pointer points to (RFC 0011). - std::string fieldSpelled; - if (const auto *unary = dyn_cast(stripped)) { - storageItself = unary->getOpcode() == UO_AddrOf; - if (storageItself) { - storageSpelled = spell(*unary->getSubExpr()); - if (const auto *member = - dyn_cast(unary->getSubExpr()->IgnoreParens())) - fieldSpelled = member->getMemberDecl()->getNameAsString(); - } - } - if (const auto *ref = dyn_cast(stripped)) - storageItself = ref->getType()->isArrayType(); - bool anyInvalid = false; - bool allInvalid = true; - bool anyUnknown = false; - for (const core::Target &target : value.targets) { - const core::ObjectInfo &info = run.table().info(target.object); - const core::ObjectState *object = heap.findObject(state, target.object); - bool stack = object != nullptr && object->family == core::StackFamily; - bool literal = info.key.kind == core::ObjectKind::Literal; - bool nonHeap = info.key.kind == core::ObjectKind::Local || - info.key.kind == core::ObjectKind::Global || literal || - info.key.kind == core::ObjectKind::Function || stack; - // Only an allocation of this function (or one a callee handed it fresh) - // is known to start where it points; where a caller's pointer points in - // its object is not known here. - bool heapObject = info.key.kind == core::ObjectKind::HeapRecent || - info.key.kind == core::ObjectKind::HeapOld; - std::optional offset; - if (target.offset.isConstant()) - offset = target.offset.constant; - else if (target.offset.known) - if (auto c = state.zone.constant(target.offset.var)) - offset = (target.offset.scale * *c) + target.offset.constant; - // A parameter some caller passes a cursor (its kind is Unknown, §7.3) - // may point anywhere into its allocation. - bool cursor = false; - if (info.key.kind == core::ObjectKind::Entry && info.key.path.isParam() && - info.key.path.steps.size() == 1 && - info.key.path.steps.front().step == core::PathStep::Deref) - if (const KindEntry *kind = - run.unitRun().input.kinds.param(run.decl(), info.key.path.index); - kind != nullptr && !kind->hasShape() && - !kind->shapeFromSystemHeader()) - cursor = true; - // (A release that may take any part of a heap object takes this one.) - if (anywhere && heapObject) { - allInvalid = false; - continue; - } - if (nonHeap) { - anyInvalid = true; - if (out.message.empty()) { - std::string storage = info.key.kind == core::ObjectKind::Global - ? info.name - : run.frameName(target.object); - if (storageItself) { - out.message = - literal - ? "a string literal is " + verb - : "'" + (storageSpelled.empty() ? storage : storageSpelled) + - "' is " + verb + " but is not a heap object"; - if (!literal) { - out.note = "'" + storage + "' is declared here"; - out.noteAt = info.created; - } - } else if (literal) { - out.message = "'" + subject; - out.message += "' is "; - out.message += verb; - out.message += " but points to a string literal"; - } else { - out.message = "'" + subject; - out.message += "' is "; - out.message += verb; - out.message += " but points to '"; - out.message += storage; - out.message += "', which is not a heap object"; - out.note = "'" + storage + "' is declared here"; - out.noteAt = info.created; - } - } - } else if (offset && *offset != 0 && !cursor && - (heapObject || - (info.key.kind == core::ObjectKind::Entry && *offset > 0))) { - // A positive offset from any valid pointer is never the start of an - // allocation; a negative one from a caller's pointer may be (`p - 1`). - anyInvalid = true; - if (out.message.empty()) { - std::string where = "may not point to the start of its allocation"; - if (!fieldSpelled.empty()) { - where = "points to field '" + fieldSpelled + "' of its allocation"; - } else if (element && *offset % *element == 0) { - std::int64_t count = *offset / *element; - std::int64_t magnitude = count < 0 ? -count : count; - where = "points " + std::to_string(magnitude) + - (magnitude == 1 ? " element" : " elements") + - (count > 0 ? " past" : " before") + - " the start of its allocation"; - } else if (auto field = [&]() -> std::optional { - QualType type = - info.type != 0 ? typeOfHandle(info.type) : QualType(); - if (type.isNull() || !type->isRecordType()) - return std::nullopt; - const RecordDecl *record = type->getAsRecordDecl(); - if (record == nullptr || !record->isCompleteDefinition()) - return std::nullopt; - const ASTRecordLayout &layout = - context.getASTRecordLayout(record); - for (const FieldDecl *f : record->fields()) - if (std::cmp_equal( - layout.getFieldOffset(f->getFieldIndex()) / - context.getCharWidth(), - *offset)) - return f->getNameAsString(); - return std::nullopt; - }()) { - where = "points to field '" + *field + "' of its allocation"; - } - out.message = "'" + subject; - out.message += "' is "; - out.message += verb; - out.message += " but "; - out.message += where; - out.note = "allocated here"; - out.noteAt = info.created; - } - } else if (cursor || !heapObject) { - allInvalid = false; - anyUnknown = true; - } else if (!offset) { - // Somewhere in its object (`strchr(p, 'x')`): possibly not its start. - allInvalid = false; - anyInvalid = true; - if (out.message.empty()) { - out.message = "'" + subject; - out.message += "' is "; - out.message += verb; - out.message += " but may not point to the start of its allocation"; - } - } else { - allInvalid = false; - } - } - if (!anyInvalid) { - out.kind = anyUnknown ? ReleaseCheck::Kind::UnknownIndex - : ReleaseCheck::Kind::Proven; - return out; - } - out.kind = - allInvalid ? ReleaseCheck::Kind::Violation : ReleaseCheck::Kind::Possible; - if (!allInvalid) { - // `may point`. - std::size_t at = out.message.find(" but points"); - if (at != std::string::npos) - out.message.replace(at, 11, " but may point"); - } - return out; -} - -// Declared a const member in Engine.h. -// NOLINTNEXTLINE(readability-convert-member-functions-to-static) -void Transfer::addNullNote(core::Diagnostic &diagnostic, - const core::SymInfo &value, - const std::string &name) const { - if (!value.nullOrigin || name.empty()) - return; - const core::NullOrigin &origin = *value.nullOrigin; - switch (origin.reason) { - case core::NullOrigin::Reason::Assigned: - addNote(diagnostic, inQuotes(name) + " is assigned NULL here", - origin.where); - return; - case core::NullOrigin::Reason::Tested: - addNote(diagnostic, - inQuotes(name) + " may be null: it is compared with NULL here", - origin.where); - return; - case core::NullOrigin::Reason::Allocated: - return; - } -} - -core::Diagnostic Transfer::nullArgument(const Expr &arg, - const core::SymInfo &value, - const std::string &callee, - const FunctionDecl *declared) const { - // A null constant has no name (`get(NULL)`). - std::string name = - arg.IgnoreParenCasts()->isNullPointerConstant( - context, Expr::NPC_ValueDependentIsNotNull) != Expr::NPCK_NotNull - ? std::string() - : spell(arg); - core::Diagnostic diagnostic = - makeDiagnostic(core::diag::NullDereference, - (name.empty() ? std::string("a null pointer") - : inQuotes(name) + ", which is null,") + - " is passed to " + callee + ", which dereferences it", - context, arg.getBeginLoc(), core::Severity::Error); - addNullNote(diagnostic, value, name); - // (Not for an implicitly declared builtin: its "declaration" is here.) - if (declared != nullptr && !declared->isImplicit()) - addNote( - diagnostic, callee + " is declared here", - toCoreLocation(context.getSourceManager(), declared->getLocation())); - return diagnostic; -} - -void Transfer::allocationFailure(const core::SymInfo &value, const Expr &at, - const Stmt &site) { - if (!value.allocatorSource || !run.isPublishing()) - return; - std::string callee = "an allocation"; - core::SourceLocation allocated; - if (value.nullOrigin && - value.nullOrigin->reason == core::NullOrigin::Reason::Allocated) { - if (!value.nullOrigin->detail.empty()) - callee = "the result of " + inQuotes(value.nullOrigin->detail); - allocated = value.nullOrigin->where; - } - core::Diagnostic diagnostic = makeDiagnostic( - core::diag::AllocationFailure, - callee + " is used without a null test; it is null when allocation " - "fails", - context, at.getBeginLoc(), core::Severity::Warning); - addNote(diagnostic, "allocated here", allocated); - run.report(std::move(diagnostic), core::Certainty::Possible, &site, - core::Facet::Null); -} - -/// The name of parameter `index` of `callee` for messages. -static std::string parameterName(const FunctionDecl &callee, unsigned index) { - for (const FunctionDecl *redecl : callee.redecls()) - if (index < redecl->getNumParams() && - !redecl->getParamDecl(index)->getName().empty()) - return "'" + redecl->getParamDecl(index)->getNameAsString() + "'"; - return "parameter " + std::to_string(index + 1); -} - -void Decider::unknownCall(const CallExpr &call, bool callback) { - const FunctionDecl *callee = call.getDirectCallee(); - std::string detail; - std::optional fixit; - const SourceManager &sm = context.getSourceManager(); - std::optional uncovered; - for (unsigned i = 0; i < call.getNumArgs(); ++i) - if (call.getArg(i)->getType()->isPointerType()) { - uncovered = i; - break; - } - if (callee == nullptr) { - detail = "the target of " + inQuotes(transfer.spell(*call.getCallee())) + - " is unknown; annotate the parameters of its function type"; - } else if (uncovered && *uncovered < callee->getNumParams()) { - std::string name = callee->getNameAsString(); - detail = "declare '" + name + "' with WEAVEC_BORROWED on " + - parameterName(*callee, *uncovered) + - " if it neither keeps nor frees it"; - const ParmVarDecl *param = callee->getFirstDecl()->getParamDecl(*uncovered); - SourceLocation at = sm.getFileLoc(param->getLocation()); - if (at.isValid() && !sm.isInSystemHeader(at)) - fixit = core::FixItHint{.location = toCoreLocation(sm, at), - .insertion = "WEAVEC_BORROWED "}; - } else if (callee->getReturnType()->isPointerType()) { - std::string name = callee->getNameAsString(); - detail = "declare the result of '" + name + - "' WEAVEC_OWNED or WEAVEC_BORROWED, or define '" + name + - "' in this program"; - SourceLocation at = sm.getFileLoc(callee->getFirstDecl()->getLocation()); - if (at.isValid() && !sm.isInSystemHeader(at)) - fixit = core::FixItHint{.location = toCoreLocation(sm, at), - .insertion = "WEAVEC_BORROWED "}; - } else { - detail = "define '" + callee->getNameAsString() + - "' in this program, or link a unit that has its WeaveC record"; - } - decide(core::Facet::Temporal, - core::FacetDecision::unresolvedFor( - callback ? core::UnresolvedReason::Callback - : core::UnresolvedReason::UnknownCallee, - std::move(detail))); - if (fixit && run.isPublishing() && applies(core::Facet::Temporal)) - out.suggest(*site.stmt, site.kind, site.boundary, core::Facet::Temporal, - std::move(*fixit)); -} - -//===----------------------------------------------------------------------===// -// Transfer entry points -//===----------------------------------------------------------------------===// - -void Transfer::decideSites(const Stmt &stmt) { - if (isa(stmt)) - return; - const SiteIndex &sites = run.sites(); - for (core::SiteId id : sites.sitesOf(stmt)) { - const SiteInfo *info = sites.info(id); - if (info == nullptr) - continue; - switch (info->kind) { - case core::SiteKind::Raw: - // (A raw release is the call's, below.) - if (isa(info->stmt)) - break; - Decider(*this, *info).access(); - break; - case core::SiteKind::Deref: - case core::SiteKind::Index: - Decider(*this, *info).access(); - break; - case core::SiteKind::Call: - if (info->boundary == core::Boundary::Exit) - decideExitSite(stmt, isa(stmt)); - break; - default: - break; - } - } -} - -void Transfer::accessed(const Stmt &stmt) { - const SiteIndex &sites = run.sites(); - // After a proven or checked dereference the pointer is non-null (RFC - // 0030 §3.2), in every pass, so the blocks after it start from that. - const Expr *base = nullptr; - if (const auto *unary = dyn_cast(&stmt); - unary != nullptr && unary->getOpcode() == UO_Deref) - base = unary->getSubExpr(); - else if (const auto *member = dyn_cast(&stmt); - member != nullptr && member->isArrow()) - base = member->getBase(); - else if (const auto *subscript = dyn_cast(&stmt); - subscript != nullptr && - subscript->getBase()->getType()->isPointerType()) - base = subscript->getBase(); - if (base != nullptr) { - bool unsafeSite = false; - for (core::SiteId id : sites.sitesOf(stmt)) - if (const SiteInfo *info = sites.info(id)) - unsafeSite = unsafeSite || info->inUnsafe; - if (!unsafeSite) { - core::Sym pointer = valueOf(*base); - core::SymInfo &info = heap.infoMut(state, pointer); - if (info.null != core::PointerNull::Null || run.isPublishing()) { - if (info.type == core::SymInfo::Type::Pointer) - info.null = core::PointerNull::NonNull; - // (What arithmetic made from it, even from a value of no known - // type: an untyped union cell.) - heap.markNonNull(state, pointer); - } - } - } -} - -void Transfer::decideExitSite(const Stmt &stmt, bool isReturn) { - const SiteIndex &sites = run.sites(); - auto id = sites.findExit(stmt); - if (!id) - return; - const SiteInfo *info = sites.info(*id); - if (info == nullptr) - return; - Decider decider(*this, *info); - // A returned pointer to this activation's own storage (RFC 0002). - if (isReturn && state.result != core::ZeroSym) { - const core::SymInfo &value = heap.info(state, state.result); - bool anyLocal = false; - bool allLocal = !value.targets.empty(); - std::string local; - core::SourceLocation declared; - for (const core::Target &target : value.targets) { - const core::ObjectInfo &objectInfo = run.table().info(target.object); - // Storage of this frame (§5.7): a local, a parameter, a compound - // literal, an `alloca` block. - if (run.isFrameObject(state, target.object)) { - anyLocal = true; - if (local.empty()) { - local = run.frameName(target.object); - declared = objectInfo.created; - } - } else { - allLocal = false; - } - } - // A returned pointer into a released object is a use of it. - if (!anyLocal && value.type == core::SymInfo::Type::Pointer && - value.null != core::PointerNull::Null) { - const auto &ret = cast(stmt); - if (ret.getRetValue() != nullptr && - heap.temporal(state, state.result).kind != - core::TemporalVerdict::Kind::Proven && - heap.temporal(state, state.result).kind != - core::TemporalVerdict::Kind::MayAliasReleased) - decider.temporalOf(state.result, *ret.getRetValue(), false); - } - if (anyLocal) { - bool definite = allLocal && value.null == core::PointerNull::NonNull; - decider.decide(core::Facet::Temporal, - definite ? core::FacetDecision::violation() - : core::FacetDecision::unresolvedFor( - core::UnresolvedReason::MayDangle)); - const auto &ret = cast(stmt); - // RFC 0002: a returned variable is named; `return &x` is the - // returned pointer. - std::string holder = "returned pointer"; - if (ret.getRetValue() != nullptr) - if (const auto *ref = - dyn_cast(ret.getRetValue()->IgnoreParenImpCasts()); - ref != nullptr && isa(ref->getDecl())) - holder = inQuotes(ref->getDecl()->getNameAsString()); - core::Diagnostic diagnostic = makeDiagnostic( - core::diag::LifetimeTooShort, - holder + " may outlive " + inQuotes(local) + ", which it points to", - run.ast(), - ret.getRetValue() != nullptr ? ret.getRetValue()->getBeginLoc() - : ret.getBeginLoc(), - definite ? core::Severity::Error : core::Severity::Warning); - addNote(diagnostic, inQuotes(local) + " is declared here", declared); - run.report(std::move(diagnostic), - definite ? core::Certainty::Definite - : core::Certainty::Possible, - &stmt, core::Facet::Temporal); - return; - } - } - decider.decide(core::Facet::Temporal, core::FacetDecision::proven()); -} - -std::vector -Transfer::callTargets(const CallExpr &call, core::Sym calleeValue) const { - std::vector targets; - if (const FunctionDecl *direct = call.getDirectCallee()) - targets.push_back(direct); - if (targets.empty() && calleeValue != core::ZeroSym) { - const core::SymInfo &value = heap.info(state, calleeValue); - if (value.type == core::SymInfo::Type::Function && value.functionsKnown) - for (core::Handle handle : value.functions) - if (const auto *fn = fromHandle(handle)) - targets.push_back(fn); - } - if (targets.empty()) - if (auto resolution = slotResolution(call)) - targets = slotTargets(*resolution); - return targets; -} - -bool Transfer::calleeTouches(const CallExpr &call, core::Sym calleeValue, - unsigned index) const { - std::vector targets = callTargets(call, calleeValue); - if (targets.empty()) - return true; - for (const FunctionDecl *target : targets) { - const core::FunctionEffects *effects = - run.unitRun().summaryOf(*target->getCanonicalDecl()); - // (A library row names what it reads: `free` reads nothing.) - if (effects == nullptr) { - if (governingLibraryEntry(*target, run.unitRun().library())) - continue; - return true; - } - for (const auto *paths : {&effects->reads, &effects->writes}) - for (const core::SummaryPath &path : *paths) - if (path.isParam() && path.index == index && !path.steps.empty() && - path.steps.front().step == core::PathStep::Deref) - return true; - } - return false; -} - -/// RFC 0034 §6.3: a library row gives argument `index` of a call of -/// `callee` no access requirement and no effect (`mmap`'s address hint). -static bool libraryUsesValueOnly(const FunctionDecl &callee, unsigned index, - const core::LibrarySpec &library) { - auto match = governingLibraryEntry(callee, library); - if (!match) - return false; - const int row = match->rowArgument(index); - return row >= 0 && match->entry->usesValueOnly(static_cast(row)); -} - -/// RFC 0033 §1: whether `param` appears in `body` only as a value: an -/// operand of `==` or `!=`, tested for truth, converted to an integer, a -/// variadic argument of a call whose literal format reads no string (`%p`), -/// an argument a library row gives no access and no effect (RFC 0034 -/// §6.3), or an argument of a function of the unit that itself uses it only -/// so (`depth` levels down). -static bool onlyValueUses(UnitRun &unit, const FunctionDecl &definition, - unsigned index, unsigned depth) { - auto [cached, inserted] = - unit.valueOnly.try_emplace({&definition, index, depth}, false); - if (!inserted) - return cached->second; - const ParmVarDecl ¶m = *definition.getParamDecl(index); - const ParentMap parents(definition.getBody()); - const auto parentOf = [&](const Stmt &child) { - return parents.getParent(&child); - }; - // The use of an argument by its callee. - const auto valueArgument = [&](const CallExpr &call, const Expr &argument) { - unsigned index = 0; - while (index < call.getNumArgs() && - call.getArg(index)->IgnoreParenImpCasts() != - argument.IgnoreParenImpCasts()) - ++index; - const FunctionDecl *callee = call.getDirectCallee(); - if (callee == nullptr || index == call.getNumArgs()) - return false; - if (index >= callee->getNumParams()) { - // A variadic argument: a literal format that reads no string reads - // nothing through it. - for (const Expr *other : call.arguments()) - if (const auto *format = - dyn_cast(other->IgnoreParenImpCasts())) - return format->getCharByteWidth() == 1 && - !format->getString().contains("%s") && - !format->getString().contains("%n") && - !format->getString().contains("%."); - return false; - } - const FunctionDecl *definition = nullptr; - if (!callee->hasBody(definition) || definition == nullptr) - return libraryUsesValueOnly(*callee, index, unit.library()); - return depth > 0 && !definition->isVariadic() && - onlyValueUses(unit, *definition, index, depth - 1); - }; - std::vector work{definition.getBody()}; - while (!work.empty()) { - const Stmt *stmt = work.back(); - work.pop_back(); - if (stmt == nullptr) - continue; - if (const auto *ref = dyn_cast(stmt); - ref != nullptr && ref->getDecl() == ¶m) { - // Up through parentheses and the load of its value. - const Stmt *use = parentOf(*ref); - const Stmt *operand = ref; - while (use != nullptr) { - const auto *cast = dyn_cast(use); - if (!isa(use) && - (cast == nullptr || (cast->getCastKind() != CK_LValueToRValue && - cast->getCastKind() != CK_NoOp && - cast->getCastKind() != CK_BitCast))) - break; - operand = use; - use = parentOf(*use); - } - const auto *binary = dyn_cast_or_null(use); - const auto *unary = dyn_cast_or_null(use); - const auto *cast = dyn_cast_or_null(use); - const auto *call = dyn_cast_or_null(use); - // A truth test (`if (p)`, `p && …`, `!p`) is a comparison with null. - const auto truthTest = [&] { - if (const auto *test = dyn_cast_or_null(use)) - return test->getCond() == operand; - if (const auto *test = dyn_cast_or_null(use)) - return test->getCond() == operand; - if (const auto *test = dyn_cast_or_null(use)) - return test->getCond() == operand; - if (const auto *test = dyn_cast_or_null(use)) - return test->getCond() == operand; - if (const auto *test = dyn_cast_or_null(use)) - return test->getCond() == operand; - return (binary != nullptr && binary->isLogicalOp()) || - (unary != nullptr && unary->getOpcode() == UO_LNot); - }; - const bool value = - isa_and_nonnull(use) || - (binary != nullptr && binary->isEqualityOp()) || truthTest() || - (cast != nullptr && (cast->getCastKind() == CK_PointerToIntegral || - cast->getCastKind() == CK_PointerToBoolean)) || - (call != nullptr && call->getCallee() != operand && - valueArgument(*call, *cast_or_null(operand))); - if (!value) - return false; - continue; - } - for (const Stmt *child : stmt->children()) - work.push_back(child); - } - return cached->second = true; -} - -bool Transfer::usesValueOnly(const CallExpr &call, core::Sym calleeValue, - const std::vector &args, - unsigned index) const { - const std::vector targets = - callTargets(call, calleeValue); - if (targets.empty()) - return false; - const core::LibrarySpec &library = run.unitRun().library(); - for (const FunctionDecl *target : targets) { - const FunctionDecl *definition = nullptr; - if (!target->hasBody(definition) || definition == nullptr) { - if (libraryUsesValueOnly(*target, index, library)) - continue; - return false; - } - if (index >= definition->getNumParams() || definition->isVariadic()) - return false; - const core::FunctionEffects *effects = - run.unitRun().summaryOf(*target->getCanonicalDecl()); - if (effects == nullptr || effects->incomplete || - !onlyValueUses(run.unitRun(), *definition, index, 3)) - return false; - } - return !calleeReleases(call, calleeValue, args, index, true); -} - -bool Transfer::calleeReleases(const CallExpr &call, core::Sym calleeValue, - const std::vector &args, - unsigned index, bool possibly) const { - std::vector targets = callTargets(call, calleeValue); - if (targets.empty()) - return false; - // Whether one function releases the argument (`possibly`: on some - // outcome or under some test is enough). - auto releases = [&](const FunctionDecl &target) { - const core::FunctionEffects *effects = - run.unitRun().summaryOf(*target.getCanonicalDecl()); - if (effects == nullptr) { - if (run.unitRun().hasBody(*target.getCanonicalDecl())) - return false; - // A library row that releases the argument (`free`). - if (auto match = governingLibraryEntry(target, run.unitRun().library())) - return index < match->entry->params.size() && - match->entry->params[index].effect == - core::LibraryParam::Effect::Release; - // RFC 0031 §5.4 (a declared contract), RFC 0010: a declaration whose - // parameter is `WEAVEC_OWNED` or `WEAVEC_RELEASES` releases it. - return std::ranges::any_of( - target.redecls(), [&](const FunctionDecl *redecl) { - if (index >= redecl->getNumParams()) - return false; - AnnotationSet set = getAnnotations(*redecl->getParamDecl(index)); - return set.owned || set.frees || set.releases; - }); - } - const core::SummaryPath released = core::SummaryPath::param(index).deref(); - for (const core::PathEffect &effect : effects->effects) { - if (possibly && effect.kind == core::PathEffect::Kind::Release && - effect.path == released) - return true; - if (effect.kind != core::PathEffect::Kind::Release || effect.may || - !effect.when.classes.empty() || !(effect.path == released)) - continue; - if (effect.when.paramZero) { - auto [param, zero] = *effect.when.paramZero; - auto argument = param < args.size() ? state.zone.constant(args[param]) - : std::nullopt; - if (!argument || (*argument == 0) != zero) - continue; - } - if (effect.when.paramsEqual && - argumentsEqual(args, *effect.when.paramsEqual) != - effect.when.paramsEqual->equal) - continue; - // (A case on a value at entry: not decided here.) - if (effect.when.entryZero) - continue; - return true; - } - return false; - }; - // Some target may release it; every target must, to say it does. - if (possibly) - return std::ranges::any_of( - targets, [&](const FunctionDecl *fn) { return releases(*fn); }); - return std::ranges::all_of( - targets, [&](const FunctionDecl *fn) { return releases(*fn); }); -} - -/// Whether ownership annotations on `callee`'s declarations cover every -/// pointer argument of `call` (and no library row governs it first). -static bool declaresEveryPointer(const FunctionDecl &callee, - const CallExpr &call, - const core::LibrarySpec &library) { - if (governingLibraryEntry(callee, library)) - return false; - SignatureAnnotations signature = collectAnnotations(callee); - if (!signature.anyOwnership()) - return false; - for (unsigned i = 0; i < call.getNumArgs(); ++i) - if (call.getArg(i)->getType()->isPointerType() && - (i >= signature.params.size() || !signature.params[i].ownership())) - return false; - return true; -} - -void Transfer::decideCall(const CallExpr &call, - const std::vector &args, - core::Sym calleeValue) { - const SiteIndex &sites = run.sites(); - const FunctionDecl *direct = call.getDirectCallee(); - const UnitRun &unit = run.unitRun(); - // RFC 0003: what the callee's annotations do to this function's own. - if (run.isPublishing()) - checkCallAnnotations(call, args); - for (core::SiteId id : sites.sitesOf(call)) { - const SiteInfo *info = sites.info(id); - if (info == nullptr) - continue; - Decider decider(*this, *info); - // Arguments that must not be null (the library row, declared or - // inferred requirements). - for (const ArgumentNeed &need : info->arguments) { - if (need.argument >= args.size() || !need.nonnull || need.inferred) - continue; - const core::SymInfo &value = heap.info(state, args[need.argument]); - core::FacetDecision decision; - // RFC 0031 §5.4: a length that is zero accepts null (`memcpy(p, x, - // 0)`): nothing to check. - std::function isZero = - [&](const SourceTerm &term) -> bool { - switch (term.kind) { - case SourceTerm::Kind::Constant: - return term.constant == 0; - case SourceTerm::Kind::Expr: { - if (term.expr == nullptr) - return false; - auto c = state.zone.constant(valueOf(*term.expr)); - return c && *c == 0; - } - case SourceTerm::Kind::Mul: - return std::ranges::any_of(term.operands, isZero); - case SourceTerm::Kind::Add: - return !term.operands.empty() && - std::ranges::all_of(term.operands, isZero); - default: - return false; - } - }; - // RFC 0030 §8.3: a length that is non-zero for every value makes a - // `null-if-zero` argument's null requirement definite. - std::function isNonZero = - [&](const SourceTerm &term) -> bool { - switch (term.kind) { - case SourceTerm::Kind::Constant: - return term.constant != 0; - case SourceTerm::Kind::Expr: { - if (term.expr == nullptr) - return false; - core::Sym length = valueOf(*term.expr); - auto lo = state.zone.lower(length); - return (lo && *lo > 0) || - (heap.info(state, length).nonZero && lo && *lo >= 0); - } - case SourceTerm::Kind::Mul: - return !term.operands.empty() && - std::ranges::all_of(term.operands, isNonZero); - default: - return false; - } - }; - bool nonZeroLength = !need.allowedIfZero || - (need.unlessZero && isNonZero(*need.unlessZero)); - if (value.null == core::PointerNull::NonNull || - (need.allowedIfZero && need.unlessZero && isZero(*need.unlessZero))) { - decision = core::FacetDecision::proven(); - } else if (need.systemApi) { - decision = - core::FacetDecision::trustedFor(core::TrustReason::SystemApi); - } else if (value.null == core::PointerNull::Null && nonZeroLength && - !value.allocatorSource) { - decision = core::FacetDecision::violation(); - std::string callee = - direct != nullptr - ? inQuotes(info->library ? info->library->entry->name - : direct->getNameAsString()) - : "a function pointer"; - run.report( - nullArgument(*call.getArg(need.argument), value, callee, direct), - core::Certainty::Definite, &call, core::Facet::Null); - } else { - decision = core::FacetDecision::unresolvedFor( - core::UnresolvedReason::Undecided); - if (run.applies(info->id, core::Facet::Null)) - allocationFailure(value, *call.getArg(need.argument), call); - } - if (run.isPublishing() && run.applies(info->id, core::Facet::Null)) - run.ledger().requirement(call, core::Facet::Null, decision); - } - // A library call's temporal facet is its arguments' uses: proven unless - // one of them says otherwise (decisions keep the worst). - if (info->kind == core::SiteKind::LibCall) { - std::optional open; - if (direct == nullptr) - if (auto resolution = slotResolution(call)) - open = core::openCallTemporalDecision(*resolution); - // RFC 0030 §8.2, §5.3: a row's statement about hidden behaviour (a - // callback clause, a static slot) is trusted; a `sync` callback whose - // target is unknown is the unknown-callee default. - if (!open && info->library) { - const core::LibraryMatch &match = *info->library; - bool unknownTarget = false; - for (unsigned i = 0; i < call.getNumArgs() && i < args.size(); ++i) - if (const core::LibraryParam *param = match.param(i); - param != nullptr && param->callback && - param->callback->kind == core::LibCallback::Kind::Sync && - syncTargets(args[i]).empty()) - unknownTarget = true; - if (unknownTarget) - open = core::FacetDecision::unresolvedFor( - core::UnresolvedReason::Callback, - "the callback of " + inQuotes(match.entry->name) + - " is not known here"); - else if (match.entry->trustsLibrarySpec()) - open = - core::FacetDecision::trustedFor(core::TrustReason::LibrarySpec); - } - decider.decide(core::Facet::Temporal, - open ? *open : core::FacetDecision::proven()); - if (info->library) - for (const std::string &slot : info->library->entry->reads) - decider.readsState(slot); - } - // Every pointer argument is a use of its object (the callee may read - // it), except the one a release consumes, which the release decides - // (a raw one too, RFC 0004). - const bool releaseSite = - info->kind == core::SiteKind::Release || - (info->kind == core::SiteKind::Raw && isa(info->stmt)); - if (!releaseSite && (info->kind != core::SiteKind::Call || - info->boundary != core::Boundary::Exit)) - for (unsigned i = 0; i < call.getNumArgs() && i < args.size(); ++i) { - const Expr &arg = *call.getArg(i); - if (!arg.getType()->isPointerType() || args[i] == core::ZeroSym) - continue; - const core::SymInfo &value = heap.info(state, args[i]); - if (value.type != core::SymInfo::Type::Pointer || - value.null == core::PointerNull::Null) - continue; - core::TemporalVerdict verdict = heap.temporal(state, args[i]); - // RFC 0033 §1: a callee whose summary neither reads, writes nor - // releases through the argument uses only its value (a table keyed - // by address, `set_remove(s, p)` after `free(p)`), which accesses - // no memory. - if (verdict.kind != core::TemporalVerdict::Kind::Proven && - usesValueOnly(call, calleeValue, args, i)) - continue; - // A second release when the callee releases it, or may where the - // argument is only possibly released already, or may and reads - // nothing through it first; otherwise the first invalid operation - // is the callee's use of it. - if (verdict.kind != core::TemporalVerdict::Kind::Proven) - decider.temporalOf( - args[i], arg, - calleeReleases(call, calleeValue, args, i) || - ((verdict.kind == core::TemporalVerdict::Kind::MayReleased || - (verdict.kind == core::TemporalVerdict::Kind::Violation && - !calleeTouches(call, calleeValue, i))) && - calleeReleases(call, calleeValue, args, i, true))); - } - if (releaseSite && info->operand != nullptr) - for (unsigned i = 0; i < call.getNumArgs() && i < args.size(); ++i) { - const Expr &arg = *call.getArg(i); - if (&arg == info->operand || - arg.IgnoreParenImpCasts() == info->operand->IgnoreParenImpCasts()) - continue; - if (!arg.getType()->isPointerType() || args[i] == core::ZeroSym) - continue; - const core::SymInfo &value = heap.info(state, args[i]); - if (value.type != core::SymInfo::Type::Pointer || - value.null == core::PointerNull::Null) - continue; - if (heap.temporal(state, args[i]).kind != - core::TemporalVerdict::Kind::Proven) - decider.temporalOf(args[i], arg, false); - } - switch (info->kind) { - case core::SiteKind::Raw: - case core::SiteKind::Release: { - if (info->operand == nullptr) - break; - core::Sym pointer = valueOf(*info->operand); - std::string family; - if (info->library) - for (const core::LibraryParam ¶m : info->library->entry->params) - if (param.effect == core::LibraryParam::Effect::Release || - param.effect == core::LibraryParam::Effect::Realloc) - family = param.family.empty() ? std::string(core::HeapFamily) - : param.family; - decider.release(pointer, family); - if (run.isPublishing()) - checkConsumeAnnotation(pointer, *info->operand, call, /*moved=*/false); - break; - } - case core::SiteKind::LibCall: - // Requirement records are decided by the library rules. - decideLibraryCall(call, *info, args); - break; - case core::SiteKind::Call: { - if (info->boundary == core::Boundary::Exit) - break; - // The callee operand of an indirect call. - if (direct == nullptr) { - const core::SymInfo &target = heap.info(state, calleeValue); - if (target.type == core::SymInfo::Type::Pointer && - target.null == core::PointerNull::Null && !target.allocatorSource && - !target.mayUninit) { - // A call through a null function pointer (RFC 0030 §3.2). - decider.decide(core::Facet::Null, core::FacetDecision::violation()); - const Expr &callee = *call.getCallee(); - std::string name = spell(callee); - core::Diagnostic diagnostic = makeDiagnostic( - core::diag::NullDereference, - "dereference of " + inQuotes(name) + ", which is null", run.ast(), - callee.getBeginLoc(), core::Severity::Error); - addNullNote(diagnostic, target, name); - decider.report(std::move(diagnostic), core::Certainty::Definite, - core::Facet::Null); - } else { - decider.decide(core::Facet::Null, - target.type == core::SymInfo::Type::Function && - target.functionsKnown && - !target.functions.empty() - ? core::FacetDecision::proven() - : core::FacetDecision::unresolvedFor( - core::UnresolvedReason::Undecided)); - } - } - // RFC 0004: a raw pointer handed to a callee that takes a tracked - // pointer there (or its ownership), outside an unsafe region. - if (direct != nullptr && !info->inUnsafe && run.isPublishing()) - for (unsigned i = 0; i < call.getNumArgs() && i < args.size() && - i < direct->getNumParams(); - ++i) { - const core::SymInfo value = heap.info(state, args[i]); - if (value.type != core::SymInfo::Type::Pointer || !value.raw || - value.rawSome) - continue; - AnnotationSet set; - for (const FunctionDecl *redecl : direct->redecls()) - if (i < redecl->getNumParams()) - set.merge(getAnnotations(*redecl->getParamDecl(i))); - if (set.raw || set.unsafe) - continue; - const Expr &argument = *call.getArg(i); - std::string name = - !isa(argument.IgnoreParenImpCasts()) - ? spell(argument) - : std::string(); - core::Diagnostic diagnostic = makeDiagnostic( - core::diag::UnsafeOperation, - inQuotes(direct->getNameAsString()) + - (set.owned ? " takes ownership of raw pointer " - : " dereferences raw pointer ") + - (name.empty() ? std::string() : inQuotes(name) + " ") + - "outside an unsafe region", - run.ast(), argument.getBeginLoc(), core::Severity::Error); - addNote(diagnostic, rawNote(value, name), value.rawAt); - diagnostic.addNote(UnsafeFixIt, core::SourceLocation{}); - decider.report(std::move(diagnostic), core::Certainty::Definite, - core::Facet::Spatial); - } - decideCallKinds(call, *info, args); - // The temporal facet by what the callee is. - if (direct != nullptr) { - const FunctionDecl *canonical = direct->getCanonicalDecl(); - if (unit.summaryOf(*canonical) != nullptr || unit.hasBody(*canonical)) { - decider.decide(core::Facet::Temporal, core::FacetDecision::proven()); - } else if (const OwnershipContract *contract = - unit.input.kinds.ownership(*direct); - (contract != nullptr && !contract->empty()) || - declaresEveryPointer(*direct, call, unit.library())) { - // RFC 0003, RFC 0030 §5.1: ownership annotations on every pointer - // parameter are the callee's contract. - decider.decide(core::Facet::Temporal, - core::FacetDecision::trustedFor( - core::TrustReason::ExternContract)); - } else if (isPlatformDeclaration(*direct, unit.library(), - run.ast().getSourceManager())) { - decider.decide( - core::Facet::Temporal, - core::FacetDecision::trustedFor(core::TrustReason::SystemApi)); - } else if (governingLibraryEntry(*direct, unit.library())) { - decider.decide( - core::Facet::Temporal, - core::FacetDecision::trustedFor(core::TrustReason::LibrarySpec)); - } else { - bool anyPointer = false; - for (const Expr *arg : call.arguments()) - anyPointer = anyPointer || arg->getType()->isPointerType(); - if (anyPointer || call.getType()->isPointerType()) - decider.unknownCall(call, false); - else - decider.decide(core::Facet::Temporal, - core::FacetDecision::unresolvedFor( - core::UnresolvedReason::UnknownCallee, - "define '" + direct->getNameAsString() + - "' in this program, or link a unit that " - "has its WeaveC record")); - } - } else { - const core::SymInfo &target = heap.info(state, calleeValue); - const bool functionsKnown = - target.type == core::SymInfo::Type::Function && - target.functionsKnown && !target.functions.empty(); - std::optional resolution; - if (!functionsKnown) - resolution = slotResolution(call); - if (functionsKnown || - (resolution && - (resolution->kind == core::IndirectCallKind::ClosedSingle || - resolution->kind == core::IndirectCallKind::ClosedJoin))) - decider.decide(core::Facet::Temporal, core::FacetDecision::proven()); - else if (resolution) - if (auto decision = core::openCallTemporalDecision(*resolution)) - decider.decide(core::Facet::Temporal, *decision); - else - decider.unknownCall(call, true); - else - decider.unknownCall(call, true); - } - break; - } - default: - break; - } - } -} - -} // namespace weavec::analysis::engine diff --git a/lib/Analysis/EngineExpr.cpp b/lib/Analysis/EngineExpr.cpp deleted file mode 100644 index 0da006c9..00000000 --- a/lib/Analysis/EngineExpr.cpp +++ /dev/null @@ -1,3671 +0,0 @@ -//===- EngineExpr.cpp - Expressions in the object engine ------------------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// RFC 0031 §5.1: rvalues evaluate to symbols, lvalues to addresses; loads and -// stores go through the address. Every subexpression is a CFG element, so an -// expression's operands are found in the block's memo (or, for the operands -// of `?:`, `&&` and `||`, in the state's expression values). -// -//===----------------------------------------------------------------------===// - -#include "Engine.h" -#include "weavec/Analysis/ClangLocation.h" - -#include "clang/AST/RecordLayout.h" -#include "clang/Basic/SourceManager.h" -#include "clang/Lex/Lexer.h" - -#include -#include -#include -#include - -using namespace clang; - -namespace weavec::analysis::engine { - -Transfer::Transfer(FunctionRun &functionRun, core::HeapState &heapState) - : run(functionRun), heap(functionRun.domain()), state(heapState), - context(functionRun.ast()) {} - -//===----------------------------------------------------------------------===// -// Types and constants -//===----------------------------------------------------------------------===// - -std::optional Transfer::sizeOf(QualType type) const { - if (type.isNull() || type->isIncompleteType() || type->isFunctionType() || - type->isVoidType()) - return std::nullopt; - if (type->isVariablyModifiedType()) - return std::nullopt; - return static_cast( - context.getTypeSizeInChars(type).getQuantity()); -} - -void Transfer::captureVlas(QualType type) { - // Only the dimensions this declaration spells: a typedef's were taken - // where the typedef ran. - const Type *node = type.getTypePtrOrNull(); - while (node != nullptr) { - if (const auto *paren = dyn_cast(node)) { - node = paren->getInnerType().getTypePtrOrNull(); - } else if (const auto *pointer = dyn_cast(node)) { - node = pointer->getPointeeType().getTypePtrOrNull(); - } else if (const auto *vla = dyn_cast(node)) { - if (const Expr *size = vla->getSizeExpr()) { - core::Sym count = valueOf(*size); - state.exprs.set(handleOf(vla), count); - // RFC 0017: a dimension that is zero or negative for every value. - if (auto hi = state.zone.upper(count); - hi && *hi <= 0 && run.isPublishing()) { - core::Diagnostic diagnostic; - diagnostic.id = core::diag::InvalidIntegerOperation; - diagnostic.severity = core::Severity::Error; - diagnostic.message = "invalid integer operation: nonpositive " - "variable array dimension"; - diagnostic.location = - toCoreLocation(context.getSourceManager(), size->getBeginLoc()); - run.report(std::move(diagnostic), core::Certainty::Definite, size, - std::nullopt); - } - } - node = vla->getElementType().getTypePtrOrNull(); - } else if (const auto *array = dyn_cast(node)) { - node = array->getElementType().getTypePtrOrNull(); - } else if (const auto *attributed = dyn_cast(node)) { - node = attributed->getModifiedType().getTypePtrOrNull(); - } else { - node = nullptr; - } - } -} - -core::Sym Transfer::vlaCount(const VariableArrayType &vla) { - if (const core::Sym *held = state.exprs.find(handleOf(&vla))) - return *held; - // Declared where the analysis did not see it run: any count. - return unknownValue(context.getSizeType()); -} - -std::optional Transfer::bytesOf(QualType type, const Expr &at) { - if (type.isNull()) - return std::nullopt; - if (!type->isVariablyModifiedType()) { - auto size = sizeOf(type); - if (!size) - return std::nullopt; - return constant(*size, context.getSizeType()); - } - const ArrayType *array = context.getAsArrayType(type); - if (array == nullptr) - return std::nullopt; - auto element = bytesOf(array->getElementType(), at); - if (!element) - return std::nullopt; - core::Sym count = core::ZeroSym; - if (const auto *vla = dyn_cast(array)) { - if (vla->getSizeExpr() == nullptr) - return std::nullopt; - count = vlaCount(*vla); - // A dimension that may be zero or negative makes no storage C defines - // (RFC 0017): nothing is known of the object's size. - auto lo = state.zone.lower(count); - if (!lo || *lo < 1) - return std::nullopt; - count = convertInteger(count, vla->getSizeExpr()->getType(), - context.getSizeType()); - } else if (const auto *fixed = dyn_cast(array)) { - count = constant(static_cast(fixed->getZExtSize()), - context.getSizeType()); - } else { - return std::nullopt; - } - core::Sym bytes = - arithmetic(BO_Mul, count, *element, context.getSizeType(), at); - // A product that may wrap is no size (RFC 0017): the object is as large - // as the mathematical product, which `size_t` cannot hold. - const core::SymInfo &info = heap.info(state, bytes); - if (!info.defined || !info.defined->exact) - return std::nullopt; - return bytes; -} - -/// The leaves below `base` of a value of `type` (RFC 0015 §4). -static bool leavesOf(const ASTContext &context, QualType type, - std::int64_t base, - std::vector> &out, - int depth) { - if (depth > 4 || out.size() > 32 || type.isNull() || type->isIncompleteType()) - return false; - type = type.getCanonicalType(); - if (type->isPointerType() || type->isIntegralOrEnumerationType()) { - out.emplace_back(base, type); - return true; - } - if (type->isRealFloatingType()) - return true; // no pointer or integer facts to carry - if (const auto *array = context.getAsConstantArrayType(type)) { - QualType element = array->getElementType(); - if (element->isIncompleteType()) - return false; - auto size = static_cast( - context.getTypeSizeInChars(element).getQuantity()); - std::uint64_t count = array->getSize().getZExtValue(); - if (count > 16 || size <= 0) - return false; - for (std::uint64_t i = 0; i < count; ++i) - if (!leavesOf(context, element, - base + (static_cast(i) * size), out, - depth + 1)) - return false; - return true; - } - const RecordDecl *record = type->getAsRecordDecl(); - if (record == nullptr || !record->isCompleteDefinition() || record->isUnion()) - return false; - const ASTRecordLayout &layout = context.getASTRecordLayout(record); - for (const FieldDecl *field : record->fields()) { - if (field->isBitField()) - return false; - auto offset = static_cast( - layout.getFieldOffset(field->getFieldIndex()) / context.getCharWidth()); - if (!leavesOf(context, field->getType(), base + offset, out, depth + 1)) - return false; - } - return true; -} - -bool Transfer::recordLeaves( - QualType type, std::vector> &out) const { - out.clear(); - return leavesOf(context, type, 0, out, 0); -} - -bool Transfer::copyRecord(const Address &to, const Address &from, - QualType type) { - std::vector> leaves; - if (to.top || from.top || to.targets.empty() || from.targets.empty() || - !recordLeaves(type, leaves)) - return false; - std::vector values; - for (const auto &[offset, leafType] : leaves) { - Address cell = from; - for (core::Target &target : cell.targets) - target.offset = target.offset.plusConstant(offset); - values.push_back(load(cell, leafType, nullptr)); - } - for (std::size_t i = 0; i < leaves.size(); ++i) { - Address cell = to; - for (core::Target &target : cell.targets) - target.offset = target.offset.plusConstant(leaves[i].first); - store(cell, values[i], leaves[i].second, nullptr); - } - return true; -} - -std::optional Transfer::integerType(QualType type) const { - if (type.isNull()) - return std::nullopt; - type = type.getCanonicalType(); - if (type->isPointerType()) - return core::IntegerType{.width = static_cast( - context.getTypeSize(context.getUIntPtrType())), - .isSigned = false}; - if (!type->isIntegralOrEnumerationType()) - return std::nullopt; - return core::IntegerType{.width = - static_cast(context.getTypeSize(type)), - .isSigned = type->isSignedIntegerOrEnumerationType(), - .isBoolean = type->isBooleanType()}; -} - -/// The range of a C integer type, when it fits in 64-bit bounds. -static std::pair, std::optional> -typeRange(const core::IntegerType &type) { - if (type.isBoolean) - return {0, 1}; - if (type.isSigned) { - if (type.width >= 64) - return {INT64_MIN, INT64_MAX}; - std::int64_t hi = - static_cast(std::uint64_t{1} << (type.width - 1)) - 1; - return {-hi - 1, hi}; - } - if (type.width >= 63) - return {0, std::nullopt}; - return {0, static_cast(std::uint64_t{1} << type.width) - 1}; -} - -core::Sym Transfer::constant(std::int64_t value, QualType type) { - core::Sym sym = heap.constant(state, value, integerType(type)); - heap.infoMut(state, sym).ctype = typeHandle(type); - if (!type.isNull() && type->isPointerType()) { - core::SymInfo &info = heap.infoMut(state, sym); - info.type = core::SymInfo::Type::Pointer; - info.null = value == 0 ? core::PointerNull::Null : core::PointerNull::Maybe; - // RFC 0033 §2: a non-zero address written as a number is a pointer of - // unknown provenance, not a raw one. - if (value != 0) { - core::ObjectId any = run.unknownObject(); - heap.ensure(state, any); - info.targets = {core::Target{.object = any}}; - info.rawCast = true; - } - } - return sym; -} - -core::Sym Transfer::constant(const llvm::APSInt &value, QualType type) { - if (value.isSigned() ? value.getSignificantBits() <= 64 - : value.getActiveBits() <= 63) - return constant(value.isSigned() - ? value.getSExtValue() - : static_cast(value.getZExtValue()), - type); - core::Sym sym = unknownValue(type); - auto integer = integerType(type); - if (!integer || integer->isBoolean || integer->width > 64 || - value.getActiveBits() > 64) - return sym; - // (An unsigned 64-bit value above `INT64_MAX`: its interval, §4.4.) - core::IntegerRange range = core::IntegerRange::singleton( - core::IntegerValue::ofBits(*integer, value.getZExtValue())); - if (!range.empty()) - heap.infoMut(state, sym).values = range; - return sym; -} - -core::Sym Transfer::nullPointer(QualType type) { - core::SymInfo info; - info.type = core::SymInfo::Type::Pointer; - info.null = core::PointerNull::Null; - info.ctype = typeHandle(type); - return heap.fresh(state, info); -} - -core::Sym Transfer::unknownValue(QualType type) { - core::SymInfo info; - info.ctype = typeHandle(type); - if (!type.isNull() && type->isPointerType()) { - if (type->getPointeeType()->isFunctionType()) { - info.type = core::SymInfo::Type::Function; - return heap.fresh(state, info); - } - info.type = core::SymInfo::Type::Pointer; - core::ObjectId any = run.unknownObject(); - heap.ensure(state, any); - info.targets = {core::Target{.object = any}}; - return heap.fresh(state, info); - } - if (auto integer = integerType(type)) { - info.type = core::SymInfo::Type::Int; - info.intType = integer; - core::Sym sym = heap.fresh(state, info); - auto [lo, hi] = typeRange(*integer); - state.zone.addRange(sym, lo, hi); - return sym; - } - return heap.fresh(state, info); -} - -core::Sym Transfer::pointerTo(const Address &address, QualType pointee, - std::string name) { - core::SymInfo info; - info.type = core::SymInfo::Type::Pointer; - info.targets = address.targets; - info.top = address.top; - info.null = core::PointerNull::NonNull; - info.name = std::move(name); - // RFC 0011: `&p->f` borrows the object `p` points to. - info.derived = address.base != core::ZeroSym; - info.ctype = - pointee.isNull() ? 0 : typeHandle(context.getPointerType(pointee)); - if (address.top) { - core::ObjectId any = run.unknownObject(); - heap.ensure(state, any); - info.targets = {core::Target{.object = any}}; - info.top = false; - } - for (const core::Target &target : info.targets) - heap.ensure(state, target.object); - return heap.fresh(state, info); -} - -core::Term Transfer::termOf(core::Sym sym) const { - const core::SymInfo &info = heap.info(state, sym); - if (auto c = state.zone.constant(sym)) - return core::Term::of(*c); - if (info.linear) - return *info.linear; - if (info.type == core::SymInfo::Type::Int) - return core::Term::ofSym(sym); - return core::Term::unknown(); -} - -//===----------------------------------------------------------------------===// -// Spelling -//===----------------------------------------------------------------------===// - -std::string Transfer::spell(const Expr &expr) const { - const Expr *e = expr.IgnoreParenImpCasts(); - if (const auto *ref = dyn_cast(e)) - return ref->getDecl()->getNameAsString(); - if (const auto *member = dyn_cast(e)) { - std::string base = spell(*member->getBase()); - return base + (member->isArrow() ? "->" : ".") + - member->getMemberDecl()->getNameAsString(); - } - if (const auto *unary = dyn_cast(e)) { - if (unary->getOpcode() == UO_Deref) { - const Expr *sub = unary->getSubExpr()->IgnoreImpCasts(); - if (isa(sub)) - return "*(" + spell(*sub) + ")"; - return "*" + spell(*unary->getSubExpr()); - } - if (unary->getOpcode() == UO_AddrOf) - return "&" + spell(*unary->getSubExpr()); - } - if (const auto *subscript = dyn_cast(e)) - return spell(*subscript->getBase()) + "[" + spell(*subscript->getIdx()) + - "]"; - if (const auto *cast = dyn_cast(e)) - return spell(*cast->getSubExpr()); - const SourceManager &sm = context.getSourceManager(); - CharSourceRange range = CharSourceRange::getTokenRange(e->getSourceRange()); - std::string text = - Lexer::getSourceText(range, sm, context.getLangOpts()).str(); - std::erase_if(text, [](char c) { return c == '\n' || c == '\t'; }); - return text; -} - -//===----------------------------------------------------------------------===// -// Elements -//===----------------------------------------------------------------------===// - -void Transfer::element(const CFGElement &element) { - if (auto stmt = element.getAs()) { - const Stmt *s = stmt->getStmt(); - // A value dead where a statement starts is leaked (RFC 0007). - if (statementStart && run.isPublishing()) - run.checkLeaks(state, *s, FunctionRun::LeakPoint::Statement); - statementStart = run.isStatementLevel(*s); - run.noteElement(*s); - if (auto early = run.vlaCaptureBefore.find(s); - early != run.vlaCaptureBefore.end()) - for (const VarDecl *var : early->second) - captureVlas(var->getType()); - if (const auto *expr = dyn_cast(s)) { - evaluate(*expr); - } else if (const auto *decl = dyn_cast(s)) { - for (const Decl *d : decl->decls()) { - if (const auto *var = dyn_cast(d)) { - if (!var->hasGlobalStorage() && !run.vlaCapturedEarly.contains(var)) - captureVlas(var->getType()); - declare(*var); - } else if (const auto *alias = dyn_cast(d)) { - captureVlas(alias->getUnderlyingType()); - } - } - } else if (const auto *ret = dyn_cast(s)) { - returned(*ret); - return; - } else if (const auto *assembly = dyn_cast(s)) { - inlineAssembly(*assembly); - } - // (The site's facets are decided first: they are about the pointer - // before the access.) - if (!run.isPublishing()) - accessed(*s); - if (run.isPublishing()) { - decideSites(*s); - accessed(*s); - if (run.isStatementLevel(*s)) { - // The statement uses the objects its values point to. - for (const auto &[expr, result] : memo) { - if (result.value == core::ZeroSym || isa(expr)) - continue; - const core::SymInfo *info = state.syms.find(result.value); - if (info == nullptr) - continue; - for (const core::Target &target : info->targets) - if (state.objects.contains(target.object) && !isa(s)) - state.objects.at(target.object).lastUse = handleOf(s); - } - std::string released; - if (const auto *callExpr = dyn_cast(s)) - for (core::SiteId id : run.sites().sitesOf(*callExpr)) - if (const SiteInfo *info = run.sites().info(id); - info != nullptr && info->kind == core::SiteKind::Release && - info->operand != nullptr) { - released = spell(*info->operand); - declaredOwners(*info->operand, released); - } - if (!released.empty()) - run.checkLeaks(state, *s, FunctionRun::LeakPoint::Release, released); - } - } - return; - } - if (auto cleanup = element.getAs()) { - if (const VarDecl *var = cleanup->getVarDecl()) - cleanupFunction(*var); - return; - } - if (auto lifetime = element.getAs()) { - if (const VarDecl *var = lifetime->getVarDecl()) - lifetimeEnds(*var); - return; - } -} - -// NOLINTNEXTLINE(readability-convert-member-functions-to-static): block hook -void Transfer::finishBlock(const CFGBlock &block) { - (void)block; -} - -void Transfer::declaredOwners(const Expr &operand, - const std::string &released) { - const core::SymInfo value = heap.info(state, valueOf(operand)); - if (value.type != core::SymInfo::Type::Pointer || value.top) - return; - QualType pointee = operand.IgnoreParenImpCasts()->getType(); - if (!pointee->isPointerType()) - return; - const RecordDecl *record = pointee->getPointeeType()->getAsRecordDecl(); - if (record == nullptr || !record->isCompleteDefinition()) - return; - const ASTRecordLayout &layout = context.getASTRecordLayout(record); - for (const core::Target &container : value.targets) { - // RFC 0007 *Owned fields*: what an entry container's `WEAVEC_OWNED` - // field held at entry is the container's to release with it. - if (run.table().info(container.object).key.kind != - core::ObjectKind::Entry || - !container.offset.isConstant()) - continue; - for (const FieldDecl *field : record->fields()) { - if (!field->getType()->isPointerType() || !getAnnotations(*field).owned) - continue; - Address cell; - cell.targets = {core::Target{ - .object = container.object, - .offset = container.offset.plusConstant(static_cast( - layout.getFieldOffset(field->getFieldIndex()) / - context.getCharWidth()))}}; - const core::SymInfo held = - heap.info(state, load(cell, field->getType(), nullptr)); - if (held.type != core::SymInfo::Type::Pointer || - held.null == core::PointerNull::Null || held.top) - continue; - for (const core::Target &target : held.targets) { - if (run.table().info(target.object).key.kind != - core::ObjectKind::Entry || - !state.objects.contains(target.object)) - continue; - core::ObjectState &object = state.objects.at(target.object); - if (object.owned || object.escaped || object.life != core::Life::Live) - continue; - object.owned = true; - object.holder = released + "->" + field->getNameAsString(); - run.declaredOwner[target.object] = std::make_pair( - field->getNameAsString(), - toCoreLocation(context.getSourceManager(), field->getLocation())); - } - } - } -} - -core::Sym Transfer::conditionValue(const Expr &condition) { - return valueOf(condition); -} - -ExprResult Transfer::evaluate(const Expr &expr) { - if (auto it = memo.find(&expr); it != memo.end()) - return it->second; - if (const core::Sym *carried = state.exprs.find(handleOf(&expr)); - carried != nullptr && !run.evaluatedHere(expr)) { - ExprResult result{.value = *carried, .address = std::nullopt}; - memo[&expr] = result; - return result; - } - ExprResult result = evaluateUncached(expr); - // RFC 0017: an integer constant expression is its value, even where its - // operands do not fit the zone's 64-bit bounds (`(size_t)-1 / 8 + 1`). - if (result.value != core::ZeroSym && !result.address && - expr.getType()->isIntegerType() && !expr.getType()->isBooleanType() && - !state.zone.constant(result.value) && - (isa(expr) || isa(expr) || - isa(expr)) && - !expr.HasSideEffects(context)) { - Expr::EvalResult folded; - if (expr.EvaluateAsInt(folded, context)) - result.value = constant(folded.Val.getInt(), expr.getType()); - } - memo[&expr] = result; - if (result.value != core::ZeroSym) { - if (run.isCrossBlock(expr)) - state.exprs.set(handleOf(&expr), result.value); - if (const Expr *parent = run.armOf(expr)) - state.exprs.set(handleOf(parent), result.value); - } - return result; -} - -core::Sym Transfer::valueOf(const Expr &expr) { - ExprResult result = evaluate(expr); - if (result.value != core::ZeroSym) - return result.value; - if (result.address) { - // An lvalue used as a value: a load (a record or array rvalue). - QualType type = expr.getType(); - if (type->isArrayType()) - return pointerTo(*result.address, - context.getAsArrayType(type)->getElementType(), - spell(expr)); - core::Sym loaded = load(*result.address, type, &expr); - memo[&expr].value = loaded; - return loaded; - } - return unknownValue(expr.getType()); -} - -Address Transfer::addressOf(const Expr &expr) { - ExprResult result = evaluate(expr); - if (result.address) - return *result.address; - // An rvalue of pointer type used as an address (`*f()` folded, a record - // returned by value): a temporary object. - Address address; - address.targets = {core::Target{.object = run.literalObject(expr)}}; - heap.ensure(state, address.targets[0].object); - if (result.value != core::ZeroSym) - heap.write(state, address.targets[0].object, core::CellKey{}, result.value, - false); - return address; -} - -//===----------------------------------------------------------------------===// -// Memory -//===----------------------------------------------------------------------===// - -/// The cell key of an offset: concrete for a constant, a selected element -/// cell for an affine offset (§4.2 *Amendment (arrays)*), none for an -/// unknown offset. -static std::optional cellKeyOf(const core::Term &offset) { - return core::CellKey::at(offset); -} - -bool Transfer::complementary(const Address &address) const { - if (address.top || address.targets.size() != 2) - return false; - auto existence = [&](core::ObjectId id) -> std::optional { - const core::ObjectInfo &info = run.table().info(id); - if (!info.singular) - return std::nullopt; - if (info.key.kind == core::ObjectKind::Entry && !info.key.dead && - info.heldIn) - return core::EntryTest{.object = info.heldIn->first, - .key = - core::CellKey{.offset = info.heldIn->second}, - .zero = false}; - if (const core::ObjectState *object = heap.findObject(state, id)) - return object->existsIfEntry; - return std::nullopt; - }; - auto first = existence(address.targets[0].object); - auto second = existence(address.targets[1].object); - return first && second && first->object == second->object && - first->key == second->key && first->zero != second->zero; -} - -core::Sym Transfer::load(const Address &address, QualType type, - const Expr *at) { - (void)at; - if (address.top || address.targets.empty() || type.isNull()) - return unknownValue(type); - core::Sym result = core::ZeroSym; - core::SymInfo hint; - if (type->isPointerType()) - hint.type = type->getPointeeType()->isFunctionType() - ? core::SymInfo::Type::Function - : core::SymInfo::Type::Pointer; - else - hint.type = integerType(type) ? core::SymInfo::Type::Int - : core::SymInfo::Type::Unknown; - hint.ctype = typeHandle(type); - for (const core::Target &target : address.targets) { - heap.ensure(state, target.object); - std::optional key = cellKeyOf(target.offset); - core::Sym value = core::ZeroSym; - if (!key) { - // An unknown offset: any cell of the object. - value = unknownValue(type); - const core::ObjectState *object = heap.findObject(state, target.object); - std::vector cells; - if (object != nullptr) { - for (const auto &[cellKey, sym] : object->cells) - if (heap.info(state, sym).type == hint.type) - cells.push_back(sym); - for (const core::Segment &segment : object->segments) - if (heap.info(state, segment.value).type == hint.type) - cells.push_back(segment.value); - } - for (core::Sym cell : cells) - value = heap.mergeWeak(state, value, cell); - } else { - // A cell, or an element by the element facts (§4.2 *Amendment - // (arrays)*). - value = heap.load(state, target.object, *key, hint); - } - result = - result == core::ZeroSym ? value : heap.mergeWeak(state, result, value); - } - // Complementary objects: the value an earlier load read from the same - // two cells, so that what the path learnt of it still holds. - if (complementary(address)) { - auto first = cellKeyOf(address.targets[0].offset); - auto second = cellKeyOf(address.targets[1].offset); - auto firstValue = first - ? heap.read(state, address.targets[0].object, *first) - : std::nullopt; - auto secondValue = - second ? heap.read(state, address.targets[1].object, *second) - : std::nullopt; - if (firstValue && secondValue && first->isConcrete() && - second->isConcrete()) { - core::MergedLoad entry{.first = address.targets[0].object, - .firstKey = *first, - .firstValue = *firstValue, - .second = address.targets[1].object, - .secondKey = *second, - .secondValue = *secondValue, - .merged = core::ZeroSym}; - bool found = false; - for (const core::MergedLoad &known : state.mergedLoads) - if (known.first == entry.first && known.firstKey == entry.firstKey && - known.firstValue == entry.firstValue && - known.second == entry.second && - known.secondKey == entry.secondKey && - known.secondValue == entry.secondValue && - state.syms.contains(known.merged)) { - result = known.merged; - found = true; - break; - } - if (!found) { - entry.merged = result; - state.mergedLoads.push_back(entry); - } - } - } - // RFC 0004: a pointer loaded through a raw pointer is raw. - if (address.base != core::ZeroSym && heap.info(state, address.base).raw && - heap.info(state, result).type == core::SymInfo::Type::Pointer && - !heap.info(state, result).raw) { - core::SymInfo copy = heap.info(state, result); - copy.raw = true; - copy.rawAt = at != nullptr ? toCoreLocation(context.getSourceManager(), - at->getBeginLoc()) - : heap.info(state, address.base).rawAt; - copy.rawOrigin = core::SymInfo::RawOrigin::Loaded; - copy.rawFrom = heap.info(state, address.base).name; - copy.rawSome = heap.info(state, address.base).rawSome; - result = heap.fresh(state, copy); - } - // Reinterpretation (§4.2): a pointer read from a cell that holds an - // integer, and the reverse. - const core::SymInfo &loaded = heap.info(state, result); - if (hint.type == core::SymInfo::Type::Pointer && - loaded.type == core::SymInfo::Type::Int) { - // (Even the bits of a pointer, stored as an integer: RFC 0030 §2.3.) - if (auto c = state.zone.constant(result); c && *c == 0) - return nullPointer(type); - core::Sym raw = unknownValue(type); - heap.infoMut(state, raw).rawCast = true; - return raw; - } - if (hint.type == core::SymInfo::Type::Int && - loaded.type == core::SymInfo::Type::Pointer) { - core::Sym number = unknownValue(type); - heap.infoMut(state, number).pointerBehind = result; - return number; - } - if (loaded.type == core::SymInfo::Type::Unknown && - hint.type != core::SymInfo::Type::Unknown) - return unknownValue(type); - return result; -} - -void Transfer::store(const Address &address, core::Sym value, QualType type, - const Expr *at, const std::string &holderName) { - if (address.top) { - // A store through a pointer the engine cannot follow: every object that - // could be the target forgets its cells. - std::vector ids; - for (const auto &[id, object] : state.objects) { - core::ObjectKind kind = run.table().info(id).key.kind; - if (kind != core::ObjectKind::Local || object.escaped) - ids.push_back(id); - } - for (core::ObjectId id : ids) { - heap.forgetCells(state, id, 0, std::nullopt); - state.objects.at(id).havocked = true; - } - return; - } - bool strong = address.targets.size() == 1 && - run.table().info(address.targets[0].object).singular; - // Two objects of which each path has exactly one: the caller's object a - // cell's entry value points to, and the one made where that value was - // null (`if (!g) g = malloc(…); g->data = …`). The store is to the one - // that exists, strongly. - if (complementary(address)) - strong = true; - core::Sym overwritten = core::ZeroSym; - for (const core::Target &target : address.targets) { - heap.ensure(state, target.object); - std::optional key = cellKeyOf(target.offset); - if (!key) { - // An unknown offset: every cell may be the one written. - std::vector keys; - for (const auto &[cellKey, sym] : state.objects.at(target.object).cells) - keys.push_back(cellKey); - for (const core::CellKey &cellKey : keys) - heap.write(state, target.object, cellKey, value, true); - // Every range may hold it too. - std::vector segments = - state.objects.at(target.object).segments; - for (core::Segment &segment : segments) - segment.value = heap.mergeWeak(state, segment.value, value); - state.objects.at(target.object).segments = std::move(segments); - state.objects.at(target.object).havocked = - state.objects.at(target.object).havocked || keys.empty(); - state.objects.at(target.object).nulWithin.reset(); - state.objects.at(target.object).nulFrom.reset(); - continue; - } - // RFC 0012: the store may overwrite the object's terminator. - if (auto width = sizeOf(type)) - stringStored(target.object, target.offset, *width, value); - else - stringStored(target.object, target.offset, 1U << 20U, value); - bool weak = !strong || key->isSummary(); - run.writtenCells.insert({target.object, *key}); - if (weak && !heap.read(state, target.object, *key)) { - core::SymInfo hint = heap.info(state, value); - run.unwritten(state, target.object, *key, hint); - } - if (!weak) - if (auto old = heap.read(state, target.object, *key)) - overwritten = *old; - heap.write(state, target.object, *key, value, weak); - if (run.isPublishing()) - recordFrameStore(target.object, *key, value, at, holderName); - // Messages name an allocation after where it was last stored. - if (at != nullptr || !holderName.empty()) { - const core::SymInfo &stored = heap.info(state, value); - std::string holder = !holderName.empty() ? holderName : std::string(); - if (holder.empty()) - if (const auto *assign = dyn_cast_or_null(at)) - holder = spell(*assign->getLHS()); - if (!holder.empty()) - for (const core::Target &t : stored.targets) - if (state.objects.contains(t.object)) - state.objects.at(t.object).holder = holder; - } - // A pointer stored into an object that already escaped escapes too; - // one stored in the caller's memory or a global is reachable from the - // leak roots, and escapes at the next call (EngineCalls.cpp). - bool visible = state.objects.at(target.object).escaped; - if (visible) { - const core::SymInfo &stored = heap.info(state, value); - std::vector targets; - targets.reserve(stored.targets.size()); - for (const core::Target &t : stored.targets) - targets.push_back(t.object); - for (core::ObjectId id : targets) - if (state.objects.contains(id)) - state.objects.at(id).escaped = true; - } - } - if (overwritten != core::ZeroSym && at != nullptr) - checkLeaks(overwritten, *at); -} - -std::vector Transfer::pairGuard() const { - std::vector facts; - const FunctionDecl &function = run.decl(); - std::vector> held; - for (unsigned i = 0; i < function.getNumParams(); ++i) { - const ParmVarDecl *param = function.getParamDecl(i); - if (!run.isUnmodifiedParam(i) || !param->getType()->isPointerType()) - continue; - if (auto sym = - heap.read(state, run.variableObject(*param), core::CellKey{})) - held.emplace_back(i, *sym); - } - for (std::size_t a = 0; a < held.size(); ++a) - for (std::size_t b = a + 1; b < held.size(); ++b) - if (auto equal = - core::Heap::pointersEqual(state, held[a].second, held[b].second)) - facts.push_back(core::ParamPairTest{ - .first = held[a].first, .second = held[b].first, .equal = *equal}); - return facts; -} - -std::optional -Transfer::argumentsEqual(const std::vector &args, - const core::ParamPairTest &test) const { - if (test.first >= args.size() || test.second >= args.size()) - return std::nullopt; - core::Sym x = args[test.first]; - core::Sym y = args[test.second]; - if (auto known = core::Heap::pointersEqual(state, x, y)) - return known; - const core::SymInfo &a = heap.info(state, x); - const core::SymInfo &b = heap.info(state, y); - if (a.type != core::SymInfo::Type::Pointer || - b.type != core::SymInfo::Type::Pointer || a.top || b.top) - return std::nullopt; - if (a.null == core::PointerNull::Null && b.null == core::PointerNull::Null) - return true; - if ((a.null == core::PointerNull::Null && - b.null == core::PointerNull::NonNull) || - (b.null == core::PointerNull::Null && - a.null == core::PointerNull::NonNull)) - return false; - if (a.null != core::PointerNull::NonNull || - b.null != core::PointerNull::NonNull || a.targets.empty() || - b.targets.empty()) - return std::nullopt; - // Into objects that cannot overlap. - for (const core::Target &s : a.targets) - for (const core::Target &t : b.targets) - if (heap.mayOverlap(state, s.object, t.object)) - return std::nullopt; - return false; -} - -std::optional isZeroValue(const core::Heap &heap, - const core::HeapState &state, core::Sym value) { - const core::SymInfo &info = heap.info(state, value); - if (info.type == core::SymInfo::Type::Pointer) { - // A null test of a pointer parameter (`if (b == NULL)`) is its `=0`. - if (info.null == core::PointerNull::Null) - return true; - if (info.null == core::PointerNull::NonNull) - return false; - return std::nullopt; - } - auto lo = state.zone.lower(value); - auto hi = state.zone.upper(value); - if (lo && hi && *lo == 0 && *hi == 0) - return true; - if ((lo && *lo > 0) || (hi && *hi < 0) || info.nonZero) - return false; - return std::nullopt; -} - -std::vector Transfer::nonNullLocals() const { - std::vector held; - for (const VarDecl *var : run.fixedPointerLocals()) { - const core::ObjectState *holder = - heap.findObject(state, run.variableObject(*var)); - const core::Sym *value = - holder != nullptr ? holder->cells.find(core::CellKey{}) : nullptr; - if (value != nullptr && - heap.info(state, *value).null == core::PointerNull::NonNull) - held.push_back(handleOf(var)); - } - std::ranges::sort(held); - return held; -} - -std::vector> Transfer::paramGuard() const { - std::vector> facts; - const FunctionDecl &function = run.decl(); - for (unsigned i = 0; i < function.getNumParams(); ++i) { - const ParmVarDecl *param = function.getParamDecl(i); - if (!run.isUnmodifiedParam(i) || !param->getType()->isIntegerType()) - continue; - auto held = heap.read(state, run.variableObject(*param), core::CellKey{}); - if (!held) - continue; - if (auto zero = isZeroValue(heap, state, *held)) - facts.emplace_back(i, *zero); - } - return facts; -} - -void Transfer::checkLeaks(core::Sym overwritten, const Stmt &at) { - const core::SymInfo &old = heap.info(state, overwritten); - if (old.type != core::SymInfo::Type::Pointer || old.targets.empty()) - return; - std::vector owned; - for (const core::Target &target : old.targets) { - const core::ObjectState *object = heap.findObject(state, target.object); - core::ObjectKind kind = run.table().info(target.object).key.kind; - if (object != nullptr && object->owned && !object->escaped && - object->life == core::Life::Live && - object->family != core::StackFamily && - (kind == core::ObjectKind::HeapRecent || - kind == core::ObjectKind::HeapOld)) - owned.push_back(target.object); - } - if (owned.empty() || !run.isPublishing()) - return; - std::vector reachable = - heap.reachableObjects(state, run.roots(state)); - for (core::ObjectId id : owned) { - if (std::ranges::find(reachable, id) != reachable.end()) - continue; - std::string name = state.objects.at(id).holder; - if (name.empty()) - name = !old.name.empty() ? old.name : run.table().info(id).name; - core::Diagnostic diagnostic; - diagnostic.id = core::diag::Leak; - diagnostic.severity = core::Severity::Warning; - diagnostic.message = - "'" + name + "' is leaked: it is overwritten without being released"; - diagnostic.location = - toCoreLocation(context.getSourceManager(), at.getBeginLoc()); - run.report(std::move(diagnostic), core::Certainty::Possible, &at, - std::nullopt); - // Reported once: the object is no longer owned here. - state.objects.at(id).owned = false; - } -} - -//===----------------------------------------------------------------------===// -// Declarations and scopes -//===----------------------------------------------------------------------===// - -void Transfer::zeroFill(const Address &address, QualType type) { - (void)type; - for (const core::Target &target : address.targets) { - heap.ensure(state, target.object); - core::ObjectState &object = state.objects.at(target.object); - if (target.offset.isConstant() && target.offset.constant == 0) { - object.cells = {}; - object.zeroed = true; - object.uninitialised = false; - } - } -} - -void Transfer::initialize(const Address &address, QualType type, - const Expr *init) { - if (init == nullptr) - return; - init = init->IgnoreParens(); - if (const auto *list = dyn_cast(init)) { - if (list->isSyntacticForm() && list->getSemanticForm() != nullptr) - list = list->getSemanticForm(); - QualType canonical = type.getCanonicalType(); - // Everything not named is zero. - zeroFill(address, type); - if (list->isStringLiteralInit() && list->getNumInits() == 1) { - initialize(address, type, list->getInit(0)); - return; - } - if (const RecordDecl *record = canonical->getAsRecordDecl()) { - if (!record->isCompleteDefinition()) - return; - const ASTRecordLayout &layout = context.getASTRecordLayout(record); - unsigned index = 0; - for (const FieldDecl *field : record->fields()) { - if (record->isUnion() && list->getInitializedFieldInUnion() != field) - continue; - if (index >= list->getNumInits()) - break; - const Expr *fieldInit = list->getInit(record->isUnion() ? 0 : index); - ++index; - auto offset = static_cast( - layout.getFieldOffset(field->getFieldIndex()) / - context.getCharWidth()); - Address fieldAddress = address; - for (core::Target &target : fieldAddress.targets) - target.offset = target.offset.plusConstant(offset); - if (field->isBitField()) { - if (fieldInit != nullptr && !isa(fieldInit)) - (void)storeBitField(fieldAddress, valueOf(*fieldInit), - field->getType(), *field, fieldInit); - continue; - } - initialize(fieldAddress, field->getType(), fieldInit); - } - return; - } - if (const auto *array = context.getAsArrayType(canonical)) { - auto size = sizeOf(array->getElementType()); - if (!size) - return; - for (unsigned i = 0; i < list->getNumInits() && i < 64; ++i) { - Address element = address; - for (core::Target &target : element.targets) - target.offset = target.offset.plusConstant(*size * i); - initialize(element, array->getElementType(), list->getInit(i)); - } - return; - } - if (list->getNumInits() == 1) - initialize(address, type, list->getInit(0)); - return; - } - if (isa(init)) { - if (type->isScalarType()) - store(address, constant(0, type), type, nullptr); - return; - } - if (const auto *literal = dyn_cast(init)) { - // A character array initialised by a literal holds its characters, then - // zeros to its end; an array no longer than the literal's characters - // has no terminator (RFC 0012). - auto size = sizeOf(type); - QualType element = context.getAsArrayType(type) != nullptr - ? context.getAsArrayType(type)->getElementType() - : context.CharTy; - for (const core::Target &target : address.targets) { - heap.ensure(state, target.object); - core::ObjectState &object = state.objects.at(target.object); - object.cells = {}; - object.uninitialised = false; - if (literal->getCharByteWidth() != 1 || !size || - !target.offset.isConstant()) { - object.havocked = true; - continue; - } - llvm::StringRef bytes = literal->getBytes(); - auto count = std::min( - static_cast(bytes.size()), *size); - if (count > 64) { - // Too long to keep byte by byte: the string fact only. - object.havocked = true; - std::size_t nul = bytes.find('\0'); - if (nul == llvm::StringRef::npos && count < *size) - nul = static_cast(count); - if (nul != llvm::StringRef::npos && std::cmp_less(nul, *size)) { - object.nulWithin = - target.offset.plusConstant(static_cast(nul)); - object.nulFrom = target.offset; - } - continue; - } - object.zeroed = true; - object.havocked = false; - object.forgotten.clear(); - object.mayForgotten.clear(); - std::vector chars; - chars.reserve(static_cast(std::max(count, 0))); - for (std::int64_t i = 0; i < count; ++i) - chars.push_back( - constant(element->isUnsignedIntegerType() - ? static_cast(static_cast( - bytes[static_cast(i)])) - : static_cast(static_cast( - bytes[static_cast(i)])), - element)); - for (std::int64_t i = 0; i < count; ++i) - heap.write(state, target.object, - core::CellKey{.offset = target.offset.constant + i}, - chars[static_cast(i)], false); - } - return; - } - QualType canonical = type.getCanonicalType(); - if (canonical->isRecordType()) { - // A record copied from another: the cells follow (a whole copy). - const Expr *copied = init; - if (const auto *cast = dyn_cast(copied); - cast != nullptr && cast->getCastKind() == CK_LValueToRValue) - copied = cast->getSubExpr(); - if (copied->isGLValue() && copyRecord(address, addressOf(*copied), type)) - return; - ExprResult source = evaluate(*init); - if (source.address && source.address->targets.size() == 1 && - address.targets.size() == 1) { - core::ObjectId from = source.address->targets[0].object; - core::ObjectId to = address.targets[0].object; - if (source.address->targets[0].offset.isConstant() && - address.targets[0].offset.isConstant() && - address.targets[0].offset.constant == 0 && - source.address->targets[0].offset.constant == 0 && - state.objects.contains(from)) { - const core::ObjectState original = state.objects.at(from); - core::ObjectState © = heap.ensure(state, to); - copy.cells = original.cells; - // What the source's unwritten cells read as (§4.2). - copy.havocked = original.havocked; - copy.forgotten = original.forgotten; - copy.mayForgotten = original.mayForgotten; - copy.zeroed = original.zeroed; - copy.uninitialised = original.uninitialised; - // Messages name an allocation after the member now holding it - // (`'p.a' is leaked`). - nameHeldByMembers(to, type, run.table().info(to).name); - return; - } - } - for (const core::Target &target : address.targets) { - heap.ensure(state, target.object); - state.objects.at(target.object).cells = {}; - state.objects.at(target.object).havocked = true; - } - return; - } - std::string holder; - if (address.targets.size() == 1) - holder = run.table().info(address.targets[0].object).name; - store(address, valueOf(*init), type, init, holder); -} - -void Transfer::nameHeldByMembers(core::ObjectId object, QualType type, - const std::string &prefix) { - const RecordDecl *record = type->getAsRecordDecl(); - const core::ObjectState *holder = heap.findObject(state, object); - if (record == nullptr || !record->isCompleteDefinition() || - holder == nullptr || prefix.empty()) - return; - const ASTRecordLayout &layout = context.getASTRecordLayout(record); - std::vector> named; - for (const FieldDecl *field : record->fields()) { - if (!field->getType()->isPointerType() || field->getName().empty()) - continue; - auto offset = static_cast( - layout.getFieldOffset(field->getFieldIndex()) / context.getCharWidth()); - const core::Sym *held = holder->cells.find(core::CellKey{.offset = offset}); - if (held == nullptr) - continue; - for (const core::Target &target : heap.info(state, *held).targets) - named.emplace_back(target.object, - prefix + "." + field->getNameAsString()); - } - for (const auto &[id, name] : named) - if (state.objects.contains(id) && state.objects.at(id).owned) - state.objects.at(id).holder = name; -} - -void Transfer::declare(const VarDecl &var) { - core::ObjectId object = run.variableObject(var); - if (var.hasGlobalStorage()) { - heap.ensure(state, object); - return; - } - // A fresh activation of the variable. - core::ObjectState fresh; - fresh.uninitialised = true; - QualType type = var.getType(); - if (const auto *vla = context.getAsVariableArrayType(type)) { - if (auto bytes = bytesOf(type, *vla->getSizeExpr())) - fresh.extent = core::Extent{.bytes = termOf(*bytes), - .cls = core::ExtentClass::Exact}; - } else if (auto size = sizeOf(type)) { - fresh.extent = core::Extent{.bytes = core::Term::of(*size), - .cls = core::ExtentClass::Exact}; - } - state.objects.set(object, fresh); - if (const Expr *init = var.getInit()) { - Address address; - address.targets = {core::Target{.object = object}}; - initialize(address, type, init); - if (type->isArrayType()) - run.byteWrites[object] = - toCoreLocation(context.getSourceManager(), var.getLocation()); - // RFC 0004: a declaration with a safe kind asserts it of its value. - if (type->isPointerType() && !isa(init->IgnoreParens())) - if (auto held = heap.read(state, object, core::CellKey{})) { - core::Sym laundered = - launder(*held, getAnnotations(var), var.getNameAsString(), *init); - if (laundered != *held) - heap.write(state, object, core::CellKey{}, laundered, false); - } - } -} - -void Transfer::lifetimeEnds(const VarDecl &var) { - if (var.hasGlobalStorage()) - return; - core::ObjectId object = run.variableObject(var); - if (!state.objects.contains(object)) - return; - // (The record a `return` hands back keeps what it holds: the caller - // receives it.) - if (const core::Sym *returned = state.exprs.find(handleOf(&run.decl()))) - for (const core::Target &target : heap.info(state, *returned).targets) - if (target.object == object) { - state.objects.at(object).life = core::Life::Ended; - return; - } - core::ObjectState &ended = state.objects.at(object); - ended.life = core::Life::Ended; - ended.cells = {}; -} - -void Transfer::returned(const ReturnStmt &ret) { - if (const Expr *value = ret.getRetValue()) { - QualType type = value->getType(); - if (type->isRecordType()) { - state.result = core::ZeroSym; - // RFC 0013: where the returned record is, for the summary's - // `result.f` stores (EngineSummary.cpp). - const Expr *returnedRecord = value->IgnoreParens(); - if (const auto *cast = dyn_cast(returnedRecord); - cast != nullptr && cast->getCastKind() == CK_LValueToRValue) - returnedRecord = cast->getSubExpr(); - std::optional
storage; - if (returnedRecord->isGLValue()) - storage = addressOf(*returnedRecord); - else - storage = evaluate(*returnedRecord).address; - if (storage && !storage->top && storage->targets.size() == 1) - state.exprs.set(handleOf(&run.decl()), - pointerTo(*storage, type, std::string())); - } else { - state.result = valueOf(*value); - } - // RFC 0004: returning a raw value from a function whose result is - // declared with a safe kind asserts that kind. - if (type->isPointerType()) { - AnnotationSet declared; - for (const FunctionDecl *redecl : run.decl().redecls()) { - AnnotationSet set = getAnnotations(*redecl); - declared.owned = declared.owned || set.owned; - declared.borrowed = declared.borrowed || set.borrowed; - declared.mutBorrowed = declared.mutBorrowed || set.mutBorrowed; - } - state.result = launder(state.result, declared, std::string(), *value); - } - if (state.result != core::ZeroSym) - checkReturnAnnotation(state.result, *value); - } - if (run.isPublishing()) { - decideSites(ret); - exitLifetimes(ret, &ret); - } - run.checkLeaks(state, ret, FunctionRun::LeakPoint::Exit); - run.noteExit(state); -} - -//===----------------------------------------------------------------------===// -// Evaluation -//===----------------------------------------------------------------------===// - -core::Sym Transfer::pointerAdd(core::Sym pointer, core::Sym index, - std::int64_t size, bool subtract, - const Expr &at) { - const core::SymInfo base = heap.info(state, pointer); - core::SymInfo out = base; - out.entryOf.reset(); - out.name = spell(at); - out.pending.clear(); - out.condition.reset(); - out.linear.reset(); - out.derived = true; - // (Non-zero arithmetic on null is undefined: the result is null exactly - // when the pointer is, and follows it when a check or a test decides it.) - auto follow = [&](core::Sym result) { - core::Sym root = pointer; - for (const auto &[derived, from] : state.nullFollows) - if (derived == pointer) { - root = from; - break; - } - state.nullFollows.emplace_back(result, root); - return result; - }; - // A non-zero amount makes a non-null pointer: were the pointer null, the - // arithmetic would be undefined, and its result is not the null pointer - // (a check of it could not fail). - const auto lowest = state.zone.lower(index); - const auto highest = state.zone.upper(index); - const bool nonZero = - size != 0 && ((lowest && *lowest > 0) || (highest && *highest < 0)); - if (base.type != core::SymInfo::Type::Pointer) { - out.type = core::SymInfo::Type::Pointer; - out.targets.clear(); - core::ObjectId any = run.unknownObject(); - heap.ensure(state, any); - out.targets = {core::Target{.object = any}}; - out.null = nonZero ? core::PointerNull::NonNull : core::PointerNull::Maybe; - core::Sym result = heap.fresh(state, out); - return nonZero ? result : follow(result); - } - if (nonZero && out.null == core::PointerNull::Maybe) - out.null = core::PointerNull::NonNull; - core::Term step = termOf(index); - if (step.known) { - step.scale *= subtract ? -size : size; - step.constant *= subtract ? -size : size; - if (step.isConstant()) - step.scale = 0; - } - for (core::Target &target : out.targets) { - auto sum = step.known ? target.offset.plus(step) : std::nullopt; - target.offset = sum && sum->known ? *sum : core::Term::unknown(); - } - // RFC 0015 array storage (§4.2 *Amendment (arrays)*): an index that is - // not a constant expression makes the object's cells of this size - // elements, which fold into ranges at loop heads. - const Expr *indexExpr = nullptr; - if (const auto *subscript = dyn_cast(&at)) - indexExpr = subscript->getIdx(); - else if (const auto *binary = dyn_cast(&at)) - indexExpr = binary->getLHS()->getType()->isIntegerType() ? binary->getLHS() - : binary->getRHS(); - bool variable = isa(at) || - (indexExpr != nullptr && !indexExpr->isValueDependent() && - !indexExpr->isIntegerConstantExpr(context)); - if (variable && size > 0 && std::cmp_less_equal(size, 1U << 20U)) - for (const core::Target &target : base.targets) - if (state.objects.contains(target.object) && - state.objects.at(target.object).stride == 0) - state.objects.at(target.object).stride = - static_cast(size); - // The result keeps the pointer's nullness. - core::Sym result = heap.fresh(state, out); - return out.null == core::PointerNull::Maybe ? follow(result) : result; -} - -ExprResult Transfer::evaluateUncached(const Expr &expr) { - ExprResult result; - switch (expr.getStmtClass()) { - case Stmt::ParenExprClass: - return evaluate(*cast(expr).getSubExpr()); - case Stmt::ConstantExprClass: - case Stmt::ExprWithCleanupsClass: - return evaluate(*cast(expr).getSubExpr()); - // A record a call returned, named by a member access (`f().x`): the - // call's temporary, where its summary put the fields. - case Stmt::MaterializeTemporaryExprClass: - return evaluate(*cast(expr).getSubExpr()); - case Stmt::IntegerLiteralClass: { - const auto &literal = cast(expr); - result.value = constant( - llvm::APSInt(literal.getValue(), /*isUnsigned=*/true), expr.getType()); - return result; - } - case Stmt::CharacterLiteralClass: - result.value = - constant(cast(expr).getValue(), expr.getType()); - return result; - case Stmt::StringLiteralClass: - case Stmt::PredefinedExprClass: { - core::ObjectId object = run.literalObject(expr); - core::ObjectState &literal = heap.ensure(state, object); - literal.readonly = true; - // Its bytes are the literal's (EngineStrings.cpp, FunctionRun::unwritten). - if (auto size = sizeOf(expr.getType())) - literal.extent = core::Extent{.bytes = core::Term::of(*size), - .cls = core::ExtentClass::Exact}; - Address address; - address.targets = {core::Target{.object = object}}; - result.address = address; - return result; - } - case Stmt::DeclRefExprClass: { - const auto &ref = cast(expr); - const ValueDecl *decl = ref.getDecl(); - if (const auto *var = dyn_cast(decl)) { - core::ObjectId object = run.variableObject(*var); - core::ObjectState &variable = heap.ensure(state, object); - if (!variable.extent) - if (auto size = sizeOf(var->getType())) - variable.extent = core::Extent{.bytes = core::Term::of(*size), - .cls = core::ExtentClass::Exact}; - Address address; - address.targets = {core::Target{.object = object}}; - result.address = address; - return result; - } - if (const auto *enumerator = dyn_cast(decl)) { - llvm::APSInt value = enumerator->getInitVal(); - result.value = value.getSignificantBits() <= 64 - ? constant(value.getExtValue(), expr.getType()) - : unknownValue(expr.getType()); - return result; - } - if (const auto *fn = dyn_cast(decl)) { - core::SymInfo info; - info.type = core::SymInfo::Type::Function; - info.functionsKnown = true; - info.functions = {handleOf(fn->getCanonicalDecl())}; - info.name = fn->getNameAsString(); - result.value = heap.fresh(state, info); - return result; - } - result.value = unknownValue(expr.getType()); - return result; - } - case Stmt::MemberExprClass: { - const auto &member = cast(expr); - const auto *field = dyn_cast(member.getMemberDecl()); - std::int64_t offset = 0; - if (field != nullptr && field->getParent()->isCompleteDefinition()) { - const ASTRecordLayout &layout = - context.getASTRecordLayout(field->getParent()); - offset = static_cast( - layout.getFieldOffset(field->getFieldIndex()) / - context.getCharWidth()); - } - Address address; - if (member.isArrow()) { - core::Sym base = valueOf(*member.getBase()); - const core::SymInfo &info = heap.info(state, base); - address.base = base; - address.top = info.top; - address.targets = info.targets; - if (info.type != core::SymInfo::Type::Pointer) - address.top = true; - } else { - address = addressOf(*member.getBase()); - } - for (core::Target &target : address.targets) - target.offset = target.offset.plusConstant(offset); - result.address = address; - return result; - } - case Stmt::ArraySubscriptExprClass: { - const auto &subscript = cast(expr); - core::Sym base = valueOf(*subscript.getBase()); - core::Sym index = valueOf(*subscript.getIdx()); - std::int64_t size = sizeOf(expr.getType()).value_or(1); - core::Sym element = pointerAdd(base, index, size, false, expr); - const core::SymInfo &info = heap.info(state, element); - Address address; - address.base = base; - address.targets = info.targets; - address.top = info.top || info.type != core::SymInfo::Type::Pointer; - result.address = address; - return result; - } - case Stmt::UnaryOperatorClass: { - const auto &unary = cast(expr); - if (unary.getOpcode() == UO_Deref) { - core::Sym base = valueOf(*unary.getSubExpr()); - const core::SymInfo &info = heap.info(state, base); - Address address; - address.base = base; - address.targets = info.targets; - address.top = info.top || info.type != core::SymInfo::Type::Pointer; - if (info.type == core::SymInfo::Type::Function) - address.top = false; - result.address = address; - if (info.type == core::SymInfo::Type::Function) - result.value = base; - return result; - } - if (unary.getOpcode() == UO_AddrOf) { - const Expr *sub = unary.getSubExpr(); - if (sub->getType()->isFunctionType()) { - result.value = valueOf(*sub); - return result; - } - Address address = addressOf(*sub); - result.value = pointerTo(address, sub->getType(), spell(*sub)); - return result; - } - result.value = evaluateUnary(unary); - return result; - } - case Stmt::BinaryOperatorClass: - case Stmt::CompoundAssignOperatorClass: - result.value = evaluateBinary(cast(expr)); - if (result.value == core::ZeroSym) - result.value = unknownValue(expr.getType()); - return result; - case Stmt::ImplicitCastExprClass: - case Stmt::CStyleCastExprClass: { - const auto &castExpr = cast(expr); - if (castExpr.getCastKind() == CK_LValueBitCast || - castExpr.getCastKind() == CK_LValueToRValueBitCast) { - result.address = addressOf(*castExpr.getSubExpr()); - return result; - } - if (castExpr.getCastKind() == CK_NoOp && expr.isGLValue()) { - return evaluate(*castExpr.getSubExpr()); - } - result.value = evaluateCast(castExpr); - return result; - } - case Stmt::ConditionalOperatorClass: - case Stmt::BinaryConditionalOperatorClass: { - // The arms recorded their values under the operator (§2). - if (const core::Sym *value = state.exprs.find(handleOf(&expr))) { - result.value = *value; - return result; - } - const auto &conditional = cast(expr); - auto armValue = [&](const Expr *arm) -> core::Sym { - if (auto it = memo.find(arm); it != memo.end()) - return it->second.value; - return core::ZeroSym; - }; - core::Sym a = armValue(conditional.getTrueExpr()); - core::Sym b = armValue(conditional.getFalseExpr()); - if (a != core::ZeroSym && b != core::ZeroSym) - result.value = heap.mergeWeak(state, a, b); - else if (a != core::ZeroSym || b != core::ZeroSym) - result.value = a != core::ZeroSym ? a : b; - else - result.value = unknownValue(expr.getType()); - return result; - } - case Stmt::CallExprClass: { - // RFC 0013: a record returned by value lands in a temporary of the - // caller, which the callee's summary describes (`result.f`) or, for a - // callee the analysis does not follow, holds unknown values. - std::optional
temporary; - if (expr.getType()->isRecordType()) { - core::ObjectId object = run.recordResultObject(cast(expr)); - core::ObjectState fresh; - fresh.havocked = true; - if (auto size = sizeOf(expr.getType())) - fresh.extent = core::Extent{.bytes = core::Term::of(*size), - .cls = core::ExtentClass::Exact}; - state.objects.set(object, fresh); - temporary = Address{}; - temporary->targets = {core::Target{.object = object}}; - } - result.value = call(cast(expr)); - if (temporary) - result.address = std::move(temporary); - return result; - } - case Stmt::UnaryExprOrTypeTraitExprClass: { - const auto &trait = cast(expr); - if (trait.getKind() == UETT_SizeOf) { - QualType argument = trait.getTypeOfArgument(); - if (argument->isVariablyModifiedType()) { - // `sizeof vla`: the dimensions captured at its declaration. - if (auto bytes = bytesOf(argument, expr)) { - result.value = *bytes; - return result; - } - result.value = unknownValue(expr.getType()); - return result; - } - } - Expr::EvalResult value; - if (expr.EvaluateAsInt(value, context)) - result.value = constant(value.Val.getInt().getExtValue(), expr.getType()); - else - result.value = unknownValue(expr.getType()); - return result; - } - case Stmt::OffsetOfExprClass: { - Expr::EvalResult value; - if (expr.EvaluateAsInt(value, context)) - result.value = constant(value.Val.getInt().getExtValue(), expr.getType()); - else - result.value = unknownValue(expr.getType()); - return result; - } - case Stmt::CompoundLiteralExprClass: { - const auto &literal = cast(expr); - core::ObjectId object = run.literalObject(expr); - core::ObjectState fresh; - if (auto size = sizeOf(expr.getType())) - fresh.extent = core::Extent{.bytes = core::Term::of(*size), - .cls = core::ExtentClass::Exact}; - state.objects.set(object, fresh); - Address address; - address.targets = {core::Target{.object = object}}; - initialize(address, expr.getType(), literal.getInitializer()); - result.address = address; - return result; - } - case Stmt::ImplicitValueInitExprClass: - result.value = expr.getType()->isPointerType() - ? nullPointer(expr.getType()) - : constant(0, expr.getType()); - return result; - case Stmt::StmtExprClass: { - const auto &statement = cast(expr); - const CompoundStmt *body = statement.getSubStmt(); - if (body != nullptr && !body->body_empty()) - if (const auto *last = dyn_cast(body->body_back())) { - result.value = valueOf(*last); - return result; - } - result.value = unknownValue(expr.getType()); - return result; - } - case Stmt::ChooseExprClass: - return evaluate(*cast(expr).getChosenSubExpr()); - case Stmt::GenericSelectionExprClass: - return evaluate(*cast(expr).getResultExpr()); - case Stmt::OpaqueValueExprClass: - if (const Expr *source = cast(expr).getSourceExpr()) - return evaluate(*source); - result.value = unknownValue(expr.getType()); - return result; - case Stmt::VAArgExprClass: - result.value = unknownValue(expr.getType()); - if (expr.getType()->isPointerType()) - heap.infoMut(state, result.value).rawCast = true; - return result; - case Stmt::InitListExprClass: - result.value = unknownValue(expr.getType()); - return result; - case Stmt::AtomicExprClass: { - // RFC 0031 §5.1: an atomic operation's value is unknown, and what it - // may write through its pointer operands (every one but the location a - // plain load reads) holds unknown bytes after it. - const auto &atomic = cast(expr); - const bool load = atomic.getOp() == AtomicExpr::AO__atomic_load_n || - atomic.getOp() == AtomicExpr::AO__c11_atomic_load; - std::vector> operands; - for (unsigned i = 0; i < atomic.getNumSubExprs(); ++i) - if (const Expr *operand = atomic.getSubExprs()[i]) - operands.emplace_back(operand, valueOf(*operand)); - for (const auto &[operand, value] : operands) { - QualType type = operand->getType(); - if (!type->isPointerType() || type->getPointeeType().isConstQualified() || - (load && operand == atomic.getPtr())) - continue; - const core::SymInfo info = heap.info(state, value); - if (info.type != core::SymInfo::Type::Pointer) - continue; - auto bytes = sizeOf(type->getPointeeType()); - for (const core::Target &target : info.targets) { - heap.ensure(state, target.object); - if (bytes && target.offset.isConstant()) - heap.forgetCells(state, target.object, target.offset.constant, bytes); - else - heap.forgetCells(state, target.object, 0, std::nullopt); - state.objects.at(target.object).stored = true; - } - } - result.value = unknownValue(expr.getType()); - return result; - } - default: - result.value = unknownValue(expr.getType()); - return result; - } -} - -void Transfer::uninitialisedRead(core::Sym value, const Expr &lvalue, - const Address &address) { - // RFC 0008, RFC 0031 §5.9: reading a local pointer that no path assigned - // (to copy, pass, release or dereference it) is the error, reported where - // it is read and once per value: a copy reports at the copy. - if (!run.isPublishing() || address.top || address.targets.empty() || - run.isDiscarded(lvalue)) - return; - const core::SymInfo &info = heap.info(state, value); - if (info.type != core::SymInfo::Type::Pointer || !info.uninit || - info.null != core::PointerNull::Null) - return; - for (const core::Target &target : address.targets) - if (run.table().info(target.object).key.kind != core::ObjectKind::Local) - return; - if (!run.reportedUninit.insert(value).second) - return; - core::Diagnostic diagnostic; - diagnostic.id = core::diag::UseOfUninitialized; - diagnostic.severity = core::Severity::Error; - diagnostic.message = - "use of '" + spell(lvalue) + "' before it was initialized"; - diagnostic.location = - toCoreLocation(context.getSourceManager(), lvalue.getBeginLoc()); - const core::ObjectId object = address.targets.front().object; - const core::ObjectInfo &declared = run.table().info(object); - if (declared.created.isValid()) - diagnostic.addNote("'" + run.frameName(object) + "' is declared here", - declared.created); - run.report(std::move(diagnostic), core::Certainty::Definite, nullptr, - core::Facet::Null); -} - -core::Sym Transfer::evaluateCast(const CastExpr &castExpr) { - const Expr &sub = *castExpr.getSubExpr(); - QualType type = castExpr.getType(); - switch (castExpr.getCastKind()) { - case CK_LValueToRValue: { - ExprResult operand = evaluate(sub); - if (!operand.address) - return operand.value != core::ZeroSym ? operand.value - : unknownValue(type); - const FieldDecl *bitField = sub.getSourceBitField(); - core::Sym loaded = - bitField != nullptr - ? loadBitField(*operand.address, type, *bitField, &castExpr) - : load(*operand.address, type, &castExpr); - // RFC 0004: a place declared `WEAVEC_RAW` (a field, a local) holds raw - // pointers. - if (heap.info(state, loaded).type == core::SymInfo::Type::Pointer) { - const Expr *place = sub.IgnoreParenImpCasts(); - const Decl *declared = nullptr; - if (const auto *member = dyn_cast(place)) - declared = member->getMemberDecl(); - else if (const auto *ref = dyn_cast(place); - ref != nullptr && !isa(ref->getDecl())) - declared = ref->getDecl(); - if (declared != nullptr && getAnnotations(*declared).raw && - !heap.info(state, loaded).raw) { - core::SymInfo copy = heap.info(state, loaded); - copy.pending.clear(); - copy.entryOf.reset(); - copy.raw = true; - copy.rawOrigin = core::SymInfo::RawOrigin::Declared; - copy.rawAt = - toCoreLocation(context.getSourceManager(), declared->getLocation()); - loaded = heap.fresh(state, copy); - } - } - // §4.5 D6: a pointer loaded from an owning slot of the object `base` - // points to is distinct from that object (A3), whatever becomes of it. - if (const auto *member = dyn_cast(sub.IgnoreParenImpCasts()); - member != nullptr && member->isArrow() && - heap.info(state, loaded).type == core::SymInfo::Type::Pointer && - operand.address->base != core::ZeroSym && - run.unitRun().owningSlots.contains( - member->getMemberDecl()->getCanonicalDecl())) { - const core::Sym base = operand.address->base; - const std::vector above = heap.info(state, base).ancestors; - core::SymInfo &derived = heap.infoMut(state, loaded); - if (std::ranges::find(derived.ancestors, base) == - derived.ancestors.end()) { - derived.ancestors.insert(derived.ancestors.begin(), base); - for (core::Sym ancestor : above) - if (derived.ancestors.size() < 8 && - std::ranges::find(derived.ancestors, ancestor) == - derived.ancestors.end()) - derived.ancestors.push_back(ancestor); - if (derived.ancestors.size() > 8) - derived.ancestors.resize(8); - } - } - // Name the loaded value after the lvalue it was read from. - core::SymInfo &info = heap.infoMut(state, loaded); - // (A raw value keeps the first place it was read from.) - if (info.raw && info.rawVia.empty() && !info.name.empty() && - info.name.find('(') == std::string::npos) - info.rawVia = info.name; - if (info.name.empty() || info.type == core::SymInfo::Type::Pointer) - info.name = spell(sub); - uninitialisedRead(loaded, sub, *operand.address); - return loaded; - } - case CK_ArrayToPointerDecay: { - Address address = addressOf(sub); - QualType element = - context.getAsArrayType(sub.getType()) != nullptr - ? context.getAsArrayType(sub.getType())->getElementType() - : QualType(); - return pointerTo(address, element, spell(sub)); - } - case CK_FunctionToPointerDecay: - case CK_BuiltinFnToFnPtr: - return valueOf(sub); - case CK_NullToPointer: { - core::Sym null = nullPointer(type); - heap.infoMut(state, null).nullOrigin = - core::NullOrigin{.reason = core::NullOrigin::Reason::Assigned, - .where = toCoreLocation(context.getSourceManager(), - castExpr.getBeginLoc()), - .detail = {}}; - return null; - } - case CK_NoOp: - case CK_BitCast: - case CK_AddressSpaceConversion: - case CK_AtomicToNonAtomic: - case CK_NonAtomicToAtomic: { - core::Sym value = valueOf(sub); - // An allocation gets its type from its first typed use. - if (type->isPointerType()) { - QualType pointee = type->getPointeeType(); - if (!pointee->isVoidType() && !pointee->isCharType() && - !pointee->isIncompleteType() && !pointee->isFunctionType()) { - const core::SymInfo &info = heap.info(state, value); - for (const core::Target &target : info.targets) { - core::ObjectKind kind = run.table().info(target.object).key.kind; - if ((kind == core::ObjectKind::HeapRecent || - kind == core::ObjectKind::HeapOld) && - target.offset.isConstant() && target.offset.constant == 0) - run.table().setTypeIfUnknown(target.object, typeHandle(pointee)); - } - } - } - return value; - } - case CK_IntegralToPointer: { - core::Sym value = valueOf(sub); - const core::SymInfo &info = heap.info(state, value); - if (info.pointerBehind != core::ZeroSym) - return info.pointerBehind; - if (auto c = state.zone.constant(value); c && *c == 0) - return nullPointer(type); - // RFC 0033 §2: an integer converted to a pointer is a pointer of - // unknown provenance (`raw-cast`), guarded like one an unknown callee - // returned; only a declaration makes a raw pointer. - core::Sym converted = unknownValue(type); - heap.infoMut(state, converted).rawCast = true; - return converted; - } - case CK_PointerToIntegral: { - core::Sym pointer = valueOf(sub); - // RFC 0007 *Escape*: a pointer cast to an integer may be kept where - // the analysis cannot follow it, so what it points to is not leaked. - { - std::vector targets; - for (const core::Target &target : heap.info(state, pointer).targets) - targets.push_back(target.object); - for (core::ObjectId id : targets) - if (state.objects.contains(id)) - state.objects.at(id).escaped = true; - } - core::Sym number = unknownValue(type); - heap.infoMut(state, number).pointerBehind = pointer; - if (heap.info(state, pointer).null == core::PointerNull::Null) - state.zone.addRange(number, 0, 0); - return number; - } - case CK_PointerToBoolean: - case CK_IntegralToBoolean: - case CK_MemberPointerToBoolean: { - core::Sym operand = valueOf(sub); - core::Sym result = unknownValue(type); - state.zone.addRange(result, 0, 1); - core::SymInfo &info = heap.infoMut(state, result); - info.condition = - core::Condition{.op = core::Condition::Op::NonZero, .left = operand}; - // Decided statically when the operand is. - const core::SymInfo &op = heap.info(state, operand); - if (op.type == core::SymInfo::Type::Pointer) { - if (op.null == core::PointerNull::Null) - state.zone.addRange(result, 0, 0); - else if (op.null == core::PointerNull::NonNull) - state.zone.addRange(result, 1, 1); - } else if (auto lo = state.zone.lower(operand); lo && *lo > 0) { - state.zone.addRange(result, 1, 1); - } else if (auto c = state.zone.constant(operand)) { - state.zone.addRange(result, *c != 0 ? 1 : 0, *c != 0 ? 1 : 0); - } - return result; - } - case CK_IntegralCast: - // RFC 0017: modular conversion. - return convertInteger(valueOf(sub), sub.getType(), type); - case CK_BooleanToSignedIntegral: { - (void)valueOf(sub); - core::Sym out = unknownValue(type); - state.zone.addRange(out, -1, 0); - return out; - } - case CK_ToVoid: - default: - (void)valueOf(sub); - return unknownValue(type); - } -} - -core::Sym Transfer::evaluateUnary(const UnaryOperator &op) { - const Expr &sub = *op.getSubExpr(); - QualType type = op.getType(); - switch (op.getOpcode()) { - case UO_PreInc: - case UO_PreDec: - case UO_PostInc: - case UO_PostDec: { - Address address = addressOf(sub); - QualType valueType = sub.getType(); - const FieldDecl *bitField = sub.getSourceBitField(); - core::Sym old = bitField != nullptr - ? loadBitField(address, valueType, *bitField, &op) - : load(address, valueType, &op); - bool increment = op.isIncrementOp(); - core::Sym updated = core::ZeroSym; - if (valueType->isPointerType()) { - core::Sym one = constant(1, context.IntTy); - std::int64_t size = sizeOf(valueType->getPointeeType()).value_or(1); - updated = pointerAdd(old, one, size, !increment, sub); - } else { - // RFC 0017: a narrow operand is promoted, then converted back. - QualType computation = valueType; - if (valueType->isIntegerType() && - context.isPromotableIntegerType(valueType)) - computation = context.getPromotedIntegerType(valueType); - core::Sym widened = computation == valueType - ? old - : convertInteger(old, valueType, computation); - core::Sym one = constant(1, computation); - updated = arithmetic(increment ? BO_Add : BO_Sub, widened, one, - computation, op); - if (computation != valueType) - updated = convertInteger(updated, computation, valueType); - } - checkWriteAnnotation(address, op); - if (bitField != nullptr) - updated = storeBitField(address, updated, valueType, *bitField, &op); - else - store(address, updated, valueType, &op); - return op.isPrefix() ? updated : old; - } - case UO_Plus: - return valueOf(sub); - case UO_Minus: { - core::Sym zero = constant(0, type); - return arithmetic(BO_Sub, zero, valueOf(sub), type, op); - } - case UO_LNot: { - core::Sym operand = valueOf(sub); - core::Sym result = unknownValue(type); - state.zone.addRange(result, 0, 1); - core::SymInfo &info = heap.infoMut(state, result); - info.condition = core::Condition{.op = core::Condition::Op::Eq, - .left = operand, - .rightIsConstant = true, - .constant = 0}; - return result; - } - case UO_Not: { - (void)valueOf(sub); - return unknownValue(type); - } - case UO_Extension: - return valueOf(sub); - default: - (void)valueOf(sub); - return unknownValue(type); - } -} - -//===----------------------------------------------------------------------===// -// Integers (RFC 0017, RFC 0031 §4.4, §5.10) -//===----------------------------------------------------------------------===// - -/// The C integer operation of a binary operator. -static std::optional integerOpOf(BinaryOperatorKind kind) { - switch (kind) { - case BO_Add: - return core::IntegerOp::Add; - case BO_Sub: - return core::IntegerOp::Subtract; - case BO_Mul: - return core::IntegerOp::Multiply; - case BO_Div: - return core::IntegerOp::Divide; - case BO_Rem: - return core::IntegerOp::Remainder; - case BO_Shl: - return core::IntegerOp::ShiftLeft; - case BO_Shr: - return core::IntegerOp::ShiftRight; - case BO_And: - return core::IntegerOp::BitAnd; - case BO_Or: - return core::IntegerOp::BitOr; - case BO_Xor: - return core::IntegerOp::BitXor; - default: - return std::nullopt; - } -} - -static bool commutative(core::IntegerOp op) { - return op == core::IntegerOp::Add || op == core::IntegerOp::Multiply || - op == core::IntegerOp::BitAnd || op == core::IntegerOp::BitOr || - op == core::IntegerOp::BitXor; -} - -/// The least and greatest values of `type`, as mathematical integers. -static __int128 typeMin(const core::IntegerType &type) { - if (type.isBoolean || !type.isSigned) - return 0; - return -static_cast<__int128>(static_cast(1) - << (type.width - 1)); -} -static __int128 typeMax(const core::IntegerType &type) { - if (type.isBoolean) - return 1; - if (type.isSigned) - return static_cast<__int128>(static_cast(1) - << (type.width - 1)) - - 1; - return static_cast<__int128>(static_cast(1) - << type.width) - - 1; -} - -/// An integer value as a mathematical integer. -static __int128 mathematical(const core::IntegerValue &value) { - return value.negative() ? -static_cast<__int128>(value.magnitude()) - : static_cast<__int128>(value.magnitude()); -} - -/// `sym`'s bounds as a value of `type`: its zone bounds within the type's -/// range (every integer symbol lies in its type's range, §4.4), and its -/// interval where the zone cannot hold it (an unsigned 64-bit value above -/// `INT64_MAX`). -static std::pair<__int128, __int128> boundsOf(const core::HeapState &state, - core::Sym sym, - const core::IntegerType &type) { - __int128 lo = typeMin(type); - __int128 hi = typeMax(type); - if (auto lower = state.zone.lower(sym); lower && *lower > lo) - lo = *lower; - if (auto upper = state.zone.upper(sym); upper && *upper < hi) - hi = *upper; - if (const core::SymInfo *info = state.syms.find(sym); - info != nullptr && info->values && info->values->type == type && - !info->values->empty()) { - lo = std::max(lo, mathematical(*info->values->minimum())); - hi = std::min(hi, mathematical(*info->values->maximum())); - } - if (lo > hi) - return {typeMin(type), typeMax(type)}; - return {lo, hi}; -} - -/// `sym`'s values as a `type`, for RFC 0017's range evaluation. -static core::IntegerRange rangeOf(const core::HeapState &state, core::Sym sym, - const core::IntegerType &type) { - auto [lo, hi] = boundsOf(state, sym, type); - core::IntegerRange range = core::IntegerRange::between( - core::IntegerValue::ofBits(type, static_cast(lo)), - core::IntegerValue::ofBits(type, static_cast(hi))); - if (const core::SymInfo *info = state.syms.find(sym); - info != nullptr && info->values && info->values->type == type) - if (core::IntegerRange both = range.intersect(*info->values); !both.empty()) - return both; - return range; -} - -/// Bounds `sym` by `range`: the zone takes the hull's 64-bit bounds, and a -/// range beyond them (an unsigned 64-bit value above `INT64_MAX`) is kept as -/// the symbol's interval (§4.4). -static void boundBy(const core::Heap &heap, core::HeapState &state, - core::Sym sym, const core::IntegerRange &range) { - if (range.empty() || range.isFull()) - return; - std::optional lo = range.minimum()->signedValue(); - std::optional hi = range.maximum()->signedValue(); - if (lo || hi) - state.zone.addRange(sym, lo, hi); - if (hi) - return; - core::SymInfo &info = heap.infoMut(state, sym); - if (info.type != core::SymInfo::Type::Int) - return; - if (info.values && info.values->type == range.type) { - core::IntegerRange both = info.values->intersect(range); - if (!both.empty()) - info.values = both; - } else { - info.values = range; - } -} - -static __int128 floorDiv(__int128 value, __int128 divisor) { - return value >= 0 ? value / divisor : -((-value + divisor - 1) / divisor); -} - -/// RFC 0017 wrap-around: the multiple of 2^width that every value of the -/// mathematical range `[lo, hi]` loses when it is stored in `type`, when -/// they all lose the same one (`UINT_MAX + 2` is `1` with a shift of 2^32). -static std::optional<__int128> wrapShift(__int128 lo, __int128 hi, - const core::IntegerType &type) { - if (type.isBoolean || type.width == 0 || type.width > 64) - return std::nullopt; - auto modulus = - static_cast<__int128>(static_cast(1) << type.width); - __int128 k = floorDiv(lo - typeMin(type), modulus); - if (floorDiv(hi - typeMin(type), modulus) != k) - return std::nullopt; - return k * modulus; -} - -static bool fitsInt64(__int128 value) { - return value >= INT64_MIN && value <= INT64_MAX; -} - -/// Whether `base + delta` stays within `type` because the zone keeps -/// `base` below another integer by at least `delta` (`i < n` for a `size_t -/// i`, whose upper bound does not fit the zone's 64-bit bounds). -static bool belowAnother(const core::Heap &heap, const core::HeapState &state, - core::Sym base, __int128 delta, - const core::IntegerType &type) { - for (core::Sym other : state.zone.symbols()) { - if (other == core::ZeroSym || other == base) - continue; - auto bound = state.zone.bound(base, other); - if (!bound) - continue; - const core::SymInfo &info = heap.info(state, other); - if (info.type != core::SymInfo::Type::Int || !info.intType) - continue; - __int128 otherMax = typeMax(*info.intType); - if (auto upper = state.zone.upper(other); upper && *upper < otherMax) - otherMax = *upper; - if (otherMax + *bound + delta <= typeMax(type)) - return true; - } - return false; -} - -void Transfer::reportInteger(core::IntegerError error, const Expr &at) { - if (!run.isPublishing() || error == core::IntegerError::None || - error == core::IntegerError::IncompatibleTypes) - return; - core::Diagnostic diagnostic; - diagnostic.id = core::diag::InvalidIntegerOperation; - diagnostic.severity = core::Severity::Error; - diagnostic.message = - "invalid integer operation: " + std::string(core::toString(error)); - diagnostic.location = - toCoreLocation(context.getSourceManager(), at.getBeginLoc()); - run.report(std::move(diagnostic), core::Certainty::Definite, &at, - std::nullopt); -} - -/// RFC 0017 §3: the values of `left op right` computed without wrap-around -/// that `type` can hold (a checked operation's result when it did not -/// overflow); none when there are none. -static std::optional -exactRange(core::IntegerOp op, std::pair<__int128, __int128> left, - std::pair<__int128, __int128> right, const core::IntegerType &type) { - __int128 lo = 0; - __int128 hi = 0; - switch (op) { - case core::IntegerOp::Add: - lo = left.first + right.first; - hi = left.second + right.second; - break; - case core::IntegerOp::Subtract: - lo = left.first - right.second; - hi = left.second - right.first; - break; - case core::IntegerOp::Multiply: { - bool first = true; - for (__int128 x : {left.first, left.second}) - for (__int128 y : {right.first, right.second}) { - __int128 product = 0; - // (Beyond 128 bits, saturated: the type's limits clip it below.) - if (__builtin_mul_overflow(x, y, &product)) { - const auto most = - static_cast<__int128>(~static_cast(0) >> 1U); - product = (x < 0) != (y < 0) ? -most - 1 : most; - } - lo = first ? product : std::min(lo, product); - hi = first ? product : std::max(hi, product); - first = false; - } - break; - } - default: - return std::nullopt; - } - lo = std::max(lo, typeMin(type)); - hi = std::min(hi, typeMax(type)); - if (lo > hi) - return std::nullopt; - return core::IntegerRange::between( - core::IntegerValue::ofBits(type, static_cast(lo)), - core::IntegerValue::ofBits(type, static_cast(hi))); -} - -std::optional Transfer::checkedProduct(core::Sym left, - core::Sym right, - QualType type, - const Expr &at) { - auto integer = integerType(type); - if (integer && !integer->isBoolean && integer->width <= 64 && - !exactRange(core::IntegerOp::Multiply, boundsOf(state, left, *integer), - boundsOf(state, right, *integer), *integer)) - return std::nullopt; - return arithmetic(BO_Mul, left, right, type, at); -} - -core::Sym Transfer::checkedArithmetic(const CallExpr &call, core::IntegerOp op, - const std::vector &args, - core::Sym result) { - QualType outType = call.getArg(2)->getType(); - if (!outType->isPointerType()) - return result; - outType = outType->getPointeeType(); - auto a = integerType(call.getArg(0)->getType()); - auto b = integerType(call.getArg(1)->getType()); - auto destination = integerType(outType); - if (!a || !b || !destination || a->width > 64 || b->width > 64 || - destination->width > 64 || destination->isBoolean) - return result; - // Computed in infinite precision, then stored converted, with the flag - // saying whether the destination holds it. - core::CheckedIntegerRange checked = - core::evaluateCheckedInteger(op, rangeOf(state, args[0], *a), - rangeOf(state, args[1], *b), *destination); - core::Sym stored = unknownValue(outType); - boundBy(heap, state, stored, checked.values); - const core::SymInfo &out = heap.info(state, args[2]); - if (out.type == core::SymInfo::Type::Pointer && !out.top) { - Address address; - address.targets = out.targets; - store(address, stored, outType, nullptr); - } - core::Sym flag = unknownValue(call.getType()); - state.zone.addRange(flag, 0, 1); - if (auto overflow = checked.overflow.constant()) - state.zone.addRange(flag, static_cast(overflow->bits), - static_cast(overflow->bits)); - // RFC 0017 §3: when the flag is false the stored value is the exact - // result, within the operands' mathematical range. - if (auto exact = exactRange(op, boundsOf(state, args[0], *a), - boundsOf(state, args[1], *b), *destination); - exact && !exact->isFull()) { - core::PendingCase success; - success.kind = core::PendingCase::Kind::Bound; - success.classes = {"zero"}; - success.subject = stored; - success.bound = *exact; - heap.infoMut(state, flag).pending.push_back(std::move(success)); - } - (void)result; - return flag; -} - -core::Sym Transfer::convertInteger(core::Sym value, QualType from, - QualType to) { - auto target = integerType(to); - if (!target) - return unknownValue(to); - return convertInteger(value, from, *target, to); -} - -core::Sym Transfer::convertInteger(core::Sym value, QualType from, - const core::IntegerType &targetType, - QualType to) { - auto source = integerType(from); - const core::IntegerType *target = &targetType; - if (!source || source->width > 64 || target->width > 64) - return unknownValue(to); - auto [lo, hi] = boundsOf(state, value, *source); - const core::SymInfo &info = heap.info(state, value); - if (!target->isBoolean && lo >= typeMin(*target) && hi <= typeMax(*target) && - info.type != core::SymInfo::Type::Pointer) { - // The value is unchanged: keep the symbol and its relations. - core::SymInfo &kept = heap.infoMut(state, value); - if (kept.type == core::SymInfo::Type::Unknown) { - kept.type = core::SymInfo::Type::Int; - kept.intType = *target; - } - return value; - } - core::Sym pointerBehind = info.pointerBehind; - core::Sym out = unknownValue(to); - boundBy(heap, state, out, rangeOf(state, value, *source).converted(*target)); - // Every value wraps by the same multiple of 2^width: the result is the - // value moved by it (RFC 0017, `(unsigned char)256` is `0`). - if (!target->isBoolean) - if (auto shift = wrapShift(lo, hi, *target); shift && fitsInt64(*shift)) { - state.zone.addEq(out, value, static_cast(-*shift)); - core::Term base = termOf(value); - if (base.known && !base.isConstant()) - heap.infoMut(state, out).linear = - base.plusConstant(static_cast(-*shift)); - } - if (pointerBehind != core::ZeroSym) - heap.infoMut(state, out).pointerBehind = pointerBehind; - return out; -} - -/// RFC 0017: whether no other member of the bit-field's record shares a -/// byte with it, so that the cell at its first byte is its own. -static bool bitFieldAlone(const ASTContext &context, const FieldDecl &field) { - const RecordDecl *record = field.getParent(); - if (record == nullptr || record->isUnion() || - !record->isCompleteDefinition() || field.isZeroLengthBitField()) - return false; - const ASTRecordLayout &layout = context.getASTRecordLayout(record); - const std::uint64_t charWidth = context.getCharWidth(); - auto bytes = [&](const FieldDecl &member) - -> std::optional> { - std::uint64_t start = layout.getFieldOffset(member.getFieldIndex()); - std::uint64_t width = 0; - if (member.isBitField()) - width = member.getBitWidthValue(); - else if (!member.getType()->isIncompleteType()) - width = context.getTypeSize(member.getType()); - if (width == 0) - return std::nullopt; - return std::make_pair(start / charWidth, (start + width - 1) / charWidth); - }; - auto own = bytes(field); - if (!own) - return false; - for (const FieldDecl *other : record->fields()) { - if (other == &field) - continue; - auto theirs = bytes(*other); - if (theirs && theirs->first <= own->second && own->first <= theirs->second) - return false; - } - return true; -} - -std::optional -Transfer::bitFieldType(const FieldDecl &field) const { - auto type = integerType(field.getType()); - if (!type || type->isBoolean || !field.isBitField() || - field.getBitWidth()->isValueDependent()) - return std::nullopt; - const unsigned width = field.getBitWidthValue(); - if (width == 0 || width > type->width) - return std::nullopt; - type->width = width; - return type; -} - -core::Sym Transfer::loadBitField(const Address &address, QualType type, - const FieldDecl &field, const Expr *at) { - auto width = bitFieldType(field); - auto integer = integerType(type); - if (!width || !integer) - return load(address, type, at); - if (!bitFieldAlone(context, field)) { - core::Sym value = unknownValue(type); - boundBy(heap, state, value, - core::IntegerRange::full(*width).converted(*integer)); - return value; - } - core::Sym loaded = load(address, type, at); - if (heap.info(state, loaded).type != core::SymInfo::Type::Int) - return loaded; - // Every value of the cell is one of the width's (a store converts). - auto [lo, hi] = boundsOf(state, loaded, *integer); - if (lo >= typeMin(*width) && hi <= typeMax(*width)) - return loaded; - return convertInteger(loaded, type, *width, type); -} - -core::Sym Transfer::storeBitField(const Address &address, core::Sym value, - QualType type, const FieldDecl &field, - const Expr *at) { - auto width = bitFieldType(field); - if (!width || !integerType(type)) { - store(address, value, type, at); - return value; - } - core::Sym stored = convertInteger(value, type, *width, type); - if (bitFieldAlone(context, field)) { - store(address, stored, type, at); - return stored; - } - // The bytes it shares hold another member's bits too: forget them. - const std::uint64_t charWidth = context.getCharWidth(); - const std::uint64_t start = context.getASTRecordLayout(field.getParent()) - .getFieldOffset(field.getFieldIndex()); - const auto bytes = static_cast( - ((start % charWidth) + field.getBitWidthValue() + charWidth - 1) / - charWidth); - for (const core::Target &target : address.targets) - if (target.offset.isConstant()) - heap.forgetCells(state, target.object, target.offset.constant, bytes); - else - heap.forgetCells(state, target.object, 0, std::nullopt); - return stored; -} - -core::Sym Transfer::arithmetic(BinaryOperatorKind kind, core::Sym left, - core::Sym right, QualType type, const Expr &at) { - auto integer = integerType(type); - auto op = integerOpOf(kind); - if (!integer || !op || integer->width > 64 || integer->isBoolean) - return unknownValue(type); - // §4.1: symbols are immutable, so the same operation on the same values - // is the same value (`malloc(rows * cols)` and `p[rows * cols]`). - auto c1 = state.zone.constant(left); - auto c2 = state.zone.constant(right); - core::SymDefinition definition{.op = *op, .left = left, .right = right}; - if (c2) { - definition.right = core::ZeroSym; - definition.constant = c2; - } else if (c1 && commutative(*op)) { - definition.left = right; - definition.right = core::ZeroSym; - definition.constant = c1; - } else if (commutative(*op) && definition.right < definition.left) { - std::swap(definition.left, definition.right); - } - core::Handle ctype = typeHandle(type); - FunctionRun::OperationKey key{*op, definition.left, definition.right, - definition.constant, ctype}; - if (auto it = run.operations.find(key); it != run.operations.end()) - if (const core::SymInfo *same = state.syms.find(it->second); - same != nullptr && same->defined && - same->defined->sameOperation(definition) && same->ctype == ctype) - return it->second; - - const bool wrapSigned = context.getLangOpts().isSignedOverflowDefined(); - const bool shift = - *op == core::IntegerOp::ShiftLeft || *op == core::IntegerOp::ShiftRight; - core::IntegerType rhsType = *integer; - if (shift) - rhsType = - heap.info(state, right) - .intType.value_or(core::IntegerType{.width = 64, .isSigned = true}); - core::IntegerRangeEvaluation evaluation = - core::evaluateInteger(*op, rangeOf(state, left, *integer), - rangeOf(state, right, rhsType), wrapSigned); - if (evaluation.alwaysInvalid) - reportInteger(evaluation.error, at); - core::Sym result = unknownValue(type); - if (!evaluation.mayBeInvalid) - boundBy(heap, state, result, evaluation.values); - - auto [lo1, hi1] = boundsOf(state, left, *integer); - auto [lo2, hi2] = boundsOf(state, right, rhsType); - // Whether the C result is the mathematical one for every value. - bool exact = false; - auto fits = [&](__int128 lo, __int128 hi) { - return lo >= typeMin(*integer) && hi <= typeMax(*integer); - }; - switch (*op) { - case core::IntegerOp::Add: - exact = fits(lo1 + lo2, hi1 + hi2); - break; - case core::IntegerOp::Subtract: - exact = fits(lo1 - hi2, hi1 - lo2); - break; - case core::IntegerOp::Multiply: { - // (Bounds of 64-bit types: a product may not fit 128 bits either.) - bool wraps = false; - auto product = [&](__int128 x, __int128 y) { - __int128 result = 0; - wraps = __builtin_mul_overflow(x, y, &result) || wraps; - return result; - }; - const std::array<__int128, 4> products = { - product(lo1, lo2), product(lo1, hi2), product(hi1, lo2), - product(hi1, hi2)}; - exact = !wraps && fits(*std::ranges::min_element(products), - *std::ranges::max_element(products)); - break; - } - case core::IntegerOp::Divide: - exact = c2 && *c2 > 0 && lo1 >= 0; - break; - default: - break; - } - - // RFC 0017 §3: a range guard (`n <= INT_MAX / m`) keeps a product of - // non-negative operands from wrapping. - if (*op == core::IntegerOp::Multiply && !exact && lo1 >= 0 && lo2 >= 0) { - auto guard = [&](core::Sym x, core::Sym y) -> std::optional<__int128> { - const core::SymInfo *info = state.syms.find(x); - if (info == nullptr || !info->productAtMost || - info->productAtMost->first != y) - return std::nullopt; - return static_cast<__int128>(info->productAtMost->second); - }; - auto bound = guard(left, right); - if (!bound) - bound = guard(right, left); - if (bound) { - __int128 top = std::min(*bound, typeMax(*integer)); - exact = true; - if (fitsInt64(top)) - state.zone.addRange(result, 0, static_cast(top)); - } - } - - // `a = b + c` with a constant `c` records `a - b = c` (§4.4), moved by the - // wrap when every value wraps alike (RFC 0017). - const bool additive = - *op == core::IntegerOp::Add || *op == core::IntegerOp::Subtract; - if (additive && !evaluation.alwaysInvalid && - (c2 || (c1 && *op == core::IntegerOp::Add))) { - core::Sym base = c2 ? left : right; - auto delta = static_cast<__int128>(c2 ? *c2 : *c1); - if (c2 && *op != core::IntegerOp::Add) - delta = -delta; - auto [lo, hi] = boundsOf(state, base, *integer); - std::optional<__int128> moved = wrapShift(lo + delta, hi + delta, *integer); - if (moved && *moved != 0 && integer->isSigned && !wrapSigned) - moved.reset(); // signed overflow: undefined, no relation - // Below another value of the type by at least `delta`: no wrap, signed - // or not (a loop's induction after widening dropped its bounds). - if (!moved && delta > 0 && belowAnother(heap, state, base, delta, *integer)) - moved = 0; - if (moved && fitsInt64(delta - *moved)) { - auto total = static_cast(delta - *moved); - state.zone.addEq(result, base, total); - core::Term term = termOf(base); - if (term.known && !term.isConstant()) - heap.infoMut(state, result).linear = term.plusConstant(total); - exact = exact || *moved == 0; - } - } - // `a = b * k` in an unsigned type that may wrap: `b * k` reduced, never - // more than it (an allocation of it ends there at the latest). - if (*op == core::IntegerOp::Multiply && !exact && (c1 || c2) && - !integer->isSigned && !integer->isBoolean && integer->width == 64) { - std::int64_t k = c2 ? *c2 : *c1; - core::Term base = termOf(c2 ? left : right); - if (k > 0 && base.known && !base.isConstant() && base.scale > 0 && - base.constant >= 0) { - __int128 scale = static_cast<__int128>(base.scale) * k; - __int128 constant = static_cast<__int128>(base.constant) * k; - if (fitsInt64(scale) && fitsInt64(constant)) - heap.infoMut(state, result).unwrapped = - core::Term::ofSym(base.var, static_cast(scale), - static_cast(constant)); - } - } - // `a = b * k` without wrap-around is linear in `b`. - if (*op == core::IntegerOp::Multiply && exact && (c1 || c2)) { - std::int64_t k = c2 ? *c2 : *c1; - core::Term base = termOf(c2 ? left : right); - if (base.known) { - __int128 scale = static_cast<__int128>(base.scale) * k; - __int128 constant = static_cast<__int128>(base.constant) * k; - if (fitsInt64(scale) && fitsInt64(constant)) - heap.infoMut(state, result).linear = - base.isConstant() - ? core::Term::of(static_cast(constant)) - : core::Term::ofSym(base.var, static_cast(scale), - static_cast(constant)); - } - } - definition.exact = exact; - if (definition.left == left) - if (auto k = rangeOf(state, left, *integer).constant()) - definition.leftValue = *k; - heap.infoMut(state, result).defined = definition; - run.operations[key] = result; - return result; -} - -core::Sym Transfer::compare(BinaryOperatorKind kind, core::Sym left, - core::Sym right, QualType operandType) { - core::Sym result = unknownValue(context.IntTy); - state.zone.addRange(result, 0, 1); - core::Condition condition; - switch (kind) { - case BO_EQ: - condition.op = core::Condition::Op::Eq; - break; - case BO_NE: - condition.op = core::Condition::Op::Ne; - break; - case BO_LT: - condition.op = core::Condition::Op::Lt; - break; - case BO_LE: - condition.op = core::Condition::Op::Le; - break; - case BO_GT: - condition.op = core::Condition::Op::Gt; - break; - case BO_GE: - condition.op = core::Condition::Op::Ge; - break; - default: - return result; - } - condition.left = left; - // The same value on both sides. - if (left == right && (kind == BO_EQ || kind == BO_NE || kind == BO_LE || - kind == BO_GE || kind == BO_LT || kind == BO_GT)) { - bool truth = kind == BO_EQ || kind == BO_LE || kind == BO_GE; - state.zone.addRange(result, truth ? 1 : 0, truth ? 1 : 0); - return result; - } - if (auto c = state.zone.constant(right); c && !operandType->isPointerType()) { - condition.rightIsConstant = true; - condition.constant = *c; - } else if (operandType->isPointerType() && - heap.info(state, right).null == core::PointerNull::Null) { - condition.rightIsConstant = true; - condition.constant = 0; - } else if (operandType->isPointerType() && - heap.info(state, left).null == core::PointerNull::Null) { - // `NULL == p`: swap. - condition.left = right; - condition.rightIsConstant = true; - condition.constant = 0; - } else if (auto k = state.zone.constant(left); - k && !operandType->isPointerType() && - (kind == BO_EQ || kind == BO_NE)) { - // `DIRECT == t`: swap, so its edges refine `t` (RFC 0034 §6.2). - condition.left = right; - condition.rightIsConstant = true; - condition.constant = *k; - } else { - condition.right = right; - } - condition.isUnsigned = operandType->isUnsignedIntegerType(); - heap.infoMut(state, result).condition = condition; - // Two pointers into known objects at known offsets. - if (operandType->isPointerType() && !condition.rightIsConstant && - (kind == BO_EQ || kind == BO_NE)) { - const core::SymInfo &a = heap.info(state, left); - const core::SymInfo &b = heap.info(state, right); - if (a.type == core::SymInfo::Type::Pointer && - b.type == core::SymInfo::Type::Pointer && !a.top && !b.top && - a.null == core::PointerNull::NonNull && - b.null == core::PointerNull::NonNull && a.targets.size() == 1 && - b.targets.size() == 1 && a.targets[0].offset.isConstant() && - b.targets[0].offset.isConstant()) { - std::optional equal; - if (a.targets[0].object == b.targets[0].object && - run.table().info(a.targets[0].object).singular) - equal = a.targets[0].offset.constant == b.targets[0].offset.constant; - else if (!heap.mayOverlap(state, a.targets[0].object, - b.targets[0].object)) - equal = false; - if (equal) { - bool truth = kind == BO_EQ ? *equal : !*equal; - state.zone.addRange(result, truth ? 1 : 0, truth ? 1 : 0); - return result; - } - } - } - // Decide it now when the facts do. - core::HeapState whenTrue = state; - core::HeapState whenFalse = state; - bool canTrue = refine(run, whenTrue, result, true); - bool canFalse = refine(run, whenFalse, result, false); - if (canTrue && !canFalse) - state.zone.addRange(result, 1, 1); - else if (!canTrue && canFalse) - state.zone.addRange(result, 0, 0); - return result; -} - -core::Sym Transfer::evaluateAssign(const BinaryOperator &op) { - const Expr &lhs = *op.getLHS(); - QualType type = lhs.getType(); - Address address = addressOf(lhs); - core::Sym value = core::ZeroSym; - if (op.getOpcode() == BO_Assign) { - if (type->isRecordType()) { - // A record assignment copies the cells. - const Expr *rhs = op.getRHS()->IgnoreParens(); - if (const auto *cast = dyn_cast(rhs); - cast != nullptr && cast->getCastKind() == CK_LValueToRValue) - rhs = cast->getSubExpr(); - if (rhs->isGLValue() && copyRecord(address, addressOf(*rhs), type)) - return unknownValue(type); - ExprResult source = evaluate(*op.getRHS()); - if (source.address && source.address->targets.size() == 1 && - address.targets.size() == 1 && - source.address->targets[0].offset.isConstant() && - address.targets[0].offset.isConstant() && - address.targets[0].offset.constant == 0 && - source.address->targets[0].offset.constant == 0 && - state.objects.contains(source.address->targets[0].object)) { - const core::ObjectState from = - state.objects.at(source.address->targets[0].object); - core::ObjectState © = heap.ensure(state, address.targets[0].object); - copy.cells = from.cells; - copy.havocked = from.havocked; - copy.forgotten = from.forgotten; - copy.mayForgotten = from.mayForgotten; - copy.zeroed = from.zeroed; - copy.uninitialised = from.uninitialised; - } else { - for (const core::Target &target : address.targets) { - heap.ensure(state, target.object).cells = {}; - state.objects.at(target.object).havocked = true; - } - } - return unknownValue(type); - } - value = valueOf(*op.getRHS()); - // RFC 0004: an assignment to a place declared with a safe kind. - const Expr *target = lhs.IgnoreParenImpCasts(); - const ValueDecl *declared = nullptr; - if (const auto *ref = dyn_cast(target)) - declared = ref->getDecl(); - else if (const auto *member = dyn_cast(target)) - declared = member->getMemberDecl(); - if (declared != nullptr && type->isPointerType()) - value = - launder(value, getAnnotations(*declared), spell(lhs), *op.getRHS()); - checkWriteAnnotation(address, op); - } else { - checkWriteAnnotation(address, op); - const FieldDecl *bitField = lhs.getSourceBitField(); - core::Sym old = bitField != nullptr - ? loadBitField(address, type, *bitField, &op) - : load(address, type, &op); - core::Sym rhs = valueOf(*op.getRHS()); - BinaryOperatorKind kind = - BinaryOperator::getOpForCompoundAssignment(op.getOpcode()); - if (type->isPointerType() && (kind == BO_Add || kind == BO_Sub)) { - std::int64_t size = sizeOf(type->getPointeeType()).value_or(1); - value = pointerAdd(old, rhs, size, kind == BO_Sub, op); - } else if (const auto *compound = dyn_cast(&op); - compound != nullptr && type->isIntegerType() && - compound->getComputationLHSType()->isIntegerType() && - compound->getComputationResultType()->isIntegerType()) { - // RFC 0017: computed in the promoted type, then converted back. - QualType computation = compound->getComputationLHSType(); - QualType result = compound->getComputationResultType(); - core::Sym promoted = - computation == type ? old : convertInteger(old, type, computation); - value = arithmetic(kind, promoted, rhs, result, op); - if (result != type) - value = convertInteger(value, result, type); - } else { - value = arithmetic(kind, old, rhs, type, op); - } - } - if (const FieldDecl *bitField = lhs.getSourceBitField()) - value = storeBitField(address, value, type, *bitField, &op); - else - store(address, value, type, &op); - if (const auto *member = dyn_cast(lhs.IgnoreParenImpCasts())) - checkSizedFieldStore(*member, address); - return value; -} - -std::optional Transfer::truthValue(const Expr &expr) { - const Expr *bare = expr.IgnoreParenImpCasts(); - auto truth = [&](core::Condition condition) { - core::Sym result = unknownValue(context.IntTy); - state.zone.addRange(result, 0, 1); - heap.infoMut(state, result).condition = condition; - return result; - }; - if (const auto *op = dyn_cast(bare)) { - if (op->isComparisonOp()) - return comparison(*op); - if (op->isLogicalOp()) { - auto left = truthValue(*op->getLHS()); - auto right = left ? truthValue(*op->getRHS()) : std::nullopt; - if (!left || !right) - return std::nullopt; - return truth(core::Condition{.op = op->getOpcode() == BO_LAnd - ? core::Condition::Op::And - : core::Condition::Op::Or, - .left = *left, - .right = *right}); - } - return std::nullopt; - } - if (const auto *op = dyn_cast(bare); - op != nullptr && op->getOpcode() == UO_LNot) { - auto inner = truthValue(*op->getSubExpr()); - if (!inner) - return std::nullopt; - return truth(core::Condition{.op = core::Condition::Op::Eq, - .left = *inner, - .rightIsConstant = true, - .constant = 0}); - } - QualType type = bare->getType(); - if (type->isPointerType() || type->isIntegerType()) - return truth(core::Condition{.op = core::Condition::Op::NonZero, - .left = valueOf(*bare)}); - return std::nullopt; -} - -core::Sym Transfer::evaluateBinary(const BinaryOperator &op) { - BinaryOperatorKind kind = op.getOpcode(); - if (op.isAssignmentOp()) - return evaluateAssign(op); - if (kind == BO_Comma) { - (void)valueOf(*op.getLHS()); - return valueOf(*op.getRHS()); - } - if (kind == BO_LAnd || kind == BO_LOr) { - // Used as a value (`__builtin_expect(p && n, 1)`, `!(p || n)`): what - // its truth says of both operands, which a side-effect-free operator - // lets us evaluate again here (§5.1). - if (!op.HasSideEffects(context)) - if (auto truth = truthValue(op)) - return *truth; - core::Sym result = unknownValue(op.getType()); - state.zone.addRange(result, 0, 1); - return result; - } - const Expr &lhs = *op.getLHS(); - const Expr &rhs = *op.getRHS(); - core::Sym left = valueOf(lhs); - core::Sym right = valueOf(rhs); - if (op.isComparisonOp()) - return compare(kind, left, right, lhs.getType()); - QualType lt = lhs.getType(); - QualType rt = rhs.getType(); - if ((kind == BO_Add || kind == BO_Sub) && lt->isPointerType() && - rt->isIntegerType()) { - std::int64_t size = sizeOf(lt->getPointeeType()).value_or(1); - return pointerAdd(left, right, size, kind == BO_Sub, op); - } - if (kind == BO_Add && rt->isPointerType() && lt->isIntegerType()) { - std::int64_t size = sizeOf(rt->getPointeeType()).value_or(1); - return pointerAdd(right, left, size, false, op); - } - if (kind == BO_Sub && lt->isPointerType() && rt->isPointerType()) { - // The difference of two pointers into one object. - const core::SymInfo &a = heap.info(state, left); - const core::SymInfo &b = heap.info(state, right); - core::Sym result = unknownValue(op.getType()); - std::int64_t size = sizeOf(lt->getPointeeType()).value_or(1); - if (a.targets.size() == 1 && b.targets.size() == 1 && - a.targets[0].object == b.targets[0].object && - a.targets[0].offset.isConstant() && b.targets[0].offset.isConstant() && - size > 0) { - std::int64_t diff = - (a.targets[0].offset.constant - b.targets[0].offset.constant) / size; - state.zone.addRange(result, diff, diff); - } - return result; - } - return arithmetic(kind, left, right, op.getType(), op); -} - -//===----------------------------------------------------------------------===// -// Refinement on branches -//===----------------------------------------------------------------------===// - -/// The result classes a value may still have. -static std::set possibleClasses(const core::Heap &heap, - const core::HeapState &state, - core::Sym sym) { - const core::SymInfo &info = heap.info(state, sym); - if (info.type == core::SymInfo::Type::Pointer) { - switch (info.null) { - case core::PointerNull::Null: - return {"null"}; - case core::PointerNull::NonNull: - return {"nonnull"}; - case core::PointerNull::Maybe: - return {"null", "nonnull"}; - } - } - std::set out; - auto lo = state.zone.lower(sym); - auto hi = state.zone.upper(sym); - if (!lo || *lo < 0) - out.insert("negative"); - if ((!lo || *lo <= 0) && (!hi || *hi >= 0) && !info.nonZero) - out.insert("zero"); - if (!hi || *hi > 0) - out.insert("positive"); - return out; -} - -/// RFC 0031 §6.3: every cell and carried expression holding `from` holds -/// `to` instead (a join symbol a test resolved to one of its members). -static void replaceHeld(core::HeapState &state, core::Sym from, core::Sym to) { - if (from == to || !state.syms.contains(to)) - return; - std::vector> cells; - for (const auto &[id, object] : state.objects) - for (const auto &[key, sym] : object.cells) - if (sym == from) - cells.emplace_back(id, key); - for (const auto &[id, key] : cells) - state.objects.at(id).cells.set(key, to); - std::vector exprs; - for (const auto &[handle, sym] : state.exprs) - if (sym == from) - exprs.push_back(handle); - for (core::Handle handle : exprs) - state.exprs.set(handle, to); -} - -/// Applies the pending cases of `sym` a test has decided (RFC 0030 §9.1): -/// a selected release is made again with its own record; one the test rules -/// out is undone, unless another selected case releases the same value. -static void resolvePending(FunctionRun &run, core::HeapState &state, - core::Sym sym) { - const core::Heap &heap = run.domain(); - const core::SymInfo *info = state.syms.find(sym); - if (info == nullptr || info->pending.empty()) - return; - std::set possible = possibleClasses(heap, state, sym); - std::vector kept; - std::vector selected; - std::vector excluded; - for (const core::PendingCase &pending : info->pending) { - bool all = true; - bool none = true; - for (const std::string &c : possible) { - bool in = std::ranges::find(pending.classes, c) != pending.classes.end(); - all = all && in; - none = none && !in; - } - // A release whose parameter test the path has decided the other way - // never happened here. - if (all && pending.kind == core::PendingCase::Kind::Release && - pending.argumentZero && - state.syms.contains(pending.argumentZero->first)) - if (auto zero = isZeroValue(heap, state, pending.argumentZero->first); - zero && *zero != pending.argumentZero->second) { - excluded.push_back(pending); - continue; - } - if (all) - selected.push_back(pending); - else if (none) - excluded.push_back(pending); - else - kept.push_back(pending); - } - auto releasedBySelected = [&](core::Sym subject) { - auto releases = [&](const core::PendingCase &pending) { - return pending.subject == subject && - pending.kind == core::PendingCase::Kind::Release; - }; - return std::ranges::any_of(selected, releases) || - std::ranges::any_of(kept, releases); - }; - for (const core::PendingCase &pending : excluded) - if (pending.kind == core::PendingCase::Kind::Stored) - replaceHeld(state, pending.subject, pending.previous); - for (const core::PendingCase &pending : excluded) { - if (pending.kind != core::PendingCase::Kind::Release || - !state.syms.contains(pending.subject) || - releasedBySelected(pending.subject)) - continue; - core::SymInfo &subject = heap.infoMut(state, pending.subject); - if (subject.release && subject.release->where == pending.record.where) - subject.release.reset(); - std::vector targets; - targets.reserve(subject.targets.size()); - for (const core::Target &target : subject.targets) - targets.push_back(target.object); - for (core::ObjectId id : targets) { - if (!state.objects.contains(id)) - continue; - core::ObjectState &object = state.objects.at(id); - if (!object.record || object.record->where != pending.record.where) - continue; - object.life = core::Life::Live; - object.record.reset(); - object.effectReleased = false; - object.effectMayReleased = false; - } - } - for (const core::PendingCase &pending : selected) { - if (!state.syms.contains(pending.subject)) - continue; - if (pending.kind == core::PendingCase::Kind::Bound) { - if (pending.bound) - boundBy(heap, state, pending.subject, *pending.bound); - continue; - } - if (pending.kind == core::PendingCase::Kind::NonNull) { - heap.infoMut(state, pending.subject).null = core::PointerNull::NonNull; - continue; - } - if (pending.kind == core::PendingCase::Kind::Stored) { - replaceHeld(state, pending.subject, pending.stored); - continue; - } - if (pending.kind == core::PendingCase::Kind::Absent) { - // The store did not happen on this path: its new objects own nothing. - std::vector targets; - for (const core::Target &target : - heap.info(state, pending.subject).targets) - targets.push_back(target.object); - for (core::ObjectId id : targets) - if (state.objects.contains(id)) { - state.objects.at(id).owned = false; - state.objects.at(id).absent = true; - } - continue; - } - // Replace the conditional release by the selected one. - core::SymInfo &subject = heap.infoMut(state, pending.subject); - subject.release.reset(); - std::vector targets; - targets.reserve(subject.targets.size()); - for (const core::Target &target : subject.targets) - targets.push_back(target.object); - for (core::ObjectId id : targets) - if (state.objects.contains(id) && state.objects.at(id).record && - state.objects.at(id).record->where == pending.record.where) { - state.objects.at(id).record.reset(); - state.objects.at(id).life = core::Life::Live; - } - core::ReleaseRecord record = pending.record; - // (Its parameter test still open: a possible release, which a later - // test of the argument settles.) - bool open = pending.argumentZero && - (!state.syms.contains(pending.argumentZero->first) || - !isZeroValue(heap, state, pending.argumentZero->first)); - if (open) { - record.conditional = true; - record.allPaths = false; - } - heap.release(state, pending.subject, record); - if (open && state.syms.contains(pending.argumentZero->first)) { - core::PendingCase onArgument = pending; - onArgument.argumentZero.reset(); - onArgument.classes = - pending.argumentZero->second - ? std::vector{"zero"} - : std::vector{"positive", "negative"}; - onArgument.record = record; - heap.infoMut(state, pending.argumentZero->first) - .pending.push_back(std::move(onArgument)); - } - } - heap.infoMut(state, sym).pending = std::move(kept); -} - -bool Transfer::refine(FunctionRun &run, core::HeapState &state, - core::Sym condition, bool truth) { - bool feasible = refineCondition(run, state, condition, truth); - if (!feasible) - return false; - // Pending cases of the values the condition tested. - const core::SymInfo *info = state.syms.find(condition); - std::vector tested{condition}; - if (info != nullptr && info->condition) { - tested.push_back(info->condition->left); - if (!info->condition->rightIsConstant) - tested.push_back(info->condition->right); - const core::SymInfo *left = state.syms.find(info->condition->left); - if (left != nullptr && left->pointerBehind != core::ZeroSym) - tested.push_back(left->pointerBehind); - if (left != nullptr && left->condition) - tested.push_back(left->condition->left); - } - if (info != nullptr && info->pointerBehind != core::ZeroSym) - tested.push_back(info->pointerBehind); - for (core::Sym sym : tested) - resolvePending(run, state, sym); - // What the path now knows of the values cells held at entry. - for (core::Sym sym : tested) { - const core::SymInfo *value = state.syms.find(sym); - if (value == nullptr || !value->entryOf) - continue; - auto zero = isZeroValue(run.domain(), state, sym); - if (!zero) - continue; - core::EntryTest test{.object = value->entryOf->first, - .key = value->entryOf->second, - .zero = *zero}; - auto &tests = state.entryTests; - std::erase_if(tests, [&](const core::EntryTest &other) { - return other.object == test.object && other.key == test.key; - }); - tests.insert(std::ranges::lower_bound(tests, test), test); - } - return true; -} - -bool Transfer::refineSwitchEdge(FunctionRun &run, core::HeapState &state, - core::Sym value, QualType type, - const SwitchStmt &switchStmt, - const CFGBlock &successor, bool isDefault) { - ASTContext &context = run.ast(); - if (!type->isIntegralOrEnumerationType() || state.unreachable) - return !state.unreachable; - // A label's value converted to the promoted type of the condition (C17 - // 6.8.4.2p5): `case -1` of an `unsigned long long` is its maximum. - auto labelValue = [&](const Expr *label) -> std::optional { - Expr::EvalResult result; - if (label == nullptr || label->isValueDependent() || - !label->EvaluateAsInt(result, context)) - return std::nullopt; - llvm::APSInt converted = - result.Val.getInt().extOrTrunc(context.getIntWidth(type)); - converted.setIsUnsigned(type->isUnsignedIntegerOrEnumerationType()); - return converted; - }; - Transfer transfer(run, state); - auto holds = [&](BinaryOperatorKind kind, const llvm::APSInt &label) { - core::Sym bound = transfer.constant(label, type); - core::Sym test = transfer.compare(kind, value, bound, type); - return refine(run, state, test, true); - }; - if (isDefault) { - // No label matched. - for (const SwitchCase *label = switchStmt.getSwitchCaseList(); - label != nullptr; label = label->getNextSwitchCase()) { - const auto *single = dyn_cast(label); - if (single == nullptr || single->caseStmtIsGNURange()) - continue; - if (auto v = labelValue(single->getLHS()); v && !holds(BO_NE, *v)) - return false; - } - return true; - } - const auto *label = dyn_cast_or_null(successor.getLabel()); - if (label == nullptr) - return true; - auto low = labelValue(label->getLHS()); - if (!low) - return true; - if (!label->caseStmtIsGNURange()) - return holds(BO_EQ, *low); - auto high = labelValue(label->getRHS()); - return holds(BO_GE, *low) && (!high || holds(BO_LE, *high)); -} - -void Transfer::settlePending(FunctionRun &run, core::HeapState &state, - core::Sym sym) { - resolvePending(run, state, sym); -} - -bool Transfer::selectClass(FunctionRun &run, core::HeapState &state, - core::Sym sym, core::ResultClass resultClass) { - const core::Heap &heap = run.domain(); - const core::SymInfo &info = heap.info(state, sym); - bool feasible = true; - switch (resultClass) { - case core::ResultClass::Null: - case core::ResultClass::NonNull: - if (info.type != core::SymInfo::Type::Pointer) - return false; - feasible = refineCondition(run, state, sym, - resultClass == core::ResultClass::NonNull); - break; - case core::ResultClass::Zero: - feasible = !info.nonZero && state.zone.addRange(sym, 0, 0); - break; - case core::ResultClass::Positive: - feasible = state.zone.addRange(sym, 1, std::nullopt); - break; - case core::ResultClass::Negative: - feasible = state.zone.addRange(sym, std::nullopt, -1); - break; - } - // A comparison's truth refines its operands too (`return *out != NULL`). - if (feasible && heap.info(state, sym).condition && - resultClass != core::ResultClass::Null && - resultClass != core::ResultClass::NonNull) - feasible = refineCondition(run, state, sym, - resultClass != core::ResultClass::Zero); - if (!feasible) - return false; - resolvePending(run, state, sym); - return true; -} - -bool Transfer::refineCondition(FunctionRun &run, core::HeapState &state, - core::Sym condition, bool truth) { - // A condition its own operands reach again (as an operand evaluated - // before a joining call once made, `Heap::join`'s `keepBelow`), or one - // nested past this depth: nothing more is refined, which is always - // sound. - constexpr std::size_t MaxConditionDepth = 64; - std::vector &open = run.openConditions; - if (open.size() >= MaxConditionDepth || - std::ranges::find(open, condition) != open.end()) - return true; - struct Nested { - std::vector &open; - Nested(std::vector &at, core::Sym sym) : open(at) { - open.push_back(sym); - } - Nested(const Nested &) = delete; - Nested &operator=(const Nested &) = delete; - ~Nested() { open.pop_back(); } - } nested(open, condition); - core::Heap &heap = run.domain(); - const core::SymInfo info = heap.info(state, condition); - auto setNull = [&](core::Sym pointer, bool isNull) { - core::SymInfo &p = heap.infoMut(state, pointer); - if (isNull) { - if (p.null == core::PointerNull::NonNull) - return false; - p.null = core::PointerNull::Null; - // A failed allocation made no object on this path. - // Nor the objects the same call created inside it (a summary's - // `result->f` stores, RFC 0013). - if (p.allocatorSource) { - std::vector work; - work.reserve(p.targets.size()); - for (const core::Target &target : p.targets) - work.push_back(target.object); - std::set seen; - std::optional site; - while (!work.empty()) { - core::ObjectId id = work.back(); - work.pop_back(); - if (!seen.insert(id).second || !state.objects.contains(id)) - continue; - const core::ObjectKey &key = run.table().info(id).key; - if (key.kind != core::ObjectKind::HeapRecent && - key.kind != core::ObjectKind::HeapOld) - continue; - if (!site) - site = key.handle; - if (key.handle != *site) - continue; - state.objects.at(id).owned = false; - state.objects.at(id).absent = true; - for (const auto &[cell, held] : state.objects.at(id).cells) { - const core::SymInfo *value = state.syms.find(held); - if (value != nullptr) - for (const core::Target &target : value->targets) - work.push_back(target.object); - } - } - } - } else { - if (p.null == core::PointerNull::Null) - return false; - p.null = core::PointerNull::NonNull; - heap.markNonNull(state, pointer); - } - // A pointer the value was converted from shares the test. - return true; - }; - auto nonZero = [&](core::Sym value, bool isNonZero) -> bool { - const core::SymInfo &v = heap.info(state, value); - if (v.type == core::SymInfo::Type::Pointer) - return setNull(value, !isNonZero); - if (v.pointerBehind != core::ZeroSym && - !setNull(v.pointerBehind, !isNonZero)) - return false; - if (v.condition) { - // A nested condition: `if (!(p == NULL))`. - if (!refineCondition(run, state, value, isNonZero)) - return false; - } - if (isNonZero) { - auto lo = state.zone.lower(value); - auto hi = state.zone.upper(value); - if (lo && hi && *lo == 0 && *hi == 0) - return false; - heap.infoMut(state, value).nonZero = true; - if (lo && *lo == 0) - return state.zone.addRange(value, 1, std::nullopt); - if (hi && *hi == 0) - return state.zone.addRange(value, std::nullopt, -1); - return true; - } - if (heap.info(state, value).nonZero) - return false; - return state.zone.addRange(value, 0, 0); - }; - if (state.unreachable) - return false; - if (!info.condition) { - if (auto c = state.zone.constant(condition)) - return (*c != 0) == truth; - return nonZero(condition, truth); - } - // A decided boolean. - if (auto c = state.zone.constant(condition)) - if ((*c != 0) != truth) - return false; - core::Condition cond = *info.condition; - using Op = core::Condition::Op; - if (cond.op == Op::NonZero) - return nonZero(cond.left, truth); - // A true `&&` or a false `||` decides both operands; the other outcome - // says nothing of either. - if (cond.op == Op::And || cond.op == Op::Or) { - if ((cond.op == Op::And) != truth) - return true; - return refineCondition(run, state, cond.left, truth) && - refineCondition(run, state, cond.right, truth); - } - Op op = cond.op; - if (!truth) { - switch (op) { - case Op::Eq: - op = Op::Ne; - break; - case Op::Ne: - op = Op::Eq; - break; - case Op::Lt: - op = Op::Ge; - break; - case Op::Le: - op = Op::Gt; - break; - case Op::Gt: - op = Op::Le; - break; - case Op::Ge: - op = Op::Lt; - break; - case Op::NonZero: - case Op::And: - case Op::Or: - break; - } - } - const core::SymInfo &left = heap.info(state, cond.left); - if (left.type == core::SymInfo::Type::Pointer) { - if (cond.rightIsConstant && cond.constant == 0) { - if (op == Op::Eq) - return setNull(cond.left, true); - if (op == Op::Ne) - return setNull(cond.left, false); - } - // Two pointers into one object compare as their offsets (`p < buf + - // n`, RFC 0031 §5.1). - if (cond.rightIsConstant) - return true; - // RFC 0014: the path remembers how two pointers compared. - if ((op == Op::Eq || op == Op::Ne) && - heap.info(state, cond.right).type == core::SymInfo::Type::Pointer && - !core::Heap::assumePointersEqual(state, cond.left, cond.right, - op == Op::Eq)) - return false; - // A pointer unequal to one naming exactly one place does not point - // there (`if (line != sentinel) free(line);` never frees the sentinel). - // A null other pointer names no place, unless it is the value at entry - // that an entry object stands behind: null there, that object does not - // exist, so nothing points to it either. - if (op == Op::Ne) { - const core::SymInfo &other = heap.info(state, cond.right); - auto names = [&](const core::SymInfo &value) { - if (value.null == core::PointerNull::NonNull) - return true; - return value.entryOf.has_value() && value.targets.size() == 1 && - run.table().info(value.targets[0].object).key.kind == - core::ObjectKind::Entry && - value.targets[0].offset.isConstant() && - value.targets[0].offset.constant == 0; - }; - if (other.type == core::SymInfo::Type::Pointer && !other.top && - names(other) && other.targets.size() == 1 && - other.targets[0].offset.isConstant() && - run.table().info(other.targets[0].object).singular && !left.top && - left.targets.size() > 1) { - const core::Target place = other.targets[0]; - core::SymInfo &narrowed = heap.infoMut(state, cond.left); - std::erase_if(narrowed.targets, [&](const core::Target &target) { - return target.object == place.object && target.offset.isConstant() && - target.offset.constant == place.offset.constant; - }); - // (Nothing left: null, or no such path.) - if (narrowed.targets.empty()) { - if (narrowed.null == core::PointerNull::NonNull) - return false; - narrowed.null = core::PointerNull::Null; - } - return true; - } - } - const core::SymInfo &right = heap.info(state, cond.right); - if (right.type != core::SymInfo::Type::Pointer || left.top || right.top || - left.targets.size() != 1 || right.targets.size() != 1 || - left.targets[0].object != right.targets[0].object || - !run.table().info(left.targets[0].object).singular) - return true; - const core::Term l = left.targets[0].offset; - const core::Term r = right.targets[0].offset; - auto linear = [](const core::Term &term) { - return term.known && (term.isConstant() || term.scale == 1); - }; - if (!linear(l) || !linear(r)) - return true; - core::Sym x = l.isConstant() ? core::ZeroSym : l.var; - core::Sym y = r.isConstant() ? core::ZeroSym : r.var; - // x + cl (op) y + cr - std::int64_t cl = l.constant; - std::int64_t cr = r.constant; - bool ok = true; - auto le = [&](core::Sym a, core::Sym b, __int128 c) { - if (c < INT64_MIN || c > INT64_MAX) - return; - if (a == b) { - ok = ok && c >= 0; - return; - } - ok = ok && state.zone.addLE(a, b, static_cast(c)); - }; - __int128 gap = static_cast<__int128>(cr) - cl; - switch (op) { - case Op::Lt: - le(x, y, gap - 1); - break; - case Op::Le: - le(x, y, gap); - break; - case Op::Gt: - le(y, x, -gap - 1); - break; - case Op::Ge: - le(y, x, -gap); - break; - case Op::Eq: - le(x, y, gap); - le(y, x, -gap); - break; - case Op::Ne: - if (x == y && gap == 0) - return false; - break; - case Op::NonZero: - case Op::And: - case Op::Or: - break; - } - return ok && !state.zone.isBottom(); - } - bool ok = true; - // §4.4: an operand whose interval the zone cannot hold (an unsigned - // 64-bit value above `INT64_MAX`) is refined by the intervals. - auto integerOp = [](Op relation) { - switch (relation) { - case Op::Eq: - return core::IntegerOp::Equal; - case Op::Ne: - return core::IntegerOp::NotEqual; - case Op::Lt: - return core::IntegerOp::Less; - case Op::Le: - return core::IntegerOp::LessEqual; - case Op::Gt: - return core::IntegerOp::Greater; - case Op::Ge: - case Op::NonZero: - case Op::And: - case Op::Or: - break; - } - return core::IntegerOp::GreaterEqual; - }; - auto refineValues = [&](core::Sym x, Op relation, - const std::optional &other) { - const core::SymInfo &xi = heap.info(state, x); - if (!ok || !other || xi.type != core::SymInfo::Type::Int || !xi.intType || - other->type != *xi.intType || relation == Op::NonZero) - return; - core::IntegerRange range = - rangeOf(state, x, *xi.intType).satisfying(integerOp(relation), *other); - if (range.empty()) { - ok = false; - return; - } - boundBy(heap, state, x, range); - }; - auto hasValues = [&](core::Sym x) { - const core::SymInfo *xi = state.syms.find(x); - return xi != nullptr && xi->values.has_value(); - }; - auto le = [&](core::Sym x, core::Sym y, std::int64_t c) { - ok = ok && state.zone.addLE(x, y, c); - // A linear operand (`i + 1 < n`) refines its base too. - const core::SymInfo &xi = heap.info(state, x); - // (A bound 64 bits cannot hold is not added.) - std::int64_t shifted = 0; - if (ok && xi.linear && xi.linear->scale == 1 && - xi.linear->var != core::ZeroSym && - !__builtin_sub_overflow(c, xi.linear->constant, &shifted)) - ok = state.zone.addLE(xi.linear->var, y, shifted); - const core::SymInfo &yi = heap.info(state, y); - if (ok && yi.linear && yi.linear->scale == 1 && - yi.linear->var != core::ZeroSym && - !__builtin_add_overflow(c, yi.linear->constant, &shifted)) - ok = state.zone.addLE(x, yi.linear->var, shifted); - }; - if (cond.rightIsConstant) { - std::int64_t c = cond.constant; - // `x <= c - 1` and `x >= c + 1`, `x >= c`, where 64 bits hold the bound. - auto below = [&] { - if (c != INT64_MIN) - le(cond.left, core::ZeroSym, c - 1); - }; - auto above = [&] { - if (c != INT64_MAX) - le(core::ZeroSym, cond.left, -(c + 1)); - }; - auto atLeast = [&] { - if (c != INT64_MIN) - le(core::ZeroSym, cond.left, -c); - }; - // RFC 0034 §6.2: the constants the value is known not to equal, which - // the zone's bounds cannot hold (`t != DIRECT` with `t` in [0, 3]). - const core::SymInfo &tested = heap.info(state, cond.left); - switch (op) { - case Op::Eq: - if ((c == 0 && tested.nonZero) || - std::ranges::binary_search(tested.excluded, c)) - return false; - le(cond.left, core::ZeroSym, c); - atLeast(); - break; - case Op::Ne: { - auto lo = state.zone.lower(cond.left); - auto hi = state.zone.upper(cond.left); - if (lo && hi && *lo == c && *hi == c) - return false; - if (lo && *lo == c) { - above(); - } else if (hi && *hi == c) { - below(); - } else if ((!lo || *lo < c) && (!hi || *hi > c)) { - // (Zero too: `nonZero` stays what a truth test makes, which the - // summary's entry tests read.) - core::SymInfo &value = heap.infoMut(state, cond.left); - auto at = std::ranges::lower_bound(value.excluded, c); - if (value.excluded.size() < core::MaxExcluded && - (at == value.excluded.end() || *at != c)) - value.excluded.insert(at, c); - } - break; - } - case Op::Lt: - below(); - break; - case Op::Le: - le(cond.left, core::ZeroSym, c); - break; - case Op::Gt: - above(); - break; - case Op::Ge: - atLeast(); - break; - case Op::NonZero: - case Op::And: - case Op::Or: - break; - } - if (ok && left.pointerBehind != core::ZeroSym && c == 0) { - if (op == Op::Eq) - ok = setNull(left.pointerBehind, true); - else if (op == Op::Ne) - ok = setNull(left.pointerBehind, false); - } - if (auto type = heap.info(state, cond.left).intType; - hasValues(cond.left) && type) - refineValues(cond.left, op, - core::IntegerRange::singleton(core::IntegerValue::ofBits( - *type, static_cast(c)))); - // A truth value compared with 0 or 1 (`(p != NULL) == 0`): the - // comparison it stands for holds or fails. - if (ok && (op == Op::Eq || op == Op::Ne) && (c == 0 || c == 1) && - heap.info(state, cond.left).condition) - ok = refineCondition(run, state, cond.left, (op == Op::Eq) == (c == 1)); - return ok && !state.zone.isBottom(); - } - core::Sym r = cond.right; - // RFC 0017 §3: `x <= K / y` (a range guard) bounds the product `x * y` - // by `K`. - auto noteGuard = [&](core::Sym x, core::Sym quotient) { - const core::SymInfo *q = state.syms.find(quotient); - if (!ok || q == nullptr || !q->defined || - q->defined->op != core::IntegerOp::Divide || - q->defined->right == core::ZeroSym || q->defined->constant) - return; - std::optional dividend; - if (const auto &k = q->defined->leftValue; k && !k->negative()) - dividend = k->magnitude(); - const core::SymInfo *xi = state.syms.find(x); - if (!dividend || xi == nullptr || xi->type != core::SymInfo::Type::Int) - return; - heap.infoMut(state, x).productAtMost = - std::make_pair(q->defined->right, *dividend); - }; - switch (op) { - case Op::Lt: - case Op::Le: - noteGuard(cond.left, r); - break; - case Op::Gt: - case Op::Ge: - noteGuard(r, cond.left); - break; - default: - break; - } - if (hasValues(cond.left) || hasValues(r)) { - const core::SymInfo &li = heap.info(state, cond.left); - const core::SymInfo &ri = heap.info(state, r); - if (li.intType && ri.intType && *li.intType == *ri.intType) { - core::IntegerRange leftRange = rangeOf(state, cond.left, *li.intType); - core::IntegerRange rightRange = rangeOf(state, r, *ri.intType); - refineValues(cond.left, op, rightRange); - Op reversed = op; - switch (op) { - case Op::Lt: - reversed = Op::Gt; - break; - case Op::Le: - reversed = Op::Ge; - break; - case Op::Gt: - reversed = Op::Lt; - break; - case Op::Ge: - reversed = Op::Le; - break; - default: - break; - } - refineValues(r, reversed, leftRange); - } - } - switch (op) { - case Op::Eq: - le(cond.left, r, 0); - le(r, cond.left, 0); - break; - case Op::Ne: - break; - case Op::Lt: - le(cond.left, r, -1); - break; - case Op::Le: - le(cond.left, r, 0); - break; - case Op::Gt: - le(r, cond.left, -1); - break; - case Op::Ge: - le(r, cond.left, 0); - break; - case Op::NonZero: - case Op::And: - case Op::Or: - break; - } - return ok && !state.zone.isBottom(); -} - -} // namespace weavec::analysis::engine diff --git a/lib/Analysis/EngineIntegers.h b/lib/Analysis/EngineIntegers.h deleted file mode 100644 index ad44b187..00000000 --- a/lib/Analysis/EngineIntegers.h +++ /dev/null @@ -1,148 +0,0 @@ -//===- EngineIntegers.h - Clang target types and numeric operators --------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// - -#ifndef WEAVEC_LIB_ANALYSIS_ENGINEINTEGERS_H -#define WEAVEC_LIB_ANALYSIS_ENGINEINTEGERS_H - -#include "weavec/Core/Integer.h" - -#include "clang/AST/ASTContext.h" -#include "clang/AST/Decl.h" -#include "clang/AST/Expr.h" -#include "clang/AST/OperationKinds.h" -#include "clang/AST/Type.h" -#include "clang/Basic/Builtins.h" - -#include - -namespace weavec::analysis { - -[[nodiscard]] inline std::optional -checkedIntegerOp(const clang::CallExpr &call) { - // The value rule of the overflow-checking compiler builtins, by their - // builtin id (RFC 0030 §8: their rows give only the write through the - // result pointer). - const auto *callee = call.getDirectCallee(); - if (!callee || call.getNumArgs() != 3) - return std::nullopt; - switch (callee->getBuiltinID()) { - case clang::Builtin::BI__builtin_add_overflow: - case clang::Builtin::BI__builtin_sadd_overflow: - case clang::Builtin::BI__builtin_saddl_overflow: - case clang::Builtin::BI__builtin_saddll_overflow: - case clang::Builtin::BI__builtin_uadd_overflow: - case clang::Builtin::BI__builtin_uaddl_overflow: - case clang::Builtin::BI__builtin_uaddll_overflow: - return core::IntegerOp::Add; - case clang::Builtin::BI__builtin_sub_overflow: - case clang::Builtin::BI__builtin_ssub_overflow: - case clang::Builtin::BI__builtin_ssubl_overflow: - case clang::Builtin::BI__builtin_ssubll_overflow: - case clang::Builtin::BI__builtin_usub_overflow: - case clang::Builtin::BI__builtin_usubl_overflow: - case clang::Builtin::BI__builtin_usubll_overflow: - return core::IntegerOp::Subtract; - case clang::Builtin::BI__builtin_mul_overflow: - case clang::Builtin::BI__builtin_smul_overflow: - case clang::Builtin::BI__builtin_smull_overflow: - case clang::Builtin::BI__builtin_smulll_overflow: - case clang::Builtin::BI__builtin_umul_overflow: - case clang::Builtin::BI__builtin_umull_overflow: - case clang::Builtin::BI__builtin_umulll_overflow: - return core::IntegerOp::Multiply; - default: - return std::nullopt; - } -} - -[[nodiscard]] inline std::optional -integerTypeOf(clang::QualType type, const clang::ASTContext &context) { - if (type.isNull()) - return std::nullopt; - if (const auto *atomic = type->getAs()) - type = atomic->getValueType(); - if (!type->isIntegerType() || type->isDependentType()) - return std::nullopt; - if (type->isBooleanType()) - return core::BooleanType; - const unsigned width = context.getIntWidth(type); - if (width == 0 || width > 64) - return std::nullopt; - return core::IntegerType{.width = width, - .isSigned = type->isSignedIntegerType(), - .isBoolean = false}; -} - -[[nodiscard]] inline std::optional -integerTypeOf(const clang::ValueDecl &decl, const clang::ASTContext &context) { - auto type = integerTypeOf(decl.getType(), context); - const auto *field = llvm::dyn_cast(&decl); - if (type && field && field->isBitField() && - !field->getBitWidth()->isValueDependent()) { - const auto width = field->getBitWidthValue(); - if (width == 0 || width > type->width) - return std::nullopt; - type->width = width; - } - return type; -} - -[[nodiscard]] inline std::optional -integerOpOf(clang::BinaryOperatorKind op) { - using enum core::IntegerOp; - switch (op) { - case clang::BO_Add: - case clang::BO_AddAssign: - return Add; - case clang::BO_Sub: - case clang::BO_SubAssign: - return Subtract; - case clang::BO_Mul: - case clang::BO_MulAssign: - return Multiply; - case clang::BO_Div: - case clang::BO_DivAssign: - return Divide; - case clang::BO_Rem: - case clang::BO_RemAssign: - return Remainder; - case clang::BO_Shl: - case clang::BO_ShlAssign: - return ShiftLeft; - case clang::BO_Shr: - case clang::BO_ShrAssign: - return ShiftRight; - case clang::BO_And: - case clang::BO_AndAssign: - return BitAnd; - case clang::BO_Or: - case clang::BO_OrAssign: - return BitOr; - case clang::BO_Xor: - case clang::BO_XorAssign: - return BitXor; - case clang::BO_EQ: - return Equal; - case clang::BO_NE: - return NotEqual; - case clang::BO_LT: - return Less; - case clang::BO_LE: - return LessEqual; - case clang::BO_GT: - return Greater; - case clang::BO_GE: - return GreaterEqual; - default: - return std::nullopt; - } -} - -} // namespace weavec::analysis - -#endif // WEAVEC_LIB_ANALYSIS_ENGINEINTEGERS_H diff --git a/lib/Analysis/EngineInvariants.cpp b/lib/Analysis/EngineInvariants.cpp deleted file mode 100644 index 2cac39c4..00000000 --- a/lib/Analysis/EngineInvariants.cpp +++ /dev/null @@ -1,286 +0,0 @@ -//===- EngineInvariants.cpp - Counted-field invariants (RFC 0031) ---------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// RFC 0030 §7.6, restored by RFC 0031 *Implementation amendments* -// ("Counted-field invariants"): Houdini over `KindInference`'s candidates -// `count(d) == f + c` and `bytes(d) == f + c`. Every candidate is assumed -// at entry; a checking run of each function that writes `d` or `f` -// refutes the candidates an object it hands out (at a call, or at its -// exit) breaks; the survivors are exact extents of `d`, with which the -// functions that read `d` are analysed once more. -// -//===----------------------------------------------------------------------===// - -#include "Engine.h" - -#include "clang/AST/RecordLayout.h" -#include "clang/AST/RecursiveASTVisitor.h" - -using namespace clang; - -namespace weavec::analysis::engine { - -namespace { -/// Which of `fields` a body writes (an assignment, an increment, an -/// initializer of their record) and which it reads. -class FieldUses : public RecursiveASTVisitor { -public: - FieldUses(const std::set &fields, - const std::set &records) - : fields(fields), records(records) {} - bool writes = false; - bool reads = false; - - // NOLINTBEGIN(readability-identifier-naming,bugprone-derived-method-shadowing-base-method): - // RecursiveASTVisitor's CRTP hooks are found by name. - bool VisitBinaryOperator(BinaryOperator *op) { - if (op->isAssignmentOp()) - noteWrite(op->getLHS()); - return true; - } - bool VisitUnaryOperator(UnaryOperator *op) { - if (op->isIncrementDecrementOp()) - noteWrite(op->getSubExpr()); - return true; - } - bool VisitInitListExpr(InitListExpr *init) { - if (const RecordDecl *record = init->getType()->getAsRecordDecl()) - writes = writes || records.contains(record->getDefinition()); - return true; - } - bool VisitMemberExpr(MemberExpr *member) { - if (const auto *field = dyn_cast(member->getMemberDecl())) - reads = reads || fields.contains(field); - return true; - } - // NOLINTEND(readability-identifier-naming,bugprone-derived-method-shadowing-base-method) - -private: - const std::set &fields; - const std::set &records; - void noteWrite(const Expr *lhs) { - if (const auto *member = dyn_cast(lhs->IgnoreParenImpCasts())) - if (const auto *field = dyn_cast(member->getMemberDecl())) - writes = writes || fields.contains(field); - } -}; -} // namespace - -const KindEntry *UnitRun::fieldKind(const FieldDecl &field) const { - const KindEntry *entry = input.kinds.field(field); - if (entry != nullptr && - (core::hasExtent(entry->kind.shape) || entry->hasDeclaredShape())) - return entry; - auto assumed = assumedFields.find(&field); - return assumed != assumedFields.end() ? &assumed->second : entry; -} - -/// The byte offset of `field` in its record. -static std::int64_t fieldOffset(const ASTContext &context, - const FieldDecl &field) { - const ASTRecordLayout &layout = context.getASTRecordLayout(field.getParent()); - return static_cast( - layout.getFieldOffset(field.getFieldIndex()) / context.getCharWidth()); -} - -/// The element size a `count(d)` candidate counts in, or none. -static std::optional elementSize(const ASTContext &context, - const FieldDecl &pointer) { - QualType pointee = pointer.getType()->getPointeeType(); - if (pointee.isNull() || pointee->isIncompleteType() || - pointee->isVoidType() || pointee->isFunctionType()) - return std::nullopt; - return static_cast( - context.getTypeSizeInChars(pointee).getQuantity()); -} - -void FunctionRun::checkInvariants(core::HeapState state, - core::ObjectId object) const { - const core::ObjectInfo &info = objects.info(object); - QualType type = info.type != 0 ? typeOfHandle(info.type) : QualType(); - const RecordDecl *record = type.isNull() ? nullptr : type->getAsRecordDecl(); - if (record == nullptr || record->getDefinition() == nullptr) - return; - record = record->getDefinition(); - for (const ResolvedCandidate *candidate : unit.standing) { - if (candidate->record != record || unit.refuted.contains(candidate)) - continue; - // `d` null holds whatever `f` is (the extent of no object); otherwise - // `d` points to the start of one object whose extent is `f + c` - // elements (or bytes). - std::int64_t scale = 1; - if (!candidate->candidate.bytes) { - auto size = elementSize(context, *candidate->pointer); - if (!size || *size <= 0) { - unit.refuted.insert(candidate); - continue; - } - scale = *size; - } - auto read = [&](const FieldDecl &field) { - core::CellKey key{.offset = fieldOffset(context, field)}; - core::SymInfo hint; - hint.type = field.getType()->isPointerType() - ? core::SymInfo::Type::Pointer - : core::SymInfo::Type::Int; - if (auto held = heap.read(state, object, key)) - return *held; - return unwritten(state, object, key, hint); - }; - core::Sym pointer = read(*candidate->pointer); - core::Sym count = read(*candidate->count); - const core::SymInfo &value = heap.info(state, pointer); - if (value.type == core::SymInfo::Type::Pointer && - value.null == core::PointerNull::Null) - continue; - // `d` as it was at entry: its extent is the assumed candidate's, so - // only that one is decided, and only when `f` changed; another - // candidate over a changed `f` cannot be shown. - if (value.entryOf) { - if (heap.info(state, count).entryOf) - continue; - auto assumed = unit.assumedCandidate.find(candidate->pointer); - if (assumed == unit.assumedCandidate.end() || - assumed->second != candidate) { - unit.refuted.insert(candidate); - continue; - } - } - bool holds = false; - if (value.type == core::SymInfo::Type::Pointer && !value.top && - value.targets.size() == 1 && - value.targets[0].offset == core::Term::of(0)) { - const core::ObjectState *target = - heap.findObject(state, value.targets[0].object); - if (target != nullptr && target->extent && - target->extent->cls == core::ExtentClass::Exact && - target->extent->bytes.known) { - core::Term expected = core::Term::ofSym( - count, scale, candidate->candidate.offset * scale); - // Equal, or the size type's reduction of it (`malloc(n * sizeof - // *d)` with `f = n`): what `f * sizeof *d` computes in C. - holds = - (heap.lessEqual(state, target->extent->bytes, expected) == true && - heap.lessEqual(state, expected, target->extent->bytes) == true) || - (target->extent->unwrapped && target->extent->unwrapped->known && - target->extent->unwrapped->var == count && - target->extent->unwrapped->scale == scale && - target->extent->unwrapped->constant == - candidate->candidate.offset * scale); - } - } - if (!holds) - unit.refuted.insert(candidate); - // (A value this function stored that meets it: a witness.) - else if (!value.entryOf) - unit.witnessed.insert(candidate); - } -} - -void UnitRun::inferInvariants( - const std::vector &definitions, - const std::function &shouldReport) { - static constexpr int MaxRounds = 4; - if (standing.empty()) - return; - std::set fields; - std::set pointers; - std::set records; - for (const ResolvedCandidate *candidate : standing) { - fields.insert(candidate->pointer); - fields.insert(candidate->count); - pointers.insert(candidate->pointer); - records.insert(candidate->record); - } - std::vector writers; - std::vector readers; - for (const FunctionDecl *fn : definitions) { - FieldUses writes(fields, records); - writes.TraverseStmt(fn->getBody()); - if (writes.writes) - writers.push_back(fn); - FieldUses reads(pointers, records); - reads.TraverseStmt(fn->getBody()); - if (reads.reads) - readers.push_back(fn); - } - if (readers.empty()) { - standing.clear(); - return; - } - auto assume = [&] { - assumedFields.clear(); - assumedCandidate.clear(); - for (const ResolvedCandidate *candidate : standing) { - if (assumedFields.contains(candidate->pointer)) - continue; - assumedCandidate.emplace(candidate->pointer, candidate); - KindEntry entry; - core::ExtentTerm extent = core::ExtentTerm::of( - core::ExtentPath::ofField(candidate->count->getNameAsString()), 1, - candidate->candidate.offset); - entry.kind = candidate->candidate.bytes - ? core::PointerKind::sized(extent) - : core::PointerKind::counted(extent); - entry.kind.source = core::KindSource::Inferred; - entry.extentClass = core::ExtentClass::Exact; - assumedFields.emplace(candidate->pointer, std::move(entry)); - } - }; - bool settled = false; - for (int round = 0; round < MaxRounds && !settled; ++round) { - assume(); - refuted.clear(); - witnessed.clear(); - for (const FunctionDecl *fn : writers) { - // (Every candidate refuted: the rest of the round decides nothing.) - if (refuted.size() == standing.size()) - break; - FunctionRun run(*this, *fn, discarding, RunMode::Summary); - run.checkingInvariants = true; - (void)run.run(); - } - settled = refuted.empty(); - // A witness is a value the writer stored itself; with fewer - // candidates assumed a later round finds no more of them, so a round - // that witnesses none it keeps decides that none stands. - if (std::ranges::none_of(standing, [&](const ResolvedCandidate *c) { - return witnessed.contains(c) && !refuted.contains(c); - })) { - standing.clear(); - break; - } - std::erase_if(standing, [&](const ResolvedCandidate *candidate) { - return refuted.contains(candidate); - }); - } - refuted.clear(); - // (Still refuting after the last round: none stands. One no function - // establishes would only replace what the kinds give.) - if (!settled) - standing.clear(); - std::erase_if(standing, [&](const ResolvedCandidate *candidate) { - return !witnessed.contains(candidate); - }); - assume(); - if (assumedFields.empty()) - return; - // The readers once more, their rows replacing the first run's; their - // summaries stay what callers already used. - for (const FunctionDecl *fn : readers) { - if (!shouldReport(*fn)) - continue; - authoritative.beginFunction(*fn); - FunctionRun run(*this, *fn, authoritative, RunMode::Authoritative); - if (run.run().overBudget) - authoritative.overBudget(*fn); - } - assumedFields.clear(); -} - -} // namespace weavec::analysis::engine diff --git a/lib/Analysis/EngineKinds.cpp b/lib/Analysis/EngineKinds.cpp deleted file mode 100644 index 1bda4806..00000000 --- a/lib/Analysis/EngineKinds.cpp +++ /dev/null @@ -1,549 +0,0 @@ -//===- EngineKinds.cpp - Pointer kinds in the object engine ---------------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// RFC 0030 §7.2–§7.5 over the object domain: what `KindInference` found -// seeds the entry objects' extents and nullness, strengthens the decisions -// of the accesses a requirement covers, and becomes requirement records at -// the calls that must meet it. -// -//===----------------------------------------------------------------------===// - -#include "Engine.h" -#include "weavec/Analysis/ClangLocation.h" -#include "weavec/Analysis/KindTable.h" - -#include "clang/AST/RecordLayout.h" -#include "clang/Basic/SourceManager.h" - -using namespace clang; - -namespace weavec::analysis::engine { - -/// §7.5: the requirement's guard holds whatever the arguments (`0 < 8`). -static bool alwaysHolds(const RequirementGuard &guard) { - if (!guard.lhs.isConstant() || !guard.rhs.isConstant()) - return false; - return guard.relation == RequirementGuard::Relation::Less - ? guard.lhs.offset < guard.rhs.offset - : guard.lhs.offset <= guard.rhs.offset; -} - -/// §7.5: a guarded count that is zero or less whenever its guard fails. -static bool vacuousWhenFalse(const MustAccessRequirement &requirement) { - if (!requirement.guard) - return true; - const RequirementGuard &guard = *requirement.guard; - const core::PointerKind &kind = requirement.kind; - if (kind.shape != core::PointerShape::Counted && - kind.shape != core::PointerShape::Sized) - return false; - if (!guard.lhs.isConstant() || guard.rhs.isConstant() || - kind.extent.isConstant() || kind.extent.path != guard.rhs.path || - kind.extent.scale != guard.rhs.scale || guard.rhs.scale <= 0) - return false; - std::int64_t last = guard.lhs.offset; - if (guard.relation == RequirementGuard::Relation::LessEqual) - last -= 1; - return last + (kind.extent.offset - guard.rhs.offset) <= 0; -} - -/// §7.5: whether the null part of `requirement` is what the calls check. -static bool nullEnforced(const KindEntry &entry, - const MustAccessRequirement &requirement) { - if (requirement.kind.nullability != core::Nullability::Nonnull) - return false; - const MustAccessRequirement *first = nullptr; - for (const MustAccessRequirement &each : entry.mustAccess) { - if (each.kind.nullability != core::Nullability::Nonnull) - continue; - if (!each.guard) - return true; - if (first == nullptr) - first = &each; - } - return first != nullptr && first->guard == requirement.guard; -} - -/// The bytes of one element of a `T *` (1 for `void`). -static std::optional elementBytes(const ASTContext &context, - QualType pointee) { - if (pointee.isNull()) - return std::nullopt; - if (pointee->isVoidType()) - return 1; - if (pointee->isIncompleteType() || pointee->isFunctionType() || - !pointee->isConstantSizeType()) - return std::nullopt; - return static_cast( - context.getTypeSizeInChars(pointee).getQuantity()); -} - -/// §7.1: the bytes a Single `T *` guarantees: the object width. -static std::optional singleWidth(const ASTContext &context, - QualType pointee) { - if (pointee.isNull()) - return std::nullopt; - if (pointee->isVoidType()) - return 1; - auto size = elementBytes(context, pointee); - if (!size) - return std::nullopt; - if (const RecordDecl *record = pointee->getAsRecordDecl(); - record != nullptr && !record->isUnion() && - record->isCompleteDefinition()) { - const FieldDecl *last = nullptr; - for (const FieldDecl *field : record->fields()) - last = field; - if (last != nullptr && - (last->getType()->isIncompleteArrayType() || - (context.getAsConstantArrayType(last->getType()) != nullptr && - context.getLangOpts().getStrictFlexArraysLevel() == - LangOptions::StrictFlexArraysLevelKind::Default))) { - const ASTRecordLayout &layout = context.getASTRecordLayout(record); - return static_cast( - layout.getFieldOffset(last->getFieldIndex()) / - context.getCharWidth()); - } - } - return size; -} - -std::optional -FunctionRun::paramExtent(unsigned index, const std::vector &values, - bool &nonnull) const { - const KindEntry *entry = unit.input.kinds.param(function, index); - if (entry == nullptr) - return std::nullopt; - QualType pointee = function.getParamDecl(index)->getType()->getPointeeType(); - auto termBytes = [&](const core::ExtentTerm &term, - std::int64_t unit) -> std::optional { - if (term.isConstant()) - return core::Term::of(term.offset * unit); - if (term.path->root != core::ExtentPath::Root::Param || - term.path->param >= values.size() || - values[term.path->param] == core::ZeroSym) - return std::nullopt; - if (term.scale * unit <= 0) - return std::nullopt; - return core::Term::ofSym(values[term.path->param], term.scale * unit, - term.offset * unit); - }; - auto extentOf = [&](const core::PointerKind &kind, - core::ExtentClass cls) -> std::optional { - std::optional bytes; - switch (kind.shape) { - case core::PointerShape::Single: - if (auto width = singleWidth(context, pointee)) - bytes = core::Term::of(*width); - cls = core::ExtentClass::LowerBound; - break; - case core::PointerShape::Counted: - if (auto elementSize = elementBytes(context, pointee)) - bytes = termBytes(kind.extent, *elementSize); - break; - case core::PointerShape::Sized: - bytes = termBytes(kind.extent, 1); - break; - default: - break; - } - if (!bytes) - return std::nullopt; - return core::Extent{.bytes = *bytes, .cls = cls}; - }; - std::optional extent; - nonnull = entry->kind.nullability == core::Nullability::Nonnull && - (entry->kind.source == core::KindSource::Inferred || - entry->declaresNonnull()); - if (entry->hasEnforcedRequirement() && !entry->hasDeclaredShape()) - for (const MustAccessRequirement &requirement : entry->mustAccess) { - bool unguarded = !requirement.guard || alwaysHolds(*requirement.guard); - if (unguarded && - requirement.kind.nullability == core::Nullability::Nonnull && - entry->enforcement == RequirementEnforcement::CallSites) - nonnull = true; - if (!extent && (unguarded || vacuousWhenFalse(requirement))) - extent = extentOf(requirement.kind, core::ExtentClass::LowerBound); - } - if (entry->mainArgv) { - extent = extentOf(entry->kind, core::ExtentClass::Declared); - nonnull = true; - } - if (!extent && entry->hasShape() && !entry->shapeFromSystemHeader()) - extent = - extentOf(entry->kind, - entry->extentClass.value_or(core::ExtentClass::LowerBound)); - // A1's Single default, unless the kind is Unknown: a static callee some - // caller passes a cursor to relies on nothing (§7.3). - if (!extent && (entry->hasShape() || entry->shapeFromSystemHeader())) - if (auto width = singleWidth(context, pointee)) - extent = core::Extent{.bytes = core::Term::of(*width), - .cls = core::ExtentClass::LowerBound}; - return extent; -} - -bool Transfer::isArgvElement(const Expr &pointer) const { - const auto *element = - dyn_cast(pointer.IgnoreParenImpCasts()); - const auto *ref = - element != nullptr - ? dyn_cast(element->getBase()->IgnoreParenImpCasts()) - : nullptr; - const auto *param = - ref != nullptr ? dyn_cast(ref->getDecl()) : nullptr; - if (param == nullptr || param->getDeclContext() != &run.decl()) - return false; - const KindEntry *entry = run.unitRun().input.kinds.param( - run.decl(), param->getFunctionScopeIndex()); - return entry != nullptr && entry->mainArgv; -} - -core::FacetDecision Transfer::covered(const SiteInfo &site, core::Facet facet, - core::FacetDecision decision) const { - if ((facet != core::Facet::Spatial && facet != core::Facet::Null) || - decision.outcome == core::SiteOutcome::Violation || - decision.outcome == core::SiteOutcome::Proven || - decision.outcome == core::SiteOutcome::Trusted) - return decision; - const FunctionDecl &function = run.decl(); - const KindTable &kinds = run.unitRun().input.kinds; - // §7.3: an element of `main`'s argv is nul-terminated. - if (facet == core::Facet::Spatial && site.kind == core::SiteKind::Deref && - site.operand != nullptr && isArgvElement(*site.operand)) - return core::FacetDecision::trustedFor(core::TrustReason::SystemApi, - "an element of 'argv' is a " - "nul-terminated string"); - if (isa(site.stmt)) - return decision; - for (const CoveringRequirement &cover : kinds.covering(*site.stmt)) { - if (cover.function != function.getCanonicalDecl()) - continue; - const KindEntry *entry = kinds.param(function, cover.param); - if (entry == nullptr || cover.requirement >= entry->mustAccess.size() || - !entry->enforcement) - continue; - const MustAccessRequirement &requirement = - entry->mustAccess[cover.requirement]; - if (entry->hasDeclaredShape()) { - if (facet == core::Facet::Spatial && !entry->shapeFromSystemHeader() && - entry->kind.sameShape(requirement.kind)) - return core::FacetDecision::proven(); - continue; - } - if (*entry->enforcement == RequirementEnforcement::CallSites) { - if (facet == core::Facet::Spatial || nullEnforced(*entry, requirement)) - return core::FacetDecision::proven(); - continue; - } - if (facet == core::Facet::Spatial && - decision.outcome == core::SiteOutcome::Unresolved && - requirement.kind.shape != core::PointerShape::Single) - return core::FacetDecision::trustedFor( - core::TrustReason::CallerContract, - "'" + function.getNameAsString() + "' requires " + - requirement.toString() + " of '" + - function.getParamDecl(cover.param)->getNameAsString() + - "' from its callers"); - } - return decision; -} - -std::optional -Transfer::coveredArgument(const CallExpr &call, core::Sym pointer, - bool string) const { - // The argument is a parameter's own value: its object, at its start. - const core::SymInfo &value = heap.info(state, pointer); - if (value.type != core::SymInfo::Type::Pointer || value.top || - value.targets.size() != 1 || - !(value.targets[0].offset == core::Term::of(0))) - return std::nullopt; - const core::ObjectInfo &info = run.table().info(value.targets[0].object); - if (info.key.kind != core::ObjectKind::Entry || !info.key.path.isParam() || - info.key.path.steps.size() != 1 || - info.key.path.steps.front().step != core::PathStep::Deref) - return std::nullopt; - const FunctionDecl &function = run.decl(); - const KindTable &kinds = run.unitRun().input.kinds; - for (const CoveringRequirement &cover : kinds.covering(call)) { - if (cover.function != function.getCanonicalDecl() || - cover.param != info.key.path.index) - continue; - const KindEntry *entry = kinds.param(function, cover.param); - if (entry == nullptr || cover.requirement >= entry->mustAccess.size() || - !entry->enforcement || entry->hasDeclaredShape()) - continue; - const MustAccessRequirement &requirement = - entry->mustAccess[cover.requirement]; - bool matches = - string ? requirement.kind.shape == core::PointerShape::NulTerminated - : requirement.kind.shape != core::PointerShape::NulTerminated && - requirement.kind.shape != core::PointerShape::Unknown; - if (!matches) - continue; - // §7.5: the static callers check it, or the callers are trusted with it. - if (*entry->enforcement == RequirementEnforcement::CallSites) - return core::FacetDecision::proven(); - return core::FacetDecision::trustedFor( - core::TrustReason::CallerContract, - "'" + function.getNameAsString() + "' requires " + - requirement.toString() + " of '" + - function.getParamDecl(cover.param)->getNameAsString() + - "' from its callers"); - } - return std::nullopt; -} - -void Transfer::decideCallKinds(const CallExpr &call, const SiteInfo &site, - const std::vector &args) { - const FunctionDecl *callee = call.getDirectCallee(); - if (callee == nullptr || site.kind != core::SiteKind::Call || - !run.isPublishing()) - return; - const KindTable &kinds = run.unitRun().input.kinds; - std::vector requirements; - // An extent term over the callee's parameters, as this call passes them. - auto termAt = [&](const core::ExtentTerm &term) -> core::Term { - if (term.isConstant()) - return core::Term::of(term.offset); - if (term.path->root != core::ExtentPath::Root::Param || - term.path->param >= args.size() || - args[term.path->param] == core::ZeroSym) - return core::Term::unknown(); - core::Term value = termOf(args[term.path->param]); - if (!value.known) - return value; - value.scale *= term.scale; - value.constant = (value.constant * term.scale) + term.offset; - if (value.isConstant()) - value.scale = 0; - return value; - }; - // The bytes from where argument `from` points to where `to` points. - auto between = [&](unsigned from, - unsigned to) -> std::optional { - if (from >= args.size() || to >= args.size()) - return std::nullopt; - const core::SymInfo &a = heap.info(state, args[from]); - const core::SymInfo &b = heap.info(state, args[to]); - if (a.targets.size() != 1 || b.targets.size() != 1 || - a.targets[0].object != b.targets[0].object) - return std::nullopt; - auto diff = b.targets[0].offset.plus( - core::Term{.var = a.targets[0].offset.var, - .scale = -a.targets[0].offset.scale, - .constant = -a.targets[0].offset.constant, - .known = a.targets[0].offset.known}); - if (!diff || !diff->isConstant()) - return std::nullopt; - return diff->constant; - }; - auto holds = [&](const RequirementGuard &guard) -> std::optional { - if (alwaysHolds(guard)) - return true; - if (!guard.lhs.isConstant() && !guard.rhs.isConstant() && - guard.lhs.path->root == core::ExtentPath::Root::Param && - guard.rhs.path->root == core::ExtentPath::Root::Param && - guard.lhs.path->param < callee->getNumParams() && - callee->getParamDecl(guard.lhs.path->param) - ->getType() - ->isPointerType()) { - auto span = between(guard.lhs.path->param, guard.rhs.path->param); - if (!span) - return std::nullopt; - return guard.relation == RequirementGuard::Relation::Less ? *span > 0 - : *span >= 0; - } - core::Term lhs = termAt(guard.lhs); - core::Term rhs = termAt(guard.rhs); - if (!lhs.known || !rhs.known) - return std::nullopt; - if (guard.relation == RequirementGuard::Relation::Less) - lhs = lhs.plusConstant(1); - return heap.lessEqual(state, lhs, rhs); - }; - for (unsigned i = 0; - i < call.getNumArgs() && i < callee->getNumParams() && i < args.size(); - ++i) { - const KindEntry *param = kinds.param(*callee, i); - if (param == nullptr) - continue; - QualType pointee = callee->getParamDecl(i)->getType()->getPointeeType(); - auto unit = elementBytes(context, pointee); - // §7.3: a callee that relies on Single, passed a possible cursor. - if (std::ranges::find(site.reliance, i) != site.reliance.end()) - if (auto bytes = singleWidth(context, pointee)) { - ArgRequirement row; - row.argument = i; - row.need = core::Term::of(*bytes); - row.rowOnly = true; - requirements.push_back(row); - } - // §7.2: a declared `ended-by(q)`. - if (param->hasDeclaredShape() && !param->shapeFromSystemHeader() && - param->kind.shape == core::PointerShape::EndedBy) { - ArgRequirement out; - out.argument = i; - out.enforced = true; - if (unit && !param->kind.extent.isConstant() && - param->kind.extent.path->root == core::ExtentPath::Root::Param) - if (auto span = between(i, param->kind.extent.path->param)) { - std::int64_t bytes = *span + (param->kind.extent.offset * *unit); - out.need = core::Term::of(bytes); - } - requirements.push_back(out); - continue; - } - bool enforced = - param->hasEnforcedRequirement() && !param->hasDeclaredShape(); - bool contract = - param->enforcement == RequirementEnforcement::CallerContract && - !param->hasDeclaredShape(); - if (!enforced && !contract) - continue; - for (const MustAccessRequirement &requirement : param->mustAccess) { - if (contract && requirement.kind.shape == core::PointerShape::Single) - continue; - ArgRequirement out; - out.argument = i; - out.enforced = true; - std::optional guarded = - requirement.guard ? holds(*requirement.guard) : std::optional(true); - if (guarded == false) { - out.need = core::Term::of(0); - requirements.push_back(out); - continue; - } - out.guarded = !guarded; - const core::PointerKind &kind = requirement.kind; - switch (kind.shape) { - case core::PointerShape::Single: - if (auto bytes = singleWidth(context, pointee)) - out.need = core::Term::of(*bytes); - break; - case core::PointerShape::Counted: - case core::PointerShape::Sized: { - std::int64_t size = - kind.shape == core::PointerShape::Sized ? 1 : unit.value_or(0); - if (size <= 0) - continue; - core::Term count = termAt(kind.extent); - if (count.known) - out.need = count.isConstant() - ? core::Term::of(count.constant * size) - : core::Term::ofSym(count.var, count.scale * size, - count.constant * size); - break; - } - case core::PointerShape::EndedBy: - if (unit && !kind.extent.isConstant() && - kind.extent.path->root == core::ExtentPath::Root::Param) - if (auto span = between(i, kind.extent.path->param)) - out.need = core::Term::of(*span + (kind.extent.offset * *unit)); - break; - case core::PointerShape::NulTerminated: - out.kind = ArgRequirement::Kind::String; - out.argvElement = isArgvElement(*call.getArg(i)); - break; - case core::PointerShape::Unknown: - continue; - } - requirements.push_back(out); - } - if (!enforced) - continue; - // The null part. - const ArgumentNeed *need = nullptr; - for (const ArgumentNeed &each : site.arguments) - if (each.argument == i && each.inferred) - need = &each; - if (need == nullptr) - continue; - auto publishNull = [&](const core::FacetDecision &decision) { - if (!run.applies(site.id, core::Facet::Null)) - return; - run.ledger().requirement(call, core::Facet::Null, decision); - }; - std::optional guardHolds = true; - for (const MustAccessRequirement &requirement : param->mustAccess) - if (requirement.kind.nullability == core::Nullability::Nonnull && - nullEnforced(*param, requirement)) { - guardHolds = - requirement.guard ? holds(*requirement.guard) : std::optional(true); - break; - } - if (guardHolds == false) { - publishNull(core::FacetDecision::proven()); - continue; - } - const core::SymInfo &value = heap.info(state, args[i]); - if (value.null == core::PointerNull::NonNull) { - publishNull(core::FacetDecision::proven()); - continue; - } - if (value.null != core::PointerNull::Null || value.allocatorSource || - guardHolds != true) { - publishNull(core::FacetDecision::unresolvedFor( - core::UnresolvedReason::Undecided)); - continue; - } - publishNull(core::FacetDecision::violation()); - run.report(nullArgument(*call.getArg(i), value, - "'" + callee->getNameAsString() + "'", callee), - core::Certainty::Definite, &call, core::Facet::Null); - } - // §7.2: declared shapes are checked at every call. - for (const SiteInfo::DeclaredShape &shape : site.declaredShapes) { - if (shape.argument >= call.getNumArgs()) - continue; - ArgRequirement requirement; - requirement.argument = shape.argument; - requirement.enforced = true; - const core::PointerKind &kind = shape.kind; - if (kind.shape == core::PointerShape::NulTerminated) { - requirement.kind = ArgRequirement::Kind::String; - requirement.argvElement = isArgvElement(*call.getArg(shape.argument)); - requirements.push_back(requirement); - continue; - } - core::Term count = core::Term::unknown(); - if (kind.extent.isConstant()) - count = core::Term::of(kind.extent.offset); - else if (kind.extent.path->root == core::ExtentPath::Root::Param && - kind.extent.path->param < call.getNumArgs()) - count = termAt(kind.extent); - auto element = elementBytes(context, shape.pointee); - switch (kind.shape) { - case core::PointerShape::Counted: - if (!element) - continue; - if (count.known) - requirement.need = - count.isConstant() - ? core::Term::of(count.constant * *element) - : core::Term::ofSym(count.var, count.scale * *element, - count.constant * *element); - break; - case core::PointerShape::Sized: - requirement.need = count; - break; - case core::PointerShape::Single: - if (auto width = singleWidth(context, shape.pointee)) - requirement.need = core::Term::of(*width); - else - continue; - break; - default: - continue; - } - requirements.push_back(requirement); - } - if (!requirements.empty()) - decideArguments(call, site, requirements, args, /*library=*/false); -} - -} // namespace weavec::analysis::engine diff --git a/lib/Analysis/EngineLibrary.cpp b/lib/Analysis/EngineLibrary.cpp deleted file mode 100644 index 407071aa..00000000 --- a/lib/Analysis/EngineLibrary.cpp +++ /dev/null @@ -1,624 +0,0 @@ -//===- EngineLibrary.cpp - Library-call requirements (object engine) ------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// RFC 0030 §8 and §3.3, over the object domain (RFC 0031 §5.4): each buffer -// argument of a `LibrarySpec` row is a requirement record on the call's -// spatial facet — its need in bytes against what the argument points into — -// proven, a violation against an exact extent, or unresolved; `disjoint` -// rows compare the two ranges. -// -//===----------------------------------------------------------------------===// - -#include "Engine.h" -#include "weavec/Analysis/ClangLocation.h" - -#include "clang/Basic/SourceManager.h" - -using namespace clang; - -namespace weavec::analysis::engine { - -const Expr &pointedObject(const Expr &argument) { - // (`buf` for `&buf[3]` and `buf + 2`.) - const Expr *base = argument.IgnoreParenImpCasts(); - for (int depth = 0; depth < 8; ++depth) { - if (const auto *unary = dyn_cast(base); - unary != nullptr && unary->getOpcode() == UO_AddrOf) { - base = unary->getSubExpr()->IgnoreParenImpCasts(); - continue; - } - if (const auto *subscript = dyn_cast(base)) { - base = subscript->getBase()->IgnoreParenImpCasts(); - continue; - } - if (const auto *binary = dyn_cast(base); - binary != nullptr && binary->isAdditiveOp() && - binary->getLHS()->getType()->isPointerType()) { - base = binary->getLHS()->IgnoreParenImpCasts(); - continue; - } - break; - } - return *base; -} - -/// The pointee of an argument before its implicit conversions. -static std::optional accessedElement(const Expr &argument) { - QualType type = argument.IgnoreParenImpCasts()->getType(); - QualType pointee; - if (type->isPointerType()) - pointee = type->getPointeeType(); - else if (const auto *array = type->getAsArrayTypeUnsafe()) - pointee = array->getElementType(); - else - return std::nullopt; - if (pointee->isVoidType() || pointee->isIncompleteType() || - pointee->isFunctionType() || !pointee->isConstantSizeType()) - return std::nullopt; - return pointee; -} - -namespace { -/// How a term's value relates to the true one; `None` when it is neither. -enum class TermBound : std::uint8_t { Exact, AtMost, AtLeast, None }; -} // namespace - -static TermBound combineBounds(TermBound a, TermBound b) { - if (a == b || b == TermBound::Exact) - return a; - if (a == TermBound::Exact) - return b; - return TermBound::None; -} - -/// A row term over the call's argument values, with how it relates to the -/// true value (every row term is monotone in its operands). -static core::Term valueTerm(Transfer &transfer, const core::LibTerm &term, - const CallExpr &call, - const core::LibraryMatch &match, - const std::vector &args, - TermBound &bound) { - using Kind = core::LibTerm::Kind; - auto operand = [&](std::size_t i) { - return i < term.operands.size() - ? valueTerm(transfer, term.operands[i], call, match, args, bound) - : core::Term::unknown(); - }; - switch (term.kind) { - case Kind::Constant: - return core::Term::of(term.value); - case Kind::Argument: { - int index = match.callArgument(term.arg); - if (index < 0 || static_cast(index) >= args.size() || - args[static_cast(index)] == core::ZeroSym) - return core::Term::unknown(); - return transfer.termOf(args[static_cast(index)]); - } - case Kind::StringLength: { - // RFC 0012: the length the string facts give, exactly or at most. - int index = match.callArgument(term.arg); - if (index < 0 || static_cast(index) >= args.size()) - return core::Term::unknown(); - Transfer::StringFacts facts = - transfer.stringFacts(args[static_cast(index)]); - if (facts.length == Transfer::StringFacts::Length::Unknown) - return core::Term::unknown(); - if (facts.length == Transfer::StringFacts::Length::AtMost) - bound = combineBounds(bound, TermBound::AtMost); - return facts.term; - } - case Kind::Product: { - core::Term a = operand(0); - core::Term b = operand(1); - if (!a.known || !b.known) - return core::Term::unknown(); - if (a.isConstant() && b.isConstant()) { - if (a.constant < 0 || b.constant < 0) - bound = combineBounds(bound, TermBound::None); - return core::Term::of(a.constant * b.constant); - } - const core::Term &k = a.isConstant() ? a : b; - const core::Term &x = a.isConstant() ? b : a; - if (!k.isConstant()) - return core::Term::unknown(); - if (k.constant < 0) - bound = combineBounds(bound, TermBound::None); - return core::Term::ofSym(x.var, x.scale * k.constant, - x.constant * k.constant); - } - case Kind::Sum: { - auto sum = operand(0).plus(operand(1)); - return sum ? *sum : core::Term::unknown(); - } - case Kind::Difference: - return operand(0).plusConstant(-term.value); - case Kind::Quotient: { - // (Not linear: known only for a constant.) - core::Term a = operand(0); - if (!a.isConstant() || term.value <= 0 || a.constant < 0) - return core::Term::unknown(); - return core::Term::of(a.constant / term.value); - } - case Kind::FormatLength: { - // A literal format's least output; exact when nothing in it varies. - Transfer::FormatFacts format = transfer.formatFacts(call, match); - if (!format.literal) - return core::Term::unknown(); - if (!format.exact) - bound = combineBounds(bound, TermBound::AtLeast); - return core::Term::of(format.lower); - } - case Kind::Min: { - core::Term a = operand(0); - core::Term b = operand(1); - if (!a.known || !b.known) - return core::Term::unknown(); - if (a.isConstant() && b.isConstant()) - return core::Term::of(std::min(a.constant, b.constant)); - if (transfer.domain().lessEqual(transfer.heapState(), a, b).value_or(false)) - return a; - if (transfer.domain().lessEqual(transfer.heapState(), b, a).value_or(false)) - return b; - return core::Term::unknown(); - } - case Kind::Macro: - return core::Term::unknown(); - } - return core::Term::unknown(); -} - -/// §8: the `str` family, whose destination is bounded by its member. -static bool isStringFamily(llvm::StringRef name) { - return name.starts_with("str") || name.starts_with("stp") || - name.starts_with("wcs"); -} - -void Transfer::decideArguments(const CallExpr &call, const SiteInfo &site, - const std::vector &requirements, - const std::vector &args, - bool library) { - if (!run.isPublishing()) - return; - LedgerAdapter &ledger = run.ledger(); - // The requirement being decided, for §7.5's cover of an unresolved one. - const ArgRequirement *current = nullptr; - auto publish = [&](unsigned argument, core::FacetDecision decision) { - if (decision.outcome == core::SiteOutcome::Unresolved && - current != nullptr && argument < args.size()) - if (auto covered = - coveredArgument(call, args[argument], - current->kind == ArgRequirement::Kind::String)) - decision = *covered; - ledger.requirement(call, core::Facet::Spatial, decision); - }; - // An amount for messages: `6 bytes`, `'n' bytes`, `'strlen(s)' + 1 bytes`. - auto nameFor = [&](core::Sym sym, bool own) -> std::string { - const std::string &made = heap.info(state, sym).name; - if (own && !made.empty()) - return made; - if (auto place = nameOf(sym, /*extent=*/true)) - return messageSpelling(*place); - return made; - }; - auto spellBytes = [&](const core::Term &term, bool own = false) { - return spellAmount(term, own); - }; - // Where the object the argument points into came from. - auto addObjectNote = [&](core::Diagnostic &diagnostic, const Expr &argument, - const core::SymInfo &pointer) { - if (pointer.targets.size() != 1) - return; - const core::ObjectInfo &info = run.table().info(pointer.targets[0].object); - if (!info.created.isValid()) - return; - std::string spelled = spell(argument); - switch (info.key.kind) { - case core::ObjectKind::Local: - case core::ObjectKind::Global: - diagnostic.addNote(spelled == info.name - ? "'" + spelled + "' is declared here" - : "the object behind '" + spelled + - "' is declared here", - info.created); - break; - case core::ObjectKind::HeapRecent: - case core::ObjectKind::HeapOld: - diagnostic.addNote("'" + spelled + "' is allocated here", info.created); - break; - default: - break; - } - }; - std::string callee; - if (site.library) - callee = "'" + site.library->entry->name + "'"; - else if (const FunctionDecl *direct = call.getDirectCallee()) - callee = "'" + direct->getNameAsString() + "'"; - else - callee = "a function pointer"; - for (const ArgRequirement &requirement : requirements) { - current = &requirement; - unsigned i = requirement.argument; - if (i >= call.getNumArgs() || i >= args.size()) - continue; - const Expr &argument = *call.getArg(i); - core::Sym pointer = args[i]; - const core::SymInfo value = heap.info(state, pointer); - const core::Term &need = requirement.need; - // A zero-length argument may be null (§5.4). - if (value.null == core::PointerNull::Null) { - publish(i, core::FacetDecision::proven()); - continue; - } - // Writing into a string literal. - if (requirement.writes && value.targets.size() == 1 && - run.table().info(value.targets[0].object).key.kind == - core::ObjectKind::Literal) { - publish(i, core::FacetDecision::violation()); - core::Diagnostic diagnostic; - diagnostic.id = core::diag::OutOfBounds; - diagnostic.message = "write through '" + spell(argument) + - "', which points to a string literal"; - diagnostic.location = - toCoreLocation(context.getSourceManager(), argument.getBeginLoc()); - run.report(std::move(diagnostic), core::Certainty::Definite, &call, - core::Facet::Spatial); - continue; - } - if (requirement.kind == ArgRequirement::Kind::Bytes && need.isConstant() && - need.constant <= 0) { - publish(i, core::FacetDecision::proven()); - continue; - } - // What the argument points into. - std::optional extent; - core::Term start = core::Term::unknown(); - if (requirement.memberBound) - if (const auto *member = - dyn_cast(argument.IgnoreParenImpCasts()); - member != nullptr && isa_and_nonnull( - member->getType()->getAsArrayTypeUnsafe())) - if (auto size = sizeOf(member->getType())) { - extent = core::Extent{.bytes = core::Term::of(*size), - .cls = core::ExtentClass::Declared}; - start = core::Term::of(0); - } - if (!extent && value.targets.size() == 1 && !value.top) { - const core::ObjectState *object = - heap.findObject(state, value.targets[0].object); - if (object != nullptr && object->extent && object->extent->bytes.known) { - extent = object->extent; - start = value.targets[0].offset; - } - } - if (requirement.kind == ArgRequirement::Kind::String) { - if (requirement.argvElement) { - if (!requirement.formatArgument) - publish( - i, core::FacetDecision::trustedFor(core::TrustReason::SystemApi)); - continue; - } - // RFC 0012: a NUL known inside the object ends the string there; no - // NUL to the end of an exact extent is a read past it. - Transfer::StringFacts facts = stringFacts(pointer); - if (facts.length != Transfer::StringFacts::Length::Unknown && extent && - heap.lessEqual(state, facts.nulAt.plusConstant(1), extent->bytes) - .value_or(false)) { - publish(i, core::FacetDecision::proven()); - continue; - } - if (facts.unterminated && extent && - extent->cls == core::ExtentClass::Exact) { - publish(i, core::FacetDecision::violation()); - core::Diagnostic diagnostic; - diagnostic.id = core::diag::OutOfBounds; - diagnostic.severity = core::Severity::Error; - diagnostic.message = callee + " reads past the end of '" + - spell(argument) + "', which is not NUL-terminated"; - diagnostic.location = - toCoreLocation(context.getSourceManager(), argument.getBeginLoc()); - // Where the bytes that have no terminator were written. - if (value.targets.size() == 1) - if (auto written = run.byteWrites.find(value.targets[0].object); - written != run.byteWrites.end()) - diagnostic.addNote("'" + spell(argument) + - "' is left without a terminator here", - written->second); - run.report(std::move(diagnostic), core::Certainty::Definite, &call, - core::Facet::Spatial); - continue; - } - if (!extent) { - publish(i, core::FacetDecision::unresolvedFor( - core::UnresolvedReason::UnknownExtent)); - continue; - } - } else if (!extent) { - publish(i, core::FacetDecision::unresolvedFor( - core::UnresolvedReason::UnknownExtent)); - continue; - } else if (need.known) { - auto end = start.plus(need); - std::optional fits = - end ? heap.lessEqual(state, *end, extent->bytes) : std::nullopt; - std::optional nonNegative = - start.known ? heap.lessEqual(state, core::Term::of(0), start) - : std::nullopt; - // A need known only as a bound proves one way and violates the other. - bool atMost = requirement.bound != ArgRequirement::Bound::AtLeast; - bool atLeast = requirement.bound != ArgRequirement::Bound::AtMost; - if (fits.value_or(false) && nonNegative.value_or(false) && atMost) { - publish(i, core::FacetDecision::proven()); - continue; - } - if (fits && !*fits && atLeast && - extent->cls == core::ExtentClass::Exact && requirement.enforced && - !requirement.guarded) { - publish(i, core::FacetDecision::violation()); - run.requirementViolated.insert(&call); - std::string object = "'" + spell(pointedObject(argument)) + "'"; - std::string least = requirement.bound == ArgRequirement::Bound::AtLeast - ? "at least " - : ""; - // Measured from the object's start, as its extent is. - core::Term reach = end ? *end : need; - std::string message = callee; - message += library ? " accesses " : " requires "; - message += least; - message += spellBytes(reach, true).value_or("?"); - message += library ? " of " : " behind "; - message += object; - message += ", which has "; - message += spellBytes(extent->bytes).value_or("?"); - // One value under two names: `('strlen(s)' equals 'n')`. - if (!need.isConstant() && !extent->bytes.isConstant() && - need.var == extent->bytes.var) { - std::string made = nameFor(need.var, true); - std::string held = nameFor(need.var, false); - if (!made.empty() && !held.empty() && made != held) { - message += " ('"; - message += made; - message += "' equals '"; - message += held; - message += "')"; - } - } - core::Diagnostic diagnostic; - diagnostic.id = core::diag::OutOfBounds; - diagnostic.message = std::move(message); - diagnostic.location = - toCoreLocation(context.getSourceManager(), argument.getBeginLoc()); - addObjectNote(diagnostic, argument, value); - run.report(std::move(diagnostic), core::Certainty::Definite, &call, - core::Facet::Spatial); - continue; - } - } - if (requirement.rowOnly || !core::isCheckOperand(extent->cls)) { - publish(i, core::FacetDecision::unresolvedFor( - core::UnresolvedReason::UnknownExtent)); - continue; - } - publish(i, core::FacetDecision::unresolvedFor( - core::UnresolvedReason::Undecided)); - } -} - -void Transfer::decideLibraryCall(const CallExpr &call, const SiteInfo &site, - const std::vector &args) { - if (!site.library || !run.isPublishing()) - return; - const core::LibraryMatch &match = *site.library; - LedgerAdapter &ledger = run.ledger(); - // RFC 0030 §9.3: through an open slot the row decides temporal facts - // only; what the function really stored there needs is unknown. - if (call.getDirectCallee() == nullptr) - if (auto resolution = slotResolution(call); - resolution && - (resolution->kind == core::IndirectCallKind::OpenKnown || - resolution->kind == core::IndirectCallKind::OpenUnknown)) { - if (ledger.applies(site.id, core::Facet::Spatial)) - ledger.decideAs( - call, site.kind, site.boundary, core::Facet::Spatial, - core::FacetDecision::unresolvedFor( - core::UnresolvedReason::Callback, - resolution->open ? resolution->open->detail : std::string())); - return; - } - const bool stringRow = isStringFamily(match.entry->name); - auto publish = [&](unsigned /*argument*/, - const core::FacetDecision &decision) { - ledger.requirement(call, core::Facet::Spatial, decision); - }; - std::string callee = "'" + match.entry->name + "'"; - std::vector requirements; - for (unsigned i = 0; i < call.getNumArgs() && i < args.size(); ++i) { - const core::LibraryParam *param = match.param(i); - if (param == nullptr || param->type != core::LibraryParam::Type::Pointer) - continue; - bool sized = param->bytes || param->count; - if (param->access == core::LibraryParam::Access::None && !sized && - !param->string) - continue; - const Expr &argument = *call.getArg(i); - bool writes = param->access == core::LibraryParam::Access::Write || - param->access == core::LibraryParam::Access::ReadWrite; - std::optional element = accessedElement(argument); - if (sized || !param->string) { - ArgRequirement requirement; - requirement.argument = i; - requirement.writes = writes; - requirement.memberBound = stringRow && writes; - requirement.enforced = true; - requirement.format = match.entry->format.has_value(); - TermBound bound = TermBound::Exact; - if (param->bytes) { - requirement.need = - valueTerm(*this, *param->bytes, call, match, args, bound); - } else if (param->count && element) { - auto size = sizeOf(*element).value_or(1); - core::Term count = - valueTerm(*this, *param->count, call, match, args, bound); - if (count.known) - requirement.need = - count.isConstant() - ? core::Term::of(count.constant * size) - : core::Term::ofSym(count.var, count.scale * size, - count.constant * size); - } else if (!param->count && element) { - if (auto size = sizeOf(*element)) - requirement.need = core::Term::of(*size); - } else { - // An object only the library makes and reads (`FILE`): A3. - publish(i, core::FacetDecision::proven()); - continue; - } - if (bound == TermBound::None) - requirement.need = core::Term::unknown(); - else if (bound == TermBound::AtMost) - requirement.bound = ArgRequirement::Bound::AtMost; - else if (bound == TermBound::AtLeast) - requirement.bound = ArgRequirement::Bound::AtLeast; - requirements.push_back(requirement); - } - if (param->string) { - ArgRequirement requirement; - requirement.argument = i; - requirement.kind = ArgRequirement::Kind::String; - requirement.writes = writes; - requirement.memberBound = stringRow && writes; - requirement.argvElement = isArgvElement(argument); - requirements.push_back(requirement); - } - } - // A literal `printf` format: each `%s` argument is a string the call - // reads (RFC 0012), and the conversions read no more arguments than are - // passed (RFC 0030 §8.2). - Transfer::FormatFacts format = formatFacts(call, match); - if (format.literal) { - for (unsigned argument : format.strings) { - if (argument >= call.getNumArgs() || argument >= args.size() || - !call.getArg(argument)->getType()->isPointerType()) - continue; - ArgRequirement requirement; - requirement.argument = argument; - requirement.kind = ArgRequirement::Kind::String; - requirement.argvElement = isArgvElement(*call.getArg(argument)); - requirement.formatArgument = true; - requirements.push_back(requirement); - } - // A `%.Ns` argument: at most N bytes, no terminator needed. Its record - // states no need, but names it for the call's liveness guard. - for (unsigned argument : format.bounded) - if (argument < call.getNumArgs() && argument < args.size() && - call.getArg(argument)->getType()->isPointerType()) - requirements.push_back( - ArgRequirement{.argument = argument, - .bound = ArgRequirement::Bound::AtMost, - .rowOnly = true, - .formatArgument = true}); - if (format.reads && *format.reads > format.passed) { - core::Diagnostic diagnostic; - diagnostic.id = core::diag::OutOfBounds; - diagnostic.severity = core::Severity::Error; - diagnostic.message = "format string of " + callee + " reads " + - std::to_string(*format.reads) + " arguments but " + - std::to_string(format.passed) + " are passed"; - diagnostic.location = - toCoreLocation(context.getSourceManager(), call.getBeginLoc()); - run.report(std::move(diagnostic), core::Certainty::Definite, &call, - core::Facet::Spatial); - } - } else if (match.entry->format) { - // A format that is no literal: which arguments its conversions read, - // and as what, is not known here (RFC 0030 §8.2). - int at = match.callArgument(match.entry->format->format); - if (at >= 0) - publish(static_cast(at), - core::FacetDecision::unresolvedFor( - core::UnresolvedReason::Inexpressible, - "the format is not a string literal")); - } - decideArguments(call, site, requirements, args, /*library=*/true); - // `disjoint(d, s, n)` (§3.3). - for (const core::LibDisjoint &disjoint : match.entry->disjoint) { - int first = match.callArgument(disjoint.first); - int second = match.callArgument(disjoint.second); - if (first < 0 || second < 0 || - static_cast(std::max(first, second)) >= args.size()) - continue; - auto argument = static_cast(first); - TermBound lengthBound = TermBound::Exact; - core::Term length = - valueTerm(*this, disjoint.length, call, match, args, lengthBound); - if (lengthBound != TermBound::Exact) - length = core::Term::unknown(); - const core::SymInfo a = heap.info(state, args[argument]); - const core::SymInfo b = - heap.info(state, args[static_cast(second)]); - if (length.isConstant() && length.constant <= 0) { - publish(argument, core::FacetDecision::proven()); - continue; - } - bool distinct = - !a.top && !b.top && !a.targets.empty() && !b.targets.empty(); - if (distinct) - for (const core::Target &x : a.targets) - for (const core::Target &y : b.targets) - if (heap.mayOverlap(state, x.object, y.object)) - distinct = false; - auto isLiteral = [&](const core::SymInfo &info) { - return info.targets.size() == 1 && - run.table().info(info.targets[0].object).key.kind == - core::ObjectKind::Literal; - }; - if (distinct || isLiteral(a) || isLiteral(b)) { - publish(argument, core::FacetDecision::proven()); - continue; - } - // One object at known offsets: overlap is decided. - if (a.targets.size() == 1 && b.targets.size() == 1 && - a.targets[0].object == b.targets[0].object && - run.table().info(a.targets[0].object).singular && - a.targets[0].offset.isConstant() && b.targets[0].offset.isConstant() && - length.isConstant()) { - std::int64_t gap = - a.targets[0].offset.constant - b.targets[0].offset.constant; - if (gap < 0) - gap = -gap; - // RFC 0033 §1: a copy onto itself (`fe25519_copy(h, h)`, a struct - // assigned to itself) is undefined by the letter but leaves the bytes - // as they were in every supported C library: no violation. - if (gap == 0 || gap >= length.constant) { - publish(argument, core::FacetDecision::proven()); - continue; - } - publish(argument, core::FacetDecision::violation()); - std::string object = run.table().info(a.targets[0].object).name; - core::Diagnostic diagnostic; - diagnostic.id = core::diag::OutOfBounds; - diagnostic.message = callee; - diagnostic.message += " copies "; - diagnostic.message += std::to_string(length.constant); - diagnostic.message += " bytes between overlapping ranges of '"; - diagnostic.message += object; - diagnostic.message += '\''; - diagnostic.location = - toCoreLocation(context.getSourceManager(), call.getBeginLoc()); - diagnostic.addNote("'" + object + "' is declared here", - run.table().info(a.targets[0].object).created); - run.report(std::move(diagnostic), core::Certainty::Definite, &call, - core::Facet::Spatial); - continue; - } - publish(argument, core::FacetDecision::unresolvedFor( - core::UnresolvedReason::Undecided)); - } -} - -} // namespace weavec::analysis::engine diff --git a/lib/Analysis/EngineLifetimes.cpp b/lib/Analysis/EngineLifetimes.cpp deleted file mode 100644 index 7d1c73a6..00000000 --- a/lib/Analysis/EngineLifetimes.cpp +++ /dev/null @@ -1,1341 +0,0 @@ -//===- EngineLifetimes.cpp - Lifetimes and boundaries in the object engine -//-===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// RFC 0031 §5.6–§5.7 and RFC 0030 §6: what the frame leaves behind and what -// a boundary hands over. -// -// - Frame storage (a local, a parameter, a compound literal, an `alloca` -// block) that a use reaches after its lifetime ended, or that a cell the -// caller can reach still points to at the exit, is `lifetime-too-short`, -// reported where the pointer was stored (RFC 0002, RFC 0011 *Deferred -// lifetime checks*). -// - At a call and at an exit the engine publishes the boundary facts of -// RFC 0030 §9.4 over the objects the other side can reach (§5.6). -// - `WEAVEC_ASSUME(e)` is an Assume site whose assertion facet is proven, -// checked or contradicted, and the analysis assumes `e` after it -// (RFC 0030 §6.2). -// -//===----------------------------------------------------------------------===// - -#include "Engine.h" -#include "weavec/Analysis/Annotations.h" -#include "weavec/Analysis/BypassedDeclarations.h" -#include "weavec/Analysis/ClangLocation.h" - -#include "clang/AST/RecordLayout.h" -#include "clang/Basic/SourceManager.h" -#include "clang/Lex/Lexer.h" - -#include -#include -#include - -using namespace clang; - -namespace weavec::analysis::engine { - -//===----------------------------------------------------------------------===// -// Frame storage -//===----------------------------------------------------------------------===// - -bool FunctionRun::isFrameObject(const core::HeapState &state, - core::ObjectId id) const { - const core::ObjectInfo &info = objects.info(id); - switch (info.key.kind) { - case core::ObjectKind::Local: - // Locals, parameters, compound literals and temporaries: a variable - // with static storage is a `Global` object. - return !info.key.dead; - case core::ObjectKind::HeapRecent: - case core::ObjectKind::HeapOld: - // RFC 0030 §8.2: `alloca` storage belongs to the frame. - if (const core::ObjectState *object = state.objects.find(id)) - return object->family == core::StackFamily; - return false; - default: - return false; - } -} - -std::string FunctionRun::frameName(core::ObjectId id) const { - const core::ObjectInfo &info = objects.info(id); - if (info.key.kind == core::ObjectKind::HeapRecent || - info.key.kind == core::ObjectKind::HeapOld) - return ""; - return info.name; -} - -void FunctionRun::noteFrameStore(core::ObjectId holder, core::CellKey key, - core::ObjectId frame, - std::string holderSpelling) { - if (currentElement == nullptr) - return; - frameStores[{holder, key, frame}] = - FrameStore{.at = currentElement, .holder = std::move(holderSpelling)}; -} - -const FunctionRun::FrameStore * -FunctionRun::frameStore(core::ObjectId holder, core::CellKey key, - core::ObjectId frame) const { - auto it = frameStores.find({holder, key, frame}); - return it == frameStores.end() ? nullptr : &it->second; -} - -void FunctionRun::noteBorrowStore(core::ObjectId holder, core::CellKey key, - std::string holderSpelling) { - if (currentElement == nullptr) - return; - borrowStores[{holder, key}] = - FrameStore{.at = currentElement, .holder = std::move(holderSpelling)}; -} - -const FunctionRun::FrameStore * -FunctionRun::borrowStore(core::ObjectId holder, core::CellKey key) const { - auto it = borrowStores.find({holder, key}); - return it == borrowStores.end() ? nullptr : &it->second; -} - -const VarDecl *FunctionRun::localVariable(core::ObjectId id) const { - for (const auto &[decl, object] : localObjects) - if (object == id) - return dyn_cast(decl); - return nullptr; -} - -bool FunctionRun::isAddressTaken(const VarDecl &var) const { - auto index = localIndex.find(var.getCanonicalDecl()); - return index != localIndex.end() && addressTaken.test(index->second); -} - -void Transfer::recordFrameStore(core::ObjectId holder, core::CellKey key, - core::Sym value, const Expr *at, - const std::string &holderName) { - const core::SymInfo &info = heap.info(state, value); - if (info.type != core::SymInfo::Type::Pointer) - return; - auto spelling = [&] { - if (const auto *assign = dyn_cast_or_null(at); - assign != nullptr && assign->isAssignmentOp()) - return spell(*assign->getLHS()); - return holderName; - }; - if (info.derived) - run.noteBorrowStore(holder, key, spelling()); - std::vector frames; - for (const core::Target &target : info.targets) - if (target.object != holder && run.isFrameObject(state, target.object)) - frames.push_back(target.object); - if (frames.empty()) - return; - std::string spelled = spelling(); - for (core::ObjectId frame : frames) - run.noteFrameStore(holder, key, frame, spelled); -} - -/// The byte offset of `field` in its record. -static std::int64_t offsetOf(const ASTContext &context, - const FieldDecl &field) { - const ASTRecordLayout &layout = context.getASTRecordLayout(field.getParent()); - return static_cast( - layout.getFieldOffset(field.getFieldIndex()) / context.getCharWidth()); -} - -/// The direct field of record type `type` at byte `offset`, if one starts -/// there. -static const FieldDecl *fieldStartingAt(const ASTContext &context, - QualType type, std::int64_t offset) { - if (type.isNull()) - return nullptr; - const RecordDecl *record = type->getAsRecordDecl(); - if (record == nullptr || !record->isCompleteDefinition()) - return nullptr; - for (const FieldDecl *field : record->fields()) - if (offsetOf(context, *field) == offset && !field->getName().empty()) - return field; - return nullptr; -} - -/// A cell's spelling for messages: `g`, `st->fs`, `*out`. -static std::string cellName(const FunctionRun &run, core::ObjectId object, - core::CellKey key) { - const core::ObjectInfo &info = run.table().info(object); - QualType type = info.type != 0 ? typeOfHandle(info.type) : QualType(); - const FieldDecl *field = - key.isSummary() ? nullptr : fieldStartingAt(run.ast(), type, key.offset); - if (info.key.kind == core::ObjectKind::Global) { - std::string name = info.name; - return field != nullptr ? name + "." + field->getNameAsString() : name; - } - if (field != nullptr) - return info.name + "->" + field->getNameAsString(); - return "*" + info.name; -} - -/// The innermost named field of `type` whose storage holds byte `offset`: -/// through the elements of arrays and the members of nested records. Null -/// when `type` is no record, or no named field holds the byte. -static const FieldDecl *fieldHolding(const ASTContext &context, QualType type, - std::int64_t offset) { - const FieldDecl *found = nullptr; - for (unsigned depth = 0; depth < 32 && !type.isNull() && offset >= 0; - ++depth) { - if (const ArrayType *array = context.getAsArrayType(type)) { - QualType element = array->getElementType(); - if (element->isIncompleteType() || !element->isConstantSizeType()) - break; - std::int64_t size = context.getTypeSizeInChars(element).getQuantity(); - if (size <= 0) - break; - offset %= size; - type = element; - continue; - } - const RecordDecl *record = type->getAsRecordDecl(); - if (record == nullptr || !record->isCompleteDefinition() || - record->isInvalidDecl()) - break; - const FieldDecl *holder = nullptr; - for (const FieldDecl *field : record->fields()) { - if (field->isBitField() || field->getType()->isIncompleteType() || - !field->getType()->isConstantSizeType()) - continue; - std::int64_t start = offsetOf(context, *field); - std::int64_t size = - context.getTypeSizeInChars(field->getType()).getQuantity(); - if (offset >= start && offset < start + size) { - holder = field; - break; - } - } - if (holder == nullptr) - break; - if (!holder->getName().empty() && !record->getName().empty()) - found = holder; - offset -= offsetOf(context, *holder); - type = holder->getType(); - } - return found; -} - -std::string cellClass(const FunctionRun &run, core::ObjectId object, - core::CellKey key) { - const core::ObjectInfo &info = run.table().info(object); - QualType type = info.type != 0 ? typeOfHandle(info.type) : QualType(); - // RFC 0030 §9.4: the class of a cell is the field that holds it, and for a - // global outside any record the global. An element of an array is of its - // array's class (a summary cell's offset is its position in an element): - // a dangling pointer stored into `g[1]` or `r->slot[i]` breaks the entry - // assumption of every load from that array. - if (const FieldDecl *field = fieldHolding(run.ast(), type, key.offset)) { - const RecordDecl *record = field->getParent(); - return record->getKindName().str() + " " + record->getNameAsString() + "." + - field->getNameAsString(); - } - if (info.key.kind == core::ObjectKind::Global) - return info.name; - return {}; -} - -/// RFC 0030 §9.4: whether a cell is an owning slot: a field or global some -/// function of the unit releases a value loaded from, or one declared -/// `WEAVEC_OWNED`. -static bool isOwningCell(const FunctionRun &run, core::ObjectId object, - core::CellKey key) { - if (key.isSummary()) - return false; - const core::ObjectInfo &info = run.table().info(object); - QualType type = info.type != 0 ? typeOfHandle(info.type) : QualType(); - const Decl *slot = fieldStartingAt(run.ast(), type, key.offset); - if (slot == nullptr && info.key.kind == core::ObjectKind::Global && - key.offset == 0) - slot = fromHandle(info.key.handle); - if (slot == nullptr) - return false; - if (run.unitRun().owningSlots.contains(slot->getCanonicalDecl())) - return true; - if (const auto *named = dyn_cast(slot)) - return getAnnotations(*named).owned; - return false; -} - -/// The summary path of a cell of an object reached by `path`. -static core::SummaryPath cellPath(const FunctionRun &run, - const core::SummaryPath &path, - core::ObjectId object, core::CellKey key) { - if (key.isSummary()) - return path.indexed(); - const core::ObjectInfo &info = run.table().info(object); - QualType type = info.type != 0 ? typeOfHandle(info.type) : QualType(); - if (const FieldDecl *field = fieldStartingAt(run.ast(), type, key.offset)) - return path.field(field->getName()); - if (key.offset != 0) - return path.field("#" + std::to_string(key.offset)); - return path; -} - -void Transfer::danglingUse(const Expr &operand, - const core::TemporalVerdict &verdict, - const Stmt *site, bool definite) { - if (!run.isPublishing() || verdict.object == 0) - return; - core::ObjectId frame = verdict.object; - // Storage a callee left behind (`danglingValue`): its own exit reported - // it. - if (!run.isFrameObject(state, frame)) - return; - // The cell the pointer was read from, and where it was put there. - const FunctionRun::FrameStore *stored = nullptr; - const Expr *lvalue = operand.IgnoreParenImpCasts(); - if (lvalue->isGLValue() && !lvalue->HasSideEffects(context)) { - Address address = addressOf(*lvalue); - if (!address.top && address.targets.size() == 1 && - address.targets[0].offset.isConstant()) - stored = run.frameStore( - address.targets[0].object, - core::CellKey{.offset = address.targets[0].offset.constant}, frame); - } - std::string holder = stored != nullptr && !stored->holder.empty() - ? stored->holder - : spell(operand); - std::string name = run.frameName(frame); - core::Diagnostic diagnostic; - diagnostic.id = core::diag::LifetimeTooShort; - diagnostic.severity = - definite ? core::Severity::Error : core::Severity::Warning; - diagnostic.message = - "'" + holder + "' may outlive '" + name + "', which it points to"; - diagnostic.location = toCoreLocation( - context.getSourceManager(), - stored != nullptr ? stored->at->getBeginLoc() : operand.getBeginLoc()); - const core::ObjectInfo &info = run.table().info(frame); - if (info.created.isValid()) - diagnostic.addNote("'" + name + "' is declared here", info.created); - run.report(std::move(diagnostic), - definite ? core::Certainty::Definite : core::Certainty::Possible, - site, core::Facet::Temporal); -} - -core::Sym Transfer::danglingValue(const CallExpr &call, QualType type, - const core::SummaryPath &path) { - QualType pointee = type->getPointeeType(); - std::string callee = call.getDirectCallee() != nullptr - ? call.getDirectCallee()->getNameAsString() - : spell(*call.getCallee()); - core::ObjectId object = run.allocationObject( - call, pointee, "the storage of '" + callee + "'", path); - core::ObjectState ended; - ended.life = core::Life::MayEnded; - state.objects.set(object, ended); - core::SymInfo info; - info.type = core::SymInfo::Type::Pointer; - info.targets = {core::Target{.object = object}}; - info.null = core::PointerNull::NonNull; - info.name = spell(call); - info.ctype = typeHandle(type); - return heap.fresh(state, info); -} - -std::optional -Transfer::spellArgumentPath(const CallExpr &call, - const core::SummaryPath &path) const { - if (!path.isParam() || path.isRoot() || path.index >= call.getNumArgs()) - return std::nullopt; - const Expr *arg = call.getArg(path.index)->IgnoreParenImpCasts(); - std::string text = spell(*arg); - bool addressOf = false; - if (const auto *unary = dyn_cast(arg); - unary != nullptr && unary->getOpcode() == UO_AddrOf) { - text = spell(*unary->getSubExpr()); - addressOf = true; - } - if (text.empty()) - return std::nullopt; - // Pairs of a dereference and a field. - for (std::size_t i = 0; i + 1 < path.steps.size(); i += 2) { - if (path.steps[i].step != core::PathStep::Deref || - path.steps[i + 1].step != core::PathStep::Field) - return std::nullopt; - text += (addressOf && i == 0 ? "." : "->") + path.steps[i + 1].field; - } - if (path.steps.size() % 2 != 0) - return std::nullopt; - return text; -} - -void Transfer::calleeRelease(const CallExpr &call, core::Sym value, - const core::SummaryPath &path, bool certain) { - if (!run.isPublishing()) - return; - const core::SymInfo &info = heap.info(state, value); - if (info.type != core::SymInfo::Type::Pointer || info.top || - info.targets.empty() || info.null == core::PointerNull::Null) - return; - // Storage that is no heap object on every path (RFC 0030 §3.4); a - // position inside an object the caller did not allocate here is the - // callee's business, as it may compose the offset back. - std::optional storage; - bool every = true; - for (const core::Target &target : info.targets) { - const core::ObjectInfo &objectInfo = run.table().info(target.object); - const core::ObjectState *object = heap.findObject(state, target.object); - bool nonHeap = objectInfo.key.kind == core::ObjectKind::Local || - objectInfo.key.kind == core::ObjectKind::Global || - objectInfo.key.kind == core::ObjectKind::Literal || - (object != nullptr && object->family == core::StackFamily); - if (nonHeap && !storage) - storage = target.object; - every = every && nonHeap; - } - std::string subject = - path.isParam() && path.isRoot() && path.index < call.getNumArgs() - ? spell(*call.getArg(path.index)) - : std::string("the pointer"); - if (auto spelled = spellArgumentPath(call, path)) - subject = *spelled; - if (!storage) { - // RFC 0008: an allocation of this function released at a known - // position past its start. - if (info.targets.size() != 1) - return; - const core::Target &target = info.targets.front(); - const core::ObjectInfo &objectInfo = run.table().info(target.object); - if ((objectInfo.key.kind != core::ObjectKind::HeapRecent && - objectInfo.key.kind != core::ObjectKind::HeapOld) || - !target.offset.isConstant() || target.offset.constant == 0) - return; - std::int64_t bytes = target.offset.constant; - std::string position = std::to_string(bytes) + " bytes"; - if (path.isParam() && path.isRoot() && path.index < call.getNumArgs()) { - QualType type = call.getArg(path.index)->IgnoreParenImpCasts()->getType(); - if (type->isPointerType()) - if (auto size = sizeOf(type->getPointeeType()); - size && *size > 0 && bytes % *size == 0) - position = std::to_string(bytes / *size) + - (bytes / *size == 1 ? " element" : " elements"); - } - bool definite = certain && objectInfo.singular; - core::Diagnostic diagnostic; - diagnostic.id = core::diag::InvalidRelease; - diagnostic.severity = - definite ? core::Severity::Error : core::Severity::Warning; - diagnostic.message = "'" + subject + "' is released but " + - (definite ? "points " : "may point ") + position + - (bytes > 0 ? " past" : " before") + - " the start of its allocation"; - diagnostic.location = - toCoreLocation(context.getSourceManager(), call.getBeginLoc()); - if (objectInfo.created.isValid()) - diagnostic.addNote("allocated here", objectInfo.created); - run.report(std::move(diagnostic), - definite ? core::Certainty::Definite : core::Certainty::Possible, - &call, core::Facet::Spatial); - return; - } - const core::ObjectInfo &objectInfo = run.table().info(*storage); - bool literal = objectInfo.key.kind == core::ObjectKind::Literal; - std::string name = objectInfo.key.kind == core::ObjectKind::Global - ? objectInfo.name - : run.frameName(*storage); - bool definite = every && certain; - std::string may = definite ? "" : "may "; - core::Diagnostic diagnostic; - diagnostic.id = core::diag::InvalidRelease; - diagnostic.severity = - definite ? core::Severity::Error : core::Severity::Warning; - diagnostic.message = - "'" + subject + "' is released but " + may + - (definite ? "points" : "point") + - (literal ? std::string(" to a string literal") + - (definite ? "" : ", which is not a heap object") - : " to '" + name + "', which is not a heap object"); - diagnostic.location = - toCoreLocation(context.getSourceManager(), call.getBeginLoc()); - if (!literal && objectInfo.created.isValid()) - diagnostic.addNote("'" + name + "' is declared here", objectInfo.created); - run.report(std::move(diagnostic), - definite ? core::Certainty::Definite : core::Certainty::Possible, - &call, core::Facet::Spatial); -} - -//===----------------------------------------------------------------------===// -// Boundaries (§5.6) -//===----------------------------------------------------------------------===// - -namespace { -/// An object the other side of a boundary reaches, with how it names it. -struct Reached { - core::ObjectId object = 0; - core::SummaryPath path; -}; -} // namespace - -/// Every object reachable from `roots` through the memory, first path -/// first. Frame objects are entered only when `enterFrames`. -static std::vector reachWithPaths(FunctionRun &run, - const core::HeapState &state, - std::vector roots, - bool enterFrames) { - const core::Heap &heap = run.domain(); - std::set seen; - std::deque work; - std::vector out; - for (Reached &root : roots) - if (seen.insert(root.object).second) - work.push_back(std::move(root)); - while (!work.empty()) { - Reached current = std::move(work.front()); - work.pop_front(); - const core::ObjectState *object = state.objects.find(current.object); - if (object == nullptr) - continue; - out.push_back(current); - if (object->life == core::Life::Released) - continue; - for (const auto &[key, sym] : object->cells) { - const core::SymInfo &value = heap.info(state, sym); - if (value.type != core::SymInfo::Type::Pointer) - continue; - for (const core::Target &target : value.targets) { - if (!enterFrames && run.isFrameObject(state, target.object)) - continue; - if (!seen.insert(target.object).second) - continue; - work.push_back(Reached{ - .object = target.object, - .path = cellPath(run, current.path, current.object, key).deref()}); - } - } - } - return out; -} - -/// Whether `value` may hold a pointer the program released (not the -/// unknown-callee default, which is not a release the boundary can name), -/// and where it was released. -static std::optional -releasedValue(const core::Heap &heap, const core::HeapState &state, - const core::SymInfo &value) { - auto counts = [](const core::ReleaseRecord &record) { - return !record.unknownOrigin() && - record.reason != core::ReleaseRecord::Reason::Moved; - }; - if (value.null == core::PointerNull::Null) - return std::nullopt; - if (value.release && counts(*value.release)) - return value.release->where; - for (const core::Target &target : value.targets) { - const core::ObjectState *object = heap.findObject(state, target.object); - if (object == nullptr) - continue; - if ((object->life == core::Life::Released || - object->life == core::Life::MayReleased) && - object->record && counts(*object->record)) - return object->record->where; - if (object->life == core::Life::Ended || - object->life == core::Life::MayEnded) - return core::SourceLocation{}; - } - return std::nullopt; -} - -void Transfer::callBoundary(const CallExpr &call, - const std::vector &args) { - if (!run.isPublishing() || state.unreachable) - return; - // §5.7: frame storage a global holds at a call is handed to the callee - // through the global. - for (const auto &[id, object] : state.objects) { - if (run.table().info(id).key.kind != core::ObjectKind::Global) - continue; - for (const auto &[key, sym] : object.cells) - for (const core::Target &target : heap.info(state, sym).targets) - if (run.isFrameObject(state, target.object)) - run.exposedFrames.insert(target.object); - } - // RFC 0030 §9.4: the boundaries are the Call sites, the exits of calls - // that do not return, and the library calls that hand an argument to a - // callback. - const SiteIndex &sites = run.ledger().siteIndex(); - bool boundary = false; - for (core::SiteId id : sites.sitesOf(call)) - if (const SiteInfo *info = sites.info(id)) { - if (info->boundary) - boundary = true; - if (info->library && info->library->entry->hasCallback()) - boundary = true; - // RFC 0030 §2.1: the exit a call that does not return stands for. - if (info->boundary == core::Boundary::Exit && - run.ledger().applies(info->id, core::Facet::Temporal)) - run.ledger().decideAs(call, info->kind, info->boundary, - core::Facet::Temporal, - core::FacetDecision::proven()); - } - if (!boundary) - return; - UnitRun &unit = run.unitRun(); - std::vector roots; - for (unsigned i = 0; i < call.getNumArgs() && i < args.size(); ++i) { - if (args[i] == core::ZeroSym) - continue; - const core::SymInfo &value = heap.info(state, args[i]); - if (value.type != core::SymInfo::Type::Pointer) - continue; - for (const core::Target &target : value.targets) - roots.push_back(Reached{.object = target.object, - .path = core::SummaryPath::param(i).deref()}); - } - for (const auto &[id, object] : state.objects) { - const core::ObjectInfo &info = run.table().info(id); - if (info.key.kind != core::ObjectKind::Global) - continue; - const auto *var = fromHandle(info.key.handle); - if (var == nullptr) - continue; - roots.push_back(Reached{ - .object = id, .path = core::SummaryPath::global(unit.globalId(*var))}); - } - BoundaryFacts facts; - std::set> seen; - // Owning cells the callee reaches, for *owner uniqueness*. - struct Owner { - core::ObjectId holder; - core::SummaryPath place; - std::string placeClass; - std::string name; - core::Sym value; - core::Target target; - }; - std::vector owners; - for (const Reached &reached : reachWithPaths(run, state, roots, true)) { - // (A holder that may be gone is its own temporal fact: its cells matter - // only where it is live, as at an exit.) - const core::ObjectState *object = state.objects.find(reached.object); - if (object == nullptr || object->life != core::Life::Live) - continue; - for (const auto &[key, sym] : object->cells) { - const core::SymInfo &value = heap.info(state, sym); - if (value.type != core::SymInfo::Type::Pointer) - continue; - if (isOwningCell(run, reached.object, key) && !value.top && - value.targets.size() == 1 && value.null != core::PointerNull::Null && - run.table().info(value.targets[0].object).singular) { - std::string placeClass = cellClass(run, reached.object, key); - if (!placeClass.empty()) - owners.push_back( - Owner{.holder = reached.object, - .place = cellPath(run, reached.path, reached.object, key), - .placeClass = std::move(placeClass), - .name = cellName(run, reached.object, key), - .value = sym, - .target = value.targets[0]}); - } - auto released = releasedValue(heap, state, value); - if (!released) - continue; - std::string placeClass = cellClass(run, reached.object, key); - if (placeClass.empty()) - continue; - core::SummaryPath place = - cellPath(run, reached.path, reached.object, key); - if (!seen.insert({place, placeClass}).second) - continue; - facts.dangling.push_back( - BoundaryFacts::Dangling{.place = place, - .released = *released, - .placeClass = std::move(placeClass), - .name = cellName(run, reached.object, key)}); - } - } - // *Owner uniqueness*: two owning places the other side can reach that - // hold the same object; what it releases through one it releases - // through the other. - for (std::size_t i = 0; i < owners.size(); ++i) - for (std::size_t j = i + 1; j < owners.size(); ++j) { - const Owner &a = owners[i]; - const Owner &b = owners[j]; - if (a.place == b.place) - continue; - if (a.value != b.value && !(a.target == b.target)) - continue; - facts.sharedOwners.push_back( - BoundaryFacts::SharedOwners{.first = a.place, - .second = b.place, - .placeClass = a.placeClass, - .otherClass = b.placeClass, - .names = a.name + "' and '" + b.name}); - } - // RFC 0031 §8, the owner forest: an owning cell whose object is - // reachable, through owning cells, from the object it owns. - std::map> owns; - for (const Owner &owner : owners) - owns[owner.holder].push_back(owner.target.object); - for (const Owner &owner : owners) { - std::set seenObjects; - std::vector work{owner.target.object}; - bool cycle = false; - while (!work.empty() && !cycle) { - core::ObjectId id = work.back(); - work.pop_back(); - if (id == owner.holder) { - cycle = true; - break; - } - if (!seenObjects.insert(id).second) - continue; - if (auto it = owns.find(id); it != owns.end()) - work.insert(work.end(), it->second.begin(), it->second.end()); - } - if (cycle) - facts.sharedOwners.push_back( - BoundaryFacts::SharedOwners{.first = owner.place, - .second = owner.place, - .placeClass = owner.placeClass, - .otherClass = owner.placeClass, - .names = owner.name, - .cycle = true}); - } - if (!facts.dangling.empty() || !facts.sharedOwners.empty()) - run.ledger().boundary(call, std::move(facts)); -} - -//===----------------------------------------------------------------------===// -// Exits (§5.7) -//===----------------------------------------------------------------------===// - -/// Decides the temporal facet of the exit site of `exit`. -static void decideExit(const FunctionRun &run, const Stmt &exit, - const core::FacetDecision &decision) { - const SiteIndex &sites = run.ledger().siteIndex(); - auto id = sites.findExit(exit); - if (!id) - return; - const SiteInfo *info = sites.info(*id); - if (info == nullptr || !run.ledger().applies(*id, core::Facet::Temporal)) - return; - run.ledger().decideAs(*info->stmt, info->kind, info->boundary, - core::Facet::Temporal, decision); -} - -void Transfer::exitLifetimes(const Stmt &exit, const ReturnStmt *ret) { - if (!run.isPublishing() || state.unreachable) - return; - const SourceManager &sm = context.getSourceManager(); - auto lifetimeError = [&](const std::string &holder, core::ObjectId frame, - SourceLocation at, bool definite) { - std::string name = run.frameName(frame); - core::Diagnostic diagnostic; - diagnostic.id = core::diag::LifetimeTooShort; - diagnostic.severity = - definite ? core::Severity::Error : core::Severity::Warning; - diagnostic.message = - "'" + holder + "' may outlive '" + name + "', which it points to"; - diagnostic.location = toCoreLocation(sm, at); - const core::ObjectInfo &info = run.table().info(frame); - if (info.created.isValid()) { - diagnostic.addNote("'" + name + "' is declared here", info.created); - // Where it goes out of scope: the end of the block that declares it, - // or this exit for the function's own scope. - SourceLocation end = isa(exit) - ? cast(exit).getRBracLoc() - : exit.getBeginLoc(); - if (const VarDecl *var = run.localVariable(frame)) { - std::vector> work{ - {run.decl().getBody(), nullptr}}; - while (!work.empty()) { - auto [s, block] = work.back(); - work.pop_back(); - if (s == nullptr) - continue; - if (const auto *decls = dyn_cast(s)) { - if (std::find(decls->decl_begin(), decls->decl_end(), var) != - decls->decl_end() && - block != nullptr && block != run.decl().getBody()) - end = block->getRBracLoc(); - continue; - } - const auto *compound = dyn_cast(s); - for (const Stmt *child : s->children()) - work.emplace_back(child, compound != nullptr ? compound : block); - } - } - if (end.isValid()) - diagnostic.addNote("'" + name + "' goes out of scope here", - toCoreLocation(sm, end)); - } - decideExit(run, exit, - definite ? core::FacetDecision::violation() - : core::FacetDecision::unresolvedFor( - core::UnresolvedReason::MayDangle)); - run.report(std::move(diagnostic), - definite ? core::Certainty::Definite : core::Certainty::Possible, - &exit, core::Facet::Temporal); - }; - // The frame objects a value may point to, and whether it points to - // nothing else. - auto framesOf = [&](const core::SymInfo &value, bool &only) { - std::vector frames; - only = !value.targets.empty() && !value.top && - value.null == core::PointerNull::NonNull; - for (const core::Target &target : value.targets) { - if (run.isFrameObject(state, target.object)) - frames.push_back(target.object); - else - only = false; - } - return frames; - }; - // A record returned by value that holds a pointer to the frame. - if (ret != nullptr && ret->getRetValue() != nullptr && - ret->getRetValue()->getType()->isRecordType()) { - Address address = addressOf(*ret->getRetValue()); - for (const core::Target &target : address.targets) { - const core::ObjectState *object = heap.findObject(state, target.object); - if (object == nullptr) - continue; - for (const auto &[key, sym] : object->cells) { - const core::SymInfo &value = heap.info(state, sym); - if (value.type != core::SymInfo::Type::Pointer) - continue; - bool only = false; - std::vector frames = framesOf(value, only); - if (frames.empty()) - continue; - lifetimeError(spell(*ret->getRetValue()), frames.front(), - ret->getRetValue()->getBeginLoc(), - only && address.targets.size() == 1); - break; - } - } - } - // Cells the caller can reach: below the parameters and the globals. - std::vector roots; - UnitRun &unit = run.unitRun(); - for (const auto &[id, object] : state.objects) { - const core::ObjectInfo &info = run.table().info(id); - if (info.key.dead) - continue; - switch (info.key.kind) { - case core::ObjectKind::Global: - if (const auto *var = fromHandle(info.key.handle)) - roots.push_back( - Reached{.object = id, - .path = core::SummaryPath::global(unit.globalId(*var))}); - break; - case core::ObjectKind::Entry: - case core::ObjectKind::EntrySummary: - roots.push_back(Reached{.object = id, .path = info.key.path}); - break; - default: - break; - } - } - BoundaryFacts facts; - for (const Reached &reached : reachWithPaths(run, state, roots, false)) { - if (run.isFrameObject(state, reached.object)) - continue; - const core::ObjectState *object = state.objects.find(reached.object); - if (object == nullptr || object->life != core::Life::Live) - continue; - bool global = - run.table().info(reached.object).key.kind == core::ObjectKind::Global; - for (const auto &[key, sym] : object->cells) { - const core::SymInfo &value = heap.info(state, sym); - if (value.type != core::SymInfo::Type::Pointer) - continue; - bool only = false; - std::vector frames = framesOf(value, only); - if (frames.empty()) - continue; - core::ObjectId frame = frames.front(); - // RFC 0030 §9.4 amendment 1: storage whose lifetime ended, where the - // caller finds it. - std::string placeClass = cellClass(run, reached.object, key); - if (!placeClass.empty()) - facts.dangling.push_back(BoundaryFacts::Dangling{ - .place = cellPath(run, reached.path, reached.object, key), - .released = {}, - .placeClass = std::move(placeClass), - .name = cellName(run, reached.object, key)}); - // §5.7: a global that held the storage at a call was handed to the - // callee; what it holds at the exit is the boundary's fact only. - if (global && run.exposedFrames.contains(frame)) - continue; - const FunctionRun::FrameStore *stored = - run.frameStore(reached.object, key, frame); - std::string holder = stored != nullptr && !stored->holder.empty() - ? stored->holder - : cellName(run, reached.object, key); - // RFC 0033 §1: storing the frame's address where the caller can - // reach it accesses no memory; only a use of it after the frame ends - // would, which the analysis reports where it sees it. A warning, and - // nothing to trap when it is lowered. - lifetimeError(holder, frame, - stored != nullptr ? stored->at->getBeginLoc() - : exit.getBeginLoc(), - false); - (void)only; - } - } - if (!facts.dangling.empty()) - run.ledger().boundary(exit, std::move(facts)); - // §5.7: storage of this frame left in objects the result reaches (a new - // object the function returns) outlives the frame too. - if (ret == nullptr || state.result == core::ZeroSym) - return; - const core::SymInfo &result = heap.info(state, state.result); - if (result.type != core::SymInfo::Type::Pointer || result.top) - return; - std::set seen; - for (const Reached &reached : reachWithPaths(run, state, roots, false)) - seen.insert(reached.object); - std::vector fromResult; - for (const core::Target &target : result.targets) { - core::ObjectKind kind = run.table().info(target.object).key.kind; - if ((kind == core::ObjectKind::HeapRecent || - kind == core::ObjectKind::HeapOld) && - !seen.contains(target.object)) - fromResult.push_back( - Reached{.object = target.object, - .path = core::SummaryPath::result().deref()}); - } - if (fromResult.empty()) - return; - for (const Reached &reached : reachWithPaths(run, state, fromResult, false)) { - if (seen.contains(reached.object) || - run.isFrameObject(state, reached.object)) - continue; - const core::ObjectState *object = state.objects.find(reached.object); - if (object == nullptr || object->life != core::Life::Live) - continue; - for (const auto &[key, sym] : object->cells) { - const core::SymInfo &value = heap.info(state, sym); - if (value.type != core::SymInfo::Type::Pointer) - continue; - bool only = false; - std::vector frames = framesOf(value, only); - if (frames.empty()) - continue; - const FunctionRun::FrameStore *stored = - run.frameStore(reached.object, key, frames.front()); - std::string holder = stored != nullptr && !stored->holder.empty() - ? stored->holder - : cellName(run, reached.object, key); - lifetimeError(holder, frames.front(), - stored != nullptr ? stored->at->getBeginLoc() - : exit.getBeginLoc(), - false); - (void)only; - } - } -} - -//===----------------------------------------------------------------------===// -// Raw pointers (RFC 0004) -//===----------------------------------------------------------------------===// - -bool FunctionRun::inUnsafeRegion(const Stmt &stmt) const { - if (const SiteIndex::FunctionSites *sites = - out.siteIndex().function(function); - sites != nullptr && sites->unsafe) - return true; - for (const FunctionDecl *redecl : function.redecls()) - if (getAnnotations(*redecl).unsafe) - return true; - for (const Stmt *at = &stmt; at != nullptr; at = parentOf(*at)) - if (isUnsafeBlock(*at)) - return true; - return false; -} - -const Stmt *FunctionRun::parentOf(const Stmt &stmt) const { - if (!parentMap && function.getBody() != nullptr) - parentMap = std::make_unique(function.getBody()); - return parentMap ? parentMap->getParent(&stmt) : nullptr; -} - -bool FunctionRun::isDiscarded(const Expr &expr) const { - for (const Stmt *at = parentOf(expr); at != nullptr; at = parentOf(*at)) { - if (const auto *cast = dyn_cast(at)) { - if (cast->getCastKind() == CK_ToVoid) - return true; - if (isa(cast)) - continue; - return false; - } - if (!isa(at)) - return false; - } - return false; -} - -/// The local an argument passes the address of: `&x`, `&x.f`, `&x[i]`, or -/// an array `x` decaying. -static const VarDecl *addressedLocal(const Expr &argument) { - const Expr *expr = argument.IgnoreParenCasts(); - bool addressed = false; - if (const auto *unary = dyn_cast(expr); - unary != nullptr && unary->getOpcode() == UO_AddrOf) { - addressed = true; - expr = unary->getSubExpr(); - } - for (unsigned depth = 0; depth < 16; ++depth) { - expr = expr->IgnoreParenImpCasts(); - if (const auto *member = dyn_cast(expr); - member != nullptr && !member->isArrow()) { - expr = member->getBase(); - continue; - } - if (const auto *subscript = dyn_cast(expr)) { - expr = subscript->getBase(); - continue; - } - break; - } - const auto *ref = dyn_cast(expr); - const auto *var = - ref != nullptr ? dyn_cast(ref->getDecl()) : nullptr; - if (var == nullptr || !var->hasLocalStorage()) - return nullptr; - return addressed || var->getType()->isArrayType() ? var : nullptr; -} - -bool FunctionRun::isPassedToCall(const VarDecl &var) const { - if (!passedToCalls) { - passedToCalls.emplace(); - std::vector pending{function.getBody()}; - while (!pending.empty()) { - const Stmt *stmt = pending.back(); - pending.pop_back(); - if (stmt == nullptr) - continue; - if (const auto *call = dyn_cast(stmt)) - for (const Expr *argument : call->arguments()) - if (const VarDecl *local = addressedLocal(*argument)) - passedToCalls->insert(local->getCanonicalDecl()); - for (const Stmt *child : stmt->children()) - pending.push_back(child); - } - } - return passedToCalls->contains(var.getCanonicalDecl()); -} - -bool FunctionRun::isBypassed(const Expr &operand) const { - const auto *ref = dyn_cast(operand.IgnoreParenImpCasts()); - const auto *var = - ref != nullptr ? dyn_cast(ref->getDecl()) : nullptr; - if (var == nullptr || function.getBody() == nullptr) - return false; - if (!bypassed) { - bypassed.emplace(); - for (const VarDecl *declared : bypassedDeclarations(*function.getBody())) - bypassed->insert(declared->getCanonicalDecl()); - } - return bypassed->contains(var->getCanonicalDecl()); -} - -core::Sym Transfer::launder(core::Sym value, const AnnotationSet &declared, - const std::string &target, const Expr &source) { - if (!declared.safeOwnership()) - return value; - const core::SymInfo info = heap.info(state, value); - if (info.type != core::SymInfo::Type::Pointer || !info.raw) - return value; - if (run.isPublishing() && !run.inUnsafeRegion(source)) { - const Expr *stripped = source.IgnoreParenImpCasts(); - std::string phrase = "raw pointer"; - std::string name; - if (isa(stripped) || isa(stripped)) { - name = spell(*stripped); - phrase += " '" + name + "'"; - } - std::string message = - target.empty() - ? phrase + - " is returned from a function whose return type is " - "annotated " + - macroSpelling(declared) + " outside an unsafe region" - : phrase + " is assigned to '" + target + "', which is declared " + - macroSpelling(declared) + ", outside an unsafe region"; - core::Diagnostic diagnostic; - diagnostic.id = core::diag::UnsafeOperation; - diagnostic.severity = core::Severity::Error; - diagnostic.message = std::move(message); - const SourceManager &sm = context.getSourceManager(); - diagnostic.location = toCoreLocation(sm, source.getBeginLoc()); - if (info.rawAt.isValid()) { - // RFC 0033 §2: a raw value is declared so, or came from one. - std::string origin = "declared WEAVEC_RAW"; - if (info.rawOrigin == core::SymInfo::RawOrigin::Loaded) - origin = "loaded through a raw pointer"; - else if (info.rawOrigin == core::SymInfo::RawOrigin::Returned) - origin = "handed out by a callee"; - diagnostic.addNote((name.empty() ? std::string("the pointer is raw: ") - : "'" + name + "' is raw: ") + - origin + " here", - info.rawAt); - } - diagnostic.addNote("move this operation into a WEAVEC_UNSAFE block or " - "function, or assert the pointer's ownership first", - toCoreLocation(sm, source.getBeginLoc())); - run.report(std::move(diagnostic), core::Certainty::Definite, nullptr, - std::nullopt); - } - // The assertion holds from here on either way: not asserting would only - // cascade into a report per later use. - core::SymInfo asserted = info; - asserted.raw = false; - asserted.pending.clear(); - asserted.condition.reset(); - return heap.fresh(state, asserted); -} - -void Transfer::inlineAssembly(const GCCAsmStmt &assembly) { - // RFC 0030 §5.7: the statement may have released, retained or replaced - // what its pointer operands reach, as an unknown callee may. - core::ReleaseRecord record; - record.reason = core::ReleaseRecord::Reason::UnknownCallee; - record.via = "inline assembly"; - record.where = - toCoreLocation(context.getSourceManager(), assembly.getBeginLoc()); - record.allPaths = false; - std::vector start; - auto operand = [&](const Expr *expr) { - if (expr == nullptr || !expr->getType()->isPointerType()) - return; - for (const core::Target &target : heap.info(state, valueOf(*expr)).targets) - start.push_back(target.object); - }; - for (const Expr *input : assembly.inputs()) - operand(input); - for (const Expr *output : assembly.outputs()) { - operand(output); - // What an output operand holds afterwards is unknown. - if (output != nullptr && output->isGLValue()) { - Address address = addressOf(*output); - store(address, unknownValue(output->getType()), output->getType(), - nullptr); - } - } - unknownEffect(start, record, /*mayOwn=*/false); -} - -void Transfer::cleanupFunction(const VarDecl &var) { - // RFC 0030 §5.1: `__attribute__((cleanup(f)))` calls `f(&var)` where the - // variable's scope ends, at no call expression. Until that call is - // analysed like any other, it is an unknown callee handed the variable: - // it may have released, retained or replaced what the variable reaches. - const auto *attribute = var.getAttr(); - core::ReleaseRecord record; - record.reason = core::ReleaseRecord::Reason::UnknownCallee; - record.via = attribute != nullptr && attribute->getFunctionDecl() != nullptr - ? attribute->getFunctionDecl()->getNameAsString() - : std::string("a cleanup function"); - record.where = toCoreLocation(context.getSourceManager(), var.getLocation()); - record.allPaths = false; - // The function was handed the last pointer to what the variable owns: - // releasing it is its job, so nothing leaks here. - unknownEffect({run.variableObject(var)}, record, /*mayOwn=*/true); -} - -void Transfer::unknownEffect(const std::vector &start, - const core::ReleaseRecord &record, bool mayOwn) { - std::set seen(start.begin(), start.end()); - std::vector work = start; - while (!work.empty()) { - core::ObjectId id = work.back(); - work.pop_back(); - if (!state.objects.contains(id)) - continue; - for (const auto &[key, sym] : state.objects.at(id).cells) - for (const core::Target &target : heap.info(state, sym).targets) - if (seen.insert(target.object).second) - work.push_back(target.object); - } - for (core::ObjectId id : seen) { - if (!state.objects.contains(id)) - continue; - core::ObjectKind kind = run.table().info(id).key.kind; - core::ObjectState &object = state.objects.at(id); - object.cells = {}; - object.havocked = true; - object.escaped = true; - if (kind != core::ObjectKind::Local && kind != core::ObjectKind::Global && - kind != core::ObjectKind::Literal && - kind != core::ObjectKind::Function && object.life == core::Life::Live) { - object.life = core::Life::UnknownReleased; - object.record = record; - if (mayOwn) - object.owned = false; - } - } -} - -//===----------------------------------------------------------------------===// -// Assumptions (RFC 0030 §6.2) -//===----------------------------------------------------------------------===// - -/// `e` in `weavec_assume_((e) != 0)`. -static const Expr &assumedExpression(const Expr &argument) { - const Expr *e = argument.IgnoreParenImpCasts(); - if (const auto *compare = dyn_cast(e); - compare != nullptr && compare->getOpcode() == BO_NE) - if (const auto *zero = - dyn_cast(compare->getRHS()->IgnoreParenImpCasts()); - zero != nullptr && zero->getValue() == 0) - return *compare->getLHS()->IgnoreParens(); - return *e; -} - -/// Refines `state` by `e` being `truth`; false when that is infeasible. -static bool assume(FunctionRun &run, core::HeapState &state, const Expr &e, - bool truth, int depth) { - const Expr *x = e.IgnoreParens(); - if (depth > 16) - return true; - if (const auto *cast = dyn_cast(x)) { - switch (cast->getCastKind()) { - case CK_NoOp: - case CK_IntegralCast: - case CK_IntegralToBoolean: - case CK_BooleanToSignedIntegral: - return assume(run, state, *cast->getSubExpr(), truth, depth + 1); - default: - break; - } - } - if (const auto *unary = dyn_cast(x); - unary != nullptr && unary->getOpcode() == UO_LNot) - return assume(run, state, *unary->getSubExpr(), !truth, depth + 1); - if (const auto *binary = dyn_cast(x)) { - BinaryOperatorKind op = binary->getOpcode(); - bool conjunction = (op == BO_LAnd && truth) || (op == BO_LOr && !truth); - bool disjunction = (op == BO_LAnd && !truth) || (op == BO_LOr && truth); - if (conjunction) - return assume(run, state, *binary->getLHS(), truth, depth + 1) && - assume(run, state, *binary->getRHS(), truth, depth + 1); - if (disjunction) { - // `!a || (a && !b)` for `!(a && b)`; `a || (!a && b)` for `a || b`. - bool first = op == BO_LOr; - core::HeapState left = state; - bool leftFeasible = - assume(run, left, *binary->getLHS(), first, depth + 1); - core::HeapState right = state; - bool rightFeasible = - assume(run, right, *binary->getLHS(), !first, depth + 1) && - assume(run, right, *binary->getRHS(), first, depth + 1); - if (!leftFeasible && !rightFeasible) - return false; - if (leftFeasible && rightFeasible) - state = run.domain().join(left, right, handleOf(binary)); - else - state = leftFeasible ? std::move(left) : std::move(right); - return true; - } - // `x != 0` and `x == 0` test `x`. - if (op == BO_NE || op == BO_EQ) - if (const auto *zero = - dyn_cast(binary->getRHS()->IgnoreParenImpCasts()); - zero != nullptr && zero->getValue() == 0 && - !binary->getLHS()->getType()->isPointerType()) - return assume(run, state, *binary->getLHS(), - op == BO_NE ? truth : !truth, depth + 1); - // A comparison over the operands' values here (a value carried from - // another block has lost its condition at the join). - if (binary->isComparisonOp() && !binary->HasSideEffects(run.ast())) { - Transfer scratch(run, state); - core::Sym condition = scratch.comparison(*binary); - return Transfer::refine(run, state, condition, truth); - } - } - // Any other expression: its value, evaluated in `state`. - if (x->HasSideEffects(run.ast())) - return true; - Transfer scratch(run, state); - core::Sym value = scratch.valueOf(*x); - return Transfer::refine(run, state, value, truth); -} - -bool Transfer::assumption(const CallExpr &call) { - const FunctionDecl *callee = call.getDirectCallee(); - if (callee == nullptr || call.getNumArgs() != 1) - return false; - bool annotated = false; - for (const FunctionDecl *redecl : callee->redecls()) - annotated = annotated || getAnnotations(*redecl).assume; - if (!annotated) - return false; - const Expr &condition = *call.getArg(0); - if (run.isPublishing()) { - const SiteIndex &sites = run.ledger().siteIndex(); - std::optional id = sites.find(call, core::SiteKind::Assume); - core::HeapState holds = state; - bool canHold = assume(run, holds, condition, true, 0); - core::HeapState fails = state; - bool canFail = assume(run, fails, condition, false, 0); - if (id && run.ledger().applies(*id, core::Facet::Assertion)) { - core::FacetDecision decision = - core::FacetDecision::unresolvedFor(core::UnresolvedReason::Undecided); - if (!canHold) - decision = core::FacetDecision::violation(); - else if (!canFail) - decision = core::FacetDecision::proven(); - run.ledger().decideAs(call, core::SiteKind::Assume, std::nullopt, - core::Facet::Assertion, decision); - } - if (!canHold) { - const Expr &assumed = assumedExpression(condition); - const SourceManager &sm = context.getSourceManager(); - CharSourceRange range = Lexer::makeFileCharRange( - CharSourceRange::getTokenRange(assumed.getSourceRange()), sm, - context.getLangOpts()); - std::string text = - Lexer::getSourceText(range, sm, context.getLangOpts()).str(); - if (text.empty()) { - llvm::raw_string_ostream os(text); - assumed.printPretty(os, nullptr, context.getPrintingPolicy()); - } - core::Diagnostic diagnostic; - diagnostic.id = core::diag::ContradictedAssumption; - diagnostic.severity = core::Severity::Error; - diagnostic.message = "assumption '" + text + "' is false here"; - diagnostic.location = toCoreLocation(sm, call.getBeginLoc()); - // The facts that refute it, where the variable got its value. - std::vector operands{&assumed}; - std::set noted; - while (!operands.empty()) { - const Expr *e = operands.back(); - operands.pop_back(); - if (const auto *ref = dyn_cast(e->IgnoreParenImpCasts())) - if (const auto *var = dyn_cast(ref->getDecl()); - var != nullptr && var->getType()->isIntegerType() && - noted.insert(var).second) { - core::ObjectId object = run.variableObject(*var); - if (auto held = heap.read(state, object, core::CellKey{})) - if (auto value = state.zone.constant(*held)) - diagnostic.addNote("'" + var->getNameAsString() + "' is " + - std::to_string(*value) + " here", - toCoreLocation(sm, var->getLocation())); - } - for (const Stmt *child : e->children()) - if (const auto *operand = dyn_cast_or_null(child)) - operands.push_back(operand); - } - run.report(std::move(diagnostic), core::Certainty::Definite, &call, - core::Facet::Assertion); - } - } - // The analysis assumes `e` from here on, as on the true edge of `if (e)`: - // sound in the enforcing modes, which trap first. A refuted assumption - // ends the path. - if (!assume(run, state, condition, true, 0)) - state.unreachable = true; - return true; -} - -} // namespace weavec::analysis::engine diff --git a/lib/Analysis/EngineRun.cpp b/lib/Analysis/EngineRun.cpp deleted file mode 100644 index 3ed17ada..00000000 --- a/lib/Analysis/EngineRun.cpp +++ /dev/null @@ -1,2539 +0,0 @@ -//===- EngineRun.cpp - One function body in the object engine -------------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// RFC 0031 §3–§4: the always-add CFG, the entry state, the fixpoint with -// widening at loop heads, and the final (publishing) pass. Objects are -// interned here, and the unwritten cells of the entry heap are materialised -// (§4.6). -// -//===----------------------------------------------------------------------===// - -#include "Engine.h" -#include "weavec/Analysis/Annotations.h" -#include "weavec/Analysis/BypassedDeclarations.h" -#include "weavec/Analysis/ClangLocation.h" - -#include "clang/AST/ParentMap.h" -#include "clang/AST/RecordLayout.h" -#include "clang/AST/RecursiveASTVisitor.h" -#include "clang/Basic/SourceManager.h" - -#include -#include -#include -#include -#include -#include -#include - -using namespace clang; - -namespace weavec::analysis::engine { - -core::Handle typeHandle(QualType type) noexcept { - if (type.isNull()) - return 0; - return handleOf(type.getCanonicalType().getAsOpaquePtr()); -} - -QualType typeOfHandle(core::Handle handle) noexcept { - // NOLINTBEGIN(cppcoreguidelines-pro-type-reinterpret-cast,performance-no-int-to-ptr): - // a type handle is the type's opaque pointer. - return QualType::getFromOpaquePtr( - reinterpret_cast(static_cast(handle))); - // NOLINTEND(cppcoreguidelines-pro-type-reinterpret-cast,performance-no-int-to-ptr) -} - -/// The largest number of times one field may repeat in an entry path before -/// the path is folded (§4.6 k-limit), and the longest path. -static constexpr unsigned MaxFieldRepeats = 2; -static constexpr std::size_t MaxEntryDepth = 6; -/// §4.8: widening after this many joins at a loop head. -static constexpr unsigned WidenAfter = 2; -/// Widening rounds a loop head stops at program constants before a growing -/// bound is dropped (§4.8). -static constexpr unsigned ThresholdRounds = 8; -static constexpr unsigned MaxVisitsPerBlock = 64; -/// A head many edges reach may change once or twice a round per edge. -static constexpr unsigned VisitsPerPredecessor = 2; -/// Joins at one loop head, changing or not, before the function is over -/// budget: a head that many back edges reach and that keeps changing is -/// not converging, and each join of a large state is costly. A head that -/// many edges reach (an interpreter's dispatch, one edge per opcode) gets -/// as many per edge, so a round of them does not exhaust it. -static constexpr unsigned MaxJoinsPerBlock = 256; -static constexpr unsigned JoinsPerPredecessor = 64; -/// RFC 0034 §7.1: the size of a run's entry states (memory), and of its -/// graph, over budget. -static constexpr std::uint64_t MaxRetainedState = 1000000; -static constexpr unsigned MaxBlocks = 100000; -/// Symbols a block's joined state may hold before the function is over -/// budget: every later join pairs them all (sqlite's `sqlite3VdbeExec` -/// reached 4,677; the next largest in the corpus, under 1,000). -static constexpr std::size_t MaxStateSymbols = 2048; - -FunctionRun::FunctionRun(UnitRun &unitRun, const FunctionDecl &fn, - LedgerAdapter &adapter, RunMode runMode, - const AliasContext *alias, unsigned depth) - : unit(unitRun), function(fn), aliasContext(alias), contextDepth(depth), - context(unitRun.context()), out(adapter), publishTo(&adapter), - mode(runMode), heap(objects, *this) {} - -FunctionRun::~FunctionRun() = default; - -//===----------------------------------------------------------------------===// -// Objects -//===----------------------------------------------------------------------===// - -/// The name a declaration is spelled with. -static std::string nameOf(const NamedDecl &decl) { - return decl.getNameAsString(); -} - -core::ObjectId FunctionRun::variableObject(const VarDecl &var) const { - const VarDecl *canonical = var.getCanonicalDecl(); - if (auto it = localObjects.find(canonical); it != localObjects.end()) - return it->second; - core::ObjectKey key; - key.kind = var.hasGlobalStorage() ? core::ObjectKind::Global - : core::ObjectKind::Local; - key.handle = handleOf(canonical); - core::ObjectInfo info; - info.type = typeHandle(var.getType()); - info.singular = true; - info.name = nameOf(var); - info.created = toCoreLocation(context.getSourceManager(), var.getLocation()); - core::ObjectId id = objects.intern(key, info); - localObjects[canonical] = id; - return id; -} - -core::ObjectId FunctionRun::literalObject(const Expr &literal) const { - core::ObjectKey key; - key.kind = isa(literal) || isa(literal) - ? core::ObjectKind::Literal - : core::ObjectKind::Local; - key.handle = handleOf(&literal); - key.expression = key.kind == core::ObjectKind::Local; - core::ObjectInfo info; - info.type = typeHandle(literal.getType()); - info.name = key.kind == core::ObjectKind::Literal ? "a string literal" - : "a compound literal"; - info.created = - toCoreLocation(context.getSourceManager(), literal.getBeginLoc()); - return objects.intern(key, info); -} - -core::ObjectId FunctionRun::recordResultObject(const CallExpr &call) const { - core::ObjectKey key; - key.kind = core::ObjectKind::Local; - key.handle = handleOf(&call); - key.expression = true; - core::ObjectInfo info; - info.type = typeHandle(call.getType()); - const FunctionDecl *callee = call.getDirectCallee(); - info.name = "the result of '" + - (callee != nullptr ? callee->getNameAsString() - : std::string("an indirect call")) + - "'"; - info.created = toCoreLocation(context.getSourceManager(), call.getBeginLoc()); - return objects.intern(key, info); -} - -core::ObjectId -FunctionRun::allocationObject(const Expr &site, QualType pointee, - const std::string &name, - const core::SummaryPath &path) const { - core::ObjectKey key; - key.kind = core::ObjectKind::HeapRecent; - key.handle = handleOf(&site); - key.path = path; - core::ObjectInfo info; - info.type = pointee.isNull() || pointee->isVoidType() || pointee->isCharType() - ? 0 - : typeHandle(pointee); - info.name = name; - info.created = toCoreLocation(context.getSourceManager(), site.getBeginLoc()); - return objects.intern(key, info); -} - -core::ObjectId FunctionRun::callResultObject(const Expr &site, QualType pointee, - const std::string &name) const { - core::ObjectKey key; - key.kind = core::ObjectKind::CallResult; - key.handle = handleOf(&site); - key.path = core::SummaryPath::result().deref(); - core::ObjectInfo info; - info.type = - pointee.isNull() || pointee->isVoidType() ? 0 : typeHandle(pointee); - info.name = name; - info.created = toCoreLocation(context.getSourceManager(), site.getBeginLoc()); - return objects.intern(key, info); -} - -/// Stable handles for the library's hidden state slots: the address of the -/// slot's name in a process-wide set (never a Clang entity). -static core::Handle stateHandle(const std::string &slot) { - static std::mutex lock; - static std::set slots; - std::scoped_lock guard(lock); - return handleOf(&*slots.insert(slot).first); -} - -core::ObjectId FunctionRun::stateObject(const std::string &slot) const { - core::ObjectKey key; - key.kind = core::ObjectKind::CallResult; - key.handle = stateHandle(slot); - key.path = core::SummaryPath::result().deref(); - key.cell = 1; - core::ObjectInfo info; - info.name = "<" + slot + ">"; - return objects.intern(key, info); -} - -bool FunctionRun::isStateObject(core::ObjectId id) const { - const core::ObjectKey &key = objects.info(id).key; - return key.kind == core::ObjectKind::CallResult && key.cell == 1 && - key.path == core::SummaryPath::result().deref(); -} - -core::ObjectId FunctionRun::unknownObject() const { - core::ObjectKey key; - key.kind = core::ObjectKind::Unknown; - core::ObjectInfo info; - info.singular = false; - info.name = "an unknown object"; - return objects.intern(key, info); -} - -/// The field at byte `offset` of `type`, descending into nested records and -/// arrays, with its byte offset; for an array, the element type at the -/// offset with no field. -namespace { -struct FieldAt { - const FieldDecl *field = nullptr; - QualType type; -}; -} // namespace - -static std::optional fieldAt(const ASTContext &context, QualType type, - std::int64_t offset) { - type = type.getCanonicalType(); - for (int depth = 0; depth < 16; ++depth) { - if (offset < 0) - return std::nullopt; - if (const auto *array = context.getAsArrayType(type)) { - QualType element = array->getElementType(); - if (element->isIncompleteType()) - return std::nullopt; - std::int64_t size = static_cast( - context.getTypeSizeInChars(element).getQuantity()); - if (size <= 0) - return std::nullopt; - offset %= size; - type = element.getCanonicalType(); - continue; - } - const RecordDecl *record = type->getAsRecordDecl(); - if (record == nullptr || !record->isCompleteDefinition()) { - if (offset == 0) - return FieldAt{.field = nullptr, .type = type}; - return std::nullopt; - } - const ASTRecordLayout &layout = context.getASTRecordLayout(record); - const FieldDecl *found = nullptr; - std::int64_t foundOffset = 0; - for (const FieldDecl *field : record->fields()) { - auto fieldOffset = static_cast( - layout.getFieldOffset(field->getFieldIndex()) / - context.getCharWidth()); - QualType fieldType = field->getType(); - std::int64_t fieldSize = - fieldType->isIncompleteType() - ? 0 - : static_cast( - context.getTypeSizeInChars(fieldType).getQuantity()); - bool inside = - offset >= fieldOffset && (offset < fieldOffset + fieldSize || - (fieldSize == 0 && offset == fieldOffset) || - fieldType->isIncompleteArrayType()); - if (inside) { - found = field; - foundOffset = fieldOffset; - if (!record->isUnion()) - break; - // A union: prefer a pointer member. - if (fieldType->isPointerType()) - break; - } - } - if (found == nullptr) - return std::nullopt; - offset -= foundOffset; - QualType fieldType = found->getType().getCanonicalType(); - if (offset == 0 && !fieldType->isRecordType() && - !context.getAsArrayType(fieldType)) - return FieldAt{.field = found, .type = fieldType}; - if (fieldType->isRecordType() || context.getAsArrayType(fieldType)) { - type = fieldType; - if (offset == 0 && !fieldType->isRecordType()) { - // The first element of an array member. - continue; - } - continue; - } - return FieldAt{.field = found, .type = fieldType}; - } - return std::nullopt; -} - -/// The entry path of an object whose cells hold entry values, if it has one. -static std::optional -entryPathOf(const core::ObjectTable &objects, core::ObjectId id, - const FunctionDecl &function, UnitRun &unit) { - const core::ObjectInfo &info = objects.info(id); - switch (info.key.kind) { - case core::ObjectKind::Global: - if (const auto *var = - dyn_cast_or_null(fromHandle(info.key.handle))) - return core::SummaryPath::global(unit.globalId(*var)); - return std::nullopt; - case core::ObjectKind::Entry: - case core::ObjectKind::EntrySummary: - case core::ObjectKind::CallResult: - return info.key.path; - case core::ObjectKind::Local: - if (const auto *param = dyn_cast_or_null(variableOf(info))) - for (unsigned i = 0; i < function.getNumParams(); ++i) - if (function.getParamDecl(i)->getCanonicalDecl() == - param->getCanonicalDecl()) - return core::SummaryPath::param(i); - return std::nullopt; - default: - return std::nullopt; - } -} - -core::ObjectId FunctionRun::childEntryObject(const core::HeapState &state, - core::ObjectId parent, - core::CellKey key, - QualType pointee, - const FieldDecl *field) const { - (void)state; - const core::ObjectInfo &parentInfo = objects.info(parent); - std::optional parentPath = - entryPathOf(objects, parent, function, unit); - core::ObjectKey childKey; - core::ObjectInfo info; - info.type = - pointee.isNull() || pointee->isVoidType() ? 0 : typeHandle(pointee); - info.singular = true; - bool owning = - field != nullptr && unit.owningSlots.contains(field->getCanonicalDecl()); - info.fromOwningSlot = owning; - if (owning) - info.ownedFrom = parent; - std::string step = field != nullptr ? field->getNameAsString() - : "#" + std::to_string(key.offset); - // An element no concrete offset names: the objects of every element - // (§4.2 *Amendment (arrays)*), several runtime objects. - bool elements = key.isSummary() || key.isSelected(); - if (elements) { - step = "[*]"; - info.singular = false; - } - if (parentPath) { - core::SummaryPath path = *parentPath; - if (elements) - path = elementsOf(path); - else if (parentInfo.type != 0) - // Through nested members (`box.data`), so a caller's paths resolve. - path = cellPath(context, typeOfHandle(parentInfo.type), path, key.offset, - false); - else if (field != nullptr || key.offset != 0) - path = path.field(step); - path = path.deref(); - // §4.6 k-limit: a field repeated too often, or a path too long, folds - // into a summary of everything below the prefix. - std::map repeats; - bool fold = path.steps.size() > MaxEntryDepth * 2; - for (const core::PathElem &elem : path.steps) - if (elem.step == core::PathStep::Field && - ++repeats[elem.field] > MaxFieldRepeats) - fold = true; - childKey.kind = parentInfo.key.kind == core::ObjectKind::CallResult - ? core::ObjectKind::CallResult - : core::ObjectKind::Entry; - childKey.handle = parentInfo.key.kind == core::ObjectKind::CallResult - ? parentInfo.key.handle - : 0; - if (fold || parentInfo.key.kind == core::ObjectKind::EntrySummary) { - // The summary of the chain: the prefix before the first repeat. - core::SummaryPath prefix = path.rootPath(); - std::map seen; - for (const core::PathElem &elem : path.steps) { - if (elem.step == core::PathStep::Field && ++seen[elem.field] > 1) - break; - prefix.steps.pushBack(elem); - } - childKey.kind = core::ObjectKind::EntrySummary; - childKey.path = std::move(prefix); - info.singular = false; - // §4.5 D3 below the k-limit: a summary reached through an owning slot - // of an entry object stands for objects owned below it (a load through - // it by an owning slot stays owned below that object), so it is one - // object per such owner, and distinct from the owner and what the - // owner is owned by. Reached any other way, it is owned by nothing. - core::ObjectId owner = 0; - if (owning) - owner = parentInfo.key.kind == core::ObjectKind::EntrySummary - ? parentInfo.ownedFrom - : parent; - childKey.parent = owner; - info.ownedFrom = owner; - } else { - childKey.path = std::move(path); - if (key.isConcrete() && childKey.kind == core::ObjectKind::Entry) - info.heldIn = std::make_pair(parent, key.offset); - } - info.name = parentInfo.name.empty() - ? step - : parentInfo.name + (elements ? "[*]" : "->" + step); - } else { - // Below an object with no entry path (an allocation reached by an - // unknown callee, an unknown object): an unknown object. - return unknownObject(); - } - return objects.intern(childKey, info); -} - -/// The extent a kind gives an entry object. -static std::optional extentOfKind(const ASTContext &context, - const KindEntry *kind, - QualType pointee) { - std::optional width; - if (!pointee.isNull() && !pointee->isIncompleteType() && - !pointee->isVoidType() && !pointee->isFunctionType()) - width = static_cast( - context.getTypeSizeInChars(pointee).getQuantity()); - if (kind == nullptr) { - // The A1/A3 Single default: one element. - if (width) - return core::Extent{.bytes = core::Term::of(*width), - .cls = core::ExtentClass::LowerBound}; - return std::nullopt; - } - switch (kind->kind.shape) { - case core::PointerShape::Single: - if (width) - return core::Extent{.bytes = core::Term::of(*width), - .cls = core::ExtentClass::LowerBound}; - return std::nullopt; - case core::PointerShape::Counted: - case core::PointerShape::Sized: - if (kind->kind.extent.isConstant()) { - std::int64_t elementSize = - kind->kind.shape == core::PointerShape::Counted && width ? *width : 1; - core::ExtentClass cls = - kind->extentClass.value_or(core::ExtentClass::LowerBound); - return core::Extent{ - .bytes = core::Term::of(kind->kind.extent.offset * elementSize), - .cls = cls}; - } - return std::nullopt; - default: - return std::nullopt; - } -} - -core::Sym FunctionRun::entryValue(core::HeapState &state, QualType type, - core::ObjectId parent, core::CellKey key, - const FieldDecl *field) const { - type = type.getCanonicalType(); - core::SymInfo value; - if (type->isPointerType()) { - QualType pointee = type->getPointeeType(); - if (pointee->isFunctionType()) { - value.type = core::SymInfo::Type::Function; - value.functionsKnown = false; - core::Sym sym = heap.fresh(state, value); - return sym; - } - core::ObjectId child = childEntryObject(state, parent, key, pointee, field); - heap.ensure(state, child); - const KindEntry *kind = field != nullptr ? unit.fieldKind(*field) : nullptr; - core::ObjectState &childState = heap.object(state, child); - // The kind's extent holds of every object an element stands for. - if (!childState.extent && - (objects.info(child).singular || !key.isConcrete())) - childState.extent = extentOfKind(context, kind, pointee); - // A count in a sibling field (`WEAVEC_COUNTED_BY(len)`). - if (kind != nullptr && field != nullptr && - core::hasExtent(kind->kind.shape) && kind->kind.extent.path && - kind->kind.extent.path->root == core::ExtentPath::Root::Field && - !kind->shapeFromSystemHeader()) { - const RecordDecl *record = field->getParent(); - const FieldDecl *sibling = nullptr; - for (const FieldDecl *candidate : record->fields()) - if (candidate->getName() == kind->kind.extent.path->field) - sibling = candidate; - if (sibling != nullptr && record->isCompleteDefinition() && - sibling->getType()->isIntegralOrEnumerationType()) { - const ASTRecordLayout &layout = context.getASTRecordLayout(record); - std::int64_t base = - key.offset - static_cast( - layout.getFieldOffset(field->getFieldIndex()) / - context.getCharWidth()); - core::CellKey siblingKey{ - .offset = - base + static_cast( - layout.getFieldOffset(sibling->getFieldIndex()) / - context.getCharWidth())}; - core::Sym count = core::ZeroSym; - if (auto existing = heap.read(state, parent, siblingKey)) - count = *existing; - else - count = - unwritten(state, parent, siblingKey, - core::SymInfo{.type = core::SymInfo::Type::Int, - .ctype = typeHandle(sibling->getType())}); - std::int64_t elementSize = - kind->kind.shape == core::PointerShape::Counted && - !pointee->isVoidType() && !pointee->isIncompleteType() - ? static_cast( - context.getTypeSizeInChars(pointee).getQuantity()) - : 1; - core::Term bytes = - core::Term::ofSym(count, kind->kind.extent.scale * elementSize, - kind->kind.extent.offset * elementSize); - core::Extent extent{ - .bytes = bytes, - .cls = kind->extentClass.value_or(core::ExtentClass::Declared)}; - // RFC 0030 §7.6: an inferred invariant holds as C computes `f * - // sizeof *d`, which may wrap: the bytes are that product's value, - // never more than the term (RFC 0031 *Implementation amendments*). - if (kind->kind.source == core::KindSource::Inferred && - bytes.scale > 1) { - auto top = state.zone.upper(count); - __int128 most = - top ? (static_cast<__int128>(*top) * bytes.scale) + bytes.constant - : static_cast<__int128>(INT64_MAX) + 1; - if (!top || most > INT64_MAX) { - core::SymInfo product{.type = core::SymInfo::Type::Int, - .ctype = typeHandle(context.getSizeType())}; - product.unwrapped = bytes; - product.defined = - core::SymDefinition{.op = core::IntegerOp::Multiply, - .left = count, - .constant = bytes.scale}; - core::Sym computed = heap.fresh(state, product); - state.zone.addRange(computed, 0, std::nullopt); - extent.bytes = core::Term::ofSym(computed); - extent.unwrapped = bytes; - } - } - heap.object(state, child).extent = extent; - } - } - value.type = core::SymInfo::Type::Pointer; - value.targets = { - core::Target{.object = child, .offset = core::Term::of(0)}}; - value.null = kind != nullptr && kind->declaresNonnull() - ? core::PointerNull::NonNull - : core::PointerNull::Maybe; - value.name = objects.info(child).name; - value.ctype = typeHandle(type); - return heap.fresh(state, value); - } - if (type->isIntegralOrEnumerationType()) { - value.type = core::SymInfo::Type::Int; - core::IntegerType integer{ - .width = static_cast(context.getTypeSize(type)), - .isSigned = type->isSignedIntegerOrEnumerationType(), - .isBoolean = type->isBooleanType()}; - value.intType = integer; - value.ctype = typeHandle(type); - core::Sym sym = heap.fresh(state, value); - if (integer.width <= 63 || !integer.isSigned) { - if (integer.isSigned) { - std::int64_t hi = - static_cast(std::uint64_t{1} << (integer.width - 1)) - - 1; - state.zone.addRange(sym, -hi - 1, hi); - } else if (integer.width < 63) { - state.zone.addRange( - sym, 0, - static_cast(std::uint64_t{1} << integer.width) - 1); - } else { - state.zone.addRange(sym, 0, std::nullopt); - } - } - return sym; - } - value.type = core::SymInfo::Type::Unknown; - value.ctype = typeHandle(type); - return heap.fresh(state, value); -} - -bool FunctionRun::typesMayAlias(core::Handle first, core::Handle second) const { - if (first == 0 || second == 0 || first == second) - return true; - if (!unit.input.options.strictAliasing) - return true; - QualType a = typeOfHandle(first); - QualType b = typeOfHandle(second); - if (a.isNull() || b.isNull()) - return true; - if (a->isCharType() || b->isCharType() || a->isVoidType() || b->isVoidType()) - return true; - if (a->isIncompleteType() || b->isIncompleteType()) - return true; - if (context.typesAreCompatible(a.getUnqualifiedType(), - b.getUnqualifiedType())) - return true; - // An object may contain the other as a member (a struct and its first - // field): the aggregate may alias its members. - auto contains = [&](QualType outer, QualType inner) { - const RecordDecl *record = outer->getAsRecordDecl(); - if (record == nullptr || !record->isCompleteDefinition()) - return false; - return llvm::any_of(record->fields(), [&](const FieldDecl *field) { - QualType type = field->getType().getCanonicalType(); - if (context.typesAreCompatible(type.getUnqualifiedType(), - inner.getUnqualifiedType())) - return true; - if (const auto *array = context.getAsArrayType(type); - array != nullptr && context.typesAreCompatible( - array->getElementType().getUnqualifiedType(), - inner.getUnqualifiedType())) - return true; - // (Nested aggregates: conservatively yes.) - return type->isRecordType() && type != outer.getCanonicalType(); - }); - }; - return contains(a, b) || contains(b, a); -} - -/// The function an object of `type` initialised by `init` holds at byte -/// `offset`, when its initializer names one there. -static const FunctionDecl *initializedFunction(const ASTContext &context, - QualType type, const Expr *init, - std::int64_t offset) { - for (int depth = 0; depth < 16 && init != nullptr && offset >= 0; ++depth) { - init = init->IgnoreParens(); - type = type.getCanonicalType(); - const auto *list = dyn_cast(init); - if (list != nullptr && list->isSyntacticForm() && - list->getSemanticForm() != nullptr) - list = list->getSemanticForm(); - if (const auto *array = context.getAsConstantArrayType(type)) { - if (list == nullptr) - return nullptr; - QualType element = array->getElementType(); - if (element->isIncompleteType()) - return nullptr; - std::int64_t size = context.getTypeSizeInChars(element).getQuantity(); - if (size <= 0) - return nullptr; - auto index = static_cast(offset / size); - if (index >= list->getNumInits()) - return nullptr; - init = list->getInit(index); - type = element; - offset %= size; - continue; - } - if (const RecordDecl *record = type->getAsRecordDecl()) { - if (list == nullptr || record->isUnion() || - !record->isCompleteDefinition()) - return nullptr; - const ASTRecordLayout &layout = context.getASTRecordLayout(record); - const Expr *next = nullptr; - for (const FieldDecl *field : record->fields()) { - if (field->isBitField() || - field->getFieldIndex() >= list->getNumInits()) - continue; - auto at = static_cast( - layout.getFieldOffset(field->getFieldIndex()) / - context.getCharWidth()); - std::int64_t size = - field->getType()->isIncompleteType() - ? 0 - : context.getTypeSizeInChars(field->getType()).getQuantity(); - if (offset >= at && offset < at + size) { - next = list->getInit(field->getFieldIndex()); - type = field->getType(); - offset -= at; - break; - } - } - init = next; - continue; - } - if (offset != 0 || list != nullptr) - return nullptr; - const Expr *designator = init->IgnoreParenCasts(); - if (const auto *address = dyn_cast(designator); - address != nullptr && address->getOpcode() == UO_AddrOf) - designator = address->getSubExpr()->IgnoreParenCasts(); - if (const auto *ref = dyn_cast(designator)) - return dyn_cast(ref->getDecl()); - return nullptr; - } - return nullptr; -} - -core::Sym FunctionRun::unwritten(core::HeapState &state, core::ObjectId object, - core::CellKey key, - const core::SymInfo &hint) const { - ++materialisations; - // Bytes rewritten on some paths only: the value the cell would read - // otherwise, or an unknown one. - if (const core::ObjectState &weak = heap.ensure(state, object); - !weak.forgets(key) && weak.mayForget(key)) { - std::vector> ranges = - weak.mayForgotten; - state.objects.at(object).mayForgotten.clear(); - core::Sym plain = unwritten(state, object, key, hint); - state.objects.at(object).mayForgotten = std::move(ranges); - const core::SymInfo &known = heap.info(state, plain); - core::SymInfo unknown; - unknown.type = known.type; - unknown.ctype = known.ctype; - if (unknown.type == core::SymInfo::Type::Pointer) { - core::ObjectId any = unknownObject(); - heap.ensure(state, any); - unknown.targets = {core::Target{.object = any}}; - } - core::Sym merged = heap.mergeWeak(state, plain, heap.fresh(state, unknown)); - if (!key.isSummary()) - state.objects.at(object).cells.set(key, merged); - return merged; - } - const core::ObjectState &target = heap.ensure(state, object); - const core::ObjectInfo &info = objects.info(object); - QualType objectType = info.type != 0 ? typeOfHandle(info.type) : QualType(); - std::optional at; - if (!objectType.isNull()) - at = fieldAt(context, objectType, - key.isSelected() ? key.position().offset : key.offset); - // (An element position is kept modulo its stride, so it may name another - // member that shares it, `n` for `kids[j]` in `{int n; T *kids[4];}`: the - // lvalue's type is the element's then.) - if (at && !key.isConcrete() && hint.ctype != 0 && - !ASTContext::hasSameUnqualifiedType(at->type, typeOfHandle(hint.ctype))) - at.reset(); - QualType cellType = at ? at->type : QualType(); - if (cellType.isNull() && hint.ctype != 0) - cellType = typeOfHandle(hint.ctype); - const FieldDecl *field = at ? at->field : nullptr; - core::Sym value = core::ZeroSym; - // A summary cell holds only what stores through unknown indices wrote - // (§4.2 *Amendment (arrays)*): the value of an element no store reached - // is returned, not stored. - auto setCell = [&](core::Sym sym) { - if (!key.isSummary()) - state.objects.at(object).cells.set(key, sym); - return sym; - }; - // Memory an unknown callee reached holds whatever it left there - // (RFC 0030 §5.1), whatever the object's kind. - if (target.forgets(key) && info.key.kind != core::ObjectKind::Focus) { - core::SymInfo unknown; - unknown.type = hint.type; - if (unknown.type == core::SymInfo::Type::Unknown) - unknown.type = !cellType.isNull() && cellType->isPointerType() - ? core::SymInfo::Type::Pointer - : core::SymInfo::Type::Int; - unknown.ctype = !cellType.isNull() ? typeHandle(cellType) : hint.ctype; - if (unknown.type == core::SymInfo::Type::Pointer) { - core::ObjectId any = unknownObject(); - heap.ensure(state, any); - unknown.targets = {core::Target{.object = any}}; - } - return setCell(heap.fresh(state, unknown)); - } - switch (info.key.kind) { - case core::ObjectKind::Literal: { - // A string literal's bytes (RFC 0012). - const auto *expr = fromHandle(info.key.handle); - const auto *literal = dyn_cast_or_null(expr); - if (const auto *predefined = dyn_cast_or_null(expr)) - literal = predefined->getFunctionName(); - if (literal != nullptr && literal->getCharByteWidth() == 1 && - !key.isSummary() && key.offset >= 0 && - std::cmp_less_equal(key.offset, literal->getByteLength()) && - !cellType.isNull() && cellType->isIntegerType() && - context.getTypeSizeInChars(cellType).getQuantity() == 1) { - llvm::StringRef bytes = literal->getBytes(); - char byte = std::cmp_less(key.offset, bytes.size()) - ? bytes[static_cast(key.offset)] - : '\0'; - std::int64_t number = cellType->isUnsignedIntegerType() - ? static_cast(byte) - : static_cast(byte); - core::SymInfo constant; - constant.type = core::SymInfo::Type::Int; - constant.intType = core::IntegerType{ - .width = 8, .isSigned = !cellType->isUnsignedIntegerType()}; - constant.ctype = typeHandle(cellType); - core::Sym sym = heap.fresh(state, constant); - state.zone.addRange(sym, number, number); - return setCell(sym); - } - break; - } - case core::ObjectKind::Global: { - // A function table the unit never writes holds its initializer's - // targets, cell by cell. - const auto *var = fromHandle(info.key.handle); - const VarDecl *initialized = nullptr; - const Expr *init = - var != nullptr ? var->getAnyInitializer(initialized) : nullptr; - if (init != nullptr && key.isConcrete() && !cellType.isNull() && - cellType->isFunctionPointerType() && unit.keepsInitializer(*var)) - if (const FunctionDecl *fn = - initializedFunction(context, var->getType(), init, key.offset)) { - core::SymInfo known; - known.type = core::SymInfo::Type::Function; - known.functionsKnown = true; - known.functions = {handleOf(fn->getCanonicalDecl())}; - known.name = fn->getNameAsString(); - // Its entry value (§6.2: no store). - known.entryOf = std::make_pair(object, key); - return setCell(heap.fresh(state, known)); - } - break; - } - case core::ObjectKind::Focus: { - // Every candidate's value at the cell. - std::vector focused = target.candidates; - for (core::ObjectId candidate : focused) { - if (!state.objects.contains(candidate)) - continue; - core::Sym sym = heap.load(state, candidate, key, hint); - value = value == core::ZeroSym ? sym : heap.mergeWeak(state, value, sym); - } - if (value == core::ZeroSym) - value = cellType.isNull() - ? heap.fresh(state, core::SymInfo{.type = hint.type}) - : entryValue(state, cellType, object, key, field); - return setCell(value); - } - case core::ObjectKind::Local: - case core::ObjectKind::HeapRecent: - case core::ObjectKind::HeapOld: - if (info.key.kind == core::ObjectKind::Local && - entryPathOf(objects, object, function, unit)) { - // A parameter passed by value: its members are entry values. - if (!cellType.isNull()) - return setCell(entryValue(state, cellType, object, key, field)); - } - if (target.zeroed || info.key.kind == core::ObjectKind::Local || - target.uninitialised) { - core::SymInfo zero; - zero.type = hint.type; - if (zero.type == core::SymInfo::Type::Unknown) - zero.type = !cellType.isNull() && cellType->isPointerType() - ? core::SymInfo::Type::Pointer - : core::SymInfo::Type::Int; - zero.ctype = !cellType.isNull() ? typeHandle(cellType) : hint.ctype; - bool uninit = !target.zeroed; - // RFC 0030 §5.4: in a function that calls `setjmp`, a `longjmp` may - // return to it after a store this path does not see; its local reads - // as possibly unassigned, not certainly. - bool jumped = - uninit && info.key.kind == core::ObjectKind::Local && callsSetjmp(); - // RFC 0033 §1: a local whose address is taken may be written where - // no summary records it (a callee handed `&op_info`); that it was - // never written is then no fact the program states, and it reads as - // possibly unassigned. - if (uninit && info.key.kind == core::ObjectKind::Local) - if (const clang::VarDecl *var = localVariable(object); - var != nullptr && isPassedToCall(*var)) - jumped = true; - if (zero.type == core::SymInfo::Type::Pointer) { - zero.null = core::PointerNull::Null; - zero.uninit = uninit && !jumped; - // RFC 0030 §11: null only because zero-initialisation lowered the - // allocation; C leaves it garbage, so its use is checked, not a - // definite null dereference. - zero.mayUninit = (target.zeroed && target.uninitialised && - info.key.kind != core::ObjectKind::Local) || - jumped; - } else { - zero.uninit = uninit && !jumped; - zero.mayUninit = jumped; - } - core::Sym sym = heap.fresh(state, zero); - if (zero.type == core::SymInfo::Type::Int && !uninit) - state.zone.addRange(sym, 0, 0); - return setCell(sym); - } - [[fallthrough]]; - default: - break; - } - // The cell's entry value, marked as such (§6.2: an unchanged cell is no - // store). - auto entryCell = [&](core::Sym sym) { - if (key.isConcrete()) { - core::SymInfo &entry = heap.infoMut(state, sym); - entry.entryOf = std::make_pair(object, key); - entry.entryOrigins = {*entry.entryOf}; - } - return setCell(sym); - }; - if (!cellType.isNull() && - (cellType->isPointerType() || cellType->isIntegralOrEnumerationType())) - return entryCell(entryValue(state, cellType, object, key, field)); - core::SymInfo unknown; - unknown.type = hint.type; - unknown.ctype = hint.ctype; - if (hint.type == core::SymInfo::Type::Pointer) { - QualType pointee; - if (hint.ctype != 0) { - QualType hinted = typeOfHandle(hint.ctype); - if (!hinted.isNull() && hinted->isPointerType()) - pointee = hinted->getPointeeType(); - } - core::ObjectId child = childEntryObject(state, object, key, pointee, field); - heap.ensure(state, child); - unknown.targets = {core::Target{.object = child}}; - } - return entryCell(heap.fresh(state, unknown)); -} - -std::vector -FunctionRun::roots(const core::HeapState &state) const { - std::vector found; - for (const auto &[id, object] : state.objects) { - core::ObjectKind kind = objects.info(id).key.kind; - if ((kind == core::ObjectKind::Local || kind == core::ObjectKind::Global) && - object.life != core::Life::Ended) - found.push_back(id); - } - return found; -} - -void FunctionRun::dropDeadLocals(core::HeapState &state, unsigned block) const { - if (block >= liveInBlock.size() || state.unreachable) - return; - const llvm::BitVector &live = liveInBlock[block]; - std::vector dead; - for (const auto &[id, object] : state.objects) { - const core::ObjectInfo &info = objects.info(id); - if (info.key.kind != core::ObjectKind::Local || info.key.dead) - continue; - const VarDecl *var = variableOf(info); - // (A parameter's holder and a fixed local are read at the exits.) - if (var == nullptr || isa(var) || - std::ranges::find(fixedLocals, var) != fixedLocals.end()) - continue; - auto index = localIndex.find(var->getCanonicalDecl()); - if (index == localIndex.end() || index->second >= live.size() || - live.test(index->second) || addressTaken.test(index->second)) - continue; - // (Within its scope a check may still name it, as a witness.) - auto scope = localScope.find(var->getCanonicalDecl()); - const SourceLocation at = - block < blockLocation.size() ? blockLocation[block] : SourceLocation(); - if (scope == localScope.end() || at.isInvalid()) - continue; - const SourceManager &sm = context.getSourceManager(); - bool inside = !sm.isBeforeInTranslationUnit(at, scope->second.getBegin()) && - !sm.isBeforeInTranslationUnit(scope->second.getEnd(), at); - if (inside) - continue; - dead.push_back(id); - } - for (core::ObjectId id : dead) - state.objects.erase(id); -} - -void FunctionRun::dropDeadValues(core::HeapState &state, unsigned block) const { - if (block >= carriedLiveIn.size() || state.unreachable || state.exprs.empty()) - return; - const llvm::SparseBitVector<> &live = carriedLiveIn[block]; - state.exprs.eraseIf([&](core::Handle handle, core::Sym) { - auto index = carriedIndex.find(handle); - return index != carriedIndex.end() && !live.test(index->second); - }); -} - -bool FunctionRun::callsSetjmp() const { - const SiteIndex::FunctionSites *own = sites().function(function); - return own != nullptr && own->callsSetjmp; -} - -const SiteIndex &FunctionRun::sites() const { - return unit.authoritative.siteIndex(); -} - -bool FunctionRun::applies(core::SiteId id, core::Facet facet) const { - return unit.authoritative.applies(id, facet); -} - -//===----------------------------------------------------------------------===// -// Leaks (RFC 0007, RFC 0031 §5.8) -//===----------------------------------------------------------------------===// - -bool FunctionRun::liveAfter(const Stmt &at, const VarDecl &var) const { - auto index = localIndex.find(var.getCanonicalDecl()); - if (index == localIndex.end()) - return true; - if (addressTaken.test(index->second)) - return true; - auto live = liveAfterStmt.find(&at); - if (live == liveAfterStmt.end()) - return true; - return live->second.test(index->second); -} - -bool FunctionRun::liveBefore(const Stmt &at, const VarDecl &var) const { - auto index = localIndex.find(var.getCanonicalDecl()); - if (index == localIndex.end()) - return true; - if (addressTaken.test(index->second)) - return true; - auto live = liveBeforeStmt.find(&at); - if (live == liveBeforeStmt.end()) - return true; - return live->second.test(index->second); -} - -void FunctionRun::checkLeaks(core::HeapState &state, const Stmt &at, - LeakPoint point, const std::string &released) { - if (!publishing || state.unreachable) - return; - // RFC 0030 §3.4: not at a return from `main`. - bool atExit = point == LeakPoint::Exit || point == LeakPoint::ExitEdge; - // `main`'s frame lasts until the program exits (§3.4). - if ((atExit || point == LeakPoint::Scope) && function.isMain()) - return; - // Nor on a path that ends the program. - if (currentBlockNoReturn) - return; - std::vector rootIds; - for (const auto &[id, object] : state.objects) { - const core::ObjectInfo &info = objects.info(id); - if (object.life == core::Life::Ended) - continue; - if (info.key.kind == core::ObjectKind::Global) { - rootIds.push_back(id); - continue; - } - if (info.key.kind != core::ObjectKind::Local) - continue; - const VarDecl *var = variableOf(info); - if (var == nullptr) { - // A compound literal lives to the end of its block; a call's record - // result is a temporary of its statement, dead at an exit. - if ((!atExit && point != LeakPoint::Scope) || - !(info.key.expression && - isa_and_nonnull(fromHandle(info.key.handle)))) - rootIds.push_back(id); - continue; - } - if (atExit) - continue; - if (point == LeakPoint::Scope) { - rootIds.push_back(id); - continue; - } - // `main`'s frame lasts until the program exits (§3.4). - bool live = function.isMain() || - (point == LeakPoint::Statement ? liveBefore(at, *var) - : liveAfter(at, *var)); - if (live) - rootIds.push_back(id); - } - // Entry objects are the caller's: what they hold is escaped. - for (const auto &[id, object] : state.objects) { - core::ObjectKind kind = objects.info(id).key.kind; - if ((kind == core::ObjectKind::Entry && !object.owned) || - kind == core::ObjectKind::EntrySummary || - kind == core::ObjectKind::CallResult) - rootIds.push_back(id); - } - std::vector reachable = heap.reachableObjects(state, rootIds); - std::set reached(reachable.begin(), reachable.end()); - std::vector leaked; - for (const auto &[id, object] : state.objects) { - core::ObjectKind kind = objects.info(id).key.kind; - bool candidate = kind == core::ObjectKind::HeapRecent || - kind == core::ObjectKind::HeapOld || - (kind == core::ObjectKind::Entry && object.owned); - // (`alloca` storage is the frame's, RFC 0030 §8.2: it ends, never leaks.) - if (!candidate || !object.owned || object.escaped || - object.life != core::Life::Live || reached.contains(id) || - object.family == core::StackFamily) - continue; - // Not made on this path (the test that selected its allocation failed). - if (object.existsIf && state.syms.contains(object.existsIf->first)) - if (auto zero = isZeroValue(heap, state, object.existsIf->first); - zero && *zero != object.existsIf->second) - continue; - leaked.push_back(id); - } - for (core::ObjectId id : leaked) { - core::ObjectState &object = state.objects.at(id); - object.owned = false; - reportedLeaks.insert(id); - // Reported at the last use of the value when there is one, else where - // it is found dead. - // A value found dead where a statement starts, or at a `return`, is - // reported there; one lost at the end of a block or of the body at its - // last use. - const Stmt *where = &at; - bool atStatement = point == LeakPoint::Statement || - point == LeakPoint::Release || - point == LeakPoint::ExitEdge || - (point == LeakPoint::Exit && isa(at)); - if (!atStatement && object.lastUse != 0 && point != LeakPoint::Scope) - where = fromHandle(object.lastUse); - // RFC 0007: once per path on which the resource is lost. The blocks of - // the final pass start from the fixpoint's states, which never saw a - // report: a loss in a block an earlier report's block reaches is the - // same loss, found again. - bool again = false; - for (const auto &[other, block] : leakBlocks) - again = again || (other == id && blockReaches(block, currentBlock)); - if (again) - continue; - leakBlocks.emplace_back(id, currentBlock); - std::string name = object.holder; - const core::ObjectInfo &info = objects.info(id); - std::string message; - if (name.empty()) { - // RFC 0007: `result of '' is leaked` at a discarded allocating call. - std::string callee = info.name; - if (const auto *call = - dyn_cast_or_null(fromHandle(info.key.handle)); - call != nullptr && (info.key.kind == core::ObjectKind::HeapRecent || - info.key.kind == core::ObjectKind::HeapOld)) { - if (const FunctionDecl *direct = call->getDirectCallee()) - callee = direct->getNameAsString(); - } - message = "result of '" + callee + "' is leaked"; - } else if (point == LeakPoint::Release && !released.empty() && - name != released) { - message = ("'" + llvm::Twine(name) + "' is leaked when '" + released + - "' is freed") - .str(); - } else { - message = "'" + name + "' is leaked"; - } - core::Diagnostic diagnostic; - diagnostic.id = core::diag::Leak; - diagnostic.severity = core::Severity::Warning; - diagnostic.message = std::move(message); - // RFC 0007 notes: where the resource came from (a discarded result has - // no holder to point at). - if (auto owner = declaredOwner.find(id); owner != declaredOwner.end()) { - if (!name.empty() && owner->second.second.isValid()) - diagnostic.addNote("'" + owner->second.first + - "' is declared WEAVEC_OWNED here", - owner->second.second); - } else if (!name.empty() && info.created.isValid()) { - if (info.key.kind == core::ObjectKind::Entry) - diagnostic.addNote("'" + info.name + "' is declared WEAVEC_OWNED here", - info.created); - else - diagnostic.addNote("allocated here", info.created); - } - diagnostic.location = toCoreLocation( - context.getSourceManager(), - point == LeakPoint::Scope ? where->getEndLoc() : where->getBeginLoc()); - report(std::move(diagnostic), core::Certainty::Possible, where, - std::nullopt); - } -} - -bool FunctionRun::blockReaches(unsigned from, unsigned to) const { - if (from == to) - return true; - if (!cfg) - return false; - std::vector byId(cfg->getNumBlockIDs(), nullptr); - for (const CFGBlock *block : *cfg) - byId[block->getBlockID()] = block; - std::vector seen(byId.size(), false); - std::vector pending{from}; - while (!pending.empty()) { - unsigned id = pending.back(); - pending.pop_back(); - if (id >= byId.size() || seen[id] || byId[id] == nullptr) - continue; - seen[id] = true; - for (const CFGBlock::AdjacentBlock &succ : byId[id]->succs()) - if (const CFGBlock *next = succ.getReachableBlock()) { - if (next->getBlockID() == to) - return true; - pending.push_back(next->getBlockID()); - } - } - return false; -} - -//===----------------------------------------------------------------------===// -// Reporting -//===----------------------------------------------------------------------===// - -void FunctionRun::report(core::Diagnostic diagnostic, core::Certainty certainty, - const Stmt *site, std::optional facet) { - if (!publishing) - return; - // RFC 0034 §6.1: in the replay, a report of a candidate at its location - // witnesses it on this path when definite, and counters it when not. - if (witnessing) { - for (Candidate &candidate : candidates) - if (candidate.id == diagnostic.id && - candidate.where == diagnostic.location) { - if (certainty == core::Certainty::Definite && - diagnostic.severity == core::Severity::Error) - candidate.reportedHere = true; - else - candidate.otherwiseHere = true; - } - return; - } - auto key = - std::make_pair(site, std::string(diagnostic.id) + diagnostic.message); - if (!reported.insert(key).second) - return; - if (mode == RunMode::Authoritative && site != nullptr && facet && - certainty == core::Certainty::Definite && - diagnostic.severity == core::Severity::Error) - candidates.push_back(Candidate{.site = site, - .facet = *facet, - .id = std::string(diagnostic.id), - .where = diagnostic.location}); - out.report(std::move(diagnostic), certainty, site, facet); -} - -/// RFC 0034 §6.1: the replay's bounds. -static constexpr unsigned ReplayPaths = 256; -static constexpr unsigned ReplayTransfers = 20000; -static constexpr unsigned ReplayLoopTurns = 2; - -void FunctionRun::confirmCandidates() { - if (candidates.empty() || mode != RunMode::Authoritative || !cfg) - return; - LedgerAdapter witness(context, sites(), {}, LedgerAdapter::Mode::Witness); - // A decision of a candidate's facet at its site that is no violation: - // this path reaches the site and does not fail it. - witness.observer = [this](const Stmt &site, core::Facet facet, - const core::FacetDecision &decision) { - if (decision.outcome == core::SiteOutcome::Violation) - return; - for (Candidate &candidate : candidates) - if (candidate.site == &site && candidate.facet == facet) - candidate.otherwiseHere = true; - }; - publishTo = &witness; - witnessing = true; - publishing = true; - struct Frame { - unsigned block = 0; - core::HeapState state; - std::vector turns; - }; - std::vector stack; - const unsigned entry = cfg->getEntry().getBlockID(); - bool complete = entryStates[entry].has_value(); - if (complete) - stack.push_back( - Frame{.block = entry, - .state = *entryStates[entry], - .turns = std::vector(cfg->getNumBlockIDs(), 0)}); - unsigned paths = 0; - unsigned transfersLeft = ReplayTransfers; - std::vector byId(cfg->getNumBlockIDs(), nullptr); - for (const CFGBlock *block : *cfg) - byId[block->getBlockID()] = block; - while (!stack.empty()) { - Frame frame = std::move(stack.back()); - stack.pop_back(); - const CFGBlock *block = byId[frame.block]; - if (block == nullptr) - continue; - // A loop taken more than twice on one path leaves it from its head's - // fixpoint state, by its exits only; anything else repeated stops. - bool leaveLoop = false; - if (frame.turns[frame.block]++ >= ReplayLoopTurns) { - if (!loopHead[frame.block] || !entryStates[frame.block]) - continue; - frame.state = *entryStates[frame.block]; - leaveLoop = true; - } - if (transfersLeft-- == 0) { - complete = false; - break; - } - // (Each path reports for itself: what makes a finding once per run.) - requirementViolated.clear(); - reportedUninit.clear(); - std::vector> outs; - transferBlock(*block, frame.state, outs); - // A definite report in this block witnesses its candidate whatever the - // site's own decision says (a trusted facet still reports, and the - // authoritative pass links the error to it); anything else counters. - for (Candidate &candidate : candidates) { - if (candidate.reportedHere) - candidate.witnessed = true; - else if (candidate.otherwiseHere) - candidate.countered = true; - candidate.reportedHere = candidate.otherwiseHere = false; - } - if (outs.empty()) { - if (++paths > ReplayPaths) { - complete = false; - break; - } - continue; - } - for (auto &[succ, state] : llvm::reverse(outs)) { - if (leaveLoop) { - const auto body = loopBody.find(frame.block); - if (body != loopBody.end() && body->second.test(succ)) - continue; - } - dropDeadLocals(state, succ); - dropDeadValues(state, succ); - stack.push_back(Frame{ - .block = succ, .state = std::move(state), .turns = frame.turns}); - } - } - witnessing = false; - publishing = false; - publishTo = &out; - for (const Candidate &candidate : candidates) - if (!complete || !candidate.witnessed || candidate.countered) - out.unconfirm(*candidate.site, candidate.facet, candidate.id); -} - -//===----------------------------------------------------------------------===// -// The CFG and the fixpoint -//===----------------------------------------------------------------------===// - -void FunctionRun::buildCfg() { - CFG::BuildOptions options; - options.setAllAlwaysAdd(); - options.AddLifetime = true; - options.AddImplicitDtors = false; - options.AddScopes = false; - options.PruneTriviallyFalseEdges = true; - cfg = CFG::buildCFG(&function, function.getBody(), &context, options); - // RFC 0034 §7.1: no pre-pass (they are quadratic) over a graph too large. - if (!cfg || cfg->getNumBlockIDs() > MaxBlocks) - return; - entryStates.assign(cfg->getNumBlockIDs(), std::nullopt); - visits.assign(cfg->getNumBlockIDs(), 0); - joinsAt.assign(cfg->getNumBlockIDs(), 0); - loopHead.assign(cfg->getNumBlockIDs(), false); - // The arms of conditional operators. - struct ArmCollector : public RecursiveASTVisitor { - llvm::DenseMap &arms; - explicit ArmCollector(llvm::DenseMap &out) - : arms(out) {} - // NOLINTBEGIN(readability-identifier-naming,bugprone-derived-method-shadowing-base-method): - // RecursiveASTVisitor's CRTP hooks are found by name. - bool VisitAbstractConditionalOperator(AbstractConditionalOperator *op) { - // (The CFG evaluates an arm's parentheses as the expression inside.) - arms[op->getTrueExpr()->IgnoreParens()] = op; - arms[op->getFalseExpr()->IgnoreParens()] = op; - return true; - } - // NOLINTEND(readability-identifier-naming,bugprone-derived-method-shadowing-base-method) - }; - ArmCollector(arms).TraverseStmt(function.getBody()); - // RFC 0017: dimensions of a declaration with an initializer are taken - // before the initializer runs, as CodeGen does. - for (const CFGBlock *block : *cfg) { - for (const CFGElement &element : *block) { - auto stmt = element.getAs(); - const auto *decl = stmt ? dyn_cast(stmt->getStmt()) : nullptr; - if (decl == nullptr) - continue; - for (const Decl *each : decl->decls()) { - const auto *var = dyn_cast(each); - if (var == nullptr || var->getInit() == nullptr || - var->hasGlobalStorage() || - !var->getType()->isVariablyModifiedType()) - continue; - // The initializer's nodes; the first of them the block evaluates. - struct Nodes : RecursiveASTVisitor { - llvm::DenseSet all; - // NOLINTBEGIN(readability-identifier-naming,bugprone-derived-method-shadowing-base-method): - // RecursiveASTVisitor's CRTP hooks are found by name. - bool VisitStmt(Stmt *s) { - all.insert(s); - return true; - } - // NOLINTEND(readability-identifier-naming,bugprone-derived-method-shadowing-base-method) - } nodes; - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast): visitor API - nodes.TraverseStmt(const_cast(var->getInit())); - for (const CFGElement &candidate : *block) - if (auto first = candidate.getAs(); - first && nodes.all.contains(first->getStmt())) { - vlaCaptureBefore[first->getStmt()].push_back(var); - vlaCapturedEarly.insert(var); - break; - } - } - } - } - // Which block evaluates each expression, and which values a later block - // reads. - for (const CFGBlock *block : *cfg) - for (const CFGElement &element : *block) - if (auto stmt = element.getAs()) - evaluatedIn[stmt->getStmt()] = block->getBlockID(); - for (const CFGBlock *block : *cfg) { - auto noteChildren = [&](const Stmt *parent) { - // (A conditional's value is the one its arm recorded under it, §2: - // it reads neither its condition nor its arms.) - if (isa(parent)) - return; - for (const Stmt *child : parent->children()) { - if (child == nullptr) - continue; - auto it = evaluatedIn.find(child); - if (it != evaluatedIn.end() && it->second != block->getBlockID()) - if (const auto *expr = dyn_cast(child)) { - crossBlock.insert(expr); - // (Spent once read, except an operand of a conditional or - // logical operator, whose untaken side keeps its last value.) - const auto *logical = dyn_cast(parent); - if (!isa(parent) && - (logical == nullptr || !logical->isLogicalOp())) - consumedBy[block->getBlockID()].push_back(handleOf(expr)); - } - } - }; - for (const CFGElement &element : *block) - if (auto stmt = element.getAs()) - noteChildren(stmt->getStmt()); - // (An expression terminator, `?:` or `&&`, reads its operands; a - // statement's only its condition, below.) - if (const Stmt *terminator = block->getTerminatorStmt(); - terminator != nullptr && isa(terminator)) - noteChildren(terminator); - if (const Stmt *condition = block->getTerminatorCondition(false)) { - auto it = evaluatedIn.find(condition); - if (it != evaluatedIn.end() && it->second != block->getBlockID()) - if (const auto *conditionExpr = dyn_cast(condition)) - crossBlock.insert(conditionExpr); - } - } - // Liveness of the carried expression values (`dropDeadValues`): a block - // reads the ones in its elements' subtrees, its expression terminator's - // and its condition's (the transfer reads a carried value of any - // expression evaluated elsewhere), and evaluating one again, or an arm - // of a conditional (which records the operator's value), replaces it. - { - carriedIndex.clear(); - llvm::DenseMap index; - auto track = [&](const Expr *expr) { - if (index.try_emplace(expr, index.size()).second) - carriedIndex.try_emplace(handleOf(expr), index.size() - 1); - }; - for (const Expr *expr : crossBlock) - track(expr); - for (const auto &[arm, op] : arms) - track(op); - const unsigned blocks = cfg->getNumBlockIDs(); - // (Sparse: thousands of blocks carry thousands of short-lived values.) - std::vector> use(blocks); - std::vector> kill(blocks); - for (const CFGBlock *block : *cfg) { - llvm::SparseBitVector<> &reads = use[block->getBlockID()]; - llvm::SparseBitVector<> &writes = kill[block->getBlockID()]; - std::vector pending; - for (const CFGElement &element : *block) - if (auto stmt = element.getAs()) { - pending.push_back(stmt->getStmt()); - if (const auto *expr = dyn_cast(stmt->getStmt())) { - if (auto it = index.find(expr); it != index.end()) - writes.set(it->second); - if (auto arm = arms.find(expr); arm != arms.end()) - writes.set(index.find(arm->second)->second); - } - } - // (A conditional's branch reads only its condition.) - if (const Stmt *terminator = block->getTerminatorStmt(); - terminator != nullptr && isa(terminator) && - !isa(terminator)) - pending.push_back(terminator); - if (const Stmt *condition = block->getTerminatorCondition(false)) - pending.push_back(condition); - while (!pending.empty()) { - const Stmt *stmt = pending.back(); - pending.pop_back(); - if (const auto *expr = dyn_cast(stmt)) - if (auto it = index.find(expr); it != index.end()) - reads.set(it->second); - // (A conditional reads only the value its arm recorded; an opaque - // value, `a ?: b`'s, reads its source.) - if (isa(stmt)) - continue; - if (const auto *opaque = dyn_cast(stmt)) - if (const Expr *source = opaque->getSourceExpr()) - pending.push_back(source); - for (const Stmt *child : stmt->children()) - if (child != nullptr) - pending.push_back(child); - } - if (const auto *conditional = - dyn_cast_or_null( - block->getTerminatorStmt())) - if (auto it = index.find(conditional); it != index.end()) - writes.set(it->second); - } - carriedLiveIn.assign(blocks, llvm::SparseBitVector<>()); - std::vector postOrder(cfg->begin(), cfg->end()); - for (bool changed = true; changed;) { - changed = false; - for (const CFGBlock *block : postOrder) { - const unsigned id = block->getBlockID(); - llvm::SparseBitVector<> live; - for (const CFGBlock::AdjacentBlock &succ : block->succs()) - if (const CFGBlock *next = succ.getReachableBlock()) - live |= carriedLiveIn[next->getBlockID()]; - live.intersectWithComplement(kill[id]); - live |= use[id]; - if (live != carriedLiveIn[id]) { - carriedLiveIn[id] = std::move(live); - changed = true; - } - } - } - } - // Locals, which of them have their address taken, and statement-level - // elements (for leaks). - struct LocalCollector : public RecursiveASTVisitor { - std::vector vars; - llvm::DenseSet taken; - // NOLINTBEGIN(readability-identifier-naming,bugprone-derived-method-shadowing-base-method): - // RecursiveASTVisitor's CRTP hooks are found by name. - bool VisitVarDecl(VarDecl *var) { - if (!var->hasGlobalStorage()) - vars.push_back(var); - if (var->getType()->isArrayType() || var->getType()->isRecordType()) - taken.insert(var); - return true; - } - bool VisitUnaryOperator(UnaryOperator *op) { - if (op->getOpcode() == UO_AddrOf) - if (const auto *ref = - dyn_cast(op->getSubExpr()->IgnoreParens())) - if (const auto *var = dyn_cast(ref->getDecl())) - taken.insert(var); - return true; - } - // NOLINTEND(readability-identifier-naming,bugprone-derived-method-shadowing-base-method) - }; - LocalCollector locals; - for (const ParmVarDecl *param : function.parameters()) - locals.vars.push_back(param); - locals.TraverseStmt(function.getBody()); - for (const VarDecl *var : locals.vars) - localIndex.try_emplace(var->getCanonicalDecl(), localIndex.size()); - addressTaken.resize(localIndex.size()); - for (const VarDecl *var : locals.taken) - if (auto it = localIndex.find(var->getCanonicalDecl()); - it != localIndex.end()) - addressTaken.set(it->second); - struct ParamWrites : public RecursiveASTVisitor { - std::set written; - void note(const Expr *expr) { - if (const auto *ref = dyn_cast(expr->IgnoreParenImpCasts())) - if (const auto *var = dyn_cast(ref->getDecl())) - written.insert(var->getCanonicalDecl()); - } - // NOLINTBEGIN(readability-identifier-naming,bugprone-derived-method-shadowing-base-method): - // RecursiveASTVisitor's CRTP hooks are found by name. - bool VisitBinaryOperator(BinaryOperator *op) { - if (op->isAssignmentOp()) - note(op->getLHS()); - return true; - } - bool VisitUnaryOperator(UnaryOperator *op) { - if (op->isIncrementDecrementOp() || op->getOpcode() == UO_AddrOf) - note(op->getSubExpr()); - return true; - } - // NOLINTEND(readability-identifier-naming,bugprone-derived-method-shadowing-base-method) - }; - ParamWrites writes; - writes.TraverseStmt(function.getBody()); - for (unsigned i = 0; i < function.getNumParams(); ++i) - if (!writes.written.contains(function.getParamDecl(i)->getCanonicalDecl())) - unmodifiedParams.insert(i); - if (const auto *body = dyn_cast_or_null(function.getBody())) - for (const Stmt *stmt : body->body()) - if (const auto *decl = dyn_cast(stmt)) - for (const Decl *each : decl->decls()) - if (const auto *var = dyn_cast(each); - var != nullptr && var->hasLocalStorage() && var->hasInit() && - var->getType()->isPointerType() && - !writes.written.contains(var->getCanonicalDecl())) - fixedLocals.push_back(var); - ParentMap parents(function.getBody()); - // Where each block is, and each local's scope (`dropDeadLocals`). - { - const SourceManager &sm = context.getSourceManager(); - blockLocation.assign(cfg->getNumBlockIDs(), SourceLocation()); - for (const CFGBlock *block : *cfg) { - SourceLocation at; - for (const CFGElement &element : *block) - if (auto stmt = element.getAs()) { - at = stmt->getStmt()->getBeginLoc(); - break; - } - if (at.isInvalid()) - if (const Stmt *term = block->getTerminatorStmt()) - at = term->getBeginLoc(); - if (at.isValid()) - blockLocation[block->getBlockID()] = sm.getExpansionLoc(at); - } - class Declarations : public RecursiveASTVisitor { - public: - std::vector found; - // NOLINTBEGIN(readability-identifier-naming,bugprone-derived-method-shadowing-base-method): - // RecursiveASTVisitor's CRTP hooks are found by name. - bool VisitDeclStmt(DeclStmt *decl) { - found.push_back(decl); - return true; - } - // NOLINTEND(readability-identifier-naming,bugprone-derived-method-shadowing-base-method) - } declarations; - declarations.TraverseStmt(function.getBody()); - for (const DeclStmt *decl : declarations.found) { - const Stmt *scope = parents.getParent(decl); - while (scope != nullptr && !isa(scope)) - scope = parents.getParent(scope); - if (scope == nullptr) - continue; - for (const Decl *each : decl->decls()) - if (const auto *var = dyn_cast(each)) - localScope[var->getCanonicalDecl()] = - SourceRange(sm.getExpansionLoc(var->getLocation()), - sm.getExpansionLoc(scope->getEndLoc())); - } - } - for (const CFGBlock *block : *cfg) - for (const CFGElement &element : *block) - if (auto stmt = element.getAs()) { - const Stmt *parent = parents.getParent(stmt->getStmt()); - if (parent == nullptr || - (!isa(parent) && !isa(parent) && - !isa(parent))) - statementLevel.insert(stmt->getStmt()); - } - // Backward liveness of the locals, per element. - { - // (A sub-expression that is an element of its own is counted there.) - llvm::DenseSet elements; - for (const CFGBlock *block : *cfg) - for (const CFGElement &element : *block) - if (auto stmt = element.getAs()) - elements.insert(stmt->getStmt()); - auto width = localIndex.size(); - std::vector liveIn(cfg->getNumBlockIDs(), - llvm::BitVector(width)); - // (To the fixpoint, blocks from the exit back: the live-in sets decide - // which locals a block's state keeps, `dropDeadLocals`.) - std::vector backward(cfg->begin(), cfg->end()); - std::ranges::sort(backward, [](const CFGBlock *a, const CFGBlock *b) { - return a->getBlockID() < b->getBlockID(); - }); - bool changed = true; - while (changed) { - changed = false; - for (const CFGBlock *block : backward) { - llvm::BitVector current(width); - for (const CFGBlock::AdjacentBlock &succ : block->succs()) - if (const CFGBlock *reachable = succ.getReachableBlock()) - current |= liveIn[reachable->getBlockID()]; - for (const CFGElement &element : llvm::reverse(*block)) { - auto stmt = element.getAs(); - if (!stmt) - continue; - liveAfterStmt[stmt->getStmt()] = current; - // Every local the element refers to, however deep (an element - // may hold its operands: `ra->tt == 0`). - auto refer = [&](const Stmt *root, auto &self) -> void { - if (root == nullptr) - return; - if (const auto *ref = dyn_cast(root)) - if (const auto *var = dyn_cast(ref->getDecl())) - if (auto found = localIndex.find(var->getCanonicalDecl()); - found != localIndex.end()) - current.set(found->second); - for (const Stmt *child : root->children()) - if (child != nullptr && !elements.contains(child)) - self(child, self); - }; - // (A declaration makes fresh storage: nothing earlier reads it; - // its initializer does.) - if (const auto *decl = dyn_cast(stmt->getStmt())) { - for (const Decl *each : decl->decls()) - if (const auto *var = dyn_cast(each)) - if (auto found = localIndex.find(var->getCanonicalDecl()); - found != localIndex.end()) - current.reset(found->second); - for (const Decl *each : decl->decls()) - if (const auto *var = dyn_cast(each)) - refer(var->getInit(), refer); - } else { - refer(stmt->getStmt(), refer); - } - liveBeforeStmt[stmt->getStmt()] = current; - } - if (current != liveIn[block->getBlockID()]) { - liveIn[block->getBlockID()] = current; - changed = true; - } - } - } - liveInBlock = std::move(liveIn); - } - // Loop heads: targets of back edges in a depth-first order. - std::vector color(cfg->getNumBlockIDs(), 0); - std::vector> stack; - const CFGBlock &entry = cfg->getEntry(); - stack.emplace_back(&entry, entry.succ_begin()); - color[entry.getBlockID()] = 1; - while (!stack.empty()) { - auto &[block, it] = stack.back(); - if (it == block->succ_end()) { - color[block->getBlockID()] = 2; - stack.pop_back(); - continue; - } - const CFGBlock *succ = it->getReachableBlock(); - ++it; - if (succ == nullptr) - continue; - if (color[succ->getBlockID()] == 1) { - loopHead[succ->getBlockID()] = true; - backEdges.emplace(block->getBlockID(), succ->getBlockID()); - } else if (color[succ->getBlockID()] == 0) { - color[succ->getBlockID()] = 1; - stack.emplace_back(succ, succ->succ_begin()); - } - } - std::vector byId(cfg->getNumBlockIDs(), nullptr); - for (const CFGBlock *block : *cfg) - byId[block->getBlockID()] = block; - for (auto [source, head] : backEdges) { - auto [it, inserted] = - loopBody.try_emplace(head, llvm::BitVector(cfg->getNumBlockIDs())); - llvm::BitVector &body = it->second; - body.set(head); - std::vector pending; - if (!body.test(source)) { - body.set(source); - pending.push_back(source); - } - while (!pending.empty()) { - const CFGBlock *block = byId[pending.back()]; - pending.pop_back(); - if (block == nullptr) - continue; - for (const CFGBlock::AdjacentBlock &pred : block->preds()) - if (const CFGBlock *from = pred.getReachableBlock(); - from != nullptr && !body.test(from->getBlockID())) { - body.set(from->getBlockID()); - pending.push_back(from->getBlockID()); - } - } - } -} - -void FunctionRun::initialState(core::HeapState &state) { - Transfer transfer(*this, state); - // RFC 0030 §11: a local whose declaration a jump can bypass holds garbage - // from entry: the declaration is where zero-initialisation would run. - if (function.getBody() != nullptr) - for (const VarDecl *var : bypassedDeclarations(*function.getBody())) - heap.ensure(state, variableObject(*var)).uninitialised = true; - // Integer parameters first, so a pointer's declared extent can name them. - std::vector paramValues(function.getNumParams(), core::ZeroSym); - for (unsigned i = 0; i < function.getNumParams(); ++i) { - const ParmVarDecl *param = function.getParamDecl(i); - core::ObjectId holder = variableObject(*param); - core::ObjectState &holderState = heap.ensure(state, holder); - if (auto size = transfer.sizeOf(param->getType())) - holderState.extent = core::Extent{.bytes = core::Term::of(*size), - .cls = core::ExtentClass::Exact}; - QualType type = param->getType().getCanonicalType(); - if (type->isIntegralOrEnumerationType()) { - paramValues[i] = - entryValue(state, type, holder, core::CellKey{}, nullptr); - if (aliasContext != nullptr && i < aliasContext->constants.size() && - aliasContext->constants[i]) { - const std::int64_t constant = *aliasContext->constants[i]; - auto integer = transfer.integerType(type); - // (An unsigned 64-bit value above `INT64_MAX` comes as its bits; the - // zone cannot hold it, its interval does.) - if (constant < 0 && integer && !integer->isSigned && - integer->width == 64) - heap.infoMut(state, paramValues[i]).values = - core::IntegerRange::singleton(core::IntegerValue::ofBits( - *integer, static_cast(constant))); - else - state.zone.addRange(paramValues[i], constant, constant); - } - // C11 5.1.2.2.1: `argc` is non-negative. - if (i == 0 && function.isMain()) - state.zone.addRange(paramValues[i], 0, std::nullopt); - heap.write(state, holder, core::CellKey{}, paramValues[i], false); - } - } - for (unsigned i = 0; i < function.getNumParams(); ++i) { - const ParmVarDecl *param = function.getParamDecl(i); - QualType type = param->getType().getCanonicalType(); - if (!type->isPointerType()) - continue; - core::ObjectId holder = variableObject(*param); - QualType pointee = type->getPointeeType(); - // §6.6: a parameter the caller made share another's object. - if (aliasContext != nullptr && i < aliasContext->params.size() && - aliasContext->params[i].first != i) { - auto [rep, offset] = aliasContext->params[i]; - core::ObjectId repHolder = variableObject(*function.getParamDecl(rep)); - if (auto repValue = heap.read(state, repHolder, core::CellKey{})) { - if (offset == 0) { - heap.write(state, holder, core::CellKey{}, *repValue, false); - continue; - } - core::SymInfo shared = heap.info(state, *repValue); - for (core::Target &target : shared.targets) - target.offset = target.offset.plusConstant(offset); - shared.name = param->getNameAsString(); - heap.write(state, holder, core::CellKey{}, heap.fresh(state, shared), - false); - continue; - } - } - core::SymInfo value; - if (pointee->isFunctionType()) { - value.type = core::SymInfo::Type::Function; - value.functionsKnown = false; - // §7 *Amendment (cross-unit contexts)*: the callbacks the caller - // passes, this unit's by their declarations, another's by name. - if (aliasContext != nullptr) - for (const auto &[bound, names] : aliasContext->callbacks) { - if (bound != i) - continue; - value.functionsKnown = true; - for (const std::string &name : names) { - if (const FunctionDecl *fn = unit.functionNamed(name)) - value.functions.push_back(handleOf(fn->getCanonicalDecl())); - else - value.foreignFunctions.push_back(name); - } - } - heap.write(state, holder, core::CellKey{}, heap.fresh(state, value), - false); - continue; - } - core::ObjectKey key; - key.kind = core::ObjectKind::Entry; - key.path = core::SummaryPath::param(i).deref(); - core::ObjectInfo info; - info.type = pointee->isVoidType() ? 0 : typeHandle(pointee); - info.name = param->getNameAsString(); - info.created = - toCoreLocation(context.getSourceManager(), param->getLocation()); - core::ObjectId object = objects.intern(key, info); - core::ObjectState &objectState = heap.ensure(state, object); - bool nonnull = false; - objectState.extent = paramExtent(i, paramValues, nonnull); - // §6.6 *Amendment (numeric contexts)*: the integers the caller's object - // holds are the entry values of its cells. - if (aliasContext != nullptr) - for (const auto &[owner, offset, number] : aliasContext->cells) { - if (owner != i || - heap.read(state, object, core::CellKey{.offset = offset})) - continue; - core::Sym cell = - unwritten(state, object, core::CellKey{.offset = offset}, - core::SymInfo{.type = core::SymInfo::Type::Int}); - if (heap.info(state, cell).type == core::SymInfo::Type::Int) - state.zone.addRange(cell, number, number); - } - // RFC 0002: an owned parameter is this function's to release. - for (const ParmVarDecl *redecl : {param}) - if (getAnnotations(*redecl).owned) { - heap.object(state, object).owned = true; - heap.object(state, object).holder = param->getNameAsString(); - heap.object(state, object).family = std::string(core::HeapFamily); - } - value.type = core::SymInfo::Type::Pointer; - value.targets = {core::Target{.object = object}}; - value.null = - nonnull ? core::PointerNull::NonNull : core::PointerNull::Maybe; - value.name = param->getNameAsString(); - value.ctype = typeHandle(type); - // RFC 0004: a parameter declared `WEAVEC_RAW` is a raw origin. - if (getAnnotations(*param).raw) { - value.raw = true; - value.rawAt = - toCoreLocation(context.getSourceManager(), param->getLocation()); - value.rawOrigin = core::SymInfo::RawOrigin::Declared; - } - core::Sym sym = heap.fresh(state, value); - heap.write(state, holder, core::CellKey{}, sym, false); - } -} - -/// §6.6: globals the caller made point into a parameter's object. -static void seedContextGlobals(FunctionRun &run, core::HeapState &state, - const AliasContext *alias, - const FunctionDecl &function) { - if (alias == nullptr) - return; - core::Heap &heap = run.domain(); - // Argument cells that point into one object: the second holds the - // first's value, shifted. - auto pointee = [&](unsigned param) -> std::optional { - if (param >= function.getNumParams()) - return std::nullopt; - auto held = - heap.read(state, run.variableObject(*function.getParamDecl(param)), - core::CellKey{}); - if (!held) - return std::nullopt; - const core::SymInfo &value = heap.info(state, *held); - if (value.type != core::SymInfo::Type::Pointer || value.targets.size() != 1) - return std::nullopt; - return value.targets.front().object; - }; - for (const AliasContext::CellAlias &cell : alias->cellAliases) { - auto object = pointee(cell.param); - auto repObject = pointee(cell.repParam); - if (!object || !repObject) - continue; - core::SymInfo hint; - hint.type = core::SymInfo::Type::Pointer; - core::CellKey repKey{.offset = cell.repCell}; - core::Sym repValue = core::ZeroSym; - if (auto held = heap.read(state, *repObject, repKey)) - repValue = *held; - else - repValue = run.unwritten(state, *repObject, repKey, hint); - core::SymInfo shared = heap.info(state, repValue); - for (core::Target &target : shared.targets) - target.offset = target.offset.plusConstant(cell.shift); - heap.ensure(state, *object); - heap.write(state, *object, core::CellKey{.offset = cell.cell}, - heap.fresh(state, shared), false); - } - // Globals that hold pointers into one object: the second's value is the - // first's, shifted. - for (const auto &[var, rep, offset] : alias->globalAliases) { - core::ObjectId repObject = run.variableObject(*rep); - heap.ensure(state, repObject); - core::SymInfo hint; - hint.type = core::SymInfo::Type::Pointer; - hint.ctype = typeHandle(rep->getType()); - core::Sym repValue = core::ZeroSym; - if (auto held = heap.read(state, repObject, core::CellKey{})) - repValue = *held; - else - repValue = run.unwritten(state, repObject, core::CellKey{}, hint); - core::SymInfo shared = heap.info(state, repValue); - for (core::Target &target : shared.targets) - target.offset = target.offset.plusConstant(offset); - shared.name = var->getNameAsString(); - core::ObjectId global = run.variableObject(*var); - heap.ensure(state, global); - heap.write(state, global, core::CellKey{}, heap.fresh(state, shared), - false); - } - for (const auto &[var, rep, offset] : alias->globals) { - core::ObjectId repHolder = run.variableObject(*function.getParamDecl(rep)); - auto repValue = heap.read(state, repHolder, core::CellKey{}); - if (!repValue) - continue; - core::SymInfo shared = heap.info(state, *repValue); - for (core::Target &target : shared.targets) - target.offset = target.offset.plusConstant(offset); - shared.name = var->getNameAsString(); - core::ObjectId global = run.variableObject(*var); - heap.ensure(state, global); - heap.write(state, global, core::CellKey{}, heap.fresh(state, shared), - false); - } - // §6.6 *Amendment (numeric contexts)*: the integers the object a pointer - // global points to holds. - for (const auto &[var, offset, number] : alias->globalCells) { - core::ObjectId global = run.variableObject(*var); - heap.ensure(state, global); - core::Sym pointer = core::ZeroSym; - if (auto held = heap.read(state, global, core::CellKey{})) - pointer = *held; - else - pointer = - run.unwritten(state, global, core::CellKey{}, - core::SymInfo{.type = core::SymInfo::Type::Pointer, - .ctype = typeHandle(var->getType())}); - const core::SymInfo &info = heap.info(state, pointer); - if (info.type != core::SymInfo::Type::Pointer || info.top || - info.targets.size() != 1 || !info.targets[0].offset.isConstant()) - continue; - core::ObjectId object = info.targets[0].object; - core::CellKey key{.offset = info.targets[0].offset.constant + offset}; - if (heap.read(state, object, key)) - continue; - core::Sym cell = run.unwritten( - state, object, key, core::SymInfo{.type = core::SymInfo::Type::Int}); - if (heap.info(state, cell).type == core::SymInfo::Type::Int) - state.zone.addRange(cell, number, number); - } -} - -/// The pointer a branch condition tests against null (`p`, `!p`, `p == -/// NULL`, `NULL != p`), for the note of a null finding on its null edge -/// (RFC 0008). -static const Expr *nullTested(const Expr *condition, - const ASTContext &context) { - while (condition != nullptr) { - condition = condition->IgnoreParenImpCasts(); - if (const auto *unary = dyn_cast(condition); - unary != nullptr && unary->getOpcode() == UO_LNot) { - condition = unary->getSubExpr(); - continue; - } - if (const auto *binary = dyn_cast(condition); - binary != nullptr && - (binary->getOpcode() == BO_EQ || binary->getOpcode() == BO_NE)) { - auto isNull = [&](const Expr *e) { - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast): Clang API - return e->isNullPointerConstant(const_cast(context), - Expr::NPC_ValueDependentIsNotNull) != - Expr::NPCK_NotNull; - }; - if (isNull(binary->getRHS())) - condition = binary->getLHS(); - else if (isNull(binary->getLHS())) - condition = binary->getRHS(); - else - return nullptr; - continue; - } - break; - } - return condition != nullptr && condition->getType()->isPointerType() - ? condition - : nullptr; -} - -bool FunctionRun::transferBlock( - const CFGBlock &block, core::HeapState state, - std::vector> &outs) { - currentBlock = block.getBlockID(); - currentBlockNoReturn = block.hasNoReturnElement(); - // §4.1: the operations a join kept are found again by value numbering. - for (const auto &[sym, info] : state.syms) - if (info.defined) - operations[{info.defined->op, info.defined->left, info.defined->right, - info.defined->constant, info.ctype}] = sym; - Transfer transfer(*this, state); - for (const CFGElement &element : block) { - transfer.element(element); - if (state.unreachable) - return true; - } - transfer.finishBlock(block); - if (state.unreachable) - return true; - const auto *condition = - dyn_cast_or_null(block.getTerminatorCondition(false)); - // The block of a short-circuit operand branches on that operand (`b` of - // `if (a || b)`), which is its last element, not on the whole `a || b`. - if (const auto *logical = dyn_cast_or_null( - condition ? condition->IgnoreParens() : nullptr); - logical != nullptr && logical->isLogicalOp()) - if (const Expr *last = block.getLastCondition(); - last != nullptr && last != logical) - condition = last; - core::Sym conditionSym = core::ZeroSym; - // A `switch` refines its value on each edge by the labels (RFC 0017). - const auto *switchStmt = - dyn_cast_or_null(block.getTerminatorStmt()); - if (switchStmt != nullptr) { - if (condition != nullptr && - condition->getType()->isIntegralOrEnumerationType()) - conditionSym = transfer.conditionValue(*condition); - } else if (condition != nullptr && block.succ_size() == 2) { - conditionSym = transfer.conditionValue(*condition); - } - // RFC 0008: the pointer the condition tests, whose null edge gets the - // test as the reason it may be null. - core::Sym testedSym = core::ZeroSym; - core::SourceLocation testedAt; - if (conditionSym != core::ZeroSym) - if (const Expr *tested = nullTested(condition, context)) { - testedSym = transfer.valueOf(*tested); - testedAt = - toCoreLocation(context.getSourceManager(), tested->getBeginLoc()); - } - // What dies with the block is leaked before the collection drops it. - if (publishing) { - const Stmt *last = nullptr; - for (const CFGElement &element : block) - if (auto stmt = element.getAs()) - last = stmt->getStmt(); - if (last != nullptr && !isa(last)) { - // The condition's value is still needed on the edges. - core::HeapState probe = state; - if (conditionSym != core::ZeroSym) - probe.exprs.set(handleOf(&block), conditionSym); - checkLeaks(probe, *last, LeakPoint::BlockEnd); - for (core::ObjectId id : reportedLeaks) - if (state.objects.contains(id)) - state.objects.at(id).owned = false; - } else if (last == nullptr) { - // Lifetimes that end here drop what only they held, reported at the - // end of their scope. - const Stmt *scope = function.getBody(); - for (const CFGElement &element : block) - if (auto lifetime = element.getAs()) - if (const Stmt *trigger = lifetime->getTriggerStmt()) { - scope = trigger; - break; - } - checkLeaks(state, *scope, LeakPoint::Scope); - } - } - // The condition survives the collection (it is read on the edges). - core::Handle pin = handleOf(&block); - if (conditionSym != core::ZeroSym) - state.exprs.set(pin, conditionSym); - heap.collect(state, roots(state), nullptr); - unsigned index = 0; - for (const CFGBlock::AdjacentBlock &adjacent : block.succs()) { - const CFGBlock *succ = adjacent.getReachableBlock(); - unsigned position = index++; - if (succ == nullptr) - continue; - core::HeapState copy = state; - if (conditionSym != core::ZeroSym && switchStmt != nullptr) { - if (!Transfer::refineSwitchEdge(*this, copy, conditionSym, - condition->getType(), *switchStmt, *succ, - position + 1 == block.succ_size())) - continue; - } else if (conditionSym != core::ZeroSym && - !Transfer::refine(*this, copy, conditionSym, position == 0)) { - continue; - } - if (testedSym != core::ZeroSym && copy.syms.contains(testedSym) && - state.syms.contains(testedSym)) { - core::SymInfo &tested = heap.infoMut(copy, testedSym); - if (tested.type == core::SymInfo::Type::Pointer && - tested.null == core::PointerNull::Null && - heap.info(state, testedSym).null != core::PointerNull::Null && - !tested.allocatorSource) - tested.nullOrigin = - core::NullOrigin{.reason = core::NullOrigin::Reason::Tested, - .where = testedAt, - .detail = {}}; - } - copy.exprs.erase(pin); - // A conditional's value is its arm's on this path: none an earlier - // evaluation left (an arm that records none reads as unknown). - if (const auto *conditional = dyn_cast_or_null( - block.getTerminatorStmt())) - copy.exprs.erase(handleOf(conditional)); - // The values this block read from earlier blocks are spent (their - // symbols need not stay). - if (auto spent = consumedBy.find(block.getBlockID()); - spent != consumedBy.end()) - for (core::Handle handle : spent->second) - copy.exprs.erase(handle); - outs.emplace_back(succ->getBlockID(), std::move(copy)); - } - return true; -} - -namespace { -/// The integer constants of a body: its literals, `sizeof`s and array -/// sizes (in elements and bytes). -class ConstantCollector : public RecursiveASTVisitor { -public: - explicit ConstantCollector(const ASTContext &context) : context(context) {} - std::set values; - - // NOLINTBEGIN(readability-identifier-naming,bugprone-derived-method-shadowing-base-method): - // RecursiveASTVisitor's CRTP hooks are found by name. - bool VisitIntegerLiteral(IntegerLiteral *literal) { - if (literal->getValue().getActiveBits() <= 62) - add(static_cast(literal->getValue().getZExtValue())); - return true; - } - bool VisitUnaryExprOrTypeTraitExpr(UnaryExprOrTypeTraitExpr *trait) { - Expr::EvalResult value; - if (!trait->isValueDependent() && trait->EvaluateAsInt(value, context)) - add(value.Val.getInt().getExtValue()); - return true; - } - bool VisitVarDecl(VarDecl *var) { - if (const ConstantArrayType *array = - context.getAsConstantArrayType(var->getType())) { - add(static_cast(array->getSize().getZExtValue())); - if (!var->getType()->isIncompleteType()) - add(static_cast( - context.getTypeSizeInChars(var->getType()).getQuantity())); - } - return true; - } - // NOLINTEND(readability-identifier-naming,bugprone-derived-method-shadowing-base-method) - -private: - const ASTContext &context; - // `i < n` bounds `i` by `n - 1`, and `i <= n` lets it reach `n + 1`. - void add(std::int64_t value) { - if (values.size() < 256) - for (std::int64_t near : {value - 1, value, value + 1}) { - values.insert(near); - values.insert(-near); - } - } -}; -} // namespace - -std::vector FunctionRun::wideningThresholds(unsigned head) const { - // §4.8: a bound that grew at a loop head stops at the nearest constant - // the loop tests (`i < 3` keeps `i <= 3`), else goes to the type's - // limits. - ConstantCollector collector(context); - collector.values = {-1, 0, 1}; - auto body = loopBody.find(head); - if (body == loopBody.end()) { - if (Stmt *statements = function.getBody()) - collector.TraverseStmt(statements); - } else { - for (const CFGBlock *block : *cfg) - if (body->second.test(block->getBlockID())) - if (const Stmt *condition = block->getTerminatorCondition(false)) - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast): visitor API - collector.TraverseStmt(const_cast(condition)); - } - return {collector.values.begin(), collector.values.end()}; -} - -RunResult FunctionRun::run() { - RunResult result; - if (std::getenv("WEAVEC_ENGINE_TRACE") != nullptr) - llvm::errs() << "run " << function.getNameAsString() << " mode " - << static_cast(mode) << " depth " << contextDepth << "\n"; - if (mode == RunMode::Authoritative) - validateAnnotations(); - buildCfg(); - if (!cfg || cfg->getNumBlockIDs() > MaxBlocks) { - result.effects.incomplete = cfg ? "its graph is over budget" : "no CFG"; - overBudget = result.overBudget = true; - result.spentBudget = spentBudget = cfg != nullptr; - return result; - } - // Reverse post-order for the worklist priority. - std::vector rpo; - { - std::vector seen(cfg->getNumBlockIDs(), false); - std::vector post; - std::vector> - stack; - const CFGBlock &entry = cfg->getEntry(); - seen[entry.getBlockID()] = true; - stack.emplace_back(&entry, entry.succ_begin()); - while (!stack.empty()) { - auto &[block, it] = stack.back(); - if (it == block->succ_end()) { - post.push_back(block->getBlockID()); - stack.pop_back(); - continue; - } - const CFGBlock *succ = it->getReachableBlock(); - ++it; - if (succ != nullptr && !seen[succ->getBlockID()]) { - seen[succ->getBlockID()] = true; - stack.emplace_back(succ, succ->succ_begin()); - } - } - rpo.assign(post.rbegin(), post.rend()); - } - std::vector rank(cfg->getNumBlockIDs(), ~0U); - for (unsigned i = 0; i < rpo.size(); ++i) - rank[rpo[i]] = i; - std::map> thresholds; - std::vector byId(cfg->getNumBlockIDs(), nullptr); - for (const CFGBlock *block : *cfg) - byId[block->getBlockID()] = block; - order.clear(); - for (unsigned id : rpo) - order.push_back(byId[id]); - auto blockById = [&](unsigned id) { return byId[id]; }; - core::HeapState start; - initialState(start); - seedContextGlobals(*this, start, aliasContext, function); - entryStates[cfg->getEntry().getBlockID()] = start; - std::set> worklist; // (rank, block) - worklist.emplace(rank[cfg->getEntry().getBlockID()], - cfg->getEntry().getBlockID()); - const std::uint64_t budget = unit.input.options.budget; - // RFC 0033 §9: this run's share of what the unit has left. - const std::uint64_t share = unit.runShare(); - static const bool Trace = std::getenv("WEAVEC_ENGINE_TRACE") != nullptr; - auto thresholdsOf = [&](unsigned head) -> const std::vector & { - auto [it, inserted] = thresholds.try_emplace(head); - if (inserted) - it->second = wideningThresholds(head); - return it->second; - }; - // §12: a loop head a back edge changed waits until nothing in its loop - // is pending, so it takes in every back edge of the round at once rather - // than restarting the round at each (a dispatch loop's many cases). - std::vector changedByBackEdge(cfg->getNumBlockIDs(), false); - auto nextBlock = [&] { - for (auto candidate = worklist.begin(); candidate != worklist.end(); - ++candidate) { - unsigned head = candidate->second; - if (!changedByBackEdge[head]) - return candidate; - const llvm::BitVector &body = loopBody.find(head)->second; - bool bodyPending = false; - for (auto other = std::next(candidate); - other != worklist.end() && !bodyPending; ++other) - bodyPending = body.test(other->second); - if (!bodyPending) - return candidate; - } - // (Irreducible loops whose bodies hold each other's heads.) - return worklist.begin(); - }; - // RFC 0034 §7.1: work is the size of what a transfer or a join handles. - const auto sizeOf = [](const core::HeapState &state) -> std::uint64_t { - return 1 + state.syms.size() + state.objects.size(); - }; - const auto spend = [&](std::uint64_t amount) { - work += amount; - unit.unitWork += amount; - }; - retained = sizeOf(start); - while (!worklist.empty()) { - auto next = nextBlock(); - unsigned id = next->second; - worklist.erase(next); - changedByBackEdge[id] = false; - ++transfers; - if ((budget != 0 && work > budget) || (share != 0 && work > share) || - retained > MaxRetainedState) { - overBudget = true; - spentBudget = - (budget != 0 && work > budget) || retained > MaxRetainedState; - break; - } - const CFGBlock *block = byId[id]; - if (block == nullptr || !entryStates[id]) - continue; - spend(sizeOf(*entryStates[id])); - std::vector> outs; - transferBlock(*block, *entryStates[id], outs); - if (Trace) - llvm::errs() << "visit B" << id << " -> " << outs.size() << " outs\n"; - for (auto &[succ, state] : outs) { - dropDeadLocals(state, succ); - dropDeadValues(state, succ); - if (Trace) - llvm::errs() << " to B" << succ - << (entryStates[succ] ? " join" : " first") << "\n" - << heap.dump(state); - if (!entryStates[succ]) { - retained += sizeOf(state); - entryStates[succ] = std::move(state); - worklist.emplace(rank[succ], succ); - continue; - } - ++joins; - // (A loop entered again from outside, with what an enclosing loop's - // round changed: its budget counts from here.) - if (loopHead[succ] && !backEdges.contains({id, succ})) { - visits[succ] = 0; - joinsAt[succ] = 0; - } - core::HeapState joined = heap.join( - *entryStates[succ], state, handleOf(blockById(succ)), loopHead[succ]); - spend(sizeOf(joined) + sizeOf(state)); - // Widening starts after a loop head's first joins; its budget counts - // the joins that changed its state (another back edge of the same - // round that adds nothing is none), and all its joins. - if (loopHead[succ] && ++joinsAt[succ] > WidenAfter) - joined = heap.widen( - *entryStates[succ], joined, handleOf(blockById(succ)), - visits[succ] > ThresholdRounds ? std::vector{} - : thresholdsOf(succ)); - bool changed = !(joined == *entryStates[succ]); - // (A settled loop head may come back renumbered: that is no change.) - if (changed && loopHead[succ] && - heap.equivalent(joined, *entryStates[succ])) - changed = false; - if (loopHead[succ] && changed) - ++visits[succ]; - const unsigned joinLimit = std::max( - MaxJoinsPerBlock, JoinsPerPredecessor * blockById(succ)->pred_size()); - const unsigned visitLimit = std::max( - MaxVisitsPerBlock, - VisitsPerPredecessor * blockById(succ)->pred_size()); - if (visits[succ] > visitLimit || joinsAt[succ] > joinLimit || - joined.syms.size() > MaxStateSymbols) { - overBudget = true; - worklist.clear(); - break; - } - if (Trace) - llvm::errs() << " joined at B" << succ << "\n" << heap.dump(joined); - if (changed) { - retained += sizeOf(joined); - retained -= std::min(retained, sizeOf(*entryStates[succ])); - entryStates[succ] = std::move(joined); - worklist.emplace(rank[succ], succ); - if (backEdges.contains({id, succ})) - changedByBackEdge[succ] = true; - } - } - } - result.transfers = transfers; - result.work = work; - // §12: what the run cost. - if (core::AnalysisStats *stats = unit.input.options.stats) { - stats->add("engine_runs"); - stats->add("block_transfers", transfers); - stats->add("work", work); - stats->add("joins", joins); - stats->add("materialisations", materialisations); - stats->atLeast("objects_max", objects.size()); - std::uint64_t zone = 0; - for (const std::optional &entry : entryStates) - if (entry) - zone = std::max(zone, entry->zone.symbols().size()); - stats->atLeast("zone_symbols_max", zone); - } - if (overBudget) { - result.overBudget = true; - result.spentBudget = spentBudget; - result.effects.incomplete = "the analysis budget was exceeded"; - return result; - } - finalPass(); - // RFC 0030 §7.6: what the function hands out at its exits meets the - // invariants (its own locals are gone). - if (checkingInvariants) - for (const core::HeapState &exit : exits) - for (const auto &[id, object] : exit.objects) - if (object.life == core::Life::Live && - objects.info(id).key.kind != core::ObjectKind::Local) - checkInvariants(exit, id); - if (const char *level = std::getenv("WEAVEC_ENGINE_DUMP"); - level != nullptr && std::string_view(level) == "3") - dump(llvm::errs()); - result.effects = deriveEffects(); - // RFC 0034 §6.1: after the summary, so the replay changes nothing it - // derives from. - confirmCandidates(); - return result; -} - -void FunctionRun::finalPass() { - publishing = mode != RunMode::Summary; - inFinalPass = true; - exits.clear(); - requirementViolated.clear(); - // Blocks with no statements that reach the exit without a branch: an - // edge into one leaves the function (the join after an `if` at the end of - // the body). - std::vector> fallsToExit(cfg->getNumBlockIDs()); - auto emptyToExit = [&](auto &self, const CFGBlock *block, int depth) -> bool { - if (block == nullptr || depth > 8) - return false; - if (block == &cfg->getExit()) - return true; - std::optional &known = fallsToExit[block->getBlockID()]; - if (known) - return *known; - bool empty = - block->getTerminatorStmt() == nullptr && block->succ_size() == 1; - for (const CFGElement &element : *block) - empty = empty && !element.getAs(); - known = empty && self(self, *block->succ_begin(), depth + 1); - return *known; - }; - // Every reachable block once, from its fixpoint entry state, in order. - for (const CFGBlock *block : order) { - if (!entryStates[block->getBlockID()]) - continue; - std::vector> outs; - core::HeapState state = *entryStates[block->getBlockID()]; - transferBlock(*block, state, outs); - // Falling off the end of the body (a `return` noted its own exit). - bool returns = false; - for (const CFGElement &element : *block) - if (auto stmt = element.getAs()) - returns = isa(stmt->getStmt()); - for (auto &[succ, outState] : outs) { - // RFC 0031 §5.8: an object this path leaves live that the join - // ahead no longer shows live is leaked on this path, at the branch. - const CFGBlock *next = nullptr; - for (const CFGBlock::AdjacentBlock &adjacent : block->succs()) - if (adjacent && adjacent->getBlockID() == succ) - next = adjacent; - if (publishing && !returns && succ != cfg->getExit().getBlockID() && - block->getTerminatorStmt() != nullptr && - emptyToExit(emptyToExit, next, 0) && entryStates[succ]) { - core::HeapState edge = outState; - for (const auto &[id, object] : entryStates[succ]->objects) - if (object.life == core::Life::Live && object.owned && - edge.objects.contains(id)) - edge.objects.at(id).escaped = true; - checkLeaks(edge, *block->getTerminatorStmt(), LeakPoint::ExitEdge); - } - if (succ == cfg->getExit().getBlockID() && !returns) { - Transfer transfer(*this, outState); - if (publishing) { - transfer.decideExitSite(*function.getBody(), false); - transfer.exitLifetimes(*function.getBody(), nullptr); - } - checkLeaks(outState, *function.getBody(), LeakPoint::Exit); - exits.push_back(outState); - } - } - } - publishing = false; -} - -void FunctionRun::dump(llvm::raw_ostream &os) { - os << "function " << function.getNameAsString() << "\n"; - if (!cfg) { - os << " (no CFG)\n"; - return; - } - for (const CFGBlock *block : *cfg) { - os << " block B" << block->getBlockID() << "\n"; - if (entryStates[block->getBlockID()]) - os << heap.dump(*entryStates[block->getBlockID()]); - else - os << " unreachable\n"; - } -} - -} // namespace weavec::analysis::engine diff --git a/lib/Analysis/EngineStrings.cpp b/lib/Analysis/EngineStrings.cpp deleted file mode 100644 index 8582c328..00000000 --- a/lib/Analysis/EngineStrings.cpp +++ /dev/null @@ -1,475 +0,0 @@ -//===- EngineStrings.cpp - String facts in the object engine --------------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// RFC 0012 *String facts* over the object domain (RFC 0031 §4.3): what is -// known of the NUL-terminated string a pointer points to comes from the -// object's bytes (a literal's contents, the byte cells stores and fills -// wrote, a zeroed object) and from the object's string fact (`nulWithin`: a -// NUL at an offset, and none from `nulFrom` up to it), which the rows' -// `writes-str` and the measuring calls (`strlen`) set. A length known -// exactly decides a need both ways; one known only as a bound proves. -// -//===----------------------------------------------------------------------===// - -#include "Engine.h" -#include "weavec/Analysis/ClangLocation.h" - -#include "clang/AST/RecordLayout.h" -#include "clang/Basic/SourceManager.h" - -#include - -using namespace clang; - -namespace weavec::analysis::engine { - -/// The bytes of a string literal object, without its terminating NUL. -static std::optional -literalBytes(const core::ObjectInfo &info) { - if (info.key.kind != core::ObjectKind::Literal) - return std::nullopt; - const auto *expr = fromHandle(info.key.handle); - const auto *literal = dyn_cast_or_null(expr); - if (const auto *predefined = dyn_cast_or_null(expr)) - literal = predefined->getFunctionName(); - if (literal == nullptr || literal->getCharByteWidth() != 1) - return std::nullopt; - return literal->getBytes(); -} - -/// The width of the scalar at `offset` in an object of `type`, when the -/// type says (a value keeps the type it was made with, not its cell's). -static std::optional -scalarWidth(const ASTContext &context, QualType type, std::int64_t offset) { - for (int depth = 0; depth < 16 && !type.isNull(); ++depth) { - type = type.getCanonicalType(); - if (type->isIncompleteType() && !type->isIncompleteArrayType()) - return std::nullopt; - if (const ArrayType *array = context.getAsArrayType(type)) { - QualType element = array->getElementType(); - if (element->isIncompleteType()) - return std::nullopt; - auto size = static_cast( - context.getTypeSizeInChars(element).getQuantity()); - if (size <= 0) - return std::nullopt; - offset %= size; - type = element; - continue; - } - if (const RecordDecl *record = type->getAsRecordDecl()) { - if (!record->isCompleteDefinition() || record->isUnion()) - return std::nullopt; - const ASTRecordLayout &layout = context.getASTRecordLayout(record); - QualType next; - for (const FieldDecl *field : record->fields()) { - if (field->isBitField() || field->getType()->isIncompleteType()) - continue; - auto start = static_cast( - layout.getFieldOffset(field->getFieldIndex()) / - context.getCharWidth()); - auto size = static_cast( - context.getTypeSizeInChars(field->getType()).getQuantity()); - if (offset >= start && offset < start + size) { - next = field->getType(); - offset -= start; - break; - } - } - if (next.isNull()) - return std::nullopt; - type = next; - continue; - } - if (!type->isScalarType() || offset != 0) - return std::nullopt; - return static_cast( - context.getTypeSizeInChars(type).getQuantity()); - } - return std::nullopt; -} - -/// The bytes a value of `info` stored in memory occupies. -static std::optional valueWidth(const core::SymInfo &info) { - if (info.type == core::SymInfo::Type::Int && info.intType) - return info.intType->isBoolean ? 1 : std::max(1U, info.intType->width / 8); - if (info.type == core::SymInfo::Type::Pointer) - return 8; - return std::nullopt; -} - -Transfer::Byte Transfer::valueByte(core::Sym sym, - std::optional width) const { - const core::SymInfo &info = heap.info(state, sym); - if (info.type == core::SymInfo::Type::Pointer) - return info.null == core::PointerNull::Null ? Byte::Zero : Byte::Unknown; - if (info.type != core::SymInfo::Type::Int) - return Byte::Unknown; - if (auto c = state.zone.constant(sym); c && *c == 0) - return Byte::Zero; - // A non-zero value is a non-zero byte only when it is one byte wide (a - // value converted without change keeps a wider type, `char c = 'a'`). - if (width.value_or(valueWidth(info).value_or(0)) != 1) - return Byte::Unknown; - if (auto c = state.zone.constant(sym); c && (*c < -128 || *c > 255)) - return Byte::Unknown; - auto lo = state.zone.lower(sym); - auto hi = state.zone.upper(sym); - if ((lo && *lo > 0) || (hi && *hi < 0)) - return Byte::NonZero; - return Byte::Unknown; -} - -Transfer::Byte Transfer::byteAt(core::ObjectId id, std::int64_t offset) const { - const core::ObjectState *object = heap.findObject(state, id); - if (object == nullptr || offset < 0) - return Byte::Unknown; - const core::ObjectInfo &info = run.table().info(id); - if (auto bytes = literalBytes(info)) { - if (std::cmp_greater(offset, bytes->size())) - return Byte::Unknown; - if (std::cmp_equal(offset, bytes->size())) - return Byte::Zero; - return (*bytes)[static_cast(offset)] == '\0' ? Byte::Zero - : Byte::NonZero; - } - auto combine = [](std::optional a, Byte b) { - return !a || *a == b ? b : Byte::Unknown; - }; - std::optional byte; - bool concrete = false; - QualType objectType = info.type != 0 ? typeOfHandle(info.type) : QualType(); - for (const auto &[key, sym] : object->cells) { - const core::SymInfo &value = heap.info(state, sym); - std::optional width = - objectType.isNull() ? std::nullopt - : scalarWidth(context, objectType, key.offset); - if (!width) - width = valueWidth(value); - if (key.isSummary()) { - // Every element position of the summary may hold its value. - if (!width || offset < key.offset || - (offset - key.offset) % key.stride >= *width) - continue; - byte = combine(byte, valueByte(sym, width)); - continue; - } - if (offset < key.offset || !width || offset >= key.offset + *width) - continue; - Byte here = valueByte(sym, width); - if (here == Byte::NonZero && key.offset != offset) - here = Byte::Unknown; - byte = combine(byte, here); - concrete = true; - } - if (!concrete) { - // A byte no cell holds: what the object's creation left there. - bool zero = object->zeroed && !object->forgetsAny() && - !object->uninitialised && - (info.key.kind == core::ObjectKind::Local || - info.key.kind == core::ObjectKind::HeapRecent || - info.key.kind == core::ObjectKind::HeapOld); - byte = combine(byte, zero ? Byte::Zero : Byte::Unknown); - } - return byte.value_or(Byte::Unknown); -} - -/// `-term`. -static core::Term negated(const core::Term &term) { - core::Term out = term; - out.scale = -out.scale; - out.constant = -out.constant; - if (out.isConstant()) - out.scale = 0; - return out; -} - -Transfer::StringFacts Transfer::stringFacts(core::Sym pointer) const { - StringFacts out; - const core::SymInfo &value = heap.info(state, pointer); - if (value.type != core::SymInfo::Type::Pointer || value.top || - value.targets.size() != 1 || !value.targets[0].offset.known) - return out; - const core::Target &target = value.targets[0]; - const core::ObjectState *object = heap.findObject(state, target.object); - if (object == nullptr || object->life != core::Life::Live) - return out; - const core::Term &start = target.offset; - // The object's string fact: a NUL at `nulWithin`, from any start up to it. - if (object->nulWithin) - if (auto rest = object->nulWithin->plus(negated(start)); - rest && rest->known && - heap.lessEqual(state, core::Term::of(0), *rest).value_or(false) && - heap.lessEqual(state, core::Term::of(0), start).value_or(false)) { - // Exactly when no NUL lies from the start to it. - bool exact = false; - if (object->nulFrom) { - exact = heap.lessEqual(state, *object->nulFrom, start).value_or(false); - if (!exact && start.isConstant() && object->nulFrom->isConstant() && - object->nulFrom->constant - start.constant <= 256) { - exact = true; - for (std::int64_t at = start.constant; at < object->nulFrom->constant; - ++at) - exact = exact && byteAt(target.object, at) == Byte::NonZero; - } - } - out.length = - exact ? StringFacts::Length::Exact : StringFacts::Length::AtMost; - out.term = *rest; - out.nulAt = *object->nulWithin; - return out; - } - if (!start.isConstant() || start.constant < 0) - return out; - // The bytes from the start: the first known NUL ends the string, exactly - // when every byte before it is known not to be one. - std::optional end; - if (object->extent && object->extent->bytes.isConstant()) - end = object->extent->bytes.constant; - if (auto bytes = literalBytes(run.table().info(target.object))) - end = static_cast(bytes->size()) + 1; - std::int64_t limit = start.constant + 256; - if (end && *end < limit) - limit = *end; - bool allNonZero = true; - for (std::int64_t at = start.constant; at < limit; ++at) { - Byte byte = byteAt(target.object, at); - if (byte == Byte::Zero) { - out.length = - allNonZero ? StringFacts::Length::Exact : StringFacts::Length::AtMost; - out.term = core::Term::of(at - start.constant); - out.nulAt = core::Term::of(at); - return out; - } - if (byte != Byte::NonZero) - allNonZero = false; - } - // No byte to the object's end is NUL. - if (allNonZero && end && limit == *end && start.constant < *end) - out.unterminated = true; - return out; -} - -core::Term Transfer::measureString(core::Sym pointer, const Expr *argument) { - StringFacts facts = stringFacts(pointer); - if (facts.length == StringFacts::Length::Exact) - return facts.term; - const core::SymInfo &value = heap.info(state, pointer); - if (value.type != core::SymInfo::Type::Pointer || value.top || - value.targets.size() != 1 || !value.targets[0].offset.known || - facts.unterminated) - return core::Term::unknown(); - const core::Target target = value.targets[0]; - const core::ObjectInfo &info = run.table().info(target.object); - if (!info.singular || info.key.kind == core::ObjectKind::Literal || - !heap.lessEqual(state, core::Term::of(0), target.offset).value_or(false)) - return core::Term::unknown(); - core::ObjectState *object = state.objects.find(target.object) != nullptr - ? &state.objects.at(target.object) - : nullptr; - if (object == nullptr || object->life != core::Life::Live) - return core::Term::unknown(); - // A returned `strlen(p)` is the string's length from here on: the NUL - // lies that far from the pointer, and none before it (RFC 0012 *Length - // places*). - core::Sym length = unknownValue(context.getSizeType()); - // No object is larger than PTRDIFF_MAX bytes, so `strlen(s) + 1` does not - // wrap. - state.zone.addRange(length, 0, INT64_MAX - 1); - core::SymInfo &lengthInfo = heap.infoMut(state, length); - if (argument != nullptr) - lengthInfo.name = "strlen(" + spell(*argument) + ")"; - auto nul = target.offset.plus(core::Term::ofSym(length)); - if (!nul) - return core::Term::ofSym(length); - object = &state.objects.at(target.object); - object->nulWithin = *nul; - object->nulFrom = target.offset; - // The string ends inside an object whose extent is known. - if (object->extent && object->extent->bytes.known && - (object->extent->cls == core::ExtentClass::Exact || - object->extent->cls == core::ExtentClass::Declared)) { - const core::Term &bytes = object->extent->bytes; - if (bytes.isConstant() && nul->scale == 1) - state.zone.addLE(length, core::ZeroSym, - bytes.constant - 1 - nul->constant); - else if (bytes.var != core::ZeroSym && bytes.scale == 1 && nul->scale == 1) - state.zone.addLE(length, bytes.var, bytes.constant - 1 - nul->constant); - } - return core::Term::ofSym(length); -} - -void Transfer::noteStringWritten(core::Sym pointer, const core::Term &length) { - const core::SymInfo &value = heap.info(state, pointer); - if (value.type != core::SymInfo::Type::Pointer || value.top || - value.targets.size() != 1 || !length.known) - return; - const core::Target target = value.targets[0]; - if (!state.objects.contains(target.object)) - return; - auto nul = target.offset.plus(length); - if (!nul || !nul->known) - return; - core::ObjectState &object = state.objects.at(target.object); - // A terminator written past the end of the object (the write was out of - // bounds, and reported so) is no fact about the object's bytes. - if (object.extent && object.extent->cls == core::ExtentClass::Exact && - heap.lessEqual(state, object.extent->bytes, *nul).value_or(false)) - return; - object.nulWithin = *nul; - object.nulFrom = target.offset; -} - -void Transfer::stringStored(core::ObjectId id, const core::Term &offset, - std::int64_t width, core::Sym value) { - core::ObjectState *object = - state.objects.find(id) != nullptr ? &state.objects.at(id) : nullptr; - if (object == nullptr || !object->nulWithin) - return; - const core::Term nul = *object->nulWithin; - Byte byte = width == 1 ? valueByte(value, width) : Byte::Unknown; - if (width > 1 && state.zone.constant(value) == 0) - byte = Byte::Zero; - // Wholly before the NUL: it stays; a possible NUL is excluded only after - // the store. - if (offset.known && - heap.lessEqual(state, offset.plusConstant(width), nul).value_or(false)) { - if (byte != Byte::NonZero && object->nulFrom && - !heap.lessEqual(state, offset.plusConstant(width), *object->nulFrom) - .value_or(false)) - object->nulFrom = offset.plusConstant(width); - return; - } - // Wholly after it: nothing changes. - if (offset.known && - heap.lessEqual(state, nul.plusConstant(1), offset).value_or(false)) - return; - // Over it: a NUL written exactly there keeps it. - if (byte == Byte::Zero && width == 1 && offset == nul) - return; - object->nulWithin.reset(); - object->nulFrom.reset(); -} - -//===----------------------------------------------------------------------===// -// Formats (RFC 0012, RFC 0030 §8.2) -//===----------------------------------------------------------------------===// - -// NOLINTBEGIN(readability-convert-member-functions-to-static): the other -// engine files call it through the instance. -Transfer::FormatFacts -Transfer::formatFacts(const CallExpr &call, - const core::LibraryMatch &match) const { - // NOLINTEND(readability-convert-member-functions-to-static) - FormatFacts out; - const auto &format = match.entry->format; - if (!format || format->kind != core::LibFormat::Kind::Printf) - return out; - int index = match.callArgument(format->format); - int first = match.callArgument(format->first); - if (index < 0 || static_cast(index) >= call.getNumArgs()) - return out; - const auto *text = dyn_cast( - call.getArg(static_cast(index))->IgnoreParenImpCasts()); - if (text == nullptr || text->getCharByteWidth() != 1) - return out; - out.literal = true; - if (format->vaList) - return out; - out.passed = first >= 0 && call.getNumArgs() > static_cast(first) - ? call.getNumArgs() - static_cast(first) - : 0U; - out.reads = core::formatArgumentCount(text->getString(), format->kind); - // The least output: every literal byte, one per conversion, and the - // known length of a `%s` argument; exact when nothing varies. - llvm::StringRef spec = text->getString(); - unsigned argument = - first >= 0 ? static_cast(first) : call.getNumArgs(); - std::int64_t lower = 0; - bool exact = true; - for (std::size_t i = 0; i < spec.size(); ++i) { - if (spec[i] != '%') { - ++lower; - continue; - } - if (++i >= spec.size()) - break; - if (spec[i] == '%') { - ++lower; - continue; - } - bool sized = false; - bool precision = false; - while (i < spec.size() && llvm::StringRef("-+ #0").contains(spec[i])) - ++i; - if (i < spec.size() && spec[i] == '*') { - ++argument; - ++i; - sized = true; - } else { - while (i < spec.size() && llvm::isDigit(spec[i])) { - ++i; - sized = true; - } - } - if (i < spec.size() && spec[i] == '.') { - ++i; - sized = true; - precision = true; - if (i < spec.size() && spec[i] == '*') { - ++argument; - ++i; - } else { - while (i < spec.size() && llvm::isDigit(spec[i])) - ++i; - } - } - while (i < spec.size() && llvm::StringRef("hljztLq").contains(spec[i])) - ++i; - if (i >= spec.size()) - break; - switch (spec[i]) { - case 'c': - ++lower; - exact = exact && !sized; - ++argument; - break; - case 's': { - (precision ? out.bounded : out.strings).push_back(argument); - std::optional length; - if (argument < call.getNumArgs()) - if (const auto *literal = dyn_cast( - call.getArg(argument)->IgnoreParenImpCasts()); - literal != nullptr && literal->getCharByteWidth() == 1) - length = static_cast( - literal->getBytes() - .take_until([](char c) { return c == '\0'; }) - .size()); - if (length && !sized) - lower += *length; - else - exact = false; - ++argument; - break; - } - case 'n': - ++argument; - break; - default: - ++lower; - exact = false; - ++argument; - break; - } - } - out.lower = lower; - out.exact = exact; - return out; -} - -} // namespace weavec::analysis::engine diff --git a/lib/Analysis/EngineSummary.cpp b/lib/Analysis/EngineSummary.cpp deleted file mode 100644 index 1e8a9cd0..00000000 --- a/lib/Analysis/EngineSummary.cpp +++ /dev/null @@ -1,3574 +0,0 @@ -//===- EngineSummary.cpp - Format-30 summaries in the object engine -------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// RFC 0031 §6: a summary is read off the exit states against the entry heap -// (§6.2) and applied at a call by walking the callee's paths through the -// caller's memory (§6.3). Effects keyed on a result class wait on the -// result symbol until a test of it selects the class (RFC 0030 §9.1). -// -//===----------------------------------------------------------------------===// - -#include "Engine.h" -#include "weavec/Analysis/ClangLocation.h" - -#include "clang/AST/RecordLayout.h" -#include "clang/AST/RecursiveASTVisitor.h" -#include "clang/Basic/SourceManager.h" - -#include -#include -#include -#include -#include -#include - -using namespace clang; - -namespace weavec::analysis::engine { - -//===----------------------------------------------------------------------===// -// Derivation (§6.2) -//===----------------------------------------------------------------------===// - -/// The result classes of one exit's returned value. -static std::vector classesOf(const core::Heap &heap, - const core::HeapState &state, - QualType returnType) { - using core::ResultClass; - if (state.result == core::ZeroSym || returnType->isVoidType()) - return {}; - const core::SymInfo &value = heap.info(state, state.result); - if (value.type == core::SymInfo::Type::Pointer) { - switch (value.null) { - case core::PointerNull::Null: - return {ResultClass::Null}; - case core::PointerNull::NonNull: - return {ResultClass::NonNull}; - case core::PointerNull::Maybe: - return {ResultClass::Null, ResultClass::NonNull}; - } - } - if (value.type == core::SymInfo::Type::Int) { - std::vector classes; - auto lo = state.zone.lower(state.result); - auto hi = state.zone.upper(state.result); - if (!lo || *lo < 0) - classes.push_back(ResultClass::Negative); - if ((!lo || *lo <= 0) && (!hi || *hi >= 0)) - classes.push_back(ResultClass::Zero); - if (!hi || *hi > 0) - classes.push_back(ResultClass::Positive); - if (hi && *hi < 0) - std::erase(classes, ResultClass::Zero); - if (lo && *lo >= 0) - std::erase(classes, ResultClass::Negative); - return classes; - } - return {}; -} - -namespace { -/// The effect a path's object got on one exit. -struct ExitEffect { - core::PathEffect::Kind kind = {}; - bool may = false; - std::string family; - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::vector> guard = {}; - /// A release's offset into the object; none when not a constant. - std::optional offset = 0; - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::vector pairs = {}; -}; -} // namespace - -/// The field name at `offset` of `type`, or `#`. -static std::string fieldNameAt(const ASTContext &context, QualType type, - std::int64_t offset) { - if (!type.isNull()) - if (const RecordDecl *record = type->getAsRecordDecl(); - record != nullptr && record->isCompleteDefinition()) { - const ASTRecordLayout &layout = context.getASTRecordLayout(record); - for (const FieldDecl *field : record->fields()) - if (std::cmp_equal(layout.getFieldOffset(field->getFieldIndex()) / - context.getCharWidth(), - offset) && - !field->getName().empty()) - return field->getNameAsString(); - } - return "#" + std::to_string(offset); -} - -core::SummaryPath cellPath(const ASTContext &context, QualType type, - core::SummaryPath path, std::int64_t offset, - bool named) { - type = type.isNull() ? type : type.getCanonicalType(); - bool stepped = false; - for (int depth = 0; depth < 16 && !type.isNull(); ++depth) { - const RecordDecl *record = type->getAsRecordDecl(); - if (record == nullptr || !record->isCompleteDefinition()) - break; - const ASTRecordLayout &layout = context.getASTRecordLayout(record); - const FieldDecl *found = nullptr; - std::int64_t at = 0; - for (const FieldDecl *field : record->fields()) { - auto start = static_cast( - layout.getFieldOffset(field->getFieldIndex()) / - context.getCharWidth()); - QualType fieldType = field->getType(); - std::int64_t size = - fieldType->isIncompleteType() - ? 0 - : static_cast( - context.getTypeSizeInChars(fieldType).getQuantity()); - if (offset < start || - (offset >= start + size && (size != 0 || offset != start))) - continue; - if (found == nullptr || fieldType->isPointerType()) { - found = field; - at = start; - } - // A union: prefer a pointer member (as the loads do). - if (!record->isUnion() || fieldType->isPointerType()) - break; - } - if (found == nullptr || found->isBitField()) - break; - // RFC 0031 §7: an anonymous member is spelled by where it starts. - path = path.field(found->getName().empty() ? "#" + std::to_string(at) - : found->getNameAsString()); - stepped = true; - offset -= at; - type = found->getType().getCanonicalType(); - if (offset == 0 && !type->isRecordType()) - return path; - } - if (offset == 0 && (stepped || !named)) - return path; - return path.field("#" + std::to_string(offset)); -} - -/// Calls `visit(offset, type)` for each scalar member of `type` from byte -/// `base` (a union's members, which share their bytes, once per offset; a -/// member array or bit-field is skipped: its cells are not named here). -template -static void forEachScalar(const ASTContext &context, QualType type, - std::int64_t base, std::set &seen, - Visit visit, int depth = 0) { - if (type.isNull() || depth > 8) - return; - type = type.getCanonicalType(); - if (type->isPointerType() || type->isIntegralOrEnumerationType()) { - if (seen.insert(base).second) - visit(base, type); - return; - } - const RecordDecl *record = type->getAsRecordDecl(); - if (record == nullptr || !record->isCompleteDefinition()) - return; - const ASTRecordLayout &layout = context.getASTRecordLayout(record); - for (const FieldDecl *field : record->fields()) { - if (field->isBitField()) - continue; - forEachScalar(context, field->getType(), - base + static_cast( - layout.getFieldOffset(field->getFieldIndex()) / - context.getCharWidth()), - seen, visit, depth + 1); - } -} - -template -void FunctionRun::describeContents( - const core::HeapState &state, core::ObjectId object, - const core::SummaryPath &path, - std::map &stores, int depth, - Describe &describe) { - if (depth > 2) - return; - if (depth == 0) - visitedFresh = {object}; - const core::ObjectState *found = heap.findObject(state, object); - if (found == nullptr) - return; - const core::ObjectInfo &info = objects.info(object); - QualType type = info.type != 0 ? typeOfHandle(info.type) : QualType(); - for (const auto &[key, sym] : found->cells) { - if (!key.isConcrete()) - continue; - const core::SymInfo &value = heap.info(state, sym); - core::SummaryPath dest = cellPath(context, type, path, key.offset, true); - // An unknown value too (and one of another type): the caller's new - // object would otherwise read its unwritten (zero) value there. - core::ValueDesc desc = value.type == core::SymInfo::Type::Pointer || - value.type == core::SymInfo::Type::Int - ? describe(state, sym) - : core::ValueDesc{}; - bool below = desc.kind == core::ValueDesc::Kind::Fresh && !desc.many && - !desc.offset && !desc.interior && value.targets.size() == 1; - // A new object whose contents are not described below reads its cells - // as unknown in the caller, not as zeros. - if (desc.kind == core::ValueDesc::Kind::Fresh && (!below || depth >= 2)) - desc.zeroed = false; - stores[dest] = desc; - if (below && depth < 2 && - visitedFresh.insert(value.targets[0].object).second) - describeContents(state, value.targets[0].object, dest.deref(), stores, - depth + 1, describe); - } - // Its elements (RFC 0015 §5): what some elements hold, a weak store - // (`p[*]`, `p[*].f`). - QualType element = type; - if (!element.isNull()) - if (const auto *array = context.getAsArrayType(element)) - element = array->getElementType(); - auto put = [&](const core::CellKey &position, core::Sym sym) { - core::SummaryPath dest = elementsOf(path); - if (!element.isNull() && element->isRecordType()) - dest = dest.field(fieldNameAt(context, element, position.offset)); - else if (position.offset != 0) - dest = dest.field("#" + std::to_string(position.offset)); - core::ValueDesc desc = describe(state, sym); - auto [it, inserted] = stores.emplace(dest, desc); - if (!inserted && !(it->second == desc)) - it->second = core::ValueDesc{}; - }; - for (const auto &[key, sym] : found->cells) - if (!key.isConcrete()) - put(key.isSummary() ? key : key.position(), sym); - for (const core::Segment &segment : found->segments) - put(segment.position, segment.value); -} - -namespace { -/// Finds the parameters a body assigns or takes the address of. -class ParameterWrites : public RecursiveASTVisitor { -public: - std::set written; - // RecursiveASTVisitor's CRTP hooks are found by name. - // NOLINTBEGIN(readability-identifier-naming,bugprone-derived-method-shadowing-base-method) - bool VisitBinaryOperator(BinaryOperator *op) { - if (op->isAssignmentOp()) - note(op->getLHS()); - return true; - } - bool VisitUnaryOperator(UnaryOperator *op) { - if (op->isIncrementDecrementOp() || op->getOpcode() == UO_AddrOf) - note(op->getSubExpr()); - return true; - } - // NOLINTEND(readability-identifier-naming,bugprone-derived-method-shadowing-base-method) - -private: - void note(const Expr *expr) { - if (const auto *ref = dyn_cast(expr->IgnoreParenImpCasts())) - if (const auto *param = dyn_cast(ref->getDecl())) - written.insert(param->getCanonicalDecl()); - } -}; -} // namespace - -std::optional -FunctionRun::parameterTerm(const core::HeapState &state, - const core::Term &term) const { - if (!term.known) - return std::nullopt; - auto fits = [](__int128 value) { - return value >= INT64_MIN && value <= INT64_MAX; - }; - if (term.isConstant()) - return core::PathTerm{ - .path = std::nullopt, .scale = 1, .constant = term.constant}; - if (auto value = state.zone.constant(term.var)) { - __int128 folded = - (static_cast<__int128>(term.scale) * *value) + term.constant; - if (!fits(folded)) - return std::nullopt; - return core::PathTerm{.path = std::nullopt, - .scale = 1, - .constant = static_cast(folded)}; - } - if (!unchangedParams) { - ParameterWrites writes; - if (const Stmt *body = function.getBody()) - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast): Clang's API - writes.TraverseStmt(const_cast(body)); - std::vector unchanged; - for (const ParmVarDecl *param : function.parameters()) - unchanged.push_back(!writes.written.contains(param->getCanonicalDecl())); - unchangedParams = std::move(unchanged); - } - // A parameter the body never changes still holds its entry value. - for (unsigned i = 0; i < function.getNumParams(); ++i) { - const ParmVarDecl *param = function.getParamDecl(i); - if (!(*unchangedParams)[i] || - !param->getType()->isIntegralOrEnumerationType()) - continue; - auto held = heap.read(state, variableObject(*param), core::CellKey{}); - if (!held) - continue; - std::optional offset; - if (*held == term.var) { - offset = 0; - } else { - auto up = state.zone.bound(term.var, *held); - auto down = state.zone.bound(*held, term.var); - if (up && down && *down != INT64_MIN && *up == -*down) - offset = up; - } - if (!offset) - continue; - __int128 constant = static_cast<__int128>(term.constant) + - (static_cast<__int128>(term.scale) * *offset); - if (!fits(constant)) - return std::nullopt; - return core::PathTerm{.path = core::SummaryPath::param(i), - .scale = term.scale, - .constant = static_cast(constant)}; - } - return std::nullopt; -} - -template -void FunctionRun::describeElements( - const core::HeapState &state, core::ObjectId id, - const core::ObjectState &contents, const core::SummaryPath &objectPath, - Describe &describe, std::map &releases, - std::map &stores) { - const core::ObjectInfo &info = objects.info(id); - QualType type = info.type != 0 ? typeOfHandle(info.type) : QualType(); - QualType element = type; - if (!element.isNull()) - if (const auto *array = context.getAsArrayType(element)) - element = array->getElementType(); - // The path of the element cell at `position`: `p[*]`, or `p[*].f` for a - // record element. - // (For a member array, the elements a position's range counts are the - // member's, `shift` of them further on: a position is kept modulo its - // stride, so the second field of `sub[i]` may be counted from `sub[i+1]`.) - std::int64_t shift = 0; - auto elementPath = - [&](const core::CellKey &position) -> std::optional { - shift = 0; - // Elements of a member array (`n->b[i]` in one record): the stride is - // the member's element size, not the record's. - if (!element.isNull() && element->isRecordType() && - !element->isIncompleteType() && - static_cast( - context.getTypeSizeInChars(element).getQuantity()) != - static_cast(position.stride)) - if (const RecordDecl *record = element->getAsRecordDecl(); - record != nullptr && record->isCompleteDefinition() && - !objectPath.steps.empty() && - objectPath.steps.back().step == core::PathStep::Deref && - element == type) { - const ASTRecordLayout &layout = context.getASTRecordLayout(record); - for (const FieldDecl *field : record->fields()) { - const auto *array = context.getAsConstantArrayType(field->getType()); - if (array == nullptr || field->getName().empty()) - continue; - QualType member = array->getElementType(); - auto start = static_cast( - layout.getFieldOffset(field->getFieldIndex()) / - context.getCharWidth()); - auto size = static_cast( - context.getTypeSizeInChars(field->getType()).getQuantity()); - auto stride = static_cast( - context.getTypeSizeInChars(member).getQuantity()); - if (std::cmp_not_equal(stride, position.stride) || size <= 0 || - stride <= 0) - continue; - // The element of the member the position's first byte falls in. - std::int64_t within = position.offset - start; - std::int64_t back = within >= 0 ? 0 : (-within + stride - 1) / stride; - within += back * stride; - if (within < 0 || within >= stride) - continue; - shift = -back; - core::SummaryPath elements = - elementsOf(objectPath.field(field->getName())); - // (A record element: the member of it at that offset.) - if (member->isRecordType()) { - std::string name = fieldNameAt(context, member, within); - if (name.front() == '#') - return std::nullopt; - return elements.field(name); - } - if (within != 0) - continue; - return elements; - } - return std::nullopt; - } - core::SummaryPath path = elementsOf(objectPath); - if (!element.isNull() && element->isRecordType()) { - std::string name = fieldNameAt(context, element, position.offset); - if (name.front() == '#') - return std::nullopt; - return path.field(name); - } - if (position.offset != 0) - return std::nullopt; - return path; - }; - // A value some element of this object held at entry. - auto entryElement = [&](const core::SymInfo &value) { - if (value.type != core::SymInfo::Type::Pointer || value.top || - value.targets.empty()) - return false; - return std::ranges::all_of(value.targets, [&](const core::Target &target) { - const core::ObjectInfo &pointee = - objects.info(objects.liveVersion(target.object)); - return pointee.key.kind == core::ObjectKind::Entry && - objectPath.isProperPrefixOf(pointee.key.path) && - pointee.key.path.steps.size() <= objectPath.steps.size() + 3; - }); - }; - auto note = [&](const core::CellKey &position, - std::optional> bounds, - core::Sym value) { - auto path = elementPath(position); - if (!path) - return; - if (bounds && shift != 0) { - bounds->first = bounds->first.plusConstant(shift); - bounds->second = bounds->second.plusConstant(shift); - } - std::optional range; - if (bounds) { - auto from = parameterTerm(state, bounds->first); - auto to = parameterTerm(state, bounds->second); - if (from && to) - range = core::ElementRange{.from = *from, .to = *to}; - } - const core::SymInfo &held = heap.info(state, value); - if (entryElement(held)) { - // The elements' own entry values, released or only read; when the - // function stored into the object, possibly another element's: some - // elements may now hold any element's value. - if (held.release && held.release->definite()) - releases[{path->deref(), range}] = held.release->family; - // (Or null, where the function joined in a null it stored.) - else if (contents.stored) - stores[{*path, std::nullopt}] = - core::ValueDesc{.kind = core::ValueDesc::Kind::Path, - .path = path, - .maybeNull = held.nullJoined}; - return; - } - // Only a store changes what an element holds. - if (contents.stored) - stores[{*path, range}] = describe(state, value); - }; - for (const core::Segment &segment : contents.segments) - note(segment.position, std::make_pair(segment.from, segment.to), - segment.value); - for (const auto &[key, sym] : contents.cells) { - if (key.isSummary()) { - note(key, std::nullopt, sym); - continue; - } - if (!key.isSelected()) - continue; - core::CellKey position = key.position(); - core::Term index = core::Term::ofSym( - key.index, 1, - (key.offset - position.offset) / static_cast(key.stride)); - note(position, std::make_pair(index, index.plusConstant(1)), sym); - } -} - -// NOLINTNEXTLINE(readability-function-size): one pass over the exits' views -core::FunctionEffects FunctionRun::deriveEffects() { - core::FunctionEffects effects; - if (exits.empty()) { - effects.returns = core::FunctionEffects::Returns::Never; - return effects; - } - QualType returnType = function.getReturnType(); - // RFC 0030 §9.1: an exit whose result carries pending cases is one exit - // per class of the result, each with the cases it selects applied. - // So is one that returns a fixed local a release was guarded by (the - // release happened where it was non-null: not on the null class). - auto guardingLocals = [&](const core::HeapState &exit) { - std::vector found; - for (const clang::VarDecl *var : fixedLocals) { - const core::ObjectState *holder = - heap.findObject(exit, variableObject(*var)); - const core::Sym *value = - holder != nullptr ? holder->cells.find(core::CellKey{}) : nullptr; - if (value == nullptr || *value != exit.result) - continue; - for (const auto &[id, object] : exit.objects) - if (object.record && - std::binary_search(object.record->nonNullLocals.begin(), - object.record->nonNullLocals.end(), - handleOf(var))) { - found.push_back(handleOf(var)); - break; - } - } - return found; - }; - std::vector split; - for (const core::HeapState &exit : exits) { - const core::SymInfo *result = - exit.result != core::ZeroSym ? exit.syms.find(exit.result) : nullptr; - const std::vector guarding = - result != nullptr && result->type == core::SymInfo::Type::Pointer - ? guardingLocals(exit) - : std::vector{}; - if (result == nullptr || - (result->pending.empty() && guarding.empty() && !result->condition)) { - split.push_back(exit); - continue; - } - std::vector classes = classesOf(heap, exit, returnType); - for (core::ResultClass c : classes) { - core::HeapState refined = exit; - if (!Transfer::selectClass(*this, refined, exit.result, c)) - continue; - if (c == core::ResultClass::Null) { - std::vector undone; - for (const auto &[id, object] : refined.objects) - if (object.record && - std::ranges::any_of(guarding, [&](core::Handle var) { - return std::ranges::binary_search(object.record->nonNullLocals, - var); - })) - undone.push_back(id); - for (core::ObjectId id : undone) { - core::ObjectState &object = refined.objects.at(id); - object.life = core::Life::Live; - object.record.reset(); - object.effectReleased = false; - object.effectMayReleased = false; - } - } - split.push_back(std::move(refined)); - } - } - // Per exit: its classes, and the effect per entry path. - struct ExitView { - std::vector classes; - std::map byPath; - std::map stores; - /// Byte ranges of entry objects whose bytes the function rewrote with - /// values it cannot describe (`StoreEffect::bytes`), by object path, - /// each with whether it may only have (`mayForgotten`). - std::map>> - forgotten; - /// RFC 0013 heap outputs, §6.3: the contents of a new object the - /// function left in an entry cell, below that cell's dereference, by - /// the cell (a caller reads them through the new value it stores). - std::map contents; - std::map contentOf; - /// RFC 0012 *String facts* of the objects the summary names, by path - /// and contents prefix (`StringEffect`). - std::map>, - std::pair>> - strings; - /// Cells that hold their entry value or the stored one (a join of a - /// path that stored and one that did not): a possible store. - std::set keepsEntry; - /// Stores made on exactly the paths of an entry test (the cell's - /// `storedIff`). - std::map storeGuards; - std::optional result; - std::map resultStores; - /// The exit returns a record whose fields `resultStores` describe. - bool recordResult = false; - /// RFC 0015 §5: releases of the elements a range selects, and what a - /// range of elements (or some elements, without a range) now holds. - std::map elementReleases; - std::map elementStores; - }; - std::vector views; - auto pathOf = [&](core::ObjectId id) -> std::optional { - const core::ObjectInfo &info = objects.info(objects.liveVersion(id)); - if (info.key.kind == core::ObjectKind::Entry || - info.key.kind == core::ObjectKind::EntrySummary) - return info.key.path; - return std::nullopt; - }; - // Per exit: the index of each new object described so far. - std::map freshIndex; - auto describe = [&](const core::HeapState &state, - core::Sym sym) -> core::ValueDesc { - core::ValueDesc desc; - const core::SymInfo &value = heap.info(state, sym); - if (value.type == core::SymInfo::Type::Pointer) { - if (value.null == core::PointerNull::Null) { - desc.kind = core::ValueDesc::Kind::Null; - return desc; - } - desc.maybeNull = value.null == core::PointerNull::Maybe; - if (value.targets.size() == 1 && !value.top) { - const core::Target &target = value.targets[0]; - const core::ObjectInfo &info = objects.info(target.object); - const core::ObjectState *object = heap.findObject(state, target.object); - switch (info.key.kind) { - case core::ObjectKind::HeapRecent: - case core::ObjectKind::HeapOld: - // A pointer into it at a constant offset (a cursor after its - // base, RFC 0011), or somewhere inside it (past a header whose - // size the path chose, `(char *)block + header`). - if (object != nullptr && object->owned && - object->life == core::Life::Live) { - desc.kind = core::ValueDesc::Kind::Fresh; - if (!target.offset.isConstant()) - desc.interior = true; - else if (target.offset.constant != 0) - desc.offset = target.offset.constant; - desc.family = object->family; - desc.object = - freshIndex - .try_emplace(target.object, - static_cast(freshIndex.size())) - .first->second; - // (Bytes the callee wrote are no longer its zeros.) - desc.zeroed = object->zeroed && !object->forgetsAny(); - // The extent over the parameters: a constant, or a symbol a - // parameter held at entry. - std::optional folded; - if (object->extent && object->extent->bytes.known) { - const core::Term &bytes = object->extent->bytes; - if (bytes.isConstant()) - folded = bytes.constant; - else if (auto c = state.zone.constant(bytes.var)) - folded = (bytes.scale * *c) + bytes.constant; - } - if (folded) - desc.extent = core::PathTerm{ - .path = std::nullopt, .scale = 1, .constant = *folded}; - // (Read in this state: symbols are numbered per state, and an - // unchanged integer parameter still holds its entry value.) - else if (object->extent && object->extent->bytes.known) - desc.extent = parameterTerm(state, object->extent->bytes); - return desc; - } - break; - case core::ObjectKind::Entry: - if (target.offset.isConstant()) { - desc.kind = core::ValueDesc::Kind::Path; - // (Null too only where the function joined in a null of its - // own, `nullJoined`: the entry value's own nullness is the - // caller's.) - desc.maybeNull = desc.maybeNull && value.nullJoined; - // The object's path names the pointee; the value is the pointer - // that was stored above it. - core::SummaryPath path = info.key.path; - if (!path.steps.empty() && - path.steps.back().step == core::PathStep::Deref) - path.steps.popBack(); - desc.path = path; - desc.offset = target.offset.constant; - return desc; - } - break; - case core::ObjectKind::Literal: - case core::ObjectKind::Global: - desc.kind = core::ValueDesc::Kind::Static; - return desc; - default: - break; - } - } - // Several new objects of one family (the elements a loop filled): - // each element its own (RFC 0015 §5). - if (!value.top && value.targets.size() > 1) { - bool fresh = true; - std::string family; - for (const core::Target &target : value.targets) { - core::ObjectKind kind = objects.info(target.object).key.kind; - const core::ObjectState *object = - heap.findObject(state, target.object); - if ((kind != core::ObjectKind::HeapRecent && - kind != core::ObjectKind::HeapOld) || - target.offset != core::Term::of(0) || object == nullptr || - !object->owned || object->life != core::Life::Live || - (!family.empty() && object->family != family)) { - fresh = false; - break; - } - family = object->family; - } - if (fresh) { - desc.kind = core::ValueDesc::Kind::Fresh; - desc.family = family; - desc.many = true; - // (Into the objects at one constant offset, or at some offset.) - const core::Term &first = value.targets.front().offset; - bool same = first.isConstant(); - for (const core::Target &target : value.targets) - same = same && target.offset == first; - if (!same) - desc.interior = true; - else if (first.constant != 0) - desc.offset = first.constant; - desc.object = - freshIndex - .try_emplace(value.targets[0].object, - static_cast(freshIndex.size())) - .first->second; - return desc; - } - } - // §5.7: storage of this frame outlives nothing the caller holds. - bool frame = !value.targets.empty() && !value.top; - for (const core::Target &target : value.targets) { - const core::ObjectState *object = heap.findObject(state, target.object); - frame = frame && - (isFrameObject(state, target.object) || - (object != nullptr && (object->life == core::Life::Ended || - object->life == core::Life::MayEnded))); - } - desc.kind = frame ? core::ValueDesc::Kind::Dangling - : core::ValueDesc::Kind::Unknown; - desc.raw = !frame && value.raw; - desc.rawSome = desc.raw && value.rawSome; - return desc; - } - if (value.type == core::SymInfo::Type::Int) { - desc.kind = core::ValueDesc::Kind::Int; - desc.lo = state.zone.lower(sym); - desc.hi = state.zone.upper(sym); - // (Unsigned 64-bit values above `INT64_MAX`, which only the interval - // holds.) - if (value.values && !value.values->empty() && !value.values->isFull() && - !value.values->type.isSigned && value.values->type.width == 64 && - value.values->maximum()->bits > static_cast(INT64_MAX)) - desc.range = *value.values; - return desc; - } - // Functions the value is known to be one of, by portable name (§7 - // *Amendment (cross-unit contexts)*: a callback handed out). - if (value.type == core::SymInfo::Type::Function && value.functionsKnown && - (!value.functions.empty() || !value.foreignFunctions.empty())) { - desc.kind = core::ValueDesc::Kind::Function; - desc.functions = value.foreignFunctions; - for (core::Handle handle : value.functions) - if (const auto *fn = fromHandle(handle)) - desc.functions.push_back(unit.portableName(*fn)); - std::ranges::sort(desc.functions); - auto repeated = std::ranges::unique(desc.functions); - desc.functions.erase(repeated.begin(), repeated.end()); - return desc; - } - // A function pointer a parameter held at entry (a setter's `h->fn = - // fn`); any other function value is unknown to the caller, whose calls - // through the cell then resolve by the slot solution (RFC 0030 §9.3). - // (Read in this state, symbols being numbered per state: an unmodified - // parameter still holds its entry value.) - if (value.type == core::SymInfo::Type::Function) - for (unsigned i = 0; i < function.getNumParams(); ++i) { - if (!isUnmodifiedParam(i)) - continue; - auto held = heap.read(state, variableObject(*function.getParamDecl(i)), - core::CellKey{}); - if (held && *held == sym) { - desc.kind = core::ValueDesc::Kind::Path; - desc.path = core::SummaryPath::param(i); - return desc; - } - } - desc.kind = core::ValueDesc::Kind::Unknown; - return desc; - }; - // The value an entry cell `dest` holds at an exit, and the new object it - // points to when it is one. A join of the cell's entry value (a pointer - // to the start of the entry object below it) and another value is that - // value, stored on some paths only. - auto describeCell = [&](const core::HeapState &exit, core::Sym sym, - const core::SummaryPath &dest, ExitView &view, - core::ObjectId &fresh) -> core::ValueDesc { - const core::SymInfo &value = heap.info(exit, sym); - fresh = 0; - if (value.type != core::SymInfo::Type::Pointer || value.top || - value.targets.size() < 2) { - core::ValueDesc desc = describe(exit, sym); - if (desc.kind == core::ValueDesc::Kind::Fresh && - value.targets.size() == 1) - fresh = value.targets[0].object; - return desc; - } - core::SummaryPath below = dest.deref(); - core::SymInfo narrowed = value; - narrowed.targets.clear(); - bool keeps = false; - for (const core::Target &target : value.targets) { - const core::ObjectInfo &info = objects.info(target.object); - if (info.key.kind == core::ObjectKind::Entry && !info.key.dead && - info.key.path == below && target.offset == core::Term::of(0)) { - keeps = true; - continue; - } - narrowed.targets.push_back(target); - } - if (!keeps) - return describe(exit, sym); - core::HeapState scratch = exit; - core::Sym other = heap.fresh(scratch, narrowed); - core::ValueDesc desc = describe(scratch, other); - if (desc.kind == core::ValueDesc::Kind::Unknown || - (desc.kind == core::ValueDesc::Kind::Path && desc.path && - *desc.path == dest)) - return describe(exit, sym); - if (desc.kind == core::ValueDesc::Kind::Fresh && - narrowed.targets.size() == 1) - fresh = narrowed.targets[0].object; - view.keepsEntry.insert(dest); - return desc; - }; - // A new object stored into an entry cell: its contents, kept apart from - // the stores into entry objects (which name the same paths below the - // cell's old value). - auto describeStored = [&](const core::HeapState &exit, ExitView &view, - const core::SummaryPath &dest, - core::ObjectId object) { - std::map contents; - describeContents(exit, object, dest.deref(), contents, 0, describe); - for (auto &[path, desc] : contents) { - view.contents[path] = desc; - view.contentOf[path] = dest; - } - }; - const char *dumpLevel = std::getenv("WEAVEC_ENGINE_DUMP"); - bool dumpExits = dumpLevel != nullptr && std::string_view(dumpLevel) == "2"; - for (const core::HeapState &exit : split) { - freshIndex.clear(); - if (dumpExits) - llvm::errs() << "exit of " << function.getNameAsString() << " result s" - << exit.result << "\n" - << heap.dump(exit); - ExitView view; - view.classes = classesOf(heap, exit, returnType); - if (exit.result != core::ZeroSym) - view.result = describe(exit, exit.result); - // Stores into globals (RFC 0005 `global(g)`): what the function left in - // a global's cells. - for (const auto &[id, object] : exit.objects) { - const core::ObjectInfo &info = objects.info(id); - if (info.key.kind != core::ObjectKind::Global) - continue; - const auto *var = fromHandle(info.key.handle); - if (var == nullptr) - continue; - core::SummaryPath root = core::SummaryPath::global(unit.globalId(*var)); - for (const auto &[key, sym] : object.cells) { - const core::SymInfo &value = heap.info(exit, sym); - if (!key.isConcrete()) - continue; - // A cell still holding its entry value is no store; nor is one of - // an object no store reached (only read). - if (!object.stored || value.entryOf == std::make_pair(id, key)) - continue; - core::SummaryPath dest = - cellPath(context, var->getType(), root, key.offset, false); - core::ObjectId fresh = 0; - core::ValueDesc desc = describeCell(exit, sym, dest, view, fresh); - // (Unless null was joined in: `if (p) *p = NULL`.) - if (desc.kind == core::ValueDesc::Kind::Path && desc.path && - *desc.path == dest && !desc.maybeNull) - continue; - view.stores[dest] = desc; - for (const auto &[at, test] : object.storedIff) - if (at == key) - view.storeGuards[dest] = test; - if (fresh != 0 && desc.kind == core::ValueDesc::Kind::Fresh && - !desc.many && !desc.offset && !desc.interior) - describeStored(exit, view, dest, fresh); - } - // Its elements (RFC 0015 §5). - describeElements(exit, id, object, root, describe, view.elementReleases, - view.elementStores); - } - for (const auto &[id, object] : exit.objects) { - auto path = pathOf(id); - if (!path) - continue; - const core::ObjectInfo &info = objects.info(id); - bool released = - object.life == core::Life::Released || object.effectReleased; - bool mayReleased = - object.life == core::Life::MayReleased || object.effectMayReleased; - bool unknown = object.life == core::Life::UnknownReleased; - std::string family = object.record ? object.record->family : ""; - if (unknown) { - view.byPath[*path] = ExitEffect{ - .kind = core::PathEffect::Kind::Unknown, .may = true, .family = ""}; - } else if (released || mayReleased) { - bool moved = object.record && object.record->reason == - core::ReleaseRecord::Reason::Moved; - view.byPath[*path] = ExitEffect{ - .kind = moved ? core::PathEffect::Kind::Move - : core::PathEffect::Kind::Release, - .may = !released || !info.singular || - (object.record && object.record->conditional), - .family = family, - .guard = object.record - ? object.record->paramGuard - : std::vector>{}, - .offset = object.releaseOffset, - .pairs = object.record ? object.record->pairGuard - : std::vector{}}; - } else if (object.escaped) { - view.byPath[*path] = ExitEffect{ - .kind = core::PathEffect::Kind::Escape, .may = false, .family = ""}; - } - // Bytes written without cells to show for them (a `memcpy` into the - // object, a store at an unknown offset): an unknown value stored at - // the object's own path, which the caller takes as every byte of the - // object rewritten (instantiate()). - // (A dead copy an unknown callee may have released, §4.6, still names - // the entry object when no live object does: its stores stand.) - bool dead = info.key.dead; - if (dead && object.life == core::Life::UnknownReleased && - !object.effectReleased) { - core::ObjectKey live = info.key; - live.dead = false; - std::optional twin = objects.lookup(live); - dead = twin && exit.objects.contains(*twin); - } - bool bytesWritten = false; - if (info.key.kind == core::ObjectKind::Entry && !dead && - object.havocked && !info.key.path.steps.empty() && - info.key.path.steps.back().step == core::PathStep::Deref) { - view.stores[info.key.path] = core::ValueDesc{}; - bytesWritten = true; - } - // An entry object the function zero-filled (`memset(p, 0, sizeof *p)`): - // each scalar member it wrote no other value into holds zero. - if (info.key.kind == core::ObjectKind::Entry && !dead && object.zeroed && - !object.havocked && info.type != 0) { - std::set seen; - forEachScalar( - context, typeOfHandle(info.type), 0, seen, - [&](std::int64_t offset, QualType type) { - if (object.cells.contains(core::CellKey{.offset = offset})) - return; - core::ValueDesc zero; - if (type->isPointerType()) { - zero.kind = core::ValueDesc::Kind::Null; - } else { - zero.kind = core::ValueDesc::Kind::Int; - zero.lo = 0; - zero.hi = 0; - } - view.stores[cellPath(context, typeOfHandle(info.type), - info.key.path, offset, false)] = zero; - }); - } - if (!bytesWritten && info.key.kind == core::ObjectKind::Entry && !dead && - (!object.forgotten.empty() || !object.mayForgotten.empty()) && - !info.key.path.steps.empty() && - info.key.path.steps.back().step == core::PathStep::Deref) { - auto &ranges = view.forgotten[info.key.path]; - for (const auto &[from, to] : object.forgotten) - ranges.emplace(from, to, false); - for (const auto &[from, to] : object.mayForgotten) - ranges.emplace(from, to, true); - } - // Stores into the entry object's cells. - if (info.key.kind == core::ObjectKind::Entry && !dead) - for (const auto &[key, sym] : object.cells) { - const core::SymInfo &value = heap.info(exit, sym); - // (A function pointer the callee stored is a store too: the - // caller's indirect calls through the cell must see it. So is a - // value of no known type, read through a `void *` (a field read - // by an accessor that takes `void *`): the caller forgets what the - // cell held.) - if (!key.isConcrete()) - continue; - // The callee rewrote the object's bytes: an unknown store at its - // own path says so (below), not its cells. - if (bytesWritten && key.offset == 0) - continue; - // A cell still holding its entry value is no store; nor is one - // of an object no store reached (only read). - if (!object.stored || value.entryOf == std::make_pair(id, key)) - continue; - QualType type = info.type != 0 ? typeOfHandle(info.type) : QualType(); - core::SummaryPath dest = - cellPath(context, type, info.key.path, key.offset, false); - core::ObjectId fresh = 0; - core::ValueDesc desc = describeCell(exit, sym, dest, view, fresh); - // A cell still holding what it held at entry is no store (one - // that may hold a null stored instead is). - if (desc.kind == core::ValueDesc::Kind::Path && desc.path && - *desc.path == dest && !desc.maybeNull) - continue; - // An unknown value in a scalar object's only cell: spelled as its - // cell (`*p.#0`), which an unknown store at the object's own path - // (every byte rewritten, above) is not. - if (desc.kind == core::ValueDesc::Kind::Unknown && - dest == info.key.path) - dest = dest.field("#0"); - view.stores[dest] = desc; - for (const auto &[at, test] : object.storedIff) - if (at == key) - view.storeGuards[dest] = test; - if (fresh != 0 && desc.kind == core::ValueDesc::Kind::Fresh && - !desc.many && !desc.offset && !desc.interior) - describeStored(exit, view, dest, fresh); - } - if (info.key.kind == core::ObjectKind::Entry && !info.key.dead) - describeElements(exit, id, object, info.key.path, describe, - view.elementReleases, view.elementStores); - } - // A record returned by value: its fields, `result.f` (RFC 0013), when - // it is storage of this frame whose every cell the exit knows. - if (returnType->isRecordType()) - if (const core::Sym *storage = exit.exprs.find(handleOf(&function))) { - const core::SymInfo &pointer = heap.info(exit, *storage); - if (pointer.type == core::SymInfo::Type::Pointer && !pointer.top && - pointer.targets.size() == 1 && - pointer.targets[0].offset == core::Term::of(0)) { - core::ObjectId record = pointer.targets[0].object; - const core::ObjectState *object = heap.findObject(exit, record); - const VarDecl *var = localVariable(record); - if (objects.info(record).key.kind == core::ObjectKind::Local && - object != nullptr && !object->forgetsAny() && !object->zeroed && - object->segments.empty() && - (var == nullptr || !isa(var))) { - view.recordResult = true; - describeContents(exit, record, core::SummaryPath::result(), - view.resultStores, 0, describe); - } - } - } - if (exit.result != core::ZeroSym) { - // A fresh result's contents: `result*.f` (RFC 0013's heap outputs). - // Those of one the result points into are not described, so its - // cells read as unknown in the caller, not as zeros (a string - // header before where it points). - if (view.result->kind == core::ValueDesc::Kind::Fresh && - !view.result->many && !view.result->offset && - !view.result->interior) { - const core::SymInfo &value = heap.info(exit, exit.result); - describeContents(exit, value.targets[0].object, - core::SummaryPath::result().deref(), view.resultStores, - 0, describe); - } else if (view.result->kind == core::ValueDesc::Kind::Fresh) { - view.result->zeroed = false; - } - } - // RFC 0012 *String facts* (§6.1 `string`): of the new objects the exit - // describes and of the entry objects, relative to where their pointer - // points. - std::map byIndex; - for (const auto &[id, index] : freshIndex) - byIndex.emplace(index, id); - auto noteString = [&](core::ObjectId id, const core::SummaryPath &path, - std::optional contents, - std::int64_t pointerOffset) { - const core::ObjectState *object = heap.findObject(exit, id); - if (object == nullptr || !object->nulWithin) - return; - auto within = - parameterTerm(exit, object->nulWithin->plusConstant(-pointerOffset)); - if (!within) - return; - std::optional from; - if (object->nulFrom) - from = - parameterTerm(exit, object->nulFrom->plusConstant(-pointerOffset)); - view.strings[{path, contents}] = {*within, from}; - }; - auto freshString = [&](const core::ValueDesc &desc, - const core::SummaryPath &path, - std::optional contents) { - if (desc.kind != core::ValueDesc::Kind::Fresh || desc.many) - return; - if (auto it = byIndex.find(desc.object); it != byIndex.end()) - noteString(it->second, path, contents, desc.offset.value_or(0)); - }; - if (view.result) - freshString(*view.result, core::SummaryPath::result().deref(), - std::nullopt); - for (const auto &[dest, desc] : view.resultStores) - freshString(desc, dest.deref(), std::nullopt); - for (const auto &[dest, desc] : view.stores) - freshString(desc, dest.deref(), - static_cast(dest.steps.size())); - for (const auto &[dest, desc] : view.contents) - freshString( - desc, dest.deref(), - static_cast(view.contentOf.at(dest).steps.size())); - for (const auto &[id, object] : exit.objects) { - const core::ObjectInfo &info = objects.info(id); - if (info.key.kind == core::ObjectKind::Entry && !info.key.dead && - info.singular) - noteString(id, info.key.path, std::nullopt, 0); - } - views.push_back(std::move(view)); - } - // Join the exits, per path and kind of effect: one on every exit is - // `always`; one on some is keyed by the result classes of the exits it is - // on and, where those do not separate it from the others, by a parameter - // test; otherwise it is a possible effect. - std::set> keys; - for (const ExitView &view : views) - for (const auto &[path, effect] : view.byPath) - keys.emplace(path, effect.kind); - // RFC 0030 §9.1: an unmodified integer parameter whose zero test holds on - // every exit in `present` and fails on every other exit in `relevant`. - auto paramCase = [&](const std::vector &present, - const std::vector &relevant) - -> std::optional> { - for (unsigned i = 0; i < function.getNumParams(); ++i) { - if (!unmodifiedParams.contains(i) || - (!function.getParamDecl(i)->getType()->isIntegerType() && - !function.getParamDecl(i)->getType()->isPointerType())) - continue; - core::ObjectId holder = variableObject(*function.getParamDecl(i)); - bool nonZeroWhere = true; - bool zeroWhere = true; - for (std::size_t e = 0; e < split.size(); ++e) { - if (!present[e] && !relevant[e]) - continue; - auto held = heap.read(split[e], holder, core::CellKey{}); - if (!held) { - nonZeroWhere = zeroWhere = false; - break; - } - std::optional zero = isZeroValue(heap, split[e], *held); - bool isZero = zero == true; - bool nonZero = zero == false; - if (present[e]) { - nonZeroWhere = nonZeroWhere && nonZero; - zeroWhere = zeroWhere && isZero; - } else { - nonZeroWhere = nonZeroWhere && isZero; - zeroWhere = zeroWhere && nonZero; - } - } - if (nonZeroWhere) - return std::make_pair(i, false); - if (zeroWhere) - return std::make_pair(i, true); - } - return std::nullopt; - }; - // RFC 0014: a comparison of two unmodified pointer parameters that holds - // on every exit in `present` and fails on every other exit in `relevant`. - auto pairCase = [&](const std::vector &present, - const std::vector &relevant) - -> std::optional { - std::vector> pointers; - for (unsigned i = 0; i < function.getNumParams(); ++i) - if (unmodifiedParams.contains(i) && - function.getParamDecl(i)->getType()->isPointerType()) - pointers.emplace_back(i, variableObject(*function.getParamDecl(i))); - for (std::size_t a = 0; a < pointers.size(); ++a) - for (std::size_t b = a + 1; b < pointers.size(); ++b) - for (bool equal : {true, false}) { - bool holds = true; - for (std::size_t e = 0; e < split.size() && holds; ++e) { - if (!present[e] && !relevant[e]) - continue; - auto x = heap.read(split[e], pointers[a].second, core::CellKey{}); - auto y = heap.read(split[e], pointers[b].second, core::CellKey{}); - std::optional same = - x && y ? core::Heap::pointersEqual(split[e], *x, *y) - : std::nullopt; - holds = same && *same == (present[e] ? equal : !equal); - } - if (holds) - return core::ParamPairTest{.first = pointers[a].first, - .second = pointers[b].first, - .equal = equal}; - } - return std::nullopt; - }; - // The path over the interface of the cell an entry test is of. - auto entryCellPath = - [&](const core::EntryTest &test) -> std::optional { - if (!test.key.isConcrete()) - return std::nullopt; - const core::ObjectInfo &info = objects.info(test.object); - QualType type = info.type != 0 ? typeOfHandle(info.type) : QualType(); - if (info.key.kind == core::ObjectKind::Entry && !info.key.dead) - return cellPath(context, type, info.key.path, test.key.offset, false); - if (info.key.kind == core::ObjectKind::Global) - if (const auto *var = fromHandle(info.key.handle)) - return cellPath(context, var->getType(), - core::SummaryPath::global(unit.globalId(*var)), - test.key.offset, false); - return std::nullopt; - }; - // A zero test of a value a cell held at entry that holds on every exit in - // `present` and fails on every other exit in `relevant`: a lazy - // initialisation (`if (!g) g = malloc(…)`), keyed by the entry path. - auto entryCase = [&](const std::vector &present, - const std::vector &relevant) - -> std::optional> { - std::optional first; - for (std::size_t e = 0; e < split.size() && !first; ++e) - if (present[e]) - first = e; - if (!first) - return std::nullopt; - for (const core::EntryTest &candidate : split[*first].entryTests) { - bool holds = true; - for (std::size_t e = 0; e < split.size() && holds; ++e) { - if (!present[e] && !relevant[e]) - continue; - const auto &tests = split[e].entryTests; - auto it = std::ranges::find_if(tests, [&](const core::EntryTest &test) { - return test.object == candidate.object && test.key == candidate.key; - }); - holds = it != tests.end() && - it->zero == (present[e] ? candidate.zero : !candidate.zero); - } - if (!holds) - continue; - if (auto path = entryCellPath(candidate)) - return std::make_pair(*path, candidate.zero); - } - return std::nullopt; - }; - std::set allClasses; - for (const ExitView &view : views) - allClasses.insert(view.classes.begin(), view.classes.end()); - for (const auto &[path, kind] : keys) { - std::set with; - std::vector present; - bool onAll = true; - bool anyMay = false; - // (Exits that released the value at different offsets: at one the - // caller cannot tell.) - bool anyOffset = false; - bool mixedFamily = false; - const ExitEffect *first = nullptr; - // RFC 0014: the pointer parameter comparisons every such exit's - // release was made under. - std::vector pairs; - for (const ExitView &view : views) { - auto it = view.byPath.find(path); - bool here = it != view.byPath.end() && it->second.kind == kind; - present.push_back(here); - if (!here) { - onAll = false; - continue; - } - if (first == nullptr) - pairs = it->second.pairs; - else - std::erase_if(pairs, [&](const core::ParamPairTest &test) { - return std::find(it->second.pairs.begin(), it->second.pairs.end(), - test) == it->second.pairs.end(); - }); - if (first == nullptr) - first = &it->second; - // RFC 0034 §6.3: a family two exits disagree on is unknown. - if (first->family != it->second.family) - mixedFamily = true; - anyOffset = - anyOffset || !it->second.offset || it->second.offset != first->offset; - anyMay = anyMay || it->second.may; - with.insert(view.classes.begin(), view.classes.end()); - } - core::PathEffect effect; - effect.kind = kind; - effect.path = path; - effect.family = mixedFamily ? std::string() : first->family; - effect.anyOffset = anyOffset; - effect.offset = anyOffset ? 0 : *first->offset; - effect.may = anyMay; - // Made only where the comparison held: an activation where it fails - // makes none. - if (!pairs.empty()) - effect.when.paramsEqual = pairs.front(); - if (onAll) { - // A possible release made under a parameter test is keyed by it - // (lossy: other conditions may have been dropped). - if (anyMay && !first->guard.empty()) { - effect.when.paramZero = first->guard.front(); - effect.lossy = true; - } - effects.effects.push_back(effect); - continue; - } - // The exits without the effect whose classes the classes of `with` do - // not rule out. - std::vector relevant; - bool separated = !with.empty(); - for (std::size_t e = 0; e < views.size(); ++e) { - bool overlaps = false; - if (!present[e]) - for (core::ResultClass c : views[e].classes) - overlaps = overlaps || with.contains(c); - relevant.push_back(overlaps); - separated = separated && !overlaps; - } - // Without result classes (a `void` function) no class tells the exits - // apart: a parameter test must, against every exit without the effect. - if (with.empty()) - for (std::size_t e = 0; e < views.size(); ++e) - relevant[e] = !present[e]; - bool narrowing = with.size() < allClasses.size(); - if (separated) { - effect.when.classes.assign(with.begin(), with.end()); - } else if (auto keyed = paramCase(present, relevant)) { - effect.when.paramZero = keyed; - if (narrowing) - effect.when.classes.assign(with.begin(), with.end()); - } else if (auto pair = pairCase(present, relevant)) { - effect.when.paramsEqual = pair; - if (narrowing) - effect.when.classes.assign(with.begin(), with.end()); - } else if (auto entry = entryCase(present, relevant)) { - effect.when.entryZero = std::move(entry); - if (narrowing) - effect.when.classes.assign(with.begin(), with.end()); - } else { - // Possible, and only on the classes of the exits that have it. - effect.may = true; - if (narrowing) - effect.when.classes.assign(with.begin(), with.end()); - } - effects.effects.push_back(effect); - } - // Two exits' values of one cell: the same value, or a new object on some - // exits and null on the others (the cell holds either). - auto joinValue = [](std::optional &into, - const core::ValueDesc &desc, bool &same) { - if (!into) { - into = desc; - return; - } - core::ValueDesc a = *into; - core::ValueDesc b = desc; - bool maybeNull = a.maybeNull || b.maybeNull; - a.maybeNull = b.maybeNull = false; - if (a == b) { - into->maybeNull = maybeNull; - return; - } - if (a.kind == core::ValueDesc::Kind::Null && - b.kind == core::ValueDesc::Kind::Fresh) { - into = desc; - into->maybeNull = true; - return; - } - if (a.kind == core::ValueDesc::Kind::Fresh && - b.kind == core::ValueDesc::Kind::Null) { - into->maybeNull = true; - return; - } - // Integers: the hull of the two. - if (a.kind == core::ValueDesc::Kind::Int && - b.kind == core::ValueDesc::Kind::Int && !a.range && !b.range && - !a.path && !b.path) { - into->lo = - a.lo && b.lo ? std::optional(std::min(*a.lo, *b.lo)) : std::nullopt; - into->hi = - a.hi && b.hi ? std::optional(std::max(*a.hi, *b.hi)) : std::nullopt; - into->maybeNull = maybeNull; - return; - } - same = false; - }; - // Stores: on every exit, keyed by the result classes of the exits they - // are on when those separate them from the others, or possible. - std::set stored; - for (const ExitView &view : views) - for (const auto &[path, desc] : view.stores) - stored.insert(path); - for (const core::SummaryPath &path : stored) { - bool onAll = true; - std::optional value; - bool same = true; - std::set with; - for (const ExitView &view : views) { - auto it = view.stores.find(path); - if (it == view.stores.end() || view.keepsEntry.contains(path)) - onAll = false; - if (it == view.stores.end()) - continue; - with.insert(view.classes.begin(), view.classes.end()); - joinValue(value, it->second, same); - } - bool separated = !onAll && !with.empty(); - std::vector present; - std::vector relevant; - // (A cell that may still hold its entry value is a possible store.) - bool keeps = false; - for (const ExitView &view : views) { - bool here = view.stores.contains(path); - present.push_back(here); - relevant.push_back(!here); - if (!here) - for (core::ResultClass c : view.classes) - separated = separated && !with.contains(c); - keeps = keeps || view.keepsEntry.contains(path); - } - separated = separated && !keeps; - core::StoreEffect store; - store.dest = path; - store.value = same && value ? *value : core::ValueDesc{}; - // A new object on some classes and null on the others (its allocation - // failed there): the object was never made on those (§6.3). - if (onAll && same && value && value->kind == core::ValueDesc::Kind::Fresh) { - std::set nullClasses; - std::set freshClasses; - for (const ExitView &view : views) { - const core::ValueDesc &each = view.stores.at(path); - (each.kind == core::ValueDesc::Kind::Null ? nullClasses : freshClasses) - .insert(view.classes.begin(), view.classes.end()); - } - bool disjoint = - std::ranges::none_of(nullClasses, [&](core::ResultClass c) { - return freshClasses.contains(c); - }); - if (!nullClasses.empty() && !freshClasses.empty() && disjoint) - store.absentOn.assign(nullClasses.begin(), nullClasses.end()); - } - // Made on exactly the paths of one entry test, on every exit: keyed by - // it (the exits without the store took the opposite test). - std::optional iff; - bool consistent = true; - for (std::size_t e = 0; e < views.size() && consistent; ++e) { - const ExitView &view = views[e]; - if (view.stores.contains(path)) { - auto guard = view.storeGuards.find(path); - consistent = - guard != view.storeGuards.end() && (!iff || *iff == guard->second); - if (consistent) - iff = guard->second; - } else if (iff) { - core::EntryTest opposite = *iff; - opposite.zero = !opposite.zero; - consistent = std::binary_search(split[e].entryTests.begin(), - split[e].entryTests.end(), opposite); - } - } - // (Exits before the first store's: checked against the guard found.) - for (std::size_t e = 0; e < views.size() && consistent && iff; ++e) - if (!views[e].stores.contains(path)) { - core::EntryTest opposite = *iff; - opposite.zero = !opposite.zero; - consistent = std::binary_search(split[e].entryTests.begin(), - split[e].entryTests.end(), opposite); - } - std::optional iffPath = - consistent && iff ? entryCellPath(*iff) : std::nullopt; - if (iffPath) - store.when.entryZero = std::make_pair(*iffPath, iff->zero); - else if (separated) - store.when.classes.assign(with.begin(), with.end()); - else if (!onAll && same && !keeps) - // RFC 0030 §9.1: a store an unmodified parameter's test selects, or - // the test of a value at entry. - if (auto keyed = paramCase(present, relevant)) - store.when.paramZero = keyed; - else if (auto entry = entryCase(present, relevant)) - store.when.entryZero = std::move(entry); - else - store.may = true; - else - store.may = !onAll; - effects.stores.push_back(store); - } - // Bytes rewritten with unknown values: every range some exit has, on all - // of them when each exit has it. - std::set rewritten; - for (const ExitView &view : views) - for (const auto &[path, ranges] : view.forgotten) - rewritten.insert(path); - for (const core::SummaryPath &path : rewritten) { - std::set> ranges; - for (const ExitView &view : views) - if (auto it = view.forgotten.find(path); it != view.forgotten.end()) - for (const auto &[from, to, weak] : it->second) - ranges.emplace(from, to); - for (const auto &range : ranges) { - core::StoreEffect store; - store.dest = path; - store.bytes = range; - // (Definite when every exit rewrote them on every path.) - store.may = !std::ranges::all_of(views, [&](const ExitView &view) { - auto it = view.forgotten.find(path); - return it != view.forgotten.end() && - it->second.contains( - std::make_tuple(range.first, range.second, false)); - }); - effects.stores.push_back(store); - } - } - // The contents of the new objects left in entry cells, over the exits - // that leave one there: the caller writes them into that object. - std::set> contentPaths; - for (const ExitView &view : views) - for (const auto &[path, container] : view.contentOf) - contentPaths.emplace(path, container); - for (const auto &[path, container] : contentPaths) { - bool onAll = true; - std::optional value; - bool same = true; - for (const ExitView &view : views) { - auto holder = view.stores.find(container); - if (holder == view.stores.end() || - holder->second.kind != core::ValueDesc::Kind::Fresh) - continue; - auto it = view.contents.find(path); - if (it == view.contents.end() || view.contentOf.at(path) != container) { - onAll = false; - continue; - } - joinValue(value, it->second, same); - } - core::StoreEffect store; - store.dest = path; - store.value = same && value ? *value : core::ValueDesc{}; - store.may = !onAll; - store.contents = static_cast(container.steps.size()); - effects.stores.push_back(store); - } - // String facts, on every exit where their object exists. - std::set>> - stringKeys; - for (const ExitView &view : views) - for (const auto &[key, fact] : view.strings) - stringKeys.insert(key); - for (const auto &key : stringKeys) { - const auto &[path, contents] = key; - auto relevant = [&](const ExitView &view) { - if (path.isResult()) - return view.result && view.result->kind == core::ValueDesc::Kind::Fresh; - if (contents) { - core::SummaryPath container = path; - container.steps.truncate(*contents); - auto it = view.stores.find(container); - return it != view.stores.end() && - it->second.kind == core::ValueDesc::Kind::Fresh; - } - return true; - }; - std::optional>> - fact; - bool holds = true; - for (const ExitView &view : views) { - if (!relevant(view)) - continue; - auto it = view.strings.find(key); - if (it == view.strings.end() || (fact && !(*fact == it->second))) { - holds = false; - break; - } - fact = it->second; - } - if (holds && fact) - effects.strings.push_back(core::StringEffect{.path = path, - .contents = contents, - .nulWithin = fact->first, - .nulFrom = fact->second}); - } - // Element ranges (RFC 0015 §5): on every exit, or possible. - std::set released; - std::set written; - for (const ExitView &view : views) { - for (const auto &[key, family] : view.elementReleases) - released.insert(key); - for (const auto &[key, desc] : view.elementStores) - written.insert(key); - } - for (const ElementKey &key : released) { - bool onAll = true; - std::string family; - for (const ExitView &view : views) { - auto it = view.elementReleases.find(key); - if (it == view.elementReleases.end()) - onAll = false; - else - family = it->second; - } - core::PathEffect effect; - effect.kind = core::PathEffect::Kind::Release; - effect.path = key.first; - effect.family = family; - effect.elements = key.second; - effect.may = !onAll || !key.second; - effects.effects.push_back(effect); - } - for (const ElementKey &key : written) { - bool onAll = true; - std::optional value; - bool same = true; - for (const ExitView &view : views) { - auto it = view.elementStores.find(key); - if (it == view.elementStores.end()) { - onAll = false; - continue; - } - if (!value) - value = it->second; - else if (!(*value == it->second)) - same = false; - } - core::StoreEffect store; - store.dest = key.first; - store.value = same && value ? *value : core::ValueDesc{}; - store.may = !onAll || !key.second; - store.elements = key.second; - effects.stores.push_back(store); - } - // A fresh result's contents, over the exits that return it. - std::set resultPaths; - std::size_t freshExits = 0; - // A record result is described on every exit or not at all (a caller - // reads the fields no store names as never written). - bool records = returnType->isRecordType() && - std::ranges::all_of(views, [](const ExitView &view) { - return view.recordResult; - }); - auto describesResult = [&](const ExitView &view) { - return (records && view.recordResult) || - (view.result && view.result->kind == core::ValueDesc::Kind::Fresh); - }; - for (const ExitView &view : views) - if (describesResult(view)) { - ++freshExits; - for (const auto &[path, desc] : view.resultStores) - resultPaths.insert(path); - } - for (const core::SummaryPath &path : resultPaths) { - std::size_t count = 0; - std::optional value; - bool same = true; - for (const ExitView &view : views) { - if (!describesResult(view)) - continue; - auto it = view.resultStores.find(path); - if (it == view.resultStores.end()) - continue; - ++count; - if (!value) - value = it->second; - else if (!(*value == it->second)) - same = false; - } - core::StoreEffect store; - store.dest = path; - store.value = same && value ? *value : core::ValueDesc{}; - store.may = count != freshExits; - effects.stores.push_back(store); - } - // Results: one alternative per distinct description, with a parameter - // test every exit returning it passes. - std::map> results; - for (const ExitView &view : views) - if (view.result) - results[*view.result].insert(view.classes.begin(), view.classes.end()); - const std::vector none(views.size(), false); - for (const auto &[desc, classes] : results) { - std::vector present; - bool onAll = true; - for (const ExitView &view : views) { - present.push_back(view.result && *view.result == desc); - onAll = onAll && present.back(); - } - effects.results.push_back(core::ResultEffect{ - .value = desc, - .classes = {classes.begin(), classes.end()}, - .paramZero = onAll ? std::nullopt : paramCase(present, none)}); - } - // RFC 0030 §9.2: a class on which a pointer parameter is always non-null. - for (unsigned i = 0; i < function.getNumParams(); ++i) { - if (!function.getParamDecl(i)->getType()->isPointerType()) - continue; - core::ObjectId holder = variableObject(*function.getParamDecl(i)); - std::set maybeNull; - std::set all; - for (std::size_t e = 0; e < split.size(); ++e) { - const core::HeapState &exit = split[e]; - all.insert(views[e].classes.begin(), views[e].classes.end()); - auto held = heap.read(exit, holder, core::CellKey{}); - // The parameter's entry value, if the variable still holds it. - if (!held || heap.info(exit, *held).null != core::PointerNull::NonNull) - maybeNull.insert(views[e].classes.begin(), views[e].classes.end()); - } - for (core::ResultClass c : all) - if (!maybeNull.contains(c)) - effects.nonNullOn[c].push_back(core::SummaryPath::param(i)); - } - // §6.6: the entry objects the function read cells of or stored into (a - // release alone, or handing the pointer on, is neither). - std::set read; - std::set wrote; - for (const core::HeapState &exit : split) - for (const auto &[id, object] : exit.objects) { - const core::ObjectInfo &info = objects.info(id); - if (info.key.kind != core::ObjectKind::Entry && - info.key.kind != core::ObjectKind::EntrySummary) - continue; - // (A cell still holding its entry value was loaded; a stored one - // counts as written.) - bool loaded = !object.segments.empty(); - for (const auto &[key, sym] : object.cells) - if (const core::SymInfo *value = exit.syms.find(sym); - value != nullptr && value->entryOf == std::make_pair(id, key)) - loaded = true; - if (loaded) - read.insert(info.key.path); - if (object.stored || object.forgetsAny()) - wrote.insert(info.key.path); - } - effects.reads.assign(read.begin(), read.end()); - effects.writes.assign(wrote.begin(), wrote.end()); - effects.unknownGlobals = ranUnknownCode; - return effects; -} - -//===----------------------------------------------------------------------===// -// Instantiation (§6.3) -//===----------------------------------------------------------------------===// - -/// The byte offset of the field named `name` in `type`, or none. -static std::optional -fieldOffset(const ASTContext &context, QualType type, llvm::StringRef name) { - if (type.isNull()) - return std::nullopt; - if (name.starts_with("#")) { - std::int64_t value = 0; - if (name.drop_front().getAsInteger(10, value)) - return std::nullopt; - return value; - } - const RecordDecl *record = type->getAsRecordDecl(); - if (record == nullptr || !record->isCompleteDefinition()) - return std::nullopt; - const ASTRecordLayout &layout = context.getASTRecordLayout(record); - for (const FieldDecl *field : record->fields()) - if (field->getName() == name) - return static_cast( - layout.getFieldOffset(field->getFieldIndex()) / - context.getCharWidth()); - return std::nullopt; -} - -/// The type of the member a field step names: the record's field by name -/// or, for `#`, the field starting there; a step by offset into a -/// non-record keeps the element type. -static QualType fieldStepType(const ASTContext &context, QualType type, - llvm::StringRef name, std::int64_t offset) { - const RecordDecl *record = type.isNull() ? nullptr : type->getAsRecordDecl(); - if (record == nullptr || !record->isCompleteDefinition()) { - if (!name.starts_with("#") || type.isNull()) - return {}; - // A byte offset into an array: its element's scalar there. Inside a - // scalar (the high word of a `double` a union also holds as two - // integers), what the callee wrote there has no type here; past its end - // it is another element of the same type. - if (context.getAsArrayType(type) == nullptr && !type->isIncompleteType() && - offset > 0 && - offset < static_cast( - context.getTypeSizeInChars(type).getQuantity())) - return {}; - QualType leaf = type; - while (const auto *array = context.getAsArrayType(leaf)) - leaf = array->getElementType(); - if (leaf->isRecordType()) - return {}; - // RFC 0033 §1: an offset that is no element's start is a byte there - // (`memset(p, 0, 16)` over `int *p` stores bytes `#0`..`#15`); taking - // it for a whole element would reach past the bytes the callee wrote. - if (!leaf->isIncompleteType()) { - const std::int64_t width = context.getTypeSizeInChars(leaf).getQuantity(); - if (width > 1 && offset % width != 0) - return context.UnsignedCharTy; - } - return leaf; - } - const ASTRecordLayout &layout = context.getASTRecordLayout(record); - for (const FieldDecl *field : record->fields()) { - if (name.starts_with("#") - ? std::cmp_equal(layout.getFieldOffset(field->getFieldIndex()) / - context.getCharWidth(), - offset) - : field->getName() == name) - return field->getType(); - } - return {}; -} - -namespace { -/// Resolves a callee's paths in the caller. -class PathResolver { -public: - PathResolver(Transfer &transfer, const CallExpr &call, - const std::vector &args) - : transfer(transfer), heap(transfer.domain()), - state(transfer.heapState()), call(call), args(args), - context(transfer.functionRun().ast()) {} - - /// The pointer values at a path that ends in a cell (no trailing deref): - /// `param(0)` is the argument; `param(0)*.buf` is the value in that - /// field. None when the path leaves what the caller can follow. - std::optional valueAt(const core::SummaryPath &path, - QualType *type = nullptr); - /// The cells a path names (a path ending in a field). - std::optional
cellAt(const core::SummaryPath &path, QualType *type); - /// RFC 0015 §5: the elements a path's last index step selects, as the - /// cell of element 0 in each target (with the fields after the step), the - /// element size and the cell's type. - struct Elements { - std::vector targets; - std::int64_t stride = 0; - QualType cellType; - }; - std::optional elementsAt(const core::SummaryPath &path); - /// The value of "some element" at a path whose last index step has no - /// range: every element's value, weakly. - std::optional anyElementAt(const core::SummaryPath &path); - void setResult(core::Sym sym, QualType type) { - result = sym; - resultType = type; - } - /// A record result's storage in the caller (`result.f` names its cells). - void setResultStorage(Address storage) { resultStorage = std::move(storage); } - /// §6.3: the new object `store` puts in its cell. The contents stores - /// below that cell (and a new result's) are read through it, not through - /// the cell's old value. - void setStored(const core::StoreEffect &store, core::Sym sym) { - stored[{store.dest, store.contents.has_value()}] = sym; - } - /// `cellAt` and `elementsAt` for a store's destination: a contents store - /// (or a new result's) reads its dereferences from the new objects of - /// `setStored`, and fails where none was stored; any other path, and a - /// value, is read in the entry state. - std::optional
destination(const core::StoreEffect &store, - QualType *type) { - through = &store; - auto out = cellAt(store.dest, type); - through = nullptr; - return out; - } - std::optional destinationElements(const core::StoreEffect &store) { - through = &store; - auto out = elementsAt(store.dest); - through = nullptr; - return out; - } - -private: - /// The pointer a dereference of the cell `prefix` names reads. - std::optional loadThrough(const core::SummaryPath &prefix, - const Address &cell, QualType type) { - if (through != nullptr && (through->contents || through->dest.isResult())) { - std::size_t from = through->contents.value_or(0); - if (prefix.steps.size() >= from) { - // The cell holding the new object itself is a store of the entry - // heap; the cells below it are its contents. - bool contents = - through->contents && prefix.steps.size() > *through->contents; - auto it = stored.find({prefix, contents}); - if (it == stored.end()) - return std::nullopt; - return it->second; - } - } - return transfer.load(cell, type, nullptr); - } - std::map, core::Sym> stored; - const core::StoreEffect *through = nullptr; - std::optional
resultStorage; - core::Sym result = core::ZeroSym; - QualType resultType; - Transfer &transfer; - core::Heap &heap; - core::HeapState &state; - const CallExpr &call; - const std::vector &args; - ASTContext &context; -}; -} // namespace - -std::optional
PathResolver::cellAt(const core::SummaryPath &path, - QualType *typeOut) { - QualType type; - core::Sym value = core::ZeroSym; - if (path.root == core::SummaryRoot::Param) { - if (path.index >= args.size() || path.index >= call.getNumArgs()) - return std::nullopt; - value = args[path.index]; - type = call.getArg(path.index)->IgnoreParenImpCasts()->getType(); - // The path is the callee's: through its parameter's type when that is - // a typed pointer, and an array argument is a pointer to its elements. - if (const FunctionDecl *callee = call.getDirectCallee(); - callee != nullptr && path.index < callee->getNumParams()) { - QualType declared = callee->getParamDecl(path.index)->getType(); - if (declared->isPointerType() && - !declared->getPointeeType()->isVoidType()) - type = declared; - } - if (type->isArrayType()) - type = context.getArrayDecayedType(type); - } else if (path.root == core::SummaryRoot::Result && !resultStorage) { - if (result == core::ZeroSym) - return std::nullopt; - value = result; - type = resultType; - } else if (path.root == core::SummaryRoot::Result || - path.root == core::SummaryRoot::Global) { - // A global root names the variable's storage itself, and a record - // result the caller's temporary (RFC 0013 `result.f`). - Address address; - if (path.root == core::SummaryRoot::Result) { - address = *resultStorage; - type = resultType; - } else { - const VarDecl *var = - transfer.functionRun().unitRun().globalDecl(path.index); - if (var == nullptr) - return std::nullopt; - address.targets = { - core::Target{.object = transfer.functionRun().variableObject(*var)}}; - heap.ensure(state, address.targets[0].object); - type = var->getType(); - } - Address current = address; - for (std::size_t i = 0; i < path.steps.size(); ++i) { - const core::PathElem &elem = path.steps[i]; - if (elem.step == core::PathStep::Deref) { - core::SummaryPath prefix = path; - prefix.steps.truncate(i); - std::optional read = loadThrough(prefix, current, type); - if (!read) - return std::nullopt; - core::Sym loaded = *read; - const core::SymInfo &info = heap.info(state, loaded); - if (info.type != core::SymInfo::Type::Pointer || info.top) - return std::nullopt; - current.targets = info.targets; - type = type->isPointerType() ? type->getPointeeType() : QualType(); - } else if (elem.step == core::PathStep::Field) { - auto offset = fieldOffset(context, type, elem.field); - if (!offset) - return std::nullopt; - for (core::Target &target : current.targets) - target.offset = target.offset.plusConstant(*offset); - type = fieldStepType(context, type, elem.field, *offset); - } else { - return std::nullopt; - } - } - if (typeOut != nullptr) - *typeOut = type; - return current; - } else { - return std::nullopt; - } - // A parameter root is a value; the first step must be a dereference. - Address current; - bool haveAddress = false; - for (std::size_t i = 0; i < path.steps.size(); ++i) { - const core::PathElem &elem = path.steps[i]; - if (elem.step == core::PathStep::Deref) { - if (haveAddress) { - core::SummaryPath prefix = path; - prefix.steps.truncate(i); - std::optional read = loadThrough(prefix, current, type); - if (!read) - return std::nullopt; - value = *read; - } - const core::SymInfo &info = heap.info(state, value); - if (info.type != core::SymInfo::Type::Pointer || info.top) - return std::nullopt; - current.targets = info.targets; - current.top = false; - haveAddress = true; - if (const auto *array = - !type.isNull() ? context.getAsArrayType(type) : nullptr) - type = array->getElementType(); // an array argument decays - else - type = type->isPointerType() ? type->getPointeeType() : QualType(); - } else if (elem.step == core::PathStep::Field) { - if (!haveAddress) - return std::nullopt; - auto offset = fieldOffset(context, type, elem.field); - if (!offset) - return std::nullopt; - for (core::Target &target : current.targets) - target.offset = target.offset.plusConstant(*offset); - type = fieldStepType(context, type, elem.field, *offset); - } else { - return std::nullopt; - } - } - if (!haveAddress) - return std::nullopt; - if (typeOut != nullptr) - *typeOut = type; - return current; -} - -std::optional -PathResolver::elementsAt(const core::SummaryPath &path) { - std::size_t step = path.steps.size(); - for (std::size_t i = 0; i < path.steps.size(); ++i) - if (path.steps[i].step == core::PathStep::Index) - step = i; - if (step == path.steps.size()) - return std::nullopt; - core::SummaryPath prefix = path; - prefix.steps.truncate(step); - QualType type; - auto base = cellAt(prefix, &type); - if (!base || base->top || type.isNull()) - return std::nullopt; - QualType element = type; - if (const auto *array = context.getAsArrayType(type)) - element = array->getElementType(); - auto size = transfer.sizeOf(element); - if (!size || *size <= 0) - return std::nullopt; - std::int64_t within = 0; - QualType cellType = element; - for (std::size_t i = step + 1; i < path.steps.size(); ++i) { - const core::PathElem &elem = path.steps[i]; - if (elem.step != core::PathStep::Field) - return std::nullopt; - auto offset = fieldOffset(context, cellType, elem.field); - if (!offset) - return std::nullopt; - within += *offset; - QualType fieldType; - if (const RecordDecl *record = cellType->getAsRecordDecl()) - for (const FieldDecl *field : record->fields()) - if (field->getName() == elem.field) - fieldType = field->getType(); - cellType = fieldType; - if (cellType.isNull()) - return std::nullopt; - } - Elements out; - out.stride = *size; - out.cellType = cellType; - for (core::Target target : base->targets) { - target.offset = target.offset.plusConstant(within); - out.targets.push_back(target); - } - return out; -} - -/// The element position and the index of element 0's cell at byte `offset` -/// among elements of `stride` bytes. -static std::optional> -elementBase(const core::Term &offset, std::int64_t stride) { - if (!offset.known || stride <= 0 || std::cmp_greater(stride, 1U << 20U)) - return std::nullopt; - std::int64_t position = ((offset.constant % stride) + stride) % stride; - core::CellKey key{.offset = position, - .stride = static_cast(stride), - .index = core::ZeroSym}; - std::int64_t first = (offset.constant - position) / stride; - if (offset.isConstant()) - return std::make_pair(key, core::Term::of(first)); - if (offset.scale % stride != 0) - return std::nullopt; - return std::make_pair( - key, core::Term::ofSym(offset.var, offset.scale / stride, first)); -} - -std::optional -PathResolver::anyElementAt(const core::SummaryPath &path) { - auto elements = elementsAt(path); - if (!elements || elements->cellType.isNull()) - return std::nullopt; - core::SymInfo hint; - if (elements->cellType->isPointerType()) - hint.type = core::SymInfo::Type::Pointer; - else if (transfer.integerType(elements->cellType)) - hint.type = core::SymInfo::Type::Int; - else - hint.type = core::SymInfo::Type::Unknown; - hint.ctype = typeHandle(elements->cellType); - core::Sym value = core::ZeroSym; - for (const core::Target &target : elements->targets) { - auto base = elementBase(target.offset, elements->stride); - core::Sym some = base ? heap.load(state, target.object, base->first, hint) - : transfer.unknownValue(elements->cellType); - value = value == core::ZeroSym ? some : heap.mergeWeak(state, value, some); - } - if (value == core::ZeroSym) - return std::nullopt; - return value; -} - -std::optional PathResolver::valueAt(const core::SummaryPath &path, - QualType *typeOut) { - if (path.root == core::SummaryRoot::Param && path.steps.empty()) { - if (path.index >= args.size()) - return std::nullopt; - if (typeOut != nullptr && path.index < call.getNumArgs()) - *typeOut = call.getArg(path.index)->IgnoreParenImpCasts()->getType(); - return args[path.index]; - } - // A path through an index step: some element, weakly. - for (const core::PathElem &elem : path.steps) - if (elem.step == core::PathStep::Index) { - if (auto elements = elementsAt(path); elements && typeOut != nullptr) - *typeOut = elements->cellType; - return anyElementAt(path); - } - QualType type; - auto cell = cellAt(path, &type); - if (!cell) - return std::nullopt; - if (typeOut != nullptr) - *typeOut = type; - if (type.isNull()) - return std::nullopt; - return transfer.load(*cell, type, nullptr); -} - -/// An integer the callee left, as the caller's `value` of its own type: its -/// range when every value of it is one of the type's; otherwise the callee -/// wrote bytes of another type there (`memcpy` of a `U32` into a byte -/// buffer), which say nothing of the cell's value (RFC 0017). -static void boundWithinType(core::HeapState &state, core::Sym value, - const core::ValueDesc &desc) { - auto lower = state.zone.lower(value); - auto upper = state.zone.upper(value); - if ((desc.lo && lower && *desc.lo < *lower) || - (desc.hi && upper && *desc.hi > *upper) || - (desc.lo && upper && *desc.lo > *upper) || - (desc.hi && lower && *desc.hi < *lower)) - return; - state.zone.addRange(value, desc.lo, desc.hi); -} - -/// The bytes one element's cell of `type` spans: its size, or the whole -/// stride when that is not known. -static std::int64_t cellBytes(const ASTContext &context, QualType type, - std::int64_t stride) { - if (type.isNull() || type->isIncompleteType()) - return stride; - return std::min( - stride, static_cast( - context.getTypeSizeInChars(type).getQuantity())); -} - -void Transfer::storePastObject(const CallExpr &call, - const core::StoreEffect &store, - const core::Target &target, - const core::Term &pointer, __int128 start, - __int128 end) { - // (A range of unknown values says the callee may have written some of - // its elements; a known value, that it wrote every one: an element it - // skipped would hold its entry value too.) - const core::ValueDesc &value = store.value; - if (value.kind == core::ValueDesc::Kind::Unknown || - (value.kind == core::ValueDesc::Kind::Int && !value.range && - (!value.lo || !value.hi))) - return; - // (Once per call: a requirement RFC 0030 §7.5 checked there says it.) - if (start >= end || !pointer.isConstant() || !store.dest.isParam() || - store.dest.index >= call.getNumArgs() || - run.requirementViolated.contains(&call)) - return; - const core::ObjectState *object = heap.findObject(state, target.object); - if (object == nullptr || object->life != core::Life::Live || - !object->extent || object->extent->cls != core::ExtentClass::Exact || - !object->extent->bytes.isConstant()) - return; - const std::int64_t size = object->extent->bytes.constant; - if (start >= 0 && end <= size) - return; - auto bytes = [](__int128 count) { - return std::to_string(static_cast(count)) + - (count == 1 ? " byte" : " bytes"); - }; - std::string callee = "the callee"; - if (const FunctionDecl *direct = call.getDirectCallee()) - callee = "'" + direct->getNameAsString() + "'"; - // Measured from the object's start, as its extent is (RFC 0030 §7.5). - const Expr &passed = *call.getArg(store.dest.index); - const std::string argument = spell(pointedObject(passed)); - core::Diagnostic diagnostic; - diagnostic.id = core::diag::OutOfBounds; - diagnostic.severity = core::Severity::Error; - diagnostic.message = - start < 0 ? callee + " requires '" + argument + "' before its start" - : callee + " requires " + bytes(end) + " behind '" + argument + - "', which has " + bytes(size); - diagnostic.location = - toCoreLocation(context.getSourceManager(), passed.getBeginLoc()); - const core::ObjectInfo &info = run.table().info(target.object); - if ((info.key.kind == core::ObjectKind::HeapRecent || - info.key.kind == core::ObjectKind::HeapOld) && - info.created.isValid()) - diagnostic.addNote("'" + argument + "' is allocated here", info.created); - if (run.isPublishing()) - run.ledger().decideAs(call, core::SiteKind::Call, core::Boundary::Call, - core::Facet::Spatial, - core::FacetDecision::violation(diagnostic.message)); - run.requirementViolated.insert(&call); - run.report(std::move(diagnostic), core::Certainty::Definite, &call, - core::Facet::Spatial); -} - -void Transfer::boundByRange(core::Sym value, QualType type, - const core::ValueDesc &desc) { - auto integer = integerType(type); - if (!desc.range || !integer || integer->isBoolean) - return; - core::IntegerRange range = desc.range->converted(*integer); - core::SymInfo &info = heap.infoMut(state, value); - if (info.values && info.values->type == *integer) - range = range.intersect(*info.values); - if (!range.empty()) - info.values = range; -} - -core::Sym Transfer::functionValue(const core::ValueDesc &desc) { - // This unit's functions (defined or declared) by their declarations, - // another's by name. - core::SymInfo info; - info.type = core::SymInfo::Type::Function; - info.functionsKnown = true; - for (const std::string &name : desc.functions) { - if (const FunctionDecl *fn = run.unitRun().functionNamed(name)) - info.functions.push_back(handleOf(fn->getCanonicalDecl())); - else - info.foreignFunctions.push_back(name); - } - return heap.fresh(state, info); -} - -bool Transfer::pendingOnNullArgument(const std::vector &args, - core::Sym value, - const core::SummaryPath &path, - const core::SourceLocation &where) const { - // The callee reaches the value through the argument, so the argument is - // not null there: a release still pending on a null result of the call - // that made the argument (`moved = wrap(owned, 1)`, which frees `owned` - // only when it returns null) did not happen on this path. - if (!path.isParam() || path.steps.empty() || path.index >= args.size()) - return false; - const core::SymInfo &argument = heap.info(state, args[path.index]); - std::vector targets; - for (const core::Target &target : heap.info(state, value).targets) - targets.push_back(target.object); - for (const core::PendingCase &pending : argument.pending) { - if (pending.kind != core::PendingCase::Kind::Release || - pending.record.where != where || - std::ranges::find(pending.classes, "nonnull") != pending.classes.end()) - continue; - const core::SymInfo *subject = state.syms.find(pending.subject); - if (subject == nullptr) - continue; - for (const core::Target &target : subject->targets) - if (std::ranges::find(targets, target.object) != targets.end()) - return true; - } - return false; -} - -bool Transfer::lostView(const CallExpr &call, - const std::vector &args, - const core::SummaryPath &valuePath) { - if (!valuePath.isParam() || valuePath.steps.empty() || - valuePath.index >= args.size()) - return false; - const core::SymInfo &root = heap.info(state, args[valuePath.index]); - if (root.type != core::SymInfo::Type::Pointer || root.top || - root.targets.empty() || root.null == core::PointerNull::Null) - return false; - // The unknown-callee default over what the argument reaches (RFC 0030 - // §5.1): any of it may be what the callee released. - std::vector start; - start.reserve(root.targets.size()); - for (const core::Target &target : root.targets) - start.push_back(target.object); - core::ReleaseRecord record; - record.reason = core::ReleaseRecord::Reason::UnknownCallee; - record.where = toCoreLocation(context.getSourceManager(), call.getBeginLoc()); - record.allPaths = false; - for (core::ObjectId id : heap.reachableObjects(state, start)) { - if (!state.objects.contains(id)) - continue; - core::ObjectKind kind = run.table().info(id).key.kind; - if (kind == core::ObjectKind::Local || kind == core::ObjectKind::Global || - kind == core::ObjectKind::Literal || kind == core::ObjectKind::Function) - continue; - core::ObjectState &object = state.objects.at(id); - if (object.life == core::Life::Live) { - object.life = core::Life::UnknownReleased; - object.record = record; - } - } - if (run.isPublishing()) - for (core::SiteId id : run.sites().sitesOf(call)) { - const SiteInfo *info = run.sites().info(id); - if (info == nullptr || info->kind != core::SiteKind::Call || - info->boundary != core::Boundary::Call || - !run.applies(id, core::Facet::Temporal)) - continue; - run.ledger().decideAs(call, info->kind, info->boundary, - core::Facet::Temporal, - core::FacetDecision::unresolvedFor( - core::UnresolvedReason::RawCast, - "incompatible or unknown object view at " - "call")); - } - return true; -} - -/// RFC 0034 §6.3: the new objects a summary leaves in several places (its -/// result, cells) on exits a call's arguments do not show to be the same: -/// each exit numbers its own new objects from #0, so one object of the -/// call stands for several (`if (w) *a = f(); else *b = f();`) and is not -/// singular. The result's parameter test only names a test its exits pass; -/// a store's is the exits it is on. -static std::set -scatteredObjects(const core::Heap &heap, const core::HeapState &state, - const core::FunctionEffects &effects, - const std::vector &args) { - using Test = std::optional>; - // The test the call leaves open, or none; false when it excludes. - auto open = [&](Test &test) { - if (!test || test->first >= args.size() || - args[test->first] == core::ZeroSym) - return true; - std::optional zero = isZeroValue(heap, state, args[test->first]); - if (zero && *zero != test->second) - return false; - if (zero) - test.reset(); - return true; - }; - std::set all; - for (const core::ResultEffect &alternative : effects.results) - all.insert(alternative.classes.begin(), alternative.classes.end()); - auto classes = [&](std::vector of) { - std::ranges::sort(of); - return std::ranges::includes(of, all) ? std::vector{} - : of; - }; - struct Places { - std::vector stores; - bool may = false; - std::optional> result; - std::vector resultTests; - }; - std::map places; - for (core::ResultEffect alternative : effects.results) - if (alternative.value.kind == core::ValueDesc::Kind::Fresh && - open(alternative.paramZero)) { - Places &at = places[alternative.value.object]; - std::vector with = - at.result.value_or(std::vector{}); - with.insert(with.end(), alternative.classes.begin(), - alternative.classes.end()); - at.result = classes(with); - at.resultTests.push_back(alternative.paramZero); - } - for (const core::StoreEffect &store : effects.stores) { - core::EffectCase when = store.when; - if (store.value.kind != core::ValueDesc::Kind::Fresh || store.contents || - store.dest.isResult() || !open(when.paramZero)) - continue; - when.classes = classes(when.classes); - Places &at = places[store.value.object]; - at.stores.push_back(when); - at.may = at.may || store.may; - } - std::set several; - for (const auto &[object, at] : places) { - if (at.stores.size() + (at.result ? 1 : 0) < 2) - continue; - bool apart = at.may || std::ranges::any_of(at.stores, [&](const auto &c) { - return !(c == at.stores.front()); - }); - if (at.result) { - const core::EffectCase &store = at.stores.front(); - apart = apart || store.paramsEqual || store.entryZero || - store.classes != *at.result; - for (const Test &test : at.resultTests) - apart = apart || (store.paramZero && test != store.paramZero); - } - if (apart) - several.insert(object); - } - return several; -} - -/// The value a `path` description names at the call: the value `at` the -/// path holds, `offset` bytes on, or null where the description says the -/// callee may have left null instead (its entry value joined with null). -core::Sym Transfer::pathValue(core::Sym at, const core::ValueDesc &desc, - QualType type) { - core::Sym value = at; - if (desc.offset && *desc.offset != 0 && - heap.info(state, value).type == core::SymInfo::Type::Pointer) { - core::SymInfo shifted = heap.info(state, value); - shifted.entryOf.reset(); - shifted.pending.clear(); - for (core::Target &target : shifted.targets) - target.offset = target.offset.plusConstant(*desc.offset); - value = heap.fresh(state, shifted); - } - // (Also over an uninitialised value, which reads as null too.) - if (desc.maybeNull && type->isPointerType() && - heap.info(state, value).type == core::SymInfo::Type::Pointer) - value = heap.mergeWeak(state, value, nullPointer(type)); - return value; -} - -// NOLINTNEXTLINE(readability-function-size): one pass over the summary -core::Sym Transfer::instantiate(const CallExpr &call, - const core::FunctionEffects &effects, - const std::vector &args) { - PathResolver resolver(*this, call, args); - // The callee's new objects by their summary index. - std::map created; - std::map madeBy; - std::set several = - scatteredObjects(heap, state, effects, args); - auto freshObject = [&](const core::ValueDesc &desc, QualType pointee, - bool owned) -> core::ObjectId { - if (auto it = created.find(desc.object); it != created.end()) { - // RFC 0034 §6.3: rows that give one new object different facts are - // applied together: their join (an unknown family or extent). - const core::ValueDesc &first = *madeBy.at(desc.object); - if (state.objects.contains(it->second)) { - core::ObjectState &made = state.objects.at(it->second); - if (first.family != desc.family) - made.family.clear(); - if (first.extent != desc.extent) - made.extent.reset(); - made.zeroed = made.zeroed && desc.zeroed; - } - return it->second; - } - madeBy[desc.object] = &desc; - core::SummaryPath key = core::SummaryPath::result(); - key.index = desc.object; - core::ObjectId object = - run.allocationObject(call, pointee, spell(call), key); - if (desc.many || several.contains(desc.object)) { - // One object per element (RFC 0015 §5): several runtime objects. - core::ObjectKey manyKey = run.table().info(object).key; - manyKey.kind = core::ObjectKind::HeapOld; - core::ObjectInfo manyInfo = run.table().info(object); - manyInfo.singular = false; - object = run.table().intern(manyKey, manyInfo); - } - core::ObjectState allocated; - allocated.family = desc.family; - allocated.owned = owned; - allocated.zeroed = desc.zeroed; - core::Term extent = core::Term::unknown(); - if (desc.extent && !desc.extent->path) { - extent = core::Term::of(desc.extent->constant); - } else if (desc.extent && desc.extent->path && - desc.extent->path->isParam() && - desc.extent->path->steps.empty() && - desc.extent->path->index < args.size()) { - core::Term base = termOf(args[desc.extent->path->index]); - if (base.known) { - base.scale *= desc.extent->scale; - base.constant = - (base.constant * desc.extent->scale) + desc.extent->constant; - if (base.isConstant()) - base.scale = 0; - extent = base; - } - } - if (extent.known) - allocated.extent = - core::Extent{.bytes = extent, .cls = core::ExtentClass::Exact}; - state.objects.set(object, allocated); - created[desc.object] = object; - return object; - }; - core::SourceLocation here = - toCoreLocation(context.getSourceManager(), call.getBeginLoc()); - // RFC 0004: a raw pointer the callee returned or stored stays raw. - auto rawValue = [&](core::Sym value, const core::ValueDesc &desc) { - core::SymInfo &info = heap.infoMut(state, value); - if (desc.raw && info.type == core::SymInfo::Type::Pointer) { - info.raw = true; - info.rawSome = desc.rawSome; - info.rawAt = here; - info.rawOrigin = core::SymInfo::RawOrigin::Returned; - info.rawFrom = spell(*call.getCallee()); - } - }; - // The result, from its alternatives. - QualType type = call.getType(); - core::Sym result = core::ZeroSym; - bool anyNull = false; - bool anyNonNull = false; - bool fresh = false; - auto argumentFails = - [&](const std::optional> &test) { - if (!test || test->first >= args.size()) - return false; - auto argument = state.zone.constant(args[test->first]); - return argument && (*argument == 0) != test->second; - }; - for (const core::ResultEffect &alternative : effects.results) { - if (argumentFails(alternative.paramZero)) - continue; - const core::ValueDesc &desc = alternative.value; - core::Sym value = core::ZeroSym; - switch (desc.kind) { - case core::ValueDesc::Kind::Null: - anyNull = true; - continue; - case core::ValueDesc::Kind::Fresh: { - QualType pointee = - type->isPointerType() ? type->getPointeeType() : QualType(); - core::ObjectId object = freshObject(desc, pointee, true); - core::SymInfo info; - info.type = core::SymInfo::Type::Pointer; - info.targets = {core::Target{ - .object = object, - .offset = desc.interior ? core::Term::unknown() - : core::Term::of(desc.offset.value_or(0))}}; - info.null = core::PointerNull::NonNull; - info.name = spell(call); - info.ctype = typeHandle(type); - value = heap.fresh(state, info); - break; - } - case core::ValueDesc::Kind::Path: - if (desc.path) - if (auto at = resolver.valueAt(*desc.path)) - value = pathValue(*at, desc, type); - break; - case core::ValueDesc::Kind::Int: - value = unknownValue(type); - boundWithinType(state, value, desc); - boundByRange(value, type, desc); - break; - case core::ValueDesc::Kind::Dangling: - if (type->isPointerType()) - value = danglingValue(call, type, core::SummaryPath::result()); - break; - case core::ValueDesc::Kind::Function: - value = functionValue(desc); - break; - case core::ValueDesc::Kind::Static: - case core::ValueDesc::Kind::Unknown: - break; - } - if (value == core::ZeroSym) { - value = unknownValue(type); - rawValue(value, desc); - } - if (heap.info(state, value).type == core::SymInfo::Type::Pointer) { - if (desc.maybeNull) - anyNull = true; - anyNonNull = true; - } - fresh = fresh || desc.kind == core::ValueDesc::Kind::Fresh; - result = - result == core::ZeroSym ? value : heap.mergeWeak(state, result, value); - } - if (result == core::ZeroSym) - result = anyNull && type->isPointerType() ? nullPointer(type) - : unknownValue(type); - if (type->isPointerType()) { - // RFC 0033 §1: a result that is an argument on some classes and null on - // others is a value of its own. Made maybe-null in place, it would be - // the argument's value, and a test of the result would refine the - // argument (`if (add(list, v) == NULL)` taken as `list == NULL`). - if (anyNull && anyNonNull && - heap.info(state, result).type == core::SymInfo::Type::Pointer && - heap.info(state, result).null != core::PointerNull::Maybe) { - core::SymInfo own = heap.info(state, result); - result = heap.fresh(state, std::move(own)); - } - core::SymInfo &info = heap.infoMut(state, result); - if (info.type == core::SymInfo::Type::Pointer) { - if (anyNull && anyNonNull) - info.null = core::PointerNull::Maybe; - info.allocatorSource = fresh && anyNull; - if (info.allocatorSource) - info.nullOrigin = - core::NullOrigin{.reason = core::NullOrigin::Reason::Allocated, - .where = here, - .detail = spell(*call.getCallee())}; - } - } - resolver.setResult(result, type); - // RFC 0013: a record result's fields (`result.f` stores) go into the - // caller's temporary, which then holds nothing else. - if (type->isRecordType()) { - bool described = false; - for (const core::StoreEffect &store : effects.stores) - described = - described || (store.dest.isResult() && !store.dest.steps.empty() && - store.dest.steps.front().step == core::PathStep::Field); - core::ObjectId temporary = run.recordResultObject(call); - if (described && state.objects.contains(temporary)) { - core::ObjectState &object = state.objects.at(temporary); - object.cells = {}; - object.havocked = false; - object.forgotten.clear(); - object.mayForgotten.clear(); - object.uninitialised = true; - Address storage; - storage.targets = {core::Target{.object = temporary}}; - resolver.setResultStorage(storage); - } - } - // Effects on the entry heap. - auto recordFor = [&](const core::PathEffect &effect) { - core::ReleaseRecord record; - record.reason = effect.kind == core::PathEffect::Kind::Move - ? core::ReleaseRecord::Reason::Moved - : core::ReleaseRecord::Reason::Freed; - record.where = here; - record.family = effect.family; - record.conditional = effect.may || !effect.when.classes.empty(); - record.lossy = effect.lossy; - return record; - }; - // A callee's index term at the call. - auto termAtCall = [&](const core::PathTerm &term) -> core::Term { - if (!term.path) - return core::Term::of(term.constant); - std::optional value = resolver.valueAt(*term.path); - if (!value) - return core::Term::unknown(); - core::Term base = termOf(*value); - if (!base.known) - return base; - __int128 scale = static_cast<__int128>(base.scale) * term.scale; - __int128 constant = - (static_cast<__int128>(base.constant) * term.scale) + term.constant; - if (scale < INT64_MIN || scale > INT64_MAX || constant < INT64_MIN || - constant > INT64_MAX) - return core::Term::unknown(); - if (base.isConstant()) - return core::Term::of(static_cast(constant)); - return core::Term::ofSym(base.var, static_cast(scale), - static_cast(constant)); - }; - // The elements `[from, to)` of a callee's range at the call, per target: - // the position and the caller's index bounds, or none (then weakly). - // (And the elements' position in the object, `sub[*].sp` at 8 of 16, - // for some elements weakly; none at an unknown offset.) - struct CallerRange { - core::ObjectId object; - std::optional>> - range; - std::optional position = std::nullopt; - }; - auto someAt = [&](const core::Target &target, std::int64_t stride) { - CallerRange caller{.object = target.object, .range = std::nullopt}; - if (auto base = elementBase(target.offset, stride)) - caller.position = base->first; - return caller; - }; - auto rangesAt = [&](const PathResolver::Elements &elements, - const core::ElementRange &range) { - core::Term from = termAtCall(range.from); - core::Term to = termAtCall(range.to); - std::vector out; - for (const core::Target &target : elements.targets) { - CallerRange caller = someAt(target, elements.stride); - if (auto base = elementBase(target.offset, elements.stride)) { - auto lo = from.plus(base->second); - auto hi = to.plus(base->second); - if (lo && hi && lo->known && hi->known) - caller.range = std::make_pair(base->first, std::make_pair(*lo, *hi)); - } - out.push_back(caller); - } - return out; - }; - // RFC 0031 §5.4 (a declared contract first), RFC 0003: a parameter the - // callee's declaration annotates `WEAVEC_BORROWED` or `WEAVEC_MUT` is not - // consumed, whatever its definition does (that is its own mismatch). - std::vector borrowedParams; - if (const FunctionDecl *callee = call.getDirectCallee()) { - SignatureAnnotations signature = collectAnnotations(*callee); - for (const AnnotationSet &set : signature.params) - borrowedParams.push_back(set.borrowed || set.mutBorrowed); - } - // Values a possible release of the summary reached (no result class - // tells when it happens). - std::set possiblyReleased; - // Effects name the callee's entry state: an unknown effect, which forgets - // the cells it reaches, comes after every effect that reads a path - // through them (RFC 0031 §6.3). - std::vector ordered; - for (const core::PathEffect &effect : effects.effects) - if (effect.kind != core::PathEffect::Kind::Unknown) - ordered.push_back(effect); - for (const core::PathEffect &effect : effects.effects) - if (effect.kind == core::PathEffect::Kind::Unknown) - ordered.push_back(effect); - // Their paths too name the entry state: each is resolved before any - // forgets the cells another's path reads (`*p` and `*p->next`). - std::vector> unknownValues(ordered.size()); - for (std::size_t i = 0; i < ordered.size(); ++i) - if (ordered[i].kind == core::PathEffect::Kind::Unknown) { - core::SummaryPath valuePath = ordered[i].path; - if (!valuePath.steps.empty() && - valuePath.steps.back().step == core::PathStep::Deref) - valuePath.steps.popBack(); - unknownValues[i] = resolver.valueAt(valuePath); - } - for (std::size_t ordinal = 0; ordinal < ordered.size(); ++ordinal) { - const core::PathEffect &effect = ordered[ordinal]; - if ((effect.kind == core::PathEffect::Kind::Release || - effect.kind == core::PathEffect::Kind::Move) && - effect.path.isParam() && effect.path.index < borrowedParams.size() && - borrowedParams[effect.path.index] && effect.path.steps.size() == 1) - continue; - // The value whose object the path names: the path without its final - // dereference. - core::SummaryPath valuePath = effect.path; - if (!valuePath.steps.empty() && - valuePath.steps.back().step == core::PathStep::Deref) - valuePath.steps.popBack(); - if (effect.elements && (effect.kind == core::PathEffect::Kind::Release || - effect.kind == core::PathEffect::Kind::Move)) { - // RFC 0015 §5: every element of the range released. - auto elements = resolver.elementsAt(valuePath); - if (!elements || elements->cellType.isNull()) - continue; - core::ReleaseRecord record = recordFor(effect); - core::SymInfo hint; - hint.type = core::SymInfo::Type::Pointer; - hint.ctype = typeHandle(elements->cellType); - for (const CallerRange &caller : rangesAt(*elements, *effect.elements)) { - if (caller.range) { - heap.releaseElements(state, caller.object, caller.range->first, - caller.range->second.first, - caller.range->second.second, record, hint); - continue; - } - // Not expressible here: some elements, weakly. - core::Sym some = caller.position ? heap.load(state, caller.object, - *caller.position, hint) - : unknownValue(elements->cellType); - core::ReleaseRecord weak = record; - weak.allPaths = false; - weak.aliasOnly = true; - if (heap.info(state, some).type == core::SymInfo::Type::Pointer) - heap.release(state, some, weak); - } - continue; - } - std::optional value = - effect.kind == core::PathEffect::Kind::Unknown - ? unknownValues[ordinal] - : resolver.valueAt(valuePath); - core::PathEffect keyed = effect; - if (effect.when.paramZero) { - auto [index, zero] = *effect.when.paramZero; - std::optional argument; - if (index < args.size() && args[index] != core::ZeroSym) - argument = isZeroValue(heap, state, args[index]); - if (argument) { - if (*argument != zero) - continue; // the argument selects the exits without the effect - } else { - keyed.may = true; - } - } - if (effect.when.paramsEqual) { - std::optional equal = - argumentsEqual(args, *effect.when.paramsEqual); - if (!equal) - keyed.may = true; - else if (*equal != effect.when.paramsEqual->equal) - continue; // the arguments select the paths without the effect - } - if (effect.when.entryZero) { - std::optional zero; - if (auto at = resolver.valueAt(effect.when.entryZero->first)) - zero = isZeroValue(heap, state, *at); - if (!zero) - keyed.may = true; - else if (*zero != effect.when.entryZero->second) - continue; // the value the callee tested selects the other paths - } - const core::PathEffect &applied = keyed; - (void)applied; - switch (effect.kind) { - case core::PathEffect::Kind::Release: - case core::PathEffect::Kind::Move: { - // RFC 0031 §4.2, RFC 0030 §2.3: a path the caller's memory does not - // hold as the callee saw it (a `void *` handed a record of another - // layout): what the callee released is not known here. - if ((!value || heap.info(state, *value).rawCast || - heap.info(state, *value).type != core::SymInfo::Type::Pointer) && - lostView(call, args, valuePath)) - break; - if (!value) - break; - // RFC 0031 §4.2: the callee released a pointer where the caller's - // type holds no pointer (`release_field(&second)` through a `struct - // first *`): a pointer made of those bytes, released as a - // `free((void *)n)` would be. - if (const core::SymInfo &held = heap.info(state, *value); - held.type == core::SymInfo::Type::Int && - held.pointerBehind == core::ZeroSym) { - if (auto c = state.zone.constant(*value); c && *c == 0) - break; - core::Sym raw = unknownValue(context.VoidPtrTy); - core::SymInfo &rawInfo = heap.infoMut(state, raw); - rawInfo.raw = true; - rawInfo.rawAt = here; - value = raw; - if (run.isPublishing()) - run.ledger().decideAs(call, core::SiteKind::Call, - core::Boundary::Call, core::Facet::Temporal, - core::FacetDecision::unresolvedFor( - core::UnresolvedReason::RawCast)); - } - // An interior release: the callee released the value plus `offset`. - // (At an offset the callee does not know: into the same objects, at - // one the caller does not know either.) - if ((effect.offset != 0 || effect.anyOffset) && - heap.info(state, *value).type == core::SymInfo::Type::Pointer) { - core::SymInfo shifted = heap.info(state, *value); - shifted.pending.clear(); - shifted.release.reset(); - for (core::Target &target : shifted.targets) - target.offset = effect.anyOffset - ? core::Term::unknown() - : target.offset.plusConstant(effect.offset); - value = heap.fresh(state, shifted); - } - const core::SymInfo &info = heap.info(state, *value); - if (info.type != core::SymInfo::Type::Pointer || - info.null == core::PointerNull::Null) - break; - core::ReleaseRecord record = recordFor(keyed); - record.via = valuePath.isRoot() && valuePath.index < call.getNumArgs() - ? spell(*call.getArg(valuePath.index)) - : ""; - // Through an index step: some element, which the caller cannot tell - // (§4.9): no evidence about any one value. - bool indexed = false; - for (const core::PathElem &elem : valuePath.steps) - indexed = indexed || elem.step == core::PathStep::Index; - if (indexed) { - record.allPaths = false; - record.aliasOnly = true; - } - // RFC 0031 §5.5: the callee releases an argument that is no start of - // a heap object (`release(&x)`), reported at the call. - if (effect.kind == core::PathEffect::Kind::Release && !indexed && - (!valuePath.isParam() || !valuePath.isRoot() || - valuePath.index >= call.getNumArgs())) - calleeRelease(call, *value, valuePath, - !effect.may && effect.when.always() && !effect.lossy); - if (effect.kind == core::PathEffect::Kind::Release && !indexed && - valuePath.isParam() && valuePath.isRoot() && - valuePath.index < call.getNumArgs() && run.isPublishing()) { - const Expr &argument = *call.getArg(valuePath.index); - ReleaseCheck check = - releaseCheck(*value, record.via, argument, "released"); - if (check.kind == ReleaseCheck::Kind::Violation || - check.kind == ReleaseCheck::Kind::Possible) { - bool definite = check.kind == ReleaseCheck::Kind::Violation && - !record.conditional; - if (!definite) { - std::size_t at = check.message.find(" but points"); - if (at != std::string::npos) - check.message.replace(at, 11, " but may point"); - } - core::Diagnostic diagnostic; - diagnostic.id = core::diag::InvalidRelease; - diagnostic.severity = - definite ? core::Severity::Error : core::Severity::Warning; - diagnostic.message = check.message; - diagnostic.location = - toCoreLocation(context.getSourceManager(), call.getBeginLoc()); - if (check.noteAt.isValid()) - diagnostic.addNote(check.note, check.noteAt); - run.report(std::move(diagnostic), - definite ? core::Certainty::Definite - : core::Certainty::Possible, - &call, core::Facet::Spatial); - } - } - // A release through a path of a value this function already released - // (RFC 0030 §3.1): the second free is the callee's, at the call. - std::optional spelled; - if (effect.kind == core::PathEffect::Kind::Release && !indexed && - run.isPublishing() && !(valuePath.isParam() && valuePath.isRoot())) { - spelled = spellArgumentPath(call, valuePath); - // A global the callee releases, spelled by its name here. - if (!spelled && valuePath.isGlobal()) - if (const VarDecl *var = - functionRun().unitRun().globalDecl(valuePath.index)) - spelled = valuePath.toString(var->getNameAsString()); - } - if (spelled) { - core::TemporalVerdict before = heap.temporal(state, *value); - bool certain = - !keyed.may && effect.when.classes.empty() && !effect.lossy; - // (Only for values of objects that stand for one runtime object - // each: a k-limited summary object's elements are not told apart, - // RFC 0031 §4.6.) - bool singular = !heap.info(state, *value).top; - for (const core::Target &target : heap.info(state, *value).targets) - singular = singular && run.table().info(target.object).singular; - if (!singular) - before.kind = core::TemporalVerdict::Kind::Proven; - // (Not a record this call made, nor one still pending on a null - // argument the path goes through.) - if (before.record && (before.record->where == here || - pendingOnNullArgument(args, *value, valuePath, - before.record->where))) - before.kind = core::TemporalVerdict::Kind::Proven; - if (before.record && !before.record->unknownOrigin() && - before.record->reason != core::ReleaseRecord::Reason::Moved && - (before.kind == core::TemporalVerdict::Kind::Violation || - before.kind == core::TemporalVerdict::Kind::MayReleased)) { - bool definite = - certain && before.kind == core::TemporalVerdict::Kind::Violation; - core::Diagnostic diagnostic; - diagnostic.id = core::diag::DoubleFree; - diagnostic.severity = - definite ? core::Severity::Error : core::Severity::Warning; - diagnostic.message = - "'" + *spelled + "' " + - (definite ? "is freed twice" : "may be freed twice"); - diagnostic.location = - toCoreLocation(context.getSourceManager(), call.getBeginLoc()); - if (before.record->where.isValid()) - diagnostic.addNote(definite ? "previously freed here" - : "previously freed here on some " - "paths", - before.record->where); - run.report(std::move(diagnostic), - definite ? core::Certainty::Definite - : core::Certainty::Possible, - &call, core::Facet::Temporal); - } - } - // RFC 0007: the family the callee releases with against the one the - // object was allocated in, as a direct release checks it. - if (!effect.family.empty() && !indexed && run.isPublishing() && - info.targets.size() == 1 && !info.top) { - const core::ObjectState *object = - heap.findObject(state, info.targets[0].object); - if (object != nullptr && !object->family.empty() && - object->family != effect.family && - object->life == core::Life::Live) { - bool definite = !keyed.may && effect.when.always() && !effect.lossy; - std::string place = - spellArgumentPath(call, valuePath) - .value_or(valuePath.isParam() && valuePath.isRoot() && - valuePath.index < call.getNumArgs() - ? spell(*call.getArg(valuePath.index)) - : std::string("an argument")); - core::Diagnostic diagnostic; - diagnostic.id = core::diag::MismatchedRelease; - diagnostic.severity = - definite ? core::Severity::Error : core::Severity::Warning; - diagnostic.message = - "'" + place + "' " + (definite ? "is" : "may be") + - " released with '" + effect.family + - "' but must be released with '" + object->family + "'"; - diagnostic.location = here; - if (run.table().info(info.targets[0].object).created.isValid()) - diagnostic.addNote( - "allocated here", - run.table().info(info.targets[0].object).created); - if (run.isPublishing()) - run.ledger().decideAs( - call, core::SiteKind::Call, core::Boundary::Call, - core::Facet::Temporal, - definite ? core::FacetDecision::violation() - : core::FacetDecision::unresolvedFor( - core::UnresolvedReason::MayReleased)); - run.report(std::move(diagnostic), - definite ? core::Certainty::Definite - : core::Certainty::Possible, - &call, core::Facet::Temporal); - } - } - heap.release(state, *value, record); - if (effect.may || effect.lossy) - possiblyReleased.insert(*value); - if (!effect.when.classes.empty()) { - core::PendingCase pending; - for (core::ResultClass c : effect.when.classes) - pending.classes.emplace_back(core::toString(c)); - pending.subject = *value; - pending.record = record; - pending.record.conditional = effect.may || effect.lossy; - // (A parameter test the arguments did not decide: tested again - // when the class is.) - if (effect.when.paramZero && - effect.when.paramZero->first < args.size() && - args[effect.when.paramZero->first] != core::ZeroSym && - !isZeroValue(heap, state, args[effect.when.paramZero->first])) - pending.argumentZero = - std::make_pair(args[effect.when.paramZero->first], - effect.when.paramZero->second); - heap.infoMut(state, result).pending.push_back(pending); - } - break; - } - case core::PathEffect::Kind::Unknown: { - if (!value) - break; - const core::SymInfo &info = heap.info(state, *value); - std::vector objects; - objects.reserve(info.targets.size()); - for (const core::Target &target : info.targets) - objects.push_back(target.object); - // The code the callee could not see had the object, and so every - // object the caller's memory reaches from it (the callee's summary - // names only those it had materialised), as a direct call to unknown - // code does (havocReachable()). - objects = reachableFrom(heap, state, objects); - core::ReleaseRecord record; - record.reason = core::ReleaseRecord::Reason::UnknownCallee; - record.where = here; - record.allPaths = false; - for (core::ObjectId id : objects) - if (state.objects.contains(id)) { - core::ObjectState &object = state.objects.at(id); - // (Storage of the caller's frame, a global or a literal is - // written, never released, RFC 0030 §5.1.) - core::ObjectKind kind = run.table().info(id).key.kind; - if (kind != core::ObjectKind::Local && - kind != core::ObjectKind::Global && - kind != core::ObjectKind::Literal && - kind != core::ObjectKind::Function) { - object.life = core::Life::UnknownReleased; - object.record = record; - } - object.cells = {}; - object.havocked = true; - } - break; - } - case core::PathEffect::Kind::Escape: - if (value) { - const core::SymInfo &info = heap.info(state, *value); - std::vector objects; - objects.reserve(info.targets.size()); - for (const core::Target &target : info.targets) - objects.push_back(target.object); - for (core::ObjectId id : objects) - if (state.objects.contains(id)) - state.objects.at(id).escaped = true; - } - break; - case core::PathEffect::Kind::ShareDown: - case core::PathEffect::Kind::ShareUp: - break; - } - } - // Stores into the caller's memory. Their places and values name the - // callee's entry state, so every one is read before any is written. - struct Write { - const core::StoreEffect *effect; - QualType cellType; - std::optional
cell; - std::optional elements; - core::Sym value; - }; - std::vector writes; - // Whether a store of a new object's contents could not be placed. - bool lostContents = false; - // Stores an argument selects (RFC 0030 §9.1): kept as they apply. - std::vector selected; - // Those whose parameter test the argument leaves open: on the other - // paths the cell keeps its old value, and what that value says about - // itself (a release the same call made) holds there. - std::set evidenced; - // The new object a store makes on exactly the paths where the caller's - // own entry value was null (an undecided lazy initialisation): it exists - // where that entry test holds. - std::map madeWhere; - for (const core::StoreEffect &store : effects.stores) { - if (!store.when.paramZero && !store.when.entryZero) { - selected.push_back(store); - continue; - } - // Whether the call's values decide the case: an argument's, or the - // value at the entry path the callee tested. - bool decided = true; - bool excluded = false; - if (store.when.paramZero) { - auto [index, zero] = *store.when.paramZero; - std::optional argument; - if (index < args.size() && args[index] != core::ZeroSym) - argument = isZeroValue(heap, state, args[index]); - decided = decided && argument.has_value(); - excluded = excluded || (argument && *argument != zero); - } - std::optional entryTest; - if (store.when.entryZero) { - std::optional held; - if (auto at = resolver.valueAt(store.when.entryZero->first)) { - held = isZeroValue(heap, state, *at); - if (const core::SymInfo *value = state.syms.find(*at); - value != nullptr && value->entryOf && - value->entryOf->second.isConcrete()) - entryTest = core::EntryTest{.object = value->entryOf->first, - .key = value->entryOf->second, - .zero = store.when.entryZero->second}; - } - decided = decided && held.has_value(); - excluded = excluded || (held && *held != store.when.entryZero->second); - } - if (excluded) - continue; - core::StoreEffect applied = store; - applied.when.paramZero.reset(); - applied.when.entryZero.reset(); - if (!decided && !store.may && !store.when.paramZero && entryTest && - store.value.kind == core::ValueDesc::Kind::Fresh) - madeWhere[selected.size()] = *entryTest; - applied.may = applied.may || !decided; - if (!decided) - evidenced.insert(selected.size()); - selected.push_back(applied); - } - // The new objects stored into cells first: the stores below those cells - // are their contents. - std::map freshStored; - std::vector byDepth; - byDepth.reserve(selected.size()); - for (const core::StoreEffect &store : selected) - byDepth.push_back(&store); - std::ranges::stable_sort( - byDepth, [](const core::StoreEffect *a, const core::StoreEffect *b) { - return a->dest.steps.size() < b->dest.steps.size(); - }); - for (const core::StoreEffect *pointer : byDepth) { - const core::StoreEffect &store = *pointer; - if (store.value.kind != core::ValueDesc::Kind::Fresh) - continue; - bool indexed = false; - for (const core::PathElem &elem : store.dest.steps) - indexed = indexed || elem.step == core::PathStep::Index; - QualType cellType; - if (indexed || !resolver.destination(store, &cellType) || - cellType.isNull() || !cellType->isPointerType()) - continue; - core::ObjectId object = - freshObject(store.value, cellType->getPointeeType(), true); - core::SymInfo info; - info.type = core::SymInfo::Type::Pointer; - info.targets = { - core::Target{.object = object, - .offset = core::Term::of(store.value.offset.value_or(0))}}; - info.null = store.value.maybeNull ? core::PointerNull::Maybe - : core::PointerNull::NonNull; - // RFC 0030 §3.2: a new object the callee may have failed to make; a - // null test of the cell tells the failure, which made no object. - info.allocatorSource = store.value.maybeNull; - core::Sym value = heap.fresh(state, info); - if (auto made = - madeWhere.find(static_cast(&store - selected.data())); - made != madeWhere.end() && state.objects.contains(object)) - state.objects.at(object).existsIfEntry = made->second; - freshStored[&store] = value; - resolver.setStored(store, value); - } - for (const core::StoreEffect &store : selected) { - QualType cellType; - std::optional
cell; - std::optional elements; - bool indexed = false; - for (const core::PathElem &elem : store.dest.steps) - indexed = indexed || elem.step == core::PathStep::Index; - // A new object's contents the caller cannot place: its other cells are - // not what the object was made with either (below). - bool contents = store.contents || store.dest.isResult(); - if (indexed) { - elements = resolver.destinationElements(store); - if (!elements || elements->cellType.isNull()) { - lostContents = lostContents || contents; - continue; - } - cellType = elements->cellType; - } else { - cell = resolver.destination(store, &cellType); - if (!cell || cellType.isNull()) { - lostContents = lostContents || contents; - continue; - } - } - core::Sym value = core::ZeroSym; - switch (store.value.kind) { - case core::ValueDesc::Kind::Null: - value = cellType->isPointerType() ? nullPointer(cellType) - : constant(0, cellType); - break; - case core::ValueDesc::Kind::Path: - if (store.value.path) - if (auto at = resolver.valueAt(*store.value.path)) - value = pathValue(*at, store.value, cellType); - break; - case core::ValueDesc::Kind::Int: - value = unknownValue(cellType); - boundWithinType(state, value, store.value); - boundByRange(value, cellType, store.value); - break; - case core::ValueDesc::Kind::Dangling: - if (cellType->isPointerType()) - value = danglingValue(call, cellType, store.dest); - break; - case core::ValueDesc::Kind::Function: - value = functionValue(store.value); - break; - case core::ValueDesc::Kind::Fresh: { - if (auto it = freshStored.find(&store); it != freshStored.end()) { - value = it->second; - } else { - QualType pointee = - cellType->isPointerType() ? cellType->getPointeeType() : QualType(); - core::ObjectId object = freshObject(store.value, pointee, true); - core::SymInfo info; - info.type = core::SymInfo::Type::Pointer; - info.targets = {core::Target{ - .object = object, - .offset = store.value.interior - ? core::Term::unknown() - : core::Term::of(store.value.offset.value_or(0))}}; - info.null = store.value.maybeNull ? core::PointerNull::Maybe - : core::PointerNull::NonNull; - info.allocatorSource = store.value.maybeNull; - value = heap.fresh(state, info); - } - // A store on some result classes only: on the others the object was - // never made, which a test of the result tells. - if (!store.when.classes.empty() && result != core::ZeroSym) { - core::PendingCase absent; - absent.kind = core::PendingCase::Kind::Absent; - for (core::ResultClass c : - {core::ResultClass::Null, core::ResultClass::NonNull, - core::ResultClass::Zero, core::ResultClass::Positive, - core::ResultClass::Negative}) - if (std::ranges::find(store.when.classes, c) == - store.when.classes.end()) - absent.classes.emplace_back(core::toString(c)); - absent.subject = value; - heap.infoMut(state, result).pending.push_back(absent); - } - // Null on some classes (the allocation failed there): never made on - // those, non-null on the others. - if (!store.absentOn.empty() && result != core::ZeroSym) { - core::PendingCase absent; - absent.kind = core::PendingCase::Kind::Absent; - core::PendingCase made; - made.kind = core::PendingCase::Kind::NonNull; - for (core::ResultClass c : - {core::ResultClass::Null, core::ResultClass::NonNull, - core::ResultClass::Zero, core::ResultClass::Positive, - core::ResultClass::Negative}) - (std::ranges::find(store.absentOn, c) != store.absentOn.end() ? absent - : made) - .classes.emplace_back(core::toString(c)); - absent.subject = value; - made.subject = value; - heap.infoMut(state, result).pending.push_back(absent); - heap.infoMut(state, result).pending.push_back(made); - } - break; - } - default: - break; - } - if (value == core::ZeroSym) { - value = unknownValue(cellType); - rawValue(value, store.value); - } - writes.push_back(Write{.effect = &store, - .cellType = cellType, - .cell = std::move(cell), - .elements = std::move(elements), - .value = value}); - } - // Contents the caller could not place (a type it reads differently): the - // new objects' unwritten cells read as unknown, not as zeros. - if (lostContents) { - for (const auto &[index, object] : created) - if (state.objects.contains(object)) - state.objects.at(object).havocked = true; - if (type->isRecordType()) { - core::ObjectId temporary = run.recordResultObject(call); - if (state.objects.contains(temporary)) - state.objects.at(temporary).havocked = true; - } - } - // The objects of the string facts, named in the entry state (§6.3). - std::vector> strings; - for (const core::StringEffect &string : effects.strings) { - core::StoreEffect probe; - probe.dest = string.path; - probe.contents = string.contents; - QualType probeType; - auto address = resolver.destination(probe, &probeType); - if (address && !address->top && address->targets.size() == 1 && - run.table().info(address->targets[0].object).singular) - strings.emplace_back(&string, address->targets[0]); - } - // Two stores of different values that land on one caller cell (aliased - // arguments, `writes(&x, &x)`): the summary does not order them, so the - // cell holds either (the alias context of §6.6 orders them). - std::map, core::Sym> collided; - { - std::map, std::vector> - landing; - for (const Write &write : writes) - if (write.cell && write.cell->targets.size() == 1 && !write.cell->top && - write.cell->targets[0].offset.isConstant()) - landing[{write.cell->targets[0].object, - write.cell->targets[0].offset.constant}] - .push_back(write.value); - for (auto &[at, values] : landing) { - std::ranges::sort(values); - auto repeated = std::ranges::unique(values); - values.erase(repeated.begin(), repeated.end()); - if (values.size() < 2) - continue; - core::Sym joined = values.front(); - for (std::size_t i = 1; i < values.size(); ++i) - joined = heap.mergeWeak(state, joined, values[i]); - collided[at] = joined; - } - } - // An unknown value at an object's own path, or over some of its bytes: - // the callee rewrote them (deriveEffects), so none of the cells there or - // of its string facts stand. These come first: the summary's other - // stores are what the callee wrote after. (Its bytes only: a pointer - // into the middle of a caller's object, such as `&s.end`, names that - // member.) On some paths only, the cells keep their values beside - // unknown ones: a pointer the caller left there still reaches its object. - auto rewrites = [](const core::StoreEffect &store) { - return store.value.kind == core::ValueDesc::Kind::Unknown && - !store.dest.steps.empty() && - store.dest.steps.back().step == core::PathStep::Deref; - }; - auto unknownLike = [&](core::Sym sym) { - const core::SymInfo &old = heap.info(state, sym); - core::SymInfo unknown; - unknown.type = old.type; - unknown.ctype = old.ctype; - if (unknown.type == core::SymInfo::Type::Pointer) { - core::ObjectId any = run.unknownObject(); - heap.ensure(state, any); - unknown.targets = {core::Target{.object = any}}; - } - return heap.fresh(state, unknown); - }; - for (Write &write : writes) { - const core::StoreEffect &store = *write.effect; - if (!rewrites(store) || !write.cell) - continue; - bool weak = store.may || !store.when.always(); - for (const core::Target &target : write.cell->targets) { - heap.ensure(state, target.object); - std::optional from; - std::optional size; - if (target.offset.isConstant()) { - if (store.bytes) { - from = target.offset.constant + store.bytes->first; - size = store.bytes->second - store.bytes->first; - } else if (auto bytes = sizeOf(write.cellType); bytes && *bytes > 0) { - from = target.offset.constant; - size = bytes; - } - } - if (weak) - heap.weakenCells(state, target.object, from.value_or(0), size, - unknownLike); - else - heap.forgetCells(state, target.object, from.value_or(0), size); - state.objects.at(target.object).stored = true; - } - } - for (Write &write : writes) { - const core::StoreEffect &store = *write.effect; - QualType cellType = write.cellType; - std::optional
&cell = write.cell; - std::optional &elements = write.elements; - core::Sym value = write.value; - if (cell && cell->targets.size() == 1 && !cell->top && - cell->targets[0].offset.isConstant()) - if (auto it = collided.find( - {cell->targets[0].object, cell->targets[0].offset.constant}); - it != collided.end()) - value = it->second; - if (elements) { - // RFC 0015 §5: a range of elements each holding such a value, or (no - // range) some elements, weakly. - std::vector callers; - if (store.elements) - callers = rangesAt(*elements, *store.elements); - else - for (const core::Target &target : elements->targets) - callers.push_back(someAt(target, elements->stride)); - // RFC 0031 *Implementation amendments*, "Stores past the caller's - // object": a range the callee writes on every return, outside the one - // object the argument points into. - if (store.elements && !store.may && store.when.always() && - effects.returns == core::FunctionEffects::Returns::Always && - callers.size() == 1 && callers[0].range && - elements->targets.size() == 1) - if (const core::Term &from = callers[0].range->second.first, - &to = callers[0].range->second.second; - from.isConstant() && to.isConstant() && from.constant < to.constant) - storePastObject( - call, store, elements->targets[0], elements->targets[0].offset, - static_cast<__int128>(callers[0].range->first.offset) + - (static_cast<__int128>(from.constant) * elements->stride), - static_cast<__int128>(callers[0].range->first.offset) + - (static_cast<__int128>(to.constant - 1) * elements->stride) + - cellBytes(context, elements->cellType, elements->stride)); - for (const CallerRange &caller : callers) { - if (caller.range) { - heap.writeElements(state, caller.object, caller.range->first, - caller.range->second.first, - caller.range->second.second, value, - store.may || !store.when.always()); - continue; - } - heap.ensure(state, caller.object); - if (caller.position) { - // (Joined with what the elements held: their values read first.) - if (!heap.read(state, caller.object, *caller.position)) - run.unwritten(state, caller.object, *caller.position, - heap.info(state, value)); - heap.write(state, caller.object, *caller.position, value, true); - continue; - } - // (At an offset not known here: any of its bytes.) - heap.forgetCells(state, caller.object, 0, std::nullopt); - state.objects.at(caller.object).havocked = true; - } - continue; - } - // The same for one cell. - if (!store.may && store.when.always() && - effects.returns == core::FunctionEffects::Returns::Always && cell && - !cell->top && cell->targets.size() == 1 && - cell->targets[0].offset.isConstant()) - if (auto bytes = sizeOf(cellType); bytes && *bytes > 0 && - store.dest.isParam() && - store.dest.index < args.size()) { - // (Behind the argument, where it points into the object.) - __int128 start = cell->targets[0].offset.constant; - __int128 end = start + *bytes; - if (store.bytes) { - end = start + store.bytes->second; - start += store.bytes->first; - } - for (const core::Target &argument : - heap.info(state, args[store.dest.index]).targets) - if (argument.object == cell->targets[0].object) - storePastObject(call, store, cell->targets[0], argument.offset, - start, end); - } - // (Rewritten bytes were forgotten above.) - if (rewrites(store)) - continue; - if (store.may || !store.when.always()) { - // A store on some paths only: the old value or the new. - for (const core::Target &target : cell->targets) - heap.ensure(state, target.object); - Address weakCell = *cell; - core::Sym old = load(weakCell, cellType, nullptr); - core::Sym stored = value; - value = evidenced.contains( - static_cast(write.effect - selected.data())) - ? heap.mergePossible(state, old, value) - : heap.mergeWeak(state, old, value); - // A store on some result classes: a test of the result tells which - // value the cell holds (RFC 0030 §9.1, RFC 0031 §6.3). Not when the - // call may have released the old or the new value on some class it - // cannot name: the join keeps that release an alias's (§5.5), where - // either value alone would make it a possible finding the summary - // cannot place. - if (!store.may && !store.when.classes.empty() && !store.when.paramZero && - result != core::ZeroSym && value != old && value != stored && - !possiblyReleased.contains(old) && - !possiblyReleased.contains(stored)) { - core::PendingCase pending; - pending.kind = core::PendingCase::Kind::Stored; - for (core::ResultClass c : store.when.classes) - pending.classes.emplace_back(core::toString(c)); - pending.subject = value; - pending.stored = stored; - pending.previous = old; - heap.infoMut(state, result).pending.push_back(pending); - } - } - this->store(*cell, value, cellType, nullptr); - } - // RFC 0012: the strings the callee left, once its stores are made. - for (const auto &[string, target] : strings) { - if (!state.objects.contains(target.object)) - continue; - core::Term within = termAtCall(string->nulWithin); - auto at = within.known ? target.offset.plus(within) : std::nullopt; - if (!at) - continue; - std::optional from; - if (string->nulFrom) { - core::Term start = termAtCall(*string->nulFrom); - if (start.known) - from = target.offset.plus(start); - } - core::ObjectState &object = state.objects.at(target.object); - object.nulWithin = *at; - object.nulFrom = from; - } - // Code the callee ran that the analysis does not see (and the - // unknown-callee default of an incomplete summary) may write any global. - if (effects.incomplete || effects.unknownGlobals) { - core::ReleaseRecord record; - record.reason = core::ReleaseRecord::Reason::UnknownCallee; - record.where = here; - record.allPaths = false; - if (const FunctionDecl *direct = call.getDirectCallee()) - record.via = direct->getNameAsString(); - forgetGlobals(record); - } - // An incomplete summary adds the unknown-callee default (RFC 0030 §5.5): - // what the arguments reach may be written, and released where it may be. - if (effects.incomplete) - for (unsigned i = 0; i < call.getNumArgs() && i < args.size(); ++i) { - QualType argType = call.getArg(i)->getType(); - if (argType->isPointerType()) - havocArgument(call, args[i], - argType->getPointeeType().isConstQualified(), false); - } - if (effects.returns == core::FunctionEffects::Returns::Never) - state.unreachable = true; - // Cases the arguments left one result class for are decided now. - settlePending(run, state, result); - // Guard functions (RFC 0030 §9.2): what a class of the result implies. - for (const auto &[resultClass, paths] : effects.nonNullOn) - for (const core::SummaryPath &path : paths) - if (path.isParam() && path.steps.empty() && path.index < args.size()) { - core::PendingCase pending; - pending.classes = {std::string(core::toString(resultClass))}; - pending.kind = core::PendingCase::Kind::NonNull; - pending.subject = args[path.index]; - heap.infoMut(state, result).pending.push_back(pending); - } - return result; -} - -} // namespace weavec::analysis::engine diff --git a/lib/Analysis/EngineUnit.cpp b/lib/Analysis/EngineUnit.cpp deleted file mode 100644 index b140407b..00000000 --- a/lib/Analysis/EngineUnit.cpp +++ /dev/null @@ -1,806 +0,0 @@ -//===- EngineUnit.cpp - The object engine's unit driver -------------------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// RFC 0031 §3: the unit's call graph bottom up, summaries to a fixpoint per -// strongly connected component (publishing into a discarding adapter), then -// one authoritative pass per reported function. -// -//===----------------------------------------------------------------------===// - -#include "Engine.h" -#include "weavec/Analysis/KindTable.h" -#include "weavec/Analysis/SiteCollector.h" -#include "weavec/Core/Effects.h" -#include "weavec/Core/Scc.h" - -#include "clang/AST/RecursiveASTVisitor.h" -#include "clang/Basic/SourceManager.h" - -#include -#include -#include -#include - -using namespace clang; - -namespace weavec::analysis { - -//===----------------------------------------------------------------------===// -// ObjectEngine -//===----------------------------------------------------------------------===// - -ObjectEngine::ObjectEngine() = default; -ObjectEngine::~ObjectEngine() = default; - -void ObjectEngine::analyzeUnit(const EngineInput &engineInput, - LedgerAdapter &out) { - input = std::make_unique(engineInput); - unit = std::make_unique(*input, out); - const SiteIndex &sites = engineInput.sites; - const SourceManager &sm = engineInput.context.getSourceManager(); - if (engineInput.options.shouldReport) - unit->analyzeAll(engineInput.options.shouldReport); - else - unit->analyzeAll([&sites, &sm](const FunctionDecl &function) { - return sites.function(function) != nullptr || - sm.isInMainFile(sm.getExpansionLoc(function.getLocation())); - }); - exported = unit->exports(); -} - -UnitExports ObjectEngine::exports() { - return std::move(exported); -} - -const core::FunctionEffects * -ObjectEngine::summaryOf(const FunctionDecl &function) const { - if (!unit) - return nullptr; - auto it = unit->summaries.find(function.getCanonicalDecl()); - return it != unit->summaries.end() ? &it->second : nullptr; -} - -UnitExports ObjectEngine::discover(const EngineInput &input) { - LedgerAdapter discarding(input.context, LedgerAdapter::Mode::Discarding); - return engine::UnitRun(input, discarding).exports(); -} - -void ObjectEngine::dump(const FunctionDecl &function, llvm::raw_ostream &os) { - // `--dump-analysis` (unstable): the function's summary, and with - // WEAVEC_ENGINE_DUMP=3 its block states too. - os << "function '" << function.getNameAsString() << "':\n"; - if (const core::FunctionEffects *effects = summaryOf(function)) { - os << " summary:\n"; - std::string text = core::toText(*effects); - for (llvm::StringRef line : - llvm::split(llvm::StringRef(text).rtrim('\n'), '\n')) - if (!line.empty()) - os << " " << line << '\n'; - } - if (!unit) - return; - const char *level = std::getenv("WEAVEC_ENGINE_DUMP"); - if (level != nullptr && std::string_view(level) == "3") - unit->dump(function, os); -} - -namespace engine { - -//===----------------------------------------------------------------------===// -// UnitRun -//===----------------------------------------------------------------------===// - -/// RFC 0033 §9, RFC 0034 §7.1: a unit's default budget of work, a share -/// per site (its analysis costs a bounded multiple of compiling it). -static constexpr std::uint64_t MinUnitBudget = 20000000; -static constexpr std::uint64_t UnitBudgetPerSite = 400; - -UnitRun::UnitRun(const EngineInput &engineInput, LedgerAdapter &adapter) - : input(engineInput), authoritative(adapter), - discarding(engineInput.context, LedgerAdapter::Mode::Discarding) { - if (input.options.unitBudget) { - unitBudget = *input.options.unitBudget; - } else { - std::uint64_t sites = 0; - for (const SiteIndex::FunctionSites &function : input.sites.functions()) - sites += function.sites.size(); - unitBudget = std::max(MinUnitBudget, UnitBudgetPerSite * sites); - } -} - -std::uint32_t UnitRun::globalId(const VarDecl &var) { - return internGlobal(var); -} - -std::uint32_t UnitRun::internGlobal(const VarDecl &var) const { - const VarDecl *canonical = var.getCanonicalDecl(); - if (auto it = globalIds.find(canonical); it != globalIds.end()) - return it->second; - auto id = static_cast(globalList.size()); - globalList.push_back(canonical); - globalIds[canonical] = id; - return id; -} - -const VarDecl *UnitRun::globalDecl(std::uint32_t id) const { - return id < globalList.size() ? globalList[id] : nullptr; -} - -bool UnitRun::hasBody(const FunctionDecl &callee) const { - const FunctionDecl *definition = nullptr; - if (!callee.hasBody(definition) || definition == nullptr) - return false; - const SourceManager &sm = context().getSourceManager(); - return !sm.isInSystemHeader(sm.getExpansionLoc(definition->getLocation())); -} - -const core::FunctionEffects * -UnitRun::summaryOf(const FunctionDecl &callee) const { - auto it = summaries.find(callee.getCanonicalDecl()); - if (it != summaries.end()) - return &it->second; - // §7: a function another unit defines, in `weavec --whole-program`. - if (input.database == nullptr || hasBody(callee) || - !callee.isExternallyVisible() || callee.getIdentifier() == nullptr) - return nullptr; - std::string name = callee.getNameAsString(); - if (auto found = imported.find(name); found != imported.end()) - return &found->second; - const core::FunctionEffects *effects = input.database->findEffects(name); - if (effects == nullptr) - return nullptr; - return importEffects(name, *effects); -} - -const core::FunctionEffects * -UnitRun::importEffects(const std::string &key, - const core::FunctionEffects &effects) const { - if (auto found = imported.find(key); found != imported.end()) - return &found->second; - std::string name = key; - if (!globalsByName) { - globalsByName.emplace(); - for (const Decl *decl : context().getTranslationUnitDecl()->decls()) - if (const auto *var = dyn_cast(decl); - var != nullptr && var->hasGlobalStorage()) - globalsByName->try_emplace(portableName(*var), var->getCanonicalDecl()); - } - const GlobalNames &names = input.database->globals(); - core::FunctionEffects local = core::renumberGlobals( - effects, [&](std::uint32_t id) -> std::optional { - if (id >= names.size()) - return std::nullopt; - auto var = globalsByName->find(names.nameOf(id).str()); - if (var == globalsByName->end()) - return std::nullopt; - return internGlobal(*var->second); - }); - if (std::getenv("WEAVEC_ENGINE_DUMP") != nullptr) - llvm::errs() << "imported " << name << "\n" << core::toText(local); - return &imported.emplace(std::move(name), std::move(local)).first->second; -} - -std::string UnitRun::portableName(const VarDecl &var) const { - if (var.isExternallyVisible()) - return var.getNameAsString(); - const SourceManager &sm = context().getSourceManager(); - std::string source; - if (const auto entry = sm.getFileEntryRefForID(sm.getMainFileID())) - source = entry->getName().str(); - return source + "#" + var.getNameAsString(); -} - -namespace { -/// The functions a body calls: directly, or through a slot whose solution -/// names them (§3 step 1). -class CalleeCollector : public RecursiveASTVisitor { -public: - explicit CalleeCollector(const EngineInput &input) : input(input) {} - std::set callees; - // NOLINTBEGIN(readability-identifier-naming,bugprone-derived-method-shadowing-base-method): - // RecursiveASTVisitor's CRTP hooks are found by name. - bool VisitCallExpr(CallExpr *call) { - if (const FunctionDecl *callee = call->getDirectCallee()) { - callees.insert(callee->getCanonicalDecl()); - return true; - } - const core::SlotSolution *solution = input.slotSolution; - if (input.database != nullptr && input.database->programFacts) - solution = &input.database->programFacts->slots; - if (input.slotCollection == nullptr || solution == nullptr) - return true; - if (auto slot = input.slotCollection->calleeSlot(*call)) - for (const std::string &name : solution->resolveCall(*slot).targets) - if (const FunctionDecl *fn = input.slotCollection->function(name)) - callees.insert(fn->getCanonicalDecl()); - return true; - } - bool VisitDeclRefExpr(DeclRefExpr *ref) { - // A function whose address is taken is a possible callee of the - // indirect calls (ordered before its takers, which are few). - if (const auto *fn = dyn_cast(ref->getDecl())) - callees.insert(fn->getCanonicalDecl()); - return true; - } - // NOLINTEND(readability-identifier-naming,bugprone-derived-method-shadowing-base-method) - -private: - const EngineInput &input; -}; - -/// §4.5 D2, RFC 0030 §9.4: the fields and globals a value released by the -/// unit is loaded from. -class OwningCollector : public RecursiveASTVisitor { -public: - OwningCollector(const core::LibrarySpec &library, const KindTable &kinds, - llvm::DenseSet &slots) - : library(library), kinds(kinds), slots(slots) {} - - // (The function whose body is being visited.) - // NOLINTBEGIN(readability-identifier-naming,bugprone-derived-method-shadowing-base-method): - // RecursiveASTVisitor's CRTP hooks are found by name. - bool TraverseFunctionDecl(FunctionDecl *function) { - const FunctionDecl *outer = enclosing; - enclosing = function->doesThisDeclarationHaveABody() ? function : outer; - bool result = - RecursiveASTVisitor::TraverseFunctionDecl(function); - enclosing = outer; - return result; - } - bool VisitVarDecl(VarDecl *var) { - if (const Expr *init = var->getInit()) - noteSource(*var, *init); - return true; - } - bool VisitBinaryOperator(BinaryOperator *op) { - if (op->getOpcode() != BO_Assign) - return true; - if (const auto *ref = - dyn_cast(op->getLHS()->IgnoreParenImpCasts())) - if (const auto *var = dyn_cast(ref->getDecl())) - noteSource(*var, *op->getRHS()); - return true; - } - bool VisitCallExpr(CallExpr *call) { - const FunctionDecl *callee = call->getDirectCallee(); - if (callee == nullptr) - return true; - std::vector released; - if (auto match = governingLibraryEntry(*callee, library)) { - for (unsigned i = 0; i < call->getNumArgs(); ++i) - if (const core::LibraryParam *param = match->param(i); - param != nullptr && - (param->effect == core::LibraryParam::Effect::Release || - param->effect == core::LibraryParam::Effect::Realloc)) - released.push_back(i); - } else if (const OwnershipContract *contract = kinds.ownership(*callee)) { - for (const auto &argument : contract->arguments) - if (!argument.retains) - released.push_back(argument.index); - } - for (unsigned i : released) - if (i < call->getNumArgs()) - noteRelease(*call->getArg(i)); - // A function of the unit may release what it is handed: decided once - // every body is seen (`finish`). - if (released.empty() && callee->hasBody()) - for (unsigned i = 0; i < call->getNumArgs(); ++i) - if (call->getArg(i)->getType()->isPointerType()) - handed.push_back(Handed{.callee = callee->getCanonicalDecl(), - .index = i, - .arg = call->getArg(i), - .caller = enclosing}); - return true; - } - // NOLINTEND(readability-identifier-naming,bugprone-derived-method-shadowing-base-method) - void finish() { - // RFC 0030 §9.4: a slot is owning when some function releases a value - // loaded from it, itself or through a function it hands the value to - // that releases its parameter (`free_tree(t->left)`): the releasing - // parameters, to a fixpoint. - for (bool changed = true; changed;) { - changed = false; - for (const Handed &call : handed) - if (releasing.contains({call.callee, call.index}) && !call.counted) { - call.counted = true; - const FunctionDecl *outer = enclosing; - enclosing = call.caller; - noteRelease(*call.arg); - enclosing = outer; - changed = true; - } - for (const auto &[function, param] : pendingParams) - if (releasing.insert({function, param}).second) - changed = true; - pendingParams.clear(); - } - for (const Expr *expr : releasedExprs) - noteReleased(*expr, 0); - } - -private: - const core::LibrarySpec &library; - const KindTable &kinds; - llvm::DenseSet &slots; - std::vector releasedExprs; - /// A pointer argument handed to a function of the unit. - struct Handed { - const FunctionDecl *callee; - unsigned index; - const Expr *arg; - const FunctionDecl *caller; - mutable bool counted = false; - }; - std::vector handed; - /// The parameters (function, index) the unit's functions release. - std::set> releasing; - std::vector> pendingParams; - const FunctionDecl *enclosing = nullptr; - - /// `expr` is released: its slots are owning, and a parameter of the - /// function around it that it is makes that parameter releasing. - void noteRelease(const Expr &expr) { - releasedExprs.push_back(&expr); - if (enclosing == nullptr) - return; - if (const auto *ref = dyn_cast(expr.IgnoreParenCasts())) - if (const auto *param = dyn_cast(ref->getDecl())) - pendingParams.emplace_back(enclosing->getCanonicalDecl(), - param->getFunctionScopeIndex()); - } - llvm::DenseMap> sources; - - static const Decl *slotOf(const Expr &expr) { - const Expr *stripped = expr.IgnoreParenCasts(); - if (const auto *member = dyn_cast(stripped)) - return dyn_cast(member->getMemberDecl()); - if (const auto *ref = dyn_cast(stripped)) - if (const auto *var = dyn_cast(ref->getDecl()); - var != nullptr && var->hasGlobalStorage()) - return var; - if (const auto *subscript = dyn_cast(stripped)) - return slotOf(*subscript->getBase()); - return nullptr; - } - void noteSource(const VarDecl &var, const Expr &value) { - if (const Decl *slot = slotOf(value)) - sources[&var].push_back(slot->getCanonicalDecl()); - else if (const auto *ref = dyn_cast(value.IgnoreParenCasts())) - if (const auto *from = dyn_cast(ref->getDecl())) - sources[&var].push_back(from); - } - void noteReleased(const Expr &expr, int depth) { - if (depth > 4) - return; - if (const Decl *slot = slotOf(expr)) { - slots.insert(slot->getCanonicalDecl()); - return; - } - if (const auto *ref = dyn_cast(expr.IgnoreParenCasts())) - if (const auto *var = dyn_cast(ref->getDecl())) - noteVar(*var, depth); - } - void noteVar(const VarDecl &var, int depth) { - // (Locals copied into each other in a loop: each is followed once.) - if (depth > MaxCopyDepth || !followed.insert(&var).second) - return; - auto it = sources.find(&var); - if (it == sources.end()) - return; - for (const Decl *source : it->second) { - if (const auto *from = dyn_cast(source); - from != nullptr && !from->hasGlobalStorage()) { - noteVar(*from, depth + 1); - continue; - } - slots.insert(source); - } - } - static constexpr int MaxCopyDepth = 64; - llvm::DenseSet followed; -}; - -/// RFC 0031 §4.6: the unit's references to variables with static storage -/// and internal linkage, and those of them that only read a scalar value -/// (`table[1]`, `s.f` loaded) or sit in an unevaluated operand. A variable -/// no other reference reaches holds its initializer for the whole run. -class GlobalReads : public RecursiveASTVisitor { -public: - // NOLINTBEGIN(readability-identifier-naming,bugprone-derived-method-shadowing-base-method,readability-convert-member-functions-to-static): - // RecursiveASTVisitor's CRTP hooks are found by name. - bool VisitDeclRefExpr(DeclRefExpr *ref) { - if (const auto *var = dyn_cast(ref->getDecl()); - var != nullptr && var->hasGlobalStorage()) - references.push_back(ref); - return true; - } - bool VisitImplicitCastExpr(ImplicitCastExpr *cast) { - if (cast->getCastKind() != CK_LValueToRValue) - return true; - // Down through subscripts of arrays (not of pointers) and `.` members. - const Expr *lvalue = cast->getSubExpr()->IgnoreParens(); - while (true) { - if (const auto *subscript = dyn_cast(lvalue)) { - const auto *decay = - dyn_cast(subscript->getBase()->IgnoreParens()); - if (decay == nullptr || decay->getCastKind() != CK_ArrayToPointerDecay) - return true; - lvalue = decay->getSubExpr()->IgnoreParens(); - continue; - } - if (const auto *member = dyn_cast(lvalue); - member != nullptr && !member->isArrow()) { - lvalue = member->getBase()->IgnoreParens(); - continue; - } - break; - } - if (const auto *ref = dyn_cast(lvalue)) - reads.insert(ref); - return true; - } - bool TraverseUnaryExprOrTypeTraitExpr(UnaryExprOrTypeTraitExpr *) { - return true; // Unevaluated (a variable-length operand has no global). - } - // NOLINTEND(readability-identifier-naming,bugprone-derived-method-shadowing-base-method,readability-convert-member-functions-to-static) - - std::vector references; - llvm::DenseSet reads; -}; -} // namespace - -bool UnitRun::keepsInitializer(const VarDecl &var) const { - if (!initializerOnly) { - GlobalReads collector; - collector.TraverseDecl(context().getTranslationUnitDecl()); - llvm::DenseSet written; - for (const DeclRefExpr *ref : collector.references) - if (!collector.reads.contains(ref)) - written.insert(cast(ref->getDecl())->getCanonicalDecl()); - initializerOnly.emplace(); - for (const DeclRefExpr *ref : collector.references) { - const auto *read = cast(ref->getDecl())->getCanonicalDecl(); - if (!written.contains(read)) - initializerOnly->insert(read); - } - } - const VarDecl *canonical = var.getCanonicalDecl(); - return !canonical->isExternallyVisible() && - !canonical->getType().isVolatileQualified() && - initializerOnly->contains(canonical); -} - -void UnitRun::computeOwningSlots() { - OwningCollector collector(library(), input.kinds, owningSlots); - collector.TraverseDecl(context().getTranslationUnitDecl()); - collector.finish(); -} - -/// `component`'s members in a postorder of the call edges between them. -static std::vector -calleesFirst(const std::vector &component, - const std::vector> &adjacency) { - llvm::DenseSet members(component.begin(), component.end()); - llvm::DenseSet seen; - std::vector order; - for (unsigned root : component) { - if (!seen.insert(root).second) - continue; - std::vector> frames{{root, 0}}; - while (!frames.empty()) { - auto &[node, next] = frames.back(); - if (next < adjacency[node].size()) { - unsigned callee = adjacency[node][next++]; - if (members.contains(callee) && seen.insert(callee).second) - frames.emplace_back(callee, 0); - continue; - } - order.push_back(node); - frames.pop_back(); - } - } - return order; -} - -void UnitRun::analyzeAll( - const std::function &shouldReport) { - computeOwningSlots(); - // The unit's definitions. - std::vector definitions; - llvm::DenseMap index; - const SourceManager &sm = context().getSourceManager(); - for (const Decl *decl : context().getTranslationUnitDecl()->decls()) { - const auto *fn = dyn_cast(decl); - if (fn == nullptr || !fn->doesThisDeclarationHaveABody()) - continue; - if (sm.isInSystemHeader(sm.getExpansionLoc(fn->getLocation()))) - continue; - const FunctionDecl *canonical = fn->getCanonicalDecl(); - if (index.contains(canonical)) - continue; - index[canonical] = static_cast(definitions.size()); - definitions.push_back(fn); - } - // The call graph and its components, callees first. - std::vector> adjacency(definitions.size()); - for (unsigned i = 0; i < definitions.size(); ++i) { - CalleeCollector collector(input); - collector.TraverseStmt(definitions[i]->getBody()); - for (const FunctionDecl *callee : collector.callees) - if (auto it = index.find(callee); it != index.end()) - adjacency[i].push_back(it->second); - } - std::vector> components = - core::stronglyConnectedComponents(adjacency); - functionsLeft = std::max(1, definitions.size()); - for (std::vector &component : components) { - // A component's members run callees first (a postorder of its own - // edges), so a round sees the summaries its callees made in it rather - // than the last round's, and fewer rounds settle. - if (component.size() > 2) - component = calleesFirst(component, adjacency); - bool recursive = component.size() > 1; - if (!recursive) - for (unsigned callee : adjacency[component.front()]) - recursive = recursive || callee == component.front(); - if (recursive) { - // Summary rounds to a fixpoint (§3 step 2). - for (unsigned member : component) { - summaries[definitions[member]->getCanonicalDecl()] = {}; - unsettled.insert(definitions[member]->getCanonicalDecl()); - } - // RFC 0031 §6.4: after three rounds a summary that still changes - // widens (joined with the last round's, its moving integer bounds - // dropped); after eight the rest are incomplete. - static constexpr int WidenFrom = 3; - static constexpr int MaxRounds = 8; - // A member runs again only when a summary it calls changed since its - // last run: its summary is a function of theirs. - llvm::DenseMap> callers; - llvm::DenseSet members(component.begin(), component.end()); - for (unsigned member : component) - for (unsigned callee : adjacency[member]) - if (members.contains(callee)) - callers[callee].push_back(member); - llvm::DenseSet dirty = members; - // A summary that says unknown code may write any global has callers - // forget them all (§5.1: what they hold, and what they reach may be - // released), so its writes to one global say nothing more, except to - // the C library's own, which forgetting keeps; inside a component - // they climb a call edge a round and keep it from settling. - auto dropCoveredGlobals = [&](core::FunctionEffects &effects) { - if (!effects.unknownGlobals) - return; - auto covered = [&](const core::SummaryPath &path) { - if (!path.isGlobal()) - return false; - const VarDecl *var = globalDecl(path.index); - return var != nullptr && - !sm.isInSystemHeader(sm.getExpansionLoc(var->getLocation())); - }; - std::erase_if(effects.effects, [&](const core::PathEffect &effect) { - return effect.kind == core::PathEffect::Kind::Unknown && - covered(effect.path); - }); - std::erase_if(effects.stores, [&](const core::StoreEffect &store) { - return covered(store.dest); - }); - }; - for (int round = 0; round < MaxRounds && !dirty.empty(); ++round) { - for (unsigned member : component) { - if (!dirty.erase(member)) - continue; - const FunctionDecl *fn = definitions[member]; - // (One that spent the per-function budget keeps its incomplete - // summary: run again, it spends it again. A run cut short by its - // share of the unit's budget may get a larger share later.) - if (exhausted.contains(fn->getCanonicalDecl())) - continue; - FunctionRun run(*this, *fn, discarding, RunMode::Summary); - RunResult result = run.run(); - if (result.spentBudget) - exhausted.insert(fn->getCanonicalDecl()); - core::FunctionEffects &slot = summaries[fn->getCanonicalDecl()]; - dropCoveredGlobals(result.effects); - core::FunctionEffects next = - round >= WidenFrom ? core::widenEffects(slot, result.effects) - : std::move(result.effects); - if (!(next == slot)) { - slot = std::move(next); - for (unsigned caller : callers[member]) - dirty.insert(caller); - } - } - } - const bool stable = dirty.empty(); - // (One that did not settle may return even where its last round - // said it never does.) - if (!stable) - for (unsigned member : component) { - core::FunctionEffects &slot = - summaries[definitions[member]->getCanonicalDecl()]; - slot.incomplete = "the recursive summaries did not converge"; - if (slot.returns == core::FunctionEffects::Returns::Never) - slot.returns = core::FunctionEffects::Returns::May; - } - } - // The authoritative pass of each member; its summary stands for the - // members outside a cycle (§3 step 3). - for (unsigned member : component) { - const FunctionDecl *fn = definitions[member]; - bool report = shouldReport(*fn); - if (report) - authoritative.beginFunction(*fn); - // A member whose summary round spent the per-function budget spends - // it in its authoritative pass too, which then publishes nothing: it - // is over budget without that run. - RunResult result; - if (exhausted.contains(fn->getCanonicalDecl())) { - result.overBudget = true; - result.transfers = std::numeric_limits::max(); - result.effects = summaries[fn->getCanonicalDecl()]; - } else { - FunctionRun run(*this, *fn, report ? authoritative : discarding, - report ? RunMode::Authoritative : RunMode::Summary); - result = run.run(); - } - if (functionsLeft > 1) - --functionsLeft; - transfersOf[fn->getCanonicalDecl()] = result.transfers; - workOf[fn->getCanonicalDecl()] = - result.overBudget ? std::numeric_limits::max() - : result.work; - if (result.overBudget) { - overBudget.insert(fn->getCanonicalDecl()); - if (report) - authoritative.overBudget(*fn); - } - if (!recursive) - summaries[fn->getCanonicalDecl()] = std::move(result.effects); - if (std::getenv("WEAVEC_ENGINE_DUMP") != nullptr) - llvm::errs() << "summary " << fn->getNameAsString() << "\n" - << core::toText(summaries[fn->getCanonicalDecl()]); - } - unsettled.clear(); - } - // RFC 0030 §7.6: only a record the unit alone can make (defined in its - // main file) has every store in view; a header's is another unit's too, - // whose stores only the link step would see (A3, not verified). - if (input.inferred != nullptr) - for (const ResolvedCandidate &candidate : - input.inferred->resolvedCandidates()) - if (candidate.record != nullptr && candidate.pointer != nullptr && - candidate.count != nullptr && - sm.isInMainFile(sm.getExpansionLoc(candidate.record->getLocation()))) - standing.push_back(&candidate); - inferInvariants(definitions, shouldReport); - serveContextRequests(shouldReport); -} - -namespace { -/// §7: what a unit's definitions call that it does not define, and which -/// of its functions have their address taken. -class InterfaceCollector : public RecursiveASTVisitor { -public: - explicit InterfaceCollector(const ASTContext &context) : context(context) {} - std::set called; - std::set addressTaken; - std::set indirectTypes; - - // Pre-order: a call is visited before the reference that names its - // callee, so that reference is known to be no address taken. - // NOLINTBEGIN(readability-identifier-naming,bugprone-derived-method-shadowing-base-method): - // RecursiveASTVisitor's CRTP hooks are found by name. - bool VisitCallExpr(CallExpr *call) { - if (const FunctionDecl *callee = call->getDirectCallee()) { - called.insert(callee->getCanonicalDecl()); - directCallees.insert(call->getCallee()->IgnoreParenImpCasts()); - return true; - } - QualType type = call->getCallee()->getType(); - if (type->isPointerType()) - type = type->getPointeeType(); - if (std::string key = functionTypeKey(type, context); !key.empty()) - indirectTypes.insert(std::move(key)); - return true; - } - bool VisitDeclRefExpr(DeclRefExpr *ref) { - if (const auto *fn = dyn_cast(ref->getDecl()); - fn != nullptr && !directCallees.contains(ref)) - addressTaken.insert(fn->getCanonicalDecl()); - return true; - } - // NOLINTEND(readability-identifier-naming,bugprone-derived-method-shadowing-base-method) - -private: - const ASTContext &context; - std::set directCallees; -}; -} // namespace - -const std::vector & -UnitRun::localCandidates(const std::string &typeKey) const { - if (!candidatesByType) { - candidatesByType.emplace(); - InterfaceCollector collector(context()); - collector.TraverseDecl(context().getTranslationUnitDecl()); - for (const FunctionDecl *fn : collector.addressTaken) - if (std::string key = functionTypeKey(fn->getType(), context()); - !key.empty()) - (*candidatesByType)[key].push_back(fn); - } - static const std::vector None; - auto it = candidatesByType->find(typeKey); - return it != candidatesByType->end() ? it->second : None; -} - -UnitExports UnitRun::exports() { - UnitExports exports; - const SourceManager &sm = context().getSourceManager(); - if (const auto entry = sm.getFileEntryRefForID(sm.getMainFileID())) - exports.source = entry->getName().str(); - InterfaceCollector collector(context()); - collector.TraverseDecl(context().getTranslationUnitDecl()); - // Globals by the export table's numbering. - auto toExport = [&](std::uint32_t id) -> std::optional { - const VarDecl *var = globalDecl(id); - if (var == nullptr) - return std::nullopt; - return exports.globals.idFor(portableName(*var)); - }; - for (const Decl *decl : context().getTranslationUnitDecl()->decls()) { - const auto *fn = dyn_cast(decl); - if (fn == nullptr || !fn->doesThisDeclarationHaveABody() || fn->isMain() || - fn->getIdentifier() == nullptr || - sm.isInSystemHeader(sm.getExpansionLoc(fn->getLocation()))) - continue; - const bool external = fn->isExternallyVisible(); - const bool taken = collector.addressTaken.contains(fn->getCanonicalDecl()); - if (!external && !taken) - continue; - ExportedFunction exported; - exported.typeKey = functionTypeKey(fn->getType(), context()); - exported.external = external; - exported.addressTaken = taken; - if (auto it = summaries.find(fn->getCanonicalDecl()); it != summaries.end()) - exported.effects = core::renumberGlobals(it->second, toExport); - exports.functions[fn->getNameAsString()] = std::move(exported); - } - // What the predefines declare is the check prelude's (RFC 0030 §10.2): the - // runtime entry points its helpers call are no callee of the program. - auto inPredefines = [&](const FunctionDecl &fn) { - return sm.isWrittenInBuiltinFile(sm.getExpansionLoc(fn.getLocation())); - }; - for (const FunctionDecl *callee : collector.called) - if (!hasBody(*callee) && callee->isExternallyVisible() && - callee->getIdentifier() != nullptr && callee->getBuiltinID() == 0 && - !inPredefines(*callee)) - exports.imports.insert(callee->getNameAsString()); - // A function of another unit this one only refers to (a callback it - // hands out): its summary is what calls through the value apply. - for (const FunctionDecl *referenced : collector.addressTaken) - if (!hasBody(*referenced) && referenced->isExternallyVisible() && - referenced->getIdentifier() != nullptr && - referenced->getBuiltinID() == 0) - exports.imports.insert(referenced->getNameAsString()); - exports.indirectTypes = std::move(collector.indirectTypes); - // §7 *Amendment (cross-unit contexts)*. - exports.contextRequests = contextRequests; - for (const auto &[request, effects] : servedContexts) - exports.contextEffects[request] = core::renumberGlobals(effects, toExport); - return exports; -} - -void UnitRun::dump(const FunctionDecl &function, llvm::raw_ostream &os) { - FunctionRun run(*this, function, discarding, RunMode::Summary); - (void)run.run(); - run.dump(os); -} - -} // namespace engine -} // namespace weavec::analysis diff --git a/lib/Analysis/KindInference.cpp b/lib/Analysis/KindInference.cpp deleted file mode 100644 index 33761f17..00000000 --- a/lib/Analysis/KindInference.cpp +++ /dev/null @@ -1,1020 +0,0 @@ -//===- KindInference.cpp - Inferred pointer kinds (RFC 0030) --------------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// §7.3: the Single-valid judgement, the greatest fixpoint over slots, -// results, static parameters and variables, and the table it fills. The -// walk that collects the stores is in KindInferenceCollect.cpp, §7.5 in -// KindInferenceMustAccess.cpp, §7.6 in KindInferenceFields.cpp. -// -//===----------------------------------------------------------------------===// - -#include "weavec/Analysis/KindInference.h" - -#include "KindInferenceImpl.h" -#include "weavec/Analysis/AttributeReader.h" -#include "weavec/Analysis/ProgramDatabase.h" -#include "weavec/Analysis/SlotCollector.h" - -#include "clang/AST/Attr.h" -#include "clang/AST/RecordLayout.h" -#include "clang/AST/Type.h" -#include "clang/Basic/LangOptions.h" - -#include "llvm/ADT/STLExtras.h" - -#include -#include -#include - -namespace weavec::analysis { - -// -- Helpers ------------------------------------------------------------------ - -ValueWidth meet(const ValueWidth &a, const ValueWidth &b) { - if (a.nullOnly) - return b.nullOnly ? a - : ValueWidth{.bytes = b.bytes, - .nullOnly = false, - .nullability = core::Nullability::Nullable, - .why = b.why}; - if (b.nullOnly) - return meet(b, a); - ValueWidth out; - out.nullability = core::join(a.nullability, b.nullability); - if (!a.bytes || !b.bytes) { - out.why = !a.bytes ? a.why : b.why; - return out; - } - out.bytes = std::min(*a.bytes, *b.bytes); - return out; -} - -bool isFlexibleArrayMember(const clang::FieldDecl &field, - const clang::ASTContext &context) { - const clang::RecordDecl *record = field.getParent(); - if (record == nullptr || record->isUnion()) - return false; - const clang::FieldDecl *last = nullptr; - for (const clang::FieldDecl *each : record->fields()) - last = each; - if (last != &field && last != field.getCanonicalDecl()) - return false; - const clang::ArrayType *array = context.getAsArrayType(field.getType()); - if (array == nullptr) - return false; - const auto *constant = llvm::dyn_cast(array); - if (constant == nullptr) - return true; // `T f[]` - const std::uint64_t size = constant->getSize().getZExtValue(); - using Level = clang::LangOptions::StrictFlexArraysLevelKind; - switch (context.getLangOpts().getStrictFlexArraysLevel()) { - case Level::Default: - return true; // §7.4: every trailing array at level 0. - case Level::OneZeroOrIncomplete: - return size <= 1; - case Level::ZeroOrIncomplete: - return size == 0; - case Level::IncompleteOnly: - return false; - } - return true; -} - -std::uint64_t objectWidth(clang::QualType pointee, - const clang::ASTContext &context) { - if (pointee.isNull()) - return 0; - pointee = pointee.getCanonicalType(); - if (pointee->isVoidType()) - return 1; - if (pointee->isIncompleteType() || pointee->isFunctionType() || - pointee->isDependentType() || pointee->isSizelessType()) - return 0; - if (pointee->isVariableArrayType()) { - const clang::ArrayType *array = context.getAsArrayType(pointee); - return array != nullptr ? objectWidth(array->getElementType(), context) : 0; - } - if (const auto *record = pointee->getAsRecordDecl()) { - const clang::FieldDecl *last = nullptr; - for (const clang::FieldDecl *field : record->fields()) - last = field; - if (last != nullptr && isFlexibleArrayMember(*last, context)) { - const clang::ASTRecordLayout &layout = context.getASTRecordLayout(record); - return layout.getFieldOffset(last->getFieldIndex()) / - context.getCharWidth(); - } - } - return static_cast( - context.getTypeSizeInChars(pointee).getQuantity()); -} - -bool isObjectPointer(clang::QualType type) { - return !type.isNull() && type->isPointerType() && - !type->isFunctionPointerType(); -} - -bool isCharacter(clang::QualType type) { - return !type.isNull() && type->isCharType(); -} - -const clang::Expr *stripPointerCasts(const clang::Expr *expr) { - while (expr != nullptr) { - expr = expr->IgnoreParens(); - const auto *cast = llvm::dyn_cast(expr); - if (cast == nullptr) - break; - // Only conversions from one pointer type to another. - const clang::CastKind kind = cast->getCastKind(); - if ((kind != clang::CK_BitCast && kind != clang::CK_NoOp && - kind != clang::CK_AddressSpaceConversion) || - !cast->getSubExpr()->getType()->isPointerType()) - break; - expr = cast->getSubExpr(); - } - return expr; -} - -const clang::DeclRefExpr *calleeReference(const clang::CallExpr &call) { - const clang::Expr *callee = call.getCallee(); - while (callee != nullptr) { - callee = callee->IgnoreParenImpCasts(); - const auto *unary = llvm::dyn_cast(callee); - if (unary == nullptr || (unary->getOpcode() != clang::UO_Deref && - unary->getOpcode() != clang::UO_AddrOf && - unary->getOpcode() != clang::UO_Plus)) - break; - callee = unary->getSubExpr(); - } - const auto *ref = llvm::dyn_cast_or_null(callee); - return ref != nullptr && llvm::isa(ref->getDecl()) - ? ref - : nullptr; -} - -const clang::ParmVarDecl *parameterOf(const clang::Expr *expr) { - if (expr == nullptr) - return nullptr; - const auto *ref = - llvm::dyn_cast(expr->IgnoreParenImpCasts()); - return ref != nullptr ? llvm::dyn_cast(ref->getDecl()) - : nullptr; -} - -const clang::VarDecl *variableOf(const clang::Expr *expr) { - if (expr == nullptr) - return nullptr; - const auto *ref = - llvm::dyn_cast(expr->IgnoreParenImpCasts()); - return ref != nullptr ? llvm::dyn_cast(ref->getDecl()) - : nullptr; -} - -std::string recordName(const clang::RecordDecl &record) { - if (!record.getName().empty()) - return record.getNameAsString(); - if (const clang::TypedefNameDecl *name = record.getTypedefNameForAnonDecl()) - return name->getNameAsString(); - return ""; -} - -std::string fieldName(const clang::FieldDecl &field) { - return recordName(*field.getParent()) + "." + field.getNameAsString(); -} - -std::string recordKey(const clang::RecordDecl &record, - const clang::ASTContext &context) { - std::string key = - recordTypeKey(context.getCanonicalTagType(&record), context); - if (!key.empty()) - return key; - // An anonymous struct is named by its typedef, which no tag can spell. - if (const clang::TypedefNameDecl *name = record.getTypedefNameForAnonDecl()) - return name->getNameAsString(); - return {}; -} - -// -- Nodes -------------------------------------------------------------------- - -unsigned KindInferenceState::addNode(KindNode::Kind kind, - const clang::Decl *decl, - clang::QualType pointee) { - const auto index = static_cast(nodes.size()); - KindNode node; - node.kind = kind; - node.decl = decl; - node.pointee = pointee; - node.needed = objectWidth(pointee, context); - nodes.push_back(std::move(node)); - if (kind != KindNode::Kind::ParamEntry) - nodeOf[decl] = index; - return index; -} - -std::string KindInferenceState::nodeName(unsigned index) const { - const KindNode &node = nodes[index]; - if (const auto *field = llvm::dyn_cast(node.decl)) - return "'" + fieldName(*field) + "'"; - if (const auto *function = llvm::dyn_cast(node.decl)) - return "the result of '" + function->getNameAsString() + "'"; - if (const auto *named = llvm::dyn_cast(node.decl)) - return "'" + named->getNameAsString() + "'"; - return "a slot"; -} - -std::optional -KindInferenceState::slotNode(const clang::Expr *lvalue) const { - if (lvalue == nullptr) - return std::nullopt; - lvalue = lvalue->IgnoreParens(); - const clang::Decl *decl = nullptr; - if (const auto *ref = llvm::dyn_cast(lvalue)) { - if (const auto *variable = llvm::dyn_cast(ref->getDecl())) - decl = variable->getCanonicalDecl(); - } else if (const auto *member = llvm::dyn_cast(lvalue)) { - if (const auto *field = - llvm::dyn_cast(member->getMemberDecl())) - decl = field->getCanonicalDecl(); - } - if (decl == nullptr) - return std::nullopt; - const auto found = nodeOf.find(decl); - if (found == nodeOf.end()) - return std::nullopt; - return found->second; -} - -ValueWidth KindInferenceState::loadOf(unsigned index) const { - const KindNode &node = nodes[index]; - if (node.demoted) - return ValueWidth::unknown("a load from " + nodeName(index) + - ", which is unknown"); - switch (node.kind) { - case KindNode::Kind::Field: - case KindNode::Kind::Global: { - // A slot is nullable unless declared otherwise: zero-initialised - // objects hold null in every pointer field. - const KindEntry *entry = nullptr; - if (const auto *field = llvm::dyn_cast(node.decl)) - entry = kinds.field(*field); - else if (const auto *variable = llvm::dyn_cast(node.decl)) - entry = kinds.variable(*variable); - return ValueWidth::of(node.needed, - entry != nullptr && entry->declaresNonnull() - ? core::Nullability::Nonnull - : core::Nullability::Nullable); - } - case KindNode::Kind::Result: { - const auto *function = llvm::cast(node.decl); - const KindEntry *entry = kinds.result(*function); - core::Nullability nullability = - node.nonnull ? core::Nullability::Nonnull : core::Nullability::Nullable; - if (entry != nullptr && entry->nullabilityLevel) - nullability = entry->kind.nullability; - return ValueWidth::of(node.needed, nullability); - } - case KindNode::Kind::ParamEntry: - return ValueWidth::of(node.needed, node.nonnull - ? core::Nullability::Nonnull - : core::Nullability::Nullable); - case KindNode::Kind::Value: - return ValueWidth::of(node.least, node.nonnull - ? core::Nullability::Nonnull - : core::Nullability::Nullable); - } - return ValueWidth::unknown("an unmodelled load"); -} - -std::optional -KindInferenceState::constantOf(const clang::Expr *expr) const { - if (expr == nullptr || expr->isValueDependent() || - !expr->getType()->isIntegralOrEnumerationType()) - return std::nullopt; - if (const auto value = expr->getIntegerConstantExpr(context)) - return value->tryExtValue(); - return std::nullopt; -} - -ValueWidth -KindInferenceState::declaredWidth(const KindEntry &entry, - clang::QualType pointee, - const clang::CallExpr *call) const { - const core::Nullability nullability = entry.kind.nullability; - const std::uint64_t element = objectWidth(pointee, context); - const core::ExtentTerm &term = entry.kind.extent; - const auto evaluate = - [&](const core::ExtentTerm &extent) -> std::optional { - if (extent.isConstant()) - return extent.offset; - if (call == nullptr || extent.path->root != core::ExtentPath::Root::Param || - extent.path->param >= call->getNumArgs()) - return std::nullopt; - const auto value = constantOf(call->getArg(extent.path->param)); - if (!value) - return std::nullopt; - return (*value * extent.scale) + extent.offset; - }; - switch (entry.kind.shape) { - case core::PointerShape::Single: - case core::PointerShape::NulTerminated: - return ValueWidth::of(element, nullability); - case core::PointerShape::Counted: - case core::PointerShape::Sized: { - auto value = evaluate(term); - if (value && entry.extentFactor && call != nullptr && - *entry.extentFactor < call->getNumArgs()) { - const auto factor = constantOf(call->getArg(*entry.extentFactor)); - value = factor ? std::optional(*value * *factor) : std::nullopt; - } - if (!value || *value <= 0) - return ValueWidth::unknown("a declared extent that may be empty"); - const auto count = static_cast(*value); - return ValueWidth::of(entry.kind.shape == core::PointerShape::Counted - ? count * element - : count, - nullability); - } - case core::PointerShape::EndedBy: - return ValueWidth::unknown("an ended-by pointer, which may be at its end"); - case core::PointerShape::Unknown: - break; - } - return ValueWidth::unknown("a pointer of unknown kind"); -} - -ValueWidth KindInferenceState::libraryValue(const core::LibraryResult &result, - const core::LibraryMatch &match, - const clang::CallExpr &call, - clang::QualType pointee) const { - const core::Nullability nullability = - result.null == core::LibraryResult::Null::Never - ? core::Nullability::Nonnull - : core::Nullability::Nullable; - const std::uint64_t element = objectWidth(pointee, context); - const std::string name = "'" + match.entry->name + "'"; - const auto argument = [&](unsigned rowArg) -> const clang::Expr * { - const int index = match.callArgument(rowArg); - if (index < 0 || static_cast(index) >= call.getNumArgs()) - return nullptr; - return call.getArg(static_cast(index)); - }; - switch (result.kind) { - case core::LibraryResult::Kind::Fresh: - case core::LibraryResult::Kind::Static: { - if (!result.extent) - return ValueWidth::of(result.string ? element : 0, nullability); - const auto bytes = result.extent->evaluate(core::LibTerm::Values{ - .argument = - [&](unsigned rowArg) { return constantOf(argument(rowArg)); }, - .stringLength = nullptr, - .formatLength = nullptr, - .macro = nullptr}); - if (bytes && *bytes >= 0) - return ValueWidth::of(static_cast(*bytes), nullability); - if (result.string) - return ValueWidth::of(element, nullability); - return ValueWidth::unknown("an allocation by " + name + - " of a size that is not constant"); - } - case core::LibraryResult::Kind::Arg: { - const clang::Expr *arg = argument(result.arg); - if (arg == nullptr) - return ValueWidth::unknown("the result of " + name); - ValueWidth value = judge(arg); - if (value.nullOnly) - return ValueWidth::unknown("the result of " + name + - " for a null argument"); - return value; - } - case core::LibraryResult::Kind::Interior: - case core::LibraryResult::Kind::InteriorState: - if (result.string) - return ValueWidth::of(element, nullability); - return ValueWidth::unknown("a pointer into another object, from " + name); - case core::LibraryResult::Kind::Unknown: - case core::LibraryResult::Kind::Void: - case core::LibraryResult::Kind::Int: - break; - } - return ValueWidth::unknown("a pointer of unknown provenance, from " + name); -} - -// -- The Single-valid judgement (§7.3) -// ----------------------------------------- - -/// The bytes of a declared object of `type`: `sizeof`, one element of a VLA, -/// zero for an incomplete type. -static std::uint64_t objectSize(clang::QualType type, - const clang::ASTContext &context) { - if (type.isNull() || type->isIncompleteType() || type->isFunctionType()) - return 0; - if (type->isVariableArrayType()) { - const clang::ArrayType *array = context.getAsArrayType(type); - return array != nullptr ? objectSize(array->getElementType(), context) : 0; - } - return static_cast( - context.getTypeSizeInChars(type).getQuantity()); -} - -ValueWidth KindInferenceState::judge(const clang::Expr *expr) const { - if (expr == nullptr) - return ValueWidth::unknown("an unmodelled expression"); - expr = expr->IgnoreParens(); - if (!expr->isValueDependent() && - expr->isNullPointerConstant(context, - clang::Expr::NPC_ValueDependentIsNotNull) != - clang::Expr::NPCK_NotNull) - return ValueWidth::null(); - if (const auto *cast = llvm::dyn_cast(expr)) { - switch (cast->getCastKind()) { - case clang::CK_NullToPointer: - return ValueWidth::null(); - case clang::CK_ArrayToPointerDecay: - return judgeArray(cast->getSubExpr()); - case clang::CK_LValueToRValue: - return judgeLoad(cast->getSubExpr()); - case clang::CK_IntegralToPointer: - return ValueWidth::unknown("a conversion from an integer"); - case clang::CK_BitCast: - case clang::CK_NoOp: - case clang::CK_AddressSpaceConversion: - // §7.4 rule 4: a conversion keeps the width it had; whether that - // covers the new pointee is the caller's comparison. - return judge(cast->getSubExpr()); - default: - return ValueWidth::unknown("an unmodelled conversion"); - } - } - if (const auto *unary = llvm::dyn_cast(expr)) { - switch (unary->getOpcode()) { - case clang::UO_AddrOf: { - ValueWidth address = judgeAddress(unary->getSubExpr()); - if (address.bytes) - address.nullability = core::Nullability::Nonnull; - return address; - } - case clang::UO_Extension: - return judge(unary->getSubExpr()); - default: - return ValueWidth::unknown("pointer arithmetic"); - } - } - if (const auto *binary = llvm::dyn_cast(expr)) { - if (binary->getOpcode() == clang::BO_Assign || - binary->getOpcode() == clang::BO_Comma) - return judge(binary->getRHS()); - return ValueWidth::unknown("pointer arithmetic"); - } - if (const auto *conditional = - llvm::dyn_cast(expr)) - return meet(judge(conditional->getTrueExpr()), - judge(conditional->getFalseExpr())); - if (const auto *opaque = llvm::dyn_cast(expr)) - return opaque->getSourceExpr() != nullptr - ? judge(opaque->getSourceExpr()) - : ValueWidth::unknown("an unmodelled expression"); - if (const auto *call = llvm::dyn_cast(expr)) - return judgeCall(*call); - if (const auto *generic = llvm::dyn_cast(expr); - generic != nullptr && !generic->isResultDependent()) - return judge(generic->getResultExpr()); - if (const auto *choose = llvm::dyn_cast(expr)) - return judge(choose->getChosenSubExpr()); - if (llvm::isa(expr)) - return ValueWidth::unknown("a value from va_arg"); - if (expr->isGLValue()) - return judgeLoad(expr); - return ValueWidth::unknown("an unmodelled expression"); -} - -ValueWidth KindInferenceState::judgeLoad(const clang::Expr *lvalue) const { - lvalue = lvalue->IgnoreParens(); - if (const auto node = slotNode(lvalue)) - return loadOf(*node); - if (const auto *ref = llvm::dyn_cast(lvalue)) { - if (const auto *variable = llvm::dyn_cast(ref->getDecl())) { - const KindEntry *entry = - variable->hasGlobalStorage() ? kinds.variable(*variable) : nullptr; - if (entry != nullptr && entry->hasDeclaredShape()) - return declaredWidth(*entry, variable->getType()->getPointeeType(), - nullptr); - return ValueWidth::unknown("a load from '" + variable->getNameAsString() + - "'"); - } - return ValueWidth::unknown("an unmodelled load"); - } - if (const auto *member = llvm::dyn_cast(lvalue)) { - if (const auto *field = - llvm::dyn_cast(member->getMemberDecl())) { - if (const KindEntry *entry = kinds.field(*field); - entry != nullptr && entry->hasDeclaredShape()) - return declaredWidth(*entry, field->getType()->getPointeeType(), - nullptr); - return ValueWidth::unknown("a load from '" + fieldName(*field) + "'"); - } - return ValueWidth::unknown("an unmodelled load"); - } - if (const auto *subscript = - llvm::dyn_cast(lvalue)) { - const auto *decay = llvm::dyn_cast( - subscript->getBase()->IgnoreParens()); - if (decay != nullptr && - decay->getCastKind() == clang::CK_ArrayToPointerDecay) - return ValueWidth::unknown("a load from an array of pointers"); - } - return ValueWidth::unknown("a load through a pointer"); -} - -/// Whether the object a member access reads is there: `x.f` on a named -/// object, or `p->f` with `p` Single-valid for the record. -static ValueWidth memberBase(const KindInferenceState &state, - const clang::MemberExpr &member) { - if (!member.isArrow()) - return state.judgeAddress(member.getBase()); - ValueWidth base = state.judge(member.getBase()); - const clang::QualType record = member.getBase()->getType()->getPointeeType(); - if (!base.covers(objectWidth(record, state.context)) || base.nullOnly) - return ValueWidth::unknown("a member of a pointer that may not reach it"); - return base; -} - -ValueWidth KindInferenceState::judgeAddress(const clang::Expr *lvalue) const { - lvalue = lvalue->IgnoreParens(); - const auto nonnull = [](std::uint64_t bytes) { - return ValueWidth::of(bytes, core::Nullability::Nonnull); - }; - if (const auto *ref = llvm::dyn_cast(lvalue)) { - if (const auto *variable = llvm::dyn_cast(ref->getDecl())) - return nonnull(objectSize(variable->getType(), context)); - return ValueWidth::unknown("the address of a function"); - } - if (const auto *member = llvm::dyn_cast(lvalue)) { - const auto *field = - llvm::dyn_cast(member->getMemberDecl()); - if (field == nullptr) - return ValueWidth::unknown("an unmodelled member"); - ValueWidth base = memberBase(*this, *member); - if (!base.bytes) - return base; - if (isFlexibleArrayMember(*field, context)) - return nonnull(0); - return nonnull(objectSize(field->getType(), context)); - } - if (const auto *subscript = - llvm::dyn_cast(lvalue)) { - // `&a[c]` inside an array object: what is left of it. - const auto *decay = llvm::dyn_cast( - subscript->getBase()->IgnoreParens()); - const auto index = constantOf(subscript->getIdx()); - if (decay == nullptr || - decay->getCastKind() != clang::CK_ArrayToPointerDecay || !index || - *index < 0) - return ValueWidth::unknown("pointer arithmetic"); - const ValueWidth array = judgeArray(decay->getSubExpr()); - const std::uint64_t element = objectSize(subscript->getType(), context); - const auto offset = static_cast(*index) * element; - if (!array.bytes || *array.bytes == 0 || offset >= *array.bytes) - return ValueWidth::unknown("pointer arithmetic"); - return nonnull(*array.bytes - offset); - } - if (const auto *unary = llvm::dyn_cast(lvalue); - unary != nullptr && unary->getOpcode() == clang::UO_Deref) - return judge(unary->getSubExpr()); - if (llvm::isa(lvalue)) - return nonnull(objectSize(lvalue->getType(), context)); - if (llvm::isa(lvalue) || - llvm::isa(lvalue)) - return judgeArray(lvalue); - return ValueWidth::unknown("the address of an unmodelled object"); -} - -ValueWidth KindInferenceState::judgeArray(const clang::Expr *array) const { - array = array->IgnoreParens(); - const auto nonnull = [](std::uint64_t bytes) { - return ValueWidth::of(bytes, core::Nullability::Nonnull); - }; - if (const auto *literal = llvm::dyn_cast(array)) - return nonnull(literal->getByteLength() + literal->getCharByteWidth()); - if (const auto *predefined = llvm::dyn_cast(array)) { - if (const clang::StringLiteral *name = predefined->getFunctionName()) - return nonnull(name->getByteLength() + name->getCharByteWidth()); - return nonnull(1); - } - if (const auto *ref = llvm::dyn_cast(array)) { - if (const auto *variable = llvm::dyn_cast(ref->getDecl())) - return nonnull(objectSize(variable->getType(), context)); - return ValueWidth::unknown("an unmodelled array"); - } - if (const auto *member = llvm::dyn_cast(array)) { - const auto *field = - llvm::dyn_cast(member->getMemberDecl()); - if (field == nullptr) - return ValueWidth::unknown("an unmodelled member"); - ValueWidth base = memberBase(*this, *member); - if (!base.bytes) - return base; - // §7.4: a flexible array's extent never comes from its declared bound. - if (isFlexibleArrayMember(*field, context)) - return nonnull(0); - return nonnull(objectSize(field->getType(), context)); - } - if (llvm::isa(array)) - return nonnull(objectSize(array->getType(), context)); - if (llvm::isa(array)) - return judgeAddress(array); // a row of a multi-dimensional array - if (const auto *unary = llvm::dyn_cast(array); - unary != nullptr && unary->getOpcode() == clang::UO_Deref) - return judge(unary->getSubExpr()); - return ValueWidth::unknown("an array the analysis cannot name"); -} - -ValueWidth KindInferenceState::judgeCall(const clang::CallExpr &call) const { - const clang::QualType pointee = call.getType()->getPointeeType(); - const clang::FunctionDecl *callee = call.getDirectCallee(); - if (callee == nullptr) { - // §9.3: through a closed slot, the join of the targets' results; through - // an open one, the A3 default of a function outside the unit. - if (options.slots == nullptr || options.slotSolution == nullptr) - return ValueWidth::unknown("the result of an indirect call"); - const auto slot = options.slots->calleeSlot(call); - if (!slot) - return ValueWidth::unknown("the result of an indirect call"); - const core::CallResolution resolution = - options.slotSolution->resolveCall(*slot); - switch (resolution.kind) { - case core::IndirectCallKind::OpenKnown: - case core::IndirectCallKind::OpenUnknown: - return ValueWidth::of(objectWidth(pointee, context)); - case core::IndirectCallKind::ClosedEmpty: - return ValueWidth::unknown("a call through a pointer that is null"); - case core::IndirectCallKind::ClosedSingle: - case core::IndirectCallKind::ClosedJoin: - break; - } - std::optional joined; - for (const std::string &target : resolution.targets) { - const clang::FunctionDecl *function = options.slots->function(target); - ValueWidth value = ValueWidth::unknown("the result of an indirect call"); - if (function != nullptr) { - const auto found = nodeOf.find(function->getCanonicalDecl()); - value = found != nodeOf.end() - ? loadOf(found->second) - : ValueWidth::of(objectWidth(pointee, context)); - } - joined = joined ? meet(*joined, value) : value; - } - return joined ? *joined - : ValueWidth::unknown("the result of an indirect call"); - } - if (const auto match = governingLibraryEntry(*callee, library)) - return libraryValue(match->entry->result, *match, call, pointee); - const KindEntry *entry = kinds.result(*callee); - if (entry != nullptr && entry->hasDeclaredShape()) - return declaredWidth(*entry, pointee, &call); - if (const auto found = nodeOf.find(callee->getCanonicalDecl()); - found != nodeOf.end() && - nodes[found->second].kind == KindNode::Kind::Result) - return loadOf(found->second); - // A3: the result of a function outside the unit is Single-or-nullable. - return ValueWidth::of(objectWidth(pointee, context), - entry != nullptr && entry->declaresNonnull() - ? core::Nullability::Nonnull - : core::Nullability::Nullable); -} - -// -- The fixpoint (§7.3) -// ------------------------------------------------------- - -void KindInferenceState::demote(unsigned index, const clang::Stmt *store, - std::string reason) { - KindNode &node = nodes[index]; - if (node.demoted) - return; - node.demoted = true; - // What demoted the node is not judged for nullness: nullable. - node.nonnull = false; - node.why = std::move(reason); - (void)store; -} - -/// The value one input brings to its node in the current round. -static ValueWidth valueOf(const KindInferenceState &state, - const Incoming &input) { - if (input.fixed) - return *input.fixed; - if (input.node) - return state.loadOf(*input.node); - return state.judge(input.value); -} - -void KindInferenceState::solve() { - // A greatest fixpoint: every node starts `single` (and nonnull), and each - // round demotes what an input no longer supports. Demotion, the least - // width and the nullability only ever move down. - bool changed = true; - while (changed) { - changed = false; - for (unsigned index = 0; index < nodes.size(); ++index) { - KindNode &node = nodes[index]; - if (node.demoted) - continue; - if (!node.forced.empty()) { - demote(index, node.forced.front().store, node.forced.front().reason); - changed = true; - continue; - } - std::uint64_t least = KindNode::Top; - bool nonnull = true; - std::optional why; - for (const Incoming &input : node.incoming) { - const ValueWidth value = valueOf(*this, input); - if (value.nullOnly) { - nonnull = false; - continue; - } - if (value.nullability != core::Nullability::Nonnull) - nonnull = false; - if (!value.bytes) { - why = value.why.empty() - ? std::string("a value that is not Single-valid") - : value.why; - break; - } - least = std::min(least, *value.bytes); - } - if (!why && node.kind != KindNode::Kind::Value && least < node.needed) - why = "a value with " + std::to_string(least) + - " bytes, fewer than the " + std::to_string(node.needed) + - " of '" + node.pointee.getAsString() + "'"; - if (why) { - demote(index, nullptr, std::move(*why)); - changed = true; - continue; - } - if (node.kind == KindNode::Kind::Value && least != node.least) { - node.least = least; - changed = true; - } - if (nonnull != node.nonnull) { - node.nonnull = nonnull; - changed = true; - } - } - } -} - -/// §13.1 `slotKinds.demotedBy`: the hidden stores, then every input that is -/// not Single-valid under the final kinds, in source order, once each. -static std::vector demotionsOf(const KindInferenceState &state, - const KindNode &node) { - std::vector out; - const auto add = [&](KindDemotion demotion) { - const bool seen = llvm::any_of(out, [&](const KindDemotion &each) { - return each.store == demotion.store && each.reason == demotion.reason; - }); - if (!seen) - out.push_back(std::move(demotion)); - }; - for (const KindDemotion &forced : node.forced) - add(forced); - for (const Incoming &input : node.incoming) { - const ValueWidth value = valueOf(state, input); - if (value.covers(node.needed)) - continue; - add(KindDemotion{ - .store = input.store, - .reason = value.bytes ? "a value with " + std::to_string(*value.bytes) + - " bytes, fewer than the " + - std::to_string(node.needed) + " of '" + - node.pointee.getAsString() + "'" - : value.why}); - } - const clang::SourceManager &sm = state.context.getSourceManager(); - std::ranges::stable_sort( - out, [&sm](const KindDemotion &a, const KindDemotion &b) { - if (a.store == nullptr || b.store == nullptr) - return a.store != nullptr && b.store == nullptr; - const clang::SourceLocation left = a.store->getBeginLoc(); - const clang::SourceLocation right = b.store->getBeginLoc(); - return left != right && sm.isBeforeInTranslationUnit(left, right); - }); - return out; -} - -// -- The table -// ------------------------------------------------------------------ - -bool KindInferenceState::joinsArguments( - const clang::FunctionDecl &function) const { - const auto found = functions.find(function.getCanonicalDecl()); - if (found == functions.end() || found->second.definition == nullptr) - return false; - const FunctionFacts &facts = found->second; - return !function.isExternallyVisible() && !function.isMain() && - !facts.addressTaken && !facts.calls.empty(); -} - -/// The shape of a slot, result or static parameter the fixpoint decided, -/// over what `entry` declares (its nullability). -static void inferShape(KindEntry &entry, bool single, - core::Nullability nullability) { - entry.kind.shape = - single ? core::PointerShape::Single : core::PointerShape::Unknown; - entry.kind.extent = core::ExtentTerm{}; - entry.kind.source = core::KindSource::Inferred; - if (!entry.nullabilityLevel) - entry.kind.nullability = nullability; - entry.extentClass = core::extentClassOf(entry.kind); -} - -void KindInferenceState::fillTable() { - // Slots. - for (const KindNode &node : nodes) { - if (node.kind != KindNode::Kind::Field && - node.kind != KindNode::Kind::Global) - continue; - const auto *field = llvm::dyn_cast(node.decl); - const auto *variable = llvm::dyn_cast(node.decl); - const KindEntry *declared = - field != nullptr ? kinds.field(*field) : kinds.variable(*variable); - KindEntry entry = declared != nullptr ? *declared : KindEntry{}; - if (entry.shapeLevel) - continue; - inferShape(entry, !node.demoted, core::Nullability::Nullable); - if (node.demoted) - entry.demotedBy = demotionsOf(*this, node); - if (field != nullptr) - kinds.setField(*field, std::move(entry)); - else - kinds.setVariable(*variable, std::move(entry)); - } - - // Functions: results, then parameters. - std::vector all = referenced; - for (const clang::FunctionDecl *definition : definitions) - all.push_back(definition->getCanonicalDecl()); - llvm::DenseSet done; - for (const clang::FunctionDecl *canonical : all) { - if (!done.insert(canonical).second || kinds.governedByLibrary(*canonical)) - continue; - const auto facts = functions.find(canonical); - const clang::FunctionDecl *definition = - facts != functions.end() ? facts->second.definition : nullptr; - - if (isObjectPointer(canonical->getReturnType())) { - const KindEntry *declared = kinds.result(*canonical); - KindEntry entry = declared != nullptr ? *declared : KindEntry{}; - if (!entry.shapeLevel) { - const auto node = nodeOf.find(canonical); - if (definition != nullptr && node != nodeOf.end()) { - const KindNode &result = nodes[node->second]; - inferShape(entry, !result.demoted, - result.nonnull && !result.incoming.empty() - ? core::Nullability::Nonnull - : core::Nullability::Nullable); - if (result.demoted) - entry.demotedBy = demotionsOf(*this, result); - } else { - // A3: a result from outside the unit is Single-or-nullable. - inferShape(entry, true, core::Nullability::Nullable); - entry.kind.source = core::KindSource::Default; - } - kinds.setResult(*canonical, std::move(entry)); - } - } - - if (definition == nullptr) - continue; - const bool join = joinsArguments(*definition); - for (unsigned i = 0; i < definition->getNumParams(); ++i) { - const clang::ParmVarDecl *param = definition->getParamDecl(i); - if (!isObjectPointer(param->getType())) - continue; - const KindEntry *declared = kinds.param(*definition, i); - KindEntry entry = declared != nullptr ? *declared : KindEntry{}; - if (!entry.shapeLevel) { - if (i == 1 && isMainArgv(*definition)) { - // §7.3: `counted(argc + 1) nonnull`, trusted as system-api. - entry.kind = core::PointerKind::counted( - core::ExtentTerm::of(core::ExtentPath::ofParam(0), 1, 1), - core::Nullability::Nonnull, core::KindSource::Declared); - entry.shapeLevel = KindLevel::SystemHeader; - entry.nullabilityLevel = KindLevel::SystemHeader; - entry.extentClass = core::ExtentClass::Declared; - entry.mainArgv = true; - } else if (const auto found = entryOf.find(param); - join && found != entryOf.end()) { - const KindNode &start = nodes[found->second]; - inferShape(entry, !start.demoted, - start.nonnull ? core::Nullability::Nonnull - : core::Nullability::Nullable); - if (start.demoted) - entry.demotedBy = demotionsOf(*this, start); - } else { - // A1: Single-or-nullable, and whether the body leans on it. - inferShape(entry, true, core::Nullability::Nullable); - entry.kind.source = core::KindSource::Default; - entry.reliesOnSingle = reliesOnDefault(*param); - } - } - kinds.setParam(*definition, i, std::move(entry)); - } - } -} - -void KindInferenceState::run() { - collect(); - addHiddenStores(); - solve(); - fillTable(); - if (options.mustAccess) - inferMustAccess(); - if (options.fieldCandidates) - inferFieldCandidates(); -} - -// -- The result -// ----------------------------------------------------------------- - -KindInferenceResult::KindInferenceResult() = default; -KindInferenceResult::~KindInferenceResult() = default; -KindInferenceResult::KindInferenceResult(KindInferenceResult &&) noexcept = - default; -KindInferenceResult & -KindInferenceResult::operator=(KindInferenceResult &&) noexcept = default; - -const std::vector & -KindInferenceResult::resolvedCandidates() const noexcept { - static const std::vector None; - if (state == nullptr) - return None; - return state->candidates; -} - -const std::vector & -KindInferenceResult::disqualified() const noexcept { - static const std::vector None; - if (state == nullptr) - return None; - return state->disqualified; -} - -bool KindInferenceResult::isSingleValid(const clang::Expr &value, - clang::QualType pointee) const { - return state != nullptr && - state->judge(&value).covers(objectWidth(pointee, state->context)); -} - -bool KindInferenceResult::argumentIsSingleValid(const clang::CallExpr &call, - unsigned index) const { - if (state == nullptr || index >= call.getNumArgs()) - return false; - clang::QualType parameter; - if (const clang::FunctionDecl *callee = call.getDirectCallee(); - callee != nullptr && index < callee->getNumParams()) { - parameter = callee->getParamDecl(index)->getType(); - } else if (const auto *prototype = call.getCallee() - ->getType() - ->getPointeeOrArrayElementType() - ->getAs(); - prototype != nullptr && index < prototype->getNumParams()) { - parameter = prototype->getParamType(index); - } else { - parameter = call.getArg(index)->getType(); - } - if (!isObjectPointer(parameter)) - return false; - return isSingleValid(*call.getArg(index), parameter->getPointeeType()); -} - -KindInferenceResult KindInference::infer(KindTable &kinds) { - KindInferenceResult result; - result.state = - std::make_unique(context, library, options, kinds); - result.state->run(); - // The CFGs and parent maps of every definition are the run's; the result - // outlives it into the object engine's runs, which build their own. - result.state->cfgs.clear(); - result.state->parentMaps.clear(); - return result; -} - -std::shared_ptr -UnitKinds::build(clang::ASTContext &context, const core::LibrarySpec &library) { - auto kinds = std::make_shared(); - kinds->table = AttributeReader(context, library).read(); - kinds->slots = SlotCollector(context, library).collect(); - kinds->solution = kinds->slots.solve(); - KindInferenceOptions options; - options.slots = &kinds->slots; - options.slotSolution = &kinds->solution; - kinds->inferred = - KindInference(context, library, options).infer(kinds->table); - return kinds; -} - -} // namespace weavec::analysis diff --git a/lib/Analysis/KindInferenceCollect.cpp b/lib/Analysis/KindInferenceCollect.cpp deleted file mode 100644 index 5464195a..00000000 --- a/lib/Analysis/KindInferenceCollect.cpp +++ /dev/null @@ -1,980 +0,0 @@ -//===- KindInferenceCollect.cpp - The stores of a unit (RFC 0030) ---------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// §7.3: one walk over the unit collects every value that reaches a slot, a -// result, a static parameter or a variable, and the stores the syntax does -// not show: stores through `*q`, slot addresses passed to callees, -// byte-wise writes, union punning and conversions from byte buffers. -// `addHiddenStores` then turns the latter into node inputs. -// -//===----------------------------------------------------------------------===// - -#include "KindInferenceImpl.h" - -#include "clang/AST/RecursiveASTVisitor.h" - -#include "llvm/ADT/STLExtras.h" - -#include -#include -#include -#include -#include - -namespace weavec::analysis { - -namespace { - -/// Every declaration, store, call and conversion of the unit. -class Collector : public clang::RecursiveASTVisitor { -public: - explicit Collector(KindInferenceState &inference) : state(inference) {} - - // RecursiveASTVisitor's CRTP hooks are found by name. - // NOLINTBEGIN(readability-identifier-naming,bugprone-derived-method-shadowing-base-method) - bool TraverseFunctionDecl(clang::FunctionDecl *function) { - const clang::FunctionDecl *saved = current; - std::vector savedFields = std::move(mentioned); - mentioned.clear(); - if (function->doesThisDeclarationHaveABody()) - current = function; - const bool result = RecursiveASTVisitor::TraverseFunctionDecl(function); - relateMentioned(); - current = saved; - mentioned = std::move(savedFields); - return result; - } - - bool VisitFunctionDecl(clang::FunctionDecl *function); - bool VisitFieldDecl(clang::FieldDecl *field); - bool VisitRecordDecl(clang::RecordDecl *record); - bool VisitVarDecl(clang::VarDecl *variable); - bool VisitBinaryOperator(clang::BinaryOperator *op); - bool VisitUnaryOperator(clang::UnaryOperator *op); - bool VisitCallExpr(clang::CallExpr *call); - bool VisitDeclRefExpr(clang::DeclRefExpr *ref); - bool VisitReturnStmt(clang::ReturnStmt *ret); - bool VisitInitListExpr(clang::InitListExpr *list); - bool VisitCastExpr(clang::CastExpr *cast); - bool VisitMemberExpr(clang::MemberExpr *member); - // NOLINTEND(readability-identifier-naming,bugprone-derived-method-shadowing-base-method) - -private: - KindInferenceState &state; - const clang::FunctionDecl *current = nullptr; - /// Direct-callee references, which do not take the function's address. - llvm::DenseSet callees; - llvm::DenseSet referencedSeen; - /// The fields the current function mentions (§7.6 pruning). - std::vector mentioned; - - void store(const clang::Expr *lhs, const clang::Expr *value, - const clang::Stmt *where); - void arithmetic(const clang::Expr *lvalue, const clang::Stmt *where); - void characterStore(const clang::Expr *lhs, const clang::Stmt *where); - void noteVariable(const clang::Expr *lvalue, bool assigned, bool modified); - void libraryCall(const clang::CallExpr &call, - const core::LibraryMatch &match); - void slotArguments(const clang::CallExpr &call); - void byteWrite(const clang::Expr *argument, const clang::Stmt *where, - std::string what, bool keepsPointers); - void relateMentioned(); -}; - -} // namespace - -bool Collector::VisitFunctionDecl(clang::FunctionDecl *function) { - const clang::FunctionDecl *canonical = function->getCanonicalDecl(); - KindInferenceState::FunctionFacts &facts = state.functions[canonical]; - if (!function->doesThisDeclarationHaveABody() || - function->isDependentContext()) - return true; - facts.definition = function; - state.definitions.push_back(function); - for (const clang::ParmVarDecl *param : function->parameters()) { - if (isObjectPointer(param->getType())) - state.addNode(KindNode::Kind::Value, param, - param->getType()->getPointeeType()); - } - const clang::QualType result = function->getReturnType(); - const KindEntry *declared = state.kinds.result(*function); - if (isObjectPointer(result) && - (declared == nullptr || !declared->hasDeclaredShape())) - state.addNode(KindNode::Kind::Result, canonical, result->getPointeeType()); - return true; -} - -bool Collector::VisitFieldDecl(clang::FieldDecl *field) { - if (!isObjectPointer(field->getType()) || - state.nodeOf.contains(field->getCanonicalDecl())) - return true; - const KindEntry *declared = state.kinds.field(*field); - if (declared != nullptr && declared->hasDeclaredShape()) - return true; - state.addNode(KindNode::Kind::Field, field->getCanonicalDecl(), - field->getType()->getPointeeType()); - return true; -} - -bool Collector::VisitRecordDecl(clang::RecordDecl *record) { - if (record->isThisDeclarationADefinition() && !record->isDependentType()) - state.records.push_back(record); - return true; -} - -bool Collector::VisitVarDecl(clang::VarDecl *variable) { - if (llvm::isa(variable) || - !isObjectPointer(variable->getType())) - return true; - const clang::VarDecl *canonical = variable->getCanonicalDecl(); - if (variable->hasGlobalStorage()) { - const KindEntry *declared = state.kinds.variable(*variable); - if (declared != nullptr && declared->hasDeclaredShape()) - return true; - if (!state.nodeOf.contains(canonical)) - state.addNode(KindNode::Kind::Global, canonical, - variable->getType()->getPointeeType()); - } else if (!state.nodeOf.contains(canonical)) { - state.addNode(KindNode::Kind::Value, canonical, - variable->getType()->getPointeeType()); - } - const auto found = state.nodeOf.find(canonical); - if (found == state.nodeOf.end()) - return true; - KindNode &node = state.nodes[found->second]; - if (const clang::Expr *init = variable->getInit()) { - // `T *p = {v}`: the one element is the value. - if (const auto *list = - llvm::dyn_cast(init->IgnoreParenImpCasts()); - list != nullptr && list->getNumInits() == 1) - init = list->getInit(0); - node.incoming.push_back(Incoming{.value = init, - .fixed = std::nullopt, - .node = std::nullopt, - .store = variable->getInit()}); - } else if (!variable->hasGlobalStorage() && - variable->isThisDeclarationADefinition() == - clang::VarDecl::Definition) { - // Uninitialised: null under zero-initialisation (§11), and read only - // after a store otherwise. - node.incoming.push_back(Incoming{.value = nullptr, - .fixed = ValueWidth::null(), - .node = std::nullopt, - .store = nullptr}); - } - return true; -} - -void Collector::noteVariable(const clang::Expr *lvalue, bool assigned, - bool modified) { - const auto *ref = - llvm::dyn_cast(lvalue->IgnoreParenImpCasts()); - const auto *variable = - ref != nullptr ? llvm::dyn_cast(ref->getDecl()) : nullptr; - if (variable == nullptr || variable->hasGlobalStorage()) - return; - KindInferenceState::VariableUse &use = - state.variableUses[variable->getCanonicalDecl()]; - use.assigned = use.assigned || assigned; - use.modified = use.modified || modified; -} - -/// A slot whose stores are not inferred: a declared kind is checked at each -/// store instead (§7.4 rule 5), and an element of an array of pointers is no -/// slot (a load from one is never Single-valid). -static bool untrackedLvalue(const clang::Expr *lvalue) { - if (llvm::isa(lvalue) || - llvm::isa(lvalue)) - return true; - const auto *subscript = llvm::dyn_cast(lvalue); - if (subscript == nullptr) - return false; - const auto *decay = llvm::dyn_cast( - subscript->getBase()->IgnoreParens()); - return decay != nullptr && - decay->getCastKind() == clang::CK_ArrayToPointerDecay; -} - -void Collector::store(const clang::Expr *lhs, const clang::Expr *value, - const clang::Stmt *where) { - lhs = lhs->IgnoreParens(); - if (const auto node = state.slotNode(lhs)) { - state.nodes[*node].incoming.push_back(Incoming{.value = value, - .fixed = std::nullopt, - .node = std::nullopt, - .store = where}); - return; - } - if (untrackedLvalue(lhs)) - return; - // `*q = v`, `q[i] = v`: the syntax does not name the slot (§7.3). - state.indirectStores.push_back(IndirectStore{.value = value, - .type = lhs->getType(), - .store = where, - .arithmetic = false}); -} - -void Collector::arithmetic(const clang::Expr *lvalue, - const clang::Stmt *where) { - lvalue = lvalue->IgnoreParens(); - if (const auto node = state.slotNode(lvalue)) { - state.nodes[*node].forced.push_back( - KindDemotion{.store = where, .reason = "pointer arithmetic"}); - return; - } - if (untrackedLvalue(lvalue)) - return; - state.indirectStores.push_back(IndirectStore{.value = nullptr, - .type = lvalue->getType(), - .store = where, - .arithmetic = true}); -} - -void Collector::byteWrite(const clang::Expr *argument, const clang::Stmt *where, - std::string what, bool keepsPointers) { - const clang::QualType type = argument->getType(); - if (!type->isPointerType()) - return; - clang::QualType pointee = type->getPointeeType(); - while (const clang::ArrayType *array = state.context.getAsArrayType(pointee)) - pointee = array->getElementType(); - if (const clang::RecordDecl *record = pointee->getAsRecordDecl()) - state.byteWrites.push_back(KindInferenceState::ByteWrite{ - .record = record->getDefinition() != nullptr ? record->getDefinition() - : record, - .demotion = KindDemotion{.store = where, .reason = std::move(what)}, - .keepsPointers = keepsPointers}); -} - -void Collector::characterStore(const clang::Expr *lhs, - const clang::Stmt *where) { - if (!isCharacter(lhs->getType())) - return; - lhs = lhs->IgnoreParens(); - const clang::Expr *base = nullptr; - if (const auto *unary = llvm::dyn_cast(lhs); - unary != nullptr && unary->getOpcode() == clang::UO_Deref) - base = unary->getSubExpr(); - else if (const auto *subscript = - llvm::dyn_cast(lhs)) - base = subscript->getBase(); - if (base == nullptr) - return; - const clang::Expr *object = stripPointerCasts(base); - if (const auto *address = llvm::dyn_cast(object); - address != nullptr && address->getOpcode() == clang::UO_AddrOf) { - if (const auto node = state.slotNode(address->getSubExpr()->IgnoreParens())) - state.nodes[*node].forced.push_back(KindDemotion{ - .store = where, .reason = "a character-typed store into it"}); - } - byteWrite(object, where, "a character-typed store", false); -} - -bool Collector::VisitBinaryOperator(clang::BinaryOperator *op) { - const clang::Expr *lhs = op->getLHS(); - const clang::QualType type = lhs->getType(); - if (!op->isAssignmentOp()) - return true; - noteVariable(lhs, /*assigned=*/true, - /*modified=*/op->isCompoundAssignmentOp()); - characterStore(lhs, op); - if (const auto *member = - llvm::dyn_cast(lhs->IgnoreParens())) { - const auto *field = - llvm::dyn_cast(member->getMemberDecl()); - if (field != nullptr && field->getParent()->isUnion()) - state.unionStores[field->getParent()].emplace_back(field, op); - } - if (!isObjectPointer(type)) - return true; - if (op->getOpcode() == clang::BO_Assign) - store(lhs, op->getRHS(), op); - else - arithmetic(lhs, op); - return true; -} - -bool Collector::VisitUnaryOperator(clang::UnaryOperator *op) { - const clang::Expr *sub = op->getSubExpr(); - if (op->isIncrementDecrementOp()) { - noteVariable(sub, /*assigned=*/false, /*modified=*/true); - characterStore(sub, op); - if (isObjectPointer(sub->getType())) - arithmetic(sub, op); - return true; - } - if (op->getOpcode() != clang::UO_AddrOf) - return true; - const clang::Expr *target = sub->IgnoreParens(); - if (const clang::VarDecl *variable = variableOf(target); - variable != nullptr && !variable->hasGlobalStorage()) { - KindInferenceState::VariableUse &use = - state.variableUses[variable->getCanonicalDecl()]; - use.addressTaken = true; - use.modified = true; - } - if (const auto node = state.slotNode(target)) { - KindNode &slot = state.nodes[*node]; - if (slot.kind == KindNode::Kind::Value) - slot.forced.push_back( - KindDemotion{.store = op, .reason = "its address is taken"}); - else - state.addressTaken.insert(*node); - } - if (const auto *member = llvm::dyn_cast(target)) - if (const auto *field = - llvm::dyn_cast(member->getMemberDecl())) - state.fieldAddresses.try_emplace(field->getCanonicalDecl(), op); - return true; -} - -/// The call argument a row's argument `rowArg` is, if any. -static const clang::Expr *rowArgument(const clang::CallExpr &call, - const core::LibraryMatch &match, - unsigned rowArg) { - const int index = match.callArgument(rowArg); - if (index < 0 || static_cast(index) >= call.getNumArgs()) - return nullptr; - return call.getArg(static_cast(index)); -} - -void Collector::libraryCall(const clang::CallExpr &call, - const core::LibraryMatch &match) { - const core::LibraryEntry &entry = *match.entry; - for (unsigned i = 0; i < call.getNumArgs(); ++i) { - const core::LibraryParam *param = match.param(i); - if (param == nullptr) - continue; - const bool writes = param->access == core::LibraryParam::Access::Write || - param->access == core::LibraryParam::Access::ReadWrite; - if (!writes && !param->out) - continue; - const clang::Expr *object = stripPointerCasts(call.getArg(i)); - if (const auto *address = llvm::dyn_cast(object); - address != nullptr && address->getOpcode() == clang::UO_AddrOf) { - if (const auto node = - state.slotNode(address->getSubExpr()->IgnoreParens()); - node && state.nodes[*node].kind != KindNode::Kind::Value) { - state.slotAddressArguments.push_back( - SlotAddressArgument{.call = &call, - .index = i, - .node = node, - .type = object->getType()}); - continue; - } - } - if (param->out && object->getType()->isPointerType() && - isObjectPointer(object->getType()->getPointeeType())) { - state.slotAddressArguments.push_back( - SlotAddressArgument{.call = &call, - .index = i, - .node = std::nullopt, - .type = object->getType()}); - continue; - } - if (!writes) - continue; - // A zero fill writes null pointers, and a copy from an object of the - // same type copies Single-valid ones (§7.3); §7.6 still counts both. - bool keeps = false; - for (const core::LibFill &fill : entry.fills) { - if (!std::cmp_equal(match.callArgument(fill.dst), i)) - continue; - const auto zero = fill.value.evaluate(core::LibTerm::Values{ - .argument = [&](unsigned rowArg) -> std::optional { - return state.constantOf(rowArgument(call, match, rowArg)); - }, - .stringLength = nullptr, - .formatLength = nullptr, - .macro = nullptr}); - keeps = keeps || zero == 0; - } - for (const core::LibCopy © : entry.copies) { - const clang::Expr *source = rowArgument(call, match, copy.src); - if (!std::cmp_equal(match.callArgument(copy.dst), i) || source == nullptr) - continue; - const clang::QualType from = stripPointerCasts(source)->getType(); - const clang::QualType to = object->getType(); - keeps = keeps || (from->isPointerType() && to->isPointerType() && - clang::ASTContext::hasSameUnqualifiedType( - from->getPointeeType(), to->getPointeeType())); - } - byteWrite(object, &call, "a byte-wise write by '" + entry.name + "'", - keeps); - } -} - -void Collector::slotArguments(const clang::CallExpr &call) { - for (unsigned i = 0; i < call.getNumArgs(); ++i) { - const clang::Expr *object = stripPointerCasts(call.getArg(i)); - if (const auto *address = llvm::dyn_cast(object); - address != nullptr && address->getOpcode() == clang::UO_AddrOf) { - if (const auto node = - state.slotNode(address->getSubExpr()->IgnoreParens()); - node && state.nodes[*node].kind != KindNode::Kind::Value) - state.slotAddressArguments.push_back( - SlotAddressArgument{.call = &call, - .index = i, - .node = node, - .type = object->getType()}); - continue; - } - const clang::QualType type = object->getType(); - if (type->isPointerType() && isObjectPointer(type->getPointeeType())) - state.slotAddressArguments.push_back(SlotAddressArgument{ - .call = &call, .index = i, .node = std::nullopt, .type = type}); - } -} - -bool Collector::VisitCallExpr(clang::CallExpr *call) { - if (current != nullptr) - state.callsIn[current].push_back(call); - if (const clang::DeclRefExpr *ref = calleeReference(*call)) { - callees.insert(ref); - const auto *callee = llvm::cast(ref->getDecl()); - state.functions[callee->getCanonicalDecl()].calls.push_back(call); - } - if (const clang::FunctionDecl *callee = call->getDirectCallee()) { - if (const auto match = governingLibraryEntry(*callee, state.library)) { - libraryCall(*call, *match); - return true; - } - } - slotArguments(*call); - return true; -} - -bool Collector::VisitDeclRefExpr(clang::DeclRefExpr *ref) { - if (const auto *function = - llvm::dyn_cast(ref->getDecl())) { - const clang::FunctionDecl *canonical = function->getCanonicalDecl(); - if (referencedSeen.insert(canonical).second) - state.referenced.push_back(canonical); - if (!callees.contains(ref)) - state.functions[canonical].addressTaken = true; - return true; - } - if (const auto *param = llvm::dyn_cast(ref->getDecl()); - param != nullptr && current != nullptr && - param->getDeclContext() == current) - state.parameterReferences[param].push_back(ref); - return true; -} - -bool Collector::VisitReturnStmt(clang::ReturnStmt *ret) { - if (current == nullptr || ret->getRetValue() == nullptr) - return true; - const auto found = state.nodeOf.find(current->getCanonicalDecl()); - if (found != state.nodeOf.end() && - state.nodes[found->second].kind == KindNode::Kind::Result) - state.nodes[found->second].incoming.push_back( - Incoming{.value = ret->getRetValue(), - .fixed = std::nullopt, - .node = std::nullopt, - .store = ret}); - return true; -} - -bool Collector::VisitInitListExpr(clang::InitListExpr *list) { - if (!list->isSemanticForm()) - return true; - const clang::RecordDecl *record = list->getType()->getAsRecordDecl(); - if (record == nullptr) - return true; - const auto initialise = [&](const clang::FieldDecl *field, - const clang::Expr *init) { - if (field == nullptr || init == nullptr || - llvm::isa(init) || - !isObjectPointer(field->getType())) - return; - const auto found = state.nodeOf.find(field->getCanonicalDecl()); - if (found != state.nodeOf.end()) - state.nodes[found->second].incoming.push_back( - Incoming{.value = init, - .fixed = std::nullopt, - .node = std::nullopt, - .store = list}); - }; - if (record->isUnion()) { - const clang::FieldDecl *field = list->getInitializedFieldInUnion(); - if (field != nullptr && list->getNumInits() > 0) { - state.unionStores[record].emplace_back(field, list); - initialise(field, list->getInit(0)); - } - return true; - } - unsigned index = 0; - for (const clang::FieldDecl *field : record->fields()) { - // CodeGen's order: unnamed bit-fields take no initialiser. - if (field->isUnnamedBitField()) - continue; - if (index >= list->getNumInits()) - break; - initialise(field, list->getInit(index++)); - } - return true; -} - -bool Collector::VisitCastExpr(clang::CastExpr *cast) { - const clang::QualType to = cast->getType(); - const clang::QualType from = cast->getSubExpr()->getType(); - if (cast->getCastKind() != clang::CK_BitCast || !to->isPointerType() || - !from->isPointerType()) - return true; - const clang::QualType toPointee = - to->getPointeeType().getCanonicalType().getUnqualifiedType(); - const clang::QualType fromPointee = - from->getPointeeType().getCanonicalType().getUnqualifiedType(); - // `(void **)&s`: the record's pointer fields may be stored through the new - // type (§7.3), as if their addresses were taken. - if (const clang::RecordDecl *record = fromPointee->getAsRecordDecl(); - record != nullptr && toPointee->isPointerType()) { - for (const clang::FieldDecl *field : record->fields()) - if (const auto found = state.nodeOf.find(field->getCanonicalDecl()); - found != state.nodeOf.end()) - state.addressTaken.insert(found->second); - } - // `(T **)bytes` from anything but a fresh allocation may point into any - // object: every slot is as good as address-taken. - if (toPointee->isPointerType() && !fromPointee->isPointerType() && - fromPointee->getAsRecordDecl() == nullptr) { - const auto *call = llvm::dyn_cast( - stripPointerCasts(cast->getSubExpr())->IgnoreParenImpCasts()); - const clang::FunctionDecl *callee = - call != nullptr ? call->getDirectCallee() : nullptr; - const auto match = callee != nullptr - ? governingLibraryEntry(*callee, state.library) - : std::nullopt; - if (!match || match->entry->result.kind != core::LibraryResult::Kind::Fresh) - state.everySlotTaken = true; - } - // `(uintptr_t *)&s->p`: the slot may be overwritten with other bytes. - if (const auto *address = llvm::dyn_cast( - stripPointerCasts(cast->getSubExpr())); - address != nullptr && address->getOpcode() == clang::UO_AddrOf && - !toPointee->isPointerType() && !toPointee->isVoidType() && - !isCharacter(toPointee)) - if (const auto node = state.slotNode(address->getSubExpr())) - state.nodes[*node].forced.push_back(KindDemotion{ - .store = cast, - .reason = "its address is converted to '" + to.getAsString() + "'"}); - // A record made from another object type (§7.6), or from a byte buffer - // no store into its fields wrote (§7.3). - const clang::RecordDecl *record = toPointee->getAsRecordDecl(); - if (record == nullptr || fromPointee->isVoidType() || - fromPointee == toPointee || fromPointee->isIncompleteType()) - return true; - state.conversions.push_back(KindInferenceState::Conversion{ - .record = - record->getDefinition() != nullptr ? record->getDefinition() : record, - .where = cast, - .fromBytes = isCharacter(fromPointee)}); - return true; -} - -bool Collector::VisitMemberExpr(clang::MemberExpr *member) { - if (current == nullptr) - return true; - if (const auto *field = - llvm::dyn_cast(member->getMemberDecl())) - mentioned.push_back(field->getCanonicalDecl()); - return true; -} - -void Collector::relateMentioned() { - llvm::sort(mentioned); - mentioned.erase(std::ranges::unique(mentioned).begin(), mentioned.end()); - for (const clang::FieldDecl *pointer : mentioned) { - if (!isObjectPointer(pointer->getType())) - continue; - for (const clang::FieldDecl *count : mentioned) - if (count->getParent() == pointer->getParent() && - count->getType()->isIntegerType()) - state.related.insert({pointer, count}); - } -} - -/// §7.3: `argv` of `main(int argc, char **argv[, char **envp])`, while -/// neither `argc` nor `argv` is assigned. -bool KindInferenceState::isMainArgv( - const clang::FunctionDecl &definition) const { - if (!definition.isMain() || definition.getNumParams() < 2) - return false; - const clang::ParmVarDecl *argc = definition.getParamDecl(0); - const clang::ParmVarDecl *argv = definition.getParamDecl(1); - const clang::QualType type = argv->getType(); - if (!argc->getType()->isIntegerType() || !type->isPointerType() || - !type->getPointeeType()->isPointerType() || - !isCharacter(type->getPointeeType()->getPointeeType())) - return false; - return llvm::all_of(std::array{argc, argv}, - [&](const clang::ParmVarDecl *param) { - const auto use = variableUses.find(param); - return use == variableUses.end() || - (!use->second.assigned && !use->second.modified); - }); -} - -void KindInferenceState::collect() { - Collector collector(*this); - collector.TraverseDecl(context.getTranslationUnitDecl()); - - // Each parameter's value starts as its entry value (§7.3): declared, the - // join of a static function's arguments, or the A1 default. - for (const clang::FunctionDecl *definition : definitions) { - const bool join = joinsArguments(*definition); - for (unsigned i = 0; i < definition->getNumParams(); ++i) { - const clang::ParmVarDecl *param = definition->getParamDecl(i); - const auto found = nodeOf.find(param); - if (found == nodeOf.end()) - continue; - const unsigned value = found->second; - const clang::QualType pointee = nodes[value].pointee; - const KindEntry *entry = kinds.param(*definition, i); - Incoming start{.value = nullptr, - .fixed = std::nullopt, - .node = std::nullopt, - .store = nullptr}; - if (entry != nullptr && entry->hasDeclaredShape()) { - start.fixed = declaredWidth(*entry, pointee, nullptr); - } else if (i == 1 && isMainArgv(*definition)) { - start.fixed = ValueWidth::of(objectWidth(pointee, context), - core::Nullability::Nonnull); - } else if (join) { - const unsigned entryNode = - addNode(KindNode::Kind::ParamEntry, param, pointee); - entryOf[param] = entryNode; - for (const clang::CallExpr *call : - functions[definition->getCanonicalDecl()].calls) - if (i < call->getNumArgs()) - nodes[entryNode].incoming.push_back( - Incoming{.value = call->getArg(i), - .fixed = std::nullopt, - .node = std::nullopt, - .store = call}); - start.node = entryNode; - } else { - start.fixed = ValueWidth::of( - nodes[value].needed, entry != nullptr && entry->declaresNonnull() - ? core::Nullability::Nonnull - : core::Nullability::Nullable); - } - nodes[value].incoming.insert(nodes[value].incoming.begin(), start); - } - } -} - -/// Every pointer field of `record` and of the records nested in it by value. -static void -forEachPointerField(const clang::RecordDecl *record, - const clang::ASTContext &context, - const std::function &visit, - unsigned depth = 0) { - if (record == nullptr || depth > 16) - return; - if (const clang::RecordDecl *definition = record->getDefinition()) - record = definition; - for (const clang::FieldDecl *field : record->fields()) { - clang::QualType type = field->getType(); - while (const clang::ArrayType *array = context.getAsArrayType(type)) - type = array->getElementType(); - if (isObjectPointer(type)) - visit(*field); - else if (const clang::RecordDecl *nested = type->getAsRecordDecl()) - forEachPointerField(nested, context, visit, depth + 1); - } -} - -void KindInferenceState::addHiddenStores() { - // §7.3: a store through `*q` of type `T *` reaches every address-taken - // slot of a compatible type; through `void *` and character lvalues, - // every address-taken slot. - const auto compatible = [&](unsigned node, clang::QualType pointer) { - const clang::QualType through = - pointer->getPointeeType().getCanonicalType().getUnqualifiedType(); - const clang::QualType slot = - nodes[node].pointee.getCanonicalType().getUnqualifiedType(); - return through->isVoidType() || isCharacter(through) || - slot->isVoidType() || isCharacter(slot) || through == slot; - }; - if (everySlotTaken) - for (unsigned node = 0; node < nodes.size(); ++node) - if (nodes[node].kind == KindNode::Kind::Field || - nodes[node].kind == KindNode::Kind::Global) - addressTaken.insert(node); - std::vector taken(addressTaken.begin(), addressTaken.end()); - llvm::sort(taken); - for (const IndirectStore &store : indirectStores) { - for (const unsigned node : taken) { - if (!compatible(node, store.type)) - continue; - if (store.arithmetic) - nodes[node].forced.push_back(KindDemotion{ - .store = store.store, - .reason = "pointer arithmetic through a pointer to it"}); - else - nodes[node].incoming.push_back(Incoming{.value = store.value, - .fixed = std::nullopt, - .node = std::nullopt, - .store = store.store}); - } - } - - // §7.3: a slot whose address is passed to a callee is unknown unless the - // callee only stores through it what the rules above already see. - for (const SlotAddressArgument &argument : slotAddressArguments) { - std::vector targets; - if (argument.node) - targets.push_back(*argument.node); - else - for (const unsigned node : taken) - if (compatible(node, argument.type->getPointeeType())) - targets.push_back(node); - if (targets.empty()) - continue; - const clang::CallExpr &call = *argument.call; - const clang::FunctionDecl *callee = call.getDirectCallee(); - if (callee != nullptr) { - if (const auto match = governingLibraryEntry(*callee, library)) { - const core::LibraryParam *param = match->param(argument.index); - const bool writes = - param != nullptr && - (param->access == core::LibraryParam::Access::Write || - param->access == core::LibraryParam::Access::ReadWrite); - for (const unsigned target : targets) { - if (param != nullptr && param->out) - nodes[target].incoming.push_back( - Incoming{.value = nullptr, - .fixed = libraryValue(*param->out, *match, call, - nodes[target].pointee), - .node = std::nullopt, - .store = &call}); - else if (writes) - nodes[target].forced.push_back(KindDemotion{ - .store = &call, - .reason = "a byte-wise write by '" + match->entry->name + "'"}); - } - continue; - } - const clang::FunctionDecl *definition = callee->getDefinition(); - if (definition != nullptr && - argument.index < definition->getNumParams() && - confinedParameter(*definition->getParamDecl(argument.index))) - continue; - } - const std::string who = callee != nullptr - ? "'" + callee->getNameAsString() + "'" - : std::string("an indirect call"); - const std::string reason = argument.node - ? "its address is passed to " + who - : "a pointer to it may be passed to " + who; - for (const unsigned target : targets) - nodes[target].forced.push_back( - KindDemotion{.store = &call, .reason = reason}); - } - - // §7.3: a byte-wise write demotes every pointer field of the object's - // type; so does making the object from a byte buffer. - const auto demoteFields = [&](const clang::RecordDecl *record, - const KindDemotion &demotion) { - forEachPointerField(record, context, [&](const clang::FieldDecl &field) { - if (const auto found = nodeOf.find(field.getCanonicalDecl()); - found != nodeOf.end()) - nodes[found->second].forced.push_back(demotion); - }); - }; - for (const ByteWrite &write : byteWrites) - if (!write.keepsPointers) - demoteFields(write.record, write.demotion); - for (const Conversion &conversion : conversions) - if (conversion.fromBytes) - demoteFields(conversion.record, - KindDemotion{.store = conversion.where, - .reason = "its object is converted from a " - "byte buffer"}); - - // A store to another member of a union rewrites the pointer's bytes. - for (const auto &[record, stores] : unionStores) { - for (const clang::FieldDecl *field : record->fields()) { - const auto found = nodeOf.find(field->getCanonicalDecl()); - if (found == nodeOf.end()) - continue; - for (const auto &[stored, where] : stores) { - if (stored->getCanonicalDecl() == field->getCanonicalDecl() || - clang::ASTContext::hasSameUnqualifiedType(stored->getType(), - field->getType())) - continue; - nodes[found->second].forced.push_back( - KindDemotion{.store = where, - .reason = "a store to '" + fieldName(*stored) + - "', another member of the union"}); - break; - } - } - } -} - -const clang::ParentMap & -KindInferenceState::parentsOf(const clang::FunctionDecl &function) const { - std::unique_ptr &map = parentMaps[&function]; - if (map == nullptr) - // `ParentMap` takes a mutable root but only reads it. - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast) - map = std::make_unique( - const_cast(function.getBody())); - return *map; -} - -/// The expression a reference's value flows into, past parentheses and the -/// conversions that keep the pointer (`child` is set to the last one). -static const clang::Stmt *valueParent(const clang::ParentMap &parents, - const clang::Stmt *&child, - bool &toInteger) { - toInteger = false; - const clang::Stmt *parent = parents.getParent(child); - while (parent != nullptr) { - if (llvm::isa(parent)) { - child = parent; - parent = parents.getParent(parent); - continue; - } - const auto *cast = llvm::dyn_cast(parent); - if (cast == nullptr) - break; - const clang::CastKind kind = cast->getCastKind(); - if (kind == clang::CK_PointerToBoolean || - kind == clang::CK_PointerToIntegral) { - toInteger = true; - return parent; - } - if (kind != clang::CK_LValueToRValue && kind != clang::CK_NoOp && - kind != clang::CK_BitCast) - break; - child = parent; - parent = parents.getParent(parent); - } - return parent; -} - -/// `stmt` is inside an unevaluated operand (`sizeof`, `_Alignof`). -static bool unevaluated(const clang::Stmt *stmt, - const clang::ParentMap &parents) { - for (const clang::Stmt *parent = parents.getParent(stmt); parent != nullptr; - parent = parents.getParent(parent)) - if (const auto *trait = - llvm::dyn_cast(parent); - trait != nullptr && !trait->getTypeOfArgument()->isVariableArrayType()) - return true; - return false; -} - -bool KindInferenceState::confinedParameter( - const clang::ParmVarDecl ¶m) const { - if (const auto use = variableUses.find(¶m); - use != variableUses.end() && - (use->second.assigned || use->second.modified)) - return false; - const auto references = parameterReferences.find(¶m); - if (references == parameterReferences.end()) - return true; - const auto *function = - llvm::dyn_cast(param.getDeclContext()); - if (function == nullptr || function->getBody() == nullptr) - return false; - const clang::ParentMap &parents = parentsOf(*function); - for (const clang::DeclRefExpr *ref : references->second) { - if (unevaluated(ref, parents)) - continue; - const clang::Stmt *child = ref; - bool toInteger = false; - const clang::Stmt *parent = valueParent(parents, child, toInteger); - if (toInteger) - continue; - if (const auto *unary = - llvm::dyn_cast_or_null(parent); - unary != nullptr && (unary->getOpcode() == clang::UO_Deref || - unary->getOpcode() == clang::UO_LNot)) - continue; - if (const auto *subscript = - llvm::dyn_cast_or_null(parent); - subscript != nullptr && subscript->getBase() == child) - continue; - if (const auto *binary = - llvm::dyn_cast_or_null(parent); - binary != nullptr && - (binary->isComparisonOp() || binary->isLogicalOp())) - continue; - return false; - } - return true; -} - -bool KindInferenceState::reliesOnDefault( - const clang::ParmVarDecl ¶m) const { - const auto references = parameterReferences.find(¶m); - if (references == parameterReferences.end()) - return false; - const auto *function = - llvm::dyn_cast(param.getDeclContext()); - if (function == nullptr || function->getBody() == nullptr) - return true; - const clang::ParentMap &parents = parentsOf(*function); - for (const clang::DeclRefExpr *ref : references->second) { - if (unevaluated(ref, parents)) - continue; - const clang::Stmt *child = ref; - bool toInteger = false; - const clang::Stmt *parent = valueParent(parents, child, toInteger); - if (toInteger || parent == nullptr) - continue; - if (const auto *unary = llvm::dyn_cast(parent)) { - if (unary->getOpcode() == clang::UO_Deref || - unary->getOpcode() == clang::UO_Plus || - unary->getOpcode() == clang::UO_Extension) - return true; - continue; // `&p`, `!p`, `++p`: not a use of the value's extent. - } - if (const auto *member = llvm::dyn_cast(parent)) { - if (member->isArrow() && member->getBase() == child) - return true; - continue; - } - if (const auto *subscript = - llvm::dyn_cast(parent)) { - // `p[0]` rests on the default; `p[i]` never does (§7.3). - const auto index = constantOf(subscript->getIdx()); - if (subscript->getBase() == child && index == 0) - return true; - continue; - } - if (const auto *binary = llvm::dyn_cast(parent)) { - if (binary->isComparisonOp() || binary->isLogicalOp() || - binary->isAdditiveOp() || binary->isCompoundAssignmentOp() || - (binary->isAssignmentOp() && binary->getLHS() == child) || - (binary->getOpcode() == clang::BO_Comma && binary->getLHS() == child)) - continue; - return true; - } - if (const auto *cast = llvm::dyn_cast(parent)) { - if (cast->getType()->isIntegerType() || cast->getType()->isVoidType()) - continue; - return true; - } - if (llvm::isa(parent)) - continue; - if (const auto *conditional = - llvm::dyn_cast(parent); - conditional != nullptr && conditional->getCond() == child) - continue; - // Stored, returned, passed, or copied: the value flows on. - return true; - } - return false; -} - -} // namespace weavec::analysis diff --git a/lib/Analysis/KindInferenceDump.cpp b/lib/Analysis/KindInferenceDump.cpp deleted file mode 100644 index 86770e38..00000000 --- a/lib/Analysis/KindInferenceDump.cpp +++ /dev/null @@ -1,159 +0,0 @@ -//===- KindInferenceDump.cpp - `weavec --dump-kinds` (RFC 0030) -----------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// A debugging aid (unstable format): the unit's kind table, its problems and -// suggestions and the §7.6 candidates, one line per fact, for the -// declarations of the main file, in source order. -// -//===----------------------------------------------------------------------===// - -#include "KindInferenceImpl.h" - -#include "clang/Basic/SourceManager.h" - -#include -#include -#include -#include - -namespace weavec::analysis { - -/// `:` of `loc`, or `?`. -static std::string lineColumn(const clang::SourceManager &sm, - clang::SourceLocation loc) { - const clang::PresumedLoc at = sm.getPresumedLoc(sm.getExpansionLoc(loc)); - if (!at.isValid()) - return "?"; - return std::to_string(at.getLine()) + ":" + std::to_string(at.getColumn()); -} - -/// ` [[, ]][, shape ][, nullability ]` -/// and the flags. -static std::string describe(const KindEntry &entry) { - std::string text = entry.kind.toString() + " [" + - std::string(core::toString(entry.kind.source)); - if (entry.extentClass) - text += ", " + std::string(core::toString(*entry.extentClass)); - if (entry.shapeLevel) - text += ", shape " + std::string(toString(*entry.shapeLevel)); - if (entry.nullabilityLevel) - text += ", nullability " + std::string(toString(*entry.nullabilityLevel)); - text += ']'; - if (entry.extentFactor) - text += " factor param " + std::to_string(*entry.extentFactor); - if (entry.reliesOnSingle) - text += " relies-on-single"; - if (entry.mainArgv) - text += " argv"; - return text; -} - -void dumpKinds(const KindTable &kinds, const KindInferenceResult &inferred, - clang::ASTContext &context, llvm::raw_ostream &os) { - const clang::SourceManager &sm = context.getSourceManager(); - // Lines with the location they sort by. - std::vector> lines; - const auto inMain = [&](clang::SourceLocation loc) { - return loc.isValid() && sm.isInMainFile(sm.getExpansionLoc(loc)); - }; - const auto add = [&](clang::SourceLocation loc, const KindEntry &entry, - std::string head) { - std::string text = std::move(head) + ": " + describe(entry); - for (const MustAccessRequirement &requirement : entry.mustAccess) - text += "\n requires " + requirement.toString() + " at " + - lineColumn(sm, requirement.access->getBeginLoc()) + " [" + - std::string(toString(entry.enforcement.value_or( - RequirementEnforcement::CallerContract))) + - "]"; - for (const KindDemotion &demotion : entry.demotedBy) - text += "\n demoted at " + - (demotion.store != nullptr - ? lineColumn(sm, demotion.store->getBeginLoc()) - : std::string("?")) + - ": " + demotion.reason; - lines.emplace_back(loc, std::move(text)); - }; - - for (const auto &[key, entry] : kinds.paramEntries()) { - const clang::FunctionDecl *function = key.first; - const clang::FunctionDecl *shown = function->getDefinition() != nullptr - ? function->getDefinition() - : function; - if (key.second >= shown->getNumParams()) - continue; - const clang::ParmVarDecl *param = shown->getParamDecl(key.second); - if (!inMain(param->getLocation())) - continue; - add(param->getLocation(), entry, - "param " + function->getNameAsString() + " " + - std::to_string(key.second) + " '" + param->getNameAsString() + "'"); - } - for (const auto &[function, entry] : kinds.resultEntries()) { - const clang::FunctionDecl *shown = function->getDefinition() != nullptr - ? function->getDefinition() - : function; - if (inMain(shown->getLocation())) - add(shown->getLocation(), entry, "result " + function->getNameAsString()); - } - for (const auto &[field, entry] : kinds.fieldEntries()) - if (inMain(field->getLocation())) - add(field->getLocation(), entry, - "field " + recordName(*field->getParent()) + "." + - field->getNameAsString()); - for (const auto &[variable, entry] : kinds.variableEntries()) { - const clang::VarDecl *shown = variable->getDefinition() != nullptr - ? variable->getDefinition() - : variable; - if (inMain(shown->getLocation())) - add(shown->getLocation(), entry, - "variable " + variable->getNameAsString()); - } - for (const auto &[function, contract] : kinds.ownershipEntries()) { - if (!inMain(function->getLocation())) - continue; - std::string text = "ownership " + function->getNameAsString() + ":"; - if (contract.freshResult) - text += " fresh(" + *contract.freshResult + ")"; - for (const OwnershipContract::Argument &argument : contract.arguments) - text += std::string(argument.retains ? " holds(" : " takes(") + - std::to_string(argument.index) + ", " + argument.family + ")"; - text += " [" + std::string(toString(contract.level)) + "]"; - lines.emplace_back(function->getLocation(), std::move(text)); - } - for (const KindProblem &problem : kinds.problems()) - if (inMain(problem.location)) - lines.emplace_back(problem.location, - "problem " + lineColumn(sm, problem.location) + ": " + - problem.message); - for (const KindSuggestion &suggestion : kinds.suggestions()) - if (inMain(suggestion.location)) - lines.emplace_back(suggestion.location, - "suggestion " + lineColumn(sm, suggestion.location) + - ": " + suggestion.message + " (insert '" + - suggestion.insert + "')"); - for (const ResolvedCandidate &resolved : inferred.resolvedCandidates()) { - const FieldCandidate &candidate = resolved.candidate; - lines.emplace_back(resolved.record->getLocation(), - "candidate " + candidate.record + ": " + - (candidate.bytes ? "bytes(" : "count(") + - candidate.pointer + ") == " + candidate.count + - " + " + std::to_string(candidate.offset)); - } - for (const DisqualifiedRecord &record : inferred.disqualified()) - lines.emplace_back(record.record->getLocation(), - "disqualified " + record.key + ": " + record.reason); - - std::ranges::stable_sort(lines, [&sm](const auto &a, const auto &b) { - return a.first != b.first && sm.isBeforeInTranslationUnit(a.first, b.first); - }); - os << "kinds:\n"; - for (const auto &[loc, text] : lines) - os << " " << text << "\n"; -} - -} // namespace weavec::analysis diff --git a/lib/Analysis/KindInferenceFields.cpp b/lib/Analysis/KindInferenceFields.cpp deleted file mode 100644 index 676e9b01..00000000 --- a/lib/Analysis/KindInferenceFields.cpp +++ /dev/null @@ -1,164 +0,0 @@ -//===- KindInferenceFields.cpp - Counted relations of fields (RFC 0030) ---===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// §7.6: the counted-field candidates of the unit's structs, with the -// disqualifications that hold before round 1. -// -//===----------------------------------------------------------------------===// - -#include "KindInferenceImpl.h" - -#include "clang/Basic/SourceManager.h" - -#include "llvm/ADT/STLExtras.h" - -#include -#include -#include -#include - -namespace weavec::analysis { - -/// `record` and every record nested in it by value. -static void -forEachNestedRecord(const clang::RecordDecl *record, - const clang::ASTContext &context, - const std::function &visit, - unsigned depth = 0) { - if (record == nullptr || depth > 16) - return; - if (const clang::RecordDecl *definition = record->getDefinition()) - record = definition; - visit(*record); - for (const clang::FieldDecl *field : record->fields()) { - clang::QualType type = field->getType(); - while (const clang::ArrayType *array = context.getAsArrayType(type)) - type = array->getElementType(); - if (const clang::RecordDecl *nested = type->getAsRecordDecl()) - forEachNestedRecord(nested, context, visit, depth + 1); - } -} - -void KindInferenceState::inferFieldCandidates() { - const clang::SourceManager &sm = context.getSourceManager(); - // Before round 1 (§7.6): the writes the store-group rule cannot see drop - // every candidate of the struct. - llvm::DenseMap> - dropped; - const auto drop = [&](const clang::RecordDecl &record, std::string reason, - const clang::Stmt *where) { - const clang::RecordDecl *definition = record.getDefinition(); - dropped.try_emplace(definition != nullptr ? definition : &record, - std::move(reason), where); - }; - std::vector> - addresses(fieldAddresses.begin(), fieldAddresses.end()); - std::ranges::stable_sort(addresses, [&sm](const auto &a, const auto &b) { - return sm.isBeforeInTranslationUnit(a.second->getBeginLoc(), - b.second->getBeginLoc()); - }); - for (const auto &[field, where] : addresses) - drop(*field->getParent(), - "the address of '" + fieldName(*field) + "' is taken", where); - for (const ByteWrite &write : byteWrites) - forEachNestedRecord( - write.record, context, [&](const clang::RecordDecl &record) { - drop(record, - "an object of '" + recordName(record) + - "' is written byte-wise (" + write.demotion.reason + ")", - write.demotion.store); - }); - for (const Conversion &conversion : conversions) - forEachNestedRecord(conversion.record, context, - [&](const clang::RecordDecl &record) { - drop(record, - "an object of '" + recordName(record) + - "' is converted from another type", - conversion.where); - }); - for (const clang::RecordDecl *record : records) { - if (!record->isUnion()) - continue; - const auto members = - std::distance(record->field_begin(), record->field_end()); - for (const clang::FieldDecl *field : record->fields()) - if (const clang::RecordDecl *nested = field->getType()->getAsRecordDecl(); - nested != nullptr && members > 1) - drop(*nested, - "'" + recordName(*nested) + "' shares the union '" + - recordName(*record) + "' with other members", - nullptr); - } - - for (const clang::RecordDecl *record : records) { - if (record->isUnion() || sm.isInSystemHeader(record->getLocation())) - continue; - const std::string key = recordKey(*record, context); - if (key.empty()) - continue; - std::vector pointers; - std::vector counts; - for (const clang::FieldDecl *field : record->fields()) { - const clang::QualType type = field->getType(); - if (isObjectPointer(type)) { - // A declared kind needs no candidate; an opaque pointee has no - // element size to count. - const KindEntry *entry = kinds.field(*field); - const bool declared = entry != nullptr && entry->hasDeclaredShape(); - const clang::QualType pointee = type->getPointeeType(); - if (!declared && - (pointee->isVoidType() || !pointee->isIncompleteType())) - pointers.push_back(field); - } else if (type->isIntegerType() && !type->isBooleanType()) { - counts.push_back(field); - } - } - if (pointers.empty() || counts.empty()) - continue; - if (const auto found = dropped.find(record); found != dropped.end()) { - disqualified.push_back(DisqualifiedRecord{.record = record, - .key = key, - .reason = found->second.first, - .where = found->second.second}); - continue; - } - for (const clang::FieldDecl *pointer : pointers) { - const clang::QualType pointee = pointer->getType()->getPointeeType(); - // One unit is enough where elements are bytes: `bytes` for `void`, - // `count` for character pointees. - const bool wantCount = !pointee->isVoidType(); - const bool wantBytes = - objectWidth(pointee, context) != 1 || pointee->isVoidType(); - for (const clang::FieldDecl *count : counts) { - if (options.pruneUnrelatedCandidates && - !related.contains( - {pointer->getCanonicalDecl(), count->getCanonicalDecl()})) - continue; - for (const std::int64_t offset : {0, 1}) { - for (const bool bytes : {false, true}) { - if ((bytes && !wantBytes) || (!bytes && !wantCount)) - continue; - candidates.push_back(ResolvedCandidate{ - .candidate = - FieldCandidate{.record = key, - .pointer = pointer->getNameAsString(), - .count = count->getNameAsString(), - .bytes = bytes, - .offset = offset}, - .record = record, - .pointer = pointer, - .count = count}); - } - } - } - } - } -} - -} // namespace weavec::analysis diff --git a/lib/Analysis/KindInferenceImpl.h b/lib/Analysis/KindInferenceImpl.h deleted file mode 100644 index a8ef470c..00000000 --- a/lib/Analysis/KindInferenceImpl.h +++ /dev/null @@ -1,373 +0,0 @@ -//===- KindInferenceImpl.h - KindInference internals ------------*- C++ -*-===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// The state `KindInference` builds and `KindInferenceResult` keeps: the -// facts one walk over the unit collects, the §7.3 fixpoint's nodes, and the -// helpers the must-access (KindInferenceMustAccess.cpp) and field -// (KindInferenceFields.cpp) parts share. -// -//===----------------------------------------------------------------------===// - -#ifndef WEAVEC_LIB_ANALYSIS_KINDINFERENCEIMPL_H -#define WEAVEC_LIB_ANALYSIS_KINDINFERENCEIMPL_H - -#include "weavec/Analysis/KindInference.h" -#include "weavec/Analysis/KindTable.h" - -#include "clang/AST/ASTContext.h" -#include "clang/AST/Decl.h" -#include "clang/AST/Expr.h" -#include "clang/AST/ParentMap.h" -#include "clang/Analysis/Analyses/Dominators.h" -#include "clang/Analysis/CFG.h" - -#include "llvm/ADT/DenseMap.h" -#include "llvm/ADT/DenseSet.h" -#include "llvm/ADT/SmallVector.h" - -#include -#include -#include -#include -#include -#include - -namespace weavec::analysis { - -/// The bytes a pointer value guarantees from where it points (a §7.1 lower -/// bound), as the Single-valid rules judge it syntactically. -struct ValueWidth { - /// None: nothing is guaranteed. Zero: a valid object of unknown size, - /// which is Single-valid only for an incomplete pointee. - std::optional bytes = std::nullopt; - /// The null pointer constant: Single-valid, and no bound on the width. - bool nullOnly = false; - core::Nullability nullability = core::Nullability::Nullable; - /// Why `bytes` is none. - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::string why = {}; - - [[nodiscard]] static ValueWidth null() { - return ValueWidth{.bytes = std::nullopt, - .nullOnly = true, - .nullability = core::Nullability::Nullable, - .why = {}}; - } - [[nodiscard]] static ValueWidth unknown(std::string reason) { - return ValueWidth{.bytes = std::nullopt, - .nullOnly = false, - .nullability = core::Nullability::Nullable, - .why = std::move(reason)}; - } - [[nodiscard]] static ValueWidth - of(std::uint64_t width, - core::Nullability nullability = core::Nullability::Nullable) { - return ValueWidth{.bytes = width, - .nullOnly = false, - .nullability = nullability, - .why = {}}; - } - /// Single-valid for a pointer whose pointee needs `needed` bytes. - [[nodiscard]] bool covers(std::uint64_t needed) const { - return nullOnly || (bytes.has_value() && *bytes >= needed); - } -}; - -/// The meet of two values that may both reach one place. -[[nodiscard]] ValueWidth meet(const ValueWidth &a, const ValueWidth &b); - -/// §7.4: the object width of `pointee`: `sizeof`, or the offset of a -/// flexible trailing array member; 1 for `void`; 0 for an incomplete type. -[[nodiscard]] std::uint64_t objectWidth(clang::QualType pointee, - const clang::ASTContext &context); -/// §7.4: `field` is a trailing array member the unit's -/// `-fstrict-flex-arrays` level treats as flexible. -[[nodiscard]] bool isFlexibleArrayMember(const clang::FieldDecl &field, - const clang::ASTContext &context); -/// A pointer to an object (not a function pointer). -[[nodiscard]] bool isObjectPointer(clang::QualType type); -/// A character type (`char`, `signed char`, `unsigned char`). -[[nodiscard]] bool isCharacter(clang::QualType type); -/// `expr` without parentheses and conversions between object pointer types -/// (and to `void *`), for "the object's type" of a byte-wise write. -[[nodiscard]] const clang::Expr *stripPointerCasts(const clang::Expr *expr); -/// The function a call names, through `*`, `&` and parentheses. -[[nodiscard]] const clang::DeclRefExpr * -calleeReference(const clang::CallExpr &call); -/// The parameter `expr` reads (through parentheses and implicit casts). -[[nodiscard]] const clang::ParmVarDecl *parameterOf(const clang::Expr *expr); -/// The variable `expr` names (through parentheses and implicit casts). -[[nodiscard]] const clang::VarDecl *variableOf(const clang::Expr *expr); -/// `'.'`-style names for messages and dumps. -[[nodiscard]] std::string recordName(const clang::RecordDecl &record); -[[nodiscard]] std::string fieldName(const clang::FieldDecl &field); -/// The record type key of §7.6 and §13.1 (`struct buf`), or empty for a -/// record without a stable name. -[[nodiscard]] std::string recordKey(const clang::RecordDecl &record, - const clang::ASTContext &context); - -/// One value that reaches a §7.3 node. -struct Incoming { - /// Judged in every round; or - const clang::Expr *value = nullptr; - /// a fixed contribution (a parameter's A1 entry value, a `LibrarySpec` - /// out value); or - std::optional fixed = std::nullopt; - /// the load value of another node (a static parameter's entry). - std::optional node = std::nullopt; - /// The store, initialiser, call or `return` it arrives by. - const clang::Stmt *store = nullptr; -}; - -/// A node of the §7.3 fixpoint. -struct KindNode { - enum class Kind : std::uint8_t { - /// A pointer field (field-based: every object of the record). - Field, - /// A pointer variable with static storage. - Global, - /// The result of a function the unit defines. - Result, - /// A static function's parameter at entry: the join of its arguments. - ParamEntry, - /// The value of a local or parameter variable wherever it is read: the - /// meet of every value assigned to it. - Value, - }; - Kind kind = Kind::Field; - const clang::Decl *decl = nullptr; - clang::QualType pointee; - /// `objectWidth(pointee)`. - std::uint64_t needed = 0; - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::vector incoming = {}; - /// Stores the syntax does not show (§7.3), which demote unconditionally. - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::vector forced = {}; - - // The fixpoint state. - bool demoted = false; - /// Value nodes: the least width assigned; `Top` when only null is. - std::uint64_t least = Top; - /// Every value that reaches the node is nonnull. - bool nonnull = true; - /// The first reason the node was demoted. - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::string why = {}; - - static constexpr std::uint64_t Top = - std::numeric_limits::max(); -}; - -/// A store through `*q` whose target the syntax does not name (§7.3): it -/// reaches every address-taken slot of a compatible type. -struct IndirectStore { - const clang::Expr *value = nullptr; - /// The lvalue's pointer type (`T *` for `*q = v` with `q` a `T **`). - clang::QualType type; - const clang::Stmt *store = nullptr; - /// `++*q` and friends: pointer arithmetic, never Single-valid. - bool arithmetic = false; -}; - -/// A call argument that is the address of a slot, or a pointer that may -/// point to one (§7.3 "a slot whose address is passed to a callee"). -struct SlotAddressArgument { - const clang::CallExpr *call = nullptr; - unsigned index = 0; - /// The slot's node, or none when the argument is a `T **` value that may - /// point to any address-taken slot of a compatible type. - std::optional node = std::nullopt; - /// Argument `index`'s type. - clang::QualType type; -}; - -/// A function's CFG (every expression an element, in evaluation order) with -/// its post-dominator tree and where each statement sits (§7.5). -struct FunctionCfg { - std::unique_ptr cfg; - std::unique_ptr postDominators; - /// Statement to (block, element index). - llvm::DenseMap> - where; - /// Loop statement to the block whose terminator it is (its header). - llvm::DenseMap headers; - - [[nodiscard]] bool postDominates(const clang::CFGBlock *a, - const clang::CFGBlock *b) const { - return postDominators->dominates(a, b); - } -}; - -/// Everything `KindInference` computes; `KindInferenceResult` keeps it. -class KindInferenceState { -public: - KindInferenceState(clang::ASTContext &ctx, const core::LibrarySpec &spec, - KindInferenceOptions inferenceOptions, KindTable &table) - : context(ctx), library(spec), options(inferenceOptions), kinds(table) {} - - void run(); - - // -- Queries (valid after `run`) ------------------------------------------- - [[nodiscard]] ValueWidth judge(const clang::Expr *expr) const; - [[nodiscard]] bool alwaysReturns(const clang::FunctionDecl &function) const; - [[nodiscard]] bool knownToReturn(const clang::CallExpr &call) const; - - clang::ASTContext &context; - const core::LibrarySpec &library; - KindInferenceOptions options; - KindTable &kinds; - - // -- What the walk collects ------------------------------------------------- - struct FunctionFacts { - const clang::FunctionDecl *definition = nullptr; - bool addressTaken = false; - // NOLINTNEXTLINE(readability-redundant-member-init): designated init - std::vector calls = {}; - }; - /// By canonical declaration. - llvm::DenseMap functions; - /// Every function the unit defines, in source order. - std::vector definitions; - /// Every function the unit declares and references, in source order. - std::vector referenced; - /// The calls in each definition's body. - llvm::DenseMap> - callsIn; - - struct VariableUse { - bool assigned = false; - /// Incremented, decremented, `+=`/`-=`, or its address taken. - bool modified = false; - bool addressTaken = false; - }; - /// Parameters and locals, by declaration. - llvm::DenseMap variableUses; - - std::vector nodes; - llvm::DenseMap nodeOf; - /// Static parameters' entry nodes, by the definition's parameter. - llvm::DenseMap entryOf; - - std::vector indirectStores; - std::vector slotAddressArguments; - /// Slots whose address is taken. - llvm::DenseSet addressTaken; - /// A pointer to pointers was made from a pointer to something else (other - /// than a fresh allocation): it may point into any object, so every slot - /// counts as address-taken. - bool everySlotTaken = false; - /// A byte-wise write into an object of `record` (§7.3; §7.6): a - /// `LibrarySpec` argument with `w` or `rw` access, or a character-typed - /// store. A zero fill or a copy from the same type leaves the pointer - /// fields Single-valid (§7.3) but still disqualifies §7.6 candidates. - struct ByteWrite { - const clang::RecordDecl *record = nullptr; - // NOLINTNEXTLINE(readability-redundant-member-init): designated init - KindDemotion demotion = {}; - bool keepsPointers = false; - }; - std::vector byteWrites; - /// Union members stored, by union. - llvm::DenseMap< - const clang::RecordDecl *, - std::vector>> - unionStores; - - // §7.6 inputs. - /// Fields whose address is taken, with where. - llvm::DenseMap fieldAddresses; - /// A pointer to `record` converted from a pointer to another object type - /// (§7.6); `fromBytes` when that is a character buffer, whose bytes no - /// store into the record's fields wrote (§7.3). - struct Conversion { - const clang::RecordDecl *record = nullptr; - const clang::CastExpr *where = nullptr; - bool fromBytes = false; - }; - std::vector conversions; - /// References to each parameter of a function the unit defines, for the - /// reliance and confinement classifications. - llvm::DenseMap> - parameterReferences; - /// Records defined by the unit, in source order. - std::vector records; - /// Pairs of fields some function mentions together. - llvm::DenseSet> - related; - - // -- Results ---------------------------------------------------------------- - std::vector candidates; - std::vector disqualified; - mutable llvm::DenseMap returns; - - // -- Steps ------------------------------------------------------------------ - void collect(); - void addHiddenStores(); - void solve(); - void fillTable(); - void inferMustAccess(); - void inferFieldCandidates(); - - // Helpers shared by the steps. - [[nodiscard]] std::optional - slotNode(const clang::Expr *lvalue) const; - [[nodiscard]] ValueWidth loadOf(unsigned index) const; - [[nodiscard]] ValueWidth judgeLoad(const clang::Expr *lvalue) const; - [[nodiscard]] ValueWidth judgeAddress(const clang::Expr *lvalue) const; - [[nodiscard]] ValueWidth judgeArray(const clang::Expr *array) const; - [[nodiscard]] ValueWidth judgeCall(const clang::CallExpr &call) const; - [[nodiscard]] ValueWidth declaredWidth(const KindEntry &entry, - clang::QualType pointee, - const clang::CallExpr *call) const; - [[nodiscard]] std::optional - constantOf(const clang::Expr *expr) const; - /// §7.3: `function` is `main(int argc, char **argv[, ...])` and neither - /// `argc` nor `argv` is ever assigned. - [[nodiscard]] bool isMainArgv(const clang::FunctionDecl &definition) const; - /// Whether the static function `function` gets the §7.3 join. - [[nodiscard]] bool joinsArguments(const clang::FunctionDecl &function) const; - /// Whether `param` (of a function the unit defines) only ever has - /// `*param` read or written and is compared, never copied, passed on, - /// returned or modified: a callee whose stores through it the - /// indirect-store rule already sees. - [[nodiscard]] bool confinedParameter(const clang::ParmVarDecl ¶m) const; - /// §7.3 `reliesOnSingle`: some use of `param`'s value may rest on its - /// Single default (every use but comparisons, conversions to integers, - /// pointer arithmetic, non-zero subscripts and assignments to it). - [[nodiscard]] bool reliesOnDefault(const clang::ParmVarDecl ¶m) const; - /// The value of a `LibrarySpec` result or out value at `call`. - [[nodiscard]] ValueWidth libraryValue(const core::LibraryResult &result, - const core::LibraryMatch &match, - const clang::CallExpr &call, - clang::QualType pointee) const; - /// The CFG of a function the unit defines, built on first use; null when - /// Clang cannot build one. - [[nodiscard]] const FunctionCfg * - cfgOf(const clang::FunctionDecl &function) const; - mutable llvm::DenseMap> - cfgs; - /// The parent map of a function's body, built on first use. - [[nodiscard]] const clang::ParentMap & - parentsOf(const clang::FunctionDecl &function) const; - mutable llvm::DenseMap> - parentMaps; - [[nodiscard]] std::string nodeName(unsigned index) const; - unsigned addNode(KindNode::Kind kind, const clang::Decl *decl, - clang::QualType pointee); - void demote(unsigned index, const clang::Stmt *store, std::string reason); -}; - -} // namespace weavec::analysis - -#endif // WEAVEC_LIB_ANALYSIS_KINDINFERENCEIMPL_H diff --git a/lib/Analysis/KindInferenceMustAccess.cpp b/lib/Analysis/KindInferenceMustAccess.cpp deleted file mode 100644 index 5293b5fd..00000000 --- a/lib/Analysis/KindInferenceMustAccess.cpp +++ /dev/null @@ -1,1366 +0,0 @@ -//===- KindInferenceMustAccess.cpp - Must-access requirements (RFC 0030) --===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// §7.5: requirements R1–R5 for each pointer parameter, from the function's -// CFG and post-dominator tree alone. An access is a must-access when its -// block post-dominates the entry (or, inside a canonical counted loop, the -// loop's header post-dominates the entry and the access's block -// post-dominates the loop body's entry), and nothing that may run before it -// assigns the parameter, tests its nullness, or calls a function not known -// to return. -// -// "Known to return" for a function the unit defines is the syntactic -// greatest fixpoint documented in KindInference.h. -// -//===----------------------------------------------------------------------===// - -#include "KindInferenceImpl.h" - -#include "clang/AST/RecursiveASTVisitor.h" -#include "clang/Basic/Builtins.h" -#include "clang/Basic/SourceManager.h" - -#include "llvm/ADT/STLExtras.h" -#include "llvm/ADT/ScopeExit.h" -#include "llvm/Support/CheckedArithmetic.h" - -#include -#include - -namespace weavec::analysis { - -const FunctionCfg * -KindInferenceState::cfgOf(const clang::FunctionDecl &function) const { - std::unique_ptr &slot = cfgs[&function]; - if (slot != nullptr) - return slot->cfg != nullptr ? slot.get() : nullptr; - slot = std::make_unique(); - clang::CFG::BuildOptions build; - build.setAllAlwaysAdd(); - // `buildCFG` takes a mutable body but only reads it. - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast) - slot->cfg = clang::CFG::buildCFG( - &function, const_cast(function.getBody()), &context, - build); - if (slot->cfg == nullptr) - return nullptr; - slot->postDominators = - std::make_unique(slot->cfg.get()); - for (const clang::CFGBlock *block : *slot->cfg) { - unsigned index = 0; - for (const clang::CFGElement &element : *block) { - if (const auto stmt = element.getAs()) - slot->where.try_emplace(stmt->getStmt(), block, index); - ++index; - } - if (const clang::Stmt *terminator = block->getTerminatorStmt()) - slot->headers.try_emplace(terminator, block); - } - return slot.get(); -} - -// -- Known to return (§7.5) -// ---------------------------------------------------- - -/// Known to return without looking at the unit's own definitions: none for -/// a call to a function the unit defines. -static std::optional returnsOutsideUnit(const clang::CallExpr &call, - const core::LibrarySpec &library, - const clang::SourceManager &sm) { - const clang::FunctionDecl *callee = call.getDirectCallee(); - if (callee == nullptr) - return false; // indirect: unknown - if (callee->isNoReturn()) - return false; - if (const auto match = governingLibraryEntry(*callee, library)) - return match->entry->knownToReturn(); - if (callee->getDefinition() != nullptr) - return std::nullopt; - if (callee->getBuiltinID() != 0) - return true; - return isPlatformDeclaration(*callee, library, sm); -} - -bool KindInferenceState::alwaysReturns( - const clang::FunctionDecl &function) const { - if (returns.empty() && !definitions.empty()) { - // A greatest fixpoint: every definition returns until one of its calls - // may not, which then spreads to its callers. - const clang::SourceManager &sm = context.getSourceManager(); - llvm::DenseMap> - callers; - std::vector falling; - for (const clang::FunctionDecl *definition : definitions) - returns[definition->getCanonicalDecl()] = !definition->isNoReturn(); - for (const clang::FunctionDecl *definition : definitions) { - const clang::FunctionDecl *caller = definition->getCanonicalDecl(); - if (const auto calls = callsIn.find(definition); calls != callsIn.end()) { - for (const clang::CallExpr *call : calls->second) { - const auto outside = returnsOutsideUnit(*call, library, sm); - if (!outside) - callers[call->getDirectCallee()->getCanonicalDecl()].push_back( - caller); - else if (!*outside) - returns[caller] = false; - } - } - if (!returns[caller]) - falling.push_back(caller); - } - while (!falling.empty()) { - const clang::FunctionDecl *callee = falling.back(); - falling.pop_back(); - for (const clang::FunctionDecl *caller : callers[callee]) { - bool &flag = returns[caller]; - if (flag) { - flag = false; - falling.push_back(caller); - } - } - } - } - const auto found = returns.find(function.getCanonicalDecl()); - return found != returns.end() && found->second; -} - -bool KindInferenceState::knownToReturn(const clang::CallExpr &call) const { - const auto outside = - returnsOutsideUnit(call, library, context.getSourceManager()); - if (outside) - return *outside; - return alwaysReturns(*call.getDirectCallee()->getDefinition()); -} - -// -- Syntax helpers -// --------------------------------------------------------------- - -/// `stmt` assigns, increments or decrements `variable`. -static bool modifies(const clang::Stmt *stmt, const clang::VarDecl *variable) { - if (const auto *binary = llvm::dyn_cast(stmt)) - return binary->isAssignmentOp() && variableOf(binary->getLHS()) == variable; - if (const auto *unary = llvm::dyn_cast(stmt)) - return (unary->isIncrementDecrementOp() || - unary->getOpcode() == clang::UO_AddrOf) && - variableOf(unary->getSubExpr()) == variable; - return false; -} - -/// Whether anything in `root` other than `except` modifies `variable`. -static bool modifiedIn(const clang::Stmt *root, const clang::VarDecl *variable, - const clang::Stmt *except = nullptr) { - if (root == nullptr) - return false; - if (root != except && modifies(root, variable)) - return true; - return llvm::any_of(root->children(), [&](const clang::Stmt *child) { - return modifiedIn(child, variable, except); - }); -} - -/// `condition` tests `param` against null (`p`, `!p`, `p == NULL`, and the -/// same inside `&&`, `||` and `__builtin_expect`). -static bool testsNullness(const clang::Expr *condition, - const clang::VarDecl ¶m, - clang::ASTContext &context) { - if (condition == nullptr) - return false; - condition = condition->IgnoreParenImpCasts(); - if (variableOf(condition) == ¶m) - return true; - if (const auto *unary = llvm::dyn_cast(condition)) - return unary->getOpcode() == clang::UO_LNot && - testsNullness(unary->getSubExpr(), param, context); - if (const auto *call = llvm::dyn_cast(condition)) { - const unsigned builtin = call->getBuiltinCallee(); - return (builtin == clang::Builtin::BI__builtin_expect || - builtin == clang::Builtin::BI__builtin_expect_with_probability) && - call->getNumArgs() > 0 && - testsNullness(call->getArg(0), param, context); - } - const auto *binary = llvm::dyn_cast(condition); - if (binary == nullptr) - return false; - if (binary->isLogicalOp()) - return testsNullness(binary->getLHS(), param, context) || - testsNullness(binary->getRHS(), param, context); - if (!binary->isEqualityOp()) - return false; - const auto isNull = [&](const clang::Expr *side) { - return side->isNullPointerConstant( - context, clang::Expr::NPC_ValueDependentIsNotNull) != - clang::Expr::NPCK_NotNull; - }; - return (variableOf(binary->getLHS()) == ¶m && isNull(binary->getRHS())) || - (variableOf(binary->getRHS()) == ¶m && isNull(binary->getLHS())); -} - -/// A `break` (not inside a nested loop or switch), `return` or `goto` in a -/// loop body: some iteration may not run to its end (§7.5). -static bool leavesEarly(const clang::Stmt *body, unsigned depth = 0) { - if (body == nullptr) - return false; - if (llvm::isa(body) || llvm::isa(body) || - llvm::isa(body)) - return true; - if (llvm::isa(body)) - return depth == 0; - const bool breakable = - llvm::isa(body) || llvm::isa(body) || - llvm::isa(body) || llvm::isa(body); - return llvm::any_of(body->children(), [&](const clang::Stmt *child) { - return leavesEarly(child, depth + (breakable ? 1 : 0)); - }); -} - -/// `stmt` is `++v`, `v++`, `v += 1` or `v = v + 1`. -static bool incrementsByOne(const clang::Stmt *stmt, - const clang::VarDecl *variable, - const KindInferenceState &state) { - if (stmt == nullptr || variable == nullptr) - return false; - if (const auto *expr = llvm::dyn_cast(stmt)) - stmt = expr->IgnoreParenImpCasts(); - if (const auto *unary = llvm::dyn_cast(stmt)) - return unary->isIncrementOp() && - variableOf(unary->getSubExpr()) == variable; - const auto *binary = llvm::dyn_cast(stmt); - if (binary == nullptr || variableOf(binary->getLHS()) != variable) - return false; - if (binary->getOpcode() == clang::BO_AddAssign) - return state.constantOf(binary->getRHS()) == 1; - if (binary->getOpcode() != clang::BO_Assign) - return false; - const auto *sum = llvm::dyn_cast( - binary->getRHS()->IgnoreParenImpCasts()); - if (sum == nullptr || sum->getOpcode() != clang::BO_Add) - return false; - return (variableOf(sum->getLHS()) == variable && - state.constantOf(sum->getRHS()) == 1) || - (variableOf(sum->getRHS()) == variable && - state.constantOf(sum->getLHS()) == 1); -} - -/// `v`, `v + k`, `k + v` or `v - k`: the constant `k` (negated for `-`). -static std::optional offsetFrom(const clang::Expr *index, - const clang::VarDecl *variable, - const KindInferenceState &state) { - index = index->IgnoreParenImpCasts(); - if (variableOf(index) == variable) - return 0; - const auto *binary = llvm::dyn_cast(index); - if (binary == nullptr) - return std::nullopt; - if (binary->getOpcode() == clang::BO_Add) { - if (variableOf(binary->getLHS()) == variable) - return state.constantOf(binary->getRHS()); - if (variableOf(binary->getRHS()) == variable) - return state.constantOf(binary->getLHS()); - } - if (binary->getOpcode() == clang::BO_Sub && - variableOf(binary->getLHS()) == variable) - if (const auto k = state.constantOf(binary->getRHS())) - return llvm::checkedMul(*k, std::int64_t{-1}); - return std::nullopt; -} - -/// An integer parameter of `function` that nothing assigns. -static std::optional -unmodifiedParameter(const clang::Expr *expr, - const clang::FunctionDecl &function, - const KindInferenceState &state) { - const clang::ParmVarDecl *param = parameterOf(expr); - if (param == nullptr || param->getDeclContext() != &function) - return std::nullopt; - if (const auto use = state.variableUses.find(param); - use != state.variableUses.end() && - (use->second.assigned || use->second.modified)) - return std::nullopt; - return param->getFunctionScopeIndex(); -} - -/// §7.5: `expr` as an affine term over `function`'s unmodified integer -/// parameters (at most one), with constant scale and offset. -static std::optional -parameterTerm(const clang::Expr *expr, const clang::FunctionDecl &function, - const KindInferenceState &state, unsigned depth = 0) { - if (expr == nullptr || depth > 8) - return std::nullopt; - expr = expr->IgnoreParenImpCasts(); - if (const auto constant = state.constantOf(expr)) - return core::ExtentTerm::constant(*constant); - if (const auto index = unmodifiedParameter(expr, function, state)) { - if (!parameterOf(expr)->getType()->isIntegerType()) - return std::nullopt; - return core::ExtentTerm::of(core::ExtentPath::ofParam(*index)); - } - const auto *binary = llvm::dyn_cast(expr); - if (binary == nullptr) - return std::nullopt; - const auto lhs = parameterTerm(binary->getLHS(), function, state, depth + 1); - const auto rhs = parameterTerm(binary->getRHS(), function, state, depth + 1); - if (!lhs || !rhs) - return std::nullopt; - switch (binary->getOpcode()) { - case clang::BO_Add: - case clang::BO_Sub: { - if (!rhs->isConstant() && - (binary->getOpcode() == clang::BO_Sub || !lhs->isConstant())) - return std::nullopt; - const core::ExtentTerm &variable = rhs->isConstant() ? *lhs : *rhs; - const std::int64_t constant = rhs->isConstant() ? rhs->offset : lhs->offset; - const auto offset = binary->getOpcode() == clang::BO_Add - ? llvm::checkedAdd(variable.offset, constant) - : llvm::checkedSub(variable.offset, constant); - if (!offset) - return std::nullopt; - core::ExtentTerm out = variable; - out.offset = *offset; - return out; - } - case clang::BO_Mul: { - if (!lhs->isConstant() && !rhs->isConstant()) - return std::nullopt; - const core::ExtentTerm &variable = lhs->isConstant() ? *rhs : *lhs; - const std::int64_t factor = lhs->isConstant() ? lhs->offset : rhs->offset; - const auto scale = llvm::checkedMul(variable.scale, factor); - const auto offset = llvm::checkedMul(variable.offset, factor); - if (!scale || !offset) - return std::nullopt; - core::ExtentTerm out = variable; - if (!out.isConstant()) - out.scale = *scale; - out.offset = *offset; - return out; - } - default: - return std::nullopt; - } -} - -// -- Must-access conditions -// ------------------------------------------------------ - -namespace { - -/// What checking one function's accesses needs. -struct MustAccessContext { - const KindInferenceState &state; - const clang::FunctionDecl &function; - const FunctionCfg &cfg; - const clang::ParentMap &parents; -}; - -/// A canonical counted loop `for (i = c; i < e; ++i)` (§7.5 R2). -struct CountedLoop { - const clang::ForStmt *loop = nullptr; - const clang::VarDecl *index = nullptr; - std::int64_t start = 0; - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - core::ExtentTerm end = {}; - /// `i <= e`. - bool inclusive = false; - - [[nodiscard]] RequirementGuard guard() const { - return RequirementGuard{ - .lhs = core::ExtentTerm::constant(start), - .relation = inclusive ? RequirementGuard::Relation::LessEqual - : RequirementGuard::Relation::Less, - .rhs = end}; - } - /// False when the guard is constantly false: the loop never runs. - [[nodiscard]] bool mayRun() const { - if (!end.isConstant()) - return true; - return inclusive ? start <= end.offset : start < end.offset; - } -}; - -} // namespace - -/// The blocks that may run before `block` on a path from the entry, or from -/// `stop` (included, not passed), never entering `excluded`. -static std::vector -blocksBefore(const clang::CFGBlock *block, - const llvm::DenseSet &excluded, - const clang::CFGBlock *stop) { - std::vector out; - llvm::DenseSet seen; - seen.insert(block); - std::vector work; - const auto push = [&](const clang::CFGBlock *from) { - for (const clang::CFGBlock::AdjacentBlock &pred : from->preds()) { - const clang::CFGBlock *previous = pred.getReachableBlock(); - if (previous != nullptr && !excluded.contains(previous) && - seen.insert(previous).second) - work.push_back(previous); - } - }; - if (block != stop) - push(block); - while (!work.empty()) { - const clang::CFGBlock *next = work.back(); - work.pop_back(); - out.push_back(next); - if (next != stop) - push(next); - } - return out; -} - -/// Nothing among the first `limit` elements of `block` breaks a -/// must-access of `param`: a call not known to return, or a modification -/// of `param` other than `allowed`. -static bool cleanElements(const KindInferenceState &state, - const clang::CFGBlock &block, unsigned limit, - const clang::ParmVarDecl ¶m, - const clang::Stmt *allowed) { - unsigned index = 0; - for (const clang::CFGElement &element : block) { - if (index++ >= limit) - break; - const auto stmt = element.getAs(); - if (!stmt) - continue; - const clang::Stmt *each = stmt->getStmt(); - if (const auto *call = llvm::dyn_cast(each); - call != nullptr && !state.knownToReturn(*call)) - return false; - if (each != allowed && modifies(each, ¶m)) - return false; - } - return true; -} - -/// §7.5's conditions on everything that may run before the first `limit` -/// elements of `block`. -static bool cleanBefore(const MustAccessContext &c, - const clang::ParmVarDecl ¶m, - const clang::CFGBlock *block, unsigned limit, - const llvm::DenseSet &excluded, - const clang::CFGBlock *stop, - const clang::Stmt *allowed) { - for (const clang::CFGBlock *before : blocksBefore(block, excluded, stop)) { - if (!cleanElements(c.state, *before, ~0U, param, allowed)) - return false; - if (testsNullness(llvm::dyn_cast_or_null( - before->getTerminatorCondition()), - param, c.state.context)) - return false; - } - return cleanElements(c.state, *block, limit, param, allowed); -} - -/// The blocks of a loop: those the body's entry reaches without passing its -/// header. -static llvm::DenseSet -loopBlocks(const clang::CFGBlock *header, const clang::CFGBlock *body) { - llvm::DenseSet out; - out.insert(body); - std::vector work{body}; - while (!work.empty()) { - const clang::CFGBlock *next = work.back(); - work.pop_back(); - for (const clang::CFGBlock::AdjacentBlock &succ : next->succs()) { - const clang::CFGBlock *after = succ.getReachableBlock(); - if (after != nullptr && after != header && out.insert(after).second) - work.push_back(after); - } - } - return out; -} - -/// An unguarded must-access at element `limit` of `block` (§7.5). -static bool mustAccessAlways(const MustAccessContext &c, - const clang::ParmVarDecl ¶m, - const clang::CFGBlock *block, unsigned limit) { - if (!c.cfg.postDominates(block, &c.cfg.cfg->getEntry())) - return false; - return cleanBefore(c, param, block, limit, {}, nullptr, nullptr); -} - -/// §7.5 inside a canonical loop: its header post-dominates the entry, -/// nothing before it breaks a must-access, every call in the loop returns, -/// `param` changes in the loop only by `allowed`, and the access at element -/// `limit` of `block` runs on every iteration (or is in the condition). -static bool mustAccessInLoop(const MustAccessContext &c, - const clang::ParmVarDecl ¶m, - const clang::Stmt &loop, - const clang::CFGBlock *block, unsigned limit, - const clang::Stmt *allowed) { - const auto found = c.cfg.headers.find(&loop); - if (found == c.cfg.headers.end()) - return false; - const clang::CFGBlock *header = found->second; - if (!c.cfg.postDominates(header, &c.cfg.cfg->getEntry()) || - header->succ_empty()) - return false; - const clang::CFGBlock *body = header->succ_begin()->getReachableBlock(); - if (body == nullptr) - return false; - const llvm::DenseSet inside = - loopBlocks(header, body); - if (!cleanBefore(c, param, header, header->size(), inside, nullptr, - allowed) || - testsNullness( - llvm::dyn_cast_or_null(header->getTerminatorCondition()), - param, c.state.context)) - return false; - for (const clang::CFGBlock *each : inside) - if (!cleanElements(c.state, *each, ~0U, param, allowed)) - return false; - if (block == header) - return true; - if (!inside.contains(block) || !c.cfg.postDominates(block, body)) - return false; - llvm::DenseSet outside; - outside.insert(header); - for (const clang::CFGBlock *before : blocksBefore(block, outside, body)) - if (testsNullness(llvm::dyn_cast_or_null( - before->getTerminatorCondition()), - param, c.state.context)) - return false; - (void)limit; - return true; -} - -// -- Canonical loops (§7.5) -// ------------------------------------------------------- - -/// The variable a loop's init sets and the value it sets it to: -/// `v = x` or `T v = x`. -static std::pair -initialisation(const clang::Stmt *init) { - if (const auto *assign = llvm::dyn_cast_or_null(init); - assign != nullptr && assign->getOpcode() == clang::BO_Assign) - return {variableOf(assign->getLHS()), assign->getRHS()}; - if (const auto *decl = llvm::dyn_cast_or_null(init); - decl != nullptr && decl->isSingleDecl()) - if (const auto *variable = - llvm::dyn_cast(decl->getSingleDecl()); - variable != nullptr && variable->getInit() != nullptr) - return {variable, variable->getInit()}; - return {nullptr, nullptr}; -} - -/// `v < e`, `e > v`, `v != e`, `e != v` (exclusive), `v <= e`, `e >= v` -/// (inclusive): the bound `e`. -static std::pair -upperBound(const clang::Expr *condition, const clang::VarDecl *variable) { - const auto *compare = llvm::dyn_cast_or_null( - condition != nullptr ? condition->IgnoreParenImpCasts() : nullptr); - if (compare == nullptr || variable == nullptr) - return {nullptr, false}; - const bool left = variableOf(compare->getLHS()) == variable; - const bool right = variableOf(compare->getRHS()) == variable; - switch (compare->getOpcode()) { - case clang::BO_LT: - return {left ? compare->getRHS() : nullptr, false}; - case clang::BO_GT: - return {right ? compare->getLHS() : nullptr, false}; - case clang::BO_NE: - if (left) - return {compare->getRHS(), false}; - return {right ? compare->getLHS() : nullptr, false}; - case clang::BO_LE: - return {left ? compare->getRHS() : nullptr, true}; - case clang::BO_GE: - return {right ? compare->getLHS() : nullptr, true}; - default: - return {nullptr, false}; - } -} - -/// §7.5 R2: `for (i = c; i < e; ++i)` with `c >= 0` constant, `e` affine -/// over parameters, no `break`, `return` or `goto` out of the body, and `i` -/// not otherwise modified in it. -static std::optional countedLoop(const clang::ForStmt &loop, - const MustAccessContext &c) { - const auto [index, start] = initialisation(loop.getInit()); - if (index == nullptr || index->hasGlobalStorage() || - !index->getType()->isIntegerType()) - return std::nullopt; - // An index whose address is taken may change through a pointer. - if (const auto use = c.state.variableUses.find(index); - use != c.state.variableUses.end() && use->second.addressTaken) - return std::nullopt; - const auto first = c.state.constantOf(start); - if (!first || *first < 0) - return std::nullopt; - const auto [bound, inclusive] = upperBound(loop.getCond(), index); - if (bound == nullptr) - return std::nullopt; - const auto end = parameterTerm(bound, c.function, c.state); - if (!end || !incrementsByOne(loop.getInc(), index, c.state) || - leavesEarly(loop.getBody()) || modifiedIn(loop.getBody(), index)) - return std::nullopt; - CountedLoop counted{.loop = &loop, - .index = index, - .start = *first, - .end = *end, - .inclusive = inclusive}; - if (!counted.mayRun()) - return std::nullopt; - return counted; -} - -/// A pointer parameter of the function that nothing assigns. -static const clang::ParmVarDecl *unmodifiedPointer(const clang::Expr *expr, - const MustAccessContext &c) { - const clang::ParmVarDecl *param = parameterOf(expr); - if (param == nullptr || param->getDeclContext() != &c.function || - !isObjectPointer(param->getType())) - return nullptr; - const auto use = c.state.variableUses.find(param); - if (use != c.state.variableUses.end() && - (use->second.assigned || use->second.modified)) - return nullptr; - return param; -} - -namespace { -/// §7.5 R3: `for (x = p; x < q; ++x)` or `while (p < q) { ...; p++; }`. -struct RangeLoop { - const clang::Stmt *loop = nullptr; - const clang::VarDecl *cursor = nullptr; - const clang::ParmVarDecl *start = nullptr; - const clang::ParmVarDecl *end = nullptr; - bool inclusive = false; - /// The while form's closing increment of the parameter itself. - const clang::Stmt *increment = nullptr; -}; -} // namespace - -static std::optional rangeLoop(const clang::Stmt &loop, - const MustAccessContext &c) { - RangeLoop range; - range.loop = &loop; - const clang::Expr *condition = nullptr; - if (const auto *forLoop = llvm::dyn_cast(&loop)) { - const auto [cursor, init] = initialisation(forLoop->getInit()); - const clang::ParmVarDecl *start = - init != nullptr ? parameterOf(init) : nullptr; - if (cursor == nullptr || cursor->hasGlobalStorage() || start == nullptr || - start->getDeclContext() != &c.function || - !isObjectPointer(start->getType()) || - !incrementsByOne(forLoop->getInc(), cursor, c.state) || - leavesEarly(forLoop->getBody()) || - modifiedIn(forLoop->getBody(), cursor)) - return std::nullopt; - range.cursor = cursor; - range.start = start; - condition = forLoop->getCond(); - } else if (const auto *whileLoop = llvm::dyn_cast(&loop)) { - const auto *body = - llvm::dyn_cast(whileLoop->getBody()); - const auto *compare = llvm::dyn_cast_or_null( - whileLoop->getCond()->IgnoreParenImpCasts()); - if (body == nullptr || body->body_empty() || compare == nullptr) - return std::nullopt; - const clang::ParmVarDecl *start = parameterOf(compare->getLHS()); - if (start == nullptr || start->getDeclContext() != &c.function || - !isObjectPointer(start->getType()) || - !incrementsByOne(body->body_back(), start, c.state) || - leavesEarly(body) || modifiedIn(body, start, body->body_back())) - return std::nullopt; - range.cursor = start; - range.start = start; - range.increment = body->body_back(); - if (const auto *expr = llvm::dyn_cast(range.increment)) - range.increment = expr->IgnoreParenImpCasts(); - condition = compare; - } else { - return std::nullopt; - } - const auto [bound, inclusive] = upperBound(condition, range.cursor); - range.end = bound != nullptr ? unmodifiedPointer(bound, c) : nullptr; - if (range.end == nullptr || range.end == range.start || - !clang::ASTContext::hasSameUnqualifiedType( - range.end->getType()->getPointeeType(), - range.start->getType()->getPointeeType())) - return std::nullopt; - range.inclusive = inclusive; - return range; -} - -namespace { -/// §7.5 R4: `while (*s) s++`, `while (p[i]) i++`, `for (; *s; ++s)`. -struct Scan { - const clang::Stmt *loop = nullptr; - const clang::ParmVarDecl *param = nullptr; - /// The condition's read of the element. - const clang::Expr *read = nullptr; - /// The increment of the parameter itself, when it is the cursor. - const clang::Stmt *increment = nullptr; -}; -} // namespace - -/// Counts the statements that modify `variable` in `root`. -static unsigned modificationCount(const clang::Stmt *root, - const clang::VarDecl *variable) { - if (root == nullptr) - return 0; - unsigned count = modifies(root, variable) ? 1 : 0; - for (const clang::Stmt *child : root->children()) - count += modificationCount(child, variable); - return count; -} - -/// A scan's condition `E`, `E != 0` or `0 != E`: the element read `E`. -static const clang::Expr *scannedElement(const clang::Expr *condition, - const KindInferenceState &state) { - if (condition == nullptr) - return nullptr; - condition = condition->IgnoreParenImpCasts(); - if (const auto *compare = llvm::dyn_cast(condition); - compare != nullptr && compare->getOpcode() == clang::BO_NE) { - if (state.constantOf(compare->getRHS()) == 0) - return compare->getLHS()->IgnoreParenImpCasts(); - if (state.constantOf(compare->getLHS()) == 0) - return compare->getRHS()->IgnoreParenImpCasts(); - return nullptr; - } - return condition; -} - -static std::optional scanLoop(const clang::Stmt &loop, - const MustAccessContext &c) { - const clang::Expr *condition = nullptr; - const clang::Stmt *increment = nullptr; - const clang::Stmt *body = nullptr; - const clang::Stmt *init = nullptr; - if (const auto *whileLoop = llvm::dyn_cast(&loop)) { - // The body is the increment alone. - condition = whileLoop->getCond(); - increment = whileLoop->getBody(); - if (const auto *compound = llvm::dyn_cast(increment)) { - if (compound->size() != 1) - return std::nullopt; - increment = compound->body_front(); - } - } else if (const auto *forLoop = llvm::dyn_cast(&loop)) { - condition = forLoop->getCond(); - increment = forLoop->getInc(); - body = forLoop->getBody(); - init = forLoop->getInit(); - if (leavesEarly(body)) - return std::nullopt; - } else { - return std::nullopt; - } - const clang::Expr *element = scannedElement(condition, c.state); - const clang::Expr *base = nullptr; - const clang::Expr *index = nullptr; - if (const auto *unary = llvm::dyn_cast_or_null(element); - unary != nullptr && unary->getOpcode() == clang::UO_Deref) { - base = unary->getSubExpr(); - } else if (const auto *subscript = - llvm::dyn_cast_or_null(element)) { - base = subscript->getBase(); - index = subscript->getIdx(); - if (c.state.constantOf(index) == 0) - index = nullptr; // `s[0]` - } - const clang::VarDecl *cursor = base != nullptr ? variableOf(base) : nullptr; - if (cursor == nullptr || cursor->hasGlobalStorage() || - !isObjectPointer(cursor->getType())) - return std::nullopt; - if (const auto *expr = llvm::dyn_cast_or_null(increment)) - increment = expr->IgnoreParenImpCasts(); - Scan scan{ - .loop = &loop, .param = nullptr, .read = element, .increment = nullptr}; - const auto *param = llvm::dyn_cast(cursor); - if (index != nullptr) { - // `p[i]`: the parameter itself, and an index from zero. - const clang::VarDecl *counter = variableOf(index); - if (param == nullptr || counter == nullptr || counter->hasGlobalStorage() || - !incrementsByOne(increment, counter, c.state) || - modifiedIn(body, param)) - return std::nullopt; - if (const auto [variable, value] = initialisation(init); - variable != nullptr) { - if (variable != counter || c.state.constantOf(value) != 0 || - modifiedIn(body, counter)) - return std::nullopt; - } else if (init != nullptr || counter->getInit() == nullptr || - c.state.constantOf(counter->getInit()) != 0 || - modificationCount(c.function.getBody(), counter) != 1) { - return std::nullopt; - } - scan.param = param; - } else if (param != nullptr) { - // `while (*p) p++`, `for (; *p; ++p)`: the parameter is the cursor. - if (init != nullptr || !incrementsByOne(increment, param, c.state) || - modifiedIn(body, param)) - return std::nullopt; - scan.param = param; - scan.increment = increment; - } else { - // A local cursor that starts at the parameter. - if (!incrementsByOne(increment, cursor, c.state)) - return std::nullopt; - const clang::Expr *start = nullptr; - if (const auto [variable, value] = initialisation(init); - variable != nullptr) { - if (variable != cursor || modifiedIn(body, cursor)) - return std::nullopt; - start = value; - } else if (init == nullptr && - modificationCount(c.function.getBody(), cursor) == 1) { - start = cursor->getInit(); - } - scan.param = start != nullptr ? parameterOf(start) : nullptr; - if (scan.param == nullptr || modifiedIn(body, scan.param)) - return std::nullopt; - } - if (scan.param->getDeclContext() != &c.function || - !isObjectPointer(scan.param->getType())) - return std::nullopt; - return scan; -} - -// -- Accesses -// ---------------------------------------------------------------------- - -namespace { -/// An access through a pointer variable (§7.5). -struct Access { - /// The dereference, member access, subscript, or the call. - const clang::Expr *expr = nullptr; - const clang::VarDecl *base = nullptr; - /// `*p`, `p->f`: an element; `p[e]`, `*(p + e)`: an index; a string - /// argument of a `LibrarySpec` row (R4). - enum class Form : std::uint8_t { Element, Index, String }; - Form form = Form::Element; - const clang::Expr *index = nullptr; - core::Nullability nullability = core::Nullability::Nonnull; -}; -} // namespace - -/// The lvalue only designates an object whose address is taken: `&p->f`, -/// or `p->a` decaying into a pointer nothing dereferences. -static bool addressOnly(const clang::Expr *expr, - const clang::ParentMap &parents) { - const clang::Stmt *child = expr; - const clang::Stmt *parent = parents.getParent(child); - while (parent != nullptr) { - const auto *member = llvm::dyn_cast(parent); - if (llvm::isa(parent) || - (member != nullptr && !member->isArrow() && - member->getBase() == child)) { - child = parent; - parent = parents.getParent(parent); - continue; - } - if (const auto *cast = llvm::dyn_cast(parent); - cast != nullptr && - cast->getCastKind() == clang::CK_ArrayToPointerDecay) { - const clang::Stmt *user = parents.getParentIgnoreParens(parent); - if (const auto *subscript = - llvm::dyn_cast_or_null(user); - subscript != nullptr && - subscript->getBase()->IgnoreParens() == parent) - return false; - if (const auto *unary = - llvm::dyn_cast_or_null(user); - unary != nullptr && unary->getOpcode() == clang::UO_Deref) - return false; - return true; - } - const auto *unary = llvm::dyn_cast(parent); - return unary != nullptr && unary->getOpcode() == clang::UO_AddrOf; - } - return false; -} - -/// Every access in `stmt` through a variable `wanted` accepts, outside -/// unevaluated operands. -static void -collectAccesses(const clang::Stmt *stmt, const MustAccessContext &c, - const std::function &wanted, - std::vector &out) { - if (stmt == nullptr || llvm::isa(stmt)) - return; - const auto base = [&](const clang::Expr *expr) -> const clang::VarDecl * { - const clang::VarDecl *variable = variableOf(expr); - return variable != nullptr && wanted(variable) ? variable : nullptr; - }; - const auto push = [&](const clang::Expr *expr, const clang::VarDecl *variable, - Access::Form form, const clang::Expr *index) { - if (variable != nullptr && !addressOnly(expr, c.parents)) - out.push_back(Access{.expr = expr, - .base = variable, - .form = form, - .index = index, - .nullability = core::Nullability::Nonnull}); - }; - if (const auto *unary = llvm::dyn_cast(stmt); - unary != nullptr && unary->getOpcode() == clang::UO_Deref) { - const clang::Expr *sub = unary->getSubExpr()->IgnoreParenImpCasts(); - if (const clang::VarDecl *variable = base(sub)) { - push(unary, variable, Access::Form::Element, nullptr); - } else if (const auto *sum = llvm::dyn_cast(sub); - sum != nullptr && sum->getOpcode() == clang::BO_Add) { - if (const clang::VarDecl *left = base(sum->getLHS())) - push(unary, left, Access::Form::Index, sum->getRHS()); - else if (const clang::VarDecl *right = base(sum->getRHS())) - push(unary, right, Access::Form::Index, sum->getLHS()); - } - } else if (const auto *member = llvm::dyn_cast(stmt); - member != nullptr && member->isArrow()) { - push(member, base(member->getBase()), Access::Form::Element, nullptr); - } else if (const auto *subscript = - llvm::dyn_cast(stmt)) { - push(subscript, base(subscript->getBase()), Access::Form::Index, - subscript->getIdx()); - } else if (const auto *call = llvm::dyn_cast(stmt)) { - // R4: a string argument of a `LibrarySpec` row. - const clang::FunctionDecl *callee = call->getDirectCallee(); - const auto match = callee != nullptr - ? governingLibraryEntry(*callee, c.state.library) - : std::nullopt; - for (unsigned i = 0; match && i < call->getNumArgs(); ++i) { - const core::LibraryParam *param = match->param(i); - const clang::VarDecl *variable = - param != nullptr && param->string - ? base(stripPointerCasts(call->getArg(i))) - : nullptr; - if (variable != nullptr) - out.push_back(Access{ - .expr = call, - .base = variable, - .form = Access::Form::String, - .index = nullptr, - .nullability = param->null == core::LibraryParam::Null::Forbidden - ? core::Nullability::Nonnull - : core::Nullability::Nullable}); - } - } - for (const clang::Stmt *child : stmt->children()) - collectAccesses(child, c, wanted, out); -} - -/// The bytes from the local's value that `access` needs, when constant: -/// the object width of the local's pointee for `*b` and `b->f` (R1), up to -/// the element read for a subscript of a trailing member array -/// (`b->y[10]`), and `(k + 1)` elements for `b[k]` (R5). -static std::optional copyAccessBytes(const Access &access, - const MustAccessContext &c) { - const clang::ASTContext &context = c.state.context; - const clang::QualType pointee = access.base->getType()->getPointeeType(); - const auto width = static_cast(objectWidth(pointee, context)); - if (width <= 0) - return std::nullopt; - if (access.form == Access::Form::Index) { - const auto k = c.state.constantOf(access.index); - const auto elements = - k && *k >= 0 ? llvm::checkedAdd(*k, std::int64_t{1}) : std::nullopt; - const auto size = context.getTypeSizeInChars(pointee).getQuantity(); - return elements ? llvm::checkedMul(*elements, size) : std::nullopt; - } - if (access.form != Access::Form::Element) - return std::nullopt; - const auto *member = llvm::dyn_cast(access.expr); - const auto *field = - member != nullptr - ? llvm::dyn_cast(member->getMemberDecl()) - : nullptr; - const clang::ConstantArrayType *array = - field != nullptr ? context.getAsConstantArrayType(field->getType()) - : nullptr; - if (array == nullptr || field->isBitField()) - return width; - // `b->y[k]`: the subscript's element is inside the object only if the - // member bounds it; a trailing one may reach past the object width. - const clang::Stmt *user = c.parents.getParentIgnoreParenCasts(member); - const auto *subscript = - llvm::dyn_cast_or_null(user); - const auto k = subscript != nullptr ? c.state.constantOf(subscript->getIdx()) - : std::nullopt; - if (!k || *k < 0) - return width; - const auto offset = static_cast(context.getFieldOffset(field) / - context.getCharWidth()); - const auto element = - context.getTypeSizeInChars(array->getElementType()).getQuantity(); - const auto count = llvm::checkedAdd(*k, std::int64_t{1}); - const auto bytes = count ? llvm::checkedMul(*count, element) : std::nullopt; - const auto end = bytes ? llvm::checkedAdd(offset, *bytes) : std::nullopt; - if (!end) - return std::nullopt; - return std::max(width, *end); -} - -/// The requirement an access gives (§7.5 R1, R2, R4, R5), under the guard -/// of the canonical loop it is in, if any. -static std::optional -requirementOf(const Access &access, const CountedLoop *loop, - const MustAccessContext &c) { - MustAccessRequirement out; - out.access = access.expr; - if (loop != nullptr) - out.guard = loop->guard(); - const auto inferred = [](core::PointerKind kind) { - kind.source = core::KindSource::Inferred; - return kind; - }; - switch (access.form) { - case Access::Form::Element: - // `p->y[k]` of a trailing (flexible) member may reach past the object - // width: the requirement is the bytes it reaches (§7.4, §7.5 R1). - if (const auto bytes = copyAccessBytes(access, c); - bytes && - std::cmp_greater(*bytes, - objectWidth(access.base->getType()->getPointeeType(), - c.state.context))) { - out.kind = inferred(core::PointerKind::sized( - core::ExtentTerm::constant(*bytes), core::Nullability::Nonnull)); - out.rule = MustAccessRule::R1; - // The access it covers is the subscript. - out.access = c.parents.getParentIgnoreParenCasts(access.expr); - return out; - } - out.kind = inferred(core::PointerKind::single(core::Nullability::Nonnull)); - out.rule = MustAccessRule::R1; - return out; - case Access::Form::String: - out.kind = inferred(core::PointerKind::nulTerminated(access.nullability)); - out.rule = MustAccessRule::R4; - return out; - case Access::Form::Index: - break; - } - if (loop != nullptr) { - if (const auto k = offsetFrom(access.index, loop->index, c.state)) { - // `p[i + k]` for `c <= i < e`: `e + k` elements (`e + k + 1` for - // `i <= e`), as mathematical integers. - core::ExtentTerm count = loop->end; - auto offset = llvm::checkedAdd(count.offset, *k); - if (offset && loop->inclusive) - offset = llvm::checkedAdd(*offset, std::int64_t{1}); - if (!offset) - return std::nullopt; - count.offset = *offset; - if (count.isConstant() && count.offset <= 0) - return std::nullopt; - out.kind = inferred( - core::PointerKind::counted(count, core::Nullability::Nonnull)); - out.rule = MustAccessRule::R2; - return out; - } - } - if (const auto constant = c.state.constantOf(access.index)) { - if (*constant < 0) - return std::nullopt; - out.kind = inferred( - *constant == 0 ? core::PointerKind::single(core::Nullability::Nonnull) - : core::PointerKind::counted( - core::ExtentTerm::constant(*constant + 1), - core::Nullability::Nonnull)); - out.rule = *constant == 0 ? MustAccessRule::R1 : MustAccessRule::R5; - return out; - } - auto term = parameterTerm(access.index, c.function, c.state); - if (!term) - return std::nullopt; - const auto offset = llvm::checkedAdd(term->offset, std::int64_t{1}); - if (!offset) - return std::nullopt; - term->offset = *offset; - out.kind = - inferred(core::PointerKind::counted(*term, core::Nullability::Nonnull)); - out.rule = MustAccessRule::R5; - return out; -} - -/// The `for` loop whose body holds `stmt` when that is its innermost loop. -static const clang::ForStmt *enclosingForBody(const clang::Stmt *stmt, - const clang::ParentMap &parents) { - const clang::Stmt *child = stmt; - for (const clang::Stmt *parent = parents.getParent(child); parent != nullptr; - child = parent, parent = parents.getParent(parent)) { - if (const auto *loop = llvm::dyn_cast(parent)) - return loop->getBody() == child ? loop : nullptr; - if (llvm::isa(parent) || llvm::isa(parent)) - return nullptr; - } - return nullptr; -} - -/// Every `for` and `while` loop in `stmt`. -static void loopsIn(const clang::Stmt *stmt, - std::vector &out) { - if (stmt == nullptr) - return; - if (llvm::isa(stmt) || llvm::isa(stmt)) - out.push_back(stmt); - for (const clang::Stmt *child : stmt->children()) - loopsIn(child, out); -} - -// -- Local copies of a parameter -// --------------------------------------------------- - -/// §7.5: the locals initialised from a parameter of `params`, through -/// conversions between object pointer types (`struct big *b = v;`), when -/// neither the local nor the parameter is ever modified or has its address -/// taken: an access through the local is one through the parameter. -static llvm::DenseMap -localCopies(const clang::Stmt *stmt, - const llvm::DenseSet ¶ms, - const KindInferenceState &state) { - llvm::DenseMap out; - const auto untouched = [&](const clang::VarDecl *variable) { - const auto use = state.variableUses.find(variable); - return use == state.variableUses.end() || - (!use->second.assigned && !use->second.modified && - !use->second.addressTaken); - }; - const std::function walk = - [&](const clang::Stmt *each) { - if (each == nullptr) - return; - if (const auto *decl = llvm::dyn_cast(each)) - for (const clang::Decl *d : decl->decls()) { - const auto *local = llvm::dyn_cast(d); - if (local == nullptr || !local->isLocalVarDecl() || - local->isStaticLocal() || !local->hasInit() || - !isObjectPointer(local->getType()) || !untouched(local)) - continue; - const clang::ParmVarDecl *param = - parameterOf(stripPointerCasts(local->getInit())); - if (param != nullptr && params.contains(param) && untouched(param)) - out.try_emplace(local, param); - } - for (const clang::Stmt *child : each->children()) - walk(child); - }; - walk(stmt); - return out; -} - -// -- The requirements -// -------------------------------------------------------------- - -void KindInferenceState::inferMustAccess() { - const clang::SourceManager &sm = context.getSourceManager(); - for (const clang::FunctionDecl *definition : definitions) { - if (sm.isInSystemHeader(definition->getLocation())) - continue; - llvm::DenseSet params; - for (const clang::ParmVarDecl *param : definition->parameters()) { - const auto use = variableUses.find(param); - if (isObjectPointer(param->getType()) && - (use == variableUses.end() || !use->second.addressTaken)) - params.insert(param); - } - if (params.empty()) - continue; - // (Built for this definition and dropped after it: a unit's every CFG - // at once is more than the unit's AST.) - const llvm::scope_exit dropped([&] { - cfgs.erase(definition); - parentMaps.erase(definition); - }); - const FunctionCfg *cfg = cfgOf(*definition); - if (cfg == nullptr) - continue; - const MustAccessContext c{.state = *this, - .function = *definition, - .cfg = *cfg, - .parents = parentsOf(*definition)}; - llvm::DenseMap> - found; - // One requirement per kind and guard; the accesses that give it are - // the ones it covers. - const auto add = [&](const clang::ParmVarDecl *param, - MustAccessRequirement requirement) { - std::vector &list = found[param]; - const auto same = - llvm::find_if(list, [&](const MustAccessRequirement &each) { - return each.kind == requirement.kind && - each.guard == requirement.guard; - }); - if (same != list.end()) { - if (!llvm::is_contained(same->covers, requirement.access)) - same->covers.push_back(requirement.access); - return; - } - requirement.covers = {requirement.access}; - list.push_back(std::move(requirement)); - }; - const auto position = [&](const clang::Stmt *stmt) - -> std::optional> { - const auto where = cfg->where.find(stmt); - if (where == cfg->where.end()) - return std::nullopt; - return where->second; - }; - - // R1, R2, R5, and R4 through `LibrarySpec` strings. - std::vector accesses; - collectAccesses( - definition->getBody(), c, - [&](const clang::VarDecl *variable) { - return params.contains(variable); - }, - accesses); - for (const Access &access : accesses) { - const auto *param = llvm::cast(access.base); - const auto at = position(access.expr); - if (!at) - continue; - if (mustAccessAlways(c, *param, at->first, at->second)) { - if (auto requirement = requirementOf(access, nullptr, c)) - add(param, std::move(*requirement)); - continue; - } - const clang::ForStmt *loop = enclosingForBody(access.expr, c.parents); - const auto counted = - loop != nullptr ? countedLoop(*loop, c) : std::nullopt; - if (!counted || - !mustAccessInLoop(c, *param, *loop, at->first, at->second, nullptr)) - continue; - if (auto requirement = requirementOf(access, &*counted, c)) - add(param, std::move(*requirement)); - } - - // R1 and R5 through a local copy of a parameter, in bytes: the copy may - // view the object as another type (§7.5; probe 09's `struct big *b = - // v; b->y[10]`). Only where the direct calls enforce the requirement: - // an exported function's (a `qsort` comparator's) stays unresolved. - const auto owner = functions.find(definition->getCanonicalDecl()); - const bool enforcedAtCalls = - !definition->isExternallyVisible() && - (owner == functions.end() || !owner->second.addressTaken); - const auto copies = enforcedAtCalls - ? localCopies(definition->getBody(), params, *this) - : decltype(localCopies(nullptr, params, *this)){}; - if (!copies.empty()) { - std::vector through; - collectAccesses( - definition->getBody(), c, - [&](const clang::VarDecl *variable) { - return copies.contains(variable); - }, - through); - for (const Access &access : through) { - const clang::ParmVarDecl *param = copies.lookup(access.base); - const auto at = position(access.expr); - if (!at || !mustAccessAlways(c, *param, at->first, at->second)) - continue; - const bool tested = llvm::any_of( - blocksBefore(at->first, {}, nullptr), - [&](const clang::CFGBlock *before) { - return testsNullness(llvm::dyn_cast_or_null( - before->getTerminatorCondition()), - *access.base, context); - }); - const auto bytes = tested ? std::nullopt : copyAccessBytes(access, c); - if (!bytes || *bytes <= 0) - continue; - add(param, MustAccessRequirement{.kind = core::PointerKind::sized( - core::ExtentTerm::constant(*bytes), - core::Nullability::Nonnull, - core::KindSource::Inferred), - .guard = std::nullopt, - .rule = MustAccessRule::R1, - .access = access.expr}); - } - } - - // R3 and R4 scans. - std::vector loops; - loopsIn(definition->getBody(), loops); - for (const clang::Stmt *loop : loops) { - if (const auto range = rangeLoop(*loop, c); - range && params.contains(range->start)) { - const clang::Stmt *body = - llvm::isa(loop) - ? llvm::cast(loop)->getBody() - : llvm::cast(loop)->getBody(); - std::vector through; - collectAccesses( - body, c, - [&](const clang::VarDecl *variable) { - return variable == range->cursor; - }, - through); - for (const Access &access : through) { - const auto at = position(access.expr); - const bool element = access.form == Access::Form::Element || - (access.form == Access::Form::Index && - constantOf(access.index) == 0); - if (!element || !at || - !mustAccessInLoop(c, *range->start, *loop, at->first, at->second, - range->increment)) - continue; - add(range->start, - MustAccessRequirement{ - .kind = core::PointerKind::endedBy( - core::ExtentPath::ofParam( - range->end->getFunctionScopeIndex()), - range->inclusive ? 1 : 0, core::Nullability::Nonnull, - core::KindSource::Inferred), - .guard = - RequirementGuard{ - .lhs = core::ExtentTerm::of(core::ExtentPath::ofParam( - range->start->getFunctionScopeIndex())), - .relation = - range->inclusive - ? RequirementGuard::Relation::LessEqual - : RequirementGuard::Relation::Less, - .rhs = core::ExtentTerm::of(core::ExtentPath::ofParam( - range->end->getFunctionScopeIndex()))}, - .rule = MustAccessRule::R3, - .access = access.expr}); - break; - } - } - if (const auto scan = scanLoop(*loop, c); - scan && params.contains(scan->param)) { - const auto at = position(scan->read); - if (at && mustAccessInLoop(c, *scan->param, *loop, at->first, - at->second, scan->increment)) - add(scan->param, - MustAccessRequirement{ - .kind = core::PointerKind::nulTerminated( - core::Nullability::Nonnull, core::KindSource::Inferred), - .guard = std::nullopt, - .rule = MustAccessRule::R4, - .access = scan->read}); - } - } - - // §7.5 enforcement: at every direct call of a static function whose - // address is not taken; otherwise as the caller's contract. - const auto facts = functions.find(definition->getCanonicalDecl()); - const bool local = - !definition->isExternallyVisible() && - (facts == functions.end() || !facts->second.addressTaken); - for (auto &[param, requirements] : found) { - std::ranges::stable_sort( - requirements, [&sm](const MustAccessRequirement &a, - const MustAccessRequirement &b) { - const clang::SourceLocation left = a.access->getBeginLoc(); - const clang::SourceLocation right = b.access->getBeginLoc(); - return left != right && sm.isBeforeInTranslationUnit(left, right); - }); - const unsigned index = param->getFunctionScopeIndex(); - const KindEntry *existing = kinds.param(*definition, index); - KindEntry entry = existing != nullptr ? *existing : KindEntry{}; - entry.mustAccess = std::move(requirements); - entry.enforcement = local ? RequirementEnforcement::CallSites - : RequirementEnforcement::CallerContract; - kinds.setParam(*definition, index, std::move(entry)); - } - } -} - -} // namespace weavec::analysis diff --git a/lib/Analysis/KindTable.cpp b/lib/Analysis/KindTable.cpp deleted file mode 100644 index f2df093c..00000000 --- a/lib/Analysis/KindTable.cpp +++ /dev/null @@ -1,272 +0,0 @@ -//===- KindTable.cpp - Pointer kinds by declaration (RFC 0030) ------------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// - -#include "weavec/Analysis/KindTable.h" - -#include "weavec/Analysis/ClangLocation.h" - -#include "clang/AST/Type.h" - -#include "llvm/ADT/STLExtras.h" - -#include - -namespace weavec::analysis { - -std::string_view toString(KindLevel level) noexcept { - switch (level) { - case KindLevel::Annotation: - return "annotation"; - case KindLevel::Ecosystem: - return "ecosystem"; - case KindLevel::LibrarySpec: - return "library-spec"; - case KindLevel::SystemHeader: - return "system-header"; - } - return "annotation"; -} - -std::string_view toString(MustAccessRule rule) noexcept { - switch (rule) { - case MustAccessRule::R1: - return "R1"; - case MustAccessRule::R2: - return "R2"; - case MustAccessRule::R3: - return "R3"; - case MustAccessRule::R4: - return "R4"; - case MustAccessRule::R5: - return "R5"; - } - return "R1"; -} - -std::string_view toString(RequirementEnforcement enforcement) noexcept { - switch (enforcement) { - case RequirementEnforcement::CallSites: - return "call-sites"; - case RequirementEnforcement::CallerContract: - return "caller-contract"; - } - return "call-sites"; -} - -std::string RequirementGuard::toString() const { - return lhs.toString() + - (relation == Relation::Less ? std::string(" < ") - : std::string(" <= ")) + - rhs.toString(); -} - -std::string MustAccessRequirement::toString() const { - std::string text; - if (guard) - text = guard->toString() + " -> "; - return text + kind.toString() + " (" + std::string(analysis::toString(rule)) + - ")"; -} - -bool KindEntry::hasShape() const noexcept { - return kind.shape != core::PointerShape::Unknown; -} - -bool KindEntry::hasDeclaredShape() const noexcept { - return shapeLevel.has_value() && hasShape(); -} - -bool KindEntry::isNonnull() const noexcept { - return kind.nullability == core::Nullability::Nonnull; -} - -bool KindEntry::declaresNonnull() const noexcept { - return nullabilityLevel.has_value() && isNonnull(); -} - -bool KindEntry::hasEnforcedRequirement() const noexcept { - return enforcement == RequirementEnforcement::CallSites && - !mustAccess.empty(); -} - -bool KindEntry::shapeFromSystemHeader() const noexcept { - return shapeLevel == KindLevel::SystemHeader; -} - -bool KindEntry::nullabilityFromSystemHeader() const noexcept { - return nullabilityLevel == KindLevel::SystemHeader; -} - -const KindEntry *KindTable::param(const clang::FunctionDecl &function, - unsigned index) const { - const auto found = params.find({function.getCanonicalDecl(), index}); - return found == params.end() ? nullptr : &found->second; -} - -const KindEntry *KindTable::result(const clang::FunctionDecl &function) const { - const auto found = results.find(function.getCanonicalDecl()); - return found == results.end() ? nullptr : &found->second; -} - -const KindEntry *KindTable::field(const clang::FieldDecl &field) const { - const auto found = fields.find(field.getCanonicalDecl()); - return found == fields.end() ? nullptr : &found->second; -} - -const KindEntry *KindTable::variable(const clang::VarDecl &variable) const { - const auto found = variables.find(variable.getCanonicalDecl()); - return found == variables.end() ? nullptr : &found->second; -} - -bool KindTable::governedByLibrary(const clang::FunctionDecl &function) const { - return library.contains(function.getCanonicalDecl()); -} - -void KindTable::setParam(const clang::FunctionDecl &function, unsigned index, - KindEntry entry) { - params.insert_or_assign({function.getCanonicalDecl(), index}, - std::move(entry)); - coveringBuilt = false; -} - -void KindTable::setResult(const clang::FunctionDecl &function, - KindEntry entry) { - results.insert_or_assign(function.getCanonicalDecl(), std::move(entry)); -} - -void KindTable::setField(const clang::FieldDecl &field, KindEntry entry) { - fields.insert_or_assign(field.getCanonicalDecl(), std::move(entry)); -} - -void KindTable::setVariable(const clang::VarDecl &variable, KindEntry entry) { - variables.insert_or_assign(variable.getCanonicalDecl(), std::move(entry)); -} - -void KindTable::setGovernedByLibrary(const clang::FunctionDecl &function) { - library.insert(function.getCanonicalDecl()); -} - -void KindTable::addProblem(KindProblem problem) { - problemList.push_back(std::move(problem)); -} - -const OwnershipContract * -KindTable::ownership(const clang::FunctionDecl &function) const { - const auto found = contracts.find(function.getCanonicalDecl()); - return found == contracts.end() ? nullptr : &found->second; -} - -void KindTable::setOwnership(const clang::FunctionDecl &function, - OwnershipContract contract) { - contracts.insert_or_assign(function.getCanonicalDecl(), std::move(contract)); -} - -void KindTable::addSuggestion(KindSuggestion suggestion) { - suggestionList.push_back(std::move(suggestion)); -} - -std::vector -kindProblemDiagnostics(const KindTable &table, const clang::SourceManager &sm) { - std::vector ordered; - ordered.reserve(table.problems().size()); - for (const KindProblem &problem : table.problems()) - ordered.push_back(&problem); - std::ranges::stable_sort( - ordered, [&sm](const KindProblem *a, const KindProblem *b) { - return a->location != b->location && - sm.isBeforeInTranslationUnit(a->location, b->location); - }); - std::vector out; - for (const KindProblem *problem : ordered) { - // A redeclaration repeats its annotations; report each problem once. - const bool repeated = llvm::any_of(out, [&](const core::Diagnostic &seen) { - return seen.message == problem->message && - seen.location == toCoreLocation(sm, problem->location); - }); - if (repeated) - continue; - out.push_back(core::Diagnostic{ - .severity = core::Severity::Warning, - .certainty = core::Certainty::Definite, - .id = core::diag::InvalidAnnotation, - .message = problem->message, - .location = toCoreLocation(sm, problem->location), - .notes = {}, - .fixits = {}, - }); - } - return out; -} - -llvm::ArrayRef -KindTable::covering(const clang::Stmt &access) const { - if (!coveringBuilt) { - coveringIndex.clear(); - for (const auto &[key, entry] : params) - for (unsigned r = 0; r < entry.mustAccess.size(); ++r) { - const MustAccessRequirement &requirement = entry.mustAccess[r]; - const CoveringRequirement covered{ - .function = key.first, .param = key.second, .requirement = r}; - if (requirement.covers.empty() && requirement.access != nullptr) - coveringIndex[requirement.access].push_back(covered); - for (const clang::Stmt *stmt : requirement.covers) - coveringIndex[stmt].push_back(covered); - } - coveringBuilt = true; - } - const auto found = coveringIndex.find(&access); - if (found == coveringIndex.end()) - return {}; - return found->second; -} - -static core::LibraryParam::Type classOf(clang::QualType type) { - if (type->isFunctionPointerType()) - return core::LibraryParam::Type::Function; - if (type->isPointerType() || type->isArrayType()) - return core::LibraryParam::Type::Pointer; - if (type->isIntegralOrEnumerationType()) - return core::LibraryParam::Type::Int; - return core::LibraryParam::Type::Other; -} - -std::optional -librarySignatureOf(const clang::FunctionDecl &function) { - const auto *prototype = function.getType()->getAs(); - if (prototype == nullptr) - return std::nullopt; - core::LibSignature signature; - signature.params.reserve(prototype->getNumParams()); - for (const clang::QualType param : prototype->getParamTypes()) - signature.params.push_back(classOf(param)); - signature.variadic = prototype->isVariadic(); - signature.pointerResult = prototype->getReturnType()->isPointerType(); - return signature; -} - -std::optional -governingLibraryEntry(const clang::FunctionDecl &function, - const core::LibrarySpec &library) { - // §8: only when the program does not define the function; per unit, the - // program is the unit (the link step decides across units, §13.2). An - // inline copy of a builtin that defers to the library's (glibc's - // `_FORTIFY_SOURCE` wrappers: `memcpy` calling `__builtin___memcpy_chk`) - // defines nothing of the program's (RFC 0033 §5). - const clang::FunctionDecl *definition = nullptr; - if (function.getIdentifier() == nullptr || - (function.isDefined(definition) && - !definition->isInlineBuiltinDeclaration())) - return std::nullopt; - const llvm::StringRef name = function.getName(); - const std::string_view callee(name.data(), name.size()); - if (const auto signature = librarySignatureOf(function)) - return library.lookup(callee, *signature); - return library.lookup(callee); -} - -} // namespace weavec::analysis diff --git a/lib/Analysis/LedgerAdapter.cpp b/lib/Analysis/LedgerAdapter.cpp deleted file mode 100644 index 95295c4e..00000000 --- a/lib/Analysis/LedgerAdapter.cpp +++ /dev/null @@ -1,566 +0,0 @@ -//===- LedgerAdapter.cpp - The engine seam (RFC 0030) ---------------------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// - -#include "weavec/Analysis/LedgerAdapter.h" - -#include "weavec/Analysis/ClangLocation.h" - -#include "clang/AST/Expr.h" -#include "clang/Basic/SourceManager.h" -#include "clang/Lex/Lexer.h" - -#include "llvm/ADT/DenseMap.h" -#include "llvm/ADT/STLExtras.h" - -#include -#include - -namespace weavec::analysis { - -LedgerAdapter::LedgerAdapter(clang::ASTContext &ctx, const SiteIndex &siteIndex, - LedgerAdapterOptions adapterOptions, - Mode adapterMode) - : context(ctx), sites(siteIndex), options(std::move(adapterOptions)), - mode(adapterMode) { - if (mode == Mode::Authoritative || mode == Mode::Witness) - unit.functions = sites.ledgers(); - unit.source = options.source; -} - -/// The sites of a unit nothing is decided about. -static const SiteIndex &noSites() { - static const SiteIndex None; - return None; -} - -LedgerAdapter::LedgerAdapter(clang::ASTContext &ctx, Mode adapterMode) - : LedgerAdapter(ctx, noSites(), {}, adapterMode) { - assert(adapterMode != Mode::Authoritative && - "an authoritative adapter needs the unit's sites"); -} - -core::FacetRecord *LedgerAdapter::record(core::SiteId id, core::Facet facet) { - core::Site *site = unit.site(id); - return site != nullptr ? site->facet(facet) : nullptr; -} - -bool LedgerAdapter::applies(core::SiteId id, core::Facet facet) const { - const core::Site *site = unit.site(id); - return site != nullptr && site->hasFacet(facet); -} - -void LedgerAdapter::beginFunction(const clang::FunctionDecl &function) { - if (isDiscarding()) - return; - current = &function; - const SiteIndex::FunctionSites *analysed = sites.function(function); - if (analysed == nullptr) - return; - unit.functions[analysed->index].resetDecisions(); - overBudgetFunctions.erase(analysed->index); -} - -void LedgerAdapter::noteOrphan(const clang::Stmt &stmt, core::Facet facet) { - // §2.6 item 2: an internal error. Release builds record an - // `unresolved(unanalysed)` row in the enclosing function. - const SiteIndex::FunctionSites *function = - current != nullptr ? sites.function(*current) : nullptr; - if (function == nullptr) - return; - orphans.push_back( - Orphan{.stmt = &stmt, .function = function->index, .facet = facet}); -} - -void LedgerAdapter::decideAt(std::optional id, - const clang::Stmt &stmt, core::Facet facet, - const core::FacetDecision &decision) { - assert(id.has_value() && - "a decision about a statement SiteCollector did not enumerate"); - assert(decision.isWellFormed() && - "a decision whose reason does not fit its outcome"); - if (!id) { - noteOrphan(stmt, facet); - return; - } - // A facet that does not apply to the site is vacuous (§2.1): the - // decision says nothing the ledger records. - if (core::FacetRecord *facetRecord = record(*id, facet)) - facetRecord->decide(decision); -} - -void LedgerAdapter::decide(const clang::Stmt &site, core::Facet facet, - core::SiteOutcome outcome, - std::optional unresolved, - std::optional trusted, - std::string detail) { - if (observer) - observer(site, facet, - core::FacetDecision{.outcome = outcome, - .unresolved = unresolved, - .trusted = trusted, - .detail = detail}); - if (isDiscarding()) - return; - decideAt(sites.find(site), site, facet, - core::FacetDecision{.outcome = outcome, - .unresolved = unresolved, - .trusted = trusted, - .detail = std::move(detail)}); -} - -void LedgerAdapter::decideAs(const clang::Stmt &site, core::SiteKind kind, - std::optional boundary, - core::Facet facet, - const core::FacetDecision &decision) { - if (observer) - observer(site, facet, decision); - if (isDiscarding()) - return; - decideAt(sites.find(site, kind, boundary), site, facet, decision); -} - -void LedgerAdapter::suggest(const clang::Stmt &site, core::SiteKind kind, - std::optional boundary, - core::Facet facet, core::FixItHint fixit) { - if (isDiscarding()) - return; - const std::optional id = sites.find(site, kind, boundary); - if (!id) - return; - core::FacetRecord *facetRecord = record(*id, facet); - if (facetRecord != nullptr && !facetRecord->fixit) - facetRecord->fixit = std::move(fixit); -} - -void LedgerAdapter::requirement(const clang::Stmt &site, core::Facet facet, - const core::FacetDecision &decision) { - if (isDiscarding()) - return; - const std::optional id = sites.find(site); - assert(id.has_value() && - "a requirement of a statement SiteCollector did not enumerate"); - if (!id) { - noteOrphan(site, facet); - return; - } - if (core::FacetRecord *facetRecord = this->record(*id, facet)) - facetRecord->decide(decision); -} - -void LedgerAdapter::boundary(const clang::Stmt &site, BoundaryFacts facts) { - if (isDiscarding()) - return; - std::optional id = sites.findExit(site); - if (!id) - id = sites.find(site); - if (id) - boundaryList.push_back( - PublishedBoundary{.site = *id, .facts = std::move(facts)}); -} - -void LedgerAdapter::reliesOn(core::SiteId site, std::string placeClass) { - if (isDiscarding() || placeClass.empty()) - return; - relied[site].insert(std::move(placeClass)); -} - -void LedgerAdapter::boundaryDecisions(std::vector decisions) { - if (isDiscarding()) - return; - boundaryRows = std::move(decisions); -} - -void LedgerAdapter::applyBoundaries() { - for (const BoundaryDecision &row : boundaryRows) { - core::FacetRecord *facet = record(row.site, core::Facet::Temporal); - if (facet == nullptr) - continue; - // §9.4 *Propagation*: only a proven facet rests on the assumption the - // boundary broke. A trusted one rests on its own reason, so it is not - // downgraded. - if (row.propagated && - (!facet->decided || facet->outcome() != core::SiteOutcome::Proven)) - continue; - facet->decide(core::FacetDecision::unresolvedFor(row.reason, row.detail)); - } -} - -void LedgerAdapter::overBudget(const clang::FunctionDecl &function) { - if (isDiscarding()) - return; - if (const SiteIndex::FunctionSites *analysed = sites.function(function)) - overBudgetFunctions.insert(analysed->index); -} - -static bool contains(const clang::SourceManager &sm, clang::SourceRange range, - clang::SourceLocation loc) { - const clang::CharSourceRange expanded = sm.getExpansionRange(range); - const clang::SourceLocation begin = expanded.getBegin(); - const clang::SourceLocation end = expanded.getEnd(); - if (begin.isInvalid() || end.isInvalid() || loc.isInvalid()) - return false; - return !sm.isBeforeInTranslationUnit(loc, begin) && - !sm.isBeforeInTranslationUnit(end, loc); -} - -std::string -LedgerAdapter::functionNameAt(const core::SourceLocation &loc) const { - const clang::SourceManager &sm = context.getSourceManager(); - const clang::SourceLocation at = toClangLocation(loc); - if (current != nullptr && - (at.isInvalid() || - contains(sm, current->getSourceRange(), sm.getExpansionLoc(at)))) - return current->getNameAsString(); - if (at.isInvalid()) - return {}; - if (const SiteIndex::FunctionSites *function = sites.functionAt(at, sm)) - return function->decl->getNameAsString(); - return {}; -} - -void LedgerAdapter::publish(core::Diagnostic diagnostic, - core::Certainty certainty, - std::optional id, - std::optional facet) { - core::LedgerDiagnostic entry; - entry.id = std::string(diagnostic.id); - entry.severity = diagnostic.severity; - entry.certainty = certainty; - entry.message = diagnostic.message; - entry.location = diagnostic.location; - for (const core::Diagnostic ¬e : diagnostic.notes) - entry.notes.push_back( - core::LedgerNote{.message = note.message, .location = note.location}); - const auto index = static_cast(ledgerDiagnostics.size()); - if (id) { - entry.function = unit.functions[id->function].name; - if (facet && record(*id, *facet) != nullptr) { - entry.site = id->ordinal; - entry.facet = facet; - } - } else { - entry.function = functionNameAt(diagnostic.location); - } - ledgerDiagnostics.push_back(std::move(entry)); - link(index, diagnostic, certainty, id, facet); - emittedKeys[{std::string(diagnostic.id), diagnostic.location.file, - diagnostic.location.line, diagnostic.location.column, - diagnostic.message}] = index; - emitted.push_back(std::move(diagnostic)); -} - -void LedgerAdapter::link(std::uint32_t index, - const core::Diagnostic &diagnostic, - core::Certainty certainty, - std::optional id, - std::optional facet) { - if (!id) - return; - // §3.4 and (V): a definite error is a violation of its facet, whatever - // path reported it. The facet is made to apply when the site kind would - // not otherwise carry it (a callee's requirement at a Call site). - if (facet && certainty == core::Certainty::Definite && - diagnostic.severity == core::Severity::Error) { - if (core::Site *site = unit.site(*id)) - site->addFacet(*facet).decide( - core::FacetDecision::violation(diagnostic.message)); - } - core::FacetRecord *linked = facet ? record(*id, *facet) : nullptr; - if (linked != nullptr && !linked->diagnostic) - linked->diagnostic = index; -} - -/// The facet a definite error of `id` is a violation of (§3, §17.3's -/// matching facets), or none for ids that are about no facet. -static std::optional facetOfDiagnostic(std::string_view id) { - namespace diag = core::diag; - if (id == diag::OutOfBounds || id == diag::InvalidRelease) - return core::Facet::Spatial; - if (id == diag::NullDereference || id == diag::UseOfUninitialized) - return core::Facet::Null; - if (id == diag::UseAfterFree || id == diag::DoubleFree || - id == diag::UseAfterMove || id == diag::ConflictingBorrow || - id == diag::LifetimeTooShort || id == diag::MismatchedRelease) - return core::Facet::Temporal; - if (id == diag::ContradictedAssumption) - return core::Facet::Assertion; - return std::nullopt; -} - -void LedgerAdapter::report(core::Diagnostic diagnostic, - core::Certainty certainty, const clang::Stmt *site, - std::optional facet) { - if (mode == Mode::Discarding) - return; - diagnostic.certainty = certainty; - auto [seen, fresh] = emittedKeys.try_emplace( - {std::string(diagnostic.id), diagnostic.location.file, - diagnostic.location.line, diagnostic.location.column, - diagnostic.message}, - std::nullopt); - // Reported again (a function analysed once more, §7.6): the rows the new - // run published link to the diagnostic the first one made. - const std::optional known = - fresh ? std::nullopt : seen->second; - if (!fresh && !known) - return; - if (mode == Mode::Collecting || mode == Mode::Witness) { - emitted.push_back(std::move(diagnostic)); - return; - } - std::optional id; - if (site != nullptr) { - id = sites.find(*site); - if (!id) - id = sites.findExit(*site); - } - // (V): a definite error is a violation of a site (§3.4). One reported - // without its site, or at a subexpression that is no site of - // its own (the argument of a call whose callee requires more than it has), - // belongs to the innermost site around it, on the facet its id governs. - if (!id && certainty == core::Certainty::Definite && - diagnostic.severity == core::Severity::Error) { - if (!facet) - facet = facetOfDiagnostic(diagnostic.id); - const clang::SourceLocation at = - site != nullptr ? site->getBeginLoc() - : clang::SourceLocation::getFromRawEncoding( - static_cast( - diagnostic.location.opaque)); - if (facet && at.isValid()) - id = sites.innermostAt(at, std::nullopt, context.getSourceManager()); - } - if (known) { - link(*known, diagnostic, certainty, id, facet); - return; - } - publish(std::move(diagnostic), certainty, id, facet); -} - -void LedgerAdapter::unconfirm(const clang::Stmt &site, core::Facet facet, - std::string_view id) { - if (isDiscarding()) - return; - std::optional found = sites.find(site); - if (!found) - found = sites.findExit(site); - const auto unresolved = - core::FacetDecision::unresolvedFor(core::UnresolvedReason::Unconfirmed); - if (found) - if (core::FacetRecord *facetRecord = record(*found, facet); - facetRecord != nullptr && - facetRecord->outcome() == core::SiteOutcome::Violation) - // (Assigned: a violation outranks every other decision.) - facetRecord->decision = unresolved; - const clang::SourceManager &sm = context.getSourceManager(); - const auto inSite = [&](const core::SourceLocation &location) { - const clang::SourceLocation at = toClangLocation(location); - return at.isValid() && - contains(sm, site.getSourceRange(), sm.getExpansionLoc(at)); - }; - for (core::Diagnostic &diagnostic : emitted) { - if (diagnostic.id != id || diagnostic.severity != core::Severity::Error || - !inSite(diagnostic.location)) - continue; - diagnostic.severity = core::Severity::Warning; - diagnostic.certainty = core::Certainty::Possible; - diagnostic.addNote("not confirmed on a feasible path", diagnostic.location); - for (core::LedgerDiagnostic &entry : ledgerDiagnostics) - if (entry.id == id && entry.location == diagnostic.location && - entry.message == diagnostic.message) { - entry.severity = core::Severity::Warning; - entry.certainty = core::Certainty::Possible; - entry.notes.push_back( - core::LedgerNote{.message = "not confirmed on a feasible path", - .location = diagnostic.location}); - } - } -} - -//===----------------------------------------------------------------------===// -// finish -//===----------------------------------------------------------------------===// - -/// §2.6: the outcome of a facet no record decided. -static core::FacetDecision defaultFor(const SiteInfo &site, core::Facet facet, - bool overBudget) { - // §5.2: a facet that rests only on system-header attributes. - if ((facet == core::Facet::Null && site.nullSystemApi) || - (facet == core::Facet::Spatial && site.spatialSystemApi)) - return core::FacetDecision::trustedFor(core::TrustReason::SystemApi); - // §7.4: an IntToPtr site's own facets (inside an unsafe region they are - // trusted, §6.1). - if (site.kind == core::SiteKind::IntToPtr && - (facet == core::Facet::Spatial || facet == core::Facet::Temporal)) - return core::FacetDecision::unresolvedFor(core::UnresolvedReason::RawCast, - "made from a non-pointer value"); - core::FacetDecision decision = core::defaultDecision(overBudget); - if (decision.unresolved == core::UnresolvedReason::Unanalysed) - decision.detail = "no decision was published"; - return decision; -} - -void LedgerAdapter::fillDefaults() { - for (const SiteIndex::FunctionSites &function : sites.functions()) { - core::FunctionLedger &row = unit.functions[function.index]; - for (const SiteInfo &info : function.sites) { - core::Site *site = row.site(info.id.ordinal); - if (site == nullptr) - continue; - for (const core::Facet facet : core::AllFacets) { - core::FacetRecord *facetRecord = site->facet(facet); - if (facetRecord == nullptr) - continue; - if (!facetRecord->decided) - facetRecord->decide(defaultFor(info, facet, row.overBudget)); - } - } - } -} - -/// Replaces a decision unless it is a violation: definite violations stand -/// in every region (§6.1). -static void overrideRecord(core::FacetRecord &facetRecord, - const core::FacetDecision &decision) { - if (facetRecord.outcome() != core::SiteOutcome::Violation) - facetRecord.decision = decision; -} - -void LedgerAdapter::applyOverrides() { - for (const SiteIndex::FunctionSites &function : sites.functions()) { - core::FunctionLedger &row = unit.functions[function.index]; - for (const SiteInfo &info : function.sites) { - core::Site *site = row.site(info.id.ordinal); - if (site == nullptr) - continue; - // §6.1: spatial and null facets inside an unsafe region are trusted, - // and every facet of a Raw or IntToPtr site. An Assume site keeps its - // assertion. - if (info.inUnsafe && info.kind != core::SiteKind::Assume) { - const bool everyFacet = info.kind == core::SiteKind::Raw || - info.kind == core::SiteKind::IntToPtr; - for (const core::Facet facet : core::AllFacets) { - core::FacetRecord *facetRecord = site->facet(facet); - if (facetRecord == nullptr || - (facet == core::Facet::Temporal && !everyFacet) || - facet == core::Facet::Assertion) - continue; - overrideRecord(*facetRecord, core::FacetDecision::trustedFor( - core::TrustReason::Unsafe)); - } - } - // §5.3: a pointer shared with a thread or signal handler. Its - // temporal facet, and a null or spatial facet flow facts proved, are - // trusted(concurrency); what the types prove stays proven. - if (options.concurrent && options.concurrent(info)) { - if (core::FacetRecord *temporal = site->facet(core::Facet::Temporal); - temporal != nullptr && - temporal->outcome() == core::SiteOutcome::Proven) - overrideRecord(*temporal, core::FacetDecision::trustedFor( - core::TrustReason::Concurrency)); - for (const core::Facet facet : - {core::Facet::Spatial, core::Facet::Null}) { - core::FacetRecord *facetRecord = site->facet(facet); - if (facetRecord == nullptr || - facetRecord->outcome() != core::SiteOutcome::Proven || - info.provenByType) - continue; - facetRecord->decision = - core::FacetDecision::trustedFor(core::TrustReason::Concurrency); - } - } - if (!function.callsSetjmp) - continue; - // §5.4: values may be stale after a `longjmp`. - if (core::FacetRecord *temporal = site->facet(core::Facet::Temporal)) - overrideRecord(*temporal, core::FacetDecision::unresolvedFor( - core::UnresolvedReason::Setjmp)); - for (const core::Facet facet : - {core::Facet::Spatial, core::Facet::Null}) { - core::FacetRecord *facetRecord = site->facet(facet); - if (facetRecord == nullptr || - facetRecord->outcome() != core::SiteOutcome::Proven || - info.provenByType) - continue; - // A proof from flow facts no longer stands (§5.4). - facetRecord->decision = - core::FacetDecision::unresolvedFor(core::UnresolvedReason::Setjmp); - } - } - } -} - -/// The site kind of a statement no site was enumerated for. -static core::SiteKind orphanKind(const clang::Stmt &stmt) { - if (llvm::isa(stmt)) - return core::SiteKind::Index; - if (llvm::isa(stmt)) - return core::SiteKind::Call; - if (const auto *cast = llvm::dyn_cast(&stmt)) - return cast->getCastKind() == clang::CK_IntegralToPointer - ? core::SiteKind::IntToPtr - : core::SiteKind::Cast; - return core::SiteKind::Deref; -} - -void LedgerAdapter::appendOrphanRows() { - const clang::SourceManager &sm = context.getSourceManager(); - // One row per statement, with every facet decided about it. - llvm::DenseMap rowOf; - for (const Orphan &orphan : orphans) { - core::FunctionLedger &row = unit.functions[orphan.function]; - const core::FacetDecision unanalysed = core::FacetDecision::unresolvedFor( - core::UnresolvedReason::Unanalysed, - "an operation the site collector did not enumerate"); - if (const auto found = rowOf.find(orphan.stmt); found != rowOf.end()) { - core::FacetRecord &facetRecord = - row.sites[found->second].addFacet(orphan.facet); - if (!facetRecord.decided) - facetRecord.decide(unanalysed); - continue; - } - rowOf[orphan.stmt] = static_cast(row.sites.size()); - core::Site site; - site.ordinal = static_cast(row.sites.size()); - site.kind = orphanKind(*orphan.stmt); - site.location = toCoreLocation(sm, orphan.stmt->getBeginLoc()); - const llvm::StringRef text = clang::Lexer::getSourceText( - sm.getExpansionRange(orphan.stmt->getSourceRange()), sm, - context.getLangOpts()); - site.text = core::siteText(std::string_view(text.data(), text.size())); - if (site.kind == core::SiteKind::Call) - site.boundary = llvm::isa(orphan.stmt) - ? core::Boundary::Call - : core::Boundary::Exit; - site.addFacet(orphan.facet).decide(unanalysed); - row.sites.push_back(std::move(site)); - } -} - -core::Ledger LedgerAdapter::finish() { - core::Ledger result; - if (isDiscarding() || finished) - return result; - finished = true; - for (const SiteIndex::FunctionSites &function : sites.functions()) - unit.functions[function.index].overBudget = - overBudgetFunctions.contains(function.index); - fillDefaults(); - applyOverrides(); - applyBoundaries(); - appendOrphanRows(); - - result.units.push_back(std::move(unit)); - result.diagnostics = ledgerDiagnostics; - core::sortDiagnostics(result); - assert(core::completenessProblems(result).empty() && - "RFC 0030 §2.6: the ledger must be complete by construction"); - return result; -} - -} // namespace weavec::analysis diff --git a/lib/Analysis/ProgramDatabase.cpp b/lib/Analysis/ProgramDatabase.cpp deleted file mode 100644 index 7fe7d749..00000000 --- a/lib/Analysis/ProgramDatabase.cpp +++ /dev/null @@ -1,191 +0,0 @@ -//===- ProgramDatabase.cpp - Summaries across translation units -----------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// - -#include "weavec/Analysis/ProgramDatabase.h" - -#include "weavec/Core/Effects.h" - -#include "clang/AST/Decl.h" -#include "clang/AST/DeclBase.h" -#include "clang/AST/PrettyPrinter.h" -#include "clang/AST/RecordLayout.h" -#include "clang/Basic/Version.h" - -#include "llvm/ADT/StringExtras.h" - -#include -#include -#include -#include - -using namespace clang; - -namespace weavec::analysis { - -// -- GlobalNames -------------------------------------------------------------- - -std::uint32_t GlobalNames::idFor(llvm::StringRef name) { - const auto [it, inserted] = - ids.try_emplace(name.str(), static_cast(names.size())); - if (inserted) - names.push_back(name.str()); - return it->second; -} - -llvm::StringRef GlobalNames::nameOf(std::uint32_t id) const { - return id < names.size() ? llvm::StringRef(names[id]) : ""; -} - -// -- UnitExports -------------------------------------------------------------- - -bool UnitExports::sameFunctionsAs(const UnitExports &other) const { - return functions == other.functions && globals == other.globals; -} - -bool UnitExports::sameSummariesAs(const UnitExports &other) const { - return sameFunctionsAs(other) && contextEffects == other.contextEffects && - contextRequests == other.contextRequests; -} - -// -- Type keys ---------------------------------------------------------------- - -/// The canonical spelling of `type`, or empty when an anonymous record is -/// involved (spelled by its location, which no other unit shares: no stable -/// key; RFC 0005, *Accepted false positives*). -static std::string stableTypeKey(QualType type, const ASTContext &context) { - PrintingPolicy policy(context.getLangOpts()); - policy.SuppressTagKeyword = false; -#if CLANG_VERSION_MAJOR >= 23 - policy.AnonymousTagNameStyle = - llvm::to_underlying(PrintingPolicy::AnonymousTagMode::SourceLocation); -#else - policy.AnonymousTagLocations = true; -#endif - policy.SuppressScope = true; - policy.Bool = false; - std::string key = type.getCanonicalType().getAsString(policy); - for (const char *marker : - {"(unnamed ", "(unnamed)", "(anonymous ", "getAs()) - type = pointer->getPointeeType(); - if (!type->isFunctionType()) - return {}; - return stableTypeKey(type, context); -} - -std::string recordTypeKey(QualType type, const ASTContext &context) { - if (type.isNull()) - return {}; - const QualType canonical = type.getCanonicalType().getUnqualifiedType(); - if (!canonical->isRecordType()) - return {}; - return stableTypeKey(canonical, context); -} - -// -- ProgramDatabase ---------------------------------------------------------- - -void ProgramDatabase::add(const UnitExports &unit) { - const auto toDatabase = - [&](std::uint32_t id) -> std::optional { - if (id >= unit.globals.size()) - return std::nullopt; - return globalNames.idFor(unit.globals.nameOf(id)); - }; - const auto fold = - [](std::map> &into, - llvm::StringRef key, core::FunctionEffects effects) { - auto [it, inserted] = into.try_emplace(key.str(), effects); - if (!inserted) - it->second = core::joinEffects(it->second, effects); - }; - for (const auto &[name, function] : unit.functions) { - core::FunctionEffects effects = - core::renumberGlobals(function.effects, toDatabase); - if (function.external) - fold(byName, name, effects); - // (An internal function another unit may be handed as a callback, by - // its portable name.) - else if (function.addressTaken && !unit.source.empty()) - fold(byName, unit.source + "#" + name, effects); - if (function.addressTaken && !function.typeKey.empty()) - fold(byType, function.typeKey, std::move(effects)); - } - for (const auto &[request, effects] : unit.contextEffects) { - core::FunctionEffects renumbered = - core::renumberGlobals(effects, toDatabase); - auto [it, inserted] = byContext.try_emplace(request, renumbered); - if (!inserted) - it->second = core::joinEffects(it->second, renumbered); - } - requested.insert(unit.contextRequests.begin(), unit.contextRequests.end()); -} - -const core::FunctionEffects * -ProgramDatabase::contextEffects(const ContextRequest &request) const { - auto it = byContext.find(request); - return it != byContext.end() ? &it->second : nullptr; -} - -void ProgramDatabase::clear() { - globalNames = GlobalNames{}; - byName.clear(); - byType.clear(); - byContext.clear(); - requested.clear(); -} - -const core::FunctionEffects * -ProgramDatabase::findEffects(llvm::StringRef name) const { - auto it = byName.find(name); - return it != byName.end() ? &it->second : nullptr; -} - -const core::FunctionEffects * -ProgramDatabase::candidateEffects(llvm::StringRef typeKey) const { - auto it = byType.find(typeKey); - return it != byType.end() ? &it->second : nullptr; -} - -void ProgramDatabase::dump(llvm::raw_ostream &os) const { - os << "program:\n"; - auto summary = [&](const core::FunctionEffects &effects) { - std::string text = core::toText(effects); - for (llvm::StringRef line : - llvm::split(llvm::StringRef(text).rtrim('\n'), '\n')) - os << " " << line << '\n'; - }; - for (const auto &[name, effects] : byName) { - os << " function '" << name << "':\n"; - summary(effects); - } - for (const auto &[key, effects] : byType) { - os << " candidate '" << key << "':\n"; - summary(effects); - } - for (std::size_t id = 0; id < globalNames.size(); ++id) - os << " global" << id << " '" - << globalNames.nameOf(static_cast(id)) << "'\n"; - for (const ContextRequest &request : requested) { - os << " context '" << request.callee << "' '" << request.key << "':\n"; - if (const core::FunctionEffects *effects = contextEffects(request)) - summary(*effects); - else - os << " unserved\n"; - } -} - -} // namespace weavec::analysis diff --git a/lib/Analysis/SafetyEngine.cpp b/lib/Analysis/SafetyEngine.cpp deleted file mode 100644 index 1b849d6a..00000000 --- a/lib/Analysis/SafetyEngine.cpp +++ /dev/null @@ -1,15 +0,0 @@ -//===- SafetyEngine.cpp - The engine interface (RFC 0030) -----------------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// - -#include "weavec/Analysis/SafetyEngine.h" - -namespace weavec::analysis { - -SafetyEngine::~SafetyEngine() = default; - -} // namespace weavec::analysis diff --git a/lib/Analysis/SiteCollector.cpp b/lib/Analysis/SiteCollector.cpp deleted file mode 100644 index ff2b3b41..00000000 --- a/lib/Analysis/SiteCollector.cpp +++ /dev/null @@ -1,1737 +0,0 @@ -//===- SiteCollector.cpp - Sites of emitted functions (RFC 0030) ----------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// - -#include "weavec/Analysis/SiteCollector.h" - -#include "weavec/Analysis/Annotations.h" -#include "weavec/Analysis/ClangLocation.h" -#include "weavec/Analysis/KindInference.h" - -#include "clang/AST/Attr.h" -#include "clang/AST/Expr.h" -#include "clang/AST/RecursiveASTVisitor.h" -#include "clang/Basic/Builtins.h" -#include "clang/Basic/SourceManager.h" -#include "clang/Lex/Lexer.h" - -#include "llvm/ADT/DenseSet.h" -#include "llvm/ADT/STLExtras.h" -#include "llvm/Support/CheckedArithmetic.h" - -#include -#include -#include -#include - -namespace weavec::analysis { - -//===----------------------------------------------------------------------===// -// SiteIndex -//===----------------------------------------------------------------------===// - -const SiteIndex::FunctionSites * -SiteIndex::function(const clang::FunctionDecl &decl) const { - const auto found = byFunction.find(decl.getCanonicalDecl()); - return found == byFunction.end() ? nullptr : &functionList[found->second]; -} - -const SiteInfo *SiteIndex::info(core::SiteId id) const { - if (id.function >= functionList.size()) - return nullptr; - const std::vector &sites = functionList[id.function].sites; - return id.ordinal < sites.size() ? &sites[id.ordinal] : nullptr; -} - -llvm::ArrayRef SiteIndex::sitesOf(const clang::Stmt &stmt) const { - const auto found = byStmt.find(&stmt); - if (found == byStmt.end()) - return {}; - return found->second; -} - -static bool isNonReturningCallExit(const SiteInfo &site) { - return site.boundary == core::Boundary::Exit && - llvm::isa(site.stmt); -} - -std::optional SiteIndex::find(const clang::Stmt &stmt) const { - for (const core::SiteId id : sitesOf(stmt)) { - const SiteInfo *site = info(id); - if (site != nullptr && !isNonReturningCallExit(*site)) - return id; - } - return std::nullopt; -} - -std::optional -SiteIndex::find(const clang::Stmt &stmt, core::SiteKind kind, - std::optional boundary) const { - for (const core::SiteId id : sitesOf(stmt)) { - const SiteInfo *site = info(id); - if (site != nullptr && site->kind == kind && - (!boundary || site->boundary == boundary)) - return id; - } - return std::nullopt; -} - -std::optional SiteIndex::findExit(const clang::Stmt &stmt) const { - return find(stmt, core::SiteKind::Call, core::Boundary::Exit); -} - -static bool contains(clang::SourceLocation begin, clang::SourceLocation end, - clang::SourceLocation loc, - const clang::SourceManager &sm) { - if (begin.isInvalid() || end.isInvalid() || loc.isInvalid()) - return false; - return !sm.isBeforeInTranslationUnit(loc, begin) && - !sm.isBeforeInTranslationUnit(end, loc); -} - -const SiteIndex::FunctionSites * -SiteIndex::functionAt(clang::SourceLocation loc, - const clang::SourceManager &sm) const { - const clang::SourceLocation at = sm.getExpansionLoc(loc); - for (const FunctionSites &function : functionList) - if (contains(function.begin, function.end, at, sm)) - return &function; - return nullptr; -} - -std::optional -SiteIndex::innermostAt(clang::SourceLocation loc, - std::optional facet, - const clang::SourceManager &sm) const { - const FunctionSites *function = functionAt(loc, sm); - if (function == nullptr) - return std::nullopt; - const clang::SourceLocation at = sm.getExpansionLoc(loc); - const core::FunctionLedger &row = rows[function->index]; - const SiteInfo *best = nullptr; - for (const SiteInfo &site : function->sites) { - if (!contains(site.begin, site.end, at, sm)) - continue; - if (facet && !row.sites[site.id.ordinal].hasFacet(*facet)) - continue; - // Innermost: a range inside the best one so far. Equal ranges keep the - // later ordinal, which is the inner site (ties sort inner first, but a - // call's exit comes after the call). - if (best == nullptr || (contains(best->begin, best->end, site.begin, sm) && - contains(best->begin, best->end, site.end, sm) && - !isNonReturningCallExit(site))) - best = &site; - } - if (best == nullptr) - return std::nullopt; - return best->id; -} - -//===----------------------------------------------------------------------===// -// Syntactic helpers -//===----------------------------------------------------------------------===// - -bool isPlatformDeclaration(const clang::FunctionDecl &function, - const core::LibrarySpec &library, - const clang::SourceManager &sm) { - const clang::SourceLocation location = - sm.getExpansionLoc(function.getCanonicalDecl()->getLocation()); - if (!sm.isInSystemHeader(location)) - return false; - const llvm::StringRef path = sm.getFilename(location); - if (path.empty()) - return false; - if (path.contains("/lib/clang/")) - return true; - const bool darwinSdk = path.contains(".sdk/"); - // The include directory that found the header is not known here: every - // suffix of the path is a candidate name. - llvm::StringRef rest = path; - while (!rest.empty()) { - if (library.isPlatformHeader(std::string_view(rest.data(), rest.size()), - darwinSdk)) - return true; - const std::size_t slash = rest.find('/'); - if (slash == llvm::StringRef::npos) - break; - rest = rest.drop_front(slash + 1); - } - return false; -} - -bool isReturnsTwice(const clang::FunctionDecl &function, - const core::LibrarySpec &library) { - if (function.hasAttr()) - return true; - if (function.getBuiltinID() == clang::Builtin::BI__builtin_setjmp) - return true; - if (function.getIdentifier() == nullptr) - return false; - const llvm::StringRef name = function.getName(); - const auto match = library.lookup(std::string_view(name.data(), name.size())); - return match && match->entry->returnsTwice; -} - -std::optional argumentTerm(const core::ExtentTerm &term, - const clang::CallExpr &call) { - if (term.isConstant()) - return SourceTerm::ofConstant(term.offset); - if (term.path->root != core::ExtentPath::Root::Param || - term.path->param >= call.getNumArgs()) - return std::nullopt; - const clang::Expr &arg = *call.getArg(term.path->param); - if (!arg.getType()->isIntegerType()) - return std::nullopt; - SourceTerm out = SourceTerm::ofExpr(arg); - if (term.scale != 1) - out = SourceTerm::mul(std::move(out), SourceTerm::ofConstant(term.scale)); - if (term.offset == std::numeric_limits::min()) - return std::nullopt; - if (term.offset > 0) - out = SourceTerm::add(std::move(out), SourceTerm::ofConstant(term.offset)); - else if (term.offset < 0) - out = SourceTerm::sub(std::move(out), SourceTerm::ofConstant(-term.offset)); - return out; -} - -std::optional guardTerm(const RequirementGuard &guard, - const clang::CallExpr &call) { - // R2's `c < e` over a loop bound: `e - c`, and `c <= e` is `e - c + 1`, - // one term over the argument (R3's pointer guards have no term). - if (!guard.lhs.isConstant() || guard.rhs.isConstant() || guard.rhs.scale <= 0) - return std::nullopt; - auto offset = llvm::checkedSub(guard.rhs.offset, guard.lhs.offset); - if (offset && guard.relation == RequirementGuard::Relation::LessEqual) - offset = llvm::checkedAdd(*offset, std::int64_t{1}); - if (!offset) - return std::nullopt; - // The helpers stop a negative signed argument at zero before they add - // (§10.2): `last + 1` would pass for `last == -1`. Only a term that adds - // nothing to a signed argument is exact. - const clang::Expr *arg = - guard.rhs.path->root == core::ExtentPath::Root::Param && - guard.rhs.path->param < call.getNumArgs() - ? call.getArg(guard.rhs.path->param) - : nullptr; - if (arg == nullptr || (*offset > 0 && arg->getType()->isSignedIntegerType())) - return std::nullopt; - core::ExtentTerm difference = guard.rhs; - difference.offset = *offset; - return argumentTerm(difference, call); -} - -bool callDoesNotReturn(const clang::CallExpr &call, - const core::LibrarySpec &library) { - if (const clang::FunctionDecl *callee = call.getDirectCallee()) { - if (callee->isNoReturn()) - return true; - if (const auto match = governingLibraryEntry(*callee, library)) - return match->entry->noreturn || match->entry->exits; - return false; - } - clang::QualType type = call.getCallee()->getType(); - if (const auto *pointer = type->getAs()) - type = pointer->getPointeeType(); - if (const auto *function = type->getAs()) - return function->getNoReturnAttr(); - return false; -} - -/// `p + i`, `p - i` (with a pointer result). -static bool isPointerOffset(const clang::Expr *expr) { - const auto *binary = - llvm::dyn_cast(expr->IgnoreParens()); - return binary != nullptr && - (binary->getOpcode() == clang::BO_Add || - binary->getOpcode() == clang::BO_Sub) && - binary->getType()->isPointerType(); -} - -/// §2.1 PtrArith: `p + i`, `p - i`, `++p`, `p++`, `--p`, `p--`, `p += i`, -/// `p -= i`, and `&p[i]` (`&*(p + i)`). -static bool isPointerArithmetic(const clang::Expr *expr) { - expr = expr->IgnoreParens(); - if (isPointerOffset(expr)) - return true; - if (const auto *unary = llvm::dyn_cast(expr)) { - if (unary->isIncrementDecrementOp()) - return unary->getType()->isPointerType(); - if (unary->getOpcode() != clang::UO_AddrOf) - return false; - const clang::Expr *sub = unary->getSubExpr()->IgnoreParens(); - if (llvm::isa(sub)) - return true; - const auto *deref = llvm::dyn_cast(sub); - return deref != nullptr && deref->getOpcode() == clang::UO_Deref && - isPointerOffset(deref->getSubExpr()); - } - if (const auto *compound = - llvm::dyn_cast(expr)) - return (compound->getOpcode() == clang::BO_AddAssign || - compound->getOpcode() == clang::BO_SubAssign) && - compound->getType()->isPointerType(); - return false; -} - -/// The array lvalue a pointer operand decays from, through the implicit -/// conversions of an argument (`char *` to `void *`), or null. -static const clang::Expr *decayedArray(const clang::Expr *pointer) { - const clang::Expr *expr = pointer->IgnoreParens(); - while (const auto *cast = llvm::dyn_cast(expr)) { - if (cast->getCastKind() == clang::CK_ArrayToPointerDecay) - return cast->getSubExpr(); - if (cast->getCastKind() != clang::CK_NoOp && - cast->getCastKind() != clang::CK_BitCast) - return nullptr; - expr = cast->getSubExpr()->IgnoreParens(); - } - return nullptr; -} - -/// §2.1: whether the object an array lvalue designates can end its lifetime -/// before the site. Automatic storage in scope and static storage cannot. -static bool arrayStorageCanEnd(const clang::Expr *lvalue) { - const clang::Expr *expr = lvalue; - for (unsigned depth = 0; expr != nullptr && depth < 64; ++depth) { - expr = expr->IgnoreParens(); - if (llvm::isa(expr)) - return false; - if (const auto *member = llvm::dyn_cast(expr)) { - if (member->isArrow()) - return true; - expr = member->getBase(); - continue; - } - if (const auto *subscript = - llvm::dyn_cast(expr)) { - const clang::Expr *array = decayedArray(subscript->getBase()); - if (array == nullptr) - return true; - expr = array; - continue; - } - return true; - } - return true; -} - -/// Whether the statement can complete normally, so that control can reach -/// what follows it (for the end of a body, §2.1). Conservative: true unless -/// the syntax shows otherwise. -static bool canComplete(const clang::Stmt *stmt, - const clang::ASTContext &context, - const core::LibrarySpec &library, unsigned depth = 0); - -/// A `break` that leaves the loop or switch `stmt` belongs to. -static bool hasOwnBreak(const clang::Stmt *stmt) { - if (stmt == nullptr) - return false; - if (llvm::isa(stmt)) - return true; - // A nested loop or switch owns the breaks inside it. - if (llvm::isa(stmt)) - return false; - return llvm::any_of(stmt->children(), [](const clang::Stmt *child) { - return hasOwnBreak(child); - }); -} - -static bool isTrueConstant(const clang::Expr *condition, - const clang::ASTContext &context) { - if (condition == nullptr) - return true; - const auto value = condition->getIntegerConstantExpr(context); - return value && !value->isZero(); -} - -static bool canComplete(const clang::Stmt *stmt, - const clang::ASTContext &context, - const core::LibrarySpec &library, unsigned depth) { - if (stmt == nullptr || depth > 256) - return true; - if (llvm::isa(stmt)) - return false; - if (const auto *compound = llvm::dyn_cast(stmt)) - return compound->body_empty() || - canComplete(compound->body_back(), context, library, depth + 1); - if (const auto *label = llvm::dyn_cast(stmt)) - return canComplete(label->getSubStmt(), context, library, depth + 1); - if (const auto *attributed = llvm::dyn_cast(stmt)) - return canComplete(attributed->getSubStmt(), context, library, depth + 1); - if (const auto *branch = llvm::dyn_cast(stmt)) - return branch->getElse() == nullptr || - canComplete(branch->getThen(), context, library, depth + 1) || - canComplete(branch->getElse(), context, library, depth + 1); - // An endless loop completes only through a `break` of its own. - if (const auto *loop = llvm::dyn_cast(stmt)) - return !isTrueConstant(loop->getCond(), context) || - hasOwnBreak(loop->getBody()); - if (const auto *loop = llvm::dyn_cast(stmt)) - return !isTrueConstant(loop->getCond(), context) || - hasOwnBreak(loop->getBody()); - if (const auto *loop = llvm::dyn_cast(stmt)) - return !isTrueConstant(loop->getCond(), context) || - hasOwnBreak(loop->getBody()); - if (const auto *expr = llvm::dyn_cast(stmt)) { - const auto *call = - llvm::dyn_cast(expr->IgnoreParenImpCasts()); - return call == nullptr || !callDoesNotReturn(*call, library); - } - return true; -} - -/// §7.5: the null part of a static callee's inferred requirements on -/// argument `index` of `call`: unguarded when one requirement is, else under -/// the first requirement's guard (the body relies on no other, §7.5 and -/// KindSeeding.cpp); none when no requirement is non-null. -static std::optional inferredNullNeed(const KindEntry ¶m, - const clang::CallExpr &call, - unsigned index) { - const MustAccessRequirement *first = nullptr; - for (const MustAccessRequirement &requirement : param.mustAccess) { - if (requirement.kind.nullability != core::Nullability::Nonnull) - continue; - if (!requirement.guard) - return ArgumentNeed{.argument = static_cast(index), - .nonnull = true, - .inferred = true}; - if (first == nullptr) - first = &requirement; - } - if (first == nullptr) - return std::nullopt; - ArgumentNeed need{.argument = static_cast(index), - .nonnull = true, - .inferred = true}; - // A guard with no C spelling here allows a null argument. - need.allowedIfZero = !guardTerm(*first->guard, call).has_value(); - return need; -} - -namespace { - -/// Where a subexpression is evaluated. -struct Context { - /// Inside a `WEAVEC_UNSAFE` function or block. - bool unsafe = false; -}; - -/// How the parent uses an lvalue: accessed, only its address taken (`&`), -/// or decayed to a pointer (an array). -enum class Use : std::uint8_t { Value, AddressOf, Decay }; - -/// The facets a site gets. -struct FacetSet { - bool spatial = false; - bool null = false; - bool temporal = false; - bool assertion = false; -}; - -/// A site before ordinals are assigned. -struct PendingSite { - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - SiteInfo info = {}; - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - FacetSet facets = {}; - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::string callee = {}; - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - std::string text = {}; - /// Where the ledger reports the site (its begin, or the closing brace). - // NOLINTNEXTLINE(readability-redundant-member-init): designated-init default - clang::SourceLocation location = {}; - std::size_t sequence = 0; -}; - -/// The variables a body assigns, increments or takes the address of. -class ModifiedVariables : public clang::RecursiveASTVisitor { -public: - llvm::DenseSet modified; - - // RecursiveASTVisitor's CRTP hooks are found by name. - // NOLINTNEXTLINE(readability-identifier-naming,bugprone-derived-method-shadowing-base-method) - bool VisitUnaryOperator(clang::UnaryOperator *op) { - if (op->isIncrementDecrementOp() || op->getOpcode() == clang::UO_AddrOf) - note(op->getSubExpr()); - return true; - } - // NOLINTNEXTLINE(readability-identifier-naming,bugprone-derived-method-shadowing-base-method) - bool VisitBinaryOperator(clang::BinaryOperator *op) { - if (op->isAssignmentOp()) - note(op->getLHS()); - return true; - } - -private: - void note(const clang::Expr *expr) { - if (const auto *ref = - llvm::dyn_cast(expr->IgnoreParenImpCasts())) - if (const auto *variable = llvm::dyn_cast(ref->getDecl())) - modified.insert(variable->getCanonicalDecl()); - } -}; - -/// Walks one emitted function's body and records its sites. -class Walker { -public: - Walker(clang::ASTContext &ctx, const KindTable &kindTable, - const core::LibrarySpec &spec, const clang::FunctionDecl &fn, - const KindInferenceResult *inference, const SlotCollection *unitSlots, - const core::SlotSolution *slotSolution) - : context(ctx), sm(ctx.getSourceManager()), kinds(kindTable), - library(spec), function(fn), signature(collectAnnotations(fn)), - inferred(inference), slots(unitSlots), solution(slotSolution) {} - - /// Walks the body; the sites are then in `sites` in walk order. - void walkBody(); - - std::vector sites; - bool callsSetjmp = false; - -private: - clang::ASTContext &context; - const clang::SourceManager &sm; - const KindTable &kinds; - const core::LibrarySpec &library; - const clang::FunctionDecl &function; - SignatureAnnotations signature; - const KindInferenceResult *inferred; - const SlotCollection *slots; - const core::SlotSolution *solution; - /// RFC 0030 §9.3: the callee of a call, following a slot that is closed - /// on one target which a `LibrarySpec` row governs (`fp = memcpy; - /// fp(dst, src, n)` applies memcpy's row, as a direct call would). Every - /// other indirect call has no callee here. - [[nodiscard]] const clang::FunctionDecl * - calleeOf(const clang::CallExpr &call) const; - llvm::DenseSet modified; - llvm::DenseSet visited; - std::size_t sequence = 0; - - void walkStmt(const clang::Stmt *stmt, Context ctx); - void walkExpr(const clang::Expr *expr, Context ctx, Use use = Use::Value, - const KindEntry *required = nullptr); - void walkDeref(const clang::UnaryOperator &deref, Context ctx, Use use); - void walkUnary(const clang::UnaryOperator &unary, Context ctx, - const KindEntry *required); - void walkBinary(const clang::BinaryOperator &binary, Context ctx, Use use, - const KindEntry *required); - void walkSubscript(const clang::ArraySubscriptExpr &subscript, Context ctx, - Use use); - void walkMember(const clang::MemberExpr &member, Context ctx, Use use); - void walkCast(const clang::CastExpr &cast, Context ctx, Use use, - const KindEntry *required); - void walkCall(const clang::CallExpr &call, Context ctx); - void walkInitList(const clang::InitListExpr &list, Context ctx); - void walkDecl(const clang::Decl &decl, Context ctx); - void walkVariablyModified(clang::QualType type, Context ctx); - - PendingSite &addSite(core::SiteKind kind, const clang::Stmt &stmt, - FacetSet facets, Context ctx); - void addStore(const clang::Stmt &store, const clang::Expr &value, - const KindEntry &slot, Context ctx); - void addCallSites(const clang::CallExpr &call, Context ctx); - - /// The declared kind of the slot `lvalue` designates, when it has a shape. - [[nodiscard]] const KindEntry *slotKind(const clang::Expr *lvalue) const; - /// §2.1 Raw: a raw origin visible in the syntax. - [[nodiscard]] bool hasRawOrigin(const clang::Expr *pointer, - unsigned depth = 0) const; - [[nodiscard]] bool isWidening(clang::QualType from, clang::QualType to) const; - /// Whether a stored or passed value carries its own PtrArith or Cast - /// obligation, so that the store needs no Cast site of its own. - [[nodiscard]] bool carriesOwnObligation(const clang::Expr *value) const; - [[nodiscard]] bool isNullConstant(const clang::Expr *value) const; - [[nodiscard]] bool isFlexible(const clang::Expr &array) const; - [[nodiscard]] std::optional - constantBound(const clang::Expr &array) const; - [[nodiscard]] std::optional - libraryTerm(const core::LibTerm &term, const clang::CallExpr &call, - const core::LibraryMatch &match) const; -}; - -} // namespace - -//===----------------------------------------------------------------------===// -// The walk -//===----------------------------------------------------------------------===// - -PendingSite &Walker::addSite(core::SiteKind kind, const clang::Stmt &stmt, - FacetSet facets, Context ctx) { - PendingSite &site = sites.emplace_back(); - site.info.kind = kind; - site.info.stmt = &stmt; - const clang::CharSourceRange range = - sm.getExpansionRange(stmt.getSourceRange()); - site.info.begin = range.getBegin(); - site.info.end = range.getEnd(); - site.info.inUnsafe = ctx.unsafe; - site.facets = facets; - site.location = sm.getExpansionLoc(stmt.getBeginLoc()); - // The text of an operation inside a macro argument is the argument's own - // text; inside a macro body it is the whole invocation. - clang::CharSourceRange written = clang::Lexer::makeFileCharRange( - clang::CharSourceRange::getTokenRange(stmt.getSourceRange()), sm, - context.getLangOpts()); - if (written.isInvalid()) - written = range; - const llvm::StringRef text = - clang::Lexer::getSourceText(written, sm, context.getLangOpts()); - site.text = core::siteText(std::string_view(text.data(), text.size())); - site.sequence = sequence++; - return site; -} - -void Walker::walkBody() { - clang::Stmt *body = function.getBody(); - if (body == nullptr) - return; - ModifiedVariables finder; - finder.TraverseStmt(body); - modified = std::move(finder.modified); - Context ctx; - ctx.unsafe = signature.unsafe; - walkStmt(body, ctx); - if (!canComplete(body, context, library)) - return; - // The end of the body is an exit when control can reach it (§2.1). - PendingSite &end = - addSite(core::SiteKind::Call, *body, FacetSet{.temporal = true}, ctx); - end.info.boundary = core::Boundary::Exit; - const auto *compound = llvm::dyn_cast(body); - const clang::SourceLocation brace = sm.getExpansionLoc( - compound != nullptr ? compound->getRBracLoc() : body->getEndLoc()); - end.info.begin = brace; - end.info.end = brace; - end.location = brace; - end.text = "}"; -} - -void Walker::walkStmt(const clang::Stmt *stmt, Context ctx) { - if (stmt == nullptr) - return; - if (const auto *expr = llvm::dyn_cast(stmt)) { - walkExpr(expr, ctx); - return; - } - if (!visited.insert(stmt).second) - return; - if (const auto *decls = llvm::dyn_cast(stmt)) { - for (const clang::Decl *decl : decls->decls()) - walkDecl(*decl, ctx); - return; - } - if (const auto *ret = llvm::dyn_cast(stmt)) { - const KindEntry *result = kinds.result(function); - walkExpr(ret->getRetValue(), ctx, Use::Value, - result != nullptr && result->hasDeclaredShape() ? result - : nullptr); - PendingSite &exit = - addSite(core::SiteKind::Call, *ret, FacetSet{.temporal = true}, ctx); - exit.info.boundary = core::Boundary::Exit; - return; - } - if (const auto *attributed = llvm::dyn_cast(stmt)) { - if (isUnsafeBlock(*attributed)) - ctx.unsafe = true; - walkStmt(attributed->getSubStmt(), ctx); - return; - } - if (const auto *label = llvm::dyn_cast(stmt)) { - walkExpr(label->getLHS(), ctx); - walkExpr(label->getRHS(), ctx); - walkStmt(label->getSubStmt(), ctx); - return; - } - if (const auto *assembly = llvm::dyn_cast(stmt)) { - // RFC 0033 *Implementation amendments*: an operand the assembly takes - // in memory only (`"+m" (*(uint64_t (*)[16])d)`) is passed by address, - // and which of its bytes the assembly touches C does not say: its - // lvalue is no access of its whole type. Register operands are loaded. - const auto inMemory = [](llvm::StringRef constraint) { - return constraint.find_first_of("mQoV") != llvm::StringRef::npos && - constraint.find_first_of("rgiX") == llvm::StringRef::npos; - }; - for (unsigned i = 0; i < assembly->getNumOutputs(); ++i) - walkExpr(assembly->getOutputExpr(i), ctx, - inMemory(assembly->getOutputConstraint(i)) ? Use::AddressOf - : Use::Value); - for (unsigned i = 0; i < assembly->getNumInputs(); ++i) - walkExpr(assembly->getInputExpr(i), ctx, - inMemory(assembly->getInputConstraint(i)) ? Use::AddressOf - : Use::Value); - return; - } - for (const clang::Stmt *child : stmt->children()) - walkStmt(child, ctx); -} - -void Walker::walkExpr(const clang::Expr *expr, Context ctx, Use use, - const KindEntry *required) { - if (expr == nullptr || !visited.insert(expr).second) - return; - if (const auto *paren = llvm::dyn_cast(expr)) { - walkExpr(paren->getSubExpr(), ctx, use, required); - return; - } - if (const auto *unary = llvm::dyn_cast(expr)) { - if (unary->getOpcode() == clang::UO_Deref) - walkDeref(*unary, ctx, use); - else - walkUnary(*unary, ctx, required); - return; - } - if (const auto *binary = llvm::dyn_cast(expr)) { - walkBinary(*binary, ctx, use, required); - return; - } - if (const auto *choice = llvm::dyn_cast(expr)) { - walkExpr(choice->getCond(), ctx); - walkExpr(choice->getTrueExpr(), ctx, Use::Value, required); - walkExpr(choice->getFalseExpr(), ctx, Use::Value, required); - return; - } - if (const auto *elvis = - llvm::dyn_cast(expr)) { - // `a ?: b`: `a` is evaluated once and read through opaque values. - walkExpr(elvis->getCommon(), ctx); - walkExpr(elvis->getCond(), ctx); - walkExpr(elvis->getTrueExpr(), ctx, Use::Value, required); - walkExpr(elvis->getFalseExpr(), ctx, Use::Value, required); - return; - } - if (const auto *opaque = llvm::dyn_cast(expr)) { - walkExpr(opaque->getSourceExpr(), ctx); - return; - } - if (const auto *subscript = llvm::dyn_cast(expr)) { - walkSubscript(*subscript, ctx, use); - return; - } - if (const auto *member = llvm::dyn_cast(expr)) { - walkMember(*member, ctx, use); - return; - } - if (const auto *call = llvm::dyn_cast(expr)) { - walkCall(*call, ctx); - return; - } - if (const auto *cast = llvm::dyn_cast(expr)) { - walkCast(*cast, ctx, use, required); - return; - } - if (const auto *trait = - llvm::dyn_cast(expr)) { - // Unevaluated, except `sizeof` of a variable-length array, which C - // evaluates (C11 6.5.3.4p2). - if (trait->getKind() != clang::UETT_SizeOf) - return; - if (trait->isArgumentType()) { - if (context.getAsVariableArrayType(trait->getArgumentType()) != nullptr) - walkVariablyModified(trait->getArgumentType(), ctx); - return; - } - // The operand is evaluated for its address and size, not read. - if (context.getAsVariableArrayType(trait->getArgumentExpr()->getType()) != - nullptr) - walkExpr(trait->getArgumentExpr(), ctx, Use::Decay); - return; - } - if (const auto *generic = llvm::dyn_cast(expr)) { - // Only the selected association is evaluated. - if (!generic->isResultDependent()) - walkExpr(generic->getResultExpr(), ctx, use, required); - return; - } - if (const auto *choose = llvm::dyn_cast(expr)) { - walkExpr(choose->getChosenSubExpr(), ctx, use, required); - return; - } - if (const auto *statement = llvm::dyn_cast(expr)) { - walkStmt(statement->getSubStmt(), ctx); - return; - } - if (const auto *literal = llvm::dyn_cast(expr)) { - walkExpr(literal->getInitializer(), ctx); - return; - } - if (const auto *list = llvm::dyn_cast(expr)) { - walkInitList(*list, ctx); - return; - } - if (const auto *vaArg = llvm::dyn_cast(expr)) { - walkExpr(vaArg->getSubExpr(), ctx); - if (vaArg->getType()->isPointerType()) { - PendingSite &site = - addSite(core::SiteKind::IntToPtr, *vaArg, - FacetSet{.spatial = true, .temporal = true}, ctx); - site.info.operand = vaArg; - } - return; - } - if (const auto *pseudo = llvm::dyn_cast(expr)) { - // CodeGen evaluates the semantic form. - for (const clang::Expr *semantic : pseudo->semantics()) - walkExpr(semantic, ctx); - return; - } - // A block literal's body is a function of its own. - if (llvm::isa(expr)) - return; - if (const auto *constant = llvm::dyn_cast(expr)) { - walkExpr(constant->getSubExpr(), ctx, use, required); - return; - } - for (const clang::Stmt *child : expr->children()) - walkStmt(child, ctx); -} - -static bool isZeroConstant(const clang::Expr *expr, - const clang::ASTContext &context) { - const auto value = expr->getIntegerConstantExpr(context); - return value && value->isZero(); -} - -/// §2.1: the null facet exists only for a pointer value, not an array -/// lvalue; the temporal facet only when the object's lifetime can end. -static void applyOperandRules(FacetSet &facets, const clang::Expr *pointer) { - const clang::Expr *array = decayedArray(pointer); - if (array == nullptr) - return; - facets.null = false; - if (!arrayStorageCanEnd(array)) - facets.temporal = false; -} - -/// `&object`: a pointer to one whole object, whose width the types give. -static bool isAddressOfObject(const clang::Expr *pointer) { - const auto *address = - llvm::dyn_cast(pointer->IgnoreParenImpCasts()); - if (address == nullptr || address->getOpcode() != clang::UO_AddrOf) - return false; - const clang::Expr *object = address->getSubExpr()->IgnoreParens(); - return !llvm::isa(object); -} - -void Walker::walkUnary(const clang::UnaryOperator &unary, Context ctx, - const KindEntry *required) { - const clang::Expr *sub = unary.getSubExpr(); - if (unary.getOpcode() == clang::UO_AddrOf) { - walkExpr(sub, ctx, Use::AddressOf); - if (required == nullptr || !isPointerArithmetic(&unary)) - return; - // `&p[i]` and `&*(p + i)` in a required position (§7.4). - const clang::Expr *object = sub->IgnoreParens(); - const clang::Expr *pointer = nullptr; - if (const auto *subscript = - llvm::dyn_cast(object)) - pointer = subscript->getBase(); - else if (const auto *deref = llvm::dyn_cast(object)) - pointer = deref->getSubExpr(); - PendingSite &site = addSite(core::SiteKind::PtrArith, unary, - FacetSet{.spatial = true}, ctx); - site.info.operand = pointer; - site.info.spatialSystemApi = required->shapeFromSystemHeader(); - return; - } - if (unary.isIncrementDecrementOp() && unary.getType()->isPointerType()) { - // The new value is stored back into the operand, and the result flows - // into the parent: either can be a required position. - const KindEntry *slot = slotKind(sub); - const KindEntry *kind = slot != nullptr ? slot : required; - walkExpr(sub, ctx); - if (kind == nullptr) - return; - PendingSite &site = addSite(core::SiteKind::PtrArith, unary, - FacetSet{.spatial = true}, ctx); - site.info.operand = sub; - site.info.spatialSystemApi = kind->shapeFromSystemHeader(); - return; - } - walkExpr(sub, ctx); -} - -void Walker::walkBinary(const clang::BinaryOperator &binary, Context ctx, - Use use, const KindEntry *required) { - const clang::Expr *lhs = binary.getLHS(); - const clang::Expr *rhs = binary.getRHS(); - const bool pointer = binary.getType()->isPointerType(); - switch (binary.getOpcode()) { - case clang::BO_Assign: { - const KindEntry *slot = slotKind(lhs); - walkExpr(lhs, ctx); - walkExpr(rhs, ctx, Use::Value, slot); - if (slot != nullptr) - addStore(binary, *rhs, *slot, ctx); - return; - } - case clang::BO_AddAssign: - case clang::BO_SubAssign: - case clang::BO_Add: - case clang::BO_Sub: { - walkExpr(lhs, ctx); - walkExpr(rhs, ctx); - if (!pointer) - return; - const KindEntry *slot = - binary.isCompoundAssignmentOp() ? slotKind(lhs) : nullptr; - const KindEntry *kind = slot != nullptr ? slot : required; - if (kind == nullptr) - return; - const clang::Expr *operand = lhs->getType()->isPointerType() ? lhs : rhs; - PendingSite &site = addSite(core::SiteKind::PtrArith, binary, - FacetSet{.spatial = true}, ctx); - site.info.operand = operand; - site.info.spatialSystemApi = kind->shapeFromSystemHeader(); - return; - } - case clang::BO_Comma: - walkExpr(lhs, ctx); - walkExpr(rhs, ctx, use, required); - return; - default: - walkExpr(lhs, ctx); - walkExpr(rhs, ctx); - return; - } -} - -void Walker::walkDeref(const clang::UnaryOperator &deref, Context ctx, - Use use) { - const clang::Expr *sub = deref.getSubExpr(); - // `(*fp)(...)` belongs to the call; `&*p` is `p` (§2.1). - if (deref.getType()->isFunctionType() || use == Use::AddressOf) { - walkExpr(sub, ctx); - return; - } - const clang::Expr *pointer = sub; - const clang::Expr *index = nullptr; - const bool offset = isPointerOffset(sub); - if (offset) { - const auto *binary = llvm::cast(sub->IgnoreParens()); - const bool lhsPointer = binary->getLHS()->getType()->isPointerType(); - pointer = lhsPointer ? binary->getLHS() : binary->getRHS(); - if (binary->getOpcode() == clang::BO_Add) - index = lhsPointer ? binary->getRHS() : binary->getLHS(); - } - core::SiteKind kind = offset ? core::SiteKind::Index : core::SiteKind::Deref; - FacetSet facets{.spatial = true, .null = true, .temporal = true}; - if (!offset && use == Use::Decay) - facets = FacetSet{.null = true}; - if (hasRawOrigin(pointer)) - kind = core::SiteKind::Raw; - applyOperandRules(facets, pointer); - walkExpr(sub, ctx); - PendingSite &site = addSite(kind, deref, facets, ctx); - site.info.operand = pointer; - site.info.index = index; - if (!facets.spatial || (offset && index == nullptr)) - return; - if (const clang::Expr *array = decayedArray(pointer)) - site.info.provenByType = index == nullptr && !isFlexible(*array) && - constantBound(*array).value_or(0) >= 1; - else if (!offset && isAddressOfObject(pointer)) - site.info.provenByType = true; -} - -void Walker::walkSubscript(const clang::ArraySubscriptExpr &subscript, - Context ctx, Use use) { - const clang::Expr *base = subscript.getBase(); - const clang::Expr *index = subscript.getIdx(); - const clang::Expr *array = decayedArray(base); - const auto walkOperands = [&] { - if (array != nullptr) - walkExpr(array, ctx, Use::Decay); - else - walkExpr(base, ctx); - walkExpr(index, ctx); - }; - // `&p[i]` is `p + i`: a PtrArith site, and only in a required position. - if (use == Use::AddressOf) { - walkOperands(); - return; - } - const bool zero = array == nullptr && isZeroConstant(index, context); - core::SiteKind kind = zero ? core::SiteKind::Deref : core::SiteKind::Index; - FacetSet facets{.spatial = true, .null = true, .temporal = true}; - if (zero && use == Use::Decay) - facets = FacetSet{.null = true}; - if (array != nullptr) - applyOperandRules(facets, base); - else if (hasRawOrigin(base)) - kind = core::SiteKind::Raw; - walkOperands(); - PendingSite &site = addSite(kind, subscript, facets, ctx); - site.info.operand = base; - site.info.index = zero ? nullptr : index; - if (!facets.spatial) - return; - if (array != nullptr) { - const auto bound = - isFlexible(*array) ? std::nullopt : constantBound(*array); - const auto at = index->getIntegerConstantExpr(context); - site.info.provenByType = bound && at && at->isNonNegative() && - at->getActiveBits() < 63 && - at->getExtValue() < *bound; - } -} - -void Walker::walkMember(const clang::MemberExpr &member, Context ctx, Use use) { - const clang::Expr *base = member.getBase(); - if (!member.isArrow()) { - walkExpr(base, ctx, use); - return; - } - // `&((T *)0)->f`, the old `offsetof` idiom, accesses nothing (§2.1). - if (use != Use::Value && isNullConstant(base->IgnoreParenCasts())) { - walkExpr(base, ctx); - return; - } - FacetSet facets = - use == Use::Value - ? FacetSet{.spatial = true, .null = true, .temporal = true} - : FacetSet{.null = true}; - applyOperandRules(facets, base); - const core::SiteKind kind = - hasRawOrigin(base) ? core::SiteKind::Raw : core::SiteKind::Deref; - walkExpr(base, ctx); - PendingSite &site = addSite(kind, member, facets, ctx); - site.info.operand = base; - if (!facets.spatial) - return; - if (const clang::Expr *array = decayedArray(base)) - site.info.provenByType = - !isFlexible(*array) && constantBound(*array).value_or(0) >= 1; - else if (isAddressOfObject(base)) - site.info.provenByType = true; -} - -void Walker::walkCast(const clang::CastExpr &cast, Context ctx, Use use, - const KindEntry *required) { - const clang::Expr *sub = cast.getSubExpr(); - // A cast to a variably modified type evaluates the type's sizes. - if (const auto *written = llvm::dyn_cast(&cast)) - walkVariablyModified(written->getTypeAsWritten(), ctx); - switch (cast.getCastKind()) { - case clang::CK_ArrayToPointerDecay: - walkExpr(sub, ctx, Use::Decay); - return; - case clang::CK_IntegralToPointer: { - // A null pointer constant converts with CK_NullToPointer instead. - walkExpr(sub, ctx); - PendingSite &site = - addSite(core::SiteKind::IntToPtr, cast, - FacetSet{.spatial = true, .temporal = true}, ctx); - site.info.operand = &cast; - return; - } - case clang::CK_BitCast: - case clang::CK_NoOp: - if (required != nullptr && cast.getType()->isPointerType() && - sub->getType()->isPointerType() && - isWidening(sub->getType(), cast.getType())) { - walkExpr(sub, ctx); - PendingSite &site = - addSite(core::SiteKind::Cast, cast, FacetSet{.spatial = true}, ctx); - site.info.operand = sub; - site.info.spatialSystemApi = required->shapeFromSystemHeader(); - return; - } - // A conversion that is not widening passes the value, and its required - // position, through. - walkExpr(sub, ctx, cast.getCastKind() == clang::CK_NoOp ? use : Use::Value, - required); - return; - default: - walkExpr(sub, ctx); - return; - } -} - -/// A compiler builtin that is not a library function (`__builtin_clz`, -/// `__builtin_constant_p`). The `LibrarySpec` lists every builtin whose -/// arguments are pointers or which changes control flow; one without a row -/// is lowered to plain instructions and calls nothing, so it is no site. -static bool isCompilerBuiltin(const clang::FunctionDecl &callee, - const clang::ASTContext &context) { - const unsigned id = callee.getBuiltinID(); - return id != 0 && !context.BuiltinInfo.isLibFunction(id); -} - -/// Builtins whose arguments are not evaluated. -static bool hasUnevaluatedArguments(const clang::FunctionDecl &callee) { - const unsigned id = callee.getBuiltinID(); - return id == clang::Builtin::BI__builtin_constant_p || - id == clang::Builtin::BI__builtin_object_size || - id == clang::Builtin::BI__builtin_dynamic_object_size || - id == clang::Builtin::BI__builtin_classify_type; -} - -const clang::FunctionDecl *Walker::calleeOf(const clang::CallExpr &call) const { - if (const clang::FunctionDecl *direct = call.getDirectCallee()) - return direct; - if (slots == nullptr || solution == nullptr) - return nullptr; - const auto slot = slots->calleeSlot(call); - if (!slot) - return nullptr; - const core::CallResolution resolution = solution->resolveCall(*slot); - // Closed on that one target, or open with it as its only known one: the - // row says which arguments the call needs, and the engine decides those - // needs `unresolved(callback)` when the slot is open (§9.3). - if ((resolution.kind != core::IndirectCallKind::ClosedSingle && - resolution.kind != core::IndirectCallKind::OpenKnown) || - resolution.targets.size() != 1) - return nullptr; - const clang::FunctionDecl *target = - slots->function(resolution.targets.front()); - if (target == nullptr) - return nullptr; - const auto match = governingLibraryEntry(*target, library); - // Only what the row needs behind its arguments follows the slot. What it - // allocates or releases decides ownership and §11's zero-initialisation, - // which are the syntactic call's business (gate G8's oracle takes the - // address of an allocator and expects its wrapper). - if (!match || match->entry->allocates() || match->entry->releases()) - return nullptr; - return target; -} - -void Walker::walkCall(const clang::CallExpr &call, Context ctx) { - const clang::FunctionDecl *callee = calleeOf(call); - if (callee != nullptr && isReturnsTwice(*callee, library)) - callsSetjmp = true; - const bool governed = - callee != nullptr && governingLibraryEntry(*callee, library).has_value(); - walkExpr(call.getCallee(), ctx); - if (callee == nullptr || !hasUnevaluatedArguments(*callee)) { - for (unsigned i = 0; i < call.getNumArgs(); ++i) { - // §7.4: an argument for a parameter with a declared kind is a - // required position. A LibrarySpec row checks its own arguments. - // Inferred and default kinds are not: `hasDeclaredShape`. - const KindEntry *param = - callee != nullptr && !governed ? kinds.param(*callee, i) : nullptr; - walkExpr(call.getArg(i), ctx, Use::Value, - param != nullptr && param->hasDeclaredShape() ? param : nullptr); - } - } - addCallSites(call, ctx); -} - -std::optional -Walker::libraryTerm(const core::LibTerm &term, const clang::CallExpr &call, - const core::LibraryMatch &match) const { - const auto argument = [&](unsigned rowArg) -> const clang::Expr * { - const int index = match.callArgument(rowArg); - if (index < 0 || static_cast(index) >= call.getNumArgs()) - return nullptr; - return call.getArg(static_cast(index)); - }; - const auto operand = [&](std::size_t i) -> std::optional { - if (i >= term.operands.size()) - return std::nullopt; - return libraryTerm(term.operands[i], call, match); - }; - switch (term.kind) { - case core::LibTerm::Kind::Constant: - return SourceTerm::ofConstant(term.value); - case core::LibTerm::Kind::Argument: - if (const clang::Expr *arg = argument(term.arg)) - return SourceTerm::ofExpr(*arg); - return std::nullopt; - case core::LibTerm::Kind::StringLength: - if (const clang::Expr *arg = argument(term.arg)) - return SourceTerm::strLen(SourceTerm::ofExpr(*arg)); - return std::nullopt; - case core::LibTerm::Kind::Product: - case core::LibTerm::Kind::Sum: { - auto lhs = operand(0); - auto rhs = operand(1); - if (!lhs || !rhs) - return std::nullopt; - return term.kind == core::LibTerm::Kind::Product - ? SourceTerm::mul(std::move(*lhs), std::move(*rhs)) - : SourceTerm::add(std::move(*lhs), std::move(*rhs)); - } - case core::LibTerm::Kind::Difference: { - auto lhs = operand(0); - if (!lhs) - return std::nullopt; - return SourceTerm::sub(std::move(*lhs), SourceTerm::ofConstant(term.value)); - } - case core::LibTerm::Kind::Quotient: { - auto lhs = operand(0); - if (!lhs) - return std::nullopt; - return SourceTerm::div(std::move(*lhs), term.value); - } - // `fmtlen` is never a check term (§8.1); a macro's value and `min` have - // no C spelling at the call. - case core::LibTerm::Kind::FormatLength: - case core::LibTerm::Kind::Macro: - case core::LibTerm::Kind::Min: - return std::nullopt; - } - return std::nullopt; -} - -/// The function a `WEAVEC_ASSUME` calls (RFC 0012). -static bool isAssumeFunction(const clang::FunctionDecl &callee) { - return llvm::any_of(callee.redecls(), [](const clang::FunctionDecl *redecl) { - return getAnnotations(*redecl).assume; - }); -} - -/// The callee operand of an indirect call, through `*` and conversions. -static const clang::Expr *indirectCallee(const clang::CallExpr &call) { - const clang::Expr *callee = call.getCallee(); - for (unsigned depth = 0; depth < 16; ++depth) { - callee = callee->IgnoreParenImpCasts(); - const auto *deref = llvm::dyn_cast(callee); - if (deref == nullptr || deref->getOpcode() != clang::UO_Deref) - break; - callee = deref->getSubExpr(); - } - return callee; -} - -void Walker::addCallSites(const clang::CallExpr &call, Context ctx) { - const clang::FunctionDecl *callee = calleeOf(call); - const std::string name = - callee != nullptr ? callee->getNameAsString() : std::string(); - if (callee != nullptr && isAssumeFunction(*callee)) { - PendingSite &site = - addSite(core::SiteKind::Assume, call, FacetSet{.assertion = true}, ctx); - site.info.callee = callee; - site.callee = name; - return; - } - const auto match = callee != nullptr ? governingLibraryEntry(*callee, library) - : std::nullopt; - if (!match && callee != nullptr && isCompilerBuiltin(*callee, context)) - return; - std::optional released; - if (match && match->entry->releases()) { - for (unsigned i = 0; i < call.getNumArgs() && !released; ++i) { - const core::LibraryParam *param = match->param(i); - if (param != nullptr && - (param->effect == core::LibraryParam::Effect::Release || - param->effect == core::LibraryParam::Effect::Realloc)) - released = i; - } - } - - FacetSet facets; - core::SiteKind kind = core::SiteKind::Call; - std::vector needs; - std::vector shapes; - std::vector reliance; - const clang::Expr *operand = nullptr; - bool nullSystemApi = false; - bool spatialSystemApi = false; - if (released) { - // §2.1 Release: the start of an allocation, non-null when the row says - // so, and live (of the releasing family). - const clang::Expr *arg = call.getArg(*released); - const core::LibraryParam ¶m = *match->param(*released); - kind = hasRawOrigin(arg) ? core::SiteKind::Raw : core::SiteKind::Release; - facets = FacetSet{.spatial = true, - .null = param.null == core::LibraryParam::Null::Forbidden, - .temporal = true}; - applyOperandRules(facets, arg); - if (facets.null) - needs.push_back(ArgumentNeed{ - .argument = static_cast(*released), .nonnull = true}); - operand = arg; - } else if (match) { - // §2.1 LibCall: the facets the row's requirements name. - kind = core::SiteKind::LibCall; - for (unsigned i = 0; i < call.getNumArgs(); ++i) { - const clang::Expr *arg = call.getArg(i); - const core::LibraryParam *param = match->param(i); - if (param == nullptr) { - // Variadic arguments are borrowed for the call (§8). - if (match->entry->format && arg->getType()->isPointerType()) { - FacetSet borrowed{.temporal = true}; - applyOperandRules(borrowed, arg); - facets.temporal = facets.temporal || borrowed.temporal; - } - continue; - } - if (param->callback) - facets.temporal = true; - if (param->type != core::LibraryParam::Type::Pointer) - continue; - const bool accessed = param->access != core::LibraryParam::Access::None || - param->bytes || param->count || param->string; - FacetSet argument{ - .spatial = accessed, - .null = param->null != core::LibraryParam::Null::Allowed, - .temporal = accessed || - param->effect != core::LibraryParam::Effect::Borrow || - param->out.has_value()}; - applyOperandRules(argument, arg); - facets.spatial = facets.spatial || argument.spatial; - facets.temporal = facets.temporal || argument.temporal; - if (!argument.null) - continue; - facets.null = true; - ArgumentNeed need{.argument = static_cast(i), - .nonnull = true}; - if (param->null == core::LibraryParam::Null::AllowedIfZero) { - need.allowedIfZero = true; - if (param->zeroTerm) - need.unlessZero = libraryTerm(*param->zeroTerm, call, *match); - } - needs.push_back(std::move(need)); - } - if (!match->entry->disjoint.empty()) - facets.spatial = true; - if (match->entry->hasCallback()) - facets.temporal = true; - } else { - // §2.1 Call: the boundary, the callee operand of an indirect call, and - // each argument with a declared requirement. - facets.temporal = true; - if (callee == nullptr) { - facets.null = true; - operand = indirectCallee(call); - } - bool spatialOnlySystem = true; - bool nullOnlySystem = callee != nullptr; - for (unsigned i = 0; callee != nullptr && i < call.getNumArgs(); ++i) { - const KindEntry *param = kinds.param(*callee, i); - if (param == nullptr) - continue; - const clang::Expr *arg = call.getArg(i); - if (param->hasDeclaredShape()) { - facets.spatial = true; - spatialOnlySystem = spatialOnlySystem && param->shapeFromSystemHeader(); - if (!param->shapeFromSystemHeader()) - shapes.push_back(SiteInfo::DeclaredShape{ - .argument = static_cast(i), - .kind = param->kind, - .pointee = - i < callee->getNumParams() - ? callee->getParamDecl(i)->getType()->getPointeeType() - : clang::QualType()}); - } else if (param->hasEnforcedRequirement()) { - // §7.5: a static callee's inferred requirement is checked at every - // direct call, under its guard. - facets.spatial = true; - spatialOnlySystem = false; - if (auto need = inferredNullNeed(*param, call, i); - need && !param->declaresNonnull() && decayedArray(arg) == nullptr) { - facets.null = true; - nullOnlySystem = false; - needs.push_back(std::move(*need)); - } - } else if (param->enforcement == RequirementEnforcement::CallerContract && - llvm::any_of(param->mustAccess, - [](const MustAccessRequirement &requirement) { - return requirement.kind.shape != - core::PointerShape::Single; - })) { - // §7.5: an exported callee's extent beyond Single is its callers' - // contract, verified at each call the unit makes (the link step - // verifies the others). Its nullability stays the body's. - facets.spatial = true; - spatialOnlySystem = false; - } - if (!param->hasDeclaredShape() && !param->hasEnforcedRequirement() && - param->reliesOnSingle && inferred != nullptr && - arg->getType()->isPointerType() && - !inferred->argumentIsSingleValid(call, i)) { - // §7.3: a value that may be a cursor, passed where the callee relies - // on the Single default, gets the Call site's row. - facets.spatial = true; - spatialOnlySystem = false; - reliance.push_back(static_cast(i)); - } - if (!param->declaresNonnull() || !arg->getType()->isPointerType() || - decayedArray(arg) != nullptr) - continue; - facets.null = true; - nullOnlySystem = nullOnlySystem && param->nullabilityFromSystemHeader(); - needs.push_back( - ArgumentNeed{.argument = static_cast(i), - .nonnull = true, - .systemApi = param->nullabilityFromSystemHeader()}); - } - nullSystemApi = facets.null && nullOnlySystem; - spatialSystemApi = facets.spatial && spatialOnlySystem; - } - - PendingSite &site = addSite(kind, call, facets, ctx); - if (kind == core::SiteKind::Call) - site.info.boundary = core::Boundary::Call; - site.info.callee = callee; - site.info.library = match; - site.info.operand = operand; - site.info.arguments = std::move(needs); - site.info.declaredShapes = std::move(shapes); - site.info.reliance = std::move(reliance); - site.info.nullSystemApi = nullSystemApi; - site.info.spatialSystemApi = spatialSystemApi; - site.callee = name; - - if (callDoesNotReturn(call, library)) { - PendingSite &exit = - addSite(core::SiteKind::Call, call, FacetSet{.temporal = true}, ctx); - exit.info.boundary = core::Boundary::Exit; - exit.info.callee = callee; - exit.callee = name; - } -} - -void Walker::addStore(const clang::Stmt &store, const clang::Expr &value, - const KindEntry &slot, Context ctx) { - // §2.1 Cast: a pointer stored into a slot with a declared kind, unless - // the value carries the obligation itself (a PtrArith or widening Cast - // site) or is null, which satisfies every shape. - if (!value.getType()->isPointerType() || carriesOwnObligation(&value) || - isNullConstant(&value)) - return; - PendingSite &site = - addSite(core::SiteKind::Cast, store, FacetSet{.spatial = true}, ctx); - site.info.operand = &value; - site.info.spatialSystemApi = slot.shapeFromSystemHeader(); -} - -void Walker::walkInitList(const clang::InitListExpr &list, Context ctx) { - // CodeGen reads the semantic form (§2.1). - const clang::InitListExpr *semantic = - list.isSemanticForm() ? &list : list.getSemanticForm(); - if (semantic == nullptr) - semantic = &list; - if (semantic != &list && !visited.insert(semantic).second) - return; - const clang::RecordDecl *record = semantic->getType()->getAsRecordDecl(); - const auto store = [&](const clang::FieldDecl *field, - const clang::Expr *init) { - const KindEntry *slot = field != nullptr ? kinds.field(*field) : nullptr; - if (slot != nullptr && !slot->hasDeclaredShape()) - slot = nullptr; - walkExpr(init, ctx, Use::Value, slot); - if (slot != nullptr && init != nullptr) - addStore(*init, *init, *slot, ctx); - }; - if (record == nullptr) { - for (const clang::Expr *init : semantic->inits()) - walkExpr(init, ctx); - return; - } - if (record->isUnion()) { - for (const clang::Expr *init : semantic->inits()) - store(semantic->getInitializedFieldInUnion(), init); - return; - } - auto field = record->field_begin(); - for (const clang::Expr *init : semantic->inits()) { - while (field != record->field_end() && field->isUnnamedBitField()) - ++field; - const clang::FieldDecl *current = - field != record->field_end() ? *field : nullptr; - if (field != record->field_end()) - ++field; - store(current, init); - } -} - -void Walker::walkDecl(const clang::Decl &decl, Context ctx) { - if (const auto *variable = llvm::dyn_cast(&decl)) { - walkVariablyModified(variable->getType(), ctx); - if (const clang::Expr *init = variable->getInit()) - walkExpr(init, ctx); - return; - } - if (const auto *alias = llvm::dyn_cast(&decl)) - walkVariablyModified(alias->getUnderlyingType(), ctx); -} - -void Walker::walkVariablyModified(clang::QualType type, Context ctx) { - // The size expressions of a variably modified type are evaluated where - // the type is declared, cast to, or taken the `sizeof` of. - for (unsigned depth = 0; - depth < 32 && !type.isNull() && type->isVariablyModifiedType(); - ++depth) { - if (const auto *variable = context.getAsVariableArrayType(type)) { - walkExpr(variable->getSizeExpr(), ctx); - type = variable->getElementType(); - } else if (const auto *array = context.getAsArrayType(type)) { - type = array->getElementType(); - } else if (const auto *pointer = type->getAs()) { - type = pointer->getPointeeType(); - } else { - return; - } - } -} - -const KindEntry *Walker::slotKind(const clang::Expr *lvalue) const { - const clang::Expr *expr = lvalue->IgnoreParenImpCasts(); - const KindEntry *entry = nullptr; - if (const auto *member = llvm::dyn_cast(expr)) { - if (const auto *field = - llvm::dyn_cast(member->getMemberDecl())) - entry = kinds.field(*field); - } else if (const auto *ref = llvm::dyn_cast(expr)) { - if (const auto *variable = llvm::dyn_cast(ref->getDecl()); - variable != nullptr && variable->hasGlobalStorage()) - entry = kinds.variable(*variable); - } - return entry != nullptr && entry->hasDeclaredShape() ? entry : nullptr; -} - -bool Walker::hasRawOrigin(const clang::Expr *pointer, unsigned depth) const { - if (pointer == nullptr || depth > 16) - return false; - const clang::Expr *expr = pointer->IgnoreParens(); - // A pointer converted from an integer is not raw (RFC 0033 §2): only a - // declaration makes one. - while (const auto *cast = llvm::dyn_cast(expr)) { - if (cast->getCastKind() != clang::CK_LValueToRValue && - cast->getCastKind() != clang::CK_NoOp && - cast->getCastKind() != clang::CK_BitCast) - return false; - expr = cast->getSubExpr()->IgnoreParens(); - } - if (const auto *ref = llvm::dyn_cast(expr)) { - if (const auto *param = llvm::dyn_cast(ref->getDecl()); - param != nullptr && param->getDeclContext() == &function) { - const unsigned index = param->getFunctionScopeIndex(); - return index < signature.params.size() && signature.params[index].raw; - } - return getAnnotations(*ref->getDecl()).raw; - } - if (const auto *member = llvm::dyn_cast(expr)) { - if (getAnnotations(*member->getMemberDecl()).raw) - return true; - // Loaded through a raw pointer. - return member->isArrow() && hasRawOrigin(member->getBase(), depth + 1); - } - if (const auto *unary = llvm::dyn_cast(expr)) - return (unary->getOpcode() == clang::UO_Deref || - unary->isIncrementDecrementOp()) && - hasRawOrigin(unary->getSubExpr(), depth + 1); - if (const auto *subscript = llvm::dyn_cast(expr)) - return decayedArray(subscript->getBase()) == nullptr && - hasRawOrigin(subscript->getBase(), depth + 1); - if (const auto *binary = llvm::dyn_cast(expr)) { - if (isPointerOffset(binary)) - return hasRawOrigin(binary->getLHS()->getType()->isPointerType() - ? binary->getLHS() - : binary->getRHS(), - depth + 1); - if (binary->getOpcode() == clang::BO_Comma || - binary->getOpcode() == clang::BO_Assign) - return hasRawOrigin(binary->getRHS(), depth + 1); - if (binary->isCompoundAssignmentOp()) - return hasRawOrigin(binary->getLHS(), depth + 1); - return false; - } - if (const auto *choice = llvm::dyn_cast(expr)) - return hasRawOrigin(choice->getTrueExpr(), depth + 1) && - hasRawOrigin(choice->getFalseExpr(), depth + 1); - // Handed out as raw by a callee. - if (const auto *call = llvm::dyn_cast(expr)) - if (const clang::FunctionDecl *callee = call->getDirectCallee()) - return collectAnnotations(*callee).result.raw; - return false; -} - -bool Walker::isWidening(clang::QualType from, clang::QualType to) const { - // §2.1 Cast: `U` is a complete object type wider than `T`; `void` and - // character types count as one byte. - const clang::QualType source = from->getPointeeType(); - const clang::QualType target = to->getPointeeType(); - if (source.isNull() || target.isNull() || source->isFunctionType() || - target->isFunctionType() || target->isVoidType() || - target->isIncompleteType() || !target->isConstantSizeType()) - return false; - const auto width = [this](clang::QualType type) -> std::int64_t { - if (type->isVoidType() || type->isCharType() || type->isIncompleteType() || - !type->isConstantSizeType()) - return 1; - return context.getTypeSizeInChars(type).getQuantity(); - }; - return width(target) > width(source); -} - -bool Walker::carriesOwnObligation(const clang::Expr *value) const { - const clang::Expr *expr = value; - for (unsigned depth = 0; expr != nullptr && depth < 32; ++depth) { - expr = expr->IgnoreParens(); - if (isPointerArithmetic(expr)) - return true; - const auto *cast = llvm::dyn_cast(expr); - if (cast == nullptr || (cast->getCastKind() != clang::CK_BitCast && - cast->getCastKind() != clang::CK_NoOp)) - return false; - const clang::Expr *sub = cast->getSubExpr(); - if (cast->getType()->isPointerType() && sub->getType()->isPointerType() && - isWidening(sub->getType(), cast->getType())) - return true; - expr = sub; - } - return false; -} - -bool Walker::isNullConstant(const clang::Expr *value) const { - if (llvm::isa(value->IgnoreParens())) - return true; - return value->isNullPointerConstant( - context, clang::Expr::NPC_ValueDependentIsNotNull) != - clang::Expr::NPCK_NotNull; -} - -bool Walker::isFlexible(const clang::Expr &array) const { - // §7.4: at the default -fstrict-flex-arrays level every trailing array - // member is flexible, whatever its declared bound. - return array.isFlexibleArrayMemberLike( - context, context.getLangOpts().getStrictFlexArraysLevel()); -} - -std::optional -Walker::constantBound(const clang::Expr &array) const { - const auto *constant = context.getAsConstantArrayType(array.getType()); - if (constant == nullptr) - return std::nullopt; - const auto size = constant->getSize().tryZExtValue(); - if (!size || *size > static_cast( - std::numeric_limits::max())) - return std::nullopt; - return static_cast(*size); -} - -//===----------------------------------------------------------------------===// -// SiteCollector -//===----------------------------------------------------------------------===// - -bool SiteCollector::isEmitted(const clang::FunctionDecl &function) { - if (!function.doesThisDeclarationHaveABody() || function.isInvalidDecl()) - return false; - // Functions defined in system headers are never analysed or instrumented - // (§5.6), and are not in the ledger (§2.6); nor is the check prelude, - // which `weavec-cc` appends to the predefines (§10.2). - const clang::SourceManager &sm = context.getSourceManager(); - if (sm.isInSystemHeader(function.getLocation()) || - sm.isWrittenInBuiltinFile(sm.getExpansionLoc(function.getLocation()))) - return false; - if (context.DeclMustBeEmitted(&function)) - return true; - // C99 `inline` and `gnu_inline` definitions: `available_externally`, - // inlined at -O1 and above. - if (context.GetGVALinkageForFunction(&function) == - clang::GVA_AvailableExternally) - return true; - // Internal-linkage definitions that are used. - return !function.isExternallyVisible() && function.isUsed(); -} - -/// Function definitions in declaration (source) order. -static void collectDefinitions(const clang::DeclContext &dc, - std::vector &out) { - for (const clang::Decl *decl : dc.decls()) { - if (const auto *function = llvm::dyn_cast(decl)) { - if (function->doesThisDeclarationHaveABody()) - out.push_back(function); - continue; - } - if (llvm::isa(decl)) - collectDefinitions(*llvm::cast(decl), out); - } -} - -std::vector SiteCollector::emittedFunctions() { - std::vector definitions; - collectDefinitions(*context.getTranslationUnitDecl(), definitions); - std::vector emitted; - for (const clang::FunctionDecl *function : definitions) - if (isEmitted(*function)) - emitted.push_back(function); - return emitted; -} - -SiteIndex SiteCollector::collect() { - const clang::SourceManager &sm = context.getSourceManager(); - SiteIndex index; - for (const clang::FunctionDecl *function : emittedFunctions()) { - const auto functionIndex = - static_cast(index.functionList.size()); - Walker walker(context, kinds, library, *function, inferred, slots, - solution); - walker.walkBody(); - std::vector &pending = walker.sites; - // §2.1: ordinals in source order; sites that begin at the same place - // keep walk order, which puts inner operations first and a call's exit - // after the call. - std::ranges::stable_sort( - pending, [&sm](const PendingSite &a, const PendingSite &b) { - if (a.location != b.location && - sm.isBeforeInTranslationUnit(a.location, b.location)) - return true; - if (a.location != b.location && - sm.isBeforeInTranslationUnit(b.location, a.location)) - return false; - return a.sequence < b.sequence; - }); - - SiteIndex::FunctionSites sites; - sites.decl = function; - sites.index = functionIndex; - sites.unsafe = collectAnnotations(*function).unsafe; - sites.callsSetjmp = walker.callsSetjmp; - const clang::CharSourceRange range = - sm.getExpansionRange(function->getSourceRange()); - sites.begin = range.getBegin(); - sites.end = range.getEnd(); - - core::FunctionLedger row; - row.name = function->getNameAsString(); - const core::SourceLocation where = - toCoreLocation(sm, function->getLocation()); - row.file = where.file; - row.line = where.line; - row.sites.reserve(pending.size()); - sites.sites.reserve(pending.size()); - for (std::size_t i = 0; i < pending.size(); ++i) { - PendingSite &site = pending[i]; - const core::SiteId id{.function = functionIndex, - .ordinal = static_cast(i)}; - site.info.id = id; - core::Site &ledgerSite = row.sites.emplace_back(); - ledgerSite.ordinal = id.ordinal; - ledgerSite.kind = site.info.kind; - ledgerSite.location = toCoreLocation(sm, site.location); - ledgerSite.text = std::move(site.text); - ledgerSite.boundary = site.info.boundary; - ledgerSite.callee = std::move(site.callee); - if (site.facets.spatial) - ledgerSite.addFacet(core::Facet::Spatial); - if (site.facets.null) - ledgerSite.addFacet(core::Facet::Null); - if (site.facets.temporal) - ledgerSite.addFacet(core::Facet::Temporal); - if (site.facets.assertion) - ledgerSite.addFacet(core::Facet::Assertion); - index.byStmt[site.info.stmt].push_back(id); - sites.sites.push_back(std::move(site.info)); - } - index.byFunction[function->getCanonicalDecl()] = functionIndex; - index.functionList.push_back(std::move(sites)); - index.rows.push_back(std::move(row)); - } - return index; -} - -} // namespace weavec::analysis diff --git a/lib/Analysis/SlotCollector.cpp b/lib/Analysis/SlotCollector.cpp deleted file mode 100644 index 7247572a..00000000 --- a/lib/Analysis/SlotCollector.cpp +++ /dev/null @@ -1,908 +0,0 @@ -//===- SlotCollector.cpp - Function-pointer slots of a unit (RFC 0030) ----===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// - -#include "weavec/Analysis/SlotCollector.h" - -#include "weavec/Analysis/KindTable.h" -#include "weavec/Analysis/ProgramDatabase.h" - -#include "clang/AST/RecursiveASTVisitor.h" -#include "clang/Basic/SourceManager.h" - -#include "llvm/ADT/DenseSet.h" -#include "llvm/ADT/STLExtras.h" - -#include -#include - -namespace weavec::analysis { - -// -- Names -------------------------------------------------------------------- - -std::string SlotCollector::functionName(const clang::FunctionDecl &function, - llvm::StringRef unit) { - if (function.isExternallyVisible()) - return function.getNameAsString(); - return unit.str() + ":" + function.getNameAsString(); -} - -std::string SlotCollector::recordSlotKey(const clang::RecordDecl &record, - const clang::ASTContext &context, - llvm::StringRef unit) { - const clang::SourceManager &sm = context.getSourceManager(); - const clang::RecordDecl *definition = record.getDefinition(); - const clang::RecordDecl &named = definition != nullptr ? *definition : record; - std::string key = recordTypeKey(context.getCanonicalTagType(&named), context); - if (key.empty()) { - if (const clang::TypedefNameDecl *name = named.getTypedefNameForAnonDecl()) - key = name->getNameAsString(); - } - if (key.empty()) { - // An anonymous record: named by where it is declared. - const clang::PresumedLoc where = - sm.getPresumedLoc(sm.getExpansionLoc(named.getLocation())); - key = std::string("@") + - (where.isValid() ? std::string(where.getFilename()) + ":" + - std::to_string(where.getLine()) + ":" + - std::to_string(where.getColumn()) - : std::string("?")); - } - // §9.3: a record a main file defines is the unit's own. - if (sm.isInMainFile(sm.getExpansionLoc(named.getLocation()))) - return unit.str() + ":" + key; - return key; -} - -std::optional -SlotCollection::calleeSlot(const clang::CallExpr &call) const { - const auto found = callIndex.find(&call); - if (found == callIndex.end()) - return std::nullopt; - return calls[found->second].second; -} - -const clang::FunctionDecl * -SlotCollection::function(llvm::StringRef name) const { - const auto found = functions.find(name); - return found == functions.end() ? nullptr : found->second; -} - -/// The reference that names a direct call's callee, through `*`, `&` and -/// parentheses. -static const clang::DeclRefExpr * -directCalleeReference(const clang::CallExpr &call) { - const clang::Expr *callee = call.getCallee(); - while (callee != nullptr) { - callee = callee->IgnoreParenImpCasts(); - const auto *unary = llvm::dyn_cast(callee); - if (unary == nullptr || (unary->getOpcode() != clang::UO_Deref && - unary->getOpcode() != clang::UO_AddrOf)) - break; - callee = unary->getSubExpr(); - } - const auto *ref = llvm::dyn_cast_or_null(callee); - return ref != nullptr && llvm::isa(ref->getDecl()) - ? ref - : nullptr; -} - -/// A function pointer, or an array of them (whose elements share a slot). -static bool holdsFunctionPointers(clang::QualType type, - const clang::ASTContext &context) { - while (const clang::ArrayType *array = context.getAsArrayType(type)) - type = array->getElementType(); - return type->isFunctionPointerType(); -} - -// The walker is the collection's friend, so it has a name outside an anonymous -// namespace. - -namespace { -/// Where a function-pointer value may come from. -struct SlotSources { - std::set functions; - std::vector slots; - std::vector opens; -}; -} // namespace - -/// Every function-pointer value of the unit. -class SlotWalker : public clang::RecursiveASTVisitor { -public: - SlotWalker(clang::ASTContext &ctx, const core::LibrarySpec &spec, - SlotCollection &collection) - : context(ctx), sm(ctx.getSourceManager()), library(spec), - out(collection) {} - - // RecursiveASTVisitor's CRTP hooks are found by name. - // NOLINTBEGIN(readability-identifier-naming,bugprone-derived-method-shadowing-base-method) - bool TraverseFunctionDecl(clang::FunctionDecl *function) { - // RFC 0030 §10.2: the check prelude is WeaveC's own code. - if (sm.isWrittenInBuiltinFile(sm.getExpansionLoc(function->getLocation()))) - return true; - const clang::FunctionDecl *saved = current; - if (function->doesThisDeclarationHaveABody()) - current = function; - const bool result = RecursiveASTVisitor::TraverseFunctionDecl(function); - current = saved; - return result; - } - bool VisitFunctionDecl(clang::FunctionDecl *function); - bool VisitRecordDecl(clang::RecordDecl *record); - bool VisitVarDecl(clang::VarDecl *variable); - bool VisitBinaryOperator(clang::BinaryOperator *op); - bool VisitUnaryOperator(clang::UnaryOperator *op); - bool VisitReturnStmt(clang::ReturnStmt *ret); - bool VisitCallExpr(clang::CallExpr *call); - bool VisitDeclRefExpr(clang::DeclRefExpr *ref); - bool VisitCastExpr(clang::CastExpr *cast); - bool VisitCompoundLiteralExpr(clang::CompoundLiteralExpr *literal); - // NOLINTEND(readability-identifier-naming,bugprone-derived-method-shadowing-base-method) - - /// After the walk: stores the syntax does not name, escaping addresses, - /// byte-wise writes, and the closedness rules. - void finish(); - -private: - clang::ASTContext &context; - const clang::SourceManager &sm; - const core::LibrarySpec &library; - SlotCollection &out; - const clang::FunctionDecl *current = nullptr; - - /// Direct-callee references, which do not take a function's address. - llvm::DenseSet callees; - llvm::DenseSet addressTaken; - /// `&slot` operands that are arguments of calls, and whether each - /// escapes there (a callee without a body, an indirect call, or a row - /// that writes through it). - llvm::DenseMap> - addressArguments; - /// Slots whose address is taken. - std::set taken; - /// Values stored through a pointer the syntax does not name. - std::vector unnamed; - /// Record types whose objects may come from, or go to, code outside the - /// unit (§9.3 closedness). - llvm::DenseSet leaked; - /// Records whose function-pointer fields receive outside bytes. - std::vector> written; - /// Union members stored, by union. - llvm::DenseMap> - unionStores; - /// Records defined in the unit, in source order. - std::vector records; - - [[nodiscard]] std::string name(const clang::FunctionDecl &function) const { - return SlotCollector::functionName(function, out.unit()); - } - [[nodiscard]] core::SlotKey fieldSlot(const clang::FieldDecl &field) const { - return core::SlotKey::field( - SlotCollector::recordSlotKey(*field.getParent(), context, out.unit()), - field.getNameAsString()); - } - [[nodiscard]] std::optional - variableSlot(const clang::VarDecl &variable) const; - [[nodiscard]] std::optional - lvalueSlot(const clang::Expr *lvalue) const; - [[nodiscard]] SlotSources sources(const clang::Expr *expr); - void lvalueSources(const clang::Expr *lvalue, SlotSources &into); - void flow(const SlotSources &from, const core::SlotKey &to); - void escape(const SlotSources &from); - void initialise(const clang::Expr *init, std::optional slot, - clang::QualType type); - [[nodiscard]] core::SlotKey calleeSlotOf(const clang::CallExpr &call); - void leak(clang::QualType type); - void byteWrite(const clang::Expr *argument, std::string why); - [[nodiscard]] std::string where(const clang::Stmt &stmt) const; -}; - -std::optional -SlotWalker::variableSlot(const clang::VarDecl &variable) const { - if (const auto *param = llvm::dyn_cast(&variable)) { - const auto *function = - llvm::dyn_cast(param->getDeclContext()); - if (function == nullptr) - return std::nullopt; - return core::SlotKey::param(name(*function), - param->getFunctionScopeIndex()); - } - const std::string own = variable.getNameAsString(); - if (variable.isStaticLocal()) { - const auto *function = - llvm::dyn_cast(variable.getDeclContext()); - return core::SlotKey::staticGlobal( - out.unit(), - (function != nullptr ? function->getNameAsString() + "." : "") + own); - } - if (variable.hasGlobalStorage()) - return variable.isExternallyVisible() - ? core::SlotKey::global(own) - : core::SlotKey::staticGlobal(out.unit(), own); - const auto *function = - llvm::dyn_cast(variable.getDeclContext()); - std::string scope = out.unit(); - if (function != nullptr) - scope = name(*function); - else if (current != nullptr) - scope = name(*current); - return core::SlotKey::local(scope, own); -} - -std::optional -SlotWalker::lvalueSlot(const clang::Expr *lvalue) const { - lvalue = lvalue->IgnoreParens(); - if (const auto *ref = llvm::dyn_cast(lvalue)) { - if (const auto *variable = llvm::dyn_cast(ref->getDecl())) - return variableSlot(*variable); - return std::nullopt; - } - if (const auto *member = llvm::dyn_cast(lvalue)) { - if (const auto *field = - llvm::dyn_cast(member->getMemberDecl())) - return fieldSlot(*field); - return std::nullopt; - } - if (const auto *subscript = - llvm::dyn_cast(lvalue)) { - // An element of an array of function pointers shares the array's slot; - // an element through a pointer has no name. - const clang::Expr *base = subscript->getBase()->IgnoreParenImpCasts(); - if (base->getType()->isArrayType()) - return lvalueSlot(base); - } - return std::nullopt; -} - -void SlotWalker::lvalueSources(const clang::Expr *lvalue, SlotSources &into) { - lvalue = lvalue->IgnoreParens(); - if (const auto slot = lvalueSlot(lvalue)) { - into.slots.push_back(*slot); - return; - } - if (llvm::isa(lvalue)) { - into.opens.emplace_back("an unmodelled compound literal"); - return; - } - into.opens.emplace_back("a load through a pointer"); -} - -SlotSources SlotWalker::sources(const clang::Expr *expr) { - SlotSources into; - std::vector work{expr}; - unsigned steps = 0; - while (!work.empty() && ++steps < 256) { - const clang::Expr *each = work.back(); - work.pop_back(); - if (each == nullptr) - continue; - each = each->IgnoreParens(); - if (!each->isValueDependent() && - each->isNullPointerConstant(context, - clang::Expr::NPC_ValueDependentIsNotNull) != - clang::Expr::NPCK_NotNull) - continue; - if (const auto *cast = llvm::dyn_cast(each)) { - const clang::Expr *sub = cast->getSubExpr(); - switch (cast->getCastKind()) { - case clang::CK_LValueToRValue: - lvalueSources(sub, into); - continue; - case clang::CK_FunctionToPointerDecay: - case clang::CK_NoOp: - work.push_back(sub); - continue; - case clang::CK_NullToPointer: - continue; - case clang::CK_BitCast: - // Casts between function-pointer types preserve the value (§9.3). - if (sub->getType()->isFunctionPointerType() || - sub->getType()->isFunctionType()) - work.push_back(sub); - else - into.opens.emplace_back("a conversion from a data pointer"); - continue; - case clang::CK_IntegralToPointer: - into.opens.emplace_back("a conversion from an integer"); - continue; - default: - into.opens.emplace_back("an unmodelled conversion"); - continue; - } - } - if (const auto *ref = llvm::dyn_cast(each)) { - if (const auto *function = - llvm::dyn_cast(ref->getDecl())) - into.functions.insert(name(*function)); - else - lvalueSources(each, into); - continue; - } - if (const auto *unary = llvm::dyn_cast(each)) { - // `&f` and `*fp` keep the value; `*pp` loads through a pointer. - if (unary->getOpcode() == clang::UO_AddrOf || - (unary->getOpcode() == clang::UO_Deref && - unary->getType()->isFunctionType())) - work.push_back(unary->getSubExpr()); - else if (unary->getOpcode() == clang::UO_Deref) - into.opens.emplace_back("a load through a pointer"); - else - into.opens.emplace_back("an unmodelled expression"); - continue; - } - if (llvm::isa(each) || - llvm::isa(each)) { - lvalueSources(each, into); - continue; - } - if (const auto *call = llvm::dyn_cast(each)) { - if (const clang::FunctionDecl *callee = call->getDirectCallee()) - into.slots.push_back(core::SlotKey::result(name(*callee))); - else - into.slots.push_back(core::SlotKey::callResult(calleeSlotOf(*call))); - continue; - } - if (const auto *conditional = - llvm::dyn_cast(each)) { - work.push_back(conditional->getTrueExpr()); - work.push_back(conditional->getFalseExpr()); - continue; - } - if (const auto *opaque = llvm::dyn_cast(each)) { - if (opaque->getSourceExpr() != nullptr) - work.push_back(opaque->getSourceExpr()); - continue; - } - if (const auto *binary = llvm::dyn_cast(each); - binary != nullptr && (binary->getOpcode() == clang::BO_Comma || - binary->getOpcode() == clang::BO_Assign)) { - work.push_back(binary->getRHS()); - continue; - } - if (const auto *choose = llvm::dyn_cast(each)) { - work.push_back(choose->getChosenSubExpr()); - continue; - } - if (const auto *generic = llvm::dyn_cast(each); - generic != nullptr && !generic->isResultDependent()) { - work.push_back(generic->getResultExpr()); - continue; - } - into.opens.emplace_back(llvm::isa(each) - ? "a value from va_arg" - : "an unmodelled expression"); - } - if (!work.empty()) - into.opens.emplace_back("an expression too deep to follow"); - return into; -} - -void SlotWalker::flow(const SlotSources &from, const core::SlotKey &to) { - for (const std::string &function : from.functions) - out.all.addMember(function, to); - for (const core::SlotKey &slot : from.slots) - if (slot != to) - out.all.addSubset(slot, to); - for (const std::string &why : from.opens) - out.all.addOpen(to, why); -} - -void SlotWalker::escape(const SlotSources &from) { - // Code outside the unit may call what it receives. - flow(from, core::SlotKey::param(std::string(core::UnknownFunction), 0)); -} - -void SlotWalker::initialise(const clang::Expr *init, - std::optional slot, - clang::QualType type) { - if (init == nullptr || llvm::isa(init)) - return; - const auto *list = llvm::dyn_cast(init->IgnoreParens()); - if (list == nullptr) { - if (slot && type->isFunctionPointerType()) - flow(sources(init), *slot); - return; - } - if (!list->isSemanticForm() && list->getSemanticForm() != nullptr) - list = list->getSemanticForm(); - if (const clang::ArrayType *array = context.getAsArrayType(type)) { - // Array elements share the array's slot. - for (const clang::Expr *element : list->inits()) - initialise(element, slot, array->getElementType()); - return; - } - const clang::RecordDecl *record = type->getAsRecordDecl(); - if (record == nullptr) { - if (list->getNumInits() == 1) - initialise(list->getInit(0), slot, type); - return; - } - if (record->isUnion()) { - if (const clang::FieldDecl *field = list->getInitializedFieldInUnion(); - field != nullptr && list->getNumInits() > 0) { - unionStores[record].push_back(field); - initialise(list->getInit(0), - holdsFunctionPointers(field->getType(), context) - ? std::optional(fieldSlot(*field)) - : std::nullopt, - field->getType()); - } - return; - } - unsigned index = 0; - for (const clang::FieldDecl *field : record->fields()) { - if (field->isUnnamedBitField()) - continue; - if (index >= list->getNumInits()) - break; - initialise(list->getInit(index++), - holdsFunctionPointers(field->getType(), context) - ? std::optional(fieldSlot(*field)) - : std::nullopt, - field->getType()); - } -} - -core::SlotKey SlotWalker::calleeSlotOf(const clang::CallExpr &call) { - const SlotSources from = sources(call.getCallee()); - if (from.functions.empty() && from.opens.empty() && from.slots.size() == 1) - return from.slots.front(); - // A callee expression with no single slot gets a slot of its own. - const core::SlotKey slot = - core::SlotKey::local(current != nullptr ? name(*current) : out.unit(), - ""); - flow(from, slot); - return slot; -} - -std::string SlotWalker::where(const clang::Stmt &stmt) const { - const clang::PresumedLoc at = - sm.getPresumedLoc(sm.getExpansionLoc(stmt.getBeginLoc())); - if (!at.isValid()) - return "?"; - return std::to_string(at.getLine()) + ":" + std::to_string(at.getColumn()); -} - -void SlotWalker::leak(clang::QualType type) { - std::vector work{type}; - llvm::DenseSet seen; - while (!work.empty()) { - clang::QualType each = work.back(); - work.pop_back(); - if (each.isNull() || - !seen.insert(each.getCanonicalType().getTypePtr()).second) - continue; - each = each.getCanonicalType(); - if (const auto *pointer = each->getAs()) { - work.push_back(pointer->getPointeeType()); - } else if (const clang::ArrayType *array = context.getAsArrayType(each)) { - work.push_back(array->getElementType()); - } else if (const clang::RecordDecl *record = each->getAsRecordDecl()) { - const clang::RecordDecl *definition = record->getDefinition(); - if (definition == nullptr) - continue; - leaked.insert(definition); - for (const clang::FieldDecl *field : definition->fields()) - work.push_back(field->getType()); - } - } -} - -void SlotWalker::byteWrite(const clang::Expr *argument, std::string why) { - clang::QualType type = argument->getType(); - if (!type->isPointerType()) - return; - type = type->getPointeeType(); - while (const clang::ArrayType *array = context.getAsArrayType(type)) - type = array->getElementType(); - if (const clang::RecordDecl *record = type->getAsRecordDecl()) - written.emplace_back(record, std::move(why)); -} - -/// `expr` without parentheses and conversions between pointer types. -static const clang::Expr *withoutPointerCasts(const clang::Expr *expr) { - while (true) { - expr = expr->IgnoreParens(); - const auto *cast = llvm::dyn_cast(expr); - if (cast == nullptr || - (cast->getCastKind() != clang::CK_BitCast && - cast->getCastKind() != clang::CK_NoOp) || - !cast->getSubExpr()->getType()->isPointerType()) - return expr; - expr = cast->getSubExpr(); - } -} - -bool SlotWalker::VisitFunctionDecl(clang::FunctionDecl *function) { - const std::string own = name(*function); - const clang::FunctionDecl *known = out.function(own); - if (known == nullptr || function->doesThisDeclarationHaveABody()) - out.functions[own] = function; - if (function->doesThisDeclarationHaveABody()) { - out.unitRules.defined.insert(own); - if (function->isExternallyVisible()) - out.unitRules.exported.insert(own); - } - return true; -} - -bool SlotWalker::VisitVarDecl(clang::VarDecl *variable) { - if (llvm::isa(variable)) - return true; - const clang::QualType type = variable->getType(); - // §9.3 closedness: objects reachable from an externally visible variable - // may be stored by other units. - if (variable->hasGlobalStorage() && variable->isExternallyVisible()) - leak(type); - if (const clang::Expr *init = variable->getInit()) - initialise(init, - holdsFunctionPointers(type, context) ? variableSlot(*variable) - : std::nullopt, - type); - return true; -} - -bool SlotWalker::VisitCompoundLiteralExpr(clang::CompoundLiteralExpr *literal) { - initialise(literal->getInitializer(), std::nullopt, literal->getType()); - return true; -} - -bool SlotWalker::VisitReturnStmt(clang::ReturnStmt *ret) { - if (current != nullptr && ret->getRetValue() != nullptr && - current->getReturnType()->isFunctionPointerType()) - flow(sources(ret->getRetValue()), core::SlotKey::result(name(*current))); - return true; -} - -bool SlotWalker::VisitRecordDecl(clang::RecordDecl *record) { - if (record->isThisDeclarationADefinition()) - records.push_back(record); - return true; -} - -bool SlotWalker::VisitBinaryOperator(clang::BinaryOperator *op) { - if (!op->isAssignmentOp()) - return true; - const clang::Expr *lhs = op->getLHS()->IgnoreParens(); - if (const auto *member = llvm::dyn_cast(lhs)) - if (const auto *field = - llvm::dyn_cast(member->getMemberDecl()); - field != nullptr && field->getParent()->isUnion()) - unionStores[field->getParent()].push_back(field); - // A character-typed store into an object writes its bytes. - if (lhs->getType()->isCharType()) { - const clang::Expr *base = nullptr; - if (const auto *unary = llvm::dyn_cast(lhs); - unary != nullptr && unary->getOpcode() == clang::UO_Deref) - base = unary->getSubExpr(); - else if (const auto *subscript = - llvm::dyn_cast(lhs)) - base = subscript->getBase(); - if (base != nullptr) - byteWrite(withoutPointerCasts(base), "a character-typed store"); - } - if (op->getOpcode() != clang::BO_Assign || - !lhs->getType()->isFunctionPointerType()) - return true; - SlotSources value = sources(op->getRHS()); - if (const auto slot = lvalueSlot(lhs)) - flow(value, *slot); - else - unnamed.push_back(std::move(value)); - return true; -} - -bool SlotWalker::VisitUnaryOperator(clang::UnaryOperator *op) { - if (op->getOpcode() != clang::UO_AddrOf) - return true; - const clang::Expr *target = op->getSubExpr()->IgnoreParens(); - if (!holdsFunctionPointers(target->getType(), context)) - return true; - const auto slot = lvalueSlot(target); - if (!slot) - return true; - taken.insert(*slot); - // An address that is only an argument of a function the unit defines (or - // of a row that does not write through it) is followed by the stores - // through it; any other escapes. - const auto argument = addressArguments.find(op); - const std::optional escapes = - argument == addressArguments.end() - ? std::optional("its address escapes") - : argument->second; - if (!escapes) - return true; - if (slot->kind == core::SlotKind::Static) - out.unitRules.escapedStatics.insert(slot->staticName()); - else - out.all.addOpen(*slot, *escapes); - return true; -} - -/// A call to a `LibrarySpec` allocator. -static bool isAllocation(const clang::Expr *expr, - const core::LibrarySpec &library) { - const auto *call = llvm::dyn_cast( - withoutPointerCasts(expr)->IgnoreParenImpCasts()); - const clang::FunctionDecl *callee = - call != nullptr ? call->getDirectCallee() : nullptr; - if (callee == nullptr) - return false; - const auto match = governingLibraryEntry(*callee, library); - return match && match->entry->result.kind == core::LibraryResult::Kind::Fresh; -} - -bool SlotWalker::VisitCallExpr(clang::CallExpr *call) { - if (const clang::DeclRefExpr *ref = directCalleeReference(*call)) - callees.insert(ref); - const clang::FunctionDecl *callee = call->getDirectCallee(); - const auto match = callee != nullptr ? governingLibraryEntry(*callee, library) - : std::nullopt; - const bool body = callee != nullptr && callee->getDefinition() != nullptr; - const std::string who = callee != nullptr - ? "'" + callee->getNameAsString() + "'" - : std::string("an indirect call"); - std::optional through; - if (callee == nullptr) { - through = calleeSlotOf(*call); - out.callIndex[call] = out.calls.size(); - out.calls.emplace_back(call, *through); - } - for (unsigned i = 0; i < call->getNumArgs(); ++i) { - const clang::Expr *arg = call->getArg(i); - const core::LibraryParam *row = match ? match->param(i) : nullptr; - const bool rowWrites = - row != nullptr && - (row->access == core::LibraryParam::Access::Write || - row->access == core::LibraryParam::Access::ReadWrite); - const clang::Expr *stripped = withoutPointerCasts(arg); - if (const auto *address = llvm::dyn_cast(stripped); - address != nullptr && address->getOpcode() == clang::UO_AddrOf) - addressArguments[address] = - !body && (!match || rowWrites || (row != nullptr && row->out)) - ? std::optional("its address is passed to " + who) - : std::nullopt; - if (arg->getType()->isFunctionPointerType()) { - const SlotSources value = sources(arg); - if (through) - flow(value, core::SlotKey::callParam(*through, i)); - else if (i < callee->getNumParams()) - flow(value, core::SlotKey::param(name(*callee), i)); - else - escape(value); // a variadic argument reaches no parameter slot - } - // §9.3 closedness: objects handed to code the unit does not define. - if (!body && !match) - leak(arg->getType()); - if (rowWrites) { - // A zero fill leaves null, and a copy from the same type copies the - // fields' own values. - bool keeps = false; - for (const core::LibFill &fill : match->entry->fills) - keeps = - keeps || (match->callArgument(fill.dst) == static_cast(i) && - fill.value.kind == core::LibTerm::Kind::Constant && - fill.value.value == 0); - for (const core::LibCopy © : match->entry->copies) { - const int source = match->callArgument(copy.src); - if (match->callArgument(copy.dst) != static_cast(i) || - source < 0 || static_cast(source) >= call->getNumArgs()) - continue; - const clang::QualType from = - withoutPointerCasts(call->getArg(static_cast(source))) - ->getType(); - keeps = keeps || (from->isPointerType() && - stripped->getType()->isPointerType() && - clang::ASTContext::hasSameUnqualifiedType( - from->getPointeeType(), - stripped->getType()->getPointeeType())); - } - if (!keeps) - byteWrite(stripped, - "a byte-wise write by '" + match->entry->name + "'"); - } - } - if ((callee != nullptr && !body && !match) || callee == nullptr) - leak(call->getType()); - return true; -} - -bool SlotWalker::VisitDeclRefExpr(clang::DeclRefExpr *ref) { - if (const auto *function = - llvm::dyn_cast(ref->getDecl()); - function != nullptr && !callees.contains(ref)) - addressTaken.insert(function->getCanonicalDecl()); - return true; -} - -bool SlotWalker::VisitCastExpr(clang::CastExpr *cast) { - const clang::QualType to = cast->getType(); - const clang::Expr *sub = cast->getSubExpr(); - const clang::QualType from = sub->getType(); - // A function converted to a data pointer or an integer escapes (§9.3). - if ((from->isFunctionPointerType() || from->isFunctionType()) && - (cast->getCastKind() == clang::CK_PointerToIntegral || - (cast->getCastKind() == clang::CK_BitCast && - !to->isFunctionPointerType()))) - escape(sources(sub)); - // A record reached by converting another pointer may be any object, and - // one made from a byte buffer holds bytes in its function pointers. - if (cast->getCastKind() == clang::CK_BitCast && to->isPointerType() && - from->isPointerType()) { - const clang::QualType target = - to->getPointeeType().getCanonicalType().getUnqualifiedType(); - const clang::QualType source = - from->getPointeeType().getCanonicalType().getUnqualifiedType(); - if (target->getAsRecordDecl() != nullptr && target != source) { - if (!isAllocation(sub, library)) - leak(to); - if (source->isCharType()) - byteWrite(cast, "its object is converted from a byte buffer"); - } - } - return true; -} - -void SlotWalker::finish() { - // Values stored through a pointer the syntax does not name reach every - // address-taken slot, and whatever else it points to: they escape. - for (const SlotSources &value : unnamed) { - for (const core::SlotKey &slot : taken) - flow(value, slot); - escape(value); - } - - // Outside bytes in function-pointer fields: byte-wise writes, objects - // made from byte buffers, and stores to other members of a union. - const std::function - openFields = [&](const clang::RecordDecl *record, const std::string &why, - unsigned depth) { - if (record == nullptr || depth > 16) - return; - if (const clang::RecordDecl *definition = record->getDefinition()) - record = definition; - for (const clang::FieldDecl *field : record->fields()) { - clang::QualType type = field->getType(); - while (const clang::ArrayType *array = context.getAsArrayType(type)) - type = array->getElementType(); - if (type->isFunctionPointerType()) - out.all.addOpen(fieldSlot(*field), why); - else if (const clang::RecordDecl *nested = type->getAsRecordDecl()) - openFields(nested, why, depth + 1); - } - }; - for (const auto &[record, why] : written) - openFields(record, why, 0); - for (const auto &[record, stored] : unionStores) { - for (const clang::FieldDecl *field : record->fields()) { - if (!holdsFunctionPointers(field->getType(), context)) - continue; - for (const clang::FieldDecl *other : stored) { - if (other == field || clang::ASTContext::hasSameUnqualifiedType( - other->getType(), field->getType())) - continue; - out.all.addOpen(fieldSlot(*field), - "a store to '" + other->getNameAsString() + - "', another member of the union"); - break; - } - } - } - - // Functions code outside the unit may call: their parameters and results - // carry outside objects. - for (const auto &[own, function] : out.functions) { - if (!function->doesThisDeclarationHaveABody() || - (!function->isExternallyVisible() && - !addressTaken.contains(function->getCanonicalDecl()))) - continue; - for (const clang::ParmVarDecl *param : function->parameters()) - leak(param->getType()); - leak(function->getReturnType()); - } - - // §9.3: the fields of a record the main file defines are closed when no - // object of it comes from or goes to code outside the unit. - for (const clang::RecordDecl *record : records) { - if (!sm.isInMainFile(sm.getExpansionLoc(record->getLocation())) || - leaked.contains(record)) - continue; - if (llvm::any_of(record->fields(), [&](const clang::FieldDecl *field) { - return holdsFunctionPointers(field->getType(), context); - })) - out.unitRules.confinedRecords.insert( - SlotCollector::recordSlotKey(*record, context, out.unit())); - } -} - -SlotCollection SlotCollector::collect() { - SlotCollection out; - out.unitName = options.unit; - if (out.unitName.empty()) { - const clang::SourceManager &sm = context.getSourceManager(); - if (const auto entry = sm.getFileEntryRefForID(sm.getMainFileID())) - out.unitName = entry->getName().str(); - else - out.unitName = ""; - } - out.unitRules.scope = core::SlotScope::Unit; - SlotWalker walker(context, library, out); - walker.TraverseDecl(context.getTranslationUnitDecl()); - walker.finish(); - return out; -} - -// -- The dump -// ----------------------------------------------------------------------- - -static void dumpSlotLines(const SlotCollection &slots, - const core::SlotSolution &solution, - const clang::SourceManager &sm, - llvm::raw_ostream &os) { - os << "slot constraints:\n"; - for (const core::SlotConstraint &constraint : - slots.constraints().constraints()) { - switch (constraint.kind) { - case core::SlotConstraint::Kind::Member: - os << " member " << constraint.function << " -> " - << constraint.slot.toString() << "\n"; - break; - case core::SlotConstraint::Kind::Subset: - os << " subset " << constraint.from.toString() << " -> " - << constraint.slot.toString() << "\n"; - break; - case core::SlotConstraint::Kind::Open: - os << " open " << constraint.slot.toString() << " (" << constraint.detail - << ")\n"; - break; - } - } - os << "slot rules:"; - for (const std::string &record : slots.rules().confinedRecords) - os << " confined(" << record << ")"; - for (const std::string &name : slots.rules().escapedStatics) - os << " escaped(" << name << ")"; - os << "\nindirect calls:\n"; - for (const auto &[call, slot] : slots.indirectCalls()) { - const clang::PresumedLoc at = - sm.getPresumedLoc(sm.getExpansionLoc(call->getBeginLoc())); - const core::CallResolution resolution = solution.resolveCall(slot); - os << " " - << (at.isValid() ? std::to_string(at.getLine()) + ":" + - std::to_string(at.getColumn()) - : std::string("?")) - << ": through " << slot.toString() << ": " - << core::toString(resolution.kind); - if (!resolution.targets.empty()) { - os << " {"; - llvm::interleaveComma(resolution.targets, os); - os << "}"; - } - if (resolution.open) - os << " (" << resolution.open->detail << ")"; - os << "\n"; - } -} - -void dumpSlots(const SlotCollection &slots, const core::SlotSolution &solution, - clang::ASTContext &context, llvm::raw_ostream &os) { - std::string text; - llvm::raw_string_ostream lines(text); - dumpSlotLines(slots, solution, context.getSourceManager(), lines); - // The unit's own names read `:` here, for brevity. - const std::string prefix = slots.unit() + ":"; - const llvm::StringRef shown = ":"; - for (std::size_t at = text.find(prefix); at != std::string::npos; - at = text.find(prefix, at + shown.size())) - text.replace(at, prefix.size(), shown.str()); - os << text; -} - -} // namespace weavec::analysis diff --git a/lib/Analysis/SourceTerm.cpp b/lib/Analysis/SourceTerm.cpp deleted file mode 100644 index cc354f31..00000000 --- a/lib/Analysis/SourceTerm.cpp +++ /dev/null @@ -1,126 +0,0 @@ -//===- SourceTerm.cpp - Quantities spelled over C at a site ---------------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// - -#include "weavec/Analysis/SourceTerm.h" - -#include -#include - -namespace weavec::analysis { - -SourceTerm::Step SourceTerm::Step::member(std::string name) { - return Step{.kind = Kind::Member, .field = std::move(name)}; -} - -SourceTerm::Step SourceTerm::Step::deref() { - return Step{.kind = Kind::Deref}; -} - -SourceTerm SourceTerm::ofConstant(std::int64_t value) { - SourceTerm term; - term.kind = Kind::Constant; - term.constant = value; - return term; -} - -SourceTerm SourceTerm::ofPlace(const clang::ValueDecl &decl, - std::vector path) { - SourceTerm term; - term.kind = Kind::Place; - term.decl = &decl; - term.path = std::move(path); - return term; -} - -SourceTerm SourceTerm::ofExpr(const clang::Expr &expr) { - SourceTerm term; - term.kind = Kind::Expr; - term.expr = &expr; - return term; -} - -static SourceTerm binaryTerm(SourceTerm::Kind kind, SourceTerm lhs, - SourceTerm rhs) { - SourceTerm term; - term.kind = kind; - term.operands.push_back(std::move(lhs)); - term.operands.push_back(std::move(rhs)); - return term; -} - -SourceTerm SourceTerm::add(SourceTerm lhs, SourceTerm rhs) { - return binaryTerm(Kind::Add, std::move(lhs), std::move(rhs)); -} - -SourceTerm SourceTerm::sub(SourceTerm lhs, SourceTerm rhs) { - return binaryTerm(Kind::Sub, std::move(lhs), std::move(rhs)); -} - -SourceTerm SourceTerm::mul(SourceTerm lhs, SourceTerm rhs) { - return binaryTerm(Kind::Mul, std::move(lhs), std::move(rhs)); -} - -SourceTerm SourceTerm::div(SourceTerm lhs, std::int64_t divisor) { - return binaryTerm(Kind::Div, std::move(lhs), ofConstant(divisor)); -} - -SourceTerm SourceTerm::strLen(SourceTerm pointer) { - SourceTerm term; - term.kind = Kind::StrLen; - term.operands.push_back(std::move(pointer)); - return term; -} - -std::string SourceTerm::toString() const { - switch (kind) { - case Kind::Constant: - return std::to_string(constant); - case Kind::Place: { - std::string text = decl != nullptr ? decl->getNameAsString() : ""; - for (const Step &step : path) { - if (step.kind == Step::Kind::Deref) { - text.insert(0, "(*"); - text += ')'; - } else { - text += '.'; - text += step.field; - } - } - return text; - } - case Kind::Add: - case Kind::Sub: - case Kind::Mul: - case Kind::Div: { - if (operands.size() != 2) - return ""; - const char *op = " * "; - if (kind == Kind::Add) - op = " + "; - else if (kind == Kind::Sub) - op = " - "; - else if (kind == Kind::Div) - op = " / "; - std::string text = "(" + operands[0].toString(); - text += op; - text += operands[1].toString(); - text += ')'; - return text; - } - case Kind::StrLen: - return "strlen(" + - (operands.empty() ? std::string("") - : operands.front().toString()) + - ")"; - case Kind::Expr: - return ""; - } - return ""; -} - -} // namespace weavec::analysis diff --git a/lib/Analysis/UnitPipeline.cpp b/lib/Analysis/UnitPipeline.cpp deleted file mode 100644 index 5a35a4b8..00000000 --- a/lib/Analysis/UnitPipeline.cpp +++ /dev/null @@ -1,140 +0,0 @@ -//===- UnitPipeline.cpp - One unit's analysis (RFC 0030) ------------------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// - -#include "weavec/Analysis/UnitPipeline.h" - -#include "weavec/Analysis/BoundaryInvariants.h" -#include "weavec/Analysis/Concurrency.h" -#include "weavec/Analysis/KindInference.h" -#include "weavec/Analysis/KindTable.h" -#include "weavec/Analysis/ObjectEngine.h" -#include "weavec/Analysis/SiteCollector.h" - -#include "clang/Basic/SourceManager.h" - -#include -#include -#include -#include - -namespace weavec::analysis { - -/// The main source as the user named it. -static std::string mainSource(const clang::ASTContext &context) { - const clang::SourceManager &sm = context.getSourceManager(); - if (const auto entry = sm.getFileEntryRefForID(sm.getMainFileID())) - return entry->getName().str(); - return {}; -} - -UnitPipelineResult runUnitAnalysis(clang::ASTContext &context, - const UnitPipelineOptions &options, - core::DiagnosticSink &out) { - UnitPipelineResult result; - const core::LibrarySpec &library = core::LibrarySpec::shipped(); - if (options.discoverOnly) { - // RFC 0005 discovery: what the unit defines and calls, nothing more. - const SiteIndex noSites; - const KindTable noKinds; - const core::FnSlots noSlots; - result.exports = ObjectEngine::discover(EngineInput{ - .context = context, - .sites = noSites, - .kinds = noKinds, - .library = library, - .slots = noSlots, - .database = options.database, - .options = options.engine, - }); - return result; - } - - // §1 step 2: kinds, then sites, before any engine fact. Every round needs - // the kinds, which seed the engine's extents (§15 item 14), so that a - // silent round computes the summaries the authoritative one does; only a - // round that builds the ledger needs the sites. - std::shared_ptr kinds = UnitKinds::build(context, library); - auto sites = std::make_shared(); - if (options.buildLedger) - *sites = SiteCollector(context, kinds->table, library, &kinds->inferred, - &kinds->slots, &kinds->solution) - .collect(); - - LedgerAdapterOptions adapterOptions; - adapterOptions.source = mainSource(context); - // §5.3: G, what threads and signal handlers share, before the engine runs. - if (options.buildLedger) { - auto share = std::make_shared( - collectConcurrencyShare(context, library)); - if (!share->empty()) - adapterOptions.concurrent = [share](const SiteInfo &info) { - return info.operand != nullptr && rootedInShare(*info.operand, *share); - }; - } - // A silent round (no ledger) keeps nothing (§2.6). - LedgerAdapter adapter(context, *sites, std::move(adapterOptions), - options.buildLedger ? LedgerAdapter::Mode::Authoritative - : LedgerAdapter::Mode::Discarding); - - // *Diagnostics*: the malformed and conflicting declarations - // `AttributeReader` found (`invalid-annotation`), before the engine's. - for (core::Diagnostic &diagnostic : - kindProblemDiagnostics(kinds->table, context.getSourceManager())) - adapter.report(std::move(diagnostic), core::Certainty::Possible); - - const EngineInput input{ - .context = context, - .sites = *sites, - .kinds = kinds->table, - .library = library, - .slots = kinds->slots.constraints(), - .database = options.database, - .inferred = &kinds->inferred, - .slotCollection = &kinds->slots, - .slotSolution = &kinds->solution, - .options = options.engine, - }; - ObjectEngine engine; - engine.analyzeUnit(input, adapter); - result.exports = engine.exports(); - // `--dump-analysis`: the main file's functions, in source order. - if (llvm::raw_ostream *dump = options.engine.dumpStream) { - const clang::SourceManager &sm = context.getSourceManager(); - for (const clang::Decl *decl : context.getTranslationUnitDecl()->decls()) - if (const auto *fn = llvm::dyn_cast(decl); - fn != nullptr && fn->doesThisDeclarationHaveABody() && - sm.isInMainFile(sm.getExpansionLoc(fn->getLocation()))) - engine.dump(*fn, *dump); - } - - // §1 step 2, §9.4: the boundary invariants judge what the engine - // published, and `finish` records their rows and propagation. At link the - // other units' rows come along, so the propagation is program-wide - // (§13.2 step 5). - if (options.buildLedger) { - llvm::ArrayRef program; - if (options.database != nullptr && options.database->programFacts) - program = options.database->programFacts->boundaries; - BoundaryVerdicts verdicts = checkBoundaryInvariants( - *sites, adapter.boundaries(), program, adapter.reliances()); - result.exports.boundaries = std::move(verdicts.exported); - adapter.boundaryDecisions(std::move(verdicts.decisions)); - } - - core::Ledger ledger = adapter.finish(); - if (options.buildLedger) - result.ledger = std::make_shared(std::move(ledger)); - result.kinds = std::move(kinds); - - // §1 step 3: the diagnostics, in the order they were published. - for (const core::Diagnostic &diagnostic : adapter.diagnostics()) - out.report(diagnostic); - return result; -} - -} // namespace weavec::analysis diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 802f13ce..ed9b8dcc 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -1,7 +1,6 @@ # Library components, ordered by dependency: -# Core -- Clang/LLVM-independent ownership model -# Analysis -- Clang AST -> core facts; the checkers -# Frontend -- Clang FrontendAction / tooling integration +# Core -- the C library table of the guards (no Clang or LLVM) +# Frontend -- the guard passes, the unsafe regions, the enforcement ledger +# and the compiler driver add_subdirectory(Core) -add_subdirectory(Analysis) add_subdirectory(Frontend) diff --git a/lib/Core/CMakeLists.txt b/lib/Core/CMakeLists.txt index 9e04d35b..ecca87a5 100644 --- a/lib/Core/CMakeLists.txt +++ b/lib/Core/CMakeLists.txt @@ -1,23 +1,9 @@ -# weavec::Core -- the frontend-independent ownership/borrow/lifetime model. +# weavec::Core -- the C library table the guards read (RFC 0036 §6). # -# This library MUST NOT depend on LLVM or Clang. Keeping it standard-library -# only is what allows the model to be reused outside the Clang integration. -weavec_add_library( - Core - SOURCES Diagnostic.cpp - Effects.cpp - FnSlots.cpp - Heap.cpp - Integer.cpp - Ledger.cpp - LibrarySpec.cpp - Path.cpp - PointerKind.cpp - Scc.cpp - SourceLocation.cpp - Zone.cpp) +# This library MUST NOT depend on LLVM or Clang. +weavec_add_library(Core SOURCES LibrarySpec.cpp) -# RFC 0030 §8: the library table is embedded in the library as a byte array. +# RFC 0036 §6: the library table is embedded in the library as a byte array. # Generated at configure time; CMAKE_CONFIGURE_DEPENDS re-runs the configure # step (and file(CONFIGURE) rewrites the .inc only when its content changes) # whenever LibrarySpec.txt is edited, so LibrarySpec.cpp rebuilds. diff --git a/lib/Core/Diagnostic.cpp b/lib/Core/Diagnostic.cpp deleted file mode 100644 index c7c0013b..00000000 --- a/lib/Core/Diagnostic.cpp +++ /dev/null @@ -1,33 +0,0 @@ -//===- Diagnostic.cpp - Frontend-neutral diagnostics ----------------------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// - -#include "weavec/Core/Diagnostic.h" - -#include -#include - -namespace weavec::core { - -Diagnostic &Diagnostic::addNote(std::string noteMessage, - SourceLocation noteLocation) { - notes.push_back(Diagnostic{ - .severity = Severity::Note, - .id = id, - .message = std::move(noteMessage), - .location = std::move(noteLocation), - .notes = {}, - .fixits = {}, - }); - return *this; -} - -void DiagnosticCollector::report(const Diagnostic &diagnostic) { - items.push_back(diagnostic); -} - -} // namespace weavec::core diff --git a/lib/Core/Effects.cpp b/lib/Core/Effects.cpp deleted file mode 100644 index 43d7695e..00000000 --- a/lib/Core/Effects.cpp +++ /dev/null @@ -1,601 +0,0 @@ -//===- Effects.cpp - Function summaries (RFC 0031) ------------------------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// - -#include "weavec/Core/Effects.h" - -#include -#include -#include -#include -#include -#include - -namespace weavec::core { - -/// A path with its root spelled: `paramN`, `globalN` or `result`. -static std::string pathText(const SummaryPath &path) { - switch (path.root) { - case SummaryRoot::Param: - return path.toString("param" + std::to_string(path.index)); - case SummaryRoot::Global: - return path.toString("global" + std::to_string(path.index)); - case SummaryRoot::Result: - return path.toString("result"); - } - return path.toString("?"); -} - -std::string_view toString(ResultClass value) noexcept { - switch (value) { - case ResultClass::Null: - return "null"; - case ResultClass::NonNull: - return "nonnull"; - case ResultClass::Zero: - return "zero"; - case ResultClass::Positive: - return "positive"; - case ResultClass::Negative: - return "negative"; - } - return "?"; -} - -/// `null|nonnull`, for the store text. -static std::string classesText(const std::vector &classes) { - std::string out; - for (std::size_t i = 0; i < classes.size(); ++i) - out += (i == 0 ? "" : "|") + std::string(core::toString(classes[i])); - return out; -} - -std::string EffectCase::toString() const { - if (always()) - return "always"; - std::string out; - if (!classes.empty()) { - out = "result"; - for (std::size_t i = 0; i < classes.size(); ++i) { - out += i == 0 ? " " : "|"; - out += core::toString(classes[i]); - } - } - if (paramZero) - out += std::string(out.empty() ? "" : " and ") + "param " + - std::to_string(paramZero->first) + - (paramZero->second ? " =0" : " !=0"); - if (entryZero) - out += std::string(out.empty() ? "" : " and ") + "entry " + - pathText(entryZero->first) + (entryZero->second ? " =0" : " !=0"); - if (paramsEqual) - out += std::string(out.empty() ? "" : " and ") + "param " + - std::to_string(paramsEqual->first) + - (paramsEqual->equal ? " == " : " != ") + "param " + - std::to_string(paramsEqual->second); - return out; -} - -std::string PathTerm::toString() const { - if (!path) - return std::to_string(constant); - std::string out = pathText(*path); - if (scale != 1) - out += " scale " + std::to_string(scale); - if (constant != 0) - out += " plus " + std::to_string(constant); - return out; -} - -std::string ElementRange::toString() const { - return "[" + from.toString() + ", " + to.toString() + ")"; -} - -std::string ValueDesc::toString() const { - switch (kind) { - case Kind::Null: - return "null"; - case Kind::Fresh: - return "fresh#" + std::to_string(object) + " " + - (family.empty() ? std::string("free") : family) + - (extent ? " extent " + extent->toString() : std::string()) + - (zeroed ? " zeroed" : "") + (many ? " many" : "") + - (maybeNull ? " maybe-null" : "") + (interior ? " interior" : "") + - (offset && *offset != 0 ? " offset " + std::to_string(*offset) - : std::string()); - case Kind::Path: - return "path " + (path ? pathText(*path) : std::string("?")) + - (offset && *offset != 0 ? " offset " + std::to_string(*offset) - : std::string()) + - (maybeNull ? " maybe-null" : ""); - case Kind::Static: - return "static"; - case Kind::Int: - return "int [" + (lo ? std::to_string(*lo) : std::string("-inf")) + ", " + - (hi ? std::to_string(*hi) : std::string("inf")) + "]" + - (range ? " " + range->toString() : std::string()); - case Kind::Dangling: - return "dangling"; - case Kind::Unknown: - if (!raw) - return "unknown"; - return rawSome ? "unknown raw-some" : "unknown raw"; - case Kind::Function: { - std::string out = "function"; - for (std::size_t i = 0; i < functions.size(); ++i) - out += (i == 0 ? " " : ",") + functions[i]; - return out; - } - } - return "?"; -} - -std::string toText(const FunctionEffects &effects) { - std::string out; - switch (effects.returns) { - case FunctionEffects::Returns::Always: - out += " always-returns\n"; - break; - case FunctionEffects::Returns::May: - out += " may-not-return\n"; - break; - case FunctionEffects::Returns::Never: - out += " never-returns\n"; - break; - } - if (effects.incomplete) - out += " incomplete " + *effects.incomplete + "\n"; - if (effects.unknownGlobals) - out += " unknown globals\n"; - for (const ResultEffect &result : effects.results) { - out += " result " + result.value.toString(); - if (result.value.maybeNull && result.value.kind != ValueDesc::Kind::Fresh) - out += " maybe-null"; - out += " when"; - for (ResultClass c : result.classes) - out += " " + std::string(toString(c)); - if (result.paramZero) - out += " and param " + std::to_string(result.paramZero->first) + - (result.paramZero->second ? " =0" : " !=0"); - out += '\n'; - } - for (const PathEffect &effect : effects.effects) { - static constexpr std::array Names = { - "release", "move", "unknown", "escape", "share -1", "share +1"}; - out += " " + std::string(Names.at(static_cast(effect.kind))) + - " " + pathText(effect.path); - if (effect.elements) - out += " elements " + effect.elements->toString(); - if (!effect.family.empty()) - out += " " + effect.family; - if (effect.anyOffset) - out += " offset unknown"; - else if (effect.offset != 0) - out += " offset " + std::to_string(effect.offset); - if (effect.lossy) - out += " lossy"; - if (effect.may) - out += " may"; - out += " when " + effect.when.toString() + "\n"; - } - for (const StoreEffect &store : effects.stores) - out += - " store " + std::string(store.contents ? "(new) " : "") + - pathText(store.dest) + - (store.elements ? " elements " + store.elements->toString() - : std::string()) + - (store.bytes ? " bytes " + std::to_string(store.bytes->first) + ".." + - std::to_string(store.bytes->second) - : std::string()) + - " := " + store.value.toString() + (store.may ? " may" : "") + - (store.when.always() ? std::string() - : " when " + store.when.toString()) + - (store.absentOn.empty() ? std::string() - : " absent on " + classesText(store.absentOn)) + - "\n"; - for (const auto &[resultClass, paths] : effects.nonNullOn) - for (const SummaryPath &path : paths) - out += " nonnull-on " + std::string(toString(resultClass)) + " " + - pathText(path) + "\n"; - for (const StringEffect &string : effects.strings) - out += " string " + std::string(string.contents ? "(new) " : "") + - pathText(string.path) + " nul-within " + - string.nulWithin.toString() + - (string.nulFrom ? " from " + string.nulFrom->toString() - : std::string()) + - "\n"; - return out; -} - -//===----------------------------------------------------------------------===// -// Join -//===----------------------------------------------------------------------===// - -FunctionEffects joinEffects(const FunctionEffects &left, - const FunctionEffects &rightIn) { - // The two sides number their new objects separately: the right side's - // come after the left's. - std::uint32_t shift = 0; - auto noteObject = [&](const ValueDesc &value) { - if (value.kind == ValueDesc::Kind::Fresh) - shift = std::max(shift, value.object + 1); - }; - for (const ResultEffect &result : left.results) - noteObject(result.value); - for (const StoreEffect &store : left.stores) - noteObject(store.value); - FunctionEffects right = rightIn; - for (ResultEffect &result : right.results) - if (result.value.kind == ValueDesc::Kind::Fresh) - result.value.object += shift; - for (StoreEffect &store : right.stores) - if (store.value.kind == ValueDesc::Kind::Fresh) - store.value.object += shift; - - FunctionEffects out; - out.returns = left.returns == right.returns ? left.returns - : FunctionEffects::Returns::May; - out.incomplete = left.incomplete ? left.incomplete : right.incomplete; - out.unknownGlobals = left.unknownGlobals || right.unknownGlobals; - // Effects: on both sides as is (possible if possible on either); on one - // side only, possible. - auto sameEffect = [](const PathEffect &a, const PathEffect &b) { - return a.kind == b.kind && a.path == b.path && a.family == b.family && - a.when == b.when && a.offset == b.offset && - a.anyOffset == b.anyOffset && a.elements == b.elements; - }; - for (const PathEffect &effect : left.effects) { - PathEffect joined = effect; - auto other = - std::ranges::find_if(right.effects, [&](const PathEffect &candidate) { - return sameEffect(effect, candidate); - }); - if (other == right.effects.end()) { - joined.may = true; - } else { - joined.may = effect.may || other->may; - joined.lossy = effect.lossy || other->lossy; - } - out.effects.push_back(std::move(joined)); - } - for (const PathEffect &effect : right.effects) - if (std::ranges::none_of(left.effects, [&](const PathEffect &candidate) { - return sameEffect(effect, candidate); - })) { - PathEffect joined = effect; - joined.may = true; - out.effects.push_back(std::move(joined)); - } - // Stores: the same place on both sides keeps a value both agree on. - auto samePlace = [](const StoreEffect &a, const StoreEffect &b) { - return a.dest == b.dest && a.elements == b.elements && - a.contents == b.contents && a.bytes == b.bytes; - }; - for (const StoreEffect &store : left.stores) { - StoreEffect joined = store; - auto other = - std::ranges::find_if(right.stores, [&](const StoreEffect &candidate) { - return samePlace(store, candidate); - }); - if (other == right.stores.end()) { - joined.may = true; - } else { - joined.may = store.may || other->may; - if (!(store.value == other->value) || !(store.when == other->when)) { - joined.value = ValueDesc{}; - joined.when = EffectCase{}; - } - } - out.stores.push_back(std::move(joined)); - } - for (const StoreEffect &store : right.stores) - if (std::ranges::none_of(left.stores, [&](const StoreEffect &candidate) { - return samePlace(store, candidate); - })) { - StoreEffect joined = store; - joined.may = true; - out.stores.push_back(std::move(joined)); - } - // Results: the alternatives of both. - // A result alternative is kept once: the same up to which new object it - // is (the right side's objects are renumbered after the left's, so a - // widening join would otherwise add the same allocation each round), when - // no store names that object. - auto named = [&](const FunctionEffects &effects, std::uint32_t object) { - return std::ranges::any_of(effects.stores, [&](const StoreEffect &store) { - return store.value.kind == ValueDesc::Kind::Fresh && - store.value.object == object; - }); - }; - auto sameUpToObject = [](const ResultEffect &a, const ResultEffect &b) { - if (a.value.kind != ValueDesc::Kind::Fresh || - b.value.kind != ValueDesc::Kind::Fresh) - return a == b; - ResultEffect x = a; - ResultEffect y = b; - x.value.object = y.value.object = 0; - return x == y; - }; - out.results.clear(); - for (const ResultEffect &result : left.results) - if (std::ranges::none_of(out.results, [&](const ResultEffect &kept) { - return sameUpToObject(kept, result) && - (kept == result || !named(left, result.value.object)); - })) - out.results.push_back(result); - for (const ResultEffect &result : right.results) - if (std::ranges::none_of(out.results, [&](const ResultEffect &kept) { - return kept == result || (sameUpToObject(kept, result) && - !named(right, result.value.object)); - })) - out.results.push_back(result); - // A guarantee both give. - for (const auto &[resultClass, paths] : left.nonNullOn) { - auto other = right.nonNullOn.find(resultClass); - if (other == right.nonNullOn.end()) - continue; - for (const SummaryPath &path : paths) - if (std::ranges::find(other->second, path) != other->second.end()) - out.nonNullOn[resultClass].push_back(path); - } - auto unite = [](std::vector a, - const std::vector &b) { - for (const SummaryPath &path : b) - if (std::ranges::find(a, path) == a.end()) - a.push_back(path); - return a; - }; - out.reads = unite(left.reads, right.reads); - out.writes = unite(left.writes, right.writes); - // A string fact both sides give. - for (const StringEffect &string : left.strings) - if (std::ranges::find(right.strings, string) != right.strings.end()) - out.strings.push_back(string); - return out; -} - -//===----------------------------------------------------------------------===// -// Renumbering -//===----------------------------------------------------------------------===// - -FunctionEffects widenEffects(const FunctionEffects &previous, - const FunctionEffects &next) { - // An unknown effect on every case covers every object reachable from its - // path (the caller havocs them, `Transfer::instantiate`): what else - // either side says may happen below it is folded into it before the - // join, or a recursion over a tree names each deeper path a round later. - std::vector covering; - for (const FunctionEffects *side : {&previous, &next}) - for (const PathEffect &effect : side->effects) - if (effect.kind == PathEffect::Kind::Unknown && effect.when.always() && - !effect.elements) - covering.push_back(effect.path); - auto covered = [&](const SummaryPath &path, bool orEqual) { - return std::ranges::any_of(covering, [&](const SummaryPath &cover) { - return cover.isProperPrefixOf(path) || (orEqual && cover == path); - }); - }; - auto fold = [&](FunctionEffects effects) { - std::erase_if(effects.effects, [&](const PathEffect &effect) { - bool foldable = effect.kind == PathEffect::Kind::Unknown || - ((effect.kind == PathEffect::Kind::Release || - effect.kind == PathEffect::Kind::Move) && - effect.may); - return foldable && covered(effect.path, false); - }); - // (A store below one: the caller forgets it with the rest.) - std::erase_if(effects.stores, [&](const StoreEffect &store) { - return !store.value.raw && covered(store.dest, true); - }); - return effects; - }; - FunctionEffects out = joinEffects(fold(previous), fold(next)); - // The integer alternatives of one case, as one hull. - using Case = std::pair, - std::optional>>; - auto hulls = [](const std::vector &results) { - std::map out; - for (const ResultEffect &result : results) { - if (result.value.kind != ValueDesc::Kind::Int) - continue; - Case key{result.classes, result.paramZero}; - auto [it, inserted] = out.try_emplace(key, result.value); - if (inserted) - continue; - ValueDesc &hull = it->second; - hull.lo = hull.lo && result.value.lo - ? std::optional(std::min(*hull.lo, *result.value.lo)) - : std::nullopt; - hull.hi = hull.hi && result.value.hi - ? std::optional(std::max(*hull.hi, *result.value.hi)) - : std::nullopt; - if (!(hull.range == result.value.range)) - hull.range.reset(); - if (!(hull.path == result.value.path)) - hull.path.reset(); - } - return out; - }; - const std::map before = hulls(previous.results); - std::map after = hulls(out.results); - for (auto &[key, hull] : after) { - auto old = before.find(key); - if (old == before.end()) - continue; - // (A bound that moved since the last round does not stop moving.) - if (!old->second.lo || !hull.lo || *hull.lo < *old->second.lo) - hull.lo.reset(); - if (!old->second.hi || !hull.hi || *hull.hi > *old->second.hi) - hull.hi.reset(); - if (!(old->second.range == hull.range)) - hull.range.reset(); - } - std::vector results; - std::set placed; - for (const ResultEffect &result : out.results) { - if (result.value.kind != ValueDesc::Kind::Int) { - results.push_back(result); - continue; - } - Case key{result.classes, result.paramZero}; - if (!placed.insert(key).second) - continue; - ResultEffect merged = result; - merged.value = after.at(key); - results.push_back(std::move(merged)); - } - out.results = std::move(results); - // New objects numbered by first appearance, so two rounds that differ - // only by how the join numbered them compare equal. - std::map number; - auto renumber = [&](ValueDesc &value) { - if (value.kind != ValueDesc::Kind::Fresh) - return; - auto [it, inserted] = number.try_emplace( - value.object, static_cast(number.size())); - value.object = it->second; - }; - for (ResultEffect &result : out.results) - renumber(result.value); - for (StoreEffect &store : out.stores) - renumber(store.value); - return out; -} - -FunctionEffects renumberGlobals(const FunctionEffects &effects, - const GlobalRenumbering &map) { - // A path with its global root renumbered, or none. - auto path = [&](const SummaryPath &in) -> std::optional { - if (in.root != SummaryRoot::Global) - return in; - auto id = map(in.index); - if (!id) - return std::nullopt; - SummaryPath out = in; - out.index = *id; - return out; - }; - auto term = [&](const PathTerm &in) -> std::optional { - if (!in.path) - return in; - auto renamed = path(*in.path); - if (!renamed) - return std::nullopt; - PathTerm out = in; - out.path = std::move(renamed); - return out; - }; - auto range = [&](const std::optional &in, - bool &ok) -> std::optional { - if (!in) - return in; - auto from = term(in->from); - auto to = term(in->to); - if (!from || !to) { - ok = false; - return std::nullopt; - } - return ElementRange{.from = std::move(*from), .to = std::move(*to)}; - }; - auto value = [&](const ValueDesc &in) { - ValueDesc out = in; - if (in.path) { - out.path = path(*in.path); - if (!out.path) - return ValueDesc{}; - } - if (in.extent) - out.extent = term(*in.extent); - return out; - }; - // A case's entry test through a global the unit cannot name: dropped (the - // effect becomes a possible one). - auto when = [&](EffectCase in, bool &may) { - if (in.entryZero) { - auto renamed = path(in.entryZero->first); - if (renamed) { - in.entryZero->first = std::move(*renamed); - } else { - in.entryZero.reset(); - may = true; - } - } - return in; - }; - FunctionEffects out; - out.returns = effects.returns; - out.incomplete = effects.incomplete; - out.unknownGlobals = effects.unknownGlobals; - for (const PathEffect &effect : effects.effects) { - auto renamed = path(effect.path); - bool ok = true; - auto elements = range(effect.elements, ok); - if (!renamed || !ok) { - if (!out.incomplete) - out.incomplete = "an effect through a global this unit does not " - "declare"; - continue; - } - PathEffect copy = effect; - copy.path = std::move(*renamed); - copy.elements = std::move(elements); - copy.when = when(copy.when, copy.may); - out.effects.push_back(std::move(copy)); - } - for (const StoreEffect &store : effects.stores) { - auto renamed = path(store.dest); - bool ok = true; - auto elements = range(store.elements, ok); - // A store into a global this unit cannot name still writes it: the - // unit that can learns so from the summaries that call this one. - if (!renamed || !ok) { - out.unknownGlobals = true; - continue; - } - StoreEffect copy = store; - copy.dest = std::move(*renamed); - copy.elements = std::move(elements); - copy.value = value(store.value); - copy.when = when(copy.when, copy.may); - out.stores.push_back(std::move(copy)); - } - for (const ResultEffect &result : effects.results) { - ResultEffect copy = result; - copy.value = value(result.value); - out.results.push_back(std::move(copy)); - } - for (const auto &[resultClass, paths] : effects.nonNullOn) - for (const SummaryPath &in : paths) - if (auto renamed = path(in)) - out.nonNullOn[resultClass].push_back(std::move(*renamed)); - for (const StringEffect &string : effects.strings) { - auto renamed = path(string.path); - auto within = term(string.nulWithin); - std::optional from; - if (string.nulFrom) { - from = term(*string.nulFrom); - if (!from) - continue; - } - if (!renamed || !within) - continue; - StringEffect copy = string; - copy.path = std::move(*renamed); - copy.nulWithin = std::move(*within); - copy.nulFrom = std::move(from); - out.strings.push_back(std::move(copy)); - } - for (const SummaryPath &in : effects.reads) - if (auto renamed = path(in)) - out.reads.push_back(std::move(*renamed)); - for (const SummaryPath &in : effects.writes) - if (auto renamed = path(in)) - out.writes.push_back(std::move(*renamed)); - return out; -} - -} // namespace weavec::core diff --git a/lib/Core/FnSlots.cpp b/lib/Core/FnSlots.cpp deleted file mode 100644 index 2b2e728f..00000000 --- a/lib/Core/FnSlots.cpp +++ /dev/null @@ -1,790 +0,0 @@ -//===- FnSlots.cpp - Function-pointer slots (RFC 0030) --------------------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// - -#include "weavec/Core/FnSlots.h" - -#include -#include -#include - -namespace weavec::core { - -//===----------------------------------------------------------------------===// -// Keys and constraints -//===----------------------------------------------------------------------===// - -SlotKey SlotKey::field(std::string record, std::string field) { - return SlotKey{.kind = SlotKind::Field, - .scope = std::move(record), - .name = std::move(field)}; -} - -SlotKey SlotKey::global(std::string name) { - return SlotKey{.kind = SlotKind::Global, .name = std::move(name)}; -} - -SlotKey SlotKey::staticGlobal(std::string unit, std::string name) { - return SlotKey{.kind = SlotKind::Static, - .scope = std::move(unit), - .name = std::move(name)}; -} - -SlotKey SlotKey::param(std::string function, std::uint32_t index) { - return SlotKey{ - .kind = SlotKind::Param, .scope = std::move(function), .index = index}; -} - -SlotKey SlotKey::result(std::string function) { - return SlotKey{.kind = SlotKind::Result, .scope = std::move(function)}; -} - -SlotKey SlotKey::local(std::string function, std::string name) { - return SlotKey{.kind = SlotKind::Local, - .scope = std::move(function), - .name = std::move(name)}; -} - -SlotKey SlotKey::callParam(const SlotKey &callee, std::uint32_t index) { - return SlotKey{ - .kind = SlotKind::CallParam, .scope = callee.toString(), .index = index}; -} - -SlotKey SlotKey::callResult(const SlotKey &callee) { - return SlotKey{.kind = SlotKind::CallResult, .scope = callee.toString()}; -} - -std::string SlotKey::toString() const { - switch (kind) { - case SlotKind::Field: - return "field " + scope + " " + name; - case SlotKind::Global: - return "global " + name; - case SlotKind::Static: - return "static " + scope + ":" + name; - case SlotKind::Param: - return "param " + scope + " " + std::to_string(index); - case SlotKind::Result: - return "result " + scope; - case SlotKind::Local: - return "local " + scope + " " + name; - case SlotKind::CallParam: - return "call-param " + std::to_string(index) + " " + scope; - case SlotKind::CallResult: - return "call-result " + scope; - } - return ""; -} - -static bool consumePrefix(std::string_view &text, std::string_view prefix) { - if (!text.starts_with(prefix)) - return false; - text.remove_prefix(prefix.size()); - return true; -} - -static bool parseIndex(std::string_view text, std::uint32_t &value) { - if (text.empty()) - return false; - const auto [end, ec] = - std::from_chars(text.data(), text.data() + text.size(), value); - return ec == std::errc() && end == text.data() + text.size(); -} - -/// Splits `text` at its last occurrence of `separator` into two non-empty -/// parts. -static std::optional> -splitLast(std::string_view text, char separator) { - const std::size_t at = text.rfind(separator); - if (at == std::string_view::npos || at == 0 || at + 1 == text.size()) - return std::nullopt; - return std::pair{text.substr(0, at), text.substr(at + 1)}; -} - -std::optional SlotKey::parse(std::string_view text) { - if (consumePrefix(text, "field ")) { - const auto parts = splitLast(text, ' '); - if (!parts) - return std::nullopt; - return field(std::string(parts->first), std::string(parts->second)); - } - if (consumePrefix(text, "global ")) { - if (text.empty()) - return std::nullopt; - return global(std::string(text)); - } - if (consumePrefix(text, "static ")) { - const auto parts = splitLast(text, ':'); - if (!parts) - return std::nullopt; - return staticGlobal(std::string(parts->first), std::string(parts->second)); - } - if (consumePrefix(text, "param ")) { - const auto parts = splitLast(text, ' '); - std::uint32_t index = 0; - if (!parts || !parseIndex(parts->second, index)) - return std::nullopt; - return param(std::string(parts->first), index); - } - if (consumePrefix(text, "result ")) { - if (text.empty()) - return std::nullopt; - return result(std::string(text)); - } - if (consumePrefix(text, "local ")) { - const auto parts = splitLast(text, ' '); - if (!parts) - return std::nullopt; - return local(std::string(parts->first), std::string(parts->second)); - } - if (consumePrefix(text, "call-param ")) { - const std::size_t space = text.find(' '); - std::uint32_t index = 0; - if (space == std::string_view::npos || - !parseIndex(text.substr(0, space), index)) - return std::nullopt; - const auto callee = parse(text.substr(space + 1)); - if (!callee) - return std::nullopt; - return callParam(*callee, index); - } - if (consumePrefix(text, "call-result ")) { - const auto callee = parse(text); - if (!callee) - return std::nullopt; - return callResult(*callee); - } - return std::nullopt; -} - -std::optional SlotKey::callee() const { - if (kind != SlotKind::CallParam && kind != SlotKind::CallResult) - return std::nullopt; - return parse(scope); -} - -bool SlotKey::isLocal() const { - switch (kind) { - case SlotKind::Local: - return true; - case SlotKind::CallParam: - case SlotKind::CallResult: { - const auto calleeKey = callee(); - return calleeKey && calleeKey->isLocal(); - } - case SlotKind::Field: - case SlotKind::Global: - case SlotKind::Static: - case SlotKind::Param: - case SlotKind::Result: - return false; - } - return false; -} - -SlotConstraint SlotConstraint::member(std::string function, SlotKey slot) { - return SlotConstraint{.kind = Kind::Member, - .slot = std::move(slot), - .function = std::move(function)}; -} - -SlotConstraint SlotConstraint::subset(SlotKey from, SlotKey to) { - return SlotConstraint{ - .kind = Kind::Subset, .slot = std::move(to), .from = std::move(from)}; -} - -SlotConstraint SlotConstraint::open(SlotKey slot, std::string detail) { - return SlotConstraint{ - .kind = Kind::Open, .slot = std::move(slot), .detail = std::move(detail)}; -} - -std::string_view toString(IndirectCallKind kind) noexcept { - switch (kind) { - case IndirectCallKind::ClosedEmpty: - return "closed-empty"; - case IndirectCallKind::ClosedSingle: - return "closed-single"; - case IndirectCallKind::ClosedJoin: - return "closed-join"; - case IndirectCallKind::OpenKnown: - return "open-known"; - case IndirectCallKind::OpenUnknown: - return "open-unknown"; - } - return ""; -} - -std::optional -openCallTemporalDecision(const CallResolution &resolution) { - std::string detail = resolution.open ? resolution.open->detail : ""; - switch (resolution.kind) { - case IndirectCallKind::OpenKnown: - return FacetDecision::trustedFor(TrustReason::ExternContract, - std::move(detail)); - case IndirectCallKind::OpenUnknown: - return FacetDecision::unresolvedFor(UnresolvedReason::Callback, - std::move(detail)); - case IndirectCallKind::ClosedEmpty: - case IndirectCallKind::ClosedSingle: - case IndirectCallKind::ClosedJoin: - return std::nullopt; - } - return std::nullopt; -} - -//===----------------------------------------------------------------------===// -// The seed rules -//===----------------------------------------------------------------------===// - -namespace { -/// What the closed-slot rules say about one slot on its own. -struct Seed { - std::optional open; - /// Values in the slot are visible to code outside the solved program. - bool escaping = false; -}; -} // namespace - -static std::string_view outsideCode(const SlotRules &rules) { - return rules.scope == SlotScope::Unit ? "code outside this unit" - : "code outside the analysed program"; -} - -static Seed seedOf(const SlotKey &key, const SlotRules &rules) { - const bool closedWorld = rules.closedWorld(); - Seed seed; - switch (key.kind) { - case SlotKind::Field: - if (!closedWorld && !rules.confinedRecords.contains(key.scope)) { - seed.open = OpenSource{.seed = key, - .detail = "'" + key.scope + "." + key.name + - "' may be stored by " + - std::string(outsideCode(rules))}; - seed.escaping = true; - } - return seed; - case SlotKind::Global: - if (!closedWorld) { - seed.open = OpenSource{.seed = key, - .detail = "'" + key.name + "' may be stored by " + - std::string(outsideCode(rules))}; - seed.escaping = true; - } - return seed; - case SlotKind::Static: - if (rules.escapedStatics.contains(key.staticName())) { - seed.open = OpenSource{ - .seed = key, .detail = "the address of '" + key.name + "' escapes"}; - seed.escaping = true; - } - return seed; - case SlotKind::Param: - if (!closedWorld && rules.exported.contains(key.scope)) - seed.open = - OpenSource{.seed = key, - .detail = "values stored by '" + key.scope + - "' parameter " + std::to_string(key.index)}; - // Passing a value to a function without a body hands it to unknown - // code. - seed.escaping = !rules.defined.contains(key.scope); - return seed; - case SlotKind::Result: - if (!rules.defined.contains(key.scope)) - seed.open = OpenSource{ - .seed = key, - .detail = key.scope == UnknownFunction - ? std::string("the result of an unknown callee") - : "the result of '" + key.scope + - "', which has no body here"}; - seed.escaping = !closedWorld && rules.exported.contains(key.scope); - return seed; - case SlotKind::Local: - case SlotKind::CallParam: - case SlotKind::CallResult: - return seed; - } - return seed; -} - -/// The open source of a parameter of `f` once `f`'s address reached an open -/// position. -static OpenSource escapedSource(const SlotKey ¶m, const SlotRules &rules) { - return OpenSource{.seed = param, - .detail = "'" + param.scope + "' may be called by " + - std::string(outsideCode(rules))}; -} - -//===----------------------------------------------------------------------===// -// The solver -//===----------------------------------------------------------------------===// - -namespace { -struct SolverNode { - SlotKey key; - std::set targets; - std::optional open; - /// Inclusion edges `this ⊆ out[i]`. - std::vector out; - /// Call keys whose callee is this node. - std::vector callParams; - std::vector callResults; - /// Call keys: the callee's node. - std::optional callee; - bool escapingSeed = false; -}; - -/// A worklist over nodes; a node is processed when its targets or its -/// openness grew, and pushes what it reaches. -class Solver { -public: - explicit Solver(const SlotRules &rules) : rules(rules) {} - - void load(const std::set &constraints); - void run(); - - std::vector nodes; - std::set> escaped; - std::size_t steps = 0; - -private: - std::size_t intern(const SlotKey &key); - bool addEdge(std::size_t from, std::size_t to); - void push(std::size_t node); - void markEscaped(const std::string &function); - void process(std::size_t node); - - const SlotRules &rules; - std::map ids; - std::set> edges; - std::map, std::less<>> paramNodes; - std::deque work; - std::vector queued; -}; -} // namespace - -std::size_t Solver::intern(const SlotKey &key) { - if (const auto found = ids.find(key); found != ids.end()) - return found->second; - const std::size_t id = nodes.size(); - ids.emplace(key, id); - const Seed seed = seedOf(key, rules); - nodes.push_back(SolverNode{.key = key, - .targets = {}, - .open = seed.open, - .out = {}, - .callParams = {}, - .callResults = {}, - .callee = std::nullopt, - .escapingSeed = seed.escaping}); - queued.push_back(false); - if (key.kind == SlotKind::Param) { - paramNodes[key.scope].push_back(id); - if (!nodes[id].open && escaped.contains(key.scope)) - nodes[id].open = escapedSource(key, rules); - } - if (const auto calleeKey = key.callee()) { - const std::size_t callee = intern(*calleeKey); - nodes[id].callee = callee; - if (key.kind == SlotKind::CallParam) - nodes[callee].callParams.push_back(id); - else - nodes[callee].callResults.push_back(id); - // The callee connects the new call key to its targets. - push(callee); - } - push(id); - return id; -} - -bool Solver::addEdge(std::size_t from, std::size_t to) { - if (!edges.emplace(from, to).second) - return false; - nodes[from].out.push_back(to); - return true; -} - -void Solver::push(std::size_t node) { - if (queued[node]) - return; - queued[node] = true; - work.push_back(node); -} - -void Solver::markEscaped(const std::string &function) { - if (!escaped.insert(function).second) - return; - const auto found = paramNodes.find(function); - if (found == paramNodes.end()) - return; - for (const std::size_t param : found->second) { - if (!nodes[param].open) { - nodes[param].open = escapedSource(nodes[param].key, rules); - push(param); - } - } -} - -void Solver::load(const std::set &constraints) { - for (const SlotConstraint &constraint : constraints) { - switch (constraint.kind) { - case SlotConstraint::Kind::Member: - nodes[intern(constraint.slot)].targets.insert(constraint.function); - break; - case SlotConstraint::Kind::Subset: { - const std::size_t from = intern(constraint.from); - const std::size_t to = intern(constraint.slot); - addEdge(from, to); - break; - } - case SlotConstraint::Kind::Open: - // An explicit source names the open value better than a rule does. - nodes[intern(constraint.slot)].open = - OpenSource{.seed = constraint.slot, .detail = constraint.detail}; - break; - } - } -} - -void Solver::process(std::size_t node) { - ++steps; - // Inclusions. - for (std::size_t i = 0; i < nodes[node].out.size(); ++i) { - const std::size_t to = nodes[node].out[i]; - bool changed = false; - for (const std::string &target : nodes[node].targets) - changed |= nodes[to].targets.insert(target).second; - if (nodes[node].open && !nodes[to].open) { - nodes[to].open = nodes[node].open; - changed = true; - } - if (changed) - push(to); - } - // Dynamic call inclusions through this node. `intern` may grow `nodes`, - // so everything is re-read by index. - const std::vector targets(nodes[node].targets.begin(), - nodes[node].targets.end()); - for (std::size_t k = 0; k < nodes[node].callParams.size(); ++k) { - const std::size_t callParam = nodes[node].callParams[k]; - const std::uint32_t index = nodes[callParam].key.index; - for (const std::string &target : targets) { - const std::size_t param = intern(SlotKey::param(target, index)); - if (addEdge(callParam, param)) - push(callParam); - } - // Through an open callee, the arguments reach unknown code. - if (nodes[node].open) - push(callParam); - } - for (std::size_t k = 0; k < nodes[node].callResults.size(); ++k) { - const std::size_t callResult = nodes[node].callResults[k]; - for (const std::string &target : targets) { - const std::size_t result = intern(SlotKey::result(target)); - if (addEdge(result, callResult)) - push(result); - } - if (nodes[node].open && !nodes[callResult].open) { - nodes[callResult].open = - OpenSource{.seed = nodes[callResult].key, - .detail = "the result of a call through '" + - nodes[node].key.toString() + "'"}; - push(callResult); - } - } - // A function whose address reaches an open position may be called by - // code outside the solved program, with any arguments. - const std::optional callee = nodes[node].callee; - const bool escaping = nodes[node].escapingSeed || - (nodes[node].key.kind == SlotKind::CallParam && - callee && nodes[*callee].open.has_value()); - if (escaping) { - const std::vector functions(nodes[node].targets.begin(), - nodes[node].targets.end()); - for (const std::string &function : functions) - markEscaped(function); - } -} - -void Solver::run() { - while (!work.empty()) { - const std::size_t node = work.front(); - work.pop_front(); - queued[node] = false; - process(node); - } -} - -SlotSolution FnSlots::solve(const SlotRules &rules) const { - Solver solver(rules); - solver.load(all); - solver.run(); - SlotSolution solution; - solution.rules = rules; - for (SolverNode &node : solver.nodes) - solution.states.emplace( - std::move(node.key), - SlotSolution::SlotState{.targets = std::move(node.targets), - .open = std::move(node.open)}); - solution.escaped = std::move(solver.escaped); - solution.stepCount = solver.steps; - return solution; -} - -//===----------------------------------------------------------------------===// -// Queries -//===----------------------------------------------------------------------===// - -const std::set &SlotSolution::targets(const SlotKey &slot) const { - static const std::set None; - const auto found = states.find(slot); - return found == states.end() ? None : found->second.targets; -} - -std::optional SlotSolution::openSource(const SlotKey &slot) const { - if (const auto found = states.find(slot); found != states.end()) - return found->second.open; - // A slot no constraint mentions holds no value from the solved program; - // the rules alone decide. - switch (slot.kind) { - case SlotKind::Param: - if (auto open = seedOf(slot, rules).open) - return open; - if (escaped.contains(slot.scope)) - return escapedSource(slot, rules); - return std::nullopt; - case SlotKind::CallResult: { - const auto calleeKey = slot.callee(); - if (calleeKey && isOpen(*calleeKey)) - return OpenSource{.seed = slot, - .detail = "the result of a call through '" + - calleeKey->toString() + "'"}; - return std::nullopt; - } - case SlotKind::Field: - case SlotKind::Global: - case SlotKind::Static: - case SlotKind::Result: - case SlotKind::Local: - case SlotKind::CallParam: - return seedOf(slot, rules).open; - } - return std::nullopt; -} - -bool SlotSolution::escapes(std::string_view function) const { - return escaped.contains(function); -} - -CallResolution SlotSolution::resolveCall(const SlotKey &callee) const { - const std::set &found = targets(callee); - CallResolution resolution{ - .targets = std::vector(found.begin(), found.end()), - .open = openSource(callee)}; - if (resolution.open) - resolution.kind = resolution.targets.empty() ? IndirectCallKind::OpenUnknown - : IndirectCallKind::OpenKnown; - else if (resolution.targets.empty()) - resolution.kind = IndirectCallKind::ClosedEmpty; - else - resolution.kind = resolution.targets.size() == 1 - ? IndirectCallKind::ClosedSingle - : IndirectCallKind::ClosedJoin; - return resolution; -} - -std::vector SlotSolution::slots() const { - std::vector keys; - keys.reserve(states.size()); - for (const auto &[key, state] : states) - keys.push_back(key); - return keys; -} - -//===----------------------------------------------------------------------===// -// Building, merging, export -//===----------------------------------------------------------------------===// - -void FnSlots::add(SlotConstraint constraint) { - all.insert(std::move(constraint)); -} - -void FnSlots::addMember(std::string function, SlotKey slot) { - add(SlotConstraint::member(std::move(function), std::move(slot))); -} - -void FnSlots::addSubset(SlotKey from, SlotKey to) { - add(SlotConstraint::subset(std::move(from), std::move(to))); -} - -void FnSlots::addOpen(SlotKey slot, std::string detail) { - add(SlotConstraint::open(std::move(slot), std::move(detail))); -} - -void FnSlots::merge(const FnSlots &other) { - all.insert(other.all.begin(), other.all.end()); -} - -namespace { -/// What may flow out of a TU-private slot: non-local slots it copies, -/// functions stored in it, and outside values. -struct LocalValue { - std::set slots; - std::set functions; - std::set opens; - - /// Adds `other`; whether anything was new. - bool absorb(const LocalValue &other) { - const std::size_t before = slots.size() + functions.size() + opens.size(); - slots.insert(other.slots.begin(), other.slots.end()); - functions.insert(other.functions.begin(), other.functions.end()); - opens.insert(other.opens.begin(), other.opens.end()); - return slots.size() + functions.size() + opens.size() != before; - } -}; -} // namespace - -/// What reading `key` yields, in terms of non-local slots, functions and -/// outside values. `values` holds the `local` slots' values so far. -static LocalValue valueOf(const SlotKey &key, - const std::map &values) { - LocalValue value; - if (!key.isLocal()) { - value.slots.insert(key); - return value; - } - if (key.kind == SlotKind::Local) { - if (const auto found = values.find(key); found != values.end()) - value = found->second; - return value; - } - if (const auto calleeKey = key.callee(); - calleeKey && key.kind == SlotKind::CallResult) { - // A call through a local calls every callee the local holds. - const LocalValue callee = valueOf(*calleeKey, values); - for (const SlotKey &slot : callee.slots) - value.slots.insert(SlotKey::callResult(slot)); - for (const std::string &function : callee.functions) - value.slots.insert(SlotKey::result(function)); - if (!callee.opens.empty()) - value.slots.insert(SlotKey::result(std::string(UnknownFunction))); - } - // A `call-param` key is only ever written. - return value; -} - -/// The non-local slots a write to `key` reaches. -static std::vector -sinksOf(const SlotKey &key, const std::map &values) { - if (!key.isLocal()) - return {key}; - std::vector sinks; - if (const auto calleeKey = key.callee(); - calleeKey && key.kind == SlotKind::CallParam) { - const LocalValue callee = valueOf(*calleeKey, values); - for (const SlotKey &slot : callee.slots) - sinks.push_back(SlotKey::callParam(slot, key.index)); - for (const std::string &function : callee.functions) - sinks.push_back(SlotKey::param(function, key.index)); - if (!callee.opens.empty()) - sinks.push_back(SlotKey::param(std::string(UnknownFunction), key.index)); - } - // Writes to `local` slots are accounted in their values. - return sinks; -} - -FnSlots FnSlots::withoutLocals() const { - std::map values; - for (const SlotConstraint &constraint : all) { - if (constraint.slot.kind != SlotKind::Local) - continue; - if (constraint.kind == SlotConstraint::Kind::Member) - values[constraint.slot].functions.insert(constraint.function); - else if (constraint.kind == SlotConstraint::Kind::Open) - values[constraint.slot].opens.insert(constraint.detail); - } - for (bool changed = true; changed;) { - changed = false; - for (const SlotConstraint &constraint : all) { - if (constraint.kind != SlotConstraint::Kind::Subset || - constraint.slot.kind != SlotKind::Local) - continue; - const LocalValue incoming = valueOf(constraint.from, values); - changed |= values[constraint.slot].absorb(incoming); - } - } - - FnSlots exported; - for (const SlotConstraint &constraint : all) { - const std::vector sinks = sinksOf(constraint.slot, values); - if (sinks.empty()) - continue; - switch (constraint.kind) { - case SlotConstraint::Kind::Member: - for (const SlotKey &sink : sinks) - exported.addMember(constraint.function, sink); - break; - case SlotConstraint::Kind::Open: - for (const SlotKey &sink : sinks) - exported.addOpen(sink, constraint.detail); - break; - case SlotConstraint::Kind::Subset: { - const LocalValue source = valueOf(constraint.from, values); - for (const SlotKey &sink : sinks) { - for (const SlotKey &slot : source.slots) - exported.addSubset(slot, sink); - for (const std::string &function : source.functions) - exported.addMember(function, sink); - for (const std::string &detail : source.opens) - exported.addOpen(sink, detail); - } - break; - } - } - } - return exported; -} - -std::vector FnSlots::rows() const { - std::map bySlot; - for (const SlotConstraint &constraint : all) { - SlotRow &row = bySlot[constraint.slot]; - row.slot = constraint.slot; - switch (constraint.kind) { - case SlotConstraint::Kind::Member: - row.targets.push_back(constraint.function); - break; - case SlotConstraint::Kind::Subset: - row.sources.push_back(constraint.from); - break; - case SlotConstraint::Kind::Open: - // Constraints are sorted, so the first detail is the least. - if (!row.open) - row.open = constraint.detail; - break; - } - } - std::vector rows; - rows.reserve(bySlot.size()); - for (auto &[slot, row] : bySlot) - rows.push_back(std::move(row)); - return rows; -} - -FnSlots FnSlots::fromRows(std::span rows) { - FnSlots slots; - for (const SlotRow &row : rows) { - for (const std::string &target : row.targets) - slots.addMember(target, row.slot); - for (const SlotKey &source : row.sources) - slots.addSubset(source, row.slot); - if (row.open) - slots.addOpen(row.slot, *row.open); - } - return slots; -} - -} // namespace weavec::core diff --git a/lib/Core/Heap.cpp b/lib/Core/Heap.cpp deleted file mode 100644 index 822cfff9..00000000 --- a/lib/Core/Heap.cpp +++ /dev/null @@ -1,3919 +0,0 @@ -//===- Heap.cpp - The object engine's abstract heap -----------------------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// - -#include "weavec/Core/Heap.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace weavec::core { - -HeapOracle::~HeapOracle() = default; - -std::string_view spell(ObjectKind kind) noexcept { - switch (kind) { - case ObjectKind::Local: - return "local"; - case ObjectKind::Global: - return "global"; - case ObjectKind::Literal: - return "literal"; - case ObjectKind::Function: - return "function"; - case ObjectKind::HeapRecent: - return "heap"; - case ObjectKind::HeapOld: - return "heap-old"; - case ObjectKind::Entry: - return "entry"; - case ObjectKind::EntrySummary: - return "entry-summary"; - case ObjectKind::Materialized: - return "materialized"; - case ObjectKind::Focus: - return "focus"; - case ObjectKind::CallResult: - return "call-result"; - case ObjectKind::Unknown: - return "unknown"; - } - return "?"; -} - -//===----------------------------------------------------------------------===// -// ObjectTable -//===----------------------------------------------------------------------===// - -ObjectId ObjectTable::intern(const ObjectKey &key, ObjectInfo info) { - if (auto it = byKey.find(key); it != byKey.end()) - return it->second; - info.key = key; - objects.push_back(std::move(info)); - auto id = static_cast(objects.size()); - byKey.emplace(key, id); - return id; -} - -std::optional ObjectTable::lookup(const ObjectKey &key) const { - if (auto it = byKey.find(key); it != byKey.end()) - return it->second; - return std::nullopt; -} - -ObjectId ObjectTable::deadCopy(ObjectId id) { - const ObjectInfo &live = info(id); - if (live.key.dead) - return id; - ObjectKey key = live.key; - key.dead = true; - ObjectInfo copy = live; - return intern(key, std::move(copy)); -} - -ObjectId ObjectTable::liveVersion(ObjectId id) const { - const ObjectInfo &object = info(id); - if (!object.key.dead) - return id; - ObjectKey key = object.key; - key.dead = false; - if (auto live = lookup(key)) - return *live; - return id; -} - -//===----------------------------------------------------------------------===// -// Terms and records -//===----------------------------------------------------------------------===// - -static std::optional checkedAdd(std::int64_t a, std::int64_t b) { - __int128 sum = static_cast<__int128>(a) + b; - if (sum > INT64_MAX || sum < INT64_MIN) - return std::nullopt; - return static_cast(sum); -} - -std::optional Term::plus(const Term &other) const { - if (!known || !other.known) - return Term::unknown(); - auto constantSum = checkedAdd(constant, other.constant); - if (!constantSum) - return std::nullopt; - bool leftVar = var != ZeroSym && scale != 0; - bool rightVar = other.var != ZeroSym && other.scale != 0; - if (!leftVar && !rightVar) - return Term::of(*constantSum); - if (leftVar && !rightVar) - return Term::ofSym(var, scale, *constantSum); - if (!leftVar && rightVar) - return Term::ofSym(other.var, other.scale, *constantSum); - if (var == other.var) { - auto scaleSum = checkedAdd(scale, other.scale); - if (!scaleSum) - return std::nullopt; - if (*scaleSum == 0) - return Term::of(*constantSum); - return Term::ofSym(var, *scaleSum, *constantSum); - } - return std::nullopt; -} - -Term Term::plusConstant(std::int64_t delta) const { - if (!known) - return *this; - auto sum = checkedAdd(constant, delta); - if (!sum) - return Term::unknown(); - Term out = *this; - out.constant = *sum; - return out; -} - -ReleaseRecord joinRecords(const ReleaseRecord &left, - const ReleaseRecord &right) { - // RFC 0030 §3.1: a known record wins over an unknown one for the - // reason, location and names; the bits join. - ReleaseRecord out = - left.unknownOrigin() && !right.unknownOrigin() ? right : left; - out.allPaths = left.allPaths && right.allPaths && - left.unknownOrigin() == right.unknownOrigin(); - out.conditional = left.conditional || right.conditional; - out.lossy = left.lossy || right.lossy; - out.aliasOnly = left.aliasOnly && right.aliasOnly; - if (left.unknownOrigin() && right.unknownOrigin()) - out.reason = left.reason; - // Only the parameter facts both releases had. - out.paramGuard.clear(); - for (const auto &fact : left.paramGuard) - if (std::ranges::find(right.paramGuard, fact) != right.paramGuard.end()) - out.paramGuard.push_back(fact); - out.entryGuard.clear(); - std::ranges::set_intersection(left.entryGuard, right.entryGuard, - std::back_inserter(out.entryGuard)); - out.pairGuard.clear(); - for (const ParamPairTest &fact : left.pairGuard) - if (std::ranges::find(right.pairGuard, fact) != right.pairGuard.end()) - out.pairGuard.push_back(fact); - out.nonNullLocals.clear(); - std::ranges::set_intersection(left.nonNullLocals, right.nonNullLocals, - std::back_inserter(out.nonNullLocals)); - return out; -} - -//===----------------------------------------------------------------------===// -// Symbols and objects -//===----------------------------------------------------------------------===// - -// NOLINTNEXTLINE(readability-convert-member-functions-to-static): Heap API -Sym Heap::fresh(HeapState &state, SymInfo info) const { - Sym sym = state.nextSym++; - state.syms.set(sym, std::move(info)); - return sym; -} - -// NOLINTNEXTLINE(readability-convert-member-functions-to-static): Heap API -const SymInfo &Heap::info(const HeapState &state, Sym sym) const { - static const SymInfo None; - if (const SymInfo *found = state.syms.find(sym)) - return *found; - return None; -} - -// NOLINTNEXTLINE(readability-convert-member-functions-to-static): Heap API -SymInfo &Heap::infoMut(HeapState &state, Sym sym) const { - return state.syms.at(sym); -} - -void Heap::markNonNull(HeapState &state, Sym pointer) const { - Sym root = pointer; - for (const auto &[derived, from] : state.nullFollows) - if (derived == pointer) { - root = from; - break; - } - auto refine = [&](Sym sym) { - const SymInfo *value = state.syms.find(sym); - if (value != nullptr && value->type == SymInfo::Type::Pointer && - value->null == PointerNull::Maybe) - infoMut(state, sym).null = PointerNull::NonNull; - }; - refine(root); - for (const auto &[derived, from] : state.nullFollows) - if (from == root) - refine(derived); -} - -Sym Heap::constant(HeapState &state, std::int64_t value, - std::optional type) const { - SymInfo info; - info.type = SymInfo::Type::Int; - info.intType = type; - Sym sym = fresh(state, std::move(info)); - state.zone.addRange(sym, value, value); - return sym; -} - -Sym Heap::pointer(HeapState &state, std::vector targets, - PointerNull null, std::string name) const { - SymInfo info; - info.type = SymInfo::Type::Pointer; - std::ranges::sort(targets); - targets.erase(std::ranges::unique(targets).begin(), targets.end()); - info.targets = std::move(targets); - info.null = null; - info.name = std::move(name); - return fresh(state, std::move(info)); -} - -// NOLINTNEXTLINE(readability-convert-member-functions-to-static): Heap API -ObjectState &Heap::object(HeapState &state, ObjectId id) const { - return state.objects.at(id); -} - -// NOLINTNEXTLINE(readability-convert-member-functions-to-static): Heap API -ObjectState &Heap::ensure(HeapState &state, ObjectId id) const { - return state.objects.at(id); -} - -//===----------------------------------------------------------------------===// -// Term comparisons (§4.4) -//===----------------------------------------------------------------------===// - -/// Lower and upper bounds of a term, from the zone. -namespace { -struct TermRange { - std::optional<__int128> lo; - std::optional<__int128> hi; -}; -} // namespace - -static TermRange rangeOf(const Zone &zone, const Term &term) { - TermRange out; - if (!term.known) - return out; - if (term.var == ZeroSym || term.scale == 0) { - out.lo = term.constant; - out.hi = term.constant; - return out; - } - auto lo = zone.lower(term.var); - auto hi = zone.upper(term.var); - if (term.scale > 0) { - if (lo) - out.lo = (static_cast<__int128>(*lo) * term.scale) + term.constant; - if (hi) - out.hi = (static_cast<__int128>(*hi) * term.scale) + term.constant; - } else { - if (hi) - out.lo = (static_cast<__int128>(*hi) * term.scale) + term.constant; - if (lo) - out.hi = (static_cast<__int128>(*lo) * term.scale) + term.constant; - } - return out; -} - -/// Whether `left <= right` holds for every value (`Proven`), for no value -/// (`Refuted`) or neither. -namespace { -enum class Order : std::uint8_t { Proven, Refuted, Unknown }; -} // namespace - -static Order compareTerms(const Zone &zone, const Term &left, - const Term &right) { - if (!left.known || !right.known) - return Order::Unknown; - // Same variable: compare the linear parts. - bool leftVar = left.var != ZeroSym && left.scale != 0; - bool rightVar = right.var != ZeroSym && right.scale != 0; - if (leftVar && rightVar && left.var == right.var && - left.scale == right.scale) { - return left.constant <= right.constant ? Order::Proven : Order::Refuted; - } - // Same scale, different variables: a zone query on the difference. - if (leftVar && rightVar && left.scale == right.scale && left.scale > 0) { - // scale*(x - y) <= c2 - c1 <=> x - y <= floor((c2 - c1) / scale) - __int128 diff = static_cast<__int128>(right.constant) - left.constant; - __int128 q = diff >= 0 ? diff / left.scale - : -((-diff + left.scale - 1) / left.scale); - if (q >= INT64_MIN && q <= INT64_MAX && - zone.entails(left.var, right.var, static_cast(q))) - return Order::Proven; - // Refuted when y - x <= ceil-bound making left > right always. - __int128 strict = q + 1; // x - y >= q + 1 <=> y - x <= -(q + 1) - if (-strict >= INT64_MIN && -strict <= INT64_MAX && - zone.entails(right.var, left.var, static_cast(-strict))) - return Order::Refuted; - } - // A constant against a scaled variable. - if (!leftVar && rightVar && right.scale > 0) { - // c1 <= s*y + c2 <=> y >= ceil((c1 - c2) / s) - __int128 need = static_cast<__int128>(left.constant) - right.constant; - __int128 q = need >= 0 ? (need + right.scale - 1) / right.scale - : -((-need) / right.scale); - if (auto lo = zone.lower(right.var); lo && *lo >= q) - return Order::Proven; - if (auto hi = zone.upper(right.var); hi && *hi < q) - return Order::Refuted; - } - if (leftVar && !rightVar && left.scale > 0) { - // s*x + c1 <= c2 <=> x <= floor((c2 - c1) / s) - __int128 room = static_cast<__int128>(right.constant) - left.constant; - __int128 q = room >= 0 ? room / left.scale - : -((-room + left.scale - 1) / left.scale); - if (auto hi = zone.upper(left.var); hi && *hi <= q) - return Order::Proven; - if (auto lo = zone.lower(left.var); lo && *lo > q) - return Order::Refuted; - } - TermRange l = rangeOf(zone, left); - TermRange r = rangeOf(zone, right); - if (l.hi && r.lo && *l.hi <= *r.lo) - return Order::Proven; - if (l.lo && r.hi && *l.lo > *r.hi) - return Order::Refuted; - return Order::Unknown; -} - -//===----------------------------------------------------------------------===// -// Cell keys and element positions (§4.2 *Amendment (arrays)*) -//===----------------------------------------------------------------------===// - -Term CellKey::byteTerm() const { - if (isSummary()) - return Term::unknown(); - if (isSelected()) - return Term::ofSym(index, stride, offset); - return Term::of(offset); -} - -CellKey CellKey::position() const { - if (stride == 0) - return *this; - auto size = static_cast(stride); - std::int64_t base = offset % size; - if (base < 0) - base += size; - return CellKey{.offset = base, .stride = stride, .index = ZeroSym}; -} - -std::optional CellKey::at(const Term &offset) { - if (!offset.known) - return std::nullopt; - if (offset.isConstant()) - return CellKey{.offset = offset.constant, .stride = 0, .index = ZeroSym}; - if (offset.scale <= 0 || offset.scale > std::int64_t{1U << 20U}) - return std::nullopt; - return CellKey{.offset = offset.constant, - .stride = static_cast(offset.scale), - .index = offset.var}; -} - -static std::int64_t commonDivisor(std::int64_t a, std::int64_t b) { - a = a < 0 ? -a : a; - b = b < 0 ? -b : b; - while (b != 0) { - std::int64_t rest = a % b; - a = b; - b = rest; - } - return a; -} - -/// Whether the byte offsets `a` and `b` name the same cell (true), different -/// cells (false), or neither. Cells are keyed by their offsets (§4.2), so -/// offsets with different residues modulo the scales' common divisor are -/// different cells: other fields of the same or another element. -static std::optional sameOffset(const Zone &zone, const Term &a, - const Term &b) { - if (!a.known || !b.known) - return std::nullopt; - if (a == b) - return true; - bool aVar = a.var != ZeroSym && a.scale != 0; - bool bVar = b.var != ZeroSym && b.scale != 0; - if (aVar || bVar) { - std::int64_t divisor = - commonDivisor(aVar ? a.scale : 0, bVar ? b.scale : 0); - if (divisor > 1) { - __int128 difference = static_cast<__int128>(a.constant) - b.constant; - if (difference % divisor != 0) - return false; - } - } - Order le = compareTerms(zone, a, b); - Order ge = compareTerms(zone, b, a); - if (le == Order::Proven && ge == Order::Proven) - return true; - if (le == Order::Refuted || ge == Order::Refuted) - return false; - if (compareTerms(zone, a, b.plusConstant(-1)) == Order::Proven || - compareTerms(zone, b, a.plusConstant(-1)) == Order::Proven) - return false; - return std::nullopt; -} - -/// `sym` as `root + delta` (mod 2^width) through the additions and -/// subtractions of constants that computed it in one integer type. -static std::pair constantOffsetRoot(const HeapState &state, - Sym sym) { - __int128 delta = 0; - const SymInfo *info = state.syms.find(sym); - for (int depth = 0; - depth < 8 && info != nullptr && info->defined && info->intType && - info->defined->right == ZeroSym && info->defined->constant; - ++depth) { - const SymDefinition &definition = *info->defined; - if (definition.op != IntegerOp::Add && definition.op != IntegerOp::Subtract) - break; - const SymInfo *base = state.syms.find(definition.left); - if (base == nullptr || base->intType != info->intType) - break; - delta += definition.op == IntegerOp::Add ? *definition.constant - : -*definition.constant; - sym = definition.left; - info = base; - } - return {sym, delta}; -} - -/// §4.9: whether two index symbols hold different values on every path, -/// though the zone relates neither to the other: one is the other plus a -/// constant that is not a multiple of 2^width (`i + 1` of an `int` that may -/// overflow still differs from `i`, RFC 0017). -static bool differentValues(const HeapState &state, Sym a, Sym b) { - if (a == b) - return false; - const SymInfo *ai = state.syms.find(a); - const SymInfo *bi = state.syms.find(b); - if (ai == nullptr || bi == nullptr || !ai->intType || - ai->intType != bi->intType || ai->intType->width == 0 || - ai->intType->width > 64) - return false; - auto [rootA, deltaA] = constantOffsetRoot(state, a); - auto [rootB, deltaB] = constantOffsetRoot(state, b); - if (rootA != rootB) - return false; - __int128 difference = deltaA - deltaB; - if (ai->intType->width < 64) - difference %= static_cast<__int128>(static_cast(1) - << ai->intType->width); - else - difference %= - static_cast<__int128>(static_cast(1) << 64U); - return difference != 0; -} - -/// `sameOffset`, also telling apart two cells whose index symbols hold -/// different values (`differentValues`) at the same scale and constant. -static std::optional sameOffset(const HeapState &state, const Term &a, - const Term &b) { - std::optional same = sameOffset(state.zone, a, b); - if (!same && a.known && b.known && a.scale == b.scale && a.scale != 0 && - a.constant == b.constant && a.var != ZeroSym && b.var != ZeroSym && - differentValues(state, a.var, b.var)) - return false; - return same; -} - -/// The element index of the cell at byte `offset` among the elements at -/// `position`. `at` is false when the cell is not at that position, and -/// unset when that is not known. -namespace { -struct ElementIndex { - std::optional at; - Term index = Term::unknown(); -}; -} // namespace - -static ElementIndex elementIndex(const CellKey &position, const Term &offset) { - ElementIndex out; - if (!offset.known || position.stride == 0) - return out; - auto size = static_cast(position.stride); - bool var = offset.var != ZeroSym && offset.scale != 0; - if (var && offset.scale % size != 0) - return out; - std::int64_t rest = offset.constant - position.offset; - if (((rest % size) + size) % size != 0) { - out.at = false; - return out; - } - out.at = true; - std::int64_t base = rest / size; - out.index = - var ? Term::ofSym(offset.var, offset.scale / size, base) : Term::of(base); - return out; -} - -/// Whether the elements `[from, to)` at `position` contain the cell at byte -/// `offset`. -static Order inRange(const Zone &zone, const CellKey &position, - const Term &from, const Term &to, const Term &offset) { - ElementIndex element = elementIndex(position, offset); - if (element.at && !*element.at) - return Order::Refuted; - if (compareTerms(zone, to, from) == Order::Proven) - return Order::Refuted; // an empty range - if (!element.at) - return Order::Unknown; - Order lower = compareTerms(zone, from, element.index); - Order upper = compareTerms(zone, element.index.plusConstant(1), to); - if (lower == Order::Proven && upper == Order::Proven) - return Order::Proven; - if (lower == Order::Refuted || upper == Order::Refuted) - return Order::Refuted; - return Order::Unknown; -} - -/// Whether element positions may share cells: equal positions, or -/// different strides (cells of one may be cells of the other). -static bool positionsOverlap(const CellKey &a, const CellKey &b) { - if (a.stride == b.stride) - return a.offset == b.offset; - return true; -} - -/// Whether two ranges at one position must be disjoint. -static bool rangesDisjoint(const Zone &zone, const Segment &a, - const Segment &b) { - if (!positionsOverlap(a.position, b.position)) - return true; - if (a.position != b.position) - return false; - return compareTerms(zone, a.to, b.from) == Order::Proven || - compareTerms(zone, b.to, a.from) == Order::Proven; -} - -// NOLINTNEXTLINE(readability-convert-member-functions-to-static): Heap API -std::optional Heap::sameCell(const HeapState &state, const Term &first, - const Term &second) const { - return sameOffset(state, first, second); -} - -//===----------------------------------------------------------------------===// -// Memory -//===----------------------------------------------------------------------===// - -// NOLINTNEXTLINE(readability-convert-member-functions-to-static): Heap API -std::optional Heap::read(const HeapState &state, ObjectId object, - CellKey key) const { - const ObjectState *found = state.objects.find(object); - if (found == nullptr) - return std::nullopt; - if (const Sym *sym = found->cells.find(key)) - return *sym; - return std::nullopt; -} - -/// The note of whichever side may be null (RFC 0008): a side that is -/// non-null contributes nothing to the joined value's nullness. -static std::optional joinNullOrigins(const SymInfo &a, - const SymInfo &b) { - if (a.nullOrigin && a.null != PointerNull::NonNull) - return a.nullOrigin; - if (b.nullOrigin && b.null != PointerNull::NonNull) - return b.nullOrigin; - return a.nullOrigin ? a.nullOrigin : b.nullOrigin; -} - -/// `out`'s entry origins: those of `a` and of `b`. -/// Whether a join of the pointers `a` and `b` takes in a null one beside -/// a value that is not (`SymInfo::nullJoined`). -static bool joinsNull(const SymInfo &a, const SymInfo &b) { - auto isNull = [](const SymInfo &value) { - return value.null == PointerNull::Null && !value.uninit; - }; - return a.nullJoined || b.nullJoined || (isNull(a) && !isNull(b)) || - (isNull(b) && !isNull(a)); -} - -static void uniteEntryOrigins(SymInfo &out, const SymInfo &a, - const SymInfo &b) { - // (Both sorted.) - if (b.entryOrigins.empty() || a.entryOrigins == b.entryOrigins) { - out.entryOrigins = a.entryOrigins; - } else if (a.entryOrigins.empty()) { - out.entryOrigins = b.entryOrigins; - } else { - out.entryOrigins.clear(); - out.entryOrigins.reserve(a.entryOrigins.size() + b.entryOrigins.size()); - std::ranges::set_union(a.entryOrigins, b.entryOrigins, - std::back_inserter(out.entryOrigins)); - out.entryOrigins.erase(std::ranges::unique(out.entryOrigins).begin(), - out.entryOrigins.end()); - } - out.entryOriginsLost = a.entryOriginsLost || b.entryOriginsLost; - if (out.entryOrigins.size() > MaxEntryOrigins) { - out.entryOrigins.resize(MaxEntryOrigins); - out.entryOriginsLost = true; - } -} - -Sym Heap::mergeWeak(HeapState &state, Sym left, Sym right) const { - if (left == right) - return left; - const SymInfo &a = info(state, left); - const SymInfo &b = info(state, right); - SymInfo out; - out.type = a.type == b.type ? a.type : SymInfo::Type::Unknown; - out.name = !a.name.empty() ? a.name : b.name; - uniteEntryOrigins(out, a, b); - if (out.type == SymInfo::Type::Int) { - out.intType = a.intType ? a.intType : b.intType; - if (a.values && b.values && a.values->type == b.values->type) - out.values = a.values->united(*b.values); - } else if (out.type == SymInfo::Type::Pointer) { - out.top = a.top || b.top; - if (!out.top) { - out.targets = a.targets; - out.targets.insert(out.targets.end(), b.targets.begin(), b.targets.end()); - std::ranges::sort(out.targets); - out.targets.erase(std::ranges::unique(out.targets).begin(), - out.targets.end()); - if (out.targets.size() > 8) { - out.targets.clear(); - out.top = true; - } - } - out.null = a.null == b.null ? a.null : PointerNull::Maybe; - out.allocatorSource = a.allocatorSource || b.allocatorSource; - out.nullOrigin = joinNullOrigins(a, b); - if (a.release || b.release) { - ReleaseRecord record = a.release ? *a.release : *b.release; - if (a.release && b.release) - record = joinRecords(*a.release, *b.release); - record.allPaths = false; - record.aliasOnly = true; - out.release = record; - } - // RFC 0033 §2: raw is a must fact. A raw value merged with another is - // raw only on some paths: its accesses are unresolved, never an error. - out.raw = a.raw && b.raw; - out.rawSome = a.rawSome || b.rawSome || a.raw != b.raw; - out.rawAt = a.raw ? a.rawAt : b.rawAt; - out.rawOrigin = a.raw ? a.rawOrigin : b.rawOrigin; - out.rawFrom = a.raw ? a.rawFrom : b.rawFrom; - out.rawVia = a.raw ? a.rawVia : b.rawVia; - out.rawCast = a.rawCast || b.rawCast || a.raw != b.raw; - out.uninit = a.uninit && b.uninit; - out.mayUninit = a.uninit || b.uninit || a.mayUninit || b.mayUninit; - out.nullJoined = joinsNull(a, b); - } else if (out.type == SymInfo::Type::Function) { - out.functionsKnown = a.functionsKnown && b.functionsKnown; - if (out.functionsKnown) { - out.foreignFunctions = a.foreignFunctions; - out.foreignFunctions.insert(out.foreignFunctions.end(), - b.foreignFunctions.begin(), - b.foreignFunctions.end()); - std::ranges::sort(out.foreignFunctions); - out.foreignFunctions.erase( - std::ranges::unique(out.foreignFunctions).begin(), - out.foreignFunctions.end()); - out.functions = a.functions; - out.functions.insert(out.functions.end(), b.functions.begin(), - b.functions.end()); - std::ranges::sort(out.functions); - out.functions.erase(std::ranges::unique(out.functions).begin(), - out.functions.end()); - if (out.functions.size() > 32) { - out.functions.clear(); - out.functionsKnown = false; - } - } - } - Sym merged = fresh(state, std::move(out)); - if (info(state, merged).type == SymInfo::Type::Int) { - // The hull of the two ranges. - auto lo1 = state.zone.lower(left); - auto lo2 = state.zone.lower(right); - auto hi1 = state.zone.upper(left); - auto hi2 = state.zone.upper(right); - std::optional lo; - std::optional hi; - if (lo1 && lo2) - lo = std::min(*lo1, *lo2); - if (hi1 && hi2) - hi = std::max(*hi1, *hi2); - state.zone.addRange(merged, lo, hi); - } - return merged; -} - -Sym Heap::copyValue(HeapState &state, Sym value) const { - SymInfo copy = info(state, value); - copy.entryOf.reset(); - copy.pending.clear(); - copy.condition.reset(); - copy.linear.reset(); - copy.unwrapped.reset(); - copy.productAtMost.reset(); - bool integer = copy.type == SymInfo::Type::Int; - Sym sym = fresh(state, std::move(copy)); - if (integer) - state.zone.addRange(sym, state.zone.lower(value), state.zone.upper(value)); - return sym; -} - -/// A release record that is evidence about this very value (not made by a -/// weak merge). -static bool hasEvidence(const SymInfo &value) { - return value.release && !value.release->aliasOnly; -} - -Sym Heap::mergePossible(HeapState &state, Sym left, Sym right) const { - if (left == right) - return left; - bool evidence = - hasEvidence(info(state, left)) || hasEvidence(info(state, right)); - Sym merged = mergeWeak(state, left, right); - if (evidence) { - SymInfo &out = infoMut(state, merged); - if (out.release) { - out.release->aliasOnly = false; - out.release->allPaths = false; - } - } - return merged; -} - -Sym Heap::joinUniform(HeapState &state, Sym left, Sym right) const { - if (left == right) - return left; - std::optional a = info(state, left).release; - std::optional b = info(state, right).release; - Sym merged = mergeWeak(state, left, right); - // Every element was released on both sides: every element was released. - if (a && b) - infoMut(state, merged).release = joinRecords(*a, *b); - return merged; -} - -Sym Heap::load(HeapState &state, ObjectId object, CellKey key, - const SymInfo &hint) const { - ensure(state, object); - // A summary key reads "some element": its cell holds only what stores - // through unknown indices wrote. - if (key.isSummary()) - return anyElement(state, object, key, hint); - if (auto existing = read(state, object, key)) - return *existing; - Sym value = readElement(state, object, key, hint); - state.objects.at(object).cells.set(key, value); - if (key.isSelected()) - limitElements(state, object, key); - return value; -} - -Sym Heap::readElement(HeapState &state, ObjectId object, CellKey key, - const SymInfo &hint) const { - Term offset = key.byteTerm(); - std::vector> cells; - std::vector segments; - if (const ObjectState *found = state.objects.find(object)) { - for (const auto &[cell, sym] : found->cells) - if (cell != key) - cells.emplace_back(cell, sym); - segments = found->segments; - } - // Values of cells and ranges that may be this cell. - std::vector possible; - for (const auto &[cell, sym] : cells) { - if (cell.isSummary()) - continue; - std::optional same = sameOffset(state, offset, cell.byteTerm()); - if (same && *same) - return sym; - if (!same) - possible.push_back(sym); - } - Sym value = ZeroSym; - for (const Segment &segment : segments) { - Order in = - inRange(state.zone, segment.position, segment.from, segment.to, offset); - if (in == Order::Proven) { - value = segment.isCopy() ? copiedElement(state, segment, offset, hint) - : copyValue(state, segment.value); - break; - } - if (in == Order::Unknown) - possible.push_back(segment.isCopy() - ? copiedElement(state, segment, offset, hint) - : segment.value); - } - if (value == ZeroSym) { - // No range must hold it: the value no store reached, or one a store - // through an unknown index left. - value = oracle.unwritten(state, object, key, hint); - for (const auto &[cell, sym] : cells) - if (cell.isSummary()) { - ElementIndex element = elementIndex(cell, offset); - if (!element.at || *element.at) - value = mergeWeak(state, value, asPointer(state, sym, hint)); - } - } - for (Sym sym : possible) - value = mergePossible(state, value, asPointer(state, sym, hint)); - return value; -} - -Sym Heap::asPointer(HeapState &state, Sym sym, const SymInfo &hint) const { - // A cell of another kind that may be the one a pointer is read from (an - // integer member that an element's unbounded index may reach, or that a - // loop folded into the element ranges) is read through a cast (§4.2): - // any object, so the pointer keeps its own targets beside it (RFC 0034 - // §6.3, a release in a loop left early). - const SymInfo &value = info(state, sym); - if (hint.type != SymInfo::Type::Pointer || - (value.type != SymInfo::Type::Int && - value.type != SymInfo::Type::Unknown)) - return sym; - ObjectInfo unknown; - unknown.singular = false; - unknown.name = "an unknown object"; - ObjectId any = table.intern(ObjectKey{.kind = ObjectKind::Unknown}, unknown); - ensure(state, any); - SymInfo cast; - cast.type = SymInfo::Type::Pointer; - cast.targets = {Target{.object = any}}; - cast.null = PointerNull::Maybe; - cast.rawCast = true; - return fresh(state, std::move(cast)); -} - -Sym Heap::anyElement(HeapState &state, ObjectId object, CellKey key, - const SymInfo &hint) const { - Sym value = oracle.unwritten(state, object, key, hint); - std::vector members; - if (const ObjectState *found = state.objects.find(object)) { - for (const auto &[cell, sym] : found->cells) { - if (cell.isSummary()) { - if (positionsOverlap(cell, key)) - members.push_back(sym); - continue; - } - ElementIndex element = elementIndex(key, cell.byteTerm()); - if (!element.at || *element.at) - members.push_back(sym); - } - for (const Segment &segment : found->segments) - if (positionsOverlap(segment.position, key)) - members.push_back(segment.value); - } - for (Sym sym : members) - value = mergeWeak(state, value, asPointer(state, sym, hint)); - return value; -} - -Sym Heap::rangeValue(HeapState &state, ObjectId object, CellKey position, - const Term &from, const Term &to, - const SymInfo &hint) const { - const ObjectState &found = ensure(state, object); - std::vector segments = found.segments; - std::optional summary; - if (const Sym *sym = found.cells.find(position)) - summary = *sym; - Sym value = ZeroSym; - bool covered = false; - Segment range{.position = position, .from = from, .to = to, .value = ZeroSym}; - for (const Segment &segment : segments) { - if (rangesDisjoint(state.zone, segment, range)) - continue; - if (segment.position == position && - compareTerms(state.zone, segment.from, from) == Order::Proven && - compareTerms(state.zone, to, segment.to) == Order::Proven) { - // The newest range that may overlap covers it. - value = value == ZeroSym ? segment.value - : joinUniform(state, value, segment.value); - covered = true; - break; - } - value = value == ZeroSym ? segment.value - : joinUniform(state, value, segment.value); - } - if (!covered) { - Sym base = oracle.unwritten(state, object, position, hint); - if (summary) - base = mergeWeak(state, base, *summary); - value = value == ZeroSym ? base : joinUniform(state, value, base); - } - return value; -} - -void Heap::write(HeapState &state, ObjectId objectId, CellKey key, Sym value, - bool weak) const { - ObjectState &written = ensure(state, objectId); - written.stored = true; - // A store here, on every path: no longer one entry test's. - std::erase_if(written.storedIff, [&](const auto &guard) { - return !key.isConcrete() || guard.first == key; - }); - if (key.isSummary()) { - writeSummary(state, objectId, key, value); - return; - } - Term offset = key.byteTerm(); - Sym stored = value; - if (weak) { - SymInfo hint = info(state, value); - stored = mergeWeak(state, load(state, objectId, key, hint), value); - } - // Other element cells: the same cell gets the value; one that may be it - // may now hold it (a weak update). - std::vector> others; - for (const auto &[cell, sym] : state.objects.at(objectId).cells) - if (cell != key && !cell.isSummary()) - others.emplace_back(cell, sym); - for (const auto &[cell, sym] : others) { - std::optional same = sameOffset(state, offset, cell.byteTerm()); - if (same && !*same) - continue; - Sym next = same ? stored : mergeWeak(state, sym, value); - state.objects.at(objectId).cells.set(cell, next); - } - // Ranges that may hold the cell no longer describe it exactly; one that - // must hold it is shadowed by the cell. - std::vector segments = state.objects.at(objectId).segments; - for (Segment &segment : segments) - if (inRange(state.zone, segment.position, segment.from, segment.to, - offset) == Order::Unknown) - segment.value = mergeWeak(state, segment.value, value); - state.objects.at(objectId).segments = std::move(segments); - state.objects.at(objectId).cells.set(key, stored); - if (key.isSelected()) - limitElements(state, objectId, key); - // A focus object's candidates may be the object written. - std::vector candidates = state.objects.at(objectId).candidates; - for (ObjectId candidate : candidates) { - if (!state.objects.contains(candidate)) - continue; - SymInfo hint = info(state, value); - Sym old = load(state, candidate, key, hint); - Sym next = mergeWeak(state, old, value); - state.objects.at(candidate).cells.set(key, next); - } -} - -void Heap::writeSummary(HeapState &state, ObjectId objectId, CellKey key, - Sym value) const { - const ObjectState &target = state.objects.at(objectId); - Sym old = ZeroSym; - if (const Sym *existing = target.cells.find(key)) - old = *existing; - std::vector> covered; - for (const auto &[cell, sym] : target.cells) { - if (cell.isSummary()) - continue; - ElementIndex element = elementIndex(key, cell.byteTerm()); - if (!element.at || *element.at) - covered.emplace_back(cell, sym); - } - std::vector segments = target.segments; - Sym merged = old == ZeroSym ? value : mergeWeak(state, old, value); - state.objects.at(objectId).cells.set(key, merged); - for (const auto &[cell, sym] : covered) - state.objects.at(objectId).cells.set(cell, mergeWeak(state, sym, value)); - for (Segment &segment : segments) - if (positionsOverlap(segment.position, key)) - segment.value = mergeWeak(state, segment.value, value); - state.objects.at(objectId).segments = std::move(segments); -} - -void Heap::evictCell(HeapState &state, ObjectId objectId, CellKey key) const { - auto held = read(state, objectId, key); - if (!held || key.isSummary()) - return; - // A concrete cell is an element of the object's stride. - std::uint32_t stride = - key.stride != 0 ? key.stride : state.objects.at(objectId).stride; - if (stride == 0) - return; - CellKey position = - CellKey{.offset = key.offset, .stride = stride, .index = ZeroSym} - .position(); - Sym value = *held; - state.objects.at(objectId).cells.erase(key); - Term offset = key.byteTerm(); - // Every range that may hold the element may now hold its value; so may - // every element a store through an unknown index reaches, unless a range - // must hold it. - std::vector segments = state.objects.at(objectId).segments; - bool covered = false; - for (Segment &segment : segments) { - Order in = - inRange(state.zone, segment.position, segment.from, segment.to, offset); - if (in == Order::Refuted) - continue; - segment.value = mergeWeak(state, segment.value, value); - if (in == Order::Proven) { - covered = true; - break; - } - } - state.objects.at(objectId).segments = std::move(segments); - if (!covered) { - const Sym *summary = state.objects.at(objectId).cells.find(position); - Sym merged = summary == nullptr ? value : mergeWeak(state, *summary, value); - state.objects.at(objectId).cells.set(position, merged); - } -} - -void Heap::limitElements(HeapState &state, ObjectId objectId, - CellKey keep) const { - std::vector selected; - for (const auto &[cell, sym] : state.objects.at(objectId).cells) - if (cell.isSelected() && cell != keep) - selected.push_back(cell); - for (std::size_t i = 0; i + MaxSelectedCells <= selected.size(); ++i) - evictCell(state, objectId, selected[i]); - trimSegments(state, objectId); -} - -void Heap::evictSegments(HeapState &state, ObjectId objectId, - const std::vector &evict) const { - std::vector segments = state.objects.at(objectId).segments; - std::vector kept; - kept.reserve(segments.size()); - std::vector removed; - for (std::size_t i = 0; i < segments.size(); ++i) { - if (i < evict.size() && evict[i]) { - // (What an evicted one shadowed, the older ranges after it, may hold - // its value.) - for (std::size_t j = i + 1; j < segments.size(); ++j) - if ((j >= evict.size() || !evict[j]) && - !rangesDisjoint(state.zone, segments[j], segments[i])) - segments[j].value = - mergeWeak(state, segments[j].value, segments[i].value); - removed.push_back(segments[i]); - continue; - } - kept.push_back(segments[i]); - } - if (removed.empty()) - return; - state.objects.at(objectId).segments = std::move(kept); - for (const Segment &segment : removed) { - const Sym *summary = - state.objects.at(objectId).cells.find(segment.position); - Sym merged = summary == nullptr ? segment.value - : mergeWeak(state, *summary, segment.value); - state.objects.at(objectId).cells.set(segment.position, merged); - } -} - -void Heap::trimSegments(HeapState &state, ObjectId objectId) const { - const std::vector &segments = state.objects.at(objectId).segments; - if (segments.size() <= MaxSegmentsPerPosition) - return; - std::map counts; - std::vector evict(segments.size(), false); - bool any = false; - for (std::size_t i = 0; i < segments.size(); ++i) - if (++counts[segments[i].position] > MaxSegmentsPerPosition) { - evict[i] = true; - any = true; - } - if (any) - evictSegments(state, objectId, evict); -} - -bool Heap::foldCell(HeapState &state, ObjectId objectId, CellKey key) const { - const ObjectState &target = state.objects.at(objectId); - if (target.stride == 0 || key.isSummary()) - return false; - auto held = read(state, objectId, key); - if (!held) - return false; - if (key.isSelected() && key.stride % target.stride != 0) - return false; - CellKey position = - CellKey{.offset = key.offset, .stride = target.stride, .index = ZeroSym} - .position(); - Term offset = key.byteTerm(); - ElementIndex element = elementIndex(position, offset); - if (!element.at || !*element.at) - return false; - Sym value = *held; - std::vector segments = target.segments; - state.objects.at(objectId).cells.erase(key); - Term next = element.index.plusConstant(1); - for (Segment &segment : segments) { - if (segment.position == position) { - std::optional after = - sameOffset(state.zone, segment.to, element.index); - std::optional before = sameOffset(state.zone, segment.from, next); - if ((after && *after) || (before && *before)) { - if (after && *after) - segment.to = next; - else - segment.from = element.index; - segment.value = joinUniform(state, segment.value, value); - state.objects.at(objectId).segments = std::move(segments); - return true; - } - } - // A newer range that may hold the element: an older one cannot grow - // over it. - if (inRange(state.zone, segment.position, segment.from, segment.to, - offset) != Order::Refuted) - break; - } - segments.insert(segments.begin(), Segment{.position = position, - .from = element.index, - .to = next, - .value = value}); - state.objects.at(objectId).segments = std::move(segments); - limitElements(state, objectId, CellKey{}); - return true; -} - -void Heap::releaseElements(HeapState &state, ObjectId objectId, - CellKey position, const Term &from, const Term &to, - const ReleaseRecord &record, - const SymInfo &hint) const { - ensure(state, objectId); - ReleaseRecord possible = record; - possible.allPaths = false; - // The cells in the range. - std::vector> cells; - for (const auto &[cell, sym] : state.objects.at(objectId).cells) - if (!cell.isSummary()) - cells.emplace_back(cell, sym); - for (const auto &[cell, sym] : cells) { - Order in = inRange(state.zone, position, from, to, cell.byteTerm()); - if (in == Order::Refuted || - info(state, sym).type != SymInfo::Type::Pointer || - info(state, sym).null == PointerNull::Null) - continue; - release(state, sym, in == Order::Proven ? record : possible); - } - // The rest: a range whose elements hold the released values. - Sym value = - copyValue(state, rangeValue(state, objectId, position, from, to, hint)); - if (info(state, value).type == SymInfo::Type::Pointer && - info(state, value).null != PointerNull::Null) - release(state, value, record); - std::vector segments = state.objects.at(objectId).segments; - segments.insert( - segments.begin(), - Segment{.position = position, .from = from, .to = to, .value = value}); - state.objects.at(objectId).segments = std::move(segments); - limitElements(state, objectId, CellKey{}); -} - -void Heap::writeElements(HeapState &state, ObjectId objectId, CellKey position, - const Term &from, const Term &to, Sym value, - bool weak) const { - ensure(state, objectId).storedIff.clear(); - ensure(state, objectId).stored = true; - std::vector> cells; - for (const auto &[cell, sym] : state.objects.at(objectId).cells) - if (!cell.isSummary()) - cells.emplace_back(cell, sym); - for (const auto &[cell, sym] : cells) { - Order in = inRange(state.zone, position, from, to, cell.byteTerm()); - if (in == Order::Refuted) - continue; - Sym next = in == Order::Proven && !weak ? copyValue(state, value) - : mergeWeak(state, sym, value); - state.objects.at(objectId).cells.set(cell, next); - } - Sym element = value; - if (weak) { - SymInfo hint = info(state, value); - element = mergeWeak( - state, rangeValue(state, objectId, position, from, to, hint), value); - } - std::vector segments = state.objects.at(objectId).segments; - segments.insert( - segments.begin(), - Segment{.position = position, .from = from, .to = to, .value = element}); - state.objects.at(objectId).segments = std::move(segments); - limitElements(state, objectId, CellKey{}); -} - -void Heap::copyElements(HeapState &state, ObjectId objectId, CellKey position, - const Term &from, const Term &to, Sym value, - ObjectId source, std::int64_t shift) const { - ensure(state, objectId).stored = true; - Segment range{.position = position, - .from = from, - .to = to, - .value = value, - .source = source, - .shift = shift, - .copied = value}; - // The element cells the range must hold take their source element's - // value; one it may hold may now hold it. - std::vector> cells; - for (const auto &[cell, sym] : state.objects.at(objectId).cells) - if (!cell.isSummary()) - cells.emplace_back(cell, sym); - for (const auto &[cell, sym] : cells) { - Order in = inRange(state.zone, position, from, to, cell.byteTerm()); - if (in == Order::Refuted) - continue; - SymInfo hint = info(state, sym); - Sym element = copiedElement(state, range, cell.byteTerm(), hint); - state.objects.at(objectId).cells.set( - cell, - in == Order::Proven ? element : mergePossible(state, sym, element)); - } - std::vector segments = state.objects.at(objectId).segments; - segments.insert(segments.begin(), range); - state.objects.at(objectId).segments = std::move(segments); - limitElements(state, objectId, CellKey{}); -} - -Sym Heap::copiedElement(HeapState &state, const Segment &segment, - const Term &offset, const SymInfo &hint) const { - std::optional key = CellKey::at(offset.plusConstant(segment.shift)); - if (!key || key->isSummary()) - return copyValue(state, segment.value); - ObjectId source = segment.source; - ensure(state, source); - const auto entryOf = std::make_pair(source, *key); - if (key->isConcrete()) { - // The entry value itself, where a load of it left it. - if (const Sym *held = state.objects.at(source).cells.find(*key); - held != nullptr && info(state, *held).entryOf == entryOf) - return *held; - std::vector found; - for (const auto &[sym, symInfo] : state.syms) - if (symInfo.entryOf == entryOf) - found.push_back(sym); - if (!found.empty()) { - Sym value = found.front(); - for (std::size_t i = 1; i < found.size(); ++i) - value = mergePossible(state, value, found[i]); - return value; - } - } - const ObjectState &object = state.objects.at(source); - // No store has reached the source since: a load reads the entry value - // (and later loads of the element read the same symbol). - if (!object.stored && !object.forgetsAny()) - return load(state, source, *key, hint); - // Otherwise the entry value, which nothing holds any more. - std::optional held = read(state, source, *key); - Sym value = oracle.unwritten(state, source, *key, hint); - if (held) - state.objects.at(source).cells.set(*key, *held); - else - state.objects.at(source).cells.erase(*key); - return value; -} - -static void -addForgotten(std::vector> &ranges, - std::int64_t from, std::int64_t to); - -void Heap::weakenCells(HeapState &state, ObjectId objectId, std::int64_t from, - std::optional size, - const std::function &unknownLike) const { - if (!state.objects.contains(objectId)) - return; - std::vector> inside; - for (const auto &[key, sym] : state.objects.at(objectId).cells) { - bool within = true; - if (size && !key.isSummary()) { - Term offset = key.byteTerm(); - within = compareTerms(state.zone, offset.plusConstant(1), - Term::of(from)) != Order::Proven && - compareTerms(state.zone, Term::of(from + *size), offset) != - Order::Proven; - } - if (within) - inside.emplace_back(key, sym); - } - for (const auto &[key, sym] : inside) { - Sym merged = mergeWeak(state, sym, unknownLike(sym)); - state.objects.at(objectId).cells.set(key, merged); - } - ObjectState &target = state.objects.at(objectId); - target.storedIff.clear(); - // (Every byte: the widest range the offsets can name.) - if (!target.havocked) - addForgotten(target.mayForgotten, size ? from : INT64_MIN / 2, - size ? from + *size : INT64_MAX / 2); - target.nulWithin.reset(); - target.nulFrom.reset(); -} - -/// Adds `[from, to)` to the sorted, disjoint `ranges`, merging what it -/// overlaps or touches. -static void -addForgotten(std::vector> &ranges, - std::int64_t from, std::int64_t to) { - if (from >= to) - return; - std::vector> out; - out.reserve(ranges.size() + 1); - bool placed = false; - for (const auto &range : ranges) { - if (range.second < from) { - out.push_back(range); - } else if (to < range.first) { - if (!placed) { - out.emplace_back(from, to); - placed = true; - } - out.push_back(range); - } else { - from = std::min(from, range.first); - to = std::max(to, range.second); - } - } - if (!placed) - out.emplace_back(from, to); - std::ranges::sort(out); - ranges = std::move(out); -} - -/// Whether `key` may lie in one of `ranges`. (An element cell's byte is -/// not a constant here: any range may hold it.) -static bool -inRanges(const std::vector> &ranges, - const CellKey &key) { - if (ranges.empty()) - return false; - if (!key.isConcrete()) - return true; - return std::ranges::any_of(ranges, [&](const auto &r) { - return r.first <= key.offset && key.offset < r.second; - }); -} - -bool ObjectState::forgets(const CellKey &key) const { - return havocked || inRanges(forgotten, key); -} - -bool ObjectState::mayForget(const CellKey &key) const { - return inRanges(mayForgotten, key); -} - -/// The ranges both `a` and `b` cover. -static std::vector> -intersectRanges(const std::vector> &a, - const std::vector> &b) { - std::vector> out; - for (const auto &x : a) - for (const auto &y : b) { - std::int64_t from = std::max(x.first, y.first); - std::int64_t to = std::min(x.second, y.second); - if (from < to) - out.emplace_back(from, to); - } - std::ranges::sort(out); - return out; -} - -// NOLINTNEXTLINE(readability-convert-member-functions-to-static): Heap API -void Heap::forgetCells(HeapState &state, ObjectId objectId, std::int64_t from, - std::optional size) const { - if (!state.objects.contains(objectId)) - return; - const Zone &zone = state.zone; - ObjectState &target = state.objects.at(objectId); - target.storedIff.clear(); - target.cells.eraseIf([&](const CellKey &key, Sym) { - if (!size || key.isSummary()) - return true; - Term offset = key.byteTerm(); - // Kept only when it must lie outside the bytes forgotten. - return compareTerms(zone, offset.plusConstant(1), Term::of(from)) != - Order::Proven && - compareTerms(zone, Term::of(from + *size), offset) != Order::Proven; - }); - // What the forgotten bytes hold is unknown: an unwritten cell there no - // longer reads as the zero or uninitialised value of the object's - // creation, nor as its entry value. - if (!size) { - target.havocked = true; - target.forgotten.clear(); - target.mayForgotten.clear(); - } else if (*size > 0 && !target.havocked) { - addForgotten(target.forgotten, from, from + *size); - } - if (!size || *size > 0) { - target.nulWithin.reset(); - target.nulFrom.reset(); - } - if (!size) { - target.segments.clear(); - return; - } - std::vector kept; - for (const Segment &segment : target.segments) { - auto stride = static_cast(segment.position.stride); - Term start = segment.from; - Term end = segment.to; - start.scale *= stride; - start.constant = (start.constant * stride) + segment.position.offset; - end.scale *= stride; - end.constant = (end.constant * stride) + segment.position.offset; - if (compareTerms(zone, end, Term::of(from)) == Order::Proven || - compareTerms(zone, Term::of(from + *size), start) == Order::Proven) - kept.push_back(segment); - } - target.segments = std::move(kept); -} - -//===----------------------------------------------------------------------===// -// Distinctness (§4.5) -//===----------------------------------------------------------------------===// - -bool Heap::ownedBelow(ObjectId object, ObjectId ancestor) const { - ObjectId live = table.liveVersion(ancestor); - ObjectId current = table.liveVersion(object); - for (int depth = 0; depth < 16 && current != 0; ++depth) { - const ObjectInfo &info = table.info(current); - ObjectId parent = info.ownedFrom; - if (parent == 0) - return false; - parent = table.liveVersion(parent); - if (parent == live) - return true; - current = parent; - } - return false; -} - -/// Whether objects of these kinds existed before the activation began. -static bool isEntryLike(ObjectKind kind) { - return kind == ObjectKind::Entry || kind == ObjectKind::EntrySummary || - kind == ObjectKind::CallResult; -} - -bool Heap::mayOverlap(const HeapState &state, ObjectId first, - ObjectId second) const { - if (first == second) - return true; - ObjectId a = table.liveVersion(first); - ObjectId b = table.liveVersion(second); - // An object and its own dead copy: on the paths where the copy exists, no - // pointer to the live version does (§4.6). - if (a == b) - return false; - // Focus objects (and their dead copies) may be any of their candidates. - auto candidatesOf = [&](ObjectId id) -> const std::vector * { - if (table.info(id).key.kind != ObjectKind::Focus) - return nullptr; - if (const ObjectState *found = state.objects.find(id)) - return &found->candidates; - return nullptr; - }; - if (table.info(first).key.kind == ObjectKind::Focus) { - if (const auto *candidates = candidatesOf(first)) - for (ObjectId candidate : *candidates) - if (mayOverlap(state, candidate, second)) - return true; - return false; - } - if (table.info(second).key.kind == ObjectKind::Focus) { - if (const auto *candidates = candidatesOf(second)) - for (ObjectId candidate : *candidates) - if (mayOverlap(state, first, candidate)) - return true; - return false; - } - const ObjectInfo &x = table.info(a); - const ObjectInfo &y = table.info(b); - ObjectKind kx = x.key.kind; - ObjectKind ky = y.key.kind; - if (kx == ObjectKind::Unknown || ky == ObjectKind::Unknown) - return true; - // A materialised object is one member of its parent; it may be what the - // parent may be, except the rest of the parent. - if (kx == ObjectKind::Materialized) { - if (table.liveVersion(x.key.parent) == b) - return false; - return mayOverlap(state, x.key.parent, second); - } - if (ky == ObjectKind::Materialized) { - if (table.liveVersion(y.key.parent) == a) - return false; - return mayOverlap(state, first, y.key.parent); - } - if (kx == ObjectKind::Function || ky == ObjectKind::Function) - return false; - bool entryX = isEntryLike(kx); - bool entryY = isEntryLike(ky); - if (!entryX && !entryY) - return false; // D4, D5: distinct objects this activation created - if (entryX != entryY) { - // Entry objects existed before the activation: they cannot be a local, - // a literal or an allocation the activation made (D4), but a caller's - // pointer may point to a global or a literal. - ObjectKind other = entryX ? ky : kx; - const ObjectInfo &otherInfo = entryX ? y : x; - const ObjectInfo &entryInfo = entryX ? x : y; - if (entryInfo.key.kind == ObjectKind::CallResult) { - // An unknown callee may return anything it could reach: not an - // allocation of this activation that never escaped (every call to - // unknown code marks what it can reach escaped, and an escape is never - // undone, so one that has not escaped now had not at the call). - if (other == ObjectKind::HeapRecent || other == ObjectKind::HeapOld) { - const ObjectState *object = state.objects.find(entryX ? b : a); - if (object != nullptr && !object->escaped) - return false; - } - return other != ObjectKind::Literal || - oracle.typesMayAlias(entryInfo.type, otherInfo.type); - } - if (other == ObjectKind::Global || other == ObjectKind::Literal) - return oracle.typesMayAlias(entryInfo.type, otherInfo.type); - return false; - } - // Two entry objects: D1, D2, D3. - if (!oracle.typesMayAlias(x.type, y.type)) - return false; - if (x.fromOwningSlot && y.fromOwningSlot) - return false; - if (ownedBelow(a, b) || ownedBelow(b, a)) - return false; - return true; -} - -//===----------------------------------------------------------------------===// -// Queries -//===----------------------------------------------------------------------===// - -std::optional Heap::pointersEqual(const HeapState &state, Sym first, - Sym second) { - if (first == second) - return true; - if (second < first) - std::swap(first, second); - for (const PointerFact &fact : state.pointerFacts) - if (fact.first == first && fact.second == second) - return fact.equal; - return std::nullopt; -} - -bool Heap::assumePointersEqual(HeapState &state, Sym first, Sym second, - bool equal) { - if (auto known = pointersEqual(state, first, second)) - return *known == equal; - if (second < first) - std::swap(first, second); - PointerFact fact{.first = first, .second = second, .equal = equal}; - state.pointerFacts.insert(std::ranges::lower_bound(state.pointerFacts, fact), - fact); - return true; -} - -static bool isReleasedLife(Life life) { - return life == Life::Released || life == Life::MayReleased || - life == Life::UnknownReleased; -} - -TemporalVerdict Heap::temporal(const HeapState &state, Sym pointer) const { - using Kind = TemporalVerdict::Kind; - const SymInfo &value = info(state, pointer); - TemporalVerdict verdict; - auto rank = [](Kind kind) { - switch (kind) { - case Kind::Violation: - return 6; - case Kind::MayReleased: - return 5; - case Kind::MayDangle: - return 4; - case Kind::UnknownCallee: - case Kind::Callback: - return 3; - case Kind::MayAliasReleased: - return 2; - case Kind::Proven: - return 0; - } - return 0; - }; - auto raise = [&](Kind kind, std::optional record, - ObjectId object = 0) { - if (rank(kind) > rank(verdict.kind)) { - verdict.kind = kind; - verdict.record = std::move(record); - verdict.object = object; - } - }; - auto fromRecord = [&](const ReleaseRecord &record, bool single) { - if (record.reason == ReleaseRecord::Reason::UnknownCallee) - raise(Kind::UnknownCallee, record); - else if (record.reason == ReleaseRecord::Reason::Callback) - raise(Kind::Callback, record); - else if (record.aliasOnly || !single) - raise(Kind::MayAliasReleased, record); - else if (record.definite()) - raise(Kind::Violation, record); - else - raise(Kind::MayReleased, record); - }; - if (value.release) - fromRecord(*value.release, true); - bool single = value.targets.size() == 1 && !value.top; - auto derivedFrom = [&](Sym releaser) { - if (releaser == ZeroSym) - return false; - if (releaser == pointer) - return false; - return std::ranges::find(value.ancestors, releaser) != - value.ancestors.end(); - }; - for (const Target &target : value.targets) { - const ObjectState *object = state.objects.find(target.object); - bool singular = table.info(target.object).singular; - // Memory the analysis knows nothing about (what an unknown callee left - // in a cell, an integer made a pointer) may be anything, freed or not - // (RFC 0030 §5.1). - if (table.info(target.object).key.kind == ObjectKind::Unknown) - raise(Kind::UnknownCallee, std::nullopt); - if (object != nullptr) { - switch (object->life) { - case Life::Live: - break; - case Life::Released: - case Life::MayReleased: - if (object->record && !derivedFrom(object->releasedBy)) { - ReleaseRecord record = *object->record; - if (object->life == Life::MayReleased) - record.allPaths = false; - fromRecord(record, single && singular); - } else if (!derivedFrom(object->releasedBy)) { - raise(Kind::MayAliasReleased, std::nullopt); - } - break; - case Life::UnknownReleased: - if (object->record) - fromRecord(*object->record, single && singular); - else - raise(Kind::UnknownCallee, std::nullopt); - break; - case Life::Ended: - if (single && singular) - raise(Kind::Violation, std::nullopt, target.object); - else - raise(Kind::MayDangle, std::nullopt, target.object); - break; - case Life::MayEnded: - raise(Kind::MayDangle, std::nullopt, target.object); - break; - } - } - // Objects released elsewhere that this target may be (§4.5). - for (const auto &[otherId, other] : state.objects) { - if (otherId == target.object || !isReleasedLife(other.life)) - continue; - if (derivedFrom(other.releasedBy)) - continue; - if (!mayOverlap(state, target.object, otherId)) - continue; - if (other.life == Life::UnknownReleased) { - if (other.record && - other.record->reason == ReleaseRecord::Reason::Callback) - raise(Kind::Callback, other.record); - else - raise(Kind::UnknownCallee, other.record); - } else { - raise(Kind::MayAliasReleased, other.record); - } - } - } - if (value.top) { - for (const auto &[otherId, other] : state.objects) - if (isReleasedLife(other.life)) { - raise(other.life == Life::UnknownReleased ? Kind::UnknownCallee - : Kind::MayAliasReleased, - other.record); - break; - } - } - return verdict; -} - -// NOLINTNEXTLINE(readability-convert-member-functions-to-static): Heap API -std::optional Heap::lessEqual(const HeapState &state, const Term &left, - const Term &right) const { - switch (compareTerms(state.zone, left, right)) { - case Order::Proven: - return true; - case Order::Refuted: - return false; - case Order::Unknown: - return std::nullopt; - } - return std::nullopt; -} - -SpatialVerdict Heap::spatial(const HeapState &state, Sym pointer, - const Term &extraOffset, - std::int64_t width) const { - return spatialAt(state, pointer, extraOffset, Term::of(width)); -} - -SpatialVerdict Heap::spatialAt(const HeapState &state, Sym pointer, - const Term &extraOffset, - const Term &need) const { - using Kind = SpatialVerdict::Kind; - const SymInfo &value = info(state, pointer); - SpatialVerdict out; - if (value.top || value.targets.empty()) { - out.kind = Kind::UnknownExtent; - return out; - } - bool allProven = true; - bool allViolate = true; - bool anyUnknownExtent = false; - bool anyUnknownIndex = false; - std::optional firstExtent; - for (const Target &target : value.targets) { - const ObjectState *object = state.objects.find(target.object); - std::optional extent; - if (object != nullptr) - extent = object->extent; - if (!extent || !extent->bytes.known) { - anyUnknownExtent = true; - allProven = false; - allViolate = false; - continue; - } - if (!firstExtent) - firstExtent = extent; - else if (!(firstExtent->bytes == extent->bytes)) - anyUnknownIndex = true; // different check expressions per target - auto offset = target.offset.plus(extraOffset); - if (!offset || !offset->known) { - anyUnknownIndex = true; - allProven = false; - allViolate = false; - continue; - } - auto endSum = need.known ? offset->plus(need) : std::nullopt; - if (!endSum || !endSum->known) { - anyUnknownIndex = true; - allProven = false; - allViolate = false; - continue; - } - Term end = *endSum; - Order lower = compareTerms(state.zone, Term::of(0), *offset); - Order upper = compareTerms(state.zone, end, extent->bytes); - if (upper != Order::Refuted && extent->unwrapped && - compareTerms(state.zone, end, *extent->unwrapped) == Order::Refuted) - upper = Order::Refuted; - bool proven = lower == Order::Proven && upper == Order::Proven; - bool violate = lower == Order::Refuted || upper == Order::Refuted; - if (!proven) - allProven = false; - if (!violate || extent->cls != ExtentClass::Exact) - allViolate = false; - if (violate && lower == Order::Refuted) - out.beforeStart = true; - if (violate && !out.reach) { - TermRange r = rangeOf(state.zone, end); - if (r.lo && *r.lo >= INT64_MIN && *r.lo <= INT64_MAX) - out.reach = static_cast(*r.lo); - } - if (violate && !out.end) - out.end = end; - } - out.extent = firstExtent; - if (allProven) { - out.kind = Kind::Proven; - return out; - } - if (allViolate) { - out.kind = Kind::Violation; - return out; - } - if (anyUnknownExtent) { - out.kind = Kind::UnknownExtent; - return out; - } - if (anyUnknownIndex) { - out.kind = Kind::UnknownIndex; - return out; - } - if (firstExtent && (firstExtent->cls == ExtentClass::Exact || - firstExtent->cls == ExtentClass::Declared)) { - out.kind = Kind::Checkable; - return out; - } - out.kind = Kind::UnknownExtent; - return out; -} - -//===----------------------------------------------------------------------===// -// Releases -//===----------------------------------------------------------------------===// - -void Heap::release(HeapState &state, Sym pointer, const ReleaseRecord &record, - bool possibly) const { - SymInfo &value = infoMut(state, pointer); - value.release = record; - if (possibly) - value.release->allPaths = false; - std::vector targets = value.targets; - bool single = targets.size() == 1 && !value.top && !possibly; - for (const Target &target : targets) { - ObjectState &object = state.objects.at(target.object); - bool strong = single && table.info(target.object).singular; - ReleaseRecord objectRecord = record; - if (!strong) - objectRecord.allPaths = false; - if (object.record && isReleasedLife(object.life)) - objectRecord = joinRecords(*object.record, objectRecord); - object.record = objectRecord; - object.releasedBy = pointer; - object.releaseOffset = target.offset.isConstant() - ? std::optional(target.offset.constant) - : std::nullopt; - if (record.unknownOrigin()) - object.life = Life::UnknownReleased; - else if (strong) - object.life = Life::Released; - else if (object.life != Life::Released) - object.life = Life::MayReleased; - if (strong) - object.effectReleased = true; - else - object.effectMayReleased = true; - } -} - -//===----------------------------------------------------------------------===// -// Reachability and collection -//===----------------------------------------------------------------------===// - -/// Every symbol an object or symbol refers to. -static void referencedSyms(const SymInfo &info, std::vector &out) { - for (const Target &target : info.targets) - if (target.offset.var != ZeroSym) - out.push_back(target.offset.var); - if (info.pointerBehind != ZeroSym) - out.push_back(info.pointerBehind); - if (info.linear && info.linear->var != ZeroSym) - out.push_back(info.linear->var); - if (info.unwrapped && info.unwrapped->var != ZeroSym) - out.push_back(info.unwrapped->var); - // The operands of the operation that computed it, which a witness spells - // when no C place holds the value (RFC 0031 §5.3). - if (info.defined) { - if (info.defined->left != ZeroSym) - out.push_back(info.defined->left); - if (info.defined->right != ZeroSym) - out.push_back(info.defined->right); - } -} - -std::vector -Heap::reachableObjects(const HeapState &state, - const std::vector &roots) const { - std::set seen; - std::deque work; - auto visitSym = [&](Sym sym, auto &self) -> void { - const SymInfo &value = info(state, sym); - for (const Target &target : value.targets) - if (seen.insert(target.object).second) - work.push_back(target.object); - if (value.pointerBehind != ZeroSym) - self(value.pointerBehind, self); - }; - for (ObjectId root : roots) - if (seen.insert(root).second) - work.push_back(root); - for (const auto &[handle, sym] : state.exprs) - visitSym(sym, visitSym); - if (state.result != ZeroSym) - visitSym(state.result, visitSym); - while (!work.empty()) { - ObjectId id = work.front(); - work.pop_front(); - const ObjectState *object = state.objects.find(id); - if (object == nullptr) - continue; - // A released object's storage holds nothing any more. - if (object->life == Life::Released) - continue; - for (const auto &[key, sym] : object->cells) - visitSym(sym, visitSym); - for (const Segment &segment : object->segments) - visitSym(segment.value, visitSym); - for (ObjectId candidate : object->candidates) - if (seen.insert(candidate).second) - work.push_back(candidate); - } - return {seen.begin(), seen.end()}; -} - -/// Calls `visit` on every symbol field of `value`, in a fixed order, and -/// leaves each zero, so what remains compares by value. -template -static void stripSyms(SymInfo &value, Visit visit) { - auto sym = [&](Sym &field) { - visit(field); - field = ZeroSym; - }; - auto term = [&](Term &field) { sym(field.var); }; - if (value.condition) { - sym(value.condition->left); - sym(value.condition->right); - } - sym(value.pointerBehind); - if (value.productAtMost) - sym(value.productAtMost->first); - for (Target &target : value.targets) - term(target.offset); - for (Sym &ancestor : value.ancestors) - sym(ancestor); - for (PendingCase &pending : value.pending) { - sym(pending.subject); - sym(pending.stored); - sym(pending.previous); - if (pending.argumentZero) - sym(pending.argumentZero->first); - } - if (value.linear) - term(*value.linear); - if (value.unwrapped) - term(*value.unwrapped); - if (value.defined) { - sym(value.defined->left); - sym(value.defined->right); - } -} - -template -static void stripSyms(ObjectState &object, Visit visit) { - auto sym = [&](Sym &field) { - visit(field); - field = ZeroSym; - }; - auto term = [&](Term &field) { sym(field.var); }; - PMap cells; - for (const auto &[key, value] : object.cells) { - Sym held = value; - sym(held); - cells.set(key, ZeroSym); - } - object.cells = std::move(cells); - for (Segment &segment : object.segments) { - term(segment.from); - term(segment.to); - sym(segment.value); - sym(segment.copied); - } - if (object.extent) { - term(object.extent->bytes); - if (object.extent->unwrapped) - term(*object.extent->unwrapped); - } - if (object.nulWithin) - term(*object.nulWithin); - if (object.nulFrom) - term(*object.nulFrom); - sym(object.releasedBy); -} - -// NOLINTNEXTLINE(readability-convert-member-functions-to-static): Heap API -bool Heap::equivalent(const HeapState &a, const HeapState &b) const { - if (a.unreachable != b.unreachable || a.syms.size() != b.syms.size() || - a.objects.size() != b.objects.size() || - a.exprs.size() != b.exprs.size() || - a.pointerFacts.size() != b.pointerFacts.size() || - !(a.entryTests == b.entryTests)) - return false; - std::map forward; - std::map backward; - std::vector> work; - auto unify = [&](Sym x, Sym y) { - if (x == ZeroSym || y == ZeroSym) - return x == y; - auto f = forward.find(x); - auto g = backward.find(y); - if (f != forward.end() || g != backward.end()) - return f != forward.end() && g != backward.end() && f->second == y && - g->second == x; - forward.emplace(x, y); - backward.emplace(y, x); - work.emplace_back(x, y); - return true; - }; - auto unifyAll = [&](const std::vector &xs, const std::vector &ys) { - if (xs.size() != ys.size()) - return false; - for (std::size_t i = 0; i < xs.size(); ++i) - if (!unify(xs[i], ys[i])) - return false; - return true; - }; - // Objects, cell by cell (a selected cell's key names a symbol, and its - // order depends on the numbering: such an object compares as is). - auto ia = a.objects.begin(); - auto ib = b.objects.begin(); - for (; ia != a.objects.end(); ++ia, ++ib) { - if (ia->first != ib->first) - return false; - bool selected = false; - for (const auto &[key, value] : ia->second.cells) - selected = selected || key.isSelected(); - for (const auto &[key, value] : ib->second.cells) - selected = selected || key.isSelected(); - if (selected) { - if (!(ia->second == ib->second)) - return false; - for (const auto &[key, value] : ia->second.cells) - if (!unify(value, value)) - return false; - continue; - } - ObjectState x = ia->second; - ObjectState y = ib->second; - std::vector xs; - std::vector ys; - stripSyms(x, [&](Sym s) { xs.push_back(s); }); - stripSyms(y, [&](Sym s) { ys.push_back(s); }); - if (!(x == y) || !unifyAll(xs, ys)) - return false; - } - auto ea = a.exprs.begin(); - auto eb = b.exprs.begin(); - for (; ea != a.exprs.end(); ++ea, ++eb) - if (ea->first != eb->first || !unify(ea->second, eb->second)) - return false; - if (!unify(a.result, b.result)) - return false; - // What each symbol pair says, and the symbols that names. - // NOLINTNEXTLINE(modernize-loop-convert): unifying appends to the work list - for (std::size_t done = 0; done < work.size(); ++done) { - auto [x, y] = work[done]; - const SymInfo *ix = a.syms.find(x); - const SymInfo *iy = b.syms.find(y); - if (ix == nullptr || iy == nullptr) { - if (ix != iy) - return false; - continue; - } - SymInfo cx = *ix; - SymInfo cy = *iy; - std::vector xs; - std::vector ys; - stripSyms(cx, [&](Sym s) { xs.push_back(s); }); - stripSyms(cy, [&](Sym s) { ys.push_back(s); }); - if (!(cx == cy) || !unifyAll(xs, ys)) - return false; - } - // Every symbol accounted for, and the zone and the facts the same over - // the correspondence. - if (forward.size() != a.syms.size()) - return false; - for (Sym s : a.zone.symbols()) - if (s != ZeroSym && !forward.contains(s)) - return false; - Zone mapped = a.zone.renamed([&](Sym s) { - auto it = forward.find(s); - return it != forward.end() ? it->second : ZeroSym; - }); - if (!(mapped == b.zone)) - return false; - std::vector facts; - for (const PointerFact &fact : a.pointerFacts) { - auto first = forward.find(fact.first); - auto second = forward.find(fact.second); - if (first == forward.end() || second == forward.end()) - return false; - PointerFact renamedFact{.first = std::min(first->second, second->second), - .second = std::max(first->second, second->second), - .equal = fact.equal}; - facts.push_back(renamedFact); - } - std::ranges::sort(facts); - if (!(facts == b.pointerFacts)) - return false; - if (a.mergedLoads.size() != b.mergedLoads.size()) - return false; - for (std::size_t i = 0; i < a.mergedLoads.size(); ++i) { - MergedLoad x = a.mergedLoads[i]; - const MergedLoad &y = b.mergedLoads[i]; - auto map = [&](Sym s) { - if (s == ZeroSym) - return ZeroSym; - auto it = forward.find(s); - return it != forward.end() ? it->second : Sym{~0U}; - }; - x.firstValue = map(x.firstValue); - x.secondValue = map(x.secondValue); - x.merged = map(x.merged); - if (!(x == y)) - return false; - } - return true; -} - -void Heap::collect(HeapState &state, const std::vector &roots, - const std::function &leaked) const { - if (state.unreachable) - return; - std::vector reachable = reachableObjects(state, roots); - std::set keep(reachable.begin(), reachable.end()); - std::vector drop; - for (const auto &[id, object] : state.objects) - if (!keep.contains(id)) - drop.push_back(id); - for (ObjectId id : drop) { - const ObjectInfo &objectInfo = table.info(id); - ObjectState object = *state.objects.find(id); - ObjectKind kind = objectInfo.key.kind; - bool released = isReleasedLife(object.life) || object.effectReleased || - object.effectMayReleased; - switch (kind) { - case ObjectKind::Entry: - case ObjectKind::EntrySummary: - case ObjectKind::CallResult: - case ObjectKind::Focus: - case ObjectKind::Materialized: - // Entry objects stay for the summary; once released and - // unreachable they become dead copies (§4.6). - if (objectInfo.key.dead) - continue; - if (!released && kind != ObjectKind::Focus && - kind != ObjectKind::Materialized) - continue; - state.objects.erase(id); - if (released) { - ObjectId dead = table.deadCopy(id); - object.cells = {}; - object.segments.clear(); - if (const ObjectState *existing = state.objects.find(dead)) { - ObjectState merged = *existing; - merged.effectReleased = - merged.effectReleased || object.effectReleased; - merged.effectMayReleased = - merged.effectMayReleased || object.effectMayReleased; - merged.life = - merged.life == object.life ? merged.life : Life::MayReleased; - if (object.record && merged.record) - merged.record = joinRecords(*merged.record, *object.record); - else if (object.record) - merged.record = object.record; - for (ObjectId candidate : object.candidates) - if (std::ranges::find(merged.candidates, candidate) == - merged.candidates.end()) - merged.candidates.push_back(candidate); - merged.releasedBy = ZeroSym; - state.objects.set(dead, merged); - } else { - object.releasedBy = ZeroSym; - state.objects.set(dead, object); - } - } - break; - case ObjectKind::HeapRecent: - case ObjectKind::HeapOld: - if (object.owned && !isReleasedLife(object.life) && !object.escaped && - object.life != Life::Ended && leaked) - leaked(id); - state.objects.erase(id); - break; - case ObjectKind::Local: - case ObjectKind::Global: - case ObjectKind::Literal: - case ObjectKind::Function: - case ObjectKind::Unknown: - if (object.life == Life::Ended || kind == ObjectKind::Literal || - kind == ObjectKind::Function) - state.objects.erase(id); - break; - } - } - // Symbols nothing refers to are dropped with their zone rows. - std::set live; - std::vector pending; - auto hold = [&](Sym sym) { - if (sym != ZeroSym && live.insert(sym).second) - pending.push_back(sym); - }; - for (const auto &[id, object] : state.objects) { - for (const auto &[key, sym] : object.cells) { - hold(sym); - hold(key.index); - } - for (const Segment &segment : object.segments) { - hold(segment.value); - hold(segment.from.var); - hold(segment.to.var); - } - if (object.extent && object.extent->bytes.var != ZeroSym) - hold(object.extent->bytes.var); - if (object.extent && object.extent->unwrapped && - object.extent->unwrapped->var != ZeroSym) - hold(object.extent->unwrapped->var); - if (object.nulWithin && object.nulWithin->var != ZeroSym) - hold(object.nulWithin->var); - if (object.nulFrom && object.nulFrom->var != ZeroSym) - hold(object.nulFrom->var); - hold(object.releasedBy); - } - for (const auto &[handle, sym] : state.exprs) - hold(sym); - hold(state.result); - while (!pending.empty()) { - Sym sym = pending.back(); - pending.pop_back(); - std::vector refs; - const SymInfo &value = info(state, sym); - referencedSyms(value, refs); - for (Sym ancestor : value.ancestors) - refs.push_back(ancestor); - if (value.condition) { - refs.push_back(value.condition->left); - if (!value.condition->rightIsConstant) - refs.push_back(value.condition->right); - } - for (const PendingCase &pendingCase : value.pending) { - refs.push_back(pendingCase.subject); - refs.push_back(pendingCase.stored); - refs.push_back(pendingCase.previous); - } - for (Sym ref : refs) - hold(ref); - } - state.syms.eraseIf( - [&](Sym sym, const SymInfo &) { return !live.contains(sym); }); - state.zone.restrict([&](Sym sym) { return live.contains(sym); }); - std::erase_if(state.nullFollows, [&](const std::pair &link) { - return !live.contains(link.first); - }); -} - -//===----------------------------------------------------------------------===// -// Join and widening (§4.8) -//===----------------------------------------------------------------------===// - -/// Whether `sym` (with attributes `info`) is known in `state` not to equal -/// `c`: an excluded constant, or a zone bound that leaves it out. -static bool excludes(const HeapState &state, Sym sym, const SymInfo &info, - std::int64_t c) { - if ((c == 0 && info.nonZero) || std::ranges::binary_search(info.excluded, c)) - return true; - auto lo = state.zone.lower(sym); - auto hi = state.zone.upper(sym); - return (lo && *lo > c) || (hi && *hi < c); -} - -/// RFC 0034 §6.2: the constants a joined value is known not to equal are -/// those both sides exclude (by their own sets or their zone bounds). -static std::vector joinExcluded(const HeapState &left, Sym a, - const SymInfo &aInfo, - const HeapState &right, Sym b, - const SymInfo &bInfo) { - std::vector out; - for (std::int64_t c : aInfo.excluded) - if (excludes(right, b, bInfo, c)) - out.push_back(c); - for (std::int64_t c : bInfo.excluded) - if (!std::ranges::binary_search(aInfo.excluded, c) && - excludes(left, a, aInfo, c)) - out.push_back(c); - std::ranges::sort(out); - if (out.size() > MaxExcluded) - out.resize(MaxExcluded); - return out; -} - -namespace { -/// The pairing of two states' symbols into result symbols. -class Pairing { -public: - Pairing(const Heap &heap, HeapState &left, HeapState &right, HeapState &out, - Sym keepBelow = ZeroSym) - : heap(heap), left(left), right(right), out(out), keepBelow(keepBelow) { - out.nextSym = std::max(out.nextSym, keepBelow); - } - - /// The result symbol for `(a, b)`; zero on a side means the side has no - /// such value. - Sym pair(Sym a, Sym b) { - if (a == ZeroSym && b == ZeroSym) - return ZeroSym; - const std::uint64_t key = keyOf(a, b); - if (auto it = results.find(key); it != results.end()) - return it->second; - Sym result = a == b && a < keepBelow ? a : out.nextSym++; - results.emplace(key, result); - if (a != ZeroSym && b != ZeroSym) { - bothLeft.try_emplace(a, result); - bothRight.try_emplace(b, result); - } - pairs.push_back(SymPair{.result = result, - .left = a, - .right = b, - .hasLeft = a != ZeroSym, - .hasRight = b != ZeroSym}); - return result; - } - - /// `pair` for a cell of an object both sides have that one side never - /// wrote (it reads the entry value there, or nothing): a release of the - /// value happened on the other side's paths only. - Sym pairOneSided(Sym a, Sym b) { - Sym result = pair(a, b); - oneSidedCells.insert(result); - return result; - } - - Term pairTerm(const Term &a, const Term *b) { - if (!a.known) - return a; - if (b != nullptr) { - if (!b->known) - return Term::unknown(); - if (a.scale == b->scale && a.constant == b->constant && - (a.var == ZeroSym) == (b->var == ZeroSym)) { - if (a.var == ZeroSym) - return a; - return Term::ofSym(pair(a.var, b->var), a.scale, a.constant); - } - // §4.8: terms of different shapes (a cursor at offset 0 on one path - // and 1 on the other): one symbol per side equal to its term, joined, - // so the zone keeps the range and the relations of each. - Sym x = sideSym(left, a); - Sym y = sideSym(right, *b); - if (x == ZeroSym || y == ZeroSym) - return Term::unknown(); - return Term::ofSym(pair(x, y), 1, 0); - } - if (a.var == ZeroSym) - return a; - return Term::ofSym(onlyLeft(a.var), a.scale, a.constant); - } - Term pairTermRight(const Term &b) { - if (!b.known || b.var == ZeroSym) - return b; - return Term::ofSym(onlyRight(b.var), b.scale, b.constant); - } - /// A new integer symbol of `state` equal to `term` (bounded by it when - /// its scale is not 1). - Sym sideSym(HeapState &state, const Term &term) { - if (!term.known) - return ZeroSym; - SymInfo info; - info.type = SymInfo::Type::Int; - Sym sym = heap.fresh(state, info); - if (term.isConstant()) { - state.zone.addRange(sym, term.constant, term.constant); - } else if (term.scale == 1) { - state.zone.addEq(sym, term.var, term.constant); - } else { - TermRange range = rangeOf(state.zone, term); - auto fit = [](std::optional<__int128> v) -> std::optional { - if (v && *v >= INT64_MIN && *v <= INT64_MAX) - return static_cast(*v); - return std::nullopt; - }; - state.zone.addRange(sym, fit(range.lo), fit(range.hi)); - } - return sym; - } - /// The result for a value only one side's term uses (an object or target - /// only that side has): a result that already pairs it with the other - /// side equals it on every path of its side, and the term holds on no - /// other path, so sharing it keeps the term related to the cells that - /// hold the value (an entry object's extent and the count field it was - /// read from). - Sym onlyLeft(Sym a) { - auto it = bothLeft.find(a); - return it != bothLeft.end() ? it->second : pair(a, ZeroSym); - } - Sym onlyRight(Sym b) { - auto it = bothRight.find(b); - return it != bothRight.end() ? it->second : pair(ZeroSym, b); - } - - /// Computes the attributes of every result symbol. - void finish(bool widen) { - // (Joining attributes may pair more symbols: `pairs` grows meanwhile.) - for (; finished < pairs.size(); ++finished) { - SymPair p = pairs[finished]; - out.syms.set(p.result, joinInfo(p, widen)); - } - } - - /// The result symbol of `(a, b)` when the join made one. - [[nodiscard]] Sym find(Sym a, Sym b) const { - auto it = results.find(keyOf(a, b)); - return it != results.end() ? it->second : ZeroSym; - } - - /// The unique result symbol a left (or right) symbol maps to, if unique. - Sym uniqueLeft(Sym a) const { return unique(a, true); } - Sym uniqueRight(Sym b) const { return unique(b, false); } - - std::vector pairs; - -private: - const Heap &heap; - HeapState &left; - HeapState &right; - HeapState &out; - /// `Heap::join`'s `keepBelow`. - Sym keepBelow = ZeroSym; - static std::uint64_t keyOf(Sym a, Sym b) { - return (static_cast(a) << 32U) | b; - } - std::unordered_map results; - /// The first result pairing each side's symbol with the other side's. - std::unordered_map bothLeft; - std::unordered_map bothRight; - /// Results of `pairOneSided`. - std::unordered_set oneSidedCells; - /// The pairs whose attributes `finish` has computed. - std::size_t finished = 0; - - Sym unique(Sym sym, bool isLeft) const { - Sym found = ZeroSym; - for (const SymPair &p : pairs) { - Sym side = isLeft ? p.left : p.right; - if (side != sym) - continue; - if (found != ZeroSym) - return ZeroSym; - found = p.result; - } - return found; - } - - SymInfo joinInfo(const SymPair &p, bool widen) { - // (Pairing terms may add symbols to either side; a map holds each - // symbol's attributes in a box of its own, which that leaves in place.) - static_assert(PMap::StableValues); - const SymInfo *a = p.hasLeft ? left.syms.find(p.left) : nullptr; - const SymInfo *b = p.hasRight ? right.syms.find(p.right) : nullptr; - static const SymInfo None; - if (p.hasLeft && a == nullptr) - a = &None; - if (p.hasRight && b == nullptr) - b = &None; - if (a == nullptr || b == nullptr) { - // (`pair` makes no pair without a side.) - // NOLINTNEXTLINE(clang-analyzer-core.NonNullParamChecker): one is set - SymInfo joined = a == nullptr ? renamed(*b, false) : renamed(*a, true); - if (joined.release && oneSidedCells.contains(p.result)) - joined.release->allPaths = false; - return joined; - } - SymInfo joined; - joined.type = a->type == b->type ? a->type : SymInfo::Type::Unknown; - joined.name = !a->name.empty() ? a->name : b->name; - // The same cell's entry value on both sides. - if (a->entryOf == b->entryOf) - joined.entryOf = a->entryOf; - uniteEntryOrigins(joined, *a, *b); - if (joined.type == SymInfo::Type::Int) { - joined.intType = a->intType ? a->intType : b->intType; - joined.nonZero = a->nonZero && b->nonZero; - // (Nothing to do, the common case, when neither side excludes one.) - if (!a->excluded.empty() || !b->excluded.empty()) - joined.excluded = joinExcluded(left, p.left, *a, right, p.right, *b); - joined.ctype = a->ctype == b->ctype ? a->ctype : 0; - // §4.4: intervals beyond the zone join as their hull; a widening - // forgets one that grew (the type's range). - if (a->values && b->values && a->values->type == b->values->type && - (!widen || *a->values == *b->values)) - joined.values = a->values->united(*b->values); - // §5.3: a value both sides computed by the same operation is that - // operation of the paired operands on every path. - // (Only over operands both sides still hold, so a definition never - // keeps a chain of earlier values alive.) - auto held = [](const HeapState &state, Sym sym) { - return sym == ZeroSym || state.syms.contains(sym); - }; - if (a->defined && b->defined && a->defined->op == b->defined->op && - a->defined->constant == b->defined->constant && - (a->defined->right == ZeroSym) == (b->defined->right == ZeroSym) && - joined.ctype != 0 && held(left, a->defined->left) && - held(left, a->defined->right) && held(right, b->defined->left) && - held(right, b->defined->right)) - joined.defined = SymDefinition{ - .op = a->defined->op, - .left = pair(a->defined->left, b->defined->left), - .right = pair(a->defined->right, b->defined->right), - .constant = a->defined->constant, - .exact = a->defined->exact && b->defined->exact, - .leftValue = a->defined->leftValue == b->defined->leftValue - ? a->defined->leftValue - : std::nullopt}; - if (a->linear && b->linear && a->linear->scale == b->linear->scale && - a->linear->constant == b->linear->constant) { - Term linear = pairTerm(*a->linear, &*b->linear); - if (linear.known && !linear.isConstant()) - joined.linear = linear; - } - if (a->unwrapped && b->unwrapped && - a->unwrapped->scale == b->unwrapped->scale && - a->unwrapped->constant == b->unwrapped->constant) { - Term unwrapped = pairTerm(*a->unwrapped, &*b->unwrapped); - if (unwrapped.known && !unwrapped.isConstant()) - joined.unwrapped = unwrapped; - } - } else if (joined.type == SymInfo::Type::Pointer) { - joined.top = a->top || b->top; - if (!joined.top) { - std::map merged; - std::set both; - for (const Target &t : a->targets) - for (const Target &u : b->targets) - if (t.object == u.object) { - both.insert(t.object); - merged[t.object] = pairTerm(t.offset, &u.offset); - } - for (const Target &t : a->targets) - if (!both.contains(t.object)) - merged[t.object] = pairTerm(t.offset, nullptr); - for (const Target &u : b->targets) - if (!both.contains(u.object)) - merged[u.object] = pairTermRight(u.offset); - for (const auto &[object, offset] : merged) - joined.targets.push_back(Target{.object = object, .offset = offset}); - if (joined.targets.size() > 8) { - joined.targets.clear(); - joined.top = true; - } - } - joined.null = a->null == b->null ? a->null : PointerNull::Maybe; - joined.allocatorSource = a->allocatorSource || b->allocatorSource; - joined.nullOrigin = joinNullOrigins(*a, *b); - if (a->release && b->release) { - joined.release = joinRecords(*a->release, *b->release); - } else if (a->release || b->release) { - ReleaseRecord record = a->release ? *a->release : *b->release; - record.allPaths = false; - joined.release = record; - } - // RFC 0033 §2: as in `mergeWeak`. - joined.raw = a->raw && b->raw; - joined.rawSome = a->rawSome || b->rawSome || a->raw != b->raw; - joined.rawAt = a->raw ? a->rawAt : b->rawAt; - joined.rawOrigin = a->raw ? a->rawOrigin : b->rawOrigin; - joined.rawFrom = a->raw ? a->rawFrom : b->rawFrom; - joined.rawVia = a->raw ? a->rawVia : b->rawVia; - joined.rawCast = a->rawCast || b->rawCast || a->raw != b->raw; - if (a->shares && b->shares && *a->shares == *b->shares) - joined.shares = a->shares; - joined.uninit = a->uninit && b->uninit; - joined.mayUninit = a->uninit || b->uninit || a->mayUninit || b->mayUninit; - joined.nullJoined = joinsNull(*a, *b); - joined.derived = a->derived && b->derived; - // Ancestors both sides agree on. - for (Sym ancestor : a->ancestors) { - Sym l = uniqueLeft(ancestor); - if (l == ZeroSym) - continue; - for (Sym other : b->ancestors) - if (uniqueRight(other) == l) - joined.ancestors.push_back(l); - } - } else if (joined.type == SymInfo::Type::Function) { - joined.functionsKnown = a->functionsKnown && b->functionsKnown; - if (joined.functionsKnown) { - joined.foreignFunctions = a->foreignFunctions; - joined.foreignFunctions.insert(joined.foreignFunctions.end(), - b->foreignFunctions.begin(), - b->foreignFunctions.end()); - std::ranges::sort(joined.foreignFunctions); - joined.foreignFunctions.erase( - std::ranges::unique(joined.foreignFunctions).begin(), - joined.foreignFunctions.end()); - joined.functions = a->functions; - joined.functions.insert(joined.functions.end(), b->functions.begin(), - b->functions.end()); - std::ranges::sort(joined.functions); - joined.functions.erase(std::ranges::unique(joined.functions).begin(), - joined.functions.end()); - if (joined.functions.size() > 32) { - joined.functions.clear(); - joined.functionsKnown = false; - } - } - } - (void)widen; - return joined; - } - - SymInfo renamed(const SymInfo &value, bool isLeft) { - SymInfo joined = value; - joined.condition.reset(); - joined.linear.reset(); - joined.unwrapped.reset(); - joined.defined.reset(); - joined.productAtMost.reset(); - joined.pending.clear(); - for (Target &target : joined.targets) - target.offset = isLeft ? pairTerm(target.offset, nullptr) - : pairTermRight(target.offset); - if (value.pointerBehind != ZeroSym) - joined.pointerBehind = isLeft ? pair(value.pointerBehind, ZeroSym) - : pair(ZeroSym, value.pointerBehind); - joined.ancestors.clear(); - for (Sym ancestor : value.ancestors) { - Sym mapped = isLeft ? uniqueLeft(ancestor) : uniqueRight(ancestor); - if (mapped != ZeroSym) - joined.ancestors.push_back(mapped); - } - return joined; - } -}; -} // namespace - -/// The join of two lives. -static Life joinLife(Life a, Life b) { - if (a == b) - return a; - if (a == Life::UnknownReleased || b == Life::UnknownReleased) - return Life::UnknownReleased; - if (a == Life::Ended || b == Life::Ended || a == Life::MayEnded || - b == Life::MayEnded) - return Life::MayEnded; - return Life::MayReleased; -} - -static std::optional joinObjectRecords(const ObjectState &a, - const ObjectState &b) { - if (a.record && b.record) - return joinRecords(*a.record, *b.record); - if (a.record || b.record) { - ReleaseRecord record = a.record ? *a.record : *b.record; - record.allPaths = false; - return record; - } - return std::nullopt; -} - -/// The cells of one side the alignment leaves alone (matched across the -/// join). -using CellSet = std::set>; - -/// Fills the concrete cells one state lacks for objects both states have -/// (§4.8). Summary cells hold what stores through unknown indices wrote, -/// so one missing on a side is nothing written there and needs no value; -/// selected cells were aligned before (`alignSelected`). -static void alignCells(const Heap &heap, HeapState &left, HeapState &right, - const CellSet &skipLeft, const CellSet &skipRight) { - for (int round = 0; round < 4; ++round) { - bool changed = false; - std::vector shared; - for (const auto &[id, object] : left.objects) - if (right.objects.contains(id)) - shared.push_back(id); - for (ObjectId id : shared) { - std::vector> onlyLeft; - std::vector> onlyRight; - const ObjectState &l = *left.objects.find(id); - const ObjectState &r = *right.objects.find(id); - for (const auto &[key, sym] : l.cells) - if (key.isConcrete() && !r.cells.contains(key) && - !skipLeft.contains({id, key})) - onlyLeft.emplace_back(key, sym); - for (const auto &[key, sym] : r.cells) - if (key.isConcrete() && !l.cells.contains(key) && - !skipRight.contains({id, key})) - onlyRight.emplace_back(key, sym); - // An element the other side's ranges cannot tell apart gives way: - // its value joins its own side's ranges instead (§4.2 *Amendment - // (arrays)*). - auto vague = [&](const HeapState &other, const CellKey &key) { - const ObjectState &object = *other.objects.find(id); - for (const Segment &segment : object.segments) { - Order in = inRange(other.zone, segment.position, segment.from, - segment.to, key.byteTerm()); - if (in == Order::Proven) - return false; - if (in == Order::Unknown) - return true; - } - return false; - }; - // (A cell an earlier materialisation wrote meanwhile, such as the - // count field an entry pointer's extent reads, keeps its value.) - for (const auto &[key, sym] : onlyLeft) { - if (right.objects.find(id)->cells.contains(key)) - continue; - if (vague(right, key) && left.objects.find(id)->stride != 0) { - heap.evictCell(left, id, key); - } else { - const SymInfo &hint = heap.info(left, sym); - heap.load(right, id, key, hint); - } - changed = true; - } - for (const auto &[key, sym] : onlyRight) { - if (left.objects.find(id)->cells.contains(key)) - continue; - if (vague(left, key) && right.objects.find(id)->stride != 0) { - heap.evictCell(right, id, key); - } else { - const SymInfo &hint = heap.info(right, sym); - heap.load(left, id, key, hint); - } - changed = true; - } - } - if (!changed) - return; - } -} - -/// The objects both states have. -static std::vector sharedObjects(const HeapState &left, - const HeapState &right) { - std::vector shared; - for (const auto &[id, object] : left.objects) - if (right.objects.contains(id)) - shared.push_back(id); - return shared; -} - -/// §4.2 *Amendment (arrays)*: at a loop head, the element cells the -/// iteration changed (on the back edge `right`, against the head's `left`) -/// fold into segments on both sides, so a cell written at the induction -/// variable becomes a range that grows with it. -static void foldChangedElements(const Heap &heap, HeapState &left, - HeapState &right) { - for (ObjectId id : sharedObjects(left, right)) { - // (Reads through `find`: `at` copies an object another state shares.) - const std::uint32_t leftStride = left.objects.find(id)->stride; - const std::uint32_t rightStride = right.objects.find(id)->stride; - std::uint32_t stride = leftStride != 0 ? leftStride : rightStride; - if (stride == 0) - continue; - if (leftStride != stride) - left.objects.at(id).stride = stride; - if (rightStride != stride) - right.objects.at(id).stride = stride; - // Changed: new, or a value with other facts (another value, a - // release). Symbols are numbered per state (a join renumbers them), so - // the numbers are no evidence either way; a cell whose value has the - // same facts stays a cell, and the join pairs its two values, which is - // sound whether or not they are one value. - std::vector changed; - const PMap &leftCells = left.objects.find(id)->cells; - for (const auto &[key, sym] : right.objects.find(id)->cells) { - if (key.isSummary()) - continue; - const Sym *before = leftCells.find(key); - if (before == nullptr || - !(heap.info(left, *before) == heap.info(right, sym))) - changed.push_back(key); - } - // The head's own cell for the element gives way to the range the - // iteration grows: its value joins the ranges that hold it. - for (const CellKey &key : changed) - if (heap.foldCell(right, id, key) && - left.objects.find(id)->cells.contains(key)) - heap.evictCell(left, id, key); - } -} - -/// Selected cells on one side only: materialised on the other side when a -/// variable holds their index symbol on both sides (the same value there), -/// else evicted (their value becomes a weak write). Symbols are numbered -/// per state, so a number both states have is otherwise no evidence of one -/// value. -static void alignSelected(const Heap &heap, HeapState &left, HeapState &right, - const CellSet &skipLeft, const CellSet &skipRight, - const std::vector> &pairs) { - std::set shared; - for (const auto &[a, b] : pairs) - if (a == b) - shared.insert(a); - for (ObjectId id : sharedObjects(left, right)) { - // (Reads through `find`: `at` copies an object another state shares.) - auto cellsOf = [&](const HeapState &state) -> const PMap & { - return state.objects.find(id)->cells; - }; - std::vector> onlyLeft; - std::vector> onlyRight; - for (const auto &[key, sym] : cellsOf(left)) - if (key.isSelected() && !cellsOf(right).contains(key) && - !skipLeft.contains({id, key})) - onlyLeft.emplace_back(key, sym); - for (const auto &[key, sym] : cellsOf(right)) - if (key.isSelected() && !cellsOf(left).contains(key) && - !skipRight.contains({id, key})) - onlyRight.emplace_back(key, sym); - auto align = [&](HeapState &from, HeapState &to, - const std::vector> &keys) { - for (const auto &[key, sym] : keys) { - if (shared.contains(key.index)) { - const SymInfo &hint = heap.info(from, sym); - heap.load(to, id, key, hint); - } else { - heap.evictCell(from, id, key); - } - } - }; - align(left, right, onlyLeft); - align(right, left, onlyRight); - // A cell a materialisation on the other side evicted (the bound on - // selected cells). - std::vector stale; - for (const auto &[key, sym] : cellsOf(left)) - if (key.isSelected() && !cellsOf(right).contains(key) && - !skipLeft.contains({id, key})) - stale.push_back(key); - for (const CellKey &key : stale) - heap.evictCell(left, id, key); - stale.clear(); - for (const auto &[key, sym] : cellsOf(right)) - if (key.isSelected() && !cellsOf(left).contains(key) && - !skipRight.contains({id, key})) - stale.push_back(key); - for (const CellKey &key : stale) - heap.evictCell(right, id, key); - } -} - -namespace { -/// How a bound of a joined segment is formed: a constant, or -/// `scale * s + value` for the result symbol `s` pairing `left` and -/// `right`. `leftTerm` and `rightTerm` are what it is on each side. -struct BoundSpec { - bool constant = true; - std::int64_t value = 0; - std::int64_t scale = 1; - Sym left = ZeroSym; - Sym right = ZeroSym; - Term leftTerm = Term::unknown(); - Term rightTerm = Term::unknown(); -}; - -/// A segment of the join: the segments it pairs (either may be absent: the -/// range is empty on that side) and its bounds. -struct SegmentMatch { - std::optional left; - std::optional right; - BoundSpec from; - BoundSpec to; -}; - -/// Matches the segments of one object across a join (§4.2 *Amendment -/// (arrays)*): bounds correspond when a pair of symbols the join pairs -/// anyway (the variables' values) equals them on each side. -class SegmentMatcher { -public: - SegmentMatcher(const Heap &heap, const HeapState &left, - const HeapState &right, - const std::vector> &pairs) - : heap(heap), left(left), right(right), pairs(pairs) {} - - /// The matches of `id`'s segments, or the segments to evict first. - /// Segments are pushed newest first, so per position the two lists are - /// aligned from their oldest ends; what one side has in front of the - /// aligned part is new there, and matches an empty range on the other - /// side or is evicted. - bool plan(ObjectId id, std::vector &out, - std::vector &evictLeft, - std::vector &evictRight) const { - const std::vector &ls = left.objects.find(id)->segments; - const std::vector &rs = right.objects.find(id)->segments; - std::set positions; - for (const Segment &segment : ls) - positions.insert(segment.position); - for (const Segment &segment : rs) - positions.insert(segment.position); - for (const CellKey &position : positions) { - std::vector li; - std::vector ri; - for (std::size_t i = 0; i < ls.size(); ++i) - if (ls[i].position == position) - li.push_back(i); - for (std::size_t j = 0; j < rs.size(); ++j) - if (rs[j].position == position) - ri.push_back(j); - // The aligned tails, oldest first. - std::vector tail; - std::size_t a = li.size(); - std::size_t b = ri.size(); - while (a > 0 && b > 0) { - const Segment &l = ls[li[a - 1]]; - const Segment &r = rs[ri[b - 1]]; - auto from = matchBound(l.from, r.from); - auto to = matchBound(l.to, r.to); - if (!from || !to) - break; - tail.push_back(SegmentMatch{ - .left = li[a - 1], .right = ri[b - 1], .from = *from, .to = *to}); - --a; - --b; - } - // The new ones in front: empty on the other side, or evicted. - for (std::size_t j = 0; j < b; ++j) { - if (auto bounds = matchEmpty(rs[ri[j]], false)) - out.push_back(SegmentMatch{.left = std::nullopt, - .right = ri[j], - .from = bounds->first, - .to = bounds->second}); - else - evictRight.push_back(ri[j]); - } - for (std::size_t i = 0; i < a; ++i) { - if (auto bounds = matchEmpty(ls[li[i]], true)) - out.push_back(SegmentMatch{.left = li[i], - .right = std::nullopt, - .from = bounds->first, - .to = bounds->second}); - else - evictLeft.push_back(li[i]); - } - out.insert(out.end(), tail.rbegin(), tail.rend()); - } - std::ranges::sort(evictLeft); - std::ranges::sort(evictRight); - return evictLeft.empty() && evictRight.empty(); - } - -private: - const Heap &heap; - const HeapState &left; - const HeapState &right; - const std::vector> &pairs; - - /// The ways the join can form a bound that is `bound` on one side. - std::vector candidates(const Term &bound, bool onLeft) const { - std::vector out; - if (!bound.known) - return out; - const HeapState &own = onLeft ? left : right; - if (bound.isConstant()) { - out.push_back(BoundSpec{.constant = true, - .value = bound.constant, - .scale = 0, - .left = ZeroSym, - .right = ZeroSym, - .leftTerm = Term::of(bound.constant), - .rightTerm = Term::of(bound.constant)}); - } - for (const auto &[a, b] : pairs) { - Sym mine = onLeft ? a : b; - std::optional offset = difference(own.zone, bound, mine); - if (!offset) - continue; - out.push_back(BoundSpec{.constant = false, - .value = *offset, - .scale = 1, - .left = a, - .right = b, - .leftTerm = Term::ofSym(a, 1, *offset), - .rightTerm = Term::ofSym(b, 1, *offset)}); - } - return out; - } - - /// `bound - sym` when the zone makes it a constant. - static std::optional difference(const Zone &zone, - const Term &bound, Sym sym) { - if (!bound.known) - return std::nullopt; - if (bound.isConstant()) { - auto value = zone.constant(sym); - if (!value || *value == INT64_MIN) - return std::nullopt; - return checkedAdd(bound.constant, -*value); - } - if (bound.scale != 1) - return std::nullopt; - if (bound.var == sym) - return bound.constant; - auto upper = zone.bound(bound.var, sym); - auto lower = zone.bound(sym, bound.var); - if (!upper || !lower || *lower == INT64_MIN || *upper != -*lower) - return std::nullopt; - return checkedAdd(*upper, bound.constant); - } - -public: - /// A bound that is `l` on the left and `r` on the right: one the join - /// pairs anyway, else the pair of the two bounds' own symbols. - std::optional matchBound(const Term &l, const Term &r) const { - if (auto spec = matchBoth(l, r)) - return spec; - if (!l.known || !r.known || l.isConstant() || r.isConstant() || - l.scale != r.scale || l.constant != r.constant) - return std::nullopt; - return BoundSpec{.constant = false, - .value = l.constant, - .scale = l.scale, - .left = l.var, - .right = r.var, - .leftTerm = l, - .rightTerm = r}; - } - - /// A bound that is `l` on the left and `r` on the right. - std::optional matchBoth(const Term &l, const Term &r) const { - for (const BoundSpec &spec : candidates(l, true)) { - std::optional same = heap.sameCell(right, spec.rightTerm, r); - if (same && *same) - return spec; - } - return std::nullopt; - } - -private: - /// Bounds for a segment of one side that are an empty range on the - /// other. - std::optional> - matchEmpty(const Segment &segment, bool onLeft) const { - const HeapState &other = onLeft ? right : left; - for (const BoundSpec &from : candidates(segment.from, onLeft)) - for (const BoundSpec &to : candidates(segment.to, onLeft)) { - const Term &start = onLeft ? from.rightTerm : from.leftTerm; - const Term &end = onLeft ? to.rightTerm : to.leftTerm; - if (heap.lessEqual(other, end, start).value_or(false)) - return std::make_pair(from, to); - } - return std::nullopt; - } -}; -} // namespace - -/// The integer values the join pairs anyway: the variables' cells. -static std::vector> variablePairs(const Heap &heap, - const ObjectTable &table, - const HeapState &left, - const HeapState &right) { - std::vector> pairs; - for (ObjectId id : sharedObjects(left, right)) { - ObjectKind kind = table.info(id).key.kind; - if (kind != ObjectKind::Local && kind != ObjectKind::Global) - continue; - const ObjectState &l = *left.objects.find(id); - const ObjectState &r = *right.objects.find(id); - for (const auto &[key, sym] : l.cells) { - if (!key.isConcrete()) - continue; - const Sym *other = r.cells.find(key); - if (other != nullptr && heap.info(left, sym).type == SymInfo::Type::Int && - heap.info(right, *other).type == SymInfo::Type::Int) - pairs.emplace_back(sym, *other); - } - } - return pairs; -} - -/// Plans the segments of every shared object, evicting those that match -/// nothing on the other side. -static std::map> -planSegments(const Heap &heap, const ObjectTable &table, HeapState &left, - HeapState &right) { - std::map> plans; - for (int round = 0; round < 8; ++round) { - plans.clear(); - bool evicted = false; - std::vector> pairs = - variablePairs(heap, table, left, right); - SegmentMatcher matcher(heap, left, right, pairs); - for (ObjectId id : sharedObjects(left, right)) { - if (left.objects.find(id)->segments.empty() && - right.objects.find(id)->segments.empty()) - continue; - std::vector matches; - std::vector evictLeft; - std::vector evictRight; - if (matcher.plan(id, matches, evictLeft, evictRight)) { - plans[id] = std::move(matches); - continue; - } - auto marks = [](const std::vector &indices, - std::size_t size) { - std::vector out(size, false); - for (std::size_t index : indices) - if (index < size) - out[index] = true; - return out; - }; - heap.evictSegments( - left, id, marks(evictLeft, left.objects.find(id)->segments.size())); - heap.evictSegments( - right, id, - marks(evictRight, right.objects.find(id)->segments.size())); - evicted = true; - } - if (!evicted) - return plans; - } - // Still unmatched: every segment goes. - plans.clear(); - for (ObjectId id : sharedObjects(left, right)) - for (HeapState *side : {&left, &right}) - heap.evictSegments( - *side, id, - std::vector(side->objects.find(id)->segments.size(), true)); - return plans; -} - -namespace { -/// Two element cells, one on each side, at indices the join pairs: on each -/// path the joined cell is that side's cell (§4.2 *Amendment (arrays)*). -/// The first iteration of a loop reads `a[0]` where later ones read -/// `a[i]`; matched, the two are one cell `a[i]` of the join. -struct CellMatch { - CellKey left; - CellKey right; - CellKey position; - BoundSpec index; -}; -} // namespace - -/// The element index of a cell at an object's stride, if it has one. -static std::optional> -strideIndex(const ObjectState &object, const CellKey &key) { - if (object.stride == 0 || key.isSummary()) - return std::nullopt; - if (key.isSelected() && key.stride % object.stride != 0) - return std::nullopt; - CellKey position = - CellKey{.offset = key.offset, .stride = object.stride, .index = ZeroSym} - .position(); - ElementIndex element = elementIndex(position, key.byteTerm()); - if (!element.at || !*element.at) - return std::nullopt; - return std::make_pair(position, element.index); -} - -static std::map> -matchCells(const Heap &heap, const ObjectTable &table, const HeapState &left, - const HeapState &right, CellSet &skipLeft, CellSet &skipRight) { - std::map> matches; - std::vector> pairs = - variablePairs(heap, table, left, right); - SegmentMatcher matcher(heap, left, right, pairs); - for (ObjectId id : sharedObjects(left, right)) { - const ObjectState &l = *left.objects.find(id); - const ObjectState &r = *right.objects.find(id); - if (l.stride == 0 || l.stride != r.stride) - continue; - std::vector onlyRight; - for (const auto &[key, sym] : r.cells) - if (!key.isSummary() && !l.cells.contains(key)) - onlyRight.push_back(key); - std::vector used(onlyRight.size(), false); - for (const auto &[key, sym] : l.cells) { - if (key.isSummary() || r.cells.contains(key)) - continue; - auto leftIndex = strideIndex(l, key); - if (!leftIndex) - continue; - for (std::size_t j = 0; j < onlyRight.size(); ++j) { - if (used[j]) - continue; - auto rightIndex = strideIndex(r, onlyRight[j]); - if (!rightIndex || rightIndex->first != leftIndex->first) - continue; - auto spec = matcher.matchBoth(leftIndex->second, rightIndex->second); - if (!spec || spec->constant) - continue; - used[j] = true; - matches[id].push_back(CellMatch{.left = key, - .right = onlyRight[j], - .position = leftIndex->first, - .index = *spec}); - skipLeft.emplace(id, key); - skipRight.emplace(id, onlyRight[j]); - break; - } - } - } - return matches; -} - -/// The joint of two object states (cells paired separately). -static ObjectState joinObjectAttributes(const ObjectState &a, - const ObjectState &b) { - ObjectState out; - out.life = joinLife(a.life, b.life); - out.record = joinObjectRecords(a, b); - if (!isReleasedLife(a.life)) - out.releaseOffset = b.releaseOffset; - else if (!isReleasedLife(b.life) || a.releaseOffset == b.releaseOffset) - out.releaseOffset = a.releaseOffset; - out.family = a.family == b.family ? a.family : std::string(); - // An object one side never made is owned where the other side made it. - out.absent = a.absent && b.absent; - out.owned = (a.owned || a.absent) && (b.owned || b.absent) && !out.absent; - out.escaped = a.escaped || b.escaped; - out.readonly = a.readonly && b.readonly; - out.zeroed = a.zeroed && b.zeroed; - out.uninitialised = a.uninitialised || b.uninitialised; - out.havocked = a.havocked || b.havocked; - out.forgotten.clear(); - out.mayForgotten.clear(); - if (!out.havocked) { - // Forgotten on both sides, or on one side only (there the other side's - // value still holds). - out.forgotten = intersectRanges(a.forgotten, b.forgotten); - for (const auto *ranges : - {&a.forgotten, &b.forgotten, &a.mayForgotten, &b.mayForgotten}) - for (const auto &range : *ranges) - addForgotten(out.mayForgotten, range.first, range.second); - } - out.stored = a.stored || b.stored; - out.effectReleased = a.effectReleased && b.effectReleased; - out.effectMayReleased = a.effectMayReleased || b.effectMayReleased || - a.effectReleased != b.effectReleased; - // For messages only: a name and a last use either side has. - out.holder = !a.holder.empty() ? a.holder : b.holder; - out.lastUse = a.lastUse != 0 ? a.lastUse : b.lastUse; - out.candidates = a.candidates; - for (ObjectId candidate : b.candidates) - if (std::ranges::find(out.candidates, candidate) == out.candidates.end()) - out.candidates.push_back(candidate); - std::ranges::sort(out.candidates); - return out; -} - -static HeapState combineStates(const Heap &heap, ObjectTable &table, - const HeapState &leftIn, - const HeapState &rightIn, Handle block, - bool widen, bool loopHead, - const std::vector &thresholds, - Sym keepBelow = ZeroSym) { - if (leftIn.unreachable) - return rightIn; - if (rightIn.unreachable) - return leftIn; - HeapState left = leftIn; - HeapState right = rightIn; - // Elements first (§4.2 *Amendment (arrays)*): what the loop body wrote - // becomes ranges, selected cells are aligned or evicted, segments are - // matched or evicted; then the concrete cells are aligned. - if (loopHead) - foldChangedElements(heap, left, right); - CellSet skipLeft; - CellSet skipRight; - std::map> cellMatches = - matchCells(heap, table, left, right, skipLeft, skipRight); - alignSelected(heap, left, right, skipLeft, skipRight, - variablePairs(heap, table, left, right)); - std::map> plans = - planSegments(heap, table, left, right); - alignCells(heap, left, right, skipLeft, skipRight); - HeapState out; - Pairing pairing(heap, left, right, out, keepBelow); - // A selected cell's key names its index symbol, which the join renames. - auto renameKey = [&](CellKey key, bool onLeft, bool onRight) { - if (key.isSelected()) - key.index = pairing.pair(onLeft ? key.index : ZeroSym, - onRight ? key.index : ZeroSym); - return key; - }; - auto boundTerm = [&](const BoundSpec &spec) { - if (spec.constant) - return Term::of(spec.value); - return Term::ofSym(pairing.pair(spec.left, spec.right), spec.scale, - spec.value); - }; - - // Focus objects (§4.6, *Amendment (S1)*): a variable pointing to one - // object on each side, different objects. - struct FocusPlan { - ObjectId holder; - CellKey key; - ObjectId leftObject; - ObjectId rightObject; - ObjectId focus; - }; - std::vector focusPlans; - auto singleTarget = [&](const HeapState &state, Sym sym) -> ObjectId { - const SymInfo &value = heap.info(state, sym); - if (value.type != SymInfo::Type::Pointer || value.top || - value.targets.size() != 1) - return 0; - return value.targets.front().object; - }; - auto focusable = [&](ObjectId id) { - ObjectKind kind = table.info(id).key.kind; - return kind == ObjectKind::Entry || kind == ObjectKind::Materialized || - kind == ObjectKind::Focus || kind == ObjectKind::CallResult; - }; - - std::set ids; - std::vector> oneSided; - for (const auto &[id, object] : left.objects) - ids.insert(id); - for (const auto &[id, object] : right.objects) - ids.insert(id); - for (ObjectId id : ids) { - const ObjectState *a = left.objects.find(id); - const ObjectState *b = right.objects.find(id); - ObjectState result; - if (a != nullptr && b != nullptr) { - result = joinObjectAttributes(*a, *b); - result.stride = a->stride != 0 ? a->stride : b->stride; - for (const auto &[key, sym] : b->cells) - if (!a->cells.contains(key) && !skipRight.contains({id, key})) - result.cells.set(renameKey(key, false, true), - pairing.pairOneSided(ZeroSym, sym)); - if (auto matched = cellMatches.find(id); matched != cellMatches.end()) - for (const CellMatch &match : matched->second) { - // The cell at `stride * (scale * s + value) + offset`. - Term index = boundTerm(match.index); - auto stride = static_cast(match.position.stride); - CellKey key{ - .offset = match.position.offset + (stride * index.constant), - .stride = static_cast(stride * index.scale), - .index = index.var}; - result.cells.set(key, pairing.pair(*a->cells.find(match.left), - *b->cells.find(match.right))); - } - if (auto plan = plans.find(id); plan != plans.end()) - for (const SegmentMatch &match : plan->second) { - const Segment &shape = - match.left ? a->segments[*match.left] : b->segments[*match.right]; - result.segments.push_back(Segment{ - .position = shape.position, - .from = boundTerm(match.from), - .to = boundTerm(match.to), - .value = pairing.pair( - match.left ? a->segments[*match.left].value : ZeroSym, - match.right ? b->segments[*match.right].value : ZeroSym)}); - } - for (const auto &[key, sym] : a->cells) { - if (skipLeft.contains({id, key})) - continue; - const Sym *other = b->cells.find(key); - Sym bs = other != nullptr ? *other : ZeroSym; - result.cells.set(renameKey(key, true, other != nullptr), - other != nullptr ? pairing.pair(sym, bs) - : pairing.pairOneSided(sym, bs)); - ObjectKind holderKind = table.info(id).key.kind; - if (other != nullptr && *other != sym && - (holderKind == ObjectKind::Local || - holderKind == ObjectKind::Global) && - key.isConcrete()) { - ObjectId lo = singleTarget(left, sym); - ObjectId ro = singleTarget(right, *other); - if (lo != 0 && ro != 0 && lo != ro && focusable(lo) && focusable(ro)) - focusPlans.push_back(FocusPlan{.holder = id, - .key = key, - .leftObject = lo, - .rightObject = ro, - .focus = 0}); - } - } - auto pairExtent = - [&](const std::optional &x, - const std::optional &y) -> std::optional { - if (!x || !y) - return std::nullopt; - Extent e; - e.bytes = pairing.pairTerm(x->bytes, &y->bytes); - e.cls = x->cls == y->cls ? x->cls : ExtentClass::LowerBound; - if (!e.bytes.known) - return std::nullopt; - if (x->unwrapped && y->unwrapped && - x->unwrapped->scale == y->unwrapped->scale && - x->unwrapped->constant == y->unwrapped->constant) { - Term unwrapped = pairing.pairTerm(*x->unwrapped, &*y->unwrapped); - if (unwrapped.known) - e.unwrapped = unwrapped; - } - return e; - }; - result.extent = pairExtent(a->extent, b->extent); - if (a->nulWithin && b->nulWithin) { - Term t = pairing.pairTerm(*a->nulWithin, &*b->nulWithin); - if (t.known) { - result.nulWithin = t; - if (a->nulFrom && b->nulFrom) { - Term from = pairing.pairTerm(*a->nulFrom, &*b->nulFrom); - if (from.known) - result.nulFrom = from; - } - } - } - if (a->releasedBy != ZeroSym && a->releasedBy == b->releasedBy) - result.releasedBy = ZeroSym; - } else { - const ObjectState *only = a != nullptr ? a : b; - bool isLeft = a != nullptr; - result = *only; - result.cells = {}; - // An entry object exists on every path; the side that never - // materialised it left it live (§4.6), so a release on the other - // side is a release on some paths only. - // A dead copy (§4.6) stands for the same object as its live version. - ObjectKind onlyKind = table.info(id).key.kind; - ObjectKey otherKey = table.info(id).key; - otherKey.dead = !otherKey.dead; - std::optional counterpart = table.lookup(otherKey); - const ObjectState *there = - counterpart ? (isLeft ? right : left).objects.find(*counterpart) - : nullptr; - bool releasedThere = there != nullptr && (there->life == Life::Released || - there->effectReleased); - if ((onlyKind == ObjectKind::Entry || - onlyKind == ObjectKind::EntrySummary) && - !releasedThere && - (result.life == Life::Released || result.effectReleased)) { - result.life = joinLife(result.life, Life::Live); - if (result.record) - result.record->allPaths = false; - result.effectMayReleased = true; - result.effectReleased = false; - } - // (A global or entry object exists on the other side too, unread - // there: its cells hold the values of that side's paths only.) - const bool implicit = onlyKind == ObjectKind::Global || - onlyKind == ObjectKind::Entry || - onlyKind == ObjectKind::EntrySummary; - for (const auto &[key, sym] : only->cells) { - const Sym onLeft = isLeft ? sym : ZeroSym; - const Sym onRight = isLeft ? ZeroSym : sym; - result.cells.set(renameKey(key, isLeft, !isLeft), - implicit ? pairing.pairOneSided(onLeft, onRight) - : pairing.pair(onLeft, onRight)); - } - for (Segment &segment : result.segments) { - segment.from = isLeft ? pairing.pairTerm(segment.from, nullptr) - : pairing.pairTermRight(segment.from); - segment.to = isLeft ? pairing.pairTerm(segment.to, nullptr) - : pairing.pairTermRight(segment.to); - segment.value = isLeft ? pairing.pair(segment.value, ZeroSym) - : pairing.pair(ZeroSym, segment.value); - // Renumbered: a copied range becomes the plain range its value - // describes. - segment.source = 0; - segment.copied = ZeroSym; - } - // The terms after every object's cells are paired (below). - result.extent.reset(); - result.nulWithin.reset(); - result.nulFrom.reset(); - oneSided.emplace_back(id, isLeft); - result.releasedBy = ZeroSym; - } - out.objects.set(id, std::move(result)); - } - // An object one side has: its extent and string fact over that side's - // values, related to the cells both sides hold them in (Pairing::onlyLeft). - for (const auto &[id, isLeft] : oneSided) { - const ObjectState &only = - isLeft ? *left.objects.find(id) : *right.objects.find(id); - ObjectState &result = out.objects.at(id); - if (only.extent) { - Extent e = *only.extent; - e.bytes = isLeft ? pairing.pairTerm(e.bytes, nullptr) - : pairing.pairTermRight(e.bytes); - if (e.unwrapped) { - Term unwrapped = isLeft ? pairing.pairTerm(*e.unwrapped, nullptr) - : pairing.pairTermRight(*e.unwrapped); - e.unwrapped = - unwrapped.known ? std::optional(unwrapped) : std::nullopt; - } - result.extent = e; - } - if (only.nulWithin) - result.nulWithin = isLeft ? pairing.pairTerm(*only.nulWithin, nullptr) - : pairing.pairTermRight(*only.nulWithin); - if (only.nulFrom) - result.nulFrom = isLeft ? pairing.pairTerm(*only.nulFrom, nullptr) - : pairing.pairTermRight(*only.nulFrom); - } - // Expression values, after every symbol the objects reach is numbered: - // a value one side still holds must not renumber the rest, or two - // states that differ only in it never compare equal at a loop head. - pairing.finish(widen); - std::set handles; - for (const auto &[handle, sym] : left.exprs) - handles.insert(handle); - for (const auto &[handle, sym] : right.exprs) - handles.insert(handle); - for (Handle handle : handles) { - const Sym *a = left.exprs.find(handle); - const Sym *b = right.exprs.find(handle); - out.exprs.set(handle, pairing.pair(a ? *a : ZeroSym, b ? *b : ZeroSym)); - } - if (left.result != ZeroSym || right.result != ZeroSym) - out.result = pairing.pair(left.result, right.result); - pairing.finish(widen); - - // Focus objects. - for (FocusPlan &plan : focusPlans) { - ObjectKey key; - key.kind = ObjectKind::Focus; - key.handle = block; - key.parent = plan.holder; - key.cell = plan.key.offset; - ObjectInfo info; - const ObjectInfo &l = table.info(plan.leftObject); - const ObjectInfo &r = table.info(plan.rightObject); - info.type = l.type == r.type ? l.type : 0; - info.singular = true; - info.name = l.name; - plan.focus = table.intern(key, info); - // The focus object's state pairs the two sides' objects. - const ObjectState *lo = left.objects.find(plan.leftObject); - const ObjectState *ro = right.objects.find(plan.rightObject); - if (lo == nullptr || ro == nullptr) - continue; - ObjectState focusState = joinObjectAttributes(*lo, *ro); - focusState.candidates.clear(); - auto addCandidates = [&](ObjectId id, const ObjectState &state) { - if (table.info(id).key.kind == ObjectKind::Focus) { - for (ObjectId c : state.candidates) - focusState.candidates.push_back(c); - } else { - focusState.candidates.push_back(id); - } - }; - addCandidates(plan.leftObject, *lo); - addCandidates(plan.rightObject, *ro); - std::ranges::sort(focusState.candidates); - focusState.candidates.erase( - std::ranges::unique(focusState.candidates).begin(), - focusState.candidates.end()); - focusState.candidates.erase( - std::ranges::remove(focusState.candidates, plan.focus).begin(), - focusState.candidates.end()); - std::set keys; - for (const auto &[k, s] : lo->cells) - keys.insert(k); - for (const auto &[k, s] : ro->cells) - keys.insert(k); - // The candidates keep their ranges; the focus object reads them - // through its candidates. - focusState.segments.clear(); - for (const CellKey &k : keys) { - const Sym *x = lo->cells.find(k); - const Sym *y = ro->cells.find(k); - focusState.cells.set(renameKey(k, x != nullptr, y != nullptr), - pairing.pair(x ? *x : ZeroSym, y ? *y : ZeroSym)); - } - if (lo->extent && ro->extent) { - Extent e; - e.bytes = pairing.pairTerm(lo->extent->bytes, &ro->extent->bytes); - e.cls = lo->extent->cls == ro->extent->cls ? lo->extent->cls - : ExtentClass::LowerBound; - if (e.bytes.known) - focusState.extent = e; - } - out.objects.set(plan.focus, focusState); - // The variable now points to the focus object. - const Sym *cell = out.objects.find(plan.holder)->cells.find(plan.key); - if (cell != nullptr) { - SymInfo &value = out.syms.at(*cell); - Term offset = Term::unknown(); - const SymInfo &a = heap.info( - left, *left.objects.find(plan.holder)->cells.find(plan.key)); - const SymInfo &b = heap.info( - right, *right.objects.find(plan.holder)->cells.find(plan.key)); - if (a.targets.front().offset == b.targets.front().offset && - a.targets.front().offset.isConstant()) - offset = a.targets.front().offset; - value.targets = {Target{.object = plan.focus, .offset = offset}}; - value.top = false; - } - } - pairing.finish(widen); - - // Existence guards: an owned allocation one side made and the other did - // not exists where a local's value tests as it did on the side that made - // it, when the two sides' values test differently. - { - auto zeroTest = [&](const HeapState &state, - Sym sym) -> std::optional { - const SymInfo *info = state.syms.find(sym); - if (info == nullptr) - return std::nullopt; - if (info->type == SymInfo::Type::Pointer) { - if (info->null == PointerNull::Null) - return true; - if (info->null == PointerNull::NonNull) - return false; - return std::nullopt; - } - if (info->type != SymInfo::Type::Int) - return std::nullopt; - auto lo = state.zone.lower(sym); - auto hi = state.zone.upper(sym); - if (lo && hi && *lo == 0 && *hi == 0) - return true; - if (info->nonZero || (lo && *lo > 0) || (hi && *hi < 0)) - return false; - return std::nullopt; - }; - std::map byResult; - for (const SymPair &p : pairing.pairs) - if (p.hasLeft && p.hasRight) - byResult[p.result] = &p; - // The locals' values that the two sides test differently. - std::vector> leftTests; - for (const auto &[id, object] : out.objects) { - if (table.info(id).key.kind != ObjectKind::Local) - continue; - for (const auto &[key, sym] : object.cells) { - auto p = byResult.find(sym); - if (p == byResult.end()) - continue; - auto l = zeroTest(left, p->second->left); - auto r = zeroTest(right, p->second->right); - if (l && r && *l != *r) - leftTests.emplace_back(sym, *l); - } - } - for (const auto &[id, isLeft] : oneSided) { - ObjectState &object = out.objects.at(id); - ObjectKind kind = table.info(id).key.kind; - // (A guard it had names the other state's symbols.) - object.existsIf.reset(); - object.existsIfEntry.reset(); - if (!object.owned || - (kind != ObjectKind::HeapRecent && kind != ObjectKind::HeapOld)) - continue; - // An entry test the side that made it took and the other did not. - const HeapState &made = isLeft ? left : right; - const HeapState &other = isLeft ? right : left; - for (const EntryTest &test : made.entryTests) { - EntryTest opposite = test; - opposite.zero = !opposite.zero; - if (std::ranges::binary_search(other.entryTests, opposite)) { - object.existsIfEntry = test; - break; - } - } - if (!leftTests.empty()) - object.existsIf = std::make_pair(leftTests.front().first, - isLeft ? leftTests.front().second - : !leftTests.front().second); - } - // One made on both sides keeps a guard both had over paired symbols. - std::vector> kept; - for (const auto &[id, object] : out.objects) { - const ObjectState *a = left.objects.find(id); - const ObjectState *b = right.objects.find(id); - if (a == nullptr || b == nullptr || (!a->existsIf && !b->existsIf)) - continue; - if (a->existsIf && b->existsIf && - a->existsIf->second == b->existsIf->second) - if (Sym paired = pairing.find(a->existsIf->first, b->existsIf->first)) - kept.emplace_back(id, paired); - } - for (const auto &[id, paired] : kept) - out.objects.at(id).existsIf = - std::make_pair(paired, left.objects.find(id)->existsIf->second); - // An entry guard both sides had. - for (const auto &[id, object] : out.objects) { - const ObjectState *a = left.objects.find(id); - const ObjectState *b = right.objects.find(id); - if (a != nullptr && b != nullptr && a->existsIfEntry && - a->existsIfEntry == b->existsIfEntry) - kept.emplace_back(id, ZeroSym); - } - for (const auto &[id, paired] : kept) - if (paired == ZeroSym) - out.objects.at(id).existsIfEntry = left.objects.find(id)->existsIfEntry; - } - - // Cells stored on exactly the paths of one entry test (a lazy - // initialisation, §6.2): one side stored the cell under a test whose - // opposite the other side took, leaving the entry value there. - { - // Whether a side's cell holds a value this activation stored (false: - // its entry value; none: unknown). - auto stored = [&](const HeapState &state, ObjectId id, - const CellKey &key) -> std::optional { - const ObjectState *object = state.objects.find(id); - if (object == nullptr) - return false; - const Sym *held = object->cells.find(key); - if (held == nullptr) - return object->forgets(key) ? std::nullopt : std::optional(false); - const SymInfo *info = state.syms.find(*held); - if (info == nullptr) - return std::nullopt; - return !(info->entryOf && *info->entryOf == std::make_pair(id, key)); - }; - auto opposite = [](const HeapState &made, - const HeapState &kept) -> std::optional { - for (const EntryTest &test : made.entryTests) - for (const EntryTest &other : kept.entryTests) - if (other.object == test.object && other.key == test.key && - other.zero != test.zero) - return test; - return std::nullopt; - }; - auto guardOf = [](const ObjectState *object, - const CellKey &key) -> std::optional { - if (object != nullptr) - for (const auto &[at, test] : object->storedIff) - if (at == key) - return test; - return std::nullopt; - }; - std::vector>>> - guards; - for (const auto &[id, object] : out.objects) { - const ObjectKey &objectKey = table.info(id).key; - if ((objectKey.kind != ObjectKind::Entry || objectKey.dead) && - objectKey.kind != ObjectKind::Global) - continue; - std::vector> cells; - for (const auto &[key, sym] : object.cells) { - if (!key.isConcrete()) - continue; - auto l = stored(left, id, key); - auto r = stored(right, id, key); - std::optional guard; - if (l == true && r == false) { - guard = opposite(left, right); - } else if (l == false && r == true) { - guard = opposite(right, left); - } else if (l == true && r == true) { - auto a = guardOf(left.objects.find(id), key); - auto b = guardOf(right.objects.find(id), key); - if (a && b && *a == *b) - guard = a; - } - if (guard) - cells.emplace_back(key, *guard); - } - if (!cells.empty() || !object.storedIff.empty()) - guards.emplace_back(id, std::move(cells)); - } - for (auto &[id, cells] : guards) - out.objects.at(id).storedIff = std::move(cells); - } - - // Entry tests both sides decided alike (they name cells, not symbols). - std::ranges::set_intersection(left.entryTests, right.entryTests, - std::back_inserter(out.entryTests)); - - // RFC 0014: a pointer comparison both sides decided alike, over the - // symbols that pair their operands. - if (!left.pointerFacts.empty() && !right.pointerFacts.empty()) { - std::map> byLeft; - for (const SymPair &p : pairing.pairs) - if (p.hasLeft && p.hasRight) - byLeft[p.left].push_back(&p); - for (const PointerFact &fact : left.pointerFacts) { - auto x = byLeft.find(fact.first); - auto y = byLeft.find(fact.second); - if (x == byLeft.end() || y == byLeft.end()) - continue; - for (const SymPair *a : x->second) - for (const SymPair *b : y->second) - if (Heap::pointersEqual(right, a->right, b->right) == fact.equal) - Heap::assumePointersEqual(out, a->result, b->result, fact.equal); - } - } - - // Symbols: the zone over the result symbols. - std::vector noThresholds; - out.zone = Zone::combine(left.zone, right.zone, pairing.pairs, widen, - widen ? thresholds : noThresholds); - // A join keeps both sides' unmatched ranges: each position's newest - // `MaxSegmentsPerPosition` stay, or a loop head compounds them. - std::vector crowded; - for (const auto &[id, object] : out.objects) - if (object.segments.size() > Heap::MaxSegmentsPerPosition) - crowded.push_back(id); - for (ObjectId id : crowded) - heap.trimSegments(out, id); - return out; -} - -HeapState Heap::join(const HeapState &left, const HeapState &right, - Handle block, bool loopHead, Sym keepBelow) const { - return combineStates(*this, table, left, right, block, /*widen=*/false, - loopHead, {}, keepBelow); -} - -HeapState Heap::widen(const HeapState &previous, const HeapState &next, - Handle block, - const std::vector &thresholds) const { - return combineStates(*this, table, previous, next, block, /*widen=*/true, - /*loopHead=*/false, thresholds); -} - -//===----------------------------------------------------------------------===// -// Dumps -//===----------------------------------------------------------------------===// - -std::string Heap::dump(const HeapState &state) const { - std::ostringstream os; - if (state.unreachable) - return "unreachable\n"; - auto name = [&](Sym sym) { return "s" + std::to_string(sym); }; - auto spellTerm = [&](const Term &term) { - if (!term.known) - return std::string("?"); - if (term.var == ZeroSym || term.scale == 0) - return std::to_string(term.constant); - std::string out = - (term.scale == 1 ? "" : std::to_string(term.scale) + "*") + - name(term.var); - if (term.constant != 0) - out += (term.constant > 0 ? "+" : "") + std::to_string(term.constant); - return out; - }; - for (const auto &[id, object] : state.objects) { - const ObjectInfo &objectInfo = table.info(id); - os << " o" << id << " " << spell(objectInfo.key.kind) - << (objectInfo.key.dead ? " dead" : "") << " '" << objectInfo.name - << "'"; - switch (object.life) { - case Life::Live: - break; - case Life::Released: - os << " released"; - break; - case Life::MayReleased: - os << " may-released"; - break; - case Life::UnknownReleased: - os << " unknown-released"; - break; - case Life::Ended: - os << " ended"; - break; - case Life::MayEnded: - os << " may-ended"; - break; - } - if (object.extent) { - os << " extent=" << spellTerm(object.extent->bytes) << "/" - << toString(object.extent->cls); - if (object.extent->unwrapped) - os << " at-most=" << spellTerm(*object.extent->unwrapped); - } - for (const auto &[from, to] : object.forgotten) - os << " forgotten=[" << from << "," << to << ")"; - for (const auto &[from, to] : object.mayForgotten) - os << " may-forgotten=[" << from << "," << to << ")"; - if (!object.candidates.empty()) { - os << " candidates={"; - for (ObjectId c : object.candidates) - os << " o" << c; - os << " }"; - } - os << "\n"; - for (const Segment &segment : object.segments) { - os << " [" << spellTerm(segment.from) << ".." << spellTerm(segment.to) - << ")*" << segment.position.stride << "+" << segment.position.offset - << " = " << name(segment.value); - if (const auto &release = info(state, segment.value).release) - os << (release->definite() ? " released" : " may-released"); - os << "\n"; - } - for (const auto &[key, sym] : object.cells) { - if (key.isSelected()) - os << " [" << key.stride << "*" << name(key.index) << "+" - << key.offset << "] = " << name(sym); - else - os << " [" << (key.isSummary() ? "*" : "") << key.offset - << (key.isSummary() ? "/" + std::to_string(key.stride) : "") - << "] = " << name(sym); - const SymInfo &value = info(state, sym); - if (value.type == SymInfo::Type::Pointer) { - os << " ->"; - if (value.top) - os << " top"; - for (const Target &t : value.targets) - os << " o" << t.object << "+" << spellTerm(t.offset); - if (value.null == PointerNull::Null) - os << " null"; - else if (value.null == PointerNull::NonNull) - os << " nonnull"; - else - os << " maybe-null"; - if (value.release) - os << (value.release->definite() ? " released" : " may-released"); - } - os << "\n"; - } - } - if (!state.exprs.empty()) { - os << " exprs:"; - for (const auto &[handle, sym] : state.exprs) - os << " " << std::hex << handle << std::dec << "=" << name(sym); - os << "\n"; - } - std::string zone = state.zone.toString(name); - if (!zone.empty()) - os << " zone: " << zone << "\n"; - return os.str(); -} - -} // namespace weavec::core diff --git a/lib/Core/Integer.cpp b/lib/Core/Integer.cpp deleted file mode 100644 index 7badeeb8..00000000 --- a/lib/Core/Integer.cpp +++ /dev/null @@ -1,793 +0,0 @@ -//===- Integer.cpp - Target integer values and bounded ranges -------------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// - -#include "weavec/Core/Integer.h" - -#include -#include -#include -#include -#include -#include - -namespace weavec::core { - -bool IntegerType::valid() const noexcept { - return width >= 1 && width <= 64 && (!isBoolean || (width == 1 && !isSigned)); -} - -std::uint64_t IntegerType::mask() const noexcept { - if (!valid()) - return 0; - return width == 64 ? UINT64_MAX : (std::uint64_t{1} << width) - 1; -} - -std::uint64_t IntegerType::signBit() const noexcept { - return valid() && isSigned ? std::uint64_t{1} << (width - 1) : 0; -} - -std::uint64_t IntegerType::rank(std::uint64_t bits) const noexcept { - return (bits & mask()) ^ signBit(); -} - -std::string IntegerType::toString() const { - return isBoolean ? "b1" - : std::string(isSigned ? "i" : "u") + std::to_string(width); -} - -IntegerValue IntegerValue::ofBits(IntegerType type, - std::uint64_t bits) noexcept { - return {.type = type, - .bits = type.isBoolean ? static_cast(bits != 0) - : bits & type.mask()}; -} - -bool IntegerValue::negative() const noexcept { - return (bits & type.signBit()) != 0; -} - -std::uint64_t IntegerValue::magnitude() const noexcept { - return negative() ? ((~bits + 1) & type.mask()) : bits; -} - -std::optional IntegerValue::signedValue() const noexcept { - if (!negative()) { - if (bits > static_cast(INT64_MAX)) - return std::nullopt; - return static_cast(bits); - } - const std::uint64_t mag = magnitude(); - if (mag == (std::uint64_t{1} << 63U)) - return INT64_MIN; - return -static_cast(mag); -} - -IntegerValue IntegerValue::converted(IntegerType destination) const noexcept { - if (destination.isBoolean) - return ofBits(destination, static_cast(bits != 0)); - const std::uint64_t extended = negative() ? bits | ~type.mask() : bits; - return ofBits(destination, extended); -} - -std::string IntegerValue::toString() const { - return (negative() ? "-" : "") + std::to_string(magnitude()); -} - -bool isUnary(IntegerOp op) noexcept { - return op == IntegerOp::Negate || op == IntegerOp::Complement || - op == IntegerOp::LogicalNot; -} - -bool isComparison(IntegerOp op) noexcept { - return op >= IntegerOp::Equal && op <= IntegerOp::GreaterEqual; -} - -IntegerOp negateComparison(IntegerOp op) noexcept { - switch (op) { - case IntegerOp::Equal: - return IntegerOp::NotEqual; - case IntegerOp::NotEqual: - return IntegerOp::Equal; - case IntegerOp::Less: - return IntegerOp::GreaterEqual; - case IntegerOp::LessEqual: - return IntegerOp::Greater; - case IntegerOp::Greater: - return IntegerOp::LessEqual; - case IntegerOp::GreaterEqual: - return IntegerOp::Less; - default: - return op; - } -} - -std::string_view toString(IntegerError error) noexcept { - switch (error) { - case IntegerError::None: - return ""; - case IntegerError::SignedOverflow: - return "signed integer overflow"; - case IntegerError::DivisionByZero: - return "division by zero"; - case IntegerError::SignedDivisionOverflow: - return "signed division overflow"; - case IntegerError::ShiftCount: - return "invalid shift count"; - case IntegerError::SignedLeftShift: - return "invalid signed left shift"; - case IntegerError::IncompatibleTypes: - return "unsupported integer type"; - } - return "unsupported integer operation"; -} - -IntegerEvaluation evaluateInteger(IntegerOp op, IntegerValue lhs, - IntegerValue rhs, bool wrapSigned) { - const auto fail = [](IntegerError error) -> IntegerEvaluation { - return {.value = std::nullopt, .error = error}; - }; - const IntegerType type = lhs.type; - const bool shift = op == IntegerOp::ShiftLeft || op == IntegerOp::ShiftRight; - if (!type.valid() || !rhs.type.valid() || - (!isUnary(op) && !shift && type != rhs.type)) - return fail(IntegerError::IncompatibleTypes); - const auto value = [type](std::uint64_t bits) -> IntegerEvaluation { - return {.value = IntegerValue::ofBits(type, bits)}; - }; - const auto boolean = [](bool condition) -> IntegerEvaluation { - return {.value = IntegerValue::ofBits( - BooleanType, static_cast(condition))}; - }; - const std::uint64_t a = lhs.bits; - const std::uint64_t b = rhs.bits; - const bool signedCheck = type.isSigned && !wrapSigned; - const std::uint64_t positiveMax = type.signBit() - 1; - switch (op) { - case IntegerOp::Add: { - const auto result = IntegerValue::ofBits(type, a + b); - if (signedCheck && lhs.negative() == rhs.negative() && - result.negative() != lhs.negative()) - return fail(IntegerError::SignedOverflow); - return {.value = result}; - } - case IntegerOp::Subtract: { - const auto result = IntegerValue::ofBits(type, a - b); - if (signedCheck && lhs.negative() != rhs.negative() && - result.negative() != lhs.negative()) - return fail(IntegerError::SignedOverflow); - return {.value = result}; - } - case IntegerOp::Multiply: - if (signedCheck) { - const std::uint64_t limit = - lhs.negative() != rhs.negative() ? type.signBit() : positiveMax; - const auto divisor = rhs.magnitude(); - if (divisor != 0 && lhs.magnitude() > limit / divisor) - return fail(IntegerError::SignedOverflow); - } - return value(a * b); - case IntegerOp::Divide: - case IntegerOp::Remainder: { - const auto divisor = rhs.magnitude(); - if (divisor == 0) - return fail(IntegerError::DivisionByZero); - if (type.isSigned && a == type.signBit() && b == type.mask()) - return fail(IntegerError::SignedDivisionOverflow); - const std::uint64_t mag = op == IntegerOp::Divide - ? lhs.magnitude() / divisor - : lhs.magnitude() % divisor; - const bool negative = op == IntegerOp::Divide - ? lhs.negative() != rhs.negative() - : lhs.negative(); - return value(negative ? ~mag + 1 : mag); - } - case IntegerOp::ShiftLeft: - case IntegerOp::ShiftRight: { - if (rhs.negative() || b >= type.width) - return fail(IntegerError::ShiftCount); - const auto count = static_cast(b); - if (op == IntegerOp::ShiftLeft) { - if (type.isSigned && (lhs.negative() || a > (positiveMax >> count))) - return fail(IntegerError::SignedLeftShift); - return value(a << count); - } - if (lhs.negative() && count != 0) - return value((a >> count) | (type.mask() ^ (type.mask() >> count))); - return value(a >> count); - } - case IntegerOp::BitAnd: - return value(a & b); - case IntegerOp::BitOr: - return value(a | b); - case IntegerOp::BitXor: - return value(a ^ b); - case IntegerOp::Negate: - if (signedCheck && a == type.signBit()) - return fail(IntegerError::SignedOverflow); - return value(~a + 1); - case IntegerOp::Complement: - return value(~a); - case IntegerOp::LogicalNot: - return boolean(a == 0); - case IntegerOp::Equal: - return boolean(a == b); - case IntegerOp::NotEqual: - return boolean(a != b); - case IntegerOp::Less: - return boolean(type.rank(a) < type.rank(b)); - case IntegerOp::LessEqual: - return boolean(type.rank(a) <= type.rank(b)); - case IntegerOp::Greater: - return boolean(type.rank(a) > type.rank(b)); - case IntegerOp::GreaterEqual: - return boolean(type.rank(a) >= type.rank(b)); - case IntegerOp::Minimum: - return value(type.rank(a) <= type.rank(b) ? a : b); - case IntegerOp::Maximum: - return value(type.rank(a) >= type.rank(b) ? a : b); - } - return fail(IntegerError::IncompatibleTypes); -} - -IntegerRange IntegerRange::full(IntegerType type) { - return fromRanks(type, {{.lower = 0, .upper = type.mask()}}); -} - -IntegerRange IntegerRange::singleton(IntegerValue value) { - const auto rank = value.type.rank(value.bits); - return fromRanks(value.type, {{.lower = rank, .upper = rank}}); -} - -IntegerRange IntegerRange::between(IntegerValue lower, IntegerValue upper) { - if (lower.type != upper.type) - return full(lower.type); - return fromRanks(lower.type, {{.lower = lower.type.rank(lower.bits), - .upper = upper.type.rank(upper.bits)}}); -} - -IntegerRange IntegerRange::fromRanks(IntegerType type, - std::vector ranges) { - IntegerRange result(type); - if (!type.valid()) - return result; - std::ranges::sort(ranges); - for (const auto &range : ranges) { - if (range.lower > range.upper || range.upper > type.mask()) - continue; - if (!result.intervals.empty()) { - auto &last = result.intervals.back(); - if (range.lower <= last.upper || - (last.upper != UINT64_MAX && range.lower == last.upper + 1)) { - last.upper = std::max(last.upper, range.upper); - continue; - } - } - result.intervals.push_back(range); - } - // Too many pieces to keep: the hull of what is left, never the whole type. - // `{-3, -1, 1, 3}` collapses to `[-3, 3]`, which still refutes `x == 12345`; - // dropping to the full type would forget every bound the branches gave. - if (result.intervals.size() > MaxIntegerIntervals) - result.intervals = {{.lower = result.intervals.front().lower, - .upper = result.intervals.back().upper}}; - return result; -} - -bool IntegerRange::isFull() const noexcept { - return intervals.size() == 1 && intervals.front().lower == 0 && - intervals.front().upper == type.mask(); -} - -bool IntegerRange::contains(IntegerValue value) const noexcept { - if (value.type != type) - return false; - const auto rank = type.rank(value.bits); - return std::ranges::any_of(intervals, [rank](const auto &interval) { - return rank >= interval.lower && rank <= interval.upper; - }); -} - -std::optional IntegerRange::constant() const noexcept { - if (intervals.size() != 1 || - intervals.front().lower != intervals.front().upper) - return std::nullopt; - return IntegerValue::ofBits(type, type.rank(intervals.front().lower)); -} - -std::optional IntegerRange::minimum() const noexcept { - if (empty()) - return std::nullopt; - return IntegerValue::ofBits(type, type.rank(intervals.front().lower)); -} - -std::optional IntegerRange::maximum() const noexcept { - if (empty()) - return std::nullopt; - return IntegerValue::ofBits(type, type.rank(intervals.back().upper)); -} - -IntegerRange IntegerRange::converted(IntegerType destination) const { - if (destination == type) - return *this; - if (destination.isBoolean) { - const auto zero = IntegerValue::ofBits(type, 0); - if (empty()) - return IntegerRange(destination); - if (!contains(zero)) - return singleton(IntegerValue::ofBits(destination, 1)); - return constant() ? singleton(IntegerValue::ofBits(destination, 0)) - : full(destination); - } - std::vector ranges; - for (const auto &interval : intervals) { - const auto span = interval.upper - interval.lower; - if (span >= destination.mask()) - return full(destination); - const auto first = IntegerValue::ofBits(type, type.rank(interval.lower)) - .converted(destination); - const auto begin = destination.rank(first.bits); - if (span <= destination.mask() - begin) { - ranges.push_back({.lower = begin, .upper = begin + span}); - } else { - ranges.push_back({.lower = begin, .upper = destination.mask()}); - ranges.push_back( - {.lower = 0, .upper = span - (destination.mask() - begin) - 1}); - } - } - return fromRanks(destination, std::move(ranges)); -} - -IntegerRange IntegerRange::intersect(const IntegerRange &other) const { - if (type != other.type) - return *this; - std::vector ranges; - for (const auto &a : intervals) - for (const auto &b : other.intervals) { - const auto lower = std::max(a.lower, b.lower); - const auto upper = std::min(a.upper, b.upper); - if (lower <= upper) - ranges.push_back({.lower = lower, .upper = upper}); - } - return fromRanks(type, std::move(ranges)); -} - -IntegerRange IntegerRange::united(const IntegerRange &other) const { - if (type != other.type) - return full(type); - auto ranges = intervals; - ranges.insert(ranges.end(), other.intervals.begin(), other.intervals.end()); - return fromRanks(type, std::move(ranges)); -} - -IntegerRange IntegerRange::satisfying(IntegerOp op, - const IntegerRange &rhs) const { - if (type != rhs.type || !isComparison(op)) - return *this; - if (empty() || rhs.empty()) - return IntegerRange(type); - if (op == IntegerOp::Equal) - return intersect(rhs); - if (op == IntegerOp::NotEqual) { - const auto value = rhs.constant(); - if (!value) - return *this; - const auto at = type.rank(value->bits); - std::vector ranges; - for (const auto &range : intervals) { - if (at < range.lower || at > range.upper) { - ranges.push_back(range); - } else { - if (range.lower < at) - ranges.push_back({.lower = range.lower, .upper = at - 1}); - if (at < range.upper) - ranges.push_back({.lower = at + 1, .upper = range.upper}); - } - } - return fromRanks(type, std::move(ranges)); - } - std::uint64_t lower = 0; - std::uint64_t upper = type.mask(); - if (op == IntegerOp::Less || op == IntegerOp::LessEqual) { - upper = rhs.intervals.back().upper; - if (op == IntegerOp::Less) { - if (upper == 0) - return IntegerRange(type); - --upper; - } - } else { - lower = rhs.intervals.front().lower; - if (op == IntegerOp::Greater) { - if (lower == type.mask()) - return IntegerRange(type); - ++lower; - } - } - return intersect(fromRanks(type, {{.lower = lower, .upper = upper}})); -} - -std::string IntegerRange::toString() const { - std::string text = type.toString() + ":"; - for (const auto &range : intervals) { - if (text.back() != ':') - text += ','; - text += std::to_string(range.lower) + "-" + std::to_string(range.upper); - } - return text; -} - -static std::optional> -smallValues(const IntegerRange &range) { - std::vector result; - for (const auto &interval : range.all()) { - if (interval.upper - interval.lower >= 16 || - result.size() + - static_cast(interval.upper - interval.lower) + 1 > - 16) - return std::nullopt; - for (auto rank = interval.lower;; ++rank) { - result.push_back(IntegerValue::ofBits(range.type, range.type.rank(rank))); - if (rank == interval.upper) - break; - } - } - return result; -} - -static IntegerRangeEvaluation -enumerateInteger(IntegerOp op, const std::vector &lhs, - const std::vector &rhs, IntegerType resultType, - bool wrapSigned) { - std::vector ranges; - bool invalid = false; - IntegerError error = IntegerError::None; - for (const auto a : lhs) { - for (const auto b : rhs) { - const auto result = evaluateInteger(op, a, b, wrapSigned); - if (result.value) { - const auto rank = resultType.rank(result.value->bits); - ranges.push_back({.lower = rank, .upper = rank}); - } else { - invalid = true; - if (error == IntegerError::None) - error = result.error; - } - if (isUnary(op)) - break; - } - } - // A possibly undefined execution must not become a path-pruning premise. - const bool always = invalid && ranges.empty(); - return {.values = - invalid ? IntegerRange::full(resultType) - : IntegerRange::fromRanks(resultType, std::move(ranges)), - .mayBeInvalid = invalid, - .alwaysInvalid = always, - .error = error}; -} - -IntegerRangeEvaluation evaluateInteger(IntegerOp op, const IntegerRange &lhs, - const IntegerRange &rhs, - bool wrapSigned) { - const auto type = lhs.type; - const auto resultType = - isComparison(op) || op == IntegerOp::LogicalNot ? BooleanType : type; - const auto unknown = [resultType](bool invalid = false, - IntegerError error = IntegerError::None, - bool always = - false) -> IntegerRangeEvaluation { - return {.values = IntegerRange::full(resultType), - .mayBeInvalid = invalid, - .alwaysInvalid = always, - .error = error}; - }; - if (lhs.empty() || rhs.empty()) - return {.values = IntegerRange(resultType)}; - const bool shift = op == IntegerOp::ShiftLeft || op == IntegerOp::ShiftRight; - if (!isUnary(op) && !shift && type != rhs.type) - return unknown(true, IntegerError::IncompatibleTypes); - const auto aValues = smallValues(lhs); - const auto bValues = - isUnary(op) ? std::optional(std::vector{IntegerValue::ofBits(type, 0)}) - : smallValues(rhs); - if (aValues && bValues) - return enumerateInteger(op, *aValues, *bValues, resultType, wrapSigned); - const auto aMin = *lhs.minimum(); - const auto aMax = *lhs.maximum(); - const auto bMin = *rhs.minimum(); - const auto bMax = *rhs.maximum(); - if (isComparison(op)) { - if (lhs.satisfying(op, rhs).empty()) - return {.values = IntegerRange::singleton( - IntegerValue::ofBits(BooleanType, 0))}; - if (lhs.satisfying(negateComparison(op), rhs).empty()) - return {.values = IntegerRange::singleton( - IntegerValue::ofBits(BooleanType, 1))}; - return unknown(); - } - if (op == IntegerOp::LogicalNot) { - if (!lhs.contains(IntegerValue::ofBits(type, 0))) - return {.values = IntegerRange::singleton( - IntegerValue::ofBits(BooleanType, 0))}; - return unknown(); - } - if (op == IntegerOp::Minimum || op == IntegerOp::Maximum) { - const auto low = evaluateInteger(op, aMin, bMin); - const auto high = evaluateInteger(op, aMax, bMax); - return {.values = IntegerRange::between(*low.value, *high.value)}; - } - if (op == IntegerOp::Add || op == IntegerOp::Subtract) { - std::vector ranges; - for (const auto &a : lhs.all()) { - for (const auto &b : rhs.all()) { - const auto first = evaluateInteger( - op, IntegerValue::ofBits(type, type.rank(a.lower)), - IntegerValue::ofBits( - type, type.rank(op == IntegerOp::Add ? b.lower : b.upper)), - wrapSigned); - const auto last = evaluateInteger( - op, IntegerValue::ofBits(type, type.rank(a.upper)), - IntegerValue::ofBits( - type, type.rank(op == IntegerOp::Add ? b.upper : b.lower)), - wrapSigned); - if (!first.value || !last.value) - return unknown(true, IntegerError::SignedOverflow); - const auto spanA = a.upper - a.lower; - const auto spanB = b.upper - b.lower; - if (spanA >= type.mask() - spanB) - return unknown(); - const auto span = spanA + spanB; - const auto begin = type.rank(first.value->bits); - if (span <= type.mask() - begin) { - ranges.push_back({.lower = begin, .upper = begin + span}); - } else { - ranges.push_back({.lower = begin, .upper = type.mask()}); - ranges.push_back( - {.lower = 0, .upper = span - (type.mask() - begin) - 1}); - } - } - } - return {.values = IntegerRange::fromRanks(type, std::move(ranges))}; - } - if (op == IntegerOp::Multiply) { - // Scale each modular interval before taking a hull. A signed-to-unsigned - // conversion can produce two separated intervals; collapsing them first - // would invent zero in a nonzero converted count times a small constant. - const auto lhsConstant = lhs.constant(); - const auto rhsConstant = rhs.constant(); - if (!type.isSigned && (lhsConstant || rhsConstant)) { - const auto factor = lhsConstant ? lhsConstant->bits : rhsConstant->bits; - const auto &input = lhsConstant ? rhs : lhs; - if (factor == 0) - return {.values = - IntegerRange::singleton(IntegerValue::ofBits(type, 0))}; - std::vector ranges; - for (const auto &interval : input.all()) { - const auto span = interval.upper - interval.lower; - if (span > type.mask() / factor) - return unknown(); - const auto distance = span * factor; - const auto begin = (interval.lower * factor) & type.mask(); - if (distance <= type.mask() - begin) { - ranges.push_back({.lower = begin, .upper = begin + distance}); - } else { - ranges.push_back({.lower = begin, .upper = type.mask()}); - ranges.push_back( - {.lower = 0, .upper = distance - (type.mask() - begin) - 1}); - } - } - return {.values = IntegerRange::fromRanks(type, std::move(ranges))}; - } - if (!type.isSigned && bMax.bits != 0 && aMax.bits > type.mask() / bMax.bits) - return unknown(); - std::uint64_t lower = type.mask(); - std::uint64_t upper = 0; - for (const auto a : {aMin, aMax}) - for (const auto b : {bMin, bMax}) { - const auto result = evaluateInteger(op, a, b, wrapSigned); - if (!result.value) - return unknown(true, result.error); - if (type.isSigned && wrapSigned) { - const auto checked = evaluateInteger(op, a, b, false); - if (!checked.value) - return unknown(); - } - lower = std::min(lower, type.rank(result.value->bits)); - upper = std::max(upper, type.rank(result.value->bits)); - } - return {.values = IntegerRange::fromRanks( - type, {{.lower = lower, .upper = upper}})}; - } - if (op == IntegerOp::Divide || op == IntegerOp::Remainder) { - if (bMin.bits == 0 || bMax.bits == 0 || - rhs.contains(IntegerValue::ofBits(rhs.type, 0))) - return unknown(true, IntegerError::DivisionByZero, - rhs.constant().has_value()); - if (type.isSigned && - lhs.contains(IntegerValue::ofBits(type, type.signBit())) && - rhs.contains(IntegerValue::ofBits(type, type.mask()))) - return unknown(true, IntegerError::SignedDivisionOverflow); - if (!aMin.negative() && !bMin.negative()) { - if (op == IntegerOp::Divide) - return {.values = IntegerRange::between( - IntegerValue::ofBits(type, aMin.bits / bMax.bits), - IntegerValue::ofBits(type, aMax.bits / bMin.bits))}; - return { - .values = IntegerRange::between( - IntegerValue::ofBits(type, 0), - IntegerValue::ofBits(type, std::min(aMax.bits, bMax.bits - 1)))}; - } - return unknown(); - } - if (shift) { - if (bMin.negative() || bMax.bits >= type.width) - return unknown(true, IntegerError::ShiftCount, - !bMin.negative() && bMin.bits >= type.width); - if (op == IntegerOp::ShiftLeft && type.isSigned && - (aMin.negative() || aMax.bits > ((type.signBit() - 1) >> bMax.bits))) - return unknown(true, IntegerError::SignedLeftShift); - if (op == IntegerOp::ShiftRight && rhs.constant()) - return {.values = IntegerRange::between( - *evaluateInteger(op, aMin, bMin, wrapSigned).value, - *evaluateInteger(op, aMax, bMin, wrapSigned).value)}; - if (op == IntegerOp::ShiftLeft && !aMin.negative() && - aMax.bits <= (type.mask() >> bMax.bits)) - return {.values = IntegerRange::between( - IntegerValue::ofBits(type, aMin.bits << bMin.bits), - IntegerValue::ofBits(type, aMax.bits << bMax.bits))}; - return unknown(); - } - if (op == IntegerOp::Negate) { - if (type.isSigned && !wrapSigned && - lhs.contains(IntegerValue::ofBits(type, type.signBit()))) - return unknown(true, IntegerError::SignedOverflow); - const auto zero = IntegerRange::singleton(IntegerValue::ofBits(type, 0)); - return evaluateInteger(IntegerOp::Subtract, zero, lhs, wrapSigned); - } - if (op == IntegerOp::Complement) { - std::vector ranges; - for (const auto &interval : lhs.all()) - ranges.push_back({.lower = type.mask() - interval.upper, - .upper = type.mask() - interval.lower}); - return {.values = IntegerRange::fromRanks(type, std::move(ranges))}; - } - if (op == IntegerOp::BitAnd && (!aMin.negative() || !bMin.negative())) { - // RFC 0017/0019: a nonnegative mask clears the sign bit even if the - // other operand is signed or crosses zero. Only nonnegative operands - // supply numeric upper bounds on the resulting bit pattern. - auto upper = type.mask(); - if (!aMin.negative()) - upper = std::min(upper, aMax.bits); - if (!bMin.negative()) - upper = std::min(upper, bMax.bits); - return {.values = IntegerRange::between(IntegerValue::ofBits(type, 0), - IntegerValue::ofBits(type, upper))}; - } - return unknown(); -} - -// RFC 0017: the checked-arithmetic builtins (`__builtin_add_overflow`). - -namespace { -struct CheckedMagnitude { - std::uint64_t high = 0; - std::uint64_t low = 0; - bool negative = false; -}; -} // namespace - -static CheckedMagnitude checkedMagnitude(IntegerOp op, IntegerValue lhs, - IntegerValue rhs) { - const auto a = lhs.magnitude(); - const auto b = rhs.magnitude(); - if (op == IntegerOp::Multiply) { - constexpr std::uint64_t HalfMask = UINT32_MAX; - const auto aLow = a & HalfMask; - const auto aHigh = a >> 32U; - const auto bLow = b & HalfMask; - const auto bHigh = b >> 32U; - const auto first = aLow * bLow; - const auto middle = (aHigh * bLow) + (first >> 32U); - const auto carry = middle >> 32U; - const auto second = (middle & HalfMask) + (aLow * bHigh); - return {.high = (aHigh * bHigh) + carry + (second >> 32U), - .low = (second << 32U) | (first & HalfMask), - .negative = a != 0 && b != 0 && lhs.negative() != rhs.negative()}; - } - const bool negativeA = lhs.negative(); - const bool negativeB = - rhs.negative() != (op == IntegerOp::Subtract && b != 0); - if (negativeA == negativeB) { - const auto low = a + b; - return {.high = low < a ? 1U : 0U, .low = low, .negative = negativeA}; - } - return {.high = 0, - .low = a >= b ? a - b : b - a, - .negative = a != b && (a > b ? negativeA : negativeB)}; -} - -/// Below, inside or above the destination's mathematical value interval. -static int relativeToType(const CheckedMagnitude &value, IntegerType type) { - if (value.negative) { - if (!type.isSigned || value.high != 0 || value.low > type.signBit()) - return -1; - } else { - const auto maximum = type.isSigned ? type.signBit() - 1 : type.mask(); - if (value.high != 0 || value.low > maximum) - return 1; - } - return 0; -} - -std::optional -evaluateCheckedInteger(IntegerOp op, IntegerValue lhs, IntegerValue rhs, - IntegerType destination) { - if ((op != IntegerOp::Add && op != IntegerOp::Subtract && - op != IntegerOp::Multiply) || - !lhs.type.valid() || !rhs.type.valid() || !destination.valid()) - return std::nullopt; - const auto magnitude = checkedMagnitude(op, lhs, rhs); - const auto bits = - magnitude.negative ? std::uint64_t{0} - magnitude.low : magnitude.low; - const auto value = IntegerValue::ofBits( - destination, destination.isBoolean - ? static_cast(magnitude.high != 0 || - magnitude.low != 0) - : bits); - return CheckedIntegerValue{ - .value = value, .overflow = relativeToType(magnitude, destination) != 0}; -} - -CheckedIntegerRange evaluateCheckedInteger(IntegerOp op, - const IntegerRange &lhs, - const IntegerRange &rhs, - IntegerType destination) { - CheckedIntegerRange result{.values = IntegerRange::full(destination), - .overflow = IntegerRange::full(BooleanType)}; - if (lhs.empty() || rhs.empty()) - return {.values = IntegerRange(destination), - .overflow = IntegerRange(BooleanType)}; - if (const auto a = lhs.constant()) - if (const auto b = rhs.constant()) - if (const auto exact = evaluateCheckedInteger(op, *a, *b, destination)) - return {.values = IntegerRange::singleton(exact->value), - .overflow = IntegerRange::singleton(IntegerValue::ofBits( - BooleanType, static_cast(exact->overflow)))}; - if (op != IntegerOp::Add && op != IntegerOp::Subtract && - op != IntegerOp::Multiply) - return result; - if (!destination.isBoolean) { - const auto wrapped = evaluateInteger(op, lhs.converted(destination), - rhs.converted(destination), true); - if (!wrapped.mayBeInvalid) - result.values = wrapped.values; - } - bool allInside = true; - bool allOutside = true; - for (const auto &a : lhs.all()) - for (const auto &b : rhs.all()) { - bool below = true; - bool above = true; - for (const auto x : {a.lower, a.upper}) - for (const auto y : {b.lower, b.upper}) { - const auto value = checkedMagnitude( - op, IntegerValue::ofBits(lhs.type, lhs.type.rank(x)), - IntegerValue::ofBits(rhs.type, rhs.type.rank(y))); - const auto side = relativeToType(value, destination); - allInside &= side == 0; - below &= side < 0; - above &= side > 0; - } - allOutside &= below || above; - } - if (allInside || allOutside) - result.overflow = IntegerRange::singleton(IntegerValue::ofBits( - BooleanType, static_cast(allOutside))); - return result; -} - -} // namespace weavec::core diff --git a/lib/Core/Ledger.cpp b/lib/Core/Ledger.cpp deleted file mode 100644 index 9928b7fc..00000000 --- a/lib/Core/Ledger.cpp +++ /dev/null @@ -1,497 +0,0 @@ -//===- Ledger.cpp - Per-site analysis outcomes (RFC 0030, 0035) -----------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// - -#include "weavec/Core/Ledger.h" - -#include -#include -#include -#include -#include - -namespace weavec::core { - -//===----------------------------------------------------------------------===// -// Spellings -//===----------------------------------------------------------------------===// - -std::string_view toString(SiteKind kind) noexcept { - switch (kind) { - case SiteKind::Deref: - return "deref"; - case SiteKind::Index: - return "index"; - case SiteKind::PtrArith: - return "ptr-arith"; - case SiteKind::Cast: - return "cast"; - case SiteKind::IntToPtr: - return "int-to-ptr"; - case SiteKind::LibCall: - return "lib-call"; - case SiteKind::Release: - return "release"; - case SiteKind::Call: - return "call"; - case SiteKind::Assume: - return "assume"; - case SiteKind::Raw: - return "raw"; - } - return ""; -} - -std::string_view toString(Facet facet) noexcept { - switch (facet) { - case Facet::Spatial: - return "spatial"; - case Facet::Null: - return "null"; - case Facet::Temporal: - return "temporal"; - case Facet::Assertion: - return "assertion"; - } - return ""; -} - -std::string_view toString(SiteOutcome outcome) noexcept { - switch (outcome) { - case SiteOutcome::Proven: - return "proven"; - case SiteOutcome::Violation: - return "violation"; - case SiteOutcome::Unresolved: - return "unresolved"; - case SiteOutcome::Trusted: - return "trusted"; - } - return ""; -} - -std::string_view toString(UnresolvedReason reason) noexcept { - switch (reason) { - case UnresolvedReason::UnknownExtent: - return "unknown-extent"; - case UnresolvedReason::UnknownIndex: - return "unknown-index"; - case UnresolvedReason::Inexpressible: - return "inexpressible"; - case UnresolvedReason::MayReleased: - return "may-released"; - case UnresolvedReason::MayMoved: - return "may-moved"; - case UnresolvedReason::MayAliasReleased: - return "may-alias-released"; - case UnresolvedReason::MayInvalidRelease: - return "may-invalid-release"; - case UnresolvedReason::MayMismatchedRelease: - return "may-mismatched-release"; - case UnresolvedReason::MayDangle: - return "may-dangle"; - case UnresolvedReason::MayConflict: - return "may-conflict"; - case UnresolvedReason::UnknownCallee: - return "unknown-callee"; - case UnresolvedReason::Callback: - return "callback"; - case UnresolvedReason::Setjmp: - return "setjmp"; - case UnresolvedReason::Budget: - return "budget"; - case UnresolvedReason::Unanalysed: - return "unanalysed"; - case UnresolvedReason::RawCast: - return "raw-cast"; - case UnresolvedReason::DanglingEscape: - return "dangling-escape"; - case UnresolvedReason::SecondOwner: - return "second-owner"; - case UnresolvedReason::NoZeroInit: - return "no-zero-init"; - case UnresolvedReason::Unconfirmed: - return "unconfirmed"; - case UnresolvedReason::Undecided: - return "undecided"; - } - return ""; -} - -std::string_view toString(TrustReason reason) noexcept { - switch (reason) { - case TrustReason::Unsafe: - return "unsafe"; - case TrustReason::SystemApi: - return "system-api"; - case TrustReason::LibrarySpec: - return "library-spec"; - case TrustReason::ExternContract: - return "extern-contract"; - case TrustReason::CallerContract: - return "caller-contract"; - case TrustReason::ExternalUnit: - return "external-unit"; - case TrustReason::Concurrency: - return "concurrency"; - } - return ""; -} - -//===----------------------------------------------------------------------===// -// Decisions and merging -//===----------------------------------------------------------------------===// - -FacetDecision FacetDecision::proven() { - return FacetDecision{.outcome = SiteOutcome::Proven}; -} - -FacetDecision FacetDecision::violation(std::string detail) { - return FacetDecision{.outcome = SiteOutcome::Violation, - .detail = std::move(detail)}; -} - -FacetDecision FacetDecision::unresolvedFor(UnresolvedReason reason, - std::string detail) { - return FacetDecision{.outcome = SiteOutcome::Unresolved, - .unresolved = reason, - .detail = std::move(detail)}; -} - -FacetDecision FacetDecision::trustedFor(TrustReason reason, - std::string detail) { - return FacetDecision{.outcome = SiteOutcome::Trusted, - .trusted = reason, - .detail = std::move(detail)}; -} - -bool FacetDecision::isWellFormed() const noexcept { - return (outcome == SiteOutcome::Unresolved) == unresolved.has_value() && - (outcome == SiteOutcome::Trusted) == trusted.has_value(); -} - -std::string_view FacetDecision::reasonText() const noexcept { - if (unresolved) - return toString(*unresolved); - if (trusted) - return toString(*trusted); - return {}; -} - -bool FacetDecision::losesTo(const FacetDecision &other) const noexcept { - return outcomeRank(other.outcome) > outcomeRank(outcome); -} - -void FacetRecord::decide(const FacetDecision &record) { - if (!decided) { - decision = record; - decided = true; - return; - } - if (decision.losesTo(record)) { - decision = record; - return; - } - // Equal rank keeps the earlier record; a matching later record may still - // supply the detail the earlier one lacked. - if (decision.detail.empty() && record.outcome == decision.outcome && - record.unresolved == decision.unresolved && - record.trusted == decision.trusted) - decision.detail = record.detail; -} - -void FacetRecord::reset() { - *this = FacetRecord{}; -} - -FacetDecision defaultDecision(bool overBudget) { - return FacetDecision::unresolvedFor( - overBudget ? UnresolvedReason::Budget : UnresolvedReason::Unanalysed); -} - -//===----------------------------------------------------------------------===// -// Sites, functions, units -//===----------------------------------------------------------------------===// - -static bool isAsciiSpace(char c) noexcept { - return c == ' ' || c == '\t' || c == '\n' || c == '\v' || c == '\f' || - c == '\r'; -} - -static bool isUtf8Continuation(char c) noexcept { - return (static_cast(c) & 0xC0U) == 0x80U; -} - -std::string siteText(std::string_view source) { - std::string text; - text.reserve(std::min(source.size(), MaxSiteTextBytes + 4)); - for (const char c : source) { - if (!isAsciiSpace(c)) - text += c; - } - if (text.size() > MaxSiteTextBytes) { - std::size_t cut = MaxSiteTextBytes; - // Never split a UTF-8 sequence: back up to the start of the sequence - // the first dropped byte continues. - while (cut > 0 && isUtf8Continuation(text[cut])) - --cut; - text.resize(cut); - } - return text; -} - -FacetRecord &Site::addFacet(Facet facet) { - std::optional &slot = facets.at(static_cast(facet)); - if (!slot) - slot.emplace(); - return *slot; -} - -FacetRecord *Site::facet(Facet facet) { - std::optional &slot = facets.at(static_cast(facet)); - return slot ? &*slot : nullptr; -} - -const FacetRecord *Site::facet(Facet facet) const { - const std::optional &slot = - facets.at(static_cast(facet)); - return slot ? &*slot : nullptr; -} - -SiteOutcome Site::outcome() const noexcept { - SiteOutcome result = SiteOutcome::Proven; - for (const std::optional &record : facets) { - if (record) - result = maxByRank(result, record->outcome()); - } - return result; -} - -Site *FunctionLedger::site(std::uint32_t ordinal) noexcept { - if (ordinal < sites.size() && sites[ordinal].ordinal == ordinal) - return &sites[ordinal]; - const auto found = std::ranges::find(sites, ordinal, &Site::ordinal); - return found == sites.end() ? nullptr : &*found; -} - -const Site *FunctionLedger::site(std::uint32_t ordinal) const noexcept { - if (ordinal < sites.size() && sites[ordinal].ordinal == ordinal) - return &sites[ordinal]; - const auto found = std::ranges::find(sites, ordinal, &Site::ordinal); - return found == sites.end() ? nullptr : &*found; -} - -void FunctionLedger::resetDecisions() { - for (Site &site : sites) { - for (std::optional &record : site.facets) { - if (record) - record->reset(); - } - } -} - -Site *UnitLedger::site(SiteId id) noexcept { - return id.function < functions.size() - ? functions[id.function].site(id.ordinal) - : nullptr; -} - -const Site *UnitLedger::site(SiteId id) const noexcept { - return id.function < functions.size() - ? functions[id.function].site(id.ordinal) - : nullptr; -} - -std::vector completenessProblems(const Ledger &ledger) { - std::vector problems; - for (std::size_t u = 0; u < ledger.units.size(); ++u) { - const UnitLedger &unit = ledger.units[u]; - for (const FunctionLedger &function : unit.functions) { - const std::string where = "unit " + std::to_string(u) + " ('" + - unit.source + "'), function '" + function.name + - "'"; - for (std::size_t s = 0; s < function.sites.size(); ++s) { - const Site &site = function.sites[s]; - const std::string at = where + ", site " + std::to_string(s); - if (site.ordinal != s) - problems.push_back(at + ": ordinal " + std::to_string(site.ordinal)); - for (const Facet facet : AllFacets) { - const FacetRecord *record = site.facet(facet); - if (record == nullptr) - continue; - const std::string facetAt = - at + ": " + std::string(toString(facet)) + " facet"; - if (!record->decided) - problems.push_back(facetAt + " is undecided"); - else if (!record->decision.isWellFormed()) - problems.push_back(facetAt + " has a reason that does not fit '" + - std::string(toString(record->outcome())) + "'"); - if (record->diagnostic && - *record->diagnostic >= ledger.diagnostics.size()) - problems.push_back(facetAt + " links to missing diagnostic " + - std::to_string(*record->diagnostic)); - } - } - } - } - return problems; -} - -void sortDiagnostics(Ledger &ledger) { - std::vector order(ledger.diagnostics.size()); - std::iota(order.begin(), order.end(), 0U); - const auto key = [&](std::uint32_t index) { - const LedgerDiagnostic &diagnostic = ledger.diagnostics[index]; - return std::tie(diagnostic.location.file, diagnostic.location.line, - diagnostic.location.column, diagnostic.id, - diagnostic.message); - }; - std::ranges::stable_sort( - order, [&](std::uint32_t a, std::uint32_t b) { return key(a) < key(b); }); - std::vector position(order.size()); - std::vector sorted; - sorted.reserve(order.size()); - for (std::size_t i = 0; i < order.size(); ++i) { - position[order[i]] = static_cast(i); - sorted.push_back(std::move(ledger.diagnostics[order[i]])); - } - ledger.diagnostics = std::move(sorted); - for (UnitLedger &unit : ledger.units) { - for (FunctionLedger &function : unit.functions) { - for (Site &site : function.sites) { - for (std::optional &record : site.facets) { - if (record && record->diagnostic && - *record->diagnostic < position.size()) - record->diagnostic = position[*record->diagnostic]; - } - } - } - } -} - -//===----------------------------------------------------------------------===// -// Rollup -//===----------------------------------------------------------------------===// - -void OutcomeCounts::add(SiteOutcome outcome, std::uint64_t count) noexcept { - switch (outcome) { - case SiteOutcome::Proven: - proven += count; - return; - case SiteOutcome::Violation: - violation += count; - return; - case SiteOutcome::Unresolved: - unresolved += count; - return; - case SiteOutcome::Trusted: - trusted += count; - return; - } -} - -void LedgerSummary::addUnit(const UnitLedger &unit) { - for (const FunctionLedger &function : unit.functions) { - if (function.overBudget) - overBudget.push_back(function.name); - for (const Site &site : function.sites) { - ++sites; - outcomes.add(site.outcome()); - } - } -} - -void LedgerSummary::addDiagnostic(const LedgerDiagnostic &diagnostic) noexcept { - if (diagnostic.severity == Severity::Error) - ++errors; - else if (diagnostic.severity == Severity::Warning) - ++warnings; -} - -LedgerSummary summarize(const Ledger &ledger) { - LedgerSummary summary; - for (const UnitLedger &unit : ledger.units) - summary.addUnit(unit); - for (const LedgerDiagnostic &diagnostic : ledger.diagnostics) - summary.addDiagnostic(diagnostic); - return summary; -} - -//===----------------------------------------------------------------------===// -// The summary line -//===----------------------------------------------------------------------===// - -std::string formatThousands(std::uint64_t value) { - const std::string digits = std::to_string(value); - std::string text; - text.reserve(digits.size() + (digits.size() / 3)); - for (std::size_t i = 0; i < digits.size(); ++i) { - if (i != 0 && (digits.size() - i) % 3 == 0) - text += ','; - text += digits[i]; - } - return text; -} - -/// `1 site`, `4,210 sites`. -static std::string counted(std::uint64_t count, std::string_view noun) { - std::string text = formatThousands(count); - text += ' '; - text += noun; - if (count != 1) - text += 's'; - return text; -} - -static std::string joined(std::span items) { - std::string text; - for (const std::string &item : items) { - if (!text.empty()) - text += ", "; - text += item; - } - return text; -} - -/// `3,050 proven, 1,130 not proven, 0 violations, 30 trusted`. -static std::string outcomesText(const OutcomeCounts &outcomes) { - return formatThousands(outcomes.proven) + " proven, " + - formatThousands(outcomes.unresolved) + " not proven, " + - counted(outcomes.violation, "violation") + ", " + - formatThousands(outcomes.trusted) + " trusted"; -} - -/// `; 0 errors, 2 warnings[; 1 function over budget ()]`. -static std::string findingsText(const LedgerSummary &summary) { - std::string text = "; " + counted(summary.errors, "error") + ", " + - counted(summary.warnings, "warning"); - if (!summary.overBudget.empty()) - text += "; " + counted(summary.overBudget.size(), "function") + - " over budget (" + joined(summary.overBudget) + ")"; - return text; -} - -std::string unitSummaryLine(std::string_view source, - const LedgerSummary &summary) { - std::string text = "weavec: "; - text += source; - text += ": " + counted(summary.sites, "site") + ": " + - outcomesText(summary.outcomes) + findingsText(summary); - return text; -} - -std::string programSummaryLine(std::string_view program, std::size_t units, - const LedgerSummary &summary) { - std::string text = "weavec: program "; - text += program; - text += ": " + counted(summary.sites, "site") + " in " + - counted(units, "unit") + ": " + outcomesText(summary.outcomes) + - findingsText(summary); - return text; -} - -} // namespace weavec::core diff --git a/lib/Core/LibrarySpec.cpp b/lib/Core/LibrarySpec.cpp index 4f92b6c2..45e4fceb 100644 --- a/lib/Core/LibrarySpec.cpp +++ b/lib/Core/LibrarySpec.cpp @@ -1,4 +1,4 @@ -//===- LibrarySpec.cpp - The declarative C library table ------------------===// +//===- LibrarySpec.cpp - The C library table of the guards ----------------===// // // Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. // See LICENSE for license information. @@ -6,11 +6,10 @@ // //===----------------------------------------------------------------------===// // -// RFC 0030 §8. The parser reads the `LibrarySpec.txt` syntax documented at +// RFC 0036 §6. The parser reads the `LibrarySpec.txt` syntax documented at // the top of that file: comments and continuations are removed first, giving -// logical lines that each hold one directive or one entry; each entry is then -// parsed by recursive descent and validated on its own, and the table as a -// whole is validated last (overloads, aliases). +// logical lines that each hold one entry; each entry is parsed by recursive +// descent and validated on its own, and the aliases are indexed last. // //===----------------------------------------------------------------------===// @@ -18,7 +17,6 @@ #include #include -#include #include #include #include @@ -43,11 +41,6 @@ LibTerm LibTerm::stringLength(unsigned index) { .arg = static_cast(index)}; } -LibTerm LibTerm::formatLength(unsigned index) { - return LibTerm{.kind = Kind::FormatLength, - .arg = static_cast(index)}; -} - static LibTerm binary(LibTerm::Kind kind, LibTerm left, LibTerm right) { LibTerm term{.kind = kind}; term.operands.push_back(std::move(left)); @@ -96,10 +89,6 @@ std::string LibTerm::str() const { return "a" + std::to_string(arg); case Kind::StringLength: return "strlen(a" + std::to_string(arg) + ")"; - case Kind::FormatLength: - return "fmtlen(a" + std::to_string(arg) + ")"; - case Kind::Macro: - return macro; case Kind::Product: return parenthesised(operands[0], isAdditive(operands[0])) + "*" + parenthesised(operands[1], isAdditive(operands[1]) || @@ -158,10 +147,6 @@ std::optional LibTerm::evaluate(const Values &values) const { return ask(values.argument, arg); case Kind::StringLength: return ask(values.stringLength, arg); - case Kind::FormatLength: - return ask(values.formatLength, arg); - case Kind::Macro: - return values.macro ? values.macro(macro) : std::nullopt; case Kind::Difference: { const auto left = operands[0].evaluate(values); if (!left || value == std::numeric_limits::min()) @@ -192,103 +177,13 @@ std::optional LibTerm::evaluate(const Values &values) const { } //===----------------------------------------------------------------------===// -// Results, entries and matches +// Entries and matches //===----------------------------------------------------------------------===// -bool LibraryResult::isPointer() const noexcept { - return kind != Kind::Void && kind != Kind::Int; -} - const LibraryParam *LibraryEntry::param(unsigned index) const noexcept { return index < params.size() ? ¶ms[index] : nullptr; } -bool LibraryEntry::allocates() const noexcept { - if (result.kind == LibraryResult::Kind::Fresh || - (result.kind == LibraryResult::Kind::Arg && !result.family.empty())) - return true; - return std::ranges::any_of(params, [](const LibraryParam &p) { - return p.out && p.out->kind == LibraryResult::Kind::Fresh; - }); -} - -bool LibraryEntry::releases() const noexcept { - return std::ranges::any_of(params, [](const LibraryParam &p) { - return p.effect == LibraryParam::Effect::Release || - p.effect == LibraryParam::Effect::Realloc; - }); -} - -bool LibraryEntry::hasCallback() const noexcept { - return std::ranges::any_of( - params, [](const LibraryParam &p) { return p.callback.has_value(); }); -} - -bool LibraryEntry::usesValueOnly(unsigned index) const noexcept { - const LibraryParam *p = param(index); - if (p == nullptr || p->type != LibraryParam::Type::Pointer || - p->access != LibraryParam::Access::None || p->bytes || p->count || - p->string || p->effect != LibraryParam::Effect::Borrow || p->callback || - p->out) - return false; - const auto names = [index](unsigned argument) { return argument == index; }; - for (const LibraryParam &other : params) - if (other.callback && std::ranges::any_of(other.callback->arguments, names)) - return false; - return std::ranges::none_of(disjoint, - [&](const LibDisjoint &d) { - return names(d.first) || names(d.second); - }) && - std::ranges::none_of( - copies, - [&](const LibCopy &c) { return names(c.dst) || names(c.src); }) && - std::ranges::none_of(fills, - [&](const LibFill &f) { return names(f.dst); }) && - std::ranges::none_of( - writesString, - [&](const LibStringWrite &w) { return names(w.dst); }) && - (!format || !names(format->format)); -} - -bool LibraryEntry::knownToReturn() const noexcept { - return !noreturn && !exits; -} - -bool LibraryEntry::trustsLibrarySpec() const noexcept { - const auto hidden = [](const LibraryResult &value) { - return value.kind == LibraryResult::Kind::Static || - value.kind == LibraryResult::Kind::InteriorState || - !value.state.empty(); - }; - if (hidden(result) || !invalidates.empty() || !reads.empty()) - return true; - return std::ranges::any_of(params, [&hidden](const LibraryParam &p) { - return p.effect == LibraryParam::Effect::Retain || p.callback || - (p.out && hidden(*p.out)); - }); -} - -LibSignature LibraryEntry::signature() const { - LibSignature signature{.variadic = variadic, - .pointerResult = result.isPointer()}; - for (const LibraryParam &p : params) - signature.params.push_back(p.type); - return signature; -} - -bool LibraryEntry::accepts(const LibSignature &declared) const { - if (declared.params.size() != params.size() || - declared.variadic != variadic || - declared.pointerResult != result.isPointer()) - return false; - for (std::size_t i = 0; i < params.size(); ++i) { - const LibraryParam::Type row = params[i].type; - if (row != LibraryParam::Type::Other && row != declared.params[i]) - return false; - } - return true; -} - int LibraryMatch::rowArgument(unsigned callArgument) const noexcept { if (alias == nullptr) return static_cast(callArgument); @@ -314,37 +209,13 @@ int LibraryMatch::callArgument(unsigned rowArg) const noexcept { (static_cast(rowArg) - map.back()); } -const LibraryParam *LibraryMatch::param(unsigned callArgument) const noexcept { - const int row = rowArgument(callArgument); - return entry == nullptr || row < 0 ? nullptr - : entry->param(static_cast(row)); -} - -/// The nullability a result kind has when the row does not say. -static LibraryResult::Null defaultNull(LibraryResult::Kind kind) { - switch (kind) { - case LibraryResult::Kind::Fresh: - return LibraryResult::Null::OnFailure; - case LibraryResult::Kind::Interior: - case LibraryResult::Kind::InteriorState: - case LibraryResult::Kind::Unknown: - return LibraryResult::Null::May; - case LibraryResult::Kind::Void: - case LibraryResult::Kind::Int: - case LibraryResult::Kind::Static: - case LibraryResult::Kind::Arg: - return LibraryResult::Null::Never; - } - return LibraryResult::Null::Never; -} - //===----------------------------------------------------------------------===// // Lexical helpers //===----------------------------------------------------------------------===// namespace { -/// One logical line: a directive or an entry, with comments removed and -/// continuations joined, and the physical line of each character. +/// One logical line: an entry, with comments removed and continuations +/// joined, and the physical line of each character. struct LogicalLine { std::string text; std::vector lines; @@ -394,14 +265,8 @@ static std::vector logicalLines(std::string_view text) { static bool isDigit(char c) { return c >= '0' && c <= '9'; } -static bool isUpper(char c) { - return c >= 'A' && c <= 'Z'; -} -static bool isLower(char c) { - return c >= 'a' && c <= 'z'; -} static bool isIdentifierStart(char c) { - return isUpper(c) || isLower(c) || c == '_'; + return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_'; } static bool isIdentifierChar(char c) { return isIdentifierStart(c) || isDigit(c); @@ -412,54 +277,6 @@ static bool isIdentifier(std::string_view text) { std::ranges::all_of(text, isIdentifierChar); } -static std::optional accessNamed(std::string_view word) { - using Access = LibraryParam::Access; - if (word == "r") - return Access::Read; - if (word == "w") - return Access::Write; - if (word == "rw") - return Access::ReadWrite; - if (word == "none") - return Access::None; - return std::nullopt; -} - -/// The effect a flag names; `escape` for the one without a family. -static LibraryParam::Effect effectNamed(std::string_view flag) { - using Effect = LibraryParam::Effect; - if (flag == "release") - return Effect::Release; - if (flag == "realloc") - return Effect::Realloc; - if (flag == "retain") - return Effect::Retain; - if (flag == "init") - return Effect::Init; - if (flag == "fini") - return Effect::Fini; - return Effect::Escape; -} - -static LibraryResult::Null nullNamed(std::string_view flag) { - if (flag == "nonnull") - return LibraryResult::Null::Never; - if (flag == "null-on-failure") - return LibraryResult::Null::OnFailure; - return LibraryResult::Null::May; -} - -/// The key a flag is recorded under: flags that exclude one another share -/// the key of their group. -static std::string -exclusionKey(const std::string &flag, - std::initializer_list> groups) { - for (const auto &[member, group] : groups) - if (member) - return std::string(group); - return flag; -} - namespace { /// Reads terms from `text` starting at `pos`, advancing `pos`. class TermReader { @@ -550,13 +367,6 @@ class TermReader { } return left; } - std::optional lengthOf(bool format) { - const auto index = argumentIndex(); - if (!index || !accept(")")) - return fail(format ? "expected 'fmtlen(aN)'" : "expected 'strlen(aN)'"); - return format ? LibTerm::formatLength(*index) - : LibTerm::stringLength(*index); - } std::optional atom() { skipSpace(); if (pos >= text.size()) @@ -573,10 +383,12 @@ class TermReader { return fail("expected ')' in a term"); return inner; } - if (accept("strlen(")) - return lengthOf(false); - if (accept("fmtlen(")) - return lengthOf(true); + if (accept("strlen(")) { + const auto index = argumentIndex(); + if (!index || !accept(")")) + return fail("expected 'strlen(aN)'"); + return LibTerm::stringLength(*index); + } if (accept("min(")) { auto left = sum(); if (!left) @@ -596,13 +408,6 @@ class TermReader { return fail("argument index out of range in a term"); return LibTerm::argument(*index); } - if (isUpper(text[pos]) || text[pos] == '_') { - const std::size_t begin = pos; - while (pos < text.size() && isIdentifierChar(text[pos])) - ++pos; - return LibTerm{.kind = LibTerm::Kind::Macro, - .macro = std::string(text.substr(begin, pos - begin))}; - } return fail("expected a term"); } }; @@ -640,18 +445,14 @@ class LibrarySpecParser { std::string message; const LogicalLine *line = nullptr; std::size_t pos = 0; - /// The header entries are declared in; empty under `builtins`. - std::string header; - bool haveSection = false; - bool underPattern = false; /// The entry being parsed, for messages. std::string entryName; - bool fail(const std::string &why, std::optional at = {}) { + bool fail(const std::string &why) { if (!message.empty()) return false; - unsigned number = at.value_or(0); - if (!at && line != nullptr && !line->lines.empty()) + unsigned number = 0; + if (line != nullptr && !line->lines.empty()) number = line->lines[std::min(pos, line->lines.size() - 1)]; message = "LibrarySpec.txt:" + std::to_string(number) + ": "; if (!entryName.empty()) @@ -667,12 +468,9 @@ class LibrarySpecParser { skipSpace(); return pos >= line->text.size(); } - bool peek(char c) { - skipSpace(); - return pos < line->text.size() && line->text[pos] == c; - } bool accept(char c) { - if (!peek(c)) + skipSpace(); + if (pos >= line->text.size() || line->text[pos] != c) return false; ++pos; return true; @@ -707,448 +505,199 @@ class LibrarySpecParser { } return text; } - std::optional parenthesisedName(std::string_view flag, - std::string_view what) { - const std::string context = "after '" + std::string(flag) + "'"; - if (!expect('(', context)) - return std::nullopt; - auto text = name(what); - if (!text || !expect(')', "after " + std::string(what))) - return std::nullopt; - return text; - } - std::optional integer(bool allowMinusOne) { + std::optional index(std::string_view what) { skipSpace(); - const bool negative = allowMinusOne && accept('-'); const std::size_t begin = pos; - std::int64_t value = 0; - while (pos < line->text.size() && isDigit(line->text[pos]) && - value < 1000000) - value = (value * 10) + (line->text[pos++] - '0'); - if (pos == begin || (negative && value != 1)) { - fail(negative ? "only -1 may be negative" : "expected an integer"); + unsigned value = 0; + while (pos < line->text.size() && isDigit(line->text[pos]) && value < 1000) + value = (value * 10) + static_cast(line->text[pos++] - '0'); + if (pos == begin) { + fail("expected " + std::string(what)); return std::nullopt; } - return negative ? -value : value; - } - std::optional index(std::string_view what) { - const auto value = integer(false); - if (!value) - return std::nullopt; - if (*value > 255) { + if (value > 255) { fail(std::string(what) + " out of range"); return std::nullopt; } - return static_cast(*value); + return static_cast(value); } - /// A term inside a clause's argument list, after its '(' or a ','. - std::optional clauseTerm(std::string_view clause) { + std::optional term(std::string_view where) { TermReader reader(line->text, pos); - auto term = reader.read(); - if (!term) - fail(reader.error() + " in '" + std::string(clause) + "(…)'"); - return term; + auto result = reader.read(); + if (!result) + fail(reader.error() + " in '" + std::string(where) + "(…)'"); + return result; } std::optional parenthesisedTerm(std::string_view flag) { if (!expect('(', "after '" + std::string(flag) + "'")) return std::nullopt; - TermReader reader(line->text, pos); - auto term = reader.read(); - if (!term) { - fail(reader.error() + " in '" + std::string(flag) + "(…)'"); + auto result = term(flag); + if (!result || + !expect(')', "after the term of '" + std::string(flag) + "'")) return std::nullopt; - } - if (!expect(')', "after the term of '" + std::string(flag) + "'")) - return std::nullopt; - return term; + return result; } - bool parseLine(); - bool parseHeader(); - bool parseEntry(std::string rowName); + bool parseEntry(); std::optional parseParam(); - bool parseParamFlag(LibraryParam ¶m, const std::string &flag, - std::set &seen); - std::optional parseCallback(const std::string &flag); - std::optional parseResult(bool out, bool &noreturn); - bool parseResultFlag(LibraryResult &result, const std::string &flag, bool out, - std::set &seen); + bool parseResult(LibraryEntry &entry); bool parseClause(LibraryEntry &entry, const std::string &clause); bool validateTerm(const LibraryEntry &entry, const LibTerm &term, std::string_view where); - bool validateResult(const LibraryEntry &entry, const LibraryResult &result, - std::string_view where); - bool validateEntry(LibraryEntry &entry); - bool validateTable(); + bool validateEntry(const LibraryEntry &entry); }; std::optional LibrarySpecParser::parseParam() { - using Type = LibraryParam::Type; - const std::string kind = word(); LibraryParam param; - if (kind == "int" || kind == "other") { - param.type = kind == "int" ? Type::Int : Type::Other; - if (peek(':')) { - fail("'" + kind + "' parameters take no flags"); - return std::nullopt; - } + const std::string head = word(); + if (head == "int") { + param.type = LibraryParam::Type::Int; return param; } - if (kind == "fn") { - param.type = Type::Function; - } else if (const auto access = accessNamed(kind)) { - param.type = Type::Pointer; - param.access = *access; - } else { - fail(kind.empty() ? "expected a parameter" - : "unknown parameter type '" + kind + "'"); + if (head == "other") { + param.type = LibraryParam::Type::Other; + return param; + } + if (head == "fn") { + param.type = LibraryParam::Type::Function; + return param; + } + param.type = LibraryParam::Type::Pointer; + if (head == "r") + param.access = LibraryParam::Access::Read; + else if (head == "w") + param.access = LibraryParam::Access::Write; + else if (head == "rw") + param.access = LibraryParam::Access::ReadWrite; + else if (head == "none") + param.access = LibraryParam::Access::None; + else { + fail("expected a parameter, found '" + head + "'"); return std::nullopt; } std::set seen; while (accept(':')) { const std::string flag = word(); - if (!parseParamFlag(param, flag, seen)) - return std::nullopt; - } - return param; -} - -std::optional -LibrarySpecParser::parseCallback(const std::string &flag) { - LibCallback callback; - if (flag == "at-exit") { - callback.kind = LibCallback::Kind::AtExit; - return callback; - } - callback.kind = - flag == "sync" ? LibCallback::Kind::Sync : LibCallback::Kind::Entry; - if (!expect('(', "after '" + flag + "'")) - return std::nullopt; - if (!accept(')')) { - do { - const auto argument = index("callback argument"); - if (!argument) - return std::nullopt; - callback.arguments.push_back(*argument); - } while (accept(',')); - if (!expect(')', "after the arguments of '" + flag + "(…)'")) - return std::nullopt; - } - if (callback.kind == LibCallback::Kind::Entry && - callback.arguments.size() > 1) { - fail("'entry(…)' takes at most one argument"); - return std::nullopt; - } - return callback; -} - -bool LibrarySpecParser::parseParamFlag(LibraryParam ¶m, - const std::string &flag, - std::set &seen) { - using Effect = LibraryParam::Effect; - const bool function = param.type == LibraryParam::Type::Function; - const bool callback = flag == "sync" || flag == "entry" || flag == "at-exit"; - const bool effect = flag == "release" || flag == "realloc" || - flag == "retain" || flag == "escape" || flag == "init" || - flag == "fini"; - const bool null = flag == "null-ok" || flag == "null-if-zero"; - const std::string key = exclusionKey( - flag, {{callback, "callback"}, {effect, "effect"}, {null, "null"}}); - if (!seen.insert(key).second) - return fail(key == flag ? "duplicate flag '" + flag + "'" - : "conflicting " + key + " flags ('" + flag + "')"); - if (function && !callback && flag != "null-ok") - return fail("'" + flag + "' is not a flag of 'fn' parameters"); - if (flag == "bytes" || flag == "count") { - auto term = parenthesisedTerm(flag); - if (!term) - return false; - if ((flag == "bytes" && param.count) || (flag == "count" && param.bytes)) - return fail("'bytes' and 'count' exclude one another"); - (flag == "bytes" ? param.bytes : param.count) = std::move(*term); - } else if (flag == "str") { - param.string = true; - } else if (flag == "null-ok") { - param.null = LibraryParam::Null::Allowed; - } else if (flag == "null-if-zero") { - auto term = parenthesisedTerm(flag); - if (!term) - return false; - param.null = LibraryParam::Null::AllowedIfZero; - param.zeroTerm = std::move(*term); - } else if (flag == "range") { - auto term = parenthesisedTerm(flag); - if (!term) - return false; - param.range = std::move(*term); - } else if (flag == "escape") { - param.effect = Effect::Escape; - } else if (effect) { - const bool state = flag == "retain"; - auto text = parenthesisedName(flag, state ? "a state slot" : "a family"); - if (!text) - return false; - param.effect = effectNamed(flag); - (state ? param.state : param.family) = std::move(*text); - } else if (callback) { - auto parsed = parseCallback(flag); - if (!parsed) - return false; - param.callback = std::move(*parsed); - } else if (flag == "out") { - if (!expect('(', "after 'out'")) - return false; - bool noreturn = false; - auto value = parseResult(true, noreturn); - if (!value || !expect(')', "after the value of 'out(…)'")) - return false; - param.out = std::move(*value); - } else { - return fail(flag.empty() ? "expected a parameter flag after ':'" - : "unknown parameter flag '" + flag + "'"); - } - return true; -} - -std::optional LibrarySpecParser::parseResult(bool out, - bool &noreturn) { - using Kind = LibraryResult::Kind; - const std::string kind = word(); - LibraryResult result; - if (kind == "void" || kind == "int" || kind == "noreturn") { - if (out) { - fail("an 'out' value must be a pointer, not '" + kind + "'"); + if (!seen.insert(flag).second) { + fail("duplicate flag '" + flag + "'"); return std::nullopt; } - result.kind = kind == "int" ? Kind::Int : Kind::Void; - noreturn = kind == "noreturn"; - if (kind == "int" && accept(':')) { - if (word() != "value") { - fail("the only flag of an 'int' result is 'value(…)'"); + if (flag == "bytes" || flag == "count") { + auto value = parenthesisedTerm(flag); + if (!value) return std::nullopt; - } - result.value = parenthesisedTerm("value"); - if (!result.value) + (flag == "bytes" ? param.bytes : param.count) = std::move(*value); + } else if (flag == "str") { + param.string = true; + } else if (flag == "null-ok") { + param.null = LibraryParam::Null::Allowed; + } else if (flag == "null-if-zero") { + auto value = parenthesisedTerm(flag); + if (!value) return std::nullopt; - } - if (peek(':')) { - fail("'" + kind + "' results take no " + - (kind == "int" ? "other flags" : "flags")); + param.null = LibraryParam::Null::AllowedIfZero; + param.zeroTerm = std::move(*value); + } else if (flag == "release") { + param.effect = LibraryParam::Effect::Release; + } else if (flag == "realloc") { + param.effect = LibraryParam::Effect::Realloc; + } else { + fail("unknown parameter flag '" + flag + "'"); return std::nullopt; } - return result; } - if (kind == "fresh" || kind == "static" || kind == "interior-state") { - const bool family = kind == "fresh"; - auto text = parenthesisedName(kind, family ? "a family" : "a state slot"); - if (!text) - return std::nullopt; - result.kind = Kind::InteriorState; - if (family) - result.kind = Kind::Fresh; - else if (kind == "static") - result.kind = Kind::Static; - (family ? result.family : result.state) = std::move(*text); - } else if (kind == "arg" || kind == "interior") { - if (!expect('(', "after '" + kind + "'")) - return std::nullopt; - const auto argument = index("argument index"); - if (!argument || !expect(')', "after the argument index")) - return std::nullopt; - result.kind = kind == "arg" ? Kind::Arg : Kind::Interior; - result.arg = *argument; - } else if (kind == "ptr") { - result.kind = Kind::Unknown; - } else { - fail(kind.empty() ? "expected a result" - : "unknown result kind '" + kind + "'"); - return std::nullopt; - } - result.null = defaultNull(result.kind); - std::set seen; - while (accept(':')) { - const std::string flag = word(); - if (!parseResultFlag(result, flag, out, seen)) - return std::nullopt; - } - const bool zeroFlag = seen.contains("zero"); - const bool fresh = result.kind == Kind::Fresh || - (result.kind == Kind::Arg && !result.family.empty()); - if (zeroFlag && !fresh) { - fail("'zero-init' and 'no-zero-init' apply only to fresh results"); - return std::nullopt; - } - if (fresh && result.family == HeapFamily && !zeroFlag) { - fail("a 'fresh(free)' value needs 'zero-init' or 'no-zero-init' (§11)"); - return std::nullopt; - } - if (result.extent && result.kind != Kind::Fresh && - result.kind != Kind::Static) { - fail("'extent' applies only to fresh and static results"); + if (seen.contains("null-ok") && seen.contains("null-if-zero")) { + fail("'null-ok' and 'null-if-zero' exclude each other"); return std::nullopt; } - if (result.offset && result.kind != Kind::Interior) { - fail("'offset' applies only to 'interior(N)' results"); + if (param.bytes && param.count) { + fail("'bytes' and 'count' exclude each other"); return std::nullopt; } - if (result.zeroFilled && (result.kind != Kind::Fresh || !result.extent)) { - fail("'zero-filled' applies only to fresh results with an extent"); + if (param.string && param.access != LibraryParam::Access::Read && + param.access != LibraryParam::Access::ReadWrite) { + fail("'str' needs 'r' or 'rw'"); return std::nullopt; } - return result; + return param; } -bool LibrarySpecParser::parseResultFlag(LibraryResult &result, - const std::string &flag, bool out, - std::set &seen) { - const bool null = - flag == "null-on-failure" || flag == "nonnull" || flag == "null-ok"; - const bool zero = flag == "zero-init" || flag == "no-zero-init"; - const bool alternative = flag == "or-fresh" || flag == "or-static"; - const std::string key = exclusionKey( - flag, {{null, "null"}, {zero, "zero"}, {alternative, "alternative"}}); - if (!seen.insert(key).second) - return fail(key == flag ? "duplicate flag '" + flag + "'" - : "conflicting " + key + " flags ('" + flag + "')"); - if (flag == "extent" || flag == "offset") { - auto term = parenthesisedTerm(flag); - if (!term) - return false; - (flag == "extent" ? result.extent : result.offset) = std::move(*term); - } else if (null) { - result.null = nullNamed(flag); - } else if (zero) { - result.zeroInit = flag == "zero-init"; - } else if (flag == "zero-filled") { - result.zeroFilled = true; - } else if (flag == "str") { - result.string = true; - } else if (flag == "replaces") { - if (!out || result.kind != LibraryResult::Kind::Fresh) - return fail("'replaces' applies only to a fresh 'out' value"); - result.replaces = true; - } else if (alternative) { - if (result.kind != LibraryResult::Kind::Arg) - return fail("'" + flag + "' applies only to 'arg(N)' results"); - const bool fresh = flag == "or-fresh"; - auto text = parenthesisedName(flag, fresh ? "a family" : "a state slot"); - if (!text) - return false; - (fresh ? result.family : result.state) = std::move(*text); +bool LibrarySpecParser::parseResult(LibraryEntry &entry) { + const std::string kind = word(); + if (kind == "void") { + entry.result = LibraryEntry::Result::Void; + } else if (kind == "ptr") { + entry.result = LibraryEntry::Result::Pointer; + } else if (kind == "int") { + entry.result = LibraryEntry::Result::Int; + if (accept(':')) { + if (word() != "value") + return fail("expected 'value(…)' after 'int:'"); + entry.value = parenthesisedTerm("value"); + if (!entry.value) + return false; + } } else { - return fail(flag.empty() ? "expected a result flag after ':'" - : "unknown result flag '" + flag + "'"); + return fail("expected 'void', 'int' or 'ptr', found '" + kind + "'"); } return true; } bool LibrarySpecParser::parseClause(LibraryEntry &entry, const std::string &clause) { - if (clause == "exits" || clause == "returns-twice" || clause == "wide") { - bool *flag = &entry.returnsTwice; - if (clause == "exits") - flag = &entry.exits; - else if (clause == "wide") - flag = &entry.wide; - if (*flag) - return fail("duplicate clause '" + clause + "'"); - *flag = true; - } else if (clause == "invalidates" || clause == "reads") { - auto state = parenthesisedName(clause, "a state slot"); - if (!state) - return false; - auto &list = clause == "reads" ? entry.reads : entry.invalidates; - if (std::ranges::find(list, *state) != list.end()) - return fail("duplicate clause '" + clause + "(" + *state + ")'"); - list.push_back(std::move(*state)); - } else if (clause == "disjoint") { + if (clause == "wide") { + if (entry.wide) + return fail("duplicate clause 'wide'"); + entry.wide = true; + return true; + } + if (clause == "disjoint") { if (!expect('(', "after 'disjoint'")) return false; - LibDisjoint disjoint; - const auto first = index("argument index"); - if (!first || !expect(',', "in 'disjoint(…)'")) - return false; - const auto second = index("argument index"); - if (!second || !expect(',', "in 'disjoint(…)'")) - return false; - TermReader reader(line->text, pos); - auto length = reader.read(); - if (!length) - return fail(reader.error() + " in 'disjoint(…)'"); - if (!expect(')', "after 'disjoint(…'")) - return false; - disjoint.first = *first; - disjoint.second = *second; - disjoint.length = std::move(*length); - entry.disjoint.push_back(std::move(disjoint)); - } else if (clause == "copies" || clause == "fills") { - const std::string context = "in '" + clause + "(…)'"; - if (!expect('(', "after '" + clause + "'")) - return false; - const auto dst = index("argument index"); - if (!dst || !expect(',', context)) - return false; - std::optional src; - std::optional value; - if (clause == "copies") - src = index("argument index"); - else - value = clauseTerm(clause); - if ((!src && !value) || !expect(',', context)) - return false; - auto length = clauseTerm(clause); - if (!length || !expect(')', "after '" + clause + "(…'")) - return false; - if (src) - entry.copies.push_back( - LibCopy{.dst = *dst, .src = *src, .length = std::move(*length)}); - else - entry.fills.push_back(LibFill{.dst = *dst, - .value = std::move(*value), - .length = std::move(*length)}); - } else if (clause == "writes-str") { - if (!expect('(', "after 'writes-str'")) + auto first = index("an argument index"); + if (!first || !expect(',', "in 'disjoint'")) return false; - const auto dst = index("argument index"); - if (!dst) + auto second = index("an argument index"); + if (!second || !expect(',', "in 'disjoint'")) return false; - LibStringWrite write{.dst = *dst, .length = std::nullopt}; - if (accept(',')) { - write.length = clauseTerm(clause); - if (!write.length) - return false; - } - if (!expect(')', "after 'writes-str(…'")) + auto length = term("disjoint"); + if (!length || !expect(')', "after 'disjoint(…'")) return false; - entry.writesString.push_back(std::move(write)); - } else if (clause == "printf" || clause == "scanf") { + entry.disjoint.push_back(LibDisjoint{ + .first = *first, .second = *second, .length = std::move(*length)}); + return true; + } + if (clause == "printf" || clause == "scanf") { if (entry.format) - return fail("more than one format clause"); + return fail("a row has at most one format clause"); if (!expect('(', "after '" + clause + "'")) return false; - const auto format = index("format argument"); - if (!format || !expect(',', "in '" + clause + "(…)'")) + auto format = index("the format's index"); + if (!format || !expect(',', "in '" + clause + "'")) return false; - const auto first = index("first variadic argument"); + auto first = index("the first argument's index"); if (!first || !expect(')', "after '" + clause + "(…'")) return false; entry.format = LibFormat{.kind = clause == "printf" ? LibFormat::Kind::Printf : LibFormat::Kind::Scanf, .format = *format, - .first = *first}; - } else if (clause == "wrapper") { + .first = *first, + .vaList = !entry.variadic}; + return true; + } + if (clause == "wrapper") { if (entry.wrapper) return fail("duplicate clause 'wrapper'"); if (!expect('(', "after 'wrapper'")) return false; auto wrapped = name("a wrapper name"); - if (!wrapped) - return false; - if (!expect(')', "after the wrapper name")) + if (!wrapped || !expect(')', "after the wrapper name")) return false; entry.wrapper = LibWrapper{.name = std::move(*wrapped)}; - } else if (clause == "chk") { + return true; + } + if (clause == "chk") { if (!expect('(', "after 'chk'")) return false; auto alias = name("an alias name"); @@ -1156,95 +705,90 @@ bool LibrarySpecParser::parseClause(LibraryEntry &entry, return false; LibraryChk chk{.name = std::move(*alias)}; do { - const auto argument = integer(true); - if (!argument) - return false; - if (*argument > 127) - return fail("alias argument index out of range"); - chk.argumentOf.push_back(static_cast(*argument)); + if (accept("size")) { + if (chk.sizeArgument >= 0) + return fail("an alias has at most one 'size' argument"); + chk.sizeArgument = static_cast(chk.argumentOf.size()); + chk.argumentOf.push_back(-1); + } else if (accept('_')) { + chk.argumentOf.push_back(-1); + } else { + auto row = index("a row argument index, '_' or 'size'"); + if (!row) + return false; + if (*row > 127) + return fail("alias argument index out of range"); + chk.argumentOf.push_back(static_cast(*row)); + } } while (accept(',')); - if (!expect(')', "after the arguments of 'chk(…)'")) + if (!expect(')', "after the alias's arguments")) return false; entry.chk.push_back(std::move(chk)); - } else { - return fail(clause.empty() ? "expected a clause or ';'" - : "unknown clause '" + clause + "'"); - } - return true; -} - -bool LibrarySpecParser::parseLine() { - pos = 0; - if (atEnd()) - return true; - const std::string first = word(); - if (first == "header") - return parseHeader(); - if (first == "builtins") { - if (!expect(';', "after 'builtins'")) - return false; - if (!atEnd()) - return fail("unexpected text after ';'"); - header.clear(); - haveSection = true; - underPattern = false; return true; } - if (!isIdentifier(first)) - return fail(first.empty() ? "expected an entry or a directive" - : "'" + first + "' is not a function name"); - return parseEntry(first); + return fail("unknown clause '" + clause + "'"); } -static bool isHeaderName(std::string_view stem) { - const auto allowed = [](char c) { - return isIdentifierChar(c) || c == '.' || c == '/' || c == '-' || c == '+'; - }; - return !stem.empty() && stem.front() != '/' && stem.back() != '/' && - stem.find("..") == std::string_view::npos && - stem.find("//") == std::string_view::npos && - std::ranges::all_of(stem, allowed); +bool LibrarySpecParser::validateTerm(const LibraryEntry &entry, + const LibTerm &value, + std::string_view where) { + if ((value.kind == LibTerm::Kind::Argument || + value.kind == LibTerm::Kind::StringLength) && + value.arg >= entry.params.size()) + return fail("argument a" + std::to_string(value.arg) + " of " + + std::string(where) + " is past the row's parameters"); + if (value.kind == LibTerm::Kind::StringLength && + entry.params[value.arg].type != LibraryParam::Type::Pointer) + return fail("strlen(a" + std::to_string(value.arg) + ") of " + + std::string(where) + " names a non-pointer"); + return std::ranges::all_of(value.operands, [&](const LibTerm &operand) { + return validateTerm(entry, operand, where); + }); } -bool LibrarySpecParser::parseHeader() { - skipSpace(); - const std::size_t begin = pos; - while (pos < line->text.size() && line->text[pos] != ';' && - line->text[pos] != ' ') - ++pos; - const std::string path = line->text.substr(begin, pos - begin); - if (!expect(';', "after the header name")) - return false; - if (!atEnd()) - return fail("unexpected text after ';'"); - const bool pattern = path.ends_with("/*"); - const std::string_view stem = - std::string_view(path).substr(0, path.size() - (pattern ? 2 : 0)); - if (!isHeaderName(stem)) - return fail("invalid header name '" + path + "'"); - if (std::ranges::find(spec.headerList, path) == spec.headerList.end()) { - spec.headerList.push_back(path); - if (pattern) - spec.headerPrefixes.push_back(std::string(stem) + "/"); - else - spec.headerNames.insert(path); +bool LibrarySpecParser::validateEntry(const LibraryEntry &entry) { + for (std::size_t i = 0; i < entry.params.size(); ++i) { + const LibraryParam ¶m = entry.params[i]; + const std::string where = "parameter " + std::to_string(i); + for (const std::optional *value : + {¶m.bytes, ¶m.count, ¶m.zeroTerm}) + if (*value && !validateTerm(entry, **value, where)) + return false; + if (param.count && !entry.wide) + return fail("'count' needs a wide row"); } - header = pattern ? std::string() : path; - haveSection = true; - underPattern = pattern; + if (entry.value && !validateTerm(entry, *entry.value, "the result")) + return false; + for (const LibDisjoint &disjoint : entry.disjoint) + if (disjoint.first >= entry.params.size() || + disjoint.second >= entry.params.size() || + !validateTerm(entry, disjoint.length, "'disjoint'")) + return fail("'disjoint' names an argument past the row's parameters"); + if (entry.format && + (entry.format->format >= entry.params.size() || + (!entry.variadic && entry.format->first >= entry.params.size()))) + return fail("the format clause names an argument past the parameters"); + // The runtime's wrapper is the row's own: a row that redirects to + // another's would check that row's arguments. + if (entry.wrapper && entry.wrapper->name != entry.name) + return fail("'wrapper(" + entry.wrapper->name + ")' must name the row"); + for (const LibraryChk &chk : entry.chk) + for (const std::int8_t row : chk.argumentOf) + if (row >= 0 && !entry.variadic && + std::cmp_greater_equal(row, entry.params.size())) + return fail("alias '" + chk.name + + "' names an argument past the row's parameters"); return true; } -bool LibrarySpecParser::parseEntry(std::string rowName) { - entryName = rowName; - if (!haveSection) - return fail("an entry must follow a 'header' or 'builtins' line"); - if (underPattern) - return fail("an entry must follow a concrete 'header', not a pattern"); +bool LibrarySpecParser::parseEntry() { LibraryEntry entry; - entry.name = std::move(rowName); - entry.header = header; - entry.line = line->lines.front(); + entry.line = line->lines.empty() ? 0 : line->lines.front(); + auto rowName = name("a function name"); + if (!rowName) + return false; + entry.name = *rowName; + entryName = entry.name; if (!expect('(', "after the function name")) return false; if (!accept(')')) { @@ -1258,244 +802,53 @@ bool LibrarySpecParser::parseEntry(std::string rowName) { return false; entry.params.push_back(std::move(*param)); } while (accept(',')); - if (!expect(')', entry.variadic ? "after '...'" : "after the parameters")) + if (!expect(')', "after the parameters")) return false; } if (!accept("->")) return fail("expected '->' after the parameters"); - bool noreturn = false; - auto result = parseResult(false, noreturn); - if (!result) + if (!parseResult(entry)) return false; - entry.result = std::move(*result); - entry.noreturn = noreturn; while (!accept(';')) { if (atEnd()) return fail("expected ';' at the end of the entry"); - if (!parseClause(entry, word())) + const std::string clause = word(); + if (clause.empty()) + return fail("expected a clause or ';'"); + if (!parseClause(entry, clause)) return false; } if (!atEnd()) - return fail("unexpected text after ';' (one entry per line)"); + return fail("unexpected text after ';'"); if (!validateEntry(entry)) return false; + spec.byName.emplace(entry.name, spec.rows.size()); spec.rows.push_back(std::move(entry)); entryName.clear(); return true; } -static bool isPointerParam(const LibraryEntry &entry, unsigned index) { - return index < entry.params.size() && - entry.params[index].type == LibraryParam::Type::Pointer; -} - -static void forEachLeaf(const LibTerm &term, - const std::function &visit) { - if (term.operands.empty()) - visit(term); - for (const LibTerm &operand : term.operands) - forEachLeaf(operand, visit); -} - -bool LibrarySpecParser::validateTerm(const LibraryEntry &entry, - const LibTerm &term, - std::string_view where) { - std::string problem; - forEachLeaf(term, [&](const LibTerm &leaf) { - if (!problem.empty() || leaf.kind == LibTerm::Kind::Constant || - leaf.kind == LibTerm::Kind::Macro) - return; - const std::string spelled = leaf.str(); - if (leaf.arg >= entry.params.size()) - problem = "'" + spelled + "' in " + std::string(where) + - " names a missing parameter"; - else if (leaf.kind == LibTerm::Kind::StringLength && - entry.params[leaf.arg].type != LibraryParam::Type::Pointer) - problem = "'" + spelled + "' needs a pointer parameter"; - else if (leaf.kind == LibTerm::Kind::FormatLength && - (!entry.format || entry.format->kind != LibFormat::Kind::Printf || - entry.format->format != leaf.arg)) - problem = "'" + spelled + "' needs a 'printf(" + - std::to_string(leaf.arg) + ", …)' clause"; - }); - return problem.empty() || fail(problem); -} - -bool LibrarySpecParser::validateResult(const LibraryEntry &entry, - const LibraryResult &result, - std::string_view where) { - using Kind = LibraryResult::Kind; - if (result.kind == Kind::Arg || result.kind == Kind::Interior) { - const LibraryParam *param = entry.param(result.arg); - if (param == nullptr || param->type != LibraryParam::Type::Pointer) - return fail(std::string(where) + " names argument " + - std::to_string(result.arg) + ", which is not a pointer"); - const bool alternative = !result.family.empty() || !result.state.empty(); - if (alternative && param->null != LibraryParam::Null::Allowed) - return fail(std::string(where) + - ": 'or-fresh'/'or-static' need argument " + - std::to_string(result.arg) + " to be 'null-ok'"); - } - return std::ranges::all_of( - std::initializer_list *>{ - &result.extent, &result.value, &result.offset}, - [&](const std::optional *term) { - return !*term || validateTerm(entry, **term, where); - }); -} - -bool LibrarySpecParser::validateEntry(LibraryEntry &entry) { - const auto count = entry.params.size(); - const auto isPointer = [&entry](unsigned index) { - return index < entry.params.size() && - entry.params[index].type == LibraryParam::Type::Pointer; - }; - for (std::size_t i = 0; i < count; ++i) { - const LibraryParam ¶m = entry.params[i]; - const std::string where = "parameter " + std::to_string(i); - for (const auto *term : - {¶m.bytes, ¶m.count, ¶m.zeroTerm, ¶m.range}) - if (*term && !validateTerm(entry, **term, where)) - return false; - if (param.range && param.effect != LibraryParam::Effect::Release) - return fail(where + ": 'range' needs 'release'"); - if (param.string && param.access != LibraryParam::Access::Read && - param.access != LibraryParam::Access::ReadWrite) - return fail(where + ": 'str' needs read access ('r' or 'rw')"); - if (param.callback) - for (const std::uint8_t argument : param.callback->arguments) - if (argument >= count) - return fail(where + ": the callback names missing argument " + - std::to_string(argument)); - if (param.out && - !validateResult(entry, *param.out, "the out value of " + where)) - return false; - } - if (!validateResult(entry, entry.result, "the result")) - return false; - // The runtime's version of a call is named after it: a row copied from - // another must not keep that one's wrapper. - if (entry.wrapper && entry.wrapper->name != entry.name) - return fail("'wrapper(" + entry.wrapper->name + - ")' must name the row's " - "own function, '" + - entry.name + "'"); - if (entry.exits && !entry.noreturn) - return fail("'exits' needs a 'noreturn' result"); - if (entry.returnsTwice && entry.result.kind != LibraryResult::Kind::Int) - return fail("'returns-twice' needs an 'int' result"); - for (const LibDisjoint &disjoint : entry.disjoint) { - if (!isPointer(disjoint.first) || !isPointer(disjoint.second) || - disjoint.first == disjoint.second) - return fail("'disjoint' needs two different pointer parameters"); - if (!validateTerm(entry, disjoint.length, "'disjoint'")) - return false; - } - const auto accesses = [&entry](unsigned index, LibraryParam::Access access) { - if (!isPointerParam(entry, index)) - return false; - const LibraryParam::Access actual = entry.params[index].access; - return actual == access || actual == LibraryParam::Access::ReadWrite; - }; - for (const LibCopy © : entry.copies) { - if (!accesses(copy.dst, LibraryParam::Access::Write) || - !accesses(copy.src, LibraryParam::Access::Read) || copy.dst == copy.src) - return fail("'copies' needs a written and a different read pointer " - "parameter"); - if (!validateTerm(entry, copy.length, "'copies'")) - return false; - } - for (const LibFill &fill : entry.fills) { - if (!accesses(fill.dst, LibraryParam::Access::Write)) - return fail("'fills' needs a written pointer parameter"); - if (!validateTerm(entry, fill.value, "'fills'") || - !validateTerm(entry, fill.length, "'fills'")) - return false; - } - for (const LibStringWrite &write : entry.writesString) { - if (!accesses(write.dst, LibraryParam::Access::Write)) - return fail("'writes-str' needs a written pointer parameter"); - if (write.length && !validateTerm(entry, *write.length, "'writes-str'")) - return false; - } - if (entry.format) { - LibFormat &format = *entry.format; - if (!isPointer(format.format) || !entry.params[format.format].string) - return fail("the format argument must be an 'r:str' parameter"); - format.vaList = !entry.variadic; - if (entry.variadic && format.first != count) - return fail("the first variadic argument is " + std::to_string(count) + - ", not " + std::to_string(format.first)); - if (!entry.variadic && - (format.first >= count || - entry.params[format.first].type != LibraryParam::Type::Other)) - return fail("a 'v…' function's va_list argument must be 'other'"); - } - for (const LibraryChk &alias : entry.chk) { - if (alias.name == entry.name) - return fail("an alias cannot repeat the entry's name"); - std::vector uses(count, 0); - for (std::size_t i = 0; i < alias.argumentOf.size(); ++i) { - // NOLINTNEXTLINE(bugprone-signed-char-misuse,cert-str34-c): an index - const int target = alias.argumentOf[i]; - const bool last = i + 1 == alias.argumentOf.size(); - if (target == -1) - continue; - if (std::cmp_equal(target, count) && entry.variadic && last) - continue; - if (target < 0 || std::cmp_greater_equal(target, count)) - return fail("alias '" + alias.name + "' maps to missing argument " + - std::to_string(target)); - ++uses[static_cast(target)]; - } - for (std::size_t i = 0; i < count; ++i) - if (uses[i] != 1) - return fail("alias '" + alias.name + "' must map argument " + - std::to_string(i) + " exactly once"); - } - return true; -} - -bool LibrarySpecParser::validateTable() { - for (std::size_t i = 0; i < spec.rows.size(); ++i) { - const LibraryEntry &row = spec.rows[i]; - const auto [first, last] = spec.byName.equal_range(row.name); - for (auto it = first; it != last; ++it) { - if (spec.rows[it->second].signature() != row.signature()) - continue; - entryName = row.name; - return fail("defined again with the same signature (first on line " + - std::to_string(spec.rows[it->second].line) + ")", - row.line); - } - spec.byName.emplace(row.name, i); - } - for (std::size_t i = 0; i < spec.rows.size(); ++i) { - const LibraryEntry &row = spec.rows[i]; - for (std::size_t k = 0; k < row.chk.size(); ++k) { - const std::string &alias = row.chk[k].name; - entryName = row.name; - if (spec.byName.contains(alias)) - return fail("alias '" + alias + "' is also an entry name", row.line); - if (!spec.byAlias.emplace(alias, std::make_pair(i, k)).second) - return fail("alias '" + alias + "' is declared twice", row.line); - } - } - entryName.clear(); - return true; -} - bool LibrarySpecParser::parse(std::string_view text) { const std::vector lines = logicalLines(text); for (const LogicalLine &logical : lines) { line = &logical; - entryName.clear(); - if (!parseLine()) + pos = 0; + if (atEnd()) + continue; + if (!parseEntry()) return false; } line = nullptr; - entryName.clear(); - return validateTable(); + for (std::size_t row = 0; row < spec.rows.size(); ++row) + for (std::size_t alias = 0; alias < spec.rows[row].chk.size(); ++alias) { + const std::string &name = spec.rows[row].chk[alias].name; + if (spec.byName.contains(name) || spec.byAlias.contains(name)) { + entryName = spec.rows[row].name; + return fail("alias '" + name + "' is already a row or an alias"); + } + spec.byAlias.emplace(name, std::make_pair(row, alias)); + } + return true; } //===----------------------------------------------------------------------===// @@ -1510,69 +863,45 @@ std::optional LibrarySpec::parse(std::string_view text, error = parser.error(); return std::nullopt; } - error.clear(); return spec; } const LibraryEntry *LibrarySpec::find(std::string_view name) const { - const auto it = byName.lower_bound(name); - return it == byName.end() || it->first != name ? nullptr : &rows[it->second]; -} - -std::optional -LibrarySpec::resolve(std::string_view callee, - const LibSignature *declared) const { - const auto direct = [&](std::string_view name) -> const LibraryEntry * { - const auto [first, last] = byName.equal_range(name); - for (auto it = first; it != last; ++it) - if (declared == nullptr || rows[it->second].accepts(*declared)) - return &rows[it->second]; - return nullptr; - }; - const auto aliased = - [&](std::string_view name) -> std::optional { - const auto it = byAlias.find(name); - if (it == byAlias.end()) - return std::nullopt; - const LibraryEntry &row = rows[it->second.first]; - return LibraryMatch{.entry = &row, .alias = &row.chk[it->second.second]}; - }; - if (const LibraryEntry *entry = direct(callee)) - return LibraryMatch{.entry = entry}; - if (auto match = aliased(callee)) - return match; - constexpr std::string_view Builtin = "__builtin_"; - if (!callee.starts_with(Builtin)) - return std::nullopt; - callee.remove_prefix(Builtin.size()); - if (const LibraryEntry *entry = direct(callee)) - return LibraryMatch{.entry = entry}; - return aliased(callee); + const auto found = byName.find(name); + return found == byName.end() ? nullptr : &rows[found->second]; } -std::optional LibrarySpec::lookup(std::string_view callee) const { - return resolve(callee, nullptr); +/// Whether a call of `arguments` arguments fits `entry`. +static bool fits(const LibraryEntry &entry, unsigned arguments) { + return entry.variadic ? arguments >= entry.params.size() + : arguments == entry.params.size(); } std::optional LibrarySpec::lookup(std::string_view callee, - const LibSignature &declared) const { - return resolve(callee, &declared); -} - -bool LibrarySpec::isPlatformHeader(std::string_view relativePath, - bool darwinSdk) const { - if (darwinSdk) - return true; - std::string path(relativePath); - std::ranges::replace(path, '\\', '/'); - while (path.starts_with("./")) - path.erase(0, 2); - if (headerNames.contains(path)) - return true; - return std::ranges::any_of(headerPrefixes, [&path](const std::string &p) { - return path.starts_with(p); - }); + std::optional arguments) const { + for (const bool builtin : {false, true}) { + std::string_view name = callee; + if (builtin) { + if (!name.starts_with("__builtin_")) + break; + name.remove_prefix(sizeof("__builtin_") - 1); + } + if (const auto found = byAlias.find(name); found != byAlias.end()) { + const LibraryEntry &entry = rows[found->second.first]; + return LibraryMatch{.entry = &entry, + .alias = &entry.chk[found->second.second]}; + } + const auto [first, last] = byName.equal_range(name); + if (first == last) + continue; + if (arguments) + for (auto it = first; it != last; ++it) + if (fits(rows[it->second], *arguments)) + return LibraryMatch{.entry = &rows[it->second]}; + return LibraryMatch{.entry = &rows[first->second]}; + } + return std::nullopt; } // The table text, as `LibrarySpecTextData` / `LibrarySpecTextSize`; generated @@ -1591,77 +920,4 @@ const LibrarySpec &LibrarySpec::shipped() { return Shipped; } -std::optional formatArgumentCount(std::string_view format, - LibFormat::Kind kind) { - const auto digit = [](char c) { return c >= '0' && c <= '9'; }; - unsigned count = 0; - for (std::size_t i = 0; i < format.size(); ++i) { - if (format[i] != '%') - continue; - if (++i >= format.size()) - return std::nullopt; - if (format[i] == '%') - continue; - // `%1$d`: positional arguments are matched by index, not counted. - std::size_t j = i; - while (j < format.size() && digit(format[j])) - ++j; - if (j > i && j < format.size() && format[j] == '$') - return std::nullopt; - if (kind == LibFormat::Kind::Scanf) { - const bool suppressed = format[i] == '*'; - if (suppressed) - ++i; - while (i < format.size() && digit(format[i])) - ++i; - while (i < format.size() && - std::string_view("hljztLqm").find(format[i]) != - std::string_view::npos) - ++i; - if (i >= format.size()) - return std::nullopt; - if (format[i] == '[') { - // A scan set: `]` right after `[` or `[^` is a member. - ++i; - if (i < format.size() && format[i] == '^') - ++i; - if (i < format.size() && format[i] == ']') - ++i; - while (i < format.size() && format[i] != ']') - ++i; - if (i >= format.size()) - return std::nullopt; - } - count += suppressed ? 0 : 1; - continue; - } - while (i < format.size() && - std::string_view("-+ #0'").find(format[i]) != std::string_view::npos) - ++i; - for (int part = 0; part < 2 && i < format.size(); ++part) { - if (part == 1) { - if (format[i] != '.') - break; - ++i; - } - if (i < format.size() && format[i] == '*') { - ++count; - ++i; - } else { - while (i < format.size() && digit(format[i])) - ++i; - } - } - while (i < format.size() && std::string_view("hljztLq").find(format[i]) != - std::string_view::npos) - ++i; - if (i >= format.size()) - return std::nullopt; - // `%m` prints `strerror(errno)` and reads no argument. - if (format[i] != 'm') - ++count; - } - return count; -} - } // namespace weavec::core diff --git a/lib/Core/LibrarySpec.txt b/lib/Core/LibrarySpec.txt index 2ebc938a..c70b404c 100644 --- a/lib/Core/LibrarySpec.txt +++ b/lib/Core/LibrarySpec.txt @@ -1,195 +1,86 @@ -# LibrarySpec.txt - the declarative C library table (RFC 0030 section 8) +# LibrarySpec.txt - the C library table of the guards (RFC 0036 section 6) # # Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. # See LICENSE for license information. # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception # # CMake embeds this file in the Core library (lib/Core/CMakeLists.txt) and -# core::LibrarySpec parses it on first use. unittests/Core/LibrarySpecTest.cpp -# checks every entry against an independent, hand-written expectation table, -# so an entry changed here must be changed there too. +# core::LibrarySpec parses it on first use. The guard pass reads it +# (lib/Frontend/GuardLibrary.cpp, RFC 0035 section 2.5): before a direct call +# of a function named here, each pointer argument the row reads or writes is +# guarded over the bytes its term gives, evaluated on the call's arguments; +# a row with a wrapper is redirected to the runtime's checked version. +# unittests/Core/LibrarySpecTest.cpp checks the rows against an independent +# expectation table. # # LEXICAL STRUCTURE # '#' starts a comment. A line whose last character before any comment is -# '\' continues on the next line. Each logical line holds one directive or -# one entry, ended by ';'. The file is ASCII. +# '\' continues on the next line. Each logical line holds one entry, ended +# by ';'. The file is ASCII. # -# DIRECTIVES -# header NAME; NAME, relative to its include directory ("stdio.h", -# "sys/ioctl.h"), is a C library, POSIX or platform header -# (section 5.2), and the entries that follow are declared in -# it. "DIR/*" names every header under DIR; an entry cannot -# follow a pattern directly. -# builtins; The entries that follow are compiler builtins, declared by -# no header (__builtin_expect, __builtin_object_size). -# -# A system header is a C library, POSIX or platform header when it is -# under the toolchain's resource directory, when it belongs to a Darwin SDK -# (every SDK header counts), or when it is named in a header directive. -# Third-party libraries installed as system headers (sqlite3.h, libcap's -# sys/capability.h, a JSON library's header) are not listed: their -# functions are unknown -# callees (section 5.1). -# -# ENTRIES (section 8.2; '+' marks an extension of the RFC's grammar) +# ENTRIES # entry ::= NAME '(' params ')' '->' result { clause } ';' # params ::= [ param { ',' param } ] [ ',' '...' ] -# param ::= 'int' | 'other' | 'fn' { ':' fflag } | access { ':' pflag } -# fflag ::= callback | 'null-ok'+ +# param ::= 'int' | 'other' | 'fn' | access { ':' pflag } # access ::= 'r' | 'w' | 'rw' | 'none' -# pflag ::= 'bytes(' term ')' | 'count(' term ')'+ | 'str' | 'null-ok' -# | 'null-if-zero(' term ')' | 'release(' FAMILY ')' -# | 'range(' term ')'+ -# | 'realloc(' FAMILY ')' | 'retain(' STATE ')' | 'escape' -# | 'init(' FAMILY ')'+ | 'fini(' FAMILY ')'+ -# | 'out(' value ')'+ | callback+ -# callback ::= 'sync(' [ INT { ',' INT } ] ')' | 'entry(' [ INT ] ')' -# | 'at-exit' -# result ::= 'void' | 'int' [ ':value(' term ')' ]+ | 'noreturn' | value -# value ::= rkind { ':' rflag } -# rkind ::= 'fresh(' FAMILY ')' | 'static(' STATE ')' | 'arg(' INT ')' -# | 'interior(' INT ')' | 'interior-state(' STATE ')' | 'ptr' -# rflag ::= 'extent(' term ')' | 'null-on-failure' | 'nonnull' -# | 'null-ok' | 'zero-init' | 'no-zero-init' | 'str'+ -# | 'replaces'+ | 'or-fresh(' FAMILY ')'+ -# | 'or-static(' STATE ')'+ | 'offset(' term ')'+ -# | 'zero-filled'+ -# clause ::= 'disjoint(' INT ',' INT ',' term ')' -# | 'copies(' INT ',' INT ',' term ')'+ -# | 'fills(' INT ',' term ',' term ')'+ -# | 'writes-str(' INT [ ',' term ] ')'+ -# | 'invalidates(' STATE ')' | 'reads(' STATE ')' | 'exits' -# | 'returns-twice' | 'wide'+ | 'printf(' INT ',' INT ')' -# | 'scanf(' INT ',' INT ')' | 'chk(' NAME ':' INT { ',' INT } ')' -# | 'wrapper(' NAME ')'+ -# term ::= INT | 'a' N | 'strlen(a' N ')' | 'fmtlen(a' N ')' | MACRO+ -# | term '*' term | term '+' term | term '-' INT -# | term '/' INT+ -# | 'min(' term ',' term ')' | '(' term ')'+ +# pflag ::= 'bytes(' term ')' | 'count(' term ')' | 'str' | 'null-ok' +# | 'null-if-zero(' term ')' | 'release' | 'realloc' +# result ::= 'void' | 'int' [ ':value(' term ')' ] | 'ptr' +# clause ::= 'disjoint(' INT ',' INT ',' term ')' | 'wide' +# | 'printf(' INT ',' INT ')' | 'scanf(' INT ',' INT ')' +# | 'chk(' NAME ':' carg { ',' carg } ')' | 'wrapper(' NAME ')' +# carg ::= INT | '_' | 'size' +# term ::= INT | 'a' N | 'strlen(a' N ')' | term '*' term +# | term '+' term | term '-' INT | term '/' INT +# | 'min(' term ',' term ')' | '(' term ')' # # PARAMETERS # int, other, fn an integer; any other non-pointer (floating, struct, -# va_list); a function pointer. +# va_list); a function pointer. None is guarded. # r, w, rw, none the call reads, writes, reads and writes, or does not -# access the bytes behind the pointer. Without bytes, count -# or str the pointer needs one element of its pointee type. -# bytes(t) it needs t accessible bytes. -# count(t)+ it needs t accessible elements of its pointee type -# (wchar_t for the wide functions). -# str its pointee holds a string terminated within its object -# (by a zero wchar_t for wide strings); needs r or rw. -# null-ok null is allowed. null-if-zero(t): allowed when t is zero -# (section 8.3). Otherwise null is forbidden. -# release(F) the call releases the pointer, of release family F. -# range(t)+ (with release) the call releases the t bytes from the -# pointer (munmap): the object only when they cover it -# from its start, possibly otherwise, and the pointer may -# point anywhere in it (RFC 0034 section 6.3). -# realloc(F) as realloc: released on a non-null result; on a null -# result kept, or released when the result's extent (the -# requested size) is zero; kept when there is no extent. -# retain(S) the pointer is kept in hidden state slot S; releasing it -# while kept makes the next reads(S) a use after free. -# escape the library keeps the pointer where no entry follows it -# (setvbuf's buffer, pthread_create's argument). -# init(F)+ the object behind the pointer acquires a resource of -# family F (regcomp, glob). fini(F)+: that resource ends. -# The object's own storage is not the resource: it stays -# valid, fini on a local is no invalid release, and init -# after fini is no use after release. -# out(v)+ the pointer points to a pointer slot, and the call stores -# v there (asprintf's buffer, strtol's end pointer). The -# nullability of v describes the stored value; with -# null-on-failure nothing usable is stored when the call -# reports failure. -# sync(i, ...) the function is called zero or more times before the call -# returns, its k-th pointer parameter pointing into call -# argument i_k (qsort: sync(0,0)); sync() when it only -# receives the library's own storage (nftw). -# entry(i) the function becomes an entry point (a thread, a signal -# handler) that receives argument i; entry() when it -# receives nothing from the call (signal). -# at-exit the function runs after the main flow has ended. -# On a pointer parameter, a callback flag describes the -# function pointers stored in the pointee (sigaction). +# access the bytes behind the pointer. Without bytes, +# count or str, its first byte is guarded. +# bytes(t) t bytes are guarded. +# count(t) (wide rows only) t wchar_t elements are guarded. +# str a string, guarded up to its terminator (a zero wchar_t +# in wide rows, which the guards do not scan). +# null-ok null is allowed. null-if-zero(t): allowed when t is +# zero. Otherwise null is forbidden. +# release, realloc the call releases (free) or reallocates (realloc) the +# pointer: the allocator validates it, nothing is guarded. # # RESULTS -# void, int no result; any non-pointer result. -# noreturn the call does not return. -# fresh(F) a new resource of family F: 'free' is the heap, 'stack' -# the calling frame (alloca); other families are named -# after their releaser (fclose, closedir, freeaddrinfo). -# static(S) a borrow of hidden state slot S, a global place . -# arg(N) argument N itself. -# interior(N) a pointer into argument N's object. When argument N is -# null, the result points into the object that the entry's -# out(interior(N)) slot held before the call (strtok_r), -# or into static storage when there is no such slot -# (basename(NULL) is "."). -# interior-state(S) a pointer into the value slot S retains (strtok). -# ptr a pointer of unknown provenance. -# extent(t) (fresh, static) the value has t accessible bytes. -# nonnull, null-on-failure, null-ok -# never null (for arg: exactly the argument); null exactly -# when the call fails; possibly null. Defaults: fresh -# null-on-failure; static and arg nonnull; interior, -# interior-state and ptr null-ok. -# zero-init, no-zero-init -# (section 11) whether a fresh value may be lowered to a -# zeroing wrapper; every fresh(free) value carries one. A -# str value without an extent was written up to its -# terminator. -# str+ the value points to a terminated string. -# replaces+ (out values) the slot's previous value is consumed as by -# realloc of the same family before the new one is stored. -# or-fresh(F)+, or-static(S)+ -# (arg results) when argument N is null the result is -# fresh(F) (realpath, getcwd) or static(S) (tmpnam). -# value(t)+ (int results) the result equals t (strlen: strlen(a0)). -# offset(t)+ (interior results) the result is t elements past the -# start of argument N (stpcpy: strlen(a1)). -# zero-filled+ (fresh results) the library sets the whole extent to -# zero (calloc), whatever section 11 lowering does. +# void, int, ptr no result, any non-pointer result, a pointer. +# value(t) the result equals t (strlen: strlen(a0)); a call whose +# result only a value computes from what it reads is +# replaced by the guards that compute t. # # CLAUSES -# disjoint(a,b,t) the first t bytes behind arguments a and b do not overlap. -# copies(d,s,t)+ the call copies the first t bytes of argument s to -# argument d (memcpy; strncpy copies min(a2,strlen(a1)+1)). -# fills(d,v,t)+ the call sets the first t bytes of argument d to v -# (memset: fills(0,a1,a2); bzero: fills(0,0,a1)). -# writes-str(d[,t])+ -# on success argument d holds a string of t characters, -# or of unknown length without t (fgets). Terms use the -# values before the call; a negative t writes nothing -# (snprintf with a zero size). -# invalidates(S) possibly ends every borrow of slot S (section 8.2), -# before the call's own result borrows S (getpwnam reuses -# and may reallocate its static buffer). -# reads(S) the call reads the values slot S retains. -# exits the call ends the process: leaks are not reported there -# (section 8.4). Needs noreturn. -# returns-twice setjmp-like (section 5.4). -# wide+ the row's strings end in a zero wchar_t, and count(t) +# disjoint(a,b,t) the first t bytes behind arguments a and b must not +# overlap. +# wide the row's strings end in a zero wchar_t, and count(t) # and strlen(aN) count wchar_t elements (wcslen). # printf(f,v), scanf(f,v) # argument f is the format; v is the first variadic # argument, or the va_list parameter of a v- function. -# wrapper(N)+ RFC 0035 section 2.5: weavec-cc replaces the call with -# the runtime's checked __weavec_rt_N, which takes the -# call's arguments and its site (first when the call is -# variadic, last otherwise) and checks what the call reads -# and writes as it runs: bytes no term states before the -# call (a format's output), or fewer than the terms state -# (memchr reads only up to the byte it finds). -# chk(A: i0, i1, ...) -# A is an alias of the entry (a fortified form): its k-th -# argument is the entry's argument i_k, or dropped (-1). -# For a variadic entry, alias arguments after the list -# continue from the last index. Every entry also answers to -# __builtin_NAME. The macOS SDK (secure/_string.h, -# secure/_stdio.h) and glibc (bits/*_fortified.h, -# bits/stdio2.h) spell the same builtins; glibc also calls -# the __NAME_chk functions directly. +# A constant format's %s and %n arguments are guarded. +# wrapper(N) weavec-cc replaces the call with the runtime's checked +# __weavec_rt_N, which takes the call's arguments and its +# site (first when the call is variadic, last otherwise) +# and checks what the call reads and writes as it runs. N +# is the row's name. +# chk(A: c0, c1, ...) +# A is an alias of the row (a fortified form): its k-th +# argument is the row's argument c_k, dropped ('_', a +# flag), or the size of the object the row writes +# ('size': the __builtin_object_size the header passed, +# in wchar_t elements for wide rows), which the guards +# check (RFC 0036 section 6). For a variadic row, alias +# arguments after the list continue from the last index. +# Every row also answers to __builtin_NAME. The macOS SDK +# (secure/_string.h, secure/_stdio.h) and glibc +# (bits/*_fortified.h, bits/stdio2.h) spell the same +# builtins; glibc also calls the __NAME_chk functions. # # TERMS # aN the value of argument N. @@ -197,394 +88,122 @@ # min(t, e) a strlen(aN) in e is bounded by t, as # strnlen(aN, t), and needs no terminator within t # elements: min(a2,strlen(a1)+1) is what strncpy reads. -# fmtlen(aN) the length of the output of the format at argument N; a -# requirement, never a check term (section 8.1). -# MACRO+ the value of that macro in the calling unit (BUFSIZ); -# unknown where it is not defined. No unit defines -# __WEAVEC_UNBOUNDED: a requirement that uses it is never -# proven or checked (gets). -# t / N+ t divided by the positive integer N, rounded down -# (RFC 0033 section 3: select's fd_set words). -# Terms are mathematical integers; '*' and '/' bind tighter than '+' and -# '-'. +# t / N t divided by the positive integer N, rounded down. +# Terms are mathematical integers; a value that leaves 64 bits saturates. +# '*' and '/' bind tighter than '+' and '-'. # -# PORTABILITY (RFC 0033 section 3) +# PORTABILITY # A row states what every supported C library (Darwin's libSystem, glibc) # does with an argument, not what a standard permits it to do: where they # differ, the row takes the more permissive behaviour. Null is allowed if # any of them accepts it, and a count of zero allows null if any of them # then reads nothing. A row that requires more than a library does is a -# false stop. A library accepts null when its manual (or the standard it -# implements, or a nullability annotation in its headers) says what the -# call does with null: an error such as EINVAL, a default, or nothing done. -# An undocumented test in one library for a call that crashes in another -# (glibc's EINVAL for a null printf format) is not acceptance, and neither -# is a system call's EFAULT. An extent is what the call reads or writes, -# or, for a buffer whose size is an argument, that size (the fortified -# forms of both libraries enforce it). +# false trap. An extent is what the call reads or writes, or, for a buffer +# whose size is an argument, that size (the fortified forms of both +# libraries enforce it). Element sizes are those of the 64-bit targets +# (int and gid_t 4 bytes, pointers 8). # -# OVERLOADS AND ABSENCES -# A name may have several entries when their signatures (parameter classes, -# arity, variadic, pointer result) differ; the engine uses the one that -# accepts the declaration (glibc and BSD qsort_r, GNU and XSI strerror_r). -# Arguments in '...' positions are borrowed for the call, and read or -# written as the format says for printf and scanf entries. -# Deliberately absent (section 5.2 applies): ioctl (its third argument's -# layout depends on the request) and syscall. The buffers behind iovec and -# msghdr arrays are nested in the argument, where no term can name them. - -#============================================================================ -# The header list (section 5.2). The entry groups below repeat their header. -# The list is consulted only for system headers under the SDK or sysroot. -#============================================================================ - -# ISO C (C23) -header assert.h; -header complex.h; -header ctype.h; -header errno.h; -header fenv.h; -header float.h; -header inttypes.h; -header iso646.h; -header limits.h; -header locale.h; -header math.h; -header setjmp.h; -header signal.h; -header stdalign.h; -header stdarg.h; -header stdatomic.h; -header stdbit.h; -header stdbool.h; -header stdckdint.h; -header stddef.h; -header stdint.h; -header stdio.h; -header stdlib.h; -header stdnoreturn.h; -header string.h; -header tgmath.h; -header threads.h; -header time.h; -header uchar.h; -header wchar.h; -header wctype.h; - -# POSIX.1-2024 (and headers POSIX.1-2008 had) -header aio.h; -header arpa/inet.h; -header cpio.h; -header devctl.h; -header dirent.h; -header dlfcn.h; -header endian.h; -header fcntl.h; -header fmtmsg.h; -header fnmatch.h; -header ftw.h; -header glob.h; -header grp.h; -header iconv.h; -header langinfo.h; -header libgen.h; -header libintl.h; -header monetary.h; -header mqueue.h; -header ndbm.h; -header net/if.h; -header netdb.h; -header netinet/in.h; -header netinet/tcp.h; -header nl_types.h; -header poll.h; -header pthread.h; -header pwd.h; -header regex.h; -header sched.h; -header search.h; -header semaphore.h; -header spawn.h; -header strings.h; -header stropts.h; -header sys/ipc.h; -header sys/mman.h; -header sys/msg.h; -header sys/resource.h; -header sys/select.h; -header sys/sem.h; -header sys/shm.h; -header sys/socket.h; -header sys/stat.h; -header sys/statvfs.h; -header sys/time.h; -header sys/times.h; -header sys/types.h; -header sys/uio.h; -header sys/un.h; -header sys/utsname.h; -header sys/wait.h; -header syslog.h; -header tar.h; -header termios.h; -header trace.h; -header ulimit.h; -header unistd.h; -header utime.h; -header utmpx.h; -header wordexp.h; - -# glibc, musl and BSD headers outside POSIX -header alloca.h; -header argp.h; -header byteswap.h; -header crypt.h; -header elf.h; -header err.h; -header error.h; -header execinfo.h; -header features.h; -header fts.h; -header getopt.h; -header ifaddrs.h; -header lastlog.h; -header link.h; -header malloc.h; -header mntent.h; -header obstack.h; -header paths.h; -header printf.h; -header pty.h; -header resolv.h; -header sgtty.h; -header shadow.h; -header stdc-predef.h; -header sysexits.h; -header ucontext.h; -header utmp.h; -header values.h; -header xlocale.h; -header arpa/ftp.h; -header arpa/nameser.h; -header arpa/nameser_compat.h; -header arpa/telnet.h; -header arpa/tftp.h; -header sys/acct.h; -header sys/auxv.h; -header sys/cdefs.h; -header sys/dir.h; -header sys/epoll.h; -header sys/errno.h; -header sys/event.h; -header sys/eventfd.h; -header sys/fanotify.h; -header sys/fcntl.h; -header sys/file.h; -header sys/filio.h; -header sys/fsuid.h; -header sys/inotify.h; -header sys/io.h; -header sys/ioccom.h; -header sys/ioctl.h; -header sys/kd.h; -header sys/klog.h; -header sys/mount.h; -header sys/mtio.h; -header sys/param.h; -header sys/personality.h; -header sys/pidfd.h; -header sys/poll.h; -header sys/prctl.h; -header sys/procfs.h; -header sys/ptrace.h; -header sys/queue.h; -header sys/quota.h; -header sys/random.h; -header sys/reboot.h; -header sys/rseq.h; -header sys/sendfile.h; -header sys/signal.h; -header sys/signalfd.h; -header sys/single_threaded.h; -header sys/sockio.h; -header sys/statfs.h; -header sys/swap.h; -header sys/syscall.h; -header sys/sysctl.h; -header sys/sysinfo.h; -header sys/syslog.h; -header sys/sysmacros.h; -header sys/timeb.h; -header sys/timerfd.h; -header sys/timex.h; -header sys/ttycom.h; -header sys/ttydefaults.h; -header sys/ucontext.h; -header sys/unistd.h; -header sys/user.h; -header sys/vfs.h; -header sys/vt.h; -header sys/xattr.h; - -# Implementation directories: every header under them. -header asm/*; -header asm-generic/*; -header bits/*; -header gnu/*; -header linux/*; -header net/*; -header netinet/*; -header netpacket/*; -header scsi/*; +# OVERLOADS +# A name may have several rows; a call uses the first whose arity it +# matches (a variadic row accepts more arguments), or else the first. +# Deliberately absent: ioctl (its third argument's layout depends on the +# request) and syscall. The buffers behind iovec and msghdr arrays are +# nested in the argument, where no term can name them. #============================================================================ # Heap allocation and release #============================================================================ -header stdlib.h; -malloc (int) -> fresh(free):extent(a0):null-on-failure:zero-init; -calloc (int, int) -> \ - fresh(free):extent(a0*a1):null-on-failure:zero-init:zero-filled; -realloc (rw:null-ok:realloc(free), int) -> \ - fresh(free):extent(a1):null-on-failure:zero-init; -reallocarray (rw:null-ok:realloc(free), int, int) -> \ - fresh(free):extent(a1*a2):null-on-failure:zero-init; -# BSD: the argument is freed on failure as well. -reallocf (rw:null-ok:release(free), int) -> \ - fresh(free):extent(a1):null-on-failure:no-zero-init; -aligned_alloc (int, int) -> fresh(free):extent(a1):null-on-failure:zero-init; -posix_memalign (w:out(fresh(free):extent(a2):null-on-failure:zero-init), \ - int, int) -> int; -valloc (int) -> fresh(free):extent(a0):null-on-failure:zero-init; -free (none:null-ok:release(free)) -> void; -free_sized (none:null-ok:release(free), int) -> void; -free_aligned_sized (none:null-ok:release(free), int, int) -> void; +posix_memalign (w:bytes(8), int, int) -> int; # glibc: allocates the result of realpath(path, NULL) in one call. -canonicalize_file_name (r:str:null-ok) -> \ - fresh(free):null-on-failure:zero-init:str; -realpath (r:str:null-ok, w:null-ok:bytes(PATH_MAX)) -> \ - arg(1):null-on-failure:or-fresh(free):zero-init:str \ - chk(__realpath_chk: 0,1,-1); - -header malloc.h; -memalign (int, int) -> fresh(free):extent(a1):null-on-failure:zero-init; -pvalloc (int) -> fresh(free):extent(a0):null-on-failure:zero-init; -malloc_usable_size (none:null-ok) -> int; - -header malloc/malloc.h; -malloc_size (none:null-ok) -> int; -malloc_good_size (int) -> int; - -header alloca.h; -alloca (int) -> fresh(stack):extent(a0):nonnull:zero-init; - -header string.h; -strdup (r:str) -> \ - fresh(free):extent(strlen(a0)+1):null-on-failure:zero-init:str; -strndup (r:bytes(min(a1,strlen(a0)+1)):null-if-zero(a1), int) -> \ - fresh(free):extent(min(a1,strlen(a0))+1):null-on-failure:zero-init:str; - -header wchar.h; -wcsdup (r:str) -> fresh(free):null-on-failure:zero-init:str wide; - -header stdio.h; -asprintf (w:out(fresh(free):extent(fmtlen(a1)+1):null-on-failure:zero-init:str), \ - r:str, ...) -> int printf(1,2) chk(__asprintf_chk: 0,-1,1,2); -vasprintf (w:out(fresh(free):extent(fmtlen(a1)+1):null-on-failure:zero-init:str), \ - r:str, other) -> int printf(1,2) chk(__vasprintf_chk: 0,-1,1,2); +canonicalize_file_name (r:str:null-ok) -> ptr; +realpath (r:str:null-ok, w:null-ok) -> ptr chk(__realpath_chk:0,1,size); + +strdup (r:str) -> ptr; +strndup (r:bytes(min(a1,strlen(a0)+1)):null-if-zero(a1), int) -> ptr; + +wcsdup (r:str) -> ptr wide; + +asprintf (w:bytes(8), r:str, ...) -> int printf(1,2) \ + chk(__asprintf_chk:0,_,1,2); +vasprintf (w:bytes(8), r:str, other) -> int printf(1,2) \ + chk(__vasprintf_chk:0,_,1,2); # The buffer is reallocated in place of the old one, and stays allocated # after a failed read: it is freed on every path. -getline (rw:null-ok:out(fresh(free):null-ok:zero-init:str:replaces), \ - rw:null-ok, rw) -> int; -getdelim (rw:null-ok:out(fresh(free):null-ok:zero-init:str:replaces), \ - rw:null-ok, int, rw) -> int; -tempnam (r:str:null-ok, r:str:null-ok) -> \ - fresh(free):null-on-failure:zero-init:str reads(environ); - -header unistd.h; +getline (rw:null-ok:bytes(8), rw:null-ok, rw) -> int; +getdelim (rw:null-ok:bytes(8), rw:null-ok, int, rw) -> int; +tempnam (r:str:null-ok, r:str:null-ok) -> ptr; + # getcwd(NULL, n) allocates (glibc, BSD, macOS). -getcwd (w:null-ok:bytes(a1), int) -> \ - arg(0):null-on-failure:or-fresh(free):zero-init:str \ - chk(__getcwd_chk: 0,1,-1); -get_current_dir_name () -> fresh(free):null-on-failure:zero-init:str \ - reads(environ); +getcwd (w:null-ok:bytes(a1), int) -> ptr chk(__getcwd_chk:0,1,size); -header execinfo.h; -backtrace (w:count(a1), int) -> int; -backtrace_symbols (r:count(a1), int) -> fresh(free):null-on-failure:no-zero-init; -backtrace_symbols_fd (r:count(a1), int, int) -> void; +backtrace (w:bytes(a1*8), int) -> int; +backtrace_symbols (r:bytes(a1*8), int) -> ptr; +backtrace_symbols_fd (r:bytes(a1*8), int, int) -> void; #============================================================================ # , #============================================================================ -header string.h; -memcpy (w:bytes(a2):null-if-zero(a2), r:bytes(a2):null-if-zero(a2), int) \ - -> arg(0) disjoint(0,1,a2) copies(0,1,a2) \ - chk(__builtin___memcpy_chk: 0,1,2,-1) chk(__memcpy_chk: 0,1,2,-1); -memmove (w:bytes(a2):null-if-zero(a2), r:bytes(a2):null-if-zero(a2), int) \ - -> arg(0) copies(0,1,a2) \ - chk(__builtin___memmove_chk: 0,1,2,-1) chk(__memmove_chk: 0,1,2,-1); -memset (w:bytes(a2):null-if-zero(a2), int, int) -> arg(0) fills(0,a1,a2) \ - chk(__builtin___memset_chk: 0,1,2,-1) chk(__memset_chk: 0,1,2,-1); -memset_explicit (w:bytes(a2):null-if-zero(a2), int, int) -> arg(0) \ - fills(0,a1,a2); -memcmp (r:bytes(a2):null-if-zero(a2), r:bytes(a2):null-if-zero(a2), int) \ - -> int; +memcpy (w:bytes(a2):null-if-zero(a2), r:bytes(a2):null-if-zero(a2), int) -> \ + ptr disjoint(0,1,a2) chk(__builtin___memcpy_chk:0,1,2,size) \ + chk(__memcpy_chk:0,1,2,size); +memmove (w:bytes(a2):null-if-zero(a2), r:bytes(a2):null-if-zero(a2), int) -> \ + ptr chk(__builtin___memmove_chk:0,1,2,size) \ + chk(__memmove_chk:0,1,2,size); +memset (w:bytes(a2):null-if-zero(a2), int, int) -> ptr \ + chk(__builtin___memset_chk:0,1,2,size) chk(__memset_chk:0,1,2,size); +memset_explicit (w:bytes(a2):null-if-zero(a2), int, int) -> ptr; +memcmp (r:bytes(a2):null-if-zero(a2), r:bytes(a2):null-if-zero(a2), int) -> \ + int; # C11: memchr stops at the first match, so a NUL ends what memchr(s, 0, n) # reads; a byte other than NUL found earlier is not described. -memchr (r:bytes(min(a2,strlen(a0)+1)):null-if-zero(a2), int, int) -> \ - interior(0):null-ok wrapper(memchr); -memrchr (r:bytes(a2):null-if-zero(a2), int, int) -> interior(0):null-ok; +memchr (r:bytes(min(a2,strlen(a0)+1)):null-if-zero(a2), int, int) -> ptr \ + wrapper(memchr); +memrchr (r:bytes(a2):null-if-zero(a2), int, int) -> ptr; # GNU: searches without a bound for a byte the caller promises is there. -rawmemchr (r:bytes(__WEAVEC_UNBOUNDED), int) -> interior(0):nonnull; +rawmemchr (r, int) -> ptr; memmem (r:bytes(a1):null-if-zero(a1), int, r:bytes(a3):null-if-zero(a3), int) \ - -> interior(0):null-ok; -mempcpy (w:bytes(a2):null-if-zero(a2), r:bytes(a2):null-if-zero(a2), int) \ - -> interior(0):offset(a2):nonnull \ - disjoint(0,1,a2) copies(0,1,a2) \ - chk(__builtin___mempcpy_chk: 0,1,2,-1) chk(__mempcpy_chk: 0,1,2,-1); + -> ptr; +mempcpy (w:bytes(a2):null-if-zero(a2), r:bytes(a2):null-if-zero(a2), int) -> \ + ptr disjoint(0,1,a2) chk(__builtin___mempcpy_chk:0,1,2,size) \ + chk(__mempcpy_chk:0,1,2,size); # memccpy stops after the byte c; a NUL-terminated source bounds the read for # the common c == 0 use, and a source without c or NUL is read for n bytes. memccpy (w:bytes(min(a3,strlen(a1)+1)):null-if-zero(a3), \ - r:bytes(min(a3,strlen(a1)+1)):null-if-zero(a3), int, int) -> \ - interior(0):null-ok disjoint(0,1,min(a3,strlen(a1)+1)) \ - chk(__builtin___memccpy_chk: 0,1,2,3,-1); -strcpy (w:bytes(strlen(a1)+1), r:str) -> arg(0) \ - disjoint(0,1,strlen(a1)+1) writes-str(0,strlen(a1)) \ - wrapper(strcpy) \ - chk(__builtin___strcpy_chk: 0,1,-1) chk(__strcpy_chk: 0,1,-1); + r:bytes(min(a3,strlen(a1)+1)):null-if-zero(a3), int, int) -> ptr \ + disjoint(0,1,min(a3,strlen(a1)+1)) \ + chk(__builtin___memccpy_chk:0,1,2,3,size); +strcpy (w:bytes(strlen(a1)+1), r:str) -> ptr disjoint(0,1,strlen(a1)+1) \ + wrapper(strcpy) chk(__builtin___strcpy_chk:0,1,size) \ + chk(__strcpy_chk:0,1,size); strncpy (w:bytes(a2):null-if-zero(a2), \ - r:bytes(min(a2,strlen(a1)+1)):null-if-zero(a2), int) -> arg(0) \ - disjoint(0,1,min(a2,strlen(a1)+1)) copies(0,1,min(a2,strlen(a1)+1)) \ - chk(__builtin___strncpy_chk: 0,1,2,-1) chk(__strncpy_chk: 0,1,2,-1); -stpcpy (w:bytes(strlen(a1)+1), r:str) -> \ - interior(0):offset(strlen(a1)):nonnull \ - disjoint(0,1,strlen(a1)+1) writes-str(0,strlen(a1)) \ - wrapper(stpcpy) \ - chk(__builtin___stpcpy_chk: 0,1,-1) chk(__stpcpy_chk: 0,1,-1); + r:bytes(min(a2,strlen(a1)+1)):null-if-zero(a2), int) -> ptr \ + disjoint(0,1,min(a2,strlen(a1)+1)) \ + chk(__builtin___strncpy_chk:0,1,2,size) chk(__strncpy_chk:0,1,2,size); +stpcpy (w:bytes(strlen(a1)+1), r:str) -> ptr disjoint(0,1,strlen(a1)+1) \ + wrapper(stpcpy) chk(__builtin___stpcpy_chk:0,1,size) \ + chk(__stpcpy_chk:0,1,size); stpncpy (w:bytes(a2):null-if-zero(a2), \ - r:bytes(min(a2,strlen(a1)+1)):null-if-zero(a2), int) \ - -> interior(0):offset(min(a2,strlen(a1))):nonnull \ - disjoint(0,1,min(a2,strlen(a1)+1)) copies(0,1,min(a2,strlen(a1)+1)) \ - chk(__builtin___stpncpy_chk: 0,1,2,-1) chk(__stpncpy_chk: 0,1,2,-1); -strcat (rw:bytes(strlen(a0)+strlen(a1)+1):str, r:str) -> arg(0) \ - disjoint(0,1,strlen(a0)+strlen(a1)+1) \ - writes-str(0,strlen(a0)+strlen(a1)) wrapper(strcat) \ - chk(__builtin___strcat_chk: 0,1,-1) chk(__strcat_chk: 0,1,-1); + r:bytes(min(a2,strlen(a1)+1)):null-if-zero(a2), int) -> ptr \ + disjoint(0,1,min(a2,strlen(a1)+1)) \ + chk(__builtin___stpncpy_chk:0,1,2,size) chk(__stpncpy_chk:0,1,2,size); +strcat (rw:bytes(strlen(a0)+strlen(a1)+1):str, r:str) -> ptr \ + disjoint(0,1,strlen(a0)+strlen(a1)+1) wrapper(strcat) \ + chk(__builtin___strcat_chk:0,1,size) chk(__strcat_chk:0,1,size); strncat (rw:bytes(strlen(a0)+min(a2,strlen(a1))+1):str, \ - r:bytes(min(a2,strlen(a1)+1)):null-if-zero(a2), int) -> arg(0) \ + r:bytes(min(a2,strlen(a1)+1)):null-if-zero(a2), int) -> ptr \ disjoint(0,1,strlen(a0)+min(a2,strlen(a1))+1) \ - writes-str(0,strlen(a0)+min(a2,strlen(a1))) \ - chk(__builtin___strncat_chk: 0,1,2,-1) chk(__strncat_chk: 0,1,2,-1); + chk(__builtin___strncat_chk:0,1,2,size) chk(__strncat_chk:0,1,2,size); # BSD, glibc 2.38: the whole source is read to compute the result. strlcpy (w:bytes(a2):null-if-zero(a2), r:str, int) -> int:value(strlen(a1)) \ - disjoint(0,1,min(a2,strlen(a1)+1)) writes-str(0,min(a2-1,strlen(a1))) \ - chk(__builtin___strlcpy_chk: 0,1,2,-1) chk(__strlcpy_chk: 0,1,2,-1); + disjoint(0,1,min(a2,strlen(a1)+1)) \ + chk(__builtin___strlcpy_chk:0,1,2,size) chk(__strlcpy_chk:0,1,2,size); strlcat (rw:bytes(a2):null-if-zero(a2), r:str, int) -> int \ - chk(__builtin___strlcat_chk: 0,1,2,-1) chk(__strlcat_chk: 0,1,2,-1); + chk(__builtin___strlcat_chk:0,1,2,size) chk(__strlcat_chk:0,1,2,size); strlen (r:str) -> int:value(strlen(a0)); -strnlen (r:bytes(min(a1,strlen(a0)+1)):null-if-zero(a1), int) -> int:value(min(a1,strlen(a0))); +strnlen (r:bytes(min(a1,strlen(a0)+1)):null-if-zero(a1), int) -> \ + int:value(min(a1,strlen(a0))); strcmp (r:str, r:str) -> int wrapper(strcmp); strncmp (r:bytes(min(a2,strlen(a0)+1)):null-if-zero(a2), \ r:bytes(min(a2,strlen(a1)+1)):null-if-zero(a2), int) -> int \ @@ -594,72 +213,56 @@ strcoll_l (r:str, r:str, none:null-ok) -> int; strxfrm (w:bytes(a2):null-if-zero(a2), r:str, int) -> int; strxfrm_l (w:bytes(a2):null-if-zero(a2), r:str, int, none:null-ok) -> int; strverscmp (r:str, r:str) -> int; -strchr (r:str, int) -> interior(0):null-ok wrapper(strchr); -strrchr (r:str, int) -> interior(0):null-ok; -strchrnul (r:str, int) -> interior(0):nonnull; -strstr (r:str, r:str) -> interior(0):null-ok; -strcasestr (r:str, r:str) -> interior(0):null-ok; -strpbrk (r:str, r:str) -> interior(0):null-ok; +strchr (r:str, int) -> ptr wrapper(strchr); +strrchr (r:str, int) -> ptr; +strchrnul (r:str, int) -> ptr; +strstr (r:str, r:str) -> ptr; +strcasestr (r:str, r:str) -> ptr; +strpbrk (r:str, r:str) -> ptr; strspn (r:str, r:str) -> int; strcspn (r:str, r:str) -> int; -strtok (rw:str:null-ok:retain(strtok), r:str) -> \ - interior-state(strtok):null-ok reads(strtok); -strtok_r (rw:str:null-ok, r:str, rw:out(interior(0):null-ok)) -> \ - interior(0):null-ok; -strsep (rw, r:str) -> ptr:null-ok:str; -strerror (int) -> static(strerror):nonnull:str; -strerror_l (int, none) -> static(strerror):nonnull:str; +strtok (rw:str:null-ok, r:str) -> ptr; +strtok_r (rw:str:null-ok, r:str, rw:bytes(8)) -> ptr; +strsep (rw, r:str) -> ptr; # GNU: the message is in buf or in static storage. XSI: an int result. -strerror_r (int, w:bytes(a2):null-if-zero(a2), int) -> ptr:nonnull:str; +strerror_r (int, w:bytes(a2):null-if-zero(a2), int) -> ptr; strerror_r (int, w:bytes(a2):null-if-zero(a2), int) -> int; -strsignal (int) -> static(strsignal):null-ok:str; -explicit_bzero (w:bytes(a1):null-if-zero(a1), int) -> void fills(0,0,a1) \ - chk(__explicit_bzero_chk: 0,1,-1); +explicit_bzero (w:bytes(a1):null-if-zero(a1), int) -> void \ + chk(__explicit_bzero_chk:0,1,size); # macOS: a null s is EINVAL, and min(smax, n) bytes are set. -memset_s (w:bytes(min(a1,a3)):null-ok, int, int, int) -> int \ - fills(0,a2,min(a1,a3)); - -header strings.h; -bzero (w:bytes(a1):null-if-zero(a1), int) -> void fills(0,0,a1); -bcopy (r:bytes(a2):null-if-zero(a2), w:bytes(a2):null-if-zero(a2), int) -> void \ - copies(1,0,a2); -bcmp (r:bytes(a2):null-if-zero(a2), r:bytes(a2):null-if-zero(a2), int) -> int; -index (r:str, int) -> interior(0):null-ok; -rindex (r:str, int) -> interior(0):null-ok; +memset_s (w:bytes(min(a1,a3)):null-ok, int, int, int) -> int; + +bzero (w:bytes(a1):null-if-zero(a1), int) -> void; +bcopy (r:bytes(a2):null-if-zero(a2), w:bytes(a2):null-if-zero(a2), int) -> \ + void; +bcmp (r:bytes(a2):null-if-zero(a2), r:bytes(a2):null-if-zero(a2), int) -> \ + int; +index (r:str, int) -> ptr; +rindex (r:str, int) -> ptr; strcasecmp (r:str, r:str) -> int wrapper(strcasecmp); strncasecmp (r:bytes(min(a2,strlen(a0)+1)):null-if-zero(a2), \ r:bytes(min(a2,strlen(a1)+1)):null-if-zero(a2), int) -> int \ wrapper(strncasecmp); strcasecmp_l (r:str, r:str, none:null-ok) -> int; strncasecmp_l (r:bytes(min(a2,strlen(a0)+1)):null-if-zero(a2), \ - r:bytes(min(a2,strlen(a1)+1)):null-if-zero(a2), int, none:null-ok) -> int; -ffs (int) -> int; -ffsl (int) -> int; -ffsll (int) -> int; + r:bytes(min(a2,strlen(a1)+1)):null-if-zero(a2), int, none:null-ok) -> \ + int; #============================================================================ # #============================================================================ -header stdio.h; -fopen (r:str, r:str) -> fresh(fclose):null-on-failure; -fdopen (int, r:str) -> fresh(fclose):null-on-failure; -# On failure the old stream is closed as well; that path is not modelled. -freopen (r:str:null-ok, r:str, rw) -> arg(2):null-on-failure; -tmpfile () -> fresh(fclose):null-on-failure; -fmemopen (w:bytes(a1):null-ok:escape, int, r:str) \ - -> fresh(fclose):null-on-failure; -open_memstream ( \ - w:null-ok:escape:out(fresh(free):null-on-failure:no-zero-init), \ - w:null-ok:escape) -> fresh(fclose):null-on-failure; -fclose (rw:release(fclose)) -> int; -popen (r:str, r:str) -> fresh(pclose):null-on-failure reads(environ); -pclose (rw:release(pclose)) -> int; +fopen (r:str, r:str) -> ptr; +fdopen (int, r:str) -> ptr; +freopen (r:str:null-ok, r:str, rw) -> ptr; +fmemopen (w:bytes(a1):null-ok, int, r:str) -> ptr; +open_memstream (w:null-ok:bytes(8), w:null-ok) -> ptr; +popen (r:str, r:str) -> ptr; fflush (rw:null-ok) -> int; fpurge (rw) -> int; -setvbuf (rw, w:bytes(a3):null-ok:escape, int, int) -> int; -setbuf (rw, w:bytes(BUFSIZ):null-ok:escape) -> void; -setbuffer (rw, w:bytes(a2):null-ok:escape, int) -> void; +setvbuf (rw, w:bytes(a3):null-ok, int, int) -> int; +setbuf (rw, w:null-ok) -> void; +setbuffer (rw, w:bytes(a2):null-ok, int) -> void; setlinebuf (rw) -> void; fileno (r) -> int; fileno_unlocked (r) -> int; @@ -679,274 +282,207 @@ ftrylockfile (rw) -> int; fgetc (rw) -> int; getc (rw) -> int; getc_unlocked (rw) -> int; -getchar () -> int; -getchar_unlocked () -> int; ungetc (int, rw) -> int; fputc (int, rw) -> int; putc (int, rw) -> int; putc_unlocked (int, rw) -> int; -putchar (int) -> int; -putchar_unlocked (int) -> int; -fgets (w:bytes(a1):null-if-zero(a1), int, rw) -> arg(0):null-ok writes-str(0) \ - chk(__fgets_chk: 0,-1,1,2); -# gets has no bound: no unit defines the macro, so its requirement is never -# proven (C11 removed gets). -gets (w:bytes(__WEAVEC_UNBOUNDED)) -> arg(0):null-ok wrapper(gets) \ - chk(__gets_chk: 0,-1); +fgets (w:bytes(a1):null-if-zero(a1), int, rw) -> ptr \ + chk(__fgets_chk:0,size,1,2); +# gets has no bound: its wrapper checks each byte before it writes it (C11 +# removed gets). +gets (w) -> ptr wrapper(gets) chk(__gets_chk:0,size); # BSD: the line is in the stream's buffer, not terminated. -fgetln (rw, w) -> interior(0):null-ok; -fread (w:bytes(a1*a2):null-if-zero(a1*a2), int, int, rw) -> int chk(__fread_chk: 0,-1,1,2,3); +fgetln (rw, w) -> ptr; +fread (w:bytes(a1*a2):null-if-zero(a1*a2), int, int, rw) -> int \ + chk(__fread_chk:0,size,1,2,3); fread_unlocked (w:bytes(a1*a2):null-if-zero(a1*a2), int, int, rw) -> int \ - chk(__fread_unlocked_chk: 0,-1,1,2,3); + chk(__fread_unlocked_chk:0,size,1,2,3); fwrite (r:bytes(a1*a2):null-if-zero(a1*a2), int, int, rw) -> int; fwrite_unlocked (r:bytes(a1*a2):null-if-zero(a1*a2), int, int, rw) -> int; puts (r:str:null-ok) -> int; fputs (r:str:null-ok, rw) -> int; perror (r:str:null-ok) -> void; -printf (r:str, ...) -> int printf(0,1) \ - chk(__builtin___printf_chk: -1,0,1) chk(__printf_chk: -1,0,1); +printf (r:str, ...) -> int printf(0,1) chk(__builtin___printf_chk:_,0,1) \ + chk(__printf_chk:_,0,1); fprintf (rw, r:str, ...) -> int printf(1,2) \ - chk(__builtin___fprintf_chk: 0,-1,1,2) chk(__fprintf_chk: 0,-1,1,2); -dprintf (int, r:str, ...) -> int printf(1,2) chk(__dprintf_chk: 0,-1,1,2); -sprintf (w:bytes(fmtlen(a1)+1), r:str, ...) -> int:value(fmtlen(a1)) \ - writes-str(0,fmtlen(a1)) printf(1,2) wrapper(sprintf) \ - chk(__builtin___sprintf_chk: 0,-1,-1,1,2) chk(__sprintf_chk: 0,-1,-1,1,2); -snprintf (w:bytes(a1):null-if-zero(a1), int, r:str, ...) \ - -> int:value(fmtlen(a2)) writes-str(0,min(a1-1,fmtlen(a2))) printf(2,3) \ - wrapper(snprintf) chk(__builtin___snprintf_chk: 0,1,-1,-1,2,3) \ - chk(__snprintf_chk: 0,1,-1,-1,2,3); -vprintf (r:str, other) -> int printf(0,1) \ - chk(__builtin___vprintf_chk: -1,0,1) chk(__vprintf_chk: -1,0,1); + chk(__builtin___fprintf_chk:0,_,1,2) chk(__fprintf_chk:0,_,1,2); +dprintf (int, r:str, ...) -> int printf(1,2) chk(__dprintf_chk:0,_,1,2); +sprintf (w, r:str, ...) -> int printf(1,2) wrapper(sprintf) \ + chk(__builtin___sprintf_chk:0,_,size,1,2) \ + chk(__sprintf_chk:0,_,size,1,2); +snprintf (w:bytes(a1):null-if-zero(a1), int, r:str, ...) -> int printf(2,3) \ + wrapper(snprintf) chk(__builtin___snprintf_chk:0,1,_,size,2,3) \ + chk(__snprintf_chk:0,1,_,size,2,3); +vprintf (r:str, other) -> int printf(0,1) chk(__builtin___vprintf_chk:_,0,1) \ + chk(__vprintf_chk:_,0,1); vfprintf (rw, r:str, other) -> int printf(1,2) \ - chk(__builtin___vfprintf_chk: 0,-1,1,2) chk(__vfprintf_chk: 0,-1,1,2); -vdprintf (int, r:str, other) -> int printf(1,2) chk(__vdprintf_chk: 0,-1,1,2); -vsprintf (w:bytes(fmtlen(a1)+1), r:str, other) -> int:value(fmtlen(a1)) \ - writes-str(0,fmtlen(a1)) printf(1,2) wrapper(vsprintf) \ - chk(__builtin___vsprintf_chk: 0,-1,-1,1,2) \ - chk(__vsprintf_chk: 0,-1,-1,1,2); -vsnprintf (w:bytes(a1):null-if-zero(a1), int, r:str, other) \ - -> int:value(fmtlen(a2)) writes-str(0,min(a1-1,fmtlen(a2))) printf(2,3) \ - wrapper(vsnprintf) chk(__builtin___vsnprintf_chk: 0,1,-1,-1,2,3) \ - chk(__vsnprintf_chk: 0,1,-1,-1,2,3); + chk(__builtin___vfprintf_chk:0,_,1,2) chk(__vfprintf_chk:0,_,1,2); +vdprintf (int, r:str, other) -> int printf(1,2) chk(__vdprintf_chk:0,_,1,2); +vsprintf (w, r:str, other) -> int printf(1,2) wrapper(vsprintf) \ + chk(__builtin___vsprintf_chk:0,_,size,1,2) \ + chk(__vsprintf_chk:0,_,size,1,2); +vsnprintf (w:bytes(a1):null-if-zero(a1), int, r:str, other) -> int \ + printf(2,3) wrapper(vsnprintf) \ + chk(__builtin___vsnprintf_chk:0,1,_,size,2,3) \ + chk(__vsnprintf_chk:0,1,_,size,2,3); # glibc renames the scanf family for C99 and C23 semantics. -scanf (r:str, ...) -> int scanf(0,1) wrapper(scanf) \ - chk(__isoc99_scanf: 0,1) chk(__isoc23_scanf: 0,1); +scanf (r:str, ...) -> int scanf(0,1) wrapper(scanf) chk(__isoc99_scanf:0,1) \ + chk(__isoc23_scanf:0,1); fscanf (rw, r:str, ...) -> int scanf(1,2) wrapper(fscanf) \ - chk(__isoc99_fscanf: 0,1,2) chk(__isoc23_fscanf: 0,1,2); + chk(__isoc99_fscanf:0,1,2) chk(__isoc23_fscanf:0,1,2); sscanf (r:str, r:str, ...) -> int scanf(1,2) wrapper(sscanf) \ - chk(__isoc99_sscanf: 0,1,2) chk(__isoc23_sscanf: 0,1,2); + chk(__isoc99_sscanf:0,1,2) chk(__isoc23_sscanf:0,1,2); vscanf (r:str, other) -> int scanf(0,1) wrapper(vscanf) \ - chk(__isoc99_vscanf: 0,1) chk(__isoc23_vscanf: 0,1); + chk(__isoc99_vscanf:0,1) chk(__isoc23_vscanf:0,1); vfscanf (rw, r:str, other) -> int scanf(1,2) wrapper(vfscanf) \ - chk(__isoc99_vfscanf: 0,1,2) chk(__isoc23_vfscanf: 0,1,2); + chk(__isoc99_vfscanf:0,1,2) chk(__isoc23_vfscanf:0,1,2); vsscanf (r:str, r:str, other) -> int scanf(1,2) wrapper(vsscanf) \ - chk(__isoc99_vsscanf: 0,1,2) chk(__isoc23_vsscanf: 0,1,2); + chk(__isoc99_vsscanf:0,1,2) chk(__isoc23_vsscanf:0,1,2); remove (r:str) -> int; rename (r:str, r:str) -> int; renameat (int, r:str, int, r:str) -> int; -tmpnam (w:null-ok:bytes(L_tmpnam)) -> \ - arg(0):null-on-failure:or-static(tmpnam):str; -ctermid (w:null-ok:bytes(L_ctermid)) -> arg(0):or-static(ctermid):str; +tmpnam (w:null-ok) -> ptr; +ctermid (w:null-ok) -> ptr; #============================================================================ # , : conversions, environment, sorting, exit #============================================================================ -header stdlib.h; -atoi (r:str) -> int; -atol (r:str) -> int; -atoll (r:str) -> int; -atof (r:str) -> int; +atoi (r:str) -> int wrapper(atoi); +atol (r:str) -> int wrapper(atol); +atoll (r:str) -> int wrapper(atoll); +atof (r:str) -> int wrapper(atof); # The end pointer points into the string; glibc renames the integer forms # for C23 semantics. -strtol (r:str, w:null-ok:out(interior(0):nonnull), int) -> int \ - chk(__isoc23_strtol: 0,1,2); -strtoll (r:str, w:null-ok:out(interior(0):nonnull), int) -> int \ - chk(__isoc23_strtoll: 0,1,2); -strtoul (r:str, w:null-ok:out(interior(0):nonnull), int) -> int \ - chk(__isoc23_strtoul: 0,1,2); -strtoull (r:str, w:null-ok:out(interior(0):nonnull), int) -> int \ - chk(__isoc23_strtoull: 0,1,2); -strtod (r:str, w:null-ok:out(interior(0):nonnull)) -> int; -strtof (r:str, w:null-ok:out(interior(0):nonnull)) -> int; -strtold (r:str, w:null-ok:out(interior(0):nonnull)) -> int; -getenv (r:str:null-ok) -> static(environ):null-ok:str reads(environ); -secure_getenv (r:str) -> static(environ):null-ok:str reads(environ); -setenv (r:str:null-ok, r:str, int) -> int invalidates(environ) reads(environ); -unsetenv (r:str:null-ok) -> int invalidates(environ) reads(environ); +strtol (r:str, w:null-ok:bytes(8), int) -> int wrapper(strtol) \ + chk(__isoc23_strtol:0,1,2); +strtoll (r:str, w:null-ok:bytes(8), int) -> int wrapper(strtoll) \ + chk(__isoc23_strtoll:0,1,2); +strtoul (r:str, w:null-ok:bytes(8), int) -> int wrapper(strtoul) \ + chk(__isoc23_strtoul:0,1,2); +strtoull (r:str, w:null-ok:bytes(8), int) -> int wrapper(strtoull) \ + chk(__isoc23_strtoull:0,1,2); +strtod (r:str, w:null-ok:bytes(8)) -> int wrapper(strtod); +strtof (r:str, w:null-ok:bytes(8)) -> int wrapper(strtof); +strtold (r:str, w:null-ok:bytes(8)) -> int wrapper(strtold); +getenv (r:str:null-ok) -> ptr; +secure_getenv (r:str) -> ptr; +setenv (r:str:null-ok, r:str, int) -> int; +unsetenv (r:str:null-ok) -> int; # The string itself becomes part of the environment. -putenv (r:str:null-ok:retain(environ)) -> int invalidates(environ) \ - reads(environ); -clearenv () -> int invalidates(environ); -system (r:str:null-ok) -> int reads(environ); -qsort (rw:bytes(a1*a2):null-if-zero(a1*a2), int, int, fn:sync(0,0)) -> void; +putenv (r:str:null-ok) -> int; +system (r:str:null-ok) -> int; +qsort (rw:bytes(a1*a2):null-if-zero(a1*a2), int, int, fn) -> void; # glibc (and C2y): compar(a, b, arg). BSD and macOS: compar(thunk, a, b). -qsort_r (rw:bytes(a1*a2):null-if-zero(a1*a2), int, int, fn:sync(0,0,4), \ - none:null-ok) -> void; -qsort_r (rw:bytes(a1*a2):null-if-zero(a1*a2), int, int, none:null-ok, \ - fn:sync(3,0,0)) -> void; -bsearch (none:null-ok, r:bytes(a2*a3):null-if-zero(a2*a3), int, int, \ - fn:sync(0,1)) -> interior(1):null-ok; -exit (int) -> noreturn exits; -_Exit (int) -> noreturn exits; -quick_exit (int) -> noreturn exits; -abort () -> noreturn exits; -atexit (fn:at-exit) -> int; -at_quick_exit (fn:at-exit) -> int; -on_exit (fn:at-exit, none:null-ok:escape) -> int; +qsort_r (rw:bytes(a1*a2):null-if-zero(a1*a2), int, int, fn, none:null-ok) -> \ + void; +qsort_r (rw:bytes(a1*a2):null-if-zero(a1*a2), int, int, none:null-ok, fn) -> \ + void; +bsearch (none:null-ok, r:bytes(a2*a3):null-if-zero(a2*a3), int, int, fn) -> \ + ptr; mblen (r:bytes(min(a1,strlen(a0)+1)):null-ok, int) -> int; mbtowc (w:null-ok, r:bytes(min(a2,strlen(a1)+1)):null-ok, int) -> int; -wctomb (w:bytes(MB_CUR_MAX):null-ok, int) -> int chk(__wctomb_chk: 0,1,-1); -mbstowcs (w:count(a2):null-ok, r:str, int) -> int \ - chk(__mbstowcs_chk: 0,1,2,-1); +wctomb (w:null-ok, int) -> int chk(__wctomb_chk:0,1,size); +mbstowcs (w:bytes(a2*4):null-ok, r:str, int) -> int \ + chk(__mbstowcs_chk:0,1,2,_); wcstombs (w:bytes(a2):null-ok, r:str, int) -> int \ - chk(__wcstombs_chk: 0,1,2,-1); + chk(__wcstombs_chk:0,1,2,size); mkstemp (rw:str) -> int; mkostemp (rw:str, int) -> int; mkstemps (rw:str, int) -> int; -mkdtemp (rw:str) -> arg(0):null-on-failure; -mktemp (rw:str) -> arg(0); -rand () -> int; -srand (int) -> void; +mkdtemp (rw:str) -> ptr; +mktemp (rw:str) -> ptr; rand_r (rw) -> int; -random () -> int; -srandom (int) -> void; # The library keeps using the state array after the call. -initstate (int, w:bytes(a2):escape, int) -> ptr:null-ok; -setstate (rw:escape) -> ptr:null-ok; -drand48 () -> int; -erand48 (rw:count(3)) -> int; -lrand48 () -> int; -nrand48 (rw:count(3)) -> int; -mrand48 () -> int; -jrand48 (rw:count(3)) -> int; -srand48 (int) -> void; -seed48 (r:count(3)) -> static(seed48):nonnull; -lcong48 (r:count(7)) -> void; -arc4random () -> int; -arc4random_uniform (int) -> int; +initstate (int, w:bytes(a2), int) -> ptr; +setstate (rw) -> ptr; +erand48 (rw:bytes(6)) -> int; +nrand48 (rw:bytes(6)) -> int; +jrand48 (rw:bytes(6)) -> int; +seed48 (r:bytes(6)) -> ptr; +lcong48 (r:bytes(14)) -> void; arc4random_buf (w:bytes(a1), int) -> void; -getsubopt (rw, r, w:out(ptr:null-ok)) -> int; +getsubopt (rw, r, w:bytes(8)) -> int; a64l (r:str) -> int; -l64a (int) -> static(l64a):nonnull:str; # At most three averages are stored. -getloadavg (w:count(min(a1,3)):null-if-zero(a1), int) -> int; -# BSD and macOS: the name set by setprogname is kept, not copied. -getprogname () -> interior-state(progname):nonnull:str reads(progname); -setprogname (r:str:retain(progname)) -> void; -ptsname (int) -> static(ptsname):null-on-failure:str; +getloadavg (w:bytes(min(a1,3)*8):null-if-zero(a1), int) -> int; +setprogname (r:str) -> void; ptsname_r (int, w:bytes(a2):null-ok, int) -> int; -posix_openpt (int) -> int; -grantpt (int) -> int; -unlockpt (int) -> int; -header inttypes.h; -strtoimax (r:str, w:null-ok:out(interior(0):nonnull), int) -> int \ - chk(__isoc23_strtoimax: 0,1,2); -strtoumax (r:str, w:null-ok:out(interior(0):nonnull), int) -> int \ - chk(__isoc23_strtoumax: 0,1,2); +strtoimax (r:str, w:null-ok:bytes(8), int) -> int wrapper(strtoimax) \ + chk(__isoc23_strtoimax:0,1,2); +strtoumax (r:str, w:null-ok:bytes(8), int) -> int wrapper(strtoumax) \ + chk(__isoc23_strtoumax:0,1,2); #============================================================================ # , , and file-system relatives #============================================================================ -header unistd.h; -read (int, w:bytes(a2):null-if-zero(a2), int) -> int chk(__read_chk: 0,1,2,-1); +read (int, w:bytes(a2):null-if-zero(a2), int) -> int \ + chk(__read_chk:0,1,2,size); write (int, r:bytes(a2):null-if-zero(a2), int) -> int; pread (int, w:bytes(a2):null-if-zero(a2), int, int) -> int \ - chk(__pread_chk: 0,1,2,3,-1) chk(__pread64_chk: 0,1,2,3,-1); + chk(__pread_chk:0,1,2,3,size) chk(__pread64_chk:0,1,2,3,size); pwrite (int, r:bytes(a2):null-if-zero(a2), int, int) -> int; -close (int) -> int; -pipe (w:count(2)) -> int; -pipe2 (w:count(2), int) -> int; -dup (int) -> int; -dup2 (int, int) -> int; -dup3 (int, int, int) -> int; -lseek (int, int, int) -> int; +pipe (w:bytes(8)) -> int; +pipe2 (w:bytes(8), int) -> int; access (r:str, int) -> int; faccessat (int, r:str, int, int) -> int; unlink (r:str) -> int; unlinkat (int, r:str, int) -> int; rmdir (r:str) -> int; chdir (r:str) -> int; -fchdir (int) -> int; -getwd (w:bytes(PATH_MAX)) -> arg(0):null-on-failure; +getwd (w) -> ptr; readlink (r:str, w:bytes(a2):null-if-zero(a2), int) -> int \ - chk(__readlink_chk: 0,1,2,-1); + chk(__readlink_chk:0,1,2,size); readlinkat (int, r:str, w:bytes(a3):null-if-zero(a3), int) -> int \ - chk(__readlinkat_chk: 0,1,2,3,-1); + chk(__readlinkat_chk:0,1,2,3,size); symlink (r:str, r:str) -> int; symlinkat (r:str, int, r:str) -> int; link (r:str, r:str) -> int; linkat (int, r:str, int, r:str, int) -> int; chown (r:str, int, int) -> int; -fchown (int, int, int) -> int; lchown (r:str, int, int) -> int; fchownat (int, r:str, int, int, int) -> int; truncate (r:str, int) -> int; -ftruncate (int, int) -> int; -fsync (int) -> int; -fdatasync (int) -> int; -sync () -> void; -isatty (int) -> int; -ttyname (int) -> static(ttyname):null-on-failure:str; -ttyname_r (int, w:bytes(a2):null-ok, int) -> int chk(__ttyname_r_chk: 0,1,2,-1); -gethostname (w:bytes(a1), int) -> int chk(__gethostname_chk: 0,1,-1); +ttyname_r (int, w:bytes(a2):null-ok, int) -> int \ + chk(__ttyname_r_chk:0,1,2,size); +gethostname (w:bytes(a1), int) -> int chk(__gethostname_chk:0,1,size); sethostname (r:bytes(a1), int) -> int; -getdomainname (w:bytes(a1), int) -> int chk(__getdomainname_chk: 0,1,-1); -getlogin () -> static(getlogin):null-on-failure:str; -getlogin_r (w:bytes(a1), int) -> int chk(__getlogin_r_chk: 0,1,-1); +getdomainname (w:bytes(a1), int) -> int chk(__getdomainname_chk:0,1,size); +getlogin_r (w:bytes(a1), int) -> int chk(__getlogin_r_chk:0,1,size); # GNU getopt permutes argv; optarg points into its strings. -getopt (int, rw:count(a0), r:str) -> int; -execv (r:str, r:null-ok) -> int reads(environ); +getopt (int, rw:bytes(a0*8), r:str) -> int; +execv (r:str, r:null-ok) -> int; execve (r:str, r:null-ok, r:null-ok) -> int; -execvp (r:str, r:null-ok) -> int reads(environ); -execvpe (r:str, r:null-ok, r:null-ok) -> int reads(environ); -execl (r:str, r:str:null-ok, ...) -> int reads(environ); -execlp (r:str, r:str:null-ok, ...) -> int reads(environ); +execvp (r:str, r:null-ok) -> int; +execvpe (r:str, r:null-ok, r:null-ok) -> int; +execl (r:str, r:str:null-ok, ...) -> int; +execlp (r:str, r:str:null-ok, ...) -> int; execle (r:str, r:str:null-ok, ...) -> int; fexecve (int, r:null-ok, r:null-ok) -> int; -fork () -> int; -vfork () -> int returns-twice; -_exit (int) -> noreturn exits; -alarm (int) -> int; -pause () -> int; -sleep (int) -> int; -usleep (int) -> int; -getpid () -> int; -getppid () -> int; -sysconf (int) -> int; pathconf (r:str, int) -> int; -fpathconf (int, int) -> int; confstr (int, w:bytes(a2):null-if-zero(a2), int) -> int \ - chk(__confstr_chk: 0,1,2,-1); + chk(__confstr_chk:0,1,2,size); chroot (r:str) -> int; -nice (int) -> int; getentropy (w:bytes(a1):null-if-zero(a1), int) -> int; -getgroups (int, w:count(a0):null-if-zero(a0)) -> int \ - chk(__getgroups_chk: 0,1,-1); -swab (r:bytes(a2):null-if-zero(a2), w:bytes(a2):null-if-zero(a2), int) -> void \ - disjoint(0,1,a2); - -header getopt.h; -getopt_long (int, rw:count(a0), r:str, r:null-ok, w:null-ok) -> int; -getopt_long_only (int, rw:count(a0), r:str, r:null-ok, w:null-ok) -> int; - -header fcntl.h; -open (r:str, int, ...) -> int chk(__open_2: 0,1); -openat (int, r:str, int, ...) -> int chk(__openat_2: 0,1,2); +getgroups (int, w:bytes(a0*4):null-if-zero(a0)) -> int \ + chk(__getgroups_chk:0,1,size); +swab (r:bytes(a2):null-if-zero(a2), w:bytes(a2):null-if-zero(a2), int) -> \ + void disjoint(0,1,a2); + +getopt_long (int, rw:bytes(a0*8), r:str, r:null-ok, w:null-ok) -> int; +getopt_long_only (int, rw:bytes(a0*8), r:str, r:null-ok, w:null-ok) -> int; + +open (r:str, int, ...) -> int chk(__open_2:0,1); +openat (int, r:str, int, ...) -> int chk(__openat_2:0,1,2); creat (r:str, int) -> int; -fcntl (int, int, ...) -> int; -posix_fadvise (int, int, int, int) -> int; -posix_fallocate (int, int, int) -> int; -header sys/stat.h; stat (r:str, w) -> int; fstat (int, w) -> int; lstat (r:str, w) -> int; fstatat (int, r:str, w, int) -> int; chmod (r:str, int) -> int; -fchmod (int, int) -> int; fchmodat (int, r:str, int, int) -> int; mkdir (r:str, int) -> int; mkdirat (int, r:str, int) -> int; @@ -954,25 +490,20 @@ mkfifo (r:str, int) -> int; mkfifoat (int, r:str, int) -> int; mknod (r:str, int, int) -> int; mknodat (int, r:str, int, int) -> int; -umask (int) -> int; -utimensat (int, r:str:null-ok, r:count(2):null-ok, int) -> int; -futimens (int, r:count(2):null-ok) -> int; +utimensat (int, r:str:null-ok, r:bytes(32):null-ok, int) -> int; +futimens (int, r:bytes(32):null-ok) -> int; -header sys/statvfs.h; statvfs (r:str, w) -> int; fstatvfs (int, w) -> int; -header sys/statfs.h; statfs (r:str, w) -> int; fstatfs (int, w) -> int; -header utime.h; utime (r:str, r:null-ok) -> int; -header sys/time.h; -utimes (r:str, r:count(2):null-ok) -> int; -futimes (int, r:count(2):null-ok) -> int; -lutimes (r:str, r:count(2):null-ok) -> int; +utimes (r:str, r:bytes(32):null-ok) -> int; +futimes (int, r:bytes(32):null-ok) -> int; +lutimes (r:str, r:bytes(32):null-ok) -> int; gettimeofday (w:null-ok, w:null-ok) -> int; settimeofday (r:null-ok, r:null-ok) -> int; getitimer (int, w) -> int; @@ -982,42 +513,35 @@ setitimer (int, r:null-ok, w:null-ok) -> int; # , , , #============================================================================ -header dirent.h; -opendir (r:str) -> fresh(closedir):null-on-failure; -fdopendir (int) -> fresh(closedir):null-on-failure; -closedir (rw:release(closedir)) -> int; +opendir (r:str) -> ptr; # The entry lives in the stream: closedir ends it. -readdir (rw) -> interior(0):null-ok; -readdir_r (rw, w, w:out(arg(1):null-ok)) -> int; +readdir (rw) -> ptr; +readdir_r (rw, w, w:bytes(8)) -> int; rewinddir (rw) -> void; seekdir (rw, int) -> void; telldir (r) -> int; dirfd (r) -> int; # The array and each entry in it are the caller's to free. -scandir (r:str, w:out(fresh(free):null-on-failure:no-zero-init), \ - fn:null-ok:sync(), fn:null-ok:sync()) -> int; +scandir (r:str, w:bytes(8), fn, fn) -> int; alphasort (r, r) -> int; versionsort (r, r) -> int; -header time.h; time (w:null-ok) -> int; -clock () -> int; -difftime (int, int) -> int; -mktime (rw) -> int reads(environ); +mktime (rw) -> int; timegm (rw) -> int; # asctime and ctime share one buffer, gmtime and localtime one struct tm. -asctime (r) -> static(asctime):null-on-failure:str; -ctime (r) -> static(asctime):null-on-failure:str reads(environ); -gmtime (r) -> static(tm):null-on-failure; -localtime (r) -> static(tm):null-on-failure reads(environ); -asctime_r (r, w:bytes(26)) -> arg(1):null-on-failure:str; -ctime_r (r, w:bytes(26)) -> arg(1):null-on-failure:str reads(environ); -gmtime_r (r, w) -> arg(1):null-on-failure; -localtime_r (r, w) -> arg(1):null-on-failure reads(environ); -strftime (w:bytes(a1):null-if-zero(a1), int, r:str, r) -> int reads(environ); -strftime_l (w:bytes(a1):null-if-zero(a1), int, r:str, r, none:null-ok) -> int; -strptime (r:str, r:str, w) -> interior(0):null-ok; -tzset () -> void reads(environ); +asctime (r) -> ptr; +ctime (r) -> ptr; +gmtime (r) -> ptr; +localtime (r) -> ptr; +asctime_r (r, w:bytes(26)) -> ptr; +ctime_r (r, w:bytes(26)) -> ptr; +gmtime_r (r, w) -> ptr; +localtime_r (r, w) -> ptr; +strftime (w:bytes(a1):null-if-zero(a1), int, r:str, r) -> int; +strftime_l (w:bytes(a1):null-if-zero(a1), int, r:str, r, none:null-ok) -> \ + int; +strptime (r:str, r:str, w) -> ptr; timespec_get (w, int) -> int; timespec_getres (w:null-ok, int) -> int; clock_gettime (int, w) -> int; @@ -1026,36 +550,22 @@ clock_settime (int, r) -> int; clock_nanosleep (int, int, r, w:null-ok) -> int; nanosleep (r, w:null-ok) -> int; # glibc's timer_t is an opaque pointer that is never dereferenced. -timer_create (int, r:null-ok:entry(), w) -> int; +timer_create (int, r:null-ok, w) -> int; timer_settime (none:null-ok, int, r, w:null-ok) -> int; timer_gettime (none:null-ok, w) -> int; -timer_getoverrun (none:null-ok) -> int; -timer_delete (none:null-ok) -> int; -header sys/mman.h; # mmap fails with MAP_FAILED, not null. -mmap (none:null-ok, int, int, int, int, int) -> \ - fresh(munmap):extent(a1):nonnull wrapper(mmap); +mmap (none:null-ok, int, int, int, int, int) -> ptr wrapper(mmap); # munmap may unmap part of a mapping (a guard page): RFC 0034 section 6.3. -munmap (none:null-ok:release(munmap):range(a1), int) -> int \ - wrapper(munmap); -mprotect (none:null-ok, int, int) -> int; -msync (none:null-ok, int, int) -> int; -madvise (none:null-ok, int, int) -> int; -posix_madvise (none:null-ok, int, int) -> int; -mlock (none:null-ok, int) -> int; -munlock (none:null-ok, int) -> int; +munmap (none:null-ok:release, int) -> int wrapper(munmap); mincore (none:null-ok, int, w:null-if-zero(a1)) -> int; shm_open (r:str, int, int) -> int; shm_unlink (r:str) -> int; memfd_create (r:str, int) -> int; -header dlfcn.h; -dlopen (r:str:null-ok, int) -> fresh(dlclose):null-on-failure; -dlclose (none:null-ok:release(dlclose)) -> int; +dlopen (r:str:null-ok, int) -> ptr; # The handle may be a pseudo-handle (RTLD_DEFAULT, RTLD_NEXT). -dlsym (none:null-ok, r:str) -> ptr:null-ok; -dlerror () -> static(dlerror):null-ok:str; +dlsym (none:null-ok, r:str) -> ptr; dladdr (none:null-ok, w) -> int; #============================================================================ @@ -1064,107 +574,67 @@ dladdr (none:null-ok, w) -> int; # The non-reentrant lookups return static storage that the next lookup of # the same database overwrites, and glibc may reallocate it. -header pwd.h; -getpwnam (r:str) -> static(passwd):null-ok invalidates(passwd); -getpwuid (int) -> static(passwd):null-ok invalidates(passwd); -getpwent () -> static(passwd):null-ok invalidates(passwd); -setpwent () -> void; -endpwent () -> void; -getpwnam_r (r:str, w, w:bytes(a3), int, w:out(arg(1):null-ok)) -> int; -getpwuid_r (int, w, w:bytes(a3), int, w:out(arg(1):null-ok)) -> int; - -header grp.h; -getgrnam (r:str) -> static(group):null-ok invalidates(group); -getgrgid (int) -> static(group):null-ok invalidates(group); -getgrent () -> static(group):null-ok invalidates(group); -setgrent () -> void; -endgrent () -> void; -getgrnam_r (r:str, w, w:bytes(a3), int, w:out(arg(1):null-ok)) -> int; -getgrgid_r (int, w, w:bytes(a3), int, w:out(arg(1):null-ok)) -> int; -setgroups (int, r:count(a0):null-if-zero(a0)) -> int; +getpwnam (r:str) -> ptr; +getpwnam_r (r:str, w, w:bytes(a3), int, w:bytes(8)) -> int; +getpwuid_r (int, w, w:bytes(a3), int, w:bytes(8)) -> int; + +getgrnam (r:str) -> ptr; +getgrnam_r (r:str, w, w:bytes(a3), int, w:bytes(8)) -> int; +getgrgid_r (int, w, w:bytes(a3), int, w:bytes(8)) -> int; +setgroups (int, r:bytes(a0*4):null-if-zero(a0)) -> int; initgroups (r:str, int) -> int; -header locale.h; # The previous result's storage is freed by glibc when the locale changes. -setlocale (int, r:str:null-ok) -> static(setlocale):null-on-failure:str \ - invalidates(setlocale) invalidates(localeconv) invalidates(nl_langinfo); -localeconv () -> static(localeconv):nonnull; +setlocale (int, r:str:null-ok) -> ptr; # The base locale is reused for the result, or freed, on success only. -newlocale (int, r:str:null-ok, none:null-ok:realloc(freelocale)) -> \ - fresh(freelocale):null-on-failure; -duplocale (none:null-ok) -> fresh(freelocale):null-on-failure; -freelocale (none:null-ok:release(freelocale)) -> void; -uselocale (none:null-ok) -> ptr:null-ok; +newlocale (int, r:str:null-ok, none:null-ok:realloc) -> ptr; -header langinfo.h; -nl_langinfo (int) -> static(nl_langinfo):nonnull:str; -nl_langinfo_l (int, none:null-ok) -> static(nl_langinfo):nonnull:str; - -header iconv.h; # iconv_open fails with (iconv_t)-1, not null. -iconv_open (r:str, r:str) -> fresh(iconv_close):nonnull; +iconv_open (r:str, r:str) -> ptr; iconv (none, rw:null-ok, rw:null-ok, rw:null-ok, rw:null-ok) -> int; -iconv_close (none:release(iconv_close)) -> int; -header libgen.h; # POSIX basename and dirname may modify the path; glibc's libgen.h renames # basename to __xpg_basename. -basename (rw:str:null-ok) -> interior(0):nonnull:str \ - chk(__xpg_basename: 0); -dirname (rw:str:null-ok) -> interior(0):nonnull:str; +basename (rw:str:null-ok) -> ptr chk(__xpg_basename:0); +dirname (rw:str:null-ok) -> ptr; -header regex.h; -regcomp (w:init(regfree), r:str, int) -> int; -regexec (r, r:str, int, w:count(a2):null-ok, int) -> int; +regcomp (w, r:str, int) -> int; +regexec (r, r:str, int, w:bytes(a2*8):null-ok, int) -> int; regerror (int, r:null-ok, w:bytes(a3):null-if-zero(a3), int) -> int; -regfree (rw:fini(regfree)) -> void; +regfree (rw) -> void; -header fnmatch.h; fnmatch (r:str, r:str, int) -> int; -header glob.h; -glob (r:str, int, fn:null-ok:sync(), rw:init(globfree)) -> int; -globfree (rw:fini(globfree)) -> void; +glob (r:str, int, fn, rw) -> int; +globfree (rw) -> void; -header wordexp.h; -wordexp (r:str, rw:init(wordfree), int) -> int reads(environ); -wordfree (rw:null-ok:fini(wordfree)) -> void; +wordexp (r:str, rw, int) -> int; +wordfree (rw:null-ok) -> void; -header err.h; -err (int, r:str:null-ok, ...) -> noreturn exits printf(1,2); -errx (int, r:str:null-ok, ...) -> noreturn exits printf(1,2); -verr (int, r:str:null-ok, other) -> noreturn exits printf(1,2); -verrx (int, r:str:null-ok, other) -> noreturn exits printf(1,2); +err (int, r:str:null-ok, ...) -> void printf(1,2); +errx (int, r:str:null-ok, ...) -> void printf(1,2); +verr (int, r:str:null-ok, other) -> void printf(1,2); +verrx (int, r:str:null-ok, other) -> void printf(1,2); warn (r:str:null-ok, ...) -> void printf(0,1); warnx (r:str:null-ok, ...) -> void printf(0,1); vwarn (r:str:null-ok, other) -> void printf(0,1); vwarnx (r:str:null-ok, other) -> void printf(0,1); -header error.h; # Exits when the status is non-zero, so it is not noreturn. error (int, int, r:str, ...) -> void printf(2,3); -error_at_line (int, int, r:str:null-ok, int, r:str, ...) -> void \ - printf(4,5); +error_at_line (int, int, r:str:null-ok, int, r:str, ...) -> void printf(4,5); -header syslog.h; # glibc and BSD keep the ident pointer, not a copy. -openlog (r:str:null-ok:retain(syslog), int, int) -> void; -syslog (int, r:str, ...) -> void printf(1,2) reads(syslog) \ - chk(__syslog_chk: 0,-1,1,2); -vsyslog (int, r:str, other) -> void printf(1,2) reads(syslog) \ - chk(__vsyslog_chk: 0,-1,1,2); -closelog () -> void; -setlogmask (int) -> int; - -header sys/utsname.h; +openlog (r:str:null-ok, int, int) -> void; +syslog (int, r:str, ...) -> void printf(1,2) chk(__syslog_chk:0,_,1,2); +vsyslog (int, r:str, other) -> void printf(1,2) chk(__vsyslog_chk:0,_,1,2); + uname (w) -> int; -header sys/resource.h; getrlimit (int, w) -> int; setrlimit (int, r) -> int; getrusage (int, w) -> int; -header termios.h; tcgetattr (int, w) -> int; tcsetattr (int, int, r) -> int; cfmakeraw (w) -> void; @@ -1178,19 +648,10 @@ cfsetospeed (w, int) -> int; #============================================================================ # pthread_t, thrd_t: an integer on glibc, a pointer on macOS ('other'). -header pthread.h; -pthread_create (w, r:null-ok, fn:entry(3), none:null-ok:escape) -> int; +pthread_create (w, r:null-ok, fn, none:null-ok) -> int; pthread_join (other, w:null-ok) -> int; -pthread_detach (other) -> int; -pthread_exit (none:null-ok:escape) -> noreturn; -pthread_self () -> int; -pthread_self () -> ptr:nonnull; -pthread_equal (other, other) -> int; -pthread_cancel (other) -> int; -pthread_kill (other, int) -> int; pthread_setcancelstate (int, w:null-ok) -> int; pthread_setcanceltype (int, w:null-ok) -> int; -pthread_testcancel () -> void; pthread_attr_init (w) -> int; pthread_attr_destroy (rw) -> int; pthread_attr_setdetachstate (rw, int) -> int; @@ -1198,7 +659,7 @@ pthread_attr_getdetachstate (r, w) -> int; pthread_attr_setstacksize (rw, int) -> int; pthread_attr_getstacksize (r, w) -> int; # The thread runs on the caller's stack memory. -pthread_attr_setstack (rw, none:escape, int) -> int; +pthread_attr_setstack (rw, none, int) -> int; pthread_mutex_init (w, r:null-ok) -> int; pthread_mutex_destroy (rw) -> int; pthread_mutex_lock (rw) -> int; @@ -1234,27 +695,18 @@ pthread_spin_unlock (rw) -> int; pthread_barrier_init (w, r:null-ok, int) -> int; pthread_barrier_wait (rw) -> int; pthread_barrier_destroy (rw) -> int; -pthread_once (rw, fn:sync()) -> int; +pthread_once (rw, fn) -> int; # Destructors run at thread exit, concurrently with other threads. -pthread_key_create (w, fn:null-ok:entry()) -> int; -pthread_key_delete (int) -> int; -pthread_setspecific (int, none:null-ok:escape) -> int; -pthread_getspecific (int) -> ptr:null-ok; +pthread_key_create (w, fn) -> int; pthread_sigmask (int, r:null-ok, w:null-ok) -> int; -pthread_atfork (fn:null-ok:entry(), fn:null-ok:entry(), \ - fn:null-ok:entry()) -> int; # glibc names the thread explicitly; macOS names the calling thread. pthread_setname_np (other, r:str) -> int; pthread_setname_np (r:str) -> int; pthread_getname_np (other, w:bytes(a2), int) -> int; -header threads.h; -thrd_create (w, fn:entry(2), none:null-ok:escape) -> int; +thrd_create (w, fn, none:null-ok) -> int; thrd_join (other, w:null-ok) -> int; -thrd_detach (other) -> int; -thrd_exit (int) -> noreturn; thrd_sleep (r, w:null-ok) -> int; -thrd_yield () -> void; mtx_init (w, int) -> int; mtx_lock (rw) -> int; mtx_trylock (rw) -> int; @@ -1267,19 +719,10 @@ cnd_broadcast (rw) -> int; cnd_wait (rw, rw) -> int; cnd_timedwait (rw, rw, r) -> int; cnd_destroy (rw) -> void; -tss_create (w, fn:null-ok:entry()) -> int; -tss_delete (int) -> void; -tss_get (int) -> ptr:null-ok; -tss_set (int, none:null-ok:escape) -> int; -call_once (rw, fn:sync()) -> void; - -header signal.h; -# SIG_DFL is a null handler; the result is the previous handler. -signal (int, fn:null-ok:entry()) -> ptr; -sigaction (int, r:null-ok:entry(), w:null-ok) -> int; -raise (int) -> int; -kill (int, int) -> int; -killpg (int, int) -> int; +tss_create (w, fn) -> int; +call_once (rw, fn) -> void; + +sigaction (int, r:null-ok, w:null-ok) -> int; sigemptyset (w) -> int; sigfillset (w) -> int; sigaddset (rw, int) -> int; @@ -1292,83 +735,48 @@ sigwaitinfo (r, w:null-ok) -> int; sigtimedwait (r, w:null-ok, r:null-ok) -> int; sigpending (w) -> int; sigaltstack (r:null-ok, w:null-ok) -> int; -siginterrupt (int, int) -> int; -sigqueue (int, int, other) -> int; psignal (int, r:str:null-ok) -> void; psiginfo (r, r:str:null-ok) -> void; -header sys/wait.h; wait (w:null-ok) -> int; waitpid (int, w:null-ok, int) -> int; waitid (int, int, w:null-ok, int) -> int; wait4 (int, w:null-ok, int, w:null-ok) -> int; -# jmp_buf decays to a pointer; the rows do not model its contents. glibc's -# setjmp and sigsetjmp are macros for _setjmp and __sigsetjmp, and its -# fortified longjmp is __longjmp_chk. -header setjmp.h; -setjmp (other) -> int returns-twice; -_setjmp (other) -> int returns-twice; -sigsetjmp (other, int) -> int returns-twice chk(__sigsetjmp: 0,1); -longjmp (other, int) -> noreturn chk(__longjmp_chk: 0,1); -_longjmp (other, int) -> noreturn; -siglongjmp (other, int) -> noreturn; - -header sched.h; -sched_yield () -> int; sched_getaffinity (int, int, w:bytes(a1)) -> int; sched_setaffinity (int, int, r:bytes(a1)) -> int; sched_getparam (int, w:null-ok) -> int; sched_setparam (int, r:null-ok) -> int; sched_setscheduler (int, int, r:null-ok) -> int; -header semaphore.h; # SEM_FAILED is null on glibc and (sem_t *)-1 on macOS. -sem_open (r:str, int, ...) -> fresh(sem_close):null-ok; -sem_close (rw:release(sem_close)) -> int; +sem_open (r:str, int, ...) -> ptr; sem_unlink (r:str) -> int; sem_init (w, int, int) -> int; -# A semaphore sem_open returns may be no address at all (Darwin's is a -# small integer handle), so the calls on one read nothing a guard can name. -sem_destroy (none) -> int; -sem_wait (none) -> int; -sem_trywait (none) -> int; sem_timedwait (none, r) -> int; -sem_post (none) -> int; sem_getvalue (none, w) -> int; -header search.h; # The tree keeps the key; comparisons see the key and the tree's keys. -tsearch (none:null-ok:escape, rw:null-ok, fn:sync(0,1)) -> ptr:null-ok; -tfind (none:null-ok, r:null-ok, fn:sync(0,1)) -> ptr:null-ok; -tdelete (none:null-ok, rw:null-ok, fn:sync(0,1)) -> ptr:null-ok; -twalk (none:null-ok, fn:sync(0)) -> void; -tdestroy (none:null-ok, fn:sync()) -> void; +tsearch (none:null-ok, rw:null-ok, fn) -> ptr; +tfind (none:null-ok, r:null-ok, fn) -> ptr; +tdelete (none:null-ok, rw:null-ok, fn) -> ptr; # The element count is behind a pointer, which no term can read. -lfind (none:null-ok, r:bytes(__WEAVEC_UNBOUNDED):null-ok, rw, int, \ - fn:sync(0,1)) \ - -> interior(1):null-ok; -lsearch (none:null-ok, rw:bytes(__WEAVEC_UNBOUNDED), rw, int, fn:sync(0,1)) \ - -> interior(1):nonnull; - -header ftw.h; -ftw (r:str, fn:sync(), int) -> int; -nftw (r:str, fn:sync(), int, int) -> int; - -header spawn.h; -posix_spawn (w:null-ok, r:str, r:null-ok, r:null-ok, r:null-ok, r:null-ok) \ +lfind (none:null-ok, r:null-ok, rw, int, fn) -> ptr; +lsearch (none:null-ok, rw, rw, int, fn) -> ptr; + +ftw (r:str, fn, int) -> int; +nftw (r:str, fn, int, int) -> int; + +posix_spawn (w:null-ok, r:str, r:null-ok, r:null-ok, r:null-ok, r:null-ok) -> \ + int; +posix_spawnp (w:null-ok, r:str, r:null-ok, r:null-ok, r:null-ok, r:null-ok) \ -> int; -posix_spawnp (w:null-ok, r:str, r:null-ok, r:null-ok, r:null-ok, \ - r:null-ok) -> int \ - reads(environ); -header poll.h; -poll (rw:count(a1):null-if-zero(a1), int, int) -> int \ - chk(__poll_chk: 0,1,2,-1); -ppoll (rw:count(a1):null-if-zero(a1), int, r:null-ok, r:null-ok) -> int \ - chk(__ppoll_chk: 0,1,2,3,-1); +poll (rw:bytes(a1*8):null-if-zero(a1), int, int) -> int \ + chk(__poll_chk:0,1,2,size); +ppoll (rw:bytes(a1*8):null-if-zero(a1), int, r:null-ok, r:null-ok) -> int \ + chk(__ppoll_chk:0,1,2,3,size); -header sys/select.h; # The sets are read and written only in the words that cover a0 descriptors # (howmany(a0, NFDBITS) words, at least (a0 + 7) / 8 bytes on every library), # not sizeof(fd_set): a program may allocate exactly those words. @@ -1377,166 +785,103 @@ select (int, rw:null-ok:bytes((a0+7)/8), rw:null-ok:bytes((a0+7)/8), \ pselect (int, rw:null-ok:bytes((a0+7)/8), rw:null-ok:bytes((a0+7)/8), \ rw:null-ok:bytes((a0+7)/8), r:null-ok, r:null-ok) -> int; -header sys/epoll.h; -epoll_create (int) -> int; -epoll_create1 (int) -> int; epoll_ctl (int, int, int, r:null-ok) -> int; -epoll_wait (int, w:count(a2):null-if-zero(a2), int, int) -> int; -epoll_pwait (int, w:count(a2):null-if-zero(a2), int, int, r:null-ok) -> int; +epoll_wait (int, w:bytes(a2*12):null-if-zero(a2), int, int) -> int; +epoll_pwait (int, w:bytes(a2*12):null-if-zero(a2), int, int, r:null-ok) -> \ + int; -header sys/event.h; -kqueue () -> int; -kevent (int, r:count(a2):null-if-zero(a2), int, \ - w:count(a4):null-if-zero(a4), int, r:null-ok) -> int; +kevent (int, r:bytes(a2*32):null-if-zero(a2), int, \ + w:bytes(a4*32):null-if-zero(a4), int, r:null-ok) -> int; #============================================================================ # Sockets, name resolution, interfaces, I/O vectors #============================================================================ -# Socket addresses whose length is behind a pointer (accept, getsockname) -# need one struct sockaddr. -header sys/socket.h; -socket (int, int, int) -> int; -socketpair (int, int, int, w:count(2)) -> int; +socketpair (int, int, int, w:bytes(8)) -> int; bind (int, r:bytes(a2):null-if-zero(a2), int) -> int; connect (int, r:bytes(a2):null-if-zero(a2), int) -> int; -listen (int, int) -> int; accept (int, w:null-ok, rw:null-ok) -> int; accept4 (int, w:null-ok, rw:null-ok, int) -> int; -shutdown (int, int) -> int; send (int, r:bytes(a2):null-if-zero(a2), int, int) -> int; sendto (int, r:bytes(a2):null-if-zero(a2), int, int, r:bytes(a5):null-ok, \ int) -> int; sendmsg (int, r, int) -> int; recv (int, w:bytes(a2):null-if-zero(a2), int, int) -> int \ - chk(__recv_chk: 0,1,2,-1,3); -recvfrom (int, w:bytes(a2):null-if-zero(a2), int, int, w:null-ok, \ - rw:null-ok) -> int chk(__recvfrom_chk: 0,1,2,-1,3,4,5); + chk(__recv_chk:0,1,2,size,3); +recvfrom (int, w:bytes(a2):null-if-zero(a2), int, int, w:null-ok, rw:null-ok) \ + -> int chk(__recvfrom_chk:0,1,2,size,3,4,5); recvmsg (int, rw, int) -> int; getsockopt (int, int, int, w:null-ok, rw) -> int; setsockopt (int, int, int, r:bytes(a4):null-if-zero(a4), int) -> int; getsockname (int, w, rw) -> int; getpeername (int, w, rw) -> int; -header netdb.h; -getaddrinfo (r:str:null-ok, r:str:null-ok, r:null-ok, \ - w:out(fresh(freeaddrinfo):null-on-failure)) -> int; -freeaddrinfo (none:null-ok:release(freeaddrinfo)) -> void; -gai_strerror (int) -> static(gai_strerror):nonnull:str; +getaddrinfo (r:str:null-ok, r:str:null-ok, r:null-ok, w:bytes(8)) -> int; getnameinfo (r:bytes(a1):null-ok, int, w:bytes(a3):null-ok, int, \ w:bytes(a5):null-ok, int, int) -> int; -gethostbyname (r:str) -> static(hostent):null-ok invalidates(hostent); -gethostbyname2 (r:str, int) -> static(hostent):null-ok invalidates(hostent); -gethostbyaddr (r:bytes(a1), int, int) -> static(hostent):null-ok \ - invalidates(hostent); -getservbyname (r:str, r:str:null-ok) -> static(servent):null-ok \ - invalidates(servent); -getservbyport (int, r:str:null-ok) -> static(servent):null-ok \ - invalidates(servent); -getprotobyname (r:str) -> static(protoent):null-ok invalidates(protoent); -getprotobynumber (int) -> static(protoent):null-ok invalidates(protoent); +gethostbyname (r:str) -> ptr; +gethostbyname2 (r:str, int) -> ptr; +gethostbyaddr (r:bytes(a1), int, int) -> ptr; +getservbyname (r:str, r:str:null-ok) -> ptr; +getservbyport (int, r:str:null-ok) -> ptr; +getprotobyname (r:str) -> ptr; herror (r:str:null-ok) -> void; -hstrerror (int) -> static(hstrerror):nonnull:str; # The address size depends on the family (4 or 16 bytes): one element. -header arpa/inet.h; -inet_ntop (int, r, w:bytes(a3), int) -> arg(2):null-on-failure:str; +inet_ntop (int, r, w:bytes(a3), int) -> ptr; inet_pton (int, r:str, w) -> int; inet_addr (r:str) -> int; inet_aton (r:str, w:null-ok) -> int; inet_network (r:str) -> int; -inet_ntoa (other) -> static(inet_ntoa):nonnull:str; -htons (int) -> int; -htonl (int) -> int; -ntohs (int) -> int; -ntohl (int) -> int; -header ifaddrs.h; -getifaddrs (w:out(fresh(freeifaddrs):null-on-failure)) -> int; -freeifaddrs (none:null-ok:release(freeifaddrs)) -> void; +getifaddrs (w:bytes(8)) -> int; -header net/if.h; if_nametoindex (r:str) -> int; -if_indextoname (int, w:bytes(IF_NAMESIZE)) -> arg(1):null-on-failure:str; -if_nameindex () -> fresh(if_freenameindex):null-on-failure; -if_freenameindex (none:null-ok:release(if_freenameindex)) -> void; +if_indextoname (int, w) -> ptr; # Only the iovec array is described; the buffers it names are not. -header sys/uio.h; -readv (int, r:count(a2):null-if-zero(a2), int) -> int; -writev (int, r:count(a2):null-if-zero(a2), int) -> int; -preadv (int, r:count(a2):null-if-zero(a2), int, int) -> int; -pwritev (int, r:count(a2):null-if-zero(a2), int, int) -> int; +readv (int, r:bytes(a2*16):null-if-zero(a2), int) -> int; +writev (int, r:bytes(a2*16):null-if-zero(a2), int) -> int; +preadv (int, r:bytes(a2*16):null-if-zero(a2), int, int) -> int; +pwritev (int, r:bytes(a2*16):null-if-zero(a2), int, int) -> int; -header sys/sendfile.h; sendfile (int, int, rw:null-ok, int) -> int; -header sys/random.h; getrandom (w:bytes(a1):null-if-zero(a1), int, int) -> int; -header sys/eventfd.h; -eventfd (int, int) -> int; - -header sys/inotify.h; -inotify_init () -> int; -inotify_init1 (int) -> int; inotify_add_watch (int, r:str, int) -> int; -inotify_rm_watch (int, int) -> int; -header sys/timerfd.h; -timerfd_create (int, int) -> int; timerfd_settime (int, int, r, w:null-ok) -> int; timerfd_gettime (int, w) -> int; -header sys/signalfd.h; signalfd (int, r, int) -> int; -header sys/prctl.h; -prctl (int, ...) -> int; - -header sys/sysinfo.h; sysinfo (w) -> int; -header sys/sysctl.h; sysctlbyname (r:str, w:null-ok, rw:null-ok, r:null-ok, int) -> int; -header sys/file.h; -flock (int, int) -> int; - -header sys/shm.h; -shmget (int, int, int) -> int; -# shmat fails with (void *)-1, not null. -shmat (int, none:null-ok, int) -> fresh(shmdt):nonnull; -shmdt (none:null-ok:release(shmdt)) -> int; shmctl (int, int, rw:null-ok) -> int; -header mqueue.h; mq_open (r:str, int, ...) -> int; -mq_close (int) -> int; mq_unlink (r:str) -> int; mq_send (int, r:bytes(a2):null-if-zero(a2), int, int) -> int; mq_receive (int, w:bytes(a2), int, w:null-ok) -> int; # The control block and its buffer are used until the operation completes. -header aio.h; -aio_read (rw:escape) -> int; -aio_write (rw:escape) -> int; +aio_read (rw) -> int; +aio_write (rw) -> int; aio_error (r) -> int; aio_return (rw) -> int; -aio_suspend (r:count(a1), int, r:null-ok) -> int; +aio_suspend (r:bytes(a1*8), int, r:null-ok) -> int; aio_cancel (int, rw:null-ok) -> int; -header mntent.h; -setmntent (r:str, r:str) -> fresh(endmntent):null-on-failure; -getmntent (rw) -> static(mntent):null-ok invalidates(mntent); -endmntent (rw:null-ok:release(endmntent)) -> int; +setmntent (r:str, r:str) -> ptr; +getmntent (rw) -> ptr; -header fts.h; -fts_open (r, int, fn:null-ok:sync()) -> fresh(fts_close):null-on-failure; -fts_read (rw) -> interior(0):null-ok; -fts_children (rw, int) -> interior(0):null-ok; +fts_open (r, int, fn) -> ptr; +fts_read (rw) -> ptr; +fts_children (rw, int) -> ptr; fts_set (rw, rw, int) -> int; -fts_close (rw:release(fts_close)) -> int; #============================================================================ # Wide and multibyte characters (, , ) @@ -1544,24 +889,22 @@ fts_close (rw:release(fts_close)) -> int; # Lengths count wchar_t elements ('wide'): count(t), and strlen(aN) is # wcslen. No disjoint clauses: their lengths are in bytes. -header wchar.h; wcslen (r:str) -> int:value(strlen(a0)) wide; wcsnlen (r:count(min(a1,strlen(a0)+1)):null-if-zero(a1), int) -> \ int:value(min(a1,strlen(a0))) wide; -wcscpy (w:count(strlen(a1)+1), r:str) -> arg(0) chk(__wcscpy_chk: 0,1,-1) wide; +wcscpy (w:count(strlen(a1)+1), r:str) -> ptr chk(__wcscpy_chk:0,1,size) wide; wcsncpy (w:count(a2):null-if-zero(a2), \ - r:count(min(a2,strlen(a1)+1)):null-if-zero(a2), int) -> arg(0) \ - chk(__wcsncpy_chk: 0,1,2,-1) wide; -wcpcpy (w:count(strlen(a1)+1), r:str) -> interior(0):nonnull \ - chk(__wcpcpy_chk: 0,1,-1) wide; + r:count(min(a2,strlen(a1)+1)):null-if-zero(a2), int) -> ptr \ + chk(__wcsncpy_chk:0,1,2,size) wide; +wcpcpy (w:count(strlen(a1)+1), r:str) -> ptr chk(__wcpcpy_chk:0,1,size) wide; wcpncpy (w:count(a2):null-if-zero(a2), \ - r:count(min(a2,strlen(a1)+1)):null-if-zero(a2), int) \ - -> interior(0):nonnull chk(__wcpncpy_chk: 0,1,2,-1) wide; -wcscat (rw:count(strlen(a0)+strlen(a1)+1):str, r:str) -> arg(0) \ - chk(__wcscat_chk: 0,1,-1) wide; + r:count(min(a2,strlen(a1)+1)):null-if-zero(a2), int) -> ptr \ + chk(__wcpncpy_chk:0,1,2,size) wide; +wcscat (rw:count(strlen(a0)+strlen(a1)+1):str, r:str) -> ptr \ + chk(__wcscat_chk:0,1,size) wide; wcsncat (rw:count(strlen(a0)+min(a2,strlen(a1))+1):str, \ - r:count(min(a2,strlen(a1)+1)):null-if-zero(a2), int) -> arg(0) \ - chk(__wcsncat_chk: 0,1,2,-1) wide; + r:count(min(a2,strlen(a1)+1)):null-if-zero(a2), int) -> ptr \ + chk(__wcsncat_chk:0,1,2,size) wide; wcscmp (r:str, r:str) -> int wide; wcsncmp (r:count(min(a2,strlen(a0)+1)):null-if-zero(a2), \ r:count(min(a2,strlen(a1)+1)):null-if-zero(a2), int) -> int wide; @@ -1570,129 +913,102 @@ wcsncasecmp (r:count(min(a2,strlen(a0)+1)):null-if-zero(a2), \ r:count(min(a2,strlen(a1)+1)):null-if-zero(a2), int) -> int wide; wcscoll (r:str, r:str) -> int wide; wcsxfrm (w:count(a2):null-if-zero(a2), r:str, int) -> int wide; -wcschr (r:str, int) -> interior(0):null-ok wide; -wcsrchr (r:str, int) -> interior(0):null-ok wide; -wcsstr (r:str, r:str) -> interior(0):null-ok wide; -wcspbrk (r:str, r:str) -> interior(0):null-ok wide; +wcschr (r:str, int) -> ptr wide; +wcsrchr (r:str, int) -> ptr wide; +wcsstr (r:str, r:str) -> ptr wide; +wcspbrk (r:str, r:str) -> ptr wide; wcsspn (r:str, r:str) -> int wide; wcscspn (r:str, r:str) -> int wide; -wcstok (rw:str:null-ok, r:str, rw:out(interior(0):null-ok)) -> \ - interior(0):null-ok wide; +wcstok (rw:str:null-ok, r:str, rw:bytes(8)) -> ptr wide; wcswidth (r:count(min(a1,strlen(a0)+1)):null-if-zero(a1), int) -> int wide; -wcwidth (int) -> int wide; -wmemcpy (w:count(a2):null-if-zero(a2), r:count(a2):null-if-zero(a2), int) \ - -> arg(0) chk(__wmemcpy_chk: 0,1,2,-1) wide; -wmemmove (w:count(a2):null-if-zero(a2), r:count(a2):null-if-zero(a2), int) \ - -> arg(0) chk(__wmemmove_chk: 0,1,2,-1) wide; -wmemset (w:count(a2):null-if-zero(a2), int, int) -> arg(0) \ - chk(__wmemset_chk: 0,1,2,-1) wide; -wmemcmp (r:count(a2):null-if-zero(a2), r:count(a2):null-if-zero(a2), int) \ - -> int wide; -wmemchr (r:count(min(a2,strlen(a0)+1)):null-if-zero(a2), int, int) -> \ - interior(0):null-ok wide; -wmempcpy (w:count(a2):null-if-zero(a2), r:count(a2):null-if-zero(a2), int) \ - -> interior(0):nonnull \ - chk(__wmempcpy_chk: 0,1,2,-1) wide; -wcstol (r:str, w:null-ok:out(interior(0):nonnull), int) -> int \ - chk(__isoc23_wcstol: 0,1,2) wide; -wcstoll (r:str, w:null-ok:out(interior(0):nonnull), int) -> int \ - chk(__isoc23_wcstoll: 0,1,2) wide; -wcstoul (r:str, w:null-ok:out(interior(0):nonnull), int) -> int \ - chk(__isoc23_wcstoul: 0,1,2) wide; -wcstoull (r:str, w:null-ok:out(interior(0):nonnull), int) -> int \ - chk(__isoc23_wcstoull: 0,1,2) wide; -wcstod (r:str, w:null-ok:out(interior(0):nonnull)) -> int wide; -wcstof (r:str, w:null-ok:out(interior(0):nonnull)) -> int wide; -wcstold (r:str, w:null-ok:out(interior(0):nonnull)) -> int wide; -btowc (int) -> int wide; -wctob (int) -> int wide; +wmemcpy (w:count(a2):null-if-zero(a2), r:count(a2):null-if-zero(a2), int) -> \ + ptr chk(__wmemcpy_chk:0,1,2,size) wide; +wmemmove (w:count(a2):null-if-zero(a2), r:count(a2):null-if-zero(a2), int) -> \ + ptr chk(__wmemmove_chk:0,1,2,size) wide; +wmemset (w:count(a2):null-if-zero(a2), int, int) -> ptr \ + chk(__wmemset_chk:0,1,2,size) wide; +wmemcmp (r:count(a2):null-if-zero(a2), r:count(a2):null-if-zero(a2), int) -> \ + int wide; +wmemchr (r:count(min(a2,strlen(a0)+1)):null-if-zero(a2), int, int) -> ptr \ + wide; +wmempcpy (w:count(a2):null-if-zero(a2), r:count(a2):null-if-zero(a2), int) -> \ + ptr chk(__wmempcpy_chk:0,1,2,size) wide; +wcstol (r:str, w:null-ok:bytes(8), int) -> int chk(__isoc23_wcstol:0,1,2) \ + wide; +wcstoll (r:str, w:null-ok:bytes(8), int) -> int chk(__isoc23_wcstoll:0,1,2) \ + wide; +wcstoul (r:str, w:null-ok:bytes(8), int) -> int chk(__isoc23_wcstoul:0,1,2) \ + wide; +wcstoull (r:str, w:null-ok:bytes(8), int) -> int chk(__isoc23_wcstoull:0,1,2) \ + wide; +wcstod (r:str, w:null-ok:bytes(8)) -> int wide; +wcstof (r:str, w:null-ok:bytes(8)) -> int wide; +wcstold (r:str, w:null-ok:bytes(8)) -> int wide; mbsinit (r:null-ok) -> int wide; mbrlen (r:bytes(min(a1,strlen(a0)+1)):null-ok, int, rw:null-ok) -> int wide; mbrtowc (w:null-ok, r:bytes(min(a2,strlen(a1)+1)):null-ok, int, rw:null-ok) \ -> int wide; -wcrtomb (w:bytes(MB_CUR_MAX):null-ok, int, rw:null-ok) -> int \ - chk(__wcrtomb_chk: 0,1,2,-1) wide; +wcrtomb (w:null-ok, int, rw:null-ok) -> int chk(__wcrtomb_chk:0,1,2,size) \ + wide; mbsrtowcs (w:count(a2):null-ok, rw, int, rw:null-ok) -> int \ - chk(__mbsrtowcs_chk: 0,1,2,3,-1) wide; + chk(__mbsrtowcs_chk:0,1,2,3,size) wide; wcsrtombs (w:bytes(a2):null-ok, rw, int, rw:null-ok) -> int \ - chk(__wcsrtombs_chk: 0,1,2,3,-1) wide; + chk(__wcsrtombs_chk:0,1,2,3,size) wide; mbsnrtowcs (w:count(a3):null-ok, rw, int, int, rw:null-ok) -> int \ - chk(__mbsnrtowcs_chk: 0,1,2,3,4,-1) wide; + chk(__mbsnrtowcs_chk:0,1,2,3,4,size) wide; wcsnrtombs (w:bytes(a3):null-ok, rw, int, int, rw:null-ok) -> int \ - chk(__wcsnrtombs_chk: 0,1,2,3,4,-1) wide; + chk(__wcsnrtombs_chk:0,1,2,3,4,size) wide; fgetwc (rw) -> int wide; getwc (rw) -> int wide; -getwchar () -> int wide; fputwc (int, rw) -> int wide; putwc (int, rw) -> int wide; -putwchar (int) -> int wide; ungetwc (int, rw) -> int wide; -fgetws (w:count(a1):null-if-zero(a1), int, rw) -> arg(0):null-ok \ - chk(__fgetws_chk: 0,-1,1,2) wide; +fgetws (w:count(a1):null-if-zero(a1), int, rw) -> ptr \ + chk(__fgetws_chk:0,size,1,2) wide; fputws (r:str, rw) -> int wide; fwide (rw, int) -> int wide; -wprintf (r:str, ...) -> int printf(0,1) chk(__wprintf_chk: -1,0,1) wide; -fwprintf (rw, r:str, ...) -> int printf(1,2) \ - chk(__fwprintf_chk: 0,-1,1,2) wide; +wprintf (r:str, ...) -> int printf(0,1) chk(__wprintf_chk:_,0,1) wide; +fwprintf (rw, r:str, ...) -> int printf(1,2) chk(__fwprintf_chk:0,_,1,2) \ + wide; swprintf (w:count(a1):null-if-zero(a1), int, r:str, ...) -> int printf(2,3) \ - chk(__swprintf_chk: 0,1,-1,-1,2,3) wide; -vwprintf (r:str, other) -> int printf(0,1) chk(__vwprintf_chk: -1,0,1) wide; -vfwprintf (rw, r:str, other) -> int printf(1,2) \ - chk(__vfwprintf_chk: 0,-1,1,2) wide; -vswprintf (w:count(a1):null-if-zero(a1), int, r:str, other) \ - -> int printf(2,3) chk(__vswprintf_chk: 0,1,-1,-1,2,3) wide; -wscanf (r:str, ...) -> int scanf(0,1) \ - chk(__isoc99_wscanf: 0,1) chk(__isoc23_wscanf: 0,1) wide; -fwscanf (rw, r:str, ...) -> int scanf(1,2) \ - chk(__isoc99_fwscanf: 0,1,2) chk(__isoc23_fwscanf: 0,1,2) wide; -swscanf (r:str, r:str, ...) -> int scanf(1,2) \ - chk(__isoc99_swscanf: 0,1,2) chk(__isoc23_swscanf: 0,1,2) wide; -vwscanf (r:str, other) -> int scanf(0,1) \ - chk(__isoc99_vwscanf: 0,1) chk(__isoc23_vwscanf: 0,1) wide; -vfwscanf (rw, r:str, other) -> int scanf(1,2) \ - chk(__isoc99_vfwscanf: 0,1,2) chk(__isoc23_vfwscanf: 0,1,2) wide; -vswscanf (r:str, r:str, other) -> int scanf(1,2) \ - chk(__isoc99_vswscanf: 0,1,2) chk(__isoc23_vswscanf: 0,1,2) wide; -wcsftime (w:count(a1):null-if-zero(a1), int, r:str, r) -> int reads(environ) \ + chk(__swprintf_chk:0,1,_,size,2,3) wide; +vwprintf (r:str, other) -> int printf(0,1) chk(__vwprintf_chk:_,0,1) wide; +vfwprintf (rw, r:str, other) -> int printf(1,2) chk(__vfwprintf_chk:0,_,1,2) \ wide; -open_wmemstream ( \ - w:null-ok:escape:out(fresh(free):null-on-failure:no-zero-init), \ - w:null-ok:escape) -> fresh(fclose):null-on-failure wide; +vswprintf (w:count(a1):null-if-zero(a1), int, r:str, other) -> int \ + printf(2,3) chk(__vswprintf_chk:0,1,_,size,2,3) wide; +wscanf (r:str, ...) -> int scanf(0,1) chk(__isoc99_wscanf:0,1) \ + chk(__isoc23_wscanf:0,1) wide; +fwscanf (rw, r:str, ...) -> int scanf(1,2) chk(__isoc99_fwscanf:0,1,2) \ + chk(__isoc23_fwscanf:0,1,2) wide; +swscanf (r:str, r:str, ...) -> int scanf(1,2) chk(__isoc99_swscanf:0,1,2) \ + chk(__isoc23_swscanf:0,1,2) wide; +vwscanf (r:str, other) -> int scanf(0,1) chk(__isoc99_vwscanf:0,1) \ + chk(__isoc23_vwscanf:0,1) wide; +vfwscanf (rw, r:str, other) -> int scanf(1,2) chk(__isoc99_vfwscanf:0,1,2) \ + chk(__isoc23_vfwscanf:0,1,2) wide; +vswscanf (r:str, r:str, other) -> int scanf(1,2) chk(__isoc99_vswscanf:0,1,2) \ + chk(__isoc23_vswscanf:0,1,2) wide; +wcsftime (w:count(a1):null-if-zero(a1), int, r:str, r) -> int wide; +open_wmemstream (w:null-ok:bytes(8), w:null-ok) -> ptr wide; -header wctype.h; wctype (r:str) -> int; wctrans (r:str) -> int; -header uchar.h; mbrtoc8 (w:null-ok, r:bytes(min(a2,strlen(a1)+1)):null-ok, int, rw:null-ok) \ -> int; -mbrtoc16 (w:null-ok, r:bytes(min(a2,strlen(a1)+1)):null-ok, int, \ - rw:null-ok) -> int; -mbrtoc32 (w:null-ok, r:bytes(min(a2,strlen(a1)+1)):null-ok, int, \ - rw:null-ok) -> int; -c8rtomb (w:bytes(MB_CUR_MAX):null-ok, int, rw:null-ok) -> int; -c16rtomb (w:bytes(MB_CUR_MAX):null-ok, int, rw:null-ok) -> int; -c32rtomb (w:bytes(MB_CUR_MAX):null-ok, int, rw:null-ok) -> int; +mbrtoc16 (w:null-ok, r:bytes(min(a2,strlen(a1)+1)):null-ok, int, rw:null-ok) \ + -> int; +mbrtoc32 (w:null-ok, r:bytes(min(a2,strlen(a1)+1)):null-ok, int, rw:null-ok) \ + -> int; +c8rtomb (w:null-ok, int, rw:null-ok) -> int; +c16rtomb (w:null-ok, int, rw:null-ok) -> int; +c32rtomb (w:null-ok, int, rw:null-ok) -> int; #============================================================================ # , , #============================================================================ -# Scalar functions have no pointer effects; they are listed so that their -# __builtin_ forms are known. -header math.h; -fabs (other) -> int; -fabsf (other) -> int; -fabsl (other) -> int; -floor (other) -> int; -floorf (other) -> int; -floorl (other) -> int; -ceil (other) -> int; -ceilf (other) -> int; -ceill (other) -> int; -trunc (other) -> int; -truncf (other) -> int; -truncl (other) -> int; frexp (other, w) -> int; frexpf (other, w) -> int; frexpl (other, w) -> int; @@ -1710,163 +1026,14 @@ sincosf (other, w, w) -> void; sincosl (other, w, w) -> void; lgamma_r (other, w) -> int; -# errno is a per-thread location: glibc and musl, macOS. -header errno.h; -__errno_location () -> static(errno):nonnull; -__error () -> static(errno):nonnull; - -# glibc's classification tables; the table pointers they return point into -# the middle of their tables, which no row can describe. -header ctype.h; -__ctype_b_loc () -> static(ctype):nonnull; -__ctype_tolower_loc () -> static(ctype):nonnull; -__ctype_toupper_loc () -> static(ctype):nonnull; - #============================================================================ -# Compiler builtins +# Assertions #============================================================================ -# Every entry above also answers to __builtin_NAME (__builtin_memcpy, -# __builtin_alloca, __builtin_setjmp). These are the builtins with no library -# counterpart whose arguments are pointers or which change control flow. -# Their value semantics (__builtin_expect's result is its argument, the -# overflow builtins' flag) are the engine's, keyed by Clang builtin id; the -# rows give their memory effects. - -builtins; -__builtin_object_size (none:null-ok, int) -> int; -__builtin_dynamic_object_size (none:null-ok, int) -> int; -__builtin_expect (int, int) -> int; -__builtin_expect_with_probability (int, int, other) -> int; -__builtin_unpredictable (int) -> int; -__builtin_assume (int) -> void; -__builtin_assume_aligned (none:null-ok, int, ...) -> arg(0); -__builtin_launder (none:null-ok) -> arg(0); -__builtin_prefetch (none:null-ok, ...) -> void; -__builtin_unreachable () -> noreturn; -__builtin_trap () -> noreturn exits; -__builtin_verbose_trap (r:str, r:str) -> noreturn exits; -__builtin_debugtrap () -> void; -# va_list is an array on some targets and a pointer on others: 'other'. -__builtin_va_start (other, ...) -> void; -__builtin_c23_va_start (other, ...) -> void; -__builtin_va_end (other) -> void; -__builtin_va_copy (other, other) -> void; -__builtin_frame_address (int) -> ptr:null-ok; -__builtin_return_address (int) -> ptr:null-ok; -__builtin_extract_return_addr (none:null-ok) -> arg(0); -__builtin___clear_cache (none, none) -> void; -__builtin_memcpy_inline (w:bytes(a2):null-if-zero(a2), \ - r:bytes(a2):null-if-zero(a2), int) -> void \ - disjoint(0,1,a2) copies(0,1,a2); -__builtin_memset_inline (w:bytes(a2):null-if-zero(a2), int, int) -> void \ - fills(0,a1,a2); -__builtin_alloca_with_align (int, int) -> \ - fresh(stack):extent(a0):nonnull:zero-init; -__builtin_alloca_uninitialized (int) -> \ - fresh(stack):extent(a0):nonnull:no-zero-init; -__builtin_alloca_with_align_uninitialized (int, int) -> \ - fresh(stack):extent(a0):nonnull:no-zero-init; -__builtin_add_overflow (other, other, w) -> int; -__builtin_sub_overflow (other, other, w) -> int; -__builtin_mul_overflow (other, other, w) -> int; -__builtin_sadd_overflow (int, int, w) -> int; -__builtin_saddl_overflow (int, int, w) -> int; -__builtin_saddll_overflow (int, int, w) -> int; -__builtin_uadd_overflow (int, int, w) -> int; -__builtin_uaddl_overflow (int, int, w) -> int; -__builtin_uaddll_overflow (int, int, w) -> int; -__builtin_ssub_overflow (int, int, w) -> int; -__builtin_ssubl_overflow (int, int, w) -> int; -__builtin_ssubll_overflow (int, int, w) -> int; -__builtin_usub_overflow (int, int, w) -> int; -__builtin_usubl_overflow (int, int, w) -> int; -__builtin_usubll_overflow (int, int, w) -> int; -__builtin_smul_overflow (int, int, w) -> int; -__builtin_smull_overflow (int, int, w) -> int; -__builtin_smulll_overflow (int, int, w) -> int; -__builtin_umul_overflow (int, int, w) -> int; -__builtin_umull_overflow (int, int, w) -> int; -__builtin_umulll_overflow (int, int, w) -> int; -__builtin_bswap16 (int) -> int; -__builtin_bswap32 (int) -> int; -__builtin_bswap64 (int) -> int; -__atomic_thread_fence (int) -> void; -__atomic_signal_fence (int) -> void; -__atomic_is_lock_free (int, none:null-ok) -> int; -__atomic_always_lock_free (int, none:null-ok) -> int; -__c11_atomic_thread_fence (int) -> void; -__c11_atomic_signal_fence (int) -> void; -__c11_atomic_is_lock_free (int) -> int; -# Sema resolves each __sync builtin to its sized form (_1 to _16). -__sync_fetch_and_add (rw, int, ...) -> int \ - chk(__sync_fetch_and_add_1: 0,1) chk(__sync_fetch_and_add_2: 0,1) \ - chk(__sync_fetch_and_add_4: 0,1) chk(__sync_fetch_and_add_8: 0,1) \ - chk(__sync_fetch_and_add_16: 0,1); -__sync_fetch_and_sub (rw, int, ...) -> int \ - chk(__sync_fetch_and_sub_1: 0,1) chk(__sync_fetch_and_sub_2: 0,1) \ - chk(__sync_fetch_and_sub_4: 0,1) chk(__sync_fetch_and_sub_8: 0,1) \ - chk(__sync_fetch_and_sub_16: 0,1); -__sync_fetch_and_or (rw, int, ...) -> int \ - chk(__sync_fetch_and_or_1: 0,1) chk(__sync_fetch_and_or_2: 0,1) \ - chk(__sync_fetch_and_or_4: 0,1) chk(__sync_fetch_and_or_8: 0,1) \ - chk(__sync_fetch_and_or_16: 0,1); -__sync_fetch_and_and (rw, int, ...) -> int \ - chk(__sync_fetch_and_and_1: 0,1) chk(__sync_fetch_and_and_2: 0,1) \ - chk(__sync_fetch_and_and_4: 0,1) chk(__sync_fetch_and_and_8: 0,1) \ - chk(__sync_fetch_and_and_16: 0,1); -__sync_fetch_and_xor (rw, int, ...) -> int \ - chk(__sync_fetch_and_xor_1: 0,1) chk(__sync_fetch_and_xor_2: 0,1) \ - chk(__sync_fetch_and_xor_4: 0,1) chk(__sync_fetch_and_xor_8: 0,1) \ - chk(__sync_fetch_and_xor_16: 0,1); -__sync_add_and_fetch (rw, int, ...) -> int \ - chk(__sync_add_and_fetch_1: 0,1) chk(__sync_add_and_fetch_2: 0,1) \ - chk(__sync_add_and_fetch_4: 0,1) chk(__sync_add_and_fetch_8: 0,1) \ - chk(__sync_add_and_fetch_16: 0,1); -__sync_sub_and_fetch (rw, int, ...) -> int \ - chk(__sync_sub_and_fetch_1: 0,1) chk(__sync_sub_and_fetch_2: 0,1) \ - chk(__sync_sub_and_fetch_4: 0,1) chk(__sync_sub_and_fetch_8: 0,1) \ - chk(__sync_sub_and_fetch_16: 0,1); -__sync_or_and_fetch (rw, int, ...) -> int \ - chk(__sync_or_and_fetch_1: 0,1) chk(__sync_or_and_fetch_2: 0,1) \ - chk(__sync_or_and_fetch_4: 0,1) chk(__sync_or_and_fetch_8: 0,1) \ - chk(__sync_or_and_fetch_16: 0,1); -__sync_and_and_fetch (rw, int, ...) -> int \ - chk(__sync_and_and_fetch_1: 0,1) chk(__sync_and_and_fetch_2: 0,1) \ - chk(__sync_and_and_fetch_4: 0,1) chk(__sync_and_and_fetch_8: 0,1) \ - chk(__sync_and_and_fetch_16: 0,1); -__sync_xor_and_fetch (rw, int, ...) -> int \ - chk(__sync_xor_and_fetch_1: 0,1) chk(__sync_xor_and_fetch_2: 0,1) \ - chk(__sync_xor_and_fetch_4: 0,1) chk(__sync_xor_and_fetch_8: 0,1) \ - chk(__sync_xor_and_fetch_16: 0,1); -__sync_bool_compare_and_swap (rw, int, int, ...) -> int \ - chk(__sync_bool_compare_and_swap_1: 0,1,2) \ - chk(__sync_bool_compare_and_swap_2: 0,1,2) \ - chk(__sync_bool_compare_and_swap_4: 0,1,2) \ - chk(__sync_bool_compare_and_swap_8: 0,1,2) \ - chk(__sync_bool_compare_and_swap_16: 0,1,2); -__sync_val_compare_and_swap (rw, int, int, ...) -> int \ - chk(__sync_val_compare_and_swap_1: 0,1,2) \ - chk(__sync_val_compare_and_swap_2: 0,1,2) \ - chk(__sync_val_compare_and_swap_4: 0,1,2) \ - chk(__sync_val_compare_and_swap_8: 0,1,2) \ - chk(__sync_val_compare_and_swap_16: 0,1,2); -__sync_lock_test_and_set (rw, int, ...) -> int \ - chk(__sync_lock_test_and_set_1: 0,1) \ - chk(__sync_lock_test_and_set_2: 0,1) \ - chk(__sync_lock_test_and_set_4: 0,1) \ - chk(__sync_lock_test_and_set_8: 0,1) \ - chk(__sync_lock_test_and_set_16: 0,1); -__sync_lock_release (w, ...) -> void \ - chk(__sync_lock_release_1: 0) chk(__sync_lock_release_2: 0) \ - chk(__sync_lock_release_4: 0) chk(__sync_lock_release_8: 0) \ - chk(__sync_lock_release_16: 0); -__sync_synchronize () -> void; # The functions assert() calls when an assertion fails: glibc and musl, # macOS. They abort the process. -header assert.h; -__assert_fail (r:str, r:str, int, r:str:null-ok) -> noreturn exits; -__assert_perror_fail (int, r:str, int, r:str:null-ok) -> noreturn exits; -__assert_rtn (r:str:null-ok, r:str, int, r:str) -> noreturn exits; -__assert (r:str, r:str, int) -> noreturn exits; +__assert_fail (r:str, r:str, int, r:str:null-ok) -> void; +__assert_perror_fail (int, r:str, int, r:str:null-ok) -> void; +__assert_rtn (r:str:null-ok, r:str, int, r:str) -> void; +__assert (r:str, r:str, int) -> void; diff --git a/lib/Core/Path.cpp b/lib/Core/Path.cpp deleted file mode 100644 index c545de60..00000000 --- a/lib/Core/Path.cpp +++ /dev/null @@ -1,84 +0,0 @@ -//===- Path.cpp - Places relative to a function's interface ---------------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// - -#include "weavec/Core/Path.h" - -#include - -namespace weavec::core { - -SummaryPath SummaryPath::deref() const { - SummaryPath result = *this; - result.steps.pushBack(PathElem{.step = PathStep::Deref, .field = {}}); - return result; -} - -SummaryPath SummaryPath::field(std::string_view name) const { - SummaryPath result = *this; - result.steps.pushBack( - PathElem{.step = PathStep::Field, .field = std::string(name)}); - return result; -} - -SummaryPath SummaryPath::indexed(std::string_view selector) const { - // Indexing every element of an index or a dereference collapses onto it. - if (selector.empty() && !steps.empty() && - ((steps.back().step == PathStep::Index && steps.back().field.empty()) || - steps.back().step == PathStep::Deref)) - return *this; - SummaryPath result = *this; - result.steps.pushBack( - PathElem{.step = PathStep::Index, .field = std::string(selector)}); - return result; -} - -bool SummaryPath::isProperPrefixOf(const SummaryPath &other) const { - if (root != other.root || index != other.index || - steps.size() >= other.steps.size()) - return false; - for (std::size_t i = 0; i < steps.size(); ++i) { - if (steps[i] != other.steps[i]) - return false; - } - return true; -} - -std::string SummaryPath::toString(std::string_view rootName) const { - std::string name(rootName); - std::size_t i = 0; - while (i < steps.size()) { - switch (steps[i].step) { - case PathStep::Deref: - if (i + 1 < steps.size() && steps[i + 1].step == PathStep::Index && - !steps[i + 1].field.empty()) { - name += "[" + steps[i + 1].field + "]"; - i += 2; - continue; - } - // `(*p).f` is spelled `p->f`; a trailing or non-field-followed deref - // is spelled `*p`. - if (i + 1 < steps.size() && steps[i + 1].step == PathStep::Field) { - name += "->" + steps[i + 1].field; - i += 2; - continue; - } - name.insert(0, 1, '*'); - break; - case PathStep::Field: - name += "." + steps[i].field; - break; - case PathStep::Index: - name += "[" + (steps[i].field.empty() ? "*" : steps[i].field) + "]"; - break; - } - ++i; - } - return name; -} - -} // namespace weavec::core diff --git a/lib/Core/PointerKind.cpp b/lib/Core/PointerKind.cpp deleted file mode 100644 index 9eb097c3..00000000 --- a/lib/Core/PointerKind.cpp +++ /dev/null @@ -1,377 +0,0 @@ -//===- PointerKind.cpp - The pointer-kind lattice (RFC 0030) --------------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// - -#include "weavec/Core/PointerKind.h" - -#include -#include -#include -#include -#include - -namespace weavec::core { - -std::string_view toString(PointerShape shape) noexcept { - switch (shape) { - case PointerShape::Single: - return "single"; - case PointerShape::Counted: - return "counted"; - case PointerShape::Sized: - return "sized"; - case PointerShape::EndedBy: - return "ended-by"; - case PointerShape::NulTerminated: - return "nul-terminated"; - case PointerShape::Unknown: - return "unknown"; - } - return ""; -} - -std::string_view toString(Nullability nullability) noexcept { - switch (nullability) { - case Nullability::Nonnull: - return "nonnull"; - case Nullability::Nullable: - return "nullable"; - } - return ""; -} - -std::string_view toString(KindSource source) noexcept { - switch (source) { - case KindSource::Declared: - return "declared"; - case KindSource::Inferred: - return "inferred"; - case KindSource::Default: - return "default"; - } - return ""; -} - -std::string_view toString(ExtentClass extentClass) noexcept { - switch (extentClass) { - case ExtentClass::Exact: - return "exact"; - case ExtentClass::Declared: - return "declared"; - case ExtentClass::LowerBound: - return "lower-bound"; - } - return ""; -} - -std::optional parseNullability(std::string_view text) { - if (text == "nonnull") - return Nullability::Nonnull; - if (text == "nullable") - return Nullability::Nullable; - return std::nullopt; -} - -std::optional parseKindSource(std::string_view text) { - if (text == "declared") - return KindSource::Declared; - if (text == "inferred") - return KindSource::Inferred; - if (text == "default") - return KindSource::Default; - return std::nullopt; -} - -//===----------------------------------------------------------------------===// -// Paths and terms -//===----------------------------------------------------------------------===// - -template -static bool parseInteger(std::string_view text, Integer &value) { - if (text.empty()) - return false; - const auto [end, ec] = - std::from_chars(text.data(), text.data() + text.size(), value); - return ec == std::errc() && end == text.data() + text.size(); -} - -static bool isIdentifier(std::string_view text) noexcept { - if (text.empty()) - return false; - const auto isStart = [](char c) { - return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'; - }; - const auto isContinue = [&](char c) { - return isStart(c) || (c >= '0' && c <= '9'); - }; - return isStart(text.front()) && - std::all_of(text.begin() + 1, text.end(), isContinue); -} - -/// Splits on single spaces; an empty token (a doubled, leading or trailing -/// space) makes the whole text malformed. -static std::optional> -splitTokens(std::string_view text) { - std::vector tokens; - std::size_t start = 0; - while (true) { - const std::size_t space = text.find(' ', start); - const std::string_view token = text.substr( - start, space == std::string_view::npos ? std::string_view::npos - : space - start); - if (token.empty()) - return std::nullopt; - tokens.push_back(token); - if (space == std::string_view::npos) - return tokens; - start = space + 1; - } -} - -ExtentPath ExtentPath::ofParam(std::uint32_t index) { - return ExtentPath{.root = Root::Param, .param = index}; -} - -ExtentPath ExtentPath::ofField(std::string name) { - return ExtentPath{.root = Root::Field, .field = std::move(name)}; -} - -std::string ExtentPath::toString() const { - if (root == Root::Param) - return "param " + std::to_string(param); - return "." + field; -} - -/// Parses a path from its tokens: `param` and an index, or `.`. -static std::optional -parsePathTokens(std::span tokens) { - if (tokens.size() == 2 && tokens[0] == "param") { - std::uint32_t index = 0; - if (!parseInteger(tokens[1], index)) - return std::nullopt; - return ExtentPath::ofParam(index); - } - if (tokens.size() == 1 && tokens[0].size() > 1 && tokens[0].front() == '.' && - isIdentifier(tokens[0].substr(1))) - return ExtentPath::ofField(std::string(tokens[0].substr(1))); - return std::nullopt; -} - -ExtentTerm ExtentTerm::constant(std::int64_t value) { - return ExtentTerm{.path = std::nullopt, .scale = 1, .offset = value}; -} - -ExtentTerm ExtentTerm::of(ExtentPath path, std::int64_t scale, - std::int64_t offset) { - return ExtentTerm{.path = std::move(path), .scale = scale, .offset = offset}; -} - -std::string ExtentTerm::toString() const { - if (!path) - return std::to_string(offset); - return path->toString() + " scale " + std::to_string(scale) + " plus " + - std::to_string(offset); -} - -std::optional ExtentTerm::parse(std::string_view text) { - const auto tokens = splitTokens(text); - if (!tokens) - return std::nullopt; - const std::span all(*tokens); - std::int64_t value = 0; - if (all.size() == 1 && parseInteger(all[0], value)) - return constant(value); - // ` scale plus `: the path is everything before `scale`. - if (all.size() >= 5 && all[all.size() - 4] == "scale" && - all[all.size() - 2] == "plus") { - std::int64_t scale = 0; - std::int64_t offset = 0; - const auto path = parsePathTokens(all.first(all.size() - 4)); - if (!path || !parseInteger(all[all.size() - 3], scale) || - !parseInteger(all[all.size() - 1], offset)) - return std::nullopt; - return of(*path, scale, offset); - } - if (const auto path = parsePathTokens(all)) - return of(*path); - return std::nullopt; -} - -bool operator==(const ExtentTerm &a, const ExtentTerm &b) noexcept { - if (a.path != b.path || a.offset != b.offset) - return false; - return !a.path || a.scale == b.scale; -} - -//===----------------------------------------------------------------------===// -// Kinds -//===----------------------------------------------------------------------===// - -PointerKind PointerKind::single(Nullability nullability, KindSource source) { - return PointerKind{.shape = PointerShape::Single, - .nullability = nullability, - .source = source}; -} - -PointerKind PointerKind::counted(ExtentTerm count, Nullability nullability, - KindSource source) { - return PointerKind{.shape = PointerShape::Counted, - .extent = std::move(count), - .nullability = nullability, - .source = source}; -} - -PointerKind PointerKind::sized(ExtentTerm bytes, Nullability nullability, - KindSource source) { - return PointerKind{.shape = PointerShape::Sized, - .extent = std::move(bytes), - .nullability = nullability, - .source = source}; -} - -PointerKind PointerKind::endedBy(ExtentPath end, std::int64_t offset, - Nullability nullability, KindSource source) { - return PointerKind{.shape = PointerShape::EndedBy, - .extent = ExtentTerm::of(std::move(end), 1, offset), - .nullability = nullability, - .source = source}; -} - -PointerKind PointerKind::nulTerminated(Nullability nullability, - KindSource source) { - return PointerKind{.shape = PointerShape::NulTerminated, - .nullability = nullability, - .source = source}; -} - -PointerKind PointerKind::unknown(Nullability nullability, KindSource source) { - return PointerKind{.shape = PointerShape::Unknown, - .nullability = nullability, - .source = source}; -} - -bool PointerKind::sameShape(const PointerKind &other) const noexcept { - return shape == other.shape && (!hasExtent(shape) || extent == other.extent); -} - -std::string PointerKind::toString() const { - std::string text(core::toString(shape)); - if (hasExtent(shape)) { - text += '('; - if (shape == PointerShape::EndedBy && extent.path && extent.scale == 1 && - extent.offset == 0) - text += extent.path->toString(); - else - text += extent.toString(); - text += ')'; - } - text += ' '; - text += core::toString(nullability); - return text; -} - -std::optional PointerKind::parse(std::string_view text, - KindSource source) { - const std::size_t space = text.rfind(' '); - if (space == std::string_view::npos) - return std::nullopt; - const auto nullability = parseNullability(text.substr(space + 1)); - if (!nullability) - return std::nullopt; - const std::string_view shape = text.substr(0, space); - if (shape == "single") - return single(*nullability, source); - if (shape == "nul-terminated") - return nulTerminated(*nullability, source); - if (shape == "unknown") - return unknown(*nullability, source); - const std::size_t open = shape.find('('); - if (open == std::string_view::npos || shape.back() != ')') - return std::nullopt; - const std::string_view name = shape.substr(0, open); - const auto term = - ExtentTerm::parse(shape.substr(open + 1, shape.size() - open - 2)); - if (!term) - return std::nullopt; - if (name == "counted") - return counted(*term, *nullability, source); - if (name == "sized") - return sized(*term, *nullability, source); - if (name == "ended-by" && term->path) - return PointerKind{.shape = PointerShape::EndedBy, - .extent = *term, - .nullability = *nullability, - .source = source}; - return std::nullopt; -} - -bool operator==(const PointerKind &a, const PointerKind &b) noexcept { - return a.sameShape(b) && a.nullability == b.nullability && - a.source == b.source; -} - -//===----------------------------------------------------------------------===// -// Join and conjunction -//===----------------------------------------------------------------------===// - -/// Default < Inferred < Declared. -static int sourceStrength(KindSource source) noexcept { - switch (source) { - case KindSource::Default: - return 0; - case KindSource::Inferred: - return 1; - case KindSource::Declared: - return 2; - } - return 0; -} - -KindSource join(KindSource a, KindSource b) noexcept { - return sourceStrength(a) <= sourceStrength(b) ? a : b; -} - -PointerKind join(const PointerKind &a, const PointerKind &b, - const JoinFacts &facts) { - PointerKind result = PointerKind::unknown(join(a.nullability, b.nullability), - join(a.source, b.source)); - if (a.sameShape(b)) { - result.shape = a.shape; - result.extent = a.extent; - return result; - } - const auto atLeastOne = [&](const ExtentTerm &term) { - return (term.isConstant() && term.offset >= 1) || - (facts.provesAtLeastOne && facts.provesAtLeastOne(term)); - }; - const bool singleAndCounted = - (a.shape == PointerShape::Single && b.shape == PointerShape::Counted && - atLeastOne(b.extent)) || - (b.shape == PointerShape::Single && a.shape == PointerShape::Counted && - atLeastOne(a.extent)); - if (singleAndCounted) - result.shape = PointerShape::Single; - return result; -} - -std::optional extentClassOf(const PointerKind &kind) noexcept { - switch (kind.shape) { - case PointerShape::Unknown: - case PointerShape::NulTerminated: - return std::nullopt; - case PointerShape::Single: - return ExtentClass::LowerBound; - case PointerShape::Counted: - case PointerShape::Sized: - case PointerShape::EndedBy: - return kind.source == KindSource::Declared ? ExtentClass::Declared - : ExtentClass::LowerBound; - } - return std::nullopt; -} - -} // namespace weavec::core diff --git a/lib/Core/Scc.cpp b/lib/Core/Scc.cpp deleted file mode 100644 index 185b18c9..00000000 --- a/lib/Core/Scc.cpp +++ /dev/null @@ -1,100 +0,0 @@ -//===- Scc.cpp - Strongly connected components ----------------------------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// - -#include "weavec/Core/Scc.h" - -#include -#include -#include - -namespace weavec::core { - -namespace { - -/// Iterative Tarjan: emits components in reverse topological order of the -/// condensation. -class SccFinder { -public: - explicit SccFinder(const std::vector> &adjacency) - : graph(adjacency), index(adjacency.size(), Unvisited), - low(adjacency.size(), 0), onStack(adjacency.size(), false) {} - - std::vector> run() { - for (unsigned root = 0; root < graph.size(); ++root) { - if (index[root] == Unvisited) - visit(root); - } - return components; - } - -private: - static constexpr unsigned Unvisited = ~0U; - - const std::vector> &graph; - std::vector index; - std::vector low; - std::vector onStack; - std::vector stack; - std::vector> components; - unsigned counter = 0; - - void visit(unsigned root) { - struct Frame { - unsigned node; - std::size_t nextEdge; - }; - std::vector frames{Frame{.node = root, .nextEdge = 0}}; - enter(root); - while (!frames.empty()) { - Frame &frame = frames.back(); - const unsigned node = frame.node; - if (frame.nextEdge < graph[node].size()) { - const unsigned succ = graph[node][frame.nextEdge++]; - if (index[succ] == Unvisited) { - enter(succ); - frames.push_back(Frame{.node = succ, .nextEdge = 0}); - } else if (onStack[succ]) { - low[node] = std::min(low[node], index[succ]); - } - continue; - } - if (low[node] == index[node]) { - std::vector component; - unsigned member = 0; - do { - member = stack.back(); - stack.pop_back(); - onStack[member] = false; - component.push_back(member); - } while (member != node); - std::ranges::sort(component); - components.push_back(std::move(component)); - } - frames.pop_back(); - if (!frames.empty()) { - const unsigned parent = frames.back().node; - low[parent] = std::min(low[parent], low[node]); - } - } - } - - void enter(unsigned node) { - index[node] = low[node] = counter++; - stack.push_back(node); - onStack[node] = true; - } -}; - -} // namespace - -std::vector> stronglyConnectedComponents( - const std::vector> &adjacency) { - return SccFinder(adjacency).run(); -} - -} // namespace weavec::core diff --git a/lib/Core/SourceLocation.cpp b/lib/Core/SourceLocation.cpp deleted file mode 100644 index 077bf4b7..00000000 --- a/lib/Core/SourceLocation.cpp +++ /dev/null @@ -1,24 +0,0 @@ -//===- SourceLocation.cpp - Frontend-neutral source positions -------------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// - -#include "weavec/Core/SourceLocation.h" - -namespace weavec::core { - -std::string SourceLocation::toString() const { - if (!isValid()) - return ""; - std::string result = file.empty() ? std::string("") : file; - result += ':'; - result += std::to_string(line); - result += ':'; - result += std::to_string(column); - return result; -} - -} // namespace weavec::core diff --git a/lib/Core/Zone.cpp b/lib/Core/Zone.cpp deleted file mode 100644 index 0aae963a..00000000 --- a/lib/Core/Zone.cpp +++ /dev/null @@ -1,751 +0,0 @@ -//===- Zone.cpp - Difference-bound constraints over symbols ---------------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// - -#include "weavec/Core/Zone.h" - -#include -#include -#include -#include - -namespace weavec::core { - -/// `a + b`, or none when it does not fit (a dropped bound only forgets). -static std::optional addBound(std::int64_t a, std::int64_t b) { - __int128 sum = static_cast<__int128>(a) + static_cast<__int128>(b); - if (sum > INT64_MAX || sum < INT64_MIN) - return std::nullopt; - return static_cast(sum); -} - -std::optional Zone::stored(Sym x, Sym y) const { - const auto *row = rows.find(x); - if (row == nullptr) - return std::nullopt; - const std::int64_t *c = row->find(y); - if (c == nullptr) - return std::nullopt; - return *c; -} - -std::optional Zone::implied(Sym x, Sym y) const { - if (x == ZeroSym || y == ZeroSym) - return std::nullopt; - auto ux = stored(x, ZeroSym); - auto ly = stored(ZeroSym, y); - if (!ux || !ly) - return std::nullopt; - auto sum = addBound(*ux, *ly); - if (!sum || *sum >= LooseRelation) - return std::nullopt; - return sum; -} - -std::optional Zone::bound(Sym x, Sym y) const { - if (bottom) - return INT64_MIN; - if (x == y) - return 0; - auto own = stored(x, y); - auto zero = implied(x, y); - if (own && zero) - return std::min(*own, *zero); - return own ? own : zero; -} - -void Zone::noteAdded(Sym x, Sym y) { - if (x == ZeroSym || y == ZeroSym) - return; - ++degrees.at(x); - ++degrees.at(y); -} - -void Zone::noteRemoved(Sym x, Sym y) { - if (x == ZeroSym || y == ZeroSym) - return; - for (Sym sym : {x, y}) - if (const std::uint32_t *count = degrees.find(sym)) { - if (*count <= 1) - degrees.erase(sym); - else - degrees.set(sym, *count - 1); - } -} - -void Zone::setRaw(Sym x, Sym y, std::int64_t c) { - if (x == y) - return; - const auto *row = rows.find(x); - const bool existed = row != nullptr && row->contains(y); - rows.at(x).set(y, c); - if (!existed) - noteAdded(x, y); -} - -void Zone::tighten(Sym x, Sym y, std::int64_t c) { - if (x == y) { - if (c < 0) - setBottom(); - return; - } - // A relation between two symbols this loose (what their C types' ranges - // give) proves nothing and would relate every symbol to every other: - // forgotten, which is sound. - if (x != ZeroSym && y != ZeroSym && c >= LooseRelation) - return; - // (Nor one their bounds against zero already imply.) - auto current = bound(x, y); - if (!current || c < *current) - setRaw(x, y, c); -} - -bool Zone::addLE(Sym x, Sym y, std::int64_t c) { - return addLimited(x, y, c, true); -} - -bool Zone::addLimited(Sym x, Sym y, std::int64_t c, bool limit) { - if (bottom) - return false; - if (x == y) { - if (c < 0) - setBottom(); - return !bottom; - } - if (auto current = bound(x, y); current && *current <= c) - return true; - // Incremental closure: for every u reaching x and every v reached from y, - // u - v <= (u - x) + c + (y - v). A path through zero on either side is - // what the bounds against zero imply (the zero row and column, updated - // here), so u ranges over zero and the symbols with a stored bound on - // `u - x`, and v over zero and those with one on `y - v`. - std::vector> intoX{{x, 0}}; - std::vector> fromY{{y, 0}}; - if (x != ZeroSym) { - if (auto zx = stored(ZeroSym, x)) - intoX.emplace_back(ZeroSym, *zx); - for (const auto &[u, row] : rows) { - if (u == x || u == ZeroSym) - continue; - if (row.contains(x)) - if (auto b = bound(u, x)) - intoX.emplace_back(u, *b); - } - } - if (y != ZeroSym) - if (const auto *row = rows.find(y)) - for (const auto &[v, b] : *row) - if (v != y) { - auto effective = v == ZeroSym ? std::optional(b) : bound(y, v); - if (effective) - fromY.emplace_back(v, *effective); - } - for (const auto &[u, ux] : intoX) - for (const auto &[v, yv] : fromY) { - auto left = addBound(ux, c); - if (!left) - continue; - auto total = addBound(*left, yv); - if (!total) - continue; - tighten(u, v, *total); - if (bottom) - return false; - } - if (limit) - enforceLimit(); - return true; -} - -bool Zone::addRange(Sym x, std::optional lo, - std::optional hi) { - if (hi && !addLE(x, ZeroSym, *hi)) - return false; - if (lo && *lo != INT64_MIN && !addLE(ZeroSym, x, -*lo)) - return false; - return !bottom; -} - -bool Zone::entails(Sym x, Sym y, std::int64_t c) const { - if (bottom) - return true; - auto b = bound(x, y); - return b && *b <= c; -} - -void Zone::restrictTo(const std::function &keep) { - if (bottom) - return; - bool dropped = !rows.empty() && [&] { - for (const auto &[u, row] : rows) { - if (u != ZeroSym && !keep(u)) - return true; - for (const auto &[y, c] : row) - if (y != ZeroSym && !keep(y)) - return true; - } - return false; - }(); - if (!dropped) - return; - rows.eraseIf([&](Sym u, const PMap &) { - return u != ZeroSym && !keep(u); - }); - std::vector>> changed; - for (const auto &[u, row] : rows) { - PMap kept = row; - kept.eraseIf([&](Sym y, std::int64_t) { return y != ZeroSym && !keep(y); }); - if (kept.size() != row.size()) - changed.emplace_back(u, std::move(kept)); - } - for (auto &[u, row] : changed) - rows.set(u, std::move(row)); - // The size limit's measure, from the relational bounds left. - std::map counts; - for (const auto &[u, row] : rows) - if (u != ZeroSym) - for (const auto &[y, c] : row) - if (y != ZeroSym) { - ++counts[u]; - ++counts[y]; - } - degrees.clear(); - for (const auto &[sym, count] : counts) - degrees.set(sym, count); -} - -std::vector Zone::symbols() const { - std::vector all; - for (const auto &[x, row] : rows) { - all.push_back(x); - for (const auto &[y, c] : row) - all.push_back(y); - } - std::ranges::sort(all); - all.erase(std::ranges::unique(all).begin(), all.end()); - return all; -} - -void Zone::assign(std::vector> entries) { - std::ranges::sort(entries); - std::vector>> built; - std::vector related; - for (std::size_t i = 0; i < entries.size();) { - const Sym x = std::get<0>(entries[i]); - std::vector> row; - for (; i < entries.size() && std::get<0>(entries[i]) == x; ++i) { - const auto &[from, to, c] = entries[i]; - row.emplace_back(to, c); - if (from != ZeroSym && to != ZeroSym) { - related.push_back(from); - related.push_back(to); - } - } - built.emplace_back(x, PMap::fromSorted(std::move(row))); - } - rows = PMap>::fromSorted(std::move(built)); - std::ranges::sort(related); - std::vector> counts; - for (Sym sym : related) - if (!counts.empty() && counts.back().first == sym) - ++counts.back().second; - else - counts.emplace_back(sym, 1); - degrees = PMap::fromSorted(std::move(counts)); -} - -bool Zone::equals(const Zone &other) const { - if (bottom != other.bottom) - return false; - if (bottom) - return true; - if (rows.sharesWith(other.rows)) - return true; - // The bounds against zero, stored alike. - auto unary = [](const Zone &zone) { - std::vector> out; - for (const auto &[x, row] : zone.rows) - for (const auto &[y, c] : row) - if (x == ZeroSym || y == ZeroSym) - out.emplace_back(x, y, c); - return out; - }; - if (unary(*this) != unary(other)) - return false; - // Every relation either stores, as both zones bound it. - auto relationsAgree = [](const Zone &a, const Zone &b) { - for (const auto &[x, row] : a.rows) { - if (x == ZeroSym) - continue; - for (const auto &[y, c] : row) - if (y != ZeroSym && a.bound(x, y) != b.bound(x, y)) - return false; - } - return true; - }; - return relationsAgree(*this, other) && relationsAgree(other, *this); -} - -void Zone::enforceLimit() { - // Keep the symbols in the most relational bounds; the others keep their - // bounds against zero only. - if (degrees.size() <= MaxRelational) - return; - std::vector> order; - order.reserve(degrees.size()); - for (const auto &[sym, bounds] : degrees) - order.emplace_back(bounds, sym); - std::ranges::sort(order); - std::set demote; - const std::size_t keep = MaxRelational - (MaxRelational / 4); - for (std::size_t i = 0; i + keep < order.size(); ++i) - demote.insert(order[i].second); - // One pass over the rows: a demoted symbol's row keeps its bound against - // zero, and every other row drops its bounds on demoted symbols. - std::vector>> changed; - for (const auto &[u, row] : rows) { - bool demoted = demote.contains(u); - bool touched = false; - PMap kept; - for (const auto &[y, c] : row) { - // (Bounds against zero stay: `0 - y` is a lower bound of `y`.) - if (u != ZeroSym && y != ZeroSym && (demoted || demote.contains(y))) { - touched = true; - noteRemoved(u, y); - continue; - } - kept.set(y, c); - } - if (touched) - changed.emplace_back(u, std::move(kept)); - } - for (auto &[u, row] : changed) - rows.set(u, std::move(row)); -} - -/// The closure of `zone` (Floyd-Warshall), the size limit kept once at the -/// end. A path through a symbol with no stored relation goes through zero, -/// which its bounds against zero already give: only zero and the symbols -/// in stored relations take part. -Zone Zone::closure(const Zone &zone, const std::vector &symbols) { - (void)symbols; - Zone out = zone; - if (zone.bottom) - return out; - std::set related{ZeroSym}; - for (const auto &[x, row] : zone.rows) - if (x != ZeroSym) - for (const auto &[y, c] : row) - if (y != ZeroSym) { - related.insert(x); - related.insert(y); - } - if (related.size() <= 1) - return out; - const std::vector nodes(related.begin(), related.end()); - const std::size_t n = nodes.size(); - constexpr std::int64_t None = INT64_MAX; - std::vector d(n * n, None); - for (std::size_t i = 0; i < n; ++i) - for (std::size_t j = 0; j < n; ++j) { - if (i == j) { - d[(i * n) + j] = 0; - } else if (auto c = zone.bound(nodes[i], nodes[j])) { - d[(i * n) + j] = *c; - } - } - for (std::size_t k = 0; k < n; ++k) - for (std::size_t i = 0; i < n; ++i) { - const std::int64_t ik = d[(i * n) + k]; - if (ik == None) - continue; - for (std::size_t j = 0; j < n; ++j) { - const std::int64_t kj = d[(k * n) + j]; - if (kj == None) - continue; - const __int128 sum = static_cast<__int128>(ik) + kj; - if (sum < INT64_MIN || sum >= None) - continue; - std::int64_t &ij = d[(i * n) + j]; - if (ij == None || sum < ij) - ij = static_cast(sum); - } - } - for (std::size_t i = 0; i < n; ++i) - if (d[(i * n) + i] < 0) { - out.setBottom(); - return out; - } - // (Nodes[0] is zero.) The bounds against zero first, then the relations - // they leave something to say. - for (std::size_t i = 1; i < n; ++i) { - if (d[i * n] != None) - out.setRaw(nodes[i], ZeroSym, d[i * n]); - if (d[i] != None) - out.setRaw(ZeroSym, nodes[i], d[i]); - } - for (std::size_t i = 1; i < n; ++i) - for (std::size_t j = 1; j < n; ++j) { - if (i == j || d[(i * n) + j] == None) - continue; - auto zero = out.implied(nodes[i], nodes[j]); - if (d[(i * n) + j] < LooseRelation && (!zero || d[(i * n) + j] < *zero)) - out.setRaw(nodes[i], nodes[j], d[(i * n) + j]); - } - out.enforceLimit(); - return out; -} - -Zone Zone::combine(const Zone &left, const Zone &right, - const std::vector &pairs, bool widen, - const std::vector &thresholds) { - Zone out; - if (left.isBottom() && right.isBottom()) { - out.setBottom(); - return out; - } - // A bottom side contributes nothing: the other side's projection stands. - std::vector all; - all.reserve(pairs.size() + 1); - all.push_back(SymPair{.result = ZeroSym, - .left = ZeroSym, - .right = ZeroSym, - .hasLeft = !left.isBottom(), - .hasRight = !right.isBottom()}); - // (Only a symbol a side's zone bounds, or one several results share on - // a side, `x - x = 0`, can give a bound in the result.) - const std::vector leftSyms = left.symbols(); - const std::vector rightSyms = right.symbols(); - auto sharedOn = [&](bool isLeft) { - std::vector used; - for (const SymPair &pair : pairs) - if (isLeft ? pair.hasLeft : pair.hasRight) - used.push_back(isLeft ? pair.left : pair.right); - std::ranges::sort(used); - std::vector shared; - for (std::size_t i = 1; i < used.size(); ++i) - if (used[i] == used[i - 1] && - (shared.empty() || shared.back() != used[i])) - shared.push_back(used[i]); - return shared; - }; - const std::vector leftShared = sharedOn(true); - const std::vector rightShared = sharedOn(false); - auto bounded = [](const std::vector &syms, - const std::vector &shared, Sym sym) { - return std::ranges::binary_search(syms, sym) || - std::ranges::binary_search(shared, sym); - }; - for (const SymPair &pair : pairs) { - SymPair copy = pair; - copy.hasLeft = copy.hasLeft && !left.isBottom(); - copy.hasRight = copy.hasRight && !right.isBottom(); - if (!(copy.hasLeft && bounded(leftSyms, leftShared, copy.left)) && - !(copy.hasRight && bounded(rightSyms, rightShared, copy.right))) - continue; - all.push_back(copy); - } - // Each side's stored bounds against zero, per entry of `all`. - const std::size_t count = all.size(); - std::vector> upLeft(count); - std::vector> downLeft(count); - std::vector> upRight(count); - std::vector> downRight(count); - for (std::size_t i = 1; i < count; ++i) { - if (all[i].hasLeft) { - upLeft[i] = left.stored(all[i].left, ZeroSym); - downLeft[i] = left.stored(ZeroSym, all[i].left); - } - if (all[i].hasRight) { - upRight[i] = right.stored(all[i].right, ZeroSym); - downRight[i] = right.stored(ZeroSym, all[i].right); - } - } - auto sideBound = [&](bool isLeft, std::size_t ia, - std::size_t ib) -> std::optional { - if (ib == 0) - return isLeft ? upLeft[ia] : upRight[ia]; - if (ia == 0) - return isLeft ? downLeft[ib] : downRight[ib]; - return isLeft ? left.bound(all[ia].left, all[ib].left) - : right.bound(all[ia].right, all[ib].right); - }; - auto nextThreshold = [&](std::int64_t value) -> std::optional { - std::optional best; - for (std::int64_t t : thresholds) - if (t >= value && (!best || t < *best)) - best = t; - return best; - }; - // The result bound on `a - b`, from the sides' (what zero implies - // included). - auto combined = [&](std::size_t ia, - std::size_t ib) -> std::optional { - const SymPair &a = all[ia]; - const SymPair &b = all[ib]; - bool leftApplies = a.hasLeft && b.hasLeft; - bool rightApplies = a.hasRight && b.hasRight; - std::optional lb; - std::optional rb; - if (leftApplies) - lb = sideBound(true, ia, ib); - if (rightApplies) - rb = sideBound(false, ia, ib); - std::optional result; - if (leftApplies && rightApplies) { - if (!widen) { - if (lb && rb) - result = std::max(*lb, *rb); - } else if (lb && rb) { - // A bound between two symbols stops only at -1, 0 or 1 (`i - n` - // around a loop exit); the program's constants are for bounds - // against zero (`i <= 3`), or the relation climbs one constant a - // round. - bool relational = a.result != ZeroSym && b.result != ZeroSym; - if (*rb <= *lb) - result = lb; - else if (!relational) - result = nextThreshold(*rb); - else if (*rb <= -1) - result = -1; - else if (*rb <= 0) - result = 0; - else if (*rb <= 1) - result = 1; - } - } else if (leftApplies || rightApplies) { - // Only one side has both values. A bound against zero, or between - // two symbols that side alone has, constrains nothing the other - // side has; a relation to a symbol both sides have would, through - // the closure, tighten that symbol with one side's facts only. - bool aBoth = a.hasLeft && a.hasRight; - bool bBoth = b.hasLeft && b.hasRight; - bool constantBound = a.result == ZeroSym || b.result == ZeroSym; - if (constantBound || (!aBoth && !bBoth)) - result = leftApplies ? lb : rb; - } - return result; - }; - // Bounds against zero first: whether a relation says more than they do - // depends on them. - std::vector> entries; - std::vector> outUp(count); - std::vector> outDown(count); - for (std::size_t i = 1; i < count; ++i) { - outUp[i] = combined(i, 0); - if (outUp[i]) - entries.emplace_back(all[i].result, ZeroSym, *outUp[i]); - outDown[i] = combined(0, i); - if (outDown[i]) - entries.emplace_back(ZeroSym, all[i].result, *outDown[i]); - } - // Relations: those a side stores, those between results sharing a - // symbol on a side (`x - x = 0`), and those each side's bounds against - // zero imply that the result's do not (both values moving the same way - // between the sides, `i` and `j` counted together); widening, every pair - // bounded on both sides. - using Index = std::vector>; - Index leftBy; - Index rightBy; - for (std::size_t i = 1; i < count; ++i) { - if (all[i].hasLeft) - leftBy.emplace_back(all[i].left, i); - if (all[i].hasRight) - rightBy.emplace_back(all[i].right, i); - } - std::ranges::sort(leftBy); - std::ranges::sort(rightBy); - auto usersOf = [](const Index &by, Sym sym) { - return std::ranges::equal_range( - by, std::make_pair(sym, std::size_t{0}), - [](const auto &x, const auto &y) { return x.first < y.first; }); - }; - std::vector> candidates; - auto collect = [&](const Zone &side, const Index &by) { - for (const auto &[x, row] : side.rows) { - if (x == ZeroSym) - continue; - auto [fromBegin, fromEnd] = usersOf(by, x); - if (fromBegin == fromEnd) - continue; - for (const auto &[y, c] : row) { - if (y == ZeroSym) - continue; - auto [toBegin, toEnd] = usersOf(by, y); - for (auto i = fromBegin; i != fromEnd; ++i) - for (auto j = toBegin; j != toEnd; ++j) - candidates.emplace_back(i->second, j->second); - } - } - for (std::size_t start = 0; start < by.size();) { - std::size_t end = start; - while (end < by.size() && by[end].first == by[start].first) - ++end; - if (end - start > 1) - for (std::size_t i = start; i < end; ++i) - for (std::size_t j = start; j < end; ++j) - candidates.emplace_back(by[i].second, by[j].second); - start = end; - } - }; - if (!left.isBottom()) - collect(left, leftBy); - if (!right.isBottom()) - collect(right, rightBy); - { - auto negated = [](const std::optional &down) - -> std::optional { - if (!down || *down == INT64_MIN) - return std::nullopt; - return -*down; - }; - // (-1, 0, 1: how the upper bound of a, or the lower bound of b, moves - // from the left side to the right; 2 when a side has none.) - std::vector upUp; - std::vector upDown; - std::vector downUp; - std::vector downDown; - // (Widening: bounds that fall or rise the other way.) - std::vector upperFalls; - std::vector lowerRises; - for (std::size_t i = 1; i < count; ++i) { - const SymPair &p = all[i]; - if (!p.hasLeft || !p.hasRight) - continue; - const auto &ul = upLeft[i]; - const auto &ur = upRight[i]; - auto ll = negated(downLeft[i]); - auto lr = negated(downRight[i]); - if (widen) { - // (Widening: a relation neither side stores gives one the result's - // bounds do not imply only when a bound moved, and then only when - // the right side's `upper(a) - lower(b)` is at most 1; `upUp` and - // `downDown` here hold the moving ones, `upDown` and `downUp` every - // bounded one, paired below within that window.) - if (ul && ur) { - upDown.push_back(i); - if (*ur > *ul) - upUp.push_back(i); - if (*ur < *ul) - upperFalls.push_back(i); - } - if (ll && lr) { - downUp.push_back(i); - if (*lr < *ll) - downDown.push_back(i); - if (*lr > *ll) - lowerRises.push_back(i); - } - continue; - } - if (ul && ur && *ur > *ul) - upUp.push_back(i); - if (ul && ur && *ur < *ul) - upDown.push_back(i); - if (ll && lr && *lr > *ll) - downUp.push_back(i); - if (ll && lr && *lr < *ll) - downDown.push_back(i); - } - if (!widen) { - for (std::size_t i : upUp) - for (std::size_t j : downUp) - candidates.emplace_back(i, j); - for (std::size_t i : upDown) - for (std::size_t j : downDown) - candidates.emplace_back(i, j); - } else { - // NOLINTBEGIN(clang-analyzer-core.uninitialized.UndefReturn) - // (the lists above hold only entries bounded on the right side) - auto upperR = [&](std::size_t i) { return *upRight[i]; }; - auto lowerR = [&](std::size_t j) { return *negated(downRight[j]); }; - // NOLINTEND(clang-analyzer-core.uninitialized.UndefReturn) - // Lower bounds (right side) descending: those with `upper(a) - - // lower(b) <= 1` come first. - std::vector byLower = downUp; - std::ranges::sort(byLower, [&](std::size_t x, std::size_t y) { - return lowerR(x) > lowerR(y); - }); - for (std::size_t i : upUp) { - const std::int64_t up = upperR(i); - for (std::size_t j : byLower) { - auto gap = addBound(up, -lowerR(j)); - if (!gap || *gap > 1) - break; - candidates.emplace_back(i, j); - } - } - // A relation the right side keeps as tight as the left's, where both - // bounds moved by as much (then the widened bounds lose it). - for (std::size_t i : upUp) - for (std::size_t j : lowerRises) - candidates.emplace_back(i, j); - for (std::size_t i : upperFalls) - for (std::size_t j : downDown) - candidates.emplace_back(i, j); - // Upper bounds (right side) ascending, for the moving lower bounds. - std::vector byUpper = upDown; - std::ranges::sort(byUpper, [&](std::size_t x, std::size_t y) { - return upperR(x) < upperR(y); - }); - for (std::size_t j : downDown) { - const std::int64_t low = lowerR(j); - for (std::size_t i : byUpper) { - auto gap = addBound(upperR(i), -low); - if (!gap || *gap > 1) - break; - candidates.emplace_back(i, j); - } - } - } - } - std::ranges::sort(candidates); - candidates.erase(std::ranges::unique(candidates).begin(), candidates.end()); - for (const auto &[ia, ib] : candidates) { - if (all[ia].result == all[ib].result) - continue; - auto result = combined(ia, ib); - if (!result || *result >= LooseRelation) - continue; - // (What the result's bounds against zero imply: `Zone::implied`.) - std::optional zero; - if (outUp[ia] && outDown[ib]) - if (auto sum = addBound(*outUp[ia], *outDown[ib]); - sum && *sum < LooseRelation) - zero = sum; - if (!zero || *result < *zero) - entries.emplace_back(all[ia].result, all[ib].result, *result); - } - out.assign(std::move(entries)); - if (!widen) - out = closure(out, {}); - out.enforceLimit(); - return out; -} - -std::string Zone::toString(const std::function &name) const { - if (bottom) - return "bottom"; - std::string out; - auto spell = [&](Sym sym) { - return sym == ZeroSym ? std::string("0") : name(sym); - }; - for (const auto &[x, row] : rows) - for (const auto &[y, c] : row) { - if (!out.empty()) - out += ", "; - if (y == ZeroSym) - out += spell(x) + " <= " + std::to_string(c); - else if (x == ZeroSym) - out += spell(y) + " >= " + std::to_string(-c); - else - out += spell(x) + " - " + spell(y) + " <= " + std::to_string(c); - } - return out; -} - -} // namespace weavec::core diff --git a/lib/Frontend/AnalysisStats.cpp b/lib/Frontend/AnalysisStats.cpp deleted file mode 100644 index 59b9aa1d..00000000 --- a/lib/Frontend/AnalysisStats.cpp +++ /dev/null @@ -1,116 +0,0 @@ -//===- AnalysisStats.cpp - Invocation statistics output (RFC 0020) --------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -#include "weavec/Frontend/AnalysisStats.h" - -#include "llvm/ADT/SmallString.h" -#include "llvm/Support/FileSystem.h" -#include "llvm/Support/raw_ostream.h" - -#include -#include -#include - -namespace weavec::frontend { - -/// A JSON string: valid UTF-8 sequences are kept, quotes and backslashes -/// escaped, and control bytes and invalid UTF-8 written as `\u00XX`. -static std::string jsonString(std::string_view value) { - static constexpr std::string_view Hex = "0123456789abcdef"; - std::string result = "\""; - for (std::size_t i = 0; i < value.size(); ++i) { - const auto byte = static_cast(value[i]); - if (byte >= 0x80) { - unsigned length = 0; - if (byte >= 0xc2 && byte <= 0xdf) - length = 2; - else if (byte >= 0xe0 && byte <= 0xef) - length = 3; - else if (byte >= 0xf0 && byte <= 0xf4) - length = 4; - bool valid = length != 0 && i + length <= value.size(); - for (unsigned j = 1; valid && j < length; ++j) - valid = (static_cast(value[i + j]) & 0xc0U) == 0x80U; - if (valid) { - const auto next = static_cast(value[i + 1]); - valid = (byte != 0xe0 || next >= 0xa0) && - (byte != 0xed || next < 0xa0) && - (byte != 0xf0 || next >= 0x90) && (byte != 0xf4 || next < 0x90); - } - if (valid) { - result.append(value.substr(i, length)); - i += length - 1; - continue; - } - } - if (byte == '"' || byte == '\\') { - result += '\\'; - result += value[i]; - } else if (byte < 0x20 || byte >= 0x80) { - result += "\\u00"; - result += Hex[byte >> 4U]; - result += Hex[byte & 15U]; - } else { - result += value[i]; - } - } - result += '"'; - return result; -} - -bool writeAtomicText(std::string_view path, std::string_view text) { - llvm::SmallString<256> temporary; - int descriptor = -1; - auto error = llvm::sys::fs::createUniqueFile( - std::string(path) + ".tmp-%%%%%%", descriptor, temporary); - if (!error) { - llvm::raw_fd_ostream stream(descriptor, true); - stream << text; - stream.close(); - if (stream.has_error()) { - error = stream.error(); - stream.clear_error(); - } - if (!error) - error = llvm::sys::fs::rename(temporary, path); - } - if (error && !temporary.empty()) - std::ignore = llvm::sys::fs::remove(temporary); - return !error; -} - -bool writeAnalysisStats(std::string_view path, const core::AnalysisStats *stats, - bool final) { - if (path.empty()) - return true; - std::ostringstream out; - out << R"({"version":1,"counters":{)"; - bool first = true; - if (stats) - for (const auto &[name, value] : stats->counters) { - if (!first) - out << ','; - first = false; - out << jsonString(name) << ':' << value; - } - out << "},\"nanoseconds\":{"; - first = true; - if (stats) - for (const auto &[name, value] : stats->nanoseconds) { - if (!first) - out << ','; - first = false; - out << jsonString(name) << ':' << value; - } - out << "},\"final\":" << (final ? "true" : "false") << "}\n"; - if (writeAtomicText(path, out.str())) - return true; - llvm::errs() << "weavec: error: cannot write analysis statistics '" << path - << "'\n"; - return false; -} -} // namespace weavec::frontend diff --git a/lib/Frontend/AnalysisSummary.cpp b/lib/Frontend/AnalysisSummary.cpp deleted file mode 100644 index ff47246f..00000000 --- a/lib/Frontend/AnalysisSummary.cpp +++ /dev/null @@ -1,107 +0,0 @@ -//===- AnalysisSummary.cpp - The analysis's summary lines -----------------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// - -#include "weavec/Frontend/AnalysisSummary.h" - -#include "llvm/ADT/SmallString.h" -#include "llvm/Support/FileSystem.h" -#include "llvm/Support/Path.h" - -#include -#include -#include -#include -#include - -namespace weavec::frontend { - -void applyDiagnosticControl(core::Ledger &ledger, - const DiagnosticControl &control) { - std::vector> moved(ledger.diagnostics.size()); - std::vector shown; - shown.reserve(ledger.diagnostics.size()); - for (std::size_t i = 0; i < ledger.diagnostics.size(); ++i) { - core::LedgerDiagnostic &diagnostic = ledger.diagnostics[i]; - const std::optional adjusted = - control.apply(core::Diagnostic{.severity = diagnostic.severity, - .certainty = diagnostic.certainty, - .id = diagnostic.id, - .message = {}, - .location = {}, - .notes = {}, - .fixits = {}}); - if (!adjusted) - continue; - diagnostic.severity = adjusted->severity; - moved[i] = static_cast(shown.size()); - shown.push_back(std::move(diagnostic)); - } - ledger.diagnostics = std::move(shown); - for (core::UnitLedger &unit : ledger.units) - for (core::FunctionLedger &function : unit.functions) - for (core::Site &site : function.sites) - for (std::optional &record : site.facets) - if (record && record->diagnostic) - record->diagnostic = *record->diagnostic < moved.size() - ? moved[*record->diagnostic] - : std::nullopt; -} - -std::string summaryName(llvm::StringRef source, - llvm::StringRef workingDirectory) { - if (!llvm::sys::path::is_absolute(source)) - return source.str(); - llvm::SmallString<256> cwd(workingDirectory); - if (cwd.empty() && llvm::sys::fs::current_path(cwd)) - return source.str(); - llvm::sys::path::remove_dots(cwd, /*remove_dot_dot=*/true); - llvm::SmallString<256> path(source); - llvm::sys::path::remove_dots(path, /*remove_dot_dot=*/true); - llvm::StringRef prefix = cwd; - if (prefix.size() > 1 && llvm::sys::path::is_separator(prefix.back())) - prefix = prefix.drop_back(); - const llvm::StringRef full = path; - if (full == prefix) - return "."; - if (full.starts_with(prefix) && full.size() > prefix.size() && - llvm::sys::path::is_separator(full[prefix.size()])) - return llvm::sys::path::convert_to_slash( - full.drop_front(prefix.size() + 1)); - return llvm::sys::path::convert_to_slash(full); -} - -/// Writes one summary line to `os`. -static void printLine(const std::string &line, llvm::raw_ostream &os) { - // The summary is one whole line on `os` (stderr), while the analysis dump - // of `--dump-analysis` goes to the buffered `llvm::outs()`. A caller that - // joins the two onto one file descriptor -- `2>&1 | FileCheck`, a - // terminal, a log -- sees two independently buffered streams over one - // descriptor, which interleave wherever a buffer happens to fill rather - // than at a line boundary. Draining the dump stream first fixes the - // order: the whole dump, then the summary. - llvm::outs().flush(); - os << line << '\n'; - os.flush(); -} - -void printUnitSummary(core::Ledger ledger, llvm::StringRef name, - const DiagnosticControl &control, llvm::raw_ostream &os) { - applyDiagnosticControl(ledger, control); - printLine(core::unitSummaryLine(name, core::summarize(ledger)), os); -} - -void printProgramSummary(core::Ledger ledger, llvm::StringRef program, - const DiagnosticControl &control, - llvm::raw_ostream &os) { - applyDiagnosticControl(ledger, control); - printLine(core::programSummaryLine(program, ledger.units.size(), - core::summarize(ledger)), - os); -} - -} // namespace weavec::frontend diff --git a/lib/Frontend/CMakeLists.txt b/lib/Frontend/CMakeLists.txt index f3234c1f..c825dd4a 100644 --- a/lib/Frontend/CMakeLists.txt +++ b/lib/Frontend/CMakeLists.txt @@ -1,15 +1,10 @@ # weavec::Frontend -- Clang FrontendAction / libTooling integration. weavec_add_library( Frontend - SOURCES AnalysisStats.cpp - ClangDiagnosticSink.cpp - DiagnosticControl.cpp - Driver.cpp - FrontendAction.cpp - ProgramAnalysis.cpp + SOURCES Driver.cpp ResourceDir.cpp - # RFC 0035: the guard pass, its frames, globals and library calls, - # the unsafe regions and the enforcement ledger. + # RFC 0035, RFC 0036: the guard passes, their frames, globals and + # library calls, the unsafe regions and the enforcement ledger. EnforcementLedger.cpp GuardExpand.cpp GuardFrames.cpp @@ -19,12 +14,7 @@ weavec_add_library( GuardLibrary.cpp GuardPass.cpp UnsafeRegions.cpp - # RFC 0035 §8: the advisory analysis's summary lines and what - # `weavec --whole-program` checks across units. - AnalysisSummary.cpp - InterfaceFacts.cpp - ProgramChecks.cpp - PUBLIC_DEPS weavec::Analysis + PUBLIC_DEPS weavec::Core USES_LLVM) target_compile_definitions( @@ -47,7 +37,6 @@ weavec_link_llvm( TransformUtils) weavec_link_clang( weavecFrontend - clangTooling clangFrontendTool clangFrontend clangCodeGen diff --git a/lib/Frontend/ClangDiagnosticSink.cpp b/lib/Frontend/ClangDiagnosticSink.cpp deleted file mode 100644 index 908fc64b..00000000 --- a/lib/Frontend/ClangDiagnosticSink.cpp +++ /dev/null @@ -1,74 +0,0 @@ -//===- ClangDiagnosticSink.cpp - Forward core diagnostics to Clang --------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// - -#include "weavec/Frontend/ClangDiagnosticSink.h" - -#include "weavec/Analysis/ClangLocation.h" - -#include "clang/Basic/FileManager.h" -#include "clang/Basic/SourceManager.h" - -namespace weavec::frontend { - -static clang::DiagnosticsEngine::Level toClangLevel(core::Severity severity) { - switch (severity) { - case core::Severity::Note: - return clang::DiagnosticsEngine::Note; - case core::Severity::Warning: - return clang::DiagnosticsEngine::Warning; - case core::Severity::Error: - return clang::DiagnosticsEngine::Error; - } - return clang::DiagnosticsEngine::Error; -} - -void ClangDiagnosticSink::report(const core::Diagnostic &diagnostic) { - emit(diagnostic, /*isNote=*/false); - for (const core::Diagnostic ¬e : diagnostic.notes) - emit(note, /*isNote=*/true); -} - -void ClangDiagnosticSink::emit(const core::Diagnostic &diagnostic, - bool isNote) { - // getCustomDiagID caches per (level, format string), so this is cheap. - // Messages are passed as arguments so '%' in user identifiers is safe. - const clang::DiagnosticsEngine::Level level = - isNote ? clang::DiagnosticsEngine::Note - : toClangLevel(diagnostic.severity); - const unsigned id = isNote ? engine.getCustomDiagID(level, "%0") - : engine.getCustomDiagID(level, "%0 [weavec::%1]"); - - auto location = analysis::toClangLocation(diagnostic.location); - if (location.isInvalid() && engine.hasSourceManager() && - diagnostic.location.line != 0 && !diagnostic.location.file.empty()) { - auto &sources = engine.getSourceManager(); - if (const auto file = sources.getFileManager().getOptionalFileRef( - diagnostic.location.file)) { - location = sources.translateFileLineCol(&file->getFileEntry(), - diagnostic.location.line, - diagnostic.location.column); - // RFC 0030 §13.2: the link step reports at places no compile of this - // process has read; the file is entered on first use. - if (location.isInvalid()) - location = sources.translateLineCol( - sources.getOrCreateFileID(*file, clang::SrcMgr::C_User), - diagnostic.location.line, diagnostic.location.column); - } - } - auto builder = engine.Report(location, id); - builder << diagnostic.message; - if (!isNote) - builder << diagnostic.id; - for (const core::FixItHint &fixit : diagnostic.fixits) { - const clang::SourceLocation at = analysis::toClangLocation(fixit.location); - if (at.isValid()) - builder << clang::FixItHint::CreateInsertion(at, fixit.insertion); - } -} - -} // namespace weavec::frontend diff --git a/lib/Frontend/DiagnosticControl.cpp b/lib/Frontend/DiagnosticControl.cpp deleted file mode 100644 index a09dd509..00000000 --- a/lib/Frontend/DiagnosticControl.cpp +++ /dev/null @@ -1,185 +0,0 @@ -//===- DiagnosticControl.cpp - -W flags for WeaveC diagnostics ------------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// - -#include "weavec/Frontend/DiagnosticControl.h" - -#include - -namespace weavec::frontend { - -namespace { - -/// The shape of a `-W` flag once its prefix is recognised. -struct Request { - /// `weavec` (the group) or `weavec-`. - llvm::StringRef target; - DiagnosticControl::Level level = DiagnosticControl::Level::Default; - /// `-Wno-weavec-`: only legal for warnings. - bool disables = false; -}; - -} // namespace - -static std::optional classify(llvm::StringRef flag) { - Request request; - llvm::StringRef rest; - if (flag.consume_front("-Werror=")) { - request.level = DiagnosticControl::Level::Error; - rest = flag; - } else if (flag.consume_front("-Wno-error=")) { - request.level = DiagnosticControl::Level::Warning; - rest = flag; - } else if (flag.consume_front("-Wno-")) { - request.level = DiagnosticControl::Level::Off; - request.disables = true; - rest = flag; - } else if (flag.consume_front("-W")) { - request.level = DiagnosticControl::Level::Default; - rest = flag; - } else { - return std::nullopt; - } - if (rest != "weavec" && !rest.starts_with("weavec-")) - return std::nullopt; - request.target = rest; - return request; -} - -bool DiagnosticControl::isWeaveCFlag(llvm::StringRef flag) { - return classify(flag).has_value(); -} - -/// Whether some diagnostic of `id` is a warning by default (RFC 0030, -/// *Diagnostics*); `-Wno-weavec-` is refused for the other ids, which -/// are always errors. -static bool hasWarningForm(std::string_view id) { - return core::diag::defaultSeverity(id, core::Certainty::Definite) == - core::Severity::Warning || - core::diag::defaultSeverity(id, core::Certainty::Possible) == - core::Severity::Warning; -} - -bool DiagnosticControl::parse(llvm::StringRef flag, std::string &error) { - const std::optional request = classify(flag); - if (!request) - return false; - - if (request->target == "weavec") { - // Later flags win, as in Clang, except that `-Werror=weavec` does not - // resurrect a warning an earlier `-Wno-weavec-` disabled. - all = request->level; - const bool keepDisabled = - request->level == Level::Error || request->level == Level::Warning; - for (auto it = perId.begin(); it != perId.end();) { - if (keepDisabled && it->second == Level::Off) - ++it; - else - it = perId.erase(it); - } - // Only `-Wweavec` and `-Wno-weavec` decide the off-by-default ids. - if (request->level == Level::Default || request->level == Level::Off) { - enableAll = request->level == Level::Default; - enabledIds.clear(); - } - return true; - } - - const llvm::StringRef id = request->target.drop_front(sizeof("weavec-") - 1); - if (core::diag::isRemoved(id)) { - error = "unknown WeaveC diagnostic '" + id.str() + "' (removed by " + - std::string(core::diag::removedBy(id)) + ")"; - return true; - } - if (!core::diag::isKnown(id)) { - error = - "unknown WeaveC diagnostic '" + id.str() + "' in '" + flag.str() + "'"; - return true; - } - // An id whose errors an earlier flag lowered (`-Wno-error=weavec`, as - // `weavec-cc -fweavec-diagnose` does) may be disabled. - const bool lowered = levelFor(id) == Level::Warning; - if (request->disables && !hasWarningForm(id) && !lowered) { - error = "'" + flag.str() + "': '" + id.str() + - "' is an error and cannot be disabled; use -Wno-error=weavec-" + - id.str() + " to make it a warning"; - return true; - } - perId[id.str()] = request->level; - if (request->disables && lowered) - silenced.emplace(id.str()); - else - silenced.erase(std::string(id)); - if (request->level != Level::Warning) - enabledIds[id.str()] = request->level != Level::Off; - return true; -} - -DiagnosticControl::Level -DiagnosticControl::levelFor(std::string_view id) const { - if (const auto it = perId.find(id); it != perId.end()) - return it->second; - return all; -} - -bool DiagnosticControl::enabledByFlags(std::string_view id) const { - if (core::diag::isEnabledByDefault(id) && !offByDefault.contains(id)) - return true; - if (const auto it = enabledIds.find(id); it != enabledIds.end()) - return it->second; - return enableAll; -} - -std::optional -DiagnosticControl::apply(const core::Diagnostic &diagnostic) const { - if (!enabledByFlags(diagnostic.id)) - return std::nullopt; - const Level level = levelFor(diagnostic.id); - const bool warning = - core::diag::defaultSeverity(diagnostic.id, diagnostic.certainty) == - core::Severity::Warning; - switch (level) { - case Level::Default: - return diagnostic; - case Level::Off: - // `-Wno-weavec` is a group request: it disables the warnings and leaves - // the errors alone, unless they were lowered first. - if (warning || silenced.contains(diagnostic.id)) - return std::nullopt; - return diagnostic; - case Level::Warning: { - core::Diagnostic adjusted = diagnostic; - if (adjusted.severity == core::Severity::Error) - adjusted.severity = core::Severity::Warning; - return adjusted; - } - case Level::Error: { - core::Diagnostic adjusted = diagnostic; - if (adjusted.severity == core::Severity::Warning) - adjusted.severity = core::Severity::Error; - return adjusted; - } - } - return diagnostic; -} - -void FilteringSink::report(const core::Diagnostic &diagnostic) { - const std::optional adjusted = table.apply(diagnostic); - if (!adjusted) - return; - const ReportedDiagnostic key = ReportedDiagnostic::of(*adjusted); - if (skip != nullptr && skip->contains(key)) - return; - forwarded.insert(key); - if (adjusted->severity == core::Severity::Error) - ++errorCount; - else if (adjusted->severity == core::Severity::Warning) - ++warningCount; - downstream.report(*adjusted); -} - -} // namespace weavec::frontend diff --git a/lib/Frontend/Driver.cpp b/lib/Frontend/Driver.cpp index dbe01fbc..a1b77fe7 100644 --- a/lib/Frontend/Driver.cpp +++ b/lib/Frontend/Driver.cpp @@ -9,14 +9,12 @@ #include "weavec/Frontend/Driver.h" #include "weavec/Config/Version.h" -#include "weavec/Core/Ledger.h" -#include "weavec/Frontend/AnalysisStats.h" #include "weavec/Frontend/EnforcementLedger.h" -#include "weavec/Frontend/FrontendAction.h" #include "weavec/Frontend/GuardPass.h" #include "weavec/Frontend/ResourceDir.h" #include "weavec/Frontend/UnsafeRegions.h" +#include "clang/AST/ASTConsumer.h" #include "clang/Basic/Diagnostic.h" #include "clang/Basic/DiagnosticIDs.h" #include "clang/Basic/DiagnosticOptions.h" @@ -35,6 +33,7 @@ #include "clang/Frontend/TextDiagnosticBuffer.h" #include "clang/Frontend/TextDiagnosticPrinter.h" #include "clang/FrontendTool/Utils.h" +#include "clang/Lex/PreprocessorOptions.h" #include "llvm/ADT/IntrusiveRefCntPtr.h" #include "llvm/ADT/STLExtras.h" @@ -83,9 +82,8 @@ static std::string invalidValue(llvm::StringRef arg, llvm::StringRef value, /// then says what is wrong with it, if anything. static bool consumeValueFlag(DriverOptions &options, llvm::StringRef arg, std::string &error) { - static constexpr std::array Names{ - "-fweavec-analysis-stats", "-fweavec-checks", "-fweavec-ledger", - "-fweavec-budget", "-fweavec-unit-budget"}; + static constexpr std::array Names{"-fweavec-checks", + "-fweavec-ledger"}; const auto [flag, value] = arg.split('='); if (!llvm::is_contained(Names, flag)) return false; @@ -93,11 +91,7 @@ static bool consumeValueFlag(DriverOptions &options, llvm::StringRef arg, error = "missing value for '" + flag.str() + "'"; return true; } - if (flag == "-fweavec-analysis-stats") { - options.analysisStatsPath = value.str(); - if (!options.stats) - options.stats = std::make_shared(); - } else if (flag == "-fweavec-checks") { + if (flag == "-fweavec-checks") { if (value == "trap") options.checks = DriverOptions::Checks::Trap; else if (value == "report") @@ -108,16 +102,8 @@ static bool consumeValueFlag(DriverOptions &options, llvm::StringRef arg, options.checks = DriverOptions::Checks::None; else error = invalidValue(arg, value, "trap, report, verify or none"); - } else if (flag == "-fweavec-ledger") { - options.ledger = value.str(); } else { - std::uint64_t budget = 0; - if (value.getAsInteger(10, budget)) - error = invalidValue(arg, value, "a number"); - else if (flag == "-fweavec-budget") - options.budget = budget; - else - options.unitBudget = budget; + options.ledger = value.str(); } options.spellings.push_back(arg.str()); return true; @@ -131,18 +117,12 @@ bool DriverOptions::consume(llvm::StringRef arg, std::string &error) { if (name.consume_front("-fno-")) { value = false; } else if (!name.consume_front("-f")) { - if (control.parse(arg, error)) { - spellings.push_back(arg.str()); - return true; - } return false; } if (name == "weavec") { enabled = value; - } else if (name == "weavec-diagnose") { - diagnose = value; - } else if (name == "weavec-dump-analysis") { - dumpAnalysis = value; + } else if (name == "weavec-asan-compat") { + asanCompat = value; } else if (name == "weavec-zero-init") { zeroInit = value; } else if (name == "weavec-summary") { @@ -158,7 +138,7 @@ bool DriverOptions::consume(llvm::StringRef arg, std::string &error) { } llvm::StringRef driverFlagsHelp() { - return R"(WeaveC flags of weavec-cc (RFC 0035); every other flag is Clang's. + return R"(WeaveC flags of weavec-cc (RFC 0036); every other flag is Clang's. -fweavec, -fno-weavec Guard memory accesses (the default), or compile as plain Clang. @@ -174,16 +154,9 @@ llvm::StringRef driverFlagsHelp() { receives .ledger.json per unit; a file receives one ledger. -fweavec-summary, -fno-weavec-summary Print the summary line on stderr (default: when a ledger is written). - -fweavec-diagnose - Also run WeaveC's ownership and lifetime analysis and print what it - finds as warnings (-Werror=weavec makes them errors). - -fweavec-budget=, -fweavec-unit-budget= - The analysis's work budgets per function and per unit. - -fweavec-dump-analysis, -fweavec-analysis-stats= - Debugging output of the analysis. - -Wno-weavec-, -Wweavec-, -Werror=weavec[-], - -Wno-error=weavec[-], -Wweavec, -Wno-weavec - Control the analysis's diagnostics. + -fweavec-asan-compat, -fno-weavec-asan-compat + Compile as AddressSanitizer would for the source: __SANITIZE_ADDRESS__ + and __has_feature(address_sanitizer) (default: on). )"; } @@ -208,15 +181,29 @@ static bool isCodeGenAction(clang::frontend::ActionKind action) { namespace { -/// Clang's action for the job with the unsafe-region collector and, under -/// `-fweavec-diagnose`, the analysis running beside its code generator. +/// RFC 0036 §1: the unit is parsed with AddressSanitizer in its language +/// options (its macros, `__has_feature(address_sanitizer)`); at the end of +/// the unit, before the code generator's backend reads them, this takes it +/// out again, so that ASan's own instrumentation never runs. +class AsanOptionsReset final : public clang::ASTConsumer { +public: + explicit AsanOptionsReset(clang::LangOptions &lang) : lang(lang) {} + void HandleTranslationUnit(clang::ASTContext & /*context*/) override { + lang.Sanitize.set(clang::SanitizerKind::Address, false); + } + +private: + clang::LangOptions ⟨ +}; + +/// Clang's action for the job with the unsafe-region collector running +/// beside its code generator. class WeaveCWrapperAction final : public clang::WrapperFrontendAction { public: WeaveCWrapperAction(std::unique_ptr wrapped, - std::shared_ptr unsafe, - std::optional analysis) + std::shared_ptr unsafe, bool asanCompat) : WrapperFrontendAction(std::move(wrapped)), unsafe(std::move(unsafe)), - analysis(std::move(analysis)) {} + asanCompat(asanCompat) {} protected: std::unique_ptr @@ -225,8 +212,9 @@ class WeaveCWrapperAction final : public clang::WrapperFrontendAction { std::vector> consumers; if (unsafe) consumers.push_back(createUnsafeRegionCollector(compiler, unsafe)); - if (analysis) - consumers.push_back(createWeaveCConsumer(compiler, *analysis)); + if (asanCompat) + consumers.push_back( + std::make_unique(compiler.getLangOpts())); std::unique_ptr inner = WrapperFrontendAction::CreateASTConsumer(compiler, inFile); if (!inner) @@ -239,7 +227,7 @@ class WeaveCWrapperAction final : public clang::WrapperFrontendAction { private: std::shared_ptr unsafe; - std::optional analysis; + bool asanCompat; }; } // namespace @@ -255,30 +243,6 @@ static void initializeTargets() { (void)Initialized; } -/// The analysis's options under `-fweavec-diagnose` (RFC 0035 §8): every -/// finding a warning. -static FrontendOptions analysisOptions(const DriverOptions &weavec) { - FrontendOptions options; - options.engine.stats = weavec.stats.get(); - options.analysisStatsPath = weavec.analysisStatsPath; - options.engine.dumpStream = weavec.dumpAnalysis ? &llvm::outs() : nullptr; - options.engine.zeroInit = weavec.zeroInitialises(); - if (weavec.budget) - options.engine.budget = *weavec.budget; - options.engine.unitBudget = weavec.unitBudget; - // Warnings, unless the command line's own -W flags, applied after, say - // otherwise. - DiagnosticControl control; - control.turnOffByDefault(core::diag::Leak); - std::string ignored; - (void)control.parse("-Wno-error=weavec", ignored); - for (const std::string &flag : weavec.spellings) - if (DiagnosticControl::isWeaveCFlag(flag)) - (void)control.parse(flag, ignored); - options.control = std::move(control); - return options; -} - /// The guard pass's mode for a `-fweavec-checks` mode. static GuardOptions::Mode guardMode(DriverOptions::Checks checks) { switch (checks) { @@ -372,8 +336,30 @@ static bool emitEnforcementLedger(const clang::CompilerInstance &compiler, return ok; } -static int runCc1Job(llvm::ArrayRef argv, const char *argv0, - const DriverOptions *driver = nullptr) { +/// RFC 0036 §5: ASan's macros for the source (`__SANITIZE_ADDRESS__`, +/// `__has_feature(address_sanitizer)`) unless a sanitizer of the command +/// line already gives them; whether it did. +static bool applyAsanCompat(clang::CompilerInstance &compiler, + const DriverOptions &weavec) { + if (!weavec.enforces() || !weavec.asanCompat || + compiler.getLangOpts().Sanitize.hasOneOf( + clang::SanitizerKind::Address | clang::SanitizerKind::KernelAddress | + clang::SanitizerKind::HWAddress)) + return false; + compiler.getLangOpts().Sanitize.set(clang::SanitizerKind::Address, true); + // Clang predefines _FORTIFY_SOURCE=0 for ASan on Darwin (ASan intercepts + // the C library there); WeaveC checks the fortified forms (§5.5), so the + // SDK's own default applies unless the command line chose a level. + clang::PreprocessorOptions &preprocessor = compiler.getPreprocessorOpts(); + const bool chosen = llvm::any_of(preprocessor.Macros, [](const auto ¯o) { + return llvm::StringRef(macro.first).starts_with("_FORTIFY_SOURCE"); + }); + if (llvm::Triple(compiler.getTargetOpts().Triple).isOSDarwin() && !chosen) + preprocessor.addMacroUndef("_FORTIFY_SOURCE"); + return true; +} + +static int runCc1Job(llvm::ArrayRef argv, const char *argv0) { DriverOptions weavec; std::vector cc1Args; for (const char *arg : argv) { @@ -442,8 +428,12 @@ static int runCc1Job(llvm::ArrayRef argv, const char *argv0, clang::CreateFrontendAction(*compiler); if (!inner) return 1; - if (inner->usesPreprocessorOnly()) + if (inner->usesPreprocessorOnly()) { + // The same macros as the compile that follows (ccache's preprocessor + // mode, Autoconf's CPP checks). + (void)applyAsanCompat(*compiler, weavec); return compiler->ExecuteAction(*inner) ? 0 : 1; + } const bool codegen = isCodeGenAction(compiler->getFrontendOpts().ProgramAction); @@ -453,20 +443,12 @@ static int runCc1Job(llvm::ArrayRef argv, const char *argv0, unsafe = std::make_shared(); ledger = configureEnforcement(*compiler, weavec, unsafe); } - if (driver != nullptr && driver->stats) - weavec.stats = driver->stats; - std::optional analysis; - if (weavec.diagnose) - analysis = analysisOptions(weavec); - - WeaveCWrapperAction action(std::move(inner), unsafe, std::move(analysis)); + const bool asanCompat = applyAsanCompat(*compiler, weavec); + WeaveCWrapperAction action(std::move(inner), unsafe, asanCompat); success = compiler->ExecuteAction(action); if (success && ledger) success = emitEnforcementLedger(*compiler, weavec, *ledger); - const bool statsOK = - driver != nullptr || - writeAnalysisStats(weavec.analysisStatsPath, weavec.stats.get()); - return success && statsOK ? 0 : 1; + return success ? 0 : 1; } //===----------------------------------------------------------------------===// @@ -599,6 +581,30 @@ static std::string runtimeObstacle(llvm::ArrayRef args) { return {}; } +/// RFC 0036 §1: whether the build should define `_FORTIFY_SOURCE=3`: a glibc +/// target, an optimising compile (glibc fortifies only then, and warns +/// otherwise), and a command line that does not define or undefine it. +static bool wantsFortification(llvm::ArrayRef args) { + std::string triple = llvm::sys::getDefaultTargetTriple(); + bool optimises = false; + for (std::size_t i = 1; i < args.size(); ++i) { + llvm::StringRef arg = args[i]; + if (arg.starts_with("-D_FORTIFY_SOURCE") || + arg.starts_with("-U_FORTIFY_SOURCE") || + ((arg == "-D" || arg == "-U") && i + 1 < args.size() && + llvm::StringRef(args[i + 1]).starts_with("_FORTIFY_SOURCE"))) + return false; + if (arg.starts_with("-O")) + optimises = arg != "-O0"; + if (arg.consume_front("--target=")) + triple = arg.str(); + else if ((arg == "-target" || arg == "--target") && i + 1 < args.size()) + triple = args[++i]; + } + const llvm::Triple target(llvm::Triple::normalize(triple)); + return optimises && target.isOSLinux() && target.isGNUEnvironment(); +} + /// RFC 0035 §7: adds the runtime archives to every link job, before the /// first library on its line (a linker takes a symbol from the first /// library that defines it, and scans an archive only for what is undefined @@ -849,7 +855,9 @@ int runDriver(llvm::ArrayRef argv, void *mainAddress) { printJobsOnly = true; if (text.starts_with("-isysroot") || text.starts_with("--sysroot")) hasSysroot = true; - if (looksLikeSource(text)) + // A source file, or `-` (standard input) other than `-o`'s operand. + if (looksLikeSource(text) || + (text == "-" && llvm::StringRef(clangArgs.back()) != "-o")) hasSource = true; clangArgs.push_back(arg); } @@ -881,6 +889,8 @@ int runDriver(llvm::ArrayRef argv, void *mainAddress) { add(include); } add("-D__WEAVEC__=1"); + if (weavec.enforces() && wantsFortification(clangArgs)) + add("-D_FORTIFY_SOURCE=3"); } } if (!hasSysroot) { @@ -908,8 +918,8 @@ int runDriver(llvm::ArrayRef argv, void *mainAddress) { diags, "weavec-cc: WeaveC C compiler"); driver.setTargetAndMode( clang::driver::ToolChain::getTargetAndModeFromProgramName("clang")); - const auto cc1 = [&weavec](llvm::SmallVectorImpl &args) { - return runCc1Job(llvm::ArrayRef(args).drop_front(2), args[0], &weavec); + const auto cc1 = [](llvm::SmallVectorImpl &args) { + return runCc1Job(llvm::ArrayRef(args).drop_front(2), args[0]); }; driver.CC1Main = cc1; if (const std::string resources = getClangResourceDir(); !resources.empty()) @@ -974,9 +984,7 @@ int runDriver(llvm::ArrayRef argv, void *mainAddress) { break; } } - const bool statsOK = - writeAnalysisStats(weavec.analysisStatsPath, weavec.stats.get()); - return statsOK ? status : 1; + return status; } } // namespace weavec::frontend diff --git a/lib/Frontend/FrontendAction.cpp b/lib/Frontend/FrontendAction.cpp deleted file mode 100644 index ffc871b5..00000000 --- a/lib/Frontend/FrontendAction.cpp +++ /dev/null @@ -1,251 +0,0 @@ -//===- FrontendAction.cpp - Clang frontend integration --------------------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// - -#include "weavec/Frontend/FrontendAction.h" - -#include "weavec/Analysis/LedgerAdapter.h" -#include "weavec/Analysis/UnitPipeline.h" -#include "weavec/Frontend/AnalysisSummary.h" -#include "weavec/Frontend/ClangDiagnosticSink.h" -#include "weavec/Frontend/InterfaceFacts.h" - -#include "clang/AST/ASTConsumer.h" -#include "clang/AST/ASTContext.h" -#include "clang/Basic/FileManager.h" -#include "clang/Frontend/ASTUnit.h" -#include "clang/Frontend/CompilerInstance.h" -#include "clang/Frontend/TextDiagnosticPrinter.h" - -#include -#include -#include - -namespace weavec::frontend { - -UnitResult analyzeTranslationUnit(clang::ASTContext &context, - clang::DiagnosticsEngine &diagnostics, - const FrontendOptions &options) { - // RFC 0030 §1 steps 2 and 3: kinds, sites, the engine through the ledger - // adapter, planning; the diagnostics come back in the engine's order. - UnitResult result; - // As Clang's own analyzer does: a unit that failed to parse is not - // analysed (its records may have no layout). - if (diagnostics.hasUncompilableErrorOccurred()) { - result.errors = 1; - return result; - } - analysis::UnitPipelineOptions pipeline; - pipeline.engine = options.engine; - if (options.silent) - pipeline.engine.dumpStream = nullptr; - // RFC 0030 §5.6: every emitted function is analysed and reported, those - // of user headers included (the engine asks the unit's sites); a silent - // round reports nothing. - if (options.silent) - pipeline.engine.shouldReport = [](const clang::FunctionDecl &) { - return false; - }; - pipeline.database = options.database; - pipeline.discoverOnly = options.discoverOnly; - pipeline.buildLedger = !options.silent; - core::DiagnosticCollector collected; - analysis::UnitPipelineResult unit = - analysis::runUnitAnalysis(context, pipeline, collected); - result.exports = std::move(unit.exports); - result.ledger = std::move(unit.ledger); - if (options.holdFor && !options.silent) - result.held = - std::ranges::any_of(result.exports.contextRequests, options.holdFor); - if (result.held) { - result.ledger = nullptr; - return result; - } - if (options.discoverOnly) { - // RFC 0030 §13.2 step 2: the whole-program driver solves the slots of - // every unit before it analyses any. - if (options.collectInterface) - result.interface = - std::make_shared(collectSlotFacts(context)); - return result; - } - // What `weavec --whole-program` checks across units. - if (options.collectInterface && !options.silent) - result.interface = std::make_shared( - collectInterfaceFacts(context, FactsInput{.exports = result.exports, - .kinds = unit.kinds.get()})); - - ClangDiagnosticSink clangSink(diagnostics); - FilteringSink sink(clangSink, options.control, options.alreadyReported); - if (!options.silent) - for (const auto &diagnostic : collected.diagnostics()) - sink.report(diagnostic); - result.reported = sink.reported(); - result.errors = sink.errors(); - result.warnings = sink.warnings(); - return result; -} - -/// The directory the unit's relative paths are relative to. -static std::string workingDirectoryOf(clang::FileManager &files) { - std::string cwd = files.getFileSystemOpts().WorkingDir; - if (cwd.empty()) - if (const llvm::ErrorOr current = - files.getVirtualFileSystem().getCurrentWorkingDirectory()) - cwd = *current; - return cwd; -} - -/// RFC 0035 §8: the summary line of a reporting run, under -/// `FrontendOptions::summary`. -static void printSummary(const UnitResult &result, llvm::StringRef source, - clang::FileManager &files, - const FrontendOptions &options) { - if (!options.summary || options.silent || options.discoverOnly || - !result.ledger) - return; - printUnitSummary(*result.ledger, - summaryName(source, workingDirectoryOf(files)), - options.control); -} - -UnitResult analyzeRetainedUnit(clang::ASTUnit &ast, - const FrontendOptions &options) { - auto &diagnostics = ast.getDiagnostics(); - auto *previous = diagnostics.getClient(); - auto owned = diagnostics.takeClient(); - // RFC 0020: Clang renders each diagnostic in many small writes. A private - // bounded stderr stream combines them without changing global stream state - // or retaining a translation unit's entire output. Descriptor 2 stays open. - class DiagnosticStream final : public llvm::raw_ostream { - public: - DiagnosticStream() : llvm::raw_ostream(true), destination(2, false) { - destination.SetBufferSize(16384); - enable_colors(llvm::errs().colors_enabled()); - destination.enable_colors(colors_enabled()); - } - void finishDiagnostic() { destination.flush(); } - bool is_displayed() const override { return destination.is_displayed(); } - bool has_colors() const override { return destination.has_colors(); } - - private: - // Clang's formatted stream takes its immediate sink's buffer. Keep this - // facade unbuffered so locationless diagnostics cannot bypass a pending - // source snippet in that formatted stream. Only the destination buffers. - llvm::raw_fd_ostream destination; - void write_impl(const char *data, std::size_t size) override { - destination.write(data, size); - } - std::uint64_t current_pos() const override { return destination.tell(); } - }; - DiagnosticStream diagnosticOutput; - class BufferedDiagnosticPrinter final : public clang::TextDiagnosticPrinter { - public: - BufferedDiagnosticPrinter(DiagnosticStream &output, - clang::DiagnosticOptions &options) - : clang::TextDiagnosticPrinter(output, options), output(output) {} - void HandleDiagnostic(clang::DiagnosticsEngine::Level level, - const clang::Diagnostic &info) override { - clang::TextDiagnosticPrinter::HandleDiagnostic(level, info); - // Preserve prompt delivery, including an isolated error before a later - // long-running analysis or fatal exit. - output.finishDiagnostic(); - } - - private: - DiagnosticStream &output; - }; - BufferedDiagnosticPrinter printer(diagnosticOutput, - diagnostics.getDiagnosticOptions()); - diagnostics.setClient(&printer, false); - diagnostics.Reset(true); - printer.BeginSourceFile(ast.getLangOpts(), &ast.getPreprocessor()); - auto result = - analyzeTranslationUnit(ast.getASTContext(), diagnostics, options); - printSummary(result, ast.getMainFileName(), ast.getFileManager(), options); - if (diagnostics.hasErrorOccurred() && result.errors == 0) - result.errors = 1; - printer.EndSourceFile(); - diagnosticOutput.finishDiagnostic(); - const auto warnings = printer.getNumWarnings(); - const auto errors = printer.getNumErrors(); - if (warnings || errors) { - if (warnings) - llvm::errs() << warnings << " warning" << (warnings == 1 ? "" : "s"); - if (warnings && errors) - llvm::errs() << " and "; - if (errors) - llvm::errs() << errors << " error" << (errors == 1 ? "" : "s"); - llvm::errs() << " generated.\n"; - } - const bool owns = static_cast(owned); - diagnostics.setClient(owns ? owned.release() : previous, owns); - return result; -} - -namespace { - -class WeaveCConsumer final : public clang::ASTConsumer { -public: - WeaveCConsumer(clang::CompilerInstance &compiler, FrontendOptions opts) - : compiler(compiler), options(std::move(opts)) { - if (options.engine.stats) - options.engine.stats->add("unit_parses"); - // RFC 0030 §3.1: under `-fno-strict-aliasing` any two pointee types may - // designate one object. - options.engine.strictAliasing = !compiler.getCodeGenOpts().RelaxedAliasing; - } - void HandleTranslationUnit(clang::ASTContext &context) override { - auto result = - analyzeTranslationUnit(context, compiler.getDiagnostics(), options); - const clang::FrontendOptions &frontend = compiler.getFrontendOpts(); - if (!frontend.Inputs.empty() && frontend.Inputs.front().isFile() && - compiler.hasFileManager()) - printSummary(result, frontend.Inputs.front().getFile(), - compiler.getFileManager(), options); - if (options.onResult) - options.onResult(std::move(result)); - } - -private: - clang::CompilerInstance &compiler; - FrontendOptions options; -}; - -class WeaveCActionFactory final : public clang::tooling::FrontendActionFactory { -public: - explicit WeaveCActionFactory(FrontendOptions opts) - : options(std::move(opts)) {} - - std::unique_ptr create() override { - return std::make_unique(options); - } - -private: - FrontendOptions options; -}; - -} // namespace - -std::unique_ptr -createWeaveCConsumer(clang::CompilerInstance &compiler, - const FrontendOptions &options) { - return std::make_unique(compiler, options); -} - -std::unique_ptr -WeaveCAction::CreateASTConsumer(clang::CompilerInstance &compiler, - llvm::StringRef /*inFile*/) { - return createWeaveCConsumer(compiler, options); -} - -std::unique_ptr -createWeaveCActionFactory(FrontendOptions options) { - return std::make_unique(std::move(options)); -} - -} // namespace weavec::frontend diff --git a/lib/Frontend/GuardFrames.cpp b/lib/Frontend/GuardFrames.cpp index 8258d75f..79ddb45c 100644 --- a/lib/Frontend/GuardFrames.cpp +++ b/lib/Frontend/GuardFrames.cpp @@ -7,7 +7,9 @@ //===----------------------------------------------------------------------===// // // RFC 0035 §3. The tracked static allocas of a function become offsets into -// one frame alloca with redzones between them. The frame's shadow is written +// one frame alloca with redzones between them; two whose lifetimes never +// overlap may share bytes (RFC 0036 §2), as Clang's stack colouring lets +// them share stack space. The frame's shadow is written // at entry (redzones poisoned, objects whose scope has not begun poisoned as // out of scope) and cleared before each return; lifetime markers write the // scope of their object. A tracked dynamic alloca gets a redzone after it, @@ -20,6 +22,7 @@ #include "GuardPassImpl.h" #include "llvm/ADT/SmallPtrSet.h" +#include "llvm/Analysis/StackLifetime.h" #include "llvm/IR/IntrinsicInst.h" #include "llvm/IR/Intrinsics.h" @@ -38,6 +41,11 @@ static constexpr std::uint8_t ShadowScope = 0xF8; /// below a frame's first array. static constexpr std::uint64_t FrameAlign = 32; static constexpr std::uint64_t LeftZone = 64; +/// RFC 0036 amendment 8: locals share bytes only in a frame that would be +/// larger than this without sharing. Below it the redzones cost little, and +/// keeping every local apart keeps a use after scope from landing in a +/// local whose scope began later. +static constexpr std::uint64_t ShareAbove = 512; namespace { @@ -130,34 +138,74 @@ static void layoutStatic(FunctionContext &context, [](const FrameObject &a, const FrameObject &b) { return a.alloca->getAlign() > b.alloca->getAlign(); }); - std::uint64_t offset = LeftZone; - for (FrameObject &object : objects) { + + // RFC 0036 §2: two objects may share bytes when their lifetimes never + // overlap. An object without lifetime markers is live throughout. + // (Amendment 8: only in a frame that would be large without sharing.) + std::uint64_t unshared = LeftZone; + for (const FrameObject &object : objects) + unshared += + paddedSize(object.size) + + std::max(FrameAlign, object.alloca->getAlign().value()); + const bool scoped = unshared > ShareAbove && + llvm::any_of(objects, [](const FrameObject &object) { + return !object.starts.empty(); + }); + llvm::SmallVector lifetimeAllocas; + for (const FrameObject &object : objects) + lifetimeAllocas.push_back(object.alloca); + llvm::StackLifetime lifetimes(function, lifetimeAllocas, + llvm::StackLifetime::LivenessType::May); + if (scoped) + lifetimes.run(); + auto together = [&](const FrameObject &a, const FrameObject &b) { + return !scoped || a.starts.empty() || b.starts.empty() || + lifetimes.getLiveRange(a.alloca).overlaps( + lifetimes.getLiveRange(b.alloca)); + }; + auto extentEnd = [](const FrameObject &object) { + return object.offset + paddedSize(object.size); + }; + // Each object at the lowest aligned offset where its extent (the object + // and its redzone) meets no extent of an object live with it. + std::uint64_t frameSize = LeftZone; + for (std::size_t i = 0; i < objects.size(); ++i) { + FrameObject &object = objects[i]; const std::uint64_t align = std::max(FrameAlign, object.alloca->getAlign().value()); - offset = (offset + align - 1) & ~(align - 1); + const std::uint64_t extent = paddedSize(object.size); + std::uint64_t offset = (LeftZone + align - 1) & ~(align - 1); + for (bool clash = true; clash;) { + clash = false; + for (std::size_t j = 0; j < i; ++j) + if (offset < extentEnd(objects[j]) && + objects[j].offset < offset + extent && + together(objects[j], object)) { + offset = (extentEnd(objects[j]) + align - 1) & ~(align - 1); + clash = true; + } + } object.offset = offset; - offset += paddedSize(object.size); + frameSize = std::max(frameSize, offset + extent); } - const std::uint64_t frameSize = offset; - // The shadow at entry: redzones, objects in scope or not yet. + // The shadow at entry: redzones, objects in scope or not yet. An object + // live at entry shares bytes with none. llvm::SmallVector entry(frameSize / 16, ShadowMid); for (std::uint64_t g = 0; g < LeftZone / 16; ++g) entry[g] = ShadowLeft; + std::uint64_t objectsEnd = 0; for (const FrameObject &object : objects) { llvm::SmallVector bytes; objectBytes(object.size, bytes); - const bool scoped = !object.starts.empty(); + const bool inScope = object.starts.empty(); for (std::size_t i = 0; i < bytes.size(); ++i) - entry[(object.offset / 16) + i] = scoped ? ShadowScope : bytes[i]; - } - { - const FrameObject &last = *std::ranges::max_element( - objects, {}, [](const FrameObject &object) { return object.offset; }); - for (std::uint64_t g = (last.offset + last.size + 15) / 16; - g < frameSize / 16; ++g) - entry[g] = ShadowRight; + if (inScope || entry[(object.offset / 16) + i] == ShadowMid) + entry[(object.offset / 16) + i] = inScope ? bytes[i] : ShadowScope; + objectsEnd = std::max(objectsEnd, object.offset + object.size); } + for (std::uint64_t g = (objectsEnd + 15) / 16; g < frameSize / 16; ++g) + entry[g] = ShadowRight; // The frame, at the top of the entry block. llvm::BasicBlock &first = function.getEntryBlock(); @@ -182,6 +230,15 @@ static void layoutStatic(FunctionContext &context, llvm::SmallVector live; objectBytes(object.size, live); const llvm::SmallVector dead(live.size(), ShadowScope); + // A redzone that shares bytes with another object's says so again at + // the start, so that an overflow into it is reported as one. + const bool sharesRedzone = + llvm::any_of(objects, [&](const FrameObject &other) { + return &other != &object && other.offset < extentEnd(object) && + object.offset + object.size < other.offset + other.size; + }); + if (sharesRedzone) + live.resize(paddedSize(object.size) / 16, ShadowMid); for (llvm::IntrinsicInst *start : object.starts) { context.writeShadow(start, at, live); start->eraseFromParent(); diff --git a/lib/Frontend/GuardInsert.cpp b/lib/Frontend/GuardInsert.cpp index 86bf72fe..b55268d6 100644 --- a/lib/Frontend/GuardInsert.cpp +++ b/lib/Frontend/GuardInsert.cpp @@ -225,6 +225,10 @@ static llvm::Value *nullBaseOf(llvm::Value *pointer, } void insertGuards(ModuleContext &module, llvm::Function &function) { + // RFC 0036 §1: the unit was parsed with AddressSanitizer's language + // options for its macros; nothing else of ASan's applies, and the + // attribute would keep the optimiser from its usual work. + function.removeFnAttr(llvm::Attribute::SanitizeAddress); std::vector found; for (llvm::BasicBlock &block : function) for (llvm::Instruction &instruction : block) diff --git a/lib/Frontend/GuardLibrary.cpp b/lib/Frontend/GuardLibrary.cpp index 2b8b2979..49c94c59 100644 --- a/lib/Frontend/GuardLibrary.cpp +++ b/lib/Frontend/GuardLibrary.cpp @@ -16,7 +16,8 @@ // (the calls whose bytes only the call computes, those that read less than // their terms state, and `mmap` and `munmap`, whose versions clear the // shadow of what they map); a call that only computes a string's length is -// replaced by the string guard that computes it. +// replaced by the string guard that computes it. RFC 0036 §6: a fortified +// form's object size bounds what the call writes, inline or in the wrapper. // //===----------------------------------------------------------------------===// @@ -71,6 +72,48 @@ struct LibraryCall { bytes, LedgerRow::Outcome::Guarded, reason); } + /// RFC 0036 §6: the object size a fortified form passes (an i64), or + /// null when the call is no fortified form or the size is unknown (-1). + [[nodiscard]] llvm::Value *objectSize() const { + const int at = match.sizeArgument(); + if (at < 0 || static_cast(at) >= call.arg_size()) + return nullptr; + llvm::Value *size = call.getArgOperand(static_cast(at)); + if (!size->getType()->isIntegerTy()) + return nullptr; + if (const auto *constant = llvm::dyn_cast(size); + constant != nullptr && constant->isMinusOne()) + return nullptr; + llvm::IRBuilder<> builder(&call); + llvm::Value *bytes = builder.CreateZExtOrTrunc(size, context.i64); + // A wide row's size counts wchar_t elements. + if (match.entry->wide) + bytes = builder.CreateMul( + bytes, llvm::ConstantInt::get(context.i64, wcharSize())); + return bytes; + } + + /// Whether the row's wrapper takes a limit: some fortified form of the + /// row passes an object size. + [[nodiscard]] bool wrapperTakesLimit() const { + return llvm::any_of(match.entry->chk, [](const core::LibraryChk &chk) { + return chk.sizeArgument >= 0; + }); + } + + /// RFC 0036 §6: `bytes` written at `pointer` within the object size. + void checkLimit(llvm::Value *pointer, llvm::Value *bytes, + llvm::Value *limit) const { + llvm::IRBuilder<> builder(&call); + llvm::Value *over = builder.CreateICmpUGT(bytes, limit); + llvm::Instruction *slow = + llvm::SplitBlockAndInsertIfThen(over, &call, false); + llvm::IRBuilder<> at(slow); + ModuleContext::callSlow( + at, context.limitFailFunction(), + {at.CreatePtrToInt(pointer, context.i64), bytes, limit, site(true)}); + } + /// The bytes of a `wchar_t` (the module's `wchar_size`, 4 by default). [[nodiscard]] std::uint64_t wcharSize() const { if (const auto *size = llvm::mdconst::extract_or_null( @@ -106,7 +149,7 @@ struct LibraryCall { } /// A term's value as an unsigned 64-bit integer, saturated; null when a - /// leaf has no value in the IR (`fmtlen`, a macro). `bound` bounds the + /// leaf has no value in the IR (a wide string's length). `bound` bounds the /// `strlen` leaves (inside `min`). llvm::Value *evaluate(const core::LibTerm &term, llvm::IRBuilder<> &builder, llvm::Value *bound = nullptr) const { @@ -136,9 +179,6 @@ struct LibraryCall { return stringLength(builder, pointer, bound, nullOk(term.arg) ? MarkerNullOk : 0); } - case Kind::FormatLength: - case Kind::Macro: - return nullptr; case Kind::Product: case Kind::Sum: { llvm::Value *left = evaluate(term.operands[0], builder, bound); @@ -227,6 +267,7 @@ struct LibraryCall { void LibraryCall::guardArguments() const { const core::LibraryEntry &entry = *match.entry; + llvm::Value *limit = objectSize(); for (unsigned index = 0; index < entry.params.size(); ++index) { const core::LibraryParam ¶m = entry.params[index]; if (param.type != core::LibraryParam::Type::Pointer || @@ -280,6 +321,12 @@ void LibraryCall::guardArguments() const { builder.CreateSelect(builder.CreateIsNull(pointer), llvm::ConstantInt::get(module().i64, 0), bytes); guardBytes(pointer, bytes, write); + // The object a fortified form writes is the first it writes; its bytes + // are addressable by now, so a failure is within an object. + if (limit != nullptr && write) { + checkLimit(pointer, bytes, limit); + limit = nullptr; + } } // The operands a copy must not overlap. for (const core::LibDisjoint &disjoint : entry.disjoint) { @@ -463,7 +510,18 @@ static bool redirect(LibraryCall &call, bool late) { } // A read's site: the wrapper marks what it writes. llvm::Constant *site = call.site(false); + // RFC 0036 §6: the object size of a fortified form, or no limit. + llvm::Value *limit = nullptr; + if (call.wrapperTakesLimit()) { + limit = call.objectSize(); + if (limit == nullptr) + limit = llvm::ConstantInt::getAllOnesValue(module.i64); + } if (entry.variadic) { + if (limit != nullptr) { + arguments.insert(arguments.begin(), limit); + types.insert(types.begin(), module.i64); + } arguments.insert(arguments.begin(), site); types.insert(types.begin(), module.ptr); // The variadic arguments follow the row's last named one. @@ -474,6 +532,10 @@ static bool redirect(LibraryCall &call, bool late) { ++at) arguments.push_back(call.call.getArgOperand(at)); } else { + if (limit != nullptr) { + arguments.push_back(limit); + types.push_back(module.i64); + } arguments.push_back(site); types.push_back(module.ptr); } @@ -493,20 +555,18 @@ static bool redirect(LibraryCall &call, bool late) { } /// Whether the row only computes its `value` from what it reads: pointer -/// arguments read or untouched and kept by nothing, no clause and no -/// format. Such a call (`strlen`) is replaced by what its value term -/// evaluates to, the string guards included. +/// arguments read or untouched, no clause and no format. Such a call +/// (`strlen`) is replaced by what its value term evaluates to, the string +/// guards included. static bool computesItsValue(const core::LibraryEntry &entry) { - if (!entry.result.value || entry.wide || entry.variadic || entry.format || - entry.wrapper || !entry.disjoint.empty() || !entry.copies.empty() || - !entry.fills.empty() || !entry.writesString.empty() || - !entry.invalidates.empty() || !entry.reads.empty() || entry.noreturn) + if (!entry.value || entry.wide || entry.variadic || entry.format || + entry.wrapper || !entry.disjoint.empty()) return false; return llvm::all_of(entry.params, [](const core::LibraryParam ¶m) { return param.type != core::LibraryParam::Type::Pointer || ((param.access == core::LibraryParam::Access::Read || param.access == core::LibraryParam::Access::None) && - param.effect == core::LibraryParam::Effect::Borrow && !param.out); + param.effect == core::LibraryParam::Effect::Borrow); }); } @@ -537,7 +597,8 @@ void guardLibraryCalls(ModuleContext &module, llvm::Function &function, const llvm::StringRef name = cName(call->getCalledFunction()->getName()); if (module.isUnsafe(call->getDebugLoc())) continue; - const std::optional match = spec.lookup(name); + const std::optional match = + spec.lookup(name, call->arg_size()); if (!match || match->entry == nullptr) continue; LibraryCall library{.context = module, @@ -577,8 +638,7 @@ void guardLibraryCalls(ModuleContext &module, llvm::Function &function, if (match->alias == nullptr && computesItsValue(*match->entry) && call->getType()->isIntegerTy(64)) { llvm::IRBuilder<> builder(call); - if (llvm::Value *value = - library.evaluate(*match->entry->result.value, builder); + if (llvm::Value *value = library.evaluate(*match->entry->value, builder); value != nullptr && value->getType() == call->getType()) { call->replaceAllUsesWith(value); call->eraseFromParent(); diff --git a/lib/Frontend/GuardPass.cpp b/lib/Frontend/GuardPass.cpp index da866045..c88f6d11 100644 --- a/lib/Frontend/GuardPass.cpp +++ b/lib/Frontend/GuardPass.cpp @@ -6,9 +6,9 @@ // //===----------------------------------------------------------------------===// // -// RFC 0035: the three passes' entry points and registration, and what they -// share: the module's context (types, markers, runtime entry points, site -// records, the ledger) and the inline checks of the shadow. +// RFC 0035, RFC 0036: the guard passes' entry points and registration, and +// what they share: the module's context (types, markers, runtime entry points, +// site records, the ledger) and the inline checks of the shadow. // //===----------------------------------------------------------------------===// @@ -21,6 +21,7 @@ #include "llvm/IR/DebugInfoMetadata.h" #include "llvm/IR/IntrinsicInst.h" #include "llvm/IR/MDBuilder.h" +#include "llvm/IR/Verifier.h" #include "llvm/Passes/PassBuilder.h" #include "llvm/TargetParser/Triple.h" #include "llvm/Transforms/Utils/BasicBlockUtils.h" @@ -322,6 +323,14 @@ llvm::FunctionCallee ModuleContext::strlenFunction() { llvm::FunctionType::get(i64, {ptr, i64, ptr}, false)); } +llvm::FunctionCallee ModuleContext::limitFailFunction() { + llvm::FunctionCallee callee = runtime( + "limit_fail", llvm::FunctionType::get(llvm::Type::getVoidTy(context), + {i64, i64, i64, ptr}, false)); + slowPath(callee, module); + return callee; +} + llvm::GlobalVariable *ModuleContext::shadowDescriptor() { if (descriptor != nullptr) return descriptor; @@ -796,6 +805,16 @@ void FunctionContext::writeShadow(llvm::Instruction *before, } } +/// In a build with assertions, the function each pass leaves is valid IR: +/// a broken one fails at the pass that broke it, not in the back end. +static void verified([[maybe_unused]] const llvm::Function &function) { +#ifndef NDEBUG + if (llvm::verifyFunction(function, &llvm::errs())) + llvm::report_fatal_error(llvm::Twine("weavec: a guard pass broke ") + + function.getName()); +#endif +} + } // namespace guard llvm::PreservedAnalyses GuardInsertPass::run(llvm::Module &module, @@ -804,8 +823,10 @@ llvm::PreservedAnalyses GuardInsertPass::run(llvm::Module &module, return llvm::PreservedAnalyses::all(); guard::ModuleContext context(module, options); for (llvm::Function &function : module) - if (!guard::skips(function)) + if (!guard::skips(function)) { guard::insertGuards(context, function); + guard::verified(function); + } return llvm::PreservedAnalyses::none(); } @@ -815,6 +836,7 @@ llvm::PreservedAnalyses GuardPrunePass::run(llvm::Function &function, return llvm::PreservedAnalyses::all(); guard::ModuleContext context(*function.getParent(), options); guard::pruneGuards(context, function); + guard::verified(function); return llvm::PreservedAnalyses::none(); } @@ -826,9 +848,10 @@ GuardLoopPass::run(llvm::Function &function, function.getParent()->getDataLayout().getPointerSizeInBits() != 64) return llvm::PreservedAnalyses::all(); guard::ModuleContext context(*function.getParent(), options); - return guard::versionLoops(context, function, analyses) - ? llvm::PreservedAnalyses::none() - : llvm::PreservedAnalyses::all(); + const bool changed = guard::versionLoops(context, function, analyses); + guard::verified(function); + return changed ? llvm::PreservedAnalyses::none() + : llvm::PreservedAnalyses::all(); } llvm::PreservedAnalyses @@ -846,6 +869,7 @@ GuardExpandPass::run(llvm::Module &module, work.push_back(&function); for (llvm::Function *function : work) { guard::expandGuards(context, *function, functions); + guard::verified(*function); functions.invalidate(*function, llvm::PreservedAnalyses::none()); } guard::instrumentGlobals(context); diff --git a/lib/Frontend/GuardPassImpl.h b/lib/Frontend/GuardPassImpl.h index 0fa62cc0..17cba29e 100644 --- a/lib/Frontend/GuardPassImpl.h +++ b/lib/Frontend/GuardPassImpl.h @@ -172,6 +172,8 @@ class ModuleContext { llvm::FunctionCallee rangeFunction(); llvm::FunctionCallee rangeOkFunction(); llvm::FunctionCallee strlenFunction(); + /// RFC 0036 §6: a fortified call writes more than its object size. + llvm::FunctionCallee limitFailFunction(); /// `__weavec_rt_shadow`. llvm::GlobalVariable *shadowDescriptor(); diff --git a/lib/Frontend/InterfaceFacts.cpp b/lib/Frontend/InterfaceFacts.cpp deleted file mode 100644 index af41f93b..00000000 --- a/lib/Frontend/InterfaceFacts.cpp +++ /dev/null @@ -1,237 +0,0 @@ -//===- InterfaceFacts.cpp - A unit's interface facts ----------------------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// - -#include "weavec/Frontend/InterfaceFacts.h" - -#include "weavec/Analysis/Annotations.h" -#include "weavec/Analysis/ClangLocation.h" -#include "weavec/Analysis/KindInference.h" -#include "weavec/Analysis/KindTable.h" -#include "weavec/Analysis/SlotCollector.h" - -#include "clang/AST/Decl.h" -#include "clang/Basic/SourceManager.h" - -#include -#include -#include -#include -#include - -namespace weavec::frontend { - -std::string spellKind(const core::PointerKind &kind) { - return std::string(core::toString(kind.source)) + " " + kind.toString(); -} - -std::optional parseKind(std::string_view text) { - const std::size_t space = text.find(' '); - if (space == std::string_view::npos) - return std::nullopt; - const std::optional source = - core::parseKindSource(text.substr(0, space)); - if (!source) - return std::nullopt; - return core::PointerKind::parse(text.substr(space + 1), *source); -} - -bool DeclaredInterface::empty() const noexcept { - return !result && !ownership && - std::ranges::all_of(params, [](const DeclaredParam ¶m) { - return !param.kind && !param.ownership; - }); -} - -/// The unit's top-level functions by name: the definition, when there is -/// one, else the first declaration. -static std::map> -unitFunctions(const clang::ASTContext &context) { - std::map> functions; - for (const clang::Decl *decl : context.getTranslationUnitDecl()->decls()) { - const auto *function = llvm::dyn_cast(decl); - if (function == nullptr || function->getIdentifier() == nullptr) - continue; - const clang::FunctionDecl *&known = functions[function->getNameAsString()]; - if (known == nullptr || function->doesThisDeclarationHaveABody()) - known = function; - } - return functions; -} - -static bool isPointer(clang::QualType type) { - return type->isPointerType() || type->isArrayType(); -} - -/// A kind some declaration outside the system headers states (§7.2 levels -/// 1 and 2): the declared shape, or `unknown` with a declared `nonnull`. -static std::optional -declaredKind(const analysis::KindEntry *entry) { - if (entry == nullptr) - return std::nullopt; - const auto userLevel = [](const std::optional &level) { - return level && (*level == analysis::KindLevel::Annotation || - *level == analysis::KindLevel::Ecosystem); - }; - const bool shape = entry->hasDeclaredShape() && userLevel(entry->shapeLevel); - const bool nonnull = - entry->declaresNonnull() && userLevel(entry->nullabilityLevel); - if (!shape && !nonnull) - return std::nullopt; - core::PointerKind kind = shape ? entry->kind : core::PointerKind::unknown(); - kind.nullability = - nonnull ? core::Nullability::Nonnull : core::Nullability::Nullable; - return kind.toString(); -} - -/// The ownership annotation `set` states, spelled as the macro. -static std::optional -ownershipOf(const analysis::AnnotationSet &set) { - if (set.owned) - return "WEAVEC_OWNED"; - if (set.borrowed) - return "WEAVEC_BORROWED"; - if (set.mutBorrowed) - return "WEAVEC_MUT"; - if (set.raw) - return "WEAVEC_RAW"; - if (set.retains) - return "WEAVEC_RETAINS"; - if (set.releases) - return "WEAVEC_RELEASES"; - return std::nullopt; -} - -static FunctionInterface functionInterface(const clang::FunctionDecl &function, - const analysis::KindTable &kinds, - const clang::SourceManager &sm) { - FunctionInterface facts; - for (unsigned i = 0; i < function.getNumParams(); ++i) { - const clang::ParmVarDecl *param = function.getParamDecl(i); - const analysis::KindEntry *entry = kinds.param(function, i); - if (!isPointer(param->getType()) || entry == nullptr) { - facts.params.emplace_back(); - continue; - } - facts.params.emplace_back(spellKind(entry->kind)); - } - facts.location = analysis::toCoreLocation(sm, function.getLocation()); - facts.location->opaque = 0; - return facts; -} - -static DeclaredInterface declaredInterface(const clang::FunctionDecl &function, - const analysis::KindTable &kinds) { - DeclaredInterface declared; - const clang::FunctionDecl *latest = function.getMostRecentDecl(); - for (unsigned i = 0; i < latest->getNumParams(); ++i) { - DeclaredParam param; - analysis::AnnotationSet annotations; - for (const clang::FunctionDecl *redecl : function.redecls()) { - if (i >= redecl->getNumParams()) - continue; - const clang::ParmVarDecl *decl = redecl->getParamDecl(i); - annotations.merge(analysis::getAnnotations(*decl)); - if (param.name.empty() && decl->getIdentifier() != nullptr) - param.name = decl->getNameAsString(); - } - if (isPointer(latest->getParamDecl(i)->getType())) - param.kind = declaredKind(kinds.param(function, i)); - param.ownership = ownershipOf(annotations); - declared.params.push_back(std::move(param)); - } - analysis::AnnotationSet result; - for (const clang::FunctionDecl *redecl : function.redecls()) - result.merge(analysis::getAnnotations(*redecl)); - if (function.getReturnType()->isPointerType()) { - declared.result = declaredKind(kinds.result(function)); - declared.ownership = ownershipOf(result); - } - return declared; -} - -/// The declaration a contradiction is reported at: the first one, in source -/// order, with a WeaveC annotation on it or its parameters, else the first -/// one. -static const clang::FunctionDecl * -blamedDeclaration(const clang::FunctionDecl &f, - const clang::SourceManager &sm) { - const clang::FunctionDecl *blamed = nullptr; - for (const clang::FunctionDecl *redecl : f.redecls()) { - bool annotated = analysis::getAnnotations(*redecl).any(); - for (const clang::ParmVarDecl *param : redecl->parameters()) - annotated = annotated || analysis::getAnnotations(*param).any(); - if (annotated && (blamed == nullptr || - sm.isBeforeInTranslationUnit(redecl->getLocation(), - blamed->getLocation()))) - blamed = redecl; - } - return blamed != nullptr ? blamed : f.getFirstDecl(); -} - -/// The unit's function-pointer slots (§9.3), with local slots eliminated. -static SlotFacts slotFacts(const analysis::SlotCollection &slots) { - SlotFacts facts; - facts.rows = slots.exported().rows(); - facts.unit = slots.unit(); - const core::SlotRules &rules = slots.rules(); - facts.defined = rules.defined; - facts.exported = rules.exported; - facts.confinedRecords = rules.confinedRecords; - facts.escapedStatics = rules.escapedStatics; - return facts; -} - -InterfaceFacts collectInterfaceFacts(clang::ASTContext &context, - const FactsInput &input) { - const core::LibrarySpec &library = core::LibrarySpec::shipped(); - const clang::SourceManager &sm = context.getSourceManager(); - // The analysis's own kinds when it passed them (RFC 0030 §1 step 2). - std::shared_ptr built; - const analysis::UnitKinds *unitKinds = input.kinds; - if (unitKinds == nullptr) { - built = analysis::UnitKinds::build(context, library); - unitKinds = built.get(); - } - const analysis::KindTable &kinds = unitKinds->table; - const analysis::SlotCollection &slots = unitKinds->slots; - const auto functions = unitFunctions(context); - - InterfaceFacts facts; - facts.slots = slotFacts(slots); - // RFC 0030 §9.4: what the boundary invariants found, for the - // program-wide propagation. - facts.boundaries = input.exports.boundaries; - for (const auto &[name, exported] : input.exports.functions) { - const auto it = functions.find(name); - if (it != functions.end() && it->second->hasBody()) - facts.functions.emplace( - name, functionInterface(*it->second->getDefinition(), kinds, sm)); - } - for (const std::string &name : input.exports.imports) { - const auto it = functions.find(name); - if (it == functions.end()) - continue; - ImportInterface import{.declared = declaredInterface(*it->second, kinds), - .location = std::nullopt}; - core::SourceLocation location = analysis::toCoreLocation( - sm, blamedDeclaration(*it->second, sm)->getLocation()); - location.opaque = 0; - import.location = std::move(location); - facts.imports.emplace(name, std::move(import)); - } - return facts; -} - -InterfaceFacts collectSlotFacts(clang::ASTContext &context) { - InterfaceFacts facts; - facts.slots = slotFacts( - analysis::SlotCollector(context, core::LibrarySpec::shipped()).collect()); - return facts; -} - -} // namespace weavec::frontend diff --git a/lib/Frontend/ProgramAnalysis.cpp b/lib/Frontend/ProgramAnalysis.cpp deleted file mode 100644 index 8b592c10..00000000 --- a/lib/Frontend/ProgramAnalysis.cpp +++ /dev/null @@ -1,612 +0,0 @@ -//===- ProgramAnalysis.cpp - Whole-program driver -------------------------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// - -#include "weavec/Frontend/ProgramAnalysis.h" - -#include "weavec/Analysis/LedgerAdapter.h" -#include "weavec/Core/Diagnostic.h" -#include "weavec/Core/Effects.h" -#include "weavec/Core/Scc.h" -#include "weavec/Frontend/AnalysisStats.h" -#include "weavec/Frontend/AnalysisSummary.h" -#include "weavec/Frontend/ProgramChecks.h" - -#include "llvm/ADT/STLExtras.h" -#include "llvm/Support/raw_ostream.h" - -#include -#include -#include -#include -#include -#include - -namespace weavec::frontend { - -ProgramAnalysis::ProgramAnalysis(FrontendOptions opts) - : options(std::move(opts)), - // A unit may run again after it reported (to serve a context): its - // summary line is its last run's, printed when the program is done. - unitSummaries(options.summary) { - options.summary = false; -} - -void ProgramAnalysis::addUnit(std::unique_ptr unit) { - units.push_back(Unit{.unit = std::move(unit)}); -} - -void ProgramAnalysis::touchRetainedUnit(ProgramUnit &unit) { - std::erase(retainedUnits, &unit); - retainedUnits.push_back(&unit); - trimRetainedUnits(); -} - -void ProgramAnalysis::trimRetainedUnits() { - while (boundedRetention && retainedUnits.size() > 1) { - auto *oldest = retainedUnits.front(); - retainedUnits.erase(retainedUnits.begin()); - if (oldest->releaseAST() && options.engine.stats) - options.engine.stats->add("unit_evictions"); - } -} - -std::optional -ProgramAnalysis::runUnit(ProgramUnit &unit, const FrontendOptions &overrides) { - FrontendOptions run = options; - run.database = overrides.database; - run.alreadyReported = overrides.alreadyReported; - run.silent = overrides.silent; - run.holdFor = overrides.holdFor; - run.discoverOnly = overrides.discoverOnly; - run.collectInterface = overrides.collectInterface; - if (run.silent) - run.engine.dumpStream = nullptr; - - std::optional result; - run.onResult = [&result](UnitResult r) { result = std::move(r); }; - const bool clean = unit.analyze(run); - if (!result) - return std::nullopt; - touchRetainedUnit(unit); - if (!clean && result->errors == 0) { - // Clang itself reported errors (the unit does not compile). Count one - // so the run is not reported clean. - result->errors = 1; - } - if (!writeAnalysisStats(run.analysisStatsPath, run.engine.stats, false)) - return std::nullopt; - return result; -} - -/// Exports with every summary at the bottom: the start of a fixpoint. -static analysis::UnitExports skeleton(const analysis::UnitExports &exports) { - analysis::UnitExports result = exports; - for (auto &[name, function] : result.functions) - function.effects = core::FunctionEffects{}; - return result; -} - -std::vector> ProgramAnalysis::unitGraph() const { - std::map, std::less<>> definers; - std::map, std::less<>> candidates; - for (unsigned i = 0; i < units.size(); ++i) { - if (!units[i].exports) - continue; - for (const auto &[name, function] : units[i].exports->functions) { - if (function.external) - definers[name].push_back(i); - if (function.addressTaken && !function.typeKey.empty()) - candidates[function.typeKey].push_back(i); - } - } - - std::vector> adjacency(units.size()); - for (unsigned i = 0; i < units.size(); ++i) { - if (!units[i].exports) - continue; - std::vector &edges = adjacency[i]; - for (const std::string &name : units[i].exports->imports) { - if (const auto it = definers.find(name); it != definers.end()) - edges.insert(edges.end(), it->second.begin(), it->second.end()); - } - for (const std::string &key : units[i].exports->indirectTypes) { - if (const auto it = candidates.find(key); it != candidates.end()) { - edges.insert(edges.end(), it->second.begin(), it->second.end()); - for (const unsigned definer : it->second) - adjacency[definer].push_back(i); - } - } - std::erase(edges, i); - std::ranges::sort(edges); - edges.erase(std::ranges::unique(edges).begin(), edges.end()); - } - return adjacency; -} - -analysis::ProgramDatabase ProgramAnalysis::databaseFor( - const std::vector &members) const { - analysis::ProgramDatabase db = settled; - for (const analysis::UnitExports &exports : members) - db.add(exports); - return db; -} - -static void announce(llvm::raw_ostream *dump, const ProgramUnit &unit) { - if (dump != nullptr) - *dump << "unit '" << unit.name() << "':\n"; -} - -void ProgramAnalysis::analyzeAcyclic(unsigned index, Result &result) { - Unit &unit = units[index]; - announce(options.engine.dumpStream, *unit.unit); - - FrontendOptions overrides; - overrides.database = &settled; - overrides.alreadyReported = &unit.reported; - overrides.collectInterface = interfaces; - overrides.holdFor = holdForUnserved(); - std::optional run = runUnit(*unit.unit, overrides); - if (!run) { - result.failed.push_back(unit.unit->name()); - // The discovered view is the best the rest of the program can get. - if (unit.exports) - settled.add(*unit.exports); - return; - } - result.errors += run->errors; - result.warnings += run->warnings; - settled.add(run->exports); - settle(unit, std::move(*run)); -} - -void ProgramAnalysis::settle(Unit &unit, UnitResult run) const { - unit.exports = std::move(run.exports); - unit.held = run.held; - unit.reported.insert(run.reported.begin(), run.reported.end()); - // RFC 0030 §2.6: only the last reporting run publishes. - if (run.ledger && (interfaces || unitSummaries)) - unit.ledger = std::move(run.ledger); - if (run.interface) - unit.interface = std::move(run.interface); -} - -void ProgramAnalysis::widen(analysis::UnitExports &exports, - const analysis::UnitExports &previous) { - // Summaries join by global id, which means the same only under one table. - if (!(exports.globals == previous.globals)) - return; - for (auto &[name, function] : exports.functions) - if (const auto before = previous.functions.find(name); - before != previous.functions.end()) - function.effects = - core::joinEffects(function.effects, before->second.effects); -} - -void ProgramAnalysis::analyzeCyclic(const std::vector &component, - Result &result) { - // Every member starts at the bottom and the group is iterated silently - // until no member's exports change; the reporting pass then sees the - // fixpoint (RFC 0005, *The whole-program algorithm*). - std::vector current; - std::vector broken(component.size(), false); - current.reserve(component.size()); - for (const unsigned member : component) - current.push_back(skeleton(*units[member].exports)); - - // Each member sees the newest exports of every other member; the database - // is rebuilt only after some member's exports changed. - analysis::ProgramDatabase db = databaseFor(current); - - // RFC 0010, *Whole-program fixpoint*: a member whose inputs did not change - // since it last ran produces the same exports, so only the members that - // depend on a changed one are re-run: the unit graph's edges restricted to - // the group (`imports` and `indirectTypes` against the members' - // definitions). A member with no dependency inside the group runs once. - const std::vector> adjacency = unitGraph(); - std::map position; - for (unsigned k = 0; k < component.size(); ++k) - position[component[k]] = k; - std::vector> dependents(component.size()); - for (unsigned k = 0; k < component.size(); ++k) { - for (const unsigned target : adjacency[component[k]]) { - if (const auto it = position.find(target); it != position.end()) - dependents[it->second].push_back(k); - } - } - std::vector dirty(component.size(), true); - - // RFC 0005, *The whole-program algorithm*: a member's new summaries reach - // the members that import them **within the same round** only when those - // come later in the order, so a group visited in the order its units were - // named advances one fact by about two members per round. Visit it in - // reverse post-order of the definer-to-consumer graph instead — the - // schedule every iterative dataflow solver uses — so one round carries a - // fact the length of a call chain. Only the order changes: the round loop, - // the widening and the reporting pass below (which keeps the component's - // own order) are untouched. - const std::vector schedule = [&] { - std::vector visitOrder(component.size()); - std::vector incoming(component.size(), 0); - for (const auto &consumers : dependents) - for (const unsigned consumer : consumers) - ++incoming[consumer]; - std::iota(visitOrder.begin(), visitOrder.end(), 0U); - std::ranges::stable_sort(visitOrder, [&](unsigned a, unsigned b) { - return incoming[a] < incoming[b]; - }); - std::vector seen(component.size(), false); - std::vector postOrder; - postOrder.reserve(component.size()); - std::vector> frames; - for (const unsigned root : visitOrder) { - if (seen[root]) - continue; - seen[root] = true; - frames.emplace_back(root, 0); - while (!frames.empty()) { - const auto [node, next] = frames.back(); - if (next < dependents[node].size()) { - frames.back().second = next + 1; - const unsigned consumer = dependents[node][next]; - if (!seen[consumer]) { - seen[consumer] = true; - frames.emplace_back(consumer, 0); - } - continue; - } - postOrder.push_back(node); - frames.pop_back(); - } - } - return std::vector(postOrder.rbegin(), postOrder.rend()); - }(); - - bool stale = false; - bool changed = true; - for (unsigned round = 0; round < MaxRounds && changed; ++round) { - if (options.engine.stats) - options.engine.stats->add("program_fixpoint_rounds"); - changed = false; - for (const unsigned k : schedule) { - if (broken[k] || !dirty[k]) - continue; - dirty[k] = false; - if (stale) { - // RFC 0020: no analyzer is active between unit runs. Release the - // old summaries before constructing their complete replacement. - db.clear(); - db = databaseFor(current); - stale = false; - } - FrontendOptions overrides; - overrides.database = &db; - overrides.silent = true; - std::optional run = - runUnit(*units[component[k]].unit, overrides); - if (!run) { - broken[k] = true; - result.failed.push_back(units[component[k]].unit->name()); - continue; - } - analysis::UnitExports exports = std::move(run->exports); - // RFC 0011, *Whole-program widening*: a group that oscillates (a - // must-fact one member drops makes another add one back) is joined - // towards what every round agreed on; `join` only ever weakens. - if (round >= WidenAfter) - widen(exports, current[k]); - // (The contexts are served after the fixpoint, `serveContexts`.) - if (!exports.sameFunctionsAs(current[k])) { - changed = true; - stale = true; - for (const unsigned dependent : dependents[k]) - if (!broken[dependent]) - dirty[dependent] = true; - current[k] = std::move(exports); - } - } - } - if (changed) { - std::vector names; - names.reserve(component.size()); - for (const unsigned member : component) - names.push_back(units[member].unit->name()); - result.nonConverging.push_back(std::move(names)); - } - - if (stale) { - db.clear(); - db = databaseFor(current); - } - for (unsigned k = 0; k < component.size(); ++k) { - Unit &unit = units[component[k]]; - if (broken[k]) - continue; - announce(options.engine.dumpStream, *unit.unit); - FrontendOptions overrides; - overrides.database = &db; - overrides.alreadyReported = &unit.reported; - overrides.collectInterface = interfaces; - overrides.holdFor = holdForUnserved(); - std::optional run = runUnit(*unit.unit, overrides); - if (!run) { - broken[k] = true; - result.failed.push_back(unit.unit->name()); - continue; - } - result.errors += run->errors; - result.warnings += run->warnings; - settle(unit, std::move(*run)); - // The unit now owns its completed export; the approximation has no - // remaining reader. Keep failed members' previous exports below. - current[k] = analysis::UnitExports{}; - } - db.clear(); - for (unsigned k = 0; k < component.size(); ++k) { - auto &unit = units[component[k]]; - if (broken[k]) - unit.exports = std::move(current[k]); - settled.add(*unit.exports); - } -} - -void ProgramAnalysis::analyzeComponent(const std::vector &component, - Result &result) { - if (component.size() == 1 && !units[component.front()].exports) - return; - if (component.size() == 1) - analyzeAcyclic(component.front(), result); - else - analyzeCyclic(component, result); -} - -ProgramAnalysis::Result ProgramAnalysis::run() { - Result result; - settled.clear(); - attempted.clear(); - boundedRetention = false; - retainedUnits.clear(); - - // Discovery: one parse, no analysis, for units whose exports are not - // already on hand. - for (Unit &unit : units) { - if (unit.exports) - continue; - FrontendOptions overrides; - overrides.discoverOnly = true; - overrides.collectInterface = interfaces; - if (std::optional run = runUnit(*unit.unit, overrides)) { - unit.exports = std::move(run->exports); - unit.interface = std::move(run->interface); - } else { - result.failed.push_back(unit.unit->name()); - } - } - // RFC 0030 §13.2 step 2 in `weavec --whole-program`. - if (interfaces) - solveDiscoveredSlots(); - settled.programFacts = programFacts; - - // RFC 0020: after discovery, keep one retained AST at a time unless an - // analysis dump needs them all. - boundedRetention = options.engine.dumpStream == nullptr; - // Include ASTs retained by a previous invocation of this ProgramAnalysis. - retainedUnits.clear(); - for (const auto &unit : units) - retainedUnits.push_back(unit.unit.get()); - trimRetainedUnits(); - const std::vector> adjacency = unitGraph(); - for (const std::vector &component : - core::stronglyConnectedComponents(adjacency)) { - analyzeComponent(component, result); - } - serveContexts(result); - if (unitSummaries) { - for (const Unit &unit : units) - if (unit.ledger && !unit.ledger->units.empty()) - printUnitSummary(*unit.ledger, - summaryName(unit.ledger->units.front().source, {}), - options.control); - } - - if (llvm::raw_ostream *dump = options.engine.dumpStream) { - settled.dump(*dump); - if (programFacts) - dumpProgramSlots(programFacts->slots, *dump); - } - return result; -} - -/// Whether `exports` defines the function named `portable`. -static bool definesName(const analysis::UnitExports &exports, - const std::string &portable) { - return std::ranges::any_of(exports.functions, [&](const auto &entry) { - const auto &[name, function] = entry; - return (function.external && name == portable) || - (!function.external && exports.source + "#" + name == portable); - }); -} - -bool ProgramAnalysis::runsDefinitionOf(const std::string &portable) const { - return std::ranges::any_of(units, [&](const Unit &unit) { - return unit.exports && definesName(*unit.exports, portable); - }); -} - -std::function -ProgramAnalysis::holdForUnserved() const { - return [this](const analysis::ContextRequest &request) { - return settled.contextEffects(request) == nullptr && - !attempted.contains(request) && runsDefinitionOf(request.callee); - }; -} - -void ProgramAnalysis::serveContexts(Result &result) { - static constexpr unsigned MaxContextRounds = 8; - // The database of every unit's newest exports. - auto rebuild = [&] { - settled.clear(); - settled.programFacts = programFacts; - for (const Unit &unit : units) - if (unit.exports) - settled.add(*unit.exports); - }; - auto reportingRun = [&](unsigned index, bool hold) -> std::optional { - rebuild(); - Unit &unit = units[index]; - announce(options.engine.dumpStream, *unit.unit); - FrontendOptions overrides; - overrides.database = &settled; - overrides.alreadyReported = &unit.reported; - overrides.collectInterface = interfaces; - if (hold) - overrides.holdFor = holdForUnserved(); - std::optional run = runUnit(*unit.unit, overrides); - if (!run) { - result.failed.push_back(unit.unit->name()); - return std::nullopt; - } - result.errors += run->errors; - result.warnings += run->warnings; - const analysis::UnitExports before = std::move(*unit.exports); - settle(unit, std::move(*run)); - return !unit.exports->sameSummariesAs(before); - }; - const std::vector> adjacency = unitGraph(); - std::vector> dependents(units.size()); - for (unsigned i = 0; i < units.size(); ++i) - for (const unsigned definer : adjacency[i]) - dependents[definer].push_back(i); - const std::vector> order = - core::stronglyConnectedComponents(adjacency); - std::set pending; - for (unsigned round = 0; round < MaxContextRounds; ++round) { - rebuild(); - // Requests no unit has served yet: their definers run, once per - // request (one the definer cannot serve stays unserved). - for (const analysis::ContextRequest &request : settled.requests()) - if (settled.contextEffects(request) == nullptr && - attempted.insert(request).second) - for (unsigned i = 0; i < units.size(); ++i) - if (units[i].exports && - definesName(*units[i].exports, request.callee)) - pending.insert(i); - if (pending.empty()) - break; - std::set next; - for (const std::vector &component : order) - for (const unsigned index : component) { - if (!pending.contains(index) || !units[index].exports) - continue; - const std::optional changed = reportingRun(index, true); - // Its callers use the contexts it now serves, or its new summaries: - // those still held report, the others see what changed. - if (changed && *changed) - for (const unsigned dependent : dependents[index]) - next.insert(dependent); - } - pending = std::move(next); - } - // Every unit still held reports now, with whatever is served. - for (const std::vector &component : order) - for (const unsigned index : component) - if (units[index].held && units[index].exports) - (void)reportingRun(index, false); - rebuild(); -} - -void ProgramAnalysis::solveDiscoveredSlots() { - std::vector members; - bool executable = false; - for (const Unit &unit : units) { - if (!unit.interface) - continue; - ProgramMember member; - member.source = unit.unit->name(); - member.facts.slots = unit.interface->slots; - executable = executable || unit.interface->slots.defined.contains("main"); - members.push_back(std::move(member)); - } - auto facts = std::make_shared(); - facts->slots = solveProgramSlots(members, executable); - facts->boundaries = programBoundaries(members); - programFacts = std::move(facts); -} - -const core::Ledger *ProgramAnalysis::ledgerOf(std::size_t index) const { - return index < units.size() ? units[index].ledger.get() : nullptr; -} - -const InterfaceFacts *ProgramAnalysis::interfaceOf(std::size_t index) const { - return index < units.size() ? units[index].interface.get() : nullptr; -} - -const analysis::UnitExports * -ProgramAnalysis::exportsOf(std::size_t index) const { - return index < units.size() && units[index].exports ? &*units[index].exports - : nullptr; -} - -std::string ProgramAnalysis::unitName(std::size_t index) const { - return index < units.size() ? units[index].unit->name() : std::string(); -} - -bool CompilationDatabaseUnit::run( - clang::tooling::FrontendActionFactory &factory) { - clang::tooling::ClangTool tool(compilations, {source}); - for (const clang::tooling::ArgumentsAdjuster &adjuster : adjusters) - tool.appendArgumentsAdjuster(adjuster); - return tool.run(&factory) == 0; -} - -bool CompilationDatabaseUnit::analyze(const FrontendOptions &options) { - if (!attemptedParse) { - attemptedParse = true; - core::AnalysisTimer timer(options.engine.stats, "parsing"); - clang::tooling::ClangTool tool(compilations, {source}); - for (const auto &adjuster : adjusters) - tool.appendArgumentsAdjuster(adjuster); - if (options.engine.stats) - options.engine.stats->add("unit_parses"); - // (A unit that failed to parse is not analysed: see - // `analyzeTranslationUnit`.) - if (tool.buildASTs(asts) != 0 || asts.empty() || - llvm::any_of(asts, [](const std::unique_ptr &ast) { - return ast->getDiagnostics().hasUncompilableErrorOccurred(); - })) { - asts.clear(); - return false; - } - multipleCommands = asts.size() != 1; - if (multipleCommands) - asts.clear(); - } else if (options.engine.stats) { - options.engine.stats->add("unit_reuses"); - } - if (multipleCommands) - return ProgramUnit::analyze(options); - if (asts.empty()) - return false; - auto &ast = *asts.front(); - auto result = analyzeRetainedUnit(ast, options); - if (options.onResult) - options.onResult(std::move(result)); - return true; -} - -bool CompilationDatabaseUnit::releaseAST() { - if (asts.empty()) - return false; - // ClangTool fills this vector in uninstrumented LLVM. Discard its backing - // storage too, so reparsing cannot reuse ASan-poisoned spare capacity. - decltype(asts){}.swap(asts); - attemptedParse = false; - multipleCommands = false; - return true; -} - -} // namespace weavec::frontend diff --git a/lib/Frontend/ProgramChecks.cpp b/lib/Frontend/ProgramChecks.cpp deleted file mode 100644 index 9de7e083..00000000 --- a/lib/Frontend/ProgramChecks.cpp +++ /dev/null @@ -1,405 +0,0 @@ -//===- ProgramChecks.cpp - What a whole program checks --------------------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// - -#include "weavec/Frontend/ProgramChecks.h" - -#include "weavec/Frontend/ClangDiagnosticSink.h" - -#include "clang/Basic/Diagnostic.h" -#include "clang/Basic/DiagnosticIDs.h" -#include "clang/Basic/DiagnosticOptions.h" -#include "clang/Basic/FileManager.h" -#include "clang/Basic/LangOptions.h" -#include "clang/Basic/SourceManager.h" -#include "clang/Frontend/TextDiagnosticPrinter.h" - -#include "llvm/ADT/IntrusiveRefCntPtr.h" -#include "llvm/ADT/SmallString.h" -#include "llvm/Support/FileSystem.h" -#include "llvm/Support/Path.h" - -#include -#include -#include -#include -#include -#include - -namespace weavec::frontend { - -//===----------------------------------------------------------------------===// -// Paths -//===----------------------------------------------------------------------===// - -/// `path` taken against `base` when relative, without `.` and `..`. -static std::string absoluteFrom(llvm::StringRef path, llvm::StringRef base) { - if (path.empty()) - return {}; - llvm::SmallString<256> absolute(path); - if (!llvm::sys::path::is_absolute(absolute)) { - llvm::SmallString<256> joined(base); - if (joined.empty()) - std::ignore = llvm::sys::fs::current_path(joined); - llvm::sys::path::append(joined, path); - absolute = std::move(joined); - } - llvm::sys::path::remove_dots(absolute, /*remove_dot_dot=*/true); - return absolute.str().str(); -} - -std::string displayPath(llvm::StringRef path, llvm::StringRef base, - llvm::StringRef cwd) { - if (path.empty()) - return {}; - std::string absolute = absoluteFrom(path, base); - llvm::SmallString<256> directory(cwd); - if (directory.empty()) - std::ignore = llvm::sys::fs::current_path(directory); - const std::string root = absoluteFrom(directory, directory); - llvm::StringRef prefix = root; - if (prefix.size() > 1 && prefix.ends_with("/")) - prefix = prefix.drop_back(); - const llvm::StringRef full = absolute; - if (full == prefix) - return "."; - if (prefix == "/") - return full.drop_front().str(); - if (full.starts_with(prefix) && full.size() > prefix.size() && - full[prefix.size()] == '/') - return full.drop_front(prefix.size() + 1).str(); - return absolute; -} - -//===----------------------------------------------------------------------===// -// Slots and boundaries -//===----------------------------------------------------------------------===// - -core::SlotSolution solveProgramSlots(std::span members, - bool executable) { - core::FnSlots constraints; - core::SlotRules rules; - rules.scope = core::SlotScope::Program; - rules.executable = executable; - for (const ProgramMember &member : members) { - const SlotFacts &slots = member.facts.slots; - constraints.merge(core::FnSlots::fromRows(slots.rows)); - rules.defined.insert(slots.defined.begin(), slots.defined.end()); - rules.exported.insert(slots.exported.begin(), slots.exported.end()); - rules.confinedRecords.insert(slots.confinedRecords.begin(), - slots.confinedRecords.end()); - rules.escapedStatics.insert(slots.escapedStatics.begin(), - slots.escapedStatics.end()); - } - return constraints.solve(rules); -} - -std::vector -programBoundaries(std::span members) { - std::vector rows; - for (const ProgramMember &member : members) { - for (analysis::BoundaryRow row : member.facts.boundaries) { - row.unit = member.source; - rows.push_back(std::move(row)); - } - } - std::ranges::sort(rows); - return rows; -} - -void dumpProgramSlots(const core::SlotSolution &slots, llvm::raw_ostream &os) { - os << "program slots:\n"; - for (const core::SlotKey &slot : slots.slots()) { - const std::set &targets = slots.targets(slot); - const std::optional open = slots.openSource(slot); - if (slot.isLocal() || (targets.empty() && !open)) - continue; - os << " " << slot.toString() << ": {"; - bool first = true; - for (const std::string &target : targets) { - os << (first ? "" : ", ") << target; - first = false; - } - os << "} "; - if (open) - os << "open (" << open->detail << ")"; - else - os << "closed"; - os << '\n'; - } -} - -//===----------------------------------------------------------------------===// -// Declarations -//===----------------------------------------------------------------------===// - -/// The member that defines `name` with external linkage, other than -/// `except`. -static std::optional -definerOf(std::span members, const std::string &name, - std::size_t except) { - for (std::size_t m = 0; m < members.size(); ++m) { - if (m == except) - continue; - const auto &functions = members[m].exports.functions; - const auto it = functions.find(name); - if (it != functions.end() && it->second.external) - return m; - } - return std::nullopt; -} - -/// What a definition's summary (RFC 0031 §6) says about argument `param`. -static bool hasEffect(const core::FunctionEffects &effects, - core::PathEffect::Kind kind, std::uint32_t param) { - const core::SummaryPath object = core::SummaryPath::param(param).deref(); - return std::ranges::any_of(effects.effects, [&](const core::PathEffect &e) { - return e.kind == kind && e.path == object; - }); -} - -/// Whether the callee keeps a copy of argument `param` in memory the caller -/// can reach after the call. -static bool storesArgument(const core::FunctionEffects &effects, - std::uint32_t param) { - const core::SummaryPath root = core::SummaryPath::param(param); - if (hasEffect(effects, core::PathEffect::Kind::Escape, param)) - return true; - return std::ranges::any_of(effects.stores, [&](const core::StoreEffect &s) { - return s.value.kind == core::ValueDesc::Kind::Path && s.value.path && - *s.value.path == root; - }); -} - -/// The result's non-null alternatives are all new objects. -static bool returnsOnlyFresh(const core::FunctionEffects &effects) { - bool any = false; - for (const core::ResultEffect &result : effects.results) { - if (result.value.kind == core::ValueDesc::Kind::Null) - continue; - if (result.value.kind != core::ValueDesc::Kind::Fresh) - return false; - any = true; - } - return any; -} - -/// The result may point into memory the caller already had. -static bool returnsBorrowed(const core::FunctionEffects &effects) { - return std::ranges::any_of(effects.results, [](const core::ResultEffect &r) { - return r.value.kind == core::ValueDesc::Kind::Path || - r.value.kind == core::ValueDesc::Kind::Static; - }); -} - -static bool mayReturnNull(const core::FunctionEffects &effects) { - return std::ranges::any_of(effects.results, [](const core::ResultEffect &r) { - return r.value.kind == core::ValueDesc::Kind::Null || r.value.maybeNull; - }); -} - -/// Not named `quoted`: with a `std::string_view` argument, argument-dependent -/// lookup would also find `std::quoted`, which returns a stream manipulator. -static std::string quotedName(std::string_view text) { - return "'" + std::string(text) + "'"; -} - -/// The spelling of parameter `index` in messages. -static std::string parameterName(const DeclaredInterface &declared, - std::size_t index) { - if (index < declared.params.size() && !declared.params[index].name.empty()) - return quotedName(declared.params[index].name); - return "parameter " + std::to_string(index + 1); -} - -namespace { - -/// One contradiction between a declaration and its definition: what the -/// declaration states and what the definition does. -struct Finding { - std::string declared; - std::string done; -}; - -} // namespace - -/// §13.2 step 3 for one import against its definition. -static std::vector -contradictions(const ImportInterface &import, - const analysis::ExportedFunction &function, - const FunctionInterface *definition) { - std::vector findings; - const core::FunctionEffects &summary = function.effects; - const DeclaredInterface &declared = import.declared; - const auto definedKind = - [&](std::size_t index) -> std::optional { - if (definition == nullptr || index >= definition->params.size() || - !definition->params[index]) - return std::nullopt; - auto kind = parseKind(*definition->params[index]); - if (!kind || kind->source != core::KindSource::Declared) - return std::nullopt; - return kind; - }; - for (std::size_t i = 0; i < declared.params.size(); ++i) { - const DeclaredParam ¶m = declared.params[i]; - const auto index = static_cast(i); - const std::string name = parameterName(declared, i); - if (param.ownership == "WEAVEC_BORROWED" || - param.ownership == "WEAVEC_MUT") { - std::string verb; - if (hasEffect(summary, core::PathEffect::Kind::Release, index)) - verb = "frees"; - else if (hasEffect(summary, core::PathEffect::Kind::Move, index)) - verb = "takes ownership of"; - else if (storesArgument(summary, index)) - verb = "stores"; - if (!verb.empty()) { - verb.append(" ").append(name); - findings.push_back( - Finding{.declared = *param.ownership, .done = std::move(verb)}); - } - } - if (!param.kind) - continue; - const auto stated = core::PointerKind::parse(*param.kind); - const auto defined = definedKind(i); - if (!stated || !defined) - continue; - const bool shape = defined->shape != core::PointerShape::Unknown && - stated->shape != core::PointerShape::Unknown && - !stated->sameShape(*defined); - const bool nullability = - defined->nullability == core::Nullability::Nonnull && - stated->nullability == core::Nullability::Nullable; - if (shape || nullability) - findings.push_back( - Finding{.declared = "with " + name + " " + *param.kind, - .done = "declares " + name + " " + defined->toString()}); - } - if (declared.ownership == "WEAVEC_OWNED") { - if (returnsBorrowed(summary)) - findings.push_back(Finding{.declared = "WEAVEC_OWNED", - .done = "returns a borrowed pointer"}); - } else if ((declared.ownership == "WEAVEC_BORROWED" || - declared.ownership == "WEAVEC_MUT") && - returnsOnlyFresh(summary)) { - findings.push_back(Finding{.declared = *declared.ownership, - .done = "returns a fresh allocation"}); - } - if (declared.result) { - const auto stated = core::PointerKind::parse(*declared.result); - if (stated && stated->nullability == core::Nullability::Nonnull && - mayReturnNull(summary)) - findings.push_back(Finding{.declared = "to return " + *declared.result, - .done = "may return null"}); - } - return findings; -} - -static core::SourceLocation absoluteLocation(core::SourceLocation location, - llvm::StringRef base) { - location.file = absoluteFrom(location.file, base); - location.opaque = 0; - return location; -} - -std::vector -verifyDeclarations(std::span members, - llvm::StringRef cwd) { - std::vector diagnostics; - for (std::size_t m = 0; m < members.size(); ++m) { - const ProgramMember &member = members[m]; - for (const auto &[name, import] : member.facts.imports) { - if (import.declared.empty()) - continue; - const std::optional definer = definerOf(members, name, m); - if (!definer) - continue; - const ProgramMember &defining = members[*definer]; - const auto interface = defining.facts.functions.find(name); - const FunctionInterface *definition = - interface == defining.facts.functions.end() ? nullptr - : &interface->second; - const std::vector findings = contradictions( - import, defining.exports.functions.at(name), definition); - if (findings.empty()) - continue; - const std::string file = displayPath(defining.source, defining.cwd, cwd); - for (const Finding &finding : findings) { - core::Diagnostic diagnostic{ - .severity = core::Severity::Error, - .certainty = core::Certainty::Definite, - .id = core::diag::AnnotationMismatch, - .message = quotedName(name) + " is declared " + finding.declared + - " here but its definition in " + quotedName(file) + " " + - finding.done, - .location = import.location - ? absoluteLocation(*import.location, member.cwd) - : core::SourceLocation{}, - .notes = {}, - .fixits = {}}; - if (definition != nullptr && definition->location) - diagnostic.addNote( - "defined here", - absoluteLocation(*definition->location, defining.cwd)); - diagnostics.push_back(std::move(diagnostic)); - } - } - } - return diagnostics; -} - -//===----------------------------------------------------------------------===// -// Printing -//===----------------------------------------------------------------------===// - -struct ProgramDiagnosticPrinter::Engines { - explicit Engines(std::string prefix) - : locatedPrinter(llvm::errs(), options), - located(llvm::makeIntrusiveRefCnt(), options, - &locatedPrinter, /*ShouldOwnClient=*/false), - files(clang::FileSystemOptions{}), sources(located, files), - unlocatedPrinter(llvm::errs(), options), - unlocated(llvm::makeIntrusiveRefCnt(), options, - &unlocatedPrinter, /*ShouldOwnClient=*/false), - locatedSink(located), unlocatedSink(unlocated) { - located.setSourceManager(&sources); - locatedPrinter.BeginSourceFile(language, nullptr); - unlocatedPrinter.setPrefix(std::move(prefix)); - } - Engines(const Engines &) = delete; - Engines &operator=(const Engines &) = delete; - Engines(Engines &&) = delete; - Engines &operator=(Engines &&) = delete; - ~Engines() { locatedPrinter.EndSourceFile(); } - - clang::DiagnosticOptions options; - clang::LangOptions language; - clang::TextDiagnosticPrinter locatedPrinter; - clang::DiagnosticsEngine located; - clang::FileManager files; - clang::SourceManager sources; - clang::TextDiagnosticPrinter unlocatedPrinter; - clang::DiagnosticsEngine unlocated; - ClangDiagnosticSink locatedSink; - ClangDiagnosticSink unlocatedSink; -}; - -ProgramDiagnosticPrinter::ProgramDiagnosticPrinter(std::string prefix) - : engines(std::make_unique(std::move(prefix))) {} - -ProgramDiagnosticPrinter::~ProgramDiagnosticPrinter() = default; - -void ProgramDiagnosticPrinter::report(const core::Diagnostic &diagnostic) { - if (diagnostic.location.isValid()) - engines->locatedSink.report(diagnostic); - else - engines->unlocatedSink.report(diagnostic); -} - -} // namespace weavec::frontend diff --git a/lib/Frontend/UnsafeRegions.cpp b/lib/Frontend/UnsafeRegions.cpp index e3362c2d..88bf83de 100644 --- a/lib/Frontend/UnsafeRegions.cpp +++ b/lib/Frontend/UnsafeRegions.cpp @@ -8,8 +8,6 @@ #include "weavec/Frontend/UnsafeRegions.h" -#include "weavec/Analysis/Annotations.h" - #include "clang/AST/ASTConsumer.h" #include "clang/AST/ASTContext.h" #include "clang/AST/Attr.h" @@ -106,7 +104,7 @@ class UnsafeVisitor final : public clang::RecursiveASTVisitor { bool VisitAttributedStmt(clang::AttributedStmt *statement) { for (const clang::Attr *attribute : statement->getAttrs()) if (const auto *annotate = llvm::dyn_cast(attribute); - annotate && annotate->getAnnotation() == analysis::spelling::Unsafe) + annotate && annotate->getAnnotation() == UnsafeAnnotation) add(statement->getSubStmt()->getSourceRange()); return true; } @@ -119,7 +117,7 @@ class UnsafeVisitor final : public clang::RecursiveASTVisitor { template static bool isUnsafe(Range attributes) { return std::ranges::any_of(attributes, [](const clang::AnnotateAttr *a) { - return a->getAnnotation() == analysis::spelling::Unsafe; + return a->getAnnotation() == UnsafeAnnotation; }); } @@ -183,7 +181,7 @@ class UnsafeRegionCollector final : public clang::ASTConsumer { return std::ranges::any_of(function.specific_attrs(), [](const clang::AnnotateAttr *attribute) { return attribute->getAnnotation() == - analysis::spelling::Unsafe; + UnsafeAnnotation; }); } diff --git a/resources/include/weavec.h b/resources/include/weavec.h index 41aa960f..c7701cfe 100644 --- a/resources/include/weavec.h +++ b/resources/include/weavec.h @@ -6,46 +6,29 @@ |* |*===----------------------------------------------------------------------===*| |* -|* Lightweight annotations that let you guide WeaveC's ownership inference -|* where it cannot prove safety on its own. They are inputs to WeaveC's -|* advisory analysis (`weavec`, `weavec-cc -fweavec-diagnose`) and do not -|* change the generated code, except WEAVEC_UNSAFE: weavec-cc guards every -|* memory access at run time outside an unsafe region, whatever the other -|* annotations say. Every macro expands to nothing on compilers that do not -|* understand Clang's `annotate` attribute, so annotated code stays buildable -|* with any C toolchain. +|* weavec-cc guards every memory access of the C it compiles. WEAVEC_UNSAFE +|* marks the code that must not be guarded: a function (placed before its +|* definition) or a block (placed before a compound statement) whose accesses +|* read past an object on purpose (a word-at-a-time scan, a conservative +|* stack scan). The macro expands to nothing on compilers that do not +|* understand Clang's `annotate` attribute, so the code stays buildable with +|* any C toolchain. |* |* #include |* -|* struct buffer *WEAVEC_OWNED buffer_new(size_t n); -|* void buffer_free(struct buffer *WEAVEC_OWNED b); -|* size_t buffer_len(const struct buffer *WEAVEC_BORROWED b); -|* void buffer_push(struct buffer *WEAVEC_MUT b, int v); -|* void *WEAVEC_RAW map_pages(size_t n); // no ownership guarantee -|* struct node *WEAVEC_NULLABLE find(struct tree *t, int key); -|* void log_line(const char *WEAVEC_NONNULL msg); -|* -|* typedef void (*dtor_t)(void *WEAVEC_OWNED); // callbacks, too -|* -|* struct obj { int WEAVEC_REFCOUNT rc; ... }; // reference counting -|* struct obj *obj_ref(struct obj *WEAVEC_RETAINS o); -|* void obj_unref(struct obj *WEAVEC_RELEASES o); -|* FILE *WEAVEC_OWNED WEAVEC_OWNED_BY(fclose) open_log(void); -|* -|* WEAVEC_UNSAFE void poke(void) { ... } // the body is an unsafe region +|* WEAVEC_UNSAFE void scan(void) { ... } // the body is an unsafe region |* WEAVEC_UNSAFE { ... } // or just a block |* -|* void fill(char *WEAVEC_COUNTED_BY(len) buf, size_t len); // extents -|* size_t sum(const int *WEAVEC_ENDED_BY(end) p, const int *end); -|* size_t name_len(const char *WEAVEC_STRING name); +|* A function marked no_sanitize("address") is an unsafe region too, as it is +|* to AddressSanitizer. |* \*===----------------------------------------------------------------------===*/ #ifndef WEAVEC_H #define WEAVEC_H -#define WEAVEC_H_VERSION_MAJOR 0 -#define WEAVEC_H_VERSION_MINOR 10 +#define WEAVEC_H_VERSION_MAJOR 1 +#define WEAVEC_H_VERSION_MINOR 0 #if defined(__has_attribute) #if __has_attribute(annotate) @@ -57,144 +40,14 @@ #define WEAVEC_ANNOTATE_(text) #endif -/* The spellings below must match weavec::analysis::spelling. */ - -/** - * The pointer uniquely owns its referent and must release it exactly once. - * On a struct field, the object owns the referent: releasing the object - * while the field still holds one is reported as a leak. - */ -#define WEAVEC_OWNED WEAVEC_ANNOTATE_("weavec.owned") - -/** A shared, read-only borrow; the referent outlives the borrow. */ -#define WEAVEC_BORROWED WEAVEC_ANNOTATE_("weavec.borrowed") - -/** An exclusive, mutable borrow; no other access may occur while it lives. */ -#define WEAVEC_MUT WEAVEC_ANNOTATE_("weavec.mut_borrowed") - -/** - * A raw pointer: no ownership guarantee at all. The analysis reports - * dereferencing or releasing it outside a WEAVEC_UNSAFE function or block. - * Copying, comparing and passing it to another WEAVEC_RAW parameter are - * fine. - */ -#define WEAVEC_RAW WEAVEC_ANNOTATE_("weavec.raw") +/* The spelling must match weavec::frontend::UnsafeAnnotation. */ /** * Makes a function body (when placed before its definition) or a block (when * placed before a compound statement) an unsafe region: weavec-cc does not - * guard its memory accesses or check its array indexes at run time (as for a - * function marked no_sanitize("address")), and the analysis trusts its raw - * pointers and its spatial and null operations. The analysis still tracks - * temporal state (frees, moves) there and still reports possible temporal - * findings and definite violations; what the region does to the surrounding - * code is checked there. The only annotation that changes the generated - * code. Keep regions small and document the invariant that makes the code - * sound. + * guard its memory accesses or check its array indexes at run time. Keep + * regions small and document the invariant that makes the code sound. */ #define WEAVEC_UNSAFE WEAVEC_ANNOTATE_("weavec.unsafe") -/** - * The pointer may be null. On a parameter, the body treats it as possibly - * null until it tests it, and callers may pass null; on a return type, - * callers must test the result before dereferencing it; on a variable or - * field, every load is treated as possibly null. Does not change ownership. - */ -#define WEAVEC_NULLABLE WEAVEC_ANNOTATE_("weavec.nullable") - -/** - * The pointer is never null. On a parameter, a definitely null argument is - * reported; on a return type, the result needs no test; on a variable or - * field, loads are never reported. Does not change ownership. - */ -#define WEAVEC_NONNULL WEAVEC_ANNOTATE_("weavec.nonnull") - -/** - * On a pointer parameter: the callee takes a reference on the argument's - * object (increments its reference count). The caller's pointer gains a - * share, which the next copy of it carries away (`q = obj_ref(p)` leaves - * `p` and `q` each holding one). Does not consume the argument. - */ -#define WEAVEC_RETAINS WEAVEC_ANNOTATE_("weavec.retains") - -/** - * On a pointer parameter: the callee releases one reference on the - * argument's object. The argument's name is dead afterwards, as after - * `free`; other pointers holding their own reference are untouched. - */ -#define WEAVEC_RELEASES WEAVEC_ANNOTATE_("weavec.releases") - -/** - * On an integer struct field: the field is a reference count. A reference - * taken through it (`o->rc++`, a WEAVEC_RETAINS callee) and never released - * is reported as a leak, even when no releasing function is in view. - */ -#define WEAVEC_REFCOUNT WEAVEC_ANNOTATE_("weavec.refcount") - -/** - * Next to WEAVEC_OWNED on a parameter or return type: the referent must be - * released with `f` (`WEAVEC_OWNED WEAVEC_OWNED_BY(fclose)`), and releasing - * it with anything else is reported as a mismatched release. - */ -#define WEAVEC_OWNED_BY(f) WEAVEC_ANNOTATE_("weavec.family." #f) - -/** - * On a pointer parameter or field: at least `n` elements (bytes for void - * and character pointees) are accessible; `n` names a sibling parameter or - * field, in any position (`void fill(char *WEAVEC_COUNTED_BY(len) buf, - * size_t len)`, `struct buf { char *WEAVEC_COUNTED_BY(cap) data; size_t - * cap; }`). The analysis checks the kind at every call and every store and - * proves accesses through the pointer against it. weavec-cc guards those - * accesses against the object the pointer points into, not against `n`. - */ -#define WEAVEC_COUNTED_BY(n) WEAVEC_ANNOTATE_("weavec.counted_by." #n) - -/** - * The same as WEAVEC_COUNTED_BY(n): `n` counts elements (bytes for void and - * character pointees), unlike Clang's byte-counting `sized_by`. New code - * should use WEAVEC_COUNTED_BY. - */ -#define WEAVEC_SIZED_BY(n) WEAVEC_ANNOTATE_("weavec.sized_by." #n) - -/** - * On a pointer parameter or field: `[p, q)` lies in one object, where `q` - * names a sibling pointer parameter or field (`size_t sum(const int - * *WEAVEC_ENDED_BY(end) p, const int *end)`). - */ -#define WEAVEC_ENDED_BY(q) WEAVEC_ANNOTATE_("weavec.ended_by." #q) - -/** - * On a pointer parameter, field or return type: the pointer is - * NUL-terminated within its object (a zero element lies at or after it). - */ -#define WEAVEC_STRING WEAVEC_ANNOTATE_("weavec.string") - -/** Non-zero when the translation unit is being processed by WeaveC. */ -#if defined(__WEAVEC__) -#define WEAVEC_ENABLED 1 -#else -#define WEAVEC_ENABLED 0 -#endif - -/** - * States that `expr` holds here, and the analysis assumes it from here on, - * as if the code below were inside `if (expr)`: `WEAVEC_ASSUME(len <= cap)` - * lets the checker prove an access in bounds when the invariant that makes - * it so is not visible in the function. When the analysis refutes `expr`, - * that is a `contradicted-assumption`. Nothing checks it at run time, and - * no guard depends on it: it is a statement to the analysis only. `expr` - * must be side-effect free; under other compilers it is an unevaluated - * operand and is not compiled at all. - */ -#if WEAVEC_ENABLED -WEAVEC_ANNOTATE_("weavec.assume") -static inline void weavec_assume_(int condition) { - (void)condition; -} -#define WEAVEC_ASSUME(expr) weavec_assume_((expr) != 0) -#else -/* Unevaluated, so `expr` is neither run nor an unused-variable warning. */ -#define WEAVEC_ASSUME(expr) ((void)sizeof((expr) != 0)) -#endif - #endif /* WEAVEC_H */ diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index dd900e39..e5bc9b11 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -2,9 +2,10 @@ # lib/weavec next to include/weavec.h: # # libweavec_rt.a the runtime every enforcing link carries: the arena -# allocator and its shadow memory, the guards' slow -# paths, the checked library calls, frame and global -# registration, and the reports +# allocator and its shadow memory, the sweep (RFC 0036), +# the guards' slow paths, frames, the checked library +# calls, global registration, ASan's interface and the +# reports # libweavec_alloc.a malloc, free and the rest of the standard allocation # functions over the arena; left out of a link whose # program defines the allocator itself (RFC 0032, 2.6) @@ -17,8 +18,16 @@ if(NOT CMAKE_C_COMPILER_ID MATCHES "Clang") message(FATAL_ERROR "The WeaveC runtime must be built with Clang (CC).") endif() -add_library(weavec_rt STATIC weavec_alloc.c weavec_guard.c weavec_libc.c - weavec_owner.c weavec_report.c weavec_rt.h) +add_library( + weavec_rt STATIC + weavec_alloc.c + weavec_asan.c + weavec_guard.c + weavec_libc.c + weavec_owner.c + weavec_report.c + weavec_sweep.c + weavec_rt.h) add_library(weavec_alloc STATIC weavec_malloc.c weavec_rt.h) foreach(_weavec_runtime IN ITEMS weavec_rt weavec_alloc) diff --git a/runtime/test/rt_test.c b/runtime/test/rt_test.c index c0cb6982..cdf77924 100644 --- a/runtime/test/rt_test.c +++ b/runtime/test/rt_test.c @@ -6,7 +6,7 @@ |* |*===----------------------------------------------------------------------===*| |* -|* RFC 0035, section 5. One program, linked with libweavec_rt.a and +|* RFC 0035, section 5, and RFC 0036. One program, linked with libweavec_rt.a and |* libweavec_alloc.a, so `malloc` here is the arena's. Each test is a |* function; a test that must stop the program runs in a child process and |* the parent checks how it died. Run without arguments for every test, or @@ -31,6 +31,7 @@ #include #include #include +#include #include #if defined(__APPLE__) @@ -196,42 +197,52 @@ static void testShadowEncoding(void) { /* The program's path, for a test that runs in a copy of itself. */ static const char *program; -/* The quarantine budget is read when the program starts: a test that needs - * none runs in a copy started with WEAVEC_RT_QUARANTINE=0. True in that - * copy; in this one, runs it and checks that it passed. */ -static int withoutQuarantine(void) { - const char *budget = getenv("WEAVEC_RT_QUARANTINE"); - pid_t child; - int status = 0; - if (budget != NULL && strcmp(budget, "0") == 0) - return 1; - child = fork(); - if (child == 0) { - char *arguments[3]; - arguments[0] = (char *)program; - arguments[1] = (char *)current; - arguments[2] = NULL; - setenv("WEAVEC_RT_QUARANTINE", "0", 1); - execv(program, arguments); - _exit(127); - } - CHECK(child > 0 && waitpid(child, &status, 0) == child && - WIFEXITED(status) && WEXITSTATUS(status) == 0); - return 0; +/* RFC 0036 section 3.4: an address no word of the program holds. */ +static const uintptr_t Hidden = 0x5a5a5a5a5a5a5a5aULL; + +/* Allocates `n` bytes, writes them, releases them, and returns the + * address hidden, so that no pointer to the block is left once it returns + * (its frame is below the stack pointer, which a sweep does not scan). */ +static __attribute__((noinline)) uintptr_t releaseHere(size_t n) { + unsigned char *volatile p = (unsigned char *)malloc(n); + uintptr_t hidden; + memset(p, 0xff, n); + hidden = (uintptr_t)p ^ Hidden; + free(p); + CHECK(!addressable(p, 1)); + p = NULL; + return hidden; +} + +/* Releases the block 16 KiB below the caller, where scrubStack clears + * whatever the allocator's calls left behind. */ +static __attribute__((noinline)) uintptr_t releasedBlock(size_t n) { + volatile unsigned char depth[16384]; + depth[0] = 0; + return releaseHere(n) + depth[0]; +} + +/* Clears the stack below the caller, where released frames left copies. */ +static __attribute__((noinline)) void scrubStack(void) { + unsigned char scratch[65536]; + memset(scratch, 0, sizeof scratch); + /* Keeps the optimiser from removing the stores to a dead array. */ + __asm__ volatile("" : : "r"(scratch) : "memory"); } static void testReuseIsZero(void) { - /* With no quarantine a released slot comes back at once, zeroed. */ + /* A released slot comes back after a sweep, zeroed. */ unsigned round; - if (!withoutQuarantine()) - return; - for (round = 0; round < 64; ++round) { - unsigned char *p = (unsigned char *)opaque(malloc(200)); + for (round = 0; round < 16; ++round) { + unsigned char *p; unsigned i; + (void)releasedBlock(200); + scrubStack(); + weavecRtSweep(); + p = (unsigned char *)opaque(malloc(200)); for (i = 0; i < 200; ++i) CHECK(p[i] == 0); CHECK(addressable(p, 200) && !addressable(p, 201)); - memset(p, 0xff, 200); free(p); } } @@ -298,33 +309,175 @@ static void testRealloc(void) { #endif } -static void testQuarantine(void) { - /* With no budget the newest release is the first to be reused. */ - enum { Blocks = 64 }; - void *blocks[Blocks]; +/* Allocates blocks of `n` bytes until one is `target` (at most 256): that + * block, or null; the others are released again. A sweep hands released + * slots out in address order, so the target need not be the first. Not + * inlined: its array would sit, uninitialised, in the frame the sweep scans. */ +static __attribute__((noinline)) unsigned char *reuse(uintptr_t target, + size_t n) { + unsigned char *taken[256]; + unsigned char *found = NULL; + unsigned count = 0; + unsigned i; + while (count < 256 && found == NULL) { + unsigned char *p = (unsigned char *)opaque(malloc(n)); + if ((uintptr_t)p == target) + found = p; + else + taken[count++] = p; + } + for (i = 0; i < count; ++i) + free(taken[i]); + return found; +} + +static void testSweepRecycles(void) { + /* A released block nothing points to is reused after a sweep, and its + * bytes are zero again. (A size class no other test or library call + * uses: a stale copy of an earlier block's address, left in a frame the + * scan reads, would keep the slot.) */ + volatile uintptr_t hidden = releasedBlock(2700); + unsigned char *again; unsigned i; - void *again; - if (!withoutQuarantine()) + int zero = 1; + scrubStack(); + weavecRtSweep(); + again = reuse(hidden ^ Hidden, 2700); + CHECK(again != NULL); + if (again == NULL) return; - for (i = 0; i < Blocks; ++i) - blocks[i] = opaque(malloc(1000)); - for (i = 0; i < Blocks; ++i) - free(blocks[i]); - again = opaque(malloc(1000)); - CHECK(addressable(again, 1000)); + CHECK(addressable(again, 2700)); + for (i = 0; i < 2700; ++i) + zero &= again[i] == 0; + CHECK(zero); free(again); } static void testQuarantineHolds(void) { - /* Under the default budget a released block stays released. */ - void *p = opaque(malloc(1000)); + /* A released block a variable still points to is never reused, however + * many sweeps run. */ + void *volatile p = opaque(malloc(1000)); unsigned i; free(p); - for (i = 0; i < 100; ++i) - free(opaque(malloc(1000))); + for (i = 0; i < 100; ++i) { + void *q = opaque(malloc(1000)); + CHECK(q != p); + free(q); + weavecRtSweep(); + } CHECK(!addressable(p, 1)); } +static void *volatile *heapHolder; + +static void testSweepFollowsTheHeap(void) { + /* A pointer stored in a live heap block keeps its target; once it is + * overwritten, the target is reused. (A size class of its own, as + * above.) */ + volatile uintptr_t hidden = releasedBlock(5300); + heapHolder = (void *volatile *)malloc(64); + heapHolder[3] = (void *)(hidden ^ Hidden); + scrubStack(); + weavecRtSweep(); + { + void *q = opaque(malloc(5300)); + CHECK((uintptr_t)q != (hidden ^ Hidden)); + CHECK(!addressable(heapHolder[3], 1)); + free(q); + } + heapHolder[3] = NULL; + scrubStack(); + weavecRtSweep(); + { + unsigned char *q = reuse(hidden ^ Hidden, 5300); + CHECK(q != NULL); + free(q); + } + free((void *)heapHolder); + heapHolder = NULL; +} + +static void *volatile threadHeld; +static volatile int threadRelease; + +static void *holder(void *argument) { + /* Keeps the block in a local (a register or the stack) until told. */ + void *volatile mine = argument; + while (!__atomic_load_n(&threadRelease, __ATOMIC_ACQUIRE)) + sched_yield(); + mine = NULL; + return mine; +} + +static uint64_t monotonic(void) { + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + return (uint64_t)now.tv_sec * 1000000000ULL + (uint64_t)now.tv_nsec; +} + +static void testPacing(void) { + /* RFC 0036 section 3.3: past the soft budget (16 MiB), a release sweeps + * only once eight times the last sweep's duration has passed; the hard + * budget (256 MiB) sweeps at once. */ + enum { Block = 60000, Count = 400 }; + static void *blocks[Count]; + unsigned long long before; + unsigned i; + weavecRtSweep(); + before = weavecRtStats[WeavecRtStatSweeps]; + for (i = 0; i < Count; ++i) + blocks[i] = opaque(malloc(Block)); + /* As if the last sweep had just taken 1,000 seconds. */ + weavecRtSwept(monotonic(), 1000000000000ULL); + for (i = 0; i < Count; ++i) + free(blocks[i]); + CHECK(weavecRtStats[WeavecRtStatSweeps] == before); + /* As if it had taken no time: the next release sweeps. */ + weavecRtSwept(monotonic(), 0); + free(opaque(malloc(Block))); + CHECK(weavecRtStats[WeavecRtStatSweeps] == before + 1); +} + +static void testLibraryDataIsScanned(void) { + /* A block only the C library's own data points to (the buffer stdin was + * given lives in its FILE, in the library's data; on Darwin, in the + * dyld shared cache, scanned through the image table) is not reused. */ + enum { Bytes = 7000 }; + char *buffer = (char *)opaque(malloc(Bytes)); + volatile uintptr_t hidden = (uintptr_t)buffer ^ Hidden; + unsigned char *again; + CHECK(setvbuf(stdin, buffer, _IOFBF, Bytes) == 0); + buffer = NULL; + free((void *)(hidden ^ Hidden)); + scrubStack(); + weavecRtSweep(); + again = reuse(hidden ^ Hidden, Bytes); + CHECK(again == NULL); + (void)setvbuf(stdin, NULL, _IONBF, 0); +} + +static void testSweepStopsThreads(void) { + /* A pointer only another thread holds keeps its block. */ + volatile uintptr_t hidden = releasedBlock(48); + pthread_t thread; + CHECK(pthread_create(&thread, NULL, holder, (void *)(hidden ^ Hidden)) == 0); + scrubStack(); + weavecRtSweep(); + { + unsigned i; + void *blocks[8]; + for (i = 0; i < 8; ++i) { + blocks[i] = opaque(malloc(48)); + CHECK((uintptr_t)blocks[i] != (hidden ^ Hidden)); + } + for (i = 0; i < 8; ++i) + free(blocks[i]); + } + __atomic_store_n(&threadRelease, 1, __ATOMIC_RELEASE); + CHECK(pthread_join(thread, NULL) == 0); + threadRelease = 0; +} + static void doubleFree(void) { void *p = opaque(malloc(24)); free(p); @@ -528,12 +681,12 @@ static void testStrings(void) { static void copyTooLong(void) { char *p = (char *)opaque(malloc(8)); - (void)__weavec_rt_strcpy(p, "eight chars", &trapSite); + (void)__weavec_rt_strcpy(p, "eight chars", (size_t)-1, &trapSite); } static void printTooLong(void) { char *p = (char *)opaque(malloc(8)); - (void)__weavec_rt_sprintf(&trapSite, p, "%d-%d", 12345, 6789); + (void)__weavec_rt_sprintf(&trapSite, (size_t)-1, p, "%d-%d", 12345, 6789); } static void compareTooFar(void) { @@ -552,24 +705,24 @@ static void printUnterminated(void) { char out[64]; char *p = (char *)opaque(malloc(4)); memcpy(p, "abcd", 4); - (void)__weavec_rt_snprintf(&trapSite, out, sizeof out, "[%s]", p); + (void)__weavec_rt_snprintf(&trapSite, (size_t)-1, out, sizeof out, "[%s]", p); } static void testCheckedCalls(void) { char *p = (char *)opaque(malloc(16)); char *q; - CHECK(__weavec_rt_strcpy(p, "short", &trapSite) == p); + CHECK(__weavec_rt_strcpy(p, "short", (size_t)-1, &trapSite) == p); CHECK(strcmp(p, "short") == 0); - CHECK(__weavec_rt_strcat(p, "er", &trapSite) == p); + CHECK(__weavec_rt_strcat(p, "er", (size_t)-1, &trapSite) == p); CHECK(strcmp(p, "shorter") == 0); - q = __weavec_rt_stpcpy(p, "abc", &trapSite); + q = __weavec_rt_stpcpy(p, "abc", (size_t)-1, &trapSite); CHECK(q == p + 3 && *q == 0); - CHECK(__weavec_rt_sprintf(&trapSite, p, "%d", 42) == 2); + CHECK(__weavec_rt_sprintf(&trapSite, (size_t)-1, p, "%d", 42) == 2); CHECK(strcmp(p, "42") == 0); /* A size larger than the buffer is no error when the output fits. */ - CHECK(__weavec_rt_snprintf(&trapSite, p, 1000, "%s", "fits") == 4); + CHECK(__weavec_rt_snprintf(&trapSite, (size_t)-1, p, 1000, "%s", "fits") == 4); CHECK(strcmp(p, "fits") == 0); - CHECK(__weavec_rt_snprintf(&trapSite, p, 4, "%s", "truncated") == 9); + CHECK(__weavec_rt_snprintf(&trapSite, (size_t)-1, p, 4, "%s", "truncated") == 9); CHECK(strcmp(p, "tru") == 0); /* A search or comparison checks only what it read. */ memcpy(p, "ab\nc", 4); @@ -603,7 +756,7 @@ static void testCheckedCalls(void) { char out[16]; char *word = (char *)opaque(malloc(4)); memcpy(word, "abcd", 4); - CHECK(__weavec_rt_snprintf(&trapSite, out, sizeof out, "%.4s|%d|%s", + CHECK(__weavec_rt_snprintf(&trapSite, (size_t)-1, out, sizeof out, "%.4s|%d|%s", word, 7, (const char *)NULL) > 0); CHECK(strncmp(out, "abcd|7|", 7) == 0); free(word); @@ -655,6 +808,59 @@ static void testUnpoisonStack(void) { CHECK(addressable((void *)poisoned, 64)); } +/*===-- ASan's interface (RFC 0036 section 5) ------------------------------===*/ + +void __asan_poison_memory_region(const volatile void *address, size_t size); +void __asan_unpoison_memory_region(const volatile void *address, size_t size); +int __asan_address_is_poisoned(const volatile void *address); +void *__asan_region_is_poisoned(void *address, size_t size); + +static void testAsanInterface(void) { + char *pool = (char *)opaque(malloc(256)); + __asan_poison_memory_region(pool + 64, 128); + CHECK(__asan_address_is_poisoned(pool + 64)); + CHECK(!__asan_address_is_poisoned(pool + 63)); + CHECK(__asan_region_is_poisoned(pool, 100) == pool + 64); + CHECK(shadowAt(pool + 64) == WeavecRtShadowUser); + __asan_unpoison_memory_region(pool + 64, 40); + CHECK(!__asan_address_is_poisoned(pool + 103)); + CHECK(__asan_address_is_poisoned(pool + 104)); + __asan_unpoison_memory_region(pool + 64, 128); + CHECK(__asan_region_is_poisoned(pool, 256) == NULL); + free(pool); +} + +/*===-- Number parsing and fortified limits (RFC 0036 section 6) ----------===*/ + +static void parseUnterminated(void) { + char *digits = (char *)opaque(malloc(3)); + memcpy(digits, "123", 3); + (void)__weavec_rt_strtol(digits, NULL, 10, &trapSite); +} + +static void copyPastLimit(void) { + char *p = (char *)opaque(malloc(32)); + (void)__weavec_rt_strcpy(p, "more than eight", 8, &trapSite); +} + +static void testParsersAndLimits(void) { + char *text = (char *)opaque(malloc(3)); + char *end = NULL; + char line[256]; + memcpy(text, "12,", 3); + /* strtol reads up to the comma: no terminator needed. */ + CHECK(__weavec_rt_strtol(text, &end, 10, &trapSite) == 12); + CHECK(end == text + 2); + CHECK(__weavec_rt_atoi(text, &trapSite) == 12); + CHECK(__weavec_rt_strtod(text, NULL, &trapSite) == 12.0); + memcpy(text, " -x", 3); + CHECK(__weavec_rt_strtol(text, &end, 10, &trapSite) == 0 && end == text); + free(text); + CHECK(trapsIn(parseUnterminated)); + firstLine(copyPastLimit, line, sizeof line); + CHECK(strstr(line, "weavec: intra-object-overflow at rt_test.c:") == line); +} + static void testMappings(void) { void *map = __weavec_rt_mmap(NULL, 1 << 16, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0, NULL); @@ -759,8 +965,12 @@ static const struct { {"reuse-is-zero", testReuseIsZero}, {"alignment", testAlignment}, {"realloc", testRealloc}, - {"quarantine", testQuarantine}, + {"sweep-recycles", testSweepRecycles}, {"quarantine-holds", testQuarantineHolds}, + {"sweep-follows-the-heap", testSweepFollowsTheHeap}, + {"sweep-stops-threads", testSweepStopsThreads}, + {"pacing", testPacing}, + {"library-data-is-scanned", testLibraryDataIsScanned}, {"invalid-releases", testInvalidReleases}, {"huge", testHuge}, {"foreign", testForeign}, @@ -770,6 +980,8 @@ static const struct { {"globals", testGlobals}, {"allocas", testAllocas}, {"unpoison-stack", testUnpoisonStack}, + {"asan-interface", testAsanInterface}, + {"parsers-and-limits", testParsersAndLimits}, {"mappings", testMappings}, {"array-bounds", testArrayBounds}, {"threads", testThreads}, diff --git a/runtime/weavec_alloc.c b/runtime/weavec_alloc.c index 1359baeb..d2cf8479 100644 --- a/runtime/weavec_alloc.c +++ b/runtime/weavec_alloc.c @@ -24,8 +24,11 @@ |* A request takes a slot of the smallest class strictly larger than it, so |* at least one byte after every object belongs to no object. Slot words, |* the free lists and the quarantine queues live in the metadata, never in -|* freed memory. Released slots wait in a quarantine before they are reused; -|* requests no class can hold are mapped on their own and kept in a table. +|* freed memory. RFC 0036 section 3: a released slot waits in its class's +|* quarantine until a sweep (weavec_sweep.c) finds no pointer to it; only +|* then does it join the free list. Requests no class can hold are mapped on +|* their own and kept in a table; a released one stays reserved, without +|* access, until a sweep finds no pointer to it either. |* |* The allocator needs nothing from the C library's allocator, so it can |* replace it: it maps its own memory and takes its own locks. @@ -42,6 +45,7 @@ #include #include #include +#include #include #if defined(__APPLE__) @@ -80,6 +84,8 @@ typedef struct { /* The quarantine queue: slot index + 1 of its oldest and newest slot. */ uint32_t deadHead; uint32_t deadTail; + /* The sweep's marks, one bit per slot, mapped on the first sweep. */ + uint64_t *marks; /* Bytes of the region, and of its metadata, that are accessible. */ uintptr_t committed; uintptr_t metaCommitted; @@ -95,6 +101,8 @@ typedef struct { size_t mapBytes; /* 0 while live; else the order of its release. */ unsigned long long dead; + /* A pointer to the released block was found by the running sweep. */ + int marked; } HugeBlock; static WeavecRtLock initLock; @@ -105,18 +113,28 @@ static size_t hugeCapacity; /* The lowest and highest address a huge block has had: a quick rejection. */ static uintptr_t hugeLow = ~(uintptr_t)0; static uintptr_t hugeHigh; -/* Released huge blocks stay mapped, without access, until this many newer - * ones were released: a stale pointer then finds a dead block, and nothing +/* Released huge blocks stay reserved, without access, until a sweep finds + * no pointer to them: a stale pointer then finds a dead block, and nothing * else can be mapped there. */ -enum { HugeTombstones = 32 }; static size_t hugeDeadCount; static unsigned long long hugeReleases; static size_t pageBytes; -/* RFC 0034, section 5.3. */ +/* RFC 0036 section 3.3: the configured quarantine (`Q`), the bytes it + * holds, what the last sweep left in it (pointers still reach them), and + * the bytes of live blocks. */ static size_t quarantineBudget = (size_t)16 << 20; static size_t quarantineBytes; -static unsigned sweepCursor; +static size_t retainedBytes; +static size_t liveBytes; +/* WEAVEC_RT_STATS=1: the most the live heap and the quarantine held. */ +static size_t peakLive; +static size_t peakQuarantine; +/* The pacing of section 3.3: when the last sweep ended and how long it + * took, and the released bytes at which the time is looked at next. */ +static uint64_t lastSweepEnd; +static uint64_t lastSweepTook; +static size_t nextPacingCheck; enum { DataChunk = 1 << 20, MetaChunk = 64 * 1024 }; @@ -233,6 +251,7 @@ static void reserveShadow(void) { return; } dumpable(map, bytes + 64, 0); + weavecRtInternal((uintptr_t)map, (uintptr_t)map + bytes + 64); __weavec_rt_shadow.bits = bits; __atomic_store_n(&__weavec_rt_shadow.base, (uintptr_t)map, __ATOMIC_RELEASE); /* An access near null fails its guard even when the optimiser moved @@ -242,10 +261,33 @@ static void reserveShadow(void) { __atomic_store_n(&__weavec_rt_shadow.mask, bytes - 1, __ATOMIC_RELEASE); } +/* Writes `n` shadow bytes of `value`: a small object's few bytes with + * stores (each release and allocation writes them), larger runs with + * memset. */ +static inline void fill(unsigned char *at, unsigned char value, size_t n) { + if (n > 16) { + memset(at, value, n); + return; + } + if (n >= 8) { + const uint64_t pattern = 0x0101010101010101ULL * value; + memcpy(at, &pattern, sizeof pattern); + memcpy(at + n - 8, &pattern, sizeof pattern); + } else if (n >= 4) { + const uint32_t pattern = 0x01010101U * value; + memcpy(at, &pattern, sizeof pattern); + memcpy(at + n - 4, &pattern, sizeof pattern); + } else if (n != 0) { + at[0] = value; + at[n - 1] = value; + at[n / 2] = value; + } +} + void weavecRtShadowSet(uintptr_t base, size_t size, unsigned char value) { if (!weavecRtShadowReady() || size == 0 || !weavecRtInWindow(base)) return; - memset(weavecRtShadowOf(base), value, (size + 15) >> 4); + fill(weavecRtShadowOf(base), value, (size + 15) >> 4); } void weavecRtShadowObject(uintptr_t base, size_t size, unsigned char tail) { @@ -258,7 +300,7 @@ void weavecRtShadowObject(uintptr_t base, size_t size, unsigned char tail) { if (whole >= 4 * pageBytes) weavecRtShadowClear(base, base + (whole << 4)); else if (whole != 0) - memset(weavecRtShadowOf(base), WeavecRtShadowAddressable, whole); + fill(weavecRtShadowOf(base), WeavecRtShadowAddressable, whole); /* A partial last granule says how many of its bytes are the object's; an * empty object's granule is all tail. */ if (rest != 0) @@ -373,6 +415,8 @@ static int reserve(unsigned shift) { if (map == MAP_FAILED) return 0; dumpable(map, total, 0); + /* The sweep scans the arena's live slots, not the reservation. */ + weavecRtInternal((uintptr_t)map, (uintptr_t)map + total); /* Regions start at multiples of their size, so that the slots of a * power-of-two class are aligned to it. */ start = ((uintptr_t)map + WeavecRtShadowGranule + align - 1) & ~(align - 1); @@ -458,19 +502,19 @@ static int commit(uintptr_t start, uintptr_t *committed, uintptr_t end, return 1; } -/* A slot of `region` for an object of `size` bytes, live; null when the - * region is exhausted or memory is. `recycled` says the slot was used - * before, so its bytes are not known to be zero. */ -static void *takeSlot(unsigned region, size_t size, int *recycled) { +/* Takes a slot of `region` off its free list, or a fresh one; false when + * the region is exhausted or memory is. The class's lock is held. + * `recycled` says the slot was used before, so its bytes are not known to + * be zero. */ +static int popSlot(unsigned region, uint32_t *taken, int *recycled) { const struct __weavec_rt_class *entry = &__weavec_rt_heap.table[region]; ClassState *state = &classState[region]; uint32_t *words = slotWords(region); uint32_t slot; *recycled = 0; - weavecRtLock(&state->lock); if (state->freeHead != 0) { slot = state->freeHead - 1; - state->freeHead = loadWord(&words[slot]) >> 2; + state->freeHead = weavecRtWordValue(loadWord(&words[slot])); *recycled = 1; } else { const uintptr_t limit = (uintptr_t)1 << __weavec_rt_heap.shift; @@ -481,10 +525,8 @@ static void *takeSlot(unsigned region, size_t size, int *recycled) { ((uintptr_t)slot + 1) * entry->size, DataChunk, limit) || !commit((uintptr_t)words, &state->metaCommitted, ((uintptr_t)slot + 1) * sizeof(uint32_t), MetaChunk, - limit >> 2)) { - weavecRtUnlock(&state->lock); - return NULL; - } + limit >> 2)) + return 0; /* RFC 0035 section 5.1: slots the region has not handed out yet are * no object's, so an index that jumps past an object's slot into them * fails; each is written again when it is allocated. */ @@ -503,8 +545,30 @@ static void *takeSlot(unsigned region, size_t size, int *recycled) { WeavecRtShadowGranule, WeavecRtShadowHeapTail); __atomic_store_n(&state->bump, slot + 1, __ATOMIC_RELAXED); } - storeWord(&words[slot], ((uint32_t)size << 2) | WeavecRtLive); + *taken = slot; + return 1; +} + +/* A slot of `region` for an object of `size` bytes, live; null when the + * region is exhausted or memory is. */ +static void *takeSlot(unsigned region, size_t size, int *recycled) { + const struct __weavec_rt_class *entry = &__weavec_rt_heap.table[region]; + ClassState *state = &classState[region]; + uint32_t slot; + weavecRtLock(&state->lock); + if (!popSlot(region, &slot, recycled)) { + weavecRtUnlock(&state->lock); + return NULL; + } + storeWord(&slotWords(region)[slot], + ((uint32_t)size << WeavecRtWordShift) | WeavecRtLive); weavecRtUnlock(&state->lock); + { + const size_t live = + __atomic_add_fetch(&liveBytes, entry->size, __ATOMIC_RELAXED); + if (live > peakLive) + peakLive = live; + } return (void *)(regionStart(region) + (uintptr_t)slot * entry->size); } @@ -549,38 +613,61 @@ __attribute__((constructor)) static void readQuarantineBudget(void) { quarantineBudget = (size_t)strtoull(text, NULL, 10); } -/* Moves the oldest dead slot of a class to its free list. The class's lock - * is held. False when the class has none. */ -static int recycleOldest(unsigned region) { - ClassState *state = &classState[region]; - uint32_t *words = slotWords(region); - uint32_t slot; - if (state->deadHead == 0) +static uint64_t nanoseconds(void) { + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + return (uint64_t)now.tv_sec * 1000000000ULL + (uint64_t)now.tv_nsec; +} + +/* RFC 0036 section 3.3: whether the quarantine should be swept. `gained`, + * the bytes released since the last sweep, is past the hard budget + * max(2 Q, live / 2); or past the soft budget max(Q, live / 4) with the + * last sweep at least eight of its durations ago, so that sweeps take at + * most about a ninth of the run time. The clock is read once per sixteenth + * of the soft budget released. */ +static int overBudget(void) { + const size_t held = __atomic_load_n(&quarantineBytes, __ATOMIC_RELAXED); + const size_t kept = __atomic_load_n(&retainedBytes, __ATOMIC_RELAXED); + const size_t live = __atomic_load_n(&liveBytes, __ATOMIC_RELAXED); + const size_t gained = held > kept ? held - kept : 0; + const size_t soft = quarantineBudget > live / 4 ? quarantineBudget : live / 4; + const size_t hard = + 2 * quarantineBudget > live / 2 ? 2 * quarantineBudget : live / 2; + if (gained < soft || + gained < __atomic_load_n(&nextPacingCheck, __ATOMIC_RELAXED)) return 0; - slot = state->deadHead - 1; - state->deadHead = loadWord(&words[slot]) >> 2; - if (state->deadHead == 0) - state->deadTail = 0; - storeWord(&words[slot], (state->freeHead << 2) | WeavecRtFree); - state->freeHead = slot + 1; - weavecRtCount(WeavecRtStatRecycled); - (void)__atomic_sub_fetch(&quarantineBytes, __weavec_rt_heap.table[region].size, - __ATOMIC_RELAXED); - return 1; + if (gained >= hard || + nanoseconds() - __atomic_load_n(&lastSweepEnd, __ATOMIC_RELAXED) >= + 8 * __atomic_load_n(&lastSweepTook, __ATOMIC_RELAXED)) + return 1; + __atomic_store_n(&nextPacingCheck, gained + soft / 16, __ATOMIC_RELAXED); + return 0; +} + +void weavecRtPeaks(size_t *live, size_t *quarantine) { + *live = peakLive; + *quarantine = peakQuarantine; } -static inline int overBudget(void) { - return __atomic_load_n(&quarantineBytes, __ATOMIC_RELAXED) > quarantineBudget; +void weavecRtSwept(uint64_t ended, uint64_t took) { + __atomic_store_n(&lastSweepEnd, ended, __ATOMIC_RELAXED); + __atomic_store_n(&lastSweepTook, took, __ATOMIC_RELAXED); + __atomic_store_n(&nextPacingCheck, 0, __ATOMIC_RELAXED); } -/* Gives the whole pages inside a dead slot back to the system. */ +/* Gives the whole pages inside a dead slot back to the system, at once: a + * released slot waits for a sweep while fresh slots are taken, so its pages + * must not stay resident meanwhile (Darwin's MADV_FREE leaves them resident + * until the system needs memory; a fresh mapping drops them). */ static void giveBack(uintptr_t base, size_t bytes) { const uintptr_t first = (base + pageBytes - 1) & ~(uintptr_t)(pageBytes - 1); const uintptr_t last = (base + bytes) & ~(uintptr_t)(pageBytes - 1); if (last <= first) return; #if defined(__APPLE__) - (void)madvise((void *)first, last - first, MADV_FREE); + if (mmap((void *)first, last - first, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0) == MAP_FAILED) + (void)madvise((void *)first, last - first, MADV_FREE); #else (void)madvise((void *)first, last - first, MADV_DONTNEED); #endif @@ -590,12 +677,13 @@ static void giveBack(uintptr_t base, size_t bytes) { * released, and a page given back would read 0, addressable. */ static void decommit(uintptr_t base, size_t bytes) { giveBack(base, bytes); } -/* Makes the live slot `ref` dead and queues it. */ +/* Makes the live slot `ref` dead and queues it in its class's quarantine; + * sweeps when the quarantine is over budget. */ static void retire(SlotRef ref) { const struct __weavec_rt_class *entry = &__weavec_rt_heap.table[ref.region]; ClassState *state = &classState[ref.region]; uint32_t *words = slotWords(ref.region); - /* Before the slot is queued: once it is, another thread may recycle it. */ + /* Before the slot is queued: a sweep may recycle it once it is. */ shadowDead(ref.base, entry->size); if (entry->size >= WeavecRtDecommitBytes) decommit(ref.base, entry->size); @@ -603,27 +691,20 @@ static void retire(SlotRef ref) { storeWord(&words[ref.slot], WeavecRtDead); if (state->deadTail != 0) storeWord(&words[state->deadTail - 1], - ((ref.slot + 1) << 2) | WeavecRtDead); + ((ref.slot + 1) << WeavecRtWordShift) | WeavecRtDead); else state->deadHead = ref.slot + 1; state->deadTail = ref.slot + 1; - (void)__atomic_add_fetch(&quarantineBytes, entry->size, __ATOMIC_RELAXED); - while (overBudget() && recycleOldest(ref.region)) { - } - weavecRtUnlock(&state->lock); - /* Other classes hold the rest of an exceeded budget. */ { - unsigned tries; - for (tries = 0; overBudget() && tries < __weavec_rt_heap.classes; ++tries) { - const unsigned victim = - __atomic_fetch_add(&sweepCursor, 1, __ATOMIC_RELAXED) % - __weavec_rt_heap.classes; - weavecRtLock(&classState[victim].lock); - while (overBudget() && recycleOldest(victim)) { - } - weavecRtUnlock(&classState[victim].lock); - } + const size_t held = + __atomic_add_fetch(&quarantineBytes, entry->size, __ATOMIC_RELAXED); + if (held > peakQuarantine) + peakQuarantine = held; } + (void)__atomic_sub_fetch(&liveBytes, entry->size, __ATOMIC_RELAXED); + weavecRtUnlock(&state->lock); + if (overBudget()) + weavecRtSweep(); } /*===-- Huge blocks ---------------------------------------------------------===*/ @@ -685,8 +766,12 @@ static void *hugeAlloc(size_t size, size_t alignment) { } if (hugeCount != 0) memcpy(grown, hugeBlocks, hugeCount * sizeof(HugeBlock)); - if (hugeBlocks != NULL) + weavecRtInternal((uintptr_t)grown, + (uintptr_t)grown + capacity * sizeof(HugeBlock)); + if (hugeBlocks != NULL) { + weavecRtInternalDrop((uintptr_t)hugeBlocks); (void)munmap(hugeBlocks, hugeCapacity * sizeof(HugeBlock)); + } hugeBlocks = grown; hugeCapacity = capacity; } @@ -698,6 +783,7 @@ static void *hugeAlloc(size_t size, size_t alignment) { hugeBlocks[at].map = map; hugeBlocks[at].mapBytes = mapBytes; hugeBlocks[at].dead = 0; + hugeBlocks[at].marked = 0; ++hugeCount; shadowLive(base, size, bytes); if (map < __atomic_load_n(&hugeLow, __ATOMIC_RELAXED)) @@ -732,10 +818,8 @@ static int hugeFind(uintptr_t address, HugeBlock *out) { /* Releases the live huge block that starts at `address`; false when there * is none. */ static int hugeFree(uintptr_t address) { - HugeBlock gone; long index; int ok = 0; - int unmap = 0; if (!maybeHuge(address)) return 0; weavecRtLock(&hugeLock); @@ -755,31 +839,14 @@ static int hugeFree(uintptr_t address) { } else { shadowDead(block->base, block->size); } - /* The memory goes back; the address range stays this block's. */ + /* The memory goes back; the address range stays this block's until a + * sweep finds no pointer to it. */ (void)mmap((void *)block->map, block->mapBytes, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED | MAP_NORESERVE, -1, 0); ok = 1; - if (++hugeDeadCount > HugeTombstones) { - size_t i; - size_t oldest = hugeCount; - for (i = 0; i < hugeCount; ++i) - if (hugeBlocks[i].dead != 0 && - (oldest == hugeCount || - hugeBlocks[i].dead < hugeBlocks[oldest].dead)) - oldest = i; - gone = hugeBlocks[oldest]; - memmove(&hugeBlocks[oldest], &hugeBlocks[oldest + 1], - (hugeCount - oldest - 1) * sizeof(HugeBlock)); - --hugeCount; - --hugeDeadCount; - unmap = 1; - } + ++hugeDeadCount; } weavecRtUnlock(&hugeLock); - if (unmap) { - weavecRtShadowClear(gone.base, gone.base + gone.size + 16); - (void)munmap((void *)gone.map, gone.mapBytes); - } return ok; } @@ -900,6 +967,12 @@ void *__weavec_rt_alloc(size_t size, size_t alignment) { weavecRtCount(WeavecRtStatAllocations); if (region < __weavec_rt_heap.classes) { block = takeSlot(region, size, &recycled); + /* A full region whose quarantine holds slots: a sweep may free some. */ + if (block == NULL && + __atomic_load_n(&classState[region].deadHead, __ATOMIC_RELAXED) != 0) { + weavecRtSweep(); + block = takeSlot(region, size, &recycled); + } if (block != NULL) { if (recycled && size != 0) zeroFill((uintptr_t)block, size); @@ -918,9 +991,9 @@ static int ownedSize(const void *p, size_t *size) { if (inArena(address)) { const SlotRef ref = slotOf(address); const uint32_t word = wordOf(ref); - if (ref.base != address || (word & 3) != WeavecRtLive) + if (ref.base != address || (word & WeavecRtWordState) != WeavecRtLive) return 0; - *size = word >> 2; + *size = weavecRtWordValue(word); return 1; } { @@ -936,9 +1009,9 @@ static int ownedSize(const void *p, size_t *size) { static const char *whyNotReleasable(uintptr_t address) { const SlotRef ref = slotOf(address); const uint32_t word = wordOf(ref); - if ((word & 3) == WeavecRtDead) + if ((word & WeavecRtWordState) == WeavecRtDead) return "the block was already released"; - if ((word & 3) != WeavecRtLive) + if ((word & WeavecRtWordState) != WeavecRtLive) return "no block is allocated there"; return "not the start of its block"; } @@ -951,7 +1024,7 @@ void __weavec_rt_free(void *p) { if (inArena(address)) { const SlotRef ref = slotOf(address); const uint32_t word = wordOf(ref); - if (ref.base != address || (word & 3) != WeavecRtLive) + if (ref.base != address || (word & WeavecRtWordState) != WeavecRtLive) __weavec_rt_fatal("invalid release", p, whyNotReleasable(address)); weavecRtCount(WeavecRtStatReleases); retire(ref); @@ -980,15 +1053,15 @@ void *__weavec_rt_realloc(void *p, size_t size) { const SlotRef ref = slotOf(address); uint32_t *word = &slotWords(ref.region)[ref.slot]; const uint32_t value = wordOf(ref); - if (ref.base != address || (value & 3) != WeavecRtLive) + if (ref.base != address || (value & WeavecRtWordState) != WeavecRtLive) __weavec_rt_fatal("invalid release", p, whyNotReleasable(address)); - old = value >> 2; + old = weavecRtWordValue(value); if (classFor(size) == ref.region) { /* The block stays; bytes it gains are zero (section 2.3). */ if (size > old) memset((char *)p + old, 0, size - old); shadowLive(address, size, __weavec_rt_heap.table[ref.region].size); - storeWord(word, ((uint32_t)size << 2) | WeavecRtLive); + storeWord(word, ((uint32_t)size << WeavecRtWordShift) | WeavecRtLive); return p; } } else if (!ownedSize(p, &old)) { @@ -1030,10 +1103,12 @@ struct weavecRtHeapObject weavecRtHeapObjectAt(uintptr_t address) { if (inArena(address)) { const SlotRef ref = slotOf(address); const uint32_t word = wordOf(ref); - object.found = (word & 3) != WeavecRtNever; - object.live = (word & 3) == WeavecRtLive; + object.found = (word & WeavecRtWordState) != WeavecRtNever; + object.live = (word & WeavecRtWordState) == WeavecRtLive; object.base = ref.base; - object.size = object.live ? word >> 2 : 0; + /* A released slot: its whole extent. */ + object.size = object.live ? weavecRtWordValue(word) + : __weavec_rt_heap.table[ref.region].size; return object; } { @@ -1050,7 +1125,7 @@ struct weavecRtHeapObject weavecRtHeapObjectAt(uintptr_t address) { /*===-- fork ----------------------------------------------------------------===*/ -static void lockAll(void) { +void weavecRtLockAll(void) { unsigned i; weavecRtLock(&initLock); for (i = 0; i < WeavecRtClassCount; ++i) @@ -1058,7 +1133,7 @@ static void lockAll(void) { weavecRtLock(&hugeLock); } -static void unlockAll(void) { +void weavecRtUnlockAll(void) { unsigned i; weavecRtUnlock(&hugeLock); for (i = WeavecRtClassCount; i-- > 0;) @@ -1066,9 +1141,162 @@ static void unlockAll(void) { weavecRtUnlock(&initLock); } -static void forkPrepare(void) { lockAll(); } -static void forkParent(void) { unlockAll(); } -static void forkChild(void) { unlockAll(); } +/* The sweep's lock comes first, so that a fork never copies a sweep in + * progress. */ +static void forkPrepare(void) { + weavecRtSweepLock(); + weavecRtLockAll(); +} +static void forkParent(void) { + weavecRtUnlockAll(); + weavecRtSweepUnlock(); +} +static void forkChild(void) { + weavecRtUnlockAll(); + weavecRtSweepUnlock(); +} + +/*===-- The allocator's side of the sweep (RFC 0036 section 3.4) -----------===*/ + +/* Whether the quarantine or the released huge blocks hold anything. */ +int weavecRtSweepWanted(void) { + return __atomic_load_n(&quarantineBytes, __ATOMIC_RELAXED) != 0 || + __atomic_load_n(&hugeDeadCount, __ATOMIC_RELAXED) != 0; +} + +/* Maps the marks of each class whose quarantine holds slots. Every lock is + * held. */ +int weavecRtPrepareMarks(void) { + unsigned region; + for (region = 0; region < __weavec_rt_heap.classes; ++region) { + ClassState *state = &classState[region]; + if (state->deadHead == 0 || state->marks != NULL) + continue; + { + const size_t bytes = + ((size_t)__weavec_rt_heap.table[region].capacity + 63) / 64 * 8; + void *map = mmap(NULL, bytes, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0); + if (map == MAP_FAILED) + return 0; + weavecRtInternal((uintptr_t)map, (uintptr_t)map + bytes); + state->marks = (uint64_t *)map; + } + } + return 1; +} + +/* Marks the quarantined slot or released huge block `value` points into. */ +static inline void markAddress(uintptr_t value) { + if (value - __weavec_rt_heap.base < __weavec_rt_heap.bytes) { + /* A class with nothing in its quarantine needs no slot index. */ + const ClassState *quick = + &classState[(value - __weavec_rt_heap.base) >> __weavec_rt_heap.shift]; + SlotRef ref; + if (quick->marks == NULL || quick->deadHead == 0) + return; + ref = slotOf(value); + if ((wordOf(ref) & WeavecRtWordState) == WeavecRtDead) + classState[ref.region].marks[ref.slot >> 6] |= (uint64_t)1 + << (ref.slot & 63); + return; + } + if (hugeDeadCount != 0 && value >= hugeLow && value < hugeHigh) { + const long index = hugeIndex(value); + if (index >= 0 && hugeBlocks[index].dead != 0 && + value < hugeBlocks[index].map + hugeBlocks[index].mapBytes) + hugeBlocks[index].marked = 1; + } +} + +void weavecRtMarkWords(uintptr_t low, uintptr_t high) { + const uintptr_t *word = (const uintptr_t *)((low + 7) & ~(uintptr_t)7); + const uintptr_t *end = (const uintptr_t *)(high & ~(uintptr_t)7); + for (; word < end; ++word) + markAddress(*word); +} + +void weavecRtMarkHeap(void) { + unsigned region; + size_t i; + for (region = 0; region < __weavec_rt_heap.classes; ++region) { + const struct __weavec_rt_class *entry = &__weavec_rt_heap.table[region]; + const uint32_t *words = slotWords(region); + const uint32_t bump = classState[region].bump; + uint32_t slot; + for (slot = 0; slot < bump; ++slot) { + const uint32_t word = loadWord(&words[slot]); + if ((word & WeavecRtWordState) == WeavecRtLive) { + const uintptr_t base = regionStart(region) + (uintptr_t)slot * entry->size; + weavecRtMarkWords(base, base + weavecRtWordValue(word)); + } + } + } + for (i = 0; i < hugeCount; ++i) + if (hugeBlocks[i].dead == 0) + weavecRtMarkWords(hugeBlocks[i].base, + hugeBlocks[i].base + hugeBlocks[i].size); +} + +void weavecRtRecycle(struct weavecRtSweepCounts *counts, int recycle) { + unsigned region; + size_t i; + for (region = 0; region < __weavec_rt_heap.classes; ++region) { + ClassState *state = &classState[region]; + uint32_t *words = slotWords(region); + const size_t size = __weavec_rt_heap.table[region].size; + uint32_t keptHead = 0; + uint32_t slot; + if (state->marks == NULL || state->deadHead == 0) + continue; + /* By slot, from the last: the free list and the quarantine come out in + * address order, so that slots are reused, and their words read, in + * order. */ + for (slot = state->bump; slot-- > 0;) { + if ((loadWord(&words[slot]) & WeavecRtWordState) != WeavecRtDead) + continue; + if (!recycle || (state->marks[slot >> 6] >> (slot & 63) & 1) != 0) { + if (keptHead == 0) + state->deadTail = slot + 1; + storeWord(&words[slot], + (keptHead << WeavecRtWordShift) | WeavecRtDead); + keptHead = slot + 1; + ++counts->retained; + counts->retainedBytes += size; + } else { + storeWord(&words[slot], + (state->freeHead << WeavecRtWordShift) | WeavecRtFree); + state->freeHead = slot + 1; + quarantineBytes -= size; + ++counts->recycled; + weavecRtCount(WeavecRtStatRecycled); + } + } + state->deadHead = keptHead; + if (keptHead == 0) + state->deadTail = 0; + memset(state->marks, 0, ((size_t)state->bump + 63) / 64 * 8); + } + __atomic_store_n(&retainedBytes, quarantineBytes, __ATOMIC_RELAXED); + for (i = 0; i < hugeCount;) { + HugeBlock *block = &hugeBlocks[i]; + if (block->dead == 0 || block->marked || !recycle) { + if (block->dead != 0) { + ++counts->retained; + counts->retainedBytes += block->size; + } + block->marked = 0; + ++i; + continue; + } + weavecRtShadowClear(block->base, block->base + block->size + 16); + (void)munmap((void *)block->map, block->mapBytes); + memmove(block, block + 1, (hugeCount - i - 1) * sizeof(HugeBlock)); + --hugeCount; + --hugeDeadCount; + ++counts->recycled; + } +} /*===-- The Darwin zone (section 2.5) ---------------------------------------===*/ diff --git a/runtime/weavec_asan.c b/runtime/weavec_asan.c new file mode 100644 index 00000000..42230f6d --- /dev/null +++ b/runtime/weavec_asan.c @@ -0,0 +1,151 @@ +/*===- weavec_asan.c - AddressSanitizer's interface ----------------*- C -*-===*\ +|* +|* Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. +|* See LICENSE for license information. +|* SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +|* +|*===----------------------------------------------------------------------===*| +|* +|* RFC 0036, section 5. weavec-cc compiles code as AddressSanitizer would +|* (`__SANITIZE_ADDRESS__`, `__has_feature(address_sanitizer)`), so code +|* written to be ASan-clean calls ASan's interface: an allocator that +|* poisons its free space, a coroutine library that announces its stack +|* switches, a test that asks whether an address is poisoned. These are +|* those functions over WeaveC's shadow: a poisoned granule is `0xF7` +|* (`use-after-poison`); what ASan does that WeaveC does not (leak +|* checking, fake stacks, stack traces) does nothing. +|* +\*===----------------------------------------------------------------------===*/ + +#include "weavec_rt.h" + +#include + +#define WEAVEC_ASAN_API WEAVEC_RT_API __attribute__((weak)) + +WEAVEC_ASAN_API void __asan_poison_memory_region(const volatile void *address, + size_t size); +WEAVEC_ASAN_API void +__asan_unpoison_memory_region(const volatile void *address, size_t size); +WEAVEC_ASAN_API int __asan_address_is_poisoned(const volatile void *address); +WEAVEC_ASAN_API void *__asan_region_is_poisoned(void *address, size_t size); +WEAVEC_ASAN_API void __asan_describe_address(void *address); +WEAVEC_ASAN_API void __asan_handle_no_return(void); +WEAVEC_ASAN_API void *__asan_get_current_fake_stack(void); +WEAVEC_ASAN_API void *__asan_addr_is_in_fake_stack(void *fake, void *address, + void **begin, void **end); +WEAVEC_ASAN_API void __sanitizer_print_stack_trace(void); +WEAVEC_ASAN_API void __sanitizer_set_report_path(const char *path); +WEAVEC_ASAN_API void +__sanitizer_annotate_contiguous_container(const void *begin, const void *end, + const void *oldEnd, + const void *newEnd); +WEAVEC_ASAN_API void __sanitizer_start_switch_fiber(void **save, + const void *bottom, + size_t size); +WEAVEC_ASAN_API void __sanitizer_finish_switch_fiber(void *save, + const void **bottom, + size_t *size); +WEAVEC_ASAN_API void __lsan_ignore_object(const void *p); +WEAVEC_ASAN_API void __lsan_disable(void); +WEAVEC_ASAN_API void __lsan_enable(void); +WEAVEC_ASAN_API void __lsan_register_root_region(const void *p, size_t size); +WEAVEC_ASAN_API void __lsan_unregister_root_region(const void *p, size_t size); +WEAVEC_ASAN_API void __lsan_do_leak_check(void); +WEAVEC_ASAN_API int __lsan_do_recoverable_leak_check(void); + +/* ASan poisons only whole granules inside the region (its granule is 8 + * bytes, WeaveC's 16), and leaves a partial last granule as it was. */ +void __asan_poison_memory_region(const volatile void *address, size_t size) { + const uintptr_t low = ((uintptr_t)address + 15) & ~(uintptr_t)15; + const uintptr_t high = ((uintptr_t)address + size) & ~(uintptr_t)15; + if (high > low) + weavecRtShadowSet(low, high - low, WeavecRtShadowUser); +} + +/* Unpoisons the region, its partial last granule as the first bytes of + * that granule. */ +void __asan_unpoison_memory_region(const volatile void *address, size_t size) { + const uintptr_t start = (uintptr_t)address; + const uintptr_t low = start & ~(uintptr_t)15; + const uintptr_t end = start + size; + if (size == 0 || !weavecRtShadowReady() || !weavecRtInWindow(start)) + return; + if ((end & ~(uintptr_t)15) > low) + memset(weavecRtShadowOf(low), WeavecRtShadowAddressable, + ((end & ~(uintptr_t)15) - low) >> 4); + if ((end & 15) != 0) { + unsigned char *last = weavecRtShadowOf(end); + if (*last != WeavecRtShadowAddressable && *last < (end & 15)) + *last = (unsigned char)(end & 15); + else if (*last >= WeavecRtShadowGranule) + *last = (unsigned char)(end & 15); + } +} + +int __asan_address_is_poisoned(const volatile void *address) { + return !__weavec_rt_range_ok((uintptr_t)address, 1); +} + +void *__asan_region_is_poisoned(void *address, size_t size) { + size_t at; + if (size == 0 || __weavec_rt_range_ok((uintptr_t)address, size)) + return NULL; + for (at = 0; at < size; ++at) + if (!__weavec_rt_range_ok((uintptr_t)address + at, 1)) + return (char *)address + at; + return NULL; +} + +void __asan_describe_address(void *address) { + const struct weavecRtHeapObject object = + weavecRtHeapObjectAt((uintptr_t)address); + (void)object; +} + +void __asan_handle_no_return(void) { __weavec_rt_unpoison_stack(); } +void *__asan_get_current_fake_stack(void) { return NULL; } +void *__asan_addr_is_in_fake_stack(void *fake, void *address, void **begin, + void **end) { + (void)fake; + (void)address; + (void)begin; + (void)end; + return NULL; +} +void __sanitizer_print_stack_trace(void) {} +void __sanitizer_set_report_path(const char *path) { (void)path; } +void __sanitizer_annotate_contiguous_container(const void *begin, + const void *end, + const void *oldEnd, + const void *newEnd) { + (void)begin; + (void)end; + (void)oldEnd; + (void)newEnd; +} +void __sanitizer_start_switch_fiber(void **save, const void *bottom, + size_t size) { + (void)save; + (void)bottom; + (void)size; +} +void __sanitizer_finish_switch_fiber(void *save, const void **bottom, + size_t *size) { + (void)save; + (void)bottom; + (void)size; +} +void __lsan_ignore_object(const void *p) { (void)p; } +void __lsan_disable(void) {} +void __lsan_enable(void) {} +void __lsan_register_root_region(const void *p, size_t size) { + (void)p; + (void)size; +} +void __lsan_unregister_root_region(const void *p, size_t size) { + (void)p; + (void)size; +} +void __lsan_do_leak_check(void) {} +int __lsan_do_recoverable_leak_check(void) { return 0; } diff --git a/runtime/weavec_guard.c b/runtime/weavec_guard.c index 4e099af0..0b9ab793 100644 --- a/runtime/weavec_guard.c +++ b/runtime/weavec_guard.c @@ -6,11 +6,12 @@ |* |*===----------------------------------------------------------------------===*| |* -|* RFC 0035, sections 2 to 5. The guard pass checks the shadow inline and -|* calls the slow path when a byte it reads is not 0; the slow path decides -|* exactly, from the shadow alone, and names a failure by the shadow value -|* that caused it. Frames and globals poison their redzones; this file -|* clears what a non-local exit left behind and registers the globals. +|* RFC 0035, sections 2 to 5, and RFC 0036, sections 4 and 6. The guard +|* pass checks the shadow inline and calls the slow path when a byte it +|* reads is not 0; the slow path decides exactly, from the shadow alone, and +|* names a failure by the shadow value that caused it. Frames and globals +|* poison their redzones; this file clears what a non-local exit left behind +|* and registers the globals, in the process's runtime. |* \*===----------------------------------------------------------------------===*/ @@ -31,7 +32,8 @@ static inline int isPoison(unsigned char value) { } /* The name of a failure whose first unaddressable byte has shadow `value`; - * for a partial granule, the poison after it says whose object it is. */ + * for a partial granule, the poison after it says whose object it is (one + * with none after it is the program's, through ASan's interface). */ static const char *kindOf(unsigned char value, uintptr_t granule) { unsigned look; if (!isPoison(value)) { @@ -44,7 +46,7 @@ static const char *kindOf(unsigned char value, uintptr_t granule) { break; } if (!isPoison(value)) - return "buffer-overflow"; + return "use-after-poison"; } switch (value) { case WeavecRtShadowHeapTail: @@ -59,6 +61,8 @@ static const char *kindOf(unsigned char value, uintptr_t granule) { return "dynamic-stack-buffer-overflow"; case WeavecRtShadowStackScope: return "stack-use-after-scope"; + case WeavecRtShadowUser: + return "use-after-poison"; case WeavecRtShadowGlobal: return "global-buffer-overflow"; case WeavecRtShadowNull: @@ -310,6 +314,15 @@ void __weavec_rt_alloca_unpoison(uintptr_t low, uintptr_t high) { void __weavec_rt_globals_register(const struct __weavec_rt_global *globals, uint64_t n) { uint64_t i; + /* A shared image registers in the process's runtime, whose shadow its + * guards read (RFC 0036 section 4). */ + { + const struct __weavec_rt_dispatch *owner = weavecRtForward(); + if (owner != 0) { + owner->registerGlobals(globals, n); + return; + } + } (void)weavecRtInitialise(); for (i = 0; i < n; ++i) { const struct __weavec_rt_global *global = &globals[i]; @@ -326,11 +339,27 @@ void __weavec_rt_globals_register(const struct __weavec_rt_global *globals, void __weavec_rt_globals_unregister(const struct __weavec_rt_global *globals, uint64_t n) { uint64_t i; + { + const struct __weavec_rt_dispatch *owner = weavecRtForward(); + if (owner != 0) { + owner->unregisterGlobals(globals, n); + return; + } + } for (i = 0; i < n; ++i) weavecRtShadowClear(globals[i].address, globals[i].address + globals[i].padded); } +/*===-- Fortified sizes (RFC 0036 section 6) ------------------------------===*/ + +WEAVEC_RT_SLOW void __weavec_rt_limit_fail(uintptr_t address, uint64_t width, + uint64_t limit, + const struct __weavec_rt_site *site) { + weavecRtFailObject("intra-object-overflow", address, width, address, + address + limit, site); +} + /*===-- Array bounds (section 5.4) -----------------------------------------===*/ /* Clang's `array-bounds` instrumentation calls these with its own site diff --git a/runtime/weavec_libc.c b/runtime/weavec_libc.c index 0189e69e..41f8096b 100644 --- a/runtime/weavec_libc.c +++ b/runtime/weavec_libc.c @@ -11,7 +11,9 @@ |* itself computes (a string copy, a formatted write), which computes them |* before the call writes; a search or comparison, which checks the bytes it |* read up to where it stopped; and the mapping calls, which clear the -|* shadow of what they map or unmap. +|* shadow of what they map or unmap. RFC 0036 section 6: a writing call is +|* also checked against the object size of its fortified form (`limit`), and +|* the number parsers check what they read after the call. |* \*===----------------------------------------------------------------------===*/ @@ -22,7 +24,9 @@ #include "weavec_rt.h" #include +#include #include +#include #include #include #include @@ -30,10 +34,19 @@ /* A range guard, inline when one shadow load decides it. */ static inline void checkRange(uintptr_t address, uint64_t n, const struct __weavec_rt_site *site) { + if (n == 0) + return; if (!weavecRtQuickOk(address, n)) __weavec_rt_range(address, n, site); } +/* A fortified call's object size: no more than `limit` bytes at d. */ +static inline void checkLimit(const char *d, uint64_t n, size_t limit, + const struct __weavec_rt_site *write) { + if (n > limit) + __weavec_rt_limit_fail((uintptr_t)d, n, limit, write); +} + /* The site of a write: the pass marks the site of the call as a read. */ static struct __weavec_rt_site writeSite(const struct __weavec_rt_site *site) { struct __weavec_rt_site write = {0, 0, 0, WeavecRtSiteWrite}; @@ -45,11 +58,13 @@ static struct __weavec_rt_site writeSite(const struct __weavec_rt_site *site) { } /* Copies the string at s and its terminator to d, checked; its length. */ -static uint64_t copyString(char *d, const char *s, +static uint64_t copyString(char *d, const char *s, size_t limit, const struct __weavec_rt_site *site) { const uint64_t n = __weavec_rt_strlen(s, ~(uint64_t)0, site); const struct __weavec_rt_site write = writeSite(site); + checkRange((uintptr_t)s, n + 1, site); checkRange((uintptr_t)d, n + 1, &write); + checkLimit(d, n + 1, limit, &write); if (d != s && (uintptr_t)d < (uintptr_t)s + n + 1 && (uintptr_t)s < (uintptr_t)d + n + 1) __weavec_rt_overlap((uintptr_t)d, (uintptr_t)s, n + 1, &write); @@ -57,23 +72,24 @@ static uint64_t copyString(char *d, const char *s, return n; } -char *__weavec_rt_strcpy(char *d, const char *s, +char *__weavec_rt_strcpy(char *d, const char *s, size_t limit, const struct __weavec_rt_site *site) { - (void)copyString(d, s, site); + (void)copyString(d, s, limit, site); return d; } -char *__weavec_rt_stpcpy(char *d, const char *s, +char *__weavec_rt_stpcpy(char *d, const char *s, size_t limit, const struct __weavec_rt_site *site) { - return d + copyString(d, s, site); + return d + copyString(d, s, limit, site); } -char *__weavec_rt_strcat(char *d, const char *s, +char *__weavec_rt_strcat(char *d, const char *s, size_t limit, const struct __weavec_rt_site *site) { const uint64_t at = __weavec_rt_strlen(d, ~(uint64_t)0, site); const uint64_t n = __weavec_rt_strlen(s, ~(uint64_t)0, site); const struct __weavec_rt_site write = writeSite(site); - checkRange((uintptr_t)d + at, n + 1, &write); + checkRange((uintptr_t)d, at + n + 1, &write); + checkLimit(d, at + n + 1, limit, &write); memcpy(d + at, s, (size_t)n + 1); return d; } @@ -472,13 +488,13 @@ int __weavec_rt_scanf(const struct __weavec_rt_site *site, const char *format, } int __weavec_rt_vsnprintf(char *d, size_t n, const char *format, - va_list arguments, + va_list arguments, size_t limit, const struct __weavec_rt_site *site) { checkFormat(format, arguments, site); - /* A destination of n addressable bytes takes whatever the call writes, - * in one formatting pass; otherwise the length the call would write is - * computed first, and what it writes checked. */ - if (n != 0 && !__weavec_rt_range_ok((uintptr_t)d, n)) { + /* A destination of n addressable bytes within its limit takes whatever + * the call writes, in one formatting pass; otherwise the length the call + * would write is computed first, and what it writes checked. */ + if (n != 0 && (!__weavec_rt_range_ok((uintptr_t)d, n) || n > limit)) { va_list copy; int length; va_copy(copy, arguments); @@ -489,23 +505,24 @@ int __weavec_rt_vsnprintf(char *d, size_t n, const char *format, (uint64_t)length + 1 < n ? (uint64_t)length + 1 : n; const struct __weavec_rt_site write = writeSite(site); checkRange((uintptr_t)d, written, &write); + checkLimit(d, written, limit, &write); } } return vsnprintf(d, n, format, arguments); } -int __weavec_rt_snprintf(const struct __weavec_rt_site *site, char *d, - size_t n, const char *format, ...) { +int __weavec_rt_snprintf(const struct __weavec_rt_site *site, size_t limit, + char *d, size_t n, const char *format, ...) { va_list arguments; int result; va_start(arguments, format); - result = __weavec_rt_vsnprintf(d, n, format, arguments, site); + result = __weavec_rt_vsnprintf(d, n, format, arguments, limit, site); va_end(arguments); return result; } int __weavec_rt_vsprintf(char *d, const char *format, va_list arguments, - const struct __weavec_rt_site *site) { + size_t limit, const struct __weavec_rt_site *site) { va_list copy; int length; checkFormat(format, arguments, site); @@ -517,33 +534,39 @@ int __weavec_rt_vsprintf(char *d, const char *format, va_list arguments, { const struct __weavec_rt_site write = writeSite(site); checkRange((uintptr_t)d, (uint64_t)length + 1, &write); + checkLimit(d, (uint64_t)length + 1, limit, &write); } return vsnprintf(d, (size_t)length + 1, format, arguments); } -int __weavec_rt_sprintf(const struct __weavec_rt_site *site, char *d, - const char *format, ...) { +int __weavec_rt_sprintf(const struct __weavec_rt_site *site, size_t limit, + char *d, const char *format, ...) { va_list arguments; int result; va_start(arguments, format); - result = __weavec_rt_vsprintf(d, format, arguments, site); + result = __weavec_rt_vsprintf(d, format, arguments, limit, site); va_end(arguments); return result; } /* gets, which no length bounds: each byte is checked before it is * written. */ -char *__weavec_rt_gets(char *d, const struct __weavec_rt_site *site) { +char *__weavec_rt_gets(char *d, size_t limit, + const struct __weavec_rt_site *site) { const struct __weavec_rt_site write = writeSite(site); uint64_t n = 0; int c; + /* Byte by byte, each next to the last: the first that is not addressable + * is the report's. */ while ((c = getchar()) != EOF && c != '\n') { checkRange((uintptr_t)d + n, 1, &write); + checkLimit(d, n + 1, limit, &write); d[n++] = (char)c; } if (c == EOF && (n == 0 || ferror(stdin))) return NULL; checkRange((uintptr_t)d + n, 1, &write); + checkLimit(d, n + 1, limit, &write); d[n] = 0; return d; } @@ -642,3 +665,80 @@ int __weavec_rt_munmap(void *address, size_t length, weavecRtShadowClear((uintptr_t)address, (uintptr_t)address + length); return result; } + +/*===-- Number parsing (RFC 0036 section 6) --------------------------------===*/ + +/* Checks the bytes a number parser read from s, which stopped at end, and + * stores end through the caller's end pointer. */ +static void parsed(const char *s, char *end, char **out, + const struct __weavec_rt_site *site) { + uint64_t read; + if (end != s) { + read = (uint64_t)(end - s) + 1; + } else { + /* Nothing converted: the white space and sign skipped, and the + * character that refused. */ + const char *at = s; + while (isspace((unsigned char)*at)) + ++at; + if (*at == '+' || *at == '-') + ++at; + read = (uint64_t)(at - s) + 1; + } + checkRange((uintptr_t)s, read, site); + if (out != NULL) { + const struct __weavec_rt_site write = writeSite(site); + checkRange((uintptr_t)out, sizeof *out, &write); + *out = end; + } +} + +#define WEAVEC_RT_PARSER(type, name, call) \ + type __weavec_rt_##name(const char *s, char **end, int base, \ + const struct __weavec_rt_site *site) { \ + char *stop = (char *)s; \ + type value; \ + readsNull(s, 1, site); \ + value = call(s, &stop, base); \ + parsed(s, stop, end, site); \ + return value; \ + } + +WEAVEC_RT_PARSER(long, strtol, strtol) +WEAVEC_RT_PARSER(long long, strtoll, strtoll) +WEAVEC_RT_PARSER(unsigned long, strtoul, strtoul) +WEAVEC_RT_PARSER(unsigned long long, strtoull, strtoull) +WEAVEC_RT_PARSER(intmax_t, strtoimax, strtoimax) +WEAVEC_RT_PARSER(uintmax_t, strtoumax, strtoumax) + +#define WEAVEC_RT_FLOAT_PARSER(type, name, call) \ + type __weavec_rt_##name(const char *s, char **end, \ + const struct __weavec_rt_site *site) { \ + char *stop = (char *)s; \ + type value; \ + readsNull(s, 1, site); \ + value = call(s, &stop); \ + parsed(s, stop, end, site); \ + return value; \ + } + +WEAVEC_RT_FLOAT_PARSER(double, strtod, strtod) +WEAVEC_RT_FLOAT_PARSER(float, strtof, strtof) +WEAVEC_RT_FLOAT_PARSER(long double, strtold, strtold) + +int __weavec_rt_atoi(const char *s, const struct __weavec_rt_site *site) { + return (int)__weavec_rt_strtol(s, NULL, 10, site); +} + +long __weavec_rt_atol(const char *s, const struct __weavec_rt_site *site) { + return __weavec_rt_strtol(s, NULL, 10, site); +} + +long long __weavec_rt_atoll(const char *s, + const struct __weavec_rt_site *site) { + return __weavec_rt_strtoll(s, NULL, 10, site); +} + +double __weavec_rt_atof(const char *s, const struct __weavec_rt_site *site) { + return __weavec_rt_strtod(s, NULL, site); +} diff --git a/runtime/weavec_owner.c b/runtime/weavec_owner.c index 43c3e6f0..02ff2fa8 100644 --- a/runtime/weavec_owner.c +++ b/runtime/weavec_owner.c @@ -20,13 +20,22 @@ #include -enum { WeavecRtDispatchMagic = 0x52435657, WeavecRtDispatchVersion = 3 }; +enum { WeavecRtDispatchMagic = 0x52435657, WeavecRtDispatchVersion = 4 }; const struct __weavec_rt_dispatch __weavec_rt_dispatch = { - WeavecRtDispatchMagic, WeavecRtDispatchVersion, &__weavec_rt_heap, - &__weavec_rt_shadow, weavecRtInitialise, __weavec_rt_alloc, - __weavec_rt_free, __weavec_rt_realloc, __weavec_rt_size, - weavecRtHeapObjectAt, __weavec_rt_fatal}; + WeavecRtDispatchMagic, + WeavecRtDispatchVersion, + &__weavec_rt_heap, + &__weavec_rt_shadow, + weavecRtInitialise, + __weavec_rt_alloc, + __weavec_rt_free, + __weavec_rt_realloc, + __weavec_rt_size, + weavecRtHeapObjectAt, + __weavec_rt_fatal, + __weavec_rt_globals_register, + __weavec_rt_globals_unregister}; #if defined(__APPLE__) diff --git a/runtime/weavec_report.c b/runtime/weavec_report.c index c6d68679..1c1c033c 100644 --- a/runtime/weavec_report.c +++ b/runtime/weavec_report.c @@ -31,6 +31,7 @@ #include "weavec_rt.h" +#include #include #include #include @@ -86,6 +87,9 @@ static int firstReport(unsigned long long hash) { /* WEAVEC_RT_ABORT=1. */ static int abortSetting; +/* WEAVEC_RT_BACKTRACE=1: the stack of the access that stops the program, + * after its report. */ +static int backtraceSetting; /* WEAVEC_RT_REPORT_LOG=: every report is appended to that file, one * write per line, for a test harness that keeps the output of a passing test @@ -97,6 +101,8 @@ __attribute__((constructor)) static void readReportSettings(void) { const char *value = getenv("WEAVEC_RT_ABORT"); const char *path; abortSetting = value != NULL && strcmp(value, "1") == 0; + value = getenv("WEAVEC_RT_BACKTRACE"); + backtraceSetting = value != NULL && strcmp(value, "1") == 0; /* A set-user-ID or set-group-ID program does not write where its * environment says. */ #if defined(__APPLE__) @@ -195,6 +201,33 @@ static int finish(struct Line *line, char *text) { return (int)(line->at - text); } +/* `address` relative to the object [lo, hi), for the second line. */ +static int describeRange(uintptr_t address, uintptr_t lo, uintptr_t hi, + const char *noun, char *text, size_t room) { + struct Line line = {text, text + room - 1}; + put(&line, "weavec: "); + putHex(&line, address); + if (address >= hi) { + put(&line, " is "); + putUnsigned(&line, address - hi, 10); + put(&line, " bytes after the "); + } else if (address < lo) { + put(&line, " is "); + putUnsigned(&line, lo - address, 10); + put(&line, " bytes before the "); + } else { + put(&line, " is "); + putUnsigned(&line, address - lo, 10); + put(&line, " bytes inside the "); + } + putUnsigned(&line, hi - lo, 10); + put(&line, "-byte "); + put(&line, noun); + put(&line, " at "); + putHex(&line, lo); + return finish(&line, text); +} + /* Where a heap address lies relative to its object, for the second line; * its size, 0 when there is nothing to say. */ static int describeHeap(uintptr_t address, char *text, size_t room) { @@ -202,26 +235,65 @@ static int describeHeap(uintptr_t address, char *text, size_t room) { struct Line line = {text, text + room - 1}; if (!object.found) return 0; - put(&line, "weavec: "); - putHex(&line, address); if (!object.live) { + put(&line, "weavec: "); + putHex(&line, address); put(&line, " is inside a released heap block at "); putHex(&line, object.base); - } else if (address >= object.base + object.size) { - put(&line, " is "); - putUnsigned(&line, address - object.base - object.size, 10); - put(&line, " bytes after the "); - putUnsigned(&line, object.size, 10); - put(&line, "-byte heap object at "); - putHex(&line, object.base); - } else { - return 0; + return finish(&line, text); } - return finish(&line, text); + if (address >= object.base + object.size) + return describeRange(address, object.base, object.base + object.size, + "heap object", text, room); + return 0; +} + +/* The first line of a report; false when report mode already reported the + * site. */ +static int reportLine(const char *kind, uintptr_t address, uint64_t width, + const struct __weavec_rt_site *site); + +static void stop(const struct __weavec_rt_site *site) { + const unsigned flags = site != NULL ? site->flags : 0; + if ((flags & WeavecRtSiteReport) != 0 && !shouldAbort()) + return; + __weavec_rt_trapping(); + if (backtraceSetting) { + void *frames[64]; + backtrace_symbols_fd(frames, backtrace(frames, 64), 2); + } + __builtin_trap(); +} + +void weavecRtFailObject(const char *kind, uintptr_t address, uint64_t width, + uintptr_t lo, uintptr_t hi, + const struct __weavec_rt_site *site) { + char text[256]; + int size; + if (!reportLine(kind, address, width, site)) + return; + size = describeRange(address, lo, hi, "destination", text, sizeof text); + if (size > 0) + emit(text, size); + stop(site); } void weavecRtFail(const char *kind, uintptr_t address, uint64_t width, const struct __weavec_rt_site *site) { + char text[256]; + int size; + if (!reportLine(kind, address, width, site)) + return; + if (strncmp(kind, "heap-", 5) == 0) { + size = describeHeap(address, text, sizeof text); + if (size > 0) + emit(text, size); + } + stop(site); +} + +static int reportLine(const char *kind, uintptr_t address, uint64_t width, + const struct __weavec_rt_site *site) { const unsigned flags = site != NULL ? site->flags : 0; const char *file = site != NULL && site->file != NULL ? site->file : NULL; const unsigned line = site != NULL ? site->line : 0; @@ -229,9 +301,8 @@ void weavecRtFail(const char *kind, uintptr_t address, uint64_t width, const int report = (flags & WeavecRtSiteReport) != 0 && !shouldAbort(); char text[1024]; struct Line out = {text, text + sizeof text - 1}; - int size; if (report && !firstReport(siteHash(kind, file, line, column))) - return; + return 0; put(&out, "weavec: "); if ((flags & WeavecRtSiteProven) != 0) put(&out, "weavec.proven: "); @@ -266,15 +337,7 @@ void weavecRtFail(const char *kind, uintptr_t address, uint64_t width, putHex(&out, address); } emit(text, finish(&out, text)); - if (strncmp(kind, "heap-", 5) == 0) { - size = describeHeap(address, text, sizeof text); - if (size > 0) - emit(text, size); - } - if (report) - return; - __weavec_rt_trapping(); - __builtin_trap(); + return 1; } /* Counted without synchronisation: a diagnostic aid, not an account. */ @@ -282,8 +345,9 @@ unsigned long long weavecRtStats[WeavecRtStatCount]; __attribute__((destructor)) static void printStats(void) { static const char *const Names[WeavecRtStatCount] = { - "allocations", "releases", "recycled slots", "huge blocks", - "slow guards", "range guards", "string guards", "stack unpoisons"}; + "allocations", "releases", "recycled slots", "huge blocks", + "slow guards", "range guards", "string guards", "stack unpoisons", + "sweeps"}; const char *value = getenv("WEAVEC_RT_STATS"); int i; /* (The owner's counters count every image's work.) */ @@ -301,6 +365,41 @@ __attribute__((destructor)) static void printStats(void) { (void)written; } } + { + struct weavecRtSweepCounts last; + char text[256]; + struct Line line = {text, text + sizeof text - 1}; + weavecRtLastSweep(&last); + put(&line, "weavec: runtime: sweeps took "); + putUnsigned(&line, weavecRtSweepNanos / 1000000, 10); + put(&line, " ms (longest "); + putUnsigned(&line, weavecRtSweepLongest / 1000000, 10); + put(&line, " ms); the last retained "); + putUnsigned(&line, last.retained, 10); + put(&line, " blocks ("); + putUnsigned(&line, last.retainedBytes, 10); + put(&line, " bytes)"); + { + const long written = (long)write(2, text, (size_t)finish(&line, text)); + (void)written; + } + } + { + size_t live; + size_t quarantine; + char text[160]; + struct Line line = {text, text + sizeof text - 1}; + weavecRtPeaks(&live, &quarantine); + put(&line, "weavec: runtime: at most "); + putUnsigned(&line, live >> 20, 10); + put(&line, " MiB of live slots and "); + putUnsigned(&line, quarantine >> 20, 10); + put(&line, " MiB of released ones waiting for a sweep"); + { + const long written = (long)write(2, text, (size_t)finish(&line, text)); + (void)written; + } + } } void __weavec_rt_fatal(const char *what, const void *p, const char *why) { diff --git a/runtime/weavec_rt.h b/runtime/weavec_rt.h index 650046fb..2f13b3ce 100644 --- a/runtime/weavec_rt.h +++ b/runtime/weavec_rt.h @@ -6,7 +6,8 @@ |* |*===----------------------------------------------------------------------===*| |* -|* RFC 0035, section 5. Shared by the runtime's own sources and its tests; +|* RFC 0035, section 5, and RFC 0036, section 5. Shared by the runtime's own +|* sources and its tests; |* not installed. Compiled code reaches the runtime through the entry points |* the guard pass calls (lib/Frontend/GuardPass*.cpp declares the same |* ones), the shadow descriptor it reads inline, and the standard allocation @@ -62,6 +63,8 @@ enum { WeavecRtShadowStackMid = 0xF2, WeavecRtShadowStackRight = 0xF3, WeavecRtShadowStackDynamic = 0xF4, + /* RFC 0036 section 5: poisoned by the program through ASan's interface. */ + WeavecRtShadowUser = 0xF7, WeavecRtShadowStackScope = 0xF8, WeavecRtShadowGlobal = 0xF9, WeavecRtShadowHeapTail = 0xFA, @@ -160,14 +163,22 @@ struct __weavec_rt_heap_t { WEAVEC_RT_API extern struct __weavec_rt_heap_t __weavec_rt_heap; -/* Slot states: the low two bits of a slot word. */ +/* A slot word is `value << 2 | state`: a live slot's value is its object's + * size, a free or dead slot's the index + 1 of the next slot on its list (0 + * for none). */ enum { WeavecRtNever = 0, WeavecRtLive = 1, WeavecRtFree = 2, - WeavecRtDead = 3 + WeavecRtDead = 3, + WeavecRtWordState = 3, + WeavecRtWordShift = 2 }; +static inline uint32_t weavecRtWordValue(uint32_t word) { + return word >> WeavecRtWordShift; +} + WEAVEC_RT_API void *__weavec_rt_alloc(size_t size, size_t alignment); WEAVEC_RT_API void __weavec_rt_free(void *p); WEAVEC_RT_API void *__weavec_rt_realloc(void *p, size_t size); @@ -228,25 +239,30 @@ WEAVEC_RT_API void __weavec_rt_overlap(uintptr_t destination, uintptr_t source, uint64_t n, const struct __weavec_rt_site *site); -/* Checked library calls (section 2.5). */ -WEAVEC_RT_API char *__weavec_rt_strcpy(char *d, const char *s, +/* Checked library calls (section 2.5). A writing call whose row has a + * fortified alias takes `limit`, the size of the object it writes at most + * (RFC 0036 section 6: a fortified call's object size; SIZE_MAX when + * unknown): after the call's arguments and before the site, or right after + * the site when the call is variadic. */ +WEAVEC_RT_API char *__weavec_rt_strcpy(char *d, const char *s, size_t limit, const struct __weavec_rt_site *site); -WEAVEC_RT_API char *__weavec_rt_stpcpy(char *d, const char *s, +WEAVEC_RT_API char *__weavec_rt_stpcpy(char *d, const char *s, size_t limit, const struct __weavec_rt_site *site); -WEAVEC_RT_API char *__weavec_rt_strcat(char *d, const char *s, +WEAVEC_RT_API char *__weavec_rt_strcat(char *d, const char *s, size_t limit, const struct __weavec_rt_site *site); WEAVEC_RT_API int __weavec_rt_sprintf(const struct __weavec_rt_site *site, - char *d, const char *format, ...); + size_t limit, char *d, + const char *format, ...); WEAVEC_RT_API int __weavec_rt_vsprintf(char *d, const char *format, - va_list arguments, + va_list arguments, size_t limit, const struct __weavec_rt_site *site); WEAVEC_RT_API int __weavec_rt_snprintf(const struct __weavec_rt_site *site, - char *d, size_t n, const char *format, - ...); + size_t limit, char *d, size_t n, + const char *format, ...); WEAVEC_RT_API int __weavec_rt_vsnprintf(char *d, size_t n, const char *format, - va_list arguments, + va_list arguments, size_t limit, const struct __weavec_rt_site *site); -WEAVEC_RT_API char *__weavec_rt_gets(char *d, +WEAVEC_RT_API char *__weavec_rt_gets(char *d, size_t limit, const struct __weavec_rt_site *site); /* The scanf family checks, after the call, what each conversion that ran * wrote. */ @@ -279,6 +295,42 @@ WEAVEC_RT_API int __weavec_rt_strcasecmp(const char *a, const char *b, WEAVEC_RT_API int __weavec_rt_strncasecmp(const char *a, const char *b, size_t n, const struct __weavec_rt_site *site); +/* RFC 0036 section 6: the number parsers check, after the call, the bytes + * the C library read: up to the end pointer and the character that stopped + * it, or, when nothing was converted, the leading white space and sign and + * the character after them. */ +WEAVEC_RT_API long __weavec_rt_strtol(const char *s, char **end, int base, + const struct __weavec_rt_site *site); +WEAVEC_RT_API long long __weavec_rt_strtoll(const char *s, char **end, + int base, + const struct __weavec_rt_site *site); +WEAVEC_RT_API unsigned long +__weavec_rt_strtoul(const char *s, char **end, int base, + const struct __weavec_rt_site *site); +WEAVEC_RT_API unsigned long long +__weavec_rt_strtoull(const char *s, char **end, int base, + const struct __weavec_rt_site *site); +WEAVEC_RT_API intmax_t __weavec_rt_strtoimax(const char *s, char **end, + int base, + const struct __weavec_rt_site *site); +WEAVEC_RT_API uintmax_t +__weavec_rt_strtoumax(const char *s, char **end, int base, + const struct __weavec_rt_site *site); +WEAVEC_RT_API double __weavec_rt_strtod(const char *s, char **end, + const struct __weavec_rt_site *site); +WEAVEC_RT_API float __weavec_rt_strtof(const char *s, char **end, + const struct __weavec_rt_site *site); +WEAVEC_RT_API long double +__weavec_rt_strtold(const char *s, char **end, + const struct __weavec_rt_site *site); +WEAVEC_RT_API int __weavec_rt_atoi(const char *s, + const struct __weavec_rt_site *site); +WEAVEC_RT_API long __weavec_rt_atol(const char *s, + const struct __weavec_rt_site *site); +WEAVEC_RT_API long long __weavec_rt_atoll(const char *s, + const struct __weavec_rt_site *site); +WEAVEC_RT_API double __weavec_rt_atof(const char *s, + const struct __weavec_rt_site *site); /* The mappings clear the shadow of what they map and unmap. */ WEAVEC_RT_API void *__weavec_rt_mmap(void *address, size_t length, int protection, int flags, int fd, @@ -287,7 +339,7 @@ WEAVEC_RT_API void *__weavec_rt_mmap(void *address, size_t length, WEAVEC_RT_API int __weavec_rt_munmap(void *address, size_t length, const struct __weavec_rt_site *site); -/* Frames (section 3). */ +/* Frames (RFC 0035 section 3). */ WEAVEC_RT_API void __weavec_rt_unpoison_stack(void); /* A dynamic stack object of `size` bytes at `address` (32-aligned), followed * by its redzone up to the next 32-byte boundary and 32 bytes more. */ @@ -296,7 +348,15 @@ WEAVEC_RT_API void __weavec_rt_alloca_poison(uintptr_t address, uint64_t size); * return frees. */ WEAVEC_RT_API void __weavec_rt_alloca_unpoison(uintptr_t low, uintptr_t high); -/* Globals (section 4): `n` records {address, size, size with redzone}. */ +/* A fortified call that writes `width` bytes at `address`, more than the + * `limit` its object size allows (RFC 0036 section 6). */ +WEAVEC_RT_API WEAVEC_RT_SLOW void +__weavec_rt_limit_fail(uintptr_t address, uint64_t width, uint64_t limit, + const struct __weavec_rt_site *site); + +/* Globals (section 4): `n` records {address, size, size with redzone}. On + * Darwin a copy that forwards registers them with the owner (RFC 0036 + * section 4). */ struct __weavec_rt_global { uintptr_t address; uint64_t size; @@ -315,6 +375,11 @@ __weavec_rt_globals_unregister(const struct __weavec_rt_global *globals, * the site asks for a report, which is printed once per site. */ void weavecRtFail(const char *kind, uintptr_t address, uint64_t width, const struct __weavec_rt_site *site); +/* As weavecRtFail, with a second line that places `address` relative to + * the destination [lo, hi) (RFC 0036 section 6). */ +void weavecRtFailObject(const char *kind, uintptr_t address, uint64_t width, + uintptr_t lo, uintptr_t hi, + const struct __weavec_rt_site *site); /* Called by a failure right before it traps, so that the trap ends the * program even where the program blocked or caught SIGTRAP and SIGILL. */ WEAVEC_RT_API void __weavec_rt_trapping(void); @@ -344,6 +409,9 @@ struct __weavec_rt_dispatch { size_t (*size)(const void *); struct weavecRtHeapObject (*object)(uintptr_t); void (*fatal)(const char *, const void *, const char *); + /* RFC 0036 section 4: version 4. */ + void (*registerGlobals)(const struct __weavec_rt_global *, uint64_t); + void (*unregisterGlobals)(const struct __weavec_rt_global *, uint64_t); }; WEAVEC_RT_API extern const struct __weavec_rt_dispatch __weavec_rt_dispatch; @@ -386,6 +454,38 @@ static inline const struct __weavec_rt_dispatch *weavecRtForward(void) { * no room for the arena. */ int weavecRtInitialise(void); +/* The runtime's own mappings, which the sweep does not scan. */ +void weavecRtInternal(uintptr_t low, uintptr_t high); +void weavecRtInternalDrop(uintptr_t low); + +/* The sweep (weavec_sweep.c), and the allocator's side of it. */ +struct weavecRtSweepCounts { + size_t recycled; + size_t retained; + size_t retainedBytes; +}; +void weavecRtSweep(void); +void weavecRtLastSweep(struct weavecRtSweepCounts *out); +extern unsigned long long weavecRtSweepNanos; +extern unsigned long long weavecRtSweepLongest; +/* RFC 0036 section 3.3: a sweep ended at `ended` (CLOCK_MONOTONIC + * nanoseconds) after `took`; the allocator paces the next one by it. */ +void weavecRtSwept(uint64_t ended, uint64_t took); +/* The most bytes the live heap's slots and the quarantine held, for + * WEAVEC_RT_STATS=1 (not synchronised). */ +void weavecRtPeaks(size_t *live, size_t *quarantine); +void weavecRtSweepLock(void); +void weavecRtSweepUnlock(void); +void weavecRtLockAll(void); +void weavecRtUnlockAll(void); +int weavecRtSweepWanted(void); +int weavecRtPrepareMarks(void); +void weavecRtMarkWords(uintptr_t low, uintptr_t high); +void weavecRtMarkHeap(void); +/* Moves what nothing marked to the free lists (unless `recycle` is 0), and + * clears the marks. */ +void weavecRtRecycle(struct weavecRtSweepCounts *counts, int recycle); + /* WEAVEC_RT_STATS=1: what the runtime did, printed when the program exits. */ enum { WeavecRtStatAllocations, @@ -396,6 +496,7 @@ enum { WeavecRtStatRanges, WeavecRtStatStrings, WeavecRtStatStackUnpoisons, + WeavecRtStatSweeps, WeavecRtStatCount }; extern unsigned long long weavecRtStats[WeavecRtStatCount]; diff --git a/runtime/weavec_sweep.c b/runtime/weavec_sweep.c new file mode 100644 index 00000000..81987db7 --- /dev/null +++ b/runtime/weavec_sweep.c @@ -0,0 +1,864 @@ +/*===- weavec_sweep.c - Reuse after a sweep -------------------------*- C -*-===*\ +|* +|* Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. +|* See LICENSE for license information. +|* SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +|* +|*===----------------------------------------------------------------------===*| +|* +|* RFC 0036, section 3.4. A released heap block waits in its class's +|* quarantine; a sweep moves it to the free list only when no pointer to it +|* is left: +|* +|* 1. stop every other thread (Darwin: thread_suspend; Linux: a signal +|* whose handler waits), holding every allocator lock; +|* 2. mark, scanning conservatively word by aligned word: each thread's +|* registers and stack, every readable and writable mapping of the +|* process but the runtime's own (of the dyld shared cache, only the +|* writable segments of the images the process loaded), and every live +|* heap block; +|* 3. recycle what nothing marked, and resume. +|* +|* A sweep that cannot stop every thread, or that finds a thread it did not +|* stop, recycles nothing: safety never depends on it running. +|* +|* The sweep runs on the thread whose release (or allocation) found the +|* quarantine over budget. It calls nothing that takes a lock another +|* thread could hold while stopped: no allocation, no stdio, no dyld. +|* +\*===----------------------------------------------------------------------===*/ + +#ifndef _GNU_SOURCE +#define _GNU_SOURCE 1 +#endif + +#include "weavec_rt.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(__APPLE__) +#include +#include +#include +#include +#else +#include +#include +#endif + +#ifndef MAP_ANONYMOUS +#define MAP_ANONYMOUS MAP_ANON +#endif + +/*===-- Internal mappings ---------------------------------------------------===*/ + +/* The runtime's own mappings, which hold no program pointer (the shadow, + * the arena's reservation and metadata, the huge table, the marks). */ +enum { InternalSlots = 512 }; +static struct { + uintptr_t low; + uintptr_t high; +} internalRanges[InternalSlots]; +static unsigned internalCount; +static WeavecRtLock internalLock; + +void weavecRtInternal(uintptr_t low, uintptr_t high) { + weavecRtLock(&internalLock); + if (internalCount < InternalSlots) { + internalRanges[internalCount].low = low; + internalRanges[internalCount].high = high; + ++internalCount; + } + weavecRtUnlock(&internalLock); +} + +void weavecRtInternalDrop(uintptr_t low) { + unsigned i; + weavecRtLock(&internalLock); + for (i = 0; i < internalCount; ++i) + if (internalRanges[i].low == low) { + internalRanges[i] = internalRanges[--internalCount]; + break; + } + weavecRtUnlock(&internalLock); +} + +/*===-- The stopped world ---------------------------------------------------===*/ + +/* A stretch of memory to scan, and the stacks: a thread's stack is scanned + * from its stack pointer (less the red zone below it) to the end of the + * mapping that holds it. */ +enum { MaxThreads = 4096 }; +#if defined(__x86_64__) +enum { RedZone = 128 }; +#else +enum { RedZone = 0 }; +#endif + +typedef struct { + uintptr_t sp; + /* The mapping that holds the stack pointer, found while stopped. */ + uintptr_t low; + uintptr_t high; +} StackWindow; + +typedef struct { + StackWindow stacks[MaxThreads + 1]; + unsigned stackCount; + /* Registers of the stopped threads (Darwin), scanned as words. */ + uintptr_t registers[MaxThreads][34]; + unsigned registerCount; +} World; + +/* The sweep's state, mapped once: too large for a stack. */ +static World *world; +static WeavecRtLock sweepLock; + +void weavecRtSweepLock(void) { weavecRtLock(&sweepLock); } +void weavecRtSweepUnlock(void) { weavecRtUnlock(&sweepLock); } + +static int trySweepLock(void) { + return __atomic_exchange_n(&sweepLock.held, 1, __ATOMIC_ACQUIRE) == 0; +} + +static uint64_t nanoseconds(void) { + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + return (uint64_t)now.tv_sec * 1000000000ULL + (uint64_t)now.tv_nsec; +} + +/* Sorts the internal ranges by their start, for scanExcluding. */ +static void sortInternal(void) { + unsigned i; + for (i = 1; i < internalCount; ++i) { + const uintptr_t low = internalRanges[i].low; + const uintptr_t high = internalRanges[i].high; + unsigned j = i; + while (j > 0 && internalRanges[j - 1].low > low) { + internalRanges[j] = internalRanges[j - 1]; + --j; + } + internalRanges[j].low = low; + internalRanges[j].high = high; + } +} + +/* Scans [low, high) but the runtime's own mappings in it (sorted). */ +static void scanExcluding(uintptr_t low, uintptr_t high) { + unsigned i; + for (i = 0; i < internalCount && low < high; ++i) { + const uintptr_t a = internalRanges[i].low; + const uintptr_t b = internalRanges[i].high; + if (b <= low || a >= high) + continue; + if (a > low) + weavecRtMarkWords(low, a); + low = b; + } + if (low < high) + weavecRtMarkWords(low, high); +} + +/* The first address at or after `address` outside the runtime's own + * mappings (sorted). */ +static uintptr_t skipInternal(uintptr_t address) { + unsigned i; + for (i = 0; i < internalCount; ++i) + if (address >= internalRanges[i].low && address < internalRanges[i].high) + address = internalRanges[i].high; + return address; +} + +/* Scans a writable mapping, a stack only from its stack pointer. */ +static void scanMapping(uintptr_t low, uintptr_t high) { + unsigned i; + for (i = 0; i < world->stackCount; ++i) { + const StackWindow *stack = &world->stacks[i]; + if (stack->sp >= low && stack->sp < high) { + const uintptr_t from = + stack->sp - RedZone > low ? stack->sp - RedZone : low; + scanExcluding(from, high); + return; + } + } + scanExcluding(low, high); +} + + +#if defined(__APPLE__) + +/*===-- Darwin --------------------------------------------------------------===*/ + +static thread_act_array_t threads; +static mach_msg_type_number_t threadCount; + +static int stopThreads(void) { + const thread_t self = mach_thread_self(); + mach_msg_type_number_t i; + int ok = 1; + (void)mach_port_deallocate(mach_task_self(), self); + if (task_threads(mach_task_self(), &threads, &threadCount) != KERN_SUCCESS) { + threads = NULL; + threadCount = 0; + return 0; + } + if (threadCount > MaxThreads) + ok = 0; + for (i = 0; i < threadCount && ok; ++i) { + if (threads[i] == self) + continue; + if (thread_suspend(threads[i]) != KERN_SUCCESS) + ok = 0; + } + /* The threads not suspended are not resumed either. */ + for (; i < threadCount; ++i) + if (threads[i] != self) { + (void)mach_port_deallocate(mach_task_self(), threads[i]); + threads[i] = MACH_PORT_NULL; + } + if (!ok) + return 0; + /* Each stopped thread's registers and stack pointer. */ + for (i = 0; i < threadCount; ++i) { + if (threads[i] == self || threads[i] == MACH_PORT_NULL) + continue; +#if defined(__aarch64__) + { + arm_thread_state64_t state; + mach_msg_type_number_t count = ARM_THREAD_STATE64_COUNT; + unsigned r; + if (thread_get_state(threads[i], ARM_THREAD_STATE64, + (thread_state_t)&state, &count) != KERN_SUCCESS) + return 0; + for (r = 0; r < 29; ++r) + world->registers[world->registerCount][r] = state.__x[r]; + world->registers[world->registerCount][29] = + (uintptr_t)arm_thread_state64_get_fp(state); + world->registers[world->registerCount][30] = + (uintptr_t)arm_thread_state64_get_lr(state); + world->stacks[world->stackCount].sp = + (uintptr_t)arm_thread_state64_get_sp(state); + } +#elif defined(__x86_64__) + { + x86_thread_state64_t state; + mach_msg_type_number_t count = x86_THREAD_STATE64_COUNT; + const uint64_t *raw = (const uint64_t *)&state; + unsigned r; + if (thread_get_state(threads[i], x86_THREAD_STATE64, + (thread_state_t)&state, &count) != KERN_SUCCESS) + return 0; + for (r = 0; r < sizeof state / sizeof(uint64_t) && r < 34; ++r) + world->registers[world->registerCount][r] = raw[r]; + world->stacks[world->stackCount].sp = state.__rsp; + } +#else + return 0; +#endif + ++world->registerCount; + ++world->stackCount; + } + return 1; +} + +static void resumeThreads(void) { + const thread_t self = mach_thread_self(); + mach_msg_type_number_t i; + (void)mach_port_deallocate(mach_task_self(), self); + for (i = 0; i < threadCount; ++i) { + if (threads[i] == MACH_PORT_NULL) + continue; + if (threads[i] != self) + (void)thread_resume(threads[i]); + (void)mach_port_deallocate(mach_task_self(), threads[i]); + } + (void)vm_deallocate(mach_task_self(), (vm_address_t)threads, + threadCount * sizeof(thread_t)); + threads = NULL; + threadCount = 0; +} + +/* Whether a thread exists that was not stopped. */ +static int threadsChanged(void) { + thread_act_array_t now; + mach_msg_type_number_t count; + mach_msg_type_number_t i; + int changed; + if (task_threads(mach_task_self(), &now, &count) != KERN_SUCCESS) + return 1; + changed = count != threadCount; + for (i = 0; i < count; ++i) + (void)mach_port_deallocate(mach_task_self(), now[i]); + (void)vm_deallocate(mach_task_self(), (vm_address_t)now, + count * sizeof(thread_t)); + return changed; +} + +/* The pages of [low, high) the program may have written (mincore); a page + * never touched, or a file's page never modified, holds nothing it wrote. */ +static void scanResident(uintptr_t low, uintptr_t high) { + enum { Pages = 4096 }; + const uintptr_t page = (uintptr_t)getpagesize(); + char vector[Pages]; + while (low < high) { + const uintptr_t chunk = + high - low > Pages * page ? Pages * page : high - low; + const uintptr_t count = (chunk + page - 1) / page; + uintptr_t i; + uintptr_t run = 0; + if (mincore((void *)low, (size_t)chunk, vector) != 0) { + scanMapping(low, low + chunk); + low += chunk; + continue; + } + /* Runs of touched pages, one scan each. */ + for (i = 0; i <= count; ++i) { + /* A page the program may have written: modified, compressed or + * paged out, or anonymous; not a clean page of a file (the shared + * cache's data as dyld mapped it). */ + const int touched = + i < count && + ((vector[i] & (MINCORE_MODIFIED | MINCORE_PAGED_OUT)) != 0 || + (vector[i] & (MINCORE_INCORE | MINCORE_ANONYMOUS)) == + (MINCORE_INCORE | MINCORE_ANONYMOUS)); + if (touched) + continue; + if (i > run) { + const uintptr_t end = low + i * page < low + chunk ? low + i * page + : low + chunk; + scanMapping(low + run * page, end); + } + run = i + 1; + } + low += chunk; + } +} + +#ifndef VM_MEMORY_VM_RECLAIM +#define VM_MEMORY_VM_RECLAIM 22 +#endif + +/* Whether a region may hold what the program wrote: readable and writable, + * with pages written or paged out (an untouched page reads zero, a clean + * page of a file holds the file), and not the kernel's reclaim ring, which + * holds only the system allocator's free ranges. The page counts cost one + * call per region; mincore then finds the pages, at about a microsecond + * each. */ +static int worthScanning(const vm_region_submap_info_data_64_t *info) { + return (info->protection & (VM_PROT_READ | VM_PROT_WRITE)) == + (VM_PROT_READ | VM_PROT_WRITE) && + info->share_mode != SM_EMPTY && + (info->pages_dirtied != 0 || info->pages_swapped_out != 0) && + info->user_tag != VM_MEMORY_VM_RECLAIM; +} + +/* The stack pointer of a stopped thread inside [low, high), or 0. */ +static uintptr_t stackIn(uintptr_t low, uintptr_t high) { + unsigned i; + for (i = 0; i < world->stackCount; ++i) + if (world->stacks[i].sp >= low && world->stacks[i].sp < high) + return world->stacks[i].sp; + return 0; +} + +/* RFC 0036 section 3.4: the writable segments of the images the process + * loaded from the dyld shared cache, kept from dyld's callbacks. The sweep + * scans them instead of the cache's writable mappings, which hold the data + * of every image in the cache (hundreds of megabytes the process never + * touched, whose pages mincore would be asked about on every sweep). */ +#ifndef MH_DYLIB_IN_CACHE +#define MH_DYLIB_IN_CACHE 0x80000000 +#endif +extern const void *_dyld_get_shared_cache_range(size_t *length); + +enum { ImageSegmentSlots = 4096 }; +static struct { + const struct mach_header *header; + uintptr_t low; + uintptr_t high; +} imageSegments[ImageSegmentSlots]; +static unsigned imageSegmentCount; +/* Taken by the callbacks and by the sweep before it stops the threads, so + * that no stopped thread holds it. */ +static WeavecRtLock imageLock; +static uintptr_t cacheLow; +static uintptr_t cacheHigh; + +static void addImage(const struct mach_header *header, intptr_t slide) { + const struct mach_header_64 *image = (const struct mach_header_64 *)header; + const struct load_command *command; + uint32_t i; + if (image->magic != MH_MAGIC_64 || (image->flags & MH_DYLIB_IN_CACHE) == 0) + return; + command = (const struct load_command *)(image + 1); + weavecRtLock(&imageLock); + for (i = 0; i < image->ncmds; ++i) { + if (command->cmd == LC_SEGMENT_64) { + const struct segment_command_64 *segment = + (const struct segment_command_64 *)command; + if ((segment->initprot & VM_PROT_WRITE) != 0 && segment->vmsize != 0 && + imageSegmentCount < ImageSegmentSlots) { + imageSegments[imageSegmentCount].header = header; + imageSegments[imageSegmentCount].low = + (uintptr_t)(segment->vmaddr + (uint64_t)slide); + imageSegments[imageSegmentCount].high = + (uintptr_t)(segment->vmaddr + (uint64_t)slide + segment->vmsize); + ++imageSegmentCount; + } + } + command = (const struct load_command *)((const char *)command + + command->cmdsize); + } + weavecRtUnlock(&imageLock); +} + +static void removeImage(const struct mach_header *header, intptr_t slide) { + unsigned i = 0; + (void)slide; + weavecRtLock(&imageLock); + while (i < imageSegmentCount) + if (imageSegments[i].header == header) + imageSegments[i] = imageSegments[--imageSegmentCount]; + else + ++i; + weavecRtUnlock(&imageLock); +} + +/* dyld calls addImage at once for every image already loaded. */ +__attribute__((constructor)) static void watchImages(void) { + size_t length = 0; + const void *start = _dyld_get_shared_cache_range(&length); + if (start == NULL || length == 0) + return; + cacheLow = (uintptr_t)start; + cacheHigh = cacheLow + length; + _dyld_register_func_for_add_image(addImage); + _dyld_register_func_for_remove_image(removeImage); +} + +static void lockImages(void) { weavecRtLock(&imageLock); } +static void unlockImages(void) { weavecRtUnlock(&imageLock); } + +/* Every readable, writable mapping that was touched, but the shared + * cache's, whose loaded images' writable segments are scanned instead. */ +static void scanMappings(void) { + mach_vm_address_t address = 0; + unsigned i; + for (;;) { + mach_vm_size_t size = 0; + natural_t depth = 0; + vm_region_submap_info_data_64_t info; + mach_msg_type_number_t count = VM_REGION_SUBMAP_INFO_COUNT_64; + /* The runtime's own reservations (the arena's committed chunks are a + * region each) are skipped whole, not asked about one by one. */ + address = skipInternal(address); + if (address >= cacheLow && address < cacheHigh) + address = cacheHigh; + if (mach_vm_region_recurse(mach_task_self(), &address, &size, &depth, + (vm_region_recurse_info_t)&info, + &count) != KERN_SUCCESS) + break; + if (!info.is_submap && worthScanning(&info) && + (address + size <= cacheLow || address >= cacheHigh)) { + const uintptr_t low = (uintptr_t)address; + const uintptr_t high = (uintptr_t)(address + size); + /* A stack is resident from its stack pointer up. */ + if (stackIn(low, high) != 0) + scanMapping(low, high); + else + scanResident(low, high); + } + address += size; + } + for (i = 0; i < imageSegmentCount; ++i) + scanResident(imageSegments[i].low, imageSegments[i].high); +} + +#else + +/*===-- Linux#else + +/*===-- Linux ---------------------------------------------------------------===*/ + +static int stopSignal; +static pid_t stopped[MaxThreads]; +static volatile uintptr_t stoppedSp[MaxThreads]; +static volatile uintptr_t stoppedInterruptedSp[MaxThreads]; +static unsigned stopTargets; +static int stoppedCount; +static int stopGeneration; +/* Whether a stop is in progress: a signal delivered after the sweep resumed + * the threads is answered at once. */ +static int stopping; + +static long futex(int *address, int op, int value, + const struct timespec *timeout) { + return syscall(SYS_futex, address, op, value, timeout, NULL, 0); +} + +static void stopHandler(int signal, siginfo_t *info, void *context) { + const int saved = errno; + const pid_t self = (pid_t)syscall(SYS_gettid); + const int generation = __atomic_load_n(&stopGeneration, __ATOMIC_ACQUIRE); + volatile uintptr_t here = 0; + unsigned i; + (void)signal; + (void)info; + if (!__atomic_load_n(&stopping, __ATOMIC_ACQUIRE)) + return; + __builtin_unwind_init(); + for (i = 0; i < stopTargets; ++i) + if (stopped[i] == self) { + stoppedSp[i] = (uintptr_t)&here; + { + const ucontext_t *uc = (const ucontext_t *)context; +#if defined(__x86_64__) + stoppedInterruptedSp[i] = (uintptr_t)uc->uc_mcontext.gregs[REG_RSP]; +#elif defined(__aarch64__) + stoppedInterruptedSp[i] = (uintptr_t)uc->uc_mcontext.sp; +#else + (void)uc; +#endif + } + break; + } + __atomic_add_fetch(&stoppedCount, 1, __ATOMIC_RELEASE); + (void)futex(&stoppedCount, FUTEX_WAKE_PRIVATE, 1, NULL); + while (__atomic_load_n(&stopGeneration, __ATOMIC_ACQUIRE) == generation) + (void)futex(&stopGeneration, FUTEX_WAIT_PRIVATE, generation, NULL); + errno = saved; +} + +/* The signal that stops threads: SIGPWR, WEAVEC_RT_SWEEP_SIGNAL, or SIGXCPU + * when the program handles SIGPWR; 0 when none is free. */ +__attribute__((constructor)) static void readSweepSignal(void) { + const char *text = getenv("WEAVEC_RT_SWEEP_SIGNAL"); + stopSignal = text != NULL ? atoi(text) : 0; +} + +static int installHandler(void) { + static int installed; + static const int Candidates[2] = {SIGPWR, SIGXCPU}; + int i; + if (installed) + return stopSignal != 0; + installed = 1; + for (i = stopSignal != 0 ? -1 : 0; i < 2; ++i) { + const int signal = i < 0 ? stopSignal : Candidates[i]; + struct sigaction old; + struct sigaction action; + if (sigaction(signal, NULL, &old) != 0) + continue; + if (i >= 0 && ((old.sa_flags & SA_SIGINFO) != 0 || + (old.sa_handler != SIG_DFL && old.sa_handler != SIG_IGN))) + continue; + memset(&action, 0, sizeof action); + action.sa_sigaction = stopHandler; + action.sa_flags = SA_SIGINFO | SA_RESTART; + sigfillset(&action.sa_mask); + if (sigaction(signal, &action, NULL) == 0) { + stopSignal = signal; + return 1; + } + } + stopSignal = 0; + return 0; +} + +/* The tasks of the process, by reading /proc/self/task with getdents64 + * (opendir allocates). */ +static unsigned listTasks(pid_t *out, unsigned max, int *overflow) { + char buffer[4096]; + unsigned count = 0; + const int fd = open("/proc/self/task", O_RDONLY | O_DIRECTORY | O_CLOEXEC); + *overflow = 0; + if (fd < 0) + return 0; + for (;;) { + const long got = syscall(SYS_getdents64, fd, buffer, sizeof buffer); + long at = 0; + if (got <= 0) + break; + while (at < got) { + const unsigned short length = *(unsigned short *)(buffer + at + 16); + const char *name = buffer + at + 19; + if (name[0] >= '0' && name[0] <= '9') { + if (count < max) + out[count++] = (pid_t)atoi(name); + else + *overflow = 1; + } + at += length; + } + } + (void)close(fd); + return count; +} + +static pid_t tasks[MaxThreads]; +static unsigned taskCount; + +static int stopThreads(void) { + const pid_t self = (pid_t)syscall(SYS_gettid); + const pid_t process = getpid(); + int overflow; + unsigned i; + unsigned sent = 0; + uint64_t deadline; + taskCount = listTasks(tasks, MaxThreads, &overflow); + if (overflow || taskCount == 0) + return 0; + if (taskCount == 1) + return 1; + if (!installHandler()) + return 0; + stopTargets = 0; + for (i = 0; i < taskCount; ++i) + if (tasks[i] != self) { + stoppedSp[stopTargets] = 0; + stoppedInterruptedSp[stopTargets] = 0; + stopped[stopTargets++] = tasks[i]; + } + __atomic_store_n(&stoppedCount, 0, __ATOMIC_RELEASE); + __atomic_store_n(&stopping, 1, __ATOMIC_RELEASE); + for (i = 0; i < stopTargets; ++i) { + if (syscall(SYS_tgkill, process, stopped[i], stopSignal) == 0) + ++sent; + else + stopped[i] = 0; + } + /* A thread that does not answer within a second (it blocks the signal) + * holds up recycling. */ + deadline = nanoseconds() + 1000000000ULL; + for (;;) { + const int now = __atomic_load_n(&stoppedCount, __ATOMIC_ACQUIRE); + struct timespec wait = {0, 1000000}; + if ((unsigned)now >= sent) + break; + if (nanoseconds() > deadline) + return 0; + (void)futex(&stoppedCount, FUTEX_WAIT_PRIVATE, now, &wait); + } + for (i = 0; i < stopTargets; ++i) { + if (stopped[i] == 0 || stoppedSp[i] == 0) + continue; + world->stacks[world->stackCount].sp = stoppedSp[i]; + ++world->stackCount; + if (stoppedInterruptedSp[i] != 0) { + world->stacks[world->stackCount].sp = stoppedInterruptedSp[i]; + ++world->stackCount; + } + } + return 1; +} + +static void resumeThreads(void) { + __atomic_store_n(&stopping, 0, __ATOMIC_RELEASE); + __atomic_add_fetch(&stopGeneration, 1, __ATOMIC_RELEASE); + (void)futex(&stopGeneration, FUTEX_WAKE_PRIVATE, 0x7fffffff, NULL); + /* Every stopped thread has left the handler's wait before the next stop + * can reuse the table: they leave on the generation, which only grows. */ +} + +static int threadsChanged(void) { + pid_t now[MaxThreads]; + int overflow; + const unsigned count = listTasks(now, MaxThreads, &overflow); + unsigned i; + unsigned j; + if (overflow || count != taskCount) + return 1; + for (i = 0; i < count; ++i) { + for (j = 0; j < taskCount && tasks[j] != now[i]; ++j) { + } + if (j == taskCount) + return 1; + } + return 0; +} + +/* Parses a hexadecimal number at *at. */ +static uintptr_t hex(const char **at) { + uintptr_t value = 0; + for (;;) { + const char c = **at; + unsigned digit; + if (c >= '0' && c <= '9') + digit = (unsigned)(c - '0'); + else if (c >= 'a' && c <= 'f') + digit = (unsigned)(c - 'a' + 10); + else + return value; + value = value * 16 + digit; + ++*at; + } +} + +/* /proc/self/maps, read whole into a buffer of the runtime's own. */ +static char *mapsText; +static size_t mapsRoom; + +static size_t readMaps(void) { + size_t used = 0; + const int fd = open("/proc/self/maps", O_RDONLY | O_CLOEXEC); + if (fd < 0) + return 0; + for (;;) { + long got; + if (used + 4096 > mapsRoom) { + const size_t room = mapsRoom == 0 ? (size_t)1 << 20 : mapsRoom * 2; + char *grown = (char *)mmap(NULL, room, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if ((void *)grown == MAP_FAILED) { + (void)close(fd); + return 0; + } + weavecRtInternal((uintptr_t)grown, (uintptr_t)grown + room); + if (mapsText != NULL) { + memcpy(grown, mapsText, used); + weavecRtInternalDrop((uintptr_t)mapsText); + (void)munmap(mapsText, mapsRoom); + } + mapsText = grown; + mapsRoom = room; + } + got = read(fd, mapsText + used, mapsRoom - used - 1); + if (got <= 0) + break; + used += (size_t)got; + } + (void)close(fd); + mapsText[used] = 0; + return used; +} + +static void scanMappings(void) { + const char *at; + if (readMaps() == 0) + return; + /* The stacks' mappings first. */ + at = mapsText; + while (*at != 0) { + const char *line = at; + const uintptr_t low = hex(&at); + uintptr_t high; + unsigned i; + ++at; + high = hex(&at); + for (i = 0; i < world->stackCount; ++i) + if (world->stacks[i].sp >= low && world->stacks[i].sp < high) { + world->stacks[i].low = low; + world->stacks[i].high = high; + } + while (*at != 0 && *at != '\n') + ++at; + if (*at == '\n') + ++at; + (void)line; + } + at = mapsText; + while (*at != 0) { + const uintptr_t low = hex(&at); + uintptr_t high; + int readable; + int writable; + ++at; + high = hex(&at); + ++at; + readable = at[0] == 'r'; + writable = at[1] == 'w'; + if (readable && writable) { + /* [vvar] and [vsyscall] are never rw; a device mapping might be. */ + const char *name = at; + while (*name != 0 && *name != '\n' && *name != '[' && *name != '/') + ++name; + if (strncmp(name, "/dev/", 5) != 0) + scanMapping(low, high); + } + while (*at != 0 && *at != '\n') + ++at; + if (*at == '\n') + ++at; + } +} + +static void lockImages(void) {} +static void unlockImages(void) {} + +#endif + +/*===-- The sweep -----------------------------------------------------------===*/ + +static struct weavecRtSweepCounts lastCounts; +unsigned long long weavecRtSweepNanos; +unsigned long long weavecRtSweepLongest; + +static int mapWorld(void) { + void *map; + if (world != NULL) + return 1; + map = mmap(NULL, sizeof(World), PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (map == MAP_FAILED) + return 0; + weavecRtInternal((uintptr_t)map, (uintptr_t)map + sizeof(World)); + world = (World *)map; + return 1; +} + +void weavecRtSweep(void) { + uint64_t started; + struct weavecRtSweepCounts counts = {0, 0, 0}; + int stoppedAll; + /* The calling thread's callee-saved registers are spilled to this frame, + * which its stack scan covers: it is scanned from `here`. */ + volatile uintptr_t here = 0; + __builtin_unwind_init(); + if (weavecRtForward() != 0 || !weavecRtSweepWanted() || !trySweepLock()) + return; + if (!mapWorld()) { + weavecRtSweepUnlock(); + return; + } + started = nanoseconds(); + lockImages(); + weavecRtLockAll(); + world->stackCount = 0; + world->registerCount = 0; + stoppedAll = stopThreads(); + if (stoppedAll && weavecRtPrepareMarks()) { + unsigned i; + world->stacks[world->stackCount].sp = (uintptr_t)&here; + ++world->stackCount; + sortInternal(); + for (i = 0; i < world->registerCount; ++i) + weavecRtMarkWords((uintptr_t)world->registers[i], + (uintptr_t)(world->registers[i] + 34)); + scanMappings(); + weavecRtMarkHeap(); + weavecRtRecycle(&counts, !threadsChanged()); + weavecRtCount(WeavecRtStatSweeps); + lastCounts = counts; + } + resumeThreads(); + weavecRtUnlockAll(); + unlockImages(); + { + const uint64_t ended = nanoseconds(); + const uint64_t took = ended - started; + weavecRtSweepNanos += took; + if (took > weavecRtSweepLongest) + weavecRtSweepLongest = took; + weavecRtSwept(ended, took); + } + weavecRtSweepUnlock(); +} + +void weavecRtLastSweep(struct weavecRtSweepCounts *out) { *out = lastCounts; } diff --git a/scripts/check-hygiene.py b/scripts/check-hygiene.py index 103ea90f..bd4e40ce 100755 --- a/scripts/check-hygiene.py +++ b/scripts/check-hygiene.py @@ -1,16 +1,8 @@ #!/usr/bin/env python3 -"""Check repository hygiene: gate H2 of RFC 0030 (docs/rfcs/0030-prove-or-trap.md) -as RFC 0031 (docs/rfcs/0031-object-engine.md, section 2) moves it to the object engine. - -Seven checks, one per bullet of H2 (its fourth bullet is split in two): - - retired-name 0 occurrences of SafetyState, CheckedContract, - checkContracts or --checked, or of the old engine's - FunctionDataflow, DataflowEngine, TranslationUnitAnalyzer, - FunctionAnalyzer, SummaryStore or FunctionSummary, in - lib/, tools/ and docs/ outside docs/rfcs/ (superseded - RFCs keep their text). Plain case-sensitive substrings: - --checked-report counts. +"""Check repository hygiene: gate H1 of RFC 0036 (docs/rfcs/0036-deterministic-heap-reuse.md). + +Seven checks, the rules of AGENTS.md that a script can hold the tree to: + library-name-test 0 `== "NAME"` or `"NAME" ==` comparisons (any whitespace around ==) in the C and C++ sources (.h .hpp .cpp .cc .c .def .inc) of lib/, include/ and tools/ outside @@ -18,60 +10,39 @@ lib/Core/LibrarySpec.txt answers to: an entry, a chk() alias (a fortified form), or the __builtin_ spelling of either. Comparisons inside comments do not count. + Library behaviour goes in the table, which the guard + passes read through GuardLibrary.cpp. corpus-word 0 occurrences of the words cJSON, jansson, linenoise, - jsmn, zlib, lua, Lua, sds and minigzip, and of RFC 0033's - fresh and sealed projects zstd, libuv, oniguruma, redis, - expat, pcre2, libevent, libsodium, libxml2, libpng, - mbedtls, msgpack and yyjson, in any file under lib/, + jsmn, zlib, lua, Lua, sds and minigzip, and of the + corpus's later projects (zstd, libuv, oniguruma, redis, + expat, ...; CORPUS_WORDS below), in any file under lib/, comments included (the word rule is below). - engine-include only lib/Analysis/Engine*.cpp and - lib/Analysis/ObjectEngine.cpp include Engine.h (the - engine's private header): no other file under lib/ or - include/ includes a header whose last path component is - Engine.h. Nor does a file of a new component (RFC 0030 - section 1: every file under lib/ or include/ whose name - starts with SiteCollector, AttributeReader, - KindInference, SlotCollector, BoundaryInvariants, - CheckPlanner or LedgerAdapter) include it through the - project headers it includes. The include chain is - reported. - engine-sink the engine receives no DiagnosticSink (it publishes only - through LedgerAdapter): no identifier containing - DiagnosticSink (ClangDiagnosticSink too) in - lib/Analysis/Engine*.{h,cpp}, lib/Analysis/ObjectEngine.cpp - or include/weavec/Analysis/ObjectEngine.h, comments and - string literals ignored. A missing lib/Analysis/Engine.h - is a violation. - engine-lines lib/Analysis/Engine*.h and lib/Analysis/Engine*.cpp - (EngineIntegers.h included) total at most 21,500 lines. + core-include lib/Core and include/weavec/Core include no Clang or LLVM + header: no include whose path starts with clang/, + clang-c/, llvm/ or llvm-c/ in their C and C++ sources. + runtime-include the runtime (runtime/, its tests included) includes no + WeaveC header but its own weavec_rt.h: every quoted + include names weavec_rt.h (by its last path component), + and no angled include's path starts with weavec. + runtime-entry every runtime entry point the passes name is declared in + runtime/weavec_rt.h: each `runtime("NAME"` call (the + pass's declaration of __weavec_rt_NAME) and each + "__weavec_rt_NAME" string literal in the C and C++ + sources of lib/Frontend, and each wrapper(NAME) clause of + lib/Core/LibrarySpec.txt (the passes redirect the call to + __weavec_rt_NAME). A name the header does not contain as + an identifier (comments ignored) is a violation, and so + is a missing header. A name the passes compute (passed + through a variable) is not seen. library-lines the code under lib/, include/ and tools/ totals at most - 57,000 lines. lib/Core/LibrarySpec.txt does not count: - it is a declarative table, and its growth is coverage, - not sprawl. Both limits are ratchets (RFC 0030 gate H2): - they are lowered as code is deleted, and raised only by - an RFC amendment that records the measurement. RFC 0031 - stage S7 recorded them: the measurement at the end of S7 - (19,654 and 64,367 lines), rounded up to a multiple of - 500. RFC 0032 (gate H1, Implementation amendments) - raised the library limit to 69,000 for the planner's - guards, the range caches, object registration and the - driver's runtime handling (68,744 lines measured). - RFC 0033 (gate H1, Implementation amendments) raised - them to 21,500 and 70,000 for the witness rule, the - length guards, the link modes and the unit budget - (21,078 and 69,980 lines measured). RFC 0034 (gate - H1, Implementation amendments) raised them to 22,000 - and 72,000 for the shadow guards' passes, confirmation - by replay and the work and memory budgets. RFC 0035 - (stage S6) lowered them to 21,500 and 58,000 once the - check planner, the unit records, the link step and the - analysis ledger were deleted (21,121 and 57,963 lines - measured), and the library limit to 57,000 for gate H1 - once the summary text format, the link-only paths and - the code only tests used were deleted (55,992 lines - measured). + 9,000 lines (RFC 0036 gate H1; the headers and the + driver's tool count too). lib/Core/LibrarySpec.txt does + not count: it is a declarative table, and its growth is + coverage, not sprawl. The limit is a ratchet: it is + lowered as code is deleted, and raised only by an RFC + amendment that records the measurement. runtime-lines the C runtime (runtime/, its tests under runtime/test/ - excluded) totals at most 4,000 lines (RFC 0032 gate H1). + excluded) totals at most 6,000 lines (RFC 0036 gate H1). It is counted apart from the libraries: it is linked into compiled programs, not into the compiler. @@ -80,37 +51,31 @@ Underscores and other punctuation are boundaries, so cJSON_IsString, lua_State and are violations, while evaluation, sdsnew, LUA and Zlib are not. -engine-include ignores commented-out includes; it resolves an include against -the including file's directory, lib/Analysis/ and include/, follows only files -under lib/ and include/, and counts every include whose last component is -Engine.h, resolved or not (SafetyEngine.h and ObjectEngine.h are other -headers). +The include checks ignore commented-out includes. Files come from `git ls-files --cached --others --exclude-standard`, so ignored -build output (docs/node_modules, docs/dist, docs/.astro, ...) is never read -while new untracked files are; files deleted in the working tree are skipped. -When the root is not the top of a git work tree (a source archive), the -directories are walked instead, skipping node_modules, dist, .generated, -.astro, test-results, playwright-report, __pycache__, .git and build*. -Symbolic links are skipped, and so are binary files (a NUL byte in the first -8 KiB), by every check, the line counts included. Text is read as UTF-8 with -invalid bytes replaced. Lines are counted as `wc -l` counts them, plus one -for a last line that has no trailing newline. +build output is never read while new untracked files are; files deleted in +the working tree are skipped. When the root is not the top of a git work tree +(a source archive), the directories are walked instead, skipping +node_modules, dist, .generated, .astro, test-results, playwright-report, +__pycache__, .git and build*. Symbolic links are skipped, and so are binary +files (a NUL byte in the first 8 KiB), by every check, the line counts +included. Text is read as UTF-8 with invalid bytes replaced. Lines are +counted as `wc -l` counts them, plus one for a last line that has no trailing +newline. Exit status: 0 when every check passes, 1 when any fails, 2 on a usage error. Examples: scripts/check-hygiene.py - scripts/check-hygiene.py --root ~/src/weavec-0.11.0 --json hygiene.json + scripts/check-hygiene.py --root ~/src/weavec-0.17.0 --json hygiene.json """ from __future__ import annotations import argparse -import collections import dataclasses -import fnmatch import json import os import posixpath @@ -119,40 +84,28 @@ import subprocess import sys from pathlib import Path -from typing import Callable, Container, Iterator, Optional +from typing import Iterator, Optional ROOT = Path(__file__).resolve().parent.parent # --------------------------------------------------------------------------- -# Gate H2 (RFC 0030, Acceptance gates, Hygiene; RFC 0031, section 2) +# Gate H1 (RFC 0036, Acceptance gates, Hygiene) # --------------------------------------------------------------------------- -# Recorded by RFC 0031 stage S7 and RFC 0032, 0033 and 0034 gate H1 (see the -# module docstring). -ENGINE_LINE_LIMIT = 21_500 -LIBRARY_LINE_LIMIT = 57_000 -RUNTIME_LINE_LIMIT = 4_000 - -# retired-name -RETIRED_CHECKED_NAMES = ("SafetyState", "CheckedContract", "checkContracts", "--checked") -# The old engine's (RFC 0031 section 1). -RETIRED_ENGINE_NAMES = ("FunctionDataflow", "DataflowEngine", "TranslationUnitAnalyzer", - "FunctionAnalyzer", "SummaryStore", "FunctionSummary") -RETIRED_NAMES = RETIRED_CHECKED_NAMES + RETIRED_ENGINE_NAMES -RETIRED_NAME_DIRS = ("lib", "tools", "docs") -RETIRED_NAME_EXEMPT = ("docs/rfcs/",) -# The pages that still describe the old engine until the rewrite that RFC -# 0031 section 10 asks for lands. Remove each one as it is rewritten. -# library-name-test (section 8) +# Recorded by RFC 0036 gate H1 and RFC 0032 gate H1 (see the module docstring). +LIBRARY_LINE_LIMIT = 9_000 +RUNTIME_LINE_LIMIT = 6_000 + +# library-name-test LIBRARY_SPEC = "lib/Core/LibrarySpec.txt" LIBRARY_SPEC_PREFIX = "lib/Core/LibrarySpec" LIBRARY_DIRECTIVES = ("header", "builtins") BUILTIN_PREFIX = "__builtin_" SOURCE_DIRS = ("lib", "include", "tools") SOURCE_SUFFIXES = (".h", ".hpp", ".cpp", ".cc", ".c", ".def", ".inc") -# corpus-word (the corpus projects of section 17.5, and RFC 0033 section 11's -# fresh and sealed projects) +# corpus-word (test/corpus/manifest.json's projects, by the RFC that chose them) CORPUS_WORDS = ("cJSON", "jansson", "linenoise", "jsmn", "zlib", "lua", "Lua", "sds", "minigzip", + # RFC 0033, section 11. "zstd", "libuv", "oniguruma", "redis", "expat", "pcre2", "libevent", "libsodium", "libxml2", "libpng", "mbedtls", "msgpack", "yyjson", # RFC 0034, section 9. @@ -160,37 +113,28 @@ "libdeflate", "zlib-ng", "curl", "cmark", "libgit2", "libjpeg", "opus", "flac", "giflib", "wren") CORPUS_WORD_DIRS = ("lib",) -# engine-include (RFC 0030 sections 1 and 14, RFC 0031 section 2) -NEW_COMPONENTS = ("SiteCollector", "AttributeReader", "KindInference", "SlotCollector", - "BoundaryInvariants", "CheckPlanner", "LedgerAdapter") -COMPONENT_DIRS = ("lib", "include") -ENGINE_HEADER = "Engine.h" -ENGINE_DIR = "lib/Analysis" -ENGINE_INCLUDERS = ("Engine*.cpp", "ObjectEngine.cpp") # in ENGINE_DIR -# Searched after the including file's directory. -INCLUDE_PATH = ("lib/Analysis", "include") -# engine-sink (RFC 0030 section 14) -ENGINE_PRIVATE_HEADER = "lib/Analysis/Engine.h" -ENGINE_SINK_PATTERNS = ("Engine*.h", "Engine*.cpp", "ObjectEngine.cpp") # in ENGINE_DIR -ENGINE_PUBLIC_HEADER = "include/weavec/Analysis/ObjectEngine.h" -DIAGNOSTIC_SINK = "DiagnosticSink" -# engine-lines and library-lines (RFC 0030 section 18, The line budget) -ENGINE_LINE_PATTERNS = ("Engine*.h", "Engine*.cpp") # in ENGINE_DIR +# core-include (AGENTS.md, rule 1) +CORE_DIRS = ("lib/Core", "include/weavec/Core") +FOREIGN_INCLUDE_PREFIXES = ("clang/", "clang-c/", "llvm/", "llvm-c/") +# runtime-include and runtime-entry (AGENTS.md, rule 9) +RUNTIME_HEADER = "runtime/weavec_rt.h" +RUNTIME_SOURCE_SUFFIXES = (".c", ".h") +RUNTIME_PREFIX = "__weavec_rt_" +PASS_DIRS = ("lib/Frontend",) +# library-lines and runtime-lines LIBRARY_DIRS = ("lib", "include", "tools") # What a total's violation line shows in place of a path. -ENGINE_TOTAL = "lib/Analysis/Engine*.{h,cpp}" LIBRARY_TOTAL = "{lib,include,tools}/**" RUNTIME_DIRS = ("runtime",) RUNTIME_TESTS = "runtime/test/" RUNTIME_TOTAL = "runtime/**" CHECKS = ( - ("retired-name", "checked-mode or old-engine names outside docs/rfcs/"), ("library-name-test", '== "" against a LibrarySpec name outside lib/Core/LibrarySpec*'), ("corpus-word", "a corpus project's name (cJSON, jansson, ..., zstd, redis, ...) in lib/"), - ("engine-include", "Engine.h included outside Engine*.cpp and ObjectEngine.cpp"), - ("engine-sink", "the engine naming a DiagnosticSink"), - ("engine-lines", "lib/Analysis/Engine*.{h,cpp} over their line limit"), + ("core-include", "a Clang or LLVM header included by lib/Core or include/weavec/Core"), + ("runtime-include", "a WeaveC header other than weavec_rt.h included by runtime/"), + ("runtime-entry", "a runtime entry point the passes name that runtime/weavec_rt.h does not declare"), ("library-lines", "lib/, include/ and tools/ over their line limit"), ("runtime-lines", "runtime/ (without runtime/test/) over its line limit"), ) @@ -199,7 +143,7 @@ # File selection # --------------------------------------------------------------------------- -SCANNED_DIRS = ("lib", "include", "tools", "docs", "runtime") +SCANNED_DIRS = ("lib", "include", "tools", "runtime") WALK_SKIPPED_DIRS = frozenset(("node_modules", "dist", ".generated", ".astro", "test-results", "playwright-report", "__pycache__", ".git")) WALK_SKIPPED_PREFIX = "build" @@ -227,7 +171,7 @@ class Result: unreadable: list[str] = dataclasses.field(default_factory=list) library_entries: Optional[int] = None library_aliases: Optional[int] = None - engine_lines: dict[str, int] = dataclasses.field(default_factory=dict) + entry_points: dict[str, bool] = dataclasses.field(default_factory=dict) library_lines: dict[str, int] = dataclasses.field(default_factory=dict) runtime_lines: dict[str, int] = dataclasses.field(default_factory=dict) violations: list[Violation] = dataclasses.field(default_factory=list) @@ -443,22 +387,6 @@ def include_directives(code: str) -> list[tuple[int, str, str]]: return found -def resolve_include(including: str, path: str, known: Container[str]) -> Optional[str]: - """The file among `known` that `#include "path"` in `including` names: - relative to the including file's directory, then to INCLUDE_PATH.""" - for base in (posixpath.dirname(including), *INCLUDE_PATH): - candidate = posixpath.normpath(posixpath.join(base, path)) - if candidate in known: - return candidate - return None - - -def identifiers_containing(text: str, word: str) -> list[tuple[int, str]]: - """(offset, identifier) of each identifier in `text` that contains `word`.""" - pattern = re.compile(r"(? LibraryNames: # --------------------------------------------------------------------------- -# Checks 1 to 3: text +# Checks 1 and 2: text # --------------------------------------------------------------------------- @@ -536,13 +464,6 @@ def find_all(pattern: re.Pattern, text: str) -> Iterator[tuple[int, int, str]]: yield number, match.start() + 1, match.group() -def check_retired_names(tree: Tree, result: Result) -> None: - pattern = re.compile("|".join(re.escape(name) for name in RETIRED_NAMES)) - for file in tree.files(RETIRED_NAME_DIRS, exclude=RETIRED_NAME_EXEMPT): - for number, column, name in find_all(pattern, file.text): - result.add("retired-name", file.path, number, f"'{name}' at column {column}") - - def _is_word(character: str) -> bool: return character.isalnum() or character == "_" @@ -612,83 +533,67 @@ def check_corpus_words(tree: Tree, result: Result) -> None: # --------------------------------------------------------------------------- -# Checks 4 and 5: the engine seam (RFC 0030 section 14, "Two rules keep the -# seam honest"; RFC 0031 section 2) +# Checks 3 to 5: layering (AGENTS.md, rules 1 and 9) # --------------------------------------------------------------------------- -def _in_engine_dir(path: str, patterns: tuple[str, ...]) -> bool: - """Whether `path` is directly in ENGINE_DIR and its name matches one of - `patterns`.""" - name = posixpath.basename(path) - return (posixpath.dirname(path) == ENGINE_DIR - and any(fnmatch.fnmatchcase(name, pattern) for pattern in patterns)) - - -def _shortest_chain(start: str, includes: Callable[[str], list[tuple[int, str, str]]], - resolve: Callable[[str, str], Optional[str]]) -> Optional[list[str]]: - """The shortest include chain from `start` to Engine.h: `path:line` for - each include followed, then Engine.h as resolved (or as spelled).""" - parents: dict[str, Optional[tuple[str, int]]] = {start: None} - queue = collections.deque([start]) - while queue: - node = queue.popleft() - for line, spelling, included in includes(node): - target = resolve(node, included) - if posixpath.basename(included) == ENGINE_HEADER: - hops = [f"{node}:{line}", target or spelling] - while parents[node] is not None: - node, parent_line = parents[node] - hops.insert(0, f"{node}:{parent_line}") - return hops - if target is not None and target not in parents: - parents[target] = (node, line) - queue.append(target) - return None - - -def check_engine_includes(tree: Tree, result: Result) -> None: - files = {file.path: file for file in tree.files(COMPONENT_DIRS)} - directives: dict[str, list[tuple[int, str, str]]] = {} - chains: dict[str, Optional[list[str]]] = {} - - def includes(path: str) -> list[tuple[int, str, str]]: - if path not in directives: - directives[path] = include_directives(files[path].lexed.code) - return directives[path] - - def resolve(including: str, path: str) -> Optional[str]: - return resolve_include(including, path, files) - - for path in files: - if _in_engine_dir(path, ENGINE_INCLUDERS): - continue # no new component's name starts with Engine - component = posixpath.basename(path).startswith(NEW_COMPONENTS) - for line, spelling, included in includes(path): - target = resolve(path, included) - if posixpath.basename(included) == ENGINE_HEADER: - result.add("engine-include", path, line, - f"includes Engine.h directly: {path}:{line} -> {target or spelling}") - continue - if not component or target is None: - continue - if target not in chains: - chains[target] = _shortest_chain(target, includes, resolve) - if chains[target] is not None: - chain = " -> ".join([f"{path}:{line}", *chains[target]]) - result.add("engine-include", path, line, f"includes Engine.h transitively: {chain}") - - -def check_engine_sink(tree: Tree, result: Result) -> None: - if tree.get(ENGINE_PRIVATE_HEADER) is None: - result.add("engine-sink", ENGINE_PRIVATE_HEADER, None, - "missing or unreadable, so the engine cannot be checked") - for file in tree.files(COMPONENT_DIRS): - if not (_in_engine_dir(file.path, ENGINE_SINK_PATTERNS) or file.path == ENGINE_PUBLIC_HEADER): +def check_core_includes(tree: Tree, result: Result) -> None: + for file in tree.files(CORE_DIRS): + if not file.path.endswith(SOURCE_SUFFIXES): + continue + for line, spelling, path in include_directives(file.lexed.code): + if path.startswith(FOREIGN_INCLUDE_PREFIXES): + result.add("core-include", file.path, line, f"includes {spelling}") + + +def check_runtime_includes(tree: Tree, result: Result) -> None: + for file in tree.files(RUNTIME_DIRS): + if not file.path.endswith(RUNTIME_SOURCE_SUFFIXES): + continue + for line, spelling, path in include_directives(file.lexed.code): + quoted = spelling.startswith('"') + if quoted and posixpath.basename(path) != posixpath.basename(RUNTIME_HEADER): + result.add("runtime-include", file.path, line, f"includes {spelling}, not weavec_rt.h") + elif not quoted and path.startswith("weavec"): + result.add("runtime-include", file.path, line, f"includes the WeaveC header {spelling}") + + +_RUNTIME_CALL = re.compile(r'(? dict[str, tuple[str, int]]: + """Each runtime entry point the passes name, with where it is first named.""" + named: dict[str, tuple[str, int]] = {} + for file in tree.files(PASS_DIRS): + if not file.path.endswith(SOURCE_SUFFIXES): continue - bare = file.lexed.bare - for offset, name in identifiers_containing(bare, DIAGNOSTIC_SINK): - result.add("engine-sink", file.path, line_at(bare, offset), f"the engine names {name}") + code = file.lexed.code + for match in _RUNTIME_CALL.finditer(code): + named.setdefault(RUNTIME_PREFIX + match.group(1), (file.path, line_at(code, match.start()))) + for start, _, body in file.lexed.strings: + if _RUNTIME_NAME.fullmatch(body): + named.setdefault(body, (file.path, line_at(code, start))) + spec = tree.get(LIBRARY_SPEC) + if spec is not None: + for line, text in library_spec_lines(spec.text): + for match in _WRAPPER.finditer(text): + named.setdefault(RUNTIME_PREFIX + match.group(1), (LIBRARY_SPEC, line)) + return dict(sorted(named.items())) + + +def check_runtime_entries(tree: Tree, result: Result) -> None: + header = tree.get(RUNTIME_HEADER) + if header is None: + result.add("runtime-entry", RUNTIME_HEADER, None, + "missing or unreadable, so the runtime's entry points are unknown") + return + declared = set(_RUNTIME_NAME.findall(header.lexed.bare)) + result.entry_points = {name: name in declared for name in entry_points(tree)} + for name, (path, line) in entry_points(tree).items(): + if name not in declared: + result.add("runtime-entry", path, line, f"{name} is not declared in {RUNTIME_HEADER}") # --------------------------------------------------------------------------- @@ -700,16 +605,6 @@ def _over_limit(total: int, limit: int, files: int) -> str: return f"{total:,} lines in {files:,} files, {total - limit:,} over the limit of {limit:,}" -def check_engine_lines(tree: Tree, result: Result) -> None: - for file in tree.files((ENGINE_DIR,)): - if _in_engine_dir(file.path, ENGINE_LINE_PATTERNS): - result.engine_lines[file.path] = file.lines - total = sum(result.engine_lines.values()) - if total > ENGINE_LINE_LIMIT: - result.add("engine-lines", ENGINE_TOTAL, None, - _over_limit(total, ENGINE_LINE_LIMIT, len(result.engine_lines))) - - def check_library_lines(tree: Tree, result: Result) -> None: for file in tree.files(LIBRARY_DIRS): if file.path == LIBRARY_SPEC: @@ -730,9 +625,8 @@ def check_runtime_lines(tree: Tree, result: Result) -> None: _over_limit(total, RUNTIME_LINE_LIMIT, len(result.runtime_lines))) -CHECK_FUNCTIONS = (check_retired_names, check_library_name_tests, check_corpus_words, - check_engine_includes, check_engine_sink, check_engine_lines, - check_library_lines, check_runtime_lines) +CHECK_FUNCTIONS = (check_library_name_tests, check_corpus_words, check_core_includes, check_runtime_includes, + check_runtime_entries, check_library_lines, check_runtime_lines) # --------------------------------------------------------------------------- @@ -753,19 +647,18 @@ def run_checks(root: Path) -> Result: def summary_lines(result: Result) -> list[str]: source = "git ls-files" if result.file_source == "git" else "a directory walk (not a git work tree)" - engine = sum(result.engine_lines.values()) library = sum(result.library_lines.values()) runtime = sum(result.runtime_lines.values()) lines = [ - "Summary (RFC 0030 and 0031, gate H2; RFC 0032, gate H1)", + "Summary (RFC 0036, gate H1)", f" files: {result.files:,} from {source}; skipped {len(result.binary)} binary, " f"{len(result.unreadable)} unreadable", ] if result.library_entries is not None: lines.append(f" LibrarySpec: {result.library_entries:,} entries, {result.library_aliases:,} chk " f"aliases, and their {BUILTIN_PREFIX} spellings") - lines.append(f" engine lines: {engine:,} / {ENGINE_LINE_LIMIT:,} " - f"({len(result.engine_lines):,} {ENGINE_DIR}/Engine*.{{h,cpp}} files)") + declared = sum(result.entry_points.values()) + lines.append(f" entry points: {declared:,} of {len(result.entry_points):,} declared in {RUNTIME_HEADER}") lines.append(f" library lines: {library:,} / {LIBRARY_LINE_LIMIT:,} " f"({len(result.library_lines):,} code files under lib/, include/ and tools/, " f"{LIBRARY_SPEC} excluded)") @@ -776,7 +669,7 @@ def summary_lines(result: Result) -> list[str]: lines.append(f" {check:<{width}} {result.count(check):>6} {description}") total = len(result.violations) verdict = "PASS" if total == 0 else "FAIL" - lines.append(f"H2: {verdict} ({total:,} violation{'' if total == 1 else 's'})") + lines.append(f"H1: {verdict} ({total:,} violation{'' if total == 1 else 's'})") return lines @@ -784,15 +677,14 @@ def document(result: Result) -> dict: """The results as JSON.""" return { "schema": "weavec-hygiene", - "version": 2, + "version": 3, "root": str(result.root), "passed": not result.violations, "files": {"source": result.file_source, "count": result.files, "binarySkipped": result.binary, "unreadable": result.unreadable}, "librarySpec": {"entries": result.library_entries, "aliases": result.library_aliases}, + "entryPoints": result.entry_points, "lines": { - "engine": {"total": sum(result.engine_lines.values()), "limit": ENGINE_LINE_LIMIT, - "files": result.engine_lines}, "library": {"total": sum(result.library_lines.values()), "limit": LIBRARY_LINE_LIMIT, "files": len(result.library_lines)}, "runtime": {"total": sum(result.runtime_lines.values()), "limit": RUNTIME_LINE_LIMIT, diff --git a/scripts/corpus-gate.py b/scripts/corpus-gate.py index 0dcf1001..dd00a351 100755 --- a/scripts/corpus-gate.py +++ b/scripts/corpus-gate.py @@ -8,13 +8,10 @@ Modes (combine freely; at least one, or --update-from): - --quick per config, the advisory analysis (`weavec`, per file, or - `weavec --whole-program` for wholeProgram configs), whose - definite errors need a verdict in triage.json, none of them - "false"; and `weavec-cc -c -O2 -fweavec-ledger=` of each file, - whose enforcement ledgers and the analysis' summary lines are - compared with the expected.json ratchet (the proven share of - the accesses must not drop). Every PR. + --quick per config, `weavec-cc -c -O2 -fweavec-ledger=` of each file, + whose enforcement ledgers are compared with the expected.json + ratchet (the proven share of the accesses must not drop, the + unguarded accesses must not rise). Every PR. --full --quick, then each config's build and test suite with CC=weavec-cc in trap mode, rerun in report mode to attribute failures and traps to lines, and built once more with the @@ -108,23 +105,6 @@ # -O in compile.args comes later and wins). QUICK_FLAGS = ("-O2",) -# `file:line:col: severity: message [weavec::id]`. -DIAG_RE = re.compile( - r"^(?P[^:\n]+):(?P\d+):(?P\d+): " - r"(?Perror|warning): (?P.*) \[weavec::(?P[a-z0-9-]+)\]$" -) -# Anything Clang itself reports (parse errors, missing headers) has no -# `[weavec::...]` tag; it fails the analysis, so a broken setup never looks clean. -CLANG_DIAG_RE = re.compile(r"^(?P[^:\n]+):\d+:\d+: (?Perror|fatal error): ") -# The analysis' summary lines (lib/Core/Ledger.cpp): one per unit, and one -# for the program under --whole-program. -SUMMARY_RE = re.compile( - r"^weavec: (?:program (?P.+?)|(?P.+?)): (?P[\d,]+) sites?" - r"(?: in (?P[\d,]+) units?)?: (?P[\d,]+) proven, (?P[\d,]+) not proven, " - r"(?P[\d,]+) violations?, (?P[\d,]+) trusted; (?P[\d,]+) errors?, " - r"(?P[\d,]+) warnings?(?:; (?P[\d,]+) functions? over budget \(.*\))?\s*$" -) -ANALYSIS_COUNTS = ("sites", "proven", "notProven", "violations", "trusted", "errors", "warnings", "overBudget") # A run-time report (runtime/weavec_report.c, RFC 0035 section 5.3). A test # harness may prefix the line (CTest -V prints "12: "). REPORT_RE = re.compile( @@ -136,7 +116,8 @@ KINDS = ( "heap-buffer-overflow", "heap-use-after-free", "stack-buffer-overflow", "stack-use-after-scope", "dynamic-stack-buffer-overflow", "global-buffer-overflow", - "buffer-overflow", "null-dereference", "unterminated-string", "index-out-of-bounds", + "intra-object-overflow", "use-after-poison", + "null-dereference", "unterminated-string", "index-out-of-bounds", "invalid-release", "double-free", "invalid-access", "overlapping-copy", ) # How shells, make and CTest describe a process that died by SIGTRAP or @@ -454,7 +435,6 @@ class Config: project: Project files: list[str] args: list[str] - whole_program: bool = False build: list[str] = dataclasses.field(default_factory=list) test: list[str] = dataclasses.field(default_factory=list) test_timeout: float | None = None @@ -491,8 +471,7 @@ def config(self, name: str) -> Config: NAMED_SETS = SETS[2:] # The sets that run only when asked for (--set, --only). SEALED_SETS = ("sealed", "sealed34", "sealed35") -CONFIG_KEYS = frozenset(("name", "notes", "compile", "wholeProgram", "build", "test", "testTimeout", "bench", - "heldOut", "set")) +CONFIG_KEYS = frozenset(("name", "notes", "compile", "build", "test", "testTimeout", "bench", "heldOut", "set")) BENCH_KEYS = frozenset(("name", "build", "command", "repeat", "input", "check")) SHA_RE = re.compile(r"[0-9a-f]{40}") @@ -558,8 +537,7 @@ def load_manifest(path: Path, support_root: Path) -> Manifest: problems.append(f"config {cname}: testTimeout must be a positive number of seconds") config = Config( name=cname, project=project, files=list(compile_.get("files", [])), - args=list(compile_.get("args", [])), whole_program=bool(c.get("wholeProgram", False)), - build=list(c.get("build", [])), test=list(c.get("test", [])), test_timeout=timeout, + args=list(compile_.get("args", [])), build=list(c.get("build", [])), test=list(c.get("test", [])), test_timeout=timeout, bench=bench, notes=c.get("notes", ""), held_out=held_out is True, set=corpus_set) project.configs.append(config) configs.append(config) @@ -664,54 +642,7 @@ def config_files(config: Config, root: Path, tracked: set[str] | None = None) -> return files -# -- the advisory analysis (RFC 0035, section 8) -------------------------------- - - -@dataclasses.dataclass -class Diagnostic: - file: str - line: int - col: int - severity: str - id: str - message: str - - def render(self) -> str: - return f"{self.file}:{self.line}:{self.col}: {self.severity}: {self.message} [weavec::{self.id}]" - - def to_json(self) -> dict: - return dataclasses.asdict(self) - - -def parse_diagnostics(text: str, root: Path) -> tuple[list[Diagnostic], int]: - """WeaveC diagnostics and the number of Clang errors in tool output.""" - diagnostics: list[Diagnostic] = [] - clang_errors = 0 - for line in text.splitlines(): - m = DIAG_RE.match(line) - if m: - diagnostics.append(Diagnostic(file=rel_path(m.group("file"), root), line=int(m.group("line")), - col=int(m.group("col")), severity=m.group("severity"), - id=m.group("id"), message=m.group("message"))) - elif CLANG_DIAG_RE.match(line): - clang_errors += 1 - return diagnostics, clang_errors - - -def parse_summaries(text: str) -> tuple[list[dict], dict | None]: - """The analysis' unit summary lines, and its program line if any.""" - units: list[dict] = [] - program = None - for line in text.splitlines(): - m = SUMMARY_RE.match(line.strip()) - if not m: - continue - counts = {key: int((m.group(key) or "0").replace(",", "")) for key in ANALYSIS_COUNTS} - if m.group("program") is not None: - program = counts - else: - units.append({"source": m.group("source"), **counts}) - return units, program +# -- the enforcement ledger (RFC 0035, section 9) -------------------------------- def add_counts(into: dict, counts: dict, keys: Iterable[str]) -> None: @@ -719,65 +650,6 @@ def add_counts(into: dict, counts: dict, keys: Iterable[str]) -> None: into[key] = into.get(key, 0) + int(counts.get(key, 0)) -def classify_failure(result: ProcResult, diagnostics: list[Diagnostic], clang_errors: int, - summaries: int) -> str: - """Setup and tool failures must never look like clean code.""" - if result.error: - return result.error - if result.timed_out: - return f"timeout after {result.seconds:.0f} seconds" - if clang_errors: - return f"{clang_errors} Clang error(s)" - if result.returncode < 0 or result.returncode > 1: - return f"weavec exited with {describe_status(result)}" - if result.returncode and not any(d.severity == "error" for d in diagnostics): - return f"weavec failed without a WeaveC error (status {result.returncode})" - if not summaries: - return "weavec printed no summary line" - return "" - - -@dataclasses.dataclass -class AnalysisRun: - """One `weavec` process: a file, or the whole program.""" - files: list[str] - counts: dict - diagnostics: list[Diagnostic] - cpu: float - failure: str = "" - - -def analysis_command(weavec: str, config: Config, files: list[Path], support_root: Path) -> list[str]: - argv = [weavec] - if config.whole_program: - argv.append("--whole-program") - argv.extend(str(f) for f in files) - # Clang stops after 20 errors per unit by default. - argv.extend(["--", "-ferror-limit=0", *expand_args(config, support_root)]) - return argv - - -def run_analysis(weavec: str, config: Config, root: Path, files: list[Path], support_root: Path, - timeout: float) -> AnalysisRun: - result = run_process(analysis_command(weavec, config, files, support_root), cwd=root, timeout=timeout) - diagnostics, clang_errors = parse_diagnostics(result.output, root) - units, program = parse_summaries(result.output) - counts: dict = {} - if config.whole_program and program is not None: - counts = dict(program) - else: - for unit in units: - add_counts(counts, unit, ANALYSIS_COUNTS) - failure = classify_failure(result, diagnostics, clang_errors, len(units) + (program is not None)) - if not failure and len(units) != len(files): - failure = f"{len(units)} summary line(s) for {len(files)} file(s)" - return AnalysisRun(files=[f.relative_to(root).as_posix() for f in files], counts=counts, - diagnostics=diagnostics, cpu=result.cpu, failure=failure) - - -# -- the enforcement ledger (RFC 0035, section 9) -------------------------------- - - def read_ledger(path: Path) -> tuple[dict, collections.Counter]: """The summed summary of a ledger's units, and its rows by outcome:reason.""" try: @@ -855,12 +727,9 @@ def run_compile(weavec_cc: str, reference_cc: str | None, config: Config, root: @dataclasses.dataclass class Quick: config: str - analysis: dict = dataclasses.field(default_factory=dict) ledger: dict = dataclasses.field(default_factory=lambda: {key: 0 for key in LEDGER_COUNTS}) reasons: collections.Counter = dataclasses.field(default_factory=collections.Counter) - diagnostics: list[Diagnostic] = dataclasses.field(default_factory=list) units: dict = dataclasses.field(default_factory=dict) - analysis_cpu: float = 0.0 compile_cpu: float = 0.0 failures: list[str] = dataclasses.field(default_factory=list) @@ -870,24 +739,11 @@ def proven_share(self) -> float | None: def measured(self) -> dict: """The ratchet shape (expected.json).""" - return { - "ledger": {**{key: self.ledger[key] for key in LEDGER_COUNTS}, "provenShare": self.proven_share}, - "analysis": {key: self.analysis.get(key, 0) for key in ANALYSIS_COUNTS}, - } + return {"ledger": {**{key: self.ledger[key] for key in LEDGER_COUNTS}, "provenShare": self.proven_share}} def to_json(self) -> dict: return {**self.measured(), "reasons": dict(sorted(self.reasons.items())), - "analysisCpuSeconds": round(self.analysis_cpu, 2), "compileCpuSeconds": round(self.compile_cpu, 2), - "units": self.units, "failures": self.failures, - "diagnostics": [d.to_json() for d in self.diagnostics]} - - def add_analysis(self, run: AnalysisRun) -> None: - self.analysis_cpu += run.cpu - if run.failure: - self.failures.append(f"weavec {' '.join(run.files)[:80]}: {run.failure}") - return - add_counts(self.analysis, run.counts, ANALYSIS_COUNTS) - self.diagnostics.extend(run.diagnostics) + "compileCpuSeconds": round(self.compile_cpu, 2), "units": self.units, "failures": self.failures} def add_compile(self, run: CompileRun) -> None: self.compile_cpu += run.cpu @@ -911,23 +767,19 @@ def add_compile(self, run: CompileRun) -> None: RATCHET_FIELDS: tuple[tuple[tuple[str, ...], str], ...] = ( (("ledger", "provenShare"), "higher"), (("ledger", "unguarded"), "lower"), - (("analysis", "errors"), "lower"), - (("analysis", "warnings"), "lower"), ) # Counts whose change is a note: the code under test changes them both ways. TRACKED_FIELDS: tuple[tuple[str, ...], ...] = ( ("ledger", "accesses"), ("ledger", "proven"), ("ledger", "guarded"), - ("analysis", "sites"), ("analysis", "proven"), ) EXPECTED_COMMENT = [ "The corpus ratchet (RFC 0035, section 11), written by scripts/corpus-gate.py --update (or", "--update-from a results file measured elsewhere, for example by CI). For each platform,", "configs. records what --quick measured: `ledger`, the enforcement ledgers of the", - "per-file weavec-cc -c -O2 compiles (accesses, proven, guarded, unguarded, provenShare), and", - "`analysis`, the summary lines of the weavec analysis (sites, proven, notProven, violations,", - "trusted, errors, warnings, overBudget). provenShare must not drop, and unguarded, errors and", - "warnings must not rise; a better value and a change of the other counts are reported until", - "--update records them. test/corpus/README.md documents the gate.", + "per-file weavec-cc -c -O2 compiles (accesses, proven, guarded, unguarded, provenShare).", + "provenShare must not drop and unguarded must not rise; a better value and a change of the", + "other counts are reported until --update records them. test/corpus/README.md documents the", + "gate.", ] @@ -1013,34 +865,25 @@ def load_expected(path: Path, update: bool) -> dict: # -- triage (triage.json) ------------------------------------------------------------- -TRIAGE_KEYS = ("config", "id", "certainty", "file", "line", "verdict", "note") +TRIAGE_FIELDS = frozenset(("schema", "version", "_comment", "guardFailures")) GUARD_FAILURE_KEYS = ("config", "file", "line", "verdict", "note") @dataclasses.dataclass class Triage: - entries: list[dict] guard_failures: list[dict] def load_triage(path: Path) -> Triage: - """Verdicts on the analysis' findings, and the guard failures of test - suites that are true bugs of the project.""" + """The guard failures of test suites that are true bugs of the project.""" if not path.exists(): - return Triage([], []) + return Triage([]) data = read_json(path) check_schema(data, path, TRIAGE_SCHEMA, TRIAGE_VERSION) problems = [] - entries = list(data.get("entries", [])) - for entry in entries: - where = f"{entry.get('config')} {entry.get('file')}:{entry.get('line')}" - missing = [k for k in TRIAGE_KEYS if k not in entry] - if missing: - problems.append(f"entry {where}: missing {', '.join(missing)}") - elif entry["verdict"] not in ("true", "false"): - problems.append(f"entry {where}: verdict must be \"true\" or \"false\"") - elif entry["certainty"] not in ("definite", "possible"): - problems.append(f"entry {where}: certainty must be definite or possible") + unknown = sorted(set(data) - TRIAGE_FIELDS) + if unknown: + problems.append(f"unknown field(s) {', '.join(unknown)}") failures = list(data.get("guardFailures", [])) for entry in failures: where = f"{entry.get('config')} {entry.get('file')}:{entry.get('line')}" @@ -1054,63 +897,7 @@ def load_triage(path: Path) -> Triage: problems.append(f"guardFailures entry {where}: unknown kind {entry['kind']!r}") if problems: raise GateError(f"{path}: invalid triage:\n " + "\n ".join(problems)) - return Triage(entries, failures) - - -def findings_of(config: str, diagnostics: Iterable[Diagnostic]) -> list[dict]: - """The analysis' findings: an error is definite, a warning possible.""" - seen = set() - out = [] - for d in diagnostics: - key = (d.file, d.line, d.id) - if key in seen: - continue - seen.add(key) - out.append({"config": config, "id": d.id, "certainty": "definite" if d.severity == "error" else "possible", - "file": d.file, "line": d.line, "message": d.message}) - return out - - -@dataclasses.dataclass -class TriageResult: - definite: list[dict] = dataclasses.field(default_factory=list) - possible: int = 0 - untriaged: list[dict] = dataclasses.field(default_factory=list) - false_errors: list[dict] = dataclasses.field(default_factory=list) - stale: list[dict] = dataclasses.field(default_factory=list) - - @property - def failed(self) -> bool: - return bool(self.untriaged or self.false_errors) - - def to_json(self) -> dict: - return dataclasses.asdict(self) - - -def check_triage(findings: list[dict], entries: list[dict], configs_run: set[str]) -> TriageResult: - """Every definite error needs a verdict and none may be "false"; possible - findings are counted, triaged or not. An entry no finding of a config - that ran matches is stale.""" - result = TriageResult() - by_key = {(e["config"], normalise_path(e["file"]), int(e["line"]), e["id"]): e for e in entries} - matched = set() - for finding in findings: - key = (finding["config"], normalise_path(finding["file"]), finding["line"], finding["id"]) - entry = by_key.get(key) - if entry is not None: - matched.add(key) - if finding["certainty"] != "definite": - result.possible += 1 - continue - result.definite.append(finding) - if entry is None: - result.untriaged.append(finding) - elif entry["verdict"] == "false": - result.false_errors.append({**finding, "note": entry.get("note", "")}) - for key, entry in by_key.items(): - if entry["config"] in configs_run and key not in matched: - result.stale.append(entry) - return result + return Triage(failures) def triaged_failure(guard_failures: list[dict], config: str, report: dict) -> bool: @@ -1487,6 +1274,8 @@ class BenchRun: times: dict = dataclasses.field(default_factory=dict) minimum: dict = dataclasses.field(default_factory=dict) outputs: dict = dataclasses.field(default_factory=dict) + # The peak resident memory of each build's runs, in bytes (RFC 0036 gate G7). + peak_rss: dict = dataclasses.field(default_factory=dict) ratio: float | None = None asan_ratio: float | None = None reports: list[dict] = dataclasses.field(default_factory=list) @@ -1494,7 +1283,8 @@ class BenchRun: def to_json(self) -> dict: return {"config": self.config, "name": self.name, "times": self.times, "minimum": self.minimum, - "outputs": self.outputs, "ratio": self.ratio, "asanRatio": self.asan_ratio, + "outputs": self.outputs, "peakRssMiB": {k: round(v / 2 ** 20, 1) for k, v in self.peak_rss.items()}, + "ratio": self.ratio, "asanRatio": self.asan_ratio, "reports": self.reports, "failures": self.failures} @@ -1503,7 +1293,6 @@ def to_json(self) -> dict: @dataclasses.dataclass class Binaries: - weavec: str | None weavec_cc: str | None reference_cc: str | None @@ -1535,7 +1324,7 @@ def tool_version(path: str | None) -> str: return f"unavailable: {exc}" text = (out.stdout or out.stderr).strip().splitlines() # weavec-cc prints Clang's version block first; its own line follows. - own = [line for line in text if line.startswith(("weavec-cc version", "weavec version"))] + own = [line for line in text if line.startswith("weavec-cc version")] return own[0] if own else (text[0] if text else "") @@ -1716,7 +1505,6 @@ def __init__(self, args: argparse.Namespace): self.jobs = max(1, args.jobs) self.triage = load_triage(args.triage) self.measured: dict[str, dict] = {} - self.findings: list[dict] = [] self.injection_runs: list[InjectionRun] = [] self.binaries = self.resolve_binaries() self.announce_binaries() @@ -1726,10 +1514,8 @@ def __init__(self, args: argparse.Namespace): def resolve_binaries(self) -> Binaries: a = self.args built = [ROOT / "build" / "release" / "bin", ROOT / "build" / "dev" / "bin"] - weavec_needed = (a.quick or a.full) and not a.reference_only cc_needed = (a.quick or a.full or a.inject or a.bench) and not a.reference_only reference_needed = a.reference_only or a.full or a.bench - weavec = resolve_binary(a.weavec, [d / "weavec" for d in built], "--weavec", weavec_needed) weavec_cc = resolve_binary(a.weavec_cc, [d / "weavec-cc" for d in built], "--weavec-cc", cc_needed) reference = a.cc or default_reference_cc() if reference: @@ -1737,19 +1523,18 @@ def resolve_binaries(self) -> Binaries: elif reference_needed: raise GateError("--cc: no reference compiler; set WEAVEC_LLVM_PREFIX or pass --cc") if a.reference_only: - weavec = weavec_cc = None + weavec_cc = None self.results["binaries"] = { name: {"path": path, "version": tool_version(path)} - for name, path in (("weavec", weavec), ("weavec-cc", weavec_cc), ("reference-cc", reference)) + for name, path in (("weavec-cc", weavec_cc), ("reference-cc", reference)) if path } - return Binaries(weavec, weavec_cc, reference) + return Binaries(weavec_cc, reference) def announce_binaries(self) -> None: """Say which binaries are under test, and refuse to time a Debug one: its verdicts are right and its timings mean nothing.""" - for what, path in (("weavec", self.binaries.weavec), ("weavec-cc", self.binaries.weavec_cc), - ("reference compiler", self.binaries.reference_cc)): + for what, path in (("weavec-cc", self.binaries.weavec_cc), ("reference compiler", self.binaries.reference_cc)): if not path: continue build = build_type_of(path) @@ -1758,8 +1543,7 @@ def announce_binaries(self) -> None: if what != "reference compiler" else "")) if what == "reference compiler" or not build or build.lower() == "release": continue - explicit = self.args.weavec if what == "weavec" else self.args.weavec_cc - if self.args.bench and not explicit: + if self.args.bench and not self.args.weavec_cc: raise GateError(f"{what}: {path} is a {build} build and --bench measures CPU time. " f"Pass --{what} explicitly, or build a Release tree.") if self.args.full: @@ -1816,17 +1600,13 @@ def probe(self) -> None: raise GateError(f"{self.binaries.weavec_cc} does not write a {LEDGER_SCHEMA} version {LEDGER_VERSION} " f"ledger (RFC 0035): {describe_status(result)}; {exc}; " f"{result.stderr.strip()[:300]}") from None - result = run_process([self.binaries.weavec, str(source), "--"], cwd=scratch, timeout=120) - if not parse_summaries(result.output)[0]: - raise GateError(f"{self.binaries.weavec} prints no summary line (RFC 0035, section 8): " - f"{describe_status(result)}; {result.output.strip()[:300]}") # ---- quick ---- def run_quick(self) -> None: self.probe() timed = self.binaries.reference_cc if self.args.full else None - log(f"quick: weavec {self.binaries.weavec}, weavec-cc {self.binaries.weavec_cc}" + log(f"quick: weavec-cc {self.binaries.weavec_cc}" + (f", per-file compiles timed against {timed}" if timed else "")) tasks: list[tuple[Config, Callable[[], Any]]] = [] for config in self.configs: @@ -1834,61 +1614,30 @@ def run_quick(self) -> None: files = config_files(config, root, self.tracked[config.project.name]) work = self.run_dir / "quick" / config.name work.mkdir(parents=True, exist_ok=True) - groups = [files] if config.whole_program else [[f] for f in files] - for group in groups: - tasks.append((config, lambda c=config, r=root, g=group: run_analysis( - self.binaries.weavec, c, r, g, self.support_root, self.args.timeout))) for file in files: tasks.append((config, lambda c=config, r=root, f=file, w=work: run_compile( self.binaries.weavec_cc, timed, c, r, f, self.support_root, w, self.args.timeout))) - # The whole programs first: they take longest. - tasks.sort(key=lambda t: not t[0].whole_program) quick = {c.name: Quick(config=c.name) for c in self.configs} with concurrent.futures.ThreadPoolExecutor(max_workers=self.jobs) as pool: futures = {pool.submit(task): config for config, task in tasks} for future in concurrent.futures.as_completed(futures): result = future.result() measure = quick[futures[future].name] - if isinstance(result, AnalysisRun): - measure.add_analysis(result) - vlog(f"[{measure.config}] weavec {' '.join(result.files)[:60]}: {result.failure or 'ok'}") - else: - measure.add_compile(result) - vlog(f"[{measure.config}] weavec-cc -c {result.file}: {result.failure or 'ok'}") + measure.add_compile(result) + vlog(f"[{measure.config}] weavec-cc -c {result.file}: {result.failure or 'ok'}") for config in self.configs: self.record_quick(config, quick[config.name]) def record_quick(self, config: Config, measure: Quick) -> None: - measure.diagnostics.sort(key=lambda d: (d.file, d.line, d.col, d.id)) self.entry(config)["quick"] = measure.to_json() for failure in measure.failures: self.fail(f"{config.name}: {failure}", config=config.name) if not measure.failures: self.measured[config.name] = measure.measured() - self.findings.extend(findings_of(config.name, measure.diagnostics)) - a = measure.analysis provenness = measure.proven_share log(f"[{config.name}] {measure.ledger['accesses']} accesses, " - f"{'-' if provenness is None else f'{provenness:.1%}'} proven, {measure.ledger['unguarded']} unguarded; " - f"analysis: {a.get('sites', 0)} sites, {a.get('errors', 0)} errors, {a.get('warnings', 0)} warnings " - f"({measure.analysis_cpu + measure.compile_cpu:.1f} s CPU)") - - def evaluate_triage(self) -> None: - result = check_triage(self.findings, self.triage.entries, self.selected) - self.results["triage"] = result.to_json() - for finding in result.untriaged: - self.fail(f"untriaged definite {finding['id']} in {finding['config']} at {finding['file']}:" - f"{finding['line']}: {finding['message']}") - for finding in result.false_errors: - self.fail(f"definite {finding['id']} triaged false in {finding['config']} at {finding['file']}:" - f"{finding['line']}: {finding['message']}") - for entry in result.stale: - self.note(f"stale triage entry: {entry['config']} {entry['file']}:{entry['line']} {entry['id']}") - per_config = collections.Counter(f["config"] for f in result.definite) - self.gate("triage", not result.failed, - {"definiteErrors": len(result.definite), "possible": result.possible, - "untriaged": len(result.untriaged), "triagedFalse": len(result.false_errors), - "perConfig": dict(sorted(per_config.items()))}, failed_above=True) + f"{'-' if provenness is None else f'{provenness:.1%}'} proven, {measure.ledger['unguarded']} unguarded " + f"({measure.compile_cpu:.1f} s CPU)") # ---- full: builds and tests ---- @@ -2170,6 +1919,8 @@ def run_bench(self, config: Config) -> None: f"{result.output.strip()[-300:]}") break run.times.setdefault(label, []).append(round(result.user, 4)) + if result.maxrss: + run.peak_rss[label] = max(run.peak_rss.get(label, 0), result.maxrss) run.outputs.setdefault(label, result.stdout.strip()[:200]) if run.failures: break @@ -2197,15 +1948,15 @@ def run_bench(self, config: Config) -> None: log(f"[{config.name}] bench {bench.name}: min user CPU " + ", ".join(f"{k} {v:.3f} s" for k, v in run.minimum.items()) + (f"; ratio {run.ratio:.3f}" if run.ratio is not None else "") - + (f"; ASan {run.asan_ratio:.3f}" if run.asan_ratio is not None else "")) + + (f"; ASan {run.asan_ratio:.3f}" if run.asan_ratio is not None else "") + + ("; peak RSS " + ", ".join(f"{k} {v / 2 ** 20:.1f} MiB" for k, v in run.peak_rss.items()) + if run.peak_rss else "")) # ---- gates ---- def evaluate(self) -> None: a = self.args gates = self.manifest.gates - if (a.quick or a.full) and not a.reference_only: - self.evaluate_triage() if a.full and not a.reference_only: self.evaluate_drop_in() if a.checks == "trap": @@ -2247,7 +1998,7 @@ def ratchet(self) -> None: measured = {n: m for n, m in self.measured.items() if n not in self.tool_failures} skipped = sorted(set(self.measured) - set(measured)) if skipped: - log(f"not recording {', '.join(skipped)}: their analyses, compiles, builds or runs failed") + log(f"not recording {', '.join(skipped)}: their compiles, builds or runs failed") if not measured: log("nothing to record in expected.json") return @@ -2268,7 +2019,7 @@ def ratchet(self) -> None: def producer(self) -> str: versions = self.results.get("binaries", {}) - return (versions.get("weavec-cc") or versions.get("weavec") or {}).get("version", "") + return (versions.get("weavec-cc") or {}).get("version", "") # ---- main flow ---- @@ -2342,17 +2093,17 @@ def yes_no(value): if not group: continue print(f"{corpus_set} configs:") - print(f" {'config':<{width}} {'accesses':>8} {'proven':>7} {'errors':>6} {'warnings':>8} " + print(f" {'config':<{width}} {'accesses':>8} {'proven':>7} {'unguarded':>9} " f"{'built':>5} {'tests':>5} {'traps':>5} {'build x':>7} {'run x':>6}") for c in group: entry = results["configs"].get(c.name) or {} quick = entry.get("quick") or {} - ledger, analysis = quick.get("ledger") or {}, quick.get("analysis") or {} + ledger = quick.get("ledger") or {} checked = (entry.get("builds") or {}).get(results["checks"]) or {} proven = ledger.get("provenShare") print(f" {c.name:<{width}} {cell(ledger.get('accesses')):>8} " f"{cell(None if proven is None else proven * 100, '{:.1f}%'):>7} " - f"{cell(analysis.get('errors')):>6} {cell(analysis.get('warnings')):>8} " + f"{cell(ledger.get('unguarded')):>9} " f"{yes_no(checked.get('built')):>5} {yes_no(checked.get('testsPassed')):>5} " f"{cell(entry.get('traps')):>5} {cell(entry.get('buildCpuRatio'), '{:.2f}'):>7} " f"{cell((entry.get('bench') or {}).get('ratio'), '{:.2f}'):>6}") @@ -2381,7 +2132,7 @@ def update_from(results_path: Path, expected_path: Path) -> int: raise GateError(f"{results_path}: the run measured nothing") expected = load_expected(expected_path, update=True) versions = results.get("binaries", {}) - producer = (versions.get("weavec-cc") or versions.get("weavec") or {}).get("version", "") + producer = (versions.get("weavec-cc") or {}).get("version", "") write_json(expected_path, merge_expected(expected, measured, results["platform"], results["machine"], producer)) log(f"updated {expected_path} for {results['platform']} from {results_path} ({', '.join(sorted(measured))})") return 0 @@ -2391,9 +2142,10 @@ def update_from(results_path: Path, expected_path: Path) -> int: def parse_args(argv: list[str]) -> argparse.Namespace: - ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter, + allow_abbrev=False) modes = ap.add_argument_group("modes") - modes.add_argument("--quick", action="store_true", help="the analysis and the per-file ledgers") + modes.add_argument("--quick", action="store_true", help="the per-file enforcement ledgers") modes.add_argument("--full", action="store_true", help="quick + builds, tests, injections, benchmarks") modes.add_argument("--inject", action="store_true", help="the injected bugs") modes.add_argument("--bench", action="store_true", help="the benchmarks") @@ -2407,8 +2159,7 @@ def parse_args(argv: list[str]) -> argparse.Namespace: mods.add_argument("--update", action="store_true", help="record this run's measurements in expected.json") mods.add_argument("--no-asan", action="store_true", help="benchmarks without the ASan build") bins = ap.add_argument_group("binaries") - bins.add_argument("--weavec", help="weavec under test (default build/release/bin, then build/dev/bin)") - bins.add_argument("--weavec-cc", help="weavec-cc under test (same defaults)") + bins.add_argument("--weavec-cc", help="weavec-cc under test (default build/release/bin, then build/dev/bin)") bins.add_argument("--cc", help="reference compiler (default $WEAVEC_LLVM_PREFIX/bin/clang, else clang)") sel = ap.add_argument_group("selection and resources") sel.add_argument("--only", action="append", nargs="+", default=[], metavar="CONFIG", @@ -2420,7 +2171,7 @@ def parse_args(argv: list[str]) -> argparse.Namespace: help="run the configs of this set alone (repeatable; one of " + ", ".join(SETS) + ")") sel.add_argument("--injection", action="append", default=[], metavar="ID", help="run only this injection") sel.add_argument("--jobs", type=int, default=os.cpu_count() or 4, help="parallel processes (default: CPUs)") - sel.add_argument("--timeout", type=float, default=1800, help="seconds per analysis or compile (default 1800)") + sel.add_argument("--timeout", type=float, default=1800, help="seconds per per-file compile (default 1800)") sel.add_argument("--build-timeout", type=float, default=3600, help="seconds per build or test step") sel.add_argument("--repeat", type=int, help="benchmark runs per build (default: the manifest's, 7)") sel.add_argument("--workdir", type=Path, default=DEFAULT_WORKDIR, diff --git a/scripts/run-cases.py b/scripts/run-cases.py index 9508cd5d..ca69980f 100755 --- a/scripts/run-cases.py +++ b/scripts/run-cases.py @@ -1,24 +1,22 @@ #!/usr/bin/env python3 -"""Run the test/cases suites (RFC 0035 section 11). +"""Run the test/cases suites (RFC 0035 section 11, RFC 0036 section 9). Each case is a C file under test/cases// whose expectations are written as line-comment markers (test/cases/README.md has the grammar). For every selected case the runner -1. analyses it with the `weavec` tool (`--whole-program` when it has - several units) and checks the diagnostics against the BUG, CLEAN and - ALLOW markers; -2. builds it with `weavec-cc` and its FLAGS (trap mode, or verify mode under +1. builds it with `weavec-cc` and its FLAGS (trap mode, or verify mode under `--checks verify`), writing the enforcement ledger when a marker reads it, and checks the ledger against the GUARDED, PROVEN, UNGUARDED and EXPECT-LEDGER markers; -3. runs the executable once per RUN-INPUT and attributes every stop to a +2. runs the executable once per RUN-INPUT and attributes every stop to a line and a kind: a `weavec: at ::` report, an invalid release the allocator stopped (no location), or a fault (no location, a null dereference). Every stop must be one a TRAP marker - expects, and every TRAP marker must be hit by some run; -4. runs the ASan oracle (`--asan` or an ASAN marker); -5. with `--checks verify`, fails any case that reports `weavec.proven`. + expects, and every TRAP marker must be hit by some run; a CLEAN case + must not stop; +3. runs the ASan oracle (`--asan` or an ASAN marker); +4. with `--checks verify`, fails any case that reports `weavec.proven`. A DETECT case is judged instead by whether its bug stops (its first report is on a STOP line, or it faults) and whether its fixed twin runs clean; @@ -58,44 +56,35 @@ RESOURCE_INCLUDE = ROOT / "resources" / "include" # --------------------------------------------------------------------------- -# Vocabulary (RFC 0035 sections 5.3 and 9, and the Diagnostics section) +# Vocabulary (RFC 0035 sections 5.3 and 9, RFC 0036 Diagnostics) # --------------------------------------------------------------------------- # The `version` of the enforcement ledger this runner reads. LEDGER_VERSION = 3 -# Run-time report kinds (runtime/weavec_report.c, section 5.3). +# Run-time report kinds (runtime/weavec_report.c). KINDS = ( "heap-buffer-overflow", "heap-use-after-free", "stack-buffer-overflow", "stack-use-after-scope", "dynamic-stack-buffer-overflow", "global-buffer-overflow", - "buffer-overflow", "null-dereference", "unterminated-string", "index-out-of-bounds", - "invalid-release", "invalid-access", "overlapping-copy", + "intra-object-overflow", "use-after-poison", "null-dereference", "unterminated-string", + "index-out-of-bounds", "invalid-release", "invalid-access", "overlapping-copy", ) -# The diagnostic ids of the analysis (include/weavec/Core/Diagnostic.h). -IDS = frozenset(( - "use-after-free", "double-free", "use-after-move", "conflicting-borrow", - "lifetime-too-short", "unsafe-operation", "annotation-mismatch", "invalid-annotation", - "leak", "mismatched-release", "null-dereference", "use-of-uninitialized", - "invalid-release", "out-of-bounds", "invalid-integer-operation", - "contradicted-assumption", "allocation-failure", -)) LEDGER_REASONS = { "GUARDED": ("access", "range", "string", "checked-call", "loop-range"), "PROVEN": ("in-bounds", "dominated", "merged", "optimized"), "UNGUARDED": ("unsafe",), } -FILE_MARKERS = frozenset(("CLEAN", "ALLOW", "RUN-INPUT", "EXPECT-LEDGER", "FLAGS", "UNITS", - "ASAN", "TOOL", "DETECT", "XFAIL", "TRAP-AT")) -LINE_MARKERS = frozenset(("BUG", "TRAP", "GUARDED", "PROVEN", "UNGUARDED", "NEUTRALISED", - "MISS", "STOP")) +FILE_MARKERS = frozenset(("CLEAN", "RUN-INPUT", "EXPECT-LEDGER", "FLAGS", "UNITS", "ASAN", + "DETECT", "XFAIL", "TRAP-AT")) +LINE_MARKERS = frozenset(("TRAP", "GUARDED", "PROVEN", "UNGUARDED", "MISS", "STOP")) MARKERS = FILE_MARKERS | LINE_MARKERS -NO_ARGUMENT_MARKERS = frozenset(("CLEAN", "ASAN", "TOOL", "STOP")) +NO_ARGUMENT_MARKERS = frozenset(("CLEAN", "ASAN", "STOP")) # A TRAP and the ledger markers may name a kind or a reason, or not. OPTIONAL_ARGUMENT_MARKERS = frozenset(("TRAP", "GUARDED", "PROVEN", "UNGUARDED")) # Markers a detection case (DETECT) cannot have: it is judged by whether its # bug stops, not by what is reported where. -NOT_IN_DETECTION = frozenset(("CLEAN", "ALLOW", "TOOL", "BUG", "TRAP", "GUARDED", "PROVEN", - "UNGUARDED", "NEUTRALISED", "EXPECT-LEDGER")) +NOT_IN_DETECTION = frozenset(("CLEAN", "TRAP", "GUARDED", "PROVEN", "UNGUARDED", + "EXPECT-LEDGER")) LEDGER_MARKERS = frozenset(("GUARDED", "PROVEN", "UNGUARDED")) EXPECTATION_MARKERS = frozenset(("CLEAN", "EXPECT-LEDGER")) | LINE_MARKERS COMPARISONS = ("==", "!=", "<=", ">=", "<", ">") @@ -248,13 +237,6 @@ class Expectation: def parse_argument(kind: str, argument: str, directory: Path) -> Any: """Validate a marker argument and return its parsed form.""" - if kind == "BUG": - parts = argument.split() - if len(parts) not in (1, 2) or (len(parts) == 2 and parts[1] not in ("definite", "possible")): - raise ValueError("BUG takes ' [definite|possible]'") - if parts[0] not in IDS: - raise ValueError(f"unknown diagnostic id '{parts[0]}'") - return (parts[0], parts[1] if len(parts) == 2 else None) if kind == "TRAP": if argument and argument not in KINDS: raise ValueError(f"unknown report kind '{argument}' (one of {', '.join(KINDS)})") @@ -263,18 +245,8 @@ def parse_argument(kind: str, argument: str, directory: Path) -> Any: if argument and argument not in LEDGER_REASONS[kind]: raise ValueError(f"unknown {kind.lower()} reason '{argument}'") return argument or None - if kind == "NEUTRALISED": - if argument != "zero-init": - raise ValueError("NEUTRALISED takes 'zero-init'") - return argument if kind == "MISS": return argument - if kind == "ALLOW": - ids = argument.split() - unknown = [i for i in ids if i not in IDS] - if unknown: - raise ValueError(f"unknown diagnostic id '{unknown[0]}'") - return tuple(ids) if kind == "RUN-INPUT": try: tokens = shlex.split(argument) @@ -383,8 +355,6 @@ class Case: unit_flags: dict[Path, tuple[str, ...]] flags: tuple[str, ...] clean: bool - allow: frozenset[str] - tool: bool asan: bool run_inputs: list[RunInput] expectations: list[Expectation] @@ -399,24 +369,13 @@ class Case: def line_markers(self, kind: str) -> list[Marker]: return [m for m in self.markers if m.kind == kind] - @property - def bugs(self) -> list[Marker]: - return self.line_markers("BUG") - @property def traps(self) -> list[Marker]: return self.line_markers("TRAP") - def bug_lines(self) -> set[tuple[Path, int]]: - return {(m.file, m.line) for m in self.markers if m.kind in ("BUG", "MISS", "NEUTRALISED", "TRAP")} - @property def is_bug_case(self) -> bool: - return bool(self.bug_lines()) - - def analysed_units(self) -> list[Path]: - """Units the analysis sees: those not compiled with -fno-weavec.""" - return [u for u in self.units if "-fno-weavec" not in self.unit_flags.get(u, ())] + return bool(self.traps) def relative(path: Path | str | None) -> str: @@ -484,9 +443,7 @@ def markers_of(file: Path) -> SourceMarkers: errors.append(f"{path.name}: FLAGS of a case's main file cannot contain -fno-weavec") case = Case( path=path, rel=rel, suite=rel.split("/", 1)[0], units=units, unit_flags=unit_flags, - flags=main_flags, clean=bool(file_markers["CLEAN"]), - allow=frozenset(i for marker in file_markers["ALLOW"] for i in marker.value), - tool=bool(file_markers["TOOL"]), asan=bool(file_markers["ASAN"]), + flags=main_flags, clean=bool(file_markers["CLEAN"]), asan=bool(file_markers["ASAN"]), run_inputs=[marker.value for marker in file_markers["RUN-INPUT"]], expectations=[marker.value for marker in file_markers["EXPECT-LEDGER"]], markers=markers, has_main=any(defines_main(t) for t in texts.values()), errors=errors, @@ -508,17 +465,20 @@ def markers_of(file: Path) -> SourceMarkers: if marker.kind == "MISS" and (marker.file, marker.line) not in stops: errors.append(f"{marker.file.name}:{marker.line}: in a DETECT case MISS marks a STOP line " f"as a known miss") - elif "STOP" in kinds: - errors.append(f"{path.name}: STOP needs a DETECT file marker") + else: + if "STOP" in kinds: + errors.append(f"{path.name}: STOP needs a DETECT file marker") + if "MISS" in kinds: + errors.append(f"{path.name}: MISS marks a STOP line of a DETECT case") if not kinds & EXPECTATION_MARKERS: errors.append(f"{path.name}: no expectation (CLEAN, EXPECT-LEDGER or a line marker)") - if case.clean and {"BUG", "MISS", "NEUTRALISED", "TRAP"} & kinds: - errors.append(f"{path.name}: CLEAN contradicts its BUG, MISS, NEUTRALISED or TRAP markers") - if case.allow and not case.clean: - errors.append(f"{path.name}: ALLOW only applies to a CLEAN case") - if case.tool and (case.run_inputs or case.asan): - errors.append(f"{path.name}: a TOOL case is not run (no RUN-INPUT or ASAN)") - if case.asan and not case.tool and not case.has_main: + if case.clean and "TRAP" in kinds: + errors.append(f"{path.name}: CLEAN contradicts its TRAP markers") + if (not case.has_main and case.detect is None and not case.expectations + and not kinds & LEDGER_MARKERS): + errors.append(f"{path.name}: a case without main has nothing to run: it needs " + f"EXPECT-LEDGER or a ledger marker") + if case.asan and not case.has_main: errors.append(f"{path.name}: ASAN needs a unit that defines main") if case.run_inputs and not case.has_main: errors.append(f"{path.name}: RUN-INPUT needs a unit that defines main") @@ -558,31 +518,9 @@ def matches(case: Case, pattern: str) -> bool: # Flags # --------------------------------------------------------------------------- -_W_FLAG = re.compile(r"^-W(?:no-)?(?:error=)?weavec(?:-[a-z0-9-]+)?$") - - -def tool_flags(flags: tuple[str, ...] | list[str]) -> tuple[list[str], list[str]]: - """Split a case's FLAGS into the `weavec` tool's own options and the compiler flags.""" - own: list[str] = [] - compiler: list[str] = [] - for flag in flags: - if flag.startswith("-fweavec-budget="): - own.append("--budget=" + flag.split("=", 1)[1]) - elif flag == "-fno-weavec-zero-init": - own.append("--no-zero-init") - elif _W_FLAG.match(flag): - own.append(flag) - elif flag.startswith("-fweavec") or flag.startswith("-fno-weavec"): - continue - else: - compiler.append(flag) - return own, compiler - - def plain_flags(flags: tuple[str, ...] | list[str]) -> list[str]: """The flags another compiler accepts: WeaveC's own dropped.""" - return [f for f in flags if not (f.startswith("-fweavec") or f.startswith("-fno-weavec") - or _W_FLAG.match(f))] + return [f for f in flags if not (f.startswith("-fweavec") or f.startswith("-fno-weavec"))] # --------------------------------------------------------------------------- @@ -590,22 +528,6 @@ def plain_flags(flags: tuple[str, ...] | list[str]) -> list[str]: # --------------------------------------------------------------------------- -@dataclasses.dataclass(frozen=True) -class Diagnostic: - file: str - line: int - column: int - severity: str # "error" or "warning" - id: str - message: str - - def text(self) -> str: - return f"{relative(self.file)}:{self.line}:{self.column}: {self.severity}: {self.message} [weavec::{self.id}]" - - -_DIAGNOSTIC = re.compile(r"^(.+?):(\d+):(\d+): (error|warning): (.*) \[weavec::([a-z0-9-]+)\]$") - - def resolve(file: str, cwd: Path | None) -> str: path = Path(file) if not path.is_absolute() and cwd is not None: @@ -616,17 +538,6 @@ def resolve(file: str, cwd: Path | None) -> str: return str(path) -def parse_diagnostics(output: str, cwd: Path | None = None) -> list[Diagnostic]: - result: list[Diagnostic] = [] - for line in output.splitlines(): - match = _DIAGNOSTIC.match(line.strip()) - if match: - file, number, column, severity, message, ident = match.groups() - result.append(Diagnostic(resolve(file, cwd), int(number), int(column), severity, - ident, message)) - return result - - @dataclasses.dataclass(frozen=True) class Report: """A run-time stop: a report with a location, or one without.""" @@ -708,7 +619,6 @@ class Evidence: commands: list[str] = dataclasses.field(default_factory=list) failures: list[str] = dataclasses.field(default_factory=list) notes: list[str] = dataclasses.field(default_factory=list) - diagnostics: list[Diagnostic] = dataclasses.field(default_factory=list) built: bool = False runs: list[Run] = dataclasses.field(default_factory=list) ledger: dict | None = None @@ -751,7 +661,6 @@ def compare(actual: Any, op: str, expected: Any) -> bool: @dataclasses.dataclass class Config: - weavec: Path weavec_cc: Path clang: str checks: str @@ -804,27 +713,6 @@ def first_line(text: str) -> str: # --------------------------------------------------------------------------- -def analyse(case: Case, cfg: Config, ev: Evidence, directory: Path) -> None: - """The `weavec` tool over the case's analysed units.""" - units = case.analysed_units() - if not units: - return - own, compiler = tool_flags(case.flags) - command = [str(cfg.weavec), *own] - if len(units) > 1: - command.append("--whole-program") - command.extend(str(u) for u in units) - command.extend(["--", f"-I{RESOURCE_INCLUDE}", *compiler]) - ev.commands.append(shlex.join(command)) - code, out, err, timed_out = run_process(command, directory, cfg.compile_timeout) - if timed_out: - ev.failures.append("the analysis timed out") - return - ev.diagnostics = parse_diagnostics(out + "\n" + err, directory) - if code not in (0, 1): - ev.failures.append(f"the analysis crashed ({code}): {first_line(err)}") - - def build(case: Case, cfg: Config, ev: Evidence, directory: Path, extra: list[str], ledger: bool) -> Path | None: """`weavec-cc` per unit, then the link.""" @@ -905,66 +793,6 @@ def run_asan(case: Case, cfg: Config, ev: Evidence, directory: Path) -> None: # --------------------------------------------------------------------------- -# A BUG marker of these ids is also satisfied by a run that stops on its line: -# the analysis is advisory, and the guards catch what it does not report. -STOPPABLE_IDS = frozenset(("out-of-bounds", "null-dereference", "use-after-free", "double-free", - "invalid-release", "use-of-uninitialized", "mismatched-release", - "lifetime-too-short", "use-after-move", "allocation-failure", - "contradicted-assumption", "unsafe-operation")) - - -def stopped_lines(ev: Evidence) -> set[tuple[str, int]]: - lines = set() - for run in ev.runs: - for report in run.reports: - if report.file is not None: - lines.add((report.file, report.line)) - return lines - - -def judge_diagnostics(case: Case, ev: Evidence, failures: list[str], ran: bool) -> None: - stops = stopped_lines(ev) - by_line: dict[tuple[str, int], list[Diagnostic]] = collections.defaultdict(list) - for diagnostic in ev.diagnostics: - by_line[(diagnostic.file, diagnostic.line)].append(diagnostic) - expected = set() - # `BUG: // MISS: `: the analysis is known not to report it; - # `// NEUTRALISED: zero-init`: zero-initialisation defines it away. - missed = {located(m.file, m.line) for m in case.markers - if m.kind in ("MISS", "NEUTRALISED")} - # A stop with no location (the allocator's, a fault) of the BUG's kind. - unlocated = {run.stop.kind for run in ev.runs - if run.stop is not None and run.stop.file is None} - for bug in case.bugs: - ident, certainty = bug.value - key = located(bug.file, bug.line) - expected.add(key) - if key in missed: - continue - found = [d for d in by_line.get(key, []) if d.id == ident] - if certainty == "definite": - found = [d for d in found if d.severity == "error"] - elif certainty == "possible": - found = [d for d in found if d.severity == "warning"] - if not found and ident in STOPPABLE_IDS and (key in stops or not ran - or ident in unlocated): - continue - if not found: - failures.append(f"{loc(bug)}: expected weavec::{ident}" - + (f" ({certainty})" if certainty else "") - + " from the analysis or a stop on its line") - expected |= {located(m.file, m.line) for m in case.markers if m.kind in ("MISS", "NEUTRALISED")} - for diagnostic in ev.diagnostics: - key = (diagnostic.file, diagnostic.line) - if key in expected: - continue - # ALLOW excuses warnings only: an error always fails a CLEAN case. - if case.clean and (diagnostic.id not in case.allow or diagnostic.severity == "error"): - failures.append(f"unexpected diagnostic in a CLEAN case: {diagnostic.text()}") - elif diagnostic.severity == "error" and not case.clean: - failures.append(f"unexpected error: {diagnostic.text()}") - - def judge_runs(case: Case, ev: Evidence, failures: list[str], notes: list[str]) -> None: traps = case.traps hit: set[Marker] = set() @@ -983,9 +811,7 @@ def judge_runs(case: Case, ev: Evidence, failures: list[str], notes: list[str]) matching = [m for m in traps if (m.value is None or m.value == stop.kind) and (stop.file is None or located(m.file, m.line) == (stop.file, stop.line))] - on_bug_line = stop.file is None and bool(case.bugs) or any( - located(m.file, m.line) == (stop.file, stop.line) for m in case.bugs) - if not matching and not on_bug_line: + if not matching: failures.append(f"{label}: unexpected stop: {stop.text()} ({run.describe()})") hit.update(matching) if ev.runs: @@ -1062,34 +888,24 @@ def run_markers(case: Case, cfg: Config) -> dict: "notes": [], "kind": "markers"} directory = scratch_for(case, cfg) try: - analysed = bool(case.bugs or case.clean or case.tool) - if analysed: - analyse(case, cfg, ev, directory) - if not case.tool: - ledger = bool([m for m in case.markers if m.kind in LEDGER_MARKERS] - or case.expectations) - exe = build(case, cfg, ev, directory / "build", [], ledger) - judge_ledger(case, ev, failures) - if exe is not None and not cfg.no_run: - run_all(case, cfg, ev, exe, directory / "build") - judge_runs(case, ev, failures, ev.notes) - if case.clean: - for run in ev.runs: - if run.reports or run.signal is not None: - failures.append(f"run {shlex.join(run.args) or '(no arguments)'}: " - f"{run.describe()} in a CLEAN case") - if (cfg.asan or case.asan) and case.has_main and not cfg.no_run: - run_asan(case, cfg, ev, directory / "asan") - bug_case = case.is_bug_case - if bug_case and case.asan and ev.asan_ran and ev.asan_report is None: - failures.append("ASan reported nothing") - if not bug_case and ev.asan_report is not None: - failures.append(f"ASan reported {ev.asan_report} in a case without a bug") - if analysed: - # A case that cannot run (no main, TOOL) has no stop to excuse - # a BUG the analysis does not report. - runnable = case.has_main and not case.tool - judge_diagnostics(case, ev, failures, bool(ev.runs) or not runnable) + ledger = bool([m for m in case.markers if m.kind in LEDGER_MARKERS] or case.expectations) + exe = build(case, cfg, ev, directory / "build", [], ledger) + judge_ledger(case, ev, failures) + if exe is not None and not cfg.no_run: + run_all(case, cfg, ev, exe, directory / "build") + judge_runs(case, ev, failures, ev.notes) + if case.clean: + for run in ev.runs: + if run.reports or run.signal is not None: + failures.append(f"run {shlex.join(run.args) or '(no arguments)'}: " + f"{run.describe()} in a CLEAN case") + if (cfg.asan or case.asan) and case.has_main and not cfg.no_run: + run_asan(case, cfg, ev, directory / "asan") + bug_case = case.is_bug_case + if bug_case and case.asan and ev.asan_ran and ev.asan_report is None: + failures.append("ASan reported nothing") + if not bug_case and ev.asan_report is not None: + failures.append(f"ASan reported {ev.asan_report} in a case without a bug") failures.extend(ev.failures) finally: if not cfg.keep: @@ -1097,7 +913,6 @@ def run_markers(case: Case, cfg: Config) -> dict: return { "case": case.rel, "suite": case.suite, "status": "fail" if failures else "pass", "failures": failures, "notes": ev.notes, "kind": "markers", - "diagnostics": [d.text() for d in ev.diagnostics], "runs": [{"args": list(r.args), "result": r.describe(), "reports": [x.text() for x in r.reports]} for r in ev.runs], "asan": ev.asan_report, "commands": ev.commands, @@ -1231,7 +1046,6 @@ def main(argv: list[str] | None = None) -> int: formatter_class=argparse.RawDescriptionHelpFormatter, epilog=__doc__.split("\n\n", 1)[1]) parser.add_argument("--weavec-cc", type=Path, help="weavec-cc to test (default: /bin)") - parser.add_argument("--weavec", type=Path, help="weavec to test (default: /bin)") parser.add_argument("--build-dir", type=Path, help="where bin/ is (default: build/release, else build/dev)") parser.add_argument("--clang", default=None, help="the reference compiler for --asan") @@ -1241,7 +1055,7 @@ def main(argv: list[str] | None = None) -> int: parser.add_argument("--checks", choices=("trap", "verify"), default="trap", help="the -fweavec-checks mode of the builds") parser.add_argument("--asan", action="store_true", help="run the ASan oracle for every case") - parser.add_argument("--no-run", action="store_true", help="analyse and build only") + parser.add_argument("--no-run", action="store_true", help="build only") parser.add_argument("--filter", action="append", default=[], metavar="GLOB", help="select cases by path under the cases tree (repeatable)") parser.add_argument("--json", type=Path, metavar="OUT", help="write the results as JSON") @@ -1255,12 +1069,10 @@ def main(argv: list[str] | None = None) -> int: args = parser.parse_args(argv) build_dir = args.build_dir or default_build_dir() - weavec = args.weavec or build_dir / "bin" / "weavec" weavec_cc = args.weavec_cc or build_dir / "bin" / "weavec-cc" - for tool in (weavec, weavec_cc): - if not tool.exists(): - print(f"run-cases: {tool} does not exist; build it first", file=sys.stderr) - return 2 + if not weavec_cc.exists(): + print(f"run-cases: {weavec_cc} does not exist; build it first", file=sys.stderr) + return 2 clang = args.clang or shutil.which("clang") or "clang" if args.clang is None: prefix = os.environ.get("WEAVEC_LLVM_PREFIX") @@ -1272,7 +1084,7 @@ def main(argv: list[str] | None = None) -> int: print("run-cases: no case selected", file=sys.stderr) return 2 scratch = Path(tempfile.mkdtemp(prefix="weavec-cases.")) - cfg = Config(weavec=weavec.resolve(), weavec_cc=weavec_cc.resolve(), clang=clang, + cfg = Config(weavec_cc=weavec_cc.resolve(), clang=clang, checks=args.checks, asan=args.asan, no_run=args.no_run, keep=args.keep, compile_timeout=args.timeout, run_timeout=args.run_timeout, scratch=scratch) results: list[dict] = [] diff --git a/scripts/test_check_hygiene.py b/scripts/test_check_hygiene.py index a1de232f..d7f0962b 100755 --- a/scripts/test_check_hygiene.py +++ b/scripts/test_check_hygiene.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Tests for scripts/check-hygiene.py (RFC 0030 gate H2, as RFC 0031 amends it). +"""Tests for scripts/check-hygiene.py (RFC 0036 gate H1). Each test builds a small fake WeaveC tree in a temporary directory. Those trees are not git work trees, so the checker walks them; GitSelectionTest @@ -27,7 +27,8 @@ sys.modules["check_hygiene"] = hygiene SPEC.loader.exec_module(hygiene) -# The real grammar: comments, continuation lines, directives, chk aliases. +# The real grammar: comments, continuation lines, directives, chk aliases, +# wrappers. LIBRARY_SPEC = r""" # LibrarySpec.txt - a small table in the real grammar. # fake (int) -> void; <- a commented-out entry is no entry @@ -39,69 +40,57 @@ copies(0,1,a2) chk(__builtin___memcpy_chk: 0,1,2,-1) \ chk(__memcpy_chk: 0,1,2,-1); fgets (w:bytes(a1), int, rw) -> \ - arg(0):null-on-failure:str chk(__fgets_chk: 0,-1,1,2); + arg(0):null-on-failure:str chk(__fgets_chk: 0,-1,1,2) wrapper(fgets); builtins; __builtin_expect (int, int) -> int; """ -ENGINE_H = """\ -#ifndef WEAVEC_LIB_ANALYSIS_ENGINE_H -#define WEAVEC_LIB_ANALYSIS_ENGINE_H - -#include "weavec/Analysis/LedgerAdapter.h" -#include "weavec/Analysis/ObjectEngine.h" - -namespace weavec::analysis { - -/// Publishes only through `ledgerAdapter`, never a DiagnosticSink. -class FunctionRun { -public: - FunctionRun(clang::ASTContext &ctx, LedgerAdapter &ledgerAdapter); - const char *why = "no DiagnosticSink here"; - // MEMBERS -}; - -} // namespace weavec::analysis - -#endif // WEAVEC_LIB_ANALYSIS_ENGINE_H +RUNTIME_HEADER = """\ +/* weavec_rt.h: __weavec_rt_documented is only named in a comment. */ +#include +struct __weavec_rt_shadow_t { uintptr_t base; }; +extern struct __weavec_rt_shadow_t __weavec_rt_shadow; +void __weavec_rt_guard(uintptr_t address, uint64_t width); +void __weavec_rt_unpoison_stack(void); +char *__weavec_rt_fgets(char *d, int n, void *stream); """ -ENGINE_RUN_CPP = """\ -#include "Engine.h" - -using namespace weavec::analysis; +GUARD_PASS_CPP = """\ +#include "GuardPassImpl.h" +#include "llvm/IR/Module.h" -FunctionRun::FunctionRun(clang::ASTContext &ctx, LedgerAdapter &ledgerAdapter) {} +llvm::FunctionCallee ModuleContext::guard() { return runtime("guard", guardType); } +llvm::Value *ModuleContext::shadow() { return module.getOrInsertGlobal("__weavec_rt_shadow", type); } +llvm::FunctionCallee ModuleContext::runtime(llvm::StringRef name, llvm::FunctionType *type) { + return module.getOrInsertFunction(("__weavec_rt_" + name).str(), type); +} """ -OBJECT_ENGINE_H = """\ -#pragma once -#include "weavec/Analysis/SafetyEngine.h" -class ObjectEngine : public SafetyEngine { -public: - void run(LedgerAdapter &ledgerAdapter) override; -}; +GUARD_FRAMES_CPP = """\ +#include "GuardPassImpl.h" + +void unpoison(ModuleContext &module) { + llvm::FunctionCallee unpoison = module.runtime( + "unpoison_stack", + llvm::FunctionType::get(llvm::Type::getVoidTy(module.context), false)); +} """ # A tree that passes every check. CLEAN_TREE = { - "lib/CMakeLists.txt": "add_subdirectory(Core)\nadd_subdirectory(Analysis)\n", + "lib/CMakeLists.txt": "add_subdirectory(Core)\nadd_subdirectory(Frontend)\n", "lib/Core/LibrarySpec.txt": LIBRARY_SPEC, - "lib/Core/LibrarySpec.cpp": 'bool isFree(std::string_view n) { return n == "free"; }\n', - "lib/Analysis/Engine.h": ENGINE_H, - "lib/Analysis/EngineRun.cpp": ENGINE_RUN_CPP, - "lib/Analysis/LedgerAdapter.cpp": '#include "weavec/Analysis/LedgerAdapter.h"\n', - "include/weavec/Analysis/LedgerAdapter.h": ( - '#pragma once\n#include "weavec/Core/Diagnostic.h"\n' - "// The adapter owns the sink: that is the seam.\n" - "class LedgerAdapter {\n weavec::core::DiagnosticSink &sink;\n};\n"), - "include/weavec/Analysis/ObjectEngine.h": OBJECT_ENGINE_H, - "include/weavec/Analysis/SafetyEngine.h": "#pragma once\nclass SafetyEngine {};\n", - "include/weavec/Core/Diagnostic.h": "#pragma once\nclass DiagnosticSink {};\n", - "tools/weavec/main.cpp": "int main() { return 0; }\n", - "docs/rfcs/0030-prove-or-trap.md": "The --checked flag and SafetyState are retired.\n", - "docs/rfcs/0031-object-engine.md": "Replaces FunctionDataflow and DataflowEngine.\n", - "docs/pages/reference/cli.md": "# CLI\n\nweavec [options] files\n", + "lib/Core/LibrarySpec.cpp": ('#include "weavec/Core/LibrarySpec.h"\n#include \n' + 'bool isFree(std::string_view n) { return n == "free"; }\n'), + "include/weavec/Core/LibrarySpec.h": "#pragma once\n#include \n", + "lib/Frontend/GuardPass.cpp": GUARD_PASS_CPP, + "lib/Frontend/GuardFrames.cpp": GUARD_FRAMES_CPP, + "include/weavec/Frontend/GuardPass.h": "#pragma once\n#include \n", + "tools/weavec-cc/main.cpp": "int main() { return 0; }\n", + "runtime/weavec_rt.h": RUNTIME_HEADER, + "runtime/weavec_guard.c": '#include "weavec_rt.h"\n#include \nvoid f(void) {}\n', + "runtime/test/rt_test.c": '#include "../weavec_rt.h"\nint main(void) { return 0; }\n', + "docs/pages/reference/cli.md": "# CLI\n\nweavec-cc [options] files\n", } @@ -145,59 +134,22 @@ def test_clean_tree_passes_every_check(self): self.assertEqual(result.violations, []) self.assertEqual(result.file_source, "walk") self.assertEqual((result.library_entries, result.library_aliases), (5, 3)) + self.assertEqual(result.entry_points, {"__weavec_rt_fgets": True, "__weavec_rt_guard": True, + "__weavec_rt_shadow": True, "__weavec_rt_unpoison_stack": True}) code, output = self.run_main() self.assertEqual(code, 0) - self.assertTrue(output.startswith("Summary (RFC 0030 and 0031, gate H2; RFC 0032, gate H1)\n"), output) - self.assertEqual(output.splitlines()[-1], "H2: PASS (0 violations)") + self.assertTrue(output.startswith("Summary (RFC 0036, gate H1)\n"), output) + self.assertEqual(output.splitlines()[-1], "H1: PASS (0 violations)") def test_walk_skips_build_and_tool_output(self): for skipped in ("node_modules/pkg", "dist", ".generated", ".astro", "test-results", "playwright-report", "__pycache__", "build-release"): - self.write(f"docs/{skipped}/notes.md", "--checked\n") - self.write(f"lib/{skipped}/Notes.cpp", "SafetyState state;\n") - self.assertEqual(self.found("retired-name"), []) - + self.write(f"lib/{skipped}/Notes.cpp", "// cJSON\n") + self.assertEqual(self.found("corpus-word"), []) -class RetiredNameTest(TreeTest): - def test_docs_rfcs_are_exempt_but_other_docs_are_not(self): - self.write("docs/rfcs/0018-checked-code.md", "Use --checked and checkContracts.\n") - self.write("docs/pages/reference/cli.md", """ - # CLI - - --checked enables checked mode. - See `--checked-report` too. - """) - self.assertEqual(self.found("retired-name"), [ - ("docs/pages/reference/cli.md", 3, "'--checked' at column 1"), - ("docs/pages/reference/cli.md", 4, "'--checked' at column 6"), - ]) - - def test_lib_and_tools_count_but_include_is_not_scanned(self): - self.write("lib/Core/Safety.cpp", "// safetystate is fine\nSafetyState a; CheckedContract b;\n") - self.write("tools/weavec/Options.cpp", "bool checkContracts = false;\n") - self.write("include/weavec/Core/Safety.h", "struct CheckedContract;\n") - self.assertEqual(self.found("retired-name"), [ - ("lib/Core/Safety.cpp", 2, "'SafetyState' at column 1"), - ("lib/Core/Safety.cpp", 2, "'CheckedContract' at column 16"), - ("tools/weavec/Options.cpp", 1, "'checkContracts' at column 6"), - ]) - - def test_old_engine_names(self): - self.write("lib/Analysis/Old.cpp", "SummaryStore store; FunctionSummary summary;\n") - self.write("tools/weavec/main.cpp", "// TranslationUnitAnalyzer, FunctionAnalyzer\n") - self.write("docs/development.md", "Run DataflowEngine.\n") - self.write("docs/architecture.md", "The engine: `FunctionDataflow`, and --checked.\n") - self.write("docs/rfcs/0030-prove-or-trap.md", "FunctionDataflow stays in RFC text.\n") - result = hygiene.run_checks(self.root) - self.assertEqual([(v.path, v.line, v.message) for v in result.violations], [ - ("docs/architecture.md", 1, "'FunctionDataflow' at column 14"), - ("docs/architecture.md", 1, "'--checked' at column 37"), - ("docs/development.md", 1, "'DataflowEngine' at column 5"), - ("lib/Analysis/Old.cpp", 1, "'SummaryStore' at column 1"), - ("lib/Analysis/Old.cpp", 1, "'FunctionSummary' at column 21"), - ("tools/weavec/main.cpp", 1, "'TranslationUnitAnalyzer' at column 4"), - ("tools/weavec/main.cpp", 1, "'FunctionAnalyzer' at column 29"), - ]) + def test_docs_are_not_scanned(self): + self.write("docs/pages/corpus.md", 'zlib: name == "free"\n#include \n') + self.assertEqual(hygiene.run_checks(self.root).violations, []) class LibrarySpecNamesTest(unittest.TestCase): @@ -209,7 +161,7 @@ def test_logical_lines_drop_comments_and_join_continuations(self): "copies(0,1,a2) chk(__builtin___memcpy_chk: 0,1,2,-1) " "chk(__memcpy_chk: 0,1,2,-1);") self.assertEqual(lines[11], "fgets (w:bytes(a1), int, rw) -> arg(0):null-on-failure:str " - "chk(__fgets_chk: 0,-1,1,2);") + "chk(__fgets_chk: 0,-1,1,2) wrapper(fgets);") self.assertNotIn(9, lines) # joined to line 8 def test_entries_aliases_and_builtin_spellings(self): @@ -223,14 +175,14 @@ def test_entries_aliases_and_builtin_spellings(self): self.assertEqual(names["__builtin_free"], "__builtin_ spelling (of free)") self.assertEqual(names["__builtin___fgets_chk"], "__builtin_ spelling (of __fgets_chk, a chk alias of fgets)") - for word in ("header", "builtins", "stdlib", "string", "fake", "arg", "chk", "copies"): + for word in ("header", "builtins", "stdlib", "string", "fake", "arg", "chk", "copies", "wrapper"): self.assertNotIn(word, names) self.assertEqual(len(names), 15) # 5 entries, 3 aliases, 7 new __builtin_ spellings class LibraryNameTestTest(TreeTest): def test_comparisons_in_either_order_outside_comments(self): - self.write("lib/Analysis/Calls.cpp", r''' + self.write("lib/Frontend/Calls.cpp", r''' bool a(llvm::StringRef name) { return name == "free"; } bool b(llvm::StringRef name) { return "free" == name; } bool c(llvm::StringRef name) { return name == "freedom" || name != "free"; } @@ -245,29 +197,29 @@ def test_comparisons_in_either_order_outside_comments(self): ''') entry = "compares against a LibrarySpec entry" self.assertEqual(self.found("library-name-test"), [ - ("lib/Analysis/Calls.cpp", 1, f'== "free" {entry}'), - ("lib/Analysis/Calls.cpp", 2, f'"free" == {entry}'), - ("lib/Analysis/Calls.cpp", 4, f'== "malloc" {entry}'), - ("lib/Analysis/Calls.cpp", 8, f'== "memcpy" {entry}'), - ("lib/Analysis/Calls.cpp", 10, + ("lib/Frontend/Calls.cpp", 1, f'== "free" {entry}'), + ("lib/Frontend/Calls.cpp", 2, f'"free" == {entry}'), + ("lib/Frontend/Calls.cpp", 4, f'== "malloc" {entry}'), + ("lib/Frontend/Calls.cpp", 8, f'== "memcpy" {entry}'), + ("lib/Frontend/Calls.cpp", 10, '== "__memcpy_chk" compares against a LibrarySpec chk alias (of memcpy)'), - ("lib/Analysis/Calls.cpp", 10, + ("lib/Frontend/Calls.cpp", 10, '== "__builtin_memcpy" compares against a LibrarySpec __builtin_ spelling (of memcpy)'), - ("lib/Analysis/Calls.cpp", 11, f'"fgets" == {entry}'), - ("lib/Analysis/Calls.cpp", 11, f'== "free" {entry}'), + ("lib/Frontend/Calls.cpp", 11, f'"fgets" == {entry}'), + ("lib/Frontend/Calls.cpp", 11, f'== "free" {entry}'), ]) def test_only_c_sources_outside_library_spec_count(self): self.write("lib/Core/LibrarySpecTable.inc", 'if (name == "free") return;\n') - self.write("lib/Analysis/notes.md", 'name == "free"\n') - self.write("include/weavec/Analysis/Names.def", 'CASE(name == "malloc")\n') - self.write("tools/weavec/main.cc", 'bool x = arg == "fgets";\n') - self.write("docs/examples/names.c", 'int y = arg == "free";\n') + self.write("lib/Frontend/notes.md", 'name == "free"\n') + self.write("include/weavec/Frontend/Names.def", 'CASE(name == "malloc")\n') + self.write("tools/weavec-cc/main.cc", 'bool x = arg == "fgets";\n') + self.write("runtime/weavec_libc.c", 'int y = arg == "free";\n') self.assertEqual([(path, line) for path, line, _ in self.found("library-name-test")], - [("include/weavec/Analysis/Names.def", 1), ("tools/weavec/main.cc", 1)]) + [("include/weavec/Frontend/Names.def", 1), ("tools/weavec-cc/main.cc", 1)]) def test_lexing_keeps_literals_comments_and_numbers_apart(self): - self.write("lib/Analysis/Lexing.cpp", r''' + self.write("lib/Frontend/Lexing.cpp", r''' auto raw = R"x(a)" == "free")x"; char quote = '"'; bool a = n == "malloc"; long big = 1'000; bool b = n == "free"; @@ -278,9 +230,18 @@ def test_lexing_keeps_literals_comments_and_numbers_apart(self): self.assertEqual([(line, message.split()[1]) for _, line, message in self.found("library-name-test")], [(2, '"malloc"'), (3, '"free"'), (5, '"fgets"'), (6, '"memcpy"')]) + def test_lexing_keeps_offsets_and_lines(self): + text = 'a = "x{"; // }\nb = \'}\'; /* {\n */ c = R"(})";\n' + lexed = hygiene.lex_c(text) + self.assertEqual((len(lexed.code), len(lexed.bare)), (len(text), len(text))) + self.assertEqual(lexed.bare.count("\n"), text.count("\n")) + self.assertNotIn("{", lexed.bare) + self.assertNotIn("}", lexed.bare) + self.assertEqual([body for _, _, body in lexed.strings], ["x{"]) + def test_missing_library_spec_is_a_violation(self): (self.root / "lib/Core/LibrarySpec.txt").unlink() - self.write("lib/Analysis/Calls.cpp", 'bool a = n == "free";\n') + self.write("lib/Frontend/Calls.cpp", 'bool a = n == "free";\n') code, output = self.run_main() self.assertEqual(code, 1) self.assertEqual(output.splitlines()[0], "lib/Core/LibrarySpec.txt: library-name-test: " @@ -294,7 +255,7 @@ def test_word_rule(self): line = "cJSON_IsString lua_State evaluation sdsnew LUA" words = [word for _, _, word in hygiene.find_all(pattern, line)] self.assertEqual(words, ["cJSON", "lua"]) - self.write("lib/Analysis/Words.cpp", """ + self.write("lib/Frontend/Words.cpp", """ // cJSON_IsString and lua_State are corpus names. int evaluation = sdsnew() + LUA + Zlib + lua2 + sds9 + x_sds; #include // Lua's jansson, linenoise, jsmn, sds, minigzip @@ -317,7 +278,7 @@ def test_fresh_and_sealed_projects(self): fresh_and_sealed = ("zstd", "libuv", "oniguruma", "redis", "expat", "pcre2", "libevent", "libsodium", "libxml2", "libpng", "mbedtls", "msgpack", "yyjson") self.assertTrue(set(fresh_and_sealed) <= set(hygiene.CORPUS_WORDS)) - self.write("lib/Analysis/Fresh.cpp", """ + self.write("lib/Frontend/Fresh.cpp", """ // ZSTD_compress, redisCommand and expatriate are not words; msgpack_pack is. #include // libuv, oniguruma, pcre2, libevent, libsodium // libxml2 libpng mbedtls yyjson redis expat @@ -340,172 +301,102 @@ def test_fresh_and_sealed_projects(self): def test_every_file_under_lib_and_nothing_else(self): self.edit("lib/Core/LibrarySpec.txt", "a small table", "a small table (jansson.h is not listed)") - self.write("lib/Analysis/README.txt", "Measured on zlib.\n") - self.write("include/weavec/Analysis/Corpus.h", "// lua\n") - self.write("tools/weavec/main.cpp", "// cJSON\n") + self.write("lib/Frontend/README.txt", "Measured on zlib.\n") + self.write("include/weavec/Frontend/Corpus.h", "// lua\n") + self.write("tools/weavec-cc/main.cpp", "// cJSON\n") + self.write("runtime/weavec_libc.c", "/* sds */\n") self.write("docs/pages/corpus.md", "sds\n") self.assertEqual(self.found("corpus-word"), [ - ("lib/Analysis/README.txt", 1, "word 'zlib' at column 13"), ("lib/Core/LibrarySpec.txt", 1, "word 'jansson' at column 36"), + ("lib/Frontend/README.txt", 1, "word 'zlib' at column 13"), ]) -class EngineIncludeTest(TreeTest): - def test_only_engine_sources_include_engine_h(self): - for allowed in ("lib/Analysis/EngineCalls.cpp", "lib/Analysis/ObjectEngine.cpp"): - self.write(allowed, '#include "Engine.h"\n') - self.write("lib/Analysis/SafetyEngine.cpp", '#include "Engine.h"\n') - self.write("lib/Analysis/EngineIntegers.h", '#include "Engine.h"\n') # a header - self.write("lib/Analysis/Sub/EngineWalk.cpp", '#include "../Engine.h"\n') # not in lib/Analysis - self.write("lib/Frontend/Driver.cpp", "#include \n") - self.write("lib/Analysis/UnitPipeline.cpp", """ - // #include "Engine.h" - #include "weavec/Analysis/ObjectEngine.h" - #include "weavec/Analysis/SafetyEngine.h" - """) - self.write("tools/weavec/main.cpp", '#include "../../lib/Analysis/Engine.h"\n') # not scanned - self.assertEqual(self.found("engine-include"), [ - ("lib/Analysis/EngineIntegers.h", 1, - "includes Engine.h directly: lib/Analysis/EngineIntegers.h:1 -> lib/Analysis/Engine.h"), - ("lib/Analysis/SafetyEngine.cpp", 1, - "includes Engine.h directly: lib/Analysis/SafetyEngine.cpp:1 -> lib/Analysis/Engine.h"), - ("lib/Analysis/Sub/EngineWalk.cpp", 1, - "includes Engine.h directly: lib/Analysis/Sub/EngineWalk.cpp:1 -> lib/Analysis/Engine.h"), - ("lib/Frontend/Driver.cpp", 1, - "includes Engine.h directly: lib/Frontend/Driver.cpp:1 -> "), - ]) - - def test_direct_includes_by_new_components_resolved_or_not(self): - self.write("lib/Analysis/SiteCollector.cpp", """ - #include "weavec/Analysis/SiteCollector.h" - #include "Engine.h" - """) - self.write("include/weavec/Analysis/CheckPlanner.h", """ - #pragma once - # include +class CoreIncludeTest(TreeTest): + def test_clang_and_llvm_headers_in_core(self): + self.write("lib/Core/LibrarySpec.cpp", """ + #include "weavec/Core/LibrarySpec.h" + #include "llvm/ADT/StringRef.h" + // #include + /* #include */ + # include + #include + #include "Helpers/clang/Thing.h" """) - self.assertEqual(self.found("engine-include"), [ - ("include/weavec/Analysis/CheckPlanner.h", 2, "includes Engine.h directly: " - "include/weavec/Analysis/CheckPlanner.h:2 -> "), - ("lib/Analysis/SiteCollector.cpp", 2, "includes Engine.h directly: " - "lib/Analysis/SiteCollector.cpp:2 -> lib/Analysis/Engine.h"), + self.write("include/weavec/Core/Table.h", "#pragma once\n#include \n") + self.write("lib/Core/Notes.txt", "#include \n") + self.write("lib/Frontend/Driver.cpp", '#include "clang/Driver/Driver.h"\n') + self.assertEqual(self.found("core-include"), [ + ("include/weavec/Core/Table.h", 2, "includes "), + ("lib/Core/LibrarySpec.cpp", 2, 'includes "llvm/ADT/StringRef.h"'), + ("lib/Core/LibrarySpec.cpp", 5, "includes "), ]) - def test_transitive_include_by_a_new_component_reports_the_chain(self): - self.write("lib/Analysis/KindInferenceFields.cpp", """ - // Field invariants. - #include "KindInferenceImpl.h" +class RuntimeIncludeTest(TreeTest): + def test_only_weavec_rt_h(self): + self.write("runtime/weavec_alloc.c", """ + #include "weavec_rt.h" + #include "weavec_alloc.h" + #include + #include + // #include "other.h" + #include """) - self.write("lib/Analysis/KindInferenceImpl.h", """ - #pragma once - #include "weavec/Analysis/Bridge.h" - """) - self.write("include/weavec/Analysis/Bridge.h", """ - #pragma once - #include "weavec/Analysis/LedgerAdapter.h" - - #include "Engine.h" - """) - self.assertEqual(self.found("engine-include"), [ - ("include/weavec/Analysis/Bridge.h", 4, "includes Engine.h directly: " - "include/weavec/Analysis/Bridge.h:4 -> lib/Analysis/Engine.h"), - ("lib/Analysis/KindInferenceFields.cpp", 3, "includes Engine.h transitively: " - "lib/Analysis/KindInferenceFields.cpp:3 -> lib/Analysis/KindInferenceImpl.h:2 -> " - "include/weavec/Analysis/Bridge.h:4 -> lib/Analysis/Engine.h"), - ("lib/Analysis/KindInferenceImpl.h", 2, "includes Engine.h transitively: " - "lib/Analysis/KindInferenceImpl.h:2 -> include/weavec/Analysis/Bridge.h:4 -> " - "lib/Analysis/Engine.h"), + self.write("runtime/test/rt_test.c", '#include "../weavec_rt.h"\n#include "helpers.h"\n') + self.write("runtime/CMakeLists.txt", '# #include "notes.h"\n') + self.assertEqual(self.found("runtime-include"), [ + ("runtime/test/rt_test.c", 2, 'includes "helpers.h", not weavec_rt.h'), + ("runtime/weavec_alloc.c", 2, 'includes "weavec_alloc.h", not weavec_rt.h'), + ("runtime/weavec_alloc.c", 3, "includes the WeaveC header "), + ("runtime/weavec_alloc.c", 4, "includes the WeaveC header "), ]) - def test_other_headers_comments_and_cycles_do_not_count(self): - self.write("lib/Analysis/SlotCollector.cpp", """ - // #include "Engine.h" - /* #include "Engine.h" */ - #include "Cycle.h" - #include "EngineTypes.h" - #include "weavec/Analysis/ObjectEngine.h" - """) - self.write("lib/Analysis/Cycle.h", '#include "SlotCollector.h"\n#include "Cycle.h"\n') - self.write("lib/Analysis/SlotCollector.h", '#include "Cycle.h"\n') - self.write("lib/Analysis/EngineTypes.h", "#pragma once\nstruct Access {};\n") - self.assertEqual(self.found("engine-include"), []) - - -class EngineSinkTest(TreeTest): - def test_a_sink_in_the_engine(self): - self.edit("lib/Analysis/Engine.h", " // MEMBERS\n", - " core::DiagnosticSink &sink;\n struct Pending { ClangDiagnosticSink *clang; };\n") - self.write("lib/Analysis/EngineCalls.cpp", """ - #include "Engine.h" - void call(DiagnosticSink &sink) {} + +class RuntimeEntryTest(TreeTest): + def test_names_the_passes_use_must_be_declared(self): + self.write("lib/Frontend/GuardLoops.cpp", """ + llvm::FunctionCallee test = module.runtime( + "range_ok", type); + // module.runtime("commented", type); + const char *name = "__weavec_rt_documented"; + const char *prefix = "__weavec_rt_"; + bool runtimes = myruntime("not_a_pass_call"); """) - self.write("lib/Analysis/ObjectEngine.cpp", 'ObjectEngine::ObjectEngine(DiagnosticSink *s) {}\n') - self.edit("include/weavec/Analysis/ObjectEngine.h", "public:\n", - "public:\n explicit ObjectEngine(core::DiagnosticSink &sink);\n") - self.assertEqual(self.found("engine-sink"), [ - ("include/weavec/Analysis/ObjectEngine.h", 5, "the engine names DiagnosticSink"), - ("lib/Analysis/Engine.h", 14, "the engine names DiagnosticSink"), - ("lib/Analysis/Engine.h", 15, "the engine names ClangDiagnosticSink"), - ("lib/Analysis/EngineCalls.cpp", 2, "the engine names DiagnosticSink"), - ("lib/Analysis/ObjectEngine.cpp", 1, "the engine names DiagnosticSink"), + self.edit("lib/Core/LibrarySpec.txt", "free (none:null-ok:release(free)) -> void;", + "free (none:null-ok:release(free)) -> void wrapper(free);") + self.write("lib/Core/Notes.cpp", 'auto f = runtime("core_is_not_a_pass");\n') + self.assertEqual(self.found("runtime-entry"), [ + # By name. + ("lib/Frontend/GuardLoops.cpp", 4, + "__weavec_rt_documented is not declared in runtime/weavec_rt.h"), + ("lib/Core/LibrarySpec.txt", 5, "__weavec_rt_free is not declared in runtime/weavec_rt.h"), + ("lib/Frontend/GuardLoops.cpp", 1, "__weavec_rt_range_ok is not declared in runtime/weavec_rt.h"), ]) - - def test_a_sink_outside_the_engine_is_allowed(self): - for path in ("lib/Analysis/SafetyEngine.cpp", "lib/Analysis/Sub/EngineReport.cpp", - "lib/Frontend/EngineDriver.cpp", "include/weavec/Analysis/EngineOptions.h", - "tools/weavec/Engine.cpp"): - self.write(path, "void report(core::DiagnosticSink &sink) {}\n") - self.assertEqual(self.found("engine-sink"), []) - - def test_a_missing_engine_header_is_a_violation(self): - (self.root / "lib/Analysis/Engine.h").unlink() - self.assertEqual(self.found("engine-sink"), [ - ("lib/Analysis/Engine.h", None, "missing or unreadable, so the engine cannot be checked"), + self.edit("runtime/weavec_rt.h", "void __weavec_rt_unpoison_stack(void);", + "void __weavec_rt_unpoison_stack(void);\nint __weavec_rt_range_ok(uint64_t a, uint64_t n);\n" + "void __weavec_rt_free(void *p);\nextern int __weavec_rt_documented;") + self.assertEqual(self.found("runtime-entry"), []) + + def test_a_missing_runtime_header_is_a_violation(self): + (self.root / "runtime/weavec_rt.h").unlink() + self.assertEqual(self.found("runtime-entry"), [ + ("runtime/weavec_rt.h", None, "missing or unreadable, so the runtime's entry points are unknown"), ]) - def test_lexing_keeps_offsets_and_lines(self): - text = 'a = "x{"; // }\nb = \'}\'; /* {\n */ c = R"(})";\n' - lexed = hygiene.lex_c(text) - self.assertEqual((len(lexed.code), len(lexed.bare)), (len(text), len(text))) - self.assertEqual(lexed.bare.count("\n"), text.count("\n")) - self.assertNotIn("{", lexed.bare) - self.assertNotIn("}", lexed.bare) - self.assertEqual([body for _, _, body in lexed.strings], ["x{"]) - class LineLimitTest(TreeTest): def test_count_lines_is_wc_plus_a_last_line_without_newline(self): for data, lines in ((b"", 0), (b"\n", 1), (b"a", 1), (b"a\nb", 2), (b"a\nb\n", 2), (b"\n\n", 2)): self.assertEqual(hygiene.count_lines(data), lines, data) - def test_engine_total(self): - (self.root / "lib/Analysis/EngineCalls.cpp").write_bytes(b"a\nb\nc") # 3 lines - (self.root / "lib/Analysis/EngineIntegers.h").write_bytes(b"x\n") # 1 line - for other in ("lib/Analysis/SafetyEngine.cpp", "lib/Analysis/Engine.txt", - "lib/Analysis/Sub/EngineWalk.cpp", "include/weavec/Analysis/EngineOptions.h", - "tools/weavec/EngineTool.cpp"): - self.write(other, "one\n") - total = 19 + 5 + 3 + 1 # Engine.h, EngineRun.cpp, EngineCalls.cpp, EngineIntegers.h - with mock.patch.object(hygiene, "ENGINE_LINE_LIMIT", total): - result = hygiene.run_checks(self.root) - self.assertEqual(result.engine_lines, { - "lib/Analysis/Engine.h": 19, "lib/Analysis/EngineCalls.cpp": 3, - "lib/Analysis/EngineIntegers.h": 1, "lib/Analysis/EngineRun.cpp": 5, - }) - self.assertEqual(result.count("engine-lines"), 0) - with mock.patch.object(hygiene, "ENGINE_LINE_LIMIT", total - 1): - self.assertEqual(self.found("engine-lines"), [ - ("lib/Analysis/Engine*.{h,cpp}", None, "28 lines in 4 files, 1 over the limit of 27"), - ]) - - def test_the_real_limits_are_multiples_of_500(self): - """Measured plus about 10%, rounded up (the module docstring).""" - for limit in (hygiene.ENGINE_LINE_LIMIT, hygiene.LIBRARY_LINE_LIMIT, hygiene.RUNTIME_LINE_LIMIT): - self.assertEqual(limit % 500, 0) + def test_the_real_limits(self): + """RFC 0036 gate H1 and RFC 0032 gate H1 (the module docstring).""" + self.assertEqual((hygiene.LIBRARY_LINE_LIMIT, hygiene.RUNTIME_LINE_LIMIT), (9_000, 6_000)) def test_runtime_total_leaves_out_its_tests(self): """RFC 0032 gate H1: the runtime is counted apart, without runtime/test/.""" + shutil.rmtree(self.root / "runtime") for path, data in (("runtime/weavec_alloc.c", b"a\nb\nc\n"), ("runtime/weavec_rt.h", b"h\n"), ("runtime/CMakeLists.txt", b"x\n"), ("runtime/test/rt_test.c", b"t\n" * 50)): (self.root / path).parent.mkdir(parents=True, exist_ok=True) @@ -523,16 +414,16 @@ def test_runtime_total_leaves_out_its_tests(self): def test_library_total_counts_every_text_file_under_lib_include_and_tools(self): for top in ("lib", "include", "tools"): shutil.rmtree(self.root / top) - for path, data in (("lib/CMakeLists.txt", b"a\nb\n"), ("lib/Analysis/Partial.cpp", b"1\n2\n3"), - ("include/weavec/Empty.h", b"\n"), ("tools/weavec/CMakeLists.txt", b"x\ny\n"), - ("lib/Analysis/blob.bin", b"\0\1\2\n" * 10), ("docs/pages/long.md", b"line\n" * 100)): + for path, data in (("lib/CMakeLists.txt", b"a\nb\n"), ("lib/Frontend/Partial.cpp", b"1\n2\n3"), + ("include/weavec/Empty.h", b"\n"), ("tools/weavec-cc/CMakeLists.txt", b"x\ny\n"), + ("lib/Frontend/blob.bin", b"\0\1\2\n" * 10), ("docs/pages/long.md", b"line\n" * 100)): (self.root / path).parent.mkdir(parents=True, exist_ok=True) (self.root / path).write_bytes(data) with mock.patch.object(hygiene, "LIBRARY_LINE_LIMIT", 8): result = hygiene.run_checks(self.root) - self.assertEqual(result.library_lines, {"include/weavec/Empty.h": 1, "lib/Analysis/Partial.cpp": 3, - "lib/CMakeLists.txt": 2, "tools/weavec/CMakeLists.txt": 2}) - self.assertEqual(result.binary, ["lib/Analysis/blob.bin"]) + self.assertEqual(result.library_lines, {"include/weavec/Empty.h": 1, "lib/CMakeLists.txt": 2, + "lib/Frontend/Partial.cpp": 3, "tools/weavec-cc/CMakeLists.txt": 2}) + self.assertEqual(result.binary, ["lib/Frontend/blob.bin"]) self.assertEqual(result.count("library-lines"), 0) with mock.patch.object(hygiene, "LIBRARY_LINE_LIMIT", 7): self.assertEqual(self.found("library-lines"), [ @@ -540,7 +431,7 @@ def test_library_total_counts_every_text_file_under_lib_include_and_tools(self): ]) def test_library_total_excludes_the_declarative_table(self): - """The table is data, and its growth is coverage: RFC 0030 gate H2.""" + """The table is data, and its growth is coverage.""" result = hygiene.run_checks(self.root) self.assertNotIn(hygiene.LIBRARY_SPEC, result.library_lines) self.assertIn("lib/Core/LibrarySpec.cpp", result.library_lines) @@ -552,45 +443,43 @@ def test_library_total_excludes_the_declarative_table(self): class MainTest(TreeTest): def test_output_json_and_exit_code(self): - self.write("docs/pages/reference/cli.md", "# CLI\n--checked\n") + self.write("lib/Frontend/Notes.txt", "Corpus notes:\nzlib\n") out = self.root / "reports" / "hygiene.json" with mock.patch.object(hygiene, "LIBRARY_LINE_LIMIT", 10): code, output = self.run_main("--json", str(out)) self.assertEqual(code, 1) lines = output.splitlines() - self.assertEqual(lines[0], "docs/pages/reference/cli.md:2: retired-name: '--checked' at column 1") + self.assertEqual(lines[0], "lib/Frontend/Notes.txt:2: corpus-word: word 'zlib' at column 1") self.assertRegex(lines[1], r"^\{lib,include,tools\}/\*\*: library-lines: " - r"\d+ lines in 10 files, \d+ over the limit of 10$") - self.assertEqual(lines[2:4], ["", "Summary (RFC 0030 and 0031, gate H2; RFC 0032, gate H1)"]) - self.assertRegex(output, r"\n files: +14 from a directory walk \(not a git work tree\); " + r"\d+ lines in 8 files, \d+ over the limit of 10$") + self.assertEqual(lines[2:4], ["", "Summary (RFC 0036, gate H1)"]) + self.assertRegex(output, r"\n files: +12 from a directory walk \(not a git work tree\); " r"skipped 0 binary, 0 unreadable\n") self.assertRegex(output, r"\n LibrarySpec: +5 entries, 3 chk aliases, and their __builtin_ spellings\n") - self.assertRegex(output, r"\n engine lines: +24 / 21,500 \(2 lib/Analysis/Engine\*\.\{h,cpp\} files\)\n") - self.assertRegex(output, r"\n library lines: +\d+ / 10 \(10 code files under lib/, include/ and " + self.assertRegex(output, r"\n entry points: +4 of 4 declared in runtime/weavec_rt\.h\n") + self.assertRegex(output, r"\n library lines: +\d+ / 10 \(8 code files under lib/, include/ and " r"tools/, lib/Core/LibrarySpec\.txt excluded\)\n") - self.assertRegex(output, r"\n retired-name +1 checked-mode or old-engine names") - self.assertRegex(output, r"\n engine-include +0 ") - self.assertRegex(output, r"\n engine-sink +0 ") - self.assertRegex(output, r"\n engine-lines +0 ") - self.assertRegex(output, r"\n library-name-test +0 ") - self.assertRegex(output, r"\n library-lines +1 ") - self.assertRegex(output, r"\n runtime lines: +0 / 4,000 \(0 files under runtime/, runtime/test/ excluded\)\n") - self.assertRegex(output, r"\n runtime-lines +0 ") - self.assertEqual(lines[-1], "H2: FAIL (2 violations)") + self.assertRegex(output, r"\n runtime lines: +\d+ / 6,000 \(2 files under runtime/, runtime/test/ " + r"excluded\)\n") + for check, count in (("library-name-test", 0), ("corpus-word", 1), ("core-include", 0), + ("runtime-include", 0), ("runtime-entry", 0), ("library-lines", 1), + ("runtime-lines", 0)): + self.assertRegex(output, rf"\n {check} +{count} ") + self.assertEqual(lines[-1], "H1: FAIL (2 violations)") document = json.loads(out.read_text()) self.assertFalse(document["passed"]) + self.assertEqual(document["version"], 3) self.assertEqual(document["files"]["source"], "walk") - self.assertEqual(document["checks"]["retired-name"]["violations"], 1) + self.assertEqual(document["checks"]["corpus-word"]["violations"], 1) self.assertEqual(document["violations"][0], { - "check": "retired-name", "path": "docs/pages/reference/cli.md", "line": 2, - "message": "'--checked' at column 1"}) + "check": "corpus-word", "path": "lib/Frontend/Notes.txt", "line": 2, + "message": "word 'zlib' at column 1"}) self.assertIsNone(document["violations"][1]["line"]) - self.assertEqual(document["version"], 2) - self.assertEqual(document["lines"]["engine"], { - "total": 24, "limit": hygiene.ENGINE_LINE_LIMIT, - "files": {"lib/Analysis/Engine.h": 19, "lib/Analysis/EngineRun.cpp": 5}}) + self.assertEqual(document["entryPoints"]["__weavec_rt_guard"], True) self.assertEqual(document["lines"]["library"]["limit"], 10) - self.assertEqual(document["lines"]["library"]["files"], 10) + self.assertEqual(document["lines"]["library"]["files"], 8) + self.assertEqual(sorted(document["lines"]["runtime"]["files"]), + ["runtime/weavec_guard.c", "runtime/weavec_rt.h"]) def test_usage_errors_exit_2(self): with contextlib.redirect_stderr(io.StringIO()) as errors: @@ -610,18 +499,17 @@ def git(self, *args): subprocess.run(["git", *args], cwd=self.root, check=True, capture_output=True) def test_ignored_files_are_not_scanned_and_untracked_ones_are(self): - self.write(".gitignore", "/docs/node_modules/\n/lib/Generated/\n") - self.write("lib/Gone.cpp", "SafetyState gone;\n") + self.write(".gitignore", "/lib/Generated/\n") + self.write("lib/Gone.cpp", "// zlib\n") self.git("init", "-q") self.git("add", "-A") (self.root / "lib/Gone.cpp").unlink() # still in the index - self.write("docs/node_modules/pkg/README.md", "--checked\n") - self.write("lib/Generated/Table.cpp", "SafetyState generated;\n" * 100) - self.write("lib/Analysis/New.cpp", "bool checkContracts;\n") # untracked, not ignored + self.write("lib/Generated/Table.cpp", "// zlib\n" * 100) + self.write("lib/Frontend/New.cpp", "// cJSON\n") # untracked, not ignored result = hygiene.run_checks(self.root) self.assertEqual(result.file_source, "git") - self.assertEqual([(v.path, v.line) for v in result.violations], [("lib/Analysis/New.cpp", 1)]) - self.assertIn("lib/Analysis/New.cpp", result.library_lines) + self.assertEqual([(v.path, v.line) for v in result.violations], [("lib/Frontend/New.cpp", 1)]) + self.assertIn("lib/Frontend/New.cpp", result.library_lines) self.assertNotIn("lib/Generated/Table.cpp", result.library_lines) self.assertNotIn("lib/Gone.cpp", result.library_lines) self.assertEqual(result.unreadable, []) diff --git a/scripts/test_corpus_gate.py b/scripts/test_corpus_gate.py index 0b08427c..ca53879f 100644 --- a/scripts/test_corpus_gate.py +++ b/scripts/test_corpus_gate.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """Tests for scripts/corpus-gate.py (RFC 0035, section 11). -Synthetic data only: fake tools written by the tests stand in for weavec, +Synthetic data only: fake tools written by the tests stand in for weavec-cc and the reference compiler (the "programs" they build are shell scripts), and a local git repository stands in for a corpus project, so the tests need neither the network, a compiler nor a WeaveC build. The checks @@ -14,7 +14,6 @@ import contextlib import difflib -import fnmatch import importlib.util import io import json @@ -41,10 +40,6 @@ def proc(returncode=0, stdout="", stderr="", timed_out=False, error=""): return gate.ProcResult("cmd", returncode, stdout, stderr, 0.1, timed_out=timed_out, error=error) -def diag(file="a.c", line=1, id_="use-after-free", severity="error", message="use of 'p' after it was freed"): - return gate.Diagnostic(file=file, line=line, col=1, severity=severity, id=id_, message=message) - - def ledger_document(*summaries, version=3, rows=()): return {"schema": "weavec-ledger", "version": version, "producer": {"name": "weavec-cc"}, "units": [{"source": "a.c", "object": "a.o", "summary": s, "rows": list(rows)} for s in summaries]} @@ -65,49 +60,6 @@ def report(kind="heap-use-after-free", file="a.c", line=3, col=5, proven=False): class ParsingTest(unittest.TestCase): - def test_diagnostics_are_relative_to_the_checkout(self): - with tempfile.TemporaryDirectory() as directory: - root = Path(directory) - (root / "src").mkdir() - text = "\n".join([ - f"{root}/src/a.c:3:5: error: 'p' is freed twice [weavec::double-free]", - "src/b.c:7:1: warning: 'q' is leaked [weavec::leak]", - f"{root}/src/a.c:9:1: error: unknown type name 'foo'", - " 3 | free(p);", - "In file included from x.h:1:", - ]) - diagnostics, clang_errors = gate.parse_diagnostics(text, root) - self.assertEqual([(d.file, d.line, d.id, d.severity) for d in diagnostics], - [("src/a.c", 3, "double-free", "error"), ("src/b.c", 7, "leak", "warning")]) - self.assertEqual(clang_errors, 1) - self.assertEqual(diagnostics[0].render(), "src/a.c:3:5: error: 'p' is freed twice [weavec::double-free]") - - def test_summary_lines(self): - text = "\n".join([ - "weavec: cJSON.c: 1,221 sites: 858 proven, 361 not proven, 0 violations, 2 trusted; 0 errors, 4 warnings", - "weavec: b.c: 1 site: 0 proven, 0 not proven, 1 violation, 0 trusted; 1 error, 1 warning; " - "2 functions over budget (f, g)", - "weavec: program program: 1,745 sites in 2 units: 1,109 proven, 634 not proven, 0 violations, " - "2 trusted; 0 errors, 10 warnings", - "weavec: a.c: 812 accesses: 431 proven, 381 guarded, 0 unguarded", - ]) - units, program = gate.parse_summaries(text) - self.assertEqual([u["source"] for u in units], ["cJSON.c", "b.c"]) - self.assertEqual(units[0]["sites"], 1221) - self.assertEqual((units[1]["violations"], units[1]["errors"], units[1]["overBudget"]), (1, 1, 2)) - self.assertEqual((program["sites"], program["proven"], program["warnings"]), (1745, 1109, 10)) - - def test_failures_never_look_clean(self): - ok = gate.classify_failure - self.assertEqual(ok(proc(), [], 0, 1), "") - self.assertEqual(ok(proc(1), [diag()], 0, 1), "") # a definite error is a result - self.assertIn("without a WeaveC error", ok(proc(1), [], 0, 1)) - self.assertIn("SIGSEGV", ok(proc(-11), [], 0, 1)) - self.assertIn("Clang error", ok(proc(1), [], 2, 1)) - self.assertIn("timeout", ok(proc(timed_out=True), [], 0, 1)) - self.assertEqual(ok(proc(error="No such file"), [], 0, 1), "No such file") - self.assertIn("no summary line", ok(proc(), [], 0, 0)) - def test_reports(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) @@ -209,15 +161,11 @@ def test_quick_measurement(self): quick.add_compile(gate.CompileRun("a.c", {"accesses": 8, "proven": 2, "guarded": 6, "unguarded": 0}, gate.collections.Counter({"proven:in-bounds": 2}), 0.5, 2 ** 20, 0.4)) quick.add_compile(gate.CompileRun("b.c", {}, gate.collections.Counter(), 0.1, None, failure="exit status 1")) - quick.add_analysis(gate.AnalysisRun(["a.c"], {"sites": 5, "errors": 1, "warnings": 2}, [diag()], 0.2)) - quick.add_analysis(gate.AnalysisRun(["b.c"], {"sites": 9}, [], 0.2, failure="timeout")) - measured = quick.measured() - self.assertEqual(measured["ledger"], {"accesses": 8, "proven": 2, "guarded": 6, "unguarded": 0, - "provenShare": 0.25}) - self.assertEqual((measured["analysis"]["sites"], measured["analysis"]["errors"]), (5, 1)) - self.assertEqual(len(quick.failures), 2) + self.assertEqual(quick.measured(), {"ledger": {"accesses": 8, "proven": 2, "guarded": 6, "unguarded": 0, + "provenShare": 0.25}}) + self.assertEqual(quick.failures, ["b.c: exit status 1"]) self.assertEqual(quick.units["a.c"]["referenceCpuSeconds"], 0.4) - self.assertEqual(len(quick.to_json()["diagnostics"]), 1) + self.assertEqual(quick.to_json()["reasons"], {"proven:in-bounds": 2}) self.assertIsNone(gate.Quick(config="empty").proven_share) @@ -239,8 +187,8 @@ def load(self, data): def test_repository_manifest(self): manifest = gate.load_manifest(CORPUS / "manifest.json", CORPUS / "support") names = {c.name for c in manifest.configs} - for name in ("sds", "cJSON", "cJSON-program", "jsmn", "log.c", "printf", "linenoise", - "linenoise-program", "zlib", "lua", "jansson"): + for name in ("sds", "cJSON", "jsmn", "log.c", "printf", "linenoise", "linenoise-program", "zlib", "lua", + "jansson"): self.assertIn(name, names) self.assertEqual(manifest.config(name).set, "original") self.assertTrue(set(c.set for c in manifest.configs) <= set(gate.SETS)) @@ -281,6 +229,7 @@ def test_invalid_manifests(self): cases = [ ({"name": "a", "compile": {"files": ["a.c"]}, "lowered": []}, "unknown field"), ({"name": "a", "compile": {"files": ["a.c"]}, "link": {"args": []}}, "unknown field"), + ({"name": "a", "compile": {"files": ["a.c"]}, "wholeProgram": True}, "unknown field"), ({"name": "a", "compile": {}}, "compile.files is missing"), ({"name": "a", "compile": {"files": ["a.c"]}, "set": "fresh35"}, "needs heldOut true"), ({"name": "a", "compile": {"files": ["a.c"]}, "heldOut": True, "set": "fresh36"}, "set must be one of"), @@ -300,7 +249,8 @@ def test_invalid_manifests(self): self.load(bad_sha) def test_arguments(self): - for argv, message in ((["--weavec", "x"], "choose a mode"), + for argv, message in ((["--weavec-cc", "x"], "choose a mode"), + (["--quick", "--weavec", "x"], "unrecognized"), (["--quick", "--reference-only"], "no --quick"), (["--full", "--reference-only", "--update"], "not recorded"), (["--bench", "--update"], "--update records"), @@ -317,11 +267,9 @@ def test_arguments(self): # -- the ratchet ----------------------------------------------------------------------- -def measured(proven=50, accesses=100, unguarded=0, errors=0, warnings=2, sites=40): +def measured(proven=50, accesses=100, unguarded=0): return {"ledger": {"accesses": accesses, "proven": proven, "guarded": accesses - proven - unguarded, - "unguarded": unguarded, "provenShare": gate.share(proven, accesses)}, - "analysis": {"sites": sites, "proven": 30, "notProven": 10, "violations": 0, "trusted": 0, - "errors": errors, "warnings": warnings, "overBudget": 0}} + "unguarded": unguarded, "provenShare": gate.share(proven, accesses)}} class RatchetTest(unittest.TestCase): @@ -337,18 +285,16 @@ def test_equal_passes(self): def test_worse_fails(self): expected = self.expected(a=measured()) - for now, field in ((measured(proven=49), "provenShare"), (measured(unguarded=1), "unguarded"), - (measured(errors=1), "errors"), (measured(warnings=3), "warnings")): + for now, field in ((measured(proven=49), "provenShare"), (measured(unguarded=1), "unguarded")): result = gate.compare_ratchet({"a": now}, expected, self.platform) self.assertTrue(result.failed, field) - self.assertIn(f"a.{'analysis' if field in ('errors', 'warnings') else 'ledger'}.{field}", - result.regressions[0]) + self.assertIn(f"a.ledger.{field}", result.regressions[0]) def test_better_and_changed_counts_are_notes(self): expected = self.expected(a=measured()) - result = gate.compare_ratchet({"a": measured(proven=60, warnings=1)}, expected, self.platform) + result = gate.compare_ratchet({"a": measured(proven=60)}, expected, self.platform) self.assertFalse(result.failed) - self.assertEqual(len(result.improvements), 2) + self.assertEqual(result.improvements, ["a.ledger.provenShare: 0.5 -> 0.6 (better)"]) self.assertIn("a.ledger.proven: 50 -> 60", result.changes) result = gate.compare_ratchet({"a": measured(proven=100, accesses=200)}, expected, self.platform) self.assertFalse(result.failed) # the same share of more accesses @@ -388,57 +334,32 @@ def test_repository_expected(self): for platform, section in expected["platforms"].items(): for name, record in section["configs"].items(): self.assertIn(name, names, f"{platform}: {name}") - self.assertEqual(set(record), {"ledger", "analysis"}) + self.assertEqual(set(record), {"ledger"}) self.assertEqual(set(record["ledger"]), set(gate.LEDGER_COUNTS) | {"provenShare"}) - self.assertEqual(set(record["analysis"]), set(gate.ANALYSIS_COUNTS)) # -- triage ---------------------------------------------------------------------------- -def entry(config="c", file="a.c", line=1, id_="use-after-free", certainty="definite", verdict="true"): - return {"config": config, "id": id_, "certainty": certainty, "file": file, "line": line, - "verdict": verdict, "note": "n"} - - class TriageTest(unittest.TestCase): - def test_findings(self): - findings = gate.findings_of("c", [diag(), diag(), diag(line=2, severity="warning", id_="leak")]) - self.assertEqual([(f["certainty"], f["line"]) for f in findings], [("definite", 1), ("possible", 2)]) - - def test_definite_errors_need_a_true_verdict(self): - findings = gate.findings_of("c", [diag(line=1), diag(line=2), diag(line=3), diag(line=4, severity="warning")]) - result = gate.check_triage(findings, [entry(line=1), entry(line=2, verdict="false"), entry(line=9), - entry(config="other", line=9)], {"c"}) - self.assertEqual([f["line"] for f in result.untriaged], [3]) - self.assertEqual([f["line"] for f in result.false_errors], [2]) - self.assertEqual((len(result.definite), result.possible), (3, 1)) - self.assertEqual([e["line"] for e in result.stale], [9]) # the other config did not run - self.assertTrue(result.failed) - ok = gate.check_triage(gate.findings_of("c", [diag(file="./a.c")]), [entry()], {"c"}) - self.assertFalse(ok.failed) - def test_load_triage(self): with tempfile.TemporaryDirectory() as directory: path = Path(directory) / "triage.json" - self.assertEqual(gate.load_triage(path).entries, []) + self.assertEqual(gate.load_triage(path).guard_failures, []) - def write(entries=(), failures=(), version=2): + def write(failures=(), version=2, **extra): path.write_text(json.dumps({"schema": gate.TRIAGE_SCHEMA, "version": version, - "entries": list(entries), "guardFailures": list(failures)})) + "guardFailures": list(failures), **extra})) - write([entry()], [{"config": "c", "file": "a.c", "line": 1, "verdict": "true", "note": "n", - "kind": "heap-buffer-overflow"}]) + good = {"config": "c", "file": "a.c", "line": 1, "verdict": "true", "note": "n"} + write([{**good, "kind": "heap-buffer-overflow"}]) self.assertEqual(len(gate.load_triage(path).guard_failures), 1) - for entries, failures, message in ( - ([{**entry(), "verdict": "maybe"}], [], "verdict must be"), - ([{**entry(), "certainty": "likely"}], [], "certainty must be"), - ([{k: v for k, v in entry().items() if k != "note"}], [], "missing note"), - ([], [{"config": "c", "file": "a.c", "line": 1, "verdict": "false", "note": "n"}], - "must be \"true\""), - ([], [{"config": "c", "file": "a.c", "line": 1, "verdict": "true", "note": "n", "kind": "object"}], - "unknown kind")): - write(entries, failures) + for failures, extra, message in ( + ([{**good, "verdict": "false"}], {}, "must be \"true\""), + ([{k: v for k, v in good.items() if k != "note"}], {}, "missing note"), + ([{**good, "kind": "object"}], {}, "unknown kind"), + ([good], {"entries": []}, "unknown field\\(s\\) entries")): + write(failures, **extra) with self.assertRaisesRegex(gate.GateError, message): gate.load_triage(path) write(version=1) @@ -460,15 +381,11 @@ def test_repository_triage(self): triage = gate.load_triage(CORPUS / "triage.json") manifest = gate.load_manifest(CORPUS / "manifest.json", CORPUS / "support") names = {c.name for c in manifest.configs} - keys = set() - for e in triage.entries + triage.guard_failures: + # Empty since RFC 0036: jansson's word-at-a-time hash, the last entry, + # takes its exact path when it sees AddressSanitizer's macros. + for e in triage.guard_failures: self.assertIn(e["config"], names) - for e in triage.entries: - key = (e["config"], e["file"], e["line"], e["id"]) - self.assertNotIn(key, keys, key) - keys.add(key) - self.assertTrue(any(fnmatch.fnmatch(e["file"], p) for p in manifest.config(e["config"]).files), - f"{key}: not a file the config analyses") + self.assertTrue(manifest.config(e["config"]).build, e) # -- injections ------------------------------------------------------------------------ @@ -648,43 +565,13 @@ def test_verify(self): # -- end to end, with fake tools ------------------------------------------------------------ -FAKE_WEAVEC = r'''#!PYTHON -"""A stand-in for weavec: a diagnostic per BUG line, a summary line per file.""" -import os, sys -args = sys.argv[1:] -if args == ["--version"]: - print("weavec version fake"); sys.exit(0) -files = [a for a in args[:args.index("--")] if a.endswith(".c")] -status = 0 -totals = [0, 0, 0] -for f in files: - errors = warnings = 0 - lines = open(f).read().splitlines() - for number, line in enumerate(lines, 1): - if "BUG!" in line: - print(f"{os.path.abspath(f)}:{number}:1: error: 'p' is freed twice [weavec::double-free]", file=sys.stderr) - errors += 1 - elif "BUG" in line: - print(f"{os.path.abspath(f)}:{number}:1: warning: 'p' may be freed twice [weavec::double-free]", - file=sys.stderr) - warnings += 1 - sites = len(lines) + int(os.environ.get("FAKE_SITES", "0")) - print(f"weavec: {os.path.basename(f)}: {sites:,} sites: {sites} proven, 0 not proven, 0 violations, " - f"0 trusted; {errors} errors, {warnings} warnings", file=sys.stderr) - totals = [totals[0] + sites, totals[1] + errors, totals[2] + warnings] - status = status or (1 if errors else 0) -if "--whole-program" in args: - print(f"weavec: program program: {totals[0]} sites in {len(files)} units: {totals[0]} proven, 0 not proven, " - f"0 violations, 0 trusted; {totals[1]} errors, {totals[2]} warnings", file=sys.stderr) -sys.exit(status) -''' - FAKE_CC = r'''#!PYTHON """A stand-in for weavec-cc (and, named fake-clang, for the reference compiler). -c writes an "object" that lists its source and, for weavec-cc with -fweavec-ledger=, a version 3 ledger: one access per line, FAKE_PROVEN of -them proven. Linking writes a shell script: the program. A source line +them proven. Linking writes a shell script: the program. A source that +contains FAKE_FAIL fails to compile with weavec-cc. A source line `REPORT kind` makes it report that guard at that line when FAKE_TRAP is set at run time, `REPORT! kind` always; `PROVEN kind` reports a weavec.proven monitor in verify mode when FAKE_MONITOR is set. In trap and verify mode a @@ -704,6 +591,9 @@ def test_verify(self): sources += open(obj).read().split() sources = [os.path.abspath(s) if os.path.exists(s) else s for s in sources] if "-c" in args: + fail = os.environ.get("FAKE_FAIL") + if fail and not reference and any(fail in open(s).read() for s in sources): + print(f"{os.path.basename(sources[0])}: error: injected failure", file=sys.stderr); sys.exit(1) with open(out, "w") as f: f.write("\n".join(sources)) if ledger and not reference: @@ -756,25 +646,23 @@ def setUp(self): self.root = Path(self.directory.name) self.upstream = self.root / "upstream" self.upstream.mkdir() - (self.upstream / "a.c").write_text("int helper(int i) {\n return i; /* BUG! */\n}\n") - (self.upstream / "b.c").write_text("int other(void) { return 1; }\n/* BUG */\n") + (self.upstream / "a.c").write_text("int helper(int i) {\n return i;\n}\n") + (self.upstream / "b.c").write_text("int other(void) { return 1; }\nint another(void) { return 2; }\n") (self.upstream / "prog.c").write_text(PROGRAM) self.sha = self.commit() bin_dir = self.root / "bin" bin_dir.mkdir() - self.weavec = bin_dir / "fake-weavec" - self.weavec.write_text(FAKE_WEAVEC.replace("#!PYTHON", f"#!{sys.executable}")) self.weavec_cc = bin_dir / "fake-weavec-cc" self.weavec_cc.write_text(FAKE_CC.replace("#!PYTHON", f"#!{sys.executable}")) self.clang = bin_dir / "fake-clang" self.clang.write_text(FAKE_CC.replace("#!PYTHON", f"#!{sys.executable}")) - for tool in (self.weavec, self.weavec_cc, self.clang): + for tool in (self.weavec_cc, self.clang): tool.chmod(0o755) self.manifest_data = manifest_data( {"name": "units", "compile": {"files": ["a.c", "b.c"], "args": ["-I."]}, "build": ['"$CC" -c a.c -o a.o', '"$CC" prog.c a.o -o prog'], "test": ["./prog"], "bench": {"name": "b", "build": ['"$CC" b.c -o benchprog'], "command": "./benchprog", "repeat": 2}}, - {"name": "whole", "compile": {"files": ["*.c"]}, "wholeProgram": True}, + {"name": "glob", "compile": {"files": ["*.c"]}}, {"name": "held", "heldOut": True, "set": "fresh35", "compile": {"files": ["b.c"]}}, gates={"buildTime": {"maxBuildRatio": 1000}}) self.manifest_data["projects"][0].update(url=str(self.upstream), sha=self.sha) @@ -785,7 +673,7 @@ def setUp(self): self.injections = self.root / "injections" / "injections.json" self.injections.parent.mkdir() self.write_injections([]) - self.write_triage([entry(config=name, file="a.c", line=2, id_="double-free") for name in ("units", "whole")]) + self.write_triage([]) def commit(self) -> str: env = {**os.environ, "GIT_AUTHOR_NAME": "t", "GIT_AUTHOR_EMAIL": "t@example.com", @@ -795,8 +683,8 @@ def commit(self) -> str: return subprocess.run(["git", "rev-parse", "HEAD"], cwd=self.upstream, check=True, capture_output=True, text=True).stdout.strip() - def write_triage(self, entries, failures=()): - self.triage.write_text(json.dumps({"schema": gate.TRIAGE_SCHEMA, "version": 2, "entries": entries, + def write_triage(self, failures): + self.triage.write_text(json.dumps({"schema": gate.TRIAGE_SCHEMA, "version": 2, "guardFailures": list(failures)})) def write_injections(self, items): @@ -816,7 +704,7 @@ def run_gate(self, *extra, env=None): argv = ["--manifest", str(self.manifest), "--expected", str(self.expected), "--triage", str(self.triage), "--injections", str(self.injections), "--workdir", str(self.root / "work"), "--support-dir", str(self.root), "--bench-dir", str(self.root), "--jobs", "2", - "--weavec", str(self.weavec), "--weavec-cc", str(self.weavec_cc), "--cc", str(self.clang), + "--weavec-cc", str(self.weavec_cc), "--cc", str(self.clang), "--json", str(self.root / "results.json"), *extra] saved = {k: os.environ.get(k) for k in (env or {})} os.environ.update(env or {}) @@ -838,18 +726,19 @@ def results(self): def recorded(self): return json.loads(self.expected.read_text())["platforms"][gate.platform_key()]["configs"] - def test_quick_ratchet_and_triage(self): + def test_quick_ratchet(self): status, output = self.run_gate("--quick", "--update") self.assertEqual(status, 0, output) self.assertNotIn("held", self.recorded()) # --quick leaves the held-out configs out self.assertEqual(self.recorded()["units"]["ledger"], {"accesses": 5, "proven": 2, "guarded": 3, "unguarded": 0, "provenShare": 0.4}) - self.assertEqual(self.recorded()["units"]["analysis"]["errors"], 1) - self.assertEqual(self.recorded()["whole"]["analysis"]["warnings"], 1) + self.assertEqual(set(self.recorded()["units"]), {"ledger"}) status, output = self.run_gate("--quick") self.assertEqual(status, 0, output) - self.assertEqual(self.results()["gates"]["triage"]["status"], "pass") - self.assertEqual(self.results()["configs"]["whole"]["quick"]["analysis"]["sites"], 11) + self.assertEqual(self.results()["gates"], {}) + quick = self.results()["configs"]["glob"]["quick"] + self.assertEqual(quick["ledger"]["accesses"], 11) + self.assertEqual(sorted(quick["units"]), ["a.c", "b.c", "prog.c"]) # More proven accesses: a note, not a failure. status, output = self.run_gate("--quick", env={"FAKE_PROVEN": "3"}) self.assertEqual(status, 0, output) @@ -862,28 +751,24 @@ def test_quick_ratchet_and_triage(self): status, output = self.run_gate("--update-from", str(self.root / "results.json")) self.assertEqual(status, 0, output) self.assertEqual(self.recorded()["units"]["ledger"]["proven"], 0) - # A definite error needs a verdict, and not a false one. - self.write_triage([entry(config="units", file="a.c", line=2, id_="double-free")]) - status, output = self.run_gate("--quick", env={"FAKE_PROVEN": "0"}) - self.assertEqual(status, 1, output) - self.assertIn("untriaged definite double-free in whole at a.c:2", output) - self.write_triage([entry(config=name, file="a.c", line=2, id_="double-free", verdict="false") - for name in ("units", "whole")]) - status, output = self.run_gate("--quick", env={"FAKE_PROVEN": "0"}) - self.assertEqual(status, 1, output) - self.assertIn("definite double-free triaged false in units at a.c:2", output) # The held-out configs run when asked for. - self.write_triage([entry(config=name, file="a.c", line=2, id_="double-free") for name in ("units", "whole")]) status, output = self.run_gate("--quick", "--held-out", env={"FAKE_PROVEN": "0"}) self.assertEqual(status, 0, output) self.assertIn("held: not recorded", output) - self.assertEqual(self.results()["sets"], {"original": ["units", "whole"], "fresh35": ["held"]}) + self.assertEqual(self.results()["sets"], {"original": ["glob", "units"], "fresh35": ["held"]}) def test_a_tool_failure_is_not_recorded(self): - (self.root / "bin" / "fake-weavec").write_text(f"#!{sys.executable}\nimport sys; sys.exit(3)\n") + (self.root / "bin" / "fake-weavec-cc").write_text(f"#!{sys.executable}\nimport sys; sys.exit(3)\n") status, output = self.run_gate("--quick", "--update") self.assertEqual(status, 2, output) # the probe sees it first - self.assertIn("prints no summary line", output) + self.assertIn("does not write a weavec-ledger version 3 ledger", output) + self.assertFalse(self.expected.exists()) + + def test_a_failed_compile_is_not_recorded(self): + status, output = self.run_gate("--quick", "--update", env={"FAKE_FAIL": "another"}) + self.assertEqual(status, 1, output) + self.assertIn("units: b.c: weavec-cc -c: exit status 1: b.c: error: injected failure", output) + self.assertIn("nothing to record in expected.json", output) self.assertFalse(self.expected.exists()) def test_a_modified_checkout_is_refused(self): @@ -917,8 +802,7 @@ def test_full(self): self.assertEqual(self.results()["gates"]["drop-in"]["status"], "fail") self.assertEqual(self.results()["configs"]["units"]["traps"], 1) # A true bug of the project, triaged: not a trap. - self.write_triage([entry(config=name, file="a.c", line=2, id_="double-free") for name in ("units", "whole")], - [{"config": "units", "file": "prog.c", "line": 3, "verdict": "true", "note": "n"}]) + self.write_triage([{"config": "units", "file": "prog.c", "line": 3, "verdict": "true", "note": "n"}]) status, output = self.run_gate("--full", "--only", "units", "--no-asan", env={"FAKE_TRAP": "1"}) self.assertEqual(status, 0, output) self.assertIn("trapped only at guard failures triaged as true bugs", output) @@ -935,10 +819,9 @@ def test_verify(self): self.assertEqual(self.results()["gates"]["verify"]["status"], "fail") def test_injections(self): - self.write_patch("stops.patch", "a.c", " return i; /* BUG! */\n", - " return i; /* BUG! */\n /* REPORT! heap-buffer-overflow INJECTED */\n") - self.write_patch("elsewhere.patch", "a.c", " return i; /* BUG! */\n", - " return i; /* BUG! */\n /* INJECTED */\n") + self.write_patch("stops.patch", "a.c", " return i;\n", + " return i;\n /* REPORT! heap-buffer-overflow INJECTED */\n") + self.write_patch("elsewhere.patch", "a.c", " return i;\n", " return i;\n /* INJECTED */\n") self.write_injections([ {"id": "stops", "config": "units", "patch": "stops.patch", "file": "a.c", "line": 3}, {"id": "wrong-kind", "config": "units", "patch": "stops.patch", "file": "a.c", "line": 3, diff --git a/scripts/test_run_cases.py b/scripts/test_run_cases.py index 1b378a8d..d913b106 100755 --- a/scripts/test_run_cases.py +++ b/scripts/test_run_cases.py @@ -1,10 +1,10 @@ #!/usr/bin/env python3 -"""Tests for the test/cases runner (scripts/run-cases.py, RFC 0035 section 11). +"""Tests for the test/cases runner (scripts/run-cases.py, RFC 0036 section 9). -Unit level: the marker grammar, case loading and discovery, flag translation, -output parsing and the judging of synthetic evidence. The steps that start -processes (analyse, build, run_all, run_asan) are replaced by fakes, so no -compiler is needed.""" +Unit level: the marker grammar, case loading and discovery, flags, output +parsing and the judging of synthetic evidence. The steps that start +processes (build, run_all, run_asan) are replaced by fakes, so no compiler +is needed.""" import contextlib import importlib.util import io @@ -58,10 +58,6 @@ def run(code=0, *reports, args=(), timed_out=False): return rc.Run(tuple(args), code, timed_out, list(reports)) -def diag(file, line, ident, severity="error"): - return rc.Diagnostic(str(file), line, 3, severity, ident, "message") - - # --------------------------------------------------------------------------- # Marker grammar # --------------------------------------------------------------------------- @@ -70,10 +66,10 @@ def diag(file, line, ident, severity="error"): class ScanTest(unittest.TestCase): def test_comments_in_literals_and_block_comments_are_skipped(self): scan = rc.scan_source(textwrap.dedent("""\ - const char *s = "// BUG: leak"; /* // BUG: leak */ + const char *s = "// TRAP"; /* // TRAP */ char c = '/'; char q = '"'; // tail /* a block - // BUG: leak + // TRAP */ int x; """)) self.assertEqual([(c.line, c.body.strip(), c.code_before) for c in scan.comments], @@ -109,34 +105,28 @@ def test_malformed_segments(self): for text, expected in (("CLEAN please", "takes no argument"), ("CLEAN.", "takes no argument"), ("STOP: here", "takes no argument"), - ("BUG leak", "needs ':'"), + ("MISS why", "needs ':'"), ("MISS", "needs ':'"), - ("BUG:", "needs an argument"), - ("BUGS: leak", "did you mean 'BUG'"), + ("MISS:", "needs an argument"), + ("TRAPS: x", "did you mean 'TRAP'"), ("UNIT: a.c", "did you mean 'UNITS'")): with self.subTest(text=text): with self.assertRaisesRegex(ValueError, expected): rc.parse_segment(text) def test_split_segments(self): - self.assertEqual(rc.split_segments(" BUG: leak // MISS: why "), ["BUG: leak", "MISS: why"]) + self.assertEqual(rc.split_segments(" STOP // MISS: why "), ["STOP", "MISS: why"]) + + def test_the_analysis_markers_are_gone(self): + for text in ("BUG: leak", "ALLOW: leak", "TOOL", "NEUTRALISED: zero-init"): + with self.subTest(text=text): + self.assertIsNone(rc.parse_segment(text)) class ArgumentTest(unittest.TestCase): def parse(self, kind, argument, directory=Path(".")): return rc.parse_argument(kind, argument, directory) - def test_bug(self): - self.assertEqual(self.parse("BUG", "leak"), ("leak", None)) - self.assertEqual(self.parse("BUG", "use-after-free definite"), ("use-after-free", "definite")) - self.assertEqual(self.parse("BUG", "out-of-bounds possible"), ("out-of-bounds", "possible")) - with self.assertRaisesRegex(ValueError, "definite|possible"): - self.parse("BUG", "leak maybe") - with self.assertRaisesRegex(ValueError, "unknown diagnostic id"): - self.parse("BUG", "no-such-id") - with self.assertRaisesRegex(ValueError, "unknown diagnostic id"): - self.parse("BUG", "unresolved-operation") # deleted with the require levels - def test_trap_kinds(self): self.assertIsNone(self.parse("TRAP", "")) for kind in rc.KINDS: @@ -155,14 +145,8 @@ def test_ledger_reasons(self): with self.subTest(kind=kind), self.assertRaisesRegex(ValueError, "unknown"): self.parse(kind, reason) - def test_neutralised_miss_allow(self): - self.assertEqual(self.parse("NEUTRALISED", "zero-init"), "zero-init") - with self.assertRaisesRegex(ValueError, "zero-init"): - self.parse("NEUTRALISED", "zero") + def test_miss(self): self.assertEqual(self.parse("MISS", "free text, any words"), "free text, any words") - self.assertEqual(self.parse("ALLOW", "leak double-free"), ("leak", "double-free")) - with self.assertRaisesRegex(ValueError, "unknown diagnostic id 'nope'"): - self.parse("ALLOW", "leak nope") def test_run_input(self): with tempfile.TemporaryDirectory() as directory: @@ -209,35 +193,31 @@ def test_line_and_file_markers(self): // FLAGS: -O2 // ASAN #include int f(int *p) { - return p[4]; // BUG: out-of-bounds // MISS: the index is opaque + return p[4]; // STOP // MISS: the index is opaque } int g(int *p) { return *p; } // TRAP // GUARDED: access """) self.assertEqual(got.errors, []) self.assertEqual([(m.kind, m.line) for m in got.file_markers], [("FLAGS", 2), ("ASAN", 2)]) self.assertEqual([(m.kind, m.line, m.value) for m in got.line_markers], - [("BUG", 5, ("out-of-bounds", None)), ("MISS", 5, "the index is opaque"), + [("STOP", 5, ""), ("MISS", 5, "the index is opaque"), ("TRAP", 7, None), ("GUARDED", 7, "access")]) - def test_neutralised_on_a_bug_line(self): - got = markers("int f(void) { int x; return x; } // BUG: use-of-uninitialized // NEUTRALISED: zero-init\n") - self.assertEqual([m.kind for m in got.line_markers], ["BUG", "NEUTRALISED"]) - def test_placement_errors(self): got = markers(""" - // BUG: leak + // TRAP int x; // CLEAN // ASAN int y; """) self.assertEqual(len(got.errors), 3, got.errors) - self.assertIn("x.c:1: line marker BUG is on a line without code", got.errors[0]) + self.assertIn("x.c:1: line marker TRAP is on a line without code", got.errors[0]) self.assertIn("x.c:2: file marker CLEAN must be on a comment line", got.errors[1]) self.assertIn("x.c:3: file marker ASAN must appear before the first declaration", got.errors[2]) def test_one_bad_segment_keeps_the_others(self): - got = markers("int x; // BUG: leak // TRAP: bounds\n") - self.assertEqual([m.kind for m in got.line_markers], ["BUG"]) + got = markers("int x; // GUARDED // TRAP: bounds\n") + self.assertEqual([m.kind for m in got.line_markers], ["GUARDED"]) self.assertEqual(len(got.errors), 1) self.assertIn("unknown report kind 'bounds'", got.errors[0]) @@ -261,8 +241,8 @@ def test_case_fields(self): // XFAIL: two #include int main(int argc, char **argv) { - int a[2]; a[argc] = 0; // BUG: out-of-bounds // TRAP: stack-buffer-overflow - return 0; // BUG: leak possible + int a[2]; a[argc] = 0; // TRAP: stack-buffer-overflow + return 0; // GUARDED } """) self.assertEqual(case.errors, []) @@ -271,10 +251,8 @@ def test_case_fields(self): self.assertEqual([r.args for r in case.run_inputs], [("1",), ("2",)]) self.assertEqual(case.run_inputs[1].stdin, self.cases / "s" / "in.txt") self.assertTrue(case.asan and case.has_main and case.is_bug_case) - self.assertFalse(case.clean or case.tool) + self.assertFalse(case.clean) self.assertEqual([e.pointer for e in case.expectations], ["/summary/unguarded"]) - self.assertEqual([(b.line, b.value) for b in case.bugs], - [(11, ("out-of-bounds", None)), (12, ("leak", "possible"))]) self.assertEqual([(t.line, t.value) for t in case.traps], [(11, "stack-buffer-overflow")]) self.assertEqual(case.xfail, "one; two") self.assertIsNone(case.detect) @@ -309,42 +287,35 @@ def test_units(self): self.assertEqual(case.unit_flags, {unit: ("-DU",), plain: ("-fno-weavec",)}) self.assertEqual(case.flags, ("-O1",)) self.assertEqual([(t.file, t.line) for t in case.traps], [(unit, 2)]) - self.assertEqual(case.analysed_units(), [self.cases / "s" / "a.c", unit]) def test_unit_errors(self): self.write("s/Inputs/bad.c", "// CLEAN\nint b;\n") errors = self.errors(""" // UNITS: Inputs/bad.c Inputs/missing.c Inputs/bad.c // CLEAN - int x; + int main(void) { return 0; } """) self.assertEqual(len(errors), 3, errors) self.assertIn("UNITS file 'Inputs/missing.c' does not exist", errors[0]) self.assertIn("UNITS file 'Inputs/bad.c' is listed twice", errors[1]) self.assertIn("bad.c:1: file marker CLEAN is only allowed in the case's main file", errors[2]) self.assertIn("cannot contain -fno-weavec", - self.errors("// FLAGS: -fno-weavec\n// CLEAN\nint x;\n")[0]) + self.errors("// FLAGS: -fno-weavec\n// CLEAN\nint main(void) { return 0; }\n")[0]) def test_consistency_errors(self): for text, expected in ( - ("int x;\n", "no expectation"), - ("// CLEAN\nint x; // BUG: leak\n", "CLEAN contradicts"), - ("// CLEAN\nint x; // TRAP\n", "CLEAN contradicts"), - ("// ALLOW: leak\nint x; // BUG: leak\n", "ALLOW only applies to a CLEAN case"), - ("// TOOL\n// RUN-INPUT: 1\n// CLEAN\nint main(void) { return 0; }\n", "TOOL case is not run"), - ("// ASAN\nint x; // BUG: leak\n", "ASAN needs a unit that defines main"), - ("// RUN-INPUT: 1\n// CLEAN\nint x;\n", "RUN-INPUT needs a unit that defines main"), - ("int x; // STOP\n", "STOP needs a DETECT file marker")): + ("int main(void) { return 0; }\n", "no expectation"), + ("// CLEAN\nint main(void) { return 0; } // TRAP\n", "CLEAN contradicts"), + ("// ASAN\nint x; // GUARDED\n", "ASAN needs a unit that defines main"), + ("// RUN-INPUT: 1\nint x; // GUARDED\n", "RUN-INPUT needs a unit that defines main"), + ("int x; // TRAP\n", "a case without main has nothing to run"), + ("int main(void) { return 0; } // STOP\n", "STOP needs a DETECT file marker"), + ("int main(void) { return 0; } // TRAP // MISS: why\n", "MISS marks a STOP line")): with self.subTest(text=text): errors = self.errors(text) self.assertEqual(len(errors), 1, errors) self.assertIn(expected, errors[0]) - def test_tool_case_needs_no_main(self): - case = self.case("s/t.c", "// TOOL\nvoid f(int *p) { p[9] = 0; } // BUG: out-of-bounds\n") - self.assertEqual(case.errors, []) - self.assertTrue(case.tool) - def test_detection_case(self): case = self.case("d/p.c", """ // DETECT: -DFIX @@ -365,14 +336,14 @@ def test_detection_errors(self): // CLEAN int x; // MISS: why """) - self.assertEqual(len(errors), 6, errors) + self.assertEqual(len(errors), 5, errors) for expected in ("DETECT is given more than once", "a DETECT case has no CLEAN", "DETECT needs a unit that defines main", "needs a STOP line", - "MISS marks a STOP line", "CLEAN contradicts"): + "MISS marks a STOP line"): self.assertTrue(any(expected in e for e in errors), (expected, errors)) - errors = self.errors("// DETECT: -DFIX\nint main(void) { return 0; } // STOP // BUG: leak // TRAP\n") + errors = self.errors("// DETECT: -DFIX\nint main(void) { return 0; } // STOP // GUARDED // TRAP\n") self.assertEqual(len(errors), 1, errors) - self.assertIn("has no BUG, TRAP markers", errors[0]) + self.assertIn("has no GUARDED, TRAP markers", errors[0]) def test_defines_main(self): for text, expected in (("int main(void) {", True), ("static int main(int c, char **v)\n{", True), @@ -388,8 +359,8 @@ def test_discover_and_select(self): self.write("a/two.c", "int t;\n") self.write("a/Inputs/helper.c", "int h;\n") self.write("a/deep/Inputs/x.c", "int h;\n") - self.write("a/deep/three.c", "// CLEAN\nint x;\n") - self.write("b/four.c", "// CLEAN\nint x;\n") + self.write("a/deep/three.c", "// CLEAN\nint main(void) { return 0; }\n") + self.write("b/four.c", "// CLEAN\nint main(void) { return 0; }\n") self.write("b/notes.md", "// CLEAN\n") cases, parsed = rc.discover(self.cases) self.assertEqual([c.rel for c in cases], ["a/deep/three.c", "a/one.c", "b/four.c"]) @@ -409,37 +380,13 @@ def test_discover_and_select(self): class FlagsTest(unittest.TestCase): - def test_tool_flags(self): - own, compiler = rc.tool_flags(["-O2", "-fno-weavec-zero-init", "-Wweavec-leak", - "-Wno-weavec", "-Werror=weavec", "-Wno-error=weavec-leak", - "-fweavec-budget=50", "-fweavec-checks=report", - "-fweavec-diagnose", "-fno-weavec", "-DX", "-Wall"]) - self.assertEqual(own, ["--no-zero-init", "-Wweavec-leak", "-Wno-weavec", "-Werror=weavec", - "-Wno-error=weavec-leak", "--budget=50"]) - self.assertEqual(compiler, ["-O2", "-DX", "-Wall"]) - def test_plain_flags(self): self.assertEqual(rc.plain_flags(("-O2", "-fweavec-checks=verify", "-fno-weavec-zero-init", - "-Wweavec-leak", "-Wweavec", "-std=c11")), + "-fno-weavec-asan-compat", "-std=c11")), ["-O2", "-std=c11"]) class ParseOutputTest(unittest.TestCase): - def test_diagnostics(self): - with tempfile.TemporaryDirectory() as directory: - directory = Path(directory).resolve() - got = rc.parse_diagnostics(textwrap.dedent("""\ - a.c:4:7: error: use after free of 'p' [weavec::use-after-free] - 4 | *p = 1; - /abs/b.c:9:2: warning: possible leak [weavec::leak] - a.c:5:1: note: released here - a.c:6:1: warning: unused variable 'x' [-Wunused-variable] - weavec: a.c: 10 sites - """), directory) - self.assertEqual(got, [ - rc.Diagnostic(str(directory / "a.c"), 4, 7, "error", "use-after-free", "use after free of 'p'"), - rc.Diagnostic(str(Path("/abs/b.c").resolve()), 9, 2, "warning", "leak", "possible leak")]) - def test_reports(self): with tempfile.TemporaryDirectory() as directory: directory = Path(directory).resolve() @@ -501,84 +448,6 @@ def test_compare(self): # --------------------------------------------------------------------------- -class JudgeDiagnosticsTest(Workspace): - def judge(self, case, diagnostics=(), runs=None): - ev = rc.Evidence(diagnostics=list(diagnostics), runs=list(runs or [])) - failures = [] - rc.judge_diagnostics(case, ev, failures, bool(ev.runs) if runs is not None else True) - return failures - - def bug_case(self, marker="BUG: use-after-free"): - return self.case("s/b.c", f""" - int main(void) {{ - return 0; // {marker} - }} - """) - - def test_bug_needs_its_id_on_its_line(self): - case = self.bug_case() - path = case.path - self.assertEqual(self.judge(case, [diag(path, 2, "use-after-free", "warning")]), []) - failures = self.judge(case, [diag(path, 2, "double-free")]) - self.assertEqual(len(failures), 1, failures) - self.assertIn("b.c:2: expected weavec::use-after-free from the analysis or a stop", failures[0]) - failures = self.judge(case, [diag(path, 1, "use-after-free")]) - self.assertTrue(any("unexpected error" in f and ":1:3:" in f for f in failures), failures) - - def test_certainty(self): - definite = self.bug_case("BUG: use-after-free definite") - possible = self.case("s/p.c", "int main(void) {\n return 0; // BUG: use-after-free possible\n}\n") - self.assertEqual(self.judge(definite, [diag(definite.path, 2, "use-after-free", "error")]), []) - self.assertIn("(definite)", self.judge(definite, [diag(definite.path, 2, "use-after-free", "warning")])[0]) - self.assertEqual(self.judge(possible, [diag(possible.path, 2, "use-after-free", "warning")]), []) - self.assertIn("(possible)", self.judge(possible, [diag(possible.path, 2, "use-after-free", "error")])[0]) - - def test_a_stop_on_the_line_satisfies_a_stoppable_bug(self): - case = self.bug_case() - self.assertEqual(self.judge(case, runs=[run(TRAP, report("heap-use-after-free", case.path, 2))]), []) - self.assertTrue(self.judge(case, runs=[run(TRAP, report("heap-use-after-free", case.path, 1))])) - self.assertTrue(self.judge(case, runs=[run(0)])) - - def test_a_stop_does_not_satisfy_a_bug_it_cannot_stop(self): - case = self.bug_case("BUG: leak") - self.assertTrue(self.judge(case, runs=[run(TRAP, report("heap-use-after-free", case.path, 2))])) - - def test_an_unlocated_stop_of_the_same_kind(self): - release = self.bug_case("BUG: invalid-release") - self.assertEqual(self.judge(release, runs=[run(ABRT, report("invalid-release"))]), []) - null = self.case("s/n.c", "int main(void) {\n return 0; // BUG: null-dereference\n}\n") - self.assertEqual(self.judge(null, runs=[run(SEGV)]), []) - self.assertTrue(self.judge(release, runs=[run(SEGV)])) - - def test_without_a_run_a_stoppable_bug_is_not_required(self): - case = self.bug_case() - self.assertEqual(self.judge(case, runs=[]), []) - self.assertTrue(self.judge(self.bug_case("BUG: leak"), runs=[])) - - def test_miss_and_neutralised(self): - for marker in ("BUG: use-after-free // MISS: aliased through a global", - "BUG: use-of-uninitialized // NEUTRALISED: zero-init"): - with self.subTest(marker=marker): - case = self.bug_case(marker) - self.assertEqual(self.judge(case, runs=[run(0)]), []) - # A report on the line is not unexpected either. - self.assertEqual(self.judge(case, [diag(case.path, 2, "leak")], runs=[run(0)]), []) - - def test_clean_and_allow(self): - case = self.case("s/c.c", "// CLEAN\n// ALLOW: leak\nint main(void) { return 0; }\n") - self.assertEqual(self.judge(case, [diag(case.path, 3, "leak", "warning")]), []) - failures = self.judge(case, [diag(case.path, 3, "double-free", "warning")]) - self.assertIn("unexpected diagnostic in a CLEAN case", failures[0]) - # ALLOW excuses warnings only. - self.assertTrue(self.judge(case, [diag(case.path, 3, "leak", "error")])) - - def test_a_bug_case_tolerates_warnings_elsewhere_but_not_errors(self): - case = self.bug_case() - ok = diag(case.path, 2, "use-after-free") - self.assertEqual(self.judge(case, [ok, diag(case.path, 1, "leak", "warning")]), []) - self.assertIn("unexpected error", self.judge(case, [ok, diag(case.path, 1, "leak")])[0]) - - class JudgeRunsTest(Workspace): def judge(self, case, *runs): failures = [] @@ -625,12 +494,6 @@ def test_unlocated_stops(self): self.assertIn("unexpected stop: null-dereference at ", self.judge(self.trap_case(), run(SEGV))[0]) - def test_stop_on_a_bug_line(self): - case = self.case("s/b.c", "int main(void) {\n return 0; // BUG: out-of-bounds\n}\n") - self.assertEqual(self.judge(case, run(TRAP, report("heap-buffer-overflow", case.path, 2))), []) - self.assertEqual(self.judge(case, run(ABRT, report("invalid-release"))), []) - self.assertIn("unexpected stop", self.judge(case, run(TRAP, report("heap-buffer-overflow", case.path, 1)))[0]) - def test_stop_in_a_shared_helper(self): helper = self.write("s/Inputs/h.c", "int h(int *p) {\n return p[9];\n}\n") case = self.case("s/a.c", "// UNITS: Inputs/h.c\n// TRAP-AT: Inputs/h.c:2\nint main(void) { return 0; }\n") @@ -709,11 +572,10 @@ def setUp(self): self.scratch = Path(self.directory.name).resolve() / "scratch" self.scratch.mkdir() self.runs = {"build": [], "bug": [], "twin": []} - self.diagnostics = [] self.ledger = None self.asan = None self.builds = [] - fakes = {"analyse": self.fake_analyse, "build": self.fake_build, "run_all": self.fake_run_all, + fakes = {"build": self.fake_build, "run_all": self.fake_run_all, "run_asan": self.fake_run_asan} stack = contextlib.ExitStack() self.addCleanup(stack.close) @@ -723,15 +585,12 @@ def setUp(self): stack.callback(setattr, rc, name, original) def config(self, **overrides): - values = dict(weavec=Path("weavec"), weavec_cc=Path("weavec-cc"), clang="clang", checks="trap", + values = dict(weavec_cc=Path("weavec-cc"), clang="clang", checks="trap", asan=False, no_run=False, keep=False, compile_timeout=1.0, run_timeout=1.0, scratch=self.scratch) values.update(overrides) return rc.Config(**values) - def fake_analyse(self, case, cfg, ev, directory): - ev.diagnostics = list(self.diagnostics) - def fake_build(self, case, cfg, ev, directory, extra, ledger): self.builds.append((directory.name, tuple(extra), ledger)) ev.ledger = self.ledger if ledger else None @@ -748,7 +607,7 @@ def fake_run_asan(self, case, cfg, ev, directory): class RunMarkersTest(Fakes): def test_bug_case_passes_on_its_stop(self): - case = self.case("s/b.c", "// ASAN\nint main(void) {\n return 0; // BUG: out-of-bounds // TRAP\n}\n") + case = self.case("s/b.c", "// ASAN\nint main(void) {\n return 0; // TRAP\n}\n") self.runs["build"] = [run(TRAP, report("heap-buffer-overflow", case.path, 3))] self.asan = "heap-buffer-overflow" result = rc.run_case(case, self.config()) @@ -774,17 +633,12 @@ def test_ledger_is_requested_only_when_read(self): self.assertEqual(rc.run_case(case, self.config())["status"], "pass") self.assertEqual(self.builds, [("build", (), True)]) - def test_no_run_and_tool(self): + def test_no_run(self): case = self.case("s/b.c", "int main(void) {\n return 0; // TRAP\n}\n") self.assertEqual(rc.run_case(case, self.config(no_run=True))["status"], "pass") - tool = self.case("s/t.c", "// TOOL\nvoid f(int *p) { p[9] = 0; } // BUG: out-of-bounds\n") - self.builds.clear() - self.diagnostics = [diag(tool.path, 2, "out-of-bounds", "warning")] - self.assertEqual(rc.run_case(tool, self.config())["status"], "pass") - self.assertEqual(self.builds, []) def test_marker_errors_and_xfail(self): - bad = self.case("s/e.c", "int x;\n") + bad = self.case("s/e.c", "int main(void) { return 0; }\n") self.assertEqual(rc.run_case(bad, self.config())["status"], "error") xfail = self.case("s/x.c", "// XFAIL: not yet\nint main(void) {\n return 0; // TRAP\n}\n") self.runs["build"] = [run(0)] @@ -865,7 +719,7 @@ def test_summary(self): stopped = rc.run_case(case, self.config()) self.runs["bug"] = [run(0)] missed = rc.run_case(case, self.config()) - error = rc.run_case(self.case("s/e.c", "int x;\n"), self.config()) + error = rc.run_case(self.case("s/e.c", "int main(void) { return 0; }\n"), self.config()) summary = rc.summarize([stopped, missed, error]) self.assertEqual(summary["suites"], {"d": {"pass": 1, "fail": 1}, "s": {"error": 1}}) self.assertEqual(summary["totals"], {"pass": 1, "fail": 1, "error": 1}) diff --git a/test/Analysis/analysis-stats.c b/test/Analysis/analysis-stats.c deleted file mode 100644 index 82cb30f9..00000000 --- a/test/Analysis/analysis-stats.c +++ /dev/null @@ -1,15 +0,0 @@ -// RUN: %weavec --analysis-stats=%t.stats.json %s -- -// RUN: FileCheck %s --check-prefix=STATS < %t.stats.json -// RUN: not %weavec --analysis-stats=%t.stats.json/unwritable %s -- 2>&1 | FileCheck %s --check-prefix=OUTPUT -// RUN: not %weavec --analysis-stats= %s -- 2>&1 | FileCheck %s --check-prefix=EMPTY -// OUTPUT: weavec: error: cannot write analysis statistics '{{.*}}.stats.json/unwritable' -// EMPTY: weavec: error: analysis statistics require a path -// STATS: "version":1 -// STATS-SAME: "unit_parses":1 -// STATS-SAME: "final":true -// RFC 0020: work counts describe actual execution; explicit output errors fail. -// The object engine (RFC 0031 §12) does not yet record its own counters -// (block transfers, joins, materialisations), so only the driver's count of -// parsed units is pinned; the old engine's CFG and per-function counters are -// gone with it (RFC 0031 §10). -int main(void) { return 0; } diff --git a/test/Analysis/rfc0002-borrows.c b/test/Analysis/rfc0002-borrows.c deleted file mode 100644 index ad1ff347..00000000 --- a/test/Analysis/rfc0002-borrows.c +++ /dev/null @@ -1,161 +0,0 @@ -// RFC 0002: taking an address creates a loan held by the pointer variable; -// loans end when the holder is last used (RFC 0006) or reassigned. Freeing -// or moving a borrowed object is always a conflict. RFC 0030 §16 removes -// `--exclusive-borrows` and with it RFC 0001's full exclusivity rule. -// RUN: not %weavec %s -- 2>&1 | FileCheck --check-prefixes=CHECK,LAX %s -#include "../Inputs/prelude.h" -#include - -void peek(const int *WEAVEC_BORROWED p); -void poke(int *WEAVEC_MUT p); -void take(void *WEAVEC_OWNED p); - -struct node { - int v; -}; - -void two_mutable(void) { - int x = 0; - int *a = &x; - int *b = &x; - use(a); - use(b); -} - -void shared_then_mutable(void) { - int x = 0; - const int *a = &x; - int *b = &x; - use((void *)a); - use(b); -} - -void mutable_then_shared(void) { - int x = 0; - int *a = &x; - const int *b = &x; - use(a); - use((void *)b); -} - -void write_while_borrowed(void) { - int x = 0; - int *a = &x; - x = 1; - use(a); -} - -void free_while_borrowed(void) { - struct node *n = malloc(sizeof *n); - if (!n) - return; - int *a = &n->v; - free(n); - // `&n->v` is a derived copy of `n` (RFC 0011): the use is the report. - // CHECK: rfc0002-borrows.c:[[@LINE+1]]:7: error: use of 'a' after it was freed [weavec::use-after-free] - use(a); -} - -void move_while_borrowed(struct node *WEAVEC_OWNED n) { - int *a = &n->v; - take(n); - // CHECK: rfc0002-borrows.c:[[@LINE+1]]:7: error: use of 'a' after it was moved [weavec::use-after-move] - use(a); -} - -void temporary_borrows(void) { - int x = 0; - peek(&x); - poke(&x); // fine: the borrow for peek ended with the call - int *a = &x; - poke(&x); - use(a); -} - -void array_decay(void) { - int a[4]; - int *p = a; - int *q = &a[1]; - use(p); - use(q); -} - -// A loan whose holder is still live when the object is freed conflicts in -// both modes (RFC 0006, *Loans end at the last use of their holder*). -void free_before_last_use(struct node *WEAVEC_OWNED n) { - int *a = &n->v; - free(n); - // CHECK: rfc0002-borrows.c:[[@LINE+1]]:4: error: use of 'a' after it was freed [weavec::use-after-free] - *a = 1; -} - -// Clean: shared borrows coexist, writes through the borrow are fine, and -// loans end with their holder. -void shared_borrows(void) { - int x = 0; - const int *a = &x; - const int *b = &x; - use((void *)a); - use((void *)b); -} - -void write_through(void) { - int x = 0; - int *a = &x; - *a = 5; - use(a); -} - -void holder_reassigned(void) { - int x = 0; - int *a = &x; - a = NULL; - x = 1; - use(a); -} - -void holder_out_of_scope(void) { - int x = 0; - { - int *a = &x; - use(a); - } - x = 1; -} - -void holder_per_iteration(int n) { - int x = 0; - for (int i = 0; i < n; ++i) { - int *a = &x; - use(a); - } -} - -// Clean in both modes: the loan ends at the holder's last use, not at the -// end of its scope (RFC 0006). -void holder_dead(void) { - int x = 0; - int *a = &x; - use(a); - int *b = &x; // `a` is dead: no conflict even when exclusive - use(b); - x = 1; // `b` is dead too -} - -void free_after_last_use(struct node *WEAVEC_OWNED n) { - int *a = &n->v; - *a = 1; - free(n); -} - -// Clean by default: a view of a buffer that another routine writes is the -// ordinary shape of C string handling (RFC 0006, *Bugs deliberately not -// caught*). Exclusivity rejects it. -void view_then_write(void) { - int buf[8]; - int *p = buf; - poke(buf); - use(p); -} - -// LAX: 3 errors generated. diff --git a/test/Analysis/rfc0004-function-pointers.c b/test/Analysis/rfc0004-function-pointers.c deleted file mode 100644 index 9d297f03..00000000 --- a/test/Analysis/rfc0004-function-pointers.c +++ /dev/null @@ -1,94 +0,0 @@ -// RFC 0004, "Signatures for function pointers": an indirect call is resolved -// through ownership annotations on the function-pointer type, else through -// the join of every address-taken function of that type in the translation -// unit; with neither it is a call into unknown code like an unannotated -// extern: RFC 0030 §5.1 records it as an `unresolved(unknown-callee)` row. -// RUN: not %weavec %s -- 2>&1 | FileCheck %s -#include "../Inputs/prelude.h" -#include - -struct node { - int v; - struct node *next; -}; - -// 1. Annotations on the type: a typedef of a function-pointer type. -typedef void (*dtor_t)(void *WEAVEC_OWNED); -void through_typedef(dtor_t dtor, struct node *n) { - dtor(n); - // CHECK: rfc0004-function-pointers.c:[[@LINE+1]]:7: error: use of 'n' after it was moved [weavec::use-after-move] - use(n); -} - -// An ownership annotation on the declarator describes the result. -typedef WEAVEC_OWNED struct node *(*maker_t)(void); -void owned_result(maker_t make) { - struct node *n = make(); - free(n); - // CHECK: rfc0004-function-pointers.c:[[@LINE+1]]:7: error: use of 'n' after it was freed [weavec::use-after-free] - use(n); -} - -// A field of function-pointer type, annotated inline. -struct allocator { - void *(*WEAVEC_OWNED alloc)(size_t); - void (*release)(void *WEAVEC_OWNED); -}; -void through_field(struct allocator *a) { - struct node *n = a->alloc(sizeof *n); - a->release(n); - // CHECK: rfc0004-function-pointers.c:[[@LINE+1]]:7: error: use of 'n' after it was moved [weavec::use-after-move] - use(n); -} - -// A parameter of function-pointer type, annotated inline. -void through_param(void (*drop)(struct node *WEAVEC_OWNED), struct node *n) { - drop(n); - // CHECK: rfc0004-function-pointers.c:[[@LINE+1]]:7: error: use of 'n' after it was moved [weavec::use-after-move] - use(n); -} - -// 2. The join of address-taken functions of the type. -static void node_free(struct node *n) { free(n); } -static void node_peek(struct node *n) { use(n); } -struct hooks { - void (*on_drop)(struct node *); -}; -static struct hooks H = {node_free}; -void through_table(struct node *n) { - H.on_drop(n); - // CHECK: rfc0004-function-pointers.c:[[@LINE+1]]:7: error: use of 'n' after it was freed [weavec::use-after-free] - use(n); -} -void register_peek(struct hooks *h) { h->on_drop = node_peek; } -void through_callback(void (*cb)(struct node *), struct node *n) { - // RFC 0030 §9.3: an indirect call whose slot has no known target takes - // the §5.1 default with the reason `callback`. - // The detail names the slot the value came from (RFC 0030 §9.3). - cb(n); /* RFC 0014: a type match alone does not identify this value. */ - use(n); -} - -// A function's own address is not "taken" by calling it directly. -static int helper(int x) { return x; } -int calls_directly(int x) { return helper(x); } - -// 3. Otherwise unknown code, at every call. -void boundary(int (*cmp)(const void *, const void *), char *a, char *b) { - cmp(a, b); - // The first call may have released `a` and `b` (RFC 0031 §5.4, per - // object), through a callback (RFC 0030 §9.3), which is what the second - // call's facet reports. - cmp(b, a); -} - -// An unresolvable callee that is not a place. -static struct node *(*hook)(void); -static struct node *(*get_hook(void))(void) { return hook; } -void boundary_without_place(void) { - struct node *n = get_hook()(); - use(n); -} - -// CHECK-NOT: warning: -// CHECK: 5 errors generated. diff --git a/test/Analysis/rfc0004-posix.c b/test/Analysis/rfc0004-posix.c deleted file mode 100644 index 7833d8f6..00000000 --- a/test/Analysis/rfc0004-posix.c +++ /dev/null @@ -1,115 +0,0 @@ -// RFC 0004, "The library table": the shipped table covers the POSIX and -// common GNU/BSD functions real programs call, so checked code that uses them -// does not warn, and their ownership effects are modelled. -// RUN: not %weavec %s -- 2>&1 | FileCheck %s -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// `getline` stores a fresh buffer through its first argument. -void lines(FILE *f) { - char *line = NULL; - size_t cap = 0; - while (getline(&line, &cap, f) != -1) - fputs(line, stdout); - free(line); - // CHECK: rfc0004-posix.c:[[@LINE+1]]:8: error: use of 'line' after it was freed [weavec::use-after-free] - puts(line); -} - -// `asprintf` too. -void formatted(int n) { - char *s; - // A false positive the object engine accepts (RFC 0031 *Accepted false - // positives*, §5.8): the table's `null-on-failure` buffer is not tied to - // the negative result, so the failure path keeps a possible allocation - // (reported after the function's other findings). - if (asprintf(&s, "%d", n) < 0) - return; - free(s); - // CHECK: rfc0004-posix.c:[[@LINE+2]]:3: error: 's' is freed twice [weavec::double-free] - // CHECK: rfc0004-posix.c:[[@LINE-3]]:5: warning: result of 'asprintf' is leaked [weavec::leak] - free(s); -} - -// Directory streams are owned; an entry lives inside the stream. -void listing(const char *path) { - DIR *d = opendir(path); - if (!d) - return; - struct dirent *e = readdir(d); - closedir(d); - // RFC 0031 §5.11: the name is the operand as written at the site. - // CHECK: rfc0004-posix.c:[[@LINE+1]]:8: error: use of 'e->d_name' after it was freed [weavec::use-after-free] - puts(e->d_name); - // CHECK: rfc0004-posix.c:[[@LINE-4]]:3: note: freed here (through 'd') -} - -// Mappings, address lists and dynamic library handles are owned. -void owned_handles(const char *host) { - void *m = mmap(NULL, 4096, PROT_READ, MAP_PRIVATE, -1, 0); - if (m != MAP_FAILED) - munmap(m, 4096); - struct addrinfo *res = NULL; - if (getaddrinfo(host, "80", NULL, &res) == 0) { - freeaddrinfo(res); - // CHECK: rfc0004-posix.c:[[@LINE+1]]:5: error: 'res' is freed twice [weavec::double-free] - freeaddrinfo(res); - } - void *lib = dlopen("libm.so", RTLD_NOW); - if (lib) - dlclose(lib); -} - -// Everyday POSIX with no ownership effects: nothing to report, nothing to -// warn about, even in strict mode. -static void *worker(void *arg) { return arg; } -int everyday(const char *path, char *buf, size_t n) { - int fd = open(path, O_RDONLY); - if (fd < 0) - return errno; - struct stat st; - if (fstat(fd, &st) == 0 && read(fd, buf, n) > 0) - write(1, buf, n); - close(fd); - char cwd[64]; - if (getcwd(cwd, sizeof cwd)) - puts(cwd); - char *save; - for (char *tok = strtok_r(buf, " ", &save); tok; tok = strtok_r(NULL, " ", &save)) - puts(tok); - struct timespec ts; - clock_gettime(CLOCK_MONOTONIC, &ts); - struct tm tm; - time_t now = ts.tv_sec; - localtime_r(&now, &tm); - pthread_mutex_t mu; - pthread_mutex_init(&mu, NULL); - pthread_mutex_lock(&mu); - pthread_mutex_unlock(&mu); - pthread_mutex_destroy(&mu); - pthread_t th; - if (pthread_create(&th, NULL, worker, buf) == 0) - pthread_join(th, NULL); - int s = socket(AF_INET, SOCK_STREAM, 0); - if (s >= 0) { - struct sockaddr_storage addr; - socklen_t len = sizeof addr; - getsockname(s, (struct sockaddr *)&addr, &len); - close(s); - } - return 0; -} - -// CHECK: 1 warning and 4 errors generated. diff --git a/test/Analysis/rfc0004-raw.c b/test/Analysis/rfc0004-raw.c deleted file mode 100644 index 177d21f7..00000000 --- a/test/Analysis/rfc0004-raw.c +++ /dev/null @@ -1,115 +0,0 @@ -// RFC 0004, "Raw pointers", as amended by RFC 0033 §2: a pointer declared -// WEAVEC_RAW, loaded through a raw pointer or handed out as raw by a callee -// carries no ownership guarantee. Dereferencing, releasing, passing it where -// the callee does either, or asserting a kind for it is an unsafe-operation -// outside an unsafe region. Copying and comparing it is fine. A pointer -// converted from an integer is not raw: it is a pointer of unknown -// provenance, guarded where it is used. -// RUN: not %weavec %s -- 2>&1 | FileCheck %s --implicit-check-not='cast from an integer' -#include "../Inputs/prelude.h" -#include - -typedef unsigned long uintptr_t; -struct node { - int v; - struct node *next; -}; - -// RFC 0033 §2: no error for a pointer converted from an integer. -void deref_int_cast(uintptr_t x) { - struct node *n = (struct node *)x; - // CHECK-NOT: rfc0004-raw.c:[[@LINE+1]]:{{.*}}error - n->v = 1; - ((struct node *)x)->v = 1; - free((char *)x); -} - -void copies_and_comparisons_are_fine(struct node *WEAVEC_RAW x, - struct node *m) { - struct node *n = x; - struct node *o = n; - if (o == m || n == NULL) - return; - n = m; - n->v = 1; /* reassigned from a tracked pointer: no longer raw */ -} - -void release(char *WEAVEC_RAW p) { - // CHECK: rfc0004-raw.c:[[@LINE+1]]:8: error: 'free' releases raw pointer 'p' outside an unsafe region [weavec::unsafe-operation] - free(p); -} - -void pass_to_borrowing_callee(char *WEAVEC_RAW p) { - char *q = p; - // CHECK: rfc0004-raw.c:[[@LINE+1]]:7: error: 'use' dereferences raw pointer 'q' outside an unsafe region [weavec::unsafe-operation] - use(q); - // CHECK: note: 'q' is raw: declared WEAVEC_RAW here (through 'p') -} - -static void take(struct node *WEAVEC_OWNED n) { free(n); } -void pass_to_owning_callee(struct node *WEAVEC_RAW n) { - // CHECK: rfc0004-raw.c:[[@LINE+1]]:8: error: 'take' takes ownership of raw pointer 'n' outside an unsafe region [weavec::unsafe-operation] - take(n); -} - -// WEAVEC_RAW on a parameter, a field and a local. -void raw_param(struct node *WEAVEC_RAW r) { - // CHECK: rfc0004-raw.c:[[@LINE+1]]:3: error: dereference of raw pointer 'r' outside an unsafe region [weavec::unsafe-operation] - r->v = 1; - // CHECK-NEXT: {{.*}}r->v = 1; - // CHECK-NEXT: {{.*}}^ - // CHECK-NEXT: rfc0004-raw.c:[[@LINE-5]]:40: note: 'r' is raw: declared WEAVEC_RAW here - // CHECK: note: move this operation into a WEAVEC_UNSAFE block or function, or assert the pointer's ownership first -} - -struct ctx { - void *WEAVEC_RAW cookie; -}; -void raw_field(struct ctx *c) { - struct node *n = c->cookie; - // CHECK: rfc0004-raw.c:[[@LINE+1]]:7: error: 'use' dereferences raw pointer 'n' outside an unsafe region [weavec::unsafe-operation] - use(n); - // CHECK: note: 'n' is raw: declared WEAVEC_RAW here (through 'c->cookie') -} - -void raw_local(struct node *m) { - struct node *WEAVEC_RAW r = m; /* storing into a raw place drops the value out of the model */ - // CHECK: rfc0004-raw.c:[[@LINE+1]]:3: error: dereference of raw pointer 'r' outside an unsafe region [weavec::unsafe-operation] - r->v = 1; - m->v = 1; /* m itself is untouched */ -} - -// A value loaded through a raw pointer is raw. -void loaded_through_raw(struct node *WEAVEC_RAW r) { - struct node *n; - WEAVEC_UNSAFE { n = r->next; } - // CHECK: rfc0004-raw.c:[[@LINE+1]]:3: error: dereference of raw pointer 'n' outside an unsafe region [weavec::unsafe-operation] - n->v = 1; - // CHECK: note: 'n' is raw: loaded through raw pointer 'r' here -} - -// A callee's raw result is raw for the caller, through the inferred summary: -// here the summary returns the raw argument itself. -static void *lookup(void *WEAVEC_RAW x) { return x; } -void from_callee(void *WEAVEC_RAW x) { - int *p = lookup(x); - // CHECK: rfc0004-raw.c:[[@LINE+1]]:4: error: dereference of raw pointer 'p' outside an unsafe region [weavec::unsafe-operation] - *p = 1; - // CHECK: note: 'p' is raw: declared WEAVEC_RAW here (through 'x') -} - -// RFC 0033 §2: rawness is a must fact. A value raw on some paths only is -// guarded, not an error. -void joined(int c, struct node *WEAVEC_RAW x) { - struct node *n = c ? malloc(sizeof *n) : x; - // CHECK-NOT: rfc0004-raw.c:[[@LINE+1]]:{{.*}}error - n->v = 1; - free(n); -} - -// Pointer-to-integer conversion takes a value out of the model silently. -uintptr_t to_integer(struct node *WEAVEC_OWNED n) { - return (uintptr_t)n; -} - -// CHECK: 8 errors generated. diff --git a/test/Analysis/rfc0004-unsafe-regions.c b/test/Analysis/rfc0004-unsafe-regions.c deleted file mode 100644 index db9b64f2..00000000 --- a/test/Analysis/rfc0004-unsafe-regions.c +++ /dev/null @@ -1,91 +0,0 @@ -// RFC 0004, "Unsafe regions": a WEAVEC_UNSAFE block or function body is -// analysed, its raw operations are permitted, and its effects flow out and -// are checked in the code around it. RFC 0030 §6.1: no diagnostic is dropped -// for being inside a region; a definite temporal violation there is an error. -// RUN: not %weavec %s -- 2>&1 | FileCheck %s -// RUN: not %weavec --dump-analysis %s -- 2>/dev/null | FileCheck --check-prefix=DUMP %s -#include "../Inputs/prelude.h" -#include - -typedef unsigned long uintptr_t; -struct node { - int v; - struct node *next; -}; - -// Raw operations inside the region are fine. -void permitted(uintptr_t x) { - WEAVEC_UNSAFE { - struct node *n = (struct node *)x; - n->v = 1; - free(n); - } -} - -WEAVEC_UNSAFE void whole_function(struct node *WEAVEC_RAW r) { - r->v = 1; - free(r); - free(r); // CHECK: rfc0004-unsafe-regions.c:[[@LINE]]:3: error: 'r' is freed twice [weavec::double-free] -} - -// Nothing inside is suppressed ... -void suppressed(struct node *p) { - free(p); - WEAVEC_UNSAFE { - use(p); // CHECK: rfc0004-unsafe-regions.c:[[@LINE]]:9: error: use of 'p' after it was freed [weavec::use-after-free] - free(p); // CHECK: rfc0004-unsafe-regions.c:[[@LINE]]:5: error: 'p' is freed twice [weavec::double-free] - } -} - -// ... but a free inside is a free, seen outside. -void escapes(struct node *p) { - WEAVEC_UNSAFE { free(p); } - // CHECK: rfc0004-unsafe-regions.c:[[@LINE+1]]:7: error: use of 'p' after it was freed [weavec::use-after-free] - use(p); - // CHECK: rfc0004-unsafe-regions.c:[[@LINE-3]]:19: note: freed here -} - -// A raw value used inside stays raw outside. -void raw_escapes(struct node *WEAVEC_RAW x) { - struct node *n; - WEAVEC_UNSAFE { n = x; n->v = 1; } - // CHECK: rfc0004-unsafe-regions.c:[[@LINE+1]]:3: error: dereference of raw pointer 'n' outside an unsafe region [weavec::unsafe-operation] - n->v = 2; -} - -// The unsafe function's summary is consulted by its callers like any other. -WEAVEC_UNSAFE static void release(struct node *n) { free(n); } -void caller(struct node *n) { - release(n); - // CHECK: rfc0004-unsafe-regions.c:[[@LINE+1]]:7: error: use of 'n' after it was freed [weavec::use-after-free] - use(n); -} - -// A bodyless WEAVEC_UNSAFE declaration is no ownership contract: the callee is -// unknown (RFC 0030 §5.1), and the use after it is unresolved, not reported. -WEAVEC_UNSAFE void vouched(void *p); -void trusts(struct node *n) { - vouched(n); - use(n); -} - -// A nested block is just part of the region. -void nested(uintptr_t x) { - WEAVEC_UNSAFE { - struct node *n = (struct node *)x; - if (n) { - n->v = 1; - } - } -} - -// CHECK: 6 errors generated. - -// The dump is the format-30 summary (RFC 0031 §6.1, §12): a release inside -// a region flows out of the function like any other. -// DUMP-LABEL: function 'whole_function': -// DUMP: release *param0 free when always -// DUMP-LABEL: function 'escapes': -// DUMP: release *param0 free when always -// DUMP-LABEL: function 'release': -// DUMP: release *param0 free when always diff --git a/test/Analysis/rfc0006-conditions.c b/test/Analysis/rfc0006-conditions.c deleted file mode 100644 index a209ba5b..00000000 --- a/test/Analysis/rfc0006-conditions.c +++ /dev/null @@ -1,105 +0,0 @@ -// RFC 0006, *Condition facts on CFG edges*: pointer equality tests refine -// the alias relation on the edge they hold on, and `!=` separates only -// exact aliases (pointer arithmetic makes a copy interior). -// RUN: not %weavec %s -- 2>&1 | FileCheck %s -// RUN: not %weavec --dump-analysis %s -- 2>&1 | FileCheck --check-prefix=DUMP %s -#include "../Inputs/prelude.h" - -struct list { - struct list *next; -}; - -static char *get(void) { return malloc(4); } -extern char *sentinel_value; - -// Clean: on the `!=` edge the two pointers are known to be distinct. -// The object engine does not refine the fresh `l` to null on the `==` edge -// (a fresh object is distinct from what a global holds, RFC 0031 §4.5 D4, -// so only null can compare equal), and reports a leak there: a possible -// finding on correct code (RFC 0031 *Accepted false positives*). -void sentinel(void) { - char *l = get(); - if (l == sentinel_value) - // CHECK: rfc0006-conditions.c:[[@LINE+1]]:5: warning: 'l' is leaked [weavec::leak] - return; - free(l); - use(sentinel_value); -} - -void not_equal_then_free(char *p, char *q) { - if (p != q) { - free(p); - use(q); - } -} - -void unlink(struct list *head, struct list *victim) { - struct list *cur = head; - if (cur != victim) { - free(victim); - use(cur); - } -} - -// The linenoise idiom: a reader that returns either a fresh line or a -// sentinel global, looped on until it does not; the exit edge separates. -int ready(void); -static char *feed(void) { return ready() ? get() : sentinel_value; } -// Format 30 (RFC 0031 §6.1) has no value for "a fresh block or the entry -// value of a global", so `feed`'s result, and with it `read_line`'s, is -// unknown: the callers' accesses are unresolved rather than proven (a loss -// of precision, not of a finding). -// DUMP-LABEL: function 'feed': -// DUMP: result unknown maybe-null when null nonnull -char *read_line(void) { - char *res; - while ((res = feed()) == sentinel_value) - ; - return res; -} -// DUMP-LABEL: function 'read_line': -// DUMP: result unknown maybe-null when null nonnull -void reader_loop(void) { - for (;;) { - char *line = read_line(); - if (line == NULL) - break; - line[0] = 0; - free(line); - } -} - -// Reported: on the `==` edge the two are the same object. -void equal_then_free(char *p, char *q) { - if (p == q) { - free(p); - // The object engine remembers `p == q` (RFC 0031 *Implementation - // amendments*, *Pointer comparisons*) but does not use it to decide the - // temporal facet of `q`: not proven, no longer definite - // (the retired golden comparison of RFC 0030, *Lit tests*). - use(q); - } -} - -// Reported: `q = p + 1` is an interior alias; `!=` does not separate it. -void interior_alias(char *p) { - char *q = p + 1; - if (p != q) { - free(p); - // CHECK: rfc0006-conditions.c:[[@LINE+1]]:9: error: use of 'q' after it was freed [weavec::use-after-free] - use(q); - } -} - -// Reported: a copy is exact and is separated, but the join after the `if` -// restores the may-alias. -void separated_then_joined(char *p, char *q) { - char *r = p; - if (r != q) - use(q); - free(p); - // CHECK: rfc0006-conditions.c:[[@LINE+1]]:7: error: use of 'r' after it was freed [weavec::use-after-free] - use(r); -} - -// CHECK: 1 warning and 2 errors generated. diff --git a/test/Analysis/rfc0006-outcomes.c b/test/Analysis/rfc0006-outcomes.c deleted file mode 100644 index 1ecd3922..00000000 --- a/test/Analysis/rfc0006-outcomes.c +++ /dev/null @@ -1,151 +0,0 @@ -// RFC 0006, *Outcome-conditional summaries*: a callee that consumes its -// argument only on the paths returning some class of value is summarised -// per class, and a caller's test of the result retracts the consumption on -// the edge where it did not happen. `realloc` is the library instance: it -// moves its argument on the non-null class and, when the size is zero, on -// the null class as well (RFC 0030 §8.2). -// RUN: not %weavec %s -- 2>&1 | FileCheck %s -// RUN: not %weavec --dump-analysis %s -- 2>&1 | FileCheck --check-prefix=DUMP %s -#include "../Inputs/prelude.h" - -struct node { - int v; -}; - -// Consumes `n` only when it returns 0. -static int try_take(struct node *n, int c) { - if (c) { - free(n); - return 0; - } - return -1; -} -// The release happens only when `c` is non-zero: each result class carries -// the parameter test its exits pass, and the release is keyed by the class -// (RFC 0031 §6.1 and *Implementation amendments*, *Pending cases*). -// DUMP-LABEL: function 'try_take': -// DUMP: result int [-1, -1] when negative and param 1 =0 -// DUMP-NEXT: result int [0, 0] when zero and param 1 !=0 -// DUMP-NEXT: release *param0 free when result zero - -// Consumes `p` only when it returns non-null (a `realloc` wrapper). -static char *grow(char *p, size_t n) { - char *q = realloc(p, n); - if (!q) - return NULL; - return q; -} -// `realloc` moves its argument into the result on the nonnull class; on the -// null class it keeps it (the zero-initialisation wrapper never asks for -// zero bytes, RFC 0031 *Implementation amendments*). -// DUMP-LABEL: function 'grow': -// DUMP: result null when null -// DUMP-NEXT: result fresh#0 free extent param1 zeroed when nonnull -// DUMP-NEXT: move *param0 free when result nonnull -// DUMP-LABEL: function 'guarded': -// DUMP: release *param0 free when always - -// Clean: the test selects the class that did not consume. -void guarded(struct node *n, int c) { - int rc = try_take(n, c); - if (rc != 0) - free(n); -} - -void guarded_direct(struct node *n, int c) { - if (try_take(n, c) < 0) - free(n); -} - -void guarded_in_condition(struct node *n, int c) { - int rc; - if ((rc = try_take(n, c)) == 0) - return; - n->v = 1; - free(n); -} - -void grown(char *p) { - char *q = grow(p, 16); - if (q == NULL) { - free(p); // `grow` failed: `p` is still ours - return; - } - free(q); -} - -void realloc_null_edge(char **buf) { - char *p = *buf; - char *q = realloc(p, 16); - if (q == NULL) { - free(p); - return; - } - *buf = q; -} - -// A callee that reallocates a path below its argument and returns it (Lua's -// `resizearray`): the result is the resource itself, not a dangling copy of -// what was consumed (RFC 0006, *Interaction with existing RFCs*). -struct table { - char *array; - size_t n; -}; -static char *resize(struct table *t, size_t n) { - if (n == t->n) - return t->array; - return realloc(t->array, n); -} -// The result is the fresh block, null, or `t->array` itself; the move of -// `t->array` is keyed by the result class, and possible because the -// `n == t->n` exit returns the array unmoved. -// DUMP-LABEL: function 'resize': -// DUMP: result fresh#0 free extent param1 zeroed when nonnull and param 0 !=0 -// DUMP-NEXT: result path param0->array when null nonnull and param 0 !=0 -// DUMP-NEXT: move *param0->array free may when result nonnull - -void resized(struct table *t, size_t n) { - char *na = resize(t, n); - if (na == NULL) - return; - t->array = na; - use(t->array); -} - -// Reported: the selected class consumed, or nothing was tested. -void wrong_branch(struct node *n, int c) { - int rc = try_take(n, c); - // `try_take` returns 0 only after freeing `n`, so on this edge the release - // is certain (RFC 0031 §6.3, the result class selects the effect). - if (rc == 0) - // CHECK: rfc0006-outcomes.c:[[@LINE+1]]:9: error: use of 'n' after it was freed [weavec::use-after-free] - use(n); -} - -void untested(struct node *n, int c) { - try_take(n, c); - // CHECK: rfc0006-outcomes.c:[[@LINE+1]]:3: warning: use of 'n' after it may have been freed [weavec::use-after-free] - n->v = 1; -} - -void realloc_untested(char *p) { - char *q = realloc(p, 16); - // CHECK: rfc0006-outcomes.c:[[@LINE+1]]:3: warning: use of 'p' after it may have been moved [weavec::use-after-move] - free(p); - // CHECK: rfc0006-outcomes.c:[[@LINE+1]]:3: warning: 'q' is leaked [weavec::leak] - use(q); -} - -void result_overwritten(char *p) { - char *q = realloc(p, 8); - // CHECK: rfc0006-outcomes.c:[[@LINE+1]]:3: warning: 'q' is leaked: it is overwritten without being released [weavec::leak] - q = malloc(2); - // Reported at the branch that takes the leaking path (RFC 0031 - // *Implementation amendments*, *Leaks on some paths*). - // CHECK: rfc0006-outcomes.c:[[@LINE+1]]:7: warning: 'q' is leaked [weavec::leak] - if (q == NULL) - // CHECK: rfc0006-outcomes.c:[[@LINE+1]]:5: warning: use of 'p' after it may have been moved [weavec::use-after-move] - free(p); -} - -// CHECK: 6 warnings and 1 error generated. diff --git a/test/Analysis/rfc0006-written.c b/test/Analysis/rfc0006-written.c deleted file mode 100644 index f0185043..00000000 --- a/test/Analysis/rfc0006-written.c +++ /dev/null @@ -1,29 +0,0 @@ -// RFC 0006, *`written` forgets what lies below*: a callee that overwrites -// an object (`memcpy` into it) makes every fact about its sub-objects -// stale; the freed record of a field does not survive the overwrite. -// RUN: %weavec %s -- 2>&1 | FileCheck --allow-empty %s -// RUN: %weavec --dump-analysis %s -- 2>&1 | FileCheck --check-prefix=DUMP %s -#include "../Inputs/prelude.h" - -void *memcpy(void *dest, const void *src, size_t n); - -struct n { - char *string; - int v; -}; - -struct n tmp; - -// Clean: `root->string` was freed, then `*root` was overwritten wholesale. -void replace(struct n *root) { - free(root->string); - memcpy(root, &tmp, sizeof *root); - use(root->string); -} - -// The summary does not claim `param 0 *.string` is freed either: the exit -// state no longer has it. -// DUMP: replace -// DUMP-NOT: param 0 *.string: freed - -// CHECK-NOT: error: diff --git a/test/Analysis/rfc0007-clean.c b/test/Analysis/rfc0007-clean.c deleted file mode 100644 index aaa62e65..00000000 --- a/test/Analysis/rfc0007-clean.c +++ /dev/null @@ -1,294 +0,0 @@ -// RFC 0007, *Snippets that must be clean*, plus the idioms that a leak -// checker most often gets wrong: null-tested results, ownership handed to -// the caller or to a struct, unknown callees, `getline`, loops over arrays, -// nulled and conditionally-freed fields, wrappers, `goto` cleanup, `exit`, -// by-value struct returns, and pointers laundered through integers. -// RUN: %weavec %s -- 2>&1 | FileCheck --allow-empty --check-prefix=QUIET %s -// QUIET-NOT: {{warning|error}}: -// RFC 0030 §5.1: the calls into unknown code are not proven, not warnings. -#include -#include -#include -#include -#include - -struct buf { - char *data; - size_t n; -}; - -struct box { - char *WEAVEC_OWNED p; -}; - -struct node { - int v; - struct node *next; -}; - -void register_thing(void *p); -static void xfree(void *p) { free(p); } - -// The RFC's list. -int checked(void) { - char *p = malloc(8); - if (!p) - return -1; - free(p); - return 0; -} -char *handed_out(void) { - char *p = malloc(8); - return p; -} -void stored(struct buf *b) { b->data = malloc(8); } -void kept(char *p) { - static char *keep; - keep = p; -} -void to_unknown(void) { - char *p = malloc(8); - register_thing(p); -} -void lines(FILE *f) { - char *l = NULL; - size_t n = 0; - while (getline(&l, &n, f) != -1) { - } - free(l); -} -void loop(char **a, int n) { - for (int i = 0; i < n; i++) - free(a[i]); - free(a); -} -void nulled(struct box *b) { - free(b->p); - b->p = NULL; - free(b); -} -void maybe(struct box *b) { - if (b->p) - free(b->p); - free(b); -} -void wrapper(char *p) { xfree(p); } - -// More null-test spellings. -int checked2(void) { - char *p = malloc(8); - if (p == NULL) - return -1; - free(p); - return 0; -} -int checked3(void) { - char *p; - if (!(p = malloc(8))) - return -1; - free(p); - return 0; -} -int checked4(void) { - char *p = malloc(8); - if (p) { - p[0] = 0; - free(p); - } - return 0; -} -// Not clean: when `c` is zero the block is neither returned nor freed. The -// object engine reports the leak at the return (RFC 0031 §5.8); the old -// engine missed it. -char *handed_out_or_null(int c) { - char *p = malloc(8); - // QUIET: rfc0007-clean.c:[[@LINE+2]]:18: warning: 'p' is leaked [weavec::leak] - // QUIET-NOT: {{warning|error}}: - return c ? p : NULL; -} - -// Ownership kept by a global or a `static` local. -void kept_owned(void) { - static char *keep; - char *p = malloc(8); - keep = p; -} - -// Through an out-parameter, in both directions. -void out_param(char **out) { *out = malloc(8); } -int out_param_user(void) { - char *s; - out_param(&s); - free(s); - return 0; -} - -// Into a container the caller owns. -void push(struct node **head, struct node *n) { - n->next = *head; - *head = n; -} -void push_use(struct node **head) { - struct node *n = malloc(sizeof *n); - if (!n) - return; - push(head, n); -} -void arr_fill(char **a, int n) { - for (int i = 0; i < n; i++) - a[i] = malloc(4); -} -void local_arr(void) { - char *a[4]; - for (int i = 0; i < 4; i++) - a[i] = malloc(4); - for (int i = 0; i < 4; i++) - free(a[i]); -} - -// By-value structs carry their owned fields. -struct buf make(void) { - struct buf b; - b.data = malloc(8); - b.n = 8; - return b; -} -void by_value_out(struct buf *out) { - struct buf b; - b.data = malloc(8); - b.n = 8; - *out = b; -} -void tmp_struct(void) { - struct buf b = {.data = malloc(8), .n = 8}; - free(b.data); -} - -// Laundered through an integer: the checker can no longer follow it. -void to_int(void) { - char *p = malloc(8); - uintptr_t u = (uintptr_t)p; - (void)u; -} - -// Library functions that keep their argument (RFC 0007, *Assumptions*). -void into_env(void) { - char *s = strdup("A=b"); - if (s) - putenv(s); -} - -// Constructors: fields of a fresh object own nothing yet, and the error path -// frees the object alone. -struct box *box_new(void) { - struct box *b = malloc(sizeof *b); - if (!b) - return NULL; - b->p = malloc(4); - if (!b->p) { - free(b); - return NULL; - } - return b; -} -void box_free(struct box *b) { - free(b->p); - free(b); -} -void memset_fresh(void) { - struct buf *b = malloc(sizeof *b); - if (!b) - return; - memset(b, 0, sizeof *b); - b->data = malloc(4); - free(b->data); - free(b); -} - -// `goto` cleanup and `free(NULL)`. -void goto_cleanup(void) { - char *a = NULL, *b = NULL; - a = malloc(8); - if (!a) - goto out; - b = malloc(8); - if (!b) - goto out; -out: - free(a); - free(b); -} - -// `realloc` with the failure path handled (RFC 0006). A size that may be -// zero would make the `free(p)` a possible double free (RFC 0030 §8.2). -void realloc_grow(char *WEAVEC_OWNED p, size_t n) { - if (n == 0) { - free(p); - return; - } - char *q = realloc(p, n); - if (!q) { - free(p); - return; - } - free(q); -} - -// A block that ends in `exit` is not a death point. -void exit_path(void) { - char *p = malloc(8); - if (!p) - exit(1); - p[0] = 0; - exit(0); -} - -// Linked lists. -void list_free(struct node *n) { - while (n) { - struct node *next = n->next; - free(n); - n = next; - } -} -struct node *list_new(void) { - struct node *n = malloc(sizeof *n); - if (!n) - return NULL; - n->next = NULL; - return n; -} - -// Both arms release. -int cond_free(int c) { - char *p = malloc(8); - if (!p) - return -1; - if (c) { - free(p); - return 1; - } - free(p); - return 0; -} - -// Swapping two owned pointers moves both. -void swap(char **a, char **b) { - char *t = *a; - *a = *b; - *b = t; -} - -// Owned parameters released or stored. -void owned_param_ok(char *WEAVEC_OWNED p) { free(p); } -void owned_param_stored(struct buf *b, char *WEAVEC_OWNED p) { b->data = p; } - -// A callback context handed to an unknown registrar. -void cb(void *ctx); -void register_cb(void (*fn)(void *), void *ctx); -void with_ctx(void) { - char *ctx = malloc(8); - register_cb(cb, ctx); -} - -// The only diagnostic is the leak in `handed_out_or_null`. diff --git a/test/Analysis/rfc0007-families.c b/test/Analysis/rfc0007-families.c deleted file mode 100644 index 57fd33dd..00000000 --- a/test/Analysis/rfc0007-families.c +++ /dev/null @@ -1,78 +0,0 @@ -// RFC 0007, *Mismatched releases*: every allocator belongs to the family of -// its releaser; releasing a resource with a function of another family is an -// error, through wrappers and through `realloc` too. -// RUN: not %weavec %s -- 2>&1 | FileCheck %s -// RUN: not %weavec --dump-analysis %s -- 2>&1 | FileCheck --check-prefix=DUMP %s -#include -#include -#include - -// The RFC's snippets that must be reported. - -void family(const char *path) { - FILE *f = fopen(path, "r"); - if (!f) - return; - // CHECK: rfc0007-families.c:[[@LINE+2]]:3: error: 'f' is released with 'free' but must be released with 'fclose' [weavec::mismatched-release] - // CHECK: rfc0007-families.c:[[@LINE-4]]:13: note: allocated here - free(f); -} - -void family2(void) { - char *p = malloc(8); - if (!p) - return; - // CHECK: rfc0007-families.c:[[@LINE+1]]:3: error: 'p' is released with 'fclose' but must be released with 'free' [weavec::mismatched-release] - fclose((FILE *)p); -} - -void family3(const char *path) { - FILE *f = fopen(path, "r"); - if (!f) - return; - // CHECK: rfc0007-families.c:[[@LINE+1]]:7: error: 'f' is released with 'free' but must be released with 'fclose' [weavec::mismatched-release] - f = realloc(f, sizeof *f); - free(f); -} - -// Through a wrapper: the family is the canonical releaser's name, not the -// wrapper's. -static void xfree(void *p) { free(p); } - -void via_wrapper(const char *path) { - FILE *f = fopen(path, "r"); - if (!f) - return; - // CHECK: rfc0007-families.c:[[@LINE+1]]:3: error: 'f' is released with 'free' but must be released with 'fclose' [weavec::mismatched-release] - xfree(f); -} - -// The mismatch travels with copies. -void via_copy(const char *path) { - FILE *f = fopen(path, "r"); - if (!f) - return; - void *v = f; - // CHECK: rfc0007-families.c:[[@LINE+1]]:3: error: 'v' is released with 'free' but must be released with 'fclose' [weavec::mismatched-release] - free(v); -} - -// Clean: the matching releaser, a wrapper of it, and `strdup` is family free. -void fine(const char *path) { - FILE *f = fopen(path, "r"); - if (f) - fclose(f); - char *s = strdup(path); - xfree(s); -} - -// The inferred summaries carry the family (RFC 0007, *Summary text format*). -// In format 30 (RFC 0031 §6.1) the family is the effect's and the fresh -// value's. -// DUMP-LABEL: function 'xfree': -// DUMP: release *param0 free when always -// DUMP-LABEL: function 'opens': -// DUMP: result fresh#0 fclose {{.*}}when null nonnull -FILE *opens(const char *path) { return fopen(path, "r"); } - -// CHECK: 5 errors generated. diff --git a/test/Analysis/rfc0008-null.c b/test/Analysis/rfc0008-null.c deleted file mode 100644 index 11ec08b6..00000000 --- a/test/Analysis/rfc0008-null.c +++ /dev/null @@ -1,225 +0,0 @@ -// RFC 0008, *Nullness*: a pointer that is null or may be null on some path -// is not dereferenced, nor passed to a callee that dereferences it. Testing -// the pointer, or its callee's outcome, clears the fact on the surviving edge. -// RUN: not %weavec %s -- 2>&1 | FileCheck %s -// RUN: not %weavec --dump-analysis %s -- 2>&1 | FileCheck --check-prefix=DUMP %s -#include -#include -#include - -struct node { - int value; - struct node *next; -}; - -// The RFC's snippets that must be reported. - -int unchecked(void) { - struct node *n = malloc(sizeof *n); - n->value = 1; - free(n); - return 0; -} - -int constant(void) { - struct node *n = NULL; - // CHECK: rfc0008-null.c:[[@LINE+2]]:10: error: dereference of 'n', which is null [weavec::null-dereference] - // CHECK: rfc0008-null.c:[[@LINE-2]]:20: note: 'n' is assigned NULL here - return n->value; -} - -int tested_then_merged(struct node *n) { - if (n == NULL) - n = malloc(sizeof *n); - n->value = 0; - free(n); - return 0; -} - -// A callee's dereference becomes a `requires` fact; passing a maybe-null -// pointer to it is the error, at the call. -static int value_of(struct node *n) { return n->value; } - -int passes_null(void) { - struct node *n = malloc(sizeof *n); - int v = value_of(n); - free(n); - return v; -} - -// A callee that may return null hands the fact to its caller. -static struct node *make(int v) { - struct node *n = malloc(sizeof *n); - if (n) - n->value = v; - return n; -} - -int from_callee(void) { - struct node *n = make(1); - int v = n->value; - free(n); - return v; -} - -// A test that merges back leaves the place maybe-null with the test's note. -// RFC 0030 §7.5: 'value_of' is static and dereferences its parameter on every -// path, so each call must pass a non-null pointer; this one passes null. -int tested_then_passed(struct node *n) { - if (!n) { - // CHECK: rfc0008-null.c:[[@LINE+3]]:21: error: 'n', which is null, is passed to 'value_of', which dereferences it [weavec::null-dereference] - // CHECK: rfc0008-null.c:[[@LINE-2]]:8: note: 'n' may be null: it is compared with NULL here - // CHECK: rfc0008-null.c:{{[0-9]+}}:12: note: 'value_of' is declared here - return value_of(n); - } - return value_of(n); -} - -// Annotations (RFC 0008, *Annotations*): `WEAVEC_NULLABLE` on a result makes -// callers test it; `WEAVEC_NONNULL` on a parameter makes callers prove it. -// Neither says anything about ownership, so the RFC 0003 annotations stay. -extern struct node *lookup(int key) WEAVEC_BORROWED WEAVEC_NULLABLE; -extern int consume_nonnull(struct node *n WEAVEC_BORROWED WEAVEC_NONNULL); - -int annotated(void) { - struct node *n = lookup(1); - return n->value; -} - -int annotated_param(struct node *n) { - if (!n) { - // CHECK: rfc0008-null.c:[[@LINE+3]]:28: error: 'n', which is null, is passed to 'consume_nonnull', which dereferences it [weavec::null-dereference] - // CHECK: rfc0008-null.c:[[@LINE-2]]:8: note: 'n' may be null: it is compared with NULL here - // CHECK: rfc0008-null.c:[[@LINE-11]]:12: note: 'consume_nonnull' is declared here - return consume_nonnull(n); - } - return consume_nonnull(n); -} - -// Clean: every idiom for testing a pointer clears the fact on the edge -// where it is non-null, the fact travels with the value (so a copy taken -// before the test is cleared with it), and unknown pointers are trusted. -int clean(struct node *unknown) { - struct node *a = malloc(sizeof *a); - if (!a) - return 1; - a->value = 1; - struct node *b = malloc(sizeof *b); - struct node *b2 = b; - if (b == NULL) { - free(a); - return 1; - } - b2->value = a->value; - struct node *c = malloc(sizeof *c); - int v = c ? c->value : 0; - if (c && c->next) - v += c->next->value; - char *s = strdup("x"); - if (s != NULL) - s[0] = 'y'; - free(s); - free(c); - free(b); - free(a); - return v + unknown->value; -} - -// Clean: a redundant test of a pointer already known non-null (cJSON's -// `can_access_at_index` retests on every use) does not make it maybe-null -// once the edges merge; `(T *)0` is a null test like `NULL`; unchecked code -// may store through anything it is handed by address (RFC 0008, -// *Implementation notes*). -struct list { - unsigned len; - struct node **items; -}; -extern void fill(struct list *out); - -int redundant_tests(struct node *n) { - if (n == NULL || n->next == NULL) - return 0; - while (n != (struct node *)0 && n->value > 0 && n->next->value == 0) - n->value--; - return n->value; -} - -int filled_by_unchecked_code(void) { - struct list l = {0, NULL}; - // RFC 0030 §5.1: unknown code, a ledger row; what it reaches is unknown. - fill(&l); - if (l.len == 0) - return 0; - return l.items[0]->value; -} - -// A pointer that a callee's outcome makes non-null: the `notnull` fact. -// `open_node`'s allocation was never made on its zero class and is non-null -// on the other (RFC 0031 §6.3, `absent-on`): the failing branch leaks -// nothing. -static int open_node(struct node **out) { - *out = malloc(sizeof **out); - return *out != NULL; -} - -int outcome(void) { - struct node *n; - if (!open_node(&n)) - return 1; - n->value = 2; - free(n); - return 0; -} - -// A `fresh` store the callee did not perform on its failing class is in -// `null{...}` so the caller drops the record (RFC 0007); it says nothing -// about the value, which is what the caller stored there before. -struct buf { - char *data; - unsigned len; -}; - -static int grow(struct buf *b, unsigned n) { - if (n <= b->len) - return 0; - char *p = realloc(b->data, n); - if (p == NULL) - return -1; - b->data = p; - b->len = n; - return 0; -} - -void truncate_to(struct buf *b, unsigned n) { - if (grow(b, n) == -1 && n > b->len) - n = b->len; - b->data[n] = 0; -} - -// The summary vocabulary (RFC 0031 §6.1, format 30): `nonnull-on` per -// result class for a parameter the function dereferences (the old -// `requires{...}`), a `maybe-null` result or store (the old `null` among the -// returns), and effects and stores per result class. -// DUMP: function 'value_of': -// DUMP: nonnull-on zero param0 -// DUMP-NEXT: nonnull-on positive param0 -// DUMP-NEXT: nonnull-on negative param0 -// DUMP: function 'make': -// DUMP: result fresh#0 free extent 16 zeroed maybe-null when null nonnull -// DUMP: function 'redundant_tests': -// DUMP: result int [0, 0] when zero -// DUMP-NOT: nonnull-on zero -// DUMP: function 'open_node': -// DUMP: store *param0 := fresh#0 free extent 16 zeroed maybe-null -// DUMP: nonnull-on zero param0 -// DUMP: function 'grow': -// DUMP: result int [-1, -1] when negative -// DUMP: move *param0->data free may when result zero -// The replacement is stored only once `realloc` succeeded: not maybe-null. -// DUMP-NOT: maybe-null -// DUMP: store param0->data := fresh#0 free extent param1{{( zeroed)?}} may{{$}} -// DUMP: function 'truncate_to': -// DUMP-NOT: maybe-null -// DUMP: move *param0->data free may when always - -// CHECK: 3 errors generated. diff --git a/test/Analysis/rfc0008-replaced.c b/test/Analysis/rfc0008-replaced.c deleted file mode 100644 index e61d491e..00000000 --- a/test/Analysis/rfc0008-replaced.c +++ /dev/null @@ -1,107 +0,0 @@ -// RFC 0008, *Replaced values*: a callee that releases a caller-visible value -// and then reinitialises the place (`realloc`, `free` + `= NULL`) still -// consumed the caller's *old* value; a copy of it the caller kept is dead. -// Also *Struct-by-value results*: the pointer fields of a returned record are -// tracked in the caller through the `result` summary root. -// RUN: not %weavec %s -- 2>&1 | FileCheck %s -// RUN: not %weavec --dump-analysis %s -- 2>&1 | FileCheck --check-prefix=DUMP %s -#include - -struct vec { - int *items; - int cap; -}; - -// The RFC's motivating hole: the copy of the old items is used after `grow` -// moved it into `realloc`. -static int grow(struct vec *v) { - int *bigger = realloc(v->items, sizeof *bigger * (v->cap + 8)); - if (!bigger) - return 0; - v->items = bigger; - v->cap += 8; - return 1; -} - -int hole(struct vec *v) { - int *old = v->items; - if (!grow(v)) - return 1; - // RFC 0031 §5.11: the note comes from the release record, which a callee's - // summary effect makes at the call without the name it released through. - // CHECK: rfc0008-replaced.c:[[@LINE+2]]:10: error: use of 'old' after it was moved [weavec::use-after-move] - // CHECK: rfc0008-replaced.c:[[@LINE-5]]:8: note: moved here - return old[0]; -} - -// `free` then `= NULL`: the same, with `freed`. -static void reset(struct vec *v) { - free(v->items); - v->items = NULL; -} - -int reset_hole(struct vec *v) { - int *old = v->items; - reset(v); - // CHECK: rfc0008-replaced.c:[[@LINE+2]]:10: error: use of 'old' after it was freed [weavec::use-after-free] - // CHECK: rfc0008-replaced.c:[[@LINE-2]]:3: note: freed here - return old[0]; -} - -// Clean: the place itself holds the replacement, so using it is fine; a -// `replaced` path is not `freed` for the field's own next use. -int clean(struct vec *v) { - if (!grow(v)) - return 1; - v->items[0] = 1; - reset(v); - if (v->items) - return v->items[0]; - return 0; -} - -// Struct-by-value results: the caller owns `p.a` and `p.b` and must release -// both. -struct pair { - char *a; - char *b; -}; - -static struct pair make(void) { - struct pair p; - p.a = malloc(4); - p.b = malloc(4); - return p; -} - -int leaky(void) { - struct pair p = make(); - free(p.b); - // CHECK: rfc0008-replaced.c:[[@LINE+2]]:3: warning: 'p.a' is leaked [weavec::leak] - // CHECK: rfc0008-replaced.c:[[@LINE-3]]:19: note: allocated here - return 0; -} - -int tidy(void) { - struct pair p = make(); - free(p.a); - free(p.b); - return 0; -} - -// The summary vocabulary (RFC 0031 §6.1, format 30): a release or move -// per result class, a store of the replacement, `result` as a store root. -// DUMP: function 'grow': -// The success class moves the items into the new block; the failure class -// keeps them (RFC 0030 §8.2's release of a zero-size request does not -// arise: the zero-initialisation wrapper asks for one byte instead). -// DUMP: move *param0->items free when result positive -// DUMP: store param0->items := fresh#0 free {{.*}}when result positive -// DUMP: function 'reset': -// DUMP: release *param0->items free when always -// DUMP-NEXT: store param0->items := null -// DUMP: function 'make': -// DUMP: store result.a := fresh#0 free extent 4 -// DUMP-NEXT: store result.b := fresh#1 free extent 4 - -// CHECK: 1 warning and 2 errors generated. diff --git a/test/Analysis/rfc0008-uninit.c b/test/Analysis/rfc0008-uninit.c deleted file mode 100644 index e053ab4e..00000000 --- a/test/Analysis/rfc0008-uninit.c +++ /dev/null @@ -1,86 +0,0 @@ -// RFC 0008, *Uninitialised pointers*: a pointer variable, or a pointer field -// of a record variable, declared without an initialiser holds nothing until -// it is assigned; reading, dereferencing, copying or releasing it before then -// is an error. Initialising it by any route (assignment, a callee's store, a -// mutable borrow, `memset`) clears the fact. -// RUN: not %weavec %s -- 2>&1 | FileCheck %s -// RUN: not %weavec --dump-analysis %s -- 2>&1 | FileCheck --check-prefix=DUMP %s -#include -#include - -struct buf { - char *data; - int len; -}; - -// The RFC's snippets that must be reported. - -void local(void) { - char *p; - // RFC 0031 §5.9: the use is the released operand, not the call. - // CHECK: rfc0008-uninit.c:[[@LINE+2]]:8: error: use of 'p' before it was initialized [weavec::use-of-uninitialized] - // CHECK: rfc0008-uninit.c:[[@LINE-3]]:9: note: 'p' is declared here - free(p); -} - -int field(void) { - struct buf b; - b.len = 0; - // CHECK: rfc0008-uninit.c:[[@LINE+2]]:10: error: use of 'b.data' before it was initialized [weavec::use-of-uninitialized] - // CHECK: rfc0008-uninit.c:[[@LINE-3]]:14: note: 'b' is declared here - return b.data[0]; -} - -void copied(void) { - char *p; - // CHECK: rfc0008-uninit.c:[[@LINE+1]]:13: error: use of 'p' before it was initialized [weavec::use-of-uninitialized] - char *q = p; - free(q); -} - -int maybe(int c) { - char *p; - if (c) - p = malloc(4); - free(p); - return 0; -} - -// Clean: every way of initialising the place. -static void init(char **out) { *out = malloc(4); } - -int clean(int c) { - char *a; - a = malloc(4); - free(a); - char *b; - init(&b); - free(b); - struct buf s; - memset(&s, 0, sizeof s); - free(s.data); - struct buf t = {0}; - free(t.data); - static char *st; - free(st); - char *late; - if (c) - late = malloc(2); - else - late = NULL; - free(late); - return 0; -} - -// The record never reaches a summary: only locals can be uninitialised. -// DUMP: function 'local': -// DUMP-NEXT: summary: -// DUMP-NEXT: always-returns -// DUMP-NEXT: function 'field': -// DUMP: function 'maybe': -// DUMP-NEXT: summary: -// DUMP-NEXT: always-returns -// DUMP-NEXT: result int [0, 0] when zero -// DUMP-NEXT: function 'init': - -// CHECK: 3 errors generated. diff --git a/test/Analysis/rfc0009-arguments.c b/test/Analysis/rfc0009-arguments.c deleted file mode 100644 index 4f4fe95d..00000000 --- a/test/Analysis/rfc0009-arguments.c +++ /dev/null @@ -1,180 +0,0 @@ -// RFC 0009, *Deriving guards* and *Return alternatives*: a callee's consume, -// store or return that happens only under a fact about its parameters is -// summarised with a `when` guard; the caller translates the guard to its -// arguments and applies the effect only when its own facts do not refute it. -// RUN: not %weavec %s -- 2>&1 | FileCheck %s -// RUN: not %weavec --dump-analysis %s -- 2>&1 | FileCheck --check-prefix=DUMP %s -#include "../Inputs/prelude.h" -#include - -struct buf { - char *data; - int noalloc; -}; - -struct state { - char *msg; - int err; -}; - -// Lua's `l_alloc`: both arms consume `ptr` (`free` or `realloc`), so that -// effect is unconditional; only a non-zero size yields a fresh block. Each -// outcome class keeps its guard (RFC 0009, *Guards*): the null class frees -// `ptr` only for a zero size, so a caller that tests the result and knows -// the size is non-zero still owns the block. -// Format 30 (RFC 0031 §6.1) keys each effect by the result class and the -// size parameter's zero test. -// DUMP-LABEL: function 'l_alloc': -// DUMP: result null when null -// DUMP-NEXT: result fresh#0 free extent param3 {{.*}}when nonnull and param 3 !=0 -// DUMP-NEXT: release *param1 free when result null and param 3 =0 -// DUMP-NEXT: move *param1 free when result nonnull -void *l_alloc(void *ud, void *ptr, size_t osize, size_t nsize) { - (void)ud; - (void)osize; - if (nsize == 0) { - free(ptr); - return NULL; - } - return realloc(ptr, nsize); -} - -// Lua's `luaS_resize` shape: on failure the table is left as it was, which -// is a dangling `hash` only when the size was zero (the block was freed). -// The summary text joins the size-zero release into the move and keeps the -// replacement a possible store (RFC 0031 *Implementation amendments*, *Stores -// that keep the entry value possible*); `nsize * 8` is no parameter test. -// DUMP-LABEL: function 'resize_table': -// DUMP: move *param1->hash free when always -// DUMP-NEXT: store param1->hash := fresh#0 free {{.*}}may -struct table { - void **hash; - int size; -}; -void resize_table(void *ud, struct table *t, int nsize) { - void **nv = l_alloc(ud, t->hash, 8, nsize * sizeof(void *)); - if (nv == NULL) { - /* leave the table as it was */ - } else { - t->hash = nv; - t->size = nsize; - } -} - -// cJSON's `printbuffer` shape: the free depends on a flag in the object. -// The flag is no result class or parameter test, so the release is possible -// in the summary text; a caller whose record decides the flag gets the exact -// effect (`keep_static`, `free_heap`). -// DUMP-LABEL: function 'release': -// DUMP: release *param0->data free may when always -void release(struct buf *b) { - if (!b->noalloc) - free(b->data); -} - -// zlib's `gz_error`: the store depends on the argument being non-null. -// DUMP-LABEL: function 'gz_error': -// DUMP: store param0->msg := path param2 when param 2 !=0 -void gz_error(struct state *s, int err, char *msg) { - s->err = err; - if (msg != NULL) - s->msg = msg; -} - -// Clean callers: the argument decides. - -void grow(void *ud) { - char *p = malloc(8); - char *q = l_alloc(ud, p, 8, 16); - use(q); - free(q); -} - -void known_size(void *ud) { - size_t n = 32; - char *p = malloc(8); - char *q = l_alloc(ud, p, 8, n); - use(q); - free(q); -} - -void scaled_size(void *ud, int count) { - char *p = malloc(8); - if (count > 0) { - char *q = l_alloc(ud, p, 8, count * 8); - use(q); - free(q); - } else { - free(p); - } -} - -// The null edge frees `t->hash` only for a zero size, which this caller -// refuted: returning is neither a leak nor leaves a freed value behind. -void grow_or_keep(void *ud, struct table *t) { - if (t->size == 0) - return; - void **nv = l_alloc(ud, t->hash, 8, t->size * 2 * sizeof(void *)); - if (nv == NULL) - return; - t->hash = nv; -} - -void keep_static(void) { - char stack[8]; - struct buf b; - b.data = stack; - b.noalloc = 1; - release(&b); - use(b.data); -} - -void store_null(struct state *s) { - gz_error(s, 1, NULL); -} - -// Reported callers. - -// The discarded result is null here, not a leak; the block itself is gone. -// A zero size decides `l_alloc`'s parameter test, so the release is certain -// (RFC 0031 *Implementation amendments*, *Pending cases and exit splitting*). -void shrink(void *ud) { - char *p = malloc(8); - l_alloc(ud, p, 8, 0); - // CHECK: rfc0009-arguments.c:[[@LINE+1]]:7: error: use of 'p' after it was freed [weavec::use-after-free] - use(p); -} - -void unknown_size(void *ud, size_t n) { - char *p = malloc(8); - char *q = l_alloc(ud, p, 8, n); - // Freed for a zero size, moved when `realloc` succeeds, live when it fails: - // the untested result names the move of the non-null class (RFC 0031 §6.1). - // CHECK: rfc0009-arguments.c:[[@LINE+1]]:7: warning: use of 'p' after it may have been moved [weavec::use-after-move] - use(p); - free(q); -} - -void free_heap(void) { - struct buf b; - b.data = malloc(8); - b.noalloc = 0; - release(&b); - // CHECK: rfc0009-arguments.c:[[@LINE+1]]:7: error: use of 'b.data' after it was freed [weavec::use-after-free] - use(b.data); -} - -void unknown_flag(struct buf *b) { - release(b); - // CHECK: rfc0009-arguments.c:[[@LINE+1]]:7: warning: use of 'b->data' after it may have been freed [weavec::use-after-free] - use(b->data); -} - -void store_local(struct state *s) { - char local[8]; - // RFC 0033 §1: storing the address is no access; a warning. - // CHECK: rfc0009-arguments.c:[[@LINE+1]]:3: warning: 's->msg' may outlive 'local', which it points to [weavec::lifetime-too-short] - gz_error(s, 1, local); -} - -// CHECK: 3 warnings and 2 errors generated. diff --git a/test/Analysis/rfc0009-noreturn.c b/test/Analysis/rfc0009-noreturn.c deleted file mode 100644 index 2509227b..00000000 --- a/test/Analysis/rfc0009-noreturn.c +++ /dev/null @@ -1,114 +0,0 @@ -// RFC 0009, *Inferred `noreturn`*: a function whose exit is unreachable is -// summarised `never-returns`, transitively through wrappers, and a call to it -// ends the path like a call to a function declared `noreturn` does. -// RUN: %weavec %s -- 2>&1 | FileCheck %s -// RUN: %weavec --dump-analysis %s -- 2>&1 | FileCheck --check-prefix=DUMP %s -#include "../Inputs/prelude.h" -#include - -void abort(void); -typedef int jmp_buf[16]; -void longjmp(jmp_buf env, int val); -static jmp_buf env; - -// Unannotated wrappers around `abort` and `longjmp`. -// DUMP-LABEL: function 'die': -// DUMP-NEXT: summary: -// DUMP-NEXT: never-returns -static void die(const char *msg) { - use(msg); - abort(); -} - -// DUMP-LABEL: function 'fail': -// DUMP-NEXT: summary: -// DUMP-NEXT: never-returns -static void fail(int code) { - if (code > 3) - die("big"); - die("small"); -} - -// DUMP-LABEL: function 'throw_': -// DUMP-NEXT: summary: -// DUMP-NEXT: never-returns -static void throw_(int code) { - longjmp(env, code); -} - -// DUMP-LABEL: function 'spin': -// DUMP-NEXT: summary: -// DUMP-NEXT: never-returns -static void spin(void) { - for (;;) - ; -} - -// Returns on some paths: not `never-returns` (*Future work*). -// DUMP-LABEL: function 'check': -// DUMP-NOT: never-returns -// DUMP: function 'good_path': -static void check(int ok) { - if (!ok) - die("bad"); -} - -// Clean: the bad path never reaches the use. -void good_path(int bad) { - char *q = malloc(8); - if (bad) { - free(q); - fail(bad); - } - use(q); - free(q); -} - -void good_path_throw(int bad) { - char *q = malloc(8); - if (bad) { - free(q); - throw_(1); - } - use(q); - free(q); -} - -// Clean: nothing is leaked at the end of a block that is never left. -void no_leak_after_die(int bad) { - char *q = malloc(8); - if (bad) - die("bad"); - free(q); -} - -// Clean: code after the call is dead. -// DUMP-LABEL: function 'dead_tail': -// DUMP-NEXT: summary: -// DUMP-NEXT: never-returns -void dead_tail(char *p) { - free(p); - die("x"); - use(p); -} - -void spins(char *p) { - free(p); - spin(); - use(p); -} - -// Reported: `check` returns when `ok` is non-zero. -void check_returns(int bad) { - char *q = malloc(8); - if (bad) { - free(q); - check(bad); - } - // CHECK: rfc0009-noreturn.c:[[@LINE+1]]:7: warning: use of 'q' after it may have been freed [weavec::use-after-free] - use(q); - // CHECK: rfc0009-noreturn.c:[[@LINE+1]]:3: warning: 'q' may be freed twice [weavec::double-free] - free(q); -} - -// CHECK: 2 warnings generated. diff --git a/test/Analysis/rfc0009-replaced.c b/test/Analysis/rfc0009-replaced.c deleted file mode 100644 index 3c52d7fb..00000000 --- a/test/Analysis/rfc0009-replaced.c +++ /dev/null @@ -1,92 +0,0 @@ -// RFC 0009, *Deriving guards*, *Replaced values under a guard*: a value gone -// at the callee's exit only under a guard was replaced (or never consumed) -// on every other returning path, so the unreplaced consume the caller must -// see applies under that guard. RFC 0008, *Replaced values*: a replaced -// consume that finds the value already gone is one report, after which the -// place holds the callee's new value. -// RUN: not %weavec %s -- 2>&1 | FileCheck %s -// RUN: not %weavec --dump-analysis %s -- 2>&1 | FileCheck --check-prefix=DUMP %s -#include "../Inputs/prelude.h" - -struct L { - char *stack; -}; - -struct W { - struct L *L; -}; - -static void finish(struct L *L) { free(L->stack); } - -static void append(struct L *L, const char *b) { - free(L->stack); - L->stack = malloc(8); - use(b); -} - -// Lua's `str_writer`: a null block finishes (the stack is freed for good), -// anything else appends (the stack is freed and replaced). Format 30 (RFC -// 0031 §6.1) releases the entry value, which both arms free, and keys the -// store of the replacement by the block's zero test. -// DUMP-LABEL: function 'writer': -// DUMP: release *param0->stack free when always -// DUMP-NEXT: store param0->stack := fresh#0 free extent 8 {{.*}}when param 1 !=0 -void writer(struct L *L, const char *b) { - if (b == NULL) - finish(L); - else - append(L, b); -} - -// A buffer is never null: neither call frees the caller's stack. -void twice(struct L *L) { - char buf[4]; - writer(L, buf); - writer(L, buf); - use(L->stack); -} - -void twice_null(struct L *L) { - writer(L, NULL); - // CHECK: [[@LINE+1]]:3: error: 'L->stack' is freed twice [weavec::double-free] - writer(L, NULL); -} - -void unknown(struct L *L, const char *b) { - writer(L, b); - // CHECK: [[@LINE+1]]:3: warning: 'L->stack' may be freed twice [weavec::double-free] - writer(L, b); -} - -// `via` consumes `L->stack` through a local alias and its callee stores a -// new value there: the caller's place is replaced, and the object engine's -// store names it in the caller's terms (RFC 0031 §6.1, paths from the entry -// heap). -// DUMP-LABEL: function 'via': -// DUMP: release *param0->stack free when always -// DUMP-NEXT: store param0->stack := fresh#0 free extent 8 -static void through(struct W *w) { - free(w->L->stack); - w->L->stack = malloc(8); -} - -static void via(struct L *L) { - struct W w; - w.L = L; - through(&w); -} - -// One report, at the first call after the free; the calls after it use the -// value `via` left. -// DUMP-LABEL: function 'cascade': -// RFC 0013: the final allocation written through the local alias reaches callers. -// DUMP: release *param0->stack free when always -// DUMP-NEXT: store param0->stack := fresh#0 free extent 8 -void cascade(struct L *L) { - free(L->stack); - // CHECK: [[@LINE+1]]:3: error: 'L->stack' is freed twice [weavec::double-free] - via(L); - via(L); - via(L); -} -// CHECK-NOT: error: diff --git a/test/Analysis/rfc0009-scalars.c b/test/Analysis/rfc0009-scalars.c deleted file mode 100644 index 16a8be10..00000000 --- a/test/Analysis/rfc0009-scalars.c +++ /dev/null @@ -1,211 +0,0 @@ -// RFC 0009, *Scalar facts in the state* and *Refuting guards in the state*: -// a test of an integer place refines what is known about it, a move or a -// held resource made under a fact carries it as a guard, and a later test -// that contradicts the guard drops the record. -// RUN: %weavec %s -- 2>&1 | FileCheck %s -// RUN: %weavec --dump-analysis %s -- 2>&1 | FileCheck --check-prefix=DUMP %s -#include "../Inputs/prelude.h" -#include - -struct buf { - char *data; - int owned; -}; - -// Correct code: two tests of one integer are one test. The old engine -// carried the test as a guard on the move in its state and proved these -// uses. The object engine keys a release by the parameter's test only in the -// summary (RFC 0031 *Implementation amendments*, *Pending cases and exit -// splitting*, `paramGuard`, spelled `lossy ... when param ...`); in the -// state the object is `may-released` after the join, so each use is a -// possible finding: RFC 0030's accepted correlated-conditions false positive -// (*Accepted false positives and false traps*, probe 41e), which RFC 0031 -// *Accepted false positives* keeps. - -// DUMP-LABEL: function 'truthy': -// DUMP: release *param1 free lossy may when param 0 !=0 -void truthy(int c, char *p) { - if (c) - free(p); - if (!c) - // CHECK: rfc0009-scalars.c:[[@LINE+1]]:9: warning: use of 'p' after it may have been freed [weavec::use-after-free] - use(p); -} - -// DUMP-LABEL: function 'eqzero': -// DUMP: release *param1 free lossy may when param 0 =0 -void eqzero(int n, char *p) { - if (n == 0) - free(p); - if (n != 0) - // CHECK: rfc0009-scalars.c:[[@LINE+1]]:9: warning: use of 'p' after it may have been freed [weavec::use-after-free] - use(p); -} - -// A parameter test is a zero test: `n > 0` and `n == 3` key the release by -// `n != 0`, lossily (RFC 0031 §6.1). -// DUMP-LABEL: function 'sign': -// DUMP: release *param1 free lossy may when param 0 !=0 -void sign(int n, char *p) { - if (n > 0) - free(p); - if (n <= 0) - // CHECK: rfc0009-scalars.c:[[@LINE+1]]:9: warning: use of 'p' after it may have been freed [weavec::use-after-free] - use(p); -} - -// DUMP-LABEL: function 'constant': -// DUMP: release *param1 free lossy may when param 0 !=0 -void constant(int n, char *p) { - if (n == 3) - free(p); - if (n == 4) - // CHECK: rfc0009-scalars.c:[[@LINE+1]]:9: warning: use of 'p' after it may have been freed [weavec::use-after-free] - use(p); -} - -// DUMP-LABEL: function 'switched': -// DUMP: release *param1 free lossy may when param 0 =0 -void switched(int n, char *p) { - switch (n) { - case 0: - free(p); - break; - default: - break; - } - switch (n) { - case 1: - // CHECK: rfc0009-scalars.c:[[@LINE+1]]:9: warning: use of 'p' after it may have been freed [weavec::use-after-free] - use(p); - break; - default: - break; - } -} - -// A local assigned a constant: the `if (c)` edge is infeasible and the move -// never happens. The summary says nothing about `c` (it is not the caller's). -// DUMP-LABEL: function 'local_constant': -// DUMP: release *param0 free when always -void local_constant(char *p) { - int c = 0; - if (c) - free(p); - use(p); - free(p); -} - -// A field of the caller's object: no parameter test keys the release. -// DUMP-LABEL: function 'field': -// DUMP: release *param0->data free may when always -void field(struct buf *b) { - if (b->owned) - free(b->data); - if (!b->owned) - // CHECK: rfc0009-scalars.c:[[@LINE+1]]:9: warning: use of 'b->data' after it may have been freed [weavec::use-after-free] - use(b->data); -} - -// A held resource under a guard the early return's edge refutes: no leak. -int merged(int c) { - char *p = NULL; - if (c) - p = malloc(8); - if (!c) - return -1; - free(p); - return 0; -} - -// Reported: the tests do not exclude each other. -void overlap(int n, char *p) { - if (n > 0) - free(p); - if (n != 0) - // CHECK: rfc0009-scalars.c:[[@LINE+1]]:9: warning: use of 'p' after it may have been freed [weavec::use-after-free] - use(p); -} - -// Reported: the flag was reassigned from an unknown value, which drops the -// guard's conjunct and leaves the move unconditional. -void reassigned(int c, int d, char *p) { - if (c) - free(p); - c = d; - if (!c) - // CHECK: rfc0009-scalars.c:[[@LINE+1]]:9: warning: use of 'p' after it may have been freed [weavec::use-after-free] - use(p); -} - -// Reported: a computed expression establishes nothing about `n`. -void computed(int n, char *p) { - if (n == 0) - free(p); - if ((n & 1) != 0) - // CHECK: rfc0009-scalars.c:[[@LINE+1]]:9: warning: use of 'p' after it may have been freed [weavec::use-after-free] - use(p); -} - -// Reported: a guarded resource whose guard nothing refutes is still lost, -// at the branch that allocated it (RFC 0031 *Implementation amendments*, -// *Leaks on some paths*). -int leaked(int c) { - char *p = NULL; - if (c) - // CHECK: rfc0009-scalars.c:[[@LINE+2]]:5: warning: 'p' is leaked [weavec::leak] - // CHECK: rfc0009-scalars.c:[[@LINE+1]]:9: note: allocated here - p = malloc(8); - return 0; -} - -// RFC 0017: comparisons use the operands' C types, including full-width -// unsigned constants. `ULONG_MAX` is not signed `-1`, so `i > ULONG_MAX` -// fails; `(size_t)-1` is exactly SIZE_MAX, so the sentinel test succeeds. -static void touch(char *p) { p[0] = 1; } - -int above_max(void) { - size_t i = 0; - char *p = malloc(4); - if (i > 18446744073709551615UL) { free(p); return 1; } - touch(p); - free(p); - return 0; -} - -int sentinel(void) { - size_t n = (size_t)-1; - char *p = malloc(4); - if (n == (size_t)-1) { free(p); return 1; } - // Clean: the exact sentinel comparison makes this dereference unreachable. - touch(p); - free(p); - return 0; -} - -// `unsigned x = -1` is `UINT_MAX`, a positive value, so `x > 5` holds; a -// negative signed operand of an unsigned comparison ranks above every -// non-negative one. -void minus_one(void) { - unsigned x = -1; - int y = -1; - char *p = malloc(4); - if (x > 5) - touch(p); - free(p); - p = malloc(4); - if (y > 5u) - touch(p); - free(p); -} - -// Clean: the edge is dead in the mathematics too. -void dead(void) { - size_t i = 0; - char *p = malloc(4); - if (i != 0) - touch(p); - free(p); -} - -// CHECK: 10 warnings generated. diff --git a/test/Analysis/rfc0010-escapes.c b/test/Analysis/rfc0010-escapes.c deleted file mode 100644 index 1e09a83e..00000000 --- a/test/Analysis/rfc0010-escapes.c +++ /dev/null @@ -1,124 +0,0 @@ -// RFC 0010, *Stores out of sight*: a value copied into a node the callee -// allocates and links into the caller's container has a second home the -// summary cannot name; the callee says `escaped`, wrappers pass it on, and -// the caller reports neither the value nor the share it just took as leaked. -// RUN: %weavec %s -- 2>&1 | FileCheck %s -// Keep diagnostics separate so buffered stdout cannot split a dump label. -// RUN: %weavec --dump-analysis %s -- 2>%t.dump.err | FileCheck --check-prefix=DUMP %s -#include "../Inputs/prelude.h" -#include - -struct obj { - int rc; -}; - -static struct obj *obj_ref(struct obj *o) { - o->rc++; - return o; -} - -static void obj_unref(struct obj *o) { - if (--o->rc == 0) - free(o); -} - -struct pair { - struct pair *next; - struct obj *value; -}; - -struct table { - struct pair *first; -}; - -// `p->value = value` has no caller-visible destination; `value` escapes, and -// so does the old head, which lives on in the new node's `next`. The object -// engine names both homes: the new node's contents are stores below the -// stored object (RFC 0031 §6.3, *Amendment (heap outputs)*). -// DUMP-LABEL: function 'table_set': -// DUMP: store param0->first := fresh#0 free extent 16 zeroed when result zero -// DUMP-NEXT: store (new) param0->first->next := path param0->first -// DUMP-NEXT: store (new) param0->first->value := path param1 -static int table_set(struct table *t, struct obj *value) { - struct pair *p = malloc(sizeof *p); - if (!p) - return -1; - p->value = value; - p->next = t->first; - t->first = p; - return 0; -} - -// The wrapper that releases on failure: `escaped` and the conditional share -// release both reach the caller. -// DUMP-LABEL: function 'table_set_new': -// DUMP: release *param1 free may when result negative and param 1 !=0 -// DUMP: store param0->first := fresh#0 free extent 16 zeroed when result zero -// DUMP: store (new) param0->first->value := path param1 -static int table_set_new(struct table *t, struct obj *value) { - if (!value) - return -1; - if (table_set(t, value)) { - obj_unref(value); - return -1; - } - return 0; -} - -// The share-taking wrapper: `obj_ref(value)` is `value` or null, so `param 1` -// of the callee resolves to `value` and its count update, release and escape -// compose. The object engine does not infer RFC 0010 count functions yet, so -// the share release is a possible release of `value` (RFC 0031 §5.5; -// the retired golden comparison of RFC 0030). -// DUMP-LABEL: function 'table_set_shared': -// DUMP: release *param1 free may when result negative -// DUMP: store param1->rc := int -// DUMP: store (new) param0->first->value := path param1 -static int table_set_shared(struct table *t, struct obj *value) { - return table_set_new(t, obj_ref(value)); -} - -struct obj *iter_value(void *it); - -// Clean: the node keeps the owned object. -int owned_into_table(struct table *t) { - struct obj *o = malloc(sizeof *o); - if (!o) - return -1; - o->rc = 1; - return table_set(t, o); -} - -// Clean: the share the wrapper took went into the node. -int shares_into_table(struct table *t, void *it) { - struct obj *value; - while ((value = iter_value(it)) != NULL) { - if (table_set_shared(t, value)) - return -1; - } - return 0; -} - -// A node on the stack dies with the call: no escape (no store the caller -// could see). -// DUMP-LABEL: function 'use_locally': -// DUMP-NOT: store -// DUMP-LABEL: function 'still_leaks': -struct ctx { - struct obj *o; -}; -int use_locally(struct obj *o) { - struct ctx c; - struct ctx *p = &c; - p->o = o; - return p->o->rc; -} - -int still_leaks(void) { - struct obj *a = malloc(sizeof *a); - if (!a) - return -1; - // CHECK-NOT: leaked - // CHECK: rfc0010-escapes.c:[[@LINE+1]]:3: warning: 'a' is leaked [weavec::leak] - return use_locally(a); -} diff --git a/test/Analysis/rfc0010-outcomes.c b/test/Analysis/rfc0010-outcomes.c deleted file mode 100644 index 0a81ea18..00000000 --- a/test/Analysis/rfc0010-outcomes.c +++ /dev/null @@ -1,145 +0,0 @@ -// RFC 0010, *Per-outcome stores* and *Per-outcome integer facts*: a store a -// callee makes only on some outcome classes is retracted on the others, and -// what the callee left in the caller's integer memory per class is known -// after the call, which lets a wrapped decrement release a share. -// RUN: not %weavec %s -- 2>&1 | FileCheck %s -// RUN: not %weavec --dump-analysis %s -- 2>&1 | FileCheck --check-prefix=DUMP %s -#include "../Inputs/prelude.h" -#include - -// -- Per-outcome stores ------------------------------------------------------ - -struct bag { - char *items[8]; - int n; -}; - -// The store happens on the zero class only; the negative class leaves the -// caller's memory alone and carries the test that failed. -// RFC 0017: the requirement uses entry n, before the postfix increment. The -// eight-byte slot starts at n*8 and ends at n*8+8; the typed guard excludes 8. -// The object engine exports the store through the variable index `b->n` as a -// possible store to some element, without the result class that makes it -// (RFC 0031 §4.9, *Summaries*: a range whose bounds cannot be expressed is -// exported as a possible effect on some elements); a store at a constant -// index keeps `when result zero`. -// DUMP-LABEL: function 'bag_put': -// DUMP: result int [-1, -1] when negative -// DUMP: result int [0, 0] when zero -// DUMP: store param0->items[*] := path param1 may -static int bag_put(struct bag *b, char *s) { - if (b->n == 8) - return -1; - b->items[b->n++] = s; - return 0; -} - -// Clean: on the failure edge the bag does not hold `s`, so freeing it is -// this function's job; on success it escaped into the bag. -int put_or_free(struct bag *b) { - char *s = malloc(8); - if (!s) - return -1; - if (bag_put(b, s) < 0) { - free(s); - return -1; - } - return 0; -} - -// The failure edge without the free: `s` is still this function's. -int put_and_forget(struct bag *b) { - char *s = malloc(8); - if (!s) - return -1; - // The old engine reported `'s' is leaked` on the failure edge below. The - // object engine's summary of `bag_put` stores `s` into the bag possibly on - // every class (above), so on that edge `s` may have escaped and no leak is - // reported: a lost warning, never a proof (the retired golden comparison of RFC 0030, - // *Lit tests*). - if (bag_put(b, s) < 0) - return -1; - return 0; -} - -// -- Per-outcome integer facts ----------------------------------------------- - -struct obj { - int rc; -}; - -// The wrapped decrement: `*r` is zero exactly on the positive class. The -// comparison splits the exit by its truth (RFC 0031 *Pending cases and exit -// splitting*); the written count is exported as an interval, not per class -// (RFC 0031 §6.1 drops the per-outcome integer facts of format 29). -// DUMP-LABEL: function 'dec_and_test': -// DUMP: result int [0, 0] when zero and param 0 !=0 -// DUMP: result int [1, 1] when positive and param 0 !=0 -// DUMP: store *param0 := int [-2147483648, 2147483647] -static int dec_and_test(int *r) { return --*r == 0; } - -// Through the helper, the unref releases `o` when the count reaches zero. -// The object engine does not infer RFC 0010 count functions yet, so it is a -// possible release (RFC 0031 §5.5; the retired golden comparison of RFC 0030). -// DUMP-LABEL: function 'obj_unref': -// DUMP: release *param0 free may when always -// DUMP: store param0->rc := int -static void obj_unref(struct obj *o) { - if (dec_and_test(&o->rc)) - free(o); -} - -static struct obj *obj_new(void) { - struct obj *o = malloc(sizeof *o); - if (!o) - return NULL; - o->rc = 1; - return o; -} - -int twice(void) { - struct obj *a = obj_new(); - if (!a) - return -1; - obj_unref(a); - // `a->rc` is 1, so the first call frees `a`; the second call's first access - // is the read of `o->rc` in `dec_and_test`, a use of the freed object before - // its second release (RFC 0031 §6.6, *Amendment (numeric contexts)*: the - // call is analysed with the count the caller knows; with an unknown count - // the finding is a warning). - // CHECK: rfc0010-outcomes.c:[[@LINE+1]]:13: error: use of 'a' after it was freed [weavec::use-after-free] - obj_unref(a); - return 0; -} - -// A caller learns the callee's facts on the edge it takes. -struct box { - int filled; - char *p; -}; -// DUMP-LABEL: function 'fill': -// DUMP: result int [-1, -1] when negative -// DUMP: result int [0, 0] when zero -// DUMP: store param0->p := path param1 when result zero -static int fill(struct box *b, char *p) { - if (!p) { - b->filled = 0; - return -1; - } - b->p = p; - b->filled = 1; - return 0; -} - -// Clean: on the failure edge `b->filled` is zero, so the use is unreachable -// and `p` was never stored. -void consumer(struct box *b) { - char *p = malloc(8); - if (fill(b, p) < 0) { - if (b->filled) - use(b->p); - free(p); - } -} - -// CHECK: 1 error generated. diff --git a/test/Analysis/rfc0010-refcount.c b/test/Analysis/rfc0010-refcount.c deleted file mode 100644 index 53ca4a4c..00000000 --- a/test/Analysis/rfc0010-refcount.c +++ /dev/null @@ -1,206 +0,0 @@ -// RFC 0010, *Inferring reference counts*, *Shares* and *Bugs caught*: a -// count increment retains a share on the holder, a zero-guarded free after -// a decrement releases one, copies of a holder with a surplus take their own -// share, and a plain free kills every name. -// RUN: not %weavec %s -- 2>&1 | FileCheck %s -// RUN: not %weavec --dump-analysis %s -- 2>&1 | FileCheck --check-prefix=DUMP %s -#include "../Inputs/prelude.h" -#include - -struct obj { - int rc; - char *name; -}; - -static struct obj *obj_new(void) { - struct obj *o = malloc(sizeof *o); - if (!o) - return NULL; - o->rc = 1; - o->name = malloc(4); - return o; -} - -// The returning ref: a store to the count, the argument copied out. -// DUMP-LABEL: function 'obj_ref': -// DUMP: result path param0 when nonnull -// DUMP: store param0->rc := int -static struct obj *obj_ref(struct obj *o) { - o->rc++; - return o; -} - -// The unref: a release guarded by the count. The object engine does not infer -// RFC 0010 count functions yet, so it is a possible release of `o` and its -// name (RFC 0031 §5.5; the retired golden comparison of RFC 0030). -// DUMP-LABEL: function 'obj_unref': -// DUMP: release *param0 free may when always -// DUMP: release *param0->name free may when always -static void obj_unref(struct obj *o) { - if (--o->rc == 0) { - free(o->name); - free(o); - } -} - -// The other spellings of the decrement (RFC 0010, *Recognising increments -// and decrements*). -// DUMP-LABEL: function 'unref_post': -// DUMP: release *param0 free may when always -static void unref_post(struct obj *o) { - if (o->rc-- == 1) - free(o); -} -// DUMP-LABEL: function 'unref_atomic': -// DUMP: release *param0 free may when always -static void unref_atomic(struct obj *o) { - if (__atomic_fetch_sub(&o->rc, 1, __ATOMIC_ACQ_REL) == 1) - free(o); -} -// DUMP-LABEL: function 'unref_sync': -// DUMP: release *param0 free may when always -static void unref_sync(struct obj *o) { - if (__sync_sub_and_fetch(&o->rc, 1) == 0) - free(o); -} - -// A free not guarded by the count reaching zero is a plain free. -// DUMP-LABEL: function 'not_a_release': -// DUMP: release *param0 free when always -static void not_a_release(struct obj *o) { - o->rc--; - free(o); -} - -// Clean: a share taken and given back, through a copy or the holder itself. -int balanced(void) { - struct obj *a = obj_new(); - if (!a) - return -1; - struct obj *b = obj_ref(a); - obj_unref(b); - use(a->name); - obj_ref(a); - obj_unref(a); - use(a->name); - obj_unref(a); - return 0; -} - -// Clean: a stored copy carries the surplus share; releasing the source -// leaves it. -struct holder { - struct obj *o; -}; -int stored_share(struct holder *h) { - struct obj *a = obj_new(); - if (!a) - return -1; - h->o = obj_ref(a); - obj_unref(a); - return h->o->rc; -} - -// Clean: a share retained on a parameter is the caller's business; a -// release of a share this function does not own is a discipline. -// Without RFC 0010 count inference the object engine gives a possible use -// after free here: a warning on correct code (RFC 0031 §5.5; -// the retired golden comparison of RFC 0030, *Lit tests*). -void keep(struct obj *o) { obj_ref(o); } -int retained_then_released(struct obj *o) { - obj_ref(o); - obj_unref(o); - // CHECK: rfc0010-refcount.c:[[@LINE+1]]:10: warning: use of 'o' after it may have been freed [weavec::use-after-free] - return o->rc; -} - -// Bugs. The counts below are known where each call is made, so each call is -// analysed with them (RFC 0031 §6.6, *Amendment (numeric contexts)*) and the -// releases are definite frees: the third unref's first access of the freed -// object is its read of `o->rc`, a use before the second release. -int one_release_too_many(void) { - struct obj *a = obj_new(); - if (!a) - return -1; - obj_ref(a); - obj_unref(a); - obj_unref(a); - // CHECK: rfc0010-refcount.c:[[@LINE+1]]:13: error: use of 'a' after it was freed [weavec::use-after-free] - obj_unref(a); - // CHECK: rfc0010-refcount.c:[[@LINE-3]]:3: note: freed here - return 0; -} - -int use_after_last(void) { - struct obj *a = obj_new(); - if (!a) - return -1; - obj_unref(a); - // CHECK: rfc0010-refcount.c:[[@LINE+1]]:10: error: use of 'a' after it was freed [weavec::use-after-free] - return a->rc; - // CHECK: rfc0010-refcount.c:[[@LINE-3]]:3: note: freed here -} - -// The old engine reported a definite use after the share release. Without -// count inference the object engine reports a possible use after free: an -// error became a warning, the facet is still not proven (RFC 0031 §5.5; -// the retired golden comparison of RFC 0030, *Lit tests*). -int released_borrow(struct obj *o) { - obj_unref(o); - // CHECK: rfc0010-refcount.c:[[@LINE+1]]:10: warning: use of 'o' after it may have been freed [weavec::use-after-free] - return o->rc; - // CHECK: rfc0010-refcount.c:[[@LINE-3]]:3: note: freed here on some paths -} - -int plain_free_kills_shares(void) { - struct obj *a = obj_new(); - if (!a) - return -1; - struct obj *b = obj_ref(a); - // RFC 0013: obj_new's owned name is visible through the result; the object - // engine names it by the call that made it (RFC 0031 §5.8, §5.11). - // CHECK: rfc0010-refcount.c:[[@LINE+1]]:3: warning: result of 'obj_new' is leaked [weavec::leak] - free(a); - // CHECK: rfc0010-refcount.c:[[@LINE+1]]:10: error: use of 'b' after it was freed [weavec::use-after-free] - return b->rc; - // CHECK: rfc0010-refcount.c:[[@LINE-3]]:3: note: freed here (through 'a') -} - -// Leaks of shares (RFC 0010, *Leaks of shares*): the caller of `keep` loses -// the share it took; a local retained and dropped is a leak because -// `struct obj.rc` is a known count (`obj_unref` releases through it). -int caller_loses_share(void) { - struct obj *a = obj_new(); - if (!a) - return -1; - keep(a); - obj_unref(a); - // `a` keeps the share `keep` took, and with it `a->name`, which the object - // engine reports too (the second leak, named by the call that made it). - // CHECK: rfc0010-refcount.c:[[@LINE+2]]:10: warning: 'a' is leaked [weavec::leak] - // CHECK: rfc0010-refcount.c:[[@LINE+1]]:10: warning: result of 'obj_new' is leaked [weavec::leak] - return 0; -} -struct list { - struct obj *head; -}; -// The old engine reported `'p' is leaked` (note: reference taken here) at the -// `obj_ref`. The object engine does not infer the count, so a share taken on -// a borrowed object is not an owned object and its leak is not reported: a -// lost warning (RFC 0031 §5.5; the retired golden comparison of RFC 0030, *Lit tests*). -void local_retained(struct list *l) { - struct obj *p = l->head; - obj_ref(p); -} - -// A field nobody releases through is not a count: no leak. -struct sized { - int len; - char *buf; -}; -void not_a_count(struct sized *s) { - struct sized *t = s; - t->len++; -} - -// CHECK: 5 warnings and 3 errors generated. diff --git a/test/Analysis/rfc0011-bounds.c b/test/Analysis/rfc0011-bounds.c deleted file mode 100644 index 68260354..00000000 --- a/test/Analysis/rfc0011-bounds.c +++ /dev/null @@ -1,311 +0,0 @@ -// RFC 0011, *Extents* and *Bounds checks*: an allocation or declaration -// gives an object its extent; an access at a constant or symbolic offset -// the extent cannot hold is `out-of-bounds`, with the index as written and -// the object's origin in a note. -// RUN: not %weavec %s -- -ferror-limit=0 2>&1 | FileCheck %s -// RUN: not %weavec --dump-analysis %s -- 2>/dev/null | FileCheck --check-prefix=DUMP %s -#include "../Inputs/prelude.h" -#include - -void *calloc(size_t, size_t); -void *memset(void *, int, size_t); - -struct rec { char name[8]; int id; }; -struct flex { int n; char data[]; }; - -// -- Constants ---------------------------------------------------------------- - -void declared(void) { - char buf[10]; - buf[9] = 0; - // CHECK: rfc0011-bounds.c:[[@LINE+1]]:3: error: 'buf[10]' is out of bounds: index 10 of an object of 10 bytes [weavec::out-of-bounds] - buf[10] = 0; - // CHECK: rfc0011-bounds.c:[[@LINE-4]]:8: note: 'buf' is declared here -} - -// The extent of the allocation is in the summary of what returns it. -// DUMP-LABEL: function 'eight': -// DUMP: result fresh#0 free extent 8 zeroed maybe-null when null nonnull -static char *eight(void) { return malloc(8); } - -void heap(void) { - char *p = eight(); - if (!p) - return; - p[7] = 0; - // CHECK: rfc0011-bounds.c:[[@LINE+1]]:3: error: 'p[8]' is out of bounds: index 8 of an object of 8 bytes [weavec::out-of-bounds] - p[8] = 0; - // CHECK: rfc0011-bounds.c:[[@LINE-6]]:13: note: 'p' is allocated here - free(p); -} - -// A folded constant shows its value. -void folded(void) { - int data = 10; - int buffer[10]; - // CHECK: rfc0011-bounds.c:[[@LINE+1]]:3: error: 'buffer[data]' is out of bounds: index 'data' (10) of an object of 40 bytes [weavec::out-of-bounds] - buffer[data] = 1; -} - -void deref(void) { - int *q = malloc(2 * sizeof *q); - if (!q) - return; - *(q + 1) = 1; - // CHECK: rfc0011-bounds.c:[[@LINE+1]]:3: error: '*(q + 2)' is out of bounds: index 2 of an object of 8 bytes [weavec::out-of-bounds] - *(q + 2) = 1; - free(q); -} - -// A field past an allocation too small for its struct. -void short_struct(void) { - struct rec *r = malloc(8); - if (!r) - return; - r->name[0] = 0; - // CHECK: rfc0011-bounds.c:[[@LINE+1]]:3: error: 'r->id' is out of bounds: it reaches 12 bytes into 'r', which has 8 bytes [weavec::out-of-bounds] - r->id = 1; - free(r); -} - -// An array member is an object of its own inside its struct. -void member_array(struct rec *r) { - r->name[7] = 0; - // CHECK: rfc0011-bounds.c:[[@LINE+1]]:3: error: 'r->name[8]' is out of bounds: index 8 of an object of 8 bytes [weavec::out-of-bounds] - r->name[8] = 0; - // CHECK: rfc0011-bounds.c:13:19: note: 'r->name' is declared here -} - -// -- Before the start --------------------------------------------------------- - -void negative(void) { - char buf[16]; - char *p = buf; - // CHECK: rfc0011-bounds.c:[[@LINE+1]]:3: error: 'p[-1]' is out of bounds: index -1 is before the start of 'p' [weavec::out-of-bounds] - p[-1] = 0; - // CHECK: rfc0011-bounds.c:[[@LINE-4]]:8: note: the object behind 'p' is declared here -} - -void underwrite(void) { - char *buf = malloc(16); - if (!buf) - return; - char *p = buf - 8; - // CHECK: rfc0011-bounds.c:[[@LINE+1]]:3: error: 'p[0]' is out of bounds: index 0 is before the start of 'p' [weavec::out-of-bounds] - p[0] = 'A'; - free(buf); -} - -// -- The pointer's own position counts ---------------------------------------- - -void stepped(void) { - int *q = malloc(8 * sizeof *q); - if (!q) - return; - int *p = q + 2; - p[5] = 0; - // CHECK: rfc0011-bounds.c:[[@LINE+1]]:3: error: 'p[6]' is out of bounds: index 6 of an object of 32 bytes [weavec::out-of-bounds] - p[6] = 0; - free(q); -} - -void walked(void) { - char *s = malloc(4); - if (!s) - return; - char *p = s; - p++; - p++; - p++; - *p = 0; - p++; - // CHECK: rfc0011-bounds.c:[[@LINE+1]]:3: error: '*p' is out of bounds: it reaches 5 bytes into 'p', which has 4 bytes [weavec::out-of-bounds] - *p = 0; - free(s); -} - -// -- Symbolic extents --------------------------------------------------------- - -// RFC 0017 keeps the conversion to size_t in allocation summaries. Callers -// below guard positive counts so their examples retain mathematical extents. -// The old engine's summaries carried `extent=mul(4, u64(n))`. Format 30 has no -// numeric output expressions (RFC 0031 §6.1), and `n * sizeof(int)` of an -// `int` (converted, it may wrap) is no extent term over `n` (RFC 0031 §6.2; -// as the *Variable-length arrays* amendment says, a byte size that may wrap is -// no extent), so the results below have none: the accesses through them that -// the old engine reported are `unresolved(unknown-extent)`, never proven -// (the retired golden comparison of RFC 0030, *Lit tests*). -// DUMP-LABEL: function 'ints': -// DUMP: result fresh#0 free zeroed maybe-null when null nonnull -static int *ints(int n) { return malloc(n * sizeof(int)); } -// DUMP-LABEL: function 'zeroed': -// DUMP: result fresh#0 free zeroed maybe-null when null nonnull -static int *zeroed(int n) { return calloc(n, sizeof(int)); } - -void at_n(int n) { - if (n <= 0) - return; - int *p = ints(n); - if (!p) - return; - p[n - 1] = 0; - // Old engine: error: 'p[n]' is out of bounds: 'n' is the number of elements - // of 'p'. Object engine: no extent through `ints` (above). - p[n] = 0; - free(p); -} - -void bytes(size_t n) { - // Leave room for the terminator without wrapping the allocation size. - if (n == (size_t)-1) - return; - char *p = malloc(n + 1); - if (!p) - return; - p[n] = 0; - // CHECK: rfc0011-bounds.c:[[@LINE+1]]:3: error: 'p[n + 1]' is out of bounds: it reaches 'n' + 2 bytes into 'p', which has 'n' + 1 bytes [weavec::out-of-bounds] - p[n + 1] = 0; - free(p); -} - -// A write to the counter forgets the extent expressed in it. -void counter_moves(int n) { - int *p = zeroed(n); - if (!p) - return; - n = n + 1; - p[n - 1] = 0; - free(p); -} - -// The flexible-array idiom: `sizeof *f + n` bytes hold `data[0..n-1]`, and -// one more is the classic off-by-one. -void flexible(int n) { - if (n <= 0) - return; - struct flex *f = malloc(sizeof *f + n); - if (!f) - return; - f->data[n - 1] = 0; - // CHECK: rfc0011-bounds.c:[[@LINE+1]]:3: error: 'f->data[n]' is out of bounds: it reaches 'n' + 5 bytes into 'f', which has 'n' + 4 bytes [weavec::out-of-bounds] - f->data[n] = 0; - free(f); -} - -// -- Relations ---------------------------------------------------------------- - -void loops(int n) { - // The inclusive loop must also be able to increment at its boundary. - if (n <= 0 || n == __INT_MAX__) - return; - int *p = ints(n); - if (!p) - return; - for (int i = 0; i < n; i++) - p[i] = 0; - for (int i = 0; i <= n; i++) - p[i] = 0; - for (int i = 0; i < n; i++) - p[i + 1] = 0; - for (int i = 0; i < n - 1; i++) - p[i + 1] = 0; - free(p); -} - -void guards(int i, int n) { - if (i < 0 || n <= 0) - return; - int *p = ints(n); - if (!p) - return; - if (i < n) - p[i] = 1; - // Old engine: the three accesses below were errors ('i' is at least 'n' / - // above 'n', the number of elements of 'p'). Object engine: no extent - // through `ints` (above), so they are not proven. - if (i >= n) - p[i] = 1; - if (i > n) - p[i] = 1; - if (n <= i) - p[i] = 1; - free(p); -} - -// A copy of the index carries the relation; a write to it drops it. -void copies(int i, int n) { - if (i < 0 || n <= 0) - return; - int *p = ints(n); - if (!p) - return; - int j = i; - // Old engine: error: 'p[i]' is out of bounds: 'entry(i)' is at least 'n'. - // Object engine: no extent through `ints` (above), not proven. - if (j >= n) - p[i] = 1; - if (i >= n) { - i = 0; - p[i] = 1; - } - free(p); -} - -// -- Library calls ------------------------------------------------------------ - -void clears(size_t n) { - if (n == (size_t)-1) - return; - char *p = malloc(n); - if (!p) - return; - memset(p, 0, n); - // CHECK: rfc0011-bounds.c:[[@LINE+1]]:10: error: 'memset' accesses 'n' + 1 bytes of 'p', which has 'n' bytes [weavec::out-of-bounds] - memset(p, 0, n + 1); - free(p); -} - -// -- Constant upper bounds ---------------------------------------------------- - -// `i < 8` bounds `i` above (RFC 0011, *Relations*): the loop's boundary may -// reach past a smaller object; a guard that fits proves the access. -void counted(void) { - char buf[4]; - for (int i = 0; i < 8; i++) - buf[i] = 0; - // RFC 0030 §3.3: `buf[i]` may reach 7 here, a checked facet. - for (int i = 0; i < 4; i++) - buf[i] = 0; - for (int i = 0; i <= 4; i++) - buf[i] = 0; -} - -void guarded(int i) { - int ints[8]; - if (i >= 0 && i < 8) - ints[i] = 0; - if (i >= 0 && i <= 8) - ints[i] = 0; -} - -// An object of at most `n <= 4` bytes cannot hold a constant access past 4. -void small_object(int n) { - if (n < 0 || n > 4) - return; - char *p = malloc(n); - if (!p) - return; - p[3] = 0; - // CHECK: rfc0011-bounds.c:[[@LINE+1]]:3: error: 'p[4]' is out of bounds: index 4 of an object of 'n' bytes [weavec::out-of-bounds] - p[4] = 0; - free(p); -} - -// A length bounded above by a constant, against a constant extent. -void bounded_length(size_t n) { - char buf[4]; - if (n < 5) - memset(buf, 0, n); - if (n < 9) - memset(buf, 0, n); -} diff --git a/test/Analysis/rfc0011-derived.c b/test/Analysis/rfc0011-derived.c deleted file mode 100644 index c31200ce..00000000 --- a/test/Analysis/rfc0011-derived.c +++ /dev/null @@ -1,239 +0,0 @@ -// RFC 0011, *Derived pointers*: a pointer into an object is a copy of its -// base at an offset. A release through a field pointer reaches the summary; -// `container_of` walks back to the allocation; arithmetic that returns to -// the start is a release of the start; anything else is `invalid-release` -// with the offset named. -// RUN: not %weavec %s -- 2>&1 | FileCheck %s -// RUN: not %weavec --dump-analysis %s -- 2>/dev/null | FileCheck --check-prefix=DUMP %s -#include "../Inputs/prelude.h" -#include - -#define offsetof(T, f) __builtin_offsetof(T, f) -#define container_of(p, T, f) ((T *)((char *)(p) - offsetof(T, f))) - -struct inner { char *buf; }; -struct outer { struct inner in; int k; }; -struct link { struct link *next; }; -struct list { struct link *head; }; -struct node { struct link link; int v; }; - -// -- A field pointer is the object at that field ----------------------------- - -// The release through `i` is a release of `o->in.buf` in the summary. -// DUMP-LABEL: function 'release_inner': -// DUMP: release *param0->in.buf free when always -static void release_inner(struct outer *o) { - struct inner *i = &o->in; - free(i->buf); -} - -void caller(struct outer *o) { - release_inner(o); - // CHECK: rfc0011-derived.c:[[@LINE+1]]:7: error: use of 'o->in.buf' after it was freed [weavec::use-after-free] - use(o->in.buf); - // CHECK: rfc0011-derived.c:[[@LINE-3]]:3: note: freed here -} - -// Freeing the object while a field pointer is still used is a use after -// free through the field pointer (not a conflicting borrow). -void through_field(void) { - struct outer *o = malloc(sizeof *o); - if (!o) - return; - struct inner *i = &o->in; - free(o); - // CHECK: rfc0011-derived.c:[[@LINE+1]]:7: error: use of 'i' after it was freed [weavec::use-after-free] - use(i); -} - -// -- container_of ------------------------------------------------------------- - -// Freed at minus the offset of `in`: the summary says so, and the caller -// composes it with the field pointer it passed. The object engine names -// offsets in bytes (RFC 0031 *Implementation amendments*, *Interior -// releases*), and `in` is at byte 0, so the release is at the start. -// DUMP-LABEL: function 'free_container': -// DUMP: release *param0 free when always -void free_container(struct inner *i) { - free(container_of(i, struct outer, in)); -} - -// Clean: `&o->in` at `+in`, freed at `-in`, is `o` freed at the start. -void use_container(void) { - struct outer *o = malloc(sizeof *o); - if (!o) - return; - free_container(&o->in); -} - -// Clean: the member pointer handed out is the fresh object at the field -// (byte 0, so no offset). -// DUMP-LABEL: function 'make_inner': -// DUMP: result null when null -// DUMP: result fresh#0 free extent 16 zeroed when nonnull -struct inner *make_inner(void) { - struct outer *o = malloc(sizeof *o); - if (!o) - return 0; - return &o->in; -} - -// Clean: the intrusive list takes `&n->link`, which is `n` at offset zero. -static void push(struct list *l, struct link *WEAVEC_OWNED n) { - n->next = l->head; - l->head = n; -} -void link_node(struct list *l) { - struct node *n = malloc(sizeof *n); - if (!n) - return; - push(l, &n->link); -} - -// -- Releases away from the start --------------------------------------------- - -// The old engine reported `free(&o->in)` as `points to field 'in'`. `in` is -// the first member, so `&o->in` is the allocation's start and the release is -// valid: RFC 0031 §5.5 makes a release invalid only at an offset that is -// provably non-zero. A field past the start keeps the message. -void field_release(void) { - struct outer *o = malloc(sizeof *o); - if (!o) - return; - // CHECK: rfc0011-derived.c:[[@LINE+1]]:3: error: 'o' is released but points to field 'k' of its allocation [weavec::invalid-release] - free(&o->k); - // CHECK: rfc0011-derived.c:[[@LINE-5]]:21: note: allocated here -} - -// Clean: the first member is at the start. -void first_field_release(void) { - struct outer *o = malloc(sizeof *o); - if (!o) - return; - free(&o->in); -} - -// Clean: `p - 3` is `s` again, and the summary says `s` is freed at zero. -// DUMP-LABEL: function 'rebase': -// DUMP: release *param0 free when always -void rebase(char *WEAVEC_OWNED s) { - char *p = s + 3; - free(p - 3); -} - -// DUMP-LABEL: function 'bad_rebase': -// DUMP: release *param0 free offset 1 when always -void bad_rebase(char *WEAVEC_OWNED s) { - char *p = s + 3; - // CHECK: rfc0011-derived.c:[[@LINE+1]]:3: error: 'p' is released but points 1 element past the start of its allocation [weavec::invalid-release] - free(p - 2); -} - -// Four `int` elements are 16 bytes. -// DUMP-LABEL: function 'stepped': -// DUMP: release *param0 free offset 16 when always -void stepped(int *WEAVEC_OWNED s) { - s += 4; - // CHECK: rfc0011-derived.c:[[@LINE+1]]:3: error: 's' is released but points 4 elements past the start of its allocation [weavec::invalid-release] - free(s); -} - -// Clean: the walking pointer is separated from `s` by `!=`-free stepping; -// `s` itself is still at the start. -void walk(char *WEAVEC_OWNED s) { - char *p = s; - while (*p) - p++; - free(s); -} - -// Clean: a loop that steps up and back down leaves the offset unknown, and -// an unknown offset is not reported (RFC 0011, *Accepted false positives* -// lists what is; this is not). -void walk_back(char *WEAVEC_OWNED s, int n) { - char *p = s; - for (int i = 0; i < n; i++) - p++; - for (int i = 0; i < n; i++) - p--; - use(p); - free(s); -} - -// -- Derived pointers and nullness ------------------------------------------- - -typedef struct frame { int x; int trap; struct frame *next; } frame; -typedef struct state { frame *ci; frame base_ci; } state; - -// A callee returning `&L->base_ci` hands out `L` at that field. -// DUMP-LABEL: function 'precall': -// DUMP: result null when null and param 1 !=0 -// DUMP: result path param0 offset 8 when nonnull -frame *precall(state *L, int c) { - if (c) - return NULL; - return &L->base_ci; -} - -// Clean: on some iteration `ci` and `newci` are both `L` at `base_ci`, so -// they are exact aliases there; but the relation is a may-relation after the -// join, and `newci == NULL` says nothing definite about `ci`. -int execute(state *L, frame *ci, int c) { - int v; -startfunc: - v = ci->x; - { - frame *newci; - if ((newci = precall(L, c)) == NULL) - v = ci->trap; - else { - ci = newci; - goto startfunc; - } - } - return v; -} - -// A local that equals a caller's place is named by it, not by the derived -// name it may also carry (Lua's `ci = L->ci = next_ci(L)`). `L->ci` ends as -// `L->base_ci.next` or the entry `L->ci->next`, which no one format-30 value -// spells (RFC 0031 §6.1), so the store and the result are `unknown`. -// DUMP-LABEL: function 'next_frame': -// DUMP: result unknown maybe-null when null nonnull -// DUMP: store param0->ci := unknown -frame *next_frame(state *L, int c) { - if (c) - L->ci = &L->base_ci; - frame *ci = L->ci = L->ci->next; - return ci; -} - -// -- Freeing an object and the borrows through it ---------------------------- - -typedef struct hlist { struct hlist *prev; struct hlist *next; } hlist; -typedef struct pair { hlist list; int v; } pair; -typedef struct bucket { hlist *first; hlist *last; } bucket; -typedef struct table { bucket *buckets; hlist list; } table; - -// Clean: the bucket array holds pointers into pairs it does not own (they are -// on the table's intrusive list); freeing the array leaves them borrowed by -// nothing that is going away (jansson's `hashtable_do_rehash`). -int rehash(table *t, pair *p) { - bucket *nb = malloc(sizeof(bucket)); - if (!nb) - return -1; - t->buckets->last = &p->list; - p->list.next = &t->list; - free(t->buckets); - t->buckets = nb; - return 0; -} - -// The object's own storage does go with it. -struct cell { int *buf; int n; }; -void own(struct cell *c) { - int *keep = &c->n; - free(c); - // CHECK: rfc0011-derived.c:[[@LINE+1]]:4: error: use of 'keep' after it was freed [weavec::use-after-free] - *keep = 1; -} diff --git a/test/Analysis/rfc0011-requirements.c b/test/Analysis/rfc0011-requirements.c deleted file mode 100644 index 9e327aec..00000000 --- a/test/Analysis/rfc0011-requirements.c +++ /dev/null @@ -1,218 +0,0 @@ -// RFC 0011, *Bounds checks* (library calls) and *Extents in summaries*: a -// libc call with a buffer and a length is checked against the buffer's -// extent; an access a function makes unconditionally past what its -// parameter's type promises is a requirement in its summary, checked at -// every call; requirements compose through wrappers. -// RUN: not %weavec %s -- -ferror-limit=0 2>&1 | FileCheck %s -// RUN: not %weavec --dump-analysis %s -- 2>/dev/null | FileCheck --check-prefix=DUMP %s -#include "../Inputs/prelude.h" -#include - -void *memcpy(void *, const void *, size_t); -void *memset(void *, int, size_t); -char *fgets(char *, int, void *); -struct outer { char *buf; int k; }; - -// -- Library buffer/length pairs ---------------------------------------------- - -void copy(void) { - char src[16]; - char *dst = malloc(8); - if (!dst) - return; - memcpy(dst, src, 8); - // CHECK: rfc0011-requirements.c:[[@LINE+1]]:10: error: 'memcpy' accesses 16 bytes of 'dst', which has 8 bytes [weavec::out-of-bounds] - memcpy(dst, src, 16); - // CHECK: rfc0011-requirements.c:[[@LINE-6]]:15: note: 'dst' is allocated here - free(dst); -} - -// The source side is checked too. -void source_short(void) { - char big[400]; - char small[200]; - // CHECK: rfc0011-requirements.c:[[@LINE+1]]:15: error: 'memcpy' accesses 400 bytes of 'small', which has 200 bytes [weavec::out-of-bounds] - memcpy(big, small, 400); - // CHECK: rfc0011-requirements.c:[[@LINE-3]]:8: note: 'small' is declared here -} - -void read_line(void *f) { - char line[32]; - if (fgets(line, 32, f)) - use(line); - // CHECK: rfc0011-requirements.c:[[@LINE+1]]:13: error: 'fgets' accesses 64 bytes of 'line', which has 32 bytes [weavec::out-of-bounds] - if (fgets(line, 64, f)) - use(line); -} - -// A symbolic length against a symbolic extent, with a relation. -void clear(char *WEAVEC_SIZED_BY(n) p, size_t n, size_t m) { - if (m <= n) - memset(p, 0, m); - if (m > n) - memset(p, 0, m); -} - -// -- Requirements inferred from a body --------------------------------------- - -// Format 30 carries no `requires-extent` (RFC 0031 §6.1): the requirement of -// a must-access is the Call site's (RFC 0030 §7.5), and the summaries below -// pin the accesses themselves, as stores at a constant byte offset or over an -// element range (RFC 0031 §4.9, *Summaries*). -// A constant access past the pointee's size. -// DUMP-LABEL: function 'put7': -// DUMP: store param0->#7 := int [0, 0] -static void put7(char *b) { b[7] = 0; } - -// A symbolic one, in the parameter that indexes it. -// DUMP-LABEL: function 'put_n': -// DUMP: store *param0[*] elements [param1, param1 plus 1) := int [0, 0] -static void put_n(char *b, size_t n) { b[n] = 0; } - -// A loop below a parameter: the boundary is the requirement. The `int` -// counter's range is not exported (RFC 0031 §4.9, *Known limits*), so the -// store is to some elements. -// DUMP-LABEL: function 'fill': -// DUMP: store *param0[*] := int [0, 0] may -static void fill(int *b, int n) { - for (int i = 0; i < n; i++) - b[i] = 0; -} - -// What the type promises is not a requirement. -// DUMP-LABEL: function 'first': -// DUMP: store param0->k := int [1, 1] -static void first(struct outer *o) { o->k = 1; } - -// RFC 0017: requirements keep both class guards and typed ordering guards, -// and record the first byte accessed as well as the end of the access. An -// access under a test of an integer parameter is a possible store in -// format 30 (RFC 0031 §6.1). -// DUMP-LABEL: function 'on_zero': -// DUMP: store param0->#7 := int [0, 0] may -static void on_zero(char *b, int n) { - if (n == 0) - b[7] = 0; -} -// DUMP-LABEL: function 'guarded': -// DUMP: store param0->#4 := int [0, 0] may -static void guarded(char *b, int n) { - if (n > 4) - b[4] = 0; -} - -// A library call on a parameter is a requirement in its length. -// DUMP-LABEL: function 'clears': -// DUMP: store *param0 := unknown -static void clears(void *b, size_t n) { memset(b, 0, n); } - -// A local index bounded above by a constant needs the boundary. RFC 0017 -// represents a loop bounded by both a constant and a parameter with min. -// DUMP-LABEL: function 'put8': -// DUMP: store *param0[*] elements [0, 8) := int [0, 0] -static void put8(char *b) { - for (int i = 0; i < 8; i++) - b[i] = 0; -} -// DUMP-LABEL: function 'put_le8': -// DUMP: store *param0[*] elements [0, 9) := int [0, 0] -static void put_le8(char *b) { - for (int i = 0; i <= 8; i++) - b[i] = 0; -} -// DUMP-LABEL: function 'either': -// DUMP: store *param0[*] := int [0, 0] may -static void either(int *b, int n) { - for (int i = 0; i < n && i < 16; i++) - b[i] = 0; -} - -// -- Checked at the call ----------------------------------------------------- - -void calls(void) { - char small[4]; - char big[8]; - put7(big); - // CHECK: rfc0011-requirements.c:[[@LINE+1]]:8: error: 'put7' requires 8 bytes behind 'small', which has 4 bytes [weavec::out-of-bounds] - put7(small); - put8(big); - // CHECK: rfc0011-requirements.c:[[@LINE+1]]:8: error: 'put8' requires 8 bytes behind 'small', which has 4 bytes [weavec::out-of-bounds] - put8(small); - // CHECK: rfc0011-requirements.c:[[@LINE+1]]:11: error: 'put_le8' requires 9 bytes behind 'big', which has 8 bytes [weavec::out-of-bounds] - put_le8(big); - int four[4]; - either(four, 4); - // RFC 0030 §7.5: `i < n && i < 16` is no canonical counted loop, so no - // requirement comes from it; but the context of `either(four, 100)` - // writes `b[0..16)` on every return, past `four` (RFC 0031 - // *Implementation amendments*, "Stores past the caller's object"). - // CHECK: rfc0011-requirements.c:[[@LINE+1]]:10: error: 'either' requires 64 bytes behind 'four', which has 16 bytes [weavec::out-of-bounds] - either(four, 100); - char *heap = malloc(4); - if (!heap) - return; - put_n(heap, 3); - // CHECK: rfc0011-requirements.c:[[@LINE+1]]:9: error: 'put_n' requires 5 bytes behind 'heap', which has 4 bytes [weavec::out-of-bounds] - put_n(heap, 4); - int ints[4]; - fill(ints, 4); - // CHECK: rfc0011-requirements.c:[[@LINE+1]]:8: error: 'fill' requires 32 bytes behind 'ints', which has 16 bytes [weavec::out-of-bounds] - fill(ints, 8); - // Clean: a call that needs nothing (Lua's `tablerehash(tb->hash, 0, n)`) - // is satisfied by any object; it is not an access before the start. - fill(ints, 0); - clears(small, 0); - on_zero(small, 1); - // RFC 0030 §7.5: an access under a branch is no must-access, and a - // `LibrarySpec` byte requirement is none of R1-R5: neither call is checked - // against a requirement. - // But the context of `on_zero(small, 0)` takes the branch and stores to - // `b[7]` on every return (RFC 0031 *Implementation amendments*, "Stores - // past the caller's object"). - // CHECK: rfc0011-requirements.c:[[@LINE+1]]:11: error: 'on_zero' requires 8 bytes behind 'small', which has 4 bytes [weavec::out-of-bounds] - on_zero(small, 0); - clears(small, 4); - // And the context of `clears(small, 5)` has `memset` write five bytes. - // CHECK: rfc0011-requirements.c:[[@LINE+1]]:10: error: 'clears' requires 5 bytes behind 'small', which has 4 bytes [weavec::out-of-bounds] - clears(small, 5); - free(heap); -} - -// The argument's own position in its object counts. -void at_offset(void) { - char buf[10]; - put7(buf + 2); - // CHECK: rfc0011-requirements.c:[[@LINE+1]]:8: error: 'put7' requires 11 bytes behind 'buf', which has 10 bytes [weavec::out-of-bounds] - put7(&buf[3]); -} - -// -- Through wrappers -------------------------------------------------------- - -// The extent of a wrapped allocation reaches the caller. -// DUMP-LABEL: function 'xmalloc': -// DUMP: result fresh#0 free extent param0 zeroed when nonnull -static char *xmalloc(size_t n) { - char *p = malloc(n); - if (!p) - __builtin_trap(); - return p; -} - -// A callee's requirement on what this function passes through is this -// function's requirement. -// DUMP-LABEL: function 'deeper': -// DUMP: store param0->#7 := int [0, 0] -static void deeper(char *b) { put7(b); } - -void via_wrappers(void) { - char *p = xmalloc(4); - // CHECK: rfc0011-requirements.c:[[@LINE+1]]:3: error: 'p[4]' is out of bounds: index 4 of an object of 4 bytes [weavec::out-of-bounds] - p[4] = 0; - // RFC 0030 §7.5: a callee's requirement does not compose into its caller's - // (R1-R5 name accesses and string library calls only); but `deeper` - // stores to `b[7]` on every return, past `p` (RFC 0031 *Implementation - // amendments*, "Stores past the caller's object"). - // CHECK: rfc0011-requirements.c:[[@LINE+1]]:10: error: 'deeper' requires 8 bytes behind 'p', which has 4 bytes [weavec::out-of-bounds] - deeper(p); - free(p); -} diff --git a/test/Analysis/rfc0012-assume.c b/test/Analysis/rfc0012-assume.c deleted file mode 100644 index e57db6b7..00000000 --- a/test/Analysis/rfc0012-assume.c +++ /dev/null @@ -1,61 +0,0 @@ -// RFC 0012, *`WEAVEC_ASSUME`*: the argument holds from the call on, as on -// the true edge of `if (arg)`; an assumption the facts contradict ends the -// path; the annotation belongs to the header's function alone. -// RUN: not %weavec %s -- -ferror-limit=0 2>&1 | FileCheck %s -// -// With the analysis off the macro is an unevaluated no-op that still counts -// as a use of its operands: no warnings from Clang. -// RUN: %weavec_cc -fno-weavec -fsyntax-only -Wall -Wextra -Wno-unused-parameter -I%resource_dir %s 2>&1 | count 0 -#include "../Inputs/prelude.h" -#include - -struct pair { - char *p; - size_t len; - size_t cap; -}; - -// The invariant `len < cap` is not visible here; the assumption states it. -void put(struct pair *b, char c) { - WEAVEC_ASSUME(b->len < b->cap); - char *d = malloc(b->cap); - if (!d) - return; - d[b->len] = c; - // CHECK: rfc0012-assume.c:[[@LINE+1]]:3: error: 'd[b->cap]' is out of bounds: 'b->cap' is the number of elements of 'd' [weavec::out-of-bounds] - d[b->cap] = c; - free(d); -} - -// Without it the access is undecided and nothing is reported. -void put_unassumed(struct pair *b, char c) { - char *d = malloc(b->cap); - if (!d) - return; - d[b->len] = c; - free(d); -} - -// A pointer fact. -void nonnull(int *p) { - WEAVEC_ASSUME(p != NULL); - *p = 1; -} - -// A contradicted assumption ends the path: nothing below it runs. -void contradiction(int *p) { - if (p) - return; - WEAVEC_ASSUME(p != NULL); - *p = 1; -} - -void without(int *p) { - if (p) - return; - // CHECK: rfc0012-assume.c:[[@LINE+1]]:4: error: dereference of 'p', which is null [weavec::null-dereference] - *p = 1; -} - -// CHECK: rfc0012-assume.c:[[@LINE+1]]:40: warning: 'weavec.assume' is not an annotation for 'mine' [weavec::invalid-annotation] -WEAVEC_ANNOTATE_("weavec.assume") void mine(int c) { (void)c; } diff --git a/test/Analysis/rfc0012-relations.c b/test/Analysis/rfc0012-relations.c deleted file mode 100644 index 4d4269e4..00000000 --- a/test/Analysis/rfc0012-relations.c +++ /dev/null @@ -1,73 +0,0 @@ -// RFC 0012, *Offset relations and lower bounds* and *Bounds checks through -// offsets*: `i <= n - 1` and `j = i + 1` are relations with an offset, `i >= -// 8` is a lower bound, and both decide bounds checks. -// RUN: not %weavec %s -- -ferror-limit=0 2>&1 | FileCheck %s -// `--dump-analysis` printed the old engine's exit relations; the object -// engine's dump is the format-30 summary (RFC 0031 §6.1), which has no state -// of by-value parameters, so `bound` below checks its bound by an access. -#include "../Inputs/prelude.h" - -// -- Offsets ------------------------------------------------------------------ - -void offsets(size_t n, size_t i) { - // RFC 0017: keep n - 1 and the allocation's byte product from wrapping. - if (n == 0 || n > (size_t)-1 / sizeof(int)) - return; - int *a = malloc(n * sizeof *a); - if (!a) - return; - if (i <= n - 1) - a[i] = 0; - if (i <= n - 1) - a[i + 1] = 0; - if (i < n - 1) - a[i + 1] = 0; - free(a); -} - -// A copy with an offset: `j == i + 1`. -void copies(size_t n, size_t i) { - // The copied i + 1 is an affine equality only when it cannot wrap. - if (n == 0 || n > (size_t)-1 / sizeof(int) || i == (size_t)-1) - return; - int *a = malloc(n * sizeof *a); - if (!a) - return; - size_t j = i + 1; - if (i < n) - a[j] = 0; - if (j < n) - a[j] = 0; - free(a); -} - -// -- Lower bounds ------------------------------------------------------------- - -void lower(size_t i) { - char buf[8]; - if (i >= 8) - // CHECK: rfc0012-relations.c:[[@LINE+1]]:5: error: 'buf[i]' is out of bounds: 'i' is at least 8 in an object of 8 bytes [weavec::out-of-bounds] - buf[i] = 0; - if (i > 7) - // CHECK: rfc0012-relations.c:[[@LINE+1]]:5: error: 'buf[i]' is out of bounds: 'i' is at least 8 in an object of 8 bytes [weavec::out-of-bounds] - buf[i] = 0; - if (i >= 7) - buf[i] = 0; - if (i < 8) - buf[i] = 0; - // The failing edge of `i < 8` is `i >= 8`. - if (i < 8) - return; - // CHECK: rfc0012-relations.c:[[@LINE+1]]:3: error: 'buf[i]' is out of bounds: 'i' is at least 8 in an object of 8 bytes [weavec::out-of-bounds] - buf[i] = 0; -} - -// The bound is in the state: `i >= 8` on the edge where `i < 8` fails (the -// other path never returns, so the state after the test is that edge's). -void bound(size_t i) { - char buf[8]; - if (i < 8) - __builtin_trap(); - // CHECK: rfc0012-relations.c:[[@LINE+1]]:3: error: 'buf[i]' is out of bounds: 'i' is at least 8 in an object of 8 bytes [weavec::out-of-bounds] - buf[i] = 0; -} diff --git a/test/Analysis/rfc0012-strings.c b/test/Analysis/rfc0012-strings.c deleted file mode 100644 index 4d9e09c2..00000000 --- a/test/Analysis/rfc0012-strings.c +++ /dev/null @@ -1,171 +0,0 @@ -// RFC 0012, *String facts*, *Length places*, *Sources of string facts* and -// *String checks*: what the checker knows about the NUL-terminated string an -// object holds, where the knowledge comes from, and the copies and reads it -// checks against it. -// RUN: not %weavec %s -- -ferror-limit=0 2>&1 | FileCheck %s -// `--dump-analysis` printed the old engine's scalar and spatial state; the -// object engine's dump is the format-30 summary (RFC 0031 §6.1), which holds -// no state of a function's locals, so the length place is checked through -// the diagnostics that state it. -#include "../Inputs/prelude.h" - -size_t strlen(const char *s); -char *strcpy(char *dst, const char *src); -char *strncpy(char *dst, const char *src, size_t n); -char *strcat(char *dst, const char *src); -char *strdup(const char *s); -int sprintf(char *buf, const char *fmt, ...); -int puts(const char *s); -int printf(const char *fmt, ...); -void *memset(void *dst, int c, size_t n); - -// -- Literals against declared extents ---------------------------------------- - -void literals(void) { - char buf[4]; - // CHECK: rfc0012-strings.c:[[@LINE+1]]:10: error: 'strcpy' accesses 6 bytes of 'buf', which has 4 bytes [weavec::out-of-bounds] - strcpy(buf, "hello"); - // CHECK: rfc0012-strings.c:[[@LINE-3]]:8: note: 'buf' is declared here - strcpy(buf, "abc"); - // CHECK: rfc0012-strings.c:[[@LINE+1]]:10: error: 'strcat' accesses 5 bytes of 'buf', which has 4 bytes [weavec::out-of-bounds] - strcat(buf, "d"); - // CHECK: rfc0012-strings.c:[[@LINE+1]]:11: error: 'sprintf' accesses 5 bytes of 'buf', which has 4 bytes [weavec::out-of-bounds] - sprintf(buf, "%s!", "abc"); -} - -// A format's minimum output: one digit for `%d`, the literal text, the NUL. -void formats(int x) { - char buf[4]; - sprintf(buf, "%d", x); - // CHECK: rfc0012-strings.c:[[@LINE+1]]:11: error: 'sprintf' accesses at least 5 bytes of 'buf', which has 4 bytes [weavec::out-of-bounds] - sprintf(buf, "%d!!!", x); -} - -// -- Length places ------------------------------------------------------------- - -// `strlen(s)` is a place; the allocation's extent and the copy's need are -// both stated in it. (`s` is Single by RFC 0030 §7.3: at least one byte.) -void short_by_one(const char *s) { - char *d = malloc(strlen(s)); - if (!d) - return; - // CHECK: rfc0012-strings.c:[[@LINE+1]]:10: error: 'strcpy' accesses 'strlen(s)' + 1 bytes of 'd', which has 'strlen(s)' bytes [weavec::out-of-bounds] - strcpy(d, s); - // CHECK: rfc0012-strings.c:[[@LINE-5]]:13: note: 'd' is allocated here - free(d); -} - -// Through a variable the relation `n == strlen(s)` carries the fact. -void through_a_variable(const char *s) { - size_t n = strlen(s); - char *d = malloc(n); - if (!d) - return; - // CHECK: rfc0012-strings.c:[[@LINE+1]]:10: error: 'strcpy' accesses 'strlen(s)' + 1 bytes of 'd', which has 'n' bytes ('strlen(s)' equals 'n') [weavec::out-of-bounds] - strcpy(d, s); - free(d); -} - -// The exact allocation is clean, and the string's length is known after -// the copy: an index at the terminator is fine, one past it is not. -void exact(const char *s) { - char *d = malloc(strlen(s) + 1); - if (!d) - return; - strcpy(d, s); - d[strlen(s)] = 0; - // CHECK: rfc0012-strings.c:[[@LINE+1]]:3: error: 'd[strlen(s) + 1]' is out of bounds: it reaches 'strlen(s)' + 2 bytes into 'd', which has 'strlen(s)' + 1 bytes [weavec::out-of-bounds] - d[strlen(s) + 1] = 0; - free(d); -} - -// `strdup` measures its argument: the result has the length and one more -// byte of extent. -void duplicated(const char *s) { - char *d = strdup(s); - if (!d) - return; - d[strlen(s)] = 0; - // CHECK: rfc0012-strings.c:[[@LINE+1]]:10: error: 'strcat' accesses 'strlen(s)' + 2 bytes of 'd', which has 'strlen(s)' + 1 bytes [weavec::out-of-bounds] - strcat(d, "x"); - free(d); -} - -// -- Unterminated objects ----------------------------------------------------- - -// `strncpy` with a source at least as long as the count leaves no -// terminator in a buffer the count fills; every terminator-seeking read of -// it is reported, with the note at the copy. -void unterminated(void) { - char name[8]; - strncpy(name, "0123456789", sizeof name); - // CHECK: rfc0012-strings.c:[[@LINE+1]]:23: error: 'strlen' reads past the end of 'name', which is not NUL-terminated [weavec::out-of-bounds] - size_t len = strlen(name); - // CHECK: rfc0012-strings.c:[[@LINE-3]]:3: note: 'name' is left without a terminator here - // CHECK: rfc0012-strings.c:[[@LINE+1]]:8: error: 'puts' reads past the end of 'name', which is not NUL-terminated [weavec::out-of-bounds] - puts(name); - // CHECK: rfc0012-strings.c:[[@LINE+1]]:18: error: 'printf' reads past the end of 'name', which is not NUL-terminated [weavec::out-of-bounds] - printf("%s\n", name); - (void)len; -} - -// A NUL stored into the object terminates it again. -void repaired(void) { - char name[8]; - strncpy(name, "0123456789", sizeof name); - name[sizeof name - 1] = 0; - puts(name); -} - -// An initialiser as long as the array, or with no NUL among its elements. -void initialisers(void) { - char a[4] = "abcd"; - char b[3] = {'a', 'b', 'c'}; - char c[4] = "abc"; - // CHECK: rfc0012-strings.c:[[@LINE+1]]:8: error: 'puts' reads past the end of 'a', which is not NUL-terminated [weavec::out-of-bounds] - puts(a); - // CHECK: rfc0012-strings.c:[[@LINE-5]]:8: note: 'a' is left without a terminator here - // CHECK: rfc0012-strings.c:[[@LINE+1]]:8: error: 'puts' reads past the end of 'b', which is not NUL-terminated [weavec::out-of-bounds] - puts(b); - puts(c); -} - -// `memset` with a non-zero byte over the whole object; with zero it is the -// empty string. -void filled(void) { - char a[4]; - char b[4]; - memset(a, 'x', sizeof a); - memset(b, 0, sizeof b); - // CHECK: rfc0012-strings.c:[[@LINE+1]]:8: error: 'puts' reads past the end of 'a', which is not NUL-terminated [weavec::out-of-bounds] - puts(a); - puts(b); - strcat(b, "abc"); - // CHECK: rfc0012-strings.c:[[@LINE+1]]:10: error: 'strcat' accesses 5 bytes of 'b', which has 4 bytes [weavec::out-of-bounds] - strcat(b, "d"); -} - -// -- Facts are on the object -------------------------------------------------- - -// A pointer to the array shares its facts; a copy at an offset sees the -// length from there. -void aliased(void) { - char buf[8] = "abcde"; - char *p = buf + 2; - strcat(p, "fg"); - // CHECK: rfc0012-strings.c:[[@LINE+1]]:10: error: 'strcat' accesses 9 bytes of 'p', which has 8 bytes [weavec::out-of-bounds] - strcat(p, "h"); - // CHECK: rfc0012-strings.c:[[@LINE-5]]:8: note: the object behind 'p' is declared here -} - -// -- Deliberately not caught -------------------------------------------------- - -// Nothing is known about `src`: the copy may or may not fit, and the buffer -// may or may not be terminated afterwards (RFC 0012, *Bugs deliberately not -// caught*). -void unknown_source(const char *src) { - char name[8]; - strcpy(name, src); - strncpy(name, src, sizeof name); - puts(name); -} diff --git a/test/Analysis/rfc0013-heap.c b/test/Analysis/rfc0013-heap.c deleted file mode 100644 index e2d53f84..00000000 --- a/test/Analysis/rfc0013-heap.c +++ /dev/null @@ -1,63 +0,0 @@ -// RFC 0013: reachable child ownership, aliases, sizes and strings survive calls. -// RUN: not %weavec %s -- -ferror-limit=0 2>&1 | FileCheck %s -// RUN: not %weavec --dump-analysis %s -- 2>/dev/null | FileCheck --check-prefix=DUMP %s -#include "../cases/evaluation/Inputs/heap.c" - -// DUMP-LABEL: function 'string_box': -// DUMP: store result->data := fresh#1 free extent 4 -// DUMP: string *result->data nul-within 3 from 0 - -void overflow(void) { - struct box *b = box_new(); if (!b) return; - // CHECK: rfc0013-heap.c:[[@LINE+1]]:3: error: 'b->data[4]' is out of bounds: index 4 of an object of 4 bytes [weavec::out-of-bounds] - b->data[4] = 0; - free(b->data); free(b); -} -void leak(void) { - struct box *b = box_new(); if (!b) return; - // RFC 0031 §5.8, §5.11: the leak is reported where freeing 'b' drops the - // last reference to the data, named as it was created. - // CHECK: rfc0013-heap.c:[[@LINE+1]]:3: warning: result of 'box_new' is leaked [weavec::leak] - free(b); -} -void alias(void) { - char *p = malloc(4); if (!p) return; - struct box *b = box_wrap(p); - if (!b) { free(p); return; } - free(p); - // CHECK: rfc0013-heap.c:[[@LINE+1]]:3: error: use of 'b->data' after it was freed [weavec::use-after-free] - b->data[0] = 0; - free(b); -} -void changed_size(void) { - size_t n = 4; - char *p = malloc(n); if (!p) return; - n = 8; - // CHECK: rfc0013-heap.c:[[@LINE+1]]:3: error: 'p[7]' is out of bounds: index 7 of an object of 4 bytes [weavec::out-of-bounds] - p[7] = 0; - free(p); -} -struct box *empty(void) { - struct box *b = box_new(); if (!b) return NULL; - free(b->data); b->data = NULL; return b; -} -void null_field(void) { - struct box *b = empty(); if (!b) return; - // CHECK: rfc0013-heap.c:[[@LINE+1]]:3: error: dereference of 'b->data', which is null [weavec::null-dereference] - b->data[0] = 0; - free(b); -} -char *strcpy(char *, const char *); -struct box *string_box(void) { - struct box *b = box_new(); if (!b) return NULL; - strcpy(b->data, "abc"); return b; -} -void string_field(void) { - struct box *b = string_box(); if (!b) return; - char dst[3]; - // CHECK: rfc0013-heap.c:[[@LINE+1]]:10: error: 'strcpy' accesses 4 bytes of 'dst', which has 3 bytes [weavec::out-of-bounds] - strcpy(dst, b->data); - free(b->data); free(b); -} - -// CHECK: 1 warning and 5 errors generated. diff --git a/test/Analysis/rfc0015-limits.c b/test/Analysis/rfc0015-limits.c deleted file mode 100644 index 87d3b995..00000000 --- a/test/Analysis/rfc0015-limits.c +++ /dev/null @@ -1,54 +0,0 @@ -// RFC 0015: exhausted selection budgets preserve earlier temporal evidence. -// RFC 0030 §15 item 3: an exhausted budget is no diagnostic. The object -// engine has no array element limit: `a` is one entry object whose elements -// are cells by offset (RFC 0031 §4.6, §4.9), so all 33 accesses are within -// budget and none is `unresolved(budget)`. -// RUN: not %weavec %s -- 2>&1 | FileCheck %s -#include "../Inputs/prelude.h" -void bounded(char **a) { - free(a[0]); - use(a[1]); - use(a[2]); - use(a[3]); - use(a[4]); - use(a[5]); - use(a[6]); - use(a[7]); - use(a[8]); - use(a[9]); - use(a[10]); - use(a[11]); - use(a[12]); - use(a[13]); - use(a[14]); - use(a[15]); - use(a[16]); - use(a[17]); - use(a[18]); - use(a[19]); - use(a[20]); - use(a[21]); - use(a[22]); - use(a[23]); - use(a[24]); - use(a[25]); - use(a[26]); - use(a[27]); - use(a[28]); - use(a[29]); - use(a[30]); - use(a[31]); - // CHECK-NOT: analysis is incomplete - use(a[32]); - // CHECK: rfc0015-limits.c:[[@LINE+1]]:3: error: use of 'a[0]' after it was freed [weavec::use-after-free] - a[0][0]=1; -} -void weak(char **a, char *p, int i, int j) { - char *old=a[0]; free(a[0]); - // RFC 0017 represents this typed product as a symbolic selector. The - // possible update must still preserve the old pointer's release evidence. - // CHECK-NOT: analysis is incomplete - a[i*j]=p; - // CHECK: rfc0015-limits.c:[[@LINE+1]]:3: error: use of 'old' after it was freed [weavec::use-after-free] - old[0]=1; -} diff --git a/test/Analysis/rfc0016-clean.c b/test/Analysis/rfc0016-clean.c deleted file mode 100644 index 3ec10b3f..00000000 --- a/test/Analysis/rfc0016-clean.c +++ /dev/null @@ -1,45 +0,0 @@ -// RFC 0016: callers with related pointers retain safe ordering and replacement. -// The contexts are clean. RFC 0030 §2.6: every function also gets the generic -// (authoritative) pass, which cannot tell `a[i]` from `a[j]` in `selected`: -// a possible finding there, and nothing else. -// RUN: %weavec %s -- 2>&1 | FileCheck --check-prefix=QUIET %s -// QUIET-NOT: {{warning|error}}: -// QUIET: rfc0016-clean.c:[[@LINE+23]]:61: warning: use of 'a[j]' after it may have been freed [weavec::use-after-free] -// QUIET-NOT: {{warning|error}}: -#include "../Inputs/prelude.h" - -static void before(char *a, char *b) { *b = 1; free(a); } -static void once(char *a, char *b) { - char *saved = a; - if (saved == b) free(saved); - else { free(a); free(b); } -} -static void clear(char **a, char **b) { - free(*a); *a = 0; - if (*b) **b = 1; -} -static void replace(char **a, char **b) { - free(*a); *a = malloc(4); - if (*b) **b = 1; -} -struct box { char *data; int release; }; -static void guarded(struct box *a, char *b) { - if (a->release) free(a->data); - else *b = 1; -} -static void selected(char **a, int i, int j) { free(a[i]); *a[j] = 1; } -void good(void) { - char *p = malloc(4); if (!p) return; - before(p, p + 1); - p = malloc(4); if (!p) return; - once(p, p); - p = malloc(4); if (!p) return; - clear(&p, &p); - p = malloc(4); if (!p) return; - replace(&p, &p); free(p); - p = malloc(4); if (!p) return; - struct box b = {p, 0}; guarded(&b, p); free(p); - p = malloc(4); char *q = malloc(4); - if (!p || !q) { free(p); free(q); return; } - char *items[2] = {p, q}; selected(items, 0, 1); free(q); -} diff --git a/test/Analysis/rfc0017-integer-diagnostics.c b/test/Analysis/rfc0017-integer-diagnostics.c deleted file mode 100644 index 4f6e29f5..00000000 --- a/test/Analysis/rfc0017-integer-diagnostics.c +++ /dev/null @@ -1,36 +0,0 @@ -// RFC 0017: definite invalid C integer operations have a stable diagnostic. -// RUN: not %weavec %s -- -ferror-limit=0 2>&1 | FileCheck %s -// RUN: %weavec -Wno-error=weavec-invalid-integer-operation %s -- 2>&1 | FileCheck %s --check-prefix=LOWERED -// LOWERED-COUNT-6: warning: invalid integer operation: -#include "../Inputs/prelude.h" -int add(int n) { - if (n != 2147483647) return 0; - // CHECK: rfc0017-integer-diagnostics.c:[[@LINE+1]]:10: error: invalid integer operation: signed integer overflow [weavec::invalid-integer-operation] - return n + 1; -} -int division(int n) { - if (n != 0) return 0; - // CHECK: error: invalid integer operation: division by zero [weavec::invalid-integer-operation] - return 2 / n; -} -int min_divided(int n) { - if (n != (-2147483647 - 1)) return 0; - // CHECK: error: invalid integer operation: signed division overflow [weavec::invalid-integer-operation] - return n / -1; -} -int shift(int n) { - if (n != 32) return 0; - // CHECK: error: invalid integer operation: invalid shift count [weavec::invalid-integer-operation] - return 1 << n; -} -int negative_shift(int n) { - if (n != -1) return 0; - // CHECK: error: invalid integer operation: invalid signed left shift [weavec::invalid-integer-operation] - return n << 1; -} -void dimension(int n) { - if (n != 0) return; - // CHECK: error: invalid integer operation: nonpositive variable array dimension [weavec::invalid-integer-operation] - char a[n]; (void)a; -} -// CHECK: 6 errors generated. diff --git a/test/Analysis/rfc0017-target-and-unsafe.c b/test/Analysis/rfc0017-target-and-unsafe.c deleted file mode 100644 index cc86da31..00000000 --- a/test/Analysis/rfc0017-target-and-unsafe.c +++ /dev/null @@ -1,46 +0,0 @@ -// RFC 0017: target width, signed wrapping mode, and diagnostics inside an -// unsafe region (RFC 0030 §6.1: no diagnostic is dropped for being inside -// one). -// RUN: not %weavec %s -- -fwrapv -ferror-limit=0 2>&1 | FileCheck %s -// RUN: not %weavec %s -- -target i386-unknown-linux-gnu -fwrapv -ferror-limit=0 2>&1 | FileCheck %s -#include "../Inputs/prelude.h" -#include "weavec.h" - -void unsigned_target(size_t n) { - if(n != (size_t)-1) return; - n++; - int *p=malloc(sizeof *p); if(!p)return; - free(p); - // CHECK: error: use of 'p' after it was freed [weavec::use-after-free] - if(n==0) *p=1; -} - -void signed_wrapping(int n) { - if(n != 2147483647) return; - n++; - int *p=malloc(sizeof *p); if(!p)return; - free(p); - // CHECK: error: use of 'p' after it was freed [weavec::use-after-free] - if(n<0) *p=1; -} - -int shift_is_still_invalid(int n) { - if(n!=-1)return 0; - // CHECK: error: invalid integer operation: invalid signed left shift [weavec::invalid-integer-operation] - return n<<1; -} - -void unsafe_effects(void) { - unsigned char n=255; - int *p=malloc(sizeof *p); if(!p)return; - WEAVEC_UNSAFE { n++; free(p); } - // CHECK: error: use of 'p' after it was freed [weavec::use-after-free] - if(n==0) *p=1; -} - -WEAVEC_UNSAFE int inside_unsafe(int n) { - if(n!=-1)return 0; - // CHECK: error: invalid integer operation: invalid signed left shift [weavec::invalid-integer-operation] - return n<<1; -} -// CHECK: 5 errors generated. diff --git a/test/Analysis/rfc0030-assume.c b/test/Analysis/rfc0030-assume.c deleted file mode 100644 index 09c55d2d..00000000 --- a/test/Analysis/rfc0030-assume.c +++ /dev/null @@ -1,30 +0,0 @@ -// RFC 0030 §6.2: an Assume site's assertion facet is proven when the analysis -// proves `e` there, a violation (the `contradicted-assumption` error) when it -// refutes `e`, and checked otherwise; `e` holds after the site in every case. -// RUN: not %weavec %s -- 2>&1 | FileCheck %s -#include -#include - -int proven(int n) { - if (n <= 0) - return 0; - WEAVEC_ASSUME(n > 0); - return n; -} - -int checked(char *b, int n) { - WEAVEC_ASSUME(n > 0); - return b[n - 1]; -} - -size_t last(const char *s) { - size_t len = 0; - (void)s; - // CHECK: rfc0030-assume.c:[[@LINE+2]]:3: error: assumption 'len > 0' is false here [weavec::contradicted-assumption] - // CHECK: rfc0030-assume.c:[[@LINE-3]]:10: note: 'len' is 0 here - WEAVEC_ASSUME(len > 0); - return len - 1; -} - -// CHECK-NOT: error: -// CHECK: 1 error generated. diff --git a/test/Analysis/rfc0030-budget.c b/test/Analysis/rfc0030-budget.c deleted file mode 100644 index 2092f286..00000000 --- a/test/Analysis/rfc0030-budget.c +++ /dev/null @@ -1,35 +0,0 @@ -// RFC 0030 §5.5, RFC 0034 §7.1: a function whose analysis would take more -// work (the sizes of the states it transfers and joins) than its budget -// stops; its operations take the §2.6 defaults with reason `budget`, the -// summary line lists it, and its callers apply the unknown-callee default -// at its calls. -// RUN: %weavec --budget=100 %s -- 2>&1 | FileCheck %s -// RUN: %weavec --budget=0 %s -- 2>&1 | FileCheck --check-prefix=FULL %s -#include - -// CHECK: rfc0030-budget.c: {{.*}}; 1 function over budget (walk) -// FULL-NOT: over budget -int walk(const int *p, int n) { - int s = 0; - for (int i = 0; i < n; i++) - for (int j = 0; j < i; j++) - if (p[i] > p[j]) - s += p[j]; - return s; -} - -// The caller stays within its budget; the call to `walk` may have done -// anything to what it was handed. -int caller(void) { - int *p = malloc(4 * sizeof *p); - if (!p) - return 0; - p[0] = 1; - // The call's own facet is about `p`, which is live here: RFC 0031 §5.4 - // decides it from the caller's state, so it is proven; what `walk` did is - // the unknown-callee default after the call. - int s = walk(p, 4); - s += p[0]; - free(p); - return s; -} diff --git a/test/Analysis/rfc0030-certainty.c b/test/Analysis/rfc0030-certainty.c deleted file mode 100644 index 80f94fd9..00000000 --- a/test/Analysis/rfc0030-certainty.c +++ /dev/null @@ -1,141 +0,0 @@ -// RFC 0030 §3, *Diagnostics*: a finding is definite (an error) or possible -// (a warning with the "may" wording); a possible null or bounds finding is a -// checked facet with no diagnostic; `allocation-failure` is off by default. -// RUN: not %weavec %s -- 2>&1 | FileCheck %s -// RUN: not %weavec -Wweavec-allocation-failure %s -- 2>&1 | FileCheck --check-prefix=ALLOC %s -// RUN: not %weavec -Wno-weavec-use-after-free %s -- 2>&1 | FileCheck --check-prefix=QUIET %s -#include -#include - -struct node { - int v; -}; - -void definite(char *p) { - free(p); - // CHECK: rfc0030-certainty.c:[[@LINE+2]]:3: error: use of 'p' after it was freed [weavec::use-after-free] - // CHECK: rfc0030-certainty.c:[[@LINE-2]]:3: note: freed here - p[0] = 1; -} - -// Freed on one path into the use: possible. -void possible(char *p, int c) { - if (c) - free(p); - // CHECK: rfc0030-certainty.c:[[@LINE+2]]:3: warning: use of 'p' after it may have been freed [weavec::use-after-free] - // CHECK: rfc0030-certainty.c:[[@LINE-2]]:5: note: freed here on some paths - p[0] = 1; -} - -void twice(char *p, int c) { - if (c) - free(p); - // CHECK: rfc0030-certainty.c:[[@LINE+2]]:3: warning: 'p' may be freed twice [weavec::double-free] - // CHECK: rfc0030-certainty.c:[[@LINE-2]]:5: note: previously freed here on some paths - free(p); -} - -// `realloc` moves its argument only when it succeeds: untested, possible. -void untested(char *p) { - char *q = realloc(p, 16); - // CHECK: rfc0030-certainty.c:[[@LINE+1]]:3: warning: use of 'p' after it may have been moved [weavec::use-after-move] - p[0] = 1; - free(q); -} - -// A loop's first iteration frees nothing yet: the second free is possible, -// the use after it in the same iteration definite. -void loop(char *p, int n) { - while (n--) { - // CHECK: rfc0030-certainty.c:[[@LINE+1]]:5: warning: 'p' may be freed twice [weavec::double-free] - free(p); - // CHECK: rfc0030-certainty.c:[[@LINE+1]]:5: error: use of 'p' after it was freed [weavec::use-after-free] - p[0] = 1; - } -} - -void storage(int c) { - char buf[8]; - char *p = c ? buf : malloc(8); - // CHECK: rfc0030-certainty.c:[[@LINE+2]]:3: warning: 'p' is released but may point to 'buf', which is not a heap object [weavec::invalid-release] - // CHECK: rfc0030-certainty.c:[[@LINE-3]]:8: note: 'buf' is declared here - free(p); -} - -// Null: definite only. -int null(struct node *n) { - if (n == NULL) - // CHECK: rfc0030-certainty.c:[[@LINE+1]]:12: error: dereference of 'n', which is null [weavec::null-dereference] - return n->v; - return 0; -} - -// An allocation's result used untested is checked, and an -// `allocation-failure` only when that is enabled. -// CHECK-NOT: allocation-failure -// ALLOC: rfc0030-certainty.c:[[@LINE+4]]:3: warning: the result of 'malloc' is used without a null test; it is null when allocation fails [weavec::allocation-failure] -// ALLOC: rfc0030-certainty.c:[[@LINE+2]]:20: note: allocated here -void unchecked(void) { - struct node *n = malloc(sizeof *n); - n->v = 1; - free(n); -} - -// Bounds: definite only, against an exact extent. -void bounds(int i) { - char a[4]; - // CHECK: rfc0030-certainty.c:[[@LINE+1]]:3: error: 'a[4]' is out of bounds: index 4 of an object of 4 bytes [weavec::out-of-bounds] - a[4] = 0; - if (i >= 0 && i <= 4) - a[i] = 0; // may be 4: a checked facet, no diagnostic -} - -// RFC 0030 §3.4: what a release is of is as certain as the release. `hold` -// releases its argument only when `own` is set, and the join of the two arms -// keeps the copy without the flag, so the release is claimed where it does -// not happen: the storage it would release is a possible finding, never an -// error that would drop the object. -struct held { - char *bytes; -}; - -static struct held *hold(char *text, int own) { - char *v; - struct held *h; - if (own) - v = text; - else { - v = strdup(text); - if (!v) - return NULL; - } - h = malloc(sizeof *h); - if (!h) { - free(v); - return NULL; - } - h->bytes = v; - return h; -} - -void possible_release(void) { - char buf[8]; - buf[0] = 0; - // The object engine keeps the constant `own` of each call (RFC 0031 - // *Implementation amendments*, *Alias contexts* and *Pending cases*): with - // `own == 0` `hold` releases only its own copy, so no release of the - // literal or of `buf` is claimed at all. Freeing only the holder leaks - // that copy (§5.8), which the old engine did not see. - // CHECK-NOT: invalid-release - // CHECK: rfc0030-certainty.c:[[@LINE+1]]:3: warning: result of 'hold' is leaked [weavec::leak] - free(hold("abc", 0)); - // CHECK-NOT: invalid-release - // CHECK: rfc0030-certainty.c:[[@LINE+1]]:3: warning: result of 'hold' is leaked [weavec::leak] - free(hold(buf, 0)); - // CHECK-NOT: invalid-release -} - -// -Wno-weavec-use-after-free drops the possible warnings; errors stay. -// QUIET-NOT: may have been freed -// QUIET: error: use of 'p' after it was freed -// QUIET-NOT: may have been freed diff --git a/test/Analysis/rfc0030-declared-kinds.c b/test/Analysis/rfc0030-declared-kinds.c deleted file mode 100644 index 154e2382..00000000 --- a/test/Analysis/rfc0030-declared-kinds.c +++ /dev/null @@ -1,120 +0,0 @@ -// RFC 0030 §7.2: every source of a declared kind reaches the engine (§15 -// item 14), one function per row of the table. A declared kind holds inside -// the definition under A1 and is checked at every call; a definite shortfall -// against an exact extent is the call's `out-of-bounds` error. -// RUN: not %weavec %s -- 2>&1 | FileCheck %s -#include -#include - -void *memset(void *, int, size_t); - -// WEAVEC_COUNTED_BY on a parameter: `p[i]` for `i < n` is proven inside. -int counted(const int *WEAVEC_COUNTED_BY(n) p, size_t n) { - int s = 0; - for (size_t i = 0; i < n; i++) - s += p[i]; - return s; -} - -// A loop to `m` needs `counted(m)`, which no call checks: the declared -// `counted(n)` rules `p`, so `p[i]` is the body's check, never proven. -int counted_to(const int *WEAVEC_COUNTED_BY(n) p, size_t n, size_t m) { - int s = 0; - for (size_t i = 0; i < m; i++) - s += p[i]; - return s; -} - -// WEAVEC_SIZED_BY on a `void *` parameter counts bytes. -void sized(void *WEAVEC_SIZED_BY(n) dst, size_t n) { - memset(dst, 0, n); -} - -// WEAVEC_ENDED_BY: `[p, end)` lies in one object (the engine has no extent -// term for a pointer difference, so the body's `*p` is unresolved). -int ended(const int *WEAVEC_ENDED_BY(end) p, const int *end) { - int s = 0; - for (; p < end; p++) - s += *p; - return s; -} - -// WEAVEC_STRING: the scan stays inside the string. -size_t string(const char *WEAVEC_STRING s) { - size_t n = 0; - while (s[n]) - n++; - return n; -} - -// WEAVEC_NONNULL: the dereference inside is proven. -int nonnull(const int *WEAVEC_NONNULL p) { - return *p; -} - -// `counted_by` on a field: the index is checked against the count, whose -// read checks the object pointer itself (§10.3 rule 5). -struct pkt { - int len; - char *buf __attribute__((counted_by(len))); -}; -char field(const struct pkt *p, int i) { - return p->buf[i]; -} - -// `alloc_size`: the result is Sized(n) bytes, a declared kind. -void *grab(size_t n) __attribute__((alloc_size(1))); -char result(size_t i) { - char *p = grab(8); - return p ? p[i] : 0; -} - -// `nonnull(1)` and `_Nonnull`: nullability at level 2. -int attr(const int *p) __attribute__((nonnull(1))); -int attr(const int *p) { - return *p; -} -int attr2(const int *_Nonnull p) { - return *p; -} - -// `T p[static N]`: Counted(N) and non-null. -int sum4(const int a[static 4]) { - return a[3]; -} - -// A VLA parameter `T p[n]`: Counted(n), nullable. -int vla(size_t n, const int p[n]) { - int s = 0; - for (size_t i = 0; i < n; i++) - s += p[i]; - return s; -} - -// `ownership_returns` and `ownership_takes`: an unknown callee's contract. -void *pool_get(size_t n) __attribute__((ownership_returns(pool))); -void pool_put(void *p) __attribute__((ownership_takes(pool, 1))); - -void calls(size_t n) { - int four[4] = {1, 2, 3, 4}; - int three[3] = {1, 2, 3}; - char b[4] = "abc"; - (void)counted(four, n); - sized(b, n); - (void)ended(four, four + 4); - // CHECK: rfc0030-declared-kinds.c:[[@LINE+1]]:15: error: 'ended' requires 20 bytes behind 'four', which has 16 bytes [weavec::out-of-bounds] - (void)ended(four, four + 5); - // RFC 0031 §4.3 (string fact): `b` holds "abc" and its terminator, so the string the - // call requires is proven. - (void)string(b); - (void)nonnull(n ? four : 0); - // CHECK: rfc0030-declared-kinds.c:[[@LINE+1]]:14: error: 'sum4' requires 16 bytes behind 'three', which has 12 bytes [weavec::out-of-bounds] - (void)sum4(three); - (void)vla(n, four); - char *p = pool_get(8); - if (!p) - return; - pool_put(p); - // CHECK: rfc0030-declared-kinds.c:[[@LINE+1]]:3: error: use of 'p' after it was freed [weavec::use-after-free] - p[0] = 1; -} diff --git a/test/Analysis/rfc0030-library.c b/test/Analysis/rfc0030-library.c deleted file mode 100644 index 025e4f75..00000000 --- a/test/Analysis/rfc0030-library.c +++ /dev/null @@ -1,60 +0,0 @@ -// RFC 0030 §8, §5.3: what the library table says beyond a summary. Hidden -// state (`retain`, `reads`, `invalidates`), a `sync` callback's target as a -// may-effect of the call, `alloca` storage, and a literal format's arity. -// RUN: not %weavec %s -- 2>&1 | FileCheck %s -#include -#include -#include -#include - -char *next_field(void) { - char *s = strdup("a,b"); - if (!s) - return NULL; - strtok(s, ","); - free(s); - // CHECK: rfc0030-library.c:[[@LINE+1]]:10: error: use of '' after it was freed [weavec::use-after-free] - return strtok(NULL, ","); -} - -int stale_environment(void) { - const char *v = getenv("WEAVEC_LIT"); - setenv("WEAVEC_LIT", "other", 1); - // CHECK: rfc0030-library.c:[[@LINE+1]]:14: warning: use of 'v' after it may have been freed [weavec::use-after-free] - return v ? v[0] : 0; -} - -static char *victim; -static int compare(const void *a, const void *b) { - free(victim); - victim = NULL; - return *(const int *)a - *(const int *)b; -} - -int sort_then_use(char *p) { - int xs[2] = {2, 1}; - victim = p; - qsort(xs, 2, sizeof xs[0], compare); - // CHECK: rfc0030-library.c:[[@LINE+1]]:10: warning: use of 'p' after it may have been freed [weavec::use-after-free] - return p[0]; -} - -int sort_unknown(int (*order)(const void *, const void *)) { - int xs[2] = {2, 1}; - qsort(xs, 2, sizeof xs[0], order); - return xs[0]; -} -// The object engine words the detail itself (RFC 0031 §5.4); the reason is -// RFC 0030's. - -char *frame(void) { - char *p = alloca(8); - p[0] = 1; - // CHECK: rfc0030-library.c:[[@LINE+1]]:10: error: 'p' may outlive '', which it points to [weavec::lifetime-too-short] - return p; -} - -void too_few(const char *s) { - // CHECK: rfc0030-library.c:[[@LINE+1]]:3: error: format string of 'printf' reads 2 arguments but 1 are passed [weavec::out-of-bounds] - printf("%s %s\n", s); -} diff --git a/test/Analysis/rfc0030-literal-write.c b/test/Analysis/rfc0030-literal-write.c deleted file mode 100644 index ffec052d..00000000 --- a/test/Analysis/rfc0030-literal-write.c +++ /dev/null @@ -1,24 +0,0 @@ -// RFC 0030 *Diagnostics* (`out-of-bounds`): a string literal has no -// writable byte, so a store through a pointer that points into one on every -// path, or a library call that writes through it, is a definite error. On -// some paths only, the spatial facet is `unresolved(unknown-extent)` and -// nothing is reported. -// RUN: not %weavec %s -- 2>&1 | FileCheck %s -#include - -void store(void) { - char *p = "abc"; - // CHECK: rfc0030-literal-write.c:[[@LINE+1]]:3: error: write through 'p', which points to a string literal [weavec::out-of-bounds] - p[0] = 'x'; -} - -char *split(void) { - // CHECK: rfc0030-literal-write.c:[[@LINE+1]]:17: error: write through '"a,b"', which points to a string literal [weavec::out-of-bounds] - return strtok("a,b", ","); -} - -// CHECK-NOT: string literal -void maybe(int c, char *buf) { - char *p = c ? "abc" : buf; - p[0] = 'x'; -} diff --git a/test/Analysis/rfc0030-owner-uniqueness.c b/test/Analysis/rfc0030-owner-uniqueness.c deleted file mode 100644 index 8e9af7bd..00000000 --- a/test/Analysis/rfc0030-owner-uniqueness.c +++ /dev/null @@ -1,71 +0,0 @@ -// RFC 0030 §9.4, *Owner uniqueness*: a release through a name that is not an -// owning place must not be re-attributed to the object that contains it. A -// field pointer aimed at an inner member of its own object (`s->cur = -// &s->first`) is such a name: releasing what `s->cur` points at is a release -// of `s->cur`, not of `s`, and a use of `s` afterwards is at most possible. -// The record such a release leaves on `s` is local — no summary may carry it -// — and a record of unknown origin joined into it must not lift that. -// RUN: %weavec %s -- 2>&1 | FileCheck --check-prefix=CLEAN --allow-empty %s -// RUN: %weavec --dump-analysis %s -- 2>/dev/null | FileCheck --check-prefix=DUMP %s -#include - -struct link { - struct link *next; -}; - -struct holder { - struct link first; - struct link *cur; - int count; -}; - -extern void opaque(struct holder *h); - -static void reset(struct holder *h) { h->cur = &h->first; } - -// Releases the chain hanging off `h->cur`; `h->cur` itself is replaced. -static void drop(struct holder *h) { - struct link *it = h->cur; - while (it != NULL) { - struct link *next = it->next; - free(it); - it = next; - h->count--; - } - h->cur = NULL; -} - -// The release is `h->cur`'s, not `h`'s: the summary must say so, and the -// unknown callee in between must not turn the local record on `h` into an -// exported one. -// In format 30 (RFC 0031 §6.1) `drop` releases the chain from `h->cur`, never -// `*h`. In `walk` the unknown callee leaves `h->cur` unknown, so what `drop` -// frees through it is the unknown-callee default on what `h` reaches -// (RFC 0030 §5.1); still no release of `*h` itself. -// DUMP-LABEL: function 'drop': -// DUMP-NOT: release *param0 free -// DUMP: release *param0->cur free may when always -// DUMP-NOT: release *param0 free -// DUMP: store param0->cur := null -// DUMP-LABEL: function 'walk': -// DUMP-NOT: release *param0 free -// DUMP: store param0->count := int -// DUMP-NEXT: store param0->cur := null -// DUMP-NOT: release *param0 free -// DUMP-LABEL: function 'use': -void walk(struct holder *h) { - reset(h); - opaque(h); - drop(h); -} - -// And the caller of such a function keeps its own object: no use-after-free -// on `h` at the read, and none on the second call either. -// CLEAN-NOT: error: -// CLEAN-NOT: use-after-free -// CLEAN-NOT: double-free -int use(struct holder *h) { - walk(h); - walk(h); - return h->count; -} diff --git a/test/Analysis/rfc0030-reallocator-outcomes.c b/test/Analysis/rfc0030-reallocator-outcomes.c deleted file mode 100644 index bd033029..00000000 --- a/test/Analysis/rfc0030-reallocator-outcomes.c +++ /dev/null @@ -1,99 +0,0 @@ -// RFC 0030 §8.2 and §9.1: an allocator that answers a zero new size by -// freeing its block and returning null (the C17 `realloc(p, 0)` contract) is -// a consume keyed on that outcome — `--dump-analysis` prints -// `outcome null{block: freed(free) when[nbytes =0]}`. Everything downstream -// turns on whether that `when` survives into the caller's summary. -// -// The three cases below are the reduction of what was the largest -// possible-temporal family on the corpus, and each pins a different half of -// the mechanism. They differ by one line at a time, so keep all three. -// RUN: %weavec %s -- 2>&1 | FileCheck %s -#include - -struct table { - void **slots; - int size; -}; - -static void *reallocate(void *block, size_t nbytes) { - if (nbytes == 0) { - free(block); - return NULL; - } - return realloc(block, nbytes); -} - -// A. TRUE POSITIVE, and the one expectation here that must never be -// weakened. An unguarded retry really does free twice: for `nbytes == 0` the -// first call frees `block` and returns null, and the retry frees it again. -// CHECK: rfc0030-reallocator-outcomes.c:[[@LINE+5]]:13: warning: 'block' may be freed twice [weavec::double-free] -// CHECK: rfc0030-reallocator-outcomes.c:[[@LINE+2]]:17: note: previously freed here on some paths -void *retry_unguarded(void *block, size_t nbytes) { - void *grown = reallocate(block, nbytes); - if (grown == NULL) - grown = reallocate(block, nbytes); - return grown; -} - -// B. CLEAN. Without a retry the `when[nbytes = 0]` guard reaches the caller, -// so `resize_plain`'s summary keeps `replaced` and two resizes in a row are -// not a double free — even though nothing here bounds the new size. -static void *call_once(void *block, size_t nbytes) { - return reallocate(block, nbytes); -} - -static void resize_plain(struct table *t, int n) { - void **grown = (void **)call_once(t->slots, (size_t)n * 8); - if (grown == NULL) - return; /* leave the table as it was */ - t->slots = grown; - t->size = n; -} - -void two_plain_resizes(struct table *t) { - resize_plain(t, t->size / 2); - resize_plain(t, t->size / 2); -} - -// C. CLEAN, and the reason the family above collapsed. The only difference -// from B is the guarded retry, which cannot free: it runs under -// `nbytes > 0`, the one condition under which `reallocate` does not free. -// The retry makes the returned value a merge of two call results, so the -// `PendingOutcome` of each reaches the merge at the end of the `if`. The -// retry's null class consumes nothing — `notePendingOutcome` drops a consume -// whose guard the arguments refute, and `nbytes > 0` refutes -// `when[nbytes = 0]` — and `PendingOutcome::unite` (lib/Core/AnalysisState.cpp, -// RFC 0030 §9.1) used to read that silence as disagreement and erase the -// outcome altogether. `call_with_retry` then had no classes, `resize_retry` -// lost `replaced`, and the caller saw two unguarded frees. -// -// A class one side consumes nothing on now leaves the other side's guarded -// consume standing, so `call_with_retry`'s summary matches -// `call_once`'s. Keep A warning: it is the same shape without the guard, -// and it is a real double free. -static void *call_with_retry(void *block, size_t nbytes) { - void *grown = reallocate(block, nbytes); - if (grown == NULL && nbytes > 0) - grown = reallocate(block, nbytes); /* after an emergency collection */ - return grown; -} - -static void resize_retry(struct table *t, int n) { - if (n <= 0) - return; /* the new size is non-zero, so nothing below can free */ - { - void **grown = (void **)call_with_retry(t->slots, (size_t)n * 8); - if (grown == NULL) - return; - t->slots = grown; - t->size = n; - } -} - -void two_retry_resizes(struct table *t) { - resize_retry(t, t->size / 2); - resize_retry(t, t->size / 2); -} - -// A's is the only one: neither B nor C may add another. -// CHECK-NOT: freed twice diff --git a/test/Analysis/rfc0030-slot-candidate-classes.c b/test/Analysis/rfc0030-slot-candidate-classes.c deleted file mode 100644 index f06b1918..00000000 --- a/test/Analysis/rfc0030-slot-candidate-classes.c +++ /dev/null @@ -1,61 +0,0 @@ -// RFC 0030 §9.1, *Widened cases*: a summary's `effects` map is the union over -// its result classes, and it is the per-class entries that qualify a consume — -// `realloc` records `p: moved(free)` unguarded there and `when[n zero]` only on -// its null class. A candidate with no class-dependent consumption keeps no -// classes at all, so joining it into that summary folds the classes away and -// would leave the bare union behind as a must-fact. It is a widened consume: -// the join claims it on paths the other candidate does not consume on, so no -// caller may make a definite finding from it. -// -// The slot below holds two targets: a `realloc` wrapper and one that only ever -// returns null. Releasing the old pointer after the call is at most *possible* -// (it is a real release only when the wrapper ran with a zero size), never a -// certain use after move. -// RUN: %weavec %s -- 2>&1 | FileCheck %s -// RUN: %weavec %s -- 2>&1 | FileCheck --check-prefix=NOTDEF %s -#include - -struct hooks { - void *(*reallocate)(void *, size_t); -}; - -static void *wrap_realloc(void *pointer, size_t size) { - return realloc(pointer, size); -} - -static void *always_fail(void *pointer, size_t size) { - (void)pointer; - (void)size; - return NULL; -} - -// CHECK: :[[@LINE+9]]:{{[0-9]+}}: warning: use of 'buffer' after it may have been moved -// NOTDEF-NOT: after it was moved -static char *grow(struct hooks *h, char *buffer, size_t size) { - char *grown = (char *)h->reallocate(buffer, size); - if (grown != NULL) { - return grown; - } - // The wrapper releases `buffer` only for a zero size, and `always_fail` - // never releases it, so this is a possible double free and no more. - free(buffer); - return NULL; -} - -int main(void) { - struct hooks growing; - struct hooks failing; - char *p = (char *)malloc(16); - if (p == NULL) { - return 1; - } - growing.reallocate = wrap_realloc; - failing.reallocate = always_fail; - p = grow(&growing, p, 32); - if (p == NULL) { - return 1; - } - free(p); - (void)failing; - return 0; -} diff --git a/test/Analysis/rfc0030-sound-defaults.c b/test/Analysis/rfc0030-sound-defaults.c deleted file mode 100644 index c98b6b8e..00000000 --- a/test/Analysis/rfc0030-sound-defaults.c +++ /dev/null @@ -1,55 +0,0 @@ -// RFC 0030 §5 and §3.1: the defaults for code the analysis cannot see. None -// of them is a diagnostic: the facets are not proven (RFC 0035 §8). -// RUN: %weavec %s -- 2>&1 | FileCheck --check-prefix=QUIET %s -#include -#include - -// QUIET-NOT: {{warning|error}}: - -void consume(char *p); - -// §5.1: 'consume' may have released or kept 'p'; the release after it is -// unresolved too, and replaces its record. -int unknown(void) { - char *p = malloc(8); - if (!p) - return 0; - consume(p); - int v = p[0]; - free(p); - return v; -} - -// §5.2: a platform function without a table entry borrows its arguments. -int system_api(int fd) { - struct winsize ws; - if (ioctl(fd, TIOCGWINSZ, &ws) == -1) - return 80; - return ws.ws_col; -} - -// §5.7: inline assembly handed `p` is unknown code. -int assembly(void) { - char *p = malloc(8); - if (!p) - return 0; - __asm__ volatile("" : : "r"(p) : "memory"); - int v = p[0]; - free(p); - return v; -} - -// §3.1: `b` may be the released `a` (both point to char). -void two(char *a, char *b) { - free(a); - b[0] = 1; -} - -// §11: without zero-initialisation a pointer that may be uninitialised may -// hold garbage no null check catches. -int deref(int c, int *q) { - int *p; - if (c) - p = q; - return *p; -} diff --git a/test/Annotations/Inputs/kinds-lstate.h b/test/Annotations/Inputs/kinds-lstate.h deleted file mode 100644 index c7b8ba07..00000000 --- a/test/Annotations/Inputs/kinds-lstate.h +++ /dev/null @@ -1,17 +0,0 @@ -/* A shape of Lua's lstate.h for the RFC 0030 §9.3 slot tests: the state and - * its allocator slot are declared in a header, so other units may store - * other allocators into it. */ -#ifndef WEAVEC_TEST_KINDS_LSTATE_H -#define WEAVEC_TEST_KINDS_LSTATE_H - -typedef unsigned long size_t; -typedef void *(*lua_Alloc)(void *ud, void *ptr, size_t osize, size_t nsize); - -struct global_State { - lua_Alloc frealloc; - void *ud; -}; - -struct global_State *new_state(void); - -#endif /* WEAVEC_TEST_KINDS_LSTATE_H */ diff --git a/test/Annotations/Inputs/vendor-kinds.h b/test/Annotations/Inputs/vendor-kinds.h deleted file mode 100644 index 5d78c23d..00000000 --- a/test/Annotations/Inputs/vendor-kinds.h +++ /dev/null @@ -1,9 +0,0 @@ -/* A third-party header found via -isystem: its attributes are §7.2 level 4 - * (RFC 0030). */ -#ifndef WEAVEC_TEST_VENDOR_KINDS_H -#define WEAVEC_TEST_VENDOR_KINDS_H - -void vendor_fill(int *p) __attribute__((nonnull)); -void *vendor_alloc(unsigned long n) __attribute__((malloc)); - -#endif /* WEAVEC_TEST_VENDOR_KINDS_H */ diff --git a/test/Annotations/Inputs/vendor.h b/test/Annotations/Inputs/vendor.h deleted file mode 100644 index 192632f8..00000000 --- a/test/Annotations/Inputs/vendor.h +++ /dev/null @@ -1,8 +0,0 @@ -/* Stands in for a third-party header found via -isystem. Calls into it are - * not reported as unchecked boundaries (RFC 0003). */ -#ifndef WEAVEC_TEST_VENDOR_H -#define WEAVEC_TEST_VENDOR_H - -void vendor_touch(void *p); - -#endif /* WEAVEC_TEST_VENDOR_H */ diff --git a/test/Annotations/kinds-alloc-size.c b/test/Annotations/kinds-alloc-size.c deleted file mode 100644 index d7a19f72..00000000 --- a/test/Annotations/kinds-alloc-size.c +++ /dev/null @@ -1,30 +0,0 @@ -// RFC 0030 §7.2, row 6: `alloc_size(i[, j])` on a function gives its result -// `sized(arg i [* arg j])`, at the ecosystem level (level 4 in a system -// header). The result of a call with constant arguments is Single-valid -// when the size covers the pointee (§7.3), so storing it keeps a slot -// `single`. -// RUN: %weavec --dump-kinds %s -- | FileCheck %s - -typedef unsigned long size_t; - -// CHECK: result pool_alloc: sized(param 0 scale 1 plus 0) nullable [declared, declared, shape ecosystem] -void *pool_alloc(size_t n) __attribute__((alloc_size(1))); -// CHECK: result pool_calloc: sized(param 1 scale 1 plus 0) nullable [declared, declared, shape ecosystem] factor param 0 -void *pool_calloc(size_t count, size_t size) __attribute__((alloc_size(2, 1))); - -struct item { long a, b; }; -struct list { - // CHECK: field list.one: single nullable [inferred, lower-bound] - struct item *one; - // CHECK: field list.small: unknown nullable [inferred] - // CHECK-NEXT: demoted at [[@LINE+8]]:3: a value with 8 bytes, fewer than the 16 of 'struct item' - struct item *small; - // CHECK: field list.many: single nullable [inferred, lower-bound] - struct item *many; -}; - -void make(struct list *l) { - l->one = pool_alloc(sizeof(struct item)); - l->small = pool_alloc(8); - l->many = pool_calloc(4, sizeof(struct item)); -} diff --git a/test/Annotations/kinds-array-parameters.c b/test/Annotations/kinds-array-parameters.c deleted file mode 100644 index 2ee30b6f..00000000 --- a/test/Annotations/kinds-array-parameters.c +++ /dev/null @@ -1,33 +0,0 @@ -// RFC 0030 §7.2, rows 9 and 10: `T p[static N]` is `counted(N) nonnull`; -// a VLA parameter `T p[n]`, with `n` an earlier parameter, is `counted(n) -// nullable`. A constant `T p[N]` without `static` is no requirement (C gives -// it none and code commonly passes fewer elements): it only yields a fix-it -// suggestion that inserts `static`. -// RUN: %weavec --dump-kinds %s -- | FileCheck %s - -// CHECK: param digest 0 'out': counted(32) nonnull [declared, declared, shape ecosystem, nullability ecosystem] -void digest(unsigned char out[static 32]); -// CHECK: param scale 1 'v': counted(param 0 scale 1 plus 0) nullable [declared, declared, shape ecosystem] -// CHECK: param scale 2 'w': counted(param 0 scale 1 plus 1) nullable [declared, declared, shape ecosystem] -void scale(int n, double v[n], double w[n + 1]); -// CHECK: param both 1 'm': counted(param 0 scale 1 plus 0) nonnull [declared, declared, shape ecosystem, nullability ecosystem] -void both(int n, double m[static n]); -// `[*]` names no count. -// CHECK-NOT: param star -void star(int n, double s[*]); - -// CHECK-NOT: param loose 0 -// CHECK: suggestion [[@LINE+1]]:18: 'k' is declared with 4 elements, which C does not require of callers; declare it 'k[static 4]' to require them (insert 'static ') -void loose(int k[4]); -// The suggestion goes on the definition, not on this prototype. -// CHECK-NOT: suggestion [[@LINE+1]]: -void later(int k[4]); -// CHECK: suggestion [[@LINE+1]]:18: 'k' is declared with 4 elements, which C does not require of callers; declare it 'k[static 4]' to require them (insert 'static ') -void later(int k[4]) { (void)k; } - -void use(unsigned char *d, double *x, int *k) { - digest(d); - scale(2, x, x); - both(2, x); - loose(k); -} diff --git a/test/Annotations/kinds-count-attributes.c b/test/Annotations/kinds-count-attributes.c deleted file mode 100644 index acc81959..00000000 --- a/test/Annotations/kinds-count-attributes.c +++ /dev/null @@ -1,23 +0,0 @@ -// RFC 0030 §7.2, row 5: Clang's `counted_by(n)`, `counted_by_or_null(n)`, -// `sized_by(n)` and `sized_by_or_null(n)` on fields (and parameters where -// Clang accepts them) are `counted(n)` or `sized(n)` at the ecosystem level, -// nonnull unless `_or_null`. A counted flexible array member has no -// nullability. `sized_by` counts bytes, unlike WEAVEC_SIZED_BY. -// RUN: %weavec --dump-kinds %s -- | FileCheck %s - -struct packet { - int n; - // CHECK: field packet.counted: counted(.n scale 1 plus 0) nonnull [declared, declared, shape ecosystem, nullability ecosystem] - int *__attribute__((counted_by(n))) counted; - // CHECK: field packet.maybe: counted(.n scale 1 plus 0) nullable [declared, declared, shape ecosystem, nullability ecosystem] - int *__attribute__((counted_by_or_null(n))) maybe; - // CHECK: field packet.bytes: sized(.n scale 1 plus 0) nonnull [declared, declared, shape ecosystem, nullability ecosystem] - int *__attribute__((sized_by(n))) bytes; - // CHECK: field packet.maybe_bytes: sized(.n scale 1 plus 0) nullable [declared, declared, shape ecosystem, nullability ecosystem] - void *__attribute__((sized_by_or_null(n))) maybe_bytes; - // CHECK: field packet.payload: counted(.n scale 1 plus 0) nullable [declared, declared, shape ecosystem] - char payload[] __attribute__((counted_by(n))); -}; - -// (This Clang accepts the attributes on fields only.) -int use(struct packet *p) { return p->n; } diff --git a/test/Annotations/kinds-counted-by.c b/test/Annotations/kinds-counted-by.c deleted file mode 100644 index 69866c94..00000000 --- a/test/Annotations/kinds-counted-by.c +++ /dev/null @@ -1,57 +0,0 @@ -// RFC 0030 §7.2, row 1: WEAVEC_COUNTED_BY(n) and WEAVEC_SIZED_BY(n) on -// parameters and fields are `counted(n)`, and `sized(n)` for void and -// character pointees. The two macros are synonyms. `n` names any sibling -// parameter (in any position) or sibling field; a name that resolves to -// nothing, or to a non-integer, is an invalid annotation and the kind is -// dropped. -// RUN: %weavec --dump-kinds %s -- | FileCheck %s -#include - -#if WEAVEC_H_VERSION_MINOR < 9 -#error "weavec.h 0.9 spells WEAVEC_COUNTED_BY" -#endif - -typedef unsigned long size_t; - -struct buf { - // CHECK: field buf.items: counted(.cap scale 1 plus 0) nullable [declared, declared, shape annotation] - int *WEAVEC_COUNTED_BY(cap) items; - // CHECK: field buf.bytes: sized(.cap scale 1 plus 0) nullable [declared, declared, shape annotation] - char *WEAVEC_COUNTED_BY(cap) bytes; - // CHECK: field buf.raw: sized(.cap scale 1 plus 0) nullable [declared, declared, shape annotation] - void *WEAVEC_SIZED_BY(cap) raw; - // CHECK: field buf.legacy: counted(.cap scale 1 plus 0) nullable [declared, declared, shape annotation] - long *WEAVEC_SIZED_BY(cap) legacy; - size_t cap; - // CHECK: problem [[@LINE+1]]:35: 'nowhere' in WEAVEC_COUNTED_BY does not name a parameter or field - int *WEAVEC_COUNTED_BY(nowhere) lost; - // CHECK: problem [[@LINE+1]]:32: 'lost' in WEAVEC_COUNTED_BY is not an integer parameter or field - int *WEAVEC_COUNTED_BY(lost) pointer_count; -}; - -// The count may follow the pointer: names resolve in any position. -// CHECK: param fill 0 'out': sized(param 1 scale 1 plus 0) nullable [declared, declared, shape annotation] -void fill(char *WEAVEC_COUNTED_BY(len) out, size_t len); -// CHECK: param sum 1 'values': counted(param 0 scale 1 plus 0) nullable [declared, declared, shape annotation] -long sum(int n, const int *WEAVEC_SIZED_BY(n) values); -// CHECK: problem [[@LINE+1]]:48: 'missing' in WEAVEC_COUNTED_BY does not name a parameter or field -void bad_name(char *WEAVEC_COUNTED_BY(missing) p, size_t n); -// CHECK: problem [[@LINE+1]]:42: 'q' in WEAVEC_COUNTED_BY is not an integer parameter or field -void bad_type(char *WEAVEC_COUNTED_BY(q) p, char *q); -// CHECK: problem [[@LINE+1]]:43: 'x' is declared WEAVEC_COUNTED_BY(n) but is not a pointer -void not_pointer(int WEAVEC_COUNTED_BY(n) x, int n); - -// A redeclaration that renames the count keeps the kind (the annotation -// was resolved on the declaration that wrote it). -// CHECK: param renamed 0 'data': counted(param 1 scale 1 plus 0) nullable [declared, declared, shape annotation] -void renamed(int *WEAVEC_COUNTED_BY(n) data, int n); -void renamed(int *data, int count) { (void)data; (void)count; } - -void use(char *c, int *i) { - fill(c, 1); - (void)sum(1, i); - bad_name(c, 1); - bad_type(c, c); - not_pointer(1, 1); -} -// CHECK-NOT: problem diff --git a/test/Annotations/kinds-defaults.c b/test/Annotations/kinds-defaults.c deleted file mode 100644 index addabefe..00000000 --- a/test/Annotations/kinds-defaults.c +++ /dev/null @@ -1,48 +0,0 @@ -// RFC 0030 §7.3, parameters and results: a parameter of a `static` function -// whose address is never taken gets the join of its arguments at the direct -// calls (Single-valid ones give `single`, nonnull when every one is); every -// other parameter is `single nullable` by A1, marked `relies-on-single` when -// a use of its value may rest on that default. `argv` of `main` is -// `counted(argc + 1) nonnull` at level 4 while `argc` and `argv` are never -// assigned. A result is the join over the unit's `return`s, or the A3 -// default for a function the unit only declares. -// RUN: %weavec --dump-kinds %s -- | FileCheck %s - -struct point { int x, y; }; - -// CHECK: param norm 0 'p': single nonnull [inferred, lower-bound] -static int norm(struct point *p) { return p->x * p->x + p->y * p->y; } -// CHECK: param shift 0 'p': unknown nullable [inferred] -// CHECK: demoted at {{[0-9]+}}:24: pointer arithmetic -static int shift(int *p) { return p[0]; } -// No direct call: the A1 default. -// CHECK: param unused 0 'p': single nullable [default, lower-bound] relies-on-single -static int unused(int *p) { return *p; } -// The address is taken: the A1 default. -// CHECK: param taken 0 'p': single nullable [default, lower-bound] relies-on-single -static int taken(int *p) { return *p; } - -int calls(int *array, int (*out)(int *)) { - struct point here = {1, 2}; - out = taken; - return norm(&here) + shift(array + 1) + shift(array) + out(array); -} - -// Comparisons, arithmetic and subscripts past 0 do not rest on the default. -// CHECK: param compare 0 'p': single nullable [default, lower-bound]{{$}} -int compare(const char *p, const char *q) { return p < q && p != 0 && p[1]; } - -// CHECK: result pick: single nonnull [inferred, lower-bound] -static struct point origin; -struct point *pick(void) { return &origin; } -// CHECK: result maybe: single nullable [inferred, lower-bound] -struct point *maybe(int c) { return c ? &origin : 0; } -// CHECK: result cursor: unknown nullable [inferred] -// CHECK-NEXT: demoted at [[@LINE+1]]:23: pointer arithmetic -int *cursor(int *p) { return p + 1; } -// CHECK: result external: single nullable [default, lower-bound] -struct point *external(void); -struct point *use_external(void) { return external(); } - -// CHECK: param main 1 'argv': counted(param 0 scale 1 plus 1) nonnull [declared, declared, shape system-header, nullability system-header] argv -int main(int argc, char **argv) { return argv[argc - 1] != 0; } diff --git a/test/Annotations/kinds-ended-by.c b/test/Annotations/kinds-ended-by.c deleted file mode 100644 index 63754010..00000000 --- a/test/Annotations/kinds-ended-by.c +++ /dev/null @@ -1,29 +0,0 @@ -// RFC 0030 §7.2, row 2: WEAVEC_ENDED_BY(q) on a pointer parameter or field -// is `ended-by(q)`: `[p, q)` lies in one object. `q` names a sibling -// pointer parameter or field, in any position; anything else is an invalid -// annotation and the kind is dropped. -// RUN: %weavec --dump-kinds %s -- | FileCheck %s -#include - -struct span { - // CHECK: field span.first: ended-by(.last) nullable [declared, declared, shape annotation] - const int *WEAVEC_ENDED_BY(last) first; - const int *last; - // CHECK: problem [[@LINE+1]]:36: 'size' in WEAVEC_ENDED_BY is not a pointer parameter or field - const int *WEAVEC_ENDED_BY(size) other; - int size; -}; - -// CHECK: param total 0 'p': ended-by(param 1) nullable [declared, declared, shape annotation] -long total(const int *WEAVEC_ENDED_BY(end) p, const int *end); -// CHECK: param backwards 1 'p': ended-by(param 0) nullable [declared, declared, shape annotation] -long backwards(const int *end, const int *WEAVEC_ENDED_BY(end) p); -// CHECK: problem [[@LINE+1]]:44: 'n' in WEAVEC_ENDED_BY is not a pointer parameter or field -long counted(const int *WEAVEC_ENDED_BY(n) p, int n); -// CHECK: problem [[@LINE+1]]:47: 'stop' in WEAVEC_ENDED_BY does not name a parameter or field -long nowhere(const int *WEAVEC_ENDED_BY(stop) p, const int *end); - -long use(const int *a, const int *b) { - return total(a, b) + backwards(b, a) + counted(a, 1) + nowhere(a, b); -} -// CHECK-NOT: problem diff --git a/test/Annotations/kinds-fields.c b/test/Annotations/kinds-fields.c deleted file mode 100644 index 15c3ecd7..00000000 --- a/test/Annotations/kinds-fields.c +++ /dev/null @@ -1,55 +0,0 @@ -// RFC 0030 §7.6: the candidates `count(d) == f + c` and `bytes(d) == f + c` -// (c in {0, 1}) of a struct's pointer and integer fields are proposed for -// Houdini, except for a struct some write the analysis cannot see reaches: a -// field's address taken, a byte-wise write, a conversion from another type. -// RUN: %weavec --dump-kinds %s -- | FileCheck %s -#include - -typedef unsigned long size_t; -void *malloc(size_t); -void *memcpy(void *, const void *, size_t); - -struct vec { - int *data; - size_t len; -}; -// CHECK: candidate struct vec: count(data) == len + 0 -// CHECK-NEXT: candidate struct vec: bytes(data) == len + 0 -// CHECK-NEXT: candidate struct vec: count(data) == len + 1 -// CHECK-NEXT: candidate struct vec: bytes(data) == len + 1 - -// A declared kind needs no candidate. -struct text { - char *WEAVEC_COUNTED_BY(cap) buf; - size_t cap; -}; - -// CHECK: disqualified struct taken: the address of 'taken.n' is taken -struct taken { int *p; int n; }; -// CHECK: disqualified struct copied: an object of 'copied' is written byte-wise (a byte-wise write by 'memcpy') -struct copied { int *p; int n; }; -// CHECK: disqualified struct punned: an object of 'punned' is converted from another type -struct punned { int *p; int n; }; - -void grow(struct vec *v, size_t n) { - v->data = malloc(n * sizeof *v->data); - v->len = n; -} - -void reset(struct vec *v, struct text *t) { - v->len = 0; - int x = 1; v->data = &x + (v->len > 0); - t->buf = 0; - t->cap = 0; -} - -void others(struct taken *a, struct copied *b, const struct copied *c, - long *raw) { - int *where = &a->n; - (void)where; - memcpy(b, c, sizeof *b); - struct punned *p = (struct punned *)raw; - (void)p; - a->p = b->p; - a->n = b->n; -} diff --git a/test/Annotations/kinds-fn-slots.c b/test/Annotations/kinds-fn-slots.c deleted file mode 100644 index 5dce4520..00000000 --- a/test/Annotations/kinds-fn-slots.c +++ /dev/null @@ -1,59 +0,0 @@ -// RFC 0030 §9.3: the unit's function-pointer slots. Designators stored, -// returned or passed are members of their slot; copies, direct calls and -// indirect calls connect slots; outside values open them. Per TU, a field -// of a record the main file defines is closed while no object of it comes -// from or goes to code outside the unit, and a `static` variable while its -// address does not escape. The Lua pattern: `g->frealloc` stored from an -// exported function's parameter is open, so calls through it are -// `open-unknown` in a per-TU compile (and `unresolved(callback)`, not -// silent). -// RUN: %weavec --dump-kinds %s -- -I %S/Inputs | FileCheck %s -#include "kinds-lstate.h" - -// Lua's lstate.c: the allocator comes from the caller. -struct global_State *lua_newstate(lua_Alloc f, void *ud) { - struct global_State *g = new_state(); - g->frealloc = f; - g->ud = ud; - return g; -} - -void *lua_grow(struct global_State *g, void *block, size_t n) { - return (*g->frealloc)(g->ud, block, 0, n); -} - -// A table the unit owns: its record is defined here and never leaves. -static void hello(void) {} -static void bye(void) {} -struct ops { void (*run)(void); }; -static struct ops table = {hello}; -static void (*hook)(void) = bye; -static void (*pick(int c))(void) { return c ? hello : bye; } -static void apply(void (*f)(void)) { f(); } - -int drive(int c, unsigned long bits) { - table.run(); - hook(); - pick(c)(); - apply(hello); - void (*raw)(void) = (void (*)(void))bits; - raw(); - return 0; -} - -// CHECK: slot constraints: -// CHECK-DAG: member :hello -> field :struct ops run -// CHECK-DAG: member :bye -> static :hook -// CHECK-DAG: member :hello -> param :apply 0 -// CHECK-DAG: member :bye -> result :pick -// CHECK-DAG: member :hello -> result :pick -// CHECK-DAG: subset param lua_newstate 0 -> field struct global_State frealloc -// CHECK-DAG: open local drive raw (a conversion from an integer) -// CHECK: slot rules: confined(:struct ops) -// CHECK-NEXT: indirect calls: -// CHECK-NEXT: 22:10: through field struct global_State frealloc: open-unknown ('struct global_State.frealloc' may be stored by code outside this unit) -// CHECK-NEXT: 32:38: through param :apply 0: closed-single {:hello} -// CHECK-NEXT: 35:3: through field :struct ops run: closed-single {:hello} -// CHECK-NEXT: 36:3: through static :hook: closed-single {:bye} -// CHECK-NEXT: 37:3: through result :pick: closed-join {:bye, :hello} -// CHECK-NEXT: 40:3: through local drive raw: open-unknown (a conversion from an integer) diff --git a/test/Annotations/kinds-must-access.c b/test/Annotations/kinds-must-access.c deleted file mode 100644 index 1f7d5d34..00000000 --- a/test/Annotations/kinds-must-access.c +++ /dev/null @@ -1,131 +0,0 @@ -// RFC 0030 §7.5, must-access requirements from the CFG and post-dominator -// tree: R1 `*p` gives `single nonnull`; R2 `p[i + k]` in a canonical counted -// loop gives `counted(e + k)` under `c < e`; R3 a pointer-range loop gives -// `ended-by(q)` under `p < q`; R4 a scan, or a string argument of a -// LibrarySpec row, gives `nul-terminated`; R5 an unguarded `p[e]` gives -// `counted(e + 1)`. A requirement of a static function whose address is not -// taken is enforced at its call sites; any other is the caller's contract. -// RUN: %weavec --dump-kinds %s -- | FileCheck %s - -typedef unsigned long size_t; -size_t strlen(const char *); -void abort(void) __attribute__((noreturn)); -void unknown(void); - -// CHECK: param r1 0 'p': {{.*}} -// CHECK-NEXT: requires single nonnull (R1) at [[@LINE+1]]:31 [caller-contract] -int r1(const int *p) { return *p; } - -// CHECK: param r2 0 'p': {{.*}} -// CHECK-NEXT: requires 0 < param 1 scale 1 plus 0 -> counted(param 1 scale 1 plus 1) nonnull (R2) at [[@LINE+3]]:5 [caller-contract] -void r2(int *p, int n) { - for (int i = 0; i < n; i++) - p[i + 1] = 0; -} - -// CHECK: param r2_inclusive 0 'p': {{.*}} -// CHECK-NEXT: requires 2 <= param 1 scale 1 plus 0 -> counted(param 1 scale 1 plus 1) nonnull (R2) at [[@LINE+3]]:5 [caller-contract] -void r2_inclusive(int *p, int n) { - for (int i = 2; i <= n; ++i) - p[i] = 0; -} - -// CHECK: param r3 0 'p': {{.*}} -// CHECK-NEXT: requires param 0 scale 1 plus 0 < param 1 scale 1 plus 0 -> ended-by(param 1) nonnull (R3) at [[@LINE+4]]:12 [caller-contract] -int r3(const int *p, const int *q) { - int sum = 0; - for (const int *x = p; x < q; ++x) - sum += *x; - return sum; -} - -// CHECK: param r3_while 0 'p': {{.*}} -// CHECK-NEXT: requires param 0 scale 1 plus 0 < param 1 scale 1 plus 0 -> ended-by(param 1) nonnull (R3) at [[@LINE+3]]:5 [caller-contract] -void r3_while(char *p, char *q) { - while (p < q) { - *p = 0; - p++; - } -} - -// CHECK: param r4_scan 0 's': {{.*}} -// CHECK-NEXT: requires nul-terminated nonnull (R4) at [[@LINE+3]]:10 [caller-contract] -size_t r4_scan(const char *s) { - size_t n = 0; - while (s[n]) - n++; - return n; -} - -// CHECK: param r4_library 0 's': {{.*}} -// CHECK-NEXT: requires nul-terminated nonnull (R4) at [[@LINE+1]]:43 [caller-contract] -size_t r4_library(const char *s) { return strlen(s) + 1; } - -// CHECK: param r5 0 'p': {{.*}} -// CHECK-NEXT: requires counted(param 1 scale 1 plus 0) nonnull (R5) at [[@LINE+1]]:41 [caller-contract] -int r5(const int *p, size_t n) { return p[n - 1]; } - -// Static and not address-taken: enforced at the calls. -// CHECK: param local 0 'p': {{.*}} -// CHECK-NEXT: requires counted(4) nonnull (R5) at [[@LINE+1]]:41 [call-sites] -static int local(const int *p) { return p[3]; } -int call_local(const int *p) { return local(p); } - -// No requirement when something before the access may leave the function -// or the caller may pass null on purpose. -// CHECK: param tested 0 'p': {{.*}} -// CHECK-NOT: requires -int tested(const int *p) { - if (!p) - return 0; - return *p; -} -// CHECK: param guarded 0 'p': {{.*}} -// CHECK-NOT: requires -int guarded(const int *p, int c) { return c ? *p : 0; } -// CHECK: param after_unknown 0 'p': {{.*}} -// CHECK-NOT: requires -int after_unknown(const int *p) { - unknown(); - return *p; -} -// CHECK: param after_abort 0 'p': {{.*}} -// CHECK-NOT: requires -int after_abort(const int *p, int c) { - if (c) - abort(); - return *p; -} -// CHECK: param reassigned 0 'p': {{.*}} -// CHECK-NOT: requires -int reassigned(const int *p, const int *q) { - p = q; - return *p; -} -// A loop that may stop early is no canonical counted loop. -// CHECK: param early 0 'p': {{.*}} -// CHECK-NOT: requires -int early(const int *p, int n) { - for (int i = 0; i < n; i++) - if (p[i] == 0) - break; - return 0; -} - -// A helper the unit defines that always returns keeps the access a -// must-access; one that may exit does not. -static void note(void) {} -static void fatal(void) { abort(); } -// CHECK: param after_note 0 'p': {{.*}} -// CHECK-NEXT: requires single nonnull (R1) at [[@LINE+3]]:10 [caller-contract] -int after_note(const int *p) { - note(); - return *p; -} -// CHECK: param after_fatal 0 'p': {{.*}} -// CHECK-NOT: requires -int after_fatal(const int *p, int c) { - if (c) - fatal(); - return *p; -} diff --git a/test/Annotations/kinds-nonnull-attributes.c b/test/Annotations/kinds-nonnull-attributes.c deleted file mode 100644 index 0b0e8681..00000000 --- a/test/Annotations/kinds-nonnull-attributes.c +++ /dev/null @@ -1,29 +0,0 @@ -// RFC 0030 §7.2, row 7: `nonnull` (all pointer parameters), `nonnull(i, ...)` -// (the listed ones), `nonnull` on a parameter, `returns_nonnull`, `_Nonnull` -// and `_Nullable` declare nullability at the ecosystem level (level 4 in a -// system header, where a LibrarySpec row outranks them). -// RUN: %weavec --dump-kinds %s -- -isystem %S/Inputs | FileCheck %s -#include - -// CHECK: param all 0 'a': unknown nonnull [declared, nullability ecosystem] -// CHECK: param all 1 'b': unknown nonnull [declared, nullability ecosystem] -void all(int *a, int *b) __attribute__((nonnull)); -// CHECK-NOT: param some 0 -// CHECK: param some 1 'b': unknown nonnull [declared, nullability ecosystem] -void some(int *a, int *b) __attribute__((nonnull(2))); -// CHECK: param each 0 'a': unknown nonnull [declared, nullability ecosystem] -// CHECK: param each 1 'b': unknown nonnull [declared, nullability ecosystem] -// CHECK: param each 2 'c': unknown nullable [declared, nullability ecosystem] -void each(int *a __attribute__((nonnull)), int *_Nonnull b, int *_Nullable c); -// CHECK: result fresh: single nonnull [default, lower-bound, nullability ecosystem] -int *fresh(void) __attribute__((returns_nonnull)); - -void use(int *p) { - all(p, p); - some(p, p); - each(p, p, p); - (void)fresh(); - vendor_fill(p); -} -// A system header's attribute is level 4. -// CHECK-NOT: vendor_fill diff --git a/test/Annotations/kinds-nullability.c b/test/Annotations/kinds-nullability.c deleted file mode 100644 index 3a0e778f..00000000 --- a/test/Annotations/kinds-nullability.c +++ /dev/null @@ -1,31 +0,0 @@ -// RFC 0030 §7.2, row 4: WEAVEC_NONNULL and WEAVEC_NULLABLE declare the -// nullability of a parameter, field, result or variable, and nothing about -// its shape, which the other rows or §7.3 inference give. Both on one -// declaration is a conflict: the weaker, nullable, is used. -// RUN: %weavec --dump-kinds %s -- | FileCheck %s -#include - -struct node { - // CHECK: field node.next: single nullable [inferred, lower-bound, nullability annotation] - struct node *WEAVEC_NULLABLE next; - // CHECK: field node.owner: single nonnull [inferred, lower-bound, nullability annotation] - struct node *WEAVEC_NONNULL owner; -}; - -// CHECK: variable head: single nonnull [inferred, lower-bound, nullability annotation] -struct node *WEAVEC_NONNULL head; - -// CHECK: param visit 0 'n': single nonnull [default, lower-bound, nullability annotation] -int visit(struct node *WEAVEC_NONNULL n) { return n->owner != 0; } -// CHECK: result find: single nullable [default, lower-bound, nullability annotation] -struct node *WEAVEC_NULLABLE find(int key); -// CHECK: result root: single nonnull [default, lower-bound, nullability annotation] -WEAVEC_NONNULL struct node *root(void); -// CHECK: param torn 0 'both': unknown nullable [declared, nullability annotation] -// CHECK-NEXT: problem [[@LINE+1]]:55: 'both' is declared both WEAVEC_NULLABLE and WEAVEC_NONNULL -void torn(struct node *WEAVEC_NONNULL WEAVEC_NULLABLE both); - -int use(void) { - torn(head); - return visit(find(1)) + visit(root()); -} diff --git a/test/Annotations/kinds-ownership-attributes.c b/test/Annotations/kinds-ownership-attributes.c deleted file mode 100644 index 019468cb..00000000 --- a/test/Annotations/kinds-ownership-attributes.c +++ /dev/null @@ -1,26 +0,0 @@ -// RFC 0030 §7.2, row 8: `malloc`, `ownership_returns(m)`, -// `ownership_takes(m, i)` and `ownership_holds(m, i)` on a function state an -// ownership contract (a fresh result of family `m`; argument `i` released or -// retained), which the unknown-callee rules (§5.1) read. They are contracts, -// not pointer kinds: the result's kind stays the A3 default. -// RUN: %weavec --dump-kinds %s -- | FileCheck %s - -typedef unsigned long size_t; - -// CHECK: result arena_new: single nullable [default, lower-bound] -// CHECK-NEXT: ownership arena_new: fresh(free) [ecosystem] -void *arena_new(size_t n) __attribute__((malloc)); -// CHECK: result pool_get: single nullable [default, lower-bound] -// CHECK-NEXT: ownership pool_get: fresh(pool) [ecosystem] -void *pool_get(size_t n) __attribute__((ownership_returns(pool))); -// CHECK: ownership pool_put: takes(0, pool) [ecosystem] -void pool_put(void *p) __attribute__((ownership_takes(pool, 1))); -// CHECK: ownership pool_keep: holds(1, pool) [ecosystem] -void pool_keep(int tag, void *p) __attribute__((ownership_holds(pool, 2))); - -void use(void) { - void *p = pool_get(8); - pool_keep(1, p); - pool_put(p); - (void)arena_new(8); -} diff --git a/test/Annotations/kinds-precedence.c b/test/Annotations/kinds-precedence.c deleted file mode 100644 index e7594d67..00000000 --- a/test/Annotations/kinds-precedence.c +++ /dev/null @@ -1,25 +0,0 @@ -// RFC 0030 §7.2 precedence: WEAVEC_* annotations (level 1) outrank -// ecosystem attributes (level 2), which outrank the LibrarySpec row -// (level 3) and attributes in system headers (level 4). Shape and -// nullability are resolved separately. A conflict within one level is an -// invalid annotation, and the weaker kind (the join) is used. -// RUN: %weavec --dump-kinds %s -- | FileCheck %s -#include - -// The annotation's shape wins; `static` still gives the nullability. -// CHECK: param mixed 1 'p': counted(param 0 scale 1 plus 0) nonnull [declared, declared, shape annotation, nullability ecosystem] -void mixed(int n, int p[static 4] WEAVEC_COUNTED_BY(n)); -// The annotation's nullability wins over the attribute's. -// CHECK: param lenient 0 'p': unknown nullable [declared, nullability annotation] -void lenient(int *WEAVEC_NULLABLE p __attribute__((nonnull))); -// Two redeclarations that disagree: the join, and a problem. -// CHECK: param torn 0 'p': unknown nullable [declared, shape annotation] -// CHECK-NEXT: problem [[@LINE+2]]:37: conflicting kinds for 'p': counted(param 1 scale 1 plus 0) nullable and counted(param 2 scale 1 plus 0) nullable -void torn(int *WEAVEC_COUNTED_BY(n) p, int n, int m); -void torn(int *WEAVEC_COUNTED_BY(m) p, int n, int m); - -void use(int *p) { - mixed(4, p); - lenient(p); - torn(p, 1, 1); -} diff --git a/test/Annotations/kinds-slots.c b/test/Annotations/kinds-slots.c deleted file mode 100644 index 44233358..00000000 --- a/test/Annotations/kinds-slots.c +++ /dev/null @@ -1,92 +0,0 @@ -// RFC 0030 §7.3, slot kinds: every pointer field and pointer variable with -// static storage starts `single` and is demoted to `unknown`, as a greatest -// fixpoint, by a store whose value is not Single-valid, and by the stores -// the syntax does not show: through `*q` into an address-taken slot, a slot -// address passed to a callee that may store through it, a byte-wise write, -// a store to another member of a union. Each demoted slot lists why. -// RUN: %weavec --dump-kinds %s -- | FileCheck %s - -typedef unsigned long size_t; -void *malloc(size_t); -void *memcpy(void *, const void *, size_t); -void *memset(void *, int, size_t); -void opaque(char **where); - -struct node { - // CHECK: field node.next: single nullable [inferred, lower-bound] - struct node *next; - // CHECK: field node.name: single nullable [inferred, lower-bound] - const char *name; - // CHECK: field node.follow: unknown nullable [inferred] - // CHECK-NEXT: demoted at [[@LINE+22]]:3: a load from 'node.cursor', which is unknown - struct node *follow; - // CHECK: field node.cursor: unknown nullable [inferred] - // CHECK-NEXT: demoted at [[@LINE+18]]:3: pointer arithmetic - struct node *cursor; - // CHECK: field node.tiny: unknown nullable [inferred] - // CHECK-NEXT: demoted at [[@LINE+17]]:3: a value with 4 bytes, fewer than the 48 of 'struct node' - struct node *tiny; - // CHECK: field node.sized: unknown nullable [inferred] - // CHECK-NEXT: demoted at [[@LINE+15]]:3: an allocation by 'malloc' of a size that is not constant - struct node *sized; -}; - -// CHECK: variable last: single nullable [inferred, lower-bound] -static struct node *last; - -void link(struct node *n, size_t count, const char *label) { - struct node *fresh = malloc(sizeof *fresh); - n->next = fresh; - n->name = label; - last = n; - n->cursor = n + 1; - n->follow = n->cursor; - n->tiny = malloc(4); - n->sized = malloc(count); - n->name = "literal"; - n->next = 0; -} - -// Stores through a pointer reach every address-taken slot of its type. -struct holder { - // CHECK: field holder.slot: unknown nullable [inferred] - // CHECK-NEXT: demoted at [[@LINE+10]]:3: pointer arithmetic - char *slot; - // CHECK: field holder.passed: unknown nullable [inferred] - // CHECK-NEXT: demoted at [[@LINE+7]]:3: pointer arithmetic - // CHECK-NEXT: demoted at [[@LINE+7]]:3: its address is passed to 'opaque' - char *passed; -}; - -void through(struct holder *h, char *buffer) { - char **where = &h->slot; - *where = buffer + 1; - opaque(&h->passed); -} - -// A byte-wise write demotes the object's pointer fields; a zero fill does -// not. -struct copied { - // CHECK: field copied.data: unknown nullable [inferred] - // CHECK-NEXT: demoted at [[@LINE+9]]:3: a byte-wise write by 'memcpy' - int *data; -}; -struct zeroed { - // CHECK: field zeroed.data: single nullable [inferred, lower-bound] - int *data; -}; - -void bytes(struct copied *c, struct zeroed *z, const char *raw) { - memcpy(c, raw, sizeof *c); - memset(z, 0, sizeof *z); -} - -// A store to another member of a union rewrites the pointer's bytes. -union value { - // CHECK: field value.p: unknown nullable [inferred] - // CHECK-NEXT: demoted at [[@LINE+5]]:28: a store to 'value.n', another member of the union - int *p; - long n; -}; - -void pun(union value *v) { v->n = 1; } diff --git a/test/Annotations/kinds-string.c b/test/Annotations/kinds-string.c deleted file mode 100644 index 328296b2..00000000 --- a/test/Annotations/kinds-string.c +++ /dev/null @@ -1,30 +0,0 @@ -// RFC 0030 §7.2, row 3: WEAVEC_STRING on a pointer parameter, field or -// return type is `nul-terminated`: a zero element lies at or after the -// pointer within its object. It takes no argument and has no extent term. -// RUN: %weavec --dump-kinds %s -- | FileCheck %s -#include - -struct entry { - // CHECK: field entry.key: nul-terminated nullable [declared, shape annotation] - const char *WEAVEC_STRING key; - // CHECK: field entry.wide: nul-terminated nullable [declared, shape annotation] - const int *WEAVEC_STRING wide; -}; - -// CHECK: param length 0 's': nul-terminated nullable [declared, shape annotation] -unsigned long length(const char *WEAVEC_STRING s); -// CHECK: result label: nul-terminated nullable [declared, shape annotation] -const char *WEAVEC_STRING label(int code); -// With WEAVEC_NONNULL, the nullability is declared too. -// CHECK: param greet 0 'name': nul-terminated nonnull [declared, shape annotation, nullability annotation] -void greet(const char *WEAVEC_STRING WEAVEC_NONNULL name); -// CHECK: problem [[@LINE+1]]:35: 'c' is declared WEAVEC_STRING but is not a pointer -int not_pointer(int WEAVEC_STRING c); - -// CHECK: problem [[@LINE+1]]:27: 'global' is declared WEAVEC_STRING but only parameters, fields and results take it -const char *WEAVEC_STRING global; - -unsigned long use(const char *s) { - greet(s); - return length(label(0)) + (unsigned long)not_pointer(1) + length(global); -} diff --git a/test/Annotations/ownership-annotations.c b/test/Annotations/ownership-annotations.c deleted file mode 100644 index f5969427..00000000 --- a/test/Annotations/ownership-annotations.c +++ /dev/null @@ -1,37 +0,0 @@ -// Ownership annotations are accepted, and unannotated exported functions -// are not reported. RFC 0030 §16 removes --report-unannotated, which -// offered the inferred annotations as fix-its; the ledger carries them. -// RUN: %weavec %s -- 2>&1 | FileCheck --allow-empty --check-prefix=QUIET %s -// QUIET-NOT: {{warning|error}}: -#include "../Inputs/prelude.h" -#include - -#if !WEAVEC_ENABLED -#error "weavec should define __WEAVEC__ when analysing" -#endif - -struct buffer; - -void annotated(struct buffer *WEAVEC_OWNED owned, - const struct buffer *WEAVEC_BORROWED shared, - struct buffer *WEAVEC_MUT exclusive) { - use(owned); - use((void *)shared); - use(exclusive); - free(owned); /* an owned parameter must be released (RFC 0007) */ -} - -void reads(int *p, int n) { use(p); } - -void frees(struct buffer *p) { free(p); } - -void writes(int *out, int v) { *out = v; } - -void *makes(void) { return malloc(8); } - -void untouched(int *p) {} - -// Static helpers are not part of the exported surface. -static void helper(int *p) { free(p); } -void caller(void) { helper(malloc(4)); } - diff --git a/test/Annotations/rfc0003-unknown-extern.c b/test/Annotations/rfc0003-unknown-extern.c deleted file mode 100644 index 4d5d8439..00000000 --- a/test/Annotations/rfc0003-unknown-extern.c +++ /dev/null @@ -1,42 +0,0 @@ -// RFC 0003: a call to a function with neither a definition in this -// translation unit nor ownership annotations is a checking boundary. RFC 0030 -// §5.1 makes it an unknown callee: no diagnostic, a Call site whose temporal -// facet is `unresolved(unknown-callee)` with a suggestion (a fix-it when the -// declaration is the program's own), and what it may have freed or kept is -// unresolved after it. A third-party header installed as a system header is -// no platform header (§5.2), so its functions are unknown too. RFC 0030 -// removes `annotation-required` and `--strict-externs`. -// RUN: %weavec %s -- -isystem %S/Inputs 2>&1 | FileCheck %s -#include "../Inputs/prelude.h" -#include -#include - -void mystery(void *p); -int pure(int x); -void *maker(void); -void annotated(void *WEAVEC_BORROWED p); - -void f(char *p) { - mystery(p); - mystery(p); - pure(1); - // `annotated` only borrows `p`, but `mystery` may have freed it. - annotated(p); - // No fix-it into a system header. The detail is the unknown callee's own - // suggestion (RFC 0031 §5.1), not the earlier unknown code (`mystery`) - // that may have freed `p`. - vendor_touch(p); - // `use` borrows its argument, but the argument points into memory an - // unknown callee made: unresolved, not trusted to `use`'s contract (RFC - // 0031 *Implementation amendments*, "Memory the analysis knows nothing - // about"). - use(maker()); -} - -// An unsafe region does not make a callee known. -void vouched(char *p) { - WEAVEC_UNSAFE { mystery(p); } -} - -// CHECK-NOT: warning: -// CHECK: 0 errors, 0 warnings diff --git a/test/Annotations/rfc0010-annotations.c b/test/Annotations/rfc0010-annotations.c deleted file mode 100644 index 6cfd1027..00000000 --- a/test/Annotations/rfc0010-annotations.c +++ /dev/null @@ -1,105 +0,0 @@ -// RFC 0010, *Annotations*: WEAVEC_RETAINS and WEAVEC_RELEASES describe a -// library's ref/unref pair with no body in view, WEAVEC_REFCOUNT marks a -// field as a count so a lost share is reported, and WEAVEC_OWNED_BY names -// the release family of a WEAVEC_OWNED result or parameter. -// RUN: not %weavec %s -- 2>&1 | FileCheck %s -// RUN: not %weavec --dump-analysis %s -- 2>&1 | FileCheck --check-prefix=DUMP %s -#include "../Inputs/prelude.h" -#include - -#if WEAVEC_H_VERSION_MINOR < 5 -#error "weavec.h 0.5 spells the RFC 0010 annotations" -#endif - -struct gobj; -struct gobj *g_new(void) WEAVEC_OWNED; -struct gobj *g_ref(struct gobj *WEAVEC_RETAINS o); -void g_unref(struct gobj *WEAVEC_RELEASES o); - -// Clean: the returning ref's result is a copy of its argument (the shape of -// `g_object_ref`), so `b` carries the share `g_ref` took. -int balanced(void) { - struct gobj *a = g_new(); - if (!a) - return -1; - struct gobj *b = g_ref(a); - g_unref(b); - g_unref(a); - return 0; -} - -int twice(void) { - struct gobj *a = g_new(); - if (!a) - return -1; - g_unref(a); - // CHECK: rfc0010-annotations.c:[[@LINE+1]]:3: error: 'a' is released twice [weavec::double-free] - g_unref(a); - return 0; -} - -int after(void) { - struct gobj *a = g_new(); - if (!a) - return -1; - g_unref(a); - // CHECK: rfc0010-annotations.c:[[@LINE+1]]:7: error: use of 'a' after its reference was released [weavec::use-after-free] - use(a); - return 0; -} - -// WEAVEC_REFCOUNT: the field is a count even though nothing here releases -// through it, so the share the local takes and drops is a leak. The object -// engine does not read WEAVEC_REFCOUNT: the increment is an integer store -// that leaves the caller's count unknown, no share is taken and no leak is -// reported (the retired golden comparison of RFC 0030, *Lit tests*). A leak is never -// a facet (RFC 0030 §3.4), so no outcome is lost. -struct node { - int WEAVEC_REFCOUNT refs; - struct node *next; -}; -// DUMP-LABEL: function 'retain_local': -// DUMP-NEXT: summary: -// DUMP-NEXT: always-returns -// DUMP-NEXT: store param0->next->refs := int [-2147483648, 2147483647] -void retain_local(struct node *n) { - struct node *p = n->next; - p->refs++; -} -struct sized { - int len; -}; -void not_a_count(struct sized *s) { - struct sized *t = s; - t->len++; -} - -// WEAVEC_OWNED_BY: the family of an owned result and an owned parameter. -struct handle; -void handle_close(struct handle *WEAVEC_OWNED h); -struct handle *handle_open(void) WEAVEC_OWNED WEAVEC_OWNED_BY(handle_close); -void handle_take(struct handle *WEAVEC_OWNED WEAVEC_OWNED_BY(handle_close) h); - -int right(void) { - struct handle *h = handle_open(); - if (!h) - return -1; - handle_close(h); - return 0; -} -int wrong(void) { - struct handle *h = handle_open(); - if (!h) - return -1; - // CHECK: rfc0010-annotations.c:[[@LINE+1]]:3: error: 'h' is released with 'free' but must be released with 'handle_close' [weavec::mismatched-release] - free(h); - return 0; -} - -// Contradictions on a definition (RFC 0010, *Diagnostics*). -// CHECK: rfc0010-annotations.c:[[@LINE+1]]:55: warning: 'o' is declared both WEAVEC_RETAINS and WEAVEC_RELEASES [weavec::invalid-annotation] -void both(struct gobj *WEAVEC_RETAINS WEAVEC_RELEASES o) { use(o); } -// CHECK: rfc0010-annotations.c:[[@LINE+1]]:16: warning: 'family_alone' is declared WEAVEC_OWNED_BY(handle_close) without WEAVEC_OWNED [weavec::invalid-annotation] -struct handle *family_alone(void) WEAVEC_OWNED_BY(handle_close) { return NULL; } - -// CHECK: 2 warnings and 3 errors generated. diff --git a/test/Annotations/rfc0011-sized-by.c b/test/Annotations/rfc0011-sized-by.c deleted file mode 100644 index 483c2129..00000000 --- a/test/Annotations/rfc0011-sized-by.c +++ /dev/null @@ -1,66 +0,0 @@ -// RFC 0011, *Annotation surface*: WEAVEC_SIZED_BY(n) on a pointer parameter -// gives it `n` elements inside the body and requires that many of every -// caller; a malformed one is `invalid-annotation`. -// RUN: not %weavec %s -- 2>&1 | FileCheck %s -// RUN: not %weavec %s -- 2>&1 | FileCheck --check-prefix=INVALID %s -// RUN: not %weavec --dump-analysis %s -- 2>/dev/null | FileCheck --check-prefix=DUMP %s -#include "../Inputs/prelude.h" -#include - -#if WEAVEC_H_VERSION_MINOR < 6 -#error "weavec.h 0.6 spells WEAVEC_SIZED_BY" -#endif - -// Inside the body the extent is `n` bytes; the access at `n` is one past. -// RFC 0030 §3.3: a declared count is a lower bound on the object, so that -// access is a checked facet, not an error, while the loop is proven. -// The summary records the stores over their element ranges (RFC 0031 §4.9). -// It carries no extent requirement (RFC 0031 §6.1): callers are held to the -// annotation itself, below. -// DUMP-LABEL: function 'fill': -// DUMP-NEXT: summary: -// DUMP-NEXT: always-returns -// DUMP-NEXT: store *param0[*] elements [0, param1) := int [0, 0] -// DUMP-NEXT: store *param0[*] elements [param1, param1 plus 1) := int [0, 0] -void fill(char *WEAVEC_SIZED_BY(n) p, size_t n) { - for (size_t i = 0; i < n; i++) - p[i] = 0; - p[n] = 0; -} - -// Elements, not bytes: `n` ints. `n - 1` is in the declared count only when -// `n` is positive, which the signed `n` does not promise: checked. -// DUMP-LABEL: function 'ints': -// DUMP-NEXT: summary: -// DUMP-NEXT: always-returns -// DUMP-NEXT: store *param0[*] := int [0, 0] may -void ints(int *WEAVEC_SIZED_BY(n) p, int n) { p[n - 1] = 0; } - -// The annotation is authoritative for a prototype with no body in view; it -// composes with the ownership annotation the boundary needs (RFC 0003). -void library_fill(char *WEAVEC_MUT WEAVEC_SIZED_BY(len) buf, size_t len); - -void calls(void) { - char buf[4]; - fill(buf, 4); - // CHECK: rfc0011-sized-by.c:[[@LINE+1]]:8: error: 'fill' requires 8 bytes behind 'buf', which has 4 bytes [weavec::out-of-bounds] - fill(buf, 8); - library_fill(buf, 4); - // CHECK: rfc0011-sized-by.c:[[@LINE+1]]:16: error: 'library_fill' requires 5 bytes behind 'buf', which has 4 bytes [weavec::out-of-bounds] - library_fill(buf, 5); - int four[4]; - ints(four, 4); - // CHECK: rfc0011-sized-by.c:[[@LINE+1]]:8: error: 'ints' requires 20 bytes behind 'four', which has 16 bytes [weavec::out-of-bounds] - ints(four, 5); -} - -// -- Malformed --------------------------------------------------------------- -// RFC 0030 §7.2: `AttributeReader` reports these before the engine runs, -// in its wording, and drops the kind. - -// INVALID: rfc0011-sized-by.c:[[@LINE+1]]:41: warning: 'x' is declared WEAVEC_SIZED_BY(n) but is not a pointer [weavec::invalid-annotation] -void not_pointer(int WEAVEC_SIZED_BY(n) x, int n) { (void)x; (void)n; } -// INVALID: rfc0011-sized-by.c:[[@LINE+1]]:43: warning: 'q' in WEAVEC_SIZED_BY is not an integer parameter or field [weavec::invalid-annotation] -void not_counter(char *WEAVEC_SIZED_BY(q) p, char *q) { (void)p; (void)q; } -// INVALID: rfc0011-sized-by.c:[[@LINE+1]]:39: warning: 'm' in WEAVEC_SIZED_BY does not name a parameter or field [weavec::invalid-annotation] -void no_such(char *WEAVEC_SIZED_BY(m) p, int n) { (void)p; (void)n; } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 798d6e2e..b411060d 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -89,7 +89,7 @@ set(WEAVEC_LIT_JOBS add_custom_target( check-weavec-lit COMMAND "${WEAVEC_LIT}" ${WEAVEC_LIT_ARGS} "${CMAKE_CURRENT_BINARY_DIR}" - DEPENDS weavec weavec-cc weavec_rt weavec_alloc + DEPENDS weavec-cc weavec_rt weavec_alloc COMMENT "Running WeaveC lit tests" USES_TERMINAL) add_dependencies(check-weavec check-weavec-lit) diff --git a/test/Driver/Inputs/buggy-header.h b/test/Driver/Inputs/buggy-header.h deleted file mode 100644 index df4534a2..00000000 --- a/test/Driver/Inputs/buggy-header.h +++ /dev/null @@ -1,10 +0,0 @@ -/* Deliberately buggy inline definitions for headers-analysed.c. */ -static inline void buggy(int *p) { - free(p); - free(p); -} -/* Referenced by nothing: never emitted, so never analysed. */ -static inline void unused_buggy(int *p) { - free(p); - free(p); -} diff --git a/test/Driver/Inputs/image-globals-lib.c b/test/Driver/Inputs/image-globals-lib.c new file mode 100644 index 00000000..363b9504 --- /dev/null +++ b/test/Driver/Inputs/image-globals-lib.c @@ -0,0 +1,6 @@ +// A shared image's global table and an accessor that indexes it through a +// pointer (image-globals.c). +int lib_table[4] = {1, 2, 3, 4}; +int *volatile lib_view = lib_table; + +int lib_read(long i) { return lib_view[i]; } diff --git a/test/Driver/Inputs/rfc0014-callback-client.c b/test/Driver/Inputs/rfc0014-callback-client.c deleted file mode 100644 index c2cbd666..00000000 --- a/test/Driver/Inputs/rfc0014-callback-client.c +++ /dev/null @@ -1,10 +0,0 @@ -#include -void invoke(void (*)(void *), void *); -static void drop(void *p) { free(p); } -int main(void) { - int *p = malloc(sizeof *p); - if (!p) - return 0; - invoke(drop, p); - return *p; -} diff --git a/test/Driver/Inputs/rfc0030-hook-state.c b/test/Driver/Inputs/rfc0030-hook-state.c deleted file mode 100644 index d63948fd..00000000 --- a/test/Driver/Inputs/rfc0030-hook-state.c +++ /dev/null @@ -1,12 +0,0 @@ -// The unit that stores the allocator (lstate.c's `lua_newstate`) and calls -// through it (`luaM_free`). -#include "rfc0030-hook-state.h" -static struct global_state the_state; -struct global_state *new_state(alloc_fn f, void *ud) { - the_state.frealloc = f; - the_state.ud = ud; - return &the_state; -} -void state_release(struct global_state *g, void *block, size_t size) { - (*g->frealloc)(g->ud, block, size, 0); -} diff --git a/test/Driver/Inputs/rfc0030-hook-state.h b/test/Driver/Inputs/rfc0030-hook-state.h deleted file mode 100644 index 1f46a6f5..00000000 --- a/test/Driver/Inputs/rfc0030-hook-state.h +++ /dev/null @@ -1,13 +0,0 @@ -// Lua's allocator hook, reduced: the state keeps the allocator it was made -// with in a function-pointer field, and frees through it. -#ifndef RFC0030_HOOK_STATE_H -#define RFC0030_HOOK_STATE_H -#include -typedef void *(*alloc_fn)(void *ud, void *ptr, size_t osize, size_t nsize); -struct global_state { - alloc_fn frealloc; - void *ud; -}; -struct global_state *new_state(alloc_fn f, void *ud); -void state_release(struct global_state *g, void *block, size_t size); -#endif diff --git a/test/Driver/Inputs/rfc0030-inspect.c b/test/Driver/Inputs/rfc0030-inspect.c deleted file mode 100644 index 1079cf15..00000000 --- a/test/Driver/Inputs/rfc0030-inspect.c +++ /dev/null @@ -1,4 +0,0 @@ -// The definition rfc0030-link-declarations.c declares WEAVEC_BORROWED: it -// frees its parameter (probe 38's 38_extern_impl.c). -#include -void inspect(char *p) { free(p); } diff --git a/test/Driver/asan-compat-fortify.c b/test/Driver/asan-compat-fortify.c new file mode 100644 index 00000000..f5640616 --- /dev/null +++ b/test/Driver/asan-compat-fortify.c @@ -0,0 +1,6 @@ +// RFC 0036 §1: on Darwin, an optimising build keeps the SDK's fortified +// string functions under ASan compatibility (Clang's ASan turns them off). +// REQUIRES: host-darwin +// RUN: %weavec_cc -O2 -E -dM %s | FileCheck %s +// CHECK: #define _FORTIFY_SOURCE 2 +#include diff --git a/test/Driver/asan-compat.c b/test/Driver/asan-compat.c new file mode 100644 index 00000000..a29e3d26 --- /dev/null +++ b/test/Driver/asan-compat.c @@ -0,0 +1,24 @@ +// RFC 0036 §5: weavec-cc compiles and preprocesses code as AddressSanitizer +// would, so that code written to be ASan-clean takes its ASan paths; +// -fno-weavec-asan-compat turns that off. On Darwin the SDK's fortification +// stays on (Clang turns it off for ASan, which intercepts the C library). +// RUN: %weavec_cc -fsyntax-only %s +// RUN: %weavec_cc -E -dM %s | FileCheck --check-prefix=MACROS %s +// Standard input gets WeaveC's flags as a named file does. +// RUN: %weavec_cc -E -dM -x c - < /dev/null | FileCheck --check-prefix=MACROS %s +// RUN: %weavec_cc -fno-weavec-asan-compat -E -dM -x c - < /dev/null | FileCheck --check-prefix=STDIN-PLAIN %s +// RUN: not %weavec_cc -fno-weavec-asan-compat -fsyntax-only %s 2>&1 | FileCheck --check-prefix=PLAIN %s +// RUN: not %weavec_cc -fweavec-checks=none -fsyntax-only %s 2>&1 | FileCheck --check-prefix=PLAIN %s +// +// MACROS: #define __SANITIZE_ADDRESS__ 1 +// MACROS: #define __WEAVEC__ 1 +// STDIN-PLAIN-NOT: __SANITIZE_ADDRESS__ +// STDIN-PLAIN: #define __WEAVEC__ 1 +// PLAIN: error: not built as for ASan + +#if !defined(__has_feature) +#error no __has_feature +#endif +#if !__has_feature(address_sanitizer) || !defined(__SANITIZE_ADDRESS__) +#error not built as for ASan +#endif diff --git a/test/Driver/checks-none-identity.c b/test/Driver/checks-none-identity.c index 45dcfd9b..7f89be48 100644 --- a/test/Driver/checks-none-identity.c +++ b/test/Driver/checks-none-identity.c @@ -18,14 +18,7 @@ // RUN: mv %t/unit.o %t/weavec.o // RUN: %clang -isysroot %t/sdk -O2 -flto=thin -c %s -o %t/unit.o -isystem %resource_dir -D__WEAVEC__=1 // RUN: cmp %t/unit.o %t/weavec.o -// -// The analysis (-fweavec-diagnose, RFC 0035 §8) runs beside the code -// generator, and its error, raised by -Werror=weavec, drops the object. -// RUN: not %weavec_cc -fweavec-diagnose -Werror=weavec -isysroot %t/sdk -DBUG -O2 -c %s -o %t/bug.o 2>&1 | FileCheck --check-prefix=BUG %s -// RUN: not ls %t/bug.o -// -// Actions that emit no code keep the analysis beside Clang's own consumer. -// RUN: not %weavec_cc -fweavec-diagnose -Werror=weavec -isysroot %t/sdk -DBUG -fsyntax-only %s 2>&1 | FileCheck --check-prefix=BUG %s + // A struct used through a pointer before its definition (debug info). struct node; @@ -78,17 +71,3 @@ int classify(int x) { return counter + table[0]; } } - -#ifdef BUG -void *malloc(unsigned long); -void free(void *); -int bug(void) { - int *p = malloc(sizeof *p); - if (!p) - return 0; - *p = 1; - free(p); - // BUG: checks-none-identity.c:[[@LINE+1]]:11: error: use of 'p' after it was freed [weavec::use-after-free] - return *p; -} -#endif diff --git a/test/Driver/compilation-database-p.c b/test/Driver/compilation-database-p.c deleted file mode 100644 index 003759ad..00000000 --- a/test/Driver/compilation-database-p.c +++ /dev/null @@ -1,50 +0,0 @@ -// RFC 0030 §16 (gate H3): `weavec --whole-program -p ` names no source, -// so it loads the compilation database of itself and analyses every -// file it lists as one program. Before, the tool dereferenced the database -// `CommonOptionsParser` leaves unloaded without a source, and crashed. A -// directory without a database is an error with a message. -// -// RUN: rm -rf %t && mkdir -p %t/build -// RUN: echo '[{"directory": "%S", "file": "%s", "arguments": ["cc", "-c", "%s", "-I%S/../WholeProgram/Inputs"]}, {"directory": "%S", "file": "%S/../WholeProgram/Inputs/node.c", "arguments": ["cc", "-c", "%S/../WholeProgram/Inputs/node.c", "-I%S/../WholeProgram/Inputs"]}]' > %t/build/compile_commands.json -// RUN: %weavec --whole-program -p %t/build 2>&1 | FileCheck %s -// -// `--extra-arg` applies to the database loaded here as to any other. -// RUN: %weavec --whole-program -p %t/build --extra-arg=-DSECOND 2>&1 | FileCheck --check-prefixes=CHECK,SECOND %s -// -// The parents of the directory are searched as for `-p` with a source, so -// the missing database is outside the build tree (which has one). -// RUN: not %weavec --whole-program -p /nonexistent/weavec-lit 2>&1 | FileCheck --check-prefix=MISSING %s -// RUN: not %weavec --whole-program 2>&1 | FileCheck --check-prefix=NONE %s -// RUN: not %weavec --whole-program -- -I%S 2>&1 | FileCheck --check-prefix=NONE %s -// RUN: not %weavec -p %t/build 2>&1 | FileCheck --check-prefix=NOINPUT %s -#include "../Inputs/prelude.h" -#include "node.h" - -// MISSING: weavec: error: no compilation database in '/nonexistent/weavec-lit' or any parent directory -// MISSING-NOT: Stack dump -// NONE: weavec: error: no compilation database with sources; give -p or list the files -// NOINPUT: weavec: error: no input files - -// Both units are analysed: `node_free`'s summary (from node.c) releases `n` -// where it is not null. `node_new` may return null and nothing tests it, so -// the release is possible at each call (RFC 0031 *Pending cases and exit -// splitting*: an effect keyed by a parameter's zero test the argument does -// not decide is possible): a possible double free, where the old engine gave -// a definite one (the retired golden comparison of RFC 0030, *Lit tests*). -int double_release(void) { - struct node *n = node_new(); - node_free(n); - // CHECK: compilation-database-p.c:[[@LINE+1]]:3: warning: 'n' may be freed twice [weavec::double-free] - node_free(n); - return 0; -} - -#ifdef SECOND -int second_release(void) { - struct node *n = node_new(); - node_free(n); - // SECOND: compilation-database-p.c:[[@LINE+1]]:3: warning: 'n' may be freed twice [weavec::double-free] - node_free(n); - return 0; -} -#endif diff --git a/test/Driver/diagnostics-format-sarif.c b/test/Driver/diagnostics-format-sarif.c deleted file mode 100644 index 69edc832..00000000 --- a/test/Driver/diagnostics-format-sarif.c +++ /dev/null @@ -1,40 +0,0 @@ -// RFC 0030 §16 (gate H3): -fdiagnostics-format=sarif crashes neither tool. -// -// weavec-cc renders a compile's diagnostics, those of -fweavec-diagnose -// among them, as that compile's SARIF document. -// RUN: rm -rf %t && mkdir -p %t -// RUN: %weavec_cc -fweavec-diagnose -fdiagnostics-format=sarif -c %s -o %t/bug.o -DBUG 2>&1 | FileCheck --check-prefix=SARIF %s -// -// weavec's runs create their SourceManager before Clang would attach the -// SARIF printer's document writer, so the tool refuses the flag. -// RUN: not %weavec %s -- -fdiagnostics-format=sarif 2>&1 | FileCheck --check-prefix=WEAVEC %s -// RUN: not %weavec --whole-program %s -- -fdiagnostics-format=sarif 2>&1 | FileCheck --check-prefix=WEAVEC %s -// RUN: not %weavec --extra-arg=-fdiagnostics-format=sarif %s -- 2>&1 | FileCheck --check-prefix=WEAVEC %s -// RUN: %weavec %s -- -fdiagnostics-format=sarif -fdiagnostics-format=clang -I%S/../WholeProgram/Inputs 2>&1 | FileCheck --check-prefix=LAST %s -#include "../Inputs/prelude.h" - -// SARIF: "text": "use of 'p' after it was freed [weavec::use-after-free]" -// SARIF: "version": "2.1.0" -// SARIF-NOT: Stack dump -// WEAVEC: weavec: error: -fdiagnostics-format=sarif is not supported -// WEAVEC-NOT: Stack dump -// LAST: weavec: {{.*}}diagnostics-format-sarif.c: {{[0-9]+}} sites: -// LAST-NOT: "version" -// LAST-NOT: error: - -#ifdef BUG -int use_after_free(void) { - char *p = malloc(1); - free(p); - return p[0]; -} -#else -#include "node.h" - -int main(void) { - struct node *n = node_new(); - node_free(n); - node_free(n); - return 0; -} -#endif diff --git a/test/Driver/dump-analysis.c b/test/Driver/dump-analysis.c deleted file mode 100644 index f73719e7..00000000 --- a/test/Driver/dump-analysis.c +++ /dev/null @@ -1,65 +0,0 @@ -// --dump-analysis prints each analysed function's format-30 summary -// (RFC 0031 §6.1, *Summary format 30*). The format is a debugging aid and may -// change; this pins only its shape and the facts each function is about. -// RUN: %weavec --dump-analysis %s -- | FileCheck %s -// RUN: %weavec --help | FileCheck --check-prefix=HELP %s -#include "../Inputs/prelude.h" - -struct s { - int *buf; -}; - -// The release under `if (c)` is guarded by `c` being non-zero: the summary -// keys it by the parameter's zero test (RFC 0009; RFC 0031 *Pending cases -// and exit splitting*: no result class separates it, so it is `lossy`). -// CHECK-LABEL: function 'f': -// CHECK-NEXT: summary: -// CHECK-NEXT: always-returns -// CHECK-NEXT: release *param0->buf free lossy may when param 1 !=0 -// CHECK-NOT: release -void f(struct s *p, int c) { - int x = 0; - int *a = &x; - if (c) - free(p->buf); - use(a); -} - -// A function with no effects has a summary of one line. -// CHECK-LABEL: function 'g': -// CHECK-NEXT: summary: -// CHECK-NEXT: always-returns -// CHECK-NOT: {{[a-z]}} -void g(void) {} - -// The summary is the function's interface as inferred (RFC 0003): the -// unchecked `malloc` result stored in `gp` is a fresh object of the `free` -// family with an extent of 4 bytes that may be null (RFC 0007, RFC 0008); -// the result is a copy of `p->buf`, and every exit dereferenced `p`. -// CHECK-LABEL: function 'h': -// CHECK-NEXT: summary: -// CHECK-NEXT: always-returns -// CHECK-NEXT: result path param0->buf when null nonnull -// CHECK-NEXT: store global0 := fresh#0 free extent 4{{.*}} maybe-null -// CHECK-NEXT: nonnull-on null param0 -// CHECK-NEXT: nonnull-on nonnull param0 -// Reading `p->buf` is spatially proven by `p`'s Single default (RFC 0030 -// §7.3), a lower bound of one `struct s`. -static int *gp; -int *h(struct s *p) { - gp = malloc(4); - return p->buf; -} - -// A pointer made from an integer is a value of unknown provenance in the -// summary, not a raw one (RFC 0033 §2). -// CHECK-LABEL: function 'launder': -// CHECK-NEXT: summary: -// CHECK-NEXT: always-returns -// CHECK-NEXT: result unknown maybe-null when null nonnull -char *launder(char *r, unsigned long x) { - r = (char *)x; - return r; -} - -// HELP: --dump-analysis diff --git a/test/Driver/flags.c b/test/Driver/flags.c new file mode 100644 index 00000000..f8fe2923 --- /dev/null +++ b/test/Driver/flags.c @@ -0,0 +1,45 @@ +// RFC 0035 §7, RFC 0036 §7: weavec-cc's flags parse, malformed values are +// errors, and the flags RFCs 0030, 0035 and 0036 removed are unknown WeaveC +// flags, as is every unknown -fweavec-*. The analysis's -W flags are Clang's +// to judge. +// RUN: rm -rf %t && mkdir -p %t +// RUN: %weavec_cc -fweavec-checks=report -fno-weavec-zero-init -fweavec-zero-init -fno-weavec-summary -fweavec-summary -fno-weavec-asan-compat -fweavec-asan-compat -fsyntax-only %s 2>&1 | FileCheck --allow-empty --check-prefix=ACCEPTED %s +// RUN: %weavec_cc -fweavec-checks=none -fsyntax-only %s 2>&1 | FileCheck --allow-empty --check-prefix=ACCEPTED %s +// RUN: not %weavec_cc -fweavec-ledger= -fsyntax-only %s 2>&1 | FileCheck --check-prefix=EMPTY %s +// RUN: not %weavec_cc -fweavec-checks -fsyntax-only %s 2>&1 | FileCheck --check-prefix=NOVALUE %s +// RUN: not %weavec_cc -fweavec-checks=maybe -fsyntax-only %s 2>&1 | FileCheck --check-prefix=BADVALUE %s +// RUN: not %weavec_cc -fweavec-diagnose -fsyntax-only %s 2>&1 | FileCheck --check-prefix=DIAGNOSE %s +// RUN: not %weavec_cc -fweavec-budget=7 -fsyntax-only %s 2>&1 | FileCheck --check-prefix=BUDGET %s +// RUN: not %weavec_cc -fweavec-dump-analysis -fsyntax-only %s 2>&1 | FileCheck --check-prefix=DUMP %s +// RUN: not %weavec_cc -fweavec-require=checked -fsyntax-only %s 2>&1 | FileCheck --check-prefix=REQUIRE %s +// RUN: not %weavec_cc -fno-weavec-runtime -fsyntax-only %s 2>&1 | FileCheck --check-prefix=RUNTIME %s +// RUN: %weavec_cc -Wno-weavec-leak -fsyntax-only %s 2>&1 | FileCheck --check-prefix=WARNING %s +// RUN: %weavec_cc --help-weavec | FileCheck --check-prefix=HELP %s +// +// WeaveC's flags reach every cc1 job as -Xclang arguments. +// RUN: %weavec_cc -### -fweavec-checks=verify -fno-weavec-zero-init -fweavec-ledger=%t/ledgers/ -fno-weavec-asan-compat -c %s -o %t/x.o 2>&1 | FileCheck --check-prefix=JOBS %s +// +// A file receives one ledger; an invocation that would write two to it is +// an error, while a directory takes one per unit. +// RUN: not %weavec_cc -fweavec-ledger=%t/one.json -c %s %S/Inputs/rfc0014-callback-helper.c 2>&1 | FileCheck --check-prefix=TWO %s + +// ACCEPTED-NOT: error: +// EMPTY: weavec-cc: error: missing value for '-fweavec-ledger' +// NOVALUE: weavec-cc: error: missing value for '-fweavec-checks' +// BADVALUE: weavec-cc: error: invalid value 'maybe' in '-fweavec-checks=maybe'; expected trap, report, verify or none +// DIAGNOSE: weavec-cc: error: unknown WeaveC flag '-fweavec-diagnose' +// BUDGET: weavec-cc: error: unknown WeaveC flag '-fweavec-budget=7' +// DUMP: weavec-cc: error: unknown WeaveC flag '-fweavec-dump-analysis' +// REQUIRE: weavec-cc: error: unknown WeaveC flag '-fweavec-require=checked' +// RUNTIME: weavec-cc: error: unknown WeaveC flag '-fno-weavec-runtime' +// WARNING: warning: unknown warning option '-Wno-weavec-leak' +// HELP: WeaveC flags of weavec-cc +// HELP: -fweavec-checks=trap|report|verify|none +// HELP: -fweavec-ledger= +// HELP: -fweavec-asan-compat +// HELP-NOT: -fweavec-diagnose +// JOBS: "-cc1" +// JOBS-SAME: "-fweavec-checks=verify" "-fno-weavec-zero-init" "-fweavec-ledger={{.*}}ledgers/" "-fno-weavec-asan-compat" +// TWO: weavec-cc: error: '-fweavec-ledger={{.*}}one.json' would receive 2 ledgers; name a directory (ending in '/') to get one ledger per unit + +int main(void) { return 0; } diff --git a/test/Driver/fortify.c b/test/Driver/fortify.c new file mode 100644 index 00000000..63420d43 --- /dev/null +++ b/test/Driver/fortify.c @@ -0,0 +1,14 @@ +// RFC 0036 §6: an optimising enforcing build for glibc defines +// _FORTIFY_SOURCE=3, so that the C library's fortified forms pass object +// sizes for the guards to check; not without optimisation, not when the +// command line chooses a level, not without checks. +// RUN: %weavec_cc -### --target=x86_64-linux-gnu -O2 -c %s 2>&1 | FileCheck --check-prefix=ON %s +// RUN: %weavec_cc -### --target=x86_64-linux-gnu -O0 -c %s 2>&1 | FileCheck --check-prefix=OFF %s +// RUN: %weavec_cc -### --target=x86_64-linux-gnu -O2 -D_FORTIFY_SOURCE=1 -c %s 2>&1 | FileCheck --check-prefix=CHOSEN %s +// RUN: %weavec_cc -### --target=x86_64-linux-gnu -O2 -fweavec-checks=none -c %s 2>&1 | FileCheck --check-prefix=OFF %s +// +// ON: "-D" "_FORTIFY_SOURCE=3" +// OFF-NOT: _FORTIFY_SOURCE +// CHOSEN: "-D" "_FORTIFY_SOURCE=1" +// CHOSEN-NOT: _FORTIFY_SOURCE=3 +int x; diff --git a/test/Driver/headers-analysed.c b/test/Driver/headers-analysed.c deleted file mode 100644 index 9b9d8075..00000000 --- a/test/Driver/headers-analysed.c +++ /dev/null @@ -1,14 +0,0 @@ -// RFC 0030 §5.6: every emitted function is analysed, `static inline` -// functions of user headers included (`--analyze-headers` is removed); each -// unit analyses its own copy. A header function CodeGen never emits (one no -// code of the unit references) is not, and definitions in system headers -// never are. -// RUN: not %weavec %s -- 2>&1 | FileCheck %s -// RUN: not %weavec_cc -fweavec-diagnose -Werror=weavec -fsyntax-only %s 2>&1 | FileCheck %s -#include "../Inputs/prelude.h" -#include "Inputs/buggy-header.h" - -// CHECK: buggy-header.h:4:3: error: 'p' is freed twice [weavec::double-free] -// CHECK-NOT: buggy-header.h:9: -// CHECK: 1 error generated. -void fine(int *p) { buggy(p); } diff --git a/test/Driver/image-globals.c b/test/Driver/image-globals.c new file mode 100644 index 00000000..50b5c685 --- /dev/null +++ b/test/Driver/image-globals.c @@ -0,0 +1,30 @@ +// RFC 0036 §4: a dylib's and a bundle's globals get redzones that the +// process's runtime knows, so an overflow of one inside the image stops as +// it does in the executable. +// REQUIRES: host-darwin +// RUN: rm -rf %t && mkdir -p %t +// RUN: %weavec_cc -dynamiclib -O1 %S/Inputs/image-globals-lib.c -o %t/libimage.dylib +// RUN: %weavec_cc -bundle -O1 %S/Inputs/image-globals-lib.c -o %t/image.bundle +// RUN: %weavec_cc -O1 %s -L%t -limage -Wl,-rpath,%t -o %t/prog +// RUN: %t/prog dylib 3 +// RUN: not --crash %t/prog dylib 4 2>&1 | FileCheck %s +// RUN: %t/prog %t/image.bundle 3 +// RUN: not --crash %t/prog %t/image.bundle 4 2>&1 | FileCheck %s +// +// CHECK: weavec: global-buffer-overflow at {{.*}}image-globals-lib.c:6:{{[0-9]+}}: read of 4 bytes +#include +#include +#include + +int lib_read(long i); + +int main(int argc, char **argv) { + long i = atol(argv[2]); + if (strcmp(argv[1], "dylib") == 0) + return lib_read(i) == 0; + void *image = dlopen(argv[1], RTLD_NOW | RTLD_LOCAL); + if (image == NULL) + return 2; + int (*read)(long) = (int (*)(long))dlsym(image, "lib_read"); + return read == NULL ? 2 : read(i) == 0; +} diff --git a/test/Driver/leak-default.c b/test/Driver/leak-default.c deleted file mode 100644 index d7265bbc..00000000 --- a/test/Driver/leak-default.c +++ /dev/null @@ -1,23 +0,0 @@ -// RFC 0033 §8: weavec-cc's analysis (-fweavec-diagnose, RFC 0035 §8) -// prints no leak warnings unless asked; -Wweavec-leak or -Wweavec asks, and -// -Werror=weavec-leak makes them errors. The weavec tool prints them. -// -// RUN: rm -rf %t && mkdir -p %t -// RUN: %weavec_cc -fweavec-diagnose -c %s -o %t/a.o 2>&1 | count 0 -// RUN: %weavec_cc -fweavec-diagnose -Wweavec-leak -c %s -o %t/b.o 2>&1 | FileCheck --check-prefix=LEAK %s -// RUN: %weavec_cc -fweavec-diagnose -Wweavec -c %s -o %t/c.o 2>&1 | FileCheck --check-prefix=LEAK %s -// RUN: not %weavec_cc -fweavec-diagnose -Werror=weavec-leak -c %s -o %t/d.o 2>&1 | FileCheck --check-prefix=ERROR %s -// RUN: %weavec %s -- 2>&1 | FileCheck --check-prefix=LEAK %s - -#include - -int keep(int n) { - char *p = malloc((size_t)n + 1); - if (p == NULL) - return 0; - p[0] = 1; - return p[0]; -} - -// LEAK: warning: 'p' is leaked [weavec::leak] -// ERROR: error: 'p' is leaked [weavec::leak] diff --git a/test/Driver/rfc0005-flags.c b/test/Driver/rfc0005-flags.c deleted file mode 100644 index bf0a53c0..00000000 --- a/test/Driver/rfc0005-flags.c +++ /dev/null @@ -1,69 +0,0 @@ -// RFC 0005, *Flags*: compiler-style control of WeaveC's diagnostics, on -// both tools, and the driver's -fweavec-* switches. -// -// A warning can be disabled and re-enabled; an error can only be lowered. -// RFC 0030: -Wno-weavec- drops the warnings of an id whose possible -// findings are warnings, and leaves its definite errors; an id RFC 0030 -// removed is unknown. -// RUN: %weavec %s -- 2>&1 | FileCheck --check-prefix=DEFAULT %s -// RUN: not %weavec %s -- -DBUG 2>&1 | FileCheck --check-prefix=BUG %s -// RUN: %weavec -Wno-weavec-leak %s -- 2>&1 | FileCheck --allow-empty --check-prefix=QUIET %s -// RUN: %weavec -Wno-weavec-leak -Wweavec-leak %s -- 2>&1 | FileCheck --check-prefix=DEFAULT %s -// RUN: %weavec -Wno-weavec %s -- 2>&1 | FileCheck --allow-empty --check-prefix=QUIET %s -// RUN: not %weavec -Werror=weavec-leak %s -- 2>&1 | FileCheck --check-prefix=RAISED %s -// RUN: not %weavec -Werror=weavec %s -- 2>&1 | FileCheck --check-prefix=RAISED %s -// RUN: %weavec -Wno-error=weavec-use-after-free %s -- -DBUG 2>&1 | FileCheck --check-prefix=LOWERED %s -// RUN: %weavec -Wno-error=weavec %s -- -DBUG 2>&1 | FileCheck --check-prefix=LOWERED %s -// RUN: not %weavec -Wno-weavec-use-after-free %s -- -DBUG 2>&1 | FileCheck --check-prefix=BUG %s -// RUN: not %weavec -Wno-weavec-unsafe-operation %s -- 2>&1 | FileCheck --check-prefix=REFUSED %s -// RUN: not %weavec -Wno-weavec-nonsense %s -- 2>&1 | FileCheck --check-prefix=UNKNOWN %s -// RUN: not %weavec -Wno-weavec-annotation-required %s -- 2>&1 | FileCheck --check-prefix=REMOVED %s -// -// The same spellings on the driver, whose analysis (-fweavec-diagnose, RFC -// 0035 §8) reports warnings unless -Werror=weavec raises them. -fno-weavec -// compiles without WeaveC; -fsyntax-only analyses but writes nothing. -// RUN: rm -rf %t && mkdir -p %t -// RUN: not %weavec_cc -fweavec-diagnose -Werror=weavec -c %s -o %t/bug.o -DBUG 2>&1 | FileCheck --check-prefix=BUG %s -// RUN: not ls %t/bug.o -// RUN: %weavec_cc -fweavec-diagnose -c %s -o %t/bug.o -DBUG 2>&1 | FileCheck --check-prefix=LOWERED %s -// RUN: %weavec_cc -fno-weavec -fweavec-diagnose -c %s -o %t/plain.o -DBUG 2>&1 | count 0 -// RUN: %weavec_cc -fweavec-diagnose -fsyntax-only -Wno-weavec-leak %s 2>&1 | count 0 -// RUN: not %weavec_cc -fweavec-bogus -c %s -o %t/x.o 2>&1 | FileCheck --check-prefix=BOGUS %s -// RUN: not %weavec_cc -Wno-weavec-unsafe-operation -c %s -o %t/x.o 2>&1 | FileCheck --check-prefix=REFUSED %s -// -// WeaveC's flags reach every cc1 job (as -Xclang arguments) and Clang's -// driver never sees them as its own. -// RUN: %weavec_cc -### -fno-weavec-zero-init -Wno-weavec-leak -c %s -o %t/x.o 2>&1 | FileCheck --check-prefix=JOBS %s -// RUN: %weavec_cc --version | FileCheck --check-prefix=VERSION %s -#include "../Inputs/prelude.h" - -// DEFAULT: warning: 'q' is leaked [weavec::leak] -// DEFAULT-NOT: error: -// RAISED: error: 'q' is leaked [weavec::leak] -// REFUSED: error: '-Wno-weavec-unsafe-operation': 'unsafe-operation' is an error and cannot be disabled; use -Wno-error=weavec-unsafe-operation to make it a warning -// QUIET-NOT: {{warning|error}}: -// UNKNOWN: error: unknown WeaveC diagnostic 'nonsense' in '-Wno-weavec-nonsense' -// REMOVED: error: unknown WeaveC diagnostic 'annotation-required' (removed by RFC 0030) -// BOGUS: weavec-cc: error: unknown WeaveC flag '-fweavec-bogus' -// JOBS: "-cc1" -// JOBS-SAME: "-D" "__WEAVEC__=1" -// JOBS-SAME: "-fno-weavec-zero-init" "-Wno-weavec-leak" -// JOBS-NOT: error: -// RFC 0033 §10: Clang's version block comes first, WeaveC's line after it. -// VERSION: clang version -// VERSION: weavec-cc version {{[0-9]+\.[0-9]+\.[0-9]+}} -// VERSION-NEXT: built with LLVM {{[0-9]+\.}} -void f(void) { - char *q = malloc(1); - (void)q; -} - -#ifdef BUG -int g(void) { - char *p = malloc(1); - free(p); - // BUG: rfc0005-flags.c:[[@LINE+2]]:10: error: use of 'p' after it was freed [weavec::use-after-free] - // LOWERED: rfc0005-flags.c:[[@LINE+1]]:10: warning: use of 'p' after it was freed [weavec::use-after-free] - return p[0]; -} -#endif diff --git a/test/Driver/rfc0007-flags.c b/test/Driver/rfc0007-flags.c deleted file mode 100644 index 3820921a..00000000 --- a/test/Driver/rfc0007-flags.c +++ /dev/null @@ -1,33 +0,0 @@ -// RFC 0007, *Diagnostics*: `leak` is a warning that can be disabled or -// promoted; a definite `mismatched-release` is an error that can only be -// lowered (RFC 0030: disabling the id drops only its possible findings). -// RUN: %weavec %s -- 2>&1 | FileCheck --check-prefix=DEFAULT %s -// RUN: %weavec -Wno-weavec-leak %s -- 2>&1 | FileCheck --allow-empty --check-prefix=QUIET %s -// RUN: not %weavec -Werror=weavec-leak %s -- 2>&1 | FileCheck --check-prefix=RAISED %s -// RUN: not %weavec %s -- -DMISMATCH 2>&1 | FileCheck --check-prefix=MISMATCH %s -// RUN: %weavec -Wno-error=weavec-mismatched-release %s -- -DMISMATCH 2>&1 | FileCheck --check-prefix=LOWERED %s -// RUN: not %weavec -Wno-weavec-mismatched-release %s -- -DMISMATCH 2>&1 | FileCheck --check-prefix=MISMATCH %s -#include -#include - -#ifdef MISMATCH -void mismatch(const char *path) { - FILE *f = fopen(path, "r"); - if (!f) - return; - // MISMATCH: rfc0007-flags.c:[[@LINE+2]]:3: error: 'f' is released with 'free' but must be released with 'fclose' [weavec::mismatched-release] - // LOWERED: rfc0007-flags.c:[[@LINE+1]]:3: warning: 'f' is released with 'free' but must be released with 'fclose' [weavec::mismatched-release] - free(f); -} -#else -void leak(void) { - char *p = malloc(8); - if (!p) - return; - // DEFAULT: rfc0007-flags.c:[[@LINE+2]]:3: warning: 'p' is leaked [weavec::leak] - // RAISED: rfc0007-flags.c:[[@LINE+1]]:3: error: 'p' is leaked [weavec::leak] - p[0] = 0; -} -#endif - -// QUIET-NOT: {{warning|error}}: diff --git a/test/Driver/rfc0008-flags.c b/test/Driver/rfc0008-flags.c deleted file mode 100644 index a2456c6f..00000000 --- a/test/Driver/rfc0008-flags.c +++ /dev/null @@ -1,38 +0,0 @@ -// RFC 0008, *Diagnostics*: `null-dereference`, `use-of-uninitialized` and -// `invalid-release` are errors by default; each can be lowered to a warning -// but not disabled. RFC 0030: `invalid-release` is a warning when only -// possible, so -Wno-weavec-invalid-release is accepted and drops just those. -// RUN: not %weavec %s -- 2>&1 | FileCheck --check-prefix=DEFAULT %s -// RUN: %weavec -Wno-error=weavec-null-dereference -Wno-error=weavec-use-of-uninitialized -Wno-error=weavec-invalid-release %s -- 2>&1 | FileCheck --check-prefix=LOWERED %s -// RUN: not %weavec -Wno-weavec-null-dereference %s -- 2>&1 | FileCheck --check-prefix=REFUSED-NULL %s -// RUN: not %weavec -Wno-weavec-use-of-uninitialized %s -- 2>&1 | FileCheck --check-prefix=REFUSED-UNINIT %s -// RUN: not %weavec -Wno-weavec-invalid-release %s -- 2>&1 | FileCheck --check-prefix=DEFAULT %s -#include - -// RFC 0030 §3.2: only a definite null dereference is a diagnostic. -int null_deref(void) { - int *p = NULL; - // DEFAULT: rfc0008-flags.c:[[@LINE+2]]:12: error: dereference of 'p', which is null [weavec::null-dereference] - // LOWERED: rfc0008-flags.c:[[@LINE+1]]:12: warning: dereference of 'p', which is null [weavec::null-dereference] - int v = *p; - return v; -} - -void uninit(void) { - char *p; - // RFC 0031 §5.9: the object engine reports the use of the uninitialised - // value at the operand `p`, not at the call. - // DEFAULT: rfc0008-flags.c:[[@LINE+2]]:8: error: use of 'p' before it was initialized [weavec::use-of-uninitialized] - // LOWERED: rfc0008-flags.c:[[@LINE+1]]:8: warning: use of 'p' before it was initialized [weavec::use-of-uninitialized] - free(p); -} - -void release(void) { - char buf[8]; - // DEFAULT: rfc0008-flags.c:[[@LINE+2]]:3: error: 'buf' is released but is not a heap object [weavec::invalid-release] - // LOWERED: rfc0008-flags.c:[[@LINE+1]]:3: warning: 'buf' is released but is not a heap object [weavec::invalid-release] - free(buf); -} - -// REFUSED-NULL: error: '-Wno-weavec-null-dereference': 'null-dereference' is an error and cannot be disabled; use -Wno-error=weavec-null-dereference to make it a warning -// REFUSED-UNINIT: error: '-Wno-weavec-use-of-uninitialized': 'use-of-uninitialized' is an error and cannot be disabled; use -Wno-error=weavec-use-of-uninitialized to make it a warning diff --git a/test/Driver/rfc0014-callback-link.c b/test/Driver/rfc0014-callback-link.c deleted file mode 100644 index 7a745566..00000000 --- a/test/Driver/rfc0014-callback-link.c +++ /dev/null @@ -1,13 +0,0 @@ -// RUN: not %weavec --whole-program %S/Inputs/rfc0014-callback-client.c %S/Inputs/rfc0014-callback-helper.c -- 2>&1 | FileCheck %s --check-prefix=LINK -// RFC 0014: a callback bug across units, in `weavec --whole-program`. -// -// RFC 0031 §6.1: format 30 has no callback inputs. In its own unit, -// `invoke`'s callback is unknown code that may do anything to `userdata` -// and to any global (RFC 0031 *Implementation amendments*, "Globals that -// unknown code may write"); -// across units, the call through the callback parameter takes the parameter's -// slot solution (RFC 0030 §9.3), which is `drop`, so the client's read -// after `invoke(drop, p)` is a use after free. -// LINK: error: use of 'p' after it was freed [weavec::use-after-free] -// LINK-NOT: annotation-required -// LINK-NOT: analysis-incomplete diff --git a/test/Driver/rfc0030-flags.c b/test/Driver/rfc0030-flags.c deleted file mode 100644 index 77b47fe6..00000000 --- a/test/Driver/rfc0030-flags.c +++ /dev/null @@ -1,91 +0,0 @@ -// RFC 0030 §16, RFC 0035 §7 and *Diagnostics*: the command line of both -// tools. -// -// weavec-cc's flags parse, malformed values are errors, and the flags RFC -// 0030 and RFC 0035 removed are unknown WeaveC flags, as is every unknown -// -fweavec-*. -// RUN: rm -rf %t && mkdir -p %t -// RUN: %weavec_cc -fweavec-checks=report -fweavec-budget=0 -fno-weavec-zero-init -fweavec-zero-init -fno-weavec-summary -fweavec-summary -fsyntax-only %s 2>&1 | FileCheck --allow-empty --check-prefix=ACCEPTED %s -// RUN: %weavec_cc -fweavec-checks=none -fweavec-budget=500000 -fsyntax-only %s 2>&1 | FileCheck --allow-empty --check-prefix=ACCEPTED %s -// RUN: not %weavec_cc -fweavec-budget=lots -fsyntax-only %s 2>&1 | FileCheck --check-prefix=BUDGET %s -// RUN: not %weavec_cc -fweavec-ledger= -fsyntax-only %s 2>&1 | FileCheck --check-prefix=EMPTY %s -// RUN: not %weavec_cc -fweavec-checks -fsyntax-only %s 2>&1 | FileCheck --check-prefix=NOVALUE %s -// RUN: not %weavec_cc -fweavec-strict -fsyntax-only %s 2>&1 | FileCheck --check-prefix=STRICT %s -// RUN: not %weavec_cc -fweavec-exclusive-borrows -fsyntax-only %s 2>&1 | FileCheck --check-prefix=EXCLUSIVE %s -// RUN: not %weavec_cc -fweavec-report-unannotated -fsyntax-only %s 2>&1 | FileCheck --check-prefix=UNANNOTATED %s -// RUN: not %weavec_cc -fno-weavec-analyze-headers -fsyntax-only %s 2>&1 | FileCheck --check-prefix=HEADERS %s -// RUN: not %weavec_cc -fweavec-require=checked -fsyntax-only %s 2>&1 | FileCheck --check-prefix=REQUIRE %s -// RUN: not %weavec_cc -fweavec-ledger-format=sarif -fsyntax-only %s 2>&1 | FileCheck --check-prefix=FORMAT %s -// RUN: not %weavec_cc -fweavec-link=analyze -fsyntax-only %s 2>&1 | FileCheck --check-prefix=LINK %s -// RUN: not %weavec_cc -fweavec-print-prelude -fsyntax-only %s 2>&1 | FileCheck --check-prefix=PRELUDE %s -// RUN: not %weavec_cc -fno-weavec-runtime -fsyntax-only %s 2>&1 | FileCheck --check-prefix=RUNTIME %s -// RUN: %weavec_cc --help-weavec | FileCheck --check-prefix=HELP %s -// -// WeaveC's flags reach every cc1 job as -Xclang arguments. -// RUN: %weavec_cc -### -fweavec-checks=verify -fno-weavec-zero-init -fweavec-ledger=%t/ledgers/ -fweavec-budget=7 -c %s -o %t/x.o 2>&1 | FileCheck --check-prefix=JOBS %s -// -// A file receives one ledger; an invocation that would write two to it is -// an error, while a directory takes one per unit. -// RUN: not %weavec_cc -fweavec-ledger=%t/one.json -c %s %S/Inputs/rfc0014-callback-helper.c 2>&1 | FileCheck --check-prefix=TWO %s -// -// weavec: its options, and the removed ones are unknown arguments. -// RUN: %weavec --budget=0 --no-zero-init %s -- 2>&1 | FileCheck --allow-empty --check-prefix=ACCEPTED %s -// RUN: not %weavec --strict-externs %s -- 2>&1 | FileCheck --check-prefix=TOOL-STRICT %s -// RUN: not %weavec --exclusive-borrows %s -- 2>&1 | FileCheck --check-prefix=TOOL-EXCLUSIVE %s -// RUN: not %weavec --report-unannotated %s -- 2>&1 | FileCheck --check-prefix=TOOL-UNANNOTATED %s -// RUN: not %weavec --analyze-headers %s -- 2>&1 | FileCheck --check-prefix=TOOL-HEADERS %s -// RUN: not %weavec --require=checked %s -- 2>&1 | FileCheck --check-prefix=TOOL-REQUIRE %s -// RUN: not %weavec --ledger=%t/one.json %s -- 2>&1 | FileCheck --check-prefix=TOOL-LEDGER %s -// RUN: not %weavec --ledger-format=sarif %s -- 2>&1 | FileCheck --check-prefix=TOOL-FORMAT %s -// RUN: not %weavec --no-runtime %s -- 2>&1 | FileCheck --check-prefix=TOOL-RUNTIME %s -// RUN: not %weavec --dump-record=%t/x.o.weavec 2>&1 | FileCheck --check-prefix=TOOL-RECORD %s -// -// The -W flags know the ids RFC 0030 adds and RFC 0035 keeps, refuse to -// disable the ones that are always errors, and treat an id RFC 0030 removed, -// and the require levels' ids RFC 0035 deleted, as unknown. -// RUN: %weavec -Wweavec-allocation-failure -Wno-weavec-leak -Werror=weavec-contradicted-assumption %s -- 2>&1 | FileCheck --allow-empty --check-prefix=ACCEPTED %s -// RUN: %weavec_cc -Wno-weavec-allocation-failure -Werror=weavec-leak -fsyntax-only %s 2>&1 | FileCheck --allow-empty --check-prefix=ACCEPTED %s -// RUN: not %weavec_cc -Wno-weavec-contradicted-assumption -fsyntax-only %s 2>&1 | FileCheck --check-prefix=CONTRADICTED %s -// RUN: not %weavec -Wno-weavec-checking-incomplete %s -- 2>&1 | FileCheck --check-prefix=REMOVED-INCOMPLETE %s -// RUN: not %weavec_cc -Werror=weavec-checking-failed -fsyntax-only %s 2>&1 | FileCheck --check-prefix=REMOVED-FAILED %s -// RUN: not %weavec -Wno-weavec-unresolved-operation %s -- 2>&1 | FileCheck --check-prefix=UNRESOLVED %s -// RUN: not %weavec_cc -Wno-error=weavec-unchecked-operation -fsyntax-only %s 2>&1 | FileCheck --check-prefix=UNCHECKED %s - -// ACCEPTED-NOT: error: -// BUDGET: weavec-cc: error: invalid value 'lots' in '-fweavec-budget=lots'; expected a number -// EMPTY: weavec-cc: error: missing value for '-fweavec-ledger' -// NOVALUE: weavec-cc: error: missing value for '-fweavec-checks' -// STRICT: weavec-cc: error: unknown WeaveC flag '-fweavec-strict' -// EXCLUSIVE: weavec-cc: error: unknown WeaveC flag '-fweavec-exclusive-borrows' -// UNANNOTATED: weavec-cc: error: unknown WeaveC flag '-fweavec-report-unannotated' -// HEADERS: weavec-cc: error: unknown WeaveC flag '-fno-weavec-analyze-headers' -// REQUIRE: weavec-cc: error: unknown WeaveC flag '-fweavec-require=checked' -// FORMAT: weavec-cc: error: unknown WeaveC flag '-fweavec-ledger-format=sarif' -// LINK: weavec-cc: error: unknown WeaveC flag '-fweavec-link=analyze' -// PRELUDE: weavec-cc: error: unknown WeaveC flag '-fweavec-print-prelude' -// RUNTIME: weavec-cc: error: unknown WeaveC flag '-fno-weavec-runtime' -// HELP: WeaveC flags of weavec-cc -// HELP: -fweavec-checks=trap|report|verify|none -// HELP: -fweavec-ledger= -// HELP: -fweavec-diagnose -// HELP-NOT: -fweavec-print-prelude -// HELP-NOT: -fweavec-require -// JOBS: "-cc1" -// JOBS-SAME: "-fweavec-checks=verify" "-fno-weavec-zero-init" "-fweavec-ledger={{.*}}ledgers/" "-fweavec-budget=7" -// TWO: weavec-cc: error: '-fweavec-ledger={{.*}}one.json' would receive 2 ledgers; name a directory (ending in '/') to get one ledger per unit -// TOOL-STRICT: Unknown command line argument '--strict-externs' -// TOOL-EXCLUSIVE: Unknown command line argument '--exclusive-borrows' -// TOOL-UNANNOTATED: Unknown command line argument '--report-unannotated' -// TOOL-HEADERS: Unknown command line argument '--analyze-headers' -// TOOL-REQUIRE: Unknown command line argument '--require=checked' -// TOOL-LEDGER: Unknown command line argument '--ledger={{.*}}one.json' -// TOOL-FORMAT: Unknown command line argument '--ledger-format=sarif' -// TOOL-RUNTIME: Unknown command line argument '--no-runtime' -// TOOL-RECORD: Unknown command line argument '--dump-record={{.*}}x.o.weavec' -// CONTRADICTED: weavec-cc: error: '-Wno-weavec-contradicted-assumption': 'contradicted-assumption' is an error and cannot be disabled; use -Wno-error=weavec-contradicted-assumption to make it a warning -// REMOVED-INCOMPLETE: weavec: error: unknown WeaveC diagnostic 'checking-incomplete' (removed by RFC 0030) -// REMOVED-FAILED: weavec-cc: error: unknown WeaveC diagnostic 'checking-failed' (removed by RFC 0030) -// UNRESOLVED: weavec: error: unknown WeaveC diagnostic 'unresolved-operation' (removed by RFC 0035) -// UNCHECKED: weavec-cc: error: unknown WeaveC diagnostic 'unchecked-operation' (removed by RFC 0035) - -int main(void) { return 0; } diff --git a/test/Driver/rfc0030-link-declarations.c b/test/Driver/rfc0030-link-declarations.c deleted file mode 100644 index 5da61cd4..00000000 --- a/test/Driver/rfc0030-link-declarations.c +++ /dev/null @@ -1,28 +0,0 @@ -// RFC 0030 §13.2 step 3 (probe 38), as RFC 0035 §8 keeps it: a declaration -// that says WEAVEC_BORROWED while the definition in another unit frees the -// parameter is an `annotation-mismatch` error at the declaration in -// `weavec --whole-program`, which the program's summary line counts. -// -// RUN: not %weavec --whole-program %s %S/Inputs/rfc0030-inspect.c -- 2>&1 | FileCheck --check-prefix=LINK %s -// RUN: %weavec --whole-program -Wno-error=weavec-annotation-mismatch %s %S/Inputs/rfc0030-inspect.c -- 2>&1 | FileCheck --check-prefix=LOWERED %s -#include -#include - -// LINK: rfc0030-link-declarations.c:[[@LINE+2]]:6: error: 'inspect' is declared WEAVEC_BORROWED here but its definition in '{{.*}}rfc0030-inspect.c' frees 'p' [weavec::annotation-mismatch] -// LINK: rfc0030-inspect.c:4:6: note: defined here -void inspect(char *WEAVEC_BORROWED p); - -int main(void) { - char *p = malloc(8); - if (!p) - return 1; - p[0] = 1; - inspect(p); - int r = p[0]; - free(p); - return r; -} - -// LINK: weavec: program rfc0030-link-declarations: {{.*}}; 1 error, 0 warnings -// LOWERED: warning: 'inspect' is declared WEAVEC_BORROWED here -// LOWERED: weavec: program rfc0030-link-declarations: {{.*}}; 0 errors, 1 warning diff --git a/test/Driver/rfc0030-link-slots.c b/test/Driver/rfc0030-link-slots.c deleted file mode 100644 index 421caeae..00000000 --- a/test/Driver/rfc0030-link-slots.c +++ /dev/null @@ -1,36 +0,0 @@ -// RFC 0030 §9.3, §13.2 step 2: `weavec --whole-program` solves the -// function-pointer slots of every unit together. Lua's allocator hook: one -// unit stores its caller's allocator into `g->frealloc` and frees through -// it, another passes its own `l_alloc`. Each unit alone sees an open field; -// the program sees the field closed with the one target `l_alloc`, and the -// engine's runs receive that solution (`ProgramFacts`), so the free through -// the field is a call of `l_alloc` there. -// -// RUN: %weavec --whole-program --dump-analysis %s %S/Inputs/rfc0030-hook-state.c -- -I%S/Inputs 2>/dev/null | FileCheck --check-prefix=CLOSED %s -#include -#include "rfc0030-hook-state.h" - -static void *l_alloc(void *ud, void *ptr, size_t osize, size_t nsize) { - (void)ud; - (void)osize; - if (nsize == 0) { - free(ptr); - return NULL; - } - return realloc(ptr, nsize); -} - -int main(void) { - struct global_state *g = new_state(l_alloc, NULL); - char *p = malloc(8); - if (!p) - return 1; - p[0] = 1; - state_release(g, p, 8); - return 0; -} - -// CLOSED: program slots: -// CLOSED: field struct global_state frealloc: {{[{].*}}rfc0030-link-slots.c:l_alloc} closed -// CLOSED: param new_state 0: {{[{].*}}rfc0030-link-slots.c:l_alloc} closed - diff --git a/test/Driver/system-headers.c b/test/Driver/system-headers.c deleted file mode 100644 index 29a237ff..00000000 --- a/test/Driver/system-headers.c +++ /dev/null @@ -1,20 +0,0 @@ -// Real system and builtin headers must resolve without extra flags: weavec -// points Clang at the resource directory of the installation it was built -// against. -// RUN: %weavec %s -- 2>&1 | FileCheck --allow-empty --check-prefix=QUIET %s -// QUIET-NOT: {{warning|error}}: -#include -#include -#include - -#include - -size_t safe(void) { - char *WEAVEC_OWNED s = malloc(16); - if (s == NULL) - return 0; - strcpy(s, "hi"); - size_t n = strlen(s); - free(s); - return n; -} diff --git a/test/Driver/version.c b/test/Driver/version.c index 52072478..9b472498 100644 --- a/test/Driver/version.c +++ b/test/Driver/version.c @@ -1,15 +1,8 @@ -// RUN: %weavec --version | FileCheck %s -// RUN: %weavec --help | FileCheck --check-prefix=HELP %s +// RFC 0033 §10: weavec-cc prints Clang's version block first, so that a +// configure script that reads the first line takes the Clang path, and its +// own after it. +// RUN: %weavec_cc --version | FileCheck %s // -// CHECK: weavec version {{[0-9]+\.[0-9]+\.[0-9]+}} +// CHECK: clang version +// CHECK: weavec-cc version {{[0-9]+\.[0-9]+\.[0-9]+}} // CHECK-NEXT: built with LLVM {{[0-9]+\.}} -// -// HELP: weavec options -// HELP-DAG: --budget -// HELP-DAG: --no-zero-init -// HELP-DAG: --whole-program -// HELP-NOT: --report-unannotated -// HELP-NOT: --ledger -// HELP-NOT: --require -// HELP-NOT: --dump-record -// HELP-NOT: --no-runtime diff --git a/test/Guards/alloca-entry.c b/test/Guards/alloca-entry.c new file mode 100644 index 00000000..2b01c69f --- /dev/null +++ b/test/Guards/alloca-entry.c @@ -0,0 +1,22 @@ +// RFC 0035 §3.4: a dynamic alloca at the very top of a function (libsodium's +// sodium_stackzero) gets its redzone, at every optimisation level. +// RUN: %weavec_cc -O0 %s -o %t0 && %t0 64 +// RUN: %weavec_cc -O2 %s -o %t2 && %t2 64 +// RUN: not --crash %t2 0 2>&1 | FileCheck %s +#include +#include + +__attribute__((noinline)) void zero(void *p, size_t n, size_t at) { + memset(p, 0, n); + // CHECK: weavec: {{(dynamic-)?}}stack-buffer-overflow at {{.*}}alloca-entry.c:[[@LINE+1]]:{{[0-9]+}}: write of 1 bytes + ((char *)p)[at] = 1; +} + +void stackzero(size_t len, size_t at) { zero(__builtin_alloca(len), len, at); } + +int main(int argc, char **argv) { + // An index inside the alloca, or (0) one past its 64 bytes. + size_t at = (size_t)atol(argv[argc - 1]); + stackzero(64, at == 0 ? 64 : at - 1); + return 0; +} diff --git a/test/Guards/intra-object.c b/test/Guards/intra-object.c new file mode 100644 index 00000000..259e877c --- /dev/null +++ b/test/Guards/intra-object.c @@ -0,0 +1,23 @@ +// RFC 0036 §6: a fortified string call writes no more than the object +// size its caller passes (here the 8-byte field, which Darwin's and +// glibc's headers pass at -O2); a write past it within the struct stops. +// RUN: %weavec_cc -O2 %s -o %t +// RUN: %t short +// RUN: not --crash %t AAAAAAAAAAA 2>&1 | FileCheck %s +#include +#include + +struct user { + char name[8]; + int admin; +}; + +int main(int argc, char **argv) { + struct user *u = calloc(1, sizeof *u); + if (u == NULL) + return 1; + // CHECK: weavec: intra-object-overflow at {{.*}}intra-object.c:[[@LINE+2]]:{{[0-9]+}}: write of 12 bytes at 0x[[AT:[0-9a-f]+]] + // CHECK-NEXT: weavec: 0x[[AT]] is 0 bytes inside the 8-byte destination at 0x[[AT]] + strcpy(u->name, argv[argc - 1]); + return u->admin; +} diff --git a/test/Guards/shared-frame.c b/test/Guards/shared-frame.c new file mode 100644 index 00000000..99678126 --- /dev/null +++ b/test/Guards/shared-frame.c @@ -0,0 +1,33 @@ +// RFC 0036 §2: locals whose lifetimes never overlap (the arms of a switch) +// share the frame's bytes, each with its redzone; an overflow of one of them +// is still a stack-buffer-overflow. +// RUN: %weavec_cc -O2 -S -emit-llvm %s -o - | FileCheck %s --check-prefix=IR +// RUN: %weavec_cc -O2 %s -o %t +// RUN: %t 1 7 +// RUN: not --crash %t 1 200 2>&1 | FileCheck %s +#include + +__attribute__((noinline)) void put(char *p, int i) { + // CHECK: weavec: stack-buffer-overflow at {{.*}}shared-frame.c:[[@LINE+1]]:{{[0-9]+}}: write of 1 bytes + p[i] = 1; +} + +// A left zone of 64 bytes and one 200-byte object with its redzone, not +// four. +// IR-LABEL: define {{.*}} @pick( +// IR: alloca [320 x i8], align 32 +// IR-NOT: alloca [ +// IR: ret +__attribute__((noinline)) int pick(int kind, int i) { + switch (kind) { + case 0: { char a[200]; put(a, i); return a[i & 7]; } + case 1: { char b[200]; put(b, i); return b[i & 7]; } + case 2: { char c[200]; put(c, i); return c[i & 7]; } + default: { char d[200]; put(d, i); return d[i & 7]; } + } +} + +int main(int argc, char **argv) { + (void)argc; + return pick(atoi(argv[1]), atoi(argv[2])) == 1 ? 0 : 1; +} diff --git a/test/Guards/use-after-poison.c b/test/Guards/use-after-poison.c new file mode 100644 index 00000000..1b6c9afd --- /dev/null +++ b/test/Guards/use-after-poison.c @@ -0,0 +1,30 @@ +// RFC 0036 §5: weavec-cc builds code as ASan would (`__has_feature( +// address_sanitizer)`), and a pool written for ASan poisons its free space +// through ASan's interface: an access to a poisoned byte stops. +// RUN: %weavec_cc -O1 %s -o %t +// RUN: not --crash %t 2>&1 | FileCheck %s +// RUN: %weavec_cc -fno-weavec-asan-compat -O1 %s -o %t.plain +// RUN: %t.plain | FileCheck --check-prefix=PLAIN %s +#include +#include + +#if defined(__has_feature) +#if __has_feature(address_sanitizer) +#include +#define POISONS 1 +#endif +#endif + +int main(void) { + char *pool = malloc(64); + if (pool == NULL) + return 1; +#ifdef POISONS + ASAN_POISON_MEMORY_REGION(pool + 32, 32); +#endif + pool[0] = 1; + // PLAIN: 0 + // CHECK: weavec: use-after-poison at {{.*}}use-after-poison.c:[[@LINE+1]]:{{[0-9]+}}: read of 1 bytes at 0x{{[0-9a-f]+}} + printf("%d\n", pool[40]); + return 0; +} diff --git a/test/Inputs/prelude.h b/test/Inputs/prelude.h index f83a7198..56aed1ee 100644 --- a/test/Inputs/prelude.h +++ b/test/Inputs/prelude.h @@ -7,9 +7,8 @@ typedef unsigned long size_t; void *malloc(size_t size); void *realloc(void *ptr, size_t size); void free(void *ptr); -/* The opaque "look at this pointer" helper. It is annotated because a call - * to an unannotated external function warns by default (RFC 0003). */ -void use(const void *__attribute__((annotate("weavec.borrowed"))) ptr); +/* The opaque "look at this pointer" helper. */ +void use(const void *ptr); #define NULL ((void *)0) diff --git a/test/README.md b/test/README.md index 690aac1d..bf7d4b9c 100644 --- a/test/README.md +++ b/test/README.md @@ -1,26 +1,24 @@ # Integration tests -End-to-end tests that run `weavec` and `weavec-cc` over small C programs and -check their output with [FileCheck](https://llvm.org/docs/CommandGuide/FileCheck.html), +End-to-end tests that run `weavec-cc` over small C programs and check its +output with [FileCheck](https://llvm.org/docs/CommandGuide/FileCheck.html), driven by [lit](https://llvm.org/docs/CommandGuide/lit.html). ``` test/ - Guards/ the guard passes of RFC 0035: their IR (FileCheck on - -emit-llvm: inline checks, removal rules, frames, globals, - loop ranges) and the programs they build, which must stop - at the faulty access with the runtime's report - Analysis/ the advisory analysis (use-after-free, double-free, ...) - Annotations/ the weavec.h macros and annotation handling - Driver/ command-line behaviour of weavec and weavec-cc (compile, - link, -fweavec-*/-W flags, the runtime's link line and its - fallbacks, one runtime per process) - WholeProgram/ several files analysed as one program (RFC 0005), with - their shared sources under WholeProgram/Inputs/ + Guards/ the guard passes of RFC 0035 and RFC 0036: their IR + (FileCheck on -emit-llvm: inline checks, removal rules, + frames and shared frame space, globals, loop ranges) and + the programs they build, which must stop at the faulty + access with the runtime's report + Driver/ command-line behaviour of weavec-cc (compile, link, + -fweavec-* flags, fortification, ASan's macros, the + runtime's link line and its fallbacks, one runtime per + process) Inputs/ shared headers/fixtures (not run as tests) - cases/ the case tree (recall pins, detection programs, soundness - probes, semantics), run by scripts/run-cases.py rather than - lit; see cases/README.md + cases/ the case tree (detection programs, determinism cases, + soundness probes, semantics), run by scripts/run-cases.py + rather than lit; see cases/README.md corpus/ the pinned third-party projects, run by scripts/corpus-gate.py; see corpus/README.md ``` @@ -28,13 +26,12 @@ test/ Run everything with `ctest -L integration` (or the `check-weavec-lit` target), or a single test with `lit -v build/dev/test/Guards/heap-overflow.c`. -Each test is a `.c` file whose first lines contain `// RUN:` commands. The -`%weavec` substitution expands to the built binary with the annotation header -directory already on the include path, so tests can `#include `; -`%weavec_cc` is the compiler driver, which finds the header itself. -Use `not %weavec ...` when the run is expected to fail, `not --crash %t` -when a program must stop at a guard, and `... | count 0` to assert that -nothing was printed. +Each test is a `.c` file whose first lines contain `// RUN:` commands. +`%weavec_cc` is the compiler driver, which finds `weavec.h` itself; +`%resource_dir` is the directory that holds the header. Use `not +%weavec_cc ...` when a compile is expected to fail, `not --crash %t` when +a program must stop at a guard, and `... | count 0` to assert that nothing +was printed. Unit tests for individual components live in `unittests/` instead (`GuardPassTest` runs the passes on IR text). The runtime's own test is diff --git a/test/WholeProgram/Inputs/array15.c b/test/WholeProgram/Inputs/array15.c deleted file mode 100644 index 03f3a3cf..00000000 --- a/test/WholeProgram/Inputs/array15.c +++ /dev/null @@ -1,17 +0,0 @@ -// RFC 0015: selected effects, final ranges, returned storage and traversals. -#include "array15.h" -void *memcpy(void *, const void *, size_t); -void *memmove(void *, const void *, size_t); -void array15_drop(char **a, int i) { free(a[i]); } -void array15_copy(char **d, char **s, size_t n) { memcpy(d,s,n*sizeof *d); } -void array15_compact(char **a) { memmove(a,a+1,2*sizeof *a); a[2]=NULL; } -char **array15_clone(char **source, size_t n) { - char **b=malloc(n*sizeof *b); if (!b) return NULL; - memcpy(b,source,n*sizeof *b); return b; -} -void array15_clear(char **a, size_t n) { - for (size_t i=0; irc = 1; - c->name = malloc(4); - return c; -} - -struct counted *counted_ref(struct counted *c) { - c->rc++; - return c; -} - -void counted_unref(struct counted *c) { - if (--c->rc == 0) { - free(c->name); - free(c); - } -} diff --git a/test/WholeProgram/Inputs/counted.h b/test/WholeProgram/Inputs/counted.h deleted file mode 100644 index 773c2fcb..00000000 --- a/test/WholeProgram/Inputs/counted.h +++ /dev/null @@ -1,14 +0,0 @@ -/* A reference-counted object whose ref/unref live in counted.c (RFC 0010). */ -#ifndef WEAVEC_TEST_COUNTED_H -#define WEAVEC_TEST_COUNTED_H - -struct counted { - int rc; - char *name; -}; - -struct counted *counted_new(void); -struct counted *counted_ref(struct counted *c); -void counted_unref(struct counted *c); - -#endif /* WEAVEC_TEST_COUNTED_H */ diff --git a/test/WholeProgram/Inputs/die.c b/test/WholeProgram/Inputs/die.c deleted file mode 100644 index 7f407f36..00000000 --- a/test/WholeProgram/Inputs/die.c +++ /dev/null @@ -1,22 +0,0 @@ -/* Error helpers that never return, unannotated: their `never-returns` is - * inferred here and crosses the unit boundary (RFC 0009, RFC 0005). */ -#include "../../Inputs/prelude.h" - -void abort(void); - -void die(const char *msg) { - use(msg); - abort(); -} - -void fail(int code) { - if (code > 3) - die("big"); - die("small"); -} - -/* Returns when `ok` is non-zero: not `never-returns`. */ -void check(int ok) { - if (!ok) - die("bad"); -} diff --git a/test/WholeProgram/Inputs/grow.c b/test/WholeProgram/Inputs/grow.c deleted file mode 100644 index 6e6c0f74..00000000 --- a/test/WholeProgram/Inputs/grow.c +++ /dev/null @@ -1,19 +0,0 @@ -/* A `realloc` wrapper and an error-returning consumer defined in another - * unit: their outcome-conditional summaries cross the unit boundary (RFC - * 0006, RFC 0005). */ -#include "../../Inputs/prelude.h" - -char *grow(char *p, size_t n) { - char *q = realloc(p, n); - if (!q) - return NULL; - return q; -} - -int try_take(char *p, int c) { - if (c) { - free(p); - return 0; - } - return -1; -} diff --git a/test/WholeProgram/Inputs/handle.c b/test/WholeProgram/Inputs/handle.c deleted file mode 100644 index 401af6c3..00000000 --- a/test/WholeProgram/Inputs/handle.c +++ /dev/null @@ -1,17 +0,0 @@ -/* An allocator and two releasers split from their callers, for the RFC 0007 - * whole-program tests: `log_open` hands out a `FILE` (family `fclose`), - * `log_close` closes one, and `xfree` is a wrapper around `free`. */ -#include -#include -#include "handle.h" - -FILE *log_open(const char *path) { return fopen(path, "a"); } - -void log_close(FILE *f) { - if (f) - fclose(f); -} - -void xfree(void *p) { free(p); } - -int log_peek(FILE *f) { return f != NULL; } diff --git a/test/WholeProgram/Inputs/handle.h b/test/WholeProgram/Inputs/handle.h deleted file mode 100644 index 0305944a..00000000 --- a/test/WholeProgram/Inputs/handle.h +++ /dev/null @@ -1,12 +0,0 @@ -/* Declarations for handle.c (RFC 0007 whole-program tests). */ -#ifndef WEAVEC_TEST_HANDLE_H -#define WEAVEC_TEST_HANDLE_H - -#include - -FILE *log_open(const char *path); -void log_close(FILE *f); -void xfree(void *p); -int log_peek(FILE *f); - -#endif /* WEAVEC_TEST_HANDLE_H */ diff --git a/test/WholeProgram/Inputs/handlers.c b/test/WholeProgram/Inputs/handlers.c deleted file mode 100644 index 51a3d391..00000000 --- a/test/WholeProgram/Inputs/handlers.c +++ /dev/null @@ -1,7 +0,0 @@ -/* A unit that registers a callback (takes a function's address) so the type - * has a candidate in the program (RFC 0005, indirect calls). */ -#include "../../Inputs/prelude.h" - -static void on_done(void *p) { free(p); } - -void (*get_handler(void))(void *) { return on_done; } diff --git a/test/WholeProgram/Inputs/heap13.c b/test/WholeProgram/Inputs/heap13.c deleted file mode 100644 index c621956e..00000000 --- a/test/WholeProgram/Inputs/heap13.c +++ /dev/null @@ -1,38 +0,0 @@ -// RFC 0013: this body is seen at compile time in a different translation unit. -#include "../../Inputs/prelude.h" -#include "heap13.h" -struct heap13_box *heap13_new(void) { - struct heap13_box *b = malloc(sizeof *b); if (!b) return NULL; - b->data = malloc(4); - if (!b->data) { free(b); return NULL; } - return b; -} -struct heap13_box *heap13_wrap(char *p) { - struct heap13_box *b = malloc(sizeof *b); if (!b) return NULL; - b->data = p; return b; -} - -struct heap13_box *heap13_singleton; -void heap13_ensure(void) { - if (heap13_singleton) return; - heap13_singleton = malloc(sizeof *heap13_singleton); - if (heap13_singleton) heap13_singleton->data = malloc(4); -} - -void heap13_drop_child(void) { free(heap13_singleton->data); } - -void heap13_swap(struct heap13_box *a, struct heap13_box *b) { - char *old = a->data; a->data = b->data; b->data = old; -} -void heap13_reset(struct heap13_box *b) { - struct heap13_box next = {malloc(4)}; - heap13_swap(b, &next); free(next.data); -} -void heap13_local_swap(struct heap13_box *a, struct heap13_box *b) { - struct heap13_box *x = a, *y = b; - char *old = x->data; x->data = y->data; y->data = old; -} -static void heap13_put(struct heap13_box *b, char *p) { b->data = p; } -void heap13_copy_and_free(struct heap13_box *a, struct heap13_box *b) { - heap13_put(b, a->data); free(a->data); -} diff --git a/test/WholeProgram/Inputs/heap13.h b/test/WholeProgram/Inputs/heap13.h deleted file mode 100644 index 2ac1fd13..00000000 --- a/test/WholeProgram/Inputs/heap13.h +++ /dev/null @@ -1,13 +0,0 @@ -#ifndef WEAVEC_TEST_HEAP13_H -#define WEAVEC_TEST_HEAP13_H -struct heap13_box { char *data; }; -struct heap13_box *heap13_new(void); -struct heap13_box *heap13_wrap(char *p); -extern struct heap13_box *heap13_singleton; -void heap13_ensure(void); -void heap13_drop_child(void); -void heap13_swap(struct heap13_box *a, struct heap13_box *b); -void heap13_reset(struct heap13_box *b); -void heap13_local_swap(struct heap13_box *a, struct heap13_box *b); -void heap13_copy_and_free(struct heap13_box *a, struct heap13_box *b); -#endif diff --git a/test/WholeProgram/Inputs/node.c b/test/WholeProgram/Inputs/node.c deleted file mode 100644 index 5ce3ff6c..00000000 --- a/test/WholeProgram/Inputs/node.c +++ /dev/null @@ -1,25 +0,0 @@ -/* Definitions for node.h. Analysed as its own unit; its summaries are what - * the other units of a test see (RFC 0005). */ -#include "../../Inputs/prelude.h" -#include "node.h" - -struct node *node_new(void) { - struct node *n = malloc(sizeof(struct node)); - if (n) - n->name = NULL; - return n; -} - -void node_free(struct node *n) { - if (!n) - return; - free(n->name); - free(n); -} - -int *node_vp(struct node *n) { return &n->v; } - -void node_set_name(struct node *n, char *name) { - free(n->name); - n->name = name; -} diff --git a/test/WholeProgram/Inputs/node.h b/test/WholeProgram/Inputs/node.h deleted file mode 100644 index 684d9aa5..00000000 --- a/test/WholeProgram/Inputs/node.h +++ /dev/null @@ -1,15 +0,0 @@ -/* A tiny library split across units, for the RFC 0005 tests. */ -#ifndef WEAVEC_TEST_NODE_H -#define WEAVEC_TEST_NODE_H - -struct node { - int v; - char *name; -}; - -struct node *node_new(void); -void node_free(struct node *n); -int *node_vp(struct node *n); -void node_set_name(struct node *n, char *name); - -#endif /* WEAVEC_TEST_NODE_H */ diff --git a/test/WholeProgram/Inputs/rfc0016-callee.c b/test/WholeProgram/Inputs/rfc0016-callee.c deleted file mode 100644 index a3ca6ad7..00000000 --- a/test/WholeProgram/Inputs/rfc0016-callee.c +++ /dev/null @@ -1,9 +0,0 @@ -#include "../../Inputs/prelude.h" -void release_then_write(char *a, char *b) { - free(a); - *b = 1; -} -void write_then_release(char *a, char *b) { *b = 1; free(a); } -void replace_related(char **a, char **b) { - free(*a); *a = malloc(4); if (*b) **b = 1; -} diff --git a/test/WholeProgram/Inputs/rfc0017-numeric.c b/test/WholeProgram/Inputs/rfc0017-numeric.c deleted file mode 100644 index eb57aa5c..00000000 --- a/test/WholeProgram/Inputs/rfc0017-numeric.c +++ /dev/null @@ -1,18 +0,0 @@ -// RFC 0017: typed numeric and spatial interfaces across translation units. -#include "../../Inputs/prelude.h" -unsigned char narrow(unsigned n) { return n; } -void narrow_out(unsigned n, unsigned char *out) { *out = n; } -void put_at(char *p, int i) { p[i] = 1; } -void fill_min(char *p, size_t n, size_t cap) { - for (size_t i = 0; i < n && i < cap; ++i) p[i] = 1; -} -char *make_product(size_t rows, size_t cols) { return malloc(rows * cols); } -int checked_size(size_t n, size_t m, size_t *out) { - return __builtin_mul_overflow(n, m, out); -} -void reverse_outputs(unsigned *a, unsigned *b) { *b = 1; *a = 2; } -void release_numeric_pointer(void *p) { free(p); } -void *checked_allocation(size_t n, size_t m) { - void *calloc(size_t, size_t); - return calloc(n, m); -} diff --git a/test/WholeProgram/Inputs/rfc0030-first.c b/test/WholeProgram/Inputs/rfc0030-first.c deleted file mode 100644 index aa60f4b2..00000000 --- a/test/WholeProgram/Inputs/rfc0030-first.c +++ /dev/null @@ -1,3 +0,0 @@ -// A callee whose access rests on its parameter's Single default (RFC 0030 -// §7.3: `first` relies on `p`). -int first(int *p) { return *p; } diff --git a/test/WholeProgram/Inputs/validity.c b/test/WholeProgram/Inputs/validity.c deleted file mode 100644 index c213981f..00000000 --- a/test/WholeProgram/Inputs/validity.c +++ /dev/null @@ -1,28 +0,0 @@ -// The other unit of rfc0008-validity.c. -#include "validity.h" - -#include -#include - -int vec_grow(struct vec *v) { - int *bigger = realloc(v->items, sizeof *bigger * (v->cap + 8)); - if (!bigger) - return 0; - v->items = bigger; - v->cap += 8; - return 1; -} - -void vec_reset(struct vec *v) { - free(v->items); - v->items = NULL; -} - -char *find(const char *s, char c) { return strchr(s, c); } - -int node_open(struct node **out) { - *out = malloc(sizeof **out); - return *out != NULL; -} - -int node_value(const struct node *n) { return n->value; } diff --git a/test/WholeProgram/Inputs/validity.h b/test/WholeProgram/Inputs/validity.h deleted file mode 100644 index cb410aac..00000000 --- a/test/WholeProgram/Inputs/validity.h +++ /dev/null @@ -1,26 +0,0 @@ -// Interface of validity.c: no annotations, everything is inferred from the -// definitions in that unit (RFC 0005) and travels in the program database. -#ifndef VALIDITY_H -#define VALIDITY_H - -struct vec { - int *items; - int cap; -}; - -struct node { - int value; -}; - -/// Grows `v->items` in place; returns nonzero on success. -int vec_grow(struct vec *v); -/// Releases `v->items` and nulls it. -void vec_reset(struct vec *v); -/// A pointer into `s` at the first `c`, or null. -char *find(const char *s, char c); -/// Allocates a node into `*out`; returns nonzero on success. -int node_open(struct node **out); -/// Reads the node. -int node_value(const struct node *n); - -#endif diff --git a/test/WholeProgram/Inputs/vec.c b/test/WholeProgram/Inputs/vec.c deleted file mode 100644 index 14376fc0..00000000 --- a/test/WholeProgram/Inputs/vec.c +++ /dev/null @@ -1,43 +0,0 @@ -/* The stores that fix `struct vec`'s pair (RFC 0012, *Sized fields*, - "Inference"), and the ones that refute `struct view`'s. */ -#include "../../Inputs/prelude.h" -#include "vec.h" - -int vec_init(struct vec *v, size_t cap) { - v->items = malloc(cap * sizeof *v->items); - if (!v->items) - return -1; - v->cap = cap; - v->n = 0; - return 0; -} - -int vec_push(struct vec *v, int x) { - if (v->n == v->cap) { - size_t cap = v->cap ? v->cap * 2 : 4; - int *items = realloc(v->items, cap * sizeof *items); - if (!items) - return -1; - v->items = items; - v->cap = cap; - } - v->items[v->n++] = x; - return 0; -} - -void vec_free(struct vec *v) { - free(v->items); - v->items = NULL; - v->cap = 0; - v->n = 0; -} - -void view_own(struct view *w, size_t len) { - w->raw = malloc(len * sizeof *w->raw); - w->len = len; -} - -void view_borrow(struct view *w, int *p, size_t len) { - w->raw = p; - w->len = len; -} diff --git a/test/WholeProgram/Inputs/vec.h b/test/WholeProgram/Inputs/vec.h deleted file mode 100644 index bd006b03..00000000 --- a/test/WholeProgram/Inputs/vec.h +++ /dev/null @@ -1,29 +0,0 @@ -/* A growable array whose pointer field is counted by a sibling, without an - annotation: the pair is inferred from vec.c (RFC 0012). */ -#ifndef WEAVEC_TEST_VEC_H -#define WEAVEC_TEST_VEC_H - -typedef unsigned long size_t; - -struct vec { - int *items; - size_t n; - size_t cap; -}; - -/* Allocates `cap` items. */ -int vec_init(struct vec *v, size_t cap); -/* Appends; grows when full. */ -int vec_push(struct vec *v, int x); -void vec_free(struct vec *v); - -/* A pair every store refutes: `raw` is sometimes a caller's buffer. */ -struct view { - int *raw; - size_t len; -}; - -void view_own(struct view *w, size_t len); -void view_borrow(struct view *w, int *p, size_t len); - -#endif /* WEAVEC_TEST_VEC_H */ diff --git a/test/WholeProgram/program-summary.c b/test/WholeProgram/program-summary.c deleted file mode 100644 index 5a633994..00000000 --- a/test/WholeProgram/program-summary.c +++ /dev/null @@ -1,20 +0,0 @@ -// RFC 0035 §8: `weavec --whole-program` prints one summary line per unit, -// from its last run, and one for the program, named after the unit that -// defines `main`, whatever order the units are given in. -// -// RUN: %weavec --whole-program %s %S/Inputs/rfc0030-first.c -- 2>&1 | FileCheck %s -// RUN: %weavec --whole-program %S/Inputs/rfc0030-first.c %s -- 2>&1 | FileCheck %s -// RUN: %weavec --whole-program %S/Inputs/rfc0030-first.c -- 2>&1 | FileCheck --check-prefix=LIBRARY %s - -int first(int *p); - -int main(void) { - int a[4] = {1, 2, 3, 4}; - return first(a + 1) - 2; -} - -// CHECK-DAG: weavec: {{.*}}program-summary.c: {{[0-9]+}} sites: {{[0-9]+}} proven, {{[0-9]+}} not proven, 0 violations, {{[0-9]+}} trusted; 0 errors, 0 warnings -// CHECK-DAG: weavec: {{.*}}rfc0030-first.c: 2 sites: {{[0-9]+}} proven, {{[0-9]+}} not proven, 0 violations, 0 trusted; 0 errors, 0 warnings -// CHECK: weavec: program program-summary: {{[0-9]+}} sites in 2 units: {{[0-9]+}} proven, {{[0-9]+}} not proven, 0 violations, {{[0-9]+}} trusted; 0 errors, 0 warnings - -// LIBRARY: weavec: program program: 2 sites in 1 unit: {{.*}}; 0 errors, 0 warnings diff --git a/test/WholeProgram/retention.c b/test/WholeProgram/retention.c deleted file mode 100644 index 2dac34ce..00000000 --- a/test/WholeProgram/retention.c +++ /dev/null @@ -1,42 +0,0 @@ -// RFC 0020: whole-program analysis retains a bounded number of parsed units -// and reparses on demand. An analysis dump keeps every unit it prints. -// RUN: rm -rf %t -// RUN: split-file %s %t -// RUN: %weavec --whole-program --analysis-stats=%t/ordinary.json %t/main.c %t/f*.c -- -// RUN: FileCheck %s --check-prefix=EVICT < %t/ordinary.json -// RUN: %weavec --whole-program --dump-analysis --analysis-stats=%t/dump.json %t/main.c %t/f*.c -- > %t/dump.txt -// RUN: FileCheck %s --check-prefix=KEEP --implicit-check-not=unit_evictions < %t/dump.json - -// EVICT: "unit_evictions":{{[1-9][0-9]*}}, -// KEEP: "unit_parses":8{{[,}]}} - -//--- main.c -void f1(void); -int main(void) { f1(); return 0; } - -//--- f1.c -void f7(void); -void f1(void) { f7(); } - -//--- f2.c -void f7(void); -void f2(void) { f7(); } - -//--- f3.c -void f7(void); -void f3(void) { f7(); } - -//--- f4.c -void f7(void); -void f4(void) { f7(); } - -//--- f5.c -void f7(void); -void f5(void) { f7(); } - -//--- f6.c -void f7(void); -void f6(void) { f7(); } - -//--- f7.c -void f7(void) {} diff --git a/test/WholeProgram/rfc0005-boundary-once.c b/test/WholeProgram/rfc0005-boundary-once.c deleted file mode 100644 index 18e6fcea..00000000 --- a/test/WholeProgram/rfc0005-boundary-once.c +++ /dev/null @@ -1,36 +0,0 @@ -// RFC 0005 reported `annotation-required` once per program for a callee no -// unit defines. RFC 0030 §5.1 removes the warning: every call to such a -// callee is not proven (`unresolved(unknown-callee)`), and a callee another -// unit of the program defines is known to the whole-program analysis (in a -// per-unit analysis it is unknown too). -// -// RUN: %weavec --whole-program %s %S/Inputs/node.c -- -I%S/Inputs 2>&1 | FileCheck %s -// RUN: %weavec %s -- -I%S/Inputs 2>&1 | FileCheck --check-prefix=ALONE %s -#include "../Inputs/prelude.h" -#include "node.h" - -struct blob; -struct blob *blob_open(const char *path); -void blob_close(struct blob *b); - -// CHECK-NOT: warning: -// Whole program: only the four calls to the `blob_` functions. -// CHECK: rfc0005-boundary-once.c: 9 sites: 5 proven, 4 not proven, 0 violations, 0 trusted; 0 errors, 0 warnings -// ALONE-NOT: warning: -// ALONE: rfc0005-boundary-once.c: 9 sites: 3 proven, 6 not proven, 0 violations, 0 trusted; 0 errors, 0 warnings - -void first_caller(void) { - struct blob *b = blob_open("x"); - blob_close(b); -} - -void second_caller(void) { - struct blob *b = blob_open("y"); - blob_close(b); -} - -// Defined in node.c: unknown to this unit alone. -void uses_the_library(void) { - struct node *n = node_new(); - node_free(n); -} diff --git a/test/WholeProgram/rfc0005-callbacks.c b/test/WholeProgram/rfc0005-callbacks.c deleted file mode 100644 index f37be065..00000000 --- a/test/WholeProgram/rfc0005-callbacks.c +++ /dev/null @@ -1,29 +0,0 @@ -// RFC 0005: an indirect call's candidates include the address-taken -// functions of the same type in every unit of the program (RFC 0004 finds -// only this unit's). handlers.c registers `on_done`, which frees its -// argument; calling the handler here therefore frees. -// -// RUN: not %weavec --whole-program %s %S/Inputs/handlers.c -- 2>&1 | FileCheck %s -// -// Alone, the call through `h` has no candidates: it is a call into unknown -// code (RFC 0030 §5.1), not proven, and nothing is reported. -// RUN: %weavec %s -- 2>&1 | FileCheck --check-prefix=ALONE %s -#include "../Inputs/prelude.h" - -void (*get_handler(void))(void *); - -// ALONE-NOT: {{warning|error}}: -// ALONE: 0 errors, 0 warnings -// RFC 0030 §9.3: an indirect call through a slot with no known target. The -// object engine takes the slot solution (RFC 0031 *Indirect calls at link*), -// whose detail names the open source of the slot. - -int run(void) { - char *buf = malloc(4); - if (!buf) - return 0; - void (*h)(void *) = get_handler(); - h(buf); - // CHECK: rfc0005-callbacks.c:[[@LINE+1]]:10: error: use of 'buf' after it was freed [weavec::use-after-free] - return buf[0]; -} diff --git a/test/WholeProgram/rfc0005-cross-unit.c b/test/WholeProgram/rfc0005-cross-unit.c deleted file mode 100644 index 24729c40..00000000 --- a/test/WholeProgram/rfc0005-cross-unit.c +++ /dev/null @@ -1,59 +0,0 @@ -// RFC 0005: with --whole-program, a call to a function defined in another -// unit is checked against that definition's summary instead of being a -// boundary. Bugs that need two files are found. -// -// RUN: not %weavec --whole-program %s %S/Inputs/node.c -- -I%S/Inputs 2>&1 | FileCheck %s -// RUN: not %weavec --whole-program %s %S/Inputs/node.c -- -I%S/Inputs 2>&1 | FileCheck --check-prefix=NONE %s -// -// Order of the sources on the command line does not matter: node.c is a -// dependency and is analysed first either way. -// RUN: not %weavec --whole-program %S/Inputs/node.c %s -- -I%S/Inputs 2>&1 | FileCheck %s -// -// Without --whole-program the same file is its own program and the calls -// are into unknown code (RFC 0030 §5.1): not proven, and nothing is -// reported. -// RUN: %weavec %s -- -I%S/Inputs 2>&1 | FileCheck --check-prefix=ALONE %s -#include "../Inputs/prelude.h" -#include "node.h" - -// NONE-NOT: warning: call -// ALONE-NOT: {{warning|error}}: -// ALONE: 0 errors, 0 warnings - -// `node_new` may return null and `node_free` releases only a non-null -// argument, so the bug is definite once `n` is tested (RFC 0031 §6.2: -// `release *param0 free when param 0 !=0`); untested, it is a warning. The -// callee reads `n->name` before it frees `n`, so the first invalid -// operation of the second call is that read: a use after free of the -// argument, reported at it. -int double_release(void) { - struct node *n = node_new(); - if (!n) - return 1; - node_free(n); - // CHECK: rfc0005-cross-unit.c:[[@LINE+1]]:13: error: use of 'n' after it was freed [weavec::use-after-free] - node_free(n); - return 0; -} - -int dangling_field_pointer(void) { - struct node *n = node_new(); - if (!n) - return 1; - int *p = node_vp(n); - node_free(n); - // `node_vp` returns a copy of `n` at the field `v` (RFC 0011): the - // dangling read is the report. - // CHECK: rfc0005-cross-unit.c:[[@LINE+1]]:11: error: use of 'p' after it was freed [weavec::use-after-free] - return *p; -} - -int fine(void) { - struct node *n = node_new(); - if (!n) - return 1; - *node_vp(n) = 3; - node_set_name(n, malloc(4)); - node_free(n); - return 0; -} diff --git a/test/WholeProgram/rfc0005-dump.c b/test/WholeProgram/rfc0005-dump.c deleted file mode 100644 index a8baf929..00000000 --- a/test/WholeProgram/rfc0005-dump.c +++ /dev/null @@ -1,44 +0,0 @@ -// RFC 0005, *Debug output*: in whole-program mode --dump-analysis prints -// each unit's dump in analysis order, prefixed with the -// unit, then the program database. The format is a debugging aid; this pins -// only its shape. -// -// RUN: %weavec --whole-program --dump-analysis %s %S/Inputs/node.c -- -I%S/Inputs | FileCheck %s -#include "../Inputs/prelude.h" -#include "node.h" - -// Dependencies come first: node.c defines what this unit calls (RFC 0031 -// §7; the RFC 0016 context-request components are gone, §6.1). Each unit -// prints its functions' format-30 summaries (RFC 0031 *Summary format 30*). -// CHECK: unit '{{.*}}node.c': -// CHECK-NEXT: function 'node_new': -// CHECK-NEXT: summary: -// CHECK: function 'node_free': -// CHECK: unit '{{.*}}rfc0005-dump.c': -// CHECK-NEXT: function 'release': -// CHECK-NEXT: summary: -// CHECK: program: -// CHECK-NEXT: function 'node_free': -// CHECK-NEXT: always-returns -// CHECK-NEXT: release *param0 free when param 0 !=0 -// CHECK-NEXT: release *param0->name free when param 0 !=0 -// CHECK-NEXT: function 'node_new': -// CHECK-NEXT: always-returns -// CHECK-NEXT: result fresh#0 free extent 16 {{.*}}maybe-null when null nonnull -// CHECK-NEXT: store result->name := null -// CHECK-NEXT: function 'node_set_name': -// CHECK-NEXT: always-returns -// CHECK-NEXT: release *param0->name free when always -// CHECK-NEXT: store param0->name := path param1 -// CHECK-NEXT: function 'node_vp': -// CHECK-NEXT: always-returns -// CHECK-NEXT: result path param0 when nonnull -// CHECK-NEXT: nonnull-on nonnull param0 -// `node_free` releases only a non-null argument, so `release` releases its -// own argument possibly (the key `param 0 !=0` is not carried over). -// CHECK-NEXT: function 'release': -// CHECK-NEXT: always-returns -// CHECK-NEXT: release *param0 free {{(may )?}}when -// CHECK-NEXT: release *param0->name free {{(may )?}}when - -void release(struct node *n) { node_free(n); } diff --git a/test/WholeProgram/rfc0006-outcomes.c b/test/WholeProgram/rfc0006-outcomes.c deleted file mode 100644 index 66dd415e..00000000 --- a/test/WholeProgram/rfc0006-outcomes.c +++ /dev/null @@ -1,61 +0,0 @@ -// RFC 0006: outcome-conditional summaries inferred in one unit are applied -// in another, through the whole-program database (RFC 0005). -// -// RUN: not %weavec --whole-program %s %S/Inputs/grow.c -- 2>&1 | FileCheck %s -// RUN: not %weavec --whole-program --dump-analysis %s %S/Inputs/grow.c -- 2>&1 | FileCheck --check-prefix=DUMP %s -#include "../Inputs/prelude.h" - -char *grow(char *p, size_t n); -int try_take(char *p, int c); - -// The null class keeps `p`: RFC 0030 §8.2 releases it when the size is -// zero, which the zero-initialisation wrapper never asks for (RFC 0031 -// *Implementation amendments*). -// DUMP: program: -// DUMP-NEXT: function 'grow': -// DUMP-NEXT: always-returns -// DUMP-NEXT: result null when null -// DUMP-NEXT: result fresh#0 free extent param1 {{.*}}when nonnull -// DUMP-NEXT: move *param0 free when result nonnull -// DUMP: function 'try_take': -// DUMP-NEXT: always-returns -// DUMP-NEXT: result int [-1, -1] when negative and param 1 =0 -// DUMP-NEXT: result int [0, 0] when zero and param 1 !=0 -// DUMP-NEXT: release *param0 free when result zero - -// Clean: the tests select the classes that did not consume. `grow(p, 16)` -// cannot release `p` on its null result: the numeric context the call asks -// of `grow.c` binds the size (RFC 0031 §7 *Amendment (cross-unit -// contexts)*). -void grown(char *p) { - char *q = grow(p, 16); - if (q == NULL) { - free(p); - return; - } - free(q); -} - -void guarded(char *p, int c) { - if (try_take(p, c) != 0) - free(p); -} - -// Reported: the wrong side, and no test at all. `try_take` releases `p` -// exactly when it returns zero, and `rc == 0` selects that class: the double -// free is definite (RFC 0031 §6.3, a pending case resolved by the test). -void wrong_side(char *p, int c) { - int rc = try_take(p, c); - if (rc == 0) - // CHECK: rfc0006-outcomes.c:[[@LINE+1]]:5: error: 'p' is freed twice [weavec::double-free] - free(p); -} - -void untested(char *p) { - char *q = grow(p, 8); - // CHECK: rfc0006-outcomes.c:[[@LINE+1]]:7: warning: use of 'p' after it may have been moved [weavec::use-after-move] - use(p); - free(q); -} - -// CHECK: 1 warning and 1 error generated. diff --git a/test/WholeProgram/rfc0007-families.c b/test/WholeProgram/rfc0007-families.c deleted file mode 100644 index d90a9160..00000000 --- a/test/WholeProgram/rfc0007-families.c +++ /dev/null @@ -1,58 +0,0 @@ -// RFC 0007 across units (RFC 0005): release families travel in the program -// database, so a `FILE` handed out by one unit is checked against `free` in -// another, a wrapper of `free` defined elsewhere carries family `free`, and -// a resource handed out by another unit that is never released is a leak. -// A callee summarised from its body that records no effect on a pointer -// parameter is trusted not to retain it (RFC 0007, *Assumptions*). -// -// RUN: not %weavec --whole-program %s %S/Inputs/handle.c -- -I%S/Inputs 2>&1 | FileCheck %s -// RUN: not %weavec --whole-program --dump-analysis %s %S/Inputs/handle.c -- -I%S/Inputs 2>&1 | FileCheck --check-prefix=DUMP %s -#include -#include -#include "handle.h" - -// The families in the format-30 summaries (RFC 0031 *Summary format 30*). -// DUMP: program: -// DUMP: function 'log_close': -// DUMP-NEXT: always-returns -// DUMP-NEXT: release *param0 fclose when param 0 !=0 -// DUMP-NEXT: function 'log_open': -// DUMP-NEXT: always-returns -// DUMP-NEXT: result fresh#0 fclose {{.*}}maybe-null when null nonnull -// DUMP: function 'xfree': -// DUMP-NEXT: always-returns -// DUMP-NEXT: release *param0 free when always - -void wrong_family(const char *path) { - FILE *f = log_open(path); - if (!f) - return; - // CHECK: rfc0007-families.c:[[@LINE+1]]:3: error: 'f' is released with 'free' but must be released with 'fclose' [weavec::mismatched-release] - free(f); -} - -void wrong_wrapper(const char *path) { - FILE *f = fopen(path, "r"); - if (!f) - return; - // CHECK: rfc0007-families.c:[[@LINE+1]]:3: error: 'f' is released with 'free' but must be released with 'fclose' [weavec::mismatched-release] - xfree(f); -} - -int leaked(const char *path) { - FILE *f = log_open(path); - // `log_peek` reads `f` and keeps nothing: passing it is not an escape. - // CHECK: rfc0007-families.c:[[@LINE+1]]:3: warning: 'f' is leaked [weavec::leak] - return log_peek(f); -} - -// Clean: the matching releaser from the other unit, and `xfree` on a `free` -// family resource. -void fine(const char *path) { - FILE *f = log_open(path); - log_close(f); - char *s = malloc(8); - xfree(s); -} - -// CHECK: 1 warning and 2 errors generated. diff --git a/test/WholeProgram/rfc0008-validity.c b/test/WholeProgram/rfc0008-validity.c deleted file mode 100644 index f6c4334f..00000000 --- a/test/WholeProgram/rfc0008-validity.c +++ /dev/null @@ -1,115 +0,0 @@ -// RFC 0008 across units (RFC 0005): `replaced`, `requires`, `null` returns, -// `notnull` outcomes and interior results travel in the program database, so -// a caller in this unit is checked against definitions in another. -// -// RUN: not %weavec --whole-program %s %S/Inputs/validity.c -- -I%S/Inputs 2>&1 | FileCheck %s -// RUN: not %weavec --whole-program --dump-analysis %s %S/Inputs/validity.c -- -I%S/Inputs 2>&1 | FileCheck --check-prefix=DUMP %s -#include -#include -#include "validity.h" - -// The format-30 summaries (RFC 0031 *Summary format 30*): a null result is -// a result class, `requires{param 0}` is `nonnull-on` every class, and -// `replaced` is a store. `strchr`'s result points into `s` at an offset no -// format-30 value spells, so `find` returns `unknown` (RFC 0031 §6.1). -// DUMP: program: -// DUMP: function 'find': -// DUMP-NEXT: always-returns -// DUMP-NEXT: result unknown maybe-null when null nonnull -// DUMP: function 'node_open': -// DUMP-NEXT: always-returns -// DUMP-NEXT: result int [0, 0] when zero and param 0 !=0 -// DUMP-NEXT: result int [1, 1] when positive and param 0 !=0 -// DUMP-NEXT: store *param0 := fresh#0 free extent 4 {{.*}}maybe-null absent on zero -// DUMP: function 'node_value': -// DUMP-NEXT: always-returns -// DUMP-NEXT: result int -// DUMP-NEXT: nonnull-on zero param0 -// DUMP-NEXT: nonnull-on positive param0 -// DUMP-NEXT: nonnull-on negative param0 -// The failure class keeps the items (RFC 0030 §8.2's release of a size that -// wraps to zero does not arise through the zero-initialisation wrapper); -// the success class moves them and stores the new block. -// DUMP: function 'vec_grow': -// DUMP-NEXT: always-returns -// DUMP-NEXT: result int [0, 0] when zero and param 0 !=0 -// DUMP-NEXT: result int [1, 1] when positive and param 0 !=0 -// DUMP-NEXT: move *param0->items free when result positive -// DUMP-NEXT: store param0->cap := {{.*}} when result positive -// DUMP-NEXT: store param0->items := fresh#0 free {{.*}}when result positive -// DUMP-NEXT: nonnull-on zero param0 -// DUMP-NEXT: nonnull-on positive param0 -// DUMP-NEXT: function 'vec_reset': -// DUMP-NEXT: always-returns -// DUMP-NEXT: release *param0->items free when always -// DUMP-NEXT: store param0->items := null -// The summary goes to stderr and the dump to stdout, and `2>&1` joins them on -// one descriptor. The summary must land after the whole dump, never inside a -// dump line: only draining the dump stream first orders two buffered streams -// over one descriptor (AnalysisSummary.cpp, printLine). Without that the point -// they interleave at is the point some buffer happened to fill, which differs -// between libcs -- on glibc it fell inside the 'vec_grow' line above. -// DUMP: program slots: -// DUMP: weavec: program program: {{[0-9]+}} sites in 2 units: - -int replaced_copy(struct vec *v) { - int *old = v->items; - if (!vec_grow(v)) - return 1; - // CHECK: rfc0008-validity.c:[[@LINE+1]]:10: error: use of 'old' after it was moved [weavec::use-after-move] - return old[0]; -} - -int reset_copy(struct vec *v) { - int *old = v->items; - vec_reset(v); - // CHECK: rfc0008-validity.c:[[@LINE+1]]:10: error: use of 'old' after it was freed [weavec::use-after-free] - return old[0]; -} - -int null_result(const char *s) { - char *p = find(s, 'x'); - return *p; -} - -int passes_maybe_null(void) { - struct node *n = malloc(sizeof *n); - int v = node_value(n); - free(n); - return v; -} - -void interior_release(const char *t) { - char *s = strdup(t); - if (!s) - return; - char *p = find(s, 'x'); - if (!p) { - free(s); - return; - } - // `find`'s result is `unknown` across the unit boundary (RFC 0031 §6.1; - // in one unit, `strchr`'s interior result gives `invalid-release`): the - // release is not proven, and `s` may be leaked where `p` is not `s` - // (the retired golden comparison of RFC 0030, *Lit tests*). - // CHECK: rfc0008-validity.c:[[@LINE+1]]:3: warning: 's' is leaked [weavec::leak] - free(p); -} - -// Clean: the outcome of `node_open` proves `*out` non-null; the grown vector -// is used through the place, not through a stale copy. (`node_open`'s -// allocation was never made on its zero class, RFC 0031 §6.3 `absent-on`.) -int fine(struct vec *v) { - struct node *n; - if (!node_open(&n)) - return 1; - int r = node_value(n); - free(n); - if (!vec_grow(v)) - return 1; - v->items[0] = r; - vec_reset(v); - return 0; -} - -// CHECK: 1 warning and 2 errors generated. diff --git a/test/WholeProgram/rfc0009-noreturn-units.c b/test/WholeProgram/rfc0009-noreturn-units.c deleted file mode 100644 index 72fa4cc9..00000000 --- a/test/WholeProgram/rfc0009-noreturn-units.c +++ /dev/null @@ -1,51 +0,0 @@ -// RFC 0009, *Inferred `noreturn`*: `never-returns` inferred in one unit ends -// paths in another, through the whole-program database (RFC 0005). -// -// RUN: %weavec --whole-program %s %S/Inputs/die.c -- 2>&1 | FileCheck %s -// RUN: %weavec --whole-program --dump-analysis %s %S/Inputs/die.c -- 2>&1 | FileCheck --check-prefix=DUMP %s -// -// Alone, the calls are boundaries and `fail` is assumed to return. -// RUN: %weavec %s -- 2>&1 | FileCheck --check-prefix=ALONE %s -#include "../Inputs/prelude.h" - -void die(const char *msg); -void fail(int code); -void check(int ok); - -// The format-30 summaries (RFC 0031 *Summary format 30*): `check` may -// return, so it is not `never-returns`. -// DUMP-LABEL: function 'die': -// DUMP-NEXT: summary: -// DUMP-NEXT: never-returns -// DUMP-LABEL: function 'fail': -// DUMP-NEXT: summary: -// DUMP-NEXT: never-returns -// DUMP-LABEL: function 'check': -// DUMP-NEXT: summary: -// DUMP-NEXT: {{always-returns|may-not-return}} - -// Clean with the program: the bad path never reaches the use. -void good(int bad) { - char *q = malloc(8); - if (bad) { - free(q); - fail(bad); - } - // ALONE: rfc0009-noreturn-units.c:[[@LINE+1]]:7: warning: use of 'q' after it may have been freed [weavec::use-after-free] - use(q); - free(q); -} - -// Reported either way: `check` may return. -void checked(int bad) { - char *q = malloc(8); - if (bad) { - free(q); - check(bad); - } - // CHECK: rfc0009-noreturn-units.c:[[@LINE+1]]:7: warning: use of 'q' after it may have been freed [weavec::use-after-free] - use(q); - free(q); -} - -// CHECK-NOT: good diff --git a/test/WholeProgram/rfc0010-shares.c b/test/WholeProgram/rfc0010-shares.c deleted file mode 100644 index 80a2b9c8..00000000 --- a/test/WholeProgram/rfc0010-shares.c +++ /dev/null @@ -1,72 +0,0 @@ -// RFC 0010, *Across translation units*: a ref/unref pair inferred in one -// unit is a retain and a share release in another, and the count field it -// releases through is a known count for the leak rule there. -// -// RUN: not %weavec --whole-program %s %S/Inputs/counted.c -- -I%S/Inputs 2>&1 | FileCheck %s -// RUN: not %weavec --whole-program --dump-analysis %s %S/Inputs/counted.c -- -I%S/Inputs 2>&1 | FileCheck --check-prefix=DUMP %s -// -// Alone, the calls are into unknown code: nothing is reported. -// RUN: %weavec %s -- -I%S/Inputs 2>&1 | FileCheck --check-prefix=ALONE %s -#include "../Inputs/prelude.h" -#include "counted.h" - -// The object engine does not yet infer RFC 0010 reference-count functions -// (`++c->rc` / `if (--c->rc == 0) free(c)`): `counted_unref`'s summary -// possibly releases its argument, `counted_ref` returns it, and no count -// field is exported (RFC 0031 §5.5, §6.1; the retired golden comparison of RFC 0030, -// *Lit tests*). A call whose count is known asks `counted.c` for its -// context (RFC 0031 §7 *Amendment (cross-unit contexts)*): there the count -// decides the release. -// DUMP: program: -// DUMP: function 'counted_new': -// DUMP-NEXT: always-returns -// DUMP-NEXT: result null when null -// DUMP-NEXT: result fresh#0 free extent 16 {{.*}}when nonnull -// DUMP: function 'counted_ref': -// DUMP-NEXT: always-returns -// DUMP-NEXT: result path param0 when nonnull -// DUMP: function 'counted_unref': -// DUMP-NEXT: always-returns -// DUMP-NEXT: release *param0 free may when always -// DUMP-NEXT: release *param0->name free may when always - -// Clean: a share taken and given back. -int balanced(void) { - struct counted *a = counted_new(); - if (!a) - return -1; - struct counted *b = counted_ref(a); - // The count is 2 here, 1 after this call, which releases nothing. - counted_unref(b); - counted_unref(a); - return 0; -} - -int twice(void) { - struct counted *a = counted_new(); - if (!a) - return -1; - counted_unref(a); - // The first call released `a` (its count was 1); this one reads its count. - // CHECK: rfc0010-shares.c:[[@LINE+1]]:17: error: use of 'a' after it was freed [weavec::use-after-free] - counted_unref(a); - return 0; -} - -// With the count known from the other unit, a lost share is a leak here -// (RFC 0010). Without the count (above), `counted_ref` only returns its -// argument and the leak is not reported (a lost finding, listed in -// the retired golden comparison of RFC 0030, *Lit tests*). -struct list { - struct counted *head; -}; -void lost(struct list *l) { - struct counted *p = l->head; - counted_ref(p); -} - -// Alone, the calls into the other unit are unknown code (RFC 0030 §5.1). -// ALONE-NOT: {{warning|error}}: -// ALONE: 0 errors, 0 warnings -// CHECK-NOT: {{warning|error}}: -// CHECK: 1 error generated. diff --git a/test/WholeProgram/rfc0011-extents.c b/test/WholeProgram/rfc0011-extents.c deleted file mode 100644 index a6edcbe4..00000000 --- a/test/WholeProgram/rfc0011-extents.c +++ /dev/null @@ -1,94 +0,0 @@ -// RFC 0011, *Extents in summaries*: an allocation's extent, the extent of -// what a callee writes through a parameter, and the offset a callee releases -// at all cross translation units. -// -// RFC 0031 §6.1: format-30 summaries carry no extent requirements; a store -// the callee makes on every return, outside the caller's object, is an -// error at the call (RFC 0031 *Implementation amendments*, "Stores past the -// caller's object"). -// -// RUN: not %weavec --whole-program %s %S/Inputs/buffers.c -- -I%S/Inputs 2>&1 | FileCheck %s -// RUN: not %weavec --whole-program --dump-analysis %s %S/Inputs/buffers.c -- -I%S/Inputs 2>/dev/null | FileCheck --check-prefix=DUMP %s -// -// Alone, the calls are unchecked boundaries: nothing is reported. -// RUN: %weavec %s -- -I%S/Inputs 2>&1 | FileCheck --check-prefix=ALONE %s -#include "../Inputs/prelude.h" -#include "buffers.h" - -// DUMP: program: -// DUMP: function 'buffer_fill': -// DUMP-NEXT: always-returns -// DUMP-NEXT: store *param0[*] elements [0, param1) := int [0, 0] -// DUMP-NEXT: function 'buffer_new': -// DUMP-NEXT: always-returns -// DUMP-NEXT: result fresh#0 free extent param0 {{.*}}when null nonnull -// DUMP-NEXT: function 'buffer_put8': -// DUMP-NEXT: always-returns -// DUMP-NEXT: store *param0[*] elements [0, 8) := int [0, 7] -// DUMP: function 'wrapped_release': -// DUMP-NEXT: always-returns -// DUMP-NEXT: release *param0 free offset -4 when always - - -// ALONE-NOT: error: -// ALONE-NOT: out-of-bounds - -// Clean: the allocation is as large as the callee needs. -void ok(void) { - char *b = buffer_new(8); - if (!b) - return; - buffer_put8(b); - buffer_fill(b, 8); - free(b); -} - -// The extent comes back from `buffer_new`; the requirement from `buffer_put8`. -void short_alloc(void) { - char *b = buffer_new(4); - if (!b) - return; - // CHECK: rfc0011-extents.c:[[@LINE+2]]:15: error: 'buffer_put8' requires 8 bytes behind 'b', which has 4 bytes [weavec::out-of-bounds] - // CHECK: rfc0011-extents.c:[[@LINE-4]]:13: note: 'b' is allocated here - buffer_put8(b); - free(b); -} - -// A symbolic requirement against a constant extent. -void short_fill(void) { - char buf[16]; - buffer_fill(buf, 16); - // CHECK: rfc0011-extents.c:[[@LINE+1]]:15: error: 'buffer_fill' requires 17 bytes behind 'buf', which has 16 bytes [weavec::out-of-bounds] - buffer_fill(buf, 17); -} - -// A local index the caller proves against the extent it got back. -void indexed(size_t n) { - char *b = buffer_new(n); - if (!b) - return; - // CHECK: rfc0011-extents.c:[[@LINE+1]]:3: error: 'b[n]' is out of bounds: 'n' is the number of elements of 'b' [weavec::out-of-bounds] - b[n] = 0; - free(b); -} - -// The release at an offset composes: `payload` is a derived pointer into -// the `struct wrapped`, and the callee frees the whole from it. -void release_wrapped(void) { - struct wrapped *w = malloc(sizeof *w); - if (!w) - return; - w->tag = 1; - wrapped_release(w->payload); -} - -// ... so the same pointer given twice is a double free of the container. -void release_wrapped_twice(void) { - struct wrapped *w = malloc(sizeof *w); - if (!w) - return; - wrapped_release(w->payload); - // The report names the argument the callee released through. - // CHECK: rfc0011-extents.c:[[@LINE+1]]:3: error: 'w->payload' is freed twice [weavec::double-free] - wrapped_release(w->payload); -} diff --git a/test/WholeProgram/rfc0012-sized-fields.c b/test/WholeProgram/rfc0012-sized-fields.c deleted file mode 100644 index c71eec31..00000000 --- a/test/WholeProgram/rfc0012-sized-fields.c +++ /dev/null @@ -1,55 +0,0 @@ -// RFC 0012, *Sized fields*, "Inference": the stores in vec.c witness -// `(struct vec.items, struct vec.cap, 4)` and nothing in the program refutes -// it, so the old engine checked a reader in another unit against the count; -// `struct view.raw` is stored from a caller's pointer once, which refutes it. -// -// RFC 0031 §6.1 and §7: the object engine exports no sized-field facts (the -// record's `sizedFields` and `sizedFieldLoads` are gone), and it infers -// counted-field invariants only for records a unit defines in its main file -// (*Implementation amendments*, "Counted-field invariants"): `struct vec` is -// a header's, which other units' stores would have to confirm at link (RFC -// 0030 §7.6, A3, not built), so the readers here are -// `unresolved(unknown-extent)`: never proven, never a definite -// `out-of-bounds` (the retired golden comparison of RFC 0030, *Lit tests*). This test -// keeps asserting that no access is proven or reported. -// -// RUN: %weavec --whole-program %s %S/Inputs/vec.c -- -I%S/Inputs 2>&1 | FileCheck %s -// -// Alone, nothing witnesses the pair: nothing is reported. -// RUN: %weavec %s -- -I%S/Inputs 2>&1 | FileCheck --allow-empty --check-prefix=ALONE %s -#include "../Inputs/prelude.h" -#include "vec.h" - -// CHECK-NOT: out-of-bounds -// CHECK: 0 errors, 0 warnings - -// ALONE-NOT: error: -// ALONE-NOT: out-of-bounds - -// Clean: within the count. -int sum(struct vec *v) { - int total = 0; - for (size_t i = 0; i < v->n; i++) - total += v->items[i]; - return total; -} - -// The count is `cap`, not `n` (`vec_push` writes `n` beside a store into -// `items`, so the pair `(items, n)` is not refuted, but no store witnesses -// it either): `v->items[v->cap]` is one past the end, `v->items[v->n]` is -// undecided (`n <= cap` is not known here). -int last(struct vec *v) { - int x = v->items[v->n]; - // With the pair, a checked facet (RFC 0030 §3.3); now not proven. - return v->items[v->cap] + x; -} - -// A loop to the count inclusive. -void zero(struct vec *v) { - for (size_t i = 0; i <= v->cap; i++) - v->items[i] = 0; -} - -// Refuted: `view_own` witnesses `(raw, len)`, `view_borrow` stores a pointer -// of unknown extent into `raw`; the refutation wins. -int view_last(struct view *w) { return w->raw[w->len]; } diff --git a/test/WholeProgram/rfc0013-heap.c b/test/WholeProgram/rfc0013-heap.c deleted file mode 100644 index 1d114cc0..00000000 --- a/test/WholeProgram/rfc0013-heap.c +++ /dev/null @@ -1,37 +0,0 @@ -// RFC 0013: heap state across units in the whole-program analysis. -// RUN: not %weavec --whole-program %s %S/Inputs/heap13.c -- 2>&1 | FileCheck %s -#include "../Inputs/prelude.h" -#include "Inputs/heap13.h" - -// The format-30 summaries (RFC 0031 *Summary format 30*): `heap13_new`'s -// result owns a fresh 4-byte block in `data`, and `heap13_wrap`'s result -// holds its argument there. - -void overflow(void) { - struct heap13_box *b = heap13_new(); if (!b) return; - // CHECK: rfc0013-heap.c:[[@LINE+1]]:3: error: 'b->data[4]' is out of bounds: index 4 of an object of 4 bytes [weavec::out-of-bounds] - b->data[4] = 0; - free(b->data); free(b); -} -void leak(void) { - struct heap13_box *b = heap13_new(); if (!b) return; - // The object engine names the leaked block by the call that made it - // (RFC 0031 §5.8). - // CHECK: rfc0013-heap.c:[[@LINE+1]]:3: warning: result of 'heap13_new' is leaked [weavec::leak] - free(b); -} -void alias(void) { - char *p = malloc(4); if (!p) return; - struct heap13_box *b = heap13_wrap(p); - if (!b) { free(p); return; } - free(p); - // CHECK: rfc0013-heap.c:[[@LINE+1]]:3: error: use of 'b->data' after it was freed [weavec::use-after-free] - b->data[0] = 0; - free(b); -} -void clean(void) { - struct heap13_box *b = heap13_new(); if (!b) return; - b->data[3] = 0; free(b->data); free(b); -} -int main(void) { clean(); return 0; } -// CHECK: 1 warning and 2 errors generated. diff --git a/test/WholeProgram/rfc0013-lazy.c b/test/WholeProgram/rfc0013-lazy.c deleted file mode 100644 index 3a620c7c..00000000 --- a/test/WholeProgram/rfc0013-lazy.c +++ /dev/null @@ -1,15 +0,0 @@ -// RFC 0013: entry guards on output graphs survive global remapping across units. -// RUN: not %weavec --whole-program %s %S/Inputs/heap13.c -- 2>&1 | FileCheck %s -#include "../Inputs/prelude.h" -#include "Inputs/heap13.h" -void bad(void) { - heap13_ensure(); - if (!heap13_singleton || !heap13_singleton->data) return; - heap13_drop_child(); - heap13_ensure(); - // CHECK: rfc0013-lazy.c:[[@LINE+1]]:3: error: use of 'heap13_singleton->data' after it was freed [weavec::use-after-free] - heap13_singleton->data[0] = 0; - free(heap13_singleton); heap13_singleton = NULL; -} -int main(void) { return 0; } -// CHECK: 1 error generated. diff --git a/test/WholeProgram/rfc0013-swap.c b/test/WholeProgram/rfc0013-swap.c deleted file mode 100644 index c0906ac4..00000000 --- a/test/WholeProgram/rfc0013-swap.c +++ /dev/null @@ -1,28 +0,0 @@ -// RFC 0013: swap inputs and consumption of extracted old values cross units. -// RUN: not %weavec --whole-program %s %S/Inputs/heap13.c -- 2>&1 | FileCheck %s -#include "../Inputs/prelude.h" -#include "Inputs/heap13.h" - -void good(void) { - struct heap13_box b = {malloc(8)}; - heap13_reset(&b); - if (b.data) b.data[3] = 0; - free(b.data); -} -void overflow(void) { - struct heap13_box b = {malloc(8)}; - heap13_reset(&b); - // CHECK: rfc0013-swap.c:[[@LINE+1]]:15: error: 'b.data[4]' is out of bounds: index 4 of an object of 4 bytes [weavec::out-of-bounds] - if (b.data) b.data[4] = 0; - free(b.data); -} -void stale(void) { - struct heap13_box b = {malloc(8)}; if (!b.data) return; - char *old = b.data; - heap13_reset(&b); - // CHECK: rfc0013-swap.c:[[@LINE+1]]:3: error: use of 'old' after it was freed [weavec::use-after-free] - old[0] = 0; - free(b.data); -} -int main(void) { return 0; } -// CHECK: 2 errors generated. diff --git a/test/WholeProgram/rfc0015-arrays.c b/test/WholeProgram/rfc0015-arrays.c deleted file mode 100644 index 987fba15..00000000 --- a/test/WholeProgram/rfc0015-arrays.c +++ /dev/null @@ -1,48 +0,0 @@ -// RFC 0015: arrays across units in the whole-program analysis. -// RUN: not %weavec --whole-program %s %S/Inputs/array15.c -- 2>&1 | FileCheck %s -#include "Inputs/array15.h" - -// RFC 0031 §6.1: format 30 has no array copy, fill or release forms; they -// are stores and releases over `[*]` steps, with an element range where one -// is known (`array15_drop`, `array15_clear`), and constant indices as byte -// offsets (`array15_compact`, *Summary paths*). - -// Format 30 has no per-element copy or release forms: a copy or a clone -// is known per element through the context the call asks of `array15.c` -// (RFC 0031 §7 *Amendment (cross-unit contexts)*), but the element a clear -// released is not: `cleared` is `unresolved(may-alias-released)`, never -// proven, where the old engine reported a use after free -// (the retired golden comparison of RFC 0030, *Lit tests*). - -void selected(char **a) { - array15_drop(a,0); array15_drop(a,1); - // CHECK: rfc0015-arrays.c:[[@LINE+1]]:3: error: use of 'a[0]' after it was freed [weavec::use-after-free] - a[0][0]=1; -} -void copied(char **a, char **b) { - array15_copy(b,a,3); free(a[2]); - // CHECK: rfc0015-arrays.c:[[@LINE+2]]:3: error: use of 'b[2]' after it was freed [weavec::use-after-free] - // CHECK: rfc0015-arrays.c:[[@LINE-2]]:24: note: freed here (through 'a[2]') - b[2][0]=1; -} -void compacted(char **a) { - char *old=a[1]; array15_compact(a); free(old); - // CHECK: rfc0015-arrays.c:[[@LINE+1]]:3: error: use of 'a[0]' after it was freed [weavec::use-after-free] - a[0][0]=1; -} -void returned(char **a) { - char **b=array15_clone(a,3); if (!b) return; free(a[2]); - // CHECK: rfc0015-arrays.c:[[@LINE+1]]:3: error: use of 'b[2]' after it was freed [weavec::use-after-free] - b[2][0]=1; free(b); -} -void cleared(char **a) { - char *old=a[1]; array15_clear(a,3); - old[0]=1; -} -void clean(char **a, char **b) { - array15_copy(b,a,3); free(a[2]); b[1][0]=1; - char *c[3]={NULL}; array15_fill(c,3); array15_clear(c,3); - free(c[0]); free(c[1]); free(c[2]); -} -int main(void) { return 0; } -// CHECK: 4 errors generated. diff --git a/test/WholeProgram/rfc0016-composition.c b/test/WholeProgram/rfc0016-composition.c deleted file mode 100644 index 07aff260..00000000 --- a/test/WholeProgram/rfc0016-composition.c +++ /dev/null @@ -1,27 +0,0 @@ -// RFC 0016: callee operation diagnostics from upstream context requests. -// -// RFC 0031 §7 *Amendment (cross-unit contexts)*: `bad` passes one object -// twice, asks the callee's unit for that context, and the callee's unit -// reports what the context finds there, with a note that another unit made -// the call. -// -// RUN: not %weavec --whole-program %s %S/Inputs/rfc0016-callee.c -- 2>&1 | FileCheck %s -#include "../Inputs/prelude.h" -void release_then_write(char *, char *); -void write_then_release(char *, char *); -void replace_related(char **, char **); -void bad(void) { - char *p = malloc(4); if (p) release_then_write(p, p); -} -void good(void) { - char *p = malloc(4); if (!p) return; - write_then_release(p, p); - p = malloc(4); if (!p) return; - replace_related(&p, &p); if (p) *p = 1; free(p); -} -// CHECK: rfc0016-callee.c:4:4: error: use of 'b' after it was freed [weavec::use-after-free] -// CHECK: rfc0016-callee.c:3:3: note: freed here (through 'a') -// CHECK: rfc0016-callee.c:2:6: note: called from another unit with related pointer arguments -// CHECK: 1 error generated. -// CHECK: weavec: program program: {{.*}}; 1 error, 0 warnings -// The callee's row records the violation the context found. diff --git a/test/WholeProgram/rfc0017-numeric.c b/test/WholeProgram/rfc0017-numeric.c deleted file mode 100644 index 4bf405c4..00000000 --- a/test/WholeProgram/rfc0017-numeric.c +++ /dev/null @@ -1,79 +0,0 @@ -// RFC 0017: numeric results, output values and access intervals compose. -// RUN: not %weavec --whole-program %s %S/Inputs/rfc0017-numeric.c -- -ferror-limit=0 2>&1 | FileCheck %s -// -// RFC 0031 §6.1: format-30 summaries carry no extent requirements and no -// numeric output expressions; a call's constant arguments reach the callee -// through the context it asks of the other unit (RFC 0031 §7 *Amendment -// (cross-unit contexts)*), and what that context stores on every return -// outside the caller's object is an error at the call (*Implementation -// amendments*, "Stores past the caller's object"). -#include "../Inputs/prelude.h" -unsigned char narrow(unsigned); -void narrow_out(unsigned, unsigned char *); -void put_at(char *, int); -void fill_min(char *, size_t, size_t); -char *make_product(size_t, size_t); -int checked_size(size_t, size_t, size_t *); -void reverse_outputs(unsigned *, unsigned *); -void *checked_allocation(size_t, size_t); -void narrowed(void) { - int *p = malloc(sizeof *p); if (!p) return; - free(p); - // CHECK: rfc0017-numeric.c:[[@LINE+1]]:26: error: use of 'p' after it was freed [weavec::use-after-free] - if (narrow(256) == 0) *p = 1; -} -void output(void) { - unsigned char k; narrow_out(256, &k); - int *p = malloc(sizeof *p); if (!p) return; - free(p); - // CHECK: rfc0017-numeric.c:[[@LINE+1]]:16: error: use of 'p' after it was freed [weavec::use-after-free] - if (k == 0) *p = 1; -} -void before(void) { - char a[4]; - // CHECK: rfc0017-numeric.c:[[@LINE+1]]:10: error: 'put_at' requires 'a' before its start [weavec::out-of-bounds] - put_at(a, -1); -} -void minimum(void) { - char a[4]; - // CHECK: rfc0017-numeric.c:[[@LINE+1]]:12: error: 'fill_min' requires 5 bytes behind 'a', which has 4 bytes [weavec::out-of-bounds] - fill_min(a, 7, 5); -} -void product(void) { - char *p = make_product(2, 3); if (!p) return; - // `rows * cols` is no format-30 extent term; the context `make_product(2, - // 3)` asks for has the extent 6. - // CHECK: rfc0017-numeric.c:[[@LINE+1]]:3: error: 'p[6]' is out of bounds: index 6 of an object of 6 bytes [weavec::out-of-bounds] - p[6] = 1; free(p); -} -void good(void) { - char a[4]; put_at(a + 1, -1); fill_min(a, 8, 4); - size_t size; - if (checked_size(2, 3, &size)) return; - char *p = malloc(size); if (!p) return; - p[5] = 1; free(p); - int *q = malloc(sizeof *q); if (!q) return; - // `narrow(256)` is 0 and `calloc((size_t)-1, 2)` is null in the calls' - // contexts: neither branch is taken. - free(q); if (narrow(256) != 0) *q = 1; - q = checked_allocation((size_t)-1, 2); - if (q) { free(q); *q = 1; } - unsigned x; - reverse_outputs(&x, &x); - // `x` is 2 (the callee writes `*b` before `*a`): this branch is never - // taken. - q = malloc(sizeof *q); if (!q) return; - free(q); if (x == 1) *q = 1; -} -void ordered_outputs(void) { - unsigned x; - reverse_outputs(&x, &x); - int *p = malloc(sizeof *p); if (!p) return; - free(p); - // CHECK: rfc0017-numeric.c:[[@LINE+1]]:16: error: use of 'p' after it was freed [weavec::use-after-free] - if (x == 2) *p = 1; -} -// CHECK-NOT: {{warning|error}}: -// CHECK: 6 errors generated. -// The callees' accesses at a negative index and past the array are not -// proven (RFC 0017 §5: `counted(i + 1)` does not cover a signed index). diff --git a/test/cases/CMakeLists.txt b/test/cases/CMakeLists.txt index 6798d965..f10185b8 100644 --- a/test/cases/CMakeLists.txt +++ b/test/cases/CMakeLists.txt @@ -40,8 +40,8 @@ foreach(suite IN LISTS _weavec_case_suites) NAME cases-${suite} COMMAND "${Python3_EXECUTABLE}" "${PROJECT_SOURCE_DIR}/scripts/run-cases.py" - --weavec "$" --weavec-cc "$" - --filter "${suite}/**" --jobs "${WEAVEC_CASES_JOBS}" --json + --weavec-cc "$" --filter "${suite}/**" --jobs + "${WEAVEC_CASES_JOBS}" --json "${CMAKE_CURRENT_BINARY_DIR}/cases-${suite}.json" ${_weavec_cases_args}) set_tests_properties( cases-${suite} PROPERTIES LABELS "integration;cases" TIMEOUT 900 PROCESSORS diff --git a/test/cases/README.md b/test/cases/README.md index e914ef70..6cb32547 100644 --- a/test/cases/README.md +++ b/test/cases/README.md @@ -1,11 +1,10 @@ # Test cases -`test/cases` is the tree of executable C test cases (RFC 0035 §11). Each -case is a C file whose expectations are line-comment *markers*. -`scripts/run-cases.py` checks the advisory analysis (the `weavec` tool) -against the `BUG` and `CLEAN` markers, builds the case as a default -`weavec-cc` build does, checks its enforcement ledger, runs it and attributes -every stop to a line and a report kind, and optionally runs it under ASan. +`test/cases` is the tree of executable C test cases (RFC 0035 §11, RFC 0036 +§9). Each case is a C file whose expectations are line-comment *markers*. +`scripts/run-cases.py` builds the case as a default `weavec-cc` build does, +checks its enforcement ledger, runs it and attributes every stop to a line +and a report kind, and optionally runs it under ASan. The lit suites (`test/Guards`, `test/Driver`, ...) pin exact IR and messages; these cases pin what must be reported and what must stop. @@ -14,11 +13,11 @@ messages; these cases pin what must be reported and what must stop. | `evaluation/` | the bug and clean programs of the fixed evaluation (RFC 0013) | | `pairs/` | RFC 0017's bug/clean pairs | | `recall//` | recall pins, by CWE | -| `engine/` | analysis pins reduced from the lit engine tests | | `soundness/` | soundness probes, each bug with a correct twin, and the alias probes (`alias-*`) | | `repros/` | root-cause false-positive repros and RFC 0031's held-out repros (`ooc-*`) | | `proofs/` | cases that once caught a false proof (`SOURCES.md` gives their origin) | -| `semantics//` | cases per feature: `objects/` the analysis's object domain, `runtime/` the runtime's guards and releases, `dropin/` untuned code, `confirm/` false stops and silent misses, `detect-probes/` detection probes, ... | +| `determinism/` | RFC 0036's cases: a bug per class it makes deterministic, each with a twin, and the bugs it leaves to a later RFC as `XFAIL` (its `README.md`) | +| `semantics//` | cases per feature: `runtime/` the runtime's guards and releases, `dropin/` untuned code, `confirm/` false stops and silent misses, `detect-probes/` detection probes, ... | | `detection/` | RFC 0034's detection set: one program per directory, each with a fixed twin ([Detection cases](#detection-cases)) | | `detection-blind/` | 48 blind single-file bug programs with `-DFIX` twins (its `README.md`) | @@ -37,8 +36,7 @@ scripts/run-cases.py --filter pairs/min-bound-bug.c -v --keep ``` - The binaries default to `build/release/bin`, or `build/dev/bin` when there - is no release build; `--build-dir`, `--weavec` and `--weavec-cc` override - them. The ASan oracle uses `$WEAVEC_LLVM_PREFIX/bin/clang`, else `clang` + is no release build; `--build-dir` and `--weavec-cc` override them. The ASan oracle uses `$WEAVEC_LLVM_PREFIX/bin/clang`, else `clang` on the `PATH`, or `--clang`. - `--filter GLOB` selects cases by their path under `test/cases` (`'soundness/**'`, `'pairs/*-bug.c'`); a pattern without wildcards selects @@ -48,11 +46,11 @@ scripts/run-cases.py --filter pairs/min-bound-bug.c -v --keep (a `weavec.proven:` report) fails the case whatever its markers. - `--asan` runs the ASan oracle for every case that has a `main` (the `ASAN` marker does so for one case, and also requires a report). -- `--no-run` analyses and builds only. `--jobs N` runs N cases at once +- `--no-run` builds only. `--jobs N` runs N cases at once (default: the CPU count). `--timeout` bounds each compile and link (120 s), `--run-timeout` each run (30 s). - `--json OUT` writes `{"summary", "results"}`: per-suite tallies and, per - case, its status, failures, notes, diagnostics, runs, ASan result and the + case, its status, failures, notes, runs, ASan result and the commands it ran. `--verbose` prints passing cases too; `--keep` keeps the build directories (under `$TMPDIR/weavec-cases.*`). - A case's status is `PASS`, `FAIL`, `ERROR` (a marker error), `XFAIL` or @@ -80,80 +78,66 @@ markers separated by `//`: // RUN-INPUT: 1 #include ... - p[n] = 0; // BUG: out-of-bounds // TRAP: heap-buffer-overflow + p[n] = 0; // TRAP: heap-buffer-overflow // GUARDED: access ``` | Marker | Kind | Meaning | | --- | --- | --- | -| `BUG: [definite\|possible]` | line | the analysis reports `` on this line (`definite`: an error, `possible`: a warning, omitted: either); for an id a guard can catch, a stop on this line also satisfies it ([below](#how-a-case-is-judged)) | | `TRAP[: ]` | line | some run stops on this line, with that report kind if one is given | -| `MISS: ` | line | a known miss: with `BUG` on the same line, the analysis is known not to report it; in a `DETECT` case, on a `STOP` line, the bug is known not to stop | -| `NEUTRALISED: zero-init` | line | with `BUG` on the same line: zero-initialisation defines the bug away, so the analysis need not report it | +| `MISS: ` | line | in a `DETECT` case, on a `STOP` line: the bug is known not to stop | | `GUARDED[: ]` | line | the enforcement ledger has a `guarded` row on this line (reasons: `access`, `range`, `string`, `checked-call`, `loop-range`) | | `PROVEN[: ]` | line | likewise `proven` (`in-bounds`, `dominated`, `merged`, `optimized`) | | `UNGUARDED[: ]` | line | likewise `unguarded` (`unsafe`) | | `STOP` | line | in a `DETECT` case, a line the bug must stop on | -| `CLEAN` | file | no diagnostic (but `ALLOW`ed warnings), no stop, no death by a signal, no ASan report | -| `ALLOW: [ ...]` | file | in a `CLEAN` case, warnings with these ids are accepted; justify each in a comment | +| `CLEAN` | file | no stop, no death by a signal, no ASan report | | `RUN-INPUT: [< ]` | file | run the program with these arguments (shell quoting; the input file is relative to the case); repeatable, each run independent; an empty `RUN-INPUT:` is a run without arguments | | `TRAP-AT: :` | file | as a `TRAP` on that line of another file (relative to the case), such as a shared `Inputs/` unit or header that only some of its cases expect to stop in | | `EXPECT-LEDGER: ` | file | a value of the main unit's ledger entry (`units[0]`), e.g. `/summary/unguarded == 0`; `op` is `==` `!=` `<=` `>=` `<` `>`; the value is JSON, or a bare string | -| `FLAGS: ` | file | extra `weavec-cc` flags for every compile and link (repeatable; translated for the tool, [below](#how-a-case-is-judged)) | +| `FLAGS: ` | file | extra `weavec-cc` flags for every compile and link (repeatable) | | `UNITS: [ ...]` | file | further translation units linked with this one, relative to the case | | `ASAN` | file | also run the ASan oracle, and require it to report the bug | -| `TOOL` | file | analyse only: no build, no run | | `DETECT: ` | file | a detection case ([below](#detection-cases)): `` (e.g. `-DFIX`) select the fixed twin | | `XFAIL: ` | file | the case is expected to fail today: a failure is `XFAIL`, a pass is `XPASS` (remove the marker); neither fails the run | Report kinds are those of the runtime (`runtime/weavec_report.c`, RFC 0035 -§5.3): `heap-buffer-overflow`, `heap-use-after-free`, -`stack-buffer-overflow`, `stack-use-after-scope`, -`dynamic-stack-buffer-overflow`, `global-buffer-overflow`, -`buffer-overflow`, `null-dereference`, `unterminated-string`, +§5.3, RFC 0036): `heap-buffer-overflow`, `heap-use-after-free`, +`stack-buffer-overflow`, `dynamic-stack-buffer-overflow`, +`stack-use-after-scope`, `global-buffer-overflow`, `intra-object-overflow`, +`use-after-poison`, `null-dereference`, `unterminated-string`, `index-out-of-bounds`, `invalid-release`, `invalid-access` and -`overlapping-copy`. Ids are those of `weavec::core::diag` -(`include/weavec/Core/Diagnostic.h`). +`overlapping-copy`. Rules the grammar leaves implicit, as the runner enforces them: - The marker keywords are reserved at the start of a comment segment. A - keyword used wrongly (`// CLEAN please`, `// BUG leak`), an unknown id, - kind or reason, and a near-miss keyword (`BUGS:`, `UNIT:`) are *marker - errors*: the case is reported as `ERROR` and fails the run. Other + keyword used wrongly (`// CLEAN please`), an unknown kind or reason, + and a near-miss keyword (`TRAPS:`, `UNIT:`) are *marker errors*: the case is reported as `ERROR` and fails the run. Other uppercase prefixes (`NOTE:`, `RFC 0017:`, `STAGE:`) are prose. - A line marker on a comment-only line, or a file marker on a code line or after the first declaration, is a marker error. - A unit named by `UNITS` is not a case of its own. Its line markers apply to its lines, and its own `FLAGS` to its compile only; any other file marker in a unit is a marker error. A unit whose `FLAGS` contain - `-fno-weavec` is compiled as plain Clang and the analysis does not see - it; the case's main file cannot have `-fno-weavec`. Markers are read from + `-fno-weavec` is compiled as plain Clang; the case's main file cannot + have `-fno-weavec`. Markers are read from the case and its units only, never from headers (use `TRAP-AT`). - A case needs an expectation (`CLEAN`, `EXPECT-LEDGER` or a line marker). - `CLEAN` excludes `BUG`, `MISS`, `NEUTRALISED` and `TRAP`; `ALLOW` needs - `CLEAN`; `ASAN` and `RUN-INPUT` need a unit that defines `main`; `TOOL` - excludes both. `STOP` needs `DETECT`. + `CLEAN` excludes `TRAP`; `ASAN` and `RUN-INPUT` need a unit that defines + `main`. `STOP` and `MISS` need `DETECT`. A case without `main` needs a + ledger marker or `EXPECT-LEDGER`. ## How a case is judged -1. **Analysis**, when the case has a `BUG`, is `CLEAN` or is a `TOOL` case: - `weavec [] [--whole-program] -- -I `, - with `--whole-program` when it has several analysed units. `FLAGS` are - split: `-fno-weavec-zero-init` becomes `--no-zero-init`, - `-fweavec-budget=N` becomes `--budget=N`, the `-W…weavec…` flags are the - tool's own, other `-fweavec`/`-fno-weavec` flags are dropped, and the - rest go after `--`. A crash (an exit other than 0 or 1) or a timeout - fails the case. -2. **Build** (not for `TOOL`): each unit with +1. **Build**: each unit with `weavec-cc [-fweavec-checks=verify] -I -c`, adding `-fweavec-ledger=/` when a ledger marker or `EXPECT-LEDGER` reads the ledger, then a link with the case's `FLAGS` when a unit defines `main`. A failed compile or link fails the case. -3. **Ledger.** The main unit's `.ledger.json` must be a +2. **Ledger.** The main unit's `.ledger.json` must be a `weavec-ledger` of version 3. `GUARDED`, `PROVEN` and `UNGUARDED` each need a row at their file and line with that outcome (and that reason, if given); `EXPECT-LEDGER` compares the value its pointer names. -4. **Runs**, when the build produced an executable (not with `--no-run`): +3. **Runs**, when the build produced an executable (not with `--no-run`): once per `RUN-INPUT`, or once without arguments; stdin is `/dev/null` unless redirected. A run's *stop* is its first `weavec: at ::: ...` report when it died by `SIGTRAP` or `SIGILL` @@ -161,29 +145,16 @@ Rules the grammar leaves implicit, as the runner enforces them: `weavec: invalid release of ...` (an `invalid-release` with no location); or `SIGSEGV`/`SIGBUS` (a `null-dereference` with no location, the null page). Every stop must match a `TRAP` (its kind, if it names one, and its - line, unless the stop has no location) or be on a `BUG` line (a stop with - no location is accepted in any case with a `BUG`); every `TRAP` must be - matched by some run. A timeout, a death by another signal or a trap with + line, unless the stop has no location); every `TRAP` must be matched by + some run. A timeout, a death by another signal or a trap with no report, and a report that did not stop the run fail the case; in a `CLEAN` case so does any report or signal. A `weavec.proven:` report (verify mode) fails the case. -5. **Diagnostics.** Each `BUG` is satisfied by a diagnostic with its id on - its line and of its severity. For an id a guard can catch - (`out-of-bounds`, `null-dereference`, `use-after-free`, `double-free`, - `invalid-release`, `use-of-uninitialized`, `mismatched-release`, - `lifetime-too-short`, `use-after-move`, `allocation-failure`, - `contradicted-assumption`, `unsafe-operation`), a stop located on its - line, or a stop with no location whose kind is the id, satisfies it too, - and so does the absence of any run (`TOOL`, no `main`, `--no-run`). A - `MISS` or `NEUTRALISED` on the line excuses it. Beyond the `BUG`, `MISS` - and `NEUTRALISED` lines, a `CLEAN` case fails on any diagnostic but an - `ALLOW`ed warning, and any other case on an error. -6. **ASan oracle** (`--asan` or `ASAN`), for a case with `main`: +4. **ASan oracle** (`--asan` or `ASAN`), for a case with `main`: `clang -fsanitize=address,array-bounds -fno-sanitize-recover=array-bounds -g -O0 -I` over all units with their flags minus WeaveC's own, run with - `detect_leaks=0:detect_stack_use_after_return=1`. A case without a bug - line (`BUG`, `MISS`, `NEUTRALISED`, `TRAP`) fails on any report; a case - with `ASAN` and a bug line fails without one. + `detect_leaks=0:detect_stack_use_after_return=1`. A case without a `TRAP` + fails on any report; a case with `ASAN` and a `TRAP` fails without one. An `XFAIL` case is judged as usual; its failure is reported as `XFAIL` and its pass as `XPASS`. A marker error is still `ERROR`. @@ -191,14 +162,13 @@ its pass as `XPASS`. A marker error is still `ERROR`. ## Detection cases A `DETECT` case is judged by whether its bug *stops*, not by what is -reported where, and is not analysed. It has one or more `STOP` lines (the -faulty operation) and none of `CLEAN`, `ALLOW`, `TOOL`, `BUG`, `TRAP`, -`TRAP-AT`, `GUARDED`, `PROVEN`, `UNGUARDED`, `NEUTRALISED` or -`EXPECT-LEDGER`; a unit must define `main`. The runner builds it twice, as +reported where. It has one or more `STOP` lines (the faulty operation) +and none of `CLEAN`, `TRAP`, `TRAP-AT`, `GUARDED`, `PROVEN`, `UNGUARDED` +or `EXPECT-LEDGER`; a unit must define `main`. The runner builds it twice, as is (the bug) and with the `DETECT` flags added to every compile and link (the twin), and runs each build once per `RUN-INPUT`. -- **The bug stops** when some run's stop (as in step 4) is on a `STOP` line +- **The bug stops** when some run's stop (as in step 3) is on a `STOP` line or has no location. A first stop on another line is not a stop. A bug that does not stop fails the case, unless its `STOP` line carries `MISS: `: a known miss passes with a note, and when it stops the @@ -222,9 +192,9 @@ any further units named by `UNITS`, its headers and its inputs), built with there is no manifest to update. 2. Start with a comment saying what the case is about and where it comes from, then the file markers, then the code. -3. Mark a correct program `CLEAN`. In a bug program, pin what the analysis - should report with `BUG` on that line (with `MISS: ` when it does - not, today), and where the program must stop with `TRAP: `. Give it +3. Mark a correct program `CLEAN`. In a bug program, pin where the program + must stop with `TRAP: ` (and `XFAIL: ` when it does not, + today). Give it a `main` that reaches the bug (with `RUN-INPUT` if it needs arguments), and add `ASAN` when ASan can confirm the bug. Pin enforcement itself with `GUARDED`, `UNGUARDED` or `EXPECT-LEDGER` where that is the point. @@ -238,5 +208,7 @@ analysis-ledger facets (`UNRESOLVED`, `TRUSTED`, `NOT-PROVEN`), check templates in `TRAP`, require levels and a comparison against the v0.10.0 golden binaries (`GOLDEN.md`, `KNOWN-DIFFERENCES.md`). RFC 0035 deleted the analysis ledger, the checks and the require levels; the cases were -converted to the grammar above, and the comments of older cases may still -cite the RFC they were written for. +converted to the grammar above. RFC 0036 deleted the advisory analysis and +with it the markers that pinned it (`BUG`, `MISS` on a `BUG`, +`NEUTRALISED`, `ALLOW`, `TOOL`); the comments of older cases may still cite +the RFC they were written for. diff --git a/test/cases/determinism/README.md b/test/cases/determinism/README.md new file mode 100644 index 00000000..19ace48a --- /dev/null +++ b/test/cases/determinism/README.md @@ -0,0 +1,24 @@ +# Determinism cases + +RFC 0036 §9 and gate G1: each bug of the RFC's motivation, and the other +bugs it names, stops with its report kind in every run, however much the +program allocated in between; each `_bug.c` has a correct twin, `_ok.c`, +that runs clean. + +| Case | Bug | Today | +| --- | --- | --- | +| `uaf-after-churn` | a call through a released block after ~28 MiB of allocations | stops (reuse after a sweep, §3) | +| `uaf-spray` | a read through a released block after 64 MiB of reuse | stops (§3) | +| `double-free-after-churn` | a second release after the block's slot could have been reused | stops (§3) | +| `field-strcpy` | `strcpy` past an 8-byte field into the next one | stops (the fortified size, §6) | +| `asan-pool` | an over-read of a pool object the pool poisons through ASan's interface | stops (§5) | +| `escaping-oob` | a 1-based array read one past its end in another function | stops (the slot's tail) | +| `shared-frame` | (twin only) recursion through locals in disjoint `switch` arms to 10,000 frames | runs (shared frame space, §2) | +| `heap-jump` | an index from one heap block that lands in a neighbouring live block | `XFAIL`: a later RFC | +| `global-jump` | a read through a pointer from one global into the next | `XFAIL`: a later RFC | +| `use-after-return` | a read through the address of a returned function's local | `XFAIL`: a later RFC | +| `use-after-longjmp` | a read through a local of a frame `longjmp` abandoned | `XFAIL`: a later RFC | + +The `XFAIL` cases pin the bugs RFC 0036 leaves to a later RFC (spatial +determinism and use after return); the runner reports `XPASS` when one +starts to stop, and its marker then goes. diff --git a/test/cases/determinism/asan-pool_bug.c b/test/cases/determinism/asan-pool_bug.c new file mode 100644 index 00000000..bf631107 --- /dev/null +++ b/test/cases/determinism/asan-pool_bug.c @@ -0,0 +1,42 @@ +// RFC 0036 §5: a pool allocator written for ASan poisons its free space and +// unpoisons each object it hands out; an over-read of one object into the +// next, unallocated one stops as ASan would stop it. +// RUN-INPUT: +#include +#include +#include + +#if defined(__has_feature) +#if __has_feature(address_sanitizer) +#include +#define POISON(p, n) ASAN_POISON_MEMORY_REGION((p), (n)) +#define UNPOISON(p, n) ASAN_UNPOISON_MEMORY_REGION((p), (n)) +#endif +#endif +#ifndef POISON +#define POISON(p, n) ((void)(p), (void)(n)) +#define UNPOISON(p, n) ((void)(p), (void)(n)) +#endif + +static char *pool; +static size_t top; + +static void *palloc(size_t n) { + void *p = pool + top; + top += (n + 15) & ~(size_t)15; + UNPOISON(p, n); + return p; +} + +int main(void) { + pool = malloc(4096); + if (pool == NULL) + return 1; + POISON(pool, 4096); + char *a = palloc(16); + memset(a, 'a', 16); + char out[32]; + memcpy(out, a, 32); // TRAP: use-after-poison + printf("%c\n", out[0]); + return 0; +} diff --git a/test/cases/determinism/asan-pool_ok.c b/test/cases/determinism/asan-pool_ok.c new file mode 100644 index 00000000..f9ac6260 --- /dev/null +++ b/test/cases/determinism/asan-pool_ok.c @@ -0,0 +1,45 @@ +// RFC 0036 §5: the twin of asan-pool_bug.c, which copies only its object. +// CLEAN +// RUN-INPUT: +#include +#include +#include + +#if defined(__has_feature) +#if __has_feature(address_sanitizer) +#include +#define POISON(p, n) ASAN_POISON_MEMORY_REGION((p), (n)) +#define UNPOISON(p, n) ASAN_UNPOISON_MEMORY_REGION((p), (n)) +#endif +#endif +#ifndef POISON +#define POISON(p, n) ((void)(p), (void)(n)) +#define UNPOISON(p, n) ((void)(p), (void)(n)) +#endif + +static char *pool; +static size_t top; + +static void *palloc(size_t n) { + void *p = pool + top; + top += (n + 15) & ~(size_t)15; + UNPOISON(p, n); + return p; +} + +int main(void) { + pool = malloc(4096); + if (pool == NULL) + return 1; + POISON(pool, 4096); + char *a = palloc(16); + char *b = palloc(16); + memset(a, 'a', 16); + memset(b, 'b', 16); + char out[32]; + memcpy(out, a, 16); + memcpy(out + 16, b, 16); + printf("%c%c\n", out[0], out[16]); + free(pool); + return 0; +} diff --git a/test/cases/determinism/double-free-after-churn_bug.c b/test/cases/determinism/double-free-after-churn_bug.c new file mode 100644 index 00000000..6b15f31a --- /dev/null +++ b/test/cases/determinism/double-free-after-churn_bug.c @@ -0,0 +1,19 @@ +// RFC 0036 §3: a block released twice stops at the second release, even +// after enough churn that its slot could have been reused: it is not +// reused while the stale pointer remains. +// RUN-INPUT: +#include +#include + +int main(void) { + char *p = malloc(48); + if (p == NULL) + return 1; + free(p); + for (long i = 0; i < 64L * 1024 * 1024 / 48; i++) + free(malloc(48)); + char *q = malloc(48); + free(p); // TRAP: invalid-release + printf("%d\n", p == q); + return 0; +} diff --git a/test/cases/determinism/double-free-after-churn_ok.c b/test/cases/determinism/double-free-after-churn_ok.c new file mode 100644 index 00000000..c33f4e56 --- /dev/null +++ b/test/cases/determinism/double-free-after-churn_ok.c @@ -0,0 +1,19 @@ +// RFC 0036 §3: the twin of double-free-after-churn_bug.c, which releases +// each block once. +// CLEAN +// RUN-INPUT: +#include +#include + +int main(void) { + char *p = malloc(48); + if (p == NULL) + return 1; + free(p); + for (long i = 0; i < 64L * 1024 * 1024 / 48; i++) + free(malloc(48)); + char *q = malloc(48); + free(q); + puts("ok"); + return 0; +} diff --git a/test/cases/determinism/escaping-oob_bug.c b/test/cases/determinism/escaping-oob_bug.c new file mode 100644 index 00000000..1a3a9075 --- /dev/null +++ b/test/cases/determinism/escaping-oob_bug.c @@ -0,0 +1,35 @@ +// A 1-based array (a pointer before its object, stored and passed) read one +// past its end in another function: the byte after the object is its +// slot's tail, so the guard stops it. (Bounds of a pointer stored outside +// its object were the first draft of RFC 0036; the shadow suffices here.) +// RUN-INPUT: +#include +#include + +struct vec { + double *v; // 1-based: v[1] .. v[n] + long n; +}; + +__attribute__((noinline)) static void make(struct vec *x, long n) { + x->v = (double *)malloc(sizeof(double) * (size_t)n) - 1; + x->n = n; +} + +__attribute__((noinline)) static double sum(const struct vec *x, long upto) { + double s = 0; + for (long i = 1; i <= upto; i++) + s += x->v[i]; // TRAP: heap-buffer-overflow + return s; +} + +int main(int argc, char **argv) { + struct vec a, b; + make(&a, 8); + make(&b, 8); + for (long i = 1; i <= 8; i++) + a.v[i] = b.v[i] = 1; + long upto = argc > 1 ? atol(argv[1]) : 9; + printf("%g\n", sum(&a, upto)); + return 0; +} diff --git a/test/cases/determinism/escaping-oob_ok.c b/test/cases/determinism/escaping-oob_ok.c new file mode 100644 index 00000000..f0994525 --- /dev/null +++ b/test/cases/determinism/escaping-oob_ok.c @@ -0,0 +1,37 @@ +// The twin of escaping-oob_bug.c, which stops at the last element. A +// pointer before its object, stored and passed, is no error. +// CLEAN +// RUN-INPUT: +// RUN-INPUT: 8 +#include +#include + +struct vec { + double *v; // 1-based: v[1] .. v[n] + long n; +}; + +__attribute__((noinline)) static void make(struct vec *x, long n) { + x->v = (double *)malloc(sizeof(double) * (size_t)n) - 1; + x->n = n; +} + +__attribute__((noinline)) static double sum(const struct vec *x, long upto) { + double s = 0; + for (long i = 1; i <= upto; i++) + s += x->v[i]; + return s; +} + +int main(int argc, char **argv) { + struct vec a, b; + make(&a, 8); + make(&b, 8); + for (long i = 1; i <= 8; i++) + a.v[i] = b.v[i] = 1; + long upto = argc > 1 ? atol(argv[1]) : 8; + printf("%g\n", sum(&a, upto)); + free(a.v + 1); + free(b.v + 1); + return 0; +} diff --git a/test/cases/determinism/field-strcpy_bug.c b/test/cases/determinism/field-strcpy_bug.c new file mode 100644 index 00000000..0b8e3243 --- /dev/null +++ b/test/cases/determinism/field-strcpy_bug.c @@ -0,0 +1,22 @@ +// RFC 0036 §6: a fortified strcpy into an 8-byte field followed by +// another stops at the call: it writes more than the field's object size. +// FLAGS: -O2 +// RUN-INPUT: +#include +#include +#include + +struct user { + char name[8]; + int admin; +}; + +int main(int argc, char **argv) { + struct user *u = calloc(1, sizeof *u); + if (u == NULL) + return 1; + strcpy(u->name, argc > 1 ? argv[1] : "AAAAAAAAAAA"); // TRAP: intra-object-overflow + printf("%d\n", u->admin); + free(u); + return 0; +} diff --git a/test/cases/determinism/field-strcpy_ok.c b/test/cases/determinism/field-strcpy_ok.c new file mode 100644 index 00000000..c4919b9c --- /dev/null +++ b/test/cases/determinism/field-strcpy_ok.c @@ -0,0 +1,22 @@ +// RFC 0036 §6: the twin of field-strcpy_bug.c, whose name fits. +// CLEAN +// FLAGS: -O2 +// RUN-INPUT: +#include +#include +#include + +struct user { + char name[8]; + int admin; +}; + +int main(int argc, char **argv) { + struct user *u = calloc(1, sizeof *u); + if (u == NULL) + return 1; + strcpy(u->name, argc > 1 ? argv[1] : "AAAAAAA"); + printf("%s %d\n", u->name, u->admin); + free(u); + return 0; +} diff --git a/test/cases/determinism/global-jump_bug.c b/test/cases/determinism/global-jump_bug.c new file mode 100644 index 00000000..e1d2dfe7 --- /dev/null +++ b/test/cases/determinism/global-jump_bug.c @@ -0,0 +1,17 @@ +// A read through a pointer from one global that lands in the next: the +// bytes are the next global's, so the shadow passes it. Left to the later +// RFC on provenance bounds (RFC 0036). +// XFAIL: RFC 0036 leaves spatial determinism and use after return to a later RFC +// RUN-INPUT: +#include +#include + +char name[32]; +char secret[32] = "topsecret"; + +int main(int argc, char **argv) { + char *p = name; + long i = argc > 1 ? atol(argv[1]) : (long)(secret - name); + printf("%c\n", p[i]); // TRAP: global-buffer-overflow + return 0; +} diff --git a/test/cases/determinism/global-jump_ok.c b/test/cases/determinism/global-jump_ok.c new file mode 100644 index 00000000..e7e681e8 --- /dev/null +++ b/test/cases/determinism/global-jump_ok.c @@ -0,0 +1,15 @@ +// The twin of global-jump_bug.c, its index inside `name`. +// CLEAN +// RUN-INPUT: +#include +#include + +char name[32] = "public"; +char secret[32] = "topsecret"; + +int main(int argc, char **argv) { + char *p = name; + long i = argc > 1 ? atol(argv[1]) : 2; + printf("%c %c\n", p[i], secret[0]); + return 0; +} diff --git a/test/cases/determinism/heap-jump_bug.c b/test/cases/determinism/heap-jump_bug.c new file mode 100644 index 00000000..a7e64731 --- /dev/null +++ b/test/cases/determinism/heap-jump_bug.c @@ -0,0 +1,19 @@ +// An index from `a` that lands in the live block `b` next to it: the bytes +// it touches are `b`'s, so the shadow passes it. RFC 0036 leaves bounds from +// a pointer's provenance to a later RFC; this case pins the bug for it. +// XFAIL: RFC 0036 leaves spatial determinism and use after return to a later RFC +// RUN-INPUT: +#include +#include + +int main(int argc, char **argv) { + int *a = malloc(64), *b = malloc(64); + if (a == NULL || b == NULL) + return 1; + b[0] = 0; + // The attacker's index: the distance to b, unless one is given. + long i = argc > 1 ? atol(argv[1]) : (long)(b - a); + a[i] = 0x41414141; // TRAP: heap-buffer-overflow + printf("%x\n", b[0]); + return 0; +} diff --git a/test/cases/determinism/heap-jump_ok.c b/test/cases/determinism/heap-jump_ok.c new file mode 100644 index 00000000..b752060e --- /dev/null +++ b/test/cases/determinism/heap-jump_ok.c @@ -0,0 +1,16 @@ +// The twin of heap-jump_bug.c, its index inside `a`. +// CLEAN +// RUN-INPUT: +#include +#include + +int main(int argc, char **argv) { + int *a = malloc(64), *b = malloc(64); + if (a == NULL || b == NULL) + return 1; + b[0] = 0; + long i = argc > 1 ? atol(argv[1]) : 15; + a[i] = 0x41414141; + printf("%x %x\n", a[15], b[0]); + return 0; +} diff --git a/test/cases/determinism/shared-frame_ok.c b/test/cases/determinism/shared-frame_ok.c new file mode 100644 index 00000000..1bd1650b --- /dev/null +++ b/test/cases/determinism/shared-frame_ok.c @@ -0,0 +1,28 @@ +// RFC 0036 §2: locals in the disjoint arms of a switch share the frame's +// bytes, so a recursion through them reaches the depth the reference +// compiler's frames reach (10,000 frames here, where v0.16.0's frames, one +// redzoned slot per local, overflowed an 8 MiB stack before 7,000). +// CLEAN +// FLAGS: -O2 +// RUN-INPUT: 10000 +#include +#include + +__attribute__((noinline)) static void use(char *p, int n) { p[n] = 1; } + +__attribute__((noinline)) static int depth(int n, int kind) { + if (n == 0) + return 0; + switch (kind & 3) { + case 0: { char a[200]; use(a, n & 7); return depth(n - 1, kind + 1) + a[n & 7]; } + case 1: { char b[200]; use(b, n & 7); return depth(n - 1, kind + 1) + b[n & 7]; } + case 2: { char c[200]; use(c, n & 7); return depth(n - 1, kind + 1) + c[n & 7]; } + default: { char d[200]; use(d, n & 7); return depth(n - 1, kind + 1) + d[n & 7]; } + } +} + +int main(int argc, char **argv) { + int n = argc > 1 ? atoi(argv[1]) : 1000; + printf("%d\n", depth(n, 0)); + return 0; +} diff --git a/test/cases/determinism/uaf-after-churn_bug.c b/test/cases/determinism/uaf-after-churn_bug.c new file mode 100644 index 00000000..8eb5054c --- /dev/null +++ b/test/cases/determinism/uaf-after-churn_bug.c @@ -0,0 +1,35 @@ +// RFC 0036 §3: a released block stays unaddressable until a sweep finds no +// pointer to it, however much the program allocates meanwhile: the call +// through the stale pointer stops. +// RUN-INPUT: +#include +#include + +struct handler { + void (*fn)(void); + char pad[40]; +}; + +static void greet(void) { puts("hello"); } + +static struct handler *keep; + +int main(void) { + keep = malloc(sizeof *keep); + if (keep == NULL) + return 1; + keep->fn = greet; + free(keep); + enum { Count = 600000 }; + void **live = malloc(sizeof(void *) * Count); + if (live == NULL) + return 1; + for (int i = 0; i < Count; i++) + live[i] = malloc(48); + for (int i = 0; i < Count; i += 2) + free(live[i]); + for (int i = 0; i < Count; i += 2) + live[i] = malloc(48); + keep->fn(); // TRAP: heap-use-after-free + return 0; +} diff --git a/test/cases/determinism/uaf-after-churn_ok.c b/test/cases/determinism/uaf-after-churn_ok.c new file mode 100644 index 00000000..7c84d4f9 --- /dev/null +++ b/test/cases/determinism/uaf-after-churn_ok.c @@ -0,0 +1,36 @@ +// RFC 0036 §3: the twin of uaf-after-churn_bug.c, which forgets the block +// when it releases it; the churn reuses released blocks. +// CLEAN +// RUN-INPUT: +#include +#include + +struct handler { + void (*fn)(void); + char pad[40]; +}; + +static void greet(void) { puts("hello"); } + +static struct handler *keep; + +int main(void) { + keep = malloc(sizeof *keep); + if (keep == NULL) + return 1; + keep->fn = greet; + keep->fn(); + free(keep); + keep = NULL; + enum { Count = 600000 }; + void **live = malloc(sizeof(void *) * Count); + if (live == NULL) + return 1; + for (int i = 0; i < Count; i++) + live[i] = malloc(48); + for (int i = 0; i < Count; i += 2) + free(live[i]); + for (int i = 0; i < Count; i += 2) + live[i] = malloc(48); + return keep == NULL ? 0 : 1; +} diff --git a/test/cases/determinism/uaf-spray_bug.c b/test/cases/determinism/uaf-spray_bug.c new file mode 100644 index 00000000..d134c0f3 --- /dev/null +++ b/test/cases/determinism/uaf-spray_bug.c @@ -0,0 +1,22 @@ +// RFC 0036 §3: 64 MiB of allocations and releases of the same size never +// give the released block to a new owner while a pointer to it remains. +// RUN-INPUT: +#include +#include +#include + +int main(void) { + char *p = malloc(48); + if (p == NULL) + return 1; + strcpy(p, "secret"); + free(p); + for (long i = 0; i < 64L * 1024 * 1024 / 48; i++) + free(malloc(48)); + char *q = malloc(48); + if (q == NULL) + return 1; + strcpy(q, "attacker"); + printf("%d %c\n", p == q, p[0]); // TRAP: heap-use-after-free + return 0; +} diff --git a/test/cases/determinism/uaf-spray_ok.c b/test/cases/determinism/uaf-spray_ok.c new file mode 100644 index 00000000..114dd5fc --- /dev/null +++ b/test/cases/determinism/uaf-spray_ok.c @@ -0,0 +1,25 @@ +// RFC 0036 §3: the twin of uaf-spray_bug.c: with no pointer left to the +// released block, the spray reuses it. +// CLEAN +// RUN-INPUT: +#include +#include +#include + +int main(void) { + char *p = malloc(48); + if (p == NULL) + return 1; + strcpy(p, "secret"); + free(p); + p = NULL; + for (long i = 0; i < 64L * 1024 * 1024 / 48; i++) + free(malloc(48)); + char *q = malloc(48); + if (q == NULL) + return 1; + strcpy(q, "attacker"); + printf("%c\n", q[0]); + free(q); + return 0; +} diff --git a/test/cases/determinism/use-after-longjmp_bug.c b/test/cases/determinism/use-after-longjmp_bug.c new file mode 100644 index 00000000..06566a8b --- /dev/null +++ b/test/cases/determinism/use-after-longjmp_bug.c @@ -0,0 +1,26 @@ +// A read through a local of a frame `longjmp` abandoned: the stack's shadow +// is cleared before the jump (RFC 0035 §3.5), so the read passes. A use +// after return, left to a later RFC (RFC 0036). +// XFAIL: RFC 0036 leaves spatial determinism and use after return to a later RFC +// RUN-INPUT: +#include +#include + +static jmp_buf there; +static int *saved; + +__attribute__((noinline)) static void deep(int n) { + int local[8] = {0}; + local[0] = n; + saved = local; + longjmp(there, 1); +} + +int main(void) { + if (setjmp(there) == 0) { + deep(7); + return 1; + } + printf("%d\n", saved[0]); // TRAP + return 0; +} diff --git a/test/cases/determinism/use-after-longjmp_ok.c b/test/cases/determinism/use-after-longjmp_ok.c new file mode 100644 index 00000000..42605c6d --- /dev/null +++ b/test/cases/determinism/use-after-longjmp_ok.c @@ -0,0 +1,25 @@ +// The twin of use-after-longjmp_bug.c, which keeps the value in a global +// rather than a pointer into the abandoned frame. +// CLEAN +// RUN-INPUT: +#include +#include + +static jmp_buf there; +static int saved; + +__attribute__((noinline)) static void deep(int n) { + int local[8] = {0}; + local[0] = n; + int *volatile p = local; + saved = p[0]; + longjmp(there, 1); +} + +int main(void) { + for (int round = 0; round < 1000; round++) + if (setjmp(there) == 0) + deep(round); + printf("%d\n", saved); + return 0; +} diff --git a/test/cases/determinism/use-after-return_bug.c b/test/cases/determinism/use-after-return_bug.c new file mode 100644 index 00000000..425b78bb --- /dev/null +++ b/test/cases/determinism/use-after-return_bug.c @@ -0,0 +1,26 @@ +// A read through the address of a returned function's local: the frame's +// shadow is cleared at its return, so the read passes. RFC 0036 leaves use +// after return to a later RFC; this case pins the bug for it. +// XFAIL: RFC 0036 leaves spatial determinism and use after return to a later RFC +// RUN-INPUT: +#include + +__attribute__((noinline)) static int *leak(void) { + int x[4] = {1, 2, 3, 4}; + int *volatile p = x; + return p; +} + +__attribute__((noinline)) static int clobber(void) { + volatile int y[16]; + for (int i = 0; i < 16; i++) + y[i] = 99; + return y[3]; +} + +int main(void) { + int *p = leak(); + clobber(); + printf("%d\n", p[1]); // TRAP + return 0; +} diff --git a/test/cases/determinism/use-after-return_ok.c b/test/cases/determinism/use-after-return_ok.c new file mode 100644 index 00000000..3d8241e8 --- /dev/null +++ b/test/cases/determinism/use-after-return_ok.c @@ -0,0 +1,25 @@ +// The twin of use-after-return_bug.c, which copies the values out before +// it returns. +// CLEAN +// RUN-INPUT: +#include + +__attribute__((noinline)) static int second(void) { + int x[4] = {1, 2, 3, 4}; + int *volatile p = x; + return p[1]; +} + +__attribute__((noinline)) static int clobber(void) { + volatile int y[16]; + for (int i = 0; i < 16; i++) + y[i] = 99; + return y[3]; +} + +int main(void) { + int v = second(); + clobber(); + printf("%d\n", v); + return 0; +} diff --git a/test/cases/engine/Analysis-branches.c b/test/cases/engine/Analysis-branches.c deleted file mode 100644 index c0ce63cb..00000000 --- a/test/cases/engine/Analysis-branches.c +++ /dev/null @@ -1,20 +0,0 @@ -// Engine pin converted from test/Analysis/branches.c; markers are the v0.10.0 golden diagnostics. -// Paths are joined conservatively: a pointer freed on any path is treated as -// possibly freed afterwards. -#include "Inputs/prelude.h" - -void maybe_freed(int c) { - int *p = malloc(4); - if (c) - free(p); - else - use(p); // fine: p is live on this path - use(p); // BUG: use-after-free -} - -void freed_in_loop(int n) { - int *p = malloc(4); - for (int i = 0; i < n; ++i) - free(p); // BUG: double-free - use(p); // BUG: use-after-free -} diff --git a/test/cases/engine/Analysis-clean.c b/test/cases/engine/Analysis-clean.c deleted file mode 100644 index 823c0d6c..00000000 --- a/test/cases/engine/Analysis-clean.c +++ /dev/null @@ -1,40 +0,0 @@ -// Engine pin converted from test/Analysis/clean.c; markers are the v0.10.0 golden diagnostics. -// CLEAN -// Well-formed ownership patterns must not produce any diagnostics. -#include "Inputs/prelude.h" - -void alloc_use_free(void) { - int *p = malloc(sizeof(int)); - if (!p) - return; - *p = 1; - use(p); - free(p); -} - -void reassign_after_free(void) { - int *p = malloc(4); - free(p); - p = malloc(8); - use(p); - free(p); - p = NULL; - use(p); -} - -void free_on_one_path_only(int c) { - int *p = malloc(4); - if (c) { - use(p); - } else { - free(p); - p = NULL; - } - use(p); - free(p); /* the other path's block; free(NULL) on the first (RFC 0007) */ -} - -void unrelated_pointers(int *a, int *b) { - free(a); - use(b); -} diff --git a/test/cases/engine/Analysis-double-free.c b/test/cases/engine/Analysis-double-free.c deleted file mode 100644 index 9e4440c0..00000000 --- a/test/cases/engine/Analysis-double-free.c +++ /dev/null @@ -1,7 +0,0 @@ -// Engine pin converted from test/Analysis/double-free.c; markers are the v0.10.0 golden diagnostics. -#include "Inputs/prelude.h" - -void double_free(int *p) { - free(p); - free(p); // BUG: double-free -} diff --git a/test/cases/engine/Analysis-rfc0002-aliases.c b/test/cases/engine/Analysis-rfc0002-aliases.c deleted file mode 100644 index 33213410..00000000 --- a/test/cases/engine/Analysis-rfc0002-aliases.c +++ /dev/null @@ -1,81 +0,0 @@ -// Engine pin converted from test/Analysis/rfc0002-aliases.c; markers are the v0.10.0 golden diagnostics. -// RFC 0002: pointer copies alias, so a free through one name is a free of -// every name, and the note says which name did it. -#include "Inputs/prelude.h" - -struct node { - struct node *next; - int *data; -}; - -void through_copy(void) { - char *p = malloc(8); - char *q = p; - free(q); - p[0] = 0; // BUG: use-after-free -} - -void through_conditional(int c) { - char *p = malloc(4); - char *q = malloc(4); - char *r = c ? p : q; - free(r); - use(p); // BUG: use-after-free - free(q); // BUG: double-free -} - -void fields_follow_the_alias(struct node *n) { - struct node *m = n; - free(m->data); - use(n->data); // BUG: use-after-free -} - -void copy_after_the_fact(struct node *p) { - free(p->data); - struct node *q = p; - use(q->data); // BUG: use-after-free -} - -void freeing_kills_aliases(void) { - struct node *c = malloc(sizeof *c); - struct node *d = c; - free(c); - use(d); // BUG: use-after-free -} - -// Reassigning an alias separates it again. -void reassigned_alias(void) { - char *p = malloc(4); - char *q = p; - q = malloc(4); - free(q); - use(p); - free(p); -} - -// The alias relation is not transitive across joins, so walking and -// freeing a list is clean (the RFC's motivating false positive). -void free_list(struct node *head) { - while (head) { - struct node *next = head->next; - free(head); - head = next; - } -} - -void unlink_nth(struct node *head, int n) { - struct node *cur = head; - struct node *prev = NULL; - while (cur) { - if (n-- > 0) { - prev = cur; - cur = cur->next; - continue; - } - struct node *victim = cur; - cur = cur->next; - if (prev) - prev->next = cur; - free(victim); - } -} diff --git a/test/cases/engine/Analysis-rfc0002-lifetimes.c b/test/cases/engine/Analysis-rfc0002-lifetimes.c deleted file mode 100644 index 115feb25..00000000 --- a/test/cases/engine/Analysis-rfc0002-lifetimes.c +++ /dev/null @@ -1,63 +0,0 @@ -// Engine pin converted from test/Analysis/rfc0002-lifetimes.c; markers are the v0.10.0 golden diagnostics. -// FLAGS: -Wno-return-stack-address -// RFC 0002: every scope gets a lifetime; a borrow whose holder outlives the -// borrowed object is `lifetime-too-short`. -#include "Inputs/prelude.h" - -int *gp; -static int g; - -int *return_local_via_pointer(void) { - int x = 1; - int *p = &x; - return p; // BUG: lifetime-too-short -} - -int *return_address_of_local(void) { - int x = 1; - return &x; // BUG: lifetime-too-short -} - -void escapes_inner_scope(void) { - int *p; - { - int x = 1; - p = &x; // BUG: lifetime-too-short - } - use(p); -} - -void escapes_through_out_parameter(int **out) { - int x = 1; - *out = &x; // BUG: lifetime-too-short -} - -void escapes_to_global(void) { - int x = 1; - gp = &x; // BUG: lifetime-too-short -} - -// Clean: the borrowed object outlives the holder. -void inner_holder(void) { - int x = 1; - { - int *p = &x; - use(p); - } -} - -void assigned_in_inner_scope(void) { - int *p; - int x = 0; - { - p = &x; - } - use(p); -} - -void statics_live_forever(void) { - static int s; - gp = &s; - int *p = &g; - use(p); -} diff --git a/test/cases/engine/Analysis-rfc0002-moves.c b/test/cases/engine/Analysis-rfc0002-moves.c deleted file mode 100644 index 1bf35212..00000000 --- a/test/cases/engine/Analysis-rfc0002-moves.c +++ /dev/null @@ -1,24 +0,0 @@ -// Engine pin converted from test/Analysis/rfc0002-moves.c; markers are the v0.10.0 golden diagnostics. -// RFC 0002: passing an owned pointer to a WEAVEC_OWNED parameter moves it. -#include "Inputs/prelude.h" -#include - -void take(void *WEAVEC_OWNED p); - -void use_after_move(int *WEAVEC_OWNED p) { - take(p); - use(p); // BUG: use-after-move -} - -void free_after_move(void) { - int *p = malloc(4); - take(p); - free(p); // BUG: use-after-move -} - -void move_then_reinitialise(void) { - int *p = malloc(4); - take(p); - p = malloc(4); - free(p); -} diff --git a/test/cases/engine/Analysis-rfc0002-paths.c b/test/cases/engine/Analysis-rfc0002-paths.c deleted file mode 100644 index eac4773e..00000000 --- a/test/cases/engine/Analysis-rfc0002-paths.c +++ /dev/null @@ -1,57 +0,0 @@ -// Engine pin converted from test/Analysis/rfc0002-paths.c; markers are the v0.10.0 golden diagnostics. -// RFC 0002: the checker is a dataflow over the CFG, so back edges, switch -// fallthrough, goto and short-circuit operands are all real paths. -#include "Inputs/prelude.h" - -void loop(int n) { - char *p = malloc(8); - for (int i = 0; i < n; ++i) { - p[0] = 0; // BUG: use-after-free - free(p); // BUG: double-free - } -} - -void fallthrough(int c) { - char *p = malloc(8); - switch (c) { // BUG: leak - case 0: free(p); - case 1: free(p); // BUG: double-free - } -} - -void backwards_goto(int n) { - char *p = malloc(4); -again: - free(p); // BUG: double-free - if (n--) - goto again; -} - -void short_circuit(int c) { - char *p = malloc(4); - if (c && (free(p), 1)) { - } - use(p); // BUG: use-after-free -} - -void do_while(int n) { - char *p = malloc(4); - do { - use(p); // BUG: use-after-free - free(p); // BUG: double-free - } while (n--); -} - -// Freeing on every path is not a double free, and reinitialisation kills -// the fact on the paths where it happens. -void clean(int c) { - char *p = malloc(4); - if (c) - free(p); - else - free(p); - p = malloc(4); - if (!p) - return; - free(p); -} diff --git a/test/cases/engine/Analysis-rfc0002-places.c b/test/cases/engine/Analysis-rfc0002-places.c deleted file mode 100644 index 90110ea6..00000000 --- a/test/cases/engine/Analysis-rfc0002-places.c +++ /dev/null @@ -1,53 +0,0 @@ -// Engine pin converted from test/Analysis/rfc0002-places.c; markers are the v0.10.0 golden diagnostics. -// RFC 0002: places are paths (`c.buf`, `p->in->buf`, `*pp`, `arr[*]`), not -// just variables. -#include "Inputs/prelude.h" - -struct inner { - int *buf; -}; -struct outer { - struct inner *in; - int n; -}; - -void member_of_local(void) { - struct inner c; - c.buf = malloc(4); - free(c.buf); - use(c.buf); // BUG: use-after-free -} - -void nested_arrows(struct outer *p) { - free(p->in->buf); - use(p->in->buf); // BUG: use-after-free -} - -void deref_parameter(char **pp) { - free(*pp); - use(*pp); // BUG: use-after-free -} - -int deref_freed_object(struct outer *c) { - free(c); - return c->n; // BUG: use-after-free -} - -// RFC 0015: selected elements retain separate temporal and resource state. -void array_summary(void) { - int *arr[4]; - arr[0] = malloc(4); - arr[1] = malloc(4); - free(arr[0]); - use(arr[1]); free(arr[1]); // another element: fine - use(arr[0]); // BUG: use-after-free -} - -// Pointer arithmetic keeps the identity of the object (RFC 0004, *Pointer -// identity*): `q` is `p`. -void arithmetic(void) { - char *p = malloc(4); - char *q = p + 1; - free(p); - use(q); // BUG: use-after-free -} diff --git a/test/cases/engine/Analysis-rfc0002-realloc.c b/test/cases/engine/Analysis-rfc0002-realloc.c deleted file mode 100644 index a41797e4..00000000 --- a/test/cases/engine/Analysis-rfc0002-realloc.c +++ /dev/null @@ -1,49 +0,0 @@ -// Engine pin converted from test/Analysis/rfc0002-realloc.c; markers are the v0.10.0 golden diagnostics. -// RFC 0002: `realloc` consumes its argument, but the old pointer is valid -// again on the path where the result is null. -#include "Inputs/prelude.h" - -int grow(char **buf) { - char *p = *buf; - char *q = realloc(p, 16); - if (q == NULL) { - free(p); // fine: realloc failed, p is still ours - return -1; - } - *buf = q; - return 0; -} - -void grow_in_place(void) { - char *p = malloc(4); - p = realloc(p, 8); - if (!p) - return; - free(p); -} - -void grow_in_loop(char *p, int n) { - for (int i = 0; i < n; ++i) { - char *q = realloc(p, 8); - if (!q) { - free(p); - return; - } - p = q; - } - free(p); -} - -int no_null_test(char *p) { - char *q = realloc(p, 16); - free(p); // BUG: use-after-move - use(q); - return 0; // BUG: leak -} - -void result_overwritten(char *p) { - char *q = realloc(p, 8); - q = malloc(2); // BUG: leak - if (q == NULL) // BUG: leak - free(p); // BUG: use-after-move -} diff --git a/test/cases/engine/Analysis-rfc0003-escapes.c b/test/cases/engine/Analysis-rfc0003-escapes.c deleted file mode 100644 index e48d99c9..00000000 --- a/test/cases/engine/Analysis-rfc0003-escapes.c +++ /dev/null @@ -1,63 +0,0 @@ -// Engine pin converted from test/Analysis/rfc0003-escapes.c; markers are the v0.10.0 golden diagnostics. -// RFC 0003: summaries record where pointer values flow (stores into -// caller-visible memory, returned copies and borrows), so callers see escapes -// and aliases created inside callees. -#include "Inputs/prelude.h" - -struct node { - int v; - struct node *next; -}; - -static char *g; -static void keep(char *p) { g = p; } -static void store_in(char **slot, char *p) { *slot = p; } -static struct node *next_of(struct node *n) { return n->next; } -static int *field_of(struct node *n) { return &n->v; } -static char *through(char *p) { return p; } - -void escape_local(void) { - char local[8]; - keep(local); // BUG: lifetime-too-short -} - -void escape_through_out_param(char **slot) { - int x; - store_in(slot, (char *)&x); // BUG: lifetime-too-short -} - -void escape_fine(char *outer) { - keep(outer); - keep(malloc(8)); -} - -void result_copies_field(struct node *n) { - struct node *m = next_of(n); - free(n->next); - use(m); // BUG: use-after-free -} - -void result_copies_argument(void) { - char *p = malloc(8); - char *q = through(p); - free(q); - use(p); // BUG: use-after-free -} - -void result_borrows_field(void) { - int *v; - { - struct node n; - v = field_of(&n); // BUG: lifetime-too-short - } - use(v); -} - -// Landed with RFC 0003: a plain copy of a pointer holding a loan is checked -// like a fresh borrow (previously only `&x` was). -static int *global_int; -void copied_loan(void) { - int x = 0; - int *p = &x; - global_int = p; // BUG: lifetime-too-short -} diff --git a/test/cases/engine/Analysis-rfc0003-globals.c b/test/cases/engine/Analysis-rfc0003-globals.c deleted file mode 100644 index 28c6bbf7..00000000 --- a/test/cases/engine/Analysis-rfc0003-globals.c +++ /dev/null @@ -1,31 +0,0 @@ -// Engine pin converted from test/Analysis/rfc0003-globals.c; markers are the v0.10.0 golden diagnostics. -// RFC 0003: effects on file-scope variables are summarised like effects on -// parameters, so a helper that frees a global marks it freed in its callers. -#include "Inputs/prelude.h" - -static char *cache; - -static void cache_free(void) { free(cache); } -static void cache_reset(void) { - free(cache); - cache = NULL; -} - -void freed_by_callee(void) { - cache = malloc(8); - cache_free(); - cache[0] = 1; // BUG: use-after-free -} - -void reset_is_fine(void) { - cache = malloc(8); - cache_reset(); - cache = malloc(8); - free(cache); -} - -void double_free_by_callee(void) { - cache = malloc(8); - free(cache); - cache_free(); // BUG: double-free -} diff --git a/test/cases/engine/Analysis-rfc0003-libc.c b/test/cases/engine/Analysis-rfc0003-libc.c deleted file mode 100644 index b0ebe8e4..00000000 --- a/test/cases/engine/Analysis-rfc0003-libc.c +++ /dev/null @@ -1,53 +0,0 @@ -// Engine pin converted from test/Analysis/rfc0003-libc.c; markers are the v0.10.0 golden diagnostics. -// RFC 0003: the shipped libc summary table describes the common allocator, -// releaser and aliasing functions, so real headers need no annotations. -#include -#include -#include - -char *alias_from_strchr(void) { - char *s = strdup("a=b"); - if (!s) - return NULL; - char *eq = strchr(s, '='); - free(s); - return eq; // BUG: use-after-free -} - -void end_pointer_from_strtol(const char *text) { - char *copy = strdup(text); - if (!copy) - return; - char *end; - long v = strtol(copy, &end, 10); - free(copy); - if (*end) // BUG: use-after-free - (void)v; -} - -void double_close(const char *path) { - FILE *f = fopen(path, "r"); - if (!f) - return; - fclose(f); - fclose(f); // BUG: double-free -} - -void fine(const char *path) { - char *s = strdup(path); - if (!s) - return; - size_t n = strlen(s); - char *copy = malloc(n + 1); - if (copy) { - memcpy(copy, s, n + 1); - puts(copy); - free(copy); - } - FILE *f = fopen(s, "r"); - free(s); - if (f) { - fputs("x", f); - fclose(f); - } -} diff --git a/test/cases/engine/Analysis-rfc0003-paths.c b/test/cases/engine/Analysis-rfc0003-paths.c deleted file mode 100644 index 278fb368..00000000 --- a/test/cases/engine/Analysis-rfc0003-paths.c +++ /dev/null @@ -1,73 +0,0 @@ -// Engine pin converted from test/Analysis/rfc0003-paths.c; markers are the v0.10.0 golden diagnostics. -// RFC 0003: effects on paths reachable from parameters (`b->data`, `*out`) -// are part of the summary. The caller's memory is described by the callee's -// exit state, so a destroy-and-null helper leaves the field usable. -#include "Inputs/prelude.h" - -struct buf { - char *data; - size_t len; -}; - -static void buf_init(struct buf *b, size_t n) { - b->data = malloc(n); - b->len = n; -} -static void buf_destroy(struct buf *b) { free(b->data); } -static void buf_reset(struct buf *b) { - free(b->data); - b->data = NULL; -} - -void field_by_pointer(struct buf *b) { - buf_destroy(b); - b->data[0] = 1; // BUG: use-after-free -} - -void field_by_address(void) { - struct buf b; - buf_init(&b, 8); - buf_destroy(&b); - b.data[0] = 1; // BUG: use-after-free -} - -void destroy_idiom_is_fine(void) { - struct buf b; - buf_init(&b, 8); - buf_reset(&b); - buf_init(&b, 4); - buf_reset(&b); -} - -static int make(char **out) { - *out = malloc(8); - return *out != NULL; -} - -void out_param(void) { - char *s; - if (!make(&s)) - return; - free(s); - s[0] = 1; // BUG: use-after-free -} - -void out_param_fine(void) { - char *s; - if (!make(&s)) - return; - s[0] = 1; - free(s); -} - -// Re-pointing the parameter inside the callee does not hide what it freed. -static void free_and_null(char *p) { - free(p); - p = NULL; -} - -void reassigned_parameter(void) { - char *p = malloc(8); - free_and_null(p); - p[0] = 1; // BUG: use-after-free -} diff --git a/test/cases/engine/Analysis-rfc0003-wrappers.c b/test/cases/engine/Analysis-rfc0003-wrappers.c deleted file mode 100644 index 2b5de9e4..00000000 --- a/test/cases/engine/Analysis-rfc0003-wrappers.c +++ /dev/null @@ -1,93 +0,0 @@ -// Engine pin converted from test/Analysis/rfc0003-wrappers.c; markers are the v0.10.0 golden diagnostics. -// RFC 0003: a function that frees (or moves) its parameter is summarised as -// consuming it, and callers are checked against that summary without any -// annotation, through arbitrarily deep wrappers and recursion. -#include "Inputs/prelude.h" - -struct node { - int v; - struct node *next; -}; - -static struct node *node_new(void) { return malloc(sizeof(struct node)); } -static void node_free(struct node *n) { free(n); } -static void node_free2(struct node *n) { node_free(n); } -static void node_free3(struct node *n) { - if (n) - node_free2(n); -} - -static void list_free(struct node *n) { - if (!n) - return; - list_free(n->next); - free(n); -} - -static void even_free(struct node *n); -static void odd_free(struct node *n) { - if (n) - even_free(n->next); - free(n); -} -static void even_free(struct node *n) { - if (n) - odd_free(n->next); - free(n); -} - -int use_after_wrapper(void) { - struct node *n = node_new(); - node_free(n); - return n->v; // BUG: use-after-free -} - -void double_free_through_wrapper(void) { - struct node *n = node_new(); - node_free3(n); - free(n); // BUG: double-free -} - -void recursion(struct node *a, struct node *b) { - list_free(a); - use(a); // BUG: use-after-free - odd_free(b); - use(b); // BUG: use-after-free -} - -// Unresolvable arguments are dropped, and conditional frees are may-frees. -static int free_if(struct node *n, int c) { - if (c) { - free(n); - return 1; - } - return 0; -} - -void fine(struct node *(*make)(void), void (*drop)(struct node *)) { - node_free(NULL); - node_free(node_new()); - // RFC 0030 §9.3: an indirect call through a slot with no known target is - // the §5.1 default under the reason `callback`. - struct node *n = make(); - drop(n); - use(n); -} - -void maybe(int c) { - struct node *n = node_new(); - free_if(n, c); - n->v = 1; // BUG: use-after-free -} - -// Testing the result that tells the paths apart retracts the may-free on -// the path that did not free (RFC 0006, *Outcome-conditional summaries*). -void tested(int c) { - struct node *n = node_new(); - if (!n) - return; - if (free_if(n, c)) - return; - n->v = 1; - free(n); -} diff --git a/test/cases/engine/Analysis-rfc0004-laundering.c b/test/cases/engine/Analysis-rfc0004-laundering.c deleted file mode 100644 index a71c0c8b..00000000 --- a/test/cases/engine/Analysis-rfc0004-laundering.c +++ /dev/null @@ -1,73 +0,0 @@ -// Engine pin converted from test/Analysis/rfc0004-laundering.c; markers are the v0.10.0 golden diagnostics. -// RFC 0004, "Laundering": a raw pointer is brought into the model by -// asserting its ownership inside an unsafe region, either by storing it into -// a place declared with a safe kind or by returning it from a function whose -// return type is annotated. The same assertion outside an unsafe region is -// an unsafe-operation. A pointer leaves the model by conversion to an integer -// or by being stored into a WEAVEC_RAW place. RFC 0033 §2: a pointer converted -// from an integer is no longer raw, so the raw sources here are declared. -#include "Inputs/prelude.h" -#include - -typedef unsigned long uintptr_t; -struct node { - int v; -}; - -// Assertion by return. -WEAVEC_OWNED struct node *by_return_outside(struct node *WEAVEC_RAW x) { - return x; // BUG: unsafe-operation -} -WEAVEC_OWNED struct node *by_return_inside(uintptr_t x) { - WEAVEC_UNSAFE { return (struct node *)x; } -} -void uses_by_return(uintptr_t x) { - struct node *n = by_return_inside(x); - n->v = 1; /* owned, per the callee's annotation */ - free(n); - use(n); // BUG: use-after-free -} - -// Assertion by assignment to an annotated local. -void by_local_outside(struct node *WEAVEC_RAW x) { - struct node *raw = x; - WEAVEC_OWNED struct node *n = raw; // BUG: unsafe-operation - n->v = 1; /* asserted anyway, so this does not cascade */ - free(n); -} -void by_local_inside(struct node *WEAVEC_RAW x) { - WEAVEC_OWNED struct node *n; - WEAVEC_UNSAFE { n = x; } - n->v = 1; - free(n); - free(n); // BUG: double-free -} - -// Assertion by assignment to an annotated field. -struct box { - struct node *WEAVEC_OWNED owned; -}; -void by_field(struct box *b, struct node *WEAVEC_RAW x) { - b->owned = x; // BUG: unsafe-operation - WEAVEC_UNSAFE { b->owned = x; } - free(b->owned); - use(b->owned); // BUG: use-after-free -} - -// Leaving the model. -uintptr_t out_by_integer(struct node *WEAVEC_OWNED n) { - return (uintptr_t)n; /* silent: the value is gone; nothing to check */ -} -struct ctx { - void *WEAVEC_RAW cookie; -}; -void out_by_raw_place(struct ctx *c) { - struct node *n = malloc(sizeof *n); - c->cookie = n; /* a copy into a raw place: n is still owned here */ - free(n); -} -void back_from_raw_place(struct ctx *c) { - WEAVEC_OWNED struct node *n; - WEAVEC_UNSAFE { n = c->cookie; } - free(n); -} diff --git a/test/cases/engine/Analysis-rfc0006-elements.c b/test/cases/engine/Analysis-rfc0006-elements.c deleted file mode 100644 index ad38c841..00000000 --- a/test/cases/engine/Analysis-rfc0006-elements.c +++ /dev/null @@ -1,89 +0,0 @@ -// Engine pin converted from test/Analysis/rfc0006-elements.c; markers are the v0.10.0 golden diagnostics. -// RFC 0006 regression cases, amended by RFC 0015: selected cells retain -// their history across independent updates, scalar writes and joins. -// Distinct unresolved scalar indices are not proof of disjointness. -// RFC 0030 (*Diagnostics*, §15 item 3): `analysis-incomplete` is removed; each -// such pin now has the ledger row that replaces it (`UNRESOLVED`), and is -// listed in the retired golden comparison of RFC 0030. -#include "Inputs/prelude.h" - -// Reported: same witness. -void same_constant(void) { - int *arr[4]; - arr[0] = malloc(4); - arr[1] = malloc(4); - free(arr[0]); - use(arr[1]); free(arr[1]); // another element: fine - use(arr[0]); // BUG: use-after-free -} - -void same_variable(char **a, int i, int j) { - free(a[i]); - use(a[j]); // may select the released cell // BUG: use-after-free - a[i][0] = 0; // BUG: use-after-free -} - -void double_free_element(char **a) { - free(a[0]); - free(a[0]); // BUG: double-free -} - -void whole_access_matches(char **a, int i) { - free(a[i]); - use(*a); // BUG: use-after-free -} - -void first_element(void) { - int *arr[2]; - arr[0] = malloc(4); - free(*arr); // `*arr` on an array is `arr[0]` - use(arr[0]); // BUG: use-after-free -} - -void joined_on_both_sides(char **a, int i, int c) { - if (c) - free(a[i]); - else - free(a[i]); - use(a[i]); // BUG: use-after-free -} - -// A complete cleanup loop, and preservation of old index values. -void loop_free(char **a, int n) { - for (int i = 0; i < n; i++) - free(a[i]); - free(a); -} - -void null_out(char **a, int n) { - for (int i = 0; i < n; i++) { - free(a[i]); - a[i] = NULL; - } - use(a[0]); -} - -void incremented(char **a, int i) { - free(a[i]); - i++; - use(a[i]); -} - -void reassigned(char **a, int i, int j) { - free(a[i]); - i = j; - use(a[i]); // BUG: use-after-free -} - -void unrecognised_index(char **a, int i) { - free(a[i + 1]); - use(a[i + 1]); // BUG: use-after-free -} - -void joined_with_different_witnesses(char **a, int i, int j, int c) { - if (c) - free(a[i]); - else - free(a[j]); - use(a[i]); // BUG: use-after-free -} diff --git a/test/cases/engine/Analysis-rfc0006-nll.c b/test/cases/engine/Analysis-rfc0006-nll.c deleted file mode 100644 index cb317ead..00000000 --- a/test/cases/engine/Analysis-rfc0006-nll.c +++ /dev/null @@ -1,83 +0,0 @@ -// Engine pin converted from test/Analysis/rfc0006-nll.c; markers are the v0.10.0 golden diagnostics. -// RFC 0006, *Loans end at the last use of their holder*: a loan lives while -// its holder is live, not while it is in scope. Loans held through a -// pointer, by a global, or by an address-taken local last until the holder -// is reassigned. -#include "Inputs/prelude.h" -#include - -struct node { - int v; -}; - -struct holder { - int *view; -}; - -// Clean: the holder is dead when the object goes away. -void last_use(void) { - char buf[8]; - char *p = buf; - use(p); - buf[0] = 0; -} - -void free_after_last_use(struct node *WEAVEC_OWNED n) { - int *a = &n->v; - *a = 1; - free(n); -} - -void loop_then_free(struct node *WEAVEC_OWNED n, int k) { - int *a = &n->v; - for (int i = 0; i < k; i++) - *a += i; - free(n); -} - -// Reported: the holder is used after the object is freed. `&n->v` is a -// derived copy of `n` (RFC 0011), so the use through it is the report. -void still_live(struct node *WEAVEC_OWNED n) { - int *a = &n->v; - free(n); - *a = 1; // BUG: use-after-free -} - -void live_around_loop(struct node *WEAVEC_OWNED n, int k) { - int *a = &n->v; - for (int i = 0; i < k; i++) { - if (i == 5) { - free(n); - break; - } - } - *a = 1; // BUG: use-after-free -} - -// A holder that is not a plain local never expires on liveness, and the -// loan a derived copy carries (RFC 0011, *Derived pointers*) is what -// reports the free: nothing in this function reads the copy again. -void through_pointer(struct node *WEAVEC_OWNED n, int **out) { - *out = &n->v; - free(n); // BUG: conflicting-borrow -} - -void through_field(struct node *WEAVEC_OWNED n, struct holder *h) { - h->view = &n->v; - free(n); // BUG: conflicting-borrow -} - -void address_taken(struct node *WEAVEC_OWNED n) { - int *a = &n->v; - int **pa = &a; - free(n); // BUG: conflicting-borrow - use(pa); -} - -// The holder is live up to the statement that reads it: returning it or -// copying it out of a dying frame is still caught. -int *escape(void) { - int x = 1; - int *p = &x; - return p; // BUG: lifetime-too-short -} diff --git a/test/cases/engine/Analysis-rfc0007-leaks.c b/test/cases/engine/Analysis-rfc0007-leaks.c deleted file mode 100644 index 01fb6970..00000000 --- a/test/cases/engine/Analysis-rfc0007-leaks.c +++ /dev/null @@ -1,79 +0,0 @@ -// Engine pin converted from test/Analysis/rfc0007-leaks.c; markers are the v0.10.0 golden diagnostics. -// RFC 0007, *Leaks*: an owned resource whose every holder goes out of reach -// without being released, moved or escaped is reported once, at the point -// it is lost (a return, a scope end, an overwrite, a discarded call). -#include "Inputs/prelude.h" -#include - -char *strdup(const char *s); - -struct buf { - char *data; - size_t n; -}; - -// The RFC's snippets that must be reported. - -int leak_path(int c) { - char *p = malloc(8); - if (c) - return -1; // BUG: leak - free(p); - return 0; -} - -void overwrite(void) { - char *p = malloc(8); - p = malloc(16); // BUG: leak - free(p); -} - -void owned_param(char *WEAVEC_OWNED p) { - use(p); // BUG: leak -} - -void discarded(const char *s) { - strdup(s); // BUG: leak -} - -// A value that is never read is lost right after it is stored. -int never_used(int c) { - char *p = malloc(8); - if (c) // BUG: leak - return 1; - return 2; -} - -// The record travels with copies: the leak is reported for the holder that -// dies last, and there is no second report for `p`. -void copies(void) { - char *p = malloc(8); - char *q = p; - use(q); // BUG: leak -} - -// Overwriting an owned global loses the old value for this function. -static char *global; -void global_overwrite(void) { - global = malloc(8); - global = malloc(16); // BUG: leak -} - -// A field the function itself made owned is checked on overwrite. -void field_overwrite(struct buf *b) { - b->data = malloc(8); - b->data = malloc(16); // BUG: leak -} - -// Once a merge-point false positive: `p` may own at the second `if`, but the -// resource is held under the guard `c != 0`, which the early return's edge -// refutes (RFC 0009, *Refuting guards in the state*). -int merged(int c) { - char *p = NULL; - if (c) - p = malloc(8); - if (!c) - return -1; - free(p); - return 0; -} diff --git a/test/cases/engine/Analysis-rfc0007-owned-fields.c b/test/cases/engine/Analysis-rfc0007-owned-fields.c deleted file mode 100644 index a3d21af5..00000000 --- a/test/cases/engine/Analysis-rfc0007-owned-fields.c +++ /dev/null @@ -1,99 +0,0 @@ -// Engine pin converted from test/Analysis/rfc0007-owned-fields.c; markers are the v0.10.0 golden diagnostics. -// RFC 0007, *Owned fields*: a field declared `WEAVEC_OWNED`, or one this -// function stored an owned value into, is lost when its container is freed -// without it; a caller's alias of a field the callee frees with the object -// is a use after free (the deepest-first ordering fix). -#include "Inputs/prelude.h" -#include - -char *strdup(const char *s); - -struct box { - char *WEAVEC_OWNED p; -}; - -struct buf { - char *data; - size_t n; -}; - -// The RFC's snippets that must be reported. - -void declared(struct box *b) { - free(b); // BUG: leak -} - -void inferred(struct buf *b) { - b->data = malloc(8); - free(b); // BUG: leak -} - -void elements(char **a, int n) { - a[0] = strdup("x"); - free(a); // BUG: leak -} - -// Newly caught after the ordering fix: the caller's alias of a field the -// callee frees together with the object. -static void both(struct box *b) { - free(b->p); - free(b); -} - -void alias_of_field(struct box *b) { - char *q = b->p; - both(b); - q[0] = 1; // BUG: use-after-free -} - -static void both_reversed(struct box *b) { - char *t = b->p; - free(b); - free(t); -} - -void alias_of_field_reversed(struct box *b) { - char *q = b->p; - both_reversed(b); - q[0] = 1; // BUG: use-after-free -} - -// Clean (RFC 0007, *Deliberately not caught* and *Diagnostics*). - -// Fields of a fresh object own nothing until this function stores into them. -void fresh_error_path(void) { - struct box *b = malloc(sizeof *b); - if (!b) - return; - free(b); -} - -static struct box *box_new(void) { return malloc(sizeof(struct box)); } -void fresh_from_callee(void) { - struct box *b = box_new(); - if (!b) - return; - free(b); -} - -void nulled(struct box *b) { - free(b->p); - b->p = NULL; - free(b); -} - -void maybe(struct box *b) { - if (b->p) - free(b->p); - free(b); -} - -void moved_out(struct box *b, char **out) { - *out = b->p; - free(b); -} - -// A constructor stores into a field it knows nothing about: no report. -void init(struct box *b, char *WEAVEC_OWNED p) { - b->p = p; -} diff --git a/test/cases/engine/Analysis-rfc0008-release.c b/test/cases/engine/Analysis-rfc0008-release.c deleted file mode 100644 index 0a191e27..00000000 --- a/test/cases/engine/Analysis-rfc0008-release.c +++ /dev/null @@ -1,72 +0,0 @@ -// Engine pin converted from test/Analysis/rfc0008-release.c; markers are the v0.10.0 golden diagnostics. -// RFC 0008, *Invalid releases*: a releaser is handed a pointer to a stack or -// static object, to a string literal, or into the middle of an allocation. -#include -#include -#include - -// The RFC's snippets that must be reported. - -static char table[16]; - -void stack(void) { - char buf[8]; - char *p = buf; - free(p); // BUG: invalid-release -} - -void object(void) { - int x; - int *p = &x; - free(p); // BUG: invalid-release -} - -void global(void) { - free(table); // BUG: invalid-release -} - -void literal(void) { - char *s = "hello"; - free(s); // BUG: invalid-release -} - -void interior(void) { - char *p = malloc(8); - if (!p) - return; - char *q = p + 1; - free(q); // BUG: invalid-release -} - -void searched(const char *s) { - char *p = strdup(s); - if (!p) - return; - char *q = strchr(p, 'x'); - if (!q) { - free(p); - return; - } - free(q); // BUG: invalid-release -} - -void arithmetic(void) { - char *p = malloc(8); - if (!p) - return; - free(p + 1); // BUG: invalid-release -} - -// Clean: heap objects released at their start, `p + 0`, and a pointer that -// walks forward and back again is the checker's business elsewhere. -void fine(void) { - char *p = malloc(8); - if (!p) - return; - char *q = p; - free(q + 0); - char *s = strdup("x"); - free(s); -} - -// Clang itself warns about `free(table)`; WeaveC reports it too, once. diff --git a/test/cases/engine/Analysis-rfc0011-lifetimes.c b/test/cases/engine/Analysis-rfc0011-lifetimes.c deleted file mode 100644 index d5ebf5db..00000000 --- a/test/cases/engine/Analysis-rfc0011-lifetimes.c +++ /dev/null @@ -1,64 +0,0 @@ -// Engine pin converted from test/Analysis/rfc0011-lifetimes.c; markers are the v0.10.0 golden diagnostics. -// RFC 0011, *Deferred lifetime checks*: storing the address of a local in -// longer-lived memory is reported when the local dies still pointed at, not -// at the store. The linked-frame idiom (push on entry, pop before return) is -// clean; forgetting the pop is `lifetime-too-short` at the closing brace. -#include "Inputs/prelude.h" -#include - -struct frame { struct frame *prev; int depth; }; -struct state { struct frame *fs; int *p; }; -int cond(void); - -// Clean: the store is undone before `f` dies. -void enter(struct state *st) { - struct frame f; - f.prev = st->fs; - st->fs = &f; - st->fs = f.prev; -} - -// Clean: reset to null. -void enter_reset(struct state *st) { - struct frame f; - st->fs = &f; - st->fs = NULL; -} - -// The pop is missing: reported where `f` goes out of scope. -void enter_bad(struct state *st) { - struct frame f; - f.prev = st->fs; - st->fs = &f; // BUG: lifetime-too-short -} - -// A store on one path only is still a store when the local dies. -void one_path(struct state *st) { - int local = 1; - if (cond()) - st->p = &local; // BUG: lifetime-too-short -} - -// Overwriting the escaped pointer with another local's address moves the -// report to the store that is live at the death. -int *g; -void to_global(void) { - int local = 1; - g = &local; - g = NULL; - g = &local; // BUG: lifetime-too-short -} - -// Clean: the pointer to the local dies with it. -void same_scope(void) { - int local = 1; - int *p = &local; - use(p); -} - -// Returning the address of a local is still reported at the return -// (RFC 0002): the deferral is for stores, which can be undone. -int *escape(void) { - int local = 1; - return &local; // BUG: lifetime-too-short -} diff --git a/test/cases/engine/Analysis-rfc0012-sized-fields.c b/test/cases/engine/Analysis-rfc0012-sized-fields.c deleted file mode 100644 index 03e10044..00000000 --- a/test/cases/engine/Analysis-rfc0012-sized-fields.c +++ /dev/null @@ -1,121 +0,0 @@ -// Engine pin converted from test/Analysis/rfc0012-sized-fields.c; markers are the v0.10.0 golden diagnostics. -// RFC 0012, *Sized fields*: a pointer field counted by a sibling integer -// field, declared with `WEAVEC_SIZED_BY` or inferred from the stores of the -// unit; loads get the count's extent, stores into an annotated field are -// checked against the count, a malformed annotation is reported once. -#include "Inputs/prelude.h" -#include - -char *strcpy(char *dst, const char *src); - -struct buf { - char *WEAVEC_SIZED_BY(cap) data; - size_t cap; -}; - -// -- Loads -------------------------------------------------------------------- - -void put(struct buf *b) { - b->data[b->cap] = 0; // BUG: out-of-bounds // MISS: the analysis leaves an index at or past a WEAVEC_SIZED_BY count unresolved (the count becomes a contract in RFC 0036); the guard stops at run time - if (b->cap > 0) - b->data[b->cap - 1] = 0; -} - -void fill(struct buf *b) { - for (size_t i = 0; i < b->cap; i++) - b->data[i] = 0; - for (size_t i = 0; i <= b->cap; i++) - b->data[i] = 0; // BUG: out-of-bounds // MISS: as above -} - -// The string checks see the count too. -void copy(struct buf *b) { - if (b->cap == 4) - strcpy(b->data, "hello"); // BUG: out-of-bounds // MISS: as above -} - -// -- Stores ------------------------------------------------------------------- - -void shrink(struct buf *b) { - b->data = malloc(4); - b->cap = 8; // BUG: annotation-mismatch -} - -void shrink_count_first(struct buf *b) { - b->cap = 8; - b->data = malloc(4); // BUG: annotation-mismatch -} - -// The right size, a larger object, an unknown one, or null: nothing. -void right_size(struct buf *b, size_t n) { - b->data = malloc(n); - b->cap = n; -} -void larger(struct buf *b) { - b->data = malloc(16); - b->cap = 8; -} -void unknown_size(struct buf *b, char *p) { - b->data = p; - b->cap = 8; -} -void cleared(struct buf *b) { - b->data = NULL; - b->cap = 8; -} - -// -- Malformed annotations ---------------------------------------------------- - -struct bad { - int *WEAVEC_SIZED_BY(nope) p; // BUG: invalid-annotation - int q; -}; - -int bad_one(struct bad *b) { return b->p[0]; } -int bad_two(struct bad *b) { return b->p[1]; } - -// The flexible array member of RFC 0011's model is unchanged: the extent is -// the allocation's. -struct hack { - size_t len; - char data[]; -}; - -void flexible(size_t n) { - // RFC 0017: both n - 1 and the allocation's header addition must fit. - if (n == 0 || n > (size_t)-1 - sizeof(struct hack)) - return; - struct hack *h = malloc(sizeof *h + n); - if (!h) - return; - h->data[n - 1] = 0; - h->data[n] = 0; // BUG: out-of-bounds - free(h); -} - -// -- Inference ---------------------------------------------------------------- - -struct vec { - int *items; - size_t n; - size_t cap; -}; - -// `init` witnesses `(items, cap, 4)`; `push` writes `n` without storing -// `items`, refuting `(items, n)`; nothing refutes `(items, cap)`. The reader -// is analysed once more with the count in force (RFC 0012, *Two passes in a -// unit*): `last` is reported, `last_n` is not. -void init(struct vec *v, size_t n) { - v->items = malloc(n * sizeof *v->items); - v->cap = n; - v->n = 0; -} - -void push(struct vec *v, int x) { - if (v->n < v->cap) - v->items[v->n++] = x; -} - -int last(struct vec *v) { return v->items[v->cap]; } // BUG: out-of-bounds - -int last_n(struct vec *v) { return v->items[v->n]; } diff --git a/test/cases/engine/Analysis-rfc0013-clean.c b/test/cases/engine/Analysis-rfc0013-clean.c deleted file mode 100644 index e467a184..00000000 --- a/test/cases/engine/Analysis-rfc0013-clean.c +++ /dev/null @@ -1,29 +0,0 @@ -// Engine pin converted from test/Analysis/rfc0013-clean.c; markers are the v0.10.0 golden diagnostics. -// CLEAN -// RFC 0013: clean counterparts for nullable, shared and cyclic construction. -#include "Inputs/heap.c" - -struct pair { char *a, *b; struct pair *self; }; -struct pair *pair_new(void) { - struct pair *p = malloc(sizeof *p); if (!p) return NULL; - p->a = malloc(4); if (!p->a) { free(p); return NULL; } - p->b = p->a; p->self = p; return p; -} -void good(void) { - struct pair *a = pair_new(), *b = pair_new(); - if (a) { a->b[3] = 0; free(a->a); free(a); } - if (b) { b->a[3] = 0; free(b->b); free(b); } -} -void clean_borrow(void) { - char local[4]; struct box *b = box_wrap(local); - if (!b) return; - b->data[3] = 0; free(b); -} -void snapshots(size_t n, int count) { - for (int k = 0; k < count; ++k) { - size_t size = n; char *p = malloc(n); - n = k + 1; - if (p) for (size_t i = 0; i < size; ++i) p[i] = 0; - free(p); - } -} diff --git a/test/cases/engine/Analysis-rfc0014-clean.c b/test/cases/engine/Analysis-rfc0014-clean.c deleted file mode 100644 index 8633cd2d..00000000 --- a/test/cases/engine/Analysis-rfc0014-clean.c +++ /dev/null @@ -1,49 +0,0 @@ -// Engine pin converted from test/Analysis/rfc0014-clean.c; markers are the v0.10.0 golden diagnostics. -// CLEAN -// RFC 0014: these complete, supported operations retain precise identities. -#include -#include - -static void drop(void *p) { free(p); } -static void keep(void *p) { (void)p; } -void (*unrelated)(void *) = drop; -static void invoke(void (*fn)(void *), void *data) { fn(data); } -void callback_clean(int *p) { - void (*fn)(void *) = keep; - invoke(fn, p); - *p = 1; - free(p); -} - -static void release_same(int *p, int *q) { - if (p == q) - free(p); -} -void guarded(int *p, int *q) { - int *saved = p; - if (p != q) { - release_same(saved, q); - *saved = 1; - free(saved); - } -} - -void pointer_copy(void) { - int *p = malloc(sizeof *p), *q; - if (!p) - return; - memcpy(&q, &p, sizeof p); - *q = 1; - free(q); -} - -struct inner { int *p; }; -struct outer { int tag; struct inner in; }; -static void initialize(void *object) { - struct inner *p = object; - *p->p = 1; -} -void embedded(struct outer *p) { - void *object = &p->in; - initialize(object); -} diff --git a/test/cases/engine/Analysis-rfc0014-pointer-identity.c b/test/cases/engine/Analysis-rfc0014-pointer-identity.c deleted file mode 100644 index a9782480..00000000 --- a/test/cases/engine/Analysis-rfc0014-pointer-identity.c +++ /dev/null @@ -1,47 +0,0 @@ -// Engine pin converted from test/Analysis/rfc0014-pointer-identity.c; markers are the v0.10.0 golden diagnostics. -// RFC 0014: actual callbacks, pointer predicates, complete memory copies. -// RFC 0030 (*Diagnostics*, §15 item 3): `analysis-incomplete` is removed; each -// such pin now has the ledger row that replaces it (`UNRESOLVED`), and is -// listed in the retired golden comparison of RFC 0030. -#include -#include - -static void keep(void *p) { (void)p; } -static void drop(void *p) { free(p); } -static void invoke(void (*fn)(void *), void *p) { fn(p); } -void (*unrelated)(void *) = keep; - -void callback_bad(int *p) { - invoke(drop, p); - *p = 1; // BUG: use-after-free -} - -static void release_same(int *p, int *q) { - if (p == q) - free(p); -} -void equality_bad(int *p) { - release_same(p, p); - free(p); // BUG: double-free -} - -void copied_pointer_bad(int *p) { - int *q; - memcpy(&q, &p, sizeof p); - free(p); - *q = 1; // BUG: use-after-free -} - -void partial(int **dest, int **source) { - memcpy(dest, source, 1); -} - -struct first { int *p; }; -struct second { int tag; int *p; }; -static void release_field(void *object) { - struct first *p = object; - free(p->p); -} -void incompatible(struct second *p) { - release_field(p); -} diff --git a/test/cases/engine/Analysis-rfc0015-elements.c b/test/cases/engine/Analysis-rfc0015-elements.c deleted file mode 100644 index d284c34b..00000000 --- a/test/cases/engine/Analysis-rfc0015-elements.c +++ /dev/null @@ -1,31 +0,0 @@ -// Engine pin converted from test/Analysis/rfc0015-elements.c; markers are the v0.10.0 golden diagnostics. -// RFC 0015: selected cells retain state across unrelated updates. -#include "Inputs/prelude.h" -void *memcpy(void *, const void *, size_t); -void *memmove(void *, const void *, size_t); - -void history(char **a) { - free(a[0]); free(a[1]); - a[0][0]=1; // BUG: use-after-free - free(a[0]); // BUG: double-free -} -void saved(char **a, int i) { - int old=i; free(a[i]); i=7; - a[old][0]=1; // BUG: use-after-free -} -void initialization(char *p) { - char *a[2]; a[0]=p; - a[1][0]=1; // BUG: use-of-uninitialized -} -void omitted(char *p) { - char *a[2]={p}; - a[1][0]=1; // BUG: null-dereference -} -void copied(char **a) { - char *b[2]; memcpy(b,a,sizeof b); free(a[0]); - b[0][0]=1; // BUG: use-after-free -} -void clean(char **a, char *p) { - free(a[0]); a[1][0]=1; a[0]=p; a[0][0]=1; - char *b[2]={p,p}; memmove(b,b,0); b[1][0]=1; -} diff --git a/test/cases/engine/Analysis-rfc0015-ranges.c b/test/cases/engine/Analysis-rfc0015-ranges.c deleted file mode 100644 index cb64cb4c..00000000 --- a/test/cases/engine/Analysis-rfc0015-ranges.c +++ /dev/null @@ -1,24 +0,0 @@ -// Engine pin converted from test/Analysis/rfc0015-ranges.c; markers are the v0.10.0 golden diagnostics. -// RFC 0015: symbolic snapshots, overlap, complete initialization and cleanup. -#include "Inputs/prelude.h" -void *memcpy(void *, const void *, size_t); -void *memmove(void *, const void *, size_t); - -void changed_count(char **a, char **b, size_t n) { - if (n<3) return; - memcpy(b,a,n*sizeof *a); n=0; free(a[2]); - b[2][0]=1; // BUG: use-after-free -} -void overlap_left(char **a) { - char *old=a[1]; memmove(a,a+1,2*sizeof *a); free(old); - a[0][0]=1; // BUG: use-after-free -} -void overlap_right(char **a) { - char *old=a[1]; memmove(a+1,a,2*sizeof *a); free(old); - a[2][0]=1; // BUG: use-after-free -} -void clean(void) { - char *a[4]; for (int i=0;i<4;++i) a[i]=malloc(4); - for (int i=0;i<4;++i) { free(a[i]); a[i]=NULL; } - free(a[0]); free(a[3]); -} diff --git a/test/cases/engine/Analysis-rfc0016-boundaries.c b/test/cases/engine/Analysis-rfc0016-boundaries.c deleted file mode 100644 index 745bed27..00000000 --- a/test/cases/engine/Analysis-rfc0016-boundaries.c +++ /dev/null @@ -1,40 +0,0 @@ -// Engine pin converted from test/Analysis/rfc0016-boundaries.c; markers are the v0.10.0 golden diagnostics. -// RFC 0016: every failed projection retains an explicit coverage reason. -// RFC 0030 (*Diagnostics*, §15 item 3): `analysis-incomplete` is removed. The -// limits these pins hit were the old engine's; RFC 0031's alias contexts (§6.6) -// run every call below, find no bug (each callee reads before it frees, and a -// fresh allocation is distinct from `q`), and prove the calls. The rows are -// listed in the retired golden comparison of RFC 0030. -// CLEAN -#include "Inputs/prelude.h" - -#define PARAMS_13 char *a, char *b, char *c, char *d, char *e, char *f, char *g, char *h, char *i, char *j, char *k, char *l, char *m -#define READ_13 *a=1; *b=1; *c=1; *d=1; *e=1; *f=1; *g=1; *h=1; *i=1; *j=1; *k=1; *l=1; *m=1 -static void many_relations(PARAMS_13) { READ_13; free(a); } -void relation_limit(void) { - char *p = malloc(4); if (!p) return; - many_relations(p,p,p,p,p,p,p,p,p,p,p,p,p); -} - -#define PARAMS_33 PARAMS_13, char *n, char *o, char *p, char *q, char *r, char *s, char *t, char *u, char *v, char *w, char *x, char *y, char *z, char *aa, char *ab, char *ac, char *ad, char *ae, char *af, char *ag -static void many_inputs(PARAMS_33) { - READ_13; *n=1; *o=1; *p=1; *q=1; *r=1; *s=1; *t=1; *u=1; *v=1; *w=1; - *x=1; *y=1; *z=1; *aa=1; *ab=1; *ac=1; *ad=1; *ae=1; *af=1; *ag=1; - free(a); -} -void input_limit(void) { - char *p = malloc(4); if (!p) return; - many_inputs(p,p,p,p,p,p,p,p,p,p,p,p,p,p,p,p,p,p,p,p,p,p,p,p,p,p,p,p,p,p,p,p,p); -} - -static void uncertain(char *a, char *b, char *c) { *b=1; *c=1; free(a); } -void missing_relation(char *q) { - char *p = malloc(4); if (!p) return; - uncertain(p, p, q); -} - -struct Box { char *data; }; -static void children(struct Box *a, struct Box *b) { *b->data=1; free(a->data); } -void missing_view(void *p) { - children(p, p); -} diff --git a/test/cases/engine/Analysis-rfc0016-composition.c b/test/cases/engine/Analysis-rfc0016-composition.c deleted file mode 100644 index 6d11a788..00000000 --- a/test/cases/engine/Analysis-rfc0016-composition.c +++ /dev/null @@ -1,28 +0,0 @@ -// Engine pin converted from test/Analysis/rfc0016-composition.c; markers are the v0.10.0 golden diagnostics. -// RFC 0016: one source operation versus several paths, and ordered effects. -#include "Inputs/prelude.h" - -static void after(char *a, char *b) { - free(a); - *b = 1; // BUG: use-after-free -} -static void twice(char *a, char *b) { - free(a); - free(b); // BUG: double-free -} -static void after_output(char **a, char **b) { - free(*a); - **b = 1; // BUG: use-after-free -} -static void forward(char *a, char *b) { after(a, b); } -void bad(void) { - char *p = malloc(4); - if (!p) return; - forward(p, p); - p = malloc(4); - if (!p) return; - twice(p, p); - p = malloc(4); - if (!p) return; - after_output(&p, &p); -} diff --git a/test/cases/engine/Analysis-use-after-free.c b/test/cases/engine/Analysis-use-after-free.c deleted file mode 100644 index 1b0206f6..00000000 --- a/test/cases/engine/Analysis-use-after-free.c +++ /dev/null @@ -1,14 +0,0 @@ -// Engine pin converted from test/Analysis/use-after-free.c; markers are the v0.10.0 golden diagnostics. -#include "Inputs/prelude.h" - -void use_after_free(void) { - int *p = malloc(sizeof(int)); - free(p); - *p = 1; // BUG: use-after-free -} - -void use_via_call(void) { - int *p = malloc(sizeof(int)); - free(p); - use(p); // BUG: use-after-free -} diff --git a/test/cases/engine/Annotations-invalid-annotation.c b/test/cases/engine/Annotations-invalid-annotation.c deleted file mode 100644 index 33923204..00000000 --- a/test/cases/engine/Annotations-invalid-annotation.c +++ /dev/null @@ -1,7 +0,0 @@ -// Engine pin converted from test/Annotations/invalid-annotation.c; markers are the v0.10.0 golden diagnostics. -// Unknown weavec.* annotations are reported; foreign annotations are ignored. -#include "Inputs/prelude.h" - -__attribute__((annotate("weavec.ownd"))) void typo(void) {} // BUG: invalid-annotation - -__attribute__((annotate("gsl.owner"))) void foreign(void) {} diff --git a/test/cases/engine/Annotations-rfc0003-annotation-mismatch.c b/test/cases/engine/Annotations-rfc0003-annotation-mismatch.c deleted file mode 100644 index 559396e9..00000000 --- a/test/cases/engine/Annotations-rfc0003-annotation-mismatch.c +++ /dev/null @@ -1,61 +0,0 @@ -// Engine pin converted from test/Annotations/rfc0003-annotation-mismatch.c; markers are the v0.10.0 golden diagnostics. -// RFC 0003: a definition's body is checked against its own annotations. -// Callers keep trusting the annotation; the definition is where the error is. -#include "Inputs/prelude.h" -#include - -struct node { - int v; - struct node *next; -}; -struct buf { - char *data; -}; - -void take(struct node *WEAVEC_OWNED n); -void poke(struct node *WEAVEC_MUT n); - -void frees_borrowed(struct node *WEAVEC_BORROWED n) { - free(n); // BUG: annotation-mismatch -} - -void frees_alias(struct node *WEAVEC_BORROWED n) { - struct node *m = n; - free(m); // BUG: annotation-mismatch -} - -void moves_mut(struct node *WEAVEC_MUT n) { - take(n); // BUG: annotation-mismatch -} - -void writes_borrowed(struct node *WEAVEC_BORROWED n) { - n->v = 1; // BUG: annotation-mismatch -} - -void frees_field_of_borrowed(struct buf *WEAVEC_BORROWED b) { - free(b->data); // BUG: annotation-mismatch -} - -void lends_borrowed_as_mut(struct node *WEAVEC_BORROWED n) { - poke(n); // BUG: annotation-mismatch -} - -char *WEAVEC_OWNED returns_borrow(struct buf *b) { return (char *)&b->data; } // BUG: annotation-mismatch - -char *WEAVEC_BORROWED returns_fresh(void) { return malloc(4); } // BUG: annotation-mismatch - -void consistent(struct node *WEAVEC_MUT n, const struct buf *WEAVEC_BORROWED b, - struct node *WEAVEC_OWNED o) { - n->v = 1; - use(b->data); - take(o); -} - -char *WEAVEC_OWNED returns_fresh_ok(void) { return malloc(4); } -char *WEAVEC_BORROWED returns_field_ok(struct buf *b) { return b->data; } - -// The caller believes the annotation; the lie is reported once, above. -void caller(struct node *n) { - frees_borrowed(n); - use(n); -} diff --git a/test/cases/engine/Annotations-unsafe.c b/test/cases/engine/Annotations-unsafe.c deleted file mode 100644 index 5006e2d8..00000000 --- a/test/cases/engine/Annotations-unsafe.c +++ /dev/null @@ -1,19 +0,0 @@ -// Engine pin converted from test/Annotations/unsafe.c; markers are the v0.10.0 golden diagnostics, -// plus the two RFC 0030 §6.1 adds: no diagnostic is dropped for being inside an unsafe region -// (v0.10.0 reported nothing inside one). -// WEAVEC_UNSAFE no longer opts a function or a block out of checking (RFC 0030 §6.1). -#include "Inputs/prelude.h" -#include - -WEAVEC_UNSAFE void whole_function(int *p) { - free(p); - free(p); // BUG: double-free definite -} - -void unsafe_block(int *p) { - free(p); - WEAVEC_UNSAFE { - use(p); // BUG: use-after-free definite - } - use(p); // BUG: use-after-free -} diff --git a/test/cases/engine/Inputs/cycle-b.c b/test/cases/engine/Inputs/cycle-b.c deleted file mode 100644 index 71d9717d..00000000 --- a/test/cases/engine/Inputs/cycle-b.c +++ /dev/null @@ -1,12 +0,0 @@ -/* Half of a two-unit cycle with rfc0005-cycle.c: `b_free` frees, and it is - * reached from the other unit through `a_free`, defined there. */ -#include "prelude.h" - -void a_free(void *p); - -void b_free(void *p) { free(p); } - -void b_release_twice(char *p) { - a_free(p); - free(p); // BUG: double-free definite -} diff --git a/test/cases/engine/Inputs/heap.c b/test/cases/engine/Inputs/heap.c deleted file mode 100644 index 671c3d4f..00000000 --- a/test/cases/engine/Inputs/heap.c +++ /dev/null @@ -1,15 +0,0 @@ -// RFC 0013: constructor bodies shared by helper and separate-unit cases. -#include "heap.h" -struct box *box_new(void) { - struct box *b = malloc(sizeof *b); - if (!b) return NULL; - b->data = malloc(4); - if (!b->data) { free(b); return NULL; } - return b; -} -struct box *box_wrap(char *p) { - struct box *b = malloc(sizeof *b); - if (!b) return NULL; - b->data = p; - return b; -} diff --git a/test/cases/engine/Inputs/heap.h b/test/cases/engine/Inputs/heap.h deleted file mode 100644 index e68b64eb..00000000 --- a/test/cases/engine/Inputs/heap.h +++ /dev/null @@ -1,7 +0,0 @@ -#ifndef WEAVEC_EVALUATION_HEAP_H -#define WEAVEC_EVALUATION_HEAP_H -#include "prelude.h" -struct box { char *data; }; -struct box *box_new(void); -struct box *box_wrap(char *); -#endif diff --git a/test/cases/engine/Inputs/heap13.c b/test/cases/engine/Inputs/heap13.c deleted file mode 100644 index 41ec8e83..00000000 --- a/test/cases/engine/Inputs/heap13.c +++ /dev/null @@ -1,38 +0,0 @@ -// RFC 0013: this body is seen at compile time in a different translation unit. -#include "prelude.h" -#include "heap13.h" -struct heap13_box *heap13_new(void) { - struct heap13_box *b = malloc(sizeof *b); if (!b) return NULL; - b->data = malloc(4); - if (!b->data) { free(b); return NULL; } - return b; -} -struct heap13_box *heap13_wrap(char *p) { - struct heap13_box *b = malloc(sizeof *b); if (!b) return NULL; - b->data = p; return b; -} - -struct heap13_box *heap13_singleton; -void heap13_ensure(void) { - if (heap13_singleton) return; - heap13_singleton = malloc(sizeof *heap13_singleton); - if (heap13_singleton) heap13_singleton->data = malloc(4); -} - -void heap13_drop_child(void) { free(heap13_singleton->data); } - -void heap13_swap(struct heap13_box *a, struct heap13_box *b) { - char *old = a->data; a->data = b->data; b->data = old; -} -void heap13_reset(struct heap13_box *b) { - struct heap13_box next = {malloc(4)}; - heap13_swap(b, &next); free(next.data); -} -void heap13_local_swap(struct heap13_box *a, struct heap13_box *b) { - struct heap13_box *x = a, *y = b; - char *old = x->data; x->data = y->data; y->data = old; -} -static void heap13_put(struct heap13_box *b, char *p) { b->data = p; } -void heap13_copy_and_free(struct heap13_box *a, struct heap13_box *b) { - heap13_put(b, a->data); free(a->data); -} diff --git a/test/cases/engine/Inputs/heap13.h b/test/cases/engine/Inputs/heap13.h deleted file mode 100644 index 2ac1fd13..00000000 --- a/test/cases/engine/Inputs/heap13.h +++ /dev/null @@ -1,13 +0,0 @@ -#ifndef WEAVEC_TEST_HEAP13_H -#define WEAVEC_TEST_HEAP13_H -struct heap13_box { char *data; }; -struct heap13_box *heap13_new(void); -struct heap13_box *heap13_wrap(char *p); -extern struct heap13_box *heap13_singleton; -void heap13_ensure(void); -void heap13_drop_child(void); -void heap13_swap(struct heap13_box *a, struct heap13_box *b); -void heap13_reset(struct heap13_box *b); -void heap13_local_swap(struct heap13_box *a, struct heap13_box *b); -void heap13_copy_and_free(struct heap13_box *a, struct heap13_box *b); -#endif diff --git a/test/cases/engine/Inputs/prelude.h b/test/cases/engine/Inputs/prelude.h deleted file mode 100644 index f83a7198..00000000 --- a/test/cases/engine/Inputs/prelude.h +++ /dev/null @@ -1,16 +0,0 @@ -/* Minimal libc surface so tests do not depend on system headers. */ -#ifndef WEAVEC_TEST_PRELUDE_H -#define WEAVEC_TEST_PRELUDE_H - -typedef unsigned long size_t; - -void *malloc(size_t size); -void *realloc(void *ptr, size_t size); -void free(void *ptr); -/* The opaque "look at this pointer" helper. It is annotated because a call - * to an unannotated external function warns by default (RFC 0003). */ -void use(const void *__attribute__((annotate("weavec.borrowed"))) ptr); - -#define NULL ((void *)0) - -#endif /* WEAVEC_TEST_PRELUDE_H */ diff --git a/test/cases/engine/WholeProgram-rfc0005-cycle.c b/test/cases/engine/WholeProgram-rfc0005-cycle.c deleted file mode 100644 index 831f641d..00000000 --- a/test/cases/engine/WholeProgram-rfc0005-cycle.c +++ /dev/null @@ -1,18 +0,0 @@ -// Engine pin converted from test/WholeProgram/rfc0005-cycle.c; markers are the v0.10.0 golden diagnostics. -// UNITS: Inputs/cycle-b.c -// RFC 0005: units that depend on each other form a cyclic component and are -// iterated to a fixpoint before anything is reported. `a_free` here calls -// `b_free` in cycle-b.c, and cycle-b.c calls `a_free`; both frees must be -// known for the double free in each unit to be found. -// -#include "Inputs/prelude.h" - -void b_free(void *p); - -void a_free(void *p) { b_free(p); } - -int a_use_after(void) { - char *p = malloc(1); - a_free(p); - return p[0]; // BUG: use-after-free -} diff --git a/test/cases/engine/WholeProgram-rfc0013-input-identity.c b/test/cases/engine/WholeProgram-rfc0013-input-identity.c deleted file mode 100644 index 457d19d2..00000000 --- a/test/cases/engine/WholeProgram-rfc0013-input-identity.c +++ /dev/null @@ -1,29 +0,0 @@ -// Engine pin converted from test/WholeProgram/rfc0013-input-identity.c; markers are the v0.10.0 golden diagnostics. -// UNITS: Inputs/heap13.c -// RFC 0013: input identities survive copied fields and local alias swaps. -#include "Inputs/prelude.h" -#include "Inputs/heap13.h" - -void good_copy(void) { - struct heap13_box a = {malloc(4)}, b = {malloc(8)}; - char *old = b.data; heap13_copy_and_free(&a, &b); free(old); -} -void bad_copy(void) { - struct heap13_box a = {malloc(4)}, b = {malloc(8)}; - char *old = b.data; heap13_copy_and_free(&a, &b); free(old); - free(b.data); // BUG: double-free -} -void good_swap(void) { - struct heap13_box a = {malloc(4)}, b = {malloc(8)}; - heap13_local_swap(&a, &b); - if (a.data) a.data[7] = 0; - if (b.data) b.data[3] = 0; - free(a.data); free(b.data); -} -void bad_swap(void) { - struct heap13_box a = {malloc(4)}, b = {malloc(8)}; - heap13_local_swap(&a, &b); - if (b.data) b.data[4] = 0; // BUG: out-of-bounds - free(a.data); free(b.data); -} -int main(void) { return 0; } diff --git a/test/cases/evaluation/helper-alias.c b/test/cases/evaluation/helper-alias.c deleted file mode 100644 index 47406fda..00000000 --- a/test/cases/evaluation/helper-alias.c +++ /dev/null @@ -1,12 +0,0 @@ -// RFC 0013: fixed evaluation case. -#include "Inputs/heap.h" -#include "Inputs/heap.c" -void run(void) { - char *p = malloc(4); - if (!p) return; - struct box *b = box_wrap(p); - if (!b) { free(p); return; } - free(p); - b->data[0] = 0; // BUG: use-after-free - free(b); -} diff --git a/test/cases/evaluation/helper-good.c b/test/cases/evaluation/helper-good.c deleted file mode 100644 index a735c844..00000000 --- a/test/cases/evaluation/helper-good.c +++ /dev/null @@ -1,11 +0,0 @@ -// RFC 0013: fixed evaluation case. -// CLEAN -#include "Inputs/heap.h" -#include "Inputs/heap.c" -void run(void) { - struct box *b = box_new(); - if (!b) return; - b->data[3] = 0; - free(b->data); - free(b); -} diff --git a/test/cases/evaluation/helper-leak.c b/test/cases/evaluation/helper-leak.c deleted file mode 100644 index b6a4c331..00000000 --- a/test/cases/evaluation/helper-leak.c +++ /dev/null @@ -1,8 +0,0 @@ -// RFC 0013: fixed evaluation case. -#include "Inputs/heap.h" -#include "Inputs/heap.c" -void run(void) { - struct box *b = box_new(); - if (!b) return; - free(b); // BUG: leak -} diff --git a/test/cases/evaluation/helper-overflow.c b/test/cases/evaluation/helper-overflow.c index d1aa95cb..e823d341 100644 --- a/test/cases/evaluation/helper-overflow.c +++ b/test/cases/evaluation/helper-overflow.c @@ -4,7 +4,7 @@ void run(void) { struct box *b = box_new(); if (!b) return; - b->data[4] = 0; // BUG: out-of-bounds // TRAP + b->data[4] = 0; // TRAP free(b->data); free(b); } diff --git a/test/cases/evaluation/inline-alias.c b/test/cases/evaluation/inline-alias.c deleted file mode 100644 index 9d60a68e..00000000 --- a/test/cases/evaluation/inline-alias.c +++ /dev/null @@ -1,12 +0,0 @@ -// RFC 0013: fixed evaluation case. -#include "Inputs/heap.h" -void run(void) { - char *p = malloc(4); - if (!p) return; - struct box *b = malloc(sizeof *b); - if (!b) { free(p); return; } - b->data = p; - free(p); - b->data[0] = 0; // BUG: use-after-free - free(b); -} diff --git a/test/cases/evaluation/inline-good.c b/test/cases/evaluation/inline-good.c deleted file mode 100644 index 6b8dbc35..00000000 --- a/test/cases/evaluation/inline-good.c +++ /dev/null @@ -1,12 +0,0 @@ -// RFC 0013: fixed evaluation case. -// CLEAN -#include "Inputs/heap.h" -void run(void) { - struct box *b = malloc(sizeof *b); - if (!b) return; - b->data = malloc(4); - if (!b->data) { free(b); return; } - b->data[3] = 0; - free(b->data); - free(b); -} diff --git a/test/cases/evaluation/inline-leak.c b/test/cases/evaluation/inline-leak.c deleted file mode 100644 index 6bd45a1b..00000000 --- a/test/cases/evaluation/inline-leak.c +++ /dev/null @@ -1,9 +0,0 @@ -// RFC 0013: fixed evaluation case. -#include "Inputs/heap.h" -void run(void) { - struct box *b = malloc(sizeof *b); - if (!b) return; - b->data = malloc(4); - if (!b->data) { free(b); return; } - free(b); // BUG: leak -} diff --git a/test/cases/evaluation/inline-overflow.c b/test/cases/evaluation/inline-overflow.c index ec5bbf04..cc1d7457 100644 --- a/test/cases/evaluation/inline-overflow.c +++ b/test/cases/evaluation/inline-overflow.c @@ -5,7 +5,7 @@ void run(void) { if (!b) return; b->data = malloc(4); if (!b->data) { free(b); return; } - b->data[4] = 0; // BUG: out-of-bounds // TRAP + b->data[4] = 0; // TRAP free(b->data); free(b); } diff --git a/test/cases/evaluation/output-size.c b/test/cases/evaluation/output-size.c index cbcc74a7..69d7a2ec 100644 --- a/test/cases/evaluation/output-size.c +++ b/test/cases/evaluation/output-size.c @@ -3,7 +3,7 @@ void make(char **p, size_t *n) { *n = 4; *p = malloc(*n); } void run(void) { char *p; size_t n; make(&p, &n); if (!p) return; - p[4] = 0; // BUG: out-of-bounds // TRAP + p[4] = 0; // TRAP free(p); } // Driver (RFC 0030 section 17.2): executes the defect so the runtime oracle can observe the check. diff --git a/test/cases/evaluation/product-known-miss.c b/test/cases/evaluation/product-known-miss.c index 7ed4fe70..9d38698f 100644 --- a/test/cases/evaluation/product-known-miss.c +++ b/test/cases/evaluation/product-known-miss.c @@ -3,7 +3,7 @@ #include "Inputs/heap.h" void run(size_t rows, size_t cols) { char *p = malloc(rows * cols); if (!p) return; - p[rows * cols] = 0; // BUG: out-of-bounds // TRAP + p[rows * cols] = 0; // TRAP free(p); } // Driver (RFC 0030 section 17.2): executes the defect so the runtime oracle can observe the check. diff --git a/test/cases/evaluation/rfc0014-callback-bug.c b/test/cases/evaluation/rfc0014-callback-bug.c deleted file mode 100644 index bb584396..00000000 --- a/test/cases/evaluation/rfc0014-callback-bug.c +++ /dev/null @@ -1,9 +0,0 @@ -// RFC 0014: independently identified pointer-identity evaluation. -#include -#include -static void keep(void *p) { (void)p; } -static void drop(void *p) { free(p); } -static void invoke(void (*fn)(void *), void *p) { fn(p); } -void bad(int *p) { invoke(drop, p); - *p = 1; // BUG: use-after-free -} diff --git a/test/cases/evaluation/rfc0014-callback-good.c b/test/cases/evaluation/rfc0014-callback-good.c deleted file mode 100644 index 75f25b96..00000000 --- a/test/cases/evaluation/rfc0014-callback-good.c +++ /dev/null @@ -1,8 +0,0 @@ -// RFC 0014: independently identified pointer-identity evaluation. -// CLEAN -#include -#include -static void keep(void *p) { (void)p; } -static void drop(void *p) { free(p); } -static void invoke(void (*fn)(void *), void *p) { fn(p); } -void clean(int *p) { invoke(keep, p); *p = 1; free(p); } diff --git a/test/cases/evaluation/rfc0014-copied-pointer-bug.c b/test/cases/evaluation/rfc0014-copied-pointer-bug.c deleted file mode 100644 index 514ac6e8..00000000 --- a/test/cases/evaluation/rfc0014-copied-pointer-bug.c +++ /dev/null @@ -1,6 +0,0 @@ -// RFC 0014: independently identified pointer-identity evaluation. -#include -#include -void bad(int *p) { int *q; memcpy(&q, &p, sizeof p); free(p); - *q = 1; // BUG: use-after-free -} diff --git a/test/cases/evaluation/rfc0014-copied-pointer-good.c b/test/cases/evaluation/rfc0014-copied-pointer-good.c deleted file mode 100644 index 9e6b8eb3..00000000 --- a/test/cases/evaluation/rfc0014-copied-pointer-good.c +++ /dev/null @@ -1,5 +0,0 @@ -// RFC 0014: independently identified pointer-identity evaluation. -// CLEAN -#include -#include -void clean(void) { int *p = malloc(sizeof *p), *q; if (!p) return; memcpy(&q, &p, sizeof p); *q = 1; free(q); } diff --git a/test/cases/evaluation/rfc0014-pointer-guard-bug.c b/test/cases/evaluation/rfc0014-pointer-guard-bug.c deleted file mode 100644 index 6bd782b2..00000000 --- a/test/cases/evaluation/rfc0014-pointer-guard-bug.c +++ /dev/null @@ -1,7 +0,0 @@ -// RFC 0014: independently identified pointer-identity evaluation. -#include -#include -static void release_same(int *p, int *q) { if (p == q) free(p); } -void bad(int *p) { release_same(p, p); - *p = 1; // BUG: use-after-free -} diff --git a/test/cases/evaluation/rfc0014-pointer-guard-good.c b/test/cases/evaluation/rfc0014-pointer-guard-good.c deleted file mode 100644 index 5881b6a6..00000000 --- a/test/cases/evaluation/rfc0014-pointer-guard-good.c +++ /dev/null @@ -1,6 +0,0 @@ -// RFC 0014: independently identified pointer-identity evaluation. -// CLEAN -#include -#include -static void release_same(int *p, int *q) { if (p == q) free(p); } -void clean(int *p, int *q) { if (p != q) { release_same(p, q); *p = 1; free(p); } } diff --git a/test/cases/evaluation/rfc0014-whole-callback-bug.c b/test/cases/evaluation/rfc0014-whole-callback-bug.c deleted file mode 100644 index 431802d7..00000000 --- a/test/cases/evaluation/rfc0014-whole-callback-bug.c +++ /dev/null @@ -1,9 +0,0 @@ -// RFC 0014: independently identified pointer-identity evaluation. -// UNITS: Inputs/rfc0014-callback.c -#include -#include -void invoke(void (*)(void *), void *); -static void drop(void *p) { free(p); } -void bad(int *p) { invoke(drop, p); - *p = 1; // BUG: use-after-free -} diff --git a/test/cases/evaluation/rfc0014-whole-callback-good.c b/test/cases/evaluation/rfc0014-whole-callback-good.c deleted file mode 100644 index 3a9a1315..00000000 --- a/test/cases/evaluation/rfc0014-whole-callback-good.c +++ /dev/null @@ -1,8 +0,0 @@ -// RFC 0014: independently identified pointer-identity evaluation. -// CLEAN -// UNITS: Inputs/rfc0014-callback.c -#include -#include -void invoke(void (*)(void *), void *); -static void keep(void *p) { (void)p; } -void clean(int *p) { invoke(keep, p); *p = 1; free(p); } diff --git a/test/cases/evaluation/rfc0015-array-cleanup-bug.c b/test/cases/evaluation/rfc0015-array-cleanup-bug.c deleted file mode 100644 index 04e65efd..00000000 --- a/test/cases/evaluation/rfc0015-array-cleanup-bug.c +++ /dev/null @@ -1,8 +0,0 @@ -// RFC 0015: fixed bug/clean pair; see the evaluation manifest. -#include "Inputs/prelude.h" -void *memcpy(void *, const void *, size_t); -void *memmove(void *, const void *, size_t); -static void drop(char **a, size_t n) { for(size_t i=0;ia = malloc(4); if (!p->a) { free(p); return NULL; } - p->b = p->a; return p; -} -void run(void) { - struct pair *p = make(); if (!p) return; - free(p->a); - p->b[0] = 0; // BUG: use-after-free - free(p); -} diff --git a/test/cases/evaluation/snapshot-good.c b/test/cases/evaluation/snapshot-good.c deleted file mode 100644 index c8500a9e..00000000 --- a/test/cases/evaluation/snapshot-good.c +++ /dev/null @@ -1,10 +0,0 @@ -// RFC 0013: fixed evaluation case. -// CLEAN -#include "Inputs/heap.h" -void run(size_t n) { - size_t original = n; - char *p = malloc(n); if (!p) return; - n = 1; - for (size_t i = 0; i < original; ++i) p[i] = 0; - free(p); -} diff --git a/test/cases/evaluation/string-field.c b/test/cases/evaluation/string-field.c index 2509992a..4b6d2621 100644 --- a/test/cases/evaluation/string-field.c +++ b/test/cases/evaluation/string-field.c @@ -9,7 +9,7 @@ struct box *make(void) { void run(void) { struct box *p = make(); if (!p) return; char dest[3]; - strcpy(dest, p->data); // BUG: out-of-bounds // TRAP + strcpy(dest, p->data); // TRAP free(p->data); free(p); } // Driver (RFC 0030 section 17.2): executes the defect so the runtime oracle can observe the check. diff --git a/test/cases/evaluation/symbolic-snapshot.c b/test/cases/evaluation/symbolic-snapshot.c index 11c43cb7..5dbcc799 100644 --- a/test/cases/evaluation/symbolic-snapshot.c +++ b/test/cases/evaluation/symbolic-snapshot.c @@ -4,7 +4,7 @@ void run(size_t n) { size_t original = n; char *p = malloc(n); if (!p) return; n = 1; - p[original] = 0; // BUG: out-of-bounds // TRAP + p[original] = 0; // TRAP free(p); } // Driver (RFC 0030 section 17.2): executes the defect so the runtime oracle can observe the check. diff --git a/test/cases/evaluation/value-snapshot.c b/test/cases/evaluation/value-snapshot.c index 94f20af7..68857499 100644 --- a/test/cases/evaluation/value-snapshot.c +++ b/test/cases/evaluation/value-snapshot.c @@ -3,7 +3,7 @@ void run(void) { size_t n = 4; char *p = malloc(n); if (!p) return; n = 8; - p[7] = 0; // BUG: out-of-bounds // TRAP + p[7] = 0; // TRAP free(p); } // Driver (RFC 0030 section 17.2): executes the defect so the runtime oracle can observe the check. diff --git a/test/cases/evaluation/vla-known-miss.c b/test/cases/evaluation/vla-known-miss.c index 52f8b996..2d6b5d1f 100644 --- a/test/cases/evaluation/vla-known-miss.c +++ b/test/cases/evaluation/vla-known-miss.c @@ -4,7 +4,7 @@ void run(size_t n) { if (!n) return; char local[n]; - local[n] = 0; // BUG: out-of-bounds // TRAP + local[n] = 0; // TRAP } // Driver (RFC 0030 section 17.2): executes the defect so the runtime oracle can observe the check. int main(void) { run(4); return 0; } diff --git a/test/cases/evaluation/whole-program-alias.c b/test/cases/evaluation/whole-program-alias.c deleted file mode 100644 index 7bedba6a..00000000 --- a/test/cases/evaluation/whole-program-alias.c +++ /dev/null @@ -1,12 +0,0 @@ -// RFC 0013: fixed evaluation case. -// UNITS: Inputs/heap.c -#include "Inputs/heap.h" -void run(void) { - char *p = malloc(4); - if (!p) return; - struct box *b = box_wrap(p); - if (!b) { free(p); return; } - free(p); - b->data[0] = 0; // BUG: use-after-free - free(b); -} diff --git a/test/cases/evaluation/whole-program-good.c b/test/cases/evaluation/whole-program-good.c deleted file mode 100644 index 0c08b2be..00000000 --- a/test/cases/evaluation/whole-program-good.c +++ /dev/null @@ -1,11 +0,0 @@ -// RFC 0013: fixed evaluation case. -// CLEAN -// UNITS: Inputs/heap.c -#include "Inputs/heap.h" -void run(void) { - struct box *b = box_new(); - if (!b) return; - b->data[3] = 0; - free(b->data); - free(b); -} diff --git a/test/cases/evaluation/whole-program-leak.c b/test/cases/evaluation/whole-program-leak.c deleted file mode 100644 index 520a4993..00000000 --- a/test/cases/evaluation/whole-program-leak.c +++ /dev/null @@ -1,8 +0,0 @@ -// RFC 0013: fixed evaluation case. -// UNITS: Inputs/heap.c -#include "Inputs/heap.h" -void run(void) { - struct box *b = box_new(); - if (!b) return; - free(b); // BUG: leak -} diff --git a/test/cases/evaluation/whole-program-overflow.c b/test/cases/evaluation/whole-program-overflow.c index 90b20b6e..4c5416a4 100644 --- a/test/cases/evaluation/whole-program-overflow.c +++ b/test/cases/evaluation/whole-program-overflow.c @@ -8,7 +8,7 @@ void run(void) { struct box *b = box_new(); if (!b) return; - b->data[4] = 0; // BUG: out-of-bounds + b->data[4] = 0; // TRAP: heap-buffer-overflow free(b->data); free(b); } diff --git a/test/cases/pairs/bool-bug.c b/test/cases/pairs/bool-bug.c deleted file mode 100644 index 76541057..00000000 --- a/test/cases/pairs/bool-bug.c +++ /dev/null @@ -1,11 +0,0 @@ -// RFC 0017: added regression pair. _Bool preserves nonzeroness, not the low bit of 256. -// FLAGS: -std=c11 -#include "Inputs/prelude.h" -void run(unsigned n) { - if (n != 256) return; - _Bool stored = n; - char *p = malloc(1); - if (!p) return; - free(p); - if (stored == 1) *p = 1; // BUG: use-after-free -} diff --git a/test/cases/pairs/bool-good.c b/test/cases/pairs/bool-good.c deleted file mode 100644 index 88ac0ffb..00000000 --- a/test/cases/pairs/bool-good.c +++ /dev/null @@ -1,12 +0,0 @@ -// RFC 0017: added regression pair. _Bool preserves nonzeroness, not the low bit of 256. -// CLEAN -// FLAGS: -std=c11 -#include "Inputs/prelude.h" -void run(unsigned n) { - if (n != 256) return; - _Bool stored = n; - char *p = malloc(1); - if (!p) return; - free(p); - if (stored == 0) *p = 1; -} diff --git a/test/cases/pairs/checked-builtin-bug.c b/test/cases/pairs/checked-builtin-bug.c deleted file mode 100644 index 41bc5387..00000000 --- a/test/cases/pairs/checked-builtin-bug.c +++ /dev/null @@ -1,13 +0,0 @@ -// RFC 0017: checked multiplication returns overflow and stores SIZE_MAX - 1. -// UNITS: Inputs/numeric-helpers.c -// FLAGS: -std=c11 -#include "Inputs/numeric-helpers.h" -void run(size_t rows) { - if (rows != (size_t)-1) return; - size_t bytes; - int overflow = checked_size(rows, 2, &bytes); - char *p = malloc(1); - if (!p) return; - free(p); - if (overflow && bytes == (size_t)-2) *p = 1; // BUG: use-after-free -} diff --git a/test/cases/pairs/checked-builtin-good.c b/test/cases/pairs/checked-builtin-good.c deleted file mode 100644 index 0b9374ed..00000000 --- a/test/cases/pairs/checked-builtin-good.c +++ /dev/null @@ -1,19 +0,0 @@ -// RFC 0017: checked multiplication returns overflow and stores SIZE_MAX - 1. -// CLEAN -// TOOL -// The helper's overflow result (another unit) makes the use after free -// unreachable; only the whole-program view sees it, so the pair is analysed -// with `weavec --whole-program` (RFC 0030 §Soundness, *Accepted false -// positives*: an infeasible path a per-unit compile cannot refute). -// UNITS: Inputs/numeric-helpers.c -// FLAGS: -std=c11 -#include "Inputs/numeric-helpers.h" -void run(size_t rows) { - if (rows != (size_t)-1) return; - size_t bytes; - int overflow = checked_size(rows, 2, &bytes); - char *p = malloc(1); - if (!p) return; - free(p); - if (!overflow || bytes != (size_t)-2) *p = 1; -} diff --git a/test/cases/pairs/fam-bug.c b/test/cases/pairs/fam-bug.c index 2953c13f..84c0f60e 100644 --- a/test/cases/pairs/fam-bug.c +++ b/test/cases/pairs/fam-bug.c @@ -8,7 +8,7 @@ void run(void) { if (!p) return; p->claimed = 10; int *tail = p->data; - tail[2] = 0; // BUG: out-of-bounds // TRAP + tail[2] = 0; // TRAP free(p); } // Driver (RFC 0030 section 17.2): executes the defect so the runtime oracle can observe the check. diff --git a/test/cases/pairs/fam-good.c b/test/cases/pairs/fam-good.c deleted file mode 100644 index 4d2ac9e2..00000000 --- a/test/cases/pairs/fam-good.c +++ /dev/null @@ -1,14 +0,0 @@ -// RFC 0017: added regression pair. Only two non-byte tail elements were allocated; the sibling count cannot widen them. -// CLEAN -// FLAGS: -std=c11 -#include "Inputs/prelude.h" -struct block { size_t claimed; double alignment; int data[]; }; -void run(void) { - struct block *p = malloc(__builtin_offsetof(struct block, data) + - 2 * sizeof(int)); - if (!p) return; - p->claimed = 10; - int *tail = p->data; - tail[1] = 0; - free(p); -} diff --git a/test/cases/pairs/helper-return-bug.c b/test/cases/pairs/helper-return-bug.c index d7421641..8d946451 100644 --- a/test/cases/pairs/helper-return-bug.c +++ b/test/cases/pairs/helper-return-bug.c @@ -10,7 +10,7 @@ void run(unsigned n) { if (n != 257) return; char *p = malloc(narrow_count(n)); if (!p) return; - p[1] = 0; // BUG: out-of-bounds + p[1] = 0; // TRAP: heap-buffer-overflow free(p); } // Driver (RFC 0030 section 17.2): executes the defect so the runtime oracle can observe the check. diff --git a/test/cases/pairs/helper-return-good.c b/test/cases/pairs/helper-return-good.c deleted file mode 100644 index 1a2c898e..00000000 --- a/test/cases/pairs/helper-return-good.c +++ /dev/null @@ -1,12 +0,0 @@ -// RFC 0017: added regression pair. A separate helper returns the narrowed count one, not its incoming 257. -// CLEAN -// UNITS: Inputs/numeric-helpers.c -// FLAGS: -std=c11 -#include "Inputs/numeric-helpers.h" -void run(unsigned n) { - if (n != 257) return; - char *p = malloc(narrow_count(n)); - if (!p) return; - p[0] = 0; - free(p); -} diff --git a/test/cases/pairs/memcpy-wrapped-zero-bug.c b/test/cases/pairs/memcpy-wrapped-zero-bug.c deleted file mode 100644 index 19306ea8..00000000 --- a/test/cases/pairs/memcpy-wrapped-zero-bug.c +++ /dev/null @@ -1,13 +0,0 @@ -// RFC 0017: added regression pair. A wrapped zero-byte copy preserves the old pointer; a complete copy installs null. -// FLAGS: -std=c11 -#include "Inputs/prelude.h" -void *memcpy(void *, const void *, size_t); -void run(void) { - char *p = malloc(1); - if (!p) return; - char *source[1] = {NULL}, *destination[1] = {p}; - size_t count = (size_t)-1 / sizeof p + 1; - memcpy(destination, source, count * sizeof p); - free(p); - if (destination[0]) *destination[0] = 1; // BUG: use-after-free -} diff --git a/test/cases/pairs/memcpy-wrapped-zero-good.c b/test/cases/pairs/memcpy-wrapped-zero-good.c deleted file mode 100644 index 3683240f..00000000 --- a/test/cases/pairs/memcpy-wrapped-zero-good.c +++ /dev/null @@ -1,13 +0,0 @@ -// RFC 0017: added regression pair. A wrapped zero-byte copy preserves the old pointer; a complete copy installs null. -// CLEAN -// FLAGS: -std=c11 -#include "Inputs/prelude.h" -void *memcpy(void *, const void *, size_t); -void run(void) { - char *p = malloc(1); - if (!p) return; - char *source[1] = {NULL}, *destination[1] = {p}; - memcpy(destination, source, sizeof p); - free(p); - if (destination[0]) *destination[0] = 1; -} diff --git a/test/cases/pairs/min-bound-bug.c b/test/cases/pairs/min-bound-bug.c index 8ff4647c..ebc5c833 100644 --- a/test/cases/pairs/min-bound-bug.c +++ b/test/cases/pairs/min-bound-bug.c @@ -10,7 +10,7 @@ void run(void) { char *p = malloc(4); if (!p) return; - fill_min(p, 5, 9); // BUG: out-of-bounds + fill_min(p, 5, 9); free(p); } // Driver (RFC 0030 section 17.2): executes the defect so the runtime oracle can observe the check. diff --git a/test/cases/pairs/min-bound-good.c b/test/cases/pairs/min-bound-good.c deleted file mode 100644 index e83e0e17..00000000 --- a/test/cases/pairs/min-bound-good.c +++ /dev/null @@ -1,11 +0,0 @@ -// RFC 0017: added regression pair. Both upper bounds of the helper loop determine its caller requirement. -// CLEAN -// UNITS: Inputs/numeric-helpers.c -// FLAGS: -std=c11 -#include "Inputs/numeric-helpers.h" -void run(void) { - char *p = malloc(4); - if (!p) return; - fill_min(p, 9, 4); - free(p); -} diff --git a/test/cases/pairs/mixed-signed-bug.c b/test/cases/pairs/mixed-signed-bug.c deleted file mode 100644 index c76a3b63..00000000 --- a/test/cases/pairs/mixed-signed-bug.c +++ /dev/null @@ -1,10 +0,0 @@ -// RFC 0017: added regression pair. The usual arithmetic conversions compare unsigned -1 with 1. -// FLAGS: -std=c11 -#include "Inputs/prelude.h" -void run(int delta, unsigned cap) { - if (delta != -1 || cap != 1) return; - char *p = malloc(1); - if (!p) return; - free(p); - if (delta > cap) *p = 1; // BUG: use-after-free -} diff --git a/test/cases/pairs/mixed-signed-good.c b/test/cases/pairs/mixed-signed-good.c deleted file mode 100644 index 5574321c..00000000 --- a/test/cases/pairs/mixed-signed-good.c +++ /dev/null @@ -1,11 +0,0 @@ -// RFC 0017: added regression pair. The usual arithmetic conversions compare unsigned -1 with 1. -// CLEAN -// FLAGS: -std=c11 -#include "Inputs/prelude.h" -void run(int delta, unsigned cap) { - if (delta != -1 || cap != 1) return; - char *p = malloc(1); - if (!p) return; - free(p); - if (delta < cap) *p = 1; -} diff --git a/test/cases/pairs/narrowing-bug.c b/test/cases/pairs/narrowing-bug.c deleted file mode 100644 index ad0c9093..00000000 --- a/test/cases/pairs/narrowing-bug.c +++ /dev/null @@ -1,11 +0,0 @@ -// RFC 0017: added regression pair. An eight-bit storage conversion makes 256 zero. -// FLAGS: -std=c11 -#include "Inputs/prelude.h" -void run(unsigned n) { - if (n != 256) return; - unsigned char stored = n; - char *p = malloc(1); - if (!p) return; - free(p); - if (stored == 0) *p = 1; // BUG: use-after-free -} diff --git a/test/cases/pairs/narrowing-good.c b/test/cases/pairs/narrowing-good.c deleted file mode 100644 index cd45ed64..00000000 --- a/test/cases/pairs/narrowing-good.c +++ /dev/null @@ -1,12 +0,0 @@ -// RFC 0017: added regression pair. An eight-bit storage conversion makes 256 zero. -// CLEAN -// FLAGS: -std=c11 -#include "Inputs/prelude.h" -void run(unsigned n) { - if (n != 256) return; - unsigned char stored = n; - char *p = malloc(1); - if (!p) return; - free(p); - if (stored != 0) *p = 1; -} diff --git a/test/cases/pairs/outparam-bug.c b/test/cases/pairs/outparam-bug.c index 15a7df1f..9c264ad5 100644 --- a/test/cases/pairs/outparam-bug.c +++ b/test/cases/pairs/outparam-bug.c @@ -12,7 +12,7 @@ void run(unsigned n) { narrow_count_out(n, &bytes); char *p = malloc(bytes); if (!p) return; - p[1] = 0; // BUG: out-of-bounds + p[1] = 0; // TRAP: heap-buffer-overflow free(p); } // Driver (RFC 0030 section 17.2): executes the defect so the runtime oracle can observe the check. diff --git a/test/cases/pairs/outparam-good.c b/test/cases/pairs/outparam-good.c deleted file mode 100644 index a9e5be0e..00000000 --- a/test/cases/pairs/outparam-good.c +++ /dev/null @@ -1,14 +0,0 @@ -// RFC 0017: added regression pair. A separate helper publishes a narrowed value through a wider output cell. -// CLEAN -// UNITS: Inputs/numeric-helpers.c -// FLAGS: -std=c11 -#include "Inputs/numeric-helpers.h" -void run(unsigned n) { - if (n != 257) return; - size_t bytes; - narrow_count_out(n, &bytes); - char *p = malloc(bytes); - if (!p) return; - p[0] = 0; - free(p); -} diff --git a/test/cases/pairs/product-bug.c b/test/cases/pairs/product-bug.c index 194e8147..fae8c2dc 100644 --- a/test/cases/pairs/product-bug.c +++ b/test/cases/pairs/product-bug.c @@ -6,7 +6,7 @@ void run(size_t rows, size_t cols) { if (!bytes) return; char *p = malloc(rows * cols); if (!p) return; - p[rows * cols] = 0; // BUG: out-of-bounds // TRAP + p[rows * cols] = 0; // TRAP free(p); } // Driver (RFC 0030 section 17.2): executes the defect so the runtime oracle can observe the check. diff --git a/test/cases/pairs/product-good.c b/test/cases/pairs/product-good.c deleted file mode 100644 index edf18173..00000000 --- a/test/cases/pairs/product-good.c +++ /dev/null @@ -1,12 +0,0 @@ -// RFC 0017: added regression pair. The repeated symbolic C product denotes the actual allocated byte count. -// CLEAN -// FLAGS: -std=c11 -#include "Inputs/prelude.h" -void run(size_t rows, size_t cols) { - size_t bytes = rows * cols; - if (!bytes) return; - char *p = malloc(rows * cols); - if (!p) return; - p[rows * cols - 1] = 0; - free(p); -} diff --git a/test/cases/pairs/unsigned-wrap-bug.c b/test/cases/pairs/unsigned-wrap-bug.c index a0cca566..c212622a 100644 --- a/test/cases/pairs/unsigned-wrap-bug.c +++ b/test/cases/pairs/unsigned-wrap-bug.c @@ -6,7 +6,7 @@ void run(unsigned n) { unsigned bytes = n + 2u; char *p = malloc(bytes); if (!p) return; - p[1] = 0; // BUG: out-of-bounds // TRAP + p[1] = 0; // TRAP free(p); } // Driver (RFC 0030 section 17.2): executes the defect so the runtime oracle can observe the check. diff --git a/test/cases/pairs/unsigned-wrap-good.c b/test/cases/pairs/unsigned-wrap-good.c deleted file mode 100644 index 1079fbd2..00000000 --- a/test/cases/pairs/unsigned-wrap-good.c +++ /dev/null @@ -1,12 +0,0 @@ -// RFC 0017: added regression pair. UINT_MAX + 2 wraps to one before conversion to the allocation size. -// CLEAN -// FLAGS: -std=c11 -#include "Inputs/prelude.h" -void run(unsigned n) { - if (n != (unsigned)-1) return; - unsigned bytes = n + 2u; - char *p = malloc(bytes); - if (!p) return; - p[0] = 0; - free(p); -} diff --git a/test/cases/pairs/vla-snapshot-bug.c b/test/cases/pairs/vla-snapshot-bug.c index 71a06f25..98344a89 100644 --- a/test/cases/pairs/vla-snapshot-bug.c +++ b/test/cases/pairs/vla-snapshot-bug.c @@ -7,7 +7,7 @@ void run(unsigned n) { n = 8; char *p = malloc(sizeof array); if (!p) return; - p[n - 4] = 0; // BUG: out-of-bounds // TRAP + p[n - 4] = 0; // TRAP free(p); } // Driver (RFC 0030 section 17.2): executes the defect so the runtime oracle can observe the check. diff --git a/test/cases/pairs/vla-snapshot-good.c b/test/cases/pairs/vla-snapshot-good.c deleted file mode 100644 index b8bdffce..00000000 --- a/test/cases/pairs/vla-snapshot-good.c +++ /dev/null @@ -1,13 +0,0 @@ -// RFC 0017: added regression pair. sizeof the declared VLA stays four after its bound variable becomes eight. -// CLEAN -// FLAGS: -std=c11 -#include "Inputs/prelude.h" -void run(unsigned n) { - if (n != 4) return; - char array[n]; - n = 8; - char *p = malloc(sizeof array); - if (!p) return; - p[n - 5] = 0; - free(p); -} diff --git a/test/cases/proofs/attached-payload-ignored.c b/test/cases/proofs/attached-payload-ignored.c deleted file mode 100644 index 0f1d4e88..00000000 --- a/test/cases/proofs/attached-payload-ignored.c +++ /dev/null @@ -1,7 +0,0 @@ -// Salvaged false proof (RFC 0030 section 17.2): rfc0029/attached-payload-transfer, case ignored (candidates 99-102). -// The result of attach_wrapper() is ignored, so i leaks when attach fails. -// UNITS: Inputs/attached-payload-library.c -#include "Inputs/attached-payload-api.h" -int main(void){reset_hooks();struct node *o=create();if(!o)return 0;struct node *i=create();if(!i){destroy(o);return 0;} -attach_wrapper(o,i); // MISS: i leaks when attach fails; a leak is not a facet and v0.10.0 is silent -destroy(o);return 0;} diff --git a/test/cases/proofs/attached-payload-leak.c b/test/cases/proofs/attached-payload-leak.c deleted file mode 100644 index 6a305d65..00000000 --- a/test/cases/proofs/attached-payload-leak.c +++ /dev/null @@ -1,7 +0,0 @@ -// Salvaged false proof (RFC 0030 section 17.2): rfc0029/attached-payload-transfer, case leak (candidates 99-102). -// When attach() fails (its key allocation returns null), i is never released. -// UNITS: Inputs/attached-payload-library.c -#include "Inputs/attached-payload-api.h" -int main(void){reset_hooks();struct node *o=create();if(!o)return 0;struct node *i=create();if(!i){destroy(o);return 0;} -if(!attach_wrapper(o,i)){destroy(o);return 0;} // MISS: i leaks when attach fails; a leak is not a facet and v0.10.0 is silent -destroy(o);return 0;} diff --git a/test/cases/proofs/reader-uninitialized.c b/test/cases/proofs/reader-uninitialized.c deleted file mode 100644 index 08a1ffe9..00000000 --- a/test/cases/proofs/reader-uninitialized.c +++ /dev/null @@ -1,9 +0,0 @@ -// Salvaged false proof (RFC 0030 section 17.2): rfc0029/readers, case reader-uninitialized. See SOURCES.md. -// consume() reads data[1..3], which are never written; zero-initialisation defines those reads. -// UNITS: Inputs/reader-uninitialized-cursor.c -#include "Inputs/cursor.h" -int main(void) { - unsigned char data[4]; data[0] = 1; - struct cursor c = {0, 0, data, sizeof data, 0}; - return (int)consume(&c); -} diff --git a/test/cases/proofs/recursive-offset-forward.c b/test/cases/proofs/recursive-offset-forward.c index 7feb7468..0193ed05 100644 --- a/test/cases/proofs/recursive-offset-forward.c +++ b/test/cases/proofs/recursive-offset-forward.c @@ -6,10 +6,10 @@ struct tree { struct tree *left, *right; }; static void odd(struct tree *p); static void even(struct tree *p) { if(!p) return; - odd(p->left); odd(p->right); free(p); // BUG: invalid-release definite + odd(p->left); odd(p->right); free(p); // TRAP: heap-buffer-overflow } -static void odd(struct tree *p) { even(p+1); } // BUG: invalid-release definite +static void odd(struct tree *p) { even(p+1); } int main(void) { struct tree *p=calloc(1,sizeof *p); if(!p)return 0; - odd(p); return 0; // BUG: invalid-release + odd(p); return 0; } diff --git a/test/cases/recall/CWE-121/callee_writes_past.c b/test/cases/recall/CWE-121/callee_writes_past.c index 44c98d3f..1a0ba871 100644 --- a/test/cases/recall/CWE-121/callee_writes_past.c +++ b/test/cases/recall/CWE-121/callee_writes_past.c @@ -16,13 +16,13 @@ static void fill_n(int *buf, int n) { void bad(void) { char small[4]; - fill_eight(small); // BUG: out-of-bounds + fill_eight(small); print_bytes(small, 4); } void bad_symbolic(void) { int ints[8]; - fill_n(ints, 16); // BUG: out-of-bounds + fill_n(ints, 16); print_int(ints[0]); } diff --git a/test/cases/recall/CWE-121/constant_index.c b/test/cases/recall/CWE-121/constant_index.c index e23391c4..68427341 100644 --- a/test/cases/recall/CWE-121/constant_index.c +++ b/test/cases/recall/CWE-121/constant_index.c @@ -3,7 +3,7 @@ void bad(void) { char buf[10]; - buf[10] = 'A'; // BUG: out-of-bounds // TRAP + buf[10] = 'A'; // TRAP } void good(void) { diff --git a/test/cases/recall/CWE-121/fgets_too_long.c b/test/cases/recall/CWE-121/fgets_too_long.c index f89ff854..7cb2fbc0 100644 --- a/test/cases/recall/CWE-121/fgets_too_long.c +++ b/test/cases/recall/CWE-121/fgets_too_long.c @@ -4,7 +4,7 @@ void bad(void *stream) { char line[64]; - if (fgets(line, 128, stream)) // BUG: out-of-bounds // TRAP + if (fgets(line, 128, stream)) // TRAP print_line(line); } diff --git a/test/cases/recall/CWE-121/loop_off_by_one.c b/test/cases/recall/CWE-121/loop_off_by_one.c index 482a4e3a..06ee0613 100644 --- a/test/cases/recall/CWE-121/loop_off_by_one.c +++ b/test/cases/recall/CWE-121/loop_off_by_one.c @@ -4,7 +4,7 @@ void bad(void) { int data[10]; for (int i = 0; i <= 10; i++) - data[i] = i; // BUG: out-of-bounds // TRAP + data[i] = i; // TRAP print_int(data[0]); } diff --git a/test/cases/recall/CWE-121/loop_wrong_bound.c b/test/cases/recall/CWE-121/loop_wrong_bound.c index d2352b9e..7f804119 100644 --- a/test/cases/recall/CWE-121/loop_wrong_bound.c +++ b/test/cases/recall/CWE-121/loop_wrong_bound.c @@ -6,7 +6,7 @@ void bad(void) { int src[100]; int dst[50]; for (int i = 0; i < 100; i++) - dst[i] = src[i]; // BUG: out-of-bounds // TRAP + dst[i] = src[i]; // TRAP print_int(dst[0]); } diff --git a/test/cases/recall/CWE-121/memcpy_too_much.c b/test/cases/recall/CWE-121/memcpy_too_much.c index e48461cb..c3982ffd 100644 --- a/test/cases/recall/CWE-121/memcpy_too_much.c +++ b/test/cases/recall/CWE-121/memcpy_too_much.c @@ -5,7 +5,7 @@ void bad(void) { char src[100]; char dst[50]; memset(src, 'A', 100); - memcpy(dst, src, 100); // BUG: out-of-bounds // TRAP + memcpy(dst, src, 100); // TRAP print_bytes(dst, 50); } diff --git a/test/cases/recall/CWE-121/memset_too_much.c b/test/cases/recall/CWE-121/memset_too_much.c index 2951bd21..ac762e91 100644 --- a/test/cases/recall/CWE-121/memset_too_much.c +++ b/test/cases/recall/CWE-121/memset_too_much.c @@ -10,7 +10,7 @@ struct record { void bad(void) { char buf[32]; - memset(buf, 0, 64); // BUG: out-of-bounds // TRAP + memset(buf, 0, 64); // TRAP print_bytes(buf, 32); } @@ -29,6 +29,6 @@ int main(int argc, char **argv) { int which = argc > 1 ? argv[1][0] - '0' : 0; struct record r; if (which == 1) bad(); - if (which == 2) bad_member(&r); // BUG: out-of-bounds + if (which == 2) bad_member(&r); return 0; } diff --git a/test/cases/recall/CWE-121/pointer_walk.c b/test/cases/recall/CWE-121/pointer_walk.c index 42e7694c..afd1cea1 100644 --- a/test/cases/recall/CWE-121/pointer_walk.c +++ b/test/cases/recall/CWE-121/pointer_walk.c @@ -4,7 +4,7 @@ void bad(void) { char buf[8]; char *p = buf + 4; - p[4] = 0; // BUG: out-of-bounds // TRAP + p[4] = 0; // TRAP print_bytes(buf, 8); } diff --git a/test/cases/recall/CWE-121/strcpy_literal.c b/test/cases/recall/CWE-121/strcpy_literal.c index 70dff0fa..14fc5210 100644 --- a/test/cases/recall/CWE-121/strcpy_literal.c +++ b/test/cases/recall/CWE-121/strcpy_literal.c @@ -7,20 +7,20 @@ void bad(void) { char buf[8]; - strcpy(buf, "0123456789"); // BUG: out-of-bounds // TRAP + strcpy(buf, "0123456789"); // TRAP print_line(buf); } void bad_strcat(void) { char buf[8]; strcpy(buf, "0123"); - strcat(buf, "45678"); // BUG: out-of-bounds // TRAP + strcat(buf, "45678"); // TRAP print_line(buf); } void bad_sprintf(int x) { char buf[4]; - sprintf(buf, "%d!!!", x); // BUG: out-of-bounds // TRAP + sprintf(buf, "%d!!!", x); // TRAP print_line(buf); } diff --git a/test/cases/recall/CWE-122/constant_index.c b/test/cases/recall/CWE-122/constant_index.c index 5bcf31b8..bd7c3f37 100644 --- a/test/cases/recall/CWE-122/constant_index.c +++ b/test/cases/recall/CWE-122/constant_index.c @@ -5,7 +5,7 @@ void bad(void) { char *buf = malloc(10); if (!buf) return; - buf[10] = 'A'; // BUG: out-of-bounds // TRAP + buf[10] = 'A'; // TRAP free(buf); } diff --git a/test/cases/recall/CWE-122/memcpy_too_much.c b/test/cases/recall/CWE-122/memcpy_too_much.c index d9fd26de..4ae9d943 100644 --- a/test/cases/recall/CWE-122/memcpy_too_much.c +++ b/test/cases/recall/CWE-122/memcpy_too_much.c @@ -9,7 +9,7 @@ void bad(void) { if (!dst) return; memset(src, 'A', 100); - memcpy(dst, src, 100); // BUG: out-of-bounds // TRAP + memcpy(dst, src, 100); // TRAP free(dst); } @@ -17,7 +17,7 @@ void bad_memset(void) { int *data = malloc(10 * sizeof *data); if (!data) return; - memset(data, 0, 20 * sizeof *data); // BUG: out-of-bounds // TRAP + memset(data, 0, 20 * sizeof *data); // TRAP free(data); } diff --git a/test/cases/recall/CWE-122/sized_field.c b/test/cases/recall/CWE-122/sized_field.c index 57e52c57..5ba4ee7e 100644 --- a/test/cases/recall/CWE-122/sized_field.c +++ b/test/cases/recall/CWE-122/sized_field.c @@ -11,7 +11,7 @@ struct buf { }; void bad(struct buf *b) { - b->data[b->cap] = 0; // BUG: out-of-bounds // TRAP + b->data[b->cap] = 0; // TRAP } struct vec { @@ -27,7 +27,7 @@ void vec_init(struct vec *v, size_t cap) { } int bad_inferred(struct vec *v) { - return v->items[v->cap]; // BUG: out-of-bounds // TRAP + return v->items[v->cap]; // TRAP } void good(struct buf *b, struct vec *v) { diff --git a/test/cases/recall/CWE-122/strcpy_strlen.c b/test/cases/recall/CWE-122/strcpy_strlen.c index 726232cd..2f0a3dee 100644 --- a/test/cases/recall/CWE-122/strcpy_strlen.c +++ b/test/cases/recall/CWE-122/strcpy_strlen.c @@ -9,7 +9,7 @@ void bad(const char *s) { char *d = malloc(strlen(s)); if (!d) return; - strcpy(d, s); // BUG: out-of-bounds // TRAP + strcpy(d, s); // TRAP print_line(d); free(d); } @@ -19,7 +19,7 @@ void bad_length_first(const char *s) { char *d = malloc(n); if (!d) return; - strcpy(d, s); // BUG: out-of-bounds // TRAP + strcpy(d, s); // TRAP print_line(d); free(d); } @@ -28,7 +28,7 @@ void bad_strdup_index(const char *s) { char *d = strdup(s); if (!d) return; - d[strlen(s) + 1] = 0; // BUG: out-of-bounds // TRAP + d[strlen(s) + 1] = 0; // TRAP print_line(d); free(d); } diff --git a/test/cases/recall/CWE-122/symbolic_index.c b/test/cases/recall/CWE-122/symbolic_index.c index 1c2330d2..b51169ca 100644 --- a/test/cases/recall/CWE-122/symbolic_index.c +++ b/test/cases/recall/CWE-122/symbolic_index.c @@ -7,7 +7,7 @@ void bad(size_t n) { int *data = malloc(n * sizeof *data); if (!data) return; - data[n] = 0; // BUG: out-of-bounds // TRAP + data[n] = 0; // TRAP free(data); } @@ -16,7 +16,7 @@ void bad_loop(size_t n) { if (!data) return; for (size_t i = 0; i <= n; i++) - data[i] = 0; // BUG: out-of-bounds // TRAP + data[i] = 0; // TRAP free(data); } diff --git a/test/cases/recall/CWE-122/wrapper_alloc.c b/test/cases/recall/CWE-122/wrapper_alloc.c index 36a47a5b..22dcc535 100644 --- a/test/cases/recall/CWE-122/wrapper_alloc.c +++ b/test/cases/recall/CWE-122/wrapper_alloc.c @@ -18,13 +18,13 @@ static void put_eight(char *b) { void bad(void) { char *buf = xmalloc(4); - put_eight(buf); // BUG: out-of-bounds + put_eight(buf); free(buf); } void bad_direct(void) { char *buf = xmalloc(4); - buf[4] = 0; // BUG: out-of-bounds // TRAP + buf[4] = 0; // TRAP free(buf); } diff --git a/test/cases/recall/CWE-122/wrong_size_alloc.c b/test/cases/recall/CWE-122/wrong_size_alloc.c index 4076b374..2ac35202 100644 --- a/test/cases/recall/CWE-122/wrong_size_alloc.c +++ b/test/cases/recall/CWE-122/wrong_size_alloc.c @@ -8,7 +8,7 @@ void bad(void) { int *data = malloc(10); if (!data) return; - data[9] = 0; // BUG: out-of-bounds // TRAP + data[9] = 0; // TRAP free(data); } @@ -17,7 +17,7 @@ void bad_loop(void) { if (!data) return; for (int i = 0; i < 10; i++) - data[i] = i; // BUG: out-of-bounds // TRAP + data[i] = i; // TRAP free(data); } diff --git a/test/cases/recall/CWE-124/pointer_before_start.c b/test/cases/recall/CWE-124/pointer_before_start.c index 017b333f..949201ac 100644 --- a/test/cases/recall/CWE-124/pointer_before_start.c +++ b/test/cases/recall/CWE-124/pointer_before_start.c @@ -6,7 +6,7 @@ void bad(void) { char buf[10]; char *p = buf - 8; // TRAP - p[0] = 'A'; // BUG: out-of-bounds + p[0] = 'A'; print_bytes(buf, 10); } @@ -15,7 +15,7 @@ void bad_heap(void) { if (!buf) return; char *p = buf - 1; - *p = 'A'; // BUG: out-of-bounds // TRAP + *p = 'A'; // TRAP free(buf); } diff --git a/test/cases/recall/CWE-124/write_before_start.c b/test/cases/recall/CWE-124/write_before_start.c index 0a12e8e2..87e8038e 100644 --- a/test/cases/recall/CWE-124/write_before_start.c +++ b/test/cases/recall/CWE-124/write_before_start.c @@ -5,7 +5,7 @@ void bad(void) { char buf[10]; - buf[-1] = 'A'; // BUG: out-of-bounds // TRAP + buf[-1] = 'A'; // TRAP print_bytes(buf, 10); } @@ -13,7 +13,7 @@ void bad_heap(void) { char *buf = malloc(10); if (!buf) return; - buf[-1] = 'A'; // BUG: out-of-bounds // TRAP + buf[-1] = 'A'; // TRAP free(buf); } diff --git a/test/cases/recall/CWE-126/loop_reads_past.c b/test/cases/recall/CWE-126/loop_reads_past.c index 3ba2e712..1c88306b 100644 --- a/test/cases/recall/CWE-126/loop_reads_past.c +++ b/test/cases/recall/CWE-126/loop_reads_past.c @@ -8,7 +8,7 @@ int bad(void) { int sum = 0; memset(data, 0, sizeof data); for (int i = 0; i <= 10; i++) - sum += data[i]; // BUG: out-of-bounds // TRAP + sum += data[i]; // TRAP return sum; } @@ -16,7 +16,7 @@ void bad_memcpy_source(void) { char small[8]; char big[16]; memset(small, 0, 8); - memcpy(big, small, 16); // BUG: out-of-bounds // TRAP + memcpy(big, small, 16); // TRAP print_bytes(big, 16); } diff --git a/test/cases/recall/CWE-126/read_past_end.c b/test/cases/recall/CWE-126/read_past_end.c index ff6b2ac4..732d463c 100644 --- a/test/cases/recall/CWE-126/read_past_end.c +++ b/test/cases/recall/CWE-126/read_past_end.c @@ -6,7 +6,7 @@ void bad(void) { int data[10]; memset(data, 0, sizeof data); - print_int(data[10]); // BUG: out-of-bounds // TRAP + print_int(data[10]); // TRAP } void bad_heap(void) { @@ -14,7 +14,7 @@ void bad_heap(void) { if (!data) return; memset(data, 0, 10 * sizeof *data); - print_int(data[10]); // BUG: out-of-bounds // TRAP + print_int(data[10]); // TRAP free(data); } diff --git a/test/cases/recall/CWE-126/strlen_unterminated.c b/test/cases/recall/CWE-126/strlen_unterminated.c index 277057ec..7eb8a96e 100644 --- a/test/cases/recall/CWE-126/strlen_unterminated.c +++ b/test/cases/recall/CWE-126/strlen_unterminated.c @@ -6,13 +6,13 @@ int bad(void) { char data[4] = {'a', 'b', 'c', 'd'}; - return (int)strlen(data); // BUG: out-of-bounds // TRAP + return (int)strlen(data); // TRAP } void bad_copy(void) { char data[4] = {'a', 'b', 'c', 'd'}; char out[16]; - strcpy(out, data); // BUG: out-of-bounds // TRAP + strcpy(out, data); // TRAP print_line(out); } diff --git a/test/cases/recall/CWE-127/read_before_start.c b/test/cases/recall/CWE-127/read_before_start.c index 2b2a2b6d..612e8dda 100644 --- a/test/cases/recall/CWE-127/read_before_start.c +++ b/test/cases/recall/CWE-127/read_before_start.c @@ -6,14 +6,14 @@ int bad(void) { int data[10]; memset(data, 0, sizeof data); - return data[-1]; // BUG: out-of-bounds // TRAP + return data[-1]; // TRAP } int bad_pointer(void) { char buf[10]; memset(buf, 0, 10); char *p = buf - 2; // TRAP - return p[1]; // BUG: out-of-bounds + return p[1]; } // Driver (RFC 0030 section 17.2): executes the defect so the runtime oracle can observe the check. diff --git a/test/cases/recall/CWE-170/strncpy_unterminated.c b/test/cases/recall/CWE-170/strncpy_unterminated.c index d746739e..01716b97 100644 --- a/test/cases/recall/CWE-170/strncpy_unterminated.c +++ b/test/cases/recall/CWE-170/strncpy_unterminated.c @@ -9,24 +9,24 @@ void bad(void) { char name[8]; strncpy(name, "0123456789", sizeof name); - print_int((int)strlen(name)); // BUG: out-of-bounds // TRAP + print_int((int)strlen(name)); // TRAP } void bad_puts(void) { char name[8]; strncpy(name, "0123456789", sizeof name); - puts(name); // BUG: out-of-bounds // TRAP + puts(name); // TRAP } void bad_initialiser(void) { char name[4] = "abcd"; - printf("%s\n", name); // BUG: out-of-bounds // TRAP + printf("%s\n", name); // TRAP } void bad_memset(void) { char name[8]; memset(name, 'x', sizeof name); - print_int((int)strlen(name)); // BUG: out-of-bounds // TRAP + print_int((int)strlen(name)); // TRAP } void good(void) { diff --git a/test/cases/recall/CWE-401/leak_in_callee.c b/test/cases/recall/CWE-401/leak_in_callee.c deleted file mode 100644 index 7c251b9a..00000000 --- a/test/cases/recall/CWE-401/leak_in_callee.c +++ /dev/null @@ -1,28 +0,0 @@ -// CWE-401: memory leak, a wrapper's fresh result is dropped by the caller. -#include "../recall.h" - -static char *make(size_t n) { - char *p = malloc(n); - if (p) - memset(p, 0, n); - return p; -} - -void bad(void) { - make(10); // BUG: leak -} - -void bad_struct(void) { - struct holder { char *buf; } h; - h.buf = make(10); - if (!h.buf) - return; - h.buf = NULL; // BUG: leak -} - -void good(void) { - char *p = make(10); - if (!p) - return; - free(p); -} diff --git a/test/cases/recall/CWE-401/leak_on_return.c b/test/cases/recall/CWE-401/leak_on_return.c deleted file mode 100644 index 7fe256d1..00000000 --- a/test/cases/recall/CWE-401/leak_on_return.c +++ /dev/null @@ -1,28 +0,0 @@ -// CWE-401: memory leak, an allocation not freed on every path. -#include "../recall.h" - -void bad(void) { - char *data = malloc(100); - if (!data) - return; - memset(data, 'A', 100); - print_bytes(data, 100); // BUG: leak -} - -int bad_early_return(int flag) { - char *data = malloc(100); - if (!data) - return -1; - if (flag) - return 0; // BUG: leak - free(data); - return 1; -} - -void good(void) { - char *data = malloc(100); - if (!data) - return; - memset(data, 'A', 100); - free(data); -} diff --git a/test/cases/recall/CWE-401/leak_overwritten.c b/test/cases/recall/CWE-401/leak_overwritten.c deleted file mode 100644 index f35a8bf4..00000000 --- a/test/cases/recall/CWE-401/leak_overwritten.c +++ /dev/null @@ -1,24 +0,0 @@ -// CWE-401: memory leak, the only pointer to an allocation is overwritten. -#include "../recall.h" - -void bad(void) { - char *data = malloc(100); - if (!data) - return; - data = malloc(200); // BUG: leak - if (!data) - return; - free(data); -} - -void bad_swapped(void) { - char *a = malloc(100); - char *b = malloc(200); - if (!a || !b) { - free(a); - free(b); - return; - } - a = b; // BUG: leak - free(a); -} diff --git a/test/cases/recall/CWE-415/double_free.c b/test/cases/recall/CWE-415/double_free.c deleted file mode 100644 index 57b17f8c..00000000 --- a/test/cases/recall/CWE-415/double_free.c +++ /dev/null @@ -1,30 +0,0 @@ -// CWE-415: double free, the same allocation released twice. -#include "../recall.h" - -void bad(void) { - char *data = malloc(100); - if (!data) - return; - free(data); - free(data); // BUG: double-free -} - -void bad_on_path(int flag) { - char *data = malloc(100); - if (!data) - return; - if (flag) - free(data); - free(data); // BUG: double-free -} - -void good(int flag) { - char *data = malloc(100); - if (!data) - return; - if (flag) { - free(data); - return; - } - free(data); -} diff --git a/test/cases/recall/CWE-415/double_free_alias.c b/test/cases/recall/CWE-415/double_free_alias.c deleted file mode 100644 index 574693b2..00000000 --- a/test/cases/recall/CWE-415/double_free_alias.c +++ /dev/null @@ -1,34 +0,0 @@ -// CWE-415: double free through an alias and through a callee. -#include "../recall.h" - -struct holder { - char *buf; -}; - -static void release(struct holder *h) { free(h->buf); } - -void bad_alias(void) { - char *data = malloc(100); - if (!data) - return; - char *alias = data; - free(alias); - free(data); // BUG: double-free -} - -void bad_callee(void) { - struct holder h; - h.buf = malloc(100); - if (!h.buf) - return; - release(&h); - free(h.buf); // BUG: double-free -} - -void bad_container(void) { - struct wrapped { int tag; char payload[8]; } *w = malloc(sizeof *w); - if (!w) - return; - free(w); - free(w); // BUG: double-free -} diff --git a/test/cases/recall/CWE-416/use_after_free.c b/test/cases/recall/CWE-416/use_after_free.c deleted file mode 100644 index 89f639cc..00000000 --- a/test/cases/recall/CWE-416/use_after_free.c +++ /dev/null @@ -1,28 +0,0 @@ -// CWE-416: use after free, a read and a write through a freed pointer. -#include "../recall.h" - -void bad_read(void) { - char *data = malloc(100); - if (!data) - return; - memset(data, 'A', 100); - free(data); - print_bytes(data, 100); // BUG: use-after-free -} - -void bad_write(void) { - char *data = malloc(100); - if (!data) - return; - free(data); - data[0] = 'A'; // BUG: use-after-free -} - -void good(void) { - char *data = malloc(100); - if (!data) - return; - memset(data, 'A', 100); - print_bytes(data, 100); - free(data); -} diff --git a/test/cases/recall/CWE-416/use_after_free_alias.c b/test/cases/recall/CWE-416/use_after_free_alias.c deleted file mode 100644 index 4fc7d4e4..00000000 --- a/test/cases/recall/CWE-416/use_after_free_alias.c +++ /dev/null @@ -1,45 +0,0 @@ -// CWE-416: use after free through an alias, a field pointer, and a callee -// that frees. -#include "../recall.h" - -struct node { - struct node *next; - int value; -}; - -static void destroy(struct node *n) { free(n); } - -void bad_alias(void) { - char *data = malloc(100); - if (!data) - return; - char *alias = data; - free(data); - alias[0] = 'A'; // BUG: use-after-free -} - -void bad_callee(void) { - struct node *n = malloc(sizeof *n); - if (!n) - return; - n->value = 1; - destroy(n); - print_int(n->value); // BUG: use-after-free -} - -void bad_field_pointer(void) { - struct node *n = malloc(sizeof *n); - if (!n) - return; - int *v = &n->value; - free(n); - *v = 2; // BUG: use-after-free -} - -void bad_list_walk(struct node *head) { - struct node *n = head; - while (n) { - free(n); - n = n->next; // BUG: use-after-free - } -} diff --git a/test/cases/recall/CWE-457/uninitialized_pointer.c b/test/cases/recall/CWE-457/uninitialized_pointer.c index 7d015d01..1f7b2b91 100644 --- a/test/cases/recall/CWE-457/uninitialized_pointer.c +++ b/test/cases/recall/CWE-457/uninitialized_pointer.c @@ -3,7 +3,7 @@ void bad(void) { char *data; - data[0] = 'A'; // BUG: use-of-uninitialized // TRAP + data[0] = 'A'; // TRAP } void bad_on_path(int flag) { @@ -11,7 +11,7 @@ void bad_on_path(int flag) { char *data; if (flag) data = buf; - print_line(data); // BUG: use-of-uninitialized // NEUTRALISED: zero-init + print_line(data); } void good(int flag) { diff --git a/test/cases/recall/CWE-476/null_dereference.c b/test/cases/recall/CWE-476/null_dereference.c index 892fdec8..01960633 100644 --- a/test/cases/recall/CWE-476/null_dereference.c +++ b/test/cases/recall/CWE-476/null_dereference.c @@ -2,25 +2,24 @@ // a pointer set to NULL then dereferenced. // RFC 0030 §8.4: an allocation result used without a null test is the // allocation-failure id (off by default) and a checked null facet. -// FLAGS: -Wweavec-allocation-failure // RUN-INPUT: 1 // RUN-INPUT: 2 #include "../recall.h" void bad_unchecked_malloc(void) { char *data = malloc(100); - data[0] = 'A'; // BUG: allocation-failure + data[0] = 'A'; free(data); } void bad_assigned_null(void) { int *p = NULL; - *p = 1; // BUG: null-dereference // TRAP + *p = 1; // TRAP } void bad_after_test(int *p) { if (p == NULL) - print_int(*p); // BUG: null-dereference // TRAP + print_int(*p); // TRAP } void good(void) { diff --git a/test/cases/recall/CWE-476/null_from_callee.c b/test/cases/recall/CWE-476/null_from_callee.c deleted file mode 100644 index 8edf4f42..00000000 --- a/test/cases/recall/CWE-476/null_from_callee.c +++ /dev/null @@ -1,36 +0,0 @@ -// CWE-476: NULL pointer dereference, a callee's result that may be NULL is -// dereferenced without a check, directly and through a wrapper. -#include "../recall.h" - -struct node { - int value; -}; - -static struct node *make_node(int value) { - struct node *n = malloc(sizeof *n); - if (n) - n->value = value; - return n; -} - -void bad(void) { - struct node *n = make_node(1); - print_int(n->value); // BUG: null-dereference // MISS: the analysis does not report a callee's possibly-null result; the guard stops at run time - free(n); -} - -void bad_passed_on(void) { - struct node *n = make_node(1); - (void)strlen((char *)n); // BUG: null-dereference // MISS: as above - free(n); -} - -void good(void) { - struct node *n = make_node(1); - if (!n) - return; - print_int(n->value); - free(n); -} - -// No driver: both defects misbehave only when malloc fails inside make_node. diff --git a/test/cases/repros/arrloop.c b/test/cases/repros/arrloop.c index b60eea76..ca646874 100644 --- a/test/cases/repros/arrloop.c +++ b/test/cases/repros/arrloop.c @@ -4,7 +4,6 @@ // at most a possible finding, which is a warning, and a leak is never an error; so // double-free and leak warnings are allowed, and the program must not trap. // CLEAN -// ALLOW: double-free leak // ASAN #include void free_all(char **v, int n) { int i; for (i = 0; i < n; i++) free(v[i]); free(v); } diff --git a/test/cases/repros/loopcorr.c b/test/cases/repros/loopcorr.c index 99a28153..fbb85045 100644 --- a/test/cases/repros/loopcorr.c +++ b/test/cases/repros/loopcorr.c @@ -4,7 +4,6 @@ // intended (RFC 0030 section 3.2): the maybe-null dereference is a checked facet, with no // diagnostic and no trap; the out-of-memory leak may stay a warning, hence the ALLOW. // CLEAN -// ALLOW: leak // ASAN #include typedef struct node { struct node *next, *prev; } node; diff --git a/test/cases/repros/luaalloc.c b/test/cases/repros/luaalloc.c index e4e19e1b..aa899cfa 100644 --- a/test/cases/repros/luaalloc.c +++ b/test/cases/repros/luaalloc.c @@ -16,6 +16,6 @@ static void *l_alloc(void *ud, void *ptr, size_t osize, size_t nsize) { static void mfree(G *g, void *b, size_t osize) { (*g->frealloc)(g->ud, b, osize, 0); } static void *mmalloc(G *g, size_t n) { void *p = (*g->frealloc)(g->ud, NULL, 0, n); if (!p) abort(); return p; } G *newstate(Alloc f, void *ud) { G *g = f(ud, NULL, 0, sizeof(G)); if (!g) return NULL; g->frealloc = f; g->ud = ud; return g; } -void tfree(G *g, T *t) { mfree(g, t, sizeof(T)); t->flags = 0; } // BUG: use-after-free definite -void tfree2(G *g, T *t) { mfree(g, t->arr, 4); mfree(g, t->arr, 4); mfree(g, t, sizeof(T)); } // BUG: double-free definite +void tfree(G *g, T *t) { mfree(g, t, sizeof(T)); t->flags = 0; } +void tfree2(G *g, T *t) { mfree(g, t->arr, 4); mfree(g, t->arr, 4); mfree(g, t, sizeof(T)); } // TRAP: invalid-release int main(void) { G *g = newstate(l_alloc, NULL); T *t; if (!g) return 1; t = mmalloc(g, sizeof(T)); t->arr = mmalloc(g, 4); tfree2(g, t); return 0; } diff --git a/test/cases/semantics/Inputs/rfc0030.h b/test/cases/semantics/Inputs/rfc0030.h deleted file mode 100644 index a5bc2364..00000000 --- a/test/cases/semantics/Inputs/rfc0030.h +++ /dev/null @@ -1,24 +0,0 @@ -/* RFC 0030 *Annotation surface*: the macros weavec.h 0.9 adds, for a weavec.h - * that predates them. Each expands exactly as the RFC defines it; once - * resources/include/weavec.h defines a macro, its own definition is used and - * the fallback here is skipped. The file only defines macros, so it adds no - * function to any case's ledger. */ -#ifndef WEAVEC_CASES_SEMANTICS_RFC0030_H -#define WEAVEC_CASES_SEMANTICS_RFC0030_H - -#include - -#ifndef WEAVEC_COUNTED_BY -#define WEAVEC_COUNTED_BY(n) WEAVEC_ANNOTATE_("weavec.counted_by." #n) -#endif -#ifndef WEAVEC_ENDED_BY -#define WEAVEC_ENDED_BY(q) WEAVEC_ANNOTATE_("weavec.ended_by." #q) -#endif -#ifndef WEAVEC_STRING -#define WEAVEC_STRING WEAVEC_ANNOTATE_("weavec.string") -#endif -#ifndef WEAVEC_REQUIRE_SAFE -#define WEAVEC_REQUIRE_SAFE WEAVEC_ANNOTATE_("weavec.require_safe") -#endif - -#endif /* WEAVEC_CASES_SEMANTICS_RFC0030_H */ diff --git a/test/cases/semantics/aliasing/cursor-into-data.c b/test/cases/semantics/aliasing/cursor-into-data.c index 604fdb93..fc79cd6a 100644 --- a/test/cases/semantics/aliasing/cursor-into-data.c +++ b/test/cases/semantics/aliasing/cursor-into-data.c @@ -11,7 +11,7 @@ struct buf { char *data; char *cur; }; void reset(struct buf *b) { free(b->data); - b->cur[0] = 0; // BUG: use-after-free + b->cur[0] = 0; // TRAP: heap-use-after-free b->data = NULL; b->cur = NULL; } diff --git a/test/cases/semantics/aliasing/owning-fields-distinct.c b/test/cases/semantics/aliasing/owning-fields-distinct.c deleted file mode 100644 index cb0cb140..00000000 --- a/test/cases/semantics/aliasing/owning-fields-distinct.c +++ /dev/null @@ -1,19 +0,0 @@ -// RFC 0030 §3.1: two pointers loaded from owning places are distinct, so 'free(s->a); free(s->b)' stays proven. -// STAGE: S7 -// 'pair.a' and 'pair.b' are owning slots (a value loaded from each is released), and the -// owner-uniqueness assumption (§9.4) keeps their objects distinct, so the second release is -// not may-alias-released. 'int' is not a character type and is not compatible with -// 'struct pair', so the accesses through 's' are distinct from the released objects too. -// The fields are cleared, so no boundary is left holding a released pointer: no temporal -// facet of the unit is unresolved. -// CLEAN -#include - -struct pair { int *a; int *b; }; - -void cleanup(struct pair *s) { - free(s->a); - s->a = NULL; - free(s->b); - s->b = NULL; -} diff --git a/test/cases/semantics/aliasing/two-same-argument.c b/test/cases/semantics/aliasing/two-same-argument.c index 57d22f48..e4038e90 100644 --- a/test/cases/semantics/aliasing/two-same-argument.c +++ b/test/cases/semantics/aliasing/two-same-argument.c @@ -10,7 +10,7 @@ void two(char *a, char *b) { free(a); - b[0] = 1; // BUG: use-after-free definite + b[0] = 1; // TRAP: heap-use-after-free } int main(void) { diff --git a/test/cases/semantics/assume/assumed-after-site.c b/test/cases/semantics/assume/assumed-after-site.c deleted file mode 100644 index 89b81a0c..00000000 --- a/test/cases/semantics/assume/assumed-after-site.c +++ /dev/null @@ -1,19 +0,0 @@ -// RFC 0030 §6.2: the analysis assumes the expression after the site, whatever its outcome. -// STAGE: S3 -// The assertion 'n >= 0 && n < 8' is not proven, so it is checked. After the site the -// analysis assumes it (sound in the enforcing modes, which trap first), so buf[n] is proven -// in bounds and gets no index check: the unit has no checked spatial facet. The run passes -// n == 8, and the assertion traps before the access. -// RUN-INPUT: -#include - -int pick(int n) { - char buf[8] = {0}; - WEAVEC_ASSUME(n >= 0 && n < 8); - return buf[n]; // TRAP -} - -int main(int argc, char **argv) { - (void)argv; - return pick(argc + 7); -} diff --git a/test/cases/semantics/assume/contradicted.c b/test/cases/semantics/assume/contradicted.c deleted file mode 100644 index 901af65e..00000000 --- a/test/cases/semantics/assume/contradicted.c +++ /dev/null @@ -1,13 +0,0 @@ -// RFC 0030 §6.2: an assumption the engine refutes is a contradicted-assumption error. -// STAGE: S3 -// 'len' is 0 on every path to the site, so 'len > 0' is refuted: "assumption 'len > 0' is -// false here", with a note where 'len' got its value. v0.10.0 trusted it (probe c03). -#include -#include - -size_t last(const char *s) { - size_t len = 0; - (void)s; - WEAVEC_ASSUME(len > 0); // BUG: contradicted-assumption definite - return len - 1; -} diff --git a/test/cases/semantics/assume/proven.c b/test/cases/semantics/assume/proven.c deleted file mode 100644 index add40a06..00000000 --- a/test/cases/semantics/assume/proven.c +++ /dev/null @@ -1,12 +0,0 @@ -// RFC 0030 §6.2: an assumption the engine proves stays weavec.h's no-op call and needs no check. -// STAGE: S3 -// After the early return 'n > 0' holds at the Assume site, so its assertion facet is proven -// and the call stays the no-op weavec_assume_, which the optimiser removes. -// CLEAN -#include - -int positive(int n) { - if (n <= 0) return 0; - WEAVEC_ASSUME(n > 0); - return n; -} diff --git a/test/cases/semantics/boundary/atexit-reader.c b/test/cases/semantics/boundary/atexit-reader.c index eafdc752..7c90aa53 100644 --- a/test/cases/semantics/boundary/atexit-reader.c +++ b/test/cases/semantics/boundary/atexit-reader.c @@ -10,7 +10,7 @@ static char *g; static volatile char sink; -static void reader(void) { sink = g[0]; } // BUG: use-after-free // TRAP +static void reader(void) { sink = g[0]; } // TRAP int main(void) { g = malloc(8); diff --git a/test/cases/semantics/boundary/global-written-by-unknown-code.c b/test/cases/semantics/boundary/global-written-by-unknown-code.c deleted file mode 100644 index 4d1a14e9..00000000 --- a/test/cases/semantics/boundary/global-written-by-unknown-code.c +++ /dev/null @@ -1,30 +0,0 @@ -// RFC 0031 *Implementation amendments*: globals that unknown code may write. -// STAGE: S7 -// `parse` calls `notify`, which this unit does not define, so it may write -// any global: `count` among them (http-parser's test counts messages in a -// callback its parser runs). `parse`'s summary said nothing of `count`, -// so `main` kept it at zero and reported `messages[count - 1]` as a -// definite (and false) `out-of-bounds`. The summary now says the function -// runs unseen code (`unknown-globals`), and the call forgets what the -// caller's globals hold. -// TOOL -// CLEAN -struct message { - int upgrade; -}; - -struct message messages[4]; -int count; - -void notify(const char *text); - -static int parse(const char *text) { - notify(text); - return 0; -} - -int main(void) { - count = 0; - parse("GET / HTTP/1.1"); - return messages[count - 1].upgrade; -} diff --git a/test/cases/semantics/boundary/incomplete-summary-writes-arguments.c b/test/cases/semantics/boundary/incomplete-summary-writes-arguments.c index ebbb0695..ef003433 100644 --- a/test/cases/semantics/boundary/incomplete-summary-writes-arguments.c +++ b/test/cases/semantics/boundary/incomplete-summary-writes-arguments.c @@ -6,7 +6,6 @@ // summary is incomplete: `p`, which it sets through `&p`, is no longer the // null the caller stored (a definite null dereference was reported here, as // in sqlite's `memdbFromDbSchema` after `sqlite3_file_control`). -// FLAGS: -fweavec-budget=12 // RUN-INPUT: 1 // CLEAN static int g = 7; diff --git a/test/cases/semantics/boundary/remember-free-peek.c b/test/cases/semantics/boundary/remember-free-peek.c index 35d97024..6cd50b49 100644 --- a/test/cases/semantics/boundary/remember-free-peek.c +++ b/test/cases/semantics/boundary/remember-free-peek.c @@ -11,7 +11,7 @@ static char *g_cache; static void remember(char *p) { g_cache = p; } -static int peek(void) { return g_cache[0]; } // BUG: use-after-free // TRAP +static int peek(void) { return g_cache[0]; } // TRAP int main(void) { char *p = malloc(8); diff --git a/test/cases/semantics/concurrency/handler-race.c b/test/cases/semantics/concurrency/handler-race.c index 0882a1cf..966f6045 100644 --- a/test/cases/semantics/concurrency/handler-race.c +++ b/test/cases/semantics/concurrency/handler-race.c @@ -24,7 +24,7 @@ int main(void) { signal(SIGUSR1, handler); if (g_buf) { raise(SIGUSR1); - g_buf[0] = 1; // BUG: null-dereference // TRAP + g_buf[0] = 1; // TRAP } return 0; } diff --git a/test/cases/semantics/concurrency/worker-freed-global.c b/test/cases/semantics/concurrency/worker-freed-global.c index 3543878b..e6590333 100644 --- a/test/cases/semantics/concurrency/worker-freed-global.c +++ b/test/cases/semantics/concurrency/worker-freed-global.c @@ -14,7 +14,7 @@ static volatile char sink; static void *worker(void *arg) { (void)arg; - sink = g[0]; // BUG: use-after-free + sink = g[0]; // TRAP: heap-use-after-free return NULL; } diff --git a/test/cases/semantics/confirm/const-shallow-callback.c b/test/cases/semantics/confirm/const-shallow-callback.c index c95e04ca..d5185564 100644 --- a/test/cases/semantics/confirm/const-shallow-callback.c +++ b/test/cases/semantics/confirm/const-shallow-callback.c @@ -6,7 +6,6 @@ // The callee is in another unit. An unconfirmed candidate may remain a // warning (RFC 0034, Diagnostics). // CLEAN -// ALLOW: use-of-uninitialized // UNITS: const-shallow-callback-callee.c #include #include diff --git a/test/cases/semantics/confirm/exclusive-out-params.c b/test/cases/semantics/confirm/exclusive-out-params.c index 0e4c6bff..5e785b46 100644 --- a/test/cases/semantics/confirm/exclusive-out-params.c +++ b/test/cases/semantics/confirm/exclusive-out-params.c @@ -5,7 +5,6 @@ // never hold the same object. Each argument count takes another path. An // unconfirmed candidate may remain a warning (RFC 0034, Diagnostics). // CLEAN -// ALLOW: double-free // RUN-INPUT: // RUN-INPUT: x // RUN-INPUT: x y diff --git a/test/cases/semantics/confirm/exclusive-result-or-out-param.c b/test/cases/semantics/confirm/exclusive-result-or-out-param.c index 144d468b..83eb5d8b 100644 --- a/test/cases/semantics/confirm/exclusive-result-or-out-param.c +++ b/test/cases/semantics/confirm/exclusive-result-or-out-param.c @@ -3,7 +3,6 @@ // path and returns another on the other, so the result and *a never hold // the same object. Freeing both is correct for every argument count. // CLEAN -// ALLOW: double-free // RUN-INPUT: // RUN-INPUT: x #include diff --git a/test/cases/semantics/confirm/exclusive-switch-out-params.c b/test/cases/semantics/confirm/exclusive-switch-out-params.c index c8e853c4..046db5ce 100644 --- a/test/cases/semantics/confirm/exclusive-switch-out-params.c +++ b/test/cases/semantics/confirm/exclusive-switch-out-params.c @@ -3,7 +3,6 @@ // its own fresh string through one of three out-parameters (the summary // keeps them as possible stores), so no two of a, b and c hold one object. // CLEAN -// ALLOW: double-free // RUN-INPUT: // RUN-INPUT: x // RUN-INPUT: x y diff --git a/test/cases/semantics/confirm/munmap-constant-pages.c b/test/cases/semantics/confirm/munmap-constant-pages.c index 309816fe..86b9bdbb 100644 --- a/test/cases/semantics/confirm/munmap-constant-pages.c +++ b/test/cases/semantics/confirm/munmap-constant-pages.c @@ -5,7 +5,6 @@ // pointer anywhere in the mapping, and the middle stays usable. What remains // are possible findings, hence the ALLOW. // CLEAN -// ALLOW: double-free use-after-free #include #include diff --git a/test/cases/semantics/confirm/munmap-hint-address.c b/test/cases/semantics/confirm/munmap-hint-address.c index 3ba033e4..e08e77a6 100644 --- a/test/cases/semantics/confirm/munmap-hint-address.c +++ b/test/cases/semantics/confirm/munmap-hint-address.c @@ -5,7 +5,6 @@ // warning (RFC 0034, Diagnostics); and the leak report the runner asks for // (-Wweavec-leak) takes map_at's MAP_FAILED return for a mapping. // CLEAN -// ALLOW: use-after-free leak /* LMDB mdb_env_set_mapsize: after munmap, the old base address is passed * as an address hint to mmap (never dereferenced). weavec-cc reports * use-after-free and fails the build. */ diff --git a/test/cases/semantics/confirm/munmap-partial-range.c b/test/cases/semantics/confirm/munmap-partial-range.c index 05e71dde..b1e3063a 100644 --- a/test/cases/semantics/confirm/munmap-partial-range.c +++ b/test/cases/semantics/confirm/munmap-partial-range.c @@ -6,7 +6,6 @@ // Diagnostics), and so may the possible invalid release of a range that does // not start a mapping (munmap takes any page-aligned range). // CLEAN -// ALLOW: double-free use-after-free invalid-release /* libdeflate programs/test_util.c alloc_guarded_buffer: munmap of part of a * mapping is modelled as a release of the whole mapping, so unmapping the two * guard pages is a "double free" and the middle page is "used after free". diff --git a/test/cases/semantics/confirm/munmap-whole-twice.c b/test/cases/semantics/confirm/munmap-whole-twice.c deleted file mode 100644 index 037b9a60..00000000 --- a/test/cases/semantics/confirm/munmap-whole-twice.c +++ /dev/null @@ -1,21 +0,0 @@ -// RFC 0034 section 6.3 (partial unmapping): the other side of -// munmap-partial-range.c. A munmap whose range covers the whole mapping from -// its start still releases it, so unmapping it again, or using it after, is -// a definite error. (A failed mmap returns MAP_FAILED, whose munmap fails -// with EINVAL.) -#include -#include - -void twice(size_t len) { - char *map = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, - -1, 0); - munmap(map, len); - munmap(map, len); // BUG: double-free definite -} - -int after(void) { - char *map = mmap(NULL, 4096 * 4, PROT_READ | PROT_WRITE, - MAP_PRIVATE | MAP_ANON, -1, 0); - munmap(map, 4096 * 4); - return map[0]; // BUG: use-after-free definite -} diff --git a/test/cases/semantics/confirm/release-by-flag-stream.c b/test/cases/semantics/confirm/release-by-flag-stream.c index 026810ae..d83a0865 100644 --- a/test/cases/semantics/confirm/release-by-flag-stream.c +++ b/test/cases/semantics/confirm/release-by-flag-stream.c @@ -4,7 +4,6 @@ // Applied together, the family is unknown: neither free() nor fclose() of the // result is a definite mismatched release. // CLEAN -// ALLOW: mismatched-release // RUN-INPUT: // RUN-INPUT: mem #include diff --git a/test/cases/semantics/confirm/release-by-flag.c b/test/cases/semantics/confirm/release-by-flag.c index c2c1a57c..5416dde7 100644 --- a/test/cases/semantics/confirm/release-by-flag.c +++ b/test/cases/semantics/confirm/release-by-flag.c @@ -4,7 +4,6 @@ // list, and the caller releases each with its own releaser by the flag. An // unconfirmed candidate may remain a warning (RFC 0034, Diagnostics). // CLEAN -// ALLOW: mismatched-release // RUN-INPUT: // RUN-INPUT: unix /* Janet net.c: janet_get_addrinfo returns either a calloc'd sockaddr_un diff --git a/test/cases/semantics/confirm/shared-out-params.c b/test/cases/semantics/confirm/shared-out-params.c deleted file mode 100644 index 39ab752f..00000000 --- a/test/cases/semantics/confirm/shared-out-params.c +++ /dev/null @@ -1,34 +0,0 @@ -// RFC 0034 section 6.3 (exclusive stores of one fresh object), the -// counterpart of exclusive-out-params.c: share() stores one fresh object -// through both out-parameters on every path, so a and b hold the same -// object and freeing both is a definite double free. So is a call whose -// argument decides parse()'s exclusive stores. -#include -#include - -static void share(char **a, char **b) { - char *p = malloc(4); - *a = p; - *b = p; -} - -static void parse(int which, char **a, char **b) { - if (which) *a = strdup("a"); - else *b = strdup("b"); -} - -int shared(void) { - char *a = NULL, *b = NULL; - share(&a, &b); - free(a); - free(b); // BUG: double-free definite - return 0; -} - -int decided(void) { - char *a = NULL, *b = NULL; - parse(1, &a, &b); - char *c = a; - free(a); - return *c; // BUG: use-after-free definite -} diff --git a/test/cases/semantics/confirm/unreachable-after-assert.c b/test/cases/semantics/confirm/unreachable-after-assert.c index 5ca5f846..d247b6fa 100644 --- a/test/cases/semantics/confirm/unreachable-after-assert.c +++ b/test/cases/semantics/confirm/unreachable-after-assert.c @@ -6,7 +6,6 @@ // edge is infeasible in every run, the replay's too. An unconfirmed // candidate may remain a warning (RFC 0034, Diagnostics), hence the ALLOW. // CLEAN -// ALLOW: null-dereference /* QuickJS __JS_EvalInternal (quickjs.c:37449-37484) in CONFIG_CHECK_JSVALUE * mode: the DIRECT branch ends in an assert that can never hold there, so * only the else branch (b = NULL, eval_type != DIRECT) survives; the later diff --git a/test/cases/semantics/dropin/fam-strlen-copy_bug.c b/test/cases/semantics/dropin/fam-strlen-copy_bug.c index a2f02ce0..2d5d4c32 100644 --- a/test/cases/semantics/dropin/fam-strlen-copy_bug.c +++ b/test/cases/semantics/dropin/fam-strlen-copy_bug.c @@ -6,7 +6,7 @@ #include struct message { size_t len; char data[]; }; static void put(struct message *m, const char *p) { - memcpy(m->data, p, strlen(p)); // BUG: out-of-bounds // TRAP + memcpy(m->data, p, strlen(p)); // TRAP } int main(int argc, char **argv) { if (argc < 2) return 2; diff --git a/test/cases/semantics/dropin/fortify-inline-wrapper_bug.c b/test/cases/semantics/dropin/fortify-inline-wrapper_bug.c index 41eba086..3c411456 100644 --- a/test/cases/semantics/dropin/fortify-inline-wrapper_bug.c +++ b/test/cases/semantics/dropin/fortify-inline-wrapper_bug.c @@ -10,7 +10,7 @@ #include struct packet { size_t len; unsigned char *payload; }; static void fill(struct packet *pk, const unsigned char *src) { - memcpy(pk->payload, src, pk->len); // BUG: out-of-bounds // MISS: the analysis does not report a call's argument against the callee's requirement (RFC 0035 §8); the guard stops in the callee + memcpy(pk->payload, src, pk->len); } int main(int argc, char **argv) { static unsigned char src[128]; diff --git a/test/cases/semantics/dropin/fortify-memcpy_bug.c b/test/cases/semantics/dropin/fortify-memcpy_bug.c index 96d90a3a..3b5bfd54 100644 --- a/test/cases/semantics/dropin/fortify-memcpy_bug.c +++ b/test/cases/semantics/dropin/fortify-memcpy_bug.c @@ -7,7 +7,7 @@ #include struct packet { size_t len; unsigned char *payload; }; static void fill(struct packet *pk, const unsigned char *src) { - memcpy(pk->payload, src, pk->len); // BUG: out-of-bounds // TRAP + memcpy(pk->payload, src, pk->len); // TRAP } int main(int argc, char **argv) { static unsigned char src[128]; diff --git a/test/cases/semantics/dropin/fortify-strcat_bug.c b/test/cases/semantics/dropin/fortify-strcat_bug.c index 50759f48..53064263 100644 --- a/test/cases/semantics/dropin/fortify-strcat_bug.c +++ b/test/cases/semantics/dropin/fortify-strcat_bug.c @@ -7,7 +7,7 @@ #include struct builder { char *data; size_t cap; }; static void append(struct builder *b, const char *s) { - strcat(b->data, s); // BUG: out-of-bounds // TRAP + strcat(b->data, s); // TRAP } int main(int argc, char **argv) { struct builder b; diff --git a/test/cases/semantics/dropin/free-while-copied_ok.c b/test/cases/semantics/dropin/free-while-copied_ok.c index abc8907c..0719d1c6 100644 --- a/test/cases/semantics/dropin/free-while-copied_ok.c +++ b/test/cases/semantics/dropin/free-while-copied_ok.c @@ -3,7 +3,6 @@ // (msgpack-c's msgpack_vrefbuffer_init). A warning, not an error. // STAGE: S8 // CLEAN -// ALLOW: conflicting-borrow // RUN-INPUT: 1 // RUN-INPUT: 1 fail #include diff --git a/test/cases/semantics/dropin/lowered-violation_bug.c b/test/cases/semantics/dropin/lowered-violation_bug.c index e4971ce5..d7835d59 100644 --- a/test/cases/semantics/dropin/lowered-violation_bug.c +++ b/test/cases/semantics/dropin/lowered-violation_bug.c @@ -9,7 +9,7 @@ static int run(int n) { if (p == NULL) return 1; *p = n; free(p); - if (n > 0) return *p; // BUG: use-after-free // TRAP + if (n > 0) return *p; // TRAP return 0; } int main(int argc, char **argv) { return run(argc > 1 ? atoi(argv[1]) : 0); } diff --git a/test/cases/semantics/dropin/lowered-violation_ok.c b/test/cases/semantics/dropin/lowered-violation_ok.c index 3887749d..791ee384 100644 --- a/test/cases/semantics/dropin/lowered-violation_ok.c +++ b/test/cases/semantics/dropin/lowered-violation_ok.c @@ -1,6 +1,5 @@ // RFC 0033 (V): a lowered definite violation on a path the program never takes does not trap. -// STAGE: S1 -// FLAGS: -Wno-error=weavec-use-after-free +// CLEAN // RUN-INPUT: 0 #include static int run(int n) { @@ -8,7 +7,7 @@ static int run(int n) { if (p == NULL) return 1; *p = n; free(p); - if (n > 0) return *p; // BUG: use-after-free + if (n > 0) return *p; return 0; } int main(int argc, char **argv) { return run(argc > 1 ? atoi(argv[1]) : 0); } diff --git a/test/cases/semantics/dropin/memset-callee_bug.c b/test/cases/semantics/dropin/memset-callee_bug.c deleted file mode 100644 index aa956da6..00000000 --- a/test/cases/semantics/dropin/memset-callee_bug.c +++ /dev/null @@ -1,10 +0,0 @@ -// RFC 0033 §1: the same callee handed a smaller array is still a definite violation. -// STAGE: S1 -// TOOL -#include -static void clear_ints(int *p) { memset(p, 0, 4 * sizeof(int)); } -int main(void) { - int a[3] = {1, 2, 3}; - clear_ints(a); // BUG: out-of-bounds definite - return a[2]; -} diff --git a/test/cases/semantics/dropin/printf-freed_bug.c b/test/cases/semantics/dropin/printf-freed_bug.c index 5168594b..13c38cc9 100644 --- a/test/cases/semantics/dropin/printf-freed_bug.c +++ b/test/cases/semantics/dropin/printf-freed_bug.c @@ -8,7 +8,7 @@ struct session { char user[16]; int id; }; static void audit(struct session *s, int ended) { if (ended) free(s); - printf("audit: %s\n", s->user); // BUG: use-after-free possible // TRAP + printf("audit: %s\n", s->user); // TRAP } int main(int argc, char **argv) { struct session *s = calloc(1, sizeof *s); diff --git a/test/cases/semantics/dropin/printf-pointer-freed_bug.c b/test/cases/semantics/dropin/printf-pointer-freed_bug.c index af92d2f2..3e11cf18 100644 --- a/test/cases/semantics/dropin/printf-pointer-freed_bug.c +++ b/test/cases/semantics/dropin/printf-pointer-freed_bug.c @@ -15,6 +15,6 @@ int main(int argc, char **argv) { if (name == NULL) return 1; strcpy(name, "carol"); drop(name, atoi(argv[1])); - show(stdout, name); // BUG: use-after-free possible + show(stdout, name); return 0; } diff --git a/test/cases/semantics/dropin/raw-declared_bug.c b/test/cases/semantics/dropin/raw-declared_bug.c deleted file mode 100644 index 888188f3..00000000 --- a/test/cases/semantics/dropin/raw-declared_bug.c +++ /dev/null @@ -1,8 +0,0 @@ -// RFC 0033 §2: a pointer declared WEAVEC_RAW is still raw, and dereferencing it outside an -// unsafe region is still an error. -// STAGE: S1 -// TOOL -#include -int read_raw(int *WEAVEC_RAW p) { - return *p; // BUG: unsafe-operation definite -} diff --git a/test/cases/semantics/dropin/strdup-overflow_bug.c b/test/cases/semantics/dropin/strdup-overflow_bug.c index 84065182..ca2911f7 100644 --- a/test/cases/semantics/dropin/strdup-overflow_bug.c +++ b/test/cases/semantics/dropin/strdup-overflow_bug.c @@ -8,7 +8,7 @@ #include static void scrub(char *p, int n) { if (getenv("WEAVEC_CASE_KEEP") != NULL) return; - p[n] = '*'; // BUG: out-of-bounds // TRAP + p[n] = '*'; // TRAP } int main(int argc, char **argv) { if (argc < 2) return 2; diff --git a/test/cases/semantics/dropin/strdup-uaf_bug.c b/test/cases/semantics/dropin/strdup-uaf_bug.c index 3f63378e..27a3cd34 100644 --- a/test/cases/semantics/dropin/strdup-uaf_bug.c +++ b/test/cases/semantics/dropin/strdup-uaf_bug.c @@ -14,5 +14,5 @@ int main(int argc, char **argv) { char *path = strdup("/tmp/weavec"); if (path == NULL) return 1; drop(path, atoi(argv[1])); - return read_at(path, 1); // BUG: use-after-free possible + return read_at(path, 1); } diff --git a/test/cases/semantics/dropin/trap-signals-blocked_bug.c b/test/cases/semantics/dropin/trap-signals-blocked_bug.c index 28a3bf7d..f95563d7 100644 --- a/test/cases/semantics/dropin/trap-signals-blocked_bug.c +++ b/test/cases/semantics/dropin/trap-signals-blocked_bug.c @@ -5,7 +5,7 @@ #include #include static int at(int *p, int i) { - return p[i]; // BUG: out-of-bounds // TRAP + return p[i]; // TRAP } int main(int argc, char **argv) { sigset_t all; diff --git a/test/cases/semantics/dropin/void-cast-store_bug.c b/test/cases/semantics/dropin/void-cast-store_bug.c index dd18bb2b..a4787042 100644 --- a/test/cases/semantics/dropin/void-cast-store_bug.c +++ b/test/cases/semantics/dropin/void-cast-store_bug.c @@ -13,5 +13,5 @@ int main(int argc, char **argv) { val v = {5, argc > 5 ? "x" : NULL}; const char *s = "valid"; take(&v, &s); - return (int)strlen(s); // BUG: null-dereference possible // TRAP + return (int)strlen(s); // TRAP } diff --git a/test/cases/semantics/emission/lowered-out-of-bounds.c b/test/cases/semantics/emission/lowered-out-of-bounds.c index 3af67203..691528ee 100644 --- a/test/cases/semantics/emission/lowered-out-of-bounds.c +++ b/test/cases/semantics/emission/lowered-out-of-bounds.c @@ -13,7 +13,7 @@ int main(void) { char *p = malloc(4); if (!p) return 1; - p[4] = 0; // BUG: out-of-bounds possible // TRAP + p[4] = 0; // TRAP free(p); return 0; } diff --git a/test/cases/semantics/emission/lowered-use-after-free-no-runtime.c b/test/cases/semantics/emission/lowered-use-after-free-no-runtime.c index 8beb6ce3..3e08840c 100644 --- a/test/cases/semantics/emission/lowered-use-after-free-no-runtime.c +++ b/test/cases/semantics/emission/lowered-use-after-free-no-runtime.c @@ -10,6 +10,6 @@ int main(void) { char *p = malloc(8); if (!p) return 1; free(p); - p[0] = 1; // BUG: use-after-free possible + p[0] = 1; // TRAP: heap-use-after-free return 0; } diff --git a/test/cases/semantics/emission/lowered-use-after-free.c b/test/cases/semantics/emission/lowered-use-after-free.c index 10c7a383..1e15a75b 100644 --- a/test/cases/semantics/emission/lowered-use-after-free.c +++ b/test/cases/semantics/emission/lowered-use-after-free.c @@ -12,6 +12,6 @@ int main(void) { char *p = malloc(8); if (!p) return 1; free(p); - p[0] = 1; // BUG: use-after-free possible // TRAP + p[0] = 1; // TRAP return 0; } diff --git a/test/cases/semantics/emission/qsort-wrapping-size.c b/test/cases/semantics/emission/qsort-wrapping-size.c index 1b8ce61d..4de7e5a1 100644 --- a/test/cases/semantics/emission/qsort-wrapping-size.c +++ b/test/cases/semantics/emission/qsort-wrapping-size.c @@ -17,6 +17,6 @@ static int cmp(const void *a, const void *b) { int main(int argc, char **argv) { int xs[4] = {4, 3, 2, 1}; size_t n = argc > 1 ? (size_t)strtoull(argv[1], NULL, 10) : 4; - qsort(xs, n, sizeof xs[0], cmp); // BUG: out-of-bounds // TRAP + qsort(xs, n, sizeof xs[0], cmp); // TRAP return xs[0]; } diff --git a/test/cases/semantics/emission/span-difference_bug.c b/test/cases/semantics/emission/span-difference_bug.c index c54f788e..7b9c11e5 100644 --- a/test/cases/semantics/emission/span-difference_bug.c +++ b/test/cases/semantics/emission/span-difference_bug.c @@ -7,10 +7,10 @@ struct box { char *s; long n; }; static int last(struct box *b) { char *e = &b->s[b->n]; return e[-1]; } static int back(struct box *b, long k) { - return *(b->s - k); // BUG: out-of-bounds // TRAP + return *(b->s - k); // TRAP } static int before(struct box *b, long i) { - return *(b->s + i - 1); // BUG: out-of-bounds // TRAP + return *(b->s + i - 1); // TRAP } int main(int argc, char **argv) { struct box b; diff --git a/test/cases/semantics/examples/assumption-checked.c b/test/cases/semantics/examples/assumption-checked.c deleted file mode 100644 index 2fa39d85..00000000 --- a/test/cases/semantics/examples/assumption-checked.c +++ /dev/null @@ -1,14 +0,0 @@ -// RFC 0030 §4 "Assumptions" (a): an assumption the engine cannot prove becomes a checked assertion. -// STAGE: S3 -// The assertion facet of the Assume site is checked, emitted as -// __weavec_chk_assert((n > 0) != 0). The run passes n == 0, so the assertion traps. -// RUN-INPUT: -#include - -int a(char *b, int n) { WEAVEC_ASSUME(n > 0); return b[n - 1]; } // TRAP - -int main(int argc, char **argv) { - char buf[4] = {0}; - (void)argv; - return a(buf, argc - 1); -} diff --git a/test/cases/semantics/examples/assumption-contradicted.c b/test/cases/semantics/examples/assumption-contradicted.c deleted file mode 100644 index 317fa352..00000000 --- a/test/cases/semantics/examples/assumption-contradicted.c +++ /dev/null @@ -1,16 +0,0 @@ -// RFC 0030 §4 "Assumptions" (c): an assumption the engine refutes is an error. -// STAGE: S3 -// "assumption 'i < 4' is false here [weavec::contradicted-assumption]". The analysis still -// assumes 'i < 4' after the site, so b[i] reports nothing more. ASan (without WeaveC, where -// the macro compiles to nothing) confirms the out-of-bounds read. -// ASAN -#include - -int c(void) { - char b[4] = {0}; - int i = 10; - WEAVEC_ASSUME(i < 4); // BUG: contradicted-assumption definite - return b[i]; // TRAP -} - -int main(void) { return c(); } diff --git a/test/cases/semantics/examples/checked-index.c b/test/cases/semantics/examples/checked-index.c index b66713a8..4be7485a 100644 --- a/test/cases/semantics/examples/checked-index.c +++ b/test/cases/semantics/examples/checked-index.c @@ -7,9 +7,8 @@ // RUN-INPUT: 4 // RUN-INPUT: 0 null #include -#include "../Inputs/rfc0030.h" -int at(const int *WEAVEC_COUNTED_BY(n) p, size_t n, size_t i) { return p[i]; } // TRAP // TRAP +int at(const int * p, size_t n, size_t i) { return p[i]; } // TRAP // TRAP int main(int argc, char **argv) { int xs[4] = {1, 2, 3, 4}; diff --git a/test/cases/semantics/examples/proven-static-extent.c b/test/cases/semantics/examples/proven-static-extent.c deleted file mode 100644 index 91ed47ee..00000000 --- a/test/cases/semantics/examples/proven-static-extent.c +++ /dev/null @@ -1,7 +0,0 @@ -// RFC 0030 §4 "Proven": a [static 4] parameter discharges the null and spatial facets. -// STAGE: S6 -// a[0] is a Deref site with null, spatial and temporal proven ([static 4] implies non-null, -// A1); a[3] is an Index site with spatial proven (3 < 4). No code is emitted, so no site of -// the unit is checked, unresolved, trusted or a violation. -// CLEAN -int sum4(const int a[static 4]) { return a[0] + a[3]; } diff --git a/test/cases/semantics/examples/temporal-definite.c b/test/cases/semantics/examples/temporal-definite.c index 52e879fe..137fa06b 100644 --- a/test/cases/semantics/examples/temporal-definite.c +++ b/test/cases/semantics/examples/temporal-definite.c @@ -5,7 +5,7 @@ // ASAN #include -void g(char *p) { free(p); p[0] = 1; } // BUG: use-after-free definite +void g(char *p) { free(p); p[0] = 1; } // TRAP: heap-use-after-free int main(void) { char *p = malloc(8); diff --git a/test/cases/semantics/examples/temporal-possible.c b/test/cases/semantics/examples/temporal-possible.c index cb309de3..421bd6f9 100644 --- a/test/cases/semantics/examples/temporal-possible.c +++ b/test/cases/semantics/examples/temporal-possible.c @@ -8,7 +8,7 @@ // ASAN #include -void h(char *p, int c) { if (c) free(p); p[0] = 1; } // BUG: use-after-free possible // TRAP +void h(char *p, int c) { if (c) free(p); p[0] = 1; } // TRAP int main(int argc, char **argv) { char *p = malloc(8); diff --git a/test/cases/semantics/examples/trusted-concurrency.c b/test/cases/semantics/examples/trusted-concurrency.c index f83c00ba..7a3ba2a8 100644 --- a/test/cases/semantics/examples/trusted-concurrency.c +++ b/test/cases/semantics/examples/trusted-concurrency.c @@ -25,6 +25,6 @@ int main(void) { shared[0] = 1; if (pthread_create(&t, NULL, worker, NULL) != 0) return 1; pthread_join(t, NULL); - if (shared) return shared[0]; // BUG: use-after-free + if (shared) return shared[0]; // TRAP: heap-use-after-free return 0; } diff --git a/test/cases/semantics/examples/trusted-system-api.c b/test/cases/semantics/examples/trusted-system-api.c deleted file mode 100644 index 6dd18d56..00000000 --- a/test/cases/semantics/examples/trusted-system-api.c +++ /dev/null @@ -1,14 +0,0 @@ -// RFC 0030 §4 "Trusted, system API": ioctl has no table entry, so its call is trusted. -// STAGE: S3 -// The layout of ioctl's third argument depends on the request, so it has no LibrarySpec -// entry. Declared in a platform header, it borrows its arguments for the call (the address -// of 'ws' is borrowed) and its Call site's temporal facet is trusted(system-api) (§5.2). -// CLEAN -#include - -int cols(int fd) { - struct winsize ws; - if (ioctl(fd, TIOCGWINSZ, &ws) == -1) - return 80; - return ws.ws_col; -} diff --git a/test/cases/semantics/examples/trusted-unsafe.c b/test/cases/semantics/examples/trusted-unsafe.c deleted file mode 100644 index f4ce9970..00000000 --- a/test/cases/semantics/examples/trusted-unsafe.c +++ /dev/null @@ -1,16 +0,0 @@ -// RFC 0030 §4 "Trusted, unsafe": raw operations inside WEAVEC_UNSAFE are trusted, not checked. -// STAGE: S3 -// The IntToPtr site's spatial and temporal facets are trusted(unsafe). 'reg' is converted -// from an integer (since RFC 0033 §2 a pointer of unknown provenance, not a raw one), and -// inside the region every facet of *reg is trusted(unsafe), as for a raw pointer. No checks -// are emitted. Outside the region *reg is guarded (unsafe/raw-outside-region.c). -// CLEAN -#include -#include - -void poke(uintptr_t addr) { - WEAVEC_UNSAFE { - volatile unsigned *reg = (volatile unsigned *)addr; - *reg = 1; - } -} diff --git a/test/cases/semantics/examples/unresolved-unknown-callee-link.c b/test/cases/semantics/examples/unresolved-unknown-callee-link.c index ca388280..92d79050 100644 --- a/test/cases/semantics/examples/unresolved-unknown-callee-link.c +++ b/test/cases/semantics/examples/unresolved-unknown-callee-link.c @@ -13,7 +13,7 @@ int f(void) { char *p = malloc(8); if (!p) return 0; consume(p); - return p[0]; // BUG: use-after-free definite + return p[0]; // TRAP: heap-use-after-free } int main(void) { return f(); } diff --git a/test/cases/semantics/examples/unresolved-unknown-callee.c b/test/cases/semantics/examples/unresolved-unknown-callee.c deleted file mode 100644 index d528a69f..00000000 --- a/test/cases/semantics/examples/unresolved-unknown-callee.c +++ /dev/null @@ -1,16 +0,0 @@ -// RFC 0030 §4 "Unresolved, unknown callee": a use after an unknown callee is not proven. -// STAGE: S3 -// 'consume' has no body, summary, table entry or ownership contract, so it may have freed -// 'p' (§5.1). Its Call site and the later p[0] have temporal unresolved(unknown-callee), and -// there is no diagnostic. The link half of the example is unresolved-unknown-callee-link.c. -// CLEAN -#include - -void consume(char *p); - -int f(void) { - char *p = malloc(8); - if (!p) return 0; - consume(p); - return p[0]; -} diff --git a/test/cases/semantics/examples/unresolved-unknown-extent.c b/test/cases/semantics/examples/unresolved-unknown-extent.c deleted file mode 100644 index 41b076cf..00000000 --- a/test/cases/semantics/examples/unresolved-unknown-extent.c +++ /dev/null @@ -1,12 +0,0 @@ -// RFC 0030 §4 "Unresolved, unknown extent": no check is written against a lower bound. -// STAGE: S3 -// 'b' is Single-or-nullable under A3 (§7.3). Single is a lower bound that covers element 0 -// only, so b[1000] has spatial unresolved(unknown-extent), null checked (nonnull) and -// temporal proven. The call is a Call site whose temporal facet is unresolved(unknown-callee) -// and carries a fix-it suggestion in the ledger (site 0 of 'f'). -char *get_buffer(void); /* no definition, entry or annotation */ - -int f(void) { - char *b = get_buffer(); - return b[1000]; // TRAP -} diff --git a/test/cases/semantics/examples/violation-null.c b/test/cases/semantics/examples/violation-null.c index 4a364a92..df525867 100644 --- a/test/cases/semantics/examples/violation-null.c +++ b/test/cases/semantics/examples/violation-null.c @@ -7,7 +7,7 @@ struct node { struct node *next; int v; }; -int k(struct node *n) { if (n == NULL) return n->v; return 0; } // BUG: null-dereference definite +int k(struct node *n) { if (n == NULL) return n->v; return 0; } // TRAP: null-dereference int main(int argc, char **argv) { struct node x = {NULL, 1}; diff --git a/test/cases/semantics/examples/violation-spatial.c b/test/cases/semantics/examples/violation-spatial.c index 5b549d4a..892e34e5 100644 --- a/test/cases/semantics/examples/violation-spatial.c +++ b/test/cases/semantics/examples/violation-spatial.c @@ -5,7 +5,7 @@ // ASAN #include -void f(void) { char *p = malloc(4); if (!p) return; p[4] = 0; free(p); } // BUG: out-of-bounds definite +void f(void) { char *p = malloc(4); if (!p) return; p[4] = 0; free(p); } // TRAP: heap-buffer-overflow int main(void) { f(); diff --git a/test/cases/semantics/extents/null-after-checked-access.c b/test/cases/semantics/extents/null-after-checked-access.c deleted file mode 100644 index 427fd7c5..00000000 --- a/test/cases/semantics/extents/null-after-checked-access.c +++ /dev/null @@ -1,26 +0,0 @@ -// RFC 0030 §3.2: after a checked access the pointer is not null, in every -// block after it. -// STAGE: S7 -// `s[-1]` checks `s` against null (the check traps on null); the header read -// in the `switch` case that follows, in another block, is then proven, as sds's -// `sdslen` is. The refinement is made in every pass of the analysis, so the -// blocks after the access start from it. -// CLEAN -// TOOL -#include - -struct hdr8 { - unsigned char len; - unsigned char alloc; - unsigned char flags; -}; - -size_t length(const char *s) { - unsigned char flags = (unsigned char)s[-1]; - switch (flags & 7) { - case 1: - return ((const struct hdr8 *)(s - sizeof(struct hdr8)))->len; - default: - return 0; - } -} diff --git a/test/cases/semantics/extents/subarray-subscript.c b/test/cases/semantics/extents/subarray-subscript.c index 9de85e67..beadf429 100644 --- a/test/cases/semantics/extents/subarray-subscript.c +++ b/test/cases/semantics/extents/subarray-subscript.c @@ -9,7 +9,7 @@ int get(int i, int j) { int m[3][4] = {{0}}; - return m[i][j]; // BUG: out-of-bounds // TRAP + return m[i][j]; // TRAP } int main(int argc, char **argv) { diff --git a/test/cases/semantics/kinds/attr-counted-by.c b/test/cases/semantics/kinds/attr-counted-by.c index 77490dba..6739a460 100644 --- a/test/cases/semantics/kinds/attr-counted-by.c +++ b/test/cases/semantics/kinds/attr-counted-by.c @@ -9,7 +9,7 @@ struct pkt { int len; char *buf __attribute__((counted_by(len))); }; -char at(const struct pkt *p, int i) { return p->buf[i]; } // BUG: out-of-bounds // TRAP +char at(const struct pkt *p, int i) { return p->buf[i]; } // TRAP int main(int argc, char **argv) { char storage[4] = "abc"; diff --git a/test/cases/semantics/kinds/attr-ownership.c b/test/cases/semantics/kinds/attr-ownership.c deleted file mode 100644 index 54368638..00000000 --- a/test/cases/semantics/kinds/attr-ownership.c +++ /dev/null @@ -1,22 +0,0 @@ -// RFC 0030 §7.2 (malloc, ownership_returns, ownership_takes, ownership_holds): ownership attributes are contracts. -// STAGE: S6 -// The functions are defined elsewhere, but each pointer parameter carries an ownership -// attribute, so their contracts apply and their Call sites' temporal facets are -// trusted(extern-contract) (§5.1): 'pool_get' returns a fresh object of family 'pool', -// 'pool_keep' retains its argument and 'pool_put' releases it. The use after 'pool_put' -// is a definite use-after-free. -#include - -void *pool_get(size_t n) __attribute__((ownership_returns(pool))); -void pool_keep(void *p) __attribute__((ownership_holds(pool, 1))); -void pool_put(void *p) __attribute__((ownership_takes(pool, 1))); - -void hold(char *q) { pool_keep(q); } - -int use(void) { - char *p = pool_get(8); - if (p == NULL) return 0; - p[0] = 1; - pool_put(p); - return p[0]; // BUG: use-after-free definite -} diff --git a/test/cases/semantics/kinds/cursor-through-char-pp.c b/test/cases/semantics/kinds/cursor-through-char-pp.c index af46925d..39ec1601 100644 --- a/test/cases/semantics/kinds/cursor-through-char-pp.c +++ b/test/cases/semantics/kinds/cursor-through-char-pp.c @@ -14,7 +14,7 @@ struct scanner { char *pos; }; static void advance(char **pp, int k) { *pp += k; } -int peekc(struct scanner *s) { return s->pos[0]; } // BUG: out-of-bounds // TRAP +int peekc(struct scanner *s) { return s->pos[0]; } // TRAP int main(int argc, char **argv) { struct scanner s; diff --git a/test/cases/semantics/kinds/declared-counted-by-field.c b/test/cases/semantics/kinds/declared-counted-by-field.c index 327fa403..391ed918 100644 --- a/test/cases/semantics/kinds/declared-counted-by-field.c +++ b/test/cases/semantics/kinds/declared-counted-by-field.c @@ -5,11 +5,10 @@ // RUN-INPUT: 8 // ASAN #include -#include "../Inputs/rfc0030.h" -struct buf { char *WEAVEC_COUNTED_BY(cap) data; size_t cap; }; +struct buf { char * data; size_t cap; }; -char at(const struct buf *b, size_t i) { return b->data[i]; } // BUG: out-of-bounds // TRAP +char at(const struct buf *b, size_t i) { return b->data[i]; } // TRAP int main(int argc, char **argv) { char storage[8] = "abcdefg"; diff --git a/test/cases/semantics/kinds/declared-counted-by-param.c b/test/cases/semantics/kinds/declared-counted-by-param.c index 5cac1d7a..de4c0d1b 100644 --- a/test/cases/semantics/kinds/declared-counted-by-param.c +++ b/test/cases/semantics/kinds/declared-counted-by-param.c @@ -7,9 +7,8 @@ // RUN-INPUT: 5 // ASAN #include -#include "../Inputs/rfc0030.h" -int sum(const int *WEAVEC_COUNTED_BY(n) p, size_t n) { +int sum(const int * p, size_t n) { int s = 0; for (size_t i = 0; i < n; i++) s += p[i]; // TRAP return s; @@ -18,5 +17,5 @@ int sum(const int *WEAVEC_COUNTED_BY(n) p, size_t n) { int main(int argc, char **argv) { int a[4] = {1, 2, 3, 4}; size_t n = argc > 1 ? (size_t)atoi(argv[1]) : 4; - return sum(a, n); // BUG: out-of-bounds // MISS: the analysis does not report a call's argument against the callee's requirement (RFC 0035 §8); the guard stops in the callee + return sum(a, n); } diff --git a/test/cases/semantics/kinds/declared-ended-by.c b/test/cases/semantics/kinds/declared-ended-by.c deleted file mode 100644 index b2f70df4..00000000 --- a/test/cases/semantics/kinds/declared-ended-by.c +++ /dev/null @@ -1,18 +0,0 @@ -// RFC 0030 §7.2 (WEAVEC_ENDED_BY): [p, end) must lie in one object, which every call must meet. -// STAGE: S6 -// 'p' is EndedBy(end). The first two calls pass ranges inside 'int a[4]'. The third passes -// [a, a + 5): the argument's exact extent is below the declared requirement for every -// value, so the call is a definite out-of-bounds error (§3.3). -#include "../Inputs/rfc0030.h" - -int sum_range(const int *WEAVEC_ENDED_BY(end) p, const int *end) { - int s = 0; - for (; p < end; p++) s += *p; - return s; -} - -int total(void) { - int a[4] = {1, 2, 3, 4}; - int s = sum_range(a, a) + sum_range(a, a + 4); - return s + sum_range(a, a + 5); // BUG: out-of-bounds definite -} diff --git a/test/cases/semantics/kinds/declared-invalid-name.c b/test/cases/semantics/kinds/declared-invalid-name.c deleted file mode 100644 index bef6f2e3..00000000 --- a/test/cases/semantics/kinds/declared-invalid-name.c +++ /dev/null @@ -1,15 +0,0 @@ -// RFC 0030 §7.2: a WEAVEC_COUNTED_BY name that is not a sibling is an invalid annotation. -// STAGE: S6 -// The argument must name a sibling parameter or field; 'count' names neither, so the warning -// is "'count' in WEAVEC_COUNTED_BY does not name a parameter or field" and the kind is -// dropped: p keeps the A1 default, and p[i] is never checked. It is not unresolved either: -// the unguarded p[i] is a must-access that gives Counted(i + 1) (§7.5 R5), and 'at' is -// exported, so the access that requirement covers is trusted(caller-contract). -#include -#include "../Inputs/rfc0030.h" - -int at(const int *WEAVEC_COUNTED_BY(count) p, // BUG: invalid-annotation possible - size_t n, size_t i) { - (void)n; - return p[i]; -} diff --git a/test/cases/semantics/kinds/declared-nonnull-definite.c b/test/cases/semantics/kinds/declared-nonnull-definite.c deleted file mode 100644 index 00a3c708..00000000 --- a/test/cases/semantics/kinds/declared-nonnull-definite.c +++ /dev/null @@ -1,10 +0,0 @@ -// RFC 0030 §7.2 (WEAVEC_NONNULL on a parameter): a definitely-null argument is still an error. -// STAGE: S3 -// A null pointer constant passed for a WEAVEC_NONNULL parameter is null on every path and -// not from an allocator, so it is a definite null-dereference error at the call (§3.2). -#include -#include - -int get(const int *WEAVEC_NONNULL p) { return *p; } - -int use(void) { return get(NULL); } // BUG: null-dereference definite diff --git a/test/cases/semantics/kinds/declared-nonnull.c b/test/cases/semantics/kinds/declared-nonnull.c index a4d85f82..82776cd2 100644 --- a/test/cases/semantics/kinds/declared-nonnull.c +++ b/test/cases/semantics/kinds/declared-nonnull.c @@ -1,4 +1,4 @@ -// RFC 0030 §7.2 (WEAVEC_NONNULL on a parameter): a possibly-null argument is checked at the call. +// RFC 0030 §7.2 ( on a parameter): a possibly-null argument is checked at the call. // STAGE: S6 // The declaration makes 'p' non-null: the dereference inside 'get' is proven, and a call // whose argument has unknown nullness wraps it in the nonnull check instead of reporting it @@ -6,12 +6,11 @@ // RUN-INPUT: // ASAN #include -#include -int get(const int *WEAVEC_NONNULL p) { return *p; } // TRAP +int get(const int * p) { return *p; } // TRAP int main(int argc, char **argv) { static const int x = 7; (void)argv; - return get(argc > 1 ? &x : NULL); // BUG: null-dereference // MISS: the analysis does not report a call's argument against the callee's requirement (RFC 0035 §8); the guard stops in the callee + return get(argc > 1 ? &x : NULL); } diff --git a/test/cases/semantics/kinds/declared-sized-by.c b/test/cases/semantics/kinds/declared-sized-by.c index 4a9dcf6c..a539e0bb 100644 --- a/test/cases/semantics/kinds/declared-sized-by.c +++ b/test/cases/semantics/kinds/declared-sized-by.c @@ -7,13 +7,12 @@ // ASAN #include #include -#include -void fill(void *WEAVEC_SIZED_BY(n) dst, size_t n) { memset(dst, 'x', n); } // TRAP +void fill(void * dst, size_t n) { memset(dst, 'x', n); } // TRAP int main(int argc, char **argv) { char buf[8]; size_t n = argc > 1 ? (size_t)atoi(argv[1]) : sizeof buf; - fill(buf, n); // BUG: out-of-bounds // MISS: the analysis does not report a call's argument against the callee's requirement (RFC 0035 §8); the guard stops in the callee + fill(buf, n); return buf[0] == 'x' ? 0 : 1; } diff --git a/test/cases/semantics/kinds/declared-static-array.c b/test/cases/semantics/kinds/declared-static-array.c deleted file mode 100644 index cb14222d..00000000 --- a/test/cases/semantics/kinds/declared-static-array.c +++ /dev/null @@ -1,10 +0,0 @@ -// RFC 0030 §7.2 (T p[static N]): the parameter is Counted(N) and non-null, required at every call. -// STAGE: S6 -// An argument whose exact extent is below the declared requirement is a violation at the -// call (§3.3): an int[3] passed for [static 4] is a definite out-of-bounds error. -int sum4(const int a[static 4]) { return a[0] + a[3]; } - -int three(void) { - int b[3] = {1, 2, 3}; - return sum4(b); // BUG: out-of-bounds definite -} diff --git a/test/cases/semantics/kinds/declared-string.c b/test/cases/semantics/kinds/declared-string.c index 14c74d36..e6bfaad9 100644 --- a/test/cases/semantics/kinds/declared-string.c +++ b/test/cases/semantics/kinds/declared-string.c @@ -1,4 +1,4 @@ -// RFC 0030 §7.2 (WEAVEC_STRING): a NUL-terminated requirement is checked at every call. +// RFC 0030 §7.2 (): a NUL-terminated requirement is checked at every call. // STAGE: S6 // 's' is NulTerminated. At the call the requirement is checked against the exact extent of // 'b' as __weavec_strnlen(b, 4) < 4 (§10.3 rule 2), a length check with the len template. @@ -6,9 +6,8 @@ // RUN-INPUT: x // ASAN #include -#include "../Inputs/rfc0030.h" -static size_t count_x(const char *WEAVEC_STRING s) { +static size_t count_x(const char * s) { size_t n = 0; for (; *s; s++) // TRAP if (*s == 'x') n++; @@ -19,5 +18,5 @@ int main(int argc, char **argv) { char b[4] = {'a', 'x', 'c', 0}; (void)argv; if (argc > 1) b[3] = 'd'; - return (int)count_x(b); // BUG: out-of-bounds // MISS: the analysis does not report a call's argument against the callee's requirement (RFC 0035 §8); the guard stops in the callee + return (int)count_x(b); } diff --git a/test/cases/semantics/kinds/declared-vla-param.c b/test/cases/semantics/kinds/declared-vla-param.c index e1fd3757..5a87b561 100644 --- a/test/cases/semantics/kinds/declared-vla-param.c +++ b/test/cases/semantics/kinds/declared-vla-param.c @@ -19,5 +19,5 @@ int sumv(size_t n, const int p[n]) { int main(int argc, char **argv) { int a[4] = {1, 2, 3, 4}; size_t n = argc > 1 ? (size_t)atoi(argv[1]) : 4; - return sumv(n, a); // BUG: out-of-bounds // MISS: the analysis does not report a call's argument against the callee's requirement (RFC 0035 §8); the guard stops in the callee + return sumv(n, a); } diff --git a/test/cases/semantics/kinds/relies-on-single-call.c b/test/cases/semantics/kinds/relies-on-single-call.c index cd7cd966..b11b25e0 100644 --- a/test/cases/semantics/kinds/relies-on-single-call.c +++ b/test/cases/semantics/kinds/relies-on-single-call.c @@ -14,5 +14,5 @@ int peek(const int *p) { return p[0]; } // TRAP int main(int argc, char **argv) { int a[4] = {1, 2, 3, 4}; int k = argc > 1 ? atoi(argv[1]) : 0; - return peek(a + k); // BUG: out-of-bounds // MISS: the analysis does not report a call's argument against the callee's requirement (RFC 0035 §8); the guard stops in the callee + return peek(a + k); } diff --git a/test/cases/semantics/kinds/static-cursor-argument.c b/test/cases/semantics/kinds/static-cursor-argument.c index 3bbdc1f1..16ad248b 100644 --- a/test/cases/semantics/kinds/static-cursor-argument.c +++ b/test/cases/semantics/kinds/static-cursor-argument.c @@ -9,7 +9,7 @@ // ASAN static int get(const int *p, int i) { if (i < 0) return 0; - return p[i]; // BUG: out-of-bounds // TRAP + return p[i]; // TRAP } int main(int argc, char **argv) { diff --git a/test/cases/semantics/ledger/budget.c b/test/cases/semantics/ledger/budget.c deleted file mode 100644 index 9b644b5c..00000000 --- a/test/cases/semantics/ledger/budget.c +++ /dev/null @@ -1,15 +0,0 @@ -// RFC 0030 §5.5: a function over its analysis budget takes the default outcomes with reason budget. -// STAGE: S3 -// With a budget of one block transfer 'walk' stops early. Its facets take the §2.6 defaults -// with reason budget: null checked; spatial checked when the extent is exact from the type -// and its terms are constants or unassigned parameters (a[k]), otherwise -// unresolved(budget); temporal unresolved(budget). The function's row and the summary list -// it as over budget. -// FLAGS: -fweavec-budget=1 -int walk(const int *p, int n, int k) { - int a[4] = {0}; - int s = 0; - for (int i = 0; i < n; i++) - s += p[i]; // TRAP - return s + a[k]; // TRAP -} diff --git a/test/cases/semantics/ledger/extern-contract.c b/test/cases/semantics/ledger/extern-contract.c deleted file mode 100644 index 968d82ab..00000000 --- a/test/cases/semantics/ledger/extern-contract.c +++ /dev/null @@ -1,21 +0,0 @@ -// RFC 0030 §5.1: a callee whose pointer parameters all carry ownership contracts applies them. -// STAGE: S3 -// 'inspect' is defined elsewhere, but its only pointer parameter is WEAVEC_BORROWED, an -// ownership contract: the call borrows 'p' and nothing else happens to it. The Call site's -// temporal facet is trusted(extern-contract), and the later use and release are not -// unresolved. -// CLEAN -#include -#include - -void inspect(const char *WEAVEC_BORROWED p); - -int f(void) { - char *p = malloc(8); - if (!p) return 0; - p[0] = 1; - inspect(p); - int v = p[0]; - free(p); - return v; -} diff --git a/test/cases/semantics/ledger/inline-asm.c b/test/cases/semantics/ledger/inline-asm.c deleted file mode 100644 index 5cec8ce7..00000000 --- a/test/cases/semantics/ledger/inline-asm.c +++ /dev/null @@ -1,17 +0,0 @@ -// RFC 0030 §5.7: an asm statement applies the unknown-callee default to its pointer operands. -// STAGE: S3 -// The asm statement takes 'p' as an operand, so it may have released, retained or replaced -// the object (detail "inline assembly"): the later read and the release are -// unresolved(unknown-callee), with no diagnostic. -// CLEAN -#include - -int f(void) { - char *p = malloc(8); - if (!p) return 0; - p[0] = 1; - __asm__ volatile("" : : "r"(p) : "memory"); - int v = p[0]; - free(p); - return v; -} diff --git a/test/cases/semantics/ledger/no-site-unevaluated.c b/test/cases/semantics/ledger/no-site-unevaluated.c deleted file mode 100644 index 8143a71f..00000000 --- a/test/cases/semantics/ledger/no-site-unevaluated.c +++ /dev/null @@ -1,20 +0,0 @@ -// RFC 0030 §2.1: unevaluated operands, '&*p' and the offsetof idiom create no site. -// STAGE: S3 -// 'sizeof *p' and 'sizeof p[3]' are unevaluated, '&*p' creates no site, and -// '&((struct s *)0)->b' is the old offsetof idiom on a null pointer constant. The stores go -// to globals without a declared kind, which are no sites either, so each function's only -// site is its exit at the end of the body: the unit has three sites. -// CLEAN -#include - -struct s { int a; int b; }; - -size_t width_sum; -int *same_pointer; -size_t offset_of_b; - -void widths(int *p) { width_sum = sizeof *p + sizeof p[3]; } - -void same(int *p) { same_pointer = &*p; } - -void offset_b(void) { offset_of_b = (size_t)&((struct s *)0)->b; } diff --git a/test/cases/semantics/ledger/nonnull-destructor.c b/test/cases/semantics/ledger/nonnull-destructor.c deleted file mode 100644 index cf372fe0..00000000 --- a/test/cases/semantics/ledger/nonnull-destructor.c +++ /dev/null @@ -1,21 +0,0 @@ -// RFC 0030 §5.1: a destructor declared only 'nonnull' is still an unknown callee. -// STAGE: S3 -// Nullability and extent attributes never lift the temporal default; only ownership -// contracts do (the Departure of §5.1). 'obj_destroy' may therefore have freed 'o': its -// Call site and the later read are unresolved(unknown-callee) instead of proven as if the -// call only borrowed 'o'. There is no diagnostic. -// CLEAN -#include - -struct obj { int refs; char *name; }; - -void obj_destroy(struct obj *o) __attribute__((nonnull)); - -int f(void) { - struct obj *o = malloc(sizeof *o); - if (!o) return 0; - o->refs = 1; - o->name = NULL; - obj_destroy(o); - return o->refs; -} diff --git a/test/cases/semantics/ledger/setjmp-assigned-after.c b/test/cases/semantics/ledger/setjmp-assigned-after.c index c8ec45be..94a0778f 100644 --- a/test/cases/semantics/ledger/setjmp-assigned-after.c +++ b/test/cases/semantics/ledger/setjmp-assigned-after.c @@ -9,7 +9,6 @@ // unassigned. (The allocation `fail` jumps away from is freed by the // handler, which the model does not follow from the `longjmp`: a leak.) // CLEAN -// ALLOW: leak // RUN-INPUT: #include #include diff --git a/test/cases/semantics/ledger/setjmp.c b/test/cases/semantics/ledger/setjmp.c deleted file mode 100644 index 9a257b04..00000000 --- a/test/cases/semantics/ledger/setjmp.c +++ /dev/null @@ -1,19 +0,0 @@ -// RFC 0030 §5.4: in a function that calls setjmp, every temporal facet is unresolved(setjmp). -// STAGE: S3 -// Values may be stale after a longjmp, so every temporal facet of 'run' is -// unresolved(setjmp) and the ledger marks the function. A null facet that a flow fact about -// a local would prove (the test of 'p') is checked instead. Facets proven by type alone, -// such as a constant index into the local array, stay proven. -#include -#include - -static jmp_buf env; - -int run(char *p) { - char local[4] = {0}; - if (p == NULL) return -1; - if (setjmp(env) != 0) return p[0] + local[1]; // TRAP - p[0] = 1; - local[1] = 2; - longjmp(env, 1); -} diff --git a/test/cases/semantics/ledger/system-api-borrow.c b/test/cases/semantics/ledger/system-api-borrow.c deleted file mode 100644 index cab6461a..00000000 --- a/test/cases/semantics/ledger/system-api-borrow.c +++ /dev/null @@ -1,19 +0,0 @@ -// RFC 0030 §5.2: a platform function without a table entry borrows its arguments; later uses are unaffected. -// STAGE: S3 -// 'ioctl' is declared in a platform header and has no LibrarySpec entry: the call borrows -// 'ws' with no release, retain or store effect, and its temporal facet is -// trusted(system-api). The later read and the release of 'ws' are proven under A2, so no -// temporal facet of the unit is unresolved. -// CLEAN -#include -#include - -int rows(int fd) { - struct winsize *ws = malloc(sizeof *ws); - if (!ws) return 24; - int r = 24; - if (ioctl(fd, TIOCGWINSZ, ws) != -1) - r = ws->ws_row; - free(ws); - return r; -} diff --git a/test/cases/semantics/ledger/unknown-then-free.c b/test/cases/semantics/ledger/unknown-then-free.c deleted file mode 100644 index a0bb18ca..00000000 --- a/test/cases/semantics/ledger/unknown-then-free.c +++ /dev/null @@ -1,18 +0,0 @@ -// RFC 0030 §5.1 and §3.1: a known release after an unknown callee replaces the unknown record. -// STAGE: S3 -// 'consume' is an unknown callee, so it marks 'p' Freed with unknownOrigin. The free after -// it hits that record: its own temporal facet is unresolved(unknown-callee) without a -// diagnostic (the callee may have released 'p' already), and the record is replaced by a -// known one, so the write after the free is still the definite use-after-free v0.10.0 -// reports. -#include - -void consume(char *p); - -void f(void) { - char *p = malloc(8); - if (!p) return; - consume(p); - free(p); - p[0] = 1; // BUG: use-after-free definite -} diff --git a/test/cases/semantics/library/fill-shared-unknown-object.c b/test/cases/semantics/library/fill-shared-unknown-object.c deleted file mode 100644 index 98a1c2b3..00000000 --- a/test/cases/semantics/library/fill-shared-unknown-object.c +++ /dev/null @@ -1,51 +0,0 @@ -// RFC 0031 §4.1: a write to an object that stands for several is weak. -// STAGE: S7 -// `mallocZero`'s result is the unknown object (its allocator is a hook the -// unit cannot see), which stands for every object the analysis cannot name, -// so `t->pVtab`, which a constructor set through `&t->pVtab`, points into it -// too. `memset(t->pVtab, 0, ...)` zeroes one of those objects, not all of -// them: it had zeroed the unknown object's cells, `t->pVtab` among them, and -// the dereference after it was a definite null dereference (sqlite's -// `vtabCallConstructor`). -// TOOL -// CLEAN -#include - -typedef struct vtab { - const void *pModule; - int nRef; - char *zErr; -} vtab; - -typedef struct VTable { - void *db; - void *pMod; - vtab *pVtab; - int nRef; -} VTable; - -extern void *(*xAlloc)(unsigned long); - -static void *mallocZero(unsigned long n) { - void *p = xAlloc(n); - if (p) - memset(p, 0, n); - return p; -} - -typedef int (*Ctor)(vtab **pp); - -int construct(const void *m, Ctor xConstruct) { - VTable *t = mallocZero(sizeof(VTable)); - if (!t) - return 7; - int rc = xConstruct(&t->pVtab); - if (rc != 0) - return rc; - if (t->pVtab) { - memset(t->pVtab, 0, sizeof(t->pVtab[0])); - t->pVtab->pModule = m; - t->nRef = 1; - } - return rc; -} diff --git a/test/cases/semantics/library/library-filled-result.c b/test/cases/semantics/library/library-filled-result.c deleted file mode 100644 index f7da4b54..00000000 --- a/test/cases/semantics/library/library-filled-result.c +++ /dev/null @@ -1,37 +0,0 @@ -// RFC 0030 §11: zero-initialisation covers the allocations it rewrites only. -// STAGE: S7 -// `getaddrinfo` returns a list the C library allocated and filled; its -// `ai_addr` fields are the library's, not zeros (hiredis's `net.c`). Only a -// row the zero-initialisation wrapper covers (`malloc`, `calloc`, ...) makes -// an object whose unwritten bytes read as zero; this one's read as unknown, so -// passing `ai_addr` to `connect` is no null dereference. -// The early return when `getaddrinfo` fails is reported as a possible leak of -// its result: the table cannot yet say that a non-zero result means nothing was -// stored (`null-on-failure` names no result class), so the leak is allowed. -// CLEAN -// ALLOW: leak -// TOOL -#include -#include -#include -#include - -int dial(const char *host, const char *port) { - struct addrinfo hints = {0}; - struct addrinfo *info = NULL; - hints.ai_socktype = SOCK_STREAM; - if (getaddrinfo(host, port, &hints, &info) != 0) - return -1; - int fd = -1; - for (struct addrinfo *p = info; p != NULL; p = p->ai_next) { - fd = socket(p->ai_family, p->ai_socktype, p->ai_protocol); - if (fd < 0) - continue; - if (connect(fd, p->ai_addr, p->ai_addrlen) == 0) - break; - close(fd); - fd = -1; - } - freeaddrinfo(info); - return fd; -} diff --git a/test/cases/semantics/library/realloc-zero-definite.c b/test/cases/semantics/library/realloc-zero-definite.c deleted file mode 100644 index 24ba4c1a..00000000 --- a/test/cases/semantics/library/realloc-zero-definite.c +++ /dev/null @@ -1,16 +0,0 @@ -// RFC 0030 §8.2 (realloc): with a size known to be zero, realloc releases its argument on the null class. -// STAGE: S4 -// realloc(free) releases 'p' on the non-null result class, and on the null class when the -// size is zero, because glibc's realloc(p, 0) frees 'p' and returns null (the case is -// 'outcome null a0 freed when param 1 =0'). With the size known to be zero, -// 'q = realloc(p, 0); if (!q) free(p);' is a definite double free. Only without -// zero-initialisation: its wrapper asks realloc for one byte instead of none, so the -// null class keeps 'p' (RFC 0031 *Implementation amendments*). -// FLAGS: -fno-weavec-zero-init -#include - -void shrink(char *p) { - char *q = realloc(p, 0); - if (!q) free(p); // BUG: double-free definite - free(q); -} diff --git a/test/cases/semantics/library/realloc-zero-possible.c b/test/cases/semantics/library/realloc-zero-possible.c index 521f0ca3..e6f250e3 100644 --- a/test/cases/semantics/library/realloc-zero-possible.c +++ b/test/cases/semantics/library/realloc-zero-possible.c @@ -1,16 +1,16 @@ // RFC 0030 §8.2 (realloc): with an unknown size, the zero-size release is only possible. -// STAGE: S4 // The same code as realloc-zero-definite.c with a size the caller does not know: the null // class releases 'p' only when the size is zero, so the record is conditional and the free // is a possible double free, a warning. The program builds; the run's size is non-zero. // Without zero-initialisation, as realloc-zero-definite.c. // FLAGS: -fno-weavec-zero-init +// CLEAN // RUN-INPUT: #include char *grow(char *p, size_t n) { char *q = realloc(p, n); - if (!q) free(p); // BUG: double-free possible + if (!q) free(p); return q; } diff --git a/test/cases/semantics/library/release-at-unknown-offset.c b/test/cases/semantics/library/release-at-unknown-offset.c index 7260bb50..75a4526c 100644 --- a/test/cases/semantics/library/release-at-unknown-offset.c +++ b/test/cases/semantics/library/release-at-unknown-offset.c @@ -8,7 +8,6 @@ // points: a possible finding. The header `snew` writes before its result is // not described either, so the caller reads it as unknown, not as zeros. // CLEAN -// ALLOW: invalid-release // ASAN // RUN-INPUT: #include diff --git a/test/cases/semantics/library/releasers.c b/test/cases/semantics/library/releasers.c deleted file mode 100644 index a45bce0b..00000000 --- a/test/cases/semantics/library/releasers.c +++ /dev/null @@ -1,23 +0,0 @@ -// RFC 0030 §8.3: freeaddrinfo and freeifaddrs are releasers. -// STAGE: S4 -// getaddrinfo and getifaddrs store fresh lists through their out-parameters -// (out(fresh(freeaddrinfo)), out(fresh(freeifaddrs))), and freeaddrinfo and freeifaddrs -// release them, so reading a list after its release is a definite use-after-free. -#include -#include -#include -#include - -int family(const char *host) { - struct addrinfo *res = NULL; - if (getaddrinfo(host, NULL, NULL, &res) != 0) return -1; - freeaddrinfo(res); - return res->ai_family; // BUG: use-after-free definite -} - -int first_flags(void) { - struct ifaddrs *ifa = NULL; - if (getifaddrs(&ifa) != 0) return -1; - freeifaddrs(ifa); - return ifa == NULL ? 0 : (int)ifa->ifa_flags; // BUG: use-after-free definite -} diff --git a/test/cases/semantics/library/stdout-closed-twice.c b/test/cases/semantics/library/stdout-closed-twice.c deleted file mode 100644 index 219b23b6..00000000 --- a/test/cases/semantics/library/stdout-closed-twice.c +++ /dev/null @@ -1,37 +0,0 @@ -// RFC 0031 *Implementation amendments* (the C library's own globals): a call -// the analysis does not see does not make `stdout` name another stream. -// STAGE: S7 -// Each round closes `stdout` again (zlib's minigzip with `-c -d` and two -// files, RFC 0030 G9). `open_input` is external: it may do anything to what -// the program's globals reach, but it does not reassign `stdout`, so the -// second round's `stdout` is the stream the first round closed. -// TOOL -#include -#include - -typedef struct input *input; -input open_input(const char *path); -int read_input(input in, void *buf, unsigned len); -int close_input(input in); - -static void copy_out(input in, FILE *out) { - char buf[64]; - int len; - while ((len = read_input(in, buf, sizeof buf)) > 0) - fwrite(buf, 1, (unsigned)len, out); - if (fclose(out)) - exit(1); - if (close_input(in)) - exit(1); -} - -int main(int argc, char **argv) { - do { - input in = open_input(argv[argc]); - if (in == NULL) - fprintf(stderr, "cannot open\n"); - else - copy_out(in, stdout); // BUG: double-free possible - } while (--argc); - return 0; -} diff --git a/test/cases/semantics/library/wrapper-lossy.c b/test/cases/semantics/library/wrapper-lossy.c index 63035944..9048536b 100644 --- a/test/cases/semantics/library/wrapper-lossy.c +++ b/test/cases/semantics/library/wrapper-lossy.c @@ -31,7 +31,7 @@ int main(int argc, char **argv) { free(p); return 1; } - int c = p[0]; // BUG: use-after-free possible // TRAP + int c = p[0]; // TRAP free(q); return c == 'a' ? 0 : 2; } diff --git a/test/cases/semantics/library/wrapper-param-keyed.c b/test/cases/semantics/library/wrapper-param-keyed.c deleted file mode 100644 index e95cebe7..00000000 --- a/test/cases/semantics/library/wrapper-param-keyed.c +++ /dev/null @@ -1,30 +0,0 @@ -// RFC 0030 §9.1: a wrapper that frees when its size parameter is zero is keyed to that argument. -// STAGE: S7 -// 'xrealloc' frees 'p' and returns null when 'n' is 0, and otherwise behaves as realloc: its -// cases are 'result null and param 1 =0' and 'result nonnull'. In 'drop' the test of the -// result selects the null class and the known argument 0 satisfies the parameter test, so -// the record is unconditional and the use is a definite use-after-free. In 'grow' the null -// class with a size of 16 leaves 'q' live, so freeing it there is no double free. -#include - -void *xrealloc(void *p, size_t n) { - if (n == 0) { - free(p); - return NULL; - } - return realloc(p, n); -} - -int drop(char *p) { - if (xrealloc(p, 0) != NULL) return 0; - return p[0]; // BUG: use-after-free definite -} - -char *grow(char *q) { - char *r = xrealloc(q, 16); - if (r == NULL) { - free(q); - return NULL; - } - return r; -} diff --git a/test/cases/semantics/objects/array-summary-cells_bug.c b/test/cases/semantics/objects/array-summary-cells_bug.c index 325a3d55..af5a8da8 100644 --- a/test/cases/semantics/objects/array-summary-cells_bug.c +++ b/test/cases/semantics/objects/array-summary-cells_bug.c @@ -15,5 +15,5 @@ int main(void) { for (int i = 0; i < 4; i++) free(a[i]); struct n *first = a[0]; - return first->v; // BUG: use-after-free + return first->v; // TRAP: heap-use-after-free } diff --git a/test/cases/semantics/objects/byte-copy-struct_bug.c b/test/cases/semantics/objects/byte-copy-struct_bug.c index 5440f0b2..9702c299 100644 --- a/test/cases/semantics/objects/byte-copy-struct_bug.c +++ b/test/cases/semantics/objects/byte-copy-struct_bug.c @@ -15,5 +15,5 @@ int main(void) { orig.buf[0] = 1; memcpy(©, &orig, sizeof orig); free(copy.buf); - return orig.buf[0]; // BUG: use-after-free + return orig.buf[0]; // TRAP: heap-use-after-free } diff --git a/test/cases/semantics/objects/materialise-head_bug.c b/test/cases/semantics/objects/materialise-head_bug.c index a30245fd..eaea3ac7 100644 --- a/test/cases/semantics/objects/materialise-head_bug.c +++ b/test/cases/semantics/objects/materialise-head_bug.c @@ -18,7 +18,7 @@ int main(void) { struct node *n = l.head; struct node *second = n->next; free(n); - int r = l.head->v; // BUG: use-after-free + int r = l.head->v; // TRAP: heap-use-after-free while (second) { struct node *next = second->next; free(second); diff --git a/test/cases/semantics/objects/owner-cycle_bug.c b/test/cases/semantics/objects/owner-cycle_bug.c index fbcd62ed..4146cb1e 100644 --- a/test/cases/semantics/objects/owner-cycle_bug.c +++ b/test/cases/semantics/objects/owner-cycle_bug.c @@ -21,6 +21,6 @@ int main(void) { if (!n) abort(); n->v = 1; n->next = n; - free_list(n); // BUG: use-after-free // MISS: an owning cycle is not reported by the analysis; the guard stops at the use after free in the callee + free_list(n); return 0; } diff --git a/test/cases/semantics/objects/recency-loop_bug.c b/test/cases/semantics/objects/recency-loop_bug.c index 4e202799..e3645fdc 100644 --- a/test/cases/semantics/objects/recency-loop_bug.c +++ b/test/cases/semantics/objects/recency-loop_bug.c @@ -22,7 +22,7 @@ int main(int argc, char **argv) { prev = cur; } if (old) - sum += old->v; // BUG: use-after-free // TRAP + sum += old->v; // TRAP free(prev); return sum; } diff --git a/test/cases/semantics/objects/strong-update-stale_bug.c b/test/cases/semantics/objects/strong-update-stale_bug.c index 0e1b2945..9bde6807 100644 --- a/test/cases/semantics/objects/strong-update-stale_bug.c +++ b/test/cases/semantics/objects/strong-update-stale_bug.c @@ -13,5 +13,5 @@ int main(void) { if (!bx.buf) return 1; bx.buf[0] = 1; free(q->buf); - return bx.buf[0]; // BUG: use-after-free + return bx.buf[0]; // TRAP: heap-use-after-free } diff --git a/test/cases/semantics/objects/weak-update_bug.c b/test/cases/semantics/objects/weak-update_bug.c index 96d1fec7..2cde67ad 100644 --- a/test/cases/semantics/objects/weak-update_bug.c +++ b/test/cases/semantics/objects/weak-update_bug.c @@ -12,7 +12,7 @@ int main(int argc, char **argv) { int *a = &x, *b = &y; int **p = argc > 1 ? &a : &b; *p = NULL; - int r = *a; // BUG: null-dereference // TRAP + int r = *a; // TRAP printf("%d\n", r); return 0; } diff --git a/test/cases/semantics/runtime/access-difference_bug.c b/test/cases/semantics/runtime/access-difference_bug.c index 56729bfc..232c739f 100644 --- a/test/cases/semantics/runtime/access-difference_bug.c +++ b/test/cases/semantics/runtime/access-difference_bug.c @@ -13,26 +13,26 @@ struct pair { int a; long b; }; struct box { char *s, *e; struct pair *v, *ve; struct pair **pp; }; static int minus(struct box *b, long k) { - return *(b->e - k); // BUG: out-of-bounds // TRAP + return *(b->e - k); // TRAP } static int minus_arrow(struct box *b, long k) { - return (int)(b->ve - k)->b; // BUG: out-of-bounds // TRAP + return (int)(b->ve - k)->b; // TRAP } static int minus_end(struct box *b, long k) { - return *(b->e - k); // BUG: out-of-bounds // TRAP + return *(b->e - k); // TRAP } static int negative(struct box *b, long k) { - return b->e[-k]; // BUG: out-of-bounds // TRAP + return b->e[-k]; // TRAP } static int walk_back(struct box *b, long k) { const char *p = b->e; long j; for (j = 0; j < k; j++) --p; - return *p; // BUG: out-of-bounds // TRAP + return *p; // TRAP } static int predecrement(struct box *b, long k) { const char *p = b->e - k; - return *--p; // BUG: out-of-bounds // TRAP + return *--p; // TRAP } int main(int argc, char **argv) { struct box b; diff --git a/test/cases/semantics/runtime/access-subscript_bug.c b/test/cases/semantics/runtime/access-subscript_bug.c index e9b93d58..bd68b876 100644 --- a/test/cases/semantics/runtime/access-subscript_bug.c +++ b/test/cases/semantics/runtime/access-subscript_bug.c @@ -13,25 +13,25 @@ struct pair { int a; long b; }; struct box { char *s, *e; struct pair *v, *ve; struct pair **pp; }; static int subscript_member(struct box *b, long k) { - return (int)b->v[k].b; // BUG: out-of-bounds // TRAP + return (int)b->v[k].b; // TRAP } static int address_arrow(struct box *b, long k) { - return (&b->v[k])->a; // BUG: out-of-bounds // TRAP + return (&b->v[k])->a; // TRAP } static int two_levels(struct box *b, long k) { - return (int)b->pp[0][k].b; // BUG: out-of-bounds // TRAP + return (int)b->pp[0][k].b; // TRAP } static int copy(struct box *b, long k) { - b->v[k] = b->v[0]; // BUG: out-of-bounds // TRAP + b->v[k] = b->v[0]; // TRAP return 0; } static int compound(struct box *b, long k) { - b->s[k] += 1; // BUG: out-of-bounds // TRAP + b->s[k] += 1; // TRAP return 0; } static int loop_write(struct box *b, long k) { long j; - for (j = 0; j <= k; j++) b->s[j] = 0; // BUG: out-of-bounds // TRAP + for (j = 0; j <= k; j++) b->s[j] = 0; // TRAP return 0; } int main(int argc, char **argv) { diff --git a/test/cases/semantics/runtime/access-sum_bug.c b/test/cases/semantics/runtime/access-sum_bug.c index fa2f9a84..6e6bf878 100644 --- a/test/cases/semantics/runtime/access-sum_bug.c +++ b/test/cases/semantics/runtime/access-sum_bug.c @@ -15,35 +15,35 @@ struct pair { int a; long b; }; struct box { char *s, *e; struct pair *v, *ve; struct pair **pp; }; static int plus(struct box *b, long k) { - return *(b->s + k); // BUG: out-of-bounds // TRAP + return *(b->s + k); // TRAP } static int plus_arrow(struct box *b, long k) { - return (int)(b->v + k)->b; // BUG: out-of-bounds // TRAP + return (int)(b->v + k)->b; // TRAP } static int plus_swapped(struct box *b, long k) { - return *(k + b->s); // BUG: out-of-bounds // TRAP + return *(k + b->s); // TRAP } static int deref_member(struct box *b, long k) { - return (int)(*(b->v + k)).b; // BUG: out-of-bounds // TRAP + return (int)(*(b->v + k)).b; // TRAP } static int local(struct box *b, long k) { const char *p = b->s + k; - return *p; // BUG: out-of-bounds // TRAP + return *p; // TRAP } static int advance(struct box *b, long k) { const char *p = b->s; p += k; - return *p; // BUG: out-of-bounds // TRAP + return *p; // TRAP } static int walk(struct box *b, long k) { const char *p = b->s; long j; for (j = 0; j < k; j++) p++; - return *p; // BUG: out-of-bounds // TRAP + return *p; // TRAP } static int postincrement(struct box *b, long k) { const char *p = b->s + k; - return *p++; // BUG: out-of-bounds // TRAP + return *p++; // TRAP } int main(int argc, char **argv) { struct box b; diff --git a/test/cases/semantics/runtime/cache-shared_bug.c b/test/cases/semantics/runtime/cache-shared_bug.c index 4e477e31..5ca93ed6 100644 --- a/test/cases/semantics/runtime/cache-shared_bug.c +++ b/test/cases/semantics/runtime/cache-shared_bug.c @@ -18,7 +18,7 @@ static int probe(long k) { for (j = 0; j < 2; j++) { hook(); total += p->a; - total += p[k].a; // BUG: out-of-bounds // TRAP + total += p[k].a; // TRAP } return total; } diff --git a/test/cases/semantics/runtime/callback-table_bug.c b/test/cases/semantics/runtime/callback-table_bug.c index b084dced..93879e7c 100644 --- a/test/cases/semantics/runtime/callback-table_bug.c +++ b/test/cases/semantics/runtime/callback-table_bug.c @@ -21,7 +21,7 @@ static void add(cb_t cb, void *arg) { static void fire(void) { for (int i = 0; i < ntimers; i++) timers[i].cb(timers[i].arg); } -static void show(void *p) { seen += *(int *)p; } // BUG: use-after-free // TRAP +static void show(void *p) { seen += *(int *)p; } // TRAP int main(void) { int *x = malloc(sizeof *x); if (!x) return 1; diff --git a/test/cases/semantics/runtime/cleanup-function_bug.c b/test/cases/semantics/runtime/cleanup-function_bug.c index f0c222c0..698a8984 100644 --- a/test/cases/semantics/runtime/cleanup-function_bug.c +++ b/test/cases/semantics/runtime/cleanup-function_bug.c @@ -14,7 +14,7 @@ static int total(struct table *t, long n) { long j; for (j = 0; j < n; j++) { struct row *held __attribute__((cleanup(drop))) = t->rows; - sum += held->cells[0]; // BUG: use-after-free // TRAP + sum += held->cells[0]; // TRAP } return sum; } diff --git a/test/cases/semantics/runtime/format-precision-dangling_bug.c b/test/cases/semantics/runtime/format-precision-dangling_bug.c index cacf3a67..15086a89 100644 --- a/test/cases/semantics/runtime/format-precision-dangling_bug.c +++ b/test/cases/semantics/runtime/format-precision-dangling_bug.c @@ -20,6 +20,6 @@ static void find(const char *text, const char **err) { int main(void) { const char *err; find("a,b,xyz", &err); - printf("%.3s\n", err); // BUG: use-after-free // TRAP + printf("%.3s\n", err); // TRAP return 0; } diff --git a/test/cases/semantics/runtime/global-static-local_bug.c b/test/cases/semantics/runtime/global-static-local_bug.c index 6b4f963f..2e0c62cc 100644 --- a/test/cases/semantics/runtime/global-static-local_bug.c +++ b/test/cases/semantics/runtime/global-static-local_bug.c @@ -9,7 +9,7 @@ static const int *squares(void) { return table; } static int at(const struct view *v, int i) { - return v->p[i]; // BUG: out-of-bounds // TRAP + return v->p[i]; // TRAP } int main(int argc, char **argv) { struct view v = {squares()}; diff --git a/test/cases/semantics/runtime/global-view_bug.c b/test/cases/semantics/runtime/global-view_bug.c index 2166ba37..22d27096 100644 --- a/test/cases/semantics/runtime/global-view_bug.c +++ b/test/cases/semantics/runtime/global-view_bug.c @@ -6,7 +6,7 @@ static int table[8] = {1, 2, 3, 4, 5, 6, 7, 8}; struct view { const int *p; }; static int at(const struct view *v, int i) { - return v->p[i]; // BUG: out-of-bounds // TRAP + return v->p[i]; // TRAP } int main(int argc, char **argv) { struct view v = {table}; diff --git a/test/cases/semantics/runtime/heap-index-field_bug.c b/test/cases/semantics/runtime/heap-index-field_bug.c index d8a79501..7c5feb2b 100644 --- a/test/cases/semantics/runtime/heap-index-field_bug.c +++ b/test/cases/semantics/runtime/heap-index-field_bug.c @@ -16,7 +16,7 @@ static struct vec *vec_new(size_t cap) { return v; } static void vec_set(struct vec *v, size_t i, int x) { - v->data[i] = x; // BUG: out-of-bounds // TRAP + v->data[i] = x; // TRAP } int main(int argc, char **argv) { struct vec *v = vec_new(8); diff --git a/test/cases/semantics/runtime/libcall-bytes_bug.c b/test/cases/semantics/runtime/libcall-bytes_bug.c index 7ccaf000..8a628942 100644 --- a/test/cases/semantics/runtime/libcall-bytes_bug.c +++ b/test/cases/semantics/runtime/libcall-bytes_bug.c @@ -7,7 +7,7 @@ #include #include static void copy(char *d, const char *s, size_t n) { - memcpy(d, s, n); // BUG: out-of-bounds // TRAP + memcpy(d, s, n); // TRAP } int main(int argc, char **argv) { char *src = calloc(1, 64); diff --git a/test/cases/semantics/runtime/libcall-copy-source_bug.c b/test/cases/semantics/runtime/libcall-copy-source_bug.c index de48cc3a..73236f8c 100644 --- a/test/cases/semantics/runtime/libcall-copy-source_bug.c +++ b/test/cases/semantics/runtime/libcall-copy-source_bug.c @@ -7,7 +7,7 @@ #include #include static void erase(int *a, size_t *n, size_t i) { - memmove(&a[i], &a[i + 1], (*n - i) * sizeof *a); // BUG: out-of-bounds // TRAP + memmove(&a[i], &a[i + 1], (*n - i) * sizeof *a); // TRAP --*n; } int main(void) { diff --git a/test/cases/semantics/runtime/libcall-string_bug.c b/test/cases/semantics/runtime/libcall-string_bug.c index 55f5addc..ee4ec12a 100644 --- a/test/cases/semantics/runtime/libcall-string_bug.c +++ b/test/cases/semantics/runtime/libcall-string_bug.c @@ -6,7 +6,7 @@ #include #include static void put(char *dst, const char *src) { - strcpy(dst, src); // BUG: out-of-bounds // TRAP + strcpy(dst, src); // TRAP } int main(int argc, char **argv) { char *dst = malloc(8); diff --git a/test/cases/semantics/runtime/realloc-stale_bug.c b/test/cases/semantics/runtime/realloc-stale_bug.c index e4b47868..29f4b7de 100644 --- a/test/cases/semantics/runtime/realloc-stale_bug.c +++ b/test/cases/semantics/runtime/realloc-stale_bug.c @@ -22,7 +22,7 @@ int main(int argc, char **argv) { append(&b, "hello"); char *first = b.p; for (int i = 1; i < argc; i++) append(&b, argv[i]); - int r = first[0] != 'h'; // BUG: use-after-move possible // TRAP + int r = first[0] != 'h'; // TRAP free(b.p); return r; } diff --git a/test/cases/semantics/runtime/release-interior_bug.c b/test/cases/semantics/runtime/release-interior_bug.c index d55e7569..57339a55 100644 --- a/test/cases/semantics/runtime/release-interior_bug.c +++ b/test/cases/semantics/runtime/release-interior_bug.c @@ -6,7 +6,7 @@ // ASAN #include static void drop(char *p, int skip) { - free(p + skip); // BUG: invalid-release // TRAP + free(p + skip); // TRAP } int main(int argc, char **argv) { char *p = malloc(32); diff --git a/test/cases/semantics/runtime/release-interior_ok.c b/test/cases/semantics/runtime/release-interior_ok.c index a3bb81e7..24452724 100644 --- a/test/cases/semantics/runtime/release-interior_ok.c +++ b/test/cases/semantics/runtime/release-interior_ok.c @@ -1,7 +1,6 @@ // RFC 0032 §3: the correct twin of release-interior_bug.c: offset 0 is the start of the block. // STAGE: S5 // CLEAN -// ALLOW: invalid-release // RUN-INPUT: 0 // ASAN #include diff --git a/test/cases/semantics/runtime/release-twice_bug.c b/test/cases/semantics/runtime/release-twice_bug.c index 7bbe280f..3aa0cfe7 100644 --- a/test/cases/semantics/runtime/release-twice_bug.c +++ b/test/cases/semantics/runtime/release-twice_bug.c @@ -23,6 +23,6 @@ int main(int argc, char **argv) { if (!c) return 1; c->buf = malloc(16); int r = handle(c, argc > 1); - conn_close(c); // BUG: double-free possible + conn_close(c); return r < 0; } diff --git a/test/cases/semantics/runtime/release-twice_ok.c b/test/cases/semantics/runtime/release-twice_ok.c index 1b6ae043..2d9d692a 100644 --- a/test/cases/semantics/runtime/release-twice_ok.c +++ b/test/cases/semantics/runtime/release-twice_ok.c @@ -3,7 +3,6 @@ // The possible double free is still reported at the call: a boundary facet is not guarded // (RFC 0032, Bugs deliberately not caught), so the case allows the warning. // CLEAN -// ALLOW: double-free // RUN-INPUT: // ASAN #include diff --git a/test/cases/semantics/runtime/stack-cursor_bug.c b/test/cases/semantics/runtime/stack-cursor_bug.c index 96fd8e30..6006a8f4 100644 --- a/test/cases/semantics/runtime/stack-cursor_bug.c +++ b/test/cases/semantics/runtime/stack-cursor_bug.c @@ -7,7 +7,7 @@ #include static void fill(char *dst, const char *src) { while (*src) - *dst++ = *src++; // BUG: out-of-bounds // TRAP + *dst++ = *src++; // TRAP *dst = 0; } int main(int argc, char **argv) { diff --git a/test/cases/semantics/runtime/stack-underflow_bug.c b/test/cases/semantics/runtime/stack-underflow_bug.c index 2e4751cd..075d286b 100644 --- a/test/cases/semantics/runtime/stack-underflow_bug.c +++ b/test/cases/semantics/runtime/stack-underflow_bug.c @@ -8,7 +8,7 @@ static void *same(void *p) { return p; } static void *(*volatile hide)(void *) = same; static void chop(char *s) { - s[strlen(s) - 1] = 0; // BUG: out-of-bounds // TRAP + s[strlen(s) - 1] = 0; // TRAP } int main(void) { char buf[8]; diff --git a/test/cases/semantics/runtime/stack-vla-again_bug.c b/test/cases/semantics/runtime/stack-vla-again_bug.c index 496d9288..552b583a 100644 --- a/test/cases/semantics/runtime/stack-vla-again_bug.c +++ b/test/cases/semantics/runtime/stack-vla-again_bug.c @@ -16,7 +16,7 @@ again:; char *p = hide(v); memset(v, 1, sizeof v); for (i = 0; i < 3; i++) - total += p[round ? idx : 0]; // BUG: out-of-bounds // TRAP + total += p[round ? idx : 0]; // TRAP if (round++ == 0) goto again; return total; diff --git a/test/cases/semantics/runtime/term-signed-sum_bug.c b/test/cases/semantics/runtime/term-signed-sum_bug.c index 84056fb9..f9d63f94 100644 --- a/test/cases/semantics/runtime/term-signed-sum_bug.c +++ b/test/cases/semantics/runtime/term-signed-sum_bug.c @@ -23,7 +23,7 @@ int main(void) { unsigned char *in = malloc(64); if (!in) return 1; - pad(in, used); // BUG: out-of-bounds + pad(in, used); int last = in[63]; free(in); return last; diff --git a/test/cases/semantics/runtime/uaf-container_bug.c b/test/cases/semantics/runtime/uaf-container_bug.c index d93cf8af..d70e0322 100644 --- a/test/cases/semantics/runtime/uaf-container_bug.c +++ b/test/cases/semantics/runtime/uaf-container_bug.c @@ -42,6 +42,6 @@ int main(int argc, char **argv) { push(&l, "c"); struct item *b = find(&l, "b"); if (argc > 1) remove_item(&l, argv[1]); - if (b) return b->name == NULL; // BUG: use-after-free // TRAP + if (b) return b->name == NULL; // TRAP return 0; } diff --git a/test/cases/semantics/runtime/uaf-container_ok.c b/test/cases/semantics/runtime/uaf-container_ok.c index fdee5700..20136018 100644 --- a/test/cases/semantics/runtime/uaf-container_ok.c +++ b/test/cases/semantics/runtime/uaf-container_ok.c @@ -1,7 +1,6 @@ // RFC 0032 §3: the correct twin of uaf-container_bug.c: another item is removed, and 'b' stays live. // STAGE: S3 // CLEAN -// ALLOW: leak // RUN-INPUT: c // ASAN #include diff --git a/test/cases/semantics/slots/fp-memcpy.c b/test/cases/semantics/slots/fp-memcpy.c index 8424f071..083f4fd3 100644 --- a/test/cases/semantics/slots/fp-memcpy.c +++ b/test/cases/semantics/slots/fp-memcpy.c @@ -1,10 +1,10 @@ // RFC 0030 §8 and §9.3: 'fp = memcpy' applies memcpy's row to the indirect call. -// STAGE: S7 // The static function pointer 'copy' is a closed slot whose only target is memcpy, so the // row applies exactly: the destination requirement bytes(n) is checked against the exact // extent of 'dst' with the len template. The run copies 8 bytes into 4. // RUN-INPUT: 8 // ASAN +// XFAIL: a library function called through a pointer is not guarded (RFC 0035 §2.5) #include #include @@ -13,6 +13,6 @@ static void *(*copy)(void *, const void *, size_t) = memcpy; int main(int argc, char **argv) { char dst[4]; size_t n = argc > 1 ? (size_t)atoi(argv[1]) : sizeof dst; - copy(dst, "abcdefgh", n); // BUG: out-of-bounds // MISS: a library function called through a pointer is not guarded (RFC 0035 §2.5) + copy(dst, "abcdefgh", n); // TRAP return dst[0] == 'a' ? 0 : 1; } diff --git a/test/cases/semantics/slots/hook-freed-twice.c b/test/cases/semantics/slots/hook-freed-twice.c deleted file mode 100644 index b2d780ed..00000000 --- a/test/cases/semantics/slots/hook-freed-twice.c +++ /dev/null @@ -1,29 +0,0 @@ -// RFC 0030 §9.3: a release through a hook slot whose every function releases. -// STAGE: S7 -// `hooks.deallocate` holds `internal_free` or `free`; either releases the -// string, so calling it twice on the same string is a double free (cJSON's -// `global_hooks.deallocate`). -// TOOL -#include - -typedef struct { - void (*deallocate)(void *); -} hooks_t; - -static void internal_free(void *p) { free(p); } - -static hooks_t hooks = {internal_free}; - -void use_plain_free(void) { hooks.deallocate = free; } - -typedef struct { - char *text; -} item; - -void destroy(item *it) { - if (it->text != NULL) { - hooks.deallocate(it->text); - hooks.deallocate(it->text); // BUG: double-free - it->text = NULL; - } -} diff --git a/test/cases/semantics/slots/open-slot-callback.c b/test/cases/semantics/slots/open-slot-callback.c deleted file mode 100644 index 46d2c35f..00000000 --- a/test/cases/semantics/slots/open-slot-callback.c +++ /dev/null @@ -1,17 +0,0 @@ -// RFC 0030 §9.3 and §5.1: an indirect call through an open slot with no known target is a callback default. -// STAGE: S7 -// 'hook' has external linkage and nothing in this unit stores into it, so the slot is open -// and empty: the call gets the unknown-callee default with reason callback. The Call site -// and the later use of 'p' are unresolved(callback), with no diagnostic, and the callee -// operand's null facet is checked with the function-pointer form of nonnull. -#include - -void (*hook)(char *); - -int f(void) { - char *p = malloc(8); - if (!p) return 0; - p[0] = 1; - hook(p); // TRAP - return p[0]; -} diff --git a/test/cases/semantics/temporal/flag-selected-release.c b/test/cases/semantics/temporal/flag-selected-release.c index e5a8be10..e413cf75 100644 --- a/test/cases/semantics/temporal/flag-selected-release.c +++ b/test/cases/semantics/temporal/flag-selected-release.c @@ -7,7 +7,6 @@ // literal, and a release the callee may not perform makes that a possible // `invalid-release`, not an error that would drop the object. // CLEAN -// ALLOW: invalid-release #include #include diff --git a/test/cases/semantics/temporal/free-then-guarded-wrapper.c b/test/cases/semantics/temporal/free-then-guarded-wrapper.c deleted file mode 100644 index dd221f09..00000000 --- a/test/cases/semantics/temporal/free-then-guarded-wrapper.c +++ /dev/null @@ -1,28 +0,0 @@ -// RFC 0030 §3.1: a callee that may release an argument already freed is a -// second release. -// STAGE: S7 -// `release_line` frees its argument unless it is the sentinel (linenoise's -// `linenoiseFree`); the caller freed the line already, so the call frees it -// twice. The callee's release is only possible, but the argument is freed for -// certain: the finding is the double free, not a use of a freed pointer. -// TOOL -#include - -char *sentinel_value = "more"; - -void release_line(void *line) { - if (line == sentinel_value) - return; - free(line); -} - -char *read_line(void); - -int main(void) { - char *line = read_line(); - if (line == NULL) - return 0; - free(line); - release_line(line); // BUG: double-free definite - return 0; -} diff --git a/test/cases/semantics/temporal/identity-guarded-share.c b/test/cases/semantics/temporal/identity-guarded-share.c index 0c0bb1aa..b74211e7 100644 --- a/test/cases/semantics/temporal/identity-guarded-share.c +++ b/test/cases/semantics/temporal/identity-guarded-share.c @@ -7,7 +7,6 @@ // of `held` is possible, never definite, and `drop(held)` is at most a // possible double free. // CLEAN -// ALLOW: double-free #include struct obj { diff --git a/test/cases/semantics/temporal/possible-rewrite-stays-possible.c b/test/cases/semantics/temporal/possible-rewrite-stays-possible.c deleted file mode 100644 index 8b986ec2..00000000 --- a/test/cases/semantics/temporal/possible-rewrite-stays-possible.c +++ /dev/null @@ -1,57 +0,0 @@ -// RFC 0031 §6.3: bytes a callee may have rewritten stay possibly rewritten. -// STAGE: S7 -// `refill` reads over the whole of `*ls` on some paths only, so its summary -// says the bytes may be rewritten; `advance` calls it on every path, and -// its own summary must still say "may": on the other paths `ls->fs` keeps -// the frame `open_frame` stored there. If `advance`'s summary said the -// bytes were rewritten, `parse` would lose `ls->fs`, the store `close_frame` -// makes through it would not reach `fs`, and `fs->bl` would still hold -// `&bl` at the exit: a definite (and false) `lifetime-too-short` (Lua's -// `luaX_next`, `statlist` and `mainfunc`). The `read` may have replaced -// `ls->fs` itself, so what is left is a possible finding. -#include - -typedef struct Block { - struct Block *previous; -} Block; - -typedef struct Frame { - Block *bl; - struct Frame *prev; -} Frame; - -typedef struct Lex { - int fd; - int token; - Frame *fs; -} Lex; - -static void refill(Lex *ls) { - if (ls->token == 0) - (void)read(ls->fd, ls, sizeof *ls); -} - -static void advance(Lex *ls) { - refill(ls); - ls->token = 1; -} - -static void open_frame(Lex *ls, Frame *fs, Block *bl) { - fs->prev = ls->fs; - ls->fs = fs; - bl->previous = fs->bl; - fs->bl = bl; -} - -static void close_frame(Lex *ls) { - Frame *fs = ls->fs; - fs->bl = fs->bl->previous; - ls->fs = fs->prev; -} - -void parse(Lex *ls, Frame *fs) { - Block bl; - open_frame(ls, fs, &bl); // BUG: lifetime-too-short possible - advance(ls); - close_frame(ls); -} diff --git a/test/cases/semantics/temporal/unknown-callee-resets-frame.c b/test/cases/semantics/temporal/unknown-callee-resets-frame.c deleted file mode 100644 index 4ebe5719..00000000 --- a/test/cases/semantics/temporal/unknown-callee-resets-frame.c +++ /dev/null @@ -1,52 +0,0 @@ -// RFC 0031 §6.3, §4.6: an unknown callee's effects name the entry state. -// STAGE: S7 -// `close_func` resets the block chain of the frame `ls->fs` names, hands -// `ls` to code it cannot see (which may do anything to every object `ls` -// reaches), and pops the frame. The frame is then no longer reachable from -// `ls` at the exit, but the caller still holds it: the summary must say its -// bytes may have been rewritten, and must resolve `*ls->fs` before it -// forgets the cells of `*ls`. Otherwise the caller keeps `fs->bl` pointing -// at the local block `bl` and reports that it outlives it (Lua's -// `mainfunc`). -// CLEAN -typedef struct Block { - struct Block *previous; -} Block; - -struct Lex; - -typedef struct Frame { - Block *bl; - struct Frame *prev; -} Frame; - -typedef struct Lex { - Frame *fs; -} Lex; - -void collect(Lex *ls); - -static void leave(Frame *fs) { - Block *bl = fs->bl; - fs->bl = bl->previous; -} - -static void open_frame(Lex *ls, Frame *fs, Block *bl) { - fs->prev = ls->fs; - ls->fs = fs; - bl->previous = fs->bl; - fs->bl = bl; -} - -static void close_frame(Lex *ls) { - Frame *fs = ls->fs; - leave(fs); - collect(ls); - ls->fs = fs->prev; -} - -void parse(Lex *ls, Frame *fs) { - Block bl; - open_frame(ls, fs, &bl); - close_frame(ls); -} diff --git a/test/cases/semantics/temporal/unknown-effect-reaches-frame.c b/test/cases/semantics/temporal/unknown-effect-reaches-frame.c deleted file mode 100644 index 74df8e12..00000000 --- a/test/cases/semantics/temporal/unknown-effect-reaches-frame.c +++ /dev/null @@ -1,47 +0,0 @@ -// RFC 0031 §6.3: an unknown effect reaches what the caller's memory reaches. -// STAGE: S7 -// `churn` hands `ls` to code nobody sees; its summary says only that `*ls` -// was rewritten, because `churn` never loaded `ls->fs`. The unknown code had -// `ls->fs`, which is `fs`, so in `parse` it may have rewritten `fs->bl` too. -// After it, `ls->fs` no longer names `fs` for the analysis, so the store -// `close_frame` makes through it does not reach `fs`, and without the rule -// `fs->bl` would still hold `&bl` at the exit: a definite (and false) -// `lifetime-too-short`. With it, `fs` is rewritten by the unknown code and -// nothing definite is left to say (Lua's `mainfunc`). -// CLEAN -typedef struct Block { - struct Block *previous; -} Block; - -typedef struct Frame { - Block *bl; - struct Frame *prev; -} Frame; - -typedef struct Lex { - Frame *fs; -} Lex; - -void external(Lex *ls); - -static void churn(Lex *ls) { external(ls); } - -static void open_frame(Lex *ls, Frame *fs, Block *bl) { - fs->prev = ls->fs; - ls->fs = fs; - bl->previous = fs->bl; - fs->bl = bl; -} - -static void close_frame(Lex *ls) { - Frame *fs = ls->fs; - fs->bl = fs->bl->previous; - ls->fs = fs->prev; -} - -void parse(Lex *ls, Frame *fs) { - Block bl; - open_frame(ls, fs, &bl); - churn(ls); - close_frame(ls); -} diff --git a/test/cases/semantics/unsafe/definite-violations.c b/test/cases/semantics/unsafe/definite-violations.c deleted file mode 100644 index 49f7e769..00000000 --- a/test/cases/semantics/unsafe/definite-violations.c +++ /dev/null @@ -1,19 +0,0 @@ -// RFC 0030 §6.1: WEAVEC_UNSAFE no longer hides definite violations. -// STAGE: S3 -// Temporal state is tracked inside a region exactly as outside, so a definite -// use-after-free in an unsafe function is still an error (probe 45), and a definite spatial -// violation against an exact extent inside an unsafe block is still an error. No diagnostic -// is dropped for being inside a region. -#include -#include - -WEAVEC_UNSAFE void drop(char *p) { - free(p); - p[0] = 0; // BUG: use-after-free definite -} - -int past(void) { - char b[4] = {0}; - WEAVEC_UNSAFE { b[4] = 1; } // BUG: out-of-bounds definite - return b[0]; -} diff --git a/test/cases/semantics/unsafe/possible-temporal.c b/test/cases/semantics/unsafe/possible-temporal.c deleted file mode 100644 index 9f6eea66..00000000 --- a/test/cases/semantics/unsafe/possible-temporal.c +++ /dev/null @@ -1,13 +0,0 @@ -// RFC 0030 §6.1: a possible temporal finding inside a region is reported exactly as outside it. -// STAGE: S3 -// The release on some paths gives the warning with "may" wording and the temporal facet -// unresolved(may-released), as in examples/temporal-possible.c. -#include -#include - -void h(char *p, int c) { - WEAVEC_UNSAFE { - if (c) free(p); - p[0] = 1; // BUG: use-after-free possible - } -} diff --git a/test/cases/semantics/unsafe/raw-outside-region.c b/test/cases/semantics/unsafe/raw-outside-region.c deleted file mode 100644 index 914a8313..00000000 --- a/test/cases/semantics/unsafe/raw-outside-region.c +++ /dev/null @@ -1,17 +0,0 @@ -// RFC 0030 §6.1 and §2.3, as amended by RFC 0033 §2: outside a region a Raw site is an unsafe-operation error; a pointer converted from an integer is no Raw site. -// STAGE: S3 -// 'reg' is declared WEAVEC_RAW: dereferencing it outside an unsafe region is the -// unsafe-operation error of RFC 0004. 'mmio' is converted from an integer, which since -// RFC 0033 makes a pointer of unknown provenance, not a raw one: the IntToPtr site's own -// facets are unresolved(raw-cast) and the access through it is guarded with that reason. -#include -#include - -void poke(volatile unsigned *WEAVEC_RAW reg) { - *reg = 1; // BUG: unsafe-operation definite -} - -void poke_address(uintptr_t addr) { - volatile unsigned *mmio = (volatile unsigned *)addr; - *mmio = 1; -} diff --git a/test/cases/semantics/unsafe/trusted-refines-nothing.c b/test/cases/semantics/unsafe/trusted-refines-nothing.c deleted file mode 100644 index ca4cdbc9..00000000 --- a/test/cases/semantics/unsafe/trusted-refines-nothing.c +++ /dev/null @@ -1,12 +0,0 @@ -// RFC 0030 §6.1 and §3.2: inside a region spatial and null facets are trusted(unsafe) and refine nothing. -// STAGE: S3 -// The dereference inside the region has spatial and null trusted(unsafe) and no check. A -// trusted dereference does not make 'p' non-null downstream, so the dereference after the -// region keeps a checked null facet. -#include - -int twice(int *p, int i) { - int a; - WEAVEC_UNSAFE { a = p[i]; } - return a + p[0]; // TRAP -} diff --git a/test/cases/soundness/01_uaf_field_alias_bug.c b/test/cases/soundness/01_uaf_field_alias_bug.c index b7f3d08a..3178529f 100644 --- a/test/cases/soundness/01_uaf_field_alias_bug.c +++ b/test/cases/soundness/01_uaf_field_alias_bug.c @@ -12,7 +12,7 @@ int main(void) { a->buf = p; b->buf = p; free(a->buf); - int r = b->buf[0]; // BUG: use-after-free + int r = b->buf[0]; // TRAP: heap-use-after-free free(a); free(b); return r; } diff --git a/test/cases/soundness/02_uaf_global_bug.c b/test/cases/soundness/02_uaf_global_bug.c index 91f7e3f5..361a6a81 100644 --- a/test/cases/soundness/02_uaf_global_bug.c +++ b/test/cases/soundness/02_uaf_global_bug.c @@ -3,7 +3,7 @@ #include static char *g_cache; static void remember(char *p) { g_cache = p; } -static int peek(void) { return g_cache[0]; } // BUG: use-after-free // TRAP +static int peek(void) { return g_cache[0]; } // TRAP int main(void) { char *p = malloc(8); if (!p) return 1; diff --git a/test/cases/soundness/02b_uaf_global_direct_bug.c b/test/cases/soundness/02b_uaf_global_direct_bug.c index dbb65ffe..4d56e897 100644 --- a/test/cases/soundness/02b_uaf_global_direct_bug.c +++ b/test/cases/soundness/02b_uaf_global_direct_bug.c @@ -7,5 +7,5 @@ int main(void) { p[0] = 1; g = p; free(p); - return g[0]; // BUG: use-after-free + return g[0]; // TRAP: heap-use-after-free } diff --git a/test/cases/soundness/02c_uaf_global_free_in_helper_bug.c b/test/cases/soundness/02c_uaf_global_free_in_helper_bug.c index 2d0b5b42..4e5c696c 100644 --- a/test/cases/soundness/02c_uaf_global_free_in_helper_bug.c +++ b/test/cases/soundness/02c_uaf_global_free_in_helper_bug.c @@ -4,7 +4,7 @@ static char *g; static void init(void) { g = malloc(8); if (g) g[0] = 1; } static void shutdown(void) { free(g); } -static int use(void) { return g ? g[0] : 0; } // BUG: use-after-free // TRAP +static int use(void) { return g ? g[0] : 0; } // TRAP int main(void) { init(); shutdown(); diff --git a/test/cases/soundness/02d_uaf_heap_field_helper_bug.c b/test/cases/soundness/02d_uaf_heap_field_helper_bug.c index 26de06d9..afcfaaa0 100644 --- a/test/cases/soundness/02d_uaf_heap_field_helper_bug.c +++ b/test/cases/soundness/02d_uaf_heap_field_helper_bug.c @@ -3,7 +3,7 @@ #include struct s { char *buf; }; static void drop(struct s *o) { free(o->buf); } -static int peek(struct s *o) { return o->buf[0]; } // BUG: use-after-free // TRAP +static int peek(struct s *o) { return o->buf[0]; } // TRAP int main(void) { struct s o; o.buf = malloc(8); diff --git a/test/cases/soundness/02e_uaf_heap_field_direct_bug.c b/test/cases/soundness/02e_uaf_heap_field_direct_bug.c index 45482e89..8fbcdb98 100644 --- a/test/cases/soundness/02e_uaf_heap_field_direct_bug.c +++ b/test/cases/soundness/02e_uaf_heap_field_direct_bug.c @@ -8,5 +8,5 @@ int main(void) { if (!o.buf) return 1; o.buf[0] = 1; drop(&o); - return o.buf[0]; // BUG: use-after-free + return o.buf[0]; // TRAP: heap-use-after-free } diff --git a/test/cases/soundness/02f_uaf_param_read_helper_bug.c b/test/cases/soundness/02f_uaf_param_read_helper_bug.c index 20e1b96f..f2a5c5fa 100644 --- a/test/cases/soundness/02f_uaf_param_read_helper_bug.c +++ b/test/cases/soundness/02f_uaf_param_read_helper_bug.c @@ -6,5 +6,5 @@ int main(void) { if (!p) return 1; p[0] = 1; free(p); - return peek(p); // BUG: use-after-free + return peek(p); } diff --git a/test/cases/soundness/02g_uaf_heap_field_copied_helper_bug.c b/test/cases/soundness/02g_uaf_heap_field_copied_helper_bug.c index eb2adc3f..854a679d 100644 --- a/test/cases/soundness/02g_uaf_heap_field_copied_helper_bug.c +++ b/test/cases/soundness/02g_uaf_heap_field_copied_helper_bug.c @@ -6,7 +6,7 @@ #include struct s { char *buf; }; static void drop(struct s *o) { free(o->buf); } -static int peek(struct s *o) { char *b = o->buf; return b[0]; } // BUG: use-after-free // TRAP +static int peek(struct s *o) { char *b = o->buf; return b[0]; } // TRAP static int peek_past(struct s *o) { char *b = o->buf + 1; return b[-1]; } int main(void) { struct s o; diff --git a/test/cases/soundness/03_double_free_loop_bug.c b/test/cases/soundness/03_double_free_loop_bug.c index ed9d4cdb..05d277c1 100644 --- a/test/cases/soundness/03_double_free_loop_bug.c +++ b/test/cases/soundness/03_double_free_loop_bug.c @@ -7,7 +7,7 @@ int main(int argc, char **argv) { if (!p) return 1; for (int i = 0; i < argc + 1; i++) { if (i > 5) break; - free(p); // BUG: double-free // TRAP + free(p); // TRAP } return 0; } diff --git a/test/cases/soundness/03_double_free_loop_ok.c b/test/cases/soundness/03_double_free_loop_ok.c index 31dd8e62..8038010a 100644 --- a/test/cases/soundness/03_double_free_loop_ok.c +++ b/test/cases/soundness/03_double_free_loop_ok.c @@ -7,7 +7,6 @@ // 'p' still reads as may-owned and the exit reports a possible leak. The fix // is a loop-head state that distinguishes the first turn from the later ones, // which RFC 0030 does not have; until then the warning is tolerated here. -// ALLOW: leak // ASAN #include int main(int argc, char **argv) { diff --git a/test/cases/soundness/04_iter_invalidation_realloc_bug.c b/test/cases/soundness/04_iter_invalidation_realloc_bug.c index 321162af..ebb782cf 100644 --- a/test/cases/soundness/04_iter_invalidation_realloc_bug.c +++ b/test/cases/soundness/04_iter_invalidation_realloc_bug.c @@ -11,7 +11,7 @@ int main(int argc, char **argv) { int *nv = realloc(v, cap * sizeof *nv); if (!nv) { free(v); return 1; } v = nv; - *it = 42; // BUG: use-after-move + *it = 42; // TRAP: heap-use-after-free int r = v[1]; free(v); return r; diff --git a/test/cases/soundness/05_oob_while_offbyone_bug.c b/test/cases/soundness/05_oob_while_offbyone_bug.c index 62b9e697..a973d379 100644 --- a/test/cases/soundness/05_oob_while_offbyone_bug.c +++ b/test/cases/soundness/05_oob_while_offbyone_bug.c @@ -4,7 +4,7 @@ int main(void) { char buf[8]; int i = 0; while (i <= 8) { - buf[i] = 0; // BUG: out-of-bounds // TRAP + buf[i] = 0; // TRAP i++; } return buf[0]; diff --git a/test/cases/soundness/06_int_overflow_malloc_bug.c b/test/cases/soundness/06_int_overflow_malloc_bug.c index a4284a60..3d5373b1 100644 --- a/test/cases/soundness/06_int_overflow_malloc_bug.c +++ b/test/cases/soundness/06_int_overflow_malloc_bug.c @@ -8,7 +8,7 @@ int main(int argc, char **argv) { unsigned bytes = n * 4u; /* wraps for n >= 2^30 */ int *a = malloc(bytes); if (!a) return 1; - for (unsigned i = 0; i < n; i++) a[i] = 0; // BUG: out-of-bounds // TRAP + for (unsigned i = 0; i < n; i++) a[i] = 0; // TRAP free(a); return 0; } diff --git a/test/cases/soundness/07b_sprintf_stack_overflow_bug.c b/test/cases/soundness/07b_sprintf_stack_overflow_bug.c index 8ea6f852..cf7223b8 100644 --- a/test/cases/soundness/07b_sprintf_stack_overflow_bug.c +++ b/test/cases/soundness/07b_sprintf_stack_overflow_bug.c @@ -4,6 +4,6 @@ int main(int argc, char **argv) { char buf[8]; if (argc < 2) return 0; - sprintf(buf, "x=%s", argv[1]); // BUG: out-of-bounds // TRAP + sprintf(buf, "x=%s", argv[1]); // TRAP return buf[0]; } diff --git a/test/cases/soundness/08_uninit_index_bug.c b/test/cases/soundness/08_uninit_index_bug.c index 205198b9..f1a310d5 100644 --- a/test/cases/soundness/08_uninit_index_bug.c +++ b/test/cases/soundness/08_uninit_index_bug.c @@ -1,8 +1,10 @@ +// Zero-initialisation (RFC 0035 §1) defines the read: the run is clean. +// CLEAN // Uninitialized scalar used as array index. int main(int argc, char **argv) { (void)argv; int table[4] = {1, 2, 3, 4}; int idx; if (argc > 5) idx = 1; - return table[idx]; // BUG: use-of-uninitialized // NEUTRALISED: zero-init + return table[idx]; } diff --git a/test/cases/soundness/09_type_confusion_voidp_bug.c b/test/cases/soundness/09_type_confusion_voidp_bug.c index 5332c055..d0d8e0c6 100644 --- a/test/cases/soundness/09_type_confusion_voidp_bug.c +++ b/test/cases/soundness/09_type_confusion_voidp_bug.c @@ -8,7 +8,7 @@ int main(void) { struct small *s = malloc(sizeof *s); if (!s) return 1; s->x = 1; - int r = read_big(s); // BUG: out-of-bounds + int r = read_big(s); free(s); return r; } diff --git a/test/cases/soundness/10_union_punning_bug.c b/test/cases/soundness/10_union_punning_bug.c index cf214ac3..c76d7346 100644 --- a/test/cases/soundness/10_union_punning_bug.c +++ b/test/cases/soundness/10_union_punning_bug.c @@ -6,5 +6,5 @@ int main(int argc, char **argv) { (void)argv; union pun u; u.bits = (uintptr_t)argc * 0x1000u; - return *u.p; // BUG: out-of-bounds + return *u.p; // TRAP: null-dereference } diff --git a/test/cases/soundness/11_dangling_outparam_bug.c b/test/cases/soundness/11_dangling_outparam_bug.c index b222f99e..88eb9310 100644 --- a/test/cases/soundness/11_dangling_outparam_bug.c +++ b/test/cases/soundness/11_dangling_outparam_bug.c @@ -1,11 +1,12 @@ // Dangling pointer returned through an out-parameter. // ASAN +// XFAIL: use after return is left to a later RFC (RFC 0036) static void get(int **out) { int local = 5; - *out = &local; // BUG: lifetime-too-short + *out = &local; } int main(void) { int *p; get(&p); - return *p; + return *p; // TRAP } diff --git a/test/cases/soundness/12_local_in_global_bug.c b/test/cases/soundness/12_local_in_global_bug.c index 3d92c59a..968c95fa 100644 --- a/test/cases/soundness/12_local_in_global_bug.c +++ b/test/cases/soundness/12_local_in_global_bug.c @@ -1,11 +1,12 @@ // Pointer to local stored in a global, used after the frame is gone. // ASAN +// XFAIL: use after return is left to a later RFC (RFC 0036) static int *g_ptr; static void stash(void) { int local = 7; - g_ptr = &local; // BUG: lifetime-too-short + g_ptr = &local; } int main(void) { stash(); - return *g_ptr; + return *g_ptr; // TRAP } diff --git a/test/cases/soundness/13_uaf_callback_bug.c b/test/cases/soundness/13_uaf_callback_bug.c index c587cbed..33851f3f 100644 --- a/test/cases/soundness/13_uaf_callback_bug.c +++ b/test/cases/soundness/13_uaf_callback_bug.c @@ -11,5 +11,5 @@ int main(void) { c.buf[0] = 1; c.on_done = release_buf; finish(&c); - return c.buf[0]; // BUG: use-after-free + return c.buf[0]; // TRAP: heap-use-after-free } diff --git a/test/cases/soundness/13b_uaf_callback_registry_bug.c b/test/cases/soundness/13b_uaf_callback_registry_bug.c index cbdbc72a..c5c548fe 100644 --- a/test/cases/soundness/13b_uaf_callback_registry_bug.c +++ b/test/cases/soundness/13b_uaf_callback_registry_bug.c @@ -14,5 +14,5 @@ int main(void) { p[0] = 1; on_exit_hook(drop, p); run_hooks(); - return p[0]; // BUG: use-after-free // TRAP + return p[0]; // TRAP } diff --git a/test/cases/soundness/13b_uaf_callback_registry_ok.c b/test/cases/soundness/13b_uaf_callback_registry_ok.c index dc2b158b..7d285a8e 100644 --- a/test/cases/soundness/13b_uaf_callback_registry_ok.c +++ b/test/cases/soundness/13b_uaf_callback_registry_ok.c @@ -4,7 +4,6 @@ // a free a later iteration may repeat is a possible finding, so a warning (§3.1, // as in repros/arrloop.c). Before S7 the call was unresolved and the release was // of unknown origin, which is never diagnosed. -// ALLOW: double-free // ASAN #include typedef void (*hook_fn)(void *); diff --git a/test/cases/soundness/14_unknown_extern_frees_bug.c b/test/cases/soundness/14_unknown_extern_frees_bug.c index b0926bc0..8daa38c1 100644 --- a/test/cases/soundness/14_unknown_extern_frees_bug.c +++ b/test/cases/soundness/14_unknown_extern_frees_bug.c @@ -9,5 +9,5 @@ int main(void) { if (!p) return 1; p[0] = 1; consume_buffer(p); - return p[0]; // BUG: use-after-free // TRAP + return p[0]; // TRAP } diff --git a/test/cases/soundness/14b_unknown_extern_buffer_bug.c b/test/cases/soundness/14b_unknown_extern_buffer_bug.c index 012961b5..7caaa6ee 100644 --- a/test/cases/soundness/14b_unknown_extern_buffer_bug.c +++ b/test/cases/soundness/14b_unknown_extern_buffer_bug.c @@ -1,7 +1,8 @@ // Unknown external returns a buffer of unknown size; caller indexes it far. +// XFAIL: the buffer comes from a unit built without WeaveC (-fno-weavec): its bounds are unknown // UNITS: 14b_extern_impl.c char *get_buffer(void); int main(void) { char *b = get_buffer(); - return b[1000]; // BUG: out-of-bounds // MISS: the buffer comes from a unit built without WeaveC (-fno-weavec): its bounds are unknown + return b[1000]; // TRAP } diff --git a/test/cases/soundness/14c_unknown_extern_param_bug.c b/test/cases/soundness/14c_unknown_extern_param_bug.c deleted file mode 100644 index 657feeec..00000000 --- a/test/cases/soundness/14c_unknown_extern_param_bug.c +++ /dev/null @@ -1,2 +0,0 @@ -// Exported function: parameter of unknown provenance is dereferenced/indexed with no requirement. -int first_after(const char *s, int i) { return s[i + 100]; } // BUG: out-of-bounds // MISS: nothing bounds a parameter of an exported function; the guard stops at run time diff --git a/test/cases/soundness/15_data_race_thread_bug.c b/test/cases/soundness/15_data_race_thread_bug.c index 1aeb0f03..a27bb970 100644 --- a/test/cases/soundness/15_data_race_thread_bug.c +++ b/test/cases/soundness/15_data_race_thread_bug.c @@ -11,5 +11,5 @@ int main(void) { pthread_t t; if (pthread_create(&t, NULL, worker, NULL) != 0) return 1; pthread_join(t, NULL); - return shared[0]; // BUG: use-after-free + return shared[0]; // TRAP: heap-use-after-free } diff --git a/test/cases/soundness/15b_signal_handler_bug.c b/test/cases/soundness/15b_signal_handler_bug.c index e15548d4..0fad1d15 100644 --- a/test/cases/soundness/15b_signal_handler_bug.c +++ b/test/cases/soundness/15b_signal_handler_bug.c @@ -10,5 +10,5 @@ int main(void) { g_buf[0] = 1; signal(SIGUSR1, handler); raise(SIGUSR1); - return g_buf[0]; // BUG: use-after-free + return g_buf[0]; // TRAP: heap-use-after-free } diff --git a/test/cases/soundness/16_setjmp_longjmp_bug.c b/test/cases/soundness/16_setjmp_longjmp_bug.c index 83eee1fd..9d91b37e 100644 --- a/test/cases/soundness/16_setjmp_longjmp_bug.c +++ b/test/cases/soundness/16_setjmp_longjmp_bug.c @@ -9,7 +9,7 @@ int main(void) { if (!p) return 1; p[0] = 1; if (setjmp(env) != 0) { - return p[0]; // BUG: use-after-free // TRAP + return p[0]; // TRAP } free(p); fail(); diff --git a/test/cases/soundness/16b_longjmp_dead_frame_bug.c b/test/cases/soundness/16b_longjmp_dead_frame_bug.c deleted file mode 100644 index 2bd01be9..00000000 --- a/test/cases/soundness/16b_longjmp_dead_frame_bug.c +++ /dev/null @@ -1,14 +0,0 @@ -// longjmp into a frame that has already returned. -// TOOL -#include -static jmp_buf env; -static int arm(void) { - char local[16]; - local[0] = 1; - if (setjmp(env)) return local[0]; // BUG: lifetime-too-short // MISS: a longjmp into a frame that has returned is not detected (RFC 0035, assumption A4) - return 0; -} -int main(void) { - arm(); - longjmp(env, 1); -} diff --git a/test/cases/soundness/17_variadic_misuse_bug.c b/test/cases/soundness/17_variadic_misuse_bug.c index db57570b..ac00d0e6 100644 --- a/test/cases/soundness/17_variadic_misuse_bug.c +++ b/test/cases/soundness/17_variadic_misuse_bug.c @@ -5,7 +5,7 @@ static int sum_ptrs(int n, ...) { va_list ap; va_start(ap, n); int s = 0; - for (int i = 0; i < n; i++) { int *p = va_arg(ap, int *); s += *p; } // BUG: out-of-bounds + for (int i = 0; i < n; i++) { int *p = va_arg(ap, int *); s += *p; } // TRAP: null-dereference va_end(ap); return s; } diff --git a/test/cases/soundness/18_format_string_bug.c b/test/cases/soundness/18_format_string_bug.c index 024a25df..17a9fdea 100644 --- a/test/cases/soundness/18_format_string_bug.c +++ b/test/cases/soundness/18_format_string_bug.c @@ -1,4 +1,6 @@ -// Format string: user-controlled format, and missing argument. +// Format string: user-controlled format, and missing argument. The missing +// argument is read as a string: the run stops, by the guard of the checked +// printf or by a fault on what the slot holds. // RUN-INPUT: %s%s%s%s%s%s%s%s // No ASAN marker: what ASan sees depends on whatever the missing // argument's slot holds, which differs between machines. @@ -6,6 +8,6 @@ int main(int argc, char **argv) { if (argc < 2) return 0; printf(argv[1]); - printf("%s %s\n", argv[1]); // BUG: out-of-bounds + printf("%s %s\n", argv[1]); // TRAP return 0; } diff --git a/test/cases/soundness/19_vla_size_bug.c b/test/cases/soundness/19_vla_size_bug.c index 9e9c3405..32ed0cb0 100644 --- a/test/cases/soundness/19_vla_size_bug.c +++ b/test/cases/soundness/19_vla_size_bug.c @@ -6,6 +6,6 @@ int main(int argc, char **argv) { if (argc < 2) return 0; int n = atoi(argv[1]); char vla[n]; - vla[n] = 0; // BUG: out-of-bounds // TRAP + vla[n] = 0; // TRAP return vla[0]; } diff --git a/test/cases/soundness/20_ptr_arith_past_end_bug.c b/test/cases/soundness/20_ptr_arith_past_end_bug.c index e465ce5e..3129f04d 100644 --- a/test/cases/soundness/20_ptr_arith_past_end_bug.c +++ b/test/cases/soundness/20_ptr_arith_past_end_bug.c @@ -5,5 +5,5 @@ int main(int argc, char **argv) { int a[4] = {0, 1, 2, 3}; int *p = a; p += 3 + argc; - return *p; // BUG: out-of-bounds // TRAP + return *p; // TRAP } diff --git a/test/cases/soundness/20b_ptr_walk_bug.c b/test/cases/soundness/20b_ptr_walk_bug.c index bc251d2f..24dcfca8 100644 --- a/test/cases/soundness/20b_ptr_walk_bug.c +++ b/test/cases/soundness/20b_ptr_walk_bug.c @@ -7,5 +7,5 @@ static int sum(const int *b, const int *e) { } int main(void) { int a[4] = {1, 2, 3, 4}; - return sum(a, a + 4); // BUG: out-of-bounds + return sum(a, a + 4); } diff --git a/test/cases/soundness/21_memcpy_overlap_bug.c b/test/cases/soundness/21_memcpy_overlap_bug.c index aa678521..57d60c18 100644 --- a/test/cases/soundness/21_memcpy_overlap_bug.c +++ b/test/cases/soundness/21_memcpy_overlap_bug.c @@ -3,6 +3,6 @@ #include int main(void) { char buf[16] = "abcdefghijklmno"; - memcpy(buf + 2, buf, 8); // BUG: out-of-bounds + memcpy(buf + 2, buf, 8); // TRAP: overlapping-copy return buf[3]; } diff --git a/test/cases/soundness/22_realloc_zero_bug.c b/test/cases/soundness/22_realloc_zero_bug.c deleted file mode 100644 index 5367e22a..00000000 --- a/test/cases/soundness/22_realloc_zero_bug.c +++ /dev/null @@ -1,12 +0,0 @@ -// realloc(p, 0) (may free p, UB in C23), then old pointer freed again / used. -// ASAN -#include -int main(void) { - char *p = malloc(8); - if (!p) return 1; - p[0] = 1; - char *q = realloc(p, 0); - (void)q; - free(p); // BUG: use-after-move - return 0; -} diff --git a/test/cases/soundness/22b_realloc_alias_bug.c b/test/cases/soundness/22b_realloc_alias_bug.c index c6554eb3..f8056216 100644 --- a/test/cases/soundness/22b_realloc_alias_bug.c +++ b/test/cases/soundness/22b_realloc_alias_bug.c @@ -8,7 +8,7 @@ int main(void) { char *alias = p; char *q = realloc(p, 1 << 20); if (!q) { free(p); return 1; } - int r = alias[0]; // BUG: use-after-move + int r = alias[0]; // TRAP: heap-use-after-free free(q); return r; } diff --git a/test/cases/soundness/23_alloca_bug.c b/test/cases/soundness/23_alloca_bug.c index ce7c8641..00a0ceaf 100644 --- a/test/cases/soundness/23_alloca_bug.c +++ b/test/cases/soundness/23_alloca_bug.c @@ -4,11 +4,11 @@ static char *make(void) { char *p = alloca(8); p[0] = 1; - return p; // BUG: lifetime-too-short + return p; } int main(void) { char *q = alloca(8); - q[8] = 1; // BUG: out-of-bounds + q[8] = 1; // TRAP: stack-buffer-overflow char *d = make(); return d[0]; } diff --git a/test/cases/soundness/24_flexible_array_bug.c b/test/cases/soundness/24_flexible_array_bug.c index 03269251..b501dd73 100644 --- a/test/cases/soundness/24_flexible_array_bug.c +++ b/test/cases/soundness/24_flexible_array_bug.c @@ -8,7 +8,7 @@ int main(int argc, char **argv) { struct msg *m = malloc(sizeof *m + 4); if (!m) return 1; m->len = 4; - for (int i = 0; i <= m->len; i++) m->data[i] = (char)argc; // BUG: out-of-bounds // TRAP + for (int i = 0; i <= m->len; i++) m->data[i] = (char)argc; // TRAP int r = m->data[0]; free(m); return r; diff --git a/test/cases/soundness/25_strtok_static_bug.c b/test/cases/soundness/25_strtok_static_bug.c index d335c4c2..b28dcce4 100644 --- a/test/cases/soundness/25_strtok_static_bug.c +++ b/test/cases/soundness/25_strtok_static_bug.c @@ -6,6 +6,6 @@ int main(void) { if (!s) return 1; char *t = strtok(s, ","); free(s); - t = strtok(NULL, ","); // BUG: use-after-free + t = strtok(NULL, ","); return t ? t[0] : 0; // TRAP } diff --git a/test/cases/soundness/25b_string_literal_write_bug.c b/test/cases/soundness/25b_string_literal_write_bug.c index 2f1688e0..27444547 100644 --- a/test/cases/soundness/25b_string_literal_write_bug.c +++ b/test/cases/soundness/25b_string_literal_write_bug.c @@ -1,7 +1,8 @@ -// Write into a string literal via strtok. +// Write into a string literal via strtok: the literal is read-only, so the +// write faults. #include int main(void) { char *s = "a,b"; - char *t = strtok(s, ","); // BUG: out-of-bounds + char *t = strtok(s, ","); // TRAP return t ? t[0] : 0; } diff --git a/test/cases/soundness/25c_static_result_bug.c b/test/cases/soundness/25c_static_result_bug.c deleted file mode 100644 index 483c6aa0..00000000 --- a/test/cases/soundness/25c_static_result_bug.c +++ /dev/null @@ -1,22 +0,0 @@ -// Pointer to a static result buffer invalidated by the next call (not a memory error, logic) + -// getenv result used after setenv may be freed. -// -// No oracle marker: whether this is a use-after-free depends on the libc. -// Darwin's setenv releases the entry it replaces when the new value does not -// fit, and ASan reports it; glibc never frees a published "NAME=value" string -// (__add_to_environ overwrites the slot and keeps the old string), precisely -// because callers may still hold a getenv result, so on Linux ASan has nothing -// to find. The BUG marker stays: WeaveC cannot know which libc the program will -// be linked against, so it must report the read either way. The run (no -// arguments) stops before the read: on Darwin the runtime's guard traps there -// (RFC 0033 §6: the C library's allocations are in the arena), on glibc -// nothing is freed. -#include -int main(int argc, char **argv) { - (void)argv; - if (argc < 2) return 0; - setenv("WEAVEC_PROBE", "first-value", 1); - const char *v = getenv("WEAVEC_PROBE"); - setenv("WEAVEC_PROBE", "a-much-longer-second-value-to-force-reallocation", 1); - return v ? v[0] : 0; // BUG: use-after-free -} diff --git a/test/cases/soundness/26_container_uaf_ok.c b/test/cases/soundness/26_container_uaf_ok.c index 3e7b44b7..b239b20e 100644 --- a/test/cases/soundness/26_container_uaf_ok.c +++ b/test/cases/soundness/26_container_uaf_ok.c @@ -8,7 +8,6 @@ // place is not, and both exits report it as a possible leak. Recognising a // fill loop that bails out on a failed allocation would retire it; that is an // engine change RFC 0030 has not made, so the warning is tolerated here. -// ALLOW: leak // ASAN #include int main(void) { diff --git a/test/cases/soundness/27_struct_return_dangling_bug.c b/test/cases/soundness/27_struct_return_dangling_bug.c index 50f5fd41..f03fef76 100644 --- a/test/cases/soundness/27_struct_return_dangling_bug.c +++ b/test/cases/soundness/27_struct_return_dangling_bug.c @@ -1,12 +1,13 @@ // Pointer to local returned inside a struct by value. // ASAN +// XFAIL: use after return is left to a later RFC (RFC 0036) struct view { int *p; }; static struct view make(void) { int x = 5; struct view v = { &x }; - return v; // BUG: lifetime-too-short + return v; } int main(void) { struct view v = make(); - return *v.p; + return *v.p; // TRAP } diff --git a/test/cases/soundness/28_free_non_heap_bug.c b/test/cases/soundness/28_free_non_heap_bug.c index 94fa78a3..a0c5725b 100644 --- a/test/cases/soundness/28_free_non_heap_bug.c +++ b/test/cases/soundness/28_free_non_heap_bug.c @@ -4,6 +4,6 @@ static void release(void *p) { free(p); } int main(void) { int x = 0; int *p = &x; - release(p); // BUG: invalid-release + release(p); // TRAP: invalid-release return 0; } diff --git a/test/cases/soundness/29_negative_index_bug.c b/test/cases/soundness/29_negative_index_bug.c index 345e4ceb..fff79a5a 100644 --- a/test/cases/soundness/29_negative_index_bug.c +++ b/test/cases/soundness/29_negative_index_bug.c @@ -6,6 +6,6 @@ int main(int argc, char **argv) { if (argc < 2) return 0; int a[4] = {0, 1, 2, 3}; int i = atoi(argv[1]); - if (i < 4) return a[i]; // BUG: out-of-bounds // TRAP + if (i < 4) return a[i]; // TRAP return 0; } diff --git a/test/cases/soundness/30_unterminated_strlen_bug.c b/test/cases/soundness/30_unterminated_strlen_bug.c index 1c945605..0bd3c8fe 100644 --- a/test/cases/soundness/30_unterminated_strlen_bug.c +++ b/test/cases/soundness/30_unterminated_strlen_bug.c @@ -3,5 +3,5 @@ #include int main(void) { char buf[4] = {'a', 'b', 'c', 'd'}; - return (int)strlen(buf); // BUG: out-of-bounds + return (int)strlen(buf); // TRAP: unterminated-string } diff --git a/test/cases/soundness/31_truncation_memcpy_bug.c b/test/cases/soundness/31_truncation_memcpy_bug.c index 748c8076..5c970205 100644 --- a/test/cases/soundness/31_truncation_memcpy_bug.c +++ b/test/cases/soundness/31_truncation_memcpy_bug.c @@ -7,6 +7,6 @@ int main(int argc, char **argv) { char buf[64]; size_t len = strlen(argv[1]); unsigned char n = (unsigned char)len; - if (n < sizeof buf) memcpy(buf, argv[1], len); // BUG: out-of-bounds // TRAP + if (n < sizeof buf) memcpy(buf, argv[1], len); // TRAP return buf[0]; } diff --git a/test/cases/soundness/32_uninit_heap_read_bug.c b/test/cases/soundness/32_uninit_heap_read_bug.c index add93860..bbdf8457 100644 --- a/test/cases/soundness/32_uninit_heap_read_bug.c +++ b/test/cases/soundness/32_uninit_heap_read_bug.c @@ -1,8 +1,10 @@ +// Zero-initialisation (RFC 0035 §1) defines the read: the run is clean. +// CLEAN #include int main(void) { int *a = malloc(4 * sizeof *a); if (!a) return 1; - int r = a[2]; // BUG: use-of-uninitialized // NEUTRALISED: zero-init + int r = a[2]; free(a); return r; } diff --git a/test/cases/soundness/33_double_free_aliased_records_bug.c b/test/cases/soundness/33_double_free_aliased_records_bug.c index fae8f851..a48b6343 100644 --- a/test/cases/soundness/33_double_free_aliased_records_bug.c +++ b/test/cases/soundness/33_double_free_aliased_records_bug.c @@ -10,6 +10,6 @@ int main(void) { struct obj *a = obj_new(d); struct obj *b = obj_new(d); obj_free(a); - obj_free(b); // BUG: double-free + obj_free(b); // TRAP: invalid-release return 0; } diff --git a/test/cases/soundness/34_goto_cleanup_double_free_bug.c b/test/cases/soundness/34_goto_cleanup_double_free_bug.c index 2ea5431e..b5db31bb 100644 --- a/test/cases/soundness/34_goto_cleanup_double_free_bug.c +++ b/test/cases/soundness/34_goto_cleanup_double_free_bug.c @@ -12,7 +12,7 @@ int work(int fail) { return 0; err: free(b); - free(a); // BUG: double-free // TRAP + free(a); // TRAP return -1; } int main(int argc, char **argv) { (void)argv; return work(argc > 1); } diff --git a/test/cases/soundness/35_uninit_ptr_field_bug.c b/test/cases/soundness/35_uninit_ptr_field_bug.c index 4658b6ca..b9ee9758 100644 --- a/test/cases/soundness/35_uninit_ptr_field_bug.c +++ b/test/cases/soundness/35_uninit_ptr_field_bug.c @@ -2,5 +2,5 @@ struct node { int v; struct node *next; }; int main(void) { struct node n; n.v = 1; - return n.next->v; // BUG: use-of-uninitialized + return n.next->v; // TRAP: null-dereference } diff --git a/test/cases/soundness/36_heap_oob_param_bug.c b/test/cases/soundness/36_heap_oob_param_bug.c index 3343184f..ee5a7b43 100644 --- a/test/cases/soundness/36_heap_oob_param_bug.c +++ b/test/cases/soundness/36_heap_oob_param_bug.c @@ -6,7 +6,7 @@ int main(int argc, char **argv) { (void)argv; char *p = malloc(4); if (!p) return 1; - fill(p, (size_t)argc + 4); // BUG: out-of-bounds // MISS: the analysis does not report a call's argument against the callee's requirement (RFC 0035 §8); the guard stops in the callee + fill(p, (size_t)argc + 4); free(p); return 0; } diff --git a/test/cases/soundness/37_exported_api_no_callers_bug.c b/test/cases/soundness/37_exported_api_no_callers_bug.c deleted file mode 100644 index b5e5d38e..00000000 --- a/test/cases/soundness/37_exported_api_no_callers_bug.c +++ /dev/null @@ -1,13 +0,0 @@ -// A library-style exported API with no callers in the program: are bugs inside it found? -#include -#include -struct buf { char *data; size_t len, cap; }; -int buf_push(struct buf *b, char c) { - if (b->len > b->cap) return -1; /* should be >= */ - // RFC 0030 §7.3: 'b' is Single by A1, so the loads of b->data and b->len on the - // next line are proven; the subscript, the bug site, has no extent. - b->data[b->len++] = c; // BUG: out-of-bounds // MISS: nothing bounds a field of an exported function's parameter; the guard stops at run time - return 0; -} -void buf_reset(struct buf *b) { free(b->data); b->len = 0; } /* data dangles */ -int buf_first(struct buf *b) { return b->data[0]; } diff --git a/test/cases/soundness/38_extern_impl.c b/test/cases/soundness/38_extern_impl.c index 82b1ebe9..73c42424 100644 --- a/test/cases/soundness/38_extern_impl.c +++ b/test/cases/soundness/38_extern_impl.c @@ -1,4 +1,4 @@ // Second unit of 38_lying_annotation_bug.c: the definition that contradicts the -// WEAVEC_BORROWED declaration; the link step must verify it (RFC 0030 section 13.2). +// declaration; the link step must verify it (RFC 0030 section 13.2). #include void inspect(char *p) { free(p); } diff --git a/test/cases/soundness/38_lying_annotation_bug.c b/test/cases/soundness/38_lying_annotation_bug.c index 024d5a75..984b9d73 100644 --- a/test/cases/soundness/38_lying_annotation_bug.c +++ b/test/cases/soundness/38_lying_annotation_bug.c @@ -2,8 +2,7 @@ // UNITS: 38_extern_impl.c // ASAN #include -#include -void inspect(char *WEAVEC_BORROWED p); // BUG: annotation-mismatch definite +void inspect(char * p); int main(void) { char *p = malloc(8); if (!p) return 1; diff --git a/test/cases/soundness/39_false_assume_bug.c b/test/cases/soundness/39_false_assume_bug.c deleted file mode 100644 index d121f40a..00000000 --- a/test/cases/soundness/39_false_assume_bug.c +++ /dev/null @@ -1,10 +0,0 @@ -// ASAN -#include -#include -int main(int argc, char **argv) { - (void)argv; - char buf[4] = {0}; - int i = argc + 10; - WEAVEC_ASSUME(i < 4); // BUG: contradicted-assumption // MISS: the analysis does not refute an assumption on a value from the input - return buf[i]; // TRAP -} diff --git a/test/cases/soundness/40_struct_copy_uaf_bug.c b/test/cases/soundness/40_struct_copy_uaf_bug.c index e96a3d09..6dfdaca0 100644 --- a/test/cases/soundness/40_struct_copy_uaf_bug.c +++ b/test/cases/soundness/40_struct_copy_uaf_bug.c @@ -10,5 +10,5 @@ int main(void) { h1.n = 8; struct holder h2 = h1; free(h1.buf); - return h2.buf[0]; // BUG: use-after-free + return h2.buf[0]; // TRAP: heap-use-after-free } diff --git a/test/cases/soundness/41b_late_iteration_uaf_bug.c b/test/cases/soundness/41b_late_iteration_uaf_bug.c index 186c7afa..2b98bfe6 100644 --- a/test/cases/soundness/41b_late_iteration_uaf_bug.c +++ b/test/cases/soundness/41b_late_iteration_uaf_bug.c @@ -8,7 +8,7 @@ int main(void) { int s = 0; for (int i = 0; i < 100; i++) { if (i == 50) free(p); - if (i == 60) s += p[0]; // BUG: use-after-free // TRAP + if (i == 60) s += p[0]; // TRAP } return s; } diff --git a/test/cases/soundness/41c_deep_call_chain_bug.c b/test/cases/soundness/41c_deep_call_chain_bug.c index 06882a15..abd9b1ef 100644 --- a/test/cases/soundness/41c_deep_call_chain_bug.c +++ b/test/cases/soundness/41c_deep_call_chain_bug.c @@ -12,5 +12,5 @@ int main(void) { if (!p) return 1; p[0] = 1; f1(p); - return p[0]; // BUG: use-after-free + return p[0]; // TRAP: heap-use-after-free } diff --git a/test/cases/soundness/41d_many_branches_bug.c b/test/cases/soundness/41d_many_branches_bug.c index b82587b9..fd537d99 100644 --- a/test/cases/soundness/41d_many_branches_bug.c +++ b/test/cases/soundness/41d_many_branches_bug.c @@ -13,5 +13,5 @@ int main(int argc, char **argv) { B(0) B(1) B(2) B(3) B(4) B(5) B(6) B(7) B(8) B(9) B(10) B(11) B(12) B(13) B(14) B(15) B(16) B(17) B(18) B(19) B(20) B(21) B(22) B(23) B(24) B(25) B(26) B(27) B(28) B(29) free(p); - return x + p[argc & 1]; // BUG: use-after-free // TRAP + return x + p[argc & 1]; // TRAP } diff --git a/test/cases/soundness/42_byte_copied_pointer_bug.c b/test/cases/soundness/42_byte_copied_pointer_bug.c index 94b76779..b0f89299 100644 --- a/test/cases/soundness/42_byte_copied_pointer_bug.c +++ b/test/cases/soundness/42_byte_copied_pointer_bug.c @@ -12,5 +12,5 @@ int main(void) { unsigned char *src = (unsigned char *)&p, *dst = (unsigned char *)&q; for (size_t i = 0; i < sizeof p; i++) dst[i] = src[i]; free(p); - return q[0]; // BUG: use-after-free // TRAP + return q[0]; // TRAP } diff --git a/test/cases/soundness/43_list_free_then_next_bug.c b/test/cases/soundness/43_list_free_then_next_bug.c index 67cc79d0..83de54ca 100644 --- a/test/cases/soundness/43_list_free_then_next_bug.c +++ b/test/cases/soundness/43_list_free_then_next_bug.c @@ -3,7 +3,7 @@ #include struct node { int v; struct node *next; }; static void list_free(struct node *h) { - for (struct node *n = h; n; n = n->next) free(n); // BUG: use-after-free + for (struct node *n = h; n; n = n->next) free(n); // TRAP: heap-use-after-free } int main(void) { struct node *a = malloc(sizeof *a), *b = malloc(sizeof *b); diff --git a/test/cases/soundness/44_vector_element_ptr_bug.c b/test/cases/soundness/44_vector_element_ptr_bug.c index 4be150e0..7402ef75 100644 --- a/test/cases/soundness/44_vector_element_ptr_bug.c +++ b/test/cases/soundness/44_vector_element_ptr_bug.c @@ -17,7 +17,7 @@ int main(void) { if (vec_push(&v, 1)) return 1; int *first = &v.data[0]; for (int i = 0; i < 100; i++) if (vec_push(&v, i)) break; - int r = *first; // BUG: use-after-move // TRAP + int r = *first; // TRAP free(v.data); return r; } diff --git a/test/cases/soundness/45_unsafe_region_hides_bug.c b/test/cases/soundness/45_unsafe_region_hides_bug.c index ce3516fa..77d88177 100644 --- a/test/cases/soundness/45_unsafe_region_hides_bug.c +++ b/test/cases/soundness/45_unsafe_region_hides_bug.c @@ -1,10 +1,11 @@ // ASAN +// XFAIL: an unsafe region turns the guards off: the use after free runs unchecked (ASan reports it) #include #include int main(void) { char *p = malloc(8); if (!p) return 1; p[0] = 1; - WEAVEC_UNSAFE { free(p); p[1] = 2; } // BUG: use-after-free definite + WEAVEC_UNSAFE { free(p); p[1] = 2; } // TRAP // UNGUARDED: unsafe return 0; } diff --git a/test/cases/soundness/46_param_null_deref_bug.c b/test/cases/soundness/46_param_null_deref_bug.c index e033d15e..3e911093 100644 --- a/test/cases/soundness/46_param_null_deref_bug.c +++ b/test/cases/soundness/46_param_null_deref_bug.c @@ -8,7 +8,7 @@ int main(void) { if (!c) return 1; // RFC 0030 §7.5: first() is static and must-accesses s[0], so each call checks its // argument for null (the engine does not know c->name is zero-filled). - int r = first(c->name); // BUG: null-dereference + int r = first(c->name); free(c); return r; } diff --git a/test/cases/soundness/47_uaf_via_qsort_cb_bug.c b/test/cases/soundness/47_uaf_via_qsort_cb_bug.c index 0436f641..3d119f8e 100644 --- a/test/cases/soundness/47_uaf_via_qsort_cb_bug.c +++ b/test/cases/soundness/47_uaf_via_qsort_cb_bug.c @@ -13,5 +13,5 @@ int main(void) { p[0] = 1; g_victim = p; qsort(xs, 2, sizeof xs[0], cmp); - return p[0]; // BUG: use-after-free // TRAP + return p[0]; // TRAP } diff --git a/test/cases/soundness/49_negative_index_via_helper_bug.c b/test/cases/soundness/49_negative_index_via_helper_bug.c index 748561a1..cb6a0bef 100644 --- a/test/cases/soundness/49_negative_index_via_helper_bug.c +++ b/test/cases/soundness/49_negative_index_via_helper_bug.c @@ -3,7 +3,7 @@ // ASAN #include static int table[4] = {1, 2, 3, 4}; -static int lookup(int i) { return table[i]; } // BUG: out-of-bounds // TRAP +static int lookup(int i) { return table[i]; } // TRAP int main(int argc, char **argv) { if (argc < 2) return 0; return lookup(atoi(argv[1])); diff --git a/test/cases/soundness/50_param_passthrough_uaf_bug.c b/test/cases/soundness/50_param_passthrough_uaf_bug.c index e5022363..73fe6c50 100644 --- a/test/cases/soundness/50_param_passthrough_uaf_bug.c +++ b/test/cases/soundness/50_param_passthrough_uaf_bug.c @@ -1,7 +1,7 @@ // Exported function: frees one param and reads another which may alias (no callers here). // ASAN #include -int release_then_read(char *a, char *b) { free(a); return b[0]; } // BUG: use-after-free +int release_then_read(char *a, char *b) { free(a); return b[0]; } // TRAP: heap-use-after-free int main(void) { char *p = malloc(8); if (!p) return 1; diff --git a/test/cases/soundness/README.md b/test/cases/soundness/README.md index e562b4cc..ddffe266 100644 --- a/test/cases/soundness/README.md +++ b/test/cases/soundness/README.md @@ -1,19 +1,28 @@ # Soundness probes -The 113 soundness probes of RFC 0030 (*Motivation*, *Projected outcomes on the -soundness probes*, gate G4): 85 programs with a real memory-safety bug -(`*_bug.c`) and 28 correct twins (`*_ok.c`, plus the two correct programs -`41e_infeasible_double_free_fp.c` and `48_simple_sum_fp.c`). They were -imported unchanged in S0 apart from their markers, a header comment on each -extra unit and one added unit; the file names are the original ones. `scripts/run-cases.py` runs them -(`test/cases/README.md` has the marker grammar): +The 113 soundness probes of RFC 0030: 85 programs with a real memory-safety +bug (`*_bug.c`) and 28 correct twins (`*_ok.c`, plus the two correct +programs `41e_infeasible_double_free_fp.c` and `48_simple_sum_fp.c`). +`scripts/run-cases.py` runs them (`test/cases/README.md` has the marker +grammar): ```sh -scripts/run-cases.py --filter 'soundness/**' --asan # gate G4 -WEAVEC_GOLDEN_DIR= scripts/run-cases.py --legacy --filter 'soundness/**' # S0 +scripts/run-cases.py --filter 'soundness/**' --asan ``` -## Markers +Today each bug probe pins where the default `weavec-cc` build stops it with +`TRAP`, and each twin is `CLEAN`. Six bug probes are `XFAIL`, with the +reason in their file: three uses after return (11, 12, 27), which RFC 0036 +leaves to a later RFC; two bugs in units built without WeaveC (14b, c13); +and one inside a `WEAVEC_UNSAFE` region (45). Two read an uninitialised +value that zero-initialisation defines (08, 32) and are `CLEAN`. + +The rest of this page is the record of the import (RFC 0030, S0): the +markers the probes had then, and the classification of each probe by +v0.10.0 and by RFC 0030's projection. The analysis those columns describe +was deleted by RFC 0036. + +## History: the markers at the import - Every bug probe has one `BUG` marker with the probe's *matching id*, the id v0.10.0 reports for the caught probes, on the line where RFC 0030 diff --git a/test/cases/soundness/alias-cursor-several-arrays_bug.c b/test/cases/soundness/alias-cursor-several-arrays_bug.c index bda78a56..160a42e5 100644 --- a/test/cases/soundness/alias-cursor-several-arrays_bug.c +++ b/test/cases/soundness/alias-cursor-several-arrays_bug.c @@ -12,7 +12,7 @@ int main(void) { for (unsigned s = 0; s < 2; ++s) { const char **cursor = all[s]; for (unsigned i = 0; i < 3 + s; ++i) { - total += (int)strlen(*cursor); // BUG: out-of-bounds // TRAP + total += (int)strlen(*cursor); // TRAP cursor++; } } diff --git a/test/cases/soundness/alias-global-element-const_bug.c b/test/cases/soundness/alias-global-element-const_bug.c index 32713980..b0ded26e 100644 --- a/test/cases/soundness/alias-global-element-const_bug.c +++ b/test/cases/soundness/alias-global-element-const_bug.c @@ -10,7 +10,7 @@ static int *g[4]; static int seen; static void add(int *p) { g[1] = p; } static void fire(void) { - if (g[1]) seen += *g[1]; // BUG: use-after-free // TRAP + if (g[1]) seen += *g[1]; // TRAP } int main(void) { int *x = malloc(sizeof *x); diff --git a/test/cases/soundness/alias-global-element-index_bug.c b/test/cases/soundness/alias-global-element-index_bug.c index d0047845..077bff87 100644 --- a/test/cases/soundness/alias-global-element-index_bug.c +++ b/test/cases/soundness/alias-global-element-index_bug.c @@ -10,7 +10,7 @@ static void add(int *p) { } static void fire(void) { for (int i = 0; i < n; i++) - seen += *g[i]; // BUG: use-after-free // TRAP + seen += *g[i]; // TRAP } int main(void) { int *x = malloc(sizeof *x); diff --git a/test/cases/soundness/alias-null-box-field_bug.c b/test/cases/soundness/alias-null-box-field_bug.c index f1eb3aa4..6025c72e 100644 --- a/test/cases/soundness/alias-null-box-field_bug.c +++ b/test/cases/soundness/alias-null-box-field_bug.c @@ -13,6 +13,6 @@ int s2(void) { if (!o) abort(); bx.buf = o; q->buf = NULL; - return bx.buf[0]; /* null deref */ // BUG: null-dereference // TRAP + return bx.buf[0]; /* null deref */ // TRAP } int main(void) { return s2() == 12345; } diff --git a/test/cases/soundness/alias-oob-box-field_bug.c b/test/cases/soundness/alias-oob-box-field_bug.c index dd9a21b2..3dcdcd20 100644 --- a/test/cases/soundness/alias-oob-box-field_bug.c +++ b/test/cases/soundness/alias-oob-box-field_bug.c @@ -9,7 +9,7 @@ int s1(struct box *bp) { if (!o) abort(); bp->buf = o; struct box *q = bp; - return q->buf[10]; // BUG: out-of-bounds // TRAP + return q->buf[10]; // TRAP } int main(void) { struct box b; diff --git a/test/cases/soundness/alias-oob-replaced-buffer-direct_bug.c b/test/cases/soundness/alias-oob-replaced-buffer-direct_bug.c index 0c6bc4a1..43196440 100644 --- a/test/cases/soundness/alias-oob-replaced-buffer-direct_bug.c +++ b/test/cases/soundness/alias-oob-replaced-buffer-direct_bug.c @@ -14,7 +14,7 @@ __attribute__((noinline)) void s3(int k) { keep1 = bx.buf; bx.buf = malloc(2 * sizeof(int)); if (!q->buf) abort(); - bx.buf[10] = k; /* heap overflow: bx.buf now has 2 ints */ // BUG: out-of-bounds // TRAP + bx.buf[10] = k; /* heap overflow: bx.buf now has 2 ints */ // TRAP keep2 = bx.buf; } int main(int argc, char **argv) { (void)argv; s3(argc); puts("completed without trap"); return 0; } diff --git a/test/cases/soundness/alias-oob-replaced-buffer-kept_bug.c b/test/cases/soundness/alias-oob-replaced-buffer-kept_bug.c index 5a9d09a3..3d261135 100644 --- a/test/cases/soundness/alias-oob-replaced-buffer-kept_bug.c +++ b/test/cases/soundness/alias-oob-replaced-buffer-kept_bug.c @@ -15,7 +15,7 @@ __attribute__((noinline)) void s3(int k) { keep1 = bx.buf; q->buf = malloc(2 * sizeof(int)); if (!q->buf) abort(); - bx.buf[10] = k; /* heap overflow: bx.buf now has 2 ints */ // BUG: out-of-bounds // TRAP + bx.buf[10] = k; /* heap overflow: bx.buf now has 2 ints */ // TRAP keep2 = bx.buf; } int main(int argc, char **argv) { (void)argv; s3(argc); puts("completed without trap"); return 0; } diff --git a/test/cases/soundness/alias-oob-replaced-buffer_bug.c b/test/cases/soundness/alias-oob-replaced-buffer_bug.c index f5a0acdf..52235a88 100644 --- a/test/cases/soundness/alias-oob-replaced-buffer_bug.c +++ b/test/cases/soundness/alias-oob-replaced-buffer_bug.c @@ -14,7 +14,7 @@ void s3(int k) { int *old = bx.buf; q->buf = malloc(2 * sizeof(int)); if (!q->buf) abort(); - bx.buf[10] = k; /* heap overflow: bx.buf now has 2 ints */ // BUG: out-of-bounds // TRAP + bx.buf[10] = k; /* heap overflow: bx.buf now has 2 ints */ // TRAP free(old); free(bx.buf); } diff --git a/test/cases/soundness/alias-record-element_bug.c b/test/cases/soundness/alias-record-element_bug.c index bc6c63dd..b70e8074 100644 --- a/test/cases/soundness/alias-record-element_bug.c +++ b/test/cases/soundness/alias-record-element_bug.c @@ -7,7 +7,7 @@ static struct reg registry; static int seen; static void add(struct reg *r, int *p) { r->slot[1] = p; } static void fire(const struct reg *r) { - if (r->slot[1]) seen += *r->slot[1]; // BUG: use-after-free // TRAP + if (r->slot[1]) seen += *r->slot[1]; // TRAP } int main(void) { int *x = malloc(sizeof *x); diff --git a/test/cases/soundness/alias-uaf-alias-before-store_bug.c b/test/cases/soundness/alias-uaf-alias-before-store_bug.c index 192db0b1..21c540be 100644 --- a/test/cases/soundness/alias-uaf-alias-before-store_bug.c +++ b/test/cases/soundness/alias-uaf-alias-before-store_bug.c @@ -13,6 +13,6 @@ int a4(void) { /* heap cell, alias assigned before store */ struct n **alias = slot; *slot = o; free(o); - return (*alias)->v; // BUG: use-after-free + return (*alias)->v; // TRAP: heap-use-after-free } int main(void) { return a4() == 12345; } diff --git a/test/cases/soundness/alias-uaf-heap-box-field_bug.c b/test/cases/soundness/alias-uaf-heap-box-field_bug.c index e99b1c41..7ca48b68 100644 --- a/test/cases/soundness/alias-uaf-heap-box-field_bug.c +++ b/test/cases/soundness/alias-uaf-heap-box-field_bug.c @@ -14,7 +14,7 @@ int b1(void) { bp->a = o; struct box *q = bp; free(o); - int r = q->a->v; // BUG: use-after-free + int r = q->a->v; // TRAP: heap-use-after-free free(bp); return r; } diff --git a/test/cases/soundness/alias-uaf-heap-cell-alias_bug.c b/test/cases/soundness/alias-uaf-heap-cell-alias_bug.c index d9b126f7..1b403180 100644 --- a/test/cases/soundness/alias-uaf-heap-cell-alias_bug.c +++ b/test/cases/soundness/alias-uaf-heap-cell-alias_bug.c @@ -15,6 +15,6 @@ int a3(void) { /* heap cell, alias, no early-return join */ *slot = o; struct n **alias = slot; free(o); - return (*alias)->v; // BUG: use-after-free + return (*alias)->v; // TRAP: heap-use-after-free } int main(void) { return a3() == 12345; } diff --git a/test/cases/soundness/alias-uaf-heap-cell_bug.c b/test/cases/soundness/alias-uaf-heap-cell_bug.c index 524d34c0..fa083ead 100644 --- a/test/cases/soundness/alias-uaf-heap-cell_bug.c +++ b/test/cases/soundness/alias-uaf-heap-cell_bug.c @@ -10,7 +10,7 @@ int a1(void) { /* no alias, heap cell */ if (!slot || !o) { free(slot); free(o); return 0; } *slot = o; free(o); - int r = (*slot)->v; // BUG: use-after-free + int r = (*slot)->v; // TRAP: heap-use-after-free free(slot); return r; } diff --git a/test/cases/soundness/alias-uaf-stack-box-field_bug.c b/test/cases/soundness/alias-uaf-stack-box-field_bug.c index 20ff0d25..786491e9 100644 --- a/test/cases/soundness/alias-uaf-stack-box-field_bug.c +++ b/test/cases/soundness/alias-uaf-stack-box-field_bug.c @@ -14,6 +14,6 @@ int b2(void) { bx.a = o; struct box *q = &bx; free(o); - return q->a->v; // BUG: use-after-free + return q->a->v; // TRAP: heap-use-after-free } int main(void) { return b2() == 12345; } diff --git a/test/cases/soundness/alias-uaf-stack-cell_bug.c b/test/cases/soundness/alias-uaf-stack-cell_bug.c index a93f483c..5e23d5e6 100644 --- a/test/cases/soundness/alias-uaf-stack-cell_bug.c +++ b/test/cases/soundness/alias-uaf-stack-cell_bug.c @@ -16,6 +16,6 @@ int a2(void) { /* stack cell, alias */ *slot = o; struct n **alias = slot; free(o); - return (*alias)->v; // BUG: use-after-free + return (*alias)->v; // TRAP: heap-use-after-free } int main(void) { return a2() == 12345; } diff --git a/test/cases/soundness/c01_global_realloc_between_bug.c b/test/cases/soundness/c01_global_realloc_between_bug.c index 77112fc4..f9ca1750 100644 --- a/test/cases/soundness/c01_global_realloc_between_bug.c +++ b/test/cases/soundness/c01_global_realloc_between_bug.c @@ -14,7 +14,7 @@ int main(void) { size_t i = 40; if (i < g_len) { shrink(); - int r = g_buf[i]; // BUG: out-of-bounds // TRAP + int r = g_buf[i]; // TRAP free(g_buf); return r; } diff --git a/test/cases/soundness/c02_param_global_alias_bug.c b/test/cases/soundness/c02_param_global_alias_bug.c index 37793685..6ff35e02 100644 --- a/test/cases/soundness/c02_param_global_alias_bug.c +++ b/test/cases/soundness/c02_param_global_alias_bug.c @@ -2,7 +2,7 @@ // ASAN #include static char *g; -static int f(char *p) { free(g); g = NULL; return p[0]; } // BUG: use-after-free +static int f(char *p) { free(g); g = NULL; return p[0]; } // TRAP: heap-use-after-free int main(void) { g = calloc(8, 1); if (!g) return 1; diff --git a/test/cases/soundness/c03_contradictory_assume_bug.c b/test/cases/soundness/c03_contradictory_assume_bug.c deleted file mode 100644 index 80529b7d..00000000 --- a/test/cases/soundness/c03_contradictory_assume_bug.c +++ /dev/null @@ -1,8 +0,0 @@ -// ASAN -#include -int main(void) { - char b[4] = {0}; - int i = 10; - WEAVEC_ASSUME(i < 4); // BUG: contradicted-assumption definite - return b[i]; // TRAP -} diff --git a/test/cases/soundness/c04_union_tag_confusion_bug.c b/test/cases/soundness/c04_union_tag_confusion_bug.c index cd0d042b..873d495e 100644 --- a/test/cases/soundness/c04_union_tag_confusion_bug.c +++ b/test/cases/soundness/c04_union_tag_confusion_bug.c @@ -4,7 +4,7 @@ struct val { int tag; union { int *p; intptr_t n; } u; }; // RFC 0030 §7.3: 'v' is Single, so the loads through it on the next line are proven; // the slot 'val.u.p' shares its union with an integer, so *v->u.p has no extent. -static int get(struct val *v) { return v->tag == 1 ? *v->u.p : (int)v->u.n; } // BUG: out-of-bounds +static int get(struct val *v) { return v->tag == 1 ? *v->u.p : (int)v->u.n; } // TRAP: null-dereference int main(int argc, char **argv) { (void)argv; struct val v; diff --git a/test/cases/soundness/c05_realloc_shrink_bug.c b/test/cases/soundness/c05_realloc_shrink_bug.c index c8b63d00..1264faca 100644 --- a/test/cases/soundness/c05_realloc_shrink_bug.c +++ b/test/cases/soundness/c05_realloc_shrink_bug.c @@ -5,7 +5,7 @@ int main(void) { if (!p) return 1; char *q = realloc(p, 2); if (!q) { free(p); return 1; } - int r = q[5]; // BUG: out-of-bounds + int r = q[5]; // TRAP: heap-buffer-overflow free(q); return r; } diff --git a/test/cases/soundness/c06_fam_sizeof_bug.c b/test/cases/soundness/c06_fam_sizeof_bug.c index a3811b19..2cca7f0c 100644 --- a/test/cases/soundness/c06_fam_sizeof_bug.c +++ b/test/cases/soundness/c06_fam_sizeof_bug.c @@ -6,7 +6,7 @@ int main(int argc, char **argv) { size_t n = (size_t)argc + 3; struct msg *m = malloc(sizeof *m + n); if (!m) return 1; - m->data[n] = 0; // BUG: out-of-bounds // TRAP + m->data[n] = 0; // TRAP free(m); return 0; } diff --git a/test/cases/soundness/c07_fnptr_cast_bug.c b/test/cases/soundness/c07_fnptr_cast_bug.c index 12f78f33..60446ee1 100644 --- a/test/cases/soundness/c07_fnptr_cast_bug.c +++ b/test/cases/soundness/c07_fnptr_cast_bug.c @@ -8,5 +8,5 @@ int main(void) { if (!p) return 1; borrow_fn fn = (borrow_fn)free_it; fn(p); - return p[0]; // BUG: use-after-free + return p[0]; // TRAP: heap-use-after-free } diff --git a/test/cases/soundness/c08_recursive_free_bug.c b/test/cases/soundness/c08_recursive_free_bug.c index cdf72d56..bbf52682 100644 --- a/test/cases/soundness/c08_recursive_free_bug.c +++ b/test/cases/soundness/c08_recursive_free_bug.c @@ -8,5 +8,5 @@ int main(void) { a->l = calloc(1, sizeof *a); struct t *keep = a->l; t_free(a); - return keep ? keep->v : 0; // BUG: use-after-free // TRAP + return keep ? keep->v : 0; // TRAP } diff --git a/test/cases/soundness/c09_strncpy_unterminated_bug.c b/test/cases/soundness/c09_strncpy_unterminated_bug.c index 104d3d89..ae5dfef9 100644 --- a/test/cases/soundness/c09_strncpy_unterminated_bug.c +++ b/test/cases/soundness/c09_strncpy_unterminated_bug.c @@ -5,5 +5,5 @@ int main(int argc, char **argv) { if (argc < 2) return 0; char dst[4]; strncpy(dst, argv[1], sizeof dst); - return (int)strlen(dst); // BUG: out-of-bounds // TRAP + return (int)strlen(dst); // TRAP } diff --git a/test/cases/soundness/c10_double_free_two_globals_bug.c b/test/cases/soundness/c10_double_free_two_globals_bug.c index 43f8e5d5..cc61147d 100644 --- a/test/cases/soundness/c10_double_free_two_globals_bug.c +++ b/test/cases/soundness/c10_double_free_two_globals_bug.c @@ -1,7 +1,7 @@ // ASAN #include static char *g1, *g2; -static void cleanup(void) { free(g1); free(g2); } // BUG: double-free +static void cleanup(void) { free(g1); free(g2); } // TRAP: invalid-release int main(void) { g1 = malloc(8); g2 = g1; diff --git a/test/cases/soundness/c10b_double_free_two_params_bug.c b/test/cases/soundness/c10b_double_free_two_params_bug.c index 5b42d2dd..82b2eaf6 100644 --- a/test/cases/soundness/c10b_double_free_two_params_bug.c +++ b/test/cases/soundness/c10b_double_free_two_params_bug.c @@ -1,6 +1,6 @@ // ASAN #include -static void cleanup(char *a, char *b) { free(a); free(b); } // BUG: double-free +static void cleanup(char *a, char *b) { free(a); free(b); } // TRAP: invalid-release int main(void) { char *p = malloc(8); cleanup(p, p); diff --git a/test/cases/soundness/c10c_double_free_two_fields_bug.c b/test/cases/soundness/c10c_double_free_two_fields_bug.c index 0ec0ca6e..0258f9c2 100644 --- a/test/cases/soundness/c10c_double_free_two_fields_bug.c +++ b/test/cases/soundness/c10c_double_free_two_fields_bug.c @@ -1,7 +1,7 @@ // ASAN #include struct pair { char *a, *b; }; -static void cleanup(struct pair *s) { free(s->a); free(s->b); } // BUG: double-free +static void cleanup(struct pair *s) { free(s->a); free(s->b); } // TRAP: invalid-release int main(void) { struct pair s; s.a = malloc(8); diff --git a/test/cases/soundness/c10d_uaf_two_globals_bug.c b/test/cases/soundness/c10d_uaf_two_globals_bug.c index 45558bfc..7b2f70b8 100644 --- a/test/cases/soundness/c10d_uaf_two_globals_bug.c +++ b/test/cases/soundness/c10d_uaf_two_globals_bug.c @@ -1,7 +1,7 @@ // ASAN #include static char *g1, *g2; -static int drop_then_read(void) { free(g1); return g2[0]; } // BUG: use-after-free +static int drop_then_read(void) { free(g1); return g2[0]; } // TRAP: heap-use-after-free int main(void) { g1 = calloc(8, 1); if (!g1) return 1; diff --git a/test/cases/soundness/c10e_global_and_param_bug.c b/test/cases/soundness/c10e_global_and_param_bug.c index f0298015..2d06e49d 100644 --- a/test/cases/soundness/c10e_global_and_param_bug.c +++ b/test/cases/soundness/c10e_global_and_param_bug.c @@ -1,7 +1,7 @@ // ASAN #include static char *g; -static void drop_both(char *p) { free(p); free(g); } // BUG: double-free +static void drop_both(char *p) { free(p); free(g); } // TRAP: invalid-release int main(void) { g = malloc(8); drop_both(g); diff --git a/test/cases/soundness/c11_loop_40_iterations_bug.c b/test/cases/soundness/c11_loop_40_iterations_bug.c index 4d9ccf06..8bb4155b 100644 --- a/test/cases/soundness/c11_loop_40_iterations_bug.c +++ b/test/cases/soundness/c11_loop_40_iterations_bug.c @@ -4,6 +4,6 @@ int main(void) { int a[40]; for (int i = 0; i < 40; i++) a[i] = i; int s = 0; - for (int i = 0; i <= 40; i++) s += a[i]; // BUG: out-of-bounds // TRAP + for (int i = 0; i <= 40; i++) s += a[i]; // TRAP return s; } diff --git a/test/cases/soundness/c12_uninit_struct_field_bug.c b/test/cases/soundness/c12_uninit_struct_field_bug.c index 69e5b5ad..b5a4d52d 100644 --- a/test/cases/soundness/c12_uninit_struct_field_bug.c +++ b/test/cases/soundness/c12_uninit_struct_field_bug.c @@ -1,6 +1,6 @@ #include struct cfg { int n; int *vals; }; -static int sum(const struct cfg *c) { int s = 0; for (int i = 0; i < c->n; i++) s += c->vals[i]; return s; } // BUG: use-of-uninitialized // TRAP +static int sum(const struct cfg *c) { int s = 0; for (int i = 0; i < c->n; i++) s += c->vals[i]; return s; } // TRAP int main(void) { struct cfg c; c.n = 3; diff --git a/test/cases/soundness/c13_errno_style_extern_global_bug.c b/test/cases/soundness/c13_errno_style_extern_global_bug.c index 0914380b..58fa0ac3 100644 --- a/test/cases/soundness/c13_errno_style_extern_global_bug.c +++ b/test/cases/soundness/c13_errno_style_extern_global_bug.c @@ -1,4 +1,5 @@ // External global buffer of unknown size, indexed far. +// XFAIL: the table is defined in a unit built without WeaveC (-fno-weavec): it has no redzone // UNITS: c13_extern_impl.c extern char shared_table[]; -int main(void) { return shared_table[100000]; } // BUG: out-of-bounds // MISS: the table is defined in a unit built without WeaveC (-fno-weavec): it has no redzone +int main(void) { return shared_table[100000]; } // TRAP diff --git a/test/cases/soundness/c14_memcpy_ptr_partial_bug.c b/test/cases/soundness/c14_memcpy_ptr_partial_bug.c index e6d1a406..d44b6374 100644 --- a/test/cases/soundness/c14_memcpy_ptr_partial_bug.c +++ b/test/cases/soundness/c14_memcpy_ptr_partial_bug.c @@ -1,13 +1,19 @@ // Only half of a pointer copied: result is a forged pointer. // ASAN +// The forged pointer has no provenance (RFC 0036 A6) and points outside every +// object, into the first 4 GiB, which neither Darwin (its zero page) nor a +// position-independent executable on Linux maps: it stops as a fault. #include #include int main(void) { + // Not the first block of its class, whose address may be a multiple of + // 4 GiB (its low half, and so the forged pointer, would then be null). + char *first = calloc(8, 1); char *p = calloc(8, 1); - if (!p) return 1; + if (!p || !first) return 1; char *q = NULL; memcpy(&q, &p, 4); - int r = q ? q[0] : 0; // BUG: out-of-bounds // MISS: a pointer forged from half of another points outside every object; it stops only as a fault + int r = q ? q[0] : 0; // TRAP free(p); return r; } diff --git a/test/cases/soundness/conditional-arm-in-parens_bug.c b/test/cases/soundness/conditional-arm-in-parens_bug.c index ef55307a..888d2a18 100644 --- a/test/cases/soundness/conditional-arm-in-parens_bug.c +++ b/test/cases/soundness/conditional-arm-in-parens_bug.c @@ -12,7 +12,7 @@ static int pick(int n) { int total = 0; for (int i = 0; i < n; i++) { int t = (i & 1) ? (x = i, 5) : 0; - total += a[t]; // BUG: out-of-bounds // TRAP + total += a[t]; // TRAP } return total + x; } diff --git a/test/corpus/README.md b/test/corpus/README.md index 98de9f20..c6915b4f 100644 --- a/test/corpus/README.md +++ b/test/corpus/README.md @@ -5,19 +5,18 @@ WeaveC on real C projects at pinned revisions: RFC 0035, section 11 and the [`scripts/corpus-gate.py`](../../scripts/corpus-gate.py) (tests, which need no compiler: [`scripts/test_corpus_gate.py`](../../scripts/test_corpus_gate.py)). -Since RFC 0035 every memory access of a `weavec-cc` build is guarded unless -a local rule proves the guard redundant, and the analysis is advisory. The -gate follows: it builds each project with `CC=weavec-cc` and runs its own -test suite, which must pass with no guard failing; it measures how many -accesses the build leaves guarded (the enforcement ledger), what the -`weavec` analysis reports, whether each injected bug stops at its line, and -what the guards cost at build and run time. +Every memory access of a `weavec-cc` build is guarded unless a local rule +proves the guard redundant (RFC 0035). The gate builds each project with +`CC=weavec-cc` and runs its own test suite, which must pass with no guard +failing; it measures how many accesses the build leaves guarded (the +enforcement ledger), whether each injected bug stops at its line, and what +the guards cost at build and run time. | File | What it holds | | --- | --- | -| `manifest.json` | The 48 projects (url, 40-hex `sha`, `support` files) and their 50 configs: `compile` (files and arguments), `wholeProgram`, `build`, `test`, `testTimeout`, `bench`, `heldOut` and `set` (below); and `gates`, the limits of the gates below | +| `manifest.json` | The 48 projects (url, 40-hex `sha`, `support` files) and their 49 configs: `compile` (files and arguments), `build`, `test`, `testTimeout`, `bench`, `heldOut` and `set` (below); and `gates`, the limits of the gates below | | `expected.json` | The ratchet, per platform and config, written by `--update` | -| `triage.json` | Verdicts on the analysis' findings (`entries`), and `guardFailures`: the guard failures of test suites that are true bugs of the project | +| `triage.json` | `guardFailures`: the guard failures of test suites that are true bugs of the project | | `injections/` | `injections.json` and one patch per injected bug, by project (39), plus the drivers some of them build | | `bench/` | The benchmark workloads and drivers, and `zlib-input.py`, the generator of the 64 MiB compression input | | `support/` | Files a checkout needs that it does not have: generated configuration headers (`{support}` in compile arguments), test wrappers and data fetchers, build files | @@ -25,19 +24,18 @@ what the guards cost at build and run time. ## The configs -| Config | Files | Whole program | Build | Test | Bench | -| --- | --- | --- | --- | --- | --- | -| sds | `sds.c` | | `make sds-test` | `./sds-test` | | -| cJSON | `cJSON.c` | | CMake with tests and utils | CTest (22 tests) | parse + print | -| cJSON-program | `cJSON.c cJSON_Utils.c` | yes | (the cJSON build) | | | -| jsmn | `example/simple.c example/jsondump.c` | | `make simple_example jsondump` | `make test` (4 builds of `test/tests.c`) | | -| log.c | `src/log.c` | | | | | -| printf | `printf.c` | | | | | -| linenoise | `linenoise.c` | | | | | -| linenoise-program | `linenoise.c example.c` | yes | `make linenoise-example` | `make test` (the pty harness, 102 checks, about 20 s) | | -| zlib | 15 library files | yes | `./configure && make -j8` | `make test` | minigzip on 64 MiB | -| lua | `l*.c` | yes | the developer makefile | 27 `testes` files (below) | `lua-bench.lua` | -| jansson | 12 `src/` files | yes | CMake | CTest (215 tests) | | +| Config | Files | Build | Test | Bench | +| --- | --- | --- | --- | --- | +| sds | `sds.c` | `make sds-test` | `./sds-test` | | +| cJSON | `cJSON.c` | CMake with tests and utils | CTest (22 tests) | parse + print | +| jsmn | `example/simple.c example/jsondump.c` | `make simple_example jsondump` | `make test` (4 builds of `test/tests.c`) | | +| log.c | `src/log.c` | | | | +| printf | `printf.c` | | | | +| linenoise | `linenoise.c` | | | | +| linenoise-program | `linenoise.c example.c` | `make linenoise-example` | `make test` (the pty harness, 102 checks, about 20 s) | | +| zlib | 15 library files | `./configure && make -j8` | `make test` | minigzip on 64 MiB | +| lua | `l*.c` | the developer makefile | 27 `testes` files (below) | `lua-bench.lua` | +| jansson | 12 `src/` files | CMake | CTest (215 tests) | | The projects without a build (log.c, printf) or a C test suite (printf's is C++, which includes `printf.c` into a C++ unit) are compile-only: their @@ -90,7 +88,7 @@ a checkout: builds, injections and benchmarks work on copies under `/.gate/`, deleted afterwards unless `--keep`. ```sh -# Every PR (CI runs it on a few configs): the analysis and the ledgers. +# Every PR (CI runs it on a few configs): the enforcement ledgers. scripts/corpus-gate.py --quick [--only cJSON sds] [--json out.json] scripts/corpus-gate.py --quick --update # record the ratchet @@ -117,29 +115,25 @@ The binaries default to `build/release/bin`, then `build/dev/bin` (a Debug binary's timings mean nothing: `--bench` refuses one it found itself, and `--full` warns). The reference compiler (`--cc`) defaults to `$WEAVEC_LLVM_PREFIX/bin/clang`, else `clang`. `--jobs` bounds parallel -processes, `--timeout` each analysis or per-file compile (default 1,800 s), +processes, `--timeout` each per-file compile (default 1,800 s), `--build-timeout` each build or test step (a config's `testTimeout` replaces it for its tests). The exit status is 0 on success, 1 when a check fails and 2 on a usage or setup error. `--json OUT` writes everything the run -measured, every diagnostic and report included. +measured, every report included. ## What each mode checks -- **`--quick`**, per config: - - the advisory analysis (RFC 0035, section 8): `weavec -- - -ferror-limit=0 ` for each file, or `weavec --whole-program - -- …` for a `wholeProgram` config, in the checkout. Its - diagnostics go to the triage, its summary lines (`N sites: P proven, U - not proven, V violations, T trusted; E errors, W warnings`) to the - ratchet; - - the enforcement ledgers (RFC 0035, section 9): `weavec-cc -c -O2 - -fweavec-checks=trap -fweavec-ledger= ` for each - file (a config's own `-O` comes later and wins). A ledger must be a - `weavec-ledger` document of version 3; its units' summaries (accesses, - proven, guarded, unguarded) are summed per config and go to the ratchet. - - A crash, a timeout, a Clang error, a missing summary line or a missing - ledger fails the run, and that config is not recorded by `--update`. +- **`--quick`**, per config, the enforcement ledgers (RFC 0035, section + 9): `weavec-cc -c -O2 -fweavec-checks=trap -fweavec-ledger= + ` for each file, in the checkout (a config's own `-O` comes later + and wins). A ledger must be a `weavec-ledger` document of version 3; its + units' summaries (accesses, proven, guarded, unguarded) are summed per + config and go to the ratchet. Before the first compile, a probe checks + that `weavec-cc` writes such a ledger at all. + + A failing compile (a crash, a timeout, a Clang error) or a missing or + invalid ledger fails the run, and that config is not recorded by + `--update`. - **`--full`**: `--quick` (with each per-file compile also timed with the reference compiler, for G9), then for each config with a `build`: the build and test commands in a copy with `CC` set to a wrapper around @@ -182,7 +176,6 @@ measured, every diagnostic and report included. | Gate | RFC 0035 | Mode | Passes when | | --- | --- | --- | --- | -| `triage` | (section 8) | `--quick` | every definite error of the analysis (an `error`) has a verdict in `triage.json`, and none is `"false"` | | ratchet | (section 11) | `--quick` | no measurement is worse than `expected.json` records (below) | | `drop-in` | G1, G2, G3 | `--full` | every build passes, and every test suite in trap (or verify) mode and in report mode, with no trap but at `guardFailures` | | `injections` | (G4, G5 on the corpus) | `--inject`, `--full` | every selected injection stops at its line | @@ -199,35 +192,25 @@ sets) and G8 (start-up) are measured by their own scripts, not here. `platforms..configs.` holds what `--quick` measured: - `ledger`: `accesses`, `proven`, `guarded`, `unguarded` and `provenShare` - (proven over accesses, to four places), summed over the per-file ledgers; -- `analysis`: `sites`, `proven`, `notProven`, `violations`, `trusted`, - `errors`, `warnings` and `overBudget`, from the analysis' summary lines - (the program line for a `wholeProgram` config). - -`provenShare` must not drop and `unguarded`, `errors` and `warnings` must -not rise: a worse value fails the run. A better one, and any change of the -other counts, is reported for `--update` to record. A config or platform -with no record is reported, not failed, until `--update` records it. -Platforms are recorded separately (system headers change the numbers). -`--update` records the configs whose analyses and compiles all succeeded; -`--update-from RESULTS` records a `--json` file written elsewhere, for -example the `corpus-gate-quick-` artifact of a CI run. - -The record holds cJSON, cJSON-program, log.c and sds on `darwin-arm64`, measured -with `weavec-cc` 0.15.0-dev (9fb1dc1231cf). The other configs and platforms -have none yet: record them with `--quick --update` on a quiet machine, and -from CI's results files with `--update-from`. + (proven over accesses, to four places), summed over the per-file ledgers. + +`provenShare` must not drop and `unguarded` must not rise: a worse value +fails the run. A better one, and any change of the other counts, is +reported for `--update` to record. A config or platform with no record is +reported, not failed, until `--update` records it. Platforms are recorded +separately (system headers change the numbers). `--update` records the +configs whose compiles all succeeded; `--update-from RESULTS` records a +`--json` file written elsewhere, for example the `corpus-gate-quick-` +artifact of a CI run. + +The record holds the original configs (cJSON, jansson, jsmn, linenoise, +linenoise-program, log.c, lua, printf, sds and zlib) on `darwin-arm64`, +measured with `weavec-cc` 0.15.0-dev (90dcdf8665ea). The other configs and +platforms have none yet: record them with `--quick --update` on a quiet +machine, and from CI's results files with `--update-from`. ## Triage (`triage.json`) -`entries` holds verdicts on the analysis' findings, -`{config, id, certainty, file, line, verdict, note}`, keyed by config, file, -line and id. A definite finding is an `error` of `weavec`, a possible one a -`warning`. Every definite error a run reports needs an entry and none may -be triaged `"false"`; possible findings are counted and need none. An entry -no finding of a config that ran matches is reported as stale without -failing; entries are removed when a population is re-triaged. - `guardFailures` lists the guard failures of a test suite that are true bugs of the project: `{config, file, line, verdict, note}`, with an optional `kind` (a report kind of RFC 0035, section 5.3). The only verdict @@ -235,16 +218,19 @@ is `"true"`, and the note is the source evidence: the gate refuses an entry with any other verdict, so a false trap is never triaged away (it is a bug in WeaveC). A report at a listed site is not counted as a trap, and a test suite that fails in trap mode only because of such reports passes when its -report-mode rerun passes. An entry no run names is reported as stale. +report-mode rerun passes. An entry no run names is reported as stale. The file holds nothing else: the +gate refuses any other field. -The entries are four for jansson's `hashlittle` +There are no entries. The last four were jansson's `hashlittle` (`src/lookup3.h:259`, `260`, `263`, `264`): its tail switch reads a whole 32-bit word of the key and masks off the bytes past the key's end, as the comment above the switch says; the source selects a byte-wise tail under -Valgrind and AddressSanitizer (`NO_MASKING_TRICK`). mbedtls's test helpers -read 239 bytes past a certificate array (`library/x509_crt.c:1415`); since -RFC 0035 the read lands inside the next global, which no redzone separates -it from, so no guard fails there (as under ASan), and it has no entry. +Valgrind and AddressSanitizer (`NO_MASKING_TRICK`), which `weavec-cc` +selects too since RFC 0036 (it compiles as ASan would). mbedtls's test +helpers read 239 bytes past a certificate array +(`library/x509_crt.c:1415`); since RFC 0035 the read lands inside the next +global, which no redzone separates it from, so no guard fails there (as +under ASan), and it has no entry. ## Commands and their environment @@ -294,5 +280,5 @@ line. Measured on 2026-10-06 with `weavec-cc` 0.15.0-dev (9fb1dc1231cf), the injections of sds (5), log.c (2), printf (1) and jsmn (2) stop where their entries say. Those of cJSON, linenoise, zlib, Lua and jansson were -converted from the analysis-era entries (whose `run` commands they keep, -and whose test suites they otherwise run) and await their first run. +converted from earlier entries (whose `run` commands they keep, and whose +test suites they otherwise run) and await their first run. diff --git a/test/corpus/expected.json b/test/corpus/expected.json index 3575e155..06ac4790 100644 --- a/test/corpus/expected.json +++ b/test/corpus/expected.json @@ -5,73 +5,33 @@ "The corpus ratchet (RFC 0035, section 11), written by scripts/corpus-gate.py --update (or", "--update-from a results file measured elsewhere, for example by CI). For each platform,", "configs. records what --quick measured: `ledger`, the enforcement ledgers of the", - "per-file weavec-cc -c -O2 compiles (accesses, proven, guarded, unguarded, provenShare), and", - "`analysis`, the summary lines of the weavec analysis (sites, proven, notProven, violations,", - "trusted, errors, warnings, overBudget). provenShare must not drop, and unguarded, errors and", - "warnings must not rise; a better value and a change of the other counts are reported until", - "--update records them. test/corpus/README.md documents the gate." + "per-file weavec-cc -c -O2 compiles (accesses, proven, guarded, unguarded, provenShare).", + "provenShare must not drop and unguarded must not rise; a better value and a change of the", + "other counts are reported until --update records them. test/corpus/README.md documents the", + "gate." ], "platforms": { "darwin-arm64": { "machine": "darwin-arm64 Apple M3 x8", - "producer": "weavec-cc version 0.15.0-dev (90dcdf8665ea-dirty)", - "updated": "2026-10-06", + "producer": "weavec-cc version 0.16.0-dev (1cb608f40912-dirty)", + "updated": "2026-10-08", "configs": { "cJSON": { "ledger": { - "accesses": 734, - "proven": 398, + "accesses": 733, + "proven": 397, "guarded": 336, "unguarded": 0, - "provenShare": 0.5422 - }, - "analysis": { - "sites": 1221, - "proven": 858, - "notProven": 361, - "violations": 0, - "trusted": 2, - "errors": 0, - "warnings": 4, - "overBudget": 0 - } - }, - "cJSON-program": { - "ledger": { - "accesses": 988, - "proven": 463, - "guarded": 525, - "unguarded": 0, - "provenShare": 0.4686 - }, - "analysis": { - "sites": 1745, - "proven": 1109, - "notProven": 634, - "violations": 0, - "trusted": 2, - "errors": 0, - "warnings": 10, - "overBudget": 0 + "provenShare": 0.5416 } }, "jansson": { "ledger": { - "accesses": 841, - "proven": 350, - "guarded": 491, + "accesses": 839, + "proven": 347, + "guarded": 492, "unguarded": 0, - "provenShare": 0.4162 - }, - "analysis": { - "sites": 2391, - "proven": 1400, - "notProven": 987, - "violations": 0, - "trusted": 4, - "errors": 0, - "warnings": 33, - "overBudget": 0 + "provenShare": 0.4136 } }, "jsmn": { @@ -81,54 +41,24 @@ "guarded": 92, "unguarded": 0, "provenShare": 0.6984 - }, - "analysis": { - "sites": 384, - "proven": 239, - "notProven": 145, - "violations": 0, - "trusted": 0, - "errors": 0, - "warnings": 4, - "overBudget": 0 } }, "linenoise": { "ledger": { - "accesses": 656, - "proven": 317, - "guarded": 339, + "accesses": 657, + "proven": 316, + "guarded": 341, "unguarded": 0, - "provenShare": 0.4832 - }, - "analysis": { - "sites": 1121, - "proven": 748, - "notProven": 371, - "violations": 0, - "trusted": 2, - "errors": 0, - "warnings": 6, - "overBudget": 0 + "provenShare": 0.481 } }, "linenoise-program": { "ledger": { - "accesses": 693, - "proven": 332, - "guarded": 361, + "accesses": 695, + "proven": 330, + "guarded": 365, "unguarded": 0, - "provenShare": 0.4791 - }, - "analysis": { - "sites": 1181, - "proven": 781, - "notProven": 398, - "violations": 0, - "trusted": 2, - "errors": 0, - "warnings": 6, - "overBudget": 0 + "provenShare": 0.4748 } }, "log.c": { @@ -138,35 +68,15 @@ "guarded": 22, "unguarded": 0, "provenShare": 0.6271 - }, - "analysis": { - "sites": 74, - "proven": 50, - "notProven": 23, - "violations": 0, - "trusted": 1, - "errors": 0, - "warnings": 0, - "overBudget": 0 } }, "lua": { "ledger": { - "accesses": 7493, - "proven": 2752, + "accesses": 7491, + "proven": 2750, "guarded": 4741, "unguarded": 0, - "provenShare": 0.3673 - }, - "analysis": { - "sites": 14578, - "proven": 6451, - "notProven": 8123, - "violations": 0, - "trusted": 4, - "errors": 0, - "warnings": 79, - "overBudget": 0 + "provenShare": 0.3671 } }, "printf": { @@ -176,16 +86,6 @@ "guarded": 35, "unguarded": 0, "provenShare": 0.4355 - }, - "analysis": { - "sites": 148, - "proven": 59, - "notProven": 89, - "violations": 0, - "trusted": 0, - "errors": 0, - "warnings": 0, - "overBudget": 0 } }, "sds": { @@ -195,16 +95,6 @@ "guarded": 153, "unguarded": 0, "provenShare": 0.2783 - }, - "analysis": { - "sites": 464, - "proven": 208, - "notProven": 256, - "violations": 0, - "trusted": 0, - "errors": 0, - "warnings": 11, - "overBudget": 0 } }, "zlib": { @@ -214,16 +104,6 @@ "guarded": 2060, "unguarded": 0, "provenShare": 0.5143 - }, - "analysis": { - "sites": 4880, - "proven": 2538, - "notProven": 2342, - "violations": 0, - "trusted": 0, - "errors": 0, - "warnings": 6, - "overBudget": 0 } } } diff --git a/test/corpus/injections/injections.json b/test/corpus/injections/injections.json index f224f980..30d381c1 100644 --- a/test/corpus/injections/injections.json +++ b/test/corpus/injections/injections.json @@ -57,7 +57,7 @@ "line": 266, "unlocated": "invalid-release", "run": "ctest --test-dir out --output-on-failure -R '^(cJSON_test|parse_examples|misc_tests)$'", - "description": "cJSON_Delete frees the value string twice through the deallocate hook; the analysis of the unit alone does not see through the hook, and the runtime's allocator refuses the second release" + "description": "cJSON_Delete frees the value string twice through the deallocate hook, and the runtime's allocator refuses the second release" }, { "id": "cjson-oob-number", diff --git a/test/corpus/manifest.json b/test/corpus/manifest.json index fdeb5424..1efe1137 100644 --- a/test/corpus/manifest.json +++ b/test/corpus/manifest.json @@ -4,9 +4,8 @@ "_comment": [ "The corpus of RFC 0035, section 11, read by scripts/corpus-gate.py; test/corpus/README.md", "documents every field and gate. Each project is pinned to a 40-hex `sha` and checked out once", - "into /. Each config names the files --quick analyses with weavec (per file,", - "or as one program when `wholeProgram` is true) and compiles with weavec-cc -c -O2 into", - "enforcement ledgers (`compile`; `{support}` in its arguments expands to", + "into /. Each config names the files --quick compiles with weavec-cc -c -O2", + "into enforcement ledgers (`compile`; `{support}` in its arguments expands to", "test/corpus/support/). `build`, `test` and `bench` commands run under /bin/sh in a", "fresh copy of the checkout with CC set to the compiler (a wrapper around weavec-cc or the", "reference compiler), JOBS to the parallelism, SUPPORT to test/corpus/support/, BENCH", @@ -72,12 +71,6 @@ "command": "./cjson-bench 30", "repeat": 7 } - }, - { - "name": "cJSON-program", - "notes": "cJSON_Utils.c calls into cJSON.c (cJSON_free, cJSON_Delete, cJSON_Duplicate, ...); analysed as one program. Built and tested with the cJSON config.", - "compile": {"files": ["cJSON.c", "cJSON_Utils.c"], "args": ["-std=c89", "-I."]}, - "wholeProgram": true } ] }, @@ -137,7 +130,6 @@ "name": "linenoise-program", "notes": "example.c installs the completion and hints callbacks linenoise.c calls through pointers, and frees what linenoise() returns. The build is the project's Makefile (linenoise-example); the test is its pseudo-terminal harness linenoise-test, which drives linenoise-example (102 checks, about 20 s).", "compile": {"files": ["linenoise.c", "example.c"], "args": ["-std=c99", "-I.", "-D_GNU_SOURCE"]}, - "wholeProgram": true, "build": ["make linenoise-example CC=\"$CC\""], "test": ["make test CC=\"$CC\""], "testTimeout": 300 @@ -157,7 +149,6 @@ "files": ["adler32.c", "compress.c", "crc32.c", "deflate.c", "infback.c", "inffast.c", "inflate.c", "inftrees.c", "trees.c", "uncompr.c", "zutil.c", "gzclose.c", "gzlib.c", "gzread.c", "gzwrite.c"], "args": ["-std=c99", "-I.", "-DHAVE_UNISTD_H", "-DHAVE_STDARG_H"] }, - "wholeProgram": true, "build": ["CC=\"$CC\" ./configure", "make -j8"], "test": ["make test"], "bench": { @@ -181,7 +172,6 @@ "name": "lua", "notes": "A garbage-collected VM. realloc through the l_alloc function pointer, a 968-block interpreter loop with computed goto (luaV_execute), linked stacks of locals in the parser. The build is the developer makefile with POSIX and dlopen (as LUA_USE_LINUX, which also suits macOS) and without the Linux-only -Wl,-E; the test is the named testes subset below, run by support/lua/subset.lua in user mode. Left out: files.lua (needs /dev/full, missing on macOS), main.lua (stand-alone interpreter tests, skipped in user mode anyway), big.lua and heavy.lua (long), and the T-library tests (need a build with ltests.h).", "compile": {"files": ["l*.c"], "args": ["-std=c99", "-I.", "-DLUA_USE_POSIX"]}, - "wholeProgram": true, "build": ["make -j\"$JOBS\" CC=\"$CC\" MYCFLAGS=\"-std=c99 -DLUA_USE_POSIX -DLUA_USE_DLOPEN\" MYLDFLAGS= MYLIBS=-ldl"], "test": ["cd testes && ../lua \"$SUPPORT/subset.lua\" gc.lua db.lua calls.lua strings.lua literals.lua tpack.lua attrib.lua gengc.lua locals.lua constructs.lua code.lua cstack.lua nextvar.lua pm.lua utf8.lua api.lua memerr.lua events.lua vararg.lua closure.lua coroutine.lua goto.lua errors.lua math.lua sort.lua bitwise.lua verybig.lua"], "bench": { @@ -206,7 +196,6 @@ "files": ["src/dump.c", "src/error.c", "src/hashtable.c", "src/hashtable_seed.c", "src/load.c", "src/memory.c", "src/pack_unpack.c", "src/strbuffer.c", "src/strconv.c", "src/utf.c", "src/value.c", "src/version.c"], "args": ["-std=c99", "-Isrc", "-I{support}", "-DHAVE_CONFIG_H"] }, - "wholeProgram": true, "build": [ "cmake -S . -B out -DCMAKE_C_COMPILER=\"$CC\" -DCMAKE_BUILD_TYPE=Release -DJANSSON_BUILD_DOCS=OFF -DJANSSON_EXAMPLES=OFF", "cmake --build out -j \"$JOBS\"" @@ -238,7 +227,6 @@ ], "args": ["-I.", "-I{support}", "-D_FILE_OFFSET_BITS=64", "-DBZ_UNIX=1", "-DBZ_LCCWIN32=0"] }, - "wholeProgram": true, "build": [ "cp \"$SUPPORT/bzip2.mk\" Makefile.gate", "make -j\"$JOBS\" -f Makefile.gate CC=\"$CC\" all" @@ -261,7 +249,6 @@ "files": ["alloc.c", "async.c", "hiredis.c", "net.c", "read.c", "sds.c", "sockcompat.c", "test.c"], "args": ["-std=c99", "-I."] }, - "wholeProgram": true, "build": ["make -j\"$JOBS\" CC=\"$CC\" USE_SSL=0 static hiredis-test"], "test": ["sh \"$SUPPORT/run-tests.sh\""], "testTimeout": 600 @@ -279,7 +266,6 @@ "heldOut": true, "notes": "A callback-driven HTTP parser over caller buffers; the tests keep global pointers to the current message and settings (current_pause_parser points to a local of parse_pause). The build is the project's Makefile (test_g: strict, -O0; test_fast: -O3; bench), -Werror as shipped; the test runs test_g and test_fast (bench is built, not run: about 25 s of throughput measurement).", "compile": {"files": ["http_parser.c", "test.c"], "args": ["-I.", "-DHTTP_PARSER_STRICT=0"]}, - "wholeProgram": true, "build": ["make -j\"$JOBS\" CC=\"$CC\" test_g test_fast bench"], "test": ["./test_g", "./test_fast"] } @@ -296,7 +282,6 @@ "heldOut": true, "notes": "INI parser: a line buffer on the stack or the heap (INI_USE_STACK, INI_ALLOW_REALLOC), callbacks per name/value pair. The build compiles ini.c; the test is tests/unittest.sh, which builds and runs 15 configurations of the unit tests and writes their output over the tracked baselines, which must then be unchanged.", "compile": {"files": ["ini.c", "tests/unittest.c"], "args": ["-I."]}, - "wholeProgram": true, "build": ["\"$CC\" -Wall -c ini.c -o ini.o"], "test": ["cd tests && CC=\"$CC\" bash ./unittest.sh && git diff --exit-code -- ."] } @@ -316,7 +301,6 @@ "files": ["src/*.c"], "args": ["-Iinclude", "-I{support}", "-DHAVE_CONFIG_H", "-DYAML_DECLARE_STATIC"] }, - "wholeProgram": true, "build": [ "cmake -S . -B out -DCMAKE_C_COMPILER=\"$CC\" -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=ON", "cmake --build out -j \"$JOBS\"" @@ -429,7 +413,6 @@ "heldOut": true, "notes": "An expression parser and evaluator: te_expr trees with an anonymous union of value, bound variable and function, parameters in a trailing array, freed recursively. The build is the project's Makefile (smoke and smoke_pr run themselves as they are built, the examples and repl); the test reruns the smoke tests and the examples and evaluates one expression in repl (bench is left out: about 10 s).", "compile": {"files": ["tinyexpr.c", "smoke.c"], "args": ["-I."]}, - "wholeProgram": true, "build": ["make -j\"$JOBS\" CC=\"$CC\" smoke smoke_pr example example2 example3 repl"], "test": [ "./smoke", @@ -794,7 +777,7 @@ "name": "quickjs", "heldOut": true, "set": "fresh34", - "notes": "A JavaScript engine: reference-counted JSValues (tagged structs) over objects with shapes and property arrays, a cycle-collecting GC over intrusive list.h lists (container_of), atoms, strings and ropes, a bytecode compiler and interpreter with its own value stack, a regexp engine and Unicode tables (libregexp, libunicode), dtoa, allocation through JSMallocFunctions, and quickjs-libc's std/os modules with workers (threads). The build is the project's Makefile (-O2) with explicit targets: qjs, qjsc, run-test262, libquickjs.a and the three examples (hello, test_fib, hello_module, which qjsc compiles with $CC). quickjs.check.o (the Makefile's CONFIG_CHECK_JSVALUE build, compile-only by the project's own account; JS_GetOpaque always returns null in it) is left out of the build: WeaveC's definite null dereferences there are true but vacuous (RFC 0034, gate F1), and lowering them would need one triage entry per finding and would lower null-dereference errors across the whole build. The default target `all` builds it (and qjs.check.o), so the build names the targets; `make test` depends only on qjs on Darwin (the bjson and point shared-library tests are off there), so it builds no check object. The test is `make test` (tests/test_*.js: closures, language, builtins, loops, BigInt, cyclic imports, a worker, std/os, rw handlers) and the three examples against their expected output; run-test262 is built but not run (test262 is a separate checkout of the conformance suite). The per-file compiles are the six library sources with the Makefile's defines (-D_GNU_SOURCE, CONFIG_VERSION from VERSION) and its -funsigned-char -fwrapv.", + "notes": "A JavaScript engine: reference-counted JSValues (tagged structs) over objects with shapes and property arrays, a cycle-collecting GC over intrusive list.h lists (container_of), atoms, strings and ropes, a bytecode compiler and interpreter with its own value stack, a regexp engine and Unicode tables (libregexp, libunicode), dtoa, allocation through JSMallocFunctions, and quickjs-libc's std/os modules with workers (threads). The build is the project's Makefile (-O2) with explicit targets: qjs, qjsc, run-test262, libquickjs.a and the three examples (hello, test_fib, hello_module, which qjsc compiles with $CC). quickjs.check.o (the Makefile's CONFIG_CHECK_JSVALUE build, compile-only by the project's own account; JS_GetOpaque always returns null in it) is left out of the build. The default target `all` builds it (and qjs.check.o), so the build names the targets; `make test` depends only on qjs on Darwin (the bjson and point shared-library tests are off there), so it builds no check object. The test is `make test` (tests/test_*.js: closures, language, builtins, loops, BigInt, cyclic imports, a worker, std/os, rw handlers) and the three examples against their expected output; run-test262 is built but not run (test262 is a separate checkout of the conformance suite). The per-file compiles are the six library sources with the Makefile's defines (-D_GNU_SOURCE, CONFIG_VERSION from VERSION) and its -funsigned-char -fwrapv.", "compile": { "files": ["quickjs.c", "libregexp.c", "libunicode.c", "cutils.c", "dtoa.c", "quickjs-libc.c"], "args": ["-D_GNU_SOURCE", "-DCONFIG_VERSION=\"2026-06-04\"", "-funsigned-char", "-fwrapv"] diff --git a/test/corpus/triage.json b/test/corpus/triage.json index 2001af84..0498a7c1 100644 --- a/test/corpus/triage.json +++ b/test/corpus/triage.json @@ -2,329 +2,12 @@ "schema": "weavec-corpus-triage", "version": 2, "_comment": [ - "Triage of the corpus findings (RFC 0035, section 11), checked by scripts/corpus-gate.py.", - "`entries` holds verdicts on the findings of the advisory analysis (weavec, per file or", - "--whole-program, as --quick runs it): {config, id, certainty, file, line, verdict, note}, keyed", - "by config, file, line and id. certainty is definite (an error) or possible (a warning), verdict", - "\"true\" or \"false\", and note says why. Every definite error a run reports needs an entry, and", - "none may be triaged \"false\"; possible findings are counted and need none. An entry no finding", - "of a config that ran matches is reported as stale without failing; entries are removed when the", - "population is re-triaged.", - "`guardFailures` lists the guard failures of a test suite that are true bugs of the project:", - "{config, file, line, verdict, note, kind (optional, a run-time report kind)}, verdict always", - "\"true\" and note the source evidence. A report at such a site is not counted as a trap; an", - "entry no run names is reported as stale without failing. A false trap is never triaged: it is", - "a bug in WeaveC." + "Triage of the corpus's guard failures (RFC 0035, section 11), checked by", + "scripts/corpus-gate.py. `guardFailures` lists the guard failures of a test suite that are true", + "bugs of the project: {config, file, line, verdict, note, kind (optional, a run-time report", + "kind)}, verdict always \"true\" and note the source evidence. A report at such a site is not", + "counted as a trap; an entry no run names is reported as stale without failing. A false trap", + "is never triaged: it is a bug in WeaveC." ], - "guardFailures": [ - { - "config": "jansson", - "file": "src/lookup3.h", - "line": 259, - "verdict": "true", - "note": "TRUE out-of-bounds read, by the source's own account. hashlittle's tail switch reads a whole 32-bit word of the key and masks off the bytes past the key's end (here `c+=k[2]&0xffffff`). The comment above the switch (lookup3.h:240-247) says that the expression \"actually reads beyond the end of the string, but then masks off the part it's not allowed to read\" and that Valgrind \"will still catch it and complain\"; lookup3.h:210-220 defines NO_MASKING_TRICK, which selects the byte-wise tail instead, under VALGRIND and when the compiler reports AddressSanitizer. The key is what hashtable.c passes to hash_str (hashtable.c:38, called at 238, 265, 276 and 303): the caller's key, or a pair's flexible `key` member allocated with offsetof(pair_t, key) + key_len + 1 bytes (hashtable.c:210). When the key's object ends less than a word after the last full word of the key, the read crosses the end of the object. The report-mode run of the CTest suite names these four lines and then passes." - }, - { - "config": "jansson", - "file": "src/lookup3.h", - "line": 260, - "verdict": "true", - "note": "TRUE out-of-bounds read, by the source's own account. hashlittle's tail switch reads a whole 32-bit word of the key and masks off the bytes past the key's end (here `c+=k[2]&0xffff`). The comment above the switch (lookup3.h:240-247) says that the expression \"actually reads beyond the end of the string, but then masks off the part it's not allowed to read\" and that Valgrind \"will still catch it and complain\"; lookup3.h:210-220 defines NO_MASKING_TRICK, which selects the byte-wise tail instead, under VALGRIND and when the compiler reports AddressSanitizer. The key is what hashtable.c passes to hash_str (hashtable.c:38, called at 238, 265, 276 and 303): the caller's key, or a pair's flexible `key` member allocated with offsetof(pair_t, key) + key_len + 1 bytes (hashtable.c:210). When the key's object ends less than a word after the last full word of the key, the read crosses the end of the object. The report-mode run of the CTest suite names these four lines and then passes." - }, - { - "config": "jansson", - "file": "src/lookup3.h", - "line": 263, - "verdict": "true", - "note": "TRUE out-of-bounds read, by the source's own account. hashlittle's tail switch reads a whole 32-bit word of the key and masks off the bytes past the key's end (here `b+=k[1]&0xffffff`). The comment above the switch (lookup3.h:240-247) says that the expression \"actually reads beyond the end of the string, but then masks off the part it's not allowed to read\" and that Valgrind \"will still catch it and complain\"; lookup3.h:210-220 defines NO_MASKING_TRICK, which selects the byte-wise tail instead, under VALGRIND and when the compiler reports AddressSanitizer. The key is what hashtable.c passes to hash_str (hashtable.c:38, called at 238, 265, 276 and 303): the caller's key, or a pair's flexible `key` member allocated with offsetof(pair_t, key) + key_len + 1 bytes (hashtable.c:210). When the key's object ends less than a word after the last full word of the key, the read crosses the end of the object. The report-mode run of the CTest suite names these four lines and then passes." - }, - { - "config": "jansson", - "file": "src/lookup3.h", - "line": 264, - "verdict": "true", - "note": "TRUE out-of-bounds read, by the source's own account. hashlittle's tail switch reads a whole 32-bit word of the key and masks off the bytes past the key's end (here `b+=k[1]&0xffff`). The comment above the switch (lookup3.h:240-247) says that the expression \"actually reads beyond the end of the string, but then masks off the part it's not allowed to read\" and that Valgrind \"will still catch it and complain\"; lookup3.h:210-220 defines NO_MASKING_TRICK, which selects the byte-wise tail instead, under VALGRIND and when the compiler reports AddressSanitizer. The key is what hashtable.c passes to hash_str (hashtable.c:38, called at 238, 265, 276 and 303): the caller's key, or a pair's flexible `key` member allocated with offsetof(pair_t, key) + key_len + 1 bytes (hashtable.c:210). When the key's object ends less than a word after the last full word of the key, the read crosses the end of the object. The report-mode run of the CTest suite names these four lines and then passes." - } - ], - "entries": [ - { - "config": "jsmn", - "id": "use-after-move", - "certainty": "possible", - "file": "example/jsondump.c", - "line": 109, - "verdict": "false", - "note": "FALSE POSITIVE, family A (a failed realloc). `js = realloc_it(js, jslen + r + 1)` (example/jsondump.c:109) replaces `js` with the wrapper's result, and line 110 returns when that result is NULL, so every later use of `js` (113, 117, 128, and the argument at 109 on the next iteration) reads the buffer the successful realloc just returned; the old pointer is never read again. The size is at least 2 (`r >= 1` after the `r == 0` return at 100), so realloc cannot release `js` and return NULL. What the analysis probably loses is the separation between `realloc_it`'s two exits once its summary is applied (argument moved into a non-null result, or argument released and NULL returned; `js` also starts NULL, which makes the first call a malloc), so the moved state of the argument sticks to the reassigned `js` even past the null test." - }, - { - "config": "jsmn", - "id": "use-after-move", - "certainty": "possible", - "file": "example/jsondump.c", - "line": 113, - "verdict": "false", - "note": "FALSE POSITIVE, family A (a failed realloc). `js = realloc_it(js, jslen + r + 1)` (example/jsondump.c:109) replaces `js` with the wrapper's result, and line 110 returns when that result is NULL, so every later use of `js` (113, 117, 128, and the argument at 109 on the next iteration) reads the buffer the successful realloc just returned; the old pointer is never read again. The size is at least 2 (`r >= 1` after the `r == 0` return at 100), so realloc cannot release `js` and return NULL. What the analysis probably loses is the separation between `realloc_it`'s two exits once its summary is applied (argument moved into a non-null result, or argument released and NULL returned; `js` also starts NULL, which makes the first call a malloc), so the moved state of the argument sticks to the reassigned `js` even past the null test." - }, - { - "config": "jsmn", - "id": "use-after-move", - "certainty": "possible", - "file": "example/jsondump.c", - "line": 117, - "verdict": "false", - "note": "FALSE POSITIVE, family A (a failed realloc). `js = realloc_it(js, jslen + r + 1)` (example/jsondump.c:109) replaces `js` with the wrapper's result, and line 110 returns when that result is NULL, so every later use of `js` (113, 117, 128, and the argument at 109 on the next iteration) reads the buffer the successful realloc just returned; the old pointer is never read again. The size is at least 2 (`r >= 1` after the `r == 0` return at 100), so realloc cannot release `js` and return NULL. What the analysis probably loses is the separation between `realloc_it`'s two exits once its summary is applied (argument moved into a non-null result, or argument released and NULL returned; `js` also starts NULL, which makes the first call a malloc), so the moved state of the argument sticks to the reassigned `js` even past the null test." - }, - { - "config": "jsmn", - "id": "use-after-move", - "certainty": "possible", - "file": "example/jsondump.c", - "line": 128, - "verdict": "false", - "note": "FALSE POSITIVE, family A (a failed realloc). `js = realloc_it(js, jslen + r + 1)` (example/jsondump.c:109) replaces `js` with the wrapper's result, and line 110 returns when that result is NULL, so every later use of `js` (113, 117, 128, and the argument at 109 on the next iteration) reads the buffer the successful realloc just returned; the old pointer is never read again. The size is at least 2 (`r >= 1` after the `r == 0` return at 100), so realloc cannot release `js` and return NULL. What the analysis probably loses is the separation between `realloc_it`'s two exits once its summary is applied (argument moved into a non-null result, or argument released and NULL returned; `js` also starts NULL, which makes the first call a malloc), so the moved state of the argument sticks to the reassigned `js` even past the null test." - }, - { - "config": "linenoise", - "id": "double-free", - "certainty": "possible", - "file": "linenoise.c", - "line": 709, - "verdict": "false", - "note": "FALSE POSITIVE, family B (array of owned pointers). `freeCompletions` (linenoise.c:706) frees `lc->cvec[i]` once for each `i < lc->len`, and each element is a distinct `malloc` copy stored by `linenoiseAddCompletion` (linenoise.c:847, `lc->cvec[lc->len++] = copy`). The engine summarises the array as one cell `cvec[*]`, so the second iteration reads as a second release of the same object." - }, - { - "config": "linenoise-program", - "id": "double-free", - "certainty": "possible", - "file": "linenoise.c", - "line": 709, - "verdict": "false", - "note": "FALSE POSITIVE, family B (array of owned pointers). `freeCompletions` (linenoise.c:706) frees `lc->cvec[i]` once for each `i < lc->len`, and each element is a distinct `malloc` copy stored by `linenoiseAddCompletion` (linenoise.c:847, `lc->cvec[lc->len++] = copy`). The engine summarises the array as one cell `cvec[*]`, so the second iteration reads as a second release of the same object." - }, - { - "config": "cJSON-program", - "id": "double-free", - "certainty": "possible", - "file": "cJSON_Utils.c", - "line": 878, - "verdict": "false", - "note": "FALSE POSITIVE. In `apply_patch`'s root-replacement case, `overwrite_item(object, *value)` (cJSON_Utils.c:869) frees `object->string` at 793 and then `memcpy(root, &replacement, sizeof(cJSON))` at 804 overwrites the whole struct, so `object->string` now holds the duplicate's key, a fresh `cJSON_strdup` made by `cJSON_Duplicate` (cJSON.c, `cJSON_Duplicate_rec`); line 878 frees that new string, once. The analysis apparently does not let the struct-sized `memcpy` from the by-value `replacement` parameter overwrite the `string` field, so the pointer freed at 793 still appears to be in `object->string`." - }, - { - "config": "jansson", - "id": "double-free", - "certainty": "possible", - "file": "src/hashtable.c", - "line": 135, - "verdict": "false", - "note": "FALSE POSITIVE, family B (a loop releasing distinct elements). `hashtable_do_clear` (src/hashtable.c:128) walks the ordered list of pairs and drops each pair's reference to its value once, then frees that pair; each pair owns one reference, so a value stored under two keys has a reference count of two and is deleted only by the second `json_decref`. The engine summarises the list nodes (reached through `list_to_pair`) as one object and treats `json_decref` as a release rather than a reference drop, so the second iteration reads as a second release of the same `pair->value`." - }, - { - "config": "jansson", - "id": "double-free", - "certainty": "possible", - "file": "src/pack_unpack.c", - "line": 269, - "verdict": "false", - "note": "FALSE POSITIVE, correlated conditions. In `pack_object` `value` is released at src/pack_unpack.c:267 only when `s->has_error` is set, and handed to `json_object_setn_new_nocheck` at 269 only when it is clear; nothing between the two tests writes `s->has_error` (`json_decref` does not see `s`), so exactly one of them runs per iteration, and `value` is reassigned by `pack` on the next one. `json_object_setn_new_nocheck`'s own `json == value` release is infeasible here because `object` is the fresh `json_object()` of this call and `value` comes from the caller's arguments. The engine keeps both paths, probably because it does not correlate the two loads of `s->has_error` (family C, the `json == value` guard, is the other candidate)." - }, - { - "config": "jansson", - "id": "double-free", - "certainty": "possible", - "file": "src/pack_unpack.c", - "line": 321, - "verdict": "false", - "note": "FALSE POSITIVE, correlated conditions. In `pack_array` `value` is released at src/pack_unpack.c:319 only when `s->has_error` is set, and handed to `json_array_append_new` at 321 only when it is clear; nothing between the two tests writes `s->has_error`, so exactly one of them runs per iteration, and `value` is reassigned by `pack` on the next one. `json_array_append_new`'s `json == value` release is infeasible because `array` is the fresh `json_array()` of this call. The engine keeps both paths, probably because it does not correlate the two loads of `s->has_error` (family C, the `json == value` guard, is the other candidate)." - }, - { - "config": "jansson", - "id": "double-free", - "certainty": "possible", - "file": "src/value.c", - "line": 455, - "verdict": "false", - "note": "FALSE POSITIVE, family B (array of owned pointers). `json_delete_array` (src/value.c:451) drops the reference held by each slot `array->table[i]`, `i < entries`, once; each slot owns one reference, so a value appended twice has a reference count of two and survives the first `json_decref`. The engine summarises the table as one cell `table[*]` and reads `json_decref` as a release, so the second iteration reads as a second release of the same object." - }, - { - "config": "jansson", - "id": "double-free", - "certainty": "possible", - "file": "src/value.c", - "line": 616, - "verdict": "false", - "note": "FALSE POSITIVE, family B (array of owned pointers). `json_array_clear` (src/value.c:607) drops the reference held by each slot `array->table[i]`, `i < entries`, once, then sets `entries = 0`; each slot owns one reference, so a value appended twice has a reference count of two and survives the first `json_decref`. The engine summarises the table as one cell `table[*]` and reads `json_decref` as a release, so the second iteration reads as a second release of the same object." - }, - { - "config": "lua", - "id": "use-after-move", - "certainty": "possible", - "file": "ldo.c", - "line": 343, - "verdict": "false", - "note": "FALSE POSITIVE. luaD_reallocstack passes the old stack pointer to correctstack, which only subtracts it from the stack slots' offsets (pointer arithmetic, relstack/correctstack); the old block is never read after the reallocation moved it." - }, - { - "config": "lua", - "id": "use-after-move", - "certainty": "possible", - "file": "ldo.c", - "line": 349, - "verdict": "false", - "note": "FALSE POSITIVE. luaD_reallocstack passes the old stack pointer to correctstack, which only subtracts it from the stack slots' offsets (pointer arithmetic, relstack/correctstack); the old block is never read after the reallocation moved it." - }, - { - "config": "lua", - "id": "use-after-move", - "certainty": "possible", - "file": "lmem.c", - "line": 182, - "verdict": "false", - "note": "FALSE POSITIVE. luaM_realloc_ calls tryagain with the block only when firsttry returned NULL for a non-zero size: a failed reallocation, which keeps the block (l_alloc is realloc, and a zero size never reaches here). The analysis does not tie the move to the non-null result through the allocator function pointer." - }, - { - "config": "lua", - "id": "use-after-move", - "certainty": "possible", - "file": "lobject.c", - "line": 572, - "verdict": "false", - "note": "FALSE POSITIVE. addstr2buff reallocates buff->b only when it no longer points to the static space (buff->b == buff->space selects a fresh allocation instead), and stores the new block before any further use; the analysis joins the two arms and keeps the old value possibly moved." - }, - { - "config": "lua", - "id": "use-after-move", - "certainty": "possible", - "file": "lobject.c", - "line": 601, - "verdict": "false", - "note": "FALSE POSITIVE. &buff is the address of luaO_pushvfstring's local BuffFS, which is never freed; addstr2buff's reallocation moves buff->b, and because buff->b may point into buff itself (its static space) the analysis takes the move for a possible move of buff. The code reallocates only when buff->b is not the static space." - }, - { - "config": "lua", - "id": "use-after-move", - "certainty": "possible", - "file": "lobject.c", - "line": 606, - "verdict": "false", - "note": "FALSE POSITIVE. &buff is the address of luaO_pushvfstring's local BuffFS, which is never freed; addstr2buff's reallocation moves buff->b, and because buff->b may point into buff itself (its static space) the analysis takes the move for a possible move of buff. The code reallocates only when buff->b is not the static space." - }, - { - "config": "lua", - "id": "use-after-move", - "certainty": "possible", - "file": "lobject.c", - "line": 611, - "verdict": "false", - "note": "FALSE POSITIVE. &buff is the address of luaO_pushvfstring's local BuffFS, which is never freed; addstr2buff's reallocation moves buff->b, and because buff->b may point into buff itself (its static space) the analysis takes the move for a possible move of buff. The code reallocates only when buff->b is not the static space." - }, - { - "config": "lua", - "id": "use-after-move", - "certainty": "possible", - "file": "lobject.c", - "line": 617, - "verdict": "false", - "note": "FALSE POSITIVE. &buff is the address of luaO_pushvfstring's local BuffFS, which is never freed; addstr2buff's reallocation moves buff->b, and because buff->b may point into buff itself (its static space) the analysis takes the move for a possible move of buff. The code reallocates only when buff->b is not the static space." - }, - { - "config": "lua", - "id": "use-after-move", - "certainty": "possible", - "file": "lobject.c", - "line": 623, - "verdict": "false", - "note": "FALSE POSITIVE. &buff is the address of luaO_pushvfstring's local BuffFS, which is never freed; addstr2buff's reallocation moves buff->b, and because buff->b may point into buff itself (its static space) the analysis takes the move for a possible move of buff. The code reallocates only when buff->b is not the static space." - }, - { - "config": "lua", - "id": "use-after-move", - "certainty": "possible", - "file": "lobject.c", - "line": 629, - "verdict": "false", - "note": "FALSE POSITIVE. &buff is the address of luaO_pushvfstring's local BuffFS, which is never freed; addstr2buff's reallocation moves buff->b, and because buff->b may point into buff itself (its static space) the analysis takes the move for a possible move of buff. The code reallocates only when buff->b is not the static space." - }, - { - "config": "lua", - "id": "use-after-move", - "certainty": "possible", - "file": "lobject.c", - "line": 636, - "verdict": "false", - "note": "FALSE POSITIVE. &buff is the address of luaO_pushvfstring's local BuffFS, which is never freed; addstr2buff's reallocation moves buff->b, and because buff->b may point into buff itself (its static space) the analysis takes the move for a possible move of buff. The code reallocates only when buff->b is not the static space." - }, - { - "config": "lua", - "id": "use-after-move", - "certainty": "possible", - "file": "lobject.c", - "line": 643, - "verdict": "false", - "note": "FALSE POSITIVE. &buff is the address of luaO_pushvfstring's local BuffFS, which is never freed; addstr2buff's reallocation moves buff->b, and because buff->b may point into buff itself (its static space) the analysis takes the move for a possible move of buff. The code reallocates only when buff->b is not the static space." - }, - { - "config": "lua", - "id": "use-after-move", - "certainty": "possible", - "file": "lobject.c", - "line": 647, - "verdict": "false", - "note": "FALSE POSITIVE. &buff is the address of luaO_pushvfstring's local BuffFS, which is never freed; addstr2buff's reallocation moves buff->b, and because buff->b may point into buff itself (its static space) the analysis takes the move for a possible move of buff. The code reallocates only when buff->b is not the static space." - }, - { - "config": "lua", - "id": "use-after-move", - "certainty": "possible", - "file": "lobject.c", - "line": 651, - "verdict": "false", - "note": "FALSE POSITIVE. &buff is the address of luaO_pushvfstring's local BuffFS, which is never freed; addstr2buff's reallocation moves buff->b, and because buff->b may point into buff itself (its static space) the analysis takes the move for a possible move of buff. The code reallocates only when buff->b is not the static space." - }, - { - "config": "lua", - "id": "use-after-move", - "certainty": "possible", - "file": "lobject.c", - "line": 657, - "verdict": "false", - "note": "FALSE POSITIVE. &buff is the address of luaO_pushvfstring's local BuffFS, which is never freed; addstr2buff's reallocation moves buff->b, and because buff->b may point into buff itself (its static space) the analysis takes the move for a possible move of buff. The code reallocates only when buff->b is not the static space." - }, - { - "config": "lua", - "id": "use-after-move", - "certainty": "possible", - "file": "lobject.c", - "line": 658, - "verdict": "false", - "note": "FALSE POSITIVE. &buff is the address of luaO_pushvfstring's local BuffFS, which is never freed; addstr2buff's reallocation moves buff->b, and because buff->b may point into buff itself (its static space) the analysis takes the move for a possible move of buff. The code reallocates only when buff->b is not the static space." - }, - { - "config": "sqlite", - "id": "unsafe-operation", - "certainty": "definite", - "file": "sqlite3.c", - "line": 127325, - "verdict": "true", - "note": "TRUE under the model's rules. `zP4 = x.p4type==P4_INT32 ? SQLITE_INT_TO_PTR(x.p4.i) : x.p4.z;` makes `zP4` a pointer from an integer on one arm (RFC 0004 raw, `(void*)(intptr_t)X`), and `sqlite3VdbeAddOp4` hands it to `sqlite3VdbeChangeP4`, which reads through it when `n` (here `x.p4type`) is a string or blob type and only stores it back as an integer for P4_INT32. RFC 0004 (Passing to a dereferencing callee, and its drawback 'A raw pointer passed to a callee that only might dereference it'): the check is against the callee's summary, whose effects are may-effects, so the call is the dereference. At run time the P4_INT32 path never dereferences it; the finding is the RFC's documented coarseness, not a memory error." - }, - { - "config": "sqlite", - "id": "unsafe-operation", - "certainty": "definite", - "file": "sqlite3.c", - "line": 157755, - "verdict": "true", - "note": "TRUE under the model's rules. `sqlite3VdbeChangeP4(v, -1, SQLITE_INT_TO_PTR(n), P4_INT32)` passes a pointer made from an integer (RFC 0004 raw) to a callee that reads through `zP4` for other `n` (`vdbeChangeP4Full` copies it when `n > 0`). RFC 0004 (Passing to a dereferencing callee, and its drawback 'A raw pointer passed to a callee that only might dereference it'): the check is against the callee's summary, whose effects are may-effects, so the call is the dereference. With P4_INT32 the callee only converts it back (`SQLITE_PTR_TO_INT`)." - }, - { - "config": "sqlite", - "id": "unsafe-operation", - "certainty": "definite", - "file": "sqlite3.c", - "line": 170850, - "verdict": "true", - "note": "TRUE under the model's rules. `sqlite3_wal_hook(db, sqlite3WalDefaultHook, SQLITE_INT_TO_PTR(nFrame))` passes a pointer made from an integer (RFC 0004 raw) as the hook's context argument; `sqlite3_wal_hook` stores it and the WAL code later passes it to the installed hook, which may dereference it (the summary follows the slot's unknown targets). RFC 0004 (Passing to a dereferencing callee, and its drawback 'A raw pointer passed to a callee that only might dereference it'): the check is against the callee's summary, whose effects are may-effects, so the call is the dereference. `sqlite3WalDefaultHook` converts it back with `SQLITE_PTR_TO_INT` and never dereferences it." - } - ] + "guardFailures": [] } diff --git a/test/lit.cfg.py b/test/lit.cfg.py index 1473ed06..88bf2ed7 100644 --- a/test/lit.cfg.py +++ b/test/lit.cfg.py @@ -3,7 +3,6 @@ Test files are C sources with `// RUN:` lines. Available substitutions: - %weavec the weavec binary (annotation header already on the path) %weavec_cc the weavec-cc compiler driver %resource_dir directory containing weavec.h FileCheck, not, count LLVM test utilities @@ -26,15 +25,10 @@ # Tools -------------------------------------------------------------------- -weavec = os.path.join(config.weavec_tools_dir, "weavec") -if not os.path.exists(weavec): - lit_config.fatal(f"weavec binary not found at {weavec}; build it first") - weavec_cc = os.path.join(config.weavec_tools_dir, "weavec-cc") if not os.path.exists(weavec_cc): lit_config.fatal(f"weavec-cc binary not found at {weavec_cc}; build it first") -# Longer names first so `%weavec_cc` is not rewritten as `%weavec` + `_cc`. config.substitutions.append(("%weavec_cc", weavec_cc)) # RFC 0035: the runtime archive. config.substitutions.append( @@ -42,7 +36,6 @@ ) # The reference compiler: the clang of the LLVM WeaveC is built against. config.substitutions.append(("%clang", config.clang or "clang")) -config.substitutions.append(("%weavec", weavec)) config.substitutions.append( ("%resource_dir", os.path.join(config.weavec_resource_dir, "include")) ) diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index eda690a3..eee28f4e 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -1,2 +1 @@ -add_subdirectory(weavec) add_subdirectory(weavec-cc) diff --git a/tools/weavec/CMakeLists.txt b/tools/weavec/CMakeLists.txt deleted file mode 100644 index 7a881803..00000000 --- a/tools/weavec/CMakeLists.txt +++ /dev/null @@ -1,9 +0,0 @@ -# The `weavec` command-line tool. -weavec_add_executable( - weavec - SOURCES main.cpp - DEPS weavec::Frontend - USES_LLVM) - -weavec_link_llvm(weavec Support) -weavec_link_clang(weavec clangTooling clangBasic) diff --git a/tools/weavec/main.cpp b/tools/weavec/main.cpp deleted file mode 100644 index 3513c326..00000000 --- a/tools/weavec/main.cpp +++ /dev/null @@ -1,515 +0,0 @@ -//===- main.cpp - The weavec command-line tool ----------------------------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// `weavec` runs WeaveC's analyses over C sources using libTooling: -// -// weavec file.c -- -I include -DFOO -// weavec -p build/ file.c (using a compile_commands.json) -// weavec --whole-program -p build/ (every file of the database, as one -// program; RFC 0005) -// -// Warning control follows the compiler's spelling: -Wno-weavec-, -// -Werror=weavec-, -Wno-error=weavec. RFC 0035 §8: the analysis is -// advisory; the tool prints its diagnostics and one summary line per unit -// (and per program), and the budget (--budget) and --no-zero-init model -// the analysis of a `weavec-cc -fweavec-diagnose` build. The drop-in -// compiler driver is `weavec-cc`. -// -//===----------------------------------------------------------------------===// - -#include "weavec/Analysis/AttributeReader.h" -#include "weavec/Analysis/KindInference.h" -#include "weavec/Analysis/KindTable.h" -#include "weavec/Analysis/SlotCollector.h" -#include "weavec/Config/Version.h" -#include "weavec/Core/FnSlots.h" -#include "weavec/Core/Ledger.h" -#include "weavec/Core/LibrarySpec.h" -#include "weavec/Frontend/AnalysisStats.h" -#include "weavec/Frontend/AnalysisSummary.h" -#include "weavec/Frontend/DiagnosticControl.h" -#include "weavec/Frontend/FrontendAction.h" -#include "weavec/Frontend/ProgramAnalysis.h" -#include "weavec/Frontend/ProgramChecks.h" -#include "weavec/Frontend/ResourceDir.h" - -#include "clang/AST/ASTConsumer.h" -#include "clang/AST/ASTContext.h" -#include "clang/Frontend/CompilerInstance.h" -#include "clang/Frontend/FrontendAction.h" -#include "clang/Tooling/ArgumentsAdjusters.h" -#include "clang/Tooling/CommonOptionsParser.h" -#include "clang/Tooling/CompilationDatabase.h" -#include "clang/Tooling/Tooling.h" - -#include "llvm/ADT/STLExtras.h" -#include "llvm/ADT/SmallString.h" -#include "llvm/Support/CommandLine.h" -#include "llvm/Support/FileSystem.h" -#include "llvm/Support/InitLLVM.h" -#include "llvm/Support/Path.h" -#include "llvm/Support/raw_ostream.h" - -#include -#include -#include -#include -#include -#include - -namespace { - -namespace cl = llvm::cl; - -// Command-line options are registered through global objects by design. -// NOLINTBEGIN(cppcoreguidelines-avoid-non-const-global-variables) -cl::OptionCategory weavecCategory("weavec options"); - -cl::opt - analysisStatsPath("analysis-stats", - cl::desc("Write analysis work statistics JSON"), - cl::cat(weavecCategory)); - -cl::opt - budget("budget", - cl::desc("Work per function before its analysis stops: the sizes " - "of the states it transfers and joins (default: 20000000; " - "0: unlimited)"), - cl::init(weavec::core::DefaultBudget), cl::cat(weavecCategory)); - -cl::opt noZeroInit("no-zero-init", - cl::desc("Model a build without zero-initialisation " - "(-fno-weavec-zero-init)"), - cl::init(false), cl::cat(weavecCategory)); - -cl::opt dumpAnalysis( - "dump-analysis", - cl::desc("Print the inferred places, lifetimes, exit state and summary " - "of every analysed function to stdout (debugging aid; format " - "unstable)"), - cl::init(false), cl::cat(weavecCategory)); - -cl::opt dumpKinds( - "dump-kinds", - cl::desc("Print each unit's pointer kinds (RFC 0030 §7), must-access " - "requirements, store groups, field candidates and " - "function-pointer slots to stdout instead of analysing " - "(debugging aid; format unstable)"), - cl::init(false), cl::cat(weavecCategory)); - -cl::opt wholeProgram( - "whole-program", - cl::desc("Analyse the given sources (all sources of the compilation " - "database if none are given) as one program, so calls into " - "other files are checked against their definitions"), - cl::init(false), cl::cat(weavecCategory)); - -// HelpMessage is a constant-initialised string literal, so the usual -// initialisation-order concern does not apply (this is the libTooling idiom). -// NOLINTNEXTLINE(cppcoreguidelines-interfaces-global-init) -cl::extrahelp commonHelp(clang::tooling::CommonOptionsParser::HelpMessage); -cl::extrahelp moreHelp( - "\nWarning control: -Wno-weavec- disables a warning, -Wweavec-\n" - "re-enables it, -Werror=weavec[-] and -Wno-error=weavec[-] change\n" - "severities. Errors cannot be disabled, only lowered to warnings.\n" - "-Wweavec-allocation-failure enables the one id that is off by default.\n" - "(weavec-cc also turns leak off by default; this tool reports it.)\n" - "\nWeaveC brings inferred ownership and borrowing to existing C code.\n" - "See https://github.com/weavefoundry/weavec for documentation.\n"); - -// Any symbol inside this executable works for locating it on disk. -int mainExecutableAnchor = 0; -// NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables) - -} // namespace - -namespace { - -/// `--dump-kinds`: the declared and inferred kinds and the slots of a unit, -/// before any engine runs (RFC 0030 §7, §9.3). -class DumpKindsConsumer : public clang::ASTConsumer { -public: - // NOLINTNEXTLINE(readability-identifier-naming): ASTConsumer's hook - void HandleTranslationUnit(clang::ASTContext &context) override { - const weavec::core::LibrarySpec &library = - weavec::core::LibrarySpec::shipped(); - weavec::analysis::KindTable kinds = - weavec::analysis::AttributeReader(context, library).read(); - const weavec::analysis::SlotCollection slots = - weavec::analysis::SlotCollector(context, library).collect(); - const weavec::core::SlotSolution solution = slots.solve(); - weavec::analysis::KindInferenceOptions options; - options.slots = &slots; - options.slotSolution = &solution; - const weavec::analysis::KindInferenceResult inferred = - weavec::analysis::KindInference(context, library, options).infer(kinds); - llvm::outs() << "unit '" << slots.unit() << "'\n"; - weavec::analysis::dumpKinds(kinds, inferred, context, llvm::outs()); - weavec::analysis::dumpSlots(slots, solution, context, llvm::outs()); - } -}; - -class DumpKindsAction : public clang::ASTFrontendAction { -protected: - // NOLINTNEXTLINE(readability-identifier-naming): FrontendAction's hook - std::unique_ptr - CreateASTConsumer(clang::CompilerInstance & /*compiler*/, - llvm::StringRef /*file*/) override { - return std::make_unique(); - } -}; - -} // namespace - -static std::string currentDirectory() { - llvm::SmallString<256> cwd; - if (llvm::sys::fs::current_path(cwd)) - return {}; - return cwd.str().str(); -} - -/// The ledger diagnostic of a diagnostic the program check reported, for -/// the program's summary line. -static weavec::core::LedgerDiagnostic -ledgerDiagnosticOf(const weavec::core::Diagnostic &diagnostic) { - weavec::core::LedgerDiagnostic entry; - entry.id = std::string(diagnostic.id); - entry.severity = diagnostic.severity; - entry.certainty = diagnostic.certainty; - entry.message = diagnostic.message; - entry.location = diagnostic.location; - return entry; -} - -/// RFC 0035 §8 in `weavec --whole-program`: the declarations verified -/// against their definitions, and the program's summary line from the -/// units' last runs. False after an error. -static bool -finishProgram(const weavec::frontend::ProgramAnalysis &program, - const clang::tooling::CompilationDatabase &compilations, - const std::vector &sources, - const weavec::frontend::FrontendOptions &options) { - namespace frontend = weavec::frontend; - const std::string cwd = currentDirectory(); - std::vector members; - weavec::core::Ledger ledger; - std::string programName = "program"; - for (std::size_t i = 0; i < program.unitCount(); ++i) { - const weavec::analysis::UnitExports *exports = program.exportsOf(i); - if (exports == nullptr) - continue; - frontend::ProgramMember member; - member.source = program.unitName(i); - if (i < sources.size()) { - const auto commands = compilations.getCompileCommands(sources[i]); - if (!commands.empty()) - member.cwd = commands.front().Directory; - } - member.exports = *exports; - if (const auto *facts = program.interfaceOf(i)) - member.facts = *facts; - const weavec::core::Ledger *run = program.ledgerOf(i); - if (run != nullptr && !run->units.empty()) { - if (llvm::any_of(run->units.front().functions, - [](const weavec::core::FunctionLedger &function) { - return function.name == "main"; - })) - programName = llvm::sys::path::stem(member.source).str(); - // The unit's rows and diagnostics, its facets' links moved past the - // diagnostics of the units before it. - const auto base = static_cast(ledger.diagnostics.size()); - weavec::core::UnitLedger rows = run->units.front(); - for (weavec::core::FunctionLedger &function : rows.functions) - for (weavec::core::Site &site : function.sites) - for (auto &record : site.facets) - if (record && record->diagnostic) - *record->diagnostic += base; - ledger.units.push_back(std::move(rows)); - ledger.diagnostics.insert(ledger.diagnostics.end(), - run->diagnostics.begin(), - run->diagnostics.end()); - } - members.push_back(std::move(member)); - } - frontend::ProgramDiagnosticPrinter printer("weavec"); - frontend::FilteringSink sink(printer, options.control); - for (const weavec::core::Diagnostic &diagnostic : - frontend::verifyDeclarations(members, cwd)) { - sink.report(diagnostic); - ledger.diagnostics.push_back(ledgerDiagnosticOf(diagnostic)); - } - frontend::printProgramSummary(std::move(ledger), programName, - options.control); - return sink.errors() == 0; -} - -static void printVersion(llvm::raw_ostream &os) { - os << "weavec version " << WEAVEC_VERSION_STRING; - os << " (" << WEAVEC_GIT_REVISION; - if (WEAVEC_GIT_DIRTY) - os << "-dirty"; - os << ")\n"; - os << " built with LLVM " << WEAVEC_LLVM_VERSION_STRING << "\n"; -} - -/// Removes WeaveC's `-W` flags (which `llvm::cl` would reject) from the -/// tool's own arguments, before `--`, and applies them to `control`. False -/// after printing an error for a malformed one. -static bool extractWarningFlags(int &argc, const char **argv, - weavec::frontend::DiagnosticControl &control) { - int kept = 1; - bool passthrough = false; - for (int i = 1; i < argc; ++i) { - const llvm::StringRef arg = argv[i]; - if (!passthrough && arg == "--") - passthrough = true; - std::string error; - if (!passthrough && control.parse(arg, error)) { - if (!error.empty()) { - llvm::errs() << "weavec: error: " << error << '\n'; - return false; - } - continue; - } - argv[kept++] = argv[i]; - } - argc = kept; - return true; -} - -/// An option `CommonOptionsParser` registers itself (`-p`, `--extra-arg`, -/// `--extra-arg-before`) and keeps no accessor for. -template -static const Option *commonOption(llvm::StringRef name) { - const auto &options = cl::getRegisteredOptions(); - const auto found = options.find(name); - // The parser registers each of these names with exactly this type. - return found == options.end() ? nullptr - : static_cast(found->second); -} - -/// RFC 0030 §16: the database of `-p` for a run that names no source. -/// `CommonOptionsParser` loads none then (`cl::ZeroOrMore`), so it is loaded -/// here, with `--extra-arg-before` and `--extra-arg` applied as the parser -/// applies them. Null, with `error` set, when there is none. -static std::unique_ptr -loadBuildDatabase(std::string &error) { - const auto *buildPath = commonOption>("p"); - if (buildPath == nullptr || buildPath->getValue().empty()) { - error = "no compilation database with sources; give -p or " - "list the files"; - return nullptr; - } - std::string message; - std::unique_ptr database = - clang::tooling::CompilationDatabase::autoDetectFromDirectory( - buildPath->getValue(), message); - if (!database) { - // As for `-p` with a source, the directory's parents are searched too. - error = "no compilation database in '" + buildPath->getValue() + - "' or any parent directory"; - return nullptr; - } - auto adjusted = - std::make_unique( - std::move(database)); - const auto extraArgs = [](llvm::StringRef name) { - std::vector args; - if (const auto *list = commonOption>(name)) - args.assign(list->begin(), list->end()); - return args; - }; - adjusted->appendArgumentsAdjuster(clang::tooling::getInsertArgumentAdjuster( - extraArgs("extra-arg-before"), - clang::tooling::ArgumentInsertPosition::BEGIN)); - adjusted->appendArgumentsAdjuster(clang::tooling::getInsertArgumentAdjuster( - extraArgs("extra-arg"), clang::tooling::ArgumentInsertPosition::END)); - return adjusted; -} - -/// Whether a compile command asks for `-fdiagnostics-format=sarif` (the last -/// format given wins, as in Clang). -static bool asksForSarif(const std::vector &command) { - llvm::StringRef format; - for (std::size_t i = 0; i < command.size(); ++i) { - llvm::StringRef arg = command[i]; - if (arg.consume_front("-fdiagnostics-format=")) - format = arg; - else if (arg == "-fdiagnostics-format" && i + 1 < command.size()) - format = command[++i]; - } - return format.equals_insensitive("sarif"); -} - -/// The argument adjusters every parse gets: the annotation header and the -/// `__WEAVEC__` feature macro, Clang's builtin headers, and a default SDK on -/// Apple platforms. -static std::vector -makeAdjusters(const char *argv0) { - std::vector adjusters; - - const std::string resourceInclude = - weavec::frontend::findResourceIncludeDir(argv0, &mainExecutableAnchor); - if (!resourceInclude.empty()) { - adjusters.push_back(clang::tooling::getInsertArgumentAdjuster( - {"-isystem", resourceInclude}, - clang::tooling::ArgumentInsertPosition::BEGIN)); - } - adjusters.push_back(clang::tooling::getInsertArgumentAdjuster( - "-D__WEAVEC__=1", clang::tooling::ArgumentInsertPosition::BEGIN)); - - // Use the builtin headers of the Clang we were built against. An explicit - // -resource-dir on the command line still takes precedence (it comes later). - const std::string clangResourceDir = weavec::frontend::getClangResourceDir(); - if (!clangResourceDir.empty()) { - adjusters.push_back(clang::tooling::getInsertArgumentAdjuster( - ("-resource-dir=" + clangResourceDir).c_str(), - clang::tooling::ArgumentInsertPosition::BEGIN)); - } - - // On Apple platforms the driver library has no default SDK; mirror what the - // `clang` binary does unless the user already specified a sysroot. - const std::string sysroot = weavec::frontend::getDefaultSysroot(); - if (!sysroot.empty()) { - adjusters.emplace_back([sysroot]( - const clang::tooling::CommandLineArguments &args, - llvm::StringRef /*filename*/) { - const bool hasSysroot = llvm::any_of(args, [](const std::string &arg) { - return llvm::StringRef(arg).starts_with("-isysroot") || - llvm::StringRef(arg).starts_with("--sysroot"); - }); - if (hasSysroot) - return args; - clang::tooling::CommandLineArguments adjusted(args); - adjusted.insert(adjusted.begin() + 1, {"-isysroot", sysroot}); - return adjusted; - }); - } - return adjusters; -} - -int main(int argc, const char **argv) { - llvm::InitLLVM init(argc, argv); - cl::SetVersionPrinter(printVersion); - cl::HideUnrelatedOptions(weavecCategory); - - weavec::frontend::DiagnosticControl control; - if (!extractWarningFlags(argc, argv, control)) - return 1; - - // With `--`, the parser's database is the fixed one that follows it. - const bool fixedDatabase = - std::find(argv, argv + argc, llvm::StringRef("--")) != argv + argc; - auto expectedParser = clang::tooling::CommonOptionsParser::create( - argc, argv, weavecCategory, cl::ZeroOrMore, - "weavec: memory-safety analysis for C"); - if (!expectedParser) { - llvm::errs() << llvm::toString(expectedParser.takeError()); - return 1; - } - clang::tooling::CommonOptionsParser &parser = *expectedParser; - - // Without a source the parser loads no database (and has none to give - // unless `--` built the fixed one): `--whole-program -p ` loads it - // here and analyses every file it lists. - std::vector sources = parser.getSourcePathList(); - std::unique_ptr buildDatabase; - if (sources.empty()) { - if (!wholeProgram) { - llvm::errs() << "weavec: error: no input files\n"; - return 1; - } - std::string error = "no compilation database with sources; give -p " - " or list the files"; - if (!fixedDatabase) - buildDatabase = loadBuildDatabase(error); - if (buildDatabase) - sources = buildDatabase->getAllFiles(); - if (sources.empty()) { - if (buildDatabase) - error = "the compilation database lists no files"; - llvm::errs() << "weavec: error: " << error << '\n'; - return 1; - } - } - const clang::tooling::CompilationDatabase &compilations = - buildDatabase ? *buildDatabase : parser.getCompilations(); - - // RFC 0030 §16: the tool's runs build their SourceManager before Clang - // attaches a SARIF document writer to its printer, so Clang's SARIF - // output would crash them. - for (const std::string &source : sources) { - for (const clang::tooling::CompileCommand &command : - compilations.getCompileCommands(source)) { - if (asksForSarif(command.CommandLine)) { - llvm::errs() << "weavec: error: -fdiagnostics-format=sarif is not " - "supported\n"; - return 1; - } - } - } - - if (analysisStatsPath.getNumOccurrences() && analysisStatsPath.empty()) { - llvm::errs() << "weavec: error: analysis statistics require a path\n"; - return 1; - } - weavec::core::AnalysisStats stats; - weavec::frontend::FrontendOptions options; - options.engine.stats = analysisStatsPath.empty() ? nullptr : &stats; - options.analysisStatsPath = analysisStatsPath.getValue(); - if (dumpAnalysis) - options.engine.dumpStream = &llvm::outs(); - options.engine.zeroInit = !noZeroInit; - options.engine.budget = budget; - options.control = control; - // RFC 0035 §8: the summary line is always printed. - options.summary = true; - - const std::vector adjusters = - makeAdjusters(argv[0]); - - if (wholeProgram) { - weavec::frontend::ProgramAnalysis program(options); - // RFC 0035 §8: the program's slots, boundaries and declarations. - program.collectInterfaces(true); - for (const std::string &source : sources) { - program.addUnit( - std::make_unique( - compilations, source, adjusters)); - } - const weavec::frontend::ProgramAnalysis::Result result = program.run(); - for (const std::string &name : result.failed) - llvm::errs() << "weavec: error: cannot analyse '" << name << "'\n"; - // RFC 0033 §7: a group that does not converge keeps its widened - // summaries, as a function over budget does. - for (const std::vector &component : result.nonConverging) { - llvm::errs() << "weavec: note: the whole-program analysis of "; - llvm::interleaveComma(component, llvm::errs(), [](const std::string &n) { - llvm::errs() << '\'' << n << '\''; - }); - llvm::errs() << " did not converge (its widened summaries are used)\n"; - } - const bool finished = - finishProgram(program, compilations, sources, options); - const bool statsOK = weavec::frontend::writeAnalysisStats( - analysisStatsPath, options.engine.stats); - return result.ok() && finished && statsOK ? 0 : 1; - } - - clang::tooling::ClangTool tool(compilations, sources); - for (const clang::tooling::ArgumentsAdjuster &adjuster : adjusters) - tool.appendArgumentsAdjuster(adjuster); - if (dumpKinds) - return tool.run( - clang::tooling::newFrontendActionFactory().get()); - const int status = - tool.run(weavec::frontend::createWeaveCActionFactory(options).get()); - const bool statsOK = weavec::frontend::writeAnalysisStats( - analysisStatsPath, options.engine.stats); - return status == 0 && statsOK ? 0 : 1; -} diff --git a/unittests/Analysis/AnnotationsTest.cpp b/unittests/Analysis/AnnotationsTest.cpp deleted file mode 100644 index 2a19b704..00000000 --- a/unittests/Analysis/AnnotationsTest.cpp +++ /dev/null @@ -1,231 +0,0 @@ -//===- AnnotationsTest.cpp - Tests for annotation parsing -----------------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// - -#include "weavec/Analysis/Annotations.h" - -#include "TestUtils.h" - -#include "clang/AST/Decl.h" -#include "clang/Tooling/Tooling.h" - -#include - -namespace weavec::analysis { -namespace { - -TEST(ParseAnnotation, RecognisesKnownSpellings) { - EXPECT_EQ(parseAnnotation("weavec.owned"), Annotation::Owned); - EXPECT_EQ(parseAnnotation("weavec.borrowed"), Annotation::Borrowed); - EXPECT_EQ(parseAnnotation("weavec.mut_borrowed"), Annotation::MutBorrowed); - EXPECT_EQ(parseAnnotation("weavec.unsafe"), Annotation::Unsafe); -} - -TEST(ParseAnnotation, UnknownWeaveCAnnotationIsInvalid) { - EXPECT_EQ(parseAnnotation("weavec.bogus"), Annotation::Invalid); - EXPECT_EQ(parseAnnotation("weavec."), Annotation::Invalid); -} - -// RFC 0010, *Annotations*. -TEST(ParseAnnotation, RecognisesShareSpellings) { - EXPECT_EQ(parseAnnotation("weavec.retains"), Annotation::Retains); - EXPECT_EQ(parseAnnotation("weavec.releases"), Annotation::Releases); - EXPECT_EQ(parseAnnotation("weavec.refcount"), Annotation::Refcount); - EXPECT_EQ(parseAnnotation("weavec.family.fclose"), Annotation::Family); - EXPECT_EQ(parseAnnotation("weavec.family.my_free2"), Annotation::Family); - EXPECT_EQ(parseAnnotation("weavec.family."), Annotation::Invalid); - EXPECT_EQ(parseAnnotation("weavec.family.not an id"), Annotation::Invalid); - EXPECT_EQ(parseAnnotation("weavec.family.a(b)"), Annotation::Invalid); -} - -// RFC 0011, *Annotations*: `WEAVEC_SIZED_BY(n)`. -TEST(ParseAnnotation, RecognisesSizedBy) { - EXPECT_EQ(parseAnnotation("weavec.sized_by.n"), Annotation::SizedBy); - EXPECT_EQ(parseAnnotation("weavec.sized_by.count_2"), Annotation::SizedBy); - EXPECT_EQ(parseAnnotation("weavec.sized_by."), Annotation::Invalid); - EXPECT_EQ(parseAnnotation("weavec.sized_by.n + 1"), Annotation::Invalid); -} - -// RFC 0030, *Annotation surface*: the extent macros and `WEAVEC_STRING`; -// `WEAVEC_CHECKED` is gone, and RFC 0035 deletes `WEAVEC_REQUIRE_SAFE`. -TEST(ParseAnnotation, RecognisesKindSpellings) { - EXPECT_EQ(parseAnnotation("weavec.counted_by.len"), Annotation::CountedBy); - EXPECT_EQ(parseAnnotation("weavec.ended_by.end"), Annotation::EndedBy); - EXPECT_EQ(parseAnnotation("weavec.string"), Annotation::String); - EXPECT_EQ(parseAnnotation("weavec.require_safe"), Annotation::Invalid); - EXPECT_EQ(parseAnnotation("weavec.counted_by."), Annotation::Invalid); - EXPECT_EQ(parseAnnotation("weavec.counted_by.2n"), Annotation::Invalid); - EXPECT_EQ(parseAnnotation("weavec.ended_by.p + 1"), Annotation::Invalid); - EXPECT_EQ(parseAnnotation("weavec.checked"), Annotation::Invalid); - EXPECT_EQ(spelling::CountedByPrefix, "weavec.counted_by."); - EXPECT_EQ(spelling::EndedByPrefix, "weavec.ended_by."); -} - -TEST(GetAnnotations, CollectsKindAnnotations) { - auto ast = clang::tooling::buildASTFromCodeWithArgs( - R"c( - __attribute__((annotate("weavec.nullable"))) - char *f(char *__attribute__((annotate("weavec.counted_by.n"))) p, - unsigned long n, - const char *__attribute__((annotate("weavec.string"))) s, - int *__attribute__((annotate("weavec.ended_by.e"))) b, - int *e, - int *__attribute__((annotate("weavec.counted_by.n"))) - __attribute__((annotate("weavec.counted_by.m"))) torn, - unsigned long m); - )c", - {"-x", "c"}, "input.c"); - ASSERT_TRUE(ast); - const clang::FunctionDecl *f = nullptr; - for (const clang::Decl *d : - ast->getASTContext().getTranslationUnitDecl()->decls()) - if (const auto *fn = llvm::dyn_cast(d)) - f = fn; - ASSERT_NE(f, nullptr); - const AnnotationSet onFunction = getAnnotations(*f); - EXPECT_TRUE(onFunction.nullable); - EXPECT_TRUE(onFunction.any()); - EXPECT_FALSE(onFunction.extent()); - const AnnotationSet p = getAnnotations(*f->getParamDecl(0)); - EXPECT_EQ(p.countedBy, "n"); - EXPECT_TRUE(p.extent()); - EXPECT_FALSE(p.ownership()); - EXPECT_TRUE(getAnnotations(*f->getParamDecl(2)).string); - EXPECT_EQ(getAnnotations(*f->getParamDecl(3)).endedBy, "e"); - const AnnotationSet torn = getAnnotations(*f->getParamDecl(5)); - EXPECT_TRUE(torn.invalid) << "two counts contradict"; - AnnotationSet merged; - merged.merge(p); - merged.merge(getAnnotations(*f->getParamDecl(2))); - EXPECT_EQ(merged.countedBy, "n"); - EXPECT_TRUE(merged.string); - EXPECT_FALSE(merged.invalid); -} - -TEST(GetAnnotations, CollectsSizedBy) { - auto ast = clang::tooling::buildASTFromCodeWithArgs( - R"c( - void f(char *__attribute__((annotate("weavec.sized_by.n"))) p, - unsigned long n, - char *__attribute__((annotate("weavec.sized_by.n"))) - __attribute__((annotate("weavec.sized_by.m"))) q, - unsigned long m); - )c", - {"-x", "c"}, "input.c"); - ASSERT_TRUE(ast); - const clang::FunctionDecl *f = nullptr; - for (const clang::Decl *d : - ast->getASTContext().getTranslationUnitDecl()->decls()) - if (const auto *fn = llvm::dyn_cast(d)) - f = fn; - ASSERT_NE(f, nullptr); - const AnnotationSet p = getAnnotations(*f->getParamDecl(0)); - EXPECT_EQ(p.sizedBy, "n"); - EXPECT_TRUE(p.any()); - EXPECT_FALSE(p.ownership()) << "an extent is not an ownership kind"; - EXPECT_FALSE(p.invalid); - EXPECT_TRUE(getAnnotations(*f->getParamDecl(1)).sizedBy.empty()); - EXPECT_TRUE(getAnnotations(*f->getParamDecl(2)).invalid) - << "two counters contradict"; -} - -TEST(GetAnnotations, CollectsShareAnnotationsAndFamilies) { - auto ast = clang::tooling::buildASTFromCodeWithArgs( - R"c( - struct obj; - void f(struct obj *__attribute__((annotate("weavec.retains"))) a, - struct obj *__attribute__((annotate("weavec.releases"))) b, - struct obj *__attribute__((annotate("weavec.owned"))) - __attribute__((annotate("weavec.family.obj_free"))) c, - struct obj *__attribute__((annotate("weavec.family.x"))) - __attribute__((annotate("weavec.family.y"))) d); - struct obj { int __attribute__((annotate("weavec.refcount"))) rc; }; - )c", - {"-x", "c"}, "input.c"); - ASSERT_TRUE(ast); - - const clang::FunctionDecl *f = nullptr; - const clang::RecordDecl *obj = nullptr; - for (const clang::Decl *d : - ast->getASTContext().getTranslationUnitDecl()->decls()) { - if (const auto *fn = llvm::dyn_cast(d)) - f = fn; - if (const auto *record = llvm::dyn_cast(d); - record != nullptr && record->isCompleteDefinition()) - obj = record; - } - ASSERT_NE(f, nullptr); - ASSERT_NE(obj, nullptr); - - const AnnotationSet a = getAnnotations(*f->getParamDecl(0)); - EXPECT_TRUE(a.retains); - EXPECT_TRUE(a.ownership()); - EXPECT_FALSE(a.owned); - const AnnotationSet b = getAnnotations(*f->getParamDecl(1)); - EXPECT_TRUE(b.releases); - EXPECT_TRUE(b.ownership()); - const AnnotationSet c = getAnnotations(*f->getParamDecl(2)); - EXPECT_TRUE(c.owned); - EXPECT_EQ(c.family, "obj_free"); - EXPECT_FALSE(c.invalid); - const AnnotationSet d = getAnnotations(*f->getParamDecl(3)); - EXPECT_TRUE(d.invalid) << "two families contradict"; - - const clang::FieldDecl *rc = *obj->field_begin(); - const AnnotationSet onField = getAnnotations(*rc); - EXPECT_TRUE(onField.refcount); - EXPECT_FALSE(onField.ownership()) << "a count is not an ownership kind"; - EXPECT_TRUE(onField.any()); - - // Merging keeps the family and flags; a disagreement is invalid. - AnnotationSet merged = a; - merged.merge(c); - EXPECT_TRUE(merged.retains); - EXPECT_TRUE(merged.owned); - EXPECT_EQ(merged.family, "obj_free"); - AnnotationSet other; - other.family = "elsewhere"; - merged.merge(other); - EXPECT_TRUE(merged.invalid); -} - -TEST(ParseAnnotation, ForeignAnnotationsAreIgnored) { - EXPECT_FALSE(parseAnnotation("gsl.owner")); - EXPECT_FALSE(parseAnnotation("")); - EXPECT_FALSE(parseAnnotation("weavec_owned")); -} - -TEST(GetAnnotations, CollectsFromDeclaration) { - auto ast = clang::tooling::buildASTFromCodeWithArgs( - R"c( - __attribute__((annotate("weavec.unsafe"))) - __attribute__((annotate("something.else"))) - void f(int *__attribute__((annotate("weavec.owned"))) p) {} - )c", - {"-x", "c"}, "input.c"); - ASSERT_TRUE(ast); - - const clang::FunctionDecl *f = nullptr; - for (const clang::Decl *d : - ast->getASTContext().getTranslationUnitDecl()->decls()) - if (const auto *fn = llvm::dyn_cast(d)) - f = fn; - ASSERT_NE(f, nullptr); - - const AnnotationSet onFunction = getAnnotations(*f); - EXPECT_TRUE(onFunction.unsafe); - EXPECT_FALSE(onFunction.owned); - EXPECT_FALSE(onFunction.invalid); - - const AnnotationSet onParam = getAnnotations(*f->getParamDecl(0)); - EXPECT_TRUE(onParam.owned); - EXPECT_FALSE(onParam.unsafe); - EXPECT_TRUE(onParam.any()); -} - -} // namespace -} // namespace weavec::analysis diff --git a/unittests/Analysis/ArrayOwnershipTest.cpp b/unittests/Analysis/ArrayOwnershipTest.cpp deleted file mode 100644 index 90cfeced..00000000 --- a/unittests/Analysis/ArrayOwnershipTest.cpp +++ /dev/null @@ -1,821 +0,0 @@ -//===- ArrayOwnershipTest.cpp - RFC 0015 container contents ---------------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// - -#include "TestUtils.h" -#include "weavec/Analysis/ProgramDatabase.h" -#include "weavec/Core/Effects.h" - -#include - -#include -#include - -namespace weavec::analysis { - -static std::size_t countId(const test::AnalysisResult &result, - std::string_view id) { - return static_cast( - std::ranges::count_if(result.diagnostics.diagnostics(), - [id](const core::Diagnostic &diagnostic) { - return diagnostic.id == id; - })); -} - -/// The outcome of `facet` at the site spelled `text` in `function`. -static std::optional -outcomeAt(const test::AnalysisResult &result, std::string_view function, - std::string_view text, core::Facet facet) { - for (const core::UnitLedger &unit : result.ledger.units) - for (const core::FunctionLedger &ledger : unit.functions) - if (ledger.name == function) - for (const core::Site &site : ledger.sites) - if (site.text == text) - if (const core::FacetRecord *record = site.facet(facet)) - return record->outcome(); - return std::nullopt; -} - -/// Whether the temporal facet at `text` in `function` exists and is not -/// proven. -static bool temporalNotProven(const test::AnalysisResult &result, - std::string_view function, - std::string_view text) { - auto outcome = outcomeAt(result, function, text, core::Facet::Temporal); - return outcome && *outcome != core::SiteOutcome::Proven; -} - -static constexpr const char *Memory = R"c( -void *memcpy(void *, const void *, size_t); -void *memmove(void *, const void *, size_t); -)c"; - -TEST(ArrayOwnership, ReleasingAnotherElementDoesNotEraseHistory) { - const auto result = test::analyze(R"c( -void bad(char **a) { free(a[0]); free(a[1]); a[0][0] = 1; free(a[0]); } -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 1U); - EXPECT_EQ(countId(result, core::diag::DoubleFree), 1U); -} - -TEST(ArrayOwnership, IndependentElementsMayEachBeReleasedOnce) { - const auto result = test::analyze(R"c( -void clean(char **a) { free(a[0]); free(a[1]); } -)c"); - ASSERT_TRUE(result.ast); - EXPECT_TRUE(result.diagnostics.empty()) - << ::testing::PrintToString(test::messages(result.diagnostics)); -} - -TEST(ArrayOwnership, RecordArrayArrowPreservesAliasReleaseHistory) { - const auto result = test::analyze(R"c( -struct slot { char *p; }; -void bad(void) { - struct slot a[2]; a->p=malloc(4); a[1].p=(*a).p; - free(a[1].p); free(a->p); -} -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::DoubleFree), 1U); -} - -TEST(ArrayOwnership, AConstantIndexIsCapturedBeforeReassignment) { - const auto result = test::analyze(R"c( -void bad(char **a) { int i = 0; free(a[i]); i = 1; a[0][0] = 1; } -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 1U); -} - -TEST(ArrayOwnership, SavedSymbolicIndicesRetainTheirEntryIdentity) { - const auto result = test::analyze(R"c( -void bad(char **a, int i) { int old = i; free(a[i]); i = 9; a[old][0] = 1; } -void clean(char **a, int i) { free(a[i]); a[i+1][0] = 1; } -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 1U); -} - -// RFC 0020: the selector index must discover cells created after an earlier -// snapshot, and still consult descendants in the current flow state. -TEST(ArrayOwnership, LateSelectorsAndTrackedChildrenRemainVisible) { - const auto result = test::analyze(R"c( -struct Cell { char *value; }; -void late(char **a, char **b, int i, int j) { - free(a[i]); i = 9; - int old = j; free(b[j]); j = 10; b[old][0] = 1; -} -void child(struct Cell *a, int i) { - int old = i; free(a[i].value); i = 9; a[old].value[0] = 1; -} -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 2U); -} - -TEST(ArrayOwnership, ReplacingAReleasedSlotDoesNotReviveAnOldAlias) { - const auto result = test::analyze(R"c( -void bad(char **a, char *replacement) { - char *old = a[0]; free(a[0]); a[0] = replacement; a[0][0] = 1; old[0] = 1; -} -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 1U); -} - -TEST(ArrayOwnership, InitializingOneCellDoesNotInitializeAnother) { - const auto result = test::analyze(R"c( -void bad(char *p) { char *a[2]; a[0] = p; a[0][0] = 1; a[1][0] = 1; } -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::UseOfUninitialized), 1U); -} - -TEST(ArrayOwnership, AggregateOmissionsAreNullAndGuardsRefineOnlyTheirCell) { - const auto result = test::analyze(R"c( -void bad(char *p) { char *a[2] = {p}; a[0][0] = 1; a[1][0] = 1; } -void clean(void) { - char *a[2]; a[0] = malloc(4); if (!a[0]) return; - a[1] = malloc(4); if (!a[1]) { free(a[0]); return; } - free(a[0]); a[1][0] = 1; free(a[1]); -} -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::NullDereference), 1U); - EXPECT_EQ(countId(result, core::diag::Leak), 0U); -} - -TEST(ArrayOwnership, CompleteCopiesPreservePointerIdentity) { - const auto result = test::analyze(std::string(Memory) + R"c( -void bad(char **source) { - char *dest[2]; memcpy(dest, source, sizeof dest); - free(source[0]); dest[0][0] = 1; -} -void clean(char **source) { - char *dest[2]; memcpy(dest, source, sizeof dest); - free(source[0]); dest[1][0] = 1; free(source[1]); -} -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 1U); - EXPECT_EQ(test::incomplete(result).size(), 0U); -} - -TEST(ArrayOwnership, OverlappingMovesSnapshotEverySourceBeforeWriting) { - const auto result = test::analyze(std::string(Memory) + R"c( -void right(char **a) { - char *old = a[1]; memmove(a+1, a, 2*sizeof *a); free(old); a[2][0] = 1; -} -void left(char **a) { - char *old = a[1]; memmove(a, a+1, 2*sizeof *a); free(old); a[0][0] = 1; -} -void clean(char **a) { - char *old = a[1]; memmove(a+1, a, 2*sizeof *a); free(old); a[1][0] = 1; -} -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 2U); - EXPECT_EQ(test::incomplete(result).size(), 0U); -} - -TEST(ArrayOwnership, SelfCopyAndZeroLengthDoNotLoseContents) { - const auto result = test::analyze(std::string(Memory) + R"c( -void bad(char **a) { free(a[0]); memmove(a,a,2*sizeof *a); a[0][0] = 1; } -void zero(char **a, char **b) { free(a[0]); memcpy(a,b,0); a[0][0] = 1; } -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 2U); -} - -TEST(ArrayOwnership, ACopyRetainsTheValueBeforeSourceReplacement) { - const auto result = test::analyze(std::string(Memory) + R"c( -void bad(char **a, char *replacement) { - char *old = a[0], *b[2]; memcpy(b,a,sizeof b); a[0] = replacement; - free(old); b[0][0] = 1; a[0][0] = 1; -} -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 1U); -} - -TEST(ArrayOwnership, SelectedEffectsComposeThroughHelpers) { - const auto result = test::analyze(R"c( -static void drop(char **a, int i) { free(a[i]); } -void bad(char **a) { drop(a,0); drop(a,1); a[0][0] = 1; } -void clean(char **a) { drop(a,0); a[1][0] = 1; } -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 1U); -} - -TEST(ArrayOwnership, CopyHelpersPublishFinalContents) { - const auto result = test::analyze(std::string(Memory) + R"c( -static void copy(char **d, char **s) { memcpy(d,s,2*sizeof *d); } -static void replace(char **d, char **s, char *p) { memcpy(d,s,2*sizeof *d); d[0] = p; } -void bad(char **a, char **b) { copy(b,a); free(a[0]); b[0][0] = 1; } -void clean(char **a, char **b, char *p) { replace(b,a,p); free(a[0]); b[0][0] = 1; } -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 1U); -} - -TEST(ArrayOwnership, CallbackCellsUseTheirOwnTarget) { - const auto result = test::analyze(R"c( -static void drop(void *p) { free(p); } -static void keep(void *p) { (void)p; } -void bad(char *p) { void (*table[2])(void *) = {drop, keep}; table[0](p); p[0] = 1; } -void clean(char *p) { void (*table[2])(void *) = {drop, keep}; table[1](p); p[0] = 1; } -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 1U); - EXPECT_EQ(test::incomplete(result).size(), 0U); -} - -TEST(ArrayOwnership, NestedDimensionsHaveIndependentCells) { - const auto result = test::analyze(R"c( -void bad(char *p, char *q) { - char *a[2][2] = {{p,q},{0,0}}; free(a[0][0]); free(a[0][1]); a[0][0][0] = 1; -} -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 1U); -} - -TEST(ArrayOwnership, RecordElementsPreserveTheirPointerFields) { - const auto result = test::analyze(std::string(Memory) + R"c( -struct item { char *p; int count; }; -void bad(struct item *a) { - struct item b[2]; char *old = a[0].p; memcpy(b,a,sizeof b); free(old); b[0].p[0] = 1; -} -void clean(struct item *a) { free(a[0].p); a[1].p[0] = 1; } -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 1U); -} - -TEST(ArrayOwnership, SymbolicCopiesMaterializeKnownMembers) { - const auto result = test::analyze(std::string(Memory) + R"c( -void bad(char **d, char **s, size_t n) { - memcpy(d,s,n*sizeof *s); if (n > 0) { free(s[0]); d[0][0] = 1; } -} -void clean(char **d, char **s, size_t n) { - memcpy(d,s,n*sizeof *s); if (n > 1) { free(s[0]); d[1][0] = 1; } -} -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 1U); - EXPECT_EQ(test::incomplete(result).size(), 0U) - << ::testing::PrintToString(test::messages(result.diagnostics)); -} - -TEST(ArrayOwnership, SymbolicCopyInputAndLengthSurviveLaterWrites) { - const auto result = test::analyze(std::string(Memory) + R"c( -void bad(char **d, char **s, size_t n, char *p) { - if (n == 0) return; - memcpy(d,s,n*sizeof *s); char *old = s[0]; s[0] = p; n = 0; - free(old); d[0][0] = 1; s[0][0] = 1; -} -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 1U); -} - -TEST(ArrayOwnership, SymbolicCopySummariesInstantiateCountsAndFinalOverrides) { - const auto result = test::analyze(std::string(Memory) + R"c( -static void copy(char **d, char **s, size_t n) { memcpy(d,s,n*sizeof *s); } -static void replace(char **d, char **s, size_t n, char *p) { - copy(d,s,n); if (n) d[0] = p; -} -void bad(char **s, char **d) { copy(d,s,2); free(s[0]); d[0][0] = 1; } -void clean(char **s, char **d, char *p) { replace(d,s,2,p); free(s[0]); d[0][0] = 1; } -)c"); - ASSERT_TRUE(result.ast); - ASSERT_TRUE(result.summary("copy")); - EXPECT_GE(test::elementStores(*result.summary("copy")), 1U); - // RFC 0031 §6.6 *numeric contexts*: `copy(d,s,2)` runs `copy` with - // `n == 2`, whose summary copies each element. - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 1U) - << ::testing::PrintToString(test::messages(result.diagnostics)); -} - -TEST(ArrayOwnership, LargeCopiesAreSparseAndPreserveFarSelectedCells) { - const auto result = test::analyze(std::string(Memory) + R"c( -void bad(char **d, char **s) { memcpy(d,s,1000000*sizeof *s); free(s[999999]); d[999999][0] = 1; } -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 1U); - EXPECT_EQ(test::incomplete(result).size(), 0U); -} - -TEST(ArrayOwnership, PartialCopiesExposeCoverageAndPreserveUnrelatedHistory) { - const auto result = test::analyze(std::string(Memory) + R"c( -void bad(char **a, char **b, char **unrelated) { - free(unrelated[0]); memcpy(a,b,sizeof *a-1); unrelated[0][0] = 1; -} -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 1U); - // RFC 0031 §4.2: the pointer the copy covers in part becomes a value - // marked `raw-cast`, which a later use reports as such; the copy itself - // leaves nothing unanalysed. - EXPECT_EQ(test::incomplete(result).size(), 0U); -} - -TEST(ArrayOwnership, SteppedAliasesAndAddressedElementsAgree) { - const auto result = test::analyze(R"c( -static void drop(char **a) { free(a[0]); } -void bad(char **a) { char **q = a+1; free(q[0]); a[1][0] = 1; a[0][0] = 1; } -void addressed(char **a) { drop(&a[1]); a[1][0] = 1; a[0][0] = 1; } -void local(char *p, char *q) { char *a[2] = {p,q}; char **b=a+1; free(b[0]); a[1][0] = 1; } -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 3U) - << ::testing::PrintToString(test::messages(result.diagnostics)); -} - -TEST(ArrayOwnership, UnknownWritesPreserveEarlierFreeEvidence) { - const auto result = test::analyze(R"c( -void bad(char **a, int i, char *p) { - char *old = a[0]; free(a[0]); a[i*i] = p; old[0] = 1; -} -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 1U); - // RFC 0017 represents the product selector; joining the old and new - // cells is complete even when their membership is undecided. - EXPECT_EQ(test::incomplete(result).size(), 0U); -} - -TEST(ArrayOwnership, PartialInitializationIncludesRecordFields) { - const auto result = test::analyze(R"c( -struct item { char *p; char *q; }; -void bad(char *p) { struct item a[2]; a[0].p = p; a[1].p[0] = 1; } -void nil(char *p) { struct item a[2] = {{p}}; a[0].q[0] = 1; a[1].p[0] = 1; } -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::UseOfUninitialized), 1U); - EXPECT_EQ(countId(result, core::diag::NullDereference), 2U); -} - -TEST(ArrayOwnership, GlobalCallbackElementsDoNotMixTargets) { - const auto result = test::analyze(R"c( -static void drop(void *p) { free(p); } -static void keep(void *p) { (void)p; } -static void (*table[2])(void *) = {drop, keep}; -void bad(char *p) { table[0](p); p[0] = 1; } -void clean(char *p) { table[1](p); p[0] = 1; } -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 1U); - EXPECT_EQ(test::incomplete(result).size(), 0U); -} - -TEST(ArrayOwnership, CopyingCallbackArraysRetainsEachTarget) { - const auto result = test::analyze(std::string(Memory) + R"c( -static void drop(void *p) { free(p); } -static void keep(void *p) { (void)p; } -void bad(char *p) { - void (*a[2])(void *) = {drop, keep}, (*b[2])(void *); - memcpy(b,a,sizeof a); b[0](p); p[0] = 1; -} -void clean(char *p) { - void (*a[2])(void *) = {drop, keep}, (*b[2])(void *); - memcpy(b,a,sizeof a); b[1](p); p[0] = 1; -} -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 1U); -} - -TEST(ArrayOwnership, ConditionalCopiesDoNotReplaceUntouchedContents) { - const auto result = test::analyze(std::string(Memory) + R"c( -static void maybe(char **d, char **s, size_t n, int c) { - if (c) memcpy(d,s,n*sizeof *s); -} -void bad(char **d, char **s) { free(d[0]); maybe(d,s,2,0); d[0][0] = 1; } -void clean(char **d, char **s) { free(d[0]); maybe(d,s,2,1); d[0][0] = 1; } -)c"); - ASSERT_TRUE(result.ast); - // RFC 0031 §6.6 *numeric contexts*: `maybe(d,s,2,0)` runs `maybe` with - // `c == 0`, which copies nothing. - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 1U) - << ::testing::PrintToString(test::messages(result.diagnostics)); -} - -TEST(ArrayOwnership, ReallocationKeepsChildrenButInvalidatesOldStorage) { - const auto result = test::analyze(R"c( -void bad(char *p) { - char **a = malloc(2*sizeof *a); if (!a) return; - a[0] = p; char **old = a; char **b = realloc(a,4*sizeof *a); - if (!b) { a[0][0] = 1; free(a); return; } - free(p); b[0][0] = 1; old[0] = 0; free(b); -} -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 1U) - << ::testing::PrintToString(test::messages(result.diagnostics)); - EXPECT_EQ(countId(result, core::diag::UseAfterMove), 1U); -} - -TEST(ArrayOwnership, CopyingOwnedPointersDoesNotCreateAdditionalAllocations) { - const auto result = test::analyze(std::string(Memory) + R"c( -void clean(void) { - char *a[2] = {malloc(4),malloc(4)}, *b[2]; - memcpy(b,a,sizeof a); free(b[0]); free(b[1]); -} -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::Leak), 0U) - << ::testing::PrintToString(test::messages(result.diagnostics)); - EXPECT_EQ(countId(result, core::diag::DoubleFree), 0U); -} - -TEST(ArrayOwnership, CompleteCleanupLoopsRetainEveryReleasedElement) { - const auto result = test::analyze(R"c( -void bad(char **a) { for (int i=0; i<3; ++i) free(a[i]); a[0][0]=1; a[2][0]=1; } -void clean(char **a) { for (int i=0; i<3; ++i) free(a[i]); a[3][0]=1; } -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 2U) - << ::testing::PrintToString(test::messages(result.diagnostics)); - EXPECT_EQ(countId(result, core::diag::DoubleFree), 0U); -} - -TEST(ArrayOwnership, CleanupHelpersPreserveLengthsAcrossCalls) { - const auto result = test::analyze(R"c( -static void drop(char **a, size_t n) { for (size_t i=0; ieffects) - if (effect.kind == core::PathEffect::Kind::Release && - test::throughElement(effect.path)) - EXPECT_TRUE(effect.may) << name; - } -} - -TEST(ArrayOwnership, TraversalsAlsoSupportSeparatelyDeclaredCounters) { - const auto result = test::analyze(R"c( -static void fill(char **a, size_t n) { - size_t i; for (i=0; irc; return p; } -static void unref(struct obj *p) { if (--p->rc==0) free(p); } -void bad(struct obj *p) { - struct obj *a[1]={p}, *b[1]; memcpy(b,a,sizeof b); unref(a[0]); unref(b[0]); -} -void clean(struct obj *p) { - struct obj *a[2]={p,ref(p)}, *b[2]; memcpy(b,a,sizeof b); - unref(b[0]); unref(b[1]); -} -)c"); - ASSERT_TRUE(result.ast); - // The object engine does not yet infer RFC 0010's reference-count - // functions (`ref`/`unref` here), so `unref` is a possible release of its - // argument: the second `unref` in `bad` (the same share, copied) is a - // possible use after free rather than a release of the share twice, and - // `clean` gets the same warning (test/cases/KNOWN-DIFFERENCES.md, *Unit - // tests*). The copy keeps the pointer's identity either way. - EXPECT_EQ(countId(result, core::diag::DoubleFree) + - countId(result, core::diag::UseAfterFree), - 2U) - << ::testing::PrintToString(test::messages(result.diagnostics)); - EXPECT_TRUE(temporalNotProven(result, "bad", "unref(b[0])")); - EXPECT_EQ(countId(result, core::diag::Leak), 0U); -} - -TEST(ArrayOwnership, - SymbolicOverlapFreezesDestinationsBeforeTheyBecomeSources) { - const auto result = test::analyze(std::string(Memory) + R"c( -void bad(char **a, size_t n) { - if (n<3 || n>(size_t)-1/sizeof *a) return; char *old=a[1]; - memmove(a+1,a,n*sizeof *a); a[1][0]=1; free(old); a[2][0]=1; -} -void clean(char **a, size_t n) { - if (n<3 || n>(size_t)-1/sizeof *a) return; char *old=a[1]; - memmove(a+1,a,n*sizeof *a); free(old); a[1][0]=1; -} -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 1U) - << ::testing::PrintToString(test::messages(result.diagnostics)); -} - -TEST(ArrayOwnership, TraversalsStillCheckContainerStorage) { - const auto result = test::analyze(R"c( -void null_fill(void) { char **a=0; for(int i=0;i<2;++i) a[i]=0; } -void null_drop(void) { char **a=0; for(int i=0;i<2;++i) free(a[i]); } -void zero(void) { char **a=0; for(int i=0;i<0;++i) free(a[i]); } -void bounds(void) { char *a[2]={0}; for(int i=0;i<3;++i) a[i]=0; } -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::NullDereference), 2U) - << ::testing::PrintToString(test::messages(result.diagnostics)); - // RFC 0030 §3.3 (RFC 0031 §5.2): `a[i]` is out of bounds for `i == 2` - // only, so it is a checked facet (a trap on the last iteration), not a - // definite `out-of-bounds`, which needs every value to be. - EXPECT_EQ(countId(result, core::diag::OutOfBounds), 0U); - unsigned checked = 0; - for (const core::UnitLedger &unit : result.ledger.units) - for (const core::FunctionLedger &function : unit.functions) - for (const core::Site &site : function.sites) - if (site.location.line == 5 && site.kind == core::SiteKind::Index) - if (const core::FacetRecord *record = - site.facet(core::Facet::Spatial)) { - EXPECT_EQ(record->decision.unresolved, - core::UnresolvedReason::Undecided); - ++checked; - } - EXPECT_EQ(checked, 1U); -} - -TEST(ArrayOwnership, RecordIndicesFreezeTheirChildStateBeforeReassignment) { - const auto result = test::analyze(R"c( -struct box { char *p; }; -void bad(struct box *a, int i) { int old=i; free(a[i].p); i=9; a[old].p[0]=1; } -void clean(struct box *a, int i) { free(a[i].p); ++i; a[i].p[0]=1; } -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 1U) - << ::testing::PrintToString(test::messages(result.diagnostics)); -} - -TEST(ArrayOwnership, ReturnedFilledContainersCarryInitializationAndResources) { - const auto result = test::analyze(R"c( -static char **make(int n) { - char **a=malloc(n*sizeof *a); if (!a) return 0; - for(int i=0;istores) - if (store.dest == dest) - unknown = - store.may && store.value.kind == core::ValueDesc::Kind::Unknown; - EXPECT_TRUE(unknown) << name; - } -} - -TEST(ArrayOwnership, ShrinkingAContainerReportsOnlyUnreachableOwnedChildren) { - const auto result = test::analyze(R"c( -void bad(void) { - char **a=malloc(2*sizeof *a); if(!a) return; - a[0]=malloc(4); a[1]=malloc(4); - char **b=realloc(a,sizeof *a); - if(!b) { free(a[0]); free(a[1]); free(a); return; } - free(b[0]); free(b); -} -void clean(void) { - char **a=malloc(2*sizeof *a); if(!a) return; - a[0]=malloc(4); a[1]=malloc(4); char *saved=a[1]; - char **b=realloc(a,sizeof *a); - if(!b) { free(a[0]); free(saved); free(a); return; } - free(b[0]); free(saved); free(b); -} -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::Leak), 1U) - << ::testing::PrintToString(test::messages(result.diagnostics)); -} - -TEST(ArrayOwnership, ChainedPointersIntoLocalArraysApplyTheirOffsetOnce) { - const auto result = test::analyze(R"c( -void bad(char *p, char *q, char *r) { - char *a[3]={p,q,r}; char **b=a+1; char **c=b+1; - free(c[0]); a[2][0]=1; -} -void dereference(char *p, char *q, char *r) { - char *a[3]={p,q,r}; char **b=a+1; char **c=b+1; - free(*c); a[2][0]=1; -} -void clean(char *p, char *q, char *r) { - char *a[3]={p,q,r}; char **b=a+1; char **c=b+1; - free(*c); a[0][0]=1; a[1][0]=1; -} -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 2U) - << ::testing::PrintToString(test::messages(result.diagnostics)); - EXPECT_EQ(test::incomplete(result).size(), 0U); -} - -TEST(ArrayOwnership, RangeFillsRespectTheExistingCellBudgetAndHistory) { - std::string code = "void bounded(char **a) { free(a[40]);"; - for (unsigned i = 41; i < 72; ++i) - code += "use(a[" + std::to_string(i) + "]);"; - code += "for (int i=0; i<32; ++i) a[i]=0; a[40][0]=1; }"; - const auto result = test::analyze(code); - ASSERT_TRUE(result.ast); - // RFC 0031 §4.9: the fill is a range `[0, 32)` beside the constant cells, - // within the object's limits, so nothing is left unanalysed. - EXPECT_EQ(test::incomplete(result).size(), 0U); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 1U); -} - -} // namespace weavec::analysis diff --git a/unittests/Analysis/AttributeReaderTest.cpp b/unittests/Analysis/AttributeReaderTest.cpp deleted file mode 100644 index e22cfeea..00000000 --- a/unittests/Analysis/AttributeReaderTest.cpp +++ /dev/null @@ -1,368 +0,0 @@ -//===- AttributeReaderTest.cpp - Tests for AttributeReader and KindTable --===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// RFC 0030 §7.2: declared kinds, their precedence, and the level-4 marking -// of system-header attributes. -// -//===----------------------------------------------------------------------===// - -#include "weavec/Analysis/AttributeReader.h" - -#include "SiteTestUtils.h" -#include "weavec/Analysis/KindTable.h" -#include "weavec/Core/Diagnostic.h" - -#include - -#include -#include -#include - -namespace weavec::analysis { - -using test::collectUnit; - -/// ` /` of an entry. -static std::string spell(const KindEntry *entry) { - if (entry == nullptr) - return "none"; - const auto level = [](const std::optional &which) { - return which ? std::string(toString(*which)) : std::string("-"); - }; - return entry->kind.toString() + " " + level(entry->shapeLevel) + "/" + - level(entry->nullabilityLevel); -} - -static std::string param(const test::CollectedUnit &unit, - llvm::StringRef function, unsigned index) { - const clang::FunctionDecl *decl = unit.function(function); - return decl == nullptr ? "no function" - : spell(unit.kinds.param(*decl, index)); -} - -namespace { - -TEST(AttributeReader, WeaveCAnnotations) { - const auto unit = collectUnit(R"c( -void f(int *SIZED_BY(n) p, int n, char *SIZED_BY(n) c, void *SIZED_BY(n) v, - int *NONNULL q, int *NULLABLE r, int *SIZED_BY(missing) bad); -NONNULL char *g(void); -void use(void) { f(0, 0, 0, 0, 0, 0, 0); (void)g(); } -)c"); - EXPECT_EQ(param(unit, "f", 0), - "counted(param 1 scale 1 plus 0) nullable annotation/-"); - EXPECT_EQ(param(unit, "f", 2), - "sized(param 1 scale 1 plus 0) nullable annotation/-"); - EXPECT_EQ(param(unit, "f", 3), - "sized(param 1 scale 1 plus 0) nullable annotation/-"); - EXPECT_EQ(param(unit, "f", 4), "unknown nonnull -/annotation"); - EXPECT_EQ(param(unit, "f", 5), "unknown nullable -/annotation"); - EXPECT_EQ(param(unit, "f", 6), "none"); - EXPECT_EQ(spell(unit.kinds.result(*unit.function("g"))), - "unknown nonnull -/annotation"); - ASSERT_EQ(unit.kinds.problems().size(), 1U); - EXPECT_EQ(unit.kinds.problems().front().message, - "'missing' in WEAVEC_SIZED_BY does not name a parameter or field"); - const KindEntry *counted = unit.kinds.param(*unit.function("f"), 0); - ASSERT_NE(counted, nullptr); - EXPECT_EQ(counted->extentClass, core::ExtentClass::Declared); - EXPECT_TRUE(counted->hasShape()); - EXPECT_EQ(counted->kind.source, core::KindSource::Declared); -} - -TEST(AttributeReader, EcosystemAttributes) { - const auto unit = collectUnit(R"c( -void all(int *a, int *b) __attribute__((nonnull)); -void some(int *a, int *b) __attribute__((nonnull(2))); -void param(int *a __attribute__((nonnull)), int *_Nonnull b, int *_Nullable c); -void arrays(int n, int s[static 4], int v[n], int w[n + 1], int k[4], - int t[static n]); -int *ret(void) __attribute__((returns_nonnull)); -void *alloc(unsigned long n, unsigned long size) __attribute__((alloc_size(1, 2))); -void use(void) { all(0, 0); some(0, 0); param(0, 0, 0); arrays(0, 0, 0, 0, 0, 0); (void)ret(); (void)alloc(1, 1); } -)c"); - EXPECT_EQ(param(unit, "all", 0), "unknown nonnull -/ecosystem"); - EXPECT_EQ(param(unit, "all", 1), "unknown nonnull -/ecosystem"); - EXPECT_EQ(param(unit, "some", 0), "none"); - EXPECT_EQ(param(unit, "some", 1), "unknown nonnull -/ecosystem"); - EXPECT_EQ(param(unit, "param", 0), "unknown nonnull -/ecosystem"); - EXPECT_EQ(param(unit, "param", 1), "unknown nonnull -/ecosystem"); - EXPECT_EQ(param(unit, "param", 2), "unknown nullable -/ecosystem"); - EXPECT_EQ(param(unit, "arrays", 1), "counted(4) nonnull ecosystem/ecosystem"); - EXPECT_EQ(param(unit, "arrays", 2), - "counted(param 0 scale 1 plus 0) nullable ecosystem/-"); - EXPECT_EQ(param(unit, "arrays", 3), - "counted(param 0 scale 1 plus 1) nullable ecosystem/-"); - // A constant bound without `static` is no requirement (§7.2). - EXPECT_EQ(param(unit, "arrays", 4), "none"); - EXPECT_EQ(param(unit, "arrays", 5), - "counted(param 0 scale 1 plus 0) nonnull ecosystem/ecosystem"); - EXPECT_EQ(spell(unit.kinds.result(*unit.function("ret"))), - "unknown nonnull -/ecosystem"); - const KindEntry *allocated = unit.kinds.result(*unit.function("alloc")); - EXPECT_EQ(spell(allocated), - "sized(param 0 scale 1 plus 0) nullable ecosystem/-"); - ASSERT_NE(allocated, nullptr); - EXPECT_EQ(allocated->extentFactor, std::optional(1)); -} - -TEST(AttributeReader, Fields) { - const auto unit = collectUnit(R"c( -struct s { - int n; - int *__attribute__((counted_by(n))) counted; - char *__attribute__((sized_by_or_null(n))) sized; - int *SIZED_BY(n) annotated; - char *SIZED_BY(n) bytes; - int *_Nonnull never; - int tail[] __attribute__((counted_by(n))); -}; -)c"); - const clang::RecordDecl *record = nullptr; - for (const clang::Decl *decl : - unit.context().getTranslationUnitDecl()->decls()) - if (const auto *tag = llvm::dyn_cast(decl); - tag != nullptr && tag->getName() == "s") - record = tag; - ASSERT_NE(record, nullptr); - std::vector kinds; - for (const clang::FieldDecl *field : record->fields()) - kinds.push_back(field->getNameAsString() + ": " + - spell(unit.kinds.field(*field))); - EXPECT_EQ( - kinds, - (std::vector{ - "n: none", - "counted: counted(.n scale 1 plus 0) nonnull ecosystem/ecosystem", - "sized: sized(.n scale 1 plus 0) nullable ecosystem/ecosystem", - "annotated: counted(.n scale 1 plus 0) nullable annotation/-", - "bytes: sized(.n scale 1 plus 0) nullable annotation/-", - "never: unknown nonnull -/ecosystem", - "tail: counted(.n scale 1 plus 0) nullable ecosystem/-"})); -} - -// §7.2 precedence: WEAVEC_* annotations, then ecosystem attributes; shape -// and nullability separately; a conflict within a level uses the weaker -// kind and is recorded. -TEST(AttributeReader, Precedence) { - const auto unit = collectUnit(R"c( -void a(int *NULLABLE p __attribute__((nonnull))); -void b(int n, int p[static 4] SIZED_BY(n)); -void c(int *SIZED_BY(n) p, int n, int m); -void c(int *SIZED_BY(m) p, int n, int m); -void d(int *NONNULL p); -void d(int *NULLABLE p); -void use(void) { a(0); b(0, 0); c(0, 0, 0); d(0); } -)c"); - EXPECT_EQ(param(unit, "a", 0), "unknown nullable -/annotation"); - EXPECT_EQ(param(unit, "b", 1), - "counted(param 0 scale 1 plus 0) nonnull annotation/ecosystem"); - EXPECT_EQ(param(unit, "c", 0), "unknown nullable annotation/-"); - EXPECT_EQ(param(unit, "d", 0), "unknown nullable -/annotation"); - EXPECT_EQ(unit.kinds.problems().size(), 2U); -} - -// §7.2 level 4: attributes in system headers mark their kinds; a -// LibrarySpec row outranks them. -TEST(AttributeReader, SystemHeaders) { - const auto unit = collectUnit(R"c( -#include -void use(char *d, const char *s) { sysfn(d); (void)memcpy(d, s, 1); } -)c", - R"c( -typedef unsigned long size_t; -void sysfn(char *p) __attribute__((nonnull)); -void *memcpy(void *, const void *, size_t) __attribute__((nonnull)); -)c", - /*prelude=*/false); - const KindEntry *system = unit.kinds.param(*unit.function("sysfn"), 0); - EXPECT_EQ(spell(system), "unknown nonnull -/system-header"); - ASSERT_NE(system, nullptr); - EXPECT_TRUE(system->nullabilityFromSystemHeader()); - EXPECT_TRUE(unit.kinds.governedByLibrary(*unit.function("memcpy"))); - EXPECT_EQ(param(unit, "memcpy", 0), "none"); - // The call site's null facet rests on the level-4 attribute alone. - const SiteIndex::FunctionSites *function = - unit.sites.function(*unit.function("use")); - ASSERT_NE(function, nullptr); - ASSERT_FALSE(function->sites.empty()); - EXPECT_EQ(function->sites.front().kind, core::SiteKind::Call); - EXPECT_TRUE(function->sites.front().nullSystemApi); -} - -/// The RFC 0030 macros, spelled as `weavec.h` spells them. -constexpr const char *KindMacros = R"c( -#define COUNTED_BY(n) __attribute__((annotate("weavec.counted_by." #n))) -#define ENDED_BY(q) __attribute__((annotate("weavec.ended_by." #q))) -#define STRING __attribute__((annotate("weavec.string"))) -)c"; - -// §7.2: the new macros resolve by name to any sibling, in any position. -TEST(AttributeReader, KindMacros) { - const auto unit = collectUnit(std::string(KindMacros) + R"c( -struct s { - int *COUNTED_BY(n) items; - void *COUNTED_BY(n) bytes; - const char *STRING name; - int *ENDED_BY(stop) start; - int *stop; - int n; - int *COUNTED_BY(stop) wrong; - int *ENDED_BY(n) wrong_end; -}; -void f(int *COUNTED_BY(n) p, const char *STRING s, int *ENDED_BY(e) b, - int *e, int n); -STRING const char *g(void); -void h(int *COUNTED_BY(nope) p, int COUNTED_BY(n) x, int n); -void use(void) { f(0, 0, 0, 0, 0); (void)g(); h(0, 0, 0); } -)c"); - EXPECT_EQ(param(unit, "f", 0), - "counted(param 4 scale 1 plus 0) nullable annotation/-"); - EXPECT_EQ(param(unit, "f", 1), "nul-terminated nullable annotation/-"); - EXPECT_EQ(param(unit, "f", 2), "ended-by(param 3) nullable annotation/-"); - EXPECT_EQ(spell(unit.kinds.result(*unit.function("g"))), - "nul-terminated nullable annotation/-"); - EXPECT_EQ(param(unit, "h", 0), "none"); - const clang::RecordDecl *record = nullptr; - for (const clang::Decl *decl : - unit.context().getTranslationUnitDecl()->decls()) - if (const auto *tag = llvm::dyn_cast(decl); - tag != nullptr && tag->getName() == "s") - record = tag; - ASSERT_NE(record, nullptr); - std::vector fields; - for (const clang::FieldDecl *field : record->fields()) - fields.push_back(field->getNameAsString() + ": " + - spell(unit.kinds.field(*field))); - EXPECT_EQ(fields, - (std::vector{ - "items: counted(.n scale 1 plus 0) nullable annotation/-", - "bytes: sized(.n scale 1 plus 0) nullable annotation/-", - "name: nul-terminated nullable annotation/-", - "start: ended-by(.stop) nullable annotation/-", "stop: none", - "n: none", "wrong: none", "wrong_end: none"})); - std::vector problems; - for (const KindProblem &problem : unit.kinds.problems()) - problems.push_back(problem.message); - EXPECT_EQ(problems, - (std::vector{ - "'stop' in WEAVEC_COUNTED_BY is not an integer parameter or " - "field", - "'n' in WEAVEC_ENDED_BY is not a pointer parameter or field", - "'nope' in WEAVEC_COUNTED_BY does not name a parameter or " - "field", - "'x' is declared WEAVEC_COUNTED_BY(n) but is not a pointer"})); - const KindEntry *counted = unit.kinds.param(*unit.function("f"), 0); - ASSERT_NE(counted, nullptr); - EXPECT_TRUE(counted->hasDeclaredShape()); -} - -// §7.2: the ownership attributes become the function's contract; a constant -// array bound without `static` is a suggestion; an extent on a non-pointer -// is a problem. -TEST(AttributeReader, ContractsSuggestionsAndProblems) { - const auto unit = collectUnit(std::string(KindMacros) + R"c( -unsigned long n; -int COUNTED_BY(n) flag; -void *pool_get(unsigned long n) __attribute__((ownership_returns(pool))); -void pool_put(int t, void *p) __attribute__((ownership_takes(pool, 2))); -void pool_keep(void *p) __attribute__((ownership_holds(pool, 1))); -void *fresh(unsigned long n) __attribute__((malloc)); -void loose(int k[4]) { (void)k; } -void use(void) { pool_put(0, pool_get(1)); pool_keep(fresh(1)); } -)c"); - ASSERT_EQ(unit.kinds.problems().size(), 1U); - EXPECT_EQ(unit.kinds.problems().front().message, - "'flag' is declared WEAVEC_COUNTED_BY(n) but is not a pointer"); - const OwnershipContract *get = - unit.kinds.ownership(*unit.function("pool_get")); - ASSERT_NE(get, nullptr); - EXPECT_EQ(get->freshResult, std::optional("pool")); - const OwnershipContract *put = - unit.kinds.ownership(*unit.function("pool_put")); - ASSERT_NE(put, nullptr); - ASSERT_EQ(put->arguments.size(), 1U); - EXPECT_EQ(put->arguments.front(), - (OwnershipContract::Argument{ - .index = 1, .family = "pool", .retains = false})); - const OwnershipContract *keep = - unit.kinds.ownership(*unit.function("pool_keep")); - ASSERT_NE(keep, nullptr); - EXPECT_TRUE(keep->arguments.front().retains); - const OwnershipContract *heap = unit.kinds.ownership(*unit.function("fresh")); - ASSERT_NE(heap, nullptr); - EXPECT_EQ(heap->freshResult, std::optional("free")); - EXPECT_EQ(heap->level, KindLevel::Ecosystem); - ASSERT_EQ(unit.kinds.suggestions().size(), 1U); - EXPECT_EQ(unit.kinds.suggestions().front().insert, "static "); - EXPECT_EQ(param(unit, "loose", 0), "none"); - // The problems become `invalid-annotation` warnings. - const auto diagnostics = - kindProblemDiagnostics(unit.kinds, unit.context().getSourceManager()); - ASSERT_EQ(diagnostics.size(), 1U); - EXPECT_EQ(diagnostics.front().id, core::diag::InvalidAnnotation); - EXPECT_EQ(diagnostics.front().severity, core::Severity::Warning); -} - -// The table's inferred entries never make required positions. -TEST(KindTable, DeclaredShapesAndRequirements) { - KindEntry entry; - entry.kind = core::PointerKind::single(core::Nullability::Nonnull, - core::KindSource::Default); - EXPECT_TRUE(entry.hasShape()); - EXPECT_FALSE(entry.hasDeclaredShape()); - EXPECT_TRUE(entry.isNonnull()); - EXPECT_FALSE(entry.declaresNonnull()); - entry.shapeLevel = KindLevel::Annotation; - entry.nullabilityLevel = KindLevel::Ecosystem; - EXPECT_TRUE(entry.hasDeclaredShape()); - EXPECT_TRUE(entry.declaresNonnull()); - EXPECT_FALSE(entry.hasEnforcedRequirement()); - MustAccessRequirement requirement{ - .kind = core::PointerKind::counted( - core::ExtentTerm::of(core::ExtentPath::ofParam(1), 1, 1), - core::Nullability::Nonnull, core::KindSource::Inferred), - .guard = RequirementGuard{.lhs = core::ExtentTerm::constant(0), - .relation = RequirementGuard::Relation::Less, - .rhs = core::ExtentTerm::of( - core::ExtentPath::ofParam(1))}, - .rule = MustAccessRule::R2, - .access = nullptr}; - EXPECT_EQ(requirement.toString(), - "0 < param 1 scale 1 plus 0 -> counted(param 1 scale 1 plus 1) " - "nonnull (R2)"); - entry.mustAccess.push_back(requirement); - entry.enforcement = RequirementEnforcement::CallerContract; - EXPECT_FALSE(entry.hasEnforcedRequirement()); - entry.enforcement = RequirementEnforcement::CallSites; - EXPECT_TRUE(entry.hasEnforcedRequirement()); - EXPECT_EQ(toString(RequirementEnforcement::CallerContract), - "caller-contract"); - EXPECT_EQ(toString(MustAccessRule::R4), "R4"); -} - -TEST(KindTable, EntryHelpers) { - KindEntry entry; - EXPECT_FALSE(entry.hasShape()); - EXPECT_FALSE(entry.isNonnull()); - entry.kind = core::PointerKind::counted(core::ExtentTerm::constant(4), - core::Nullability::Nonnull, - core::KindSource::Declared); - entry.extentClass = core::extentClassOf(entry.kind); - entry.shapeLevel = KindLevel::SystemHeader; - EXPECT_TRUE(entry.hasShape()); - EXPECT_TRUE(entry.isNonnull()); - EXPECT_TRUE(entry.shapeFromSystemHeader()); - entry.shapeLevel = KindLevel::Ecosystem; - entry.kind = core::PointerKind::single(); - entry.extentClass = core::extentClassOf(entry.kind); - EXPECT_EQ(toString(KindLevel::LibrarySpec), "library-spec"); -} - -} // namespace -} // namespace weavec::analysis diff --git a/unittests/Analysis/CMakeLists.txt b/unittests/Analysis/CMakeLists.txt deleted file mode 100644 index 5f754ecb..00000000 --- a/unittests/Analysis/CMakeLists.txt +++ /dev/null @@ -1,26 +0,0 @@ -weavec_add_unittest( - WeaveCAnalysisTests - SOURCES AnnotationsTest.cpp - ArrayOwnershipTest.cpp - DynamicExtentsTest.cpp - HeapStateTest.cpp - IntegerSemanticsTest.cpp - NumericInputsTest.cpp - PointerValidityTest.cpp - PointerIdentityTest.cpp - ProgramDatabaseTest.cpp - ResourceLifecycleTest.cpp - SharedOwnershipTest.cpp - SpatialSafetyTest.cpp - StringsAndFieldsTest.cpp - ValueConditionalTest.cpp - LoopRequirementsTest.cpp - GuardCompletenessTest.cpp - AttributeReaderTest.cpp - KindInferenceTest.cpp - SlotCollectorTest.cpp - SiteCollectorTest.cpp - LedgerAdapterTest.cpp - EngineDecisionsTest.cpp - SoundDefaultsTest.cpp - DEPS weavec::Analysis weavec::Frontend) diff --git a/unittests/Analysis/DynamicExtentsTest.cpp b/unittests/Analysis/DynamicExtentsTest.cpp deleted file mode 100644 index a2ab3d7a..00000000 --- a/unittests/Analysis/DynamicExtentsTest.cpp +++ /dev/null @@ -1,403 +0,0 @@ -//===- DynamicExtentsTest.cpp - VLA and flexible tails (RFC 0017) ---------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// - -#include "TestUtils.h" - -#include "llvm/Support/raw_ostream.h" - -#include "gtest/gtest.h" - -using namespace weavec; -using namespace weavec::test; - -static unsigned countId(const AnalysisResult &result, std::string_view id) { - return static_cast( - std::ranges::count_if(result.diagnostics.diagnostics(), - [id](const core::Diagnostic &diagnostic) { - return diagnostic.id == id; - })); -} - -TEST(DynamicExtents, FixedOuterDimensionRetainsItsOwnBound) { - const auto result = analyze(R"c( - void test(void) { - unsigned n = 3; - int a[2][n]; - a[1][2] = 1; - a[2][0] = 1; - a[0][3] = 1; - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::OutOfBounds), 2U) - << ::testing::PrintToString(messages(result.diagnostics)); -} - -TEST(DynamicExtents, FixedInnerDimensionRetainsItsOwnBound) { - const auto result = analyze(R"c( - void test(void) { - unsigned n = 3; - int a[n][2]; - a[2][1] = 1; - a[3][0] = 1; - a[0][2] = 1; - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::OutOfBounds), 2U) - << ::testing::PrintToString(messages(result.diagnostics)); -} - -TEST(DynamicExtents, RepresentableMultidimensionalAccessIsProven) { - const auto result = analyze(R"c( - void test(void) { - unsigned n = 2, m = 3; - int a[n][m]; - a[1][2] = 1; - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::OutOfBounds), 0U); - EXPECT_EQ(facetCounts(result, core::Facet::Spatial).substr(0, 42), - "spatial: proven=2 violation=0 unresolved=0"); -} - -TEST(DynamicExtents, OverflowingElementByteProductCannotBeProven) { - const auto result = analyze(R"c( - void test(void) { - size_t n = (size_t)1 << (sizeof(size_t) * 8 - 2); - int a[n]; - a[0] = 1; - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::OutOfBounds), 0U); - // RFC 0030 §15 item 3: the gap is the summary's (a declaration is no - // site), and the access is not proven. - EXPECT_EQ(facetCounts(result, core::Facet::Spatial).substr(0, 42), - "spatial: proven=0 violation=0 unresolved=1"); -} - -TEST(DynamicExtents, OverflowingFixedOuterByteProductCannotBeProven) { - const auto result = analyze(R"c( - void test(void) { - size_t n = (size_t)1 << (sizeof(size_t) * 8 - 2); - char a[4][n]; - a[0][0] = 1; - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::OutOfBounds), 0U); - // RFC 0030 §15 item 3: the gap is the summary's (a declaration is no - // site), and the access is not proven. - EXPECT_EQ(facetCounts(result, core::Facet::Spatial).substr(0, 42), - "spatial: proven=0 violation=0 unresolved=2"); -} - -TEST(DynamicExtents, ByteOverflowPreservesIndependentDimensionViolations) { - const auto result = analyze(R"c( - void test(void) { - size_t n = (size_t)1 << (sizeof(size_t) * 8 - 2); - char a[4][n]; - a[4][0] = 1; - a[0][n] = 1; - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::OutOfBounds), 2U) - << ::testing::PrintToString(messages(result.diagnostics)); -} - -TEST(DynamicExtents, PossiblyOverflowingByteProductRemainsUnresolved) { - const auto result = analyze(R"c( - void test(size_t n) { - if (!n) return; - int a[n]; - a[0] = 1; - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::InvalidIntegerOperation), 0U); - EXPECT_EQ(countId(result, core::diag::OutOfBounds), 0U); - EXPECT_EQ(facetCounts(result, core::Facet::Spatial).substr(0, 42), - "spatial: proven=0 violation=0 unresolved=1"); -} - -TEST(DynamicExtents, NonpositiveDimensionsDoNotInventStorage) { - for (const auto *bound : {"0", "-1", "(unsigned char)256"}) { - SCOPED_TRACE(bound); - const auto result = - analyze("void test(void) { int n = " + std::string(bound) + - "; int a[n]; a[0] = 1; }"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::InvalidIntegerOperation), 1U); - EXPECT_EQ(countId(result, core::diag::OutOfBounds), 0U); - EXPECT_EQ(facetCounts(result, core::Facet::Spatial).substr(0, 42), - "spatial: proven=0 violation=0 unresolved=1"); - } -} - -TEST(DynamicExtents, InvalidInnerDimensionKeepsTheFixedOuterViolation) { - const auto result = analyze(R"c( - void test(void) { - int n = 0; - int a[2][n]; - a[2][0] = 1; - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::InvalidIntegerOperation), 1U); - EXPECT_EQ(countId(result, core::diag::OutOfBounds), 1U); -} - -TEST(DynamicExtents, TypedefSizeIsCapturedBeforeItsVariablesAreDeclared) { - const auto result = analyze(R"c( - void test(void) { - unsigned n = 2; - typedef int row[n]; - n = 5; - row a[3]; - row *p = &a[0]; - n = 7; - int *dead = malloc(sizeof *dead); if (!dead) return; free(dead); - if (sizeof(row) != 2 * sizeof(int) || - sizeof a != 6 * sizeof(int) || - sizeof *p != 2 * sizeof(int)) *dead = 1; - a[2][1] = 1; - a[2][2] = 1; - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 0U) - << ::testing::PrintToString(messages(result.diagnostics)); - EXPECT_EQ(countId(result, core::diag::OutOfBounds), 1U); -} - -TEST(DynamicExtents, TypedefUsesAndShadowingHaveSeparateSnapshots) { - const auto result = analyze(R"c( - void test(void) { - unsigned n = 2; - typedef int row[n]; - row first; - n = 5; - row second; - first[1] = 1; first[2] = 1; - second[1] = 1; second[2] = 1; - { - typedef int row[n]; - n = 9; - row inner; - inner[4] = 1; inner[5] = 1; - } - row last; - last[1] = 1; last[2] = 1; - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::OutOfBounds), 4U) - << ::testing::PrintToString(messages(result.diagnostics)); -} - -TEST(DynamicExtents, PointerToVlaCapturesSizeWithoutOwningAnArray) { - const auto result = analyze(R"c( - void test(void) { - unsigned n = 3; - int (*p)[n]; - n = 7; - int *dead = malloc(sizeof *dead); if (!dead) return; free(dead); - if (sizeof *p != 3 * sizeof(int) || sizeof p != sizeof(void *)) - *dead = 1; - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 0U) - << ::testing::PrintToString(messages(result.diagnostics)); - EXPECT_EQ(countId(result, core::diag::UseOfUninitialized), 0U); -} - -TEST(DynamicExtents, PointerToVlaRetainsSizeAndLifetimeAfterRelease) { - const auto result = analyze(R"c( - void test(void) { - unsigned n = 3; - int (*p)[n] = malloc(3 * sizeof(int)); if (!p) return; - n = 7; - (*p)[2] = 1; - (*p)[3] = 1; - free(p); - int *dead = malloc(sizeof *dead); if (!dead) return; free(dead); - if (sizeof *p != 3 * sizeof(int)) *dead = 1; - (*p)[0] = 1; - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::OutOfBounds), 1U); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 1U) - << ::testing::PrintToString(messages(result.diagnostics)); -} - -TEST(DynamicExtents, PointerRowTypeDoesNotProveTheBackingAllocationFits) { - const auto result = analyze(R"c( - void test(void) { - unsigned n = 3; - int (*p)[n] = malloc(1); if (!p) return; - p[0][0] = 1; - free(p); - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(facetCounts(result, core::Facet::Spatial).substr(0, 17), - "spatial: proven=0"); -} - -TEST(DynamicExtents, LoopRedeclarationCannotReuseAnEarlierPositiveDimension) { - const auto result = analyze(R"c( - void test(void) { - for (int n = 2; n >= 0; --n) { - int a[n]; - a[0] = 1; - } - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(facetCounts(result, core::Facet::Spatial).substr(0, 42), - "spatial: proven=0 violation=0 unresolved=1"); -} - -TEST(DynamicExtents, TypedefOutsideALoopDoesNotResizeOnEachIteration) { - const auto result = analyze(R"c( - void test(void) { - unsigned n = 2; - typedef int row[n]; - for (n = 3; n < 6; ++n) { - row a; - a[1] = 1; - a[2] = 1; - } - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::OutOfBounds), 1U) - << ::testing::PrintToString(messages(result.diagnostics)); -} - -TEST(DynamicExtents, SideEffectingDimensionsAreCapturedOnce) { - const auto result = analyze(R"c( - void test(void) { - unsigned n = 2; - int a[n++]; - a[0] = 1; - } - )c"); - ASSERT_TRUE(result.ast); - // RFC 0031: the dimension is the value `n++` had when the declaration - // ran (2), captured once; the access is inside it. - EXPECT_EQ(facetCounts(result, core::Facet::Spatial).substr(0, 42), - "spatial: proven=1 violation=0 unresolved=0"); -} - -TEST(DynamicExtents, SideEffectingDimensionsDoNotFabricateSizeof) { - const auto result = analyze(R"c( - void test(void) { - unsigned n = 2; - int a[n++]; - int *dead = malloc(sizeof *dead); if (!dead) return; free(dead); - if (sizeof a == 2 * sizeof(int)) *dead = 1; - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 1U) - << ::testing::PrintToString(messages(result.diagnostics)); -} - -TEST(DynamicExtents, PointerInitializerCannotReplaceItsEarlierDimensionValue) { - const auto result = analyze(R"c( - void test(void) { - unsigned n = 2; - int (*p)[n] = (n = 3, malloc(3 * sizeof(int))); - int *dead = malloc(sizeof *dead); if (!dead) { free(p); return; } - free(dead); - if (sizeof *p == 2 * sizeof(int)) *dead = 1; - free(p); - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 1U) - << ::testing::PrintToString(messages(result.diagnostics)); -} - -TEST(DynamicExtents, FlexibleTailUsesFieldOffsetIncludingRecordPadding) { - const auto result = analyze(R"c( - struct padded { double alignment; char flag; int data[]; }; - _Static_assert(sizeof(struct padded) == - __builtin_offsetof(struct padded, data) + sizeof(int), - "test requires trailing record padding"); - void test(void) { - struct padded *p = malloc(sizeof *p); if (!p) return; - p->data[0] = 1; - p->data[1] = 1; - free(p); - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::OutOfBounds), 1U) - << ::testing::PrintToString(messages(result.diagnostics)); -} - -TEST(DynamicExtents, FlexibleTailRequiresTheWholeNonByteElement) { - const auto result = analyze(R"c( - struct block { char tag; long long data[]; }; - void test(void) { - struct block *p = malloc(__builtin_offsetof(struct block, data) + - sizeof(long long) + 1); - if (!p) return; - long long *tail = p->data; - tail[0] = 1; - tail[1] = 1; - free(p); - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::OutOfBounds), 1U) - << ::testing::PrintToString(messages(result.diagnostics)); -} - -TEST(DynamicExtents, FlexibleTailBeforeTheAllocationEndRetainsLifetime) { - const auto result = analyze(R"c( - struct block { char tag; long long data[]; }; - void test(void) { - struct block *p = malloc(__builtin_offsetof(struct block, data) - 1); - if (!p) return; - long long *tail = p->data; - tail[0] = 1; - free(p); - tail[0] = 1; - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_GT(countId(result, core::diag::OutOfBounds), 0U); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 1U) - << ::testing::PrintToString(messages(result.diagnostics)); -} - -TEST(DynamicExtents, FlexibleTailCannotUseAnUncheckedSiblingCountAsStorage) { - const auto result = analyze(R"c( - struct block { unsigned count; int data[]; }; - void test(struct block *p) { - if (!p) return; - p->count = 100; - p->data[0] = 1; - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::OutOfBounds), 0U); - // `p->count` is proven by `p`'s Single default (RFC 0030 §7.3), which - // ends where the flexible member starts: `p->data[0]` falls past that - // lower bound, never proven and never a violation. - EXPECT_EQ(facetCounts(result, core::Facet::Spatial).substr(0, 42), - "spatial: proven=1 violation=0 unresolved=1"); -} diff --git a/unittests/Analysis/EngineDecisionsTest.cpp b/unittests/Analysis/EngineDecisionsTest.cpp deleted file mode 100644 index 3c6efb76..00000000 --- a/unittests/Analysis/EngineDecisionsTest.cpp +++ /dev/null @@ -1,633 +0,0 @@ -//===- EngineDecisionsTest.cpp - The engine's decisions (RFC 0030) --------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// RFC 0030 §15 items 3, 4, 12 and 15 (stage S3-B2): what the engine decides -// about every site it reaches, the requirement records of calls, the -// witnesses its checks get (seen as the planned template), raw-cast -// pointers, and the §7.4 object extents. -// -//===----------------------------------------------------------------------===// - -#include "SiteTestUtils.h" -#include "weavec/Analysis/UnitPipeline.h" - -#include - -#include -#include - -namespace weavec::analysis { - -using test::collectUnit; -using Lines = std::vector; - -/// ` =` for every site of `function`. -static Lines outcomes(const core::Ledger &ledger, llvm::StringRef function) { - Lines out; - for (const core::FunctionLedger &row : ledger.units.front().functions) { - if (row.name != function) - continue; - for (const core::Site &site : row.sites) { - std::string line = site.text; - for (const core::Facet facet : core::AllFacets) { - const core::FacetRecord *record = site.facet(facet); - if (record == nullptr) - continue; - line += " " + std::string(core::toString(facet)) + "=" + - test::compactOf(record->decision); - } - out.push_back(line); - } - } - return out; -} - -namespace { -struct Piped { - Lines diagnostics; - core::Ledger ledger; -}; -} // namespace - -static Piped pipe(const test::CollectedUnit &unit, - const UnitPipelineOptions &options = {}) { - core::DiagnosticCollector collected; - const UnitPipelineResult result = - runUnitAnalysis(unit.context(), options, collected); - Piped out; - for (const core::Diagnostic &d : collected.diagnostics()) - out.diagnostics.push_back(std::to_string(d.location.line) + ": " + - test::severityText(d.severity) + ": " + - d.message); - if (result.ledger) - out.ledger = *result.ledger; - return out; -} - -/// The line of `outcomes(ledger, function)` whose text is `text`, or "". -static std::string row(const Piped &piped, llvm::StringRef function, - llvm::StringRef text) { - for (const std::string &line : outcomes(piped.ledger, function)) - if (llvm::StringRef(line).starts_with((text + " ").str())) - return line; - return {}; -} - -namespace { - -// -- §15 item 4: every site the pass reaches ------------------------------- - -TEST(EngineDecisions, InteriorAndConsumedAccessesAreDecided) { - const auto unit = collectUnit(R"c( -struct vec { int *items; int n; }; -int at(struct vec *v, int i) { return v->items[i]; } -void drop(char **a, unsigned i) { free(a[i]); } -void dropSigned(char **a, int i) { free(a[i]); } -int local(void) { struct vec v = {0, 0}; struct vec *p = &v; return p->n; } -)c"); - const Piped piped = pipe(unit); - EXPECT_TRUE(piped.diagnostics.empty()); - // The load of `v->items` on the way to the element has its own spatial - // decision, proven by `v`'s Single default (§7.3), and so has the - // consumed load: `a[i]` is a must-access of the exported `drop`, so its - // caller contract (`counted(i + 1)`, §7.5 R5) covers it. - EXPECT_EQ( - row(piped, "at", "v->items"), - "v->items spatial=proven null=unresolved/undecided temporal=proven"); - EXPECT_EQ(row(piped, "drop", "a[i]"), - "a[i] spatial=trusted/caller-contract null=unresolved/undecided " - "temporal=proven"); - // RFC 0017 §5: a count bounds an index from above only; `dropSigned(a, - // -1)` meets `counted(0)` and still reads before `a`. - EXPECT_EQ(row(piped, "dropSigned", "a[i]"), - "a[i] spatial=unresolved/unknown-extent null=unresolved/undecided " - "temporal=proven"); - EXPECT_EQ(row(piped, "local", "p->n"), - "p->n spatial=proven null=proven temporal=proven"); -} - -TEST(EngineDecisions, AUseAfterFreeStillDecidesItsNullFacet) { - const auto unit = collectUnit(R"c( -int f(char *p) { free(p); return p[1]; } -)c"); - const Piped piped = pipe(unit); - EXPECT_EQ(row(piped, "f", "p[1]"), - "p[1] spatial=trusted/caller-contract null=unresolved/undecided " - "temporal=violation"); -} - -// §15 item 4: the start of an allocation, proven when the engine knows it. -TEST(EngineDecisions, ReleasesAreTheStartOfTheirAllocation) { - const auto unit = collectUnit(R"c( -void own(void) { char *p = malloc(4); free(p); } -void none(void) { free(0); } -void param(char *p) { free(p); } -void inner(void) { char *p = malloc(4); if (!p) return; free(p + 1); } -)c"); - const Piped piped = pipe(unit); - EXPECT_EQ(row(piped, "own", "free(p)"), - "free(p) spatial=proven temporal=proven"); - EXPECT_EQ(row(piped, "none", "free(0)"), - "free(0) spatial=proven temporal=proven"); - EXPECT_EQ(row(piped, "param", "free(p)"), - "free(p) spatial=unresolved/unknown-index temporal=proven"); - EXPECT_EQ(row(piped, "inner", "free(p+1)"), - "free(p+1) spatial=violation temporal=proven"); -} - -// -- §2.5, §8, §15 item 12: requirement records of calls --------------------- - -TEST(EngineRequirements, LibraryCallsHaveARecordPerRequirement) { - const auto unit = collectUnit(R"c( -char *strcpy(char *, const char *); -void copy(const char *src, size_t n) { char buf[16]; memcpy(buf, src, n); } -void fits(void) { char buf[16]; strcpy(buf, "hello"); } -void none(char *d, const char *s) { memcpy(d, s, 0); } -)c"); - const Piped piped = pipe(unit); - EXPECT_TRUE(piped.diagnostics.empty()) - << ::testing::PrintToString(piped.diagnostics); - // The source's extent is unknown and the destination's is not known to - // hold `n` bytes: the facet takes the highest-ranked requirement (§2.5). - // The two cannot overlap: `buf` is this activation's own storage, which - // no caller's pointer reaches (RFC 0031 §4.5 D4). - EXPECT_EQ(row(piped, "copy", "memcpy(buf,src,n)"), - "memcpy(buf,src,n) spatial=unresolved/undecided " - "null=unresolved/undecided temporal=proven"); - // Arrays in scope have no null or temporal facet (§2.1). - EXPECT_EQ(row(piped, "fits", "strcpy(buf,\"hello\")"), - "strcpy(buf,\"hello\") spatial=proven"); - // §8.3: a zero length accepts null pointers. - EXPECT_EQ(row(piped, "none", "memcpy(d,s,0)"), "memcpy(d,s,0) spatial=proven " - "null=proven temporal=proven"); -} - -// A declared kind (`SIZED_BY`) is a requirement of the call (§7.2). -TEST(EngineRequirements, DeclaredRequirementsAreCheckedAtTheCall) { - const auto unit = collectUnit(R"c( -void fill(char *SIZED_BY(n) dst, size_t n) { dst[0] = 0; } -void call(size_t n) { char buf[8]; fill(buf, n); fill(buf, 4); } -)c"); - const Piped piped = pipe(unit); - EXPECT_EQ(row(piped, "call", "fill(buf,n)"), - "fill(buf,n) spatial=unresolved/undecided " - "temporal=proven"); - EXPECT_EQ(row(piped, "call", "fill(buf,4)"), - "fill(buf,4) spatial=proven temporal=proven"); -} - -// RFC 0030 *Diagnostics*: a string literal has no writable byte. -TEST(EngineRequirements, WritesIntoAStringLiteralAreViolations) { - const auto unit = collectUnit(R"c( -char *strcpy(char *, const char *); -void store(void) { char *p = "abc"; p[0] = 'x'; } -void copy(void) { char *p = "abc"; strcpy(p, "z"); } -void maybe(int c, char *b) { char *p = c ? "abc" : b; p[0] = 'x'; } -)c"); - const Piped piped = pipe(unit); - EXPECT_EQ(piped.diagnostics, - (Lines{"3: error: write through 'p', which points to a string " - "literal", - "4: error: write through 'p', which points to a string " - "literal"})); - EXPECT_EQ(row(piped, "store", "p[0]"), - "p[0] spatial=violation null=proven temporal=proven"); - EXPECT_EQ(row(piped, "maybe", "p[0]"), - "p[0] spatial=unresolved/unknown-extent null=unresolved/undecided " - "temporal=proven"); -} - -// -- §2.3 `raw-cast`: pointers made by reinterpretation --------------------- - -TEST(EngineRawCast, ReinterpretedPointersAreUnresolved) { - const auto unit = collectUnit(R"c( -union pun { long bits; int *p; }; -int punned(union pun *u) { u->bits = 1234; return *u->p; } -int bytes(void) { - char *p = malloc(8); if (!p) return 0; - char *q; unsigned char *d = (unsigned char *)&q, *s = (unsigned char *)&p; - for (int i = 0; i < 8; i++) d[i] = s[i]; - return q[0]; -} -int vararg(int n, ...) { - __builtin_va_list ap; __builtin_va_start(ap, n); - int *p = __builtin_va_arg(ap, int *); - int *copy = p; - __builtin_va_end(ap); - return *copy; -} -int plain(union pun *u) { u->p = 0; return 0; } -)c"); - const Piped piped = pipe(unit); - EXPECT_EQ(row(piped, "punned", "*u->p"), - "*u->p spatial=unresolved/raw-cast null=unresolved/undecided " - "temporal=unresolved/raw-cast"); - EXPECT_EQ(row(piped, "bytes", "q[0]"), - "q[0] spatial=unresolved/raw-cast null=unresolved/undecided " - "temporal=unresolved/raw-cast"); - EXPECT_EQ(row(piped, "vararg", "*copy"), - "*copy spatial=unresolved/raw-cast null=unresolved/undecided " - "temporal=unresolved/raw-cast"); -} - -// -- §7.4 and §14: object extents and the witnesses of checks --------------- - -// §7.4 *Arithmetic*: the allocation holds the bytes the program passed; a -// count the facts do not give is those bytes rounded down (probe 06). -TEST(EngineExtents, AWrappedByteCountIsCheckedAsBytes) { - const auto unit = collectUnit(R"c( -void fill(unsigned n) { - unsigned bytes = n * 4u; - int *a = malloc(bytes); - if (!a) return; - for (unsigned i = 0; i < n; i++) a[i] = 0; - free(a); -} -)c"); - const Piped piped = pipe(unit); - EXPECT_EQ(row(piped, "fill", "a[i]"), - "a[i] spatial=unresolved/undecided null=proven temporal=proven"); -} - -// §7.4: a cursor into its object is checked with a span over the object; -// a pointer converted between element sizes keeps no element offset. -TEST(EngineExtents, CursorsAreCheckedAgainstTheirObject) { - const auto unit = collectUnit(R"c( -int walk(int i) { int a[8] = {0}; int *p = a + 2; return p[i]; } -int flat(void) { - int m[3][4] = {{0}}; int *q = &m[0][0]; int s = 0; - for (int k = 0; k < 12; k++) s += q[k]; - return s; -} -char bytes(void) { int a[10] = {0}; char *c = (char *)(a + 2); return c[35]; } -)c"); - const Piped piped = pipe(unit); - // `c` starts 8 bytes into `a`'s 40, so `c[35]` reaches byte 44: the - // object engine's byte offsets make it definite (RFC 0031 §5.2). - EXPECT_EQ(piped.diagnostics, - (Lines{"8: error: 'c[35]' is out of bounds: index 35 of an object " - "of 40 bytes"})); - EXPECT_EQ(row(piped, "walk", "p[i]"), - "p[i] spatial=unresolved/undecided null=proven temporal=proven"); - // `k < 12` keeps `q` within the 48 bytes of `m`, the complete object - // (RFC 0030 §7.4): proven. - EXPECT_EQ(row(piped, "flat", "q[k]"), - "q[k] spatial=proven null=proven temporal=proven"); - EXPECT_EQ(row(piped, "bytes", "c[35]"), - "c[35] spatial=violation null=proven temporal=proven"); -} - -// §7.4: a trailing array member is flexible whatever its bound; a pointer -// made from a member has the extent of the whole object; only a direct -// subscript of a non-flexible member array uses the member's bound. -TEST(EngineExtents, MembersAreBoundedByTheirObject) { - const auto unit = collectUnit(R"c( -struct fixed { int pad; int data[2]; }; -struct mid { int items[4]; int n; }; -struct s3 { int first; int second; char *name; }; -void trailing(void) { struct fixed *b = malloc(100); if (!b) return; b->data[2] = 1; free(b); } -void direct(void) { struct mid *g = malloc(sizeof *g); if (!g) return; g->items[4] = 1; free(g); } -void decayed(void) { struct mid *g = malloc(sizeof *g); if (!g) return; int *p = g->items; p[4] = 1; free(g); } -void member(void) { struct s3 s; int *p = &s.second; p[1] = 2; } -void flexible(int n) { struct fixed *b = malloc(sizeof *b + 4 * (size_t)n); if (!b) return; b->data[5] = 1; free(b); } -)c"); - const Piped piped = pipe(unit); - EXPECT_EQ(piped.diagnostics, - (Lines{"6: error: 'g->items[4]' is out of bounds: index 4 of an " - "object of 16 bytes"})); - EXPECT_EQ(row(piped, "trailing", "b->data[2]"), - "b->data[2] spatial=proven temporal=proven"); - // (`g` was tested, and `g->items` is an address inside it: non-null.) - EXPECT_EQ(row(piped, "decayed", "p[4]"), - "p[4] spatial=proven null=proven temporal=proven"); - EXPECT_EQ(row(piped, "member", "p[1]"), - "p[1] spatial=proven null=proven temporal=proven"); - // The flexible member's elements up to the end of the allocation. The - // extent `sizeof *b + 4 * (size_t)n` goes through a conversion of `n`, - // which no witness spells yet: no check can be planned, so the facet is - // unresolved, never proven (KNOWN-DIFFERENCES.md, *Unit tests*; with a - // `size_t` count it is checked). - EXPECT_EQ(row(piped, "flexible", "b->data[5]"), - "b->data[5] spatial=unresolved/undecided temporal=proven"); -} - -// -- RFC 0030 §8, §5.3: the library table in the engine (S4) ---------------- - -/// The diagnostics whose text contains `needle`. -static Lines matching(const Piped &piped, llvm::StringRef needle) { - Lines out; - for (const std::string &line : piped.diagnostics) - if (llvm::StringRef(line).contains(needle)) - out.push_back(line); - return out; -} - -// §8.2: `retain(S)` keeps `s` in ``; `reads(S)` after `s` is freed -// is a use after free, reported once. -TEST(LibraryEngine, RetainedStateReadAfterReleaseIsAUseAfterFree) { - const auto unit = collectUnit(R"c( -char *strtok(char *, const char *); -char *strdup(const char *); -char *next(void) { - char *s = strdup("a,b"); - if (!s) return 0; - strtok(s, ","); - free(s); - return strtok(0, ","); -} -)c"); - const Piped piped = pipe(unit); - EXPECT_EQ(piped.diagnostics, - (Lines{"9: error: use of '' after it was freed"})); -} - -// §8.2: `invalidates(environ)` possibly ends an earlier `getenv` result; -// without it, the result's temporal facet rests on the row. -TEST(LibraryEngine, InvalidatedStaticResultsAreMayUsesAfterFree) { - const auto unit = collectUnit(R"c( -char *getenv(const char *); -int setenv(const char *, const char *, int); -int after(void) { - const char *v = getenv("X"); - setenv("X", "y", 1); - return v ? v[0] : 0; -} -int before(void) { - const char *h = getenv("HOME"); - return h ? h[0] : 0; -} -)c"); - const Piped piped = pipe(unit); - EXPECT_FALSE(matching(piped, "7: warning: use of 'v' after it may have " - "been freed") - .empty()); - EXPECT_TRUE(matching(piped, "error").empty()); - EXPECT_EQ(row(piped, "before", "h[0]"), - "h[0] spatial=unresolved/unknown-extent null=proven " - "temporal=trusted/library-spec"); -} - -// §5.3: a `sync` callback's effects on globals are may-effects of the call; -// an unknown target is the unknown-callee default with reason `callback`. -TEST(LibraryEngine, SyncCallbacksApplyTheirTargetsAsMayEffects) { - const auto unit = collectUnit(R"c( -void qsort(void *, size_t, size_t, int (*)(const void *, const void *)); -static char *victim; -static int cmp(const void *a, const void *b) { - free(victim); victim = 0; return *(const int *)a - *(const int *)b; -} -int sorted(char *p) { - int xs[2] = {2, 1}; - victim = p; - qsort(xs, 2, sizeof(int), cmp); - return p[0]; -} -int by(int (*c)(const void *, const void *)) { - int xs[2] = {2, 1}; - qsort(xs, 2, sizeof(int), c); - return xs[0]; -} -static int less(const void *a, const void *b) { - return *(const int *)a - *(const int *)b; -} -int handed(const char **names, void (*sink)(void *), void *data) { - const char **xs = malloc(2 * sizeof(char *)); - if (!xs) return 0; - xs[0] = names[0]; xs[1] = names[1]; - qsort(xs, 2, sizeof(char *), less); - sink(data); - int r = xs[0][0]; - free(xs); - return r; -} -)c"); - const Piped piped = pipe(unit); - EXPECT_EQ(piped.diagnostics, - (Lines{"11: warning: use of 'p' after it may have been freed"})); - EXPECT_TRUE(llvm::StringRef(row(piped, "by", "qsort(xs,2,sizeof(int),c)")) - .contains("temporal=unresolved/callback")); - EXPECT_TRUE( - llvm::StringRef(row(piped, "sorted", "qsort(xs,2,sizeof(int),cmp)")) - .contains("temporal=trusted/library-spec")); - // A target that only reads what it is handed does not make the array - // reachable to a later unknown callee. - EXPECT_TRUE(llvm::StringRef(row(piped, "handed", "free(xs)")) - .contains("temporal=proven")) - << row(piped, "handed", "free(xs)"); -} - -// §5.3: what a thread entry point frees is shared: the main flow's use is -// trusted(concurrency), and its null test proves nothing (checked). -TEST(LibraryEngine, EntryTargetsShareWhatTheyTouch) { - const auto unit = collectUnit(R"c( -typedef unsigned long pthread_t; -int pthread_create(pthread_t *, const void *, void *(*)(void *), void *); -static char *shared; -static void *worker(void *arg) { (void)arg; free(shared); return 0; } -int run(void) { - pthread_t t; - shared = malloc(8); - if (!shared) return 1; - pthread_create(&t, 0, worker, 0); - return shared[0]; -} -)c"); - const Piped piped = pipe(unit); - EXPECT_TRUE(piped.diagnostics.empty()); - EXPECT_EQ(row(piped, "run", "shared[0]"), - "shared[0] spatial=trusted/concurrency null=trusted/concurrency " - "temporal=trusted/concurrency"); - EXPECT_TRUE(llvm::StringRef(row(piped, "worker", "free(shared)")) - .contains("temporal=trusted/concurrency")); -} - -// §5.3: a handler stored into `sigaction`'s `act` is an entry target; an -// `atexit` target runs after the main flow and shares nothing. -TEST(LibraryEngine, SigactionHandlersShareAtexitTargetsDoNot) { - const auto unit = collectUnit(R"c( -struct sigaction { void (*sa_handler)(int); int sa_flags; }; -int sigaction(int, const struct sigaction *, struct sigaction *); -int atexit(void (*)(void)); -static char *buf; -static void on(int s) { (void)s; free(buf); buf = 0; } -int arm(void) { - struct sigaction sa = {0, 0}; - sa.sa_handler = on; - sigaction(2, &sa, 0); - buf = malloc(4); - if (!buf) return 1; - return buf[0]; -} -static char *late; -static void bye(void) { free(late); } -int reg(void) { - atexit(bye); - late = malloc(4); - if (!late) return 1; - return late[0]; -} -)c"); - const Piped piped = pipe(unit); - EXPECT_TRUE(llvm::StringRef(row(piped, "arm", "buf[0]")) - .contains("temporal=trusted/concurrency")); - EXPECT_TRUE(llvm::StringRef(row(piped, "reg", "late[0]")) - .contains("temporal=proven")); -} - -// §9.2: a result class no path returns while the parameter may be null -// proves it non-null (a pointer result, where no numeric output says so). -TEST(LibraryEngine, GuardFunctionsProveTheirParameters) { - const auto unit = collectUnit(R"c( -struct x { int v; }; -struct x *pick(struct x *p) { if (!p) return 0; return p->v ? p : 0; } -int use(struct x *p) { if (!pick(p)) return 0; return p->v; } -int ok(struct x *p, int k) { if (k) return 1; if (!p) return 0; return 1; } -int keep(struct x *p) { if (!ok(p, 1)) return 0; return p->v; } -)c"); - const Piped piped = pipe(unit); - EXPECT_TRUE( - llvm::StringRef(row(piped, "use", "p->v")).contains("null=proven")); - EXPECT_TRUE(llvm::StringRef(row(piped, "keep", "p->v")) - .contains("null=unresolved/undecided")); -} - -// §8.4: what `main` still holds when it returns is no leak. -TEST(LibraryEngine, NoLeakAtAReturnFromMain) { - const auto unit = collectUnit(R"c( -int more(void); -int main(void) { - char *b = malloc(10); if (!b) return 1; if (more()) return 2; free(b); - return 0; -} -int other(void) { - char *b = malloc(10); if (!b) return 1; if (more()) return 2; free(b); - return 0; -} -)c"); - const Piped piped = pipe(unit); - EXPECT_EQ(piped.diagnostics, (Lines{"8: warning: 'b' is leaked"})); -} - -// §8.2: `alloca` storage is the frame's: returning it is lifetime-too-short -// and its extent is exact. -TEST(LibraryEngine, AllocaIsStackStorage) { - const auto unit = collectUnit(R"c( -void *alloca(size_t); -char *make(void) { char *p = alloca(8); p[0] = 1; return p; } -int over(void) { char *q = alloca(8); q[8] = 1; return q[0]; } -)c"); - const Piped piped = pipe(unit); - EXPECT_EQ(piped.diagnostics, - (Lines{"3: error: 'p' may outlive '', which it points to", - "4: error: 'q[8]' is out of bounds: index 8 of an object " - "of 8 bytes"})); -} - -// §8.2: a literal format that reads more arguments than are passed; a -// format that is no literal leaves the call's spatial facet inexpressible. -TEST(LibraryEngine, FormatArity) { - const auto unit = collectUnit(R"c( -int printf(const char *, ...); -void two(const char *s) { printf("%s %s\n", s); } -void any(const char *fmt) { printf(fmt); } -)c"); - const Piped piped = pipe(unit); - EXPECT_EQ(piped.diagnostics, - (Lines{"3: error: format string of 'printf' reads 2 arguments " - "but 1 are passed"})); - EXPECT_TRUE(llvm::StringRef(row(piped, "any", "printf(fmt)")) - .contains("spatial=unresolved/")); -} - -// §8.3: a null pointer with a zero length is allowed; with a known non-zero -// length it is a definite error. -TEST(LibraryEngine, NullIfZero) { - const auto unit = collectUnit(R"c( -void *memcpy(void *, const void *, size_t); -void four(const char *s) { memcpy(0, s, 4); } -void none(const char *s) { memcpy(0, s, 0); } -)c"); - const Piped piped = pipe(unit); - EXPECT_EQ(piped.diagnostics, - (Lines{"3: error: a null pointer is passed to 'memcpy', which " - "dereferences it"})); -} - -// §8.2, §11: `realloc`'s zero-size release on the null class is reported -// where the result is tested; a size no summary term expresses keeps it out -// of the wrapper's summary (callers would see it unconditionally). -TEST(LibraryEngine, ZeroSizeReleasesStayWithTheirTest) { - const auto unit = collectUnit(R"c( -void *realloc(void *, size_t); -struct buf { char *data; size_t cap; }; -static int reserve(struct buf *b, size_t len) { - size_t want = b->cap ? b->cap : 64; - while (want < len) want *= 2; - char *nb = realloc(b->data, want); - if (!nb) { b->data[0] = 0; return -1; } - b->data = nb; b->cap = want; - return 0; -} -int append(struct buf *b, size_t len) { - if (reserve(b, len) == -1) return b->data[0]; - return b->data[1]; -} -)c"); - // (Without zero-initialisation, whose wrapper never asks for zero bytes.) - UnitPipelineOptions plain; - plain.engine.zeroInit = false; - const Piped piped = pipe(unit, plain); - EXPECT_EQ(piped.diagnostics, - (Lines{"8: warning: use of 'b->data' after it may have been " - "freed"})); - EXPECT_EQ(pipe(unit).diagnostics, Lines{}); -} - -// §8.2: an exported zero-size release is a guarded consume of the null -// class; the other class still moves the old block, whatever the size, so a -// pointer into it is stale after the call (with or without result classes). -TEST(LibraryEngine, ZeroSizeReleasesKeepTheOtherClassMove) { - const auto unit = collectUnit(R"c( -void *realloc(void *, size_t); -struct vec { int *data; size_t len; }; -static int reserve(struct vec *v, size_t n) { - int *nd = realloc(v->data, n * sizeof *nd); - if (!nd) return -1; - v->data = nd; - return 0; -} -static void grow(struct vec *v, size_t n) { - int *nd = realloc(v->data, n * sizeof *nd); - if (!nd) return; - v->data = nd; -} -int stale(struct vec *v) { - int *first = &v->data[0]; - if (reserve(v, 100)) return 1; - return *first; -} -int stale_void(struct vec *v) { - int *first = &v->data[0]; - grow(v, 100); - return *first; -} -)c"); - const Piped piped = pipe(unit); - // `grow` returns nothing to test: its caller cannot tell the moving - // class from the failing one, which keeps the block (the size, 400, is - // not zero, which the call's numeric context shows, so the block is not - // freed), so the use after it is possible (RFC 0031 §6.1: a void - // function's exits join into possible effects). - EXPECT_EQ(piped.diagnostics, - (Lines{"18: error: use of 'first' after it was moved", - "23: warning: use of 'first' after it may have been " - "moved"})); -} - -} // namespace -} // namespace weavec::analysis diff --git a/unittests/Analysis/GuardCompletenessTest.cpp b/unittests/Analysis/GuardCompletenessTest.cpp deleted file mode 100644 index 94dc3863..00000000 --- a/unittests/Analysis/GuardCompletenessTest.cpp +++ /dev/null @@ -1,139 +0,0 @@ -//===- GuardCompletenessTest.cpp - Complete numeric guards (RFC 0017) -----===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// - -#include "TestUtils.h" - -#include -#include - -#include "gtest/gtest.h" - -using namespace weavec; -using namespace weavec::test; - -static unsigned countId(const AnalysisResult &result, std::string_view id) { - return static_cast( - std::ranges::count_if(result.diagnostics.diagnostics(), - [id](const core::Diagnostic &diagnostic) { - return diagnostic.id == id; - })); -} - -/// The spatial facet of the site spelled `text` at `line`, as its outcome, -/// with the reason when it is unresolved (`unresolved/unknown-extent`). -static std::string spatialAt(const AnalysisResult &result, unsigned line, - std::string_view text) { - for (const core::UnitLedger &unit : result.ledger.units) - for (const core::FunctionLedger &function : unit.functions) - for (const core::Site &site : function.sites) - if (site.location.line == line && site.text == text) - if (const core::FacetRecord *record = - site.facet(core::Facet::Spatial)) { - std::string out(core::toString(record->outcome())); - if (record->outcome() == core::SiteOutcome::Unresolved) - out += "/" + std::string(record->decision.reasonText()); - return out; - } - return "none"; -} - -TEST(GuardCompleteness, CapacityCannotDropARelationalRequirementPremise) { - const auto result = analyze(R"c( - void limited(char *p, unsigned a, unsigned b, unsigned c, unsigned d, - unsigned e, unsigned f, unsigned g, unsigned h, - unsigned n, unsigned m) { - if (!a || !b || !c || !d || !e || !f || !g || !h) return; - if (n < m) p[n] = 0; - } - void good(void) { - char two[2]; limited(two, 1,1,1,1,1,1,1,1, 2,2); - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::OutOfBounds), 0U) - << ::testing::PrintToString(messages(result.diagnostics)); -} - -TEST(GuardCompleteness, OmittedScalarFactsCannotProveAnOmittedPredicate) { - const auto result = analyze(R"c( - void limited(char *p, unsigned a, unsigned b, unsigned c, unsigned d, - unsigned e, unsigned f, unsigned g, unsigned h, unsigned n) { - if (!a || !b || !c || !d || !e || !f || !g || !h) return; - if (n == 3) p[n] = 0; - } - void good(void) { - char two[2]; limited(two, 1,1,1,1,1,1,1,1, 2); - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::OutOfBounds), 0U) - << ::testing::PrintToString(messages(result.diagnostics)); -} - -TEST(GuardCompleteness, UnknownCallConditionCannotDisappearAtCapacity) { - const auto result = analyze(R"c( - void limited(char *p, unsigned a, unsigned b, unsigned c, unsigned d, - unsigned e, unsigned f, unsigned g, unsigned h, unsigned n) { - if (!a || !b || !c || !d || !e || !f || !g || !h) return; - if (cond()) p[n] = 0; - } - )c"); - ASSERT_TRUE(result.ast); -} - -// A guarded access is none of the §7.5 rules, and RFC 0031 §6.1's -// summaries carry no extent requirement: the access stays unresolved in -// `fits`'s own unit, never proven. At link the context of `bad`'s call -// stores past `three` (RFC 0031 *Implementation amendments*, "Stores past -// the caller's object"), and `good` gets no finding. -TEST(GuardCompleteness, RetainedScalarFactsCanImplyANumericPredicate) { - static constexpr const char *Callee = R"c( - void fits(char *p, unsigned a, unsigned b, unsigned c, unsigned d, - unsigned e, unsigned f, unsigned g, unsigned n) { - if (!a || !b || !c || !d || !e || !f || !g) return; - if (n == 3) p[n] = 0; - } - )c"; - const auto result = analyze(Callee); - ASSERT_TRUE(result.ast); - EXPECT_EQ(spatialAt(result, 5, "p[n]"), "unresolved/unknown-extent"); - const auto linked = analyzeAtLink(Callee, R"c( - void fits(char *p, unsigned a, unsigned b, unsigned c, unsigned d, - unsigned e, unsigned f, unsigned g, unsigned n); - void good(void) { - char four[4]; fits(four, 1,1,1,1,1,1,1, 3); - } - void bad(void) { - char three[3]; fits(three, 1,1,1,1,1,1,1, 3); - } - )c"); - EXPECT_EQ(countId(linked, core::diag::OutOfBounds), 1U) - << ::testing::PrintToString(messages(linked.diagnostics)); -} - -// RFC 0031 §6.1: `fill`'s minimum is exported as no requirement; the access -// stays unresolved in `fill`'s own unit, never proven. At link the context -// of `bad`'s call stores past `two` (RFC 0031 *Implementation amendments*, -// "Stores past the caller's object"), and `good` gets no finding. -TEST(GuardCompleteness, CanonicalLoopBoundaryCanExcludeItsIndexPredicate) { - static constexpr const char *Callee = R"c( - void fill(char *p, unsigned n, unsigned cap) { - for (unsigned i = 0; i < n && i < cap; ++i) p[i] = 0; - } - )c"; - const auto result = analyze(Callee); - ASSERT_TRUE(result.ast); - EXPECT_EQ(spatialAt(result, 3, "p[i]"), "unresolved/unknown-extent"); - const auto linked = analyzeAtLink(Callee, R"c( - void fill(char *p, unsigned n, unsigned cap); - void good(void) { char two[2]; fill(two, 10, 2); } - void bad(void) { char two[2]; fill(two, 3, 3); } - )c"); - EXPECT_EQ(countId(linked, core::diag::OutOfBounds), 1U) - << ::testing::PrintToString(messages(linked.diagnostics)); -} diff --git a/unittests/Analysis/HeapStateTest.cpp b/unittests/Analysis/HeapStateTest.cpp deleted file mode 100644 index d9ac1fc7..00000000 --- a/unittests/Analysis/HeapStateTest.cpp +++ /dev/null @@ -1,711 +0,0 @@ -//===- HeapStateTest.cpp - Interprocedural heap state (RFC 0013) ----------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// - -#include "TestUtils.h" -#include "weavec/Analysis/ProgramDatabase.h" - -#include - -namespace weavec::analysis { - -using weavec::test::analyze; -using weavec::test::ids; -using Strings = std::vector; - -/// `[/]` of `facet` at the site whose ledger text is -/// `text`, or empty when there is none. -static std::string outcomeAt(const test::AnalysisResult &result, - std::string_view text, core::Facet facet) { - for (const core::UnitLedger &unit : result.ledger.units) - for (const core::FunctionLedger &function : unit.functions) - for (const core::Site &site : function.sites) { - const core::FacetRecord *record = site.facet(facet); - if (site.text != text || record == nullptr) - continue; - std::string out(core::toString(record->outcome())); - if (!record->decision.reasonText().empty()) - out += "/" + std::string(record->decision.reasonText()); - return out; - } - return {}; -} - -/// No diagnostic is an error (a definite finding). -static bool noErrors(const test::AnalysisResult &result) { - for (const core::Diagnostic &diagnostic : result.diagnostics.diagnostics()) - if (diagnostic.severity == core::Severity::Error) - return false; - return true; -} - -static constexpr const char *Box = R"c( - struct box { char *data; }; - struct box *make(void) { - struct box *b = malloc(sizeof *b); - if (!b) return NULL; - b->data = malloc(4); - if (!b->data) { free(b); return NULL; } - return b; - } -)c"; - -TEST(HeapState, ReturnedArgumentAliasRetainsIdentity) { - const auto result = analyze(R"c( - struct box { char *data; }; - struct box *wrap(char *p) { - struct box *b = malloc(sizeof *b); - if (!b) return NULL; - b->data = p; return b; - } - void bad(void) { - char *p = malloc(4); if (!p) return; - struct box *b = wrap(p); - if (!b) { free(p); return; } - free(p); b->data[0] = 0; free(b); - } - void good(void) { - char *p = malloc(4); if (!p) return; - struct box *b = wrap(p); - if (!b) { free(p); return; } - b->data[0] = 0; free(p); free(b); - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(ids(result.diagnostics), Strings{"use-after-free"}); -} - -TEST(HeapState, AllocationSizeUsesItsOldValue) { - const auto result = analyze(R"c( - void constant(void) { - size_t n = 4; char *p = malloc(n); if (!p) return; - n = 8; p[7] = 0; free(p); - } - void symbolic(size_t n) { - size_t original = n; - char *p = malloc(n); if (!p) return; - n = 2; p[original] = 0; free(p); - } - void good(size_t n) { - size_t original = n; - char *p = malloc(n); if (!p) return; - n = 2; - for (size_t i = 0; i < original; ++i) p[i] = 0; - free(p); - } - void make_buffer(char **out, size_t *size) { - *size = 4; *out = malloc(*size); - } - void output(void) { - char *p; size_t n; make_buffer(&p, &n); - if (!p) return; - p[4] = 0; free(p); - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(ids(result.diagnostics), - (Strings{"out-of-bounds", "out-of-bounds", "out-of-bounds"})); -} - -TEST(HeapState, NestedObjectsAndPublishedLocalAliases) { - const auto result = analyze(R"c( - struct leaf { char *data; }; - struct tree { struct leaf *child; }; - void make(struct tree **out) { - struct tree *t = malloc(sizeof *t); *out = t; - if (!t) return; - t->child = malloc(sizeof *t->child); - if (!t->child) { free(t); *out = NULL; return; } - t->child->data = malloc(4); - } - void good(void) { - struct tree *t; make(&t); if (!t) return; - if (t->child->data) t->child->data[3] = 0; - free(t->child->data); free(t->child); free(t); - } - void bad(void) { - struct tree *t; make(&t); if (!t) return; - if (t->child->data) t->child->data[4] = 0; - free(t->child->data); free(t->child); free(t); - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(ids(result.diagnostics), Strings{"out-of-bounds"}); -} - -TEST(HeapState, FinalNullAndNullableFields) { - const auto result = analyze(R"c( - struct box { char *data; }; - struct box *empty(void) { - struct box *b = malloc(sizeof *b); if (!b) return NULL; - b->data = malloc(4); free(b->data); b->data = NULL; return b; - } - struct box *maybe(void) { - struct box *b = malloc(sizeof *b); if (!b) return NULL; - b->data = malloc(4); return b; - } - void null_field(void) { - struct box *b = empty(); if (!b) return; - b->data[0] = 0; free(b); - } - void nullable_field(void) { - struct box *b = maybe(); if (!b) return; - b->data[0] = 0; free(b->data); free(b); - } - void good(void) { - struct box *b = maybe(); if (!b) return; - if (b->data) b->data[0] = 0; - free(b->data); free(b); - } - )c"); - ASSERT_TRUE(result.ast); - // `null_field` dereferences the null `empty` leaves; `nullable_field` - // dereferences an allocation `maybe` did not test (RFC 0030 §3.2). - EXPECT_EQ(ids(result.diagnostics), - (Strings{"null-dereference", "allocation-failure"})); -} - -TEST(HeapState, StringsCrossConstructorsAndForwarders) { - const auto result = analyze(R"c( - char *strcpy(char *, const char *); - void *memset(void *, int, size_t); - size_t strlen(const char *); - struct box { char *data; }; - struct box *make(void) { - struct box *b = malloc(sizeof *b); if (!b) return NULL; - b->data = malloc(8); - if (!b->data) { free(b); return NULL; } - strcpy(b->data, "hello"); return b; - } - struct box *forward(void) { return make(); } - char *unterm(void) { - char *p = malloc(4); if (!p) return NULL; - memset(p, 'a', 4); return p; - } - void bad(void) { - struct box *b = forward(); if (!b) return; - char dst[4]; strcpy(dst, b->data); - free(b->data); free(b); - } - void unterminated(void) { - char *p = unterm(); if (!p) return; - (void)strlen(p); free(p); - } - void good(void) { - struct box *b = forward(); if (!b) return; - char dst[6]; strcpy(dst, b->data); - free(b->data); free(b); - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(ids(result.diagnostics), - (Strings{"out-of-bounds", "out-of-bounds"})); -} - -TEST(HeapState, ReplacementPreservesOldAliasesAndFinalFields) { - const auto result = analyze(R"c( - struct box { char *data; }; - void reset(struct box *b) { free(b->data); b->data = NULL; } - void replace(struct box *b) { free(b->data); b->data = malloc(8); } - void old_alias(void) { - struct box b; b.data = malloc(4); if (!b.data) return; - char *old = b.data; replace(&b); - old[0] = 0; - if (b.data) b.data[7] = 0; - free(b.data); - } - void good(void) { - struct box b; b.data = malloc(4); if (!b.data) return; - reset(&b); free(b.data); - } - struct box *extract(struct box *b) { - char *old = b->data; b->data = NULL; - struct box *r = malloc(sizeof *r); - if (!r) { b->data = old; return NULL; } - r->data = old; return r; - } - void incoming(void) { - struct box b; b.data = malloc(4); if (!b.data) return; - char *old = b.data; - struct box *r = extract(&b); - if (!r) { free(b.data); return; } - free(old); r->data[0] = 0; free(r); - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(ids(result.diagnostics), - (Strings{"use-after-free", "use-after-free"})); -} - -TEST(HeapState, EscapingLocalBorrowAndReleaseFamilies) { - const auto result = analyze(R"c( - struct file; - struct file *fopen(const char *, const char *); - int fclose(struct file *); - struct box { char *data; struct file *file; }; - struct box *local_borrow(void) { - char local[4]; - struct box *b = malloc(sizeof *b); if (!b) return NULL; - b->data = local; b->file = NULL; return b; - } - struct box *make(void) { - struct box *b = malloc(sizeof *b); if (!b) return NULL; - b->data = NULL; b->file = fopen("x", "r"); return b; - } - void wrong(void) { - struct box *b = make(); if (!b) return; - free(b->file); free(b); - } - void good(void) { - struct box *b = make(); if (!b) return; - if (b->file) fclose(b->file); free(b); - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(ids(result.diagnostics), - (Strings{"lifetime-too-short", "mismatched-release"})); -} - -TEST(HeapState, ReusingSizeSnapshotInLoopNeverResizesOldObject) { - const auto result = analyze(R"c( - void good(size_t n, size_t count) { - char *older = NULL; - for (size_t k = 0; k < count; ++k) { - char *p = malloc(n); - size_t size = n; - n = k + 1; - if (p) { - for (size_t i = 0; i < size; ++i) p[i] = 0; - } - free(older); older = p; - } - free(older); - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(ids(result.diagnostics), Strings{}); -} - -TEST(HeapState, FailureRetainsTheIncomingPointerAndItsExtent) { - const auto result = analyze(R"c( - struct box { char *data; }; - int replace(struct box *b, size_t n) { - char *fresh = malloc(n); if (!fresh) return 0; - free(b->data); b->data = fresh; return 1; - } - void good(void) { - struct box b; b.data = malloc(4); if (!b.data) return; - if (!replace(&b, 8)) { - b.data[3] = 0; free(b.data); return; - } - b.data[7] = 0; free(b.data); - } - void bad(void) { - struct box b; b.data = malloc(4); if (!b.data) return; - if (!replace(&b, 8)) { - b.data[4] = 0; free(b.data); return; - } - free(b.data); - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(ids(result.diagnostics), Strings{"out-of-bounds"}); -} - -TEST(HeapState, CopiesOfConstructorResultsKeepSharedFields) { - const auto result = analyze(R"c( - struct pair { char *a, *b; }; - struct pair *make(void) { - struct pair *p = malloc(sizeof *p); if (!p) return NULL; - p->a = malloc(4); if (!p->a) { free(p); return NULL; } - p->b = p->a; - struct pair *q = p; return q; - } - struct pair *again(void) { - struct pair *a = make(); - struct pair *b = a; return b; - } - void good(void) { - struct pair *p = again(); if (!p) return; - free(p->a); free(p); - } - void bad(void) { - struct pair *p = again(); if (!p) return; - free(p->a); p->b[0] = 0; free(p); - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(ids(result.diagnostics), Strings{"use-after-free"}); -} - -TEST(HeapState, RawFieldStaysRaw) { - // RFC 0033 §2: a declared raw value stays raw through a summary's field. - const auto result = analyze(R"c( - #define RAW __attribute__((annotate("weavec.raw"))) - struct box { char *data; }; - struct box *make(char *RAW bits) { - struct box *b = malloc(sizeof *b); if (!b) return NULL; - b->data = bits; return b; - } - void bad(char *RAW bits) { - struct box *b = make(bits); if (!b) return; - b->data[0] = 0; free(b); - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(ids(result.diagnostics), Strings{"unsafe-operation"}); -} - -TEST(HeapState, IntegerConvertedFieldIsNotRaw) { - // RFC 0033 §2: a pointer converted from an integer is not raw, through a - // summary's field too. - const auto result = analyze(R"c( - struct box { char *data; }; - struct box *make(unsigned long bits) { - struct box *b = malloc(sizeof *b); if (!b) return NULL; - b->data = (char *)bits; return b; - } - void ok(unsigned long bits) { - struct box *b = make(bits); if (!b) return; - b->data[0] = 0; free(b); - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(ids(result.diagnostics), Strings{}); -} - -TEST(HeapState, RecordResultsAndCopiesPreserveSharedChildBounds) { - const auto result = analyze(R"c( - struct pair { char *a, *b; }; - struct pair make(void) { - struct pair p; p.a = malloc(4); p.b = p.a; return p; - } - struct pair forwarded(void) { return make(); } - struct pair again(void) { struct pair p = forwarded(), q = p; return q; } - void good(void) { - struct pair a = again(), b = a; - if (b.b) b.b[3] = 0; - free(b.a); - } - void overflow(void) { - struct pair a = again(), b = a; - if (b.b) b.b[4] = 0; - free(b.a); - } - void stale(void) { - struct pair a = again(), b = a; - if (!b.b) return; - free(b.a); b.b[0] = 0; - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(ids(result.diagnostics), - (Strings{"out-of-bounds", "use-after-free"})); -} - -TEST(HeapState, AssignmentAndConditionalConstructorKeepChildBounds) { - const auto result = analyze(std::string(Box) + R"c( - void good(void) { - struct box *p; p = make(); if (!p) return; - p->data[3] = 0; free(p->data); free(p); - } - void bad(int flag) { - struct box *p; p = flag ? make() : NULL; if (!p) return; - p->data[4] = 0; free(p->data); free(p); - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(ids(result.diagnostics), Strings{"out-of-bounds"}); -} - -TEST(HeapState, APublishedRootAndItsExplicitChildStoreAllocateOnce) { - const auto result = analyze(R"c( - struct box { char *data; }; - void make(struct box **out) { - *out = malloc(sizeof **out); if (!*out) return; - (*out)->data = malloc(4); - } - void good(void) { - struct box *b; make(&b); if (!b) return; - if (b->data) b->data[3] = 0; - free(b->data); free(b); - } - void bad(void) { - struct box *b; make(&b); if (!b) return; - if (b->data) b->data[4] = 0; - free(b->data); free(b); - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(ids(result.diagnostics), Strings{"out-of-bounds"}); -} - -TEST(HeapState, DerivedChildrenKeepTheirOffsetsAndObjectIdentity) { - const auto result = analyze(R"c( - struct box { char *base; char *cursor; }; - struct box *make(void) { - struct box *p = malloc(sizeof *p); if (!p) return 0; - p->base = malloc(8); - if (!p->base) { free(p); return 0; } - p->cursor = p->base; - p->cursor++; - return p; - } - void good(void) { - struct box *p = make(); if (!p) return; - p->cursor[6] = 0; - free(p->base); free(p); - } - void bad(void) { - struct box *p = make(); if (!p) return; - p->cursor[7] = 0; - free(p->base); free(p); - } - char *advance(char *p) { char *q = p; q++; return q; } - void borrowed(void) { - char *p = malloc(8); if (!p) return; - char *q = advance(p); q[7] = 0; free(p); - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(ids(result.diagnostics), - (Strings{"out-of-bounds", "out-of-bounds"})); -} - -TEST(HeapState, LazyPublicationKeepsItsEntryGuardAcrossTheCall) { - const auto result = analyze(R"c( - struct box { char *data; }; - static struct box *g; - void ensure(void) { - if (g) return; - g = malloc(sizeof *g); - if (g) g->data = malloc(4); - } - void bad(void) { - ensure(); if (!g || !g->data) return; - free(g->data); ensure(); g->data[0] = 0; - free(g); g = 0; - } - void good(void) { - ensure(); if (!g || !g->data) return; - ensure(); g->data[3] = 0; - free(g->data); free(g); g = 0; - } - )c"); - ASSERT_TRUE(result.ast); - // RFC 0031 *Entry tests*: `ensure` publishes `g` when `g` was null at - // entry, which the second call's `g` is not. - EXPECT_EQ(ids(result.diagnostics), Strings{"use-after-free"}); -} - -TEST(HeapState, AFieldWriteAfterConditionalPublicationStillRuns) { - const auto result = analyze(R"c( - struct box { char *data; }; - static struct box *g; - void set(void) { - if (!g) g = malloc(sizeof *g); - if (g) g->data = malloc(4); - } - void good(void) { - set(); if (!g || !g->data) return; - free(g->data); set(); if (!g->data) return; - g->data[3] = 0; free(g->data); free(g); g = 0; - } - void bad(void) { - set(); if (!g || !g->data) return; - free(g->data); set(); if (!g->data) return; - g->data[4] = 0; free(g->data); free(g); g = 0; - } - )c"); - ASSERT_TRUE(result.ast); - // RFC 0031 *Entry tests*: `set` publishes `g` exactly when `g` was null - // at entry, but its store to `g->data` runs after that join, and the exit - // that failed to allocate a new box stores nothing, so no entry test - // separates it: the store is possible, and the access may reach the old, - // freed data of unknown extent (KNOWN-DIFFERENCES.md, *Unit tests*). - EXPECT_EQ(outcomeAt(result, "g->data[4]", core::Facet::Spatial), - "unresolved/unknown-index"); - EXPECT_TRUE(result.diagnostics.empty()) - << ::testing::PrintToString(test::messages(result.diagnostics)); -} - -TEST(HeapState, ReturningAnExtractedPointerUsesTheEntryCell) { - const auto result = analyze(R"c( - char *extract_pointer(char **slot) { - char *p = *slot; if (!p) return 0; *slot = 0; return p; - } - char *forward(char **slot) { return extract_pointer(slot); } - void good(void) { - char *p = malloc(4); if (!p) return; - char *q = forward(&p); q[3] = 0; free(q); - } - void bad(void) { - char *p = malloc(4); if (!p) return; - char *old = p; char *q = forward(&p); - free(old); q[0] = 0; - } - void leak(void) { - char *p = malloc(4); if (!p) return; - char *q = forward(&p); q[0] = 0; - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(ids(result.diagnostics), (Strings{"use-after-free", "leak"})); -} - -TEST(HeapState, AReturnedRecordSharesAPublishedGlobalObject) { - const auto result = analyze(R"c( - struct box { char *data; }; static struct box *g; - void ensure(void) { - if (g) return; - g = malloc(sizeof *g); if (g) g->data = malloc(4); - } - struct pair { struct box *p; }; - struct pair get(void) { - ensure(); struct pair a = {g}; struct pair b = a; return b; - } - void bad(void) { - struct pair a = get(); if (!a.p || !a.p->data) return; - free(a.p->data); ensure(); g->data[0] = 0; - free(g); g = 0; - } - )c"); - ASSERT_TRUE(result.ast); - // RFC 0031 §6.1: `get`'s returned `p` is `g` after a possible publication - // (the entry value or a new box), which no value of the summary spells, so - // the caller cannot tell `a.p` is `g`: the use is not proven, but no - // longer a definite use after free (KNOWN-DIFFERENCES.md, *Unit tests*). - EXPECT_EQ(outcomeAt(result, "g->data[0]", core::Facet::Temporal), - "unresolved/may-alias-released"); - EXPECT_TRUE(noErrors(result)); -} - -TEST(HeapState, ATraversalAfterPublicationDoesNotGuardTheEarlierWrite) { - const auto result = analyze(R"c( - struct node { struct node *next; }; - struct box { char *data; }; - void reset(struct box *b, struct node *head) { - b->data = malloc(4); - for (struct node *n = head; n; n = n->next) {} - } - void good(void) { - struct node n = {0}; struct box b = {malloc(4)}; - free(b.data); reset(&b, &n); - if (b.data) b.data[3] = 0; - free(b.data); - } - void bad(void) { - struct node n = {0}; struct box b = {malloc(4)}; - free(b.data); reset(&b, &n); - if (b.data) b.data[4] = 0; - free(b.data); - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(ids(result.diagnostics), Strings{"out-of-bounds"}); -} - -TEST(HeapState, SwapBasedReplacementPreservesBothEntryValues) { - const auto result = analyze(R"c( - struct box { char *data; }; - void swap(struct box *a, struct box *b) { - char *old = a->data; a->data = b->data; b->data = old; - } - void reset(struct box *b) { - struct box n = {malloc(4)}; - swap(b, &n); free(n.data); - } - void good(void) { - struct box b = {malloc(8)}; - reset(&b); if (b.data) b.data[3] = 0; - free(b.data); - } - void bad(void) { - struct box b = {malloc(8)}; - reset(&b); if (b.data) b.data[4] = 0; - free(b.data); - } - void stale(void) { - struct box b = {malloc(8)}; if (!b.data) return; - char *old = b.data; - reset(&b); old[0] = 0; free(b.data); - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(ids(result.diagnostics), - (Strings{"out-of-bounds", "use-after-free"})); -} - -TEST(HeapState, ConsumptionOfCopiedInputsDoesNotConsumeOldDestinations) { - const auto result = analyze(R"c( - struct box { char *data; }; - void put(struct box *b, char *p) { b->data = p; } - void helper(struct box *a, struct box *b) { - put(b, a->data); free(a->data); - } - void good(void) { - struct box a = {malloc(4)}, b = {malloc(8)}; - char *old = b.data; helper(&a, &b); free(old); - } - void bad(void) { - struct box a = {malloc(4)}, b = {malloc(8)}; - char *old = b.data; helper(&a, &b); free(old); free(b.data); - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(ids(result.diagnostics), Strings{"double-free"}); -} - -TEST(HeapState, LocalAliasSwapsSnapshotBothIncomingCells) { - const auto result = analyze(R"c( - struct box { char *data; }; - void swap(struct box *a, struct box *b) { - struct box *x = a, *y = b; - char *old = x->data; x->data = y->data; y->data = old; - } - void good(void) { - struct box a = {malloc(4)}, b = {malloc(8)}; - swap(&a, &b); - if (a.data) a.data[7] = 0; - if (b.data) b.data[3] = 0; - free(a.data); free(b.data); - } - void bad(void) { - struct box a = {malloc(4)}, b = {malloc(8)}; - swap(&a, &b); if (b.data) b.data[4] = 0; - free(a.data); free(b.data); - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(ids(result.diagnostics), Strings{"out-of-bounds"}); -} - -TEST(HeapState, WritesThroughLocalAndEmbeddedAliasesAreFinalOutputs) { - const auto result = analyze(R"c( - struct box { char *data; }; struct outer { struct box box; }; - void initialize(struct outer *out) { - struct box *b = &out->box; b->data = malloc(4); - } - void replace(struct outer *out) { - struct outer *local = out; free(local->box.data); initialize(local); - } - void good(void) { - struct outer out; initialize(&out); replace(&out); - if (out.box.data) out.box.data[3] = 0; - free(out.box.data); - } - void bad(void) { - struct outer out; initialize(&out); - if (out.box.data) out.box.data[4] = 0; - free(out.box.data); - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(ids(result.diagnostics), Strings{"out-of-bounds"}); -} - -} // namespace weavec::analysis diff --git a/unittests/Analysis/IntegerSemanticsTest.cpp b/unittests/Analysis/IntegerSemanticsTest.cpp deleted file mode 100644 index c4201c51..00000000 --- a/unittests/Analysis/IntegerSemanticsTest.cpp +++ /dev/null @@ -1,623 +0,0 @@ -//===- IntegerSemanticsTest.cpp - Target C integers (RFC 0017) ------------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// - -#include "TestUtils.h" - -#include "gtest/gtest.h" - -using namespace weavec; -using namespace weavec::test; - -/// The spatial facet of the site spelled `text` at `line`, as its outcome, -/// with the reason when it is unresolved (`unresolved/unknown-extent`). -static std::string spatialAt(const AnalysisResult &result, unsigned line, - std::string_view text) { - for (const core::UnitLedger &unit : result.ledger.units) - for (const core::FunctionLedger &function : unit.functions) - for (const core::Site &site : function.sites) - if (site.location.line == line && site.text == text) - if (const core::FacetRecord *record = - site.facet(core::Facet::Spatial)) { - std::string out(core::toString(record->outcome())); - if (record->outcome() == core::SiteOutcome::Unresolved) - out += "/" + std::string(record->decision.reasonText()); - return out; - } - return "none"; -} - -TEST(IntegerSemantics, WrappedMemoryCopyDoesNotOverwriteTheUntouchedCell) { - const auto result = analyze(R"c( - void *memcpy(void *, const void *, size_t); - void bad(void) { - int *p=malloc(4); if(!p)return; - int *a[1]={0}, *b[1]={p}; - size_t n=(size_t)-1/sizeof(p)+1; - memcpy(b,a,n*sizeof(p)); - free(p); *b[0]=1; - } - void good(void) { - int *p=malloc(4); if(!p)return; - int *a[1]={0}, *b[1]={p}; - memcpy(b,a,sizeof(p)); - free(p); if(b[0]) *b[0]=1; - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(std::ranges::count(ids(result.diagnostics), - std::string(core::diag::UseAfterFree)), - 1); - EXPECT_EQ(result.diagnostics.size(), 1U) - << ::testing::PrintToString(messages(result.diagnostics)); -} - -TEST(IntegerSemantics, IncrementOutputsAndPostfixIndicesPreserveEntryValues) { - const auto result = analyze(R"c( - unsigned old(unsigned n) { return n++; } - unsigned next(unsigned n) { return ++n; } - struct index { unsigned n; }; - void put(char *p, struct index *i) { p[i->n++] = 0; } - void bad(void) { char p[2]; struct index i={2}; put(p,&i); } - void good(void) { - char p[2]; struct index i={1}; put(p,&i); - int *q=malloc(4); if(!q)return; - free(q); - if(old(4294967295u)!=4294967295u || next(4294967295u)!=0) *q=1; - } - )c"); - ASSERT_TRUE(result.ast); - // RFC 0030 §7.5: an index read from a field is none of R1-R5; but the - // context of `put(p,&i)` stores to `p[2]`, past `p` (RFC 0031 - // *Implementation amendments*, "Stores past the caller's object"). - EXPECT_EQ(messages(result.diagnostics), - (std::vector{ - "6: 'put' requires 3 bytes behind 'p', which has 2 bytes"})); - EXPECT_EQ(std::ranges::count(ids(result.diagnostics), - std::string(core::diag::UseAfterFree)), - 0); - ASSERT_TRUE(result.summary("old")); - // RFC 0031 §6.1: a format-30 summary carries no extent requirement; the - // access stays unresolved in `put`'s own unit, never proven. At link the - // context `bad` asks of `put` stores past `p`, and `good` gets no finding. - static constexpr const char *Callee = R"c( - struct index { unsigned n; }; - void put(char *p, struct index *i) { p[i->n++] = 0; } - )c"; - const auto callee = analyze(Callee); - ASSERT_TRUE(callee.ast); - EXPECT_EQ(spatialAt(callee, 3, "p[i->n++]"), "unresolved/unknown-extent"); - const auto linked = analyzeAtLink(Callee, R"c( - struct index { unsigned n; }; - void put(char *p, struct index *i); - void good(void) { char p[2]; struct index i={1}; put(p,&i); } - void bad(void) { char p[2]; struct index i={2}; put(p,&i); } - )c"); - ASSERT_TRUE(linked.ast); - EXPECT_EQ(std::ranges::count(ids(linked.diagnostics), - std::string(core::diag::OutOfBounds)), - 1); -} - -static unsigned countId(const AnalysisResult &result, std::string_view id) { - return static_cast( - std::ranges::count_if(result.diagnostics.diagnostics(), - [id](const core::Diagnostic &diagnostic) { - return diagnostic.id == id; - })); -} - -TEST(IntegerSemantics, NarrowingCannotHideATemporalError) { - const auto result = analyze(R"c( - void bad(unsigned n) { - if (n != 256) return; - unsigned char k = n; - int *p = malloc(sizeof *p); if (!p) return; - free(p); if (k == 0) *p = 1; - } - void good(unsigned n) { - if (n != 256) return; - unsigned char k = n; - int *p = malloc(sizeof *p); if (!p) return; - free(p); if (k != 0) *p = 1; - } - void boolean(unsigned n) { - if (n != 256) return; - _Bool k = n; - int *p = malloc(sizeof *p); if (!p) return; - free(p); if (k == 0) *p = 1; - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 1U); -} - -TEST(IntegerSemantics, - IncrementUsesStorageWidthAndDoesNotInventAPositiveCount) { - const auto result = analyze(R"c( - void bad(unsigned n) { - if (n != 4294967295u) return; - n++; - int *p = malloc(sizeof *p); if (!p) return; - free(p); if (n == 0) *p = 1; - } - void good(unsigned char n) { - if (n != 255) return; - ++n; - int *p = malloc(sizeof *p); if (!p) return; - free(p); if (n != 0) *p = 1; - } - void signed_bad(int n) { if (n == 2147483647) ++n; } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 1U); - EXPECT_EQ(countId(result, core::diag::InvalidIntegerOperation), 1U); -} - -TEST(IntegerSemantics, InvalidArithmeticHasAnExactDiagnostic) { - const auto result = analyze(R"c( - int add(int n) { if (n != 2147483647) return 0; return n + 1; } - int divzero(int n) { if (n != 0) return 0; return 2 / n; } - int divoverflow(int n) { if (n != (-2147483647 - 1)) return 0; return n / -1; } - int shift(int n) { if (n != 32) return 0; return 1 << n; } - int negshift(int n) { if (n != -1) return 0; return n << 1; } - unsigned wrap(unsigned n) { return n + 1; } - int unknown(int n) { return n + 1; } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(messages(result.diagnostics), - (std::vector{ - "2: invalid integer operation: signed integer overflow", - "3: invalid integer operation: division by zero", - "4: invalid integer operation: signed division overflow", - "5: invalid integer operation: invalid shift count", - "6: invalid integer operation: invalid signed left shift"})); -} - -TEST(IntegerSemantics, ActualConvertedAllocationSizeIsUsed) { - const auto result = analyze(R"c( - void bad(unsigned n) { - if (n != 256) return; - char *p = malloc((unsigned char)n); - if (p) { p[1] = 0; free(p); } - } - void good(unsigned n) { - if (n != 257) return; - char *p = malloc((unsigned char)n); - if (p) { p[0] = 0; free(p); } - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::OutOfBounds), 1U); -} - -TEST(IntegerSemantics, RepeatedProductsAndVlaBoundsAreNotLost) { - const auto result = analyze(R"c( - void product(unsigned rows, unsigned cols) { - char *p = malloc(rows * cols); - if (p) { p[rows * cols] = 0; free(p); } - } - void vla(unsigned n) { if (!n) return; char a[n]; a[n] = 0; } - void good(unsigned n) { if (!n) return; char a[n]; a[n - 1] = 0; } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::OutOfBounds), 2U); -} - -TEST(IntegerSemantics, ReturnedAndOutputValuesUseTheirDeclaredTypes) { - const auto result = analyze(R"c( - unsigned char narrow(unsigned n) { return n; } - void output(unsigned n, unsigned *r) { *r = (unsigned char)n; } - void returned(unsigned n) { - if (n != 256) return; - int *p = malloc(4); if (!p) return; - free(p); if (narrow(n) == 0) *p = 1; - } - void good(unsigned n) { - if (n != 256) return; - unsigned r; output(n, &r); - int *p = malloc(4); if (!p) return; - free(p); if (r != 0) *p = 1; - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 1U); - ASSERT_TRUE(result.summary("narrow")); -} - -TEST(IntegerSemantics, BitfieldStorageWidthIsNotThePromotedExpressionWidth) { - const auto result = analyze(R"c( - struct bits { unsigned n : 3; }; - void bad(void) { - struct bits b; b.n = 8; - int *p = malloc(4); if (!p) return; - free(p); if (b.n == 0) *p = 1; - } - void good(void) { - struct bits b; b.n = 7; b.n++; - int *p = malloc(4); if (!p) return; - free(p); if (b.n != 0) *p = 1; - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 1U); -} - -TEST(IntegerSemantics, NarrowingConditionsRemainConditionalAcrossCalls) { - const auto result = analyze(R"c( - void maybe_free(int *p, unsigned n) { if ((unsigned char)n == 0) free(p); } - void good(void) { - int *p = malloc(4); if (!p) return; - maybe_free(p, 257); *p = 1; free(p); - } - void bad(void) { - int *p = malloc(4); if (!p) return; - maybe_free(p, 256); *p = 1; - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 1U); - EXPECT_EQ(countId(result, core::diag::DoubleFree), 0U); -} - -// A minimum and a guarded access are none of the §7.5 rules, and RFC 0031 -// §6.1's summaries carry no extent requirement: the accesses stay -// unresolved in their own unit, never proven. At link the contexts of -// `wrapper(b,3,3)` and `conditional(b,2,3)` store past `b` (RFC 0031 -// *Implementation amendments*, "Stores past the caller's object"), and the -// correct calls get no finding. -TEST(IntegerSemantics, - MinimumAndConditionalRequirementsComposeThroughWrappers) { - static constexpr const char *Callees = R"c( - void fill(char *p, unsigned n, unsigned cap) { - for (unsigned i = 0; i < n && i < cap; ++i) p[i] = 0; - } - void wrapper(char *p, unsigned n, unsigned cap) { fill(p, n, cap); } - void conditional(char *p, unsigned n, unsigned m) { if (n < m) p[n] = 0; } - )c"; - const auto callees = analyze(Callees); - ASSERT_TRUE(callees.ast); - EXPECT_EQ(spatialAt(callees, 3, "p[i]"), "unresolved/unknown-extent"); - EXPECT_EQ(spatialAt(callees, 6, "p[n]"), "unresolved/unknown-extent"); - const auto result = analyzeAtLink(Callees, R"c( - void wrapper(char *p, unsigned n, unsigned cap); - void conditional(char *p, unsigned n, unsigned m); - void good(void) { char b[2]; wrapper(b,20,2); conditional(b,2,2); } - void bad(void) { char b[2]; wrapper(b,3,3); } - void also_bad(void) { char b[2]; conditional(b,2,3); } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::OutOfBounds), 2U) - << ::testing::PrintToString(messages(result.diagnostics)); -} - -TEST(IntegerSemantics, FlexibleTailKeepsItsBoundsAndEnclosingLifetime) { - const auto result = analyze(R"c( - struct v { unsigned n; int a[]; }; - void bounds_bad(void) { - struct v *p = malloc(sizeof *p + 2*sizeof(int)); if (!p) return; - int *q = p->a; q[2] = 0; free(p); - } - void good(void) { - struct v *p = malloc(sizeof *p + 2*sizeof(int)); if (!p) return; - int *q = p->a; q[1] = 0; free(p); - } - void lifetime_bad(void) { - struct v *p = malloc(sizeof *p + 2*sizeof(int)); if (!p) return; - int *q = p->a; free(p); q[0] = 0; - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::OutOfBounds), 1U); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 1U); -} - -TEST(IntegerSemantics, CompoundAssignmentsComputeBeforeStorageConversion) { - const auto result = analyze(R"c( - void bad(void) { - unsigned char n = 250; n += 6; - int *p = malloc(4); if (!p) return; - free(p); if (n == 0) *p = 1; - } - void good(void) { - unsigned n = 4294967295u; n *= 2; - n >>= 1; n ^= 2147483647u; - int *p = malloc(4); if (!p) return; - free(p); if (n != 0) *p = 1; - } - void invalid(void) { int n = 2147483647; n += 2; } - void bitfield(void) { - struct { unsigned n : 3; } b; b.n = 7; - unsigned old = b.n++; - int *p = malloc(4); if (!p) return; - free(p); if (old != 7 || b.n != 0) *p = 1; - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 1U) - << ::testing::PrintToString(messages(result.diagnostics)); - EXPECT_EQ(countId(result, core::diag::InvalidIntegerOperation), 1U); -} - -TEST(IntegerSemantics, SwitchUsesTheConvertedFullWidthValue) { - const auto result = analyze(R"c( - void bad(unsigned long long n) { - if (n != 18446744073709551615ull) return; - int *p = malloc(4); if (!p) return; free(p); - switch (n) { case -1: *p = 1; break; default: break; } - } - void good(unsigned long long n) { - if (n != 18446744073709551615ull) return; - int *p = malloc(4); if (!p) return; free(p); - switch (n) { case -1: break; default: *p = 1; } - } - void narrow(unsigned n) { - if (n != 256) return; - int *p = malloc(4); if (!p) return; free(p); - switch ((unsigned char)n) { case 0: break; default: *p = 1; } - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 1U) - << ::testing::PrintToString(messages(result.diagnostics)); -} - -TEST(IntegerSemantics, NestedVlaDimensionsAndSizeofUseDeclarationValues) { - const auto result = analyze(R"c( - void bad(unsigned rows, unsigned cols) { - if (!rows || !cols) return; - int a[rows][cols]; - a[rows][0] = 1; a[0][cols] = 1; - } - void captured(void) { - unsigned rows = 2, cols = 3; - int a[rows][cols]; rows = 8; cols = 9; - int *p = malloc(4); if (!p) return; free(p); - if (sizeof a != 24 || sizeof a[0] != 12) *p = 1; - a[1][2] = 1; a[1][3] = 1; - } - void good(unsigned n, unsigned m) { - if (!n || !m) return; - int a[n][m]; a[n-1][m-1] = 0; - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::OutOfBounds), 3U) - << ::testing::PrintToString(messages(result.diagnostics)); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 0U); -} - -TEST(IntegerSemantics, CheckedAllocatorsCannotSucceedWithAWrappedProduct) { - const auto result = analyze(R"c( - void *calloc(unsigned long, unsigned long); - void *reallocarray(void *, unsigned long, unsigned long); - void good(void) { - int *p = malloc(4); if (!p) return; free(p); - char *q = calloc((unsigned long)-1, 2); - if (q) { *p = 1; free(q); } - } - void preserves(void) { - int *p = malloc(4); if (!p) return; - int *q = reallocarray(p, (unsigned long)-1, 2); - if (!q) { *p = 1; free(p); } - } - void product(unsigned long n, unsigned long m) { - char *p = calloc(n,m); if (!p) return; - p[n*m] = 0; free(p); - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 0U); - EXPECT_EQ(countId(result, core::diag::UseAfterMove), 0U); - EXPECT_EQ(countId(result, core::diag::OutOfBounds), 1U) - << ::testing::PrintToString(messages(result.diagnostics)); -} - -TEST(IntegerSemantics, - OverflowBuiltinsStoreTheConvertedValueAndReturnOverflow) { - const auto result = analyze(R"c( - void bad(void) { - unsigned char out; - int overflow = __builtin_add_overflow(255u, 1u, &out); - int *p = malloc(4); if (!p) return; free(p); - if (overflow && out == 0) *p = 1; - } - void good(void) { - unsigned long long n = 18446744073709551615ull; - int overflow = __builtin_add_overflow(n, -1, &n); - int *p = malloc(4); if (!p) return; free(p); - if (overflow || n != 18446744073709551614ull) *p = 1; - } - int product(unsigned n, unsigned m, unsigned *out) { - return __builtin_mul_overflow(n,m,out); - } - void helper(void) { - unsigned out; - int overflow = product(65536,65536,&out); - int *p = malloc(4); if (!p) return; free(p); - if (!overflow || out != 0) *p = 1; - } - void checked(unsigned n, unsigned m) { - unsigned size; - if (__builtin_mul_overflow(n,m,&size)) return; - char *p = malloc(size); if (p) { p[size] = 1; free(p); } - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 1U) - << ::testing::PrintToString(messages(result.diagnostics)); - EXPECT_EQ(countId(result, core::diag::OutOfBounds), 1U); - EXPECT_EQ(countId(result, core::diag::InvalidIntegerOperation), 0U); -} - -TEST(IntegerSemantics, FullWidthUnsignedIndicesDoNotBecomeNegativeOrUnknown) { - const auto result = analyze(R"c( - void bad(void) { - char a[4]; a[18446744073709551615ull] = 0; - } - void bounded(unsigned long long n) { - if (n != 18446744073709551615ull) return; - int *p = malloc(16); if (!p) return; p[n] = 0; free(p); - } - void good(void) { char a[4]; a[(unsigned char)18446744073709551615ull & 3] = 0; } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::OutOfBounds), 2U); -} - -// RFC 0017 §5: a requirement's count bounds an index from above only, so -// `put`'s `counted(i + 1)` (RFC 0030 §7.5 R5) covers no access `p[i]` of a -// signed `i`: `wrap(a, -1)` meets it and writes before `a`. The access -// stays unresolved, never trusted. At link the context of `wrap(a, -1)` -// stores before `a` (RFC 0031 *Implementation amendments*, "Stores past -// the caller's object"); the correct calls get no finding. -TEST(IntegerSemantics, RequirementsRetainTheFirstAccessedByte) { - static constexpr const char *Callees = R"c( - void put(char *p, int i) { p[i] = 1; } - void wrap(char *p, int i) { put(p, i); } - )c"; - const auto callees = analyze(Callees); - ASSERT_TRUE(callees.ast); - EXPECT_EQ(spatialAt(callees, 2, "p[i]"), "unresolved/unknown-extent"); - EXPECT_EQ(spatialAt(callees, 3, "put(p,i)"), "unresolved/unknown-extent"); - const auto result = analyzeAtLink(Callees, R"c( - void wrap(char *p, int i); - void good(void) { char a[4]; wrap(a + 1, -1); } - void *memset(void *, int, size_t); - void empty(void) { char a[4]; memset(a, 0, 0); } - void bad(void) { char a[4]; wrap(a, -1); } - )c"); - EXPECT_EQ( - messages(result.diagnostics), - (std::vector{"6: 'wrap' requires 'a' before its start"})); -} - -TEST(IntegerSemantics, CheckedProductGuardsPreserveMathematicalBounds) { - const auto result = analyze(R"c( - void guarded(int n, int m) { - if (n < 0 || m <= 0 || n > 2147483647 / m) return; - int size = n * m; - int *p = malloc(sizeof *p); if (!p) return; - free(p); if (size < 0) *p = 1; - } - void builtin(int n, int m) { - if (n < 0 || m < 0) return; - int size; - if (__builtin_mul_overflow(n, m, &size)) return; - int *p = malloc(sizeof *p); if (!p) return; - free(p); if (size < 0) *p = 1; - } - void wrapped(unsigned n, unsigned m) { - if (n != 65536 || m != 65536) return; - unsigned size = n * m; - int *p = malloc(sizeof *p); if (!p) return; - free(p); if (size == 0) *p = 1; - } - )c"); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 1U); - EXPECT_EQ(countId(result, core::diag::InvalidIntegerOperation), 0U); -} - -TEST(IntegerSemantics, ReassignedInputsRetainTheirEntryIdentity) { - const auto result = analyze(R"c( - unsigned char old(unsigned n) { - unsigned saved = n; n = 1; return saved; - } - void bad(void) { - int *p = malloc(sizeof *p); if (!p) return; - free(p); if (old(256) == 0) *p = 1; - } - void good(void) { - int *p = malloc(sizeof *p); if (!p) return; - free(p); if (old(256) != 0) *p = 1; - } - )c"); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 1U); -} - -TEST(IntegerSemantics, FlexibleTailAndElementOffsetsSurviveReturns) { - const auto result = analyze(R"c( - struct block { int count; double alignment; int data[]; }; - static int *tail(void) { - struct block *b = malloc(sizeof *b + 2 * sizeof(int)); - if (!b) return NULL; - return b->data; - } - void bad(void) { int *p = tail(); if (p) { p[2] = 1; } } - void good(void) { int *p = tail(); if (p) { p[1] = 1; } } - void shifted(void) { - struct block *b = malloc(sizeof *b + 2 * sizeof(int)); - if (!b) return; - int *q = b->data + 1; q[1] = 1; free(b); - } - void small(void) { - struct block *b = malloc(4); if (!b) return; - int *q = b->data; q[0] = 1; free(b); - } - struct fixed { int pad; int data[2]; }; - void fixed_bound(void) { - struct fixed *b = malloc(100); if (!b) return; - b->data[2] = 1; free(b); - } - )c"); - ASSERT_TRUE(result.ast); - // RFC 0030 §7.4: `data[2]` is the trailing member, flexible at - // -fstrict-flex-arrays=0 whatever its bound, so `fixed_bound` stays inside - // its 100 bytes; `bad`, `shifted` and `small` are past theirs. - EXPECT_EQ(countId(result, core::diag::OutOfBounds), 3U) - << ::testing::PrintToString(messages(result.diagnostics)); -} - -TEST(IntegerSemantics, SizedFieldInferenceRetainsModularMultiplication) { - const auto result = analyze(R"c( - struct vec { int *items; size_t cap; }; - void init(struct vec *v, size_t n) { - v->items = malloc(n * sizeof *v->items); v->cap = n; - } - void bad(void) { - struct vec v; init(&v, (size_t)1 << 62); - if (v.items) v.items[0] = 1; - free(v.items); - } - void good(void) { - struct vec v; init(&v, 2); - if (v.items) v.items[1] = 1; - free(v.items); - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::OutOfBounds), 1U); -} - -TEST(IntegerSemantics, AbstractEndpointsAreNotReachableBoundaryWitnesses) { - const auto result = analyze(R"c( - void unknown(int n) { - char a[8]; for (int i=0; i=0 && i<=8) a[i]=0; } - void definite(unsigned i) { char a[8]; if(i>=8) a[i]=0; } - )c"); - ASSERT_TRUE(result.ast); - // RFC 0030 §3.3: `explicit_bound` may reach index 8 (a checked facet); - // `definite` is past the end for every value. - EXPECT_EQ(countId(result, core::diag::OutOfBounds), 1U) - << ::testing::PrintToString(messages(result.diagnostics)); -} - -TEST(IntegerSemantics, ExhaustedExpressionsRetainExplicitMissingCoverage) { - const auto result = analyze(R"c( - void large(size_t n) { - char *p = malloc(n+n+n+n+n+n+n+n+n+n+n+n+n+n+n+n+n); - if (p) { p[0]=0; free(p); } - } - )c"); - ASSERT_TRUE(result.ast); - // RFC 0030 §15 item 3: the summary records the gap, and the allocation - // whose size the engine could not build decides nothing about `p[0]`. - EXPECT_EQ(countId(result, core::diag::OutOfBounds), 0U); -} diff --git a/unittests/Analysis/KindInferenceTest.cpp b/unittests/Analysis/KindInferenceTest.cpp deleted file mode 100644 index 6aaf4676..00000000 --- a/unittests/Analysis/KindInferenceTest.cpp +++ /dev/null @@ -1,658 +0,0 @@ -//===- KindInferenceTest.cpp - Tests for KindInference (RFC 0030) ---------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// RFC 0030 §7.3–§7.6 before the engine: parameter defaults and reliance, -// static joins, `argv`, results, slot kinds and their demotions, must-access -// requirements R1–R5, store groups and §7.6 candidates. -// -//===----------------------------------------------------------------------===// - -#include "weavec/Analysis/KindInference.h" - -#include "SiteTestUtils.h" -#include "weavec/Analysis/AttributeReader.h" -#include "weavec/Analysis/KindTable.h" -#include "weavec/Analysis/SiteCollector.h" -#include "weavec/Analysis/SlotCollector.h" -#include "weavec/Core/LibrarySpec.h" - -#include "clang/Frontend/ASTUnit.h" -#include "clang/Tooling/Tooling.h" - -#include - -#include -#include -#include -#include - -namespace weavec::analysis { -namespace { - -/// A parsed unit with its declared, then inferred, kinds. Kept in place: the -/// result refers to the table. -struct InferredUnit { - std::unique_ptr ast; - KindTable kinds; - std::unique_ptr slots; - core::SlotSolution solution; - KindInferenceResult inferred; - - [[nodiscard]] clang::ASTContext &context() const { - return ast->getASTContext(); - } - [[nodiscard]] const clang::FunctionDecl * - function(llvm::StringRef name) const { - const clang::FunctionDecl *found = nullptr; - for (const clang::Decl *decl : context().getTranslationUnitDecl()->decls()) - if (const auto *fn = llvm::dyn_cast(decl); - fn != nullptr && fn->getName() == name) { - found = fn; - if (fn->doesThisDeclarationHaveABody()) - return fn; - } - return found; - } - [[nodiscard]] const clang::FieldDecl *field(llvm::StringRef record, - llvm::StringRef name) const { - for (const clang::Decl *decl : context().getTranslationUnitDecl()->decls()) - if (const auto *tag = llvm::dyn_cast(decl); - tag != nullptr && tag->getName() == record && - tag->isThisDeclarationADefinition()) - for (const clang::FieldDecl *each : tag->fields()) - if (each->getName() == name) - return each; - return nullptr; - } - [[nodiscard]] const clang::VarDecl *variable(llvm::StringRef name) const { - for (const clang::Decl *decl : context().getTranslationUnitDecl()->decls()) - if (const auto *var = llvm::dyn_cast(decl); - var != nullptr && var->getName() == name) - return var; - return nullptr; - } - - /// ` `, `+relies` when set, or `none`. - [[nodiscard]] static std::string spell(const KindEntry *entry) { - if (entry == nullptr) - return "none"; - return entry->kind.toString() + " " + - std::string(core::toString(entry->kind.source)) + - (entry->reliesOnSingle ? " +relies" : ""); - } - [[nodiscard]] std::string param(llvm::StringRef name, unsigned index) const { - const clang::FunctionDecl *fn = function(name); - return fn == nullptr ? "no function" : spell(kinds.param(*fn, index)); - } - [[nodiscard]] std::string result(llvm::StringRef name) const { - const clang::FunctionDecl *fn = function(name); - return fn == nullptr ? "no function" : spell(kinds.result(*fn)); - } - [[nodiscard]] std::string slot(llvm::StringRef record, - llvm::StringRef name) const { - const clang::FieldDecl *decl = field(record, name); - return decl == nullptr ? "no field" : spell(kinds.field(*decl)); - } - /// The reasons `record.name` was demoted. - [[nodiscard]] std::vector demotions(llvm::StringRef record, - llvm::StringRef name) const { - std::vector out; - const clang::FieldDecl *decl = field(record, name); - const KindEntry *entry = decl != nullptr ? kinds.field(*decl) : nullptr; - if (entry != nullptr) - for (const KindDemotion &demotion : entry->demotedBy) - out.push_back(demotion.reason); - return out; - } - /// The requirements of a parameter, spelled. - [[nodiscard]] std::vector requirements(llvm::StringRef name, - unsigned index) const { - std::vector out; - const clang::FunctionDecl *fn = function(name); - const KindEntry *entry = fn != nullptr ? kinds.param(*fn, index) : nullptr; - if (entry != nullptr) - for (const MustAccessRequirement &requirement : entry->mustAccess) - out.push_back(requirement.toString()); - return out; - } -}; - -} // namespace - -/// Parses `code` after the site tests' prelude and runs AttributeReader, -/// SlotCollector and KindInference over it. -static std::unique_ptr -inferUnit(const std::string &code, KindInferenceOptions options = {}) { - auto unit = std::make_unique(); - unit->ast = clang::tooling::buildASTFromCodeWithArgs( - std::string(test::SitePrelude) + code, {"-std=gnu17", "-x", "c", "-w"}, - "input.c"); - EXPECT_NE(unit->ast, nullptr); - if (unit->ast == nullptr) - return unit; - EXPECT_FALSE(unit->ast->getDiagnostics().hasErrorOccurred()); - const core::LibrarySpec &library = core::LibrarySpec::shipped(); - unit->kinds = AttributeReader(unit->context(), library).read(); - unit->slots = std::make_unique( - SlotCollector(unit->context(), library).collect()); - unit->solution = unit->slots->solve(); - options.slots = unit->slots.get(); - options.slotSolution = &unit->solution; - unit->inferred = - KindInference(unit->context(), library, options).infer(unit->kinds); - return unit; -} - -/// §7.6: the unit's candidates, sorted. -static std::vector -fieldCandidates(const KindInferenceResult &inferred) { - std::vector out; - for (const ResolvedCandidate &resolved : inferred.resolvedCandidates()) - out.push_back(resolved.candidate); - std::ranges::sort(out); - return out; -} - -namespace { - -// §7.3: A1 defaults, and the uses that rest on them. -TEST(KindInference, ParameterDefaultsAndReliance) { - const auto unit = inferUnit(R"c( -struct s { int *p; int n; }; -int deref(int *p) { return *p; } -int member(struct s *o) { return o->n; } -int stored(struct s *o, int *v) { o->p = v; return 0; } -int *returned(int *v) { return v; } -int compared(int *a, int *b) { return a < b && a != 0 && !b; } -int arithmetic(int *a) { return *(a + 1); } -int indexed(int *a, int i) { return a[i] + a[2]; } -int zero(int *a) { return a[0]; } -int sized(int *a) { return (int)sizeof(*a); } -)c"); - EXPECT_EQ(unit->param("deref", 0), "single nullable default +relies"); - EXPECT_EQ(unit->param("member", 0), "single nullable default +relies"); - EXPECT_EQ(unit->param("stored", 1), "single nullable default +relies"); - EXPECT_EQ(unit->param("returned", 0), "single nullable default +relies"); - EXPECT_EQ(unit->param("compared", 0), "single nullable default"); - EXPECT_EQ(unit->param("compared", 1), "single nullable default"); - EXPECT_EQ(unit->param("arithmetic", 0), "single nullable default"); - EXPECT_EQ(unit->param("indexed", 0), "single nullable default"); - EXPECT_EQ(unit->param("zero", 0), "single nullable default +relies"); - EXPECT_EQ(unit->param("sized", 0), "single nullable default"); - const KindEntry *entry = unit->kinds.param(*unit->function("deref"), 0); - ASSERT_NE(entry, nullptr); - EXPECT_EQ(entry->extentClass, core::ExtentClass::LowerBound); - EXPECT_FALSE(entry->hasDeclaredShape()); -} - -// §7.3: a static function whose address is not taken joins its arguments. -TEST(KindInference, StaticParametersJoinTheirArguments) { - const auto unit = inferUnit(R"c( -static int objects(int *p) { return p != 0; } -static int cursors(int *p) { return p != 0; } -static int mixed(int *p) { return p != 0; } -static int never(int *p) { return p != 0; } -static int taken(int *p) { return p != 0; } -static int chained(int *p) { return objects(p); } -int run(int *in, int (*out)(int *)) { - int x = 0; - int a[4]; - out = taken; - return objects(&x) + objects(a) + cursors(a + 1) + mixed(in) + mixed(0) + - chained(&x) + out(in); -} -)c"); - // `&x`, an array, and `chained`'s parameter, itself `&x`: all nonnull. - EXPECT_EQ(unit->param("objects", 0), "single nonnull inferred"); - EXPECT_EQ(unit->param("chained", 0), "single nonnull inferred"); - EXPECT_EQ(unit->param("cursors", 0), "unknown nullable inferred"); - EXPECT_EQ(unit->param("mixed", 0), "single nullable inferred"); - EXPECT_EQ(unit->param("never", 0), "single nullable default"); - EXPECT_EQ(unit->param("taken", 0), "single nullable default"); - // `run` passes its parameter on: that rests on its default. - EXPECT_EQ(unit->param("run", 0), "single nullable default +relies"); - const KindEntry *cursor = unit->kinds.param(*unit->function("cursors"), 0); - ASSERT_NE(cursor, nullptr); - ASSERT_EQ(cursor->demotedBy.size(), 1U); - EXPECT_EQ(cursor->demotedBy.front().reason, "pointer arithmetic"); -} - -// §7.3: `argv` of `main`, while `argc` and `argv` are never assigned. -TEST(KindInference, MainArgv) { - const auto kept = - inferUnit("int main(int argc, char **argv) { return argc; }"); - const KindEntry *argv = kept->kinds.param(*kept->function("main"), 1); - ASSERT_NE(argv, nullptr); - EXPECT_TRUE(argv->mainArgv); - EXPECT_EQ(argv->kind.toString(), "counted(param 0 scale 1 plus 1) nonnull"); - EXPECT_EQ(argv->shapeLevel, KindLevel::SystemHeader); - EXPECT_TRUE(argv->shapeFromSystemHeader()); - const auto moved = - inferUnit("int main(int argc, char **argv) { argv++; return argc; }"); - const KindEntry *shifted = moved->kinds.param(*moved->function("main"), 1); - ASSERT_NE(shifted, nullptr); - EXPECT_FALSE(shifted->mainArgv); - EXPECT_EQ(moved->param("main", 1), "single nullable default"); -} - -// §7.3: results over the unit's `return`s; A3 for the unit's declarations. -TEST(KindInference, Results) { - const auto unit = inferUnit(R"c( -struct s { int a, b; }; -static struct s one; -struct s *object(void) { return &one; } -struct s *maybe(int c) { return c ? &one : 0; } -int *cursor(int *p) { return p + 1; } -struct s *outside(void); -struct s *relay(void) { return outside(); } -char *text(void) { return "abc"; } -struct s *allocate(void) { return malloc(sizeof(struct s)); } -struct s *small(void) { return malloc(2); } -struct s *recurse(int n) { return n ? recurse(n - 1) : &one; } -int use(void) { return outside() != 0; } -)c"); - EXPECT_EQ(unit->result("object"), "single nonnull inferred"); - EXPECT_EQ(unit->result("maybe"), "single nullable inferred"); - EXPECT_EQ(unit->result("cursor"), "unknown nullable inferred"); - EXPECT_EQ(unit->result("outside"), "single nullable default"); - EXPECT_EQ(unit->result("relay"), "single nullable inferred"); - EXPECT_EQ(unit->result("text"), "single nonnull inferred"); - EXPECT_EQ(unit->result("allocate"), "single nullable inferred"); - EXPECT_EQ(unit->result("small"), "unknown nullable inferred"); - // A greatest fixpoint: the recursion does not demote itself. - EXPECT_EQ(unit->result("recurse"), "single nonnull inferred"); -} - -// §7.3: Single-valid stores keep a slot `single`; anything else demotes it, -// and the demotion spreads through loads (a greatest fixpoint). -TEST(KindInference, SlotKindsFromStores) { - const auto unit = inferUnit(R"c( -struct node { - struct node *next; - struct node *prev; - char *text; - int *cursor; - int *copy; - int *far; - int *from_int; - struct node *made; - struct node *cast_small; -}; -static int buffer[8]; -int *global_one = &buffer[3]; -int *global_end = buffer + 8; -void build(struct node *n, int *in, unsigned long bits) { - struct node *fresh = malloc(sizeof *fresh); - void *raw = malloc(sizeof(struct node)); - short small[1]; - n->next = fresh; - n->prev = n->next; - n->text = "label"; - n->cursor = in + 1; - n->copy = n->cursor; - n->far = n->copy; - n->from_int = (int *)bits; - n->made = (struct node *)raw; - n->cast_small = (struct node *)small; - n->next = n->prev; -} -)c"); - EXPECT_EQ(unit->slot("node", "next"), "single nullable inferred"); - EXPECT_EQ(unit->slot("node", "prev"), "single nullable inferred"); - EXPECT_EQ(unit->slot("node", "text"), "single nullable inferred"); - EXPECT_EQ(unit->slot("node", "cursor"), "unknown nullable inferred"); - EXPECT_EQ(unit->demotions("node", "cursor"), - std::vector{"pointer arithmetic"}); - EXPECT_EQ(unit->slot("node", "copy"), "unknown nullable inferred"); - EXPECT_EQ( - unit->demotions("node", "copy"), - std::vector{"a load from 'node.cursor', which is unknown"}); - EXPECT_EQ(unit->slot("node", "far"), "unknown nullable inferred"); - EXPECT_EQ(unit->slot("node", "from_int"), "unknown nullable inferred"); - EXPECT_EQ(unit->demotions("node", "from_int"), - std::vector{"a conversion from an integer"}); - // A local carries the least width stored into it. - EXPECT_EQ(unit->slot("node", "made"), "single nullable inferred"); - EXPECT_EQ(unit->slot("node", "cast_small"), "unknown nullable inferred"); - const clang::VarDecl *one = unit->variable("global_one"); - const clang::VarDecl *end = unit->variable("global_end"); - ASSERT_NE(one, nullptr); - ASSERT_NE(end, nullptr); - EXPECT_EQ(InferredUnit::spell(unit->kinds.variable(*one)), - "single nullable inferred"); - EXPECT_EQ(InferredUnit::spell(unit->kinds.variable(*end)), - "unknown nullable inferred"); -} - -// §7.3: stores the syntax does not show. -TEST(KindInference, HiddenStoresDemote) { - const auto unit = inferUnit(R"c( -struct box { int *a; int *b; char *c; }; -struct raw { int *p; }; -struct zero { int *p; }; -struct same { int *p; }; -union u { int *p; long n; }; -void *memset(void *, int, size_t); -void external(int **where); -static void keep(int **where, int *v) { *where = v; } -void run(struct box *x, struct raw *r, struct zero *z, struct same *s, - const struct same *t, union u *v, int *in, const char *bytes) { - int **through = &x->a; - *through = in + 2; - keep(&x->b, in); - external(&x->b); - memcpy(r, bytes, sizeof *r); - memset(z, 0, sizeof *z); - memcpy(s, t, sizeof *s); - v->n = 3; - ((char *)&x->c)[0] = 1; -} -)c"); - EXPECT_EQ(unit->demotions("box", "a"), - std::vector{"pointer arithmetic"}); - // `keep` only stores Single-valid values through its parameter; the - // external callee may store anything. - EXPECT_EQ(unit->demotions("box", "b"), - (std::vector{"pointer arithmetic", - "its address is passed to 'external'"})); - // `c` is address-taken too, and a `char *` slot is compatible with any - // store through a pointer. - EXPECT_EQ(unit->demotions("box", "c"), - (std::vector{"pointer arithmetic", - "a character-typed store into it"})); - EXPECT_EQ(unit->demotions("raw", "p"), - std::vector{"a byte-wise write by 'memcpy'"}); - EXPECT_EQ(unit->slot("zero", "p"), "single nullable inferred"); - EXPECT_EQ(unit->slot("same", "p"), "single nullable inferred"); - EXPECT_EQ(unit->demotions("u", "p"), - std::vector{ - "a store to 'u.n', another member of the union"}); -} - -// §7.3: a pointer to pointers made from anything but a fresh allocation may -// point into any object; an address converted to a pointer to non-pointers -// may be overwritten with other bytes. -TEST(KindInference, PunnedPointersToSlots) { - const auto punned = inferUnit(R"c( -struct cell { int *p; }; -struct other { int *q; }; -void pun(struct cell *c, void *any, int *in) { - int **slot = any; - *slot = in + 1; - *(unsigned long *)&c->p = 4; -} -)c"); - EXPECT_EQ(punned->demotions("cell", "p"), - (std::vector{ - "pointer arithmetic", - "its address is converted to 'unsigned long *'"})); - EXPECT_EQ(punned->demotions("other", "q"), - std::vector{"pointer arithmetic"}); - const auto fresh = inferUnit(R"c( -struct cell { int *p; }; -void fill(int *in) { - int **slots = malloc(4 * sizeof(int *)); - slots[0] = in + 1; -} -)c"); - EXPECT_EQ(fresh->slot("cell", "p"), "single nullable inferred"); -} - -// §7.5: R1–R5, their guards, and how each is enforced. -TEST(KindInference, MustAccessRules) { - const auto unit = inferUnit(R"c( -struct w { int x0; }; -int r1m(struct w *p) { return p->x0; } -void r2(int *p, int n) { for (int i = 1; i < n; ++i) p[i - 1] = 0; } -void r2e(int *p, unsigned n) { for (unsigned i = 0; i != 2 * n; i++) p[i] = 0; } -long r3(const long *p, const long *q) { - long s = 0; - for (const long *x = p; x <= q; x++) s += *x; - return s; -} -unsigned long r4(const char *s) { const char *c = s; while (*c) c++; return c - s; } -unsigned long r4f(const char *s) { unsigned long n; for (n = 0; s[n] != 0; n++) {} return n; } -unsigned long r4l(const char *s) { return strlen(s); } -int r5(const int *p, int n) { return p[2 * n + 1]; } -int r5c(const int *p) { return p[7]; } -static int local(const int *p) { return *p; } -int call(const int *p) { return local(p); } -)c"); - EXPECT_EQ(unit->requirements("r1m", 0), - std::vector{"single nonnull (R1)"}); - EXPECT_EQ(unit->requirements("r2", 0), - std::vector{ - "1 < param 1 scale 1 plus 0 -> counted(param 1 scale 1 plus " - "-1) nonnull (R2)"}); - EXPECT_EQ(unit->requirements("r2e", 0), - std::vector{ - "0 < param 1 scale 2 plus 0 -> counted(param 1 scale 2 plus " - "0) nonnull (R2)"}); - EXPECT_EQ(unit->requirements("r3", 0), - std::vector{ - "param 0 scale 1 plus 0 <= param 1 scale 1 plus 0 -> " - "ended-by(param 1 scale 1 plus 1) nonnull (R3)"}); - EXPECT_EQ(unit->requirements("r4", 0), - std::vector{"nul-terminated nonnull (R4)"}); - EXPECT_EQ(unit->requirements("r4f", 0), - std::vector{"nul-terminated nonnull (R4)"}); - EXPECT_EQ(unit->requirements("r4l", 0), - std::vector{"nul-terminated nonnull (R4)"}); - EXPECT_EQ( - unit->requirements("r5", 0), - std::vector{"counted(param 1 scale 2 plus 2) nonnull (R5)"}); - EXPECT_EQ(unit->requirements("r5c", 0), - std::vector{"counted(8) nonnull (R5)"}); - const KindEntry *local = unit->kinds.param(*unit->function("local"), 0); - ASSERT_NE(local, nullptr); - EXPECT_EQ(local->enforcement, RequirementEnforcement::CallSites); - EXPECT_TRUE(local->hasEnforcedRequirement()); - ASSERT_EQ(local->mustAccess.size(), 1U); - EXPECT_NE(local->mustAccess.front().access, nullptr); - const KindEntry *exported = unit->kinds.param(*unit->function("r1m"), 0); - ASSERT_NE(exported, nullptr); - EXPECT_EQ(exported->enforcement, RequirementEnforcement::CallerContract); - EXPECT_FALSE(exported->hasEnforcedRequirement()); -} - -// §7.5: what keeps an access from being a must-access. -TEST(KindInference, MustAccessConditions) { - const auto unit = inferUnit(R"c( -void unknown(void); -static void helper(int *q) { (void)q; } -static void fatal(void) { exit(1); } -static void loops(void) { while (1) {} } -int tested(int *p) { if (p) return *p; return 0; } -int nulltest(int *p) { if (p == 0) exit(1); return *p; } -int early(int *p, int c) { if (c) return 0; return *p; } -int unknowns(int *p) { unknown(); return *p; } -int helped(int *p) { helper(p); return *p; } -int fatals(int *p, int c) { if (c) fatal(); return *p; } -int changed(int *p, int *q) { p = q; return *p; } -int taken(int *p) { int **pp = &p; (void)pp; return *p; } -int branched(int *p, int c) { return c ? *p : 1; } -int breaks(int *p, int n) { - int s = 0; - for (int i = 0; i < n; ++i) { if (s > 3) break; s += p[i]; } - return s; -} -int modified(int *p, int n) { - int s = 0; - for (int i = 0; i < n; ++i) { s += p[i]; i += 0; } - return s; -} -int infinite(int *p) { loops(); return *p; } -)c"); - for (const char *name : - {"tested", "nulltest", "early", "unknowns", "fatals", "changed", "taken", - "branched", "breaks", "modified"}) - EXPECT_TRUE(unit->requirements(name, 0).empty()) << name; - // A helper the unit defines that always returns keeps the must-access; - // one that never returns, or may exit, does not. - EXPECT_EQ(unit->requirements("helped", 0), - std::vector{"single nonnull (R1)"}); - EXPECT_EQ(unit->requirements("infinite", 0), - std::vector{"single nonnull (R1)"}); -} - -TEST(KindInference, FieldCandidates) { - const char *code = R"c( -struct vec { int *data; unsigned long len; unsigned long cap; char *name; }; -struct flat { int *p; int n; }; -struct holder { struct flat inner; int other; }; -struct view { int *p; int n; }; -struct outside { void *mem; int size; }; -void use(struct vec *v, struct holder *h, const char *raw, long *l, - struct outside *o) { - for (unsigned long i = 0; i < v->len; i++) - v->data[i] = 0; - memcpy(h, raw, sizeof *h); - struct view *w = (struct view *)l; - w->p = 0; - w->n = 0; - o->mem = malloc(o->size); -} -)c"; - const auto unit = inferUnit(code); - std::vector candidates; - for (const FieldCandidate &candidate : fieldCandidates(unit->inferred)) - candidates.push_back(candidate.record + " " + - (candidate.bytes ? "bytes(" : "count(") + - candidate.pointer + ") == " + candidate.count + " + " + - std::to_string(candidate.offset)); - EXPECT_EQ(candidates, - (std::vector{"struct outside bytes(mem) == size + 0", - "struct outside bytes(mem) == size + 1", - "struct vec count(data) == len + 0", - "struct vec count(data) == len + 1", - "struct vec bytes(data) == len + 0", - "struct vec bytes(data) == len + 1"})); - std::vector dropped; - for (const DisqualifiedRecord &record : unit->inferred.disqualified()) - dropped.push_back(record.key + ": " + record.reason); - EXPECT_EQ(dropped, - (std::vector{ - "struct flat: an object of 'flat' is written byte-wise (a " - "byte-wise write by 'memcpy')", - "struct view: an object of 'view' is converted from another " - "type"})); - // Without pruning, every pointer and integer field pair is a candidate. - KindInferenceOptions all; - all.pruneUnrelatedCandidates = false; - const auto unpruned = inferUnit(code, all); - EXPECT_EQ(fieldCandidates(unpruned->inferred).size(), 14U); - // The designated first cut: no candidates at all. - KindInferenceOptions cut; - cut.fieldCandidates = false; - EXPECT_TRUE(fieldCandidates(inferUnit(code, cut)->inferred).empty()); -} - -// §13.1 `imports.calls` and the §7.3 Call-site row: Single-valid arguments. -TEST(KindInference, SingleValidQueries) { - const auto unit = inferUnit(R"c( -struct pair { int a, b; }; -void take(struct pair *p); -void calls(struct pair *in, int *ints) { - struct pair here; - take(&here); - take(in); - take((struct pair *)ints); - take((struct pair *)(in + 1)); - take(0); -} -)c"); - std::vector valid; - for (const clang::CallExpr *call : [&] { - std::vector calls; - const auto *body = llvm::cast( - unit->function("calls")->getBody()); - for (const clang::Stmt *stmt : body->body()) - if (const auto *call = llvm::dyn_cast(stmt)) - calls.push_back(call); - return calls; - }()) - valid.push_back(unit->inferred.argumentIsSingleValid(*call, 0)); - EXPECT_EQ(valid, (std::vector{true, true, false, false, true})); -} - -// §9.3: an indirect call's result joins its closed targets' results. -TEST(KindInference, IndirectCallResults) { - const char *code = R"c( -struct s { int a; }; -static struct s one; -static struct s *get(void) { return &one; } -static struct s *(*getter)(void) = get; -struct holder { struct s *p; }; -void keep(struct holder *h) { h->p = getter(); } -)c"; - EXPECT_EQ(inferUnit(code)->slot("holder", "p"), "single nullable inferred"); - // Without the slots, an indirect call's result is never Single-valid. - const auto alone = std::make_unique(); - alone->ast = clang::tooling::buildASTFromCodeWithArgs( - code, {"-std=gnu17", "-x", "c", "-w"}, "input.c"); - ASSERT_NE(alone->ast, nullptr); - alone->kinds = - AttributeReader(alone->context(), core::LibrarySpec::shipped()).read(); - alone->inferred = - KindInference(alone->context(), core::LibrarySpec::shipped()) - .infer(alone->kinds); - EXPECT_EQ(alone->slot("holder", "p"), "unknown nullable inferred"); -} - -// The inferred entries leave the unit's sites as the declared ones made -// them: only declared shapes are required positions (§7.4 rule 4). -TEST(KindInference, SitesDoNotChange) { - const std::string code = R"c( -static int first(int *p) { return p[0]; } -int *cursor(int *p) { return p + 1; } -struct s { int *p; }; -void use(struct s *o, int *q) { - o->p = cursor(q) + 1; - (void)first(q + 2); -} -)c"; - const test::CollectedUnit before = test::collectUnit(code); - const auto after = inferUnit(code); - const SiteIndex sites = - SiteCollector(after->context(), after->kinds, - core::LibrarySpec::shipped(), &after->inferred) - .collect(); - const auto describe = [](const SiteIndex &index, llvm::StringRef function) { - std::vector out; - for (const core::FunctionLedger &row : index.ledgers()) { - if (row.name != function) - continue; - for (const core::Site &site : row.sites) { - std::string text(core::toString(site.kind)); - if (site.boundary) - text += "/" + test::boundaryText(*site.boundary); - text += " " + site.text; - for (const core::Facet facet : core::AllFacets) - if (site.hasFacet(facet)) - text += " " + std::string(core::toString(facet)); - out.push_back(text); - } - } - return out; - }; - for (const char *function : {"first", "cursor"}) { - EXPECT_FALSE(describe(sites, function).empty()) << function; - EXPECT_EQ(describe(sites, function), describe(before.sites, function)) - << function; - } - // §7.5: the static `first`'s requirement (`p[0]`, R1) gives its direct - // call spatial and null facets; no required position appears. - std::vector use = describe(before.sites, "use"); - ASSERT_EQ(use.size(), 4U); - EXPECT_EQ(use[2], "call/call first(q+2) temporal"); - use[2] = "call/call first(q+2) spatial null temporal"; - EXPECT_EQ(describe(sites, "use"), use); -} - -} // namespace -} // namespace weavec::analysis diff --git a/unittests/Analysis/LedgerAdapterTest.cpp b/unittests/Analysis/LedgerAdapterTest.cpp deleted file mode 100644 index 94101391..00000000 --- a/unittests/Analysis/LedgerAdapterTest.cpp +++ /dev/null @@ -1,449 +0,0 @@ -//===- LedgerAdapterTest.cpp - Tests for LedgerAdapter --------------------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// RFC 0030 §14: defaults (§2.6), merging (§2.5), discarding passes, the -// UNSAFE and setjmp rules, and diagnostics and their links. -// -//===----------------------------------------------------------------------===// - -#include "weavec/Analysis/LedgerAdapter.h" - -#include "SiteTestUtils.h" -#include "weavec/Analysis/ClangLocation.h" -#include "weavec/Analysis/UnitPipeline.h" - -#include "clang/AST/Expr.h" - -#include - -#include -#include - -namespace weavec::analysis { - -using test::collectUnit; -using Lines = std::vector; - -/// ` =[/]...` for every site of `function`. -static Lines outcomes(const core::Ledger &ledger, llvm::StringRef function) { - Lines out; - for (const core::FunctionLedger &row : ledger.units.front().functions) { - if (row.name != function) - continue; - for (const core::Site &site : row.sites) { - std::string line = site.text; - for (const core::Facet facet : core::AllFacets) - if (const core::FacetRecord *record = site.facet(facet)) - line += " " + std::string(core::toString(facet)) + "=" + - test::compactOf(record->decision); - out.push_back(line); - } - } - return out; -} - -static const SiteInfo &siteOf(const test::CollectedUnit &unit, - llvm::StringRef function, std::uint32_t ordinal) { - return unit.sites.function(*unit.function(function))->sites.at(ordinal); -} - -/// The pipeline over `unit`: its diagnostics, as `: : -/// []`, and its ledger. -namespace { -struct Piped { - Lines diagnostics; - core::Ledger ledger; -}; -} // namespace -static Piped pipe(const test::CollectedUnit &unit) { - core::DiagnosticCollector collected; - const UnitPipelineResult result = - runUnitAnalysis(unit.context(), UnitPipelineOptions{}, collected); - Piped out; - for (const core::Diagnostic &d : collected.diagnostics()) { - std::string line = std::to_string(d.location.line) + ": " + - test::severityText(d.severity) + ": " + d.message; - for (const core::Diagnostic ¬e : d.notes) - line += " [" + note.message + "]"; - out.diagnostics.push_back(std::move(line)); - } - if (result.ledger) - out.ledger = *result.ledger; - return out; -} - -namespace { - -// §2.6: the defaults of undecided facets, and of an over-budget function. -TEST(LedgerAdapter, FillsDefaults) { - const auto unit = collectUnit(R"c( -int f(int *p, int i, unsigned long a) { - int arr[4] = {0}; - ASSUME(i < 4); - return *p + arr[i] + p[i] + *(int *)a; -} -int g(int *p) { return *p; } -)c"); - LedgerAdapterOptions options; - options.source = "input.c"; - LedgerAdapter adapter(unit.context(), unit.sites, options); - adapter.overBudget(*unit.function("g")); - const core::Ledger ledger = adapter.finish(); - EXPECT_TRUE(core::completenessProblems(ledger).empty()); - EXPECT_EQ( - outcomes(ledger, "f"), - (Lines{"ASSUME(i<4) assertion=unresolved/unanalysed", - std::string("return*p+arr[i]+p[i]+*(int*)a ") + - "temporal=unresolved/unanalysed", - std::string("*p spatial=unresolved/unanalysed ") + - "null=unresolved/unanalysed temporal=unresolved/unanalysed", - "arr[i] spatial=unresolved/unanalysed", - std::string("p[i] spatial=unresolved/unanalysed ") + - "null=unresolved/unanalysed temporal=unresolved/unanalysed", - std::string("*(int*)a spatial=unresolved/unanalysed ") + - "null=unresolved/unanalysed temporal=unresolved/unanalysed", - std::string("(int*)a spatial=unresolved/raw-cast ") + - "temporal=unresolved/raw-cast"})); - EXPECT_EQ(outcomes(ledger, "g"), - (Lines{"return*p temporal=unresolved/budget", - std::string("*p spatial=unresolved/budget ") + - "null=unresolved/budget temporal=unresolved/budget"})); - const core::FunctionLedger &g = ledger.units.front().functions.at( - unit.sites.function(*unit.function("g"))->index); - EXPECT_TRUE(g.overBudget); - EXPECT_EQ(ledger.units.front().source, "input.c"); -} - -// §2.5: records merge by rank within a pass; `beginFunction` discards an -// earlier pass's rows; a discarding adapter keeps nothing. -TEST(LedgerAdapter, MergesByRankAndDiscardsEarlierPasses) { - const auto unit = collectUnit("int f(int *p) { return *p; }\n"); - const SiteInfo &deref = siteOf(unit, "f", 1); - ASSERT_EQ(deref.kind, core::SiteKind::Deref); - LedgerAdapter adapter(unit.context(), unit.sites); - const auto temporal = [&] { - return test::compactOf(adapter.unitLedger() - .functions.front() - .sites[1] - .facet(core::Facet::Temporal) - ->decision); - }; - adapter.beginFunction(*unit.function("f")); - adapter.decide(*deref.stmt, core::Facet::Temporal, core::SiteOutcome::Proven); - EXPECT_EQ(temporal(), "proven"); - adapter.decide(*deref.stmt, core::Facet::Temporal, - core::SiteOutcome::Unresolved, - core::UnresolvedReason::MayAliasReleased); - EXPECT_EQ(temporal(), "unresolved/may-alias-released"); - // Equal rank keeps the earlier record. - adapter.decide(*deref.stmt, core::Facet::Temporal, - core::SiteOutcome::Unresolved, - core::UnresolvedReason::UnknownCallee); - EXPECT_EQ(temporal(), "unresolved/may-alias-released"); - adapter.decide(*deref.stmt, core::Facet::Temporal, - core::SiteOutcome::Violation); - EXPECT_EQ(temporal(), "violation"); - // A new authoritative pass starts from scratch. - adapter.beginFunction(*unit.function("f")); - adapter.decide(*deref.stmt, core::Facet::Temporal, core::SiteOutcome::Proven); - EXPECT_EQ(temporal(), "proven"); - - LedgerAdapter discarding(unit.context(), unit.sites, {}, - LedgerAdapter::Mode::Discarding); - EXPECT_TRUE(discarding.isDiscarding()); - discarding.beginFunction(*unit.function("f")); - discarding.decide(*deref.stmt, core::Facet::Temporal, - core::SiteOutcome::Violation); - discarding.report(core::Diagnostic{.severity = core::Severity::Error, - .id = core::diag::UseAfterFree, - .message = "x", - .location = {}, - .notes = {}, - .fixits = {}}, - core::Certainty::Definite); - EXPECT_TRUE(discarding.diagnostics().empty()); - EXPECT_TRUE(discarding.finish().units.empty()); -} - -// Diagnostics keep their order and link to the site and facet they are -// about. -TEST(LedgerAdapter, ReportsAndLinksDiagnostics) { - const auto unit = collectUnit("int f(int *p) { return *p; }\n"); - const SiteInfo &deref = siteOf(unit, "f", 1); - LedgerAdapter adapter(unit.context(), unit.sites); - adapter.beginFunction(*unit.function("f")); - const core::SourceLocation at = toCoreLocation( - unit.context().getSourceManager(), deref.stmt->getBeginLoc()); - core::Diagnostic second{.severity = core::Severity::Warning, - .id = core::diag::Leak, - .message = "second", - .location = at, - .notes = {}, - .fixits = {}}; - core::Diagnostic first{.severity = core::Severity::Error, - .id = core::diag::UseAfterFree, - .message = "use of 'p' after it was freed", - .location = at, - .notes = {}, - .fixits = {}}; - first.addNote("freed here", at); - adapter.report(first, core::Certainty::Definite, deref.stmt, - core::Facet::Temporal); - adapter.decide(*deref.stmt, core::Facet::Temporal, - core::SiteOutcome::Violation); - adapter.report(second, core::Certainty::Possible); - ASSERT_EQ(adapter.diagnostics().size(), 2U); - EXPECT_EQ(adapter.diagnostics()[0].message, "use of 'p' after it was freed"); - EXPECT_EQ(adapter.diagnostics()[1].message, "second"); - const core::Ledger ledger = adapter.finish(); - ASSERT_EQ(ledger.diagnostics.size(), 2U); - const core::LedgerDiagnostic &linked = ledger.diagnostics[1]; - EXPECT_EQ(linked.id, "use-after-free"); - EXPECT_EQ(linked.function, "f"); - EXPECT_EQ(linked.site, std::optional(1)); - EXPECT_EQ(linked.facet, std::optional(core::Facet::Temporal)); - ASSERT_EQ(linked.notes.size(), 1U); - EXPECT_EQ(ledger.diagnostics[0].function, "f"); - EXPECT_EQ(ledger.diagnostics[0].certainty, core::Certainty::Possible); - // Sorting kept the facet's link on the right diagnostic. - const core::FacetRecord *temporal = - ledger.units.front().functions.front().sites[1].facet( - core::Facet::Temporal); - EXPECT_EQ(temporal->diagnostic, std::optional(1)); - EXPECT_TRUE(core::completenessProblems(ledger).empty()); -} - -// §2.5: the facet takes the highest-ranked of its requirements' decisions. -TEST(LedgerAdapter, RequirementRecords) { - const auto unit = collectUnit(R"c( -void f(char *d, const char *s, unsigned long n) { memcpy(d, s, n); } -)c"); - const SiteInfo © = siteOf(unit, "f", 0); - ASSERT_EQ(copy.kind, core::SiteKind::LibCall); - LedgerAdapter adapter(unit.context(), unit.sites); - adapter.beginFunction(*unit.function("f")); - adapter.requirement(*copy.stmt, core::Facet::Spatial, - core::FacetDecision::proven()); - adapter.requirement(*copy.stmt, core::Facet::Spatial, - core::FacetDecision::unresolvedFor( - core::UnresolvedReason::UnknownExtent)); - const core::Ledger ledger = adapter.finish(); - const core::FacetRecord *spatial = - ledger.units.front().functions.front().sites[0].facet( - core::Facet::Spatial); - EXPECT_EQ(test::compactOf(spatial->decision), "unresolved/unknown-extent"); -} - -// §6.1 and §5.4, as ledger-side rules. -TEST(LedgerAdapter, UnsafeAndSetjmpRules) { - const auto unit = collectUnit(R"c( -jmp_buf env; -int f(int *p, int *RAW r) { - UNSAFE { *p = 1; *r = 2; ASSUME(*p == 1); } - return *p; -} -int g(int *p) { - if (setjmp(env)) - return 0; - return *p; -} -)c"); - LedgerAdapter adapter(unit.context(), unit.sites); - // The engine proved `*p`'s null facet in `g` from flow facts. - const SiteInfo &guarded = siteOf(unit, "g", 3); - ASSERT_EQ(guarded.kind, core::SiteKind::Deref); - adapter.beginFunction(*unit.function("g")); - adapter.decide(*guarded.stmt, core::Facet::Null, core::SiteOutcome::Proven); - const core::Ledger ledger = adapter.finish(); - EXPECT_EQ( - outcomes(ledger, "f"), - (Lines{std::string("*p spatial=trusted/unsafe null=trusted/unsafe ") + - "temporal=unresolved/unanalysed", - std::string("*r spatial=trusted/unsafe null=trusted/unsafe ") + - "temporal=trusted/unsafe", - std::string("*p spatial=trusted/unsafe null=trusted/unsafe ") + - "temporal=unresolved/unanalysed", - "ASSUME(*p==1) assertion=unresolved/unanalysed", - "return*p temporal=unresolved/unanalysed", - std::string("*p spatial=unresolved/unanalysed ") + - "null=unresolved/unanalysed temporal=unresolved/unanalysed"})); - EXPECT_EQ(outcomes(ledger, "g"), - (Lines{"setjmp(env)", "return0 temporal=unresolved/setjmp", - "return*p temporal=unresolved/setjmp", - std::string("*p spatial=unresolved/unanalysed ") + - "null=unresolved/setjmp temporal=unresolved/setjmp"})); - EXPECT_TRUE(core::completenessProblems(ledger).empty()); -} - -// §2.6 item 2: a decision about a statement no site stands for is an -// internal error: an assertion in debug builds, an `unresolved(unanalysed)` -// row otherwise. -TEST(LedgerAdapter, OrphanDecisions) { - const auto unit = collectUnit("int f(int *p) { return p != 0; }\n"); - const auto *ret = llvm::cast(siteOf(unit, "f", 0).stmt); - const clang::Expr *comparison = ret->getRetValue(); - EXPECT_DEBUG_DEATH( - { - LedgerAdapter adapter(unit.context(), unit.sites); - adapter.beginFunction(*unit.function("f")); - adapter.decide(*comparison, core::Facet::Null, - core::SiteOutcome::Proven); - const core::Ledger ledger = adapter.finish(); - EXPECT_EQ(outcomes(ledger, "f").back(), - "p!=0 null=unresolved/unanalysed"); - }, - "did not enumerate"); -} - -// The unit pipeline reports the engine's diagnostics in the engine's order -// and returns a complete ledger. -TEST(UnitPipeline, ReportsEngineDiagnosticsAndBuildsTheLedger) { - const auto unit = collectUnit(R"c( -int f(void) { - int *p = malloc(sizeof *p); - if (!p) - return 0; - free(p); - return *p; -} -)c"); - core::DiagnosticCollector collected; - UnitPipelineOptions options; - const UnitPipelineResult result = - runUnitAnalysis(unit.context(), options, collected); - ASSERT_EQ(collected.size(), 1U); - EXPECT_EQ(collected.diagnostics().front().id, core::diag::UseAfterFree); - ASSERT_NE(result.ledger, nullptr); - const core::Ledger &ledger = *result.ledger; - EXPECT_TRUE(core::completenessProblems(ledger).empty()); - ASSERT_EQ(ledger.diagnostics.size(), 1U); - EXPECT_EQ(ledger.diagnostics.front().function, "f"); - EXPECT_EQ(ledger.diagnostics.front().facet, - std::optional(core::Facet::Temporal)); - EXPECT_TRUE(result.exports.functions.contains("f")); - - UnitPipelineOptions discovery; - discovery.discoverOnly = true; - core::DiagnosticCollector none; - const UnitPipelineResult discovered = - runUnitAnalysis(unit.context(), discovery, none); - EXPECT_EQ(discovered.ledger, nullptr); - EXPECT_TRUE(none.empty()); -} - -// -- RFC 0030 §3: certainty, severity and the engine's decisions ------------ - -TEST(EngineCertainty, TemporalFindingsAreDefiniteOrPossible) { - const auto unit = collectUnit(R"c( -void g(char *p) { free(p); p[0] = 1; } -void h(char *p, int c) { if (c) free(p); p[0] = 1; } -void twice(char *p, int c) { if (c) free(p); free(p); } -void fine(char *p) { p[0] = 1; free(p); } -)c"); - const Piped piped = pipe(unit); - EXPECT_EQ(piped.diagnostics, - (Lines{"2: error: use of 'p' after it was freed [freed here]", - "3: warning: use of 'p' after it may have been freed " - "[freed here on some paths]", - "4: warning: 'p' may be freed twice [previously freed here " - "on some paths]"})); - // RFC 0030 §7.3: `p` is Single by A1, which covers `p[0]`. - EXPECT_EQ(outcomes(piped.ledger, "g")[1], - "p[0] spatial=proven null=unresolved/undecided temporal=violation"); - EXPECT_EQ(outcomes(piped.ledger, "h")[1], - "p[0] spatial=proven null=unresolved/undecided " - "temporal=unresolved/may-released"); - // §15 item 4: where a parameter points in its allocation is not known. - EXPECT_EQ(outcomes(piped.ledger, "twice")[1], - "free(p) spatial=unresolved/unknown-index " - "temporal=unresolved/may-released"); - EXPECT_EQ( - outcomes(piped.ledger, "fine"), - (Lines{"p[0] spatial=proven null=unresolved/undecided temporal=proven", - "free(p) spatial=unresolved/unknown-index temporal=proven", - "} temporal=proven"})); - ASSERT_EQ(piped.ledger.diagnostics.size(), 3U); - EXPECT_EQ(piped.ledger.diagnostics[0].certainty, core::Certainty::Definite); - EXPECT_EQ(piped.ledger.diagnostics[1].certainty, core::Certainty::Possible); - EXPECT_EQ(piped.ledger.diagnostics[1].facet, - std::optional(core::Facet::Temporal)); -} - -TEST(EngineCertainty, NullFindingsAreDefiniteOnly) { - const auto unit = collectUnit(R"c( -struct node { int v; }; -int k(struct node *n) { if (n == 0) return n->v; return 0; } -void m(void) { char *p = malloc(4); p[0] = 1; free(p); } -int q(char *p) { return p[0]; } -int r(void) { char *p = malloc(4); if (!p) return 0; int c = p[0]; free(p); return c; } -)c"); - const Piped piped = pipe(unit); - // `allocation-failure` is off by default; the engine reports it and the - // frontend drops it unless enabled. - EXPECT_EQ(piped.diagnostics, - (Lines{"3: error: dereference of 'n', which is null ['n' may be " - "null: it is compared with NULL here]", - "4: warning: the result of 'malloc' is used without a null " - "test; it is null when allocation fails [allocated here]"})); - EXPECT_EQ(outcomes(piped.ledger, "k")[1], - "n->v spatial=proven null=violation temporal=proven"); - EXPECT_EQ(outcomes(piped.ledger, "m")[1], - "p[0] spatial=proven null=unresolved/undecided temporal=proven"); - EXPECT_EQ(outcomes(piped.ledger, "q")[1], - "p[0] spatial=proven null=unresolved/undecided temporal=proven"); - EXPECT_EQ(outcomes(piped.ledger, "r")[2], - "p[0] spatial=proven null=proven temporal=proven"); -} - -TEST(EngineCertainty, SpatialFindingsAreDefiniteOnlyAgainstExactExtents) { - const auto unit = collectUnit(R"c( -void e(void) { char *p = malloc(4); if (!p) return; p[4] = 0; free(p); } -void loop(void) { char b[4]; for (int i = 0; i < 8; i++) b[i] = 0; } -void declared(char *SIZED_BY(n) p, size_t n) { p[n] = 0; } -)c"); - const Piped piped = pipe(unit); - EXPECT_EQ(piped.diagnostics, - (Lines{"2: error: 'p[4]' is out of bounds: index 4 of an object of " - "4 bytes ['p' is allocated here]"})); - EXPECT_EQ(outcomes(piped.ledger, "e")[2], - "p[4] spatial=violation null=proven temporal=proven"); - EXPECT_EQ(outcomes(piped.ledger, "loop")[0], - "b[i] spatial=unresolved/undecided"); - EXPECT_EQ(outcomes(piped.ledger, "declared")[0], - "p[n] spatial=unresolved/undecided null=unresolved/undecided " - "temporal=proven"); -} - -TEST(EngineCertainty, ContextFindingsLinkToTheCall) { - const auto unit = collectUnit(R"c( -static void two(char *a, char *b) { free(a); b[0] = 1; } -void caller(void) { char *p = malloc(4); if (!p) return; two(p, p); } -)c"); - const Piped piped = pipe(unit); - EXPECT_EQ(piped.diagnostics, - (Lines{"2: error: use of 'b' after it was freed [freed here " - "(through 'a')] [called here with related pointer " - "arguments]"})); - // The callee's own site is decided by its generic pass: `b` may alias the - // released `a` (RFC 0030 §3.1, `may-alias-released`); the context's - // finding is the call's. §7.5: `b[0]` is a must-access of the static - // `two`, whose call checks it, so its spatial and null facets are proven - // there and at the call. - EXPECT_EQ(outcomes(piped.ledger, "two")[1], - "b[0] spatial=proven null=proven " - "temporal=unresolved/may-alias-released"); - EXPECT_EQ(outcomes(piped.ledger, "caller")[2], - "two(p,p) spatial=proven null=proven temporal=violation"); - ASSERT_EQ(piped.ledger.diagnostics.size(), 1U); - EXPECT_EQ(piped.ledger.diagnostics[0].function, "caller"); - EXPECT_EQ(piped.ledger.diagnostics[0].facet, - std::optional(core::Facet::Temporal)); -} - -} // namespace -} // namespace weavec::analysis diff --git a/unittests/Analysis/LoopRequirementsTest.cpp b/unittests/Analysis/LoopRequirementsTest.cpp deleted file mode 100644 index 602c9590..00000000 --- a/unittests/Analysis/LoopRequirementsTest.cpp +++ /dev/null @@ -1,270 +0,0 @@ -//===- LoopRequirementsTest.cpp - Loop requirements (RFC 0017) ------------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// - -#include "TestUtils.h" - -#include -#include - -#include "gtest/gtest.h" - -using namespace weavec; -using namespace weavec::test; - -static unsigned countId(const AnalysisResult &result, std::string_view id) { - return static_cast( - std::ranges::count_if(result.diagnostics.diagnostics(), - [id](const core::Diagnostic &diagnostic) { - return diagnostic.id == id; - })); -} - -/// The spatial facet of the site spelled `text` at `line`, as its outcome, -/// with the reason when it is unresolved (`unresolved/unknown-extent`). -static std::string spatialAt(const AnalysisResult &result, unsigned line, - std::string_view text) { - for (const core::UnitLedger &unit : result.ledger.units) - for (const core::FunctionLedger &function : unit.functions) - for (const core::Site &site : function.sites) - if (site.location.line == line && site.text == text) - if (const core::FacetRecord *record = - site.facet(core::Facet::Spatial)) { - std::string out(core::toString(record->outcome())); - if (record->outcome() == core::SiteOutcome::Unresolved) - out += "/" + std::string(record->decision.reasonText()); - return out; - } - return "none"; -} - -static void expectIncompleteLoop(const AnalysisResult &result, - const char *name) { - SCOPED_TRACE(name); -} - -TEST(LoopRequirements, EarlyExitsDoNotRequireAnUnreachedMaximum) { - const auto result = analyze(R"c( - void before(char *p, unsigned n) { - for (unsigned i = 0; i < n; i++) { - if (i == 2) break; - p[i] = 0; - } - } - void after(char *p, unsigned n) { - for (unsigned i = 0; i < n; ++i) { - p[i] = 0; - if (i == 2) break; - } - } - void returns(char *p, unsigned n) { - for (unsigned i = 0; i < n; ++i) { - if (i == 2) return; - p[i] = 0; - } - } - void jumps(char *p, unsigned n) { - for (unsigned i = 0; i < n; ++i) { - if (i == 2) goto done; - p[i] = 0; - } - done:; - } - void skips(char *p, unsigned n) { - for (unsigned i = 0; i < n; ++i) { - if (i >= 2) continue; - p[i] = 0; - } - } - void callers(void) { - char two[2], three[3]; - before(two, 10); after(three, 10); returns(two, 10); - jumps(two, 10); skips(two, 10); - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::OutOfBounds), 0U) - << ::testing::PrintToString(messages(result.diagnostics)); - for (const auto *name : {"before", "after", "returns", "jumps", "skips"}) - expectIncompleteLoop(result, name); -} - -TEST(LoopRequirements, NoncanonicalInitializationAndStrideAreIncomplete) { - const auto result = analyze(R"c( - void nonzero(char *p, unsigned n) { - for (unsigned i = 8; i < n; ++i) p[i] = 0; - } - void stride(char *p, unsigned n) { - for (unsigned i = 0; i < n; i += 3) p[i] = 0; - } - void decrement(char *p, unsigned n) { - for (unsigned i = 0; i < n; --i) p[i] = 0; - } - void missing_init(char *p, unsigned n) { - unsigned i = 0; - for (; i < n; ++i) p[i] = 0; - } - void callers(void) { - char one[1]; nonzero(one, 2); stride(one, 3); decrement(one, 2); - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::OutOfBounds), 0U) - << ::testing::PrintToString(messages(result.diagnostics)); - for (const auto *name : {"nonzero", "stride", "decrement", "missing_init"}) - expectIncompleteLoop(result, name); -} - -TEST(LoopRequirements, MutatedAndEscapedControlsAreIncomplete) { - const auto result = analyze(R"c( - void mutate_index(char *p, unsigned n) { - for (unsigned i = 0; i < n; ++i) { p[i] = 0; i += 2; } - } - void mutate_bound(char *p, unsigned n) { - for (unsigned i = 0; i < n; ++i) { p[i] = 0; n = 1; } - } - void escape_index(char *p, unsigned n) { - unsigned i, *cursor = &i; - for (i = 0; i < n; ++i) { p[i] = 0; *cursor += 2; } - } - void escape_bound(char *p, unsigned n) { - unsigned *count = &n; - for (unsigned i = 0; i < n; ++i) { p[i] = 0; *count = 1; } - } - void callers(void) { - char one[1]; mutate_index(one, 2); mutate_bound(one, 10); - escape_index(one, 2); escape_bound(one, 10); - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::OutOfBounds), 0U) - << ::testing::PrintToString(messages(result.diagnostics)); - for (const auto *name : - {"mutate_index", "mutate_bound", "escape_index", "escape_bound"}) - expectIncompleteLoop(result, name); -} - -TEST(LoopRequirements, UnsupportedBodyControlDoesNotAcquireAMustRequirement) { - const auto result = analyze(R"c( - void conditional(char *p, unsigned n) { - for (unsigned i = 0; i < n; ++i) if (i < 2) p[i] = 0; - } - void nested(char *p, unsigned n) { - for (unsigned i = 0; i < n; ++i) - while (i < 2) { p[i] = 0; break; } - } - void call(char *p, unsigned n) { - for (unsigned i = 0; i < n; ++i) { cond(); p[i] = 0; } - } - void reused(char *p, unsigned n) { - unsigned i; - for (i = 0; i < n; ++i) p[i] = 0; - for (i = 0; i < n; i += 2) p[i] = 0; - } - void outside(char *p, unsigned n) { - unsigned i; - for (i = 0; i < n; ++i) {} - p[i] = 0; - } - )c"); - ASSERT_TRUE(result.ast); - for (const auto *name : - {"conditional", "nested", "call", "reused", "outside"}) - expectIncompleteLoop(result, name); -} - -TEST(LoopRequirements, IncrementMustFitTheIndexStorageType) { - const auto result = analyze(R"c( - void narrow(char *p, unsigned n) { - for (unsigned char i = 0; i < n; ++i) p[i] = 0; - } - void signed_narrow(char *p, unsigned n) { - for (signed char i = 0; i < n; ++i) p[i] = 0; - } - void inclusive(char *p, unsigned n) { - for (unsigned i = 0; i <= n; ++i) p[i] = 0; - } - void truncated(char *p, unsigned n) { - for (unsigned i = 0; (unsigned char)i < n; ++i) p[i] = 0; - } - )c"); - ASSERT_TRUE(result.ast); - for (const auto *name : {"narrow", "signed_narrow", "inclusive", "truncated"}) - expectIncompleteLoop(result, name); -} - -TEST(LoopRequirements, CanonicalMinimaStillComposeThroughWrappers) { - static constexpr const char *Callees = R"c( - void fill(char *p, unsigned n, unsigned cap) { - for (unsigned i = 0; i < n && i < cap; ++i) p[i] = 0; - } - void wrapper(char *p, unsigned n, unsigned cap) { fill(p, n, cap); } - void explicit_min(char *p, unsigned n, unsigned cap) { - for (unsigned i = 0; i < (n < cap ? n : cap); i++) p[i] = 0; - } - void constant_min(char *p, unsigned n) { - for (unsigned i = 0; i < n && i < 2; i++) p[i] = 0; - } - )c"; - const auto result = analyze(Callees); - ASSERT_TRUE(result.ast); - // A minimum is none of the §7.5 rules, so the callers' unit checks - // nothing, and RFC 0031 §6.1's summaries carry no extent requirement: the - // accesses stay unresolved in their own unit, never proven. At link the - // contexts of `wrapper(two, 3, 3)` and `explicit_min(two, 3, 3)` store - // past `two` (RFC 0031 *Implementation amendments*, "Stores past the - // caller's object"), and the correct calls get no finding. - EXPECT_EQ(spatialAt(result, 3, "p[i]"), "unresolved/unknown-extent"); - EXPECT_EQ(spatialAt(result, 7, "p[i]"), "unresolved/unknown-extent"); - EXPECT_EQ(spatialAt(result, 10, "p[i]"), "unresolved/unknown-extent"); - const auto linked = analyzeAtLink(Callees, R"c( - void wrapper(char *p, unsigned n, unsigned cap); - void explicit_min(char *p, unsigned n, unsigned cap); - void constant_min(char *p, unsigned n); - void good(void) { - char two[2]; wrapper(two, 10, 2); wrapper(two, 2, 10); - explicit_min(two, 10, 2); constant_min(two, 10); - } - void bad(void) { char two[2]; wrapper(two, 3, 3); explicit_min(two, 3, 3); } - )c"); - ASSERT_TRUE(linked.ast); - EXPECT_EQ(countId(linked, core::diag::OutOfBounds), 2U) - << ::testing::PrintToString(messages(linked.diagnostics)); - for (const auto *name : {"fill", "wrapper", "explicit_min", "constant_min"}) { - SCOPED_TRACE(name); - } -} - -TEST(LoopRequirements, CanonicalDeclarationsAndAssignmentsKeepTheirBounds) { - const auto result = analyze(R"c( - void signed_index(char *p, int n) { - for (int i = 0; i < n; i++) p[i] = 0; - } - void assigned_index(char *p, unsigned n) { - unsigned i; - for (i = 0; i < n; ++i) p[i] = 0; - } - void inclusive_constant(char *p) { - for (unsigned i = 0; i <= 8; ++i) p[i] = 0; - } - void good(void) { - char two[2], nine[9]; signed_index(two, 2); assigned_index(two, 2); - inclusive_constant(nine); - } - void bad(void) { - char two[2], eight[8]; signed_index(two, 3); assigned_index(two, 3); - inclusive_constant(eight); - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::OutOfBounds), 3U) - << ::testing::PrintToString(messages(result.diagnostics)); - for (const auto *name : - {"signed_index", "assigned_index", "inclusive_constant"}) { - SCOPED_TRACE(name); - } -} diff --git a/unittests/Analysis/NumericInputsTest.cpp b/unittests/Analysis/NumericInputsTest.cpp deleted file mode 100644 index 79445e59..00000000 --- a/unittests/Analysis/NumericInputsTest.cpp +++ /dev/null @@ -1,245 +0,0 @@ -//===- NumericInputsTest.cpp - Call-entry sizes (RFC 0017) ----------------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// - -#include "TestUtils.h" - -#include "llvm/Support/raw_ostream.h" - -#include "gtest/gtest.h" - -using namespace weavec; -using namespace weavec::test; - -static unsigned countId(const AnalysisResult &result, std::string_view id) { - return static_cast(std::ranges::count(ids(result.diagnostics), id)); -} - -TEST(NumericInputs, AddressTakingPreservesTheNumericValue) { - const auto result = analyze(R"c( - void bad(void) { - unsigned n = 4; - unsigned *alias = &n; - char p[4]; - p[n] = 0; - (void)alias; - } - void good(void) { - unsigned n = 0; - unsigned *alias = &n; - char p[4]; - p[n] = 0; - (void)alias; - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::OutOfBounds), 1U) - << ::testing::PrintToString(messages(result.diagnostics)); -} - -TEST(NumericInputs, UnknownWritesStillInvalidateAddressTakenValues) { - const auto result = analyze(R"c( - void overwrite(unsigned *n); - void bad(void) { - unsigned n = 0; - overwrite(&n); - int *p = malloc(4); - if (!p) return; - free(p); - if (n == 1) *p = 0; - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 1U) - << ::testing::PrintToString(messages(result.diagnostics)); -} - -TEST(NumericInputs, ReallocatedFieldUsesTheCountBeforeTheCallWritesIt) { - const auto result = analyze(R"c( - struct vec { int *items; int cap; }; - int grow(struct vec *v) { - int *p = realloc(v->items, sizeof *p * (v->cap + 8)); - if (!p) return 0; - v->items = p; - v->cap += 8; - return 1; - } - void bad(void) { - struct vec v = {malloc(8 * sizeof(int)), 8}; - if (!grow(&v)) { free(v.items); return; } - v.items[15] = 0; - v.items[16] = 0; - free(v.items); - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::OutOfBounds), 1U) - << ::testing::PrintToString(messages(result.diagnostics)); -} - -TEST(NumericInputs, ReturnedProductUsesTheInputBeforeAnOutputWrite) { - const auto result = analyze(R"c( - char *make(unsigned *n) { - char *p = malloc(*n * 2u); - *n += 1; - return p; - } - void bad(void) { - unsigned n = 4; - char *p = make(&n); - if (!p) return; - p[7] = 0; - p[8] = 0; - free(p); - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::OutOfBounds), 1U) - << ::testing::PrintToString(messages(result.diagnostics)); -} - -TEST(NumericInputs, NumericOutputsAreComputedFromTheSameEntryState) { - const auto result = analyze(R"c( - void outputs(unsigned *a, unsigned *b) { - unsigned old = *a; - *a = 10; - *b = old * 2u; - } - void bad(void) { - unsigned a = 4, b = 0; - outputs(&a, &b); - char p[8]; - p[b - 1] = 0; - p[b] = 0; - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::OutOfBounds), 1U) - << ::testing::PrintToString(messages(result.diagnostics)); -} - -TEST(NumericInputs, ReturnedAllocationGuardsTestTheEntryField) { - const auto result = analyze(R"c( - struct size { unsigned n; }; - char *make(struct size *s, unsigned limit) { - if (s->n < limit) { - char *p = malloc(s->n * 2u); - s->n = limit; - return p; - } - return 0; - } - void bad(void) { - struct size s = {4}; - char *p = make(&s, 8); - if (!p) return; - p[7] = 0; - p[8] = 0; - free(p); - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::OutOfBounds), 1U) - << ::testing::PrintToString(messages(result.diagnostics)); -} - -TEST(NumericInputs, GlobalFieldDependenciesAreCapturedBeforeTheirWrites) { - const auto result = analyze(R"c( - struct size { unsigned n; }; - struct size *global; - char *make(void) { - char *p = malloc(global->n * 2u); - global->n += 1; - return p; - } - void bad(void) { - global = malloc(sizeof *global); - if (!global) return; - global->n = 4; - char *p = make(); - if (p) { - p[7] = 0; - p[8] = 0; - free(p); - } - free(global); - global = 0; - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::OutOfBounds), 1U) - << ::testing::PrintToString(messages(result.diagnostics)); -} - -TEST(NumericInputs, ReusedCallInputDoesNotResizeAnEarlierAllocation) { - const auto result = analyze(R"c( - char *make(unsigned n) { return malloc(n * 2u); } - void repeat(void) { - char *first = 0; - for (unsigned i = 0; i < 2; ++i) { - char *p = make(i + 1); - if (!p) { free(first); return; } - if (first) { - first[2] = 0; - free(first); - } - first = p; - } - free(first); - } - )c"); - ASSERT_TRUE(result.ast); - // At the only access, first is the earlier two-byte allocation. Widening - // can lose that extent, but it must not use the new four-byte allocation - // to prove this access safe. - EXPECT_EQ(facetCounts(result, core::Facet::Spatial).substr(0, 17), - "spatial: proven=0"); -} - -TEST(NumericInputs, AliasedOutputPathsRespectTheCalleesStatementOrder) { - const auto result = analyze(R"c( - void writes(unsigned *a, unsigned *b) { *b = 1; *a = 2; } - void bad(void) { - unsigned x; - writes(&x, &x); - int *badPointer = malloc(4); - if (!badPointer) return; - free(badPointer); - if (x == 2) *badPointer = 1; - } - void good(void) { - unsigned x; - writes(&x, &x); - int *goodPointer = malloc(4); - if (!goodPointer) return; - free(goodPointer); - if (x == 1) *goodPointer = 1; - } - )c"); - ASSERT_TRUE(result.ast); - ASSERT_EQ(result.diagnostics.size(), 1U) - << ::testing::PrintToString(messages(result.diagnostics)); - const auto &diagnostic = result.diagnostics.diagnostics().front(); - EXPECT_EQ(diagnostic.id, core::diag::UseAfterFree); - EXPECT_EQ(diagnostic.message, "use of 'badPointer' after it was freed"); -} - -TEST(NumericInputs, DuplicateProjectedFactsRetainNumericOutputs) { - const auto result = analyze(R"c( - void outputs(unsigned *a, unsigned *b) { *a = 2; *b = 2; } - void bad(void) { - unsigned a, b; - outputs(&a, &b); - char p[2]; - p[a - 1] = 0; - p[b] = 0; - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::OutOfBounds), 1U) - << ::testing::PrintToString(messages(result.diagnostics)); -} diff --git a/unittests/Analysis/PointerIdentityTest.cpp b/unittests/Analysis/PointerIdentityTest.cpp deleted file mode 100644 index 88fd8d5a..00000000 --- a/unittests/Analysis/PointerIdentityTest.cpp +++ /dev/null @@ -1,548 +0,0 @@ -//===- PointerIdentityTest.cpp - RFC 0014 pointer and call identities -----===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// - -#include "TestUtils.h" -#include "weavec/Analysis/ProgramDatabase.h" - -#include - -#include - -namespace weavec::analysis { - -static std::size_t countId(const test::AnalysisResult &result, - std::string_view id) { - return static_cast( - std::ranges::count_if(result.diagnostics.diagnostics(), - [id](const core::Diagnostic &diagnostic) { - return diagnostic.id == id; - })); -} - -/// `[/]: ` of `facet` at the site whose ledger text -/// is `text`, or empty. -static std::string decisionAt(const test::AnalysisResult &result, - std::string_view text, core::Facet facet) { - for (const core::UnitLedger &unit : result.ledger.units) - for (const core::FunctionLedger &function : unit.functions) - for (const core::Site &site : function.sites) { - const core::FacetRecord *record = site.facet(facet); - if (site.text != text || record == nullptr) - continue; - std::string out(core::toString(record->outcome())); - if (!record->decision.reasonText().empty()) - out += "/" + std::string(record->decision.reasonText()); - return out + ": " + record->decision.detail; - } - return {}; -} - -static constexpr const char *Callbacks = R"c( -static void drop(void *p) { free(p); } -static void keep(void *p) { (void)p; } -)c"; - -TEST(PointerIdentity, WideAliasQueriesPreserveReleaseAndReplacement) { - // RFC 0027: query-result storage must grow past its inline capacity. - // Every saved alias remains live across the release and optional write. - for (const unsigned width : {3U, 4U, 5U, 12U}) { - for (const bool replace : {false, true}) { - SCOPED_TRACE(width); - SCOPED_TRACE(replace); - std::string source = - "struct box { int *slot; }; void client(struct box *root) {"; - for (unsigned i = 0; i < width; ++i) - source += "struct box *a" + std::to_string(i) + "=root;"; - source += "free(a0->slot);"; - if (replace) - source += "a0->slot=malloc(sizeof(int)); if(!a0->slot)return;"; - for (unsigned i = 0; i < width; ++i) - source += "use(a" + std::to_string(i) + "->slot);"; - if (replace) - source += "free(a0->slot);"; - source += '}'; - const auto result = test::analyze(source); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), replace ? 0 : width) - << ::testing::PrintToString(test::messages(result.diagnostics)); - if (replace) - EXPECT_TRUE(result.diagnostics.empty()) - << ::testing::PrintToString(test::messages(result.diagnostics)); - } - } -} - -TEST(PointerIdentity, DeepDereferencesRetainTheirReleasedPointer) { - // RFC 0027: every dereference keeps its pointer, expression and witness - // when a resolved path grows beyond its inline storage. - for (const bool replace : {false, true}) { - SCOPED_TRACE(replace); - std::string source = R"c( - struct node { struct node *next; int value; }; - int client(struct node *p) { - free(p->next->next); - )c"; - if (replace) - source += "p->next->next=malloc(sizeof(struct node));" - "if(!p->next->next)return 0; p->next->next->value=1;"; - source += "int value=p->next->next->value;"; - if (replace) - source += "free(p->next->next);"; - source += "return value;}"; - const auto result = test::analyze(source); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), replace ? 0U : 1U) - << ::testing::PrintToString(test::messages(result.diagnostics)); - if (replace) - EXPECT_TRUE(result.diagnostics.empty()) - << ::testing::PrintToString(test::messages(result.diagnostics)); - } -} - -TEST(PointerIdentity, AStoredTargetDoesNotAcquireAnUnrelatedFunctionsEffects) { - const auto result = test::analyze(std::string(Callbacks) + R"c( -void (*unrelated)(void *) = drop; -void clean(void) { - int *p = malloc(sizeof *p); if (!p) return; - void (*fn)(void *) = keep; fn(p); *p = 1; free(p); -} -)c"); - ASSERT_TRUE(result.ast); - EXPECT_TRUE(result.diagnostics.empty()) - << ::testing::PrintToString(test::messages(result.diagnostics)); -} - -TEST(PointerIdentity, ARepeatedCallbackReleaseReportsOneDoubleFree) { - const auto result = test::analyze(std::string(Callbacks) + R"c( -void bad(int *p) { void (*fn)(void *) = drop; fn(p); fn(p); } -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(test::ids(result.diagnostics), - std::vector{"double-free"}); -} - -TEST(PointerIdentity, NullCallbacksDoNotPassAsCheckedCalls) { - const auto result = test::analyze(R"c( -void bad(int *p) { void (*fn)(void *) = 0; fn(p); } -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::NullDereference), 1U); -} - -TEST(PointerIdentity, ANonNullGuardRemovesTheNullCallbackAlternative) { - const auto result = test::analyze(std::string(Callbacks) + R"c( -void clean(int *p, int select) { - void (*fn)(void *) = select ? keep : 0; - if (fn) { fn(p); use(p); } -} -)c"); - ASSERT_TRUE(result.ast); - EXPECT_TRUE(result.diagnostics.empty()) - << ::testing::PrintToString(test::messages(result.diagnostics)); -} - -TEST(PointerIdentity, HelperContextsKeepEachCallbackWithItsUserdata) { - const auto result = test::analyze(std::string(Callbacks) + R"c( -static void invoke(void (*fn)(void *), void *p) { fn(p); } -void clean(void) { - int *p = malloc(sizeof *p), *q = malloc(sizeof *q); - if (!p || !q) { free(p); free(q); return; } - invoke(keep, p); invoke(drop, q); *p = 1; free(p); -} -void bad(void) { - int *p = malloc(sizeof *p); if (!p) return; - invoke(drop, p); *p = 1; -} -)c"); - ASSERT_TRUE(result.ast); - // RFC 0031 §6.1 drops the callback context requests of format 29: one - // summary of `invoke` serves every caller, and it releases `p` through - // whichever of `keep` and `drop` the slot holds, possibly. `bad`'s use - // after free is found, as possible; `clean` gets possible findings it - // does not have (KNOWN-DIFFERENCES.md, *Unit tests*). - EXPECT_EQ(test::messages(result.diagnostics), - (std::vector{ - "9: use of 'p' after it may have been freed", - "9: 'p' may be freed twice", - "13: use of 'p' after it may have been freed"})); - for (const core::Diagnostic &diagnostic : result.diagnostics.diagnostics()) - EXPECT_EQ(diagnostic.certainty, core::Certainty::Possible) - << diagnostic.message; -} - -TEST(PointerIdentity, CallbackBodiesAreCheckedInTheRequestedContext) { - const auto result = test::analyze(R"c( -static void drop(void *p) { free(p); } -static void invoke(void (*fn)(void *), int *p) { fn(p); *p = 1; } -void caller(int *p) { invoke(drop, p); } -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 1U); -} - -TEST(PointerIdentity, NestedForwardersAndReturnsPreserveTargets) { - const auto result = test::analyze(std::string(Callbacks) + R"c( -static void invoke(void (*fn)(void *), void *p) { fn(p); } -typedef void (*Callback)(void *); -static Callback choose(void) { return drop; } -static void forward(Callback fn, void *data) { invoke(fn, data); } -void bad(int *p) { Callback fn = choose(); forward(fn, p); use(p); } -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 1U); - // RFC 0030 §9.3: `invoke`'s own pass resolves `fn` through its parameter - // slot, which holds `drop` and is open (`invoke`'s callers are outside a - // per-TU compile): the call is `trusted(extern-contract)`, not unresolved. - EXPECT_TRUE(test::unknownCalls(result).empty()); -} - -TEST(PointerIdentity, - CopiedAndReturnedCallbackParametersRetainTheirInputPaths) { - const auto result = test::analyze(std::string(Callbacks) + R"c( -typedef void (*Callback)(void *); -static Callback identity(Callback fn) { Callback saved = fn; return saved; } -static void copied(Callback fn, void *p) { Callback saved = fn; saved(p); } -void bad(int *p) { copied(identity(drop), p); use(p); } -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(test::ids(result.diagnostics), - std::vector{"use-after-free"}); -} - -TEST(PointerIdentity, CallbackFieldsSurviveRecordCopiesAndOutParameters) { - const auto result = test::analyze(std::string(Callbacks) + R"c( -struct hook { void (*fn)(void *); void *data; }; -static void install(struct hook *h) { h->fn = drop; } -void bad(int *p) { struct hook a = {keep, p}; install(&a); struct hook b = a; b.fn(b.data); use(p); } -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 1U); - EXPECT_EQ(test::unknownCalls(result), std::vector{}); -} - -// RFC 0028: copy-only setters need extra contexts for checked heap outputs; -// ordinary stores forward their input path without specializing the setter. -TEST(PointerIdentity, CopyOnlySettersKeepOrdinaryCallbackEffectsSymbolic) { - const std::string source = std::string(Callbacks) + R"c( -struct hook { void (*fn)(void *); }; -static void install(struct hook *h, void (*fn)(void *)) { h->fn = fn; } -void bad(int *p) { struct hook h = {keep}; install(&h, drop); h.fn(p); free(p); } -)c"; - const auto ordinary = test::analyze(source); - ASSERT_TRUE(ordinary.ast); - ASSERT_NE(ordinary.summary("install"), nullptr); - EXPECT_EQ(countId(ordinary, core::diag::DoubleFree), 1U); - EXPECT_EQ(test::unknownCalls(ordinary), std::vector{}); -} - -TEST(PointerIdentity, UnknownTargetsRemainUnknownDespiteAddressTakenFunctions) { - const auto result = test::analyze(std::string(Callbacks) + R"c( -void (*unrelated)(void *) = keep; -void boundary(void (*fn)(void *), void *p) { fn(p); } -)c"); - ASSERT_TRUE(result.ast); - // RFC 0030 §5.1: the call through `fn` is into unknown code. - EXPECT_EQ(test::unknownCalls(result).size(), 1U); -} - -TEST(PointerIdentity, ConditionalTargetsKeepAllPossibleEffects) { - const auto result = test::analyze(std::string(Callbacks) + R"c( -void bad(int *p, int select) { void (*fn)(void *) = select ? keep : drop; fn(p); use(p); } -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 1U); -} - -TEST(PointerIdentity, HelpersRetainKnownEffectsAlongsideAnUnknownTarget) { - const auto result = test::analyze(std::string(Callbacks) + R"c( -static void invoke(void (*fn)(void *), void *p) { fn(p); } -void bad(int *p, int select, void (*unknown)(void *)) { - invoke(select ? drop : unknown, p); use(p); -} -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 1U); - // RFC 0030 §9.3: `fn`'s slot holds `drop` and is open, because `bad` - // passes a pointer from outside: the call takes `drop`'s effects and is - // `trusted(extern-contract)`, which names the open source. - EXPECT_TRUE(test::unknownCalls(result).empty()); -} - -TEST(PointerIdentity, UnknownAssignmentDoesNotResurrectAGlobalInitializer) { - const auto result = test::analyze(std::string(Callbacks) + R"c( -void (*global)(void *) = keep; -void boundary(void (*unknown)(void *), int *p) { global = unknown; global(p); } -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(test::unknownCalls(result).size(), 1U); -} - -TEST(PointerIdentity, - GlobalCallbackWritesRemainPossibleAtLaterFunctionEntries) { - const auto result = test::analyze(std::string(Callbacks) + R"c( -void (*global)(void *) = keep; -static void set_drop(void) { global = drop; } -void bad(int *p) { global(p); use(p); } -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 1U); -} - -TEST(PointerIdentity, GlobalCallbackInferenceDoesNotDependOnFunctionOrder) { - const auto result = test::analyze(std::string(Callbacks) + R"c( -void (*global)(void *) = keep; -static void invoke(int *p) { global(p); } -void bad(int *p) { invoke(p); use(p); } -static void set_drop(void) { global = drop; } -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(test::ids(result.diagnostics), - std::vector{"use-after-free"}); -} - -TEST(PointerIdentity, FunctionsReturningAGlobalCallbackRetainItsTargets) { - const auto result = test::analyze(std::string(Callbacks) + R"c( -typedef void (*Callback)(void *); -static Callback global = drop; -static Callback get(void) { return global; } -void bad(int *p) { Callback fn = get(); fn(p); use(p); } -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(test::ids(result.diagnostics), - std::vector{"use-after-free"}); -} - -TEST(PointerIdentity, PointerInequalitySelectsTheNonConsumingHelperPath) { - const auto result = test::analyze(R"c( -static void release_same(int *p, int *q) { if (p == q) free(p); } -void clean(int *p, int *q) { if (p != q) { release_same(p, q); use(p); free(p); } } -)c"); - ASSERT_TRUE(result.ast); - // RFC 0031 *Pointer comparisons*: `release_same` releases only when its - // arguments compare equal, which the caller's `p != q` refutes. - EXPECT_TRUE(result.diagnostics.empty()) - << ::testing::PrintToString(test::messages(result.diagnostics)); -} - -TEST(PointerIdentity, PointerEqualityStillDiagnosesActualConsumption) { - const auto result = test::analyze(R"c( -static void release_same(int *p, int *q) { if (p == q) free(p); } -void bad(int *p) { release_same(p, p); use(p); } -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 1U); -} - -TEST(PointerIdentity, AComparisonAfterTheCallCanRefuteAConditionalConsume) { - const auto result = test::analyze(R"c( -static void release_same(int *p, int *q) { if (p == q) free(p); } -void clean(int *p, int *q) { release_same(p, q); if (p != q) - use(p); } -)c"); - ASSERT_TRUE(result.ast); - // RFC 0031 *Pointer comparisons*: the pair test selects the release at - // the call only, so the test after the call cannot refute the possible - // release (KNOWN-DIFFERENCES.md, *Unit tests*). - EXPECT_EQ( - test::messages(result.diagnostics), - (std::vector{"4: use of 'p' after it may have been freed"})); - for (const core::Diagnostic &diagnostic : result.diagnostics.diagnostics()) - EXPECT_EQ(diagnostic.certainty, core::Certainty::Possible) - << diagnostic.message; -} - -TEST(PointerIdentity, OverwrittenComparisonOperandsUseTheIncomingValues) { - const auto result = test::analyze(R"c( -static void replace_same(int **p, int *q, int *replacement) { - if (*p == q) { free(*p); *p = replacement; } -} -void clean(int *p, int *q, int *replacement) { - if (p != q) { replace_same(&p, q, replacement); use(p); } -} -)c"); - ASSERT_TRUE(result.ast); - EXPECT_TRUE(result.diagnostics.empty()) - << ::testing::PrintToString(test::messages(result.diagnostics)); -} - -TEST(PointerIdentity, PointerGuardsSurviveCopiesOfTheComparedValues) { - const auto result = test::analyze(R"c( -static void release_same(int *p, int *q) { if (p == q) free(p); } -void clean(int *p, int *q) { int *saved = p; if (p != q) { release_same(saved, q); use(saved); } } -)c"); - ASSERT_TRUE(result.ast); - EXPECT_TRUE(result.diagnostics.empty()) - << ::testing::PrintToString(test::messages(result.diagnostics)); -} - -static constexpr const char *Memory = R"c( -void *memcpy(void *, const void *, size_t); -void *memmove(void *, const void *, size_t); -)c"; - -TEST(PointerIdentity, MemcpyCopiesPointerIdentityAndMoveState) { - const auto result = test::analyze(std::string(Memory) + R"c( -void bad(void) { int *p = malloc(sizeof *p), *q; if (!p) return; memcpy(&q, &p, sizeof p); free(p); *q = 1; } -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(test::ids(result.diagnostics), - std::vector{"use-after-free"}); -} - -TEST(PointerIdentity, MemmoveCopiesCompleteRecordsAndNestedPointers) { - const auto result = test::analyze(std::string(Memory) + R"c( -struct inner { int *p; }; struct outer { struct inner in; int count; }; -void bad(int *p) { struct outer a = {{p}, 1}, b; memmove(&b, &a, sizeof a); free(a.in.p); use(b.in.p); } -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 1U); - EXPECT_EQ(countId(result, core::diag::UseOfUninitialized), 0U); -} - -TEST(PointerIdentity, CompleteRecordCopiesKeepChildBounds) { - const auto result = test::analyze(std::string(Memory) + R"c( -struct box { int *p; }; -void bad(void) { - struct box a = {malloc(2 * sizeof(int))}, b; - if (!a.p) return; - memcpy(&b, &a, sizeof a); b.p[2] = 1; free(a.p); -} -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(test::ids(result.diagnostics), - std::vector{"out-of-bounds"}); -} - -TEST(PointerIdentity, CompleteCopiesCarryCallbackTargets) { - const auto result = test::analyze(std::string(Memory) + Callbacks + R"c( -void bad(int *p) { void (*fn)(void *) = drop, (*copy)(void *); memcpy(©, &fn, sizeof fn); copy(p); use(p); } -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 1U); - EXPECT_EQ(test::unknownCalls(result), std::vector{}); -} - -TEST(PointerIdentity, CompleteCopiesThroughHelpersExportPointerStores) { - const auto result = test::analyze(std::string(Memory) + R"c( -static void copy_pointer(int **dest, int **source) { - memcpy(dest, source, sizeof *source); -} -void bad(int *p) { int *q; copy_pointer(&q, &p); free(p); use(q); } -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(test::ids(result.diagnostics), - std::vector{"use-after-free"}); -} - -TEST(PointerIdentity, FortifiedCopiesPreservePointerIdentity) { - const auto result = test::analyze(R"c( -void bad(int *p) { int *q; - __builtin___memcpy_chk(&q, &p, sizeof p, sizeof q); - free(p); use(q); -} -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(test::ids(result.diagnostics), - std::vector{"use-after-free"}); -} - -TEST(PointerIdentity, APartialPointerCopyReportsLostCoverage) { - const auto result = test::analyze(std::string(Memory) + R"c( -void partial(int **dest, int **source) { memcpy(dest, source, 1); } -)c"); - ASSERT_TRUE(result.ast); - // RFC 0030 §15 item 3: no diagnostic; the copy's temporal facet is a - // reinterpretation the engine did not follow (`raw-cast`, which is no - // `incomplete` reason). - EXPECT_TRUE(result.diagnostics.empty()) - << ::testing::PrintToString(test::messages(result.diagnostics)); - EXPECT_EQ(decisionAt(result, "memcpy(dest,source,1)", core::Facet::Temporal), - "unresolved/raw-cast: unsupported memory copy of " - "pointer-containing storage"); -} - -TEST(PointerIdentity, ZeroLengthCopiesDoNotTransferOwnership) { - const auto result = test::analyze(std::string(Memory) + R"c( -void clean(int *p, int *q) { memcpy(&q, &p, 0); free(p); use(q); } -)c"); - ASSERT_TRUE(result.ast); - EXPECT_TRUE(result.diagnostics.empty()) - << ::testing::PrintToString(test::messages(result.diagnostics)); -} - -TEST(PointerIdentity, ArrayDecayPreservesTheElementRecordView) { - const auto result = test::analyze(R"c( -struct box { int *p; }; -static void drop(struct box *b) { free(b->p); } -void bad(int *p) { struct box b[1] = {{p}}; drop(b); use(b[0].p); } -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(test::incomplete(result).size(), 0U); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 1U); -} - -TEST(PointerIdentity, ConstRecordViewsHaveTheSameLayout) { - const auto result = test::analyze(R"c( -struct box { int *p; }; -static void drop(const struct box *b) { free(b->p); } -void bad(const struct box *b) { drop(b); use(b->p); } -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(test::ids(result.diagnostics), - std::vector{"use-after-free"}); -} - -TEST(PointerIdentity, UnrelatedRecordViewsDoNotFabricateFieldEffects) { - const auto result = test::analyze(R"c( -struct first { int *p; }; struct second { int tag; int *p; }; -static void drop_field(void *object) { struct first *a = object; free(a->p); } -void boundary(struct second *b) { drop_field(b); } -)c"); - ASSERT_TRUE(result.ast); - // RFC 0031 §6.3: `drop_field` releases what its `struct first` view reads - // at offset 0, which `b` holds as an `int`: the call is a reinterpretation - // (`raw-cast`, which is no `incomplete` reason), not a release of `b->p`. - EXPECT_EQ(decisionAt(result, "drop_field(b)", core::Facet::Temporal), - "unresolved/raw-cast: incompatible or unknown object view at call"); - EXPECT_TRUE(result.diagnostics.empty()) - << ::testing::PrintToString(test::messages(result.diagnostics)); -} - -TEST(PointerIdentity, CompatibleErasedAndEmbeddedRecordViewsRemainChecked) { - const auto result = test::analyze(R"c( -struct first { int *p; }; struct outer { int tag; struct first in; }; -static void drop_field(void *object) { struct first *a = object; free(a->p); } -void bad(struct outer *b) { void *erased = &b->in; drop_field(erased); use(b->in.p); } -)c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(test::incomplete(result).size(), 0U); - EXPECT_EQ(countId(result, core::diag::UseAfterFree), 1U); -} - -TEST(PointerIdentity, RepeatedErasedCallsRecheckChangedObjectViews) { - const auto result = test::analyze(R"c( -struct first { int *p; }; struct second { int tag; int *p; }; -static void drop_field(void *object) { struct first *a = object; free(a->p); } -void boundary(struct first *a, struct second *b) { - void *erased = a; - for (int i = 0; i < 2; ++i) { - drop_field(erased); - erased = b; - } -} -)c"); - ASSERT_TRUE(result.ast); - // The second call hands `drop_field` a `struct second`: the call is not - // proven (RFC 0031 §6.3; its reason is the first found, here what the - // first iteration released). - EXPECT_TRUE(llvm::StringRef(decisionAt(result, "drop_field(erased)", - core::Facet::Temporal)) - .starts_with("unresolved/")); -} - -} // namespace weavec::analysis diff --git a/unittests/Analysis/PointerValidityTest.cpp b/unittests/Analysis/PointerValidityTest.cpp deleted file mode 100644 index 3593ab58..00000000 --- a/unittests/Analysis/PointerValidityTest.cpp +++ /dev/null @@ -1,592 +0,0 @@ -//===- PointerValidityTest.cpp - Null, uninitialised, invalid releases ----===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// RFC 0008, *Pointer validity*: `null-dereference`, `use-of-uninitialized`, -// `invalid-release`, the `replaced` consume flag, `result`-rooted stores and -// the `WEAVEC_NULLABLE` / `WEAVEC_NONNULL` annotations. -// -//===----------------------------------------------------------------------===// - -#include "TestUtils.h" - -#include - -namespace weavec::analysis { -namespace { - -using core::SummaryPath; -using weavec::test::analyze; -using weavec::test::ids; -using weavec::test::messages; -using weavec::test::notes; - -using Strings = std::vector; - -/// Shared types; `#line 1` keeps the snippet's line numbers as `messages` -/// counts them (the raw string's opening newline is line 1). -constexpr const char *Types = R"c( -struct node { int v; struct node *next; char *name; }; -struct buf { char *data; unsigned len; }; -#line 1 -)c"; - -// -- Null dereference --------------------------------------------------------- - -TEST(NullDereference, UncheckedAllocatorResult) { - const auto result = analyze(std::string(Types) + R"c( - int f(void) { - struct node *n = malloc(sizeof *n); - n->v = 1; - int r = n->v; - free(n); - return r; - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(messages(result.diagnostics), - (Strings{"4: the result of 'malloc' is used without a null test; " - "it is null when allocation fails"})) - << "one bad pointer reports once"; - EXPECT_EQ(ids(result.diagnostics), (Strings{"allocation-failure"})); - EXPECT_EQ(notes(result.diagnostics), (Strings{"allocated here"})); -} - -TEST(NullDereference, ConstantNull) { - const auto result = analyze(R"c( - int f(void) { - int *p = NULL; - return *p; - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(messages(result.diagnostics), - (Strings{"4: dereference of 'p', which is null"})); - EXPECT_EQ(notes(result.diagnostics), (Strings{"'p' is assigned NULL here"})); -} - -TEST(NullDereference, TestsEstablishNonNull) { - // Every recognised shape of test (RFC 0008, *Nullness*, `Tested`). - const auto result = analyze(std::string(Types) + R"c( - int early(void) { - struct node *n = malloc(sizeof *n); - if (!n) return 0; - n->v = 1; - free(n); - return 0; - } - int equal(struct node *n) { - if (n == NULL) return -1; - return n->v; - } - int and_(struct node *n) { return n && n->v; } - int or_(struct node *n) { return !n || n->v; } - int ternary(struct node *n) { return n ? n->v : 0; } - void walk(struct node *head) { - for (struct node *n = head; n; n = n->next) - n->v = 0; - } - void loop(struct node *n) { - while (n != NULL) { - n->v = 0; - n = n->next; - } - } - void not_equal(struct node *n) { - if (n != NULL) - n->v = 0; - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_TRUE(result.diagnostics.empty()) << messages(result.diagnostics)[0]; -} - -TEST(NullDereference, TestedThenMergedIsMaybeNull) { - // The null path did not end: after the merge the pointer may be null. - const auto result = analyze(std::string(Types) + R"c( - void warn(void); - int f(struct node *n) { - if (n == NULL) warn(); - return n->v; - } - void g(struct node *n) { - while (n != NULL) n = n->next; - n->v = 1; - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(messages(result.diagnostics), - (Strings{"9: dereference of 'n', which is null"})); - EXPECT_EQ(notes(result.diagnostics, 0), - (Strings{"'n' may be null: it is compared with NULL here"})); -} - -TEST(NullDereference, RedundantTestsKeepANonNullFact) { - // A pointer already known non-null and retested on every use (cJSON's - // `can_access_at_index` macro): the null edge is infeasible and must not - // make the pointer maybe-null once the edges merge (RFC 0008, - // *Implementation notes*). - const auto result = analyze(std::string(Types) + R"c( - static unsigned skip(struct buf *b) { - if (b == NULL || b->data == NULL) return 0; - while (b != NULL && b->len > 0 && b->data[b->len - 1] == ' ') b->len--; - return b->len; - } - static unsigned retest(struct buf *b) { - if (!b) return 0; - if (b == NULL) return 1; - return b->len; - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(messages(result.diagnostics), Strings{}); -} - -TEST(NullDereference, ADereferenceEstablishesNonNull) { - // The path could only have continued past `n->v` with a non-null `n`, so - // a later test whose null edge merges back does not make it maybe-null - // (cJSON's `buffer_at_offset(b)` at the top of a function, then - // `cannot_access_at_index(b, 0)` further down). Passing it to a callee - // that requires it counts as a dereference. - const auto result = analyze(std::string(Types) + R"c( - static int get(struct node *n) { return n->v; } - int direct(struct node *n) { - int v = n->v; - if (n == NULL) v = 0; - return n->v + v; - } - int via_callee(struct node *n) { - int v = get(n); - if (!n) v = 0; - return n->v + v; - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(messages(result.diagnostics), Strings{}); -} - -TEST(NullDereference, CastNullConstantsAreNullTests) { - // `(struct node *)0` is not a null pointer constant in ISO C's sense - // (zlib's `buf != (charf *)0`), but it is null all the same. - const auto result = analyze(std::string(Types) + R"c( - static int get(struct node *n) { return n->v; } - static int guarded(struct node *n, int c) { - if (c && n != (struct node *)0) return get(n); - return 0; - } - int f(void) { - struct node *n = (struct node *)0; - return n->v; - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(messages(result.diagnostics), - (Strings{"9: dereference of 'n', which is null"})); -} - -TEST(NullDereference, UncheckedCalleesMayWriteWhatTheyReach) { - // A call into unchecked code may store through any pointer it is handed: - // the facts below `&lc` and at `p` through `&p` are gone, the fact on a - // pointer passed by value stays (linenoise's completion callback fills a - // `linenoiseCompletions lc = { 0, NULL }`). - const auto result = analyze(std::string(Types) + R"c( - struct lc { unsigned len; char **cvec; }; - void fill(struct lc *out); - void look(char *p); - char by_address(void) { - struct lc lc = { 0, NULL }; - fill(&lc); - return lc.cvec[0][0]; - } - char pointer_by_address(void) { - char *p = NULL; - fill((struct lc *)&p); - return p[0]; - } - char by_value(void) { - char *p = NULL; - look(p); - return p[0]; - } - )c"); - ASSERT_TRUE(result.ast); - // RFC 0030 §5.1: the calls into unknown code report nothing; the value - // passed by value keeps its nullness. - EXPECT_EQ(messages(result.diagnostics), - (Strings{"18: dereference of 'p', which is null"})); - EXPECT_EQ( - test::unknownCalls(result), - (Strings{"7: fill(&lc)", "12: fill((structlc*)&p)", "17: look(p)"})); -} - -TEST(NullDereference, UnknownPointersAreTrusted) { - // A parameter, a loaded field, the result of unannotated code: nothing is - // known, nothing is reported (RFC 0008, *Bugs deliberately not caught*). - const auto result = analyze(std::string(Types) + R"c( - struct node *lookup(int); - int param(struct node *n) { return n->v; } - int field(struct node *n) { return n->next->v; } - int external(void) { return lookup(1)->v; } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_TRUE(result.diagnostics.empty()) << messages(result.diagnostics)[0]; - // RFC 0030 §5.1: the call to `lookup` is into unknown code. - EXPECT_EQ(test::unknownCalls(result), (Strings{"5: lookup(1)"})); -} - -TEST(NullDereference, DereferencesBecomeRequirements) { - const auto result = analyze(std::string(Types) + R"c( - static int get(struct node *n) { return n->v; } - static int tolerant(struct node *n) { return n ? n->v : 0; } - static int later(struct node *n, int c) { if (c) return 0; return n->v; } - int via_copy(struct node *n) { struct node *m = n; return m->v; } - void pass_null(void) { get(NULL); } - void pass_maybe(void) { - struct node *n = malloc(sizeof *n); - get(n); - free(n); - } - void pass_tolerant(void) { tolerant(NULL); } - void pass_checked(struct node *n) { if (n) get(n); } - void pass_unknown(struct node *n) { get(n); } - )c"); - ASSERT_TRUE(result.ast); - // RFC 0030 §7.5: `get` is static and must-accesses `n`, so every direct - // call checks its argument; a null one is the call's violation. (The - // summary's `requires` stays a may-fact, `later`'s included.) - EXPECT_EQ(messages(result.diagnostics), - (Strings{"6: a null pointer is passed to 'get', which " - "dereferences it"})); -} - -TEST(NullDereference, CalleeResultsCarryNullness) { - const auto result = analyze(std::string(Types) + R"c( - void abort(void) __attribute__((noreturn)); - static struct node *make(void) { - struct node *n = malloc(sizeof *n); - if (n) n->v = 0; - return n; - } - static struct node *make_or_die(void) { - struct node *n = malloc(sizeof *n); - if (!n) abort(); - return n; - } - int f(void) { - struct node *n = make(); - int v = n->v; - free(n); - return v; - } - int g(void) { - struct node *n = make_or_die(); - int v = n->v; - free(n); - return v; - } - int h(void) { return make()->v; } - )c"); - ASSERT_TRUE(result.ast); - // RFC 0031 §5.8: `h` drops the object `make` returns fresh, a leak the - // object engine follows the result to. - EXPECT_EQ(messages(result.diagnostics), - (Strings{"15: the result of 'make' is used without a null test; it " - "is null when allocation fails", - "25: the result of 'make' is used without a null test; it " - "is null when allocation fails", - "25: result of 'make' is leaked"})) - << "the direct dereference of a call result is checked too"; - EXPECT_EQ(notes(result.diagnostics, 0), (Strings{"allocated here"})); -} - -TEST(NullDereference, TableEntries) { - const auto result = analyze(R"c( - char *strchr(const char *, int); - void *memcpy(void *, const void *, size_t); - typedef struct FILE FILE; - int fclose(FILE *); - void found(const char *s) { - char *c = strchr(s, 'x'); - *c = 0; - } - void checked(const char *s) { - char *c = strchr(s, 'x'); - if (c) *c = 0; - } - void copy(void) { - char *d = malloc(8); - memcpy(d, "abc", 4); - free(d); - } - void close_null(void) { fclose(NULL); } - void free_null(void) { free(NULL); } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ( - messages(result.diagnostics), - (Strings{ - "16: the result of 'malloc' is used without a null test; it is null " - "when allocation fails", - "19: a null pointer is passed to 'fclose', which dereferences it"})); - EXPECT_EQ(notes(result.diagnostics, 0), (Strings{"allocated here"})); -} - -TEST(NullDereference, NullnessFollowsTheValue) { - const auto result = analyze(std::string(Types) + R"c( - void field(struct node *n) { - n->next = NULL; - n->next->v = 1; - } - void copy(void) { - struct node *n = malloc(sizeof *n); - struct node *m = n; - m->v = 1; - free(n); - } - void reassigned(struct node *other) { - struct node *n = malloc(sizeof *n); - free(n); - n = other; - n->v = 1; - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(messages(result.diagnostics), - (Strings{"4: dereference of 'n->next', which is null", - "9: the result of 'malloc' is used without a null test; " - "it is null when allocation fails"})); -} - -// -- Annotations -------------------------------------------------------------- - -TEST(NullDereference, AnnotationsOnUncheckedCallees) { - // RFC 0008, *Annotation surface*: neither annotation says anything about - // ownership, so an otherwise unannotated declaration is still an unknown - // callee (RFC 0030 §5.1), but what they do say holds. RFC 0030 §3.2: a - // result that may be null is a checked facet; a null argument where the - // declaration requires non-null is definite. - const auto result = analyze(std::string(Types) + R"c( - char *NULLABLE lookup(int k); - void need(struct node *NONNULL n); - int result(void) { return lookup(1)[0]; } - void argument(void) { need(NULL); } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(messages(result.diagnostics), - (Strings{"5: a null pointer is passed to 'need', which " - "dereferences it"})); - EXPECT_EQ(test::unknownCalls(result), - (Strings{"4: lookup(1)", "5: need(NULL)"})); -} - -TEST(NullDereference, ContradictoryAnnotationsAreInvalid) { - const auto result = analyze(std::string(Types) + R"c( - int f(struct node *NULLABLE NONNULL n) { return 0; } - char *NULLABLE NONNULL g(void) { return 0; } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ( - messages(result.diagnostics), - (Strings{"2: 'n' is declared both WEAVEC_NULLABLE and WEAVEC_NONNULL", - "3: 'g' is declared both WEAVEC_NULLABLE and " - "WEAVEC_NONNULL"})); - EXPECT_EQ(ids(result.diagnostics), - (Strings{"invalid-annotation", "invalid-annotation"})); -} - -// -- Uninitialised pointers --------------------------------------------------- - -TEST(UseOfUninitialized, LocalsAndFields) { - const auto result = analyze(std::string(Types) + R"c( - struct outer { struct buf inner; char *q; }; - void read(void) { char *p; use(p); } - void deref(void) { int *p; *p = 1; } - void copy(void) { char *p; char *q = p; use(q); } - void field(void) { struct buf b; use(b.data); } - void nested(void) { struct outer o; use(o.inner.data); } - void release(void) { char *p; free(p); } - void maybe(int c) { char *p; if (c) p = malloc(4); use(p); free(p); } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(messages(result.diagnostics), - (Strings{"3: use of 'p' before it was initialized", - "4: use of 'p' before it was initialized", - "5: use of 'p' before it was initialized", - "6: use of 'b.data' before it was initialized", - "7: use of 'o.inner.data' before it was initialized", - "8: use of 'p' before it was initialized"})) - << "a copy reports at the copy, once; a may-uninitialised value is " - "made defined by zero-initialisation (RFC 0030 §3.1)"; - EXPECT_EQ(ids(result.diagnostics)[0], "use-of-uninitialized"); - EXPECT_EQ(notes(result.diagnostics), (Strings{"'p' is declared here"})); -} - -TEST(UseOfUninitialized, InitialisationSilences) { - // (RFC 0031 §5.4: a callee writes only what it reaches through non-const - // pointees; `use(&p)` hands `p` over as `const void *` and leaves it - // uninitialised, so the address is taken by a mutable borrow here.) - const auto result = analyze(std::string(Types) + R"c( - void fill(struct buf *MUT); - void init(void) { char *p = NULL; use(p); } - void assigned(void) { char *p; p = malloc(4); use(p); free(p); } - void out(void) { struct buf b; fill(&b); use(b.data); } - void address_taken(void) { char *p; poke(&p); use(p); } - void field_set(void) { struct buf b; b.data = NULL; use(b.data); } - void static_(void) { static char *p; use(p); } - void zeroed(void) { struct buf b = {0}; use(b.data); } - void integers(void) { int n; struct buf b; b.len = 1; use(&b); } - void all_paths(int c) { char *p; if (c) p = NULL; else p = malloc(4); use(p); free(p); } - void loop(void) { char *p; for (int i = 0; i < 3; i++) { p = malloc(4); free(p); } } - void whole(struct buf src) { struct buf b; b = src; use(b.data); } - void array(void) { char *a[2]; a[0] = NULL; use(a[0]); } - typedef __builtin_va_list va_list; - void variadic(const char *fmt, ...) { - va_list ap, cpy; - __builtin_va_start(ap, fmt); - __builtin_va_copy(cpy, ap); - __builtin_va_end(cpy); - __builtin_va_end(ap); - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_TRUE(result.diagnostics.empty()) << messages(result.diagnostics)[0]; -} - -// -- Invalid releases --------------------------------------------------------- - -TEST(InvalidRelease, NonHeapObjects) { - const auto result = analyze(std::string(Types) + R"c( - typedef struct FILE FILE; - int fclose(FILE *); - static char g_buf[16]; - static struct buf g_b; - void stack(void) { char buf[8]; free(buf); } - void address(void) { int x; free(&x); } - void field(void) { struct buf b; free(&b.len); } - void literal(void) { free("abc"); } - void global(void) { free(g_buf); } - void global_struct(void) { free(&g_b); } - void wrong_family(void) { struct buf b; fclose((FILE *)&b); } - void via_copy(void) { char buf[8]; char *p = buf; free(p); } - void via_literal(void) { const char *s = "abc"; free((char *)s); } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ( - messages(result.diagnostics), - (Strings{"6: 'buf' is released but is not a heap object", - "7: 'x' is released but is not a heap object", - "8: 'b.len' is released but is not a heap object", - "9: a string literal is released", - "10: 'g_buf' is released but is not a heap object", - "11: 'g_b' is released but is not a heap object", - "12: 'b' is released but is not a heap object", - "13: 'p' is released but points to 'buf', which is not a heap " - "object", - "14: 's' is released but points to a string literal"})); - EXPECT_EQ(ids(result.diagnostics)[0], "invalid-release"); - EXPECT_EQ(notes(result.diagnostics), (Strings{"'buf' is declared here"})); - EXPECT_EQ(notes(result.diagnostics, 7), (Strings{"'buf' is declared here"})); -} - -TEST(InvalidRelease, InteriorPointers) { - const auto result = analyze(R"c( - char *strchr(const char *, int); - void offset(void) { char *p = malloc(8); if (!p) return; free(p + 1); } - void alias(void) { char *p = malloc(8); if (!p) return; char *q = p + 1; free(q); } - void incremented(void) { char *p = malloc(8); if (!p) return; p++; free(p); } - void found(void) { - char *p = malloc(8); - if (!p) return; - char *q = strchr(p, 'x'); - if (q) free(q); - } - void zero(void) { char *p = malloc(8); if (!p) return; free(p + 0); } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ( - messages(result.diagnostics), - (Strings{"3: 'p' is released but points 1 element past the start of its " - "allocation", - "4: 'q' is released but points 1 element past the start of its " - "allocation", - "5: 'p' is released but points 1 element past the start of its " - "allocation", - "10: 'q' is released but may not point to the start of its " - "allocation"})) - << "`p + 0` is `p`; a known offset is named (RFC 0011)"; - EXPECT_EQ(notes(result.diagnostics), (Strings{"allocated here"})); -} - -TEST(InvalidRelease, ValidReleasesAreClean) { - const auto result = analyze(std::string(Types) + R"c( - void heap(void) { char *p = malloc(8); free(p); } - void alias(void) { char *p = malloc(8); char *q = p; free(q); } - void param(char *OWNED p) { free(p); } - void field(struct buf *b) { free(b->data); } - void element(char **arr) { free(arr[0]); } - void conditional(int c) { char *p = c ? malloc(8) : NULL; free(p); } - void pick(char *OWNED a, char *OWNED b, int c) { free(c ? a : b); free(c ? b : a); } - void strchr_of_param(char *s) { char *strchr(const char *, int); free(strchr(s, 'x')); } - )c"); - ASSERT_TRUE(result.ast); - // `pick` frees both on one path and neither on the other: RFC 0007 leaks - // aside, no invalid release is reported. - for (const core::Diagnostic &d : result.diagnostics.diagnostics()) - EXPECT_NE(d.id, core::diag::InvalidRelease) << d.message; -} - -TEST(InvalidRelease, AFieldOfAParameterIsNotAnAllocation) { - // RFC 0031 §5.5 and A3: `b` points to a whole `struct buf`, so `&b->len` - // lies 8 bytes into the allocation that holds it, never at its start. - // (Before the object engine a caller's pointer had no known offset and - // this release was accepted.) - const auto result = analyze(std::string(Types) + R"c( - void interior_of_unknown(struct buf *b) { free(&b->len); } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(messages(result.diagnostics), - (Strings{"2: 'b' is released but points to field 'len' of its " - "allocation"})); - EXPECT_EQ(ids(result.diagnostics), (Strings{"invalid-release"})); -} - -TEST(InvalidRelease, DoesNotRepeatForTheSameStorage) { - // Clang's -Wfree-nonheap-object is separate; ours is once per release. - const auto result = analyze(R"c( - void f(int c) { - char buf[8]; - char *p = buf; - if (c) p = malloc(8); - free(p); - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(messages(result.diagnostics), - (Strings{"6: 'p' is released but may point to 'buf', which is not " - "a heap object"})) - << "may point at the stack: reported"; -} - -// -- Replaced values (the RFC 0003 soundness hole) --------------------------- - -// -- Struct-by-value results -------------------------------------------------- - -// -- Crash regression --------------------------------------------------------- - -TEST(InvalidRelease, FreeingAStaticArrayDoesNotCrash) { - const auto result = analyze(R"c( - static char table[4][8]; - void f(void) { free(table); } - void g(void) { free(table[1]); } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(ids(result.diagnostics), - (Strings{"invalid-release", "invalid-release"})); -} - -} // namespace -} // namespace weavec::analysis diff --git a/unittests/Analysis/ProgramDatabaseTest.cpp b/unittests/Analysis/ProgramDatabaseTest.cpp deleted file mode 100644 index 7720cfec..00000000 --- a/unittests/Analysis/ProgramDatabaseTest.cpp +++ /dev/null @@ -1,213 +0,0 @@ -//===- ProgramDatabaseTest.cpp - Tests for cross-unit summaries -----------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// RFC 0005: what a unit exports, how the database joins units, and how a -// unit analysed with the database sees the others' definitions. -// -//===----------------------------------------------------------------------===// - -#include "weavec/Analysis/ProgramDatabase.h" - -#include "TestUtils.h" - -#include "llvm/ADT/STLExtras.h" - -#include - -namespace weavec::analysis { -namespace { - -using core::SummaryPath; -using test::analyze; -using test::analyzeInProgram; -using test::ids; -using test::messages; - -constexpr const char *NodeUnit = R"c( -struct node { int v; char *name; }; -char *g_cache; -static char *s_cache; -struct node *node_new(int v) { - struct node *n = malloc(sizeof *n); - n->v = v; - return n; -} -void node_free(struct node *n) { free(n); } -char *node_name(struct node *n) { return n->name; } -int *node_vp(struct node *n) { return &n->v; } -static void drop_impl(void *p) { free(p); } -void (*drop_hook)(void *) = drop_impl; -void reset_caches(void) { free(g_cache); free(s_cache); } -static void helper(void) {} -int main(void) { helper(); return 0; } -)c"; - -/// The node unit's exports, kept alive for the database. -struct NodeProgram { - test::AnalysisResult unit = analyze(NodeUnit); - ProgramDatabase db; - NodeProgram() { - if (!unit.ast) - return; - db.add(unit.exports); - // RFC 0030 §9.3, §13.2 step 2: at link the slots are solved over every - // unit's constraints, and the client reads the solution through the - // program database. The program is not closed here (the client's own - // record is not among them), so its slots stay open. - auto facts = std::make_shared(); - core::SlotRules rules = unit.harness->kinds->slots.rules(); - rules.scope = core::SlotScope::Program; - facts->slots = unit.harness->kinds->slots.exported().solve(rules); - db.programFacts = std::move(facts); - } -}; - -constexpr const char *ClientHeader = R"c( -struct node { int v; char *name; }; -extern char *g_cache; -struct node *node_new(int v); -void node_free(struct node *n); -char *node_name(struct node *n); -int *node_vp(struct node *n); -extern void (*drop_hook)(void *); -void reset_caches(void); -)c"; - -TEST(ProgramDatabase, ProgramDefinitionsAreNotUnknownCallees) { - // RFC 0030 §5.1: in a per-unit analysis the calls into other units are - // unknown callees; with the program's database only the callee no unit - // defines stays one. None of them is a diagnostic. - NodeProgram program; - ASSERT_TRUE(program.unit.ast); - const std::string code = std::string(ClientHeader) + R"c( - void other(void *p); - int ok(void) { - struct node *n = node_new(1); - other(n); - node_free(n); - return 0; - } - )c"; - const auto alone = analyze(code); - ASSERT_TRUE(alone.ast); - EXPECT_TRUE(alone.diagnostics.empty()); - EXPECT_EQ(test::unknownCalls(alone).size(), 3U); - const auto together = analyzeInProgram(code, &program.db); - ASSERT_TRUE(together.ast); - EXPECT_TRUE(together.diagnostics.empty()); - // `other` may have released `n`, so the known release after it is - // unresolved too (§3.1, *A known release after an unknown one*). - EXPECT_EQ(test::unknownCalls(together), - (std::vector{"14: other(n)", "15: node_free(n)"})); -} - -TEST(ProgramDatabase, IndirectCandidatesFromOtherUnits) { - NodeProgram program; - ASSERT_TRUE(program.unit.ast); - const auto client = analyzeInProgram(std::string(ClientHeader) + R"c( - int hook(void) { - char *p = malloc(8); - drop_hook(p); - return p[0]; - } - )c", - &program.db); - ASSERT_TRUE(client.ast); - // (`p` is used without a null test: RFC 0030 §3.2's off-by-default - // `allocation-failure`, which the engine reports and the frontend drops.) - EXPECT_EQ(ids(client.diagnostics), - (std::vector{"allocation-failure", "use-after-free"})); -} - -// RFC 0020: rebuilding already-numbered contexts preserves all their facts. -TEST(ProgramDatabase, TypeKeysIgnoreTypedefsAndRejectAnonymousRecords) { - const auto unit = analyze(R"c( - typedef struct node node_t; - typedef void (*cb_t)(node_t *, const char *); - static void a(node_t *n, const char *s) { (void)n; (void)s; } - cb_t table[] = { a }; - struct { int x; } anon; - static void b(int *p) { (void)p; } - void (*bp)(int *) = b; - )c"); - ASSERT_TRUE(unit.ast); - const clang::ASTContext &ctx = unit.ast->getASTContext(); - EXPECT_EQ(functionTypeKey(unit.function("a")->getType(), ctx), - "void (struct node *, const char *)"); - EXPECT_EQ(functionTypeKey(unit.function("b")->getType(), ctx), - "void (int *)"); - const auto anon = analyze(R"c( - static void c(struct { int x; } *p) { (void)p; } - void (*cp)(void *) = (void (*)(void *))c; - )c"); - ASSERT_TRUE(anon.ast); - EXPECT_EQ( - functionTypeKey(anon.function("c")->getType(), anon.ast->getASTContext()), - ""); -} - -TEST(ProgramDatabase, DumpListsFunctionsAndCandidates) { - NodeProgram program; - ASSERT_TRUE(program.unit.ast); - std::string text; - llvm::raw_string_ostream os(text); - program.db.dump(os); - EXPECT_NE(text.find("program:\n"), std::string::npos); - EXPECT_NE(text.find(" function 'node_free':\n"), std::string::npos) << text; - EXPECT_NE(text.find("release *param0 free when always"), std::string::npos) - << text; - EXPECT_NE(text.find(" candidate 'void (void *)':\n"), std::string::npos) - << text; - EXPECT_NE(text.find("'g_cache'"), std::string::npos) << text; -} - -TEST(ProgramDatabase, SeveralDefinitionsOfANameJoin) { - // RFC 0005, *Accepted false positives*: a name two units define gets the - // join of both definitions; what only one does becomes possible. - const auto first = analyze("void f(char *p) { free(p); }"); - const auto second = analyze("void f(char *p) { (void)p; }"); - ASSERT_TRUE(first.ast); - ASSERT_TRUE(second.ast); - ProgramDatabase db; - db.add(first.exports); - db.add(second.exports); - const core::FunctionEffects *joined = db.findEffects("f"); - ASSERT_NE(joined, nullptr); - ASSERT_EQ(joined->effects.size(), 1U); - EXPECT_EQ(joined->effects[0].kind, core::PathEffect::Kind::Release); - EXPECT_TRUE(joined->effects[0].may); -} - -TEST(ProgramDatabase, GlobalsAreNumberedByName) { - // Each unit numbers its globals; the database renumbers them by name, so - // the same global has one id whichever unit mentions it first. - const auto first = analyze(R"c( - char *a, *b; - void f(void) { free(b); } - )c"); - const auto second = analyze(R"c( - char *b; - void g(void) { free(b); } - )c"); - ASSERT_TRUE(first.ast); - ASSERT_TRUE(second.ast); - ProgramDatabase db; - db.add(first.exports); - db.add(second.exports); - const core::FunctionEffects *f = db.findEffects("f"); - const core::FunctionEffects *g = db.findEffects("g"); - ASSERT_NE(f, nullptr); - ASSERT_NE(g, nullptr); - ASSERT_FALSE(f->effects.empty()); - ASSERT_FALSE(g->effects.empty()); - EXPECT_EQ(f->effects[0].path, g->effects[0].path); - EXPECT_EQ(db.globals().nameOf(f->effects[0].path.index), "b"); -} - -} // namespace -} // namespace weavec::analysis diff --git a/unittests/Analysis/ResourceLifecycleTest.cpp b/unittests/Analysis/ResourceLifecycleTest.cpp deleted file mode 100644 index b5aaf056..00000000 --- a/unittests/Analysis/ResourceLifecycleTest.cpp +++ /dev/null @@ -1,650 +0,0 @@ -//===- ResourceLifecycleTest.cpp - Leaks, families, owned fields ----------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// RFC 0007: the checker's resource lifecycle rules. Each test names the RFC -// section it pins. -// -//===----------------------------------------------------------------------===// - -#include "TestUtils.h" -#include "weavec/Core/Diagnostic.h" - -#include - -#include - -namespace weavec::test { -namespace { - -using Strings = std::vector; - -/// The libc surface the tests need beyond `Prelude`; matched by name against -/// the shipped table (RFC 0003). -constexpr const char *Libc = R"c( -typedef struct FILE FILE; -FILE *fopen(const char *, const char *); -int fclose(FILE *); -char *strdup(const char *); -int getline(char **, size_t *, FILE *); -void exit(int) __attribute__((noreturn)); -void register_thing(void *); -#line 1 -)c"; - -// -- Leaks (RFC 0007, *Bugs caught*) ----------------------------------------- - -TEST(ResourceLifecycle, LeaksAreReportedWhereTheResourceIsLost) { - const auto result = analyze(std::string(Libc) + R"c( - int leak_path(int c) { - char *p = malloc(8); - if (c) - return -1; - free(p); - return 0; - } - void overwrite(void) { - char *p = malloc(8); - p = malloc(16); - free(p); - } - void owned_param(char *OWNED p) { use(p); } - void discarded(const char *s) { strdup(s); } - void scope_end(void) { - char *p = malloc(8); - use(p); - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(ids(result.diagnostics), - (Strings{"leak", "leak", "leak", "leak", "leak"})); - EXPECT_EQ(messages(result.diagnostics), - (Strings{"5: 'p' is leaked", - "11: 'p' is leaked: it is overwritten without being " - "released", - "14: 'p' is leaked", "15: result of 'strdup' is leaked", - "18: 'p' is leaked"})); - EXPECT_EQ(notes(result.diagnostics, 0), (Strings{"allocated here"})); - EXPECT_EQ(notes(result.diagnostics, 2), - (Strings{"'p' is declared WEAVEC_OWNED here"})); - EXPECT_TRUE(notes(result.diagnostics, 3).empty()) - << "a discarded result has no holder to point at"; -} - -TEST(ResourceLifecycle, OneReportPerResource) { - const auto result = analyze(R"c( - void copies(void) { - char *p = malloc(8); - char *q = p; - use(p); - use(q); - } - void both_arms(int c) { - char *p = malloc(8); - use(p); - if (c) - return; - use(p); - } - )c"); - ASSERT_TRUE(result.ast); - // The record travels with copies: one report, for the holder that dies - // last. A resource lost on two paths is reported once per path (RFC 0007, - // *Accepted false positives*), in the order the paths are visited. - Strings reported = messages(result.diagnostics); - std::sort(reported.begin(), reported.end()); - EXPECT_EQ(reported, (Strings{"12: 'p' is leaked", "13: 'p' is leaked", - "6: 'q' is leaked"})); -} - -TEST(ResourceLifecycle, NeverReadValuesAreLostAtOnce) { - const auto result = analyze(R"c( - void f(int c) { - char *p = malloc(8); - if (c) - use(0); - } - void g(void) { - char *p; - p = malloc(8); - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(messages(result.diagnostics), - (Strings{"4: 'p' is leaked", "9: 'p' is leaked"})); -} - -// -- What is not a leak (RFC 0007, *Deliberately not caught*) ----------------- - -TEST(ResourceLifecycle, ReleasedMovedReturnedOrStoredIsNotLost) { - const auto result = analyze(std::string(Libc) + R"c( - struct buf { char *data; }; - int checked(void) { - char *p = malloc(8); - if (!p) - return -1; - free(p); - return 0; - } - char *handed_out(void) { char *p = malloc(8); return p; } - void stored(struct buf *b) { b->data = malloc(8); } - void out_param(char **out) { *out = malloc(8); } - void moved(void) { char *p = malloc(8); take(p); } - void kept(void) { static char *keep; char *p = malloc(8); keep = p; } - char *global; - void in_global(void) { global = malloc(8); } - void by_callee(void) { char *p = malloc(8); if (p) free(p); } - struct buf make(void) { struct buf b; b.data = malloc(8); return b; } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(messages(result.diagnostics), Strings{}); -} - -TEST(ResourceLifecycle, EscapesAreNotLeaks) { - const auto result = analyze(std::string(Libc) + R"c( - typedef unsigned long uintptr_t; - void to_unknown(void) { char *p = malloc(8); register_thing(p); } - void to_int(void) { char *p = malloc(8); uintptr_t u = (uintptr_t)p; (void)u; } - void to_raw(void *RAW r); - void via_raw(void) { char *p = malloc(8); to_raw(p); } - void exits(void) { char *p = malloc(8); if (!p) exit(1); use(p); exit(0); } - void fatal(const char *m) __attribute__((noreturn)); - void dies_into_fatal(char *q) { - char *p = malloc(8); - if (q == NULL) fatal("no q"); - free(p); - } - struct box { char *buf; }; - struct box *lookup(int); - void below_opaque(int i) { - struct box *b = lookup(i); - b->buf = malloc(8); - } - void below_opaque_copy(int i) { - struct box *b = lookup(i); - char *p = malloc(8); - b->buf = p; - } - )c"); - ASSERT_TRUE(result.ast); - // Nothing: the unknown callees are ledger rows (RFC 0030 §5.1). - EXPECT_TRUE(result.diagnostics.empty()) << messages(result.diagnostics)[0]; -} - -// `tb = &L->strt; tb->hash = fresh`: the store lands in the caller's object -// under a name the summary cannot report, so the value escapes; the same -// store into a borrowed local is still this function's to release. -TEST(ResourceLifecycle, StoresBelowABorrowOfCallerMemoryEscape) { - const auto result = analyze(std::string(Libc) + R"c( - struct tb { char **hash; int size; }; - struct G { struct tb strt; }; - struct G global; - void resize(struct G *g, int nsize, int osize) { - struct tb *tb = &g->strt; - char **newvect = malloc(nsize); - if (newvect == NULL) return; - tb->hash = newvect; - tb->size = nsize; - if (nsize > osize) use(newvect); - } - void resize_global(int nsize) { - struct tb *tb = &global.strt; - tb->hash = malloc(nsize); - } - void resize_local(int nsize) { - struct tb local; - struct tb *tb = &local; - char **newvect = malloc(nsize); - if (newvect == NULL) return; - tb->hash = newvect; - use(newvect); - } - )c"); - ASSERT_TRUE(result.ast); - // RFC 0031 §5.8: the allocation is named after the place it was last - // stored in. - EXPECT_EQ(messages(result.diagnostics), - (Strings{"23: 'tb->hash' is leaked"})); -} - -TEST(ResourceLifecycle, NullTestsSeeThroughBuiltinExpectAndTruthComparisons) { - const auto result = analyze(std::string(Libc) + R"c( - int expect_null(void) { - char *p = malloc(8); - if (__builtin_expect((p == NULL), 0)) return -1; - free(p); - return 0; - } - int compared_with_zero(void) { - char *p = malloc(8); - if ((p != NULL) == 0) return -1; - free(p); - return 0; - } - int nested(void) { - char *p = malloc(8); - if (__builtin_expect((p == NULL) != 0, 0)) return -1; - free(p); - return 0; - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(messages(result.diagnostics), Strings{}); -} - -TEST(ResourceLifecycle, ALogicalOperatorComputedAsAValueDecidesItsOperands) { - // Under `__builtin_expect`, `!` or `!= 0` the `&&` is a value the branch - // tests, not the operand the short-circuit CFG evaluated last: a true `&&` - // makes both operands true (Lua's `luaM_realloc_`, where the fallback - // overwrites a null `newblock`), a false one says nothing about either. - const auto result = analyze(std::string(Libc) + R"c( - void *grow(void *block, size_t n) { - void *fresh = realloc(block, n); - if (__builtin_expect((fresh == NULL && n > 0), 0)) - fresh = malloc(n); - return fresh; - } - int and_false_says_nothing(size_t n) { - char *p = malloc(8); - if (__builtin_expect((n > 0 && p != NULL), 1)) { free(p); return 0; } - return -1; - } - int or_true_says_nothing(size_t n) { - char *p = malloc(8); - if (!(p == NULL || n == 0)) { free(p); return 0; } - return -1; - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(messages(result.diagnostics), - (Strings{"11: 'p' is leaked", "16: 'p' is leaked"})); -} - -TEST(ResourceLifecycle, ABodyThatKeepsNothingIsTrusted) { - const auto result = analyze(R"c( - static int peeks(char *p) { return p != NULL; } - static void keep_it(char **slot, char *p) { *slot = p; } - int trusted(void) { - char *p = malloc(8); - int r = peeks(p); - free(p); - return r; - } - void stored_by_callee(char **slot) { - char *p = malloc(8); - keep_it(slot, p); - } - int lost(void) { - char *p = malloc(8); - return peeks(p); - } - )c"); - ASSERT_TRUE(result.ast); - // `peeks` records no effect on `p`: passing it is not an escape, so `lost` - // is a leak; `keep_it` stores it where the caller can see (RFC 0007, - // *Assumptions*). - EXPECT_EQ(messages(result.diagnostics), (Strings{"16: 'p' is leaked"})); -} - -TEST(ResourceLifecycle, NullTestsClearTheRecordOnTheNullEdge) { - const auto result = analyze(std::string(Libc) + R"c( - int bang(void) { char *p = malloc(8); if (!p) return -1; free(p); return 0; } - int eq(void) { char *p = malloc(8); if (p == NULL) return -1; free(p); return 0; } - int assign(void) { char *p; if (!(p = malloc(8))) return -1; free(p); return 0; } - void lines(FILE *f) { - char *l = NULL; size_t n = 0; - while (getline(&l, &n, f) != -1) {} - free(l); - } - void goto_cleanup(void) { - char *a = NULL, *b = NULL; - a = malloc(8); if (!a) goto out; - b = malloc(8); if (!b) goto out; - out: - free(a); free(b); - } - int short_circuit(int fd) { - char *path; - if (fd == -1 || (path = (char *)malloc(16)) == NULL) return -1; - free(path); - return 0; - } - int nested(int fd, int mode) { - char *path; - if ((fd == -1 && mode) || !(path = malloc(16))) return -1; - free(path); - return 0; - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(messages(result.diagnostics), Strings{}); -} - -TEST(ResourceLifecycle, ReallocFailurePathIsNotALeak) { - // RFC 0030 §8.2: the null class of `realloc` keeps its argument when the - // size is not zero, and frees it when it is (`realloc(p, 0)`): freeing it - // again after a failure is a possible double free when the size may be - // zero. RFC 0031 *Implementation amendments*: a build with - // zero-initialisation asks for one byte instead of none, so there the - // null class always keeps the argument. - const std::string code = R"c( - void grow(char *OWNED p, size_t n) { - char *q = realloc(p, n); - if (!q) { free(p); return; } - free(q); - } - int in_place(char **pp, size_t n) { - char *q = realloc(*pp, n); - if (!q) return -1; - *pp = q; - return 0; - } - void grow_by(char *OWNED p) { - char *q = realloc(p, 16); - if (!q) { free(p); return; } - free(q); - } - )c"; - analysis::EngineOptions plain; - plain.zeroInit = false; - const auto result = analyze(code, plain); - ASSERT_TRUE(result.ast); - EXPECT_EQ(messages(result.diagnostics), Strings{"4: 'p' may be freed twice"}); - const auto wrapped = analyze(code); - ASSERT_TRUE(wrapped.ast); - EXPECT_EQ(messages(wrapped.diagnostics), Strings{}); -} - -TEST(ResourceLifecycle, FreopenBorrowsItsStream) { - // ISO C 7.21.5.4: `freopen` returns its stream reopened, or null with the - // stream closed. The `LibrarySpec` row (RFC 0030 §8) cannot say "released - // on the null class": the stream is borrowed and the result is the stream - // or null, so losing the only name on the null path reads as a leak (a - // warning), and closing the stream after a failure is not reported. The - // assignment keeps the stream on the non-null class; the null class loses - // it at the `return` (RFC 0031 §5.8). - const auto result = analyze(std::string(Libc) + R"c( - FILE *freopen(const char *, const char *, FILE *); - int reopen(const char *path) { - FILE *f = fopen(path, "r"); - if (!f) return -1; - f = freopen(path, "rb", f); - if (!f) return -2; - return fclose(f); - } - void closed_twice(const char *path) { - FILE *f = fopen(path, "r"); - if (!f) return; - if (!freopen(path, "rb", f)) { fclose(f); return; } - fclose(f); - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(messages(result.diagnostics), Strings{"7: 'f' is leaked"}); -} - -// -- Mismatched releases (RFC 0007, *Release families*) ----------------------- - -TEST(ResourceLifecycle, FamiliesAreInferredForWrappers) { - const auto result = analyze(std::string(Libc) + R"c( - FILE *open_log(const char *path) { return fopen(path, "a"); } - char *dup_or_fresh(const char *s, int c) { return c ? strdup(s) : malloc(4); } - void *either(int c) { return c ? (void *)fopen("x", "r") : malloc(4); } - )c"); - ASSERT_TRUE(result.ast); - ASSERT_NE(result.summary("open_log"), nullptr); - ASSERT_NE(result.summary("dup_or_fresh"), nullptr); - ASSERT_NE(result.summary("either"), nullptr); -} - -// -- Owned fields (RFC 0007, *Owned fields*) ---------------------------------- - -TEST(ResourceLifecycle, OwnedFieldsAreLostWithTheirContainer) { - const auto result = analyze(std::string(Libc) + R"c( - struct box { char *OWNED p; }; - struct buf { char *data; }; - void declared(struct box *b) { free(b); } - void inferred(struct buf *b) { b->data = malloc(8); free(b); } - void elements(char **a) { a[0] = strdup("x"); free(a); } - void released(struct box *b) { free(b->p); free(b); } - void nulled(struct box *b) { free(b->p); b->p = NULL; free(b); } - void maybe(struct box *b) { if (b->p) free(b->p); free(b); } - void moved_out(struct box *b, char **out) { *out = b->p; free(b); } - void fresh(void) { struct box *b = malloc(sizeof *b); if (b) free(b); } - void init(struct box *b, char *OWNED p) { b->p = p; } - void forgotten(struct buf *b) { b->data = malloc(8); b->data = NULL; } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(messages(result.diagnostics), - (Strings{"4: 'b->p' is leaked when 'b' is freed", - "5: 'b->data' is leaked when 'b' is freed", - "6: 'a[0]' is leaked when 'a' is freed", - "13: 'b->data' is leaked: it is overwritten without " - "being released"})); - EXPECT_EQ(notes(result.diagnostics, 0), - (Strings{"'p' is declared WEAVEC_OWNED here"})); - EXPECT_EQ(notes(result.diagnostics, 1), (Strings{"allocated here"})); -} - -TEST(ResourceLifecycle, DeclaredOwnedFieldsOfFreshObjectsOwnNothing) { - const auto result = analyze(R"c( - struct box { char *OWNED p; }; - static struct box *box_new(void) { return malloc(sizeof(struct box)); } - struct box *ctor(void) { - struct box *b = malloc(sizeof *b); - if (!b) return NULL; - b->p = malloc(4); - if (!b->p) { free(b); return NULL; } - return b; - } - void from_callee(void) { struct box *b = box_new(); if (b) free(b); } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(messages(result.diagnostics), Strings{}); -} - -// -- Deepest paths first (RFC 0007, *Applying a summary*) --------------------- - -TEST(ResourceLifecycle, CalleeFreeingFieldAndObjectFreesTheCallersAlias) { - const auto result = analyze(R"c( - struct box { char *p; }; - static void both(struct box *b) { free(b->p); free(b); } - static void both_reversed(struct box *b) { char *t = b->p; free(b); free(t); } - static void only_field(struct box *b) { free(b->p); } - void f(struct box *b) { char *q = b->p; both(b); q[0] = 1; } - void g(struct box *b) { char *q = b->p; both_reversed(b); q[0] = 1; } - void h(struct box *b) { char *q = b->p; only_field(b); q[0] = 1; } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(messages(result.diagnostics), - (Strings{"6: use of 'q' after it was freed", - "7: use of 'q' after it was freed", - "8: use of 'q' after it was freed"})); -} - -TEST(ResourceLifecycle, TwoSummaryPathsNamingOneCellAreOneRelease) { - // `g->twups ~ L` in the caller makes `L->g->allgc` and - // `L->g->twups->g->allgc` one cell; the callee's summary names both. - const auto result = analyze(R"c( - struct gs; - struct th { struct th *twups; struct gs *g; }; - struct gs { char *allgc; struct th *twups; }; - static void freeall(struct th *L) { - struct gs *g = L->g; - g->twups = L; - free(g->twups->g->allgc); - } - void close_state(struct th *L) { - L->g->twups = L; - freeall(L); - } - )c"); - ASSERT_TRUE(result.ast); - // RFC 0031 §6.3: the object engine reads `g->twups->g->allgc` through the - // objects `freeall` stored, so its summary names the one cell once, and - // the caller applies one release (the old engine left the call - // unanalysed). - EXPECT_TRUE(result.diagnostics.empty()) - << ::testing::PrintToString(messages(result.diagnostics)); - EXPECT_TRUE(test::incomplete(result).empty()); - const core::FunctionEffects *summary = result.summary("close_state"); - ASSERT_NE(summary, nullptr); - ASSERT_EQ(summary->effects.size(), 1U); - EXPECT_EQ(summary->effects[0].kind, core::PathEffect::Kind::Release); - EXPECT_FALSE(summary->effects[0].may); -} - -TEST(ResourceLifecycle, ContainerCheckRunsForLibraryReleasesOnly) { - const auto result = analyze(R"c( - struct box { char *p; }; - static void box_free(struct box *b) { free(b); } - void library(struct box *b) { b->p = malloc(8); free(b); } - void defined(struct box *b) { b->p = malloc(8); box_free(b); } - )c"); - ASSERT_TRUE(result.ast); - // The container form is for releases through the library table (RFC - // 0007). A defined destructor's summary releases `b` only, after which - // nothing reaches the allocation `b->p` held: lost at the call (RFC 0031 - // §5.8), a leak the old engine did not see. - EXPECT_EQ(messages(result.diagnostics), - (Strings{"4: 'b->p' is leaked when 'b' is freed", - "5: 'b->p' is leaked"})); -} - -TEST(ResourceLifecycle, ListBuildingLoopIsClean) { - const auto result = analyze(R"c( - struct item { struct item *next; struct item *prev; struct item *child; }; - static struct item *create(void) { return malloc(sizeof(struct item)); } - static void link(struct item *prev, struct item *item) { - prev->next = item; - item->prev = prev; - } - struct item *build(int count) { - int i; - struct item *n = NULL; - struct item *p = NULL; - struct item *a = create(); - for (i = 0; a && i < count; i++) { - n = create(); - if (!n) return a; - if (!p) a->child = n; else link(p, n); - p = n; - } - return a; - } - )c"); - ASSERT_TRUE(result.ast); - // Path-insensitively `a->child = n` runs every iteration; the previous - // node is still referenced from the list, through a pointer the analysis - // has since forgotten (RFC 0007, *Escape*). - EXPECT_TRUE(result.diagnostics.empty()) << messages(result.diagnostics)[0]; -} - -TEST(ResourceLifecycle, AnEscapedAliasMeansTheResourceEscaped) { - // zlib's `gz_fetch`: `state->x.next = state->out` is done again after a - // callee nobody can see took `state->out`. The one allocation escaped - // through the alias; overwriting the other name loses nothing (RFC 0007, - // *Escape*). - const auto result = analyze(R"c( - struct st { char *out; char *next; }; - void keep(char *p); - void g(struct st *s, int c) { - s->out = malloc(8); - s->next = s->out; - if (c) keep(s->out); - s->next = s->out; - } - )c"); - ASSERT_TRUE(result.ast); - // No leak; the unknown callee itself is a ledger row (RFC 0030 §5.1). - EXPECT_TRUE(result.diagnostics.empty()) << messages(result.diagnostics)[0]; -} - -TEST(ResourceLifecycle, AFreedElementDoesNotReleaseWhatAnotherElementHolds) { - // linenoise's history: `free(history[0])` leaves a record on `*history` - // witnessed by element 0; the store to `history[len]` is a different - // element, so the alias keeps `linecopy` (RFC 0006, *Element witnesses*). - const auto result = - analyze(std::string("char *strdup(const char *);\n#line 1\n") + R"c( - static char **history; - static int history_len; - int add(const char *line) { - char *linecopy = strdup(line); - if (!linecopy) return 0; - free(history[0]); - history[history_len] = linecopy; - history_len++; - return 1; - } - int add_first(const char *line) { - char *linecopy = strdup(line); - if (!linecopy) return 0; - free(history[0]); - history[0] = linecopy; - return 1; - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_TRUE(result.diagnostics.empty()) << messages(result.diagnostics)[0]; -} - -// -- Outcome-conditional null facts (RFC 0007, *Acquiring and losing*) -------- - -TEST(ResourceLifecycle, ACopyOfAConditionallyFreedPathIsNotTracked) { - // zlib's `gz_look` / `gz_fetch`: the callee frees `state->out` on its - // error path only and otherwise hands `state->x.next` a copy of it. The - // copy is not a resource of its own (overwriting it loses nothing), and - // not a dangling pointer either (`== -1` cannot retract the class). - const auto result = analyze(R"c( - struct gz { unsigned char *out; unsigned char *next; unsigned size; }; - static int look(struct gz *state) { - state->out = malloc(32); - if (state->out == NULL) { free(state->out); return -1; } - state->next = state->out; - return 0; - } - int fetch(struct gz *state) { - if (look(state) == -1) return -1; - state->next = state->out; - return 0; - } - int comp(struct gz *state) { - if (state->size == 0 && look(state) == -1) return -1; - state->next[0] = 1; - return 0; - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_TRUE(result.diagnostics.empty()) << messages(result.diagnostics)[0]; -} - -// -- Summaries carry families and reach callers ------------------------------ - -TEST(ResourceLifecycle, CalleeFreesWhatTheCallerAllocated) { - const auto result = analyze(R"c( - struct buf { char *data; }; - static void destroy(struct buf *b) { free(b->data); free(b); } - static void reset(struct buf *b) { free(b->data); b->data = NULL; } - void via_destroy(void) { - struct buf *b = malloc(sizeof *b); - if (!b) return; - b->data = malloc(4); - destroy(b); - } - void via_reset(struct buf *b) { - b->data = malloc(4); - reset(b); - b->data = malloc(8); - reset(b); - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(messages(result.diagnostics), Strings{}); -} - -} // namespace -} // namespace weavec::test diff --git a/unittests/Analysis/SharedOwnershipTest.cpp b/unittests/Analysis/SharedOwnershipTest.cpp deleted file mode 100644 index 85a9fb16..00000000 --- a/unittests/Analysis/SharedOwnershipTest.cpp +++ /dev/null @@ -1,134 +0,0 @@ -//===- SharedOwnershipTest.cpp - Reference counts, per-outcome stores -----===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// RFC 0010: shared ownership. Each test names the RFC section it pins. -// -//===----------------------------------------------------------------------===// - -#include "TestUtils.h" -#include "weavec/Analysis/ProgramDatabase.h" - -#include - -namespace weavec::analysis { -namespace { - -using core::SummaryPath; -using test::analyze; -using test::analyzeInProgram; -using test::ids; -using test::messages; -using test::notes; - -using Strings = std::vector; - -// -- Per-outcome stores (RFC 0010, *Per-outcome stores*) ---------------------- - -TEST(SharedOwnership, ARetractedStoreLeavesTheResourceWithTheCaller) { - const auto result = analyze(R"c( - int put(char **slot, char *s, int room) { - if (!room) return -1; - *slot = s; - return 0; - } - void retracted(char **slot, int room) { - char *s = malloc(8); - if (put(slot, s, room) < 0) { - return; - } - } - void kept(char **slot, int room) { - char *s = malloc(8); - if (put(slot, s, room) == 0) - return; - use(s); - } - )c"); - ASSERT_TRUE(result.ast); - // `retracted`: on the failure edge the store did not happen, so `s` is - // still this function's and is lost at the return. `kept`: on the failure - // edge it is lost at the end. - EXPECT_EQ(messages(result.diagnostics), - (Strings{"10: 's' is leaked", "17: 's' is leaked"})); -} - -// -- Annotations (RFC 0010, *Annotations*) ----------------------------------- - -TEST(SharedOwnership, AnnotatedRetainAndRelease) { - const auto result = analyze(R"c( - struct obj; - struct obj *g_new(void) OWNED; - struct obj *g_ref(struct obj *RETAINS o); - void g_unref(struct obj *RELEASES o); - int balanced(void) { - struct obj *a = g_new(); - if (!a) return -1; - struct obj *b = g_ref(a); - g_unref(b); - g_unref(a); - return 0; - } - int twice(void) { - struct obj *a = g_new(); - if (!a) return -1; - g_unref(a); - g_unref(a); - return 0; - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(messages(result.diagnostics), - (Strings{"18: 'a' is released twice"})) - << "the result of a returning ref is a copy of its argument"; -} - -TEST(SharedOwnership, OwnedByNamesTheFamily) { - const auto result = analyze(R"c( - struct obj; - void obj_destroy(struct obj *OWNED o); - struct obj *obj_make(void) OWNED OWNED_BY(obj_destroy); - int right(void) { - struct obj *o = obj_make(); - if (!o) return -1; - obj_destroy(o); - return 0; - } - int wrong(void) { - struct obj *o = obj_make(); - if (!o) return -1; - free(o); - return 0; - } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(ids(result.diagnostics), (Strings{"mismatched-release"})); - EXPECT_EQ(messages(result.diagnostics), - (Strings{"14: 'o' is released with 'free' but must be released " - "with 'obj_destroy'"})); -} - -TEST(SharedOwnership, InvalidShareAnnotations) { - const auto result = analyze(R"c( - struct obj; - void both(struct obj *RETAINS RELEASES o) { use(o); } - struct obj *family_alone(void) OWNED_BY(obj_destroy) { return NULL; } - )c"); - ASSERT_TRUE(result.ast); - EXPECT_EQ(ids(result.diagnostics), - (Strings{"invalid-annotation", "invalid-annotation"})); - EXPECT_EQ( - messages(result.diagnostics), - (Strings{"3: 'o' is declared both WEAVEC_RETAINS and WEAVEC_RELEASES", - "4: 'family_alone' is declared WEAVEC_OWNED_BY(obj_destroy) " - "without WEAVEC_OWNED"})); -} - -// -- Whole program (RFC 0010, *Across translation units*) --------------------- - -} // namespace -} // namespace weavec::analysis diff --git a/unittests/Analysis/SiteCollectorTest.cpp b/unittests/Analysis/SiteCollectorTest.cpp deleted file mode 100644 index 7932a267..00000000 --- a/unittests/Analysis/SiteCollectorTest.cpp +++ /dev/null @@ -1,391 +0,0 @@ -//===- SiteCollectorTest.cpp - Tests for SiteCollector --------------------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// RFC 0030 §2.1, §2.6: the sites of every emitted function, their kinds, -// ordinals, facets, exclusions and markings. -// -//===----------------------------------------------------------------------===// - -#include "weavec/Analysis/SiteCollector.h" - -#include "SiteTestUtils.h" - -#include "clang/AST/Expr.h" - -#include - -#include -#include -#include - -namespace weavec::analysis { - -using test::collectUnit; -using test::describe; -using Lines = std::vector; - -static std::vector emittedNames(const test::CollectedUnit &unit) { - std::vector names; - for (const core::FunctionLedger &row : unit.sites.ledgers()) - names.push_back(row.name); - return names; -} - -namespace { - -// §2.6 step 1: definitions CodeGen may emit, never system-header ones. -TEST(SiteCollector, SelectsEmittedFunctions) { - const auto unit = collectUnit(R"c( -#include -void external(void) {} -static void unusedStatic(void) {} -static void usedStatic(void) {} -static inline void unusedInline(void) {} -static inline void usedInline(void) { sysInline(); } -__attribute__((used)) static void keptStatic(void) {} -inline int c99Inline(int x) { return x; } -extern inline __attribute__((gnu_inline)) int gnuInline(int x) { return x; } -void declared(void); -void caller(void) { usedStatic(); usedInline(); } -)c", - "static inline void sysInline(void) {}\n", - /*prelude=*/false); - EXPECT_EQ(emittedNames(unit), - (Lines{"external", "usedStatic", "usedInline", "keptStatic", - "c99Inline", "gnuInline", "caller"})); - ASSERT_NE(unit.row("usedStatic"), nullptr); - EXPECT_EQ(unit.row("external")->line, 3U); -} - -// §2.1 Deref: `*p`, `p->f`, `p[0]`; only null when the lvalue's address is -// taken or it decays; `&*p` and the `offsetof` idiom make no site. -TEST(SiteCollector, Dereferences) { - const auto unit = collectUnit(R"c( -struct s { int f; int arr[4]; int tail; }; -unsigned long f(int *p, struct s *q, int (*rows)[4]) { - int a = *p + q->f + p[0]; - int *b = &q->f; - int *c = q->arr; - int *d = &*p; - unsigned long off = (unsigned long)&((struct s *)0)->tail; - int e = rows[0][1]; - return a + *b + *c + *d + (unsigned long)off + e; -} -)c"); - EXPECT_EQ( - describe(unit, "f"), - (Lines{"deref *p spatial,null,temporal", - "deref q->f spatial,null,temporal", - "deref p[0] spatial,null,temporal", "deref q->f null", - "deref q->arr null", "deref rows[0] null", - "index rows[0][1] spatial,temporal", - std::string("call/exit return") + - "a+*b+*c+*d+(unsignedlong)off+e temporal", - "deref *b spatial,null,temporal", "deref *c spatial,null,temporal", - "deref *d spatial,null,temporal"})); -} - -// §2.1 Index: subscripts, `*(p + i)`; null and temporal only for pointer -// bases, temporal for arrays whose storage can end; `&p[i]` makes no site. -TEST(SiteCollector, Subscripts) { - const auto unit = collectUnit(R"c( -struct s { int n; int arr[4]; int more; }; -int g[8]; -int f(int *p, struct s *q, int i) { - int local[4] = {0}; - static int kept[2]; - int *at = &p[i]; - return p[i] + *(p + i) + local[i] + g[i] + q->arr[i] + kept[i] + *at + - "abc"[i]; -} -)c"); - EXPECT_EQ( - describe(unit, "f"), - (Lines{std::string("call/exit return") + - "p[i]+*(p+i)+local[i]+g[i]+q->arr[i]+" + - "kept[i]+*at+\"abc\"[i] temporal", - "index p[i] spatial,null,temporal", - "index *(p+i) spatial,null,temporal", "index local[i] spatial", - "index g[i] spatial", "deref q->arr null", - "index q->arr[i] spatial,temporal", "index kept[i] spatial", - "deref *at spatial,null,temporal", "index \"abc\"[i] spatial"})); -} - -// §2.1: the ordinal is the position in source order; operations that begin -// at the same place come inner first. -TEST(SiteCollector, OrdinalsFollowSourceOrder) { - const auto unit = collectUnit(R"c( -struct n { struct n *next; int v; }; -int f(struct n *p, int *q) { - struct n x = { .v = *q, .next = p->next }; - return p->next->next->v + x.v; -} -)c"); - const core::FunctionLedger *row = unit.row("f"); - ASSERT_NE(row, nullptr); - for (std::size_t i = 0; i < row->sites.size(); ++i) - EXPECT_EQ(row->sites[i].ordinal, i); - // The designated initialiser is walked in its semantic form (`.next` - // first), yet `*q` comes first because it comes first in the source. - EXPECT_EQ(describe(unit, "f"), - (Lines{"deref *q spatial,null,temporal", - "deref p->next spatial,null,temporal", - "call/exit returnp->next->next->v+x.v temporal", - "deref p->next spatial,null,temporal", - "deref p->next->next spatial,null,temporal", - "deref p->next->next->v spatial,null,temporal"})); -} - -// §2.1 LibCall and Release: facets as the row's requirements name them. -TEST(SiteCollector, LibraryCalls) { - const auto unit = collectUnit(R"c( -void f(char *d, const char *s, FILE *file, char *local) { - char buf[8]; - memcpy(d, s, 4); - memcpy(buf, "abc", 4); - (void)strlen(s); - free(d); - fclose(file); - free(buf); - char *m = malloc(4); - (void)m; - (void)local; -} -)c"); - EXPECT_EQ(describe(unit, "f"), - (Lines{"lib-call memcpy(d,s,4) spatial,null,temporal", - "lib-call memcpy(buf,\"abc\",4) spatial", - "lib-call strlen(s) spatial,null,temporal", - "release free(d) spatial,temporal", - "release fclose(file) spatial,null,temporal", - "release free(buf) spatial", "lib-call malloc(4) -", - "call/exit } temporal"})); - const core::FunctionLedger *row = unit.row("f"); - ASSERT_NE(row, nullptr); - EXPECT_EQ(row->sites[0].callee, "memcpy"); - const SiteInfo *copy = - unit.sites.info(core::SiteId{.function = 0, .ordinal = 0}); - ASSERT_NE(copy, nullptr); - ASSERT_TRUE(copy->library.has_value()); - EXPECT_EQ(copy->library->entry->name, "memcpy"); - // memcpy's pointers may be null when the length is zero (§8.3). - ASSERT_EQ(copy->arguments.size(), 2U); - EXPECT_TRUE(copy->arguments[0].allowedIfZero); - EXPECT_TRUE(copy->arguments[0].unlessZero.has_value()); -} - -// §2.1 Call: every other call; every exit, including calls that do not -// return; the end of the body only when control can reach it. -TEST(SiteCollector, CallsAndExits) { - const auto unit = collectUnit(R"c( -_Noreturn void die(const char *why); -void user(int *p); -int helper(int *p) { return *p; } -int f(int *p, void (*cb)(int *), int c) { - if (c) - exit(1); - if (c > 1) - die("x"); - cb(p); - user(p); - return helper(p); -} -void g(int c) { if (c) return; } -void h(int c) { if (c) return; else return; } -void loop(void) { for (;;) {} } -)c"); - EXPECT_EQ( - describe(unit, "f"), - (Lines{"lib-call exit(1) -", "call/exit exit(1) temporal", - "call/call die(\"x\") temporal", "call/exit die(\"x\") temporal", - "call/call cb(p) null,temporal", "call/call user(p) temporal", - "call/exit returnhelper(p) temporal", - "call/call helper(p) temporal"})); - EXPECT_EQ(describe(unit, "g"), - (Lines{"call/exit return temporal", "call/exit } temporal"})); - EXPECT_EQ(describe(unit, "h"), - (Lines{"call/exit return temporal", "call/exit return temporal"})); - EXPECT_EQ(describe(unit, "loop"), Lines{}); - const core::FunctionLedger *row = unit.row("f"); - ASSERT_NE(row, nullptr); - EXPECT_EQ(row->sites[4].callee, ""); - EXPECT_EQ(row->sites[5].callee, "user"); -} - -// §2.1 Assume, IntToPtr and Raw. -TEST(SiteCollector, AssumeIntToPtrAndRaw) { - const auto unit = collectUnit(R"c( -int *RAW mapped(void); -int f(unsigned long addr, int *RAW r, int n, ...) { - ASSUME(n > 0); - int *null = (int *)0; - __builtin_va_list ap; - __builtin_va_start(ap, n); - int *v = __builtin_va_arg(ap, int *); - __builtin_va_end(ap); - free(r); - return *(int *)addr + r[1] + *mapped() + *v + (null == 0); -} -)c"); - const Lines sites = describe(unit, "f"); - EXPECT_EQ(sites.front(), "assume ASSUME(n>0) assertion"); - EXPECT_NE(std::ranges::find( - sites, "int-to-ptr __builtin_va_arg(ap,int*) spatial,temporal"), - sites.end()); - EXPECT_NE(std::ranges::find(sites, "raw free(r) spatial,temporal"), - sites.end()); - // RFC 0033 §2: a pointer converted from an integer is not raw; its - // dereference is an ordinary one. - EXPECT_NE(std::ranges::find(sites, "deref *(int*)addr spatial,null,temporal"), - sites.end()); - EXPECT_NE(std::ranges::find(sites, "int-to-ptr (int*)addr spatial,temporal"), - sites.end()); - EXPECT_NE(std::ranges::find(sites, "raw r[1] spatial,null,temporal"), - sites.end()); - EXPECT_NE(std::ranges::find(sites, "raw *mapped() spatial,null,temporal"), - sites.end()); - // `(int *)0` is a null pointer constant, not a conversion from an integer. - EXPECT_EQ(std::ranges::count_if(sites, - [](const std::string &line) { - return line.starts_with("int-to-ptr"); - }), - 2); -} - -// §2.1 PtrArith and Cast: only in required positions (§7.4). -TEST(SiteCollector, RequiredPositions) { - const auto unit = collectUnit(R"c( -struct buf { char *SIZED_BY(cap) data; unsigned long cap; char *plain; }; -void take(int *SIZED_BY(n) p, int n); -void loose(int *p); -struct big { long a, b; }; -void big(struct big *p __attribute__((nonnull)), int n); -void f(int *p, char *c, struct buf *b, int n) { - take(p + 1, n); - loose(p + 1); - take(&p[2], n); - b->data = c + 1; - b->data = c; - b->data = 0; - b->plain = c + 1; - ++b->data; - struct buf local = { .data = c, .cap = 4 }; - (void)local; -} -)c"); - EXPECT_EQ( - describe(unit, "f"), - (Lines{"call/call take(p+1,n) spatial,temporal", "ptr-arith p+1 spatial", - "call/call loose(p+1) temporal", - "call/call take(&p[2],n) spatial,temporal", - "ptr-arith &p[2] spatial", "deref b->data spatial,null,temporal", - "ptr-arith c+1 spatial", "deref b->data spatial,null,temporal", - "cast b->data=c spatial", "deref b->data spatial,null,temporal", - "deref b->plain spatial,null,temporal", - "ptr-arith ++b->data spatial", - "deref b->data spatial,null,temporal", "cast c spatial", - "call/exit } temporal"})); -} - -// §2.1 exclusions: unevaluated operands, except variable-length arrays. -TEST(SiteCollector, UnevaluatedOperands) { - const auto unit = collectUnit(R"c( -unsigned long f(int *p, int n, int (*vla)[n]) { - unsigned long a = sizeof(*p) + _Alignof(int); - unsigned long b = sizeof(*vla); - int c = _Generic(0, int: 1, default: *p); - int d = __builtin_choose_expr(1, 2, *p); - int e = __builtin_constant_p(*p); - return a + b + (unsigned long)(c + d + e); -} -)c"); - EXPECT_EQ(describe(unit, "f"), - (Lines{"deref *vla null", - "call/exit returna+b+(unsignedlong)(c+d+e) temporal"})); -} - -// §6.1, §5.4: unsafe regions and returns-twice callers are marked. -TEST(SiteCollector, MarksUnsafeRegionsAndSetjmp) { - const auto unit = collectUnit(R"c( -jmp_buf env; -int f(int *p) { - int x = *p; - UNSAFE { x += p[1]; } - return x; -} -UNSAFE int g(int *p) { return *p; } -int h(int *p) { if (setjmp(env)) return 0; return *p; } -)c"); - const auto flags = [&](const char *name) { - std::vector out; - for (const SiteInfo &site : - unit.sites.function(*unit.function(name))->sites) - out.push_back(site.inUnsafe); - return out; - }; - EXPECT_EQ(flags("f"), (std::vector{false, true, false})); - EXPECT_EQ(flags("g"), (std::vector{true, true})); - EXPECT_FALSE(unit.sites.function(*unit.function("f"))->callsSetjmp); - EXPECT_TRUE(unit.sites.function(*unit.function("h"))->callsSetjmp); -} - -// The index finds every site by statement, and the exit of a call that does -// not return separately. -TEST(SiteCollector, IndexesSitesByStatement) { - const auto unit = collectUnit(R"c( -int f(int *p) { - if (!p) - exit(2); - return *p; -} -)c"); - const SiteIndex::FunctionSites *function = - unit.sites.function(*unit.function("f")); - ASSERT_NE(function, nullptr); - ASSERT_EQ(function->sites.size(), 4U); - const auto *call = llvm::cast(function->sites[0].stmt); - EXPECT_EQ(unit.sites.sitesOf(*call).size(), 2U); - EXPECT_EQ(unit.sites.find(*call)->ordinal, 0U); - EXPECT_EQ(unit.sites.findExit(*call)->ordinal, 1U); - EXPECT_EQ(unit.sites.find(*call, core::SiteKind::LibCall)->ordinal, 0U); - const clang::Stmt *ret = function->sites[2].stmt; - EXPECT_TRUE(llvm::isa(ret)); - EXPECT_EQ(unit.sites.find(*ret)->ordinal, 2U); - EXPECT_EQ( - unit.sites.info(core::SiteId{.function = function->index, .ordinal = 3}) - ->kind, - core::SiteKind::Deref); - // `weavec_assume_` is unused here, so `f` is the only emitted function. - EXPECT_EQ(unit.sites.functions().size(), 1U); -} - -// §5.3: a constant index inside a non-flexible array is proven by the -// types alone. -TEST(SiteCollector, ProvenByType) { - const auto unit = collectUnit(R"c( -struct flex { int n; int tail[1]; }; -struct fixed { int a[4]; int n; }; -int f(int *SIZED_BY(n) p, int n, int *q, int *SIZED_BY(m) r, int m, - struct flex *x, struct fixed *y, int i) { - int local[3] = {0}; - r = q; - return local[i] + p[i] + q[i] + r[i] + x->tail[i] + y->a[i] + local[1]; -} -)c"); - const SiteIndex::FunctionSites *function = - unit.sites.function(*unit.function("f")); - ASSERT_NE(function, nullptr); - Lines byType; - for (const SiteInfo &site : function->sites) - if (site.kind == core::SiteKind::Index && site.provenByType) - byType.push_back(unit.row("f")->sites[site.id.ordinal].text); - EXPECT_EQ(byType, (Lines{"local[1]"})); -} - -} // namespace -} // namespace weavec::analysis diff --git a/unittests/Analysis/SiteTestUtils.h b/unittests/Analysis/SiteTestUtils.h deleted file mode 100644 index 4b5a3b1a..00000000 --- a/unittests/Analysis/SiteTestUtils.h +++ /dev/null @@ -1,173 +0,0 @@ -//===- SiteTestUtils.h - Helpers for the RFC 0030 seam tests ---*- C++ -*-===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// - -#ifndef WEAVEC_UNITTESTS_ANALYSIS_SITETESTUTILS_H -#define WEAVEC_UNITTESTS_ANALYSIS_SITETESTUTILS_H - -#include "weavec/Analysis/AttributeReader.h" -#include "weavec/Analysis/KindTable.h" -#include "weavec/Analysis/SiteCollector.h" -#include "weavec/Core/Ledger.h" -#include "weavec/Core/LibrarySpec.h" - -#include "clang/AST/ASTContext.h" -#include "clang/AST/Decl.h" -#include "clang/Frontend/ASTUnit.h" -#include "clang/Tooling/Tooling.h" - -#include - -#include -#include -#include -#include -#include - -namespace weavec::test { - -/// A decision as `[/]`. -inline std::string compactOf(const core::FacetDecision &decision) { - std::string text(core::toString(decision.outcome)); - if (const std::string_view reason = decision.reasonText(); !reason.empty()) - text.append("/").append(reason); - return text; -} - -/// A severity as the diagnostics print it. -inline std::string severityText(core::Severity severity) { - switch (severity) { - case core::Severity::Note: - return "note"; - case core::Severity::Warning: - return "warning"; - case core::Severity::Error: - return "error"; - } - return "?"; -} - -/// A Call site's boundary: `call` or `exit`. -inline std::string boundaryText(core::Boundary boundary) { - return boundary == core::Boundary::Call ? "call" : "exit"; -} - -/// The declarations the site tests call, spelled as the C library does; the -/// `LibrarySpec` matches them by name and signature. `sys.h` is a system -/// header. -inline constexpr const char *SitePrelude = R"c( -typedef unsigned long size_t; -typedef struct FILE FILE; -void *malloc(size_t); -void free(void *); -void *memcpy(void *, const void *, size_t); -size_t strlen(const char *); -int fclose(FILE *); -void exit(int) __attribute__((noreturn)); -typedef long jmp_buf[48]; -int setjmp(jmp_buf) __attribute__((returns_twice)); -#define RAW __attribute__((annotate("weavec.raw"))) -#define UNSAFE __attribute__((annotate("weavec.unsafe"))) -#define NONNULL __attribute__((annotate("weavec.nonnull"))) -#define NULLABLE __attribute__((annotate("weavec.nullable"))) -#define SIZED_BY(n) __attribute__((annotate("weavec.sized_by." #n))) -__attribute__((annotate("weavec.assume"))) static inline void -weavec_assume_(int c) { (void)c; } -#define ASSUME(e) weavec_assume_((e) != 0) -#line 1 -)c"; - -/// A parsed unit with its declared kinds and sites. -struct CollectedUnit { - std::unique_ptr ast; - analysis::KindTable kinds; - analysis::SiteIndex sites; - - [[nodiscard]] clang::ASTContext &context() const { - return ast->getASTContext(); - } - /// The function definition named `name` (or any declaration of it). - [[nodiscard]] const clang::FunctionDecl * - function(llvm::StringRef name) const { - const clang::FunctionDecl *found = nullptr; - for (const clang::Decl *decl : - context().getTranslationUnitDecl()->decls()) { - const auto *fn = llvm::dyn_cast(decl); - if (fn == nullptr || fn->getName() != name) - continue; - found = fn; - if (fn->doesThisDeclarationHaveABody()) - return fn; - } - return found; - } - /// The ledger rows of the function named `name`, or null. - [[nodiscard]] const core::FunctionLedger *row(llvm::StringRef name) const { - for (const core::FunctionLedger &row : sites.ledgers()) - if (row.name == name) - return &row; - return nullptr; - } -}; - -/// Parses `code` (after `SitePrelude` unless `prelude` is false) as C with -/// `sys.h` available as a system header, and collects its sites. -inline CollectedUnit collectUnit(const std::string &code, - const std::string &systemHeader = {}, - bool prelude = true, - std::vector args = {}) { - CollectedUnit unit; - std::vector arguments{ - "-std=gnu17", "-x", "c", "-w", "-isystem", "/virtual/include"}; - arguments.insert(arguments.end(), args.begin(), args.end()); - clang::tooling::FileContentMappings files; - if (!systemHeader.empty()) - files.emplace_back("/virtual/include/sys.h", - "#pragma clang system_header\n" + systemHeader); - unit.ast = clang::tooling::buildASTFromCodeWithArgs( - (prelude ? std::string(SitePrelude) : std::string()) + code, arguments, - "input.c", "clang-tool", - std::make_shared(), - clang::tooling::getClangStripDependencyFileAdjuster(), files); - EXPECT_NE(unit.ast, nullptr); - if (unit.ast == nullptr) - return unit; - EXPECT_FALSE(unit.ast->getDiagnostics().hasErrorOccurred()); - const core::LibrarySpec &library = core::LibrarySpec::shipped(); - unit.kinds = analysis::AttributeReader(unit.context(), library).read(); - unit.sites = - analysis::SiteCollector(unit.context(), unit.kinds, library).collect(); - return unit; -} - -/// ` ` for every site of one function, in ordinal -/// order: `deref *p spatial,null,temporal`. Call sites spell their boundary -/// (`call/exit`). -inline std::vector describe(const CollectedUnit &unit, - llvm::StringRef function) { - std::vector out; - const core::FunctionLedger *row = unit.row(function); - if (row == nullptr) - return out; - for (const core::Site &site : row->sites) { - std::string text(core::toString(site.kind)); - if (site.boundary) - text += "/" + boundaryText(*site.boundary); - text += " " + site.text + " "; - std::string facets; - for (const core::Facet facet : core::AllFacets) - if (site.hasFacet(facet)) - facets += - (facets.empty() ? "" : ",") + std::string(core::toString(facet)); - out.push_back(text + (facets.empty() ? "-" : facets)); - } - return out; -} - -} // namespace weavec::test - -#endif // WEAVEC_UNITTESTS_ANALYSIS_SITETESTUTILS_H diff --git a/unittests/Analysis/SlotCollectorTest.cpp b/unittests/Analysis/SlotCollectorTest.cpp deleted file mode 100644 index 1f2d3e40..00000000 --- a/unittests/Analysis/SlotCollectorTest.cpp +++ /dev/null @@ -1,343 +0,0 @@ -//===- SlotCollectorTest.cpp - Tests for SlotCollector (RFC 0030) ---------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// RFC 0030 §9.3: the constraints of a unit's function-pointer values, the -// per-TU closedness rules, the export form, and the Lua allocator pattern. -// -//===----------------------------------------------------------------------===// - -#include "weavec/Analysis/SlotCollector.h" - -#include "weavec/Core/FnSlots.h" -#include "weavec/Core/LibrarySpec.h" - -#include "clang/Frontend/ASTUnit.h" -#include "clang/Tooling/Tooling.h" - -#include - -#include -#include -#include -#include -#include - -namespace weavec::analysis { -namespace { - -using core::SlotKey; -using Targets = std::set; - -struct CollectedSlots { - std::unique_ptr ast; - SlotCollection slots; - core::SlotSolution solution; - - /// Every constraint, one per line: `member`, - /// `subset` or `open`. - [[nodiscard]] std::string text() const { - std::string out; - for (const core::SlotConstraint &c : slots.constraints().constraints()) { - switch (c.kind) { - case core::SlotConstraint::Kind::Member: - out += "member\t" + c.function + "\t" + c.slot.toString(); - break; - case core::SlotConstraint::Kind::Subset: - out += "subset\t" + c.from.toString() + "\t" + c.slot.toString(); - break; - case core::SlotConstraint::Kind::Open: - out += "open\t" + c.slot.toString() + "\t" + c.detail; - break; - } - out += '\n'; - } - return out; - } - [[nodiscard]] bool has(const std::string &line) const { - return text().find(line + "\n") != std::string::npos; - } -}; - -} // namespace - -/// Parses `code` as the unit `unit` (with `header` as `lib.h` on the include -/// path) and collects its slots. -static std::unique_ptr -collect(const std::string &code, const std::string &header = {}, - const std::string &unit = "a.c") { - auto out = std::make_unique(); - clang::tooling::FileContentMappings files; - if (!header.empty()) - files.emplace_back("/virtual/lib.h", header); - out->ast = clang::tooling::buildASTFromCodeWithArgs( - code, {"-std=gnu17", "-x", "c", "-w", "-I", "/virtual"}, unit, - "clang-tool", std::make_shared(), - clang::tooling::getClangStripDependencyFileAdjuster(), files); - EXPECT_NE(out->ast, nullptr); - if (out->ast == nullptr) - return out; - EXPECT_FALSE(out->ast->getDiagnostics().hasErrorOccurred()); - out->slots = - SlotCollector(out->ast->getASTContext(), core::LibrarySpec::shipped(), - SlotCollectorOptions{.unit = unit}) - .collect(); - out->solution = out->slots.solve(); - return out; -} - -namespace { - -TEST(SlotCollector, MembersCopiesAndCalls) { - const auto unit = collect(R"c( -typedef void (*fn)(void); -static void a(void) {} -static void b(void) {} -void c(void) {} -static fn table[] = {a, b}; -fn global = c; -static fn pick(int i) { return i ? a : table[i]; } -static void run(fn f) { f(); } -void drive(int i) { - fn local = pick(i); - run(local); - run((fn)&c); - global = local; -} -)c"); - for (const char *line : - {"member\ta.c:a\tstatic a.c:table", "member\ta.c:b\tstatic a.c:table", - "member\tc\tglobal global", "member\ta.c:a\tresult a.c:pick", - "subset\tstatic a.c:table\tresult a.c:pick", - "subset\tresult a.c:pick\tlocal drive local", - "subset\tlocal drive local\tparam a.c:run 0", - "member\tc\tparam a.c:run 0", - "subset\tlocal drive local\tglobal global"}) - EXPECT_TRUE(unit->has(line)) << line << "\n" << unit->text(); - EXPECT_EQ(unit->solution.targets(SlotKey::param("a.c:run", 0)), - (Targets{"a.c:a", "a.c:b", "c"})); - // The call through `run`'s parameter: closed (`run` is static and its - // address never escapes), with three targets. - ASSERT_EQ(unit->slots.indirectCalls().size(), 1U); - const auto &[call, slot] = unit->slots.indirectCalls().front(); - EXPECT_EQ(unit->slots.calleeSlot(*call), slot); - EXPECT_EQ(slot, SlotKey::param("a.c:run", 0)); - const core::CallResolution resolution = unit->solution.resolveCall(slot); - EXPECT_EQ(resolution.kind, core::IndirectCallKind::ClosedJoin); - EXPECT_EQ(resolution.targets, - (std::vector{"a.c:a", "a.c:b", "c"})); - // `global` is externally visible: open per TU. - EXPECT_TRUE(unit->solution.isOpen(SlotKey::global("global"))); - EXPECT_NE(unit->slots.function("a.c:a"), nullptr); - EXPECT_NE(unit->slots.function("c"), nullptr); - EXPECT_EQ(unit->slots.function("missing"), nullptr); - EXPECT_TRUE(unit->slots.rules().defined.contains("a.c:pick")); - EXPECT_TRUE(unit->slots.rules().exported.contains("drive")); - EXPECT_FALSE(unit->slots.rules().exported.contains("a.c:pick")); -} - -TEST(SlotCollector, IndirectCallsThroughSlots) { - const auto unit = collect(R"c( -typedef int (*op)(int); -typedef op (*maker)(void); -static int inc(int x) { return x + 1; } -static op make(void) { return inc; } -struct ops { maker make; op apply; }; -static struct ops all = {make, inc}; -int apply(int x, int c) { - op chosen = c ? all.apply : all.make(); - return chosen(x) + (c ? inc : all.apply)(x); -} -)c"); - // `all.make()` is a call through a field; its result flows to `chosen`. - EXPECT_TRUE(unit->has( - "subset\tcall-result field a.c:struct ops make\tlocal apply chosen")) - << unit->text(); - std::vector kinds; - for (const auto &[call, slot] : unit->slots.indirectCalls()) - kinds.push_back( - slot.toString() + " " + - std::string(core::toString(unit->solution.resolveCall(slot).kind))); - // A callee expression with no single slot gets its own local slot. - EXPECT_EQ(kinds, (std::vector{ - "field a.c:struct ops make closed-single", - "local apply chosen closed-single", - "local apply closed-single"})); - EXPECT_EQ(unit->solution.targets(SlotKey::local("apply", "chosen")), - Targets{"a.c:inc"}); - EXPECT_TRUE(unit->slots.rules().confinedRecords.contains("a.c:struct ops")); -} - -TEST(SlotCollector, OpenSources) { - const auto unit = collect(R"c( -typedef void (*fn)(void); -typedef __builtin_va_list va_list; -struct box { fn f; }; -union either { fn f; long n; }; -struct packed { fn f; }; -fn from_int(unsigned long bits) { fn f = (fn)bits; return f; } -fn from_data(void *p) { fn f = (fn)p; return f; } -fn through(fn *pp) { return *pp; } -fn varargs(int n, ...) { - va_list ap; - __builtin_va_start(ap, n); - fn f = __builtin_va_arg(ap, fn); - __builtin_va_end(ap); - return f; -} -void *memcpy(void *, const void *, unsigned long); -void writes(union either *u, struct packed *p, const char *raw) { - u->n = 1; - memcpy(p, raw, sizeof *p); -} -)c"); - const std::string punned = std::string("open\tfield a.c:union either f\t") + - "a store to 'n', another member of the union"; - for (const std::string &line : - {std::string("open\tlocal from_int f\ta conversion from an integer"), - std::string( - "open\tlocal from_data f\ta conversion from a data pointer"), - std::string("open\tresult through\ta load through a pointer"), - std::string("open\tlocal varargs f\ta value from va_arg"), punned, - std::string( - "open\tfield a.c:struct packed f\ta byte-wise write by 'memcpy'")}) - EXPECT_TRUE(unit->has(line)) << line << "\n" << unit->text(); -} - -TEST(SlotCollector, EscapesAndClosedness) { - const auto unit = collect(R"c( -typedef void (*fn)(void); -static void a(void) {} -static void b(void) {} -static void d(void) {} -struct local_ops { fn f; }; -struct shared_ops { fn f; }; -static struct local_ops mine = {a}; -static fn hook = b; -static fn exposed = b; -void register_ops(struct shared_ops *ops); -void *keep(void *p); -void setup(void) { - static struct shared_ops theirs = {d}; - register_ops(&theirs); - mine.f(); - hook(); - fn *where = &exposed; - (void)where; - (void)keep((void *)d); -} -)c"); - const core::SlotRules &rules = unit->slots.rules(); - EXPECT_TRUE(rules.confinedRecords.contains("a.c:struct local_ops")); - // Objects of `shared_ops` reach a callee without a body. - EXPECT_FALSE(rules.confinedRecords.contains("a.c:struct shared_ops")); - EXPECT_TRUE(rules.escapedStatics.contains("a.c:exposed")); - EXPECT_FALSE(rules.escapedStatics.contains("a.c:hook")); - EXPECT_FALSE( - unit->solution.isOpen(SlotKey::field("a.c:struct local_ops", "f"))); - EXPECT_TRUE( - unit->solution.isOpen(SlotKey::field("a.c:struct shared_ops", "f"))); - EXPECT_FALSE(unit->solution.isOpen(SlotKey::staticGlobal("a.c", "hook"))); - EXPECT_TRUE(unit->solution.isOpen(SlotKey::staticGlobal("a.c", "exposed"))); - // A function converted to a data pointer escapes: code outside may call it. - EXPECT_TRUE(unit->has("member\ta.c:d\tparam 0")) << unit->text(); - EXPECT_TRUE(unit->solution.escapes("a.c:d")); - EXPECT_FALSE(unit->solution.escapes("a.c:a")); -} - -// §9.3 and §13.1: the export form drops local slots and keeps the solution -// of every other slot. -TEST(SlotCollector, ExportFormKeepsTheSolution) { - const auto unit = collect(R"c( -typedef void (*fn)(void); -static void a(void) {} -fn out; -void relay(fn in) { fn tmp = in; fn other = a; out = tmp; other(); tmp(); } -)c"); - const core::FnSlots exported = unit->slots.exported(); - for (const core::SlotConstraint &constraint : exported.constraints()) - EXPECT_FALSE(constraint.slot.isLocal()) << constraint.slot.toString(); - const core::SlotSolution again = exported.solve(unit->slots.rules()); - for (const SlotKey &slot : unit->solution.slots()) { - if (slot.isLocal()) - continue; - EXPECT_EQ(unit->solution.targets(slot), again.targets(slot)) - << slot.toString(); - EXPECT_EQ(unit->solution.isOpen(slot), again.isOpen(slot)) - << slot.toString(); - } - EXPECT_TRUE(again.isOpen(SlotKey::global("out"))); -} - -// §9.3, the Lua pattern: `g->frealloc` is stored from `lua_newstate`'s -// parameter, and `luaL_newstate` passes `l_alloc`. Per TU the slot is open -// and empty, so the calls are `unresolved(callback)`, not silent; at link -// the slot is `{l_alloc}`. -TEST(SlotCollector, LuaAllocatorPattern) { - const std::string header = R"c( -typedef void *(*lua_Alloc)(void *ud, void *ptr, unsigned long osize, - unsigned long nsize); -struct global_State { lua_Alloc frealloc; void *ud; }; -struct global_State *lua_newstate(lua_Alloc f, void *ud); -)c"; - const auto lstate = collect(R"c( -#include "lib.h" -struct global_State *make_state(void); -struct global_State *lua_newstate(lua_Alloc f, void *ud) { - struct global_State *g = make_state(); - g->frealloc = f; - g->ud = ud; - return g; -} -void *luaM_realloc_(struct global_State *g, void *block, unsigned long n) { - return (*g->frealloc)(g->ud, block, 0, n); -} -)c", - header, "lstate.c"); - const SlotKey frealloc = SlotKey::field("struct global_State", "frealloc"); - EXPECT_TRUE(lstate->has("subset\tparam lua_newstate 0\tfield struct " - "global_State frealloc")) - << lstate->text(); - ASSERT_EQ(lstate->slots.indirectCalls().size(), 1U); - EXPECT_EQ(lstate->slots.indirectCalls().front().second, frealloc); - const core::CallResolution perUnit = lstate->solution.resolveCall(frealloc); - EXPECT_EQ(perUnit.kind, core::IndirectCallKind::OpenUnknown); - const auto decision = core::openCallTemporalDecision(perUnit); - ASSERT_TRUE(decision); - EXPECT_EQ(decision->unresolved, core::UnresolvedReason::Callback); - - const auto lauxlib = collect(R"c( -#include "lib.h" -static void *l_alloc(void *ud, void *ptr, unsigned long osize, - unsigned long nsize) { - (void)ud; (void)osize; (void)nsize; return ptr; -} -struct global_State *luaL_newstate(void) { return lua_newstate(l_alloc, 0); } -)c", - header, "lauxlib.c"); - EXPECT_TRUE(lauxlib->has("member\tlauxlib.c:l_alloc\tparam lua_newstate 0")) - << lauxlib->text(); - EXPECT_TRUE(lauxlib->solution.escapes("lauxlib.c:l_alloc")); - - // The unit records carry the export form; the link solves them together. - core::FnSlots program = lstate->slots.exported(); - program.merge(lauxlib->slots.exported()); - core::SlotRules link; - link.scope = core::SlotScope::Program; - for (const auto *unit : {lstate.get(), lauxlib.get()}) { - link.defined.insert(unit->slots.rules().defined.begin(), - unit->slots.rules().defined.end()); - link.exported.insert(unit->slots.rules().exported.begin(), - unit->slots.rules().exported.end()); - } - const core::CallResolution linked = program.solve(link).resolveCall(frealloc); - EXPECT_EQ(linked.kind, core::IndirectCallKind::ClosedSingle); - EXPECT_EQ(linked.targets, std::vector{"lauxlib.c:l_alloc"}); -} - -} // namespace -} // namespace weavec::analysis diff --git a/unittests/Analysis/SoundDefaultsTest.cpp b/unittests/Analysis/SoundDefaultsTest.cpp deleted file mode 100644 index 14385fc4..00000000 --- a/unittests/Analysis/SoundDefaultsTest.cpp +++ /dev/null @@ -1,436 +0,0 @@ -//===- SoundDefaultsTest.cpp - Code the engine cannot see (RFC 0030) ------===// -// -// Part of WeaveC, under the Apache License v2.0 with LLVM Exceptions. -// See LICENSE for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -//===----------------------------------------------------------------------===// -// -// RFC 0030 §3.1, §5, §6 and §11 (stage S3-B3): the unknown-callee default and -// its replacement by a known release, ownership contracts, inline assembly, -// summaries that carry the default, `may-alias-released`, budgets, -// assumptions, unsafe regions, `setjmp` and `-fno-weavec-zero-init`. -// -//===----------------------------------------------------------------------===// - -#include "SiteTestUtils.h" -#include "weavec/Analysis/UnitPipeline.h" - -#include - -#include -#include - -namespace weavec::analysis { - -using test::collectUnit; -using Lines = std::vector; - -/// ` =[/][: