Skip to content

docs(maul): let a caller-built input point where its builder chose - #120

Merged
siracozmen01 merged 2 commits into
mainfrom
docs/state-checks-caller-built
Nov 4, 2026
Merged

siracozmen01 merged 2 commits into
mainfrom
docs/state-checks-caller-built

Conversation

@siracozmen01

@siracozmen01 siracozmen01 commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Problem

The rule from #119 says a value struct a call takes back carries no raw pointer and size into the library's data. maul-ui's audit found muiAccessUpdate. The library fills it from muiContext_GetAccessUpdate, but the header also documents it as input a host builds. To muiAccessTree_Apply its pointers are the caller's to keep valid, as with any buffer argument, so read literally the rule would call a documented caller-built input a breach.

Change

maul/files/docs/internals/maul-conventions.md: one sentence after the fill-only exception. A struct the header also documents as caller-built input may point into the library's data, provided the call that takes it reads through it as through any buffer argument: only within the ranges its own pointers and counts state, checking every count, offset and index it reads there as untrusted, and relying on nothing the library validated earlier. A header line alone exempts nothing: a zone call that follows indexes the open validated stays under the rule.

Verification

This is documentation only. maul-ui (#338, #339), maul-tween (#297) and maul-nav (#330, #331) close their issues under this rule only after confirming their calls read that way; any place that trusts an index found inside the data is a bug to fix.

maul-ui's audit under #119 found muiAccessUpdate, which the library fills but the header also documents as input a host builds. To the call that takes it, its pointers are caller memory, so it is not a breach; the rule now says so, and that such a call still checks every count, offset and index it can.

@siracozmen01 siracozmen01 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review at 6a06af1: changes needed (1). The new sentence opens the loophole you asked about.

1. The exemption depends on what the header says, not on what the call does. "So may one the header also documents as input a caller builds" lets any struct escape the rule through one header sentence. The weak point is "checks every count, offset and index it can": a call that cannot check something can say so and still qualify.

  • Concrete case: maul-locale adds "a caller may also build a mlocTimeZone" to zone.h. The zone calls keep trusting the zone and primary indexes inside zones, because those bytes were validated when the data was opened, and the new sentence would cover them. That is exactly #334's bug. The same goes for muiDrawList or mnavPath if their headers gain such a line.
  • What separates muiAccessUpdate from mlocTimeZone is how the call reads through the struct, not who built it:
    • muiAccessTree_Apply treats the update's arrays like any buffer argument: it reads only inside the ranges the struct's own pointers and counts state, and checks every value it reads from them as untrusted.
    • A zone call follows indexes it finds inside the bytes beyond any extent the struct states, trusting an earlier validation. That is the case the rule has to forbid.
  • Please tie the exemption to that behavior. For example: "So may one the header also documents as input a caller builds, such as an accessibility update, provided the call that takes it reads through it as through any buffer argument: only within the ranges its own pointers and counts state, checking every count, offset and index it reads there as untrusted, and relying on nothing the library validated earlier. Its pointers and counts are then the caller's to keep valid."
  • With that wording, a header sentence alone exempts nothing. muiDrawList and mnavPath stay fill-only, and mlocTimeZone stays under the rule even if its header calls it caller-built. maul-ui's check for #339 becomes concrete: Apply must never read outside the update's stated ranges and must never trust their contents.

Read, not run.

@siracozmen01 siracozmen01 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review at 2107c76: ready. My finding is resolved in the rule.

  • The exception now holds only "provided the call that takes it reads through it as through any buffer argument: only within the ranges its own pointers and counts state, checking every count, offset and index it reads there as untrusted, and relying on nothing the library validated earlier". A header sentence alone no longer exempts a struct. mlocTimeZone's calls still rely on the zones unit having been validated at open, so it stays under the rule, and a struct the library alone fills (muiDrawList, mnavPath) qualifies only if the call that takes it back really reads it this way.
  • "Its pointers and counts are then the caller's to keep valid" puts memory validity where it is for any buffer argument.

Recommended, not blocking: the body still describes the earlier wording. Its Change says the call "still checks every count, offset and index it can without trusting the pointers", and its Verification says the same in short. The body becomes the squash commit message, so it would record the weaker rule. Please bring the Change line in line with the file's "provided ..." clause.

Read, not run.

@siracozmen01
siracozmen01 merged commit 972ea41 into main Nov 4, 2026
1 check failed
siracozmen01 added a commit to Rawframe-Project/maul-tween that referenced this pull request Nov 4, 2026
## Problem

`mtwnClip_GetTrack` fills an `mtwnClipTrack` whose track, and whose easings' linear() points, point into the clip's storage. `mtwnClip_GetNode` fills a timing whose linear() points do too. The header said only that the track "references the clip's storage, valid while the clip lives". It did not say that the values layer takes such a track back as caller-built input, whose pointers and counts are the caller's to keep. With `track.path.count` raised to 2^20, `mtwnSampleTrack` reads past the clip's keyframes (#297).

By .github v0.0.51's rule on state a caller holds (Rawframe-Project/.github#120), a struct the header also documents as caller-built input may point into the library's data. That holds only if the call taking it reads through it as through any buffer argument: only within the ranges its own pointers and counts state, checking every value it reads there, and relying on nothing validated earlier.

## Change

- The `mtwnClipTrack` comment says what points into the clip's storage, that it is valid only while the clip lives (unchanged, since a sealed clip refuses every change), and that the values layer takes it as input the caller built.
- `mtwnClipNodeInfo`'s timing says its linear() points are the clip's.
- `docs/user/clips.md` says the same where it introduces `mtwnClip_GetTrack`.

## Change of results

None; comments and the guide only.

## Verification

Each of `mtwnSampleTrack`, `mtwnSamplePath`, `mtwnComputeTiming` and `mtwnSampleEasing`, read against the rule on main 64415a7:

- **Reads stay within the stated ranges.**
  - The segment search (`FindSegment`, `src/path.c:386`) is a binary search in `[0, count - 2]`, whatever the offsets or lengths hold.
  - A segment reads `keyframes[i - 1]` only for `i > 0` and `keyframes[i + 2]` only for `i + 2 < count`.
  - The mixes are read at the segment's index.
  - linear() is a binary search in `[0, count)` (`src/css_easing.c:255`).
- **Every value read is checked on each call.**
  - The track's and path's fields: `mtwnValidTrack` and `mtwnValidPath`, checked by every call.
  - The segment read: its kind, its easing as a maker's (`mtwnValidEasing`), and at constant speed not a step (`Reads`, `src/path.c:497`).
  - A color's or rotation's mix where the segment reads it (`MixReads`).
  - A timing whole, easing included (`mtwnValidateTimingWork`), at every `mtwnComputeTiming`.
- **Nothing validated when the clip was made is relied on.**
  - Out-of-order offsets, lengths or linear() inputs, NaN or infinite values, and a zero width give a value perhaps not finite. They never give a read outside the arrays, a loop without bound, or a float-to-integer conversion.
  - A NaN local progress passes the steps and linear() samplers without a cast.
  - The arc-length loops are bounded by a stack size and a rule count (`src/path_length.c:304`) and by `s_newtonSteps`.
  - The clip's own sampling (`mtwnSampleTrackWork`) runs only on the clip's own copies, from the library.
  - No count limit is skipped: the makers and the sample checks both need at least 2 keyframes or points, and no MTWN limit applies to them.

No gap was found, so no fix is needed.

- `tools/check.sh changed` passes: clang-tidy on all 45 sources (a header changed), and linux-clang-20-asan 68 of 68.
- The docs checks and the organization's docs check pass.

Closes #297
siracozmen01 added a commit to Rawframe-Project/maul-unicode that referenced this pull request Nov 4, 2026
## Problem

`.github` v0.0.48 adds the family's `tools/tidy_filter.sh`, which prints clang-tidy's header filter for a build tree, and v0.0.50 and v0.0.51 add a conventions rule on how state a caller holds is checked (Rawframe-Project/.github#119 and Rawframe-Project/.github#120). This repository lacks it, carries an older `docs/internals/maul-conventions.md`, pins the reusable workflows at v0.0.42 and v0.0.45, and builds the filter itself in two places.

## Change

- The family files are synced to v0.0.51 with `family_sync.py --write`: `tools/tidy_filter.sh` is added, and `docs/internals/maul-conventions.md` gains the rule that every clang-tidy call takes its header filter from it and the rule on state a caller holds. `AGENTS.md` lists `tidy_filter.sh` among the shared files.
- Every reusable workflow and the docs-check action are pinned at `972ea41c6abf2652d3b067e57bf0713f1661cf0e # v0.0.51`. Between the old pins and v0.0.51 only `pr-title.yml` changed among the workflows, and its old pin, v0.0.45, already had that change.
- ci.yml's clang-tidy step and `tools/check.sh`'s `tidy()` set `filter=$(tools/tidy_filter.sh <tree>)` on a line of its own, so a tree without the cache entry stops the step instead of tidying with an empty filter, and pass `--header-filter="$filter"`. `tools/check.sh`'s own `header_filter()` is gone. A changed header still tidies every source.

## Verification

- `family_sync.py --check` from v0.0.51 reports 0 differences.
- In a fresh clone of this branch, with only a header changed (`return value == value;` planted in `src/bits.h`), `tools/check.sh changed` fails on `misc-redundant-expression` in `src/bits.h`, and the ci.yml step's commands report it on the Release and Debug presets. `tools/tidy_filter.sh` on a tree whose cache lacks the entry exits 1.
- `tools/check.sh changed` passes on the clean branch, and `shellcheck -S warning` is clean on `tools/check.sh` and `tools/tidy_filter.sh`.

Closes #261
siracozmen01 added a commit to Rawframe-Project/maul-vfx that referenced this pull request Nov 4, 2026
## Problem

The family files from `.github` v0.0.51 (972ea41) had not reached maul-vfx, which is on v0.0.48 since #285. v0.0.49 and v0.0.50 are skipped because v0.0.51 holds their changes. The only family file that changes is `docs/internals/maul-conventions.md`, which gains the rule on how state a caller holds is checked (Rawframe-Project/.github#119 and Rawframe-Project/.github#120).

## Change

- Ran `family_sync.py --write maul-vfx` from a clone of `.github` checked out at the v0.0.51 tag, not the live checkout. It writes `docs/internals/maul-conventions.md` only.
- Pinned the reusable workflows and actions to 972ea41 (v0.0.51) instead of 7eeaaa4 (v0.0.48), in `ci.yml`, `pr-gate.yml` and `pr-title.yml`. The workflows and actions are the same at both tags.

No code changes. The library already follows the new rule; the audit was done against the public headers:

| The rule says | maul-vfx |
| --- | --- |
| A value struct a call takes back carries no raw pointer and size into the library's data | `mvfxEffectRefusal` (filled by the create, read and write calls, taken by `mvfxGetEffectFieldPath`) holds no pointer: inline tags and items with a `tagCount` checked against below 0 and past `MVFX_MAX_FIELD_DEPTH`. The ids are an index and a generation, checked on every call. |
| A struct the library only fills in may point into its data | The views (`mvfxEmitterView`, `mvfxMeshView`, `mvfxGridView`, `mvfxLayoutView` and the rest), `mvfxEmitterPlan` and `mvfxParticleArrays` point into the library's data and are never taken back from the caller. |
| A struct also documented as caller-built input may point where its builder chose, read as untrusted | `mvfxEffectDef`, which `mvfxReadEffect` fills into the caller's arena, is checked field by field by every call that takes it. The other defs, `mvfxSpawnRequest` and the output buffers point into caller memory. |
| Caller memory a call keeps "as checked" is read only within the extents it recorded | `mvfxInstance_SetDepthImage` borrows the pixels. It checks the image once and keeps its own prepared copy of the extents, so later steps read within them. |

## Verification

- `family_sync.py --check maul-vfx` from the same clone: 0 differences.
- `git grep` finds no `.github` pin other than 972ea41 `# v0.0.51`.
- `tools/check.sh changed` passes. Only documentation and workflows changed, so there is no build.
- The organization's documentation check (`scripts/docs_check.py --strict`) from the v0.0.51 clone: ok.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant