From 6e33dbfbb3a9ba51c7cde1e2f6ef51a41e11849e Mon Sep 17 00:00:00 2001 From: Zongyao Chen Date: Tue, 4 Aug 2026 17:00:58 +0800 Subject: [PATCH 1/6] lua-lsm: add skb payload accessors A netlink or packet policy has to look at message contents to make a decision, but the skb object only exposed metadata, so such policies could not be written at all. Expose the payload, and the raw protocol number that suites() cannot name, so content-aware policies become possible. Signed-off-by: Zongyao Chen --- security/lua/lua_net.c | 50 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/security/lua/lua_net.c b/security/lua/lua_net.c index ba701c79b019..acb18e8c6dd5 100644 --- a/security/lua/lua_net.c +++ b/security/lua/lua_net.c @@ -8,6 +8,7 @@ #include "debug.h" #include #include +#include #include #include #include @@ -21,6 +22,9 @@ #include "kvcache.h" #include "lua_object.h" +/* Upper bound of a single skb:read(), sized to stay on the kernel stack. */ +#define SKB_READ_MAX 256 + static const char *family_tostring(sa_family_t sa_family) { const char *family = NULL; @@ -206,9 +210,18 @@ SOCK_BOOL_DEF(udp) SOCK_BOOL_DEF(stream_unix) SOCK_BOOL_DEF(vsock) +static int net_sock_proto(lua_State *L) +{ + struct sock *sk = tosock(L, 1); + + lua_pushinteger(L, (lua_Integer)sk->sk_protocol); + return 1; +} + static const luaL_Reg sock_meth[] = { { "socket", net_sock_socket }, { "suites", net_sock_suites }, + { "proto", net_sock_proto }, { "listener", net_sock_listener }, { "is_inet", net_sock_is_inet }, { "is_tcp", net_sock_is_tcp }, @@ -316,12 +329,49 @@ static int net_skb_secmark(lua_State *L) return 1; } +static int skb_read_bounded(struct sk_buff *skb, lua_Integer offset, + void *dst, unsigned int size) +{ + if (offset < 0 || offset > (lua_Integer)skb->len) + return -EINVAL; + if (size > skb->len - (u32)offset) + return -EINVAL; + if (skb_copy_bits(skb, (int)offset, dst, (int)size)) + return -EFAULT; + return 0; +} + +static int net_skb_len(lua_State *L) +{ + struct sk_buff *skb = toskb(L, 1); + + lua_pushinteger(L, (lua_Integer)skb->len); + return 1; +} + +static int net_skb_read(lua_State *L) +{ + struct sk_buff *skb = toskb(L, 1); + lua_Integer offset = luaL_checkinteger(L, 2); + lua_Integer length = luaL_checkinteger(L, 3); + char buffer[SKB_READ_MAX]; + + if (length < 0 || length > SKB_READ_MAX || + skb_read_bounded(skb, offset, buffer, (unsigned int)length)) + lua_pushnil(L); + else + lua_pushlstring(L, buffer, (size_t)length); + return 1; +} + static const luaL_Reg skb_meth[] = { { "sock", net_skb_sock }, { "full_sk", net_skb_full_sk }, { "protocol", net_skb_protocol }, { "iif", net_skb_iif }, { "secmark", net_skb_secmark }, + { "len", net_skb_len }, + { "read", net_skb_read }, { NULL, NULL } }; From 3cb5e90f563a549d7e6bdb83c7cd3044f23e6a96 Mon Sep 17 00:00:00 2001 From: Zongyao Chen Date: Tue, 4 Aug 2026 17:01:07 +0800 Subject: [PATCH 2/6] doc: lua-lsm: describe skb and sock net accessors So policy authors can find the accessors and know a bad read yields nil rather than an error, keeping the decision in the policy's hands. Signed-off-by: Zongyao Chen --- security/lua/docs/API.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/security/lua/docs/API.md b/security/lua/docs/API.md index be5a8a0bfb26..9ea07024a975 100644 --- a/security/lua/docs/API.md +++ b/security/lua/docs/API.md @@ -96,4 +96,22 @@ Inode (`inode`): Dentry (`dentry`): - `dentry:path()` -> path string -For a full list, see the method tables in `lua_kernel.c` and `lua_fs.c`. +Sock (`sock`): +- `sock:suites()` -> family, type, protocol strings +- `sock:proto()` -> raw `sk_protocol` number, needed for non-IP protocol + namespaces such as netlink's `NETLINK_GENERIC` + +Skb (`skb`): +- `skb:len()` -> payload length +- `skb:read(off, len)` -> `len` bytes as a string (`len` <= 256), or `nil` when + out of range + +`skb:read()` returns raw bytes; decode multi-byte fields in Lua (e.g. with +`string.byte`), since the in-kernel Lua has no bit library. Reads are +bounds-checked and return `nil` instead of raising, so the policy decides the +verdict. Note that a Lua error inside a hook falls back to that hook's default +return value, which is "allow" for most hooks; wrap parsing in `pcall()` when +the policy must fail closed. + +For a full list, see the method tables in `lua_kernel.c`, `lua_fs.c`, and +`lua_net.c`. From 3415bf815467d0be31eef5158af537547d8f339b Mon Sep 17 00:00:00 2001 From: Zongyao Chen Date: Wed, 5 Aug 2026 16:37:30 +0800 Subject: [PATCH 3/6] lua-lsm: hand policies only full socks from skb:sock() On handshake and time-wait paths skb->sk is a request or time-wait sock, which stops short of the fields sock:proto() and sock:suites() read. A policy asking for the protocol number therefore got neighbouring slab bytes, and since those pass for a valid number the rule they feed fails open rather than erroring. Resolve skb->sk through skb_to_full_sk() so a sock handed to a policy is always a full sock, making the guarantee a property of the object rather than of each accessor. skb:full_sk() then duplicates skb:sock() and has no users, so drop it. Signed-off-by: Zongyao Chen --- security/lua/lua_net.c | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/security/lua/lua_net.c b/security/lua/lua_net.c index acb18e8c6dd5..cde3a1167f12 100644 --- a/security/lua/lua_net.c +++ b/security/lua/lua_net.c @@ -269,16 +269,11 @@ static const luaL_Reg socket_meth[] = { /********************************** sk_buff *********************************/ +/* + * A request or time-wait sk lacks the fields the sock accessors read, and the + * garbage passes for a valid protocol number, so the policy fails open. + */ static int net_skb_sock(lua_State *L) -{ - struct sk_buff *skb = toskb(L, 1); - struct sock *sk = skb->sk; - - sk ? *newsock(L) = sk : lua_pushnil(L); - return 1; -} - -static int net_skb_full_sk(lua_State *L) { struct sk_buff *skb = toskb(L, 1); struct sock *sk = skb_to_full_sk(skb); @@ -366,7 +361,6 @@ static int net_skb_read(lua_State *L) static const luaL_Reg skb_meth[] = { { "sock", net_skb_sock }, - { "full_sk", net_skb_full_sk }, { "protocol", net_skb_protocol }, { "iif", net_skb_iif }, { "secmark", net_skb_secmark }, From bb1fb8f1d4e63a9dbdad586b2a67b431cd1bf6d6 Mon Sep 17 00:00:00 2001 From: Zongyao Chen Date: Wed, 5 Aug 2026 16:37:35 +0800 Subject: [PATCH 4/6] doc: lua-lsm: note that skb:sock() yields only a full sock A policy author cannot tell from the method name that a half-open or time-wait connection resolves to the listener or to nil, and would read the nil as "no owning socket" instead of "not a full sock". Signed-off-by: Zongyao Chen --- security/lua/docs/API.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/security/lua/docs/API.md b/security/lua/docs/API.md index 9ea07024a975..97935bc9338b 100644 --- a/security/lua/docs/API.md +++ b/security/lua/docs/API.md @@ -102,6 +102,10 @@ Sock (`sock`): namespaces such as netlink's `NETLINK_GENERIC` Skb (`skb`): +- `skb:sock()` -> owning sock, or `nil`. Always a full sock: a half-open + (`SYN_RECV`) connection resolves to its listening sock and a time-wait sock + yields `nil`, because those mini-sock forms lack the fields the `sock` + accessors read - `skb:len()` -> payload length - `skb:read(off, len)` -> `len` bytes as a string (`len` <= 256), or `nil` when out of range From 9da00d4b26aedf7272de131b54b8ff4162c0e594 Mon Sep 17 00:00:00 2001 From: Zongyao Chen Date: Wed, 5 Aug 2026 17:12:04 +0800 Subject: [PATCH 5/6] lua-lsm: rename skb_read_bounded() to net_skb_read_bounded() The helper is local to lua_net.c, but skb_ is netcore's prefix and the function sits two lines from skb_copy_bits() and skb_to_full_sk(), so nothing tells a reader it is not a core networking helper. Every other static function in the file carries the module prefix. Signed-off-by: Zongyao Chen --- security/lua/lua_net.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/security/lua/lua_net.c b/security/lua/lua_net.c index cde3a1167f12..9bd181abd885 100644 --- a/security/lua/lua_net.c +++ b/security/lua/lua_net.c @@ -324,8 +324,8 @@ static int net_skb_secmark(lua_State *L) return 1; } -static int skb_read_bounded(struct sk_buff *skb, lua_Integer offset, - void *dst, unsigned int size) +static int net_skb_read_bounded(struct sk_buff *skb, lua_Integer offset, + void *dst, unsigned int size) { if (offset < 0 || offset > (lua_Integer)skb->len) return -EINVAL; @@ -352,7 +352,7 @@ static int net_skb_read(lua_State *L) char buffer[SKB_READ_MAX]; if (length < 0 || length > SKB_READ_MAX || - skb_read_bounded(skb, offset, buffer, (unsigned int)length)) + net_skb_read_bounded(skb, offset, buffer, (unsigned int)length)) lua_pushnil(L); else lua_pushlstring(L, buffer, (size_t)length); From ee8a27ab6ffc70fe04872e5268aa2e93fb7574ab Mon Sep 17 00:00:00 2001 From: Zongyao Chen Date: Wed, 5 Aug 2026 17:12:12 +0800 Subject: [PATCH 6/6] doc: lua-lsm: define the skb data window skb:len() was documented as "payload length", but it measures the window starting at skb->data, and each layer advances that pointer past its own header, so at most hooks the window still covers a header. A policy author trusting the word "payload" computes every skb:read() offset from the wrong base, and an out-of-range read yields nil rather than an error, so the mistake never surfaces. Define the window once for both accessors and record where it starts in each hook that passes an skb, since that is the only thing an offset can be computed from. Signed-off-by: Zongyao Chen --- security/lua/docs/API.md | 41 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/security/lua/docs/API.md b/security/lua/docs/API.md index 97935bc9338b..706777ffd979 100644 --- a/security/lua/docs/API.md +++ b/security/lua/docs/API.md @@ -106,9 +106,9 @@ Skb (`skb`): (`SYN_RECV`) connection resolves to its listening sock and a time-wait sock yields `nil`, because those mini-sock forms lack the fields the `sock` accessors read -- `skb:len()` -> payload length -- `skb:read(off, len)` -> `len` bytes as a string (`len` <= 256), or `nil` when - out of range +- `skb:len()` -> length in bytes of the skb's data window +- `skb:read(off, len)` -> `len` bytes of the data window as a string + (`len` <= 256), or `nil` when `off` or `off + len` falls outside it `skb:read()` returns raw bytes; decode multi-byte fields in Lua (e.g. with `string.byte`), since the in-kernel Lua has no bit library. Reads are @@ -117,5 +117,40 @@ verdict. Note that a Lua error inside a hook falls back to that hook's default return value, which is "allow" for most hooks; wrap parsing in `pcall()` when the policy must fail closed. +## The skb data window + +`skb:len()` and `skb:read()` share one base, the skb's data window: the +`skb->len` bytes starting at `skb->data`. Offset 0 of `skb:read()` is the first +byte of that window, and a read reaching into the skb's paged fragments is +stitched together transparently. + +That first byte is not the start of the packet. Each protocol layer consumes its +header by advancing `skb->data` past it, so where the window begins is a +property of the hook rather than of the packet: by the time any socket-level +hook runs on the IPv4 receive path the network header is already consumed, and +SCTP has consumed both its chunk header and the INIT header. An offset is +therefore only meaningful for the one hook it was computed for, and getting it +wrong is quiet: an out-of-range read yields `nil`, an in-range one yields +whatever field actually sits there, so a policy built on the wrong base can look +like it works. + +Bases verified against this tree: + +| hook | first byte of the window | hook called from | +| --- | --- | --- | +| `netlink_send` | the netlink message header (`struct nlmsghdr`) | `netlink_sendmsg()` | +| `socket_sock_rcv_skb` | the TCP or UDP header | `tcp_filter()`, `udp_queue_rcv_one_skb()` | +| `inet_conn_request` | the TCP header of the SYN | `tcp_v4_route_req()`, `cookie_v4_check()` | +| `inet_conn_established` | the TCP header of the SYN-ACK | `tcp_finish_connect()` | +| `socket_getpeersec_dgram` | the UDP header | `ip_cmsg_recv_security()` | +| `sctp_assoc_request` | the INIT chunk's parameters, for the INIT state | `sctp_sf_do_5_1B_init()` | +| `xfrm_decode_session` | not established; verify before relying on it | `__xfrm_decode_session()` | + +Two further details about the hooks above: +- Under `netlink_send` the window spans everything one `sendmsg()` wrote, so a + batch arrives as consecutive length-delimited messages, not one message. +- `socket_sock_rcv_skb` runs before the socket filter can trim the skb, so the + window is the untrimmed length. + For a full list, see the method tables in `lua_kernel.c`, `lua_fs.c`, and `lua_net.c`.