Skip to content

Lint writes to protected record kind fields - #64791

Open
Jake Bailey (jakebailey) wants to merge 3 commits into
mainfrom
jakebailey/mem/unsafe-layout-lint
Open

Jake Bailey (jakebailey) wants to merge 3 commits into
mainfrom
jakebailey/mem/unsafe-layout-lint

Conversation

@jakebailey

@jakebailey Jake Bailey (jakebailey) commented Oct 13, 2026 •

Copy link
Copy Markdown
Member

Node, type, and type mapper headers carry a kind that unsafe casts trust to identify the concrete allocation around the header. Assigning that kind, or copying a record by value, can leave a header that claims to be a larger record than its storage holds.

  • Marks those kind fields with a customlint:"protected" struct tag. A new protectedfield analyzer rejects writes to protected fields (assignment, ++/--, &, keyed or positional literals) and by-value copies, variables, parameters, results, and receivers of structs that contain one.
  • The constructors that set a kind opt out with //nolint:customlint, so any new exception is visible in review. Casts already check the kind at runtime and kind zero is never valid, so zero-valued records cannot be cast into larger ones.
  • The declarations transformer no longer clones a JS import declaration and retags its kind. It builds a real import declaration instead, with a new importTag5 case. Tests that built bare node headers or retagged nodes now use the factory.

@jakebailey
Jake Bailey (jakebailey) added this pull request to stack #64792 October 13, 2026 17:37
@typescript-automation typescript-automation Bot added Author: Team For Uncommitted Bug PR for untriaged, rejected, closed or missing bug labels Oct 13, 2026
@jakebailey Jake Bailey (jakebailey) changed the title Enforce compiler record construction and cast invariants Enforce compiler record construction and cast invariants The compact layouts lower in this stack rely on every header belonging to the concrete allocation its kind identifies. - Adds generated factory-kind guards, exhaustive construction and cast tests, and a type-aware analyzer guarding construction, retagging, header copying, and unsafe-cast boundaries. - Declaration emit now constructs ordinary imports instead of retagging JSDoc imports. - Protects the compact layouts without adding a physical-layout tag to every AST node. Oct 13, 2026
@jakebailey Jake Bailey (jakebailey) changed the title Enforce compiler record construction and cast invariants The compact layouts lower in this stack rely on every header belonging to the concrete allocation its kind identifies. - Adds generated factory-kind guards, exhaustive construction and cast tests, and a type-aware analyzer guarding construction, retagging, header copying, and unsafe-cast boundaries. - Declaration emit now constructs ordinary imports instead of retagging JSDoc imports. - Protects the compact layouts without adding a physical-layout tag to every AST node. Enforce compiler record construction and cast invariants Oct 13, 2026
@jakebailey
Jake Bailey (jakebailey) marked this pull request as ready for review October 13, 2026 17:42
Copilot AI balanced review requested due to automatic review settings October 13, 2026 17:42

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The analyzer permits untagged concrete records to escape from owner packages, leaving the central construction invariant unenforced.

1 open finding
What changed in this PR

Enforces compiler-record allocation/tag invariants needed by compact AST and checker layouts.

Changes:

  • Adds generated factory guards and exhaustive cast tests.
  • Introduces the unsafelayout analyzer and fixtures.
  • Reconstructs JSDoc imports safely during declaration emit.
File Description
tsc/​testdata/​tests/​cases/​conformance/​jsdoc/​importTag5.ts Expands declaration-import coverage.
tsc/​testdata/​baselines/​reference/​conformance/​importTag5.types Updates type baseline.
tsc/​testdata/​baselines/​reference/​conformance/​importTag5.symbols Updates symbol baseline.
tsc/​testdata/​baselines/​reference/​conformance/​importTag5.sourcemap.txt Adds source-map baseline.
tsc/​testdata/​baselines/​reference/​conformance/​importTag5.js.map Adds declaration maps.
tsc/​testdata/​baselines/​reference/​conformance/​importTag5.js Updates emit baseline.
tsc/​internal/​transformers/​declarations/​transform.go Reconstructs ordinary imports.
tsc/​internal/​checker/​types_test.go Expands cast validation.
tsc/​internal/​checker/​checker.go Couples allocation with type tagging.
tsc/​internal/​ast/​nodeaccessors_test.go Tests factory-kind guards.
tsc/​internal/​ast/​ast.go Documents immutable node kinds.
tsc/​internal/​ast/​ast_generated.go Adds generated kind checks.
tools/​scripts/​tsc/​generate-go-ast.ts Generates factory guards.
tools/​customlint/​unsafelayout.go Adds record-layout analyzer.
tools/​customlint/​testdata/​unsafelayout/​internal/​checker/​checker.go.golden Records checker diagnostics.
tools/​customlint/​testdata/​unsafelayout/​internal/​checker/​checker.go Exercises checker rules.
tools/​customlint/​testdata/​unsafelayout/​internal/​ast/​ast.go.golden Records AST diagnostics.
tools/​customlint/​testdata/​unsafelayout/​internal/​ast/​ast.go Exercises AST rules.
tools/​customlint/​testdata/​unsafelayout/​consumer/​consumer.go.golden Records consumer diagnostics.
tools/​customlint/​testdata/​unsafelayout/​consumer/​consumer.go Exercises cross-package rules.
tools/​customlint/​plugin.go Registers the analyzer.
Files not reviewed (2)
  • tsc/internal/ast/ast_generated.go: Generated file
  • tsc/testdata/baselines/reference/conformance/importTag5.js: Generated file

