Skip to content

Commit caf12a8

Browse files
committed
T1.28w: the L0 guards read the parser crate's name from the checked tree
The gate runs its checkpoint's copies of l0_deps.py and l0_scan.py over a candidate's tree, and their data named the parser crate as it stood (PARSER, CLASSES and ALLOWED; PARSER_CRATE and CORE_CRATES). A package that renames the crate therefore could not pass its own gate: the checkpoint's l0_deps.py refused sdm-pql as a crate with no class and an edge outside the graph, its l0_scan.py the allowlist's paths outside the old crate (T1.6b, E69(e), found by the gate at 51c387b and fed8174). Each guard now reads the parser crate's name from the checked tree's own copy of itself (l0_scan.tree_name: the last top-level assignment of PARSER, or PARSER_CRATE, as a string literal, never run), and takes its own classes, edges and core crates with the parser's entries under that name (l0_deps.tree_crates, l0_scan.parser_crates). A copy missing, unparsed, not a literal, or naming another crate the data hold is refused. Only the parser's name moves: the classes, the edges and every other list stay the running copy's, so a candidate's own copy cannot re-class a crate or grant an edge. l0_deps.py is a protected path a landing must name, and reviewers check it. selftest.py seeds it on a fixture of the crates, the bridge files and both guards' copies: the fixture as it is and the parser renamed in its crates and the guard data pass; renamed in its crates only, in the data only, the copies missing, the name not a literal and the name of another crate fail, each in both guards. With the fix undone (each guard reading its own name) the rename seed and the five refusals fail. The vocabulary fixtures now carry l0_scan.py's copy. A bug fix to a frozen guard shown by an integration failure (E56(a)); it adds no check. Refs: T1.6b, T1.24b Signed-off-by: Adi Wadaskar <awadaskar@nvidia.com>
1 parent a732611 commit caf12a8

3 files changed

Lines changed: 336 additions & 33 deletions

File tree

‎scripts/rust_core/l0_deps.py‎

Lines changed: 77 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,13 @@
2020
- the parser rule: only ``sdm-core`` depends on the parser crate, and only
2121
its adapter (``src/pql.rs``, and ``src/test_ops.rs``, compiled only with
2222
the ``test-ops`` feature) names it, by its own name or any name a
23-
``package =`` rename gives it, read from ``cargo metadata``;
23+
``package =`` rename gives it, read from ``cargo metadata``; the parser
24+
crate's name is the checked tree's: its own copy of this file's
25+
``PARSER``, read as a string literal, never run, the classes and edges
26+
this copy's with the parser's under that name (``tree_crates``), so a gate
27+
running its checkpoint's copy over a candidate that renames the crate
28+
judges the candidate's name (a bug fix at T1.28w, E56(a)); a copy
29+
missing, unparsed, not a literal, or naming another crate is refused;
2430
- nothing shipped (domain, host or binding) reaches ``sdm-testkit`` over
2531
normal or build edges;
2632
- no domain crate, and not ``sdm-host``, reaches ``pyo3``, ``jni``,
@@ -65,16 +71,18 @@
6571
import subprocess
6672
import sys
6773
from collections.abc import Iterable
74+
from dataclasses import dataclass
6875
from pathlib import Path
6976
from typing import Any
7077

7178
import tomllib
7279

7380
sys.path.insert(0, str(Path(__file__).resolve().parent))
7481
import bridge_freeze # noqa: E402
75-
from l0_scan import read_rust # noqa: E402
82+
from l0_scan import read_rust, tree_name # noqa: E402
7683

7784
REPO_ROOT = Path(__file__).resolve().parents[2]
85+
SCRIPT = Path('scripts/rust_core/l0_deps.py')
7886

7987
DOMAIN = 'domain'
8088
HOST = 'host'
@@ -280,25 +288,64 @@ def reach(self, crate: str) -> dict[str, list[str]]:
280288
return paths
281289

282290

