From b5d243463c24309b155f9b6e98544ab8e8046fba Mon Sep 17 00:00:00 2001 From: Giuseppe Scrivano Date: Thu, 13 Aug 2026 15:11:13 +0000 Subject: [PATCH 1/9] libcrun: export parse_json_file for the language bindings The Python and Lua bindings parse a process JSON blob (in their update/exec helpers) with parse_json_file() before calling libcrun_container_exec(). libcrun is built with -fvisibility=hidden and a version script (libcrun.lds) that only exports symbols matching a few name patterns, so parse_json_file was not exported and loading either module failed with: undefined symbol: parse_json_file Mark it LIBCRUN_PUBLIC (so it reaches the dynamic symbol table) and add it to the version script global list (both are required to export it). Co-Authored-By: Claude Opus 4.8 Signed-off-by: Giuseppe Scrivano --- libcrun.lds | 2 ++ src/libcrun/utils.h | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/libcrun.lds b/libcrun.lds index 8e141724d2..c9cdce5466 100644 --- a/libcrun.lds +++ b/libcrun.lds @@ -2,6 +2,8 @@ global: /* Not all the libcrun_ functions are exported, only those marked LIBCRUN_PUBLIC. */ libcrun_*; + /* Utility used by the language bindings (python/, lua/). */ + parse_json_file; /* libocispec functions */ runtime_spec_*; free_runtime_spec_*; diff --git a/src/libcrun/utils.h b/src/libcrun/utils.h index 4e8d100790..2304196f9b 100644 --- a/src/libcrun/utils.h +++ b/src/libcrun/utils.h @@ -352,7 +352,9 @@ void get_current_timestamp (char *out, size_t len); int set_blocking_fd (int fd, bool blocking, libcrun_error_t *err); -int parse_json_file (json_object **out, const char *jsondata, struct parser_context *ctx, libcrun_error_t *err); +/* Exported for the language bindings (python/, lua/), which parse a + process JSON blob before calling libcrun_container_exec. */ +LIBCRUN_PUBLIC int parse_json_file (json_object **out, const char *jsondata, struct parser_context *ctx, libcrun_error_t *err); /* Check that the string STR has the prefix PREFIX. */ static inline int From ef6b9343fcee52b7442b11c1457a0d60f0678437 Mon Sep 17 00:00:00 2001 From: Giuseppe Scrivano Date: Thu, 13 Aug 2026 15:11:13 +0000 Subject: [PATCH 2/9] libcrun: guard _GNU_SOURCE redefinition in string_map.h Python.h defines _GNU_SOURCE to 1, so a translation unit that includes it before string_map.h (as the Python binding does, transitively via container.h) hits a "_GNU_SOURCE redefined" warning, which is fatal under -Werror. Only define it when it is not already defined. Co-Authored-By: Claude Opus 4.8 Signed-off-by: Giuseppe Scrivano --- src/libcrun/string_map.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libcrun/string_map.h b/src/libcrun/string_map.h index 9819df1b87..f647769190 100644 --- a/src/libcrun/string_map.h +++ b/src/libcrun/string_map.h @@ -19,7 +19,9 @@ #ifndef STRING_MAP_H #define STRING_MAP_H -#define _GNU_SOURCE +#ifndef _GNU_SOURCE +# define _GNU_SOURCE +#endif #include From 004cf513da2924a25a891082671b332f08c2d8f5 Mon Sep 17 00:00:00 2001 From: Giuseppe Scrivano Date: Thu, 13 Aug 2026 15:13:14 +0000 Subject: [PATCH 3/9] python: include libcrun/spec.h for libcrun_container_spec libcrun_container_spec() moved from container.h to spec.h, but the Python bindings were never updated and still only included container.h. Building the bindings failed with an implicit-declaration error. Add the missing include. Closes: https://github.com/containers/crun/issues/2187 Co-Authored-By: Claude Opus 4.8 Signed-off-by: Giuseppe Scrivano --- python/crun_python.c | 1 + 1 file changed, 1 insertion(+) diff --git a/python/crun_python.c b/python/crun_python.c index 47bd7b1608..8846f32d07 100644 --- a/python/crun_python.c +++ b/python/crun_python.c @@ -36,6 +36,7 @@ python_crun.run(ctx, ctr) #include #include #include +#include #include #include From f757ac4fcf84d5e5f6c1867cc69669f9c3c26ee6 Mon Sep 17 00:00:00 2001 From: Giuseppe Scrivano Date: Thu, 13 Aug 2026 15:13:23 +0000 Subject: [PATCH 4/9] python: register the missing start method container_start() was defined but never added to the method table, so python_crun had no "start" entry and the function triggered a -Werror=unused-function build failure. Register it. Co-Authored-By: Claude Opus 4.8 Signed-off-by: Giuseppe Scrivano --- python/crun_python.c | 1 + 1 file changed, 1 insertion(+) diff --git a/python/crun_python.c b/python/crun_python.c index 8846f32d07..34fd2297d3 100644 --- a/python/crun_python.c +++ b/python/crun_python.c @@ -468,6 +468,7 @@ static PyMethodDef CrunMethods[] = { {"create", container_create, METH_VARARGS, "Create a container."}, {"delete", container_delete, METH_VARARGS, "Delete a container."}, {"kill", container_kill, METH_VARARGS, "Kill a container."}, + {"start", container_start, METH_VARARGS, "Start a container."}, {"list", containers_list, METH_VARARGS, "List the containers."}, {"status", container_status, METH_VARARGS, "Get the status of a container."}, From 0c1b48c848b21624ae72fde01b551e114f658629 Mon Sep 17 00:00:00 2001 From: Giuseppe Scrivano Date: Thu, 13 Aug 2026 15:13:31 +0000 Subject: [PATCH 5/9] python: drop the duplicate spec method entry The method table registered "spec" twice with the same handler. The duplicate is harmless at runtime but confusing; remove it. "spec" remains available via the earlier entry. Co-Authored-By: Claude Opus 4.8 Signed-off-by: Giuseppe Scrivano --- python/crun_python.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/python/crun_python.c b/python/crun_python.c index 34fd2297d3..0958c3f3e5 100644 --- a/python/crun_python.c +++ b/python/crun_python.c @@ -480,8 +480,6 @@ static PyMethodDef CrunMethods[] = { "Create a context object."}, {"set_verbosity", set_verbosity, METH_VARARGS, "Set the logging verbosity."}, {"get_verbosity", get_verbosity, METH_NOARGS, "Get the logging verbosity."}, - {"spec", container_spec, METH_VARARGS, - "Generate a new configuration file."}, {NULL, NULL, 0, NULL} }; From 3a605804159b7d3ec8ca1a4086517f8c089901f5 Mon Sep 17 00:00:00 2001 From: Giuseppe Scrivano Date: Thu, 13 Aug 2026 15:14:08 +0000 Subject: [PATCH 6/9] python: fix context capsule destructor and drop dead code make_context() created its capsule with a NULL destructor, so the libcrun_context_t and its strdup'd fields leaked whenever a context was garbage collected. It also leaked the freshly allocated context if argument parsing failed. Wire up free_context() as the capsule destructor and free the context on the parse-error path. free_context() now retrieves the pointer with PyCapsule_GetPointer() (the previous cast of the raw PyObject* was wrong for a capsule destructor) and also frees the previously-forgotten bundle field. While here, remove unused local variables from container_status, container_spec and set_verbosity that tripped -Werror. Co-Authored-By: Claude Opus 4.8 Signed-off-by: Giuseppe Scrivano --- python/crun_python.c | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/python/crun_python.c b/python/crun_python.c index 0958c3f3e5..b14994f757 100644 --- a/python/crun_python.c +++ b/python/crun_python.c @@ -106,13 +106,23 @@ container_load_from_memory (PyObject *self arg_unused, PyObject *args) } static void -free_context (void *ptr) +free_context (PyObject *ptr) { - libcrun_context_t *ctx = ptr; - char *id = (char *) ctx->id; - free (ctx->state_root); - free (ctx->notify_socket); + libcrun_context_t *ctx = PyCapsule_GetPointer (ptr, CONTEXT_OBJ_TAG); + void *id, *bundle, *state_root, *notify_socket; + + if (ctx == NULL) + return; + + id = (void *) ctx->id; + bundle = (void *) ctx->bundle; + state_root = (void *) ctx->state_root; + notify_socket = (void *) ctx->notify_socket; + free (id); + free (bundle); + free (state_root); + free (notify_socket); free (ctx); } @@ -135,13 +145,16 @@ make_context (PyObject *self arg_unused, PyObject *args, PyObject *kwargs) if (!PyArg_ParseTupleAndKeywords (args, kwargs, "s|ssbsbbbb", kwlist, &id, &bundle, &state_root, &ctx->systemd_cgroup, ¬ify_socket, &ctx->detach, &ctx->no_new_keyring, &ctx->force_no_cgroup, &ctx->no_pivot)) - return NULL; + { + free (ctx); + return NULL; + } ctx->id = xstrdup (id); ctx->bundle = xstrdup (bundle ? bundle : "."); ctx->state_root = xstrdup (state_root); ctx->notify_socket = xstrdup (notify_socket); - return PyCapsule_New (ctx, CONTEXT_OBJ_TAG, NULL); + return PyCapsule_New (ctx, CONTEXT_OBJ_TAG, free_context); } static PyObject * @@ -329,7 +342,6 @@ container_status (PyObject *self arg_unused, PyObject *args) PyObject *ctx_obj = NULL; libcrun_context_t *ctx; char *id = NULL; - libcrun_container_status_t status; cleanup_free char *buffer = NULL; FILE *memfile; int ret; @@ -414,10 +426,6 @@ static PyObject * container_spec (PyObject *self arg_unused, PyObject *args arg_unused) { libcrun_error_t err = NULL; - PyObject *ctx_obj = NULL; - libcrun_context_t *ctx; - char *id = NULL; - libcrun_container_status_t status; cleanup_free char *buffer = NULL; FILE *memfile; int ret; @@ -447,9 +455,6 @@ get_verbosity (PyObject *self arg_unused, PyObject *args) static PyObject * set_verbosity (PyObject *self arg_unused, PyObject *args) { - libcrun_error_t err; - PyObject *ctx_obj = NULL; - libcrun_context_t *ctx; int verbosity; if (!PyArg_ParseTuple (args, "i", &verbosity)) From 7369dcf9ecf4aef7caa924188c49332db64b3418 Mon Sep 17 00:00:00 2001 From: Giuseppe Scrivano Date: Thu, 13 Aug 2026 15:14:25 +0000 Subject: [PATCH 7/9] lua: include libcrun/spec.h for libcrun_container_spec libcrun_container_spec() moved from container.h to spec.h, but the Lua bindings still only included container.h. Building the bindings failed with an implicit-declaration error. Add the missing include. Co-Authored-By: Claude Opus 4.8 Signed-off-by: Giuseppe Scrivano --- lua/lua_crun.c | 1 + 1 file changed, 1 insertion(+) diff --git a/lua/lua_crun.c b/lua/lua_crun.c index cdd22b8907..fc0fecb97f 100644 --- a/lua/lua_crun.c +++ b/lua/lua_crun.c @@ -30,6 +30,7 @@ #include #include #include +#include #include #include From a1bc828ecd2cdd2e697a5b12d81d12411ff730cb Mon Sep 17 00:00:00 2001 From: Giuseppe Scrivano Date: Thu, 13 Aug 2026 15:14:33 +0000 Subject: [PATCH 8/9] lua: return luaL_error from luacrun_ctx_run luaL_error() is not declared as noreturn, so falling off the end of the function after calling it triggered a -Werror=return-type build failure. Return its result (it never actually returns) to satisfy the compiler. Co-Authored-By: Claude Opus 4.8 Signed-off-by: Giuseppe Scrivano --- lua/lua_crun.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/lua_crun.c b/lua/lua_crun.c index fc0fecb97f..84ca34ebc3 100644 --- a/lua/lua_crun.c +++ b/lua/lua_crun.c @@ -369,7 +369,7 @@ luacrun_ctx_run (lua_State *S) lua_pushnil (S); return luacrun_error (S, &crun_err) + 1; } else { - luaL_error(S, "failed to run container"); + return luaL_error (S, "failed to run container"); } } else From 9d97f481028f912e06ff3679a51a083af63d00ac Mon Sep 17 00:00:00 2001 From: Giuseppe Scrivano Date: Thu, 13 Aug 2026 15:14:45 +0000 Subject: [PATCH 9/9] ci: build and smoke-test the Python and Lua bindings The language bindings link against internal libcrun symbols but were never built in CI, so API drift (libcrun_container_spec moving to spec.h) and missing symbol exports (parse_json_file) went unnoticed until a build broke. Add a "bindings" test job that configures with --with-python-bindings and --with-lua-bindings under -Werror, builds the modules, and loads each freshly built module to run a smoke test. The smoke tests check that spec generation still works, that every documented entry point is registered, that the verbosity constants exist, and that loading a container resolves all the needed symbols. Ship the new test scripts in the tarball via EXTRA_DIST. Co-Authored-By: Claude Opus 4.8 Signed-off-by: Giuseppe Scrivano --- .github/workflows/test.yaml | 15 +++++++- Makefile.am | 3 +- lua/test_lua_bindings.lua | 56 +++++++++++++++++++++++++++++ python/test_python_bindings.py | 64 ++++++++++++++++++++++++++++++++++ 4 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 lua/test_lua_bindings.lua create mode 100644 python/test_python_bindings.py diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 7f7beb830b..dc1262b789 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -81,6 +81,7 @@ jobs: #- test: centos10-build - test: clang-format - test: clang-check + - test: bindings - test: checkpoint-restore - test: fuzzing - test: codespell @@ -97,7 +98,7 @@ jobs: sudo add-apt-repository -y ppa:criu/ppa # add-apt-repository runs apt-get update so we don't have to. - sudo apt-get install -q -y criu automake libtool autotools-dev libseccomp-dev git make libcap-dev cmake pkg-config gcc wget libsystemd-dev gperf clang-format libjson-c-dev containerd runc libasan6 libprotobuf-c-dev mawk + sudo apt-get install -q -y criu automake libtool autotools-dev libseccomp-dev git make libcap-dev cmake pkg-config gcc wget libsystemd-dev gperf clang-format libjson-c-dev containerd runc libasan6 libprotobuf-c-dev mawk python3-dev lua5.4 liblua5.4-dev # Fedora (used by maintainers as well as for packaging) has newer # md2man than in Ubuntu, so we have to install it from source. GOBIN=~/.local/bin go install github.com/cpuguy83/go-md2man/v2@latest @@ -187,6 +188,18 @@ jobs: ./configure --enable-embedded-blake3 --enable-shared make -j $(nproc) ;; + bindings) + # Build the Python and Lua bindings and load each freshly + # built module. The bindings link against internal libcrun + # symbols, so a build here catches API drift and missing + # exports (e.g. libcrun_container_spec, parse_json_file) + # before it reaches a release. + ./configure --enable-embedded-blake3 --enable-shared --with-python-bindings --with-lua-bindings CFLAGS='-Wall -Werror' + make -j $(nproc) + export LD_LIBRARY_PATH="$PWD/.libs:$LD_LIBRARY_PATH" + PYTHONPATH="$PWD/.libs" python3 python/test_python_bindings.py + LUA_CPATH="$PWD/.libs/?.so;;" lua5.4 lua/test_lua_bindings.lua + ;; embedded-blake3) ./configure --enable-embedded-blake3 make -j $(nproc) diff --git a/Makefile.am b/Makefile.am index 6905f5fa2e..4266b134a9 100644 --- a/Makefile.am +++ b/Makefile.am @@ -186,7 +186,8 @@ EXTRA_DIST = COPYING COPYING.libcrun README.md NEWS SECURITY.md rpm/crun.spec au src/libcrun/syscalls.h \ crun.1.md crun.1 libcrun.lds \ krun.1.md krun.1 \ - lua/luacrun.rockspec + lua/luacrun.rockspec lua/test_lua_bindings.lua \ + python/test_python_bindings.py if !HAVE_EMBEDDED_BLAKE3 EXTRA_DIST += src/libcrun/blake3/blake3.c \ diff --git a/lua/test_lua_bindings.lua b/lua/test_lua_bindings.lua new file mode 100644 index 0000000000..468f296204 --- /dev/null +++ b/lua/test_lua_bindings.lua @@ -0,0 +1,56 @@ +-- crun - OCI runtime written in C +-- +-- Copyright (C) 2026 crun Authors +-- crun is free software; you can redistribute it and/or modify +-- it under the terms of the GNU Lesser General Public License as published by +-- the Free Software Foundation; either version 2.1 of the License, or +-- (at your option) any later version. +-- +-- crun is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU Lesser General Public License for more details. +-- +-- You should have received a copy of the GNU Lesser General Public License +-- along with crun. If not, see . +-- +-- Smoke test for the Lua bindings. It does not run a container (that +-- requires a privileged environment); it only exercises the parts of the +-- module that must keep working so a build/link regression is caught early. +-- Set package.cpath (or LUA_CPATH) so that require("luacrun") resolves to +-- the freshly built luacrun.so before running. + +local crun = require("luacrun") + +-- Spec generation must keep working (feature exercised by callers). +local spec = crun.container_spec(true) +assert(type(spec) == "string" and #spec > 0, "generated spec is empty") +assert(spec:find("ociVersion"), "generated spec has no ociVersion") + +-- Every documented entry point must be present. This also catches a missing +-- registration in luacrun_library_reg. +local expected = { + "new_ctx", "container_spec", "new_container_from_string", + "new_container_from_file", "get_verbosity", "set_verbosity", "run", + "create_container", "delete_container", "kill_container", + "start_container", "status_container", "iter_container_names", + "update_container", +} +for _, name in ipairs(expected) do + assert(crun[name] ~= nil, "missing function " .. name) +end + +-- Verbosity constants. +for _, name in ipairs({ "VERBOSITY_ERROR", "VERBOSITY_WARNING", "VERBOSITY_DEBUG" }) do + assert(crun[name] ~= nil, "missing constant " .. name) +end + +crun.set_verbosity(crun.VERBOSITY_ERROR) +assert(crun.get_verbosity() == crun.VERBOSITY_ERROR, "verbosity round-trip failed") + +-- Loading a container from the generated spec must resolve every symbol the +-- module needs (this is what caught the missing parse_json_file / +-- libcrun_container_spec exports). +assert(crun.new_container_from_string(spec) ~= nil, "new_container_from_string failed") + +print("lua bindings smoke test: OK") diff --git a/python/test_python_bindings.py b/python/test_python_bindings.py new file mode 100644 index 0000000000..3acd76ef1a --- /dev/null +++ b/python/test_python_bindings.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +# crun - OCI runtime written in C +# +# Copyright (C) 2026 crun Authors +# crun is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. +# +# crun is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with crun. If not, see . +# +# Smoke test for the Python bindings. It does not run a container (that +# requires a privileged environment); it only exercises the parts of the +# module that must keep working so a build/link regression is caught early. +# Point PYTHONPATH at the directory holding python_crun.so before running. + +import gc +import json +import sys + +import python_crun + + +def main(): + # Spec generation must keep working (feature exercised by callers). + spec = json.loads(python_crun.spec()) + assert spec.get("ociVersion"), "generated spec has no ociVersion" + assert spec["process"]["args"], "generated spec has no process args" + + # Every documented entry point must be present. + expected = [ + "load_from_file", "load_from_memory", "run", "create", "delete", + "kill", "start", "list", "status", "update", "spec", "make_context", + "set_verbosity", "get_verbosity", + ] + missing = [name for name in expected if not hasattr(python_crun, name)] + assert not missing, "missing methods: %s" % missing + + # Verbosity constants. + for const in ("VERBOSITY_ERROR", "VERBOSITY_WARNING", "VERBOSITY_DEBUG"): + assert hasattr(python_crun, const), "missing constant %s" % const + + # Loading a container from the generated spec must resolve every symbol + # the module needs (this is what caught the missing parse_json_file / + # libcrun_container_spec exports). + python_crun.load_from_memory(json.dumps(spec)) + + # Creating and dropping a context must run the capsule destructor without + # crashing. + ctx = python_crun.make_context("crun-python-smoke-test") + del ctx + gc.collect() + + print("python bindings smoke test: OK") + + +if __name__ == "__main__": + sys.exit(main())