This is an automated email from the ASF dual-hosted git repository. xiaoxiang781216 pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/nuttx.git
commit c57ba6827cfea5b73f54b8e063b96b4b4e418253 Author: cuiziwei <[email protected]> AuthorDate: Wed May 6 17:37:17 2026 +0800 binfmt: add script (#!) binary format handler Add CONFIG_BINFMT_SCRIPT, enabling posix_spawn / exec on files starting with "#!" to auto-launch the interpreter with the script path preserved at argv[1]. Two pieces: 1. Generic binfmt mechanism struct binary_s gains a bool filename field. When it is set, exec_module() uses the caller's filename as the task name and forwards the full argv, instead of consuming argv[0] as the task name. 2. Script handler script_loadbinary() parses the shebang line, sets binp->filename, and delegates to load_module() with the interpreter path so the builtin / ELF loader picks it up. For example, executing /tmp/foo.sh with "#!/bin/sh" produces: task argv = { "/tmp/foo.sh", "/tmp/foo.sh", orig_args... } which lets sh auto-execute the script from argv[1]. The handler is registered last in the binfmt chain so builtin and ELF formats are always tried first. An optional shebang interpreter argument (e.g. "#!/usr/bin/env lua") is not supported in this simple flag-based scheme; use a direct interpreter path instead. Signed-off-by: cuiziwei <[email protected]> Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marco Casaroli <[email protected]> --- binfmt/CMakeLists.txt | 4 + binfmt/Kconfig | 13 +++ binfmt/Makefile | 6 ++ binfmt/binfmt.h | 13 +++ binfmt/binfmt_execmodule.c | 8 +- binfmt/binfmt_initialize.c | 8 ++ binfmt/script.c | 202 ++++++++++++++++++++++++++++++++++++++++++ include/nuttx/binfmt/binfmt.h | 5 ++ 8 files changed, 258 insertions(+), 1 deletion(-) diff --git a/binfmt/CMakeLists.txt b/binfmt/CMakeLists.txt index 9a3917c4bb7..8852722f1b0 100644 --- a/binfmt/CMakeLists.txt +++ b/binfmt/CMakeLists.txt @@ -67,6 +67,10 @@ if(CONFIG_BUILTIN) list(APPEND SRCS builtin.c) endif() +if(CONFIG_BINFMT_SCRIPT) + list(APPEND SRCS script.c) +endif() + target_sources(binfmt PRIVATE ${SRCS}) target_include_directories(binfmt PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_include_directories(binfmt PRIVATE ${NUTTX_DIR}/sched) diff --git a/binfmt/Kconfig b/binfmt/Kconfig index dd0678af577..69b11466b71 100644 --- a/binfmt/Kconfig +++ b/binfmt/Kconfig @@ -46,6 +46,19 @@ if NXFLAT source "binfmt/libnxflat/Kconfig" endif +config BINFMT_SCRIPT + bool "Enable script (#!) binary format" + default n + ---help--- + Enable support for executing files that start with a "#!" + (shebang) line. When posix_spawn / exec targets such a file, + the shebang interpreter is loaded via the normal binfmt chain + and the original script path is forwarded to it at argv[1], + so the interpreter (sh, lua, python, ...) auto-executes it. + + The interpreter itself must be loadable by another registered + binfmt handler (builtin, ELF, etc.). + config ELF bool "Enable the ELF Binary Format" default n diff --git a/binfmt/Makefile b/binfmt/Makefile index 6ebd09632f3..4b9a3048d54 100644 --- a/binfmt/Makefile +++ b/binfmt/Makefile @@ -46,6 +46,12 @@ ifeq ($(CONFIG_BUILTIN),y) CSRCS += builtin.c endif +# Script (#!) interpreter support + +ifeq ($(CONFIG_BINFMT_SCRIPT),y) +CSRCS += script.c +endif + # ELF application interfaces ifeq ($(CONFIG_ELF),y) diff --git a/binfmt/binfmt.h b/binfmt/binfmt.h index c362bf041c6..84000211c44 100644 --- a/binfmt/binfmt.h +++ b/binfmt/binfmt.h @@ -264,6 +264,19 @@ int builtin_initialize(void); void builtin_uninitialize(void); #endif +#ifdef CONFIG_BINFMT_SCRIPT +/**************************************************************************** + * Name: script_initialize / script_uninitialize + * + * Description: + * Register/unregister the script (#!) binary format handler. + * + ****************************************************************************/ + +int script_initialize(void); +void script_uninitialize(void); +#endif + #ifdef CONFIG_ELF /**************************************************************************** * Name: elf_initialize diff --git a/binfmt/binfmt_execmodule.c b/binfmt/binfmt_execmodule.c index d27218a9233..63513eb3928 100644 --- a/binfmt/binfmt_execmodule.c +++ b/binfmt/binfmt_execmodule.c @@ -279,7 +279,13 @@ int exec_module(FAR struct binary_s *binp, stackaddr = binp->stackaddr; #endif - if (argv && argv[0]) + /* If the binfmt handler requested argv preservation (e.g. script + * interpreter rewrites), use filename as the task name and forward the + * full argv. Otherwise the conventional path consumes argv[0] as the + * task name and forwards argv[1..] as arguments. + */ + + if (argv && argv[0] && !binp->filename) { ret = nxtask_init(tcb, argv[0], binp->priority, stackaddr, binp->stacksize, binp->entrypt, &argv[1], diff --git a/binfmt/binfmt_initialize.c b/binfmt/binfmt_initialize.c index ef110e898ba..9151c259708 100644 --- a/binfmt/binfmt_initialize.c +++ b/binfmt/binfmt_initialize.c @@ -74,6 +74,14 @@ void binfmt_initialize(void) } #endif +#ifdef CONFIG_BINFMT_SCRIPT + ret = script_initialize(); + if (ret < 0) + { + berr("ERROR: script_initialize failed: %d\n", ret); + } +#endif + UNUSED(ret); } diff --git a/binfmt/script.c b/binfmt/script.c new file mode 100644 index 00000000000..dfedcb9d55d --- /dev/null +++ b/binfmt/script.c @@ -0,0 +1,202 @@ +/**************************************************************************** + * binfmt/script.c + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. The + * ASF licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + ****************************************************************************/ + +/**************************************************************************** + * Included Files + ****************************************************************************/ + +#include <nuttx/config.h> + +#include <ctype.h> +#include <fcntl.h> +#include <string.h> +#include <errno.h> +#include <debug.h> + +#include <nuttx/binfmt/binfmt.h> +#include <nuttx/fs/fs.h> + +#include "binfmt.h" + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +#define SCRIPT_HEADER_MAX 128 + +/**************************************************************************** + * Private Function Prototypes + ****************************************************************************/ + +static int script_loadbinary(FAR struct binary_s *binp, + FAR const char *filename, + FAR const struct symtab_s *exports, + int nexports); + +/**************************************************************************** + * Private Data + ****************************************************************************/ + +static struct binfmt_s g_script_binfmt = +{ + NULL, /* next */ + script_loadbinary, /* load */ + NULL, /* unload */ +}; + +/**************************************************************************** + * Private Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: script_loadbinary + * + * Description: + * Verify that the file starts with "#!" and parse the shebang line to + * extract the interpreter path (and optional argument), then delegate + * loading to the interpreter via load_module(). Sets binp->filename + * so exec_module() launches the interpreter with the script path + * preserved in the forwarded argv. + * + ****************************************************************************/ + +static int script_loadbinary(FAR struct binary_s *binp, + FAR const char *filename, + FAR const struct symtab_s *exports, + int nexports) +{ + char header[SCRIPT_HEADER_MAX]; + FAR char *interp; + FAR char *end; + struct file file; + int ret; + + binfo("Attempting script load: %s\n", filename); + + ret = file_open(&file, filename, O_RDONLY | O_CLOEXEC); + if (ret < 0) + { + return ret; + } + + ret = file_read(&file, header, sizeof(header) - 1); + file_close(&file); + + if (ret < 0) + { + return ret; + } + + header[ret] = '\0'; + + /* Check for "#!" magic */ + + if (header[0] != '#' || header[1] != '!') + { + return -ENOEXEC; + } + + /* Terminate at newline */ + + end = strchr(header + 2, '\n'); + if (end != NULL) + { + *end = '\0'; + } + + /* Skip whitespace after "#!" to find interpreter path */ + + interp = header + 2; + while (isspace(*interp)) + { + interp++; + } + + if (*interp == '\0') + { + berr("ERROR: Empty interpreter in %s\n", filename); + return -ENOEXEC; + } + + /* Terminate the interpreter token at the first whitespace. Any + * optional argument after the interpreter is ignored -- the + * interpreter is relaunched with the script path as argv[1], which + * is enough for sh / lua / python to auto-execute the script. + */ + + end = interp; + while (*end != '\0' && !isspace(*end)) + { + end++; + } + + *end = '\0'; + + binfo("Script: interp='%s' script='%s'\n", interp, filename); + + /* Ask exec_module() to keep the original filename as the task name and + * forward the unchanged argv, so the interpreter receives the script + * path (the original filename) at argv[1]. + */ + + binp->filename = true; + + return load_module(binp, interp, exports, nexports); +} + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: script_initialize + * + * Description: + * Register the script binary format handler. Called from + * binfmt_initialize() AFTER builtin and ELF handlers so those + * formats are tried first. + * + ****************************************************************************/ + +int script_initialize(void) +{ + int ret; + + binfo("Registering Script Loader\n"); + + ret = register_binfmt(&g_script_binfmt); + if (ret < 0) + { + berr("ERROR: Failed to register script binfmt: %d\n", ret); + } + + return ret; +} + +/**************************************************************************** + * Name: script_uninitialize + ****************************************************************************/ + +void script_uninitialize(void) +{ + unregister_binfmt(&g_script_binfmt); +} diff --git a/include/nuttx/binfmt/binfmt.h b/include/nuttx/binfmt/binfmt.h index bff5e008aae..e3789330f18 100644 --- a/include/nuttx/binfmt/binfmt.h +++ b/include/nuttx/binfmt/binfmt.h @@ -30,6 +30,7 @@ #include <nuttx/config.h> #include <spawn.h> +#include <stdbool.h> #include <sys/types.h> #include <nuttx/sched.h> @@ -111,6 +112,10 @@ struct binary_s FAR void *stackaddr; /* Task stack address */ #endif + bool filename; /* Use filename as task name instead + * of argv[0]; preserve full argv. + */ + /* Unload module callback */ CODE int (*unload)(FAR struct binary_s *bin);
