Skip to content

Fix public import classification when paths differ in public-ness - #754

Open
bufdev wants to merge 1 commit into
mainfrom
fix-transitive-public-imports
Open

Fix public import classification when paths differ in public-ness#754
bufdev wants to merge 1 commit into
mainfrom
fix-transitive-public-imports

Conversation

@bufdev

@bufdev bufdev commented Aug 4, 2026

Copy link
Copy Markdown
Member

Fixes bufbuild/buf#4633. Since buf v1.68.0 (which switched to the new compiler), a symbol re-exported via import public is not always found by importers. protoc and the legacy compiler accept the same files.

Root cause

imports.Recurse classified each transitive import by looking only at whichever direct import happened to pull it in first, so a file reachable through several direct imports with differing public-ness could be misclassified. Two shapes were affected, both of which protoc accepts.

1. The public path is not visited first.

// reexport.proto
import public "plain_leaf.proto";   // imports leaf.proto non-publicly
import public "public_leaf.proto";  // imports leaf.proto publicly

Public imports are ordered first in Directs() precisely so that the public path wins, but that does not disambiguate when both direct imports are public. plain_leaf.proto is visited first, file.Public && imp.Public is false, and leaf.proto lands in the trailing non-public segment, so reexport.proto stops re-exporting it.

#665 patched the visible half of this with seenPublicImport, which is why reexport.proto itself still compiled. But Transitive().Public was left wrong, so importers of reexport.proto could not name Leaf.

2. A direct import that is also re-exported.

// reexport.proto
import "leaf.proto";
import public "public_leaf.proto";  // imports leaf.proto publicly

reexport.proto re-exports leaf.proto via public_leaf.proto, but leaf.proto has to stay in a direct segment, and Transitive() derived public-ness purely from the segment offsets, so it could not represent this at all.

Fix

Classify every transitive import up front in classifyTransitive, quantifying over all direct imports, and record public-ness as a bit on imported rather than deriving it from the segment offsets. The offsets still order the table, but they are no longer the source of truth for Transitive().Public.

This makes the classification match protoc's DescriptorBuilder::RecordPublicDependencies, which is a closure over public_dependency edges and therefore order-independent.

Testing

  • TestImportResolution gains the two shapes above; both fail without the fix. It also now asserts Import.Visible, which had no coverage.
  • New golden tests imports/public_diamond and imports/public_shadowed; their fds.yaml output was checked byte-for-byte against protoc 33.3, including that public_dependency is unchanged in shape 2.
  • Differential fuzzing against protoc: 700 random import DAGs of up to 12 files, each file referencing every symbol protoc's rule says is visible (zero rejections), plus 838 pairwise accept/reject probes agreeing with protoc in both directions. The same fuzzer flags the bug on an unpatched build within ~30 seeds.

`imports.Recurse` classified each transitive import by looking only at
whichever direct import happened to pull it in first, so a file reachable
through several direct imports with differing public-ness could be
misclassified. Two shapes were affected, both of which `protoc` accepts:

  // reexport.proto
  import public "plain_leaf.proto";  // imports leaf.proto non-publicly
  import public "public_leaf.proto"; // imports leaf.proto publicly

`plain_leaf.proto` is visited first, so leaf.proto landed in the trailing
non-public segment and reexport.proto stopped re-exporting it. #665 patched
the `visible` half of this with `seenPublicImport`, which is why the file
itself still compiled, but importers of it did not see leaf.proto.

  // reexport.proto
  import "leaf.proto";
  import public "public_leaf.proto"; // imports leaf.proto publicly

Here leaf.proto is re-exported, but it has to stay in a direct segment, and
`Transitive` derived public-ness purely from the segment offsets.

Classify every transitive import up front, quantifying over all direct
imports, and record public-ness on `imported` rather than deriving it from
the segment offsets. The offsets still order the table, but they are no
longer the source of truth for `Transitive().Public`.

Fixes bufbuild/buf#4633.
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.

Since v1.68.0, a symbol that is re-exported via import public is not found by importers. v1.67.0 and protoc accept the same files.

1 participant