fortran: Fix ICE and self-assignment bugs with recursive allocatable finalizers [PR90519]

Derived types with recursive allocatable components and FINAL procedures
trigger an ICE in gimplify_call_expr because the finalizer wrapper's result
symbol references itself (final->result = final), creating a cycle. This
patch creates a separate __result_<typename> symbol to break the cycle.

Self-assignment (a = a) with such types causes use-after-free because the
left-hand side is finalized before copying, destroying the source. This
patch adds detection using gfc_dep_compare_expr at compile time and pointer
comparison at runtime to skip finalization when lhs == rhs.

Parenthesized self-assignment (a = (a)) creates a temporary, defeating the
simple self-assignment detection. This patch adds strip_parentheses() to
look through INTRINSIC_PARENTHESES operators and ensure deep_copy is enabled
for such cases.

Test pr112459.f90 now expects 6 _final calls instead of 12 because separate
result symbols eliminate double-counting in tree dumps.

	PR fortran/90519

gcc/fortran/ChangeLog:

	* trans-expr.cc (strip_parentheses): New helper function to strip
	INTRINSIC_PARENTHESES operators from expressions.
	(is_runtime_conformable): Use strip_parentheses to handle cases
	like a = (a) when checking for self-assignment.
	(gfc_trans_assignment_1): Strip parentheses before checking if
	expr2 is a variable, ensuring deep_copy is enabled for cases like
	a = (a). Also strip parentheses when checking for self-assignment
	to avoid use-after-free in finalization.
	(gfc_trans_scalar_assign): Add comment about parentheses handling.
	* class.cc (generate_finalization_wrapper): Create separate result
	symbol for finalizer wrapper functions instead of self-referencing
	the procedure symbol, avoiding ICE in gimplify_call_expr.

gcc/testsuite/ChangeLog:

	* gfortran.dg/finalizer_recursive_alloc_1.f90: New test for ICE fix.
	* gfortran.dg/finalizer_recursive_alloc_2.f90: New execution test.
	* gfortran.dg/finalizer_self_assign.f90: New test for self-assignment
	including a = a, a = (a), and a = (((a))) cases using if/stop pattern.
	* gfortran.dg/pr112459.f90: Update to expect 6 _final calls instead
	of 12, reflecting corrected self-assignment behavior.

Signed-off-by: Christopher Albert <albert@tugraz.at>
This commit is contained in:
Christopher Albert 2025-11-07 12:41:42 +01:00 committed by Harald Anlauf
parent 5e62a23cc3
commit 1eb696fc09
6 changed files with 223 additions and 14 deletions

View File

@ -1733,10 +1733,12 @@ generate_finalization_wrapper (gfc_symbol *derived, gfc_namespace *ns,
{
gfc_symbol *final, *array, *fini_coarray, *byte_stride, *sizes, *strides;
gfc_symbol *ptr = NULL, *idx, *idx2, *is_contiguous, *offset, *nelem;
gfc_symbol *result = NULL;
gfc_component *comp;
gfc_namespace *sub_ns;
gfc_code *last_code, *block;
char *name;
char *result_name;
bool finalizable_comp = false;
gfc_expr *ancestor_wrapper = NULL, *rank;
gfc_iterator *iter;
@ -1824,7 +1826,6 @@ generate_finalization_wrapper (gfc_symbol *derived, gfc_namespace *ns,
final->attr.function = 1;
final->attr.pure = 0;
final->attr.recursive = 1;
final->result = final;
final->ts.type = BT_INTEGER;
final->ts.kind = 4;
final->attr.artificial = 1;
@ -1832,6 +1833,26 @@ generate_finalization_wrapper (gfc_symbol *derived, gfc_namespace *ns,
final->attr.if_source = IFSRC_DECL;
if (ns->proc_name->attr.flavor == FL_MODULE)
final->module = ns->proc_name->name;
/* Create a separate result symbol instead of using final->result = final.
Self-referencing result symbols (final->result = final) create a cycle
in the symbol structure that causes an ICE in gimplify_call_expr when
the finalizer wrapper is used as a procedure pointer initializer. */
result_name = xasprintf ("__result_%s", tname);
if (gfc_get_symbol (result_name, sub_ns, &result) != 0)
gfc_internal_error ("Failed to create finalizer result symbol");
free (result_name);
if (!gfc_add_flavor (&result->attr, FL_VARIABLE, result->name,
&gfc_current_locus)
|| !gfc_add_result (&result->attr, result->name, &gfc_current_locus))
gfc_internal_error ("Failed to set finalizer result attributes");
result->ts = final->ts;
result->attr.artificial = 1;
gfc_set_sym_referenced (result);
gfc_commit_symbol (result);
final->result = result;
gfc_set_sym_referenced (final);
gfc_commit_symbol (final);
@ -1959,7 +1980,7 @@ generate_finalization_wrapper (gfc_symbol *derived, gfc_namespace *ns,
/* Set return value to 0. */
last_code = gfc_get_code (EXEC_ASSIGN);
last_code->expr1 = gfc_lval_expr_from_sym (final);
last_code->expr1 = gfc_lval_expr_from_sym (result);
last_code->expr2 = gfc_get_int_expr (4, NULL, 0);
sub_ns->code = last_code;

View File

@ -11697,7 +11697,17 @@ gfc_trans_scalar_assign (gfc_se *lse, gfc_se *rse, gfc_typespec ts,
}
gfc_add_block_to_block (&block, &rse->pre);
gfc_add_block_to_block (&block, &lse->finalblock);
/* Skip finalization for self-assignment. */
if (deep_copy && lse->finalblock.head)
{
tmp = build3_v (COND_EXPR, cond, build_empty_stmt (input_location),
gfc_finish_block (&lse->finalblock));
gfc_add_expr_to_block (&block, tmp);
}
else
gfc_add_block_to_block (&block, &lse->finalblock);
gfc_add_block_to_block (&block, &lse->pre);
gfc_add_modify (&block, lse->expr,
@ -12683,12 +12693,30 @@ alloc_scalar_allocatable_for_assignment (stmtblock_t *block,
to make sure we do not check for reallocation unneccessarily. */
/* Strip parentheses from an expression to get the underlying variable.
This is needed for self-assignment detection since (a) creates a
parentheses operator node. */
static gfc_expr *
strip_parentheses (gfc_expr *expr)
{
while (expr->expr_type == EXPR_OP
&& expr->value.op.op == INTRINSIC_PARENTHESES)
expr = expr->value.op.op1;
return expr;
}
static bool
is_runtime_conformable (gfc_expr *expr1, gfc_expr *expr2)
{
gfc_actual_arglist *a;
gfc_expr *e1, *e2;
/* Strip parentheses to handle cases like a = (a). */
expr1 = strip_parentheses (expr1);
expr2 = strip_parentheses (expr2);
switch (expr2->expr_type)
{
case EXPR_VARIABLE:
@ -13390,10 +13418,15 @@ gfc_trans_assignment_1 (gfc_expr * expr1, gfc_expr * expr2, bool init_flag,
}
/* Comply with F2018 (7.5.6.3). Make sure that any finalization code is added
after evaluation of the rhs and before reallocation. */
after evaluation of the rhs and before reallocation.
Skip finalization for self-assignment to avoid use-after-free.
Strip parentheses from both sides to handle cases like a = (a). */
final_expr = gfc_assignment_finalizer_call (&lse, expr1, init_flag);
if (final_expr && !(expr2->expr_type == EXPR_VARIABLE
&& expr2->symtree->n.sym->attr.artificial))
if (final_expr
&& gfc_dep_compare_expr (strip_parentheses (expr1),
strip_parentheses (expr2)) != 0
&& !(strip_parentheses (expr2)->expr_type == EXPR_VARIABLE
&& strip_parentheses (expr2)->symtree->n.sym->attr.artificial))
{
if (lss == gfc_ss_terminator)
{
@ -13416,13 +13449,18 @@ gfc_trans_assignment_1 (gfc_expr * expr1, gfc_expr * expr2, bool init_flag,
/* If nothing else works, do it the old fashioned way! */
if (tmp == NULL_TREE)
tmp
= gfc_trans_scalar_assign (&lse, &rse, expr1->ts,
gfc_expr_is_variable (expr2) || scalar_to_array
|| expr2->expr_type == EXPR_ARRAY,
!(l_is_temp || init_flag) && dealloc,
expr1->symtree->n.sym->attr.codimension,
assoc_assign);
{
/* Strip parentheses to detect cases like a = (a) which need deep_copy. */
gfc_expr *expr2_stripped = strip_parentheses (expr2);
tmp
= gfc_trans_scalar_assign (&lse, &rse, expr1->ts,
gfc_expr_is_variable (expr2_stripped)
|| scalar_to_array
|| expr2->expr_type == EXPR_ARRAY,
!(l_is_temp || init_flag) && dealloc,
expr1->symtree->n.sym->attr.codimension,
assoc_assign);
}
/* Add the lse pre block to the body */
gfc_add_block_to_block (&body, &lse.pre);

View File

@ -0,0 +1,15 @@
! { dg-do compile }
! PR fortran/90519
module pr90519_finalizer_mod
implicit none
type :: t
type(t), allocatable :: child
contains
final :: finalize_t
end type t
contains
subroutine finalize_t(self)
type(t), intent(inout) :: self
end subroutine finalize_t
end module pr90519_finalizer_mod

View File

@ -0,0 +1,32 @@
! { dg-do run }
! { dg-output " finalizing id\\s+0\\n finalizing id\\s+1\\n finalizer count =\\s+2\\n" }
! PR fortran/90519
module pr90519_finalizer_run_mod
implicit none
integer :: finalizer_count = 0
type :: tree_t
integer :: id = -1
type(tree_t), allocatable :: child
contains
final :: finalize_tree
end type tree_t
contains
subroutine finalize_tree(self)
type(tree_t), intent(inout) :: self
finalizer_count = finalizer_count + 1
print *, 'finalizing id', self%id
end subroutine finalize_tree
end module pr90519_finalizer_run_mod
program test_finalizer
use pr90519_finalizer_run_mod
implicit none
block
type(tree_t) :: root
root%id = 0
allocate(root%child)
root%child%id = 1
end block
print *, 'finalizer count =', finalizer_count
end program test_finalizer

View File

@ -0,0 +1,101 @@
! { dg-do run }
! Test self-assignment with recursive allocatable and finalizer
! This should preserve allocatable components after a = a and a = (a)
module self_assign_mod
implicit none
type :: node_t
integer :: value = 0
type(node_t), allocatable :: next
contains
final :: finalize_node
end type node_t
contains
subroutine finalize_node(self)
type(node_t), intent(inout) :: self
end subroutine finalize_node
end module self_assign_mod
program test_self_assign
use self_assign_mod
implicit none
call test_simple_self_assign()
call test_parenthesized_self_assign()
call test_triple_parenthesized_self_assign()
call test_array_bounds()
contains
subroutine test_simple_self_assign()
type(node_t) :: a
a%value = 100
allocate(a%next)
a%next%value = 200
! Simple self-assignment should preserve all components
a = a
if (a%value /= 100) stop 1
if (.not. allocated(a%next)) stop 2
if (a%next%value /= 200) stop 3
end subroutine test_simple_self_assign
subroutine test_parenthesized_self_assign()
type(node_t) :: a
a%value = 100
allocate(a%next)
a%next%value = 200
! Parenthesized self-assignment should also preserve all components
a = (a)
if (a%value /= 100) stop 4
if (.not. allocated(a%next)) stop 5
if (a%next%value /= 200) stop 6
end subroutine test_parenthesized_self_assign
subroutine test_triple_parenthesized_self_assign()
type(node_t) :: a
a%value = 100
allocate(a%next)
a%next%value = 200
! Triple-nested parentheses should also work correctly
a = (((a)))
if (a%value /= 100) stop 7
if (.not. allocated(a%next)) stop 8
if (a%next%value /= 200) stop 9
end subroutine test_triple_parenthesized_self_assign
subroutine test_array_bounds()
type(node_t), allocatable :: b(:), c(:)
! Test array bounds behavior with parentheses.
! Per F2023:10.2.1.3, lbound((b),1) = 1 even if lbound(b,1) = 5.
! However, for b = (b) where b is already allocated with the right shape,
! NO reallocation occurs, so bounds are preserved.
! For c = (b) where c is unallocated, c gets allocated with default bounds.
allocate(b(5:5))
b(5)%value = 500
! Self-assignment with parentheses: no reallocation (same shape), bounds preserved
b = (b)
if (.not. allocated(b)) stop 10
if (lbound(b, 1) /= 5) stop 11 ! Bounds preserved (no realloc)
if (ubound(b, 1) /= 5) stop 12
if (b(5)%value /= 500) stop 13
! Assignment to unallocated array: gets default (1-based) bounds
c = (b)
if (.not. allocated(c)) stop 14
if (lbound(c, 1) /= 1) stop 15 ! Default bounds (new allocation)
if (ubound(c, 1) /= 1) stop 16
if (c(1)%value /= 500) stop 17
end subroutine test_array_bounds
end program test_self_assign

View File

@ -34,4 +34,6 @@ program myprog
print *,"After allocation"
end program myprog
! Final subroutines were called with std=gnu and -w = > 14 "_final"s.
! { dg-final { scan-tree-dump-times "_final" 12 "original" } }
! Count reduced from 12 after PR90519 fix - separate result symbols
! disambiguate procedure references from result variables.
! { dg-final { scan-tree-dump-times "_final" 6 "original" } }