Commit d1791f0b authored by Marek Vavruša's avatar Marek Vavruša Committed by yonghong-song

lua: codegen tests, tracking inverse form of null guards, IPv6 header builtins (#1991)

* lua: eliding repetitive NULL checks, fixes for read types, luacheck

This fixes verifier failures in some kernel versions that track whether
a possible NULL pointer has been check (and shouldn't be checked twice).

It also fixes accessing memory from maps with composite keys.

It fixes a case when temporary variable is used in condition,
and the body of the condition contains just an assignment from
immediate value to a variable that existed before the condition, e.g.:

```
local x = 1 -- Assign constant to 'x', it will be materialized here
if skb.len > 0 then -- New BB, not possible to fold as condition isn't const
    x = 2 -- Assign constant, it failed to materialize here
end -- End of BB
-- Value of x is always 1
```

The `bpf.socket()` support ljsyscall socket as first parameter, and
fixes compatibility with newer ljsyscall (type name has changed).

* reverse BPF_LD ntohl() semantics for <= 32bit loads as well

The loads using traditional instructions always do ntohl():

https://www.kernel.org/doc/Documentation/networking/filter.txt

They are however needed to support reads not using `skb`, e.g. NET_OFF

* proto: add builtins for traversing IPv6 header and extension headers

The IPv6 header is fixed size (40B), but it may contain either
extension header, or transport  protocol after it, so the caller
must check the `next_header` field before traversing to determine
which dissector to use to traverse it, e.g.:

```
if ip6.next_header == 44 then
   ip6 = ip6.ip6_opt -- Skip fragment header
end
if ip6.next_header == c.ip.proto_tcp then
   local tcp = ip6.tcp -- Finally, TCP
end
```

* reverse ntohl() for indirect BPF_LD as well

* lua: started codegen tests, direct skb support, bugfixes

This starts adding compiler correctness tests for basic expressions,
variable source tracking, and control flow.

Added:
* direct skb->data access (in addition to BPF_LDABS, BPF_LDIND)
* loads and stores from skb->data and map value backed memory
* unified variable source tracking (ptr_to_ctx, ptr_to_map[_or_null], ptr_to_skb, ptr_to_pkt, ptr_to_stack)
* BPF constants introduced between 4.10-4.15
* bpf.dump_string() to dump generated assembly (text) to string

Fixes:
* pointer nil check tracking
* dissectors for map value backed memory
* ljsyscall extensions when the version is too old
* KPRI nil variables used in conditions
* wrongly elided constant materialization on condition-less jumps
* loads/stores from stack memory using variable offset

* lua: track inverse null guards (if x ~= nil)

The verifier prohibits pointer comparisons except the first NULL check,
so both forms must be tracked, otherwise it will check for NULL twice
and verifier will reject it.

* lua: support cdata and constants larger than i32, fix shadowed variables

This adds support for numeric cdata constants (either in program, or
retrieved with GGET/UGET). Values larger than i32 are coerced into i64.

This also fixes shadowing of variables, and fixes materialization of
result of variable copy at the end of basic block.
parent 9252a8d0
-- Configuration for unit tests
-- See: http://olivinelabs.com/busted/
return {
default = {
lpath = "./?.lua;./?/init.lua",
helper = "./bpf/spec/helper.lua",
["auto-insulate"] = false,
}
}
std = 'luajit'
new_read_globals = {
'assert',
'describe',
'it',
}
new_globals = {
'math',
}
-- Luacheck < 0.18 doesn't support new_read_globals
for _, v in ipairs(new_read_globals) do
table.insert(new_globals, v)
end
-- Ignore some pedantic checks
ignore = {
'4.1/err', -- Shadowing err
'4.1/.', -- Shadowing one letter variables
}
This diff is collapsed.
......@@ -44,6 +44,22 @@ local function width_type(w)
end
builtins.width_type = width_type
-- Return struct member size/type (requires LuaJIT 2.1+)
-- I am ashamed that there's no easier way around it.
local function sizeofattr(ct, name)
if not ffi.typeinfo then error('LuaJIT 2.1+ is required for ffi.typeinfo') end
local cinfo = ffi.typeinfo(ct)
while true do
cinfo = ffi.typeinfo(cinfo.sib)
if not cinfo then return end
if cinfo.name == name then break end
end
local size = math.max(1, ffi.typeinfo(cinfo.sib or ct).size - cinfo.size)
-- Guess type name
return size, builtins.width_type(size)
end
builtins.sizeofattr = sizeofattr
-- Byte-order conversions for little endian
local function ntoh(x, w)
if w then x = ffi.cast(const_width_type[w/8], x) end
......@@ -76,21 +92,34 @@ if ffi.abi('be') then
return w and ffi.cast(const_width_type[w/8], x) or x
end
hton = ntoh
builtins[ntoh] = function(a, b, w) return end
builtins[hton] = function(a, b, w) return end
builtins[ntoh] = function(_, _, _) return end
builtins[hton] = function(_, _, _) return end
end
-- Other built-ins
local function xadd() error('NYI') end
builtins.xadd = xadd
builtins[xadd] = function (e, dst, a, b, off)
assert(e.V[a].const.__dissector, 'xadd(a, b) called on non-pointer')
local w = ffi.sizeof(e.V[a].const.__dissector)
builtins[xadd] = function (e, ret, a, b, off)
local vinfo = e.V[a].const
assert(vinfo and vinfo.__dissector, 'xadd(a, b[, offset]) called on non-pointer')
local w = ffi.sizeof(vinfo.__dissector)
-- Calculate structure attribute offsets
if e.V[off] and type(e.V[off].const) == 'string' then
local ct, field = vinfo.__dissector, e.V[off].const
off = ffi.offsetof(ct, field)
assert(off, 'xadd(a, b, offset) - offset is not valid in given structure')
w = sizeofattr(ct, field)
end
assert(w == 4 or w == 8, 'NYI: xadd() - 1 and 2 byte atomic increments are not supported')
-- Allocate registers and execute
e.vcopy(dst, a)
local src_reg = e.vreg(b)
local dst_reg = e.vreg(dst)
e.emit(BPF.JMP + BPF.JEQ + BPF.K, dst_reg, 0, 1, 0) -- if (dst != NULL)
local dst_reg = e.vreg(a)
-- Set variable for return value and call
e.vset(ret)
e.vreg(ret, 0, true, ffi.typeof('int32_t'))
-- Optimize the NULL check away if provably not NULL
if not e.V[a].source or e.V[a].source:find('_or_null', 1, true) then
e.emit(BPF.JMP + BPF.JEQ + BPF.K, dst_reg, 0, 1, 0) -- if (dst != NULL)
end
e.emit(BPF.XADD + BPF.STX + const_width[w], dst_reg, src_reg, off or 0, 0)
end
......@@ -137,11 +166,10 @@ end
builtins[ffi.cast] = function (e, dst, ct, x)
assert(e.V[ct].const, 'ffi.cast(ctype, x) called with bad ctype')
e.vcopy(dst, x)
if not e.V[x].const then
e.V[dst].type = ffi.typeof(e.V[ct].const)
else
if e.V[x].const and type(e.V[x].const) == 'table' then
e.V[dst].const.__dissector = ffi.typeof(e.V[ct].const)
end
e.V[dst].type = ffi.typeof(e.V[ct].const)
-- Specific types also encode source of the data
-- This is because BPF has different helpers for reading
-- different data sources, so variables must track origins.
......@@ -149,7 +177,7 @@ builtins[ffi.cast] = function (e, dst, ct, x)
-- struct skb - source of the data is socket buffer
-- struct X - source of the data is probe/tracepoint
if ffi.typeof(e.V[ct].const) == ffi.typeof('struct pt_regs') then
e.V[dst].source = 'probe'
e.V[dst].source = 'ptr_to_probe'
end
end
......@@ -160,7 +188,14 @@ builtins[ffi.new] = function (e, dst, ct, x)
assert(not x, 'NYI: ffi.new(ctype, ...) - initializer is not supported')
assert(not cdef.isptr(ct, true), 'NYI: ffi.new(ctype, ...) - ctype MUST NOT be a pointer')
e.vset(dst, nil, ct)
e.V[dst].source = 'ptr_to_stack'
e.V[dst].const = {__base = e.valloc(ffi.sizeof(ct), true), __dissector = ct}
-- Set array dissector if created an array
-- e.g. if ct is 'char [2]', then dissector is 'char'
local elem_type = tostring(ct):match('ctype<(.+)%s%[(%d+)%]>')
if elem_type then
e.V[dst].const.__dissector = ffi.typeof(elem_type)
end
end
builtins[ffi.copy] = function (e, ret, dst, src)
......@@ -169,7 +204,7 @@ builtins[ffi.copy] = function (e, ret, dst, src)
-- Specific types also encode source of the data
-- struct pt_regs - source of the data is probe
-- struct skb - source of the data is socket buffer
if e.V[src].source == 'probe' then
if e.V[src].source and e.V[src].source:find('ptr_to_probe', 1, true) then
e.reg_alloc(e.tmpvar, 1)
-- Load stack pointer to dst, since only load to stack memory is supported
-- we have to either use spilled variable or allocated stack memory offset
......@@ -221,7 +256,8 @@ builtins[print] = function (e, ret, fmt, a1, a2, a3)
-- TODO: this is materialize step
e.V[fmt].const = {__base=dst}
e.V[fmt].type = ffi.typeof('char ['..len..']')
elseif e.V[fmt].const.__base then -- NOP
elseif e.V[fmt].const.__base then -- luacheck: ignore
-- NOP
else error('NYI: print(fmt, ...) - format variable is not literal/stack memory') end
-- Prepare helper call
e.emit(BPF.ALU64 + BPF.MOV + BPF.X, 1, 10, 0, 0)
......@@ -270,7 +306,6 @@ end
-- Implements bpf_skb_load_bytes(ctx, off, var, vlen) on skb->data
local function load_bytes(e, dst, off, var)
print(e.V[off].const, e.V[var].const)
-- Set R2 = offset
e.vset(e.tmpvar, nil, off)
e.vreg(e.tmpvar, 2, false, ffi.typeof('uint64_t'))
......@@ -350,7 +385,7 @@ builtins[math.log2] = function (e, dst, x)
e.vcopy(e.tmpvar, x)
local v = e.vreg(e.tmpvar, 2)
if cdef.isptr(e.V[x].const) then -- No pointer arithmetics, dereference
e.vderef(v, v, ffi.typeof('uint64_t'))
e.vderef(v, v, {const = {__dissector=ffi.typeof('uint64_t')}})
end
-- Invert value to invert all tests, otherwise we would need and+jnz
e.emit(BPF.ALU64 + BPF.NEG + BPF.K, v, 0, 0, 0) -- v = ~v
......@@ -386,9 +421,9 @@ builtins[math.log] = function (e, dst, x)
end
-- Call-type helpers
local function call_helper(e, dst, h)
local function call_helper(e, dst, h, vtype)
e.vset(dst)
e.vreg(dst, 0, true)
e.vreg(dst, 0, true, vtype or ffi.typeof('uint64_t'))
e.emit(BPF.JMP + BPF.CALL, 0, 0, 0, h)
e.V[dst].const = nil -- Target is not a function anymore
end
......@@ -408,7 +443,7 @@ builtins.perf_submit = perf_submit
builtins.stack_id = stack_id
builtins.load_bytes = load_bytes
builtins[cpu] = function (e, dst) return call_helper(e, dst, HELPER.get_smp_processor_id) end
builtins[rand] = function (e, dst) return call_helper(e, dst, HELPER.get_prandom_u32) end
builtins[rand] = function (e, dst) return call_helper(e, dst, HELPER.get_prandom_u32, ffi.typeof('uint32_t')) end
builtins[time] = function (e, dst) return call_helper(e, dst, HELPER.ktime_get_ns) end
builtins[pid_tgid] = function (e, dst) return call_helper(e, dst, HELPER.get_current_pid_tgid) end
builtins[uid_gid] = function (e, dst) return call_helper(e, dst, HELPER.get_current_uid_gid) end
......
......@@ -15,7 +15,7 @@ limitations under the License.
]]
local ffi = require('ffi')
local bit = require('bit')
local S = require('syscall')
local has_syscall, S = pcall(require, 'syscall')
local M = {}
ffi.cdef [[
......@@ -132,23 +132,54 @@ struct bpf_stacktrace {
]]
-- Compatibility: ljsyscall doesn't have support for BPF syscall
if not S.bpf then
if not has_syscall or not S.bpf then
error("ljsyscall doesn't support bpf(), must be updated")
else
local strflag = require('syscall.helpers').strflag
-- Compatibility: ljsyscall<=0.12
if not S.c.BPF_MAP.PERCPU_HASH then
S.c.BPF_MAP.PERCPU_HASH = 5
S.c.BPF_MAP.PERCPU_ARRAY = 6
S.c.BPF_MAP.STACK_TRACE = 7
S.c.BPF_MAP.CGROUP_ARRAY = 8
S.c.BPF_MAP.LRU_HASH = 9
S.c.BPF_MAP.LRU_PERCPU_HASH = 10
S.c.BPF_MAP.LPM_TRIE = 11
if not S.c.BPF_MAP.LRU_HASH then
S.c.BPF_MAP = strflag {
UNSPEC = 0,
HASH = 1,
ARRAY = 2,
PROG_ARRAY = 3,
PERF_EVENT_ARRAY = 4,
PERCPU_HASH = 5,
PERCPU_ARRAY = 6,
STACK_TRACE = 7,
CGROUP_ARRAY = 8,
LRU_HASH = 9,
LRU_PERCPU_HASH = 10,
LPM_TRIE = 11,
ARRAY_OF_MAPS = 12,
HASH_OF_MAPS = 13,
DEVMAP = 14,
SOCKMAP = 15,
CPUMAP = 16,
}
end
if not S.c.BPF_PROG.TRACEPOINT then
S.c.BPF_PROG.TRACEPOINT = 5
S.c.BPF_PROG.XDP = 6
S.c.BPF_PROG.PERF_EVENT = 7
S.c.BPF_PROG = strflag {
UNSPEC = 0,
SOCKET_FILTER = 1,
KPROBE = 2,
SCHED_CLS = 3,
SCHED_ACT = 4,
TRACEPOINT = 5,
XDP = 6,
PERF_EVENT = 7,
CGROUP_SKB = 8,
CGROUP_SOCK = 9,
LWT_IN = 10,
LWT_OUT = 11,
LWT_XMIT = 12,
SOCK_OPS = 13,
SK_SKB = 14,
CGROUP_DEVICE = 15,
SK_MSG = 16,
RAW_TRACEPOINT = 17,
CGROUP_SOCK_ADDR = 18,
}
end
end
......@@ -180,6 +211,14 @@ function M.isptr(v, noarray)
return ctname
end
-- Return true if variable is a non-nil constant that can be used as immediate value
-- e.g. result of KSHORT and KNUM
function M.isimmconst(v)
return (type(v.const) == 'number' and not ffi.istype(v.type, ffi.typeof('void')))
or type(v.const) == 'cdata' and ffi.istype(v.type, ffi.typeof('uint64_t')) -- Lua numbers are at most 52 bits
or type(v.const) == 'cdata' and ffi.istype(v.type, ffi.typeof('int64_t'))
end
function M.osversion()
-- We have no better way to extract current kernel hex-string other
-- than parsing headers, compiling a helper function or reading /proc
......@@ -203,10 +242,10 @@ function M.event_reader(reader, event_type)
end
-- Wrap reader in interface that can interpret read event messages
return setmetatable({reader=reader,type=event_type}, {__index = {
block = function(self)
block = function(_ --[[self]])
return S.select { readfds = {reader.fd} }
end,
next = function(self, k)
next = function(_ --[[self]], k)
local len, ev = reader:next(k)
-- Filter out only sample frames
while ev and ev.type ~= S.c.PERF_RECORD.SAMPLE do
......
......@@ -37,6 +37,8 @@ local function decode_ins(func, pc)
end
if mc == 13*128 then -- BCMjump
c = pc+d-0x7fff
elseif mc == 14*128 then -- BCMcdata
c = jutil.funck(func, -d-1)
elseif mc == 9*128 then -- BCMint
c = jutil.funck(func, d)
elseif mc == 10*128 then -- BCMstr
......
......@@ -33,6 +33,21 @@ struct sk_buff {
uint32_t cb[5];
uint32_t hash;
uint32_t tc_classid;
uint32_t data;
uint32_t data_end;
uint32_t napi_id;
/* Accessed by BPF_PROG_TYPE_sk_skb types from here to ... */
uint32_t family;
uint32_t remote_ip4; /* Stored in network byte order */
uint32_t local_ip4; /* Stored in network byte order */
uint32_t remote_ip6[4]; /* Stored in network byte order */
uint32_t local_ip6[4]; /* Stored in network byte order */
uint32_t remote_port; /* Stored in network byte order */
uint32_t local_port; /* stored in host byte order */
/* ... here. */
uint32_t data_meta;
};
struct net_off_t {
......@@ -185,7 +200,7 @@ else
end
-- Map symbolic registers to architecture ABI
ffi.metatype('struct pt_regs', {
__index = function (t,k)
__index = function (_ --[[t]],k)
return assert(parm_to_reg[k], 'no such register: '..k)
end,
})
......@@ -223,7 +238,7 @@ local function next_offset(e, var, type, off, mask, shift)
if mask then
e.emit(BPF.ALU + BPF.AND + BPF.K, tmp_reg, 0, 0, mask)
end
if shift then
if shift and shift ~= 0 then
local op = BPF.LSH
if shift < 0 then
op = BPF.RSH
......@@ -264,9 +279,9 @@ M.type = function(typestr, t)
t.__dissector=ffi.typeof(typestr)
return t
end
M.skb = M.type('struct sk_buff', {__base=true})
M.pt_regs = M.type('struct pt_regs', {__base=true, source='probe'})
M.pkt = {off=0, __dissector=ffi.typeof('struct eth_t')} -- skb needs special accessors
M.skb = M.type('struct sk_buff', {source='ptr_to_ctx'})
M.pt_regs = M.type('struct pt_regs', {source='ptr_to_probe'})
M.pkt = M.type('struct eth_t', {off=0, source='ptr_to_pkt'}) -- skb needs special accessors
-- M.eth = function (...) return dissector(ffi.typeof('struct eth_t'), ...) end
M.dot1q = function (...) return dissector(ffi.typeof('struct dot1q_t'), ...) end
M.arp = function (...) return dissector(ffi.typeof('struct arp_t'), ...) end
......@@ -310,6 +325,28 @@ ffi.metatype(ffi.typeof('struct ip_t'), {
}
})
ffi.metatype(ffi.typeof('struct ip6_t'), {
__index = {
-- Skip fixed IPv6 header length (40 bytes)
-- The caller must check the value of `next_header` to skip any extension headers
icmp6 = function(e, dst) next_skip(e, dst, ffi.sizeof('struct ip6_t'), 0) end,
udp = function(e, dst) next_skip(e, dst, ffi.sizeof('struct ip6_t'), 0) end,
tcp = function(e, dst) next_skip(e, dst, ffi.sizeof('struct ip6_t'), 0) end,
ip6_opt = function(e, dst) next_skip(e, dst, ffi.sizeof('struct ip6_t'), 0) end,
}
})
local ip6_opt_ext_len_off = ffi.offsetof('struct ip6_opt_t', 'ext_len')
ffi.metatype(ffi.typeof('struct ip6_opt_t'), {
__index = {
-- Skip IPv6 extension header length (field `ext_len`)
icmp6 = function(e, dst) next_offset(e, dst, ffi.typeof('uint8_t'), ip6_opt_ext_len_off) end,
udp = function(e, dst) next_offset(e, dst, ffi.typeof('uint8_t'), ip6_opt_ext_len_off) end,
tcp = function(e, dst) next_offset(e, dst, ffi.typeof('uint8_t'), ip6_opt_ext_len_off) end,
ip6_opt = function(e, dst) next_offset(e, dst, ffi.typeof('uint8_t'), ip6_opt_ext_len_off) end,
}
})
ffi.metatype(ffi.typeof('struct tcp_t'), {
__index = {
-- Skip TCP header length (stored as number of words)
......
This diff is collapsed.
local ffi = require('ffi')
-- Define basic ctypes
ffi.cdef [[
struct bpf_insn {
uint8_t code; /* opcode */
uint8_t dst_reg:4; /* dest register */
uint8_t src_reg:4; /* source register */
uint16_t off; /* signed offset */
uint32_t imm; /* signed immediate constant */
};
]]
-- Inject mock ljsyscall for tests
package.loaded['syscall'] = {
bpf = function() error('mock') end,
c = { BPF_MAP = {}, BPF_PROG = {} },
abi = { arch = 'x64' },
}
package.loaded['syscall.helpers'] = {
strflag = function (tab)
local function flag(cache, str)
if type(str) ~= "string" then return str end
if #str == 0 then return 0 end
local s = str:upper()
if #s == 0 then return 0 end
local val = rawget(tab, s)
if not val then return nil end
cache[str] = val
return val
end
return setmetatable(tab, {__index = setmetatable({}, {__index = flag}), __call = function(t, a) return t[a] end})
end
}
\ No newline at end of file
src/lua/bpf/spec
\ No newline at end of file
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment