Claude Skill

adopt-c-bounds-safety

Guide for the C -fbounds-safety language extension. Covers the language model, pointer annotations, adopting bounds-safety in existing C code, compiler build settings and modes, and runtime debugging of bounds violations.

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download artemnovichkov-xcode-skills-skills_adopt-c-bounds-safety-aa5c1cb.zip · 48 KB
Part of artemnovichkov/xcode-skills — 15 skills

Install

skills CLI npx skills add https://github.com/artemnovichkov/xcode-skills/tree/main/skills/adopt-c-bounds-safety
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install artemnovichkov-xcode-skills@llmmart
Git git clone https://github.com/artemnovichkov/xcode-skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole artemnovichkov/xcode-skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

How to Use This Skill

When helping with -fbounds-safety adoption or code changes, ask clarifying questions about the user's codebase and goals before suggesting changes. For complex tasks involving multiple files or non-trivial annotation decisions, use plan mode to propose an approach before implementing.

-fbounds-safety Language Extension

-fbounds-safety is a C language extension that prevents out-of-bounds memory access by enforcing bounds safety at the language level. It inserts automatic bounds checks at runtime, rejects unsafe pointer operations at compile time, and requires programmers to provide bounds annotations so the compiler can guarantee safety. Out-of-bounds accesses become deterministic traps instead of exploitable vulnerabilities.

Detailed Documentation

Required reading before adoption work

You MUST have fully read the following three documents (via the Read tool) at the start of an adoption task, and re-read them via the Read tool before any source-modifying step in the adoption workflow unless their content is verifiably fresh in your active context:

Other references (read on demand)

For compiler flags, Xcode build settings, soft trap mode, and ptrcheck.h configuration, read build-settings.md.

For debugging bounds violations at runtime — trap behavior, LLDB commands, wide pointer inspection, watchpoints, crash log analysis, and soft trap debugging, read runtime-debugging.md.

Files (xcode-skills)
  • references
    • adoption-strategies.md 52.9 KB
      # Adoption Strategies for `-fbounds-safety`
      
      This guide walks through the process of adopting `-fbounds-safety` in an existing C project.
      
      `-fbounds-safety` maintains ABI compatibility, so you can adopt it without breaking clients that don't use it. Incremental adoption is supported — you can secure your code file by file over multiple releases.
      
      > **Before asking the user anything or starting any planning, present the following message to them verbatim:**
      >
      > > Preparing to help you adopt -fbounds-safety, which is a C language extension that enforces bounds safety through compile-time and runtime checks.
      > > 
      > > 1. I'll ask some questions to identify the kind of adoption you want to do.
      > > 2. I'll analyze your code and write a plan to perform the adoption.
      > > 3. Once you confirm the plan, I'll perform the adoption in multiple steps, stopping at relevant points to give you a chance to review the changes before I commit them.
      
      > **Always make a plan when applying this skill because changes are rarely trivial and the developer needs to understand the process**
      
      ## Prerequisites
      
      ### Code is under a version control system (VCS)
      
      Adoption commits at multiple checkpoints, so the project must be under a VCS this skill can drive and the working tree must be clean. Before asking the user any question or analyzing code, detect the VCS (without asking the user — if multiple, take the innermost relative to the project root) and run its status command.
      
      Once detected, record the VCS name and the concrete commands you will use for:
      
      - status
      - diff
      - staging by explicit path
      - commit
      - discarding a file's uncommitted working-tree changes
      
      Use those captured commands for every VCS operation in the rest of this skill — do not switch VCSes mid-run, and do not assume git unless git is what you detected.
      
      If no usable VCS is found, present the **No-VCS refusal** below and stop. If the working tree is not clean, present the **Dirty-tree refusal** below, including the status output, and stop. On user-reported remediation, re-run the checks before continuing.
      
      **No-VCS refusal:**
      
      > > `-fbounds-safety` adoption commits at multiple review checkpoints, so without version control I cannot checkpoint stages, revert a bad enablement, or keep your edits separate from mine at review stops.
      > >
      > > Please initialize a repository (or move to a directory already under version control) and tell me when to retry.
      
      **Dirty-tree refusal:**
      
      > > The working tree has uncommitted changes. Adoption commits at multiple review checkpoints, and pre-existing changes would get bundled into those commits and tangle prior work with adoption edits.
      > >
      > > Please commit, set aside, or discard the existing changes, then tell me when to retry. The current status output is below.
      
      ### Build system source of truth (when running under Xcode)
      
      If you have been told you are running under Xcode, use the project's `.xcworkspace` (preferred) or `.xcodeproj` as the single source of truth for all build-related queries and operations — ignore every other build-system or project-generator artifact regardless of kind (e.g., `Makefile`). Search the VCS-tracked tree (rooted at the VCS root detected above) and take the shallowest match; if more than one candidate exists at the same depth, ask the user which to use. When a `.xcworkspace` is present, treat it as the entry point and resolve the relevant `.xcodeproj` from its `contents.xcworkspacedata` — if the workspace references multiple projects, ask the user which one to adopt. Do not switch build systems mid-run.
      
      Once resolved, record the workspace path (if any), the `.xcodeproj` path, the `xcodebuild` invocation form (workspace+scheme or project+target), and the per-file `-fbounds-safety` attachment mechanism — reuse these throughout the rest of the skill rather than re-deriving them.
      
      For build-system queries and operations against the resolved project, prefer the Xcode MCP tools; fall back to other methods (e.g., reading `project.pbxproj`, running `xcodebuild`) only when those tools are insufficient.
      
      If the resolved `.xcodeproj` is produced by a generator script (e.g., a top-level `generate_xcodeproj.py`, xcodegen, Tuist), warn the user up front that per-file `-fbounds-safety` flags this skill writes into the `.xcodeproj` will be silently clobbered on the next regeneration — they must either stop regenerating or migrate the flag wiring into the generator's input.
      
      If no `.xcworkspace` or `.xcodeproj` exists anywhere in the VCS-tracked tree, present the **No-Xcode-project refusal** below and stop.
      
      **No-Xcode-project refusal:**
      
      > > I'm running under Xcode but can't find a `.xcworkspace` or `.xcodeproj` in this project. Please tell me which build system to treat as source of truth.
      
      If the user names SwiftPM (`Package.swift`) as the source of truth, decline: SwiftPM does not expose per-file C build flags, which `-fbounds-safety` adoption requires. Ask them to name a different build system.
      
      If the user names any other build system (e.g., `Makefile`), confirm it supports per-file C flag attachment and record the concrete mechanism (e.g., per-file `CFLAGS`) for use in place of Xcode-specific flag wiring throughout the rest of this skill. If it does not support per-file C flag attachment, decline as with SwiftPM and ask them to name a different build system.
      
      ## Choosing an Adoption Approach
      
      > **Before advising on adoption, ask the user whether they want full adoption or header-only adoption, then provide guidance for the chosen approach.**
      
      There are two approaches to adopting `-fbounds-safety`:
      
      - **Full adoption**: Annotate headers AND enable `-fbounds-safety` in implementation files. Provides complete bounds safety enforcement — the compiler inserts runtime bounds checks in your code and rejects unsafe operations at compile time.
      
      - **Header-only adoption**: Only annotate public headers. The implementation remains unchanged and is not compiled with `-fbounds-safety`. Lightweight alternative that benefits clients adopting `-fbounds-safety` without any runtime cost or code changes to your library's implementation. If there are no headers do not suggest this approach.
      
      ## Full Adoption
      
      ### Typical source code changes
      
      Enabling `-fbounds-safety` implicitly adds bound annotations (e.g. `__single`) on pointer/array type declarations. Each bound annotation has different restrictions on how they can be used and these restrictions are enforced by a mixture of compile time and runtime checks. The compile time checks appear as compiler diagnostics. All errors will need to be fixed and warnings should be addressed if possible. Fixing these diagnostics typically is a mixture of
      
      #### 1. Explicitly using different bounds attributes from the ones that are implicitly added.
      
      In many cases, adoption involves annotating pointers passed as parameters or stored in structures:
      
      ```c
      // BEFORE
      void take_elements(const element_t *elements, size_t count);
      
      // AFTER
      void take_elements(const element_t *__counted_by(count) elements, size_t count);
      ```
      
      Avoid ABI-incompatible annotations (`__indexable` or `__bidi_indexable`) on consumer-facing APIs. Also avoid use of `__unsafe_indexable` which is unsafe
      and defeats the purpose of using `-fbounds-safety` in the first place.
      
      Knowing which attributes to use typically requires looking at how the type is used. For example if annotating a function, looking at use sites and the implementation of that function may provide clues on what the bounds are and thus the appropriate annotation to add to that function
      
      #### 2. Adapting implementation code to work with the compile time restrictions added by using bounds attributes.
      
      e.g.:
      
      ```c
      // BEFORE
      int find_zero(int *__counted_by(count) elements, size_t count) {
          int idx = -1;
          while (idx < count && *elements != 0) {
              // error: assignment to 'int *__single __counted_by(count)' 'elements' requires corresponding assignment to 'count'
              ++elements;
              ++idx;
          }
          return idx;
      }
      
      // AFTER
      int find_zero(int *__counted_by(count) elements, size_t count) {
          int idx = -1;
          size_t original_count = count;
          while (idx < original_count && *elements != 0) {
              ++elements;
              --count;
              ++idx;
          }
          return idx;
      }
      ```
      
      #### 3. Propagating bounds annotation choices
      
      As bounds annotations on API surfaces are changed this potentially impacts all use sites of them leading to different compiler diagnostics. This requires an iterative process of changing annotations, recompiling, looking at the diagnostics and deciding what to fix, fixing, and repeating until the source file can be compiled without errors.
      
      #### 4. Refactoring code such that the use of unsafe constructs happens as few places as possible.
      
      When a project adopting `-fbounds-safety` needs to interact with code that hasn't adopted `-fbounds-safety` typically that means ingesting `__unsafe_indexable` pointers. Ideally we do not want to propagate that `__unsafe_indexable` pointer through out the codebase. Instead there should be a centralized place(s) where `__unsafe_indexable` pointers are consumed and then forged into a safe pointer type (i.e. `__unsafe_forge_bidi_indexable`) which is then propagated through the codebase. That way the majority of the project works with safe pointer types and the sources of unsafe pointers is very small and easier to audit.
      
      ### Adoption strategy
      
      #### Tracking adoption progress
      
      Adoption has many sub-steps across many files. Use `TaskCreate` at three moments so no sub-step is forgotten while keeping the active task list focused.
      
      **Moment A — before any file is modified.** Create one task for:
      
      - `Confirm approach with the user` (full vs header-only)
      - `Confirm how to run tests with the user` (full adoption only — capture how to run the tests (e.g. shell command, unit tests, etc.). If the user declines tests at this point, follow the explicit-confirmation procedure in §3 now rather than deferring it to §3 entry, so the no-tests decision is made deliberately at the earliest opportunity.)
      - Each top-level step below: 0, 1, 2, 4 (full adoption only), 5.1 (umbrella checkpoint only — full adoption only — see note below), 6 (full adoption only)
      - A trigger task `Create per-file adoption tasks` — its body creates Moment B's tasks once the adoption order is known. It must exist so per-file task creation isn't forgotten.
      
      Step 5.x umbrella checkpoint tasks are placeholders at adoption start; they apply only to full adoption (header-only adoption has its own [§3 Safe Wrapper retrofits](#3-safe-wrapper-retrofits-if-any-captured) but does not reach full adoption's §3 onwards). Per-item tasks accumulate underneath each umbrella as earlier phases (e.g. Phase 1) make decisions; their `addBlocks` wires them to the corresponding umbrella, which is itself wired into the per-file → 4 → 5.x → 6 chain (see Moment B).
      
      **Moment B — body of the `Create per-file adoption tasks` task, run immediately after step 0 completes.** For every implementation file in adoption order that does not already have a per-file task, create one named `Adopt -fbounds-safety in <file>`. (The §3 [Skipping a file's enablement](#skipping-a-files-enablement) procedure already creates a per-file task for any file flagged upfront for skip; don't re-create those.) All file-level tasks must be created at once so the full adoption scope is visible, but sub-tasks are deferred to Moment C — this keeps the pending-task list short and lets sub-step applicability be decided per file at execution time.
      
      After creating every file-level task, wire the dependency chain `files → 4 → each 5.x umbrella → 6` by calling `TaskUpdate` with the appropriate `addBlockedBy`:
      
      - The step 4 target-level task gets `addBlockedBy` listing every file-level task (so target-level enablement waits for all per-file adoption).
      - Each step 5.x umbrella checkpoint task gets `addBlockedBy [<step 4 task ID>]` (so post-target refinements wait for target-level enablement).
      - The step 6 completion-milestone task gets `addBlockedBy` listing every step 5.x umbrella (so the milestone surfaces only after the post-target batches land).
      
      If any file is later skipped via §3 [Skipping a file's enablement](#skipping-a-files-enablement), no rewiring is needed; §5 and subsequent tasks unblock automatically.
      
      **Moment C — first action when picking up any `Adopt -fbounds-safety in <file>` task.** Before modifying the file, `TaskCreate` sub-tasks for it mirroring sub-steps 3.1, 3.2, 3.3 (omit if the user did not provide a way to run the tests), 3.4, 3.5a, 3.5b. Only mark the file-level task `in_progress` after its sub-tasks exist.
      
      **Rules for marking tasks complete:**
      
      - Only mark a task `completed` when that specific sub-step is done.
      - A file-level task is complete only when all 6 of its sub-tasks are complete.
      - If a sub-task legitimately does not apply (e.g. the file has no runtime tests to exercise it), mark it complete with a one-line note explaining why. Do not skip silently.
      
      #### Commit hygiene at review stops
      
      Every commit during adoption is preceded by a stop-and-review step. During that stop the user is explicitly invited to inspect and modify the changes. **Their edits must end up in a commit — they must not be silently left in the working tree or dropped.** Follow this procedure at every commit point in this guide:
      
      1. Before staging anything, list **all** working-tree changes and inspect their diff using the captured VCS commands (e.g. `git status` + `git diff HEAD`) to enumerate them. This includes both Claude's edits and any further edits the user made while the stop was open. Do not assume the working tree contains only what Claude wrote.
      2. Classify each modified or new file as **source-code** (`.c`, `.h`, validation files) or **build-system** (Xcode `project.pbxproj`, CMakeLists, Makefiles, any per-file flag entry).
      3. Check the result against the commit's declared scope (stated at each commit site below — e.g. "source-code only", "build-system only", or "headers + validation file"):
          - If every changed file fits the scope, stage exactly those files (Claude's + user's) by explicit path and commit using the captured VCS commands.
          - If the user's edits span kinds that don't all fit the scope — for example, source-code edits appearing during a build-system-only commit — **stop and ask the user** how to split them: which go into the current commit, which should be deferred to the next one, and which (if any) should be dropped. Apply their answer, then commit.
      4. Always specify explicit paths when staging or committing — never let unrelated working-tree changes (e.g. `.DS_Store`, scratch files) get picked up. On git, this rules out `git add -A`, `git add .`, `git commit -a`, and any flag or shorthand that auto-includes modified files.
      5. Do not propose folding user edits into a previously-made commit (e.g. `git commit --amend`) unless the user explicitly asks for it.
      
      This procedure is referenced from §2, §3 step 5a, §3 step 5b, and §5.x's verify-stop-and-commit body below.
      
      #### 0. Code Research
      
      ##### Order of adoption
      
      > If the user has not stated in which target they want to do adoption and it cannot be inferred ask them to clarify which target.
      
      Once the target is known if it contains more than one `.c` source file we need to decide the order implementation files will adopt -fbounds-safety. Some analysis of the code can guide this
      
      > use a sub-agent to do this analysis and return an ordered list of implementation files
      
      - Computing a callgraph for functions in public headers can be used to guide implementation file order. Typically source files that implement public functions should adopt -fbounds-safety first as they may provide bounds information that needs to be propagated throughout the code base. Traversing the call graph starting at the roots can guide implementation file order as each node has an implementation file associated with it. If we have a -> b, and a and b are implemented in different source files then this is a hint that the implementation file a should adopt -fbounds-safety before b.
      - The same as above can be done for private headers
      
      If the user already knows a particular `.c` file is unadoptable in this pass (e.g. a known compiler crash, or they want to defer it), invoke the §3 [Skipping a file's enablement](#skipping-a-files-enablement) procedure the moment the user declares the skip.
      
      > Reminder: when running under Xcode the `.xcodeproj` is the source of truth for all build-system queries and operations — see [Build system source of truth](#build-system-source-of-truth-when-running-under-xcode).
      
      #### 1. Headers First
      
      > **Before doing this step, re-read `language-overview.md` and `common-patterns-and-pitfalls.md` in full via the Read tool.**
      
      Annotate public headers with bounds annotations on function parameters, return types, struct fields, and globals. Adding `-fbounds-safety` annotations to a header signals that the header has adopted bounds safety; clients compiled with `-fbounds-safety` will see the annotations and benefit from compile-time and call-site checks.
      
      - *(Full adoption only)* Modify headers before implementation files — implementation files will need all header definitions to have adopted `-fbounds-safety` first.
      - Clients benefit from annotated interfaces even when the implementation doesn't enable `-fbounds-safety`.
      - Unannotated interfaces result in all pointers being `__unsafe_indexable`, which is cumbersome for `-fbounds-safety` clients.
      
      Example annotations:
      
      ```c
      // C standard library style:
      void *memcpy(void *__sized_by(n) dst, const void *__sized_by(n) src, size_t n);
      
      // Custom API:
      int process_buffer(const uint8_t *__counted_by(len) data, size_t len);
      ```
      
      After adopting `-fbounds-safety` in a public header, add this directive at the start:
      
      ```c
      #include <ptrcheck.h>
      __ptrcheck_abi_assume_single()
      ```
      
      This tells the compiler that ABI-visible pointers (except `const char*`) in this header should be treated as `__single` (not `__unsafe_indexable`, which is the default for SDK headers). `__ptrcheck_abi_assume_single` also only affects the current header, it does not affect the attributes in subsequently included headers.
      
      ##### Capturing deferred Safe Wrapper retrofits
      
      When choosing `__unsafe_indexable` on a public-API function parameter or return, create a per-item Safe Wrapper task immediately. Capture happens at the moment of decision because the rationale is fresh; execution defers to step 5.1 in full adoption (see [5. Post-target-level refinements](#5-post-target-level-refinements)) or to step 3 in header-only adoption (see [3. Safe Wrapper retrofits (if any captured)](#3-safe-wrapper-retrofits-if-any-captured)).
      
      Setup: the upfront task-creation step creates the Safe Wrapper umbrella. Its name and wiring depend on the adoption mode:
      
      - **Full adoption** (Moment A): umbrella is `5.1 Commit Safe Wrapper batch`, `addBlockedBy [<step 4 task ID>]`, `addBlocks [<step 6 task ID>]`.
      - **Header-only adoption** (Header-Only Adoption's `Tracking adoption progress` subsection): umbrella is `3b. Commit Safe Wrapper batch`, `addBlockedBy [<3a task ID>]`, `addBlocks [<milestone task ID>]`.
      
      For each `__unsafe_indexable` decision on a public-API parameter or return:
      
      1. **Defensive umbrella check.** Before creating the per-item task, confirm the Safe Wrapper umbrella exists. If not (e.g. the adoption was picked up mid-stream and the upfront task-creation step never ran for this session), create it now with the wiring for the current adoption mode (see Setup above).
      2. Grep for the function's definition to identify the implementing `.c` file. (If the function is defined outside any file you're adopting, ask the user how to handle it.)
      3. `TaskCreate` a task `Add Safe Wrapper for <funcName>` with a structured description like:
      
         ```
         Apply the Safe Wrappers for Public APIs pattern.
      
         - Function: <funcName>
         - Header: <header path>
         - Implementation file: <file>.c
         - Original signature (with __unsafe_indexable):
           <verbatim signature>
         - Reason for __unsafe_indexable: <one line — e.g. "length-prefixed buffer; bound is buf[0]">
      
         See [Safe Wrappers for Public APIs](common-patterns-and-pitfalls.md#safe-wrappers-for-public-apis) for the recipe.
         ```
      
         (The "do not commit between per-item tasks" instruction lives in §5's framing in full adoption and in §3's framing in header-only, not in each per-item description.)
      4. `TaskUpdate addBlockedBy` so the wrapper task can't surface until its gating predecessor is done — `[<step 4 task ID>]` in full adoption; `[<3a Confirm Safe Wrapper application task ID>]` in header-only.
      5. `TaskUpdate addBlocks [<Safe Wrapper umbrella task ID>]` so the umbrella checkpoint waits for this wrapper.
      
      Do **not** put the wrapper list in the umbrella task's description — per-item tasks track per-item state and verification natively. The umbrella's description is just the verify-stop-and-commit body.
      
      #### 2. Create a Validation File
      
      Create a single `.c` file that includes every adopted header and compile it with `-fbounds-safety`. This ensures headers are compliant even if your project doesn't yet fully use `-fbounds-safety`.
      
      Compiling the validation file requires `-fbounds-safety` to be added as a per-file build flag on it.
      
      After creating the validation file (and any header adjustments needed to make it compile), **stop and ask the user to review before committing.** In that message:
      
      - State that header files have been modified to adopt -fbounds-safety and that a validation file has been added to ensure the changes parse when -fbounds-safety is on.
      - State that on approval the new validation file and any header changes will be committed together.
      - List the names of the modified header files and new validation file.
      - Invite the user to inspect the changes, make any further changes they need, and approve when ready to commit.
      
      On approval, commit the changes following the [Commit hygiene at review stops](#commit-hygiene-at-review-stops) procedure. The scope of this commit is **header edits + the new validation file**, committed together as a single commit — the 5a/5b source-vs-build split does not apply here.
      
      If you are doing header-only adoption, stop here. Do not proceed to "3. Enable Per-File in Implementation" — that section is only for full adoption.
      
      #### 3. Enable Per-File in Implementation
      
      > **Before doing this step, re-read `language-overview.md` and `common-patterns-and-pitfalls.md` in full via the Read tool.**
      
      Enable `-fbounds-safety` in implementation files one at a time. Use the order computed in "Order of adoption". If the compiler crashes at any point during this section, see [Handling a compiler crash](#handling-a-compiler-crash) below before continuing.
      
      > Before starting this section, confirm with the user how to run the project's tests (this should already have been captured by the `Confirm how to run tests` task in Moment A — re-confirm if it was not). If the user cannot or will not provide a way to run the tests, **stop and ask them**, verbatim:
      >
      > > Performing `-fbounds-safety` adoption without providing tests to verify runtime behavior greatly increases the chance of adopted code containing reachable runtime traps due to failing bounds checks. Are you sure you want to proceed without providing tests?
      >
      > Wait for the user's **explicit answer**.
      > - If the user confirms they want to proceed without tests: skip sub-step 3 below ("Run the project's tests and fix any runtime traps") for every file in this section. The same skip applies to §5.1 step 2.
      > - If the user changes their mind and wants to provide tests: capture how to run the tests from them (e.g. shell command, unit tests, etc.), record it for use in sub-step 3 (and §5.1 step 2), and continue with sub-step 3 enabled.
      
      1. Enable `-fbounds-safety` for a single C file by adding it as a per-file build flag.
      2. Fix compilation errors (compiler diagnostics guide you on what annotations to add). Use `-ferror-limit=0` to get unlimited diagnostics if you want to see all errors at once.
      3. Run the project's tests and fix any runtime traps. See [runtime-debugging.md](runtime-debugging.md). *(Skip this sub-step if the user could not provide a way to run the tests — see the warning at the top of this section.)*
      4. **Stop and ask the user to review the changes for this file before committing.** Before summarizing what changed, communicate the following three things in this order:
      
          1. Identify the file: state that the source-file changes under review are for `<filename>` (the actual file path).
          2. Explain what will happen on approval: the changes will be committed in two steps — first, the source-code changes committed with `-fbounds-safety` switched off for this file; second, a build-system change that re-enables `-fbounds-safety` for this file. This split is done to make it easy to revert the enablement later without losing the source-code improvements.
          3. Invite the user to inspect the changes, make any further changes they need, and approve when ready to commit.
      
          Then summarize the actual changes (annotations added, refactors, any unsafe forges introduced). Wait for the user's explicit approval. If they request adjustments, apply them, re-run the project's tests, and ask again. Only proceed to step 5 once the user has explicitly approved.
      5. Commit the work for this file as **two separate commits**. This structure is MANDATORY — do NOT combine into a single commit.
      
          **5a. Source-changes commit.**
          - Temporarily clear `-fbounds-safety` from this file's per-file build flags.
          - Verify the source still compiles without the flag.
          - If it does not compile, make the minimum changes needed to compile cleanly with the flag off, then **stop and tell the user explicitly: we stopped because additional source changes were needed since the file did not compile with `-fbounds-safety` disabled. Ask them to review the changes, make any necessary further changes, and continue when they approve.** Apply any requested adjustments and re-verify the build before proceeding. When execution resumes, the [Commit hygiene at review stops](#commit-hygiene-at-review-stops) procedure applies to whatever the user touched during this sub-stop.
          - Commit following the [Commit hygiene at review stops](#commit-hygiene-at-review-stops) procedure. Scope: **source-code only** (annotations, refactoring). Any build-system changes in the working tree are deferred to 5b — if the user's edits span both kinds, the shared procedure will stop and ask.
      
          **5b. Build-system commit.**
          - Re-add `-fbounds-safety` as a per-file build flag for this file.
          - Verify it still compiles.
          - Commit following the [Commit hygiene at review stops](#commit-hygiene-at-review-stops) procedure. Scope: **build-system only**. If the user added source-code edits between 5a and now, the shared procedure will stop and ask how to handle them — do not silently bundle them into this commit.
      
          Rationale: this separates source churn from the act of enabling the flag. If enablement has to be reverted later, only commit 5b is reverted — the source-code improvements from 5a remain. Collapsing into one commit loses this property.
      
      6. Repeat the above until every file in the adoption order is either adopted or explicitly skipped via [Skipping a file's enablement](#skipping-a-files-enablement) below.
      
      ##### Handling a compiler crash
      
      If a build during sub-step 1 (per-file flag enablement) or sub-step 2 (fixing compilation errors) crashes the compiler, clang's stderr will include a `PLEASE ATTACH THE FOLLOWING FILES TO THE BUG REPORT` block listing `.c` (preprocessed source) and `.sh` (replay script) paths in `$TMPDIR`, plus a pointer to `~/Library/Logs/DiagnosticReports/clang_<...>.crash`. That block is the cue to enter this procedure — don't keep chasing compile errors.
      
      **1. Gather a reproducer via a sub-agent.** Spawn a sub-agent (Task tool, `general-purpose`) with these self-contained instructions:
      
      - Extract the `.c` and `.sh` paths from the crash output the parent provides.
      - Re-run the `.sh` script and confirm it triggers the crash. If it does not, report that back — the crash may not be reliably reproducible.
      - **Multi-arch handling:** if the original build used multiple `-arch` options, clang reports `Error generating preprocessed source(s) - cannot generate preprocessed source with multiple -arch options` instead of producing the `.c` / `.sh`. In that case, re-invoke the same compile command with each `-arch` value individually until one (or more) crashes, gathering the reproducer per crashing arch.
      - Locate the matching crash log under `~/Library/Logs/DiagnosticReports/clang_<YYYY-MM-DD-HHMMSS>_<hostname>.crash` — pick the one whose timestamp matches the crash.
      - Bundle the `.c`, `.sh`, and `.crash` into a single zip at `<project-root>/<crashing-filename>-crash-reproducer.zip` (one zip per crashing arch if multi-arch).
      - Report back: the zip path(s), which arch(es) reproduced, and any missing files.
      
      The preprocessed `.c` and `.sh` are large (often >1 MB combined); using a sub-agent keeps that bulk out of the main conversation context.
      
      **2. Ask the user to file feedback using Feedback Assistant (non-blocking).** Say something like:
      
      > "I gathered a crash reproducer at `<zip-path>`. Please file a feedback about this Clang `-fbounds-safety` crash using Feedback Assistant — either the Feedback Assistant app or https://feedbackassistant.apple.com — and attach the archive. You can continue with the workflow before or after filing; let me know the Feedback ID if you do file, since I'll reference it in any workaround comment."
      
      Then proceed immediately to Step 3 without waiting. If the user later supplies a Feedback ID, use it; otherwise the workaround comment in Step 5 falls back to referencing the local archive path.
      
      **3. Ask the user: skip or workaround?** Say something like:
      
      > "How would you like to proceed with `<file>`?
      > (a) Skip enablement for this file (uses the skip procedure below).
      > (b) Attempt to work around the crash with light source changes (a few locations, no medium-large refactors)."
      
      Wait for the user's explicit answer.
      
      **4a. If skip:** invoke the [Skipping a file's enablement](#skipping-a-files-enablement) procedure with reason `compiler crash` (include the Feedback ID if the user supplied one). No further action needed in this sub-section.
      
      **4b. If workaround:** try light source-level changes in the failing file. Common starting points (not exhaustive — pick what fits):
      
      - Revert the most recent annotation that touched the crash site.
      - Replace the offending annotation with `__unsafe_indexable` at the specific declaration that triggers the crash. This loses bounds safety at that one site — capture it as a Safe Wrapper retrofit if it's on a public API.
      - Restructure the single expression or statement the crash points at to avoid the construct that triggers the crash.
      
      **Keep workarounds light.** If avoiding the crash would require changing more than a handful of source locations, or any structural refactoring, stop and return to Step 3 to choose skip instead. Medium-large refactors are out of scope for this procedure; that workload belongs in a separately planned change.
      
      **5. (workaround only) Leave a discoverable comment at every workaround site.** Each source location modified to dodge the crash gets a short comment that names what *would* have been written here without the crash, so a future reader can find it and restore the intended change once the compiler is fixed:
      
      ```c
      // WORKAROUND for clang -fbounds-safety crash.
      // Intended: <one-line description of the annotation/change we wanted to make here, e.g. "__counted_by(len) on `buf` parameter">.
      // See Feedback Assistant <FB-ID> (or <relative path to crash-reproducer zip>).
      ```
      
      The literal token `WORKAROUND for clang -fbounds-safety crash` must appear verbatim so the workarounds are grep-able across the codebase. The `Intended:` line briefly describes the change that would have landed here without the crash — keep it tight (one line) so it's useful but not laborious to write. Use the Feedback ID the user supplied; if none, reference the local archive path.
      
      After a successful workaround, return to sub-step 2 to fix any remaining compilation errors and proceed normally through 3, 4, 5a/5b for this file. If a *new* crash surfaces during the same file's adoption, re-enter this procedure from Step 1.
      
      ##### Skipping a file's enablement
      
      A `.c` file in the target may turn out not to be adoptable in this pass (e.g. the compiler crashes on it, or the user deliberately defers it). The user can request to skip enablement for that file at any point: upfront during §0 [Order of adoption](#order-of-adoption), or mid-stream while working through §3. Run this procedure the moment the skip is declared. If the trigger is a compiler crash, first run [Handling a compiler crash](#handling-a-compiler-crash); that procedure invokes this one on its skip branch. A target with any skipped file is referred to elsewhere in this guide as being under **partial-target adoption**.
      
      **1. Confirm with the user.** Before acting, restate that proceeding with one or more files skipped has these consequences:
      
      - **§4 [Switch to target-level enablement](#4-switch-to-target-level-enablement) is bypassed.** Per-file `-fbounds-safety` flags stay on the adopted files indefinitely; the target does not flip to `ENABLE_C_BOUNDS_SAFETY`.
      - **The `__ptrcheck_unavailable_r` migration guarantee at §5.1 becomes partial.** The attribute only fires under `-fbounds-safety`, so callers of legacy entry points in skipped files compile silently against the shim. Callers in adopted files are still caught at compile time; callers in skipped files need manual audit if you want full migration.
      - **The target's ABI is no longer uniform.** Today the workflow introduces only `__single`-ABI annotations on cross-TU functions, so this is not actively a problem — but any future use of `__bidi_indexable` or `__indexable` on an internal cross-TU function would create an ABI mismatch with callers in skipped files (wide pointer layout differs from a plain pointer).
      
      Wait for the user's explicit answer.
      
      **2. On approval:**
      
      - Ensure a per-file `Adopt -fbounds-safety in <file>` task exists for the skipped file. If Moment B has already run, it does; otherwise (the skip was declared upfront during §0) `TaskCreate` it now so every skip has the same task representation regardless of when it was declared. `TaskUpdate` that task to `completed` with a one-line note `skipped: <reason>`. If Moment C sub-tasks already exist for the file, mark each `completed` with the same note.
      - `TaskUpdate` the §4 task to `completed` with a one-line note `skipped: file(s) <X, Y, …> not adopted; per-file flags retained for adopted files`. If the §4 task was already marked complete-with-note by a previous skip, append the new file to the running list (re-edit the note via `TaskUpdate`).
      - No dependency rewiring is needed: §5.x umbrellas are already `addBlockedBy [<step 4 task ID>]`, so marking §4 complete naturally unblocks them once the remaining per-file tasks finish.
      
      **3. Handle any in-progress adoption state on the skipped file (mid-stream only).** If the per-file `-fbounds-safety` flag was already toggled on for this file, or source changes toward adoption were already started, stop and ask the user how to handle the uncommitted working-tree changes for this file. The default recommendation is to discard them (e.g. `git restore <file>`) — otherwise the file is left in a half-broken state (e.g. flag on but adoption incomplete). Apply the user's answer before moving on.
      
      Then continue with the next per-file task if mid-stream.
      
      #### 4. Switch to target-level enablement
      
      Run this step only if every file in the target was adopted. Otherwise (some file skipped via [Skipping a file's enablement](#skipping-a-files-enablement)) §4 is bypassed and the workflow proceeds directly to §5.1.
      
      When every file has been adopted it is preferable to enable `-fbounds-safety` at the target level rather than continuing to carry per-file flags. See [build-settings.md](build-settings.md) for the Xcode build settings. This change should be its own commit. Clear the per-file `-fbounds-safety` flag from every adopted file before flipping the target-wide setting.
      
      #### 5. Post-target-level refinements
      
      Project-wide source-level cleanups that depend on every translation unit being uniformly under `-fbounds-safety`. Step 4 made that uniformity ABI-atomic — once it lands, no caller in this target can be left in a non-bounds-safety build. Under partial-target adoption (§4 bypassed via [Skipping a file's enablement](#skipping-a-files-enablement)), this section's per-item tasks still execute, but the uniformity guarantee does not hold — see each sub-step's caveats.
      
      Each 5.x sub-step is structured as:
      
      - **Per-item tasks** (created in earlier phases; one per unit of work). Gated by Step 4. Track per-item state. While processing them, make the source change and mark complete — **do not commit between items.**
      - **One umbrella checkpoint task** (`5.x Commit <substep> batch`). Blocked by every per-item task. When all per-item tasks are complete, this surfaces. Its body is the verify-stop-and-commit sequence for that sub-step (defined per-substep below).
      
      ##### 5.1 Safe Wrapper retrofits
      
      > **Before doing this step, re-read `language-overview.md` and `common-patterns-and-pitfalls.md` in full via the Read tool.**
      
      For every public-API function captured during Phase 1 as a per-item `Add Safe Wrapper for <funcName>` task (struct fields are out of scope), apply the [Safe Wrappers for Public APIs](common-patterns-and-pitfalls.md#safe-wrappers-for-public-apis) pattern.
      
      Mark each per-item task complete after the source change for that wrapper is applied. Move on to the next per-item task. **Do not commit.**
      
      When all per-item Safe Wrapper tasks are complete, the `5.1 Commit Safe Wrapper batch` task surfaces. Its body:
      
      1. **Verify the target still compiles.** Fix any compilation errors introduced by the batch. *(Note: the legacy entry points are `__ptrcheck_unavailable_r`, so an un-switched caller is a compile error here — this step is what guarantees every caller migrated. Under [partial-target adoption](#skipping-a-files-enablement), the attribute only fires in adopted TUs; callers in skipped files keep compiling against the legacy shim.)*
      2. **Run the project's tests.** Use the same test command captured during the `Confirm how to run tests` task in Moment A. Fix any failing tests. *(Skip if the user could not provide a way to run the tests, mirroring §3 step 3.)*
      3. **Stop and ask the user to review the changes before committing.** Mirror §3 step 4's structure — communicate, in this order:
          1. Identify the scope. Tell the user something like: *"The changes introduce Safe Wrappers on the unsafe interfaces identified earlier. Each legacy function is now a thin shim that delegates to a `*Safe` variant with explicit count parameters, and every internal caller has been redirected to use the `*Safe` variant directly."* Then list which functions were wrapped.
          2. Explain what will happen on approval: a single commit (or one tightly-related cluster) covering the entire batch. Unlike per-file enablement — which committed the source changes and the build-system change separately — this is one source-only commit; there's no build-system component.
          3. Invite the user to inspect the changes, make any further changes they need, and approve when ready to commit.
      
          Then summarize the actual changes. Wait for explicit approval. If the user requests adjustments, apply them, re-verify (steps 1 and 2), and re-present.
      4. **On approval, commit** following the [Commit hygiene at review stops](#commit-hygiene-at-review-stops) procedure. Scope: **source-code only** (the wrapper functions, the legacy shim retypings, the `__ptrcheck_unavailable_r` markers, and every caller switched to `*Safe`).
      
      #### 6. Initial Adoption Complete
      
      At this point initial `-fbounds-safety` adoption is complete. Tell the user adoption is done and surface these follow-ups for them to consider — the skill does not perform them:
      
      - **Additional testing to look for runtime bounds-check failures.** Exercising the code beyond the existing test suite (e.g. fuzzing, broader integration tests) can uncover bounds violations that compile-time checking did not catch.
      - **Benchmark and optimize if needed.** Measure performance and binary size against the pre-adoption baseline. If overhead is unacceptable, optimization may be needed.
      
      ### Use of unsafe constructs
      
      [language-overview.md](language-overview.md) contains several escape hatches (e.g. `__unsafe_indexable` and `__unsafe_forge_*` intrinsics). Use of these constructs should be avoided when possible.
      
      ### Common Patterns, Tips, and Pitfalls
      
      For common patterns (local variables to avoid assignment restrictions, handling incompatible APIs, calling non-adopted libraries, choosing between `__indexable` and `__bidi_indexable`) and common pitfalls encountered during adoption, see [common-patterns-and-pitfalls.md](common-patterns-and-pitfalls.md).
      
      ### Soft Trap Mode
      
      Soft traps log violations instead of terminating the program, allowing you to discover multiple issues without fixing them one at a time. This is useful for:
      
      - At-desk debugging: attach a debugger, observe all soft traps, then fix
      - Identifying all bounds violations in a test suite in a single run
      
      See [build-settings.md](build-settings.md) for how to enable soft trap mode, and [runtime-debugging.md](runtime-debugging.md) for how to debug soft traps in LLDB.
      
      Note soft traps do not enforce bounds safety so to get any benefit from `-fbounds-safety` soft trap mode **must be switched off** for adoption to be considered complete.
      
      ### Performance Optimization
      
      Use optimization remarks to identify where bounds checks are emitted. Strategies to reduce overhead:
      
      - Adjust loop conditions so bounds checks match loop bounds (optimizer removes redundant checks)
      - Reorder loops to iterate from size to zero (bounds check often hoisted outside loop)
      - Add manual bounds checks before tight loops to make inner checks redundant
      - Avoid complex count expressions (e.g., division is expensive in count expressions)
      
      ## Header-Only Adoption
      
      Header-only adoption is a lightweight alternative for libraries that don't want the cost of full adoption — either in terms of engineering time or runtime overhead.
      
      ### When to Use
      
      - Your library is consumed by clients that are adopting `-fbounds-safety`
      - You want to provide safe interfaces without changing your implementation
      - You want to avoid runtime overhead in your library
      
      ### Tracking adoption progress
      
      Header-only adoption is bounded — three numbered steps, with §3 being an opt-in Safe Wrapper batch. Use `TaskCreate` once at the start so the user can see the plan and no step is silently dropped. Before any file is modified, create exactly these tasks:
      
      - `Confirm approach with the user` (header-only vs full adoption)
      - `1. Annotate public headers` (per [1. Headers First](#1-headers-first))
      - `2. Create validation file and commit` (per [2. Create a Validation File](#2-create-a-validation-file))
      - `3a. Confirm Safe Wrapper application` (gate task — its body asks the user whether to apply captured wrappers, or auto-completes if none captured; see [3. Safe Wrapper retrofits (if any captured)](#3-safe-wrapper-retrofits-if-any-captured))
      - `3b. Commit Safe Wrapper batch` (umbrella — auto-completes with **no commit** if `3a.` cleared with "no Safe Wrappers captured", "user declined", or amendment declined every captured wrapper. Otherwise runs the verify-stop-and-commit body in §3 over the remaining (approved) wrappers.)
      - `4. Header-only adoption complete` (final milestone — its body is described in [§4](#4-header-only-adoption-complete))
      
      Wire the chain with `TaskUpdate addBlockedBy` so order is enforced and the milestone only surfaces at the end:
      
      - Task `2.` is blocked by task `1.`.
      - Task `3a.` is blocked by task `2.`.
      - Task `3b.` is blocked by task `3a.`.
      - Task `4.` is blocked by task `3b.`.
      
      During §1, the [Capturing deferred Safe Wrapper retrofits](#capturing-deferred-safe-wrapper-retrofits) subsection may create per-item `Add Safe Wrapper for <funcName>` tasks. In header-only mode their wiring is `addBlockedBy [<3a task ID>], addBlocks [<3b task ID>]` — so per-items unblock once `3a.` clears (user approves) and `3b.` waits for them all.
      
      Mark a task `completed` only when its step is actually done. If a step legitimately does not apply, mark complete with a one-line note explaining why rather than skipping silently. In particular: if no per-item Safe Wrapper tasks were created during §1, mark `3a.` complete with a one-line "no Safe Wrappers captured" note when it surfaces, and `3b.` will auto-complete with the same note.
      
      ### Steps
      
      The header-annotation work and validation-file work are the same as the corresponding steps in Full Adoption. Follow these sub-sections in order:
      
      1. **[1. Headers First](#1-headers-first)** — annotate the public headers and add `__ptrcheck_abi_assume_single()`.
      2. **[2. Create a Validation File](#2-create-a-validation-file)** — create a `.c` file that includes all adopted headers and compiles with `-fbounds-safety`.
      3. **[3. Safe Wrapper retrofits (if any captured)](#3-safe-wrapper-retrofits-if-any-captured)** — apply captured Safe Wrappers (after asking the user whether to proceed) and commit. Defined in the new subsection below.
      4. **[4. Header-only adoption complete](#4-header-only-adoption-complete)** — tell the user adoption is done and surface follow-up suggestions (notably: consider full adoption in the future).
      
      Do **not** proceed to Full Adoption's "[3. Enable Per-File in Implementation](#3-enable-per-file-in-implementation)" — that is a different step (despite sharing the same number) and applies only to full adoption. Header-only's §3 above is distinct.
      
      Compiling the validation file (step 2 above) requires `-fbounds-safety` as a per-file build flag.
      
      ### 3. Safe Wrapper retrofits (if any captured)
      
      > **Before doing this step, re-read `language-overview.md` and `common-patterns-and-pitfalls.md` in full via the Read tool.**
      
      This step applies the [Safe Wrappers for Public APIs](common-patterns-and-pitfalls.md#safe-wrappers-for-public-apis) pattern to any per-item `Add Safe Wrapper for <funcName>` tasks captured during §1's [Capturing deferred Safe Wrapper retrofits](#capturing-deferred-safe-wrapper-retrofits) subsection. It is gated on user opt-in: header-only adoption defaults to "no source-file work," so we ask before doing it.
      
      The step is split across two tasks (`3a.` and `3b.`) plus the per-item tasks captured during §1.
      
      #### `3a.` body — opt-in gate
      
      1. **No-captures shortcut.** If no `Add Safe Wrapper for <funcName>` per-item tasks were created during §1, mark `3a.` complete with a one-line "no Safe Wrappers captured" note. `3b.` will auto-complete with the same note when it surfaces.
      2. **Opt-in stop.** Otherwise, stop and ask the user whether to apply the captured wrappers. Communicate, in this order:
          1. List the candidate wrappers (function names, with the one-line "Reason for `__unsafe_indexable`" captured during §1).
          2. Explain that applying these means modest source-file changes — new `*Safe` variants in the implementation file, the legacy functions become thin shims that delegate to their `*Safe` variant, and the legacy declarations are marked `__ptrcheck_unavailable_r` in the public header. Internal callers of the legacy API are **not** re-routed — they continue to call the legacy function (which now goes through the shim), so existing implementation code is left as-is.
          3. Ask whether to proceed, decline, or amend the candidate list. Make explicit that declining (or amending to drop every wrapper) results in **zero source-file changes and zero commits** — the captured per-item tasks are simply marked completed with a "user declined" note and adoption proceeds to the milestone.
      3. **Apply the answer.**
          - On **decline**: mark every per-item `Add Safe Wrapper for <funcName>` task complete with a "user declined" note, mark `3a.` complete with the same note, and let `3b.` auto-complete with the same note when it surfaces. No commit.
          - On **amendment**: edit the candidate list per user direction (e.g. mark a subset declined, leave the rest pending), then mark `3a.` complete.
          - On **approval**: mark `3a.` complete. Per-items unblock and you work each one (next subsection).
      
      #### Per-item application (between `3a.` and `3b.`)
      
      For each remaining `Add Safe Wrapper for <funcName>` per-item task, apply the [Safe Wrappers for Public APIs](common-patterns-and-pitfalls.md#safe-wrappers-for-public-apis) pattern, with the [Header-only variant](common-patterns-and-pitfalls.md#safe-wrappers-for-public-apis) adjustments. Three reminders specific to this mode:
      
      - **Do not switch internal callers** — header-only adoption deliberately leaves internal callers of the legacy API alone, so the only caller of `<funcName>Safe` in the implementation is the shim itself. This keeps the implementation-file footprint minimal.
      - **The implementation file is not under `-fbounds-safety`.** Do not add `__unsafe_forge_*` calls in the legacy shim — they are no-ops here and just clutter the diff. Conversely, do still write the Safe variant's *definition* with the same parameter annotations as the header declaration so the redeclaration is consistent and the signature is ready for full adoption later.
      - **Ensure `<ptrcheck.h>` is reachable in the implementation file.** The annotation macros need it to expand to empty when the flag is off (see [language-overview.md](language-overview.md)). Usually transitive via the public header; add `#include <ptrcheck.h>` directly if not.
      
      Mark each per-item complete after its source change is applied. **Do not commit between per-items.**
      
      #### `3b.` body — verify, stop, commit
      
      When `3b.` surfaces, branch on the state left by `3a.`:
      
      - **If `3a.` cleared with "no Safe Wrappers captured" or "user declined" (or every per-item was marked declined during the amendment branch):** mark `3b.` complete with the same one-line note as `3a.` and stop. **No verify, no review, no commit** — there are no source changes to commit.
      - **Otherwise** (`3a.` approved and at least one per-item was applied), run the body below. (Header-only mode does not capture a test command, so the build alone is the verification gate; users wishing to run tests should do so manually before approving the review stop.)
      
      1. **Verify the target still compiles.** Fix compilation errors.
      2. **Stop and ask the user to review** before committing. Mirror §5.1 step 3's structure — communicate, in this order:
          1. Identify the scope. Tell the user something like: *"The changes introduce Safe Wrappers on the unsafe interfaces identified when annotating the public headers. Each legacy function is now a thin shim that delegates to a `*Safe` variant with explicit count parameters. Internal callers of the legacy API are unchanged — they continue to call the legacy function (which now goes through the shim), so the implementation footprint stays minimal."* Then list which functions were wrapped.
          2. Explain what will happen on approval: a single commit (or one tightly-related cluster) covering the entire batch — source-only, with no separate build-system commit.
          3. Invite the user to inspect the changes, make any further changes they need, and approve when ready to commit.
      
          Then summarize the actual changes. Wait for explicit approval. If the user requests adjustments, apply them, re-verify (step 1 above), and re-present.
      3. **On approval, commit** following the [Commit hygiene at review stops](#commit-hygiene-at-review-stops) procedure. Scope: **source-code only** (the new `*Safe` definitions, the legacy shim rewrites, and the `__ptrcheck_unavailable_r` markers in the public header).
      
      ### 4. Header-only adoption complete
      
      At this point header-only `-fbounds-safety` adoption is complete. Tell the user adoption is done and surface these follow-ups for them to consider — the skill does not perform them:
      
      - **Consider full adoption in the future.** Header-only protects external clients of the library; the library's own implementation is not compiled with `-fbounds-safety`, so bugs inside the implementation are not caught at compile time and out-of-bounds accesses inside the implementation are not trapped at runtime. If stronger guarantees are wanted later, [Full Adoption](#full-adoption) extends bounds-safety to the implementation itself. The work already done — annotated public headers, the validation file, and any Safe Wrappers applied — carries forward and accelerates a future full-adoption pass.
      - **If Safe Wrappers were applied, exercise the new `*Safe` variants.** The new code paths should be tested to ensure correctness.
      
      ### What Clients Get
      
      - Clients adopting `-fbounds-safety` see the annotated interface and get bounds checks at call sites
      - The compiler verifies at the client's call site that the pointer has at least `count` elements
      - Other clients that don't use `-fbounds-safety` see the same header with no effect — annotations are invisible without the flag
      
      ### What You Don't Get
      
      - No bounds checking inside your library's implementation
      - No compiler enforcement of annotation correctness within implementation files
      - Bugs in your implementation are not caught by `-fbounds-safety`
      
      ### Useful for Cross-Language Interop
      
      Header-only annotations also provide more information to the compiler for safer interop from other languages (e.g., Swift importing your C headers).
      
    • build-settings.md 2.9 KB
      # Build Settings for `-fbounds-safety`
      
      This document covers compiler flags, build system configuration, and related settings for enabling `-fbounds-safety`.
      
      ## Enabling `-fbounds-safety`
      
      ### Per-File Enablement (Recommended for Incremental Adoption)
      
      Most projects adopt `-fbounds-safety` incrementally, enabling it one file at a time as a per-file build flag. See [adoption-strategies.md](adoption-strategies.md) for the adoption workflow.
      
      ### Project-Wide Enablement (After Adoption Is Complete)
      
      Once adoption is complete across an entire target or project, you can enable `-fbounds-safety` globally. This is desirable because it controls enablement from a single location, making it easier to switch on or off.
      
      **Xcode:** Add the custom build setting `ENABLE_C_BOUNDS_SAFETY=YES`. This applies `-fbounds-safety` only to C files — it will not bleed onto C++, Objective-C, or Objective-C++ files (unlike adding the flag to project-level C flags directly, which would).
      
      **Other Build Systems:** Pass `-fbounds-safety` to Clang for each C source file.
      
      No additional link-time libraries are required. Clients (including non-bounds-safe ones) should be oblivious to the change.
      
      ## Useful Flags
      
      ### `-ferror-limit=0`
      
      Removes the limit on compiler errors. Useful during adoption to see all diagnostics at once rather than fixing errors one batch at a time.
      
      ### `-ffreestanding`
      
      For projects without access to a `strlen` implementation. When converting `__null_terminated` pointers to indexable, `-fbounds-safety` may insert a `strlen` call. The `-ffreestanding` flag makes the compiler generate a character-counting loop instead.
      
      ### `-fbounds-safety-unique-traps`
      
      Prevents trap merging in optimized builds. By default, the optimizer merges all traps in a function into one (to reduce code size), making it difficult to determine which specific bounds check failed. This flag preserves separate trap locations, making optimized-build debugging much easier.
      
      ### `-fbounds-safety-soft-traps=call-minimal`
      
      Enables soft trap mode. Soft traps log violations instead of terminating the program — the compiler emits calls to `__bounds_safety_soft_trap` instead of trap instructions, allowing execution to continue after a bounds check failure. This is useful during adoption to discover multiple issues in a single run rather than fixing them one at a time. After all files compile and all traps are fixed use of soft trap mode **must be removed** to actually get the security benefit.
      
      **Xcode:** Add the build setting `CLANG_BOUNDS_SAFETY_SOFT_TRAPS=call-minimal`. This enables soft trap mode for every source file that uses `ENABLE_C_BOUNDS_SAFETY`. For files where you manually pass `-fbounds-safety`, add the flag directly.
      
      **Other build systems:** Pass `-fbounds-safety-soft-traps=call-minimal` to every source file that uses `-fbounds-safety`.
      
      See [runtime-debugging.md](runtime-debugging.md) for more information on debugging with soft traps.
      
    • common-patterns-and-pitfalls.md 36.3 KB
      # Common Patterns and Pitfalls
      
      This document covers common patterns for working with `-fbounds-safety` and pitfalls encountered during real-world adoption.
      
      ## Common Patterns
      
      ### Using Local Variables to Avoid Assignment Restrictions
      
      When the compiler requires pointer and count to be assigned together (the "dependent variable" rule), introduce local variables:
      
      ```c
      // This causes an error — buf and count must be assigned together:
      void fill(int *__counted_by(count) buf, size_t count) {
          while (count-- > 0) {
              *buf = count;
              buf++;  // error: assignment to 'buf' requires corresponding assignment to 'count'
          }
      }
      
      // Fix: copy to local variables (implicitly __bidi_indexable):
      void fill(int *__counted_by(countOrig) bufOrig, size_t countOrig) {
          int *buf = bufOrig;
          size_t count = countOrig;
          while (count-- > 0) {
              *buf = count;
              buf++;  // OK — buf is __bidi_indexable, no external bounds to maintain
          }
      }
      ```
      
      ### Data Organization: Prefer Rows Over Columns
      
      When a struct contains pointer fields, prefer "row" organization (array of structs) over "column" organization (struct of arrays):
      
      ```c
      // Row organization (recommended) — flat pointers, easy to annotate:
      struct gpio_config {
          uint32_t cfg;
          uint32_t *__counted_by(intStatusCount) intStatus;
          uint32_t intStatusCount;
      };
      struct gpio_config configs[N];
      
      // Column organization (problematic) — nested pointers, hard to annotate:
      uint32_t **intStatusArray;  // cannot express __counted_by for inner pointers
      ```
      
      ### Rewriting Internal APIs
      
      When an internal function's signature has pointers that cannot be made safe using ABI-compatible bounds annotations (like `__counted_by` or `__sized_by`), the ABI-incompatible `__bidi_indexable` can be used to propagate bounds because the ABI doesn't need to be preserved. This is much preferable to using `__unsafe_indexable`.
      
      In this example, an internal function originally had an out-parameter with no bounds information. By using `__bidi_indexable`, bounds from the internal fixed-size buffer propagate to callers:
      
      ```c
      // Before: no bounds on out-parameter
      static int GetExtNext(Handle *H, uint8_t **Out);
      
      // After: __bidi_indexable propagates bounds from internal buffer
      static int GetExtNext(Handle *H, uint8_t *__bidi_indexable *Out) {
          ...
          // H->Buf is a fixed-size array (e.g., uint8_t Buf[256]).
          // Assigning it through a __bidi_indexable * out-parameter
          // gives the compiler array bounds automatically — no forge needed.
          *Out = H->Buf;
          ...
      }
      ```
      
      ### Using `__bidi_indexable` / `__indexable` in a Source File That Must Compile Without `-fbounds-safety`
      
      **Before reaching for this pattern, prune.** Check each `__bidi_indexable` / `__indexable` against [Redundant `__bidi_indexable` / `__indexable` Annotations](#redundant-__bidi_indexable--__indexable-annotations) below. Locals already default to `__bidi_indexable`, and casts on expressions that are already (or can implicitly become) `__bidi_indexable` don't need the annotation. If pruning leaves no remaining uses in this file, you don't need this pattern at all.
      
      **When this pattern applies (after pruning).** A `.c` file *still* uses `__bidi_indexable` (or `__indexable`) by name — on internal helper signatures, on local variable declarations where the annotation is load-bearing, or inside cast expressions where the annotation is load-bearing — and must also compile cleanly with `-fbounds-safety` off (e.g. for the two-commit-dance source-changes commit in [adoption-strategies.md](adoption-strategies.md)).
      
      **Pattern.** At the top of the `.c` file, after `#include <ptrcheck.h>`:
      
      ```c
      #if !__has_ptrcheck
      /* ptrcheck.h leaves these undefined when -fbounds-safety is off to force
       * compile errors on ABI-breaking uses in headers. In this .c file the
       * annotations only appear on static helpers (no ABI surface), so it is
       * safe to define them as no-ops here. */
      #define __bidi_indexable
      #define __indexable
      #endif
      ```
      
      **Constraints:**
      
      - **Never put this in a header file.** Headers are shared across translation units; silently no-op'ing an ABI-breaking attribute risks an ABI mismatch between a header that defines the fallback and a TU that doesn't.
      - **Only when the annotated declarations are not ABI-visible.** Static helpers and local variables are fine; an `extern` function in this `.c` file whose signature includes `__bidi_indexable` is not — its declaration in another TU would see a different ABI.
      - **Do not also add `#if __has_ptrcheck` guards around forge/conversion intrinsic call sites.** Those have fallbacks in `ptrcheck.h` (see [Unnecessary `#if __has_ptrcheck` Guards](#unnecessary-if-__has_ptrcheck-guards) below).
      
      ### Constant Bounds on Externally-Counted Pointers
      
      Examples below use `__counted_by(N)` for concreteness; the same reasoning applies to every externally-counted pointer kind: `__counted_by`, `__counted_by_or_null`, `__sized_by`, `__sized_by_or_null`, `__ended_by`.
      
      **Cardinal rule: derive `N` from what the function body alone provably accesses, including fixed offsets, fixed-size operations, bounds flowing through annotated callees, and the static type of an index variable the body doesn't narrow further. Not from caller data, allocation patterns, or format/protocol spec invariants the body doesn't enforce.**
      
      A constant `N` is correct only if the function body provably accesses at most `N` elements/bytes for every input — counting direct accesses, sequences, fixed-size operations (e.g. `memcpy(dst, src, 4)`), and bounds flowing through annotated callees. Specifically, `N` must **not** come from:
      
      - **Runtime contents of the input.** Example: `f(const Header *H, T *buf)` reads `buf[H->indices[k]]`; the reachable bound on `buf` depends on what values are in `H->indices` at runtime — pure data, not contract.
      - **A size/count attached to the input that the count-expression grammar can't reference directly.** Tempting when the real bound (e.g. `P->capacity`) is rejected by the grammar (see [Count Expression Grammar](language-overview.md#out-and-in-out-parameters-with-__counted_by)); substituting a constant ceiling is not a fix.
      - **Format/protocol invariants about valid inputs.** Reasoning "the spec caps it at `N`, so use `N`" ties the API to the format definition, not to what the function actually accesses.
      - **Allocation patterns of any particular caller.** Example: an in-tree caller declares `T buf[256]` on its stack and passes it in; reflecting that 256 into the public API encodes one caller's choice as if it were a contract.
      
      **Honest examples** — functions whose body unconditionally accesses a fixed set of indices/offsets, the same for every input:
      
      - Writing the four bytes of a fixed-length protocol header by assigning `header[0]..header[3]` → `__counted_by(4)`.
      - Always calling `memcpy(dst, src, 16)` against a fixed-layout block → `__sized_by(16)`.
      
      **Audit procedure** before writing any constant `N`:
      
      1. Open the function body; identify the highest index/byte offset the function can reach, across all paths and inputs.
      2. Complete: "the function genuinely accesses up to `<constant>` elements/bytes because ___". If the answer is the body's own behaviour — including the static type of an index the body doesn't narrow — the constant is fine. If it lands in any of the four categories above, the constant is wrong — go to the remedy below.
      
      **Remedy when the audit fires.** Branch on visibility:
      
      - **Public API** (declared in a published header / consumed by external clients): apply [Safe Wrappers for Public APIs](#safe-wrappers-for-public-apis) — the public function becomes a thin shim with its pointer parameter re-annotated `__unsafe_indexable`, delegating to a new `*Safe` variant that takes an explicit count.
      - **Internal** (`static`, or declared only in private headers): use ABI-incompatible annotations directly — see [Rewriting Internal APIs](#rewriting-internal-apis). `__bidi_indexable` propagates bounds from the caller with no count parameter; alternatively, add an explicit count and use dynamic `__counted_by(count)` / `__sized_by(count)`.
      
      **Anti-pattern walkthrough.** A function `void apply_lookup(const Header *H, const T lookup[])` declared in a public header, where the format spec restricts `H->indices[k]` to `[0, 16)`. Wrong adoption: `lookup[__counted_by(16)]`, reasoned from "the spec caps the index at 16." Audit step 2: "the function genuinely accesses up to 16 elements because the spec says so" — that's the format/protocol-invariants category, not the body's own behaviour (the body indexes via `uint8_t` and never narrows; if a corrupted `H->indices[k]` produced 17, the body would read `lookup[17]`). Audit fires; visibility = public → Safe Wrapper. The `*Safe(H, lookup, len)` variant lets the caller declare the actual table length, and `-fbounds-safety` then traps when the runtime index exceeds it — catching data corruption at the indexing site. Had this function been declared `static`, the internal remedy would apply instead.
      
      ### Safe Wrappers for Public APIs
      
      This pattern applies to **public APIs** (declared in shipped headers, consumed by external clients, ABI must be preserved). For internal-only signatures, [Rewriting Internal APIs](#rewriting-internal-apis) above is the simpler remedy. Use Safe Wrapper for a public function when any of these apply:
      
      - The natural bound is a struct field of another parameter (`->` and `.` are rejected in count expressions; see [Count Expression Grammar](language-overview.md#out-and-in-out-parameters-with-__counted_by))
      - The natural bound requires arithmetic on a dereferenced pointer (e.g. `*count + 1`, also rejected)
      - The natural bound requires calling a function that isn't marked `__attribute__((const))` — only const-attributed functions are accepted in count expressions, so anything with side effects or hidden state (e.g. a non-const `strlen`-style helper) can't be referenced
      - The natural bound is a function-local quantity not present in the existing public signature
      - A constant `__counted_by(N)` *appears* to fit but the actual access is bounded by a dynamic quantity — see [Constant Bounds on Externally-Counted Pointers](#constant-bounds-on-externally-counted-pointers) above
      - `__unsafe_indexable` is otherwise the only option
      
      Create a bounds-safe internal implementation and reduce the public function to a thin shim:
      
      1. Move all implementation logic into a new internal safe function
      2. The original public function becomes a thin shim that delegates to the safe version
      3. Internal callers call the safe function directly — never the legacy shim. *(Skip in header-only adoption — see [§3 Safe Wrapper retrofits](adoption-strategies.md#3-safe-wrapper-retrofits-if-any-captured) for why.)*
      4. Mark the legacy function's **declaration** with `__ptrcheck_unavailable_r(safe_function_name)` — this makes it unavailable in `-fbounds-safety` builds while keeping it available for non-adopted callers. The attribute only needs to be on the declaration, not the definition.
      
      **Example:**
      
      ```c
      // Header — mark legacy API unavailable in -fbounds-safety builds
      __ptrcheck_unavailable_r(UnionSafe)
      Result *Union(const Map *A, const Map *B,
                    Pixel *__unsafe_indexable trans);
      
      // Public safe version with explicit count
      Result *UnionSafe(const Map *A, const Map *B,
                        Pixel *__counted_by(transLen) trans, int transLen) {
          // full implementation here
      }
      
      // Legacy wrapper — forges and delegates
      Result *Union(const Map *A, const Map *B,
                    Pixel *__unsafe_indexable trans) {
          Pixel *safe = __unsafe_forge_bidi_indexable(
              Pixel *, trans, B->Count * sizeof(Pixel));
          return UnionSafe(A, B, safe, B->Count);
      }
      ```
      
      Internal callers use the safe version directly, never the legacy wrapper:
      
      ```c
      void MergeColorMaps(const Map *A, const Map *B,
                          Pixel *__counted_by(B->Count) trans) {
          // Calls UnionSafe directly — not Union
          Result *merged = UnionSafe(A, B, trans, B->Count);
          ...
      }
      ```
      
      **Header-only variant.** When the Safe Wrapper is being applied as part of *header-only* adoption (see [§3 Safe Wrapper retrofits](adoption-strategies.md#3-safe-wrapper-retrofits-if-any-captured)), the implementation file is **not** compiled with `-fbounds-safety`. Three adjustments to the shape above:
      
      - **Drop the forge in the legacy shim.** With the flag off in the impl, `__unsafe_indexable` and `__counted_by(...)` are both just plain pointers — passing the legacy parameter directly to the `*Safe` variant compiles cleanly. Add a forge **only** if the file is later switched to full adoption.
      - **Keep the annotations on the Safe variant's *definition*** so it matches the header declaration verbatim. Per [language-overview.md](language-overview.md) `ptrcheck.h` expands the annotations to empty when the flag is off, so they are inert at the impl's compile site — but they are required for redeclaration consistency and they keep the signature ready for full adoption later.
      - **Ensure `<ptrcheck.h>` is reachable in the implementation file.** The annotation macros (`__counted_by`, `__counted_by_or_null`, etc.) come from `ptrcheck.h`; without it the macros are undefined and the file won't compile even with `-fbounds-safety` off. Typically the impl already includes the public header you just annotated (which itself includes `ptrcheck.h`), so this is automatic — but if the impl gets its types from a private header that doesn't transitively pull in `ptrcheck.h`, add `#include <ptrcheck.h>` directly.
      
      Concretely, the legacy shim from the example becomes:
      
      ```c
      // Legacy wrapper — header-only mode, no forge
      Result *Union(const Map *A, const Map *B,
                    Pixel *__unsafe_indexable trans) {
          return UnionSafe(A, B, trans, B->Count);
      }
      ```
      
      The `UnionSafe` definition is unchanged from the full-adoption example.
      
      - No `__unsafe_forge_*` calls should be needed to satisfy the safe function's parameter and return types — the forge belongs in the legacy wrapper, not at internal call sites
      - Internal code must **never** call the legacy wrapper — always call the safe version directly
      - The legacy wrapper exists purely for API/ABI backwards compatibility
      - Forward-declare safe functions as `static` only if needed for ordering (e.g., mutual recursion between related safe functions)
      
      **Coordinating with the adoption workflow.** If you decide on a Safe Wrapper *during* the headers-first phase (Phase 1 in [adoption-strategies.md](adoption-strategies.md#1-headers-first)), do not retrofit it inline — Phase 1 is source-file-free, and the retrofit is intrinsically cross-file. Instead, create a per-item `Add Safe Wrapper for <funcName>` task per the [Capturing deferred Safe Wrapper retrofits](adoption-strategies.md#capturing-deferred-safe-wrapper-retrofits) sub-heading. Execution lands at different points depending on the adoption mode:
      
      - **Full adoption**: at [Step 5.1 Safe Wrapper retrofits](adoption-strategies.md#51-safe-wrapper-retrofits), after the project switches to target-level `ENABLE_C_BOUNDS_SAFETY`. The `5.1 Commit Safe Wrapper batch` umbrella task is the single commit point. Under partial-target adoption (some file skipped per [Skipping a file's enablement](adoption-strategies.md#skipping-a-files-enablement)), Step 4 is bypassed and Safe Wrappers still apply at §5.1 — see §5.1's verify-step caveat for what changes.
      - **Header-only adoption**: at [§3 Safe Wrapper retrofits (if any captured)](adoption-strategies.md#3-safe-wrapper-retrofits-if-any-captured), gated on a user opt-in stop. On approval, the per-items are applied with the "switch internal callers" step skipped — header-only deliberately leaves implementation call sites untouched. The `3b. Commit Safe Wrapper batch` umbrella is the single commit point.
      
      ### Calling Non-Adopted Libraries
      
      ABI-visible pointers in SDK/system headers are `__unsafe_indexable` by default. When consuming return values or struct fields from these libraries:
      
      - Passing data in: all pointers implicitly convert to `__unsafe_indexable` — no issues
      - Getting data out: use `__unsafe_forge_bidi_indexable` or `__unsafe_forge_single` to create safe pointers
      
      ```c
      // stdin from stdio.h is __unsafe_indexable in system headers:
      FILE *f = __unsafe_forge_single(FILE *, stdin);
      ```
      
      Include external/third-party headers as system headers to prevent compilation errors (they'll default to `__unsafe_indexable`).
      
      ### String Variables and `__null_terminated`
      
      #### Choosing between `__null_terminated` and `__bidi_indexable`
      
      When a variable is used primarily as a C string — passed to string functions like `strlen`, `strtok`, `strcpy`, or iterated with `++p` — consider declaring it as `__null_terminated`. This lets the variable work directly with string functions without conversion at each use site.
      
      Apple's Libc string functions (`strlen`, `strtok`, `strchr`, etc.) accept and return `__null_terminated` pointers. Declaring a string variable as `__null_terminated` lets you use these functions directly and avoids repeated `__null_terminated` to/from `__bidi_indexable` conversions, which each require a linear scan of the string to find the terminator:
      
      ```c
      const char *__null_terminated cp;
      cp = strtok(buf, "\n");  // strtok returns __null_terminated
      strlen(cp);               // no conversion needed
      strcpy(dst, cp);          // no conversion needed
      ```
      
      If a non-adopted function returns a pointer you know is null-terminated but the return type is not annotated, use `__unsafe_forge_null_terminated` to establish the annotation once at the assignment rather than converting at every downstream use.
      
      **When NOT to use `__null_terminated`:** If the code needs pointer arithmetic beyond `+1` (e.g., `p += n`, `p[i]` with arbitrary `i`), use `__bidi_indexable` instead. `__null_terminated` only supports `+0` and `+1` arithmetic.
      
      **When you need both:** If a string needs both random-access indexing AND string API calls, keep two pointers to the same data — one `__null_terminated` for string APIs, one `__bidi_indexable` (via `__null_terminated_to_indexable`) for indexing. They must be manually kept in sync if either is advanced:
      
      ```c
      void process(const char *__null_terminated input) {
          const char *__null_terminated nt_ptr = input;
          const char *idx_ptr = __null_terminated_to_indexable(input);
      
          size_t len = strlen(nt_ptr);
      
          // Random access via indexable pointer
          for (size_t i = 0; i < len; i++) {
              if (idx_ptr[i] == ':')
                  printf("colon at offset %zu\n", i);
          }
      
          // String API via null-terminated pointer
          const char *__null_terminated found = strchr(nt_ptr, ':');
          if (found)
              printf("found: %s\n", found);
      }
      ```
      
      #### Converting to `__null_terminated` cheaply
      
      When converting from `__bidi_indexable` back to `__null_terminated`, `__unsafe_null_terminated_from_indexable(P)` must scan the string to find the terminator (O(n)). If you already know where the terminator is, pass it as a second argument for an O(1) conversion:
      
      ```c
      char *buf = (char *)malloc(len + 1);
      memcpy(buf, src, len);
      buf[len] = '\0';
      
      // O(n): scans buf to find the terminator
      return __unsafe_null_terminated_from_indexable(buf);
      
      // O(1): we know the terminator is at buf[len]
      return __unsafe_null_terminated_from_indexable(buf, &buf[len]);
      ```
      
      ### Choosing Between `__indexable` and `__bidi_indexable`
      
      - `__indexable` is 2 register words — passed by register, lower overhead
      - `__bidi_indexable` is 3 register words — passed by stack copy, higher overhead
      - Conversions between them are implicit
      
      **Guidance:**
      - For function arguments/returns that must use wide pointers, prefer `__indexable`
      - Within functions, use the default `__bidi_indexable` — no performance penalty for local use
      - Don't use `__indexable` as a security measure; `__bidi_indexable` already prevents out-of-bounds below the lower bound
      - When possible, prefer external bounds annotations (`__counted_by`, etc.) over either wide pointer type
      
      ## Common Pitfalls
      
      These are common issues encountered during real-world adoption, along with recommended solutions.
      
      ### Casting to a Larger Struct Type Traps at Runtime
      
      **Problem:** Casting a pointer to a struct type that is larger than the pointed-to memory will trap when any field is accessed via `->`, even if the specific field being accessed is within bounds.
      
      ```c
      struct element_t {
          uint8_t id;
          uint8_t len;
          uint8_t data[10]; // sizeof(element_t) == 12
      };
      
      uint8_t buffer[8];
      struct element_t *cast_buffer = (struct element_t *)buffer;
      cast_buffer->id; // TRAPS — even though id is at offset 0
      ```
      
      **Why:** When accessing a struct field via `->`, `-fbounds-safety` checks that the *entire* struct is within bounds, not just the field being accessed. This prevents intra-object overflow and avoids undefined behavior.
      
      **Fix:** Use a smaller header struct that fits within the actual buffer size, or parse by reading fields individually rather than casting the buffer:
      
      ```c
      struct header {
          uint8_t id;
          uint8_t len;
      };
      
      struct header *hdr = (struct header *)buffer;
      if (hdr->id == EXPECTED_TYPE) {
          // Now safe to access more data knowing the type
      }
      ```
      
      ### Casting Between `__single` Pointers Can Widen Bounds
      
      **Problem:** Casting between `__single` pointers of different struct types can silently increase the assumed bounds, because `__single` assumes one valid element of the *destination* type.
      
      ```c
      struct small { int a; };           // 4 bytes
      struct large { int a; int b; };    // 8 bytes
      
      struct small s = {0};
      struct small *__single r = &s;
      struct large *__single q = (struct large *)r;
      q->b; // NO trap — but accesses memory beyond 's'!
      ```
      
      **Why:** A `__single` pointer assumes it points to one valid element of its type. Casting to a larger type changes that assumption. This differs from `__bidi_indexable`, which preserves the original bounds and would trap.
      
      **Fix:** Be careful with `__single` pointer casts between types of different sizes. If you need the bounds-checked behavior, copy to a local variable (which becomes `__bidi_indexable`) before casting.
      
      ### Passing `__counted_by`/`__sized_by` Count to Non-Adopted Function
      
      **Problem:** Passing the count variable of a `__counted_by`/`__sized_by` pair to a non-adopted function produces an error about unsynchronized dynamic count pointers.
      
      ```c
      void do_work(void *__sized_by(*output_len) output, size_t *output_len) {
          // unannotated_func is not annotated with -fbounds-safety
          unannotated_func(output, output_len);
          // error: passing 'output_len' referred to by '__sized_by' to a parameter
          // that is not referred to by the same attribute
      }
      ```
      
      The signature shape above — `*__sized_by(*output_len) output, size_t *output_len` — is the fill-in-place in-out pattern covered in [language-overview.md](language-overview.md#out-and-in-out-parameters-with-__counted_by).
      
      **Why:** `-fbounds-safety` cannot guarantee the non-adopted function won't modify `*output_len` in a way that desynchronizes it from the pointer's actual bounds.
      
      **Fix:** Use a local copy of the count variable:
      
      ```c
      void do_work(void *__sized_by(*output_len) output, size_t *output_len) {
          size_t local_len = *output_len;
          unannotated_func(output, &local_len);
          *output_len = local_len;
      }
      ```
      
      ### Slicing a `__bidi_indexable` Buffer
      
      **Problem:** You have a `__bidi_indexable` pointer and need to create a sub-range (a slice) with tighter bounds.
      
      **Fix:** Assign the pointer through a function parameter with `__sized_by` or `__counted_by` to create new bounds:
      
      ```c
      void *__bidi_indexable slice(void *__sized_by(n) p, size_t n) {
          return p;
      }
      
      // Usage:
      void *__bidi_indexable full_buffer = ...;
      void *__bidi_indexable sub = slice((char *)full_buffer + offset, length);
      ```
      
      ### Annotating Malloc-Like Functions
      
      **Problem:** Custom allocation functions need bounds annotations on their return value.
      
      **Fix:** Use `__sized_by_or_null` on the return type (since allocation can fail and return NULL):
      
      ```c
      uint8_t *__sized_by_or_null(size) _Nullable
      my_allocate(size_t size);
      ```
      
      If the function has the `alloc_size` attribute, `-fbounds-safety` may infer bounds automatically.
      
      ### Working with `__counted_by` Parameters
      
      **Problem:** Pointer arithmetic or reassignment on `__counted_by` parameters requires keeping the pointer and count in sync, which is cumbersome.
      
      **Fix:** Copy both the parameter and its count to local variables at the start of the function. The local pointer becomes `__bidi_indexable` and the local count is no longer a dependent variable:
      
      ```c
      void process(int *__counted_by(count) buf_param, size_t count) {
          int *buf = buf_param; // buf is now __bidi_indexable
          size_t n = count;     // n is no longer tied to buf_param
          while (n-- > 0) {
              *buf = 0;
              buf++; // OK — no need to keep count in sync
          }
      }
      ```
      
      ### Passing Arrays to `__counted_by` Parameters
      
      **Problem:** Using `&array` instead of `array` when passing to a `__counted_by` parameter causes a type mismatch.
      
      ```c
      uint32_t arr[10];
      void process(uint32_t *__counted_by(size) data, size_t size);
      
      process(&arr, 10);  // error: incompatible pointer types
      process(arr, 10);   // OK — array decays to pointer
      ```
      
      **Why:** `&arr` has type `uint32_t (*)[10]` (pointer to array), not `uint32_t *` (pointer to element). This is standard C behavior, not specific to `-fbounds-safety`.
      
      **Fix:** Use `arr` directly (array-to-pointer decay) or `&arr[0]`.
      
      ### Unnecessary Forges on Allocator Returns
      
      **Problem:** Using `__unsafe_forge_bidi_indexable` on the return value of `malloc`/`calloc`/`realloc` (or any allocator with `alloc_size`) when assigning to a `__counted_by` or `__sized_by` field.
      
      ```c
      struct container {
          int count;
          Item *__counted_by(count) items;
      };
      
      // WRONG — forge is redundant
      Item *new_items = (Item *)realloc(c->items, newCount * sizeof(Item));
      c->count = newCount;
      c->items = __unsafe_forge_bidi_indexable(
          Item *, new_items, (size_t)newCount * sizeof(Item));
      ```
      
      **Why:** Allocators with `alloc_size` already return `__sized_by_or_null` pointers. Casting to a typed pointer gives a `__bidi_indexable` with correct bounds. The `__bidi_indexable` → `__counted_by(N)` assignment is implicit with a bounds check (per the conversion table). The forge re-derives bounds the compiler already knows.
      
      **Fix:** Assign the allocator result directly:
      
      ```c
      Item *new_items = (Item *)realloc(c->items, newCount * sizeof(Item));
      c->count = newCount;
      c->items = new_items;  // compiler inserts bounds check automatically
      ```
      
      **Rule of thumb:** Only forge when the pointer source has no bounds information (e.g., `__unsafe_indexable` from a non-adopted API). Never forge a pointer from an annotated allocator — one with `alloc_size`, `__sized_by_or_null`, or similar return-type annotations. Standard library `malloc`/`calloc`/`realloc` have `alloc_size`; custom allocators only carry bounds if explicitly annotated.
      
      ### Unnecessary Forges on Constant-Sized Arrays
      
      **Problem:** Using `__unsafe_forge_bidi_indexable` to "give bounds" to a constant-sized array `T arr[N]`. Example shape — a struct member accessed via `->`:
      
      ```c
      struct Frame { uint8_t buf[256]; };
      
      // WRONG — forge is redundant
      void process(struct Frame *p) {
          uint8_t *view = __unsafe_forge_bidi_indexable(
              uint8_t *, p->buf, sizeof(p->buf));
          /* ... use view ... */
      }
      ```
      
      **Why:** Under `-fbounds-safety`, a constant-sized array decays to a `T *__counted_by(N)` pointer when used as a value. This is true for every source — function parameter, local, global, **and struct member** — so `p->buf` already carries the bounds `[&p->buf[0], &p->buf[N])`. Assigning to a `T *` local produces `__bidi_indexable` with those bounds; the forge re-derives them.
      
      **Fix:** Drop the forge and assign directly:
      
      ```c
      void process(struct Frame *p) {
          uint8_t *view = p->buf;  // __bidi_indexable with array bounds
      }
      ```
      
      The same rule applies to `T local[N]`, a global `T g_arr[N]`, and a parameter `void f(T arr[N])` (which decays to `T *__counted_by(N)` per [function-prototype array decay](language-overview.md#external-bounds-annotations)). See also [Deriving Bounds from Objects](language-overview.md#deriving-bounds-from-objects) and the [When NOT to Forge](language-overview.md#when-not-to-forge) checklist.
      
      ### Forging a `__single` Pointer Means the Source Is Misannotated
      
      **Problem:** You find yourself writing `__unsafe_forge_bidi_indexable(T *, p, size)` (or another widening forge) where `p` is a `__single` pointer — either explicitly annotated `__single` or implicitly defaulted (ABI-visible struct fields and function parameters usually default to `__single`; see [Default Pointer Attributes](language-overview.md#default-pointer-attributes) for the `const char *` → `__null_terminated` exception). The forge papers over the underlying problem: the source annotation claims `p` points to one object, but the code's behaviour proves it points to a buffer. Two common shapes:
      
      - **Struct field:** `T *field` (implicit `__single`) on a struct, where consumer code forges a bidi view from `field` using sibling-field arithmetic for the size.
      - **Function parameter:** `T *p` (implicit `__single`) on a function, where the body forges a bidi view from `p` to read buffer contents — common shape: length-prefixed buffers where the first byte encodes the payload length.
      
      **Fix:** Correct the source annotation; do not paper over with forges. Order of preference:
      
      1. An externally counted bounds annotation if the bound is expressible in the count grammar — `__counted_by(<expr>)` / `__sized_by(<expr>)` / `__counted_by_or_null(<expr>)` / `__sized_by_or_null(<expr>)` / `__null_terminated`. (For struct fields, also consider the [FAM exception](language-overview.md#count-expression-restrictions); for public functions whose bound needs an extra parameter, consider [Safe Wrappers for Public APIs](#safe-wrappers-for-public-apis).)
      2. If the bound exists but cannot be expressed (e.g. it's encoded in the buffer itself like a length-prefixed block, or it requires arithmetic on nested struct fields that the count grammar rejects), use **explicit `__unsafe_indexable`** on the source. The forge at use sites is then expressing real information about an honestly-unsafe pointer.
      
      **Example — wrong (implicit `__single` + forge at use site, struct-field shape):**
      
      ```c
      typedef struct Frame {
          Dimensions Dim;          /* contains Width, Height */
          uint8_t *Pixels;         /* implicit __single — wrong */
      } Frame;
      
      void process(Frame *f) {
          size_t n = (size_t)f->Dim.Width * f->Dim.Height;
          uint8_t *buf = __unsafe_forge_bidi_indexable(uint8_t *, f->Pixels, n);
          /* ... use buf ... */
      }
      ```
      
      **Right (explicit `__unsafe_indexable`, same forge at use site):**
      
      ```c
      typedef struct Frame {
          Dimensions Dim;
          uint8_t *__unsafe_indexable Pixels;  /* bound = Dim.Width * Dim.Height; not expressible */
      } Frame;
      
      void process(Frame *f) {
          size_t n = (size_t)f->Dim.Width * f->Dim.Height;
          uint8_t *buf = __unsafe_forge_bidi_indexable(uint8_t *, f->Pixels, n);
          /* same forge, but now describing an honestly-unsafe pointer */
      }
      ```
      
      **Example — wrong (function-parameter shape, length-prefixed buffer):**
      
      ```c
      /* Public API: CodeBlock[0] is the payload length in bytes. */
      int put_block(File *f, const uint8_t *CodeBlock);   /* implicit __single — wrong */
      
      int put_block(File *f, const uint8_t *CodeBlock) {
          const uint8_t *view = __unsafe_forge_bidi_indexable(
              const uint8_t *, CodeBlock, 256);
          uint8_t len = view[0];
          return write_bytes(f, view, len + 1);
      }
      ```
      
      **Right (apply [Safe Wrappers for Public APIs](#safe-wrappers-for-public-apis)):**
      
      ```c
      // Header — legacy shim with __unsafe_indexable parameter, plus a new
      // count-aware variant. See Safe Wrappers for Public APIs for the full
      // 4-step pattern (including __ptrcheck_unavailable_r on the shim).
      __ptrcheck_unavailable_r(put_block_safe)
      int put_block(File *f, const uint8_t *__unsafe_indexable CodeBlock);
      
      int put_block_safe(File *f, const uint8_t *__counted_by(len) CodeBlock,
                         size_t len);
      
      // .c — implementation lives in the safe variant.
      int put_block_safe(File *f, const uint8_t *__counted_by(len) CodeBlock,
                         size_t len) {
          return write_bytes(f, CodeBlock, len);
      }
      
      // .c — legacy shim reads the length prefix and delegates.
      int put_block(File *f, const uint8_t *__unsafe_indexable CodeBlock) {
          size_t len = (size_t)CodeBlock[0] + 1;
          const uint8_t *safe = __unsafe_forge_bidi_indexable(
              const uint8_t *, CodeBlock, len);
          return put_block_safe(f, safe, len);
      }
      ```
      
      **Why it matters:** With the implicit `__single` version, any direct arithmetic or indexing on the source pointer would get a compile-time error ("arithmetic on `__single` pointer") — which forces callers to forge anyway — *but* the declared type still lies to anyone reading the header (and to any analysis tooling). The explicit `__unsafe_indexable` version produces the same compile-time discipline at consumers (they must forge to do arithmetic) while communicating accurate information about the data shape.
      
      **Don't reach for `__unsafe_indexable` when the bound can be expressed in the count grammar.** Order is: an externally counted annotation (`__counted_by` / `__sized_by` / `__null_terminated`) when the bound fits the grammar → `__single` (truly single-object) → `__unsafe_indexable` (last resort). If the only block to expressing the bound is "the count is a sibling parameter you'd have to add to the signature", a Safe Wrapper is the right answer for a public function — see [Safe Wrappers for Public APIs](#safe-wrappers-for-public-apis).
      
      ### Unnecessary `#if __has_ptrcheck` Guards
      
      **Problem:** It is tempting to wrap every bounds-safety-flavoured call site (`__unsafe_forge_bidi_indexable`, `__null_terminated_to_indexable`, `__unsafe_null_terminated_from_indexable`, etc.) in `#if __has_ptrcheck` / `#else` blocks "in case `-fbounds-safety` is off". This over-guards.
      
      **Fix:** Don't guard. `ptrcheck.h` provides flag-off fallbacks for every forge intrinsic and conversion macro — they expand to plain C casts (`((T)(P))`) or pointer pass-throughs (`(P)`) when `-fbounds-safety` is off. Code using them compiles unguarded in both modes.
      
      **Example — wrong:**
      
      ```c
      #if __has_ptrcheck
      uint8_t *buf = __unsafe_forge_bidi_indexable(uint8_t *, raw_ptr, size);
      #else
      uint8_t *buf = raw_ptr;
      #endif
      ```
      
      **Example — right:**
      
      ```c
      uint8_t *buf = __unsafe_forge_bidi_indexable(uint8_t *, raw_ptr, size);
      ```
      
      The forge expands to `((uint8_t *)raw_ptr)` when the flag is off, which is exactly what the `#else` branch was doing manually.
      
      **The one exception.** Any textual occurrence of `__bidi_indexable` or `__indexable` in source — whether as an attribute on a declaration, on a function parameter, on a local variable, or inside a cast expression — *does* need either a `#if __has_ptrcheck` guard or the per-file fallback `#define` documented in [Using `__bidi_indexable` / `__indexable` in a Source File That Must Compile Without `-fbounds-safety`](#using-__bidi_indexable--__indexable-in-a-source-file-that-must-compile-without--fbounds-safety). The fallback `#define` approach scales better than per-site guards when there are many uses in one file.
      
      ### Redundant `__bidi_indexable` / `__indexable` Annotations
      
      **Problem:** Writing `__bidi_indexable` (or `__indexable`) explicitly is redundant whenever the surrounding context already provides one. Two common shapes:
      
      - On a local variable declaration whose initializer is already a `__bidi_indexable` — locals also default to `__bidi_indexable` (see [language-overview.md §Quick Reference](language-overview.md#quick-reference-pointer-kinds-and-bounds-annotations)), so the annotation is doubly redundant.
      - In a cast on an expression that already evaluates to a `__bidi_indexable` (e.g. the result of `__unsafe_forge_bidi_indexable`) or that can be implicitly converted to one (e.g. a `__sized_by_or_null` return from an annotated allocator like `malloc`).
      
      **Fix:** Drop the annotation.
      
      **Examples — wrong:**
      
      ```c
      const char *__bidi_indexable foo = NULL;
      int *buf = (int *__bidi_indexable)__unsafe_forge_bidi_indexable(int *, raw, size);
      int *buf2 = (int *__bidi_indexable)malloc(n * sizeof(int));
      ```
      
      **Right:**
      
      ```c
      const char *foo = NULL;
      int *buf = __unsafe_forge_bidi_indexable(int *, raw, size);
      int *buf2 = malloc(n * sizeof(int));
      ```
      
      **Why it matters:** Beyond verbosity, each explicit `__bidi_indexable` you write forces the file to need either a `#if __has_ptrcheck` guard or a per-file fallback `#define` to build with the flag off (see [Using `__bidi_indexable` / `__indexable` in a Source File That Must Compile Without `-fbounds-safety`](#using-__bidi_indexable--__indexable-in-a-source-file-that-must-compile-without--fbounds-safety)) — costs you pay for no benefit, since the surrounding context already provides the same pointer kind.
      
    • language-overview.md 39.3 KB
      # `-fbounds-safety` Language Overview
      
      This document describes the `-fbounds-safety` language model — a C language extension that enforces bounds safety through compiler-inserted bounds checks, compile-time restrictions on unsafe pointer operations, and programmer-provided bounds annotations.
      
      `-fbounds-safety` mostly differs from regular C in how it handles pointers. In C, a pointer is a *point* in memory that knows its start but not its end. The end must be communicated externally with no enforced conventions — errors are common and can escalate to an attacker taking full control of a device. With `-fbounds-safety`, a pointer is a *range* of memory that knows both its start and its end. The compiler inserts bounds checks to downgrade security bugs into mere logic errors, similar to how Swift protects against out-of-bounds array access.
      
      The bounds annotations and builtin functions described in this document become available after including the `ptrcheck.h` toolchain header.
      This header should be included unconditionally, even in code that builds without `-fbounds-safety` because we can assume AppleClang. `ptrcheck.h` provides flag-off fallback definitions for **both** the bounds annotations (`__counted_by`, `__sized_by`, `__null_terminated`, `__single`, etc.) **and** the forge/conversion intrinsics (`__unsafe_forge_*`, `__null_terminated_to_indexable`, `__unsafe_null_terminated_from_indexable`, etc.). When the flag is off, annotations expand to empty and intrinsics expand to plain C casts or pointer pass-throughs, so source using them compiles unchanged. The **only** exceptions are the ABI-breaking attributes `__bidi_indexable` and `__indexable` (and their `__ptrcheck_abi_assume_*` cousins), which are deliberately left undefined so that misuse in a header produces a compile error rather than a silent ABI break. Consequently, the only code that needs `#if __has_ptrcheck` guarding (or a per-`.c`-file fallback `#define`) is code that names those two attributes by token — see [Using `__bidi_indexable` / `__indexable` in a Source File That Must Compile Without `-fbounds-safety`](common-patterns-and-pitfalls.md#using-__bidi_indexable--__indexable-in-a-source-file-that-must-compile-without--fbounds-safety) for the pattern.
      
      
      ## Quick Reference: Pointer Kinds and Bounds Annotations
      
      | Pointer Kind | Description | ABI Compatible | Default For |
      |---|---|---|---|
      | `__single` | Points to exactly one element or NULL. No arithmetic allowed. | Yes | ABI-visible pointers (params, struct fields, globals) |
      | `__bidi_indexable` | Wide pointer with lower bound, upper bound, and current value. Full arithmetic support. | No | ABI-hidden pointers (local variables) |
      | `__indexable` | Wide pointer with upper bound and current value. Forward arithmetic only. | No | (explicit only) |
      | `__unsafe_indexable` | No bounds, no checks. Escape hatch for interop with non-adopted code. | Yes | System/SDK headers without `-fbounds-safety` |
      | `__counted_by(N)` | N elements at pointer. E.g. `int *__counted_by(count) buf` | Yes | (explicit only) |
      | `__sized_by(N)` | N bytes at pointer. E.g. `void *__sized_by(size) buf` | Yes | (explicit only) |
      | `__ended_by(P)` | Range from pointer to P. E.g. `int *__ended_by(end) begin` | Yes | (explicit only) |
      | `__counted_by_or_null(N)` | Like `__counted_by` but allows NULL | Yes | (explicit only) |
      | `__sized_by_or_null(N)` | Like `__sized_by` but allows NULL | Yes | (explicit only) |
      | `__null_terminated` | Points to memory terminated by 0 as the sentinel value. Arithmetic limited to +0 and +1. | Yes | ABI-visible `const char *` pointers |
      | `__terminated_by(T)` | Points to memory terminated by sentinel value T. Arithmetic limited to +0 and +1. | Yes | (explicit only) |
      
      ## ABI Compatibility and ABI Visibility
      
      By establishing conventions for tying a pointer with its length, bounds-safe code remains ABI-compatible with bounds-unsafe code. `-fbounds-safety` enforces conventions on how to tie a pointer with its length, but to maintain maximum flexibility, it changes pointers that are hidden from the ABI.
      
      There are two categories of pointers:
      
      - **ABI-visible**: function arguments and returns, global variables, structure fields — things you would commonly put in header files
      - **ABI-hidden**: essentially only some local variables
      
      > **Only the top-level pointer is considered ABI-hidden.** For instance, in a function body, `element_t *p` creates an ABI-hidden pointer. But `element_t **p` declares an ABI-hidden pointer to an ABI-visible pointer, since the second-level pointer may have an ABI-visible source.
      
      ```c
      struct foo {
          int *bar; // visible
          int **baz; // visible pointer to a visible pointer
      };
      
      int *bar; // visible
      
      int * // visible
      baz(
          int *frob  // visible
      ) {
          int *nicate; // hidden
          int **qwop; // hidden pointer to a visible pointer
      }
      ```
      
      `-fbounds-safety` changes ABI-hidden pointers to be **bidirectionally indexable** — a wide pointer containing three components:
      
      - a current pointer value
      - a lower bound
      - an upper bound
      
      When you do pointer arithmetic on a bidirectionally indexable pointer, the only immediate check is that the operation did not overflow. There is no immediate bounds check — it is not an error to create an out-of-bounds pointer, and you can bring it back in bounds later. Bounds checks occur when: (1) the pointer is about to be dereferenced, or (2) the bounds are about to be stripped.
      
      `-fbounds-safety` changes ABI-visible pointers to be **single** by default — a compile-time error to do arithmetic on them. Single pointers have the same size and layout as regular C pointers, maintaining ABI compatibility.
      
      **Recommendation:** Stick to the default bidirectionally indexable pointers for local variables. Copy parameters to local variables to convert them to bidirectionally indexable pointers when needed.
      
      ## Attribute Placement on Multi-Level Pointers
      
      Every pointer/bounds attribute — `__single`, `__bidi_indexable`, `__indexable`, `__unsafe_indexable`, `__null_terminated`, `__terminated_by`, `__counted_by`, `__counted_by_or_null`, `__sized_by`, `__sized_by_or_null`, `__ended_by` — attaches to **the `*` that immediately precedes it**, not to "the pointer variable". On a single-pointer declaration this rarely matters, but on multi-level pointers the position of the attribute changes the meaning entirely:
      
      | Declaration                          | Parsed as                         | Meaning                                                                          |
      |--------------------------------------|-----------------------------------|----------------------------------------------------------------------------------|
      | `int *__single *p`                   | inner `*__single`, outer default  | pointer to (`int *__single`)                                                     |
      | `int **__single p`                   | inner default, outer `*__single`  | `__single` pointer to `int *`                                                    |
      | `int *__counted_by(*n) *p`           | inner counted, outer default      | pointer to a counted `int *` — the **OUT / IN-OUT** shape                        |
      | `int **__counted_by(n) p`            | inner default, outer counted      | counted array of `n` `int *` — an **array of pointers**                          |
      | `int *__single *__counted_by(*n) p`  | inner `__single`, outer counted   | real SDK form (see `malloc_get_all_zones` in `<malloc/malloc.h>`)                |
      
      Compiler diagnostics reflect this parse verbatim: writing `int **__bidi_indexable p` yields a type printed as `int *__single *__bidi_indexable`, with the inner `*` taking the default attribute.
      
      For out- and in-out-parameter patterns built on this rule, see [Out and In-Out Parameters with `__counted_by`](#out-and-in-out-parameters-with-__counted_by).
      
      ## Indexability Kinds
      
      There are 4 kinds of pointers with internal bounds. The specifier goes after the star it modifies (see "Attribute Placement on Multi-Level Pointers" above): `element_t *__bidi_indexable p`.
      
      ### `__bidi_indexable`
      
      Bidirectionally indexable pointers support arithmetic that both increases or decreases the current value. They have a current pointer value, lower bound, and upper bound. Bounds values are immutable — arithmetic only modifies the current value.
      
      Arithmetic is only a runtime error when the pointer value overflows. Bidirectionally indexable pointers are **not** ABI-compatible with C pointers.
      
      ### `__indexable`
      
      Forward-indexable pointers support arithmetic that increases the current value. They have a current pointer value and an upper bound. It is a compile-time error to add a negative value to a forward-indexable pointer. It is a runtime error if arithmetic results in a value smaller than the starting value.
      
      Forward-indexable pointers are **not** ABI-compatible with C pointers, but they are smaller than `__bidi_indexable` — eligible to be passed by registers on x86_64 and AArch64.
      
      ### `__single`
      
      Single pointers require the pointer is either `NULL` or a pointer to one valid element. It is a compile-time error to perform arithmetic on a `__single` pointer.
      
      Single pointers **are** ABI-compatible with C pointers.
      
      ### `__unsafe_indexable`
      
      Unsafely indexable pointers are an **unsafe escape hatch** — they have no bounds checks and act just like C pointers. They cannot convert to safe pointer kinds. They **are** ABI-compatible with C pointers.
      
      Use only when you can separately verify safety, or to interoperate with libraries that don't use `-fbounds-safety`. Before reaching for `__unsafe_indexable`, consider the safer alternatives described in the `__unsafe_indexable` subsection under [Escape Hatches](#escape-hatches).
      
      ### Accessing Pointer Bounds
      
      From code that enables `-fbounds-safety`, you can access a pointer `p`'s bounds:
      
      - Current value: reference `p` directly
      - Lower bound: `__ptr_lower_bound(p)`
      - Upper bound: `__ptr_upper_bound(p)`
      
      ```c
      int array[50];
      int *p = array + 5;
      int *lower = __ptr_lower_bound(p); // current value = &array[0]
      int *upper = __ptr_upper_bound(p); // current value = &array[50]
      ```
      
      ### Converting Between Indexable Pointers
      
      Conversions between the different indexable pointer types work as follows (in pseudocode; `lower`, `current` and `upper` are not directly accessible):
      
      | From/To | `__bidi_indexable` | `__indexable` | `__single` | `__unsafe_indexable` |
      |---|---|---|---|---|
      | **`__bidi_indexable`** | trivial | bounds check, then: indexable.current = bidi.current, indexable.upper = bidi.upper | bounds check, then: single.current = bidi.current | unsafe.current = bidi.current |
      | **`__indexable`** | bidi.lower = indexable.current, bidi.current = indexable.current, bidi.upper = indexable.upper | trivial | bounds check, then: single.current = indexable.current | unsafe.current = indexable.current |
      | **`__single`** | bidi.lower = single.current, bidi.current = single.current, bidi.upper = &single.current[1] | indexable.current = single.current, indexable.upper = &single.current[1] | trivial | unsafe.current = single.current |
      | **`__unsafe_indexable`** | compile-time error | compile-time error | compile-time error | trivial |
      
      ### Default Pointer Attributes
      
      The default for ABI-visible pointers changes based on context:
      
      - **In system/SDK headers**: the default is `__unsafe_indexable`
      - **In all other files**: the default is `__single`, except if the type is `const char*` in which case the attribute is `__null_terminated`.
      
      This can be changed using `__ptrcheck_abi_assume_single()` at the top of a file. If your project exports headers and has adopted `-fbounds-safety`, add this directive so clients know to treat it as a bounds-safe header. This macro is a pragma that **only affects the current file** (i.e. subsequent includes are not affected).
      
      ## External Bounds Annotations
      
      For C APIs that pass a pointer and a length, `-fbounds-safety` supports annotations that control how to fetch bounds from another value in the same scope:
      
      - **`__counted_by(X)`**: X counts how many objects are available (cannot apply to `void *`)
      - **`__sized_by(X)`**: X counts how many bytes are available (can apply to `void *`)
      - **`__ended_by(P)`**: P is a pointer marking one-past-the-end of the range
      
      Use `__counted_by` for arrays (including byte arrays), and `__sized_by` for single objects of variable size.
      
      Note `__counted_by` and `__sized_by` do not allow the pointer to be `NULL` unless the count is `0`. To allow the pointer
      to be `NULL` for any count value use `__counted_by_or_null` or `__sized_by_or_null` instead.
      
      ### `__counted_by_or_null` and `__sized_by_or_null`
      
      These variants allow the pointer to be NULL with an arbitrary count/size. Useful for functions like `malloc` that may return NULL:
      
      ```c
      void *__sized_by_or_null(size) malloc(size_t size);
      ```
      
      The bounds check first checks whether the pointer is NULL; if so, the size is ignored.
      
      ### Usage Examples
      
      ```c
      // variables:
      int count;
      int *__counted_by(count) elems;
      
      // fields:
      struct my_range {
          int *__ended_by(end) begin;
          int *end;
      };
      
      // parameters:
      void foo(int count, int *__counted_by(count) elems);
      void bar_counted(int *__counted_by(count) elems, int count);
      
      // return value:
      void *__sized_by(n) malloc(size_t n);
      ```
      
      Array types decay to counted pointers in function prototypes:
      
      ```c
      int baz(int arr[5]); // same as int baz(int *__counted_by(5) arr)
      int frob(int count, int arr[count]); // same as int frob(int count, int *__counted_by(count) arr)
      ```
      
      The `__counted_by` annotation can also be placed inside array brackets:
      
      ```c
      int baz(int arr[__counted_by(5)]);
      int frob(int count, int arr[__counted_by(count)]);
      
      // Flexible array members:
      struct flexible {
          int count;
          int flex[__counted_by(count)];
      };
      ```
      
      ### Conversion to Internal Bounds
      
      When you access a pointer with a count or end annotation, it is implicitly converted to a `__bidi_indexable` pointer:
      
      ```c
      void read_buffer(int *__counted_by(count) elems, int count) {
          // bidi.lower = elems; bidi.current = elems; bidi.upper = elems + count
          int *ptr = elems;
      }
      
      void read_buffer_with_byte_size(int *__sized_by(byte_count) elems, int byte_count) {
          // bidi.lower = elems; bidi.current = elems; bidi.upper = (char *)elems + byte_count
          int *ptr = elems;
      }
      
      void read_ranged_buffer(int *__ended_by(end) begin, int *end) {
          // bidi.lower = begin; bidi.current = begin; bidi.upper = end
          int *ptr = begin;
      }
      ```
      
      Converting from internal bounds to external bounds triggers a bounds check (since bounds will be discarded):
      
      ```c
      int elems[10];
      bar_counted(elems, 5);
      // bounds check: __ptr_lower_bound(elems) <= elems <= elems+5 <= __ptr_upper_bound(elems)
      ```
      
      ### Assignment Rules for External Bounds
      
      To prevent inconsistent states, assignments to pointer-count pairs must happen in groups. Groups are delimited by expressions with side effects (like function calls) and logical scopes:
      
      ```c
      void somefunction() {
          int count = 0;
          int *__counted_by(count) elems = NULL;
          {
              // group 1
              elems = storage;
              count = 3;
              printf("hello!"); // side effects end group 1
      
              // group 2
              count = 2;
      
              {   // scope ends group 2
                  // ...
              }
      
              // group 3
              count = 1;
              elems = storage + 1;
          } // scope ends group 3
      }
      ```
      
      > **Note:** All function calls (including `malloc`) end assignment groups. Since `-fbounds-safety` analyzes assignments right-to-left, when malloc is directly assigned to a counted pointer, the count assignment must be **after** the call to malloc.
      
      ### Count Expression Restrictions
      
      Count expressions on function parameters and return values share the same grammar. Allowed forms:
      
      - Integer constants and `sizeof` (e.g. `5`, `sizeof(int)`)
      - Direct references to parameters (e.g. `count`)
      - Arithmetic, bitwise, and shift operations on parameters (e.g. `count + 1`, `rows * cols`, `n & 0xff`, `n / 2`)
      - Casts wrapping an allowed expression (e.g. `(size_t)count`, `(size_t)*count`)
      - A single dereference of a pointer parameter (e.g. `*count`) — this is what enables the out- and in-out-parameter pattern
      - A call to a function that is marked `__attribute__((const))`
      
      Rejected forms (each produces `error: invalid argument expression to bounds attribute`):
      
      - A dereference combined with any arithmetic (e.g. `*count + 1`, `*count + 0`, `(size_t)*count - 1`) — the dereference must stand alone
      - Multi-level dereference (`**count`) or array subscript (`count[0]`)
      - Struct member access via `.` or `->` (except in the flexible-array-member case below)
      - Ternary expressions (`x ? x : 1`)
      - Calls to functions without the `const` attribute
      
      Struct fields (including flexible array members) follow a slightly looser rule:
      
      - Direct references to sibling scalar fields, and arithmetic/bitwise operations on them, are allowed in any `__counted_by`/`__sized_by` field declaration.
      - `.` access into a nested-struct sibling (e.g. `__counted_by(i.n)` where `i` is a sibling field) is allowed **only** inside flexible array member declarations.
      - `->` is **never** accepted in a count expression — not even for flexible array members. Clang reports *"arrow notation not allowed for struct member in count parameter"*.
      
      ## Out and In-Out Parameters with `__counted_by`
      
      APIs that return a pointer paired with its count — or let the caller hand in a pointer-count pair and have the callee grow or fill it — are expressed with a pointer-to-pointer argument whose inner `*` carries the bounds attribute. The shape is `T *__counted_by(*count) *out`; several macOS SDK functions use it (see "Recognising real SDK signatures" below). The positional rule from [Attribute Placement on Multi-Level Pointers](#attribute-placement-on-multi-level-pointers) is what makes this work: `__counted_by` attaches to the `*` immediately to its left, so the inner pointer carries the count and the outer `*` is just "pointer-to". The same shape also works with `__counted_by_or_null`, `__sized_by`, `__sized_by_or_null`, and `__ended_by`.
      
      Four variants:
      
      ### Pure OUT (function allocates)
      
      ```c
      void make_out(int *__counted_by(*count) *o, size_t *count);
      
      // Implementation
      void make_out(int *__counted_by(*count) *o, size_t *count) {
          size_t n = 10;
          int *p = malloc(n * sizeof *p);
          *count = n;   // assign count first, then the pointer (right-to-left analysis)
          *o = p;
      }
      
      // Caller
      void caller(void) {
          size_t count = 0;
          int *__counted_by(count) buf = NULL;   // must be adjacent to 'count'
          make_out(&buf, &count);
          for (size_t i = 0; i < count; i++) buf[i] = (int)i;
          free(buf);
      }
      ```
      
      ### INOUT (grow or resize)
      
      Identical signature shape to the OUT variant — the two are indistinguishable from the type alone. Document the direction in a comment or by naming:
      
      ```c
      void grow_inout(int *__counted_by(*count) *p, size_t *count) {
          size_t n = *count * 2;
          int *tmp = realloc(*p, n * sizeof(int));
          *count = n;
          *p = tmp;
      }
      ```
      
      ### Fill-in-place INOUT
      
      Caller owns the pointer; only `*count` changes. Matches APIs like `sysctlnametomib`:
      
      ```c
      int fill(int *__counted_by(*count) buf, size_t *count);
      ```
      
      ### OUT with by-value capacity
      
      Caller decides the size; a `count = count;` self-assignment inside the callee satisfies the dependent-variable rule (the compiler's own diagnostic suggests exactly this form):
      
      ```c
      void alloc_fixed(int *__counted_by(count) *o, size_t count) {
          int *p = malloc(count * sizeof *p);
          count = count;   // self-assign: the dependency rule needs both sides in the same group
          *o = p;
      }
      ```
      
      ### Caller-side rules
      
      These follow from the general [Assignment Rules for External Bounds](#assignment-rules-for-external-bounds) but trip up most often at out/in-out call sites:
      
      - **Adjacent declarations.** The counted pointer and its count local must be declared in back-to-back declarations with no other statement between them, or Clang reports *"local variable X must be declared right next to its dependent decl"*.
      - **No side effects between paired assignments.** `buf = malloc(...)` before `count = ...` won't compile — `malloc` ends the group. Capture the allocation in a plain local first, then assign count and pointer with nothing between them.
      - **Address-of must match, for the double-pointer shape.** In Pure OUT and INOUT (grow/resize), you pass `f(&buf, &count)` — `f(&buf, count)` triggers *"passing address of 'buf' as an indirect parameter; must also pass 'count' or its address"*. Fill-in-place INOUT passes the pointer by value with `&count`; by-value-capacity OUT passes both by value. Match the callee's signature.
      
      ### Recognising real SDK signatures
      
      | SDK function                                                                                             | Shape                 |
      |----------------------------------------------------------------------------------------------------------|-----------------------|
      | `open_memstream(char *_LIBC_COUNT(*__sizep) *__bufp, size_t *__sizep)` (`<_stdio.h>`)                     | Pure OUT              |
      | `getdelim(char *_LIBC_COUNT(*__linecapp) *__linep, size_t *__linecapp, ...)` (`<_stdio.h>`)               | INOUT (grow on demand)|
      | `sysctlnametomib(const char *, int *__counted_by(*sizep), size_t *sizep)` (`<sys/sysctl.h>`)              | Fill-in-place INOUT   |
      | `sysctl(..., void *__sized_by(*oldlenp), size_t *oldlenp, void *__sized_by(newlen), size_t newlen)`       | Mixed INOUT + IN on one call |
      | `malloc_get_all_zones(..., vm_address_t *__single *__counted_by(*count) addresses, unsigned *count)` (`<malloc/malloc.h>`) | OUT with nested `__single` + `__counted_by` |
      
      `_LIBC_COUNT(*n)` is the Apple LibC wrapper macro for `__counted_by(*n)`; `_LIBC_SIZE(*n)` wraps `__sized_by(*n)`. They expand to nothing when `-fbounds-safety` is disabled.
      
      ## Flexible Array Members
      
      Structures with flexible array members must indicate the count with `__counted_by` inside the empty array brackets:
      
      ```c
      struct flexible {
          int count;
          int elems[__counted_by(count)];
      };
      ```
      
      For a `__single` pointer to such a struct, bounds come from the current value of `count`:
      
      ```c
      struct flexible *__single flex = /* ... */;
      flex->count = flex->count - 1; // OK (unless count was 0)
      flex->count = flex->count + 1; // runtime error
      ```
      
      For a pointer with external bounds (e.g., `__sized_by`), `count` can be modified within those bounds:
      
      ```c
      struct flexible *__sized_by(12) flex = /* ... */;
      flex->count = 2; // OK
      flex->count = 3; // runtime error
      ```
      
      Pointer arithmetic on a pointer to a struct with a flexible array member is prohibited.
      
      ## Value-Terminated Arrays
      
      `-fbounds-safety` supports value-terminated arrays with `__terminated_by(TR)`. Currently `TR` must be NULL or an integer constant.
      
      ```c
      // C strings:
      const char *__null_terminated s; // equivalent to __terminated_by(0)
      ```
      
      Value-terminated arrays support arithmetic with values 0 and 1 only. It is a runtime trap to execute `ptr + 1` if `*ptr` is the terminator:
      
      ```c
      const char *s = /*...*/;
      while (*s) {
          s++; // OK
      }
      // *s == 0
      *s == 0; // OK: can read terminator
      *s = 1;  // runtime error: erasing terminator
      s++;     // runtime error: past end
      ```
      
      Note conversion to/from `__terminated_by` from/to other safe pointer kinds is implicitly disallowed because the conversion in many cases requires a linear scan of memory which has performance implications that developers likely do not want happening implicitly. Instead explicit conversion functions need to be used which mean the developer is actively choosing to take the performance cost. These conversion functions are detailed in the next section.
      
      ### Conversion Functions
      
      Three fundamental conversion functions between `__terminated_by` and indexable types:
      
      - **`__terminated_by_to_indexable(P)`**: Convert to indexable, excluding terminator from bounds. Safe operation. May insert a `strlen` call for NUL-terminated strings.
      - **`__unsafe_terminated_by_to_indexable(P)`**: Convert to indexable, including terminator in bounds. Unsafe — terminator becomes writable.
      - **`__unsafe_terminated_by_from_indexable(TR, P [, ENDP])`**: Convert indexable to `__terminated_by(TR)`. Checks that P contains TR within bounds. If ENDP specified, only verifies ENDP points to terminator. Note this function is referred to as "unsafe" because the original indexable pointer (`P`) may still exist and could be used to later overwrite the terminator and thus the resulting pointer would no longer be correctly terminated. However, if the pointer `P` (and other aliases of the result) are immediately made unusable (e.g. by making them null pointers) then this conversion from terminated_by to indexable is perfectly safe.
      
      Convenience variants for __null_terminated pointers:
      
      - `__null_terminated_to_indexable(P)`
      - `__unsafe_null_terminated_to_indexable(P)`
      - `__unsafe_null_terminated_from_indexable(P [, ENDP])`
      
      ### Example: `strdup` with `-fbounds-safety`
      
      ```c
      // -fbounds-safety enabled
      char *strdup(const char *_s) {
          const char *__indexable s = __terminated_by_to_indexable(_s);
          size_t size = __ptr_upper_bound(s) - s;
          char *result = malloc(size + 1);
          memcpy(result, s, size);
          result[size] = 0;
          return __unsafe_null_terminated_from_indexable(result, &result[size]);
      }
      ```
      
      ## Comprehensive Pointer Conversion Table
      
      The table below summarizes the allowed implicit and explicit conversions across all pointer kinds, including external bounds and value-terminated pointers. For the detailed mechanics of how internal bounds are transferred between indexable pointer kinds, see the [conversion table above](#converting-between-indexable-pointers).
      
      | From/To | `__bidi_indexable` | `__indexable` | `__single` | `__unsafe_indexable` | `__counted_by` | `__null_terminated` |
      |---|---|---|---|---|---|---|
      | **`__bidi_indexable`** | trivial | implicit (adds bounds check) | implicit (adds bounds check) | implicit | implicit (adds bounds check) | explicit only: use `__unsafe_null_terminated_from_indexable()` |
      | **`__indexable`** | implicit | trivial | implicit (adds bounds check) | implicit | implicit (adds bounds check) | explicit only: use `__unsafe_null_terminated_from_indexable()` |
      | **`__single`** | implicit | implicit | trivial | implicit | implicit (adds bounds check) | explicit only: use `__unsafe_null_terminated_from_indexable()` |
      | **`__unsafe_indexable`** | error | error | error | trivial | error | explicit only: use `__unsafe_forge_null_terminated()` |
      | **`__counted_by`** | implicit | implicit | implicit (adds bounds check) | implicit | implicit (adds bounds check) | explicit only: use `__unsafe_null_terminated_from_indexable()` |
      | **`__null_terminated`** | explicit only: use `__null_terminated_to_indexable()` | explicit only: use `__null_terminated_to_indexable()` | explicit only: use `__null_terminated_to_indexable()` | implicit | explicit only: use `__null_terminated_to_indexable()` | trivial |
      
      Notes:
      
      - **`__counted_by`** in this table represents all external bounds annotations (`__sized_by`, `__ended_by`, `__counted_by_or_null`, `__sized_by_or_null`) since they behave the same way for conversions.
      - **implicit (adds bounds check)** means the conversion happens automatically but a runtime check is inserted to verify the pointer is within the required bounds.
      - **implicit** means the conversion happens automatically with no check (bounds are transferred or dropped).
      - **explicit only** means the conversion is a compile-time error unless an explicit conversion function is used — see the [Value-Terminated Arrays](#value-terminated-arrays) section.
      - Converting from `__unsafe_indexable` to any safe pointer kind is always a compile-time error — use `__unsafe_forge_bidi_indexable()` or `__unsafe_forge_single()`.
      
      ## Deriving Bounds from Objects
      
      Rules for which bounds you get with regular C operations:
      
      - **Constant-sized arrays** (`T arr[N]` as parameter, local, global, or struct member) decay to `T *__counted_by(N)` — bounds wrap the entire array.
      - **Unsized array parameters** (`T arr[]`) decay to `T *__single`.
      - **`&arr[10]`** or `arr + 10` gets a pointer whose bounds match `arr`'s bounds
      - **`&variable`** or **`&struct_field`** gets a pointer tightly fit around that one value
      
      ```c
      struct array_inside {
          int the_array[12];
          int foo;
      };
      
      struct array_inside many_arrays[15];
      int one_array[10];
      int one_element;
      ```
      
      - `&one_element` → bounds: `[&one_element, &one_element + 1)`
      - `one_array` → bounds: `[&one_array[0], &one_array[10])`
      - `&many_arrays[0].foo` → bounds: `[&many_arrays[0].foo, &many_arrays[0].foo + 1)` — **taking the address of a field always results in bounds tightly fit around that field**, preventing intra-object overflow
      - `many_arrays[0].the_array` → bounds: `[&many_arrays[0].the_array[0], &many_arrays[0].the_array[12])`
      
      Calls to `malloc`, `calloc`, and `realloc` return pointers with bounds matching the requested size.
      
      ## Escape Hatches
      
      ### `__unsafe_forge_bidi_indexable`
      
      Creates a bidirectionally indexable pointer from any value that could be cast to a pointer in C:
      
      ```c
      void *__unsafe_forge_bidi_indexable(type, value, size_t size);
      ```
      
      Use sparingly as a last resort. The primary use case is interoperating with libraries that don't enable `-fbounds-safety`.
      
      ### `__unsafe_forge_single`
      
      Creates a `__single` pointer from an `__unsafe_indexable` pointer. Useful when interfacing with system headers that haven't adopted `-fbounds-safety`:
      
      ```c
      FILE *f = __unsafe_forge_single(FILE *, stdin);
      ```
      
      ### When to Forge
      
      Forges are appropriate when the pointer source is `__unsafe_indexable` and you can verify the bounds externally:
      
      **Consuming `__unsafe_indexable` pointers from non-adopted headers:**
      
      ```c
      // third_party_lib.h — not adopted, so all pointers default to __unsafe_indexable
      struct device *get_device(int id);
      
      // your code — forge to __single so you can dereference it
      struct device *dev = __unsafe_forge_single(struct device *, get_device(0));
      ```
      
      **Creating bounded pointers from `__unsafe_indexable` struct fields in headers you can't modify (e.g., third-party):**
      
      ```c
      // third_party_lib.h — can't change this header
      // Under -fbounds-safety, data defaults to __unsafe_indexable
      struct legacy_buffer {
          void *data;
          size_t size;
      };
      
      // your code — forge because the struct can't be annotated
      void process(struct legacy_buffer *buf) {
          void *safe = __unsafe_forge_bidi_indexable(void *, buf->data, buf->size);
      }
      ```
      
      If you own the header, annotate the struct instead: `void *__sized_by(size) data;`
      
      **Self-describing buffers where bounds can't be expressed statically:**
      
      ```c
      // Pascal-string: buf[0] is the byte count, data follows at buf[1..]
      void write_block(GifByteType *__unsafe_indexable buf) {
          int block_len = buf[0] + 1;
          GifByteType *safe = __unsafe_forge_bidi_indexable(
              GifByteType *, buf, block_len);
          fwrite(safe, 1, block_len, out);
      }
      ```
      
      ### When NOT to Forge
      
      Forges are unnecessary when the pointer already carries bounds information:
      
      **Annotated allocator returns:** `malloc`, `calloc`, `realloc` (and any function with `alloc_size` or explicit `__sized_by_or_null` on the return type) already return pointers with bounds. Casting to a typed pointer produces `__bidi_indexable` with correct bounds. Forging re-derives what the compiler already knows. Note: unannotated custom allocators returning plain `void *` do NOT carry bounds — forging may be necessary there until the allocator is annotated.
      
      ```c
      struct container {
          int count;
          Item *__counted_by(count) items;
      };
      
      // WRONG — forge is redundant
      Item *new_items = (Item *)realloc(c->items, newCount * sizeof(Item));
      c->count = newCount;
      c->items = __unsafe_forge_bidi_indexable(  // unnecessary!
          Item *, new_items, (size_t)newCount * sizeof(Item));
      
      // RIGHT — realloc has alloc_size, so the cast already carries correct bounds
      Item *new_items = (Item *)realloc(c->items, newCount * sizeof(Item));
      c->count = newCount;
      c->items = new_items;  // compiler inserts bounds check automatically
      ```
      
      **`__counted_by`/`__sized_by` pointers:** Accessing a `__counted_by(N)` or `__sized_by(N)` pointer eagerly converts it to `__bidi_indexable` with correct bounds (see "Conversion to Internal Bounds"). No forge needed.
      
      ```c
      // WRONG — forge is redundant
      Item *local = __unsafe_forge_bidi_indexable(  // unnecessary!
          Item *, c->items, (size_t)c->count * sizeof(Item));
      
      // RIGHT — accessing a __counted_by pointer eagerly converts to __bidi_indexable
      Item *local = c->items;  // already __bidi_indexable with correct bounds
      ```
      
      **Constant-sized arrays:** A declared array `T arr[N]` decays to `T *__counted_by(N)` whenever it's used as a value — whether `arr` is a function parameter, local, global, or struct member (`p->buf`). The decayed pointer already carries bounds, and assigning it to a `T *` local gives `__bidi_indexable` with the array's bounds. A forge re-derives what the compiler already knows. See [Deriving Bounds from Objects](#deriving-bounds-from-objects).
      
      ```c
      struct Frame { uint8_t buf[256]; };
      
      // WRONG — forge is redundant
      void process(struct Frame *p) {
          uint8_t *view = __unsafe_forge_bidi_indexable(  // unnecessary!
              uint8_t *, p->buf, sizeof(p->buf));
      }
      
      // RIGHT — array decay already gives bounds
      void process(struct Frame *p) {
          uint8_t *view = p->buf;  // __bidi_indexable, bounds [&p->buf[0], &p->buf[256])
      }
      ```
      
      **General rule:** If the pointer already has bounds information from its source (annotated allocator, annotated field, annotated parameter), don't forge. Only forge when the source is `__unsafe_indexable` or otherwise has no bounds.
      
      ### `__unsafe_indexable`
      
      ABI-visible pointer surfaces — function parameters, struct fields, return types, globals — cannot use the ABI-incompatible `__bidi_indexable` / `__indexable`. The choice is between an externally counted bounds annotation (e.g. `__counted_by`, `__sized_by`, `__null_terminated`), `__single`, and `__unsafe_indexable`. Walk this decision tree in order:
      
      1. **Does the pointer actually point to a buffer of multiple elements/bytes?** If no — it really is `NULL` or one object — keep `__single` (the implicit default for ABI-visible surfaces). Stop.
      2. **Can the buffer's bound be expressed in the count grammar?**
         - For function parameters: a sibling parameter, an integer constant, or `*deref` of a pointer parameter — see [Count Expression Restrictions](#count-expression-restrictions). Use `__counted_by` / `__sized_by` / `__counted_by_or_null` / `__sized_by_or_null`.
         - For struct fields: a sibling scalar in the same struct or a constant. **Flexible-array-member exception:** FAMs additionally allow `.` access into a sibling struct's scalar fields (e.g. `__counted_by(dim.n)`); `->` is still rejected even for FAMs.
         - For NUL-terminated strings: `__null_terminated`.
      3. **If the bound cannot be expressed**, the choice depends on the surface:
         - **Internal function** (`static` or in a private header): use `__bidi_indexable` directly — the ABI doesn't need preserving. See *Rewriting Internal APIs* in [common-patterns-and-pitfalls.md](common-patterns-and-pitfalls.md).
         - **Public function**: apply *Safe Wrappers for Public APIs* in [common-patterns-and-pitfalls.md](common-patterns-and-pitfalls.md).
         - **Struct field**: no `__bidi_indexable` option (ABI), no Safe Wrapper option (fields don't have shim signatures). Mark the field `__unsafe_indexable` explicitly.
      
      **Never leave the surface implicit (defaulting to `__single`) when the pointer is actually a buffer.** Implicit `__single` is a lie about the data shape; explicit `__unsafe_indexable` correctly tells consumers "no bounds info — forge at use sites". See [Forging a `__single` Pointer Means the Source Is Misannotated](common-patterns-and-pitfalls.md#forging-a-__single-pointer-means-the-source-is-misannotated) for examples.
      
      ## Principled Bounds Checks
      
      All bounds checks verify that a range of memory is within another range. Ranges are inclusive-exclusive (lower bound is dereferenceable, upper bound is one-past-the-end).
      
      For all memory accesses, `-fbounds-safety` verifies: **lower ≤ access_start ≤ access_end ≤ upper**
      
      ```c
      int array[10];
      int *p = array; // lower: &array[0], upper: &array[10]
      return p[3];    // Check [&p[3], &p[4]) within [p.lower, p.upper) — OK
      return p[13];   // Check [&p[13], &p[14]) within [p.lower, p.upper) — TRAP!
      ```
      
      Conversion operations may check larger ranges:
      
      ```c
      int foo(int *__counted_by(count) elems, int count);
      int *__bidi_indexable p = /* ... */;
      foo(p, 10); // bounds check: at least 10 elements accessible at p
      ```
      
      ## Performance Implications
      
      `-fbounds-safety` may impact performance by adding bounds checks and increasing pointer size. LLVM optimizations eliminate most of this cost.
      
      The compiler eagerly adds bounds checks, but LLVM detects redundant checks and eliminates them:
      
      ```c
      int sum(int *__counted_by(count) elems, int count) {
          int accum = 0;
          for (int i = 0; i < count; ++i) {
              accum += elems[i]; // bounds check added but eliminated — i < count guarantees safety
          }
          return accum;
      }
      ```
      
      Remaining checks typically indicate either a real bug or a pointer with internal bounds that LLVM can't statically verify.
      
      **Performance guidance:**
      - Prefer pointers with external bounds (`__counted_by`, etc.) over internal bounds in function arguments
      - `__bidi_indexable` pointers are 3 register words — always passed via stack on x86_64 and AArch64
      - `__indexable` pointers are 2 register words — can be passed in registers
      - Static and inline functions eliminate the difference in optimized builds
      
      **Measured overhead** (from Ptrdist and Olden benchmarks, 2023):
      - Code size: 9.1% geomean (range: -1.4% to 38%)
      - Runtime: 5.1% geomean (range: -1% to 29%)
      - Real-world audio codecs: ~1% runtime overhead
      
      ## Detecting `-fbounds-safety`
      
      ```c
      #if __has_feature(bounds_safety)
      /* bounds-safe code */
      #else
      /* non-bounds-safe code */
      #endif
      ```
      
      ## LibC Annotation Macros
      
      Apple's LibC headers use wrapper macros (prefixed `_LIBC_`) instead of the raw `-fbounds-safety` annotations. These are defined in `<_bounds.h>`. When `-fbounds-safety` is not enabled, these macros expand to nothing, so the headers remain compatible with non-bounds-safe builds.
      
      | LibC Macro | `-fbounds-safety` Equivalent |
      |---|---|
      | `_LIBC_COUNT(x)` | `__counted_by(x)` |
      | `_LIBC_COUNT_OR_NULL(x)` | `__counted_by_or_null(x)` |
      | `_LIBC_SIZE(x)` | `__sized_by(x)` |
      | `_LIBC_SIZE_OR_NULL(x)` | `__sized_by_or_null(x)` |
      | `_LIBC_ENDED_BY(x)` | `__ended_by(x)` |
      | `_LIBC_SINGLE` | `__single` |
      | `_LIBC_UNSAFE_INDEXABLE` | `__unsafe_indexable` |
      | `_LIBC_CSTR` | `__null_terminated` |
      | `_LIBC_NULL_TERMINATED` | `__null_terminated` |
      | `_LIBC_FLEX_COUNT(FIELD, INTCOUNT)` | `__counted_by(FIELD)` |
      | `_LIBC_SINGLE_BY_DEFAULT()` | `__ptrcheck_abi_assume_single()` |
      | `_LIBC_PTRCHECK_REPLACED(R)` | `__ptrcheck_unavailable_r(R)` |
      | `_LIBC_FORGE_PTR(P, S)` | `__unsafe_forge_bidi_indexable(__typeof__(*P) *, P, S)` |
      
      ## `alloc_size` implies `__sized_by_or_null`
      
      The `alloc_size` attribute automatically implies `__sized_by_or_null` on the return type. E.g.:
      
      ```c
      void* /*__sized_by_or_null(size)*/ my_malloc(size_t size) __attribute__((alloc_size(1)));
      void* /*__sized_by_or_null(size*count)*/ my_calloc(size_t count, size_t size) __attribute__((alloc_size(1,2)));
      ```
      
      ## Glossary
      
      | Term | Definition |
      |---|---|
      | auto bound | Variables with bounds annotation automatically inferred (e.g., local variables are implicitly `__bidi_indexable`) |
      | dependent variable | When using externally counted pointers (e.g., `__counted_by`), the pointer and the count form a pair. Modifying one requires modifying the other. |
      | wide pointer | A pointer with internal bounds (`__bidi_indexable` or `__indexable`), larger than a regular C pointer |
      | hard trap | Default `-fbounds-safety` behavior — program terminates on bounds violation |
      | soft trap | Alternative mode — violation is logged but execution continues |
      
    • runtime-debugging.md 8.7 KB
      # Runtime Debugging for `-fbounds-safety`
      
      This guide covers debugging programs built with `-fbounds-safety`, including trap behavior, LLDB commands, wide pointer inspection, and soft trap debugging.
      
      ## Optimized vs Unoptimized Builds
      
      Debug unoptimized code when possible. Optimized code is harder to debug because:
      
      - **Trap reasons are usually optimized out** — you won't know why the program trapped
      - **All traps in a function are merged into one** — difficult to determine which bounds check failed
      - **Bounds information on wide pointers may be missing** — the optimizer removes bounds checks and associated data
      
      If fully unoptimized builds aren't feasible (e.g., code size restrictions), selectively disable optimization on specific functions:
      
      ```c
      __attribute__((optnone)) void function_to_debug() {
          // ...
      }
      ```
      
      Remove the attribute when debugging is complete.
      
      ### `-fbounds-safety-unique-traps` Flag
      
      In optimized builds, use `-fbounds-safety-unique-traps` to prevent trap merging. This preserves separate trap locations, making it possible to identify which specific bounds check failed even in optimized code.
      
      ## What Happens When a Bounds Violation Occurs
      
      When `-fbounds-safety` detects an issue at runtime, it executes a trap instruction. This is handled by the environment, usually resulting in program termination.
      
      ### Debugger — Unoptimized Program with Debug Info
      
      #### Command Line LLDB
      
      The stop reason shows the bounds check failure:
      
      ```
      stop reason = Bounds check failed: Dereferencing above bounds
      ```
      
      The "Bounds check failed:" prefix indicates `-fbounds-safety` caught the issue. After the prefix is a trap reason explaining the problem.
      
      #### Xcode
      
      Xcode stops at the offending line with an annotation like:
      
      ```
      Thread 1: Bounds check failed: Dereferencing above bounds
      ```
      
      ### Debugger — Optimized Program
      
      In optimized programs the stop reason is not specific. You need to inspect the assembly to determine if a `-fbounds-safety` trap was hit.
      
      **Note:** the precise assembly instructions are not guaranteed to be stable.
      
      #### arm64/arm64e
      
      ```
      (lldb) dis -p
      ->  0x100003e60 <+296>: brk    #0x5519
      ```
      
      If the program stopped at `brk #0x5519`, this is a `-fbounds-safety` trap.
      
      #### x86_64
      
      ```
      (lldb) dis -p
      ->  0x100003e95 <+309>: ud1l   0x19(%eax), %eax
      ```
      
      If the program stopped at `ud1l` with `0x19` constant, this is a `-fbounds-safety` trap.
      
      #### armv7
      
      `-fbounds-safety` uses the `trap` instruction. No extra information distinguishes it from other traps. Debug an unoptimized build or step through assembly to confirm.
      
      ### Crash Logs
      
      #### Unoptimized with Debug Symbols
      
      The crash log shows an artificial inline frame with the trap reason:
      
      ```
      Thread 0 Crashed:
      0   parse_ints_O0   0x1025b7a2c Bounds check failed: Dereferencing above bounds + 0 [inlined]
      1   parse_ints_O0   0x1025b7a2c parse_ints + 472 (parse_ints.c:39)
      ```
      
      Frame 0 is artificial — the real crash location is frame 1.
      
      The ESR register on arm64 is annotated with `(Breakpoint) UBSAN unknown (0x19)`, indicating a `-fbounds-safety` trap.
      
      #### Optimized or No Debug Symbols
      
      No trap reason frame is present. Look for `(Breakpoint) UBSAN unknown (0x19)` in the ESR register annotation (arm64 only).
      
      #### Working with Crash Logs in LLDB
      
      Load crash logs for interactive analysis:
      
      ```
      (lldb) command script import lldb.macosx.crashlog
      (lldb) crashlog -i /path/to/crashlog.ips
      ```
      
      This creates an artificial debugging session where you can disassemble, read registers, navigate the stack, and examine source code.
      
      ## Trap Reasons
      
      Trap reasons are human-readable descriptions encoded in debug info as artificial inline frames. They are prefixed with `"Bounds check failed:"`.
      
      ```
      (lldb) bt
      * thread #1, stop reason = Bounds check failed: Dereferencing above bounds
          frame #0: parse_ints_O0`parse_ints [inlined] Bounds check failed: Dereferencing above bounds
        * frame #1: parse_ints_O0`parse_ints at parse_ints.c:39:13
      ```
      
      Trap reasons require debug info and are typically lost in optimized builds.
      
      ### Example Trap Reasons
      
      - **`indexing below lower bound in 'ptr[idx]'`**
      - **`indexing above upper bound in 'ptr[idx]'`**
      - **`Pointer below bounds while casting`** — bounds check during cast (e.g., `__bidi_indexable` → `__single`) with pointer below lower bound
      - **`Pointer to struct below bounds while taking address of struct member`** — bounds check during `&p->member` with p below lower bound
      
      If a trap shows only `"Bounds check failed"` without further detail, a specific message hasn't been implemented for that case.
      
      ## Working with Wide Pointers
      
      ### Examining Wide Pointers
      
      LLDB displays wide pointers with their bounds:
      
      ```
      (lldb) p output_buffer
      (int *__bidi_indexable) $1 = (ptr: 0x000100404080, bounds: 0x000100404080..0x0001004040a8)
      ```
      
      - `ptr:` is the current pointer value
      - `bounds:` shows lower..upper bound
      
      Out-of-bounds pointers are indicated:
      
      ```
      (int *__bidi_indexable) $2 = (out-of-bounds ptr: 0x0001004040a8, bounds: 0x000100404080..0x000100404094)
      ```
      
      Out-of-bounds wide pointers are allowed to exist but cannot be dereferenced.
      
      ### Known Limitations
      
      - In optimized code, some wide pointer components may be optimized out — LLDB shows `0x000000000000` (indistinguishable from actual NULL)
      - Partially executing a statement may show incorrect results due to partial wide pointer updates
      - If LLDB shows the wide pointer as a raw struct with `ptr`, `ub`, `lb` fields instead of the expected format, you're using an older LLDB version
      
      ## Working with Externally Counted Pointers
      
      LLDB shows the count expression (unevaluated) for externally counted pointers:
      
      ### `__counted_by`
      
      ```
      (lldb) p buffer
      (int*) (ptr: 0x000100206210 counted_by: size)
      ```
      
      ### `__sized_by`
      
      ```
      (lldb) p buffer
      (int*) (ptr: 0x000100206210 sized_by: size)
      ```
      
      ### `__ended_by`
      
      ```
      (lldb) p start
      (int*) (ptr: 0x0001003041e0 end_expr: end)
      (lldb) p end
      (int*) (ptr: 0x0001003041f0 start_expr: start)
      ```
      
      ### Known Limitations
      
      - LLDB does not automatically evaluate the count expression — you must evaluate it manually
      - Type printing omits the bounds annotations (shows `int*` instead of `int* __counted_by(size)`)
      
      ## Types Without Special Debugger Support
      
      These annotations currently have no special LLDB display — the unannotated pointer type is shown:
      
      - `__single`
      - `__terminated_by` and `__null_terminated`
      - `__unsafe_indexable`
      
      ## Expression Parsing Limitations
      
      The `-fbounds-safety` language mode is mostly off in LLDB's expression evaluator. Known issues:
      
      - `-fbounds-safety` types cannot be parsed: `p (int *__bidi_indexable) foo` will fail
      - `-fbounds-safety` builtins cannot be called: `__builtin_get_pointer_upper_bound(foo)` will fail
      - Dereferencing a wide pointer in an expression that would trap fails to execute
      
      ## Soft Traps in LLDB
      
      Soft trap mode must be enabled at build time — see [build-settings.md](build-settings.md) for the compiler flag and Xcode build setting.
      
      ### Supported OSs
      
      The mode relies on an implementation of the `__bounds_safety_soft_trap` function being provided. On macOS/iOS 27.0 and newer this symbol is provided by libSystem and so this mode will work out-of-the-box.
      On older OSs this symbol is not provided and so linker errors will be observed. However, projects can provide their own implementation so that debugging is still possible. E.g.:
      
      ```c
      #include <bounds_safety_soft_traps.h>
      
      __attribute__((noinline))
      void __bounds_safety_soft_trap(void) {
          // Provide a symbol for LLDB to set a breakpoint on but do nothing
      }
      ```
      
      If projects do implement this function it must be removed when the project switched to hard trap mode.
      
      ### Observing in LLDB
      
      LLDB includes an instrumentation plugin that automatically stops on soft traps. When a soft trap is hit:
      
      ```
      Process 779 stopped
      * thread #1, stop reason = Soft Bounds check failed: indexing above upper bound in 'ptr[idx]'
          frame #2: main`bad_read(ptr=(ptr: 0x00016af472a8, bounds: 0x00016af472a8..0x00016af472b4), idx=3) at main.c:4:62
      ```
      
      The backtrace shows:
      - Frame 0: `__bounds_safety_soft_trap` (the runtime function)
      - Frame 1: artificial frame with trap reason (`__clang_trap_msg$Bounds check failed$...`)
      - Frame 2: the actual source location (LLDB selects this frame automatically)
      
      ```
      (lldb) bt
          frame #0: libsystem_sanitizers.dylib`__bounds_safety_soft_trap
          frame #1: main`__clang_trap_msg$Bounds check failed$indexing above upper bound in 'ptr[idx]' [inlined]
        * frame #2: main`bad_read(ptr=..., idx=3) at main.c:4:62
          frame #3: main`main(argc=1, argv=...) at main.c:10:5
      ```
      
      Resume execution with `c` (continue), just like any other breakpoint.
      
      ### Disabling the Soft Trap Plugin
      
      Add to `~/.lldbinit`:
      
      ```
      plugin disable instrumentation-runtime.BoundsSafety
      ```
      
      Restart your debugging session for this to take effect. Disabling mid-session is not currently supported.
      
  • SKILL.md 2.9 KB
    ---
    description: |
      Guide for the C -fbounds-safety language extension. Covers the language model, pointer annotations, adopting bounds-safety in existing C code, compiler build settings and modes, and runtime debugging of bounds violations.
    effort: high
    when_to_use: |
      When working with, reading, reviewing, comparing, debugging or analyzing C code that has adopted -fbounds-safety or wants to adopt it.  Key syntax to look for Bounds annotations (__counted_by, __counted_by_or_null, __sized_by, __sized_by_or_null, __ended_by, __single, __indexable, __bidi_indexable, __unsafe_indexable, __null_terminated, __terminated_by), its helper functions (e.g.: __unsafe_forge_bidi_indexable, __unsafe_forge_single,  __null_terminated_to_indexable, __unsafe_null_terminated_to_indexable,  __unsafe_null_terminated_from_indexable) or other macros  (e.g. __ptrcheck_abi_assume_single) or includes of "ptrcheck.h".
    name: adopt-c-bounds-safety
    ---
    ## How to Use This Skill
    
    When helping with `-fbounds-safety` adoption or code changes, ask clarifying questions about the user's codebase and goals before suggesting changes. For complex tasks involving multiple files or non-trivial annotation decisions, use plan mode to propose an approach before implementing.
    
    # `-fbounds-safety` Language Extension
    
    `-fbounds-safety` is a C language extension that prevents out-of-bounds memory access by enforcing bounds safety at the language level. It inserts automatic bounds checks at runtime, rejects unsafe pointer operations at compile time, and requires programmers to provide bounds annotations so the compiler can guarantee safety. Out-of-bounds accesses become deterministic traps instead of exploitable vulnerabilities.
    
    ## Detailed Documentation
    
    ### Required reading before adoption work
    
    You MUST have fully read the following three documents (via the Read tool) at the start of an adoption task, and re-read them via the Read tool before any source-modifying step in the adoption workflow unless their content is verifiably fresh in your active context:
    
    - [adoption-strategies.md](references/adoption-strategies.md) — the workflow for adopting `-fbounds-safety` in an existing C project (full and header-only modes).
    - [language-overview.md](references/language-overview.md) — the language reference for `-fbounds-safety`: pointer kinds, annotations, and the rules that govern them.
    - [common-patterns-and-pitfalls.md](references/common-patterns-and-pitfalls.md) — recipes and anti-patterns encountered during real-world adoption.
    
    ### Other references (read on demand)
    
    For compiler flags, Xcode build settings, soft trap mode, and `ptrcheck.h` configuration, read [build-settings.md](references/build-settings.md).
    
    For debugging bounds violations at runtime — trap behavior, LLDB commands, wide pointer inspection, watchpoints, crash log analysis, and soft trap debugging, read [runtime-debugging.md](references/runtime-debugging.md).

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related