Files
cmake/Tests/RunCMake/list/TRANSFORM-PREDICATE-Reentrant.cmake
T
Mickaël Germain 3151f7824f cmList: Fix shared state across nested list(TRANSFORM) calls
The TRANSFORM action registry held one live action instance per action for
the whole process, and each instance carried per-call state: a raw Selector
pointer, REPLACE's helper, APPEND/PREPEND's operands.  That was harmless
while every action ran to completion uninterrupted, but commit c7af6e94d8
(list(TRANSFORM): Add PREDICATE selector, 2026-04-08, v4.4.0-rc1) added a
selector that runs a user function once per element, interleaved with the
transform.  User code reentering list(TRANSFORM) with the same action now
rebinds the shared instance mid-flight.  That produces silently wrong
results, and a use-after-free once any element is transformed after the
reentering one.  It needs no unusual code to hit: a predicate calling
find_package(Python) reaches list(TRANSFORM ... REPLACE) inside FindPython's
own module.

Make action objects immutable and per-call.  Operands and the selector become
constructor arguments, Initialize is deleted, and the registry becomes a
constexpr table of metadata with a MakeTransformAction factory.  The
InSelection guard, duplicated in all eight actions, moves into the base class.

This also closes a second hole in the same machinery.  TransformActionApply
overrode only the vector form of Initialize, so transform(APPLY, "f",
selector) dispatched to the empty two-argument virtual in the base, left
Selector null, and dereferenced it.  With no virtual Initialize left to
inherit, an APPLY action without a cmMakefile is no longer constructible, so
the throwing stub that guarded the vector form is no longer needed.

Close a third, from commit 651f82642c (Add APPLY action for list(TRANSFORM),
2026-04-08, v4.4.0-rc1): the cmMakefile overload performs APPLY
unconditionally but validated only the arity of the action passed to it.
APPEND, PREPEND and APPLY all take one argument, so transform(APPEND, "x",
makefile) passed validation and then silently ran APPLY, calling "x" as a
function instead of appending it.  Reject any action but APPLY up front.
That path is unreachable from CMake code, since HandleTransformCommand only
selects the overload for APPLY, and Tests/CMakeLib has no cmMakefile to drive
it with, so it carries no test.

Document the predicate's evaluation order while here.  It runs once per
element, immediately before that element would be transformed.  The manual
did not state the timing, which matters precisely because a predicate that
reenters list(TRANSFORM) observes the outer call mid-flight.

Fixes: #28031
2026-08-11 11:36:08 -04:00

92 lines
3.1 KiB
CMake

# A PREDICATE function that reenters list(TRANSFORM) must not corrupt the
# outer transform. The nested action must match the outer one; a different
# action uses separate state. Each case checks the nested list too, so
# repairing the outer call by breaking the inner one still fails.
# REPLACE, single element
function(pred_replace value out)
set(inner "hello")
list(TRANSFORM inner REPLACE "l" "L")
if(NOT inner STREQUAL "heLLo")
message(FATAL_ERROR "nested REPLACE is \"${inner}\", expected \"heLLo\"")
endif()
set(${out} TRUE PARENT_SCOPE)
endfunction()
set(replace_single "aXa")
list(TRANSFORM replace_single REPLACE "X" "Z" PREDICATE pred_replace)
if(NOT replace_single STREQUAL "aZa")
message(FATAL_ERROR "replace_single is \"${replace_single}\", expected \"aZa\"")
endif()
# REPLACE, only the last element reenters
# Nothing follows the reentering element: wrong value, not undefined behavior.
function(pred_replace_last value out)
if(value STREQUAL "cXc")
set(inner "hello")
list(TRANSFORM inner REPLACE "l" "L")
if(NOT inner STREQUAL "heLLo")
message(FATAL_ERROR "nested REPLACE is \"${inner}\", expected \"heLLo\"")
endif()
endif()
set(${out} TRUE PARENT_SCOPE)
endfunction()
set(replace_last "aXa" "bXb" "cXc")
list(TRANSFORM replace_last REPLACE "X" "Z" PREDICATE pred_replace_last)
if(NOT replace_last STREQUAL "aZa;bZb;cZc")
message(FATAL_ERROR "replace_last is \"${replace_last}\", expected \"aZa;bZb;cZc\"")
endif()
# APPEND
# Not redundant with REPLACE: the operand is a plain member, not a
# heap-allocated helper.
function(pred_append value out)
set(inner "q")
list(TRANSFORM inner APPEND "_NESTED")
if(NOT inner STREQUAL "q_NESTED")
message(FATAL_ERROR "nested APPEND is \"${inner}\", expected \"q_NESTED\"")
endif()
set(${out} TRUE PARENT_SCOPE)
endfunction()
set(append_single "a")
list(TRANSFORM append_single APPEND "_OUTER" PREDICATE pred_append)
if(NOT append_single STREQUAL "a_OUTER")
message(FATAL_ERROR "append_single is \"${append_single}\", expected \"a_OUTER\"")
endif()
# PREPEND
function(pred_prepend value out)
set(inner "q")
list(TRANSFORM inner PREPEND "NESTED_")
if(NOT inner STREQUAL "NESTED_q")
message(FATAL_ERROR "nested PREPEND is \"${inner}\", expected \"NESTED_q\"")
endif()
set(${out} TRUE PARENT_SCOPE)
endfunction()
set(prepend_single "a")
list(TRANSFORM prepend_single PREPEND "OUTER_" PREDICATE pred_prepend)
if(NOT prepend_single STREQUAL "OUTER_a")
message(FATAL_ERROR "prepend_single is \"${prepend_single}\", expected \"OUTER_a\"")
endif()
# TOUPPER, every element reenters
# The only case transforming an element after a reentering one, covering a
# selector that outlives the nested call.
function(pred_toupper value out)
set(inner a b)
list(TRANSFORM inner TOUPPER)
if(NOT inner STREQUAL "A;B")
message(FATAL_ERROR "nested TOUPPER is \"${inner}\", expected \"A;B\"")
endif()
set(${out} TRUE PARENT_SCOPE)
endfunction()
set(toupper_all x y z)
list(TRANSFORM toupper_all TOUPPER PREDICATE pred_toupper)
if(NOT toupper_all STREQUAL "X;Y;Z")
message(FATAL_ERROR "toupper_all is \"${toupper_all}\", expected \"X;Y;Z\"")
endif()