283-
def check_classes(graph: Graph) -> list[str]:
291+
@dataclass(frozen=True)
292+
class Crates:
293+
"""The crates' data a check reads: the parser's name and the classes
294+
and edges, the parser's under that name."""
295+
296+
parser: str
297+
classes: dict[str, str]
298+
allowed: dict[str, frozenset[str]]
299+
300+
301+
def tree_crates(root: Path) -> tuple[Crates, list[str]]:
302+
"""The crates' data for the tree at ``root``: the parser's name its own
303+
copy of this file gives (``PARSER``), with this copy's ``CLASSES`` and
304+
``ALLOWED`` under it; a name this copy gives another crate is
305+
refused."""
306+
ours = Crates(PARSER, CLASSES, ALLOWED)
307+
name, problems = tree_name(root, SCRIPT, 'PARSER')
308+
others = {*CLASSES, *ALLOWED, *REQUIRED, *BINDING_FORBIDDEN} - {PARSER}
309+
if name in others:
310+
problems = [f"the tree's {SCRIPT}: PARSER names {name}, another crate"]
311+
if name is None or problems:
312+
return ours, problems
313+
314+
def renamed(crate: str) -> str:
315+
return name if crate == PARSER else crate
316+
317+
return (
318+
Crates(
319+
name,
320+
{renamed(c): kind for c, kind in CLASSES.items()},
321+
{
322+
renamed(c): frozenset(renamed(d) for d in deps)
323+
for c, deps in ALLOWED.items()
324+
},
325+
),
326+
[],
327+
)
328+
329+
330+
def check_classes(graph: Graph, crates: Crates) -> list[str]:
284331
problems = [
285332
f'{crate}: a workspace crate with no class; add it to CLASSES and ALLOWED'
286333
for crate in graph.members
287-
if crate not in CLASSES
334+
if crate not in crates.classes
288335
]
289336
problems += [
290337
f'{crate}: classified, but not a workspace crate'
291-
for crate in CLASSES
338+
for crate in crates.classes
292339
if crate not in graph.members
293340
]
294341
return problems
295342

296343

297-
def check_edges(graph: Graph) -> list[str]:
344+
def check_edges(graph: Graph, crates: Crates) -> list[str]:
298345
problems = []
299346
members = set(graph.members)
300347
for crate in graph.members:
301-
allowed = ALLOWED.get(crate, frozenset())
348+
allowed = crates.allowed.get(crate, frozenset())
302349
first_party = {dep for dep, _ in graph.direct(crate) if dep in members}
303350
for dep, kinds in graph.direct(crate):
304351
if dep in members and dep not in allowed:
@@ -312,21 +359,22 @@ def check_edges(graph: Graph) -> list[str]:
312359
for crate in graph.members:
313360
if crate == 'sdm-core':
314361
continue
315-
if any(dep == PARSER for dep, _ in graph.direct(crate)):
362+
if any(dep == crates.parser for dep, _ in graph.direct(crate)):
316363
problems.append(
317-
f"{crate} -> {PARSER}: only sdm-core's adapter reaches the parser"
364+
f"{crate} -> {crates.parser}: only sdm-core's adapter reaches "
365+
'the parser'
318366
)
319367
return problems
320368

321369