🧠 Review effort: Balanced


💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

Comment thread tools/customlint/unsafelayout.go Outdated
@jakebailey
Jake Bailey (jakebailey) force-pushed the jakebailey/mem/unsafe-layout-lint branch from e64cd76 to 53a5360 Compare October 13, 2026 18:36
@jakebailey
Jake Bailey (jakebailey) force-pushed the jakebailey/mem/intersection-count-reuse branch 2 times, most recently from 392be0f to fd19cf8 Compare October 13, 2026 20:06
@jakebailey
Jake Bailey (jakebailey) force-pushed the jakebailey/mem/unsafe-layout-lint branch from 53a5360 to bb0ac34 Compare October 13, 2026 20:06
Base automatically changed from jakebailey/mem/intersection-count-reuse to jakebailey/mem/shared-property-cache October 13, 2026 20:06
@jakebailey
Jake Bailey (jakebailey) removed this pull request from stack #64792 October 13, 2026 20:15
@jakebailey
Jake Bailey (jakebailey) added this pull request to stack #64805 October 13, 2026 20:16
@jakebailey
Jake Bailey (jakebailey) removed this pull request from stack #64805 October 13, 2026 20:49
@jakebailey
Jake Bailey (jakebailey) changed the base branch from jakebailey/mem/shared-property-cache to jakebailey/mem/intersection-count-reuse October 13, 2026 20:49
@jakebailey
Jake Bailey (jakebailey) force-pushed the jakebailey/mem/unsafe-layout-lint branch from bb0ac34 to d16bc19 Compare October 13, 2026 20:49
@jakebailey
Jake Bailey (jakebailey) added this pull request to stack #64806 October 13, 2026 20:50
@jakebailey
Jake Bailey (jakebailey) requested a balanced review from Copilot October 13, 2026 21:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The analyzer still permits zero-value record construction and unsafe-pointer wrapper casts that bypass its enforced invariants.

2 open findings
1 resolved since last review
Files not reviewed (2)
  • tsc/internal/ast/ast_generated.go: Generated file
  • tsc/testdata/baselines/reference/conformance/importTag5.js: Generated file

🧠 Review effort: Balanced

Comment thread tools/customlint/unsafelayout.go Outdated
Comment thread tools/customlint/unsafelayout.go Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The analyzer currently permits several construction, copying, and retagging paths that bypass its intended invariants.

3 open findings
2 resolved since last review
Files not reviewed (1)
  • tsc/testdata/baselines/reference/conformance/importTag5.js: Generated file

🧠 Review effort: Balanced

Comment thread tools/customlint/unsafelayout.go Outdated
Comment thread tools/customlint/unsafelayout.go Outdated
Comment thread tools/customlint/unsafelayout.go Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

The analyzer misses compiler-record copies through value receivers and range declarations.

0 open findings

3 resolved since last review
Files not reviewed (1)
  • tsc/testdata/baselines/reference/conformance/importTag5.js: Generated file
Previously missed (1)

In code that hasn't changed since last review

Medium severity Detect implicit copies from range variable declarations

tools/​customlint/​unsafelayout.go:335

A for _, record := range records declaration performs an implicit value copy, but the defining identifier has no TypesInfo.Types entry, so checkCopy skips it; this branch only checks assignments to tags. Consequently range over a slice, map, or channel of Node/Type records can create a detached tagged header without a diagnostic. Inspect TypesInfo.Defs for := range variables and add a fixture for this case.

This issue also appears on line 340 of the same file.

🧠 Review effort: Balanced

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The analyzer still permits untagged mapper escapes and unchecked bulk record copies.

1 open finding
Files not reviewed (1)
  • tsc/testdata/baselines/reference/conformance/importTag5.js: Generated file

🧠 Review effort: Balanced

Comment thread tools/customlint/unsafelayout.go Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

The analyzer can miss shadowed tag initialization and bulk copies through copy or variadic append.

0 open findings

1 resolved since last review
Files not reviewed (1)
  • tsc/testdata/baselines/reference/conformance/importTag5.js: Generated file
Previously missed (2)

In code that hasn't changed since last review

Medium severity Identifier-only validation misses shadowed constructor parameters

tools/​customlint/​unsafelayout.go:526

This checks only the identifier spelling, so a local variable that shadows the constructor's kind parameter would be accepted as the tag source. That lets newType/newNode initialize a header with an unrelated value while the analyzer claims the constructor is verified. Resolve the identifier through TypesInfo and compare it with the actual constructor parameter object; add a shadowing case to the analyzer fixtures.

Medium severity Bulk slice copies bypass by-value header-copy checks

