Lunatik is an in-kernel Lua runtime that enables dynamic scripting inside the Linux kernel—without requiring recompilation. It provides a flexible and lightweight way to extend kernel functionality, simplifying the development of networking features, device drivers, and system customization.
We resumed work on Lunatik in early 2023 after a long hiatus in its development. Our main motivation was to fill the gaps left in previous versions, where we had abandoned the framework-oriented vision in favor of a quick delivery approach. At the time, our priority was to achieve a fast time-to-market for delivering a smart firewall solution based on Netfilter scripting to a major Internet Service Provider (ISP).
Since we first started this journey in 2008, the greatest challenge in providing kernel scripting has always been bridging the gap between the kernel and the Lua interpreter—both in extending Lua with kernel APIs and embedding Lua into kernel subsystems so that scripts could be executed in response to events (e.g., packet filtering, file operations).
With this in mind, the goal of the new Lunatik 3.x series was to restore the original vision of a complete framework. This means providing both:
- A proper runtime management system, allowing Lua instances to be seamlessly embedded into kernel subsystems.
- A structured API layer, making it easier to create new kernel-to-Lua bindings.
In this article, we will guide the process of creating new bindings—both for extending Lua with kernel functionalities and embedding Lua into kernel subsystems.
Lunatik objects are special Lua userdata objects that bridge the gap between Lua and C kernel structures. These objects act as wrappers for the kernel resources we want to expose to Lua, mediating access to ensure safety. Lunatik objects combine two key characteristics:
- Reference Counting: Lunatik objects behave like C++ shared pointers, maintaining a reference count. When the reference count reaches zero, the object is automatically destroyed, preventing memory leaks.
- Synchronization: Lunatik objects use synchronization mechanisms similar to Java monitors. Their methods are executed in a mutually exclusive manner, preventing race conditions.
Additionally, Lunatik objects support two execution modes:
- Sleepable: Objects in this mode use regular mutexes for synchronization. They can sleep if necessary, making them suitable for process context.
- Non-Sleepable: Objects in this mode use spinlocks for synchronization. They cannot sleep and are designed mainly for softirq context.
It is important to note that non-sleepable objects can execute in both softirqs and process context. This allows them to be shared across different execution contexts, ensuring greater flexibility and interoperability within the kernel.
The first step in creating a Lunatik binding is to determine which kernel subsystem we want to bind to Lua. As an example, we will start by creating a binding for kfifo—a kernel subsystem that provides lockless, high-performance FIFO (First In, First Out) queues, commonly used for efficient data buffering and interprocess communication.
In this binding, we will focus on the first type of integration, which involves extending the Lua interpreter with kernel APIs. In our framework, this is done through a Lua library implemented as a kernel module.
The first step in this process is to define the data structure and APIs that will be exposed to Lua scripts. Since the struct kfifo
is straightforward and already provides everything we need to support basic FIFO operations, we will use it as is.
Although the kfifo API offers multiple ways to read from and write to the buffer, we typically want to expose a higher-level API to Lua scripts. In this case, we chose to provide two simple interfaces:
fifo:push(
– Enqueues bytes into the buffer.) fifo:pop(
– Dequeues bytes from the buffer.)
The figure below illustrates how this API can be used to implement the classic mailbox-based message-passing mechanism in Lua. In addition to the kfifo binding, this implementation also utilizes another binding for the kernel’s completion API. The complete implementation of the mailbox mechanism in Lua can be found in lib/mailbox.lua.
local fifo = require("fifo")
local completion = require("completion")...
local sizeoft = string.packsize("T")
function MailBox:receive(timeout)
local ok, err = self.event:wait(timeout)
if not ok then error(err) end
local queue = self.queue
local header, header_size = queue:pop(sizeoft)
if header_size == 0 then
return nil
elseif header_size < sizeoft then
error("malformed message")
end
return queue:pop(string.unpack("T", header))
end
function MailBox:send(message)
self.queue:push(string.pack("s", message))
self.event:complete()
end
To implement the FIFO APIs in Lua, named Luafifo, we follow the conventional approach used in regular Lua bindings, leveraging its C API to intermediate function calls and handle data crossing between Lua and C. The only difference in Lunatik is that the first argument of a method is expected to be a Lunatik object, which stores a pointer to the data structure used by the binding in a member called private
— in this case, a struct kfifo
.
To facilitate this, Lunatik provides a macro-based template for defining a function that verifies whether an object at a given position in the Lua stack is of the expected type and returns a pointer to its underlying data structure— the private
member. The usage of this macro, called LUNATIK_PRIVATECHECKER()
, is demonstrated in the figure below.
#include
#include
#include #include
#include
#include
#include
LUNATIK_PRIVATECHECKER(luafifo_check, struct kfifo *);
static int luafifo_push(lua_State *L)
{
struct kfifo *fifo = luafifo_check(L, 1);
size_t size;
const char *buf = luaL_checklstring(L, 2, &size);
luaL_argcheck(L, size <= kfifo_avail(fifo), 2, "not enough space");
kfifo_in(fifo, buf, size);
1 Comment
lneto
With GSoC approaching, I wrote an article on how to extend Lua with kernel APIs and embed the Lua interpreter into Linux subsystems using Lunatik—an in-kernel Lua runtime that enables dynamic scripting inside the Linux kernel without requiring recompilation.
The post covers key aspects of Lunatik bindings, including runtime management, integration into the framework, and practical examples like Xtables and kfifo.
Whether you're considering a GSoC project or just interested in kernel scripting with Lua, this article might be a useful reference!