322-
def check_reach(graph: Graph) -> list[str]:
370+
def check_reach(graph: Graph, crates: Crates) -> list[str]:
323371
problems = []
324372
for crate in graph.members:
325-
kind = CLASSES.get(crate)
373+
kind = crates.classes.get(crate)
326374
if kind not in SHIPPED:
327375
continue
328376
reached = graph.reach(crate)
329-
dev = [c for c in reached if CLASSES.get(c) == DEV]
377+
dev = [c for c in reached if crates.classes.get(c) == DEV]
330378
for target in dev:
331379
problems.append(
332380
f'{crate} reaches {target} ({" -> ".join(reached[target])}); '
@@ -349,13 +397,13 @@ def crate_root(graph: Graph, crate: str) -> Path:
349397
return graph.manifests[crate].parent
350398

351399

352-
def check_parser_users(graph: Graph) -> list[str]:
400+
def check_parser_users(graph: Graph, parser: str) -> list[str]:
353401
if 'sdm-core' not in graph.manifests:
354402
return ['sdm-core: missing']
355403
root = crate_root(graph, 'sdm-core')
356404
problems = []
357-
names = {PARSER.replace('-', '_')} | graph.externs.get(
358-
('sdm-core', PARSER), set()
405+
names = {parser.replace('-', '_')} | graph.externs.get(
406+
('sdm-core', parser), set()
359407
)
360408
pattern = re.compile(
361409
r'\b(' + '|'.join(re.escape(n) for n in sorted(names)) + r')\b'
@@ -380,7 +428,7 @@ def check_parser_users(graph: Graph) -> list[str]:
380428
return problems
381429

382430

383-
def check_unsafe(graph: Graph) -> list[str]:
431+
def check_unsafe(graph: Graph, crates: Crates) -> list[str]:
384432
problems = []
385433
workspace = tomllib.loads((graph.workspace_root / 'Cargo.toml').read_text())
386434
workspace_rust = (
@@ -391,7 +439,7 @@ def check_unsafe(graph: Graph) -> list[str]:
391439
'rust/Cargo.toml: the workspace lints do not forbid unsafe_code'
392440
)
393441
for crate in graph.members:
394-
kind = CLASSES.get(crate)
442+
kind = crates.classes.get(crate)
395443
manifest = tomllib.loads(graph.manifests[crate].read_text())
396444
lints = manifest.get('lints', {})
397445
if kind in (DOMAIN, HOST):
@@ -482,17 +530,21 @@ def check_bridges(root: Path, parser_root: Path) -> list[str]:
482530

483531
def run_checks(metadata: dict[str, Any], root: Path) -> dict[str, list[str]]:
484532
graph = Graph(metadata)
533+
crates, unreadable = tree_crates(root)
534+
if unreadable:
535+
return {'classes': unreadable}
536+
parser = crates.parser
485537
parser_root = (
486-
crate_root(graph, PARSER)
487-
if PARSER in graph.manifests
488-
else root / 'rust/crates' / PARSER
538+
crate_root(graph, parser)
539+
if parser in graph.manifests
540+
else root / 'rust/crates' / parser
489541
)
490542
return {
491-
'classes': check_classes(graph),
492-
'edges': check_edges(graph),
493-
'reach': check_reach(graph),
494-
'parser': check_parser_users(graph),
495-
'unsafe': check_unsafe(graph),
543+
'classes': check_classes(graph, crates),
544+
'edges': check_edges(graph, crates),
545+
'reach': check_reach(graph, crates),
546+
'parser': check_parser_users(graph, parser),
547+
'unsafe': check_unsafe(graph, crates),
496548
'bridges': check_bridges(root, parser_root),
497549
}
498550

‎scripts/rust_core/l0_scan.py‎

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,13 @@
1717
the like, a ``py``-prefixed Python type name (``pyint``, ``pystr``), a
1818
dunder name, or a string literal naming Python, pandas, NumPy or pyarrow,
1919
is emulation vocabulary. The public names the allowlisted modules define
20-
may be used anywhere in their crate.
20+
may be used anywhere in their crate. The parser crate's name is the
21+
checked tree's: its own copy of this file's ``PARSER_CRATE``, read as a
22+
string literal, never run, with ``CORE_CRATES``' parser entry under that
23+
name (``parser_crates``), so a gate running its checkpoint's copy over a
24+
candidate that renames the crate judges the candidate's name (a bug fix at
25+
T1.28w, E56(a)); a copy missing, unparsed, not a literal, or naming
26+
another crate is refused.
2127
- ``sql``: no SQL fragment and no provider match in ``sdm-schema``,
2228
``sdm-engine``, ``sdm-request`` or ``sdm-core`` (migration plan,
2329
"Ownership"; ``sdm-core`` from T1.21c). It is a
@@ -122,6 +128,7 @@
122128
import tomllib
123129

124130
REPO_ROOT = Path(__file__).resolve().parents[2]
131+
SCRIPT = Path('scripts/rust_core/l0_scan.py')
125132

126133
# The crates whose code ships and whose rules are the core's: the domain
127134
# crates and the host layer. The binding, sdm-py, talks to Python by design,
@@ -341,7 +348,59 @@ def crate_dir(root: Path, crate: str) -> Path:
341348
)
342349

343350

344-
def read_allowlist(root: Path) -> tuple[list[Path], list[str]]:
351+
def tree_name(
352+
root: Path, script: Path, name: str
353+
) -> tuple[str | None, list[str]]:
354+
"""The string the checked tree's own copy of ``script`` assigns to
355+
``name`` at its top level, read as a literal, never run; or why it
356+
cannot be read. A gate runs its checkpoint's copy of a guard over a
357+
candidate's tree, so a crate's name in a guard's data is the tree's
358+
(T1.28w: the checkpoint's copy named the parser crate as it stood, so a
359+
package that renamed it could not pass its own gate)."""
360+
path = root / script
361+
if not path.is_file():
362+
return None, [f"the tree's {script} is missing"]
363+
try:
364+
tree = ast.parse(path.read_text(encoding='utf-8'))
365+
except SyntaxError as error:
366+
return None, [f"the tree's {script} does not parse: {error.msg}"]
367+
value: ast.expr | None = None
368+
for node in tree.body:
369+
target = (
370+
node.target
371+
if isinstance(node, ast.AnnAssign)
372+
else node.targets[0]
373+
if isinstance(node, ast.Assign) and len(node.targets) == 1
374+
else None
375+
)
376+
if isinstance(target, ast.Name) and target.id == name:
377+
value = node.value # type: ignore[union-attr]
378+
if (
379+
isinstance(value, ast.Constant)
380+
and isinstance(value.value, str)
381+
and value.value
382+
):
383+
return value.value, []
384+
return None, [f"the tree's {script}: {name} is not a string literal"]
385+
386+
387+
def parser_crates(root: Path) -> tuple[str, tuple[str, ...], list[str]]:
388+
"""The parser crate's name in the tree at ``root`` (its copy of this
389+
file's ``PARSER_CRATE``), and ``CORE_CRATES`` with the parser under it;
390+
a name this copy gives another crate is refused."""
391+
name, problems = tree_name(root, SCRIPT, 'PARSER_CRATE')
392+
others = {*CORE_CRATES, *DIALECT_FREE_CRATES, *DEV_CRATES} - {PARSER_CRATE}
393+
if name in others:
394+
problems = [
395+
f"the tree's {SCRIPT}: PARSER_CRATE names {name}, another crate"
396+
]
397+
if name is None or problems:
398+
return PARSER_CRATE, CORE_CRATES, problems
399+
crates = tuple(name if c == PARSER_CRATE else c for c in CORE_CRATES)
400+
return name, crates, []
401+
402+
403+
def read_allowlist(root: Path, parser: str) -> tuple[list[Path], list[str]]:
345404
entries: list[Path] = []
346405
problems: list[str] = []
347406
path = root / ALLOWLIST
@@ -355,7 +414,7 @@ def read_allowlist(root: Path) -> tuple[list[Path], list[str]]:
355414
if not (root / entry).is_file():
356415
problems.append(f'{ALLOWLIST}:{number}: {line} does not exist')
357416
continue
358-
parser_src = Path('rust/crates') / PARSER_CRATE / 'src'
417+
parser_src = Path('rust/crates') / parser / 'src'
359418
if parser_src not in entry.parents:
360419
problems.append(
361420
f'{ALLOWLIST}:{number}: {line} is outside {parser_src}; '
@@ -376,7 +435,10 @@ def vocabulary_term(identifier: str) -> str | None:
376435

377436

378437
def check_vocabulary(root: Path) -> list[str]:
379-
allowed, problems = read_allowlist(root)
438+
parser, crates, unreadable = parser_crates(root)
439+
if unreadable:
440+
return unreadable
441+
allowed, problems = read_allowlist(root, parser)
380442
allowed_set = {root / p for p in allowed}
381443
# The names the allowlisted modules define, and the modules themselves,
382444
# may be named anywhere in their crate.
@@ -385,7 +447,7 @@ def check_vocabulary(root: Path) -> list[str]:
385447
permitted.update(
386448
_PUB_ITEM.findall(read_rust(path.read_text()).skeleton)
387449
)
388-
for crate in CORE_CRATES:
450+
for crate in crates:
389451
src = crate_dir(root, crate) / 'src'
390452
for path in rust_files(src):
391453
if path in allowed_set:
@@ -394,7 +456,7 @@ def check_vocabulary(root: Path) -> list[str]:
394456
rel = path.relative_to(root)
395457
for offset, name in identifiers(source):
396458
term = vocabulary_term(name)
397-
if term and not (crate == PARSER_CRATE and name in permitted):
459+
if term and not (crate == parser and name in permitted):
398460
problems.append(
399461
f'{rel}:{source.line(offset)}: `{name}` is Python-emulation '
400462
f'vocabulary ({term}); emulation lives only in the modules '

0 commit comments

Comments
 (0)