Arithmetic Operation Verification
do_check calls check_alu_op to check arithmetic operations and update tracked values.
check_alu_op
This functions checks ALU operations (32-bit & 64-bit):
- Checks non arithmetic operations:
BPF_END,BPF_NEGandBPF_MOV;- Dispatches arithmetic verification to
adjust_reg_min_max_vals.
Some of the checks done in this function:
- Reserved fields must be zeroed.
- Some pointer operations are prohibited:
- All pointer arithmetic
- Pointer subtraction is not strictly checked (i.e., the result is marked as unknown) and is only allowed if
allow_ptr_leaks.
- Pointer subtraction is not strictly checked (i.e., the result is marked as unknown) and is only allowed if
- Partial copy of a pointer
- All pointer arithmetic
R10is not writable while uninitialized registers are not readable. (Seecheck_reg_arg.)- Division by zero or undefined shifts (e.g.,
u64 << 65) are prohibited.
Side effects:
- Register status update:
mark_reg_scratched,mark_reg_read,mark_reg_unknown. - Instance mark:
mark_insn_zext. - Register scalar value update:
adjust_reg_min_max_vals, which callsadjust_scalar_min_max_vals. - Register pointer value update:
adjust_ptr_min_max_vals.
src \ dst | Pointer | Scalar |
|---|---|---|
| Pointer | Forbidden unless subtracting | adjust_ptr_min_max_vals |
| Scalar | adjust_ptr_min_max_vals | adjust_scalar_min_max_vals |
Precise value tracking
Precise value tracking was introduced in this comment: bpf: precise scalar_value tracking.
You should read through the commit message to grasp the gist and what each function does.
Also, I am quoting from a comment in a LWN article: it seems that the verifier always keeps the precise values, and marking a value as being "precise" just prevents it getting pruned. If it is so, this part mostly has more to do with branch pruning than actual ALU operations.
adjust_scalar_min_max_vals
This function dispatches operations to:
scalar_min_max_add: Additionscalar_min_max_sub: Subtractionscalar_min_max_mul: Multiplicationscalar_min_max_and: Bit-wise ANDscalar_min_max_or: Bit-wise ORscalar_min_max_xor: Bit-wise XORscalar_min_max_lsh: Left shiftscalar_min_max_rsh: Right shift (unsigned)scalar_min_max_arsh: Right shift (sign bit extending)
Basically they update the tnum field and the min/max fields of a scalar:
c
// https://github.com/torvalds/linux/blob/4dc12f37a8e98e1dca5521c14625c869537b50b6/include/linux/bpf_verifier.h#L147-L166
struct bpf_reg_state {
// ...
/* For scalar types (SCALAR_VALUE), this represents our knowledge of
* the actual value.
* For pointer types, this represents the variable part of the offset
* from the pointed-to object, and is shared with all bpf_reg_states
* with the same id as us.
*/
struct tnum var_off;
/* Used to determine if any memory access using this register will
* result in a bad access.
* These refer to the same value as var_off, not necessarily the actual
* contents of the register.
*/
s64 smin_value; /* minimum possible (s64)value */
s64 smax_value; /* maximum possible (s64)value */
u64 umin_value; /* minimum possible (u64)value */
u64 umax_value; /* maximum possible (u64)value */
s32 s32_min_value; /* minimum possible (s32)value */
s32 s32_max_value; /* maximum possible (s32)value */
u32 u32_min_value; /* minimum possible (u32)value */
u32 u32_max_value; /* maximum possible (u32)value */
// ...
};
Implementation details
If ever you would like to peek into the implementation details, here are some notes:
Maintained fields:
struct tnum: tracks the individual bits in the scalar, consisting of a mask (masked bits are unknown) and a value (recording the unmasked known bits).- minimum/maximum possible values for
u64/i64/u32/i32respectively.
Before dispatching,
adjust_scalar_min_max_valstries to:- Validate the register state (e.g.,
min_value <= max_value); - Ensure
src_knownfor some opcodes (e.g., shifts), or set the register as unknown; - Sanitize something that is probably part of branch tracking or pruning...
adjust_scalar_min_max_valshas undergone refactoring, which moved portions of it out into functions likescalar(32)_min_max_.... When reading the processing logic inscalar(32)_min_max_..., you should always take note that some of these functions requiresrc_known, that is, some of them assume thatsrc_regis a constant.- Validate the register state (e.g.,
In that big
switchblock inadjust_scalar_min_max_vals:- For some opcodes,
dst_reg->var_offis changed before callingscalar(32)_min_max_...;- These opcodes are
BPF_ADDandBPF_SUB, whosescalar(32)_min_max_...functions are independent ofdst_reg->var_off.
- These opcodes are
- For some other opcodes,
dst_reg->var_offis assigned after callingscalar(32)_min_max_...; - Shifts assign to
dst_reg->var_offinsidescalar(32)_min_max_....
- For some opcodes,
The actual logic inside
scalar(32)_min_max_...functions:- Operations other than
and,orandmulrequiresrc_known, although some of them could handle unknownsrc_regin principle. - Bit-wise operations (
and,or,xor) are quite straightforward. - For
add,subandmul, uh, you might want to check out this essay: [arXiv:2105.05398] Sound, Precise, and Fast Abstract Interpretation with Tristate Numbers. - For shifts, things are quite easy if you bear in mind that they require a constant
src_reg.
- Operations other than
adjust_scalar_min_max_valscallsreg_bounds_syncafter updating the fields. It is quite crucial since it syncs the bit map (var_off) with sign bit info.In functions for
and,or,xor, you will find that they assumevar_offis in sync. The following code assumes that a positivesmin_valueimplies a syncedvar_offwith its sign bit cleared.c// https://github.com/torvalds/linux/blob/4dc12f37a8e98e1dca5521c14625c869537b50b6/kernel/bpf/verifier.c#L8622-L8655 static void scalar_min_max_and(...) { // ... dst_reg->umin_value = dst_reg->var_off.value; dst_reg->umax_value = min(dst_reg->umax_value, umax_val); if (dst_reg->smin_value < 0 || smin_val < 0) { // ... } else { /* ANDing two positives gives a positive, so safe to * cast result into s64. */ dst_reg->smin_value = dst_reg->umin_value; dst_reg->smax_value = dst_reg->umax_value; } // ... }
adjust_ptr_min_max_vals
This function is quite similar to adjust_scalar_min_max_vals, utilizing very much the same fields (s(u)min(max)_val).
- Operations allowed:
- 64-bit addition:
ptr += scalarorscalar += ptr. - 64-bit subtraction:
ptr -= scalar. - 32-bit subtraction allowed only if
allow_ptr_leaksis enabled
- 64-bit addition:
- Operations disallowed:
- Arithmetic on null-able pointers
- Adding/Subtracting non-zero values to/from a
CONST_PTR_TO_MAP - Arithmetic on
PTR_TO_PACKET_END,PTR_TO_SOCKET,PTR_TO_SOCK_COMMON,PTR_TO_TCP_SOCK,PTR_TO_XDP_SOCK... - Subtraction from stack pointer
- Some insane operations, e.g.
ptr += u64::MAX(Seecheck_reg_sane_offsetfor details)
Some extra work:
- Arithmetic operations clear the range of
pkt_ptr. sanitize_ptr_alu: No idea yet.
(WIP)