Skip to content

docs(clip): say a track read from a clip points into its storage - #299

Merged
siracozmen01 merged 1 commit into
mainfrom
docs/clip-track-pointers
Nov 4, 2026
Merged

siracozmen01 merged 1 commit into
mainfrom
docs/clip-track-pointers

Conversation

@siracozmen01

Copy link
Copy Markdown
Contributor

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 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 a68d086: ready.

1. The docs. mtwnClipTrack's comment now says three things, each matching the code:

  • the track's keyframes, lengths and mixes, and its easings' linear() points, point into the clip's storage;
  • they are valid only while the clip lives, which a sealed clip does unchanged;
  • the values layer takes the track "as input the caller built", reading only within the counts it states and checking every segment it reads, so the pointers and counts are the caller's to keep as read.

mtwnClipNodeInfo.timing gains the same note for a linear() easing's points, and clips.md says it in user terms. That is the #120 exception stated in this library's own words.

2. Spot-check: no reliance on the clip's validation.

  • mtwnSampleTrack (path.c:949-971) checks the track on every call with mtwnValidTrack:
    • the value kind is at most mtwn_valueScale;
    • mtwnValidPath: keyframes not NULL, count >= 2, channels 1 to 4, alpha in [0, 1], a log path of one channel, lengths starting at 0 with a finite end;
    • channels, logarithm, mixes and lengths match the kind.
  • It then takes the segment with FindSegment, which stays within 0 to count - 2 whatever the offsets or lengths hold ("NaN or out of order as a host's changed keyframes may leave them").
  • Before reading the segment it checks Reads: the segment kind is at most mtwn_segmentMonotone (not a step at constant speed), and timed by keyframes its easing passes mtwnValidEasing. MixReads checks the segment's color or rotation mix the same way. The segment kind is therefore checked on each call, not trusted from mtwnClip_Seal.
  • mtwnSampleEasing (easing.c:661-679) also runs mtwnValidEasing on every call. That refuses an unknown kind (the default arm accepts only up to mtwn_easingBounce) and requires a linear() easing's points != NULL && count >= 2.
  • mtwnSampleLinearPoints (css_easing.c:255) binary-searches indices in [0, count) and reads index and index + 1, both below count. Unsorted or NaN inputs give a wrong value, never a read outside the stated points.

So the two most likely places to trust clip validation, the segment kind and the easing's point count, are both checked per call, and every read is bounded by the counts the struct states. The Newton and arc-length caps are the session's claim; I did not check them here.

Read, not run.

@siracozmen01
siracozmen01 merged commit a588c44 into main Nov 4, 2026
25 checks passed
@siracozmen01
siracozmen01 deleted the docs/clip-track-pointers branch November 4, 2026 03:13
siracozmen01 added a commit that referenced this pull request Nov 4, 2026
## Problem

.github v0.0.50 and v0.0.51 add one rule to the family conventions, on state a caller holds (Rawframe-Project/.github#119 and #120). This library still carried v0.0.48's `maul-conventions.md`, so the nightly family check reported it as differing.

## Change

- The family files are synced to v0.0.51 (`family_sync.py --write`). Only `docs/internals/maul-conventions.md` changes, by the rule's one paragraph.
- Every reusable workflow pin moves to v0.0.51 (972ea41). No script or workflow changed since v0.0.48.

The library follows the rule. The audit is #297: the only structs a call takes back that point into the library's data are clip tracks and timings, which the values layer reads as caller-built input. #299 documents them that way.

## Change of results

None.

## Verification

- `family_sync.py --check` at v0.0.51 reports 0 differences.
- `tools/check.sh changed` passes; no test is reached by the changes.
- The organization's docs check passes.

Closes #300
@siracozmen01 siracozmen01 mentioned this pull request Nov 4, 2026
siracozmen01 pushed a commit that referenced this pull request Nov 4, 2026
🤖 I have created a release *beep* *boop*
---


## [1.1.0](v1.0.0...v1.1.0) (2026-11-04)

This release makes `mtwnCreateClipPlayer` check its bindings in time linear in their count, where it compared each binding with every one before it ([#292](#292)). A player of 2^20 bindings took 194 s to make at -O2 and now takes 12 ms. At the old rate, the 2^24 slots a clip may have would have taken about 11 hours. Two refusals of a def that binds a slot twice change their status, as below. `MTWN_ABI_VERSION` and a shared library's SOVERSION are now 1.1. A context def stamped by 1.0.0's headers is refused with `mtwn_errorVersion`, so a program built against 1.0.0 is rebuilt; its source builds unchanged. No value, velocity or event record changes.

### Changed

- `mtwnCreateClipPlayer` checks its refusals in this order, which its header now states: an invalid def, then the rate's limit, then the context's capacities of players and of bindings, and a slot bound twice last ([#292](#292)). A def that binds a slot twice changes its status only where another refusal also applies:
  - with the context's players or bindings full, `mtwn_errorCapacity`, not counted as misuse, where it was `mtwn_errorInvalid`, counted;
  - with a rate past `MTWN_RATE_LIMIT`, `mtwn_errorLimit`, where it was `mtwn_errorInvalid`; both are counted as misuse.
- The clips guide and `mtwnClipTrack`'s header say that a track read with `mtwnClip_GetTrack` points into the clip's storage. The values layer takes it as it takes a track the host made, reading only within the counts the track states, so the host keeps it as it was read ([#299](#299)).
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.

A clip track or timing read from a clip carries raw pointers and counts into clip storage

1 participant