Skip to content

[1/10] binfmt/elf: Run a loaded module's constructors. - #19938

Open
casaroli wants to merge 1 commit into
apache:masterfrom
casaroli:elf-run-constructors
Open

[1/10] binfmt/elf: Run a loaded module's constructors.#19938
casaroli wants to merge 1 commit into
apache:masterfrom
casaroli:elf-run-constructors

Conversation

@casaroli

@casaroli casaroli commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Summary

CONFIG_BINFMT_CONSTRUCTORS has never had any effect on a module loaded through exec(). elf_loadbinary() records .preinit_array and .init_array in binp->mod, and nothing reads them back, so a C++ module's global objects stay as .bss and its constructors are dropped without a word.

This calls the arrays at the end of the load, which is where libelf_insert() has always called them for a module that arrives through dlopen().

They cannot run on the task that will run the module: exec_module() had a hook for that, exec_ctors() through nxtask_starthook(), and it went with CONFIG_SCHED_STARTHOOK.

This is the first of ten PRs that #19673 is being split into, so each can be reviewed on its own. The first four have no FDPIC content at all and do not depend on each other, so they can merge in any order. The remaining six are the FDPIC work itself, one subsystem each, and each is a no-op with CONFIG_FDPIC off: loader core, ARM relocations, the callback entry points, the exec() path, DT_NEEDED through dlopen(), then documentation and a board configuration. The others are [2/10] #19939, [3/10] #19940 and [4/10] #19941.

Impact

A module with a global constructor now gets it run, where before it was silently skipped. That is a fix, not a break, but it is worth knowing: a module that worked around the gap by initializing from its entry point will find the constructor has already run, and should drop the workaround, since left in place it initializes twice.

A module that reaches its globals through a PIC base register is left alone. The loading task carries its own base, not the module's, so a constructor would address the wrong data. Such a module gets what it got before, which is no constructors at all. Installing the base for the length of one call is a separate problem and comes with the FDPIC work in #19673.

Destructors are unchanged: elf_unloadbinary() does not call .fini_array either.

A module with no constructors is unaffected, as is any configuration with CONFIG_BINFMT_CONSTRUCTORS disabled.

Testing

Built for mps3-an547:picostest with CONFIG_BINFMT_CONSTRUCTORS enabled, which that configuration does not set by default. tools/checkpatch.sh passes.

Runtime evidence on real hardware follows tomorrow, on an RP2350. Help is welcome from anyone with a board that can load an ELF module: enable CONFIG_BINFMT_CONSTRUCTORS and run apps/examples/sotest or apps/examples/module with a module that has a global constructor. No FDPIC, no special toolchain and no filesystem support are needed.

@github-actions

github-actions Bot commented Aug 23, 2026

Copy link
Copy Markdown

MemBrowse Memory Report

No memory changes detected for:

@casaroli casaroli changed the title [1/4] binfmt/elf: Run a loaded module's constructors. [1/10] binfmt/elf: Run a loaded module's constructors. Aug 23, 2026
@casaroli
casaroli force-pushed the elf-run-constructors branch from 0af2bf7 to 0b4e9a3 Compare August 23, 2026 21:34
Comment thread binfmt/elf.c Outdated
Comment thread binfmt/elf.c

if (binp->picbase == NULL)
{
array = (FAR void (**)(void))loadinfo.preiarr;

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.

but the elf constructor must be called in the target address environment, and done at https://github.com/apache/nuttx/blob/master/arch/arm/src/common/crt0.c.

@casaroli casaroli Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You are right, and I have pushed.

The guard is now:

#if defined(CONFIG_BINFMT_CONSTRUCTORS) && !defined(CONFIG_ARCH_ADDRENV)
  if (loadinfo.ehdr.e_type != ET_EXEC && binp->picbase == NULL)

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.

but we should move so init/deinit into dlopen, not binfmt since binfmt is only used for executable.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think only ET_EXEC is fully linked with crt0. libelf_insert(), already runs the arrays for the objects, only exec() does not

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.

exec need done in crt0.c to ensure the context is right.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

cortex-m cannot use crt0. CONFIG_BINFMT_ELF_EXECUTABLE depends on ARCH_HAVE_ELF_EXECUTABLE, and no armv7-m or armv8-m chip selects it. only 13 chips do, and almost all of them have an MMU: mpfs, qemu-rv, k230, eic7700x, two litex cores, qemu and goldfish cortex-a7, four arm64 chips, x86_64.

so crt0 is the address environment case. i already skip it: ET_EXEC is skipped, and so is any build with CONFIG_ARCH_ADDRENV.

what is left is flat and relocatable. no crt0, no _start, binfmt jumps to e_entry, nothing runs .init_array. I think that is the case CONFIG_BINFMT_CONSTRUCTORS was written for, but it does not work today.

a startup object in the relocatable output would work, and binfmt would enter it instead of main. i can do that if you want (in separate PR?), should relocatable elf lose its constructors until then?

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.

elf_loadbinary runs inside the parent process, executing constructors here will generate the strange behavior. For example, the opened file handles belong the parent process, the child process can't accept them at all.

CONFIG_BINFMT_CONSTRUCTORS has never had any effect on a relocatable module
loaded through exec().  elf_loadbinary() records .preinit_array and
.init_array in binp->mod, and nothing reads them back:

  $ git grep -n "initarr" binfmt/ sched/
  binfmt/elf.c:256:  binp->mod.initarr = loadinfo.initarr;
  binfmt/elf.c:264:  binp->mod.initarr = loadinfo.initarr;

A C++ module's global objects are therefore left as .bss and its
constructors are dropped without a word.  The task that runs the module
then reads a global that no constructor ever wrote.

Call the arrays at the end of the load, which is where libelf_insert()
has always called them for a module that arrives through dlopen().  It is
the last thing the load does, so a global is initialized before the
module's main() can see it, and nothing that can still fail runs after a
constructor has.

Three cases are left alone, because something else already serves them or
this task cannot.

A fully linked executable carries crt0, which calls the same array from
_sctors to _ectors -- libs/libc/elf/gnu-elf.ld puts .init_array and .ctors
there -- on the task that runs the module and in its own address
environment.  So ET_EXEC is skipped.

A build with an address environment is skipped for the same reason: the
environment of the module is not selected here, so this task cannot reach
the array at all.  Such a build loads an executable, thus crt0 covers it.

A module that reaches its globals through a PIC base register is skipped
because the loading task carries its own base, not the module's, and a
constructor would address the wrong data.  Installing the base for the
length of one call needs a function descriptor, which is what FDPIC adds
later in this series.

The arrays cannot run on the task that will run the module.  exec_module()
had a hook for that, exec_ctors() through nxtask_starthook(), and it went
with CONFIG_SCHED_STARTHOOK.

Destructors stay as they are: elf_unloadbinary() does not call .fini_array
either, and the object is gone by the time the exiting task could.

A module with no constructors is unaffected, as is any configuration with
CONFIG_BINFMT_CONSTRUCTORS disabled.

Built for mps3-an547:picostest with CONFIG_BINFMT_CONSTRUCTORS enabled,
which the configuration does not set by default.  Runtime evidence on
hardware follows.

Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
@casaroli
casaroli force-pushed the elf-run-constructors branch from 0b4e9a3 to 0da6915 Compare August 24, 2026 07:21
@casaroli
casaroli marked this pull request as ready for review August 24, 2026 07:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area: BINFMT Size: S The size of the change in this PR is small

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants