Skip to content

fix(solid): bound Turtle declarations and stop the parse at a limit - #456

Queued
ixuz wants to merge 2 commits into
mainfrom
fix/turtle-base-limit
Queued

ixuz wants to merge 2 commits into
mainfrom
fix/turtle-base-limit

Conversation

@ixuz

@ixuz ixuz commented Nov 1, 2026

Copy link
Copy Markdown
Member

What and why

The Turtle parser resolves each base declaration (@base, BASE) against the base before it. It resolves each relative IRI and each prefix declaration against the current base too, and it walks the whole base each time. Some bases, and many prefixes under one base, make the parse take time far beyond linear in the text, on the main thread. A declaration makes no triple, so none of the read limits counted it, and the parser read on to the end of the text even after a limit was passed. So a release small enough to pass every limit could still freeze the tab. This happened when the release was added from a link, on each later read of it or of its catalog for the deck list, and on a Studio import.

This pull request changes four things:

  • The parse stops at the first limit. A Turtle text read under limits is now given to the parser in parts, as a JSON-LD text already is. The parser gets no more once the text is past its limits (writeTurtle in readLimits.ts).
  • Base and prefix declarations are bounded. Before each part goes to the parser, the declarations that start in it are checked. The text is refused if it has more than 16 bases or more than 4,096 prefixes. It is also refused if a base has a ., .. or empty segment or an escape in its path, or if a base can grow past 512 characters. The refusal is the usual read-limit error: releaseTooLarge for a release, deckFileTooLarge for a deck file. No release the app writes has such a base, and a document needs only a few prefixes.
  • The search reads the text as the parser does. It skips strings, IRIs and comments, so a comment cannot hide a declaration, and a declaration quoted in a card's text is not counted. It takes time linear in the text. Where it might read a directive differently from the parser, it refuses.
  • Both Solid clients do this. It applies when a document is read, in getSolidDatasetLinear and in parseQuads. The clients' contract tests hold both clients to it, and growth tests check that the time to refuse a text grows linearly. A text read with no limits, from the user's own pod, is parsed as before.

docs/deck-library.md and docs/data-model.md give the new release and deck-file limits. docs/testing.md lists the new growth test.

Refs: GHSA-q3q9-h5mp-3956

Checklist

  • One issue. This pull request addresses one issue, linked above. (the advisory referenced above)
  • One step. This pull request does not finish its issue, so it says Refs:, not Closes:. Leave this unchecked otherwise.
  • Done, as AGENTS.md defines it: npm run check passes (lint, Markdown and spelling included), npm run lint:reuse too if a file was added, and npm run crosscheck too if ns/ or decks/ changed. (lint:reuse and crosscheck n/a: no file was added, and ns/ and decks/ are unchanged)
  • Tests. New and changed code has its tests, and every package stays at 100% coverage (docs/testing.md).
  • Docs. The docs say what changed, and a new page in docs/ is listed in docs/docs.ttl.
  • Languages. A new or changed message is in every locale: en, sv, ko, de, es and fr (packages/ui/src/i18n/). (n/a: no message changed; the refusal uses the existing releaseTooLarge and deckFileTooLarge)
  • Vocabulary and shapes. Any change to ns/ was agreed in an issue first; no published ns/shapes/*/vN.ttl or decks/*/vN.ttl was edited; generated files were regenerated, not edited. (n/a: ns/, decks/ and the generated files are unchanged)
  • Breaking? Nothing in the public API breaks, or a commit says so (! and a BREAKING CHANGE: footer). npm run check:breaking sees a line of api/ that goes, but not a feature removed, nor a reader that reads less than before.
  • Pod data. Any change to what the app writes to a pod comes with its migration (docs/migrations.md). (n/a: nothing written to a pod changes, and the user's own pod documents are read without limits as before)
  • Policy. Any change to an external rule the code follows (SM-2, the plural rules, a specification) is cited. (n/a: no external rule changes; the search reads Turtle as the parser already does)
  • Licenses. New content is MIT, or a deck states its own license; a new file has its SPDX header (npm run lint:reuse), or a new deck its line in REUSE.toml; third-party content is credited. (n/a: no file, deck or third-party content added)
  • Architecture. Imports follow docs/boundaries.md and the layers of docs/architecture.md.
  • Run. I ran the app (npm run dev) and tried the change, and considered the user journeys.
  • Authorship. I read and understand every line of this pull request, and answer for it, whatever tools helped write it (CONTRIBUTING.md).
  • Commits. Each commit is one logical change, in Conventional Commits form (type(scope): subject, ! and a BREAKING CHANGE: footer if it breaks something), with an imperative subject and a body that explains why (CONTRIBUTING.md).

The Turtle parser resolves each base declaration (`@base`, `BASE`)
against the base before it, and each relative IRI and each prefix
declaration's IRI against the base, walking the whole base each time,
and once more for each dot segment in it. A chain of relative bases, a
long base, a base with dot segments, or many prefix declarations under
one, takes time far beyond linear in the text on the main thread. A
declaration makes no triple, so none of the read limits counted it, and
the parser went on to the end of the text past them: a release small
enough to pass every limit could hold the tab for a long time, on
adding it from a link, on each later read of it or its catalog for the
deck list, and on a Studio import.

A Turtle text read under limits is now written to the parser in parts,
as a JSON-LD text already is, and the parser gets no more once the text
is past its limits. Before each part, the declarations that start in it
are checked. More than 16 bases, more than 4,096 prefixes, a base whose
path has a `.`, `..` or empty segment or an escape, or a base that can
grow past 512 characters (the longest of the address and the absolute
bases, with the relative ones added on), refuses the text with the
read-limit error, as one past the other limits is: a release with
releaseTooLarge, a deck file with deckFileTooLarge. No release the app
writes has such a base, and a document needs a few prefixes. The search
reads the text as the parser does, past strings, IRIs and comments, so a
comment cannot hide a declaration and one quoted in a card's text is not
counted; it takes time linear in the text, and where it could read a
directive other than the parser, it errs toward a refusal. Both Solid
clients do so, in a read of a document, in getSolidDatasetLinear and in
parseQuads, and the clients' contract holds both to it, with growth
cases for the time to refuse. A text read with no limits, from the
user's own pod, has its declarations as it makes them. The docs on
release and deck file limits say so.

Refs: GHSA-q3q9-h5mp-3956
@ixuz ixuz added the bug Something isn't working label Nov 1, 2026
@ixuz
ixuz enabled auto-merge November 1, 2026 02:50
@ixuz
ixuz added this pull request to the merge queue Nov 1, 2026
Any commits made after this event will not be merged.
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Nov 1, 2026
… test

CodeQL flagged replace("<", "<a") as incomplete escaping (alert #13).
It was no escaping, only a way to make the base IRI one character too
long; writing that IRI out says so plainly and clears the alert.
@ixuz
ixuz enabled auto-merge November 1, 2026 04:34
@ixuz
ixuz added this pull request to the merge queue Nov 1, 2026
Any commits made after this event will not be merged.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants