Skip to content

Command::quit calls exit(0) from inside the library, silently killing the host process #160

Description

@HugoFara

src/Command.cpp:2967 calls exit(0) after freeing session state. quit is registered in the script command table (Command.h:99), so it is reachable from any host embedding the engine.

From Python:

import atexit, pyforefire
atexit.register(lambda: print("ATEXIT ran", flush=True))
f = pyforefire.ForeFire()
print("before quit[]", flush=True)
try:
    f.execute("quit[]")
    print("AFTER quit[]", flush=True)
finally:
    print("FINALLY ran", flush=True)

Output is before quit[], and then the process is gone. Four problems in one result: the interpreter dies with no traceback or exception; finally, atexit, context managers and destructors never run; buffered output is lost, so without flush=True even the first print never appears (exit(0) flushes C streams, not Python's io layer); and it exits 0, so a batch job or CI step reports success.

It is not only user-triggered. Command.cpp:1125 calls quit() from the safe-topology error path, under // TODO supersafe mode ?. So an internal error can terminate the host process on its own, with exit code 0.

Suggested fix

Have quit release session state and return a status, and let the CLI front-end in app/forefire/ decide whether to exit. The path at line 1125 should propagate an error instead of terminating. If a hard abort must stay reachable, make it a distinct command that the Python binding does not expose.

Related to #159: quit is one of the few places that does delete session objects, so its ownership assumptions and the leak there should be settled together.


Drafted by Claude Opus 5 from a codebase audit. Reviewed by a maintainer before filing.

EDIT: rewrote for human readability.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions