diff --git a/.github/workflows/profiles-boost-build.yml b/.github/workflows/profiles-boost-build.yml new file mode 100644 index 0000000000000..78ee482b2a8d1 --- /dev/null +++ b/.github/workflows/profiles-boost-build.yml @@ -0,0 +1,265 @@ +name: Profiles Boost Build + +# Compiles Boost with the std::init profile enforced, using a clang built by the +# weekly profiles release, and publishes the raw compiler log for inspection. +# +# Normally dispatched by weekly-profiles-release.yml right after the weekly +# release is cut (passing that run's id in clang_run_id). Can also be run +# manually: omit clang_run_id to pull the latest published release instead. + +on: + workflow_dispatch: + inputs: + clang_run_id: + description: "weekly-profiles-release run id to take the clang artifact from (empty = latest release)" + required: false + default: "" + boost_ref: + description: "Boost superproject git ref (branch or tag)" + required: false + default: "master" + math_shards: + description: "Number of parallel b2 drivers for the Boost.Math test phase (more = more cores used; total -j stays ~nproc)" + required: false + default: "64" + math_budget_minutes: + description: "Soft time budget for the Boost.Math phase only; on expiry it stops b2, publishes partial results, and still SUCCEEDS (job stays green). Math finishes fast or never, so this can be small." + required: false + default: "90" + +permissions: + actions: read + contents: read + +jobs: + build-boost: + name: Build Boost (std::init enforced) + # ubuntu-24.04 for a newer libstdc++ (cleaner c++23); the 22.04-built clang + # runs fine here. + # runs-on: ubuntu-24.04 + runs-on: [self-hosted, Linux, X64] + # Hard backstop only. The build step enforces its own (much smaller) soft + # budget on the math phase and exits 0, so this should never fire; it just + # prevents a runaway if that watchdog itself breaks. Keep it above the soft + # budget (math_budget_minutes) + phase 1. + timeout-minutes: 240 + steps: + - name: Checkout profiles branch (helpers) + uses: actions/checkout@v4 + + - name: Install a recent libstdc++ + run: sudo apt-get update && sudo apt-get install -y g++-14 + + - name: Download clang (from weekly run) + if: inputs.clang_run_id != '' + uses: actions/download-artifact@v4 + with: + name: clang-profiles-linux-x86_64 + path: clang-dl + run-id: ${{ inputs.clang_run_id }} + github-token: ${{ github.token }} + + - name: Download clang (latest release) + if: inputs.clang_run_id == '' + env: + GH_TOKEN: ${{ github.token }} + run: | + mkdir -p clang-dl + gh release download --repo "${{ github.repository }}" \ + --pattern 'clang-profiles-linux-x86_64.tar.gz' --dir clang-dl + + - name: Unpack clang + run: | + mkdir -p "$RUNNER_TEMP/clang" + tar xzf clang-dl/clang-profiles-linux-x86_64.tar.gz -C "$RUNNER_TEMP/clang" + "$RUNNER_TEMP/clang/bin/clang++" --version + echo "PROFILES_CLANGXX=$RUNNER_TEMP/clang/bin/clang++" >> "$GITHUB_ENV" + echo "PROFILES_PRELUDE=$GITHUB_WORKSPACE/clang/utils/profiles-boost/prelude.hpp" >> "$GITHUB_ENV" + mkdir -p "$GITHUB_WORKSPACE/crash" + echo "PROFILES_CRASH_DIR=$GITHUB_WORKSPACE/crash" >> "$GITHUB_ENV" + + - name: Clone Boost + run: | + git clone --depth 1 --recursive --shallow-submodules \ + --branch "${{ inputs.boost_ref || 'master' }}" \ + https://github.com/boostorg/boost boost + + - name: Bootstrap Boost.Build + working-directory: boost + run: | + ./bootstrap.sh + ./b2 headers + + - name: Build Boost with std::init enforced + working-directory: boost + run: | + ws="$GITHUB_WORKSPACE" + log="$ws/boost-profiles.log" + rm -f "$ws"/part-*.log "$log" "$log.tmp" + + b2flags=( + --user-config="$ws/clang/utils/profiles-boost/user-config.jam" + toolset=clang-profiles + cxxstd=23 + testing.execute=off + ) + nproc_n=$(nproc) + + # --- Why this is split ------------------------------------------------- + # The bottleneck is the SINGLE-THREADED b2 driver: with the whole + # libs/*/test graph in one process it pegs one core doing dependency + # scanning/binding + serializing every compile's (very verbose) output, + # while the actual compiles sit blocked (we observed procs_running=0 -- the + # driver couldn't even feed -j3). More cores can't help one b2, and a + # bigger inner -j doesn't either; the lever that helps is MORE b2 DRIVERS. + # Boost.Math is the pathological tail (autodiff/special-function *compile* + # tests), so we fan it out across many b2 drivers (math_shards), each over + # a disjoint chunk of Math targets in its own --build-dir (so concurrent + # drivers can't race on shared config-check/library targets). Everything + # else builds first in one wide b2. Total concurrency stays ~nproc: the + # inner -j shrinks as the shard count grows (inner = nproc / shards). + # + # Output is streamed live to the Actions log: phase 1 via `tee`, the + # parallel Math phase via `tail -f` on the per-chunk part files (a single + # b2 can't stream to the console and write its own file without interleave + # corruption, so each writes a file and we mirror them). + + # --- Incremental result collector ------------------------------------- + # Previously boost-profiles.log was assembled only on the final line, so a + # timeout left it EMPTY and the whole (multi-hour) run produced nothing. + # Instead, merge the per-shard part files into the deliverable continuously + # (atomic rename), so whenever the job stops -- success OR timeout -- the + # log holds everything finished so far and the summarize/upload steps + # (if: always()) have real data to work with. + merge_now() { cat "$ws"/part-*.log > "$log.tmp" 2>/dev/null && mv "$log.tmp" "$log" || true; } + collector() { while :; do merge_now; sleep 20; done; } + collector & collectorpid=$! + + # --- Soft time budget (self-imposed, so GitHub never *cancels* us) ----- + # A GitHub timeout-minutes expiry CANCELS the job, which shows as a gray + # "Cancelled" run even though every other step succeeded. Instead we + # enforce our OWN deadline here: on expiry the watchdog stops b2 and we + # exit 0, so the step and the job stay GREEN and summarize/upload run + # normally. The timeout is surfaced as a yellow ::warning:: annotation + # plus a job-summary note (a run that stopped with one straggler shard + # still captured ~all of the data -- that's a success, not a failure). + # Genuine failures in *other* steps still fail the job as usual. + # + # It is scoped to the MATH phase only: empirically math is all-or-nothing + # -- a shard either finishes within minutes or wedges forever (b2 pegged, + # zero new output) -- so there is nothing to gain by waiting. Phase 1 is + # fast and not subject to this stall, so it runs before the clock starts. + budget_min="${{ inputs.math_budget_minutes || '60' }}" + timedout="$ws/.build-timedout"; rm -f "$timedout" + watchdog() { + sleep "$(( budget_min * 60 ))" + : > "$timedout" + echo "::warning title=Math soft-timeout::Boost.Math hit the ${budget_min}-minute budget; stopping b2 and publishing partial results (job kept green)." + pkill -x b2 2>/dev/null || true + } + watchdogpid="" + + tailpid="" + cleanup() { kill "$collectorpid" ${watchdogpid:+$watchdogpid} ${tailpid:+$tailpid} 2>/dev/null || true; } + trap cleanup EXIT + + start=$(date +%s) + + # 1) Everything except Math: one b2, full width. Also materializes the + # shared config checks and compiled Boost libraries. + nonmath=() + for d in libs/*/test; do + [ "$d" = libs/math/test ] || nonmath+=("$d") + done + echo "== phase 1: non-math ($nproc_n-way) ==" + ./b2 "${b2flags[@]}" -k -j"$nproc_n" "${nonmath[@]}" 2>&1 \ + | tee "$ws/part-000-rest.log" || true + + # Start the math-phase soft-timeout clock now (phase 1 is done). + watchdog & watchdogpid=$! + + # 2) Enumerate Math test targets authoritatively from b2's own plan + # (never from guessed source names), then build them in parallel. + # Guarded by the soft-timeout flag (defensive; normally not yet set). + if [ ! -f "$timedout" ]; then + ./b2 "${b2flags[@]}" libs/math/test -n 2>/dev/null \ + | grep -oP 'bin\.v2/libs/math/test/\K[^/]+(?=\.test)' \ + | sort -u > "$ws/math-names.txt" || true + n_math=$(wc -l < "$ws/math-names.txt" 2>/dev/null || echo 0) + echo "== phase 2: math targets enumerated: $n_math ==" + + if [ "$n_math" -eq 0 ]; then + # Enumeration failed for some reason -> never lose coverage: build + # Math in a single b2 (correct, just not parallelized). + echo "== math enumeration empty; single-b2 fallback ==" + ./b2 "${b2flags[@]}" -k -j"$nproc_n" libs/math/test 2>&1 \ + | tee "$ws/part-math-fallback.log" || true + else + outer="${{ inputs.math_shards || '64' }}" + [ "$outer" -lt 1 ] 2>/dev/null && outer=1 + [ "$outer" -gt "$n_math" ] && outer="$n_math" + inner=$(( nproc_n / outer )); [ "$inner" -lt 1 ] && inner=1 + echo "== phase 2: math via $outer parallel b2 x -j$inner ==" + split -d -a 4 -n "r/$outer" "$ws/math-names.txt" "$RUNNER_TEMP/mchunk_" + i=0; pids=() + for c in "$RUNNER_TEMP"/mchunk_*; do + ids=$(sed 's#^#libs/math/test//#' "$c" | tr '\n' ' ') + [ -z "${ids// /}" ] && continue + ./b2 "${b2flags[@]}" --build-dir="$RUNNER_TEMP/mbd_$i" \ + -k -j"$inner" $ids > "$ws/part-math-$(printf '%04d' "$i").log" 2>&1 & + pids+=($!) + i=$((i + 1)) + done + # Mirror all chunk logs to the Actions console in real time; the b2 + # processes remain the source of truth for the merge. + tail -n +1 -f "$ws"/part-math-*.log & tailpid=$! + wait "${pids[@]}" 2>/dev/null || true + kill "$tailpid" 2>/dev/null || true; tailpid="" + fi + fi # end soft-budget guard around the math phase + + # 3) Final authoritative merge (the collector already refreshes the log + # every 20s during the build; this captures the last bytes and covers + # the happy, no-timeout path). + kill "$collectorpid" ${watchdogpid:+$watchdogpid} 2>/dev/null || true + merge_now + end=$(date +%s) + printf 'done in %ss; boost-profiles.log lines: %s (from %s part files)\n' \ + "$((end - start))" "$(wc -l < "$log")" \ + "$(ls "$ws"/part-*.log 2>/dev/null | wc -l)" + + # Report a soft-timeout as a visible (yellow) note, but SUCCEED so the job + # stays green. Other steps' failures still fail the job normally. + if [ -f "$timedout" ]; then + { + echo "### :warning: Build stopped at the ${budget_min}-minute soft budget" + echo "" + echo "Boost.Math did not fully finish, but partial results were published" + echo "to \`boost-profiles.log\` ($(( end - start ))s elapsed). Treated as success on purpose." + } >> "$GITHUB_STEP_SUMMARY" + echo "Soft-timeout reached; published partial results and exiting 0 (green)." + fi + exit 0 + + - name: Summarize & visualize profile violations + if: always() + run: | + echo "_Boost ref: \`${{ inputs.boost_ref || 'master' }}\`_" >> "$GITHUB_STEP_SUMMARY" + python3 clang/utils/profiles-boost/summarize.py \ + --log "$GITHUB_WORKSPACE/boost-profiles.log" \ + --boost-root "$GITHUB_WORKSPACE/boost" \ + --json "$GITHUB_WORKSPACE/summary.json" \ + --html "$GITHUB_WORKSPACE/report.html" \ + >> "$GITHUB_STEP_SUMMARY" + + - name: Upload log, report and crash reproducers + if: always() + uses: actions/upload-artifact@v4 + with: + name: boost-profiles-log + path: | + boost-profiles.log + summary.json + report.html + crash/ + if-no-files-found: warn diff --git a/.github/workflows/schedule-profiles-release.yml b/.github/workflows/schedule-profiles-release.yml new file mode 100644 index 0000000000000..19409cb107ad4 --- /dev/null +++ b/.github/workflows/schedule-profiles-release.yml @@ -0,0 +1,17 @@ +name: Schedule Profiles Build + +on: + schedule: + - cron: '0 12 * * 5' + +permissions: + actions: write + +jobs: + dispatch: + runs-on: ubuntu-latest + steps: + - name: Trigger build on profiles-framework + env: + GH_TOKEN: ${{ github.token }} + run: gh workflow run weekly-profiles-release.yml --repo ${{ github.repository }} --ref profiles-framework diff --git a/.github/workflows/weekly-profiles-release.yml b/.github/workflows/weekly-profiles-release.yml new file mode 100644 index 0000000000000..390224c43ab5e --- /dev/null +++ b/.github/workflows/weekly-profiles-release.yml @@ -0,0 +1,133 @@ +name: Weekly Profiles Release + +on: + workflow_dispatch: + +permissions: + contents: write + +jobs: + build-linux: + name: Build Linux x86_64 + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + run: sudo apt-get install -y ninja-build ccache + + - name: Restore ccache + uses: actions/cache@v4 + with: + path: ~/.ccache + key: ccache-linux-${{ github.sha }} + restore-keys: ccache-linux- + + - name: Configure + run: | + cmake -G Ninja -S llvm -B build \ + -DCMAKE_BUILD_TYPE=Release \ + -DLLVM_ENABLE_ASSERTIONS=ON \ + -DCMAKE_C_COMPILER=gcc \ + -DCMAKE_CXX_COMPILER=g++ \ + -DCMAKE_C_COMPILER_LAUNCHER=ccache \ + -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ + -DLLVM_ENABLE_PROJECTS=clang \ + -DLLVM_TARGETS_TO_BUILD="X86;AArch64" \ + -DLLVM_INSTALL_TOOLCHAIN_ONLY=ON \ + -DCMAKE_INSTALL_PREFIX=build/install + + - name: Build and install + run: ninja -C build install-clang install-clang-resource-headers + + - name: Package + run: tar czf clang-profiles-linux-x86_64.tar.gz -C build/install . + + - uses: actions/upload-artifact@v4 + with: + name: clang-profiles-linux-x86_64 + path: clang-profiles-linux-x86_64.tar.gz + + build-windows: + name: Build Windows x86_64 + runs-on: windows-2022 + steps: + - uses: actions/checkout@v4 + + - uses: ilammy/msvc-dev-cmd@v1 + + - name: Configure + run: > + cmake -G Ninja -S llvm -B build + -DCMAKE_BUILD_TYPE=Release + -DCMAKE_C_COMPILER=cl + -DCMAKE_CXX_COMPILER=cl + -DLLVM_ENABLE_PROJECTS=clang + -DLLVM_TARGETS_TO_BUILD="X86;AArch64" + -DLLVM_INSTALL_TOOLCHAIN_ONLY=ON + -DCMAKE_INSTALL_PREFIX=build/install + + - name: Build and install + run: ninja -C build install-clang install-clang-resource-headers + + - name: Package + run: Compress-Archive -Path build\install\* -DestinationPath clang-profiles-windows-x86_64.zip + + - uses: actions/upload-artifact@v4 + with: + name: clang-profiles-windows-x86_64 + path: clang-profiles-windows-x86_64.zip + + create-release: + name: Create GitHub Release + needs: [build-linux, build-windows] + runs-on: ubuntu-latest + steps: + - uses: actions/download-artifact@v4 + with: + name: clang-profiles-linux-x86_64 + path: artifacts + + - uses: actions/download-artifact@v4 + with: + name: clang-profiles-windows-x86_64 + path: artifacts + + - name: Compute tag + id: tag + run: | + date_tag="profiles-$(date -u +%Y-%m-%d)" + echo "tag=${date_tag}-${{ github.run_number }}" >> "$GITHUB_OUTPUT" + echo "name=Profiles Build $(date -u +%Y-%m-%d) #${{ github.run_number }}" >> "$GITHUB_OUTPUT" + + - uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.tag.outputs.tag }} + name: ${{ steps.tag.outputs.name }} + target_commitish: ${{ github.sha }} + files: artifacts/* + body: | + Automated weekly build of Clang with safety profiles support. + + **Branch:** `${{ github.ref_name }}` + **Commit:** `${{ github.sha }}` + + ### Artifacts + - `clang-profiles-linux-x86_64.tar.gz` — Linux x86_64 (Ubuntu 22.04, gcc) + - `clang-profiles-windows-x86_64.zip` — Windows x86_64 (MSVC 2022) + + trigger-boost: + name: Trigger Boost profiles build + needs: [create-release] + runs-on: ubuntu-latest + permissions: + actions: write + steps: + - name: Dispatch Boost build against this run's clang + env: + GH_TOKEN: ${{ github.token }} + run: > + gh workflow run profiles-boost-build.yml + --repo ${{ github.repository }} + --ref ${{ github.ref_name }} + -f clang_run_id=${{ github.run_id }} diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst new file mode 100644 index 0000000000000..e43a3e4d4e2b0 --- /dev/null +++ b/clang/docs/ProfilesFramework.rst @@ -0,0 +1,785 @@ +====================== +C++ Profiles Framework +====================== + +.. contents:: + :depth: 2 + :local: + + +Introduction +============ + +The C++ Profiles framework (`P3589R2 +`_) lets a +translation unit opt into additional language restrictions called *profiles*. +A profile is a named set of rules enforced by the compiler, each formulated +to keep the program free of a certain class of problems -- for example, use +of uninitialized memory. Three attributes control it: + +- ``[[profiles::enforce(...)]]`` requests enforcement of one or more profiles + for the translation unit. +- ``[[profiles::suppress(...)]]`` locally exempts a declaration or statement + from an enforced profile, or from a single rule of it. +- ``[[profiles::require(...)]]`` on a module import verifies that the + imported module advertises a profile. + +Profiles do not change the meaning of well-formed programs with no undefined +behavior. Their effects are conceptually applied only after translation +phase 7: a profile cannot change the outcome of overload resolution or +template instantiation, and it is not possible to SFINAE on a profile +violation. + +Profile names are open-ended: standard (``std::``-prefixed), +implementation-defined, and third-party profiles are all requested with the +same syntax, and enforcing a profile the implementation does not know is not +an error -- it simply has no rules to enforce. Clang currently implements +two real profiles: an initial slice of the proposed ``std::init`` +initialization profile (see `The std::init Profile`_), which checks at compile +time, and an initial slice of the proposed ``std::core_ub`` profile (see +`The std::core_ub Profile`_), which inserts runtime checks. The +feature is experimental: attribute spellings, rule names, and diagnostics may +change. + + +Usage +===== + +The framework is gated on the C++-only ``-fprofiles`` flag, which defaults to +off: + +.. code-block:: console + + clang++ -std=c++23 -fprofiles example.cpp + +The attributes accept both the ``[[profiles::name(...)]]`` and the +``[[using profiles: name(...)]]`` spelling and always require an argument +clause; see the :doc:`AttributeReference` for the per-attribute reference. + +Without ``-fprofiles`` the attributes are ignored with a warning, and their +argument clauses are not checked against the P3589R2 grammar -- like any +standard attribute the implementation does not act on, an arbitrary +balanced-token argument clause is accepted. Code annotated for a +profiles-enabled build therefore still compiles (modulo the warning) with the +feature off, and no profile rule ever fires. + + +Enforcing Profiles +================== + +``[[profiles::enforce(profile-designator-list)]]`` requests enforcement of +the named profiles for the whole translation unit. It may appear only on an +*empty-declaration* that precedes every other declaration at translation-unit +scope, or on a *module-declaration* (see `Profiles and Modules`_): + +.. code-block:: c++ + + [[profiles::enforce(std::init)]]; + [[profiles::enforce(vendor::hardened(fortify: 3))]]; // designator arguments + + #include + + int main() { /* ... */ } + +A *profile-designator* is a ``::``-qualified profile name, optionally +followed by a parenthesized argument list. The arguments are not subject to +name lookup; their interpretation is up to the profile. Repeating an +enforcement with the same designator is allowed and has no effect, but +requesting the same profile with a different designator is an error, as is an +enforcement placed after another declaration: + +.. code-block:: c++ + + [[profiles::enforce(vendor::hardened(fortify: 3))]]; + [[profiles::enforce(vendor::hardened(fortify: 3))]]; // OK: no effect + [[profiles::enforce(vendor::hardened(fortify: 2))]]; // error: same profile, + // different designator + int x; + [[profiles::enforce(std::init)]]; // error: does not precede 'x' + + +Suppressing Enforcement +======================= + +``[[profiles::suppress(profile-name)]]`` on a declaration or statement +exempts it from the named profile's rules. An optional ``rule:`` argument +narrows the suppression to a single named rule, and an optional +``justification:`` argument (a string literal) records why the suppression is +there: + +.. code-block:: c++ + + [[profiles::enforce(std::init)]]; + + void fill(char *buf, int n); + + int main() { + [[profiles::suppress(std::init, + rule: "uninit_decl", + justification: "buffer is filled in by fill()")]] + char buffer[1024]; + fill(buffer, 1024); + } + +A suppression covers exactly the tokens of the declaration or statement it +appertains to -- nothing more. For a variable declaration that includes the +initializer, so violations inside the initializer are silenced; but the +variable is *not* marked as exempt at later uses, which appear in other +declarations or statements and are checked normally: + +.. code-block:: c++ + + [[profiles::suppress(std::init)]] int x; // OK: uninit_decl suppressed + int y = x; // error: 'x' is read before initialization + +To exempt an object from a profile's checks everywhere it is used, use the +profile's own per-object marker instead (for ``std::init``, ``[[uninit]]``). + + +Profiles and Modules +==================== + +A module interface advertises the profiles it enforces through +``[[profiles::enforce]]`` on its module-declaration, and importers can insist +on that advertisement with ``[[profiles::require]]``, which may appear only +on a module-import-declaration: + +.. code-block:: c++ + + // M.cppm + export module M [[profiles::enforce(std::init)]]; + + // user.cpp + import M [[profiles::require(std::init)]]; // OK: M enforces std::init + import N [[profiles::require(std::init)]]; // error unless N does too + +``[[profiles::require]]`` only verifies the advertisement; importing an +enforcing module does **not** enforce its profiles in the importer. +Enforcement is always explicit and local. A header unit participates the +same way: an ``[[profiles::enforce(...)]];`` empty-declaration in the header +is exported by the corresponding header unit and validated by +``[[profiles::require]]`` on its import. + +``-fprofiles`` does not have to be uniform across a build: a module built +without the flag imports fine into a profiles-enabled compile -- it simply +advertises no profiles, so a ``[[profiles::require]]`` on the import reports +the profile as not enforced -- and an enforcing module loads fine into a +compile with the feature off. A PCH is stricter (like other compatible +language options, it must be built with the same ``-fprofiles`` setting as +its consumer). + +Enforcement on a module interface extends to the module's implementation +units: + +- A non-partition implementation unit (``module M;``) inherits the + interface's enforcements automatically. +- A partition implementation unit (``module M:P;``) inherits them only on a + best-effort basis, because the interface's BMI is usually not built yet + when the partition is compiled. Repeat the ``[[profiles::enforce]]`` there + for guaranteed enforcement. + +A declaration and its redeclarations must appear under mutually *compatible* +profiles (P3589R2 [decl.attr.enforce]p5): redeclaring an entity from a module +or header unit that was compiled without a compatible profile is diagnosed. +Two profiles are compatible when they have the same name (designator +arguments configure a profile without changing its identity), and all +standard ``std::`` profiles are mutually compatible. + + +The ``std::init`` Profile +========================= + +``std::init`` is an initial slice of the proposed initialization profile from +Bjarne Stroustrup's "An initialization profile" (P4222R1.1; the ``§`` +references below are to that paper). Its guarantee: **no object is read or +written before it is initialized**, enforced entirely at compile time. +Following the paper: + +- Every object must be initialized at its point of definition, or be marked + as intentionally uninitialized with the ``[[uninit]]`` attribute. +- An object marked ``[[uninit]]`` must be assigned before it is read, which + is verified with simple, local flow analysis (§1.3). +- A pointer or reference to uninitialized memory must be marked with the + ``[[ref_to_uninit]]`` attribute, and reads through it are rejected. +- Non-local objects must be initialized at compile time (§3). +- Implicit initialization counts: a default constructor or the + zero-initialization of statics initializes an object, and a class with a + user-provided constructor is trusted to initialize its members (§5.1). + +``[[uninit]]`` and ``[[ref_to_uninit]]`` are ordinary C++11 attributes, +recognized regardless of ``-fprofiles`` (see the :doc:`AttributeReference` +entries for their placement rules); the rules below carry weight only while +``std::init`` is enforced. Reads and writes of ``std::byte`` objects are +exempt from all of them (§4.5). + +Each rule has a name, so it can be suppressed individually with +``[[profiles::suppress(std::init, rule: "name")]]`` (see `Suppressing +Enforcement`_): + +=========================== ====================================================== +Rule Diagnoses +=========================== ====================================================== +``uninit_decl`` A variable left (partially) uninitialized without + ``[[uninit]]``. +``uninit_read`` A read of an uninitialized object, or of an + ``[[uninit]]`` object before it is assigned. +``uninit_write`` A write to a subobject of an ``[[uninit]]`` object. +``ref_to_uninit`` A pointer or reference binding inconsistent with its + ``[[ref_to_uninit]]`` marking. +``ctor_uninit_member`` A constructor that leaves a member or base subobject + uninitialized. +``static_runtime_init`` A non-local variable with a runtime initializer. +``uninit_with_initializer`` ``[[uninit]]`` combined with an initializer, or + on an entity whose default-initialization is not + a no-op. +``pointer_marker`` ``[[uninit]]`` on a pointer. +``union_marker`` ``[[uninit]]`` on a union object or member. +``static_marker`` ``[[uninit]]`` on a variable with static or thread + storage duration. +=========================== ====================================================== + + +Uninitialized Variables +----------------------- + +An automatic-storage variable whose default-initialization would leave it -- +or, for an aggregate, any scalar subobject (§5.4) -- indeterminate must +either be initialized or carry ``[[uninit]]`` (rule ``uninit_decl``): + +.. code-block:: c++ + + struct S { int x; }; + union U { int i; float f; }; + + void f() { + int a; // error: uninitialized (uninit_decl) + int b [[uninit]]; // OK: intentionally uninitialized + int c = 3; // OK + S s; // error: default-initialization leaves 's.x' indeterminate + S t{}; // OK: value-initialized + U u; // error: an uninitialized union (§5.6) + std::string str; // OK: a user-provided default constructor is trusted + } + +A class type with a user-provided default constructor is trusted to +initialize its members (§5.1), and a data member that is itself marked +``[[uninit]]`` is acknowledged -- a type whose only indeterminate scalars are +all marked does not trigger the rule. Marking a variable of such a type +``[[uninit]]`` is likewise consistent: its default-initialization is a +genuine no-op. A union whose default-initialization activates a variant +through a default member initializer -- including initializers written on +the leaves of an anonymous-record variant, provided they initialize the +variant completely -- counts as initialized, and a union of only unnamed +bit-fields or only ``std::byte`` members has nothing to acknowledge (§4.5). + + +Where ``[[uninit]]`` May Not Go +------------------------------- + +``[[uninit]]`` asserts that an object is genuinely uninitialized, so +placements that contradict that -- or that would defeat the profile's +guarantee -- are rejected: + +.. code-block:: c++ + + int g [[uninit]]; // error: statics are zero-initialized (static_marker) + int *p [[uninit]]; // error: a pointer must be initialized, e.g. to + // nullptr (pointer_marker, §4.3) + + union U { int i; float f; }; + U u [[uninit]]; // error: delayed initialization of a union member + // would be erroneous (union_marker, §5.6) + + void f() { + int x [[uninit]] = 4; // error: marked *and* initialized + // (uninit_with_initializer, §4.2) + std::string s [[uninit]]; // error: the default constructor initializes + // it (uninit_with_initializer) + } + +Each of these keys on the array element type, so an array of pointers or of +unions is rejected exactly like a single one. A no-op initialization -- the +trivial default-initialization of a scalar or aggregate that leaves a scalar +subobject indeterminate -- is consistent with the marker; any other +synthesized initialization contradicts it and is rejected (§5.3): a member or +base with a user-provided default constructor, a default member initializer, +a virtual table pointer, or a value-initialization (``= P()``), all of which +initialize something. The same rule covers a marked data member whose type's +default-initialization is not a no-op: + +.. code-block:: c++ + + struct Str { Str() : cap(0) {} int cap; }; + struct S { int x; Str s; }; + + void g() { + S s4 [[uninit]]; // error: 's4.s' is default-constructed, so 's4' is + // not left uninitialized (uninit_with_initializer, §5.3) + } + + struct Buf { + Str s [[uninit]]; // error: default-initialization of 'Str' runs a + // constructor, so 's' cannot be left uninitialized + int n [[uninit]]; // OK: a scalar member really is left uninitialized + }; + + +Reads of Uninitialized Objects +------------------------------ + +An uninitialized object must not be read (rule ``uninit_read``). Local flow +analysis verifies reads of uninitialized locals, of ``[[uninit]]`` members +within the defining constructor's body, and of ``[[uninit]]`` members of +constructor-less aggregate locals; reads through ``[[ref_to_uninit]]`` +pointers and references and reads of subobjects of ``[[uninit]]`` objects are +rejected outright -- unless a prior whole-entity store credits the storage +as initialized (see `Binding Pointers and References`_): + +.. code-block:: c++ + + struct Buf { int n [[uninit]]; }; + + int f(int *p [[ref_to_uninit]]) { + int x [[uninit]]; + int a = x; // error: 'x' is read before it is assigned + x = 3; + int b = x; // OK: assigned on every path reaching the read + + Buf buf; + int c = buf.n; // error: 'buf.n' is not yet assigned + buf.n = 1; + int d = buf.n; // OK + + [[uninit]] int arr[4]; + int e = arr[0]; // error: array elements are not tracked; only a + // whole-object initialization can give 'arr' a value + + return *p; // error: read through [[ref_to_uninit]] (§4.5) + } + + struct T { + int m [[uninit]]; + T(int v) { + int r = m; // error: 'm' is read before the body assigns it + m = v; // OK: for a built-in type, a write is its + } // initialization (§4.5) + }; + +A member or variable counts as assigned only when every path to the read +assigns it (§1.3), and a compound assignment (``x += 1``) or an increment or +decrement reads the old value first, so it is diagnosed like a read. Inside +a constructor body only that plain whole-member assignment earns credit: +passing ``&m`` to a function (even one whose parameter is marked +``[[ref_to_uninit]]``), binding a reference to the member, calling a member +function, or letting ``this`` escape does not count as initializing ``m`` -- +the paper rejects complex constructor code (§5.1) and reserves +callee-initialization for ``now_init()`` (§6.2); suppress the rule where +such a flow is intended. A ``this``-capturing lambda might run immediately, +so member reads in its body count at the point the lambda is created (and +writes there earn no credit -- the same strict policy). For a *local* +variable the analyses instead treat any escape as an assignment (see +`Limitations`_). A local copied or moved from a tracked local is tracked +too: the copy's members inherit the source's per-member state at the copy +point -- a copy does not inherit initialization (§5.2), it inherits +whatever state the source has. A by-value *parameter* of such a class is +likewise a copy, of the caller's argument, so its marked members are +tracked from an unassigned start: a read before a local assignment (or an +escape of the parameter) is rejected even if the caller assigned the member +first. That is the call-boundary twin of the constructor-body strictness +-- the paper hands uninitialized-capable storage across calls through +marked pointers and references (§4.3), not by-value slots -- and +``[[profiles::suppress]]`` is the remedy where the by-value flow is +intended. Members of an object initialized by a *user-provided* +constructor are trusted (§5.1) and not flow-tracked; only the defining +constructor itself is checked. + + +Writes to Subobjects of Uninitialized Objects +--------------------------------------------- + +Piecemeal delayed initialization of an ``[[uninit]]`` object through its +members or elements cannot be validated statically (§5.4, §5.5), so a store +to a *proper subobject* of an ``[[uninit]]`` entity is rejected (rule +``uninit_write``); writing the whole entity is that entity's initialization, +stays legal, and credits the entity as initialized for everything after it +in parse order (see `Binding Pointers and References`_): + +.. code-block:: c++ + + struct S { int x; int y; }; + + void f() { + S s [[uninit]]; + s.x = 1; // error: writing a member of an [[uninit]] object (uninit_write) + [[uninit]] int a[2]; + a[0] = 1; // error: writing an element of an [[uninit]] object + int v [[uninit]]; + v = 7; // OK: the write initializes the whole entity + } + + void g(int *p [[ref_to_uninit]]) { + *p = 5; // OK: a write through the marker is the pointee's + // initialization + } + +A compound assignment or increment/decrement of such a subobject also reads +its old value, so the read diagnostic fires alongside this one. Assigning a +whole *class* object marked ``[[uninit]]`` (``s = S{1, 2};``) is caught too: +the member ``operator=`` binds the uninitialized object as its implicit +object argument (see the next section). + + +Binding Pointers and References +------------------------------- + +A pointer or reference must be bound consistently with its +``[[ref_to_uninit]]`` marking (rule ``ref_to_uninit``, §4.3): a marked +pointer, reference, or function return may only refer to uninitialized +memory, and an unmarked one only to initialized memory. Whether a source +refers to uninitialized memory is recognized from its form -- the address or +a subobject of an ``[[uninit]]`` entity, the value or dereference of a +marked pointer or reference, pointer and reference casts of those, a call to +a marked function, a call to a known allocator (``malloc``, +``aligned_alloc``, ``alloca``, and raw ``::operator new`` calls return +uninitialized memory, ``calloc`` zero-initialized memory, and ``realloc`` is +unclassified; §4.3 -- keyed on Clang's builtin recognition, which +``-fno-builtin`` disables), and a ``new`` +expression that default-initializes a type with indeterminate scalars +(``new int``, ``new int[n]``; §1.2) -- refined by one parse-order fact, +whole-entity stores (below): + +.. code-block:: c++ + + int i = 7; + int u [[uninit]]; + + int *p1 = &i; // OK + int *p2 = &u; // error: needs [[ref_to_uninit]] + int *p3 [[ref_to_uninit]] = &u; // OK + int *p4 [[ref_to_uninit]] = &i; // error: 'i' is initialized + int *p5 [[ref_to_uninit]] = new int; // OK: uninitialized new (§1.2) + + void sink(int *q); + void fill(int *q [[ref_to_uninit]]); + + void f() { + sink(p3); // error: 'sink' expects initialized memory + fill(p3); // OK + } + +The check applies wherever a pointer or reference is bound: variable, member, +and aggregate initialization, assignment, call arguments (including defaulted +and variadic ones), ``return`` and ``throw`` statements (including returns +inside lambdas and blocks -- a lambda's marker is written on its call +operator, in the C++23 attribute position after the lambda-introducer: +``[] [[ref_to_uninit]] () -> int* { ... }``), lambda captures, and +the implicit object argument of a member call -- so calling a member function +on an object recognized as uninitialized storage is rejected, and so is +copying a class object out of one. For the copy, the escape is the paper's +own (§7.2): declare the copy constructor's parameter ``[[ref_to_uninit]]``. +Positions that cannot carry the marker -- a variadic argument, a parameter of +a function called through a function pointer, the implicit object parameter, +a pointer element of an array in aggregate initialization +-- are checked as unmarked targets; suppress at the call site if the flow is +intended. A null pointer source -- ``nullptr``, ``0``, ``{}``, or a local +variable initialized to null -- refers to no object, so it is accepted for +marked and unmarked targets alike (§4.3, §8: the marker means "zero or more +uninitialized objects"); a *parameter* with a null default argument is not a +null source (callers may pass any pointer). A source whose form the recognizer cannot classify +(pointer arithmetic, an integer-to-pointer cast) is likewise accepted for +either target. + +The parse-order refinement: a *whole-entity store* credits its target as +initialized (§4.2, §4.5). After ``u = 5;`` the ``[[uninit]]`` variable +``u`` counts as initialized, and after ``*p = 5;`` the marked pointer's +pointee does, for every later ``*p`` access -- until ``p`` is reseated +(``p = q``, ``p += n``, ``p++``), which withdraws the credit; a store +through a marked *reference* credits its referent permanently. The credit +works in both directions: ``int *q = &u;`` after the store is accepted, and +``int *r [[ref_to_uninit]] = &u;`` is now rejected -- a credited entity +requires an unmarked target (§4.2). Element accesses (``p[i]``) are never +credited in either direction (§5.4's random-access ban applies even to +``p[0]``), element stores earn no credit, and address escapes (passing +``&u`` to a ``[[ref_to_uninit]]`` parameter) never count -- the paper +reserves callee-initialization for ``now_init()`` (§6.2). Class-typed +whole-object assignment never credits either: it is a member ``operator=`` +call on uninitialized storage, rejected as above. + +Whole-member stores are credited too, keyed per base object: after +``a.m = 5;`` on a directly named local, or ``this->m = 5;`` on the current +object (keyed to the enclosing function body, so no other function -- nor a +lambda body, which is its own function -- shares it; instantiations of one +template or generic lambda share their pattern's single body, and its +credit), binding ``a.m`` or +``&a.m`` through the *same* base is accepted, and the reverse direction +applies as for locals. The boundaries: one member level only (``x.agg.m = +5;`` is itself the piecemeal-initialization error and earns nothing), only +directly named non-reference locals or the current object (a member reached +through a pointer, reference, or any other object -- including a copy, per +§5.2 -- stays strict), and never member *pointee* stores (``*w.p = 5;``), +whose aliasing is per-value: a copy of the object shares the pointee. The +credit is purely parse-order, with no dominance or flow analysis: a store +under a condition credits everything after it, so a binding on the untaken +path is a missed diagnostic (see `Limitations`_). + +One kind of call *does* count as initialization: §6.2's ``[[now_init]]`` +attribute (its placement and spelling track an open committee question) +declares that a function initializes the storage passed to each of its +``[[ref_to_uninit]]`` parameters, and it requires at least one such +parameter. After a call to a ``[[now_init]]`` function, the argument's +storage earns exactly the credit the equivalent direct store would -- +``fill(&u)`` credits ``u`` whole, ``fill(p)`` credits the marked pointer's +pointee (until ``p`` is reseated), ``fill(&a.m)`` credits the ``(a, m)`` +pair -- with the same boundaries and the same reverse-direction consequence +(a second ``fill(&u)`` is rejected: ``u`` no longer refers to uninitialized +memory, which incidentally catches double ``construct_at``). This is R2 +§4.5's requested library annotation: declare ``construct_at`` with +``[[now_init]]`` and a ``[[ref_to_uninit]]`` first parameter and the +lifecycle *start* is checked. The §4.4 ``now_init()`` identity function +needs no compiler support at all -- declared as ``template T* +now_init(T* p [[ref_to_uninit]]);``, its unmarked return is already trusted +as initialized -- but only the attribute legalizes the original *name* after +the call. + +The lifecycle *end* is the mirror attribute ``[[now_uninit]]`` -- the +recording §4.4 notes is missing for ``destroy_at`` ("the object subjected to +``destroy_at()`` should be considered uninitialized, but there is no way of +recording that in the code"). It declares that a function ends the lifetime +of the storage passed to each of its pointer or reference parameters (which +are unmarked -- destruction takes initialized memory; apply the attribute +only to functions that destroy *every* such argument's storage), and a call +to one *withdraws* exactly the credit the equivalent ``[[now_init]]`` call +would have recorded. Declare ``destroy_at`` as ``template void +destroy_at(T* p) [[now_uninit]];`` and the construct/destroy/construct cycle +is legal, a second destruction is rejected (the storage no longer refers to +initialized memory, so binding it to the unmarked parameter is the +unmarked-direction violation), and binding the destroyed storage to an +ordinary pointer or reference is rejected the same way. A function may +carry both attributes -- a reinitializer that destroys and then +reconstructs its argument's storage -- and models destroy-then-construct: +the storage bound to its ``[[ref_to_uninit]]`` parameters is initialized +after the call. + + +Constructors +------------ + +A user-provided constructor must initialize every non-static data member +through its member-initializer list or a default member initializer unless +the member is marked ``[[uninit]]`` (rule ``ctor_uninit_member``, §5.1); an +assignment in the constructor body does not count (reads of an ``[[uninit]]`` +member before such an assignment are flow-checked, as above). Direct +non-virtual base subobjects must be initialized the same way -- a base cannot +carry ``[[uninit]]``, so it must always be initialized, by a written base +initializer or by the base's own user-provided default constructor: + +.. code-block:: c++ + + struct X { + int a; + int b = 0; + int c [[uninit]]; + X(int v) : a(v) {} // OK: 'a' is written, 'b' has a default member + }; // initializer, 'c' is acknowledged + + struct Y { + int m; + Y() { // error: 'm' is not initialized (ctor_uninit_member) + m = 1; // (a body assignment does not count) + } + }; + +A member whose type's default-initialization leaves unacknowledged scalars +indeterminate (a nested aggregate) is flagged the same way, and the members +of an anonymous struct are checked exactly like direct members (a written +initializer for one is an indirect member-initializer). An anonymous +*union* member instead needs one active member: a written leaf initializer +or a leaf default member initializer satisfies it -- default member +initializers on the leaves of an anonymous-record variant activate that +variant, which satisfies the union when they initialize it completely -- +and an anonymous union of only unnamed bit-fields or only ``std::byte`` +members has nothing to initialize. A delegating constructor is exempt -- +its target initializes the members -- and so is a union's own constructor, +whose members are mutually exclusive (§5.6). + + +Global and Static Variables +--------------------------- + +Variables with static or thread storage duration are zero-initialized, so +they are never uninitialized: ``[[uninit]]`` on one is rejected -- by +``static_marker`` when nothing else initializes the object, and by +``uninit_with_initializer`` when a written initializer or a non-no-op +default-initialization already contradicts the marker (exactly one of the +pair fires). Non-local variables with static storage duration must +additionally be initialized at compile time (rule ``static_runtime_init``, +§3), because cross-translation-unit initialization order can otherwise +produce a read of a not-yet-initialized object: + +.. code-block:: c++ + + int seed(); + + int g1 = 42; // OK: constant-initialized + int g2 = seed(); // error: runtime initializer (static_runtime_init) + thread_local int t = seed(); // OK: thread storage duration + + int &counter() { + static int c = seed(); // OK: a function-local static is initialized + return c; // on first use (§3) + } + + +Limitations +----------- + +The implemented slice is deliberately conservative. The first entry below +is a deliberate strictness -- it rejects code the paper itself rejects -- +rather than an omission; each of the others is a missed diagnostic, never a +false positive. + +- Inside a constructor body, only a plain assignment to an ``[[uninit]]`` + member or a call to a ``[[now_init]]`` function (§6.2) counts as its + initialization -- the latter for the current-object storage bound to the + callee's ``[[ref_to_uninit]]`` parameters (``&m``, ``m``, or ``this`` + itself, which credits every tracked member), as a genuine dataflow fact, + so a ``[[now_init]]`` call on one branch still does not satisfy a read at + the join (§1.2). Taking the member's address, binding a reference to it, + calling a member function, letting ``this`` escape, or passing ``&m`` to a + ``[[ref_to_uninit]]`` parameter of an *ordinary* function earns no credit, + so a later read of the member is rejected: the paper rejects complex + constructor code (§5.1) and reserves callee-initialization for + ``now_init`` (§6.2); the remedy for an intended flow is ``[[now_init]]`` + on the callee or ``[[profiles::suppress]]``. For *locals*, by contrast, + the local-aggregate pass and the plain-local analysis conservatively treat + any escape of the variable as an assignment -- there the omission is a + missed diagnostic, never a false positive. That escape-crediting is an + interim leniency relative to the paper (which credits only ``now_init``); + tightening it to ``[[now_init]]`` callees alone is future work. +- ``construct_at``/``destroy_at`` flow is modeled through the annotations: + a ``[[now_init]]``-annotated ``construct_at`` declaration checks the + lifecycle start (including double construction, via the reverse-direction + binding rule) and a ``[[now_uninit]]``-annotated ``destroy_at`` the end + (double destruction, and use-after-destroy where the destroyed storage is + *bound* -- a direct named read after a destroy stays with the flow passes, + which treat the call as an escape, so it is a missed diagnostic; a destroy + inside a constructor body earns no kill bit in the ctor-body dataflow + either). Writes through ``[[ref_to_uninit]]`` are still not verified. +- A ``new`` expression whose result is not bound to anything (``new int;``) + is not checked. +- A call through a function pointer cannot see parameter markers on the + pointed-to function, and a member call through a pointer-to-member bypasses + the object-argument check. +- Members of anonymous structs and unions, and arrays of aggregates, are not + flow-tracked. +- ``[[uninit]]`` members of locals copied from untracked sources (a call + result, a member, an element) are not flow-tracked: a copy does not + inherit initialization (§5.2) -- it copies indeterminate bits -- but the + source's per-member state is unknown, so reads of such members are + trusted (a missed diagnostic, never a false positive). Copies of tracked + locals and by-value parameters *are* tracked (see `Reads of Uninitialized + Objects`_). +- Virtual base subobjects are not checked by ``ctor_uninit_member`` (they are + initialized by the most-derived class). +- A read of a tracked member inside another member's default initializer is + not detected. +- Whole-entity store credit is parse-order only: a store under a condition + (or inside a lambda body) credits every later use in parse order, so a + read or binding on a path that skips the store is a missed diagnostic. + ``[[now_init]]`` call credit outside constructor bodies is parse-order in + the same way (inside them it is a real dataflow fact, as above). The + requires-uninitialized direction consults this credit at definition time + only -- an instantiation re-walk does not rewind parse-order state, so + re-checked statements must not trip over credit they themselves recorded + -- which makes a reverse-direction violation established only by credit + in fully dependent code a missed diagnostic as well. +- In a template, a violation in non-dependent code is diagnosed at definition + time and may be repeated at instantiation. + + +The ``std::core_ub`` Profile +============================ + +``std::core_ub`` is an initial slice of the proposed profile from Vinnie +Falco's "A Profile for Runtime-Checkable Core-Language Undefined Behavior: +std::core_ub" (P4317; the case identifiers below, in braces, are from its +Appendix A). Its guarantee: **a checkable core-language operation whose +precondition is violated ends the program rather than proceeding into +undefined behavior**. Unlike ``std::init``, it acts at run time. + +When the profile is enforced over a translation unit, the guarded operations +gain a check that traps (``llvm.ubsantrap``) on a violation, with no +in-process diagnostic -- the smallest codegen, matching deployed library +hardening. The checks are the same ones the UndefinedBehaviorSanitizer +emits, so no ``-fsanitize`` flag is needed and the two compose: a guarded +operation traps under the profile whether or not the corresponding sanitizer +is also requested. + +This slice covers the locally checkable cases of P4317 Appendix A.1 that map +to an existing check: + +.. list-table:: + :header-rows: 1 + :widths: 42 58 + + * - Guarded case + - Operation + * - ``{expr.mul.div.by.zero}`` + - Integer division or remainder by zero. + * - ``{expr.mul.representable.type.result}`` + - Signed ``+``, ``-``, ``*``, unary ``-``, and ``INT_MIN / -1`` overflow. + * - ``{expr.shift.neg.and.width}`` + - A negative or too-large shift width, or a signed left shift that loses + bits. + * - ``{basic.align.object.alignment}`` + - An access through an underaligned pointer. + * - ``{expr.unary.dereference}`` + - A dereference of a null pointer. + * - ``{expr.add.out.of.bounds}`` + - A subscript past the end of an array whose bound is known at the access. + * - ``{conv.fpint.*}`` + - A floating-point value converted to an integer type that cannot hold it. + * - ``{expr.static.cast.enum.outside.range}`` + - A load of an out-of-range enumeration value. + * - ``{stmt.return.flow.off}`` + - Flowing off the end of a value-returning function. + +Enforcement can be turned off for a function, or a declaration enclosing it, +with a whole-profile ``[[profiles::suppress(std::core_ub)]]`` (see `Suppressing +Enforcement`_). Suppression at finer granularity -- a single statement, or a +single guarded case by rule name -- is not yet implemented, nor are the many +cases of P4317 that require whole-program instrumentation. + + +Test Profiles +============= + +Clang also ships four ``test::`` profiles (``test::type_cast``, +``test::uninit_read``, ``test::class_final``, and ``test::ctor_final``) that +exist only to exercise the framework in the test suite. They are inert +without an additional ``-cc1``-only flag; see +:doc:`ProfilesFrameworkInternals`. + + +Not Yet Implemented +=================== + +``[[profiles::exempt(...)]]`` (P3589R2 §1.1.6), which would exempt named +included source files from the enforcement of a profile, is not implemented. + +As a **temporary stopgap** until ``[[profiles::exempt]]`` has wording and an +implementation, Clang exempts code that originates in a *system header* from +profile enforcement. This is on by default whenever a profile is enforced, so +enforcing a profile on a translation unit does not report violations inside the +standard library and the other system headers it transitively includes. Pass +``-fno-profiles-exempt-system-headers`` to disable the exemption and enforce +profiles in system-header code as well (spec-exact behavior). + + +Extending the Framework +======================= + +The framework is profile-agnostic: profile names are opaque strings, there is +no central registry, and adding a new profile requires no changes to the +framework itself. See :doc:`ProfilesFrameworkInternals` for the +implementation patterns and the API for adding a new profile. diff --git a/clang/docs/ProfilesFrameworkInternals.rst b/clang/docs/ProfilesFrameworkInternals.rst new file mode 100644 index 0000000000000..e4d90dfc0d239 --- /dev/null +++ b/clang/docs/ProfilesFrameworkInternals.rst @@ -0,0 +1,346 @@ +==================================== +C++ Profiles Framework Internals +==================================== + +.. contents:: + :depth: 2 + :local: + + +This document describes the implementation of the C++ Profiles framework +(`P3589R2 `_) +for Clang contributors -- in particular, how to add a new profile. For +user-facing documentation of the feature, see :doc:`ProfilesFramework`. + + +Architecture +============ + +The framework is profile-agnostic. Profile names are opaque strings and +there is no central registry: a profile is "enforced" simply because the user +wrote ``[[profiles::enforce(name)]]``, and each rule within a profile is just +a string identifier that ``[[profiles::suppress(name, rule: "...")]]`` can +target. ``SemaProfiles`` owns all the bookkeeping -- attribute parsing and +placement checking, enforcement tracking, suppression scoping, template +instantiation, module propagation, and PCH/BMI serialization -- so a profile +implementation consists only of its diagnostics plus calls to the framework +at its semantic check sites. + +Profile-rule diagnostics are defined with the ``ProfileRuleError`` diagnostic +class rather than ``Error``. It marks them SFINAE-suppressed: they do not +count as substitution failures and cannot change overload resolution, but +selected specializations replay them when actually used. The framework +passes the profile name as ``%0``: + +.. code-block:: text + + def err_profile_type_cast_reinterpret : ProfileRuleError< + "'reinterpret_cast' is unsafe under profile '%0'">; + +There are four implementation patterns for compile-time rules, keyed on when +the rule can be checked. A fifth mechanism, for a profile that acts at run +time, inserts checks during code generation (see `Runtime Checks in CodeGen: +std::core_ub`_). + + +Pattern 1: Parse-Time Check Sites +================================= + +For a rule checkable at a single semantic entry point, the entire profile +implementation is one call at that site: + +.. code-block:: c++ + + checkProfileViolation("my::profile", "my_rule", Loc, + diag::err_my_profile_rule); + +The call checks that the profile is enforced and not suppressed (via the +parse-time suppress stack) and skips unevaluated and discarded-statement +contexts. ``test::type_cast`` is the in-tree example. + +Inside a template, parse-time checks follow one unified model. A +*non-dependent* construct is checked on the template pattern, at definition +time: instantiation may reuse such a node unchanged, so deferring would +silently lose the diagnostic. This deliberately trades strict "as-if after +phase 7" purity (P3589R2 §1.1) for reuse-proof diagnostics. An +*instantiation-dependent* construct is always rebuilt at instantiation, where +the re-run check sees the substituted form, once per specialization. A +non-dependent construct that happens to be rebuilt anyway repeats its +definition-time diagnostic with an ``in instantiation of ...`` note -- an +accepted duplication. Under ``-fdelayed-template-parsing`` the body of a +never-instantiated template is never parsed, so definition-time diagnosis of +non-dependent violations does not occur in that mode. + + +Pattern 2: Post-Parse / CFG-Based +================================= + +For a rule that needs whole-function analysis. Each post-parse analysis owns +a small opt-in table of the profiles that ride it, one row per profile +(profile name, rule name, diagnostic); the framework never learns the +profile's name. ``test::uninit_read`` is the in-tree example: + +.. code-block:: c++ + + constexpr CFGUninitProfileEntry CFGUninitProfiles[] = { + {"my::profile", /*Rule=*/"", diag::err_my_profile_rule}, + }; + +The analysis's pass guard ORs in ``anyProfileEnforced(Table)`` so the pass +runs for an enforced profile even when the corresponding warning is +silenced, and the analysis's diagnostic reporter walks the table calling +``shouldEmitProfileViolation(Name, Rule, Stmt, AnalysisDeclContext)`` per use +site, emitting the entry's diagnostic (and skipping the default warning) when +it returns true. + +That overload is the post-parse counterpart of the parse-time suppress +stack: by the time the analysis runs the stack has unwound, so it walks the +AST upward from the use site -- enclosing ``AttributedStmt``\ s, +``DeclStmt``-declared variables, and the lexical ``Decl`` chain -- for a +matching ``[[profiles::suppress]]``. + + +Patterns 3 and 4: Class and Constructor Finalization +==================================================== + +For rules that run once per completed class definition (pattern 3, +``test::class_final``) or once per user-defined constructor with its complete +member-initializer list (pattern 4, ``test::ctor_final``). Both share one +dispatcher and one per-pass table shape: + +.. code-block:: c++ + + constexpr FinalizationProfile ClassFinalizationProfiles[] = { + {"my::profile", &runMyProfileCallback}, + }; + +The class hook runs from the single function every class-completion path +funnels through (parsing, template instantiation, lambda completion); the +constructor hook runs from the two functions every constructor's +member-initializer list funnels through, including instantiation. The +dispatchers filter out dependent entities (the hooks re-fire on each +instantiation), invalid ones, lambdas (pattern 3), and delegating +constructors (pattern 4). Each callback gates its diagnostics on the +decl-aware ``shouldEmitProfileViolation`` overload, which walks the finalized +declaration and its lexical parents for a suppression. + +The split between the two patterns matters: class finalization runs *before +any constructor body or member-initializer list has been parsed*, so a +pattern-3 callback must not inspect a constructor's ``inits()``. Rules that +depend on what a constructor initializes belong on pattern 4; rules that need +flow analysis belong on pattern 2. + + +Suppression Dominion Mechanics +============================== + +A ``[[profiles::suppress]]`` attribute's dominion is the token range of the +construct it appertains to (the user-level rule is stated in +:doc:`ProfilesFramework`). The parse-time suppress stack -- pushed and +popped by ``ProfileSuppressScope`` RAII guards in the parser and the +template-instantiation machinery -- enforces this positionally: each entry +records its construct's token range, and a violation matches an entry only if +its location falls within that range. This keeps a live suppress scope from +leaking into code its tokens do not cover, which would otherwise happen in +two ways: a template pattern instantiated synchronously while the scope is +live (instantiated code retains the pattern's source locations), and a class +or constructor finalized as a side effect of such an instantiation. +Conversely, a local class or lambda defined *inside* the suppressed construct +is covered, whichever path re-enters it. + +The range's end is recorded only when the construct was already fully parsed +when the entry was pushed. For a construct still being parsed no end exists +yet (a mid-parse end location would be misleadingly early), so the entry's +scope lifetime bounds the dominion instead -- exact mid-parse, because the +construct's later tokens do not exist yet, and instantiation of a template +that has no definition yet is deferred past the scope's death. + +Suppression written on a template pattern or its lexical parents is +re-established around instantiation, so it applies to instantiated code. The +reverse is not propagated: a scope live at the *point of instantiation* +covers the trigger's tokens, not the pattern's, and the positional match +above keeps it from suppressing checks inside a synchronously instantiated +body, NSDMI, default argument, or marker re-check. + + +Modules and Serialization +========================= + +``[[profiles::enforce]]`` on a module interface declaration records the +enforced designators on ``Module::EnforcedProfileDesignators``, which is what +``[[profiles::require]]`` on an import validates against. A header unit +records enforcement the same way from the empty-declaration form P3589R2 +prescribes for headers. A non-partition implementation unit inherits the +interface's enforcements through its implicit import of the primary +interface. A partition implementation unit does not implicitly import the +interface, whose BMI is normally built later, so inheritance there is +best-effort: enforcements are inherited only when the interface's BMI is +already resident, and it is never force-loaded nor its absence diagnosed -- a +missed diagnostic, never a change to the meaning of a well-formed program. +``[[profiles::enforce]]`` on a *non-interface* module-declaration is recorded +only translation-unit-locally and is invisible to importers. + +Serialization is automatic for every profile: enforcements are written to a +PCH as ``ENFORCED_PROFILES`` records and restored on load, and +``Module::EnforcedProfileDesignators`` is written to a BMI as +``SUBMODULE_ENFORCED_PROFILES`` records within each submodule block. + + +Redeclaration Compatibility +=========================== + +P3589R2 [decl.attr.enforce]p5 requires a declaration and its redeclarations +to appear in the dominions of mutually compatible profiles. +``checkRedeclarationProfileCompatibility`` runs from the module-level +redeclaration funnel and checks the rule symmetrically in both directions. +It is a framework rule: a plain error, not suppressible with +``[[profiles::suppress]]``, and diagnose-only (the redeclaration still +merges). Two profiles are compatible if they have the same name (designator +arguments configure a profile without changing its identity) or if both are +standard ``std::``-prefixed profiles. + +The previous declaration's dominion is approximated by its top-level module's +exported designators, which is exact for declarations in the module purview. +Two cases have an *unknown* dominion and are skipped rather than guessed at +(a missed diagnostic, never a wrong one): a declaration in an explicit global +module fragment (the exported set does not cover it), and a previous +declaration from the same module family (the exported set under-approximates +the interface TU's dominion, which the current unit inherits anyway). A +textual or PCH previous declaration shares the current TU's dominion and is +not checked; implicit template instantiations are exempt. + + +Runtime Checks in CodeGen: std::core_ub +======================================= + +``std::core_ub`` (documented in :doc:`ProfilesFramework`) is the first profile +whose checks run at run time, so its enforcement has to reach CodeGen -- unlike +the four compile-time patterns, which live entirely in Sema. It reuses the +UndefinedBehaviorSanitizer's existing check emission rather than adding any of +its own; the profile only decides *which* checks are on and that they trap. + +The bridge is a single choke point. ``SemaProfiles::addProfileEnforcement`` is +the one function every enforcement source passes through (the +``[[profiles::enforce]]`` attribute, an imported module, and a restored PCH), +so it mirrors each enforced profile name onto ``ASTContext`` via +``setProfileEnforced``. ``ASTContext::isProfileEnforced`` is what CodeGen +reads; the ``SemaProfiles`` enforcement list itself does not outlive Sema. + +CodeGen then turns enforcement into trapping UBSan checks in three steps: + +- ``CodeGenModule::getProfileCoreUBChecks`` returns the ``SanitizerSet`` the + profile guards when ``std::core_ub`` is enforced (empty otherwise). Adding a + guarded case is one ``Checks.set(SanitizerKind::X, true)`` line here. It is + queried per function, not cached, because enforcement is recorded only once + the leading ``[[profiles::enforce]]`` declaration is parsed, after the module + is built. +- ``CodeGenFunction::StartFunction`` ORs that set into the function's + ``SanOpts`` -- so every per-site UBSan gate (``SanOpts.has(Kind)``) fires -- + and records it in ``ProfileTrapChecks``. A whole-profile + ``[[profiles::suppress(std::core_ub)]]`` on the function or an enclosing + declaration clears the set first (``isCoreUBSuppressed``). This runs before + the ignorelist and ``no_sanitize`` handling, so both can still turn a guarded + check back off. +- ``CodeGenFunction::EmitCheck`` routes a check to a trap when + ``SanitizeTrap`` names it *or* ``isProfileTrapCheck`` does, so a + profile-enabled check traps even with no ``-fsanitize-trap`` flag. + +Because the profile only augments ``SanOpts`` and the trap decision, it needs +no new check emission, no new diagnostics, and no TableGen changes, and it +composes with an explicitly requested ``-fsanitize=`` for the same kind (the +check is emitted once; the profile forces it to trap). + + +Test Profiles +============= + +The four built-in ``test::`` profiles exist only to exercise the framework in +the test suite. They are gated on the ``-cc1``-only +``-fprofiles-test-profiles`` flag: under ``-fprofiles`` alone their +designators are still parsed, recorded, and exported across modules, but +``isProfileEnforced`` reports any ``test::``-prefixed profile as not +enforced, so no ``test::`` rule ever fires. Because that gate keys on the +``test::`` prefix, a new test-only profile must also live under ``test::``. + +- ``test::type_cast`` -- pattern 1; diagnoses ``reinterpret_cast<>`` (the + keyword form only). +- ``test::uninit_read`` -- pattern 2; rides the existing CFG + uninitialized-variables analysis. +- ``test::class_final`` -- pattern 3; fires on completion of every non-lambda + class, on instantiations rather than dependent patterns. +- ``test::ctor_final`` -- pattern 4; fires once per user-defined, + non-delegating constructor. + +The names ``test::other``, ``test::bounds``, ``test::new_profile``, and +``test::not_enforced`` are deliberately *not* implemented and appear in +negative tests as "some other profile" stand-ins; adding a real profile under +any of them would invalidate those tests. + + +The std::init Implementation Map +================================ + +``std::init`` (documented in :doc:`ProfilesFramework`) uses all four +patterns. Its rules map to mechanisms as follows: + +.. list-table:: + :header-rows: 1 + :widths: 24 12 64 + + * - Rule + - Pattern + - Primary entry points + * - ``uninit_read`` + - 2 and 1 + - ``CFGUninitProfiles`` row for local variables; + ``checkInitProfileCtorBody`` and ``checkInitProfileLocalMembers`` + (definite-assignment dataflow over ``[[uninit]]`` members; the + ctor-body pass's ``CallExpr`` arm turns a ``[[now_init]]`` call into a + ``Gen`` bit for the current-object storage bound to the callee's + marked parameters, P4222R2 §6.2); + ``checkInitProfileReadThrough`` at the lvalue-to-rvalue chokepoint, + plus compound-assignment and increment/decrement hooks + * - ``uninit_decl`` + - 1 + - ``checkInitProfileUninitDecl`` + * - ``uninit_with_initializer`` + - 1 + - ``checkInitProfileUninitWithInitializer`` + * - ``static_runtime_init`` + - 1 + - ``checkInitProfileStaticRuntimeInit`` + * - ``static_marker`` + - 1 + - ``checkInitProfileStaticMarker`` + * - ``union_marker``, ``pointer_marker`` + - attribute handler (enforcement-gated) + - ``checkInitProfileMarkerPlacement`` + * - ``ctor_uninit_member`` + - 4 + - ``ConstructorFinalizationProfiles`` row + * - ``ref_to_uninit`` + - 1 + - ``checkInitProfileRefToUninit`` behind per-site wrappers (variable + and member initialization, call arguments, returns, throws, + new-initializers, captures, object arguments) + * - ``uninit_write`` + - 1 + - ``checkInitProfileSubobjectWrite`` + +Two helpers are shared across the rules. ``refersToUninitializedMemory`` +classifies an expression as referring to initialized, uninitialized, or +unknown storage purely from its syntactic form (parse-order store credit +refines it, recorded by ``recordInitProfileStore`` and by the +``recordNowInitArgument`` / ``recordNowUninitArgument`` pair, which share +one argument-shape walk to add or withdraw the credit of storage a +``[[now_init]]`` callee initializes or a ``[[now_uninit]]`` callee +destroys); its ``UninitAccessOpts`` +presets distinguish a *binding* source (markers count everywhere), a value +*read*, and a scalar *store* (which differ in whether the top-level +``[[uninit]]`` marker counts and whether ``[[ref_to_uninit]]`` storage is +trusted). ``defaultInitLeavesScalarIndeterminate`` answers whether a type's +default-initialization leaves an unacknowledged scalar subobject +indeterminate, trusting user-provided default constructors -- the paper's +trust-the-constructor principle (P4222R1.1 §5.1), which is also why members +of objects initialized by a user-provided constructor are deliberately not +flow-tracked. diff --git a/clang/docs/ReleaseNotes.rst b/clang/docs/ReleaseNotes.rst index 031d7405e7f58..412fe563dc54e 100644 --- a/clang/docs/ReleaseNotes.rst +++ b/clang/docs/ReleaseNotes.rst @@ -145,6 +145,16 @@ C23 Feature Support Non-comprehensive list of changes in this release ------------------------------------------------- +- The experimental C++ Profiles framework (``-fprofiles``) gained an initial + slice of the ``std::core_ub`` profile (P4317). Under + ``[[profiles::enforce(std::core_ub)]]`` a set of locally checkable + core-language undefined behaviors -- integer division by zero, signed integer + overflow, invalid shifts, misaligned and null access, statically sized array + bounds, float-to-integer conversion overflow, out-of-range enumeration loads, + and flowing off the end of a value-returning function -- trap at run time, + reusing the UndefinedBehaviorSanitizer checks with no ``-fsanitize`` flag. + See :doc:`ProfilesFramework`. + - Added support for floating point and pointer values in most ``__atomic_`` builtins. diff --git a/clang/docs/index.rst b/clang/docs/index.rst index c2974a4b2f9ea..c7f0691b3cbff 100644 --- a/clang/docs/index.rst +++ b/clang/docs/index.rst @@ -31,6 +31,8 @@ Using Clang as a Compiler ScalableStaticAnalysisFramework/index DataFlowAnalysisIntro FunctionEffectAnalysis + ProfilesFramework + ProfilesFrameworkInternals AddressSanitizer ThreadSanitizer MemorySanitizer diff --git a/clang/include/clang/AST/ASTContext.h b/clang/include/clang/AST/ASTContext.h index 05302c30d18d1..a13ee99963431 100644 --- a/clang/include/clang/AST/ASTContext.h +++ b/clang/include/clang/AST/ASTContext.h @@ -764,6 +764,12 @@ class ASTContext : public RefCountedBase { /// to decide which entities should be instrumented. std::unique_ptr ProfList; + /// Names of the C++ profiles (P3589R2) enforced over this translation unit. + /// Sema records each one as [[profiles::enforce]] is processed; CodeGen + /// reads them to decide whether to emit a profile's runtime checks. The + /// only such profile today is std::core_ub (P4317). + llvm::StringSet<> EnforcedProfiles; + /// The allocator used to create AST objects. /// /// AST objects are never destructed; rather, all memory associated with the @@ -950,6 +956,17 @@ class ASTContext : public RefCountedBase { const LangOptions& getLangOpts() const { return LangOpts; } + /// Record that the named C++ profile (P3589R2) is enforced over this + /// translation unit. Called from Sema for every enforcement source: + /// [[profiles::enforce]], an imported module, and a restored PCH. + void setProfileEnforced(StringRef Name) { EnforcedProfiles.insert(Name); } + + /// True if the named C++ profile is enforced over this translation unit. + /// This is the bridge Sema-recorded enforcement takes to reach CodeGen. + bool isProfileEnforced(StringRef Name) const { + return EnforcedProfiles.contains(Name); + } + // If this condition is false, typo correction must be performed eagerly // rather than delayed in many places, as it makes use of dependent types. // the condition is false for clang's C-only codepath, as it doesn't support diff --git a/clang/include/clang/AST/Attr.h b/clang/include/clang/AST/Attr.h index 0f9fc01391c30..52e294326abe4 100644 --- a/clang/include/clang/AST/Attr.h +++ b/clang/include/clang/AST/Attr.h @@ -23,6 +23,7 @@ #include "clang/Basic/LLVM.h" #include "clang/Basic/LangOptions.h" #include "clang/Basic/OpenMPKinds.h" +#include "clang/Basic/Profiles.h" #include "clang/Basic/Sanitizers.h" #include "clang/Basic/SourceLocation.h" #include "clang/Support/Compiler.h" diff --git a/clang/include/clang/Basic/Attr.td b/clang/include/clang/Basic/Attr.td index 8ab4aaa6f5781..6b85d48f03529 100644 --- a/clang/include/clang/Basic/Attr.td +++ b/clang/include/clang/Basic/Attr.td @@ -441,6 +441,8 @@ def ObjCNonFragileRuntime def HLSL : LangOpt<"HLSL">; +def Profiles : LangOpt<"Profiles">; + // Language option for CMSE extensions def Cmse : LangOpt<"Cmse">; @@ -1719,6 +1721,30 @@ def CXX11NoReturn : InheritableAttr { let Documentation = [CXX11NoReturnDocs]; } +def Uninit : InheritableAttr { + let Spellings = [CXX11<"", "uninit", 202602>]; + let Subjects = SubjectList<[Var, Field], ErrorDiag>; + let Documentation = [UninitDocs]; +} + +def RefToUninit : InheritableAttr { + let Spellings = [CXX11<"", "ref_to_uninit", 202602>]; + let Subjects = SubjectList<[Var, Field, Function], ErrorDiag>; + let Documentation = [RefToUninitDocs]; +} + +def NowInit : InheritableAttr { + let Spellings = [CXX11<"", "now_init", 202602>]; + let Subjects = SubjectList<[Function], ErrorDiag>; + let Documentation = [NowInitDocs]; +} + +def NowUninit : InheritableAttr { + let Spellings = [CXX11<"", "now_uninit", 202602>]; + let Subjects = SubjectList<[Function], ErrorDiag>; + let Documentation = [NowUninitDocs]; +} + def NonBlocking : TypeAttr { let Spellings = [Clang<"nonblocking">]; let Args = [ExprArgument<"Cond", /*optional*/1>]; @@ -3454,6 +3480,52 @@ def Suppress : DeclOrStmtAttr { let Documentation = [SuppressDocs]; } +// C++ Profiles framework (P3589R2) + +def ProfilesEnforce : Attr { + let Spellings = [CXX11<"profiles", "enforce">]; + let Args = [ + VariadicStringArgument<"ProfileNames">, + VariadicStringArgument<"ProfileDesignators">, + VariadicUnsignedArgument<"ProfileArgumentCounts">, + VariadicStringArgument<"ProfileArgumentKeys">, + VariadicStringArgument<"ProfileArgumentValues">, + VariadicUnsignedArgument<"ProfileArgumentKinds"> + ]; + let LangOpts = [Profiles]; + let HasCustomParsing = 1; + let Documentation = [ProfilesEnforceDocs]; +} + +def ProfilesSuppress : DeclOrStmtAttr { + let Spellings = [CXX11<"profiles", "suppress">]; + let Args = [ + StringArgument<"ProfileName">, + StringArgument<"Justification", 1>, + StringArgument<"Rule", 1>, + VariadicStringArgument<"RawArguments">, + VariadicStringArgument<"RawArgumentKeys">, + VariadicStringArgument<"RawArgumentValues">, + VariadicUnsignedArgument<"RawArgumentKinds"> + ]; + let LangOpts = [Profiles]; + let HasCustomParsing = 1; + let Documentation = [ProfilesSuppressDocs]; +} + +def ProfilesRequire : Attr { + let Spellings = [CXX11<"profiles", "require">]; + let Args = [ + StringArgument<"Designator">, + VariadicStringArgument<"DesignatorArgumentKeys">, + VariadicStringArgument<"DesignatorArgumentValues">, + VariadicUnsignedArgument<"DesignatorArgumentKinds"> + ]; + let LangOpts = [Profiles]; + let HasCustomParsing = 1; + let Documentation = [ProfilesRequireDocs]; +} + def SysVABI : DeclOrTypeAttr { let Spellings = [GCC<"sysv_abi">]; // let Subjects = [Function, ObjCMethod]; diff --git a/clang/include/clang/Basic/AttrDocs.td b/clang/include/clang/Basic/AttrDocs.td index 43f827b4c60ee..987e5ceae489c 100644 --- a/clang/include/clang/Basic/AttrDocs.td +++ b/clang/include/clang/Basic/AttrDocs.td @@ -875,6 +875,181 @@ migration for code using ``[[noreturn]]`` after including ````. }]; } +def UninitDocs : Documentation { + let Category = DocCatVariable; + let Heading = "uninit"; + let Content = [{ +The ``[[uninit]]`` attribute marks a variable or non-static data +member definition as intentionally left uninitialized. It is purely +informational: it has no effect on code generation and the entity still +has the same indeterminate value it would have without the attribute. + +The attribute exists to support the initialization profile (see +:doc:`ProfilesFramework`). Under that profile, a definition such as +``int x;`` is diagnosed because it leaves ``x`` with an indeterminate +value; writing ``int x [[uninit]];`` instead suppresses that +declaration-site diagnostic and documents that the lack of an initializer +is intentional. The attribute does **not** suppress diagnostics that fire +when the variable is later read before being assigned; the read-before-init +rule still applies. + +A declaration or non-static data member with both ``[[uninit]]`` +and an initializer is ill-formed under the initialization profile (the +marker contradicts the explicit initialization). The same applies when +default-initialization of the entity's type is not a genuine no-op -- a +non-trivial default constructor, a default member initializer, or a virtual +table pointer initializes something, and a type with nothing left +indeterminate has nothing to acknowledge -- so the marker is rejected there +too. + +On a non-static data member the marker also excuses the member from the +profile's requirement that every constructor initialize it. A scalar marked +member is instead flow-checked where local analysis is sound: a read before +it is assigned is diagnosed in the constructor's own body, and -- for a +member of a class with no user-provided constructor -- in any function body +that declares a bare local of that class. A marked member of an object with +a user-provided constructor reached through any other object is trusted (the +constructor body may have assigned it, which local analysis cannot see). + +The attribute cannot be applied where leaving the entity uninitialized is +meaningless -- a reference, a function parameter, or a structured binding -- +and these placements are diagnosed regardless of ``-fprofiles``. The +initialization profile additionally rejects the marker on a union object or +union member. + +Outside the initialization profile the attribute is otherwise silently +accepted and has no effect, so code that uses it remains valid when compiled +without ``-fprofiles``. + +This attribute is distinct from the Clang vendor attribute +``[[clang::uninitialized]]``, which interacts with +``-ftrivial-auto-var-init`` to force a local variable to remain +uninitialized. + }]; +} + +def RefToUninitDocs : Documentation { + let Category = DocCatVariable; + let Heading = "ref_to_uninit"; + let Content = [{ +The ``[[ref_to_uninit]]`` attribute marks a pointer, reference, or +pointer-returning function as referring to *uninitialized* memory. It is +purely informational: it has no effect on code generation. + +The attribute exists to support the initialization profile (see +:doc:`ProfilesFramework`). That profile bans forming an ordinary pointer or +reference to uninitialized storage; ``[[ref_to_uninit]]`` is the opt-in that +lets a program thread uninitialized memory (for example, an input buffer or +a memory-pool allocation) through pointers and references. Under the profile +a ``[[ref_to_uninit]]`` pointer or reference may only be bound to +uninitialized memory, and a pointer or reference that is *not* marked may +only be bound to initialized memory. + +The attribute applies to a variable, non-static data member, function +parameter, or function (for its return value) whose type is a pointer or +reference to an object; applying it elsewhere is diagnosed regardless of +``-fprofiles``. A function pointer or reference (or a pointer-to-member) +denotes a function or member, never uninitialized memory, so it is rejected. + +Outside the initialization profile the attribute is otherwise silently +accepted and has no effect, so code that uses it remains valid when compiled +without ``-fprofiles``. + }]; +} + +def NowInitDocs : Documentation { + let Category = DocCatFunction; + let Heading = "now_init"; + let Content = [{ +The ``[[now_init]]`` attribute marks a function as *initializing* the +storage passed to each of its ``[[ref_to_uninit]]`` parameters. It is purely +informational: it has no effect on code generation. + +The attribute exists to support the initialization profile (see +:doc:`ProfilesFramework`) and implements the annotation proposed in P4222R2 +§6.2 (its exact placement and spelling track an open committee question). +Passing uninitialized memory to a ``[[ref_to_uninit]]`` parameter never by +itself convinces the profile that the memory was initialized -- the callee +may merely store the pointer. On a ``[[now_init]]`` function the profile +instead treats the storage bound to every ``[[ref_to_uninit]]`` parameter as +initialized after the call returns, so the caller may continue to use the +original name: + +.. code-block:: c++ + + [[now_init]] void fill(int *p [[ref_to_uninit]]); + + void f() { + int u [[uninit]]; + fill(&u); + int v = u; // OK: fill() initialized u + } + +The attribute requires at least one parameter marked ``[[ref_to_uninit]]`` +(it would be vacuous otherwise); this is diagnosed regardless of +``-fprofiles``. It does not cover the implicit object parameter of a member +function, which cannot carry ``[[ref_to_uninit]]``. + +Outside the initialization profile the attribute is otherwise silently +accepted and has no effect, so code that uses it remains valid when compiled +without ``-fprofiles``. + }]; +} + +def NowUninitDocs : Documentation { + let Category = DocCatFunction; + let Heading = "now_uninit"; + let Content = [{ +The ``[[now_uninit]]`` attribute marks a function as *ending the lifetime* +of the storage passed to each of its pointer or reference parameters, +leaving that storage uninitialized. It is purely informational: it has no +effect on code generation. + +The attribute is the mirror of ``[[now_init]]`` and exists to support the +initialization profile (see :doc:`ProfilesFramework`). It supplies the +recording P4222R2 §4.4 notes is missing ("the object subjected to +``destroy_at()`` should be considered uninitialized, but there is no way of +recording that in the code"; the exact placement and spelling of the marker +family track an open committee question). After a call to a +``[[now_uninit]]`` function, the profile treats the storage bound to every +pointer or reference parameter as uninitialized again, so the object can be +re-initialized -- and a second destruction, or a use of the storage through +an ordinary pointer or reference, is diagnosed: + +.. code-block:: c++ + + [[now_init]] void construct_at(int *p [[ref_to_uninit]]); + [[now_uninit]] void destroy_at(int *p); + + void f() { + int u [[uninit]]; + construct_at(&u); + destroy_at(&u); + construct_at(&u); // OK: u is uninitialized again after the destroy + } + + void g() { + int u [[uninit]]; + construct_at(&u); + destroy_at(&u); + destroy_at(&u); // error: double destruction + } + +Because the attribute covers *every* pointer or reference parameter, apply +it only to functions that end the lifetime of every such argument's +storage. It requires at least one parameter of pointer or reference type +(it would be vacuous otherwise); this is diagnosed regardless of +``-fprofiles``. A function may carry both ``[[now_uninit]]`` and +``[[now_init]]``: such a reinitializer models destroy-then-construct, so +the storage bound to its ``[[ref_to_uninit]]`` parameters is initialized +after the call. + +Outside the initialization profile the attribute is otherwise silently +accepted and has no effect, so code that uses it remains valid when compiled +without ``-fprofiles``. + }]; +} + def NoMergeDocs : Documentation { let Category = DocCatStmt; let Content = [{ @@ -6445,6 +6620,46 @@ the ``int`` parameter is the one that represents the error. }]; } +def ProfilesEnforceDocs : Documentation { + let Category = DocCatStmt; + let Content = [{ +The ``[[profiles::enforce]]`` attribute requests enforcement of one or more +C++ profiles (P3589R2) for the remainder of the translation unit. It must +appear on an empty-declaration or module-declaration before any non-empty +declaration. + +.. code-block:: c++ + + [[profiles::enforce(std::type)]]; + void f(); // subject to std::type profile rules + }]; +} + +def ProfilesSuppressDocs : Documentation { + let Category = DocCatStmt; + let Content = [{ +The ``[[profiles::suppress]]`` attribute locally suppresses enforcement of +a C++ profile (P3589R2) for the appertaining declaration or statement. + +.. code-block:: c++ + + [[profiles::suppress(std::type, justification: "legacy code")]] + void legacy_function(); + }]; +} + +def ProfilesRequireDocs : Documentation { + let Category = DocCatStmt; + let Content = [{ +The ``[[profiles::require]]`` attribute on a module-import-declaration +validates that the imported module enforces the specified profile (P3589R2). + +.. code-block:: c++ + + import MyModule [[profiles::require(std::type)]]; + }]; +} + def SuppressDocs : Documentation { let Category = DocCatStmt; let Content = [{ diff --git a/clang/include/clang/Basic/AttributeCommonInfo.h b/clang/include/clang/Basic/AttributeCommonInfo.h index 77b5eb8a1a7cc..2c2004683224f 100644 --- a/clang/include/clang/Basic/AttributeCommonInfo.h +++ b/clang/include/clang/Basic/AttributeCommonInfo.h @@ -72,7 +72,7 @@ class AttributeCommonInfo { IgnoredAttribute, UnknownAttribute, }; - enum class Scope { NONE, CLANG, GNU, MSVC, OMP, HLSL, VK, GSL, RISCV }; + enum class Scope { NONE, CLANG, GNU, MSVC, OMP, HLSL, VK, GSL, RISCV, PROFILES }; enum class AttrArgsInfo { None, Optional, diff --git a/clang/include/clang/Basic/DiagnosticParseKinds.td b/clang/include/clang/Basic/DiagnosticParseKinds.td index de10dbe5d0628..0915bb5f2bb6f 100644 --- a/clang/include/clang/Basic/DiagnosticParseKinds.td +++ b/clang/include/clang/Basic/DiagnosticParseKinds.td @@ -1890,4 +1890,14 @@ def err_hlsl_number_literal_underflow : Error< "float literal has a magnitude that is too small to be represented as a float type">; def err_hlsl_rootsig_non_zero_flag : Error<"flag value is neither a literal 0 nor a named value">; +// C++ Profiles framework (P3589R2) +def err_profiles_expected_profile_name : Error< + "expected profile name">; +def err_profiles_expected_lparen : Error< + "%0 attribute requires an argument clause">; +def err_profiles_expected_rparen : Error< + "expected ')' in %0 attribute">; +def err_profiles_invalid_argument_token : Error< + "invalid token in profile argument">; + } // end of Parser diagnostics diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td index 0c25eb2443d5e..de21e112f796d 100644 --- a/clang/include/clang/Basic/DiagnosticSemaKinds.td +++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td @@ -14115,4 +14115,113 @@ def err_cuda_device_kernel_launch_not_supported def err_cuda_device_kernel_launch_require_rdc : Error<"kernel launch from __device__ or __global__ function requires " "relocatable device code (i.e. requires -fgpu-rdc)">; +// C++ Profiles framework (P3589R2) +class ProfileRuleError : Error { + let SFINAE = SFINAE_Suppress; +} + +def err_profiles_enforce_not_empty_decl : Error< + "'profiles::enforce' attribute only allowed on empty-declarations and module-declarations">; +def err_profiles_enforce_not_at_tu_scope : Error< + "'profiles::enforce' attribute on empty-declaration must be at translation unit scope">; +def err_profiles_enforce_after_decl : Error< + "'profiles::enforce' attribute on empty-declaration must precede all non-empty declarations">; +def err_profiles_enforce_mismatch : Error< + "repeated enforcement of profile '%0' with different designator">; +def err_profiles_require_not_on_import : Error< + "'profiles::require' attribute only allowed on module-import-declarations">; +def err_profiles_require_not_enforced : Error< + "required profile '%0' is not enforced by imported module">; +def err_profiles_redecl_incompatible : Error< + "%select{redeclaration of %1 is not in the dominion of a profile " + "compatible with '%2', which module '%3' enforces where %1 was " + "previously declared|%1 was previously declared in module '%3', outside " + "the dominion of a profile compatible with '%2', which this translation " + "unit enforces}0">; +def err_profiles_suppress_justification_not_string : Error< + "'justification' argument of 'profiles::suppress' must be a string literal">; +def err_profile_type_cast_reinterpret : ProfileRuleError< + "'reinterpret_cast' is unsafe under profile '%0'">; +def err_profile_uninit_read : ProfileRuleError< + "variable %1 is read before initialization under profile '%0'">; +def err_profile_class_final_test : ProfileRuleError< + "test profile fired on completion of class %1 under profile '%0'">; +def err_profile_ctor_final_test : ProfileRuleError< + "test profile fired on finalization of a constructor for class %1 under " + "profile '%0'">; +def err_init_uninit_read : ProfileRuleError< + "variable %1 is read before initialization under profile '%0'">; +def err_init_member_read_before_init : ProfileRuleError< + "member %1 is read before initialization under profile '%0'">; +def err_init_uninit_decl : ProfileRuleError< + "variable %1 must be initialized or marked '[[uninit]]' under " + "profile '%0'">; +def err_init_uninit_union : ProfileRuleError< + "variable %1 of union type must be initialized under profile '%0'">; +def err_init_static_runtime_init : ProfileRuleError< + "non-local variable %1 requires constant initialization under " + "profile '%0'">; +def err_init_uninit_with_initializer : ProfileRuleError< + "%select{variable|member}2 %1 cannot be both '[[uninit]]' and have an " + "initializer under profile '%0'">; +def err_init_uninit_member_initialized : ProfileRuleError< + "member %1 cannot be marked '[[uninit]]' under profile '%0'; " + "default-initialization of its type %2 does not leave it uninitialized">; +def note_init_uninit_member_type : Note< + "%select{default-initialization of %0 runs a constructor|" + "no subobject of %0 is left uninitialized|" + "%0 has no usable default constructor}1">; +def err_uninit_attr_invalid_subject : Error< + "'uninit' attribute cannot be applied to %select{a reference|" + "a function parameter|a structured binding}0">; +def err_ref_to_uninit_attr_invalid_type : Error< + "'ref_to_uninit' attribute only applies to pointers, references, and " + "functions returning them">; +def err_now_init_attr_no_marked_parameter : Error< + "'now_init' attribute requires at least one parameter marked " + "'[[ref_to_uninit]]'">; +def err_now_uninit_attr_no_pointer_parameter : Error< + "'now_uninit' attribute requires at least one parameter of pointer or " + "reference type">; +def err_init_union_marker : ProfileRuleError< + "'[[uninit]]' cannot be applied to %select{a variable of union type|" + "a union member|a data member of union type}1 under profile '%0'">; +def err_init_uninit_pointer_marker : ProfileRuleError< + "'[[uninit]]' cannot be applied to a pointer under profile '%0'; " + "initialize the pointer (for example to 'nullptr')">; +def err_init_uninit_static_marker : ProfileRuleError< + "'[[uninit]]' cannot be applied to variable %1 with %select{static|thread}2 " + "storage duration under profile '%0'; it is zero-initialized">; +def err_init_ctor_uninit_member : ProfileRuleError< + "constructor does not initialize member %1 under profile '%0'">; +def err_init_ctor_uninit_anon_union : ProfileRuleError< + "constructor does not initialize any member of the anonymous union under " + "profile '%0'">; +def note_init_uninit_member_here : Note< + "member %0 declared here">; +def note_init_uninit_anon_union_here : Note< + "anonymous union declared here">; +def err_init_ctor_uninit_base : ProfileRuleError< + "constructor does not initialize base class %1 under profile '%0'">; +def note_init_uninit_base_here : Note< + "base class %0 declared here">; +def err_init_ref_to_uninit_requires_uninit : ProfileRuleError< + "%select{pointer|reference}1 marked '[[ref_to_uninit]]' must refer to " + "uninitialized memory under profile '%0'">; +def err_init_uninit_requires_ref_to_uninit : ProfileRuleError< + "%select{pointer|reference}1 to uninitialized memory must be marked " + "'[[ref_to_uninit]]' under profile '%0'">; +def err_init_uninit_read_through : ProfileRuleError< + "read %select{through a '[[ref_to_uninit]]' pointer or reference|" + "of a subobject of an '[[uninit]]' object}1 accesses " + "uninitialized memory under profile '%0'">; +def err_init_uninit_subobject_write : ProfileRuleError< + "writing %select{a member|an element}1 of an '[[uninit]]' object does not " + "initialize it under profile '%0'; initialize the whole object">; +def err_init_uninit_ref_capture : ProfileRuleError< + "capturing %1 by reference binds a reference to uninitialized memory " + "under profile '%0'">; +def err_init_member_call_on_uninit : ProfileRuleError< + "calling member function %1 binds its implicit object parameter to " + "uninitialized memory under profile '%0'">; } // end of sema component. diff --git a/clang/include/clang/Basic/LangOptions.def b/clang/include/clang/Basic/LangOptions.def index 2c93e60b48cc5..b69975bc574b3 100644 --- a/clang/include/clang/Basic/LangOptions.def +++ b/clang/include/clang/Basic/LangOptions.def @@ -154,6 +154,9 @@ LANGOPT(EmitAllDecls , 1, 0, Benign, "emitting all declarations") LANGOPT(MathErrno , 1, 1, NotCompatible, "errno in math functions") LANGOPT(Modules , 1, 0, NotCompatible, "modules semantics") LANGOPT(CPlusPlusModules , 1, 0, Compatible, "C++ modules syntax") +LANGOPT(Profiles , 1, 0, Compatible, "C++ profiles framework") +LANGOPT(ProfilesTestProfiles, 1, 0, Benign, "C++ profiles framework built-in test:: profiles") +LANGOPT(ProfilesExemptSystemHeaders, 1, 1, Benign, "exempt system-header code from C++ profile enforcement (temporary stopgap for [[profiles::exempt]])") LANGOPT(SkipODRCheckInGMF , 1, 0, NotCompatible, "Skip ODR checks for decls in the global module fragment") LANGOPT(BuiltinHeadersInSystemModules, 1, 0, NotCompatible, "builtin headers belong to system modules, and _Builtin_ modules are ignored for cstdlib headers") ENUM_LANGOPT(CompilingModule, CompilingModuleKind, 3, CMK_None, Benign, diff --git a/clang/include/clang/Basic/Module.h b/clang/include/clang/Basic/Module.h index 69a1de6f79b35..30aa802e11505 100644 --- a/clang/include/clang/Basic/Module.h +++ b/clang/include/clang/Basic/Module.h @@ -17,6 +17,7 @@ #include "clang/Basic/DirectoryEntry.h" #include "clang/Basic/FileEntry.h" +#include "clang/Basic/Profiles.h" #include "clang/Basic/SourceLocation.h" #include "llvm/ADT/ArrayRef.h" #include "llvm/ADT/DenseSet.h" @@ -457,6 +458,10 @@ class alignas(8) Module { /// module depends. llvm::SmallSetVector Imports; + /// Profile enforcements on this module's declaration (P3589R2). + using EnforcedProfile = profiles::EnforcedProfile; + SmallVector EnforcedProfileDesignators; + /// The set of top-level modules that affected the compilation of this module, /// but were not imported. llvm::SmallSetVector AffectingClangModules; diff --git a/clang/include/clang/Basic/Profiles.h b/clang/include/clang/Basic/Profiles.h new file mode 100644 index 0000000000000..34b3e2c1b8d4e --- /dev/null +++ b/clang/include/clang/Basic/Profiles.h @@ -0,0 +1,57 @@ +//===--- Profiles.h - C++ profiles framework helpers -----------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_CLANG_BASIC_PROFILES_H +#define LLVM_CLANG_BASIC_PROFILES_H + +#include "clang/Basic/SourceLocation.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/ADT/Twine.h" +#include + +namespace clang::profiles { + +enum class ProfileArgumentKind : unsigned { + Positional = 0, + Named = 1, +}; + +struct ProfileArgument { + llvm::StringRef Key; + llvm::StringRef Value; + ProfileArgumentKind Kind = ProfileArgumentKind::Positional; + SourceRange Range; + + bool isNamed() const { return Kind == ProfileArgumentKind::Named; } +}; + +/// A profile enforced by [[profiles::enforce]]: the profile name plus the +/// canonical spelling of the designator that enforced it (P3589R2 [decl.attr +/// .enforce]p3 compares repeated enforcements by their spelling). Shared by +/// Sema's enforcement list, Module's exported enforcement set, and the +/// serialized PCH record. +struct EnforcedProfile { + std::string ProfileName; + std::string Designator; +}; + +/// The canonical spelling of a profile argument: the value token for a +/// positional argument, "key : value" for a named one. Enforcement identity +/// (P3589R2 [decl.attr.enforce]p3) compares designators by this spelling. +/// A template over the argument representation so it serves both +/// ProfileArgument and the parser's owning-string argument type. +template +std::string getCanonicalProfileArgumentSpelling(const ArgumentT &Argument) { + if (!Argument.isNamed()) + return std::string(Argument.Value); + return (llvm::Twine(Argument.Key) + " : " + Argument.Value).str(); +} + +} // namespace clang::profiles + +#endif // LLVM_CLANG_BASIC_PROFILES_H diff --git a/clang/include/clang/Options/Options.td b/clang/include/clang/Options/Options.td index 1d89d983fe79a..6bf99fb642e7c 100644 --- a/clang/include/clang/Options/Options.td +++ b/clang/include/clang/Options/Options.td @@ -1695,6 +1695,24 @@ defm coroutines : BoolFOption<"coroutines", "Enable support for the C++ Coroutines">, NegFlag>; +defm profiles : BoolFOption<"profiles", + LangOpts<"Profiles">, DefaultFalse, + PosFlag, + NegFlag>, + ShouldParseIf; +def fprofiles_test_profiles : Flag<["-"], "fprofiles-test-profiles">, + Group, Visibility<[CC1Option]>, + HelpText<"Enable the C++ profiles framework's built-in test:: profiles (test suite only)">, + MarshallingInfoFlag>; +defm profiles_exempt_system_headers : BoolFOption<"profiles-exempt-system-headers", + LangOpts<"ProfilesExemptSystemHeaders">, DefaultTrue, + PosFlag, + NegFlag>, + ShouldParseIf; + defm coro_aligned_allocation : BoolFOption<"coro-aligned-allocation", LangOpts<"CoroAlignedAllocation">, DefaultFalse, PosFlag Arguments; + }; + struct ParsedProfileSuppressArgs { + std::string Name; + std::string Justification; + std::string Rule; + SmallVector RawArguments; + SmallVector Arguments; + }; + + bool ParseProfileName(std::string &Name); + bool ParseProfileDesignator(ParsedProfileDesignator &Designator); + bool ParseProfileDesignatorList( + SmallVectorImpl &Designators); + bool ParseProfileArgumentList( + SmallVectorImpl &Args); + bool ParseProfileSuppressBody(ParsedProfileSuppressArgs &Args); + bool ParseNonCommaBalancedToken(std::string &Spelling, + SourceRange *Range = nullptr); + bool ParseNonOperatorNonPunctuatorToken(std::string &Spelling, + SourceRange *Range = nullptr); + + /// Diagnose C++11 attributes on a module- or import-declaration, which + /// accept none except the profile attribute \p AllowedKind (handled in + /// Sema). \p KeywordDiagID / \p AttrDiagID are the declaration's + /// keyword-attribute and standard-attribute diagnostics. + void ProhibitModuleAttributesExcept(const ParsedAttributesView &Attrs, + ParsedAttr::Kind AllowedKind, + unsigned KeywordDiagID, + unsigned AttrDiagID); + void MaybeParseCXX11Attributes(Declarator &D) { if (isAllowedCXX11AttributeSpecifier()) { ParsedAttributes Attrs(AttrFactory); diff --git a/clang/include/clang/Sema/AnalysisBasedWarnings.h b/clang/include/clang/Sema/AnalysisBasedWarnings.h index 0ed61e56825be..ff59eab8618d0 100644 --- a/clang/include/clang/Sema/AnalysisBasedWarnings.h +++ b/clang/include/clang/Sema/AnalysisBasedWarnings.h @@ -117,6 +117,12 @@ class AnalysisBasedWarnings { // Issue warnings that require whole-translation-unit analysis. void IssueWarnings(TranslationUnitDecl *D); + /// True if a profile that rides the uninitialized-variables analysis (see the + /// CFGUninitProfiles table) is enforced. Lets the per-function dispatch keep + /// running that analysis after a TU error so these profiles still diagnose + /// every later function instead of silently stopping at the first error. + bool hasEnforcedCFGUninitProfile() const; + void registerVarDeclWarning(VarDecl *VD, PossiblyUnreachableDiag PUD); void issueWarningsForRegisteredVarDecl(VarDecl *VD); diff --git a/clang/include/clang/Sema/ParsedAttr.h b/clang/include/clang/Sema/ParsedAttr.h index 5387f9fad6cd2..944035aabe929 100644 --- a/clang/include/clang/Sema/ParsedAttr.h +++ b/clang/include/clang/Sema/ParsedAttr.h @@ -18,6 +18,7 @@ #include "clang/Basic/AttributeCommonInfo.h" #include "clang/Basic/Diagnostic.h" #include "clang/Basic/ParsedAttrInfo.h" +#include "clang/Basic/Profiles.h" #include "clang/Basic/SourceLocation.h" #include "clang/Sema/Ownership.h" #include "llvm/ADT/PointerUnion.h" @@ -96,6 +97,28 @@ struct PropertyData { : GetterId(getterId), SetterId(setterId) {} }; +struct ProfileDesignator { + StringRef Name; + StringRef Spelling; + ArrayRef Arguments; +}; + +struct ProfileEnforceArgs { + ArrayRef Designators; +}; + +struct ProfileSuppressArgs { + StringRef Name; + StringRef Justification; + StringRef Rule; + ArrayRef RawArguments; + ArrayRef Arguments; +}; + +struct ProfileRequireArgs { + ProfileDesignator Designator; +}; + } // namespace detail /// A union of the various pointer types that can be passed to an @@ -184,6 +207,8 @@ class ParsedAttr final const Expr *MessageExpr; + void *CustomData = nullptr; + const ParsedAttrInfo &Info; ArgsUnion *getArgsBuffer() { return getTrailingObjects(); } @@ -473,6 +498,36 @@ class ParsedAttr final return getPropertyDataBuffer().SetterId; } + void setCustomData(void *Data) { CustomData = Data; } + + template T &getCustomData() { + assert(CustomData && "No custom data set"); + return *static_cast(CustomData); + } + + template const T &getCustomData() const { + assert(CustomData && "No custom data set"); + return *static_cast(CustomData); + } + + const detail::ProfileEnforceArgs &getProfileEnforceArgs() const { + assert(getKind() == AT_ProfilesEnforce && + "not a profiles::enforce attribute"); + return getCustomData(); + } + + const detail::ProfileSuppressArgs &getProfileSuppressArgs() const { + assert(getKind() == AT_ProfilesSuppress && + "not a profiles::suppress attribute"); + return getCustomData(); + } + + const detail::ProfileRequireArgs &getProfileRequireArgs() const { + assert(getKind() == AT_ProfilesRequire && + "not a profiles::require attribute"); + return getCustomData(); + } + /// Set the macro identifier info object that this parsed attribute was /// declared in if it was declared in a macro. Also set the expansion location /// of the macro. @@ -717,6 +772,33 @@ class AttributePool { AttributeFactory &getFactory() const { return Factory; } + template T *make(Args &&...args) { + // Pool-allocated objects are never destroyed, only deallocated wholesale. + static_assert(std::is_trivially_destructible_v); + void *Mem = Factory.Alloc.Allocate(sizeof(T), alignof(T)); + return ::new (Mem) T(std::forward(args)...); + } + + StringRef copyString(StringRef S) { + if (S.empty()) + return {}; + char *Data = static_cast(Factory.Alloc.Allocate(S.size(), 1)); + std::memcpy(Data, S.data(), S.size()); + return {Data, S.size()}; + } + + template MutableArrayRef allocateArray(unsigned N) { + // Pool-allocated objects are never destroyed, only deallocated wholesale. + static_assert(std::is_trivially_destructible_v); + if (N == 0) + return {}; + auto *Arr = + static_cast(Factory.Alloc.Allocate(sizeof(T) * N, alignof(T))); + for (unsigned I = 0; I < N; ++I) + ::new (&Arr[I]) T(); + return {Arr, N}; + } + void clear() { Factory.reclaimPool(*this); Attrs.clear(); diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h index 832e46286194a..a7ccdb63df678 100644 --- a/clang/include/clang/Sema/Sema.h +++ b/clang/include/clang/Sema/Sema.h @@ -176,6 +176,7 @@ class SemaOpenACC; class SemaOpenCL; class SemaOpenMP; class SemaPPC; +class SemaProfiles; class SemaPseudoObject; class SemaRISCV; class SemaSPIRV; @@ -1532,6 +1533,12 @@ class Sema final : public SemaBase { return *PPCPtr; } + /// C++ Profiles framework (P3589R2); see SemaProfiles.h. + SemaProfiles &Profiles() const { + assert(ProfilesPtr); + return *ProfilesPtr; + } + SemaPseudoObject &PseudoObject() { assert(PseudoObjectPtr); return *PseudoObjectPtr; @@ -1623,6 +1630,7 @@ class Sema final : public SemaBase { std::unique_ptr OpenCLPtr; std::unique_ptr OpenMPPtr; std::unique_ptr PPCPtr; + std::unique_ptr ProfilesPtr; std::unique_ptr PseudoObjectPtr; std::unique_ptr RISCVPtr; std::unique_ptr SPIRVPtr; @@ -9953,7 +9961,8 @@ class Sema final : public SemaBase { SourceLocation ModuleLoc, ModuleDeclKind MDK, ModuleIdPath Path, ModuleIdPath Partition, ModuleImportState &ImportState, - bool SeenNoTrivialPPDirective); + bool SeenNoTrivialPPDirective, + const ParsedAttributesView &Attrs = {}); /// The parser has processed a global-module-fragment declaration that begins /// the definition of the global module fragment of the current module unit. @@ -9984,6 +9993,9 @@ class Sema final : public SemaBase { SourceLocation ImportLoc, Module *M, ModuleIdPath Path = {}); + void ActOnModuleImportAttrs(Decl *ImportDecl, + const ParsedAttributesView &Attrs); + /// The parser has processed a module import translated from a /// #include or similar preprocessing directive. void ActOnAnnotModuleInclude(SourceLocation DirectiveLoc, Module *Mod); diff --git a/clang/include/clang/Sema/SemaProfiles.h b/clang/include/clang/Sema/SemaProfiles.h new file mode 100644 index 0000000000000..cf725a8fbf0f2 --- /dev/null +++ b/clang/include/clang/Sema/SemaProfiles.h @@ -0,0 +1,572 @@ +//===----- SemaProfiles.h --- C++ profiles framework ----------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +/// \file +/// This file declares semantic analysis for the C++ profiles framework +/// (P3589R2) and the built-in std::init initialization profile (P4222R1.1). +/// See clang/docs/ProfilesFrameworkInternals.rst for the design and +/// clang/docs/ProfilesFramework.rst for the user-facing documentation. +/// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_CLANG_SEMA_SEMAPROFILES_H +#define LLVM_CLANG_SEMA_SEMAPROFILES_H + +#include "clang/AST/ASTFwd.h" +#include "clang/Basic/Profiles.h" +#include "clang/Basic/SourceLocation.h" +#include "clang/Sema/SemaBase.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringRef.h" + +namespace clang { + +class AnalysisDeclContext; +class Module; +class ParsedAttr; +class ParsedAttributesView; +class ProfilesSuppressAttr; + +class SemaProfiles : public SemaBase { +public: + SemaProfiles(Sema &S); + + struct ProfileEnforcement : profiles::EnforcedProfile { + SourceLocation EnforceLoc; + }; + SmallVector EnforcedProfiles; + + /// True if an included AST file (PCH) contributed a non-empty top-level + /// declaration to this TU. The [[profiles::enforce]] placement check + /// (P3589R2 [decl.attr.enforce]p1) consults this instead of deserializing + /// the PCH's declarations; ASTWriter ORs it forward so chained PCHs + /// propagate the bit. + bool TUPrecededByNonEmptyDecl = false; + + struct ProfileSuppressEntry { + StringRef ProfileName; + StringRef RuleName; + /// Begin location of the construct the suppression appertains to (the + /// declaration or statement, not the attribute). The entry's dominion + /// starts here (P3589R2 s2.4p3). + SourceLocation Begin; + /// End location of the construct, recorded only when the construct was + /// fully parsed at push time; invalid otherwise, leaving the dominion's + /// end bounded by the ProfileSuppressScope's lifetime. That fallback is + /// exact for a construct still being parsed -- its later tokens do not + /// exist yet, and instantiation of a not-yet-defined template is + /// deferred past the scope's death -- while a completed construct's + /// recorded end keeps a live scope from covering a pattern first + /// declared after it. + SourceLocation End; + }; + SmallVector ProfileSuppressStack; + + bool isProfileEnforced(StringRef ProfileName) const; + + /// True if any entry of \p Entries names an enforced profile. \p Entries is + /// any profile opt-in table whose elements expose a \c Name member; shared + /// by the post-parse dispatch gates (the CFG analysis pass guard and the + /// finalization dispatcher). + template + bool anyProfileEnforced(const Table &Entries) const { + return llvm::any_of( + Entries, [&](const auto &E) { return isProfileEnforced(E.Name); }); + } + + const ProfileEnforcement *getProfileEnforcement(StringRef ProfileName) const; + bool addProfileEnforcement(StringRef Name, StringRef Designator, + SourceLocation Loc); + bool processProfilesEnforceAttr( + const ParsedAttr &AL, Module *Mod, SmallVectorImpl *NewNames, + SmallVectorImpl *NewDesignators, + SmallVectorImpl *NewArgumentCounts = nullptr, + SmallVectorImpl *NewArgumentKeys = nullptr, + SmallVectorImpl *NewArgumentValues = nullptr, + SmallVectorImpl *NewArgumentKinds = nullptr); + + ProfilesSuppressAttr *makeProfilesSuppressAttr(const ParsedAttr &AL); + + /// Create an implicit ProfilesSuppressAttr carrying just a profile and rule + /// name (no justification or arguments), for propagating an active + /// suppression onto a declaration. + ProfilesSuppressAttr *makeImplicitProfilesSuppressAttr(StringRef ProfileName, + StringRef RuleName); + + /// True if a live parse-time suppress entry for \p ProfileName / + /// \p RuleName covers \p Loc. An entry matches only tokens within its + /// construct's recorded range (its dominion, P3589R2 s2.4p3); when the + /// construct was still being parsed at push time no end is recorded and + /// the owning ProfileSuppressScope's lifetime bounds the dominion's end. + /// Tokens from outside the construct -- e.g. a template pattern + /// instantiated synchronously while the scope is live, wherever it is + /// declared -- are not suppressed. + bool isProfileSuppressed(StringRef ProfileName, StringRef RuleName, + SourceLocation Loc) const; + bool isProfileSuppressed(StringRef ProfileName, StringRef RuleName, + const Decl *D) const; + bool isProfileSuppressed(StringRef ProfileName, StringRef RuleName, + const Stmt *S, AnalysisDeclContext &AC) const; + bool shouldEmitProfileViolation(StringRef ProfileName, StringRef RuleName, + SourceLocation Loc); + bool shouldEmitProfileViolation(StringRef ProfileName, StringRef RuleName, + SourceLocation Loc, const Decl *D); + bool shouldEmitProfileViolation(StringRef ProfileName, StringRef RuleName, + const Stmt *UseStmt, + AnalysisDeclContext &AC) const; + bool checkProfileViolation(StringRef ProfileName, StringRef RuleName, + SourceLocation Loc, unsigned DiagID); + + /// P3589R2 [decl.attr.enforce]p5: a declaration and its redeclarations must + /// appear in the dominions of mutually compatible profiles. Called from + /// \c Sema::CheckRedeclarationInModule when \p New redeclares \p Old. Only + /// a previous declaration from another module unit (a named module or a + /// header unit) can carry a different dominion; that TU's dominion is + /// approximated by the module's exported designator set. Profiles are + /// compatible by name, with all std:: profiles mutually compatible. + /// Diagnose-only: the redeclaration is not invalidated. + void checkRedeclarationProfileCompatibility(const NamedDecl *New, + const NamedDecl *Old); + + /// Dispatch class-finalization profile callbacks for a completed class. + /// Called from \c Sema::CheckCompletedCXXClass so parser, template + /// instantiation, and lambda finalization paths all reach the same hook. + /// Dependent, invalid, and lambda classes are filtered out. + void checkProfileViolationsAtClassFinalization(CXXRecordDecl *RD); + + /// Dispatch constructor-finalization profile callbacks once a constructor's + /// member-initializer list is complete. Called from \c ActOnMemInitializers + /// and \c ActOnDefaultCtorInitializers, which also serve template + /// instantiations (via \c InstantiateMemInitializers), so every + /// user-defined constructor is covered at the point its \c inits() is fully + /// populated -- unlike class finalization, which runs before any + /// constructor body is parsed. Dependent, invalid, and delegating + /// constructors are filtered out. + void + checkProfileViolationsAtConstructorFinalization(CXXConstructorDecl *Ctor); + + /// std::init / uninit_decl (R2, paper §4.2): diagnose an automatic variable + /// definition that leaves the object (or a scalar subobject) indeterminate + /// without an acknowledging [[uninit]] marker. Called from + /// \c ActOnUninitializedDecl after default-initialization is attempted. + void checkInitProfileUninitDecl(const VarDecl *Var); + + /// std::init / static_marker (paper §3, §4.2): diagnose [[uninit]] on a + /// static or thread-storage variable, which is zero-initialized by language + /// rule and therefore an initialized object. Called from + /// \c ActOnUninitializedDecl. + void checkInitProfileStaticMarker(const VarDecl *Var); + + /// std::init / static_runtime_init (paper §3): diagnose a non-local static + /// whose initialization needs a runtime constructor. \p CheckConstInit + /// lazily evaluates whether the initializer is constant (trivial + /// default-init counts as constant here). Returns true if the diagnostic + /// was emitted, in which case the caller skips -Wglobal-constructors. + bool + checkInitProfileStaticRuntimeInit(const VarDecl *Var, + llvm::function_ref CheckConstInit); + + /// std::init / uninit_with_initializer (R4): diagnose \p D if it is both + /// marked [[uninit]] and has an initializer. Shared by the variable + /// (\c CheckCompleteVariableDeclaration) and non-static data member + /// (\c ActOnFinishCXXInClassMemberInitializer) paths. \p Init is the + /// (possibly null) initializer; a RecoveryExpr placeholder for a failed + /// initialization does not count as a user-written initializer. + void checkInitProfileUninitWithInitializer(const ValueDecl *D, + const Expr *Init); + + /// True if default-initialization of \p T would leave at least one scalar + /// subobject with an indeterminate value. Shared by the std::init rules + /// uninit_decl (at the variable declaration), ctor_uninit_member (for a + /// class-typed member), and uninit_with_initializer. A class with a + /// user-provided default constructor is trusted (that constructor is + /// checked at its own definition). Dependent and incomplete types are + /// treated as determinate. + /// + /// When \p HonorUninitMarkers is true, a data member marked [[uninit]] + /// is treated as acknowledged and skipped, so a type whose only + /// indeterminate scalars are all marked is reported as determinate. + /// uninit_decl and ctor_uninit_member pass true (the marker excuses the + /// member, paper §6.2); uninit_with_initializer passes false because it + /// needs the factual answer (whether the default-initialization is + /// genuinely a no-op). + bool defaultInitLeavesScalarIndeterminate(QualType T, + bool HonorUninitMarkers = false); + + /// True if default-initialization of \p T is a genuine no-op that leaves + /// the object (or every array element) uninitialized -- the only state + /// consistent with an [[uninit]] marker. A non-trivial default constructor + /// (user-provided anywhere in the subtree, a default member initializer, a + /// virtual table pointer) initializes something, contradicting the marker + /// (paper §4.2 rule 2, §5.3); a deleted or absent default constructor makes + /// default-initialization ill-formed, not a no-op (the marker is + /// unsatisfiable); an all-scalars-determinate type (e.g. an + /// empty struct) has nothing uninitialized. Type-level (not a query on the + /// synthesized construct-expression) because the field flavor and + /// static_marker's no-initializer arm have no construct-expression, and + /// getBaseElementType handles arrays and scalars uniformly. Shared by + /// uninit_with_initializer and static_marker (through their common + /// initializer guard) and the field-marker flavor. + bool defaultInitIsVacuous(QualType T); + + /// If \p E (stripped of parens and implicit casts) directly names a + /// declaration -- a DeclRefExpr or a MemberExpr -- return that declaration; + /// otherwise null. The std::init checks read [[ref_to_uninit]] / + /// [[uninit]] markers only off a directly named entity. + static const ValueDecl *getDirectlyNamedDecl(const Expr *E); + + /// std::init / ref_to_uninit (paper §5): true only if \p E is affirmatively + /// recognized as referring to (for a pointer source) or, when + /// \p IsReference, denoting (for a glvalue source) uninitialized storage. + /// Recognized purely locally from the expression's syntactic form -- the + /// address of, or a subobject of, a [[uninit]] entity; a value of a + /// [[ref_to_uninit]] pointer/reference or array; a dereference of such a + /// pointer; a cast of such a pointer to another pointer type, or of such a + /// glvalue to another reference; a call to a [[ref_to_uninit]]-returning + /// function or to a known uninitialized-returning allocator (the malloc + /// and alloca builtin families and raw replaceable ::operator new calls; + /// calloc's result is initialized, realloc's + /// unknown); or a new-expression whose default-initialization leaves the + /// allocated object indeterminate (e.g. new int). A trusted-initialized + /// source and an unrecognized (unknown) source both return false (no flow + /// analysis). + bool refersToUninitializedMemory(const Expr *E, bool IsReference) const; + + /// std::init / ref_to_uninit (paper §5): check that the initialization of a + /// pointer or reference is consistent with its [[ref_to_uninit]] marking -- + /// a marked target must refer to uninitialized memory, and an unmarked + /// target must not. Shared by the variable, data-member, assignment, + /// argument, and return check sites; gated by shouldEmitProfileViolation. + /// A Decl-less call defers only on an instantiation-dependent \p Src -- + /// such a construct is always rebuilt at instantiation, re-running this + /// funnel with the substituted source -- and otherwise fires at definition + /// time; if the construct is rebuilt at instantiation anyway (a local + /// operand, a call argument, a return), the same diagnostic repeats there + /// (accepted for now). A Decl-carrying call instead defers via the + /// D->isTemplated() check in shouldEmitProfileViolation and fires on the + /// instantiated declaration. + void checkInitProfileRefToUninit(SourceLocation Loc, bool TargetIsRefToUninit, + bool IsReference, const Expr *Src, + const Decl *D = nullptr); + + /// std::init / ref_to_uninit (paper §5): check that binding \p Src to + /// \p Target (a variable, data member, parameter, or function) is + /// consistent with the target's [[ref_to_uninit]] marking. A null \p Target + /// is a binding with no declaration to carry the marker (a parameter of a + /// call through a function pointer) and is checked as unmarked. \p T is the + /// bound type -- the target's type, or the return type when \p Target is a + /// function. No-op unless \p T is a non-dependent pointer or reference (a + /// dependent type defers to instantiation, where the check site re-runs + /// with the concrete type). \p D, when available, is the declaration used + /// for suppression lookup and template-pattern deferral. + void checkInitProfileRefToUninitBinding(SourceLocation Loc, + const ValueDecl *Target, QualType T, + const Expr *Src, + const Decl *D = nullptr); + + /// std::init / uninit_read (paper §4.5): diagnose a read *through* a + /// [[ref_to_uninit]] pointer or reference, whose result is itself + /// uninitialized. Called from Sema::DefaultLvalueConversion at the single + /// lvalue-to-rvalue chokepoint, with \p Glvalue the operand being loaded + /// and \p ValueType its value type; and from the compound-assignment + /// (Sema::CheckAssignmentOperands) and increment/decrement + /// (Sema::CreateBuiltinUnaryOp) operator sites, whose reads build no + /// lvalue-to-rvalue node (the shift-compounds are excluded there because + /// their LHS promotion already funnels through the chokepoint). Reuses the + /// ref_to_uninit recognizer with its read access preset, so a direct read + /// of a named [[uninit]] object is left to the flow-based uninit_read + /// pass. A std::byte read is exempt (paper §4.5). Defers only on an + /// instantiation-dependent \p Glvalue (rebuilt at instantiation, where the + /// check re-runs); a non-dependent read fires at definition time and may + /// repeat if the read is rebuilt at instantiation anyway (accepted). + void checkInitProfileReadThrough(SourceLocation Loc, const Expr *Glvalue, + QualType ValueType); + + /// std::init / uninit_write (paper §5.4-§5.6): diagnose a scalar store to a + /// proper subobject of a named [[uninit]] entity -- delayed piecemeal + /// initialization, which only whole-object construct_at could make good. + /// Called from Sema::CheckAssignmentOperands (the shared simple/compound + /// assignment funnel) and from the built-in increment/decrement arm of + /// Sema::CreateBuiltinUnaryOp, with \p LHS the store target. Reuses the + /// recognizer with its write access preset: a store to the whole named + /// entity is its initialization (paper §4.5), and storage reached through + /// [[ref_to_uninit]] is trusted (the deferred construct_at slice), so only + /// a below-top-level [[uninit]] marker fires. A std::byte store is exempt + /// (paper §4.5). Defers only on an instantiation-dependent \p LHS (rebuilt + /// at instantiation, where the check re-runs); a non-dependent store fires + /// at definition time and may repeat if the assignment is rebuilt at + /// instantiation anyway (accepted). + void checkInitProfileSubobjectWrite(SourceLocation Loc, const Expr *LHS); + + /// std::init / ref_to_uninit (paper §5): a pointer argument passed through + /// a variadic `...` parameter, which cannot carry [[ref_to_uninit]], is + /// checked as an unmarked target. Called with the promoted argument from + /// the C++ variadic promotion loops (Sema::GatherArgumentsForCall and + /// Sema::BuildCallToObjectOfClassType); a non-pointer argument is a no-op + /// (its value read is the lvalue-to-rvalue chokepoint's). + void checkInitProfileVariadicArgument(const Expr *Arg); + + /// std::init / ref_to_uninit (paper §4.3): a by-reference lambda capture of + /// \p Var binds a reference to its storage, and a capture cannot carry + /// [[ref_to_uninit]], so capturing an entity that denotes uninitialized + /// storage -- an [[uninit]] variable, or a [[ref_to_uninit]] reference -- + /// is always the unmarked-direction violation. Called from + /// \c Sema::BuildLambdaExpr for each by-reference non-init variable capture + /// (init-captures are checked at \c createLambdaInitCaptureVarDecl); defers + /// only when the captured variable's type is instantiation-dependent. + /// TreeTransform always rebuilds a lambda at instantiation, so a deferred + /// capture re-processes there -- and a definition-time fire repeats there + /// (accepted). + void checkInitProfileRefCapture(SourceLocation Loc, const ValueDecl *Var); + + /// std::init / ref_to_uninit (paper §7.2): a member call binds its implicit + /// object parameter to \p Object, and that parameter can never carry + /// [[ref_to_uninit]], so a call on an object recognized as uninitialized + /// storage is always the unmarked-direction violation. Called from + /// \c Sema::PerformImplicitObjectArgumentInitialization, the funnel every + /// member-call flavor's object argument converts through -- dot and arrow + /// calls, member operators, functor operator(), operator->, and conversion + /// operators. Explicit-object member functions initialize their object as an + /// ordinary parameter and are already checked there; a destructor call is + /// skipped (destruction of uninitialized storage is the deferred destroy_at + /// slice), as is a static call operator (no implicit object parameter, like + /// a static member call). Defers only on an instantiation-dependent + /// \p Object -- the call + /// is rebuilt at instantiation, re-running the funnel -- and otherwise fires + /// at definition time, repeating if the call is rebuilt anyway (accepted). + void checkInitProfileObjectArgument(const Expr *Object, + const CXXMethodDecl *Method); + + /// std::init / ref_to_uninit (paper §4.3): assigning to a pointer must + /// respect the assigned-to pointer's [[ref_to_uninit]] marking; a no-op for + /// a non-pointer LHS. Hosts the cluster from Sema::CreateBuiltinBinOp's + /// BO_Assign arm. An instantiation-dependent LHS defers to the + /// instantiation rebuild (its marker cannot be read yet); the source's + /// dependence is the shared funnel's to defer on. + void checkInitProfilePointerAssignment(Expr *LHS, Expr *RHS, + SourceLocation OpLoc); + + /// std::init: the check pair every built-in assignment hosts (paper + /// §5.4-§5.6): the compound-assignment old-value load (read-through -- + /// excluding the shifts, whose LHS promotion already loads through the + /// lvalue-to-rvalue chokepoint) and the subobject-write check. Hosts the + /// cluster from Sema::CheckAssignmentOperands. \p IsCompound distinguishes + /// `op=` from `=` (!CompoundType.isNull() at the host site). + void checkInitProfileAssignmentOperands(BinaryOperatorKind Opc, + Expr *LHSExpr, bool IsCompound, + SourceLocation OpLoc); + + /// std::init: the check pair a built-in ++/-- hosts -- the old-value load + /// (read-through) and the store (subobject-write). Hosts the cluster from + /// Sema::CreateBuiltinUnaryOp's increment/decrement arm. Records the store + /// credit last, after both pre-store checks. + void checkInitProfileIncDec(Expr *Operand, SourceLocation OpLoc); + + /// std::init: record parse-order whole-entity store credit for \p LHS, the + /// left operand of a completed built-in assignment (called from the tail + /// of Sema::CheckAssignmentOperands) or the operand of a built-in ++/--. + /// Assigning a whole [[uninit]] local is its initialization (paper + /// §4.2/§4.5), and a store through the exact `*p` / `r` lvalue of a + /// [[ref_to_uninit]] local or parameter initializes the pointee (§4.3); a + /// store to a marked *pointer* itself reseats it and clears its pointee + /// credit. Element stores (p[i] = e) neither credit nor invalidate + /// (§5.4/§5.5 ban element-wise tracking), and escapes never credit (§6.2 + /// reserves callee-initialization for now_init()). Purely parse-order -- + /// no dominance or flow analysis -- so the credit errs only toward missed + /// diagnostics. Deliberately not gated on enforcement or + /// [[profiles::suppress]]: a suppressed store still initializes, and + /// failing to credit it would turn suppression into later false positives. + void recordInitProfileStore(const Expr *LHS); + + /// std::init / [[now_init]] (P4222R2 §6.2): a [[now_init]] callee + /// initializes the storage bound to each of its [[ref_to_uninit]] + /// parameters, so the binding earns the same parse-order credit the + /// equivalent direct store would. Called from the tail of + /// checkInitProfileRefToUninitBinding when \p Target is a marked parameter + /// of a [[now_init]] function; recognizes the affirmatively creditable + /// source shapes -- &u / u (whole-entity credit on an [[uninit]] local), p + /// / *p / &*p (pointee credit on a marked local/parameter pointer; §6.2's + /// initialize2(p) example), r (pointee credit on a marked reference), and + /// &base.m / base.m (per-object member credit, resolveMemberStoreBase + /// keys) -- through the recognizers' explicit-cast pass-through. Variadic + /// arguments, unmarked parameters, and calls through function pointers + /// never reach here (no marked ParmVarDecl target). Recorded regardless of + /// enforcement, suppression, or diagnosis of the binding itself (the + /// callee still initializes; recordInitProfileStore's rationale), but not + /// in never-executed contexts. + void recordNowInitArgument(const ValueDecl *Target, QualType T, + const Expr *Src); + + /// std::init / [[now_uninit]]: a [[now_uninit]] callee ends the lifetime + /// of the storage bound to each of its pointer/reference parameters + /// (P4222R2 §4.4's missing destroy_at recording), so the binding + /// *withdraws* the parse-order credit the equivalent [[now_init]] call + /// would have recorded: the storage classifies as uninitialized again, + /// re-construction becomes legal, and a second destruction or an + /// unmarked-target binding of the storage is the ordinary + /// unmarked-direction violation. Called from the tail of + /// checkInitProfileRefToUninitBinding when \p Target is a + /// pointer/reference parameter of a [[now_uninit]] function -- the + /// parameters are unmarked (they receive initialized memory), so unlike + /// recordNowInitArgument no parameter marker is required. Recognizes the + /// same source shapes, which are marker-keyed on the source side, so an + /// ordinary initialized argument withdraws nothing. Same gates as its + /// sibling: not enforcement- or suppression-gated (a suppressed destroy + /// still destroys), but never-executed contexts withdraw nothing. + void recordNowUninitArgument(const ValueDecl *Target, QualType T, + const Expr *Src); + + /// The shared shape walk of recordNowInitArgument and + /// recordNowUninitArgument: resolve \p Src (bound as \p T) to the storage + /// the annotated callee initializes or destroys -- &u / u (whole-entity), + /// p / *p / &*p (marked-pointer pointee), r (marked-reference referent), + /// &base.m / base.m (per-object member) -- and add (\p Withdraw false) or + /// remove (true) the corresponding credit bit. + void recordLifetimeAnnotatedArgument(QualType T, const Expr *Src, + bool Withdraw); + + /// True if the current expression-evaluation context never executes at + /// runtime (unevaluated or discarded-statement), mirroring + /// shouldEmitProfileViolation's context checks: a store or a callee + /// initialization seen there earns no credit. The shared gate of + /// recordInitProfileStore and recordNowInitArgument. + bool inNeverExecutedContext() const; + + /// True if \p VD is a local [[uninit]] variable credited by a recorded + /// whole-entity store; the recognizers then classify it as initialized + /// (which also enables the paper's reverse-direction rule: a credited + /// entity requires an unmarked target). + bool hasWholeObjectStoreCredit(const ValueDecl *VD) const; + + /// True if \p VD is a [[ref_to_uninit]] local/parameter pointer or + /// reference credited by a recorded store through it; the storage behind + /// it then classifies as initialized (until a pointer is reseated -- + /// references cannot be reseated, so no *store* ever clears their credit; + /// a [[now_uninit]] callee withdraws either kind). + bool hasPointeeStoreCredit(const ValueDecl *VD) const; + + /// True if the [[uninit]] member \p F of the base object identified by + /// \p Base (see resolveMemberStoreBase; null returns false) is credited by + /// a recorded whole-member store; the member then classifies as + /// initialized through that same base. + bool hasMemberStoreCredit(const Decl *Base, const FieldDecl *F) const; + + /// Resolve the identity key of a member access's base object for the + /// per-object member store credit: the parse-time pattern of the enclosing + /// function declaration for a current-object access (this->m / m / + /// (*this).m) -- so credit recorded in one function body can never satisfy + /// a binding in another, while a statement an instantiation reuses + /// (unrebuilt) from its template or generic-lambda pattern agrees with a + /// rebuilt one on the key -- or the directly named local-storage, + /// non-reference VarDecl of a dot access (a.m). Any other base -- another + /// member (a.b.m; §5.4 rejects deep delayed-initialization tracking), an + /// arrow through an arbitrary pointer value, a reference (an alias to an + /// object also reachable other ways) -- is untrackable per object: null. + const Decl *resolveMemberStoreBase(const MemberExpr *ME) const; + + /// Store-credit bits for recordInitProfileStore. + enum InitStoreCreditFlags : unsigned { + /// The [[uninit]] entity itself was assigned (u = e, u @= e, ++u). + WholeStored = 1u << 0, + /// The storage behind the [[ref_to_uninit]] entity was written through + /// the exact *p / r lvalue. + PointeeStored = 1u << 1, + }; + + /// Parse-order store credit, keyed by the credited local/parameter (only + /// local-storage VarDecls carrying the relevant marker are ever inserted). + /// Never cleared across the translation unit: the keys are unique + /// declarations, and template instantiations build fresh declarations, so + /// pattern-time and instantiation-time state stay independent. + llvm::DenseMap InitStoreCredit; + + /// Parse-order whole-member store credit, keyed per base object: the base + /// is the directly named local-storage VarDecl (a.m = e) or, for the + /// current object (this->m = e / m = e), the parse-time pattern of the + /// enclosing function declaration -- so credit recorded in one function + /// body can never satisfy a binding in another, two locals of the same + /// type never share credit, and instantiations agree with their pattern + /// on statements they reuse from it (see resolveMemberStoreBase). Only + /// WholeStored is ever set: member *pointee* stores (*a.p = e) are + /// deliberately never credited -- per-object pointee aliasing (copies + /// share pointees) makes them unsound to approximate. + /// Never cleared, for the same reasons as InitStoreCredit (instantiations + /// key on fresh field and function declarations). + llvm::DenseMap, unsigned> + MemberStoreCredit; + + /// std::init / ref_to_uninit (paper §5): a thrown pointer copy-initializes + /// the exception object, which cannot carry [[ref_to_uninit]]; a no-op for + /// a non-pointer exception object. Hosts the cluster from + /// Sema::BuildCXXThrow. + void checkInitProfileThrowOperand(const Expr *Operand); + + /// std::init / ref_to_uninit (paper §5): a written initializer for an + /// allocated pointer binds it like a variable initialization, and a heap + /// pointer object cannot carry [[ref_to_uninit]]. \p Init is the single + /// written initializer expression, or null when there is none (a no-op). + /// Hosts the cluster from Sema::BuildCXXNew, which calls it for scalar + /// allocations only: an array new's written elements are each checked by + /// the aggregate element hooks instead. An instantiation-dependent + /// allocated type defers to the instantiation rebuild. + void checkInitProfileNewInitializer(QualType AllocType, Expr *Init); + + /// std::init / pointer_marker + union_marker (paper §4.1, §5.6): diagnose + /// [[uninit]] placed on a pointer, a union variable, or a union member. + /// \p D must already carry the UninitAttr (the marker location is taken + /// from it). Decl-aware via shouldEmitProfileViolation, so it defers on a + /// templated pattern and is re-checked on the instantiated entity. + void checkInitProfileMarkerPlacement(const Decl *D); + + /// [[ref_to_uninit]] is only meaningful on a pointer or reference to an + /// object (for a function, its return type). Returns true when \p D's type + /// is invalid for the marker, diagnosing err_ref_to_uninit_attr_invalid_type + /// at \p AttrLoc unless \p Diagnose is false. A dependent type returns + /// false: validation defers to the instantiation re-check in + /// Sema::InstantiateAttrs, which drops the marker when the substituted type + /// is invalid -- silently in a SFINAE context (\p Diagnose false there), so + /// the marker can never affect overload resolution; a dropped marker is + /// inert. Not profile policy -- fires regardless of -fprofiles, like the + /// parse-time handler it serves. + bool diagnoseInvalidRefToUninitMarker(const Decl *D, SourceLocation AttrLoc, + bool Diagnose = true); + + /// [[uninit]] is meaningless on a reference (it must bind when declared). + /// Returns true when \p D's type is a reference, diagnosing + /// err_uninit_attr_invalid_subject at \p AttrLoc unless \p Diagnose is + /// false. A dependent type returns false: validation defers to the + /// instantiation re-check in Sema::InstantiateAttrs, which drops the marker + /// when the substituted type is a reference (silently in a SFINAE context). + /// The parameter / structured-binding rejections stay in the parse-time + /// handler -- they do not depend on the type. Not profile policy -- fires + /// regardless of -fprofiles. + bool diagnoseInvalidUninitMarker(const Decl *D, SourceLocation AttrLoc, + bool Diagnose = true); + + class ProfileSuppressScope { + Sema &S; + unsigned Count = 0; + + void push(StringRef ProfileName, StringRef RuleName, SourceLocation Begin, + SourceLocation End); + void addFromDecl(const Decl *D); + + public: + ProfileSuppressScope(Sema &S, const ParsedAttributesView &Attrs); + ProfileSuppressScope(Sema &S, const Decl *D, + bool WalkLexicalParents = false); + ProfileSuppressScope(Sema &S, ArrayRef Attrs, + SourceLocation Begin, SourceLocation End); + ~ProfileSuppressScope(); + }; +}; + +} // namespace clang + +#endif // LLVM_CLANG_SEMA_SEMAPROFILES_H diff --git a/clang/include/clang/Serialization/ASTBitCodes.h b/clang/include/clang/Serialization/ASTBitCodes.h index 5db0b08f877ce..8e7ea9b3520d8 100644 --- a/clang/include/clang/Serialization/ASTBitCodes.h +++ b/clang/include/clang/Serialization/ASTBitCodes.h @@ -748,6 +748,14 @@ enum ASTRecordTypes { /// Record code for #pragma clang riscv intrinsic vector. RISCV_VECTOR_INTRINSICS_PRAGMA = 78, + + /// Record code for enforced profile designators (P3589R2). + ENFORCED_PROFILES = 79, + + /// Record code for whether the TU contains a non-empty top-level + /// declaration, consulted by the [[profiles::enforce]] placement check + /// (P3589R2 [decl.attr.enforce]p1) so it need not deserialize the PCH. + PROFILES_TU_HAS_NONEMPTY_DECL = 80, }; /// Record types used within a source manager block. @@ -881,6 +889,9 @@ enum SubmoduleRecordTypes { /// Specifies affecting modules that were not imported. SUBMODULE_AFFECTING_MODULES = 18, + + /// Specifies enforced profile designators (P3589R2). + SUBMODULE_ENFORCED_PROFILES = 19, }; /// Record types used within a comments block. diff --git a/clang/include/clang/Serialization/ASTReader.h b/clang/include/clang/Serialization/ASTReader.h index 14f7d8a69a1b3..8b39b8e037809 100644 --- a/clang/include/clang/Serialization/ASTReader.h +++ b/clang/include/clang/Serialization/ASTReader.h @@ -18,6 +18,7 @@ #include "clang/Basic/DiagnosticOptions.h" #include "clang/Basic/IdentifierTable.h" #include "clang/Basic/OpenCLOptions.h" +#include "clang/Basic/Profiles.h" #include "clang/Basic/SourceLocation.h" #include "clang/Basic/StackExhaustionHandler.h" #include "clang/Basic/Version.h" @@ -1007,6 +1008,13 @@ class ASTReader /// The floating point pragma option settings. SmallVector FPPragmaOptions; + /// Enforced profile designators from PCH (P3589R2). + SmallVector SerializedEnforcedProfiles; + + /// Whether an included AST file recorded a non-empty top-level declaration + /// (PROFILES_TU_HAS_NONEMPTY_DECL, P3589R2 enforce placement). + bool SerializedTUHasNonEmptyDecl = false; + /// The pragma clang optimize location (if the pragma state is "off"). SourceLocation OptimizeOffPragmaLocation; diff --git a/clang/include/clang/Serialization/ASTWriter.h b/clang/include/clang/Serialization/ASTWriter.h index 0f3993ad01693..0638212d12313 100644 --- a/clang/include/clang/Serialization/ASTWriter.h +++ b/clang/include/clang/Serialization/ASTWriter.h @@ -641,6 +641,7 @@ class ASTWriter : public ASTDeserializationListener, void WriteModuleFileExtension(Sema &SemaRef, ModuleFileExtensionWriter &Writer); void WriteRISCVIntrinsicPragmas(Sema &SemaRef); + void WriteEnforcedProfiles(Sema &SemaRef); unsigned DeclParmVarAbbrev = 0; unsigned DeclContextLexicalAbbrev = 0; diff --git a/clang/lib/Basic/Attributes.cpp b/clang/lib/Basic/Attributes.cpp index 5878a4e3f83a4..af5c42ab5c492 100644 --- a/clang/lib/Basic/Attributes.cpp +++ b/clang/lib/Basic/Attributes.cpp @@ -236,7 +236,8 @@ getScopeFromNormalizedScopeName(StringRef ScopeName) { .Case("vk", AttributeCommonInfo::Scope::VK) .Case("msvc", AttributeCommonInfo::Scope::MSVC) .Case("omp", AttributeCommonInfo::Scope::OMP) - .Case("riscv", AttributeCommonInfo::Scope::RISCV); + .Case("riscv", AttributeCommonInfo::Scope::RISCV) + .Case("profiles", AttributeCommonInfo::Scope::PROFILES); } unsigned AttributeCommonInfo::calculateAttributeSpellingListIndex() const { diff --git a/clang/lib/CodeGen/CGExpr.cpp b/clang/lib/CodeGen/CGExpr.cpp index 069846b854a87..6a88a8ade041b 100644 --- a/clang/lib/CodeGen/CGExpr.cpp +++ b/clang/lib/CodeGen/CGExpr.cpp @@ -4173,8 +4173,12 @@ void CodeGenFunction::EmitCheck( GuardedCheck = Builder.CreateOr(Check, Builder.CreateNot(Allow)); } - // -fsanitize-trap= overrides -fsanitize-recover=. - llvm::Value *&Cond = CGM.getCodeGenOpts().SanitizeTrap.has(Ord) ? TrapCond + // -fsanitize-trap= overrides -fsanitize-recover=. A check the std::core_ub + // profile (P4317) turned on always traps, even when -fsanitize-trap does + // not name it: the profile's response to a violation is termination. + bool IsTrap = + CGM.getCodeGenOpts().SanitizeTrap.has(Ord) || isProfileTrapCheck(Ord); + llvm::Value *&Cond = IsTrap ? TrapCond : CGM.getCodeGenOpts().SanitizeRecover.has(Ord) ? RecoverableCond : FatalCond; diff --git a/clang/lib/CodeGen/CodeGenFunction.cpp b/clang/lib/CodeGen/CodeGenFunction.cpp index fb0d5e450f9d6..df28eed2e7e14 100644 --- a/clang/lib/CodeGen/CodeGenFunction.cpp +++ b/clang/lib/CodeGen/CodeGenFunction.cpp @@ -747,6 +747,21 @@ static llvm::Constant *getPrologueSignature(CodeGenModule &CGM, return CGM.getTargetCodeGenInfo().getUBSanFunctionSignature(CGM); } +// std::core_ub (P4317): true if \p D, or a declaration lexically enclosing it, +// carries a whole-profile [[profiles::suppress(std::core_ub)]] (an empty rule, +// which suppresses every case). A rule-qualified suppression names a case the +// runtime checks do not track individually, so it does not disable them here. +static bool isCoreUBSuppressed(const Decl *D) { + for (; D;) { + for (const auto *PSA : D->specific_attrs()) + if (PSA->getProfileName() == "std::core_ub" && PSA->getRule().empty()) + return true; + const DeclContext *DC = D->getLexicalDeclContext(); + D = DC ? dyn_cast(DC) : nullptr; + } + return false; +} + void CodeGenFunction::StartFunction(GlobalDecl GD, QualType RetTy, llvm::Function *Fn, const CGFunctionInfo &FnInfo, @@ -769,6 +784,17 @@ void CodeGenFunction::StartFunction(GlobalDecl GD, QualType RetTy, CurFnInfo = &FnInfo; assert(CurFn->isDeclaration() && "Function already has body?"); + // std::core_ub (P4317): an enforced profile adds its guarded UBSan checks to + // this function, in trap mode, unless the function opts out with + // [[profiles::suppress(std::core_ub)]]. Done before the ignorelist and + // no_sanitize handling below so both can still turn a guarded check off; a + // kind cleared from SanOpts is never emitted, so its leftover + // ProfileTrapChecks bit is harmless. + ProfileTrapChecks = CGM.getProfileCoreUBChecks(); + if (D && isCoreUBSuppressed(D)) + ProfileTrapChecks.clear(); + SanOpts.Mask |= ProfileTrapChecks.Mask; + // If this function is ignored for any of the enabled sanitizers, // disable the sanitizer for the function. do { diff --git a/clang/lib/CodeGen/CodeGenFunction.h b/clang/lib/CodeGen/CodeGenFunction.h index 9771b89b55aae..8afb69199d5f1 100644 --- a/clang/lib/CodeGen/CodeGenFunction.h +++ b/clang/lib/CodeGen/CodeGenFunction.h @@ -577,6 +577,17 @@ class CodeGenFunction : public CodeGenTypeCache { /// Sanitizers enabled for this function. SanitizerSet SanOpts; + /// The subset of SanOpts turned on by an enforced std::core_ub profile + /// (P4317). These always trap on a violation, even when -fsanitize-trap does + /// not name them; EmitCheck consults this to route them to a trap. + SanitizerSet ProfileTrapChecks; + + /// True if \p Ord is guarded by the std::core_ub profile in this function, + /// and so must trap rather than call a diagnostic handler. + bool isProfileTrapCheck(SanitizerKind::SanitizerOrdinal Ord) const { + return ProfileTrapChecks.has(Ord); + } + /// True if CodeGen currently emits code implementing sanitizer checks. bool IsSanitizerScope = false; diff --git a/clang/lib/CodeGen/CodeGenModule.cpp b/clang/lib/CodeGen/CodeGenModule.cpp index 3b64be7a477d6..1c027f26af52c 100644 --- a/clang/lib/CodeGen/CodeGenModule.cpp +++ b/clang/lib/CodeGen/CodeGenModule.cpp @@ -557,6 +557,39 @@ CodeGenModule::CodeGenModule(ASTContext &C, CodeGenModule::~CodeGenModule() {} +SanitizerSet CodeGenModule::getProfileCoreUBChecks() const { + SanitizerSet Checks; + if (!LangOpts.Profiles || !Context.isProfileEnforced("std::core_ub")) + return Checks; + // The locally checkable cases of P4317 Appendix A.1, each reusing the UBSan + // check that already implements it. One case is added per commit. + Checks.set(SanitizerKind::IntegerDivideByZero, true); // {expr.mul.div.by.zero} + // {expr.mul.representable.type.result}: signed +, -, *, unary -, and the + // INT_MIN/-1 division whose quotient is not representable. + Checks.set(SanitizerKind::SignedIntegerOverflow, true); + // {expr.shift.neg.and.width}: a shift width that is negative or at least the + // operand width (exponent), or a signed left shift that loses bits (base). + Checks.set(SanitizerKind::ShiftBase, true); + Checks.set(SanitizerKind::ShiftExponent, true); + // {basic.align.object.alignment}: an access through a pointer that does not + // meet the referenced type's alignment. + Checks.set(SanitizerKind::Alignment, true); + // {expr.unary.dereference}, null case: dereferencing a null pointer. + Checks.set(SanitizerKind::Null, true); + // {expr.add.out.of.bounds}, statically known bound: indexing past the end of + // an array whose bound is visible at the subscript. + Checks.set(SanitizerKind::ArrayBounds, true); + // {conv.fpint.*} and {conv.double.out.of.range}: a floating-point value whose + // truncation toward zero is outside the destination type's range. + Checks.set(SanitizerKind::FloatCastOverflow, true); + // {expr.static.cast.enum.outside.range}: loading an enumeration value that is + // outside the range of its enumerators. + Checks.set(SanitizerKind::Enum, true); + // {stmt.return.flow.off}: flowing off the end of a value-returning function. + Checks.set(SanitizerKind::Return, true); + return Checks; +} + void CodeGenModule::createObjCRuntime() { // This is just isGNUFamily(), but we want to force implementors of // new ABIs to decide how best to do this. diff --git a/clang/lib/CodeGen/CodeGenModule.h b/clang/lib/CodeGen/CodeGenModule.h index 0081bf5c4cf5f..a94e188c8ef42 100644 --- a/clang/lib/CodeGen/CodeGenModule.h +++ b/clang/lib/CodeGen/CodeGenModule.h @@ -849,6 +849,14 @@ class CodeGenModule : public CodeGenTypeCache { ASTContext &getContext() const { return Context; } const LangOptions &getLangOpts() const { return LangOpts; } + + /// The UBSan checks the std::core_ub profile (P4317) guards when it is + /// enforced over this TU, to be emitted in trap mode; an empty set when the + /// profile is not enforced. Each CodeGenFunction ORs these into its SanOpts + /// and traps them. Queried per function rather than cached, because the + /// enforcement it reads is recorded only once the leading + /// [[profiles::enforce]] declaration is parsed, after this module is built. + SanitizerSet getProfileCoreUBChecks() const; const IntrusiveRefCntPtr &getFileSystem() const { return FS; } diff --git a/clang/lib/Driver/ToolChains/Clang.cpp b/clang/lib/Driver/ToolChains/Clang.cpp index 36ba3d35ed012..96cece1ca42a7 100644 --- a/clang/lib/Driver/ToolChains/Clang.cpp +++ b/clang/lib/Driver/ToolChains/Clang.cpp @@ -6409,6 +6409,12 @@ void Clang::ConstructJob(Compilation &C, const JobAction &JA, Args.addOptInFlag(CmdArgs, options::OPT_ffixed_point, options::OPT_fno_fixed_point); + Args.addOptInFlag(CmdArgs, options::OPT_fprofiles, + options::OPT_fno_profiles); + + Args.addOptOutFlag(CmdArgs, options::OPT_fprofiles_exempt_system_headers, + options::OPT_fno_profiles_exempt_system_headers); + Args.addOptInFlag(CmdArgs, options::OPT_fexperimental_overflow_behavior_types, options::OPT_fno_experimental_overflow_behavior_types); diff --git a/clang/lib/Parse/ParseCXXInlineMethods.cpp b/clang/lib/Parse/ParseCXXInlineMethods.cpp index bc18881e89110..d04e0e788fb7e 100644 --- a/clang/lib/Parse/ParseCXXInlineMethods.cpp +++ b/clang/lib/Parse/ParseCXXInlineMethods.cpp @@ -17,6 +17,7 @@ #include "clang/Sema/DeclSpec.h" #include "clang/Sema/EnterExpressionEvaluationContext.h" #include "clang/Sema/Scope.h" +#include "clang/Sema/SemaProfiles.h" #include "llvm/ADT/ScopeExit.h" using namespace clang; @@ -375,6 +376,9 @@ void Parser::ParseLexedMethodDeclaration(LateParsedMethodDeclaration &LM) { // Start the delayed C++ method declaration Actions.ActOnStartDelayedCXXMethodDeclaration(getCurScope(), LM.Method); + SemaProfiles::ProfileSuppressScope ProfileSuppressGuard( + Actions, LM.Method->getAsFunction(), /*WalkLexicalParents=*/true); + // Introduce the parameters into scope and parse their default // arguments. InFunctionTemplateScope.Scopes.Enter(Scope::FunctionPrototypeScope | @@ -603,6 +607,9 @@ void Parser::ParseLexedMethodDef(LexedMethod &LM) { Actions.ActOnStartOfFunctionDef(getCurScope(), LM.D); + SemaProfiles::ProfileSuppressScope ProfileSuppressGuard( + Actions, LM.D->getAsFunction(), /*WalkLexicalParents=*/true); + llvm::scope_exit _([&]() { while (Tok.isNot(tok::eof)) ConsumeAnyToken(); @@ -666,6 +673,15 @@ void Parser::ParseLexedMemberInitializer(LateParsedMemberInitializer &MI) { if (!MI.Field || MI.Field->isInvalidDecl()) return; + // The suppression dominion of a member declaration includes its initializer + // tokens (P3589R2 s2.4p3). ParseCXXMemberInitializer pushes its own scope + // around the parse, but the checks run from + // ActOnFinishCXXInClassMemberInitializer (e.g. the read-through check, at + // the lvalue-to-rvalue conversion the initialization sequence performs) + // fire after that scope is gone, so cover the whole late-parse here. + SemaProfiles::ProfileSuppressScope ProfileSuppressGuard( + Actions, MI.Field, /*WalkLexicalParents=*/true); + ParenBraceBracketBalancer BalancerRAIIObj(*this); // Append the current token at the end of the new token stream so that it diff --git a/clang/lib/Parse/ParseDecl.cpp b/clang/lib/Parse/ParseDecl.cpp index 72935f427b7f8..dd46a4de60e53 100644 --- a/clang/lib/Parse/ParseDecl.cpp +++ b/clang/lib/Parse/ParseDecl.cpp @@ -27,6 +27,7 @@ #include "clang/Sema/ParsedAttr.h" #include "clang/Sema/ParsedTemplate.h" #include "clang/Sema/Scope.h" +#include "clang/Sema/SemaProfiles.h" #include "clang/Sema/SemaCUDA.h" #include "clang/Sema/SemaCodeCompletion.h" #include "clang/Sema/SemaObjC.h" @@ -2135,6 +2136,9 @@ Parser::DeclGroupPtrTy Parser::ParseDeclGroup(ParsingDeclSpec &DS, // attributes higher up the callchain. ParsedAttributes LocalAttrs(AttrFactory); LocalAttrs.takeAllPrependingFrom(Attrs); + + SemaProfiles::ProfileSuppressScope ProfileSuppressGuard(Actions, LocalAttrs); + ParsingDeclarator D(*this, DS, LocalAttrs, Context); if (TemplateInfo.TemplateParams) D.setTemplateParameterLists(*TemplateInfo.TemplateParams); @@ -2581,6 +2585,9 @@ Decl *Parser::ParseDeclarationAfterDeclaratorAndAttributes( SemaCUDA::CUDATargetContextRAII X(Actions.CUDA(), SemaCUDA::CTCK_InitGlobalVar, ThisDecl); + + SemaProfiles::ProfileSuppressScope ProfileSuppressForInit(Actions, ThisDecl); + switch (TheInitKind) { // Parse declarator '=' initializer. case InitKind::Equal: { diff --git a/clang/lib/Parse/ParseDeclCXX.cpp b/clang/lib/Parse/ParseDeclCXX.cpp index 274c354d59808..43ccc0132f0d9 100644 --- a/clang/lib/Parse/ParseDeclCXX.cpp +++ b/clang/lib/Parse/ParseDeclCXX.cpp @@ -12,6 +12,7 @@ #include "clang/AST/ASTContext.h" #include "clang/AST/DeclTemplate.h" +#include "clang/AST/Expr.h" #include "clang/AST/PrettyDeclStackTrace.h" #include "clang/Basic/AttributeCommonInfo.h" #include "clang/Basic/Attributes.h" @@ -27,6 +28,7 @@ #include "clang/Sema/EnterExpressionEvaluationContext.h" #include "clang/Sema/ParsedTemplate.h" #include "clang/Sema/Scope.h" +#include "clang/Sema/SemaProfiles.h" #include "clang/Sema/SemaCodeCompletion.h" #include "clang/Sema/SemaHLSL.h" #include "llvm/Support/TimeProfiler.h" @@ -206,6 +208,8 @@ Parser::DeclGroupPtrTy Parser::ParseNamespace(DeclaratorContext Context, getCurScope(), InlineLoc, NamespaceLoc, IdentLoc, Ident, T.getOpenLocation(), attrs, ImplicitUsingDirectiveDecl, false); + SemaProfiles::ProfileSuppressScope ProfileSuppressGuard(Actions, NamespcDecl); + PrettyDeclStackTraceEntry CrashInfo(Actions.Context, NamespcDecl, NamespaceLoc, "parsing namespace"); @@ -255,6 +259,8 @@ void Parser::ParseInnerNamespace(const InnerNamespaceInfoList &InnerNSs, assert(!ImplicitUsingDirectiveDecl && "nested namespace definition cannot define anonymous namespace"); + SemaProfiles::ProfileSuppressScope ProfileSuppressGuard(Actions, NamespcDecl); + ParseInnerNamespace(InnerNSs, ++index, InlineLoc, attrs, Tracker); NamespaceScope.Exit(); @@ -3270,6 +3276,9 @@ ExprResult Parser::ParseCXXMemberInitializer(Decl *D, bool IsFunction, : Sema::ExpressionEvaluationContext::PotentiallyEvaluated, D); + SemaProfiles::ProfileSuppressScope ProfileSuppressGuard(Actions, D, + /*WalkLexicalParents=*/true); + // CWG2760 // Default member initializers used to initialize a base or member subobject // [...] are considered to be part of the function body @@ -3657,6 +3666,8 @@ void Parser::ParseCXXMemberSpecification(SourceLocation RecordLoc, IsFinalSpelledSealed, IsAbstract, T.getOpenLocation()); + SemaProfiles::ProfileSuppressScope ProfileSuppressGuard(Actions, TagDecl); + // C++ 11p3: Members of a class defined with the keyword class are private // by default. Members of a class defined with the keywords struct or union // are public by default. @@ -4659,8 +4670,15 @@ void Parser::ParseCXX11AttributeSpecifierInternal(ParsedAttributes &Attrs, } } + // P3589R2 profile attributes need custom parsing for both the + // well-formed and the missing argument clause forms. + if (ScopeName && ScopeName->isStr("profiles") && + TryParseProfilesAttribute(AttrName, AttrLoc, Attrs, EndLoc, ScopeName, + ScopeLoc)) + AttrParsed = true; + // Parse attribute arguments - if (Tok.is(tok::l_paren)) + if (!AttrParsed && Tok.is(tok::l_paren)) AttrParsed = ParseCXX11AttributeArgs(AttrName, AttrLoc, Attrs, EndLoc, ScopeName, ScopeLoc, OpenMPTokens); @@ -5024,3 +5042,366 @@ void Parser::ParseMicrosoftIfExistsClassDeclaration( Braces.consumeClose(); } + +//===----------------------------------------------------------------------===// +// C++ Profiles framework (P3589R2) +//===----------------------------------------------------------------------===// + +bool Parser::ParseProfileName(std::string &Name) { + if (!Tok.is(tok::identifier)) { + Diag(Tok, diag::err_profiles_expected_profile_name); + return true; + } + Name = Tok.getIdentifierInfo()->getName().str(); + ConsumeToken(); + + while (Tok.is(tok::coloncolon)) { + ConsumeToken(); + if (!Tok.is(tok::identifier)) { + Diag(Tok, diag::err_profiles_expected_profile_name); + return true; + } + Name += "::"; + Name += Tok.getIdentifierInfo()->getName(); + ConsumeToken(); + } + return false; +} + +template +static ArrayRef copyProfileArguments( + AttributePool &Pool, const ProfileArguments &Parsed) { + if (Parsed.empty()) + return {}; + + auto Args = Pool.allocateArray(Parsed.size()); + for (unsigned I = 0; I < Parsed.size(); ++I) { + Args[I].Key = Pool.copyString(Parsed[I].Key); + Args[I].Value = Pool.copyString(Parsed[I].Value); + Args[I].Kind = Parsed[I].Kind; + Args[I].Range = Parsed[I].Range; + } + return Args; +} + +bool Parser::ParseNonCommaBalancedToken(std::string &Spelling, + SourceRange *Range) { + SourceLocation StartLoc = Tok.getLocation(); + if (Tok.isOneOf(tok::l_paren, tok::l_square, tok::l_brace)) { + tok::TokenKind Close; + Spelling = PP.getSpelling(Tok); + switch (Tok.getKind()) { + case tok::l_paren: Close = tok::r_paren; ConsumeParen(); break; + case tok::l_square: Close = tok::r_square; ConsumeBracket(); break; + default: Close = tok::r_brace; ConsumeBrace(); break; + } + + CachedTokens Toks; + if (!ConsumeAndStoreUntil(Close, Toks, /*StopAtSemi=*/false, + /*ConsumeFinalToken=*/true)) { + Diag(Tok, diag::err_expected) << Close; + return true; + } + + for (const auto &T : Toks) { + Spelling += " "; + Spelling += PP.getSpelling(T); + } + if (Range) + *Range = SourceRange(StartLoc, Toks.back().getLocation()); + return false; + } + + if (Tok.isOneOf(tok::comma, tok::r_paren, tok::r_square, tok::r_brace, + tok::eof)) { + Diag(Tok, diag::err_profiles_invalid_argument_token); + return true; + } + + Spelling = PP.getSpelling(Tok); + if (Range) + *Range = SourceRange(StartLoc, Tok.getLocation()); + ConsumeAnyToken(); + return false; +} + +bool Parser::ParseNonOperatorNonPunctuatorToken(std::string &Spelling, + SourceRange *Range) { + // P3589R2 [dcl.attr.profiles]: A bare profile-argument is a + // non-operator-non-punctuator-token. + if (tok::getPunctuatorSpelling(Tok.getKind()) || Tok.is(tok::eof)) { + Diag(Tok, diag::err_profiles_invalid_argument_token); + return true; + } + SourceLocation Loc = Tok.getLocation(); + Spelling = PP.getSpelling(Tok); + if (Range) + *Range = SourceRange(Loc, Loc); + ConsumeAnyToken(); + return false; +} + +bool Parser::ParseProfileArgumentList( + SmallVectorImpl &Args) { + while (true) { + ParsedProfileDesignator::Argument Arg; + if (Tok.is(tok::identifier) && NextToken().is(tok::colon)) { + SourceLocation KeyLoc = Tok.getLocation(); + Arg.Key = Tok.getIdentifierInfo()->getName().str(); + Arg.Kind = profiles::ProfileArgumentKind::Named; + ConsumeToken(); + ConsumeToken(); + + SourceRange ValueRange; + if (ParseNonCommaBalancedToken(Arg.Value, &ValueRange)) + return true; + Arg.Range = SourceRange(KeyLoc, ValueRange.getEnd()); + } else { + if (ParseNonOperatorNonPunctuatorToken(Arg.Value, &Arg.Range)) + return true; + } + Args.push_back(std::move(Arg)); + + if (!TryConsumeToken(tok::comma)) + break; + } + return false; +} + +bool Parser::ParseProfileDesignator(ParsedProfileDesignator &D) { + if (ParseProfileName(D.Name)) + return true; + + D.Spelling = D.Name; + + if (!Tok.is(tok::l_paren)) + return false; + + D.Spelling += "("; + ConsumeParen(); + + if (ParseProfileArgumentList(D.Arguments)) + return true; + + for (unsigned I = 0; I < D.Arguments.size(); ++I) { + if (I > 0) + D.Spelling += ", "; + D.Spelling += profiles::getCanonicalProfileArgumentSpelling(D.Arguments[I]); + } + + if (!Tok.is(tok::r_paren)) { + Diag(Tok, diag::err_expected) << tok::r_paren; + return true; + } + D.Spelling += ")"; + ConsumeParen(); + return false; +} + +bool Parser::ParseProfileDesignatorList( + SmallVectorImpl &Designators) { + while (true) { + ParsedProfileDesignator D; + if (ParseProfileDesignator(D)) + return true; + Designators.push_back(std::move(D)); + + if (!TryConsumeToken(tok::comma)) + break; + } + return false; +} + +bool Parser::ParseProfileSuppressBody(ParsedProfileSuppressArgs &Args) { + if (ParseProfileName(Args.Name)) + return true; + + auto ParseStringLiteralValue = [&](std::string &Out) -> bool { + SmallVector StringToks; + do { + StringToks.push_back(Tok); + ConsumeStringToken(); + } while (tok::isStringLiteral(Tok.getKind())); + StringLiteralParser Literal(StringToks, PP); + if (Literal.hadError) + return true; + Out = Literal.GetString().str(); + return false; + }; + + while (TryConsumeToken(tok::comma)) { + if (!Tok.is(tok::identifier) || !NextToken().is(tok::colon)) { + ParsedProfileDesignator::Argument Arg; + if (ParseNonOperatorNonPunctuatorToken(Arg.Value, &Arg.Range)) + return true; + Args.RawArguments.push_back( + profiles::getCanonicalProfileArgumentSpelling(Arg)); + Args.Arguments.push_back(std::move(Arg)); + continue; + } + + IdentifierInfo *KeyII = Tok.getIdentifierInfo(); + SourceLocation KeyLoc = Tok.getLocation(); + ConsumeToken(); + ConsumeToken(); + + if (KeyII->isStr("justification")) { + if (!tok::isStringLiteral(Tok.getKind())) { + Diag(Tok, diag::err_profiles_suppress_justification_not_string); + return true; + } + if (ParseStringLiteralValue(Args.Justification)) + return true; + continue; + } + + if (KeyII->isStr("rule")) { + bool Failed = tok::isStringLiteral(Tok.getKind()) + ? ParseStringLiteralValue(Args.Rule) + : ParseNonCommaBalancedToken(Args.Rule); + if (Failed) + return true; + continue; + } + + std::string Value; + SourceRange ValueRange; + if (ParseNonCommaBalancedToken(Value, &ValueRange)) + return true; + ParsedProfileDesignator::Argument Arg; + Arg.Key = KeyII->getName().str(); + Arg.Value = std::move(Value); + Arg.Kind = profiles::ProfileArgumentKind::Named; + Arg.Range = SourceRange(KeyLoc, ValueRange.getEnd()); + Args.RawArguments.push_back( + profiles::getCanonicalProfileArgumentSpelling(Arg)); + Args.Arguments.push_back(std::move(Arg)); + } + + return false; +} + +bool Parser::TryParseProfilesAttribute(IdentifierInfo *AttrName, + SourceLocation AttrNameLoc, + ParsedAttributes &Attrs, + SourceLocation *EndLoc, + IdentifierInfo *ScopeName, + SourceLocation ScopeLoc) { + assert(ScopeName && ScopeName->isStr("profiles")); + + if (!AttrName->isStr("enforce") && !AttrName->isStr("suppress") && + !AttrName->isStr("require")) + return false; + + // Without -fprofiles the attributes are ignored (their LangOpts gate makes + // Sema emit warn_attribute_ignored), so behave like any ignored standard + // attribute: accept an arbitrary balanced-token argument clause -- or none + // -- without profile grammar checking. The attribute is still created, with + // default-constructed (empty) custom data, so the Sema warning path sees + // it; no consumer reads the custom data while the feature is off. + if (!getLangOpts().Profiles) { + SourceLocation End = AttrNameLoc; + if (Tok.is(tok::l_paren)) { + BalancedDelimiterTracker T(*this, tok::l_paren); + T.consumeOpen(); + T.skipToEnd(); + End = T.getCloseLocation(); + } + if (EndLoc) + *EndLoc = End; + + AttributePool &Pool = Attrs.getPool(); + void *CustomData; + if (AttrName->isStr("enforce")) + CustomData = Pool.make(); + else if (AttrName->isStr("suppress")) + CustomData = Pool.make(); + else + CustomData = Pool.make(); + + ParsedAttr *PA = + Attrs.addNew(AttrName, SourceRange(AttrNameLoc, End), + AttributeScopeInfo(ScopeName, ScopeLoc), nullptr, 0, + ParsedAttr::Form::CXX11()); + PA->setCustomData(CustomData); + return true; + } + + if (Tok.isNot(tok::l_paren)) { + Diag(AttrNameLoc, diag::err_profiles_expected_lparen) << AttrName; + return true; + } + + ConsumeParen(); + + AttributePool &Pool = Attrs.getPool(); + + auto SkipToRParen = [&]() { + SkipUntil(tok::r_paren, StopAtSemi | StopBeforeMatch); + if (Tok.is(tok::r_paren)) + ConsumeParen(); + return true; + }; + + void *CustomData = nullptr; + if (AttrName->isStr("enforce")) { + SmallVector Parsed; + if (ParseProfileDesignatorList(Parsed)) + return SkipToRParen(); + + auto Desigs = Pool.allocateArray(Parsed.size()); + for (unsigned I = 0; I < Parsed.size(); ++I) { + Desigs[I].Name = Pool.copyString(Parsed[I].Name); + Desigs[I].Spelling = Pool.copyString(Parsed[I].Spelling); + Desigs[I].Arguments = copyProfileArguments(Pool, Parsed[I].Arguments); + } + + auto *Args = Pool.make(); + Args->Designators = Desigs; + CustomData = Args; + } else if (AttrName->isStr("suppress")) { + ParsedProfileSuppressArgs Parsed; + if (ParseProfileSuppressBody(Parsed)) + return SkipToRParen(); + + auto *Args = Pool.make(); + Args->Name = Pool.copyString(Parsed.Name); + Args->Justification = Pool.copyString(Parsed.Justification); + Args->Rule = Pool.copyString(Parsed.Rule); + if (!Parsed.RawArguments.empty()) { + auto RawBuf = Pool.allocateArray(Parsed.RawArguments.size()); + for (unsigned I = 0; I < Parsed.RawArguments.size(); ++I) + RawBuf[I] = Pool.copyString(Parsed.RawArguments[I]); + Args->RawArguments = RawBuf; + } + Args->Arguments = copyProfileArguments(Pool, Parsed.Arguments); + CustomData = Args; + } else { + ParsedProfileDesignator Parsed; + if (ParseProfileDesignator(Parsed)) + return SkipToRParen(); + + auto *Args = Pool.make(); + Args->Designator.Name = Pool.copyString(Parsed.Name); + Args->Designator.Spelling = Pool.copyString(Parsed.Spelling); + Args->Designator.Arguments = + copyProfileArguments(Pool, Parsed.Arguments); + CustomData = Args; + } + + if (!Tok.is(tok::r_paren)) { + Diag(Tok, diag::err_profiles_expected_rparen) << AttrName; + return SkipToRParen(); + } + SourceLocation RParen = Tok.getLocation(); + ConsumeParen(); + if (EndLoc) + *EndLoc = RParen; + + ParsedAttr *PA = + Attrs.addNew(AttrName, SourceRange(AttrNameLoc, RParen), + AttributeScopeInfo(ScopeName, ScopeLoc), nullptr, 0, + ParsedAttr::Form::CXX11()); + PA->setCustomData(CustomData); + return true; +} diff --git a/clang/lib/Parse/ParseExprCXX.cpp b/clang/lib/Parse/ParseExprCXX.cpp index b3d50daf66b10..616c73f7afdf2 100644 --- a/clang/lib/Parse/ParseExprCXX.cpp +++ b/clang/lib/Parse/ParseExprCXX.cpp @@ -24,6 +24,7 @@ #include "clang/Sema/EnterExpressionEvaluationContext.h" #include "clang/Sema/ParsedTemplate.h" #include "clang/Sema/Scope.h" +#include "clang/Sema/SemaProfiles.h" #include "clang/Sema/SemaCodeCompletion.h" #include "llvm/Support/Compiler.h" #include "llvm/Support/ErrorHandling.h" @@ -1482,7 +1483,12 @@ ExprResult Parser::ParseLambdaExpressionAfterIntroducer( return ExprError(); } - StmtResult Stmt(ParseCompoundStatementBody()); + StmtResult Stmt; + { + SemaProfiles::ProfileSuppressScope ProfileSuppressGuard( + Actions, Actions.getCurLambda()->CallOperator); + Stmt = ParseCompoundStatementBody(); + } BodyScope.Exit(); TemplateParamScope.Exit(); LambdaScope.Exit(); diff --git a/clang/lib/Parse/ParseStmt.cpp b/clang/lib/Parse/ParseStmt.cpp index 1a45ed66950be..afe82bcfc2a19 100644 --- a/clang/lib/Parse/ParseStmt.cpp +++ b/clang/lib/Parse/ParseStmt.cpp @@ -22,6 +22,7 @@ #include "clang/Sema/DeclSpec.h" #include "clang/Sema/EnterExpressionEvaluationContext.h" #include "clang/Sema/Scope.h" +#include "clang/Sema/SemaProfiles.h" #include "clang/Sema/SemaCodeCompletion.h" #include "clang/Sema/SemaObjC.h" #include "clang/Sema/SemaOpenACC.h" @@ -75,6 +76,8 @@ StmtResult Parser::ParseStatementOrDeclaration(StmtVector &Stmts, if (getLangOpts().HLSL) MaybeParseMicrosoftAttributes(GNUOrMSAttrs); + SemaProfiles::ProfileSuppressScope ProfileSuppressGuard(Actions, CXX11Attrs); + StmtResult Res = ParseStatementOrDeclarationAfterAttributes( Stmts, StmtCtx, TrailingElseLoc, CXX11Attrs, GNUOrMSAttrs, PrecedingLabel); diff --git a/clang/lib/Parse/Parser.cpp b/clang/lib/Parse/Parser.cpp index 5d18414b1a746..f60bd980589e1 100644 --- a/clang/lib/Parse/Parser.cpp +++ b/clang/lib/Parse/Parser.cpp @@ -2307,6 +2307,28 @@ void Parser::ParseMicrosoftIfExistsExternalDeclaration() { Braces.consumeClose(); } +void Parser::ProhibitModuleAttributesExcept(const ParsedAttributesView &Attrs, + ParsedAttr::Kind AllowedKind, + unsigned KeywordDiagID, + unsigned AttrDiagID) { + for (const ParsedAttr &AL : Attrs) { + if (AL.getKind() == AllowedKind) + continue; + if (AL.isRegularKeywordAttribute()) { + Diag(AL.getLoc(), KeywordDiagID) << AL; + AL.setInvalid(); + } else if (AL.isStandardAttributeSyntax()) { + // An unknown attribute stays a warning, exactly as in + // ProhibitCXX11Attributes. + if (AL.getKind() == ParsedAttr::UnknownAttribute) + Actions.DiagnoseUnknownAttribute(AL); + else + Diag(AL.getLoc(), AttrDiagID) << AL; + AL.setInvalid(); + } + } +} + Parser::DeclGroupPtrTy Parser::ParseModuleDecl(Sema::ModuleImportState &ImportState) { Token Introducer = Tok; @@ -2378,13 +2400,14 @@ Parser::ParseModuleDecl(Sema::ModuleImportState &ImportState) { if (!Tok.isOneOf(tok::semi, tok::l_square)) SkipUntil(tok::semi, SkipUntilFlags::StopBeforeMatch); - // We don't support any module attributes yet; just parse them and diagnose. ParsedAttributes Attrs(AttrFactory); MaybeParseCXX11Attributes(Attrs); - ProhibitCXX11Attributes(Attrs, diag::err_attribute_not_module_attr, - diag::err_keyword_not_module_attr, - /*DiagnoseEmptyAttrs=*/false, - /*WarnOnUnknownAttrs=*/true); + + // Reject non-profile attributes on the module-declaration. Profile + // attributes are handled by ActOnModuleDecl. + ProhibitModuleAttributesExcept(Attrs, ParsedAttr::AT_ProfilesEnforce, + diag::err_keyword_not_module_attr, + diag::err_attribute_not_module_attr); if (ExpectAndConsumeSemi(diag::err_expected_semi_after_module_or_import, tok::getKeywordSpelling(tok::kw_module))) @@ -2392,7 +2415,8 @@ Parser::ParseModuleDecl(Sema::ModuleImportState &ImportState) { return Actions.ActOnModuleDecl(StartLoc, ModuleLoc, MDK, Path, Partition, ImportState, - Introducer.hasSeenNoTrivialPPDirective()); + Introducer.hasSeenNoTrivialPPDirective(), + Attrs); } Decl *Parser::ParseModuleImport(SourceLocation AtLoc, @@ -2438,11 +2462,12 @@ Decl *Parser::ParseModuleImport(SourceLocation AtLoc, ParsedAttributes Attrs(AttrFactory); MaybeParseCXX11Attributes(Attrs); - // We don't support any module import attributes yet. - ProhibitCXX11Attributes(Attrs, diag::err_attribute_not_import_attr, - diag::err_keyword_not_import_attr, - /*DiagnoseEmptyAttrs=*/false, - /*WarnOnUnknownAttrs=*/true); + + // Reject non-profile attributes on the import-declaration. Profile + // attributes are handled by ActOnModuleImportAttrs. + ProhibitModuleAttributesExcept(Attrs, ParsedAttr::AT_ProfilesRequire, + diag::err_keyword_not_import_attr, + diag::err_attribute_not_import_attr); if (PP.hadModuleLoaderFatalFailure()) { // With a fatal failure in the module loader, we abort parsing. @@ -2517,6 +2542,8 @@ Decl *Parser::ParseModuleImport(SourceLocation AtLoc, if (Import.isInvalid()) return nullptr; + Actions.ActOnModuleImportAttrs(Import.get(), Attrs); + // Using '@import' in framework headers requires modules to be enabled so that // the header is parseable. Emit a warning to make the user aware. if (IsObjCAtImport && AtLoc.isValid()) { diff --git a/clang/lib/Sema/AnalysisBasedWarnings.cpp b/clang/lib/Sema/AnalysisBasedWarnings.cpp index 7ed9b43b76d9d..98257a57d3229 100644 --- a/clang/lib/Sema/AnalysisBasedWarnings.cpp +++ b/clang/lib/Sema/AnalysisBasedWarnings.cpp @@ -48,6 +48,7 @@ #include "clang/Lex/Preprocessor.h" #include "clang/Sema/ScopeInfo.h" #include "clang/Sema/SemaInternal.h" +#include "clang/Sema/SemaProfiles.h" #include "llvm/ADT/ArrayRef.h" #include "llvm/ADT/BitVector.h" #include "llvm/ADT/DenseMap.h" @@ -1686,8 +1687,946 @@ struct SortDiagBySourceLocation { } // namespace clang namespace { +// Profiles that opt into Clang's CFG-uninitialized-variables analysis. Each +// entry pairs the profile name with the diagnostic to emit when an +// uninitialized read is found and not suppressed at the use site. Adding a +// new profile that wants to ride this analysis is a single row here plus a +// ProfileRuleError diagnostic in DiagnosticSemaKinds.td. +struct CFGUninitProfileEntry { + StringRef Name; + StringRef Rule; + unsigned DiagID; + // std::byte may be read while uninitialized (paper §4); the initialization + // profile exempts it, while the generic test profile does not. + bool ExemptStdByte; +}; +constexpr CFGUninitProfileEntry CFGUninitProfiles[] = { + {"test::uninit_read", /*Rule=*/"", diag::err_profile_uninit_read, + /*ExemptStdByte=*/false}, + {"std::init", "uninit_read", diag::err_init_uninit_read, + /*ExemptStdByte=*/true}, +}; + +// True if E denotes the current object: `this` (the implicit/explicit pointer +// of an arrow access) or `*this` (the object lvalue of a dot access). +static bool isCurrentObjectBase(const Expr *E) { + E = E->IgnoreParenImpCasts(); + if (isa(E)) + return true; + const auto *UO = dyn_cast(E); + return UO && UO->getOpcode() == UO_Deref && + isa(UO->getSubExpr()->IgnoreParenImpCasts()); +} + +// If E names a non-static data member of the current object (`this->m`, the +// implicit `m`, or the equivalent `(*this).m`), return that field; otherwise +// null. Access through any other object (e.g. `other.m`) is not the current +// object's member. +static const FieldDecl *getCurrentObjectMember(const Expr *E) { + const auto *ME = dyn_cast(E->IgnoreParenImpCasts()); + if (!ME || !isCurrentObjectBase(ME->getBase())) + return nullptr; + return dyn_cast(ME->getMemberDecl()); +} + +// A class with a user-provided constructor is trusted (paper §5.1): its +// constructor body may have assigned a member, which local analysis cannot +// see, so its members are not flow-tracked. +static bool hasUserProvidedCtor(const CXXRecordDecl *RD) { + return llvm::any_of(RD->ctors(), [](const CXXConstructorDecl *C) { + return C->isUserProvided(); + }); +} + +// Visit the candidate fields of RD and of its non-virtual, constructor-less +// base classes, recursively. +template +static void forEachCandidateUninitField(const CXXRecordDecl *RD, Fn Visit) { + SmallVector RecordStack(1, RD); + while (!RecordStack.empty()) { + const CXXRecordDecl *Cur = RecordStack.pop_back_val(); + for (const FieldDecl *F : Cur->fields()) + Visit(F); + for (const CXXBaseSpecifier &BS : Cur->bases()) { + if (BS.isVirtual()) + continue; + const CXXRecordDecl *BRD = BS.getType()->getAsCXXRecordDecl(); + if (BRD && BRD->hasDefinition() && + !hasUserProvidedCtor(BRD->getDefinition())) + RecordStack.push_back(BRD->getDefinition()); + } + } +} + +// Collect the flow-trackable members of RD into Members/Index: [[uninit]] +// built-in scalar (arithmetic or enum) members whose assignment counts as +// initialization (§4.5), including those inherited from non-virtual, +// constructor-less bases -- nothing can have assigned them before the +// containing object's user code runs, so tracking them is sound. std::byte is +// exempt (§4.5), matching R1/R2. Class-type and array members (which would +// need construct_at flow modeling) and pointers (banned with [[uninit]] by R8) +// are out of scope. +static void collectTrackedUninitMembers( + Sema &S, const CXXRecordDecl *RD, + SmallVectorImpl &Members, + llvm::DenseMap &Index) { + forEachCandidateUninitField(RD, [&](const FieldDecl *F) { + if (!F->hasAttr() || !F->getDeclName() || + F->hasInClassInitializer()) + return; + QualType T = F->getType(); + if (!T->isIntegralOrEnumerationType() && !T->isFloatingType()) + return; + if (S.Context.getBaseElementType(T)->isStdByteType()) + return; + if (Index.count(F)) + return; + Index[F] = Members.size(); + Members.push_back(F); + }); +} + +// std::init constructor-body check (paper §7.1 "initialized ... before use"). +// +// A [[uninit]] scalar data member is deliberately *not* required to be +// initialized by the constructor (paper §5.1 excepts members with an +// uninitialized indicator; §5.3 leaves them for users). What is required is +// that such a member is not *read* before it is given a value (§4.5: reading an +// uninitialized object, except std::byte, is erroneous). This is the member +// analog of the R1 rule for [[uninit]] locals. +// +// A forward definite-assignment dataflow over the constructor body: a scalar +// member is "assigned" by a plain `m = e` (for a built-in type a write is its +// initialization, §4.5) and a member is definitely assigned at a point only if +// assigned on every path reaching it (§1.3: all branches are considered +// executed). A value read of a member that is not definitely assigned there is +// the violation. There is no constructor-exit requirement: a member that is +// simply never read is left as-is, exactly as the structural ctor_uninit_member +// check (R5) excuses a marked member. +// +// Crediting is strict for plain escapes: nothing but a whole-member store +// (or a written member/base initializer) marks a member assigned. Taking the +// member's address, binding a reference to it, calling a member function, +// letting `this` escape, or passing &m to a [[ref_to_uninit]] parameter of +// an ordinary function earns no credit -- the paper rejects complex +// constructor code (§5.1) and reserves callee-initialization for now_init +// (§6.2), so such code is deliberately rejected here (the remedy is +// [[profiles::suppress]]). The one sanctioned exception is exactly §6.2's: a +// call to a [[now_init]] function credits the current-object storage bound +// to its [[ref_to_uninit]] parameters (see the CallExpr arm below), as a +// real Gen bit -- so §1.2's all-branches rule still governs a call under a +// branch. This is the deliberate counterpart of +// checkInitProfileLocalMembers' "soundness over completeness" escape +// crediting below: locals conservatively credit any escape (missed +// diagnostics only, subsuming [[now_init]] callees), while constructor +// bodies get the paper's strictness. +static void checkInitProfileCtorBody(Sema &S, const CXXConstructorDecl *Ctor, + AnalysisDeclContext &AC) { + // A delegating constructor leaves member initialization to its target (paper + // §5.1 trusts the constructor that runs first), so by the time the delegating + // body runs the members are already initialized; analyzing its body would + // falsely flag a read. This mirrors how ctor_uninit_member (R5) skips them. + if (Ctor->isDelegatingConstructor()) + return; + + CFG *cfg = AC.getCFG(); + if (!cfg) + return; + + // Target members: the shared flow-trackable filter (a base with a + // user-provided constructor is trusted per paper §5.1; nothing can have + // assigned a constructor-less base's members before this body runs). + SmallVector Members; + llvm::DenseMap Index; + collectTrackedUninitMembers(S, Ctor->getParent(), Members, Index); + if (Members.empty()) + return; + const unsigned N = Members.size(); + + // Statements the pass may see: the constructor body plus each *written* + // member/base initializer expression (the CFG is built with + // AddInitializers=true, so those run as CFG elements in execution order -- + // member-initializer reads such as `X() : o(m) {}` are checked exactly like + // body reads). A written initializer's own member becomes assigned at its + // CFGInitializer element below, so declaration order decides what an + // initializer may read. Not covered: an NSDMI's subexpressions + // (CXXDefaultInitExpr is not expanded into the CFG), so a read of a tracked + // member inside another member's default initializer stays undetected. + llvm::SmallPtrSet BodyStmts; + { + SmallVector Stack; + if (const Stmt *Body = Ctor->getBody()) + Stack.push_back(Body); + for (const CXXCtorInitializer *Init : Ctor->inits()) + if (Init->isWritten()) + Stack.push_back(Init->getInit()); + while (!Stack.empty()) { + const Stmt *Cur = Stack.pop_back_val(); + if (!Cur || !BodyStmts.insert(Cur).second) + continue; + for (const Stmt *Child : Cur->children()) + Stack.push_back(Child); + } + } + + // Per-block ordered events recovered from the linearized CFG: a member load + // (an lvalue-to-rvalue conversion of `this->m`) is a Read; `m = e` is a Write + // that marks m assigned after the RHS is evaluated; a compound assignment + // `m op= e` both reads and then writes m. + enum EventKind { Read, Write, ReadWrite }; + struct Event { + EventKind Kind; + unsigned Idx; + const Expr *E; + }; + const unsigned NumBlocks = cfg->getNumBlockIDs(); + std::vector> Events(NumBlocks); + std::vector Gen(NumBlocks, llvm::BitVector(N, false)); + for (const CFGBlock *B : *cfg) { + auto &BlockEvents = Events[B->getBlockID()]; + for (const CFGElement &Elem : *B) { + // A written member initializer assigns its member at this point in + // execution order, after its init expression's events above it. (The + // former entry-state seeding could not order the initializers' own + // reads against these writes.) + if (auto OptInit = Elem.getAs()) { + const CXXCtorInitializer *CI = OptInit->getInitializer(); + if (!CI->isWritten()) + continue; + if (CI->isAnyMemberInitializer()) { + const FieldDecl *F = CI->getAnyMember(); + if (!F) + continue; + auto It = Index.find(F); + if (It == Index.end()) + continue; + BlockEvents.push_back({Write, It->second, CI->getInit()}); + Gen[B->getBlockID()].set(It->second); + } else if (CI->isBaseInitializer()) { + // A written base initializer (e.g. `: Base{1}`) gives the tracked + // members of that constructor-less base subtree their values. + const auto *BRD = CI->getBaseClass()->getAsCXXRecordDecl(); + if (!BRD || !BRD->hasDefinition()) + continue; + forEachCandidateUninitField( + BRD->getDefinition(), [&](const FieldDecl *F) { + auto It = Index.find(F); + if (It == Index.end()) + return; + BlockEvents.push_back({Write, It->second, CI->getInit()}); + Gen[B->getBlockID()].set(It->second); + }); + } + continue; + } + auto OptStmt = Elem.getAs(); + if (!OptStmt) + continue; + const Stmt *St = OptStmt->getStmt(); + if (!BodyStmts.count(St)) + continue; + if (const auto *ICE = dyn_cast(St)) { + if (ICE->getCastKind() != CK_LValueToRValue) + continue; + const FieldDecl *F = getCurrentObjectMember(ICE->getSubExpr()); + if (!F) + continue; + auto It = Index.find(F); + if (It != Index.end()) + BlockEvents.push_back({Read, It->second, ICE}); + } else if (const auto *BO = dyn_cast(St)) { + if (!BO->isAssignmentOp()) + continue; + const FieldDecl *F = getCurrentObjectMember(BO->getLHS()); + if (!F) + continue; + auto It = Index.find(F); + if (It == Index.end()) + continue; + BlockEvents.push_back( + {BO->isCompoundAssignmentOp() ? ReadWrite : Write, It->second, BO}); + Gen[B->getBlockID()].set(It->second); + } else if (const auto *UO = dyn_cast(St)) { + // A built-in ++m / m++ / --m / m-- reads the old value and then writes, + // but unlike -m / !m it carries no lvalue-to-rvalue cast, so the Read + // arm above never sees it. Model it like the compound-assignment case: + // a ReadWrite that also marks the member assigned. + if (!UO->isIncrementDecrementOp()) + continue; + const FieldDecl *F = getCurrentObjectMember(UO->getSubExpr()); + if (!F) + continue; + auto It = Index.find(F); + if (It == Index.end()) + continue; + BlockEvents.push_back({ReadWrite, It->second, UO}); + Gen[B->getBlockID()].set(It->second); + } else if (const auto *CE = dyn_cast(St)) { + // A call to a [[now_init]] function initializes the storage bound to + // each of its [[ref_to_uninit]] parameters (P4222R2 §6.2) -- the + // paper's sanctioned exception to the strict assignment-only + // crediting above. A current-object member passed as `&m` / `m` + // becomes assigned at the call element (its argument-subexpression + // events, e.g. a read of another member, precede it in the block); + // passing `this` / `*this` itself to a marked parameter hands the + // callee the whole object to initialize, so every tracked member is + // assigned. This is a real Gen bit in the dataflow, not parse-order + // credit: a [[now_init]] call under a branch still does not satisfy + // a read at the join (§1.2's all-branches rule). A plain (non- + // [[now_init]]) callee continues to earn nothing. + const FunctionDecl *Callee = CE->getDirectCallee(); + if (!Callee || !Callee->hasAttr()) + continue; + // Zip declared parameters with arguments. A member operator called + // through CXXOperatorCallExpr receives the object as argument 0 + // ahead of its declared parameters -- for a C++23 static operator + // too, whose object argument is still evaluated -- so skip it. An + // explicit-object member function instead declares its object as + // parameter 0, so its mapping is already direct. + unsigned ArgOffset = 0; + if (isa(CE)) + if (const auto *MD = dyn_cast(Callee); + MD && !MD->isExplicitObjectMemberFunction()) + ArgOffset = 1; + for (unsigned PI = 0, NP = Callee->getNumParams(); PI != NP; ++PI) { + if (PI + ArgOffset >= CE->getNumArgs()) + break; + if (!Callee->getParamDecl(PI)->hasAttr()) + continue; + const Expr *Arg = CE->getArg(PI + ArgOffset)->IgnoreParenImpCasts(); + // Peel explicit pointer/reference casts, mirroring the parse-time + // recognizers (§4.3: a cast marked pointer is itself marked). + while (const auto *Cast = dyn_cast(Arg)) { + const Expr *Sub = Cast->getSubExpr(); + if (!Sub->getType()->isPointerType() && !Sub->isGLValue()) + break; + Arg = Sub->IgnoreParenImpCasts(); + } + const Expr *G = Arg; + if (const auto *AddrOf = dyn_cast(Arg); + AddrOf && AddrOf->getOpcode() == UO_AddrOf) + G = AddrOf->getSubExpr(); + if (const FieldDecl *F = getCurrentObjectMember(G)) { + auto It = Index.find(F); + if (It == Index.end()) + continue; + BlockEvents.push_back({Write, It->second, CE}); + Gen[B->getBlockID()].set(It->second); + } else if (isCurrentObjectBase(Arg)) { + for (unsigned Idx = 0; Idx != N; ++Idx) + BlockEvents.push_back({Write, Idx, CE}); + Gen[B->getBlockID()].set(); + } + } + } else if (const auto *LE = dyn_cast(St)) { + // The lambda body is a separate function and never appears in this + // CFG, but a this-capturing lambda can read members the moment it is + // created (it may be invoked immediately). Treat every member read in + // its body -- and in nested lambda bodies, reached through children() + // -- as a Read at the LambdaExpr's program point. Writes in the body + // earn no assignment credit (the lambda may never run), consistent + // with the intersection semantics; a lambda stored now and called + // only after the member is assigned is still flagged (accepted + // imprecision). Capture initializers are ordinary CFG elements, + // already handled by the arms above. + if (llvm::none_of(LE->captures(), [](const LambdaCapture &C) { + return C.capturesThis(); + })) + continue; + SmallVector Stack(1, LE->getBody()); + while (!Stack.empty()) { + const Stmt *Cur = Stack.pop_back_val(); + if (!Cur) + continue; + const FieldDecl *F = nullptr; + if (const auto *BodyICE = dyn_cast(Cur); + BodyICE && BodyICE->getCastKind() == CK_LValueToRValue) + F = getCurrentObjectMember(BodyICE->getSubExpr()); + else if (const auto *BodyBO = dyn_cast(Cur); + BodyBO && BodyBO->isCompoundAssignmentOp()) + F = getCurrentObjectMember(BodyBO->getLHS()); + else if (const auto *BodyUO = dyn_cast(Cur); + BodyUO && BodyUO->isIncrementDecrementOp()) + F = getCurrentObjectMember(BodyUO->getSubExpr()); + if (F) { + auto It = Index.find(F); + if (It != Index.end()) + BlockEvents.push_back({Read, It->second, cast(Cur)}); + } + for (const Stmt *Child : Cur->children()) + Stack.push_back(Child); + } + } + } + } + + // Forward "definitely assigned" dataflow: nothing is assigned at function + // entry (written initializers generate their writes at their CFGInitializer + // elements); a block's entry is the intersection over its predecessors' + // exits (a member is definitely assigned only if assigned on every incoming + // path); a block's exit adds the members it assigns. Unprocessed + // (unreachable) predecessors keep the all-assigned top, so unreachable code + // is never flagged. + std::vector EntryState(NumBlocks, llvm::BitVector(N, true)); + std::vector ExitState(NumBlocks, llvm::BitVector(N, true)); + const CFGBlock &CFGEntry = cfg->getEntry(); + ForwardDataflowWorklist Worklist(*cfg, AC); + Worklist.enqueueBlock(&CFGEntry); + while (const CFGBlock *B = Worklist.dequeue()) { + llvm::BitVector In(N, true); + if (B == &CFGEntry) { + In = llvm::BitVector(N, false); + } else { + bool First = true; + for (const CFGBlock *Pred : B->preds()) { + if (!Pred) + continue; + if (First) { + In = ExitState[Pred->getBlockID()]; + First = false; + } else { + In &= ExitState[Pred->getBlockID()]; + } + } + } + EntryState[B->getBlockID()] = In; + In |= Gen[B->getBlockID()]; + if (In != ExitState[B->getBlockID()]) { + ExitState[B->getBlockID()] = In; + Worklist.enqueueSuccessors(B); + } + } + + // Replay each block from its fixpoint entry state and collect reads of a + // member that is not yet definitely assigned at that point. + std::vector> Offending(N); + for (const CFGBlock *B : *cfg) { + llvm::BitVector Assigned = EntryState[B->getBlockID()]; + for (const Event &Ev : Events[B->getBlockID()]) { + switch (Ev.Kind) { + case Read: + if (!Assigned.test(Ev.Idx)) + Offending[Ev.Idx].push_back(Ev.E); + break; + case Write: + Assigned.set(Ev.Idx); + break; + case ReadWrite: + if (!Assigned.test(Ev.Idx)) + Offending[Ev.Idx].push_back(Ev.E); + Assigned.set(Ev.Idx); + break; + } + } + } + + // Report at the first offending read (in source order) that is not + // suppressed, once per member, mirroring the local-variable reporter. + for (unsigned I = 0; I != N; ++I) { + if (Offending[I].empty()) + continue; + llvm::sort(Offending[I], [&](const Expr *A, const Expr *Bx) { + return S.SourceMgr.isBeforeInTranslationUnit(A->getBeginLoc(), + Bx->getBeginLoc()); + }); + for (const Expr *R : Offending[I]) { + if (!S.Profiles().shouldEmitProfileViolation("std::init", "uninit_read", + R, AC)) + continue; + S.Diag(R->getBeginLoc(), diag::err_init_member_read_before_init) + << "std::init" << Members[I]->getDeclName(); + S.Diag(Members[I]->getLocation(), diag::note_init_uninit_member_here) + << Members[I]->getDeclName(); + break; + } + } +} + +// If E (stripped of parens and implicit casts, including the derived-to-base +// cast of an inherited-member access) is a member access `V.m` on a directly +// named variable, return the base DeclRefExpr and the field through \p F. +// An arrow access or an access through any other expression is not a tracked +// local's member. +static const DeclRefExpr *getLocalMemberAccess(const Expr *E, + const FieldDecl *&F) { + const auto *ME = dyn_cast(E->IgnoreParenImpCasts()); + if (!ME || ME->isArrow()) + return nullptr; + const auto *FD = dyn_cast(ME->getMemberDecl()); + if (!FD) + return nullptr; + const auto *DRE = dyn_cast(ME->getBase()->IgnoreParenImpCasts()); + if (!DRE) + return nullptr; + F = FD; + return DRE; +} + +// The type-shape half of the tracked guards: a non-union, non-dependent +// class with no user-provided constructor anywhere in the contributing +// subtree (paper §5.1 trusts one: its body may assign, which local analysis +// cannot see). A reference type aliases an object also reachable other ways +// and never qualifies. +static const CXXRecordDecl *getTrackableSlotClass(QualType T) { + if (T->isReferenceType() || T->isDependentType()) + return nullptr; + const CXXRecordDecl *RD = T->getAsCXXRecordDecl(); + if (!RD || !RD->hasDefinition()) + return nullptr; + RD = RD->getDefinition(); + if (RD->isUnion() || RD->isDependentType() || hasUserProvidedCtor(RD)) + return nullptr; + return RD; +} + +// The declaration-shape half for a local: V is a non-parameter local, not +// itself [[uninit]]-marked (its subobject accesses are the parse-time +// read-through / uninit_write rules' territory, and tracking it here would +// double-diagnose), of a trackable class. How V's declaration *initializes* +// the members is the callers' half. +static const CXXRecordDecl *getTrackableLocalClass(const VarDecl *V) { + if (!V->hasLocalStorage() || isa(V) || isa(V)) + return nullptr; + if (V->isInvalidDecl() || V->hasAttr()) + return nullptr; + return getTrackableSlotClass(V->getType()); +} + +// If V is a local whose [[uninit]] members this pass may soundly flow-track +// from an all-unassigned start, return its class definition; null otherwise. +// Sound means nothing can have assigned the members before V's declaration: +// the class shape qualifies (above) and the declaration ran nothing but the +// implicit no-op default-construction. +static const CXXRecordDecl *getTrackedLocalAggregate(const VarDecl *V) { + const CXXRecordDecl *RD = getTrackableLocalClass(V); + if (!RD) + return nullptr; + // Declared without a real initializer: for a record local that is the + // synthesized call to the implicit default constructor (`Agg a;`). A + // value-initializing form -- `Agg a{}` / `= {}` (an InitListExpr), + // `Agg a = Agg()` (a CXXTemporaryObjectExpr) -- gives every member a + // value and leaves nothing to track. A *copy* does not: it copies + // indeterminate bits, and a copy does not inherit initialization (paper + // §5.2). A copy from a *tracked* local inherits the source's per-member + // state through the copy harvest in checkInitProfileLocalMembers; for an + // arbitrary untracked source that state is unknowable, so those copies + // stay untracked -- a missed diagnostic, never a false positive. + if (const Expr *Init = V->getInit()) { + const auto *CCE = dyn_cast(Init->IgnoreImplicit()); + if (!CCE || isa(CCE) || + !CCE->getConstructor()->isDefaultConstructor() || + CCE->isListInitialization() || CCE->getParenOrBraceRange().isValid()) + return nullptr; + } + return RD; +} + +// If V is copy- or move-constructed from a directly named variable, return +// that source's DeclRefExpr; null otherwise. The explicit-cast peel resolves +// the move form (`Agg b = static_cast(a);`) to its named operand, +// mirroring the parse-time recognizers' pass-through. +static const DeclRefExpr *getLocalCopySourceRef(const VarDecl *V) { + const Expr *Init = V->getInit(); + if (!Init) + return nullptr; + const auto *CCE = dyn_cast(Init->IgnoreImplicit()); + if (!CCE || CCE->getNumArgs() < 1 || + !CCE->getConstructor()->isCopyOrMoveConstructor()) + return nullptr; + const Expr *Arg = CCE->getArg(0)->IgnoreParenImpCasts(); + while (const auto *CE = dyn_cast(Arg)) { + if (!CE->getSubExpr()->isGLValue()) + break; + Arg = CE->getSubExpr()->IgnoreParenImpCasts(); + } + return dyn_cast(Arg); +} + +// std::init local-aggregate member check (paper §7.1 "initialized ... before +// use", the local-variable analog of the ctor-body pass above). +// +// An [[uninit]] scalar member of a constructor-less aggregate local is given +// a value by a plain member store (`a.m = e`; for a built-in type a write is +// its initialization, §4.5) -- the §5.3 "class exposing uninitialized +// members" pattern. A read of such a member before it is definitely assigned +// (on every path, §1.3) is the violation. This is the flow tracking the +// parse-time read-through rule's top-level drop relies on for locals: the +// drop trusts a direct member read so the legal write-then-read sequence is +// not rejected, and this pass supplies the missing read-before-write +// diagnosis. +// +// Soundness over completeness: any appearance of the variable outside a +// recognized member read or write -- &a, &a.m, a reference binding, passing a +// to any function (construct_at, memcpy), a member call, a lambda capture -- +// conservatively marks every member assigned from that point (the address may +// be used to initialize the object). This subsumes [[now_init]] callees +// (§6.2): passing &a.m to one is an escape like any other, so no dedicated +// call arm is needed here, unlike the strict ctor-body pass above. Members of +// an object with a user-provided constructor stay untracked (trusted, §5.1), as +// do objects reached through parameters, references, or other objects. A +// backward goto across the declaration re-default-initializes the object, which +// the gen-only dataflow cannot model -- a possible missed diagnostic, matching +// the ctor-body pass's accepted imprecision level. +static void checkInitProfileLocalMembers(Sema &S, AnalysisDeclContext &AC) { + CFG *cfg = AC.getCFG(); + if (!cfg) + return; + + // Harvest tracked (local, member) pairs from the CFG's (single-decl) + // DeclStmt elements; each variable's pairs are contiguous so an escape can + // set a range. + SmallVector PairField; + llvm::DenseMap FieldIdxScratch; + llvm::DenseMap> VarRange; + llvm::DenseMap, unsigned> + PairIdx; + auto HarvestVar = [&](const VarDecl *V, const CXXRecordDecl *RD) { + SmallVector Members; + FieldIdxScratch.clear(); + collectTrackedUninitMembers(S, RD, Members, FieldIdxScratch); + if (Members.empty()) + return false; + unsigned Begin = PairField.size(); + for (const FieldDecl *F : Members) { + PairIdx[{V, F}] = PairField.size(); + PairField.push_back(F); + } + VarRange[V] = {Begin, PairField.size()}; + return true; + }; + + // A by-value slot parameter is tracked from an all-unassigned start + // (which is exactly the dataflow's entry state): the parameter is a + // *copy* of the caller's argument, and a copy does not inherit + // initialization (paper §5.2) -- §4.4's Slot contract makes a marked + // member uninitialized until locally proven otherwise ("you can't ask a + // slot if it is initialized"), and the paper's way to hand + // uninitialized-capable storage across a call is a marked pointer or + // reference (§4.3), not a by-value slot. This is the call-boundary twin + // of the ctor-body pass's deliberate strictness: a caller that assigned + // the member before the call is rejected here all the same, with escape + // crediting (any bare use of the parameter) and [[profiles::suppress]] + // as the remedies. Reference parameters alias the caller's own object + // and stay untracked. + if (const auto *EnclosingFD = dyn_cast_or_null(AC.getDecl())) + for (const ParmVarDecl *P : EnclosingFD->parameters()) { + if (P->isInvalidDecl() || P->hasAttr()) + continue; + if (const CXXRecordDecl *RD = getTrackableSlotClass(P->getType())) + HarvestVar(P, RD); + } + + for (const CFGBlock *B : *cfg) { + for (const CFGElement &Elem : *B) { + auto CS = Elem.getAs(); + if (!CS) + continue; + const auto *DS = dyn_cast(CS->getStmt()); + if (!DS) + continue; + for (const Decl *Dcl : DS->decls()) { + const auto *V = dyn_cast(Dcl); + if (!V || VarRange.count(V)) + continue; + const CXXRecordDecl *RD = getTrackedLocalAggregate(V); + if (!RD) + continue; + HarvestVar(V, RD); + } + } + } + + // Second harvest: locals copy- or move-constructed from a *tracked* local. + // The copy's members inherit the source's per-member state at the copy + // point -- a copy does not inherit initialization (paper §5.2), it + // inherits whatever state the source has -- modeled by a per-member Copy + // transfer event at the DeclStmt. Keyed per source *field* (not position: + // a copy sliced from a tracked derived object shares its base's + // FieldDecls). Iterated to a fixpoint so a copy of a copy resolves + // regardless of CFG block order. + llvm::DenseMap CopySource; + for (bool Added = true; Added;) { + Added = false; + for (const CFGBlock *B : *cfg) { + for (const CFGElement &Elem : *B) { + auto CS = Elem.getAs(); + if (!CS) + continue; + const auto *DS = dyn_cast(CS->getStmt()); + if (!DS) + continue; + for (const Decl *Dcl : DS->decls()) { + const auto *V = dyn_cast(Dcl); + if (!V || VarRange.count(V)) + continue; + const CXXRecordDecl *RD = getTrackableLocalClass(V); + if (!RD) + continue; + const DeclRefExpr *SrcRef = getLocalCopySourceRef(V); + const auto *Src = + SrcRef ? dyn_cast(SrcRef->getDecl()) : nullptr; + if (!Src || !VarRange.count(Src)) + continue; + if (!HarvestVar(V, RD)) + continue; + CopySource[V] = Src; + Added = true; + } + } + } + } + + if (PairField.empty()) + return; + const unsigned N = PairField.size(); + + // A `V.m` access on a tracked local, resolved to its pair index and its + // base DeclRefExpr; Idx is ~0u (and Base null) when E is not one. + struct TrackedAccess { + unsigned Idx = ~0u; + const DeclRefExpr *Base = nullptr; + }; + auto LookupPair = [&](const Expr *E) -> TrackedAccess { + const FieldDecl *F = nullptr; + const DeclRefExpr *DRE = getLocalMemberAccess(E, F); + if (!DRE) + return {}; + const auto *V = dyn_cast(DRE->getDecl()); + if (!V) + return {}; + auto It = PairIdx.find({V, F}); + if (It == PairIdx.end()) + return {}; + return {It->second, DRE}; + }; + + // First pass: the base DeclRefExprs consumed by a recognized member read or + // write are benign -- they do not escape the object. A DeclRefExpr element + // precedes its consuming cast/operator element in a block, so the benign + // set must be complete before elements are classified. + llvm::SmallPtrSet Benign; + for (const CFGBlock *B : *cfg) { + for (const CFGElement &Elem : *B) { + auto CS = Elem.getAs(); + if (!CS) + continue; + const Stmt *St = CS->getStmt(); + const DeclRefExpr *Base = nullptr; + if (const auto *ICE = dyn_cast(St)) { + if (ICE->getCastKind() == CK_LValueToRValue) + Base = LookupPair(ICE->getSubExpr()).Base; + } else if (const auto *BO = dyn_cast(St)) { + if (BO->isAssignmentOp()) + Base = LookupPair(BO->getLHS()).Base; + } else if (const auto *UO = dyn_cast(St)) { + if (UO->isIncrementDecrementOp()) + Base = LookupPair(UO->getSubExpr()).Base; + } else if (const auto *DS = dyn_cast(St)) { + // The source ref of a tracked copy is consumed by the copy's + // implicit (retention-free) constructor and modeled by the Copy + // event, so it is not an escape -- an escape here would wrongly + // mark the *source* fully assigned. + for (const Decl *Dcl : DS->decls()) + if (const auto *V = dyn_cast(Dcl)) + if (CopySource.count(V)) + Base = getLocalCopySourceRef(V); + } + if (Base) + Benign.insert(Base); + } + } + + // Second pass: per-block ordered events. A load of `V.m` is a Read; a + // member store is a Write (a compound assignment or ++/-- reads the old + // value first); a tracked copy's DeclStmt is a per-member Copy, whose + // dest-member state becomes the source member's at that point; any + // non-benign DeclRefExpr naming a tracked local is an escape, modeled as + // a Write of every one of its tracked members. + enum EventKind { Read, Write, ReadWrite, Copy }; + struct Event { + EventKind Kind; + unsigned Idx; + const Expr *E; + unsigned SrcIdx = 0; // Copy only: the source (V, F) pair. + }; + const unsigned NumBlocks = cfg->getNumBlockIDs(); + std::vector> Events(NumBlocks); + for (const CFGBlock *B : *cfg) { + auto &BlockEvents = Events[B->getBlockID()]; + for (const CFGElement &Elem : *B) { + auto CS = Elem.getAs(); + if (!CS) + continue; + const Stmt *St = CS->getStmt(); + if (const auto *ICE = dyn_cast(St)) { + if (ICE->getCastKind() != CK_LValueToRValue) + continue; + unsigned Idx = LookupPair(ICE->getSubExpr()).Idx; + if (Idx != ~0u) + BlockEvents.push_back({Read, Idx, ICE}); + } else if (const auto *BO = dyn_cast(St)) { + if (!BO->isAssignmentOp()) + continue; + unsigned Idx = LookupPair(BO->getLHS()).Idx; + if (Idx == ~0u) + continue; + BlockEvents.push_back( + {BO->isCompoundAssignmentOp() ? ReadWrite : Write, Idx, BO}); + } else if (const auto *UO = dyn_cast(St)) { + if (!UO->isIncrementDecrementOp()) + continue; + unsigned Idx = LookupPair(UO->getSubExpr()).Idx; + if (Idx == ~0u) + continue; + BlockEvents.push_back({ReadWrite, Idx, UO}); + } else if (const auto *DRE = dyn_cast(St)) { + if (Benign.count(DRE)) + continue; + const auto *V = dyn_cast(DRE->getDecl()); + if (!V) + continue; + auto It = VarRange.find(V); + if (It == VarRange.end()) + continue; + for (unsigned Idx = It->second.first; Idx != It->second.second; ++Idx) + BlockEvents.push_back({Write, Idx, DRE}); + } else if (const auto *DS = dyn_cast(St)) { + // A tracked copy: each dest member's state becomes its source + // member's, keyed per FieldDecl (a sliced copy shares the base's + // FieldDecls with a differently laid out source range). The + // DeclStmt element follows its initializer's subexpression + // elements, so the transfer sees the source's state at the copy + // point. A source field the source range does not track cannot + // occur (the dest's fields are a subset of the source's); fall + // back to Write (assume assigned) if it somehow does. + for (const Decl *Dcl : DS->decls()) { + const auto *V = dyn_cast(Dcl); + if (!V) + continue; + auto CopyIt = CopySource.find(V); + if (CopyIt == CopySource.end()) + continue; + auto Range = VarRange.find(V)->second; + for (unsigned Idx = Range.first; Idx != Range.second; ++Idx) { + auto SrcIt = PairIdx.find({CopyIt->second, PairField[Idx]}); + if (SrcIt != PairIdx.end()) + BlockEvents.push_back({Copy, Idx, V->getInit(), SrcIt->second}); + else + BlockEvents.push_back({Write, Idx, V->getInit()}); + } + } + } + } + } + + // Replay a block's events over State: the block-level transfer function, + // shared by the fixpoint below and the reporting replay after it so the + // two always agree. When Offending is non-null, a read of a member not + // definitely assigned at its program point is collected. Copy projects + // the source bit onto the dest bit -- monotone like the set-only kinds, + // so the fixpoint below still terminates. + auto ApplyEvents = + [](ArrayRef BlockEvents, llvm::BitVector &State, + std::vector> *Offending) { + for (const Event &Ev : BlockEvents) { + switch (Ev.Kind) { + case Read: + if (Offending && !State.test(Ev.Idx)) + (*Offending)[Ev.Idx].push_back(Ev.E); + break; + case Write: + State.set(Ev.Idx); + break; + case ReadWrite: + if (Offending && !State.test(Ev.Idx)) + (*Offending)[Ev.Idx].push_back(Ev.E); + State.set(Ev.Idx); + break; + case Copy: + if (State.test(Ev.SrcIdx)) + State.set(Ev.Idx); + else + State.reset(Ev.Idx); + break; + } + } + }; + + // Forward "definitely assigned" dataflow, identical in shape to the + // ctor-body pass's: nothing is assigned at function entry (a tracked local + // cannot be referenced before its DeclStmt anyway); a block's entry is the + // intersection over its predecessors' exits; unprocessed (unreachable) + // predecessors keep the all-assigned top, so unreachable code is never + // flagged. The transfer function is the event replay above (monotone: + // events only set bits), so the worklist still reaches a fixpoint. + std::vector EntryState(NumBlocks, llvm::BitVector(N, true)); + std::vector ExitState(NumBlocks, llvm::BitVector(N, true)); + const CFGBlock &CFGEntry = cfg->getEntry(); + ForwardDataflowWorklist Worklist(*cfg, AC); + Worklist.enqueueBlock(&CFGEntry); + while (const CFGBlock *B = Worklist.dequeue()) { + llvm::BitVector In(N, true); + if (B == &CFGEntry) { + In = llvm::BitVector(N, false); + } else { + bool First = true; + for (const CFGBlock *Pred : B->preds()) { + if (!Pred) + continue; + if (First) { + In = ExitState[Pred->getBlockID()]; + First = false; + } else { + In &= ExitState[Pred->getBlockID()]; + } + } + } + EntryState[B->getBlockID()] = In; + ApplyEvents(Events[B->getBlockID()], In, /*Offending=*/nullptr); + if (In != ExitState[B->getBlockID()]) { + ExitState[B->getBlockID()] = In; + Worklist.enqueueSuccessors(B); + } + } + + // Replay each block from its fixpoint entry state and collect reads of a + // member that is not yet definitely assigned at that point. + std::vector> Offending(N); + for (const CFGBlock *B : *cfg) { + llvm::BitVector Assigned = EntryState[B->getBlockID()]; + ApplyEvents(Events[B->getBlockID()], Assigned, &Offending); + } + + // Report at the first offending read (in source order) that is not + // suppressed, once per (local, member) pair, mirroring the ctor-body pass. + for (unsigned I = 0; I != N; ++I) { + if (Offending[I].empty()) + continue; + llvm::sort(Offending[I], [&](const Expr *A, const Expr *Bx) { + return S.SourceMgr.isBeforeInTranslationUnit(A->getBeginLoc(), + Bx->getBeginLoc()); + }); + for (const Expr *R : Offending[I]) { + if (!S.Profiles().shouldEmitProfileViolation("std::init", "uninit_read", + R, AC)) + continue; + S.Diag(R->getBeginLoc(), diag::err_init_member_read_before_init) + << "std::init" << PairField[I]->getDeclName(); + S.Diag(PairField[I]->getLocation(), diag::note_init_uninit_member_here) + << PairField[I]->getDeclName(); + break; + } + } +} + class UninitValsDiagReporter : public UninitVariablesHandler { Sema &S; + AnalysisDeclContext &AC; + // When set, only the CFGUninitProfiles diagnostics are emitted; the default + // -Wuninitialized reports (self-init and the sorted const-ref/ptr/use paths) + // are skipped. The post-error profile pass sets this so it cannot resurrect + // ordinary warnings that the first TU error is meant to suppress. + bool ProfileOnly; typedef SmallVector UsesVec; typedef llvm::PointerIntPair MappedType; // Prefer using MapVector to DenseMap, so that iteration order will be @@ -1697,7 +2636,9 @@ class UninitValsDiagReporter : public UninitVariablesHandler { UsesMap uses; public: - UninitValsDiagReporter(Sema &S) : S(S) {} + UninitValsDiagReporter(Sema &S, AnalysisDeclContext &AC, + bool ProfileOnly = false) + : S(S), AC(AC), ProfileOnly(ProfileOnly) {} ~UninitValsDiagReporter() override { flushDiagnostics(); } MappedType &getUses(const VarDecl *vd) { @@ -1745,6 +2686,54 @@ class UninitValsDiagReporter : public UninitVariablesHandler { // diagnostic is printed, further diagnostics for this variable are skipped. void diagnoseUnitializedVar(const VarDecl *vd, bool hasSelfInit, UsesVec *vec) { + // A self-init (`int x = x;`) reads the uninitialized variable in its own + // initializer, but records no entry in the uses vec, so check it first -- + // at the root cause, mirroring the default path's self-init preference + // below. + if (hasSelfInit && vd->getInit()) { + const Expr *Init = vd->getInit()->IgnoreParenCasts(); + for (const CFGUninitProfileEntry &E : CFGUninitProfiles) { + if (E.ExemptStdByte && + S.Context.getBaseElementType(vd->getType())->isStdByteType()) + continue; + if (!S.Profiles().shouldEmitProfileViolation(E.Name, E.Rule, Init, AC)) + continue; + S.Diag(Init->getBeginLoc(), E.DiagID) << E.Name << vd->getDeclName(); + S.Diag(vd->getLocation(), diag::note_var_declared_here) + << vd->getDeclName(); + return; + } + } + + // If a CFG-uninit-analysis-requesting profile is enforced at any real + // use site and is not suppressed there, emit the profile diagnostic + // and skip the default warning path entirely. + for (const auto &U : *vec) { + // A const-reference binding or address-taking use is not a read; it is + // ref_to_uninit (R7) binding territory, checked at the binding site. + if (U.isConstRefOrPtrUse()) + continue; + for (const CFGUninitProfileEntry &E : CFGUninitProfiles) { + // std::byte may be read while uninitialized (paper §4). + if (E.ExemptStdByte && + S.Context.getBaseElementType(vd->getType())->isStdByteType()) + continue; + if (!S.Profiles().shouldEmitProfileViolation(E.Name, E.Rule, + U.getUser(), AC)) + continue; + S.Diag(U.getUser()->getBeginLoc(), E.DiagID) + << E.Name << vd->getDeclName(); + S.Diag(vd->getLocation(), diag::note_var_declared_here) + << vd->getDeclName(); + return; + } + } + + // The post-error pass runs purely to keep CFG-uninit profiles diagnosing; + // it must never fall through to the default -Wuninitialized reports. + if (ProfileOnly) + return; + // Specially handle the case where we have uses of an uninitialized // variable, but the root cause is an idiomatic self-init. We want // to report the diagnostic at the self-init since that is the root cause. @@ -2748,6 +3737,10 @@ void sema::AnalysisBasedWarnings::clearOverrides() { PolicyOverrides.enableThreadSafetyAnalysis = false; } +bool sema::AnalysisBasedWarnings::hasEnforcedCFGUninitProfile() const { + return S.Profiles().anyProfileEnforced(CFGUninitProfiles); +} + static void flushDiagnostics(Sema &S, const sema::FunctionScopeInfo *fscope) { for (const auto &D : fscope->PossiblyUnreachableDiags) S.Diag(D.Loc, D.PD); @@ -2817,6 +3810,76 @@ void sema::AnalysisBasedWarnings::issueWarningsForRegisteredVarDecl( S, AC, std::make_pair(SecondRange.begin(), SecondRange.end())); } +// Base CFG build options shared by the main per-function analysis pass +// (IssueWarnings) and the post-error profile rerun below, so both see the +// same CFG shape. +static void configureBaseCFGBuildOptions(AnalysisDeclContext &AC) { + // Don't generate EH edges for CallExprs as we'd like to avoid the n^2 + // explosion for destructors that can result and the compile time hit. + AC.getCFGBuildOptions().PruneTriviallyFalseEdges = true; + AC.getCFGBuildOptions().AddEHEdges = false; + AC.getCFGBuildOptions().AddInitializers = true; + AC.getCFGBuildOptions().AddImplicitDtors = true; + AC.getCFGBuildOptions().AddParameterLifetimes = true; + AC.getCFGBuildOptions().AddTemporaryDtors = true; + AC.getCFGBuildOptions().AddCXXNewAllocator = false; + AC.getCFGBuildOptions().AddCXXDefaultInitExprInCtors = true; +} + +// The always-add statement classes of the main pass's non-linearized CFG +// configuration; shared with the post-error profile rerun. +static void addNonLinearizedAlwaysAddClasses(AnalysisDeclContext &AC) { + AC.getCFGBuildOptions() + .setAlwaysAdd(Stmt::BinaryOperatorClass) + .setAlwaysAdd(Stmt::CompoundAssignOperatorClass) + .setAlwaysAdd(Stmt::BlockExprClass) + .setAlwaysAdd(Stmt::CStyleCastExprClass) + .setAlwaysAdd(Stmt::DeclRefExprClass) + .setAlwaysAdd(Stmt::ImplicitCastExprClass) + .setAlwaysAdd(Stmt::UnaryOperatorClass) + // The std::init ctor-body pass scans this-capturing lambda bodies from + // the LambdaExpr's element; without always-add, a lambda in a + // non-statement position never gets its own CFG element. + .setAlwaysAdd(Stmt::LambdaExprClass); +} + +// std::init: the CFG-based std::init checks -- the constructor-body +// read-before-init check and the local-aggregate member check (which runs +// for every definition, constructors included: the two track disjoint +// storage, this-members versus locals). Shared by the normal per-function +// pass and the post-error rerun so both paths stay in step. +static void runInitProfileCFGChecksIfEnforced(Sema &S, const Decl *D, + AnalysisDeclContext &AC) { + if (!S.Profiles().isProfileEnforced("std::init")) + return; + if (const auto *Ctor = dyn_cast(D)) + checkInitProfileCtorBody(S, Ctor, AC); + checkInitProfileLocalMembers(S, AC); +} + +// Pattern-2 profiles (the CFGUninitProfiles table) ride the uninitialized- +// variables analysis, which IssueWarnings otherwise skips once the TU has an +// uncompilable error. Re-run just that analysis for a single function so an +// early TU error does not silently disable the profile for every later +// function. Diagnostics are restricted to the profile via the ProfileOnly +// reporter. +static void runUninitProfileAnalysisAfterError(Sema &S, const Decl *D) { + AnalysisDeclContext AC(/*Mgr=*/nullptr, D); + + configureBaseCFGBuildOptions(AC); + addNonLinearizedAlwaysAddClasses(AC); + + if (CFG *cfg = AC.getCFG()) { + UninitValsDiagReporter reporter(S, AC, /*ProfileOnly=*/true); + UninitVariablesAnalysisStats stats = {}; + runUninitializedVariablesAnalysis(*cast(D), *cfg, AC, reporter, + stats); + // Keep the read-before-init checks (constructor members and local + // aggregates) alive after a TU error, for parity with the normal path. + runInitProfileCFGChecksIfEnforced(S, D, AC); + } +} + // An AST Visitor that calls a callback function on each callable DEFINITION // that is NOT in a dependent context: class CallableVisitor : public DynamicRecursiveASTVisitor { @@ -3181,6 +4244,14 @@ void clang::sema::AnalysisBasedWarnings::IssueWarnings( if (S.hasUncompilableErrorOccurred()) { // Flush out any possibly unreachable diagnostics. flushDiagnostics(S, fscope); + // Pattern-2 profiles ride the uninitialized-variables analysis and must see + // every function even after an earlier TU error; otherwise the first error + // disables them for all later functions. Run only that analysis, only for + // an enforced profile, and only on a valid decl (so the CFG is buildable). + // Other analyses keep the early-out. + if (hasEnforcedCFGUninitProfile() && !D->isInvalidDecl() && + !Diags.hasFatalErrorOccurred()) + runUninitProfileAnalysisAfterError(S, D); return; } @@ -3190,16 +4261,7 @@ void clang::sema::AnalysisBasedWarnings::IssueWarnings( // Construct the analysis context with the specified CFG build options. AnalysisDeclContext AC(/* AnalysisDeclContextManager */ nullptr, D); - // Don't generate EH edges for CallExprs as we'd like to avoid the n^2 - // explosion for destructors that can result and the compile time hit. - AC.getCFGBuildOptions().PruneTriviallyFalseEdges = true; - AC.getCFGBuildOptions().AddEHEdges = false; - AC.getCFGBuildOptions().AddInitializers = true; - AC.getCFGBuildOptions().AddImplicitDtors = true; - AC.getCFGBuildOptions().AddParameterLifetimes = true; - AC.getCFGBuildOptions().AddTemporaryDtors = true; - AC.getCFGBuildOptions().AddCXXNewAllocator = false; - AC.getCFGBuildOptions().AddCXXDefaultInitExprInCtors = true; + configureBaseCFGBuildOptions(AC); bool IsLifetimeSafetyDiagnosticEnabled = !Diags.isIgnored(diag::warn_lifetime_safety_use_after_scope, @@ -3230,14 +4292,7 @@ void clang::sema::AnalysisBasedWarnings::IssueWarnings( // Unreachable code analysis and thread safety require a linearized CFG. AC.getCFGBuildOptions().setAllAlwaysAdd(); } else { - AC.getCFGBuildOptions() - .setAlwaysAdd(Stmt::BinaryOperatorClass) - .setAlwaysAdd(Stmt::CompoundAssignOperatorClass) - .setAlwaysAdd(Stmt::BlockExprClass) - .setAlwaysAdd(Stmt::CStyleCastExprClass) - .setAlwaysAdd(Stmt::DeclRefExprClass) - .setAlwaysAdd(Stmt::ImplicitCastExprClass) - .setAlwaysAdd(Stmt::UnaryOperatorClass); + addNonLinearizedAlwaysAddClasses(AC); } if (EnableLifetimeSafetyAnalysis) AC.getCFGBuildOptions().AddLifetime = true; @@ -3302,13 +4357,14 @@ void clang::sema::AnalysisBasedWarnings::IssueWarnings( Analyzer.run(AC); } - if (!Diags.isIgnored(diag::warn_uninit_var, D->getBeginLoc()) || + if (hasEnforcedCFGUninitProfile() || + !Diags.isIgnored(diag::warn_uninit_var, D->getBeginLoc()) || !Diags.isIgnored(diag::warn_sometimes_uninit_var, D->getBeginLoc()) || !Diags.isIgnored(diag::warn_maybe_uninit_var, D->getBeginLoc()) || !Diags.isIgnored(diag::warn_uninit_const_reference, D->getBeginLoc()) || !Diags.isIgnored(diag::warn_uninit_const_pointer, D->getBeginLoc())) { if (CFG *cfg = AC.getCFG()) { - UninitValsDiagReporter reporter(S); + UninitValsDiagReporter reporter(S, AC); UninitVariablesAnalysisStats stats; std::memset(&stats, 0, sizeof(UninitVariablesAnalysisStats)); runUninitializedVariablesAnalysis(*cast(D), *cfg, AC, @@ -3328,6 +4384,12 @@ void clang::sema::AnalysisBasedWarnings::IssueWarnings( } } + // std::init: diagnose a read of a [[uninit]] scalar member before it is + // assigned -- in the constructor body for the current object's members, in + // any body for a constructor-less aggregate local's -- reusing the CFG + // built above. + runInitProfileCFGChecksIfEnforced(S, D, AC); + // TODO: Enable lifetime safety analysis for other languages once it is // stable. if (EnableLifetimeSafetyAnalysis && S.getLangOpts().CPlusPlus) { diff --git a/clang/lib/Sema/CMakeLists.txt b/clang/lib/Sema/CMakeLists.txt index 0ebf56ecffe69..ec12d752f902a 100644 --- a/clang/lib/Sema/CMakeLists.txt +++ b/clang/lib/Sema/CMakeLists.txt @@ -80,6 +80,7 @@ add_clang_library(clangSema SemaOpenMP.cpp SemaOverload.cpp SemaPPC.cpp + SemaProfiles.cpp SemaPseudoObject.cpp SemaRISCV.cpp SemaStmt.cpp diff --git a/clang/lib/Sema/Sema.cpp b/clang/lib/Sema/Sema.cpp index 3065b5e1e66d3..f1c0a47fdd0f3 100644 --- a/clang/lib/Sema/Sema.cpp +++ b/clang/lib/Sema/Sema.cpp @@ -20,9 +20,11 @@ #include "clang/AST/DeclObjC.h" #include "clang/AST/Expr.h" #include "clang/AST/ExprCXX.h" +#include "clang/AST/ParentMap.h" #include "clang/AST/PrettyDeclStackTrace.h" #include "clang/AST/StmtCXX.h" #include "clang/AST/TypeOrdering.h" +#include "clang/Analysis/AnalysisDeclContext.h" #include "clang/Basic/DarwinSDKInfo.h" #include "clang/Basic/DiagnosticOptions.h" #include "clang/Basic/PartialDiagnostic.h" @@ -31,6 +33,7 @@ #include "clang/Lex/HeaderSearch.h" #include "clang/Lex/HeaderSearchOptions.h" #include "clang/Lex/Preprocessor.h" +#include "clang/Sema/Attr.h" #include "clang/Sema/CXXFieldCollector.h" #include "clang/Sema/EnterExpressionEvaluationContext.h" #include "clang/Sema/ExternalSemaSource.h" @@ -60,6 +63,7 @@ #include "clang/Sema/SemaOpenCL.h" #include "clang/Sema/SemaOpenMP.h" #include "clang/Sema/SemaPPC.h" +#include "clang/Sema/SemaProfiles.h" #include "clang/Sema/SemaPseudoObject.h" #include "clang/Sema/SemaRISCV.h" #include "clang/Sema/SemaSPIRV.h" @@ -298,6 +302,7 @@ Sema::Sema(Preprocessor &pp, ASTContext &ctxt, ASTConsumer &consumer, OpenCLPtr(std::make_unique(*this)), OpenMPPtr(std::make_unique(*this)), PPCPtr(std::make_unique(*this)), + ProfilesPtr(std::make_unique(*this)), PseudoObjectPtr(std::make_unique(*this)), RISCVPtr(std::make_unique(*this)), SPIRVPtr(std::make_unique(*this)), @@ -2978,3 +2983,4 @@ Attr *Sema::CreateAnnotationAttr(const ParsedAttr &AL) { return CreateAnnotationAttr(AL, Str, Args); } + diff --git a/clang/lib/Sema/SemaCast.cpp b/clang/lib/Sema/SemaCast.cpp index 5360f8a2908bf..d6e3a0438cd63 100644 --- a/clang/lib/Sema/SemaCast.cpp +++ b/clang/lib/Sema/SemaCast.cpp @@ -25,6 +25,7 @@ #include "clang/Sema/Initialization.h" #include "clang/Sema/SemaHLSL.h" #include "clang/Sema/SemaObjC.h" +#include "clang/Sema/SemaProfiles.h" #include "clang/Sema/SemaRISCV.h" #include "llvm/ADT/SmallVector.h" #include "llvm/ADT/StringExtras.h" @@ -400,6 +401,11 @@ Sema::BuildCXXNamedCast(SourceLocation OpLoc, tok::TokenKind Kind, if (Op.SrcExpr.isInvalid()) return ExprError(); DiscardMisalignedMemberAddress(DestType.getTypePtr(), E); + // test::type_cast is a built-in test profile; see + // ProfilesFrameworkInternals.rst. + Profiles().checkProfileViolation("test::type_cast", "reinterpret_cast", + OpLoc, + diag::err_profile_type_cast_reinterpret); } return Op.complete(CXXReinterpretCastExpr::Create(Context, Op.ResultType, Op.ValueKind, Op.Kind, Op.SrcExpr.get(), diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp index 054b664ca0a8b..0ecc2e8bb1eb7 100644 --- a/clang/lib/Sema/SemaDecl.cpp +++ b/clang/lib/Sema/SemaDecl.cpp @@ -50,6 +50,7 @@ #include "clang/Sema/SemaCUDA.h" #include "clang/Sema/SemaHLSL.h" #include "clang/Sema/SemaInternal.h" +#include "clang/Sema/SemaProfiles.h" #include "clang/Sema/SemaObjC.h" #include "clang/Sema/SemaOpenACC.h" #include "clang/Sema/SemaOpenMP.h" @@ -1799,6 +1800,14 @@ bool Sema::CheckRedeclarationInModule(NamedDecl *New, NamedDecl *Old) { if (CheckRedeclarationExported(New, Old)) return true; + // P3589R2 [decl.attr.enforce]p5: a redeclaration must be covered by + // profiles compatible with those covering the previous declaration (and + // vice versa). Implicit instantiations are not written declarations, so + // they are exempt, matching the module-ownership check. Diagnose-only: + // merging proceeds either way. + if (!isImplicitInstantiation(New) && !isImplicitInstantiation(Old)) + Profiles().checkRedeclarationProfileCompatibility(New, Old); + return false; } @@ -2974,7 +2983,7 @@ static bool mergeDeclAttribute(Sema &S, NamedDecl *D, NewAttr = S.HLSL().mergeVkConstantIdAttr(D, *CI, CI->getId()); else if (const auto *SA = dyn_cast(Attr)) NewAttr = S.HLSL().mergeShaderAttr(D, *SA, SA->getType()); - else if (isa(Attr)) + else if (isa(Attr) || isa(Attr)) // Do nothing. Each redeclaration should be suppressed separately. NewAttr = nullptr; else if (const auto *RD = dyn_cast(Attr)) @@ -5409,6 +5418,9 @@ Decl *Sema::ParsedFreeStandingDeclSpec(Scope *S, AccessSpecifier AS, ? diag::err_no_declarators : diag::ext_no_declarators) << DS.getSourceRange(); + for (const ParsedAttr &AL : DeclAttrs) + if (AL.getKind() == ParsedAttr::AT_ProfilesEnforce) + Diag(AL.getLoc(), diag::err_profiles_enforce_not_at_tu_scope); return TagD; } @@ -14712,6 +14724,9 @@ void Sema::ActOnUninitializedDecl(Decl *RealDecl) { Var->setInit(RecoveryExpr.get()); } + Profiles().checkInitProfileUninitDecl(Var); + Profiles().checkInitProfileStaticMarker(Var); + CheckCompleteVariableDeclaration(Var); } } @@ -14818,6 +14833,13 @@ void Sema::addLifetimeBoundToImplicitThis(CXXMethodDecl *MD) { void Sema::CheckCompleteVariableDeclaration(VarDecl *var) { if (var->isInvalidDecl()) return; + Profiles().checkInitProfileUninitWithInitializer(var, var->getInit()); + + // std::init / ref_to_uninit (paper §5): a pointer or reference variable + // must be bound consistently with its [[ref_to_uninit]] marking. + Profiles().checkInitProfileRefToUninitBinding( + var->getLocation(), var, var->getType(), var->getInit(), var); + CUDA().MaybeAddConstantAttr(var); if (getLangOpts().OpenCL) { @@ -15054,6 +15076,10 @@ void Sema::CheckCompleteVariableDeclaration(VarDecl *var) { for (auto &it : Notes) Diag(it.first, it.second); var->setInvalidDecl(); + } else if (IsGlobal && + Profiles().checkInitProfileStaticRuntimeInit(var, + checkConstInit)) { + // The profile diagnostic supersedes -Wglobal-constructors below. } else if (IsGlobal && !getDiagnostics().isIgnored(diag::warn_global_constructor, var->getLocation())) { @@ -16991,9 +17017,16 @@ Decl *Sema::ActOnFinishFunctionBody(Decl *dcl, Stmt *Body, bool IsInstantiation, getDiagnostics().getSuppressAllDiagnostics()) { DiscardCleanupsInEvaluationContext(); } - if (!hasUncompilableErrorOccurred() && !isa(dcl)) { - // Since the body is valid, issue any analysis-based warnings that are - // enabled. + if (!isa(dcl) && + (!hasUncompilableErrorOccurred() || + (!dcl->isInvalidDecl() && + AnalysisWarnings.hasEnforcedCFGUninitProfile()))) { + // Normally analysis-based warnings only run for a valid body in an + // otherwise error-free TU. CFG-based profiles (e.g. std::init's + // uninit_read) must keep diagnosing later functions even after an + // earlier TU error, so still run the per-function pass for a valid body + // when such a profile is enforced; IssueWarnings restricts that + // post-error pass to profile diagnostics. ActivePolicy = &WP; } diff --git a/clang/lib/Sema/SemaDeclAttr.cpp b/clang/lib/Sema/SemaDeclAttr.cpp index 6fc749464586d..7e867ee71f3cc 100644 --- a/clang/lib/Sema/SemaDeclAttr.cpp +++ b/clang/lib/Sema/SemaDeclAttr.cpp @@ -42,6 +42,7 @@ #include "clang/Sema/Scope.h" #include "clang/Sema/ScopeInfo.h" #include "clang/Sema/Sema.h" +#include "clang/Sema/SemaProfiles.h" #include "clang/Sema/SemaAMDGPU.h" #include "clang/Sema/SemaARM.h" #include "clang/Sema/SemaAVR.h" @@ -5451,6 +5452,71 @@ static void handleSuppressAttr(Sema &S, Decl *D, const ParsedAttr &AL) { DiagnosticIdentifiers.size())); } +static void handleProfilesEnforceAttr(Sema &S, Decl *D, const ParsedAttr &AL) { + if (!isa(D)) { + S.Diag(AL.getLoc(), diag::err_profiles_enforce_not_empty_decl); + return; + } + + if (!isa(D->getDeclContext())) { + S.Diag(AL.getLoc(), diag::err_profiles_enforce_not_at_tu_scope); + return; + } + + // P3589R2 [decl.attr.enforce]p1: enforce on empty-declaration shall precede + // any non-empty-declaration. Declarations in an included PCH precede the + // main file but must not be deserialized just for this check: the PCH + // records whether it contains any (PROFILES_TU_HAS_NONEMPTY_DECL), and the + // walk below covers only the parsed declarations (noload_decls). + auto *TU = cast(D->getDeclContext()); + if (S.Profiles().TUPrecededByNonEmptyDecl) { + // The preceding declaration lives in the PCH; without deserializing it + // there is no Decl to point a note at. + S.Diag(AL.getLoc(), diag::err_profiles_enforce_after_decl); + return; + } + for (const auto *Prev : TU->noload_decls()) { + if (Prev->isImplicit() || !Prev->getLocation().isValid()) + continue; + if (!isa(Prev)) { + S.Diag(AL.getLoc(), diag::err_profiles_enforce_after_decl); + S.Diag(Prev->getLocation(), diag::note_previous_decl) << "declaration"; + return; + } + } + + // P3589R2 [decl.attr.require]p2: for header units, enforce from + // empty-declarations is visible to require. + Module *Mod = nullptr; + if (auto *M = S.getCurrentModule()) + if (M->isHeaderUnit()) + Mod = M; + + SmallVector Names, Designators; + SmallVector ArgumentCounts, ArgumentKinds; + SmallVector ArgumentKeys, ArgumentValues; + if (!S.Profiles().processProfilesEnforceAttr(AL, Mod, &Names, &Designators, + &ArgumentCounts, &ArgumentKeys, + &ArgumentValues, &ArgumentKinds)) + return; + + D->addAttr(::new (S.Context) + ProfilesEnforceAttr(S.Context, AL, Names.data(), Names.size(), + Designators.data(), Designators.size(), + ArgumentCounts.data(), + ArgumentCounts.size(), ArgumentKeys.data(), + ArgumentKeys.size(), ArgumentValues.data(), + ArgumentValues.size(), + ArgumentKinds.data(), + ArgumentKinds.size())); +} + +static void handleProfilesSuppressDeclAttr(Sema &S, Decl *D, + const ParsedAttr &AL) { + if (auto *A = S.Profiles().makeProfilesSuppressAttr(AL)) + D->addAttr(A); +} + static void handleLifetimeCategoryAttr(Sema &S, Decl *D, const ParsedAttr &AL) { TypeSourceInfo *DerefTypeLoc = nullptr; QualType ParmType; @@ -6885,6 +6951,116 @@ static void handleUninitializedAttr(Sema &S, Decl *D, const ParsedAttr &AL) { D->addAttr(::new (S.Context) UninitializedAttr(S.Context, AL)); } +static void handleUninitAttr(Sema &S, Decl *D, const ParsedAttr &AL) { + // The SubjectList has already restricted D to a variable or non-static data + // member. Reject the subjects for which "leave uninitialized" is + // meaningless: a reference (must bind when declared), a function parameter + // (initialized by the caller), and a structured binding (requires an + // initializer). These are rejected regardless of -fprofiles, like any other + // ill-formed attribute placement. + enum InvalidSubject { Reference, Parameter, StructuredBinding }; + std::optional Invalid; + if (isa(D)) + Invalid = Parameter; + else if (isa(D)) + Invalid = StructuredBinding; + + if (Invalid) { + S.Diag(AL.getLoc(), diag::err_uninit_attr_invalid_subject) + << static_cast(*Invalid); + AL.setInvalid(); + return; + } + + // A reference subject is rejected by the shared helper, which defers on a + // dependent type to the instantiation re-check in Sema::InstantiateAttrs. + if (S.Profiles().diagnoseInvalidUninitMarker(D, AL.getLoc())) { + AL.setInvalid(); + return; + } + + D->addAttr(::new (S.Context) UninitAttr(S.Context, AL)); + + // std::init / union_marker + pointer_marker (paper §4.1, §5.6). Shared with + // the template-instantiation re-check sites (VisitFieldDecl / VisitVarDecl), + // since this handler only runs on the pattern. + S.Profiles().checkInitProfileMarkerPlacement(D); +} + +static void handleRefToUninitAttr(Sema &S, Decl *D, const ParsedAttr &AL) { + // The SubjectList restricts D to a variable, non-static data member, or + // function. "Refers to uninitialized memory" is only meaningful for a + // pointer or reference to an object (for a function, its return value), so + // reject any other type. A function pointer or reference denotes a function, + // never uninitialized storage, so the marker could never be satisfied; reject + // it too (like a pointer-to-member, which is not a pointer type here). Like + // the [[uninit]] subject checks, this is not profile policy and so fires + // regardless of -fprofiles. A dependent subject defers to the instantiation + // re-check in Sema::InstantiateAttrs, once the substituted type is known. + if (S.Profiles().diagnoseInvalidRefToUninitMarker(D, AL.getLoc())) { + AL.setInvalid(); + return; + } + + D->addAttr(::new (S.Context) RefToUninitAttr(S.Context, AL)); +} + +static void handleNowInitAttr(Sema &S, Decl *D, const ParsedAttr &AL) { + // The SubjectList restricts D to a function. [[now_init]] asserts that the + // callee initializes the storage bound to each of its [[ref_to_uninit]] + // parameters (P4222R2 §6.2), so a declaration with no marked parameter + // would make it vacuous; reject it. The parameters' attributes are + // attached when the parameter declarations are built, before this + // declaration attribute is processed, so the prefix and suffix attribute + // positions both see them. A dependent parameter's marker is attached to + // the pattern unvalidated (its type check defers to instantiation), so a + // template with a marked dependent parameter passes here; should the + // instantiation drop that marker, the inherited [[now_init]] goes inert + // rather than re-diagnosed, like the dropped marker itself. Like the other + // marker subject checks, this fires regardless of -fprofiles. + if (llvm::none_of( + cast(D)->parameters(), + [](const ParmVarDecl *P) { return P->hasAttr(); })) { + S.Diag(AL.getLoc(), diag::err_now_init_attr_no_marked_parameter); + AL.setInvalid(); + return; + } + + D->addAttr(::new (S.Context) NowInitAttr(S.Context, AL)); +} + +static void handleNowUninitAttr(Sema &S, Decl *D, const ParsedAttr &AL) { + // The SubjectList restricts D to a function. [[now_uninit]] asserts that + // the callee ends the lifetime of the storage bound to each of its + // pointer/reference parameters -- the recording P4222R2 §4.4 wishes for + // ("the object subjected to destroy_at() should be considered + // uninitialized, but there is no way of recording that in the code") -- + // so a declaration with no such parameter would make it vacuous; reject + // it. Unlike [[now_init]], the parameters are unmarked (they receive + // initialized memory), so the vacuity check keys on their types: a + // pointer to an object or a reference. A function pointer or reference + // denotes a function, never destroyable storage (mirroring + // [[ref_to_uninit]]'s subject rule), and a dependent type may + // instantiate to anything, so it passes -- if it does not become a + // pointer or reference, the inherited attribute goes inert (the + // withdrawal arms only ever key on pointer/reference bindings). Like the + // other marker subject checks, this fires regardless of -fprofiles. + if (llvm::none_of(cast(D)->parameters(), + [](const ParmVarDecl *P) { + QualType T = P->getType(); + return T->isDependentType() || + ((T->isPointerType() || T->isReferenceType()) && + !T->isFunctionPointerType() && + !T->isFunctionReferenceType()); + })) { + S.Diag(AL.getLoc(), diag::err_now_uninit_attr_no_pointer_parameter); + AL.setInvalid(); + return; + } + + D->addAttr(::new (S.Context) NowUninitAttr(S.Context, AL)); +} + static void handleMIGServerRoutineAttr(Sema &S, Decl *D, const ParsedAttr &AL) { // Check that the return type is a `typedef int kern_return_t` or a typedef // around it, because otherwise MIG convention checks make no sense. @@ -7881,6 +8057,16 @@ ProcessDeclAttribute(Sema &S, Scope *scope, Decl *D, const ParsedAttr &AL, case ParsedAttr::AT_Suppress: handleSuppressAttr(S, D, AL); break; + case ParsedAttr::AT_ProfilesEnforce: + handleProfilesEnforceAttr(S, D, AL); + break; + case ParsedAttr::AT_ProfilesSuppress: + handleProfilesSuppressDeclAttr(S, D, AL); + break; + case ParsedAttr::AT_ProfilesRequire: + // Require is handled in SemaModule.cpp on import-declarations. + S.Diag(AL.getLoc(), diag::err_profiles_require_not_on_import); + break; case ParsedAttr::AT_Owner: case ParsedAttr::AT_Pointer: handleLifetimeCategoryAttr(S, D, AL); @@ -8141,6 +8327,22 @@ ProcessDeclAttribute(Sema &S, Scope *scope, Decl *D, const ParsedAttr &AL, handleUninitializedAttr(S, D, AL); break; + case ParsedAttr::AT_Uninit: + handleUninitAttr(S, D, AL); + break; + + case ParsedAttr::AT_RefToUninit: + handleRefToUninitAttr(S, D, AL); + break; + + case ParsedAttr::AT_NowInit: + handleNowInitAttr(S, D, AL); + break; + + case ParsedAttr::AT_NowUninit: + handleNowUninitAttr(S, D, AL); + break; + case ParsedAttr::AT_ObjCExternallyRetained: S.ObjC().handleExternallyRetainedAttr(D, AL); break; diff --git a/clang/lib/Sema/SemaDeclCXX.cpp b/clang/lib/Sema/SemaDeclCXX.cpp index 2ae6e5de0e3ee..2203343547c52 100644 --- a/clang/lib/Sema/SemaDeclCXX.cpp +++ b/clang/lib/Sema/SemaDeclCXX.cpp @@ -44,6 +44,7 @@ #include "clang/Sema/ScopeInfo.h" #include "clang/Sema/SemaCUDA.h" #include "clang/Sema/SemaInternal.h" +#include "clang/Sema/SemaProfiles.h" #include "clang/Sema/SemaObjC.h" #include "clang/Sema/SemaOpenMP.h" #include "clang/Sema/Template.h" @@ -4225,6 +4226,20 @@ void Sema::ActOnFinishCXXInClassMemberInitializer(Decl *D, } FD->setInClassInitializer(InitExpr.get()); + + // Pass the field to the post-initialization profile checks so the Decl-aware + // shouldEmitProfileViolation overload resolves [[profiles::suppress]] from + // the field and its lexical parents. This does not depend on a parse-time + // suppress scope still being active (the late-parsed NSDMI finishes parsing + // before this finalization runs). + Profiles().checkInitProfileUninitWithInitializer( + FD, FD->getInClassInitializer()); + + // std::init / ref_to_uninit (paper §5): a pointer or reference data member + // with a default member initializer must be bound consistently with its + // [[ref_to_uninit]] marking. + Profiles().checkInitProfileRefToUninitBinding( + FD->getLocation(), FD, FD->getType(), FD->getInClassInitializer(), FD); } /// Find the direct and/or virtual base specifiers that @@ -4664,6 +4679,20 @@ Sema::BuildMemberInitializer(ValueDecl *Member, Expr *Init, } else { Init = MemberInit.get(); } + + // std::init / ref_to_uninit (paper §5): a pointer/reference member given a + // written member-initializer must be bound consistently with its marking. + // Pass the enclosing constructor as the Decl so a class-template pattern + // defers (via D->isTemplated()) and fires once at instantiation, where + // BuildMemberInitializer re-runs with the instantiated constructor as + // CurContext, matching ctor_uninit_member. A member of an anonymous + // struct/union arrives as an IndirectFieldDecl, which never carries the + // marker; the [[ref_to_uninit]] attribute lives on the underlying field. + const ValueDecl *MarkerTarget = + IndirectMember ? IndirectMember->getAnonField() : Member; + if (auto *Ctor = dyn_cast(CurContext)) + Profiles().checkInitProfileRefToUninitBinding( + IdLoc, MarkerTarget, MarkerTarget->getType(), Init, Ctor); } if (DirectMember) { @@ -5919,6 +5948,8 @@ void Sema::ActOnMemInitializers(Decl *ConstructorDecl, SetCtorInitializers(Constructor, AnyErrors, MemInits); DiagnoseUninitializedFields(*this, Constructor); + + Profiles().checkProfileViolationsAtConstructorFinalization(Constructor); } void Sema::MarkBaseAndMemberDestructorsReferenced(SourceLocation Location, @@ -5985,6 +6016,7 @@ void Sema::ActOnDefaultCtorInitializers(Decl *CDtorDecl) { } SetCtorInitializers(Constructor, /*AnyErrors=*/false); DiagnoseUninitializedFields(*this, Constructor); + Profiles().checkProfileViolationsAtConstructorFinalization(Constructor); } } @@ -7440,6 +7472,8 @@ void Sema::CheckCompletedCXXClass(Scope *S, CXXRecordDecl *Record) { }; CheckMismatchedTypeAwareAllocators(OO_New, OO_Delete); CheckMismatchedTypeAwareAllocators(OO_Array_New, OO_Array_Delete); + + Profiles().checkProfileViolationsAtClassFinalization(Record); } /// Look up the special member function that would be called by a special diff --git a/clang/lib/Sema/SemaExpr.cpp b/clang/lib/Sema/SemaExpr.cpp index db6d93ce54791..ebad61f733b9b 100644 --- a/clang/lib/Sema/SemaExpr.cpp +++ b/clang/lib/Sema/SemaExpr.cpp @@ -53,6 +53,7 @@ #include "clang/Sema/ParsedTemplate.h" #include "clang/Sema/Scope.h" #include "clang/Sema/ScopeInfo.h" +#include "clang/Sema/SemaProfiles.h" #include "clang/Sema/SemaARM.h" #include "clang/Sema/SemaCUDA.h" #include "clang/Sema/SemaFixItUtils.h" @@ -737,6 +738,13 @@ ExprResult Sema::DefaultLvalueConversion(Expr *E) { if (!BoundsSafetyCheckUseOfCountAttrPtr(Res.get())) return ExprError(); + // std::init / uninit_read (paper §4.5): a read through a [[ref_to_uninit]] + // pointer or reference accesses uninitialized memory. This is the single + // lvalue-to-rvalue chokepoint that by-value reads (copy-init, by-value + // arguments, returns, operator operands) all funnel through. + if (getLangOpts().Profiles) + Profiles().checkInitProfileReadThrough(E->getExprLoc(), E, T); + // C++ [conv.lval]p3: // If T is cv std::nullptr_t, the result is a null pointer constant. CastKind CK = T->isNullPtrType() ? CK_NullToPointer : CK_LValueToRValue; @@ -6273,6 +6281,17 @@ bool Sema::GatherArgumentsForCall(SourceLocation CallLoc, FunctionDecl *FDecl, Arg = ArgExpr.getAs(); } + // std::init / ref_to_uninit (paper §5): a pointer or reference argument + // must match the [[ref_to_uninit]] marking of its parameter. A real + // argument is checked once, by the shared hook in + // PerformCopyInitialization; a default argument does not re-run + // copy-initialization here, so check its underlying expression (the + // recognizers don't see through the CXXDefaultArgExpr wrapper). + if (Param && getLangOpts().Profiles) + if (const auto *DAE = dyn_cast(Arg)) + Profiles().checkInitProfileRefToUninitBinding( + Arg->getExprLoc(), Param, Param->getType(), DAE->getExpr()); + // Check for array bounds violations for each argument to the call. This // check only triggers warnings when the argument isn't a more complex Expr // with its own checking, such as a BinaryOperator. @@ -6302,6 +6321,11 @@ bool Sema::GatherArgumentsForCall(SourceLocation CallLoc, FunctionDecl *FDecl, for (Expr *A : Args.slice(ArgIx)) { ExprResult Arg = DefaultVariadicArgumentPromotion(A, CallType, FDecl); Invalid |= Arg.isInvalid(); + // std::init / ref_to_uninit (paper §5): a `...` parameter cannot + // carry the marker, so a pointer argument is checked as an unmarked + // target. + if (!Arg.isInvalid()) + Profiles().checkInitProfileVariadicArgument(Arg.get()); AllArgs.push_back(Arg.get()); } } @@ -14385,6 +14409,15 @@ QualType Sema::CheckAssignmentOperands(Expr *LHSExpr, ExprResult &RHS, if (CheckForModifiableLvalue(LHSExpr, Loc, *this)) return QualType(); + // std::init / uninit_write (paper §5.4-§5.6): a scalar store to a subobject + // of a named [[uninit]] object is banned delayed initialization. This is + // the shared funnel for simple and every compound assignment, so the check + // fires exactly once per built-in assignment; class-typed operator= never + // reaches it. + if (getLangOpts().Profiles) + Profiles().checkInitProfileAssignmentOperands( + Opc, LHSExpr, /*IsCompound=*/!CompoundType.isNull(), Loc); + QualType LHSType = LHSExpr->getType(); QualType RHSType = CompoundType.isNull() ? RHS.get()->getType() : CompoundType; @@ -14520,6 +14553,15 @@ QualType Sema::CheckAssignmentOperands(Expr *LHSExpr, ExprResult &RHS, } } + // std::init: a completed built-in assignment is a store; record parse-order + // whole-entity store credit (paper §4.2/§4.5). Deliberately at the tail: a + // simple assignment's RHS lvalue-to-rvalue load is checked inside + // CheckSingleAssignmentConstraints above, so recording earlier would let + // `*p = *p;` credit itself and silently pass its own RHS read. Invalid + // assignments returned early and record nothing. + if (getLangOpts().Profiles) + Profiles().recordInitProfileStore(LHSExpr); + // C11 6.5.16p3: The type of an assignment expression is the type of the // left operand would have after lvalue conversion. // C11 6.3.2.1p2: ...this is called lvalue conversion. If the lvalue has @@ -15483,6 +15525,12 @@ ExprResult Sema::CreateBuiltinBinOp(SourceLocation OpLoc, DiagnoseSelfAssignment(*this, LHS.get(), RHS.get(), OpLoc, true); DiagnoseSelfMove(LHS.get(), RHS.get(), OpLoc); + // std::init / ref_to_uninit (paper §5): assigning a pointer must respect + // the [[ref_to_uninit]] marking of the assigned-to pointer. + if (getLangOpts().Profiles) + Profiles().checkInitProfilePointerAssignment(LHS.get(), RHS.get(), + OpLoc); + // Avoid copying a block to the heap if the block is assigned to a local // auto variable that is declared in the same scope as the block. This // optimization is unsafe if the local variable is declared in an outer @@ -16151,6 +16199,13 @@ ExprResult Sema::CreateBuiltinUnaryOp(SourceLocation OpLoc, Opc == UO_PreInc || Opc == UO_PostInc, Opc == UO_PreInc || Opc == UO_PreDec); CanOverflow = isOverflowingIntegerType(Context, resultType); + // std::init / uninit_write (paper §5.4-§5.6): a built-in ++/-- stores to + // its operand like an assignment does to its LHS. Checked here rather + // than in CheckIncrementDecrementOperand, which self-recurses on + // placeholder operands and would fire twice; overloaded class ++/-- + // never reaches CreateBuiltinUnaryOp. + if (getLangOpts().Profiles && !resultType.isNull()) + Profiles().checkInitProfileIncDec(Input.get(), OpLoc); break; case UO_AddrOf: resultType = CheckAddressOfOperand(Input, OpLoc); diff --git a/clang/lib/Sema/SemaExprCXX.cpp b/clang/lib/Sema/SemaExprCXX.cpp index 5a5bbf4d900dc..bfc828dd81ceb 100644 --- a/clang/lib/Sema/SemaExprCXX.cpp +++ b/clang/lib/Sema/SemaExprCXX.cpp @@ -905,6 +905,14 @@ ExprResult Sema::BuildCXXThrow(SourceLocation OpLoc, Expr *Ex, ExprResult Res = PerformMoveOrCopyInitialization(Entity, NRInfo, Ex); if (Res.isInvalid()) return ExprError(); + + // std::init / ref_to_uninit (paper §5): throwing a pointer to + // uninitialized memory. The Decl-less wrapper defers only on an + // instantiation-dependent operand, rebuilt at instantiation; a + // non-dependent throw is checked at definition time. + if (getLangOpts().Profiles) + Profiles().checkInitProfileThrowOperand(Ex); + Ex = Res.get(); } @@ -2607,6 +2615,21 @@ ExprResult Sema::BuildCXXNew(SourceRange Range, bool UseGlobal, Initializer = FullInit.get(); + // std::init / ref_to_uninit (paper §5): a written initializer for an + // allocated pointer binds it like a variable initialization. Scalar + // allocations only: for an array new AllocType is the *element* type, + // and each written element -- including the lone initializer of + // `new T*[k]{p}` or `new T*[k](p)` -- is already checked by the + // aggregate element hooks (InitListChecker::CheckSubElementType and + // TryOrBuildParenListInitialization), so checking it here too would + // diagnose it twice. The Decl-less wrapper defers only on an + // instantiation-dependent allocated type or initializer, rebuilt at + // instantiation; a non-dependent new-expression is checked at + // definition time. + if (getLangOpts().Profiles && !ArraySize) + Profiles().checkInitProfileNewInitializer( + AllocType, Exprs.size() == 1 ? Exprs[0] : nullptr); + // FIXME: If we have a KnownArraySize, check that the array bound of the // initializer is no greater than that constant value. diff --git a/clang/lib/Sema/SemaInit.cpp b/clang/lib/Sema/SemaInit.cpp index ede2b9beef49b..4dbae6dd27d37 100644 --- a/clang/lib/Sema/SemaInit.cpp +++ b/clang/lib/Sema/SemaInit.cpp @@ -30,6 +30,7 @@ #include "clang/Sema/Ownership.h" #include "clang/Sema/SemaHLSL.h" #include "clang/Sema/SemaObjC.h" +#include "clang/Sema/SemaProfiles.h" #include "llvm/ADT/APInt.h" #include "llvm/ADT/DenseMap.h" #include "llvm/ADT/FoldingSet.h" @@ -1592,6 +1593,30 @@ void InitListChecker::CheckSubElementType(const InitializedEntity &Entity, if (Result.isInvalid()) hadError = true; + // std::init / ref_to_uninit (paper §5): in C++ a pointer field + // initialized by an enclosing aggregate's init list is copy- + // initialized here (a scalar member never reaches CheckScalarType). + // Scoped to an aggregate subobject (a field, or an array / vector / + // complex element) so the enclosing variable/argument/return is left + // to its own site, which already handles a braced source via the + // recognizer. An element position has no declaration to carry the + // marker (Entity.getDecl() is null for the element kinds), so it is + // checked as an unmarked target -- like a variadic argument or a + // function-pointer parameter -- and the rules of paper §4.3 apply + // per pointer element. No Decl is passed: the init list can appear + // in a template independently of whether the aggregate is one, so + // checkInitProfileRefToUninit defers on an instantiation-dependent + // source and suppression comes from the parse-time stack. (A + // reference field is routed to CheckReferenceType above; an array + // of references is ill-formed.) + if (!Result.isInvalid() && + (Entity.getKind() == InitializedEntity::EK_Member || + Entity.getKind() == InitializedEntity::EK_ArrayElement || + Entity.getKind() == InitializedEntity::EK_VectorElement || + Entity.getKind() == InitializedEntity::EK_ComplexElement)) + SemaRef.Profiles().checkInitProfileRefToUninitBinding( + expr->getExprLoc(), Entity.getDecl(), ElemType, expr); + UpdateStructuredListElement(StructuredList, StructuredIndex, Result.getAs()); } else if (!Seq) { @@ -1864,6 +1889,11 @@ void InitListChecker::CheckReferenceType(const InitializedEntity &Entity, return; } + // Capture the source element before 'expr' is overwritten by the + // PerformCopyInitialization result below; the ref_to_uninit recognizer needs + // the written source, not the bound reference. + const Expr *Src = expr; + ExprResult Result; if (VerifyOnly) { if (SemaRef.CanPerformCopyInitialization(Entity,expr)) @@ -1884,6 +1914,20 @@ void InitListChecker::CheckReferenceType(const InitializedEntity &Entity, if (!VerifyOnly && expr) IList->setInit(Index, expr); + // std::init / ref_to_uninit (paper §5): a reference field bound by an + // enclosing aggregate's init list must be bound consistently with its + // marking. getParent() restricts this to a genuine aggregate subobject: a + // top-level reference member braced-init (a constructor member-initializer or + // an NSDMI) has a null parent and is checked at its own Decl-aware site, so + // the parent guard prevents a double diagnostic there. No Decl is passed: the + // init list can appear in a template independently of whether the aggregate + // is one, so checkInitProfileRefToUninit defers on an instantiation-dependent + // source and suppression comes from the parse-time stack. + if (!VerifyOnly && !Result.isInvalid() && + Entity.getKind() == InitializedEntity::EK_Member && Entity.getParent()) + SemaRef.Profiles().checkInitProfileRefToUninitBinding(Src->getExprLoc(), + Entity.getDecl(), DeclType, Src); + UpdateStructuredListElement(StructuredList, StructuredIndex, expr); ++Index; if (AggrDeductionCandidateParamTypes) @@ -6014,6 +6058,17 @@ static void TryOrBuildParenListInitialization( E->getExprLoc(), /*isDirectInit=*/false, E); if (!HandleInitializedEntity(SubEntity, SubKind, E)) return; + + // std::init / ref_to_uninit (paper §5): a pointer element initialized + // from a C++20 parenthesized aggregate list is an element binding, + // checked exactly like the braced-list hook in CheckSubElementType. An + // element cannot carry the marker (null target: unmarked, paper §4.3). + // Only in the build phase, so the verify pass does not double-diagnose; + // the trailing value-initialized filler below has no source expression + // and stays unchecked (value-initialization is null, a non-source). + if (!VerifyOnly) + S.Profiles().checkInitProfileRefToUninitBinding( + E->getExprLoc(), /*Target=*/nullptr, AT->getElementType(), E); } // ...and value-initialized for each k < i <= n; if (ArrayLength > Args.size() || Entity.isVariableLengthArrayNew()) { @@ -6103,6 +6158,15 @@ static void TryOrBuildParenListInitialization( if (!HandleInitializedEntity(SubEntity, SubKind, E)) return; + // std::init / ref_to_uninit (paper §5): a pointer or reference field + // initialized from a C++20 parenthesized aggregate list is a member + // binding, checked exactly like the braced-list hooks in + // CheckSubElementType / CheckReferenceType. Only in the build phase, + // so the verify pass does not double-diagnose. + if (!VerifyOnly) + S.Profiles().checkInitProfileRefToUninitBinding(E->getExprLoc(), FD, + FD->getType(), E); + // Unions should have only one initializer expression, so we bail out // after processing the first field. If there are more initializers then // it will be caught when we later check whether EntityIndexToProcess is @@ -10099,6 +10163,22 @@ Sema::PerformCopyInitialization(const InitializedEntity &Entity, if (ShouldTrackCopy) CurrentParameterCopyTypes.pop_back(); + // std::init / ref_to_uninit (paper §5): parameter copy-initialization is the + // funnel for call arguments from every call form -- GatherArgumentsForCall, + // overloaded operators, and calls to objects of class type -- so the binding + // check runs here, exactly once per argument. A type-only parameter entity + // (a call with no declared callee, e.g. through a function pointer) has no + // declaration that could carry [[ref_to_uninit]], so it is checked as an + // unmarked target (paper §7.2: passing uninitialized memory needs an + // appropriately declared callee). A default argument does not re-run + // copy-initialization at the call site; GatherArgumentsForCall checks those. + if (!Result.isInvalid() && Entity.isParameterKind()) { + const auto *Parm = dyn_cast_or_null(Entity.getDecl()); + Profiles().checkInitProfileRefToUninitBinding( + InitE->getExprLoc(), Parm, Parm ? Parm->getType() : Entity.getType(), + InitE); + } + return Result; } diff --git a/clang/lib/Sema/SemaLambda.cpp b/clang/lib/Sema/SemaLambda.cpp index 8572e3a742a6c..934496dcb6033 100644 --- a/clang/lib/Sema/SemaLambda.cpp +++ b/clang/lib/Sema/SemaLambda.cpp @@ -24,6 +24,7 @@ #include "clang/Sema/SemaARM.h" #include "clang/Sema/SemaCUDA.h" #include "clang/Sema/SemaInternal.h" +#include "clang/Sema/SemaProfiles.h" #include "clang/Sema/SemaOpenMP.h" #include "clang/Sema/SemaSYCL.h" #include "clang/Sema/Template.h" @@ -910,6 +911,16 @@ VarDecl *Sema::createLambdaInitCaptureVarDecl( NewVD->setInitStyle(static_cast(InitStyle)); NewVD->markUsed(Context); NewVD->setInit(Init); + + // std::init / ref_to_uninit (paper §5): an init-capture binds like a + // variable initialization but never reaches AddInitializerToDecl / + // CheckCompleteVariableDeclaration, so check the binding here. A capture + // cannot carry [[ref_to_uninit]], so only the unmarked-target direction can + // fire. Passing NewVD defers on a templated pattern; TreeTransform re-runs + // this path at instantiation. + Profiles().checkInitProfileRefToUninitBinding(Loc, NewVD, NewVD->getType(), + Init, NewVD); + if (NewVD->isParameterPack()) getCurLambda()->LocalPacks.push_back(NewVD); return NewVD; @@ -1502,6 +1513,15 @@ void Sema::ActOnStartOfLambdaDefinition(LambdaIntroducer &Intro, // Attributes on the lambda apply to the method. ProcessDeclAttributes(CurScope, Method, ParamInfo); + // P3589R2: Propagate active profile suppressions to the call operator so + // that generic lambda instantiation (which walks lexical Decl parents, not + // the enclosing stmt tree) can recover them. + if (getLangOpts().Profiles) + for (const auto &E : Profiles().ProfileSuppressStack) + Method->addAttr( + Profiles().makeImplicitProfilesSuppressAttr(E.ProfileName, + E.RuleName)); + if (Context.getTargetInfo().getTriple().isAArch64()) ARM().CheckSMEFunctionDefAttributes(Method); @@ -2261,6 +2281,12 @@ ExprResult Sema::BuildLambdaExpr(SourceLocation StartLoc, assert(From.isVariableCapture() && "unknown kind of capture"); ValueDecl *Var = From.getVariable(); LambdaCaptureKind Kind = From.isCopyCapture() ? LCK_ByCopy : LCK_ByRef; + // std::init / ref_to_uninit (paper §4.3): a by-reference capture binds + // an unmarked reference to the captured variable's storage. An + // init-capture was already checked when its variable was created + // (createLambdaInitCaptureVarDecl). + if (Kind == LCK_ByRef && !From.isInitCapture()) + Profiles().checkInitProfileRefCapture(From.getLocation(), Var); return LambdaCapture(From.getLocation(), IsImplicit, Kind, Var, From.getEllipsisLoc()); } diff --git a/clang/lib/Sema/SemaModule.cpp b/clang/lib/Sema/SemaModule.cpp index 8fc3464ed3e0c..d92524f105a12 100644 --- a/clang/lib/Sema/SemaModule.cpp +++ b/clang/lib/Sema/SemaModule.cpp @@ -18,6 +18,7 @@ #include "clang/Lex/Preprocessor.h" #include "clang/Sema/ParsedAttr.h" #include "clang/Sema/SemaInternal.h" +#include "clang/Sema/SemaProfiles.h" #include "llvm/ADT/ScopeExit.h" #include "llvm/ADT/StringExtras.h" @@ -249,7 +250,8 @@ Sema::DeclGroupPtrTy Sema::ActOnModuleDecl(SourceLocation StartLoc, SourceLocation ModuleLoc, ModuleDeclKind MDK, ModuleIdPath Path, ModuleIdPath Partition, ModuleImportState &ImportState, - bool SeenNoTrivialPPDirective) { + bool SeenNoTrivialPPDirective, + const ParsedAttributesView &Attrs) { assert(getLangOpts().CPlusPlusModules && "should only have module decl in standard C++ modules"); @@ -459,6 +461,18 @@ Sema::ActOnModuleDecl(SourceLocation StartLoc, SourceLocation ModuleLoc, TU->setModuleOwnershipKind(Decl::ModuleOwnershipKind::ReachableWhenImported); TU->setLocalOwningModule(Mod); + // Process [[profiles::enforce]] on the module-declaration. + { + Module *ExportMod = (MDK == ModuleDeclKind::Interface || + MDK == ModuleDeclKind::PartitionInterface) + ? Mod + : nullptr; + for (const auto &AL : Attrs) + if (AL.getKind() == ParsedAttr::AT_ProfilesEnforce && + AL.diagnoseLangOpts(*this)) + Profiles().processProfilesEnforceAttr(AL, ExportMod, nullptr, nullptr); + } + // We are in the module purview, but before any other (non import) // statements, so imports are allowed. ImportState = ModuleImportState::ImportAllowed; @@ -468,6 +482,29 @@ Sema::ActOnModuleDecl(SourceLocation StartLoc, SourceLocation ModuleLoc, if (auto *Listener = getASTMutationListener()) Listener->EnteringModulePurview(); + // P3589R2 [decl.attr.enforce]p4: propagate interface's enforced profiles to + // implementation unit. + if (Interface) { + for (const auto &EP : Interface->EnforcedProfileDesignators) + Profiles().addProfileEnforcement(EP.ProfileName, EP.Designator, + ModuleLoc); + } else if (getLangOpts().Profiles && + MDK == ModuleDeclKind::PartitionImplementation) { + // A partition implementation unit is a module implementation unit of M, so + // the primary interface's enforced profiles apply to it. Unlike a + // non-partition implementation unit it does not implicitly import that + // interface, and by build order M's BMI is usually not built yet here, so + // this is best-effort: inherit only when the interface is already resident; + // never force a load and never diagnose its absence. For guaranteed + // enforcement a partition implementation unit should repeat + // [[profiles::enforce]] (see ProfilesFrameworkInternals.rst). + if (Module *Primary = PP.getHeaderSearchInfo().getModuleMap().findModule( + Mod->getPrimaryModuleInterfaceName())) + for (const auto &EP : Primary->EnforcedProfileDesignators) + Profiles().addProfileEnforcement(EP.ProfileName, EP.Designator, + ModuleLoc); + } + // We already potentially made an implicit import (in the case of a module // implementation unit importing its interface). Make this module visible // and return the import decl to be added to the current TU. @@ -1600,3 +1637,36 @@ void Sema::checkReferenceToTULocalFromOtherTU( PendingCheckReferenceForTULocal.push_back( std::make_pair(FD, PointOfInstantiation)); } + +void Sema::ActOnModuleImportAttrs(Decl *D, + const ParsedAttributesView &Attrs) { + if (!D) + return; + + auto *ID = dyn_cast(D); + Module *ImportedMod = ID ? ID->getImportedModule() : nullptr; + + for (const auto &AL : Attrs) { + if (AL.getKind() != ParsedAttr::AT_ProfilesRequire) + continue; + if (!AL.diagnoseLangOpts(*this)) + continue; + + if (!ImportedMod) { + Diag(AL.getLoc(), diag::err_profiles_require_not_on_import); + continue; + } + + const auto &Args = AL.getProfileRequireArgs(); + StringRef Desig = Args.Designator.Spelling; + + bool Found = llvm::any_of( + ImportedMod->EnforcedProfileDesignators, + [&](const Module::EnforcedProfile &EP) { + return EP.Designator == Desig; + }); + + if (!Found) + Diag(AL.getLoc(), diag::err_profiles_require_not_enforced) << Desig; + } +} diff --git a/clang/lib/Sema/SemaOverload.cpp b/clang/lib/Sema/SemaOverload.cpp index 97018dbe81057..763ad2c5f7e76 100644 --- a/clang/lib/Sema/SemaOverload.cpp +++ b/clang/lib/Sema/SemaOverload.cpp @@ -33,6 +33,7 @@ #include "clang/Sema/SemaARM.h" #include "clang/Sema/SemaCUDA.h" #include "clang/Sema/SemaObjC.h" +#include "clang/Sema/SemaProfiles.h" #include "clang/Sema/Template.h" #include "clang/Sema/TemplateDeduction.h" #include "llvm/ADT/DenseSet.h" @@ -6316,6 +6317,12 @@ ExprResult Sema::PerformImplicitObjectArgumentInitialization( << From->getSourceRange(); } + // std::init / ref_to_uninit (paper §7.2): the implicit object parameter can + // never carry [[ref_to_uninit]], so a member call on an object recognized + // as uninitialized storage is the unmarked-direction violation. + if (getLangOpts().Profiles) + Profiles().checkInitProfileObjectArgument(From, Method); + if (ICS.Standard.Second == ICK_Derived_To_Base) { ExprResult FromRes = PerformObjectMemberConversion(From, Qualifier, FoundDecl, Method); @@ -16777,6 +16784,10 @@ Sema::BuildCallToObjectOfClassType(Scope *S, Expr *Obj, ExprResult Arg = DefaultVariadicArgumentPromotion( Args[i], VariadicCallType::Method, nullptr); IsError |= Arg.isInvalid(); + // std::init / ref_to_uninit (paper §5): a `...` parameter cannot carry + // the marker, so a pointer argument is checked as an unmarked target. + if (!Arg.isInvalid()) + Profiles().checkInitProfileVariadicArgument(Arg.get()); MethodArgs.push_back(Arg.get()); } } diff --git a/clang/lib/Sema/SemaProfiles.cpp b/clang/lib/Sema/SemaProfiles.cpp new file mode 100644 index 0000000000000..d635e62bf01ea --- /dev/null +++ b/clang/lib/Sema/SemaProfiles.cpp @@ -0,0 +1,2280 @@ +//===----- SemaProfiles.cpp --- C++ profiles framework --------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +/// \file +/// This file implements semantic analysis for the C++ profiles framework +/// (P3589R2) and the built-in std::init initialization profile (P4222R1.1): +/// profile enforcement and suppression state, the shared violation gate, and +/// the parse-time std::init rule checks. The CFG-based std::init checks live +/// in AnalysisBasedWarnings.cpp. +/// +//===----------------------------------------------------------------------===// + +#include "clang/Sema/SemaProfiles.h" +#include "clang/AST/Attr.h" +#include "clang/AST/DeclCXX.h" +#include "clang/AST/ExprCXX.h" +#include "clang/AST/ParentMap.h" +#include "clang/Analysis/AnalysisDeclContext.h" +#include "clang/Basic/Builtins.h" +#include "clang/Basic/Module.h" +#include "clang/Basic/SourceManager.h" +#include "clang/Sema/Attr.h" +#include "clang/Sema/ParsedAttr.h" +#include "clang/Sema/Sema.h" + +using namespace clang; + +SemaProfiles::SemaProfiles(Sema &S) : SemaBase(S) {} + + +bool SemaProfiles::isProfileEnforced(StringRef ProfileName) const { + if (!getLangOpts().Profiles) + return false; + // The built-in test:: profiles only exercise the framework; keep them inert + // unless the test suite opts in via -fprofiles-test-profiles. + if (!getLangOpts().ProfilesTestProfiles && ProfileName.starts_with("test::")) + return false; + return getProfileEnforcement(ProfileName) != nullptr; +} + +const SemaProfiles::ProfileEnforcement * +SemaProfiles::getProfileEnforcement(StringRef ProfileName) const { + for (const auto &E : EnforcedProfiles) + if (E.ProfileName == ProfileName) + return &E; + return nullptr; +} + +bool SemaProfiles::addProfileEnforcement(StringRef Name, StringRef Designator, + SourceLocation Loc) { + if (const auto *Existing = getProfileEnforcement(Name)) { + if (Existing->Designator != Designator) { + Diag(Loc, diag::err_profiles_enforce_mismatch) << Name; + Diag(Existing->EnforceLoc, diag::note_previous_attribute); + return false; + } + return true; + } + EnforcedProfiles.push_back({{Name.str(), Designator.str()}, Loc}); + // Mirror the enforcement onto ASTContext so CodeGen can see it: this is the + // one choke point every enforcement source passes through (attribute, module + // import, PCH restore), and SemaProfiles itself does not outlive Sema. + getASTContext().setProfileEnforced(Name); + return true; +} + +// Unzip profile arguments into the parallel key/value/kind arrays that the +// semantic attributes store (Attr.td cannot hold structured arguments). +static void unzipProfileArguments(ArrayRef Arguments, + SmallVectorImpl &Keys, + SmallVectorImpl &Values, + SmallVectorImpl &Kinds) { + for (const auto &Arg : Arguments) { + Keys.push_back(Arg.Key); + Values.push_back(Arg.Value); + Kinds.push_back(static_cast(Arg.Kind)); + } +} + +static void appendProfileArgumentData( + ArrayRef Arguments, + SmallVectorImpl *ArgumentCounts, + SmallVectorImpl *ArgumentKeys, + SmallVectorImpl *ArgumentValues, + SmallVectorImpl *ArgumentKinds) { + if (!ArgumentCounts) + return; + + assert(ArgumentKeys && ArgumentValues && ArgumentKinds); + ArgumentCounts->push_back(Arguments.size()); + unzipProfileArguments(Arguments, *ArgumentKeys, *ArgumentValues, + *ArgumentKinds); +} + +bool SemaProfiles::processProfilesEnforceAttr( + const ParsedAttr &AL, Module *Mod, SmallVectorImpl *NewNames, + SmallVectorImpl *NewDesignators, + SmallVectorImpl *NewArgumentCounts, + SmallVectorImpl *NewArgumentKeys, + SmallVectorImpl *NewArgumentValues, + SmallVectorImpl *NewArgumentKinds) { + const auto &Args = AL.getProfileEnforceArgs(); + if (Args.Designators.empty()) { + Diag(AL.getLoc(), diag::err_attribute_too_few_arguments) << AL << 1; + return false; + } + + for (const auto &D : Args.Designators) { + StringRef Name = D.Name; + StringRef Spelling = D.Spelling; + + // "Already recorded?" must use the ungated lookup: isProfileEnforced + // filters gated-off test:: names (and -fprofiles off), which would make + // every repetition of such a profile look new and re-append its + // designator to the attribute's argument arrays. + bool IsNew = !getProfileEnforcement(Name); + if (!addProfileEnforcement(Name, Spelling, AL.getLoc())) + continue; + + if (Mod && !llvm::any_of(Mod->EnforcedProfileDesignators, + [&](const Module::EnforcedProfile &EP) { + return EP.ProfileName == Name; + })) + Mod->EnforcedProfileDesignators.push_back({Name.str(), Spelling.str()}); + + if (IsNew) { + if (NewNames) + NewNames->push_back(Name); + if (NewDesignators) + NewDesignators->push_back(Spelling); + appendProfileArgumentData(D.Arguments, NewArgumentCounts, + NewArgumentKeys, NewArgumentValues, + NewArgumentKinds); + } + } + return true; +} + +// P3589R2 [decl.attr.enforce]p5: profiles are compatible if they are the same +// -- by name; arguments configure a profile without changing its identity -- +// or proclaimed compatible by the implementation. "All standard profiles are +// compatible with each other" is the one proclamation modeled here. +static bool areProfilesCompatible(StringRef A, StringRef B) { + return A == B || (A.starts_with("std::") && B.starts_with("std::")); +} + +void SemaProfiles::checkRedeclarationProfileCompatibility( + const NamedDecl *New, const NamedDecl *Old) { + if (!getLangOpts().Profiles) + return; + // Only a previous declaration from another module unit (a named module or a + // header unit) can carry a different profile dominion. A textual or PCH + // previous declaration shares this TU's dominion: the placement rule makes + // a TU's dominion uniform over its declarations, and a PCH's enforcements + // are restored into this TU (ENFORCED_PROFILES). + if (!Old->isFromASTFile()) + return; + Module *M = Old->getOwningModule(); + if (!M) + return; + // A declaration in an explicit global-module-fragment precedes the module + // declaration, so the module's exported enforcements do not cover it, and + // its TU's empty-declaration enforcements are not serialized into the BMI. + // Its dominion is unknown: skip rather than guess (a missed diagnostic, + // never a wrong one). An *implicit* global-module-fragment declaration (a + // purview extern "C"/"C++" declaration, the common redeclarable case) sits + // inside the module declaration's dominion and is checked. + if (M->isExplicitGlobalModule()) + return; + Module *Top = M->getTopLevelModule(); + // Within the same module family (an implementation or partition unit seeing + // its interface) the exported set under-approximates the interface TU's + // full dominion, and the interface's enforcements are inherited into this + // unit anyway; skip rather than false-positive on locally added profiles. + if (Module *Current = SemaRef.getCurrentModule()) + if (Current->getTopLevelModule()->getPrimaryModuleInterfaceName() == + Top->getPrimaryModuleInterfaceName()) + return; + + // A gated-off test:: profile is inert in this compilation, on either side. + auto IsActive = [&](StringRef Name) { + return getLangOpts().ProfilesTestProfiles || !Name.starts_with("test::"); + }; + // First active profile in Enforced with no compatible counterpart in + // Covering; empty if fully covered. + auto FindUncovered = [&](const auto &Enforced, + const auto &Covering) -> StringRef { + for (const auto &EP : Enforced) { + StringRef Name = EP.ProfileName; + if (!IsActive(Name)) + continue; + if (llvm::none_of(Covering, [&](const auto &Other) { + return areProfilesCompatible(Name, Other.ProfileName); + })) + return Name; + } + return {}; + }; + + // The rule is symmetric: every profile whose dominion covers one + // declaration must have a compatible counterpart covering the other. + // Report the first violation in each direction. + StringRef MissingHere = + FindUncovered(Top->EnforcedProfileDesignators, EnforcedProfiles); + StringRef MissingThere = + FindUncovered(EnforcedProfiles, Top->EnforcedProfileDesignators); + if (MissingHere.empty() && MissingThere.empty()) + return; + if (!MissingHere.empty()) + Diag(New->getLocation(), diag::err_profiles_redecl_incompatible) + << /*PreviouslyEnforced=*/0 << New << MissingHere << Top->Name; + if (!MissingThere.empty()) + Diag(New->getLocation(), diag::err_profiles_redecl_incompatible) + << /*PreviouslyEnforced=*/1 << New << MissingThere << Top->Name; + Diag(Old->getLocation(), diag::note_previous_declaration); +} + +ProfilesSuppressAttr * +SemaProfiles::makeProfilesSuppressAttr(const ParsedAttr &AL) { + const auto &Args = AL.getProfileSuppressArgs(); + if (Args.Name.empty()) + return nullptr; + + SmallVector RawArgs; + for (const auto &Arg : Args.RawArguments) + RawArgs.push_back(Arg); + SmallVector RawArgumentKeys; + SmallVector RawArgumentValues; + SmallVector RawArgumentKinds; + unzipProfileArguments(Args.Arguments, RawArgumentKeys, RawArgumentValues, + RawArgumentKinds); + + return ::new (getASTContext()) ProfilesSuppressAttr( + getASTContext(), AL, Args.Name, Args.Justification, Args.Rule, + RawArgs.data(), RawArgs.size(), RawArgumentKeys.data(), + RawArgumentKeys.size(), RawArgumentValues.data(), + RawArgumentValues.size(), RawArgumentKinds.data(), + RawArgumentKinds.size()); +} + +ProfilesSuppressAttr * +SemaProfiles::makeImplicitProfilesSuppressAttr(StringRef ProfileName, + StringRef RuleName) { + return ProfilesSuppressAttr::CreateImplicit( + getASTContext(), ProfileName, /*Justification=*/"", RuleName, + /*RawArguments=*/nullptr, /*RawArgumentsSize=*/0, + /*RawArgumentKeys=*/nullptr, /*RawArgumentKeysSize=*/0, + /*RawArgumentValues=*/nullptr, /*RawArgumentValuesSize=*/0, + /*RawArgumentKinds=*/nullptr, /*RawArgumentKindsSize=*/0); +} + +static bool profileSuppressMatches(StringRef EntryProfile, StringRef EntryRule, + StringRef Profile, StringRef Rule) { + return EntryProfile == Profile && + (EntryRule.empty() || EntryRule == Rule); +} + +bool SemaProfiles::isProfileSuppressed(StringRef ProfileName, + StringRef RuleName, + SourceLocation Loc) const { + const SourceManager &SM = getASTContext().getSourceManager(); + for (const auto &E : ProfileSuppressStack) { + if (!profileSuppressMatches(E.ProfileName, E.RuleName, ProfileName, + RuleName)) + continue; + // The entry's dominion is its construct's token range (P3589R2 s2.4p3): + // a violation before the recorded begin -- e.g. in a template pattern + // instantiated synchronously while the scope is live -- is outside it, + // as is one past the recorded end -- e.g. in a pattern first declared + // *after* the suppressed construct. The end is recorded only for a + // construct fully parsed at push time; when it is invalid the scope's + // lifetime bounds the dominion, which is exact mid-parse (later tokens + // are unparsed and instantiation of undefined templates is deferred). + // Fail open on an invalid location on either side + // (isBeforeInTranslationUnit rejects invalid locations), preserving + // plain-liveness behavior for synthesized code. Locations are compared + // in raw TU token order: expansion-loc normalization would collapse all + // tokens of one macro expansion onto the invocation and over-suppress. + if (Loc.isInvalid() || E.Begin.isInvalid() || + (!SM.isBeforeInTranslationUnit(Loc, E.Begin) && + (E.End.isInvalid() || !SM.isBeforeInTranslationUnit(E.End, Loc)))) + return true; + } + return false; +} + +bool SemaProfiles::isProfileSuppressed(StringRef ProfileName, + StringRef RuleName, + const Decl *D) const { + for (; D;) { + for (const auto *PSA : D->specific_attrs()) + if (profileSuppressMatches(PSA->getProfileName(), PSA->getRule(), + ProfileName, RuleName)) + return true; + const DeclContext *DC = D->getLexicalDeclContext(); + D = DC ? dyn_cast(DC) : nullptr; + } + return false; +} + +bool SemaProfiles::isProfileSuppressed(StringRef ProfileName, + StringRef RuleName, const Stmt *S, + AnalysisDeclContext &AC) const { + ParentMap &PM = AC.getParentMap(); + for (const Stmt *Cur = S; Cur; Cur = PM.getParent(Cur)) { + if (const auto *AS = dyn_cast(Cur)) + for (const Attr *A : AS->getAttrs()) + if (const auto *PSA = dyn_cast(A)) + if (profileSuppressMatches(PSA->getProfileName(), PSA->getRule(), + ProfileName, RuleName)) + return true; + // [[profiles::suppress]] on a local variable attaches to the VarDecl, + // not the enclosing DeclStmt. Walk the declared decls so the post-parse + // walker matches the parse-time ProfileSuppressForInit RAII behavior. + if (const auto *DS = dyn_cast(Cur)) + for (const Decl *D : DS->decls()) + for (const auto *PSA : D->specific_attrs()) + if (profileSuppressMatches(PSA->getProfileName(), PSA->getRule(), + ProfileName, RuleName)) + return true; + } + return isProfileSuppressed(ProfileName, RuleName, AC.getDecl()); +} + +// Temporary stopgap for the not-yet-implemented [[profiles::exempt]] (P3589R2 +// s1.1.6): exempt code originating in a system header from profile enforcement, +// so enforcing a profile on a translation unit does not diagnose violations +// inside the standard library and the other system headers it transitively +// includes. On by default; -fno-profiles-exempt-system-headers restores +// spec-exact enforcement into system-header code. Remove once +// [[profiles::exempt]] has wording and a real implementation. +static bool isExemptSystemHeaderLoc(const ASTContext &Ctx, + const LangOptions &LangOpts, + SourceLocation Loc) { + return LangOpts.ProfilesExemptSystemHeaders && Loc.isValid() && + Ctx.getSourceManager().isInSystemHeader(Loc); +} + +bool SemaProfiles::shouldEmitProfileViolation(StringRef ProfileName, + StringRef RuleName, + SourceLocation Loc) { + return shouldEmitProfileViolation(ProfileName, RuleName, Loc, /*D=*/nullptr); +} + +bool SemaProfiles::shouldEmitProfileViolation(StringRef ProfileName, + StringRef RuleName, + SourceLocation Loc, + const Decl *D) { + if (!isProfileEnforced(ProfileName)) + return false; + if (isExemptSystemHeaderLoc(getASTContext(), getLangOpts(), Loc)) + return false; + // Honor [[profiles::suppress]] from the parse-time stack and, when a Decl is + // available, from the declaration and its lexical parents. The latter does + // not depend on a parse-time scope still being active, so finalization checks + // that run after the parse scope is torn down still respect suppression. + // + // The stack consult is dominion-checked against Loc: a check that fires + // while an unrelated construct's ProfileSuppressScope is live -- a + // synchronously instantiated pattern, or a class finalized as a side effect + // of one -- matches only entries whose construct's tokens cover Loc + // (P3589R2 s2.4p3), so no explicit finalization or instantiation guard is + // needed here. + if (isProfileSuppressed(ProfileName, RuleName, Loc) || + isProfileSuppressed(ProfileName, RuleName, D)) + return false; + // P3589R2 Section 1.1: "its static semantic effects are as-if applied only + // after translation phase 7. It is not possible for a profile to change the + // outcome of overload resolution or template instantiation, nor is it + // possible to 'SFINAE out' failure of a program to satisfy a profile + // requirement." + // + // A templated entity is not yet a phase-7 entity, so a Decl-carrying rule + // fires only on its instantiation -- where D is the instantiated, + // non-templated declaration -- not on the template pattern (the [[uninit]] + // marker checks and the binding checks that pass a Decl are re-run on the + // instantiated field / variable, so they defer here). Checking the pattern + // too would double-fire, once at parse and again per instantiation. + // + // The Decl-less expression check sites (D == nullptr here) instead defer + // from their own entry points, and only when their check-relevant operands + // are instantiation-dependent -- exactly the constructs TreeTransform + // always rebuilds, so the hosting Build* routine re-runs the deferred check + // at instantiation. A fully non-dependent construct may be returned + // unchanged by TreeTransform (its Build* never re-runs), so it is checked + // at definition time instead; when such a construct is rebuilt at + // instantiation anyway (a local operand, a call argument, a return), the + // definition-time diagnostic repeats there -- accepted for now. This + // deliberately trades strict phase-7 purity for reuse-proof diagnostics: a + // non-dependent violation in a never-instantiated template, or in an + // if-constexpr branch not yet known to be discarded, diagnoses at + // definition time -- the same model the test::type_cast profile and the + // reinterpret_cast check follow. + if (D && D->isTemplated()) + return false; + if (SemaRef.isUnevaluatedContext()) + return false; + if (SemaRef.currentEvaluationContext().isDiscardedStatementContext()) + return false; + return true; +} + +bool SemaProfiles::shouldEmitProfileViolation(StringRef ProfileName, + StringRef RuleName, + const Stmt *UseStmt, + AnalysisDeclContext &AC) const { + if (!isProfileEnforced(ProfileName)) + return false; + SourceLocation Loc = + UseStmt ? UseStmt->getBeginLoc() : AC.getDecl()->getLocation(); + if (isExemptSystemHeaderLoc(getASTContext(), getLangOpts(), Loc)) + return false; + if (isProfileSuppressed(ProfileName, RuleName, UseStmt, AC)) + return false; + return true; +} + +bool SemaProfiles::checkProfileViolation(StringRef ProfileName, + StringRef RuleName, SourceLocation Loc, + unsigned DiagID) { + if (!shouldEmitProfileViolation(ProfileName, RuleName, Loc)) + return false; + Diag(Loc, DiagID) << ProfileName; + return true; +} + +void SemaProfiles::ProfileSuppressScope::push(StringRef ProfileName, + StringRef RuleName, + SourceLocation Begin, + SourceLocation End) { + S.Profiles().ProfileSuppressStack.push_back( + {ProfileName, RuleName, Begin, End}); + ++Count; +} + +SemaProfiles::ProfileSuppressScope::ProfileSuppressScope( + Sema &S, const ParsedAttributesView &Attrs) + : S(S) { + if (!S.getLangOpts().Profiles) + return; + for (const auto &AL : Attrs) { + if (AL.getKind() != ParsedAttr::AT_ProfilesSuppress) + continue; + const auto &Args = AL.getProfileSuppressArgs(); + // These are the prefix attributes of a statement or declaration about to + // be parsed, so the attribute's own location is the construct's begin and + // no end is known yet (the scope's lifetime bounds it). + if (!Args.Name.empty()) + push(Args.Name, Args.Rule, AL.getLoc(), SourceLocation()); + } +} + +/// The end location of \p D's construct if it is fully parsed, invalid +/// otherwise. A partially parsed construct's end location is usually *valid +/// but early* -- a mid-parse class collapses to its name token (the brace +/// range is set only by ActOnTagFinishDefinition, after even the late-parsed +/// members), a body-pending function ends at its declarator, an +/// uninitialized variable at its declarator -- so each arm gates on the +/// marker that the construct's real end has been seen. Returning invalid +/// falls back to scope-lifetime bounding, which is exact mid-parse. +static SourceLocation getCompletedConstructEnd(const Decl *D) { + if (const auto *TD = dyn_cast(D)) + return TD->getBraceRange().getEnd(); + if (const auto *FD = dyn_cast(D)) { + // isLateTemplateParsed makes doesThisDeclarationHaveABody true while the + // body is merely token-cached and the range still ends at the declarator. + if (FD->doesThisDeclarationHaveABody() && !FD->isLateTemplateParsed()) + return FD->getSourceRange().getEnd(); + return SourceLocation(); + } + if (const auto *VD = dyn_cast(D)) { + if (VD->hasInit()) + return VD->getSourceRange().getEnd(); + return SourceLocation(); + } + if (const auto *FD = dyn_cast(D)) { + // The in-class initializer expression is null while its late parse is + // still pending. + if (FD->hasNonNullInClassInitializer()) + return FD->getInClassInitializer()->getEndLoc(); + return SourceLocation(); + } + if (const auto *ND = dyn_cast(D)) + return ND->getRBraceLoc(); + return SourceLocation(); +} + +void SemaProfiles::ProfileSuppressScope::addFromDecl(const Decl *D) { + SourceLocation Begin = D->getBeginLoc(); + if (Begin.isInvalid()) + Begin = D->getLocation(); + SourceLocation End = getCompletedConstructEnd(D); + for (const auto *A : D->specific_attrs()) + push(A->getProfileName(), A->getRule(), Begin, End); +} + +SemaProfiles::ProfileSuppressScope::ProfileSuppressScope(Sema &S, const Decl *D, + bool WalkLexicalParents) + : S(S) { + if (!S.getLangOpts().Profiles || !D) + return; + addFromDecl(D); + if (WalkLexicalParents) { + for (const DeclContext *DC = D->getLexicalDeclContext(); DC; + DC = DC->getLexicalParent()) + if (const auto *Parent = dyn_cast(DC)) + addFromDecl(Parent); + } +} + +SemaProfiles::ProfileSuppressScope::ProfileSuppressScope(Sema &S, + ArrayRef Attrs, + SourceLocation Begin, + SourceLocation End) + : S(S) { + if (!S.getLangOpts().Profiles) + return; + for (const auto *A : Attrs) + if (const auto *PSA = dyn_cast(A)) + push(PSA->getProfileName(), PSA->getRule(), Begin, End); +} + +SemaProfiles::ProfileSuppressScope::~ProfileSuppressScope() { + assert(S.Profiles().ProfileSuppressStack.size() >= Count); + S.Profiles().ProfileSuppressStack.pop_back_n(Count); +} + +// True if a field of RD -- or, transitively, of an anonymous-record member +// of RD -- carries a default member initializer. Default-initializing a +// union performs the initialization of its single variant member with a +// default member initializer ([class.union.general]p6; at most one variant +// may have one), and initializers written on the leaves of an +// anonymous-record variant activate it the same way. +static bool anyLeafHasNSDMI(const CXXRecordDecl *RD) { + if (!RD->hasDefinition() || RD->isInvalidDecl()) + return false; + for (const FieldDecl *F : RD->fields()) { + if (F->isUnnamedBitField()) + continue; + if (F->hasInClassInitializer()) + return true; + if (F->isAnonymousStructOrUnion()) + if (const auto *FRD = F->getType()->getAsCXXRecordDecl()) + if (anyLeafHasNSDMI(FRD)) + return true; + } + return false; +} + +static bool defaultInitLeavesScalarIndeterminateImpl( + ASTContext &Ctx, QualType T, bool HonorUninitMarkers, + llvm::SmallPtrSetImpl &Visited) { + if (T->isDependentType() || T->isIncompleteType()) + return false; + if (const ArrayType *AT = Ctx.getAsArrayType(T)) + return defaultInitLeavesScalarIndeterminateImpl( + Ctx, AT->getElementType(), HonorUninitMarkers, Visited); + if (T->isReferenceType()) + return false; + const auto *RD = T->getAsCXXRecordDecl(); + if (!RD) + // Scalars, pointers, and enums are left indeterminate by default-init, + // except std::byte, which the profile permits to be uninitialized + // (paper §4), so a std::byte subobject does not make a record + // indeterminate. + return T->isScalarType() && !T->isStdByteType(); + if (RD->isInvalidDecl()) + return false; + // A union's members are mutually exclusive, so the per-member walk below does + // not apply. Default-initialization leaves it without an initialized member + // (paper §6.5) unless it has a user-provided default constructor (trusted), + // a default member initializer initializes one, or it has no members -- + // unnamed bit-fields are not members ([class.bit]) and cannot be named or + // initialized, so a union of only unnamed bit-fields counts as empty. + if (RD->isUnion()) { + if (RD->hasUserProvidedDefaultConstructor()) + return false; + bool AnyMember = false; + bool AllStdByte = true; + for (const FieldDecl *F : RD->fields()) { + if (F->isUnnamedBitField()) + continue; + AnyMember = true; + if (F->hasInClassInitializer()) + return false; + // Default member initializers on the leaves of an anonymous-record + // member activate that variant during default-initialization, so the + // union is indeterminate iff the activated variant itself still is + // (returning at the first match is sound: at most one variant may + // carry default member initializers). + if (F->isAnonymousStructOrUnion()) + if (const auto *FRD = F->getType()->getAsCXXRecordDecl(); + FRD && anyLeafHasNSDMI(FRD)) + return defaultInitLeavesScalarIndeterminateImpl( + Ctx, F->getType(), HonorUninitMarkers, Visited); + if (!Ctx.getBaseElementType(F->getType())->isStdByteType()) + AllStdByte = false; + } + // A union of only std::byte members (arrays included) mirrors the scalar + // exemption above: the profile permits std::byte to be uninitialized + // (paper §4.5), so whichever member is active needs no acknowledgement. + if (!AnyMember || AllStdByte) + return false; + // Deliberately not exempted: a member whose type avoids indeterminacy + // only through a trusted user-provided default constructor (union { + // Trusted t; }) -- the union's default-initialization activates no + // member, so that constructor never runs. + return true; + } + // Break cycles from ill-formed self-containing types (e.g. struct S {S x;}). + if (!Visited.insert(RD->getCanonicalDecl()).second) + return false; + // Trust a user-provided default constructor: ctor_uninit_member checks at its + // definition. + if (RD->hasUserProvidedDefaultConstructor()) + return false; + for (const CXXBaseSpecifier &Base : RD->bases()) + if (defaultInitLeavesScalarIndeterminateImpl(Ctx, Base.getType(), + HonorUninitMarkers, Visited)) + return true; + for (const FieldDecl *F : RD->fields()) { + if (F->isUnnamedBitField() || F->hasInClassInitializer()) + continue; + // A member the type's author marked [[uninit]] is acknowledged as + // intentionally uninitialized, so it does not leave an unacknowledged + // scalar indeterminate (paper §6.2). + if (HonorUninitMarkers && F->hasAttr()) + continue; + if (defaultInitLeavesScalarIndeterminateImpl(Ctx, F->getType(), + HonorUninitMarkers, Visited)) + return true; + } + return false; +} + +bool SemaProfiles::defaultInitLeavesScalarIndeterminate(QualType T, + bool HonorUninitMarkers) { + llvm::SmallPtrSet Visited; + return defaultInitLeavesScalarIndeterminateImpl(getASTContext(), T, + HonorUninitMarkers, Visited); +} + +bool SemaProfiles::defaultInitIsVacuous(QualType T) { + QualType BaseTy = getASTContext().getBaseElementType(T); + if (const auto *RD = BaseTy->getAsCXXRecordDecl()) { + // A non-trivial default constructor (user-provided anywhere in the + // subtree, a default member initializer, a virtual table pointer) + // initializes something, contradicting an [[uninit]] marker (paper §4.2 + // rule 2, §5.3). hasTrivialDefaultConstructor asserts without a + // definition. + if (!RD->hasDefinition() || !RD->hasTrivialDefaultConstructor()) + return false; + // A deleted default constructor keeps the triviality bit, but makes + // default-initialization ill-formed rather than a no-op: the entity can + // never be left default-initialized, so the marker is unsatisfiable. Only + // a declared deleted constructor is visible here; a lazily *implicitly* + // deleted one of an otherwise trivial type escapes this scan -- a missed + // diagnostic (never a false positive), like the framework's other + // conservative omissions. + for (const CXXConstructorDecl *Ctor : RD->getDefinition()->ctors()) + if (Ctor->isDefaultConstructor() && Ctor->isDeleted()) + return false; + } + // The factual (HonorUninitMarkers=false) walk: an all-scalars-determinate + // type (e.g. an empty struct) has nothing uninitialized, so the marker + // contradicts it too, while a type whose only indeterminate scalars are + // themselves marked members really is left uninitialized. + return defaultInitLeavesScalarIndeterminate(T, /*HonorUninitMarkers=*/false); +} + +// Whether the declaration initializer \p Init is a vacuous +// default-initialization of \p T: one that runs no code and leaves the object +// factually uninitialized, hence consistent with an [[uninit]] marker. No +// initializer at all (a scalar default-init synthesizes none) is vacuous; a +// synthesized trivial default-constructor call is vacuous iff the type's +// default-initialization is (defaultInitIsVacuous); anything else -- a +// user-written initializer, a `= P()` value-initialization (a +// CXXTemporaryObjectExpr, which zeroes), any zero-initializing construction +// -- initializes the object and contradicts the marker. The shared guard of +// static_marker and uninit_with_initializer keeps the pair complementary by +// construction: exactly one of the two fires for a marked static. +static bool isVacuousDefaultInit(SemaProfiles &SP, const Expr *Init, + QualType T) { + if (!Init) + return true; + if (const auto *CCE = dyn_cast(Init->IgnoreImplicit())) + return CCE->getConstructor()->isDefaultConstructor() && + !isa(CCE) && + !CCE->requiresZeroInitialization() && SP.defaultInitIsVacuous(T); + return false; +} + +void SemaProfiles::checkInitProfileUninitDecl(const VarDecl *Var) { + // std::init / uninit_decl: a definition without any initializer (after + // attempted default-initialization) must either carry [[uninit]] or + // be initialized by a language rule. Static / thread storage duration is + // excluded -- those are zero-initialized; runtime-init concerns are R3's. + static constexpr StringRef Profile = "std::init"; + static constexpr StringRef Rule = "uninit_decl"; + // The enforcement check gates the (possibly recursive) type walk below so + // it runs only under the profile, not on every default-initialized + // variable. + QualType BaseTy = getASTContext().getBaseElementType(Var->getType()); + if (!Var->isInvalidDecl() && Var->getStorageDuration() == SD_Automatic && + !Var->hasAttr() && + // std::byte may be left uninitialized (paper §4), so it -- and arrays + // of it -- are exempt from this rule. + !BaseTy->isStdByteType() && + shouldEmitProfileViolation(Profile, Rule, Var->getLocation(), Var) && + // A definition with no initializer (scalar / pointer / enum, or an + // array of them), or a class/aggregate type -- possibly the element + // type of an array -- whose default-init leaves a scalar subobject + // indeterminate (its synthesized constructor call provides an + // initializer, so the !getInit() test alone misses it). + (!Var->getInit() || + (BaseTy->isRecordType() && + defaultInitLeavesScalarIndeterminate(Var->getType(), + /*HonorUninitMarkers=*/true)))) { + // A union variable cannot carry [[uninit]] (union_marker bans it), + // so it must be initialized; use a message that does not suggest the + // marker as a remedy. + bool IsUnion = BaseTy->isUnionType(); + Diag(Var->getLocation(), + IsUnion ? diag::err_init_uninit_union : diag::err_init_uninit_decl) + << Profile << Var->getDeclName(); + } +} + +void SemaProfiles::checkInitProfileStaticMarker(const VarDecl *Var) { + // std::init / static_marker: a variable with static or thread storage + // duration is zero-initialized by language rule (paper §3), so it is an + // initialized object; marking it [[uninit]] contradicts paper §4.2 ("an + // initialized object marked [[uninit]] is an error"). The case with a real + // initializer -- explicit, or a default-initialization that is not a no-op + // -- is already caught by uninit_with_initializer (R4, in + // CheckCompleteVariableDeclaration); this covers the vacuous-initialization + // case R4 treats as consistent. The guard is the shared + // isVacuousDefaultInit, so the pair stays complementary by construction and + // exactly one of static_marker / uninit_with_initializer fires (this one + // runs first, from ActOnUninitializedDecl, after the synthesized + // default-initialization is attached). + static constexpr StringRef Profile = "std::init"; + QualType BaseTy = getASTContext().getBaseElementType(Var->getType()); + if (!Var->isInvalidDecl() && + (Var->getStorageDuration() == SD_Static || + Var->getStorageDuration() == SD_Thread) && + Var->hasAttr() && + // A union or pointer object -- or an array of them -- marked [[uninit]] + // is already rejected by union_marker / pointer_marker (regardless of + // storage duration, and keyed on the same base element type), and they + // retain the marker; do not pile a second diagnostic on top. + !BaseTy->isUnionType() && !BaseTy->isPointerType() && + shouldEmitProfileViolation(Profile, "static_marker", Var->getLocation(), + Var) && + isVacuousDefaultInit(*this, Var->getInit(), Var->getType())) { + bool IsThread = Var->getStorageDuration() == SD_Thread; + Diag(Var->getLocation(), diag::err_init_uninit_static_marker) + << Profile << Var->getDeclName() << IsThread; + } +} + +bool SemaProfiles::checkInitProfileStaticRuntimeInit( + const VarDecl *Var, llvm::function_ref CheckConstInit) { + // Thread-locals have thread (not static) storage duration; paper §3 scopes + // this rule to non-local *static* objects (uninit_decl likewise excludes + // thread storage). + if (Var->getTLSKind() != VarDecl::TLS_None) + return false; + // std::init / static_runtime_init: paper says non-local statics must be + // initialized at compile or link time. CheckConstInit() permits trivial + // default initialization (not a constant initializer but needs no global + // constructor), so a zero-initialized aggregate such as + // `struct S { int x; }; S g;` is not a violation. Runs before + // -Wglobal-constructors so the profile error (when enforced) takes + // precedence over the standalone warning. + static constexpr StringRef Profile = "std::init"; + static constexpr StringRef Rule = "static_runtime_init"; + // Gate on enforcement before evaluating the initializer: this call site + // sits ahead of -Wglobal-constructors' isIgnored guard, so evaluating + // first would charge every global with a non-constant initializer for the + // constant-initializer evaluation even with profiles disabled. + if (!shouldEmitProfileViolation(Profile, Rule, Var->getLocation(), Var)) + return false; + if (CheckConstInit()) + return false; + Diag(Var->getLocation(), diag::err_init_static_runtime_init) + << Profile << Var->getDeclName(); + return true; +} + +void SemaProfiles::checkInitProfileUninitWithInitializer(const ValueDecl *D, + const Expr *Init) { + // [[uninit]] documents that the entity is intentionally left + // uninitialized, so it contradicts an explicit initializer. A RecoveryExpr + // is a placeholder for an initialization that already failed (e.g. + // default-init of a const scalar), not an initializer the user wrote, so it + // must not trigger this rule. + if (!D->hasAttr() || !Init || + isa(Init->IgnoreParens())) + return; + SourceLocation Loc = D->getLocation(); + static constexpr StringRef Profile = "std::init"; + static constexpr StringRef Rule = "uninit_with_initializer"; + // Gate the (possibly recursive) type walk below on enforcement. + if (!shouldEmitProfileViolation(Profile, Rule, Loc, D)) + return; + // A vacuous default-initialization -- a synthesized trivial + // default-constructor call that runs no code and leaves the object + // indeterminate -- is consistent with the marker: the object really is left + // uninitialized, to be initialized later (e.g. via construct_at), mirroring + // the scalar case. Anything else -- an explicit initializer, a `= P()` + // value-initialization, or a default-initialization that initializes + // something (a non-trivial default constructor, paper §4.2 rule 2, §5.3) -- + // contradicts it. + if (isVacuousDefaultInit(*this, Init, D->getType())) + return; + Diag(Loc, diag::err_init_uninit_with_initializer) + << Profile << D->getDeclName() << isa(D); +} + +void SemaProfiles::checkInitProfileMarkerPlacement(const Decl *D) { + const auto *UA = D->getAttr(); + if (!UA) + return; + SourceLocation Loc = UA->getLocation(); + + // std::init / union_marker (paper §5.6): the marker is banned on a union + // object or a union member, because delayed initialization by assigning a + // member would be an erroneous assignment when compiled without the profile. + // std::init / pointer_marker (paper §4.1): "a reference cannot be + // uninitialized. The initialization profile requires the same for pointers." + // A pointer must instead be initialized (e.g. to nullptr). Both are profile + // policy (not a meaningless subject), so they are gated on enforcement; the + // marker is left in place so uninit_decl / ctor_uninit_member treat the + // entity as acknowledged rather than re-diagnosing it. + // + // Passing \p D makes shouldEmitProfileViolation defer on a templated pattern + // (paper / P3589R2: a rule fires on the instantiation, not the template), + // so the parse-time handler skips template members and the rule is re-run on + // the instantiated entity (VisitFieldDecl / VisitVarDecl), once the + // substituted type is known to be a pointer or union. + // + // Both rules key on the base element type: an array of unions or pointers + // leaves the same uninitialized elements as a single one, and the marker + // would otherwise slip past uninit_decl (which trusts marked declarations) + // entirely. The union rule also covers a union-typed data member of a + // non-union class -- delayed initialization by assigning its member is just + // as erroneous there (paper §5.6). + QualType BaseTy = + getASTContext().getBaseElementType(cast(D)->getType()); + bool UnionMember = + isa(D) && cast(D)->getParent()->isUnion(); + if ((BaseTy->isUnionType() || UnionMember) && + shouldEmitProfileViolation("std::init", "union_marker", Loc, D)) + Diag(Loc, diag::err_init_union_marker) + << "std::init" << (UnionMember ? 1 : isa(D) ? 2 : 0); + else if (BaseTy->isPointerType() && + shouldEmitProfileViolation("std::init", "pointer_marker", Loc, D)) + Diag(Loc, diag::err_init_uninit_pointer_marker) << "std::init"; +} + +bool SemaProfiles::diagnoseInvalidUninitMarker(const Decl *D, + SourceLocation AttrLoc, + bool Diagnose) { + const auto *VD = dyn_cast(D); + if (!VD) + return false; + QualType T = VD->getType(); + + // A dependent subject is validated at instantiation instead, once the + // substituted type is known (Sema::InstantiateAttrs). + if (T->isDependentType()) + return false; + + if (T->isReferenceType()) { + if (Diagnose) + Diag(AttrLoc, diag::err_uninit_attr_invalid_subject) << /*Reference=*/0u; + return true; + } + return false; +} + +bool SemaProfiles::diagnoseInvalidRefToUninitMarker(const Decl *D, + SourceLocation AttrLoc, + bool Diagnose) { + QualType T; + if (const auto *FD = dyn_cast(D)) + T = FD->getReturnType(); + else + T = cast(D)->getType(); + + // A dependent subject is validated at instantiation instead, once the + // substituted type is known (Sema::InstantiateAttrs). + if (T->isDependentType()) + return false; + + if ((!T->isPointerType() && !T->isReferenceType()) || + T->isFunctionPointerType() || T->isFunctionReferenceType()) { + if (Diagnose) + Diag(AttrLoc, diag::err_ref_to_uninit_attr_invalid_type); + return true; + } + return false; +} + +// std::init / ref_to_uninit (paper §5). Two mutually-recursive local +// recognizers over the syntactic form of a source expression -- no flow +// analysis and no type-system tracking. Uninitialized storage is only ever +// introduced by an explicit [[uninit]] / [[ref_to_uninit]] marker. +// +// The classification is tri-state: a recognized form is Initialized or +// Uninitialized, while an unrecognized one (pointer arithmetic, an +// integer-to-pointer cast, a call through a function pointer) is Unknown rather +// than assumed Initialized. Callers wanting a plain "is it uninitialized?" +// answer (SemaProfiles::refersToUninitializedMemory, the read-through check) +// treat +// Unknown as not uninitialized. +// +// How the classified expression is being accessed is carried by +// UninitAccessOpts below. +enum class UninitStorage { Initialized, Uninitialized, Unknown }; + +// How an expression is being used, for the uninit recognizers. +// +// DropTopLevelUninit: a *directly named* [[uninit]] entity does not count as +// uninitialized. A value access of such an entity is owned elsewhere: a named +// [[uninit]] object by the CFG uninit_read pass, a current-object member by +// the ctor-body pass, an [[uninit]] member of a constructor-less aggregate +// local by the local-aggregate pass (all three credit assignments), and a +// marked member of an object with a user-provided constructor reached through +// any other object is deliberately trusted (paper §5.1: its constructor body +// may have assigned it, which local analysis cannot see). So the read-through +// check must not second-guess them -- and for a store, writing the whole +// named entity IS its initialization (paper §4.5: for a built-in type, a +// write is its initialization). The flag is cleared at the first *deeper* +// subobject step (a member's member, an array element), where no flow pass +// tracks the storage and only whole-object construct_at could re-initialize +// (paper §5.4). +// +// TrustRefToUninit: [[ref_to_uninit]] markers are ignored -- the storage +// reached through a marked pointer/reference (or returned by a marked +// function) classifies as Unknown rather than Uninitialized. Stores use this: +// a scalar write through the marker is the pointee's initialization (paper +// §4.5), and verifying class-type writes (construct_at) is a deferred slice, +// so a store through the marker must be neither banned nor endorsed. +// SubscriptBase: the classification runs below an element access (p[i]), +// where pointee store credit must not apply: element-wise state is +// untrackable by design (paper §5.4/§5.5 ban random access through the +// marker), so only the whole-`*p` form is ever credited. Purely syntactic: +// p[0] is not credited even though it denotes the same storage as *p. +// +// Credit: when non-null, the recognizers consult the parse-order store +// credit recorded by SemaProfiles::recordInitProfileStore -- a credited +// entity classifies as Initialized. Null in the constexpr presets; the +// checking entry points attach it via withCredit. +struct UninitAccessOpts { + bool DropTopLevelUninit = false; + bool TrustRefToUninit = false; + bool SubscriptBase = false; + const SemaProfiles *Credit = nullptr; + + UninitAccessOpts withoutTopLevelDrop() const { + return {false, TrustRefToUninit, SubscriptBase, Credit}; + } + UninitAccessOpts withSubscriptBase() const { + return {DropTopLevelUninit, TrustRefToUninit, true, Credit}; + } + UninitAccessOpts withCredit(const SemaProfiles *SP) const { + return {DropTopLevelUninit, TrustRefToUninit, SubscriptBase, SP}; + } +}; + +// Presets: a binding source (markers count everywhere), a value read (the +// top-level drop applies), and a scalar store (additionally, storage reached +// through [[ref_to_uninit]] is trusted). +constexpr UninitAccessOpts UninitBindAccess{}; +constexpr UninitAccessOpts UninitReadAccess{/*DropTopLevelUninit=*/true}; +constexpr UninitAccessOpts UninitWriteAccess{/*DropTopLevelUninit=*/true, + /*TrustRefToUninit=*/true}; + +// Combine the arms of a conditional: Uninitialized dominates (either arm may be +// taken), then Unknown, else Initialized. +static UninitStorage combineArms(UninitStorage A, UninitStorage B) { + if (A == UninitStorage::Uninitialized || B == UninitStorage::Uninitialized) + return UninitStorage::Uninitialized; + if (A == UninitStorage::Unknown || B == UninitStorage::Unknown) + return UninitStorage::Unknown; + return UninitStorage::Initialized; +} + +static UninitStorage +glvalueDenotesUninitStorage(ASTContext &Ctx, const Expr *E, + UninitAccessOpts Opts = UninitBindAccess); + +const ValueDecl *SemaProfiles::getDirectlyNamedDecl(const Expr *E) { + E = E->IgnoreParenImpCasts(); + if (const auto *DRE = dyn_cast(E)) + return DRE->getDecl(); + if (const auto *ME = dyn_cast(E)) + return ME->getMemberDecl(); + return nullptr; +} + +// True if E denotes the current object: `this` (the implicit/explicit pointer +// of an arrow access) or `*this` (the object lvalue of a dot access). A local +// twin of AnalysisBasedWarnings.cpp's isCurrentObjectBase (the CFG passes' +// helper); each file keeps its recognizer vocabulary self-contained. +static bool isCurrentObjectExpr(const Expr *E) { + E = E->IgnoreParenImpCasts(); + if (isa(E)) + return true; + const auto *UO = dyn_cast(E); + return UO && UO->getOpcode() == UO_Deref && + isa(UO->getSubExpr()->IgnoreParenImpCasts()); +} + +// The parse-time pattern of \p FD -- the declaration whose body statements +// the current function can share. TreeTransform hands a statement back +// *unchanged* when nothing in it needs rebuilding, so a fully non-dependent +// `this->m = 5` inside a generic lambda (or a lambda in a member function +// template) runs Sema -- and earns its parse-order credit -- only once, +// while the pattern is parsed; the instantiated call operator reuses the +// statement wholesale. Keying current-object member credit on the pattern +// makes record and consult agree whether a statement was reused +// (pattern-time credit) or rebuilt (the instantiation-time key normalizes +// to the same pattern). Iterated because each transform hop adds one link: +// a generic lambda in a member function template reaches its parsed pattern +// via a member-specialization link and then a primary-template link (a +// local twin of SemaLambda.cpp's getPatternFunctionDecl). Instantiations of +// one pattern share its key -- and its statements, so the parse order the +// credit approximates is the same for all of them; a store only one +// sibling instantiation rebuilds (e.g. under a dependent `if constexpr`) +// then credits the others too, a parse-order-style missed diagnostic, never +// a false positive. +static const FunctionDecl *getParseTimePattern(const FunctionDecl *FD) { + while (FD) { + // A local function without template machinery of its own, instantiated + // while transforming an enclosing templated body. (A transformed lambda + // call operator instead carries a member-specialization link and a + // generic lambda's specialization a primary-template link -- both + // resolved by the pattern walk below.) + if (FD->getTemplatedKind() == FunctionDecl::TK_DependentNonTemplate) { + const FunctionDecl *P = FD->getInstantiatedFromDecl(); + if (!P) + return FD; + FD = P; + continue; + } + const FunctionDecl *P = FD->getTemplateInstantiationPattern(); + if (!P || P == FD) + return FD; + FD = P; + } + return FD; +} + +const Decl *SemaProfiles::resolveMemberStoreBase(const MemberExpr *ME) const { + const Expr *Base = ME->getBase()->IgnoreParenImpCasts(); + // The current object: this->m, the implicit m, or (*this).m. Keyed on the + // enclosing function declaration's parse-time pattern (`this` cannot be + // reseated, so the key is stable for the whole body; the *pattern*, so + // that a statement an instantiation reuses from its pattern and a rebuilt + // one agree on the key -- see getParseTimePattern); AllowLambda gives a + // lambda body inside a member function its own key, so its stores and the + // enclosing function's never share credit. No current function (e.g. an + // NSDMI parse) is untrackable. + if (isCurrentObjectExpr(Base)) + return getParseTimePattern( + SemaRef.getCurFunctionDecl(/*AllowLambda=*/true)); + // A directly named local object: a dot access on a local-storage, + // non-reference VarDecl (a by-value parameter is its own object and + // qualifies). A reference base is an alias to an object also reachable + // under other names, and an arrow base reaches the object through an + // arbitrary (reseatable) pointer value -- both untrackable per object, the + // same aliasing boundary that keeps fields of parameter-reached objects + // uncredited. A deeper base (a.b.m) is §5.4's rejected deep + // delayed-initialization tracking. + if (!ME->isArrow()) + if (const auto *DRE = dyn_cast(Base)) + if (const auto *VD = dyn_cast(DRE->getDecl()); + VD && VD->hasLocalStorage() && !VD->getType()->isReferenceType()) + return VD; + return nullptr; +} + +// The directly named [[ref_to_uninit]] local/parameter *pointer* of \p E, if +// any: the only pointer entity whose pointee state is tracked (the credit +// map keys on VarDecls; a marked member pointer is the pinned per-object +// aliasing boundary -- copies share pointees). Shared by the store-recording +// deref arm and the [[now_init]] argument shapes. +static const VarDecl *getCreditableMarkedPointer(const Expr *E) { + const auto *VD = + dyn_cast_or_null(SemaProfiles::getDirectlyNamedDecl(E)); + if (VD && VD->hasLocalStorage() && VD->getType()->isPointerType() && + VD->hasAttr()) + return VD; + return nullptr; +} + +// Pass-through forms shared by the pointer and glvalue recognizers, which are +// transparent to their operand: a single-element braced initializer { e } +// binds from e (modeling +// MismatchingNewDeleteDetector::getNewExprFromInitListOrExpr); a conditional +// is uninit if either arm is, so a value that may be uninit forces a marked +// target; a comma yields its right operand. \p EmptyListState classifies an +// empty braced list: {} value-initializes a pointer to null, which the +// pointer recognizer classifies Unknown (the null policy at its null arm), +// while a glvalue has no empty-list form (Unknown); a multi-element list is +// Unknown for both. Returns std::nullopt when E is not a pass-through form. +template +static std::optional +classifyUninitPassThrough(const Expr *E, UninitStorage EmptyListState, + RecurseFn Recurse) { + if (const auto *ILE = dyn_cast(E)) { + if (ILE->getNumInits() == 1) + return Recurse(ILE->getInit(0)); + return ILE->getNumInits() == 0 ? EmptyListState : UninitStorage::Unknown; + } + if (const auto *CO = dyn_cast(E)) + return combineArms(Recurse(CO->getTrueExpr()), + Recurse(CO->getFalseExpr())); + if (const auto *BO = dyn_cast(E); BO && BO->isCommaOp()) + return Recurse(BO->getRHS()); + return std::nullopt; +} + +// A call to a [[ref_to_uninit]]-returning function yields uninitialized +// storage (the pointed-to memory, or the returned referent) -- deferred to +// Unknown when the marker is trusted (a store). Known allocator callees are +// classified the same way without a marker (paper §4.3: functions like +// malloc "must be known to an analyzer enforcing the initialization +// profile"): the malloc and alloca families return uninitialized memory, +// calloc returns zero-initialized memory, and realloc returns a preserved +// prefix plus an indeterminate tail -- affirmatively neither, so Unknown. +// The builtin ID is absent under -fno-builtin / -ffreestanding (or a +// non-matching declaration), where an allocator falls back to the trusted +// default below -- a missed diagnostic, never a false positive. Any other +// unmarked direct callee is trusted Initialized (paper §4.3); a call with +// no direct callee (through a function pointer) is Unknown. Shared by both +// recognizers. +static UninitStorage classifyRefToUninitCallee(const CallExpr *CE, + UninitAccessOpts Opts) { + const FunctionDecl *FD = CE->getDirectCallee(); + if (!FD) + return UninitStorage::Unknown; + bool RefersToUninit = FD->hasAttr(); + // A direct call to a replaceable global allocation function -- raw + // ::operator new / ::operator new[] -- returns uninitialized memory like + // malloc (a new-*expression* is the recognizers' CXXNewExpr arm instead). + // The operator check excludes operator delete, and replaceability excludes + // class-specific overloads, whose semantics belong to their class. + if (FD->getDeclName().getCXXOverloadedOperator() == OO_New || + FD->getDeclName().getCXXOverloadedOperator() == OO_Array_New) + RefersToUninit |= FD->isReplaceableGlobalAllocationFunction(); + switch (FD->getBuiltinID()) { + case Builtin::BImalloc: + case Builtin::BI__builtin_malloc: + case Builtin::BIaligned_alloc: + case Builtin::BIalloca: + case Builtin::BI__builtin_alloca: + case Builtin::BI__builtin_alloca_uninitialized: + case Builtin::BI__builtin_alloca_with_align: + case Builtin::BI__builtin_alloca_with_align_uninitialized: + case Builtin::BI__builtin_operator_new: + RefersToUninit = true; + break; + case Builtin::BIcalloc: + case Builtin::BI__builtin_calloc: + return UninitStorage::Initialized; + case Builtin::BIrealloc: + case Builtin::BI__builtin_realloc: + return UninitStorage::Unknown; + default: + break; + } + if (!RefersToUninit) + return UninitStorage::Initialized; + return Opts.TrustRefToUninit ? UninitStorage::Unknown + : UninitStorage::Uninitialized; +} + +// \p E is a pointer prvalue. Classifies whether it points to uninitialized +// storage. +static UninitStorage +pointerRefersToUninitStorage(ASTContext &Ctx, const Expr *E, + UninitAccessOpts Opts = UninitBindAccess) { + if (!E) + return UninitStorage::Unknown; + E = E->IgnoreParenImpCasts(); + + // An empty braced list value-initializes a pointer to null, so it takes the + // null classification below (Unknown), keeping `= {}` and `= nullptr` + // consistent. + if (auto PassThrough = classifyUninitPassThrough( + E, /*EmptyListState=*/UninitStorage::Unknown, + [&](const Expr *Sub) { + return pointerRefersToUninitStorage(Ctx, Sub, Opts); + })) + return *PassThrough; + + // A null pointer refers to no object, so it is consistent with both a + // marked target (the marker means "zero or more uninitialized objects", + // paper §8) and an unmarked one (paper §4.3's f1(p2) example): Unknown, + // which neither direction diagnoses. A dedicated UninitStorage::Null state + // was considered and deferred -- behaviorally identical today; add it only + // when a future rule (e.g. construct_at on null) needs to distinguish null + // from unclassifiable. + if (E->isNullPointerConstant(Ctx, Expr::NPC_ValueDependentIsNotNull) != + Expr::NPCK_NotNull) + return UninitStorage::Unknown; + + // Array-to-pointer decay has been stripped above, leaving the array glvalue. + // Clear the top-level drop here, like the member-access arm: neither the CFG + // uninit_read pass nor the ctor-body pass tracks array elements, and + // element-wise delayed initialization of an [[uninit]] array is itself + // banned (paper §5.5), so below an element access the marker counts even + // for a value access. + if (E->getType()->isArrayType()) + return glvalueDenotesUninitStorage(Ctx, E, Opts.withoutTopLevelDrop()); + + // &G, where G denotes uninitialized storage. + if (const auto *UO = dyn_cast(E)) + if (UO->getOpcode() == UO_AddrOf) + return glvalueDenotesUninitStorage(Ctx, UO->getSubExpr(), Opts); + + // A value of a [[ref_to_uninit]] pointer is Uninitialized (Unknown when the + // marker is trusted); an unmarked named pointer is a trusted Initialized + // pointer (paper §4.3). + if (const ValueDecl *VD = SemaProfiles::getDirectlyNamedDecl(E)) { + if (!VD->hasAttr()) { + // An unmarked *local* whose declaration initializer is null -- a null + // pointer constant, or an empty braced list, which value-initializes to + // null -- is a null source like the literal (paper §4.3's f1(p2) + // example): Unknown. Reassignment after the null init is a documented, + // accepted missed diagnostic (parse-order leniency). Deliberately + // excluded: globals/extern (an extern pointer may be initialized + // elsewhere, and keeping them Initialized preserves the + // marked-direction diagnostics), null-NSDMI fields, and parameters -- + // a ParmVarDecl's getInit() is its *default argument*, which is not + // the parameter's value on most calls. A *marked* decl keeps its + // marker classification below (respect the explicit marker). + if (const auto *Var = dyn_cast(VD); + Var && Var->hasLocalStorage() && !isa(Var)) { + if (const Expr *Init = Var->getInit()) { + const Expr *InnerInit = Init->IgnoreParenImpCasts(); + const auto *ILE = dyn_cast(InnerInit); + if ((ILE && ILE->getNumInits() == 0) || + InnerInit->isNullPointerConstant( + Ctx, Expr::NPC_ValueDependentIsNotNull) != Expr::NPCK_NotNull) + return UninitStorage::Unknown; + } + } + return UninitStorage::Initialized; + } + // Parse-order store credit: after a whole-`*p` store, the marked + // pointer's pointee counts as initialized (paper §4.3: "p no longer + // refers to uninitialized memory") for further whole-`*p` accesses -- + // until the pointer is reseated, which clears the credit. The consult + // sits before the TrustRefToUninit branch (under the write preset the + // outcome merely changes Unknown to Initialized, both "not + // Uninitialized": no preset regression) and is skipped below an element + // access (SubscriptBase), preserving §5.4's random-access ban. + if (Opts.Credit && !Opts.SubscriptBase && + Opts.Credit->hasPointeeStoreCredit(VD)) + return UninitStorage::Initialized; + return Opts.TrustRefToUninit ? UninitStorage::Unknown + : UninitStorage::Uninitialized; + } + if (const auto *CE = dyn_cast(E)) + return classifyRefToUninitCallee(CE, Opts); + + // A default-initialized new-expression (none init style: no initializer + // written) whose allocated type's default-initialization leaves a scalar + // subobject indeterminate produces uninitialized free-store memory (paper + // §1.2/§4.3), like a [[ref_to_uninit]] allocator. The style gates this + // rather + // than hasInitializer(), which is also true for new Agg -- default- + // initializing a class synthesizes a (possibly trivial) constructor call. + // new T(...) / new T{} are value- or list-initialized; a user-provided + // default constructor is trusted by defaultInitLeavesScalarIndeterminate. + if (const auto *NE = dyn_cast(E)) { + if (NE->getInitializationStyle() != CXXNewInitializationStyle::None) + return UninitStorage::Initialized; + llvm::SmallPtrSet Visited; + return defaultInitLeavesScalarIndeterminateImpl(Ctx, NE->getAllocatedType(), + /*HonorUninitMarkers=*/true, + Visited) + ? UninitStorage::Uninitialized + : UninitStorage::Initialized; + } + + // Paper §4.3: a [[ref_to_uninit]] pointer cast to another pointer type is + // itself [[ref_to_uninit]]. Implicit casts were already stripped above, so + // this only looks through an explicit pointer-to-pointer cast; a pointer + // manufactured from an integer (operand not a pointer) is Unknown. + if (const auto *CE = dyn_cast(E)) + if (CE->getSubExpr()->getType()->isPointerType()) + return pointerRefersToUninitStorage(Ctx, CE->getSubExpr(), Opts); + + return UninitStorage::Unknown; +} + +// \p E is a glvalue. Classifies whether it denotes uninitialized storage. +static UninitStorage glvalueDenotesUninitStorage(ASTContext &Ctx, const Expr *E, + UninitAccessOpts Opts) { + if (!E) + return UninitStorage::Unknown; + E = E->IgnoreParenImpCasts(); + + if (auto PassThrough = classifyUninitPassThrough( + E, /*EmptyListState=*/UninitStorage::Unknown, + [&](const Expr *Sub) { + return glvalueDenotesUninitStorage(Ctx, Sub, Opts); + })) + return *PassThrough; + + // A named entity denotes uninitialized storage if it is [[uninit]], or + // if it is a reference marked [[ref_to_uninit]] (the glvalue is its referent, + // which is uninitialized). A [[ref_to_uninit]] *pointer* named here denotes + // the pointer object itself -- which is initialized -- so it does not count. + // Under the top-level drop the [[uninit]] arm is skipped: a value access of + // a directly named [[uninit]] object is the flow-based passes' territory, so + // only a [[ref_to_uninit]] reference (or indirection, handled below) still + // counts. Parse-order store credit clears both arms: a whole-entity store + // is the [[uninit]] entity's initialization (paper §4.2/§4.5), and a store + // through a marked reference initializes its referent (§4.3; references + // cannot be reseated, so that credit never lapses). + auto DeclDenotesUninit = [&](const ValueDecl *VD) { + return (!Opts.DropTopLevelUninit && VD->hasAttr() && + !(Opts.Credit && Opts.Credit->hasWholeObjectStoreCredit(VD))) || + (!Opts.TrustRefToUninit && VD->getType()->isReferenceType() && + VD->hasAttr() && + !(Opts.Credit && Opts.Credit->hasPointeeStoreCredit(VD))); + }; + if (const auto *DRE = dyn_cast(E)) + return DeclDenotesUninit(DRE->getDecl()) ? UninitStorage::Uninitialized + : UninitStorage::Initialized; + if (const auto *ME = dyn_cast(E)) { + // a->m reaches m through the pointer a (object *a); a.m through the + // glvalue a. When m does not itself denote uninit storage, the subobject is + // uninit exactly when its base is. The base recursion clears the top-level + // drop: the drop exists because a directly named [[uninit]] entity's value + // accesses are owned by the flow passes or deliberately trusted (see the + // UninitAccessOpts comment above), but nothing tracks a subobject reached + // through a *further* member access -- and member-wise delayed + // initialization of an [[uninit]] object is itself banned (paper §5.4; + // only whole-object construct_at re-initializes, which is uniformly + // unmodeled) -- so below the top level the marker counts for every access. + // + // Parse-order member store credit: after `a.m = 5` / `this->m = 5`, the + // marked member of that *specific* base object counts as initialized + // (paper §4.2: "After initialization, the object is no longer + // [[uninit]]"; §6: assignment initializes a built-in) -- covering both + // `a.m` (a reference binding lands in this arm directly) and `&a.m` (the + // UO_AddrOf arm recurses here). The consult keys on the same base + // identity the recording resolved, so the same member observed through + // any other object -- including a copy (§5.2: a copy does not inherit + // credit) -- stays uncredited. It applies at any chain depth: the map + // only ever holds whole-member stores (which initialize the entire + // member), and in practice only scalar members (see + // recordInitProfileStore), which have no subobjects to chain through. + const ValueDecl *MD = ME->getMemberDecl(); + if (const auto *F = dyn_cast(MD); + F && Opts.Credit && F->hasAttr() && + Opts.Credit->hasMemberStoreCredit( + Opts.Credit->resolveMemberStoreBase(ME), F)) + return UninitStorage::Initialized; + if (DeclDenotesUninit(MD)) + return UninitStorage::Uninitialized; + return ME->isArrow() ? pointerRefersToUninitStorage( + Ctx, ME->getBase(), Opts.withoutTopLevelDrop()) + : glvalueDenotesUninitStorage( + Ctx, ME->getBase(), Opts.withoutTopLevelDrop()); + } + // A call to a [[ref_to_uninit]]-returning reference function: the referent + // it returns is uninitialized. + if (const auto *CE = dyn_cast(E)) + return classifyRefToUninitCallee(CE, Opts); + // An element access classifies like its base, but pointee store credit + // must not apply below it (SubscriptBase): `*p = 5;` never legalizes + // `p[1]` -- the pointee may be an array with only element 0 written, and + // element-wise state is untrackable by design (paper §5.4/§5.5). + if (const auto *ASE = dyn_cast(E)) + return pointerRefersToUninitStorage(Ctx, ASE->getBase(), + Opts.withSubscriptBase()); + + // *p, where p points to uninitialized storage. + if (const auto *UO = dyn_cast(E)) + if (UO->getOpcode() == UO_Deref) + return pointerRefersToUninitStorage(Ctx, UO->getSubExpr(), Opts); + + // A reference cast (an explicit cast yielding a glvalue) denotes the same + // storage as its operand; propagate. Symmetric to the pointer-cast arm. + if (const auto *CE = dyn_cast(E)) + if (CE->getSubExpr()->isGLValue()) + return glvalueDenotesUninitStorage(Ctx, CE->getSubExpr(), Opts); + + return UninitStorage::Unknown; +} + +// Dispatches a binding source to the pointer or glvalue recognizer. +static UninitStorage +classifyUninitSource(ASTContext &Ctx, const Expr *E, bool IsReference, + UninitAccessOpts Opts = UninitBindAccess) { + return IsReference ? glvalueDenotesUninitStorage(Ctx, E, Opts) + : pointerRefersToUninitStorage(Ctx, E, Opts); +} + +bool SemaProfiles::refersToUninitializedMemory(const Expr *E, + bool IsReference) const { + return classifyUninitSource(getASTContext(), E, IsReference, + UninitBindAccess.withCredit(this)) == + UninitStorage::Uninitialized; +} + +void SemaProfiles::checkInitProfileRefToUninit(SourceLocation Loc, + bool TargetIsRefToUninit, + bool IsReference, const Expr *Src, + const Decl *D) { + // A RecoveryExpr is a placeholder for an initialization that already failed, + // not a source the user wrote, so it must not drive this rule. + if (!Src || isa(Src->IgnoreParens())) + return; + // An instantiation-dependent source cannot be classified yet; its construct + // is always rebuilt at instantiation, re-running this funnel with the + // substituted source. A non-dependent source is checked here, at definition + // time; if the construct is rebuilt at instantiation anyway (a local + // operand, a call argument, a return), the same diagnostic repeats there -- + // accepted for now. Decl-carrying callers are exempt: they defer via the + // D->isTemplated() check in shouldEmitProfileViolation and fire on the + // instantiated declaration. + if (!D && Src->isInstantiationDependent()) + return; + static constexpr StringRef Profile = "std::init"; + static constexpr StringRef Rule = "ref_to_uninit"; + if (!shouldEmitProfileViolation(Profile, Rule, Loc, D)) + return; + // Parse-order credit is recorded once and never rewound, so when an + // instantiation re-walks a statement the pattern already checked, the + // re-check runs against post-pattern state -- including credit this very + // statement recorded (a reused [[now_init]] argument, a this-member store + // keyed to the pattern). For the requires-uninit direction that would turn + // the pattern's pass into a false "must refer to uninitialized memory" at + // instantiation, so a marked target classifies without credit while + // instantiating: the reverse direction diagnoses at definition time, and a + // violation established only by credit in fully dependent code is a missed + // diagnostic -- the usual parse-order trade, never a false positive. + // Direct classification (an initialized global, a marked entity) is + // unaffected, so credit-free reverse violations still repeat per + // instantiation, and the accepting direction keeps credit everywhere (a + // deferred binding needs the instantiation-time record to pass). + UninitStorage SrcState = classifyUninitSource( + getASTContext(), Src, IsReference, + TargetIsRefToUninit && SemaRef.inTemplateInstantiation() + ? UninitBindAccess + : UninitBindAccess.withCredit(this)); + unsigned IsRef = IsReference ? 1 : 0; + // A marked target is a violation only against an affirmatively Initialized + // source: an Unknown one (pointer arithmetic, an integer-to-pointer cast, a + // call through a function pointer) cannot be proven initialized, so rejecting + // it would be a false positive. An unmarked target is diagnosed only against + // an affirmatively Uninitialized source (Unknown stays a missed diagnostic). + if (TargetIsRefToUninit && SrcState == UninitStorage::Initialized) + Diag(Loc, diag::err_init_ref_to_uninit_requires_uninit) << Profile << IsRef; + else if (!TargetIsRefToUninit && SrcState == UninitStorage::Uninitialized) + Diag(Loc, diag::err_init_uninit_requires_ref_to_uninit) << Profile << IsRef; +} + +void SemaProfiles::checkInitProfileRefToUninitBinding(SourceLocation Loc, + const ValueDecl *Target, QualType T, + const Expr *Src, const Decl *D) { + if (!getLangOpts().Profiles || T.isNull() || T->isDependentType() || + (!T->isPointerType() && !T->isReferenceType())) + return; + // A null Target is a binding site with no declaration to carry the marker + // (a parameter of a call through a function pointer): always unmarked. + checkInitProfileRefToUninit(Loc, + Target && Target->hasAttr(), + T->isReferenceType(), Src, D); + // Run after the check: the binding itself is judged against the + // *pre-call* state, and only then does a [[now_init]] callee's promised + // initialization -- or a [[now_uninit]] callee's promised destruction -- + // take effect for what follows in parse order. The withdrawal runs before + // the credit so a callee carrying both attributes (a reinitializer) nets + // to destroy-then-construct: the storage is initialized after the call. + recordNowUninitArgument(Target, T, Src); + recordNowInitArgument(Target, T, Src); +} + +void SemaProfiles::recordNowInitArgument(const ValueDecl *Target, QualType T, + const Expr *Src) { + // Only the binding of a [[ref_to_uninit]] parameter of a [[now_init]] + // function carries the callee's initialization promise (P4222R2 §6.2: the + // attribute "would apply to every [[ref_to_uninit]] argument"). A variadic + // argument, an unmarked parameter, or a call through a function pointer + // presents no marked ParmVarDecl and earns nothing. + const auto *Parm = dyn_cast_or_null(Target); + if (!Parm || !Src || !Parm->hasAttr()) + return; + const auto *FD = dyn_cast_or_null(Parm->getDeclContext()); + if (!FD || !FD->hasAttr()) + return; + // Like recordInitProfileStore: no enforcement, suppression, or in-template + // gate (a suppressed or pattern-parsed call still initializes; rebuilt + // instantiations re-record against fresh declarations), but a call in a + // never-executed context earns no credit. + if (inNeverExecutedContext()) + return; + recordLifetimeAnnotatedArgument(T, Src, /*Withdraw=*/false); +} + +void SemaProfiles::recordNowUninitArgument(const ValueDecl *Target, QualType T, + const Expr *Src) { + // The mirror of recordNowInitArgument: a [[now_uninit]] callee ends the + // lifetime of the storage bound to each of its pointer/reference + // parameters (P4222R2 §4.4's missing destroy_at recording). The + // parameters are *unmarked* -- destruction takes initialized memory -- so + // the gate keys on the parameter's type, not a marker; the shape walk is + // marker-keyed on the source side, so an ordinary initialized argument + // withdraws nothing. A variadic argument or a call through a function + // pointer presents no ParmVarDecl and withdraws nothing (stale credit is + // a missed diagnostic, never a false positive -- the same boundary as + // [[now_init]]). + const auto *Parm = dyn_cast_or_null(Target); + if (!Parm || !Src) + return; + const auto *FD = dyn_cast_or_null(Parm->getDeclContext()); + if (!FD || !FD->hasAttr()) + return; + // Not enforcement- or suppression-gated (a suppressed destroy still + // destroys), but a call in a never-executed context destroys nothing. + if (inNeverExecutedContext()) + return; + recordLifetimeAnnotatedArgument(T, Src, /*Withdraw=*/true); +} + +// Add or remove a credit bit: a [[now_init]] callee's initialization adds +// it, a [[now_uninit]] callee's destruction withdraws it (removal of an +// absent entry leaves a harmless zero entry behind). +static void applyCreditBit(unsigned &Entry, unsigned Bit, bool Withdraw) { + if (Withdraw) + Entry &= ~Bit; + else + Entry |= Bit; +} + +void SemaProfiles::recordLifetimeAnnotatedArgument(QualType T, const Expr *Src, + bool Withdraw) { + const Expr *E = Src->IgnoreParenImpCasts(); + // Mirror the recognizers' explicit-cast pass-through (paper §4.3: a cast + // of a marked pointer is itself marked; a reference cast denotes the same + // storage): the callee initializes the same storage either way. + while (const auto *CE = dyn_cast(E)) { + const Expr *Sub = CE->getSubExpr(); + if (!Sub->getType()->isPointerType() && !Sub->isGLValue()) + break; + E = Sub->IgnoreParenImpCasts(); + } + // The glvalue whose storage the callee initializes: the operand of &G for + // a pointer parameter, or the bound glvalue itself for a reference one. + const Expr *Glvalue = nullptr; + if (const auto *UO = dyn_cast(E); + UO && UO->getOpcode() == UO_AddrOf) + Glvalue = UO->getSubExpr()->IgnoreParenImpCasts(); + else if (T->isReferenceType()) + Glvalue = E; + if (Glvalue) { + // &base.m / base.m: per-object whole-member credit, under exactly the + // member-store keys (resolveMemberStoreBase); an untrackable base -- a + // parameter-reached object, a deeper chain -- resolves null and stays + // strict, the same boundary as a direct store. + if (const auto *ME = dyn_cast(Glvalue)) { + if (const auto *F = dyn_cast(ME->getMemberDecl()); + F && F->hasAttr()) + if (const Decl *Base = resolveMemberStoreBase(ME)) + applyCreditBit(MemberStoreCredit[{Base, F}], WholeStored, Withdraw); + return; + } + // &*p / *p: the callee initializes (or destroys) the pointee of a + // marked pointer. + if (const auto *UO = dyn_cast(Glvalue); + UO && UO->getOpcode() == UO_Deref) { + if (const VarDecl *VD = getCreditableMarkedPointer(UO->getSubExpr())) + applyCreditBit(InitStoreCredit[VD], PointeeStored, Withdraw); + return; + } + if (const auto *VD = + dyn_cast_or_null(getDirectlyNamedDecl(Glvalue)); + VD && VD->hasLocalStorage()) { + // &u / u: the whole [[uninit]] entity is initialized (or destroyed) + // by the callee, exactly as `u = e` would credit it. + if (VD->hasAttr()) + applyCreditBit(InitStoreCredit[VD], WholeStored, Withdraw); + // r (a marked reference bound onward): its referent is initialized; a + // reference cannot be reseated, so no store ever clears the credit -- + // only a [[now_uninit]] callee's withdrawal does. + else if (VD->getType()->isReferenceType() && + VD->hasAttr()) + applyCreditBit(InitStoreCredit[VD], PointeeStored, Withdraw); + } + return; + } + // p as a pointer value: the callee initializes (or destroys) p's pointee + // -- §6.2's initialize2(p) example verbatim. Only a directly named marked + // local/parameter pointer is trackable; reseating p afterwards clears + // pointee credit like any other. + if (const VarDecl *VD = getCreditableMarkedPointer(E)) + applyCreditBit(InitStoreCredit[VD], PointeeStored, Withdraw); +} + +void SemaProfiles::checkInitProfileVariadicArgument(const Expr *Arg) { + // std::init / ref_to_uninit (paper §5): a variadic argument never reaches + // parameter copy-initialization, and a `...` parameter cannot carry + // [[ref_to_uninit]], so a pointer passed through it is checked as an + // unmarked target (paper §7.2: passing uninitialized memory needs an + // appropriately declared callee). Value reads of the promoted argument + // already funnel through the lvalue-to-rvalue chokepoint; the pointer + // binding is the only direction added here. Called from the two C++ + // variadic promotion loops -- Sema::GatherArgumentsForCall and + // Sema::BuildCallToObjectOfClassType (functors, variadic lambdas) -- not + // from Sema::DefaultVariadicArgumentPromotion itself, whose other callers + // re-promote already-promoted arguments (the os_log builtin check) or + // promote during ObjC method matching, where a check would double-fire. + if (!getLangOpts().Profiles || !Arg || !Arg->getType()->isPointerType()) + return; + checkInitProfileRefToUninit(Arg->getExprLoc(), /*TargetIsRefToUninit=*/false, + /*IsReference=*/false, Arg); +} + +void SemaProfiles::checkInitProfileRefCapture(SourceLocation Loc, + const ValueDecl *Var) { + if (!getLangOpts().Profiles) + return; + // Mirrors the glvalue recognizer's named-entity arm: the captured variable + // denotes uninitialized storage if it is [[uninit]], or if it is a + // [[ref_to_uninit]] reference (the capture binds to its referent) -- in + // both cases unless parse-order store credit says it has been initialized + // (u = 5; then a by-ref capture of u is accepted, symmetric with &u). A + // copy capture is not this check's: it reads the variable in the enclosing + // function's CFG, which is the flow-based uninit_read pass's territory. + bool UninitNoCredit = + Var->hasAttr() && !hasWholeObjectStoreCredit(Var); + bool RefNoCredit = Var->getType()->isReferenceType() && + Var->hasAttr() && + !hasPointeeStoreCredit(Var); + if (!UninitNoCredit && !RefNoCredit) + return; + // The only Expr-less deferral here: an instantiation-dependent captured + // type defers to instantiation, where TreeTransform's unconditional lambda + // rebuild re-processes the capture. A concrete capture fires at definition + // time and repeats on that same rebuild -- accepted for now. + if (Var->getType()->isInstantiationDependentType()) + return; + if (!shouldEmitProfileViolation("std::init", "ref_to_uninit", Loc)) + return; + Diag(Loc, diag::err_init_uninit_ref_capture) << "std::init" << Var; +} + +void SemaProfiles::checkInitProfileObjectArgument(const Expr *Object, + const CXXMethodDecl *Method) { + // A RecoveryExpr is a placeholder for an expression that already failed, not + // an object argument the user wrote, so it must not drive this rule. + if (!getLangOpts().Profiles || !Object || + isa(Object->IgnoreParens())) + return; + // Destroying uninitialized storage is the deferred destroy_at slice (the + // paper models destruction, like construct_at, as a lifetime operation); + // implicit scope-exit destructions never reach this funnel, so diagnosing + // only the explicit s.~S() spelling would be an inconsistent sliver. + if (isa(Method)) + return; + // A static call operator (C++23) has no implicit object parameter: its + // object argument is evaluated but its value is never used, exactly like a + // static member function named through an object -- whose call path never + // reaches this funnel. BuildCallToObjectOfClassType converts the object + // argument for static call operators all the same, so skip them here. + if (Method->isStatic()) + return; + // An instantiation-dependent object argument cannot be classified yet; its + // call is always rebuilt at instantiation, re-running this funnel with the + // substituted object. A non-dependent call fires at definition time and + // repeats if the call is rebuilt at instantiation anyway -- accepted. + if (Object->isInstantiationDependent()) + return; + if (!shouldEmitProfileViolation("std::init", "ref_to_uninit", + Object->getExprLoc())) + return; + // An arrow call's object argument arrives as the pointer expression, a dot + // call's as the object glvalue; dispatch the recognizer accordingly. + bool IsPointer = Object->getType()->isPointerType(); + if (!refersToUninitializedMemory(Object, /*IsReference=*/!IsPointer)) + return; + Diag(Object->getExprLoc(), diag::err_init_member_call_on_uninit) + << "std::init" << Method; +} + +// The read-through diagnostic distinguishes indirection through a +// [[ref_to_uninit]] pointer/reference from a subobject read of a named +// [[uninit]] object, which involves no [[ref_to_uninit]] entity. Approximate +// but sufficient for phrasing: walk up the dot member / array-element chain; +// the read is the latter form iff the chain reaches an [[uninit]]-marked +// member (e.g. the class-type member in this->agg.f) or bottoms out at a +// named [[uninit]] declaration. An arrow access or a subscript on a pointer +// reaches its object through a pointer, so the pointer wording applies from +// there on. +static bool isMemberChainOfUninitObject(const Expr *E) { + E = E->IgnoreParenImpCasts(); + while (true) { + if (const auto *ME = dyn_cast(E)) { + if (ME->getMemberDecl()->hasAttr()) + return true; + if (ME->isArrow()) + return false; + E = ME->getBase()->IgnoreParenImpCasts(); + continue; + } + // a[i] and *a on an array glvalue (decay stripped below) are subobject + // accesses like a dot member access; on a pointer base they reach the + // object through the pointer. + if (const auto *ASE = dyn_cast(E)) { + const Expr *Base = ASE->getBase()->IgnoreParenImpCasts(); + if (!Base->getType()->isArrayType()) + return false; + E = Base; + continue; + } + if (const auto *UO = dyn_cast(E); + UO && UO->getOpcode() == UO_Deref) { + const Expr *Sub = UO->getSubExpr()->IgnoreParenImpCasts(); + if (!Sub->getType()->isArrayType()) + return false; + E = Sub; + continue; + } + break; + } + const auto *DRE = dyn_cast(E); + return DRE && DRE->getDecl()->hasAttr(); +} + +void SemaProfiles::checkInitProfileReadThrough(SourceLocation Loc, + const Expr *Glvalue, + QualType ValueType) { + // A RecoveryExpr is a placeholder for an expression that already failed, not + // a read the user wrote, so it must not drive this rule. + if (!Glvalue || isa(Glvalue->IgnoreParens())) + return; + // An instantiation-dependent glvalue cannot be classified yet; its read is + // always rebuilt at instantiation, where this check re-runs with the + // substituted operand. A non-dependent read fires at definition time and + // repeats if the read is rebuilt at instantiation anyway -- accepted. + if (Glvalue->isInstantiationDependent()) + return; + // Paper §4.5: reading an uninitialized std::byte is permitted. + if (getASTContext().getBaseElementType(ValueType)->isStdByteType()) + return; + if (!shouldEmitProfileViolation("std::init", "uninit_read", Loc)) + return; + if (glvalueDenotesUninitStorage(getASTContext(), Glvalue, + UninitReadAccess.withCredit(this)) != + UninitStorage::Uninitialized) + return; + Diag(Loc, diag::err_init_uninit_read_through) + << "std::init" << (isMemberChainOfUninitObject(Glvalue) ? 1 : 0); +} + +void SemaProfiles::checkInitProfileSubobjectWrite(SourceLocation Loc, + const Expr *LHS) { + // A RecoveryExpr is a placeholder for an expression that already failed, not + // a store the user wrote, so it must not drive this rule. + if (!LHS || isa(LHS->IgnoreParens())) + return; + // An instantiation-dependent store target cannot be classified yet; its + // assignment is always rebuilt at instantiation, where this check re-runs + // with the substituted LHS. A non-dependent store fires at definition time + // and repeats if the assignment is rebuilt at instantiation anyway -- + // accepted. + if (LHS->isInstantiationDependent()) + return; + // Paper §4.5: an uninitialized std::byte may be manipulated freely. + if (getASTContext().getBaseElementType(LHS->getType())->isStdByteType()) + return; + if (!shouldEmitProfileViolation("std::init", "uninit_write", Loc)) + return; + if (glvalueDenotesUninitStorage(getASTContext(), LHS, + UninitWriteAccess.withCredit(this)) != + UninitStorage::Uninitialized) + return; + Diag(Loc, diag::err_init_uninit_subobject_write) + << "std::init" << !isa(LHS->IgnoreParenImpCasts()); +} + +void SemaProfiles::checkInitProfilePointerAssignment(Expr *LHS, Expr *RHS, + SourceLocation OpLoc) { + // References cannot be reseated, so only pointer assignment applies. The + // marker is read when the LHS directly names a pointer entity; any other + // lvalue (e.g. *pp, arr[i]) cannot carry a local marker, so it is the + // default unmarked pointer (paper §4.3) and must not be bound to + // uninitialized memory. + if (!LHS->getType()->isPointerType()) + return; + // An instantiation-dependent LHS (e.g. an unresolved member access) has no + // readable marker yet -- getDirectlyNamedDecl would report it unmarked, a + // false positive when the instantiated entity is [[ref_to_uninit]]. The + // assignment is rebuilt at instantiation, where the marker is concrete. + // The source's dependence is the funnel's to defer on. + if (LHS->isInstantiationDependent()) + return; + const ValueDecl *VD = getDirectlyNamedDecl(LHS); + checkInitProfileRefToUninit(OpLoc, VD && VD->hasAttr(), + /*IsReference=*/false, RHS); +} + +void SemaProfiles::checkInitProfileAssignmentOperands(BinaryOperatorKind Opc, + Expr *LHSExpr, + bool IsCompound, + SourceLocation OpLoc) { + // A compound assignment reads the old value but builds no lvalue-to-rvalue + // node for it, so the DefaultLvalueConversion read-through chokepoint never + // sees the load; check it here. The shift forms are the exception: + // CheckShiftOperands promotes their LHS through DefaultLvalueConversion, + // which has already fired for them. + if (IsCompound && Opc != BO_ShlAssign && Opc != BO_ShrAssign) + checkInitProfileReadThrough(LHSExpr->getExprLoc(), LHSExpr, + LHSExpr->getType()); + checkInitProfileSubobjectWrite(OpLoc, LHSExpr); +} + +void SemaProfiles::checkInitProfileIncDec(Expr *Operand, SourceLocation OpLoc) { + // ++/-- reads the old value with no lvalue-to-rvalue node (unlike -x or + // !x), then stores to its operand like an assignment does to its LHS. + checkInitProfileReadThrough(Operand->getExprLoc(), Operand, + Operand->getType()); + checkInitProfileSubobjectWrite(OpLoc, Operand); + // Record last: the pre-store checks above must see pre-store state (there + // is no RHS). ++u credits the whole entity; ++p reseats a marked pointer. + recordInitProfileStore(Operand); +} + +bool SemaProfiles::inNeverExecutedContext() const { + return SemaRef.isUnevaluatedContext() || + SemaRef.currentEvaluationContext().isDiscardedStatementContext(); +} + +void SemaProfiles::recordInitProfileStore(const Expr *LHS) { + if (!getLangOpts().Profiles || !LHS) + return; + // A store in an unevaluated or discarded-statement context never executes + // (mirroring shouldEmitProfileViolation's context checks), so it earns no + // credit. There is deliberately no enforcement or [[profiles::suppress]] + // gate -- a suppressed store still initializes; failing to credit it would + // turn suppression into later false positives -- and no in-template gate: + // non-dependent code in a template is checked at definition time and must + // find pattern-time credit (instantiations rebuild their DeclRefExprs + // against fresh decls, so they re-record independently). + if (inNeverExecutedContext()) + return; + const Expr *E = LHS->IgnoreParenImpCasts(); + // *p = e: a store through the exact whole-`*p` lvalue of a marked + // local/parameter pointer is the pointee's initialization (paper + // §4.3/§4.5: for a built-in type, a write is its initialization). + // Class-typed pointees never get here: `*sp = S{...}` resolves to a member + // operator= (already rejected as a call on uninitialized storage), so + // PointeeStored is only ever set for built-in-typed pointee stores. + // Subscript stores (p[i] = e) are deliberately neither credited nor + // invalidating: the paper bans element-wise tracking (§5.4/§5.5). + if (const auto *UO = dyn_cast(E); + UO && UO->getOpcode() == UO_Deref) { + if (const VarDecl *VD = getCreditableMarkedPointer(UO->getSubExpr())) + InitStoreCredit[VD] |= PointeeStored; + return; + } + // a.m = e / this->m = e / m = e (also `@=` and `++`, via the shared + // hosts): a whole-member store to an [[uninit]] field of a trackable base + // object is that member's initialization (paper §4.2: "After + // initialization, the object is no longer [[uninit]]"; §6: ordinary + // assignment initializes a built-in), keyed per (base, field) so unrelated + // objects and other function bodies never share credit. Only single-level + // bases earn credit (x.agg.m = e resolves no base -- and is itself an + // uninit_write violation; §5.4 rejects deep delayed-initialization + // tracking), element stores (a.m[i] = e) present a subscript, not a + // MemberExpr, and stay uncredited, and a class-typed x.agg = e is a member + // operator= call (rejected as a call on uninitialized storage) that never + // reaches this built-in-assignment funnel -- so only scalar members are + // ever credited. Member *pointee* stores (*a.p = e) took the deref arm + // above, which keys on local pointers only: the pinned per-object aliasing + // boundary (copies share pointees). + if (const auto *ME = dyn_cast(E)) { + if (const auto *F = dyn_cast(ME->getMemberDecl()); + F && F->hasAttr()) + if (const Decl *Base = resolveMemberStoreBase(ME)) + MemberStoreCredit[{Base, F}] |= WholeStored; + return; + } + // Only a directly named local-storage variable can be credited beyond + // this point: statics fail hasLocalStorage. + const auto *VD = dyn_cast_or_null(getDirectlyNamedDecl(E)); + if (!VD || !VD->hasLocalStorage()) + return; + // u = e (also u @= e and ++u, via the inc-dec host): assigning the whole + // [[uninit]] entity is its initialization (paper §4.2/§4.5). + if (VD->hasAttr()) { + InitStoreCredit[VD] |= WholeStored; + return; + } + if (!VD->hasAttr()) + return; + if (VD->getType()->isReferenceType()) { + // r = e stores through the marked reference to its referent; a reference + // cannot be reseated, so the credit is never cleared. + InitStoreCredit[VD] |= PointeeStored; + } else if (VD->getType()->isPointerType()) { + // p = q / p += n / ++p reseats the marked pointer: any pointee credit no + // longer describes the new pointee. The clear lives here in the tail + // funnel -- not in checkInitProfilePointerAssignment, which runs only + // for plain assignment and would miss compound reseats. + InitStoreCredit[VD] &= ~unsigned(PointeeStored); + } +} + +bool SemaProfiles::hasWholeObjectStoreCredit(const ValueDecl *VD) const { + const auto *Var = dyn_cast(VD); + if (!Var) + return false; + auto It = InitStoreCredit.find(Var); + return It != InitStoreCredit.end() && (It->second & WholeStored); +} + +bool SemaProfiles::hasPointeeStoreCredit(const ValueDecl *VD) const { + const auto *Var = dyn_cast(VD); + if (!Var) + return false; + auto It = InitStoreCredit.find(Var); + return It != InitStoreCredit.end() && (It->second & PointeeStored); +} + +bool SemaProfiles::hasMemberStoreCredit(const Decl *Base, + const FieldDecl *F) const { + if (!Base || !F) + return false; + auto It = MemberStoreCredit.find({Base, F}); + return It != MemberStoreCredit.end() && (It->second & WholeStored); +} + +void SemaProfiles::checkInitProfileThrowOperand(const Expr *Operand) { + // A thrown pointer copy-initializes the exception object, which cannot + // carry [[ref_to_uninit]], so throwing a pointer to uninitialized memory is + // always the unmarked-direction violation. (Reads like `throw *p` funnel + // through the read-through check instead.) + QualType ExceptionObjectTy = + getASTContext().getExceptionObjectType(Operand->getType()); + if (!ExceptionObjectTy->isPointerType()) + return; + checkInitProfileRefToUninit(Operand->getExprLoc(), + /*TargetIsRefToUninit=*/false, + /*IsReference=*/false, Operand); +} + +void SemaProfiles::checkInitProfileNewInitializer(QualType AllocType, + Expr *Init) { + // A written initializer for an allocated pointer binds it like a variable + // initialization -- but a heap pointer object cannot carry + // [[ref_to_uninit]], so binding it to uninitialized memory is always the + // unmarked-direction violation. A braced `new T*{&x}` presents the + // InitListExpr, which the recognizer's single-element pass-through looks + // through -- which is why the caller invokes this for scalar allocations + // only: for an array new, AllocType is the element type and the lone + // initializer of `new T*[k]{&x}` would be peeled to the same binding the + // aggregate element hooks already diagnose. An instantiation-dependent + // allocated type (note that a dependent-pointee `T*` still passes + // isPointerType) defers to the instantiation rebuild, which re-runs this + // check with the concrete type. + if (!AllocType->isPointerType() || + AllocType->isInstantiationDependentType() || !Init) + return; + checkInitProfileRefToUninit(Init->getExprLoc(), + /*TargetIsRefToUninit=*/false, + /*IsReference=*/false, Init); +} + +namespace { +// Row for the unified finalization dispatch shared by class-finalization +// (pattern 3) and constructor-finalization (pattern 4): a profile name plus a +// callback invoked once per finalized, non-dependent, non-invalid Node (a +// CXXRecordDecl or a CXXConstructorDecl). Adding a new profile is a single row +// in the matching table below plus a ProfileRuleError diagnostic in +// DiagnosticSemaKinds.td and a callback that consults +// SemaProfiles::shouldEmitProfileViolation before emitting. +template struct FinalizationProfile { + StringRef Name; + void (*Callback)(Sema &, Node *); +}; + +void runTestClassFinalCallback(Sema &S, CXXRecordDecl *RD) { + if (!S.Profiles().shouldEmitProfileViolation("test::class_final", /*Rule=*/"", + RD->getLocation(), RD)) + return; + S.Diag(RD->getLocation(), diag::err_profile_class_final_test) + << "test::class_final" << RD; +} + +void runTestCtorFinalCallback(Sema &S, CXXConstructorDecl *Ctor) { + if (!S.Profiles().shouldEmitProfileViolation("test::ctor_final", /*Rule=*/"", + Ctor->getLocation(), Ctor)) + return; + S.Diag(Ctor->getLocation(), diag::err_profile_ctor_final_test) + << "test::ctor_final" << Ctor->getParent(); +} + +// True if any leaf field of \p RD -- recursing through nested anonymous +// records -- has a written member-initializer in \p Written. One written +// leaf gives an anonymous union its active member. +static bool anyLeafFieldWritten( + const CXXRecordDecl *RD, + const llvm::SmallPtrSetImpl &Written) { + for (const FieldDecl *F : RD->fields()) { + if (Written.count(F)) + return true; + if (F->isAnonymousStructOrUnion()) + if (const CXXRecordDecl *AnonRD = F->getType()->getAsCXXRecordDecl(); + AnonRD && AnonRD->hasDefinition() && + anyLeafFieldWritten(AnonRD->getDefinition(), Written)) + return true; + } + return false; +} + +// The per-field walk of the ctor_uninit_member callback: check every +// checkable field of \p RD against \p Written, recursing into anonymous +// struct members -- their leaves initialize exactly like direct members of +// the constructor's class (a written initializer for one is an *indirect* +// member-initializer, which the Written collection already resolved to the +// leaf FieldDecl via getAnyMember, and NSDMIs and [[uninit]] markers sit on +// the leaves), so the same per-field logic and diagnostic apply to them. +static void diagnoseCtorUninitFields( + Sema &S, const CXXConstructorDecl *Ctor, const CXXRecordDecl *RD, + const llvm::SmallPtrSetImpl &Written) { + for (const FieldDecl *F : RD->fields()) { + if (F->isUnnamedBitField()) + continue; + if (F->isAnonymousStructOrUnion()) { + const CXXRecordDecl *AnonRD = F->getType()->getAsCXXRecordDecl(); + if (!AnonRD || !AnonRD->hasDefinition() || AnonRD->isInvalidDecl()) + continue; + // An anonymous union's members are mutually exclusive: one written + // leaf gives it its active member -- deliberately lenient for a + // struct variant only partially covered by its written leaves (a + // missed diagnostic, never a false positive) -- and a vacuous + // default-initialization (an empty union, or a leaf NSDMI, which is + // the active member) needs nothing. A leaf [[uninit]] marker is not + // consulted: union_marker already rejects markers on union members. + if (AnonRD->isUnion()) { + if (anyLeafFieldWritten(AnonRD->getDefinition(), Written) || + !S.Profiles().defaultInitLeavesScalarIndeterminate( + F->getType(), /*HonorUninitMarkers=*/true)) + continue; + if (!S.Profiles().shouldEmitProfileViolation( + "std::init", "ctor_uninit_member", Ctor->getLocation(), Ctor)) + continue; + S.Diag(Ctor->getLocation(), diag::err_init_ctor_uninit_anon_union) + << "std::init"; + S.Diag(F->getLocation(), diag::note_init_uninit_anon_union_here); + continue; + } + diagnoseCtorUninitFields(S, Ctor, AnonRD->getDefinition(), Written); + continue; + } + // Other unnamed fields are skipped; a named bit-field is checked like + // any other member. Reference and const members already have dedicated + // diagnostics when left uninitialized. + if (!F->getDeclName() || F->getType()->isReferenceType() || + F->getType().isConstQualified()) + continue; + if (F->hasAttr() || F->hasInClassInitializer() || + Written.count(F)) + continue; + if (!S.Profiles().defaultInitLeavesScalarIndeterminate( + F->getType(), /*HonorUninitMarkers=*/true)) + continue; + if (!S.Profiles().shouldEmitProfileViolation( + "std::init", "ctor_uninit_member", Ctor->getLocation(), Ctor)) + continue; + S.Diag(Ctor->getLocation(), diag::err_init_ctor_uninit_member) + << "std::init" << F->getDeclName(); + S.Diag(F->getLocation(), diag::note_init_uninit_member_here) + << F->getDeclName(); + } +} + +void runStdInitCtorUninitMemberCallback(Sema &S, CXXConstructorDecl *Ctor) { + // Paper §6.1: a user-provided constructor must initialize every member via + // its member-initializer list or an NSDMI, unless the member is marked + // [[uninit]] (whose body initialization is the deferred R7 check). + // A plain assignment in the constructor body does not count. + if (!Ctor->isUserProvided()) + return; + + // A union's members are mutually exclusive; a constructor initializes at most + // one, so the "every member" rule does not apply (paper §6.5). Whether the + // active member is set is a constructor-body flow question, deferred. + if (Ctor->getParent()->isUnion()) + return; + + // Members and direct bases given a written initializer by this constructor. + llvm::SmallPtrSet Written; + llvm::SmallPtrSet WrittenBases; + for (const CXXCtorInitializer *Init : Ctor->inits()) { + if (!Init->isWritten()) + continue; + if (Init->isAnyMemberInitializer()) { + if (const FieldDecl *F = Init->getAnyMember()) + Written.insert(F); + } else if (Init->isBaseInitializer()) { + if (const Type *T = Init->getBaseClass()) + WrittenBases.insert( + S.Context.getCanonicalType(QualType(T, 0)).getTypePtr()); + } + } + + diagnoseCtorUninitFields(S, Ctor, Ctor->getParent(), Written); + + // The guarantee is over the complete object (paper §5.1, §7.1), so a + // direct base-class subobject left indeterminate is as much a violation as a + // member. A base cannot carry an [[uninit]] marker (the attribute's subjects + // are Var/Field), so an indeterminate base must always be initialized -- + // there + // is no marker escape. Virtual bases are the most-derived constructor's + // responsibility, not a local property of this constructor, so they are + // deferred. A written base-initializer initializes the base; an implicit + // (non-written) one is default-init, handled by the indeterminate check. + for (const CXXBaseSpecifier &Base : Ctor->getParent()->bases()) { + if (Base.isVirtual()) + continue; + if (WrittenBases.count( + S.Context.getCanonicalType(Base.getType()).getTypePtr())) + continue; + if (!S.Profiles().defaultInitLeavesScalarIndeterminate(Base.getType(), + /*HonorUninitMarkers=*/true)) + continue; + if (!S.Profiles().shouldEmitProfileViolation( + "std::init", "ctor_uninit_member", Ctor->getLocation(), Ctor)) + continue; + S.Diag(Ctor->getLocation(), diag::err_init_ctor_uninit_base) + << "std::init" << Base.getType(); + S.Diag(Base.getBeginLoc(), diag::note_init_uninit_base_here) + << Base.getType(); + } +} + +void runStdInitUninitFieldMarkerCallback(Sema &S, CXXRecordDecl *RD) { + // std::init / uninit_with_initializer, field flavor (paper §4.2 rule 2, + // §5.3): [[uninit]] on a data member claims default-initialization leaves + // the member uninitialized. When the member type's default-initialization + // is not a no-op (a non-trivial default constructor initializes something) + // or leaves nothing indeterminate (nothing to acknowledge), the marker is a + // contradiction, just like a variable's explicit initializer. The NSDMI + // case is the ActOnFinishCXXInClassMemberInitializer flavor's to diagnose; + // this covers the initializer-less member, which only the class walk sees. + // + // A union's members are union_marker's territory (the marker is banned on + // them wholesale, paper §5.6). + if (RD->isUnion()) + return; + for (const FieldDecl *F : RD->fields()) { + const auto *UA = F->getAttr(); + // hasInClassInitializer is style-based, so it is true even while a + // late-parsed NSDMI is still pending. + if (!UA || F->isInvalidDecl() || F->hasInClassInitializer()) + continue; + QualType BaseTy = S.Context.getBaseElementType(F->getType()); + // A union- or pointer-typed member (keyed on the same base element type) + // already draws union_marker / pointer_marker and keeps the marker; do + // not pile a second diagnostic on top. Load-bearing for union members: a + // union with a non-trivial member has a deleted -- hence non-trivial -- + // default constructor and would otherwise draw both. + if (BaseTy->isUnionType() || BaseTy->isPointerType()) + continue; + // std::byte may be left uninitialized (paper §4), mirroring + // checkInitProfileUninitDecl. + if (BaseTy->isStdByteType()) + continue; + if (S.Profiles().defaultInitIsVacuous(F->getType())) + continue; + // Decl-aware gate: defers on templated patterns (instantiations re-fire + // through CheckCompletedCXXClass) and honors [[profiles::suppress]] on + // the field or the enclosing class. Diagnose at the attribute -- the + // marker is the thing to delete -- like union_marker / pointer_marker. + if (!S.Profiles().shouldEmitProfileViolation( + "std::init", "uninit_with_initializer", UA->getLocation(), F)) + continue; + S.Diag(UA->getLocation(), diag::err_init_uninit_member_initialized) + << "std::init" << F->getDeclName() << F->getType(); + // Distinguish the non-vacuity reasons for the note: a non-trivial default + // constructor runs code (0); a trivial one that leaves no subobject + // uninitialized has nothing to acknowledge (1); a deleted or absent one + // makes the marker unsatisfiable (2). + unsigned Reason = 1; + if (const auto *MemberRD = BaseTy->getAsCXXRecordDecl(); + MemberRD && MemberRD->hasDefinition()) { + const CXXRecordDecl *Def = MemberRD->getDefinition(); + bool Deleted = !Def->hasDefaultConstructor(); + for (const CXXConstructorDecl *Ctor : Def->ctors()) + if (Ctor->isDefaultConstructor() && Ctor->isDeleted()) + Deleted = true; + if (Deleted) + Reason = 2; + else if (!Def->hasTrivialDefaultConstructor()) + Reason = 0; + } + S.Diag(F->getTypeSpecStartLoc(), diag::note_init_uninit_member_type) + << BaseTy << Reason; + } +} + +// Class-finalization opt-in table (pattern 3). +constexpr FinalizationProfile ClassFinalizationProfiles[] = { + {"test::class_final", &runTestClassFinalCallback}, + {"std::init", &runStdInitUninitFieldMarkerCallback}, +}; + +// Constructor-finalization opt-in table (pattern 4). +constexpr FinalizationProfile + ConstructorFinalizationProfiles[] = { + {"test::ctor_final", &runTestCtorFinalCallback}, + {"std::init", &runStdInitCtorUninitMemberCallback}, +}; + +// Run the enforced finalization-profile callbacks in Table for D. Merges the +// former per-node dispatchers; the per-node filter (dependent, lambda, +// delegating, ...) stays at each call site. Each callback passes D to the +// Decl-aware SemaProfiles::shouldEmitProfileViolation, which honors +// [[profiles::suppress]] +// on D or a lexical parent, so the dispatcher needs no suppress scope of its +// own. The table is taken by reference-to-array, not ArrayRef: deducing Node +// from a C array against an ArrayRef> parameter is +// not +// possible (no array-to-ArrayRef conversion happens during template argument +// deduction). +template +void dispatchFinalizationProfiles(Sema &S, Node *D, + const FinalizationProfile (&Table)[N]) { + if (!S.Profiles().anyProfileEnforced(Table)) + return; + // Finalization can run nested in an unrelated instantiation whose + // [[profiles::suppress]] scope is still on the parse-time stack. No guard + // is needed: the stack consult is dominion-checked against the entry's + // recorded construct range, so such an entry matches only if its + // construct's tokens cover the finalized declaration -- including when the + // finalized pattern is first declared after the suppressed construct. + for (const auto &E : Table) + if (S.Profiles().isProfileEnforced(E.Name)) + E.Callback(S, D); +} +} // namespace + +void SemaProfiles::checkProfileViolationsAtClassFinalization( + CXXRecordDecl *RD) { + if (!getLangOpts().Profiles || !RD) + return; + if (RD->isInvalidDecl() || RD->isDependentType() || RD->isLambda()) + return; + dispatchFinalizationProfiles(SemaRef, RD, ClassFinalizationProfiles); +} + +void SemaProfiles::checkProfileViolationsAtConstructorFinalization( + CXXConstructorDecl *Ctor) { + if (!getLangOpts().Profiles || !Ctor) + return; + // A dependent constructor pattern re-fires on instantiation; a delegating + // constructor leaves member initialization to its target. + if (Ctor->isInvalidDecl() || Ctor->isDependentContext() || + Ctor->isDelegatingConstructor()) + return; + dispatchFinalizationProfiles(SemaRef, Ctor, ConstructorFinalizationProfiles); +} + diff --git a/clang/lib/Sema/SemaStmt.cpp b/clang/lib/Sema/SemaStmt.cpp index b74af55d1bea1..61ba9fa5cafaf 100644 --- a/clang/lib/Sema/SemaStmt.cpp +++ b/clang/lib/Sema/SemaStmt.cpp @@ -33,6 +33,7 @@ #include "clang/Sema/Ownership.h" #include "clang/Sema/Scope.h" #include "clang/Sema/ScopeInfo.h" +#include "clang/Sema/SemaProfiles.h" #include "clang/Sema/SemaCUDA.h" #include "clang/Sema/SemaObjC.h" #include "clang/Sema/SemaOpenMP.h" @@ -3708,6 +3709,35 @@ StmtResult Sema::ActOnCapScopeReturnStmt(SourceLocation ReturnLoc, } } + // std::init / ref_to_uninit (paper §5): a return inside a capturing scope + // binds the returned pointer or reference against that scope's *own* + // [[ref_to_uninit]] marking -- never the enclosing function's (paper §8.2: + // the function returning the value must itself be declared appropriately). + // A lambda carries the marker on its call operator (C++23 attribute + // position, after the lambda-introducer); a block cannot carry it, so block + // returns are unmarked targets (null Target), like variadic arguments. The + // owning declaration drives the suppression walk through lexical parents + // and, for a call operator in a template -- a generic lambda's pattern, or + // any lambda in a templated entity -- defers via isTemplated: the body is + // rebuilt under a fresh LambdaScopeInfo at instantiation + // (LambdaScopeForCallOperatorInstantiationRAII / TransformLambdaExpr), so + // this hook re-runs with the concrete declaration. Runs after the + // deduced/implicit return type is resolved above, so the wrapper sees the + // concrete FnRetType (a still-dependent one no-ops there, deferring to the + // same rebuild). A return in a captured region already errored above. + if (RetValExp && getLangOpts().Profiles) { + const ValueDecl *Target = nullptr; + const Decl *ScopeDecl = nullptr; + if (CurLambda) { + Target = CurLambda->CallOperator; + ScopeDecl = CurLambda->CallOperator; + } else if (const auto *BSI = dyn_cast(CurCap)) { + ScopeDecl = BSI->TheDecl; + } + Profiles().checkInitProfileRefToUninitBinding( + RetValExp->getExprLoc(), Target, FnRetType, RetValExp, ScopeDecl); + } + // Otherwise, verify that this result type matches the previous one. We are // pickier with blocks than for normal functions because we don't have GCC // compatibility to worry about here. @@ -4116,6 +4146,17 @@ StmtResult Sema::BuildReturnStmt(SourceLocation ReturnLoc, Expr *RetValExp, } } } + // std::init / ref_to_uninit (paper §5): the returned pointer or reference + // must match the function's [[ref_to_uninit]] return marking. AllowLambda + // keeps a lambda call operator's body -- should it ever reach this + // non-capturing path -- checked against its own marker rather than the + // enclosing function's (returns inside lambdas normally take the capturing + // branch above, at parse and at instantiation alike). + if (RetValExp && getLangOpts().Profiles) + if (const FunctionDecl *FD = getCurFunctionDecl(/*AllowLambda=*/true)) + Profiles().checkInitProfileRefToUninitBinding(RetValExp->getExprLoc(), + FD, FnRetType, RetValExp); + const VarDecl *NRVOCandidate = getCopyElisionCandidate(NRInfo, FnRetType); bool HasDependentReturnType = FnRetType->isDependentType(); diff --git a/clang/lib/Sema/SemaStmtAttr.cpp b/clang/lib/Sema/SemaStmtAttr.cpp index 27fd5563cc40e..61f1ef12df96a 100644 --- a/clang/lib/Sema/SemaStmtAttr.cpp +++ b/clang/lib/Sema/SemaStmtAttr.cpp @@ -16,6 +16,7 @@ #include "clang/Sema/DelayedDiagnostic.h" #include "clang/Sema/ParsedAttr.h" #include "clang/Sema/ScopeInfo.h" +#include "clang/Sema/SemaProfiles.h" #include using namespace clang; @@ -71,6 +72,12 @@ static Attr *handleSuppressAttr(Sema &S, Stmt *St, const ParsedAttr &A, S.Context, A, DiagnosticIdentifiers.data(), DiagnosticIdentifiers.size()); } +static Attr *handleProfilesSuppressStmtAttr(Sema &S, Stmt *St, + const ParsedAttr &A, + SourceRange Range) { + return S.Profiles().makeProfilesSuppressAttr(A); +} + static Attr *handleLoopHintAttr(Sema &S, Stmt *St, const ParsedAttr &A, SourceRange) { IdentifierLoc *PragmaNameLoc = A.getArgAsIdent(0); @@ -704,6 +711,8 @@ static Attr *ProcessStmtAttribute(Sema &S, Stmt *St, const ParsedAttr &A, return handleOpenCLUnrollHint(S, St, A, Range); case ParsedAttr::AT_Suppress: return handleSuppressAttr(S, St, A, Range); + case ParsedAttr::AT_ProfilesSuppress: + return handleProfilesSuppressStmtAttr(S, St, A, Range); case ParsedAttr::AT_NoMerge: return handleNoMergeAttr(S, St, A, Range); case ParsedAttr::AT_NoInline: diff --git a/clang/lib/Sema/SemaTemplateInstantiate.cpp b/clang/lib/Sema/SemaTemplateInstantiate.cpp index 34ed5dffa11b4..2f12186b60796 100644 --- a/clang/lib/Sema/SemaTemplateInstantiate.cpp +++ b/clang/lib/Sema/SemaTemplateInstantiate.cpp @@ -29,6 +29,7 @@ #include "clang/Sema/EnterExpressionEvaluationContext.h" #include "clang/Sema/Initialization.h" #include "clang/Sema/Sema.h" +#include "clang/Sema/SemaProfiles.h" #include "clang/Sema/SemaConcept.h" #include "clang/Sema/SemaInternal.h" #include "clang/Sema/Template.h" @@ -3842,6 +3843,9 @@ bool Sema::InstantiateInClassInitializer( ActOnStartCXXInClassMemberInitializer(); CXXThisScopeRAII ThisScope(*this, Instantiation->getParent(), Qualifiers()); + SemaProfiles::ProfileSuppressScope ProfileSuppressGuard( + *this, Pattern, /*WalkLexicalParents=*/true); + ExprResult NewInit = SubstInitializer(OldInit, TemplateArgs, /*CXXDirectInit=*/false); Expr *Init = NewInit.get(); diff --git a/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp b/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp index cc24e03e77c07..35cba95de5aa1 100644 --- a/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp +++ b/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp @@ -25,6 +25,7 @@ #include "clang/Sema/Initialization.h" #include "clang/Sema/Lookup.h" #include "clang/Sema/ScopeInfo.h" +#include "clang/Sema/SemaProfiles.h" #include "clang/Sema/SemaAMDGPU.h" #include "clang/Sema/SemaCUDA.h" #include "clang/Sema/SemaHLSL.h" @@ -871,6 +872,26 @@ void Sema::InstantiateAttrs(const MultiLevelTemplateArgumentList &TemplateArgs, if (!isRelevantAttr(*this, New, TmplAttr)) continue; + // The [[ref_to_uninit]] handler deferred type validation on a dependent + // subject; re-check against the substituted type and drop the marker when + // it is invalid (the diagnostic points at the pattern's attribute, with + // the instantiation note locating the culprit). In a SFINAE context + // (declaration substitution during deduction) the invalid marker is + // dropped silently: the marker must not affect overload resolution, and a + // dropped marker is inert -- the ref_to_uninit rule never consults a + // marker on a non-pointer/reference entity. + if (isa(TmplAttr) && + Profiles().diagnoseInvalidRefToUninitMarker( + New, TmplAttr->getLocation(), /*Diagnose=*/!isSFINAEContext())) + continue; + + // Likewise for [[uninit]]: the handler deferred the reference-type + // rejection on a dependent subject. + if (isa(TmplAttr) && + Profiles().diagnoseInvalidUninitMarker(New, TmplAttr->getLocation(), + /*Diagnose=*/!isSFINAEContext())) + continue; + // FIXME: This should be generalized to more than just the AlignedAttr. const AlignedAttr *Aligned = dyn_cast(TmplAttr); if (Aligned && Aligned->isAlignmentDependent()) { @@ -1833,6 +1854,12 @@ Decl *TemplateDeclInstantiator::VisitVarDecl(VarDecl *D, if (SemaRef.getLangOpts().OpenACC) SemaRef.OpenACC().ActOnVariableDeclarator(Var); + // std::init / pointer_marker + union_marker: like the field case, re-check + // the instantiated variable now that its type is known (the parse-time + // handler deferred on the template pattern). + if (!Var->isInvalidDecl()) + SemaRef.Profiles().checkInitProfileMarkerPlacement(Var); + return Var; } @@ -1917,6 +1944,12 @@ Decl *TemplateDeclInstantiator::VisitFieldDecl(FieldDecl *D) { Field->setAccess(D->getAccess()); Owner->addDecl(Field); + // std::init / pointer_marker + union_marker: the parse-time handler deferred + // on the (dependent) template member; re-check now that the substituted type + // is known. + if (!Field->isInvalidDecl()) + SemaRef.Profiles().checkInitProfileMarkerPlacement(Field); + return Field; } @@ -5931,6 +5964,9 @@ void Sema::InstantiateFunctionDefinition(SourceLocation PointOfInstantiation, // PushDeclContext because we don't have a scope. Sema::ContextRAII savedContext(*this, Function); + SemaProfiles::ProfileSuppressScope ProfileSuppressGuard( + *this, PatternDecl, /*WalkLexicalParents=*/true); + FPFeaturesStateRAII SavedFPFeatures(*this); CurFPFeatures = FPOptions(getLangOpts()); FpPragmaStack.CurrentValue = FPOptionsOverride(); @@ -6226,6 +6262,8 @@ void Sema::InstantiateVariableInitializer( Var->setImplicitlyInline(); ContextRAII SwitchContext(*this, Var->getDeclContext()); + SemaProfiles::ProfileSuppressScope ProfileSuppressGuard( + *this, OldVar, /*WalkLexicalParents=*/true); EnterExpressionEvaluationContext Evaluated( *this, Sema::ExpressionEvaluationContext::PotentiallyEvaluated, Var, diff --git a/clang/lib/Sema/TreeTransform.h b/clang/lib/Sema/TreeTransform.h index b5272d262530c..daec22e2a3b80 100644 --- a/clang/lib/Sema/TreeTransform.h +++ b/clang/lib/Sema/TreeTransform.h @@ -40,6 +40,7 @@ #include "clang/Sema/ScopeInfo.h" #include "clang/Sema/SemaDiagnostic.h" #include "clang/Sema/SemaInternal.h" +#include "clang/Sema/SemaProfiles.h" #include "clang/Sema/SemaObjC.h" #include "clang/Sema/SemaOpenACC.h" #include "clang/Sema/SemaOpenMP.h" @@ -8311,7 +8312,11 @@ template StmtResult TreeTransform::TransformAttributedStmt(AttributedStmt *S, StmtDiscardKind SDK) { + SemaProfiles::ProfileSuppressScope ProfileSuppressGuard( + getSema(), S->getAttrs(), S->getBeginLoc(), S->getEndLoc()); + StmtResult SubStmt = getDerived().TransformStmt(S->getSubStmt(), SDK); + if (SubStmt.isInvalid()) return StmtError(); @@ -8649,7 +8654,10 @@ TreeTransform::TransformDeclStmt(DeclStmt *S) { SmallVector Decls; LambdaScopeInfo *LSI = getSema().getCurLambda(); for (auto *D : S->decls()) { + SemaProfiles::ProfileSuppressScope ProfileSuppressGuard(getSema(), D); + Decl *Transformed = getDerived().TransformDefinition(D->getLocation(), D); + if (!Transformed) return StmtError(); diff --git a/clang/lib/Serialization/ASTReader.cpp b/clang/lib/Serialization/ASTReader.cpp index 515eaf8d1caed..5a0928241569f 100644 --- a/clang/lib/Serialization/ASTReader.cpp +++ b/clang/lib/Serialization/ASTReader.cpp @@ -78,6 +78,7 @@ #include "clang/Sema/ObjCMethodList.h" #include "clang/Sema/Scope.h" #include "clang/Sema/Sema.h" +#include "clang/Sema/SemaProfiles.h" #include "clang/Sema/SemaCUDA.h" #include "clang/Sema/SemaObjC.h" #include "clang/Sema/Weak.h" @@ -3661,6 +3662,16 @@ ASTReader::ReadControlBlock(ModuleFile &F, } } +/// Decode an enforced-profile blob record (P3589R2), the inverse of the +/// ASTWriter encoding: Record[0] is the profile name length and the blob is +/// the concatenated name + designator. Shared by the ENFORCED_PROFILES (PCH) +/// and SUBMODULE_ENFORCED_PROFILES cases. +static profiles::EnforcedProfile readEnforcedProfile(ArrayRef Record, + StringRef Blob) { + unsigned NameLen = Record[0]; + return {Blob.substr(0, NameLen).str(), Blob.substr(NameLen).str()}; +} + llvm::Error ASTReader::ReadASTBlock(ModuleFile &F, unsigned ClientLoadCapabilities) { BitstreamCursor &Stream = F.Stream; @@ -4363,6 +4374,14 @@ llvm::Error ASTReader::ReadASTBlock(ModuleFile &F, FPPragmaOptions.swap(Record); break; + case ENFORCED_PROFILES: + SerializedEnforcedProfiles.push_back(readEnforcedProfile(Record, Blob)); + break; + + case PROFILES_TU_HAS_NONEMPTY_DECL: + SerializedTUHasNonEmptyDecl = true; + break; + case DECLS_WITH_EFFECTS_TO_VERIFY: for (unsigned I = 0, N = Record.size(); I != N; /*in loop*/) DeclsWithEffectsToVerify.push_back(ReadDeclID(F, Record, I)); @@ -6568,6 +6587,11 @@ llvm::Error ASTReader::ReadSubmoduleBlock(ModuleFile &F, CurrentModule->ExportAsModule = Blob.str(); ModMap.addLinkAsDependency(CurrentModule); break; + + case SUBMODULE_ENFORCED_PROFILES: + CurrentModule->EnforcedProfileDesignators.push_back( + readEnforcedProfile(Record, Blob)); + break; } } } @@ -9273,6 +9297,14 @@ void ASTReader::UpdateSema() { } } + // Restore enforced profile designators (P3589R2). + for (const auto &EP : SerializedEnforcedProfiles) + SemaObj->Profiles().addProfileEnforcement(EP.ProfileName, EP.Designator, + SourceLocation()); + SerializedEnforcedProfiles.clear(); + if (SerializedTUHasNonEmptyDecl) + SemaObj->Profiles().TUPrecededByNonEmptyDecl = true; + // For non-modular AST files, restore visiblity of modules. for (auto &Import : PendingImportedModulesSema) { if (Import.ImportLoc.isInvalid()) diff --git a/clang/lib/Serialization/ASTWriter.cpp b/clang/lib/Serialization/ASTWriter.cpp index e21a86b688dbf..968aa35ad08a5 100644 --- a/clang/lib/Serialization/ASTWriter.cpp +++ b/clang/lib/Serialization/ASTWriter.cpp @@ -68,6 +68,7 @@ #include "clang/Sema/IdentifierResolver.h" #include "clang/Sema/ObjCMethodList.h" #include "clang/Sema/Sema.h" +#include "clang/Sema/SemaProfiles.h" #include "clang/Sema/SemaCUDA.h" #include "clang/Sema/SemaObjC.h" #include "clang/Sema/Weak.h" @@ -977,6 +978,8 @@ void ASTWriter::WriteBlockInfoBlock() { RECORD(PP_UNSAFE_BUFFER_USAGE); RECORD(VTABLES_TO_EMIT); RECORD(RISCV_VECTOR_INTRINSICS_PRAGMA); + RECORD(ENFORCED_PROFILES); + RECORD(PROFILES_TU_HAS_NONEMPTY_DECL); // SourceManager Block. BLOCK(SOURCE_MANAGER_BLOCK); @@ -1015,6 +1018,7 @@ void ASTWriter::WriteBlockInfoBlock() { RECORD(SUBMODULE_PRIVATE_TEXTUAL_HEADER); RECORD(SUBMODULE_INITIALIZERS); RECORD(SUBMODULE_EXPORT_AS); + RECORD(SUBMODULE_ENFORCED_PROFILES); // Comments Block. BLOCK(COMMENTS_BLOCK); @@ -2989,6 +2993,28 @@ static unsigned getNumberOfModules(Module *Mod) { return ChildModules + 1; } +/// Create the abbrev for an enforced-profile blob record (P3589R2): the +/// profile name length as VBR followed by the concatenated name + designator +/// blob. Decoded by readEnforcedProfile in ASTReader.cpp. Shared by the +/// ENFORCED_PROFILES (PCH) and SUBMODULE_ENFORCED_PROFILES records. +static unsigned createEnforcedProfileAbbrev(llvm::BitstreamWriter &Stream, + unsigned RecordCode) { + auto Abbrev = std::make_shared(); + Abbrev->Add(llvm::BitCodeAbbrevOp(RecordCode)); + Abbrev->Add(llvm::BitCodeAbbrevOp(llvm::BitCodeAbbrevOp::VBR, + 6)); // Profile name length + Abbrev->Add( + llvm::BitCodeAbbrevOp(llvm::BitCodeAbbrevOp::Blob)); // Name + Designator + return Stream.EmitAbbrev(std::move(Abbrev)); +} + +static void emitEnforcedProfile(llvm::BitstreamWriter &Stream, + unsigned AbbrevID, unsigned RecordCode, + const profiles::EnforcedProfile &EP) { + uint64_t Record[] = {RecordCode, EP.ProfileName.size()}; + Stream.EmitRecordWithBlob(AbbrevID, Record, EP.ProfileName + EP.Designator); +} + void ASTWriter::WriteSubmodules(Module *WritingModule, ASTContext *Context) { // Enter the submodule description block. Stream.EnterSubblock(SUBMODULE_BLOCK_ID, /*bits for abbreviations*/5); @@ -3084,6 +3110,9 @@ void ASTWriter::WriteSubmodules(Module *WritingModule, ASTContext *Context) { Abbrev->Add(BitCodeAbbrevOp(BitCodeAbbrevOp::Blob)); // Macro name unsigned ExportAsAbbrev = Stream.EmitAbbrev(std::move(Abbrev)); + unsigned EnforcedProfilesAbbrev = + createEnforcedProfileAbbrev(Stream, SUBMODULE_ENFORCED_PROFILES); + // Write the submodule metadata block. RecordData::value_type Record[] = { getNumberOfModules(WritingModule), @@ -3255,6 +3284,11 @@ void ASTWriter::WriteSubmodules(Module *WritingModule, ASTContext *Context) { Stream.EmitRecordWithBlob(ExportAsAbbrev, Record, Mod->ExportAsModule); } + // Emit enforced profile designators (P3589R2). + for (const auto &EP : Mod->EnforcedProfileDesignators) + emitEnforcedProfile(Stream, EnforcedProfilesAbbrev, + SUBMODULE_ENFORCED_PROFILES, EP); + // Queue up the submodules of this module. for (auto *M : Mod->submodules()) Q.push(M); @@ -5286,6 +5320,37 @@ void ASTWriter::WriteRISCVIntrinsicPragmas(Sema &SemaRef) { Stream.EmitRecord(RISCV_VECTOR_INTRINSICS_PRAGMA, Record); } +void ASTWriter::WriteEnforcedProfiles(Sema &SemaRef) { + if (WritingModule) + return; + + // Record whether the TU contains a non-empty top-level declaration, so an + // including compile's [[profiles::enforce]] placement check (P3589R2 + // [decl.attr.enforce]p1) can consult the bit instead of deserializing this + // PCH's declarations. OR in the flag restored from a base PCH so chains + // propagate it; the walk itself covers only the decls parsed here. + bool HasNonEmptyDecl = SemaRef.Profiles().TUPrecededByNonEmptyDecl; + if (!HasNonEmptyDecl) + for (const auto *D : + SemaRef.getASTContext().getTranslationUnitDecl()->noload_decls()) + if (!D->isImplicit() && D->getLocation().isValid() && + !isa(D)) { + HasNonEmptyDecl = true; + break; + } + if (HasNonEmptyDecl) { + RecordData::value_type Record[] = {1}; + Stream.EmitRecord(PROFILES_TU_HAS_NONEMPTY_DECL, Record); + } + + if (SemaRef.Profiles().EnforcedProfiles.empty()) + return; + + unsigned AbbrevID = createEnforcedProfileAbbrev(Stream, ENFORCED_PROFILES); + for (const auto &EP : SemaRef.Profiles().EnforcedProfiles) + emitEnforcedProfile(Stream, AbbrevID, ENFORCED_PROFILES, EP); +} + //===----------------------------------------------------------------------===// // General Serialization Routines //===----------------------------------------------------------------------===// @@ -6189,6 +6254,7 @@ ASTFileSignature ASTWriter::WriteASTCore(Sema *SemaPtr, StringRef isysroot, WriteOpenCLExtensions(*SemaPtr); WriteCUDAPragmas(*SemaPtr); WriteRISCVIntrinsicPragmas(*SemaPtr); + WriteEnforcedProfiles(*SemaPtr); } // If we're emitting a module, write out the submodule information. diff --git a/clang/test/AST/ast-dump-profiles-core-ub.cpp b/clang/test/AST/ast-dump-profiles-core-ub.cpp new file mode 100644 index 0000000000000..afee1eef1aa64 --- /dev/null +++ b/clang/test/AST/ast-dump-profiles-core-ub.cpp @@ -0,0 +1,9 @@ +// RUN: %clang_cc1 -std=c++23 -fprofiles -ast-dump %s | FileCheck %s + +// The std::core_ub profile (P4317) is enforced through the same framework +// attribute as any other profile; enforcement is carried on the leading +// empty-declaration and recorded for CodeGen to read. + +[[profiles::enforce(std::core_ub)]]; +// CHECK: EmptyDecl +// CHECK-NEXT: ProfilesEnforceAttr {{.*}} std::core_ub std::core_ub 0{{$}} diff --git a/clang/test/AST/ast-dump-profiles-enforce.cpp b/clang/test/AST/ast-dump-profiles-enforce.cpp new file mode 100644 index 0000000000000..374bfcc662e90 --- /dev/null +++ b/clang/test/AST/ast-dump-profiles-enforce.cpp @@ -0,0 +1,24 @@ +// RUN: %clang_cc1 -std=c++23 -fprofiles -ast-dump %s | FileCheck %s + +// A valid repetition of an already-recorded designator has no effect +// (P3589R2 [decl.attr.enforce]p3), so it must not re-append the designator to +// the attribute's argument arrays. The "already recorded" test must use the +// ungated enforcement list: a gated-off test:: profile (recorded but reported +// not-enforced without -fprofiles-test-profiles, which this run deliberately +// omits) is still a recorded designator. + +[[profiles::enforce(test::type_cast)]]; +// CHECK: EmptyDecl +// CHECK-NEXT: ProfilesEnforceAttr {{.*}} test::type_cast test::type_cast 0{{$}} + +[[profiles::enforce(test::type_cast)]]; +// CHECK: EmptyDecl +// CHECK-NEXT: ProfilesEnforceAttr {{.*}}>{{$}} + +[[profiles::enforce(std::init, vendor(fortify: 3))]]; +// CHECK: EmptyDecl +// CHECK-NEXT: ProfilesEnforceAttr {{.*}} std::init vendor std::init vendor(fortify : 3) 0 1 fortify 3 1{{$}} + +[[profiles::enforce(std::init)]]; +// CHECK: EmptyDecl +// CHECK-NEXT: ProfilesEnforceAttr {{.*}}>{{$}} diff --git a/clang/test/CodeGenCXX/safety-profile-core-ub-alignment.cpp b/clang/test/CodeGenCXX/safety-profile-core-ub-alignment.cpp new file mode 100644 index 0000000000000..e555b9e9d5d1e --- /dev/null +++ b/clang/test/CodeGenCXX/safety-profile-core-ub-alignment.cpp @@ -0,0 +1,17 @@ +// RUN: %clang_cc1 -std=c++23 -fprofiles -triple x86_64-linux-gnu -emit-llvm %s -o - | FileCheck %s +// RUN: %clang_cc1 -std=c++23 -triple x86_64-linux-gnu -emit-llvm %s -o - | FileCheck %s --check-prefix=OFF + +// std::core_ub / {basic.align.object.alignment} (P4317 A.1): accessing an +// object through a pointer that does not meet the type's alignment is +// undefined; under enforcement the access checks the pointer's low bits and +// traps. The int load requires 4-byte alignment, so the check masks the low +// two bits. + +[[profiles::enforce(std::core_ub)]]; + +// CHECK-LABEL: define {{.*}}@_Z8load_intPv +// CHECK: and i64 %{{.*}}, 3 +// CHECK: call void @llvm.ubsantrap(i8 22) +// OFF-LABEL: define {{.*}}@_Z8load_intPv +// OFF-NOT: llvm.ubsantrap +int load_int(void *p) { return *static_cast(p); } diff --git a/clang/test/CodeGenCXX/safety-profile-core-ub-bounds.cpp b/clang/test/CodeGenCXX/safety-profile-core-ub-bounds.cpp new file mode 100644 index 0000000000000..0afd9db024b76 --- /dev/null +++ b/clang/test/CodeGenCXX/safety-profile-core-ub-bounds.cpp @@ -0,0 +1,17 @@ +// RUN: %clang_cc1 -std=c++23 -fprofiles -triple x86_64-linux-gnu -emit-llvm %s -o - | FileCheck %s +// RUN: %clang_cc1 -std=c++23 -triple x86_64-linux-gnu -emit-llvm %s -o - | FileCheck %s --check-prefix=OFF + +// std::core_ub / {expr.add.out.of.bounds} with a statically known bound +// (P4317 A.1): indexing past the end of an array whose extent is visible at +// the subscript is undefined; under enforcement the subscript checks the index +// against the bound and traps. The array-reference parameter keeps the extent +// visible. + +[[profiles::enforce(std::core_ub)]]; + +// CHECK-LABEL: define {{.*}}@_Z3getRA4_ii +// CHECK: icmp ult i64 %{{.*}}, 4 +// CHECK: call void @llvm.ubsantrap(i8 18) +// OFF-LABEL: define {{.*}}@_Z3getRA4_ii +// OFF-NOT: llvm.ubsantrap +int get(int (&a)[4], int i) { return a[i]; } diff --git a/clang/test/CodeGenCXX/safety-profile-core-ub-divide.cpp b/clang/test/CodeGenCXX/safety-profile-core-ub-divide.cpp new file mode 100644 index 0000000000000..94e5b036d969d --- /dev/null +++ b/clang/test/CodeGenCXX/safety-profile-core-ub-divide.cpp @@ -0,0 +1,22 @@ +// RUN: %clang_cc1 -std=c++23 -fprofiles -triple x86_64-linux-gnu -emit-llvm %s -o - | FileCheck %s +// RUN: %clang_cc1 -std=c++23 -triple x86_64-linux-gnu -emit-llvm %s -o - | FileCheck %s --check-prefix=OFF + +// std::core_ub / {expr.mul.div.by.zero} (P4317 A.1): under enforcement an +// integer division verifies its divisor and traps on zero, with no -fsanitize +// flag. Only the divide-by-zero check is emitted here; the INT_MIN/-1 signed +// overflow arm belongs to {expr.mul.representable.type.result}, guarded +// separately. Without -fprofiles the profile is inert. + +[[profiles::enforce(std::core_ub)]]; + +// CHECK-LABEL: define {{.*}}@_Z6divideii +// CHECK: icmp ne i32 %{{.*}}, 0 +// CHECK: call void @llvm.ubsantrap(i8 3) +// OFF-LABEL: define {{.*}}@_Z6divideii +// OFF-NOT: llvm.ubsantrap +int divide(int a, int b) { return a / b; } + +// A remainder is the same guarded operation. +// CHECK-LABEL: define {{.*}}@_Z9remainderii +// CHECK: call void @llvm.ubsantrap(i8 3) +int remainder(int a, int b) { return a % b; } diff --git a/clang/test/CodeGenCXX/safety-profile-core-ub-enum.cpp b/clang/test/CodeGenCXX/safety-profile-core-ub-enum.cpp new file mode 100644 index 0000000000000..02b4c1864d160 --- /dev/null +++ b/clang/test/CodeGenCXX/safety-profile-core-ub-enum.cpp @@ -0,0 +1,19 @@ +// RUN: %clang_cc1 -std=c++23 -fprofiles -triple x86_64-linux-gnu -emit-llvm %s -o - | FileCheck %s +// RUN: %clang_cc1 -std=c++23 -triple x86_64-linux-gnu -emit-llvm %s -o - | FileCheck %s --check-prefix=OFF + +// std::core_ub / {expr.static.cast.enum.outside.range} (P4317 A.1): producing +// an enumeration value outside the range of its enumerators is undefined; under +// enforcement a load of an enum value is range-checked and traps. (The earlier +// trap here is the null/alignment check on the pointer; the enum check is the +// one with handler id 10.) + +[[profiles::enforce(std::core_ub)]]; + +enum E { A, B, C }; + +// CHECK-LABEL: define {{.*}}@_Z3useP1E +// CHECK: icmp ule i32 %{{.*}}, 3 +// CHECK: call void @llvm.ubsantrap(i8 10) +// OFF-LABEL: define {{.*}}@_Z3useP1E +// OFF-NOT: llvm.ubsantrap +int use(E *p) { return *p; } diff --git a/clang/test/CodeGenCXX/safety-profile-core-ub-float-cast.cpp b/clang/test/CodeGenCXX/safety-profile-core-ub-float-cast.cpp new file mode 100644 index 0000000000000..4f7668177db98 --- /dev/null +++ b/clang/test/CodeGenCXX/safety-profile-core-ub-float-cast.cpp @@ -0,0 +1,17 @@ +// RUN: %clang_cc1 -std=c++23 -fprofiles -triple x86_64-linux-gnu -emit-llvm %s -o - | FileCheck %s +// RUN: %clang_cc1 -std=c++23 -triple x86_64-linux-gnu -emit-llvm %s -o - | FileCheck %s --check-prefix=OFF + +// std::core_ub / {conv.fpint.*}, {conv.double.out.of.range} (P4317 A.1): +// converting a floating-point value whose truncated value does not fit the +// destination integer type is undefined; under enforcement the conversion +// range-checks the source and traps. + +[[profiles::enforce(std::core_ub)]]; + +// CHECK-LABEL: define {{.*}}@_Z3f2id +// CHECK: fcmp +// CHECK: call void @llvm.ubsantrap(i8 5) +// CHECK: fptosi double %{{.*}} to i32 +// OFF-LABEL: define {{.*}}@_Z3f2id +// OFF-NOT: llvm.ubsantrap +int f2i(double d) { return (int)d; } diff --git a/clang/test/CodeGenCXX/safety-profile-core-ub-null.cpp b/clang/test/CodeGenCXX/safety-profile-core-ub-null.cpp new file mode 100644 index 0000000000000..100185ecbc6af --- /dev/null +++ b/clang/test/CodeGenCXX/safety-profile-core-ub-null.cpp @@ -0,0 +1,15 @@ +// RUN: %clang_cc1 -std=c++23 -fprofiles -triple x86_64-linux-gnu -emit-llvm %s -o - | FileCheck %s +// RUN: %clang_cc1 -std=c++23 -triple x86_64-linux-gnu -emit-llvm %s -o - | FileCheck %s --check-prefix=OFF + +// std::core_ub / {expr.unary.dereference} null case (P4317 A.1): dereferencing +// a null pointer is undefined; under enforcement the access checks the pointer +// against null and traps. + +[[profiles::enforce(std::core_ub)]]; + +// CHECK-LABEL: define {{.*}}@_Z5derefPi +// CHECK: icmp ne ptr %{{.*}}, null +// CHECK: call void @llvm.ubsantrap(i8 22) +// OFF-LABEL: define {{.*}}@_Z5derefPi +// OFF-NOT: llvm.ubsantrap +int deref(int *p) { return *p; } diff --git a/clang/test/CodeGenCXX/safety-profile-core-ub-overflow.cpp b/clang/test/CodeGenCXX/safety-profile-core-ub-overflow.cpp new file mode 100644 index 0000000000000..e9797a64fac51 --- /dev/null +++ b/clang/test/CodeGenCXX/safety-profile-core-ub-overflow.cpp @@ -0,0 +1,32 @@ +// RUN: %clang_cc1 -std=c++23 -fprofiles -triple x86_64-linux-gnu -emit-llvm %s -o - | FileCheck %s +// RUN: %clang_cc1 -std=c++23 -triple x86_64-linux-gnu -emit-llvm %s -o - | FileCheck %s --check-prefix=OFF + +// std::core_ub / {expr.mul.representable.type.result} (P4317 A.1): a signed +// arithmetic result that is not representable is undefined; under enforcement +// the operation checks for overflow and traps. Unsigned arithmetic wraps and +// is not guarded. + +[[profiles::enforce(std::core_ub)]]; + +// CHECK-LABEL: define {{.*}}@_Z3addii +// CHECK: @llvm.sadd.with.overflow.i32 +// CHECK: call void @llvm.ubsantrap(i8 0) +// OFF-LABEL: define {{.*}}@_Z3addii +// OFF-NOT: llvm.ubsantrap +int add(int a, int b) { return a + b; } + +// CHECK-LABEL: define {{.*}}@_Z3mulii +// CHECK: @llvm.smul.with.overflow.i32 +// CHECK: call void @llvm.ubsantrap(i8 12) +int mul(int a, int b) { return a * b; } + +// CHECK-LABEL: define {{.*}}@_Z6negatei +// CHECK: @llvm.ssub.with.overflow.i32 +// CHECK: call void @llvm.ubsantrap(i8 13) +int negate(int a) { return -a; } + +// Unsigned arithmetic is defined to wrap, so it is never guarded. +// CHECK-LABEL: define {{.*}}@_Z4uaddjj +// CHECK-NOT: llvm.ubsantrap +// CHECK: {{^}}} +unsigned uadd(unsigned a, unsigned b) { return a + b; } diff --git a/clang/test/CodeGenCXX/safety-profile-core-ub-return.cpp b/clang/test/CodeGenCXX/safety-profile-core-ub-return.cpp new file mode 100644 index 0000000000000..c76bf35a8acad --- /dev/null +++ b/clang/test/CodeGenCXX/safety-profile-core-ub-return.cpp @@ -0,0 +1,17 @@ +// RUN: %clang_cc1 -std=c++23 -fprofiles -triple x86_64-linux-gnu -emit-llvm %s -o - | FileCheck %s +// RUN: %clang_cc1 -std=c++23 -triple x86_64-linux-gnu -emit-llvm %s -o - | FileCheck %s --check-prefix=OFF + +// std::core_ub / {stmt.return.flow.off} (P4317 A.1): flowing off the end of a +// value-returning function, when the caller uses the value, is undefined; under +// enforcement the function epilogue traps on the fall-through path. + +[[profiles::enforce(std::core_ub)]]; + +// CHECK-LABEL: define {{.*}}@_Z1fb +// CHECK: call void @llvm.ubsantrap(i8 11) +// OFF-LABEL: define {{.*}}@_Z1fb +// OFF-NOT: llvm.ubsantrap +int f(bool b) { + if (b) + return 1; +} diff --git a/clang/test/CodeGenCXX/safety-profile-core-ub-shift.cpp b/clang/test/CodeGenCXX/safety-profile-core-ub-shift.cpp new file mode 100644 index 0000000000000..6f6b2cb62c4bd --- /dev/null +++ b/clang/test/CodeGenCXX/safety-profile-core-ub-shift.cpp @@ -0,0 +1,19 @@ +// RUN: %clang_cc1 -std=c++23 -fprofiles -triple x86_64-linux-gnu -emit-llvm %s -o - | FileCheck %s +// RUN: %clang_cc1 -std=c++23 -triple x86_64-linux-gnu -emit-llvm %s -o - | FileCheck %s --check-prefix=OFF + +// std::core_ub / {expr.shift.neg.and.width} (P4317 A.1): a shift whose right +// operand is negative or at least the width of the left operand, or a signed +// left shift that shifts bits out, is undefined; under enforcement the shift +// checks its operands and traps. + +[[profiles::enforce(std::core_ub)]]; + +// CHECK-LABEL: define {{.*}}@_Z3shlii +// CHECK: call void @llvm.ubsantrap(i8 20) +// OFF-LABEL: define {{.*}}@_Z3shlii +// OFF-NOT: llvm.ubsantrap +int shl(int a, int b) { return a << b; } + +// CHECK-LABEL: define {{.*}}@_Z3shrii +// CHECK: call void @llvm.ubsantrap(i8 20) +int shr(int a, int b) { return a >> b; } diff --git a/clang/test/CodeGenCXX/safety-profile-core-ub-suppress.cpp b/clang/test/CodeGenCXX/safety-profile-core-ub-suppress.cpp new file mode 100644 index 0000000000000..c9e46e0f742b8 --- /dev/null +++ b/clang/test/CodeGenCXX/safety-profile-core-ub-suppress.cpp @@ -0,0 +1,25 @@ +// RUN: %clang_cc1 -std=c++23 -fprofiles -triple x86_64-linux-gnu -emit-llvm %s -o - | FileCheck %s + +// std::core_ub (P4317): [[profiles::suppress(std::core_ub)]] is the in-source +// opt-out (SD-10 4.1). A whole-profile suppression on a function turns its +// guarded checks back off; a neighbouring unsuppressed function still traps. + +[[profiles::enforce(std::core_ub)]]; + +// CHECK-LABEL: define {{.*}}@_Z9unguardedii +// CHECK-NOT: llvm.ubsantrap +// CHECK: {{^}}} +[[profiles::suppress(std::core_ub)]] +int unguarded(int a, int b) { return a / b; } + +// CHECK-LABEL: define {{.*}}@_Z7guardedii +// CHECK: call void @llvm.ubsantrap(i8 3) +int guarded(int a, int b) { return a / b; } + +// Suppression on an enclosing namespace covers the functions inside it. +namespace [[profiles::suppress(std::core_ub)]] ns { +// CHECK-LABEL: define {{.*}}@_ZN2ns6nestedEii +// CHECK-NOT: llvm.ubsantrap +// CHECK: {{^}}} +int nested(int a, int b) { return a / b; } +} diff --git a/clang/test/Driver/fprofiles-exempt-system-headers.cpp b/clang/test/Driver/fprofiles-exempt-system-headers.cpp new file mode 100644 index 0000000000000..ad00ee319aeb6 --- /dev/null +++ b/clang/test/Driver/fprofiles-exempt-system-headers.cpp @@ -0,0 +1,12 @@ +// The system-header exemption is on by default, so the driver forwards +// -fno-profiles-exempt-system-headers to -cc1 only when the user disables it. +// (Patterns match the -fno- spelling specifically; the exemption's cc1 default +// is on, so nothing is emitted otherwise. The bare option name is avoided in +// CHECKs because it also appears in this file's own name in the -### output.) + +// RUN: %clang -### -fprofiles -std=c++23 -c %s 2>&1 | FileCheck -check-prefix=DEFAULT %s +// RUN: %clang -### -fprofiles -fprofiles-exempt-system-headers -std=c++23 -c %s 2>&1 | FileCheck -check-prefix=DEFAULT %s +// RUN: %clang -### -fprofiles -fno-profiles-exempt-system-headers -std=c++23 -c %s 2>&1 | FileCheck -check-prefix=OFF %s + +// DEFAULT-NOT: "-fno-profiles-exempt-system-headers" +// OFF: "-fno-profiles-exempt-system-headers" diff --git a/clang/test/Misc/pragma-attribute-supported-attributes-list.test b/clang/test/Misc/pragma-attribute-supported-attributes-list.test index 03b9a77ec1814..4752ab7fd948c 100644 --- a/clang/test/Misc/pragma-attribute-supported-attributes-list.test +++ b/clang/test/Misc/pragma-attribute-supported-attributes-list.test @@ -146,6 +146,8 @@ // CHECK-NEXT: NoUwtable (SubjectMatchRule_hasType_functionType) // CHECK-NEXT: NonString (SubjectMatchRule_variable, SubjectMatchRule_field) // CHECK-NEXT: NotTailCalled (SubjectMatchRule_function) +// CHECK-NEXT: NowInit (SubjectMatchRule_function) +// CHECK-NEXT: NowUninit (SubjectMatchRule_function) // CHECK-NEXT: OMPAssume (SubjectMatchRule_function, SubjectMatchRule_objc_method) // CHECK-NEXT: OSConsumed (SubjectMatchRule_variable_is_parameter) // CHECK-NEXT: OSReturnsNotRetained (SubjectMatchRule_function, SubjectMatchRule_objc_method, SubjectMatchRule_objc_property, SubjectMatchRule_variable_is_parameter) @@ -190,6 +192,7 @@ // CHECK-NEXT: RandomizeLayout (SubjectMatchRule_record) // CHECK-NEXT: ReadOnlyPlacement (SubjectMatchRule_record) // CHECK-NEXT: ReentrantCapability (SubjectMatchRule_record, SubjectMatchRule_type_alias) +// CHECK-NEXT: RefToUninit (SubjectMatchRule_variable, SubjectMatchRule_field, SubjectMatchRule_function) // CHECK-NEXT: ReleaseHandle (SubjectMatchRule_variable_is_parameter) // CHECK-NEXT: ReqdWorkGroupSize (SubjectMatchRule_function) // CHECK-NEXT: Restrict (SubjectMatchRule_function) @@ -223,6 +226,7 @@ // CHECK-NEXT: TargetVersion (SubjectMatchRule_function) // CHECK-NEXT: TestTypestate (SubjectMatchRule_function_is_member) // CHECK-NEXT: TrivialABI (SubjectMatchRule_record) +// CHECK-NEXT: Uninit (SubjectMatchRule_variable, SubjectMatchRule_field) // CHECK-NEXT: Uninitialized (SubjectMatchRule_variable_is_local) // CHECK-NEXT: UnsafeBufferUsage (SubjectMatchRule_function, SubjectMatchRule_field) // CHECK-NEXT: UseHandle (SubjectMatchRule_variable_is_parameter) diff --git a/clang/test/PCH/cxx-profiles-enforce.cpp b/clang/test/PCH/cxx-profiles-enforce.cpp new file mode 100644 index 0000000000000..75918e6ce081e --- /dev/null +++ b/clang/test/PCH/cxx-profiles-enforce.cpp @@ -0,0 +1,36 @@ +// Test this without pch. +// RUN: %clang_cc1 %s -fprofiles -fprofiles-test-profiles -std=c++20 -fsyntax-only -include %s -verify + +// Test with pch. +// RUN: %clang_cc1 %s -fprofiles -fprofiles-test-profiles -std=c++20 -emit-pch -o %t +// RUN: %clang_cc1 %s -fprofiles -fprofiles-test-profiles -std=c++20 -fsyntax-only -include-pch %t -verify + +// A header containing a non-empty declaration must fail the main file's +// enforce placement check -- through the parsed-decl walk (with a note) when +// textually included, and through the PCH's has-non-empty-decl bit (no +// deserialization, so no note) when included as a PCH. +// RUN: %clang_cc1 %s -DPCH_DECL -fprofiles -fprofiles-test-profiles -std=c++20 -fsyntax-only -include %s -verify=expected,after,afternote +// RUN: %clang_cc1 %s -DPCH_DECL -fprofiles -fprofiles-test-profiles -std=c++20 -emit-pch -o %t.decl +// RUN: %clang_cc1 %s -DPCH_DECL -fprofiles -fprofiles-test-profiles -std=c++20 -fsyntax-only -include-pch %t.decl -verify=expected,after + +#ifndef HEADER +#define HEADER + +[[profiles::enforce(test::type_cast)]]; + +#ifdef PCH_DECL +int decl_in_pch; // afternote-note {{declaration declared here}} +#endif + +#else + +// Repeating the enforcement is valid after a header that contains only +// empty-declarations, and diagnosed when the header contributed a real +// declaration. +[[profiles::enforce(test::type_cast)]]; // after-error {{'profiles::enforce' attribute on empty-declaration must precede all non-empty declarations}} + +void test() { + int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} +} + +#endif diff --git a/clang/test/PCH/profiles-core-ub-enforce.cpp b/clang/test/PCH/profiles-core-ub-enforce.cpp new file mode 100644 index 0000000000000..c5e2d4bf71725 --- /dev/null +++ b/clang/test/PCH/profiles-core-ub-enforce.cpp @@ -0,0 +1,24 @@ +// Enforcement recorded in a PCH must be restored and reach CodeGen, so a +// division in the main file traps just as it would with the attribute written +// inline (the enforce placement check is satisfied because the PCH precedes +// the main file). + +// Test without pch. +// RUN: %clang_cc1 %s -fprofiles -std=c++23 -triple x86_64-linux-gnu -include %s -emit-llvm -o - | FileCheck %s + +// Test with pch. +// RUN: %clang_cc1 %s -fprofiles -std=c++23 -triple x86_64-linux-gnu -emit-pch -o %t +// RUN: %clang_cc1 %s -fprofiles -std=c++23 -triple x86_64-linux-gnu -include-pch %t -emit-llvm -o - | FileCheck %s + +#ifndef HEADER +#define HEADER + +[[profiles::enforce(std::core_ub)]]; + +#else + +// CHECK-LABEL: define {{.*}}@_Z6divideii +// CHECK: call void @llvm.ubsantrap(i8 3) +int divide(int a, int b) { return a / b; } + +#endif diff --git a/clang/test/Parser/cxx-profiles-framework-disabled.cpp b/clang/test/Parser/cxx-profiles-framework-disabled.cpp new file mode 100644 index 0000000000000..3f12aa90b1d34 --- /dev/null +++ b/clang/test/Parser/cxx-profiles-framework-disabled.cpp @@ -0,0 +1,58 @@ +// RUN: %clang_cc1 -fsyntax-only -verify=disabled -std=c++23 %s +// RUN: %clang_cc1 -fsyntax-only -verify=enabled -fprofiles -DPROFILES_ENABLED -std=c++23 %s + +// With -fprofiles off, clang does not act on the profiles attributes: they +// are ignored with a warning like any standard attribute the implementation +// does not implement, and their argument clauses are ordinary balanced-token +// sequences, not checked against the P3589R2 profile grammar (which governs +// an implementation that enforces profiles). With -fprofiles on, the grammar +// errors fire as usual; this file asserts both behaviors side by side. + +// Well-formed spellings: ignored when off, accepted when on. +// disabled-warning@+1 {{'profiles::enforce' attribute ignored}} +[[profiles::enforce(std::type)]]; +// disabled-warning@+1 {{'profiles::enforce' attribute ignored}} +[[profiles::enforce(vendor::checks(fortify: 3))]]; + +// Malformed argument clauses: accepted as balanced-token soup when off, +// grammar errors when on. +// disabled-warning@+2 {{'profiles::enforce' attribute ignored}} +// enabled-error@+1 {{expected profile name}} +[[profiles::enforce(+)]]; +// disabled-warning@+2 {{'profiles::enforce' attribute ignored}} +// enabled-error@+1 {{expected profile name}} +[[profiles::enforce()]]; +// disabled-warning@+2 {{'profiles::enforce' attribute ignored}} +// enabled-error@+1 {{expected ')' in 'enforce' attribute}} +[[profiles::enforce(a b)]]; + +// A missing argument clause is likewise fine when off. +// disabled-warning@+2 {{'profiles::enforce' attribute ignored}} +// enabled-error@+1 {{'enforce' attribute requires an argument clause}} +[[profiles::enforce]]; +// disabled-warning@+2 {{'profiles::require' attribute ignored}} +// enabled-error@+1 {{'require' attribute requires an argument clause}} +[[profiles::require]]; + +#ifndef PROFILES_ENABLED +// Semicolons, nested parens, and braces are all balanced tokens, fine in an +// ignored attribute's argument clause. +// disabled-warning@+1 {{'profiles::enforce' attribute ignored}} +[[profiles::enforce(a; (b), {c})]]; +#endif + +// disabled-warning@+2 {{'profiles::suppress' attribute ignored}} +// enabled-error@+1 {{'justification' argument of 'profiles::suppress' must be a string literal}} +[[profiles::suppress(p, justification: 42)]] int x = 0; +// disabled-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(std::type)]] int y = 0; + +#ifndef PROFILES_ENABLED +// A genuinely unbalanced argument clause is not a valid attribute in any +// mode: the balanced-token skip still diagnoses the missing ')'. +// disabled-error@+4 {{expected ')'}} +// disabled-note@+3 {{to match this '('}} +// disabled-error@+2 {{expected ']'}} +// disabled-error@+2 {{expected external declaration}} +[[profiles::enforce(]]; +#endif diff --git a/clang/test/Parser/cxx-profiles-framework.cpp b/clang/test/Parser/cxx-profiles-framework.cpp new file mode 100644 index 0000000000000..d1a4eb052680a --- /dev/null +++ b/clang/test/Parser/cxx-profiles-framework.cpp @@ -0,0 +1,104 @@ +// RUN: %clang_cc1 -fsyntax-only -verify -fprofiles -std=c++20 %s + +// =================================================================== +// Valid enforce forms +// =================================================================== + +// Single designator +[[profiles::enforce(std::type)]]; + +// Multi-designator in one enforce +[[profiles::enforce(acme::hardened, lib::safety)]]; + +// Designator with profile arguments +[[profiles::enforce(vendor(fortify: 3, sanitize: thread))]]; + +// Nested balanced groups in arguments +[[profiles::enforce(nested(config: (a b)))]]; + +// Mixed bracket types in balanced groups +[[profiles::enforce(mixed(config: (a [b] c)))]]; + +// Bare non-operator-non-punctuator token arguments +[[profiles::enforce(bare1(3))]]; + +// Bare string literal argument +[[profiles::enforce(bare2("hello"))]]; + +// Bare identifier argument +[[profiles::enforce(bare3(abc))]]; + +// =================================================================== +// Valid suppress forms +// =================================================================== + +[[profiles::suppress(std::type)]] +void suppress_no_args(); + +[[profiles::suppress(std::type, justification: "legacy")]] +void suppress_with_justification(); + +[[profiles::suppress(std::type, rule: "reinterpret_cast")]] +void suppress_with_rule(); + +[[profiles::suppress(std::type, justification: "legacy", rule: "cast")]] +void suppress_with_both(); + +// =================================================================== +// [[using profiles: ...]] syntax +// =================================================================== + +[[using profiles: suppress(std::type)]] +void using_syntax(); + +// =================================================================== +// Profile name with :: separators +// =================================================================== + +[[profiles::suppress(a::b::c)]] +void deep_name(); + +// =================================================================== +// Parse errors +// =================================================================== + +// enforce with empty parens: parse error +[[profiles::enforce()]]; // expected-error {{expected profile name}} + +// enforce with non-identifier first token +[[profiles::enforce(42)]]; // expected-error {{expected profile name}} + +// suppress with empty parens: parse error +[[profiles::suppress()]]; // expected-error {{expected profile name}} + +// Bare argument cannot be an operator (suppress uses profile-argument-list +// directly after comma, avoiding cascading errors from nested designator parens) +[[profiles::suppress(std::type, +)]] // expected-error {{invalid token in profile argument}} +void suppress_bare_operator(); + +// Bare argument cannot be a balanced group +[[profiles::suppress(std::type, (a b))]] // expected-error {{invalid token in profile argument}} +void suppress_bare_group(); + +// =================================================================== +// Missing argument clause: must diagnose, not crash +// =================================================================== + +// enforce with no argument clause at TU scope +[[profiles::enforce]]; // expected-error {{'enforce' attribute requires an argument clause}} + +// suppress with no argument clause on a declaration +[[profiles::suppress]] void suppress_no_args_decl(); // expected-error {{'suppress' attribute requires an argument clause}} + +// require appearing on an empty-declaration with no argument clause: +// the no-l_paren diagnostic fires first; require-not-on-import is not +// reached. +[[profiles::require]]; // expected-error {{'require' attribute requires an argument clause}} + +// The [[using profiles: ...]] syntax must be gated identically. +[[using profiles: enforce]]; // expected-error {{'enforce' attribute requires an argument clause}} + +// Regression: an unknown profiles-scoped attribute without parens must +// continue to fall through to the generic "unknown attribute" warning +// and must not trigger the new "requires an argument clause" error. +[[profiles::bogus]]; // expected-warning {{unknown attribute 'profiles::bogus' ignored}} diff --git a/clang/test/SemaCXX/safety-profile-class-final.cpp b/clang/test/SemaCXX/safety-profile-class-final.cpp new file mode 100644 index 0000000000000..18c02b88b989e --- /dev/null +++ b/clang/test/SemaCXX/safety-profile-class-final.cpp @@ -0,0 +1,298 @@ +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -fprofiles-test-profiles -std=c++23 %s +// RUN: %clang_cc1 -fsyntax-only -verify=no-profiles -std=c++23 %s + +// no-profiles-warning@+1 {{'profiles::enforce' attribute ignored}} +[[profiles::enforce(test::class_final)]]; +// no-profiles-warning@+1 {{'profiles::enforce' attribute ignored}} +[[profiles::enforce(test::other)]]; + +struct Plain { // expected-error {{test profile fired on completion of class 'Plain' under profile 'test::class_final'}} + int m; +}; + +class PlainClass { // expected-error {{test profile fired on completion of class 'PlainClass' under profile 'test::class_final'}} +public: + int m; +}; + +union PlainUnion { // expected-error {{test profile fired on completion of class 'PlainUnion' under profile 'test::class_final'}} + int a; + float b; +}; + +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +struct [[profiles::suppress(test::class_final)]] SuppressedOnClass { + int m; +}; + +// Suppress with rule restriction (the implicit-rule profile uses rule ""). +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +struct [[profiles::suppress(test::class_final, rule: "")]] SuppressedOnClassByRule { + int m; +}; + +// Non-matching profile does not suppress. +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +struct [[profiles::suppress(test::other)]] NotSuppressedWrongProfile { // expected-error {{test profile fired on completion of class 'NotSuppressedWrongProfile' under profile 'test::class_final'}} + int m; +}; + +// Lambdas are filtered out by the dispatcher: the implicit closure types +// are not diagnosed even though they are CXXRecordDecls that go through +// CheckCompletedCXXClass. +void test_lambda_skip() { + auto f = []() { return 1; }; + auto g = [](int x) { return x; }; + (void)f; (void)g; +} + +// Generic lambdas: the closure's call operator is a template, but the +// closure type itself is still a lambda and must be filtered. +void test_generic_lambda_skip() { + auto f = [](auto x) { return x; }; + (void)f(0); + (void)f(0.0); +} + +// A dependent primary template does not fire on the template itself; the +// instantiated specialization's diagnostic is emitted at the primary +// template's location with a note at the instantiation site. +template +struct PrimaryTemplate { // expected-error {{test profile fired on completion of class 'PrimaryTemplate' under profile 'test::class_final'}} \ + // expected-error {{test profile fired on completion of class 'PrimaryTemplate' under profile 'test::class_final'}} + T m; +}; + +PrimaryTemplate instantiate_primary; // expected-note {{in instantiation of template class 'PrimaryTemplate' requested here}} + +PrimaryTemplate instantiate_primary_float; // expected-note {{in instantiation of template class 'PrimaryTemplate' requested here}} + +// Explicit specialization is a fresh definition and fires at its own line. +template <> +struct PrimaryTemplate { // expected-error {{test profile fired on completion of class 'PrimaryTemplate' under profile 'test::class_final'}} + char m; +}; + +// Suppress on the primary template carries through instantiation via the +// lexical-parent walk on the instantiated CXXRecordDecl. +template +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +struct [[profiles::suppress(test::class_final)]] SuppressedTemplate { + T m; +}; +SuppressedTemplate suppressed_template_inst; + +// Suppress on an enclosing namespace silences a nested class via the +// lexical-parent walk. +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +namespace [[profiles::suppress(test::class_final)]] suppressed_ns { + struct NestedInSuppressedNS { + int m; + }; + struct AlsoNested { + int m; + }; +} + +// Suppress on an enclosing class silences a nested class. +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +struct [[profiles::suppress(test::class_final)]] OuterSuppressed { // OuterSuppressed itself is suppressed. + struct Inner { // Inner reaches OuterSuppressed via lexical-parent walk. + int m; + }; + struct AlsoInner { + int m; + }; +}; + +// Suppress on an enclosing namespace also silences template instantiations +// because the instantiated CXXRecordDecl's lexical parent chain still +// reaches the namespace. +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +namespace [[profiles::suppress(test::class_final)]] suppressed_template_ns { + template + struct InsideSuppressedNS { + T m; + }; +} +suppressed_template_ns::InsideSuppressedNS inside_suppressed_ns_inst; + +// Non-matching suppress at namespace scope does not silence the inner class. +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +namespace [[profiles::suppress(test::other)]] wrong_profile_ns { + struct NotSuppressed { // expected-error {{test profile fired on completion of class 'NotSuppressed' under profile 'test::class_final'}} + int m; + }; +} + +// SFINAE: a class template whose instantiation would fire the profile +// diagnostic must not cause the substitution to fail. ProfileRuleError uses +// SFINAE_Suppress, so the diagnostic is suppressed during deduction; the +// first overload is selected because the substitution succeeds. The +// diagnostic is then replayed at the class definition (with the usual +// instantiation-context notes). +template +struct SfinaeTriggered { // expected-error {{test profile fired on completion of class 'SfinaeTriggered' under profile 'test::class_final'}} + using type = T; +}; + +template +auto sfinae_pick(T) -> typename SfinaeTriggered::type { return T{}; } // expected-note {{in instantiation of template class 'SfinaeTriggered' requested here}} + +template +auto sfinae_pick(...) -> int { return 1; } + +static_assert(__is_same(decltype(sfinae_pick(0L)), long), // expected-note {{while substituting explicitly-specified template arguments into function template 'sfinae_pick'}} + "profile violation must not SFINAE out the first overload"); + +// Local classes inside a function body fire (a function is not itself a +// class-finalization subject). +void test_local_class() { + struct LocalInFn { int m; }; // expected-error {{test profile fired on completion of class 'LocalInFn' under profile 'test::class_final'}} + LocalInFn x; + (void)x; +} + +// Function-level suppress silences a local class defined in its body via +// the decl-aware walk (the local class's lexical DeclContext is the +// attributed function) and, equivalently, via the dominion-checked +// parse-time stack -- the class's tokens are within the function's. +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(test::class_final)]] +void test_local_class_suppressed_via_fn() { + struct LocalInSuppressedFn { int m; }; + LocalInSuppressedFn x; + (void)x; +} + +// A *statement-level* suppress reaches a local class defined inside its +// dominion too: that suppression exists only on the parse-time stack (no +// Decl carries the attribute), and the class's tokens are covered. +void test_local_class_suppressed_via_stmt() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::class_final)]] { + struct LocalInSuppressedStmt { int m; }; + LocalInSuppressedStmt x; + (void)x; + } + struct LocalAfterStmt { int m; }; // expected-error {{test profile fired on completion of class 'LocalAfterStmt' under profile 'test::class_final'}} + LocalAfterStmt y; + (void)y; +} + +// Local classes inside a lambda *body* still fire. Only the closure type +// itself is filtered by isLambda(); user-defined classes nested inside the +// closure's call operator are not lambdas. +void test_local_class_inside_lambda() { + auto f = []() { + struct LocalInLambda { int m; }; // expected-error {{test profile fired on completion of class 'LocalInLambda' under profile 'test::class_final'}} + LocalInLambda x; + (void)x; + }; + f(); +} + +// Anonymous union and anonymous struct members fire with synthesized +// "(unnamed ...)" diagnostic names. +struct HasAnonymousMembers { // expected-error {{test profile fired on completion of class 'HasAnonymousMembers' under profile 'test::class_final'}} + union { // expected-error {{test profile fired on completion of class '(unnamed union}} + int a; + float b; + }; + struct { // expected-error {{test profile fired on completion of class '(unnamed struct}} + int x; + }; +}; + +// Class template partial specialization instantiation. The partial +// specialization itself is dependent (skipped); its concrete instantiation +// fires at the primary template's location with a note at the use site. +template struct PartialSpec { T m; }; // expected-error {{test profile fired on completion of class 'PartialSpec' under profile 'test::class_final'}} +template struct PartialSpec { + T *p; +}; +void use_partial_spec() { + PartialSpec x; // expected-note {{in instantiation of template class 'PartialSpec' requested here}} + (void)x; +} + +// Explicit instantiation *definition* fires at the explicit-instantiation +// directive's own line (unlike implicit instantiation, which fires at the +// primary template's line). +template struct ExplicitInst { T m; }; +template struct ExplicitInst; // expected-error {{test profile fired on completion of class 'ExplicitInst' under profile 'test::class_final'}} \ + // expected-note {{in instantiation of template class 'ExplicitInst' requested here}} + +// Explicit instantiation *declaration* (extern template) still instantiates +// the class itself per C++ rules, so it also fires at its own line. +extern template struct ExplicitInst; // expected-error {{test profile fired on completion of class 'ExplicitInst' under profile 'test::class_final'}} \ + // expected-note {{in instantiation of template class 'ExplicitInst' requested here}} + +// Friend class definitions are completed normally and fire at their own line. +class HasFriendDecl { // expected-error {{test profile fired on completion of class 'HasFriendDecl' under profile 'test::class_final'}} + friend struct FriendedLater; +}; +struct FriendedLater { int m; }; // expected-error {{test profile fired on completion of class 'FriendedLater' under profile 'test::class_final'}} + +// A suppress active only because an unrelated template instantiation is in +// progress must not leak into a separate class's finalization (P3589R2 s2.4p3, +// token-based dominion). +template +struct LeakSeparateClass { // expected-error {{test profile fired on completion of class 'LeakSeparateClass' under profile 'test::class_final'}} + T m; +}; + +template +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(test::class_final)]] void class_leak_user() { + LeakSeparateClass v; // expected-note {{in instantiation of template class 'LeakSeparateClass' requested here}} + (void)v; +} +template void class_leak_user(); // expected-note {{in instantiation of function template specialization 'class_leak_user' requested here}} + +// The same leak reaches a *member* (nested) class: a member class template is +// completed synchronously when instantiated inside the unrelated suppressed +// function template, so its finalization must not honor that suppress either. +struct LeakNestedEnclosing { // expected-error {{test profile fired on completion of class 'LeakNestedEnclosing' under profile 'test::class_final'}} + template + struct LeakInner { T m; }; // expected-error {{test profile fired on completion of class 'LeakInner' under profile 'test::class_final'}} +}; + +template +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(test::class_final)]] void nested_leak_user() { + LeakNestedEnclosing::LeakInner v; // expected-note {{in instantiation of template class 'LeakNestedEnclosing::LeakInner' requested here}} + (void)v; +} +template void nested_leak_user(); // expected-note {{in instantiation of function template specialization 'nested_leak_user' requested here}} + +// The dominion's end is bounded by the construct's recorded end: a pattern +// *first declared after* the suppressed construct (reached here via a +// dependent name) is outside its dominion even while the scope is live. +template +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(test::class_final)]] void fwd_final_user() { + typename T::type v; // expected-note {{in instantiation of template class 'FwdFinalVictim' requested here}} + (void)v; +} +template struct FwdFinalVictim { U m; }; // expected-error {{test profile fired on completion of class 'FwdFinalVictim' under profile 'test::class_final'}} +struct FwdFinalHolder { using type = FwdFinalVictim; }; // expected-error {{test profile fired on completion of class 'FwdFinalHolder' under profile 'test::class_final'}} +template void fwd_final_user(); // expected-note {{in instantiation of function template specialization 'fwd_final_user' requested here}} + +// Contrast: with a plain forward declaration *before* the suppressed +// pattern, the specialization's location is the forward declaration's name +// loc -- before the dominion begins -- so this fired even without +// end-bounding. +template struct FwdVictim; // expected-error {{test profile fired on completion of class 'FwdVictim' under profile 'test::class_final'}} +template +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(test::class_final)]] void fwd_victim_user() { + FwdVictim v; // expected-note {{in instantiation of template class 'FwdVictim' requested here}} + (void)v; +} +template struct FwdVictim { T m; }; +template void fwd_victim_user(); // expected-note {{in instantiation of function template specialization 'fwd_victim_user' requested here}} + +// Without `-fprofiles`, the enforce attribute is `warn_attribute_ignored` +// and the diagnostic never fires. This is exercised by the no-profiles RUN +// line, which expects only the two attribute-ignored warnings above. diff --git a/clang/test/SemaCXX/safety-profile-ctor-final.cpp b/clang/test/SemaCXX/safety-profile-ctor-final.cpp new file mode 100644 index 0000000000000..4c6a0263f8682 --- /dev/null +++ b/clang/test/SemaCXX/safety-profile-ctor-final.cpp @@ -0,0 +1,75 @@ +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -fprofiles-test-profiles -std=c++23 %s +// RUN: %clang_cc1 -fsyntax-only -verify=no-profiles -std=c++23 %s + +// no-profiles-warning@+1 {{'profiles::enforce' attribute ignored}} +[[profiles::enforce(test::ctor_final)]]; + +// A constructor with a member-initializer list fires once, at finalization. +struct Written { + int x; + Written() : x(0) {} // expected-error {{test profile fired on finalization of a constructor for class 'Written' under profile 'test::ctor_final'}} +}; + +// A constructor with no member-initializer list reaches the same dispatch. +struct NoList { + int x; + NoList() { x = 0; } // expected-error {{test profile fired on finalization of a constructor for class 'NoList' under profile 'test::ctor_final'}} +}; + +// Out-of-line definitions fire at the definition, not the declaration. +struct OutOfLine { + OutOfLine(); +}; +OutOfLine::OutOfLine() {} // expected-error {{test profile fired on finalization of a constructor for class 'OutOfLine' under profile 'test::ctor_final'}} + +// A defaulted constructor has no body that reaches the dispatch. +struct Defaulted { + int x; + Defaulted() = default; +}; + +// A class with no user-declared constructor never reaches the dispatch. +struct NoCtor { + int x; +}; + +// A delegating constructor leaves member initialization to its target, so it +// is skipped; the target constructor fires. +struct Delegating { + int x; + Delegating() : Delegating(0) {} + Delegating(int v) : x(v) {} // expected-error {{test profile fired on finalization of a constructor for class 'Delegating' under profile 'test::ctor_final'}} +}; + +// A dependent constructor pattern is skipped; the diagnostic fires on the +// instantiation. +template +struct Tmpl { + T x; + Tmpl() : x() {} // expected-error {{test profile fired on finalization of a constructor for class 'Tmpl' under profile 'test::ctor_final'}} +}; +template struct Tmpl; // expected-note {{in instantiation of member function 'Tmpl::Tmpl' requested here}} + +// Suppression on the constructor. +struct SuppressedCtor { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::ctor_final)]] SuppressedCtor() {} +}; + +// Suppression on the enclosing class. +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +struct [[profiles::suppress(test::ctor_final)]] SuppressedClass { + SuppressedClass() {} +}; + +// Rule-targeted suppression (the profile has a single implicit rule). +struct SuppressedByRule { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::ctor_final, rule: "")]] SuppressedByRule() {} +}; + +// A non-matching suppress does not silence the diagnostic. +struct WrongSuppress { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::other)]] WrongSuppress() {} // expected-error {{test profile fired on finalization of a constructor for class 'WrongSuppress' under profile 'test::ctor_final'}} +}; diff --git a/clang/test/SemaCXX/safety-profile-framework-modules.cppm b/clang/test/SemaCXX/safety-profile-framework-modules.cppm new file mode 100644 index 0000000000000..7e502d40db1e2 --- /dev/null +++ b/clang/test/SemaCXX/safety-profile-framework-modules.cppm @@ -0,0 +1,445 @@ +// RUN: rm -rf %t +// RUN: mkdir -p %t +// RUN: split-file %s %t + +// RUN: %clang_cc1 -std=c++20 -fprofiles -emit-module-interface %t/mod_enforced.cppm -o %t/mod_enforced.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/require_ok.cpp -fmodule-file=TestMod=%t/mod_enforced.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/require_fail.cpp -fmodule-file=TestMod=%t/mod_enforced.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/require_mismatch.cpp -fmodule-file=TestMod=%t/mod_enforced.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/require_repeated.cpp -fmodule-file=TestMod=%t/mod_enforced.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -fsyntax-only %t/impl_propagation.cpp -fmodule-file=TestMod=%t/mod_enforced.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -emit-module-interface %t/mod_gmf_enforce.cppm -o %t/mod_gmf_enforce.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/require_gmf_ok.cpp -fmodule-file=GmfMod=%t/mod_gmf_enforce.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -emit-module-interface %t/mod_gmf_only_enforce.cppm -o %t/mod_gmf_only_enforce.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/require_gmf_only_fail.cpp -fmodule-file=GmfOnlyMod=%t/mod_gmf_only_enforce.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -emit-module-interface %t/part_iface.cppm -o %t/part_iface.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/part_primary_require_ok.cppm -fmodule-file=PartMod:part=%t/part_iface.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/part_primary_require_fail.cppm -fmodule-file=PartMod:part=%t/part_iface.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -fsyntax-only %t/part_iface_violation.cppm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -fsyntax-only %t/part_impl_enforce.cppm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -fsyntax-only %t/part_impl_inherit.cppm -fmodule-file=%t/mod_enforced.pcm -Wno-eager-load-cxx-named-modules -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/part_impl_no_inherit.cppm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/import_no_local_enforce.cpp -fmodule-file=TestMod=%t/mod_enforced.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/import_gmf_only_no_leak.cpp -fmodule-file=GmfOnlyMod=%t/mod_gmf_only_enforce.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -emit-module-interface %t/mod_different_desig.cppm -o %t/mod_different_desig.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/import_two_modules.cpp -fmodule-file=TestMod=%t/mod_enforced.pcm -fmodule-file=DiffDesigMod=%t/mod_different_desig.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fsyntax-only %t/mod_noflag_enforce.cppm -verify +// RUN: %clang_cc1 -std=c++20 -emit-module-interface %t/mod_bare.cppm -o %t/mod_bare.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fsyntax-only %t/import_noflag_require.cpp -fmodule-file=BareMod=%t/mod_bare.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/import_mixed_require.cpp -fmodule-file=BareMod=%t/mod_bare.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -fsyntax-only %t/import_mixed_local_enforce.cpp -fmodule-file=BareMod=%t/mod_bare.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fsyntax-only %t/import_mixed_noflag.cpp -fmodule-file=TestMod=%t/mod_enforced.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/mod_enforce_no_args.cppm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/import_require_no_args.cpp -fmodule-file=TestMod=%t/mod_enforced.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/unknown_attr_mod.cppm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/unknown_attr_import.cpp -fmodule-file=TestMod=%t/mod_enforced.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -emit-module-interface %t/redecl_mod.cppm -o %t/redecl_mod.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -fsyntax-only %t/redecl_same.cpp -fmodule-file=RedeclMod=%t/redecl_mod.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -fsyntax-only %t/redecl_none.cpp -fmodule-file=RedeclMod=%t/redecl_mod.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -fsyntax-only %t/redecl_other.cpp -fmodule-file=RedeclMod=%t/redecl_mod.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -emit-module-interface %t/redecl_plain_mod.cppm -o %t/redecl_plain_mod.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -fsyntax-only %t/redecl_reverse.cpp -fmodule-file=RedeclPlainMod=%t/redecl_plain_mod.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -emit-module-interface %t/redecl_std_mod.cppm -o %t/redecl_std_mod.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -fsyntax-only %t/redecl_std_compat.cpp -fmodule-file=RedeclStdMod=%t/redecl_std_mod.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -emit-module-interface %t/redecl_vendor_mod.cppm -o %t/redecl_vendor_mod.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -fsyntax-only %t/redecl_vendor_args.cpp -fmodule-file=RedeclVendorMod=%t/redecl_vendor_mod.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -emit-module-interface %t/redecl_gmf_mod.cppm -o %t/redecl_gmf_mod.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -fsyntax-only %t/redecl_gmf_skip.cpp -fmodule-file=RedeclGmfMod=%t/redecl_gmf_mod.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -fsyntax-only %t/redecl_impl_extra.cpp -fmodule-file=RedeclMod=%t/redecl_mod.pcm -verify + +// =================================================================== +// Module with enforced profiles +// =================================================================== +//--- mod_enforced.cppm +// expected-no-diagnostics +export module TestMod [[profiles::enforce(test::type_cast)]]; + +export void mod_func(); + +// =================================================================== +// Require on import: OK when designators match +// =================================================================== +//--- require_ok.cpp +// expected-no-diagnostics +import TestMod [[profiles::require(test::type_cast)]]; + +// =================================================================== +// Require on import: error when profile not enforced by module +// =================================================================== +//--- require_fail.cpp +import TestMod [[profiles::require(test::not_enforced)]]; // expected-error {{required profile 'test::not_enforced' is not enforced by imported module}} + +// =================================================================== +// Require with designator mismatch +// =================================================================== +//--- require_mismatch.cpp +import TestMod [[profiles::require(test::type_cast(strict: true))]]; // expected-error {{required profile 'test::type_cast(strict : true)' is not enforced by imported module}} + +// =================================================================== +// Repeated require of same designator: OK (P3589R2 Section 2.3 example) +// =================================================================== +//--- require_repeated.cpp +// expected-no-diagnostics +import TestMod [[profiles::require(test::type_cast)]]; +import TestMod [[profiles::require(test::type_cast)]]; + +// =================================================================== +// Interface-to-implementation propagation: the implementation unit +// inherits the enforced profile from the module interface, so the +// reinterpret_cast check fires without a local enforce. +// =================================================================== +//--- impl_propagation.cpp +module TestMod; + +void impl_func() { + int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} +} + +// =================================================================== +// Module with GMF enforce preceding module-declaration enforce: +// require must still find the profile on the module. +// =================================================================== +//--- mod_gmf_enforce.cppm +// expected-no-diagnostics +module; +[[profiles::enforce(test::type_cast)]]; +export module GmfMod [[profiles::enforce(test::type_cast)]]; + +export void gmf_func(); + +//--- require_gmf_ok.cpp +// expected-no-diagnostics +import GmfMod [[profiles::require(test::type_cast)]]; + +// =================================================================== +// GMF-only enforce (no enforce on module-declaration): the profile is +// enforced in the TU but NOT exported via the module. A require on +// import must fail per P3589R2 [decl.attr.require]p2. +// =================================================================== +//--- mod_gmf_only_enforce.cppm +// expected-no-diagnostics +module; +[[profiles::enforce(test::type_cast)]]; +export module GmfOnlyMod; + +export void gmf_only_func(); + +//--- require_gmf_only_fail.cpp +import GmfOnlyMod [[profiles::require(test::type_cast)]]; // expected-error {{required profile 'test::type_cast' is not enforced by imported module}} + +// =================================================================== +// Partition interface with enforce: the profile is exported via the +// partition module, so require on partition import succeeds, and +// enforcement fires locally within the partition. +// =================================================================== +//--- part_iface.cppm +// expected-no-diagnostics +export module PartMod:part [[profiles::enforce(test::type_cast)]]; + +export void part_func(); + +// =================================================================== +// Partition interface: enforcement fires locally within the partition. +// =================================================================== +//--- part_iface_violation.cppm +export module PartViol:part [[profiles::enforce(test::type_cast)]]; + +export void part_func() { + int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} +} + +//--- part_primary_require_ok.cppm +// expected-no-diagnostics +export module PartMod; +import :part [[profiles::require(test::type_cast)]]; + +// =================================================================== +// Partition interface: require fails for a profile the partition does +// not enforce. +// =================================================================== +//--- part_primary_require_fail.cppm +export module PartMod; +import :part [[profiles::require(test::not_enforced)]]; // expected-error {{required profile 'test::not_enforced' is not enforced by imported module}} + +// =================================================================== +// Partition implementation with enforce: enforcement is active locally +// but the profile is NOT exported on the module (ExportMod is null +// for PartitionImplementation). +// =================================================================== +//--- part_impl_enforce.cppm +module PartImpl:impl [[profiles::enforce(test::type_cast)]]; + +void impl_func() { + int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} +} + +// =================================================================== +// Partition implementation inherits the primary interface's enforced +// profiles when that interface's BMI is resident (best-effort). Only the +// eager -fmodule-file= form makes TestMod resident here; the lazy +// -fmodule-file== form loads on import only and would not +// trigger inheritance (the partition impl does not import the primary). +// TestMod enforces test::type_cast, so the cast is diagnosed without a +// local enforce. +// =================================================================== +//--- part_impl_inherit.cppm +module TestMod:inherit; + +void part_inherit_func() { + int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} +} + +// =================================================================== +// Normal build: the primary interface is not implicitly imported and is +// usually built after its partitions, so its BMI is absent here and the +// partition implementation unit does NOT inherit the enforcement. This is +// the best-effort limitation; repeat [[profiles::enforce]] for guaranteed +// enforcement. +// =================================================================== +//--- part_impl_no_inherit.cppm +// expected-no-diagnostics +module TestMod:inherit; + +void part_no_inherit_func() { + int *p = reinterpret_cast(0); +} + +// =================================================================== +// Importing a module with enforce must NOT leak enforcement into the +// importer's own TU. Without a local enforce, reinterpret_cast is OK. +// =================================================================== +//--- import_no_local_enforce.cpp +// expected-no-diagnostics +import TestMod; + +void importer_func() { + int *p = reinterpret_cast(0); +} + +// =================================================================== +// GMF-only enforce must NOT leak into importers. +// =================================================================== +//--- import_gmf_only_no_leak.cpp +// expected-no-diagnostics +import GmfOnlyMod; + +void gmf_importer_func() { + int *p = reinterpret_cast(0); +} + +// =================================================================== +// Second module enforcing test::type_cast with a different designator. +// =================================================================== +//--- mod_different_desig.cppm +// expected-no-diagnostics +export module DiffDesigMod [[profiles::enforce(test::type_cast(strict: true))]]; + +export void diff_func(); + +// =================================================================== +// Importing two modules that enforce the same profile name with +// different designators must NOT produce err_profiles_enforce_mismatch +// in the importer. +// =================================================================== +//--- import_two_modules.cpp +// expected-no-diagnostics +import TestMod; +import DiffDesigMod; + +// =================================================================== +// Without -fprofiles, [[profiles::enforce]] on a module-declaration +// must emit warn_attribute_ignored instead of being silently accepted. +// =================================================================== +//--- mod_noflag_enforce.cppm +export module NoFlagMod [[profiles::enforce(test::type_cast)]]; // expected-warning {{'profiles::enforce' attribute ignored}} + +export void f(); + +// =================================================================== +// A plain module with no profile attrs, built without -fprofiles, to +// serve as an import target for the require-without-flag test below. +// =================================================================== +//--- mod_bare.cppm +// expected-no-diagnostics +export module BareMod; + +export void bare_fn(); + +// =================================================================== +// Without -fprofiles, [[profiles::require]] on an import must emit +// warn_attribute_ignored and must NOT produce the spurious +// err_profiles_require_not_enforced diagnostic. +// =================================================================== +//--- import_noflag_require.cpp +import BareMod [[profiles::require(test::type_cast)]]; // expected-warning {{'profiles::require' attribute ignored}} + +// =================================================================== +// -fprofiles is a Compatible language option: a BMI built without it +// loads into a -fprofiles compile instead of being rejected as a +// configuration mismatch (P3589R2's gradual-adoption premise). The +// no-flag BMI advertises no profiles, so require gets its ordinary +// not-enforced answer rather than a load error. +// =================================================================== +//--- import_mixed_require.cpp +import BareMod [[profiles::require(test::type_cast)]]; // expected-error {{required profile 'test::type_cast' is not enforced by imported module}} + +// =================================================================== +// Local enforcement still works across a mixed-mode import. +// =================================================================== +//--- import_mixed_local_enforce.cpp +[[profiles::enforce(test::type_cast)]]; +import BareMod; + +void mixed_local_func() { + int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} +} + +// =================================================================== +// The reverse mix: a BMI built WITH -fprofiles loads into a compile +// without the flag; require is ignored (flag off) and the module's +// enforcement does not leak into the importer. +// =================================================================== +//--- import_mixed_noflag.cpp +import TestMod [[profiles::require(test::type_cast)]]; // expected-warning {{'profiles::require' attribute ignored}} + +void mixed_noflag_func() { + int *p = reinterpret_cast(0); +} + +// =================================================================== +// [[profiles::enforce]] on a module-declaration with no argument +// clause must be diagnosed, not crash. +// =================================================================== +//--- mod_enforce_no_args.cppm +export module NoArgsMod [[profiles::enforce]]; // expected-error {{'enforce' attribute requires an argument clause}} + +export void f(); + +// =================================================================== +// [[profiles::require]] on an import-declaration with no argument +// clause must be diagnosed, not crash. +// =================================================================== +//--- import_require_no_args.cpp +import TestMod [[profiles::require]]; // expected-error {{'require' attribute requires an argument clause}} + +// =================================================================== +// An unknown attribute on a module-declaration or import-declaration +// is a warning (exactly as in ProhibitCXX11Attributes), while a known +// but non-module attribute stays an error. +// =================================================================== +//--- unknown_attr_mod.cppm +export module UnknownAttrMod [[vendor::unknown]] [[deprecated]]; +// expected-warning@-1 {{unknown attribute 'vendor::unknown' ignored}} +// expected-error@-2 {{'deprecated' attribute cannot be applied to a module}} + +export void f(); + +//--- unknown_attr_import.cpp +import TestMod [[vendor::unknown]]; // expected-warning {{unknown attribute 'vendor::unknown' ignored}} +import TestMod [[deprecated]]; // expected-error {{'deprecated' attribute cannot be applied to a module import}} + +// =================================================================== +// Redeclaration profile compatibility (P3589R2 [decl.attr.enforce]p5): +// a declaration and its redeclarations must appear in the dominions of +// mutually compatible profiles. Compatibility is by profile name (the +// arguments configure a profile without changing its identity), and all +// std:: profiles are compatible with each other. The redeclarable +// fixtures are extern "C++" purview declarations -- entities attached to +// a named module cannot be redeclared in other TUs at all -- which sit +// inside the module declaration's dominion. +// =================================================================== +//--- redecl_mod.cppm +// expected-no-diagnostics +export module RedeclMod [[profiles::enforce(test::type_cast)]]; +extern "C++" { +void redecl_api(int); +struct RedeclTag; +} + +//--- redecl_plain_mod.cppm +// expected-no-diagnostics +export module RedeclPlainMod; +extern "C++" void plain_api(int); + +//--- redecl_std_mod.cppm +// expected-no-diagnostics +export module RedeclStdMod [[profiles::enforce(std::init)]]; +extern "C++" void std_api(int); + +//--- redecl_vendor_mod.cppm +// expected-no-diagnostics +export module RedeclVendorMod [[profiles::enforce(vendor(fortify: 3))]]; +extern "C++" void vendor_api(int); + +//--- redecl_same.cpp +// Redeclaring under the same profile satisfies both directions at once. +// expected-no-diagnostics +[[profiles::enforce(test::type_cast)]]; +import RedeclMod; +void redecl_api(int); +struct RedeclTag; + +//--- redecl_none.cpp +// Forward direction: the module's profile has no compatible counterpart here. +// Both function and tag redeclarations funnel through the check. +import RedeclMod; +void redecl_api(int); // expected-error {{redeclaration of 'redecl_api' is not in the dominion of a profile compatible with 'test::type_cast', which module 'RedeclMod' enforces where 'redecl_api' was previously declared}} +struct RedeclTag; // expected-error {{redeclaration of 'RedeclTag' is not in the dominion of a profile compatible with 'test::type_cast', which module 'RedeclMod' enforces where 'RedeclTag' was previously declared}} +// expected-note@redecl_mod.cppm:* {{previous declaration is here}} +// expected-note@redecl_mod.cppm:* {{previous declaration is here}} + +//--- redecl_other.cpp +// An incompatible (non-std) profile on each side violates both directions. +[[profiles::enforce(test::other)]]; +import RedeclMod; +void redecl_api(int); // expected-error {{redeclaration of 'redecl_api' is not in the dominion of a profile compatible with 'test::type_cast', which module 'RedeclMod' enforces where 'redecl_api' was previously declared}} \ + // expected-error {{'redecl_api' was previously declared in module 'RedeclMod', outside the dominion of a profile compatible with 'test::other', which this translation unit enforces}} +// expected-note@redecl_mod.cppm:* {{previous declaration is here}} + +//--- redecl_reverse.cpp +// Reverse direction: this TU enforces a profile; the previous declaration's +// TU enforced nothing compatible. +[[profiles::enforce(test::type_cast)]]; +import RedeclPlainMod; +void plain_api(int); // expected-error {{'plain_api' was previously declared in module 'RedeclPlainMod', outside the dominion of a profile compatible with 'test::type_cast', which this translation unit enforces}} +// expected-note@redecl_plain_mod.cppm:* {{previous declaration is here}} + +//--- redecl_std_compat.cpp +// All standard profiles are compatible with each other, in both directions. +// expected-no-diagnostics +[[profiles::enforce(std::other_profile)]]; +import RedeclStdMod; +void std_api(int); + +//--- redecl_vendor_args.cpp +// Compatibility is by profile name: different arguments configure the same +// profile. +// expected-no-diagnostics +[[profiles::enforce(vendor(fortify: 2))]]; +import RedeclVendorMod; +void vendor_api(int); + +//--- redecl_gmf_mod.cppm +// expected-no-diagnostics +module; +void redecl_gmf_api(int); +export module RedeclGmfMod [[profiles::enforce(test::type_cast)]]; +export inline void use_gmf() { redecl_gmf_api(1); } + +//--- redecl_gmf_skip.cpp +// A declaration in the module's *explicit* global module fragment precedes +// the module declaration, so the exported enforcement does not cover it, and +// its TU's empty-declaration enforces are not serialized: its dominion is +// unknown and the check skips it (a missed diagnostic, never a wrong one). +// expected-no-diagnostics +import RedeclGmfMod; +void redecl_gmf_api(int); + +//--- redecl_impl_extra.cpp +// An implementation unit may add TU-local enforcement; entities of its own +// module family are skipped (the exported set under-approximates the +// interface TU's dominion, and the interface's enforcement is inherited +// here anyway). +// expected-no-diagnostics +module RedeclMod [[profiles::enforce(test::other)]]; +extern "C++" void redecl_api(int); diff --git a/clang/test/SemaCXX/safety-profile-framework.cpp b/clang/test/SemaCXX/safety-profile-framework.cpp new file mode 100644 index 0000000000000..c12c29a1a552a --- /dev/null +++ b/clang/test/SemaCXX/safety-profile-framework.cpp @@ -0,0 +1,101 @@ +// RUN: %clang_cc1 -fsyntax-only -verify -fprofiles -fprofiles-test-profiles -std=c++20 %s + +// =================================================================== +// Enforce on empty-declaration at TU scope: OK +// =================================================================== +[[profiles::enforce(test::type_cast)]]; // #enforce1 + +// =================================================================== +// Multiple different profiles enforced: OK +// =================================================================== +[[profiles::enforce(test::bounds)]]; + +// =================================================================== +// Enforce: exact repetition is OK +// =================================================================== +[[profiles::enforce(test::type_cast)]]; + +// =================================================================== +// Enforce: mismatch is an error +// =================================================================== +[[profiles::enforce(test::type_cast(strict: true))]]; // expected-error {{repeated enforcement of profile 'test::type_cast' with different designator}} \ + // expected-note@#enforce1 {{previous attribute is here}} + +// =================================================================== +// Enforce after a non-empty declaration: error +// =================================================================== +void some_function(); // #some_function +[[profiles::enforce(test::new_profile)]]; // expected-error {{'profiles::enforce' attribute on empty-declaration must precede all non-empty declarations}} \ + // expected-note@#some_function {{declaration declared here}} + +// =================================================================== +// Enforce on non-empty declaration (function): error +// =================================================================== +[[profiles::enforce(test::type_cast)]] // expected-error {{'profiles::enforce' attribute only allowed on empty-declarations and module-declarations}} +void enforced_func(); + +// =================================================================== +// Enforce inside class scope: error +// =================================================================== +struct EnforceInClass { + [[profiles::enforce(test::type_cast)]]; // expected-warning {{declaration does not declare anything}} \ + // expected-error {{'profiles::enforce' attribute on empty-declaration must be at translation unit scope}} +}; + +// =================================================================== +// Enforce inside a namespace: error +// =================================================================== +namespace ns { + [[profiles::enforce(test::type_cast)]]; // expected-error {{'profiles::enforce' attribute on empty-declaration must be at translation unit scope}} +} + +// =================================================================== +// Require not on import: error +// =================================================================== +[[profiles::require(test::type_cast)]]; // expected-error {{'profiles::require' attribute only allowed on module-import-declarations}} + +// =================================================================== +// Suppress on declarations +// =================================================================== +[[profiles::suppress(test::type_cast)]] +int suppressed_var; + +[[profiles::suppress(test::type_cast)]] +void suppressed_func(); + +// =================================================================== +// Suppress on statements +// =================================================================== +void test_stmt_suppress() { + [[profiles::suppress(test::type_cast)]] int *x = reinterpret_cast(0); + [[profiles::suppress(test::type_cast)]] { int *y = reinterpret_cast(0); } + int *z = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} +} + +// =================================================================== +// Suppress with non-string justification: error +// =================================================================== +[[profiles::suppress(test::type_cast, justification: legacy)]] // expected-error {{'justification' argument of 'profiles::suppress' must be a string literal}} +void bad_justification(); + +// =================================================================== +// Enforce at block scope: error (this is a null-statement at block +// scope, so enforce cannot appertain to it) +// =================================================================== +void test_block_scope() { + [[profiles::enforce(test::type_cast)]]; // expected-error {{'profiles::enforce' attribute cannot be applied to a statement}} +} + +// =================================================================== +// Diagnostic fires when profile IS enforced +// =================================================================== +void test_enforced_profile_errors() { + int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} +} + +// =================================================================== +// Suppress on additional declaration kinds +// =================================================================== +enum [[profiles::suppress(test::type_cast)]] SuppressedEnum { SE_A, SE_B }; + +using SuppressedAlias [[profiles::suppress(test::type_cast)]] = int; diff --git a/clang/test/SemaCXX/safety-profile-header-unit.cpp b/clang/test/SemaCXX/safety-profile-header-unit.cpp new file mode 100644 index 0000000000000..b9e403379a976 --- /dev/null +++ b/clang/test/SemaCXX/safety-profile-header-unit.cpp @@ -0,0 +1,83 @@ +// Header units (P3589R2 s2.3): [[profiles::enforce]] on an empty-declaration +// in a header compiled as a header unit is recorded on the header-unit module +// and validated by [[profiles::require]] on its import. Importing an enforced +// header unit does not enforce the profile locally. + +// RUN: rm -rf %t +// RUN: mkdir -p %t +// RUN: split-file %s %t +// +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -emit-header-unit -xc++-user-header %t/enforced.h -o %t/enforced.pcm +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -emit-header-unit -xc++-user-header %t/plain.h -o %t/plain.pcm +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -emit-header-unit -xc++-user-header %t/args.h -o %t/args.pcm +// +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -Wno-experimental-header-units -fsyntax-only %t/import_ok.cpp -fmodule-file=%t/enforced.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -Wno-experimental-header-units -fsyntax-only %t/import_fail.cpp -fmodule-file=%t/enforced.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -Wno-experimental-header-units -fsyntax-only %t/import_plain_fail.cpp -fmodule-file=%t/plain.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -Wno-experimental-header-units -fsyntax-only %t/import_no_leak.cpp -fmodule-file=%t/enforced.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -Wno-experimental-header-units -fsyntax-only %t/import_args_ok.cpp -fmodule-file=%t/args.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -Wno-experimental-header-units -fsyntax-only %t/import_args_fail.cpp -fmodule-file=%t/args.pcm -verify +// +// The -fprofiles-test-profiles gate only controls whether test:: rules fire; +// the designator is recorded and exported regardless (see the Test Profiles +// section of ProfilesFrameworkInternals.rst), so require validates identically +// under plain -fprofiles. +// RUN: %clang_cc1 -std=c++20 -fprofiles -emit-header-unit -xc++-user-header %t/enforced.h -o %t/enforced-noflag.pcm +// RUN: %clang_cc1 -std=c++20 -fprofiles -Wno-experimental-header-units -fsyntax-only %t/import_ok.cpp -fmodule-file=%t/enforced-noflag.pcm -verify +// +// Redeclaration profile compatibility (P3589R2 [decl.attr.enforce]p5) +// across a header unit, in both directions. +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -Wno-experimental-header-units -fsyntax-only %t/redecl_forward.cpp -fmodule-file=%t/enforced.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -Wno-experimental-header-units -fsyntax-only %t/redecl_reverse.cpp -fmodule-file=%t/plain.pcm -verify + +//--- enforced.h +[[profiles::enforce(test::type_cast)]]; +void hu_api(int); + +//--- plain.h +void plain_api(int); + +//--- args.h +[[profiles::enforce(vendor(fortify: 3))]]; +void args_api(int); + +//--- import_ok.cpp +// expected-no-diagnostics +import "enforced.h" [[profiles::require(test::type_cast)]]; + +//--- import_fail.cpp +import "enforced.h" [[profiles::require(test::other)]]; // expected-error {{required profile 'test::other' is not enforced by imported module}} + +//--- import_plain_fail.cpp +// A header unit built with no enforcement satisfies no requirement. +import "plain.h" [[profiles::require(test::type_cast)]]; // expected-error {{required profile 'test::type_cast' is not enforced by imported module}} + +//--- import_no_leak.cpp +// Importing an enforced header unit does not enforce the profile in the +// importer: enforcement is always explicit and local. +// expected-no-diagnostics +import "enforced.h"; +long no_leak(void *p) { return reinterpret_cast(p); } + +//--- import_args_ok.cpp +// expected-no-diagnostics +import "args.h" [[profiles::require(vendor(fortify: 3))]]; + +//--- import_args_fail.cpp +// Require compares canonical designator spellings, arguments included. +import "args.h" [[profiles::require(vendor(fortify: 2))]]; // expected-error {{required profile 'vendor(fortify : 2)' is not enforced by imported module}} + +//--- redecl_forward.cpp +// The header unit's TU enforced a profile; the redeclaring TU must enforce a +// compatible one. +import "enforced.h"; +void hu_api(int); // expected-error {{redeclaration of 'hu_api' is not in the dominion of a profile compatible with 'test::type_cast'}} +// expected-note@enforced.h:* {{previous declaration is here}} + +//--- redecl_reverse.cpp +// And symmetrically: this TU enforces a profile; the header unit's TU did not +// enforce a compatible one. +[[profiles::enforce(test::type_cast)]]; +import "plain.h"; +void plain_api(int); // expected-error {{'plain_api' was previously declared in module}} +// expected-note@plain.h:* {{previous declaration is here}} diff --git a/clang/test/SemaCXX/safety-profile-init-aggregate.cpp b/clang/test/SemaCXX/safety-profile-init-aggregate.cpp new file mode 100644 index 0000000000000..4b6d4ea6a39f4 --- /dev/null +++ b/clang/test/SemaCXX/safety-profile-init-aggregate.cpp @@ -0,0 +1,75 @@ +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 %s +// RUN: %clang_cc1 -fsyntax-only -verify=no-profiles -std=c++23 %s + +// no-profiles-warning@+1 {{'profiles::enforce' attribute ignored}} +[[profiles::enforce(std::init)]]; + +struct Trivial { int x; }; +struct AllInit { int x = 0; }; +struct WithCtor { WithCtor(); int x; }; +struct PartlyInit { int x; struct Inner { Inner(); } s; }; +struct Nested { Trivial t; }; +struct WithBase : Trivial {}; +struct MarkedMember { int x [[uninit]]; }; +struct MixedMarked { int a [[uninit]]; int b; }; +struct NestedMarked { MarkedMember m; }; + +void test_aggregate() { + Trivial a; // expected-error {{variable 'a' must be initialized or marked '[[uninit]]' under profile 'std::init'}} + Trivial b = {1}; + Trivial c = {}; + Trivial d{}; + Trivial e [[uninit]]; + (void)a; (void)b; (void)c; (void)d; (void)e; +} + +void test_nested_and_base() { + PartlyInit a; // expected-error {{variable 'a' must be initialized or marked '[[uninit]]' under profile 'std::init'}} + Nested b; // expected-error {{variable 'b' must be initialized or marked '[[uninit]]' under profile 'std::init'}} + WithBase c; // expected-error {{variable 'c' must be initialized or marked '[[uninit]]' under profile 'std::init'}} + (void)a; (void)b; (void)c; +} + +void test_trusted() { + // A type with a user-provided default constructor is trusted (the + // constructor is checked separately), and a non-static data member + // initializer covers the member. + WithCtor a; + AllInit b; + (void)a; (void)b; +} + +void test_marked_members() { + // A type whose only indeterminate scalars are [[uninit]] is trusted; + // those members are acknowledged uninitialized (paper §6.2), even through a + // nesting level. A mixed type still fires for its unmarked scalar. + MarkedMember a; + NestedMarked b; + MixedMarked c; // expected-error {{variable 'c' must be initialized or marked '[[uninit]]' under profile 'std::init'}} + (void)a; (void)b; (void)c; +} + +void test_arrays() { + // An automatic array of a class type whose default-init leaves a scalar + // subobject indeterminate is diagnosed via the base element type (paper + // section 6). + Trivial a[3]; // expected-error {{variable 'a' must be initialized or marked '[[uninit]]' under profile 'std::init'}} + Trivial b[2][3]; // expected-error {{variable 'b' must be initialized or marked '[[uninit]]' under profile 'std::init'}} + int c[5]; // expected-error {{variable 'c' must be initialized or marked '[[uninit]]' under profile 'std::init'}} + Trivial d[2] = {}; + Trivial e[2] = {{1}, {2}}; + [[uninit]] Trivial f[3]; + WithCtor g[3]; + AllInit h[3]; + (void)a; (void)b; (void)c; (void)d; (void)e; (void)f; (void)g; (void)h; +} + +void test_suppress() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "uninit_decl")]] Trivial a; + (void)a; +} + +// Non-local aggregates are zero-initialized at static-init time, so they are +// not diagnosed. +Trivial g_trivial; diff --git a/clang/test/SemaCXX/safety-profile-init-allocators.cpp b/clang/test/SemaCXX/safety-profile-init-allocators.cpp new file mode 100644 index 0000000000000..d57297d724f79 --- /dev/null +++ b/clang/test/SemaCXX/safety-profile-init-allocators.cpp @@ -0,0 +1,114 @@ +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 %s +// RUN: %clang_cc1 -fsyntax-only -verify=no-profiles -std=c++23 %s + +// std::init: known allocator callees are classified without annotation +// (paper §4.3: malloc returns a pointer to uninitialized memory, calloc to +// zero-initialized memory, and such functions "must be known to an analyzer +// enforcing the initialization profile"). The knowledge keys on Clang's +// builtin recognition, so the library functions are declared with matching +// signatures here; -fno-builtin would fall back to the trusted-initialized +// default (a missed diagnostic, never a false positive). + +// no-profiles-warning@+1 {{'profiles::enforce' attribute ignored}} +[[profiles::enforce(std::init)]]; + +typedef __SIZE_TYPE__ size_t; +extern "C" void *malloc(size_t); +extern "C" void *calloc(size_t, size_t); +extern "C" void *realloc(void *, size_t); +extern "C" void *aligned_alloc(size_t, size_t); + +void take_uninit_ptr(int *p [[ref_to_uninit]]); +void take_ptr(int *p); + +// malloc returns uninitialized memory: a marked target accepts it, and an +// unmarked one must not bind it (paper §4.3's `void* p2 = &x2` error). +void test_malloc() { + int *p [[ref_to_uninit]] = (int *)malloc(4); // OK: the paper's canonical use + void *v [[ref_to_uninit]] = malloc(4); // OK: no cast needed for void* + int *q = (int *)malloc(4); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + take_uninit_ptr((int *)malloc(4)); // OK + take_ptr((int *)malloc(4)); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} + +// The __builtin_ spellings need no declaration and classify identically. +void test_builtin_spellings() { + int *p [[ref_to_uninit]] = (int *)__builtin_malloc(4); // OK + int *q = (int *)__builtin_malloc(4); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *r = (int *)__builtin_calloc(1, 4); // OK: zero-initialized +} + +// calloc zero-initializes (paper §4.3), so its result is initialized memory +// and the marked direction flips. +void test_calloc() { + int *p = (int *)calloc(1, 4); // OK + int *q [[ref_to_uninit]] = (int *)calloc(1, 4); // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} +} + +// realloc preserves a prefix and leaves the tail indeterminate: +// affirmatively neither initialized nor uninitialized, so neither binding +// direction diagnoses. +void test_realloc(void *v) { + int *p = (int *)realloc(v, 8); // OK: unknown + int *q [[ref_to_uninit]] = (int *)realloc(v, 8); // OK: unknown +} + +void test_aligned_alloc() { + int *p [[ref_to_uninit]] = (int *)aligned_alloc(16, 16); // OK + int *q = (int *)aligned_alloc(16, 16); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} + +// alloca's stack memory is as uninitialized as malloc's heap memory. +void test_alloca() { + int *p [[ref_to_uninit]] = (int *)__builtin_alloca(4); // OK + int *q = (int *)__builtin_alloca(4); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} + +// Allocator sources compose with the existing machinery: a write through the +// marked pointer is the pointee's initialization (with parse-order credit), +// and a read before it is the read-through violation. +void test_write_then_read() { + int *p [[ref_to_uninit]] = (int *)malloc(4); + *p = 5; // OK: initializes the pointee + int x = *p; // OK: credited +} + +void test_read_through() { + int *p [[ref_to_uninit]] = (int *)malloc(4); + int x = *p; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} +} + +// A direct call to a replaceable global allocation function returns +// uninitialized memory exactly like malloc (a new-*expression* is recognized +// separately, from its initialization style). +void test_operator_new() { + int *p [[ref_to_uninit]] = (int *)::operator new(4); // OK + int *q = (int *)::operator new(4); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *r = (int *)::operator new[](8); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *s [[ref_to_uninit]] = (int *)__builtin_operator_new(4); // OK +} + +// A class-specific operator new is not replaceable; its semantics belong to +// its class, so it stays a trusted unmarked callee. +struct PoolAllocated { + static void *operator new(size_t); +}; +void test_class_specific_operator_new() { + int *p [[ref_to_uninit]] = (int *)PoolAllocated::operator new(4); // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + int *q = (int *)PoolAllocated::operator new(4); // OK: trusted +} + +// Suppression covers the binding like any other ref_to_uninit site. +void test_suppress() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "ref_to_uninit")]] + int *q = (int *)malloc(4); // OK: suppressed +} + +// A Decl-less binding of a non-dependent allocator call (a call argument) is +// checked at definition time even in a never-instantiated template, like the +// other all-non-dependent shapes (TreeTransform may reuse the node). +template +void template_malloc_arg() { + take_ptr((int *)malloc(4)); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} diff --git a/clang/test/SemaCXX/safety-profile-init-ctor-body.cpp b/clang/test/SemaCXX/safety-profile-init-ctor-body.cpp new file mode 100644 index 0000000000000..dd379afd18665 --- /dev/null +++ b/clang/test/SemaCXX/safety-profile-init-ctor-body.cpp @@ -0,0 +1,605 @@ +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 -Wno-uninitialized %s +// RUN: %clang_cc1 -fsyntax-only -verify=no-profiles -std=c++23 -Wno-uninitialized %s +// The ERROR run adds a leading unrelated error so every later function is +// analyzed through the post-error path; the same constructor-body diagnostics +// must still fire there. +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 -Wno-uninitialized -DLEADING_ERROR %s + +// no-profiles-warning@+1 {{'profiles::enforce' attribute ignored}} +[[profiles::enforce(std::init)]]; + +#ifdef LEADING_ERROR +int leading_unrelated_error = undeclared_identifier; +// expected-error@-1 {{use of undeclared identifier 'undeclared_identifier'}} +#endif + +namespace std { enum class byte : unsigned char {}; } + +// A [[uninit]] member that is never read needs no assignment: the constructor +// is not required to initialize it (paper §5.1/§5.3). +struct NeverReadEmpty { + int m [[uninit]]; + NeverReadEmpty() {} +}; + +struct NeverReadActive { + int m [[uninit]]; + int other = 0; + NeverReadActive() { other = 1; } +}; + +struct ReadAfterAssign { + int m [[uninit]]; + ReadAfterAssign() { m = 1; int y = m; (void)y; } +}; + +struct ReadBeforeAssign { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + ReadBeforeAssign() { int y = m; (void)y; m = 1; } // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +}; + +struct SelfReadOnRHS { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + SelfReadOnRHS() { m = m + 1; } // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +}; + +struct CompoundAssignReads { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + CompoundAssignReads() { m += 1; } // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +}; + +// A built-in increment or decrement reads the member's old (uninitialized) +// value before writing, exactly like a compound assignment. +struct PreIncReads { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + PreIncReads() { ++m; } // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +}; + +struct PostIncReads { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + PostIncReads() { m++; } // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +}; + +struct PreDecReads { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + PreDecReads() { --m; } // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +}; + +struct PostDecReads { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + PostDecReads() { m--; } // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +}; + +struct IncDecAfterAssign { + int m [[uninit]]; + IncDecAfterAssign() { m = 0; ++m; m--; } +}; + +struct PostIncInInitBeforeAssign { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + PostIncInInitBeforeAssign() { int y = m++; (void)y; } // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +}; + +struct PostIncInInitAfterAssign { + int m [[uninit]]; + PostIncInInitAfterAssign() { m = 0; int y = m++; (void)y; } +}; + +struct IncExplicitThis { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + IncExplicitThis() { ++this->m; } // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +}; + +struct IncDerefThis { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + IncDerefThis() { ++(*this).m; } // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +}; + +// Incrementing another object's member is not a read of the current object. +struct IncOtherObject { + int m [[uninit]]; + IncOtherObject(IncOtherObject &o) { m = 0; ++o.m; } +}; + +template +struct IncTmpl { + T m [[uninit]]; // expected-note {{member 'm' declared here}} + IncTmpl() { ++m; } // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +}; +template struct IncTmpl; // expected-note {{in instantiation of member function 'IncTmpl::IncTmpl' requested here}} + +struct IncSuppressedByRule { + int m [[uninit]]; + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "uninit_read")]] IncSuppressedByRule() { ++m; } +}; + +struct IncSuppressedStmt { + int m [[uninit]]; + IncSuppressedStmt() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] { ++m; } + } +}; + +struct OneBranchThenRead { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + OneBranchThenRead(bool b) { + if (b) + m = 1; + int y = m; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} + (void)y; + } +}; + +struct BothBranchesThenRead { + int m [[uninit]]; + BothBranchesThenRead(bool b) { + if (b) + m = 1; + else + m = 2; + int y = m; + (void)y; + } +}; + +struct LoopBodyThenRead { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + LoopBodyThenRead(int n) { + for (int i = 0; i < n; ++i) + m = i; + int y = m; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} + (void)y; + } +}; + +// std::byte may be read while uninitialized (paper §4.5). +struct ByteExempt { + std::byte b [[uninit]]; + ByteExempt() { std::byte c = b; (void)c; } +}; + +// A member initialized in the (written) member-initializer list is assigned at +// body entry, so a later read is fine and no marker/list-init contradiction is +// introduced. +struct MarkerWithListInit { + int m [[uninit]]; + MarkerWithListInit() : m(0) { int y = m; (void)y; } +}; + +// Reads inside the written member-initializer list are checked in execution +// order (declaration order): a member becomes assigned at its own +// initializer, so an earlier member initializer -- or a base initializer, +// which runs before all member initializers -- reading it is a +// read-before-init. +struct InitListReadBefore { + int o; + int m [[uninit]]; // expected-note {{member 'm' declared here}} + InitListReadBefore() : o(m), m(5) {} // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +}; + +struct InitListReadAfter { + int m [[uninit]]; + int o; + InitListReadAfter() : m(5), o(m) {} // OK: m's initializer runs first +}; + +struct InitListReadNoInit { + int o; + int m [[uninit]]; // expected-note {{member 'm' declared here}} + InitListReadNoInit() : o(m) {} // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +}; + +struct InitListSelfRead { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + InitListSelfRead() : m(m + 1) {} // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +}; + +struct InitBase { + InitBase(int); +}; +struct InitListBaseRead : InitBase { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + InitListBaseRead() : InitBase(m) {} // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +}; + +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +struct [[profiles::suppress(std::init)]] InitListReadSuppressed { + int o; + int m [[uninit]]; + InitListReadSuppressed() : o(m) {} // OK: suppressed +}; + +// A this-capturing lambda created in the ctor body may run immediately, so a +// member read in its body counts as a read at the point the lambda is +// created. Writes in the body earn no assignment credit (the lambda may never +// run), and a lambda stored now but called only after assignment is flagged +// all the same (accepted imprecision). +struct LambdaReadInvoked { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + LambdaReadInvoked() { + int y = [this] { return m; }(); // expected-error {{member 'm' is read before initialization under profile 'std::init'}} + (void)y; + } +}; + +struct LambdaReadAfterAssign { + int m [[uninit]]; + LambdaReadAfterAssign() { + m = 1; + auto l = [this] { return m; }; // OK: m assigned on every path here + (void)l; + } +}; + +struct LambdaWriteNoCredit { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + LambdaWriteNoCredit() { + auto l = [this] { m = 1; }; // OK: a body write is not a read + l(); + int y = m; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} + (void)y; + } +}; + +struct LambdaNoMemberUse { + int m [[uninit]]; + LambdaNoMemberUse() { + auto l = [this] { return 42; }; // OK: body touches no member + m = l(); + } +}; + +struct LambdaCompoundRead { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + LambdaCompoundRead() { + auto l = [this] { m += 1; }; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} + (void)l; + } +}; + +struct LambdaNestedRead { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + LambdaNestedRead() { + auto l = [this] { return [this] { return m; }; }; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} + (void)l; + } +}; + +struct LambdaImplicitThisCapture { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + LambdaImplicitThisCapture() { + auto l = [&] { return m; }; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} + (void)l; + } +}; + +// An init-capture's initializer is an ordinary CFG element of the ctor and is +// checked by the plain read arm, independent of the body scan. +struct LambdaCaptureInitRead { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + LambdaCaptureInitRead() { + auto l = [v = m] { return v; }; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} + (void)l; + } +}; + +struct LambdaReadSuppressed { + int m [[uninit]]; + LambdaReadSuppressed() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] { + auto l = [this] { return m; }; // OK: suppressed + (void)l; + } + } +}; + +// Strict crediting for plain escapes: inside a constructor body, nothing but +// a whole-member assignment counts as initializing an [[uninit]] member. +// Passing &m to a [[ref_to_uninit]] parameter of an ordinary function, +// calling a member function that assigns it, or letting `this` escape earns +// no credit -- the paper rejects complex constructor code (§5.1) and +// reserves callee-initialization for now_init (§6.2, the [[now_init]] tests +// below); the remedy for a plain callee is [[profiles::suppress]] or the +// [[now_init]] annotation. Contrast the *local*-object passes, which +// conservatively credit any escape +// (safety-profile-init-local-member-read.cpp, test_escape_*). +void init_pointee(int *p [[ref_to_uninit]]); +struct EscapeMemberAddress { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + EscapeMemberAddress() { + init_pointee(&m); + int x = m; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} + (void)x; + } +}; + +struct EscapeMemberCall { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + void setup() { m = 1; } + EscapeMemberCall() { + setup(); + int x = m; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} + (void)x; + } +}; + +struct EscapeThis; +void take_this(EscapeThis *); +struct EscapeThis { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + EscapeThis() { + take_this(this); + int x = m; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} + (void)x; + } +}; + +// The §6.2 exception: a call to a [[now_init]] function initializes the +// storage bound to each of its [[ref_to_uninit]] parameters, so a +// current-object member passed as `&m` (or `m` to a reference parameter) +// becomes assigned at the call. The credit is a real Gen bit in the +// dataflow -- not parse-order -- so §1.2's all-branches rule still governs: +// a [[now_init]] call under one branch does not satisfy a read at the join. +[[now_init]] void now_init_pointee(int *p [[ref_to_uninit]]); +[[now_init]] void now_init_referent(int &r [[ref_to_uninit]]); +struct NowInitMemberAddress { + int m [[uninit]]; + NowInitMemberAddress() { + now_init_pointee(&m); + int x = m; // OK: the [[now_init]] callee initialized m + (void)x; + } +}; + +struct NowInitMemberReference { + int m [[uninit]]; + NowInitMemberReference() { + now_init_referent(m); + int x = m; // OK + (void)x; + } +}; + +struct NowInitReadBeforeCall { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + NowInitReadBeforeCall() { + int x = m; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} + now_init_pointee(&m); + (void)x; + } +}; + +struct NowInitUnderBranch { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + NowInitUnderBranch(bool b) { + if (b) + now_init_pointee(&m); + int x = m; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} + (void)x; + } +}; + +struct NowInitBothBranches { + int m [[uninit]]; + NowInitBothBranches(bool b) { + if (b) + now_init_pointee(&m); + else + m = 0; + int x = m; // OK: initialized on every path + (void)x; + } +}; + +// Only the *marked* parameters carry the promise: an unmarked parameter of +// the same [[now_init]] callee earns nothing -- and handing it &m is already +// the parse-time binding violation. +[[now_init]] void now_init_first(int *p [[ref_to_uninit]], int *q); +struct NowInitUnmarkedParam { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + int o [[uninit]]; + NowInitUnmarkedParam() { + now_init_first(&o, &m); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int x = o; // OK: bound to the marked parameter + int y = m; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} + (void)x; (void)y; + } +}; + +// Passing `this` itself to a marked parameter hands the callee the whole +// object to initialize: every tracked member is assigned at the call. (The +// implicit object parameter of a member call cannot carry the marker, so a +// member call still earns nothing -- see EscapeMemberCall.) +struct NowInitWholeObject; +[[now_init]] void now_init_object(NowInitWholeObject *obj [[ref_to_uninit]]); +struct NowInitWholeObject { + int a [[uninit]]; + int b [[uninit]]; + NowInitWholeObject() { + now_init_object(this); + int x = a + b; // OK: the whole object was handed over for initialization + (void)x; + } +}; + +// Argument mapping through operator calls: a member operator() receives the +// object as operator-call argument 0 ahead of its declared parameters -- +// for a C++23 static operator() too, whose object argument is still +// evaluated -- while an explicit-object operator() declares the object as +// parameter 0 and maps arguments directly. +struct NowInitStaticFunctor { + [[now_init]] static void operator()(int *p [[ref_to_uninit]]); +}; +struct NowInitStaticOperator { + int m [[uninit]]; + NowInitStaticOperator() { + NowInitStaticFunctor f; + f(&m); + int x = m; // OK: &m bound the static operator()'s marked parameter + (void)x; + } +}; + +struct NowInitExplicitObjectFunctor { + [[now_init]] void operator()(this NowInitExplicitObjectFunctor &, + int *p [[ref_to_uninit]]); +}; +struct NowInitExplicitObjectOperator { + int m [[uninit]]; + NowInitExplicitObjectOperator() { + NowInitExplicitObjectFunctor f; + f(&m); + int x = m; // OK: &m bound the explicit-object operator()'s parameter 1 + (void)x; + } +}; + +// A [[now_init]] call inside a written member initializer credits in +// execution order: the call element precedes the initializer's own write, so +// a body read of the passed member is already covered. +[[now_init]] int now_init_count(int *p [[ref_to_uninit]]); +struct NowInitInMemInit { + int m [[uninit]]; + int n; + NowInitInMemInit() : n(now_init_count(&m)) { + int x = m; // OK: credited at the call inside n's initializer + (void)x; + } +}; + +// [[uninit]] members inherited from a non-virtual base with no user-provided +// constructor are tracked like the class's own: nothing can have assigned +// them before the derived body runs. A base with a user-provided constructor +// is trusted (paper §5.1) and its members left alone. +struct BaseRead { + int bm [[uninit]]; // expected-note {{member 'bm' declared here}} +}; +struct DerivedRead : BaseRead { + DerivedRead() { int y = bm; (void)y; } // expected-error {{member 'bm' is read before initialization under profile 'std::init'}} +}; + +struct BaseAssign { + int bm [[uninit]]; +}; +struct DerivedAssign : BaseAssign { + DerivedAssign() { bm = 1; int y = bm; (void)y; } // OK +}; + +struct BaseWritten { + int bm [[uninit]]; +}; +struct DerivedWrittenBaseInit : BaseWritten { + DerivedWrittenBaseInit() : BaseWritten{1} { int y = bm; (void)y; } // OK: written base initializer +}; + +struct BaseTrusted { + int bm [[uninit]]; + BaseTrusted() {} +}; +struct DerivedTrustedBase : BaseTrusted { + DerivedTrustedBase() { int y = bm; (void)y; } // OK: base ctor trusted (paper §5.1) +}; + +struct GrandBase { + int gm [[uninit]]; // expected-note {{member 'gm' declared here}} +}; +struct MidBase : GrandBase {}; +struct DerivedTwoLevels : MidBase { + DerivedTwoLevels() { int y = gm; (void)y; } // expected-error {{member 'gm' is read before initialization under profile 'std::init'}} +}; + +struct BaseSup { + int bm [[uninit]]; +}; +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +struct [[profiles::suppress(std::init)]] DerivedSup : BaseSup { + DerivedSup() { int y = bm; (void)y; } // OK: suppressed +}; + +// A delegating constructor's target initializes the members before the body +// runs (paper §5.1), so its body is not analyzed. +struct Delegating { + int m [[uninit]]; + Delegating() : Delegating(0) { int y = m; (void)y; } + Delegating(int v) : m(v) {} +}; + +// A read of another object's member is not a read of the current object. +struct ReadsOtherObject { + int m [[uninit]]; + ReadsOtherObject() { m = 0; } + ReadsOtherObject(const ReadsOtherObject &o) { m = o.m; } +}; + +struct MultipleMembers { + int a [[uninit]]; + int b [[uninit]]; // expected-note {{member 'b' declared here}} + int c [[uninit]]; + MultipleMembers() { + a = 1; + int x = a; (void)x; + int y = b; (void)y; // expected-error {{member 'b' is read before initialization under profile 'std::init'}} + c = 2; + int z = c; (void)z; + } +}; + +struct ExplicitThis { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + ExplicitThis() { int y = this->m; (void)y; } // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +}; + +// Writing through `(*this).m` is an initialization, so a later read of m is +// fine -- the same as `this->m` (the reported false positive). +struct DerefThisWriteThenRead { + int m [[uninit]]; + DerefThisWriteThenRead() { (*this).m = 1; int y = m; (void)y; } +}; + +// A real read through `(*this).m` before assignment is still diagnosed. +struct DerefThisReadBeforeInit { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + DerefThisReadBeforeInit() { int y = (*this).m; (void)y; } // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +}; + +struct OutOfLine { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + OutOfLine(); +}; +OutOfLine::OutOfLine() { int y = m; (void)y; } // expected-error {{member 'm' is read before initialization under profile 'std::init'}} + +template +struct Tmpl { + T m [[uninit]]; // expected-note {{member 'm' declared here}} + Tmpl() { T y = m; (void)y; } // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +}; +template struct Tmpl; // expected-note {{in instantiation of member function 'Tmpl::Tmpl' requested here}} + +struct SuppressedCtor { + int m [[uninit]]; + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] SuppressedCtor() { int y = m; (void)y; } +}; + +struct SuppressedByRule { + int m [[uninit]]; + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "uninit_read")]] SuppressedByRule() { int y = m; (void)y; } +}; + +struct SuppressedStmt { + int m [[uninit]]; + SuppressedStmt() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] { int y = m; (void)y; } + } +}; + +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +struct [[profiles::suppress(std::init)]] SuppressedClass { + int m [[uninit]]; + SuppressedClass() { int y = m; (void)y; } +}; diff --git a/clang/test/SemaCXX/safety-profile-init-ctor.cpp b/clang/test/SemaCXX/safety-profile-init-ctor.cpp new file mode 100644 index 0000000000000..bb54e97781602 --- /dev/null +++ b/clang/test/SemaCXX/safety-profile-init-ctor.cpp @@ -0,0 +1,365 @@ +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 %s +// RUN: %clang_cc1 -fsyntax-only -verify=no-profiles -std=c++23 %s + +// no-profiles-warning@+1 {{'profiles::enforce' attribute ignored}} +[[profiles::enforce(std::init)]]; + +struct WithCtor { WithCtor(); }; +struct Inner { int y; }; +struct InnerMarked { int y [[uninit]]; }; +struct InnerMixed { int a [[uninit]]; int b; }; + +struct MissingMember { + int x; // expected-note {{member 'x' declared here}} + MissingMember() {} // expected-error {{constructor does not initialize member 'x' under profile 'std::init'}} +}; + +struct MemInit { + int x; + MemInit() : x(0) {} +}; + +struct DefaultMemberInit { + int x = 0; + DefaultMemberInit() {} +}; + +struct Marked { + int x [[uninit]]; + Marked() {} +}; + +struct BodyAssignment { + int x; // expected-note {{member 'x' declared here}} + // A plain body assignment is not initialization for this rule. + BodyAssignment() { x = 0; } // expected-error {{constructor does not initialize member 'x' under profile 'std::init'}} +}; + +struct NestedAggregate { + Inner m; // expected-note {{member 'm' declared here}} + NestedAggregate() {} // expected-error {{constructor does not initialize member 'm' under profile 'std::init'}} +}; + +// A member whose type's only indeterminate scalar is [[uninit]] is +// acknowledged (paper §6.2), so the constructor need not initialize it. +struct NestedMarkedMember { + InnerMarked m; + NestedMarkedMember() {} +}; + +// A member whose type still leaves an unacknowledged scalar indeterminate fires. +struct NestedMixedMember { + InnerMixed m; // expected-note {{member 'm' declared here}} + NestedMixedMember() {} // expected-error {{constructor does not initialize member 'm' under profile 'std::init'}} +}; + +struct TrustedMemberCtor { + WithCtor m; + TrustedMemberCtor() {} +}; + +struct PartialInit { + int x; + int y; // expected-note {{member 'y' declared here}} + PartialInit() : x(0) {} // expected-error {{constructor does not initialize member 'y' under profile 'std::init'}} +}; + +struct OutOfLine { + int x; // expected-note {{member 'x' declared here}} + OutOfLine(); +}; +OutOfLine::OutOfLine() {} // expected-error {{constructor does not initialize member 'x' under profile 'std::init'}} + +template +struct Tmpl { + T x; // expected-note {{member 'x' declared here}} + Tmpl() {} // expected-error {{constructor does not initialize member 'x' under profile 'std::init'}} +}; +template struct Tmpl; // expected-note {{in instantiation of member function 'Tmpl::Tmpl' requested here}} + +// A delegating constructor relies on its target, which does initialize the +// member, so nothing fires. +struct Delegating { + int x; + Delegating() : Delegating(0) {} + Delegating(int v) : x(v) {} +}; + +struct SuppressedCtor { + int x; + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] SuppressedCtor() {} +}; + +struct SuppressedByRule { + int x; + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "ctor_uninit_member")]] SuppressedByRule() {} +}; + +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +struct [[profiles::suppress(std::init)]] SuppressedClass { + int x; + SuppressedClass() {} +}; + +// Constructor finalization ignores the parse-time suppress stack (it can fire +// nested in an unrelated instantiation), so a [[profiles::suppress]] on a class +// template must still reach the instantiated constructor through the decl-aware +// walk on the lexical parent -- not the stack pushed while instantiating it. +template +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +struct [[profiles::suppress(std::init)]] SuppressedTemplate { + T x; + SuppressedTemplate() {} +}; +template struct SuppressedTemplate; + +struct Base { int b; }; + +struct UninitBase : Base { // expected-note {{base class 'Base' declared here}} + UninitBase() {} // expected-error {{constructor does not initialize base class 'Base' under profile 'std::init'}} +}; + +struct BaseInitParen : Base { + BaseInitParen() : Base() {} +}; + +struct BaseInitBraces : Base { + BaseInitBraces() : Base{} {} +}; + +// A base with a user-provided default constructor is trusted. +struct TrustedBaseCtor : WithCtor { + TrustedBaseCtor() {} +}; + +// A base whose only indeterminate scalar is [[uninit]]-marked is trusted. +struct MarkedBaseSub : InnerMarked { + MarkedBaseSub() {} +}; + +struct MixedBaseMember : Base { // expected-note {{base class 'Base' declared here}} + int x; // expected-note {{member 'x' declared here}} + MixedBaseMember() {} + // expected-error@-1 {{constructor does not initialize member 'x' under profile 'std::init'}} + // expected-error@-2 {{constructor does not initialize base class 'Base' under profile 'std::init'}} +}; + +struct MultipleBases : Base, Inner { + // expected-note@-1 {{base class 'Base' declared here}} + // expected-note@-2 {{base class 'Inner' declared here}} + MultipleBases() {} + // expected-error@-1 {{constructor does not initialize base class 'Base' under profile 'std::init'}} + // expected-error@-2 {{constructor does not initialize base class 'Inner' under profile 'std::init'}} +}; + +struct OutOfLineBase : Base { // expected-note {{base class 'Base' declared here}} + OutOfLineBase(); +}; +OutOfLineBase::OutOfLineBase() {} // expected-error {{constructor does not initialize base class 'Base' under profile 'std::init'}} + +template +struct TmplBase : T { // expected-note {{base class 'Base' declared here}} + TmplBase() {} // expected-error {{constructor does not initialize base class 'Base' under profile 'std::init'}} +}; +template struct TmplBase; // expected-note {{in instantiation of member function 'TmplBase::TmplBase' requested here}} + +struct SuppressedBaseByRule : Base { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "ctor_uninit_member")]] SuppressedBaseByRule() {} +}; + +// A virtual base is the most-derived constructor's responsibility, so leaving +// it uninitialized here is deferred: no diagnostic. +struct VirtualBase : virtual Base { + VirtualBase() {} +}; + +// ============================================================ +// Anonymous aggregate members +// ============================================================ + +// The leaves of an anonymous struct member initialize exactly like direct +// members (a written initializer for one is an indirect member-initializer), +// so a constructor must cover them the same way. +struct AnonStructMissing { + struct { + int x; // expected-note {{member 'x' declared here}} + }; + AnonStructMissing() {} // expected-error {{constructor does not initialize member 'x' under profile 'std::init'}} +}; + +struct AnonStructMemInit { + struct { + int x; + }; + AnonStructMemInit() : x(1) {} // OK: indirect member-initializer covers the leaf +}; + +struct AnonStructNSDMI { + struct { + int x = 0; + }; + AnonStructNSDMI() {} // OK: the leaf's default member initializer covers it +}; + +struct AnonStructMarked { + struct { + int x [[uninit]]; + }; + AnonStructMarked() {} // OK: the leaf's marker acknowledges it +}; + +struct AnonStructPartial { + struct { + int x; + int y; // expected-note {{member 'y' declared here}} + }; + AnonStructPartial() : x(1) {} // expected-error {{constructor does not initialize member 'y' under profile 'std::init'}} +}; + +struct AnonStructNested { + struct { + struct { + int deep; // expected-note {{member 'deep' declared here}} + }; + }; + AnonStructNested() {} // expected-error {{constructor does not initialize member 'deep' under profile 'std::init'}} +}; + +struct AnonStructSuppressed { + struct { + int x; + }; + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "ctor_uninit_member")]] AnonStructSuppressed() {} +}; + +// An anonymous union needs one active member, not every member: a written +// leaf initializer or a leaf default member initializer satisfies it. +struct AnonUnionMissing { + union { // expected-note {{anonymous union declared here}} + int i; + float f; + }; + AnonUnionMissing() {} // expected-error {{constructor does not initialize any member of the anonymous union under profile 'std::init'}} +}; + +struct AnonUnionMemInit { + union { + int i; + float f; + }; + AnonUnionMemInit() : i(1) {} // OK: the written leaf is the active member +}; + +struct AnonUnionNSDMI { + union { + int i = 0; + float f; + }; + AnonUnionNSDMI() {} // OK: the leaf's default member initializer is the active member +}; + +// One written leaf of a struct variant activates the union; whether that +// variant is fully initialized is deliberately lenient (a missed diagnostic, +// never a false positive). +struct AnonUnionStructVariant { + union { + struct { + int a; + int b; + }; + float f; + }; + AnonUnionStructVariant() : a(1) {} // OK: lenient +}; + +struct AnonUnionSuppressed { + union { + int i; + float f; + }; + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "ctor_uninit_member")]] AnonUnionSuppressed() {} +}; + +// An anonymous union whose only members are unnamed bit-fields has nothing +// for the constructor to initialize: unnamed bit-fields are not members and +// no in-language initializer exists for them. +struct AnonUnionUnnamedBitFieldOnly { + union { + int : 4; + }; + AnonUnionUnnamedBitFieldOnly() {} // OK: nothing to initialize +}; + +// A std::byte-only anonymous union mirrors the scalar std::byte exemption +// (paper §4.5): whichever member is active may be left uninitialized, so +// there is nothing to acknowledge -- matching the anonymous-struct control, +// which the per-member walk already exempts. A non-byte member alongside +// keeps the union flagged. +namespace std { enum class byte : unsigned char {}; } +struct AnonUnionAllByte { + union { + std::byte b; + }; + AnonUnionAllByte() {} // OK: std::byte may be left uninitialized +}; +struct AnonStructByteControl { + struct { + std::byte b; + }; + AnonStructByteControl() {} // OK: already exempt via the member walk +}; +struct AnonUnionMixedByte { + union { // expected-note {{anonymous union declared here}} + std::byte b; + int i; + }; + AnonUnionMixedByte() {} // expected-error {{constructor does not initialize any member of the anonymous union under profile 'std::init'}} +}; + +// Default member initializers on every leaf of an anonymous-struct variant +// activate it during default-initialization and initialize it completely; +// the static_assert below is the constant-evaluation proof. +struct AnonUnionAllNSDMIVariant { + union { + struct { + int a = 1; + int b = 2; + }; + float f; + }; + constexpr AnonUnionAllNSDMIVariant() {} // OK: the variant is active and complete +}; +static_assert(AnonUnionAllNSDMIVariant().a == 1 && + AnonUnionAllNSDMIVariant().b == 2); + +// A partial-NSDMI variant is activated all the same, but default-init +// leaves its other leaf indeterminate, so the constructor stays diagnosed. +struct AnonUnionPartialNSDMIVariant { + union { // expected-note {{anonymous union declared here}} + struct { + int a = 1; + int b; + }; + float f; + }; + AnonUnionPartialNSDMIVariant() {} // expected-error {{constructor does not initialize any member of the anonymous union under profile 'std::init'}} +}; + +// The activation is visible through nesting: an all-NSDMI anonymous struct +// variant of an anonymous union inside an anonymous struct. +struct AnonNestedNSDMIVariant { + struct { + union { + struct { + int a = 1; + }; + float f; + }; + }; + AnonNestedNSDMIVariant() {} // OK: the nested variant is active and complete +}; diff --git a/clang/test/SemaCXX/safety-profile-init-decl.cpp b/clang/test/SemaCXX/safety-profile-init-decl.cpp new file mode 100644 index 0000000000000..b56e1b22515f4 --- /dev/null +++ b/clang/test/SemaCXX/safety-profile-init-decl.cpp @@ -0,0 +1,190 @@ +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 %s +// RUN: %clang_cc1 -fsyntax-only -verify=no-profiles -std=c++23 %s + +// no-profiles-warning@+1 {{'profiles::enforce' attribute ignored}} +[[profiles::enforce(std::init)]]; + +int sink(int); + +namespace std { enum class byte : unsigned char {}; } + +struct Trivial { int m; }; +struct WithCtor { WithCtor(); int m; }; +struct ByteWrap { std::byte b; }; +struct ByteAndInt { std::byte b; int x; }; + +void test_byte() { + // std::byte may be left uninitialized (paper section 4), as may arrays of it + // and records whose only members are std::byte. + std::byte a; + std::byte buf[8]; + ByteWrap w; + ByteAndInt m; // expected-error {{variable 'm' must be initialized or marked '[[uninit]]' under profile 'std::init'}} + (void)a; (void)buf; (void)w; (void)m; +} + +void test_scalars() { + int a; // expected-error {{variable 'a' must be initialized or marked '[[uninit]]' under profile 'std::init'}} + int b = 0; + int c [[uninit]]; + int d{}; + int e = sink(1); + (void)b; (void)c; (void)d; (void)e; +} + +void test_pointer() { + int* p; // expected-error {{variable 'p' must be initialized or marked '[[uninit]]' under profile 'std::init'}} + int* q = nullptr; + // A pointer cannot be left uninitialized (paper section 4.1); the marker is + // rejected rather than excusing it. + int* r [[uninit]]; // expected-error {{'[[uninit]]' cannot be applied to a pointer under profile 'std::init'; initialize the pointer (for example to 'nullptr')}} + (void)q; (void)r; +} + +struct PtrMember { + int* p [[uninit]]; // expected-error {{'[[uninit]]' cannot be applied to a pointer under profile 'std::init'; initialize the pointer (for example to 'nullptr')}} +}; + +// The marker checks key on the base element type: an array of pointers is +// banned exactly like a single pointer (paper section 4.1) -- the marker must +// not smuggle uninitialized pointers past uninit_decl element-wise. +void test_pointer_array() { + [[uninit]] int* a[2]; // expected-error {{'[[uninit]]' cannot be applied to a pointer under profile 'std::init'; initialize the pointer (for example to 'nullptr')}} + [[uninit]] int* b[2][3]; // expected-error {{'[[uninit]]' cannot be applied to a pointer under profile 'std::init'; initialize the pointer (for example to 'nullptr')}} + (void)a; (void)b; +} + +struct PtrArrayMember { + [[uninit]] int* a[2]; // expected-error {{'[[uninit]]' cannot be applied to a pointer under profile 'std::init'; initialize the pointer (for example to 'nullptr')}} +}; + +void test_pointer_array_marker_suppressed() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "pointer_marker")]] [[uninit]] int* a[2]; + (void)a; +} + +void test_pointer_marker_suppressed() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] int* p [[uninit]]; + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "pointer_marker")]] int* q [[uninit]]; + (void)p; (void)q; +} + +enum E { E0, E1 }; +void test_enum() { + E x; // expected-error {{variable 'x' must be initialized or marked '[[uninit]]' under profile 'std::init'}} + E y = E0; + E z [[uninit]]; + (void)y; (void)z; +} + +void test_class_with_user_ctor() { + // OK: user-provided default constructor is trusted. The marker is + // unnecessary on class types whose default-init runs a constructor; in + // fact combining them is a contradiction caught by R4 (the constructor + // call is the synthesized initializer). + WithCtor w; + (void)w; +} + +void test_class_trivial() { + // A trivial / aggregate class whose default-initialization leaves a scalar + // member indeterminate is diagnosed by R5 (uninit_decl), the §6 + // "classes without constructors" rule. (Detailed coverage lives in + // safety-profile-init-aggregate.cpp.) + Trivial t; // expected-error {{variable 't' must be initialized or marked '[[uninit]]' under profile 'std::init'}} + (void)t; +} + +void test_static_local() { + static int s; + thread_local int t; + (void)s; (void)t; +} + +int g_namespace_scalar; +thread_local int g_thread; +extern int g_extern; + +void test_param(int p) { + (void)p; +} + +void test_marker_then_assign() { + int x [[uninit]]; + x = 7; + (void)x; +} + +void test_suppress_decl() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "uninit_decl")]] int x; + (void)x; +} + +void test_suppress_block() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] { + int a; + int b; + (void)a; (void)b; + } +} + +template +void template_uninit() { + T x; // expected-error {{variable 'x' must be initialized or marked '[[uninit]]' under profile 'std::init'}} + (void)x; +} +template void template_uninit(); // expected-note {{in instantiation of function template specialization 'template_uninit' requested here}} + +// A *non-dependent* uninitialized local in a template body is diagnosed once -- +// at instantiation -- not on the template pattern (no double-fire). +template +void template_uninit_nondependent() { + int x; // expected-error {{variable 'x' must be initialized or marked '[[uninit]]' under profile 'std::init'}} + (void)x; +} +template void template_uninit_nondependent(); // expected-note {{in instantiation of function template specialization 'template_uninit_nondependent' requested here}} + +// An uninstantiated template pattern never reaches phase 7, so no rule fires. +template +void template_uninit_never_instantiated() { + int x; + (void)x; +} + +// A dependent local that substitutes to a pointer is deferred on the pattern +// and fires pointer_marker at instantiation, not on the template. +template +void template_ptr_marker() { + T x [[uninit]]; // #template-ptr-marker + (void)x; +} +template void template_ptr_marker(); // expected-note {{in instantiation of function template specialization 'template_ptr_marker' requested here}} +// expected-error@#template-ptr-marker {{'[[uninit]]' cannot be applied to a pointer under profile 'std::init'; initialize the pointer (for example to 'nullptr')}} + +// A [[profiles::suppress(std::init)]] live at the point of instantiation +// covers the trigger's tokens, not the pattern's (P3589R2 s2.4p3): rules +// inside a synchronously instantiated pattern must still fire. +template +auto instantiation_leak_use() { + T t; // expected-error {{variable 't' must be initialized or marked '[[uninit]]' under profile 'std::init'}} + (void)t; + return 0; +} +void instantiation_leak_trigger() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] int leaked = instantiation_leak_use(); // expected-note {{in instantiation of function template specialization 'instantiation_leak_use' requested here}} + (void)leaked; +} + +// A declarator-position suppress covers a diagnostic located at the declared +// name: entries anchor to the construct's begin, not the attribute's. +void declarator_position_suppress() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + int c [[profiles::suppress(std::init, rule: "uninit_decl")]]; + (void)c; +} diff --git a/clang/test/SemaCXX/safety-profile-init-field-marker.cpp b/clang/test/SemaCXX/safety-profile-init-field-marker.cpp new file mode 100644 index 0000000000000..2950303f009ab --- /dev/null +++ b/clang/test/SemaCXX/safety-profile-init-field-marker.cpp @@ -0,0 +1,262 @@ +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 %s +// RUN: %clang_cc1 -fsyntax-only -verify=no-profiles -std=c++23 %s + +// no-profiles-warning@+1 {{'profiles::enforce' attribute ignored}} +[[profiles::enforce(std::init)]]; + +struct PlainField { + int m [[uninit]]; +}; + +struct PlainFieldPrefix { + [[uninit]] int m; +}; + +struct FieldWithNSDMI { + int m [[uninit]] = 0; // expected-error {{member 'm' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} +}; + +struct FieldWithNSDMIPrefix { + [[uninit]] int m = 0; // expected-error {{member 'm' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} +}; + +// A static data member is a zero-initialized static object, so its definition +// is rejected by static_marker (paper section 4.2), even though the marker +// attaches to the declaration; the error fires at the out-of-line definition. +struct WithStaticDataMember { + static int s [[uninit]]; + [[uninit]] static int t; +}; +int WithStaticDataMember::s; // expected-error {{'[[uninit]]' cannot be applied to variable 's' with static storage duration under profile 'std::init'; it is zero-initialized}} +int WithStaticDataMember::t; // expected-error {{'[[uninit]]' cannot be applied to variable 't' with static storage duration under profile 'std::init'; it is zero-initialized}} + +struct MultipleFields { + int a [[uninit]]; + int b = 0; + int c [[uninit]] = 0; // expected-error {{member 'c' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} +}; + +template +struct DependentField { + T m [[uninit]]; // #dependent-field-member +}; +template struct DependentField; // OK: a non-pointer, non-union member + +// The marker is deferred on the dependent pattern and re-checked once the +// substituted type is known, so a pointer member fires pointer_marker at +// instantiation (paper section 4.1). +template struct DependentField; // expected-note {{in instantiation of template class 'DependentField' requested here}} +// expected-error@#dependent-field-member {{'[[uninit]]' cannot be applied to a pointer under profile 'std::init'; initialize the pointer (for example to 'nullptr')}} + +// The deferral is keyed on the member being templated, not on its type being +// dependent: a literally non-dependent pointer member inside a template still +// defers on the pattern and fires once, at instantiation. +template +struct NonDependentPtrField { + int *m [[uninit]]; // #nondependent-ptr-field +}; +template struct NonDependentPtrField; // expected-note {{in instantiation of template class 'NonDependentPtrField' requested here}} +// expected-error@#nondependent-ptr-field {{'[[uninit]]' cannot be applied to a pointer under profile 'std::init'; initialize the pointer (for example to 'nullptr')}} + +// An uninstantiated pattern is not yet a phase-7 entity, so nothing fires. +template +struct DependentFieldNeverInstantiated { + T m [[uninit]]; +}; + +// A dependent member instantiating to a *reference* type is rejected at +// instantiation and the marker dropped, mirroring the parse-time rejection of +// a non-dependent reference member. Like that rejection -- and unlike the +// profile-gated pointer/union rules -- this fires regardless of -fprofiles. +template +struct DependentRefField { + T m [[uninit]]; // #dependent-ref-field +}; +template struct DependentRefField; // OK +template struct DependentRefField; // expected-note {{in instantiation of template class 'DependentRefField' requested here}} \ + // no-profiles-note {{in instantiation of template class 'DependentRefField' requested here}} +// expected-error@#dependent-ref-field {{'uninit' attribute cannot be applied to a reference}} +// no-profiles-error@#dependent-ref-field {{'uninit' attribute cannot be applied to a reference}} + +int g_ref_target = 0; +template +void dependent_ref_local() { + T v [[uninit]] = g_ref_target; // #dependent-ref-local + (void)v; +} +template void dependent_ref_local(); // expected-note {{in instantiation of function template specialization 'dependent_ref_local' requested here}} \ + // no-profiles-note {{in instantiation of function template specialization 'dependent_ref_local' requested here}} +// expected-error@#dependent-ref-local {{'uninit' attribute cannot be applied to a reference}} +// no-profiles-error@#dependent-ref-local {{'uninit' attribute cannot be applied to a reference}} + +// Suppression on the dependent member carries through instantiation. +template +struct DependentFieldSuppressed { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] T m [[uninit]]; +}; +template struct DependentFieldSuppressed; + +// A member of a union template fires union_marker at instantiation regardless +// of the substituted type (paper section 5.6). +template +union DependentUnion { + T m [[uninit]]; // #dependent-union-member + int tag; +}; +int dependent_union_size = sizeof(DependentUnion); // expected-note {{in instantiation of template class 'DependentUnion' requested here}} +// expected-error@#dependent-union-member {{'[[uninit]]' cannot be applied to a union member under profile 'std::init'}} + +// expected-error@+2 {{'uninit' attribute only applies to variables and non-static data members}} +// no-profiles-error@+1 {{'uninit' attribute only applies to variables and non-static data members}} +[[uninit]] void f(); + +// Subjects on which "leave uninitialized" is meaningless are rejected +// regardless of -fprofiles. +struct ReferenceField { + int &r [[uninit]]; // expected-error {{'uninit' attribute cannot be applied to a reference}} \ + // no-profiles-error {{'uninit' attribute cannot be applied to a reference}} +}; + +void test_invalid_subjects(int p [[uninit]]) { // expected-error {{'uninit' attribute cannot be applied to a function parameter}} \ + // no-profiles-error {{'uninit' attribute cannot be applied to a function parameter}} + int n = 0; + int &lr [[uninit]] = n; // expected-error {{'uninit' attribute cannot be applied to a reference}} \ + // no-profiles-error {{'uninit' attribute cannot be applied to a reference}} + int arr[2] = {1, 2}; + [[uninit]] auto [a, b] = arr; // expected-error {{'uninit' attribute cannot be applied to a structured binding}} \ + // no-profiles-error {{'uninit' attribute cannot be applied to a structured binding}} + (void)p; (void)lr; (void)a; (void)b; +} + +// The marker re-check on the instantiated field is not suppressed by a +// [[profiles::suppress(std::init)]] live at the point of instantiation -- +// the pattern's tokens are outside that dominion (P3589R2 s2.4p3). +template +struct LeakMarker { + T p [[uninit]]; // #leak-marker-field +}; +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(std::init)]] LeakMarker leak_marker_use{nullptr}; // expected-note {{in instantiation of template class 'LeakMarker' requested here}} +// expected-error@#leak-marker-field {{'[[uninit]]' cannot be applied to a pointer under profile 'std::init'; initialize the pointer (for example to 'nullptr')}} + +// std::init / uninit_with_initializer, field flavor (paper §4.2 rule 2, +// §5.3): [[uninit]] on a member whose type's default-initialization is not a +// genuine no-op is a contradiction -- something is initialized, or nothing +// is left uninitialized. Diagnosed at the marker, with the reason at the +// member type. +namespace std { enum class byte : unsigned char {}; } + +struct RunsCtor { RunsCtor() : cap(0) {} int cap; }; +struct TrivialAgg { int a; }; + +struct MemberRunsCtor { + RunsCtor s [[uninit]]; // expected-error {{member 's' cannot be marked '[[uninit]]' under profile 'std::init'; default-initialization of its type 'RunsCtor' does not leave it uninitialized}} \ + // expected-note {{default-initialization of 'RunsCtor' runs a constructor}} +}; + +struct MemberVacuousKinds { + int x [[uninit]]; // OK: a scalar member really is left uninitialized + TrivialAgg t [[uninit]]; // OK: trivial aggregate, a genuine no-op + std::byte b [[uninit]]; // OK: std::byte may stay uninitialized (paper §4) +}; + +// An NSDMI'd marked member is the NSDMI flavor's to diagnose -- exactly one +// diagnostic, no field-flavor double (hasInClassInitializer is style-based, +// so the skip holds even while the NSDMI is late-parse-pending). +struct MemberNSDMIOnce { + RunsCtor s [[uninit]] = RunsCtor(); // expected-error {{member 's' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} +}; + +// A union- or pointer-typed marked member draws only union_marker / +// pointer_marker -- no field-flavor pile-on. Load-bearing for the union: V's +// implicitly deleted default constructor is unusable, so without the +// base-element-type skip the member would draw both diagnostics. +union V { RunsCtor s; }; +struct MemberUnionOnly { + V v [[uninit]]; // expected-error {{'[[uninit]]' cannot be applied to a data member of union type under profile 'std::init'}} +}; +struct MemberPointerOnly { + int *p [[uninit]]; // expected-error {{'[[uninit]]' cannot be applied to a pointer under profile 'std::init'; initialize the pointer (for example to 'nullptr')}} +}; + +// A member type with a deleted (or absent) default constructor can never be +// left default-initialized, so the marker is unsatisfiable. +struct NoDefault { NoDefault() = delete; int x; }; +struct MemberDeletedCtor { + NoDefault n [[uninit]]; // expected-error {{member 'n' cannot be marked '[[uninit]]' under profile 'std::init'; default-initialization of its type 'NoDefault' does not leave it uninitialized}} \ + // expected-note {{'NoDefault' has no usable default constructor}} +}; + +// An all-determinate type leaves nothing to acknowledge. +struct Empty {}; +struct MemberEmpty { + Empty e [[uninit]]; // expected-error {{member 'e' cannot be marked '[[uninit]]' under profile 'std::init'; default-initialization of its type 'Empty' does not leave it uninitialized}} \ + // expected-note {{no subobject of 'Empty' is left uninitialized}} +}; + +// A written member-initializer in some constructor does not rescue the +// marker: both branches contradict it (the member is initialized either +// way). +struct MemberCtorInit { + RunsCtor s [[uninit]]; // expected-error {{member 's' cannot be marked '[[uninit]]' under profile 'std::init'; default-initialization of its type 'RunsCtor' does not leave it uninitialized}} \ + // expected-note {{default-initialization of 'RunsCtor' runs a constructor}} + MemberCtorInit() : s{} {} +}; + +// Arrays key on the base element type, like the other marker rules. +struct MemberArrayOfCtor { + RunsCtor arr [[uninit]][2]; // expected-error {{member 'arr' cannot be marked '[[uninit]]' under profile 'std::init'; default-initialization of its type 'RunsCtor[2]' does not leave it uninitialized}} \ + // expected-note {{default-initialization of 'RunsCtor' runs a constructor}} +}; + +// A dependent marked member defers on the pattern and fires once at +// instantiation, when the substituted type's vacuity is known. +template +struct DependentVacuity { + T m [[uninit]]; // #dependent-vacuity-member +}; +template struct DependentVacuity; // OK: vacuous for a scalar +template struct DependentVacuity; // expected-note {{in instantiation of template class 'DependentVacuity' requested here}} +// expected-error@#dependent-vacuity-member {{member 'm' cannot be marked '[[uninit]]' under profile 'std::init'; default-initialization of its type 'RunsCtor' does not leave it uninitialized}} +// expected-note@#dependent-vacuity-member {{default-initialization of 'RunsCtor' runs a constructor}} + +// A marked field of an *anonymous* struct is checked when the anonymous +// record itself is finalized: the enclosing class's walk sees only the +// unnamed anonymous-record member, which cannot carry the attribute. +struct WithAnonStruct { + struct { + RunsCtor s [[uninit]]; // expected-error {{member 's' cannot be marked '[[uninit]]' under profile 'std::init'; default-initialization of its type 'RunsCtor' does not leave it uninitialized}} \ + // expected-note {{default-initialization of 'RunsCtor' runs a constructor}} + }; +}; + +// An anonymous struct inside a union is itself not a union, so its +// non-vacuous marked member draws the member diagnostic (exactly one): its +// members are variant members, where the marker is just as unsatisfiable. +union UnionWithAnonStruct { + struct { + RunsCtor s [[uninit]]; // expected-error {{member 's' cannot be marked '[[uninit]]' under profile 'std::init'; default-initialization of its type 'RunsCtor' does not leave it uninitialized}} \ + // expected-note {{default-initialization of 'RunsCtor' runs a constructor}} + }; + int tag; +}; + +// A local class is finalized like any other. +void local_class_field_marker() { + struct Local { + RunsCtor s [[uninit]]; // expected-error {{member 's' cannot be marked '[[uninit]]' under profile 'std::init'; default-initialization of its type 'RunsCtor' does not leave it uninitialized}} \ + // expected-note {{default-initialization of 'RunsCtor' runs a constructor}} + }; + (void)sizeof(Local); +} + +// Suppression: rule-targeted on the field, and whole-profile on the class. +struct SuppressedFieldMarker { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "uninit_with_initializer")]] RunsCtor s [[uninit]]; // OK: suppressed +}; +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +struct [[profiles::suppress(std::init)]] SuppressedClassFieldMarker { + RunsCtor s [[uninit]]; // OK: suppressed by the class-level attribute +}; diff --git a/clang/test/SemaCXX/safety-profile-init-local-member-read.cpp b/clang/test/SemaCXX/safety-profile-init-local-member-read.cpp new file mode 100644 index 0000000000000..5ee25161dae26 --- /dev/null +++ b/clang/test/SemaCXX/safety-profile-init-local-member-read.cpp @@ -0,0 +1,428 @@ +// All violations share one TU with a leading unrelated error: the early error +// disables the analysis-based-warnings pass for later functions, so this also +// verifies that the local-aggregate member check keeps diagnosing through the +// post-error rerun. +// RUN: %clang_cc1 -fsyntax-only -verify=expected,common -fprofiles -std=c++23 -Wno-uninitialized %s +// RUN: %clang_cc1 -fsyntax-only -verify=no-profiles,common -std=c++23 -Wno-uninitialized %s + +// std::init: an [[uninit]] scalar member of a constructor-less aggregate +// local (the paper §5.3 "class exposing uninitialized members" pattern) is +// given a value by a plain member store; a read before the member is +// definitely assigned on every path is diagnosed by a per-function +// definite-assignment pass, the local-variable analog of the ctor-body check. + +// no-profiles-warning@+1 {{'profiles::enforce' attribute ignored}} +[[profiles::enforce(std::init)]]; + +namespace std { enum class byte : unsigned char {}; } + +int leading_unrelated_error = undeclared_identifier; +// common-error@-1 {{use of undeclared identifier 'undeclared_identifier'}} + +struct Agg { + int m [[uninit]]; // expected-note 18 {{member 'm' declared here}} +}; +void take_ref(Agg &); +// The pointee is uninitialized memory, so the parameter carries the marker +// (the unmarked spelling is the ref_to_uninit binding rule's to reject). +void take_ptr(int *p [[ref_to_uninit]]); + +int test_read_before_any_write() { + Agg a; + return a.m; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +} + +int test_read_then_write() { + Agg a; + int v = a.m; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} + a.m = 1; + return v; +} + +int test_branch_one_path(bool c) { + Agg a; + if (c) + a.m = 1; + return a.m; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +} + +// A compound assignment and a built-in ++/-- read the old value before +// writing it. +void test_compound_reads_old_value() { + Agg a; + a.m += 1; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +} + +void test_incdec_reads_old_value() { + Agg a; + a.m++; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +} + +// A loop body may run zero times, so an assignment inside it does not reach a +// read after the loop... +int test_loop_may_not_run(int n) { + Agg a; + for (int i = 0; i < n; ++i) + a.m = i; + return a.m; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +} + +// ...and a read at the top of the body precedes the first iteration's write. +int test_loop_read_first_iteration(int n) { + Agg a; + int t = 0; + for (int i = 0; i < n; ++i) { + t += a.m; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} + a.m = i; + } + return t; +} + +// sizeof neither reads the member (unevaluated) nor escapes the object. +int test_sizeof_neither_reads_nor_escapes() { + Agg a; + (void)sizeof(a.m); + return a.m; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +} + +// An [[uninit]] member inherited from a constructor-less non-virtual base is +// tracked like the class's own (nothing can have assigned it earlier). +struct Base { + int bm [[uninit]]; // expected-note {{member 'bm' declared here}} +}; +struct Derived : Base { + int dm = 0; +}; +int test_base_subtree_member() { + Derived d; + return d.bm; // expected-error {{member 'bm' is read before initialization under profile 'std::init'}} +} + +int test_write_then_read() { + Agg a; + a.m = 5; + return a.m; // OK +} + +int test_branch_both_paths(bool c) { + Agg a; + if (c) + a.m = 1; + else + a.m = 2; + return a.m; // OK +} + +// Any appearance of the variable outside a recognized member read or write +// conservatively marks every member assigned: the address may be used to +// initialize the object (construct_at, memcpy, an initializing callee). +// These pin the interim (pre-now_init()) leniency, paper §6.2; contrast the +// ctor-body pass's strict assignment-only crediting +// (safety-profile-init-ctor-body.cpp, Escape*). +int test_escape_address_of_object() { + Agg a; + (void)&a; + return a.m; // OK: escaped +} + +int test_escape_address_of_member() { + Agg a; + take_ptr(&a.m); + return a.m; // OK: escaped +} + +int test_escape_reference_binding() { + Agg a; + take_ref(a); + return a.m; // OK: escaped +} + +int test_escape_lambda_capture() { + Agg a; + auto init = [&] { a.m = 5; }; + init(); + return a.m; // OK: the capture escapes the object +} + +// Placement new takes the object's address -- the same escape as &a. +void *operator new(__SIZE_TYPE__, void *p) noexcept; +int test_escape_placement_new() { + Agg a; + new (&a) Agg{1}; + return a.m; // OK: escaped +} + +// A class with a user-provided constructor is trusted (paper §5.1): its +// constructor body may have assigned the member, which local analysis cannot +// see. This pins the deliberate trust decision for non-current-object member +// reads. +struct Slot { + int y [[uninit]]; + Slot() {} +}; +int test_user_provided_ctor_trusted() { + Slot uu; + return uu.y; // OK: trusted +} + +// A base with a user-provided constructor keeps its members untracked, even +// under a constructor-less derived class. +struct TrustedBase { + int tm [[uninit]]; + TrustedBase() {} +}; +struct DerivedFromTrusted : TrustedBase {}; +int test_trusted_base_member() { + DerivedFromTrusted d; + return d.tm; // OK: trusted +} + +// A value-initializing written form gives every member a value; only the +// bare `Agg a;` form (the implicit no-op default-construction) is tracked. +int test_value_initialized_forms() { + Agg a{}; + Agg b = {}; + Agg c = Agg(); + return a.m + b.m + c.m; // OK +} + +// A copy does NOT give the [[uninit]] member a value -- it copies +// indeterminate bits (a copy does not inherit initialization, paper §5.2) +// -- but the source's per-member state is unknowable for an untracked +// source, so such copies stay untracked: a known gap, never a false +// positive. +Agg make_agg(); +int test_copy_from_untracked_source() { + Agg e = make_agg(); + return e.m; // OK: known gap (untracked source) +} + +// A by-value parameter is a copy of the caller's argument, and a copy does +// not inherit initialization (§5.2): its marked members are tracked from an +// unassigned start. This is the call-boundary twin of the ctor-body pass's +// deliberate strictness -- the paper hands uninitialized-capable storage +// across calls via marked pointers/references (§4.3), not by-value slots -- +// so a caller-initialized member is rejected all the same; escapes and +// [[profiles::suppress]] are the remedies. +int test_byvalue_parameter_tracked(Agg p) { + return p.m; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +} + +int test_byvalue_parameter_assigned(Agg p) { + p.m = 5; + return p.m; // OK +} + +int test_byvalue_parameter_escape(Agg p) { + take_ref(p); + return p.m; // OK: escaped +} + +// Re-passing the parameter by value is an escape like any other bare use +// (the copy-constructor argument reference is not a tracked-copy DeclStmt). +void use_agg(Agg); +int test_byvalue_parameter_repassed(Agg p) { + use_agg(p); + return p.m; // OK: escaped +} + +// A copy from a by-value parameter chains the tracking: the parameter +// starts unassigned, so the copy does too. +int test_copy_from_parameter(Agg other) { + Agg d = other; + return d.m; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +} + +// A reference parameter aliases the caller's own object -- not a copy -- +// and stays untracked. +int test_reference_parameter_untracked(Agg &r) { + return r.m; // OK +} + +// A local that is itself [[uninit]]-marked is the parse-time rules' +// territory: the read-through check owns its subobject reads, and exactly one +// diagnostic fires. +int test_marked_local_owned_by_read_through() { + Agg s [[uninit]]; + return s.m; // expected-error {{read of a subobject of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} +} + +// A static local is zero-initialized, never tracked. +int test_static_local_untracked() { + static Agg a; + return a.m; // OK +} + +// A member of an anonymous struct is reached through an IndirectFieldDecl +// chain, not a direct `a.m` access, so it is not tracked -- consistent with +// the anonymous-aggregate skips in the ctor-body pass and R5 (a known gap). +struct HasAnon { + struct { + int m [[uninit]]; + }; +}; +int test_anonymous_member_untracked() { + HasAnon a; + return a.m; // OK: known gap +} + +// An array of aggregates is not tracked (element tracking is the deferred +// construct_at slice). +int test_array_of_aggregates_untracked() { + Agg arr[2]; + return arr[0].m; // OK: known gap +} + +// A union local is never tracked: the harvest rejects union types (their +// members are mutually exclusive, and [[uninit]] on a union member is banned +// by union_marker anyway -- suppressed on the function to build the fixture). +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(std::init, rule: "union_marker")]] +int test_union_local_untracked() { + union U { + int x [[uninit]]; + }; + U u = {1}; + return u.x; // OK +} + +// std::byte members are exempt (paper §4.5), so a byte-only aggregate has +// nothing to track. +struct ByteBox { + std::byte b [[uninit]]; +}; +std::byte test_byte_member_exempt() { + ByteBox x; + return x.b; // OK +} + +void test_suppress_stmt() { + Agg a; + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "uninit_read")]] { + int v = a.m; // OK: suppressed + (void)v; + } +} + +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(std::init)]] +int test_suppress_decl() { + Agg a; + return a.m; // OK: suppressed +} + +// A lambda body's own locals are tracked when the lambda's call operator is +// analyzed. +void test_lambda_own_local() { + auto f = [] { + Agg a; + return a.m; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} + }; + (void)f; +} + +// A template function's body is analyzed per instantiation. +template +int template_local_member_read() { + Agg a; + return a.m; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +} +template int template_local_member_read(); // expected-note {{in instantiation of function template specialization 'template_local_member_read' requested here}} + +// ============================================================ +// Copies of tracked locals +// ============================================================ + +// A copy of a tracked local inherits the source's per-member state at the +// copy point -- a copy does not inherit initialization (paper §5.2), it +// inherits whatever state the source has -- so reading the copy's member is +// exactly as (in)valid as reading the source's was there. +int test_copy_read_before_source_assigned() { + Agg a; + Agg b = a; + return b.m; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +} + +int test_copy_after_source_assigned() { + Agg a; + a.m = 5; + Agg b = a; + return b.m; // OK: the source was assigned at the copy point +} + +int test_copy_then_dest_assigned() { + Agg a; + Agg b = a; + b.m = 1; + return b.m; // OK +} + +// The copy consumes the source ref without escaping it: the source keeps +// its own (unassigned) state. +int test_copy_keeps_source_tracked() { + Agg a; + Agg b = a; + b.m = 1; + return a.m; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +} + +// State transfers at the copy point, not later: a source assignment after +// the copy does not reach the copy. +int test_copy_point_state() { + Agg a; + Agg b = a; + a.m = 5; + return b.m; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +} + +// The all-branches rule (§1.2) applies through the copy. +int test_copy_after_branch(bool c) { + Agg a; + if (c) + a.m = 5; + Agg b = a; + return b.m; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +} + +// State flows through a chain of copies (harvested to a fixpoint, so the +// chain resolves regardless of declaration order in the CFG's block list). +int test_copy_of_copy() { + Agg a; + a.m = 5; + Agg b = a; + Agg c = b; + return c.m; // OK +} + +int test_copy_of_copy_unassigned() { + Agg a; + Agg b = a; + Agg c = b; + return c.m; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +} + +// Move construction transfers state the same way (for these classes a move +// is a copy; the explicit-cast peel resolves the directly named source). +int test_move_construction() { + Agg a; + Agg b = static_cast(a); + return b.m; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +} + +// Paren and brace copy forms behave identically to the `=` form. +int test_copy_forms() { + Agg a; + a.m = 5; + Agg b(a); + Agg c{a}; + return b.m + c.m; // OK +} + +// An escape of the copy credits the copy, like any tracked local. +int test_copy_escape() { + Agg a; + Agg b = a; + take_ref(b); + return b.m; // OK: escaped +} diff --git a/clang/test/SemaCXX/safety-profile-init-read.cpp b/clang/test/SemaCXX/safety-profile-init-read.cpp new file mode 100644 index 0000000000000..74808c588e76f --- /dev/null +++ b/clang/test/SemaCXX/safety-profile-init-read.cpp @@ -0,0 +1,202 @@ +// All violations share one TU with a leading unrelated error: the early error +// disables the analysis-based-warnings pass for later functions, so this also +// verifies that an enforced CFG-uninit profile keeps diagnosing afterwards. +// The DEMOTE run additionally enforces test::uninit_read to exercise profile +// table ordering, which would otherwise change the std::init-only diagnostics. +// RUN: %clang_cc1 -fsyntax-only -verify=expected,common -fprofiles -std=c++23 -Wno-uninitialized %s +// RUN: %clang_cc1 -fsyntax-only -verify=demote,common -fprofiles -fprofiles-test-profiles -std=c++23 -Wno-uninitialized -DDEMOTE %s +// RUN: %clang_cc1 -fsyntax-only -verify=no-profiles,common -std=c++23 -Wno-uninitialized %s + +// no-profiles-warning@+1 {{'profiles::enforce' attribute ignored}} +[[profiles::enforce(std::init)]]; +// no-profiles-warning@+1 {{'profiles::enforce' attribute ignored}} +[[profiles::enforce(test::other)]]; +#ifdef DEMOTE +[[profiles::enforce(test::uninit_read)]]; +#endif + +namespace std { enum class byte : unsigned char {}; } + +int leading_unrelated_error = undeclared_identifier; +// common-error@-1 {{use of undeclared identifier 'undeclared_identifier'}} + +// The always-compiled suppress tests suppress both std::init and +// test::uninit_read so the function-under-test demonstrates std::init behavior +// in isolation under either run. +// no-profiles-warning@+2 {{'profiles::suppress' attribute ignored}} +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(std::init)]] [[profiles::suppress(test::uninit_read)]] +void test_suppress_decl() { + int x [[uninit]]; + int y = x; + (void)y; +} + +void test_suppress_stmt_inner() { + int x [[uninit]]; + // no-profiles-warning@+2 {{'profiles::suppress' attribute ignored}} + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] [[profiles::suppress(test::uninit_read)]] { + int y = x; + (void)y; + } +} + +void test_suppress_var_init() { + int x [[uninit]]; + // no-profiles-warning@+2 {{'profiles::suppress' attribute ignored}} + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] [[profiles::suppress(test::uninit_read)]] + int y = x; + (void)y; +} + +void test_suppress_rule_targeted() { + int x [[uninit]]; + // no-profiles-warning@+2 {{'profiles::suppress' attribute ignored}} + // no-profiles-warning@+2 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "uninit_read")]] + [[profiles::suppress(test::uninit_read)]] { + int y = x; + (void)y; + } +} + +void test_marker_then_write_then_read() { + int x [[uninit]]; + x = 7; + int y = x; + (void)y; +} + +void test_param(int p) { + int y = p; + (void)y; +} + +#ifndef DEMOTE +void test_marker_does_not_excuse_read() { + int x [[uninit]]; // expected-note {{variable 'x' is declared here}} + int y = x; // expected-error {{variable 'x' is read before initialization under profile 'std::init'}} + (void)y; +} + +void test_suppress_stmt_outer() { + int x [[uninit]]; // expected-note {{variable 'x' is declared here}} + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] { + int y = x; + (void)y; + } + int z = x; // expected-error {{variable 'x' is read before initialization under profile 'std::init'}} + (void)z; +} + +template +T template_uninit() { + T x [[uninit]]; // expected-note {{variable 'x' is declared here}} + return x; // expected-error {{variable 'x' is read before initialization under profile 'std::init'}} +} +void instantiate_template_uninit() { + template_uninit(); // expected-note {{in instantiation of function template specialization 'template_uninit' requested here}} +} + +void test_selective_suppress() { + int x [[uninit]]; // expected-note {{variable 'x' is declared here}} + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::other)]] { + int y = x; // expected-error {{variable 'x' is read before initialization under profile 'std::init'}} + (void)y; + } +} + +void test_decl_suppress_does_not_extend() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] int x [[uninit]]; // expected-note {{variable 'x' is declared here}} + int y = x; // expected-error {{variable 'x' is read before initialization under profile 'std::init'}} + (void)y; +} + +// std::byte may be read while uninitialized (paper section 4), so std::init +// does not diagnose a read of an uninitialized std::byte. +void test_byte_read_exempt() { + std::byte b [[uninit]]; + std::byte c = b; + (void)c; +} + +// A self-init reads the uninitialized variable in its own initializer and is +// reported at the root cause, even with no later use. +void test_self_init() { + int x = x; // expected-error {{variable 'x' is read before initialization under profile 'std::init'}} \ + // expected-note {{variable 'x' is declared here}} + (void)&x; +} + +void test_self_init_suppressed() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] int x = x; // OK: suppressed + (void)&x; +} + +// A std::byte self-init stays exempt (paper section 4). +void test_self_init_byte_exempt() { + std::byte b = b; + (void)&b; +} + +void take_const_ref(const int &); +void take_const_ptr(const int *); + +// A const-reference or const-pointer use of an uninitialized variable is a +// binding (ref_to_uninit territory, diagnosed at the binding site), not a +// read: uninit_read must not fire on it. +void test_const_ref_use_is_not_a_read() { + int x [[uninit]]; + take_const_ref(x); // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} + +void test_const_ptr_use_is_not_a_read() { + int x [[uninit]]; + take_const_ptr(&x); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} + +// A read of a subobject of an [[uninit]] local -- an aggregate's member or an +// array's element -- is the read-through check's (this CFG pass does not track +// member accesses of record locals or arrays at all, and subobject-wise +// delayed initialization is banned, paper sections 5.4/5.5). Full coverage +// lives in safety-profile-init-ref-to-uninit.cpp. +struct Agg { int m; }; +void test_member_read_of_uninit_aggregate() { + Agg s [[uninit]]; + int y = s.m; // expected-error {{read of a subobject of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} + (void)y; +} + +void test_element_read_of_uninit_array() { + [[uninit]] int a[2]; + int y = a[0]; // expected-error {{read of a subobject of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} + (void)y; +} +#endif + +#ifdef DEMOTE +// With both test::uninit_read and std::init enforced, table order makes +// test::uninit_read fire first; suppressing it at the use site lets the +// std::init diagnostic surface. +void test_demote_test_profile() { + int x [[uninit]]; // demote-note {{variable 'x' is declared here}} + [[profiles::suppress(test::uninit_read)]] { + int y = x; // demote-error {{variable 'x' is read before initialization under profile 'std::init'}} + (void)y; + } +} + +// The std::byte exemption is std::init-only: test::uninit_read still diagnoses +// a read of an uninitialized std::byte. +void test_byte_not_exempt_under_test_profile() { + std::byte b [[uninit]]; // demote-note {{variable 'b' is declared here}} + std::byte c = b; // demote-error {{variable 'b' is read before initialization under profile 'test::uninit_read'}} + (void)c; +} +#endif diff --git a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp new file mode 100644 index 0000000000000..bc25431e2732c --- /dev/null +++ b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp @@ -0,0 +1,2671 @@ +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -fblocks -fcxx-exceptions -std=c++23 %s +// RUN: %clang_cc1 -fsyntax-only -verify=no-profiles -fblocks -fcxx-exceptions -std=c++23 %s + +// std::init / ref_to_uninit (paper §5): a [[ref_to_uninit]] pointer or +// reference must be bound to uninitialized memory, and an unmarked pointer or +// reference must not. This file exercises the rule at variable initialization. + +// no-profiles-warning@+1 {{'profiles::enforce' attribute ignored}} +[[profiles::enforce(std::init)]]; + +int g_init = 0; +int g_init2 = 0; +// Static fixtures supplying uninitialized memory for the pointer/reference +// tests below. A static [[uninit]] is rejected by static_marker (paper section +// 4.2), so suppress that rule here: the test deliberately creates uninitialized +// static storage, and suppression keeps the marker (the source stays +// "uninitialized memory" for the ref_to_uninit checks). +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(std::init, rule: "static_marker")]] [[uninit]] int g_uninit; +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(std::init, rule: "static_marker")]] [[uninit]] int g_uninit_arr[3]; +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(std::init, rule: "static_marker")]] [[uninit]] int g_uninit2; +[[ref_to_uninit]] int *allocate(int n); +[[ref_to_uninit]] void *alloc_void(); +[[ref_to_uninit]] int &get_uninit_ref(); +void h(); + +void test_pointer_target() { + int *p1 [[ref_to_uninit]] = &g_uninit; // OK + int *p2 [[ref_to_uninit]] = &g_init; // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + int *p3 = &g_uninit; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *p4 = &g_init; // OK + int *p5 = nullptr; // OK + (void)p1; (void)p2; (void)p3; (void)p4; (void)p5; +} + +void test_pointer_sources() { + int *base [[ref_to_uninit]] = &g_uninit; + int *from_ptr [[ref_to_uninit]] = base; // OK: base is [[ref_to_uninit]] + int *from_array [[ref_to_uninit]] = g_uninit_arr; // OK: array-to-pointer decay + int *from_call [[ref_to_uninit]] = allocate(3); // OK: [[ref_to_uninit]] return + int *bad_from_array = g_uninit_arr; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *bad_from_call = allocate(3); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)base; (void)from_ptr; (void)from_array; (void)from_call; + (void)bad_from_array; (void)bad_from_call; +} + +// An explicit pointer-to-pointer cast of a [[ref_to_uninit]] pointer is itself +// [[ref_to_uninit]] (paper §4.3); the cast does not launder the marking. +void test_pointer_casts() { + void *vp [[ref_to_uninit]] = &g_uninit; + int *c1 [[ref_to_uninit]] = (int *)vp; // OK + int *c2 = (int *)vp; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + + int *sc1 [[ref_to_uninit]] = static_cast(vp); // OK + int *sc2 = static_cast(vp); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *rc1 [[ref_to_uninit]] = reinterpret_cast(vp); // OK + int *rc2 = reinterpret_cast(vp); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + + // An initialized pointer round-tripped through a cast stays initialized. + void *vi = &g_init; + int *ci1 = (int *)vi; // OK + int *ci2 [[ref_to_uninit]] = (int *)vi; // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + + // Casting a [[ref_to_uninit]]-returning call result propagates the marking. + int *cc1 [[ref_to_uninit]] = (int *)alloc_void(); // OK + int *cc2 = (int *)alloc_void(); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + + // Trust model: a pointer manufactured from an integer is not propagated. + int *ti = reinterpret_cast(0xdeadbeef); // OK + + // Deref of a cast routes back through the pointer recognizer. + int &dr [[ref_to_uninit]] = *(int *)vp; // OK + + (void)c1; (void)c2; (void)sc1; (void)sc2; (void)rc1; (void)rc2; + (void)vi; (void)ci1; (void)ci2; (void)cc1; (void)cc2; (void)ti; (void)dr; +} + +void test_reference_target() { + int &r1 [[ref_to_uninit]] = g_uninit; // OK + int &r2 [[ref_to_uninit]] = g_init; // expected-error {{reference marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + int &r3 = g_uninit; // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int &r4 = g_init; // OK + int *p [[ref_to_uninit]] = &g_uninit; + int &r5 [[ref_to_uninit]] = *p; // OK: *p denotes uninitialized storage + // A [[ref_to_uninit]] reference is itself a source of uninitialized storage, + // symmetric to the [[ref_to_uninit]] pointer-copy case. + int &r6 [[ref_to_uninit]] = r1; // OK: r1 refers to uninitialized memory + int &r7 = r1; // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)r1; (void)r2; (void)r3; (void)r4; (void)r5; (void)r6; (void)r7; (void)p; +} + +// A reference cast (an explicit cast yielding a glvalue) denotes the same +// storage as its operand, and a [[ref_to_uninit]]-returning reference call +// denotes uninitialized storage. Symmetric to the pointer side. +void test_reference_casts() { + int &cr1 [[ref_to_uninit]] = static_cast(g_uninit); // OK + int &cr2 = static_cast(g_uninit); // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int &cr3 [[ref_to_uninit]] = (int &)g_init; // expected-error {{reference marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + + int &gr1 [[ref_to_uninit]] = get_uninit_ref(); // OK + int &gr2 = get_uninit_ref(); // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + + // Address of a reference cast routes back through the pointer recognizer. + int *p [[ref_to_uninit]] = &(int &)g_uninit; // OK + + (void)cr1; (void)cr2; (void)cr3; (void)gr1; (void)gr2; (void)p; +} + +// Pass-through sources are transparent to their operand: a single-element +// braced initializer is looked through to its element, a conditional is +// uninitialized if either arm is, and a comma yields its right operand. +void test_braced_pointer() { + int *b1 [[ref_to_uninit]] = {&g_uninit}; // OK + int *b2 = {&g_uninit}; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *b3 [[ref_to_uninit]] = {&g_init}; // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + int *b4 = {&g_init}; // OK + // Empty {} value-initializes to nullptr, like = nullptr: a null source is + // consistent with marked and unmarked targets alike (paper §8, §4.3; see + // test_null_sources). + int *b5 [[ref_to_uninit]] = {}; // OK + int *b6 = {}; // OK + (void)b1; (void)b2; (void)b3; (void)b4; (void)b5; (void)b6; +} + +void test_braced_reference() { + int &r1 [[ref_to_uninit]] = {g_uninit}; // OK + int &r2 = {g_uninit}; // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int &r3 [[ref_to_uninit]] = {g_init}; // expected-error {{reference marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + int &r4 = {g_init}; // OK + (void)r1; (void)r2; (void)r3; (void)r4; +} + +void test_conditional_pointer(bool c) { + int *p1 = c ? &g_uninit : &g_init; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *p2 [[ref_to_uninit]] = c ? &g_uninit : &g_uninit2; // OK: both arms uninitialized + int *p3 [[ref_to_uninit]] = c ? &g_uninit : &g_init; // OK: either arm may be uninitialized + int *p4 [[ref_to_uninit]] = c ? &g_init : &g_init2; // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + int *p5 = c ? &g_init : &g_init2; // OK + (void)p1; (void)p2; (void)p3; (void)p4; (void)p5; +} + +void test_conditional_reference(bool c) { + int &r1 [[ref_to_uninit]] = c ? g_uninit : g_uninit2; // OK + int &r2 = c ? g_uninit : g_init; // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int &r3 [[ref_to_uninit]] = c ? g_init : g_init2; // expected-error {{reference marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + int &r4 = c ? g_init : g_init2; // OK + (void)r1; (void)r2; (void)r3; (void)r4; +} + +void test_comma_pointer() { + int *p1 = (h(), &g_uninit); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *p2 [[ref_to_uninit]] = (h(), &g_uninit); // OK + int *p3 [[ref_to_uninit]] = (h(), &g_init); // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + int *p4 = (h(), &g_init); // OK + (void)p1; (void)p2; (void)p3; (void)p4; +} + +void test_comma_reference() { + int &r1 [[ref_to_uninit]] = (h(), g_uninit); // OK + int &r2 = (h(), g_uninit); // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int &r3 [[ref_to_uninit]] = (h(), g_init); // expected-error {{reference marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + int &r4 = (h(), g_init); // OK + (void)r1; (void)r2; (void)r3; (void)r4; +} + +void test_assignment() { + int *p [[ref_to_uninit]] = &g_uninit; + p = &g_uninit; // OK + p = &g_init; // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + int *q = &g_init; + q = &g_uninit; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + q = &g_init; // OK + q = nullptr; // OK + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] { q = &g_uninit; } // OK: suppressed + (void)p; (void)q; +} + +// Assignment through indirection: the assigned-to pointer is reached by +// dereference or subscript, so it cannot carry a local [[ref_to_uninit]] +// marker. It is the default unmarked pointer and must not be bound to +// uninitialized memory, exactly as a directly-named pointer would be. +void test_assignment_indirect() { + int *p = nullptr; + int **pp = &p; + *pp = &g_uninit; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + *pp = &g_init; // OK + (*pp) = &g_uninit; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + *pp = new int; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + *pp = new int(0); // OK + + int *arr[3] = {}; + arr[0] = &g_uninit; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + arr[1] = &g_init; // OK + (void)pp; (void)arr; +} + +void test_suppress() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "ref_to_uninit")]] int *s = &g_uninit; // OK: suppressed + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] int *s2 [[ref_to_uninit]] = &g_init; // OK: suppressed + (void)s; (void)s2; +} + +void take_uninit_ptr(int *p [[ref_to_uninit]]); +void take_uninit_ref(int &r [[ref_to_uninit]]); +void take_ptr(int *p); +void take_ref(const int &r); +void uninitialized_fill(int *r [[ref_to_uninit]], int val); + +void test_call_arguments() { + take_uninit_ptr(&g_uninit); // OK + take_uninit_ptr(&g_init); // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + take_uninit_ptr(g_uninit_arr); // OK: array-to-pointer decay + take_uninit_ref(g_uninit); // OK + take_uninit_ref(g_init); // expected-error {{reference marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + + take_ptr(&g_uninit); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + take_ptr(&g_init); // OK + take_ref(g_uninit); // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + take_ref(g_init); // OK + + // A [[ref_to_uninit]] reference argument matches a [[ref_to_uninit]] reference + // parameter, and is rejected for an unmarked one. + int &ru [[ref_to_uninit]] = g_uninit; + take_uninit_ref(ru); // OK + take_ref(ru); // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)ru; + + // The worked example from paper §5. + int a1[] = {1, 2, 3}; + [[uninit]] int a2[3]; + uninitialized_fill(a1, 10); // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + uninitialized_fill(a2, 10); // OK + + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] { take_ptr(&g_uninit); } // OK: suppressed +} + +// A null pointer refers to no object, so it is consistent with a marked +// target -- the marker means "zero or more uninitialized objects" (paper §8) +// -- and with an unmarked one (the paper §4.3 f1(p2) example): it classifies +// as unknown storage. This covers the literal forms and a named local whose +// declaration initializer is null. +void test_null_sources() { + take_uninit_ptr(nullptr); // OK: null literal for a marked param + take_uninit_ptr(0); // OK: literal zero + take_ptr(nullptr); // OK + + int *p2 = nullptr; + take_uninit_ptr(p2); // OK: the paper §4.3 example + take_ptr(p2); // OK + + int *q [[ref_to_uninit]] = p2; // OK: null is not affirmatively initialized + int *q2 = p2; // OK + (void)q; (void)q2; + + int *z = {}; // value-initializes to null, like = nullptr + take_uninit_ptr(z); // OK + int *zb [[ref_to_uninit]] = {nullptr}; // OK: braced null recurses to the literal + (void)zb; + + // The null-init classification is parse-order lenient: a reassignment after + // the null declaration is not tracked, so passing the now-initialized + // pointer to a marked parameter is an accepted missed diagnostic. + int *r = nullptr; + r = &g_init; + take_uninit_ptr(r); // accepted: missed diagnostic (parse-order leniency) +} + +// A zero-initialized *global* null pointer stays classified initialized: +// an extern pointer may be initialized elsewhere (another translation unit), +// and keeping globals initialized preserves the marked-direction +// diagnostics. Deliberate residual strictness. +int *g_null_ptr; +void test_null_global() { + take_uninit_ptr(g_null_ptr); // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} +} + +// A static local is excluded for the same reason as a global (not +// function-local state; hasLocalStorage is the gate). +void test_null_static_local() { + static int *sp = nullptr; + take_uninit_ptr(sp); // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} +} + +// A *parameter* is excluded too: a ParmVarDecl's initializer is its default +// argument, which is not the parameter's value on most calls -- a +// defaulted-null parameter may be passed any caller pointer, so it must keep +// drawing the marked-target diagnostic. +void null_default_param(int *cp = nullptr) { + take_uninit_ptr(cp); // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} +} + +// At the *call site* an omitted defaulted argument is the null literal +// itself: fine for a marked parameter. +void null_default_marked(int *p [[ref_to_uninit]] = nullptr); +void test_null_default_marked_param() { + null_default_marked(); // OK: the null default argument + null_default_marked(nullptr); // OK +} + +// A *marked* pointer initialized to null keeps its marker classification as +// a source: the explicit marker is respected over the null initializer. +void test_null_init_marked_decl() { + int *m [[ref_to_uninit]] = nullptr; + int *m2 = m; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)m2; +} + +// A defaulted pointer or reference argument is checked against the parameter's +// [[ref_to_uninit]] marking at the call site, like an explicit argument. The +// declarations themselves stay clean; the diagnostic fires at the call. +void def_uninit_ptr(int *p [[ref_to_uninit]] = &g_uninit); +void def_uninit_ptr_bad(int *p [[ref_to_uninit]] = &g_init); +void def_ptr(int *p = &g_init); +void def_ptr_bad(int *p = &g_uninit); +void def_uninit_ref(int &r [[ref_to_uninit]] = g_uninit); +void def_uninit_ref_bad(int &r [[ref_to_uninit]] = g_init); +void def_ref(int &r = g_init); +void def_ref_bad(int &r = g_uninit); + +void test_default_arguments() { + def_uninit_ptr(); // OK + def_uninit_ptr_bad(); // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + def_ptr(); // OK + def_ptr_bad(); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + def_uninit_ref(); // OK + def_uninit_ref_bad(); // expected-error {{reference marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + def_ref(); // OK + def_ref_bad(); // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + + // An explicit argument overrides the default and is checked on its own merits. + def_ptr_bad(&g_init); // OK + + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] { def_ptr_bad(); } // OK: suppressed +} + +// Call arguments are checked at parameter copy-initialization, which also +// covers call forms that never reach GatherArgumentsForCall: calls to objects +// of class type (functors, lambdas) and overloaded operators (member and +// non-member). +struct MarkedFunctor { + void operator()(int *p [[ref_to_uninit]]); +}; +struct UnmarkedFunctor { + void operator()(int *p); +}; + +void test_functor_arguments() { + MarkedFunctor mf; + mf(&g_uninit); // OK + mf(&g_init); // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + UnmarkedFunctor uf; + uf(&g_init); // OK + uf(&g_uninit); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] { uf(&g_uninit); } // OK: suppressed +} + +void test_lambda_arguments() { + auto l = [](int *p [[ref_to_uninit]]) {}; + l(&g_uninit); // OK + l(&g_init); // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} +} + +// A call with no declared callee (through a function pointer) has no +// parameter declaration that could carry [[ref_to_uninit]], so its arguments +// are checked as unmarked targets (paper §7.2: passing uninitialized memory +// needs an appropriately declared callee). That holds even when the pointer +// happens to point at a function whose parameter is marked -- the marker is a +// declaration property, invisible through the pointer (paper §1.3, local +// analysis); suppress at the call if the flow is intended. +void test_fnptr_call_arguments(void (*fp)(int *), void (*fr)(int &)) { + fp(&g_init); // OK + fp(&g_uninit); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *rtu [[ref_to_uninit]] = &g_uninit; + fp(rtu); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + fr(g_init); // OK + fr(g_uninit); // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + + void (*marked)(int *) = take_uninit_ptr; + marked(&g_uninit); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] { fp(&g_uninit); } // OK: suppressed + (void)rtu; +} + +// Decl-less like the declared-callee argument site; the dependent callee type +// keeps the call unchecked on the pattern, so it fires once, at instantiation. +template +void template_fnptr_call_arg(void (*fp)(T *)) { + fp(&g_uninit); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} +template void template_fnptr_call_arg(void (*)(int *)); // expected-note {{in instantiation of function template specialization 'template_fnptr_call_arg' requested here}} + +// A variadic (...) argument never reaches parameter copy-initialization, and +// a ... parameter cannot carry [[ref_to_uninit]], so a pointer passed through +// it is checked as an unmarked target (paper §7.2). A *value* passed through +// ... is promoted with an ordinary lvalue-to-rvalue load, so its read is the +// read-through chokepoint's, not this site's. +void vf(int, ...); + +void test_variadic_arguments() { + vf(0, &g_init); // OK + vf(0, &g_uninit); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *rtu [[ref_to_uninit]] = &g_uninit; + vf(0, rtu); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + vf(0, g_uninit_arr); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + vf(0, *rtu); // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + + // Through a variadic function pointer: the named arguments are the + // no-declared-callee site's, the ... arguments this one's -- exactly one + // diagnostic either way. + void (*vfp)(int, ...) = vf; + vfp(0, &g_uninit); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] { vf(0, &g_uninit); } // OK: suppressed + (void)rtu; +} + +// Decl-less with a non-dependent argument: fires at definition time, and +// again when the call (always rebuilt) re-promotes the argument at +// instantiation -- the accepted repetition. +template +void template_variadic_arg() { + vf(0, &g_uninit); // expected-error 2 {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} +template void template_variadic_arg(); // expected-note {{in instantiation of function template specialization 'template_variadic_arg' requested here}} + +// A call to an object of class type promotes its variadic arguments in its +// own loop (Sema::BuildCallToObjectOfClassType), distinct from +// GatherArgumentsForCall's; both are hooked, so variadic functors and +// variadic lambdas are covered too. +struct VariadicFunctor { + void operator()(int, ...); +}; + +void test_variadic_functor_arguments() { + VariadicFunctor f; + f(0, &g_init); // OK + f(0, &g_uninit); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + auto l = [](int, ...) {}; + l(0, &g_init); // OK + l(0, &g_uninit); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} + +// A surrogate call converts to a function pointer and calls through it, so +// its named arguments are the no-declared-callee site's. +struct Surrogate { + using FP = void (*)(int *); + operator FP(); +}; + +void test_surrogate_call_arguments() { + Surrogate s; + s(&g_init); // OK + s(&g_uninit); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} + +// An init-capture is a binding: a capture cannot carry [[ref_to_uninit]], so +// capturing a pointer or reference to uninitialized memory is always the +// unmarked-direction violation. +void test_init_captures() { + auto c1 = [p = &g_init] { (void)p; }; // OK + auto c2 = [p = &g_uninit] { (void)p; }; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *rtu [[ref_to_uninit]] = &g_uninit; + auto c3 = [&r = *rtu] { (void)r; }; // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + auto c4 = [q = rtu] { (void)q; }; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + auto c5 = [v = g_init] { (void)v; }; // OK: a by-value int copy is not a binding + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] { + auto c6 = [p = &g_uninit] { (void)p; }; // OK: suppressed + (void)c6; + } + (void)c1; (void)c2; (void)c3; (void)c4; (void)c5; +} + +// An init-capture inside a template body defers on the pattern and fires +// once, at instantiation. +template +void template_init_capture_bad() { + auto c = [p = &g_uninit] { (void)p; }; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)c; +} +template void template_init_capture_bad(); // expected-note {{in instantiation of function template specialization 'template_init_capture_bad' requested here}} + +// A by-reference capture -- explicit or via a capture-default -- is the same +// binding as an init-capture: a capture cannot carry [[ref_to_uninit]], so +// capturing an [[uninit]] variable (or a [[ref_to_uninit]] reference) by +// reference is always the unmarked-direction violation. A copy capture is not +// a binding; it reads the variable in the enclosing function's CFG, which is +// the flow-based uninit_read pass's territory. +void test_ref_captures() { + int x [[uninit]]; + int ok = 0; + // The bodies must not assign x: a body store would credit it in parse + // order and silence the capture check (see test_ref_capture_body_store). + auto c1 = [&x] { (void)x; }; // expected-error {{capturing 'x' by reference binds a reference to uninitialized memory under profile 'std::init'}} + auto c2 = [&] { (void)x; }; // expected-error {{capturing 'x' by reference binds a reference to uninitialized memory under profile 'std::init'}} + auto c3 = [&ok] { ok = 1; }; // OK: initialized + int *rtu [[ref_to_uninit]] = &g_uninit; + auto c4 = [&rtu] { (void)rtu; }; // OK: the pointer object itself is initialized + int &ur [[ref_to_uninit]] = *rtu; + auto c5 = [&ur] { (void)ur; }; // expected-error {{capturing 'ur' by reference binds a reference to uninitialized memory under profile 'std::init'}} + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "ref_to_uninit")]] { + auto c6 = [&x] { (void)x; }; // OK: suppressed + (void)c6; + } + (void)c1; (void)c2; (void)c3; (void)c4; (void)c5; +} + +// Parse-order store credit reaches the capture check both ways -- accepted +// leniencies of the scope-less credit map (false negatives only): +void test_ref_capture_after_store() { + int u [[uninit]]; + u = 5; + auto c = [&u] { (void)u; }; // OK: the store initialized u (symmetric + // with binding &u after the store) + (void)c; +} + +void test_ref_capture_body_store() { + int u [[uninit]]; + auto L = [&] { u = 5; }; // OK: the body's own store credits u at parse + // order, silencing this capture check -- the + // deliberate leniency (no FunctionScopeInfo + // scoping), pinned here + int *q = &u; // OK: credited by the body store above + (void)L; (void)q; +} + +// A by-reference capture of a variable with a non-dependent type fires at +// definition time, and again when TreeTransform's unconditional lambda +// rebuild re-processes the capture at instantiation -- the accepted +// repetition. (The body must not assign x, as in test_ref_captures.) +template +void template_ref_capture_bad() { + int x [[uninit]]; + auto c = [&x] { (void)x; }; // expected-error 2 {{capturing 'x' by reference binds a reference to uninitialized memory under profile 'std::init'}} + (void)c; +} +template void template_ref_capture_bad(); // expected-note {{in instantiation of function template specialization 'template_ref_capture_bad' requested here}} + +struct OpTag {}; +OpTag operator+(OpTag, int *p [[ref_to_uninit]]); + +struct Assignable { + Assignable &operator=(int *p [[ref_to_uninit]]); +}; + +void test_operator_arguments() { + OpTag t; + (void)(t + &g_uninit); // OK + (void)(t + &g_init); // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + + Assignable a; + a = &g_uninit; // OK + a = &g_init; // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} +} + +// A non-dependent functor-call argument fires at definition time like every +// other Decl-less binding site, and repeats when the call is rebuilt at +// instantiation (the local functor forces the rebuild). +template +void template_functor_bad() { + MarkedFunctor mf; + mf(&g_init); // expected-error 2 {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} +} +template void template_functor_bad(); // expected-note {{in instantiation of function template specialization 'template_functor_bad' requested here}} + +// Pass-through sources reach the recognizer at the call-argument site too. A +// braced scalar pointer argument additionally warns (braces around scalar +// initializer), so the braced cases here use references; the pointer recognizer +// is exercised at this site by the conditional and comma forms. +void test_passthrough_call_arguments(bool c) { + take_uninit_ref({g_uninit}); // OK + take_uninit_ref({g_init}); // expected-error {{reference marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + take_ref({g_uninit}); // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + take_ref({g_init}); // OK + + take_uninit_ptr(c ? &g_uninit : &g_init); // OK: either arm may be uninitialized + take_ptr(c ? &g_uninit : &g_init); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + take_uninit_ref(c ? g_uninit : g_uninit2);// OK + take_ref(c ? g_uninit : g_init); // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + + take_uninit_ptr((h(), &g_uninit)); // OK + take_ptr((h(), &g_uninit)); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + take_uninit_ref((h(), g_init)); // expected-error {{reference marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + take_ref((h(), g_uninit)); // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} + +struct Inner { int m; }; + +// Member access through a [[ref_to_uninit]] pointer denotes uninitialized +// storage. Arrow access (a->m, object *a) and explicit deref ((*a).m) must +// behave identically. +void test_member_through_pointer(Inner *ptr [[ref_to_uninit]]) { + int *q1 = &ptr->m; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *q2 = &(*ptr).m; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *q3 [[ref_to_uninit]] = &ptr->m; // OK + (void)q1; (void)q2; (void)q3; +} + +struct WithFields { + int *p1 [[ref_to_uninit]] = &g_uninit; // OK + int *p2 [[ref_to_uninit]] = &g_init; // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + int *p3 = &g_uninit; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *p4 = &g_init; // OK + int *p5 = nullptr; // OK + int &r1 [[ref_to_uninit]] = g_uninit; // OK + int &r2 = g_uninit; // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +}; + +// [[profiles::suppress]] on a data member must cover its initializer's +// finalization checks, not just the initializer's parsing. +struct WithSuppressedFields { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "ref_to_uninit")]] int *p1 = &g_uninit; // OK: rule-targeted suppress + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] int *p2 = &g_uninit; // OK: whole-profile suppress + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] int *p3 [[ref_to_uninit]] = &g_init; // OK: suppressed (marked target, initialized source) +}; + +// A suppress on the enclosing record covers its members' initializers. +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +struct [[profiles::suppress(std::init)]] WithClassLevelSuppress { + int *p = &g_uninit; // OK: suppressed by the class-level attribute +}; + +[[ref_to_uninit]] int *ret_uninit_ptr_ok() { return &g_uninit; } // OK +[[ref_to_uninit]] int *ret_uninit_ptr_bad() { + return &g_init; // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} +} +int *ret_ptr_bad() { + return &g_uninit; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} +int *ret_ptr_ok() { return &g_init; } // OK + +[[ref_to_uninit]] int &ret_uninit_ref_ok() { return g_uninit; } // OK +int &ret_ref_bad() { + return g_uninit; // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} + +// Pass-through sources at the return site mirror the variable-init behavior. A +// braced scalar pointer return warns (braces around scalar initializer), so the +// braced returns use references; the pointer recognizer is reached here by the +// conditional and comma forms. +[[ref_to_uninit]] int &ret_braced_ref_ok() { return {g_uninit}; } // OK +int &ret_braced_ref_bad() { + return {g_uninit}; // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} +[[ref_to_uninit]] int &ret_braced_ref_bad2() { + return {g_init}; // expected-error {{reference marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} +} + +[[ref_to_uninit]] int *ret_cond_ptr_ok(bool c) { return c ? &g_uninit : &g_init; } // OK +int *ret_cond_ptr_bad(bool c) { + return c ? &g_uninit : &g_init; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} + +[[ref_to_uninit]] int *ret_comma_ptr_ok() { return (h(), &g_uninit); } // OK +int *ret_comma_ptr_bad() { + return (h(), &g_uninit); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} + +int *ret_suppressed() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] return &g_uninit; // OK: suppressed +} + +// A return inside a lambda binds against the *lambda's own* call operator +// (paper §8.2: the function returning the value must itself be declared +// appropriately), whose [[ref_to_uninit]] marker is spelled in the C++23 +// attribute position after the lambda-introducer. A deduced return type is +// resolved before the check runs, so `auto` lambdas are covered too. +// (co_return is not this hook's: it lowers to promise.return_value(e), whose +// argument funnels through the call-argument binding site.) +void test_lambda_return_unmarked() { + auto explicit_ret = [](int *q [[ref_to_uninit]]) -> int * { + return q; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + }; + auto deduced_ret = [](int *q [[ref_to_uninit]]) { + return q; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + }; + auto ref_ret = []() -> int & { + return g_uninit; // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + }; + auto ok = []() -> int * { return &g_init; }; // OK + (void)explicit_ret; (void)deduced_ret; (void)ref_ret; (void)ok; +} + +// The marked call operator is enforced in both directions, enabling the §4.4 +// get_uninit() idiom in lambda form. +void test_lambda_return_marked() { + auto marked = [] [[ref_to_uninit]] (int *q [[ref_to_uninit]]) -> int * { + return q; // OK: marked operator returning uninitialized memory + }; + auto marked_bad = [] [[ref_to_uninit]] () -> int * { + return &g_init; // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + }; + (void)marked; (void)marked_bad; +} + +// The enclosing function's marker never leaks into a lambda's returns (and +// vice versa): each return is attributed to its own scope's declaration. +[[ref_to_uninit]] int *marked_fn_lambda_isolated() { + auto inner = []() -> int * { + return &g_uninit; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + }; + (void)inner; + return &g_uninit; // OK: the function's own marker +} +int *unmarked_fn_marked_lambda() { + auto inner = [] [[ref_to_uninit]] () -> int * { + return &g_uninit; // OK: the lambda's own marker + }; + (void)inner; + return &g_init; // OK: the function itself is unmarked +} + +// Nested lambdas: the inner return is checked against the inner operator, the +// outer return against the outer one. +void test_nested_lambda_returns() { + auto outer = [] [[ref_to_uninit]] () -> int * { + auto inner = []() -> int * { + return &g_uninit; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + }; + (void)inner; + return &g_uninit; // OK: the outer operator is marked + }; + (void)outer; +} + +// Suppression covers a lambda return like any other site: the declaration +// statement's parse-time dominion spans the lambda body's tokens. +void test_lambda_return_suppress() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] auto l = []() -> int * { + return &g_uninit; // OK: suppressed + }; + auto m = []() -> int * { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] return &g_uninit; // OK: suppressed + }; + (void)l; (void)m; +} + +// Parse-order store credit reaches lambda returns like every binding: the +// body's own store precedes the return in parse order, so the credited entity +// is initialized memory and the unmarked return is accepted (the by-ref +// capture is accepted for the same reason, see test_ref_capture_body_store). +void test_lambda_return_after_store() { + int u [[uninit]]; + auto l = [&]() -> int * { + u = 5; + return &u; // OK: credited by the store above + }; + (void)l; +} + +// A generic lambda's call operator is a template pattern: the Decl-carrying +// return check defers via isTemplated and fires when the call operator is +// instantiated -- mirroring the variable-init site (template_nondependent_bad) +// -- so a never-invoked generic lambda's return stays undiagnosed (deferred, +// never instantiated), and an invoked one fires per instantiation. +void test_generic_lambda_return() { + auto never = [](auto) -> int * { return &g_uninit; }; // OK: never instantiated + auto invoked = [](auto) -> int * { + return &g_uninit; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + }; + invoked(0); // expected-note {{in instantiation of function template specialization}} + (void)never; +} + +// A block's return is the same capturing-scope binding with no declaration to +// carry the marker, so it is checked as an unmarked target (like a variadic +// argument), through the same null-target path. +void test_block_return() { + int *(^b1)() = ^int *() { + return &g_uninit; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + }; + int *(^b2)() = ^int *() { + return &g_init; // OK + }; + (void)b1; (void)b2; +} + +template +void template_bad() { + T *p = &g_uninit; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)p; +} +template void template_bad(); // expected-note {{in instantiation of function template specialization 'template_bad' requested here}} + +// A *non-dependent* pointer bound to uninitialized memory inside a template +// body is diagnosed once, at instantiation, not on the pattern (no double-fire). +template +void template_nondependent_bad() { + int *p = &g_uninit; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)p; +} +template void template_nondependent_bad(); // expected-note {{in instantiation of function template specialization 'template_nondependent_bad' requested here}} + +// A dependent [[ref_to_uninit]] parameter defers marker validation to +// instantiation (the pattern is accepted); the instantiated parameter carries +// the marker and drives this rule at the call. +template +void dependent_marked_fill(T p [[ref_to_uninit]]) { (void)p; } +void test_dependent_marked_param() { + dependent_marked_fill(&g_uninit); // OK: marked target, uninit source + dependent_marked_fill(&g_init); // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} +} + +// A default-initialized new-expression that leaves a scalar subobject +// indeterminate (e.g. new int, new int[n], paper §1.2 / §4.3) is a source of +// uninitialized free-store memory; new T(...), new T{...}, and a type with a +// user-provided default constructor are initialized. +namespace std { enum class byte : unsigned char {}; } + +struct NewAgg { int x; }; +struct NewWithCtor { NewWithCtor(); int x; }; + +void test_new_scalar() { + int *n1 [[ref_to_uninit]] = new int; // OK + int *n2 = new int; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *n3 = new int(5); // OK + int *n4 = new int(); // OK + int *n5 = new int{}; // OK + int *n6 [[ref_to_uninit]] = new int(5); // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + int *n7 [[ref_to_uninit]] = new int(); // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + (void)n1; (void)n2; (void)n3; (void)n4; (void)n5; (void)n6; (void)n7; +} + +void test_new_array(int n) { + int *a1 [[ref_to_uninit]] = new int[10]; // OK + int *a2 = new int[10]; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *a3 [[ref_to_uninit]] = new int[n]; // OK + int *a4 = new int[n]; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)a1; (void)a2; (void)a3; (void)a4; +} + +void test_new_class() { + NewWithCtor *c1 = new NewWithCtor; // OK: user-provided default ctor trusted + NewWithCtor *c2 [[ref_to_uninit]] = new NewWithCtor; // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + NewAgg *a1 = new NewAgg; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + NewAgg *a2 [[ref_to_uninit]] = new NewAgg; // OK + std::byte *b1 = new std::byte; // OK: std::byte exemption inherited + std::byte *b2 [[ref_to_uninit]] = new std::byte; // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + (void)c1; (void)c2; (void)a1; (void)a2; (void)b1; (void)b2; +} + +struct NewInFields { + int *p1 [[ref_to_uninit]] = new int; // OK + int *p2 = new int; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *p3 = new int(5); // OK +}; + +void test_new_assignment() { + int *p [[ref_to_uninit]] = new int; + p = new int; // OK + p = new int(5); // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + int *q = new int(0); + q = new int; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + q = new int(0); // OK + (void)p; (void)q; +} + +// A written initializer for an *allocated pointer* is itself a binding: the +// heap pointer object cannot carry [[ref_to_uninit]], so it must not be bound +// to uninitialized memory. Both the parenthesized and the braced form are +// checked; copying the value of a marked pointer is the same violation, as at +// variable scope. +void test_new_pointer_init() { + int **n1 = new (int *)(&g_init); // OK + int **n2 = new (int *)(&g_uninit); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int **n3 = new (int *){&g_uninit}; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *rtu [[ref_to_uninit]] = &g_uninit; + int **n4 = new (int *)(rtu); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int **n5 [[ref_to_uninit]] = new (int *); // OK: no written initializer -- the + // allocated pointer is indeterminate, + // so the marked target accepts it + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] { + int **s = new (int *)(&g_uninit); // OK: suppressed + (void)s; + } + (void)n1; (void)n2; (void)n3; (void)n4; (void)n5; +} + +// A dependent allocated type defers on the pattern and fires once, at +// instantiation. +template +void template_new_pointer_bad() { + T **p = new (T *)(&g_uninit); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)p; +} +template void template_new_pointer_bad(); // expected-note {{in instantiation of function template specialization 'template_new_pointer_bad' requested here}} + +void test_new_call_arguments() { + take_uninit_ptr(new int); // OK + take_uninit_ptr(new int(5)); // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + take_ptr(new int); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + take_ptr(new int(5)); // OK +} + +[[ref_to_uninit]] int *ret_new_uninit_ok() { return new int; } // OK +[[ref_to_uninit]] int *ret_new_uninit_bad() { + return new int(5); // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} +} +int *ret_new_ptr_bad() { + return new int; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} +int *ret_new_ptr_ok() { return new int(0); } // OK + +void test_new_suppress() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "ref_to_uninit")]] int *s = new int; // OK: suppressed + (void)s; +} + +template +void template_new_bad() { + T *p = new T; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)p; +} +template void template_new_bad(); // expected-note {{in instantiation of function template specialization 'template_new_bad' requested here}} + +// The call-argument, pointer-assignment, and return sites pass no Decl, so +// (unlike the variable-init site, template_nondependent_bad above) they defer +// only on an instantiation-dependent source. These sources are non-dependent, +// so each fires at definition time -- and each construct is rebuilt at +// instantiation anyway (the callee's implicit cast is stripped, forcing a +// call rebuild; the local p is remapped; a return statement always rebuilds), +// so the diagnostic repeats there. The repetition is accepted for now. +template +void template_call_arg_unmarked() { + take_ptr(&g_uninit); // expected-error 2 {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} +template void template_call_arg_unmarked(); // expected-note {{in instantiation of function template specialization 'template_call_arg_unmarked' requested here}} + +template +void template_call_arg_marked() { + take_uninit_ptr(&g_init); // expected-error 2 {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} +} +template void template_call_arg_marked(); // expected-note {{in instantiation of function template specialization 'template_call_arg_marked' requested here}} + +template +void template_assignment_bad() { + int *p = nullptr; + p = &g_uninit; // expected-error 2 {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)p; +} +template void template_assignment_bad(); // expected-note {{in instantiation of function template specialization 'template_assignment_bad' requested here}} + +template +int *template_return_bad() { + return &g_uninit; // expected-error 2 {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} +template int *template_return_bad(); // expected-note {{in instantiation of function template specialization 'template_return_bad' requested here}} + +// The definition-time fire repeats once per instantiation that rebuilds the +// construct: two explicit instantiations pin the exact counts (one pattern +// fire plus one per specialization). +template +void template_assignment_repeats() { + int *p = nullptr; + p = &g_uninit; // expected-error 3 {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)p; +} +template void template_assignment_repeats(); // expected-note {{in instantiation of function template specialization 'template_assignment_repeats' requested here}} +template void template_assignment_repeats(); // expected-note {{in instantiation of function template specialization 'template_assignment_repeats' requested here}} + +template +int *template_return_repeats() { + return &g_uninit; // expected-error 3 {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} +template int *template_return_repeats(); // expected-note {{in instantiation of function template specialization 'template_return_repeats' requested here}} +template int *template_return_repeats(); // expected-note {{in instantiation of function template specialization 'template_return_repeats' requested here}} + +// An instantiation-dependent source is not checkable on the pattern: no +// definition-time fire. The construct is rebuilt at every instantiation, so +// each violating specialization diagnoses once, with its note chain. +template +void template_dependent_per_spec() { + T *p = nullptr; + p = (T *)&g_uninit; // expected-error 2 {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)p; +} +template void template_dependent_per_spec(); // expected-note {{in instantiation of function template specialization 'template_dependent_per_spec' requested here}} +template void template_dependent_per_spec(); // expected-note {{in instantiation of function template specialization 'template_dependent_per_spec' requested here}} + +// Fully non-dependent constructs whose operands transform to themselves are +// *reused* by TreeTransform at instantiation -- their Build* never re-runs. +// Deferring would silently lose the diagnostic (these all-global shapes were +// silent before), so they are checked at definition time: exactly one error, +// on the pattern, with no instantiation note. +int *g_ptr_sink = nullptr; +int **g_pp_sink = nullptr; + +template +void template_allglobal_bad() { + g_ptr_sink = &g_uninit; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + throw &g_uninit; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + g_pp_sink = new (int *)(&g_uninit); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} +template void template_allglobal_bad(); + +// The same shapes diagnose in a never-instantiated template: definition-time +// checking deliberately trades strict "as-if after phase 7" purity for +// reuse-proof diagnostics. +template +void template_allglobal_never_instantiated() { + g_ptr_sink = &g_uninit; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + throw &g_uninit; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + g_pp_sink = new (int *)(&g_uninit); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} + +// A never-instantiated template diagnoses its non-dependent violations at +// definition time; only instantiation-dependent constructs (the (T *) cast) +// stay silent without an instantiation. +template +void template_never_instantiated() { + take_ptr(&g_uninit); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *p = nullptr; + p = &g_uninit; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + p = (T *)&g_uninit; + (void)p; +} + +// A value-dependent if-constexpr condition is not yet known discarded at the +// pattern, so the branch is live at parse and its non-dependent violations +// diagnose at definition time. The f instantiation discards the branch +// (never rebuilding its statements), so nothing repeats. +template +void template_discarded_branch() { + if constexpr (sizeof(T) > 1000) { + take_ptr(&g_uninit); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *p = nullptr; + p = &g_uninit; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)p; + } +} +template void template_discarded_branch(); + +// A generic lambda's body is a template pattern even in a non-template +// function: a non-dependent violation diagnoses at definition time whether or +// not the lambda is ever invoked, and an all-global shape is reused (not +// rebuilt) when the call operator is instantiated, so invoking does not +// repeat it. +void generic_lambda_never_invoked() { + auto l = [](auto x) { g_ptr_sink = &g_uninit; }; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)l; +} + +void generic_lambda_invoked() { + auto l = [](auto x) { g_ptr_sink = &g_uninit; }; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + l(1); +} + +// A late-parsed inline member of a class template is a pattern too: a +// non-dependent violation diagnoses when its body is parsed. Suppression on +// the method or on the class covers the definition-time fire like any other. +template +struct LateParsedMember { + void m() { g_ptr_sink = &g_uninit; } // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +}; + +template +struct LateParsedSuppressMethod { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] void m() { g_ptr_sink = &g_uninit; } // OK: suppressed +}; + +template +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +struct [[profiles::suppress(std::init)]] LateParsedSuppressClass { + void m() { g_ptr_sink = &g_uninit; } // OK: suppressed +}; + +// std::init / uninit_read (paper §4.5): a read *through* a [[ref_to_uninit]] +// pointer or reference yields an uninitialized value, diagnosed at the +// lvalue-to-rvalue conversion (Sema::DefaultLvalueConversion). These reads fire +// only under -fprofiles; the no-profiles run stays clean. A direct read of a +// named [[uninit]] object is left to the flow-based uninit_read pass and is not +// retested here. +void take_value(int v); + +int test_read_through_pointer(int *p [[ref_to_uninit]], int i) { + int y1 = *p; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + int y2 = p[i]; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + int y3 = *p + 1; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + take_value(*p); // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + if (*p) // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + h(); + (void)y1; (void)y2; (void)y3; + return *p; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} +} + +void test_read_through_reference(int &r [[ref_to_uninit]], Inner *ptr [[ref_to_uninit]], + void *vp [[ref_to_uninit]]) { + int y1 = r; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + int y2 = ptr->m; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + int y3 = (*ptr).m; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + int y4 = *(int *)vp; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + int y5 = get_uninit_ref(); // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + (void)y1; (void)y2; (void)y3; (void)y4; (void)y5; +} + +// Paper §4.5: reading an uninitialized std::byte is permitted, so a read +// through a [[ref_to_uninit]] std::byte pointer/reference is not diagnosed. +void test_read_byte_exempt(std::byte *bp [[ref_to_uninit]], std::byte &br [[ref_to_uninit]]) { + std::byte b1 = *bp; // OK + std::byte b2 = br; // OK + (void)b1; (void)b2; +} + +void test_read_suppress(int *p [[ref_to_uninit]]) { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] { take_value(*p); } // OK: whole-profile suppress + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "uninit_read")]] { take_value(*p); } // OK: rule-targeted suppress +} + +// The suppression dominion of a member declaration includes its initializer +// tokens (P3589R2 s2.4p3). The read-through check fires from +// ActOnFinishCXXInClassMemberInitializer during the late parse of an NSDMI, +// so a suppression on the field (or, via the lexical parent walk, on the +// class) must cover it; a sibling member's suppression must not leak. +int *nsdmi_rtu [[ref_to_uninit]] = &g_uninit; + +struct NsdmiReadSuppress { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] int x = *nsdmi_rtu; // OK: field suppress + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "uninit_read")]] int y = *nsdmi_rtu; // OK: rule-targeted + int z = *nsdmi_rtu; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} +}; + +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +struct [[profiles::suppress(std::init)]] NsdmiClassReadSuppress { + int x = *nsdmi_rtu; // OK: class-level suppression via the lexical parent walk +}; + +// None of these is a read through the marker: a discarded-value expression and +// an address-of apply no lvalue-to-rvalue conversion, a write targets the +// glvalue without loading it, a reference binding is not a load, and copying +// the pointer value reads the (initialized) pointer object rather than through +// it. +void test_read_negatives(int *p [[ref_to_uninit]], int &r [[ref_to_uninit]], + Inner *ptr [[ref_to_uninit]], int *base [[ref_to_uninit]]) { + (void)r; // OK: discarded value + (void)*p; // OK: discarded value + int *ap [[ref_to_uninit]] = &*p; // OK: address-of is not a read + int &r2 [[ref_to_uninit]] = *p; // OK: reference binding, not a read + *p = 5; // OK: write, not a read (and it credits + // p's pointee, so it stays after the + // marked bindings above) + ptr->m = 5; // OK: write, not a read + int *q [[ref_to_uninit]] = base; // OK: reads the pointer value, not through it + (void)ap; (void)q; (void)r2; +} + +// A subobject read of a named [[uninit]] object loads an uninitialized value, +// exactly like a read through a [[ref_to_uninit]] pointer: member-wise delayed +// initialization of an [[uninit]] object is banned (paper §5.4), so no +// assignment could have given the member a value. Only the *whole-object* +// direct read of a named [[uninit]] entity is left to the flow-based +// uninit_read pass (which credits assignments). +struct Pair { int x; int y; }; +struct PairHolder { Pair p; }; + +void test_member_read_of_uninit_object() { + Pair s [[uninit]]; + int y1 = s.x; // expected-error {{read of a subobject of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} + take_value(s.y); // expected-error {{read of a subobject of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} + PairHolder o [[uninit]]; + int y2 = o.p.x; // expected-error {{read of a subobject of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} + // The arrow spelling reaches the member through a pointer, so the + // diagnostic's phrasing approximation picks the pointer wording; the read is + // diagnosed all the same. + int y3 = (&s)->x; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + (void)y1; (void)y2; (void)y3; +} + +// Discarded values and address-taking apply no lvalue-to-rvalue conversion +// and are not reads; taking the member's address is the binding checks' +// territory (unchanged behavior, retested as a regression guard). A write is +// not a read either, but a subobject store of an [[uninit]] object is itself +// banned as delayed initialization (uninit_write; full coverage in +// safety-profile-init-write.cpp). +void test_member_read_negatives() { + Pair s [[uninit]]; + s.x = 1; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} + (void)s.x; // OK: discarded value + int *p = &s.x; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *q [[ref_to_uninit]] = &s.x; // OK: binding checked by ref_to_uninit + (void)p; (void)q; +} + +void test_member_read_suppress() { + Pair s [[uninit]]; + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "uninit_read")]] { take_value(s.x); } // OK +} + +// std::byte members stay exempt (paper §4.5). +struct WithByte { std::byte b; int i; }; +void test_member_read_byte_exempt() { + WithByte s [[uninit]]; + std::byte b = s.b; // OK + (void)b; +} + +// A whole-record copy from *pp reads the uninitialized pointee (paper +// abstract: an object marked [[ref_to_uninit]] cannot be read through), but +// class types never reach the lvalue-to-rvalue chokepoint. The copy is caught +// all the same, by the binding rule at the copy constructor's reference +// parameter -- so the diagnostic is the binding one, not the read-through +// one. This holds for copy-, direct-, braced-, argument-, and return-copies +// alike. +void take_pair(Pair v); + +Pair test_record_copy_through(Pair *pp [[ref_to_uninit]]) { + Pair v = *pp; // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + Pair w(*pp); // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + Pair b = {*pp}; // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + take_pair(*pp); // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)v; (void)w; (void)b; + return *pp; // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} + +// The escape is the paper's own (§7.2): a copy constructor declared with a +// [[ref_to_uninit]] parameter accepts the uninitialized source -- and then, +// symmetrically, rejects an initialized one. +struct MarkedCopy { + int x; + MarkedCopy(); + MarkedCopy(const MarkedCopy &q [[ref_to_uninit]]); +}; + +void test_record_copy_marked_ctor(MarkedCopy *mp [[ref_to_uninit]], + const MarkedCopy &init) { + MarkedCopy v = *mp; // OK: the marked parameter accepts the uninit pointee + MarkedCopy w = init; // expected-error {{reference marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + (void)v; (void)w; +} + +// A record *containing* std::byte members is not std::byte, so the §4.5 read +// exemption does not extend to the whole-record binding. +struct ByteBox { std::byte b; }; +void test_record_copy_byte_member(ByteBox *bp [[ref_to_uninit]]) { + ByteBox v = *bp; // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)v; +} + +// An element read of a named [[uninit]] array is a subobject read exactly like +// s.x: neither flow pass tracks array elements (and element-wise delayed +// initialization is banned, paper §5.5), so the marker counts below the +// element access even for a read. *a denotes the same element as a[0]. +void test_element_read_of_uninit_array(int i) { + [[uninit]] int a[2]; + int y1 = a[0]; // expected-error {{read of a subobject of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} + take_value(a[i]); // expected-error {{read of a subobject of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} + int y2 = *a; // expected-error {{read of a subobject of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} + [[uninit]] int m[2][2]; + int y3 = m[1][0]; // expected-error {{read of a subobject of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} + (void)y1; (void)y2; (void)y3; +} + +// An [[uninit]] array *member*'s element read is flagged the same way: like +// the class-type member in HasAggMember below, an array member has no legal +// element-wise assignment path, so the marker counts even on the current +// object. +struct WithArrMember { + [[uninit]] int a[2]; + int get() { return a[0]; } // expected-error {{read of a subobject of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} +}; + +// Discarded values and address-taking apply no lvalue-to-rvalue conversion +// and are not reads; bindings to the array or its elements stay the +// ref_to_uninit checks' territory (regression guards, unchanged behavior). An +// element store is not a read either, but is itself banned as delayed +// initialization (uninit_write; full coverage in +// safety-profile-init-write.cpp). +void test_element_read_negatives(int i) { + [[uninit]] int a[2]; + a[0] = 1; // expected-error {{writing an element of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} + (void)a[0]; // OK: discarded value + int *p [[ref_to_uninit]] = &a[0]; // OK: address-of is not a read; binding checked by ref_to_uninit + int *q [[ref_to_uninit]] = a; // OK: array decay is a binding, not a read + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "uninit_read")]] { take_value(a[i]); } // OK: rule-targeted suppress + (void)p; (void)q; +} + +// std::byte arrays stay exempt (paper §4.5). +void test_element_read_byte_exempt() { + [[uninit]] std::byte b[2]; + std::byte v = b[0]; // OK + (void)v; +} + +// The dependent element type makes the read instantiation-dependent, so it +// defers on the pattern and fires once, at instantiation. +template +void template_element_read_bad() { + [[uninit]] T a[2]; + int y = a[0]; // expected-error {{read of a subobject of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} + (void)y; +} +template void template_element_read_bad(); // expected-note {{in instantiation of function template specialization 'template_element_read_bad' requested here}} + +// The §5.2 trust pattern is preserved: a scalar [[uninit]] *member* of the +// current object may be assigned in the constructor body (flow-checked by the +// ctor-body pass), so a member-function read of it is not flagged here. +struct BodyInit { + int m [[uninit]]; + BodyInit() { m = 1; } + int get() { return m; } // OK: trusted, assigned in the constructor body +}; + +// But a subobject of an [[uninit]] *class-type member* has no legal +// assignment path (member-wise delayed initialization is banned, and +// construct_at flow is uniformly unmodeled), so its read is flagged even on +// the current object. +struct HasAggMember { + Pair agg [[uninit]]; + int get() { return agg.x; } // expected-error {{read of a subobject of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} +}; + +// Like every Decl-less check, the member read fires at definition time when +// its glvalue is non-dependent, and repeats when the read is rebuilt at +// instantiation (the local s is remapped) -- the accepted repetition. +template +void template_member_read_bad() { + Pair s [[uninit]]; + int y = s.x; // expected-error 2 {{read of a subobject of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} + (void)y; +} +template void template_member_read_bad(); // expected-note {{in instantiation of function template specialization 'template_member_read_bad' requested here}} + +// A read through a [[ref_to_uninit]] parameter inside a template body fires at +// definition time when the operand is non-dependent +// (template_read_nondependent_bad; the parameter remap rebuilds the read at +// instantiation, repeating the diagnostic) and defers to instantiation when it +// is dependent (template_read_dependent_bad). Mirrors the binding template_* +// cases above. +template +void template_read_nondependent_bad(int *p [[ref_to_uninit]]) { + int y = *p; // expected-error 2 {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + (void)y; +} +template void template_read_nondependent_bad(int *); // expected-note {{in instantiation of function template specialization 'template_read_nondependent_bad' requested here}} + +template +T template_read_dependent_bad(T *p [[ref_to_uninit]]) { + return *p; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} +} +template int template_read_dependent_bad(int *); // expected-note {{in instantiation of function template specialization 'template_read_dependent_bad' requested here}} + +// A never-instantiated pattern diagnoses its non-dependent read at definition +// time, exactly once. +template +void template_read_never_instantiated(int *p [[ref_to_uninit]]) { + int y = *p; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + (void)y; +} + +// An all-global read: the definition-time fire, plus a repeat when the +// initialization of the local y is rebuilt at instantiation. +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(std::init, rule: "static_marker")]] [[uninit]] Pair g_uninit_pair; + +template +void template_global_read_bad() { + int y = g_uninit_pair.x; // expected-error 2 {{read of a subobject of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} + (void)y; +} +template void template_global_read_bad(); // expected-note {{in instantiation of function template specialization 'template_global_read_bad' requested here}} + +template +void template_global_read_never_instantiated() { + int y = g_uninit_pair.x; // expected-error {{read of a subobject of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} + (void)y; +} + +// A compound assignment and a built-in ++/-- read the old value before +// storing, but build no lvalue-to-rvalue node for the operand, so the +// operator sites check the load directly. Every compound form through a +// [[ref_to_uninit]] pointer or reference is diagnosed; the shift forms load +// through their LHS promotion instead and must fire exactly once. Reading an +// unmarked pointer's pointee is trusted, and ++ on the marked pointer itself +// reads the (initialized) pointer object, not through it. Each form gets a +// fresh marker: a compound form both reads (the error) and stores, and the +// store credits the pointee for everything after it in parse order (see +// test_pointee_store_credit) -- except through an element access, which +// never sees the credit (p[i] below, after *p's store; paper §5.4). +void test_compound_read_through(int *p [[ref_to_uninit]], int *q, + int &r [[ref_to_uninit]], + Inner *ptr [[ref_to_uninit]], int i, + int *p2 [[ref_to_uninit]], + int *p3 [[ref_to_uninit]], + int &r2 [[ref_to_uninit]], + int *p4 [[ref_to_uninit]]) { + *p += 1; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + p[i] -= 1; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + r *= 2; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + ptr->m |= 1; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + ++*p2; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + (*p3)--; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + ++r2; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + *p4 <<= 1; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + *q += 1; // OK: an unmarked pointer is trusted initialized + ++p; // OK: reads the pointer object itself, not through it +} + +void test_compound_read_suppress(int *p [[ref_to_uninit]]) { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "uninit_read")]] { *p += 1; } // OK: rule-targeted suppress +} + +// Like every Decl-less read check, the compound read fires at definition time +// on a non-dependent operand and repeats when the parameter remap rebuilds the +// assignment at instantiation. +template +void template_compound_read_bad(int *p [[ref_to_uninit]]) { + *p += 1; // expected-error 2 {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} +} +template void template_compound_read_bad(int *); // expected-note {{in instantiation of function template specialization 'template_compound_read_bad' requested here}} + +// Parse-order pointee store credit (paper §4.3/§4.5): a whole-`*p` store +// through a [[ref_to_uninit]] pointer is the pointee's initialization, so +// whole-`*p` accesses after it (in parse order) are legal -- and the paper's +// reverse direction applies: the credited pointer now refers to initialized +// memory and REQUIRES an unmarked target. Element accesses never see the +// credit in either direction (§5.4's random-access ban), and reseating the +// pointer clears it. +void test_pointee_store_credit(int *p [[ref_to_uninit]]) { + *p = 5; // OK: the write initializes the pointee (and credits it) + *p = 7; // OK: further whole-entity stores stay legal + int x = *p; // OK: credited (rejected before the store) + int *r2 [[ref_to_uninit]] = p; // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + (void)x; (void)r2; +} + +void test_pointee_read_before_store(int *p [[ref_to_uninit]]) { + int x = *p; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + *p = 5; + (void)x; +} + +// The credit is recorded at the tail of the assignment, after the RHS is +// checked: a self-assignment's RHS read must not be silenced by its own +// store (the key recording-order regression test). +void test_pointee_no_self_credit(int *p [[ref_to_uninit]]) { + *p = *p; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} +} + +// Element stores neither credit nor invalidate (§5.4/§5.5: element-wise +// state is untrackable by design)... +void test_subscript_store_no_credit(int *p [[ref_to_uninit]]) { + p[0] = 1; + int x = *p; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + (void)x; +} + +// ...and element reads never see pointee credit: `*p = 5;` must not legalize +// p[1] (the pointee may be an array with only element 0 written). The model +// is purely syntactic, so even p[0] -- the same storage as *p -- stays an +// error: only the whole-`*p` form is credited. +void test_subscript_read_not_credited(int *p [[ref_to_uninit]], int i) { + *p = 5; + int x = p[i]; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + int y = p[0]; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + (void)x; (void)y; +} + +// Reseating the pointer -- plain assignment, compound arithmetic, or ++ -- +// clears its pointee credit: the credit described the old pointee. +void test_reseat_clears_credit(int *p [[ref_to_uninit]], + int *q [[ref_to_uninit]], int n) { + *p = 5; + p = q; + int x = *p; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + *p = 5; + p += n; + int y = *p; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + *p = 5; + p++; + int z = *p; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + (void)x; (void)y; (void)z; +} + +// The *pointee* credit map keys on local VarDecls, so a [[ref_to_uninit]] +// *member* pointer is never credited: a read through it keeps failing even +// after a store through the exact same lvalue. The per-object *whole-member* +// credit below deliberately does not extend here: pointee aliasing is +// per-value, not per-object (a copy of the object shares the pointee), so +// crediting `(w, p)` would be unsound the moment w is copied. +struct WithMarkedPtrField { + int *p [[ref_to_uninit]] = &g_uninit; +}; +void test_member_pointer_never_credited(WithMarkedPtrField w) { + *w.p = 5; + int x = *w.p; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + (void)x; +} + +// A member store through a marked class-typed pointer is a subobject store: +// trusted as a write (paper §4.5) but never crediting, so the member read +// after it still fails. +void test_member_store_never_credits(Inner *ptr [[ref_to_uninit]]) { + ptr->m = 5; + int y = ptr->m; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + (void)y; +} + +// Per-object whole-member store credit (paper §4.2: "After initialization, +// the object is no longer [[uninit]]"; §6: ordinary assignment initializes a +// built-in): `a.m = 5` credits exactly the (base object, member) pair, so a +// later binding of that member through the same base is legal -- the member +// analog of the locals credit above. The base identity is the directly named +// local-storage variable or, for current-object accesses (this->m / m), the +// enclosing function declaration, so unrelated objects and other function +// bodies never share credit. Same parse-order semantics: no dominance or +// flow analysis, missed diagnostics only. +struct MemberCredit { int m [[uninit]]; }; // expected-note {{member 'm' declared here}} +void mc_sink_ptr(int *); +void mc_sink_ref(const int &); +void mc_fill(int *p [[ref_to_uninit]]); + +void test_member_store_credit() { + MemberCredit a; + mc_sink_ref(a.m); // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + mc_sink_ptr(&a.m); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + a.m = 5; + mc_sink_ref(a.m); // OK: credited + mc_sink_ptr(&a.m); // OK: credited + int *q = &a.m; // OK: credited + (void)q; +} + +// The reverse direction applies too (mirroring test_assign_credited_to_marked): +// a credited member is initialized memory and now requires an unmarked target. +void test_member_credit_reverse_direction() { + MemberCredit a; + a.m = 5; + mc_fill(&a.m); // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} +} + +// The credit is per base object: a store to one local's member says nothing +// about another local of the same type (and per §5.2, nothing about a copy). +void test_member_credit_per_object() { + MemberCredit a1, a2; + a1.m = 5; + mc_sink_ref(a1.m); // OK: credited + mc_sink_ref(a2.m); // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} + +// Current-object members key on the enclosing function: `m = 5` in one member +// function never credits a binding in another, `this->m` / `m` / `(*this).m` +// share the key within one body, and a this-capturing lambda's body is its +// own function (its stores and the enclosing function's do not mix, in +// either direction). +struct ThisMemberCredit { + int m [[uninit]]; + void store_then_bind() { + this->m = 5; + mc_sink_ref(m); // OK: credited (same key as this->m) + mc_sink_ptr(&(*this).m); // OK: credited + } + void bind_without_store() { + mc_sink_ref(m); // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + } + void lambda_store_isolated() { + auto l = [this] { m = 5; }; // records under the lambda's own key + mc_sink_ptr(&m); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)l; + } + void lambda_bind_isolated() { + m = 5; // records under this function's key + auto l = [this] { + mc_sink_ptr(&m); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + }; + (void)l; + } +}; + +// In a constructor, the parse-time binding after a whole-member store is +// credited the same way (this was the escape-adjacent false positive); the +// CFG ctor-body pass independently keeps governing *reads*, with real flow +// analysis (safety-profile-init-ctor-body.cpp). +struct CtorMemberCredit { + int m [[uninit]]; + CtorMemberCredit() { + m = 5; + mc_sink_ptr(&m); // OK: credited by the store above + } + CtorMemberCredit(int) { + mc_sink_ptr(&m); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + m = 5; + } +}; + +// Compound assignment and ++/-- record through the same arm: the store side +// credits later bindings (the read side of `a.m += 1` on a local aggregate is +// the CFG local-members pass's, which flags it with flow precision). +void test_member_credit_compound() { + MemberCredit a; + a.m += 1; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} + mc_sink_ref(a.m); // OK: the compound store credited (a, m) +} + +// Only a single-level, directly named base earns or consults credit. A store +// below a marked class-type member (x.agg.m) is itself the banned piecemeal +// initialization (uninit_write, §5.4) and resolves no base, so it earns +// nothing -- the later binding still sees agg's marker below top level. A +// whole-member `x.agg = ...` cannot credit either: a class-typed assignment +// is a member operator= call on uninitialized storage, rejected outright. +struct AggMemberCredit { MemberCredit agg [[uninit]]; }; +void test_member_credit_single_level() { + AggMemberCredit x; + x.agg.m = 5; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} + mc_sink_ptr(&x.agg.m); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + x.agg = MemberCredit(); // expected-error {{calling member function 'operator=' binds its implicit object parameter to uninitialized memory under profile 'std::init'}} +} + +// A member of an object reached through a reference or pointer parameter is +// the pinned aliasing boundary: the store is trusted as a write, but the +// binding through that alias stays strict. +void test_member_credit_alias_boundary(MemberCredit &r, MemberCredit *p) { + r.m = 5; + mc_sink_ref(r.m); // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + p->m = 5; + mc_sink_ptr(&p->m); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} + +// Credit is recorded at pattern-parse time and independently at instantiation +// (fresh field and function declarations), so the store-then-bind sequence +// holds in templates too; the dependent binding defers on the pattern and +// re-runs on the rebuilt (credited-in-order) instantiation. +template +struct TmplMemberCredit { + int m [[uninit]]; + void f() { + this->m = 5; + mc_sink_ref(this->m); // OK at the pattern and at instantiation + } +}; +template struct TmplMemberCredit; + +// TreeTransform hands a fully non-dependent statement back *unchanged* when +// instantiating a body, so `m = 5` inside a generic lambda runs Sema (and +// records its credit) only at the pattern parse. The current-object key is +// the function's parse-time pattern on record and consult alike, so the +// instantiated call operators' checks still find that credit -- while the +// per-function isolation above is unchanged (different functions have +// different patterns). +struct GenericLambdaMemberCredit { + int m [[uninit]]; + void go() { + auto l = [this](auto) { + m = 5; + mc_sink_ref(m); // OK: credited at the pattern parse and per instantiation + mc_sink_ptr(&m); // OK + }; + l(1); + l(2L); + } +}; + +// The same statement reuse through a non-generic lambda in a member function +// template: the transformed call operator reaches its parsed pattern through +// a member-specialization link. +struct LambdaInMemberTemplateCredit { + int m [[uninit]]; + template void go() { + auto l = [this] { m = 5; mc_sink_ref(m); }; // OK + l(); + } +}; +template void LambdaInMemberTemplateCredit::go(); + +// [[now_init]] (P4222R2 §6.2): binding an entity to a [[ref_to_uninit]] +// parameter of a [[now_init]] function earns the same parse-order credit as +// the equivalent direct store -- the callee initializes the storage it was +// handed. A plain (non-[[now_init]]) callee still earns nothing (the strict +// no-escape-credit doctrine; the paper reserves callee-initialization for +// exactly this annotation). Inside constructors the CFG ctor-body pass has +// its own flow-precise [[now_init]] credit (safety-profile-init-ctor-body.cpp). +[[now_init]] void now_init_fill(int *p [[ref_to_uninit]]); +[[now_init]] void now_init_fill_ref(int &r [[ref_to_uninit]]); +[[now_init]] void now_init_variadic(int *p [[ref_to_uninit]], ...); +void ni_sink(int *); +void ni_cref(const int &); + +void test_now_init_whole_local() { + int u [[uninit]]; + ni_sink(&u); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + now_init_fill(&u); // OK: marked target, uninitialized source + ni_sink(&u); // OK: the callee initialized u + ni_cref(u); // OK + now_init_fill(&u); // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} +} + +// A plain callee taking the same marked parameter earns no credit: the +// binding after it stays the strict error. +void plain_fill(int *p [[ref_to_uninit]]); +void test_plain_callee_no_credit() { + int u [[uninit]]; + plain_fill(&u); + ni_sink(&u); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} + +// Passing the marked pointer's *value* is §6.2's initialize2(p) example +// verbatim: the callee initializes the pointee, so whole-`*p` accesses after +// the call are legal -- composed with the existing reseat machinery, which +// clears the credit like any other pointee credit. +void test_now_init_pointee(int *p [[ref_to_uninit]]) { + int before = *p; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + now_init_fill(p); // OK: marked-to-marked binding + int v = *p; // OK: the callee initialized the pointee + ni_sink(p); // OK: p now refers to initialized memory + (void)before; (void)v; +} + +void test_now_init_reseat(int *p [[ref_to_uninit]], int *q [[ref_to_uninit]]) { + now_init_fill(p); + p = q; // reseating clears the pointee credit + int x = *p; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + (void)x; +} + +void test_now_init_reference(int &r [[ref_to_uninit]]) { + now_init_fill_ref(r); // OK: marked reference onward to a marked parameter + int v = r; // OK: the referent is initialized (never lapses) + (void)v; +} + +// Members earn the same per-object credit as a direct member store, under +// the same base-identity keys and boundaries: a member of a parameter- +// reached object stays uncredited (the pinned aliasing boundary). +void test_now_init_member() { + MemberCredit a; + now_init_fill(&a.m); // OK + mc_sink_ptr(&a.m); // OK: (a, m) credited by the callee + mc_sink_ref(a.m); // OK +} +void test_now_init_member_boundary(MemberCredit &r) { + now_init_fill(&r.m); // OK: marked target, uninitialized source + mc_sink_ptr(&r.m); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} + +// The callee credit survives statement reuse in instantiated lambda bodies +// exactly like a direct store (the current-object key is the parse-time +// pattern; see GenericLambdaMemberCredit above). +struct GenericLambdaNowInitMemberCredit { + int m [[uninit]]; + void go() { + auto l = [this](auto) { + now_init_fill(&m); + mc_sink_ptr(&m); // OK: credited at the pattern parse and per instantiation + }; + l(1); + } +}; + +// A variadic argument reaches no declared parameter, so it earns nothing +// even from a [[now_init]] callee (and is checked as an unmarked target). +void test_now_init_variadic_no_credit() { + int u [[uninit]]; + int w [[uninit]]; + now_init_variadic(&u, &w); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + ni_sink(&u); // OK: the marked parameter credited u + ni_sink(&w); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} + +// [[now_init]] on a function template: a call to a concrete [[now_init]] +// callee credits at pattern-parse time, so definition-time checks find it; a +// dependent callee defers with the rest of the call and credits per +// instantiation, in the rebuilt parse order. +template +[[now_init]] void now_init_tmpl(T *p [[ref_to_uninit]]); + +template +void template_now_init_nondependent() { + int u [[uninit]]; + now_init_fill(&u); + ni_sink(&u); // OK at definition time and at instantiation +} +template void template_now_init_nondependent(); + +template +void template_now_init_dependent() { + T u [[uninit]]; + now_init_tmpl(&u); + T *s = &u; // OK per instantiation: the rebuilt call credits first + (void)s; +} +template void template_now_init_dependent(); + +// R2 §4.4's now_init() library function works today as a *pure declaration* +// with no compiler support at all: its unmarked return classifies as +// initialized (trusted, §4.3), so the returned pointer launders the storage +// -- the paper's own deliberate profile hole (its `return p;` definition is +// written once, under suppression). What the declaration alone cannot do is +// legalize the *original name* after the call; that is exactly what the §6.2 +// [[now_init]] attribute adds when placed on the same declaration. +template T *now_init(T *p [[ref_to_uninit]]); +template [[now_init]] T *now_init_annotated(T *p [[ref_to_uninit]]); + +void test_now_init_library_pattern() { + int m [[uninit]]; + int v = *now_init(&m); // OK today: the unmarked return is trusted + int *s = now_init(&m); // OK: unmarked pointer from an unmarked return + ni_cref(m); // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)v; (void)s; +} +void test_now_init_library_pattern_annotated() { + int m [[uninit]]; + int *s = now_init_annotated(&m); // OK + ni_cref(m); // OK: the attribute legalizes the original name + (void)s; +} + +// R2 §4.5's requested library annotation, verbatim: construct_at takes a +// [[ref_to_uninit]] pointer and returns a pointer to an initialized object. +// As a [[now_init]]-annotated declaration the lifecycle *start* works: the +// argument's whole object is credited and a repeated construct_at is even +// caught by the reverse-direction rule (a credited source no longer refers +// to uninitialized memory). The rest of the §4.5 lifecycle -- destroy_at, +// use-after-destroy -- remains future work. +template +[[now_init]] T *construct_at(T *p [[ref_to_uninit]], A &&...args); + +struct CtorAtPayload { int x; int y; }; +void test_construct_at_bridge() { + CtorAtPayload s [[uninit]]; + construct_at(&s, 1, 2); + int v = s.x; // OK: construct_at initialized the whole object + construct_at(&s, 3, 4); // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + CtorAtPayload t [[uninit]]; + int w = t.x; // expected-error {{read of a subobject of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} + (void)v; (void)w; +} + +// The §4.5 lifecycle *end*, via [[now_uninit]] -- the recording §4.4 wishes +// for ("the object subjected to destroy_at() should be considered +// uninitialized, but there is no way of recording that in the code"): a +// call to a [[now_uninit]] function withdraws the parse-order credit of the +// storage bound to each pointer/reference parameter, so the storage is +// uninitialized again. Re-construction becomes legal, and a second +// destruction, or binding the storage to an unmarked target, is the +// ordinary unmarked-direction violation. +template +[[now_uninit]] void destroy_at(T *p); +[[now_uninit]] void nu_wipe(int *p); +[[now_uninit]] void nu_wipe_ref(int &r); +[[now_uninit]] void nu_wipe2(int *p, int *q); + +void test_destroy_at_lifecycle() { + CtorAtPayload s [[uninit]]; + construct_at(&s, 1, 2); + destroy_at(&s); // OK: destroys initialized storage + construct_at(&s, 3, 4); // OK: uninitialized again (the bridge's error above) + int v = s.x; // OK: re-credited + destroy_at(&s); // OK: re-destroy after re-construction + (void)v; +} + +void test_double_destroy() { + int u [[uninit]]; + now_init_fill(&u); + nu_wipe(&u); // OK + nu_wipe(&u); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} + +void test_use_after_destroy_binding() { + int u [[uninit]]; + now_init_fill(&u); + nu_wipe(&u); + int *q = &u; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *r [[ref_to_uninit]] = &u; // OK: uninitialized again + (void)q; (void)r; +} + +// The pointee shapes withdraw like the store-credit forms: destroying +// through the marked pointer clears its pointee credit, so a later read +// through it is the read-through violation again. +void test_destroy_pointee(int *p [[ref_to_uninit]]) { + *p = 5; + int x = *p; // OK: credited + nu_wipe(p); + int y = *p; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + (void)x; (void)y; +} + +// A by-reference [[now_uninit]] parameter destroys its referent; a marked +// reference's referent credit -- which no store can clear -- is withdrawn +// the same way. +void test_destroy_by_reference() { + int u [[uninit]]; + u = 5; + nu_wipe_ref(u); + int *q = &u; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)q; +} +void test_destroy_marked_reference(int &r [[ref_to_uninit]]) { + r = 5; + nu_wipe(&r); + int x = r; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + (void)x; +} + +// Whole-member credit is withdrawn per base object: destroying a1's member +// leaves a2's credit intact. +struct NuMember { int m [[uninit]]; }; +void test_destroy_member_per_object() { + NuMember a1, a2; + a1.m = 5; + a2.m = 5; + nu_wipe(&a1.m); + int *q1 = &a1.m; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *q2 = &a2.m; // OK: a2's credit is untouched + (void)q1; (void)q2; +} + +// A multi-parameter [[now_uninit]] function withdraws every pointer +// argument's credit (the attribute's contract covers all of them). +void test_destroy_multi_param() { + int u [[uninit]], v [[uninit]]; + u = 1; + v = 2; + nu_wipe2(&u, &v); + int *q = &u; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *r = &v; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)q; (void)r; +} + +// A destroy in a never-executed context destroys nothing (mirroring the +// no-credit contexts of the store recorder). +void test_destroy_never_executed() { + int u [[uninit]]; + u = 5; + using X = decltype(nu_wipe(&u)); + int *q = &u; // OK: the unevaluated destroy withdrew nothing + (void)q; +} + +// A suppressed destroy still destroys, exactly as a suppressed store still +// credits: failing to withdraw would turn suppression into later missed +// double-destroy diagnostics. +void test_suppressed_destroy_withdraws() { + int u [[uninit]]; + u = 5; + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] + nu_wipe(&u); + int *q = &u; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)q; +} + +// A destroy through a function pointer presents no ParmVarDecl, so it +// withdraws nothing -- stale credit, a known missed diagnostic (the same +// boundary as [[now_init]] call credit). +void test_destroy_through_function_pointer() { + int u [[uninit]]; + u = 5; + void (*fp)(int *) = nu_wipe; + fp(&u); + int *q = &u; // OK: known gap -- the marker is invisible through the pointer + (void)q; +} + +// A callee carrying both markers is a reinitializer: it destroys and then +// constructs its argument's storage, so the net post-call state is +// initialized (withdrawal is recorded before credit). +[[now_init]] [[now_uninit]] void nu_reinit(int *p [[ref_to_uninit]]); +void test_reinit_nets_initialized() { + int u [[uninit]]; + nu_reinit(&u); + int *q = &u; // OK: net state is initialized + nu_wipe(&u); // OK: destroying initialized storage + (void)q; +} +void test_reinit_reverse_direction() { + int u [[uninit]]; + nu_reinit(&u); + int *r [[ref_to_uninit]] = &u; // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + (void)r; +} + +// The reverse direction applies through the assignment funnel too: a +// credited marked pointer refers to initialized memory, so assigning it to +// another marked pointer is the requires-uninit error -- while an unmarked +// target now accepts it (paper §4.3: "p no longer refers to uninitialized +// memory"). +void test_assign_credited_to_marked(int *p [[ref_to_uninit]], + int *q [[ref_to_uninit]]) { + *p = 5; + q = p; // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} +} +void test_credited_to_unmarked(int *p [[ref_to_uninit]]) { + *p = 5; + int *s = p; // OK: the credited pointee is initialized + (void)s; +} + +// A store through a marked *reference* credits its referent; a reference +// cannot be reseated, so the credit is never cleared. +void test_marked_ref_store_credit(int &r [[ref_to_uninit]]) { + r = 5; + int x = r; // OK: the store initialized the referent + (void)x; +} + +// ...and the reference gets the reverse direction too: after the store its +// referent is initialized, so a marked reference can no longer bind to it. +void test_marked_ref_reverse_direction(int &r [[ref_to_uninit]]) { + r = 5; + int &r3 [[ref_to_uninit]] = r; // expected-error {{reference marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + (void)r3; +} + +void test_marked_ref_read_before_store(int &r [[ref_to_uninit]]) { + int x = r; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + r = 5; + (void)x; +} + +// Store credit is recorded at pattern-parse time too: non-dependent +// store-then-read inside a template is checked at definition time (the +// documented phase-7 trade-off) and must find the pattern-time credit; +// instantiations rebuild every DeclRefExpr against fresh declarations and +// re-record independently. +template +void template_store_then_read(int *p [[ref_to_uninit]]) { + *p = 5; + int x = *p; // OK at definition time and at instantiation + (void)x; +} +template void template_store_then_read(int *); + +// A store in a discarded if-constexpr branch is not instantiated, so the +// rebuilt read finds no credit at that instantiation -- while the pattern's +// store did credit the definition-time check (a dependent condition +// discards nothing at parse). +template +void template_discarded_store(int *p [[ref_to_uninit]]) { + if constexpr (B) + *p = 5; + int x = *p; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + (void)x; +} +template void template_discarded_store(int *); // OK: store instantiated +template void template_discarded_store(int *); // expected-note {{in instantiation of function template specialization 'template_discarded_store' requested here}} + +// Known residual gap (documented definition-time-purity trade-off): a store +// with a *type-dependent RHS* routes through the overloaded-operator path at +// pattern time and never reaches the built-in assignment funnel, so it earns +// no pattern-time credit and the following non-dependent read +// false-positives at definition time. The instantiation is clean (its +// rebuilt store re-records first) -- exactly one error total. +template +void template_dependent_rhs_store(int *p [[ref_to_uninit]], T t) { + *p = t; + int x = *p; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + (void)x; +} +template void template_dependent_rhs_store(int *, int); + +// std::init / ref_to_uninit (paper §5): a pointer/reference member given a +// *written* constructor member-initializer is checked with the enclosing +// constructor as the Decl, so a class-template pattern defers and fires once at +// instantiation, mirroring ctor_uninit_member. Both the parenthesized and the +// braced member-initializer forms reach the same recognizer. +struct CtorMemberPtrBad { + int *p1; + int *p2 [[ref_to_uninit]]; + CtorMemberPtrBad() : p1(&g_uninit), p2(&g_init) {} + // expected-error@-1 {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + // expected-error@-2 {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} +}; + +struct CtorMemberPtrOK { + int *p; + int *q [[ref_to_uninit]]; + CtorMemberPtrOK() : p(&g_init), q(&g_uninit) {} // OK +}; + +struct CtorMemberRefBad { + int &r; + int &s [[ref_to_uninit]]; + CtorMemberRefBad() : r(g_uninit), s(g_init) {} + // expected-error@-1 {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + // expected-error@-2 {{reference marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} +}; + +struct CtorMemberRefOK { + int &r; + int &s [[ref_to_uninit]]; + CtorMemberRefOK() : r(g_init), s(g_uninit) {} // OK +}; + +// A member of an anonymous struct/union reaches the member-initializer site as +// an IndirectFieldDecl; the [[ref_to_uninit]] marking lives on the underlying +// field and is read from there. +struct CtorAnonStructMarkedOK { + struct { + int *p [[ref_to_uninit]]; + }; + CtorAnonStructMarkedOK() : p(&g_uninit) {} // OK: marked target, uninit source +}; + +struct CtorAnonStructMarkedBad { + struct { + int *p [[ref_to_uninit]]; + }; + CtorAnonStructMarkedBad() : p(&g_init) {} // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} +}; + +struct CtorAnonStructUnmarkedBad { + struct { + int *p; + }; + CtorAnonStructUnmarkedBad() : p(&g_uninit) {} // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +}; + +struct CtorAnonUnionMarkedOK { + union { + int *p [[ref_to_uninit]]; + long *q; + }; + CtorAnonUnionMarkedOK() : p(&g_uninit) {} // OK: marked target, uninit source +}; + +// A braced member-initializer is looked through to its single element, exactly +// like the variable-init site. +struct CtorBracedPtr { + int *p; + CtorBracedPtr() : p{&g_uninit} {} // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +}; + +struct CtorBracedRef { + int &r; + CtorBracedRef() : r{g_uninit} {} // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +}; + +// An out-of-line constructor definition is checked where it is defined; the +// enclosing constructor is still the CurContext there. +struct CtorOutOfLine { + int *p; + CtorOutOfLine(); +}; +CtorOutOfLine::CtorOutOfLine() : p(&g_uninit) {} // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + +// Cast and call sources reach the recognizer at the member-init site too: a +// cast of a [[ref_to_uninit]]-returning call propagates the marking. +struct CtorCastSource { + int *p; + CtorCastSource() : p((int *)alloc_void()) {} // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +}; + +struct CtorSuppressWhole { + int *p; + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] CtorSuppressWhole() : p(&g_uninit) {} // OK: suppressed +}; + +struct CtorSuppressRule { + int *p; + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "ref_to_uninit")]] CtorSuppressRule() : p(&g_uninit) {} // OK: suppressed +}; + +template +struct CtorTmpl { + int *p; + CtorTmpl() : p(&g_uninit) {} // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +}; +template struct CtorTmpl; // expected-note {{in instantiation of member function 'CtorTmpl::CtorTmpl' requested here}} + +// A never-instantiated class template stays silent: the deferred check never +// runs on the pattern. +template +struct CtorTmplNever { + int *p; + CtorTmplNever() : p(&g_uninit) {} +}; + +// A written pointer member-initializer that binds to uninitialized memory +// yields exactly one ref_to_uninit error; the member is written, so +// ctor_uninit_member does not also fire. +struct NoDoubleFire { + int *p; + NoDoubleFire() : p(&g_uninit) {} // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +}; + +// A pointer member left uninitialized is not a ref_to_uninit binding (there is +// no written initializer) and yields only the ctor_uninit_member error. +struct UninitMemberOnly { + int *p; // expected-note {{member 'p' declared here}} + UninitMemberOnly() {} // expected-error {{constructor does not initialize member 'p' under profile 'std::init'}} +}; + +// std::init / ref_to_uninit (paper §5): a pointer/reference field initialized +// by an enclosing aggregate's init list is checked Decl-less, scoped to the +// field subobject, so the enclosing variable/argument/return is left to its own +// site (the variable site is not a pointer/reference here) and there is no +// double diagnostic. +struct AggPtr { int *p; }; +struct AggPtrMarked { int *p [[ref_to_uninit]]; }; +struct AggRef { int &r; }; +struct AggRefMarked { int &r [[ref_to_uninit]]; }; + +void test_aggregate_pointer() { + AggPtr a1{&g_uninit}; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + AggPtr a2{&g_init}; // OK + AggPtr a3 = {&g_uninit}; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + AggPtrMarked m1{&g_init}; // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + AggPtrMarked m2{&g_uninit}; // OK + (void)a1; (void)a2; (void)a3; (void)m1; (void)m2; +} + +void test_aggregate_reference() { + AggRef a1{g_uninit}; // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + AggRef a2{g_init}; // OK + AggRefMarked m1{g_init}; // expected-error {{reference marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + AggRefMarked m2{g_uninit}; // OK + (void)a1; (void)a2; (void)m1; (void)m2; +} + +void test_aggregate_designated() { + AggPtr a{.p = &g_uninit}; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + AggPtr b{.p = &g_init}; // OK + (void)a; (void)b; +} + +struct AggNested { AggPtr inner; }; + +void test_aggregate_nested() { + AggNested a{{&g_uninit}}; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + AggNested b{{&g_init}}; // OK + (void)a; (void)b; +} + +// A pointer *element* of an array is a binding position with no declaration to +// carry the marker (like a variadic argument), so it is checked as an unmarked +// target and paper §4.3's rules apply per pointer element. The enclosing array +// variable's own site no-ops (an array is not a pointer/reference), so each +// violating element fires exactly once. +void test_array_element_pointer() { + int *a1[2] = {&g_uninit, &g_init}; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *a2[2] = {&g_init, &g_init}; // OK + int *base [[ref_to_uninit]] = &g_uninit; + int *a3[1] = {base}; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *a4[1] = {allocate(3)}; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + // Null sources stay accepted: {} value-initializes the elements to null and + // nullptr is a null source, consistent with an unmarked target (paper §4.3). + int *a5[2] = {}; // OK + int *a6[2] = {nullptr, &g_init}; // OK + (void)a1; (void)a2; (void)base; (void)a3; (void)a4; (void)a5; (void)a6; +} + +// Parse-order store credit applies to element sources like any other binding: +// after the whole-entity store, &u refers to initialized memory. +void test_array_element_store_credit() { + int u [[uninit]]; + u = 5; + int *a[1] = {&u}; // OK: credited by the store above + (void)a; +} + +// Composition with nested aggregates: an array-of-struct element recurses back +// into the member gate (the field's own marker governs), and a struct's array +// member reaches the element gate. +struct WithPtrArray { int *a[2]; }; + +void test_array_element_nested() { + AggPtr arr1[1] = {{&g_uninit}}; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + AggPtrMarked arr2[1] = {{&g_uninit}}; // OK: the field carries the marker + AggPtrMarked arr3[1] = {{&g_init}}; // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + WithPtrArray w = {{&g_uninit, &g_init}}; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)arr1; (void)arr2; (void)arr3; (void)w; +} + +// A designated array element (a C99 extension in C++) is the same element +// binding. +void test_array_element_designated() { + // expected-warning@+2 {{array designators are a C99 extension}} no-profiles-warning@+2 {{array designators are a C99 extension}} + // expected-error@+1 {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *d[3] = {[1] = &g_uninit}; + (void)d; +} + +// A dependent element source defers on the pattern and is rebuilt at +// instantiation, where the Decl-less check fires per specialization (like +// template_throw_bad). +template +void template_array_element_bad() { + T *a[1] = {(T *)&g_uninit}; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)a; +} +template void template_array_element_bad(); // expected-note {{in instantiation of function template specialization 'template_array_element_bad' requested here}} + +// An aggregate temporary built from an init list is checked the same way +// wherever it appears -- as a call argument, a return value, or a new-expression +// initializer. The enclosing pointer (the parameter, the return type, the +// new-expression result) is not a pointer/reference to the field's storage, so +// only the field binding is diagnosed. +void take_agg_ptr(AggPtr a); +AggPtr make_agg_bad() { return {&g_uninit}; } // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +AggPtr make_agg_ok() { return {&g_init}; } // OK + +void test_aggregate_temporary() { + take_agg_ptr(AggPtr{&g_uninit}); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + take_agg_ptr(AggPtr{&g_init}); // OK + AggPtr *h = new AggPtr{&g_uninit}; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + delete h; +} + +void test_aggregate_suppress() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] AggPtr a{&g_uninit}; // OK: whole-profile suppress + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "ref_to_uninit")]] AggPtr b{&g_uninit}; // OK: rule-targeted suppress + (void)a; (void)b; +} + +// Aggregate field init inside a template body is non-dependent here, so it +// fires at definition time and repeats when the local variable's +// initialization is rebuilt at instantiation; a never-instantiated template +// diagnoses at definition, once. +template +void template_aggregate_bad() { + AggPtr a{&g_uninit}; // expected-error 2 {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)a; +} +template void template_aggregate_bad(); // expected-note {{in instantiation of function template specialization 'template_aggregate_bad' requested here}} + +template +void template_aggregate_never() { + AggPtr a{&g_uninit}; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)a; +} + +// A thrown pointer copy-initializes the exception object, which cannot carry +// [[ref_to_uninit]], so it must not point to uninitialized memory. A read +// like `throw *p` is the read-through check's territory instead. +void throw_ptr_ok() { throw &g_init; } // OK +void throw_ptr_bad() { throw &g_uninit; } // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +void throw_marked_ptr_bad(int *p [[ref_to_uninit]]) { throw p; } // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +void throw_ptr_suppressed() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] { throw &g_uninit; } // OK: suppressed +} + +// A dependent thrown operand defers on the pattern and is rebuilt at +// instantiation, where the check fires per specialization. A fully +// non-dependent throw fires at definition time instead (see +// template_allglobal_bad): TreeTransform reuses it unchanged, so deferring +// would lose the diagnostic. +template +void template_throw_bad() { + throw (T *)&g_uninit; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} +template void template_throw_bad(); // expected-note {{in instantiation of function template specialization 'template_throw_bad' requested here}} + +template +void template_throw_nondependent_bad() { + throw &g_uninit; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} +template void template_throw_nondependent_bad(); + +// C++20 parenthesized aggregate initialization performs the same per-field +// bindings as the braced form and is checked identically. +void test_aggregate_paren() { + AggPtr a1(&g_uninit); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + AggPtr a2(&g_init); // OK + AggPtrMarked m1(&g_init); // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + AggPtrMarked m2(&g_uninit); // OK + AggRef r1(g_uninit); // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + AggRefMarked r2(g_init); // expected-error {{reference marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + AggNested n1((AggPtr(&g_uninit))); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] AggPtr s(&g_uninit); // OK: suppressed + (void)a1; (void)a2; (void)m1; (void)m2; (void)r1; (void)r2; (void)n1; (void)s; +} + +template +void template_aggregate_paren_bad() { + AggPtr a(&g_uninit); // expected-error 2 {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)a; +} +template void template_aggregate_paren_bad(); // expected-note {{in instantiation of function template specialization 'template_aggregate_paren_bad' requested here}} + +// A parenthesized aggregate's array elements are the same unmarked element +// bindings as the braced form; the hook runs only in the build phase, so the +// verify pass adds no second diagnostic. +void test_array_element_paren() { + int *pa[2](&g_uninit, &g_init); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *pb[2](&g_init, nullptr); // OK + (void)pa; (void)pb; +} + +// A plain pointer *variable* with a braced initializer is checked once at its +// own variable site (EK_Variable); the aggregate field hooks are scoped to a +// member subobject, so this fires exactly once with no new duplicate. +void test_variable_braced_once() { + int *p{&g_uninit}; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)p; +} + +// An array new-expression's pointer elements are the same unmarked element +// bindings, for constant and variable bounds alike. The scalar +// new-initializer check stays out of array news -- a lone initializer (a +// one-element braced list, peeled by the single-element pass-through, or a +// single C++20 paren argument) binds the first *element*, already checked by +// the element hooks -- so each violation fires exactly once. Scalar +// allocations keep their single variable-like check. +void test_array_new_element(int n) { + (void)new int *[2]{&g_uninit, &g_init}; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)new int *[2]{&g_uninit}; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)new int *[1](&g_uninit); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)new int *[n]{&g_uninit}; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)new int *[n](&g_uninit); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)new int *[2]{}; // OK: value-initialized null elements + (void)new int *(&g_uninit); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)new int *{&g_uninit}; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} + +// std::init / ref_to_uninit (paper §5): a source whose syntactic form the +// recognizer does not model -- pointer arithmetic, an integer-to-pointer cast, +// or a call through a function pointer -- is unknown, not initialized. A marked +// target binds from such a source without error (it cannot be proven +// initialized, so rejecting it would be a false positive); an unmarked target +// also binds without error (a documented missed diagnostic). Neither run +// diagnoses these. +void test_unknown_pointer_arithmetic() { + int *base [[ref_to_uninit]] = &g_uninit; + int *p1 [[ref_to_uninit]] = base + 1; // OK + int *p2 = base + 1; // OK + (void)p1; (void)p2; +} + +void test_unknown_int_to_ptr(long n) { + int *p1 [[ref_to_uninit]] = reinterpret_cast(n); // OK + int *p2 = reinterpret_cast(n); // OK + (void)p1; (void)p2; +} + +void test_unknown_fnptr_call(int *(*fp)()) { + int *p1 [[ref_to_uninit]] = fp(); // OK + int *p2 = fp(); // OK + (void)p1; (void)p2; +} + +// The unknown classification propagates through the pass-through forms. +void test_unknown_passthrough(bool c) { + int *base [[ref_to_uninit]] = &g_uninit; + int *p1 [[ref_to_uninit]] = c ? base + 1 : &g_init; // OK: an unknown arm keeps the whole unknown + int *p2 [[ref_to_uninit]] = (h(), base + 1); // OK + int *p3 [[ref_to_uninit]] = {base + 1}; // OK + (void)p1; (void)p2; (void)p3; +} + +// The unknown classification reaches the assignment, call-argument, and return +// sites unchanged. +void test_unknown_assignment(long n) { + int *base [[ref_to_uninit]] = &g_uninit; + int *p [[ref_to_uninit]] = &g_uninit; + p = base + 1; // OK + p = reinterpret_cast(n); // OK + int *q = &g_init; + q = base + 1; // OK + (void)p; (void)q; +} + +void test_unknown_call_argument(long n) { + int *base [[ref_to_uninit]] = &g_uninit; + take_uninit_ptr(base + 1); // OK + take_uninit_ptr(reinterpret_cast(n)); // OK + take_ptr(base + 1); // OK +} + +[[ref_to_uninit]] int *ret_unknown_marked() { + int *base [[ref_to_uninit]] = &g_uninit; + return base + 1; // OK +} +int *ret_unknown_unmarked() { + int *base [[ref_to_uninit]] = &g_uninit; + return base + 1; // OK +} + +// Regression guard: the fix narrows only the unknown case. A marked target +// bound from an affirmatively initialized source is still rejected, and an +// unmarked target from an affirmatively uninitialized source is still rejected. +void test_unknown_regression_guard() { + int *m1 [[ref_to_uninit]] = &g_init; // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + int *m2 [[ref_to_uninit]] = new int(5); // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + int *u1 = &g_uninit; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)m1; (void)m2; (void)u1; +} + +// A member call binds its implicit object parameter to the object argument +// (paper §7.2), and that parameter can never carry [[ref_to_uninit]], so a +// call on an object recognized as uninitialized storage is always the +// unmarked-direction violation. Every member-call flavor converts its object +// argument through the same funnel: dot and arrow calls, member operators, +// functor operator(), operator->, and conversion operators. +struct Callee { + int m; + int f() { return m; } + static int sf() { return 0; } + Callee &operator=(const Callee &); + bool operator==(const Callee &) const; + operator int() const; + int *operator->(); + int operator()(int); + int &operator[](int); +}; + +void test_member_call_on_uninit_object() { + Callee s [[uninit]]; + s.f(); // expected-error {{calling member function 'f' binds its implicit object parameter to uninitialized memory under profile 'std::init'}} + (&s)->f(); // expected-error {{calling member function 'f' binds its implicit object parameter to uninitialized memory under profile 'std::init'}} +} + +void test_member_call_through_marked_pointer(Callee *p [[ref_to_uninit]]) { + p->f(); // expected-error {{calling member function 'f' binds its implicit object parameter to uninitialized memory under profile 'std::init'}} + (*p).f(); // expected-error {{calling member function 'f' binds its implicit object parameter to uninitialized memory under profile 'std::init'}} +} + +// Member operators bind the same implicit object parameter, so whole-object +// assignment to an [[uninit]] class object -- previously unchecked through +// the overloaded operator= path -- is caught here too. +void test_member_operators_on_uninit_object() { + Callee s [[uninit]]; + Callee t{}; + s = t; // expected-error {{calling member function 'operator=' binds its implicit object parameter to uninitialized memory under profile 'std::init'}} + (void)(s == t); // expected-error {{calling member function 'operator==' binds its implicit object parameter to uninitialized memory under profile 'std::init'}} + int v = s; // expected-error {{calling member function 'operator int' binds its implicit object parameter to uninitialized memory under profile 'std::init'}} + s(1); // expected-error {{calling member function 'operator()' binds its implicit object parameter to uninitialized memory under profile 'std::init'}} + s[0] = 1; // expected-error {{calling member function 'operator[]' binds its implicit object parameter to uninitialized memory under profile 'std::init'}} + (void)v; +} + +void test_member_arrow_on_uninit_object() { + Callee s [[uninit]]; + (void)*(s.operator->()); // expected-error {{calling member function 'operator->' binds its implicit object parameter to uninitialized memory under profile 'std::init'}} +} + +// A [[ref_to_uninit]]-returning reference function yields an uninitialized +// referent; calling a member function on it is the same violation. +[[ref_to_uninit]] Callee &get_uninit_callee(); +void test_member_call_on_marked_call_result() { + get_uninit_callee().f(); // expected-error {{calling member function 'f' binds its implicit object parameter to uninitialized memory under profile 'std::init'}} +} + +Callee make_callee(); +struct WithDtor { + int m; + void g(); + ~WithDtor(); +}; + +// A static call operator has no implicit object parameter; the object +// argument is evaluated but its value never used, exactly like a static +// member function named through an object. +struct StaticCall { + int m; + static int operator()(int x) { return x; } +}; + +void test_member_call_silent_forms() { + Callee s [[uninit]]; + s.sf(); // OK: a static member uses no object argument + (void)sizeof(s.f()); // OK: unevaluated + using Unevaluated = decltype(s.f()); // OK: unevaluated + (void)Unevaluated{}; + Callee t{}; + t.f(); // OK: initialized object + Callee{}.f(); // OK: a prvalue object is not uninitialized storage + make_callee().f(); // OK: unmarked call result is trusted initialized + WithDtor d [[uninit]]; + d.~WithDtor(); // OK: destruction is the deferred destroy_at slice + StaticCall c [[uninit]]; + c(1); // OK: a static call operator uses no object argument +} + +// A call through a pointer-to-member resolves no method at the call and +// bypasses the object-argument conversion -- the pointer-to-member analog of +// the call-through-function-pointer gap (a known gap, not an endorsement). +void test_member_call_through_pointer_to_member() { + Callee s [[uninit]]; + int (Callee::*pmf)() = &Callee::f; + (s.*pmf)(); // OK: known gap +} + +// The object itself being unmarked keeps the trust decision: a class whose +// *member* is [[uninit]] may still have its member functions called (its +// constructor body may have assigned the member, paper §5.1/§5.2). +struct MemberOnlyUninit { + int m [[uninit]]; + MemberOnlyUninit() { m = 1; } + int get() { return m; } +}; +void test_member_call_unmarked_object_trusted(MemberOnlyUninit &r) { + MemberOnlyUninit o; + o.get(); // OK + r.get(); // OK: unknown-state reference parameter is not affirmatively uninit +} + +// An explicit object member function initializes its object as an ordinary +// parameter, so the existing parameter binding check owns it (and its +// parameter *could* carry the marker). +struct ExplicitObj { + int m; + void f(this ExplicitObj &self); +}; +void test_member_call_explicit_object() { + ExplicitObj x [[uninit]]; + x.f(); // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} + +// A member with enable_if converts availability-check arguments under a +// SFINAE trap; the real call still diagnoses exactly once. +struct WithEnableIf { + int m; + void f() __attribute__((enable_if(true, ""))); +}; +void test_member_call_enable_if() { + WithEnableIf s [[uninit]]; + s.f(); // expected-error {{calling member function 'f' binds its implicit object parameter to uninitialized memory under profile 'std::init'}} +} + +void test_member_call_suppressed() { + Callee s [[uninit]]; + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] { s.f(); } // OK: suppressed + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "ref_to_uninit")]] { s.f(); } // OK +} + +// A dependent object argument defers to instantiation, where the rebuilt call +// re-runs the funnel; a non-dependent call in a template fires at definition +// time and repeats when the call is rebuilt at instantiation (the local is +// remapped) -- the accepted repetition. +template +void template_member_call_dependent_bad() { + T s [[uninit]]; + s.f(); // expected-error {{calling member function 'f' binds its implicit object parameter to uninitialized memory under profile 'std::init'}} +} +template void template_member_call_dependent_bad(); // expected-note {{in instantiation of function template specialization 'template_member_call_dependent_bad' requested here}} + +template +void template_member_call_nondependent_bad() { + Callee s [[uninit]]; + s.f(); // expected-error 2 {{calling member function 'f' binds its implicit object parameter to uninitialized memory under profile 'std::init'}} +} +template void template_member_call_nondependent_bad(); // expected-note {{in instantiation of function template specialization 'template_member_call_nondependent_bad' requested here}} diff --git a/clang/test/SemaCXX/safety-profile-init-static.cpp b/clang/test/SemaCXX/safety-profile-init-static.cpp new file mode 100644 index 0000000000000..6808ca5e8b9bd --- /dev/null +++ b/clang/test/SemaCXX/safety-profile-init-static.cpp @@ -0,0 +1,169 @@ +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 %s +// RUN: %clang_cc1 -fsyntax-only -verify=no-profiles -std=c++23 %s + +// no-profiles-warning@+1 {{'profiles::enforce' attribute ignored}} +[[profiles::enforce(std::init)]]; + +int runtime(); // expected-note {{declared here}} + // no-profiles-note@-1 {{declared here}} +constexpr int compile_time() { return 7; } + +int g_const = 0; +int g_constexpr = compile_time(); +int g_array_const[3] = {1, 2, 3}; +int g_runtime = runtime(); // expected-error {{non-local variable 'g_runtime' requires constant initialization under profile 'std::init'}} +int g_runtime_array[3] = {1, runtime(), 3}; // expected-error {{non-local variable 'g_runtime_array' requires constant initialization under profile 'std::init'}} + +struct Trivial { int x; }; +struct WithDtor { ~WithDtor(); }; +struct WithCtor { WithCtor(); }; + +int g_scalar; +Trivial g_trivial; +Trivial g_trivial_braced = {}; +WithDtor g_with_dtor; +WithCtor g_with_ctor; // expected-error {{non-local variable 'g_with_ctor' requires constant initialization under profile 'std::init'}} + +thread_local int t_ns = runtime(); // OK: thread storage duration, not static + +constinit int g_ci = 0; +// The constinit hard error fires regardless of -fprofiles. +constinit int g_ci_runtime = runtime(); +// expected-error@-1 {{variable does not have a constant initializer}} +// expected-note@-2 {{required by 'constinit' specifier here}} +// expected-note@-3 {{non-constexpr function 'runtime' cannot be used in a constant expression}} +// no-profiles-error@-4 {{variable does not have a constant initializer}} +// no-profiles-note@-5 {{required by 'constinit' specifier here}} +// no-profiles-note@-6 {{non-constexpr function 'runtime' cannot be used in a constant expression}} + +namespace inside { + int n_runtime = runtime(); // expected-error {{non-local variable 'n_runtime' requires constant initialization under profile 'std::init'}} +} + +void test_locals() { + int x = runtime(); + static int s = runtime(); + thread_local int t = runtime(); + (void)x; (void)s; (void)t; +} + +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(std::init, rule: "static_runtime_init")]] +int g_suppressed = runtime(); + +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(std::init)]] +int g_suppressed_all = runtime(); + +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(test::other)]] +int g_wrong_suppress = runtime(); // expected-error {{non-local variable 'g_wrong_suppress' requires constant initialization under profile 'std::init'}} + +// A suppress on the enclosing namespace covers its variables (found by the +// declaration's lexical-parent walk). +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +namespace [[profiles::suppress(std::init)]] suppressed_ns { +int n_ok = runtime(); // OK: suppressed by the namespace-level attribute +} + +// std::init / static_marker: a static or thread-local is zero-initialized by +// language rule, so marking it [[uninit]] is a contradiction (paper section +// 4.2: "an initialized object marked [[uninit]] is an error"). The +// with-initializer case stays uninit_with_initializer's (a real initializer +// already contradicts the marker); this rule covers the zero-initialized, +// no-initializer case. + +namespace std { enum class byte : unsigned char {}; } + +// The paper's section 4.2 'int glob2 [[uninit]]' example, plus the explicit +// 'static' and 'thread_local' spellings. +int g_marked [[uninit]]; // expected-error {{'[[uninit]]' cannot be applied to variable 'g_marked' with static storage duration under profile 'std::init'; it is zero-initialized}} +static int g_static_marked [[uninit]]; // expected-error {{'[[uninit]]' cannot be applied to variable 'g_static_marked' with static storage duration under profile 'std::init'; it is zero-initialized}} +thread_local int g_thread_marked [[uninit]]; // expected-error {{'[[uninit]]' cannot be applied to variable 'g_thread_marked' with thread storage duration under profile 'std::init'; it is zero-initialized}} + +// std::byte fires too: a static std::byte is zero-initialized, unlike an +// automatic one (which uninit_decl exempts because it may be left +// indeterminate). +std::byte g_byte_marked [[uninit]]; // expected-error {{'[[uninit]]' cannot be applied to variable 'g_byte_marked' with static storage duration under profile 'std::init'; it is zero-initialized}} + +// A trivial aggregate at static storage: default-init is a no-op, but the +// object is still zero-initialized, so the marker fires. +Trivial g_agg_marked [[uninit]]; // expected-error {{'[[uninit]]' cannot be applied to variable 'g_agg_marked' with static storage duration under profile 'std::init'; it is zero-initialized}} + +// A static pointer / union is owned by pointer_marker / union_marker (they fire +// regardless of storage duration), not static_marker -- exactly one diagnostic. +union U { int x; float y; }; +static int *g_ptr_marked [[uninit]]; // expected-error {{'[[uninit]]' cannot be applied to a pointer under profile 'std::init'; initialize the pointer (for example to 'nullptr')}} +static U g_union_marked [[uninit]]; // expected-error {{'[[uninit]]' cannot be applied to a variable of union type under profile 'std::init'}} + +// Arrays of pointers / unions key on the base element type: still owned by +// pointer_marker / union_marker, not static_marker (one diagnostic each). +[[uninit]] static int *g_ptr_arr_marked[2]; // expected-error {{'[[uninit]]' cannot be applied to a pointer under profile 'std::init'; initialize the pointer (for example to 'nullptr')}} +[[uninit]] static U g_union_arr_marked[2]; // expected-error {{'[[uninit]]' cannot be applied to a variable of union type under profile 'std::init'}} + +// Unmarked statics / thread-locals are fine (zero-initialized, nothing to mark). +int g_unmarked; +static int g_static_unmarked; +thread_local int g_thread_unmarked; +std::byte g_byte_unmarked; + +void test_local_statics_marked() { + static int s [[uninit]]; // expected-error {{'[[uninit]]' cannot be applied to variable 's' with static storage duration under profile 'std::init'; it is zero-initialized}} + thread_local int t [[uninit]]; // expected-error {{'[[uninit]]' cannot be applied to variable 't' with thread storage duration under profile 'std::init'; it is zero-initialized}} + static int s_ok; + thread_local int t_ok; + // Automatic storage is uninit_decl's domain, not static_marker: the marker + // is the accepted way to leave an automatic variable uninitialized. + int a [[uninit]]; + (void)s; (void)t; (void)s_ok; (void)t_ok; (void)a; +} + +// A static [[uninit]] *with* an initializer is uninit_with_initializer's (R4); +// static_marker must not add a second diagnostic (one error on the line). +int g_marked_with_init [[uninit]] = 0; // expected-error {{variable 'g_marked_with_init' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} + +void test_local_static_running_ctor() { + // The synthesized constructor call is a real initializer, so this stays + // uninit_with_initializer (R4); static_marker stays silent (one error). + static WithCtor w [[uninit]]; // expected-error {{variable 'w' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} + (void)w; +} + +struct MixedAgg { int x; WithCtor s; }; +void test_local_static_mixed() { + // A default-initialization that is not a no-op (a member's user-provided + // constructor runs) is a real initializer too, so the mixed aggregate + // SWITCHES to uninit_with_initializer; static_marker stays silent -- the + // shared vacuity guard keeps the pair complementary (exactly one error). + static MixedAgg m [[uninit]]; // expected-error {{variable 'm' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} + (void)m; +} + +// At namespace scope the same case additionally draws the independent +// static_runtime_init (the member's constructor is a runtime initializer) -- +// a pre-existing pairing, not a static_marker double. +MixedAgg g_mixed_marked [[uninit]]; // expected-error {{variable 'g_mixed_marked' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} \ + // expected-error {{non-local variable 'g_mixed_marked' requires constant initialization under profile 'std::init'}} + +// Suppression: rule-targeted and whole-profile. +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(std::init, rule: "static_marker")]] int g_marker_suppressed_rule [[uninit]]; +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(std::init)]] int g_marker_suppressed_all [[uninit]]; + +// A profile rule fires on the instantiation, not the template pattern: a +// dependent static is diagnosed once, at instantiation. +template +void template_static_marked() { + static T s [[uninit]]; // expected-error {{'[[uninit]]' cannot be applied to variable 's' with static storage duration under profile 'std::init'; it is zero-initialized}} + (void)s; +} +template void template_static_marked(); // expected-note {{in instantiation of function template specialization 'template_static_marked' requested here}} + +// An uninstantiated template pattern is not yet a phase-7 entity, so no rule +// fires on it (no expected diagnostic here). +template +void template_static_never_instantiated() { + static T s [[uninit]]; + (void)s; +} diff --git a/clang/test/SemaCXX/safety-profile-init-system-header-exempt.cpp b/clang/test/SemaCXX/safety-profile-init-system-header-exempt.cpp new file mode 100644 index 0000000000000..d19e1beb6a386 --- /dev/null +++ b/clang/test/SemaCXX/safety-profile-init-system-header-exempt.cpp @@ -0,0 +1,36 @@ +// Temporary system-header exemption for profile enforcement (stopgap for the +// not-yet-implemented [[profiles::exempt]], P3589R2 s1.1.6). By default a +// violation originating in a system header is exempt; a violation in a normal +// (-I) header is still enforced. -fno-profiles-exempt-system-headers enforces +// profiles in system-header code too. + +// RUN: rm -rf %t +// RUN: split-file %s %t +// +// Default: exemption on -- only the -I header's violation fires. +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 -I %t %t/main.cpp +// +// Exemption off: both the system-header and the -I header violations fire. +// RUN: %clang_cc1 -fsyntax-only -verify=expected,strict -fno-profiles-exempt-system-headers -fprofiles -std=c++23 -I %t %t/main.cpp + +//--- user.h +// A normal header reached via -I is not a system header, so it is enforced in +// both runs. +inline void user_fn() { + int y; // expected-error {{variable 'y' must be initialized or marked '[[uninit]]' under profile 'std::init'}} + (void)y; +} + +//--- sys.h +#pragma clang system_header +// This is a system header: exempt by default, enforced only with +// -fno-profiles-exempt-system-headers. +inline void sys_fn() { + int x; // strict-error {{variable 'x' must be initialized or marked '[[uninit]]' under profile 'std::init'}} + (void)x; +} + +//--- main.cpp +[[profiles::enforce(std::init)]]; +#include "user.h" +#include "sys.h" diff --git a/clang/test/SemaCXX/safety-profile-init-union.cpp b/clang/test/SemaCXX/safety-profile-init-union.cpp new file mode 100644 index 0000000000000..ad19baaaef47c --- /dev/null +++ b/clang/test/SemaCXX/safety-profile-init-union.cpp @@ -0,0 +1,138 @@ +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 %s +// RUN: %clang_cc1 -fsyntax-only -verify=no-profiles -std=c++23 %s + +// no-profiles-warning@+1 {{'profiles::enforce' attribute ignored}} +[[profiles::enforce(std::init)]]; + +union U { int x; float y; }; + +U g_union [[uninit]]; // expected-error {{'[[uninit]]' cannot be applied to a variable of union type under profile 'std::init'}} + +void test_union_var() { + U a [[uninit]]; // expected-error {{'[[uninit]]' cannot be applied to a variable of union type under profile 'std::init'}} + (void)a; +} + +void test_union_var_suppressed() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] U a [[uninit]]; + (void)a; +} + +union MarkedMember { + int x [[uninit]]; // expected-error {{'[[uninit]]' cannot be applied to a union member under profile 'std::init'}} + float y; +}; + +// The marker checks key on the base element type: an array of unions is +// banned exactly like a single union object (paper section 5.6). +[[uninit]] U g_union_arr[2]; // expected-error {{'[[uninit]]' cannot be applied to a variable of union type under profile 'std::init'}} + +void test_union_array_var() { + [[uninit]] U a[2]; // expected-error {{'[[uninit]]' cannot be applied to a variable of union type under profile 'std::init'}} + [[uninit]] U b[2][3]; // expected-error {{'[[uninit]]' cannot be applied to a variable of union type under profile 'std::init'}} + (void)a; (void)b; +} + +// A union-typed data member of a non-union class cannot carry the marker +// either: delayed initialization by assigning one of its members would be +// just as erroneous there (paper section 5.6). +struct HasMarkedUnionMember { + U u [[uninit]]; // expected-error {{'[[uninit]]' cannot be applied to a data member of union type under profile 'std::init'}} + [[uninit]] U arr[2]; // expected-error {{'[[uninit]]' cannot be applied to a data member of union type under profile 'std::init'}} +}; + +// A marker on a union member of a non-enforced profile is silently accepted; +// exercised by the no-profiles run above. + +// A non-union class member may carry the marker (it is not banned here). +struct NotUnion { + int x [[uninit]]; +}; + +union WithNSDMI { int x = 0; float y; }; +// Defining a union constructor must not fire ctor_uninit_member for the other +// members (they are mutually exclusive). +union WithUserCtor { int x; float y; WithUserCtor() : x(0) {} }; + +void test_uninit_union_object() { + U a; // expected-error {{variable 'a' of union type must be initialized under profile 'std::init'}} + U b = {1}; + U c{}; + WithNSDMI d; // OK: a default member initializer initializes a member + WithUserCtor e; // OK: a user-provided default constructor is trusted + (void)a; (void)b; (void)c; (void)d; (void)e; +} + +void test_uninit_union_suppressed() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] U a; + (void)a; +} + +// A union whose only members are unnamed bit-fields has nothing that +// default-initialization could leave indeterminate -- unnamed bit-fields are +// not members and no in-language initializer exists for them -- but an +// unnamed bit-field alongside a real member changes nothing. +union BitFieldOnly { int : 4; }; +union BitFieldPlusMember { int : 4; int i; }; +void test_bitfield_only_union() { + BitFieldOnly a; // OK: nothing to initialize + BitFieldPlusMember b; // expected-error {{variable 'b' of union type must be initialized under profile 'std::init'}} + (void)a; (void)b; +} + +// A union of only std::byte members (arrays included) mirrors the scalar +// std::byte exemption (paper §4.5): whichever member is active may be left +// uninitialized, so a plain declaration is accepted. A non-byte member +// alongside keeps the union flagged. +namespace std { enum class byte : unsigned char {}; } +union AllByte { std::byte b; std::byte arr[4]; }; +union MixedByte { std::byte b; int i; }; +void test_byte_union() { + AllByte a; // OK: std::byte may be left uninitialized + MixedByte b; // expected-error {{variable 'b' of union type must be initialized under profile 'std::init'}} + (void)a; (void)b; +} + +// Default member initializers on the leaves of an anonymous-struct variant +// activate it during default-initialization: an all-NSDMI variant is +// initialized completely, a partial one leaves its other leaf indeterminate. +union NSDMIVariant { struct { int a = 1; int b = 2; }; float f; }; +union PartialNSDMIVariant { struct { int a = 1; int b; }; float f; }; +void test_nsdmi_variant_union() { + NSDMIVariant a; // OK: default-init activates the complete variant + PartialNSDMIVariant b; // expected-error {{variable 'b' of union type must be initialized under profile 'std::init'}} + (void)a; (void)b; +} + +// A union data member that a constructor leaves uninitialized is diagnosed; one +// initialized via its member-initializer is accepted. +struct HasUnionMember { + U u; // expected-note {{member 'u' declared here}} + int z; + HasUnionMember() : z(0) {} // expected-error {{constructor does not initialize member 'u' under profile 'std::init'}} + HasUnionMember(int) : u{1}, z(0) {} +}; + +// A dependent local that substitutes to a union type is deferred on the pattern +// and fires union_marker at instantiation, not on the template. +template +void template_union_marker() { + T x [[uninit]]; // #template-union-marker + (void)x; +} +template void template_union_marker(); // expected-note {{in instantiation of function template specialization 'template_union_marker' requested here}} +// expected-error@#template-union-marker {{'[[uninit]]' cannot be applied to a variable of union type under profile 'std::init'}} + +// A dependent local that substitutes to an *array of* unions fires the same +// way (base element type) at instantiation. +template void template_union_marker(); // expected-note {{in instantiation of function template specialization 'template_union_marker' requested here}} +// expected-error@#template-union-marker {{'[[uninit]]' cannot be applied to a variable of union type under profile 'std::init'}} + +// An uninstantiated pattern never reaches phase 7, so the marker is silent. +template +void template_union_never_instantiated() { + T x [[uninit]]; + (void)x; +} diff --git a/clang/test/SemaCXX/safety-profile-init-with-initializer.cpp b/clang/test/SemaCXX/safety-profile-init-with-initializer.cpp new file mode 100644 index 0000000000000..2723039380029 --- /dev/null +++ b/clang/test/SemaCXX/safety-profile-init-with-initializer.cpp @@ -0,0 +1,168 @@ +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 %s +// RUN: %clang_cc1 -fsyntax-only -verify=no-profiles -std=c++23 %s + +// no-profiles-warning@+1 {{'profiles::enforce' attribute ignored}} +[[profiles::enforce(std::init)]]; + +void test_postfix_eq() { + int c [[uninit]] = 0; // expected-error {{variable 'c' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} + (void)c; +} + +void test_prefix_eq() { + [[uninit]] int d = 7; // expected-error {{variable 'd' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} + (void)d; +} + +void test_brace_init() { + int e [[uninit]] {}; // expected-error {{variable 'e' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} + (void)e; +} + +void test_paren_init() { + int f [[uninit]] (3); // expected-error {{variable 'f' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} + (void)f; +} + +void test_marker_alone() { + int x [[uninit]]; + (void)x; +} + +void test_init_alone() { + int x = 0; + (void)x; +} + +struct WithCtor { WithCtor(); }; + +void test_class_synthesized_init() { + // The implicit default-constructor call is the initializer, so the marker + // contradicts it even though there is no explicit initializer. + WithCtor x [[uninit]]; // expected-error {{variable 'x' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} + [[uninit]] WithCtor y; // expected-error {{variable 'y' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} + (void)x; (void)y; +} + +struct MarkedMemberAgg { int x [[uninit]]; }; + +void test_marked_member_agg() { + // R4 stays factual: MarkedMemberAgg's default-initialization is a genuine + // no-op that leaves x indeterminate, so marking the variable too is + // consistent rather than a contradiction. (Honoring the member marker here + // would wrongly fire uninit_with_initializer.) + MarkedMemberAgg a [[uninit]]; + (void)a; +} + +struct NoDefaultCtor { NoDefaultCtor() = delete; }; // expected-note {{'NoDefaultCtor' has been explicitly marked deleted here}} \ + // no-profiles-note {{'NoDefaultCtor' has been explicitly marked deleted here}} + +void test_failed_init_no_double_diag() { + // Default-init fails and installs a RecoveryExpr placeholder. That is not a + // user-written initializer, so R4 must not pile a spurious diagnostic on top + // of the real error (the absence of an extra '...have an initializer...' + // diagnostic here is the assertion). + NoDefaultCtor z [[uninit]]; // expected-error {{call to deleted constructor of 'NoDefaultCtor'}} \ + // no-profiles-error {{call to deleted constructor of 'NoDefaultCtor'}} + (void)z; +} + +int g_marker_with_init [[uninit]] = 42; // expected-error {{variable 'g_marker_with_init' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} + +void test_suppress() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "uninit_with_initializer")]] + int x [[uninit]] = 0; + (void)x; +} + +void test_suppress_block() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] { + int a [[uninit]] = 0; + int b [[uninit]] = 1; + (void)a; (void)b; + } +} + +// [[profiles::suppress]] on a data member covers the uninit_with_initializer +// check that runs when its NSDMI is finalized, not just its parsing. +struct WithSuppressedNSDMI { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "uninit_with_initializer")]] int m [[uninit]] = 0; // OK: rule-targeted suppress + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] int n [[uninit]] = 1; // OK: whole-profile suppress +}; + +// A suppress on the enclosing record covers its members' NSDMIs. +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +struct [[profiles::suppress(std::init)]] WithClassLevelSuppressedNSDMI { + int m [[uninit]] = 0; // OK: suppressed by the class-level attribute +}; + +// A profile rule fires on the instantiation, not on the template pattern, so a +// dependent [[uninit]]-with-initializer is diagnosed exactly once. +template +void template_marker_with_init() { + T x [[uninit]] = T{}; // expected-error {{variable 'x' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} + (void)x; +} +template void template_marker_with_init(); // expected-note {{in instantiation of function template specialization 'template_marker_with_init' requested here}} + +// A *non-dependent* declaration inside a template body is likewise diagnosed +// once -- at instantiation -- not on the pattern. +template +void template_nondependent_with_init() { + int x [[uninit]] = 0; // expected-error {{variable 'x' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} + (void)x; +} +template void template_nondependent_with_init(); // expected-note {{in instantiation of function template specialization 'template_nondependent_with_init' requested here}} + +// An uninstantiated template pattern is not yet a phase-7 entity, so no profile +// rule fires on it (no expected diagnostic here). +template +void template_never_instantiated() { + int x [[uninit]] = 0; + (void)x; +} + +// Default-initialization that is not a genuine no-op contradicts the marker +// exactly like a written initializer (paper §4.2 rule 2, §5.3): something is +// initialized, so the object is not left uninitialized. +struct MixedInner { MixedInner(); }; +struct Mixed { int x; MixedInner s; }; +struct WithNSDMIMember { int a; int b = 0; }; +struct Polymorphic { virtual void f(); int x; }; +struct TrivialAgg { int x; }; + +void test_default_init_not_noop() { + Mixed s4 [[uninit]]; // expected-error {{variable 's4' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} + WithNSDMIMember q [[uninit]]; // expected-error {{variable 'q' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} + Polymorphic v [[uninit]]; // expected-error {{variable 'v' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} + [[uninit]] Mixed arr[2]; // expected-error {{variable 'arr' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} + TrivialAgg t [[uninit]]; // OK: a genuine no-op, 't.x' really is left + // indeterminate + (void)s4; (void)q; (void)v; (void)arr; (void)t; +} + +// A `= P()` value-initialization zeroes the object -- an initialization the +// marker contradicts; the `= P{}` list form was already rejected (pinned +// here), and the two must not diverge. +struct P { int a; int b; }; +void test_value_init() { + P p [[uninit]] = P(); // expected-error {{variable 'p' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} + P p2 [[uninit]] = P{}; // expected-error {{variable 'p2' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} + (void)p; (void)p2; +} + +// An NSDMI in a class template is checked once, when its initializer is +// instantiated (here forced by initializing the specialization). +template +struct WithTemplatedNSDMI { + T m [[uninit]] = T{}; // expected-error {{member 'm' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} +}; +void use_templated_nsdmi() { + WithTemplatedNSDMI w = {}; // expected-note {{in instantiation of default member initializer 'WithTemplatedNSDMI::m' requested here}} + (void)w; +} diff --git a/clang/test/SemaCXX/safety-profile-init-write.cpp b/clang/test/SemaCXX/safety-profile-init-write.cpp new file mode 100644 index 0000000000000..4743d87b46547 --- /dev/null +++ b/clang/test/SemaCXX/safety-profile-init-write.cpp @@ -0,0 +1,380 @@ +// RUN: %clang_cc1 -fsyntax-only -verify -fprofiles -std=c++23 %s +// RUN: %clang_cc1 -fsyntax-only -verify=no-profiles -std=c++23 %s + +// std::init / uninit_write (paper §5.4-§5.6): a scalar store to a proper +// subobject of a named [[uninit]] entity is banned delayed initialization -- +// only writing the whole named entity initializes it (paper §4.5), and only +// whole-object construct_at could make a piecemeal-initialized object good. + +// no-profiles-warning@+1 {{'profiles::enforce' attribute ignored}} +[[profiles::enforce(std::init)]]; + +namespace std { enum class byte : unsigned char {}; } + +struct Pair { int x; int y; }; + +bool cond(); + +// A store to the whole named entity is its initialization (paper §4.5), for +// the marked local itself and for a directly-targeted marked member. +void test_whole_entity_store_is_initialization() { + int x [[uninit]]; + x = 7; // OK: the write initializes x + (void)x; +} + +struct WithMarkedMember { int m [[uninit]]; }; +void test_direct_marked_member_store(WithMarkedMember &o) { + o.m = 1; // OK: the store targets the marked member itself +} + +// The §5.2 constructor-body pattern is untouched: a current-object member +// store reaches the member through `this` (a pointer), in every spelling. +struct CtorBody { + int m [[uninit]]; + CtorBody() { + m = 1; // OK + this->m = 2; // OK + (*this).m = 3; // OK + } +}; + +// Member stores below a named [[uninit]] object (paper §5.4). +void test_member_store(bool c) { + Pair s [[uninit]]; + s.x = 1; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} + (&s)->x = 1; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} + Pair t [[uninit]]; + (c ? s : t).x = 1; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} + + WithMarkedMember b [[uninit]]; + b.m = 1; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} + + Pair u; // expected-error {{variable 'u' must be initialized or marked '[[uninit]]' under profile 'std::init'}} + u.x = 1; // OK: 'u' is not marked; its diagnostic is uninit_decl's, at the declaration +} + +// A marked *member* below the top level bans the store the same way, on a +// named object or on the current object -- a class-type member has no legal +// piecemeal path (only whole-object construct_at, which is unmodeled). +struct HasAgg { Pair agg [[uninit]]; }; +void test_nested_marked_member_store(HasAgg &h) { + h.agg.x = 1; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} +} +struct SelfAgg { + Pair agg [[uninit]]; + void set() { + agg.x = 1; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} + } +}; + +// Element stores of [[uninit]] arrays are the §5.5 random-access-init ban. +void test_element_store(int i) { + [[uninit]] int a[2]; + a[0] = 1; // expected-error {{writing an element of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} + a[i] = 1; // expected-error {{writing an element of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} + *a = 1; // expected-error {{writing an element of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} + [[uninit]] int m2[2][2]; + m2[1][0] = 1; // expected-error {{writing an element of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} +} + +// A marked array *member*'s elements are proper subobjects of the marked +// array, so element stores are banned even on the current object. +struct WithArrMember { + [[uninit]] int a[2]; + void set() { + a[0] = 1; // expected-error {{writing an element of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} + } +}; +void test_member_array_element_store(WithArrMember &w) { + w.a[0] = 1; // expected-error {{writing an element of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} +} + +// Member assignment to a marker-retaining union is the §5.6 ban (the marker +// itself is union_marker's; here it is suppressed and retained). +union U { int x; float y; }; +void test_union_member_store() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] U u [[uninit]]; + u.x = 9; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} +} + +// Compound assignments and built-in increments store like plain assignment, +// and every one of them also reads the old value: the shift forms load +// through their LHS promotion (DefaultLvalueConversion), the rest are checked +// at the operator sites, so each fires the read-through diagnostic exactly +// once alongside the write. +void test_compound_and_incdec_stores() { + Pair s [[uninit]]; + s.x += 1; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} \ + // expected-error {{read of a subobject of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} + s.x <<= 1; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} \ + // expected-error {{read of a subobject of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} + ++s.x; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} \ + // expected-error {{read of a subobject of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} + s.x--; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} \ + // expected-error {{read of a subobject of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} + [[uninit]] int a[2]; + --a[0]; // expected-error {{writing an element of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} \ + // expected-error {{read of a subobject of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} +} + +// Writes through a [[ref_to_uninit]] pointer or reference: for a built-in +// type the write is the pointee's initialization (paper §4.5), so they are +// legal -- and a whole-`*p` store credits the pointee as initialized in +// parse order, so the compound assignment below may read the value it wrote. +// An element store (p[3]) neither credits nor invalidates (§5.4/§5.5). A +// compound assignment through a still-uncredited marker reads uninitialized +// memory (full coverage in safety-profile-init-ref-to-uninit.cpp). +[[ref_to_uninit]] int &get_uninit_ref(); +void test_write_through_ref_to_uninit(int *p [[ref_to_uninit]], + int &r [[ref_to_uninit]], + Pair *ptr [[ref_to_uninit]]) { + *p = 5; // OK (and credits p's pointee) + p[3] = 0; // OK (no credit, no invalidation) + *p += 1; // OK: the whole-*p store above credited the pointee + r = 5; // OK + ptr->x = 5; // OK + get_uninit_ref() = 5; // OK +} + +// std::byte may be left uninitialized and manipulated freely (paper §4.5). +void test_byte_exempt() { + [[uninit]] std::byte b[2]; + b[0] = std::byte{1}; // OK +} + +// The §5.4 sanctioned route for whole-object (re)initialization: a +// [[ref_to_uninit]]-taking construct_at, whose parameter binding accepts &s. +template +T *construct_at(T *p [[ref_to_uninit]], Args &&...args); + +void test_construct_at_pattern() { + Pair s [[uninit]]; + construct_at(&s, 1, 2); // OK: the marked parameter accepts &s + // The address escape earns no store credit -- paper §6.2 reserves + // callee-initialization for now_init(); only whole-entity stores credit + // (stores-only policy) -- so the subobject write below stays an error. + s.x = 1; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} +} + +// Suppression: whole-profile and rule-targeted, on statements and on the +// enclosing declaration; a different rule does not cover the store. +void test_suppress_stmt() { + Pair s [[uninit]]; + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] { s.x = 1; } // OK + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "uninit_write")]] { s.y = 2; } // OK + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "uninit_read")]] { + s.x = 3; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} + } +} + +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(std::init, rule: "uninit_write")]] +void test_suppress_decl() { + Pair s [[uninit]]; + s.x = 1; // OK: the function-level suppression covers the body +} + +// Like every Decl-less expression check, the store check fires at definition +// time when its target is non-dependent, and repeats when the local s is +// remapped and the assignment rebuilt at instantiation -- the accepted +// repetition. A dependent target (template_write_dependent_bad) defers on the +// pattern and fires once per violating specialization. +template +void template_write_bad() { + Pair s [[uninit]]; + s.x = 1; // expected-error 2 {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} +} +template void template_write_bad(); // expected-note {{in instantiation of function template specialization 'template_write_bad' requested here}} + +template +void template_write_dependent_bad() { + T s [[uninit]]; + s.x = 1; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} +} +template void template_write_dependent_bad(); // expected-note {{in instantiation of function template specialization 'template_write_dependent_bad' requested here}} + +// A never-instantiated pattern diagnoses its non-dependent store at +// definition time, exactly once. +template +void template_write_never_instantiated() { + Pair s [[uninit]]; + s.x = 1; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} +} + +// A literal-false if-constexpr condition makes the then-branch a discarded +// statement context already at pattern parse, so its store stays silent; the +// live else-branch fires at definition time and repeats when the branch is +// rebuilt at instantiation. +template +void template_write_discarded_branch() { + Pair s [[uninit]]; + if constexpr (false) { + s.x = 1; + } else { + s.x = 2; // expected-error 2 {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} + } +} +template void template_write_discarded_branch(); // expected-note {{in instantiation of function template specialization 'template_write_discarded_branch' requested here}} + +// An all-global store is *reused* by TreeTransform at instantiation (its +// Build* never re-runs), so deferring would silently lose the diagnostic; it +// is checked at definition time -- exactly one error, with no instantiation +// note, whether or not the template is ever instantiated. +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(std::init, rule: "static_marker")]] [[uninit]] Pair g_uninit_pair; + +template +void template_allglobal_write_bad() { + g_uninit_pair.x = 1; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} +} +template void template_allglobal_write_bad(); + +template +void template_allglobal_write_never_instantiated() { + g_uninit_pair.x = 1; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} +} + +// A store that violates two rules at one location -- the subobject write into +// the [[uninit]] object and the unmarked-pointer binding of its member -- +// fires both at definition time, and both repeat on the instantiation +// rebuild. +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(std::init, rule: "static_marker")]] [[uninit]] int g_uninit_int; +struct WithPtrMember { int x; int *p; }; + +template +void template_two_rules_bad() { + WithPtrMember s [[uninit]]; + s.p = &g_uninit_int; // expected-error 2 {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} \ + // expected-error 2 {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} +template void template_two_rules_bad(); // expected-note {{in instantiation of function template specialization 'template_two_rules_bad' requested here}} + +template +void template_two_rules_never_instantiated() { + WithPtrMember s [[uninit]]; + s.p = &g_uninit_int; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} \ + // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} + +// Parse-order whole-entity store credit (paper §4.2/§4.5): assigning the +// whole [[uninit]] entity is its initialization, so bindings after the store +// (in parse order) treat it as initialized -- and the paper's reverse +// direction applies: the credited entity now REQUIRES an unmarked target +// (§4.2's `p4 = &x` error). Purely parse-order, no flow analysis: a store +// under a condition credits everything after it (§1.2 "consider all branches +// executed" -- the untaken path is a missed diagnostic, never a false +// positive). +void take_int_ptr(int *q); +void test_whole_store_credit() { + int u [[uninit]]; + u = 5; + int *q = &u; // OK: u is initialized (rejected before the store) + take_int_ptr(&u); // OK + int &br = u; // OK + int *r [[ref_to_uninit]] = &u; // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + (void)q; (void)br; (void)r; +} + +void test_binding_before_store() { + int u [[uninit]]; + int *q = &u; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + u = 5; + (void)q; +} + +void test_conditional_store_credit(bool c) { + int u [[uninit]]; + if (c) + u = 5; + int *q = &u; // OK: parse-order credit (§1.2); the untaken path is an + // accepted missed diagnostic + (void)q; +} + +// Compound assignment and ++/-- store, so they credit the whole entity -- +// while their own old-value read keeps the flow-based read-before-init +// error (recorded after the pre-store checks: no self-crediting). +void test_compound_store_credit() { + int u [[uninit]]; // expected-note {{variable 'u' is declared here}} + u += 1; // expected-error {{variable 'u' is read before initialization under profile 'std::init'}} + int *q = &u; // OK: the compound store credited u + int v [[uninit]]; // expected-note {{variable 'v' is declared here}} + ++v; // expected-error {{variable 'v' is read before initialization under profile 'std::init'}} + int *qv = &v; // OK + int w [[uninit]]; // expected-note {{variable 'w' is declared here}} + w = w + 1; // expected-error {{variable 'w' is read before initialization under profile 'std::init'}} + (void)q; (void)qv; +} + +// Class-typed whole-object assignment never credits: it resolves to a member +// operator= -- already rejected as a call on uninitialized storage -- and +// never reaches the built-in assignment funnel (crediting an erroneous +// statement would misstate the object's state). The class remedy remains +// construct_at through [[ref_to_uninit]] (paper §4.5; unmodeled slice). +void test_class_store_never_credits() { + Pair s [[uninit]]; + s = Pair{1, 2}; // expected-error {{calling member function 'operator=' binds its implicit object parameter to uninitialized memory under profile 'std::init'}} + s.x = 3; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} + int y = s.x; // expected-error {{read of a subobject of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} + (void)y; +} + +// A store in an unevaluated or discarded context never executes, so it earns +// no credit. +void test_no_credit_contexts() { + int u [[uninit]]; + (void)sizeof(u = 5); // expected-warning {{expression with side effects has no effect in an unevaluated context}} \ + // no-profiles-warning {{expression with side effects has no effect in an unevaluated context}} + int *q = &u; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int v [[uninit]]; + using TT = decltype((v = 5)); // expected-warning {{expression with side effects has no effect in an unevaluated context}} \ + // no-profiles-warning {{expression with side effects has no effect in an unevaluated context}} + int *qv = &v; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int w [[uninit]]; + if constexpr (false) { w = 5; } + int *qw = &w; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)q; (void)qv; (void)qw; +} + +// A suppressed store still initializes: the credit is recorded regardless +// of [[profiles::suppress]], so suppression cannot manufacture later false +// positives. +void test_suppressed_store_credits() { + int u [[uninit]]; + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] { u = 5; } + int *q = &u; // OK: the suppressed store still credited u + (void)q; +} + +// A nested assignment credits both targets: the inner assignment completes +// (and records) before the outer one. +void test_nested_assignment_credit() { + int u [[uninit]]; + int v [[uninit]]; + u = (v = 5); + int *qu = &u; // OK + int *qv = &v; // OK + (void)qu; (void)qv; +} + +// The credit keys on the unique VarDecl: a same-named sibling-scope local is +// a distinct declaration, so credit does not leak between them. +void test_sibling_scope_credit(bool c) { + if (c) { + int u [[uninit]]; + u = 5; + int *q = &u; // OK: this u is credited + (void)q; + } else { + int u [[uninit]]; + int *q = &u; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)q; + } +} diff --git a/clang/test/SemaCXX/safety-profile-now-init-marker.cpp b/clang/test/SemaCXX/safety-profile-now-init-marker.cpp new file mode 100644 index 0000000000000..29ab9b9c5477e --- /dev/null +++ b/clang/test/SemaCXX/safety-profile-now-init-marker.cpp @@ -0,0 +1,66 @@ +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 %s +// RUN: %clang_cc1 -fsyntax-only -verify=no-profiles -std=c++23 %s + +// The [[now_init]] marker (P4222R2 §6.2; its exact placement and spelling +// track an open committee question) is recognized regardless of -fprofiles. +// It applies to functions with at least one [[ref_to_uninit]] parameter -- +// the parameters whose storage the callee promises to initialize; with no +// profile enforced it has no effect on those valid subjects. Other +// placements, and a function with no marked parameter (a vacuous promise), +// are rejected regardless of -fprofiles. + +int g; + +[[now_init]] void fill(int *p [[ref_to_uninit]]); +[[now_init]] void fill_ref(int &r [[ref_to_uninit]]); +void fill_after_name [[now_init]] (int *p [[ref_to_uninit]]); +[[now_init]] void fill_many(int n, int *p [[ref_to_uninit]], int *q); +[[now_init]] int *fill_and_return(int *p [[ref_to_uninit]]); + +struct S { + [[now_init]] void fill_member(int *p [[ref_to_uninit]]); + [[now_init]] void fill_out_of_line(int *p [[ref_to_uninit]]); +}; +void S::fill_out_of_line(int *p [[ref_to_uninit]]) {} + +template +[[now_init]] void fill_template(T *p [[ref_to_uninit]]); + +// A dependent parameter's marker is attached to the pattern unvalidated, so +// the vacuity check accepts the template; if instantiation drops the marker +// (T deduced such that T p is not a pointer/reference), the inherited +// [[now_init]] is inert rather than re-diagnosed, like the dropped marker. +template +[[now_init]] void fill_dependent(T p [[ref_to_uninit]]) {} +template void fill_dependent(int *); +template void fill_dependent(int); + +int bad_var [[now_init]]; // expected-error {{'now_init' attribute only applies to functions}} \ + // no-profiles-error {{'now_init' attribute only applies to functions}} + +struct BadField { + int m [[now_init]]; // expected-error {{'now_init' attribute only applies to functions}} \ + // no-profiles-error {{'now_init' attribute only applies to functions}} +}; + +[[now_init]] void bad_no_params(); // expected-error {{'now_init' attribute requires at least one parameter marked '[[ref_to_uninit]]'}} \ + // no-profiles-error {{'now_init' attribute requires at least one parameter marked '[[ref_to_uninit]]'}} + +[[now_init]] void bad_unmarked_params(int *p, int &r); // expected-error {{'now_init' attribute requires at least one parameter marked '[[ref_to_uninit]]'}} \ + // no-profiles-error {{'now_init' attribute requires at least one parameter marked '[[ref_to_uninit]]'}} + +// The [[ref_to_uninit]] on a non-pointer parameter is itself rejected and +// dropped, leaving the function with no marked parameter: the vacuity error +// fires alongside. +[[now_init]] void bad_dropped_marker(int v [[ref_to_uninit]]); // expected-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} \ + // expected-error {{'now_init' attribute requires at least one parameter marked '[[ref_to_uninit]]'}} \ + // no-profiles-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} \ + // no-profiles-error {{'now_init' attribute requires at least one parameter marked '[[ref_to_uninit]]'}} + +// Inert without an enforced profile: a valid placement changes nothing about +// how either run type-checks these calls. +void use() { + int x = 0; + fill(&x); + fill_ref(x); +} diff --git a/clang/test/SemaCXX/safety-profile-now-uninit-marker.cpp b/clang/test/SemaCXX/safety-profile-now-uninit-marker.cpp new file mode 100644 index 0000000000000..50597a732e981 --- /dev/null +++ b/clang/test/SemaCXX/safety-profile-now-uninit-marker.cpp @@ -0,0 +1,67 @@ +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 %s +// RUN: %clang_cc1 -fsyntax-only -verify=no-profiles -std=c++23 %s + +// The [[now_uninit]] marker -- the mirror of [[now_init]], supplying the +// recording P4222R2 §4.4 notes is missing for destroy_at -- is recognized +// regardless of -fprofiles. It applies to functions with at least one +// parameter of pointer or reference type -- the parameters whose storage the +// callee destroys; with no profile enforced it has no effect on those valid +// subjects. Other placements, and a function with no pointer or reference +// parameter (a vacuous claim), are rejected regardless of -fprofiles. + +[[now_uninit]] void wipe(int *p); +[[now_uninit]] void wipe_ref(int &r); +[[now_uninit]] void wipe_void(void *p); +void wipe_after_name [[now_uninit]] (int *p); +[[now_uninit]] void wipe_many(int n, int *p, bool b); + +// Carrying both attributes is allowed: a reinitializer both ends and starts +// a lifetime for its argument's storage. +[[now_init]] [[now_uninit]] void reinit(int *p [[ref_to_uninit]]); + +struct S { + [[now_uninit]] void wipe_member(int *p); + [[now_uninit]] void wipe_out_of_line(int *p); +}; +void S::wipe_out_of_line(int *p) {} + +template +[[now_uninit]] void wipe_template(T *p) {} + +// A dependent parameter type may instantiate to a pointer or reference, so +// the vacuity check accepts the template; if it does not, the inherited +// attribute goes inert rather than re-diagnosed (the withdrawal arms only +// key on pointer/reference bindings). +template +[[now_uninit]] void wipe_dependent(T v) {} +template void wipe_dependent(int *); +template void wipe_dependent(int); + +int bad_var [[now_uninit]]; // expected-error {{'now_uninit' attribute only applies to functions}} \ + // no-profiles-error {{'now_uninit' attribute only applies to functions}} + +struct BadField { + int m [[now_uninit]]; // expected-error {{'now_uninit' attribute only applies to functions}} \ + // no-profiles-error {{'now_uninit' attribute only applies to functions}} +}; + +[[now_uninit]] void bad_no_params(); // expected-error {{'now_uninit' attribute requires at least one parameter of pointer or reference type}} \ + // no-profiles-error {{'now_uninit' attribute requires at least one parameter of pointer or reference type}} + +[[now_uninit]] void bad_value_params(int v, bool b); // expected-error {{'now_uninit' attribute requires at least one parameter of pointer or reference type}} \ + // no-profiles-error {{'now_uninit' attribute requires at least one parameter of pointer or reference type}} + +// A function pointer denotes a function, never destroyable storage +// (mirroring [[ref_to_uninit]]'s subject rule), so it does not satisfy the +// vacuity check. +[[now_uninit]] void bad_fn_ptr_param(void (*fp)()); // expected-error {{'now_uninit' attribute requires at least one parameter of pointer or reference type}} \ + // no-profiles-error {{'now_uninit' attribute requires at least one parameter of pointer or reference type}} + +// Inert without an enforced profile: a valid placement changes nothing about +// how either run type-checks these calls. +void use() { + int x = 0; + wipe(&x); + wipe_ref(x); + wipe_template(&x); +} diff --git a/clang/test/SemaCXX/safety-profile-ref-to-uninit-marker.cpp b/clang/test/SemaCXX/safety-profile-ref-to-uninit-marker.cpp new file mode 100644 index 0000000000000..0d490967d7b5c --- /dev/null +++ b/clang/test/SemaCXX/safety-profile-ref-to-uninit-marker.cpp @@ -0,0 +1,94 @@ +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 %s +// RUN: %clang_cc1 -fsyntax-only -verify=no-profiles -std=c++23 %s + +// The [[ref_to_uninit]] marker is recognized regardless of -fprofiles. It +// applies to pointers, references, and pointer/reference-returning functions; +// with no profile enforced it has no effect on those valid subjects. Other +// placements are rejected regardless of -fprofiles. + +int g; + +int *gp [[ref_to_uninit]] = &g; +int &gr [[ref_to_uninit]] = g; +void *gvp [[ref_to_uninit]] = &g; +[[ref_to_uninit]] int *gp_prefix = &g; + +[[ref_to_uninit]] int *allocate(int n); +[[ref_to_uninit]] int &bind_ret(); +void fill(int *p [[ref_to_uninit]]); +void bind(int &r [[ref_to_uninit]]); + +struct S { + int *m [[ref_to_uninit]]; +}; + +int bad_scalar [[ref_to_uninit]]; // expected-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} \ + // no-profiles-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} + +[[ref_to_uninit]] int bad_array[3]; // expected-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} \ + // no-profiles-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} + +[[ref_to_uninit]] void bad_return(); // expected-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} \ + // no-profiles-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} + +struct BadMember { + int m [[ref_to_uninit]]; // expected-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} \ + // no-profiles-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} +}; + +// A function pointer or reference (or a function returning one) denotes a +// function, never uninitialized memory, so the marker can never be satisfied +// and is rejected -- like a pointer-to-member, which is not a pointer type. +void some_fn(); + +void (*bad_fn_ptr [[ref_to_uninit]])(); // expected-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} \ + // no-profiles-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} + +void (&bad_fn_ref [[ref_to_uninit]])() = some_fn; // expected-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} \ + // no-profiles-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} + +[[ref_to_uninit]] void (*bad_ret_fn_ptr())(); // expected-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} \ + // no-profiles-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} + +struct C { void mf(); }; +void (C::*bad_mem_fn_ptr [[ref_to_uninit]])(); // expected-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} \ + // no-profiles-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} + +struct BadFnPtrMember { + void (*m [[ref_to_uninit]])(); // expected-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} \ + // no-profiles-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} +}; + +// A dependent subject is not validated on the template pattern; the check +// runs at instantiation, once the substituted type is known, and the marker +// is dropped when it is invalid there. +template struct DependentMember { + T m [[ref_to_uninit]]; // expected-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} \ + // no-profiles-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} +}; +template struct DependentMember; +template struct DependentMember; // expected-note {{in instantiation of template class 'DependentMember' requested here}} \ + // no-profiles-note {{in instantiation of template class 'DependentMember' requested here}} + +// A dependent *parameter* is substituted during template argument deduction, +// inside the SFINAE trap. Diagnosing there would let the marker affect +// overload resolution, so an invalid parameter marker is instead dropped +// silently: no diagnostic, and the dropped marker is inert (the ref_to_uninit +// rule never consults a marker on a non-pointer/reference parameter). +template void dependent_param(T p [[ref_to_uninit]]) {} +template void dependent_param(int *); +template void dependent_param(int); + +template [[ref_to_uninit]] T dependent_return() { return T{}; } // expected-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} \ + // no-profiles-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} +template int *dependent_return(); +template int dependent_return(); // expected-note {{in instantiation of function template specialization 'dependent_return' requested here}} \ + // no-profiles-note {{in instantiation of function template specialization 'dependent_return' requested here}} + +template void dependent_local() { + T v [[ref_to_uninit]]; // expected-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} \ + // no-profiles-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} +} +template void dependent_local(); +template void dependent_local(); // expected-note {{in instantiation of function template specialization 'dependent_local' requested here}} \ + // no-profiles-note {{in instantiation of function template specialization 'dependent_local' requested here}} diff --git a/clang/test/SemaCXX/safety-profile-type-cast.cpp b/clang/test/SemaCXX/safety-profile-type-cast.cpp new file mode 100644 index 0000000000000..c7b3a685eddd2 --- /dev/null +++ b/clang/test/SemaCXX/safety-profile-type-cast.cpp @@ -0,0 +1,874 @@ +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -fprofiles-test-profiles -std=c++23 %s +// RUN: %clang_cc1 -fsyntax-only -verify=no-profiles -std=c++23 %s +// RUN: %clang_cc1 -fsyntax-only -verify=no-test -fprofiles -std=c++23 %s + +// Under -fprofiles without -fprofiles-test-profiles the built-in test:: profiles +// are inert: the attributes are recognized (no "ignored" warnings) but no rule +// fires anywhere in this file. +// no-test-no-diagnostics + +// no-profiles-warning@+1 {{'profiles::enforce' attribute ignored}} +[[profiles::enforce(test::type_cast)]]; +// no-profiles-warning@+1 {{'profiles::enforce' attribute ignored}} +[[profiles::enforce(test::other)]]; + +void test_violation() { + int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} +} + +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(test::type_cast)]] +void test_suppress_decl() { + int *p = reinterpret_cast(0); +} + +void test_suppress_stmt() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]] { + int *p = reinterpret_cast(0); + } + int *q = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} +} + +void test_suppress_stmt_with_rule() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast, rule: "reinterpret_cast")]] { + int *p = reinterpret_cast(0); + } +} + +void test_suppress_stmt_with_bare_rule() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast, rule: reinterpret_cast)]] { + int *p = reinterpret_cast(0); + } +} + +void test_suppress_stmt_with_nonmatching_rule() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast, rule: "static_cast")]] { + int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} + } +} + +// P3589R2 [decl.attr.enforce]p2: static semantics applied after translation +// phase 7 -- no diagnostic in template definition, only at instantiation. +template +void template_cast(T x) { + auto *p = reinterpret_cast(x); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} +} +void instantiate() { + template_cast(0); // expected-note {{in instantiation of function template specialization 'template_cast' requested here}} +} + +// P3589R2 Section 1.1: profile violations must not affect overload resolution. +// If the profile error in the decltype SFINAE'd out the first overload, the +// fallback (returning 1) would be selected and the static_assert would fire. +template +auto sfinae_cast(T x) -> decltype(reinterpret_cast(x)) { + return nullptr; +} +template +auto sfinae_cast(...) -> int { return 1; } + +static_assert(__is_same(decltype(sfinae_cast(0L)), int *), + "profile violation must not SFINAE out the first overload"); + +// Profile violations are suppressed in unevaluated contexts. +void test_unevaluated() { + using T = decltype(reinterpret_cast(0)); +} + +// Suppress on TU-scope variable initializer (pull model via push in ParseDeclGroup). +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(test::type_cast)]] +int *tu_scope_var = reinterpret_cast(0); + +// Suppress on block-scope variable initializer. +void test_suppress_var_init() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]] int *p = reinterpret_cast(0); +} + +// Profile violations are suppressed in discarded if-constexpr branches. +void test_discarded_branch() { + if constexpr (false) { + int *p = reinterpret_cast(0); + } +} + +// Lambda inside enforced scope. +void test_lambda() { + auto f = []() { + int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} + }; +} + +// Nested suppression with correct save/restore. +void test_nested_suppress() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]] { + int *p = reinterpret_cast(0); + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]] { + int *q = reinterpret_cast(0); + } + int *r = reinterpret_cast(0); + } + int *s = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} +} + +// sizeof is an unevaluated context. +void test_sizeof_unevaluated() { + auto s = sizeof(reinterpret_cast(0)); +} + +// noexcept is an unevaluated context. +void test_noexcept_unevaluated() { + bool b = noexcept(reinterpret_cast(0)); +} + +// Requires-expression is an unevaluated context. +void test_requires_unevaluated() { + bool b = requires { reinterpret_cast(0); }; +} + +// Default function argument with violation. +void default_arg_func(int *p = reinterpret_cast(0)); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} + +// Suppress with justification works identically to suppress without. +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(test::type_cast, justification: "legacy code")]] +void test_suppress_with_justification() { + int *p = reinterpret_cast(0); +} + +// Suppress on various statement kinds. +void test_suppress_on_stmts() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]] + for (int *p = reinterpret_cast(0);;) break; + + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]] + while (reinterpret_cast(0)) break; + + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]] + if (reinterpret_cast(0)) {} + + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]] + (void)reinterpret_cast(0); +} + +// Suppress on null-statement is a no-op: the next statement is NOT suppressed. +void test_suppress_null_stmt() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]]; + int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} +} + +// Suppress on class definition: member functions are suppressed via DeclAttr. +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +struct [[profiles::suppress(test::type_cast)]] SuppressedStruct { + void f() { + int *p = reinterpret_cast(0); + } +}; + +// Suppress on namespace: functions inside are suppressed via DeclAttr. +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +namespace [[profiles::suppress(test::type_cast)]] suppressed_ns { + void g() { + int *p = reinterpret_cast(0); + } +} + +// Selective suppression: suppressing a different profile does not +// suppress test::type_cast violations. +void test_selective_suppress() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::other)]] { + int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} + } +} + +// Suppress on compound statement inside template: suppression must be +// effective during instantiation, not just during parsing. +template +void template_suppress_stmt(T x) { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]] { + auto *p = reinterpret_cast(x); + } +} +void instantiate_suppress_stmt() { template_suppress_stmt(0); } + +// Suppress on variable declaration inside template. +template +void template_suppress_var(T x) { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]] auto *p = reinterpret_cast(x); +} +void instantiate_suppress_var() { template_suppress_var(0); } + +// Suppress ends at statement boundary inside template. +template +void template_suppress_boundary(T x) { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]] { + auto *p = reinterpret_cast(x); + } + auto *q = reinterpret_cast(x); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} +} +void instantiate_suppress_boundary() { + template_suppress_boundary(0); // expected-note {{in instantiation of function template specialization 'template_suppress_boundary' requested here}} +} + +// Suppress on forward declaration does not propagate to definition. +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(test::type_cast)]] +void suppress_fwd_only(); + +void suppress_fwd_only() { + int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} +} + +// Suppress on field NSDMI in non-template class: suppression must be +// effective during late-parsing of the in-class initializer. +struct FieldSuppressNonTemplate { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]] int *p = reinterpret_cast(0); +}; + +// Suppress on field NSDMI in class template: suppression must carry through +// when the in-class initializer is instantiated. +template +struct FieldSuppressTemplate { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]] T *p = reinterpret_cast(0); +}; +FieldSuppressTemplate field_suppress_inst; + +// Without suppress on field, the NSDMI violation fires during instantiation. +template +struct FieldNoSuppressTemplate { // #FieldNoSuppressTemplate + T *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} \ + // expected-note@#FieldNoSuppressTemplate {{in instantiation of default member initializer 'FieldNoSuppressTemplate::p' requested here}} +}; +FieldNoSuppressTemplate field_no_suppress_inst; // expected-note {{in evaluation of exception specification for 'FieldNoSuppressTemplate::FieldNoSuppressTemplate' needed here}} + +// Profile violations fire in constexpr functions. Use a guarded path so the +// function can still produce a constant expression (avoiding the unrelated +// "constexpr function never produces a constant expression" error). +constexpr int *constexpr_cast(bool b) { + if (b) + return reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} + return nullptr; +} + +// Profile violations fire in consteval functions. +consteval int *consteval_cast(bool b) { + if (b) + return reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} + return nullptr; +} + +// Suppress works inside constexpr functions. +constexpr int *constexpr_suppress_cast(bool b) { + if (b) { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]] return reinterpret_cast(0); + } + return nullptr; +} + +// Trailing declarator-position suppress on TU-scope variable initializer. +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +int *trailing_tu_var [[profiles::suppress(test::type_cast)]] = reinterpret_cast(0); + +// Trailing declarator-position suppress on block-scope variable initializer. +void test_trailing_suppress_block() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + int *p [[profiles::suppress(test::type_cast)]] = reinterpret_cast(0); + int *q = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} +} + +// Suppress on class template definition: member functions should be suppressed +// during instantiation via DeclContext walk. +template +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +struct [[profiles::suppress(test::type_cast)]] SuppressedClassTemplate { + void f() { + T *p = reinterpret_cast(0); + } +}; +SuppressedClassTemplate suppressed_class_tmpl_inst; + +// Without suppress on class template: member violation fires during instantiation. +template +struct UnsuppressedClassTemplate { + void f() { + T *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} + } +}; +void instantiate_unsuppressed_class_tmpl() { + UnsuppressedClassTemplate x; + x.f(); // expected-note {{in instantiation of member function 'UnsuppressedClassTemplate::f' requested here}} +} + +// Suppress on variable template: suppression must carry through instantiation. +template +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(test::type_cast)]] T *var_tmpl_suppress = reinterpret_cast(0); +template int *var_tmpl_suppress; + +// Suppress on static data member template: suppression must carry through +// instantiation even when the suppress is only on the variable, not the class. +template +struct StaticMemberSuppressTemplate { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]] static T *p; +}; +template +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(test::type_cast)]] T *StaticMemberSuppressTemplate::p = reinterpret_cast(0); +template struct StaticMemberSuppressTemplate; + +// Out-of-line member function of a suppressed class: suppression does NOT +// extend past the class body. +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +struct [[profiles::suppress(test::type_cast)]] OutOfLineSuppressedClass { + void inline_ok() { + int *p = reinterpret_cast(0); + } + void out_of_line(); + static int *s; +}; + +void OutOfLineSuppressedClass::out_of_line() { + int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} +} + +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(test::type_cast)]] int *OutOfLineSuppressedClass::s = reinterpret_cast(0); + +// Out-of-line function in a formerly-suppressed namespace: suppression does +// NOT extend past the namespace body. +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +namespace [[profiles::suppress(test::type_cast)]] OutOfLineSuppressedNS { + void inline_ok() { + int *p = reinterpret_cast(0); + } + void out_of_line(); +} + +void OutOfLineSuppressedNS::out_of_line() { + int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} +} + +// Out-of-line member of a suppressed class template. +template +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +struct [[profiles::suppress(test::type_cast)]] OutOfLineSuppressedClassTemplate { + void inline_ok() { + T *p = reinterpret_cast(0); + } + void out_of_line(); +}; + +template +void OutOfLineSuppressedClassTemplate::out_of_line() { + T *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} +} + +template struct OutOfLineSuppressedClassTemplate; // expected-note {{in instantiation of member function 'OutOfLineSuppressedClassTemplate::out_of_line' requested here}} + +// Nested suppress: class template inside a suppressed outer class. +// Suppression on Outer must propagate to inline members of Inner during +// instantiation via the lexical parent chain. +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +struct [[profiles::suppress(test::type_cast)]] NestedSuppressOuter { + template + struct Inner { + void f() { T *p = reinterpret_cast(0); } + void out_of_line(); + }; +}; + +template +void NestedSuppressOuter::Inner::out_of_line() { + T *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} +} + +template struct NestedSuppressOuter::Inner; // expected-note {{in instantiation of member function 'NestedSuppressOuter::Inner::out_of_line' requested here}} + +// Nested suppress: class template inside a suppressed namespace. +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +namespace [[profiles::suppress(test::type_cast)]] NestedSuppressNS { + template + struct Inner { + void f() { T *p = reinterpret_cast(0); } + void out_of_line(); + }; +} + +template +void NestedSuppressNS::Inner::out_of_line() { + T *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} +} + +template struct NestedSuppressNS::Inner; // expected-note {{in instantiation of member function 'NestedSuppressNS::Inner::out_of_line' requested here}} + +// NSDMI in a suppressed class template: suppression applies via the lexical +// parent chain during default member initializer instantiation. +template +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +struct [[profiles::suppress(test::type_cast)]] SuppressedNSDMI { + T *p = reinterpret_cast(0); +}; +SuppressedNSDMI suppressed_nsdmi_inst; + +// Inline static data member in a suppressed class template. +template +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +struct [[profiles::suppress(test::type_cast)]] SuppressedInlineStatic { + static inline T *s = reinterpret_cast(0); +}; +template struct SuppressedInlineStatic; + +// Generic lambda defined inside a suppress block, returned, and instantiated +// outside -- the suppress must carry through instantiation. +auto get_suppressed_generic_lambda() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]] { + auto l = [](auto x) { return reinterpret_cast(x); }; + return l; + } +} +void test_generic_lambda_suppress_propagation() { + auto l = get_suppressed_generic_lambda(); + l(0); +} + +// Generic lambda inside a suppress with rule restriction: only the matching +// rule is suppressed. +auto get_rule_restricted_lambda() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast, rule: "reinterpret_cast")]] { + auto l = [](auto x) { return reinterpret_cast(x); }; + return l; + } +} +void test_generic_lambda_rule_suppress() { + auto l = get_rule_restricted_lambda(); + l(0); +} + +// Generic lambda without suppress: the violation still fires during +// instantiation. +auto get_unsuppressed_generic_lambda() { + auto l = [](auto x) { + return reinterpret_cast(x); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} + }; + return l; +} +void test_generic_lambda_no_suppress() { + auto l = get_unsuppressed_generic_lambda(); + l(0); // expected-note {{in instantiation of function template specialization 'get_unsuppressed_generic_lambda()::(lambda)::operator()' requested here}} +} + +// Non-generic lambda inside suppress: still works (regression check). +void test_nongeneric_lambda_suppress() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]] { + auto l = []() { return reinterpret_cast(0); }; + l(); + } +} + +// Suppress of a different profile does not propagate to the generic lambda. +auto get_wrong_profile_lambda() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::other)]] { + auto l = [](auto x) { + return reinterpret_cast(x); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} + }; + return l; + } +} +void test_wrong_profile_lambda() { + auto l = get_wrong_profile_lambda(); + l(0); // expected-note {{in instantiation of function template specialization 'get_wrong_profile_lambda()::(lambda)::operator()' requested here}} +} + +// Direct suppress on a non-generic lambda's declarator applies to its body. +// The attribute precedes the parameter list so it appertains to the call +// operator declaration (P2173; C++23 standard). +void test_nongeneric_lambda_direct_suppress() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + auto l = [] [[profiles::suppress(test::type_cast)]] () { + int *p = reinterpret_cast(0); + }; + l(); +} + +// Direct suppress of a non-matching profile on a non-generic lambda does not +// suppress the violation. +void test_nongeneric_lambda_direct_suppress_mismatch() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + auto l = [] [[profiles::suppress(test::other)]] () { + int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} + }; + l(); +} + +// Rule-based direct suppress on a non-generic lambda applies when the rule +// matches. +void test_nongeneric_lambda_direct_suppress_rule() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + auto l = [] [[profiles::suppress(test::type_cast, rule: "reinterpret_cast")]] () { + int *p = reinterpret_cast(0); + }; + l(); +} + +// Rule-based direct suppress on a non-generic lambda does not suppress a +// non-matching rule. +void test_nongeneric_lambda_direct_suppress_wrong_rule() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + auto l = [] [[profiles::suppress(test::type_cast, rule: "static_cast")]] () { + int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} + }; + l(); +} + +// Suppress on an inline member function definition applies to the body. +struct InlineMethodSuppress { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]] + void f() { + int *p = reinterpret_cast(0); + } +}; + +// Suppress on an inline member function with a non-matching profile does not +// suppress the violation. +struct InlineMethodWrongProfile { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::other)]] + void f() { + int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} + } +}; + +// Suppress on an inline constructor applies to the member initializer list. +struct InlineCtorMemInitSuppress { + int *p; + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]] + InlineCtorMemInitSuppress() : p(reinterpret_cast(0)) {} +}; + +// Suppress on an inline destructor applies to the destructor body. +struct InlineDtorSuppress { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]] + ~InlineDtorSuppress() { + int *p = reinterpret_cast(0); + } +}; + +// Suppress on an inline conversion operator applies to its body. +struct InlineConversionSuppress { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]] + operator int *() { + return reinterpret_cast(0); + } +}; + +// Suppress on an inline operator overload applies to its body. +struct InlineOperatorSuppress { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]] + int *operator+() { + return reinterpret_cast(0); + } +}; + +// Suppress on a late-parsed default argument of an inline member function. +struct InlineDefaultArgSuppress { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]] + void f(int *p = reinterpret_cast(0)); +}; + +// Without per-method suppress, the violation still fires in an inline body. +struct InlineMethodNoSuppress { + void f() { + int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} + } +}; + +// Per-method suppress on a member function template with a non-dependent +// violation in the body: exercises getAsFunction() on a FunctionTemplateDecl. +struct InlineMemberTemplateSuppress { + template + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]] + void f() { + int *p = reinterpret_cast(0); + } +}; +void instantiate_inline_member_template_suppress() { + InlineMemberTemplateSuppress s; + s.f(); +} + +// Suppress on a static inline data member applies to its in-class initializer. +struct StaticInlineDataMemberSuppress { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]] + static inline int *p = reinterpret_cast(0); +}; + +// Without per-member suppress, a static inline data member initializer fires. +struct StaticInlineDataMemberNoSuppress { + static inline int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} +}; + +// Suppress on a static data member initializer with a non-matching profile +// does not suppress the violation. +struct StaticInlineDataMemberWrongProfile { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::other)]] + static inline int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} +}; + +// Suppress on a nested (non-template) class must reach its late-parsed inline +// method bodies, NSDMIs, and default arguments even though the nested class's +// body is parsed before the outer class's members are late-parsed. +struct NestedSuppressInnerOuter { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + struct [[profiles::suppress(test::type_cast)]] Inner { + void f() { + int *p = reinterpret_cast(0); + } + int *p = reinterpret_cast(0); + void g(int *q = reinterpret_cast(0)); + }; +}; + +// Non-matching profile on the nested class does not suppress the violation. +struct NestedSuppressWrongProfile { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + struct [[profiles::suppress(test::other)]] Inner { + void f() { + int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} + } + int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} + void g(int *q = reinterpret_cast(0)); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} + }; +}; + +// Suppress on the outer class extends to inline bodies / NSDMIs / default +// args of a nested non-template class. +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +struct [[profiles::suppress(test::type_cast)]] OuterSuppressNested { + struct Inner { + void f() { + int *p = reinterpret_cast(0); + } + int *p = reinterpret_cast(0); + void g(int *q = reinterpret_cast(0)); + }; +}; + +// Suppress on an enclosing namespace reaches a nested non-template class. +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +namespace [[profiles::suppress(test::type_cast)]] nested_suppress_ns { + struct Inner { + void f() { + int *p = reinterpret_cast(0); + } + int *p = reinterpret_cast(0); + void g(int *q = reinterpret_cast(0)); + }; +} + +// Deeply nested: suppress on the middle class reaches the innermost class's +// inline body. +struct DeepOuter { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + struct [[profiles::suppress(test::type_cast)]] DeepMiddle { + struct DeepInner { + void f() { + int *p = reinterpret_cast(0); + } + int *p = reinterpret_cast(0); + void g(int *q = reinterpret_cast(0)); + }; + }; +}; + +// A [[profiles::suppress]] live at the point of instantiation covers the +// trigger's tokens, not the pattern's (P3589R2 s2.4p3, token-based dominion): +// parse-time checks in synchronously instantiated code must not be +// suppressed by the caller's scope. + +int instantiation_leak_target = 0; + +// Function-template body instantiated from a suppressed initializer. +template +auto instantiation_leak_fn() { + return reinterpret_cast(&instantiation_leak_target); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} +} +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(test::type_cast)]] auto *leak_fn_use = instantiation_leak_fn(); // expected-note {{in instantiation of function template specialization 'instantiation_leak_fn' requested here}} + +// NSDMI of an unrelated class template instantiated from a suppressed +// declaration. +template +struct LeakNSDMI { // #LeakNSDMI + T *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} \ + // expected-note@#LeakNSDMI {{in instantiation of default member initializer 'LeakNSDMI::p' requested here}} +}; +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(test::type_cast)]] LeakNSDMI leak_nsdmi_use; // expected-note {{in evaluation of exception specification for 'LeakNSDMI::LeakNSDMI' needed here}} + +// Variable-template initializer. +template +auto *leak_vt = reinterpret_cast(&instantiation_leak_target); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(test::type_cast)]] int *leak_vt_use = leak_vt; // expected-note {{in instantiation of variable template specialization 'leak_vt' requested here}} + +// Default argument instantiated at a suppressed call site. +template +int *leak_def(T *q = reinterpret_cast(&instantiation_leak_target)) { return q; } // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(test::type_cast)]] int *leak_def_use = leak_def(); // expected-note {{in instantiation of default function argument expression for 'leak_def' required here}} + +// A closure created during the instantiation must not absorb the caller's +// suppress either. +template +auto leak_lambda() { + auto l = [](auto x) { return reinterpret_cast(x); }; // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} + return l(0L); // expected-note {{in instantiation of function template specialization 'leak_lambda()::(lambda)::operator()' requested here}} +} +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(test::type_cast)]] auto *leak_lambda_use = leak_lambda(); // expected-note {{in instantiation of function template specialization 'leak_lambda' requested here}} + +// A suppressed *statement* inside a pattern does not reach a lexically +// unrelated pattern instantiated under it: GapLeaked's tokens precede the +// suppressed block, so its NSDMI check still fires. +template +struct GapLeaked { // #GapLeaked + T *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} \ + // expected-note@#GapLeaked {{in instantiation of default member initializer 'GapLeaked::p' requested here}} +}; +template +void gap_user() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]] { + GapLeaked v; // expected-note {{in evaluation of exception specification for 'GapLeaked::GapLeaked' needed here}} + (void)v; + } +} +template void gap_user(); // expected-note {{in instantiation of function template specialization 'gap_user' requested here}} + +// Contrast: a local class *defined inside* the suppressed block is within the +// dominion, so its NSDMI stays suppressed when instantiated under it. +template +void local_class_in_suppressed_block() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]] { + struct Local { T *p = reinterpret_cast(0); }; + Local v; + (void)v; + } +} +template void local_class_in_suppressed_block(); + +// Dominion matching compares raw TU token order, so macro-emitted code +// behaves by its expansion position: a violation spelled in a macro argument +// of the suppressed declaration is within the dominion; a pattern's +// macro-emitted violation stays outside it. +#define EMIT_CAST(ty, x) reinterpret_cast(x) +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(test::type_cast)]] int *macro_arg_suppressed = EMIT_CAST(int, &instantiation_leak_target); +template +auto leak_macro_fn() { return EMIT_CAST(T, &instantiation_leak_target); } // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(test::type_cast)]] int *leak_macro_use = leak_macro_fn(); // expected-note {{in instantiation of function template specialization 'leak_macro_fn' requested here}} + +// The dominion's end is bounded by the construct's recorded end location: a +// live suppress scope does not cover a pattern first declared or defined +// *after* the suppressed construct, even though those tokens compare after +// the dominion's begin. + +// Fwd-declared class template defined after the suppressed pattern. +template struct FwdLeak; +template +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(test::type_cast)]] void fwd_leak_user() { + FwdLeak v; // expected-note {{in evaluation of exception specification for 'FwdLeak::FwdLeak' needed here}} + (void)v; +} +template +struct FwdLeak { // #FwdLeak + T *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} \ + // expected-note@#FwdLeak {{in instantiation of default member initializer 'FwdLeak::p' requested here}} +}; +template void fwd_leak_user(); // expected-note {{in instantiation of function template specialization 'fwd_leak_user' requested here}} + +// Statement-entry variant: the suppressed block's recorded end excludes the +// later-defined pattern. +template struct FwdLeak2; +template void fwd_gap_user() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]] { + FwdLeak2 v; // expected-note {{in evaluation of exception specification for 'FwdLeak2::FwdLeak2' needed here}} + (void)v; + } +} +template +struct FwdLeak2 { // #FwdLeak2 + T *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} \ + // expected-note@#FwdLeak2 {{in instantiation of default member initializer 'FwdLeak2::p' requested here}} +}; +template void fwd_gap_user(); // expected-note {{in instantiation of function template specialization 'fwd_gap_user' requested here}} + +// TagDecl arm: the suppressed class template's brace range bounds its parent +// entry, excluding the later-defined pattern its member instantiates. +template struct FwdLeak3; +template +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +struct [[profiles::suppress(test::type_cast)]] FwdSupClass { + static void m() { + FwdLeak3 v; // expected-note {{in evaluation of exception specification for 'FwdLeak3::FwdLeak3' needed here}} + (void)v; + } +}; +template +struct FwdLeak3 { // #FwdLeak3 + T *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} \ + // expected-note@#FwdLeak3 {{in instantiation of default member initializer 'FwdLeak3::p' requested here}} +}; +void fwd_sup_use() { FwdSupClass::m(); } // expected-note {{in instantiation of member function 'FwdSupClass::m' requested here}} + +// A construct still being parsed records no end -- the scope's lifetime +// bounds the dominion, which is exact mid-parse. A class-level suppress +// therefore still covers members instantiated from a static member's +// in-class initializer, both synchronously mid-parse (the generic lambda's +// call operator) and deferred (the member function template). +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +struct [[profiles::suppress(test::type_cast)]] MidParseSync { + static inline int *x = [](auto t) { return reinterpret_cast(t); }(0); +}; +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +struct [[profiles::suppress(test::type_cast)]] MidParseDeferred { + template static T *f() { return reinterpret_cast(0); } + static inline int *x = f(); +}; diff --git a/clang/test/SemaCXX/safety-profile-uninit-read.cpp b/clang/test/SemaCXX/safety-profile-uninit-read.cpp new file mode 100644 index 0000000000000..a6b8e4f48aa9b --- /dev/null +++ b/clang/test/SemaCXX/safety-profile-uninit-read.cpp @@ -0,0 +1,140 @@ +// All violations share one TU with a leading unrelated error: the early error +// disables the analysis-based-warnings pass for later functions, so this also +// verifies that an enforced CFG-uninit profile keeps diagnosing afterwards. +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -fprofiles-test-profiles -std=c++23 -Wno-uninitialized %s +// RUN: %clang_cc1 -fsyntax-only -verify=no-profiles -std=c++23 -Wno-uninitialized %s + +// no-profiles-warning@+1 {{'profiles::enforce' attribute ignored}} +[[profiles::enforce(test::uninit_read)]]; +// no-profiles-warning@+1 {{'profiles::enforce' attribute ignored}} +[[profiles::enforce(test::other)]]; + +int leading_unrelated_error = undeclared_identifier; +// expected-error@-1 {{use of undeclared identifier 'undeclared_identifier'}} +// no-profiles-error@-2 {{use of undeclared identifier 'undeclared_identifier'}} + +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(test::uninit_read)]] +void test_suppress_decl() { + int x; + int y = x; + (void)y; +} + +void test_suppress_stmt_inner() { + int x; + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::uninit_read)]] { + int y = x; + (void)y; + } +} + +void test_suppress_var_init() { + int x; + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::uninit_read)]] int y = x; + (void)y; +} + +void test_discarded_branch() { + int x; + if constexpr (false) { + int y = x; + (void)y; + } +} + +void test_unevaluated() { + int x; + using T = decltype(x); + (void)sizeof(x); + (void)static_cast(0); +} + +void test_param(int p) { + int y = p; + (void)y; +} + +int g_static; +void test_global() { + int y = g_static; + (void)y; +} + +// A self-init reads the uninitialized variable in its own initializer; the +// profile reports it at the root cause even when there is no later use. +void test_self_init_no_use() { + int x = x; // expected-error {{variable 'x' is read before initialization under profile 'test::uninit_read'}} \ + // expected-note {{variable 'x' is declared here}} + (void)&x; +} + +void test_suppress_self_init() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::uninit_read)]] int x = x; + (void)&x; +} + +void test_violation() { + int x; // expected-note {{variable 'x' is declared here}} + int y = x; // expected-error {{variable 'x' is read before initialization under profile 'test::uninit_read'}} + (void)y; +} + +void test_suppress_stmt_outer() { + int x; // expected-note {{variable 'x' is declared here}} + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::uninit_read)]] { + int y = x; + (void)y; + } + int z = x; // expected-error {{variable 'x' is read before initialization under profile 'test::uninit_read'}} + (void)z; +} + +template +T template_uninit() { + T x; // expected-note {{variable 'x' is declared here}} + return x; // expected-error {{variable 'x' is read before initialization under profile 'test::uninit_read'}} +} +void instantiate_template_uninit() { + template_uninit(); // expected-note {{in instantiation of function template specialization 'template_uninit' requested here}} +} + +void test_self_init_with_use() { + int x = x; // expected-error {{variable 'x' is read before initialization under profile 'test::uninit_read'}} \ + // expected-note {{variable 'x' is declared here}} + int y = x; + (void)y; +} + +void test_selective_suppress() { + int x; // expected-note {{variable 'x' is declared here}} + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::other)]] { + int y = x; // expected-error {{variable 'x' is read before initialization under profile 'test::uninit_read'}} + (void)y; + } +} + +// Suppress on a declaration is token-based: it covers the initializer of that +// declaration but not later uses that live in different declarations. +void test_decl_suppress_does_not_extend() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::uninit_read)]] int x; // expected-note {{variable 'x' is declared here}} + int y = x; // expected-error {{variable 'x' is read before initialization under profile 'test::uninit_read'}} + (void)y; +} + +void take_const_ref(const int &); +void take_const_ptr(const int *); + +// A const-reference or const-pointer use of an uninitialized variable is a +// binding, not a read: the profile must not report it as one. +void test_const_ref_use_not_diagnosed() { + int x; + take_const_ref(x); + take_const_ptr(&x); +} diff --git a/clang/utils/profiles-boost/prelude.hpp b/clang/utils/profiles-boost/prelude.hpp new file mode 100644 index 0000000000000..d7462cc1ad15f --- /dev/null +++ b/clang/utils/profiles-boost/prelude.hpp @@ -0,0 +1,10 @@ +// Force-included (via `clang -include`) at the top of every Boost translation +// unit by the profiles Boost build (.github/workflows/profiles-boost-build.yml) +// so that the std::init profile is enforced across Boost without editing any +// Boost source. +// +// This must contain ONLY the enforcement empty-declaration: a +// [[profiles::enforce(...)]] attribute has to precede every other top-level +// declaration in the translation unit (P3589R2 s1.1.1), which `-include` +// guarantees by inserting this file before the main file's contents. +[[profiles::enforce(std::init)]]; diff --git a/clang/utils/profiles-boost/summarize.py b/clang/utils/profiles-boost/summarize.py new file mode 100644 index 0000000000000..7c5da00703ea4 --- /dev/null +++ b/clang/utils/profiles-boost/summarize.py @@ -0,0 +1,420 @@ +#!/usr/bin/env python3 +"""Summarize std::init profile violations from a Boost build log. + +Reads the plain-text clang log produced by the profiles Boost build +(.github/workflows/profiles-boost-build.yml), classifies each std::init profile +violation by rule kind and by Boost library, and emits: + + * a Markdown report on stdout (for $GITHUB_STEP_SUMMARY): a mermaid pie of the + rule distribution, a ranked top-libraries table, and a rule x library matrix; + * an optional summary.json with the full aggregates (--json); + * an optional self-contained report.html dashboard (--html). + +Text parsing is used deliberately: the human-readable log is a required +deliverable and per-TU SARIF capture under parallel b2 is not practical. Every +profile diagnostic ends in "under profile 'std::init'", which isolates our +errors and excludes the test:: profiles (which share some wording) and unrelated +Boost/clang errors. See the diagnostic catalog in DiagnosticSemaKinds.td. + +Uses only the Python standard library. +""" + +import argparse +import html +import json +import os +import re +import sys +from collections import Counter, defaultdict + +# A clang diagnostic line: "path:line:col: error: message". +_DIAG_RE = re.compile( + r"^(?P[^:\n]+):(?P\d+):(?P\d+):\s+" + r"(?Perror|warning|note):\s+(?P.*)$" +) +_PROFILE_RE = re.compile(r"under profile '(?P[^']+)'") +_CRASH_MARKER = "PLEASE submit a bug report" + +# Rule classification, applied in order; first match wins. Each entry is +# (rule, predicate(msg)). Ordering matters where messages share substrings +# (e.g. uninit_read's "...read through a '[[ref_to_uninit]]'..." must be caught +# before the ref_to_uninit check, and uninit_write's "does not initialize it" +# before ctor_uninit_member's "constructor does not initialize"). See +# clang/include/clang/Basic/DiagnosticSemaKinds.td for the source messages. +def _has(*needles): + return lambda m: all(n in m for n in needles) + + +def _any(*needles): + return lambda m: any(n in m for n in needles) + + +_RULES = [ + ("uninit_read", _any("is read before initialization", + "accesses uninitialized memory")), + ("uninit_write", _has("does not initialize it")), + ("ctor_uninit_member", _has("constructor does not initialize")), + ("ref_to_uninit", _any("[[ref_to_uninit]]", + "binds a reference to uninitialized memory", + "binds its implicit object parameter")), + ("pointer_marker", _has("'[[uninit]]' cannot be applied to a pointer")), + ("static_marker", _has("'[[uninit]]' cannot be applied to variable", + "storage duration")), + ("union_marker", _has("'[[uninit]]' cannot be applied to", "union")), + ("uninit_with_initializer", + _any("cannot be both '[[uninit]]' and have an initializer", + "default-initialization of its type")), + ("uninit_decl", _any("must be initialized or marked '[[uninit]]'", + "of union type must be initialized")), + ("static_runtime_init", _has("requires constant initialization")), +] + +# Stable order for display (matches _RULES, plus the catch-all). +RULE_ORDER = [name for name, _ in _RULES] + ["other"] + +_LIBS_RE = re.compile(r"(?:^|/)libs/([^/]+)/") + + +def classify_rule(msg): + for name, pred in _RULES: + if pred(msg): + return name + return "other" + + +def _lib_from_path_text(path): + # A compiled-library source: libs//... wins (most precise). + m = _LIBS_RE.search(path) + if m: + return m.group(1) + # A header: the component follows the include root's "boost" directory. Take + # the segment after the LAST "boost" segment so a doubled root like + # ".../boost/boost/container/..." (checkout dir + include dir) yields + # "container", not "boost". Strip the extension for single-file headers + # (boost/optional.hpp -> optional). + parts = path.split("/") + boost_idx = [i for i, p in enumerate(parts) if p == "boost"] + if boost_idx and boost_idx[-1] + 1 < len(parts): + comp = parts[boost_idx[-1] + 1] + return comp.split(".")[0] if "." in comp else comp + return None + + +def library_for_path(path, boost_root, cache): + """Attribute a diagnostic path to a Boost library. + + Headers under boost/ are symlinks into libs//include/..., so resolving + the real path (when boost_root is given) yields precise attribution; fall + back to the textual boost/ heuristic otherwise. + """ + if path in cache: + return cache[path] + candidates = [] + if boost_root: + for base in (boost_root, os.getcwd()): + p = path if os.path.isabs(path) else os.path.join(base, path) + try: + candidates.append(os.path.realpath(p)) + except OSError: + pass + candidates.append(path) + lib = None + for cand in candidates: + lib = _lib_from_path_text(cand) + if lib: + break + lib = lib or "" + cache[path] = lib + return lib + + +def parse_log(lines, boost_root=None): + by_rule = Counter() + by_library = Counter() + matrix = defaultdict(Counter) # library -> rule -> count + by_file = Counter() + files = set() + crash_markers = 0 + non_profile_errors = 0 + cache = {} + + for line in lines: + if _CRASH_MARKER in line: + crash_markers += 1 + m = _DIAG_RE.match(line.rstrip("\n")) + if not m or m.group("level") != "error": + continue + msg = m.group("msg") + pm = _PROFILE_RE.search(msg) + if not pm: + non_profile_errors += 1 + continue + if pm.group("profile") != "std::init": + # A different profile (e.g. a test:: profile); not counted here. + continue + path = m.group("path") + rule = classify_rule(msg) + lib = library_for_path(path, boost_root, cache) + by_rule[rule] += 1 + by_library[lib] += 1 + matrix[lib][rule] += 1 + by_file[path] += 1 + files.add(path) + + total = sum(by_rule.values()) + return { + "total_violations": total, + "files": len(files), + "libraries": len(by_library), + "crash_markers": crash_markers, + "non_profile_errors": non_profile_errors, + "by_rule": {r: by_rule.get(r, 0) for r in RULE_ORDER if by_rule.get(r)}, + "by_library": dict(by_library.most_common()), + "matrix": {lib: dict(counts) for lib, counts in matrix.items()}, + "top_files": by_file.most_common(50), + } + + +# -------------------------------------------------------------------------- +# Markdown (step summary) +# -------------------------------------------------------------------------- +def _bar(count, maximum, width=24): + if maximum <= 0: + return "" + n = max(1, round(count / maximum * width)) + return "█" * n + + +def render_markdown(data, top_libraries=20, matrix_libraries=15): + out = [] + out.append("## std::init profile violations — Boost build\n") + total = data["total_violations"] + if total == 0: + out.append("No `std::init` profile violations found in the log.\n") + if data["non_profile_errors"]: + out.append(f"\n_({data['non_profile_errors']} non-profile " + "errors seen — likely unrelated Boost/clang issues.)_\n") + if data["crash_markers"]: + out.append(f"\n**{data['crash_markers']} clang crash " + "marker(s) detected.**\n") + return "".join(out) + + out.append( + f"**{total}** violations across **{data['files']}** files in " + f"**{data['libraries']}** libraries. \n" + ) + extra = [] + if data["crash_markers"]: + extra.append(f"**{data['crash_markers']} clang crash " + "marker(s)**") + if data["non_profile_errors"]: + extra.append(f"{data['non_profile_errors']} non-profile errors") + if extra: + out.append(" · ".join(extra) + "\n") + + # Rule distribution (mermaid pie). + out.append("\n### By rule kind\n\n") + out.append("```mermaid\npie showData title Violations by rule\n") + for rule, count in sorted(data["by_rule"].items(), + key=lambda kv: kv[1], reverse=True): + out.append(f' "{rule}" : {count}\n') + out.append("```\n") + + # Top libraries. + libs = list(data["by_library"].items())[:top_libraries] + if libs: + maximum = libs[0][1] + out.append("\n### Top libraries\n\n") + out.append("| Library | Violations | |\n|---|--:|:--|\n") + for lib, count in libs: + out.append(f"| `{lib}` | {count} | {_bar(count, maximum)} |\n") + + # Rule x library matrix (top libraries as rows, occurring rules as columns). + present_rules = [r for r in RULE_ORDER if data["by_rule"].get(r)] + mlibs = list(data["by_library"].items())[:matrix_libraries] + if mlibs and present_rules: + out.append(f"\n### Rule × library (top {len(mlibs)})\n\n") + header = "| Library | " + " | ".join(present_rules) + " | Total |\n" + sep = "|---|" + "|".join(["--:"] * (len(present_rules) + 1)) + "|\n" + out.append(header) + out.append(sep) + for lib, total_lib in mlibs: + row = data["matrix"].get(lib, {}) + cells = " | ".join(str(row.get(r, 0)) for r in present_rules) + out.append(f"| `{lib}` | {cells} | {total_lib} |\n") + + return "".join(out) + + +# -------------------------------------------------------------------------- +# HTML dashboard (self-contained; no external resources) +# -------------------------------------------------------------------------- +_HTML_TEMPLATE = """ + + + + +std::init violations — Boost + + + +

std::init profile violations — Boost

+

+
+ +

By rule kind

+
+ +
RuleCountShare
+ +

By library

+
+ +
LibraryViolationsShare
+ +

Rule × library

+
+ + + + + +""" + + +def render_html(data): + payload = json.dumps(data) + # The JSON is embedded in a