From a4e922953c8d036a43ed002dc765fdcb0ee469fe Mon Sep 17 00:00:00 2001 From: Felix Fietkau Date: Fri, 9 Oct 2026 20:41:00 +0200 Subject: [PATCH] ucode: update to Git HEAD (2026-10-18) f8c2d64f546c uloop: allow reusing the existing environment for a new process 1294001ec6cb uloop: fix array reference dd91bd8396a5 tests: add test cases for use-after-free gc bugs d96db3ea5ef2 json: fix use-after-free of read method across iterations 8a74d68a34ab lib: anchor working values in filter/map/callfunc across user code 2225a502e805 socket: anchor poll() result across user fileno() callbacks 6a061cf4ef34 zlib: fix use-after-free of read method across iterations dbe92bca180b types: free uvcount=0 resources on retain=true sweep b0574723d27d zlib: zero-fill the output reserve window cc0698b723ed serial: add module skeleton, build wiring and fd helpers 114ffb8962e2 serial: add raw termios access and termios constants 3bdeeb0b1ef7 serial: add setspeed, setraw and setblocking helpers 2b22a1ac41b0 serial: add modem control line access (out-of-band signals) bed2a12895ce serial: add break, drain, flush and queue inspection 697fe07b14db serial: add setserial-style serial_struct access (Linux) 504530a290ad serial: add termios round-trip test d6544940e762 ubus: fix reply leak in deferred data callback dfca6438bc55 fs: reimplement popen() with fork/exec and true shell-free array support 9a87ce92207a debug: rework memdump to invoke fileno() on resource values bed321219636 types: expose exception_type_strings array 67b13e97a643 lib: allow die() to re-throw exception objects f2d62f82a522 lib: deduplicate source files by resolved path in vm->sources cache 255f69081304 socket: add CAN socket support 6adff7054893 rtnl: add CAN link type support b79e20e1eee5 rtnl: add vxcan link type support 5870b9995ff4 rtnl: zero reserved struct headers in nested attributes abb80c69ed2d include: add `unused` macro ff509e0698dc include: register resource type prototypes with VM c404fe2d6556 types: add per-instance prototype support for extended resources 7fb34544e945 ci: install libffi on test runners 23d5561b04b2 ffi: add LibFFI-based foreign function interface module f26fabfe3ecb ffi: Add .cast(), .copy(), .string() as ctype instance methods 19ffa8aa3218 vm: release resource type prototypes before the final GC 48e38fc6376f tests: move ffi ctype instance method tests to proper place 4d687afdc216 compiler, vm: implement tail call optimization 7074129727d9 vm: indicate tail call optimized frames in stack traces c3dc162f0abc lib: reject circular prototype chains in proto() fe9e2947d351 compiler: reject adjacent template literals e9972df30d14 linux/types.h: add 128-bit integer types 76baa6fc264a types: fix instance-proto lookup offset in ucv_key_get() fbdd169efd17 ubus: store per-call request proto in the instance proto slot 0aab97337e21 ubus: drop per-instance proto when releasing resources 4eff16514b57 uloop: report signal termination in process callbacks 9ba6ea147dee headers: split public and private API headers 4c6e64425d14 ffi: reject function values passed as variadic arguments af69c8d226a9 types, vm, lib: add metamethod support for prototyped values 1fd07aa2ba34 ffi: represent wrapped C functions as "ffi.cfn" resources c966006d635f ffi, vm: support property syntax for cdata via __get__ / __set__ 672439e7c530 vm: add debugger infrastructure (breakpoints, offsets, nested-exit fix) a5b5925b53ab debug: add interactive command line debugger with -x/-X CLI switches c2476472eb23 debug: add remote debug protocol with a client/server split d968d33f455e debug: move the debugger onto a line-based protocol, split server/client 1a771e3c1ffd debug: add line editing, EVAL, Ctrl-C interrupt and tail-call annotations 26bfd5851bcc debug: synthesize PAUSED when a resuming command cannot resume 00a8fa9dea6c ffi: add ffi.closure() for explicit C function pointer creation f061ee2e4369 ffi: fix Clang static analyzer false positives in closure code 8c017f171f0a types: Fix warning unused variable 0273114f13a6 types: raise type error for non-index array property sets in strict mode d2c7b24e366e struct: rename ucv_as_double to ucv_as_float64 4dd080fe47ab headers: privatize uc_program_entry/ucv_closure_new/ucv_compare, add uc_program_main() fda0498102bd lib, vm: replace ad-hoc value casts with ucv_as_* macros ad88dfb621d6 metamethods: object-valued __get__/__set__/__delete__ delegate instead of mis-delegating return values ea4c65b3c255 lib/fs.c: set errno on failed open() 10e44a53f808 lib.c: add optional offset argument to index() and rindex() 37d10ce70189 lib/io.c: fix io.pipe() jsdoc example 100f354f5d75 lib/math.c: export math constants (PI, E, ...) ca70f18e31b3 lib/resolv.c: validate retries/timeout and guard retry-interval division 22f64a32f784 treewide: adjust references after jow-/ucode => ucode-lang/ucode move 6373faeac549 docs: fix deep merge example 1561320a00b4 lib,main,types: include json-c compat shim where needed 83ed2a8ba815 tutorials: fix claims that disagree with the interpreter cef095d846bd Update CNAME 0c39aaa3d277 lib/log.c: accept arrays of channel and option names ad3a5d580983 lib/log.c: use the documented defaults for omitted arguments 9e1d747640fe devcontainer: add development container configuration d2c81218773b vm: clear a pending exception when entering uc_vm_execute deb063dba30d build: add minimal emscripten/wasm32 build guards 75489e3ca2d5 source: initialize runpath in uc_source_new_buffer 4e65efb46854 build: add emscripten/wasm32 CMake support 24b0a2ad2e05 lib: underline the statement an error occurred in 926a2bf7e219 lib: name the offending expression when a call fails 068e7b222b4f ucode: fix socket bind() ignoring port argument a4da50de2430 io: read the current termios before applying attributes 38f21be1bcbb uloop: redirect the task child's standard descriptors to /dev/null af8a3830661a uloop: close the task output pipe even without an input callback e6e03000000f uloop: release the handle when uloop_fd_add() fails 1e6b56558617 uloop: read task messages without losing them to EAGAIN 6e1592a2862f fs: report a write that only fails when the file is closed d075095287c8 fs: keep the bytes a short read already delivered 8c01857cadd0 resolv: file each NS, CNAME and PTR record under its own type 4fec71ddd26d struct: decode any non-zero byte as true in native mode edf5b32c5d4c zlib: inflate every member of a concatenated stream b844c63b2381 resolv: release the TXT buffer when the record is malformed 664b70791957 compiler: unwind the nested function compiler on a bad argument list ab988ea3159a struct: accumulate signed integers in an unsigned type d996fade5f36 compiler: do not overflow int building the wildcard import operand 24df8d20bde2 vm: give division by zero the right sign, and NaN for zero over zero a5c102ea2cfd tests: expect statement underline in forward declaration error ccbe4228913d types: clear resource value slots after releasing them c7e550d05ec4 socket: reject recv() lengths that overflow the printbuf offset b1c7693fc5c6 socket: do not free caller-owned control data in sendmsg() 0630355f1ce6 socket: size msg_controllen from the payload, not the cmsg length b51836754a9e types: do not dereference tagged values in ucv_string_length() 8fac388410d4 struct: bound the base64 pack field to the space reserved for it b8a8646cbb2c struct: bounds-check the field offset when unpacking 5544af285082 struct: allocate the string header when packing zero bytes 8bd004a1a423 lib: free the string match() allocated, not the one it advanced e98574152fcb io: return ENOMEM rather than aborting on an oversized read 9b45eef92402 io: give ioctl() a heap buffer the kernel cannot overrun 531c09289a4f resolv: check the SOA rdata bound after the two names 4d12bc9af856 ubus: detect cycles when converting values to blobmsg 5bcb94aec84d vallist: reject an inline string constant longer than its buffer cd73805bcb0f program: validate the export name length when loading bytecode fe546444f289 program: terminate the strings the bytecode loader reads ba4b639597e6 compiler: do not return a bytecode program that was just freed 19247f51e22a compiler: unwind the patchlist when an expression fails to parse 58774dd67277 rtnl, nl80211: check every netlink reserve return 8fff82c22a7c fs: stop readfile() losing data when the buffer cannot grow 39454f8830a9 zlib: act on the output reserve failing a90a1d2c9ebe lib: size the b64dec output buffer from the ceiling ad3be9a33322 ubus: bound the nesting depth when decoding a message 953cd5a9ea23 ubus: do not close a descriptor the script still owns 5a2c5d93ce53 lexer: discard the partial string when an escape is rejected 428232cfce1c vm: range-check local slots, upvalue indices and stack depth e170905c143b program: bound the counts and lengths the bytecode loader reads 6065b2ce63cc program: bound an export name by the bytes left in the file e1a8b8cf5d21 compiler: do not call a missing infix handler inside parentheses 2cd8d1b9726b compiler: do not read the last instruction of an empty chunk 468994d345ff compiler: compare local names without assuming they are strings 04d59c6395de compiler: guard the remaining local name comparisons 19656543aac0 types: dispatch __delete__ for keys arrays and resources cannot store dd57d12404ec compiler: do not drop the loop condition at chunk offset 0 dc9386a190fb jsdoc: accept a doc comment at the very start of a file 04d3ae59abd4 serial: add JSDoc documentation for module functions and constants 959206e27878 netaddr: add new netaddr module ported from LuCI's luci-lib-ip 904358422b3b docs: restructure tutorials around the new manual af9324c9e7c5 readme: point at the new project sites 212dd12150b6 lib: optimize trim(), ltrim(), rtrim() 2c8c36be454d struct: fix infinite loop in grow_buffer() for small sizes 131c041a61d5 platform: export the macOS pipe2() replacement to modules e6a1f945db65 fs: find the PATH separator without strchrnul() 4711d3c2c5dc lib: reject mutations of the container being sorted f0a4664b97b0 rtnl: recover the event listener from receive buffer overruns eaa09ca64305 rtnl: free the partial reply of a request that fails 5c655c357c53 rtnl: return true for an acknowledgement without data 0bfb7b97bf2f nl80211: recover the event listener from receive buffer overruns 30dfbb218fa1 nl80211: return true for an acknowledgement without data 395cfeead63f nl80211: fail a request whose reply the event socket lost f8f8fc55126f nl80211: run listener callbacks outside of a request f60d9b01b59c nl80211: free the partial reply of a request that fails 234e524f7ae0 nl80211.h: update to mainline d24e8ac715de 80be3ab390ee nl80211: request and decode the per-link statistics of a station c51e15921dc6 lib: fix module namespace export mapping for linked imports b5ce44358354 compiler: report strict redeclarations at the redeclaring name e22e4362e6d5 tests: allow restricting a testcase to platforms ef5b35ea145e tests: do not check forked children with valgrind a6b8bfe1b582 tests: allow a testcase to skip itself 7fe06b46f3c5 uloop: expose ULOOP_ERROR_CB 115d308a8514 uloop: release timers and handles once they are finished 0c1b005cd0d3 uloop: disarm a timer on set() without a timeout 986390458007 uloop: disarm an interval on set() without a value 503a9bfcffd2 uloop: test that a finished task closes its pipes f4fa308dc4f2 uloop: keep a task alive while its output callback runs fa81c11ff2a7 uloop: release a finished task after a callback exception 21a2140f2324 uloop: reject a handle without an IO event 0ea69b1e736a uloop: do not unregister a reused descriptor number on handle delete ddb06a197ef5 compiler: make strict mode function names constant bindings 620eff6a772d uloop: keep a handle alive while epoll may still report it 1942e5fce058 uloop: disable a handle whose removal fails 9a3acc008531 uloop: expose ULOOP_PRIORITY 1f54bd285339 uloop: read task messages without blocking the event loop c2319e9b26bb uloop: close the task pipes when fork() fails db2191ab014e devcontainer: remove formatting options managed by editorconfig 486104700227 compiler: account for nested imports in the importer's export offset be870dd61b95 types: give the collector a dedicated mark bit 2e0b25a23c09 socket: fall back to SO_TIMESTAMPNS for the OLD alias 4141cb38f398 ubus: pass an exception of a deferred completion callback to the guard 0fcd31713fee ubus: do not pass exit() in a callback to the guard c80855059801 socket: fix the file descriptor on 64-bit big-endian a18924e53045 tests: move the socket poll test to the lib_socket directory f4ce7eb00d22 source: allocate and release a source in one place 8d25f029d96b compiler: give dynamic loads the position of the import 08f3be072dd0 vm: read the callee type before uc_vm_call() runs it 3c9f214a2d56 serial: add RS485 mode access (Linux) 100d19ac369f vm: keep the exception of a module that failed to load 3c0a3c7c1023 vm: read the importing frame after the module load 2cf103aded34 tests: skip the RS485 serial test under valgrind 5c8d6d544730 lib: fix stack use after scope in compile config parsing 0dc042eefa5b source: rework the text access API 1dd1f88c81bb source: close the file of a source after its compilation 69db946e7c39 lib: remove the source cache of loadfile() 4436a931dc38 compiler: start each compilation from a clean source 4503599e49f1 source: read a file that cannot seek into a buffer 330cdab782be compiler: release the name list of an import() statement 153c627fd445 lib: do not store a failed module in the module table 3bf604919140 vm: keep the exception of a failed dynamic module load 71b4a144b438 lib: compile a file loaded from a module body as a program 080cbd79727f lib: release the main function of a required file 6c30ef7fd30c vm: release the previous binding of an import 6e404de5c0b1 vm: let the main closure of a program own its module exports 0adb7ba6b4d1 vm: do not interrupt the exports of a module 8cfcffe5abcd lib: find the exports of a precompiled module by their count 53ebdff102e6 vm: size object literals from the compiler's key count f8a757295dc9 types: start array entry vectors at 2 slots 00091f829801 types: avoid accessing values after converter exit 78f83f8d6b59 lib: take one reference on the prototype in proto() 96cdb4549b59 compiler: fix endless loop on syntax errors in template placeholders fb54dfe1733c types: release the old instance prototype of a resource struct uc_vm grows at its end, and programs such as hostapd embed it, so bump PKG_ABI_VERSION. Add packages for the new ffi, netaddr and serial modules. Turn them off for the host build, since ffi is otherwise built whenever libffi happens to be in the staging directory. The project moved to ucode-lang/ucode, so update PKG_SOURCE_URL. Signed-off-by: Felix Fietkau --- package/utils/ucode/Makefile | 27 +- ...-for-an-acknowledgement-without-data.patch | 64 ---- ...s-exit-requests-to-the-exception-gua.patch | 69 ++++ ...nction-to-prepare-the-child-in-proce.patch | 354 ++++++++++++++++++ ...low-reusing-the-existing-environment.patch | 57 --- ...d-optional-setup-callback-to-process.patch | 85 ----- 6 files changed, 444 insertions(+), 212 deletions(-) delete mode 100644 package/utils/ucode/patches/100-rtnl-return-true-for-an-acknowledgement-without-data.patch create mode 100644 package/utils/ucode/patches/100-uloop-do-not-pass-exit-requests-to-the-exception-gua.patch create mode 100644 package/utils/ucode/patches/101-uloop-allow-a-function-to-prepare-the-child-in-proce.patch delete mode 100644 package/utils/ucode/patches/110-uloop-allow-reusing-the-existing-environment.patch delete mode 100644 package/utils/ucode/patches/111-uloop-add-optional-setup-callback-to-process.patch diff --git a/package/utils/ucode/Makefile b/package/utils/ucode/Makefile index eb6abd6adb6..bc266b3e0db 100644 --- a/package/utils/ucode/Makefile +++ b/package/utils/ucode/Makefile @@ -11,14 +11,14 @@ PKG_NAME:=ucode PKG_RELEASE:=1 PKG_SOURCE_PROTO:=git -PKG_SOURCE_URL=https://github.com/jow-/ucode.git -PKG_SOURCE_DATE:=2026-07-09 -PKG_SOURCE_VERSION:=b885dd0fe1e974551fb1233ca5f3aa74d80b74d9 -PKG_MIRROR_HASH:=654ba9dc8714aa8caf75ef422962fb465c2b21c8fbe4518a47f9de391cce6e71 +PKG_SOURCE_URL=https://github.com/ucode-lang/ucode.git +PKG_SOURCE_DATE:=2026-10-18 +PKG_SOURCE_VERSION:=d2e8b8d2052778557ccca7e781d2ad423324961b +PKG_MIRROR_HASH:=d5a9c35c97abe4aca04f5fa970a5270d932066571ed684bb22094301978b8d58 PKG_MAINTAINER:=Jo-Philipp Wich PKG_LICENSE:=ISC -PKG_ABI_VERSION:=20230711 +PKG_ABI_VERSION:=20261009 PKG_BUILD_DEPENDS:=libmd HOST_BUILD_DEPENDS:=libjson-c/host @@ -54,7 +54,10 @@ CMAKE_HOST_OPTIONS += \ -DDEBUG_SUPPORT=ON \ -DLOG_SUPPORT=OFF \ -DZLIB_SUPPORT=ON \ - -DDIGEST_SUPPORT=OFF + -DDIGEST_SUPPORT=OFF \ + -DFFI_SUPPORT=OFF \ + -DNETADDR_SUPPORT=OFF \ + -DSERIAL_SUPPORT=OFF define Package/ucode/default @@ -137,6 +140,10 @@ $(eval $(call UcodeModule, \ debug, DEBUG_SUPPORT, +libubox +libucode, \ The debug plugin module provides runtime debugging and introspection facilities.)) +$(eval $(call UcodeModule, \ + ffi, FFI_SUPPORT, +libffi, \ + The ffi plugin module allows ucode scripts to call C functions in shared libraries through libffi.)) + $(eval $(call UcodeModule, \ fs, FS_SUPPORT, , \ The filesystem plugin module allows interaction with the local file system.)) @@ -149,6 +156,10 @@ $(eval $(call UcodeModule, \ math, MATH_SUPPORT, , \ The math plugin provides access to various procedures.)) +$(eval $(call UcodeModule, \ + netaddr, NETADDR_SUPPORT, , \ + The netaddr plugin provides IPv4, IPv6 and MAC address parsing and calculation.)) + $(eval $(call UcodeModule, \ nl80211, NL80211_SUPPORT, +libnl-tiny +libubox, \ The nl80211 plugin provides access to the Linux wireless 802.11 netlink API.)) @@ -161,6 +172,10 @@ $(eval $(call UcodeModule, \ rtnl, RTNL_SUPPORT, +libnl-tiny +libubox, \ The rtnl plugin provides access to the Linux routing netlink API.)) +$(eval $(call UcodeModule, \ + serial, SERIAL_SUPPORT, , \ + The serial plugin provides termios, modem control line and serial port configuration access.)) + $(eval $(call UcodeModule, \ socket, SOCKET_SUPPORT, , \ The socket plugin provides access to IPv4, IPv6, Unix and packet socket APIs.)) diff --git a/package/utils/ucode/patches/100-rtnl-return-true-for-an-acknowledgement-without-data.patch b/package/utils/ucode/patches/100-rtnl-return-true-for-an-acknowledgement-without-data.patch deleted file mode 100644 index 3963ec34a11..00000000000 --- a/package/utils/ucode/patches/100-rtnl-return-true-for-an-acknowledgement-without-data.patch +++ /dev/null @@ -1,64 +0,0 @@ -From: Felix Fietkau -Date: Wed, 30 Sep 2026 18:08:32 +0200 -Subject: [PATCH] rtnl: return true for an acknowledgement without data - -request() treats an ACK as the end of a reply and returns the collected -data. For a command that the kernel answers with a bare ACK, such as -RTM_SETLINK, that data is null, so a caller that tests the result for -truth sees a failure. - -Return true if an ACK ends a request without reply data. A dump that -ends without entries still returns null, and a refused request still -returns false. - -Document these results in the description of request(). - -Signed-off-by: Felix Fietkau ---- - ---- a/lib/rtnl.c -+++ b/lib/rtnl.c -@@ -3350,6 +3350,19 @@ cb_done(struct nl_msg *msg, void *arg) - } - - static int -+cb_done_ack(struct nl_msg *msg, void *arg) -+{ -+ request_state_t *s = arg; -+ -+ if (s->state == STATE_UNREPLIED) -+ s->res = ucv_boolean_new(true); -+ -+ s->state = STATE_REPLIED; -+ -+ return NL_STOP; -+} -+ -+static int - cb_error(struct sockaddr_nl *nla, struct nlmsgerr *err, void *arg) - { - request_state_t *s = arg; -@@ -3433,7 +3446,13 @@ static const struct { - * @param {number} flags - The netlink flags for the request - * @param {*} payload - The payload data for the request - * -- * @returns {?*} - The response data or null on error -+ * @returns {?(Object|Object[]|boolean)} - The response data, true for a -+ * successful acknowledgement without -+ * data, false when the kernel refuses -+ * the request or the reply cannot be -+ * received, or null for a dump -+ * without entries and when the -+ * request cannot be sent - * - * @example - * // Send a route request -@@ -3532,7 +3551,7 @@ uc_nl_request(uc_vm_t *vm, size_t nargs) - - nl_cb_set(cb, NL_CB_VALID, NL_CB_CUSTOM, cb_reply, &st); - nl_cb_set(cb, NL_CB_FINISH, NL_CB_CUSTOM, cb_done, &st); -- nl_cb_set(cb, NL_CB_ACK, NL_CB_CUSTOM, cb_done, &st); -+ nl_cb_set(cb, NL_CB_ACK, NL_CB_CUSTOM, cb_done_ack, &st); - nl_cb_err(cb, NL_CB_CUSTOM, cb_error, &st); - - nl_send_auto_complete(sock, msg); diff --git a/package/utils/ucode/patches/100-uloop-do-not-pass-exit-requests-to-the-exception-gua.patch b/package/utils/ucode/patches/100-uloop-do-not-pass-exit-requests-to-the-exception-gua.patch new file mode 100644 index 00000000000..87708075571 --- /dev/null +++ b/package/utils/ucode/patches/100-uloop-do-not-pass-exit-requests-to-the-exception-gua.patch @@ -0,0 +1,69 @@ +From: Felix Fietkau +Date: Thu, 15 Oct 2026 14:27:10 +0200 +Subject: [PATCH] uloop: do not pass exit() requests to the exception guard + +Since the exception guard was added, uc_uloop_vm_call() hands every +exception of a callback to the guard handler, also the EXCEPTION_EXIT +that exit() raises. The guard receives an "Exit" exception and the loop +keeps running. When run() returns, the script ends with code 0 instead +of the code given to exit(), because the VM already reset its call +frames for the exit request. + +End the loop and keep the exit request pending, as without a guard, so +that the script terminates with the requested code. + +Fixes: aaf712eb4fb4 ("uloop: add guard() function") +Signed-off-by: Felix Fietkau +--- + create mode 100644 tests/custom/06_lib_uloop/12_callback_exit + +--- a/lib/uloop.c ++++ b/lib/uloop.c +@@ -149,9 +149,17 @@ uc_uloop_vm_call(uc_vm_t *vm, bool mcall + { + uc_value_t *exh, *val; + +- if (uc_vm_call(vm, mcall, nargs) == EXCEPTION_NONE) ++ switch (uc_vm_call(vm, mcall, nargs)) { ++ case EXCEPTION_NONE: + return true; + ++ case EXCEPTION_EXIT: ++ goto error; ++ ++ default: ++ break; ++ } ++ + exh = uc_vm_registry_get(vm, "uloop.ex_handler"); + if (!ucv_is_callable(exh)) + goto error; +--- /dev/null ++++ b/tests/custom/06_lib_uloop/12_callback_exit +@@ -0,0 +1,26 @@ ++Calling exit() in a uloop callback ends the loop and terminates the script ++with the given code, also when an exception guard is set. The guard does not ++receive the exit request. ++ ++-- Testcase -- ++{% ++ import * as uloop from 'uloop'; ++ ++ uloop.init(); ++ ++ uloop.guard((e) => print(`guard: ${e.type}\n`)); ++ ++ uloop.timer(10, () => exit(4)); ++ uloop.timer(100, () => { ++ print("loop kept running\n"); ++ uloop.end(); ++ }); ++ ++ uloop.run(); ++ print("run() returned\n"); ++%} ++-- End -- ++ ++-- Expect exitcode -- ++4 ++-- End -- diff --git a/package/utils/ucode/patches/101-uloop-allow-a-function-to-prepare-the-child-in-proce.patch b/package/utils/ucode/patches/101-uloop-allow-a-function-to-prepare-the-child-in-proce.patch new file mode 100644 index 00000000000..3379816cd8e --- /dev/null +++ b/package/utils/ucode/patches/101-uloop-allow-a-function-to-prepare-the-child-in-proce.patch @@ -0,0 +1,354 @@ +From: Felix Fietkau +Date: Thu, 15 Oct 2026 14:29:11 +0200 +Subject: [PATCH] uloop: allow a function to prepare the child in process() + +A program started by uloop.process() can need preparatory work in the +child before exec(), for example to redirect a file descriptor. + +Accept a function instead of the executable path. The function runs in +the child after fork(), receives the argument vector and the +environment, and returns the complete argument vector to execute. +Changes to the environment object apply to the executed program. +exit() in the function sets the exit code of the child. If the function +throws an exception or does not return a non-empty array, the child +exits with code 255, the same as for a failed exec(). + +Release the event loop in the child before the function runs, like +uloop.task() does. The child otherwise shares the signal pipe of the +parent, so a signal sent to the child runs signal callbacks of the +parent, and SIGTERM does not end the child. + +Flush the output of the parent before fork(), since a flush in the child +writes the inherited buffer a second time. + +Signed-off-by: Felix Fietkau +--- + create mode 100644 tests/custom/06_lib_uloop/13_process_function + +--- a/lib/uloop.c ++++ b/lib/uloop.c +@@ -1184,6 +1184,93 @@ uc_uloop_process_cb(struct uloop_process + ucv_put(obj); + } + ++static char ** ++uc_uloop_process_argv(uc_vm_t *vm, uc_value_t *arg0, uc_value_t *list, ++ size_t first) ++{ ++ size_t len = ucv_array_length(list); ++ size_t count = len > first ? len - first : 0; ++ char **argp = calloc(count + 2, sizeof(char *)); ++ size_t i; ++ ++ if (!argp) ++ return NULL; ++ ++ argp[0] = ucv_to_string(vm, arg0); ++ ++ for (i = 0; i < count; i++) ++ argp[i + 1] = ucv_to_string(vm, ucv_array_get(list, first + i)); ++ ++ return argp; ++} ++ ++static char ** ++uc_uloop_process_envp(uc_vm_t *vm, uc_value_t *env_arg) ++{ ++ uc_stringbuf_t *buf; ++ char **envp; ++ size_t i = 0; ++ ++ if (!env_arg) ++ return environ; ++ ++ envp = calloc(ucv_object_length(env_arg) + 1, sizeof(char *)); ++ ++ if (!envp) ++ return NULL; ++ ++ ucv_object_foreach(env_arg, envk, envv) { ++ buf = xprintbuf_new(); ++ ++ ucv_stringbuf_printf(buf, "%s=", envk); ++ ucv_to_stringbuf(vm, buf, envv, false); ++ ++ envp[i++] = buf->buf; ++ ++ free(buf); ++ } ++ ++ return envp; ++} ++ ++static void ++uc_uloop_process_exec(uc_vm_t *vm, uc_value_t *executable, ++ uc_value_t *arguments, uc_value_t *env_arg) ++{ ++ uc_value_t *argv; ++ char **argp, **envp; ++ ++ uloop_done(); ++ ++ if (ucv_is_callable(executable)) { ++ uc_vm_stack_push(vm, ucv_get(executable)); ++ uc_vm_stack_push(vm, ucv_get(arguments)); ++ uc_vm_stack_push(vm, ucv_get(env_arg)); ++ ++ if (!uc_uloop_vm_call(vm, false, 2)) ++ _exit(vm->exception.type == EXCEPTION_EXIT ? vm->arg.s32 : -1); ++ ++ argv = uc_vm_stack_pop(vm); ++ ++ if (ucv_type(argv) != UC_ARRAY || !ucv_array_length(argv)) ++ _exit(-1); ++ ++ argp = uc_uloop_process_argv(vm, ucv_array_get(argv, 0), argv, 1); ++ } ++ else { ++ argp = uc_uloop_process_argv(vm, executable, arguments, 0); ++ } ++ ++ envp = uc_uloop_process_envp(vm, env_arg); ++ ++ if (!argp || !envp) ++ _exit(-1); ++ ++ execvpe(argp[0], (char * const *)argp, (char * const *)envp); ++ ++ _exit(-1); ++} ++ + /** + * Creates a process instance for executing external programs. + * +@@ -1192,14 +1279,29 @@ uc_uloop_process_cb(struct uloop_process + * vector, an optional dictionary describing environment variables, and a + * callback function to be invoked when the invoked process ends. + * ++ * When a function is given instead of the executable path, it is invoked in ++ * the child process after fork(), receiving the argument vector and the ++ * environment dictionary as parameters. It may perform any preparatory work, ++ * such as setting up file descriptors, and must return the complete argument ++ * vector to execute, with the executable path as first element. Changes made ++ * to the environment dictionary by the function apply to the environment of ++ * the executed program; the executable is searched in the `PATH` of the ++ * calling process. If the function calls `exit()`, the child process exits ++ * with the given code. If it throws an exception or does not return a ++ * non-empty array, the child process exits with code 255. ++ * ++ * The function runs outside of the event loop: signals received by the child ++ * process do not invoke the signal callbacks of the event loop. ++ * + * @function module:uloop#process + * +- * @param {string} executable +- * The path to the executable program. ++ * @param {string|Function} executable ++ * The path to the executable program, or a function returning the argument ++ * vector to execute. + * + * @param {string[]} [args] + * Optional. An array of strings representing the arguments passed to the +- * executable. ++ * executable, or to the function. + * + * @param {Object} [env] + * Optional. A dictionary describing environment variables for the process. +@@ -1211,13 +1313,26 @@ uc_uloop_process_cb(struct uloop_process + * + * @returns {?module:uloop.process} + * Returns a process instance for executing external programs. +- * Returns `null` on error, e.g. due to `exec()` failure or invalid arguments. ++ * Returns `null` on invalid arguments or `fork()` failure. If the program ++ * cannot be executed, the callback receives the exit code 255. + * + * @example + * // Create a process instance for executing 'ls' command + * const myProcess = uloop.process("/bin/ls", ["-l", "/tmp"], null, (code) => { + * printf(`Process exited with code ${code}\n`); + * }); ++ * ++ * // Redirect stderr in the child process before executing 'ls' ++ * const myProcess = uloop.process((args, env) => { ++ * const fs = require('fs'); ++ * const errlog = fs.open('/tmp/error.log', 'w'); ++ * fs.dup2(errlog.fileno(), 2); ++ * errlog.close(); ++ * ++ * return ["/bin/ls", ...args]; ++ * }, ["-l", "/tmp"], null, (code) => { ++ * printf(`Process exited with code ${code}\n`); ++ * }); + */ + static uc_value_t * + uc_uloop_process(uc_vm_t *vm, size_t nargs) +@@ -1227,55 +1342,25 @@ uc_uloop_process(uc_vm_t *vm, size_t nar + uc_value_t *env_arg = uc_fn_arg(2); + uc_value_t *callback = uc_fn_arg(3); + uc_uloop_process_t *process; +- uc_stringbuf_t *buf; +- char **argp, **envp; + pid_t pid; +- size_t i; + +- if (ucv_type(executable) != UC_STRING || ++ if ((ucv_type(executable) != UC_STRING && !ucv_is_callable(executable)) || + (arguments && ucv_type(arguments) != UC_ARRAY) || + (env_arg && ucv_type(env_arg) != UC_OBJECT) || + !ucv_is_callable(callback)) { + err_return(EINVAL); + } + ++ /* the child may flush the inherited buffer again */ ++ fflush(vm->output); ++ + pid = fork(); + + if (pid == -1) + err_return(errno); + +- if (pid == 0) { +- argp = calloc(ucv_array_length(arguments) + 2, sizeof(char *)); +- envp = environ; +- +- if (env_arg) { +- envp = calloc(ucv_object_length(env_arg) + 1, sizeof(char *)); +- i = 0; +- ucv_object_foreach(env_arg, envk, envv) { +- buf = xprintbuf_new(); +- +- ucv_stringbuf_printf(buf, "%s=", envk); +- ucv_to_stringbuf(vm, buf, envv, false); +- +- envp[i++] = buf->buf; +- +- free(buf); +- } +- } +- +- if (!argp || !envp) +- _exit(-1); +- +- argp[0] = ucv_to_string(vm, executable); +- +- for (i = 0; i < ucv_array_length(arguments); i++) +- argp[i+1] = ucv_to_string(vm, ucv_array_get(arguments, i)); +- +- execvpe((const char *)ucv_string_get(executable), +- (char * const *)argp, (char * const *)envp); +- +- _exit(-1); +- } ++ if (pid == 0) ++ uc_uloop_process_exec(vm, executable, arguments, env_arg); + + process = uc_uloop_alloc(vm, "uloop.process", sizeof(*process), callback); + process->process.pid = pid; +--- /dev/null ++++ b/tests/custom/06_lib_uloop/13_process_function +@@ -0,0 +1,105 @@ ++When the first argument of process() is a function, it runs in the child ++process, receives the argument vector and the environment, and returns the ++argument vector to execute. Changes to the environment apply to the executed ++program. exit() in the function sets the exit code of the child. An exception ++or a return value that is not a non-empty array makes the child exit with code ++255. Buffered output of the parent is written once, and signals received by ++the child do not reach the signal callbacks of the parent. ++ ++-- Testcase -- ++{% ++ import * as uloop from 'uloop'; ++ import * as fs from 'fs'; ++ ++ uloop.init(); ++ ++ const tmpdir = fs.mkdtemp('/tmp/ucode-uloop-process-XXXXXX'); ++ const outfile = tmpdir + '/out'; ++ ++ function run(label, executable, args, env) { ++ let completed = false; ++ let child = uloop.process(executable, args, env, (status) => { ++ completed = true; ++ printf('%s: %d\n', label, status); ++ uloop.end(); ++ }); ++ ++ if (!child) ++ die('Unable to start child'); ++ ++ uloop.run(); ++ ++ if (!completed) ++ die('Missing process callback'); ++ } ++ ++ run('argv', (args, env) => { ++ let out = fs.open(outfile, 'w'); ++ fs.dup2(out.fileno(), 1); ++ out.close(); ++ ++ env.GREETING = 'changed'; ++ ++ return ['/bin/sh', '-c', 'echo "$0 $1 $GREETING"; exit 3', ...args]; ++ }, ['first', 'second'], { GREETING: 'initial' }); ++ ++ print(fs.readfile(outfile)); ++ fs.unlink(outfile); ++ fs.rmdir(tmpdir); ++ ++ run('exception', () => die('setup failed')); ++ run('null', () => null); ++ run('empty', () => []); ++ run('string', () => '/bin/true'); ++ run('exit', () => exit(3)); ++ ++ printf('buffered before fork\n'); ++ run('flush', () => { ++ fs.stdout.flush(); ++ return ['/bin/true']; ++ }); ++ ++ let usr1_count = 0; ++ let usr1 = uloop.signal('SIGUSR1', () => usr1_count++); ++ ++ function run_signal(signame) { ++ uloop.timer(1, () => { ++ let child = uloop.process(() => { ++ sleep(300); ++ return ['/bin/true']; ++ }, null, null, (status) => { ++ printf('%s: %d, parent callbacks: %d\n', signame, status, usr1_count); ++ uloop.end(); ++ }); ++ ++ uloop.timer(100, () => system(['kill', `-${signame}`, `${child.pid()}`])); ++ }); ++ ++ uloop.run(); ++ } ++ ++ run_signal('USR1'); ++ run_signal('TERM'); ++ ++ usr1.delete(); ++ ++ printf('invalid: %J\n', uloop.process(123, null, null, () => null)); ++ ++ uloop.done(); ++%} ++-- End -- ++ ++-- Expect stdout -- ++argv: 3 ++first second changed ++exception: 255 ++null: 255 ++empty: 255 ++string: 255 ++exit: 3 ++buffered before fork ++flush: 0 ++USR1: 0, parent callbacks: 0 ++TERM: -15, parent callbacks: 0 ++invalid: null ++-- End -- diff --git a/package/utils/ucode/patches/110-uloop-allow-reusing-the-existing-environment.patch b/package/utils/ucode/patches/110-uloop-allow-reusing-the-existing-environment.patch deleted file mode 100644 index 018410493dc..00000000000 --- a/package/utils/ucode/patches/110-uloop-allow-reusing-the-existing-environment.patch +++ /dev/null @@ -1,57 +0,0 @@ -From: Felix Fietkau -Date: Wed, 8 Oct 2025 19:11:46 +0200 -Subject: [PATCH] uloop: allow reusing the existing environment - -When passing null as environment argument, reuse existing environ. -This makes it possible to avoid having to duplicate and convert it -by passing the output of getenv(). - -Signed-off-by: Felix Fietkau ---- - ---- a/lib/uloop.c -+++ b/lib/uloop.c -@@ -1028,7 +1028,22 @@ uc_uloop_process(uc_vm_t *vm, size_t nar - - if (pid == 0) { - argp = calloc(ucv_array_length(arguments) + 2, sizeof(char *)); -- envp = calloc(ucv_object_length(env_arg) + 1, sizeof(char *)); -+ envp = environ; -+ -+ if (env_arg) { -+ envp = calloc(ucv_object_length(env_arg) + 1, sizeof(char *)); -+ i = 0; -+ ucv_object_foreach(env_arg, envk, envv) { -+ buf = xprintbuf_new(); -+ -+ ucv_stringbuf_printf(buf, "%s=", envk); -+ ucv_to_stringbuf(vm, buf, envv, false); -+ -+ envp[i++] = buf->buf; -+ -+ free(buf); -+ } -+ } - - if (!argp || !envp) - _exit(-1); -@@ -1038,19 +1053,6 @@ uc_uloop_process(uc_vm_t *vm, size_t nar - for (i = 0; i < ucv_array_length(arguments); i++) - argp[i+1] = ucv_to_string(vm, ucv_array_get(arguments, i)); - -- i = 0; -- -- ucv_object_foreach(env_arg, envk, envv) { -- buf = xprintbuf_new(); -- -- ucv_stringbuf_printf(buf, "%s=", envk); -- ucv_to_stringbuf(vm, buf, envv, false); -- -- envp[i++] = buf->buf; -- -- free(buf); -- } -- - execvpe((const char *)ucv_string_get(executable), - (char * const *)argp, (char * const *)envp); - diff --git a/package/utils/ucode/patches/111-uloop-add-optional-setup-callback-to-process.patch b/package/utils/ucode/patches/111-uloop-add-optional-setup-callback-to-process.patch deleted file mode 100644 index e6accabe38a..00000000000 --- a/package/utils/ucode/patches/111-uloop-add-optional-setup-callback-to-process.patch +++ /dev/null @@ -1,85 +0,0 @@ -From: Felix Fietkau -Date: Wed, 8 Oct 2025 22:06:46 +0200 -Subject: [PATCH] uloop: add optional setup callback to process() - -Add optional setup callback as 5th argument to uloop.process() that is -invoked in the child process after fork() but before exec(). - -Signed-off-by: Felix Fietkau ---- - ---- a/lib/uloop.c -+++ b/lib/uloop.c -@@ -973,8 +973,9 @@ uc_uloop_process_cb(struct uloop_process - * - * This function creates a process instance for executing external programs. - * It takes the executable path string, an optional string array as the argument -- * vector, an optional dictionary describing environment variables, and a -- * callback function to be invoked when the invoked process ends. -+ * vector, an optional dictionary describing environment variables, a -+ * callback function to be invoked when the invoked process ends, and an optional -+ * setup callback to be invoked in the child process after fork(). - * - * @function module:uloop#process - * -@@ -991,6 +992,11 @@ uc_uloop_process_cb(struct uloop_process - * @param {Function} callback - * The callback function to be invoked when the invoked process ends. - * -+ * @param {Function} [setup] -+ * Optional. A callback function to be invoked in the child process after fork() -+ * but before exec(). This can be used to set up file descriptors, change working -+ * directory, or perform other initialization. -+ * - * @returns {?module:uloop.process} - * Returns a process instance for executing external programs. - * Returns `null` on error, e.g. due to `exec()` failure or invalid arguments. -@@ -1000,6 +1006,16 @@ uc_uloop_process_cb(struct uloop_process - * const myProcess = uloop.process("/bin/ls", ["-l", "/tmp"], null, (code) => { - * printf(`Process exited with code ${code}\n`); - * }); -+ * -+ * // With setup callback to redirect stderr -+ * const myProcess = uloop.process("/bin/ls", ["-l", "/tmp"], null, (code) => { -+ * printf(`Process exited with code ${code}\n`); -+ * }, () => { -+ * const fs = require('fs'); -+ * const errlog = fs.open('/tmp/error.log', 'w'); -+ * fs.dup2(errlog.fileno(), 2); -+ * errlog.close(); -+ * }); - */ - static uc_value_t * - uc_uloop_process(uc_vm_t *vm, size_t nargs) -@@ -1008,6 +1024,7 @@ uc_uloop_process(uc_vm_t *vm, size_t nar - uc_value_t *arguments = uc_fn_arg(1); - uc_value_t *env_arg = uc_fn_arg(2); - uc_value_t *callback = uc_fn_arg(3); -+ uc_value_t *setup_cb = uc_fn_arg(4); - uc_uloop_process_t *process; - uc_stringbuf_t *buf; - char **argp, **envp; -@@ -1017,7 +1034,8 @@ uc_uloop_process(uc_vm_t *vm, size_t nar - if (ucv_type(executable) != UC_STRING || - (arguments && ucv_type(arguments) != UC_ARRAY) || - (env_arg && ucv_type(env_arg) != UC_OBJECT) || -- !ucv_is_callable(callback)) { -+ !ucv_is_callable(callback) || -+ (setup_cb && !ucv_is_callable(setup_cb))) { - err_return(EINVAL); - } - -@@ -1027,6 +1045,13 @@ uc_uloop_process(uc_vm_t *vm, size_t nar - err_return(errno); - - if (pid == 0) { -+ if (setup_cb) { -+ uc_vm_stack_push(vm, ucv_get(setup_cb)); -+ -+ if (uc_uloop_vm_call(vm, false, 0)) -+ ucv_put(uc_vm_stack_pop(vm)); -+ } -+ - argp = calloc(ucv_array_length(arguments) + 2, sizeof(char *)); - envp = environ; -