Skip to content

Commit b8b21fc

Browse files
committed
bpf: Fix partial dynptr stack slot reads/writes
jira VULN-6599 cve CVE-2023-39191 commit-author Kumar Kartikeya Dwivedi <[email protected]> commit ef8fc7a upstream-diff The prototype for __mark_reg_not_init had to be moved before the new destroy_if_dynptr_stack_slot function. In newer kernels this prototype has already been moved earlier in the file. s/__get_spi/get_spi/g as the __get_spi funtion hasn't been split out yet in this kernel version. __get_spi in future kernels and get_spi in this kernel are identical. The upstream commit tweaks some selftest failure messages, but those messages don't exist in this kernel. Currently, while reads are disallowed for dynptr stack slots, writes are not. Reads don't work from both direct access and helpers, while writes do work in both cases, but have the effect of overwriting the slot_type. While this is fine, handling for a few edge cases is missing. Firstly, a user can overwrite the stack slots of dynptr partially. Consider the following layout: spi: [d][d][?] 2 1 0 First slot is at spi 2, second at spi 1. Now, do a write of 1 to 8 bytes for spi 1. This will essentially either write STACK_MISC for all slot_types or STACK_MISC and STACK_ZERO (in case of size < BPF_REG_SIZE partial write of zeroes). The end result is that slot is scrubbed. Now, the layout is: spi: [d][m][?] 2 1 0 Suppose if user initializes spi = 1 as dynptr. We get: spi: [d][d][d] 2 1 0 But this time, both spi 2 and spi 1 have first_slot = true. Now, when passing spi 2 to dynptr helper, it will consider it as initialized as it does not check whether second slot has first_slot == false. And spi 1 should already work as normal. This effectively replaced size + offset of first dynptr, hence allowing invalid OOB reads and writes. Make a few changes to protect against this: When writing to PTR_TO_STACK using BPF insns, when we touch spi of a STACK_DYNPTR type, mark both first and second slot (regardless of which slot we touch) as STACK_INVALID. Reads are already prevented. Second, prevent writing to stack memory from helpers if the range may contain any STACK_DYNPTR slots. Reads are already prevented. For helpers, we cannot allow it to destroy dynptrs from the writes as depending on arguments, helper may take uninit_mem and dynptr both at the same time. This would mean that helper may write to uninit_mem before it reads the dynptr, which would be bad. PTR_TO_MEM: [?????dd] Depending on the code inside the helper, it may end up overwriting the dynptr contents first and then read those as the dynptr argument. Verifier would only simulate destruction when it does byte by byte access simulation in check_helper_call for meta.access_size, and fail to catch this case, as it happens after argument checks. The same would need to be done for any other non-trivial objects created on the stack in the future, such as bpf_list_head on stack, or bpf_rb_root on stack. A common misunderstanding in the current code is that MEM_UNINIT means writes, but note that writes may also be performed even without MEM_UNINIT in case of helpers, in that case the code after handling meta && meta->raw_mode will complain when it sees STACK_DYNPTR. So that invalid read case also covers writes to potential STACK_DYNPTR slots. The only loophole was in case of meta->raw_mode which simulated writes through instructions which could overwrite them. A future series sequenced after this will focus on the clean up of helper access checks and bugs around that. Fixes: 97e03f5 ("bpf: Add verifier support for dynptrs") Signed-off-by: Kumar Kartikeya Dwivedi <[email protected]> Link: https://lore.kernel.org/r/[email protected] Signed-off-by: Alexei Starovoitov <[email protected]> (cherry picked from commit ef8fc7a) Signed-off-by: Brett Mastbergen <[email protected]>
1 parent c566432 commit b8b21fc

File tree

1 file changed

+92
-3
lines changed

1 file changed

+92
-3
lines changed

kernel/bpf/verifier.c

+92-3
Original file line numberDiff line numberDiff line change
@@ -695,6 +695,12 @@ static bool dynptr_type_refcounted(enum bpf_dynptr_type type)
695695
return type == BPF_DYNPTR_TYPE_RINGBUF;
696696
}
697697

698+
static void __mark_reg_not_init(const struct bpf_verifier_env *env,
699+
struct bpf_reg_state *reg);
700+
701+
static int destroy_if_dynptr_stack_slot(struct bpf_verifier_env *env,
702+
struct bpf_func_state *state, int spi);
703+
698704
static int mark_stack_slots_dynptr(struct bpf_verifier_env *env, struct bpf_reg_state *reg,
699705
enum bpf_arg_type arg_type, int insn_idx)
700706
{
@@ -762,6 +768,55 @@ static int unmark_stack_slots_dynptr(struct bpf_verifier_env *env, struct bpf_re
762768
return 0;
763769
}
764770

771+
static void __mark_reg_unknown(const struct bpf_verifier_env *env,
772+
struct bpf_reg_state *reg);
773+
774+
static int destroy_if_dynptr_stack_slot(struct bpf_verifier_env *env,
775+
struct bpf_func_state *state, int spi)
776+
{
777+
int i;
778+
779+
/* We always ensure that STACK_DYNPTR is never set partially,
780+
* hence just checking for slot_type[0] is enough. This is
781+
* different for STACK_SPILL, where it may be only set for
782+
* 1 byte, so code has to use is_spilled_reg.
783+
*/
784+
if (state->stack[spi].slot_type[0] != STACK_DYNPTR)
785+
return 0;
786+
787+
/* Reposition spi to first slot */
788+
if (!state->stack[spi].spilled_ptr.dynptr.first_slot)
789+
spi = spi + 1;
790+
791+
if (dynptr_type_refcounted(state->stack[spi].spilled_ptr.dynptr.type)) {
792+
verbose(env, "cannot overwrite referenced dynptr\n");
793+
return -EINVAL;
794+
}
795+
796+
mark_stack_slot_scratched(env, spi);
797+
mark_stack_slot_scratched(env, spi - 1);
798+
799+
/* Writing partially to one dynptr stack slot destroys both. */
800+
for (i = 0; i < BPF_REG_SIZE; i++) {
801+
state->stack[spi].slot_type[i] = STACK_INVALID;
802+
state->stack[spi - 1].slot_type[i] = STACK_INVALID;
803+
}
804+
805+
/* TODO: Invalidate any slices associated with this dynptr */
806+
807+
/* Do not release reference state, we are destroying dynptr on stack,
808+
* not using some helper to release it. Just reset register.
809+
*/
810+
__mark_reg_not_init(env, &state->stack[spi].spilled_ptr);
811+
__mark_reg_not_init(env, &state->stack[spi - 1].spilled_ptr);
812+
813+
/* Same reason as unmark_stack_slots_dynptr above */
814+
state->stack[spi].spilled_ptr.live |= REG_LIVE_WRITTEN;
815+
state->stack[spi - 1].spilled_ptr.live |= REG_LIVE_WRITTEN;
816+
817+
return 0;
818+
}
819+
765820
static bool is_dynptr_reg_valid_uninit(struct bpf_verifier_env *env, struct bpf_reg_state *reg)
766821
{
767822
struct bpf_func_state *state = func(env, reg);
@@ -1300,9 +1355,6 @@ static const int caller_saved[CALLER_SAVED_REGS] = {
13001355
BPF_REG_0, BPF_REG_1, BPF_REG_2, BPF_REG_3, BPF_REG_4, BPF_REG_5
13011356
};
13021357

1303-
static void __mark_reg_not_init(const struct bpf_verifier_env *env,
1304-
struct bpf_reg_state *reg);
1305-
13061358
/* This helper doesn't clear reg->id */
13071359
static void ___mark_reg_known(struct bpf_reg_state *reg, u64 imm)
13081360
{
@@ -3052,6 +3104,10 @@ static int check_stack_write_fixed_off(struct bpf_verifier_env *env,
30523104
env->insn_aux_data[insn_idx].sanitize_stack_spill = true;
30533105
}
30543106

3107+
err = destroy_if_dynptr_stack_slot(env, state, spi);
3108+
if (err)
3109+
return err;
3110+
30553111
mark_stack_slot_scratched(env, spi);
30563112
if (reg && !(off % BPF_REG_SIZE) && register_is_bounded(reg) &&
30573113
!register_is_null(reg) && env->bpf_capable) {
@@ -3165,6 +3221,14 @@ static int check_stack_write_var_off(struct bpf_verifier_env *env,
31653221
if (err)
31663222
return err;
31673223

3224+
for (i = min_off; i < max_off; i++) {
3225+
int spi;
3226+
3227+
spi = get_spi(i);
3228+
err = destroy_if_dynptr_stack_slot(env, state, spi);
3229+
if (err)
3230+
return err;
3231+
}
31683232

31693233
/* Variable offset writes destroy any spilled pointers in range. */
31703234
for (i = min_off; i < max_off; i++) {
@@ -5126,6 +5190,31 @@ static int check_stack_range_initialized(
51265190
}
51275191

51285192
if (meta && meta->raw_mode) {
5193+
/* Ensure we won't be overwriting dynptrs when simulating byte
5194+
* by byte access in check_helper_call using meta.access_size.
5195+
* This would be a problem if we have a helper in the future
5196+
* which takes:
5197+
*
5198+
* helper(uninit_mem, len, dynptr)
5199+
*
5200+
* Now, uninint_mem may overlap with dynptr pointer. Hence, it
5201+
* may end up writing to dynptr itself when touching memory from
5202+
* arg 1. This can be relaxed on a case by case basis for known
5203+
* safe cases, but reject due to the possibilitiy of aliasing by
5204+
* default.
5205+
*/
5206+
for (i = min_off; i < max_off + access_size; i++) {
5207+
int stack_off = -i - 1;
5208+
5209+
spi = get_spi(i);
5210+
/* raw_mode may write past allocated_stack */
5211+
if (state->allocated_stack <= stack_off)
5212+
continue;
5213+
if (state->stack[spi].slot_type[stack_off % BPF_REG_SIZE] == STACK_DYNPTR) {
5214+
verbose(env, "potential write to dynptr at off=%d disallowed\n", i);
5215+
return -EACCES;
5216+
}
5217+
}
51295218
meta->access_size = access_size;
51305219
meta->regno = regno;
51315220
return 0;

0 commit comments

Comments
 (0)