tools/​customlint/​unsafelayout.go:585

Builtin bulk-copy operations are not checked here. For example, copy(dst, src) and append(dst, src...) can copy slices of concrete compiler records, but each argument has slice type, so checkCopy does not report it and this branch handles only new/make. This bypasses the analyzer's by-value header-copy rule; inspect copy/variadic append element types and add corresponding fixtures.

🧠 Review effort: Balanced

@jakebailey

Copy link
Copy Markdown
Member Author

copilot says: The layout lint is a best-effort guard against realistic mistakes, not a proof that records are always used safely. Checked casts still panic at runtime on a mismatched kind, so a gap in the lint can't silently corrupt anything. I've fixed the shadowed kind and copy/variadic append cases; further findings that need Go-level value tracking are out of scope for this PR.

@jakebailey
Jake Bailey (jakebailey) force-pushed the jakebailey/mem/unsafe-layout-lint branch 2 times, most recently from 78d2789 to 5d142e5 Compare October 14, 2026 02:23
@jakebailey
Jake Bailey (jakebailey) force-pushed the jakebailey/mem/intersection-count-reuse branch from 1482441 to 0e717e2 Compare October 14, 2026 02:23
@jakebailey
Jake Bailey (jakebailey) force-pushed the jakebailey/mem/unsafe-layout-lint branch 2 times, most recently from 229bfc4 to 96d5244 Compare October 14, 2026 03:47
@ahejlsberg
Anders Hejlsberg (ahejlsberg) force-pushed the jakebailey/mem/unsafe-layout-lint branch from 96d5244 to 2b2f16a Compare October 14, 2026 14:14
@jakebailey
Jake Bailey (jakebailey) force-pushed the jakebailey/mem/intersection-count-reuse branch from 28400fe to e9ef3a7 Compare October 14, 2026 14:17
@jakebailey
Jake Bailey (jakebailey) force-pushed the jakebailey/mem/unsafe-layout-lint branch from 2b2f16a to 0db41f9 Compare October 14, 2026 14:17
@jakebailey
Jake Bailey (jakebailey) marked this pull request as draft October 14, 2026 14:46
Base automatically changed from jakebailey/mem/intersection-count-reuse to main October 14, 2026 16:02
@jakebailey
Jake Bailey (jakebailey) force-pushed the jakebailey/mem/unsafe-layout-lint branch from 0db41f9 to 49e4344 Compare October 14, 2026 16:02
A node's Kind identifies the concrete allocation that unsafe accessors
reinterpret it as. The declarations transformer cloned a JS import
declaration and overwrote its Kind; that only works while the two kinds
happen to share one layout. Some tests similarly built bare Node headers
or retagged one node as another kind.

Build the import declaration through the factory instead, and create
real nodes in the tests, so that nothing depends on retagging.
Unsafe casts trust the kind stored in a node, type, or type mapper header
to describe the allocation around it. Assigning that field, or copying a
record by value, can make a header claim to be a larger record than its
storage holds.

Mark those fields with a customlint:"protected" struct tag and reject
writes, address-taking, keyed literals, and by-value copies of structs
that contain a protected field. The constructors that legitimately set a
kind opt out with nolint comments, so new exceptions stay visible in
review. Casts already check the kind at runtime, and kind zero is never
valid, so zero-valued records cannot be cast into larger ones.
@jakebailey
Jake Bailey (jakebailey) removed this pull request from stack #64806 October 14, 2026 16:55
@jakebailey
Jake Bailey (jakebailey) force-pushed the jakebailey/mem/unsafe-layout-lint branch from 49e4344 to 424618b Compare October 14, 2026 16:56
@jakebailey Jake Bailey (jakebailey) changed the title Enforce compiler record construction and cast invariants Lint writes to protected record kind fields Oct 14, 2026
@jakebailey
Jake Bailey (jakebailey) marked this pull request as ready for review October 14, 2026 16:57
@jakebailey
Jake Bailey (jakebailey) requested a balanced review from Copilot October 14, 2026 17:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The analyzer leaves protected writes and by-value copies undetected through several ordinary Go constructs.

3 open findings
Files not reviewed (1)
  • tsc/testdata/baselines/reference/conformance/importTag5.js: Generated file

🧠 Review effort: Balanced

Comment thread tools/customlint/protectedfield.go
Comment on lines +74 to +75
expr := c.Node().(ast.Expr)
tv, ok := p.pass.TypesInfo.Types[expr]

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Leaving this out of scope. The lint guards against kinds that disagree with their storage. An element of a []Header is already a standalone header-sized value, and the only ways to give it a wrong kind are a write or a by-value copy out of a larger record, both of which are already flagged. Copying between such slices cannot create a new bad header.

Comment thread tools/customlint/protectedfield.go Outdated
Range clauses can assign a protected field directly, and an elided
literal inside a []*T or map[K]*T composite initializes a fresh header
just like &T{...} does. Both bypassed the write check.

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

Author: Team For Uncommitted Bug PR for untriaged, rejected, closed or missing bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants