Skip to content

Fix a use-after-free in uc_vm_call(), module loading issues and 2 new features - #493

Closed
blogic wants to merge 7 commits into
ucode-lang:masterfrom
blogic:fixes
Closed

blogic wants to merge 7 commits into
ucode-lang:masterfrom
blogic:fixes

Conversation

@blogic

@blogic blogic commented Oct 15, 2026

Copy link
Copy Markdown
Contributor

53c3f2e lib: do not cache a module whose load raised
3ec9a96 vm: read the importing frame after the module load
a8e1ad1 compiler: give dynamic loads the position of the import
d2aa3fe vm: keep the exception of a module that failed to load
d89e319 vm: read the callee type before uc_vm_call() runs it
9077139 uloop: add optional setup callback to process()
44bb9d4 vm: size object literals from the compiler's key count
5eab42c serial: add RS485 mode access (Linux)

Provide the half duplex direction control a UART driver offers for RS485
transceivers whose driver enable is wired to the RTS pin, via the Linux
TIOCGRS485 and TIOCSRS485 ioctls; compiled only on Linux.

getrs485() returns the serial_rs485 configuration as an object with the
flags as booleans (enabled, rts_on_send, rts_after_send, rx_during_tx,
terminate_bus) and the two RTS delays in milliseconds (delay_before,
delay_after). setrs485() applies a read-modify-write of the supplied
fields, or of just the enabled flag when given a boolean, and returns the
configuration read back from the driver afterwards rather than the one
requested: drivers silently drop flags and delays they cannot honour, and
the caller otherwise has no way to find out. A driver without RS485
support fails the initial read, so the error surfaces before anything is
changed.

The newer SER_RS485_RX_DURING_TX and SER_RS485_TERMINATE_BUS flags are
defined locally when the toolchain headers predate them.

Signed-off-by: John Crispin <john@phrozen.org>
Every object got a json-c table of 16 entries, 40 bytes each on 64-bit,
however few keys it held. The compiler already emits the number of
plain keys of a literal as the operand of I_NOBJ, but the VM ignored it.

Size a literal's table from that count, so a schema or config table of
small objects no longer costs about 700 bytes per object. Spreads are not
counted; an object filled by spreading grows as before, and an empty
literal keeps the 16 entries of ucv_object_new().

Signed-off-by: John Crispin <john@phrozen.org>
Comment thread lib/uloop.c
* fs.dup2(errlog.fileno(), 2);
* errlog.close();
* });
*/

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This feels unergonomic. What about this:

  • when the first argument is a function, it is invoked, receiving all subsequent arguments as params, and returning the final argv handed to exec. It can then do whatever preparatory work it needs to do:
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`);
});

uc_vm_call() took a reference to the function value on the stack, ran the
call and then inspected the type of the value to decide whether to run the
interpreter loop. For a C function the callee has returned at that point:
uc_vm_call_native() popped its frame, which released the reference that
uc_vm_call() held and the stack slot the function was called from. When
that slot held the only other reference, as for a function expression that
is called at once, the type check read freed memory.

Decide before the call whether the callee is native. Only a closure needs
the interpreter loop, and the answer does not change while the call runs.

Signed-off-by: John Crispin <john@phrozen.org>
A dynamically loaded module whose body raised comes back with the exports
it reached before the exception, and the modules table caches it in that
state. The import went on to resolve its names against that scope, so the
importer saw "Module does not export ..." in place of the exception the
module raised. Return as soon as the load left an exception pending; the
caller sees the original error with its stack trace.

Signed-off-by: John Crispin <john@phrozen.org>
The dynamic load an import turns into was emitted at source position 0, so
a stack trace through a module that raised during a dynamic load pointed
at the start of the import statement in the importer, not at the module
name. A static import records the position of the import statement.
Record the same position for the dynamic load; the import() call form
already did.

Signed-off-by: John Crispin <john@phrozen.org>
uc_vm_insn_dynload() took a pointer to the current callframe before
uc_require_library() and used it afterwards, to read the import names
and to store the imported values. Loading a ucode module runs its body,
and a body that imports further modules pushes more callframes. When the
callframe vector grows, realloc() moves it, and the saved pointer then
points into freed memory. A program compiled with a forced dynamic link
(ucode -c dynlink=name) to a module with a chain of imports reads freed
memory this way.

Read the frame after the load.

Signed-off-by: John Crispin <john@phrozen.org>
uc_require_path() stored every loaded module in the modules table, also
when the load raised. A library that dlopen() refused, a library without
uc_module_entry() and a .uc file that failed to compile were cached as
null, and the next require() or import of the name returned null without
an error.

Cache a module only when its load raised no exception, so that a later
import tries again and reports the error again.

Signed-off-by: John Crispin <john@phrozen.org>
@jow-

jow- commented Oct 16, 2026

Copy link
Copy Markdown
Collaborator

All commits except "vm: size object literals from the compiler's key count" have been cherry picked into master. For the remainder follow up in #496

@jow- jow- closed this Oct 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants