llvm / llvm/llvm-project

[flang] PDT instantiation loses module identity of PRIVATE components and bindings (accessibility leaks, wrong generic dispatch)

Open
#219,771 0 comments 0 reactions 0 assignees View on GitHub
flang
Dominant language
LLVM
Stars
40.5k
Forks
18.7k
PR merge metrics
PR metrics pending

Description

## Summary

When flang instantiates a parameterized derived type, it clones every symbol
of the type into a new scope owned by the *instantiation site*
(`DerivedTypeSpec::Instantiate`, `InstantiateHelper::BeginComponentInstantiation`
in `flang/lib/Semantics/type.cpp`). Any check that then asks "which module
declared this symbol?" via `FindModuleContaining(sym->owner())` gets the
instantiation site's module (or none, in a main program) instead of the module
containing the type definition.

This breaks at least two checks:

1. `IsAccessible` (`flang/lib/Semantics/tools.cpp`): PRIVATE type-bound
procedures **and PRIVATE data components** of a PDT become accessible
anywhere the type is instantiated — violating F2023 7.5.5 p9 (bindings) and
7.5.4.8 p2 (components). Accepts-invalid.
2. `CollectBindings` (`flang/lib/Semantics/runtime-type-info.cpp`): the
"don't override inaccessible PRIVATE bindings" guard compares
`FindModuleContaining(symbol.owner()) != FindModuleContaining(dtScope)`,
and for PDT instantiation scopes both sides are the instantiation site's
module, so the guard never fires. An extension type in another module then
wrongly *overrides* the dispatch-table slot of a PRIVATE binding that is
not accessible to it — violating F2023 7.5.7.3 p1, which makes overriding
conditional on the parent's binding being accessible. Wrong code at
runtime.

This was identified while reviewing #218832 (see the discussion there:
https://github.com/llvm/llvm-project/pull/218832#discussion_r3880632862);
both symptoms are pre-existing and independent of that PR — verified
identical before and at current main. #218832 fixed a third instance of the
same root cause (generic resolution of private PDT binding overrides,
issue #218683).

## Reproducer A — accepts-invalid (PRIVATE leaks at the instantiation site)

```fortran
! A PDT's PRIVATE binding is callable outside the declaring module.
! Expected: error per F2023 7.5.5 p9. Observed: compiles and runs.
module priv_pdt
integer, parameter :: sp = kind(1.0)
type, abstract :: base_t(k)
integer, kind :: k = sp
integer :: n = 0
contains
procedure(iface), private, deferred :: binding
generic, public :: generic => binding
end type
type, extends(base_t) :: extension_t
contains
procedure, private :: binding => impl
end type
abstract interface
subroutine iface(x, n)
import base_t, sp
class(base_t(sp)), intent(inout) :: x
integer, intent(in) :: n
end subroutine
end interface
contains
subroutine impl(x, n)
class(extension_t(sp)), intent(inout) :: x
integer, intent(in) :: n
x%n = n
end subroutine
end module

program leak_private_name
use priv_pdt
implicit none
type(extension_t(sp)) :: x
call x%binding(1) ! should be rejected: binding is PRIVATE to priv_pdt
print *, x%n
end program
```

The same leak applies to PRIVATE **data** components (F2023 7.5.4.8 p2) —
all of the following are accepted, and each is rejected correctly when the
kind parameter is removed from the type:

```fortran
module priv_data
type :: t(k)
integer, kind :: k = 4
integer, private :: secret = 42
end type
end module

program leak_private_data
use priv_data
implicit none
type(t(4)) :: x
print *, x%secret ! accepted; should be rejected (7.5.4.8 p2)
x = t(4)(secret=7) ! structure constructor: accepted; should be rejected
end program
```

It also does not matter whether the instantiation site is a main program or a
third module: instantiating `extension_t(sp)` inside a module `m3` and calling
`x%binding` there is accepted as well (the check then compares against m3, not
priv_pdt).

## Reproducer B — wrong polymorphic dispatch through an "override" of an inaccessible PRIVATE binding

```fortran
! Valid program. Expected output (and ifx output): poly generic 1
! flang prints: poly generic 2
module m1
integer, parameter :: sp = kind(1.0)
type, abstract :: base_t(k)
integer, kind :: k = sp
integer :: which = 0
contains
procedure(iface), private, deferred :: binding
generic, public :: generic => binding
end type
type, extends(base_t) :: extension_t
contains
procedure, private :: binding => impl
end type
abstract interface
subroutine iface(x, n)
import base_t, sp
class(base_t(sp)), intent(inout) :: x
integer, intent(in) :: n
end subroutine
end interface
contains
subroutine impl(x, n)
class(extension_t(sp)), intent(inout) :: x
integer, intent(in) :: n
x%which = 1
end subroutine
end module

module m2
use m1
type, extends(extension_t) :: further_t
contains
procedure :: binding => unrelated ! NEW binding: m1's is inaccessible here
end type
contains
subroutine unrelated(x, n)
class(further_t(sp)), intent(inout) :: x
integer, intent(in) :: n
x%which = 2
end subroutine
end module

program poly_only
use m2
implicit none
class(extension_t(sp)), allocatable :: poly
allocate(further_t(sp) :: poly)
call poly%generic(0)
print *, 'poly generic', poly%which
end program
```

Since `extension_t`'s `binding` is PRIVATE in m1 and not accessible in m2,
`further_t`'s `binding` does not override it (F2023 7.5.7.3 p1); the generic
must dispatch to the inherited `impl` and print 1. `CollectBindings` puts
`unrelated` into the inherited slot instead. The non-PDT version of the same
program (kind parameter removed) prints 1 as expected.

## Root cause detail

- `DerivedTypeSpec::Instantiate` creates the instantiation scope under the
scope requiring the instantiation and clones symbols with
`common::Clone(oldSymbol.details())`; nothing records, per symbol, the
declaring scope, and instantiation scopes have no symbol
(`Scope::IsParameterizedDerivedTypeInstantiation()` is
`kind_ == DerivedType && !symbol_`). The only route back to the template is
`scope.derivedTypeSpec()->typeSymbol()`.
- A cloned `GenericDetails` keeps `specificProcs_` referencing the *template*
binding symbols (no remapping pass). The fix in #218832 relies on exactly
that to recover the declaring module; if cloned generics are ever remapped
to the cloned specifics, that fix would regress unless it, too, maps clones
back to their templates.

A possible direction: a helper that maps a symbol owned by a PDT instantiation
scope back to the corresponding symbol in
`owner().derivedTypeSpec()->typeSymbol().scope()`, used by `IsAccessible` and
by `CollectBindings`' module comparison.

## Versions

- flang at llvm-project main 11e915f2b75 (2026-08-30, includes #218832) — all
behaviors above confirmed.
- Also reproduced before #218832 (b3a2fa281ee7): identical behavior, i.e.
pre-existing, not introduced by that PR.

Contributor guide

Open the contributing guide

Research direction

Start in flang/lib/Semantics/type.cpp at DerivedTypeSpec::Instantiate and InstantiateHelper::BeginComponentInstantiation, then trace IsAccessible in flang/lib/Semantics/tools.cpp and CollectBindings in flang/lib/Semantics/runtime-type-info.cpp. Investigate how symbols in parameterized derived-type instantiation scopes can recover their template declaration module. Done means PRIVATE components and bindings remain inaccessible where required, and the Reproducer B dispatches to the inherited implementation rather than the unrelated binding.

Written by the indexing model from the issue text.

Assessment

Tech stack
fortran
Domain
compilers
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.