diff --git a/security/lua/docs/API.md b/security/lua/docs/API.md index be5a8a0bfb26..706777ffd979 100644 --- a/security/lua/docs/API.md +++ b/security/lua/docs/API.md @@ -96,4 +96,61 @@ 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: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()` -> 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 +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. + +## 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`. diff --git a/security/lua/lua_net.c b/security/lua/lua_net.c index ba701c79b019..9bd181abd885 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 }, @@ -256,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); @@ -316,12 +324,48 @@ static int net_skb_secmark(lua_State *L) return 1; } +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; + 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 || + net_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 } };