diff options
| author | Paolo Bonzini <pbonzini@redhat.com> | 2026-10-15 10:11:08 +0200 |
|---|---|---|
| committer | Paolo Bonzini <pbonzini@redhat.com> | 2026-10-15 10:11:08 +0200 |
| commit | 8edffbc1f6272db167037e1a8c49b99e9cd04803 (patch) | |
| tree | b01a0cab46b5ce160820a85be5c1e98899860723 /tools | |
| parent | 439d8f9cb01cb04f9b31910efd0ae8dbdfa4ad97 (diff) | |
| parent | 34f4ce22cf25802d8026c60c13c2db81f8c8e3a0 (diff) | |
Merge tag 'loongarch-kvm-7.4' of git://git.kernel.org/pub/scm/linux/kernel/git/chenhuacai/linux-loongson into HEAD
LoongArch KVM changes for v7.4
1. Add separate vmid support.
2. Simplify MSI interrupt injection.
3. Add crash hypercall support.
4. Allow to set pv_feature until vCPU run.
5. Enable HW page table walk automatically in selftests.
Diffstat (limited to 'tools')
135 files changed, 11754 insertions, 534 deletions
diff --git a/tools/arch/riscv/include/asm/csr.h b/tools/arch/riscv/include/asm/csr.h index 21d8cee04638..8df64314d613 100644 --- a/tools/arch/riscv/include/asm/csr.h +++ b/tools/arch/riscv/include/asm/csr.h @@ -163,12 +163,24 @@ #define HGATP_MODE_SHIFT HGATP32_MODE_SHIFT #endif -/* VSIP & HVIP relation */ +/* + * VSIP & HVIP relation + * + * The bit positions are same between VSIP and HVIP for interrupt + * numbers 13-63, where there's a shift for the SSI, STI and SEI. + */ #define VSIP_TO_HVIP_SHIFT (IRQ_VS_SOFT - IRQ_S_SOFT) -#define VSIP_VALID_MASK ((_AC(1, UL) << IRQ_S_SOFT) | \ +#define VSIP_BIAS_MASK ((_AC(1, UL) << IRQ_S_SOFT) | \ (_AC(1, UL) << IRQ_S_TIMER) | \ - (_AC(1, UL) << IRQ_S_EXT) | \ - (_AC(1, UL) << IRQ_PMU_OVF)) + (_AC(1, UL) << IRQ_S_EXT)) +#define VSIP_NO_BIAS_MASK (_AC(1, UL) << IRQ_PMU_OVF) +#define VSIP_VALID_MASK (VSIP_BIAS_MASK | VSIP_NO_BIAS_MASK) +#define vsip_to_hvip(_vsip) ((((_vsip) & VSIP_BIAS_MASK) << \ + VSIP_TO_HVIP_SHIFT) | \ + ((_vsip) & VSIP_NO_BIAS_MASK)) +#define hvip_to_vsip(_hvip) ((((_hvip) >> VSIP_TO_HVIP_SHIFT) & \ + VSIP_BIAS_MASK) | \ + ((_hvip) & VSIP_NO_BIAS_MASK)) /* AIA CSR bits */ #define TOPI_IID_SHIFT 16 diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c index b749c01742ee..bfa64ae6c94d 100644 --- a/tools/lib/bpf/libbpf.c +++ b/tools/lib/bpf/libbpf.c @@ -6206,6 +6206,13 @@ bpf_object__relocate_core(struct bpf_object *obj, const char *targ_btf_path) return -EINVAL; insn = &prog->insns[insn_idx]; + if (is_ldimm64_insn(insn) && (size_t)insn_idx + 1 >= prog->insns_cnt) { + pr_warn("prog '%s': relo #%d: insn #%d (LDIMM64) is truncated\n", + prog->name, i, insn_idx); + err = -EINVAL; + goto out; + } + err = record_relo_core(prog, rec, insn_idx); if (err) { pr_warn("prog '%s': relo #%d: failed to record relocation: %s\n", diff --git a/tools/lib/bpf/relo_core.c b/tools/lib/bpf/relo_core.c index 8ad2715721cf..2672623a4198 100644 --- a/tools/lib/bpf/relo_core.c +++ b/tools/lib/bpf/relo_core.c @@ -980,23 +980,30 @@ done: } /* - * Turn instruction for which CO_RE relocation failed into invalid one with + * Turn instruction for which CO-RE relocation failed into invalid one with * distinct signature. */ -static void bpf_core_poison_insn(const char *prog_name, int relo_idx, - int insn_idx, struct bpf_insn *insn) +static int bpf_core_poison_insn(const char *prog_name, int relo_idx, + struct bpf_insn *insn, int insn_idx) { - pr_debug("prog '%s': relo #%d: substituting insn #%d w/ invalid insn\n", - prog_name, relo_idx, insn_idx); - insn->code = BPF_JMP | BPF_CALL; - insn->dst_reg = 0; - insn->src_reg = 0; - insn->off = 0; - /* if this instruction is reachable (not a dead code), - * verifier will complain with the following message: - * invalid func unknown#195896080 - */ - insn->imm = 195896080; /* => 0xbad2310 => "bad relo" */ + int insn_cnt = is_ldimm64_insn(insn) ? 2 : 1; + int i; + + for (i = 0; i < insn_cnt; i++) { + pr_debug("prog '%s': relo #%d: substituting insn #%d w/ invalid insn\n", + prog_name, relo_idx, insn_idx + i); + insn[i].code = BPF_JMP | BPF_CALL; + insn[i].dst_reg = 0; + insn[i].src_reg = 0; + insn[i].off = 0; + /* + * If this instruction is reachable (not dead code), the verifier + * will complain with "invalid func unknown#195896080". + */ + insn[i].imm = 195896080; /* => 0xbad2310 => "bad relo" */ + } + + return 0; } static int insn_bpf_size_to_bytes(struct bpf_insn *insn) @@ -1047,17 +1054,6 @@ int bpf_core_patch_insn(const char *prog_name, struct bpf_insn *insn, class = BPF_CLASS(insn->code); - if (res->poison) { -poison: - /* poison second part of ldimm64 to avoid confusing error from - * verifier about "unknown opcode 00" - */ - if (is_ldimm64_insn(insn)) - bpf_core_poison_insn(prog_name, relo_idx, insn_idx + 1, insn + 1); - bpf_core_poison_insn(prog_name, relo_idx, insn_idx, insn); - return 0; - } - orig_val = res->orig_val; new_val = res->new_val; @@ -1065,7 +1061,9 @@ poison: case BPF_ALU: case BPF_ALU64: if (BPF_SRC(insn->code) != BPF_K) - return -EINVAL; + goto bad_insn; + if (res->poison) + return bpf_core_poison_insn(prog_name, relo_idx, insn, insn_idx); if (res->validate && insn->imm != orig_val) { pr_warn("prog '%s': relo #%d: unexpected insn #%d (ALU/ALU64) value: got %d, exp %llu -> %llu\n", prog_name, relo_idx, @@ -1082,6 +1080,8 @@ poison: case BPF_LDX: case BPF_ST: case BPF_STX: + if (res->poison) + return bpf_core_poison_insn(prog_name, relo_idx, insn, insn_idx); if (res->validate && insn->off != orig_val) { pr_warn("prog '%s': relo #%d: unexpected insn #%d (LDX/ST/STX) value: got %d, exp %llu -> %llu\n", prog_name, relo_idx, insn_idx, insn->off, (unsigned long long)orig_val, @@ -1097,7 +1097,7 @@ poison: pr_warn("prog '%s': relo #%d: insn #%d (LDX/ST/STX) accesses field incorrectly. " "Make sure you are accessing pointers, unsigned integers, or fields of matching type and size.\n", prog_name, relo_idx, insn_idx); - goto poison; + return bpf_core_poison_insn(prog_name, relo_idx, insn, insn_idx); } orig_val = insn->off; @@ -1140,6 +1140,9 @@ poison: return -EINVAL; } + if (res->poison) + return bpf_core_poison_insn(prog_name, relo_idx, insn, insn_idx); + imm = (__u32)insn[0].imm | ((__u64)insn[1].imm << 32); if (res->validate && imm != orig_val) { pr_warn("prog '%s': relo #%d: unexpected insn #%d (LDIMM64) value: got %llu, exp %llu -> %llu\n", @@ -1157,6 +1160,7 @@ poison: break; } default: +bad_insn: pr_warn("prog '%s': relo #%d: trying to relocate unrecognized insn #%d, code:0x%x, src:0x%x, dst:0x%x, off:0x%x, imm:0x%x\n", prog_name, relo_idx, insn_idx, insn->code, (unsigned)insn->src_reg, (unsigned)insn->dst_reg, (unsigned)insn->off, (unsigned)insn->imm); diff --git a/tools/objtool/Makefile b/tools/objtool/Makefile index a4484fd22a96..4cc2e756af84 100644 --- a/tools/objtool/Makefile +++ b/tools/objtool/Makefile @@ -89,9 +89,11 @@ LIBOPCODES_LIBS := $(shell \ "-lopcodes -lbfd" \ "-lopcodes -lbfd -liberty" \ "-lopcodes -lbfd -liberty -lz"; do \ - echo 'extern void disassemble_init_for_target(void *);' \ - 'int main(void) { disassemble_init_for_target(0); return 0; }' | \ - $(HOSTCC) -xc - -o /dev/null $$libs 2>/dev/null && \ + printf '%s\n' \ + '$(pound)include <bfd.h>' \ + '$(pound)include <dis-asm.h>' \ + 'int main(void) { disassemble_init_for_target(0); return 0; }' | \ + $(HOSTCC) $(HOSTCFLAGS) -DPACKAGE='"objtool"' -xc - -o /dev/null $$libs 2>/dev/null && \ echo "$$libs" && break; \ done) diff --git a/tools/sched_ext/include/scx/common.bpf.h b/tools/sched_ext/include/scx/common.bpf.h index 76f5e025e107..22f24ebef8a9 100644 --- a/tools/sched_ext/include/scx/common.bpf.h +++ b/tools/sched_ext/include/scx/common.bpf.h @@ -106,13 +106,14 @@ u64 scx_bpf_now(void) __ksym __weak; void scx_bpf_events(struct scx_event_stats *events, size_t events__sz) __ksym __weak; s32 scx_bpf_cpu_to_cid(s32 cpu) __ksym __weak; s32 scx_bpf_cid_to_cpu(s32 cid) __ksym __weak; -void scx_bpf_cid_topo(s32 cid, struct scx_cid_topo *out) __ksym __weak; +void scx_bpf_cid_topo(s32 cid, struct scx_cid_topo *out, size_t out__sz) __ksym __weak; void scx_bpf_kick_cid(s32 cid, u64 flags) __ksym __weak; s32 scx_bpf_task_cid(const struct task_struct *p) __ksym __weak; s32 scx_bpf_this_cid(void) __ksym __weak; struct task_struct *scx_bpf_cid_curr(s32 cid) __ksym __weak; u32 scx_bpf_nr_cids(void) __ksym __weak; u32 scx_bpf_nr_online_cids(void) __ksym __weak; +const void __arena *scx_bpf_online_cmask(void) __ksym __weak; u32 scx_bpf_cidperf_cap(s32 cid) __ksym __weak; u32 scx_bpf_cidperf_cur(s32 cid) __ksym __weak; s32 scx_bpf_cidperf_set(s32 cid, u32 perf) __ksym __weak; diff --git a/tools/sched_ext/scx_qmap.bpf.c b/tools/sched_ext/scx_qmap.bpf.c index 9f6e61d7ca07..2f3653199842 100644 --- a/tools/sched_ext/scx_qmap.bpf.c +++ b/tools/sched_ext/scx_qmap.bpf.c @@ -24,6 +24,9 @@ * time-share that stays self-local. * self - The excl cpus the node kept for itself, plus all of held_shared. * owner - Who holds a cid - a child slot, CID_SELF, or CID_NONE. + * avail - Cpus whose caps are in effect, per ops.sub_ecaps_updated(). + * usable - self AND avail. Placement decisions use this: self is the + * delegation split and can run ahead of what the cpus honor. * * The scheduler splits its held-excl cpus among self and the children in * proportion to each node's cpu.weight, handing each the floor of its share as @@ -208,8 +211,8 @@ static int qmap_spin_lock(struct bpf_res_spin_lock *lock) } /* - * Try prev_cid, then scan cpus_allowed AND idle_cids AND self_cids round-robin - * from prev_cid + 1. Atomic claim retries on race; bounded by + * Try prev_cid, then scan cpus_allowed AND idle_cids AND usable_cids + * round-robin from prev_cid + 1. Atomic claim retries on race; bounded by * IDLE_PICK_RETRIES to keep the verifier's insn budget in check. */ #define IDLE_PICK_RETRIES 16 @@ -221,7 +224,7 @@ static s32 pick_direct_dispatch_cid(struct task_struct *p, s32 prev_cid, s32 cid; u32 i; - if (cmask_test(prev_cid, &qa.self_cids.mask) && + if (cmask_test(prev_cid, &qa.usable_cids.mask) && cmask_test_and_clear(prev_cid, &qa.idle_cids.mask)) return prev_cid; @@ -229,7 +232,7 @@ static s32 pick_direct_dispatch_cid(struct task_struct *p, s32 prev_cid, bpf_for(i, 0, IDLE_PICK_RETRIES) { cid = cmask_next_and2_set_wrap(&taskc->cpus_allowed, &qa.idle_cids.mask, - &qa.self_cids.mask, cid + 1); + &qa.usable_cids.mask, cid + 1); barrier_var(cid); if (cid >= nr_cids) return -1; @@ -358,8 +361,8 @@ s32 BPF_STRUCT_OPS(qmap_select_cid, struct task_struct *p, } /* - * A received time-shared cid is held ENQ_IMMED-only, so inserts must set - * SCX_ENQ_IMMED. + * A received time-shared cid is held ENQ_IMMED-only, so inserts meant to run + * there must set SCX_ENQ_IMMED. */ static u64 needs_immed(s32 cid) { @@ -444,9 +447,11 @@ void BPF_STRUCT_OPS(qmap_enqueue, struct task_struct *p, u64 enq_flags) * didn't grant them or we delegated them to children - would starve in * SHARED/FIFO since we only pull from those on self cids. * - * Force it onto its first allowed cid's local DSQ. If we hold that cid - * it runs. Otherwise the insert carries SCX_ENQ_RESCUE and the kernel - * diverts the task to its rescue path. + * Force it onto its first allowed cid's local DSQ with SCX_ENQ_RESCUE. + * If we hold ENQ on that cid it runs. Otherwise the kernel diverts the + * task to its rescue path. IMMED would turn the insert into a legal + * placement on a time-shared cid and the kernel would bounce it back + * here instead of rescuing it. */ if (!cmask_intersects(&taskc->cpus_allowed, &qa.self_cids.mask)) { s32 c = cmask_next_set_wrap(&taskc->cpus_allowed, 0); @@ -455,7 +460,7 @@ void BPF_STRUCT_OPS(qmap_enqueue, struct task_struct *p, u64 enq_flags) taskc->force_local = false; __sync_fetch_and_add(&qa.nr_rescue_dsp, 1); scx_bpf_dsq_insert(p, SCX_DSQ_LOCAL_ON | c, slice_ns, - enq_flags | needs_immed(c) | SCX_ENQ_RESCUE); + enq_flags | SCX_ENQ_RESCUE); return; } } @@ -540,7 +545,7 @@ void BPF_STRUCT_OPS(qmap_enqueue, struct task_struct *p, u64 enq_flags) scx_bpf_dsq_insert(p, SHARED_DSQ, 0, enq_flags); cid = cmask_next_and2_set_wrap(&taskc->cpus_allowed, &qa.idle_cids.mask, - &qa.self_cids.mask, 0); + &qa.usable_cids.mask, 0); if (cid < scx_bpf_nr_cids()) scx_bpf_kick_cid(cid, SCX_KICK_IDLE); return; @@ -618,7 +623,7 @@ static bool scan_shared_dsq(bool from_timer) if (c >= 0 && c < scx_bpf_nr_cids()) { __sync_fetch_and_add(&qa.nr_rescue_dsp, 1); scx_bpf_dsq_move(BPF_FOR_EACH_ITER, p, SCX_DSQ_LOCAL_ON | c, - needs_immed(c) | SCX_ENQ_RESCUE); + SCX_ENQ_RESCUE); } continue; } @@ -644,22 +649,27 @@ static bool scan_shared_dsq(bool from_timer) if (!(taskc = lookup_task_ctx(p))) return false; - /* only run highpri tasks on cids this node holds, not delegated ones */ + /* only run highpri tasks on cids this node can use right now */ if (cmask_test(this_cid, &taskc->cpus_allowed) && - cmask_test(this_cid, &qa.self_cids.mask)) + cmask_test(this_cid, &qa.usable_cids.mask)) cid = this_cid; else cid = cmask_next_and_set_wrap(&taskc->cpus_allowed, - &qa.self_cids.mask, + &qa.usable_cids.mask, this_cid + 1); if (cid >= nr_cids) { - /* stranded after the cull - rescue it from here */ - s32 c = cmask_next_set_wrap(&taskc->cpus_allowed, 0); + s32 c; + + /* self cids lack caps in effect yet, leave it queued */ + if (cmask_intersects(&taskc->cpus_allowed, &qa.self_cids.mask)) + continue; + /* stranded after the cull - rescue it from here */ + c = cmask_next_set_wrap(&taskc->cpus_allowed, 0); if (c >= 0 && c < nr_cids) { __sync_fetch_and_add(&qa.nr_rescue_dsp, 1); scx_bpf_dsq_move(BPF_FOR_EACH_ITER, p, SCX_DSQ_LOCAL_ON | c, - needs_immed(c) | SCX_ENQ_RESCUE); + SCX_ENQ_RESCUE); } continue; } @@ -808,10 +818,10 @@ void BPF_STRUCT_OPS(qmap_dispatch, s32 cid, struct task_struct *prev) batch--; cpuc->dsp_cnt--; if (!batch || !scx_bpf_dispatch_nr_slots()) { - if (scan_shared_dsq(false)) + if (scan_shared_dsq(false) || + scx_bpf_dsq_move_to_local(SHARED_DSQ, needs_immed(cid))) return; - scx_bpf_dsq_move_to_local(SHARED_DSQ, needs_immed(cid)); - return; + goto prev; } if (!cpuc->dsp_cnt) break; @@ -822,10 +832,14 @@ void BPF_STRUCT_OPS(qmap_dispatch, s32 cid, struct task_struct *prev) if (scan_shared_dsq(false)) return; - +prev: /* * No other tasks. @prev will keep running. Update its core_sched_seq as * if the task were enqueued and dispatched immediately. + * + * No @prev to keep running means the CPU goes idle. If its claim was + * never used, that is not a transition and ops.update_idle() stays + * silent. Restore the claim here. */ if (prev) { taskc = lookup_task_ctx(prev); @@ -834,6 +848,8 @@ void BPF_STRUCT_OPS(qmap_dispatch, s32 cid, struct task_struct *prev) taskc->core_sched_seq = qa.core_sched_tail_seqs[weight_to_idx(prev->scx.weight)]++; + } else { + cmask_set(cid, &qa.idle_cids.mask); } } @@ -945,9 +961,6 @@ s32 BPF_STRUCT_OPS_SLEEPABLE(qmap_init_task, struct task_struct *p, taskc->highpri = false; taskc->core_sched_seq = 0; cmask_init(&taskc->cpus_allowed, 0, scx_bpf_nr_cids()); - bpf_rcu_read_lock(); - cmask_from_cpumask(&taskc->cpus_allowed, p->cpus_ptr); - bpf_rcu_read_unlock(); v = bpf_task_storage_get(&task_ctx_stor, p, NULL, BPF_LOCAL_STORAGE_GET_F_CREATE); @@ -1113,7 +1126,7 @@ void BPF_STRUCT_OPS(qmap_update_idle, s32 cid, bool idle) /* * The kernel delivers update_idle() for every cid this node holds * SCX_CAP_BASE on. Track every cid's idle state regardless of - * delegation: the direct-dispatch pick masks idle_cids with self_cids + * delegation: the direct-dispatch pick masks idle_cids with usable_cids * at selection, so a cid already idle when it returns to self needs no * reseed here. */ @@ -1285,11 +1298,16 @@ struct { __type(value, struct round_robin_timer); } round_robin_timer SEC(".maps"); +enum part_pending_flags { + PART_REFRESH = BIT_U64(0), + PART_REDISTRIBUTE = BIT_U64(1), +}; + /* * Partition update synchronization. qa.part can be written from concurrent * contexts. This single-runner guard admits one writer at a time without * holding a lock across the grant/revoke kfuncs. part_pending coalesces - * repartition requests that arrive while it is held. + * refresh and repartition requests that arrive while it is held. * * They live in .bss, not the arena: rr_advance() runs from a bpf_timer * callback, where the verifier rejects atomic ops on arena memory. @@ -1538,6 +1556,19 @@ static __noinline void account_alloc(void) } /* + * usable_cids = self_cids & avail_cids. The inputs have separate writers, + * apply_partition() and qmap_sub_ecaps_updated(), so the result is rebuilt in + * full under the partition guard, in scratch first so that readers never see + * self_cids alone. + */ +static void refresh_usable(void) +{ + cmask_copy(&qa.usable_scratch.mask, &qa.self_cids.mask); + cmask_and(&qa.usable_scratch.mask, &qa.avail_cids.mask); + cmask_copy(&qa.usable_cids.mask, &qa.usable_scratch.mask); +} + +/* * apply_partition - execute the plan compute_partition() built * * Turn the owner map into the per-child, shared and self cmasks and issue the @@ -1559,6 +1590,7 @@ __noinline void apply_partition(void) /* no excl cpu: run own tasks on the held shares, evict children */ if (!qa.part.nr_excl) { cmask_copy(&qa.self_cids.mask, &qa.held_shared.mask); + refresh_usable(); bpf_for(i, 0, MAX_SUB_SCHEDS) if (qa.sub_sched_ctxs[i].cgroup_id) scx_bpf_sub_kill(qa.sub_sched_ctxs[i].cgroup_id, @@ -1596,6 +1628,7 @@ __noinline void apply_partition(void) else if (o == CID_SELF) cmask_set(cid, &qa.self_cids.mask); } + refresh_usable(); /* * Apply each child's exclusive cids as a delta against its previous @@ -1643,33 +1676,46 @@ __noinline void apply_partition(void) } } -/* - * Recompute the split off the node's held caps and apply it. The contexts this - * runs from (the sub-sched and cgroup callbacks, the rr timer) are not - * serialized by the kernel, so a single runner does the work. A caller that - * finds the guard held leaves part_pending set; the holder drains it before - * releasing, with the rr timer as a backstop. +/** + * execute_partition - Run pending partition updates + * + * The rr timer is the backstop if the loop reaches its iteration limit. */ -static void redistribute(void) +static void execute_partition(void) { + u64 pending; s32 i; - __sync_fetch_and_or(&part_pending, 1); + bpf_for(i, 0, 1024) { + if (!part_try_start()) + break; - if (!part_try_start()) - return; + pending = __sync_fetch_and_and(&part_pending, 0); + if (pending & PART_REDISTRIBUTE) { + /* charge elapsed time before repartitioning */ + account_alloc(); + compute_partition(); + apply_partition(); + } else if (pending & PART_REFRESH) { + refresh_usable(); + } - bpf_for(i, 0, 1024) { - __sync_fetch_and_and(&part_pending, 0); - /* charge elapsed time to the current partition before rebuilding it */ - account_alloc(); - compute_partition(); - apply_partition(); + /* + * Requests are published before trying the guard. Releasing it + * before checking pending work ensures a racing request is + * either observed here or handled by a caller that acquires the + * guard. + */ + part_end(); if (!__sync_fetch_and_or(&part_pending, 0)) break; } +} - part_end(); +static void redistribute(void) +{ + __sync_fetch_and_or(&part_pending, PART_REDISTRIBUTE); + execute_partition(); } /* @@ -1683,6 +1729,7 @@ int flush_alloc(void *ctx) if (part_try_start()) { account_alloc(); part_end(); + execute_partition(); } return 0; } @@ -1740,9 +1787,7 @@ static void rr_advance(void) part_end(); - /* a resplit queued while we held the guard supersedes this rotation */ - if (__sync_fetch_and_or(&part_pending, 0)) - redistribute(); + execute_partition(); } /* advance the time-shared cid pool every round_robin_ns */ @@ -1837,8 +1882,11 @@ s32 BPF_STRUCT_OPS_SLEEPABLE(qmap_init) cmask_init(&qa.rr_cids.mask, 0, nr_cids); cmask_init(&qa.prev_rr_cids.mask, 0, nr_cids); cmask_init(&qa.self_cids.mask, 0, nr_cids); + cmask_init(&qa.avail_cids.mask, 0, nr_cids); + cmask_init(&qa.usable_cids.mask, 0, nr_cids); cmask_init(&qa.to_revoke_cids.mask, 0, nr_cids); cmask_init(&qa.to_grant_cids.mask, 0, nr_cids); + cmask_init(&qa.usable_scratch.mask, 0, nr_cids); cmask_init(&qa.held_excl.mask, 0, nr_cids); cmask_init(&qa.held_shared.mask, 0, nr_cids); @@ -1852,14 +1900,16 @@ s32 BPF_STRUCT_OPS_SLEEPABLE(qmap_init) } /* - * The root starts holding every cid. qmap_sub_ecaps_updated() maintains - * per-cid shared state as effective caps settle, and redistribute() - * rebuilds owner and self from held caps. A non-root node starts with - * nothing. + * The root starts holding every cid and gets no ecaps notifications, so + * its avail set is fixed here. qmap_sub_ecaps_updated() maintains the + * per-cid state as effective caps settle, and redistribute() rebuilds + * owner and self from held caps. A non-root node starts with nothing. */ bpf_for(i, 0, nr_cids) { if (!sub_cgroup_id) { cmask_set(i, &qa.self_cids.mask); + cmask_set(i, &qa.avail_cids.mask); + cmask_set(i, &qa.usable_cids.mask); qa.part.cid_owner[i] = CID_SELF; } else { qa.part.cid_owner[i] = CID_NONE; @@ -2000,12 +2050,19 @@ void BPF_STRUCT_OPS(qmap_sub_ecaps_updated, s32 cid, u64 before, u64 after) { /* * Effective caps updated. Track which cids hold shared caps so a self - * task placed there enqueues IMMED. + * task placed there enqueues IMMED, and which cids have ENQ_IMMED in + * effect at all (avail, see the header comment). */ - if (after & SCX_CAP_ENQ_IMMED) + if (after & SCX_CAP_ENQ_IMMED) { qa.cid_shared[cid] = (after & SCX_CAP_ENQ) ? 0 : 1; - else + cmask_set(cid, &qa.avail_cids.mask); + } else { qa.cid_shared[cid] = 0; + cmask_clear(cid, &qa.avail_cids.mask); + } + + __sync_fetch_and_or(&part_pending, PART_REFRESH); + execute_partition(); } SCX_OPS_CID_DEFINE(qmap_ops, diff --git a/tools/sched_ext/scx_qmap.h b/tools/sched_ext/scx_qmap.h index c78d61806b39..e95fffcf7b23 100644 --- a/tools/sched_ext/scx_qmap.h +++ b/tools/sched_ext/scx_qmap.h @@ -165,12 +165,15 @@ struct qmap_arena { /* bpf-internal cmasks (embedded, see struct qmap_cmask) */ struct qmap_cmask self_cids; /* cids this node runs its own tasks on */ + struct qmap_cmask avail_cids; /* cids with caps in effect on the cpu */ + struct qmap_cmask usable_cids; /* self_cids & avail_cids, placeable right now */ struct qmap_cmask idle_cids; /* idle state of all cids regardless of delegation */ struct qmap_cmask rr_cids; /* the shared pool, as a mask for grant/revoke */ /* scratch cmasks */ struct qmap_cmask to_revoke_cids; /* delta cids to revoke */ struct qmap_cmask to_grant_cids; /* delta cids to grant */ + struct qmap_cmask usable_scratch; /* refresh_usable() build area */ struct qmap_cmask prev_rr_cids; /* previous shared pool, to clear stale grants */ struct qmap_cmask held_excl; /* cids held excl (ENQ): delegatable */ struct qmap_cmask held_shared; /* cids held shared (ENQ_IMMED only): self-local */ diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile index 2d960626750e..bc4fdb6f5337 100644 --- a/tools/testing/selftests/Makefile +++ b/tools/testing/selftests/Makefile @@ -35,18 +35,24 @@ TARGETS += fchmodat2 TARGETS += filesystems TARGETS += filesystems/binderfs TARGETS += filesystems/epoll +TARGETS += filesystems/eventfd TARGETS += filesystems/failfs TARGETS += filesystems/fat +TARGETS += filesystems/openat2 +TARGETS += filesystems/open_tree_ns TARGETS += filesystems/overlayfs TARGETS += filesystems/statmount TARGETS += filesystems/mount-notify +TARGETS += filesystems/mount_cycle TARGETS += filesystems/nsfs TARGETS += filesystems/fuse TARGETS += filesystems/move_mount TARGETS += filesystems/empty_mntns TARGETS += filesystems/fsmount_ns TARGETS += filesystems/fscontext_ns -TARGETS += filesystems/mntns_cleanup +TARGETS += filesystems/xattr +TARGETS += filesystems/mntns_unbindable +TARGETS += filesystems/umount_propagation TARGETS += firmware TARGETS += fpu TARGETS += ftrace @@ -103,7 +109,6 @@ TARGETS += prctl TARGETS += proc TARGETS += pstore TARGETS += ptrace -TARGETS += openat2 TARGETS += rdma TARGETS += resctrl TARGETS += riscv diff --git a/tools/testing/selftests/arm64/mte/check_gcr_el1_cswitch.c b/tools/testing/selftests/arm64/mte/check_gcr_el1_cswitch.c index d23f154d3288..5d9dc8bcfbf5 100644 --- a/tools/testing/selftests/arm64/mte/check_gcr_el1_cswitch.c +++ b/tools/testing/selftests/arm64/mte/check_gcr_el1_cswitch.c @@ -69,7 +69,7 @@ fail: int execute_test(pid_t pid) { pthread_t thread_id[MAX_THREADS]; - int thread_data[MAX_THREADS]; + intptr_t thread_data[MAX_THREADS]; for (int i = 0; i < MAX_THREADS; i++) pthread_create(&thread_id[i], NULL, diff --git a/tools/testing/selftests/bpf/prog_tests/bpf_ma_ttrace.c b/tools/testing/selftests/bpf/prog_tests/bpf_ma_ttrace.c new file mode 100644 index 000000000000..a1d41b109940 --- /dev/null +++ b/tools/testing/selftests/bpf/prog_tests/bpf_ma_ttrace.c @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: GPL-2.0 +#include <test_progs.h> +#include "bpf_ma_ttrace.skel.h" + +#define NR_ELEMS 4096 + +/* + * The first free_bulk() starts RCU tasks trace GP. The rest of the elements are + * deleted while it's in flight. They should be freed without further alloc or + * free from this map. + */ +void test_bpf_ma_ttrace(void) +{ + LIBBPF_OPTS(bpf_test_run_opts, opts); + struct bpf_ma_ttrace *skel; + __u32 cnt = NR_ELEMS; + long *vals = NULL; + int *keys = NULL; + int i, err, fd, nr_cpus; + + skel = bpf_ma_ttrace__open_and_load(); + if (!ASSERT_OK_PTR(skel, "open_and_load")) + return; + nr_cpus = libbpf_num_possible_cpus(); + if (!ASSERT_GT(nr_cpus, 0, "nr_cpus")) + goto out; + skel->bss->nr_cpus = nr_cpus; + + keys = calloc(NR_ELEMS, sizeof(*keys)); + vals = calloc(NR_ELEMS, sizeof(*vals)); + if (!ASSERT_OK_PTR(keys, "keys") || !ASSERT_OK_PTR(vals, "vals")) + goto out; + for (i = 0; i < NR_ELEMS; i++) + keys[i] = i; + + fd = bpf_map__fd(skel->maps.htab); + err = bpf_map_update_batch(fd, keys, vals, &cnt, NULL); + if (!ASSERT_OK(err, "update_batch") || !ASSERT_EQ(cnt, NR_ELEMS, "update_cnt")) + goto out; + err = bpf_map_delete_batch(fd, keys, &cnt, NULL); + if (!ASSERT_OK(err, "delete_batch") || !ASSERT_EQ(cnt, NR_ELEMS, "delete_cnt")) + goto out; + + /* Wait for all __free_rcu() callbacks to finish */ + for (i = 0; i < 300; i++) { + err = bpf_prog_test_run_opts(bpf_program__fd(skel->progs.check_ttrace), &opts); + if (!ASSERT_OK(err, "test_run") || !ASSERT_OK(opts.retval, "retval")) + goto out; + if (!skel->bss->in_progress) + break; + usleep(100000); + } + ASSERT_EQ(skel->bss->nr_caches, nr_cpus, "nr_caches"); + ASSERT_EQ(skel->bss->in_progress, 0, "in_progress"); + ASSERT_EQ(skel->bss->not_freed, 0, "not_freed"); +out: + free(keys); + free(vals); + bpf_ma_ttrace__destroy(skel); +} diff --git a/tools/testing/selftests/bpf/prog_tests/bpf_mod_race.c b/tools/testing/selftests/bpf/prog_tests/bpf_mod_race.c index ecc3d47919ad..f8497e764beb 100644 --- a/tools/testing/selftests/bpf/prog_tests/bpf_mod_race.c +++ b/tools/testing/selftests/bpf/prog_tests/bpf_mod_race.c @@ -55,38 +55,6 @@ static void *load_module_thread(void *p) return p; } -static int sys_userfaultfd(int flags) -{ - return syscall(__NR_userfaultfd, flags); -} - -static int test_setup_uffd(void *fault_addr) -{ - struct uffdio_register uffd_register = {}; - struct uffdio_api uffd_api = {}; - int uffd; - - uffd = sys_userfaultfd(O_CLOEXEC); - if (uffd < 0) - return -errno; - - uffd_api.api = UFFD_API; - uffd_api.features = 0; - if (ioctl(uffd, UFFDIO_API, &uffd_api)) { - close(uffd); - return -1; - } - - uffd_register.range.start = (unsigned long)fault_addr; - uffd_register.range.len = getpagesize(); - uffd_register.mode = UFFDIO_REGISTER_MODE_MISSING; - if (ioctl(uffd, UFFDIO_REGISTER, &uffd_register)) { - close(uffd); - return -1; - } - return uffd; -} - static void test_bpf_mod_race_config(const struct test_config *config) { void *fault_addr, *skel_fail; @@ -117,7 +85,7 @@ static void test_bpf_mod_race_config(const struct test_config *config) if (!ASSERT_OK(bpf_mod_race__attach(skel), "bpf_mod_kfunc_race__attach")) goto end_destroy; - uffd = test_setup_uffd(fault_addr); + uffd = uffd_block_page(fault_addr); if (!ASSERT_GE(uffd, 0, "userfaultfd open + register address")) goto end_destroy; diff --git a/tools/testing/selftests/bpf/prog_tests/cb_refs.c b/tools/testing/selftests/bpf/prog_tests/cb_refs.c index 78566b817fd7..490e15e7126d 100644 --- a/tools/testing/selftests/bpf/prog_tests/cb_refs.c +++ b/tools/testing/selftests/bpf/prog_tests/cb_refs.c @@ -13,7 +13,7 @@ struct { } cb_refs_tests[] = { { "underflow_prog", "release kfunc bpf_kfunc_call_test_release expects referenced PTR_TO_BTF_ID passed to R1" }, { "leak_prog", "Possibly NULL pointer passed to helper R2" }, - { "nested_cb", "Unreleased reference id=4 alloc_insn=2" }, /* alloc_insn=2{4,5} */ + { "nested_cb", "Unreleased reference id=5 alloc_insn=2" }, /* alloc_insn=2{4,5} */ { "non_cb_transfer_ref", "Unreleased reference id=4 alloc_insn=1" }, /* alloc_insn=1{1,2} */ }; diff --git a/tools/testing/selftests/bpf/prog_tests/core_reloc_raw.c b/tools/testing/selftests/bpf/prog_tests/core_reloc_raw.c index a18d3680fb16..51f42b02a267 100644 --- a/tools/testing/selftests/bpf/prog_tests/core_reloc_raw.c +++ b/tools/testing/selftests/bpf/prog_tests/core_reloc_raw.c @@ -14,6 +14,197 @@ static char log[16 * 1024]; +static int load_core_relo_insns(int btf_fd, struct bpf_insn *insns, int insn_cnt, + struct bpf_func_info *funcs, int func_cnt, + int enum_id, int access_str_off, int insn_idx, + bool relocate) +{ + struct bpf_core_relo relo = { + .insn_off = insn_idx * sizeof(struct bpf_insn), + .type_id = enum_id, + .access_str_off = access_str_off, + .kind = BPF_CORE_ENUMVAL_VALUE, + }; + union bpf_attr attr = { + .prog_type = BPF_PROG_TYPE_SOCKET_FILTER, + .insn_cnt = insn_cnt, + .insns = (__u64)insns, + .license = (__u64)"GPL", + .log_buf = (__u64)log, + .log_size = sizeof(log), + .log_level = 2, + .prog_btf_fd = btf_fd, + .func_info_rec_size = sizeof(struct bpf_func_info), + .func_info = (__u64)funcs, + .func_info_cnt = func_cnt, + }; + + if (relocate) { + attr.core_relo_cnt = 1; + attr.core_relos = (__u64)&relo; + attr.core_relo_rec_size = sizeof(relo); + } + memset(log, 0, sizeof(log)); + return sys_bpf_prog_load(&attr, sizeof(attr), 1); +} + +static void test_early_core_relo(void) +{ + static const char unrecognized[] = "trying to relocate unrecognized insn #2"; + static const struct { + const char *name; + struct bpf_insn insns[2]; + const char *err_msg; + } tests[] = { + { "poison_exit", { BPF_EXIT_INSN() }, unrecognized }, + { "poison_ja", { BPF_JMP_A(1) }, unrecognized }, + { "poison_jmp", { BPF_JMP_IMM(BPF_JEQ, BPF_REG_0, 0, 1) }, unrecognized }, + { "poison_jmp32", { BPF_JMP32_IMM(BPF_JEQ, BPF_REG_0, 0, 1) }, unrecognized }, + { "poison_call", { BPF_EMIT_CALL(BPF_FUNC_get_prandom_u32) }, unrecognized }, + { "poison_alu_reg", { BPF_MOV32_REG(BPF_REG_0, BPF_REG_1) }, unrecognized }, + { "poison_alu64_reg", { BPF_MOV64_REG(BPF_REG_0, BPF_REG_1) }, unrecognized }, + { "poison_ld_abs", { BPF_LD_ABS(BPF_W, 0) }, + "insn #2 (LDIMM64) has unexpected form" }, + { "poison_alu_imm", { BPF_MOV32_IMM(BPF_REG_0, 0) } }, + { "poison_alu64_imm", { BPF_MOV64_IMM(BPF_REG_0, 0) } }, + { "poison_ldx", { BPF_LDX_MEM(BPF_W, BPF_REG_0, BPF_REG_1, 0) } }, + { "poison_st", { BPF_ST_MEM(BPF_W, BPF_REG_10, -4, 0) } }, + { "poison_stx", { BPF_STX_MEM(BPF_W, BPF_REG_10, BPF_REG_0, -4) } }, + { "poison_ldimm64", { BPF_LD_IMM64(BPF_REG_0, 0) } }, + }; + struct test_btf { + struct btf_header hdr; + __u32 types[18]; + char strings[64]; + } raw_btf = { + .hdr = { + .magic = BTF_MAGIC, + .version = BTF_VERSION, + .hdr_len = sizeof(struct btf_header), + .type_off = 0, + .type_len = sizeof(raw_btf.types), + .str_off = offsetof(struct test_btf, strings) - + offsetof(struct test_btf, types), + .str_len = sizeof(raw_btf.strings), + }, + .types = { + BTF_TYPE_INT_ENC(1, BTF_INT_SIGNED, 0, 32, 4), /* [1] int */ + BTF_FUNC_PROTO_ENC(1, 0), /* [2] int (*)(void) */ + BTF_FUNC_ENC(5, 2), /* [3] main_fn */ + BTF_FUNC_ENC(13, 2), /* [4] sub_fn */ + BTF_TYPE_ENC(20, BTF_INFO_ENC(BTF_KIND_ENUM, 0, 1), 4), /* [5] enum */ + BTF_ENUM_ENC(45, 0), /* value = 0 */ + }, + .strings = "\0int\0main_fn\0sub_fn\0core_relo_poison_missing\0value\0" "0", + }; + struct bpf_func_info funcs[] = { + { .insn_off = 0, .type_id = 3 }, + { .insn_off = 3, .type_id = 4 }, + }; + struct bpf_insn core_only[] = { + BPF_MOV64_IMM(BPF_REG_0, 0), + BPF_JMP_IMM(BPF_JEQ, BPF_REG_0, 0, 1), + BPF_MOV64_IMM(BPF_REG_0, 0), + BPF_EXIT_INSN(), + }; + struct bpf_insn subprog[] = { + BPF_CALL_REL(2), + BPF_MOV64_IMM(BPF_REG_0, 0), + BPF_MOV64_IMM(BPF_REG_0, 0), + BPF_MOV64_IMM(BPF_REG_0, 0), + BPF_EXIT_INSN(), + }; + struct bpf_insn truncated_ldimm64[] = { + BPF_RAW_INSN(BPF_LD | BPF_IMM | BPF_DW, 0, 0, 0, 0), + }; + int access_str_off = 51; /* offset of "0" */ + int enum_id = 5; + int btf_fd, prog_fd = -1, i; + + btf_fd = bpf_btf_load(&raw_btf, sizeof(raw_btf), NULL); + if (!ASSERT_GE(btf_fd, 0, "btf_load")) + goto cleanup; + + if (test__start_subtest("without_func_info")) { + prog_fd = load_core_relo_insns(btf_fd, core_only, ARRAY_SIZE(core_only), NULL, 0, + enum_id, access_str_off, 2, false); + if (!ASSERT_GE(prog_fd, 0, "control_load")) + goto cleanup; + close(prog_fd); + prog_fd = load_core_relo_insns(btf_fd, core_only, ARRAY_SIZE(core_only), NULL, 0, + enum_id, access_str_off, 2, true); + if (!ASSERT_GE(prog_fd, 0, "poisoned_load")) + goto cleanup; + ASSERT_HAS_SUBSTR(log, "substituting insn #2", "poison_log"); + close(prog_fd); + prog_fd = -1; + } + + if (test__start_subtest("before_subprog_validation")) { + prog_fd = load_core_relo_insns(btf_fd, subprog, ARRAY_SIZE(subprog), funcs, 2, + enum_id, access_str_off, 2, true); + if (!ASSERT_LT(prog_fd, 0, "poisoned_load")) + goto cleanup; + ASSERT_HAS_SUBSTR(log, "substituting insn #2", "poison_log"); + ASSERT_HAS_SUBSTR(log, "last insn is not an exit or jmp", "poisoned_load_log"); + } + + if (test__start_subtest("truncated_ldimm64")) { + prog_fd = load_core_relo_insns(btf_fd, truncated_ldimm64, + ARRAY_SIZE(truncated_ldimm64), NULL, 0, + enum_id, access_str_off, 0, true); + if (!ASSERT_LT(prog_fd, 0, "truncated_load")) + goto cleanup; + ASSERT_HAS_SUBSTR(log, "invalid bpf_ld_imm64 insn", "truncated_load_log"); + } + + for (i = 0; i < ARRAY_SIZE(tests); i++) { + struct bpf_insn insns[] = { + BPF_MOV64_IMM(BPF_REG_0, 0), + BPF_JMP_IMM(BPF_JEQ, BPF_REG_0, 0, 1), + tests[i].insns[0], + BPF_MOV64_IMM(BPF_REG_0, 0), + BPF_EXIT_INSN(), + }; + bool is_ldimm64 = insns[2].code == (BPF_LD | BPF_DW | BPF_IMM); + + if (!test__start_subtest(tests[i].name)) + continue; + if (is_ldimm64) { + insns[1].off = 2; + insns[3] = tests[i].insns[1]; + } + prog_fd = load_core_relo_insns(btf_fd, insns, ARRAY_SIZE(insns), funcs, 1, + enum_id, access_str_off, 2, false); + if (!ASSERT_GE(prog_fd, 0, "control_load")) + goto cleanup; + close(prog_fd); + prog_fd = load_core_relo_insns(btf_fd, insns, ARRAY_SIZE(insns), funcs, 1, + enum_id, access_str_off, 2, true); + if (!tests[i].err_msg) { + ASSERT_GE(prog_fd, 0, "dead_poison_load"); + ASSERT_HAS_SUBSTR(log, "substituting insn #2", "poison_log"); + if (is_ldimm64) + ASSERT_HAS_SUBSTR(log, "substituting insn #3", "poison_ldimm64_log"); + } else { + ASSERT_LT(prog_fd, 0, "invalid_poison_load"); + ASSERT_HAS_SUBSTR(log, tests[i].err_msg, "invalid_poison_log"); + ASSERT_NULL(strstr(log, "substituting insn"), "invalid_poison_substitution"); + } + close(prog_fd); + prog_fd = -1; + } + +cleanup: + if (env.verbosity > VERBOSE_NORMAL && log[0]) { + printf("-------- program load log start --------\n"); + printf("%s", log); + printf("-------- program load log end ----------\n"); + } + close(prog_fd); + close(btf_fd); +} + /* Check that verifier rejects BPF program containing relocation * pointing to non-existent BTF type. */ @@ -120,6 +311,7 @@ out: void test_core_reloc_raw(void) { + test_early_core_relo(); if (test__start_subtest("bad_local_id")) test_bad_local_id(); } diff --git a/tools/testing/selftests/bpf/prog_tests/dynptr.c b/tools/testing/selftests/bpf/prog_tests/dynptr.c index 5fda11590708..4396560365e8 100644 --- a/tools/testing/selftests/bpf/prog_tests/dynptr.c +++ b/tools/testing/selftests/bpf/prog_tests/dynptr.c @@ -9,6 +9,7 @@ enum test_setup_type { SETUP_SYSCALL_SLEEP, SETUP_SKB_PROG, + SETUP_SKB_PROG_NONLINEAR, SETUP_SKB_PROG_TP, SETUP_XDP_PROG, }; @@ -32,6 +33,7 @@ static struct { {"test_ringbuf", SETUP_SYSCALL_SLEEP}, {"test_skb_readonly", SETUP_SKB_PROG}, {"test_dynptr_skb_data", SETUP_SKB_PROG}, + {"test_dynptr_skb_slice_non_linear", SETUP_SKB_PROG_NONLINEAR}, {"test_dynptr_skb_meta_data", SETUP_SKB_PROG}, {"test_dynptr_skb_meta_flags", SETUP_SKB_PROG}, {"test_adjust", SETUP_SYSCALL_SLEEP}, @@ -94,7 +96,9 @@ static void verify_success(const char *prog_name, enum test_setup_type setup_typ bpf_link__destroy(link); break; case SETUP_SKB_PROG: + case SETUP_SKB_PROG_NONLINEAR: { + struct __sk_buff ctx = {}; int prog_fd; char buf[64]; @@ -106,6 +110,12 @@ static void verify_success(const char *prog_name, enum test_setup_type setup_typ .repeat = 1, ); + if (setup_type == SETUP_SKB_PROG_NONLINEAR) { + ctx.data_end = ETH_HLEN + sizeof(struct iphdr); + topts.ctx_in = &ctx; + topts.ctx_size_in = sizeof(ctx); + } + prog_fd = bpf_program__fd(prog); if (!ASSERT_GE(prog_fd, 0, "prog_fd")) goto cleanup; diff --git a/tools/testing/selftests/bpf/prog_tests/exceptions.c b/tools/testing/selftests/bpf/prog_tests/exceptions.c index 3588d6f97fd4..639866ce09a9 100644 --- a/tools/testing/selftests/bpf/prog_tests/exceptions.c +++ b/tools/testing/selftests/bpf/prog_tests/exceptions.c @@ -55,6 +55,7 @@ static void test_exceptions_success(void) RUN_SUCCESS(exception_ext, 0); RUN_SUCCESS(exception_ext_mod_cb_runtime, 35); RUN_SUCCESS(exception_throw_subprog, 1); + RUN_SUCCESS(exception_throw_subprog_stack_cb, 0x1234); RUN_SUCCESS(exception_assert_nz_gfunc, 1); RUN_SUCCESS(exception_assert_zero_gfunc, 1); RUN_SUCCESS(exception_assert_neg_gfunc, 1); diff --git a/tools/testing/selftests/bpf/prog_tests/linked_list.c b/tools/testing/selftests/bpf/prog_tests/linked_list.c index c3d133c6a00d..52fabbee3dd5 100644 --- a/tools/testing/selftests/bpf/prog_tests/linked_list.c +++ b/tools/testing/selftests/bpf/prog_tests/linked_list.c @@ -714,7 +714,7 @@ static void test_btf(void) break; err = btf__load_into_kernel(btf); - ASSERT_EQ(err, -ELOOP, "check btf"); + ASSERT_EQ(err, 0, "check btf"); btf__free(btf); break; } @@ -773,7 +773,7 @@ static void test_btf(void) break; err = btf__load_into_kernel(btf); - ASSERT_EQ(err, -ELOOP, "check btf"); + ASSERT_EQ(err, 0, "check btf"); btf__free(btf); break; } diff --git a/tools/testing/selftests/bpf/prog_tests/local_kptr_ownership.c b/tools/testing/selftests/bpf/prog_tests/local_kptr_ownership.c new file mode 100644 index 000000000000..a487aa68f2ee --- /dev/null +++ b/tools/testing/selftests/bpf/prog_tests/local_kptr_ownership.c @@ -0,0 +1,296 @@ +// SPDX-License-Identifier: GPL-2.0 +/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */ + +#include <bpf/btf.h> +#include <linux/btf.h> +#include <test_progs.h> + +#define SPIN_LOCK 2 +#define LIST_HEAD 3 +#define LIST_NODE 4 +/* Keep in sync with BTF_MAX_OWNERSHIP_DEPTH. */ +#define MAX_OWNERSHIP_DEPTH 8 + +static struct btf *init_btf(void) +{ + struct btf *btf; + int id; + + btf = btf__new_empty(); + if (!ASSERT_OK_PTR(btf, "btf__new_empty")) + return NULL; + id = btf__add_int(btf, "int", 4, BTF_INT_SIGNED); + if (!ASSERT_EQ(id, 1, "btf__add_int")) + goto err_out; + id = btf__add_struct(btf, "bpf_spin_lock", 4); + if (!ASSERT_EQ(id, SPIN_LOCK, "btf__add_struct bpf_spin_lock")) + goto err_out; + id = btf__add_struct(btf, "bpf_list_head", 16); + if (!ASSERT_EQ(id, LIST_HEAD, "btf__add_struct bpf_list_head")) + goto err_out; + id = btf__add_struct(btf, "bpf_list_node", 24); + if (!ASSERT_EQ(id, LIST_NODE, "btf__add_struct bpf_list_node")) + goto err_out; + return btf; + +err_out: + btf__free(btf); + return NULL; +} + +static int add_local_kptr(struct btf *btf, int pointee_id, const char *tag) +{ + int id; + + id = btf__add_type_tag(btf, tag, pointee_id); + if (!ASSERT_GT(id, 0, "btf__add_type_tag")) + return id; + id = btf__add_ptr(btf, id); + ASSERT_GT(id, 0, "btf__add_ptr"); + return id; +} + +static void test_self_cycle(const char *tag, int expected_err) +{ + struct btf *btf; + int id, err; + + btf = init_btf(); + if (!ASSERT_OK_PTR(btf, "init_btf")) + return; + id = add_local_kptr(btf, 7, tag); + if (id <= 0) + goto out; + id = btf__add_struct(btf, "self_cycle", 8); + if (!ASSERT_EQ(id, 7, "btf__add_struct self_cycle")) + goto out; + err = btf__add_field(btf, "next", 6, 0, 0); + if (!ASSERT_OK(err, "btf__add_field self_cycle::next")) + goto out; + + err = btf__load_into_kernel(btf); + ASSERT_EQ(err, expected_err, "check btf"); +out: + btf__free(btf); +} + +static void test_aba_cycle(void) +{ + struct btf *btf; + int id, err; + + btf = init_btf(); + if (!ASSERT_OK_PTR(btf, "init_btf")) + return; + id = add_local_kptr(btf, 10, "kptr"); + if (id <= 0) + goto out; + id = add_local_kptr(btf, 9, "kptr"); + if (id <= 0) + goto out; + id = btf__add_struct(btf, "cycle_a", 8); + if (!ASSERT_EQ(id, 9, "btf__add_struct cycle_a")) + goto out; + err = btf__add_field(btf, "b", 6, 0, 0); + if (!ASSERT_OK(err, "btf__add_field cycle_a::b")) + goto out; + id = btf__add_struct(btf, "cycle_b", 8); + if (!ASSERT_EQ(id, 10, "btf__add_struct cycle_b")) + goto out; + err = btf__add_field(btf, "a", 8, 0, 0); + if (!ASSERT_OK(err, "btf__add_field cycle_b::a")) + goto out; + + err = btf__load_into_kernel(btf); + ASSERT_EQ(err, -ELOOP, "check btf"); +out: + btf__free(btf); +} + +static void test_mixed_cycle(void) +{ + struct btf *btf; + int id, err; + + btf = init_btf(); + if (!ASSERT_OK_PTR(btf, "init_btf")) + return; + id = add_local_kptr(btf, 7, "kptr"); + if (id <= 0) + goto out; + id = btf__add_struct(btf, "mixed_owner", 20); + if (!ASSERT_EQ(id, 7, "btf__add_struct mixed_owner")) + goto out; + err = btf__add_field(btf, "root", LIST_HEAD, 0, 0); + if (!ASSERT_OK(err, "btf__add_field mixed_owner::root")) + goto out; + err = btf__add_field(btf, "lock", SPIN_LOCK, 128, 0); + if (!ASSERT_OK(err, "btf__add_field mixed_owner::lock")) + goto out; + id = btf__add_decl_tag(btf, "contains:mixed_node:node", 7, 0); + if (!ASSERT_EQ(id, 8, "btf__add_decl_tag mixed_owner")) + goto out; + id = btf__add_struct(btf, "mixed_node", 32); + if (!ASSERT_EQ(id, 9, "btf__add_struct mixed_node")) + goto out; + err = btf__add_field(btf, "node", LIST_NODE, 0, 0); + if (!ASSERT_OK(err, "btf__add_field mixed_node::node")) + goto out; + err = btf__add_field(btf, "owner", 6, 192, 0); + if (!ASSERT_OK(err, "btf__add_field mixed_node::owner")) + goto out; + + err = btf__load_into_kernel(btf); + ASSERT_EQ(err, -ELOOP, "check btf"); +out: + btf__free(btf); +} + +static void test_acyclic_depth(int depth, bool child_first, bool shared_suffix, int expected_err) +{ + int ptr_id[MAX_OWNERSHIP_DEPTH + 1]; + int first_struct_id; + struct btf *btf; + int id, err, i, n, pointee_id; + + btf = init_btf(); + if (!ASSERT_OK_PTR(btf, "init_btf")) + return; + first_struct_id = 5 + 2 * depth; + for (i = 0; i < depth; i++) { + if (i == depth - 1) + pointee_id = first_struct_id + depth; + else + pointee_id = first_struct_id + (child_first ? depth - 2 - i : i + 1); + ptr_id[i] = add_local_kptr(btf, pointee_id, "kptr"); + if (ptr_id[i] <= 0) + goto out; + } + for (n = 0; n < depth; n++) { + char name[32]; + int offset = 0; + + i = child_first ? depth - 1 - n : n; + snprintf(name, sizeof(name), "owner_%d", i); + id = btf__add_struct(btf, name, shared_suffix && !i ? 16 : 8); + if (!ASSERT_EQ(id, first_struct_id + n, "btf__add_struct owner")) + goto out; + if (shared_suffix && !i) { + /* + * Visit the shared suffix through the shorter path before + * reaching it again with less remaining depth. + */ + err = btf__add_field(btf, "suffix", ptr_id[1], 0, 0); + if (!ASSERT_OK(err, "btf__add_field owner::suffix")) + goto out; + offset = 64; + } + err = btf__add_field(btf, "next", ptr_id[i], offset, 0); + if (!ASSERT_OK(err, "btf__add_field owner::next")) + goto out; + } + id = btf__add_struct(btf, "plain_leaf", 4); + if (!ASSERT_EQ(id, first_struct_id + depth, "btf__add_struct plain_leaf")) + goto out; + + err = btf__load_into_kernel(btf); + ASSERT_EQ(err, expected_err, "check btf"); +out: + btf__free(btf); +} + +static void test_graph_depth(bool rbtree, int depth, int expected_err) +{ + int root_type = LIST_HEAD, node_type = LIST_NODE, node_size = 24; + int id, err, i, lock_off, root_off, size; + struct btf *btf; + + btf = init_btf(); + if (!ASSERT_OK_PTR(btf, "init_btf")) + return; + if (rbtree) { + root_type = btf__add_struct(btf, "bpf_rb_root", 16); + if (!ASSERT_GT(root_type, 0, "btf__add_struct bpf_rb_root")) + goto out; + node_type = btf__add_struct(btf, "bpf_rb_node", 32); + if (!ASSERT_GT(node_type, 0, "btf__add_struct bpf_rb_node")) + goto out; + node_size = 32; + } + + for (i = 0; i < depth; i++) { + char name[32], tag[64]; + + lock_off = i ? node_size : 0; + root_off = lock_off + 8; + size = i == depth - 1 ? node_size : root_off + 16; + snprintf(name, sizeof(name), "graph_owner_%d", i); + id = btf__add_struct(btf, name, size); + if (!ASSERT_GT(id, 0, "btf__add_struct graph_owner")) + goto out; + if (i) { + err = btf__add_field(btf, "node", node_type, 0, 0); + if (!ASSERT_OK(err, "btf__add_field graph_owner::node")) + goto out; + } + if (i == depth - 1) + continue; + err = btf__add_field(btf, "lock", SPIN_LOCK, lock_off * 8, 0); + if (!ASSERT_OK(err, "btf__add_field graph_owner::lock")) + goto out; + err = btf__add_field(btf, "root", root_type, root_off * 8, 0); + if (!ASSERT_OK(err, "btf__add_field graph_owner::root")) + goto out; + snprintf(tag, sizeof(tag), "contains:graph_owner_%d:node", i + 1); + err = btf__add_decl_tag(btf, tag, id, i ? 2 : 1); + if (!ASSERT_GT(err, 0, "btf__add_decl_tag graph_owner")) + goto out; + } + + err = btf__load_into_kernel(btf); + ASSERT_EQ(err, expected_err, "check btf"); +out: + btf__free(btf); +} + +void test_local_kptr_ownership(void) +{ + if (test__start_subtest("self_cycle")) + test_self_cycle("kptr", -ELOOP); + if (test__start_subtest("untrusted_self_cycle")) + test_self_cycle("kptr_untrusted", 0); + if (test__start_subtest("percpu_self_cycle")) + test_self_cycle("percpu_kptr", -ELOOP); + if (test__start_subtest("ABA_cycle")) + test_aba_cycle(); + if (test__start_subtest("mixed_graph_root_cycle")) + test_mixed_cycle(); + if (test__start_subtest("max_acyclic")) + test_acyclic_depth(MAX_OWNERSHIP_DEPTH, false, false, 0); + if (test__start_subtest("too_deep_acyclic")) + test_acyclic_depth(MAX_OWNERSHIP_DEPTH + 1, false, false, -ELOOP); + if (test__start_subtest("max_acyclic_child_first")) + test_acyclic_depth(MAX_OWNERSHIP_DEPTH, true, false, 0); + if (test__start_subtest("too_deep_acyclic_child_first")) + test_acyclic_depth(MAX_OWNERSHIP_DEPTH + 1, true, false, -ELOOP); + if (test__start_subtest("max_acyclic_shared_suffix")) + test_acyclic_depth(MAX_OWNERSHIP_DEPTH, false, true, 0); + if (test__start_subtest("too_deep_acyclic_shared_suffix")) + test_acyclic_depth(MAX_OWNERSHIP_DEPTH + 1, false, true, -ELOOP); + if (test__start_subtest("list_three_types")) + test_graph_depth(false, 3, 0); + if (test__start_subtest("list_four_types")) + test_graph_depth(false, 4, 0); + if (test__start_subtest("list_max_depth")) + test_graph_depth(false, MAX_OWNERSHIP_DEPTH, 0); + if (test__start_subtest("list_too_deep")) + test_graph_depth(false, MAX_OWNERSHIP_DEPTH + 1, -ELOOP); + if (test__start_subtest("rbtree_three_types")) + test_graph_depth(true, 3, 0); + if (test__start_subtest("rbtree_four_types")) + test_graph_depth(true, 4, 0); + if (test__start_subtest("rbtree_max_depth")) + test_graph_depth(true, MAX_OWNERSHIP_DEPTH, 0); + if (test__start_subtest("rbtree_too_deep")) + test_graph_depth(true, MAX_OWNERSHIP_DEPTH + 1, -ELOOP); +} diff --git a/tools/testing/selftests/bpf/prog_tests/percpu_alloc.c b/tools/testing/selftests/bpf/prog_tests/percpu_alloc.c index a72ae0b29f6e..7b4a1e24363b 100644 --- a/tools/testing/selftests/bpf/prog_tests/percpu_alloc.c +++ b/tools/testing/selftests/bpf/prog_tests/percpu_alloc.c @@ -1,4 +1,6 @@ // SPDX-License-Identifier: GPL-2.0 +#define _GNU_SOURCE +#include <sched.h> #include <test_progs.h> #include "cgroup_helpers.h" #include "percpu_alloc_array.skel.h" @@ -350,6 +352,103 @@ static void test_lru_percpu_hash_cpu_flag(void) test_percpu_map_cpu_flag(BPF_MAP_TYPE_LRU_PERCPU_HASH); } +/* + * A BPF_F_CPU update that creates an element must zero the value on the other + * cpus, rather than leave them holding whatever the recycled element last + * contained. max_entries is 1 so the second key can only reuse the element + * the first one released. + */ +static void test_percpu_map_cpu_flag_create(enum bpf_map_type map_type, __u32 map_flags) +{ + LIBBPF_OPTS(bpf_map_create_opts, opts, .map_flags = map_flags); + const u32 stale = 0xDEADC0DE, fresh = 0xC0FFEE; + int nr_cpus, cpu, map_fd, err, key; + int pinned_cpu, value_cpu; + cpu_set_t old_mask, new_mask; + bool restore_mask = false; + u32 value; + u64 flags; + + nr_cpus = libbpf_num_possible_cpus(); + if (!ASSERT_GT(nr_cpus, 0, "libbpf_num_possible_cpus")) + return; + + if (nr_cpus < 2) { + test__skip(); + return; + } + + map_fd = bpf_map_create(map_type, "cpu_flag_create", sizeof(key), sizeof(value), 1, &opts); + if (!ASSERT_GE(map_fd, 0, "bpf_map_create")) + return; + + /* NO_PREALLOC recycles per cpu, so keep the delete and the create on one cpu. */ + err = sched_getaffinity(0, sizeof(old_mask), &old_mask); + if (!ASSERT_OK(err, "sched_getaffinity")) + goto out; + + pinned_cpu = sched_getcpu(); + if (!ASSERT_GE(pinned_cpu, 0, "sched_getcpu")) + goto out; + + CPU_ZERO(&new_mask); + CPU_SET(pinned_cpu, &new_mask); + err = sched_setaffinity(0, sizeof(new_mask), &new_mask); + if (!ASSERT_OK(err, "sched_setaffinity")) + goto out; + restore_mask = true; + + value_cpu = pinned_cpu ? 0 : 1; + + key = 1; + value = stale; + err = bpf_map_update_elem(map_fd, &key, &value, BPF_F_ALL_CPUS); + if (!ASSERT_OK(err, "bpf_map_update_elem all_cpus")) + goto out; + + err = bpf_map_delete_elem(map_fd, &key); + if (!ASSERT_OK(err, "bpf_map_delete_elem")) + goto out; + + key = 2; + value = fresh; + flags = (u64)value_cpu << 32 | BPF_F_CPU; + err = bpf_map_update_elem(map_fd, &key, &value, flags); + if (!ASSERT_OK(err, "bpf_map_update_elem specified cpu")) + goto out; + + for (cpu = 0; cpu < nr_cpus; cpu++) { + value = 0; + flags = (u64)cpu << 32 | BPF_F_CPU; + err = bpf_map_lookup_elem_flags(map_fd, &key, &value, flags); + if (!ASSERT_OK(err, "bpf_map_lookup_elem_flags specified cpu")) + goto out; + if (!ASSERT_EQ(value, cpu == value_cpu ? fresh : 0, "value on specified cpu")) + goto out; + } + +out: + if (restore_mask) + sched_setaffinity(0, sizeof(old_mask), &old_mask); + close(map_fd); +} + +static void test_percpu_hash_cpu_flag_create(void) +{ + test_percpu_map_cpu_flag_create(BPF_MAP_TYPE_PERCPU_HASH, 0); +} + +static void test_percpu_hash_cpu_flag_create_malloc(void) +{ + test_percpu_map_cpu_flag_create(BPF_MAP_TYPE_PERCPU_HASH, BPF_F_NO_PREALLOC); +} + +static void test_lru_percpu_hash_cpu_flag_create(void) +{ + /* lru without prealloc is -ENOTSUPP, so there is no malloc variant */ + test_percpu_map_cpu_flag_create(BPF_MAP_TYPE_LRU_PERCPU_HASH, 0); +} + static void test_percpu_cgroup_storage_cpu_flag(void) { struct percpu_alloc_array *skel = NULL; @@ -454,6 +553,12 @@ void test_percpu_alloc(void) test_percpu_hash_cpu_flag(); if (test__start_subtest("cpu_flag_lru_percpu_hash")) test_lru_percpu_hash_cpu_flag(); + if (test__start_subtest("cpu_flag_create_percpu_hash")) + test_percpu_hash_cpu_flag_create(); + if (test__start_subtest("cpu_flag_create_percpu_hash_malloc")) + test_percpu_hash_cpu_flag_create_malloc(); + if (test__start_subtest("cpu_flag_create_lru_percpu_hash")) + test_lru_percpu_hash_cpu_flag_create(); if (test__start_subtest("cpu_flag_percpu_cgroup_storage")) test_percpu_cgroup_storage_cpu_flag(); if (test__start_subtest("cpu_flag_array")) diff --git a/tools/testing/selftests/bpf/prog_tests/sock_destroy.c b/tools/testing/selftests/bpf/prog_tests/sock_destroy.c index 9c11938fe597..78d642a02bdb 100644 --- a/tools/testing/selftests/bpf/prog_tests/sock_destroy.c +++ b/tools/testing/selftests/bpf/prog_tests/sock_destroy.c @@ -1,4 +1,5 @@ // SPDX-License-Identifier: GPL-2.0 +#include <poll.h> #include <test_progs.h> #include <bpf/bpf_endian.h> @@ -110,6 +111,122 @@ cleanup: close(serv); } +static void test_tcp_listen_pending(struct sock_destroy_prog *skel) +{ + int serv = -1, clien = -1, accept_serv = -1, n, serv_port; + struct pollfd pfd = { .events = POLLIN }; + char buf[1]; + + serv = start_server(AF_INET6, SOCK_STREAM, NULL, 0, 0); + if (!ASSERT_GE(serv, 0, "start_server")) + goto cleanup; + serv_port = get_socket_local_port(serv); + if (!ASSERT_GE(serv_port, 0, "get_sock_local_port")) + goto cleanup; + skel->bss->serv_port = (__be16)serv_port; + + /* + * Connect but never accept, so the child sits in the accept queue + * of the listener. Wait until it's actually there. + */ + clien = connect_to_fd(serv, 0); + if (!ASSERT_GE(clien, 0, "connect_to_fd")) + goto cleanup; + pfd.fd = serv; + if (!ASSERT_EQ(poll(&pfd, 1, -1), 1, "poll listener")) + goto cleanup; + + /* Run iterator program that destroys server sockets. */ + start_iter_sockets(skel->progs.iter_tcp6_server); + + accept_serv = accept(serv, NULL, NULL); + if (!ASSERT_LT(accept_serv, 0, "accept on destroyed listener")) + goto cleanup; + ASSERT_EQ(errno, EINVAL, "error code on destroyed listener"); + + /* The unaccepted child was reset along with the listener. */ + n = recv(clien, buf, sizeof(buf), 0); + if (!ASSERT_LT(n, 0, "client recv on reset child")) + goto cleanup; + ASSERT_EQ(errno, ECONNRESET, "error code on reset child"); + +cleanup: + if (clien != -1) + close(clien); + if (accept_serv != -1) + close(accept_serv); + if (serv != -1) + close(serv); +} + +static void test_tcp_timewait(struct sock_destroy_prog *skel) +{ + int serv = -1, clien = -1, accept_serv = -1, n; + struct timeval tv = {}; + char buf[1]; + + serv = start_server(AF_INET6, SOCK_STREAM, NULL, 0, 0); + if (!ASSERT_GE(serv, 0, "start_server")) + goto cleanup; + + clien = connect_to_fd(serv, 0); + if (!ASSERT_GE(clien, 0, "connect_to_fd")) + goto cleanup; + + accept_serv = accept(serv, NULL, NULL); + if (!ASSERT_GE(accept_serv, 0, "serv accept")) + goto cleanup; + + /* + * Active close from the client, then close the server side. Once + * recv() sees EOF the server FIN has been processed and the client + * sock is in TIME_WAIT. Block without timeout so a loaded CI box + * can't race us. + */ + if (!ASSERT_OK(setsockopt(clien, SOL_SOCKET, SO_RCVTIMEO, &tv, + sizeof(tv)), "clear rcvtimeo")) + goto cleanup; + if (!ASSERT_OK(shutdown(clien, SHUT_WR), "client shutdown")) + goto cleanup; + + /* + * Make sure the server has seen the client FIN before it closes, + * so the two FINs never cross. + */ + n = recv(accept_serv, buf, sizeof(buf), 0); + if (!ASSERT_EQ(n, 0, "server recv EOF")) + goto cleanup; + + close(accept_serv); + accept_serv = -1; + + /* block until return EOF */ + n = recv(clien, buf, sizeof(buf), 0); + if (!ASSERT_EQ(n, 0, "client recv EOF")) + goto cleanup; + + /* Run iterator program that destroys the timewait client sock. */ + skel->bss->tw_found = 0; + start_iter_sockets(skel->progs.iter_tcp6_timewait); + if (!ASSERT_EQ(skel->bss->tw_found, 1, "timewait sock found")) + goto cleanup; + + ASSERT_OK(skel->bss->tw_destroy_err, "destroy timewait sock"); + + /* The destroyed timewait sock must be gone. */ + skel->bss->tw_found = 0; + start_iter_sockets(skel->progs.iter_tcp6_timewait); + ASSERT_EQ(skel->bss->tw_found, 0, "timewait sock destroyed"); + +cleanup: + if (clien != -1) + close(clien); + if (accept_serv != -1) + close(accept_serv); + if (serv != -1) + close(serv); +} + static void test_udp_client(struct sock_destroy_prog *skel) { int serv = -1, clien = -1, n = 0; @@ -204,6 +321,10 @@ void test_sock_destroy(void) test_tcp_client(skel); if (test__start_subtest("tcp_server")) test_tcp_server(skel); + if (test__start_subtest("tcp_listen_pending")) + test_tcp_listen_pending(skel); + if (test__start_subtest("tcp_timewait")) + test_tcp_timewait(skel); if (test__start_subtest("udp_client")) test_udp_client(skel); if (test__start_subtest("udp_server")) diff --git a/tools/testing/selftests/bpf/prog_tests/spin_lock.c b/tools/testing/selftests/bpf/prog_tests/spin_lock.c index 5c3579438427..e368370262c8 100644 --- a/tools/testing/selftests/bpf/prog_tests/spin_lock.c +++ b/tools/testing/selftests/bpf/prog_tests/spin_lock.c @@ -54,6 +54,8 @@ static struct { { "lock_global_sleepable_helper_subprog", "global function calls are not allowed while holding a lock" }, { "lock_global_sleepable_kfunc_subprog", "global function calls are not allowed while holding a lock" }, { "lock_global_sleepable_subprog_indirect", "global function calls are not allowed while holding a lock" }, + { "callback_value_lock_identity", "bpf_spin_unlock of different lock" }, + { "callback_inner_map_value_lock_identity", "bpf_spin_unlock of different lock" }, }; static int match_regex(const char *pattern, const char *string) diff --git a/tools/testing/selftests/bpf/prog_tests/tc_change_tail_pmtu.c b/tools/testing/selftests/bpf/prog_tests/tc_change_tail_pmtu.c new file mode 100644 index 000000000000..7acdbd5757a9 --- /dev/null +++ b/tools/testing/selftests/bpf/prog_tests/tc_change_tail_pmtu.c @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: GPL-2.0 + +#include <netinet/tcp.h> + +#include "test_progs.h" +#include "network_helpers.h" +#include "test_tc_change_tail_pmtu.skel.h" + +#define CLIENT_NS "tc-change-tail-cli-ns" +#define SERVER_NS "tc-change-tail-srv-ns" +#define CLIENT_IP "192.168.1.1" +#define SERVER_IP "192.168.1.2" + +#define TEST_PMTU 1000 +#define TEST_MSS_MAX (TEST_PMTU - 20 - 20) +#define TIMEOUT_MS 3000 +#define XFER_BYTES 8192 + +void test_tc_change_tail_pmtu(void) +{ + LIBBPF_OPTS(bpf_tcx_opts, tcx_opts); + int mss_before = 0, mss_after = 0, ifindex, port; + int srv_fd = -1, srv_conn_fd = -1, cli_fd = -1; + struct test_tc_change_tail_pmtu *skel = NULL; + struct nstoken *nstoken = NULL; + static char buf[XFER_BYTES]; + socklen_t optlen; + ssize_t bytes; + size_t total; + + if (!ASSERT_OK(make_netns(CLIENT_NS), "make client ns")) + return; + if (!ASSERT_OK(make_netns(SERVER_NS), "make server ns")) + goto out_client_ns; + + nstoken = open_netns(CLIENT_NS); + if (!ASSERT_OK_PTR(nstoken, "open client ns")) + goto out; + SYS(out, "ip link add veth1 type veth peer name veth2 netns " SERVER_NS); + SYS(out, "ip -4 addr add " CLIENT_IP "/24 dev veth1"); + SYS(out, "ip link set veth1 up"); + ifindex = if_nametoindex("veth1"); + if (!ASSERT_NEQ(ifindex, 0, "if_nametoindex")) + goto out; + close_netns(nstoken); + nstoken = NULL; + + nstoken = open_netns(SERVER_NS); + if (!ASSERT_OK_PTR(nstoken, "open server ns")) + goto out; + SYS(out, "ip -4 addr add " SERVER_IP "/24 dev veth2"); + SYS(out, "ip link set veth2 up"); + srv_fd = start_server(AF_INET, SOCK_STREAM, SERVER_IP, 0, TIMEOUT_MS); + if (!ASSERT_OK_FD(srv_fd, "start server")) + goto out; + close_netns(nstoken); + nstoken = NULL; + + skel = test_tc_change_tail_pmtu__open_and_load(); + if (!ASSERT_OK_PTR(skel, "open and load skeleton")) + goto out; + + port = get_socket_local_port(srv_fd); + if (!ASSERT_GE(port, 0, "get server port")) + goto out; + + skel->bss->server_port = port; + skel->bss->pmtu = TEST_PMTU; + + nstoken = open_netns(CLIENT_NS); + if (!ASSERT_OK_PTR(nstoken, "open client ns")) + goto out; + + skel->links.change_tail_icmp = + bpf_program__attach_tcx(skel->progs.change_tail_icmp, ifindex, + &tcx_opts); + if (!ASSERT_OK_PTR(skel->links.change_tail_icmp, "attach tcx")) + goto out; + + cli_fd = connect_to_fd(srv_fd, TIMEOUT_MS); + if (!ASSERT_OK_FD(cli_fd, "connect to server")) + goto out; + srv_conn_fd = accept(srv_fd, NULL, NULL); + if (!ASSERT_OK_FD(srv_conn_fd, "accept connection")) + goto out; + if (!ASSERT_OK(settimeo(srv_conn_fd, TIMEOUT_MS), "set server timeout")) + goto out; + + optlen = sizeof(mss_before); + if (!ASSERT_OK(getsockopt(cli_fd, IPPROTO_TCP, TCP_MAXSEG, &mss_before, + &optlen), "get mss before")) + goto out; + + bytes = send(cli_fd, buf, sizeof(buf), 0); + if (!ASSERT_EQ(bytes, (ssize_t)sizeof(buf), "send data")) + goto out; + + for (total = 0; total < sizeof(buf); total += bytes) { + bytes = recv(srv_conn_fd, buf, sizeof(buf), 0); + if (bytes <= 0) + break; + } + + ASSERT_EQ(total, sizeof(buf), "receive data"); + ASSERT_OK(skel->data->change_tail_ret, "change tail"); + ASSERT_OK(skel->bss->adjust_room_ret, "adjust room"); + ASSERT_TRUE(skel->bss->icmp_sent, "icmp sent"); + + optlen = sizeof(mss_after); + if (!ASSERT_OK(getsockopt(cli_fd, IPPROTO_TCP, TCP_MAXSEG, &mss_after, + &optlen), "get mss after")) + goto out; + + ASSERT_LT(mss_after, mss_before, "mss reduced"); + ASSERT_LE(mss_after, TEST_MSS_MAX, "mss below pmtu"); +out: + close(srv_conn_fd); + close(cli_fd); + close(srv_fd); + test_tc_change_tail_pmtu__destroy(skel); + close_netns(nstoken); + remove_netns(SERVER_NS); +out_client_ns: + remove_netns(CLIENT_NS); +} diff --git a/tools/testing/selftests/bpf/prog_tests/tramp_prog_detach.c b/tools/testing/selftests/bpf/prog_tests/tramp_prog_detach.c new file mode 100644 index 000000000000..1eb0d7237605 --- /dev/null +++ b/tools/testing/selftests/bpf/prog_tests/tramp_prog_detach.c @@ -0,0 +1,188 @@ +// SPDX-License-Identifier: GPL-2.0 +#include <test_progs.h> +#include <pthread.h> +#include <poll.h> +#include <sys/mman.h> +#include <linux/userfaultfd.h> +#include "tramp_prog_detach.skel.h" +#include "testing_helpers.h" + +/* + * Detach and free progs while a task sleeps in the prog that runs before them + * in the same trampoline image, then let that task continue through the + * image. It must not call into the freed progs. + * + * The task is held in a sleepable prog with userfaultfd, like bpf_mod_race + * does. + */ + +static struct bpf_program *pick_prog(struct tramp_prog_detach *skel, + bool fexit, bool sleepable) +{ + if (fexit) + return sleepable ? skel->progs.fexit_sleepable : + skel->progs.fexit_victim; + return sleepable ? skel->progs.fentry_sleepable : + skel->progs.fentry_victim; +} + +static struct bpf_program *sleepable_prog; + +static void *run_sleepable(void *arg) +{ + LIBBPF_OPTS(bpf_test_run_opts, topts); + + /* calls bpf_fentry_test1() */ + return (void *)(long)bpf_prog_test_run_opts(bpf_program__fd(sleepable_prog), + &topts); +} + +static struct tramp_prog_detach *load_one(bool fexit, bool sleepable) +{ + struct tramp_prog_detach *skel; + int err; + + skel = tramp_prog_detach__open(); + if (!ASSERT_OK_PTR(skel, "open")) + return NULL; + + bpf_program__set_autoload(pick_prog(skel, fexit, sleepable), true); + err = tramp_prog_detach__load(skel); + if (!ASSERT_OK(err, "load")) + goto err; + skel->bss->pid = getpid(); + err = tramp_prog_detach__attach(skel); + if (!ASSERT_OK(err, "attach")) + goto err; + return skel; +err: + tramp_prog_detach__destroy(skel); + return NULL; +} + +/* The .bss map of a destroyed skeleton goes away when its prog is freed */ +static bool wait_for_map_free(__u32 map_id) +{ + int i, fd; + + for (i = 0; i < 100; i++) { + fd = bpf_map_get_fd_by_id(map_id); + if (fd < 0) + return true; + close(fd); + usleep(100 * 1000); + } + return false; +} + +static void test_detach(bool sleepable_fexit, bool victim_fexit) +{ + struct tramp_prog_detach *sleepable = NULL, *victims[2] = {}; + struct pollfd pfd = { .events = POLLIN }; + struct uffdio_copy uffd_copy = {}; + struct bpf_map_info map_info = {}; + __u32 map_info_len = sizeof(map_info); + struct uffd_msg uffd_msg; + void *fault_page, *src_page = MAP_FAILED; + long page_size = getpagesize(); + bool started = false; + void *thread_ret; + pthread_t thread; + int i, uffd = -1; + + fault_page = mmap(NULL, page_size, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (!ASSERT_NEQ(fault_page, MAP_FAILED, "mmap fault_page")) + return; + src_page = mmap(NULL, page_size, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (!ASSERT_NEQ(src_page, MAP_FAILED, "mmap src_page")) + goto out; + + /* The most recently attached prog runs first */ + for (i = 0; i < ARRAY_SIZE(victims); i++) { + victims[i] = load_one(victim_fexit, false); + if (!victims[i]) + goto out; + } + sleepable = load_one(sleepable_fexit, true); + if (!sleepable) + goto out; + sleepable_prog = pick_prog(sleepable, sleepable_fexit, true); + + /* Not armed yet so this doesn't block, make sure sleepable runs first */ + if (!ASSERT_OK((long)run_sleepable(NULL), "dry run")) + goto out; + for (i = 0; i < ARRAY_SIZE(victims); i++) + if (!ASSERT_LT(sleepable->bss->ts, victims[i]->bss->ts, "prog order")) + goto out; + + uffd = uffd_block_page(fault_page); + if (!ASSERT_GE(uffd, 0, "userfaultfd open + register address")) + goto out; + sleepable->bss->fault_addr = fault_page; + + if (!ASSERT_OK(pthread_create(&thread, NULL, run_sleepable, NULL), + "pthread_create")) + goto out; + started = true; + + /* Wait for the thread to sleep in bpf_copy_from_user() */ + pfd.fd = uffd; + if (!ASSERT_EQ(poll(&pfd, 1, 10000), 1, "poll uffd")) + goto out; + if (!ASSERT_EQ(read(uffd, &uffd_msg, sizeof(uffd_msg)), sizeof(uffd_msg), + "read uffd")) + goto out; + if (!ASSERT_EQ(uffd_msg.event, UFFD_EVENT_PAGEFAULT, "uffd pagefault")) + goto out; + + /* + * Detach and unload the victim progs and wait for them to be freed. + * After the first one, the task is in an image that isn't the + * trampoline's current one anymore. + */ + for (i = 0; i < ARRAY_SIZE(victims); i++) { + if (!ASSERT_OK(bpf_map_get_info_by_fd(bpf_map__fd(victims[i]->maps.bss), + &map_info, &map_info_len), + "victim bss info")) + goto out; + tramp_prog_detach__destroy(victims[i]); + victims[i] = NULL; + if (!ASSERT_TRUE(wait_for_map_free(map_info.id), "victim freed")) + goto out; + } + +out: + /* Let the thread proceed with the rest of the trampoline */ + if (uffd >= 0) { + uffd_copy.dst = (unsigned long)fault_page; + uffd_copy.src = (unsigned long)src_page; + uffd_copy.len = page_size; + ASSERT_OK(ioctl(uffd, UFFDIO_COPY, &uffd_copy), "uffd copy"); + close(uffd); + } + if (started && + ASSERT_OK(pthread_join(thread, &thread_ret), "pthread_join")) + ASSERT_NULL(thread_ret, "blocking run"); + + for (i = 0; i < ARRAY_SIZE(victims); i++) + tramp_prog_detach__destroy(victims[i]); + tramp_prog_detach__destroy(sleepable); + if (src_page != MAP_FAILED) + munmap(src_page, page_size); + munmap(fault_page, page_size); +} + +void serial_test_tramp_prog_detach(void) +{ + /* a task sleeping before the original function is called */ + if (test__start_subtest("fentry")) + test_detach(false, false); + /* a task sleeping after the original function returned */ + if (test__start_subtest("fexit")) + test_detach(true, true); + /* the original function runs in between and must still be called */ + if (test__start_subtest("fentry_fexit")) + test_detach(false, true); +} diff --git a/tools/testing/selftests/bpf/prog_tests/verifier.c b/tools/testing/selftests/bpf/prog_tests/verifier.c index 64ac49ad67e6..8b439e194bcc 100644 --- a/tools/testing/selftests/bpf/prog_tests/verifier.c +++ b/tools/testing/selftests/bpf/prog_tests/verifier.c @@ -23,6 +23,7 @@ #include "verifier_bpf_trap.skel.h" #include "verifier_bswap.skel.h" #include "verifier_btf_ctx_access.skel.h" +#include "verifier_btf_flex_array.skel.h" #include "verifier_btf_unreliable_prog.skel.h" #include "verifier_call_large_imm.skel.h" #include "verifier_cfg.skel.h" @@ -53,6 +54,7 @@ #include "verifier_iterating_callbacks.skel.h" #include "verifier_jeq_infer_not_null.skel.h" #include "verifier_jit_convergence.skel.h" +#include "verifier_kfunc_perfmon.skel.h" #include "verifier_ld_ind.skel.h" #include "verifier_ldsx.skel.h" #include "verifier_leak_ptr.skel.h" @@ -186,6 +188,7 @@ void test_verifier_bpf_get_stack(void) { RUN(verifier_bpf_get_stack); } void test_verifier_bpf_trap(void) { RUN(verifier_bpf_trap); } void test_verifier_bswap(void) { RUN(verifier_bswap); } void test_verifier_btf_ctx_access(void) { RUN(verifier_btf_ctx_access); } +void test_verifier_btf_flex_array(void) { RUN(verifier_btf_flex_array); } void test_verifier_btf_unreliable_prog(void) { RUN(verifier_btf_unreliable_prog); } void test_verifier_call_large_imm(void) { RUN(verifier_call_large_imm); } void test_verifier_cfg(void) { RUN(verifier_cfg); } @@ -216,6 +219,7 @@ void test_verifier_int_ptr(void) { RUN(verifier_int_ptr); } void test_verifier_iterating_callbacks(void) { RUN(verifier_iterating_callbacks); } void test_verifier_jeq_infer_not_null(void) { RUN(verifier_jeq_infer_not_null); } void test_verifier_jit_convergence(void) { RUN(verifier_jit_convergence); } +void test_verifier_kfunc_perfmon(void) { RUN(verifier_kfunc_perfmon); } void test_verifier_load_acquire(void) { RUN(verifier_load_acquire); } void test_verifier_ld_ind(void) { RUN(verifier_ld_ind); } void test_verifier_ldsx(void) { RUN(verifier_ldsx); } diff --git a/tools/testing/selftests/bpf/progs/bpf_ma_ttrace.c b/tools/testing/selftests/bpf/progs/bpf_ma_ttrace.c new file mode 100644 index 000000000000..31b2a962e339 --- /dev/null +++ b/tools/testing/selftests/bpf/progs/bpf_ma_ttrace.c @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: GPL-2.0 +#include <vmlinux.h> +#include <bpf/bpf_helpers.h> +#include <bpf/bpf_core_read.h> +#include "bpf_kfuncs.h" + +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __uint(map_flags, BPF_F_NO_PREALLOC); + __uint(max_entries, 4096); + __type(key, int); + __type(value, long); +} htab SEC(".maps"); + +extern const void __per_cpu_offset __ksym; + +int nr_cpus; +int nr_caches; +int in_progress; +int not_freed; + +/* Look at bpf_mem_cache of every cpu that htab allocates its elements from */ +SEC("syscall") +int check_ttrace(void *ctx) +{ + struct bpf_htab *h = bpf_core_cast(&htab, struct bpf_htab); + unsigned long cache = (unsigned long)BPF_CORE_READ(h, ma.cache); + const unsigned long *offsets = &__per_cpu_offset; + struct bpf_mem_cache *c; + unsigned long off; + int cpu; + + nr_caches = 0; + in_progress = 0; + not_freed = 0; + bpf_for(cpu, 0, nr_cpus) { + if (bpf_probe_read_kernel(&off, sizeof(off), offsets + cpu)) + return -1; + c = bpf_core_cast((void *)(cache + off), struct bpf_mem_cache); + if (c->unit_size) + nr_caches++; + if (c->call_rcu_ttrace_in_progress.counter) + in_progress++; + if (c->free_by_rcu_ttrace.first || c->waiting_for_gp_ttrace.first) + not_freed++; + } + return 0; +} + +char _license[] SEC("license") = "GPL"; diff --git a/tools/testing/selftests/bpf/progs/dynptr_success.c b/tools/testing/selftests/bpf/progs/dynptr_success.c index e0745b6e467e..b668ebd61fc7 100644 --- a/tools/testing/selftests/bpf/progs/dynptr_success.c +++ b/tools/testing/selftests/bpf/progs/dynptr_success.c @@ -10,6 +10,7 @@ #include "errno.h" #define PAGE_SIZE_64K 65536 +#define TEST_SKB_LINEAR_SIZE (sizeof(struct ethhdr) + sizeof(struct iphdr)) char _license[] SEC("license") = "GPL"; @@ -212,6 +213,25 @@ int test_dynptr_skb_data(struct __sk_buff *skb) } SEC("?tc") +int test_dynptr_skb_slice_non_linear(struct __sk_buff *skb) +{ + struct bpf_dynptr ptr; + void *data; + + if (bpf_dynptr_from_skb(skb, 0, &ptr)) { + err = 1; + return 1; + } + + /* Ensure we cannot read past the end of the buffer. */ + data = bpf_dynptr_slice(&ptr, TEST_SKB_LINEAR_SIZE + 1, NULL, 1); + if (data) + err = 2; + + return 1; +} + +SEC("?tc") int test_dynptr_skb_meta_data(struct __sk_buff *skb) { struct bpf_dynptr meta; diff --git a/tools/testing/selftests/bpf/progs/exceptions.c b/tools/testing/selftests/bpf/progs/exceptions.c index c8d716fbd419..91c81971e58c 100644 --- a/tools/testing/selftests/bpf/progs/exceptions.c +++ b/tools/testing/selftests/bpf/progs/exceptions.c @@ -212,6 +212,36 @@ int exception_throw_subprog(struct __sk_buff *ctx) return 0; } +u64 exception_cb_stack_src = 0x1234; + +/* + * The address handed to the helper has to be this callback's own stack + * slot, not one from a frame that is already gone. + */ +__noinline int exception_cb_stack(u64 cookie) +{ + volatile u64 val = 0xdead; + + bpf_probe_read_kernel((void *)&val, sizeof(val), &exception_cb_stack_src); + return val; +} + +/* Throws from a subprogram that has a stack of its own. */ +__noinline static int throwing_subprog_stack(struct __sk_buff *ctx) +{ + volatile u64 pad[4] = {}; + + bpf_throw(pad[0]); + return 0; +} + +SEC("tc") +__exception_cb(exception_cb_stack) +int exception_throw_subprog_stack_cb(struct __sk_buff *ctx) +{ + return throwing_subprog_stack(ctx); +} + __noinline int assert_nz_gfunc(u64 c) { volatile u64 cookie = c; diff --git a/tools/testing/selftests/bpf/progs/iters_state_safety.c b/tools/testing/selftests/bpf/progs/iters_state_safety.c index 646026430e9b..e5bb9fe6d5e5 100644 --- a/tools/testing/selftests/bpf/progs/iters_state_safety.c +++ b/tools/testing/selftests/bpf/progs/iters_state_safety.c @@ -52,6 +52,28 @@ int create_and_destroy(void *ctx) return 0; } +/* fp+0 is not a stack slot. bpf_get_spi(0) used to alias spi 0 (fp-8). */ +SEC("?raw_tp") +__failure __msg("cannot pass in iter at an offset=0") +int destroy_fp0_fail(void *ctx) +{ + struct bpf_iter_num iter; + + asm volatile ("r1 = %[iter];" + "r2 = 0;" + "r3 = 1000;" + "call %[bpf_iter_num_new];" + /* r10 is fp+0, one byte above the top of the BPF stack */ + "r1 = r10;" + "call %[bpf_iter_num_destroy];" + : + : __imm_ptr(iter), ITER_HELPERS + : __clobber_common + ); + + return 0; +} + SEC("?raw_tp") __failure __msg("Unreleased reference id=1") int create_and_forget_to_destroy_fail(void *ctx) diff --git a/tools/testing/selftests/bpf/progs/sock_destroy_prog.c b/tools/testing/selftests/bpf/progs/sock_destroy_prog.c index 9e0bf7a54cec..0a8887543218 100644 --- a/tools/testing/selftests/bpf/progs/sock_destroy_prog.c +++ b/tools/testing/selftests/bpf/progs/sock_destroy_prog.c @@ -7,6 +7,8 @@ #include "bpf_tracing_net.h" __be16 serv_port = 0; +int tw_found = 0; +int tw_destroy_err = 0; int bpf_sock_destroy(struct sock_common *sk) __ksym; @@ -100,6 +102,34 @@ int iter_tcp6_server(struct bpf_iter__tcp *ctx) return 0; } +SEC("iter/tcp") +int iter_tcp6_timewait(struct bpf_iter__tcp *ctx) +{ + struct sock_common *sk_common = ctx->sk_common; + __u64 *val; + int key = 0; + + if (!sk_common) + return 0; + + if (sk_common->skc_family != AF_INET6) + return 0; + + if (!bpf_skc_to_tcp_timewait_sock(sk_common)) + return 0; + + val = bpf_map_lookup_elem(&tcp_conn_sockets, &key); + if (!val) + return 0; + /* The timewait sock inherits the cookie of the closed client sock. */ + if (bpf_get_socket_cookie(sk_common) != *val) + return 0; + + tw_found++; + tw_destroy_err = bpf_sock_destroy(sk_common); + + return 0; +} SEC("iter/udp") int iter_udp6_client(struct bpf_iter__udp *ctx) diff --git a/tools/testing/selftests/bpf/progs/test_spin_lock_fail.c b/tools/testing/selftests/bpf/progs/test_spin_lock_fail.c index f678ee6bd7ea..55282f20fa32 100644 --- a/tools/testing/selftests/bpf/progs/test_spin_lock_fail.c +++ b/tools/testing/selftests/bpf/progs/test_spin_lock_fail.c @@ -14,17 +14,18 @@ struct array_map { __type(key, int); __type(value, struct foo); __uint(max_entries, 1); -} array_map SEC(".maps"); +} array_map SEC(".maps"), array_map_b SEC(".maps"); struct { __uint(type, BPF_MAP_TYPE_ARRAY_OF_MAPS); - __uint(max_entries, 1); + __uint(max_entries, 2); __type(key, int); __type(value, int); __array(values, struct array_map); } map_of_maps SEC(".maps") = { .values = { [0] = &array_map, + [1] = &array_map_b, }, }; @@ -314,4 +315,66 @@ int lock_global_sleepable_subprog_indirect(struct __sk_buff *ctx) return ret; } +struct { + __uint(type, BPF_MAP_TYPE_ARRAY); + __uint(max_entries, 2); + __type(key, int); + __type(value, struct foo); +} callback_array_map SEC(".maps"); + +struct callback_ctx { + struct foo *value; +}; + +static long lock_different_value(struct bpf_map *map, int *key, + struct foo *value, struct callback_ctx *ctx) +{ + bpf_spin_lock(&value->lock); + bpf_spin_unlock(&ctx->value->lock); + return 0; +} + +static long nest_lock_different_value(struct bpf_map *map, int *key, + struct foo *value, void *data) +{ + struct callback_ctx ctx = { .value = value }; + + bpf_for_each_map_elem(&callback_array_map, lock_different_value, &ctx, 0); + return 0; +} + +SEC("?tc") +int callback_value_lock_identity(void *ctx) +{ + bpf_for_each_map_elem(&callback_array_map, nest_lock_different_value, NULL, 0); + return 0; +} + +static long nest_lock_different_inner_value(struct bpf_map *map, int *key, + struct foo *value, void *data) +{ + struct callback_ctx ctx = { .value = value }; + int inner_key = 1; + void *inner_map; + + inner_map = bpf_map_lookup_elem(&map_of_maps, &inner_key); + if (!inner_map) + return 0; + bpf_for_each_map_elem(inner_map, lock_different_value, &ctx, 0); + return 0; +} + +SEC("?tc") +int callback_inner_map_value_lock_identity(void *ctx) +{ + int inner_key = 0; + void *inner_map; + + inner_map = bpf_map_lookup_elem(&map_of_maps, &inner_key); + if (!inner_map) + return 0; + bpf_for_each_map_elem(inner_map, nest_lock_different_inner_value, NULL, 0); + return 0; +} + char _license[] SEC("license") = "GPL"; diff --git a/tools/testing/selftests/bpf/progs/test_tc_change_tail_pmtu.c b/tools/testing/selftests/bpf/progs/test_tc_change_tail_pmtu.c new file mode 100644 index 000000000000..5c4c07545bc9 --- /dev/null +++ b/tools/testing/selftests/bpf/progs/test_tc_change_tail_pmtu.c @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: GPL-2.0 + +#include <stdbool.h> +#include <stddef.h> + +#include <linux/bpf.h> +#include <linux/icmp.h> +#include <linux/if_ether.h> +#include <linux/in.h> +#include <linux/ip.h> +#include <linux/tcp.h> + +#include <bpf/bpf_helpers.h> +#include <bpf/bpf_endian.h> + +#define ICMP_SAMPLE_LEN (sizeof(struct iphdr) + 8) +#define ICMP_HDRS_LEN (sizeof(struct iphdr) + sizeof(struct icmphdr)) + +__be16 server_port = 0; +__u16 pmtu = 0; + +long change_tail_ret = 1; +long adjust_room_ret = 0; +bool icmp_sent = false; +bool icmp_err = false; + +static __always_inline __sum16 csum_fold(__wsum csum) +{ + csum = (csum & 0xffff) + (csum >> 16); + csum = (csum & 0xffff) + (csum >> 16); + + return (__sum16)~csum; +} + +SEC("tc/egress") +int change_tail_icmp(struct __sk_buff *skb) +{ + __u8 smac[ETH_ALEN], dmac[ETH_ALEN]; + void *data, *data_end; + struct icmphdr *icmp; + struct ethhdr *eth; + struct tcphdr *tcp; + __be32 saddr, daddr; + struct iphdr *ip; + __wsum csum; + + if (icmp_sent || icmp_err) + return TCX_PASS; + + data = (void *)(long)skb->data; + data_end = (void *)(long)skb->data_end; + + eth = data; + if ((void *)(eth + 1) > data_end) + return TCX_PASS; + if (eth->h_proto != bpf_htons(ETH_P_IP)) + return TCX_PASS; + + ip = (void *)(eth + 1); + if ((void *)(ip + 1) > data_end) + return TCX_PASS; + if (ip->ihl != 5 || ip->protocol != IPPROTO_TCP) + return TCX_PASS; + + tcp = (void *)(ip + 1); + if ((void *)(tcp + 1) > data_end) + return TCX_PASS; + if (tcp->dest != server_port) + return TCX_PASS; + if (bpf_ntohs(ip->tot_len) <= sizeof(*ip) + tcp->doff * 4) + return TCX_PASS; + + __builtin_memcpy(smac, eth->h_source, ETH_ALEN); + __builtin_memcpy(dmac, eth->h_dest, ETH_ALEN); + saddr = ip->saddr; + daddr = ip->daddr; + + change_tail_ret = bpf_skb_change_tail(skb, ETH_HLEN + ICMP_SAMPLE_LEN, 0); + if (change_tail_ret) { + icmp_err = true; + return TCX_PASS; + } + + adjust_room_ret = bpf_skb_adjust_room(skb, ICMP_HDRS_LEN, + BPF_ADJ_ROOM_MAC, + BPF_F_ADJ_ROOM_NO_CSUM_RESET); + if (adjust_room_ret) { + icmp_err = true; + return TCX_DROP; + } + + data = (void *)(long)skb->data; + data_end = (void *)(long)skb->data_end; + + eth = data; + ip = (void *)(eth + 1); + icmp = (void *)(ip + 1); + if ((void *)icmp + sizeof(*icmp) + ICMP_SAMPLE_LEN > data_end) { + icmp_err = true; + return TCX_DROP; + } + + __builtin_memcpy(eth->h_dest, smac, ETH_ALEN); + __builtin_memcpy(eth->h_source, dmac, ETH_ALEN); + + __builtin_memset(icmp, 0, sizeof(*icmp)); + icmp->type = ICMP_DEST_UNREACH; + icmp->code = ICMP_FRAG_NEEDED; + icmp->un.frag.mtu = bpf_htons(pmtu); + + __builtin_memset(ip, 0, sizeof(*ip)); + ip->version = 4; + ip->ihl = 5; + ip->ttl = 64; + ip->protocol = IPPROTO_ICMP; + ip->tot_len = bpf_htons(ICMP_HDRS_LEN + ICMP_SAMPLE_LEN); + ip->saddr = daddr; + ip->daddr = saddr; + + csum = bpf_csum_diff(NULL, 0, (__be32 *)icmp, + sizeof(*icmp) + ICMP_SAMPLE_LEN, 0); + icmp->checksum = csum_fold(csum); + csum = bpf_csum_diff(NULL, 0, (__be32 *)ip, sizeof(*ip), 0); + ip->check = csum_fold(csum); + icmp_sent = true; + return bpf_redirect(skb->ifindex, BPF_F_INGRESS); +} + +char _license[] SEC("license") = "GPL"; diff --git a/tools/testing/selftests/bpf/progs/tramp_prog_detach.c b/tools/testing/selftests/bpf/progs/tramp_prog_detach.c new file mode 100644 index 000000000000..507d372167ec --- /dev/null +++ b/tools/testing/selftests/bpf/progs/tramp_prog_detach.c @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-2.0 +#include "vmlinux.h" +#include <bpf/bpf_helpers.h> +#include <bpf/bpf_tracing.h> + +char _license[] SEC("license") = "GPL"; + +int pid; +void *fault_addr; +__u64 ts; + +static int do_sleepable(void) +{ + char dst; + + if (bpf_get_current_pid_tgid() >> 32 != pid) + return 0; + + ts = bpf_ktime_get_ns(); + /* blocks for as long as user space wants when fault_addr is armed */ + bpf_copy_from_user(&dst, sizeof(dst), fault_addr); + return 0; +} + +static int do_victim(void) +{ + if (bpf_get_current_pid_tgid() >> 32 != pid) + return 0; + + ts = bpf_ktime_get_ns(); + return 0; +} + +SEC("?fentry.s/bpf_fentry_test1") +int BPF_PROG(fentry_sleepable, int a) +{ + return do_sleepable(); +} + +SEC("?fentry/bpf_fentry_test1") +int BPF_PROG(fentry_victim, int a) +{ + return do_victim(); +} + +SEC("?fexit.s/bpf_fentry_test1") +int BPF_PROG(fexit_sleepable, int a, int ret) +{ + return do_sleepable(); +} + +SEC("?fexit/bpf_fentry_test1") +int BPF_PROG(fexit_victim, int a, int ret) +{ + return do_victim(); +} diff --git a/tools/testing/selftests/bpf/progs/verifier_align.c b/tools/testing/selftests/bpf/progs/verifier_align.c index 3e52686515ca..0083ef8b8ba7 100644 --- a/tools/testing/selftests/bpf/progs/verifier_align.c +++ b/tools/testing/selftests/bpf/progs/verifier_align.c @@ -284,7 +284,7 @@ __msg("26: {{.*}} R5=pkt(r=8,imm=14)") */ __msg("28: {{.*}} R4={{[^)]*}}var_off=(0x2; 0x7fc){{.*}} R5={{[^)]*}}var_off=(0x2; 0x7fc)") /* Constant is added to R5 again, setting reg->off to 18. */ -__msg("29: {{.*}} R5=pkt(id=3,{{[^)]*}}var_off=(0x2; 0x7fc)") +__msg("29: {{.*}} R5=pkt(id=3,{{[^)]*}}var_off=(0x2; 0xffc)") /* And once more we add a variable; resulting {{[^)]*}}var_off * is still (4n), fixed offset is not changed. * Also, we create a new reg->id. @@ -359,7 +359,7 @@ __msg("7: {{.*}} R6={{[^)]*}}var_off=(0x0; 0x3fc)") __msg("8: {{.*}} R6={{[^)]*}}var_off=(0x2; 0x7fc)") /* Packet pointer has (4n+2) offset */ __msg("11: {{.*}} R5={{[^)]*}}var_off=(0x2; 0x7fc)") -__msg("12: {{.*}} R4={{[^)]*}}var_off=(0x2; 0x7fc)") +__msg("12: {{.*}} R4={{[^)]*}}var_off=(0x2; 0xffc)") /* At the time the word size load is performed from R5, * its total fixed offset is NET_IP_ALIGN + reg->off (0) * which is 2. Then the variable offset is (4n+2), so @@ -375,7 +375,7 @@ __msg("17: {{.*}} R6={{[^)]*}}var_off=(0x0; 0x3fc)") * another (4n+2). */ __msg("19: {{.*}} R5={{[^)]*}}var_off=(0x2; 0xffc)") -__msg("20: {{.*}} R4={{[^)]*}}var_off=(0x2; 0xffc)") +__msg("20: {{.*}} R4={{[^)]*}}var_off=(0x2; 0x1ffc)") /* At the time the word size load is performed from R5, * its total fixed offset is NET_IP_ALIGN + reg->off (0) * which is 2. Then the variable offset is (4n+2), so diff --git a/tools/testing/selftests/bpf/progs/verifier_arena.c b/tools/testing/selftests/bpf/progs/verifier_arena.c index 815f342eb4b0..3e33766547c0 100644 --- a/tools/testing/selftests/bpf/progs/verifier_arena.c +++ b/tools/testing/selftests/bpf/progs/verifier_arena.c @@ -562,6 +562,55 @@ int arena_ptr_add_arena_ptr(void *ctx) } SEC("syscall") +__failure __msg("same insn cannot be used with and without arena pointer") +int mixed_arena_scalar_alu64_scalar_first(void *ctx) +{ + volatile register __u64 reg asm("r3"); + __u32 pick_arena = bpf_get_prandom_u32(); + + reg = 1ULL << 32; + + if (pick_arena) { + asm volatile ( + "r9 = %[arena] ll;" + "%[reg] = 0;" + "%[reg] = addr_space_cast(%[reg], 0x0, 0x1);" + : [reg] "=r"(reg) + : __imm_addr(arena) + : "r9" + ); + } + + reg += 1; + + return 0; +} + +SEC("syscall") +__failure __msg("same insn cannot be used with and without arena pointer") +int mixed_arena_scalar_alu64_arena_first(void *ctx) +{ + volatile register __u64 reg asm("r3"); + __u32 pick_scalar = bpf_get_prandom_u32(); + + asm volatile ( + "r9 = %[arena] ll;" + "%[reg] = 0;" + "%[reg] = addr_space_cast(%[reg], 0x0, 0x1);" + : [reg] "=r"(reg) + : __imm_addr(arena) + : "r9" + ); + + if (pick_scalar) + reg = 1ULL << 32; + + reg += 1; + + return 0; +} + +SEC("syscall") __success __retval(0) int scalar_xor_arena_ptr(void *ctx) { diff --git a/tools/testing/selftests/bpf/progs/verifier_async_cb_context.c b/tools/testing/selftests/bpf/progs/verifier_async_cb_context.c index e0926767bbd3..1b653bfb63eb 100644 --- a/tools/testing/selftests/bpf/progs/verifier_async_cb_context.c +++ b/tools/testing/selftests/bpf/progs/verifier_async_cb_context.c @@ -9,6 +9,11 @@ char _license[] SEC("license") = "GPL"; +struct task_struct *bpf_task_acquire(struct task_struct *p) __ksym; +void bpf_task_release(struct task_struct *p) __ksym; +void bpf_rcu_read_lock(void) __ksym; +void bpf_rcu_read_unlock(void) __ksym; + /* Timer tests */ struct timer_elem { @@ -164,6 +169,7 @@ int syscall_btf_find_prog(void *ctx) struct wq_elem { struct bpf_wq w; + struct task_struct __kptr *task; }; struct { @@ -217,6 +223,106 @@ int wq_sleepable_prog(void *ctx) return 0; } +__noinline int wq_global_acquire(void) +{ + struct task_struct *task, *acquired; + struct wq_elem *val; + int key = 0; + + val = bpf_map_lookup_elem(&wq_map, &key); + if (!val) + return 0; + + task = val->task; + if (!task) + return 0; + + acquired = bpf_task_acquire(task); + if (acquired) + bpf_task_release(acquired); + return 0; +} + +static int wq_global_rcu_cb(void *map, int *key, void *value) +{ + wq_global_acquire(); + return 0; +} + +SEC("fentry/bpf_fentry_test1") +__failure __msg("R1 must be a rcu pointer") +int wq_global_rcu_prog(void *ctx) +{ + struct wq_elem *val; + int key = 0; + + val = bpf_map_lookup_elem(&wq_map, &key); + if (!val) + return 0; + + bpf_wq_init(&val->w, &wq_map, 0); + bpf_wq_set_callback(&val->w, wq_global_rcu_cb, 0); + return 0; +} + +static int wq_global_rcu_lock_cb(void *map, int *key, void *value) +{ + bpf_rcu_read_lock(); + wq_global_acquire(); + bpf_rcu_read_unlock(); + return 0; +} + +SEC("fentry/bpf_fentry_test1") +__success +int wq_global_rcu_lock_prog(void *ctx) +{ + struct wq_elem *val; + int key = 0; + + /* Verify the same global subprog in non-sleepable and protected contexts. */ + wq_global_acquire(); + + val = bpf_map_lookup_elem(&wq_map, &key); + if (!val) + return 0; + + bpf_wq_init(&val->w, &wq_map, 0); + bpf_wq_set_callback(&val->w, wq_global_rcu_lock_cb, 0); + return 0; +} + +__weak __noinline int wq_global_no_rcu(void) +{ + return 0; +} + +static int wq_global_no_rcu_cb(void *map, int *key, void *value) +{ + wq_global_no_rcu(); + return 0; +} + +SEC("fentry/bpf_fentry_test1") +__success __log_level(4) +__msg("subprog {{[0-9]+}} (wq_global_no_rcu) global insns_self 4 insns_total 4 stack 0") +int wq_global_no_rcu_prog(void *ctx) +{ + struct wq_elem *val; + int key = 0; + + /* Verify the same global in non-sleepable and unprotected contexts. */ + wq_global_no_rcu(); + + val = bpf_map_lookup_elem(&wq_map, &key); + if (!val) + return 0; + + bpf_wq_init(&val->w, &wq_map, 0); + bpf_wq_set_callback(&val->w, wq_global_no_rcu_cb, 0); + return 0; +} + /* Task work tests */ struct task_work_elem { diff --git a/tools/testing/selftests/bpf/progs/verifier_btf_flex_array.c b/tools/testing/selftests/bpf/progs/verifier_btf_flex_array.c new file mode 100644 index 000000000000..59b84261f622 --- /dev/null +++ b/tools/testing/selftests/bpf/progs/verifier_btf_flex_array.c @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-2.0 + +#include <vmlinux.h> +#include <bpf/bpf_helpers.h> + +#include "bpf_experimental.h" +#include "bpf_misc.h" + +struct test_empty_event {}; + +struct test_flex_batch { + int nr; + struct test_empty_event events[]; +}; + +struct map_value { + struct test_flex_batch __kptr *batch; +}; + +struct { + __uint(type, BPF_MAP_TYPE_ARRAY); + __type(key, int); + __type(value, struct map_value); + __uint(max_entries, 1); +} batches SEC(".maps"); + +SEC("syscall") +__description("btf walk into flexible array of zero-sized elements") +__failure __msg("access beyond struct test_flex_batch at off 4 size 1") +int stash_and_peek(void *ctx) +{ + struct test_flex_batch *b, *old; + struct map_value *v; + int key = 0; + + v = bpf_map_lookup_elem(&batches, &key); + if (!v) + return 0; + + b = bpf_obj_new(struct test_flex_batch); + if (!b) + return 0; + b->nr = 1; + + old = bpf_kptr_xchg(&v->batch, b); + if (old) + bpf_obj_drop(old); + + b = v->batch; + if (!b) + return 0; + + return b->nr + *(char *)&b->events[0]; +} + +char _license[] SEC("license") = "GPL"; diff --git a/tools/testing/selftests/bpf/progs/verifier_direct_packet_access.c b/tools/testing/selftests/bpf/progs/verifier_direct_packet_access.c index 915a9707298b..139ff019d87d 100644 --- a/tools/testing/selftests/bpf/progs/verifier_direct_packet_access.c +++ b/tools/testing/selftests/bpf/progs/verifier_direct_packet_access.c @@ -920,4 +920,182 @@ l1_%=: r0 = *(u8*)(r9 + 0); \ : __clobber_all); } +SEC("tc") +__description("direct packet access: 8-aligned offset, check p + 8, load 8 bytes at p") +__success __retval(0) __flag(BPF_F_ANY_ALIGNMENT) +__naked void pkt_same_id_check_copy_load_base(void) +{ + asm volatile (" \ + r2 = *(u32*)(r1 + %[__sk_buff_data]); \ + r3 = *(u32*)(r1 + %[__sk_buff_data_end]); \ + r4 = *(u32*)(r1 + %[__sk_buff_mark]); \ + r4 &= 0x38; \ + if r4 > 50 goto l0_%=; \ + /* r4 is a multiple of 8, at most 48 */ \ + r5 = r2; \ + r5 += r4; \ + r6 = r5; \ + r6 += 8; \ + if r6 > r3 goto l0_%=; \ + /* [r5, r5 + 8) is in the packet */ \ + r0 = *(u64*)(r5 + 0); \ +l0_%=: r0 = 0; \ + exit; \ +" : + : __imm_const(__sk_buff_data, offsetof(struct __sk_buff, data)), + __imm_const(__sk_buff_data_end, offsetof(struct __sk_buff, data_end)), + __imm_const(__sk_buff_mark, offsetof(struct __sk_buff, mark)) + : __clobber_all); +} + +SEC("tc") +__description("direct packet access: 8-aligned offset, check p, load past it via p + 8") +__failure __msg("invalid access to packet, off=51 size=1") +__naked void pkt_same_id_check_base_load_via_copy(void) +{ + asm volatile (" \ + r2 = *(u32*)(r1 + %[__sk_buff_data]); \ + r3 = *(u32*)(r1 + %[__sk_buff_data_end]); \ + r4 = *(u32*)(r1 + %[__sk_buff_mark]); \ + r4 &= 0x38; \ + if r4 > 50 goto l0_%=; \ + /* r4 is a multiple of 8, at most 48 */ \ + r5 = r2; \ + r5 += r4; \ + r6 = r5; \ + r6 += 8; \ + if r5 > r3 goto l0_%=; \ + /* r6 - 7 is r5 + 1 */ \ + r0 = *(u8*)(r6 - 7); \ +l0_%=: r0 = 0; \ + exit; \ +" : + : __imm_const(__sk_buff_data, offsetof(struct __sk_buff, data)), + __imm_const(__sk_buff_data_end, offsetof(struct __sk_buff, data_end)), + __imm_const(__sk_buff_mark, offsetof(struct __sk_buff, mark)) + : __clobber_all); +} + +SEC("tc") +__description("direct packet access: 8-aligned offset, check p + 4, 4-byte load at p + 2") +__failure __msg("invalid access to packet, off=52 size=4") +__naked void pkt_same_id_check_base_4_load_via_copy(void) +{ + asm volatile (" \ + r2 = *(u32*)(r1 + %[__sk_buff_data]); \ + r3 = *(u32*)(r1 + %[__sk_buff_data_end]); \ + r4 = *(u32*)(r1 + %[__sk_buff_mark]); \ + r4 &= 0x38; \ + if r4 > 50 goto l0_%=; \ + /* r4 is a multiple of 8, at most 48 */ \ + r5 = r2; \ + r5 += r4; \ + r6 = r5; \ + r6 += 8; \ + r7 = r5; \ + r7 += 4; \ + if r7 > r3 goto l0_%=; \ + /* [r5, r5 + 4) is in the packet, [r5 + 2, r5 + 6) may not be */ \ + r0 = *(u32*)(r6 - 6); \ +l0_%=: r0 = 0; \ + exit; \ +" : + : __imm_const(__sk_buff_data, offsetof(struct __sk_buff, data)), + __imm_const(__sk_buff_data_end, offsetof(struct __sk_buff, data_end)), + __imm_const(__sk_buff_mark, offsetof(struct __sk_buff, mark)) + : __clobber_all); +} + +SEC("tc") +__description("direct packet access: checked pointer minus non-negative unknown keeps range") +__success __retval(0) __flag(BPF_F_ANY_ALIGNMENT) +__naked void pkt_sub_unknown_keeps_range(void) +{ + asm volatile (" \ + r2 = *(u32*)(r1 + %[__sk_buff_data]); \ + r3 = *(u32*)(r1 + %[__sk_buff_data_end]); \ + r4 = *(u32*)(r1 + %[__sk_buff_mark]); \ + r4 &= 0x1f; \ + r4 += 8; \ + r5 = r2; \ + r5 += 40; \ + if r5 > r3 goto l0_%=; \ + /* r5 is 8 to 39 bytes below the checked pointer */ \ + r5 -= r4; \ + r0 = *(u64*)(r5 + 0); \ +l0_%=: r0 = 0; \ + exit; \ +" : + : __imm_const(__sk_buff_data, offsetof(struct __sk_buff, data)), + __imm_const(__sk_buff_data_end, offsetof(struct __sk_buff, data_end)), + __imm_const(__sk_buff_mark, offsetof(struct __sk_buff, mark)) + : __clobber_all); +} + +SEC("tc") +__description("direct packet access: no pruning of a path whose checks prove fewer bytes") +__failure __msg("invalid access to packet, off=255 size=8") +__flag(BPF_F_ANY_ALIGNMENT) __flag(BPF_F_TEST_STATE_FREQ) +__naked void pkt_same_id_pruning(void) +{ + asm volatile (" \ + r2 = *(u32*)(r1 + %[__sk_buff_data]); \ + r3 = *(u32*)(r1 + %[__sk_buff_data_end]); \ + r4 = *(u32*)(r1 + %[__sk_buff_mark]); \ + r0 = *(u32*)(r1 + %[__sk_buff_priority]); \ + r4 &= 0xff; \ + r5 = r2; \ + r5 += r4; \ + if r0 != 0 goto l1_%=; \ + /* this path proves [r5, r5 + 8) */ \ + r6 = r5; \ + r6 += 8; \ + if r6 > r3 goto l0_%=; \ + goto l2_%=; \ +l1_%=: /* this path proves [r5, r5 + 7) */ \ + if r5 > r3 goto l0_%=; \ + r6 = r5; \ + r6 += 7; \ + if r6 > r3 goto l0_%=; \ +l2_%=: r0 = *(u64*)(r5 + 0); \ +l0_%=: r0 = 0; \ + exit; \ +" : + : __imm_const(__sk_buff_data, offsetof(struct __sk_buff, data)), + __imm_const(__sk_buff_data_end, offsetof(struct __sk_buff, data_end)), + __imm_const(__sk_buff_mark, offsetof(struct __sk_buff, mark)), + __imm_const(__sk_buff_priority, offsetof(struct __sk_buff, priority)) + : __clobber_all); +} + +SEC("tc") +__description("direct packet access: spilled copy of checked pointer, load past range") +__failure __msg("invalid access to packet, off=262 size=1, R7(id={{[0-9]+}},off=262,r=262)") +__naked void pkt_spilled_copy_gets_range(void) +{ + asm volatile (" \ + r2 = *(u32*)(r1 + %[__sk_buff_data]); \ + r3 = *(u32*)(r1 + %[__sk_buff_data_end]); \ + r4 = *(u32*)(r1 + %[__sk_buff_mark]); \ + r4 &= 0xff; \ + r5 = r2; \ + r5 += r4; \ + *(u64*)(r10 - 8) = r5; \ + /* proves only bytes before r5 */ \ + if r5 > r3 goto l0_%=; \ + r6 = r5; \ + r6 += 7; \ + if r6 > r3 goto l0_%=; \ + /* [r5, r5 + 7) is in the packet */ \ + r7 = *(u64*)(r10 - 8); \ + r0 = *(u8*)(r7 + 7); \ +l0_%=: r0 = 0; \ + exit; \ +" : + : __imm_const(__sk_buff_data, offsetof(struct __sk_buff, data)), + __imm_const(__sk_buff_data_end, offsetof(struct __sk_buff, data_end)), + __imm_const(__sk_buff_mark, offsetof(struct __sk_buff, mark)) + : __clobber_all); +} + char _license[] SEC("license") = "GPL"; diff --git a/tools/testing/selftests/bpf/progs/verifier_global_ptr_args.c b/tools/testing/selftests/bpf/progs/verifier_global_ptr_args.c index a3d2af8dc839..dcc2dd46751a 100644 --- a/tools/testing/selftests/bpf/progs/verifier_global_ptr_args.c +++ b/tools/testing/selftests/bpf/progs/verifier_global_ptr_args.c @@ -350,4 +350,57 @@ int anything_to_untrusted_mem(void *ctx) return 0; } +struct pkt_arg { + __u64 x; + __u8 pad[56]; +}; + +__weak int subprog_pkt_ptr_no_change(struct pkt_arg *p) +{ + if (!p) + return 0; + + return p->x; +} + +SEC("?tc") +__success +int pkt_ptr_to_global_mem_arg_no_change(struct __sk_buff *skb) +{ + void *data = (void *)(long)skb->data; + void *data_end = (void *)(long)skb->data_end; + struct pkt_arg *p = data; + + if ((void *)(p + 1) > data_end) + return 0; + + return subprog_pkt_ptr_no_change(p); +} + +__weak int subprog_pkt_ptr_changes_data(struct __sk_buff *skb __arg_ctx, + struct pkt_arg *p) +{ + if (!p) + return 0; + + bpf_skb_pull_data(skb, 0); + return p->x; +} + +SEC("?tc") +__failure __log_level(2) +__msg("R2 is a packet pointer, but func#{{[0-9]+}} may change packet data") +__msg("Caller passes invalid args into func#{{[0-9]+}} ('subprog_pkt_ptr_changes_data')") +int pkt_ptr_to_global_mem_arg_changes_data(struct __sk_buff *skb) +{ + void *data = (void *)(long)skb->data; + void *data_end = (void *)(long)skb->data_end; + struct pkt_arg *p = data; + + if ((void *)(p + 1) > data_end) + return 0; + + return subprog_pkt_ptr_changes_data(skb, p); +} + char _license[] SEC("license") = "GPL"; diff --git a/tools/testing/selftests/bpf/progs/verifier_gotox.c b/tools/testing/selftests/bpf/progs/verifier_gotox.c index 5b18c9a27717..0e27c2c79c57 100644 --- a/tools/testing/selftests/bpf/progs/verifier_gotox.c +++ b/tools/testing/selftests/bpf/progs/verifier_gotox.c @@ -47,6 +47,54 @@ DEFINE_SIMPLE_JUMP_TABLE_PROG(reserved_field_src_reg, BPF_REG_1, 0, 0, __fa DEFINE_SIMPLE_JUMP_TABLE_PROG(reserved_field_non_zero_off, BPF_REG_0, 1, 0, __failure __msg("BPF_JA|BPF_X uses reserved fields")) DEFINE_SIMPLE_JUMP_TABLE_PROG(reserved_field_non_zero_imm, BPF_REG_0, 0, 1, __failure __msg("BPF_JA|BPF_X uses reserved fields")) +#define DEFINE_TERMINAL_GOTOX_PROG(NAME, BASE) \ + __naked void NAME(void) \ + { \ + asm volatile (" \ + .pushsection .jumptables,\"\",@progbits; \ +jt0_%=: \ + .quad ret0_%= - " BASE "; \ + .size jt0_%=, 8; \ + .global jt0_%=; \ + .popsection; \ + \ + r0 = jt0_%= ll; \ + r0 = *(u64 *)(r0 + 0); \ + goto end_%=; \ +ret0_%=: \ + r0 = 0; \ + exit; \ +end_%=: \ + .8byte %[gotox_r0]; \ +" : \ + : __imm_insn(gotox_r0, BPF_RAW_INSN(BPF_JMP | BPF_JA | BPF_X, \ + BPF_REG_0, 0, 0, 0)) \ + : __clobber_all); \ + } + +SEC("socket") +__success __retval(0) +DEFINE_TERMINAL_GOTOX_PROG(jump_table_terminal_gotox, "socket") + +static __noinline __used +DEFINE_TERMINAL_GOTOX_PROG(terminal_gotox_subprog1, ".text") + +static __noinline __used int terminal_gotox_subprog2(void) +{ + return 0; +} + +SEC("socket") +__success __retval(0) +__naked void jump_table_terminal_gotox_subprog(void) +{ + asm volatile (" \ + call terminal_gotox_subprog1; \ + call terminal_gotox_subprog2; \ + exit; \ +" ::: __clobber_all); +} + /* * Gotox is forbidden when there is no jump table loaded * which points to the sub-function where the gotox is used diff --git a/tools/testing/selftests/bpf/progs/verifier_kfunc_perfmon.c b/tools/testing/selftests/bpf/progs/verifier_kfunc_perfmon.c new file mode 100644 index 000000000000..76c39ef30e96 --- /dev/null +++ b/tools/testing/selftests/bpf/progs/verifier_kfunc_perfmon.c @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: GPL-2.0 + +#include <vmlinux.h> +#include <bpf/bpf_helpers.h> +#include "bpf_misc.h" + +void *user_ptr; +char dynptr_buf[8]; +u64 kaddr; + +extern struct kmem_cache *bpf_get_kmem_cache(u64 addr) __ksym; + +SEC("socket") +__success +__caps_unpriv(CAP_BPF) +__failure_unpriv +__msg_unpriv("bpf_rdonly_cast is allowed only to CAP_PERFMON and CAP_SYS_ADMIN") +int rdonly_cast_noperfmon(void *ctx) +{ + char *p = bpf_rdonly_cast(0, 0); + + return p[0x7fff]; +} + +SEC("socket") +__success +__caps_unpriv(CAP_BPF) +__failure_unpriv +__msg_unpriv("bpf_probe_read_kernel_dynptr is allowed only to CAP_PERFMON and CAP_SYS_ADMIN") +int probe_read_kernel_dynptr_noperfmon(void *ctx) +{ + struct bpf_dynptr dptr; + + bpf_dynptr_from_mem(dynptr_buf, sizeof(dynptr_buf), 0, &dptr); + bpf_probe_read_kernel_dynptr(&dptr, 0, sizeof(dynptr_buf), user_ptr); + return 0; +} + +SEC("socket") +__success +__caps_unpriv(CAP_BPF) +__failure_unpriv +__msg_unpriv("bpf_stream_vprintk is allowed only to CAP_PERFMON and CAP_SYS_ADMIN") +int stream_vprintk_noperfmon(void *ctx) +{ + bpf_stream_printk(BPF_STDOUT, "%pB", (void *)kaddr); + return 0; +} + +SEC("socket") +__success +__caps_unpriv(CAP_BPF) +__failure_unpriv +__msg_unpriv("bpf_get_kmem_cache is allowed only to CAP_PERFMON and CAP_SYS_ADMIN") +int get_kmem_cache_noperfmon(void *ctx) +{ + return !!bpf_get_kmem_cache(kaddr); +} + +__weak int subprog_untrusted_read(void *p __arg_untrusted) +{ + return *(char *)p; +} + +SEC("socket") +__success +__caps_unpriv(CAP_BPF) +__failure_unpriv +__msg_unpriv("rdonly_untrusted_mem access is allowed only to CAP_PERFMON and CAP_SYS_ADMIN") +int arg_untrusted_read_noperfmon(void *ctx) +{ + return subprog_untrusted_read(0); +} + +char _license[] SEC("license") = "GPL"; diff --git a/tools/testing/selftests/bpf/progs/verifier_loops1.c b/tools/testing/selftests/bpf/progs/verifier_loops1.c index d248ce877f14..48a966cda199 100644 --- a/tools/testing/selftests/bpf/progs/verifier_loops1.c +++ b/tools/testing/selftests/bpf/progs/verifier_loops1.c @@ -303,4 +303,40 @@ __naked void maybe_exit_scc_bug1(void) ::: __clobber_all); } +/* + * The loop reads zero from the caller's stack on its first iteration and + * one from the callee's stack on its second iteration. At the loop header, + * only the frame number of the pointer in r1 changes. + */ +static __naked __noinline __used +void loop_stack_frames_reg(void) +{ + asm volatile ( + "*(u64 *)(r10 - 8) = 1;" +"1:" + "r0 = *(u64 *)(r1 + 0);" + "if r0 != 0 goto 2f;" + "r1 = r10;" + "r1 += -8;" + "goto 1b;" +"2:" + "exit;" + ::: __clobber_all); +} + +SEC("xdp") +__description("bounded loop changing stack frame in a register") +__success __retval(1) +__flag(BPF_F_TEST_STATE_FREQ) +__naked void bounded_loop_stack_frames_reg(void) +{ + asm volatile ( + "*(u64 *)(r10 - 8) = 0;" + "r1 = r10;" + "r1 += -8;" + "call loop_stack_frames_reg;" + "exit;" + ::: __clobber_all); +} + char _license[] SEC("license") = "GPL"; diff --git a/tools/testing/selftests/bpf/progs/verifier_meta_access.c b/tools/testing/selftests/bpf/progs/verifier_meta_access.c index 62235f032ffe..c87e0be4b2ff 100644 --- a/tools/testing/selftests/bpf/progs/verifier_meta_access.c +++ b/tools/testing/selftests/bpf/progs/verifier_meta_access.c @@ -281,4 +281,34 @@ l0_%=: r0 = 0; \ : __clobber_all); } +SEC("xdp") +__description("meta access, 8-aligned offset, check p, load a byte at p + 1 via p + 8") +__failure __msg("invalid access to packet, off=51 size=1") +__naked void meta_access_check_base_load_via_copy(void) +{ + asm volatile (" \ + r9 = r1; \ + call %[bpf_get_prandom_u32]; \ + r4 = r0; \ + r4 &= 0x38; \ + if r4 > 50 goto l0_%=; \ + /* r4 is a multiple of 8, at most 48 */ \ + r2 = *(u32*)(r9 + %[xdp_md_data_meta]); \ + r3 = *(u32*)(r9 + %[xdp_md_data]); \ + r5 = r2; \ + r5 += r4; \ + r6 = r5; \ + r6 += 8; \ + if r5 > r3 goto l0_%=; \ + /* r6 - 7 is r5 + 1 */ \ + r0 = *(u8*)(r6 - 7); \ +l0_%=: r0 = 0; \ + exit; \ +" : + : __imm(bpf_get_prandom_u32), + __imm_const(xdp_md_data, offsetof(struct xdp_md, data)), + __imm_const(xdp_md_data_meta, offsetof(struct xdp_md, data_meta)) + : __clobber_all); +} + char _license[] SEC("license") = "GPL"; diff --git a/tools/testing/selftests/bpf/progs/verifier_sock.c b/tools/testing/selftests/bpf/progs/verifier_sock.c index 4f2f3209eec8..bf9f6fb6582c 100644 --- a/tools/testing/selftests/bpf/progs/verifier_sock.c +++ b/tools/testing/selftests/bpf/progs/verifier_sock.c @@ -88,6 +88,44 @@ l0_%=: r0 = *(u32*)(r1 + %[bpf_sock_family]); \ : __clobber_all); } +SEC("socket") +__description("skb->sk: sk->rx_queue_mapping [no sign extension]") +__success __success_unpriv __retval(0) +__naked void sk_rx_queue_mapping_no_sign_ext(void) +{ + asm volatile (" \ + r1 = *(u64*)(r1 + %[__sk_buff_sk]); \ + if r1 != 0 goto l0_%=; \ + r0 = 0xdead; \ + exit; \ +l0_%=: r0 = *(u32*)(r1 + %[bpf_sock_rx_queue_mapping]); \ + r0 >>= 32; \ + exit; \ +" : + : __imm_const(__sk_buff_sk, offsetof(struct __sk_buff, sk)), + __imm_const(bpf_sock_rx_queue_mapping, offsetof(struct bpf_sock, rx_queue_mapping)) + : __clobber_all); +} + +SEC("socket") +__description("skb->sk: sk->rx_queue_mapping [narrow load mask]") +__success __success_unpriv __retval(0) +__naked void sk_rx_queue_mapping_narrow_load_mask(void) +{ + asm volatile (" \ + r1 = *(u64*)(r1 + %[__sk_buff_sk]); \ + if r1 != 0 goto l0_%=; \ + r0 = 0xdead; \ + exit; \ +l0_%=: r0 = *(u16*)(r1 + %[bpf_sock_rx_queue_mapping]); \ + r0 >>= 16; \ + exit; \ +" : + : __imm_const(__sk_buff_sk, offsetof(struct __sk_buff, sk)), + __imm_const(bpf_sock_rx_queue_mapping, offsetof(struct bpf_sock, rx_queue_mapping)) + : __clobber_all); +} + SEC("cgroup/skb") __description("skb->sk: sk->type [fullsock field]") __failure __msg("invalid sock_common access") diff --git a/tools/testing/selftests/bpf/progs/verifier_xdp_direct_packet_access.c b/tools/testing/selftests/bpf/progs/verifier_xdp_direct_packet_access.c index 0b86d95a4133..9866bc154194 100644 --- a/tools/testing/selftests/bpf/progs/verifier_xdp_direct_packet_access.c +++ b/tools/testing/selftests/bpf/progs/verifier_xdp_direct_packet_access.c @@ -1719,4 +1719,39 @@ l0_%=: r0 = 0; \ : __clobber_all); } +SEC("xdp") +__description("XDP pkt regsafe preserves packet pointer class displacement") +__failure __msg("R2 min value is outside of the allowed memory range") +__flag(BPF_F_ANY_ALIGNMENT) __flag(BPF_F_TEST_STATE_FREQ) +__naked void pkt_regsafe_class_displacement(void) +{ + asm volatile (" \ + r8 = *(u32 *)(r1 + %[xdp_md_data_end]); \ + r9 = *(u32 *)(r1 + %[xdp_md_data]); \ + r4 = *(u32 *)(r1 + %[xdp_md_rx_queue_index]); \ + r4 &= 15; \ + r0 = *(u32 *)(r1 + %[xdp_md_ingress_ifindex]); \ + if r0 != 0 goto l0_%=; \ + r2 = r9; \ + r2 += r4; \ + r3 = r2; \ + r3 += 8; \ + goto l1_%=; \ +l0_%=: r4 &= 3; \ + r4 += 8; \ + r2 = r9; \ + r2 += r4; \ + r3 = r2; \ +l1_%=: if r3 > r8 goto l2_%=; \ + r0 = *(u64 *)(r2 + 0); \ +l2_%=: r0 = 0; \ + exit; \ +" : + : __imm_const(xdp_md_data, offsetof(struct xdp_md, data)), + __imm_const(xdp_md_data_end, offsetof(struct xdp_md, data_end)), + __imm_const(xdp_md_rx_queue_index, offsetof(struct xdp_md, rx_queue_index)), + __imm_const(xdp_md_ingress_ifindex, offsetof(struct xdp_md, ingress_ifindex)) + : __clobber_all); +} + char _license[] SEC("license") = "GPL"; diff --git a/tools/testing/selftests/bpf/testing_helpers.c b/tools/testing/selftests/bpf/testing_helpers.c index c970e7793dfc..9f672d0c9dda 100644 --- a/tools/testing/selftests/bpf/testing_helpers.c +++ b/tools/testing/selftests/bpf/testing_helpers.c @@ -13,6 +13,7 @@ #include "test_progs.h" #include "testing_helpers.h" #include <linux/membarrier.h> +#include <linux/userfaultfd.h> int parse_num_list(const char *s, bool **num_set, int *num_set_len) { @@ -459,6 +460,33 @@ int kern_sync_rcu(void) return syscall(__NR_membarrier, MEMBARRIER_CMD_SHARED, 0, 0); } +int uffd_block_page(void *fault_addr) +{ + struct uffdio_register uffd_register = {}; + struct uffdio_api uffd_api = {}; + int uffd; + + uffd = syscall(__NR_userfaultfd, O_CLOEXEC); + if (uffd < 0) + return -errno; + + uffd_api.api = UFFD_API; + uffd_api.features = 0; + if (ioctl(uffd, UFFDIO_API, &uffd_api)) { + close(uffd); + return -1; + } + + uffd_register.range.start = (unsigned long)fault_addr; + uffd_register.range.len = getpagesize(); + uffd_register.mode = UFFDIO_REGISTER_MODE_MISSING; + if (ioctl(uffd, UFFDIO_REGISTER, &uffd_register)) { + close(uffd); + return -1; + } + return uffd; +} + int get_xlated_program(int fd_prog, struct bpf_insn **buf, __u32 *cnt) { __u32 buf_element_size = sizeof(struct bpf_insn); diff --git a/tools/testing/selftests/bpf/testing_helpers.h b/tools/testing/selftests/bpf/testing_helpers.h index 2edc6fb7fc52..1f83b6080716 100644 --- a/tools/testing/selftests/bpf/testing_helpers.h +++ b/tools/testing/selftests/bpf/testing_helpers.h @@ -36,6 +36,8 @@ __u64 read_perf_max_sample_freq(void); int load_bpf_testmod(bool verbose); int unload_bpf_testmod(bool verbose); int kern_sync_rcu(void); +/* returns a userfaultfd that makes accesses to fault_addr's page block */ +int uffd_block_page(void *fault_addr); int finit_module(int fd, const char *param_values, int flags); int delete_module(const char *name, int flags); int load_module(const char *path, bool verbose); diff --git a/tools/testing/selftests/cgroup/test_cpuset_prs.sh b/tools/testing/selftests/cgroup/test_cpuset_prs.sh index 131d8b4551ef..7efd5e645767 100755 --- a/tools/testing/selftests/cgroup/test_cpuset_prs.sh +++ b/tools/testing/selftests/cgroup/test_cpuset_prs.sh @@ -298,6 +298,8 @@ TEST_MATRIX=( " C0-4:X2-4 C1-4:X2-4:P2 C2-4:X4:P1 \ . . . X1 . 0 A1:0-1|A2:2-4|A3:2-4 \ A1:P0|A2:P2|A3:P-1 2-4" + " CX1-3:P1 CX1-3 CX1-3 . . . P1 . 0 A1:1-3|A2:1-3|A3:1-3 \ + A1:P1|A2:P0|A3:P-1" # Remote partition offline tests " C0-3 C1-3 C2-3 . X2-3 X2-3 X2-3:P2:O2=0 . 0 A1:0-1|A2:1|A3:3 A1:P0|A3:P2 2-3" diff --git a/tools/testing/selftests/cgroup/test_memcontrol.c b/tools/testing/selftests/cgroup/test_memcontrol.c index 3a84d068fbf3..0ed82347044e 100644 --- a/tools/testing/selftests/cgroup/test_memcontrol.c +++ b/tools/testing/selftests/cgroup/test_memcontrol.c @@ -30,7 +30,7 @@ static int page_size; int get_temp_fd(void) { - return open(".", O_TMPFILE | O_RDWR | O_EXCL); + return open(".", O_TMPFILE | O_RDWR | O_EXCL, 0600); } int alloc_pagecache(int fd, size_t size) diff --git a/tools/testing/selftests/cgroup/test_zswap.c b/tools/testing/selftests/cgroup/test_zswap.c index 609c48f38524..8df54b59513a 100644 --- a/tools/testing/selftests/cgroup/test_zswap.c +++ b/tools/testing/selftests/cgroup/test_zswap.c @@ -20,6 +20,7 @@ static int page_size; #define PATH_ZSWAP "/sys/module/zswap" #define PATH_ZSWAP_ENABLED "/sys/module/zswap/parameters/enabled" +#define PATH_ZSWAP_SHRINKER_ENABLED "/sys/module/zswap/parameters/shrinker_enabled" #define PATH_ZSWAP_STORED_PAGES "/sys/kernel/debug/zswap/stored_pages" static int read_int(const char *path, size_t *value) @@ -444,6 +445,16 @@ static int test_zswap_writeback_disabled(const char *root) return test_zswap_writeback(root, false); } +static bool zswap_shrinker_enabled(void) +{ + char value[2]; + + if (read_text(PATH_ZSWAP_SHRINKER_ENABLED, value, sizeof(value)) <= 0) + return 0; + + return value[0] == 'Y'; +} + /* * When trying to store a memcg page in zswap, if the memcg hits its memory * limit in zswap, writeback should affect only the zswapped pages of that @@ -453,6 +464,7 @@ static int test_no_invasive_cgroup_shrink(const char *root) { int ret = KSFT_FAIL; unsigned int off; + long zswpwb_before, zswpwb_after, zswpwb_target; size_t allocation_size = page_size * 1024; unsigned int nr_pages = allocation_size / page_size; char zswap_max_buf[32], mem_max_buf[32]; @@ -488,6 +500,14 @@ static int test_no_invasive_cgroup_shrink(const char *root) if (cg_read_key_long(zw_group, "memory.stat", "zswapped") < 1) goto out; + /* If the shrinker is enabled, try to let the writebacks finish first */ + if (zswap_shrinker_enabled()) + sleep(5); + + zswpwb_before = get_cg_wb_count(zw_group); + if (zswpwb_before < 0) + goto out; + /* Push wb_group memory into zswap with hard-to-compress data to trigger wb */ if (cg_enter_current(wb_group)) goto out; @@ -500,9 +520,13 @@ static int test_no_invasive_cgroup_shrink(const char *root) getrandom(&wb_allocation[off], page_size/4, 0); } - /* Verify that only zswapped memory from gwb_group has been written back */ - if (wait_for_writeback(wb_group, 5000) > 0 && get_cg_wb_count(zw_group) == 0) + /* Verify that only zswapped memory from wb_group has been written back */ + zswpwb_target = wait_for_writeback(wb_group, 5000); + zswpwb_after = get_cg_wb_count(zw_group); + + if (zswpwb_target > 0 && zswpwb_before == zswpwb_after) ret = KSFT_PASS; + out: cg_enter_current(root); if (zw_group) { diff --git a/tools/testing/selftests/drivers/net/config b/tools/testing/selftests/drivers/net/config index b6989c7d3d9d..4838adf27fa1 100644 --- a/tools/testing/selftests/drivers/net/config +++ b/tools/testing/selftests/drivers/net/config @@ -21,5 +21,6 @@ CONFIG_NET_SCH_INGRESS=y CONFIG_NET_SCH_PRIO=m CONFIG_PPP=y CONFIG_PPPOE=y +CONFIG_TLS=y CONFIG_VLAN_8021Q=m CONFIG_XDP_SOCKETS=y diff --git a/tools/testing/selftests/drivers/net/mlxsw/port_range_occ.sh b/tools/testing/selftests/drivers/net/mlxsw/port_range_occ.sh index b1f0781f6b25..c7eba3f15066 100755 --- a/tools/testing/selftests/drivers/net/mlxsw/port_range_occ.sh +++ b/tools/testing/selftests/drivers/net/mlxsw/port_range_occ.sh @@ -9,6 +9,7 @@ lib_dir=$(dirname $0)/../../../net/forwarding ALL_TESTS=" port_range_occ_test + port_range_occ_tmplt_test " NUM_NETIFS=2 source $lib_dir/lib.sh @@ -101,6 +102,31 @@ port_range_occ_test() log_test "port range occupancy" } +port_range_occ_tmplt_test() +{ + local occ + + RET=0 + occ=$(port_range_occ_get) + + # Addition of a chain template should not impact occupancy. + tc chain add dev "$swp1" ingress chain 1 protocol ip \ + flower ip_proto udp src_port 501-600 dst_port 701-800 + check_err $? "Could not add chain template" + + (( occ == $(port_range_occ_get) )) + check_err $? "Got occupancy $(port_range_occ_get), expected $occ" + + # And just for completeness sake, neither should deletion. + tc chain del dev "$swp1" ingress chain 1 + check_err $? "Could not delete chain template" + + (( occ == $(port_range_occ_get) )) + check_err $? "Got occupancy $(port_range_occ_get), expected $occ" + + log_test "port range occupancy after template create/destroy" +} + trap cleanup EXIT setup_prepare diff --git a/tools/testing/selftests/drivers/net/psp.py b/tools/testing/selftests/drivers/net/psp.py index 315648a770d0..a5b1e14f120f 100755 --- a/tools/testing/selftests/drivers/net/psp.py +++ b/tools/testing/selftests/drivers/net/psp.py @@ -23,6 +23,8 @@ from lib.py import NetNSEnter from lib.py import bkg, rand_port, wait_port_listen from lib.py import ip +TCP_ULP = 31 + def _get_outq(s): one = b'\0' * 4 @@ -333,6 +335,50 @@ def assoc_version_mismatch(cfg): ksft_eq(the_exception.nl_msg.error, -errno.EINVAL) +def _require_tls_ulp(): + with socket.create_server(("localhost", 0)) as srv, \ + socket.create_connection(srv.getsockname()) as s: + try: + s.setsockopt(socket.SOL_TCP, TCP_ULP, b"tls") + except OSError as exc: + raise KsftSkipEx("kTLS not available") from exc + + +def assoc_psp_ulp_exclusive(cfg): + """ Test that a TCP ULP cannot be attached to a PSP socket """ + _init_psp_dev(cfg) + _require_tls_ulp() + + with _make_clr_conn(cfg) as s: + try: + cfg.pspnl.rx_assoc({"version": 0, + "dev-id": cfg.psp_dev_id, + "sock-fd": s.fileno()}) + with ksft_raises(OSError) as cm: + s.setsockopt(socket.SOL_TCP, TCP_ULP, b"tls") + ksft_eq(cm.exception.errno, errno.EINVAL) + finally: + _close_conn(cfg, s) + + +def assoc_ulp_psp_exclusive(cfg): + """ Test that a PSP assoc cannot be added to a socket with a TCP ULP """ + _init_psp_dev(cfg) + _require_tls_ulp() + + with _make_clr_conn(cfg) as s: + try: + s.setsockopt(socket.SOL_TCP, TCP_ULP, b"tls") + with ksft_raises(NlError) as cm: + cfg.pspnl.rx_assoc({"version": 0, + "dev-id": cfg.psp_dev_id, + "sock-fd": s.fileno()}) + ksft_eq(cm.exception.nl_msg.error, -errno.EINVAL) + ksft_eq(cm.exception.nl_msg.extack['bad-attr'], ".sock-fd") + finally: + _close_conn(cfg, s) + + def assoc_twice(cfg): """ Test reusing Tx assoc for two sockets """ _init_psp_dev(cfg) diff --git a/tools/testing/selftests/filesystems/.gitignore b/tools/testing/selftests/filesystems/.gitignore index 9eb185fb2f9d..62f7b1c46649 100644 --- a/tools/testing/selftests/filesystems/.gitignore +++ b/tools/testing/selftests/filesystems/.gitignore @@ -7,3 +7,4 @@ anon_inode_test kernfs_test idmapped_tmpfile ustat_test +rw_hint_test diff --git a/tools/testing/selftests/filesystems/Makefile b/tools/testing/selftests/filesystems/Makefile index 03be337c1f35..93e2cc9123b0 100644 --- a/tools/testing/selftests/filesystems/Makefile +++ b/tools/testing/selftests/filesystems/Makefile @@ -1,7 +1,7 @@ # SPDX-License-Identifier: GPL-2.0 CFLAGS += $(KHDR_INCLUDES) -TEST_GEN_PROGS := devpts_pts file_stressor anon_inode_test kernfs_test fclog ustat_test +TEST_GEN_PROGS := devpts_pts file_stressor anon_inode_test kernfs_test fclog ustat_test rw_hint_test TEST_GEN_PROGS += idmapped_tmpfile TEST_GEN_PROGS_EXTENDED := dnotify_test diff --git a/tools/testing/selftests/filesystems/empty_mntns/.gitignore b/tools/testing/selftests/filesystems/empty_mntns/.gitignore index 99f89d329db2..27bcbcaeb1d1 100644 --- a/tools/testing/selftests/filesystems/empty_mntns/.gitignore +++ b/tools/testing/selftests/filesystems/empty_mntns/.gitignore @@ -2,3 +2,6 @@ clone3_empty_mntns_test empty_mntns_test overmount_chroot_test +internal_sb_reconfigure_test +nullfs_atime_test +root_readdir_test diff --git a/tools/testing/selftests/filesystems/empty_mntns/Makefile b/tools/testing/selftests/filesystems/empty_mntns/Makefile index 22e3fb915e81..22af2164c54e 100644 --- a/tools/testing/selftests/filesystems/empty_mntns/Makefile +++ b/tools/testing/selftests/filesystems/empty_mntns/Makefile @@ -4,9 +4,14 @@ CFLAGS += -Wall -O2 -g $(KHDR_INCLUDES) $(TOOLS_INCLUDES) LDLIBS += -lcap TEST_GEN_PROGS := empty_mntns_test overmount_chroot_test clone3_empty_mntns_test +TEST_GEN_PROGS += internal_sb_reconfigure_test nullfs_atime_test root_readdir_test + +LOCAL_HDRS += ../readdir_hold.h include ../../lib.mk $(OUTPUT)/empty_mntns_test: ../utils.c $(OUTPUT)/overmount_chroot_test: ../utils.c $(OUTPUT)/clone3_empty_mntns_test: ../utils.c +$(OUTPUT)/internal_sb_reconfigure_test: ../utils.c +$(OUTPUT)/root_readdir_test: LDLIBS += -pthread diff --git a/tools/testing/selftests/filesystems/empty_mntns/internal_sb_reconfigure_test.c b/tools/testing/selftests/filesystems/empty_mntns/internal_sb_reconfigure_test.c new file mode 100644 index 000000000000..cb645d1e5a9a --- /dev/null +++ b/tools/testing/selftests/filesystems/empty_mntns/internal_sb_reconfigure_test.c @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * The root of an empty mount namespace is a nullfs mount. Its superblock is + * kernel-internal and shared by every mount namespace. It can't be + * reconfigured, neither through fspick() nor through mount(MS_REMOUNT) nor + * through umount() of the root which remounts it read-only. + */ +#define _GNU_SOURCE +#include <errno.h> +#include <fcntl.h> +#include <sched.h> +#include <stdio.h> +#include <string.h> +#include <sys/mount.h> +#include <sys/statfs.h> +#include <sys/statvfs.h> +#include <sys/syscall.h> +#include <sys/vfs.h> +#include <sys/wait.h> +#include <unistd.h> + +#include "../utils.h" +#include "../wrappers.h" +#include "empty_mntns.h" +#include "kselftest_harness.h" + +#ifndef __NR_fspick +#define __NR_fspick 433 +#endif + +static int sys_fspick(int dfd, const char *path, unsigned int flags) +{ + return syscall(__NR_fspick, dfd, path, flags); +} + +/* Child exit codes. */ +enum { + CHILD_OK, + CHILD_USERNS, /* could not create the user namespace */ + CHILD_UNSHARE, /* could not create the empty mount namespace */ + CHILD_FSPICK, /* fspick() of the root was not refused with EINVAL */ + CHILD_REMOUNT, /* mount(MS_REMOUNT) was not refused with EINVAL */ + CHILD_UMOUNT, /* umount() of the root succeeded */ + CHILD_STATFS, /* statfs() of the root failed */ + CHILD_RDONLY, /* the root ended up read-only */ +}; + +static int empty_mntns_child(void) +{ + struct statfs st; + + if (enter_userns()) + return CHILD_USERNS; + if (unshare(UNSHARE_EMPTY_MNTNS)) + return CHILD_UNSHARE; + + if (sys_fspick(AT_FDCWD, "/", 0) >= 0 || errno != EINVAL) + return CHILD_FSPICK; + if (!mount(NULL, "/", NULL, MS_REMOUNT | MS_RDONLY, NULL) || + errno != EINVAL) + return CHILD_REMOUNT; + if (!umount2("/", 0)) + return CHILD_UMOUNT; + if (statfs("/", &st)) + return CHILD_STATFS; + if (st.f_flags & ST_RDONLY) + return CHILD_RDONLY; + return CHILD_OK; +} + +FIXTURE(internal_sb_reconfigure) {}; + +FIXTURE_SETUP(internal_sb_reconfigure) +{ + pid_t pid; + int status; + + pid = fork(); + ASSERT_GE(pid, 0); + if (pid == 0) { + if (enter_userns()) + _exit(1); + if (unshare(UNSHARE_EMPTY_MNTNS)) + _exit(1); + _exit(0); + } + ASSERT_EQ(waitpid(pid, &status, 0), pid); + if (!WIFEXITED(status) || WEXITSTATUS(status)) + SKIP(return, "UNSHARE_EMPTY_MNTNS not supported"); +} + +FIXTURE_TEARDOWN(internal_sb_reconfigure) {} + +TEST_F(internal_sb_reconfigure, nullfs_root) +{ + pid_t pid; + int status; + + pid = fork(); + ASSERT_GE(pid, 0); + if (pid == 0) + _exit(empty_mntns_child()); + ASSERT_EQ(waitpid(pid, &status, 0), pid); + ASSERT_TRUE(WIFEXITED(status)); + ASSERT_EQ(WEXITSTATUS(status), CHILD_OK); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/filesystems/empty_mntns/nullfs_atime_test.c b/tools/testing/selftests/filesystems/empty_mntns/nullfs_atime_test.c new file mode 100644 index 000000000000..51e34f3c4f54 --- /dev/null +++ b/tools/testing/selftests/filesystems/empty_mntns/nullfs_atime_test.c @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * The root of every empty mount namespace is the same nullfs inode. A read + * of it by one user must not change the access time another user sees. + */ +#define _GNU_SOURCE +#include <dirent.h> +#include <errno.h> +#include <fcntl.h> +#include <sched.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <sys/stat.h> +#include <sys/wait.h> + +#include "../../kselftest_harness.h" + +#ifndef UNSHARE_EMPTY_MNTNS +#define UNSHARE_EMPTY_MNTNS 0x00100000 +#endif + +enum { + CHILD_OK, + CHILD_UNSUPPORTED, + CHILD_SETUP, + CHILD_CHANGED, +}; + +static int wait_byte(int fd) +{ + char c; + + return read(fd, &c, 1) == 1 ? 0 : -1; +} + +static int send_byte(int fd) +{ + return write(fd, "x", 1) == 1 ? 0 : -1; +} + +static int empty_mntns(void) +{ + if (!unshare(UNSHARE_EMPTY_MNTNS)) + return 0; + return errno == EINVAL ? CHILD_UNSUPPORTED : CHILD_SETUP; +} + +/* the watcher: stats its root before and after the reader read its own */ +static int watcher(int to_reader, int from_reader) +{ + struct stat before, after; + int ret; + + ret = empty_mntns(); + if (ret) + return ret; + if (stat("/", &before)) + return CHILD_SETUP; + if (send_byte(to_reader) || wait_byte(from_reader)) + return CHILD_SETUP; + if (stat("/", &after)) + return CHILD_SETUP; + if (before.st_atim.tv_sec != after.st_atim.tv_sec || + before.st_atim.tv_nsec != after.st_atim.tv_nsec) + return CHILD_CHANGED; + return CHILD_OK; +} + +/* the reader: lists its own root, which is the same inode */ +static int reader(int to_watcher, int from_watcher) +{ + struct dirent *de; + DIR *d; + int ret; + + ret = empty_mntns(); + if (ret) + return ret; + if (wait_byte(from_watcher)) + return CHILD_SETUP; + d = opendir("/"); + if (!d) + return CHILD_SETUP; + while ((de = readdir(d))) + ; + closedir(d); + return send_byte(to_watcher) ? CHILD_SETUP : CHILD_OK; +} + +static int wait_child(pid_t pid) +{ + int status; + + if (waitpid(pid, &status, 0) != pid || !WIFEXITED(status)) + return -1; + return WEXITSTATUS(status); +} + +TEST(empty_mntns_root_atime) +{ + int to_reader[2], to_watcher[2], w, r; + pid_t watcher_pid, reader_pid; + + if (geteuid()) + SKIP(return, "test requires root"); + ASSERT_EQ(pipe(to_reader), 0); + ASSERT_EQ(pipe(to_watcher), 0); + + watcher_pid = fork(); + ASSERT_GE(watcher_pid, 0); + if (watcher_pid == 0) + _exit(watcher(to_reader[1], to_watcher[0])); + reader_pid = fork(); + ASSERT_GE(reader_pid, 0); + if (reader_pid == 0) + _exit(reader(to_watcher[1], to_reader[0])); + + r = wait_child(reader_pid); + w = wait_child(watcher_pid); + if (r == CHILD_UNSUPPORTED || w == CHILD_UNSUPPORTED) + SKIP(return, "UNSHARE_EMPTY_MNTNS not supported"); + EXPECT_EQ(r, CHILD_OK); + EXPECT_EQ(w, CHILD_OK) + TH_LOG("the access time of the root changed while this namespace did nothing"); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/filesystems/empty_mntns/root_readdir_test.c b/tools/testing/selftests/filesystems/empty_mntns/root_readdir_test.c new file mode 100644 index 000000000000..0ff544247027 --- /dev/null +++ b/tools/testing/selftests/filesystems/empty_mntns/root_readdir_test.c @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Reading the root of an empty mount namespace holds nothing that anybody + * else waits for: the directory never has an entry, so a readdir stuck in + * the page fault of its buffer stalls neither a create nor a lookup in it. + */ +#define _GNU_SOURCE +#include <errno.h> +#include <fcntl.h> +#include <sched.h> +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> + +#include "../../kselftest_harness.h" +#include "../readdir_hold.h" +#include "empty_mntns.h" + +TEST(readdir_blocks_nobody) +{ + struct readdir_hold hold; + bool stalled; + int dfd; + + if (geteuid()) + SKIP(return, "test requires root"); + if (readdir_hold_init(&hold)) + SKIP(return, "test requires userfaultfd"); + if (unshare(UNSHARE_EMPTY_MNTNS)) { + readdir_hold_destroy(&hold); + if (errno == EINVAL) + SKIP(return, "UNSHARE_EMPTY_MNTNS not supported"); + ASSERT_TRUE(false) + TH_LOG("unshare(UNSHARE_EMPTY_MNTNS): %m"); + } + dfd = open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC); + ASSERT_GE(dfd, 0); + ASSERT_EQ(readdir_hold_check(&hold, dfd, &stalled), 0); + /* the lookup came back while the readdir was stuck in its fault */ + EXPECT_FALSE(stalled); + close(dfd); + readdir_hold_destroy(&hold); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/filesystems/mntns_cleanup/.gitignore b/tools/testing/selftests/filesystems/mntns_cleanup/.gitignore deleted file mode 100644 index 493fbcf8d9ec..000000000000 --- a/tools/testing/selftests/filesystems/mntns_cleanup/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0-only -mntns_cleanup_test diff --git a/tools/testing/selftests/filesystems/mntns_cleanup/mntns_cleanup_test.c b/tools/testing/selftests/filesystems/mntns_cleanup/mntns_cleanup_test.c deleted file mode 100644 index 5209712568b1..000000000000 --- a/tools/testing/selftests/filesystems/mntns_cleanup/mntns_cleanup_test.c +++ /dev/null @@ -1,58 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0 - -#define _GNU_SOURCE -#include <errno.h> -#include <fcntl.h> -#include <sched.h> -#include <sys/mount.h> -#include <sys/stat.h> -#include <unistd.h> - -#include "../../kselftest_harness.h" - -FIXTURE(mntns_cleanup) { -}; - -FIXTURE_SETUP(mntns_cleanup) -{ - if (geteuid() != 0) - SKIP(return, "test requires CAP_SYS_ADMIN"); - - ASSERT_EQ(unshare(CLONE_NEWNS), 0); - ASSERT_EQ(mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL), 0); - - rmdir("/mnt_dir"); - ASSERT_EQ(mkdir("/mnt_dir", 0755), 0); - ASSERT_EQ(mount("tmpfs", "/mnt_dir", "tmpfs", 0, NULL), 0); - ASSERT_EQ(mkdir("/mnt_dir/hidden", 0755), 0); - ASSERT_EQ(mkdir("/mnt_dir/hidden/secret", 0755), 0); - ASSERT_EQ(mount("tmpfs", "/mnt_dir/hidden", "tmpfs", 0, NULL), 0); -} - -FIXTURE_TEARDOWN(mntns_cleanup) -{ -} - -/* Mounts must stay connected when a mount namespace is cleaned up. */ -TEST_F(mntns_cleanup, keeps_mounts_connected) -{ - int fd, sfd, err; - - fd = open("/mnt_dir", O_PATH | O_DIRECTORY | O_CLOEXEC); - ASSERT_GE(fd, 0); - - /* Destroy the namespace; the fd keeps /mnt_dir alive. */ - ASSERT_EQ(unshare(CLONE_NEWNS), 0); - - sfd = openat(fd, "hidden/secret", O_RDONLY); - err = errno; - if (sfd >= 0) - close(sfd); - close(fd); - - ASSERT_LT(sfd, 0) - TH_LOG("mount namespace teardown revealed what the overmount covered"); - ASSERT_EQ(err, ENOENT); -} - -TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/filesystems/mntns_cleanup/Makefile b/tools/testing/selftests/filesystems/mntns_unbindable/Makefile index 0e09e7030a5c..33a311c5bd72 100644 --- a/tools/testing/selftests/filesystems/mntns_cleanup/Makefile +++ b/tools/testing/selftests/filesystems/mntns_unbindable/Makefile @@ -1,5 +1,5 @@ # SPDX-License-Identifier: GPL-2.0 -TEST_GEN_PROGS := mntns_cleanup_test +TEST_GEN_PROGS := mntns_unbindable_test CFLAGS += -Wall -O2 -g $(KHDR_INCLUDES) diff --git a/tools/testing/selftests/filesystems/mntns_unbindable/mntns_unbindable_test.c b/tools/testing/selftests/filesystems/mntns_unbindable/mntns_unbindable_test.c new file mode 100644 index 000000000000..9aebc37cf74d --- /dev/null +++ b/tools/testing/selftests/filesystems/mntns_unbindable/mntns_unbindable_test.c @@ -0,0 +1,227 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * An unbindable mount stays unbindable in a cloned mount namespace. + */ +#define _GNU_SOURCE +#include <errno.h> +#include <fcntl.h> +#include <sched.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/mount.h> +#include <sys/stat.h> +#include <sys/syscall.h> +#include <sys/wait.h> +#include <unistd.h> + +#include "../../kselftest_harness.h" + +#ifndef OPEN_TREE_CLONE +#define OPEN_TREE_CLONE 1 +#endif +#ifndef OPEN_TREE_CLOEXEC +#define OPEN_TREE_CLOEXEC O_CLOEXEC +#endif + +static int sys_open_tree(int dfd, const char *filename, unsigned int flags) +{ + return syscall(__NR_open_tree, dfd, filename, flags); +} + +/* Child exit codes. */ +enum { + CHILD_OK, /* the operation failed with EINVAL as it must */ + CHILD_ALLOWED, /* the operation succeeded: the flag was lost */ + CHILD_UNSHARE, /* unshare(CLONE_NEWNS) failed */ + CHILD_ERRNO, /* the operation failed with some other errno */ + CHILD_MOUNTINFO, /* the mount was not found in mountinfo */ +}; + +FIXTURE(mntns_unbindable) +{ + char base[64]; + char src[80]; + char dst[80]; + bool mounted; +}; + +FIXTURE_SETUP(mntns_unbindable) +{ + self->mounted = false; + + if (geteuid() != 0) + SKIP(return, "test requires CAP_SYS_ADMIN"); + + ASSERT_EQ(unshare(CLONE_NEWNS), 0); + ASSERT_EQ(mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL), 0); + + snprintf(self->base, sizeof(self->base), "/tmp/mntns_unbindable.XXXXXX"); + ASSERT_NE(mkdtemp(self->base), NULL); + ASSERT_EQ(mount("tmpfs", self->base, "tmpfs", 0, NULL), 0); + self->mounted = true; + + snprintf(self->src, sizeof(self->src), "%s/src", self->base); + snprintf(self->dst, sizeof(self->dst), "%s/dst", self->base); + ASSERT_EQ(mkdir(self->src, 0755), 0); + ASSERT_EQ(mkdir(self->dst, 0755), 0); + + ASSERT_EQ(mount("tmpfs", self->src, "tmpfs", 0, NULL), 0); + ASSERT_EQ(mount(NULL, self->src, NULL, MS_UNBINDABLE, NULL), 0); +} + +FIXTURE_TEARDOWN(mntns_unbindable) +{ + if (self->mounted) + umount2(self->base, MNT_DETACH); + rmdir(self->base); +} + +static int classify(int ret, int err) +{ + if (ret >= 0) + return CHILD_ALLOWED; + return err == EINVAL ? CHILD_OK : CHILD_ERRNO; +} + +/* Is the mount on @mountpoint marked unbindable in /proc/self/mountinfo? */ +static int mountinfo_unbindable(const char *mountpoint) +{ + char line[4096]; + FILE *f; + int ret = CHILD_MOUNTINFO; + + f = fopen("/proc/self/mountinfo", "re"); + if (!f) + return CHILD_ERRNO; + + while (fgets(line, sizeof(line), f)) { + char *fields[6], *p = line, *opt; + int i; + + for (i = 0; i < 6; i++) { + fields[i] = strsep(&p, " "); + if (!fields[i]) + break; + } + if (i < 6 || strcmp(fields[4], mountpoint)) + continue; + + /* the optional fields, up to the "-" separator */ + ret = CHILD_ALLOWED; + while ((opt = strsep(&p, " ")) && strcmp(opt, "-")) { + if (!strcmp(opt, "unbindable")) + ret = CHILD_OK; + } + break; + } + fclose(f); + return ret; +} + +static int run_in_child(int (*fn)(const char *src, const char *dst), + const char *src, const char *dst) +{ + int status; + pid_t pid; + + pid = fork(); + if (pid < 0) + return -1; + if (pid == 0) + _exit(fn(src, dst)); + if (waitpid(pid, &status, 0) != pid || !WIFEXITED(status)) + return -1; + return WEXITSTATUS(status); +} + +static int bind_after_clone(const char *src, const char *dst) +{ + int ret; + + if (unshare(CLONE_NEWNS)) + return CHILD_UNSHARE; + ret = mount(src, dst, NULL, MS_BIND, NULL); + return classify(ret, errno); +} + +static int rbind_after_clone(const char *src, const char *dst) +{ + int ret; + + if (unshare(CLONE_NEWNS)) + return CHILD_UNSHARE; + ret = mount(src, dst, NULL, MS_BIND | MS_REC, NULL); + return classify(ret, errno); +} + +static int open_tree_after_clone(const char *src, const char *dst) +{ + int ret; + + if (unshare(CLONE_NEWNS)) + return CHILD_UNSHARE; + ret = sys_open_tree(AT_FDCWD, src, OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC); + return classify(ret, errno); +} + +static int mountinfo_after_clone(const char *src, const char *dst) +{ + if (unshare(CLONE_NEWNS)) + return CHILD_UNSHARE; + return mountinfo_unbindable(src); +} + +static int bind_after_two_clones(const char *src, const char *dst) +{ + int ret; + + if (unshare(CLONE_NEWNS)) + return CHILD_UNSHARE; + if (unshare(CLONE_NEWNS)) + return CHILD_UNSHARE; + ret = mount(src, dst, NULL, MS_BIND, NULL); + return classify(ret, errno); +} + +/* The namespace the mount was made unbindable in. */ +TEST_F(mntns_unbindable, refuses_bind) +{ + int ret = mount(self->src, self->dst, NULL, MS_BIND, NULL); + + ASSERT_EQ(classify(ret, errno), CHILD_OK); + ASSERT_EQ(mountinfo_unbindable(self->src), CHILD_OK); +} + +/* A copy of the namespace must not turn the mount bindable. */ +TEST_F(mntns_unbindable, refuses_bind_after_clone) +{ + ASSERT_EQ(run_in_child(bind_after_clone, self->src, self->dst), CHILD_OK) + TH_LOG("bind of an unbindable mount allowed in a copied mount namespace"); +} + +TEST_F(mntns_unbindable, refuses_rbind_after_clone) +{ + ASSERT_EQ(run_in_child(rbind_after_clone, self->src, self->dst), CHILD_OK) + TH_LOG("rbind of an unbindable mount allowed in a copied mount namespace"); +} + +TEST_F(mntns_unbindable, refuses_open_tree_after_clone) +{ + ASSERT_EQ(run_in_child(open_tree_after_clone, self->src, self->dst), CHILD_OK) + TH_LOG("OPEN_TREE_CLONE of an unbindable mount allowed in a copied mount namespace"); +} + +TEST_F(mntns_unbindable, mountinfo_after_clone) +{ + ASSERT_EQ(run_in_child(mountinfo_after_clone, self->src, self->dst), CHILD_OK) + TH_LOG("mountinfo does not show the mount as unbindable in a copied mount namespace"); +} + +TEST_F(mntns_unbindable, refuses_bind_after_two_clones) +{ + ASSERT_EQ(run_in_child(bind_after_two_clones, self->src, self->dst), CHILD_OK) + TH_LOG("bind of an unbindable mount allowed two mount namespace copies down"); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/filesystems/mount_cycle/.gitignore b/tools/testing/selftests/filesystems/mount_cycle/.gitignore new file mode 100644 index 000000000000..30f7dd071bcb --- /dev/null +++ b/tools/testing/selftests/filesystems/mount_cycle/.gitignore @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: GPL-2.0-only +loop_cycle_test +unmounted_tree_test +overmount_reparent_test +mount_cover_test +locked_handle_test +nsfs_rbind_loop_test +overmount_ns_file_test diff --git a/tools/testing/selftests/filesystems/mount_cycle/Makefile b/tools/testing/selftests/filesystems/mount_cycle/Makefile new file mode 100644 index 000000000000..fbe8c5e19d42 --- /dev/null +++ b/tools/testing/selftests/filesystems/mount_cycle/Makefile @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: GPL-2.0 +TEST_GEN_PROGS := loop_cycle_test unmounted_tree_test overmount_reparent_test mount_cover_test locked_handle_test +TEST_GEN_PROGS += nsfs_rbind_loop_test overmount_ns_file_test + +CFLAGS += -Wall -O2 -g $(KHDR_INCLUDES) + +LOCAL_HDRS += ../readdir_hold.h + +include ../../lib.mk + +$(OUTPUT)/locked_handle_test: LDLIBS += -pthread +$(OUTPUT)/mount_cover_test: LDLIBS += -pthread diff --git a/tools/testing/selftests/filesystems/mount_cycle/config b/tools/testing/selftests/filesystems/mount_cycle/config new file mode 100644 index 000000000000..3bd5ce46af74 --- /dev/null +++ b/tools/testing/selftests/filesystems/mount_cycle/config @@ -0,0 +1,39 @@ +CONFIG_USER_NS=y +CONFIG_TMPFS=y +CONFIG_BLK_DEV_LOOP=y +CONFIG_VFAT_FS=y +CONFIG_MSDOS_FS=y +CONFIG_NLS_CODEPAGE_437=y +CONFIG_NLS_ISO8859_1=y +CONFIG_MINIX_FS=y +CONFIG_AUTOFS_FS=y +CONFIG_ZRAM=y +CONFIG_ZRAM_WRITEBACK=y +CONFIG_KEYS=y +CONFIG_ECRYPT_FS=y +CONFIG_BINFMT_MISC=y +CONFIG_FUSE_FS=y +CONFIG_FUSE_PASSTHROUGH=y +CONFIG_BLK_DEV_ZONED=y +CONFIG_BLK_DEV_ZONED_LOOP=y +CONFIG_CONFIGFS_FS=y +CONFIG_USB_SUPPORT=y +CONFIG_USB=y +CONFIG_USB_STORAGE=y +CONFIG_SCSI=y +CONFIG_BLK_DEV_SD=y +CONFIG_USB_GADGET=y +CONFIG_USB_DUMMY_HCD=y +CONFIG_USB_CONFIGFS=y +CONFIG_USB_CONFIGFS_MASS_STORAGE=y +CONFIG_MD=y +CONFIG_BLK_DEV_MD=y +CONFIG_MD_RAID1=y +CONFIG_MD_BITMAP=y +CONFIG_MD_BITMAP_FILE=y +CONFIG_INOTIFY_USER=y +CONFIG_DNOTIFY=y +CONFIG_FANOTIFY=y +CONFIG_FILE_LOCKING=y +CONFIG_CRYPTO_AES=y +CONFIG_USERFAULTFD=y diff --git a/tools/testing/selftests/filesystems/mount_cycle/locked_handle_test.c b/tools/testing/selftests/filesystems/mount_cycle/locked_handle_test.c new file mode 100644 index 000000000000..b21f43471e49 --- /dev/null +++ b/tools/testing/selftests/filesystems/mount_cycle/locked_handle_test.c @@ -0,0 +1,377 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * may_decode_fh() refuses a file handle below a mount with locked children + * to root in a user namespace. That has to hold while the mount is lazily + * unmounted and its children, the locked ones too, are taken off it. + */ +#define _GNU_SOURCE +#include <errno.h> +#include <fcntl.h> +#include <pthread.h> +#include <sched.h> +#include <stdatomic.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <sys/mount.h> +#include <sys/prctl.h> +#include <sys/stat.h> +#include <sys/syscall.h> +#include <sys/wait.h> + +#include "../../kselftest_harness.h" + +#ifndef OPEN_TREE_CLONE +#define OPEN_TREE_CLONE 1 +#endif +#ifndef OPEN_TREE_CLOEXEC +#define OPEN_TREE_CLOEXEC O_CLOEXEC +#endif +#ifndef AT_RECURSIVE +#define AT_RECURSIVE 0x8000 +#endif + +#define DIR_LEN 64 +#define PATH_LEN 128 + +#define ROUNDS 100 /* lazy umounts raced per test */ +#define RACERS 3 /* threads in open_by_handle_at() */ +#define EXTRA_MOUNTS 64 /* make umount_tree() hold mount_lock longer */ +#define MAX_DELAY_US 3000 /* before the umount */ +#define TAIL_US 2000 /* after it */ + +struct handle { + struct file_handle fh; + unsigned char buf[MAX_HANDLE_SZ]; +}; + +/* exit codes of the child */ +enum { + CHILD_OK, + CHILD_SETUP, + CHILD_DECODED, /* decoded past a locked child */ + CHILD_MOUNTED, /* not refused while mounted */ + CHILD_UNMOUNTED, /* not refused once unmounted */ + CHILD_ALLOWED, /* refused where nothing is locked */ + CHILD_NOUSERNS, /* no user namespace to be had */ +}; + +static int write_file(const char *path, const char *s) +{ + ssize_t n = -1; + int fd; + + fd = open(path, O_WRONLY | O_CLOEXEC); + if (fd >= 0) { + n = write(fd, s, strlen(s)); + close(fd); + } + return n == (ssize_t)strlen(s) ? 0 : -1; +} + +/* Become root in a new user namespace with a private mount namespace. */ +static int enter_userns(void) +{ + uid_t uid = getuid(); + gid_t gid = getgid(); + char map[32]; + + prctl(PR_SET_DUMPABLE, 1); + /* EINVAL: no USER_NS, ENOSPC: user.max_user_namespaces is 0, EPERM: an LSM */ + if (unshare(CLONE_NEWUSER | CLONE_NEWNS)) + return errno == EINVAL || errno == ENOSPC || errno == EPERM ? + CHILD_NOUSERNS : CHILD_SETUP; + if (write_file("/proc/self/setgroups", "deny") && errno != ENOENT) + return CHILD_SETUP; + snprintf(map, sizeof(map), "0 %d 1", uid); + if (write_file("/proc/self/uid_map", map)) + return CHILD_SETUP; + snprintf(map, sizeof(map), "0 %d 1", gid); + if (write_file("/proc/self/gid_map", map)) + return CHILD_SETUP; + if (setgid(0) || setuid(0)) + return CHILD_SETUP; + if (mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL)) + return CHILD_SETUP; + return CHILD_OK; +} + +static int get_handle(const char *path, struct handle *h) +{ + int mntid; + + h->fh.handle_bytes = MAX_HANDLE_SZ; + return name_to_handle_at(AT_FDCWD, path, &h->fh, &mntid, 0); +} + +static int decode(int dfd, struct handle *h) +{ + return open_by_handle_at(dfd, &h->fh, O_RDONLY | O_DIRECTORY | O_CLOEXEC); +} + +struct race { + int dfd; + struct handle *h; + atomic_int go; + atomic_int stop; + atomic_int won; /* the first decoded descriptor */ +}; + +static void *racer(void *arg) +{ + struct race *r = arg; + + while (!atomic_load(&r->go)) + ; + while (!atomic_load(&r->stop)) { + int none = -1; + int fd; + + fd = decode(r->dfd, r->h); + if (fd < 0) + continue; + if (!atomic_compare_exchange_strong(&r->won, &none, fd)) + close(fd); + } + return NULL; +} + +/* stop the @n racers started so far and wait for them, they spin on @r */ +static void stop_racers(struct race *r, pthread_t *th, int n) +{ + int i; + + atomic_store(&r->stop, 1); + atomic_store(&r->go, 1); /* one still waiting for the start sees stop next */ + for (i = 0; i < n; i++) + pthread_join(th[i], NULL); +} + +/* + * Bind @h recursively at @w, or take a detached copy of it, and race + * open_by_handle_at() of @inner against the lazy umount. The decoded + * descriptor, if there was one, is left in @won. + */ +static int race_round(const char *h, const char *w, struct handle *inner, + bool dissolve, int *won) +{ + struct race r = { .h = inner, .won = -1 }; + pthread_t th[RACERS]; + int treefd = -1, fd, i; + + if (dissolve) { + treefd = syscall(__NR_open_tree, AT_FDCWD, h, + OPEN_TREE_CLONE | AT_RECURSIVE | OPEN_TREE_CLOEXEC); + if (treefd < 0) + return CHILD_SETUP; + /* an ordinary descriptor on the copy, treefd's close dissolves it */ + r.dfd = openat(treefd, ".", O_RDONLY | O_DIRECTORY | O_CLOEXEC); + } else { + if (mount(h, w, NULL, MS_BIND | MS_REC, NULL)) + return CHILD_SETUP; + r.dfd = open(w, O_RDONLY | O_DIRECTORY | O_CLOEXEC); + } + if (r.dfd < 0) + return CHILD_SETUP; + + fd = decode(r.dfd, inner); + if (fd >= 0 || errno != EPERM) { + if (fd >= 0) + close(fd); + return CHILD_MOUNTED; + } + + for (i = 0; i < RACERS; i++) { + if (pthread_create(&th[i], NULL, racer, &r)) { + stop_racers(&r, th, i); + return CHILD_SETUP; + } + } + atomic_store(&r.go, 1); + usleep(rand() % MAX_DELAY_US); + if (dissolve) + close(treefd); + else if (umount2(w, MNT_DETACH)) { + stop_racers(&r, th, RACERS); + return CHILD_SETUP; + } + usleep(TAIL_US); + stop_racers(&r, th, RACERS); + + fd = decode(r.dfd, inner); + if (fd >= 0) { + close(fd); + return CHILD_UNMOUNTED; + } + close(r.dfd); + *won = atomic_load(&r.won); + return CHILD_OK; +} + +static int race_child(const char *h, const char *w, struct handle *inner, + bool dissolve) +{ + int ret, won, i; + + srand(getpid()); + ret = enter_userns(); + if (ret) + return ret; + for (i = 0; i < ROUNDS; i++) { + ret = race_round(h, w, inner, dissolve, &won); + if (ret) + return ret; + if (won >= 0) + return CHILD_DECODED; + } + return CHILD_OK; +} + +/* a bind without locked children decodes while mounted and not after */ +static int allowed_child(const char *plain, const char *w, struct handle *h) +{ + int dfd, fd, ret; + + ret = enter_userns(); + if (ret) + return ret; + if (mount(plain, w, NULL, MS_BIND | MS_REC, NULL)) + return CHILD_SETUP; + dfd = open(w, O_RDONLY | O_DIRECTORY | O_CLOEXEC); + if (dfd < 0) + return CHILD_SETUP; + fd = decode(dfd, h); + if (fd < 0) + return CHILD_ALLOWED; + close(fd); + if (umount2(w, MNT_DETACH)) + return CHILD_SETUP; + fd = decode(dfd, h); + if (fd >= 0) { + close(fd); + return CHILD_UNMOUNTED; + } + return errno == EPERM ? CHILD_OK : CHILD_UNMOUNTED; +} + +FIXTURE(locked_handle) { + char base[DIR_LEN]; + char h[PATH_LEN]; /* the tree with the covered directory */ + char plain[PATH_LEN]; /* a tree with no mount in it */ + char w[PATH_LEN]; /* where the child binds either */ + struct handle inner; /* h/top/secret/inner, covered */ + struct handle uncovered; /* plain/inner */ +}; + +FIXTURE_SETUP(locked_handle) +{ + char p[PATH_LEN], e[PATH_LEN]; + int i; + + if (geteuid()) + SKIP(return, "test requires root"); + + snprintf(self->base, sizeof(self->base), "/tmp/locked_handle.XXXXXX"); + ASSERT_NE(mkdtemp(self->base), NULL); + ASSERT_EQ(unshare(CLONE_NEWNS), 0); + ASSERT_EQ(mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL), 0); + ASSERT_EQ(mount("tmpfs", self->base, "tmpfs", 0, NULL), 0); + + snprintf(self->h, sizeof(self->h), "%s/h", self->base); + snprintf(self->plain, sizeof(self->plain), "%s/plain", self->base); + snprintf(self->w, sizeof(self->w), "%s/w", self->base); + ASSERT_EQ(mkdir(self->h, 0755), 0); + ASSERT_EQ(mkdir(self->plain, 0755), 0); + ASSERT_EQ(mkdir(self->w, 0755), 0); + snprintf(p, sizeof(p), "%s/plain/inner", self->base); + ASSERT_EQ(mkdir(p, 0755), 0); + ASSERT_EQ(get_handle(p, &self->uncovered), 0); + + /* the handle is for this filesystem */ + ASSERT_EQ(mount("tmpfs", self->h, "tmpfs", 0, NULL), 0); + snprintf(p, sizeof(p), "%s/h/top", self->base); + ASSERT_EQ(mkdir(p, 0755), 0); + snprintf(p, sizeof(p), "%s/h/top/secret", self->base); + ASSERT_EQ(mkdir(p, 0755), 0); + snprintf(p, sizeof(p), "%s/h/top/secret/inner", self->base); + ASSERT_EQ(mkdir(p, 0755), 0); + ASSERT_EQ(get_handle(p, &self->inner), 0); + snprintf(e, sizeof(e), "%s/h/empty", self->base); + ASSERT_EQ(mkdir(e, 0755), 0); + + /* cover it, and some more so the umount takes longer */ + snprintf(p, sizeof(p), "%s/h/top/secret", self->base); + ASSERT_EQ(mount("tmpfs", p, "tmpfs", 0, NULL), 0); + for (i = 0; i < EXTRA_MOUNTS; i++) { + snprintf(p, sizeof(p), "%s/h/c%d", self->base, i); + ASSERT_EQ(mkdir(p, 0755), 0); + ASSERT_EQ(mount(e, p, NULL, MS_BIND, NULL), 0); + } +} + +FIXTURE_TEARDOWN(locked_handle) +{ + umount2(self->base, MNT_DETACH); + rmdir(self->base); +} + +static int wait_child(pid_t pid) +{ + int status; + + if (waitpid(pid, &status, 0) != pid || !WIFEXITED(status)) + return -1; + return WEXITSTATUS(status); +} + +TEST_F(locked_handle, lazy_umount) +{ + pid_t pid; + int ret; + + pid = fork(); + ASSERT_GE(pid, 0); + if (pid == 0) + _exit(race_child(self->h, self->w, &self->inner, false)); + ret = wait_child(pid); + TH_LOG("child exit code %d", ret); + if (ret == CHILD_NOUSERNS) + SKIP(return, "no user namespaces"); + EXPECT_EQ(ret, CHILD_OK); +} + +TEST_F(locked_handle, dissolve) +{ + pid_t pid; + int ret; + + pid = fork(); + ASSERT_GE(pid, 0); + if (pid == 0) + _exit(race_child(self->h, self->w, &self->inner, true)); + ret = wait_child(pid); + TH_LOG("child exit code %d", ret); + if (ret == CHILD_NOUSERNS) + SKIP(return, "no user namespaces"); + EXPECT_EQ(ret, CHILD_OK); +} + +TEST_F(locked_handle, allowed_use) +{ + pid_t pid; + int ret; + + pid = fork(); + ASSERT_GE(pid, 0); + if (pid == 0) + _exit(allowed_child(self->plain, self->w, &self->uncovered)); + ret = wait_child(pid); + TH_LOG("child exit code %d", ret); + if (ret == CHILD_NOUSERNS) + SKIP(return, "no user namespaces"); + EXPECT_EQ(ret, CHILD_OK); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/filesystems/mount_cycle/loop_cycle_test.c b/tools/testing/selftests/filesystems/mount_cycle/loop_cycle_test.c new file mode 100644 index 000000000000..6b4f5304c2e4 --- /dev/null +++ b/tools/testing/selftests/filesystems/mount_cycle/loop_cycle_test.c @@ -0,0 +1,1542 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * A mount that another namespace's rmdir detached, or that went down with + * the detached tree it was in when the tree's last fd was closed, keeps + * its submounts connected, and a connected submount is put by its + * parent's final mntput(). A submount whose filesystem keeps a file open + * on the parent then holds the parent's count above zero for good: + * nothing in userspace refers to either mount any more and nothing can + * release them. A loop device is the simplest such filesystem, its + * backing file sits on the parent. + */ +#define _GNU_SOURCE +#include <dirent.h> +#include <errno.h> +#include <fcntl.h> +#include <sched.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/ioctl.h> +#include <sys/mount.h> +#include <sys/stat.h> +#include <stdbool.h> +#include <sys/wait.h> +#include <unistd.h> +#include <linux/fuse.h> +#include <linux/keyctl.h> +#include <linux/major.h> +#include <linux/raid/md_u.h> +#include <linux/raid/md_p.h> +#include <linux/loop.h> +#include <linux/magic.h> +#include <sys/syscall.h> +#include <sys/sysmacros.h> +#include <sys/uio.h> + +#include "../wrappers.h" +#include "../../kselftest_harness.h" + +#define IMAGE_SIZE (1440 * 1024) +#define SECTOR 512 + +/* + * The tmpfs of the fixture. A directory of its own from mkdtemp(), so that + * the test needs no writable root directory and two instances don't take + * each other's mounts down. + */ +static char base[64]; + +/* The configfs directory of the gadget, named after the test process. */ +static char gadget[96]; + +/* A path below the base directory. Eight of them can be in use at a time. */ +static const char *at(const char *rel) +{ + static char buf[8][PATH_MAX]; + static unsigned int next; + char *p = buf[next++ % 8]; + + snprintf(p, PATH_MAX, "%s/%s", base, rel); + return p; +} + +/* A path below the gadget's configfs directory. */ +static const char *gat(const char *rel) +{ + static char buf[4][PATH_MAX]; + static unsigned int next; + char *p = buf[next++ % 4]; + + snprintf(p, PATH_MAX, "%s%s", gadget, rel); + return p; +} + +/* The loop devices this process has bound, to release them on every path. */ +#define MAX_LOOPS 4 +static int bound[MAX_LOOPS]; +static int nr_bound; + +/* What a child tells its parent: the loop devices it has bound, -1 for none. */ +struct report { + int n[2]; +}; + +/* A blank FAT12 floppy image: boot sector, two FATs, an empty root directory. */ +static int write_fat12(int fd) +{ + struct stat st; + unsigned char sector[SECTOR] = { + 0xeb, 0x3c, 0x90, 'M', 'S', 'W', 'I', 'N', '4', '.', '1', + [11] = 0x00, 0x02, /* bytes per sector: 512 */ + [13] = 1, /* sectors per cluster */ + [14] = 1, 0, /* reserved sectors */ + [16] = 2, /* FATs */ + [17] = 0xe0, 0x00, /* root directory entries: 224 */ + [19] = 0x40, 0x0b, /* total sectors: 2880 */ + [21] = 0xf0, /* media descriptor */ + [22] = 9, 0, /* sectors per FAT */ + [24] = 18, 0, /* sectors per track */ + [26] = 2, 0, /* heads */ + [38] = 0x29, /* extended boot signature */ + [39] = 0x12, 0x34, 0x56, 0x78, + [43] = 'N', 'O', ' ', 'N', 'A', 'M', 'E', ' ', ' ', ' ', ' ', + [54] = 'F', 'A', 'T', '1', '2', ' ', ' ', ' ', + [510] = 0x55, 0xaa, + }; + unsigned char fat[SECTOR] = { 0xf0, 0xff, 0xff }; + + if (pwrite(fd, sector, SECTOR, 0) != SECTOR) + return -1; + /* the first FAT and the second one, one sector each is enough */ + if (pwrite(fd, fat, SECTOR, 1 * SECTOR) != SECTOR || + pwrite(fd, fat, SECTOR, 10 * SECTOR) != SECTOR) + return -1; + if (fstat(fd, &st) || S_ISBLK(st.st_mode)) + return 0; + return ftruncate(fd, IMAGE_SIZE); +} + +/* + * A blank FAT12 image with 4 KiB sectors and @sectors of them, for a device + * with 4 KiB logical blocks (zram) or for a bigger image than a floppy. + */ +static int write_fat12_4k(int fd, unsigned int sectors) +{ + unsigned int fat_sectors = (sectors * 3 / 2 + 4095) / 4096; + unsigned char sector[4096] = { + 0xeb, 0x3c, 0x90, 'M', 'S', 'W', 'I', 'N', '4', '.', '1', + [11] = 0x00, 0x10, /* bytes per sector: 4096 */ + [13] = 1, /* sectors per cluster */ + [14] = 1, 0, /* reserved sectors */ + [16] = 2, /* FATs */ + [17] = 128, 0, /* root directory entries: one sector */ + [19] = sectors & 0xff, sectors >> 8, + [21] = 0xf8, /* media descriptor */ + [22] = fat_sectors, 0, + [24] = 63, 0, /* sectors per track */ + [26] = 255, 0, /* heads */ + [38] = 0x29, /* extended boot signature */ + [39] = 0x12, 0x34, 0x56, 0x78, + [43] = 'N', 'O', ' ', 'N', 'A', 'M', 'E', ' ', ' ', ' ', ' ', + [54] = 'F', 'A', 'T', '1', '2', ' ', ' ', ' ', + [510] = 0x55, 0xaa, + }; + unsigned char fat[4096] = { 0xf8, 0xff, 0xff }; + struct stat st; + + if (pwrite(fd, sector, sizeof(sector), 0) != sizeof(sector)) + return -1; + if (pwrite(fd, fat, sizeof(fat), 1 * 4096) != sizeof(fat) || + pwrite(fd, fat, sizeof(fat), (1 + fat_sectors) * 4096) != sizeof(fat)) + return -1; + if (fstat(fd, &st) || S_ISBLK(st.st_mode)) + return 0; + return ftruncate(fd, (off_t)sectors * 4096); +} + +#define MINIX_BLOCK 1024 +#define MINIX_BLOCKS 4096 /* a 4 MiB image */ +#define MINIX_INODES 512 +#define MINIX_ITABLE (MINIX_INODES * 32 / MINIX_BLOCK) +#define MINIX_FIRSTDATA (2 + 1 + 1 + MINIX_ITABLE) /* boot, super, imap, zmap, inodes */ + +/* + * A blank minix v1 image, for the holders that need a FIFO or a device + * node on the dying mount, which vfat can't hold. Superblock in block 1, + * one block each for the inode and zone bitmaps, the inode table, and + * the root directory in the first data zone. + */ +static int write_minix(int fd) +{ + struct { + __u16 s_ninodes, s_nzones, s_imap_blocks, s_zmap_blocks; + __u16 s_firstdatazone, s_log_zone_size; + __u32 s_max_size; + __u16 s_magic, s_state; + } sb = { + .s_ninodes = MINIX_INODES, + .s_nzones = MINIX_BLOCKS, + .s_imap_blocks = 1, + .s_zmap_blocks = 1, + .s_firstdatazone = MINIX_FIRSTDATA, + .s_max_size = (7 + 512 + 512 * 512) * MINIX_BLOCK, + .s_magic = MINIX_SUPER_MAGIC, + .s_state = 1, /* MINIX_VALID_FS */ + }; + struct { + __u16 i_mode, i_uid; + __u32 i_size, i_time; + __u8 i_gid, i_nlinks; + __u16 i_zone[9]; + } root = { + .i_mode = S_IFDIR | 0755, + .i_size = 2 * 16, + .i_nlinks = 2, + .i_zone = { MINIX_FIRSTDATA }, + }; + unsigned char imap[MINIX_BLOCK], zmap[MINIX_BLOCK], dir[MINIX_BLOCK] = {}; + int i; + + /* bit 0 is reserved in both maps, the root inode and its zone are in use */ + memset(imap, 0xff, sizeof(imap)); + for (i = 2; i <= MINIX_INODES; i++) + imap[i / 8] &= ~(1 << (i % 8)); + memset(zmap, 0xff, sizeof(zmap)); + for (i = 2; i <= MINIX_BLOCKS - MINIX_FIRSTDATA; i++) + zmap[i / 8] &= ~(1 << (i % 8)); + dir[0] = 1; + dir[2] = '.'; + dir[16] = 1; + dir[18] = '.'; + dir[19] = '.'; + + if (pwrite(fd, &sb, sizeof(sb), 1 * MINIX_BLOCK) != sizeof(sb) || + pwrite(fd, imap, sizeof(imap), 2 * MINIX_BLOCK) != sizeof(imap) || + pwrite(fd, zmap, sizeof(zmap), 3 * MINIX_BLOCK) != sizeof(zmap) || + pwrite(fd, &root, sizeof(root), 4 * MINIX_BLOCK) != sizeof(root) || + pwrite(fd, dir, sizeof(dir), MINIX_FIRSTDATA * MINIX_BLOCK) != sizeof(dir)) + return -1; + return ftruncate(fd, (off_t)MINIX_BLOCKS * MINIX_BLOCK); +} + +static int read_sysfs(const char *path, char *buf, size_t size) +{ + ssize_t n; + int fd; + + fd = open(path, O_RDONLY); + if (fd < 0) + return -1; + n = read(fd, buf, size - 1); + close(fd); + if (n < 0) + return -1; + buf[n] = '\0'; + return 0; +} + +/* Is @dev the mount source of this line of mountinfo? loop1 is not loop10. */ +static bool line_has_source(const char *line, const char *dev) +{ + const char *sep, *src, *end; + + sep = strstr(line, " - "); + if (!sep) + return false; + src = strchr(sep + 3, ' '); /* skip the filesystem type */ + if (!src) + return false; + src++; + end = strchr(src, ' '); + if (!end) + return false; + return (size_t)(end - src) == strlen(dev) && !strncmp(src, dev, end - src); +} + +/* + * Does a mount namespace of a process that /proc shows have a mount of @dev? + * The mounts under test are in no namespace at this point on any kernel, so + * this only checks that the test got as far as it thinks. + */ +static bool mounted_anywhere(const char *dev) +{ + char path[PATH_MAX], line[4096]; + struct dirent *de; + bool found = false; + DIR *proc; + FILE *f; + + proc = opendir("/proc"); + if (!proc) + return false; + while (!found && (de = readdir(proc))) { + if (de->d_name[0] < '0' || de->d_name[0] > '9') + continue; + snprintf(path, sizeof(path), "/proc/%s/mountinfo", de->d_name); + f = fopen(path, "re"); + if (!f) + continue; + while (fgets(line, sizeof(line), f)) { + if (line_has_source(line, dev)) { + found = true; + break; + } + } + fclose(f); + } + closedir(proc); + return found; +} + +/* Wait up to @ms milliseconds for the loop device to give up its backing file. */ +static bool loop_released(const char *sysfs, int ms) +{ + char buf[PATH_MAX]; + + for (; ms > 0; ms -= 100) { + if (read_sysfs(sysfs, buf, sizeof(buf)) < 0) + return errno == ENOENT; + usleep(100000); + } + return read_sysfs(sysfs, buf, sizeof(buf)) < 0 && errno == ENOENT; +} + +/* Tell the loop device to give its file up, now or when its last user is gone. */ +static void loop_clear(int n) +{ + char dev[32]; + int lfd; + + if (n < 0) + return; + snprintf(dev, sizeof(dev), "/dev/loop%d", n); + lfd = open(dev, O_RDWR); + if (lfd < 0) + return; + ioctl(lfd, LOOP_CLR_FD); + close(lfd); +} + +FIXTURE(loop_cycle) { + char dev[32]; /* the loop device the child set up */ + char sysfs[64]; /* its backing_file attribute */ + int loops[MAX_LOOPS]; /* every loop device the case has bound */ + int nr_loops; + bool gadget; /* the gadget's configfs directory exists */ +}; + +static void remember_loop(FIXTURE_DATA(loop_cycle) *self, int n) +{ + if (n >= 0 && self->nr_loops < MAX_LOOPS) + self->loops[self->nr_loops++] = n; +} + +FIXTURE_SETUP(loop_cycle) +{ + if (geteuid() != 0) + SKIP(return, "test requires CAP_SYS_ADMIN"); + if (access("/dev/loop-control", R_OK | W_OK)) + SKIP(return, "test requires loop devices"); + + ASSERT_EQ(unshare(CLONE_NEWNS), 0); + ASSERT_EQ(mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL), 0); + + snprintf(base, sizeof(base), "/tmp/loop_cycle.XXXXXX"); + ASSERT_NE(mkdtemp(base), NULL); + /* a failed assertion in the setup does not run the teardown */ + ASSERT_EQ(mount("tmpfs", base, "tmpfs", 0, NULL), 0) + rmdir(base); + snprintf(gadget, sizeof(gadget), + "/sys/kernel/config/usb_gadget/kselftest_cycle_%d", getpid()); + self->dev[0] = '\0'; + self->nr_loops = 0; + self->gadget = false; +} + +static int mount_configfs(void); +static void gadget_remove(void); + +FIXTURE_TEARDOWN(loop_cycle) +{ + if (self->gadget) + gadget_remove(); + /* whatever a failed assertion has left bound */ + for (int i = 0; i < self->nr_loops; i++) + loop_clear(self->loops[i]); + umount2(base, MNT_DETACH); + rmdir(base); +} + +/* Child exit codes. */ +enum { + CHILD_OK, + CHILD_NS, /* could not set up the namespace or the tmpfs */ + CHILD_IMAGE, /* could not write the image */ + CHILD_LOOP, /* could not set up the loop device */ + CHILD_MOUNT, /* could not mount it (vfat and msdos both refused) */ + CHILD_PIPE, /* the parent went away */ + CHILD_HOLDER, /* could not set the holder up below the mount */ + CHILD_SKIP, /* the kernel lacks what the holder needs */ + CHILD_NOFS, /* the kernel lacks the filesystem of the image */ +}; + +/* Bind the loop device that LOOP_CTL_GET_FREE names to @ifd; the device number. */ +static int loop_bind_free(int ifd) +{ + int cfd, lfd, n; + char dev[32]; + + cfd = open("/dev/loop-control", O_RDWR); + if (cfd < 0) + return -1; + n = ioctl(cfd, LOOP_CTL_GET_FREE); + close(cfd); + if (n < 0) + return -1; + snprintf(dev, sizeof(dev), "/dev/loop%d", n); + lfd = open(dev, O_RDWR); + if (lfd < 0) + return -1; + if (ioctl(lfd, LOOP_SET_FD, ifd)) + n = -1; + close(lfd); + return n; +} + +/* + * Bind a free loop device to the open image @ifd; the device number. The + * device is free when LOOP_CTL_GET_FREE names it and may be somebody else's + * a moment later, so try again when it is busy. + */ +static int loop_bind(int ifd) +{ + int n = -1; + + for (int i = 0; i < 64 && n < 0; i++) { + n = loop_bind_free(ifd); + if (n < 0 && errno != EBUSY) + return -1; + } + if (n >= 0 && nr_bound < MAX_LOOPS) + bound[nr_bound++] = n; + return n; +} + +/* Mount a FAT image, as vfat or as msdos; -1 with ENODEV if the kernel has neither. */ +static int mount_fat(const char *dev, const char *mp) +{ + int err; + + if (!mount(dev, mp, "vfat", 0, NULL)) + return 0; + err = errno; + if (!mount(dev, mp, "msdos", 0, NULL)) + return 0; + if (err != ENODEV) + errno = err; + return -1; +} + +/* + * A child that gives up has to take down what it has set up: nobody else + * knows about it. Unmount, then tell the loop devices to let go. The mounts + * are released after this process is gone and the devices follow them. + */ +static int child_fails(int ret) +{ + umount2(at("vol"), MNT_DETACH); + umount2(at("vol2"), MNT_DETACH); + umount2(at("p"), MNT_DETACH); + umount2(at("img"), MNT_DETACH); + for (int i = 0; i < nr_bound; i++) + loop_clear(bound[i]); + return ret; +} + +/* Write an image to @img, bind a loop device to it and mount that at @mp; the device number. */ +static int loop_mount(const char *img, const char *mp) +{ + char dev[32]; + int ifd, n; + + ifd = open(img, O_RDWR | O_CREAT | O_EXCL, 0600); + if (ifd < 0 || write_fat12(ifd)) + return -CHILD_IMAGE; + n = loop_bind(ifd); + close(ifd); /* the loop device holds the file from now on */ + if (n < 0) + return -CHILD_LOOP; + snprintf(dev, sizeof(dev), "/dev/loop%d", n); + if (mkdir(mp, 0755)) + return -CHILD_MOUNT; + if (mount_fat(dev, mp)) + return errno == ENODEV ? -CHILD_NOFS : -CHILD_MOUNT; + return n; +} + +/* + * Two tmpfs mounts, each carrying the image of the loop mount below the + * other: the loop mount below vol has its image on vol2 and the other way + * round. + */ +static int crossed_child(int to_parent, int from_parent) +{ + struct report r; + char c; + + if (unshare(CLONE_NEWNS) || mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL)) + return CHILD_NS; + if (mkdir(at("vol"), 0755) || mount("tmpfs", at("vol"), "tmpfs", 0, NULL) || + mkdir(at("vol2"), 0755) || mount("tmpfs", at("vol2"), "tmpfs", 0, NULL)) + return child_fails(CHILD_NS); + r.n[0] = loop_mount(at("vol2/img"), at("vol/mnt")); + if (r.n[0] < 0) + return child_fails(-r.n[0]); + r.n[1] = loop_mount(at("vol/img"), at("vol2/mnt")); + if (r.n[1] < 0) + return child_fails(-r.n[1]); + if (write(to_parent, &r, sizeof(r)) != sizeof(r)) + return child_fails(CHILD_PIPE); + if (read(from_parent, &c, 1) != 1) + return child_fails(CHILD_PIPE); + return CHILD_OK; +} + +/* + * In its own mount namespace the child mounts a tmpfs on vol, + * puts a filesystem image on it, binds a loop device to the image and + * mounts that loop device below. The loop device's backing file is a + * reference on the mount the image is on, held by the loop device, held + * by the mounted filesystem, held by the mount below. + */ +static int loop_child(int to_parent, int from_parent) +{ + struct report r = { { -1, -1 } }; + char c; + + if (unshare(CLONE_NEWNS) || mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL)) + return CHILD_NS; + if (mkdir(at("vol"), 0755) || mount("tmpfs", at("vol"), "tmpfs", 0, NULL)) + return child_fails(CHILD_NS); + r.n[0] = loop_mount(at("vol/img"), at("vol/mnt")); + if (r.n[0] < 0) + return child_fails(-r.n[0]); + + if (write(to_parent, &r, sizeof(r)) != sizeof(r)) + return child_fails(CHILD_PIPE); + /* keep the namespace alive while the parent removes the directory */ + if (read(from_parent, &c, 1) != 1) + return child_fails(CHILD_PIPE); + return CHILD_OK; +} + +/* + * The child did not report. Skip if the kernel lacks something, fail with + * what the child said otherwise. A child that a signal killed has failed. + */ +#define CHILD_GAVE_UP(pid) do { \ + int __status; \ + \ + ASSERT_EQ(waitpid(pid, &__status, 0), pid); \ + if (WIFEXITED(__status) && WEXITSTATUS(__status) == CHILD_NOFS) \ + SKIP(return, "test requires the filesystem of the image (FAT or minix)"); \ + if (WIFEXITED(__status) && WEXITSTATUS(__status) == CHILD_SKIP) \ + SKIP(return, "the kernel lacks what this holder needs"); \ + ASSERT_TRUE(false) \ + TH_LOG("child failed to set up: %s %d", \ + WIFEXITED(__status) ? "exit status" : "signal", \ + WIFEXITED(__status) ? WEXITSTATUS(__status) : WTERMSIG(__status)); \ +} while (0) + +/* The child has to leave by itself and with nothing to complain about. */ +#define CHILD_LEFT(pid) do { \ + int __status; \ + \ + ASSERT_EQ(waitpid(pid, &__status, 0), pid); \ + ASSERT_TRUE(WIFEXITED(__status)); \ + ASSERT_EQ(WEXITSTATUS(__status), CHILD_OK); \ +} while (0) + +/* + * An exclusive open of the device fails while a filesystem holds it, and + * the only filesystem that ever did is the one mounted below the dead + * mount. Give a release in flight a moment. Then the device must clear + * right away rather than only be marked for autoclear. + */ +static void assert_loop_released(struct __test_metadata *_metadata, + FIXTURE_DATA(loop_cycle) *self) +{ + int lfd; + + for (int i = 0; i < 20; i++) { + lfd = open(self->dev, O_RDONLY | O_EXCL); + if (lfd >= 0) + break; + usleep(100000); + } + EXPECT_GE(lfd, 0) + TH_LOG("%s is still held by the loop mount below the dead mount: nothing refers to either mount and nothing can release them", + self->dev); + if (lfd >= 0) + close(lfd); + + lfd = open(self->dev, O_RDWR); + ASSERT_GE(lfd, 0); + ASSERT_EQ(ioctl(lfd, LOOP_CLR_FD), 0); + close(lfd); + ASSERT_TRUE(loop_released(self->sysfs, 5000)) + TH_LOG("%s kept its backing file after LOOP_CLR_FD: the filesystem on it is still mounted somewhere nobody can reach", + self->dev); + /* somebody else may bind it from now on, so the teardown leaves it alone */ + for (int i = 0; i < self->nr_loops; i++) { + char dev[32]; + + snprintf(dev, sizeof(dev), "/dev/loop%d", self->loops[i]); + if (!strcmp(dev, self->dev)) + self->loops[i] = -1; + } +} + +/* + * rmdir of vol from here, where it is not a mountpoint, detaches + * the child's tmpfs with the loop mount connected below it. Once the child + * is gone nothing refers to either mount. The loop device must then be + * free to give up its backing file, which only happens when the mounted + * filesystem below the detached tmpfs has been released. + */ +TEST_F(loop_cycle, detached_loop_mount_released) +{ + int to_parent[2], to_child[2]; + struct report r = { { -1, -1 } }; + char buf[PATH_MAX]; + pid_t pid; + + ASSERT_EQ(pipe(to_parent), 0); + ASSERT_EQ(pipe(to_child), 0); + + pid = fork(); + ASSERT_GE(pid, 0); + if (pid == 0) { + close(to_parent[0]); + close(to_child[1]); + _exit(loop_child(to_parent[1], to_child[0])); + } + close(to_parent[1]); + close(to_child[0]); + + if (read(to_parent[0], &r, sizeof(r)) != sizeof(r)) + CHILD_GAVE_UP(pid); + remember_loop(self, r.n[0]); + snprintf(self->dev, sizeof(self->dev), "/dev/loop%d", r.n[0]); + snprintf(self->sysfs, sizeof(self->sysfs), "/sys/block/loop%d/loop/backing_file", r.n[0]); + ASSERT_EQ(read_sysfs(self->sysfs, buf, sizeof(buf)), 0); + ASSERT_NE(strstr(buf, "/vol/img"), NULL); + + /* not a mountpoint in this namespace, so the directory can go */ + ASSERT_EQ(rmdir(at("vol")), 0); + + /* the child leaves: its namespace and every reference it held are gone */ + ASSERT_EQ(write(to_child[1], "", 1), 1); + CHILD_LEFT(pid); + close(to_parent[0]); + close(to_child[1]); + + /* nothing can reach the two mounts any more */ + ASSERT_EQ(access(at("vol"), F_OK), -1); + ASSERT_FALSE(mounted_anywhere(self->dev)); + + assert_loop_released(_metadata, self); +} + +/* + * The same two mounts in a detached tree: a clone of vol from + * open_tree(), the image opened through the clone so that the loop device + * holds the clone, and the loop mount moved below the clone. The last + * close of the tree's fd dissolves the tree with the loop mount left + * connected below the dead clone, and nothing refers to either afterwards. + */ +TEST_F(loop_cycle, dissolved_tree_loop_mount_released) +{ + int tfd, ifd, fsfd, mfd, n; + char buf[PATH_MAX]; + + fsfd = sys_fsopen("vfat", 0); + if (fsfd < 0) + fsfd = sys_fsopen("msdos", 0); + if (fsfd < 0) + SKIP(return, "test requires a FAT filesystem"); + + ASSERT_EQ(mkdir(at("vol"), 0755), 0); + ASSERT_EQ(mount("tmpfs", at("vol"), "tmpfs", 0, NULL), 0); + tfd = sys_open_tree(AT_FDCWD, at("vol"), OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC); + ASSERT_GE(tfd, 0); + + /* the image, opened through the clone: the loop device holds the clone */ + ifd = openat(tfd, "img", O_RDWR | O_CREAT | O_EXCL, 0600); + ASSERT_GE(ifd, 0); + ASSERT_EQ(write_fat12(ifd), 0); + n = loop_bind(ifd); + close(ifd); + ASSERT_GE(n, 0); + remember_loop(self, n); + snprintf(self->dev, sizeof(self->dev), "/dev/loop%d", n); + snprintf(self->sysfs, sizeof(self->sysfs), "/sys/block/loop%d/loop/backing_file", n); + + /* the loop mount, moved below the clone */ + ASSERT_EQ(mkdirat(tfd, "mnt", 0755), 0); + ASSERT_EQ(sys_fsconfig(fsfd, FSCONFIG_SET_STRING, "source", self->dev, 0), 0); + ASSERT_EQ(sys_fsconfig(fsfd, FSCONFIG_CMD_CREATE, NULL, NULL, 0), 0); + mfd = sys_fsmount(fsfd, 0, 0); + ASSERT_GE(mfd, 0); + close(fsfd); + ASSERT_EQ(sys_move_mount(mfd, "", tfd, "mnt", MOVE_MOUNT_F_EMPTY_PATH), 0); + close(mfd); + + ASSERT_EQ(read_sysfs(self->sysfs, buf, sizeof(buf)), 0); + ASSERT_NE(strstr(buf, "img"), NULL); + + /* the last fd of the tree: both mounts die, the loop mount connected */ + close(tfd); + + /* nothing can reach the two mounts any more */ + ASSERT_FALSE(mounted_anywhere(self->dev)); + + assert_loop_released(_metadata, self); +} + +/* + * The cycle in two steps: rmdir of vol leaves the loop mount below it + * connected while its image's mount, vol2, is alive; then rmdir of vol2 + * takes that one with the loop mount whose image is on the dead vol. Each + * dead mount now owns a loop mount whose filesystem pins the other. + */ +TEST_F(loop_cycle, crossed_images_released) +{ + int to_parent[2], to_child[2]; + struct report r = { { -1, -1 } }; + char sysfs[2][64], dev[2][32]; + pid_t pid; + + ASSERT_EQ(pipe(to_parent), 0); + ASSERT_EQ(pipe(to_child), 0); + + pid = fork(); + ASSERT_GE(pid, 0); + if (pid == 0) { + close(to_parent[0]); + close(to_child[1]); + _exit(crossed_child(to_parent[1], to_child[0])); + } + close(to_parent[1]); + close(to_child[0]); + + if (read(to_parent[0], &r, sizeof(r)) != sizeof(r)) + CHILD_GAVE_UP(pid); + for (int i = 0; i < 2; i++) { + remember_loop(self, r.n[i]); + snprintf(dev[i], sizeof(dev[i]), "/dev/loop%d", r.n[i]); + snprintf(sysfs[i], sizeof(sysfs[i]), "/sys/block/loop%d/loop/backing_file", r.n[i]); + } + + /* step one: the mount with the first loop mount below it goes */ + ASSERT_EQ(rmdir(at("vol")), 0); + + /* step two: the other one, with the loop mount whose image is on the first */ + ASSERT_EQ(rmdir(at("vol2")), 0); + + /* the child leaves: its namespace and every reference it held are gone */ + ASSERT_EQ(write(to_child[1], "", 1), 1); + CHILD_LEFT(pid); + close(to_parent[0]); + close(to_child[1]); + + /* nothing can reach the four mounts any more */ + for (int i = 0; i < 2; i++) { + ASSERT_FALSE(mounted_anywhere(dev[i])); + strcpy(self->dev, dev[i]); + strcpy(self->sysfs, sysfs[i]); + assert_loop_released(_metadata, self); + } +} + +/* + * The holders. Each keeps a file or a path on a mount P for as long as + * its own filesystem or device lives, and each has that filesystem or + * device mounted at C below P. Once another namespace's rmdir has + * detached P with C connected below it and the child is gone, P is owned + * by nobody, C by P, and P is kept by whatever the holder still holds. + * + * P is a loop mount so that its death can be observed: the loop device + * gives its backing file up when P's superblock goes. The image sits on + * a tmpfs next to P, not above it, so the loop device's own reference is + * not part of the picture. + */ +enum holder { + HOLDER_AUTOFS, /* a FIFO on P as the daemon's pipe */ + HOLDER_ZRAM, /* a device node on P as the writeback device */ + HOLDER_ECRYPTFS, /* a directory on P as the lower directory */ + HOLDER_BINFMT_MISC, /* an executable on P as an 'F' interpreter */ + HOLDER_FUSE, /* a file on P as a passthrough backing file */ + HOLDER_ZLOOP, /* a directory on P for the zone files */ + HOLDER_GADGET, /* a file on P as a mass storage LUN, over dummy_hcd */ + HOLDER_MD, /* a file on P as an array's bitmap file */ +}; + +#define HOLDER_IMG at("img/p.img") +#define HOLDER_MNT at("p") +#define HOLDER_BELOW at("p/c") + +static int write_file(const char *path, const char *s) +{ + int fd = open(path, O_WRONLY); + ssize_t n; + + if (fd < 0) + return -1; + n = write(fd, s, strlen(s)); + close(fd); + return n == (ssize_t)strlen(s) ? 0 : -1; +} + +/* Bind a free loop device to @img; the device number. */ +static int loop_attach(const char *img) +{ + int ifd, n; + + ifd = open(img, O_RDWR); + if (ifd < 0) + return -1; + n = loop_bind(ifd); + close(ifd); + return n; +} + +static int holder_autofs(void) +{ + char opts[64]; + int pfd; + + if (mkfifo(at("p/pipe"), 0600)) + return CHILD_HOLDER; + pfd = open(at("p/pipe"), O_RDWR); + if (pfd < 0) + return CHILD_HOLDER; + snprintf(opts, sizeof(opts), "fd=%d,minproto=5,maxproto=5", pfd); + if (mount("autofs", HOLDER_BELOW, "autofs", 0, opts)) + return errno == ENODEV ? CHILD_SKIP : CHILD_HOLDER; + close(pfd); /* the mount keeps its own */ + return CHILD_OK; +} + +/* + * zram's writeback device has to be a block device node, and that node has + * to be on P. Point it at a second loop device. + */ +static void zram_reset(void); + +static int holder_zram(void) +{ + char dev[32], buf[64]; + struct stat st; + int fd, n; + + if (access("/sys/block/zram0/backing_dev", W_OK)) + return CHILD_SKIP; + /* somebody else's device, leave it alone */ + if (read_sysfs("/sys/block/zram0/initstate", buf, sizeof(buf)) || buf[0] != '0') + return CHILD_SKIP; + fd = open(at("img/wb.img"), O_RDWR | O_CREAT | O_EXCL, 0600); + if (fd < 0 || ftruncate(fd, IMAGE_SIZE)) + return CHILD_HOLDER; + close(fd); + n = loop_attach(at("img/wb.img")); + if (n < 0) + return CHILD_HOLDER; + /* the minor is not the number of the device when loop has partitions */ + snprintf(dev, sizeof(dev), "/dev/loop%d", n); + if (stat(dev, &st) || mknod(at("p/wbdev"), S_IFBLK | 0600, st.st_rdev)) + return CHILD_HOLDER; + if (write_file("/sys/block/zram0/backing_dev", at("p/wbdev"))) + return CHILD_HOLDER; + snprintf(buf, sizeof(buf), "%d", 4 * 1024 * 1024); + if (write_file("/sys/block/zram0/disksize", buf)) + goto undo; + fd = open("/dev/zram0", O_RDWR); + if (fd < 0) + goto undo; + n = write_fat12_4k(fd, 1024); /* zram has 4 KiB blocks */ + close(fd); /* the reset in undo is refused while the device is open */ + if (n) + goto undo; + if (mount_fat("/dev/zram0", HOLDER_BELOW)) { + n = errno == ENODEV ? CHILD_NOFS : CHILD_HOLDER; + zram_reset(); + return n; + } + return CHILD_OK; +undo: + zram_reset(); + return CHILD_HOLDER; +} + +/* Is @alg in /proc/crypto? A module is listed once a request has loaded it. */ +static bool crypto_has(const char *alg) +{ + char line[256], name[64]; + bool found = false; + FILE *f; + + f = fopen("/proc/crypto", "r"); + if (!f) + return true; /* no way to tell, assume it is */ + while (!found && fgets(line, sizeof(line), f)) + found = sscanf(line, "name : %63s", name) == 1 && !strcmp(name, alg); + fclose(f); + return found; +} + +/* The kernel's auth token layout, which userspace has to match byte for byte. */ +struct ecryptfs_auth_tok { + __u16 version; + __u16 token_type; + __u32 flags; + struct { + __u32 flags, encrypted_key_size, decrypted_key_size; + __u8 encrypted_key[512], decrypted_key[64]; + } session_key; + __u8 reserved[32]; + struct { + __u32 password_bytes; + __s32 hash_algo; + __u32 hash_iterations, session_key_encryption_key_bytes, flags; + __u8 session_key_encryption_key[64]; + __u8 signature[17]; + __u8 salt[8]; + } password; +} __attribute__((packed)); + +#define ECRYPTFS_SIG "0123456789abcdef" + +static int holder_ecryptfs(void) +{ + struct ecryptfs_auth_tok tok = { + .version = 0x0004, + .token_type = 0, /* ECRYPTFS_PASSWORD */ + .password.session_key_encryption_key_bytes = 16, + .password.flags = 0x02, /* ECRYPTFS_SESSION_KEY_ENCRYPTION_KEY_SET */ + .password.signature = ECRYPTFS_SIG, + }; + + /* a session keyring of this child's own, so that the key goes with it */ + if (syscall(__NR_keyctl, KEYCTL_JOIN_SESSION_KEYRING, NULL) < 0) + return errno == ENOSYS ? CHILD_SKIP : CHILD_HOLDER; + if (syscall(__NR_add_key, "user", ECRYPTFS_SIG, &tok, sizeof(tok), + KEY_SPEC_SESSION_KEYRING) < 0) + return CHILD_HOLDER; + if (mkdir(at("p/lower"), 0755)) + return CHILD_HOLDER; + if (mount(at("p/lower"), HOLDER_BELOW, "ecryptfs", 0, + "ecryptfs_sig=" ECRYPTFS_SIG ",ecryptfs_cipher=aes,ecryptfs_key_bytes=16")) { + /* EINVAL without the cipher; a module is loaded by the attempt */ + if (errno == ENODEV || (errno == EINVAL && !crypto_has("aes"))) + return CHILD_SKIP; + return CHILD_HOLDER; + } + return CHILD_OK; +} + +/* + * binfmt_misc instances are per user namespace, so the mount below P is + * made from a new one, which gets a copy of P. + */ +static int holder_binfmt_misc(void) +{ + static const char interp[] = "#!/bin/true\n"; + char reg[PATH_MAX]; + int out; + + /* 'F' opens the interpreter at registration, nothing runs it here */ + out = open(at("p/interp"), O_WRONLY | O_CREAT | O_EXCL, 0755); + if (out < 0 || write(out, interp, sizeof(interp) - 1) != sizeof(interp) - 1) + return CHILD_HOLDER; + close(out); + + /* ENOSPC: user.max_user_namespaces is 0, EPERM: an LSM says no */ + if (unshare(CLONE_NEWUSER | CLONE_NEWNS)) { + if (errno == EINVAL || errno == ENOSPC || errno == EPERM) + return CHILD_SKIP; + return CHILD_HOLDER; + } + if (write_file("/proc/self/setgroups", "deny") || + write_file("/proc/self/uid_map", "0 0 1") || + write_file("/proc/self/gid_map", "0 0 1")) + return CHILD_HOLDER; + if (mount("binfmt_misc", HOLDER_BELOW, "binfmt_misc", 0, NULL)) + return errno == ENODEV ? CHILD_SKIP : CHILD_HOLDER; + snprintf(reg, sizeof(reg), ":cycle:E::cyc::%s:F", at("p/interp")); + if (write_file(at("p/c/register"), reg)) + return CHILD_HOLDER; + return CHILD_OK; +} + +/* + * A fuse server that only ever answers FUSE_INIT, with passthrough on, and + * then registers a file on P as a backing file. The registration alone + * makes the fuse superblock hold the file. + */ +static int holder_fuse(void) +{ + struct fuse_backing_map map = {}; + struct fuse_in_header *ih; + struct fuse_init_out init = { + .major = FUSE_KERNEL_VERSION, + .minor = FUSE_KERNEL_MINOR_VERSION, + .flags = FUSE_INIT_EXT, + .flags2 = FUSE_PASSTHROUGH >> 32, + .max_write = 4096, + .max_stack_depth = 1, + }; + struct fuse_out_header oh = { .len = sizeof(oh) + sizeof(init) }; + struct iovec iov[2] = { { &oh, sizeof(oh) }, { &init, sizeof(init) } }; + char opts[64], buf[8192]; + int ffd, bfd; + ssize_t n; + + bfd = open(at("p/backing"), O_RDWR | O_CREAT | O_EXCL, 0600); + if (bfd < 0) + return CHILD_HOLDER; + ffd = open("/dev/fuse", O_RDWR); + if (ffd < 0) + return CHILD_SKIP; + snprintf(opts, sizeof(opts), "fd=%d,rootmode=40000,user_id=0,group_id=0", ffd); + if (mount("fuse", HOLDER_BELOW, "fuse", 0, opts)) + return errno == ENODEV ? CHILD_SKIP : CHILD_HOLDER; + + n = read(ffd, buf, sizeof(buf)); + ih = (void *)buf; + if (n < (ssize_t)sizeof(*ih) || ih->opcode != FUSE_INIT) + return CHILD_HOLDER; + oh.unique = ih->unique; + if (writev(ffd, iov, 2) != (ssize_t)oh.len) + return CHILD_HOLDER; + + map.fd = bfd; + if (ioctl(ffd, FUSE_DEV_IOC_BACKING_OPEN, &map) < 0) { + /* EOPNOTSUPP: no FUSE_PASSTHROUGH, ENOTTY: no such ioctl */ + if (errno == EPERM || errno == EOPNOTSUPP || errno == ENOTTY) + return CHILD_SKIP; + return CHILD_HOLDER; + } + close(bfd); /* the connection keeps its own */ + return CHILD_OK; /* ffd stays open until the child exits */ +} + +/* Wait for a device node the kernel is about to create. */ +static int open_when_there(const char *dev, int flags, int ms) +{ + int fd; + + for (; ms > 0; ms -= 100) { + fd = open(dev, flags); + if (fd >= 0) + return fd; + usleep(100000); + } + return -1; +} + +/* + * zloop keeps every zone file open. One conventional zone is enough for a + * FAT image, the sequential one stays empty. + */ +static int holder_zloop(void) +{ + char cmd[PATH_MAX]; + int fd, ret = CHILD_HOLDER; + + if (access("/dev/zloop-control", W_OK)) + return CHILD_SKIP; + /* somebody else's device, leave it alone */ + if (!access("/sys/block/zloop0", F_OK)) + return CHILD_SKIP; + if (mkdir(at("p/zl"), 0755) || mkdir(at("p/zl/0"), 0755)) + return CHILD_HOLDER; + snprintf(cmd, sizeof(cmd), + "add id=0,capacity_mb=8,zone_size_mb=4,conv_zones=1,base_dir=%s", + at("p/zl")); + if (write_file("/dev/zloop-control", cmd)) + return CHILD_HOLDER; + fd = open_when_there("/dev/zloop0", O_RDWR, 5000); + if (fd < 0 || write_fat12_4k(fd, 1024)) /* 4 KiB blocks, like P */ + goto undo; + close(fd); + fd = -1; + if (mount_fat("/dev/zloop0", HOLDER_BELOW)) { + ret = errno == ENODEV ? CHILD_NOFS : CHILD_HOLDER; + goto undo; + } + return CHILD_OK; +undo: + if (fd >= 0) + close(fd); + write_file("/dev/zloop-control", "remove id=0"); + return ret; +} + +static int mount_configfs(void) +{ + if (access("/sys/kernel/config", F_OK)) + return -1; + if (mount("configfs", "/sys/kernel/config", "configfs", 0, NULL) && errno != EBUSY) + return -1; + return 0; +} + +/* Take the gadget out of configfs again, in the reverse order of its creation. */ +static void gadget_remove(void) +{ + if (mount_configfs()) + return; + write_file(gat("/functions/mass_storage.0/lun.0/file"), "\n"); + write_file(gat("/UDC"), "\n"); + unlink(gat("/configs/c.1/mass_storage.0")); + rmdir(gat("/configs/c.1/strings/0x409")); + rmdir(gat("/configs/c.1")); + rmdir(gat("/functions/mass_storage.0")); + rmdir(gat("/strings/0x409")); + rmdir(gat("")); +} + +/* The disk usb-storage created for the gadget, by the LUN's inquiry string. */ +static int find_gadget_disk(char *dev, size_t len, int ms) +{ + char path[PATH_MAX], model[64]; + struct dirent *de; + DIR *d; + + for (; ms > 0; ms -= 100, usleep(100000)) { + d = opendir("/sys/block"); + if (!d) + return -1; + while ((de = readdir(d))) { + if (strncmp(de->d_name, "sd", 2)) + continue; + snprintf(path, sizeof(path), "/sys/block/%s/device/model", de->d_name); + if (read_sysfs(path, model, sizeof(model)) || + strncmp(model, "File-Stor Gadget", 16)) + continue; + snprintf(dev, len, "/dev/%s", de->d_name); + closedir(d); + return 0; + } + closedir(d); + } + return -1; +} + +/* + * A mass storage gadget bound to the dummy UDC, so that this kernel is + * also the USB host that sees the LUN as a SCSI disk. sd locks the + * medium on open, which is what keeps the LUN's file from being ejected. + */ +#define GADGET_STEP(x) do { \ + if (x) { \ + fprintf(stderr, "gadget: %s failed: %s\n", #x, strerror(errno)); \ + gadget_remove(); \ + return CHILD_HOLDER; \ + } \ +} while (0) + +static int holder_gadget(void) +{ + char dev[PATH_MAX]; + int fd; + + if (mount_configfs()) + return CHILD_SKIP; + if (access("/sys/kernel/config/usb_gadget", F_OK) || + access("/sys/class/udc/dummy_udc.0", F_OK)) + return CHILD_SKIP; + + fd = open(at("p/lun.img"), O_RDWR | O_CREAT | O_EXCL, 0600); + if (fd < 0 || write_fat12(fd)) + return CHILD_HOLDER; + close(fd); + + GADGET_STEP(mkdir(gat(""), 0755)); + GADGET_STEP(write_file(gat("/idVendor"), "0x1d6b")); + GADGET_STEP(write_file(gat("/idProduct"), "0x0104")); + GADGET_STEP(mkdir(gat("/strings/0x409"), 0755)); + GADGET_STEP(write_file(gat("/strings/0x409/serialnumber"), "1")); + GADGET_STEP(write_file(gat("/strings/0x409/manufacturer"), "kselftest")); + GADGET_STEP(write_file(gat("/strings/0x409/product"), "cycle")); + GADGET_STEP(mkdir(gat("/configs/c.1"), 0755)); + GADGET_STEP(mkdir(gat("/configs/c.1/strings/0x409"), 0755)); + GADGET_STEP(write_file(gat("/configs/c.1/strings/0x409/configuration"), "c")); + GADGET_STEP(mkdir(gat("/functions/mass_storage.0"), 0755)); + GADGET_STEP(write_file(gat("/functions/mass_storage.0/lun.0/removable"), "1")); + GADGET_STEP(write_file(gat("/functions/mass_storage.0/lun.0/file"), at("p/lun.img"))); + GADGET_STEP(symlink(gat("/functions/mass_storage.0"), gat("/configs/c.1/mass_storage.0"))); + /* EBUSY: the controller is somebody else's, leave it alone */ + if (write_file(gat("/UDC"), "dummy_udc.0")) { + fd = errno == EBUSY ? CHILD_SKIP : CHILD_HOLDER; + gadget_remove(); + return fd; + } + + /* usb-storage waits a second before it scans the device */ + if (find_gadget_disk(dev, sizeof(dev), 15000)) { + /* without usb-storage and sd the LUN never shows up as a disk */ + fd = (access("/sys/bus/usb/drivers/usb-storage", F_OK) || + access("/sys/bus/scsi/drivers/sd", F_OK)) ? CHILD_SKIP : CHILD_HOLDER; + gadget_remove(); + return fd; + } + fd = open_when_there(dev, O_RDONLY, 5000); + GADGET_STEP(fd < 0); + close(fd); + if (mount_fat(dev, HOLDER_BELOW)) { + fd = errno == ENODEV ? CHILD_NOFS : CHILD_HOLDER; + gadget_remove(); + return fd; + } + return CHILD_OK; +} + +#define BITMAP_MAGIC 0x6d746962 + +/* + * A RAID1 of one loop device, not persistent, with its bitmap in a file + * on P. The bitmap file needs a superblock the kernel accepts; sync_size, + * uuid and events are not looked at for a non-persistent array. + */ +static int holder_md(void) +{ + struct { + __u32 magic, version; + __u8 uuid[16]; + __u64 events, events_cleared, sync_size; + __u32 state, chunksize, daemon_sleep, write_behind; + } bsb = { + .magic = BITMAP_MAGIC, + .version = 4, + .chunksize = 64 * 1024, + .daemon_sleep = 5, + }; + mdu_array_info_t info = { + .level = 1, + .raid_disks = 1, + .size = 8 * 1024, /* KiB */ + .not_persistent = 1, + }; + mdu_disk_info_t disk = { + .major = 7, + .state = (1 << MD_DISK_ACTIVE) | (1 << MD_DISK_SYNC), + }; + int fd, mdfd, bfd, n, ret = CHILD_HOLDER; + char dev[32], buf[4096]; + struct stat st; + + fd = open(at("img/md.img"), O_RDWR | O_CREAT | O_EXCL, 0600); + if (fd < 0 || ftruncate(fd, 8 * 1024 * 1024)) + return CHILD_HOLDER; + close(fd); + n = loop_attach(at("img/md.img")); + if (n < 0) + return CHILD_HOLDER; + /* the minor is not the number of the device when loop has partitions */ + snprintf(dev, sizeof(dev), "/dev/loop%d", n); + if (stat(dev, &st)) + return CHILD_HOLDER; + disk.major = major(st.st_rdev); + disk.minor = minor(st.st_rdev); + + bfd = open(at("p/bitmap"), O_RDWR | O_CREAT | O_EXCL, 0600); + if (bfd < 0 || write(bfd, &bsb, sizeof(bsb)) != sizeof(bsb) || ftruncate(bfd, 4096)) + return CHILD_HOLDER; + + if (read_sysfs("/proc/mdstat", buf, sizeof(buf))) { + fprintf(stderr, "md: no /proc/mdstat\n"); + return CHILD_SKIP; + } + if (!strstr(buf, "[raid1]")) { + fprintf(stderr, "md: no raid1 personality: %s\n", buf); + return CHILD_SKIP; + } + /* a node of our own: /dev may not have one and is not ours to change */ + if (mknod(at("img/md0"), S_IFBLK | 0600, makedev(MD_MAJOR, 0))) + return CHILD_HOLDER; + mdfd = open(at("img/md0"), O_RDWR); + if (mdfd < 0) + return CHILD_SKIP; + /* somebody else's array: SET_ARRAY_INFO would say EINVAL, not EBUSY */ + if (!read_sysfs("/sys/block/md0/md/array_state", buf, sizeof(buf)) && + strncmp(buf, "clear", 5)) { + fprintf(stderr, "md: md0 is in use: %s", buf); + close(mdfd); + return CHILD_SKIP; + } + /* the bitmap ops are only installed once a bitmap type is chosen */ + write_file("/sys/block/md0/md/bitmap_type", "bitmap"); + /* EBUSY: somebody else's array, leave it alone */ + if (ioctl(mdfd, SET_ARRAY_INFO, &info)) + return errno == EBUSY ? CHILD_SKIP : CHILD_HOLDER; + /* the array is ours from here on and has to be stopped on every path */ + if (ioctl(mdfd, ADD_NEW_DISK, &disk)) + goto undo; + /* attach the bitmap file before the array runs, the way mdadm does */ + if (ioctl(mdfd, SET_BITMAP_FILE, bfd)) { + fprintf(stderr, "md: SET_BITMAP_FILE: %s\n", strerror(errno)); + if (errno == EINVAL) + ret = CHILD_SKIP; + goto undo; + } + close(bfd); /* the array keeps its own */ + if (ioctl(mdfd, RUN_ARRAY, NULL)) { + fprintf(stderr, "md: RUN_ARRAY: %s\n", strerror(errno)); + goto undo; + } + if (write_fat12(mdfd)) + goto undo; + close(mdfd); + if (mount_fat(at("img/md0"), HOLDER_BELOW)) { + ret = errno == ENODEV ? CHILD_NOFS : CHILD_HOLDER; + mdfd = open(at("img/md0"), O_RDWR); + goto undo; + } + return CHILD_OK; +undo: + if (mdfd >= 0) { + ioctl(mdfd, STOP_ARRAY); + close(mdfd); + } + return ret; +} + +/* + * A device keeps its file for as long as it is configured, so once nothing + * below the dead mount is left the device has to be told to let go. With + * the cycle unbroken C's filesystem still holds the device and every one + * of these refuses. + */ +static int holder_let_go_once(enum holder holder) +{ + int fd, ret; + + switch (holder) { + case HOLDER_ZRAM: + return write_file("/sys/block/zram0/reset", "1"); + case HOLDER_ZLOOP: + return write_file("/dev/zloop-control", "remove id=0"); + case HOLDER_GADGET: + if (mount_configfs()) + return -1; + /* a zero-length write is a no-op for configfs; a newline ejects */ + return write_file(gat("/functions/mass_storage.0/lun.0/file"), "\n"); + case HOLDER_MD: + fd = open(at("img/md0"), O_RDONLY); + if (fd < 0) { + /* the child's node went with its tmpfs */ + mknod(at("md0"), S_IFBLK | 0600, makedev(MD_MAJOR, 0)); + fd = open(at("md0"), O_RDONLY); + } + if (fd < 0) + return -1; + ret = ioctl(fd, STOP_ARRAY); + close(fd); + return ret; + default: + return 0; + } +} + +static void zram_reset(void) +{ + holder_let_go_once(HOLDER_ZRAM); +} + +/* + * In its own mount namespace the child puts the image on a tmpfs next to + * P, mounts P from a loop device, and sets the holder up with its own + * filesystem or device mounted at C below P. + */ +static int holder_child(int to_parent, int from_parent, enum holder holder) +{ + bool fifo = holder == HOLDER_AUTOFS || holder == HOLDER_ZRAM; + /* room for a 4 MiB zone file or a floppy image on P */ + bool big = holder == HOLDER_ZLOOP || holder == HOLDER_GADGET; + struct report r = { { -1, -1 } }; + char dev[32], c; + int ifd, n, ret; + + if (unshare(CLONE_NEWNS) || mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL)) + return CHILD_NS; + if (mkdir(at("img"), 0755) || mount("tmpfs", at("img"), "tmpfs", 0, NULL)) + return child_fails(CHILD_NS); + + ifd = open(HOLDER_IMG, O_RDWR | O_CREAT | O_EXCL, 0600); + if (ifd < 0 || (fifo ? write_minix(ifd) : + big ? write_fat12_4k(ifd, 3072) : write_fat12(ifd))) + return child_fails(CHILD_IMAGE); + close(ifd); + n = loop_attach(HOLDER_IMG); + if (n < 0) + return child_fails(CHILD_LOOP); + snprintf(dev, sizeof(dev), "/dev/loop%d", n); + if (mkdir(HOLDER_MNT, 0755)) + return child_fails(CHILD_MOUNT); + if (fifo ? mount(dev, HOLDER_MNT, "minix", 0, NULL) : mount_fat(dev, HOLDER_MNT)) + return child_fails(errno == ENODEV ? CHILD_NOFS : CHILD_MOUNT); + if (mkdir(HOLDER_BELOW, 0755)) + return child_fails(CHILD_MOUNT); + + switch (holder) { + case HOLDER_AUTOFS: + ret = holder_autofs(); + break; + case HOLDER_ZRAM: + ret = holder_zram(); + break; + case HOLDER_ECRYPTFS: + ret = holder_ecryptfs(); + break; + case HOLDER_BINFMT_MISC: + ret = holder_binfmt_misc(); + break; + case HOLDER_FUSE: + ret = holder_fuse(); + break; + case HOLDER_ZLOOP: + ret = holder_zloop(); + break; + case HOLDER_GADGET: + ret = holder_gadget(); + break; + case HOLDER_MD: + ret = holder_md(); + break; + default: + ret = CHILD_HOLDER; + } + if (ret != CHILD_OK) + return child_fails(ret); + + /* P's device first, then the one the holder has bound for itself */ + r.n[0] = n; + if (nr_bound > 1) + r.n[1] = bound[1]; + if (write(to_parent, &r, sizeof(r)) != sizeof(r) || + read(from_parent, &c, 1) != 1) { + /* the parent went away and will not tell the devices to let go */ + umount2(HOLDER_BELOW, MNT_DETACH); + for (int i = 0; i < 50 && holder_let_go_once(holder); i++) + usleep(100000); + if (holder == HOLDER_GADGET) + gadget_remove(); + return child_fails(CHILD_PIPE); + } + return CHILD_OK; +} + +/* + * The release of C's filesystem may still be in flight when the child is + * gone, and a device that is still held refuses to let go, so try for a + * while. With the cycle unbroken it refuses for good. + */ +static void holder_let_go(enum holder holder) +{ + for (int i = 0; i < 50; i++) { + if (!holder_let_go_once(holder)) + return; + usleep(100000); + } +} + +/* + * rmdir of p from here, where it is a plain directory, detaches + * P in the child's namespace with C connected below it. Once the child is + * gone the holder's file on P is the only thing left that refers to P, + * and it is dropped only when C's filesystem dies, which waits for P. + * The loop device backing P tells whether that resolved. + */ +static void holder_cycle(struct __test_metadata *_metadata, + FIXTURE_DATA(loop_cycle) *self, enum holder holder) +{ + struct report r = { { -1, -1 } }; + int to_parent[2], from_parent[2]; + pid_t pid; + + ASSERT_EQ(pipe(to_parent), 0); + ASSERT_EQ(pipe(from_parent), 0); + pid = fork(); + ASSERT_GE(pid, 0); + if (pid == 0) { + close(to_parent[0]); + close(from_parent[1]); + _exit(holder_child(to_parent[1], from_parent[0], holder)); + } + close(to_parent[1]); + close(from_parent[0]); + + if (read(to_parent[0], &r, sizeof(r)) != sizeof(r)) + CHILD_GAVE_UP(pid); + self->gadget = holder == HOLDER_GADGET; + remember_loop(self, r.n[0]); + remember_loop(self, r.n[1]); + snprintf(self->dev, sizeof(self->dev), "/dev/loop%d", r.n[0]); + snprintf(self->sysfs, sizeof(self->sysfs), "/sys/block/loop%d/loop/backing_file", r.n[0]); + + ASSERT_EQ(rmdir(HOLDER_MNT), 0); + ASSERT_EQ(write(from_parent[1], "x", 1), 1); + CHILD_LEFT(pid); + close(to_parent[0]); + close(from_parent[1]); + + holder_let_go(holder); + assert_loop_released(_metadata, self); + /* the teardown takes the gadget and the holder's own loop device away */ +} + +TEST_F(loop_cycle, autofs_pipe_on_dead_mount_released) +{ + holder_cycle(_metadata, self, HOLDER_AUTOFS); +} + +TEST_F(loop_cycle, zram_writeback_node_on_dead_mount_released) +{ + holder_cycle(_metadata, self, HOLDER_ZRAM); +} + +TEST_F(loop_cycle, ecryptfs_lower_on_dead_mount_released) +{ + holder_cycle(_metadata, self, HOLDER_ECRYPTFS); +} + +TEST_F(loop_cycle, binfmt_misc_interpreter_on_dead_mount_released) +{ + holder_cycle(_metadata, self, HOLDER_BINFMT_MISC); +} + +TEST_F(loop_cycle, fuse_backing_file_on_dead_mount_released) +{ + holder_cycle(_metadata, self, HOLDER_FUSE); +} + +TEST_F(loop_cycle, zloop_zone_files_on_dead_mount_released) +{ + holder_cycle(_metadata, self, HOLDER_ZLOOP); +} + +/* up to 15 s for the disk to show up and 12 s for a device that is held */ +TEST_F_TIMEOUT(loop_cycle, mass_storage_lun_on_dead_mount_released, 120) +{ + holder_cycle(_metadata, self, HOLDER_GADGET); +} + +TEST_F(loop_cycle, md_bitmap_file_on_dead_mount_released) +{ + holder_cycle(_metadata, self, HOLDER_MD); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/filesystems/mount_cycle/mount_cover_test.c b/tools/testing/selftests/filesystems/mount_cycle/mount_cover_test.c new file mode 100644 index 000000000000..0a0b0c037691 --- /dev/null +++ b/tools/testing/selftests/filesystems/mount_cycle/mount_cover_test.c @@ -0,0 +1,579 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * An unmounted mount that would have stayed attached to its unmounted parent + * leaves a cover behind instead. A lookup on the parent at the mountpoint + * finds knullfs: an empty read-only directory that is shared by every cover + * and every kernel thread, so it can't be watched or locked and nothing can + * be mounted on it. The mount itself is a root from then on. Where a file + * was mounted, the stand-in is an empty regular file of the same instance. + */ +#define _GNU_SOURCE +#include <errno.h> +#include <fcntl.h> +#include <sched.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <sys/fanotify.h> +#include <sys/file.h> +#include <sys/inotify.h> +#include <sys/mount.h> +#include <sys/prctl.h> +#include <sys/stat.h> +#include <sys/statvfs.h> +#include <sys/syscall.h> +#include <sys/vfs.h> +#include <sys/wait.h> + +#include "../../kselftest_harness.h" +#include "../readdir_hold.h" + +#ifndef NULL_FS_MAGIC +#define NULL_FS_MAGIC 0x4E554C4C +#endif + +#ifndef TMPFS_MAGIC +#define TMPFS_MAGIC 0x01021994 +#endif + +#ifndef F_SETDELEG +#define F_SETDELEG (F_SETLEASE + 16) +#endif + +/* struct delegation of <linux/fcntl.h>, which doesn't mix with <fcntl.h> */ +struct delegation_req { + uint32_t d_flags; + uint16_t d_type; + uint16_t __pad; +}; + +#define DIR_LEN 64 +#define PATH_LEN 128 + +/* what the parent asks the child to do */ +#define CMD_RMDIR 'r' +#define CMD_CLOSE_T 't' +#define CMD_QUIT 'q' + +static int write_file(const char *path, const char *s) +{ + ssize_t n = -1; + int fd; + + fd = open(path, O_WRONLY | O_CLOEXEC); + if (fd >= 0) { + n = write(fd, s, strlen(s)); + close(fd); + } + return n == (ssize_t)strlen(s) ? 0 : -1; +} + +static int touch(const char *path) +{ + int fd; + + fd = open(path, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0644); + if (fd < 0) + return -1; + close(fd); + return 0; +} + +/* Become root in a new user namespace with a private mount namespace. */ +static int enter_userns(void) +{ + uid_t uid = getuid(); + gid_t gid = getgid(); + char map[32]; + + prctl(PR_SET_DUMPABLE, 1); + if (unshare(CLONE_NEWUSER | CLONE_NEWNS)) + return -1; + if (write_file("/proc/self/setgroups", "deny") && errno != ENOENT) + return -1; + snprintf(map, sizeof(map), "0 %d 1", uid); + if (write_file("/proc/self/uid_map", map)) + return -1; + snprintf(map, sizeof(map), "0 %d 1", gid); + if (write_file("/proc/self/gid_map", map)) + return -1; + if (setgid(0) || setuid(0)) + return -1; + return mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL); +} + +/* + * The child mounts P on @base/p, C on P/covered and the file P/src on P/file + * in a mount namespace of its own, binds P a second time at @base/q and hands + * out descriptors on P and on C. rmdir() of @base/p from here unmounts P + * together with C and the file bind. P and C are held by the descriptors and + * the unmounted children leave their covers behind. On request the child + * removes C's mountpoint through the bind. + * + * It also mounts T on @base/t with a child on T/covered, binds T at @base/u + * with a second child on the same dentry and hands out descriptors on T and + * U. rmdir() of both from here leaves two covers on one mountpoint. On request + * the child lets go of T. + */ +static int cover_child(const char *base, int to_parent, int from_parent) +{ + char p[PATH_LEN], c[PATH_LEN], q[PATH_LEN], qc[PATH_LEN]; + char f[PATH_LEN], src[PATH_LEN], t[PATH_LEN], u[PATH_LEN], tc[PATH_LEN]; + int fds[4], ret; + char cmd; + + if (unshare(CLONE_NEWNS) || mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL)) + return 1; + snprintf(p, sizeof(p), "%s/p", base); + if (mkdir(p, 0755) || mount("tmpfs", p, "tmpfs", 0, NULL)) + return 2; + snprintf(c, sizeof(c), "%s/p/covered", base); + if (mkdir(c, 0755) || mount("tmpfs", c, "tmpfs", 0, NULL)) + return 3; + snprintf(f, sizeof(f), "%s/p/file", base); + snprintf(src, sizeof(src), "%s/p/src", base); + if (touch(src) || touch(f) || mount(src, f, NULL, MS_BIND, NULL)) + return 4; + snprintf(q, sizeof(q), "%s/q", base); + if (mkdir(q, 0755) || mount(p, q, NULL, MS_BIND, NULL)) + return 5; + snprintf(t, sizeof(t), "%s/t", base); + if (mkdir(t, 0755) || mount("tmpfs", t, "tmpfs", 0, NULL)) + return 6; + snprintf(tc, sizeof(tc), "%s/t/covered", base); + if (mkdir(tc, 0755) || mount("tmpfs", tc, "tmpfs", 0, NULL)) + return 7; + snprintf(u, sizeof(u), "%s/u", base); + if (mkdir(u, 0755) || mount(t, u, NULL, MS_BIND, NULL)) + return 8; + snprintf(tc, sizeof(tc), "%s/u/covered", base); + if (mount("tmpfs", tc, "tmpfs", 0, NULL)) + return 9; + fds[0] = open(p, O_PATH | O_DIRECTORY | O_CLOEXEC); + fds[1] = open(c, O_RDONLY | O_DIRECTORY | O_CLOEXEC); + fds[2] = open(t, O_PATH | O_DIRECTORY | O_CLOEXEC); + fds[3] = open(u, O_PATH | O_DIRECTORY | O_CLOEXEC); + if (fds[0] < 0 || fds[1] < 0 || fds[2] < 0 || fds[3] < 0) + return 10; + if (write(to_parent, fds, sizeof(fds)) != sizeof(fds)) + return 11; + + snprintf(qc, sizeof(qc), "%s/q/covered", base); + for (;;) { + if (read(from_parent, &cmd, 1) != 1) + return 12; + switch (cmd) { + case CMD_RMDIR: + ret = rmdir(qc) ? errno : 0; + if (write(to_parent, &ret, sizeof(ret)) != sizeof(ret)) + return 13; + break; + case CMD_CLOSE_T: + ret = close(fds[2]) ? errno : 0; + if (write(to_parent, &ret, sizeof(ret)) != sizeof(ret)) + return 13; + break; + case CMD_QUIT: + return 0; + default: + return 14; + } + } +} + +FIXTURE(mount_cover) { + char base[DIR_LEN]; + pid_t child; + int to_child; + int from_child; + int dfd; /* P, unmounted, held */ + int cfd; /* C, unmounted, held */ + int fd; /* what a lookup on P finds at C's mountpoint */ + int tfd; /* T, unmounted, held */ + int ufd; /* U, a bind of T, unmounted, held */ + int fan; /* fanotify group from before the user namespace, or -1 */ + struct readdir_hold hold; /* likewise from before, uffd -1 without */ +}; + +/* + * Everything the setup has made. The harness does not run the teardown + * when an assertion of the setup fails, so the setup calls this itself. + */ +static void cover_cleanup(FIXTURE_DATA(mount_cover) *self) +{ + char cmd = CMD_QUIT; + int status; + + if (self->fd >= 0) + close(self->fd); + if (self->cfd >= 0) + close(self->cfd); + if (self->dfd >= 0) + close(self->dfd); + if (self->tfd >= 0) + close(self->tfd); + if (self->ufd >= 0) + close(self->ufd); + if (self->fan >= 0) + close(self->fan); + readdir_hold_destroy(&self->hold); + if (self->child > 0) { + if (write(self->to_child, &cmd, 1) != 1) + kill(self->child, SIGKILL); + waitpid(self->child, &status, 0); + } + if (self->to_child >= 0) + close(self->to_child); + if (self->from_child >= 0) + close(self->from_child); + umount2(self->base, MNT_DETACH); + rmdir(self->base); +} + +static int same_file(int fd1, int fd2) +{ + struct stat st1, st2; + + if (fstat(fd1, &st1) || fstat(fd2, &st2)) + return 0; + return st1.st_dev == st2.st_dev && st1.st_ino == st2.st_ino; +} + +FIXTURE_SETUP(mount_cover) +{ + int to_parent[2], to_child[2], fds[4], pidfd, status; + char dir[PATH_LEN]; + struct statfs sf; + + self->child = 0; + self->to_child = self->from_child = -1; + self->dfd = self->cfd = self->fd = self->tfd = self->ufd = -1; + + /* + * A group for plain events takes CAP_SYS_ADMIN in the initial user + * namespace, so get one before that is gone. An inode mark can be + * added to it from anywhere. + */ + self->fan = fanotify_init(FAN_CLASS_NOTIF | FAN_CLOEXEC, O_RDONLY); + /* same for the userfaultfd that holds a readdir in its fault */ + readdir_hold_init(&self->hold); + + snprintf(self->base, sizeof(self->base), "/tmp/mount_cover.XXXXXX"); + ASSERT_NE(mkdtemp(self->base), NULL); + if (enter_userns()) { + cover_cleanup(self); + SKIP(return, "test requires user namespaces"); + } + ASSERT_EQ(mount("tmpfs", self->base, "tmpfs", 0, NULL), 0) + cover_cleanup(self); + + snprintf(dir, sizeof(dir), "%s/p", self->base); + ASSERT_EQ(pipe(to_parent), 0) + cover_cleanup(self); + ASSERT_EQ(pipe(to_child), 0) + cover_cleanup(self); + self->child = fork(); + ASSERT_GE(self->child, 0) + cover_cleanup(self); + if (self->child == 0) { + close(to_parent[0]); + close(to_child[1]); + _exit(cover_child(self->base, to_parent[1], to_child[0])); + } + close(to_parent[1]); + close(to_child[0]); + self->to_child = to_child[1]; + self->from_child = to_parent[0]; + if (read(self->from_child, fds, sizeof(fds)) != sizeof(fds)) { + pid_t pid = self->child; + + waitpid(pid, &status, 0); + self->child = 0; + cover_cleanup(self); + ASSERT_TRUE(false) + TH_LOG("child failed to set up: %s %d", + WIFEXITED(status) ? "step" : "signal", + WIFEXITED(status) ? WEXITSTATUS(status) : WTERMSIG(status)); + } + + pidfd = syscall(__NR_pidfd_open, self->child, 0); + ASSERT_GE(pidfd, 0) + cover_cleanup(self); + self->dfd = syscall(__NR_pidfd_getfd, pidfd, fds[0], 0); + self->cfd = syscall(__NR_pidfd_getfd, pidfd, fds[1], 0); + self->tfd = syscall(__NR_pidfd_getfd, pidfd, fds[2], 0); + self->ufd = syscall(__NR_pidfd_getfd, pidfd, fds[3], 0); + close(pidfd); + ASSERT_GE(self->dfd, 0) + cover_cleanup(self); + ASSERT_GE(self->cfd, 0) + cover_cleanup(self); + ASSERT_GE(self->tfd, 0) + cover_cleanup(self); + ASSERT_GE(self->ufd, 0) + cover_cleanup(self); + + /* unmounts P and C, C leaves its cover behind */ + ASSERT_EQ(rmdir(dir), 0) + cover_cleanup(self); + /* unmounts T and U with their children, two covers on one mountpoint */ + snprintf(dir, sizeof(dir), "%s/t", self->base); + ASSERT_EQ(rmdir(dir), 0) + cover_cleanup(self); + snprintf(dir, sizeof(dir), "%s/u", self->base); + ASSERT_EQ(rmdir(dir), 0) + cover_cleanup(self); + + self->fd = openat(self->dfd, "covered", O_RDONLY | O_DIRECTORY | O_CLOEXEC); + ASSERT_GE(self->fd, 0) + cover_cleanup(self); + ASSERT_EQ(fstatfs(self->fd, &sf), 0) + cover_cleanup(self); + ASSERT_EQ(sf.f_type, NULL_FS_MAGIC) + cover_cleanup(self); +} + +FIXTURE_TEARDOWN(mount_cover) +{ + cover_cleanup(self); +} + +TEST_F(mount_cover, not_watchable) +{ + char p[PATH_LEN]; + int ifd; + + snprintf(p, sizeof(p), "/proc/self/fd/%d", self->fd); + + ifd = inotify_init1(IN_CLOEXEC); + if (ifd < 0 && errno == ENOSYS) { + TH_LOG("no inotify in this kernel, skipping that part"); + } else { + ASSERT_GE(ifd, 0); + EXPECT_EQ(inotify_add_watch(ifd, p, IN_OPEN), -1); + EXPECT_EQ(errno, EINVAL); + close(ifd); + } + + /* dnotify ends up at the same place */ + EXPECT_EQ(fcntl(self->fd, F_NOTIFY, DN_ACCESS), -1); + EXPECT_EQ(errno, EINVAL); + + if (self->fan < 0) { + TH_LOG("no fanotify group without CAP_SYS_ADMIN in the initial user namespace, skipping the fanotify part"); + return; + } + EXPECT_EQ(fanotify_mark(self->fan, FAN_MARK_ADD, FAN_OPEN, self->fd, NULL), -1); + EXPECT_EQ(errno, EINVAL); +} + +TEST_F(mount_cover, not_lockable) +{ + struct flock fl = { + .l_type = F_RDLCK, + .l_whence = SEEK_SET, + }; + + EXPECT_EQ(flock(self->fd, LOCK_EX | LOCK_NB), -1); + EXPECT_EQ(errno, ENOLCK); + EXPECT_EQ(fcntl(self->fd, F_SETLK, &fl), -1); + EXPECT_EQ(errno, ENOLCK); + EXPECT_EQ(fcntl(self->fd, F_GETLK, &fl), -1); + EXPECT_EQ(errno, ENOLCK); +} + +/* a lease is refused for the owner and for everybody else alike */ +TEST_F(mount_cover, not_leasable) +{ + struct delegation_req deleg = { + .d_type = F_RDLCK, + }; + + EXPECT_EQ(fcntl(self->fd, F_SETLEASE, F_RDLCK), -1); + EXPECT_TRUE(errno == EINVAL || errno == EACCES); + EXPECT_EQ(fcntl(self->fd, F_SETDELEG, &deleg), -1); + EXPECT_TRUE(errno == EINVAL || errno == EACCES); +} + +TEST_F(mount_cover, not_mountable) +{ + char p[PATH_LEN]; + + snprintf(p, sizeof(p), "/proc/self/fd/%d", self->fd); + EXPECT_EQ(mount("tmpfs", p, "tmpfs", 0, NULL), -1); + EXPECT_EQ(errno, ENOENT); +} + +TEST_F(mount_cover, read_only) +{ + struct statvfs sv; + + EXPECT_EQ(mkdirat(self->fd, "x", 0755), -1); + EXPECT_EQ(errno, ENOENT); + EXPECT_EQ(fchmod(self->fd, 0777), -1); + EXPECT_EQ(errno, EROFS); + /* the immutable inode is checked before the read-only mount */ + EXPECT_EQ(faccessat(self->fd, ".", W_OK, 0), -1); + EXPECT_EQ(errno, EPERM); + ASSERT_EQ(fstatvfs(self->fd, &sv), 0); + EXPECT_TRUE(sv.f_flag & ST_RDONLY); +} + +/* the stand-in is a root of its own, ".." stays put */ +TEST_F(mount_cover, island) +{ + int fd; + + fd = openat(self->fd, "..", O_RDONLY | O_DIRECTORY | O_CLOEXEC); + ASSERT_GE(fd, 0); + EXPECT_TRUE(same_file(fd, self->fd)); + close(fd); +} + +/* + * C is alive for as long as the child holds it, but it can't be reached + * through P anymore and it's a root of its own as well. + */ +TEST_F(mount_cover, held_child_detached) +{ + struct statfs sf; + int fd; + + ASSERT_EQ(fstatfs(self->cfd, &sf), 0); + EXPECT_EQ(sf.f_type, TMPFS_MAGIC); + + fd = openat(self->cfd, "..", O_RDONLY | O_DIRECTORY | O_CLOEXEC); + ASSERT_GE(fd, 0); + EXPECT_TRUE(same_file(fd, self->cfd)); + close(fd); + + fd = openat(self->dfd, "covered", O_RDONLY | O_DIRECTORY | O_CLOEXEC); + ASSERT_GE(fd, 0); + ASSERT_EQ(fstatfs(fd, &sf), 0); + EXPECT_EQ(sf.f_type, NULL_FS_MAGIC); + close(fd); +} + +/* + * The cover goes with its mountpoint: once the child has removed C's + * mountpoint through the bind of P, the name is gone from P as well. + * What was opened through the cover before stays open. + */ +TEST_F(mount_cover, cover_goes_with_mountpoint) +{ + char cmd = CMD_RMDIR; + struct statfs sf; + int ret; + + ASSERT_EQ(write(self->to_child, &cmd, 1), 1); + ASSERT_EQ(read(self->from_child, &ret, sizeof(ret)), sizeof(ret)); + ASSERT_EQ(ret, 0); + + EXPECT_EQ(openat(self->dfd, "covered", O_RDONLY | O_DIRECTORY | O_CLOEXEC), -1); + EXPECT_EQ(errno, ENOENT); + ASSERT_EQ(fstatfs(self->fd, &sf), 0); + EXPECT_EQ(sf.f_type, NULL_FS_MAGIC); +} + +/* + * A readdir of the stand-in holds nothing that others wait for: one holder + * sticks in the page fault of its buffer, and a create and a lookup of + * another come back meanwhile. + */ +TEST_F(mount_cover, readdir_blocks_nobody) +{ + bool stalled; + + if (self->hold.uffd < 0) + SKIP(return, "test requires userfaultfd"); + ASSERT_EQ(readdir_hold_check(&self->hold, self->fd, &stalled), 0); + EXPECT_FALSE(stalled); +} + +/* where a file was mounted, the stand-in is an empty regular file */ +TEST_F(mount_cover, file_stand_in) +{ + char src[PATH_LEN], p[PATH_LEN]; + struct statfs sf; + struct stat st; + char c; + int fd; + + fd = openat(self->dfd, "file", O_RDONLY | O_CLOEXEC); + ASSERT_GE(fd, 0); + ASSERT_EQ(fstat(fd, &st), 0); + EXPECT_TRUE(S_ISREG(st.st_mode)); + ASSERT_EQ(fstatfs(fd, &sf), 0); + EXPECT_EQ(sf.f_type, NULL_FS_MAGIC); + /* the two stand-ins are two inodes */ + EXPECT_FALSE(same_file(fd, self->fd)); + EXPECT_EQ(read(fd, &c, 1), 0); + EXPECT_EQ(flock(fd, LOCK_EX | LOCK_NB), -1); + EXPECT_EQ(errno, ENOLCK); + + snprintf(src, sizeof(src), "%s/src", self->base); + ASSERT_EQ(touch(src), 0); + snprintf(p, sizeof(p), "/proc/self/fd/%d", fd); + EXPECT_EQ(mount(src, p, NULL, MS_BIND, NULL), -1); + EXPECT_EQ(errno, ENOENT); + close(fd); + + /* writes and truncates go nowhere, like /dev/null */ + fd = openat(self->dfd, "file", O_WRONLY | O_TRUNC | O_CLOEXEC); + ASSERT_GE(fd, 0); + EXPECT_EQ(write(fd, "xy", 2), 2); + EXPECT_EQ(ftruncate(fd, 4), 0); + ASSERT_EQ(fstat(fd, &st), 0); + EXPECT_EQ(st.st_size, 0); + /* the shared inode keeps its identity */ + EXPECT_EQ(fchmod(fd, 0600), -1); + EXPECT_EQ(errno, EPERM); + close(fd); + fd = openat(self->dfd, "file", O_RDONLY | O_CLOEXEC); + ASSERT_GE(fd, 0); + EXPECT_EQ(read(fd, &c, 1), 0); + close(fd); + + EXPECT_EQ(openat(self->dfd, "file", O_RDONLY | O_DIRECTORY | O_CLOEXEC), -1); + EXPECT_EQ(errno, ENOTDIR); +} + +/* + * Two unmounted parents left covers on the same dentry. A lookup on + * either finds a stand-in, and U's cover stays when T goes. + */ +TEST_F(mount_cover, shared_mountpoint) +{ + char cmd = CMD_CLOSE_T; + struct statfs sf; + int fd, ret; + + fd = openat(self->tfd, "covered", O_RDONLY | O_DIRECTORY | O_CLOEXEC); + ASSERT_GE(fd, 0); + ASSERT_EQ(fstatfs(fd, &sf), 0); + EXPECT_EQ(sf.f_type, NULL_FS_MAGIC); + close(fd); + + fd = openat(self->ufd, "covered", O_RDONLY | O_DIRECTORY | O_CLOEXEC); + ASSERT_GE(fd, 0); + ASSERT_EQ(fstatfs(fd, &sf), 0); + EXPECT_EQ(sf.f_type, NULL_FS_MAGIC); + close(fd); + + /* the last references to T go, with T its cover */ + ASSERT_EQ(write(self->to_child, &cmd, 1), 1); + ASSERT_EQ(read(self->from_child, &ret, sizeof(ret)), sizeof(ret)); + ASSERT_EQ(ret, 0); + close(self->tfd); + self->tfd = -1; + + fd = openat(self->ufd, "covered", O_RDONLY | O_DIRECTORY | O_CLOEXEC); + ASSERT_GE(fd, 0); + ASSERT_EQ(fstatfs(fd, &sf), 0); + EXPECT_EQ(sf.f_type, NULL_FS_MAGIC); + close(fd); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/filesystems/mount_cycle/nsfs_rbind_loop_test.c b/tools/testing/selftests/filesystems/mount_cycle/nsfs_rbind_loop_test.c new file mode 100644 index 000000000000..0928a584eddc --- /dev/null +++ b/tools/testing/selftests/filesystems/mount_cycle/nsfs_rbind_loop_test.c @@ -0,0 +1,193 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * A recursive bind mount of a namespace file that lives in another mount + * namespace copies whatever is stacked on top of it there. If that includes + * the file of the caller's own mount namespace, or of an older one, the copy + * would pin the namespace it is put in forever. The bind mount has to be + * refused, a plain bind mount of the file itself still works. + */ +#define _GNU_SOURCE +#include <errno.h> +#include <fcntl.h> +#include <sched.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <sys/mount.h> +#include <sys/stat.h> +#include <sys/wait.h> + +#include "../../kselftest_harness.h" + +#define DIR_LEN 64 +#define PATH_LEN 128 + +/* Child exit codes. */ +enum { + CHILD_OK, + CHILD_UNSHARE, /* could not create the newer mount namespace */ + CHILD_PIPE, /* the parent went away */ + CHILD_TMPFS, /* could not mount the tmpfs in the new namespace */ + CHILD_REC_ALLOWED, /* the recursive bind mount was not refused */ + CHILD_REC_ERRNO, /* it was refused with the wrong error */ + CHILD_PLAIN_REFUSED, /* the plain bind mount of the file was refused */ +}; + +static int write_file(const char *path, const char *s) +{ + ssize_t n = -1; + int fd; + + fd = open(path, O_WRONLY | O_CLOEXEC); + if (fd >= 0) { + n = write(fd, s, strlen(s)); + close(fd); + } + return n == (ssize_t)strlen(s) ? 0 : -1; +} + +static int create_file(const char *path) +{ + int fd = open(path, O_WRONLY | O_CREAT | O_CLOEXEC, 0644); + + if (fd < 0) + return -1; + close(fd); + return 0; +} + +/* Become root in a new user namespace with a private mount namespace. */ +static int enter_userns(void) +{ + uid_t uid = getuid(); + gid_t gid = getgid(); + char map[32]; + + if (unshare(CLONE_NEWUSER | CLONE_NEWNS)) + return -1; + if (write_file("/proc/self/setgroups", "deny") && errno != ENOENT) + return -1; + snprintf(map, sizeof(map), "0 %d 1", uid); + if (write_file("/proc/self/uid_map", map)) + return -1; + snprintf(map, sizeof(map), "0 %d 1", gid); + if (write_file("/proc/self/gid_map", map)) + return -1; + if (setgid(0) || setuid(0)) + return -1; + return mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL); +} + +static int send_msg(int fd, char msg) +{ + return write(fd, &msg, 1) == 1 ? 0 : -1; +} + +static char recv_msg(int fd) +{ + char msg; + + if (read(fd, &msg, 1) != 1) + return 0; + return msg; +} + +FIXTURE(nsfs_rbind_loop) { + char dir[DIR_LEN]; + char x[PATH_LEN]; +}; + +FIXTURE_SETUP(nsfs_rbind_loop) +{ + snprintf(self->dir, sizeof(self->dir), "/tmp/nsfs_rbind_loop.XXXXXX"); + ASSERT_NE(mkdtemp(self->dir), NULL); + if (enter_userns()) { + rmdir(self->dir); + SKIP(return, "test requires user namespaces"); + } + ASSERT_EQ(mount("tmpfs", self->dir, "tmpfs", 0, NULL), 0); + snprintf(self->x, sizeof(self->x), "%s/x", self->dir); + ASSERT_EQ(create_file(self->x), 0); +} + +FIXTURE_TEARDOWN(nsfs_rbind_loop) +{ + umount2(self->dir, MNT_DETACH); + rmdir(self->dir); +} + +/* + * The child in the newer mount namespace binds the network namespace file + * mount of the parent through @fd. Recursively that would copy the mount of + * its own mount namespace file that the parent stacked on top. + */ +static int newer_ns_child(const char *dir, int fd, int to_parent, int from_parent) +{ + char src[32], y[PATH_LEN]; + + if (unshare(CLONE_NEWNS)) + return CHILD_UNSHARE; + if (send_msg(to_parent, 'r') || recv_msg(from_parent) != 'g') + return CHILD_PIPE; + + snprintf(y, sizeof(y), "%s/y", dir); + if (mount("tmpfs", y, "tmpfs", 0, NULL)) + return CHILD_TMPFS; + snprintf(src, sizeof(src), "/proc/self/fd/%d", fd); + snprintf(y, sizeof(y), "%s/y/f", dir); + if (create_file(y)) + return CHILD_TMPFS; + + if (!mount(src, y, NULL, MS_BIND | MS_REC, NULL)) + return CHILD_REC_ALLOWED; + if (errno != EINVAL) + return CHILD_REC_ERRNO; + if (mount(src, y, NULL, MS_BIND, NULL)) + return CHILD_PLAIN_REFUSED; + umount2(y, MNT_DETACH); + return CHILD_OK; +} + +TEST_F(nsfs_rbind_loop, own_ns_file_below_foreign_source) +{ + int to_child[2], to_parent[2], fd, status; + char p[PATH_LEN]; + pid_t pid; + + snprintf(p, sizeof(p), "%s/y", self->dir); + ASSERT_EQ(mkdir(p, 0755), 0); + + /* M, a mount of our network namespace file, held by a descriptor */ + ASSERT_EQ(mount("/proc/self/ns/net", self->x, NULL, MS_BIND, NULL), 0); + fd = open(self->x, O_PATH | O_CLOEXEC); + ASSERT_GE(fd, 0); + + ASSERT_EQ(pipe(to_child), 0); + ASSERT_EQ(pipe(to_parent), 0); + pid = fork(); + ASSERT_GE(pid, 0); + if (pid == 0) { + close(to_child[1]); + close(to_parent[0]); + _exit(newer_ns_child(self->dir, fd, to_parent[1], to_child[0])); + } + close(to_child[0]); + close(to_parent[1]); + ASSERT_EQ(recv_msg(to_parent[0]), 'r'); + + /* the child's mount namespace file on top of M */ + snprintf(p, sizeof(p), "/proc/%d/ns/mnt", pid); + ASSERT_EQ(mount(p, self->x, NULL, MS_BIND, NULL), 0); + + ASSERT_EQ(send_msg(to_child[1], 'g'), 0); + ASSERT_EQ(waitpid(pid, &status, 0), pid); + ASSERT_TRUE(WIFEXITED(status)); + ASSERT_EQ(WEXITSTATUS(status), CHILD_OK); + + close(fd); + ASSERT_EQ(umount2(self->x, MNT_DETACH), 0); + ASSERT_EQ(umount2(self->x, MNT_DETACH), 0); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/filesystems/mount_cycle/overmount_ns_file_test.c b/tools/testing/selftests/filesystems/mount_cycle/overmount_ns_file_test.c new file mode 100644 index 000000000000..ea1b33fa0bf8 --- /dev/null +++ b/tools/testing/selftests/filesystems/mount_cycle/overmount_ns_file_test.c @@ -0,0 +1,190 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * A mount namespace file bind-mounted on top of the mount that is moved + * onto a shared mount isn't copied to the peers and slaves. The mount that + * already sits at the destination in a slave has to end up on top of the + * propagated copy, not below the root of the mount namespace file where no + * path walk ever finds it. + */ +#define _GNU_SOURCE +#include <errno.h> +#include <fcntl.h> +#include <sched.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <sys/mount.h> +#include <sys/stat.h> +#include <sys/syscall.h> +#include <sys/wait.h> + +#include "../wrappers.h" +#include "../../kselftest_harness.h" + +#define DIR_LEN 64 +#define PATH_LEN 128 + +static int write_file(const char *path, const char *s) +{ + ssize_t n = -1; + int fd; + + fd = open(path, O_WRONLY | O_CLOEXEC); + if (fd >= 0) { + n = write(fd, s, strlen(s)); + close(fd); + } + return n == (ssize_t)strlen(s) ? 0 : -1; +} + +static int create_file(const char *path, const char *s) +{ + ssize_t n = -1; + int fd; + + fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644); + if (fd >= 0) { + n = write(fd, s, strlen(s)); + close(fd); + } + return n == (ssize_t)strlen(s) ? 0 : -1; +} + +/* the first bytes of the file at @path, "" if it can't be read */ +static const char *read_file(const char *path, char *buf, size_t len) +{ + ssize_t n = -1; + int fd; + + fd = open(path, O_RDONLY | O_CLOEXEC); + if (fd >= 0) { + n = read(fd, buf, len - 1); + close(fd); + } + buf[n > 0 ? n : 0] = '\0'; + return buf; +} + +/* Become root in a new user namespace with a private mount namespace. */ +static int enter_userns(void) +{ + uid_t uid = getuid(); + gid_t gid = getgid(); + char map[32]; + + if (unshare(CLONE_NEWUSER | CLONE_NEWNS)) + return -1; + if (write_file("/proc/self/setgroups", "deny") && errno != ENOENT) + return -1; + snprintf(map, sizeof(map), "0 %d 1", uid); + if (write_file("/proc/self/uid_map", map)) + return -1; + snprintf(map, sizeof(map), "0 %d 1", gid); + if (write_file("/proc/self/gid_map", map)) + return -1; + if (setgid(0) || setuid(0)) + return -1; + return mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL); +} + +FIXTURE(overmount_ns_file) { + char dir[DIR_LEN]; + pid_t child; +}; + +FIXTURE_SETUP(overmount_ns_file) +{ + self->child = -1; + snprintf(self->dir, sizeof(self->dir), "/tmp/overmount_ns_file.XXXXXX"); + ASSERT_NE(mkdtemp(self->dir), NULL); + if (enter_userns()) { + rmdir(self->dir); + SKIP(return, "test requires user namespaces"); + } + ASSERT_EQ(mount("tmpfs", self->dir, "tmpfs", 0, NULL), 0); +} + +FIXTURE_TEARDOWN(overmount_ns_file) +{ + if (self->child > 0) { + kill(self->child, SIGKILL); + waitpid(self->child, NULL, 0); + } + umount2(self->dir, MNT_DETACH); + rmdir(self->dir); +} + +/* + * A is a shared tmpfs and B its slave with Q, a bind mount of a file, on + * B/file. S is a bind mount of a file with N, a bind mount of a newer mount + * namespace's file, on top of it. S is moved onto A/file. Its copy S' lands + * on B/file below Q, without N. B/file keeps reading Q and once Q is + * unmounted it reads S'. + */ +TEST_F(overmount_ns_file, existing_mount_stays_on_top) +{ + char a[PATH_LEN], b[PATH_LEN], s[PATH_LEN], p[PATH_LEN], buf[16]; + int fd, pfd[2]; + char c; + + snprintf(a, sizeof(a), "%s/A", self->dir); + snprintf(b, sizeof(b), "%s/B", self->dir); + snprintf(s, sizeof(s), "%s/s", self->dir); + ASSERT_EQ(mkdir(a, 0755), 0); + ASSERT_EQ(mkdir(b, 0755), 0); + ASSERT_EQ(mkdir(s, 0755), 0); + + /* A shared, B its slave, Q on B/file */ + ASSERT_EQ(mount("tmpfs", a, "tmpfs", 0, NULL), 0); + ASSERT_EQ(mount(NULL, a, NULL, MS_SHARED, NULL), 0); + snprintf(p, sizeof(p), "%s/A/file", self->dir); + ASSERT_EQ(create_file(p, "A"), 0); + ASSERT_EQ(mount(a, b, NULL, MS_BIND, NULL), 0); + ASSERT_EQ(mount(NULL, b, NULL, MS_SLAVE, NULL), 0); + snprintf(p, sizeof(p), "%s/Q", self->dir); + ASSERT_EQ(create_file(p, "Q"), 0); + snprintf(b, sizeof(b), "%s/B/file", self->dir); + ASSERT_EQ(mount(p, b, NULL, MS_BIND, NULL), 0); + + /* S on s/f, pinned by a file descriptor before N goes on top */ + ASSERT_EQ(mount("tmpfs", s, "tmpfs", 0, NULL), 0); + snprintf(p, sizeof(p), "%s/s/f", self->dir); + ASSERT_EQ(create_file(p, "f"), 0); + snprintf(s, sizeof(s), "%s/s/S", self->dir); + ASSERT_EQ(create_file(s, "S"), 0); + ASSERT_EQ(mount(s, p, NULL, MS_BIND, NULL), 0); + fd = open(p, O_PATH | O_CLOEXEC); + ASSERT_GE(fd, 0); + + /* a newer mount namespace whose file can be bound */ + ASSERT_EQ(pipe(pfd), 0); + self->child = fork(); + ASSERT_GE(self->child, 0); + if (self->child == 0) { + close(pfd[0]); + if (unshare(CLONE_NEWNS) || write(pfd[1], "r", 1) != 1) + _exit(1); + pause(); + _exit(0); + } + close(pfd[1]); + ASSERT_EQ(read(pfd[0], &c, 1), 1); + close(pfd[0]); + snprintf(s, sizeof(s), "/proc/%d/ns/mnt", self->child); + ASSERT_EQ(mount(s, p, NULL, MS_BIND, NULL), 0); + + ASSERT_STREQ(read_file(b, buf, sizeof(buf)), "Q"); + + snprintf(a, sizeof(a), "%s/A/file", self->dir); + ASSERT_EQ(sys_move_mount(fd, "", AT_FDCWD, a, MOVE_MOUNT_F_EMPTY_PATH), 0); + close(fd); + + /* Q is still on top of the copy in B and can be unmounted */ + EXPECT_STREQ(read_file(b, buf, sizeof(buf)), "Q"); + EXPECT_EQ(umount2(b, 0), 0); + EXPECT_STREQ(read_file(b, buf, sizeof(buf)), "S"); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/filesystems/mount_cycle/overmount_reparent_test.c b/tools/testing/selftests/filesystems/mount_cycle/overmount_reparent_test.c new file mode 100644 index 000000000000..46a56b722f3e --- /dev/null +++ b/tools/testing/selftests/filesystems/mount_cycle/overmount_reparent_test.c @@ -0,0 +1,181 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * propagate_umount() slides a surviving overmount off a mount that is being + * unmounted and unmounts that mount right after. If a file descriptor keeps + * the unmounted mount alive its ->overmount is left pointing at the moved + * mount, which is freed on its own schedule. move_mount(MOVE_MOUNT_BENEATH) + * with such a file descriptor as the target walks ->overmount in + * topmost_overmount() before it checks that the target is still mounted and + * must not step into the freed mount. + */ +#define _GNU_SOURCE +#include <errno.h> +#include <fcntl.h> +#include <sched.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <sys/mount.h> +#include <sys/stat.h> +#include <sys/syscall.h> + +#include "../wrappers.h" +#include "../../kselftest_harness.h" + +#ifndef MOVE_MOUNT_BENEATH +#define MOVE_MOUNT_BENEATH 0x00000200 +#endif + +#ifndef FSCONFIG_CMD_CREATE +#define FSCONFIG_CMD_CREATE 6 +#endif + +#define DIR_LEN 64 +#define PATH_LEN 128 + +static int write_file(const char *path, const char *s) +{ + ssize_t n = -1; + int fd; + + fd = open(path, O_WRONLY | O_CLOEXEC); + if (fd >= 0) { + n = write(fd, s, strlen(s)); + close(fd); + } + return n == (ssize_t)strlen(s) ? 0 : -1; +} + +/* Become root in a new user namespace with a private mount namespace. */ +static int enter_userns(void) +{ + uid_t uid = getuid(); + gid_t gid = getgid(); + char map[32]; + + if (unshare(CLONE_NEWUSER | CLONE_NEWNS)) + return -1; + if (write_file("/proc/self/setgroups", "deny") && errno != ENOENT) + return -1; + snprintf(map, sizeof(map), "0 %d 1", uid); + if (write_file("/proc/self/uid_map", map)) + return -1; + snprintf(map, sizeof(map), "0 %d 1", gid); + if (write_file("/proc/self/gid_map", map)) + return -1; + if (setgid(0) || setuid(0)) + return -1; + return mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL); +} + +/* A detached tmpfs mount to serve as the source of a move. */ +static int detached_tmpfs(void) +{ + int sfd, mfd; + + sfd = sys_fsopen("tmpfs", 0); + if (sfd < 0) + return -1; + if (sys_fsconfig(sfd, FSCONFIG_CMD_CREATE, NULL, NULL, 0)) { + close(sfd); + return -1; + } + mfd = sys_fsmount(sfd, 0, 0); + close(sfd); + return mfd; +} + +FIXTURE(overmount_reparent) { + char dir[DIR_LEN]; +}; + +FIXTURE_SETUP(overmount_reparent) +{ + snprintf(self->dir, sizeof(self->dir), "/tmp/overmount_reparent.XXXXXX"); + ASSERT_NE(mkdtemp(self->dir), NULL); + if (enter_userns()) { + rmdir(self->dir); + SKIP(return, "test requires user namespaces"); + } +} + +FIXTURE_TEARDOWN(overmount_reparent) +{ + char p[PATH_LEN]; + + snprintf(p, sizeof(p), "%s/b", self->dir); + umount2(p, MNT_DETACH); + snprintf(p, sizeof(p), "%s/a", self->dir); + umount2(p, MNT_DETACH); +} + +/* + * A shared mount base_a is bind-mounted to base_b as its peer. A mount X on + * base_a/mp propagates a copy X_b onto base_b/mp. A file descriptor pins X_b, + * then X_b is made private and an overmount is stacked on its root. + * + * A lazy unmount of X propagates to X_b: X_b is committed to the unmount while + * its overmount survives, so propagate_umount() reparents the overmount onto + * base_b and unmounts X_b, which the file descriptor keeps alive. Unmounting + * the reparented overmount frees it. X_b->overmount now dangles unless + * mnt_change_mountpoint() reset it. + * + * move_mount(MOVE_MOUNT_BENEATH) through the file descriptor makes + * do_lock_mount() follow X_b->overmount in topmost_overmount() before it + * notices that X_b is no longer mounted. With the pointer reset the move fails + * cleanly with ENOENT; otherwise it reads the freed overmount. + */ +TEST_F(overmount_reparent, dead_overmount_holder_not_followed) +{ + char a[PATH_LEN], b[PATH_LEN], amp[PATH_LEN], bmp[PATH_LEN]; + int fd, mfd, ret; + + snprintf(a, sizeof(a), "%s/a", self->dir); + snprintf(b, sizeof(b), "%s/b", self->dir); + ASSERT_EQ(mkdir(a, 0755), 0); + ASSERT_EQ(mkdir(b, 0755), 0); + + ASSERT_EQ(mount("tmpfs", a, "tmpfs", 0, NULL), 0); + ASSERT_EQ(mount(NULL, a, NULL, MS_SHARED, NULL), 0); + ASSERT_EQ(mount(a, b, NULL, MS_BIND, NULL), 0); + + snprintf(amp, sizeof(amp), "%s/a/mp", self->dir); + snprintf(bmp, sizeof(bmp), "%s/b/mp", self->dir); + /* base_a and base_b share one superblock, so this dir is in both. */ + ASSERT_EQ(mkdir(amp, 0755), 0); + + /* X on base_a/mp propagates a copy X_b onto base_b/mp. */ + ASSERT_EQ(mount("tmpfs", amp, "tmpfs", 0, NULL), 0); + + /* Pin X_b before anything is stacked on top of it. */ + fd = open(bmp, O_PATH | O_CLOEXEC); + ASSERT_GE(fd, 0); + + /* Keep the overmount local to X_b. */ + ASSERT_EQ(mount(NULL, bmp, NULL, MS_PRIVATE, NULL), 0); + + /* The overmount on X_b's root: X_b->overmount points at it. */ + ASSERT_EQ(mount("tmpfs", bmp, "tmpfs", 0, NULL), 0); + + /* Reparents the overmount onto base_b and unmounts X_b. */ + ASSERT_EQ(umount2(amp, MNT_DETACH), 0); + + /* Free the reparented overmount. */ + ASSERT_EQ(umount2(bmp, MNT_DETACH), 0); + usleep(100000); + + mfd = detached_tmpfs(); + ASSERT_GE(mfd, 0); + + ret = sys_move_mount(mfd, "", fd, "", + MOVE_MOUNT_F_EMPTY_PATH | MOVE_MOUNT_T_EMPTY_PATH | + MOVE_MOUNT_BENEATH); + EXPECT_EQ(ret, -1); + EXPECT_EQ(errno, ENOENT); + + close(mfd); + close(fd); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/filesystems/mount_cycle/settings b/tools/testing/selftests/filesystems/mount_cycle/settings new file mode 100644 index 000000000000..694d70710ff0 --- /dev/null +++ b/tools/testing/selftests/filesystems/mount_cycle/settings @@ -0,0 +1 @@ +timeout=300 diff --git a/tools/testing/selftests/filesystems/mount_cycle/unmounted_tree_test.c b/tools/testing/selftests/filesystems/mount_cycle/unmounted_tree_test.c new file mode 100644 index 000000000000..233f8ea92efc --- /dev/null +++ b/tools/testing/selftests/filesystems/mount_cycle/unmounted_tree_test.c @@ -0,0 +1,484 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * An unmounted mount tree that a file descriptor keeps alive is put and + * vacated behind the root's back, so nothing may walk it under + * namespace_sem: it can't be copied recursively and its propagation can't + * be changed. The mount at its root alone can still be copied and that copy + * is an ordinary mount. + */ +#define _GNU_SOURCE +#include <errno.h> +#include <fcntl.h> +#include <sched.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/mount.h> +#include <sys/stat.h> +#include <sys/syscall.h> +#include <sys/wait.h> +#include <unistd.h> +#include <linux/mount.h> + +#include "../wrappers.h" +#include "../../kselftest_harness.h" + +#ifndef __NR_mount_setattr +#define __NR_mount_setattr 442 +#endif + +#ifndef __NR_pidfd_open +#define __NR_pidfd_open 434 +#endif + +#define PATH_LEN 64 + +static inline int sys_mount_setattr(int dfd, const char *path, unsigned int flags, + struct mount_attr *attr, size_t size) +{ + return syscall(__NR_mount_setattr, dfd, path, flags, attr, size); +} + +static inline int sys_pidfd_open(pid_t pid, unsigned int flags) +{ + return syscall(__NR_pidfd_open, pid, flags); +} + +static int write_file(const char *path, const char *s) +{ + ssize_t n = -1; + int fd; + + fd = open(path, O_WRONLY | O_CLOEXEC); + if (fd >= 0) { + n = write(fd, s, strlen(s)); + close(fd); + } + return n == (ssize_t)strlen(s) ? 0 : -1; +} + +static int touch(const char *path) +{ + int fd; + + fd = open(path, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0644); + if (fd < 0) + return -1; + close(fd); + return 0; +} + +/* Become root in a new user namespace with a private mount namespace. */ +static int enter_userns(void) +{ + uid_t uid = getuid(); + gid_t gid = getgid(); + char map[32]; + + if (unshare(CLONE_NEWUSER | CLONE_NEWNS)) + return -1; + if (write_file("/proc/self/setgroups", "deny") && errno != ENOENT) + return -1; + snprintf(map, sizeof(map), "0 %d 1", uid); + if (write_file("/proc/self/uid_map", map)) + return -1; + snprintf(map, sizeof(map), "0 %d 1", gid); + if (write_file("/proc/self/gid_map", map)) + return -1; + if (setgid(0) || setuid(0)) + return -1; + return mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL); +} + +/* A private mount namespace of our own, all mounts private. */ +static int own_mntns(void) +{ + if (unshare(CLONE_NEWNS)) + return -1; + return mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL); +} + +FIXTURE(unmounted_tree) { + char dir[PATH_LEN]; +}; + +FIXTURE_SETUP(unmounted_tree) +{ + snprintf(self->dir, sizeof(self->dir), "/tmp/unmounted_tree.XXXXXX"); + ASSERT_NE(mkdtemp(self->dir), NULL); + if (enter_userns()) { + rmdir(self->dir); + SKIP(return, "test requires user namespaces"); + } + ASSERT_EQ(mount("tmpfs", self->dir, "tmpfs", 0, NULL), 0); +} + +FIXTURE_TEARDOWN(unmounted_tree) +{ + umount2(self->dir, MNT_DETACH); + rmdir(self->dir); +} + +/* A recursive copy of the mount @fd refers to must fail with EINVAL. */ +static void assert_not_walked(struct __test_metadata *_metadata, int fd, + const char *target) +{ + char link[PATH_LEN]; + int tfd; + + snprintf(link, sizeof(link), "/proc/self/fd/%d", fd); + EXPECT_EQ(mount(link, target, NULL, MS_BIND | MS_REC, NULL), -1); + EXPECT_EQ(errno, EINVAL); + + tfd = sys_open_tree(fd, "", AT_EMPTY_PATH | AT_RECURSIVE | OPEN_TREE_CLONE | + OPEN_TREE_CLOEXEC); + EXPECT_LT(tfd, 0); + EXPECT_EQ(errno, EINVAL); + if (tfd >= 0) + close(tfd); +} + +/* The mount @fd refers to can be copied on its own and mounted on @target. */ +static void assert_root_copied(struct __test_metadata *_metadata, int fd, + const char *target) +{ + int tfd; + + tfd = sys_open_tree(fd, "", AT_EMPTY_PATH | OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC); + ASSERT_GE(tfd, 0); + ASSERT_EQ(sys_move_mount(tfd, "", AT_FDCWD, target, MOVE_MOUNT_F_EMPTY_PATH), 0); + close(tfd); + ASSERT_EQ(umount2(target, MNT_DETACH), 0); +} + +/* + * A lazily unmounted bind mount of an nsfs file, kept alive by an fd, can't + * be copied recursively anymore. Copying the mount itself still works and + * so does copying a mounted bind mount of the same file. + */ +TEST_F(unmounted_tree, detached_nsfs_bind_not_walked) +{ + char x[PATH_LEN], y[PATH_LEN], z[PATH_LEN], link[PATH_LEN]; + int fd, tfd; + + snprintf(x, sizeof(x), "%s/x", self->dir); + snprintf(y, sizeof(y), "%s/y", self->dir); + snprintf(z, sizeof(z), "%s/z", self->dir); + ASSERT_EQ(touch(x), 0); + ASSERT_EQ(touch(y), 0); + ASSERT_EQ(touch(z), 0); + + ASSERT_EQ(mount("/proc/self/ns/net", x, NULL, MS_BIND, NULL), 0); + fd = open(x, O_PATH | O_CLOEXEC); + ASSERT_GE(fd, 0); + ASSERT_EQ(umount2(x, MNT_DETACH), 0); + + assert_not_walked(_metadata, fd, y); + + snprintf(link, sizeof(link), "/proc/self/fd/%d", fd); + ASSERT_EQ(mount(link, y, NULL, MS_BIND, NULL), 0); + ASSERT_EQ(umount2(y, MNT_DETACH), 0); + assert_root_copied(_metadata, fd, y); + /* opening it is fine too */ + tfd = sys_open_tree(fd, "", AT_EMPTY_PATH | OPEN_TREE_CLOEXEC); + ASSERT_GE(tfd, 0); + close(tfd); + close(fd); + + ASSERT_EQ(mount("/proc/self/ns/net", y, NULL, MS_BIND, NULL), 0); + ASSERT_EQ(mount(y, z, NULL, MS_BIND | MS_REC, NULL), 0); + tfd = sys_open_tree(AT_FDCWD, y, AT_RECURSIVE | OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC); + ASSERT_GE(tfd, 0); + close(tfd); +} + +/* Bind-mount the pidfd @pidfd on @target the way pidfd_bind_mount does. */ +static int bind_pidfd(int pidfd, const char *target) +{ + int tfd, ret; + + tfd = sys_open_tree(pidfd, "", AT_EMPTY_PATH | OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC); + if (tfd < 0) + return -1; + ret = sys_move_mount(tfd, "", AT_FDCWD, target, MOVE_MOUNT_F_EMPTY_PATH); + close(tfd); + return ret; +} + +/* The same for a bind mount of a pidfd. */ +TEST_F(unmounted_tree, detached_pidfs_bind_not_walked) +{ + char x[PATH_LEN], y[PATH_LEN]; + int pidfd, fd, tfd; + + snprintf(x, sizeof(x), "%s/x", self->dir); + snprintf(y, sizeof(y), "%s/y", self->dir); + ASSERT_EQ(touch(x), 0); + ASSERT_EQ(touch(y), 0); + + pidfd = sys_pidfd_open(getpid(), 0); + ASSERT_GE(pidfd, 0); + ASSERT_EQ(bind_pidfd(pidfd, x), 0); + fd = open(x, O_PATH | O_CLOEXEC); + ASSERT_GE(fd, 0); + ASSERT_EQ(umount2(x, MNT_DETACH), 0); + + assert_not_walked(_metadata, fd, y); + assert_root_copied(_metadata, fd, y); + close(fd); + + ASSERT_EQ(bind_pidfd(pidfd, y), 0); + tfd = sys_open_tree(AT_FDCWD, y, AT_RECURSIVE | OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC); + ASSERT_GE(tfd, 0); + close(tfd); + close(pidfd); +} + +/* What a child in its own mount namespace reports back. */ +enum { + CHILD_OK, + CHILD_NS, /* could not set up the mount namespace */ + CHILD_MOUNT, /* could not set up the mounts */ + CHILD_PIPE, /* the parent went away */ + CHILD_WALKED, /* the detached tree was copied recursively */ + CHILD_ROOT_REFUSED, /* the mount at its root wasn't copied */ + CHILD_STILL_MOUNTED, /* the copy survived the unlink of its mountpoint */ + CHILD_ERRNO, /* refused, but not with EINVAL */ +}; + +static const char *child_reason(int code) +{ + static const char *const reasons[] = { + [CHILD_OK] = "ok", + [CHILD_NS] = "could not set up the mount namespace", + [CHILD_MOUNT] = "could not set up the mounts", + [CHILD_PIPE] = "the parent went away", + [CHILD_WALKED] = "the detached tree was copied recursively", + [CHILD_ROOT_REFUSED] = "the mount at its root wasn't copied", + [CHILD_STILL_MOUNTED] = "the copy survived the unlink of its mountpoint", + [CHILD_ERRNO] = "refused, but not with EINVAL", + }; + + if (code < 0 || code >= (int)(sizeof(reasons) / sizeof(reasons[0]))) + return "child died"; + return reasons[code]; +} + +struct child_args { + const char *x; /* the nsfs bind mount goes here */ + const char *s; /* a file to bind on top of it */ + const char *y; /* a target to copy to */ +}; + +/* + * Run @fn in a child in a mount namespace of its own. Once the child + * reports that its mounts are in place, unlink @victim here, where it is a + * plain file, and let the child carry on. Returns what the child reported. + */ +static int run_child(struct __test_metadata *_metadata, + int (*fn)(const struct child_args *, int, int), + const struct child_args *a, const char *victim) +{ + int to_parent[2], to_child[2], status; + pid_t pid; + char c; + + if (pipe(to_parent) || pipe(to_child)) + return -1; + pid = fork(); + if (pid < 0) + return -1; + if (pid == 0) { + close(to_parent[0]); + close(to_child[1]); + _exit(fn(a, to_parent[1], to_child[0])); + } + close(to_parent[1]); + close(to_child[0]); + if (read(to_parent[0], &c, 1) == 1) { + /* not a mountpoint in this namespace, so the file can go */ + EXPECT_EQ(unlink(victim), 0); + EXPECT_EQ(write(to_child[1], "", 1), 1); + } + close(to_parent[0]); + close(to_child[1]); + if (waitpid(pid, &status, 0) != pid || !WIFEXITED(status)) + return -1; + return WEXITSTATUS(status); +} + +/* + * An nsfs bind mount on @x, an fd on it and a bind mount of @s on top. Once + * the parent has unlinked @x underneath, the first mount is detached and the + * second one, which stayed attached to it, has been vacated. The tree may + * not be walked, the mount at its root may still be copied. + */ +static int vacant_child(const struct child_args *a, int to_parent, int from_parent) +{ + char link[PATH_LEN]; + int fd, tfd; + char c; + + if (own_mntns()) + return CHILD_NS; + if (mount("/proc/self/ns/net", a->x, NULL, MS_BIND, NULL)) + return CHILD_MOUNT; + fd = open(a->x, O_PATH | O_CLOEXEC); + if (fd < 0 || mount(a->s, a->x, NULL, MS_BIND, NULL)) + return CHILD_MOUNT; + + if (write(to_parent, "", 1) != 1 || read(from_parent, &c, 1) != 1) + return CHILD_PIPE; + + tfd = sys_open_tree(fd, "", AT_EMPTY_PATH | AT_RECURSIVE | OPEN_TREE_CLONE | + OPEN_TREE_CLOEXEC); + if (tfd >= 0) { + close(tfd); + return CHILD_WALKED; + } + if (errno != EINVAL) + return CHILD_ERRNO; + snprintf(link, sizeof(link), "/proc/self/fd/%d", fd); + if (!mount(link, a->y, NULL, MS_BIND | MS_REC, NULL)) + return CHILD_WALKED; + if (errno != EINVAL) + return CHILD_ERRNO; + + tfd = sys_open_tree(fd, "", AT_EMPTY_PATH | OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC); + if (tfd < 0) + return CHILD_ROOT_REFUSED; + close(tfd); + /* the last reference on the detached mount collects the vacant one */ + close(fd); + return CHILD_OK; +} + +/* + * unlink() of the child's mountpoint from here, where it is a plain file, + * detaches the child's nsfs bind mount with the mount on top left attached + * to it. The mount on top loses its last reference right there and is + * vacated. The child must not be able to walk that tree. + */ +TEST_F(unmounted_tree, vacant_child_not_copied) +{ + char x[PATH_LEN], s[PATH_LEN], y[PATH_LEN]; + struct child_args a = { .x = x, .s = s, .y = y }; + int ret; + + snprintf(x, sizeof(x), "%s/x", self->dir); + snprintf(s, sizeof(s), "%s/s", self->dir); + snprintf(y, sizeof(y), "%s/y", self->dir); + ASSERT_EQ(touch(x), 0); + ASSERT_EQ(touch(s), 0); + ASSERT_EQ(touch(y), 0); + + ret = run_child(_metadata, vacant_child, &a, x); + ASSERT_EQ(ret, CHILD_OK) + TH_LOG("child: %s", child_reason(ret)); +} + +/* + * An nsfs bind mount on @x, lazily unmounted but kept alive by an fd, and a + * copy of it on @y. That copy is a mount like any other: once the parent has + * unlinked @y underneath, it is unmounted and its attributes can't be changed + * anymore. A copy that took the flag along is only unhooked, stays in the + * namespace and still takes the change. + */ +static int ordinary_copy(const struct child_args *a, int to_parent, int from_parent) +{ + struct mount_attr attr = { + .attr_set = MOUNT_ATTR_NOSUID, + }; + char link[PATH_LEN]; + int fd, fdy; + char c; + + if (own_mntns()) + return CHILD_NS; + if (mount("/proc/self/ns/net", a->x, NULL, MS_BIND, NULL)) + return CHILD_MOUNT; + fd = open(a->x, O_PATH | O_CLOEXEC); + if (fd < 0 || umount2(a->x, MNT_DETACH)) + return CHILD_MOUNT; + snprintf(link, sizeof(link), "/proc/self/fd/%d", fd); + if (mount(link, a->y, NULL, MS_BIND, NULL)) + return CHILD_MOUNT; + fdy = open(a->y, O_PATH | O_CLOEXEC); + if (fdy < 0) + return CHILD_MOUNT; + + if (write(to_parent, "", 1) != 1 || read(from_parent, &c, 1) != 1) + return CHILD_PIPE; + + if (!sys_mount_setattr(fdy, "", AT_EMPTY_PATH, &attr, sizeof(attr))) + return CHILD_STILL_MOUNTED; + if (errno != EINVAL) + return CHILD_ERRNO; + close(fdy); + close(fd); + return CHILD_OK; +} + +/* + * A copy of a detached nsfs bind mount must not take after its source and + * read as unmounted. unlink() of its mountpoint from here unmounts it like + * any other mount, so the child can't change its attributes anymore. + */ +TEST_F(unmounted_tree, copy_of_detached_bind_is_ordinary) +{ + char x[PATH_LEN], y[PATH_LEN]; + struct child_args a = { .x = x, .y = y }; + int ret; + + snprintf(x, sizeof(x), "%s/x", self->dir); + snprintf(y, sizeof(y), "%s/y", self->dir); + ASSERT_EQ(touch(x), 0); + ASSERT_EQ(touch(y), 0); + + ret = run_child(_metadata, ordinary_copy, &a, y); + ASSERT_EQ(ret, CHILD_OK) + TH_LOG("child: %s", child_reason(ret)); +} + +/* + * mount_setattr() may change the propagation of a detached tree while that + * tree is still the root of an anonymous mount namespace, but not once the + * tree has been dissolved and only a second fd keeps its root alive. + */ +TEST_F(unmounted_tree, detached_tree_setattr_refused) +{ + struct mount_attr attr = { + .propagation = MS_SHARED, + }; + char a[PATH_LEN], b[PATH_LEN]; + int tfd, fd; + + snprintf(a, sizeof(a), "%s/a", self->dir); + snprintf(b, sizeof(b), "%s/a/b", self->dir); + ASSERT_EQ(mkdir(a, 0755), 0); + ASSERT_EQ(mount("tmpfs", a, "tmpfs", 0, NULL), 0); + ASSERT_EQ(mkdir(b, 0755), 0); + ASSERT_EQ(mount("tmpfs", b, "tmpfs", 0, NULL), 0); + + tfd = sys_open_tree(AT_FDCWD, self->dir, AT_RECURSIVE | OPEN_TREE_CLONE | + OPEN_TREE_CLOEXEC); + ASSERT_GE(tfd, 0); + ASSERT_EQ(sys_mount_setattr(tfd, "", AT_EMPTY_PATH | AT_RECURSIVE, &attr, sizeof(attr)), 0); + attr.propagation = MS_PRIVATE; + ASSERT_EQ(sys_mount_setattr(tfd, "", AT_EMPTY_PATH | AT_RECURSIVE, &attr, sizeof(attr)), 0); + + /* a second reference on the root, then dissolve the tree */ + fd = sys_open_tree(tfd, "", AT_EMPTY_PATH | OPEN_TREE_CLOEXEC); + ASSERT_GE(fd, 0); + close(tfd); + + attr.propagation = MS_SHARED; + ASSERT_EQ(sys_mount_setattr(fd, "", AT_EMPTY_PATH | AT_RECURSIVE, &attr, sizeof(attr)), -1); + ASSERT_EQ(errno, EINVAL); + attr.propagation = MS_PRIVATE; + ASSERT_EQ(sys_mount_setattr(fd, "", AT_EMPTY_PATH, &attr, sizeof(attr)), -1); + ASSERT_EQ(errno, EINVAL); + close(fd); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/filesystems/open_tree_ns/.gitignore b/tools/testing/selftests/filesystems/open_tree_ns/.gitignore index fb12b93fbcaa..76f95c0ae5ef 100644 --- a/tools/testing/selftests/filesystems/open_tree_ns/.gitignore +++ b/tools/testing/selftests/filesystems/open_tree_ns/.gitignore @@ -1 +1,2 @@ open_tree_ns_test +open_tree_ns_covered_test diff --git a/tools/testing/selftests/filesystems/open_tree_ns/Makefile b/tools/testing/selftests/filesystems/open_tree_ns/Makefile index 4976ed1d7d4a..fb2aa77b6edb 100644 --- a/tools/testing/selftests/filesystems/open_tree_ns/Makefile +++ b/tools/testing/selftests/filesystems/open_tree_ns/Makefile @@ -1,5 +1,5 @@ # SPDX-License-Identifier: GPL-2.0 -TEST_GEN_PROGS := open_tree_ns_test +TEST_GEN_PROGS := open_tree_ns_test open_tree_ns_covered_test CFLAGS += -Wall -O0 -g $(KHDR_INCLUDES) $(TOOLS_INCLUDES) LDLIBS := -lcap diff --git a/tools/testing/selftests/filesystems/open_tree_ns/open_tree_ns_covered_test.c b/tools/testing/selftests/filesystems/open_tree_ns/open_tree_ns_covered_test.c new file mode 100644 index 000000000000..27f5cddfb9ef --- /dev/null +++ b/tools/testing/selftests/filesystems/open_tree_ns/open_tree_ns_covered_test.c @@ -0,0 +1,195 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * open_tree(OPEN_TREE_NAMESPACE) by a caller that isn't privileged over the + * mount namespace it copies from must not reveal what the mounts below the + * copied mount cover. Without AT_RECURSIVE the copy is refused when there's + * anything mounted below the requested directory. With AT_RECURSIVE + * unbindable mounts are copied as well. + */ +#define _GNU_SOURCE +#include <errno.h> +#include <fcntl.h> +#include <sched.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <sys/mount.h> +#include <sys/stat.h> +#include <sys/wait.h> + +#include "../wrappers.h" +#include "../../kselftest_harness.h" + +#ifndef OPEN_TREE_NAMESPACE +#define OPEN_TREE_NAMESPACE (1 << 1) +#endif + +#define DIR_LEN 64 +#define PATH_LEN 128 + +/* Child exit codes. */ +enum { + CHILD_OK, + CHILD_USERNS, /* could not create the child user namespace */ + CHILD_NONREC_ALLOWED, /* the non-recursive copy was not refused */ + CHILD_NONREC_ERRNO, /* it was refused with the wrong error */ + CHILD_REC_REFUSED, /* the recursive copy failed */ + CHILD_SETNS, /* could not enter the new mount namespace */ + CHILD_NO_COVER, /* the covering mount is missing in the copy */ + CHILD_REVEALED, /* the covered file is visible in the copy */ +}; + +static int write_file(const char *path, const char *s) +{ + ssize_t n = -1; + int fd; + + fd = open(path, O_WRONLY | O_CLOEXEC); + if (fd >= 0) { + n = write(fd, s, strlen(s)); + close(fd); + } + return n == (ssize_t)strlen(s) ? 0 : -1; +} + +static int create_file(const char *path, const char *s) +{ + ssize_t n = -1; + int fd; + + fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644); + if (fd >= 0) { + n = write(fd, s, strlen(s)); + close(fd); + } + return n == (ssize_t)strlen(s) ? 0 : -1; +} + +/* Become root in a new user namespace, the uid @uid is mapped to 0. */ +static int enter_userns(uid_t uid, gid_t gid) +{ + char map[32]; + + if (unshare(CLONE_NEWUSER)) + return -1; + if (write_file("/proc/self/setgroups", "deny") && errno != ENOENT) + return -1; + snprintf(map, sizeof(map), "0 %d 1", uid); + if (write_file("/proc/self/uid_map", map)) + return -1; + snprintf(map, sizeof(map), "0 %d 1", gid); + if (write_file("/proc/self/gid_map", map)) + return -1; + return setgid(0) || setuid(0) ? -1 : 0; +} + +FIXTURE(open_tree_ns_covered) { + char dir[DIR_LEN]; + char cover[PATH_LEN]; +}; + +FIXTURE_VARIANT(open_tree_ns_covered) { + int propagation; +}; + +FIXTURE_VARIANT_ADD(open_tree_ns_covered, private_cover) { + .propagation = MS_PRIVATE, +}; + +FIXTURE_VARIANT_ADD(open_tree_ns_covered, unbindable_cover) { + .propagation = MS_UNBINDABLE, +}; + +/* + * Root in a user namespace owns a private mount namespace with a tmpfs + * on @dir and a second tmpfs covering @dir/covered/under.txt. + */ +FIXTURE_SETUP(open_tree_ns_covered) +{ + char p[PATH_LEN]; + int fd; + + snprintf(self->dir, sizeof(self->dir), "/tmp/open_tree_ns_covered.XXXXXX"); + ASSERT_NE(mkdtemp(self->dir), NULL); + if (enter_userns(getuid(), getgid()) || unshare(CLONE_NEWNS)) { + rmdir(self->dir); + SKIP(return, "test requires user namespaces"); + } + ASSERT_EQ(mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL), 0); + ASSERT_EQ(mount("tmpfs", self->dir, "tmpfs", 0, NULL), 0); + + /* the flag has to exist before its rules can be checked */ + fd = sys_open_tree(AT_FDCWD, self->dir, + OPEN_TREE_NAMESPACE | OPEN_TREE_CLOEXEC); + if (fd < 0 && (errno == EINVAL || errno == ENOSYS)) { + umount2(self->dir, MNT_DETACH); + rmdir(self->dir); + SKIP(return, "OPEN_TREE_NAMESPACE not supported"); + } + ASSERT_GE(fd, 0); + close(fd); + + snprintf(self->cover, sizeof(self->cover), "%s/covered", self->dir); + ASSERT_EQ(mkdir(self->cover, 0755), 0); + snprintf(p, sizeof(p), "%s/covered/under.txt", self->dir); + ASSERT_EQ(create_file(p, "hidden"), 0); + ASSERT_EQ(mount("tmpfs", self->cover, "tmpfs", 0, NULL), 0); + ASSERT_EQ(mount(NULL, self->cover, NULL, variant->propagation, NULL), 0); +} + +FIXTURE_TEARDOWN(open_tree_ns_covered) +{ + umount2(self->dir, MNT_DETACH); + rmdir(self->dir); +} + +/* A caller in a new user namespace that doesn't own the mount namespace. */ +static int foreign_child(const char *dir) +{ + struct stat st; + int fd; + + if (enter_userns(0, 0)) + return CHILD_USERNS; + + fd = sys_open_tree(AT_FDCWD, dir, OPEN_TREE_NAMESPACE | OPEN_TREE_CLOEXEC); + if (fd >= 0) + return CHILD_NONREC_ALLOWED; + if (errno != EINVAL) + return CHILD_NONREC_ERRNO; + + fd = sys_open_tree(AT_FDCWD, dir, + OPEN_TREE_NAMESPACE | OPEN_TREE_CLOEXEC | AT_RECURSIVE); + if (fd < 0) + return CHILD_REC_REFUSED; + if (setns(fd, CLONE_NEWNS)) + return CHILD_SETNS; + if (stat("/covered", &st)) + return CHILD_NO_COVER; + if (!access("/covered/under.txt", F_OK) || errno != ENOENT) + return CHILD_REVEALED; + return CHILD_OK; +} + +TEST_F(open_tree_ns_covered, foreign_user_namespace) +{ + int status, fd; + pid_t pid; + + pid = fork(); + ASSERT_GE(pid, 0); + if (pid == 0) + _exit(foreign_child(self->dir)); + ASSERT_EQ(waitpid(pid, &status, 0), pid); + ASSERT_TRUE(WIFEXITED(status)); + ASSERT_EQ(WEXITSTATUS(status), CHILD_OK); + + /* the owner of the mount namespace keeps bind mount semantics */ + fd = sys_open_tree(AT_FDCWD, self->dir, + OPEN_TREE_NAMESPACE | OPEN_TREE_CLOEXEC); + ASSERT_GE(fd, 0); + close(fd); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/filesystems/overlayfs/.gitignore b/tools/testing/selftests/filesystems/overlayfs/.gitignore index 077f7a128168..b343cc430051 100644 --- a/tools/testing/selftests/filesystems/overlayfs/.gitignore +++ b/tools/testing/selftests/filesystems/overlayfs/.gitignore @@ -2,3 +2,4 @@ dev_in_maps set_layers_via_fds idmapped_mounts +automount_in_layer diff --git a/tools/testing/selftests/filesystems/overlayfs/Makefile b/tools/testing/selftests/filesystems/overlayfs/Makefile index b3185f684add..382a59bda5e7 100644 --- a/tools/testing/selftests/filesystems/overlayfs/Makefile +++ b/tools/testing/selftests/filesystems/overlayfs/Makefile @@ -9,6 +9,7 @@ LOCAL_HDRS += ../wrappers.h log.h TEST_GEN_PROGS := dev_in_maps TEST_GEN_PROGS += set_layers_via_fds TEST_GEN_PROGS += idmapped_mounts +TEST_GEN_PROGS += automount_in_layer include ../../lib.mk diff --git a/tools/testing/selftests/filesystems/overlayfs/automount_in_layer.c b/tools/testing/selftests/filesystems/overlayfs/automount_in_layer.c new file mode 100644 index 000000000000..0476c4582bdf --- /dev/null +++ b/tools/testing/selftests/filesystems/overlayfs/automount_in_layer.c @@ -0,0 +1,174 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * A layer of an overlay is a private clone of the mount it was given and + * belongs to no mount namespace. fanotify hands out descriptors on it. An + * automount triggered through one has no namespace to go into: the open has + * to fail, not oops with namespace_sem held. + */ +#define _GNU_SOURCE +#include <dirent.h> +#include <errno.h> +#include <fcntl.h> +#include <poll.h> +#include <sched.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <linux/magic.h> +#include <sys/fanotify.h> +#include <sys/mount.h> +#include <sys/stat.h> +#include <sys/vfs.h> + +#include "../../kselftest_harness.h" + +#define DIR_LEN 64 +#define PATH_LEN 192 + +static bool have_fs(const char *name) +{ + char line[128]; + bool found = false; + FILE *f; + + f = fopen("/proc/filesystems", "re"); + if (!f) + return false; + while (fgets(line, sizeof(line), f)) { + char *nl = strchr(line, '\n'); + char *tab = strchr(line, '\t'); + + if (nl) + *nl = 0; + if (tab && !strcmp(tab + 1, name)) + found = true; + } + fclose(f); + return found; +} + +static int mnt_id_of(int fd) +{ + char path[64], buf[4096], *p; + ssize_t n; + int info; + + snprintf(path, sizeof(path), "/proc/self/fdinfo/%d", fd); + info = open(path, O_RDONLY | O_CLOEXEC); + if (info < 0) + return -1; + n = read(info, buf, sizeof(buf) - 1); + close(info); + if (n <= 0) + return -1; + buf[n] = 0; + p = strstr(buf, "mnt_id:"); + return p ? atoi(p + strlen("mnt_id:")) : -1; +} + +static bool on_debugfs(int fd) +{ + struct statfs sf; + + return !fstatfs(fd, &sf) && sf.f_type == DEBUGFS_MAGIC; +} + +FIXTURE(layer) { + char base[DIR_LEN]; + int fan; + int evfd; /* on the layer clone of the lower debugfs */ +}; + +FIXTURE_SETUP(layer) +{ + char lower[PATH_LEN], other[PATH_LEN], ovl[PATH_LEN], opts[2 * PATH_LEN + 16]; + struct fanotify_event_metadata *ev; + struct pollfd pfd; + char buf[4096]; + int lfd, lower_id; + ssize_t n; + DIR *d; + + self->fan = -1; + self->evfd = -1; + if (geteuid()) + SKIP(return, "test requires root"); + if (!have_fs("debugfs") || !have_fs("tracefs")) + SKIP(return, "test requires debugfs with the tracefs automount"); + if (!have_fs("overlay")) + SKIP(return, "test requires overlayfs"); + + snprintf(self->base, sizeof(self->base), "/tmp/layer.XXXXXX"); + ASSERT_NE(mkdtemp(self->base), NULL); + ASSERT_EQ(unshare(CLONE_NEWNS), 0); + ASSERT_EQ(mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL), 0); + ASSERT_EQ(mount("tmpfs", self->base, "tmpfs", 0, "mode=0755"), 0); + snprintf(lower, sizeof(lower), "%s/lower", self->base); + snprintf(other, sizeof(other), "%s/other", self->base); + snprintf(ovl, sizeof(ovl), "%s/ovl", self->base); + ASSERT_EQ(mkdir(lower, 0755), 0); + ASSERT_EQ(mkdir(other, 0755), 0); + ASSERT_EQ(mkdir(ovl, 0755), 0); + ASSERT_EQ(mount("debugfs", lower, "debugfs", 0, NULL), 0); + snprintf(opts, sizeof(opts), "lowerdir=%s:%s", lower, other); + ASSERT_EQ(mount("overlay", ovl, "overlay", MS_RDONLY, opts), 0); + + self->fan = fanotify_init(FAN_CLASS_NOTIF | FAN_NONBLOCK | FAN_CLOEXEC, + O_RDONLY | O_CLOEXEC); + ASSERT_GE(self->fan, 0); + ASSERT_EQ(fanotify_mark(self->fan, FAN_MARK_ADD | FAN_MARK_FILESYSTEM, + FAN_OPEN | FAN_ONDIR, AT_FDCWD, lower), 0); + lfd = open(lower, O_RDONLY | O_DIRECTORY | O_CLOEXEC); + ASSERT_GE(lfd, 0); + lower_id = mnt_id_of(lfd); + close(lfd); + + /* the overlay opens its lower directory through the layer clone */ + d = opendir(ovl); + ASSERT_NE(d, NULL); + closedir(d); + pfd.fd = self->fan; + pfd.events = POLLIN; + ASSERT_EQ(poll(&pfd, 1, 5000), 1); + n = read(self->fan, buf, sizeof(buf)); + ASSERT_GT(n, 0); + for (ev = (void *)buf; FAN_EVENT_OK(ev, n); ev = FAN_EVENT_NEXT(ev, n)) { + if (ev->fd < 0) + continue; + if (self->evfd < 0 && on_debugfs(ev->fd) && + mnt_id_of(ev->fd) != lower_id) + self->evfd = ev->fd; + else + close(ev->fd); + } + ASSERT_GE(self->evfd, 0) + TH_LOG("no event on the layer clone"); +} + +FIXTURE_TEARDOWN(layer) +{ + if (self->evfd >= 0) + close(self->evfd); + if (self->fan >= 0) + close(self->fan); + umount2(self->base, MNT_DETACH); + rmdir(self->base); +} + +TEST_F(layer, automount_below_the_clone_is_refused) +{ + struct stat st; + int fd; + + /* the automount point is there */ + ASSERT_EQ(fstatat(self->evfd, "tracing", &st, AT_NO_AUTOMOUNT), 0); + /* the clone is in no namespace, so the automount has nowhere to go */ + fd = openat(self->evfd, "tracing", O_RDONLY | O_DIRECTORY | O_CLOEXEC); + EXPECT_LT(fd, 0); + EXPECT_EQ(errno, EINVAL); + if (fd >= 0) + close(fd); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/filesystems/readdir_hold.h b/tools/testing/selftests/filesystems/readdir_hold.h new file mode 100644 index 000000000000..57eee1bef470 --- /dev/null +++ b/tools/testing/selftests/filesystems/readdir_hold.h @@ -0,0 +1,224 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Hold a readdir of a directory in the page fault of its buffer and see + * whether a create and a lookup in that directory wait for it. For a + * directory that is permanently empty they must not. + */ +#ifndef __SELFTESTS_READDIR_HOLD_H +#define __SELFTESTS_READDIR_HOLD_H + +#include <errno.h> +#include <fcntl.h> +#include <poll.h> +#include <pthread.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <linux/userfaultfd.h> +#include <sys/ioctl.h> +#include <sys/mman.h> +#include <sys/stat.h> +#include <sys/syscall.h> + +#define HOLD_FAULT_MS 5000 /* for the reader to reach the fault */ +#define HOLD_QUEUE_MS 2000 /* for the create to queue up behind it */ +#define HOLD_LOOKUP_MS 5000 /* for the lookup to come back */ + +struct readdir_hold { + int uffd; + int taskdir; /* /proc/self/task, from before any namespace change */ + long page_size; + char *page; /* faults until released */ + int dfd; + pid_t creator_tid; + int created; + int done[2]; /* the finder writes a byte when it is back */ +}; + +/* + * The fault happens in the kernel, so the userfaultfd needs CAP_SYS_PTRACE + * in the initial user namespace or vm.unprivileged_userfaultfd. Call this + * before entering a user namespace. + */ +static inline int readdir_hold_init(struct readdir_hold *h) +{ + struct uffdio_api api = { .api = UFFD_API }; + + h->page = MAP_FAILED; + h->page_size = sysconf(_SC_PAGESIZE); + h->taskdir = open("/proc/self/task", O_RDONLY | O_DIRECTORY | O_CLOEXEC); + h->uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK); + if (h->uffd < 0 || h->taskdir < 0 || ioctl(h->uffd, UFFDIO_API, &api)) { + if (h->uffd >= 0) + close(h->uffd); + if (h->taskdir >= 0) + close(h->taskdir); + h->uffd = h->taskdir = -1; + return -1; + } + return 0; +} + +static inline void readdir_hold_destroy(struct readdir_hold *h) +{ + if (h->uffd >= 0) + close(h->uffd); + if (h->taskdir >= 0) + close(h->taskdir); + h->uffd = h->taskdir = -1; +} + +static inline void *readdir_hold_reader(void *arg) +{ + struct readdir_hold *h = arg; + + /* the first byte written to the buffer faults until released */ + syscall(__NR_getdents64, h->dfd, h->page, h->page_size); + return NULL; +} + +static inline void *readdir_hold_creator(void *arg) +{ + struct readdir_hold *h = arg; + + h->creator_tid = syscall(__NR_gettid); + /* takes the directory lock exclusive before it fails */ + mkdirat(h->dfd, "x", 0755); + __atomic_store_n(&h->created, 1, __ATOMIC_RELEASE); + return NULL; +} + +static inline void *readdir_hold_finder(void *arg) +{ + struct readdir_hold *h = arg; + int fd; + + /* a lookup that misses the dcache takes the lock shared */ + fd = openat(h->dfd, "no_such_name", O_RDONLY | O_CLOEXEC); + if (fd >= 0) + close(fd); + if (write(h->done[1], "x", 1) != 1) + perror("readdir_hold: finder"); + return NULL; +} + +/* the creator is back, or waits in the kernel for the lock */ +static inline bool readdir_hold_creator_settled(struct readdir_hold *h) +{ + char path[32], buf[256], *p; + ssize_t n; + int fd; + + if (__atomic_load_n(&h->created, __ATOMIC_ACQUIRE)) + return true; + if (!h->creator_tid) + return false; + snprintf(path, sizeof(path), "%d/stat", h->creator_tid); + fd = openat(h->taskdir, path, O_RDONLY | O_CLOEXEC); + if (fd < 0) + return false; + n = read(fd, buf, sizeof(buf) - 1); + close(fd); + if (n <= 0) + return false; + buf[n] = 0; + /* "pid (comm) state ..." */ + p = strrchr(buf, ')'); + return p && p[1] == ' ' && p[2] == 'D'; +} + +static inline bool readdir_hold_faulted(struct readdir_hold *h) +{ + struct pollfd pfd = { .fd = h->uffd, .events = POLLIN }; + struct uffd_msg msg; + + if (poll(&pfd, 1, HOLD_FAULT_MS) != 1) + return false; + if (read(h->uffd, &msg, sizeof(msg)) != sizeof(msg)) + return false; + return msg.event == UFFD_EVENT_PAGEFAULT; +} + +/* let the reader go on */ +static inline void readdir_hold_release(struct readdir_hold *h) +{ + struct uffdio_copy cp = { + .dst = (unsigned long)h->page, + .len = h->page_size, + }; + void *zero; + + zero = calloc(1, h->page_size); + if (!zero) + return; + cp.src = (unsigned long)zero; + if (ioctl(h->uffd, UFFDIO_COPY, &cp) && errno != EEXIST) + perror("readdir_hold: UFFDIO_COPY"); + free(zero); +} + +/* + * A readdir of @dfd that sticks in the fault of its buffer, then a create + * and a lookup in @dfd. Whether the lookup came back while the readdir was + * still stuck goes to @stalled. Returns -1 when that could not be found + * out. + */ +static inline int readdir_hold_check(struct readdir_hold *h, int dfd, + bool *stalled) +{ + pthread_t reader, creator, finder; + struct uffdio_register reg = {}; + struct pollfd pfd; + int ret = -1, i; + + h->dfd = dfd; + h->created = 0; + h->creator_tid = 0; + h->page = mmap(NULL, h->page_size, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (h->page == MAP_FAILED) + return -1; + reg.range.start = (unsigned long)h->page; + reg.range.len = h->page_size; + reg.mode = UFFDIO_REGISTER_MODE_MISSING; + if (ioctl(h->uffd, UFFDIO_REGISTER, ®) || pipe2(h->done, O_CLOEXEC)) + goto out_page; + + if (pthread_create(&reader, NULL, readdir_hold_reader, h)) + goto out_pipe; + if (!readdir_hold_faulted(h)) + goto out_reader; + if (pthread_create(&creator, NULL, readdir_hold_creator, h)) + goto out_reader; + for (i = 0; i < HOLD_QUEUE_MS / 10 && !readdir_hold_creator_settled(h); i++) + usleep(10000); + if (pthread_create(&finder, NULL, readdir_hold_finder, h)) + goto out_creator; + + pfd.fd = h->done[0]; + pfd.events = POLLIN; + *stalled = poll(&pfd, 1, HOLD_LOOKUP_MS) != 1; + ret = 0; + + readdir_hold_release(h); + pthread_join(finder, NULL); +out_creator: + if (ret) + readdir_hold_release(h); + pthread_join(creator, NULL); +out_reader: + if (ret) + readdir_hold_release(h); + pthread_join(reader, NULL); +out_pipe: + close(h->done[0]); + close(h->done[1]); +out_page: + munmap(h->page, h->page_size); + h->page = MAP_FAILED; + return ret; +} + +#endif /* __SELFTESTS_READDIR_HOLD_H */ diff --git a/tools/testing/selftests/filesystems/rw_hint_test.c b/tools/testing/selftests/filesystems/rw_hint_test.c new file mode 100644 index 000000000000..d1930f82f63b --- /dev/null +++ b/tools/testing/selftests/filesystems/rw_hint_test.c @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * F_SET_RW_HINT is refused on an immutable inode. Nothing is ever written + * to it and it may be shared with everybody, like a namespace file or the + * root of an empty mount namespace. + */ +#define _GNU_SOURCE +#include <errno.h> +#include <fcntl.h> +#include <sched.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <sys/ioctl.h> +#include <sys/stat.h> +#include <sys/wait.h> + +#include "../kselftest_harness.h" + +/* <linux/fs.h> and <linux/fcntl.h> don't mix with the libc headers */ +#ifndef FS_IOC_GETFLAGS +#define FS_IOC_GETFLAGS _IOR('f', 1, long) +#define FS_IOC_SETFLAGS _IOW('f', 2, long) +#endif +#ifndef FS_IMMUTABLE_FL +#define FS_IMMUTABLE_FL 0x00000010 +#endif +#ifndef F_LINUX_SPECIFIC_BASE +#define F_LINUX_SPECIFIC_BASE 1024 +#endif +#ifndef F_GET_RW_HINT +#define F_GET_RW_HINT (F_LINUX_SPECIFIC_BASE + 11) +#define F_SET_RW_HINT (F_LINUX_SPECIFIC_BASE + 12) +#endif +#ifndef RWH_WRITE_LIFE_SHORT +#define RWH_WRITE_LIFE_SHORT 2 +#endif +#ifndef UNSHARE_EMPTY_MNTNS +#define UNSHARE_EMPTY_MNTNS 0x00100000 +#endif + +static int set_hint(int fd, uint64_t hint) +{ + return fcntl(fd, F_SET_RW_HINT, &hint); +} + +static long get_hint(int fd) +{ + uint64_t hint; + + if (fcntl(fd, F_GET_RW_HINT, &hint)) + return -1; + return hint; +} + +TEST(immutable_file) +{ + char path[] = "/tmp/rw_hint.XXXXXX"; + int fd, flags; + + if (geteuid()) + SKIP(return, "test requires root"); + + fd = mkstemp(path); + ASSERT_GE(fd, 0); + unlink(path); + ASSERT_EQ(set_hint(fd, RWH_WRITE_LIFE_SHORT), 0); + EXPECT_EQ(get_hint(fd), RWH_WRITE_LIFE_SHORT); + + if (ioctl(fd, FS_IOC_GETFLAGS, &flags)) { + close(fd); + SKIP(return, "no file attributes on this filesystem"); + } + flags |= FS_IMMUTABLE_FL; + ASSERT_EQ(ioctl(fd, FS_IOC_SETFLAGS, &flags), 0); + EXPECT_EQ(set_hint(fd, RWH_WRITE_LIFE_SHORT), -1); + EXPECT_EQ(errno, EPERM); + flags &= ~FS_IMMUTABLE_FL; + ASSERT_EQ(ioctl(fd, FS_IOC_SETFLAGS, &flags), 0); + EXPECT_EQ(set_hint(fd, RWH_WRITE_LIFE_SHORT), 0); + close(fd); +} + +TEST(namespace_file) +{ + int fd; + + if (geteuid()) + SKIP(return, "test requires root"); + + fd = open("/proc/self/ns/mnt", O_RDONLY | O_CLOEXEC); + ASSERT_GE(fd, 0); + EXPECT_EQ(set_hint(fd, RWH_WRITE_LIFE_SHORT), -1); + EXPECT_EQ(errno, EPERM); + close(fd); +} + +TEST(empty_mntns_root) +{ + int status; + pid_t pid; + + if (geteuid()) + SKIP(return, "test requires root"); + + pid = fork(); + ASSERT_GE(pid, 0); + if (pid == 0) { + int fd; + + if (unshare(UNSHARE_EMPTY_MNTNS)) + _exit(errno == EINVAL ? 100 : 1); + fd = open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC); + if (fd < 0) + _exit(2); + if (set_hint(fd, RWH_WRITE_LIFE_SHORT) == 0) + _exit(3); + _exit(errno == EPERM ? 0 : 4); + } + ASSERT_EQ(waitpid(pid, &status, 0), pid); + ASSERT_TRUE(WIFEXITED(status)); + if (WEXITSTATUS(status) == 100) + SKIP(return, "UNSHARE_EMPTY_MNTNS not supported"); + EXPECT_EQ(WEXITSTATUS(status), 0); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/filesystems/umount_propagation/Makefile b/tools/testing/selftests/filesystems/umount_propagation/Makefile new file mode 100644 index 000000000000..31f783a92b23 --- /dev/null +++ b/tools/testing/selftests/filesystems/umount_propagation/Makefile @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: GPL-2.0 +TEST_GEN_PROGS := umount_propagation_test shrink_submounts_test locked_mount_test + +CFLAGS += -Wall -O2 -g $(KHDR_INCLUDES) + +include ../../lib.mk diff --git a/tools/testing/selftests/filesystems/umount_propagation/locked_mount_test.c b/tools/testing/selftests/filesystems/umount_propagation/locked_mount_test.c new file mode 100644 index 000000000000..0799b2ba545d --- /dev/null +++ b/tools/testing/selftests/filesystems/umount_propagation/locked_mount_test.c @@ -0,0 +1,432 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * MNT_LOCKED keeps the owner of a user namespace from revealing what a + * mount covers. The lock has to be set on the copy in that namespace and + * only there, it has to survive the expiry of a mount placed beneath the + * locked one, and it has to survive the propagated umount of such a copy. + */ +#define _GNU_SOURCE +#include <errno.h> +#include <fcntl.h> +#include <sched.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <sys/mount.h> +#include <sys/prctl.h> +#include <sys/stat.h> +#include <sys/syscall.h> +#include <sys/wait.h> + +#include "../../kselftest_harness.h" + +#ifndef MOVE_MOUNT_F_EMPTY_PATH +#define MOVE_MOUNT_F_EMPTY_PATH 0x00000004 +#endif +#ifndef MOVE_MOUNT_BENEATH +#define MOVE_MOUNT_BENEATH 0x00000200 +#endif + +#define DIR_LEN 64 +#define PATH_LEN 192 +#define FLAGS (MS_NOSUID | MS_NODEV | MS_NOEXEC) + +/* exit codes of the children, each test says what they mean */ +enum { + CHILD_OK, + CHILD_SETUP, + CHILD_STEP1, + CHILD_STEP2, + CHILD_STEP3, + CHILD_STEP4, +}; + +static int write_file(const char *path, const char *s) +{ + ssize_t n = -1; + int fd; + + fd = open(path, O_WRONLY | O_CLOEXEC); + if (fd >= 0) { + n = write(fd, s, strlen(s)); + close(fd); + } + return n == (ssize_t)strlen(s) ? 0 : -1; +} + +static int create_file(const char *path, const char *s) +{ + ssize_t n = -1; + int fd; + + fd = open(path, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0644); + if (fd >= 0) { + n = write(fd, s, strlen(s)); + close(fd); + } + return n == (ssize_t)strlen(s) ? 0 : -1; +} + +/* Root in a new user namespace with a copy of the mount namespace. */ +static int enter_userns(void) +{ + uid_t uid = getuid(); + gid_t gid = getgid(); + char map[32]; + + prctl(PR_SET_DUMPABLE, 1); + if (unshare(CLONE_NEWUSER | CLONE_NEWNS)) + return -1; + if (write_file("/proc/self/setgroups", "deny") && errno != ENOENT) + return -1; + snprintf(map, sizeof(map), "0 %d 1", uid); + if (write_file("/proc/self/uid_map", map)) + return -1; + snprintf(map, sizeof(map), "0 %d 1", gid); + if (write_file("/proc/self/gid_map", map)) + return -1; + return setgid(0) || setuid(0) ? -1 : 0; +} + +static int wait_child(pid_t pid) +{ + int status; + + if (waitpid(pid, &status, 0) != pid || !WIFEXITED(status)) + return -1; + return WEXITSTATUS(status); +} + +static int wait_byte(int fd) +{ + char c; + + return read(fd, &c, 1) == 1 ? 0 : -1; +} + +static int send_byte(int fd) +{ + return write(fd, "x", 1) == 1 ? 0 : -1; +} + +/* A read of @path fails: the file is covered. */ +static bool covered(const char *path) +{ + int fd = open(path, O_RDONLY | O_CLOEXEC); + + if (fd >= 0) + close(fd); + return fd < 0; +} + +FIXTURE(locked_mount) { + char base[DIR_LEN]; + bool tracing; /* debugfs with the tracefs automount is there */ +}; + +FIXTURE_SETUP(locked_mount) +{ + char p[PATH_LEN]; + struct stat st; + + if (geteuid()) + SKIP(return, "test requires root"); + + snprintf(self->base, sizeof(self->base), "/tmp/locked_mount.XXXXXX"); + ASSERT_NE(mkdtemp(self->base), NULL); + ASSERT_EQ(unshare(CLONE_NEWNS), 0); + ASSERT_EQ(mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL), 0); + ASSERT_EQ(mount("tmpfs", self->base, "tmpfs", 0, "mode=0755"), 0); + + /* an automount that a user can name: the tracefs below debugfs */ + snprintf(p, sizeof(p), "%s/dbg", self->base); + ASSERT_EQ(mkdir(p, 0755), 0); + self->tracing = !mount("debugfs", p, "debugfs", FLAGS, NULL); + if (self->tracing) { + /* the cases trigger it themselves */ + snprintf(p, sizeof(p), "%s/dbg/tracing", self->base); + self->tracing = !fstatat(AT_FDCWD, p, &st, AT_NO_AUTOMOUNT) && + S_ISDIR(st.st_mode); + } +} + +FIXTURE_TEARDOWN(locked_mount) +{ + umount2(self->base, MNT_DETACH); + rmdir(self->base); +} + +/* + * The child keeps a directory descriptor on the host's debugfs mount, moves + * to a user namespace of its own and triggers the automount through the + * descriptor. The mount goes below the host's mount and propagates into the + * child's copy. The child's copy has to be locked, the host's not. + */ +static int automount_child(const char *base, int dfd, int to_host, int from_host) +{ + char p[PATH_LEN]; + struct stat st; + + if (enter_userns()) + return CHILD_SETUP; + if (fstatat(dfd, "tracing/.", &st, 0)) + return CHILD_STEP1; + if (send_byte(to_host) || wait_byte(from_host)) + return CHILD_SETUP; + /* the flags of the copy are locked: EPERM */ + snprintf(p, sizeof(p), "%s/dbg/tracing", base); + if (!mount(NULL, p, NULL, MS_REMOUNT | MS_BIND, NULL) || errno != EPERM) + return CHILD_STEP2; + return CHILD_OK; +} + +TEST_F(locked_mount, automount_locked_in_the_triggering_namespace) +{ + int to_host[2], from_host[2], dfd, ret; + char p[PATH_LEN]; + pid_t pid; + + if (!self->tracing) + SKIP(return, "test requires debugfs with the tracefs automount"); + + snprintf(p, sizeof(p), "%s/dbg", self->base); + ASSERT_EQ(mount(NULL, p, NULL, MS_SHARED, NULL), 0); + dfd = open(p, O_RDONLY | O_DIRECTORY | O_CLOEXEC); + ASSERT_GE(dfd, 0); + ASSERT_EQ(pipe(to_host), 0); + ASSERT_EQ(pipe(from_host), 0); + + pid = fork(); + ASSERT_GE(pid, 0); + if (pid == 0) + _exit(automount_child(self->base, dfd, to_host[1], from_host[0])); + close(dfd); + ASSERT_EQ(wait_byte(to_host[0]), 0); + + /* the host's own mount isn't locked: the flags can go */ + snprintf(p, sizeof(p), "%s/dbg/tracing", self->base); + EXPECT_EQ(mount(NULL, p, NULL, MS_REMOUNT | MS_BIND, NULL), 0); + + ASSERT_EQ(send_byte(from_host[1]), 0); + ret = wait_child(pid); + TH_LOG("child exit code %d", ret); + EXPECT_EQ(ret, CHILD_OK); +} + +/* + * A copy of a tree with a locked cover. The child puts a shrinkable mount + * beneath the cover, which hands the lock down, unmounts the cover and then + * asks for the umount of an unlocked ancestor. That expires the shrinkable + * mount on a kernel that doesn't look at the lock, and the covered + * directory is bare. + */ +static int expiry_child(const char *base) +{ + char srv[PATH_LEN], shr[PATH_LEN], x[PATH_LEN], c[PATH_LEN]; + char hidden[PATH_LEN], secret[PATH_LEN]; + + snprintf(srv, sizeof(srv), "%s/srv", base); + snprintf(shr, sizeof(shr), "%s/shr", base); + snprintf(x, sizeof(x), "%s/x", base); + snprintf(c, sizeof(c), "%s/c", base); + snprintf(hidden, sizeof(hidden), "%s/x/hidden", base); + snprintf(secret, sizeof(secret), "%s/x/hidden/secret", base); + + if (enter_userns()) + return CHILD_SETUP; + if (mount(srv, x, NULL, MS_BIND | MS_REC, NULL) || + mount(shr, c, NULL, MS_BIND, NULL)) + return CHILD_SETUP; + /* the cover is locked */ + if (!umount2(hidden, 0) || errno != EINVAL) + return CHILD_STEP1; + if (syscall(__NR_move_mount, AT_FDCWD, c, AT_FDCWD, hidden, MOVE_MOUNT_BENEATH)) + return CHILD_STEP2; + /* the lock moved down, the cover may go */ + if (umount2(hidden, 0)) + return CHILD_STEP3; + /* the holder of the lock may not, in any way */ + if (!umount2(hidden, 0) || errno != EINVAL) + return CHILD_STEP4; + if (chdir(x)) + return CHILD_SETUP; + umount2(x, 0); + return covered(secret) ? CHILD_OK : CHILD_STEP4; +} + +TEST_F(locked_mount, expiry_leaves_a_locked_mount_alone) +{ + char p[PATH_LEN], q[PATH_LEN]; + pid_t pid; + int ret; + + if (!self->tracing) + SKIP(return, "test requires debugfs with the tracefs automount"); + + snprintf(p, sizeof(p), "%s/dbg/tracing", self->base); + snprintf(q, sizeof(q), "%s/shr", self->base); + ASSERT_EQ(mkdir(q, 0755), 0); + /* a bind of an automount is shrinkable as well */ + ASSERT_EQ(mount(p, q, NULL, MS_BIND, NULL), 0); + snprintf(p, sizeof(p), "%s/srv", self->base); + ASSERT_EQ(mkdir(p, 0755), 0); + ASSERT_EQ(mount("tmpfs", p, "tmpfs", 0, "mode=0755"), 0); + snprintf(p, sizeof(p), "%s/srv/hidden", self->base); + ASSERT_EQ(mkdir(p, 0755), 0); + snprintf(p, sizeof(p), "%s/srv/hidden/secret", self->base); + ASSERT_EQ(create_file(p, "covered-by-root\n"), 0); + snprintf(p, sizeof(p), "%s/srv/hidden", self->base); + ASSERT_EQ(mount("tmpfs", p, "tmpfs", 0, "mode=0755"), 0); + snprintf(p, sizeof(p), "%s/x", self->base); + ASSERT_EQ(mkdir(p, 0755), 0); + snprintf(p, sizeof(p), "%s/c", self->base); + ASSERT_EQ(mkdir(p, 0755), 0); + + pid = fork(); + ASSERT_GE(pid, 0); + if (pid == 0) + _exit(expiry_child(self->base)); + ret = wait_child(pid); + TH_LOG("child exit code %d", ret); + EXPECT_EQ(ret, CHILD_OK); +} + +/* + * The copy of the mount tree that a user namespace gets at its creation has + * the automounts in it locked unless they are on an expiry list. A bind of + * such a tree inside the namespace copies the lock to the automount but not + * to the root of the bind, so the child may ask for the umount of that root. + * That must not expire the locked automount below it: the root is busy, the + * umount fails and the automount has to be there afterwards. + */ +static int copied_tree_child(const char *base) +{ + char dbg[PATH_LEN], x[PATH_LEN], tracing[PATH_LEN]; + struct stat root, st; + + snprintf(dbg, sizeof(dbg), "%s/dbg", base); + snprintf(x, sizeof(x), "%s/x", base); + snprintf(tracing, sizeof(tracing), "%s/x/tracing", base); + + if (enter_userns()) + return CHILD_SETUP; + if (mount(dbg, x, NULL, MS_BIND | MS_REC, NULL)) + return CHILD_SETUP; + /* the copy of the automount carries the lock */ + if (!umount2(tracing, 0) || errno != EINVAL) + return CHILD_STEP1; + /* the root of the bind doesn't; keep it busy */ + if (chdir(x)) + return CHILD_SETUP; + if (!umount2(x, 0) || errno != EBUSY) + return CHILD_STEP2; + /* the automount below it must not have gone */ + if (stat(x, &root) || fstatat(AT_FDCWD, tracing, &st, AT_NO_AUTOMOUNT)) + return CHILD_SETUP; + return st.st_dev != root.st_dev ? CHILD_OK : CHILD_STEP3; +} + +TEST_F(locked_mount, umount_of_a_bind_leaves_a_locked_automount_alone) +{ + char p[PATH_LEN]; + struct stat st; + pid_t pid; + int ret; + + if (!self->tracing) + SKIP(return, "test requires debugfs with the tracefs automount"); + + /* the copy the child gets has to contain the automount */ + snprintf(p, sizeof(p), "%s/dbg/tracing/.", self->base); + ASSERT_EQ(stat(p, &st), 0); + snprintf(p, sizeof(p), "%s/x", self->base); + ASSERT_EQ(mkdir(p, 0755), 0); + + pid = fork(); + ASSERT_GE(pid, 0); + if (pid == 0) + _exit(copied_tree_child(self->base)); + ret = wait_child(pid); + TH_LOG("child exit code %d", ret); + EXPECT_EQ(ret, CHILD_OK); +} + +/* + * Z is a user namespace with a copy of a tree in which P/d is covered by a + * locked mount. The host mounts X on P/d, which propagates beneath Z's + * cover, and unmounts it again. Z's cover has to be locked afterwards as + * it was before. + */ +static int propagation_child(const char *base, int to_host, int from_host) +{ + char d[PATH_LEN], secret[PATH_LEN]; + + snprintf(d, sizeof(d), "%s/P/d", base); + snprintf(secret, sizeof(secret), "%s/P/d/secret", base); + + if (enter_userns()) + return CHILD_SETUP; + /* the cover is locked */ + if (!umount2(d, 0) || errno != EINVAL) + return CHILD_STEP1; + if (send_byte(to_host) || wait_byte(from_host)) + return CHILD_SETUP; + /* X came and went beneath it: still locked */ + if (!umount2(d, 0) || errno != EINVAL) + return CHILD_STEP2; + return covered(secret) ? CHILD_OK : CHILD_STEP3; +} + +TEST_F(locked_mount, propagated_copy_keeps_the_cover_locked) +{ + int to_host[2], from_host[2], ret; + char p[PATH_LEN]; + pid_t pid; + + snprintf(p, sizeof(p), "%s/P", self->base); + ASSERT_EQ(mkdir(p, 0755), 0); + ASSERT_EQ(mount("tmpfs", p, "tmpfs", 0, "mode=0755"), 0); + ASSERT_EQ(mount(NULL, p, NULL, MS_SHARED, NULL), 0); + snprintf(p, sizeof(p), "%s/P/d", self->base); + ASSERT_EQ(mkdir(p, 0755), 0); + snprintf(p, sizeof(p), "%s/P/d/secret", self->base); + ASSERT_EQ(create_file(p, "covered-by-root\n"), 0); + ASSERT_EQ(pipe(to_host), 0); + ASSERT_EQ(pipe(from_host), 0); + + pid = fork(); + ASSERT_GE(pid, 0); + if (pid == 0) { + /* M: a manager's namespace that covers P/d for Z */ + pid_t z; + + if (unshare(CLONE_NEWNS)) + _exit(CHILD_SETUP); + snprintf(p, sizeof(p), "%s/P", self->base); + if (mount(NULL, p, NULL, MS_SLAVE, NULL)) + _exit(CHILD_SETUP); + snprintf(p, sizeof(p), "%s/P/d", self->base); + if (mount("tmpfs", p, "tmpfs", 0, "mode=0755")) + _exit(CHILD_SETUP); + z = fork(); + if (z < 0) + _exit(CHILD_SETUP); + if (z == 0) + _exit(propagation_child(self->base, to_host[1], from_host[0])); + _exit(wait_child(z)); + } + ASSERT_EQ(wait_byte(to_host[0]), 0); + + /* the host mounts on P/d and unmounts again; both propagate */ + snprintf(p, sizeof(p), "%s/P/d", self->base); + ASSERT_EQ(mount("tmpfs", p, "tmpfs", 0, "mode=0755"), 0); + ASSERT_EQ(umount2(p, 0), 0); + + ASSERT_EQ(send_byte(from_host[1]), 0); + ret = wait_child(pid); + TH_LOG("child exit code %d", ret); + EXPECT_EQ(ret, CHILD_OK); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/filesystems/umount_propagation/shrink_submounts_test.c b/tools/testing/selftests/filesystems/umount_propagation/shrink_submounts_test.c new file mode 100644 index 000000000000..b024ae3417ce --- /dev/null +++ b/tools/testing/selftests/filesystems/umount_propagation/shrink_submounts_test.c @@ -0,0 +1,211 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * A synchronous umount first unmounts the shrinkable submounts of the + * victim that aren't busy. Every one of them has to be checked right + * before it is unmounted: unmounting one can slide a busy mount to where + * the propagated copy of the next one is looked up. + */ +#define _GNU_SOURCE +#include <errno.h> +#include <fcntl.h> +#include <sched.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <sys/fanotify.h> +#include <sys/mount.h> +#include <sys/stat.h> +#include <sys/statfs.h> +#include <sys/vfs.h> +#include <linux/magic.h> + +#include "../../kselftest_harness.h" + +#ifndef FAN_REPORT_MNT +#define FAN_REPORT_MNT 0x00004000 +#endif +#ifndef FAN_MARK_MNTNS +#define FAN_MARK_MNTNS 0x00000110 +#endif +#ifndef FAN_MNT_ATTACH +#define FAN_MNT_ATTACH 0x01000000 +#endif +#ifndef FAN_MNT_DETACH +#define FAN_MNT_DETACH 0x02000000 +#endif + +#define DIR_LEN 64 +#define PATH_LEN 128 + +FIXTURE(shrink_submounts) { + char base[DIR_LEN]; + char automount[PATH_LEN]; + bool mounted; + int fan; +}; + +/* + * Shrinkable mounts come from an automount. The tracefs mount below debugfs + * is one and bind mounts inherit the flag. + */ +FIXTURE_SETUP(shrink_submounts) +{ + struct stat st; + char p[PATH_LEN]; + + self->mounted = false; + self->fan = -1; + + if (geteuid() != 0) + SKIP(return, "test requires CAP_SYS_ADMIN"); + + ASSERT_EQ(unshare(CLONE_NEWNS), 0); + ASSERT_EQ(mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL), 0); + + snprintf(self->base, sizeof(self->base), "/tmp/shrink_submounts.XXXXXX"); + ASSERT_NE(mkdtemp(self->base), NULL); + ASSERT_EQ(mount("tmpfs", self->base, "tmpfs", 0, NULL), 0); + self->mounted = true; + ASSERT_EQ(mount(NULL, self->base, NULL, MS_PRIVATE, NULL), 0); + + snprintf(p, sizeof(p), "%s/dbg", self->base); + ASSERT_EQ(mkdir(p, 0755), 0); + if (mount("debugfs", p, "debugfs", 0, NULL)) { + umount2(self->base, MNT_DETACH); + rmdir(self->base); + SKIP(return, "test requires debugfs"); + } + snprintf(self->automount, sizeof(self->automount), "%s/dbg/tracing", + self->base); + snprintf(p, sizeof(p), "%s/dbg/tracing/.", self->base); + if (stat(p, &st)) { + umount2(self->base, MNT_DETACH); + rmdir(self->base); + SKIP(return, "test requires the tracefs automount"); + } +} + +FIXTURE_TEARDOWN(shrink_submounts) +{ + if (self->fan >= 0) + close(self->fan); + chdir("/"); + if (self->mounted) + umount2(self->base, MNT_DETACH); + rmdir(self->base); +} + +static bool mounted_tmpfs(const char *path) +{ + struct statfs st; + + return !statfs(path, &st) && st.f_type == TMPFS_MAGIC; +} + +/* + * P is a shared bind mount of the automount, P1 a slave of P that is shared + * in turn and P2 its peer. B, another bind mount of the automount, goes on + * P/options and propagates copies Bc1 and Bc2 onto P1/options and + * P2/options. R, a tmpfs and our working directory, sits on top of Bc2 which + * is made private first. P, with B on it, is moved to P1/instances. + * + * A synchronous umount of P1 unmounts the shrinkable submounts Bc1 and B + * first. Unmounting Bc1 takes Bc2 along and slides R to P2/options where + * the propagated copy of B is looked up next. R is busy, so B has to stay + * and the umount fails with EBUSY. + */ +TEST_F(shrink_submounts, busy_mount_moved_into_reach) +{ + char p[PATH_LEN], p1[PATH_LEN], p2[PATH_LEN], r[PATH_LEN], cwd[PATH_LEN]; + int nsfd; + + snprintf(p, sizeof(p), "%s/p", self->base); + snprintf(p1, sizeof(p1), "%s/p1", self->base); + snprintf(p2, sizeof(p2), "%s/p2", self->base); + ASSERT_EQ(mkdir(p, 0755), 0); + ASSERT_EQ(mkdir(p1, 0755), 0); + ASSERT_EQ(mkdir(p2, 0755), 0); + + /* watch the mount namespace so that the detached mounts get queued */ + self->fan = fanotify_init(FAN_REPORT_MNT, O_RDONLY); + if (self->fan >= 0) { + nsfd = open("/proc/self/ns/mnt", O_RDONLY | O_CLOEXEC); + ASSERT_GE(nsfd, 0); + EXPECT_EQ(fanotify_mark(self->fan, FAN_MARK_ADD | FAN_MARK_MNTNS, + FAN_MNT_ATTACH | FAN_MNT_DETACH, nsfd, NULL), 0); + close(nsfd); + } + + ASSERT_EQ(mount(self->automount, p, NULL, MS_BIND, NULL), 0); + ASSERT_EQ(mount(NULL, p, NULL, MS_SHARED, NULL), 0); + ASSERT_EQ(mount(p, p1, NULL, MS_BIND, NULL), 0); + ASSERT_EQ(mount(NULL, p1, NULL, MS_SLAVE, NULL), 0); + ASSERT_EQ(mount(NULL, p1, NULL, MS_SHARED, NULL), 0); + ASSERT_EQ(mount(p1, p2, NULL, MS_BIND, NULL), 0); + + /* B on P/options, copies on P1/options and P2/options */ + snprintf(r, sizeof(r), "%s/p/options", self->base); + ASSERT_EQ(mount(self->automount, r, NULL, MS_BIND, NULL), 0); + + /* R on top of Bc2 */ + snprintf(r, sizeof(r), "%s/p2/options", self->base); + ASSERT_EQ(mount(NULL, r, NULL, MS_PRIVATE, NULL), 0); + ASSERT_EQ(mount("R", r, "tmpfs", 0, NULL), 0); + ASSERT_EQ(chdir(r), 0); + + /* P, with B on it, below the victim */ + snprintf(cwd, sizeof(cwd), "%s/p1/instances", self->base); + ASSERT_EQ(mount(p, cwd, NULL, MS_MOVE, NULL), 0); + + ASSERT_TRUE(mounted_tmpfs(r)); + ASSERT_EQ(umount2(p1, 0), -1); + EXPECT_EQ(errno, EBUSY); + + /* R is still mounted and still our working directory */ + EXPECT_TRUE(mounted_tmpfs(r)); + ASSERT_NE(getcwd(cwd, sizeof(cwd)), NULL); + EXPECT_STREQ(cwd, r); +} + +/* + * T is shared and Q, a slave of T, has T moved into it, so Q receives + * propagation from its own child. M, another bind mount of the automount, is + * on T/options and that is the dentry T sits on in Q. Unmounting M makes T + * the propagated victim at that dentry in Q and takes T along. The shrink + * walk of V has just unmounted M and continues in the children of T. + */ +TEST_F(shrink_submounts, parent_goes_with_child) +{ + char v[PATH_LEN], t[PATH_LEN], q[PATH_LEN], p[PATH_LEN]; + struct stat before, after; + + snprintf(v, sizeof(v), "%s/v", self->base); + snprintf(t, sizeof(t), "%s/v/t", self->base); + snprintf(q, sizeof(q), "%s/v/q", self->base); + ASSERT_EQ(mkdir(v, 0755), 0); + ASSERT_EQ(mount("V", v, "tmpfs", 0, NULL), 0); + ASSERT_EQ(mount(NULL, v, NULL, MS_PRIVATE, NULL), 0); + ASSERT_EQ(stat(v, &before), 0); + ASSERT_EQ(mkdir(t, 0755), 0); + ASSERT_EQ(mkdir(q, 0755), 0); + + /* T shared, M on T/options before anything receives from T */ + ASSERT_EQ(mount(self->automount, t, NULL, MS_BIND, NULL), 0); + ASSERT_EQ(mount(NULL, t, NULL, MS_SHARED, NULL), 0); + snprintf(p, sizeof(p), "%s/v/t/options", self->base); + ASSERT_EQ(mount(self->automount, p, NULL, MS_BIND, NULL), 0); + + /* Q, a slave of T, and T moved into Q at the dentry M sits on */ + ASSERT_EQ(mount(t, q, NULL, MS_BIND, NULL), 0); + ASSERT_EQ(mount(NULL, q, NULL, MS_SLAVE, NULL), 0); + snprintf(p, sizeof(p), "%s/v/q/options", self->base); + ASSERT_EQ(mount(t, p, NULL, MS_MOVE, NULL), 0); + + /* M, T and then Q go, V is empty and can be unmounted */ + ASSERT_EQ(umount2(v, 0), 0); + ASSERT_EQ(stat(v, &after), 0); + EXPECT_NE(before.st_dev, after.st_dev); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/filesystems/umount_propagation/umount_propagation_test.c b/tools/testing/selftests/filesystems/umount_propagation/umount_propagation_test.c new file mode 100644 index 000000000000..9e18d54dfb32 --- /dev/null +++ b/tools/testing/selftests/filesystems/umount_propagation/umount_propagation_test.c @@ -0,0 +1,226 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * A synchronous umount fails with EBUSY when a mount it would pull out by + * propagation is still in use. + */ +#define _GNU_SOURCE +#include <errno.h> +#include <fcntl.h> +#include <sched.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/mount.h> +#include <sys/stat.h> +#include <sys/syscall.h> +#include <sys/wait.h> +#include <unistd.h> +#include <linux/mount.h> +#include <linux/stat.h> + +#include "../../kselftest_harness.h" + +#ifndef OPEN_TREE_CLONE +#define OPEN_TREE_CLONE 1 +#endif +#ifndef OPEN_TREE_CLOEXEC +#define OPEN_TREE_CLOEXEC O_CLOEXEC +#endif +#ifndef AT_RECURSIVE +#define AT_RECURSIVE 0x8000 +#endif +#ifndef MOVE_MOUNT_F_EMPTY_PATH +#define MOVE_MOUNT_F_EMPTY_PATH 0x00000004 +#endif +#ifndef MOVE_MOUNT_BENEATH +#define MOVE_MOUNT_BENEATH 0x00000200 +#endif +#ifndef STATX_MNT_ID +#define STATX_MNT_ID 0x00001000U +#endif + +static int sys_open_tree(int dfd, const char *filename, unsigned int flags) +{ + return syscall(__NR_open_tree, dfd, filename, flags); +} + +static int sys_move_mount(int from_dfd, const char *from_pathname, + int to_dfd, const char *to_pathname, + unsigned int flags) +{ + return syscall(__NR_move_mount, from_dfd, from_pathname, to_dfd, + to_pathname, flags); +} + +/* Child exit codes. */ +enum { + CHILD_OK, + CHILD_UNSHARE, /* could not set up the slave namespace */ + CHILD_OPEN_TREE, /* open_tree() failed */ + CHILD_MOVE_MOUNT, /* move_mount() failed */ + CHILD_STATX, /* statx() failed */ + CHILD_PIPE, /* the parent went away */ +}; + +/* Messages between parent and child. */ +enum { + MSG_READY = 'r', /* child: the copy is mounted and referenced */ + MSG_CHECK = 'c', /* parent: check that the copy is still attached */ + MSG_ATTACHED = 'a', /* child: it is */ + MSG_DETACHED = 'd', /* child: it is not */ + MSG_CLOSE = 'x', /* parent: drop the reference */ + MSG_CLOSED = 'y', /* child: dropped */ + MSG_EXIT = 'e', /* parent: done */ +}; + +FIXTURE(umount_propagation) +{ + char base[64]; + char victim[80]; + bool mounted; +}; + +FIXTURE_SETUP(umount_propagation) +{ + self->mounted = false; + + if (geteuid() != 0) + SKIP(return, "test requires CAP_SYS_ADMIN"); + + ASSERT_EQ(unshare(CLONE_NEWNS), 0); + ASSERT_EQ(mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL), 0); + + snprintf(self->base, sizeof(self->base), "/tmp/umount_propagation.XXXXXX"); + ASSERT_NE(mkdtemp(self->base), NULL); + ASSERT_EQ(mount("tmpfs", self->base, "tmpfs", 0, NULL), 0); + self->mounted = true; + ASSERT_EQ(mount(NULL, self->base, NULL, MS_SHARED, NULL), 0); + + snprintf(self->victim, sizeof(self->victim), "%s/victim", self->base); + ASSERT_EQ(mkdir(self->victim, 0755), 0); + ASSERT_EQ(mount("tmpfs", self->victim, "tmpfs", 0, NULL), 0); +} + +FIXTURE_TEARDOWN(umount_propagation) +{ + if (self->mounted) + umount2(self->base, MNT_DETACH); + rmdir(self->base); +} + +static int send_msg(int fd, char msg) +{ + return write(fd, &msg, 1) == 1 ? 0 : -1; +} + +static char recv_msg(int fd) +{ + char msg; + + if (read(fd, &msg, 1) != 1) + return 0; + return msg; +} + +/* Is the mount with id @mnt_id attached in this mount namespace? */ +static bool mount_attached(__u64 mnt_id) +{ + char line[4096]; + bool found = false; + FILE *f; + + f = fopen("/proc/self/mountinfo", "re"); + if (!f) + return false; + + while (fgets(line, sizeof(line), f)) { + if (strtoull(line, NULL, 10) == mnt_id) { + found = true; + break; + } + } + fclose(f); + return found; +} + +/* + * The slave namespace: take a detached copy of the shared tree and move it + * beneath the propagated copy of the victim, keeping the open_tree() + * descriptor as a reference on it. + */ +static int slave_child(const char *base, const char *victim, int to_parent, + int from_parent) +{ + struct statx stx; + int fd; + + if (unshare(CLONE_NEWNS)) + return CHILD_UNSHARE; + if (mount("", "/", NULL, MS_REC | MS_SLAVE, NULL)) + return CHILD_UNSHARE; + + fd = sys_open_tree(AT_FDCWD, base, + OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC | AT_RECURSIVE); + if (fd < 0) + return CHILD_OPEN_TREE; + if (sys_move_mount(fd, "", AT_FDCWD, victim, + MOVE_MOUNT_F_EMPTY_PATH | MOVE_MOUNT_BENEATH)) + return CHILD_MOVE_MOUNT; + if (statx(fd, "", AT_EMPTY_PATH, STATX_MNT_ID, &stx)) + return CHILD_STATX; + + if (send_msg(to_parent, MSG_READY) || recv_msg(from_parent) != MSG_CHECK) + return CHILD_PIPE; + if (send_msg(to_parent, mount_attached(stx.stx_mnt_id) ? + MSG_ATTACHED : MSG_DETACHED)) + return CHILD_PIPE; + + if (recv_msg(from_parent) != MSG_CLOSE) + return CHILD_PIPE; + close(fd); + if (send_msg(to_parent, MSG_CLOSED) || recv_msg(from_parent) != MSG_EXIT) + return CHILD_PIPE; + return CHILD_OK; +} + +TEST_F(umount_propagation, busy_copy_pulled_out) +{ + int to_child[2], to_parent[2]; + int status; + pid_t pid; + + ASSERT_EQ(pipe(to_child), 0); + ASSERT_EQ(pipe(to_parent), 0); + + pid = fork(); + ASSERT_GE(pid, 0); + if (pid == 0) { + close(to_child[1]); + close(to_parent[0]); + _exit(slave_child(self->base, self->victim, to_parent[1], + to_child[0])); + } + close(to_child[0]); + close(to_parent[1]); + + ASSERT_EQ(recv_msg(to_parent[0]), MSG_READY); + + /* the copy in the slave namespace is in use */ + ASSERT_EQ(umount2(self->victim, 0), -1); + ASSERT_EQ(errno, EBUSY); + + ASSERT_EQ(send_msg(to_child[1], MSG_CHECK), 0); + ASSERT_EQ(recv_msg(to_parent[0]), MSG_ATTACHED); + + /* and once it is not, the umount goes through */ + ASSERT_EQ(send_msg(to_child[1], MSG_CLOSE), 0); + ASSERT_EQ(recv_msg(to_parent[0]), MSG_CLOSED); + ASSERT_EQ(umount2(self->victim, 0), 0); + + ASSERT_EQ(send_msg(to_child[1], MSG_EXIT), 0); + ASSERT_EQ(waitpid(pid, &status, 0), pid); + ASSERT_TRUE(WIFEXITED(status)); + ASSERT_EQ(WEXITSTATUS(status), CHILD_OK); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/ftrace/test.d/kprobe/kprobe_non_uniq_symbol.tc b/tools/testing/selftests/ftrace/test.d/kprobe/kprobe_non_uniq_symbol.tc index bc9514428dba..07b1177c1634 100644 --- a/tools/testing/selftests/ftrace/test.d/kprobe/kprobe_non_uniq_symbol.tc +++ b/tools/testing/selftests/ftrace/test.d/kprobe/kprobe_non_uniq_symbol.tc @@ -6,7 +6,7 @@ SYMBOL='name_show' # We skip this test on kernel where SYMBOL is unique or does not exist. -if [ "$(grep -c -E "[[:alnum:]]+ t ${SYMBOL}" /proc/kallsyms)" -le '1' ]; then +if [ "$(grep -c -E "[[:alnum:]]+ t ${SYMBOL}$" /proc/kallsyms)" -le '1' ]; then exit_unsupported fi diff --git a/tools/testing/selftests/hid/hid_bpf.c b/tools/testing/selftests/hid/hid_bpf.c index 7ab86296ff23..8c41ebfb85e4 100644 --- a/tools/testing/selftests/hid/hid_bpf.c +++ b/tools/testing/selftests/hid/hid_bpf.c @@ -5,7 +5,7 @@ #include <bpf/bpf.h> struct hid_hw_request_syscall_args { - __u8 data[10]; + __u8 data[MAX_BUF_SIZE]; unsigned int hid; int retval; size_t size; @@ -54,11 +54,27 @@ FIXTURE_TEARDOWN(hid_bpf) { hid_bpf_teardown(_metadata, self, variant); \ } while (0) +FIXTURE_VARIANT(hid_bpf) { + __u8 *rdesc; + size_t rdesc_size; +}; + +FIXTURE_VARIANT_ADD(hid_bpf, numbered) { + .rdesc = rdesc, + .rdesc_size = sizeof(rdesc), +}; + +FIXTURE_VARIANT_ADD(hid_bpf, unnumbered) { + .rdesc = fido2_rdesc, + .rdesc_size = sizeof(fido2_rdesc), +}; + FIXTURE_SETUP(hid_bpf) { int err; - err = setup_uhid(_metadata, &self->hid, BUS_USB, 0x0001, 0x0a36, rdesc, sizeof(rdesc)); + err = setup_uhid(_metadata, &self->hid, BUS_USB, 0x0001, 0x0a36, + variant->rdesc, variant->rdesc_size); ASSERT_OK(err); } @@ -175,7 +191,7 @@ TEST_F(hid_bpf, raw_event) const struct test_program progs[] = { { .name = "hid_first_event" }, }; - __u8 buf[10] = {0}; + __u8 buf[MAX_BUF_SIZE] = {0}; int err; LOAD_PROGRAMS(progs); @@ -226,7 +242,7 @@ TEST_F(hid_bpf, subprog_raw_event) const struct test_program progs[] = { { .name = "hid_subprog_first_event" }, }; - __u8 buf[10] = {0}; + __u8 buf[MAX_BUF_SIZE] = {0}; int err; LOAD_PROGRAMS(progs); @@ -284,7 +300,7 @@ TEST_F(hid_bpf, test_attach_detach) { .name = "hid_second_event" }, }; struct bpf_link *link; - __u8 buf[10] = {0}; + __u8 buf[MAX_BUF_SIZE] = {0}; int err, link_fd; LOAD_PROGRAMS(progs); @@ -369,7 +385,7 @@ TEST_F(hid_bpf, test_hid_change_report) const struct test_program progs[] = { { .name = "hid_change_report_id" }, }; - __u8 buf[10] = {0}; + __u8 buf[MAX_BUF_SIZE] = {0}; int err; LOAD_PROGRAMS(progs); @@ -396,21 +412,24 @@ TEST_F(hid_bpf, test_hid_user_input_report_call) { struct hid_hw_request_syscall_args args = { .retval = -1, - .size = 10, + .size = MAX_BUF_SIZE, }; DECLARE_LIBBPF_OPTS(bpf_test_run_opts, tattrs, .ctx_in = &args, .ctx_size_in = sizeof(args), ); - __u8 buf[10] = {0}; + __u8 buf[MAX_BUF_SIZE] = {0}; int err, prog_fd; LOAD_BPF; args.hid = self->hid.hid_id; args.data[0] = 1; /* report ID */ - args.data[1] = 2; /* report ID */ - args.data[2] = 42; /* report ID */ + args.data[1] = 2; + args.data[2] = 42; + + if (variant->rdesc == fido2_rdesc) + args.data[0] = 0; prog_fd = bpf_program__fd(self->skel->progs.hid_user_input_report); @@ -428,8 +447,13 @@ TEST_F(hid_bpf, test_hid_user_input_report_call) /* read the data from hidraw */ memset(buf, 0, sizeof(buf)); err = read(self->hidraw_fd, buf, sizeof(buf)); - ASSERT_EQ(err, 6) TH_LOG("read_hidraw"); - ASSERT_EQ(buf[0], 1); + if (variant->rdesc == rdesc) { + ASSERT_EQ(err, 6) TH_LOG("read_hidraw"); + } else { + ASSERT_EQ(err, 64) + TH_LOG("read_hidraw"); + } + ASSERT_EQ(buf[0], args.data[0]); ASSERT_EQ(buf[1], 2); ASSERT_EQ(buf[2], 42); } @@ -442,7 +466,7 @@ TEST_F(hid_bpf, test_hid_user_output_report_call) { struct hid_hw_request_syscall_args args = { .retval = -1, - .size = 10, + .size = MAX_BUF_SIZE, }; DECLARE_LIBBPF_OPTS(bpf_test_run_opts, tattrs, .ctx_in = &args, @@ -455,8 +479,11 @@ TEST_F(hid_bpf, test_hid_user_output_report_call) args.hid = self->hid.hid_id; args.data[0] = 1; /* report ID */ - args.data[1] = 2; /* report ID */ - args.data[2] = 42; /* report ID */ + args.data[1] = 2; + args.data[2] = 42; + + if (variant->rdesc == fido2_rdesc) + args.data[0] = 0; prog_fd = bpf_program__fd(self->skel->progs.hid_user_output_report); @@ -472,9 +499,14 @@ TEST_F(hid_bpf, test_hid_user_output_report_call) ASSERT_OK(err) TH_LOG("error while calling bpf_prog_test_run_opts"); ASSERT_OK(cond_err) TH_LOG("error while calling waiting for the condition"); - ASSERT_EQ(args.retval, 3); + if (variant->rdesc == rdesc) { + ASSERT_EQ(args.retval, 3); + } else if (variant->rdesc == fido2_rdesc) { + ASSERT_EQ(args.retval, 65) + TH_LOG("report size error, should have 64 + 1 extra byte for the report ID 0"); + } - ASSERT_EQ(output_report[0], 1); + ASSERT_EQ(output_report[0], args.data[0]); ASSERT_EQ(output_report[1], 2); ASSERT_EQ(output_report[2], 42); @@ -491,7 +523,7 @@ TEST_F(hid_bpf, test_hid_user_raw_request_call) .retval = -1, .type = HID_FEATURE_REPORT, .request_type = HID_REQ_GET_REPORT, - .size = 10, + .size = MAX_BUF_SIZE, }; DECLARE_LIBBPF_OPTS(bpf_test_run_opts, tattrs, .ctx_in = &args, @@ -516,6 +548,37 @@ TEST_F(hid_bpf, test_hid_user_raw_request_call) /* * Call hid_hw_raw_request against the given uhid device, + * check that the program is called and does the expected. + */ +TEST_F(hid_bpf, test_hid_user_raw_request_call_eio) +{ + struct hid_hw_request_syscall_args args = { + .retval = -1, + .type = HID_FEATURE_REPORT, + .request_type = HID_REQ_GET_PROTOCOL, + .size = MAX_BUF_SIZE, + }; + DECLARE_LIBBPF_OPTS(bpf_test_run_opts, tattrs, + .ctx_in = &args, + .ctx_size_in = sizeof(args), + ); + int err, prog_fd; + + LOAD_BPF; + + args.hid = self->hid.hid_id; + args.data[0] = 1; /* report ID */ + + prog_fd = bpf_program__fd(self->skel->progs.hid_user_raw_request); + + err = bpf_prog_test_run_opts(prog_fd, &tattrs); + ASSERT_OK(err) TH_LOG("error while calling bpf_prog_test_run_opts"); + + ASSERT_EQ(args.retval, -EIO); +} + +/* + * Call hid_hw_raw_request against the given uhid device, * check that the program is called and prevents the * call to uhid. */ @@ -524,7 +587,7 @@ TEST_F(hid_bpf, test_hid_filter_raw_request_call) const struct test_program progs[] = { { .name = "hid_test_filter_raw_request" }, }; - __u8 buf[10] = {0}; + __u8 buf[MAX_BUF_SIZE] = {0}; int err; LOAD_PROGRAMS(progs); @@ -577,7 +640,7 @@ TEST_F(hid_bpf, test_hid_change_raw_request_call) const struct test_program progs[] = { { .name = "hid_test_hidraw_raw_request" }, }; - __u8 buf[10] = {0}; + __u8 buf[MAX_BUF_SIZE] = {0}; int err; LOAD_PROGRAMS(progs); @@ -603,7 +666,7 @@ TEST_F(hid_bpf, test_hid_infinite_loop_raw_request_call) const struct test_program progs[] = { { .name = "hid_test_infinite_loop_raw_request" }, }; - __u8 buf[10] = {0}; + __u8 buf[MAX_BUF_SIZE] = {0}; int err; LOAD_PROGRAMS(progs); @@ -626,7 +689,7 @@ TEST_F(hid_bpf, test_hid_filter_output_report_call) const struct test_program progs[] = { { .name = "hid_test_filter_output_report" }, }; - __u8 buf[10] = {0}; + __u8 buf[MAX_BUF_SIZE] = {0}; int err; LOAD_PROGRAMS(progs); @@ -679,7 +742,7 @@ TEST_F(hid_bpf, test_hid_change_output_report_call) const struct test_program progs[] = { { .name = "hid_test_hidraw_output_report" }, }; - __u8 buf[10] = {0}; + __u8 buf[MAX_BUF_SIZE] = {0}; int err; LOAD_PROGRAMS(progs); @@ -703,7 +766,7 @@ TEST_F(hid_bpf, test_hid_infinite_loop_output_report_call) const struct test_program progs[] = { { .name = "hid_test_infinite_loop_output_report" }, }; - __u8 buf[10] = {0}; + __u8 buf[MAX_BUF_SIZE] = {0}; int err; LOAD_PROGRAMS(progs); @@ -729,7 +792,7 @@ TEST_F(hid_bpf, test_multiply_events_wq) const struct test_program progs[] = { { .name = "hid_test_multiply_events_wq" }, }; - __u8 buf[10] = {0}; + __u8 buf[MAX_BUF_SIZE] = {0}; int err; LOAD_PROGRAMS(progs); @@ -767,7 +830,7 @@ TEST_F(hid_bpf, test_multiply_events) const struct test_program progs[] = { { .name = "hid_test_multiply_events" }, }; - __u8 buf[10] = {0}; + __u8 buf[MAX_BUF_SIZE] = {0}; int err; LOAD_PROGRAMS(progs); @@ -801,7 +864,7 @@ TEST_F(hid_bpf, test_hid_infinite_loop_input_report_call) const struct test_program progs[] = { { .name = "hid_test_infinite_loop_input_report" }, }; - __u8 buf[10] = {0}; + __u8 buf[MAX_BUF_SIZE] = {0}; int err; LOAD_PROGRAMS(progs); @@ -855,7 +918,7 @@ TEST_F(hid_bpf, test_hid_attach_flags) .insert_head = 0, }, }; - __u8 buf[10] = {0}; + __u8 buf[MAX_BUF_SIZE] = {0}; int err; LOAD_PROGRAMS(progs); @@ -886,6 +949,9 @@ TEST_F(hid_bpf, test_rdesc_fixup) }; int err, desc_size; + if (variant->rdesc != rdesc) + SKIP(return, "not compatible report descriptor"); + LOAD_PROGRAMS(progs); /* check that hid_rdesc_fixup() was executed */ diff --git a/tools/testing/selftests/hid/hid_common.h b/tools/testing/selftests/hid/hid_common.h index e3b267446fa0..b7890ba2878f 100644 --- a/tools/testing/selftests/hid/hid_common.h +++ b/tools/testing/selftests/hid/hid_common.h @@ -13,6 +13,7 @@ #include <linux/uhid.h> #define SHOW_UHID_DEBUG 0 +#define MAX_BUF_SIZE 128 #define min(a, b) \ ({ __typeof__(a) _a = (a); \ @@ -97,6 +98,28 @@ static unsigned char rdesc[] = { static __u8 feature_data[] = { 1, 2 }; +static __maybe_unused unsigned char fido2_rdesc[] = { + 0x06, 0xd0, 0xf1, /* Usage Page (FIDO Alliance) */ + 0x09, 0x01, /* Usage (U2F Authenticator Device) */ + 0xa1, 0x01, /* Collection (Application) */ + 0x09, 0x20, /* Usage (Input Report Data) */ + 0x15, 0x00, /* Logical Minimum (0) */ + 0x26, 0xff, 0x00, /* Logical Maximum (255) */ + 0x75, 0x08, /* Report Size (8) */ + 0x95, 0x40, /* Report Count (64) */ + 0x81, 0x02, /* Input (Data,Var,Abs) */ + 0x09, 0x21, /* Usage (Output Report Data) */ + 0x15, 0x00, /* Logical Minimum (0) */ + 0x26, 0xff, 0x00, /* Logical Maximum (255) */ + 0x75, 0x08, /* Report Size (8) */ + 0x95, 0x40, /* Report Count (64) */ + 0x91, 0x02, /* Output (Data,Var,Abs) */ + 0x06, 0x00, 0xff, /* Usage Page (Vendor Defined Page 1) */ + 0x09, 0x22, /* Usage (Vendor Usage 0x22) */ + 0xb1, 0x02, /* Feature (Data,Var,Abs) */ + 0xc0, /* End Collection */ +}; + #define ASSERT_OK(data) ASSERT_FALSE(data) #define ASSERT_OK_PTR(ptr) ASSERT_NE(NULL, ptr) @@ -110,7 +133,7 @@ static pthread_cond_t uhid_started = PTHREAD_COND_INITIALIZER; static pthread_mutex_t uhid_output_mtx = PTHREAD_MUTEX_INITIALIZER; static pthread_cond_t uhid_output_cond = PTHREAD_COND_INITIALIZER; -static unsigned char output_report[10]; +static unsigned char output_report[MAX_BUF_SIZE]; /* no need to protect uhid_stopped, only one thread accesses it */ static bool uhid_stopped; diff --git a/tools/testing/selftests/hid/progs/hid.c b/tools/testing/selftests/hid/progs/hid.c index 361dc7eaad22..48aa8088cc53 100644 --- a/tools/testing/selftests/hid/progs/hid.c +++ b/tools/testing/selftests/hid/progs/hid.c @@ -98,7 +98,7 @@ struct hid_bpf_ops change_report_id = { struct hid_hw_request_syscall_args { /* data needs to come at offset 0 so we can use it in calls */ - __u8 data[10]; + __u8 data[128]; unsigned int hid; int retval; size_t size; diff --git a/tools/testing/selftests/kvm/Makefile.kvm b/tools/testing/selftests/kvm/Makefile.kvm index fa10eb9e8fa7..1f4bc2a1322d 100644 --- a/tools/testing/selftests/kvm/Makefile.kvm +++ b/tools/testing/selftests/kvm/Makefile.kvm @@ -103,6 +103,7 @@ TEST_GEN_PROGS_x86 += x86/nested_tdp_fault_test TEST_GEN_PROGS_x86 += x86/nested_tsc_adjust_test TEST_GEN_PROGS_x86 += x86/nested_tsc_scaling_test TEST_GEN_PROGS_x86 += x86/nested_vmsave_vmload_test +TEST_GEN_PROGS_x86 += x86/nested_x2apic_test TEST_GEN_PROGS_x86 += x86/platform_info_test TEST_GEN_PROGS_x86 += x86/pmu_counters_test TEST_GEN_PROGS_x86 += x86/pmu_event_filter_test diff --git a/tools/testing/selftests/kvm/hardware_disable_test.c b/tools/testing/selftests/kvm/hardware_disable_test.c index 43a36ef3ead8..1c20892d6782 100644 --- a/tools/testing/selftests/kvm/hardware_disable_test.c +++ b/tools/testing/selftests/kvm/hardware_disable_test.c @@ -37,7 +37,7 @@ static void *run_vcpu(void *arg) struct kvm_vcpu *vcpu = arg; struct kvm_run *run = vcpu->run; -#ifndef _GNU_SOURCE +#ifndef __GLIBC__ kvm_sched_setaffinity(0, sizeof(cpu_set_t), &threads_cpu_set); #endif @@ -51,7 +51,7 @@ static void *sleeping_thread(void *arg) { int fd; -#ifndef _GNU_SOURCE +#ifndef __GLIBC__ kvm_sched_setaffinity(0, sizeof(cpu_set_t), &threads_cpu_set); #endif @@ -71,7 +71,7 @@ static void run_test(u32 run) u32 i, j; TEST_ASSERT_EQ(pthread_attr_init(&attr), 0); -#ifdef _GNU_SOURCE +#ifdef __GLIBC__ TEST_ASSERT_EQ(pthread_attr_setaffinity_np(&attr, sizeof(cpu_set_t), &threads_cpu_set), 0); #endif diff --git a/tools/testing/selftests/kvm/include/loongarch/processor.h b/tools/testing/selftests/kvm/include/loongarch/processor.h index 0e59daa8024e..8d9fabdc89e3 100644 --- a/tools/testing/selftests/kvm/include/loongarch/processor.h +++ b/tools/testing/selftests/kvm/include/loongarch/processor.h @@ -79,6 +79,7 @@ #define CPUCFG2_LASX BIT(7) #define CPUCFG2_LLFTP BIT(14) #define CPUCFG2_LLFTPREV GENMASK(17, 15) +#define CPUCFG2_PTW BIT(24) /* LoongArch Basic CSR registers */ #define LOONGARCH_CSR_CRMD 0x0 /* Current mode info */ @@ -128,6 +129,8 @@ #define LOONGARCH_CSR_PGD 0x1b #define LOONGARCH_CSR_PWCTL0 0x1c #define LOONGARCH_CSR_PWCTL1 0x1d +#define CSR_PWCTL1_PTW_SHIFT 24 +#define CSR_PWCTL1_PTW BIT_ULL(CSR_PWCTL1_PTW_SHIFT) #define LOONGARCH_CSR_STLBPGSIZE 0x1e #define LOONGARCH_CSR_CPUID 0x20 #define LOONGARCH_CSR_KS0 0x30 diff --git a/tools/testing/selftests/kvm/lib/loongarch/processor.c b/tools/testing/selftests/kvm/lib/loongarch/processor.c index 47bbde3e205a..2b5a742a8939 100644 --- a/tools/testing/selftests/kvm/lib/loongarch/processor.c +++ b/tools/testing/selftests/kvm/lib/loongarch/processor.c @@ -278,6 +278,7 @@ static void loongarch_set_csr(struct kvm_vcpu *vcpu, u64 id, u64 val) void loongarch_vcpu_setup(struct kvm_vcpu *vcpu) { + bool has_ptw; int ret, width; unsigned int cfg; unsigned long val; @@ -300,6 +301,12 @@ void loongarch_vcpu_setup(struct kvm_vcpu *vcpu) ret = __kvm_has_device_attr(vm->fd, KVM_LOONGARCH_VM_FEAT_CTRL, KVM_LOONGARCH_VM_FEAT_LASX); if (!ret) cfg |= CPUCFG2_LASX; + ret = __kvm_has_device_attr(vm->fd, KVM_LOONGARCH_VM_FEAT_CTRL, KVM_LOONGARCH_VM_FEAT_PTW); + if (!ret) + cfg |= CPUCFG2_PTW; + + has_ptw = !ret; + loongarch_set_cpucfg(vcpu, LOONGARCH_CPUCFG2, cfg); cfg = read_cpucfg(LOONGARCH_CPUCFG6); loongarch_set_cpucfg(vcpu, LOONGARCH_CPUCFG6, cfg); @@ -338,6 +345,9 @@ void loongarch_vcpu_setup(struct kvm_vcpu *vcpu) /* PGD page shift and width */ val = (vm->page_shift + width * (vm->mmu.pgtable_levels - 1)) | width << 6; + if (has_ptw) + val |= CSR_PWCTL1_PTW; + loongarch_set_csr(vcpu, LOONGARCH_CSR_PWCTL1, val); loongarch_set_csr(vcpu, LOONGARCH_CSR_PGDL, vm->mmu.pgd); @@ -345,8 +355,10 @@ void loongarch_vcpu_setup(struct kvm_vcpu *vcpu) * Refill exception runs on real mode * Entry address should be physical address */ - val = addr_gva2gpa(vm, (unsigned long)handle_tlb_refill); - loongarch_set_csr(vcpu, LOONGARCH_CSR_TLBRENTRY, val); + if (!has_ptw) { + val = addr_gva2gpa(vm, (unsigned long)handle_tlb_refill); + loongarch_set_csr(vcpu, LOONGARCH_CSR_TLBRENTRY, val); + } /* * General exception runs on page-enabled mode diff --git a/tools/testing/selftests/kvm/x86/nested_x2apic_test.c b/tools/testing/selftests/kvm/x86/nested_x2apic_test.c new file mode 100644 index 000000000000..ce204ce29a9c --- /dev/null +++ b/tools/testing/selftests/kvm/x86/nested_x2apic_test.c @@ -0,0 +1,222 @@ +// SPDX-License-Identifier: GPL-2.0-only +#include "test_util.h" +#include "kvm_util.h" +#include "processor.h" +#include "vmx.h" +#include "svm_util.h" + +/* + * Use the kernel's posted interrupt vectors to minimize the risk of crashing + * the host if KVM is buggy. Note, the vectors aren't set in stone, ideally + * these will be kept up-to-date if the kernel vectors change, but it's "fine" + * if they are stale. + */ +#define POSTED_INTR_VECTOR 0xf2 +#define POSTED_INTR_WAKEUP_VECTOR 0xf1 +#define POSTED_INTR_NESTED_VECTOR 0xf0 + +static bool inhibit_apicv; +static volatile unsigned int nr_irqs; + +static void guest_irq_handler(struct ex_regs *regs) +{ + nr_irqs++; + x2apic_write_reg(APIC_EOI, 0); +} + +static void l2_guest_code(void) +{ + if (inhibit_apicv) + wrmsr(MSR_IA32_APICBASE, rdmsr(MSR_IA32_APICBASE) & GENMASK_ULL(11, 0)); + + for (;;) { + x2apic_write_reg(APIC_TASKPRI, 0xf0); + GUEST_ASSERT_EQ(x2apic_read_reg(APIC_TASKPRI), 0xf0); + + asm volatile("cpuid" ::: "eax", "ebx", "ecx", "edx"); + } +} + +static void l1_svm_code(struct svm_test_data *svm) +{ + struct vmcb_control_area *ctrl = &svm->vmcb->control; + + generic_svm_setup(svm, l2_guest_code); + ctrl->intercept |= BIT_ULL(INTERCEPT_CPUID) | BIT_ULL(INTERCEPT_MSR_PROT); + + run_guest(svm->vmcb, svm->vmcb_gpa); + GUEST_ASSERT_EQ(ctrl->exit_code, SVM_EXIT_CPUID); + + stgi(); + x2apic_write_reg(APIC_TASKPRI, 0); +} + +static void l1_vmx_code(struct vmx_pages *vmx, struct hyperv_test_pages *hv_pages) +{ + u64 control; + + if (hv_pages) { + wrmsr(HV_X64_MSR_GUEST_OS_ID, HYPERV_LINUX_OS_ID); + enable_vp_assist(hv_pages->vp_assist_gpa, hv_pages->vp_assist); + evmcs_enable(); + } + + GUEST_ASSERT_EQ(prepare_for_vmx_operation(vmx), true); + + if (hv_pages) { + GUEST_ASSERT(load_evmcs(hv_pages)); + current_evmcs->hv_enlightenments_control.msr_bitmap = 1; + } else { + GUEST_ASSERT(load_vmcs(vmx)); + } + + prepare_vmcs(vmx, NULL); + GUEST_ASSERT_EQ(vmwrite(GUEST_RIP, (unsigned long)l2_guest_code), 0); + + control = vmreadz(PIN_BASED_VM_EXEC_CONTROL); + control |= PIN_BASED_EXT_INTR_MASK; + vmwrite(PIN_BASED_VM_EXEC_CONTROL, control); + + control = vmreadz(CPU_BASED_VM_EXEC_CONTROL); + control |= CPU_BASED_USE_MSR_BITMAPS | CPU_BASED_TPR_SHADOW; + GUEST_ASSERT_EQ(vmwrite(CPU_BASED_VM_EXEC_CONTROL, control), 0); + + control = vmreadz(SECONDARY_VM_EXEC_CONTROL); + control |= SECONDARY_EXEC_VIRTUALIZE_X2APIC_MODE | + SECONDARY_EXEC_APIC_REGISTER_VIRT | + SECONDARY_EXEC_VIRTUAL_INTR_DELIVERY; + control &= (rdmsr(MSR_IA32_VMX_PROCBASED_CTLS2) >> 32); + GUEST_ASSERT_EQ(vmwrite(SECONDARY_VM_EXEC_CONTROL, control), 0); + + GUEST_ASSERT(!vmlaunch()); + GUEST_ASSERT_EQ(vmreadz(VM_EXIT_REASON), EXIT_REASON_CPUID); + GUEST_ASSERT_EQ(vmwrite(GUEST_RIP, + vmreadz(GUEST_RIP) + vmreadz(VM_EXIT_INSTRUCTION_LEN)), 0); +} + +static void l1_vmx_code_part2(void) +{ + u64 control; + + control = vmreadz(CPU_BASED_VM_EXEC_CONTROL); + control &= ~CPU_BASED_TPR_SHADOW; + GUEST_ASSERT_EQ(vmwrite(CPU_BASED_VM_EXEC_CONTROL, control), 0); + + control = vmreadz(SECONDARY_VM_EXEC_CONTROL); + control &= ~(SECONDARY_EXEC_VIRTUALIZE_X2APIC_MODE | + SECONDARY_EXEC_APIC_REGISTER_VIRT | + SECONDARY_EXEC_VIRTUAL_INTR_DELIVERY); + GUEST_ASSERT_EQ(vmwrite(SECONDARY_VM_EXEC_CONTROL, control), 0); + + GUEST_ASSERT(!vmresume()); + GUEST_ASSERT_EQ(vmreadz(VM_EXIT_REASON), EXIT_REASON_CPUID); + GUEST_ASSERT_EQ(vmwrite(GUEST_RIP, + vmreadz(GUEST_RIP) + vmreadz(VM_EXIT_INSTRUCTION_LEN)), 0); +} + +static void l1_test_x2apic_intercepts(void) +{ + GUEST_ASSERT_EQ(nr_irqs, 0); + + sti_nop(); + + GUEST_ASSERT_EQ(x2apic_read_reg(APIC_TASKPRI), 0xf0); + + x2apic_write_reg(APIC_ICR, APIC_DEST_SELF | APIC_INT_ASSERT | POSTED_INTR_VECTOR); + GUEST_ASSERT_EQ(nr_irqs, 0); + + x2apic_write_reg(APIC_TASKPRI, 0xff); + GUEST_ASSERT_EQ(x2apic_read_reg(APIC_TASKPRI), 0xff); + x2apic_write_reg(APIC_ICR, APIC_DEST_SELF | APIC_INT_ASSERT | POSTED_INTR_WAKEUP_VECTOR); + GUEST_ASSERT_EQ(nr_irqs, 0); + + x2apic_write_reg(APIC_TASKPRI, 0); + GUEST_ASSERT_EQ(x2apic_read_reg(APIC_TASKPRI), 0); + + x2apic_write_reg(APIC_ICR, APIC_DEST_SELF | APIC_INT_ASSERT | POSTED_INTR_NESTED_VECTOR); + GUEST_ASSERT_EQ(nr_irqs, 3); + + nr_irqs = 0; +} + +static void l1_guest_code(void *test_data, void *hv_pages) +{ + x2apic_enable(); + + if (this_cpu_has(X86_FEATURE_SVM)) + l1_svm_code(test_data); + else + l1_vmx_code(test_data, hv_pages); + + GUEST_ASSERT_EQ(x2apic_read_reg(APIC_TASKPRI), 0); + x2apic_write_reg(APIC_TASKPRI, 0xf0); + + l1_test_x2apic_intercepts(); + + if (this_cpu_has(X86_FEATURE_VMX)) { + l1_vmx_code_part2(); + l1_test_x2apic_intercepts(); + } + + GUEST_DONE(); +} + +static void test_x2apic_intercepts(bool with_inhibit_apicv, bool use_evmcs) +{ + gva_t nested_test_data_gva, hv_pages_gva = 0; + struct kvm_vcpu *vcpu; + struct kvm_vm *vm; + struct ucall uc; + + inhibit_apicv = with_inhibit_apicv; + + vm = vm_create_with_one_vcpu(&vcpu, l1_guest_code); + vm_install_exception_handler(vm, POSTED_INTR_VECTOR, guest_irq_handler); + vm_install_exception_handler(vm, POSTED_INTR_WAKEUP_VECTOR, guest_irq_handler); + vm_install_exception_handler(vm, POSTED_INTR_NESTED_VECTOR, guest_irq_handler); + + sync_global_to_guest(vm, inhibit_apicv); + + if (kvm_cpu_has(X86_FEATURE_SVM)) + vcpu_alloc_svm(vm, &nested_test_data_gva); + else + vcpu_alloc_vmx(vm, &nested_test_data_gva); + + if (use_evmcs) { + vcpu_set_hv_cpuid(vcpu); + vcpu_enable_evmcs(vcpu); + + vcpu_alloc_hyperv_test_pages(vm, &hv_pages_gva); + } + + vcpu_args_set(vcpu, 2, nested_test_data_gva, hv_pages_gva); + + vcpu_run(vcpu); + + TEST_ASSERT_KVM_EXIT_REASON(vcpu, KVM_EXIT_IO); + + switch (get_ucall(vcpu, &uc)) { + case UCALL_DONE: + break; + case UCALL_ABORT: + REPORT_GUEST_ASSERT(uc); + break; + default: + TEST_FAIL("Expected DONE, got unexpected ucall %lu", uc.cmd); + } + + kvm_vm_free(vm); +} + +int main(int argc, char *argv[]) +{ + TEST_REQUIRE(kvm_cpu_has(X86_FEATURE_SVM) || kvm_cpu_has(X86_FEATURE_VMX)); + + test_x2apic_intercepts(true, false); + test_x2apic_intercepts(false, false); + + if (kvm_has_cap(KVM_CAP_HYPERV_ENLIGHTENED_VMCS)) { + test_x2apic_intercepts(true, true); + test_x2apic_intercepts(false, true); + } +} diff --git a/tools/testing/selftests/landlock/fs_test.c b/tools/testing/selftests/landlock/fs_test.c index 18dbdb99aeba..6e979cef884d 100644 --- a/tools/testing/selftests/landlock/fs_test.c +++ b/tools/testing/selftests/landlock/fs_test.c @@ -10493,9 +10493,9 @@ FIXTURE_TEARDOWN_PARENT(trace_layout1) } /* - * Verifies that check_rule_fs events include correct field values: domain, dev, - * ino, access_request, and grants. All values are verified against stat() of - * the rule path on a deterministic tmpfs layout. + * Verifies that check_rule_inode events include correct field values: domain, + * dev, ino, access_request, and grants. All values are verified against stat() + * of the rule path on a deterministic tmpfs layout. */ TEST_F(trace_layout1, check_rule_fs_fields) { @@ -10529,7 +10529,7 @@ TEST_F(trace_layout1, check_rule_fs_fields) EXPECT_EQ(1, tracefs_count_matches(buf, REGEX_CHECK_RULE_FS(TRACE_TASK))) { - TH_LOG("Expected 1 check_rule_fs event\n%s", buf); + TH_LOG("Expected 1 check_rule_inode event\n%s", buf); } ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_CHECK_RULE_FS(TRACE_TASK), @@ -10570,8 +10570,8 @@ TEST_F(trace_layout1, check_rule_fs_fields) } /* - * Verifies check_rule_fs behavior with multiple rules. With rules at s1d1 and - * s1d2 (a child of s1d1), accessing s1d2 produces only 1 event because the + * Verifies check_rule_inode behavior with multiple rules. With rules at s1d1 + * and s1d2 (a child of s1d1), accessing s1d2 produces only 1 event because the * pathwalk short-circuits after the first rule fully unmasks the single layer. */ TEST_F(trace_layout1, check_rule_fs_multiple_rules) @@ -10643,14 +10643,14 @@ TEST_F(trace_layout1, check_rule_fs_multiple_rules) ASSERT_NE(NULL, buf); /* - * Only 1 check_rule_fs event: the rule on dir_s1d2 fully unmasked the - * single layer, so the pathwalk short-circuits before reaching the + * Only one check_rule_inode event: the rule on dir_s1d2 fully unmasks + * the single layer, so the pathwalk short-circuits before reaching the * dir_s1d1 rule. */ count = tracefs_count_matches(buf, REGEX_CHECK_RULE_FS(TRACE_TASK)); EXPECT_EQ(1, count) { - TH_LOG("Expected 1 check_rule_fs event, got %d\n%s", count, + TH_LOG("Expected 1 check_rule_inode event, got %d\n%s", count, buf); } @@ -10777,7 +10777,7 @@ TEST_F(trace_layout1, check_rule_fs_optional_access) count = tracefs_count_matches(buf, REGEX_CHECK_RULE_FS(TRACE_TASK)); EXPECT_EQ(1, count) { - TH_LOG("Expected 1 check_rule_fs event, got %d\n%s", count, + TH_LOG("Expected 1 check_rule_inode event, got %d\n%s", count, buf); } @@ -10796,7 +10796,7 @@ TEST_F(trace_layout1, check_rule_fs_optional_access) } /* - * Verifies that check_rule_fs fires for a rule that matches the inode even when + * Verifies that check_rule_inode fires for a rule matching the inode even when * it grants none of the requested rights, so the grants set is empty. Landlock * cannot know a rule ignores the request before reading it, so the event is * still emitted (grants={}), which lets a tracer see that the rule matched. @@ -10884,7 +10884,7 @@ TEST_F(trace_layout1, check_rule_fs_empty_grant) count = tracefs_count_matches(buf, REGEX_CHECK_RULE_FS(TRACE_TASK)); EXPECT_EQ(2, count) { - TH_LOG("Expected 2 check_rule_fs events, got %d\n%s", count, + TH_LOG("Expected 2 check_rule_inode events, got %d\n%s", count, buf); } @@ -10894,7 +10894,7 @@ TEST_F(trace_layout1, check_rule_fs_empty_grant) tracefs_count_matches( buf, TRACE_PREFIX( - TRACE_TASK) "landlock_check_rule_fs: domain=[0-9a-f]\\+ " + TRACE_TASK) "landlock_check_rule_inode: domain=[0-9a-f]\\+ " "access_request=read_dir " "dev=[0-9]\\+:[0-9]\\+ ino=[0-9]\\+ " "grants={}$")) @@ -10908,7 +10908,7 @@ TEST_F(trace_layout1, check_rule_fs_empty_grant) tracefs_count_matches( buf, TRACE_PREFIX( - TRACE_TASK) "landlock_check_rule_fs: domain=[0-9a-f]\\+ " + TRACE_TASK) "landlock_check_rule_inode: domain=[0-9a-f]\\+ " "access_request=read_dir " "dev=[0-9]\\+:[0-9]\\+ ino=[0-9]\\+ " "grants={read_dir}$")) diff --git a/tools/testing/selftests/landlock/net_test.c b/tools/testing/selftests/landlock/net_test.c index a18761e0fd82..16afbfdf06bb 100644 --- a/tools/testing/selftests/landlock/net_test.c +++ b/tools/testing/selftests/landlock/net_test.c @@ -3481,13 +3481,14 @@ TEST_F(trace_net, deny_access_net_bind) } /* - * Anchors the denial fields shared by every deny_access_net event so a field - * test proves more than sport/dport: the denying domain, the same-exec bit, the - * audit-logging verdict, and the blocked access all stay populated. + * Anchors the denial fields shared by every deny_access_net event so a port + * test also proves the denying domain, execution status, logging verdict, and + * exact blocked access. */ static void expect_net_deny_common_fields(struct __test_metadata *const _metadata, - const char *const buf) + const char *const buf, + const char *const expected_blockers) { char field[64]; @@ -3511,18 +3512,21 @@ expect_net_deny_common_fields(struct __test_metadata *const _metadata, ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_DENY_ACCESS_NET(TRACE_TASK), "blockers", field, sizeof(field))); - EXPECT_STRNE("", field); + EXPECT_STREQ(expected_blockers, field); } -/* Connect and field-check tests use a separate fixture without variants. */ +enum trace_net_operation { + TRACE_NET_BIND, + TRACE_NET_SEND, +}; /* clang-format off */ -FIXTURE(trace_net_connect) { +FIXTURE(trace_net_address) { /* clang-format on */ int tracefs_ok; }; -FIXTURE_SETUP(trace_net_connect) +FIXTURE_SETUP(trace_net_address) { int ret; @@ -3547,7 +3551,7 @@ FIXTURE_SETUP(trace_net_connect) clear_cap(_metadata, CAP_SYS_ADMIN); } -FIXTURE_TEARDOWN(trace_net_connect) +FIXTURE_TEARDOWN(trace_net_address) { if (!self->tracefs_ok) return; @@ -3559,160 +3563,183 @@ FIXTURE_TEARDOWN(trace_net_connect) } /* clang-format off */ -FIXTURE_VARIANT(trace_net_connect) { +FIXTURE_VARIANT(trace_net_address) { /* clang-format on */ - /* handled_access_net, also the access allowed on the base port. */ - __u64 handled; - /* Bind the allowed base port before the denied operation. */ - bool bind_base_first; - /* Denied operation on the next port: connect (true) or bind (false). */ - bool deny_connect; + int socket_family; + int socket_type; + enum trace_net_operation operation; + int address_family; + socklen_t addrlen; + __u64 handled_access; + const char *expected_blockers; + bool address_port_zero; + bool expected_address_port; + int expected_port; }; /* clang-format off */ - -/* Denied connect(): sport=0, dport=<denied port>. */ -FIXTURE_VARIANT_ADD(trace_net_connect, connect_denied) { - .handled = LANDLOCK_ACCESS_NET_CONNECT_TCP, - .bind_base_first = false, - .deny_connect = true, +FIXTURE_VARIANT_ADD(trace_net_address, ipv4_tcp_bind) { + /* clang-format on */ + .socket_family = AF_INET, + .socket_type = SOCK_STREAM, + .operation = TRACE_NET_BIND, + .address_family = AF_INET, + .addrlen = sizeof(struct sockaddr_in), + .handled_access = LANDLOCK_ACCESS_NET_BIND_TCP, + .expected_blockers = "bind_tcp", + .expected_address_port = true, }; -/* Denied bind(): sport=<denied port>, dport=0. */ -FIXTURE_VARIANT_ADD(trace_net_connect, bind_fields) { - .handled = LANDLOCK_ACCESS_NET_BIND_TCP, - .bind_base_first = false, - .deny_connect = false, +/* Explicit bind(0) has a checked zero port. */ +/* clang-format off */ +FIXTURE_VARIANT_ADD(trace_net_address, ipv4_udp_bind_zero) { + /* clang-format on */ + .socket_family = AF_INET, + .socket_type = SOCK_DGRAM, + .operation = TRACE_NET_BIND, + .address_family = AF_INET, + .addrlen = sizeof(struct sockaddr_in), + .handled_access = LANDLOCK_ACCESS_NET_BIND_UDP, + .expected_blockers = "bind_udp", + .address_port_zero = true, + .expected_port = 0, +}; + +/* A UDP send can deny its synthetic unspecified bind endpoint. */ +/* clang-format off */ +FIXTURE_VARIANT_ADD(trace_net_address, ipv6_udp_autobind) { + /* clang-format on */ + .socket_family = AF_INET6, + .socket_type = SOCK_DGRAM, + .operation = TRACE_NET_SEND, + .address_family = AF_INET6, + .addrlen = sizeof(struct sockaddr_in6), + .handled_access = LANDLOCK_ACCESS_NET_BIND_UDP, + .expected_blockers = "bind_udp", + .expected_port = 0, }; -/* Denied connect() after an allowed bind(): the connect fields (sport=0). */ -FIXTURE_VARIANT_ADD(trace_net_connect, connect_after_bind) { - .handled = LANDLOCK_ACCESS_NET_BIND_TCP | LANDLOCK_ACCESS_NET_CONNECT_TCP, - .bind_base_first = true, - .deny_connect = true, +/* A family-only address has no checked port. */ +/* clang-format off */ +FIXTURE_VARIANT_ADD(trace_net_address, ipv6_unspec_udp_send_min) { + /* clang-format on */ + .socket_family = AF_INET6, + .socket_type = SOCK_DGRAM, + .operation = TRACE_NET_SEND, + .address_family = AF_UNSPEC, + .addrlen = sizeof(sa_family_t), + .handled_access = LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP, + .expected_blockers = "connect_send_udp", + .expected_port = -1, }; -/* clang-format on */ +static void set_trace_net_address(struct sockaddr_storage *const storage, + const int socket_family, + const int address_family, + const unsigned short port) +{ + memset(storage, 0, sizeof(*storage)); -/* - * A denied TCP bind(2) or connect(2) emits one deny_access_net event. The port - * is reported in the field matching the denied operation, in host endianness - * (the UAPI landlock_net_port_attr.port convention): a connect denial reports - * sport=0 dport=<port>, a bind denial reports sport=<port> dport=0, so a - * byte-order or field-swap bug is caught. A prior allowed bind - * (connect_after_bind) does not change the connect denial's fields. - */ -TEST_F(trace_net_connect, deny_access_net) + if (socket_family == AF_INET) { + struct sockaddr_in *const addr4 = (struct sockaddr_in *)storage; + + addr4->sin_family = address_family; + addr4->sin_port = htons(port); + addr4->sin_addr.s_addr = htonl(INADDR_LOOPBACK); + } else { + struct sockaddr_in6 *const addr6 = + (struct sockaddr_in6 *)storage; + + addr6->sin6_family = address_family; + addr6->sin6_port = htons(port); + addr6->sin6_addr = in6addr_loopback; + } +} + +/* Verifies the actionable signed port for representative checked shapes. */ +TEST_F(trace_net_address, deny_access_net) { - pid_t child; - int status; - char *buf; + const char *const event_regex = REGEX_DENY_ACCESS_NET(TRACE_TASK); + const unsigned short address_port = + variant->address_port_zero ? 0 : sock_port_start + 1; + const int expected_port = variant->expected_address_port ? + address_port : + variant->expected_port; + const struct landlock_ruleset_attr ruleset_attr = { + .handled_access_net = variant->handled_access, + }; + struct sockaddr_storage address; char field[64], expected[16]; + char *buf; + int count, ret, ruleset_fd, socket_fd, status; + pid_t child; if (!self->tracefs_ok) SKIP(return, "tracefs not available"); + set_trace_net_address(&address, variant->socket_family, + variant->address_family, address_port); + socket_fd = socket(variant->socket_family, + variant->socket_type | SOCK_CLOEXEC, 0); + ASSERT_LE(0, socket_fd); + ruleset_fd = + landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + ASSERT_EQ(0, tracefs_clear_buf()); + child = fork(); ASSERT_LE(0, child); - if (child == 0) { - struct landlock_ruleset_attr ruleset_attr = { - .handled_access_net = variant->handled, - }; - struct landlock_net_port_attr port_attr = { - .allowed_access = variant->handled, - .port = sock_port_start, - }; - struct sockaddr_in addr = { - .sin_family = AF_INET, - .sin_addr.s_addr = htonl(INADDR_LOOPBACK), - }; - int ruleset_fd, sock_fd, optval = 1, ret; - - ruleset_fd = landlock_create_ruleset(&ruleset_attr, - sizeof(ruleset_attr), 0); - if (ruleset_fd < 0) + if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) _exit(1); - if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, - &port_attr, 0)) { - close(ruleset_fd); - _exit(1); - } - prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); - if (landlock_restrict_self(ruleset_fd, 0)) { - close(ruleset_fd); - _exit(1); - } + if (landlock_restrict_self(ruleset_fd, 0)) + _exit(2); close(ruleset_fd); - sock_fd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0); - if (sock_fd < 0) - _exit(1); - - /* Bind the allowed base port first (succeeds, no event). */ - if (variant->bind_base_first) { - setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR, &optval, - sizeof(optval)); - addr.sin_port = htons(sock_port_start); - if (bind(sock_fd, (struct sockaddr *)&addr, - sizeof(addr))) { - close(sock_fd); - _exit(1); - } - } - - /* Denied operation on the next port. */ - addr.sin_port = htons(sock_port_start + 1); - if (variant->deny_connect) - ret = connect(sock_fd, (struct sockaddr *)&addr, - sizeof(addr)); - else - ret = bind(sock_fd, (struct sockaddr *)&addr, - sizeof(addr)); - if (ret == 0) { - close(sock_fd); - _exit(2); - } - if (errno != EACCES) { - close(sock_fd); + switch (variant->operation) { + case TRACE_NET_BIND: + ret = bind(socket_fd, (const struct sockaddr *)&address, + variant->addrlen); + break; + case TRACE_NET_SEND: + ret = sendto(socket_fd, "A", 1, MSG_NOSIGNAL, + (const struct sockaddr *)&address, + variant->addrlen); + break; + default: _exit(3); } - close(sock_fd); + if (ret >= 0 || errno != EACCES) + _exit(4); + close(socket_fd); + _exit(0); } + close(ruleset_fd); + close(socket_fd); ASSERT_EQ(child, waitpid(child, &status, 0)); ASSERT_TRUE(WIFEXITED(status)); - EXPECT_EQ(0, WEXITSTATUS(status)); + ASSERT_EQ(0, WEXITSTATUS(status)); buf = tracefs_read_buf(); ASSERT_NE(NULL, buf); - - EXPECT_EQ(1, tracefs_count_matches(buf, - REGEX_DENY_ACCESS_NET(TRACE_TASK))); - - expect_net_deny_common_fields(_metadata, buf); - - /* - * The denied operation's port field carries the port; the other is 0. - */ - snprintf(expected, sizeof(expected), "%llu", - (unsigned long long)(sock_port_start + 1)); - - ASSERT_EQ(0, - tracefs_extract_field(buf, REGEX_DENY_ACCESS_NET(TRACE_TASK), - "sport", field, sizeof(field))); - EXPECT_STREQ(variant->deny_connect ? "0" : expected, field); - - ASSERT_EQ(0, - tracefs_extract_field(buf, REGEX_DENY_ACCESS_NET(TRACE_TASK), - "dport", field, sizeof(field))); - EXPECT_STREQ(variant->deny_connect ? expected : "0", field); + count = tracefs_count_matches(buf, event_regex); + if (count != 1) + TH_LOG("Expected 1 denial event, got %d\n%s", count, buf); + ASSERT_EQ(1, count); + expect_net_deny_common_fields(_metadata, buf, + variant->expected_blockers); + + ASSERT_EQ(0, tracefs_extract_field(buf, event_regex, "port", field, + sizeof(field))); + snprintf(expected, sizeof(expected), "%d", expected_port); + EXPECT_STREQ(expected, field); free(buf); } -/* Field verification for the check_rule_net event on an allowed access. */ +/* Field verification for the check_rule_net_port event on an allowed access. */ /* clang-format off */ FIXTURE(trace_net_check_rule) { @@ -3757,10 +3784,11 @@ FIXTURE_TEARDOWN(trace_net_check_rule) /* * Verifies that an allowed bind matching a net-port rule emits exactly one - * landlock_check_rule_net event with the enforcing domain, the requested + * landlock_check_rule_net_port event with the enforcing domain, the requested * access, the checked port (host endianness), and the per-layer grants. The - * whole event is anchored to exact values so a revert of the check_rule_net - * emit (or a byte-order or field-plumbing regression) fails the test. + * whole event is anchored to exact values so removing the check_rule_net_port + * emission or introducing a byte-order or field-plumbing regression fails the + * test. */ TEST_F(trace_net_check_rule, check_rule_net_fields) { @@ -3832,7 +3860,7 @@ TEST_F(trace_net_check_rule, check_rule_net_fields) EXPECT_EQ(1, tracefs_count_matches(buf, REGEX_CHECK_RULE_NET(TRACE_TASK))) { - TH_LOG("Expected 1 check_rule_net event\n%s", buf); + TH_LOG("Expected 1 check_rule_net_port event\n%s", buf); } ASSERT_EQ(0, @@ -3866,11 +3894,4 @@ TEST_F(trace_net_check_rule, check_rule_net_fields) free(buf); } -/* - * IPv6 network trace tests are intentionally elided. IPv6 hook dispatch uses - * the same current_check_access_socket() code path as IPv4, validated by the - * audit tests in this file. The trace events use the same blockers/sport/dport - * fields regardless of address family. - */ - TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/landlock/trace.h b/tools/testing/selftests/landlock/trace.h index ba0c5e92001f..2ec863362173 100644 --- a/tools/testing/selftests/landlock/trace.h +++ b/tools/testing/selftests/landlock/trace.h @@ -27,14 +27,14 @@ TRACEFS_LANDLOCK_DIR "/landlock_create_domain/enable" #define TRACEFS_ENFORCE_DOMAIN_ENABLE \ TRACEFS_LANDLOCK_DIR "/landlock_enforce_domain/enable" -#define TRACEFS_ADD_RULE_FS_ENABLE \ - TRACEFS_LANDLOCK_DIR "/landlock_add_rule_fs/enable" -#define TRACEFS_ADD_RULE_NET_ENABLE \ - TRACEFS_LANDLOCK_DIR "/landlock_add_rule_net/enable" +#define TRACEFS_ADD_RULE_PATH_BENEATH_ENABLE \ + TRACEFS_LANDLOCK_DIR "/landlock_add_rule_path_beneath/enable" +#define TRACEFS_ADD_RULE_NET_PORT_ENABLE \ + TRACEFS_LANDLOCK_DIR "/landlock_add_rule_net_port/enable" #define TRACEFS_CHECK_RULE_FS_ENABLE \ - TRACEFS_LANDLOCK_DIR "/landlock_check_rule_fs/enable" + TRACEFS_LANDLOCK_DIR "/landlock_check_rule_inode/enable" #define TRACEFS_CHECK_RULE_NET_ENABLE \ - TRACEFS_LANDLOCK_DIR "/landlock_check_rule_net/enable" + TRACEFS_LANDLOCK_DIR "/landlock_check_rule_net_port/enable" #define TRACEFS_DENY_ACCESS_FS_ENABLE \ TRACEFS_LANDLOCK_DIR "/landlock_deny_access_fs/enable" #define TRACEFS_DENY_ACCESS_NET_ENABLE \ @@ -79,18 +79,18 @@ */ #define KWORKER_TASK "kworker/[0-9]\\+:[0-9]\\+" -#define REGEX_ADD_RULE_FS(task) \ - TRACE_PREFIX(task) \ - "landlock_add_rule_fs: " \ - "ruleset=[0-9a-f]\\+\\.[0-9]\\+ " \ - "access_rights=[a-z_|]* " \ - "dev=[0-9]\\+:[0-9]\\+ " \ - "ino=[0-9]\\+ " \ +#define REGEX_ADD_RULE_PATH_BENEATH(task) \ + TRACE_PREFIX(task) \ + "landlock_add_rule_path_beneath: " \ + "ruleset=[0-9a-f]\\+\\.[0-9]\\+ " \ + "access_rights=[a-z_|]* " \ + "dev=[0-9]\\+:[0-9]\\+ " \ + "ino=[0-9]\\+ " \ "path=[^ ]\\+$" -#define REGEX_ADD_RULE_NET(task) \ +#define REGEX_ADD_RULE_NET_PORT(task) \ TRACE_PREFIX(task) \ - "landlock_add_rule_net: " \ + "landlock_add_rule_net_port: " \ "ruleset=[0-9a-f]\\+\\.[0-9]\\+ " \ "access_rights=[a-z_|]* " \ "port=[0-9]\\+$" @@ -110,21 +110,21 @@ "parent=[0-9a-f]\\+ " \ "ruleset=[0-9a-f]\\+\\.[0-9]\\+$" -#define REGEX_CHECK_RULE_FS(task) \ - TRACE_PREFIX(task) \ - "landlock_check_rule_fs: " \ - "domain=[0-9a-f]\\+ " \ - "access_request=[a-z_|]* " \ - "dev=[0-9]\\+:[0-9]\\+ " \ - "ino=[0-9]\\+ " \ +#define REGEX_CHECK_RULE_FS(task) \ + TRACE_PREFIX(task) \ + "landlock_check_rule_inode: " \ + "domain=[0-9a-f]\\+ " \ + "access_request=[a-z_|]* " \ + "dev=[0-9]\\+:[0-9]\\+ " \ + "ino=[0-9]\\+ " \ "grants={[a-z_|,]*}$" -#define REGEX_CHECK_RULE_NET(task) \ - TRACE_PREFIX(task) \ - "landlock_check_rule_net: " \ - "domain=[0-9a-f]\\+ " \ - "access_request=[a-z_|]* " \ - "port=[0-9]\\+ " \ +#define REGEX_CHECK_RULE_NET(task) \ + TRACE_PREFIX(task) \ + "landlock_check_rule_net_port: " \ + "domain=[0-9a-f]\\+ " \ + "access_request=[a-z_|]* " \ + "port=[0-9]\\+ " \ "grants={[a-z_|,]*}$" #define REGEX_DENY_ACCESS_FS(task) \ @@ -145,8 +145,7 @@ "same_exec=[01] " \ "logged=[01] " \ "blockers=[a-z_|]* " \ - "sport=[0-9]\\+ " \ - "dport=[0-9]\\+$" + "port=-\\?[0-9]\\+$" #define REGEX_DENY_PTRACE(task) \ TRACE_PREFIX(task) \ diff --git a/tools/testing/selftests/landlock/trace_fs_test.c b/tools/testing/selftests/landlock/trace_fs_test.c index 4543a25c1f55..64014ade3a0e 100644 --- a/tools/testing/selftests/landlock/trace_fs_test.c +++ b/tools/testing/selftests/landlock/trace_fs_test.c @@ -121,7 +121,8 @@ FIXTURE_SETUP(trace_fs) } self->tracefs_ok = 1; - ASSERT_EQ(0, tracefs_enable_event(TRACEFS_ADD_RULE_FS_ENABLE, true)); + ASSERT_EQ(0, tracefs_enable_event(TRACEFS_ADD_RULE_PATH_BENEATH_ENABLE, + true)); ASSERT_EQ(0, tracefs_enable_event(TRACEFS_CHECK_RULE_FS_ENABLE, true)); ASSERT_EQ(0, tracefs_enable_event(TRACEFS_DENY_ACCESS_FS_ENABLE, true)); ASSERT_EQ(0, tracefs_clear()); @@ -134,7 +135,7 @@ FIXTURE_TEARDOWN(trace_fs) return; set_cap(_metadata, CAP_SYS_ADMIN); - tracefs_enable_event(TRACEFS_ADD_RULE_FS_ENABLE, false); + tracefs_enable_event(TRACEFS_ADD_RULE_PATH_BENEATH_ENABLE, false); tracefs_enable_event(TRACEFS_CHECK_RULE_FS_ENABLE, false); tracefs_enable_event(TRACEFS_DENY_ACCESS_FS_ENABLE, false); tracefs_fixture_teardown(); @@ -183,11 +184,11 @@ TEST_F(trace_fs, unsandboxed) } /* - * Verifies that adding a filesystem rule emits a landlock_add_rule_fs trace - * event with the expected path and field values: ruleset ID is non-zero, - * access_rights is non-zero, and path matches. + * Verifies that adding a filesystem rule emits a landlock_add_rule_path_beneath + * event with the expected path and field values: the ruleset ID and + * access_rights are non-zero, and the path matches. */ -TEST_F(trace_fs, add_rule_fs) +TEST_F(trace_fs, add_rule_path_beneath) { struct landlock_ruleset_attr ruleset_attr = { .handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE | @@ -215,28 +216,30 @@ TEST_F(trace_fs, add_rule_fs) buf = tracefs_read_buf(); ASSERT_NE(NULL, buf); - count = tracefs_count_matches(buf, REGEX_ADD_RULE_FS(TRACE_TASK)); + count = tracefs_count_matches(buf, + REGEX_ADD_RULE_PATH_BENEATH(TRACE_TASK)); EXPECT_EQ(1, count) { - TH_LOG("Expected 1 add_rule_fs event, got %d\n%s", count, buf); + TH_LOG("Expected 1 add_rule_path_beneath event, got %d\n%s", + count, buf); } /* Ruleset ID should be non-zero. */ - ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_ADD_RULE_FS(TRACE_TASK), - "ruleset", field_buf, - sizeof(field_buf))); + ASSERT_EQ(0, tracefs_extract_field( + buf, REGEX_ADD_RULE_PATH_BENEATH(TRACE_TASK), + "ruleset", field_buf, sizeof(field_buf))); EXPECT_STRNE("0", field_buf); /* Access rights should be non-zero. */ - ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_ADD_RULE_FS(TRACE_TASK), - "access_rights", field_buf, - sizeof(field_buf))); + ASSERT_EQ(0, tracefs_extract_field( + buf, REGEX_ADD_RULE_PATH_BENEATH(TRACE_TASK), + "access_rights", field_buf, sizeof(field_buf))); EXPECT_STRNE("", field_buf); /* Path should be /usr. */ - ASSERT_EQ(0, - tracefs_extract_field(buf, REGEX_ADD_RULE_FS(TRACE_TASK), - "path", field_buf, sizeof(field_buf))); + ASSERT_EQ(0, tracefs_extract_field( + buf, REGEX_ADD_RULE_PATH_BENEATH(TRACE_TASK), + "path", field_buf, sizeof(field_buf))); EXPECT_STREQ("/usr", field_buf); free(buf); @@ -246,7 +249,7 @@ TEST_F(trace_fs, add_rule_fs) * Verifies that a path whose escaping exceeds the trace scratch sequence does * not corrupt a sibling symbolic field. */ -TEST_F(trace_fs, add_rule_fs_escaped_path_overflow) +TEST_F(trace_fs, add_rule_path_beneath_escaped_path_overflow) { static const char access_prefix[] = "execute|write_file|read_file|"; static const char access_suffix[] = "|ioctl_dev|resolve_unix"; @@ -277,10 +280,12 @@ TEST_F(trace_fs, add_rule_fs_escaped_path_overflow) buf = tracefs_read_buf(); ASSERT_NE(NULL, buf); - count = tracefs_count_matches(buf, REGEX_ADD_RULE_FS(TRACE_TASK)); + count = tracefs_count_matches(buf, + REGEX_ADD_RULE_PATH_BENEATH(TRACE_TASK)); EXPECT_EQ(1, count) { - TH_LOG("Expected 1 add_rule_fs event, got %d\n%s", count, buf); + TH_LOG("Expected 1 add_rule_path_beneath event, got %d\n%s", + count, buf); } /* @@ -288,9 +293,9 @@ TEST_F(trace_fs, add_rule_fs_escaped_path_overflow) * field also catches scratch-sequence poisoning when the compiler * evaluates the overflowing path first, as GCC currently does. */ - ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_ADD_RULE_FS(TRACE_TASK), - "access_rights", field_buf, - sizeof(field_buf))); + ASSERT_EQ(0, tracefs_extract_field( + buf, REGEX_ADD_RULE_PATH_BENEATH(TRACE_TASK), + "access_rights", field_buf, sizeof(field_buf))); EXPECT_EQ(0, strncmp(field_buf, access_prefix, sizeof(access_prefix) - 1)); EXPECT_EQ(NULL, strstr(field_buf, "|refer|")); @@ -298,7 +303,8 @@ TEST_F(trace_fs, add_rule_fs_escaped_path_overflow) ASSERT_LE(sizeof(access_suffix) - 1, field_len); EXPECT_STREQ(access_suffix, field_buf + field_len - (sizeof(access_suffix) - 1)); - expect_truncated_path(_metadata, buf, REGEX_ADD_RULE_FS(TRACE_TASK)); + expect_truncated_path(_metadata, buf, + REGEX_ADD_RULE_PATH_BENEATH(TRACE_TASK)); free(buf); } @@ -542,7 +548,8 @@ TEST_F(trace_fs, check_rule_nested) */ TEST_F(trace_fs, deny_access_fs_denied) { - char *buf; + const char *const event_regex = REGEX_DENY_ACCESS_FS(TRACE_TASK); + char *buf, blockers[64]; int count; ASSERT_EQ(0, tracefs_clear_buf()); @@ -558,8 +565,77 @@ TEST_F(trace_fs, deny_access_fs_denied) buf = tracefs_read_buf(); ASSERT_NE(NULL, buf); - count = tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK)); - EXPECT_LE(1, count); + count = tracefs_count_matches(buf, event_regex); + EXPECT_EQ(1, count) + { + TH_LOG("Expected 1 access denial, got %d\n%s", count, buf); + } + ASSERT_EQ(0, tracefs_extract_field(buf, event_regex, "blockers", + blockers, sizeof(blockers))); + EXPECT_STREQ("read_dir", blockers); + + free(buf); +} + +/* + * Verifies that a denied mount reports the singleton topology blocker rather + * than an empty access mask. + */ +TEST_F(trace_fs, deny_change_topology) +{ + const char *const event_regex = REGEX_DENY_ACCESS_FS(TRACE_TASK); + const struct landlock_ruleset_attr ruleset_attr = { + .handled_access_fs = LANDLOCK_ACCESS_FS_REFER, + }; + char *buf, blockers[64]; + int count, ruleset_fd, status; + pid_t pid; + + ruleset_fd = + landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + ASSERT_EQ(0, tracefs_clear_buf()); + + /* Ensure that Landlock is the only expected mount denial. */ + set_cap(_metadata, CAP_SYS_ADMIN); + pid = fork(); + ASSERT_LE(0, pid); + if (pid == 0) { + if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) { + close(ruleset_fd); + _exit(1); + } + if (landlock_restrict_self(ruleset_fd, 0)) { + close(ruleset_fd); + _exit(2); + } + close(ruleset_fd); + + if (mount(NULL, "/", NULL, MS_PRIVATE | MS_REC, NULL) != -1) + _exit(3); + + if (errno != EPERM) + _exit(4); + + _exit(0); + } + close(ruleset_fd); + clear_cap(_metadata, CAP_SYS_ADMIN); + + ASSERT_EQ(pid, waitpid(pid, &status, 0)); + ASSERT_TRUE(WIFEXITED(status)); + EXPECT_EQ(0, WEXITSTATUS(status)); + + buf = tracefs_read_buf(); + ASSERT_NE(NULL, buf); + count = tracefs_count_matches(buf, event_regex); + EXPECT_EQ(1, count) + { + TH_LOG("Expected 1 topology denial, got %d\n%s", count, buf); + } + ASSERT_EQ(0, tracefs_extract_field(buf, event_regex, "blockers", + blockers, sizeof(blockers))); + EXPECT_STREQ("change_topology", blockers); free(buf); } diff --git a/tools/testing/selftests/landlock/trace_test.c b/tools/testing/selftests/landlock/trace_test.c index afdaf8511b3a..f9b293a9dd56 100644 --- a/tools/testing/selftests/landlock/trace_test.c +++ b/tools/testing/selftests/landlock/trace_test.c @@ -49,8 +49,10 @@ FIXTURE_SETUP(trace) ASSERT_EQ(0, tracefs_enable_event(TRACEFS_CREATE_RULESET_ENABLE, true)); ASSERT_EQ(0, tracefs_enable_event(TRACEFS_CREATE_DOMAIN_ENABLE, true)); ASSERT_EQ(0, tracefs_enable_event(TRACEFS_ENFORCE_DOMAIN_ENABLE, true)); - ASSERT_EQ(0, tracefs_enable_event(TRACEFS_ADD_RULE_FS_ENABLE, true)); - ASSERT_EQ(0, tracefs_enable_event(TRACEFS_ADD_RULE_NET_ENABLE, true)); + ASSERT_EQ(0, tracefs_enable_event(TRACEFS_ADD_RULE_PATH_BENEATH_ENABLE, + true)); + ASSERT_EQ(0, + tracefs_enable_event(TRACEFS_ADD_RULE_NET_PORT_ENABLE, true)); ASSERT_EQ(0, tracefs_enable_event(TRACEFS_CHECK_RULE_FS_ENABLE, true)); ASSERT_EQ(0, tracefs_enable_event(TRACEFS_CHECK_RULE_NET_ENABLE, true)); ASSERT_EQ(0, tracefs_enable_event(TRACEFS_DENY_ACCESS_FS_ENABLE, true)); @@ -72,8 +74,8 @@ FIXTURE_TEARDOWN(trace) tracefs_enable_event(TRACEFS_CREATE_RULESET_ENABLE, false); tracefs_enable_event(TRACEFS_CREATE_DOMAIN_ENABLE, false); tracefs_enable_event(TRACEFS_ENFORCE_DOMAIN_ENABLE, false); - tracefs_enable_event(TRACEFS_ADD_RULE_FS_ENABLE, false); - tracefs_enable_event(TRACEFS_ADD_RULE_NET_ENABLE, false); + tracefs_enable_event(TRACEFS_ADD_RULE_PATH_BENEATH_ENABLE, false); + tracefs_enable_event(TRACEFS_ADD_RULE_NET_PORT_ENABLE, false); tracefs_enable_event(TRACEFS_CHECK_RULE_FS_ENABLE, false); tracefs_enable_event(TRACEFS_CHECK_RULE_NET_ENABLE, false); tracefs_enable_event(TRACEFS_DENY_ACCESS_FS_ENABLE, false); @@ -103,8 +105,10 @@ TEST_F(trace, no_trace_when_disabled) ASSERT_EQ(0, tracefs_enable_event(TRACEFS_CREATE_DOMAIN_ENABLE, false)); ASSERT_EQ(0, tracefs_enable_event(TRACEFS_ENFORCE_DOMAIN_ENABLE, false)); - ASSERT_EQ(0, tracefs_enable_event(TRACEFS_ADD_RULE_FS_ENABLE, false)); - ASSERT_EQ(0, tracefs_enable_event(TRACEFS_ADD_RULE_NET_ENABLE, false)); + ASSERT_EQ(0, tracefs_enable_event(TRACEFS_ADD_RULE_PATH_BENEATH_ENABLE, + false)); + ASSERT_EQ(0, tracefs_enable_event(TRACEFS_ADD_RULE_NET_PORT_ENABLE, + false)); ASSERT_EQ(0, tracefs_enable_event(TRACEFS_CHECK_RULE_FS_ENABLE, false)); ASSERT_EQ(0, tracefs_enable_event(TRACEFS_CHECK_RULE_NET_ENABLE, false)); @@ -265,10 +269,11 @@ TEST_F(trace, ruleset_version) ASSERT_NE(0, !!dot); EXPECT_STREQ("0", dot + 1); - /* Verify 2 add_rule_fs events were emitted. */ - EXPECT_EQ(2, tracefs_count_matches(buf, REGEX_ADD_RULE_FS(TRACE_TASK))) + /* Verify two add_rule_path_beneath events were emitted. */ + EXPECT_EQ(2, tracefs_count_matches( + buf, REGEX_ADD_RULE_PATH_BENEATH(TRACE_TASK))) { - TH_LOG("Expected 2 add_rule_fs events\n%s", buf); + TH_LOG("Expected 2 add_rule_path_beneath events\n%s", buf); } /* @@ -373,7 +378,7 @@ TEST_F(trace, create_domain) tracefs_count_matches(buf, REGEX_CHECK_RULE_FS(TRACE_TASK)); ASSERT_LE(1, check_count) { - TH_LOG("Expected check_rule_fs events\n%s", buf); + TH_LOG("Expected check_rule_inode events\n%s", buf); } EXPECT_EQ(0, tracefs_extract_field(buf, REGEX_CHECK_RULE_FS(TRACE_TASK), @@ -508,9 +513,11 @@ TEST_F(trace, add_rule_invalid_fd) buf = tracefs_read_buf(); ASSERT_NE(NULL, buf); - EXPECT_EQ(0, tracefs_count_matches(buf, REGEX_ADD_RULE_FS(TRACE_TASK))) + EXPECT_EQ(0, tracefs_count_matches( + buf, REGEX_ADD_RULE_PATH_BENEATH(TRACE_TASK))) { - TH_LOG("No add_rule_fs event expected on invalid fd\n%s", buf); + TH_LOG("No add_rule_path_beneath event expected on invalid fd\n%s", + buf); } free(buf); @@ -902,10 +909,10 @@ TEST_F(trace, non_audit_visible_denial_counting) } /* - * Verifies that landlock_add_rule_net emits a trace event with the correct port - * and allowed access mask fields. + * Verifies that landlock_add_rule_net_port emits a trace event with the correct + * port and allowed access mask fields. */ -TEST_F(trace, add_rule_net_fields) +TEST_F(trace, add_rule_net_port_fields) { struct landlock_ruleset_attr ruleset_attr = { .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP, @@ -931,9 +938,10 @@ TEST_F(trace, add_rule_net_fields) buf = tracefs_read_buf(); ASSERT_NE(NULL, buf); - EXPECT_EQ(1, tracefs_count_matches(buf, REGEX_ADD_RULE_NET(TRACE_TASK))) + EXPECT_EQ(1, tracefs_count_matches(buf, + REGEX_ADD_RULE_NET_PORT(TRACE_TASK))) { - TH_LOG("Expected 1 add_rule_net event\n%s", buf); + TH_LOG("Expected 1 add_rule_net_port event\n%s", buf); } /* @@ -941,7 +949,8 @@ TEST_F(trace, add_rule_net_fields) * (landlock_net_port_attr.port). On little-endian, htons(8080) is * 36895, so this comparison catches byte-order bugs. */ - EXPECT_EQ(0, tracefs_extract_field(buf, REGEX_ADD_RULE_NET(TRACE_TASK), + EXPECT_EQ(0, tracefs_extract_field(buf, + REGEX_ADD_RULE_NET_PORT(TRACE_TASK), "port", field, sizeof(field))); EXPECT_STREQ("8080", field); /* @@ -950,9 +959,9 @@ TEST_F(trace, add_rule_net_fields) * net access bits are unhandled because the ruleset only handles * BIND_TCP). */ - EXPECT_EQ(0, - tracefs_extract_field(buf, REGEX_ADD_RULE_NET(TRACE_TASK), - "access_rights", field, sizeof(field))); + EXPECT_EQ(0, tracefs_extract_field( + buf, REGEX_ADD_RULE_NET_PORT(TRACE_TASK), + "access_rights", field, sizeof(field))); EXPECT_STREQ("bind_tcp|connect_tcp|bind_udp|connect_send_udp", field); free(buf); diff --git a/tools/testing/selftests/mm/hugetlb-mmap.c b/tools/testing/selftests/mm/hugetlb-mmap.c index 0f2aad1b7dbd..2edbc992e6bc 100644 --- a/tools/testing/selftests/mm/hugetlb-mmap.c +++ b/tools/testing/selftests/mm/hugetlb-mmap.c @@ -122,7 +122,7 @@ int main(int argc, char **argv) hugepage_size = default_huge_page_size(); if (!hugepage_size) ksft_exit_skip("Could not detect default hugetlb page size."); - ksft_print_msg("Default size hugepages (%lu kB)\n", hugepage_size >> 10); + ksft_print_msg("Default size hugepages (%zu kB)\n", hugepage_size >> 10); } /* munmap will fail if the length is not page aligned */ @@ -131,7 +131,7 @@ int main(int argc, char **argv) hugetlb_set_nr_pages(hugepage_size, nr); if (hugetlb_free_pages(hugepage_size) < nr) - ksft_exit_skip("Not enough %lu Kb pages\n", hugepage_size >> 10); + ksft_exit_skip("Not enough %zu Kb pages\n", hugepage_size >> 10); ksft_set_plan(2); ksft_print_msg("Mapping %lu Mbytes\n", (unsigned long)length >> 20); diff --git a/tools/testing/selftests/move_mount_set_group/move_mount_set_group_test.c b/tools/testing/selftests/move_mount_set_group/move_mount_set_group_test.c index 12434415ec36..9c8fc8c7f62c 100644 --- a/tools/testing/selftests/move_mount_set_group/move_mount_set_group_test.c +++ b/tools/testing/selftests/move_mount_set_group/move_mount_set_group_test.c @@ -146,17 +146,19 @@ static void null_endofword(char *word) *word = '\0'; } -static bool is_shared_mount(const char *path) +/* Does the mount on @path carry the optional field @field in mountinfo? */ +static bool mount_has_field(const char *path, const char *field) { size_t len = 0; char *line = NULL; FILE *f = NULL; + bool found = false; f = fopen("/proc/self/mountinfo", "re"); if (!f) return false; - while (getline(&line, &len, f) != -1) { + while (!found && getline(&line, &len, f) != -1) { char *opts, *target; target = get_field(line, 4); @@ -172,15 +174,29 @@ static bool is_shared_mount(const char *path) if (strcmp(target, path) != 0) continue; - null_endofword(opts); - if (strstr(opts, "shared:")) - return true; + /* the optional fields end at the "-" separator */ + while (opts && *opts != '-') { + char *next = strchr(opts, ' '); + + if (next) + *next++ = '\0'; + if (!strncmp(opts, field, strlen(field))) { + found = true; + break; + } + opts = next; + } } free(line); fclose(f); - return false; + return found; +} + +static bool is_shared_mount(const char *path) +{ + return mount_has_field(path, "shared:"); } /* Attempt to de-conflict with the selftests tree. */ @@ -372,4 +388,50 @@ TEST_F(move_mount_set_group, complex_sharing_copying) ASSERT_EQ(is_shared_mount(SET_GROUP_A), 1); } +#define SET_GROUP_B "/tmp/B" +#define SET_GROUP_C "/tmp/C" + +/* + * An unbindable mount is neither shared nor a slave, so it must not be + * accepted as the target: with a slave source it would end up unbindable + * and a slave at the same time. + */ +TEST_F(move_mount_set_group, unbindable_target) +{ + bool ret; + + ret = move_mount_set_group_supported(); + ASSERT_GE(ret, 0); + if (!ret) + SKIP(return, "move_mount(MOVE_MOUNT_SET_GROUP) is not supported"); + + ASSERT_EQ(mount(NULL, SET_GROUP_A, NULL, MS_SHARED, 0), 0); + + /* B: a slave of A's peer group */ + ASSERT_EQ(mkdir(SET_GROUP_B, 0777), 0); + ASSERT_EQ(mount(SET_GROUP_A, SET_GROUP_B, NULL, MS_BIND, NULL), 0); + ASSERT_EQ(mount(NULL, SET_GROUP_B, NULL, MS_SLAVE, 0), 0); + ASSERT_TRUE(mount_has_field(SET_GROUP_B, "master:")); + + /* C: unbindable */ + ASSERT_EQ(mkdir(SET_GROUP_C, 0777), 0); + ASSERT_EQ(mount(SET_GROUP_A, SET_GROUP_C, NULL, MS_BIND, NULL), 0); + ASSERT_EQ(mount(NULL, SET_GROUP_C, NULL, MS_UNBINDABLE, 0), 0); + ASSERT_TRUE(mount_has_field(SET_GROUP_C, "unbindable")); + + /* from a slave */ + ASSERT_EQ(syscall(__NR_move_mount, AT_FDCWD, SET_GROUP_B, + AT_FDCWD, SET_GROUP_C, MOVE_MOUNT_SET_GROUP), -1); + ASSERT_EQ(errno, EINVAL); + ASSERT_FALSE(mount_has_field(SET_GROUP_C, "master:")); + ASSERT_TRUE(mount_has_field(SET_GROUP_C, "unbindable")); + + /* from a shared mount */ + ASSERT_EQ(syscall(__NR_move_mount, AT_FDCWD, SET_GROUP_A, + AT_FDCWD, SET_GROUP_C, MOVE_MOUNT_SET_GROUP), -1); + ASSERT_EQ(errno, EINVAL); + ASSERT_FALSE(mount_has_field(SET_GROUP_C, "shared:")); + ASSERT_TRUE(mount_has_field(SET_GROUP_C, "unbindable")); +} + TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/nci/nci_dev.c b/tools/testing/selftests/nci/nci_dev.c index 312f84ee0444..07427fa42888 100644 --- a/tools/testing/selftests/nci/nci_dev.c +++ b/tools/testing/selftests/nci/nci_dev.c @@ -8,6 +8,7 @@ #include <stdlib.h> #include <errno.h> +#include <stdint.h> #include <string.h> #include <sys/ioctl.h> #include <fcntl.h> @@ -87,6 +88,16 @@ struct msgtemplate { char buf[MAX_MSG_SIZE]; }; +static int join_thread_status(pthread_t thread) +{ + void *thread_ret = NULL; + + if (pthread_join(thread, &thread_ret)) + return -1; + + return (int)(intptr_t)thread_ret; +} + static int create_nl_socket(void) { int fd; @@ -182,7 +193,7 @@ static int get_family_id(int sd, __u32 pid, __u32 *event_group) } ans; struct nlattr *na; int resp_len; - __u16 id; + __u16 id = 0; int len; int rc; @@ -438,13 +449,13 @@ FIXTURE_SETUP(NCI) else rc = pthread_create(&thread_t, NULL, virtual_dev_open, (void *)&self->virtual_nci_fd); - ASSERT_GT(rc, -1); + ASSERT_EQ(rc, 0); rc = send_cmd_with_idx(self->sd, self->fid, self->pid, NFC_CMD_DEV_UP, self->dev_idex); EXPECT_EQ(rc, 0); - pthread_join(thread_t, (void **)&status); + status = join_thread_status(thread_t); ASSERT_EQ(status, 0); self->open_state = true; } @@ -509,12 +520,12 @@ FIXTURE_TEARDOWN(NCI) rc = pthread_create(&thread_t, NULL, virtual_deinit, (void *)&self->virtual_nci_fd); - ASSERT_GT(rc, -1); + ASSERT_EQ(rc, 0); rc = send_cmd_with_idx(self->sd, self->fid, self->pid, NFC_CMD_DEV_DOWN, self->dev_idex); EXPECT_EQ(rc, 0); - pthread_join(thread_t, (void **)&status); + status = join_thread_status(thread_t); ASSERT_EQ(status, 0); } @@ -585,12 +596,11 @@ int start_polling(int dev_idx, int proto, int virtual_fd, int sd, int fid, int p void *nla_start_poll_data[2] = {&dev_idx, &proto}; int nla_start_poll_len[2] = {4, 4}; pthread_t thread_t; - int status; int rc; rc = pthread_create(&thread_t, NULL, virtual_poll_start, (void *)&virtual_fd); - if (rc < 0) + if (rc) return rc; rc = send_cmd_mt_nla(sd, fid, pid, NFC_CMD_START_POLL, 2, nla_start_poll_type, @@ -598,19 +608,17 @@ int start_polling(int dev_idx, int proto, int virtual_fd, int sd, int fid, int p if (rc != 0) return rc; - pthread_join(thread_t, (void **)&status); - return status; + return join_thread_status(thread_t); } int stop_polling(int dev_idx, int virtual_fd, int sd, int fid, int pid) { pthread_t thread_t; - int status; int rc; rc = pthread_create(&thread_t, NULL, virtual_poll_stop, (void *)&virtual_fd); - if (rc < 0) + if (rc) return rc; rc = send_cmd_with_idx(sd, fid, pid, @@ -618,8 +626,7 @@ int stop_polling(int dev_idx, int virtual_fd, int sd, int fid, int pid) if (rc != 0) return rc; - pthread_join(thread_t, (void **)&status); - return status; + return join_thread_status(thread_t); } TEST_F(NCI, start_poll) @@ -830,10 +837,14 @@ int disconnect_tag(int nfc_sock, int virtual_fd) status = pthread_create(&thread_t, NULL, virtual_deactivate_proc, (void *)&virtual_fd); + if (status) + return status; close(nfc_sock); - pthread_join(thread_t, (void **)&status); - return status; + if (status) + return -1; + + return join_thread_status(thread_t); } TEST_F(NCI, t4t_tag_read) @@ -874,13 +885,13 @@ TEST_F(NCI, deinit) else rc = pthread_create(&thread_t, NULL, virtual_deinit, (void *)&self->virtual_nci_fd); - ASSERT_GT(rc, -1); + ASSERT_EQ(rc, 0); rc = send_cmd_with_idx(self->sd, self->fid, self->pid, NFC_CMD_DEV_DOWN, self->dev_idex); EXPECT_EQ(rc, 0); - pthread_join(thread_t, (void **)&status); + status = join_thread_status(thread_t); self->open_state = 0; ASSERT_EQ(status, 0); diff --git a/tools/testing/selftests/net/.gitignore b/tools/testing/selftests/net/.gitignore index c9f46031ac73..dacd36ed8455 100644 --- a/tools/testing/selftests/net/.gitignore +++ b/tools/testing/selftests/net/.gitignore @@ -55,6 +55,7 @@ tools tun txring_overwrite txtimestamp +udp_splice_checksum udpgso udpgso_bench_rx udpgso_bench_tx diff --git a/tools/testing/selftests/net/Makefile b/tools/testing/selftests/net/Makefile index 3ee3378f8b26..590b33e16d9f 100644 --- a/tools/testing/selftests/net/Makefile +++ b/tools/testing/selftests/net/Makefile @@ -199,6 +199,7 @@ TEST_GEN_PROGS := \ tap \ tcp_port_share \ tls \ + udp_splice_checksum \ # end of TEST_GEN_PROGS TEST_FILES := \ diff --git a/tools/testing/selftests/net/af_unix/scm_rights.c b/tools/testing/selftests/net/af_unix/scm_rights.c index d82a79c21c17..c165f250220a 100644 --- a/tools/testing/selftests/net/af_unix/scm_rights.c +++ b/tools/testing/selftests/net/af_unix/scm_rights.c @@ -378,4 +378,21 @@ TEST_F(scm_rights, backtrack_from_scc) close_sockets(10); } +TEST_F(scm_rights, mixed_lowpoint) +{ + create_sockets(6); + + send_fd(0, 1); + send_fd(1, 2); + send_fd(2, 1); + send_fd(1, 0); + + send_fd(3, 4); + send_fd(4, 5); + send_fd(5, 4); + send_fd(4, 3); + + close_sockets(6); +} + TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/net/amt.sh b/tools/testing/selftests/net/amt.sh index 663744305e52..d13b20cccba9 100755 --- a/tools/testing/selftests/net/amt.sh +++ b/tools/testing/selftests/net/amt.sh @@ -150,6 +150,13 @@ setup_interface() ip netns exec "${RELAY}" ip a a 10.0.0.2/24 dev relay_gw ip netns exec "${RELAY}" ip link add amtr type amt mode relay \ local 10.0.0.2 dev relay_gw relay_port 2268 max_tunnels 4 + # Count the IGMP and MLD queries that leave the relay through its own + # amt device; test_query_egress expects none. + ip netns exec "${RELAY}" tc qdisc add dev amtr clsact + ip netns exec "${RELAY}" tc filter add dev amtr egress pref 1 \ + protocol ip flower ip_proto 0x2 action pass + ip netns exec "${RELAY}" tc filter add dev amtr egress pref 2 \ + protocol ipv6 flower ip_proto icmpv6 type 130 action pass ip netns exec "${RELAY}" ip a a 172.17.0.1/24 dev relay_src ip netns exec "${RELAY}" ip a a 2001:db8:3::1/64 dev relay_src ip netns exec "${SOURCE}" ip a a 172.17.0.2/24 dev src_relay @@ -246,6 +253,27 @@ test_ipv6_forward() fi } +# The relay sends its General Queries straight from the receive path, in +# the same context that found the tunnel. A query queued on the amt device +# instead could outlive the tunnel it was built for. The forwarding tests +# above show that the gateway got its queries. +test_query_egress() +{ + local n4 n6 + + n4=$(ip netns exec "${RELAY}" tc -s -j filter show dev amtr egress \ + pref 1 | jq '[.[].options.actions[0].stats.packets // empty] | add // 0') + n6=$(ip netns exec "${RELAY}" tc -s -j filter show dev amtr egress \ + pref 2 | jq '[.[].options.actions[0].stats.packets // empty] | add // 0') + if [ "$n4" -eq 0 ] && [ "$n6" -eq 0 ]; then + printf "TEST: %-60s [ OK ]\n" "amt relay queries bypass the amt device" + else + printf "TEST: %-60s [FAIL]\n" "amt relay queries bypass the amt device" + echo "IGMP queries on amtr egress: $n4, MLD queries: $n6" >&2 + ERR=1 + fi +} + send_mcast4() { sleep 5 @@ -287,6 +315,7 @@ wait $pid || err=$? if [ $err -eq 1 ]; then ERR=1 fi +test_query_egress printf "TEST: %-50s" "IPv4 amt traffic forwarding torture" send_mcast_torture4 printf " [ OK ]\n" diff --git a/tools/testing/selftests/net/config b/tools/testing/selftests/net/config index 30d5fcb09a83..737e7e6327b3 100644 --- a/tools/testing/selftests/net/config +++ b/tools/testing/selftests/net/config @@ -118,10 +118,10 @@ CONFIG_NFT_NAT=m CONFIG_NUMA=y CONFIG_OPENVSWITCH=m CONFIG_PAGE_POOL_STATS=y -CONFIG_SYSCTL=y CONFIG_PSAMPLE=m CONFIG_RPS=y CONFIG_SYN_COOKIES=y +CONFIG_SYSCTL=y CONFIG_SYSFS=y CONFIG_TAP=m CONFIG_TCP_CONG_DCTCP=y diff --git a/tools/testing/selftests/net/nl_nlctrl.py b/tools/testing/selftests/net/nl_nlctrl.py index fe1f66dc9435..237b3d273260 100755 --- a/tools/testing/selftests/net/nl_nlctrl.py +++ b/tools/testing/selftests/net/nl_nlctrl.py @@ -9,40 +9,86 @@ from lib.py import ksft_run, ksft_exit from lib.py import ksft_eq, ksft_ge, ksft_true, ksft_in, ksft_not_in from lib.py import NetdevFamily, EthtoolFamily, NlctrlFamily +# Families we can expect to always be around, and which between them +# cover ops with a do, with a dump, and with both. +FAMILIES = ('nlctrl', 'netdev') -def getfamily_do(ctrl) -> None: - """Query a single family by name and validate its ops.""" - fam = ctrl.getfamily({'family-name': 'netdev'}) - ksft_eq(fam['family-name'], 'netdev') + +def _get_ops(ctrl, name): + """Get the ops of a family, keyed by command id.""" + fam = ctrl.getfamily({'family-name': name}) + ksft_eq(fam['family-name'], name) ksft_true(fam['family-id'] > 0) # The format of ops is quite odd, [{$idx: {"id"...}}, {$idx: {"id"...}}] # Discard the indices and re-key by command id. ops_by_id = {v['id']: v for op in fam['ops'] for v in op.values()} - ksft_eq(len(ops_by_id), len(fam['ops'])) + ksft_eq(len(ops_by_id), len(fam['ops']), + comment=f"{name} lists a command twice") + return ops_by_id + + +def _get_policy_map(ctrl, req): + """ + The policy map in the Netlink replies looks like this: + + [{'family-id': 16, 'op-policy': {'do': 0, 'dump': 0, 'op-id': 3}}, + {'family-id': 16, 'op-policy': {'dump': 1, 'op-id': 4}}, ...] + + Return the mapping: + + {3:{'do','dump'}, 4:{'dump'}} + + The policy itself is discarded here, only return which command has policy. + """ + pol_map = {} + for msg in ctrl.getpolicy(req, dump=True): + if 'op-policy' not in msg: + continue + modes = dict(msg['op-policy']) + cmd = modes.pop('op-id') + ksft_not_in(cmd, pol_map, comment=f"command {cmd} reported twice") + pol_map[cmd] = set(modes.keys()) + return pol_map + + +def getfamily_do(ctrl) -> None: + """Query single families by name and validate their ops.""" + ops = {name: _get_ops(ctrl, name) for name in FAMILIES} + + for name, ops_by_id in ops.items(): + for op in ops_by_id.values(): + # All ops in nlctrl and netdev have a policy + ksft_in('cmd-cap-haspol', op['flags'], + comment=f"{name} op {op['id']} missing haspol") + ksft_true(op['flags'] & {'cmd-cap-do', 'cmd-cap-dump'}, + comment=f"{name} op {op['id']} has no handler") - # All ops should have a policy (either do or dump has one) - for op in ops_by_id.values(): - ksft_in('cmd-cap-haspol', op['flags'], - comment=f"op {op['id']} missing haspol") + # nlctrl getfamily (id 3) does both, getpolicy (id 10) is dump-only + ksft_in('cmd-cap-do', ops['nlctrl'][3]['flags']) + ksft_in('cmd-cap-dump', ops['nlctrl'][3]['flags']) + ksft_not_in('cmd-cap-do', ops['nlctrl'][10]['flags']) + ksft_in('cmd-cap-dump', ops['nlctrl'][10]['flags']) + + netdev = ops['netdev'] # dev-get (id 1) should support both do and dump - ksft_in('cmd-cap-do', ops_by_id[1]['flags']) - ksft_in('cmd-cap-dump', ops_by_id[1]['flags']) + ksft_in('cmd-cap-do', netdev[1]['flags']) + ksft_in('cmd-cap-dump', netdev[1]['flags']) # qstats-get (id 12) is dump-only - ksft_not_in('cmd-cap-do', ops_by_id[12]['flags']) - ksft_in('cmd-cap-dump', ops_by_id[12]['flags']) + ksft_not_in('cmd-cap-do', netdev[12]['flags']) + ksft_in('cmd-cap-dump', netdev[12]['flags']) # napi-set (id 14) is do-only and requires admin - ksft_in('cmd-cap-do', ops_by_id[14]['flags']) - ksft_not_in('cmd-cap-dump', ops_by_id[14]['flags']) - ksft_in('admin-perm', ops_by_id[14]['flags']) + ksft_in('cmd-cap-do', netdev[14]['flags']) + ksft_not_in('cmd-cap-dump', netdev[14]['flags']) + ksft_in('admin-perm', netdev[14]['flags']) # Notification-only commands (dev-add/del/change-ntf etc.) must # not appear in the ops list since they have no do/dump handlers. for ntf_id in [2, 3, 4, 6, 7, 8]: - ksft_not_in(ntf_id, ops_by_id, + ksft_not_in(ntf_id, netdev, comment=f"ntf-only cmd {ntf_id} should not be in ops") @@ -103,6 +149,41 @@ def getpolicy_dump(_ctrl) -> None: comment="linkinfo-set should not have a dump policy") +def getpolicy_op_map(ctrl) -> None: + """Check the op-to-policy map consistency. Each op with 'haspol' flag + has to have a policy. The policy back-references must name only + real ops that exist, have given modes (do vs dump) and have 'haspol'. + """ + for name in FAMILIES: + ops_by_id = _get_ops(ctrl, name) + haspol = {cmd for cmd, op in ops_by_id.items() + if 'cmd-cap-haspol' in op['flags']} + + pol_map = _get_policy_map(ctrl, {'family-name': name}) + ksft_eq(set(pol_map), haspol, + comment=f"{name} policy map does not match the op list") + + # Walk the op list rather than the map, the map may be missing + # the very op we are after. Asking for a command the family does + # not have is an error, so it must not come from the map either. + for cmd in sorted(haspol): + modes = pol_map.get(cmd, set()) + + # The kernel only reports a mode the op actually has. + if 'do' in modes: + ksft_in('cmd-cap-do', ops_by_id[cmd]['flags'], + comment=f"{name} cmd {cmd} has no do") + if 'dump' in modes: + ksft_in('cmd-cap-dump', ops_by_id[cmd]['flags'], + comment=f"{name} cmd {cmd} has no dump") + + # Asking for one op builds the map in a different place in + # the kernel, it has to report what the full dump did. + single = _get_policy_map(ctrl, {'family-name': name, 'op': cmd}) + ksft_eq(single, {cmd: modes}, + comment=f"{name} cmd {cmd} policy differs from the dump") + + def getpolicy_by_op(_ctrl) -> None: """Query policy for specific ops, check attr names are resolved.""" ndev = NetdevFamily() @@ -122,6 +203,7 @@ def main() -> None: ksft_run([getfamily_do, getfamily_dump, getpolicy_dump, + getpolicy_op_map, getpolicy_by_op], args=(ctrl, )) ksft_exit() diff --git a/tools/testing/selftests/net/ovpn/common.sh b/tools/testing/selftests/net/ovpn/common.sh index 2d844eb3aa6e..5e9c81e885e6 100644 --- a/tools/testing/selftests/net/ovpn/common.sh +++ b/tools/testing/selftests/net/ovpn/common.sh @@ -136,6 +136,19 @@ ovpn_create_ns() { ip netns add "ovpn_peer${1}" } +ovpn_peer_vpn_addr() { + local peer="$1" + local file + + if [ "${OVPN_PROTO}" == "UDP" ]; then + file="${OVPN_UDP_PEERS_FILE}" + else + file="${OVPN_TCP_PEERS_FILE}" + fi + + awk -v peer="${peer}" '$1 == peer {print $NF; exit}' "${file}" +} + ovpn_setup_ns() { local peer="ovpn_peer${1}" local server_ns="ovpn_peer0" diff --git a/tools/testing/selftests/net/ovpn/ovpn-cli.c b/tools/testing/selftests/net/ovpn/ovpn-cli.c index f4effa7580c0..3b612a8a18fe 100644 --- a/tools/testing/selftests/net/ovpn/ovpn-cli.c +++ b/tools/testing/selftests/net/ovpn/ovpn-cli.c @@ -650,6 +650,26 @@ err: return ret; } +static int ovpn_nl_put_vpn_addr(struct nl_msg *msg, + const struct ovpn_ctx *ovpn) +{ + if (!ovpn->peer_ip_set) + return 0; + + switch (ovpn->peer_ip.in4.sin_family) { + case AF_INET: + return nla_put_u32(msg, OVPN_A_PEER_VPN_IPV4, + ovpn->peer_ip.in4.sin_addr.s_addr); + case AF_INET6: + return nla_put(msg, OVPN_A_PEER_VPN_IPV6, + sizeof(struct in6_addr), + &ovpn->peer_ip.in6.sin6_addr); + default: + fprintf(stderr, "Invalid family for peer address\n"); + return -EAFNOSUPPORT; + } +} + static int ovpn_new_peer(struct ovpn_ctx *ovpn, bool is_tcp) { struct nlattr *attr; @@ -691,22 +711,9 @@ static int ovpn_new_peer(struct ovpn_ctx *ovpn, bool is_tcp) } } - if (ovpn->peer_ip_set) { - switch (ovpn->peer_ip.in4.sin_family) { - case AF_INET: - NLA_PUT_U32(ctx->nl_msg, OVPN_A_PEER_VPN_IPV4, - ovpn->peer_ip.in4.sin_addr.s_addr); - break; - case AF_INET6: - NLA_PUT(ctx->nl_msg, OVPN_A_PEER_VPN_IPV6, - sizeof(struct in6_addr), - &ovpn->peer_ip.in6.sin6_addr); - break; - default: - fprintf(stderr, "Invalid family for peer address\n"); - goto nla_put_failure; - } - } + ret = ovpn_nl_put_vpn_addr(ctx->nl_msg, ovpn); + if (ret) + goto nla_put_failure; nla_nest_end(ctx->nl_msg, attr); @@ -732,6 +739,10 @@ static int ovpn_set_peer(struct ovpn_ctx *ovpn) ovpn->keepalive_interval); NLA_PUT_U32(ctx->nl_msg, OVPN_A_PEER_KEEPALIVE_TIMEOUT, ovpn->keepalive_timeout); + + ret = ovpn_nl_put_vpn_addr(ctx->nl_msg, ovpn); + if (ret) + goto nla_put_failure; nla_nest_end(ctx->nl_msg, attr); ret = ovpn_nl_msg_send(ctx, NULL); @@ -1730,13 +1741,14 @@ static void usage(const char *cmd) fprintf(stderr, "\tmark: socket FW mark value\n"); fprintf(stderr, - "* set_peer <iface> <peer_id> <keepalive_interval> <keepalive_timeout>: set peer attributes\n"); + "* set_peer <iface> <peer_id> <keepalive_interval> <keepalive_timeout> [vpnaddr]: set peer attributes\n"); fprintf(stderr, "\tiface: ovpn interface name\n"); fprintf(stderr, "\tpeer_id: peer ID of the peer to modify\n"); fprintf(stderr, "\tkeepalive_interval: interval for sending ping messages\n"); fprintf(stderr, "\tkeepalive_timeout: time after which a peer is timed out\n"); + fprintf(stderr, "\tvpnaddr: peer VPN IP\n"); fprintf(stderr, "* del_peer <iface> <peer_id>: delete peer\n"); fprintf(stderr, "\tiface: ovpn interface name\n"); @@ -2090,6 +2102,8 @@ static int ovpn_run_cmd(struct ovpn_ctx *ovpn) return ret; ret = ovpn_new_peer(ovpn, false); + if (ret < 0) + return ret; ovpn_waitbg(); break; case CMD_NEW_MULTI_PEER: @@ -2331,6 +2345,12 @@ static int ovpn_parse_cmd_args(struct ovpn_ctx *ovpn, int argc, char *argv[]) "keepalive interval value out of range\n"); return -1; } + + if (argc > 6) { + ret = ovpn_parse_remote(ovpn, NULL, NULL, argv[6]); + if (ret < 0) + return -1; + } break; case CMD_DEL_PEER: if (argc < 4) diff --git a/tools/testing/selftests/net/ovpn/test.sh b/tools/testing/selftests/net/ovpn/test.sh index 9b5610837032..392109d5e14e 100755 --- a/tools/testing/selftests/net/ovpn/test.sh +++ b/tools/testing/selftests/net/ovpn/test.sh @@ -56,6 +56,76 @@ ovpn_prepare_network() { done } +ovpn_new_test_peer() { + local peer_id="$1" + + shift + ip netns exec ovpn_peer0 "${OVPN_CLI}" new_peer tun0 \ + "${peer_id}" none 65000 10.10.1.2 1 "$@" +} + +ovpn_set_peer_vpn_addr() { + ip netns exec ovpn_peer0 "${OVPN_CLI}" set_peer tun0 \ + "$1" 60 120 "$2" +} + +ovpn_run_vpn_addr_validation() { + local addr + local peer1_addr4 + local test_peer_id=$((OVPN_NUM_PEERS + 1)) + local test_peer_addr6="2001:db8::2" + # Do not include 0.0.0.0 or :: here. They are invalid on creation, but + # clear one address family on update and are valid if the other remains. + local -a invalid_addrs=( + "127.0.0.1" + "224.0.0.1" + "255.255.255.255" + "::1" + "::192.0.2.1" + "::ffff:192.0.2.1" + "ff02::1" + ) + + peer1_addr4=$(ovpn_peer_vpn_addr 1) + + ovpn_cmd_fail "reject peer without VPN address" \ + ovpn_new_test_peer "${test_peer_id}" + + for addr in "0.0.0.0" "::" "${invalid_addrs[@]}"; do + ovpn_cmd_fail "reject new peer VPN address ${addr}" \ + ovpn_new_test_peer "${test_peer_id}" "${addr}" + done + + ovpn_cmd_fail "reject duplicate IPv4 address on peer creation" \ + ovpn_new_test_peer "${test_peer_id}" "${peer1_addr4}" + ovpn_cmd_fail "reject clearing the last peer VPN address" \ + ovpn_set_peer_vpn_addr 1 0.0.0.0 + + for addr in "${invalid_addrs[@]}"; do + ovpn_cmd_fail "reject updated peer VPN address ${addr}" \ + ovpn_set_peer_vpn_addr 1 "${addr}" + done + + ovpn_cmd_fail "reject duplicate IPv4 address on peer update" \ + ovpn_set_peer_vpn_addr 2 "${peer1_addr4}" + + ovpn_cmd_ok "add peer IPv6 address" \ + ovpn_set_peer_vpn_addr 1 "${test_peer_addr6}" + ovpn_cmd_fail "reject duplicate IPv6 address on peer creation" \ + ovpn_new_test_peer "${test_peer_id}" "${test_peer_addr6}" + ovpn_cmd_fail "reject duplicate IPv6 address on peer update" \ + ovpn_set_peer_vpn_addr 2 "${test_peer_addr6}" + + ovpn_cmd_ok "clear peer IPv4 address" \ + ovpn_set_peer_vpn_addr 1 0.0.0.0 + ovpn_cmd_fail "reject clearing the remaining peer IPv6 address" \ + ovpn_set_peer_vpn_addr 1 :: + ovpn_cmd_ok "restore peer IPv4 address" \ + ovpn_set_peer_vpn_addr 1 "${peer1_addr4}" + ovpn_cmd_ok "clear peer IPv6 address" \ + ovpn_set_peer_vpn_addr 1 :: +} + ovpn_run_basic_traffic() { local p local header1 @@ -293,15 +363,16 @@ trap ovpn_stage_err ERR ktap_print_header if [ "${OVPN_FLOAT}" == "1" ]; then - ktap_set_plan 13 + ktap_set_plan 14 else - ktap_set_plan 12 + ktap_set_plan 13 fi ovpn_cleanup modprobe -q ovpn || true ovpn_run_stage "setup network topology" ovpn_prepare_network +ovpn_run_stage "validate peer VPN addresses" ovpn_run_vpn_addr_validation ovpn_run_stage "run baseline data traffic" ovpn_run_basic_traffic ovpn_run_stage "run LAN traffic behind peer1" ovpn_run_lan_traffic [ "${OVPN_FLOAT}" == "1" ] && ovpn_run_stage "run floating peer checks" \ diff --git a/tools/testing/selftests/net/packetdrill/config b/tools/testing/selftests/net/packetdrill/config index 83dde525c53c..b7df8bf30920 100644 --- a/tools/testing/selftests/net/packetdrill/config +++ b/tools/testing/selftests/net/packetdrill/config @@ -4,8 +4,8 @@ CONFIG_IPV6=y CONFIG_NET_NS=y CONFIG_NET_SCH_FIFO=y CONFIG_NET_SCH_FQ=y -CONFIG_SYSCTL=y CONFIG_SYN_COOKIES=y +CONFIG_SYSCTL=y CONFIG_TCP_CONG_CUBIC=y CONFIG_TCP_MD5SIG=y CONFIG_TUN=y diff --git a/tools/testing/selftests/net/packetdrill/tcp_old_ack_ts.pkt b/tools/testing/selftests/net/packetdrill/tcp_old_ack_ts.pkt new file mode 100644 index 000000000000..f072e5e4c449 --- /dev/null +++ b/tools/testing/selftests/net/packetdrill/tcp_old_ack_ts.pkt @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: GPL-2.0 +// An out-of-order segment advances SND.UNA. The gap filler still carries +// its original, older ACK. The reply must echo the gap filler's timestamp. +--tcp_ts_tick_usecs=1000 + +`./defaults.sh` + +0.000 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3 ++0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0 ++0 bind(3, ..., ...) = 0 ++0 listen(3, 1) = 0 +0.100 < S 0:0(0) win 20000 <mss 1000,sackOK,TS val 900 ecr 0> ++0 > S. 0:0(0) ack 1 <mss 1460,sackOK,TS val 100 ecr 900> +0.200 < . 1:1(0) ack 1 win 20000 <nop,nop,TS val 1000 ecr 100> ++0 accept(3, ..., ...) = 4 +0.300 write(4, ..., 17) = 17 ++0 > P. 1:18(17) ack 1 <nop,nop,TS val 300 ecr 1000> +0.310 < P. 18:35(17) ack 18 win 20000 <nop,nop,TS val 301001 ecr 300> ++0 > . 18:18(0) ack 1 <nop,nop,TS val 310 ecr 1000,nop,nop,sack 18:35> +0.320 < P. 1:18(17) ack 1 win 20000 <nop,nop,TS val 301000 ecr 100> ++0 > . 18:18(0) ack 35 <nop,nop,TS val 320 ecr 301000> ++0 read(4, ..., 34) = 34 diff --git a/tools/testing/selftests/net/packetdrill/tcp_rfc5961_reject-old-ack.pkt b/tools/testing/selftests/net/packetdrill/tcp_rfc5961_reject-old-ack.pkt new file mode 100644 index 000000000000..32dd9de1d366 --- /dev/null +++ b/tools/testing/selftests/net/packetdrill/tcp_rfc5961_reject-old-ack.pkt @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-2.0 + +`./defaults.sh +sysctl -q net.ipv4.tcp_invalid_ratelimit=0 +` + +// Test rejection of data segments carrying excessively old ACKs + +0 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3 ++0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0 ++0 bind(3, ..., ...) = 0 ++0 listen(3, 1024) = 0 + +// ---------------- Handshake ------------------- // ++0 < S 0:0(0) win 65535 ++0 > S. 0:0(0) ack 1 <...> ++0 < . 1:1(0) ack 1 win 65535 ++0 accept(3, ..., ...) = 4 + +// Populate receive memory so the following segment can use +// header prediction. ++0 < P. 1:501(500) ack 1 win 65535 ++0 > . 1:1(0) ack 501 + +// Send an in-sequence data segment carrying an excessively old ACK. ++0 < P. 501:1501(1000) ack 2794967397 win 65535 + +// Challenge ACK; RCV.NXT must remain 501. ++0 > . 1:1(0) ack 501 diff --git a/tools/testing/selftests/net/tun.c b/tools/testing/selftests/net/tun.c index abe488bac50b..afaa81c9bac5 100644 --- a/tools/testing/selftests/net/tun.c +++ b/tools/testing/selftests/net/tun.c @@ -9,6 +9,7 @@ #include <string.h> #include <unistd.h> #include <linux/if_tun.h> +#include <netinet/tcp.h> #include <sys/ioctl.h> #include <sys/socket.h> @@ -542,6 +543,141 @@ TEST_F(tun, reattach_close_delete) EXPECT_EQ(tun_delete(self->ifname), 0); } +FIXTURE(tun_vnet_gso) +{ + char ifname[IFNAMSIZ]; + int fd; +}; + +FIXTURE_SETUP(tun_vnet_gso) +{ + int flags = IFF_TAP | IFF_NO_PI | IFF_VNET_HDR; + + memset(self->ifname, 0, sizeof(self->ifname)); + self->fd = tun_open(self->ifname, flags, 0, 0, NULL); + ASSERT_GE(self->fd, 0); +} + +FIXTURE_TEARDOWN(tun_vnet_gso) +{ + if (self->fd >= 0) + close(self->fd); +} + +static int build_vlan_tcpv4_gso_packet(uint8_t *buf, int payload_len) +{ + uint16_t vlan_tag[2] = { htons(100), htons(ETH_P_IP) }; + uint8_t *cur = buf + sizeof(struct virtio_net_hdr); + struct virtio_net_hdr vh = { 0 }; + struct tcphdr tcph = { 0 }; + uint32_t sum; + + cur += build_eth(cur, ETH_P_8021Q, param_hwaddr_outer_src, + param_hwaddr_outer_dst); + + /* 802.1Q tag: VID=100, inner protocol=ETH_P_IP */ + memcpy(cur, vlan_tag, sizeof(vlan_tag)); + cur += sizeof(vlan_tag); + + cur += build_ipv4_header(cur, IPPROTO_TCP, + sizeof(tcph) + payload_len, + ¶m_ipaddr4_outer_src, + ¶m_ipaddr4_outer_dst); + + tcph.source = htons(12345); + tcph.dest = htons(80); + tcph.seq = htonl(1); + tcph.doff = sizeof(tcph) / 4; + tcph.ack = 1; + tcph.window = htons(65535); + memcpy(cur, &tcph, sizeof(tcph)); + memset(cur + sizeof(tcph), PKT_DATA, payload_len); + + sum = add_csum((const uint8_t *)¶m_ipaddr4_outer_src, + sizeof(param_ipaddr4_outer_src)); + sum += add_csum((const uint8_t *)¶m_ipaddr4_outer_dst, + sizeof(param_ipaddr4_outer_dst)); + sum += htons(IPPROTO_TCP) + htons(sizeof(tcph) + payload_len); + sum += add_csum(cur, sizeof(tcph) + payload_len); + tcph.check = finish_ip_csum(sum); + memcpy(cur, &tcph, sizeof(tcph)); + cur += sizeof(tcph) + payload_len; + + vh.gso_type = VIRTIO_NET_HDR_GSO_TCPV4; + vh.gso_size = 1400; + vh.hdr_len = (cur - buf) - sizeof(vh) - payload_len; + memcpy(buf, &vh, sizeof(vh)); + + return cur - buf; +} + +TEST_F(tun_vnet_gso, vlan_tcpv4_gso_no_csum) +{ + struct virtio_net_hdr vh; + uint8_t pkt[4096] = { 0 }; + int len, ret; + + len = build_vlan_tcpv4_gso_packet(pkt, 2800); + memcpy(&vh, pkt, sizeof(vh)); + + /* Valid VLAN-tagged TCPv4 GSO with flags = 0 (no NEEDS_CSUM) */ + vh.flags = 0; + memcpy(pkt, &vh, sizeof(vh)); + ret = write(self->fd, pkt, len); + ASSERT_EQ(ret, len); + + /* Valid VLAN-tagged TCPv4 GSO with flags = DATA_VALID */ + vh.flags = VIRTIO_NET_HDR_F_DATA_VALID; + memcpy(pkt, &vh, sizeof(vh)); + ret = write(self->fd, pkt, len); + ASSERT_EQ(ret, len); + + /* Mismatched GSO type (TCPV6 on VLAN-tagged IPv4 packet) */ + vh.flags = 0; + vh.gso_type = VIRTIO_NET_HDR_GSO_TCPV6; + memcpy(pkt, &vh, sizeof(vh)); + ret = write(self->fd, pkt, len); + ASSERT_EQ(ret, -1); + ASSERT_EQ(errno, EINVAL); + + /* TCP header truncated after 10 bytes, without NEEDS_CSUM. + * Requires the transport header offset found by flow dissection: + * pulling only sizeof(struct iphdr) + sizeof(struct tcphdr) bytes + * from the mac header would accept this frame. + */ + vh.gso_type = VIRTIO_NET_HDR_GSO_TCPV4; + vh.hdr_len = ETH_HLEN + 4 + sizeof(struct iphdr) + 10; + memcpy(pkt, &vh, sizeof(vh)); + ret = write(self->fd, pkt, sizeof(vh) + vh.hdr_len); + ASSERT_EQ(ret, -1); + ASSERT_EQ(errno, EINVAL); +} + +TEST_F(tun_vnet_gso, vlan_tcpv4_gso_no_csum_64k) +{ + /* Ethernet frame of 65540 bytes: skb->len is above U16_MAX when the + * frame is flow-dissected, before eth_type_trans() pulls ETH_HLEN. + */ + const int payload_len = 65540 - ETH_HLEN - 4 - + sizeof(struct iphdr) - sizeof(struct tcphdr); + struct virtio_net_hdr vh; + uint8_t *pkt; + int len, ret; + + pkt = calloc(1, sizeof(vh) + 65540); + ASSERT_NE(pkt, NULL); + + len = build_vlan_tcpv4_gso_packet(pkt, payload_len); + ASSERT_EQ(len, sizeof(vh) + 65540); + memcpy(&vh, pkt, sizeof(vh)); + + vh.flags = 0; + memcpy(pkt, &vh, sizeof(vh)); + ret = write(self->fd, pkt, len); + free(pkt); + ASSERT_EQ(ret, len); +} + FIXTURE(tun_vnet_udptnl) { char ifname[IFNAMSIZ]; diff --git a/tools/testing/selftests/net/udp_splice_checksum.c b/tools/testing/selftests/net/udp_splice_checksum.c new file mode 100644 index 000000000000..dc61444e6481 --- /dev/null +++ b/tools/testing/selftests/net/udp_splice_checksum.c @@ -0,0 +1,378 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Exercise UDP software checksums over multiple spliced pipe fragments. + * All traffic stays on loopback; no privileges or external services are needed. + * Exit status: 0 = pass, 1 = failure, 4 = all cases skipped. + */ +#define _GNU_SOURCE + +#include <arpa/inet.h> +#include <errno.h> +#include <fcntl.h> +#include <poll.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/socket.h> +#include <time.h> +#include <unistd.h> + +#include "kselftest.h" + +#define TIMEOUT_MS 200 + +struct test_case { + const char *name; + const char *body; + const char *prefix; + size_t fragments[3]; + bool ordinary; + bool page_tail; +}; + +static const struct test_case cases[] = { + { "ordinary-control", "abcDEFGH", "", { 3, 5 }, true, false }, + { "uniform-odd-fragments", "BBBBBBBB", "", { 3, 5 }, false, false }, + { "even-fragments", "abcdEFGH", "", { 4, 4 }, false, false }, + { "odd-fragments", "abcDEFGH", "", { 3, 5 }, false, false }, + { "odd-prefix-odd-fragments", "abcDEFGH", "P", { 3, 5 }, false, false }, + { "even-prefix-odd-fragments", "abcDEFGH", "PQ", { 3, 5 }, false, false }, + { "page-tail-odd-fragments", "abcDEFGH", "", { 3, 5 }, false, true }, + { "three-fragments", "abcDEFGhijkl", "", { 3, 4, 5 }, false, false }, + { "odd-prefix-three-fragments", + "abcDEFGhijkl", + "P", + { 3, 4, 5 }, + false, + false }, +}; + +static const char *const families[] = { "IPv4", "IPv6", "IPv4-mapped" }; + +static int error(const char *what) +{ + ksft_perror(what); + return 1; +} + +static bool unavailable(void) +{ + return errno == EAFNOSUPPORT || errno == EPROTONOSUPPORT || + errno == EADDRNOTAVAIL; +} + +static long long now_ms(void) +{ + struct timespec ts; + + if (clock_gettime(CLOCK_MONOTONIC, &ts)) + ksft_exit_fail_perror("clock_gettime"); + return (long long)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; +} + +static int receive_one(int fd, const char *expected, size_t length) +{ + struct pollfd pfd = { .fd = fd, .events = POLLIN }; + long long deadline = now_ms() + TIMEOUT_MS; + int remaining, ret; + char received[64]; + ssize_t count; + + for (;;) { + remaining = deadline - now_ms(); + if (remaining < 0) + remaining = 0; + ret = poll(&pfd, 1, remaining); + if (ret < 0) { + if (errno == EINTR) + continue; + return error("poll"); + } + if (!ret) { + ksft_print_msg("no datagram within %d ms after send\n", + TIMEOUT_MS); + return 1; + } + count = recv(fd, received, sizeof(received), + MSG_DONTWAIT | MSG_TRUNC); + if (count < 0) { + if (errno == EINTR || errno == EAGAIN) + continue; + return error("recv"); + } + break; + } + if (count != (ssize_t)length || memcmp(received, expected, length)) { + ksft_print_msg("received %zd bytes; expected %zu matching\n", + count, length); + return 1; + } + count = recv(fd, received, sizeof(received), MSG_DONTWAIT | MSG_TRUNC); + if (count >= 0) { + ksft_print_msg("unexpected extra datagram (%zd bytes)\n", + count); + return 1; + } + if (errno != EAGAIN) + return error("extra-datagram check"); + return 0; +} + +/* Separate source pipes prevent adjacent small writes merging into one buffer. */ +static int append_fragment(int dest, const char *data, size_t length) +{ + int source[2], ret = 1; + + if (pipe2(source, O_NONBLOCK | O_CLOEXEC)) + return error("source pipe"); + if (write(source[1], data, length) != (ssize_t)length) { + error("source write"); + goto out; + } + if (splice(source[0], NULL, dest, NULL, length, SPLICE_F_NONBLOCK) != + (ssize_t)length) { + error("pipe-to-pipe splice"); + goto out; + } + ret = 0; +out: + close(source[0]); + close(source[1]); + return ret; +} + +static int pipe_capacity(int fd, unsigned int slots, const char **skip_reason) +{ + long required = slots * sysconf(_SC_PAGESIZE); + int capacity = fcntl(fd, F_GETPIPE_SZ); + + if (capacity < 0) + return error("query pipe capacity"); + if (capacity >= required || + fcntl(fd, F_SETPIPE_SZ, required) >= required) + return 0; + ksft_print_msg("cannot reserve %u pipe slots: %s\n", slots, + strerror(errno)); + *skip_reason = "required pipe capacity unavailable"; + return KSFT_SKIP; +} + +/* Leave three bytes at a page's end, followed by five bytes on the next page. */ +static int append_page_tail(int dest, const char *body, + const char **skip_reason) +{ + long page = sysconf(_SC_PAGESIZE); + int source[2], ret = 1; + char *data; + + if (page < 8) { + ksft_print_msg("invalid page size %ld\n", page); + return 1; + } + data = malloc(page + 5); + if (!data) + return error("malloc"); + memset(data, 'x', page - 3); + memcpy(data + page - 3, body, 8); + if (pipe2(source, O_NONBLOCK | O_CLOEXEC)) { + error("page-tail source pipe"); + goto free_data; + } + ret = pipe_capacity(source[1], 2, skip_reason); + if (ret) + goto out; + ret = 1; + if (write(source[1], data, page + 5) != page + 5) { + error("page-tail write"); + goto out; + } + if (read(source[0], data, page - 3) != page - 3) { + error("page-tail discard"); + goto out; + } + if (splice(source[0], NULL, dest, NULL, 8, SPLICE_F_NONBLOCK) != 8) { + error("page-tail pipe-to-pipe splice"); + goto out; + } + ret = 0; +out: + close(source[0]); + close(source[1]); +free_data: + free(data); + return ret; +} + +static int run_case(unsigned int family, bool connected, + const struct test_case *test, const char **skip_reason) +{ + size_t body_length = strlen(test->body), offset = 0; + int tx_family = family == 0 ? AF_INET : AF_INET6; + int rx_family = family == 1 ? AF_INET6 : AF_INET; + int rx = -1, tx = -1, pipefd[2] = { -1, -1 }; + size_t prefix_length = strlen(test->prefix); + struct sockaddr_storage address = { 0 }; + socklen_t address_length; + struct sockaddr_in6 *v6; + struct sockaddr_in *v4; + int ret = 1, zero = 0; + char expected[64]; + unsigned int i; + ssize_t count; + + v4 = (struct sockaddr_in *)&address; + v6 = (struct sockaddr_in6 *)&address; + *skip_reason = "address family unavailable"; + memcpy(expected, test->prefix, prefix_length); + memcpy(expected + prefix_length, test->body, body_length); + rx = socket(rx_family, SOCK_DGRAM | SOCK_CLOEXEC, 0); + if (rx < 0) { + ret = unavailable() ? KSFT_SKIP : error("receive socket"); + goto out; + } + if (rx_family == AF_INET) { + v4->sin_family = AF_INET; + v4->sin_addr.s_addr = htonl(INADDR_LOOPBACK); + address_length = sizeof(*v4); + } else { + v6->sin6_family = AF_INET6; + v6->sin6_addr = in6addr_loopback; + address_length = sizeof(*v6); + } + if (bind(rx, (struct sockaddr *)&address, address_length)) { + ret = unavailable() ? KSFT_SKIP : error("bind loopback"); + goto out; + } + if (getsockname(rx, (struct sockaddr *)&address, &address_length)) { + error("getsockname"); + goto out; + } + if (family == 2) { + unsigned short port = v4->sin_port; + + memset(&address, 0, sizeof(address)); + v6->sin6_family = AF_INET6; + v6->sin6_port = port; + inet_pton(AF_INET6, "::ffff:127.0.0.1", &v6->sin6_addr); + address_length = sizeof(*v6); + } + tx = socket(tx_family, SOCK_DGRAM | SOCK_CLOEXEC, 0); + if (tx < 0) { + ret = unavailable() ? KSFT_SKIP : error("send socket"); + goto out; + } + if (family == 2 && + setsockopt(tx, IPPROTO_IPV6, IPV6_V6ONLY, &zero, sizeof(zero))) { + error("disable IPV6_V6ONLY"); + goto out; + } + if (connected && + connect(tx, (struct sockaddr *)&address, address_length)) { + error("connect"); + goto out; + } + if (test->ordinary) { + count = sendto(tx, expected, prefix_length + body_length, 0, + (struct sockaddr *)&address, address_length); + if (count != (ssize_t)(prefix_length + body_length)) { + error("ordinary sendto"); + goto out; + } + } else { + if (pipe2(pipefd, O_NONBLOCK | O_CLOEXEC)) { + error("assembled pipe"); + goto out; + } + ret = pipe_capacity(pipefd[1], test->fragments[2] ? 3 : 2, + skip_reason); + if (ret) + goto out; + ret = 1; + if (test->page_tail) { + ret = append_page_tail(pipefd[1], test->body, + skip_reason); + if (ret) + goto out; + ret = 1; + } else { + for (i = 0; i < ARRAY_SIZE(test->fragments); i++) { + if (!test->fragments[i]) + break; + if (append_fragment(pipefd[1], + test->body + offset, + test->fragments[i])) + goto out; + offset += test->fragments[i]; + } + } + /* Even an empty prefix establishes the destination and UDP cork. */ + count = sendto(tx, test->prefix, prefix_length, MSG_MORE, + (struct sockaddr *)&address, address_length); + if (count != (ssize_t)prefix_length) { + error("prime sendto(MSG_MORE)"); + goto out; + } + count = splice(pipefd[0], NULL, tx, NULL, body_length, + SPLICE_F_NONBLOCK | SPLICE_F_MORE); + if (count != (ssize_t)body_length) { + ksft_print_msg("socket splice: %zd, expected %zu: %s\n", + count, body_length, + count < 0 ? strerror(errno) : + "short transfer"); + goto out; + } + if (sendto(tx, "", 0, 0, (struct sockaddr *)&address, + address_length)) { + error("commit sendto"); + goto out; + } + } + ret = receive_one(rx, expected, prefix_length + body_length); +out: + if (pipefd[0] >= 0) { + close(pipefd[0]); + close(pipefd[1]); + } + if (tx >= 0) + close(tx); + if (rx >= 0) + close(rx); + return ret; +} + +int main(void) +{ + unsigned int family, connected, i; + const char *connection, *skip; + int ret; + + ksft_print_header(); + ksft_set_plan(ARRAY_SIZE(families) * 2 * ARRAY_SIZE(cases)); + for (family = 0; family < ARRAY_SIZE(families); family++) { + for (connected = 0; connected < 2; connected++) { + connection = connected ? "connected" : "unconnected"; + for (i = 0; i < ARRAY_SIZE(cases); i++) { + ret = run_case(family, connected, &cases[i], + &skip); + if (ret == KSFT_SKIP) + ksft_test_result_skip("%s %s %s: %s\n", + families[family], + connection, + cases[i].name, + skip); + else + ksft_test_result(!ret, "%s %s %s\n", + families[family], + connection, + cases[i].name); + } + } + } + if (!ksft_get_pass_cnt() && !ksft_get_fail_cnt()) { + /* ksft_exit_skip() would add a result beyond the plan. */ + ksft_print_cnts(); + return KSFT_SKIP; + } + ksft_finished(); +} diff --git a/tools/testing/selftests/net/veth.sh b/tools/testing/selftests/net/veth.sh index 9709dd067c72..8b867e0675a7 100755 --- a/tools/testing/selftests/net/veth.sh +++ b/tools/testing/selftests/net/veth.sh @@ -9,6 +9,8 @@ readonly DST=1 readonly DST_NAT=100 readonly NS_SRC=$BASE$SRC readonly NS_DST=$BASE$DST +# shellcheck disable=SC2155 # prefer RO variable over return value from cmd +readonly YNL="$(dirname "$(readlink -f "$0")")/../../../net/ynl/pyynl/cli.py" # "baremetal" network used for raw UDP traffic readonly BM_NET_V4=192.168.1. @@ -74,6 +76,37 @@ chk_tso_flag() { __chk_flag "$1" $2 $3 tcp-segmentation-offload } +chk_xdp_feature() { + local msg="$1" + local target=$2 + local expected=$3 + local feature=$4 + local ifindex + local out + local st + local flag + + ifindex=`ip netns exec $BASE$target cat /sys/class/net/veth$target/ifindex` + out=`ip netns exec $BASE$target "$YNL" --family netdev --do dev-get \ + --output-json --json "{\"ifindex\": $ifindex}" 2>&1` + st=$? + + printf "%-60s" "$msg" + if [ $st -ne 0 ]; then + echo " fail - ynl cli error: $out" + ret=1 + return + fi + + flag=`echo "$out" | grep -c "\"$feature\""` + if [ "$flag" = "$expected" ]; then + echo " ok " + else + echo " fail - expected $expected found $flag" + ret=1 + fi +} + chk_channels() { local msg="$1" local target=$2 @@ -222,6 +255,7 @@ fi [ $CPUS -lt 2 ] && echo "Only one CPU available, some tests will be skipped" [ $STRESS -gt 0 -a $CPUS -lt 3 ] && echo " stress test will be skipped, too" +[ ! -f "$YNL" ] && echo "ynl cli not found, ndo-xmit tests will be skipped" create_ns chk_gro_flag "default - gro flag" $SRC off @@ -283,6 +317,18 @@ chk_gro_flag " - peer gro flag" $SRC off chk_tso_flag " - tso flag" $SRC on chk_tso_flag " - peer tso flag" $DST on ip -n $NS_DST link set dev veth$DST up +if [ -f "$YNL" ]; then + chk_xdp_feature " - peer ndo-xmit" $SRC 1 ndo-xmit + # make sure the peer flag is set before disabling gro while down + ip netns exec $NS_DST ethtool -K veth$DST gro off + ip netns exec $NS_DST ethtool -K veth$DST gro on + chk_xdp_feature " - peer ndo-xmit re-armed" $SRC 1 ndo-xmit + ip -n $NS_DST link set dev veth$DST down + ip netns exec $NS_DST ethtool -K veth$DST gro off + ip -n $NS_DST link set dev veth$DST up + chk_xdp_feature " - peer ndo-xmit cleared" $SRC 0 ndo-xmit + ip netns exec $NS_DST ethtool -K veth$DST gro on +fi ip netns exec $NS_SRC ethtool -K veth$SRC tx-udp-segmentation off ip netns exec $NS_DST ethtool -K veth$DST rx-udp-gro-forwarding on chk_gro " - aggregation with TSO off" 1 diff --git a/tools/testing/selftests/sched_ext/Makefile b/tools/testing/selftests/sched_ext/Makefile index 3cfe90e0f34f..af919a56a4f4 100644 --- a/tools/testing/selftests/sched_ext/Makefile +++ b/tools/testing/selftests/sched_ext/Makefile @@ -164,10 +164,13 @@ all_test_bpfprogs := $(foreach prog,$(wildcard *.bpf.c),$(INCLUDE_DIR)/$(patsubs auto-test-targets := \ create_dsq \ dequeue \ + dequeue_iter \ + dequeue_remote \ enq_last_no_enq_fails \ ddsp_bogus_dsq_fail \ ddsp_vtimelocal_fail \ dsp_local_on \ + enable_cmask \ enq_select_cpu \ exit \ hotplug \ diff --git a/tools/testing/selftests/sched_ext/dequeue_iter.bpf.c b/tools/testing/selftests/sched_ext/dequeue_iter.bpf.c new file mode 100644 index 000000000000..76c2c71a90b6 --- /dev/null +++ b/tools/testing/selftests/sched_ext/dequeue_iter.bpf.c @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * ops.dequeue() of this scheduler iterates the user DSQ it consumes + * tasks from with bpf_iter_scx_dsq, which takes the DSQ lock. + * On a kernel that still runs ops.dequeue() with that lock held, the + * iteration self-deadlocks the CPU - this test wedges the system on + * unfixed kernels instead of failing cleanly. + * + * Copyright (c) 2026 fangqiurong <fangqiurong@kylinos.cn> + */ + +#include <scx/common.bpf.h> + +char _license[] SEC("license") = "GPL"; + +UEI_DEFINE(uei); + +#define TEST_DSQ_ID 1000 + +u64 dq_count; + +s32 BPF_STRUCT_OPS_SLEEPABLE(dequeue_iter_init) +{ + return scx_bpf_create_dsq(TEST_DSQ_ID, -1); +} + +s32 BPF_STRUCT_OPS(dequeue_iter_select_cpu, struct task_struct *p, + s32 prev_cpu, u64 wake_flags) +{ + return prev_cpu; +} + +void BPF_STRUCT_OPS(dequeue_iter_enqueue, struct task_struct *p, u64 enq_flags) +{ + scx_bpf_dsq_insert(p, TEST_DSQ_ID, SCX_SLICE_DFL, enq_flags); +} + +void BPF_STRUCT_OPS(dequeue_iter_dispatch, s32 cpu, struct task_struct *task) +{ + scx_bpf_dsq_move_to_local(TEST_DSQ_ID, 0); +} + +void BPF_STRUCT_OPS(dequeue_iter_dequeue, struct task_struct *p, u64 deq_flags) +{ + struct bpf_iter_scx_dsq it; + struct task_struct *t; + + if (!bpf_iter_scx_dsq_new(&it, TEST_DSQ_ID, 0)) { + while ((t = bpf_iter_scx_dsq_next(&it))) + ; + } + bpf_iter_scx_dsq_destroy(&it); + + __sync_fetch_and_add(&dq_count, 1); +} + +void BPF_STRUCT_OPS(dequeue_iter_exit, struct scx_exit_info *ei) +{ + UEI_RECORD(uei, ei); + scx_bpf_destroy_dsq(TEST_DSQ_ID); +} + +SEC(".struct_ops.link") +struct sched_ext_ops dequeue_iter_ops = { + .init = (void *)dequeue_iter_init, + .select_cpu = (void *)dequeue_iter_select_cpu, + .enqueue = (void *)dequeue_iter_enqueue, + .dispatch = (void *)dequeue_iter_dispatch, + .dequeue = (void *)dequeue_iter_dequeue, + .exit = (void *)dequeue_iter_exit, + .timeout_ms = 1000U, + .name = "dequeue_iter", +}; diff --git a/tools/testing/selftests/sched_ext/dequeue_iter.c b/tools/testing/selftests/sched_ext/dequeue_iter.c new file mode 100644 index 000000000000..f60711bf3b39 --- /dev/null +++ b/tools/testing/selftests/sched_ext/dequeue_iter.c @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (c) 2026 fangqiurong <fangqiurong@kylinos.cn> + */ +#include <bpf/bpf.h> +#include <scx/common.h> +#include <time.h> +#include <unistd.h> +#include "dequeue_iter.bpf.skel.h" +#include "scx_test.h" + +#define DQ_TARGET 10 +#define DQ_DEADLINE_MS 3000 + +static unsigned long long now_ms(void) +{ + struct timespec ts; + + clock_gettime(CLOCK_MONOTONIC, &ts); + + return ts.tv_sec * 1000ULL + ts.tv_nsec / 1000000; +} + +static enum scx_test_status setup(void **ctx) +{ + struct dequeue_iter *skel; + + skel = dequeue_iter__open(); + SCX_FAIL_IF(!skel, "Failed to open"); + SCX_ENUM_INIT(skel); + SCX_FAIL_IF(dequeue_iter__load(skel), "Failed to load skel"); + + *ctx = skel; + + return SCX_TEST_PASS; +} + +static enum scx_test_status run(void *ctx) +{ + struct dequeue_iter *skel = ctx; + struct bpf_link *link; + unsigned long long end; + + link = bpf_map__attach_struct_ops(skel->maps.dequeue_iter_ops); + SCX_FAIL_IF(!link, "Failed to attach scheduler"); + + end = now_ms() + DQ_DEADLINE_MS; + while (skel->bss->dq_count < DQ_TARGET && !UEI_EXITED(skel, uei) && + now_ms() < end) + usleep(100); + + bpf_link__destroy(link); + + SCX_EQ(skel->data->uei.kind, EXIT_KIND(SCX_EXIT_UNREG)); + + if (skel->bss->dq_count < DQ_TARGET) { + SCX_ERR("ops.dequeue() fired only %llu times", + (unsigned long long)skel->bss->dq_count); + return SCX_TEST_FAIL; + } + + return SCX_TEST_PASS; +} + +static void cleanup(void *ctx) +{ + struct dequeue_iter *skel = ctx; + + dequeue_iter__destroy(skel); +} + +struct scx_test dequeue_iter = { + .name = "dequeue_iter", + .description = "Verify ops.dequeue() can iterate its source user DSQ", + .setup = setup, + .run = run, + .cleanup = cleanup, +}; +REGISTER_SCX_TEST(&dequeue_iter) diff --git a/tools/testing/selftests/sched_ext/dequeue_remote.bpf.c b/tools/testing/selftests/sched_ext/dequeue_remote.bpf.c new file mode 100644 index 000000000000..983590c6d498 --- /dev/null +++ b/tools/testing/selftests/sched_ext/dequeue_remote.bpf.c @@ -0,0 +1,270 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Verify that ops.dequeue() is called when a task leaves the BPF scheduler's + * custody by being moved to the local DSQ of a CPU other than the one whose + * rq it's on (move_remote_task_to_local_dsq()). + * + * ops.enqueue() puts every task into custody and ops.dispatch() moves it to + * the dispatching CPU's local DSQ, so most moves cross CPUs. With + * @test_use_move_to_local, tasks are queued on a user DSQ and consumed with + * scx_bpf_dsq_move_to_local(). Otherwise, they are queued in a BPF queue and + * dispatched with SCX_DSQ_LOCAL_ON. + * + * Copyright (c) 2026 Google LLC. + */ + +#include <scx/common.bpf.h> + +#define SHARED_DSQ 0 +#define MAX_DISPATCH_POPS 8 + +char _license[] SEC("license") = "GPL"; + +UEI_DEFINE(uei); + +struct { + __uint(type, BPF_MAP_TYPE_QUEUE); + __uint(max_entries, 32768); + __type(value, s32); +} global_queue SEC(".maps"); + +enum task_state { + TASK_NONE = 0, + TASK_ENQUEUED, /* in BPF custody, waiting for ops.dequeue() */ + TASK_DISPATCHED, /* left custody */ +}; + +struct task_ctx { + enum task_state state; + s32 enq_cpu; /* scx_bpf_task_cpu() at ops.enqueue() */ + u64 enqueue_seq; +}; + +struct { + __uint(type, BPF_MAP_TYPE_TASK_STORAGE); + __uint(map_flags, BPF_F_NO_PREALLOC); + __type(key, int); + __type(value, struct task_ctx); +} task_ctx_stor SEC(".maps"); + +/* core_cookie only exists with CONFIG_SCHED_CORE */ +struct task_struct___core_sched { + unsigned long core_cookie; +} __attribute__((preserve_access_index)); + +bool test_use_move_to_local; + +u64 enqueue_cnt, dequeue_cnt, dispatch_dequeue_cnt, change_dequeue_cnt; +u64 remote_dispatch_cnt, remote_running_cnt, missed_dequeue_cnt; +u64 core_sched_exec_dequeue_cnt; + +static struct task_ctx *lookup_task_ctx(struct task_struct *p) +{ + return bpf_task_storage_get(&task_ctx_stor, p, 0, 0); +} + +static bool task_has_core_cookie(struct task_struct *p) +{ + struct task_struct___core_sched *t = (void *)p; + + if (!bpf_core_field_exists(t->core_cookie)) + return false; + return BPF_CORE_READ(t, core_cookie); +} + +s32 BPF_STRUCT_OPS(dequeue_remote_select_cpu, struct task_struct *p, + s32 prev_cpu, u64 wake_flags) +{ + /* no direct dispatch, always go through ops.enqueue() */ + return prev_cpu; +} + +void BPF_STRUCT_OPS(dequeue_remote_enqueue, struct task_struct *p, u64 enq_flags) +{ + struct task_ctx *tctx; + s32 pid = p->pid; + + tctx = lookup_task_ctx(p); + if (!tctx) { + scx_bpf_dsq_insert(p, SCX_DSQ_GLOBAL, SCX_SLICE_DFL, enq_flags); + return; + } + + /* the previous custody period must have ended with ops.dequeue() */ + if (tctx->state == TASK_ENQUEUED) + scx_bpf_error("%d (%s): enqueue while in ENQUEUED state seq=%llu", + p->pid, p->comm, tctx->enqueue_seq); + + /* + * Mark @p as enqueued before making it visible to ops.dispatch() on + * other CPUs, which skips queue entries of tasks not in ENQUEUED + * state as stale. + */ + tctx->state = TASK_ENQUEUED; + tctx->enq_cpu = scx_bpf_task_cpu(p); + tctx->enqueue_seq++; + + if (test_use_move_to_local) { + scx_bpf_dsq_insert(p, SHARED_DSQ, SCX_SLICE_DFL, enq_flags); + } else if (bpf_map_push_elem(&global_queue, &pid, 0)) { + scx_bpf_dsq_insert(p, SCX_DSQ_GLOBAL, SCX_SLICE_DFL, enq_flags); + tctx->state = TASK_DISPATCHED; + tctx->enq_cpu = -1; + goto out; + } + + __sync_fetch_and_add(&enqueue_cnt, 1); +out: + scx_bpf_kick_cpu(scx_bpf_task_cpu(p), SCX_KICK_IDLE); +} + +void BPF_STRUCT_OPS(dequeue_remote_dequeue, struct task_struct *p, u64 deq_flags) +{ + struct task_ctx *tctx; + + __sync_fetch_and_add(&dequeue_cnt, 1); + + tctx = lookup_task_ctx(p); + if (!tctx) + return; + + /* + * Only core scheduling can pick a task straight out of custody, and + * only if the task has a core cookie. Otherwise, the custody exit was + * missed when @p was inserted into a local DSQ and got deferred until + * @p was picked. + */ + if ((deq_flags & SCX_DEQ_CORE_SCHED_EXEC) && !task_has_core_cookie(p)) { + __sync_fetch_and_add(&core_sched_exec_dequeue_cnt, 1); + scx_bpf_error("%d (%s): late ops.dequeue() with SCX_DEQ_CORE_SCHED_EXEC (enq_cpu=%d cpu=%d seq=%llu)", + p->pid, p->comm, tctx->enq_cpu, + scx_bpf_task_cpu(p), tctx->enqueue_seq); + } + + /* ops.dequeue() ends the custody period started by ops.enqueue() */ + if (tctx->state != TASK_ENQUEUED) + scx_bpf_error("%d (%s): dequeue outside custody deq_flags=0x%llx state=%d seq=%llu", + p->pid, p->comm, deq_flags, tctx->state, + tctx->enqueue_seq); + + if (deq_flags & SCX_DEQ_SCHED_CHANGE) { + __sync_fetch_and_add(&change_dequeue_cnt, 1); + tctx->state = TASK_NONE; + } else { + __sync_fetch_and_add(&dispatch_dequeue_cnt, 1); + tctx->state = TASK_DISPATCHED; + } +} + +void BPF_STRUCT_OPS(dequeue_remote_dispatch, s32 cpu, struct task_struct *prev) +{ + struct task_ctx *tctx; + struct task_struct *p; + s32 pid; + int i; + + if (test_use_move_to_local) { + scx_bpf_dsq_move_to_local(SHARED_DSQ, 0); + return; + } + + /* pop past stale entries so that they don't leave this CPU idle */ + bpf_for(i, 0, MAX_DISPATCH_POPS) { + if (bpf_map_pop_elem(&global_queue, &pid)) + return; + + p = bpf_task_from_pid(pid); + if (!p) + continue; + + /* + * Entries are stale if @p left custody through a property + * change dequeue or was dispatched from a duplicate entry. + */ + tctx = lookup_task_ctx(p); + if (!tctx || tctx->state != TASK_ENQUEUED) { + bpf_task_release(p); + continue; + } + + if (bpf_cpumask_test_cpu(cpu, p->cpus_ptr)) { + if (scx_bpf_task_cpu(p) != cpu) + __sync_fetch_and_add(&remote_dispatch_cnt, 1); + scx_bpf_dsq_insert(p, SCX_DSQ_LOCAL_ON | cpu, + SCX_SLICE_DFL, 0); + } else { + scx_bpf_dsq_insert(p, SCX_DSQ_GLOBAL, SCX_SLICE_DFL, 0); + } + + bpf_task_release(p); + return; + } + + /* out of pops with entries left, retry instead of idling this CPU */ + if (!bpf_map_peek_elem(&global_queue, &pid)) + scx_bpf_kick_cpu(cpu, SCX_KICK_IDLE); +} + +void BPF_STRUCT_OPS(dequeue_remote_running, struct task_struct *p) +{ + struct task_ctx *tctx; + + tctx = lookup_task_ctx(p); + if (!tctx) + return; + + /* tasks can only run from a local DSQ, i.e. after leaving custody */ + if (tctx->state == TASK_ENQUEUED) { + __sync_fetch_and_add(&missed_dequeue_cnt, 1); + scx_bpf_error("%d (%s): running without ops.dequeue() (enq_cpu=%d cpu=%d seq=%llu)", + p->pid, p->comm, tctx->enq_cpu, + scx_bpf_task_cpu(p), tctx->enqueue_seq); + return; + } + + if (tctx->enq_cpu >= 0 && tctx->enq_cpu != scx_bpf_task_cpu(p)) + __sync_fetch_and_add(&remote_running_cnt, 1); + tctx->enq_cpu = -1; +} + +s32 BPF_STRUCT_OPS(dequeue_remote_init_task, struct task_struct *p, + struct scx_init_task_args *args) +{ + struct task_ctx *tctx; + + tctx = bpf_task_storage_get(&task_ctx_stor, p, 0, + BPF_LOCAL_STORAGE_GET_F_CREATE); + if (!tctx) + return -ENOMEM; + + /* task storage persists across attachments, start from scratch */ + tctx->state = TASK_NONE; + tctx->enq_cpu = -1; + tctx->enqueue_seq = 0; + + return 0; +} + +s32 BPF_STRUCT_OPS_SLEEPABLE(dequeue_remote_init) +{ + return scx_bpf_create_dsq(SHARED_DSQ, -1); +} + +void BPF_STRUCT_OPS(dequeue_remote_exit, struct scx_exit_info *ei) +{ + UEI_RECORD(uei, ei); +} + +SEC(".struct_ops.link") +struct sched_ext_ops dequeue_remote_ops = { + .select_cpu = (void *)dequeue_remote_select_cpu, + .enqueue = (void *)dequeue_remote_enqueue, + .dequeue = (void *)dequeue_remote_dequeue, + .dispatch = (void *)dequeue_remote_dispatch, + .running = (void *)dequeue_remote_running, + .init_task = (void *)dequeue_remote_init_task, + .init = (void *)dequeue_remote_init, + .exit = (void *)dequeue_remote_exit, + .flags = SCX_OPS_ENQ_LAST, + .name = "dequeue_remote", +}; diff --git a/tools/testing/selftests/sched_ext/dequeue_remote.c b/tools/testing/selftests/sched_ext/dequeue_remote.c new file mode 100644 index 000000000000..f2c1901030de --- /dev/null +++ b/tools/testing/selftests/sched_ext/dequeue_remote.c @@ -0,0 +1,204 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Verify that ops.dequeue() is called for tasks leaving BPF custody through + * an SCX-internal cross-CPU migration (move_remote_task_to_local_dsq()). + * + * Copyright (c) 2026 Google LLC. + */ +#define _GNU_SOURCE +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <signal.h> +#include <time.h> +#include <sched.h> +#include <bpf/bpf.h> +#include <scx/common.h> +#include <sys/wait.h> +#include "scx_test.h" +#include "dequeue_remote.bpf.skel.h" + +#define MAX_WORKERS 64 +#define RUN_MS 2000 + +static int nr_cpus; + +static long long now_ms(void) +{ + struct timespec ts; + + clock_gettime(CLOCK_MONOTONIC, &ts); + return ts.tv_sec * 1000LL + ts.tv_nsec / 1000000; +} + +/* mix of short bursts and sleeps to generate lots of enqueues and wakeups */ +static void worker_fn(int id) +{ + long long end = now_ms() + RUN_MS; + volatile unsigned long sum = 0; + + while (now_ms() < end) { + unsigned long j; + + for (j = 0; j < 20000 + id * 1000; j++) + sum += j; + if (id & 1) + usleep(100); + else + sched_yield(); + } + + exit(0); +} + +static enum scx_test_status run_scenario(struct dequeue_remote *skel, + bool use_move_to_local, + const char *name) +{ + enum scx_test_status ret = SCX_TEST_PASS; + struct bpf_link *link; + pid_t pids[MAX_WORKERS]; + int nr_workers, nr_forked, i; + + nr_workers = 2 * nr_cpus; + if (nr_workers < 4) + nr_workers = 4; + if (nr_workers > MAX_WORKERS) + nr_workers = MAX_WORKERS; + + skel->bss->test_use_move_to_local = use_move_to_local; + skel->bss->enqueue_cnt = 0; + skel->bss->dequeue_cnt = 0; + skel->bss->dispatch_dequeue_cnt = 0; + skel->bss->change_dequeue_cnt = 0; + skel->bss->remote_dispatch_cnt = 0; + skel->bss->remote_running_cnt = 0; + skel->bss->missed_dequeue_cnt = 0; + skel->bss->core_sched_exec_dequeue_cnt = 0; + memset(&skel->data->uei, 0, sizeof(skel->data->uei)); + + link = bpf_map__attach_struct_ops(skel->maps.dequeue_remote_ops); + SCX_FAIL_IF(!link, "Failed to attach struct_ops for %s", name); + + fflush(stdout); + fflush(stderr); + + for (nr_forked = 0; nr_forked < nr_workers; nr_forked++) { + pid_t pid = fork(); + + if (pid < 0) { + SCX_ERR("Failed to fork worker %d", nr_forked); + ret = SCX_TEST_FAIL; + break; + } + if (pid == 0) + worker_fn(nr_forked); + pids[nr_forked] = pid; + } + + /* on failure, kill the remaining workers but still reap them */ + for (i = 0; i < nr_forked; i++) { + int status; + + if (ret != SCX_TEST_PASS) + kill(pids[i], SIGKILL); + + if (waitpid(pids[i], &status, 0) != pids[i]) { + SCX_ERR("Failed to wait for worker %d", i); + ret = SCX_TEST_FAIL; + } else if (ret == SCX_TEST_PASS && status != 0) { + SCX_ERR("Worker %d exited with status %d", i, status); + ret = SCX_TEST_FAIL; + } + } + + bpf_link__destroy(link); + + if (ret != SCX_TEST_PASS) + return ret; + + printf("%s:\n", name); + printf(" workers: %d\n", nr_workers); + printf(" enqueues: %lu\n", (unsigned long)skel->bss->enqueue_cnt); + printf(" dequeues: %lu (dispatch: %lu, property_change: %lu)\n", + (unsigned long)skel->bss->dequeue_cnt, + (unsigned long)skel->bss->dispatch_dequeue_cnt, + (unsigned long)skel->bss->change_dequeue_cnt); + if (!use_move_to_local) + printf(" remote SCX_DSQ_LOCAL_ON dispatches: %lu\n", + (unsigned long)skel->bss->remote_dispatch_cnt); + printf(" ran on a CPU other than the enqueue CPU: %lu\n", + (unsigned long)skel->bss->remote_running_cnt); + printf(" ran without ops.dequeue(): %lu\n", + (unsigned long)skel->bss->missed_dequeue_cnt); + printf(" late SCX_DEQ_CORE_SCHED_EXEC dequeues: %lu\n", + (unsigned long)skel->bss->core_sched_exec_dequeue_cnt); + + if (skel->data->uei.kind != EXIT_KIND(SCX_EXIT_UNREG)) + SCX_ERR("Scheduler exited with kind=%lld: %s", + (long long)skel->data->uei.kind, skel->data->uei.msg); + SCX_EQ(skel->data->uei.kind, EXIT_KIND(SCX_EXIT_UNREG)); + + /* the test is meaningless if no task was moved across CPUs */ + SCX_GT(skel->bss->remote_running_cnt, 0); + SCX_EQ(skel->bss->enqueue_cnt, skel->bss->dequeue_cnt); + + return SCX_TEST_PASS; +} + +static enum scx_test_status setup(void **ctx) +{ + struct dequeue_remote *skel; + cpu_set_t cpus; + + SCX_FAIL_IF(sched_getaffinity(0, sizeof(cpus), &cpus), + "Failed to get CPU affinity"); + nr_cpus = CPU_COUNT(&cpus); + if (nr_cpus < 2) { + fprintf(stderr, "Skipping: requires at least 2 usable CPUs\n"); + return SCX_TEST_SKIP; + } + + skel = SCX_OPS_OPEN(dequeue_remote_ops, dequeue_remote); + SCX_OPS_LOAD(skel, dequeue_remote_ops, dequeue_remote, uei); + + *ctx = skel; + + return SCX_TEST_PASS; +} + +static enum scx_test_status run(void *ctx) +{ + struct dequeue_remote *skel = ctx; + enum scx_test_status status; + + status = run_scenario(skel, false, + "BPF queue -> SCX_DSQ_LOCAL_ON | cpu"); + if (status != SCX_TEST_PASS) + return status; + + status = run_scenario(skel, true, + "user DSQ -> scx_bpf_dsq_move_to_local()"); + if (status != SCX_TEST_PASS) + return status; + + return SCX_TEST_PASS; +} + +static void cleanup(void *ctx) +{ + struct dequeue_remote *skel = ctx; + + dequeue_remote__destroy(skel); +} + +struct scx_test dequeue_remote_test = { + .name = "dequeue_remote", + .description = "Verify ops.dequeue() on SCX-internal cross-CPU migrations", + .setup = setup, + .run = run, + .cleanup = cleanup, +}; + +REGISTER_SCX_TEST(&dequeue_remote_test) diff --git a/tools/testing/selftests/sched_ext/enable_cmask.bpf.c b/tools/testing/selftests/sched_ext/enable_cmask.bpf.c new file mode 100644 index 000000000000..0068f3c7ab3c --- /dev/null +++ b/tools/testing/selftests/sched_ext/enable_cmask.bpf.c @@ -0,0 +1,217 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * A cid-form scheduler checking the cmask cid-form ops.enable() receives: the + * header, every cid bit against p->cpus_ptr, and that set_cmask() follows with + * the same mask before set_weight() and before the task first becomes runnable, + * and never runs before enable(). + * + * Copyright (c) 2026 Tejun Heo <tj@kernel.org> + */ +#include <scx/common.bpf.h> + +char _license[] SEC("license") = "GPL"; + +struct { + __uint(type, BPF_MAP_TYPE_ARENA); + __uint(map_flags, BPF_F_MMAPABLE); + __uint(max_entries, 1 << 16); +} arena SEC(".maps"); + +struct task_ctx { + u64 enable_fp; /* fingerprint of the mask enable() received */ + bool enabled; + bool pending; /* enable() ran, the initial set_cmask() hasn't */ +}; + +struct { + __uint(type, BPF_MAP_TYPE_TASK_STORAGE); + __uint(map_flags, BPF_F_NO_PREALLOC); + __type(key, int); + __type(value, struct task_ctx); +} task_ctx_stor SEC(".maps"); + +/* details of a cid bit mismatch, filled by check_mask() */ +struct mask_mismatch { + s32 cid; + bool want; + bool got; +}; + +u64 nr_enable, nr_initial_set_cmask, nr_set_cmask, nr_set_weight; + +UEI_DEFINE(uei); + +static struct task_ctx *lookup_task_ctx(struct task_struct *p) +{ + struct task_ctx *tctx; + + tctx = bpf_task_storage_get(&task_ctx_stor, p, 0, 0); + if (!tctx) + scx_bpf_error("task_ctx lookup failed for %s[%d]", p->comm, p->pid); + return tctx; +} + +/* + * Verify @m's header and every cid bit against @p's cpumask and fingerprint the + * bits into @fp. Return 0 on success, -EINVAL on a bad header, -ENOENT on a cid + * without a cpu and -EIO on a bit mismatch with the details in @mm. + */ +static int check_mask(struct task_struct *p, const struct scx_cmask __arena *m, u64 *fp, + struct mask_mismatch *mm) +{ + u32 nr_cids = scx_bpf_nr_cids(); + u64 h = 0; + s32 cid; + + if (m->base || m->nr_cids != nr_cids || m->alloc_words != CMASK_NR_WORDS(nr_cids)) + return -EINVAL; + + bpf_for(cid, 0, nr_cids) { + bool want, got; + s32 cpu; + + cpu = scx_bpf_cid_to_cpu(cid); + if (cpu < 0) + return -ENOENT; + want = bpf_cpumask_test_cpu(cpu, p->cpus_ptr); + got = cmask_test(cid, m); + if (want != got) { + mm->cid = cid; + mm->want = want; + mm->got = got; + return -EIO; + } + h = h * 31 + got; + } + + *fp = h; + return 0; +} + +s32 BPF_STRUCT_OPS_SLEEPABLE(enable_cmask_init_task, struct task_struct *p, + struct scx_init_task_args *args) +{ + if (!bpf_task_storage_get(&task_ctx_stor, p, 0, BPF_LOCAL_STORAGE_GET_F_CREATE)) + return -ENOMEM; + return 0; +} + +void BPF_STRUCT_OPS(enable_cmask_enable, struct task_struct *p, struct scx_enable_args *args) +{ + struct scx_cmask __arena *m = (struct scx_cmask __arena *)args->cmask_arena_addr; + struct mask_mismatch mm = {}; + struct task_ctx *tctx; + int ret; + + asm volatile("" :: "r"(&arena)); + tctx = lookup_task_ctx(p); + if (!tctx) + return; + + __sync_fetch_and_add(&nr_enable, 1); + if (tctx->enabled || tctx->pending) { + scx_bpf_error("enable: %s[%d] enabled twice", p->comm, p->pid); + return; + } + + ret = check_mask(p, m, &tctx->enable_fp, &mm); + if (ret) { + scx_bpf_error("enable: %s[%d] cmask check failed %d cid=%d want=%d got=%d", + p->comm, p->pid, ret, mm.cid, mm.want, mm.got); + return; + } + tctx->enabled = true; + tctx->pending = true; +} + +void BPF_STRUCT_OPS(enable_cmask_set_cmask, struct task_struct *p, + struct scx_cmask __arena *m) +{ + struct mask_mismatch mm = {}; + struct task_ctx *tctx; + u64 fp; + int ret; + + asm volatile("" :: "r"(&arena)); + tctx = lookup_task_ctx(p); + if (!tctx) + return; + + __sync_fetch_and_add(&nr_set_cmask, 1); + if (!tctx->enabled) { + scx_bpf_error("set_cmask: %s[%d] not enabled", p->comm, p->pid); + return; + } + + ret = check_mask(p, m, &fp, &mm); + if (ret) { + scx_bpf_error("set_cmask: %s[%d] cmask check failed %d cid=%d want=%d got=%d", + p->comm, p->pid, ret, mm.cid, mm.want, mm.got); + return; + } + + if (tctx->pending) { + if (fp != tctx->enable_fp) { + scx_bpf_error("set_cmask: %s[%d] initial mask differs from enable()", + p->comm, p->pid); + return; + } + tctx->pending = false; + __sync_fetch_and_add(&nr_initial_set_cmask, 1); + } +} + +void BPF_STRUCT_OPS(enable_cmask_set_weight, struct task_struct *p, u32 weight) +{ + struct task_ctx *tctx; + + tctx = lookup_task_ctx(p); + if (!tctx) + return; + + __sync_fetch_and_add(&nr_set_weight, 1); + if (tctx->pending) + scx_bpf_error("set_weight: %s[%d] before the initial set_cmask()", p->comm, + p->pid); +} + +void BPF_STRUCT_OPS(enable_cmask_runnable, struct task_struct *p, u64 enq_flags) +{ + struct task_ctx *tctx; + + tctx = lookup_task_ctx(p); + if (!tctx) + return; + + if (tctx->pending) + scx_bpf_error("runnable: %s[%d] before the initial set_cmask()", p->comm, + p->pid); +} + +void BPF_STRUCT_OPS(enable_cmask_disable, struct task_struct *p) +{ + struct task_ctx *tctx; + + tctx = lookup_task_ctx(p); + if (!tctx) + return; + + tctx->enabled = false; + tctx->pending = false; +} + +void BPF_STRUCT_OPS(enable_cmask_exit, struct scx_exit_info *ei) +{ + UEI_RECORD(uei, ei); +} + +SCX_OPS_CID_DEFINE(enable_cmask_ops, + .init_task = (void *)enable_cmask_init_task, + .enable = (void *)enable_cmask_enable, + .set_cmask = (void *)enable_cmask_set_cmask, + .set_weight = (void *)enable_cmask_set_weight, + .runnable = (void *)enable_cmask_runnable, + .disable = (void *)enable_cmask_disable, + .exit = (void *)enable_cmask_exit, + .flags = SCX_OPS_SWITCH_PARTIAL, + .name = "enable_cmask"); diff --git a/tools/testing/selftests/sched_ext/enable_cmask.c b/tools/testing/selftests/sched_ext/enable_cmask.c new file mode 100644 index 000000000000..556bcad4431d --- /dev/null +++ b/tools/testing/selftests/sched_ext/enable_cmask.c @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: GPL-2.0 +/* Copyright (c) 2026 Tejun Heo <tj@kernel.org> */ +#define _GNU_SOURCE +#include <sched.h> +#include <stdio.h> +#include <stdlib.h> +#include <time.h> +#include <unistd.h> +#include <sys/wait.h> +#include <bpf/bpf.h> +#include <scx/common.h> +#include "enable_cmask.bpf.skel.h" +#include "scx_test.h" + +#define SCHED_EXT 7 +#define NR_CHILDREN 8 +#define MAX_CPUS 1024 + +static int cpus[MAX_CPUS]; +static int nr_cpus; + +static void spin_ms(int ms) +{ + struct timespec start, now; + + clock_gettime(CLOCK_MONOTONIC, &start); + do { + clock_gettime(CLOCK_MONOTONIC, &now); + } while ((now.tv_sec - start.tv_sec) * 1000 + + (now.tv_nsec - start.tv_nsec) / 1000000 < ms); +} + +static int pin(pid_t pid, int idx) +{ + cpu_set_t set; + + CPU_ZERO(&set); + CPU_SET(cpus[idx % nr_cpus], &set); + return sched_setaffinity(pid, sizeof(set), &set); +} + +/* + * Pin, switch to SCHED_EXT for a class-switch enable, fork a grandchild that + * inherits the policy for a fork-path enable, then change affinity a few times + * while running for set_cmask() on live tasks. + */ +static int child(int idx) +{ + struct sched_param param = {}; + int i, status; + pid_t pid; + + if (pin(0, idx) || sched_setscheduler(0, SCHED_EXT, ¶m)) + return 1; + + pid = fork(); + if (pid < 0) + return 1; + if (!pid) { + spin_ms(20); + return 0; + } + + for (i = 1; i <= 4; i++) { + if (pin(0, idx + i)) + return 1; + spin_ms(5); + } + + return waitpid(pid, &status, 0) == pid && !status ? 0 : 1; +} + +static enum scx_test_status run(void *ctx) +{ + struct enable_cmask *skel; + struct bpf_link *link; + pid_t pids[NR_CHILDREN]; + cpu_set_t set; + int i, status, failed = 0; + + if (!__COMPAT_struct_has_field("scx_enable_args", "cmask_arena_addr")) + return SCX_TEST_SKIP; + + SCX_FAIL_IF(sched_getaffinity(0, sizeof(set), &set), "Failed to read affinity"); + for (i = 0; i < MAX_CPUS && i < CPU_SETSIZE; i++) + if (CPU_ISSET(i, &set)) + cpus[nr_cpus++] = i; + if (nr_cpus < 2) + return SCX_TEST_SKIP; + + skel = enable_cmask__open(); + SCX_FAIL_IF(!skel, "Failed to open"); + SCX_ENUM_INIT(skel); + SCX_FAIL_IF(enable_cmask__load(skel), "Failed to load skel"); + + link = bpf_map__attach_struct_ops(skel->maps.enable_cmask_ops); + SCX_FAIL_IF(!link, "Failed to attach struct_ops"); + + for (i = 0; i < NR_CHILDREN; i++) { + pids[i] = fork(); + SCX_FAIL_IF(pids[i] < 0, "Failed to fork"); + if (!pids[i]) + exit(child(i)); + } + + /* affinity changes from the outside race with the children's own */ + for (i = 0; i < NR_CHILDREN; i++) + pin(pids[i], i + NR_CHILDREN); + + for (i = 0; i < NR_CHILDREN; i++) { + if (waitpid(pids[i], &status, 0) != pids[i] || status) + failed++; + } + + bpf_link__destroy(link); + + SCX_EQ(skel->data->uei.kind, EXIT_KIND(SCX_EXIT_UNREG)); + SCX_EQ(failed, 0); + SCX_GE(skel->bss->nr_enable, 2 * NR_CHILDREN); + SCX_EQ(skel->bss->nr_initial_set_cmask, skel->bss->nr_enable); + SCX_GT(skel->bss->nr_set_cmask, skel->bss->nr_initial_set_cmask); + SCX_GE(skel->bss->nr_set_weight, skel->bss->nr_enable); + printf("enable=%lu initial_set_cmask=%lu set_cmask=%lu set_weight=%lu\n", + (unsigned long)skel->bss->nr_enable, + (unsigned long)skel->bss->nr_initial_set_cmask, + (unsigned long)skel->bss->nr_set_cmask, + (unsigned long)skel->bss->nr_set_weight); + + enable_cmask__destroy(skel); + return SCX_TEST_PASS; +} + +struct scx_test enable_cmask = { + .name = "enable_cmask", + .description = "Check the cid-form ops.enable() cmask and the set_cmask() after it", + .run = run, +}; +REGISTER_SCX_TEST(&enable_cmask) diff --git a/tools/testing/selftests/tc-testing/tc-tests/actions/batch-delete.json b/tools/testing/selftests/tc-testing/tc-tests/actions/batch-delete.json new file mode 100644 index 000000000000..ef7ca4a6775b --- /dev/null +++ b/tools/testing/selftests/tc-testing/tc-tests/actions/batch-delete.json @@ -0,0 +1,115 @@ +[ + { + "id": "d710", + "name": "Release tail references after first action deletion fails", + "category": [ + "actions", + "gact" + ], + "plugins": { + "requires": "nsPlugin" + }, + "setup": [ + [ + "$TC actions flush action gact", + 0, + 1, + 255 + ], + "$TC qdisc add dev $DEV1 ingress", + "$TC actions add action pass index 1", + "$TC actions add action pass index 2", + "$TC actions add action pass index 3", + "$TC filter add dev $DEV1 protocol ip ingress u32 match u32 0 0 action gact index 1" + ], + "cmdUnderTest": "$TC actions del action gact index 1 action gact index 2 action gact index 3", + "expExitCode": "255", + "verifyCmd": "$TC actions ls action gact", + "matchPattern": "total acts 3\\b.*index 1 ref 2 bind 1\\b.*index 2 ref 1 bind 0\\b.*index 3 ref 1 bind 0\\b", + "matchCount": "1", + "teardown": [ + "$TC qdisc del dev $DEV1 ingress", + [ + "$TC actions flush action gact", + 0, + 1, + 255 + ] + ] + }, + { + "id": "d711", + "name": "Release tail references after middle action deletion fails", + "category": [ + "actions", + "gact" + ], + "plugins": { + "requires": "nsPlugin" + }, + "setup": [ + [ + "$TC actions flush action gact", + 0, + 1, + 255 + ], + "$TC qdisc add dev $DEV1 ingress", + "$TC actions add action pass index 1", + "$TC actions add action pass index 2", + "$TC actions add action pass index 3", + "$TC filter add dev $DEV1 protocol ip ingress u32 match u32 0 0 action gact index 2" + ], + "cmdUnderTest": "$TC actions del action gact index 1 action gact index 2 action gact index 3", + "expExitCode": "255", + "verifyCmd": "$TC actions ls action gact", + "matchPattern": "total acts 2\\b.*index 2 ref 2 bind 1\\b.*index 3 ref 1 bind 0\\b", + "matchCount": "1", + "teardown": [ + "$TC qdisc del dev $DEV1 ingress", + [ + "$TC actions flush action gact", + 0, + 1, + 255 + ] + ] + }, + { + "id": "d713", + "name": "Delete a tail action once after a failed batch", + "category": [ + "actions", + "gact" + ], + "plugins": { + "requires": "nsPlugin" + }, + "setup": [ + [ + "$TC actions flush action gact", + 0, + 1, + 255 + ], + "$TC qdisc add dev $DEV1 ingress", + "$TC actions add action pass index 1", + "$TC actions add action pass index 2", + "$TC filter add dev $DEV1 protocol ip ingress u32 match u32 0 0 action gact index 1" + ], + "cmdUnderTest": "$TC actions del action gact index 1 action gact index 2", + "expExitCode": "255", + "verifyCmd": "sh -c '$TC actions del action gact index 2 && $TC actions ls action gact'", + "matchPattern": "total acts 1\\b.*index 1 ref 2 bind 1\\b", + "matchCount": "1", + "teardown": [ + "$TC qdisc del dev $DEV1 ingress", + [ + "$TC actions flush action gact", + 0, + 1, + 255 + ] + ] + } +] diff --git a/tools/testing/selftests/tc-testing/tc-tests/filters/route.json b/tools/testing/selftests/tc-testing/tc-tests/filters/route.json index 2d5843aebd72..c3b6ff157d3e 100644 --- a/tools/testing/selftests/tc-testing/tc-tests/filters/route.json +++ b/tools/testing/selftests/tc-testing/tc-tests/filters/route.json @@ -412,5 +412,51 @@ "teardown": [ "$TC qdisc del dev $DEV1 ingress" ] + }, + { + "id": "4000", + "name": "Change a wildcard route filter classid only (no routing attribute)", + "category": [ + "filter", + "route" + ], + "plugins": { + "requires": "nsPlugin" + }, + "setup": [ + "$TC qdisc add dev $DEV1 ingress", + "$TC filter add dev $DEV1 parent ffff: protocol ip prio 100 route classid 1:1" + ], + "cmdUnderTest": "$TC filter change dev $DEV1 parent ffff: protocol ip prio 100 route classid 1:9", + "expExitCode": "0", + "verifyCmd": "$TC filter ls dev $DEV1 parent ffff:", + "matchPattern": "fh 0xffff8000 flowid 1:9", + "matchCount": "1", + "teardown": [ + "$TC qdisc del dev $DEV1 ingress" + ] + }, + { + "id": "907a", + "name": "Try to change a route filter with no routing attribute and a non-wildcard handle", + "category": [ + "filter", + "route" + ], + "plugins": { + "requires": "nsPlugin" + }, + "setup": [ + "$TC qdisc add dev $DEV1 ingress", + "$TC filter add dev $DEV1 parent ffff: protocol ip prio 100 route from 1 to 1 classid 1:1" + ], + "cmdUnderTest": "$TC filter change dev $DEV1 parent ffff: protocol ip prio 100 handle 0x10001 route classid 1:2", + "expExitCode": "2", + "verifyCmd": "$TC filter ls dev $DEV1 parent ffff:", + "matchPattern": "fh 0x00010001 flowid 1:1 to 1 from 1", + "matchCount": "1", + "teardown": [ + "$TC qdisc del dev $DEV1 ingress" + ] } ] diff --git a/tools/testing/selftests/tc-testing/tc-tests/filters/u32.json b/tools/testing/selftests/tc-testing/tc-tests/filters/u32.json index e2b03f2b5e89..edc5148a8d97 100644 --- a/tools/testing/selftests/tc-testing/tc-tests/filters/u32.json +++ b/tools/testing/selftests/tc-testing/tc-tests/filters/u32.json @@ -376,5 +376,53 @@ "teardown": [ "$TC qdisc del dev $DUMMY clsact" ] + }, + { + "id": "35fc", + "name": "u32 manual table then auto table: auto allocation must not alias a live manual handle", + "category": [ + "filter", + "u32" + ], + "plugins": { + "requires": "nsPlugin" + }, + "setup": [ + "$TC qdisc add dev $DEV1 ingress", + "$TC filter add dev $DEV1 ingress protocol ip pref 1 handle 801: u32 divisor 16" + ], + "cmdUnderTest": "$TC filter add dev $DEV1 ingress protocol ip pref 2 u32 divisor 16", + "expExitCode": "0", + "verifyCmd": "$TC -d filter show dev $DEV1 ingress", + "matchPattern": "fh 801:", + "matchCount": "1", + "teardown": [ + "$TC qdisc del dev $DEV1 ingress" + ] + }, + { + "id": "a6e8", + "name": "u32 manual table add/del does not leak its idr entry (re-adding the same handle succeeds)", + "category": [ + "filter", + "u32" + ], + "plugins": { + "requires": "nsPlugin" + }, + "setup": [ + "$TC qdisc add dev $DEV1 ingress", + "$TC filter add dev $DEV1 ingress protocol ip pref 1 u32 divisor 16", + "$TC filter add dev $DEV1 ingress protocol ip pref 5 handle 901: u32 divisor 1", + "$TC filter del dev $DEV1 ingress protocol ip pref 5 handle 901: u32" + ], + "cmdUnderTest": "$TC filter add dev $DEV1 ingress protocol ip pref 6 handle 901: u32 divisor 1", + "expExitCode": "0", + "verifyCmd": "$TC -d filter show dev $DEV1 ingress", + "matchPattern": "fh 901: ht divisor 1", + "matchCount": "1", + "teardown": [ + "$TC qdisc del dev $DEV1 ingress" + ] } ] diff --git a/tools/testing/selftests/tc-testing/tc-tests/qdiscs/codel.json b/tools/testing/selftests/tc-testing/tc-tests/qdiscs/codel.json index 6d515d0e5ed6..a894e6f0e267 100644 --- a/tools/testing/selftests/tc-testing/tc-tests/qdiscs/codel.json +++ b/tools/testing/selftests/tc-testing/tc-tests/qdiscs/codel.json @@ -213,5 +213,77 @@ "matchPattern": "qdisc codel 1: root refcnt [0-9]+ limit 1p target 5ms interval 100ms", "matchCount": "1", "teardown": ["$TC qdisc del dev $DEV1 handle 1: root"] + }, + { + "id": "6e44", + "name": "Create CODEL with 1us interval, accepted (sub-tick, uAPI locked)", + "category": [ + "qdisc", + "codel" + ], + "plugins": { + "requires": "nsPlugin" + }, + "setup": [], + "cmdUnderTest": "$TC qdisc add dev $DUMMY handle 1: root codel interval 1us", + "expExitCode": "0", + "verifyCmd": "$TC qdisc show dev $DUMMY", + "matchPattern": "qdisc codel 1: root refcnt [0-9]+ limit 1000p target 5ms interval 0us", + "matchCount": "1", + "teardown": ["$TC qdisc del dev $DUMMY handle 1: root"] + }, + { + "id": "a8c3", + "name": "Create CODEL with 3us interval, accepted (two ticks)", + "category": [ + "qdisc", + "codel" + ], + "plugins": { + "requires": "nsPlugin" + }, + "setup": [], + "cmdUnderTest": "$TC qdisc add dev $DUMMY handle 1: root codel interval 3us", + "expExitCode": "0", + "verifyCmd": "$TC qdisc show dev $DUMMY", + "matchPattern": "qdisc codel 1: root refcnt [0-9]+ limit 1000p target 5ms interval 2us", + "matchCount": "1", + "teardown": ["$TC qdisc del dev $DUMMY handle 1: root"] + }, + { + "id": "a695", + "name": "Create CODEL with 1024us interval boundary accepted", + "category": [ + "qdisc", + "codel" + ], + "plugins": { + "requires": "nsPlugin" + }, + "setup": [], + "cmdUnderTest": "$TC qdisc add dev $DUMMY handle 1: root codel interval 1024us", + "expExitCode": "0", + "verifyCmd": "$TC qdisc show dev $DUMMY", + "matchPattern": "qdisc codel 1: root refcnt [0-9]+ limit 1000p target 5ms interval 1.02ms", + "matchCount": "1", + "teardown": ["$TC qdisc del dev $DUMMY handle 1: root"] + }, + { + "id": "9793", + "name": "Create CODEL with 1us target, accepted (target not in control law)", + "category": [ + "qdisc", + "codel" + ], + "plugins": { + "requires": "nsPlugin" + }, + "setup": [], + "cmdUnderTest": "$TC qdisc add dev $DUMMY handle 1: root codel target 1us", + "expExitCode": "0", + "verifyCmd": "$TC qdisc show dev $DUMMY", + "matchPattern": "qdisc codel 1: root refcnt [0-9]+ limit 1000p target 0us interval 100ms", + "matchCount": "1", + "teardown": ["$TC qdisc del dev $DUMMY handle 1: root"] } ] diff --git a/tools/testing/selftests/tc-testing/tc-tests/qdiscs/fq_codel.json b/tools/testing/selftests/tc-testing/tc-tests/qdiscs/fq_codel.json index 4ce62b857fd7..de6a1b8d954a 100644 --- a/tools/testing/selftests/tc-testing/tc-tests/qdiscs/fq_codel.json +++ b/tools/testing/selftests/tc-testing/tc-tests/qdiscs/fq_codel.json @@ -316,5 +316,77 @@ "matchPattern": "qdisc fq_codel 1: root refcnt [0-9]+ limit 1p flows 1024 quantum.*target 5ms interval 100ms memory_limit 32Mb ecn drop_batch 64", "matchCount": "1", "teardown": ["$TC qdisc del dev $DEV1 handle 1: root"] + }, + { + "id": "1b4d", + "name": "Create FQ_CODEL with 1us interval, accepted (sub-tick, uAPI locked)", + "category": [ + "qdisc", + "fq_codel" + ], + "plugins": { + "requires": "nsPlugin" + }, + "setup": [], + "cmdUnderTest": "$TC qdisc add dev $DUMMY handle 1: root fq_codel interval 1us", + "expExitCode": "0", + "verifyCmd": "$TC qdisc show dev $DUMMY", + "matchPattern": "qdisc fq_codel 1: root refcnt [0-9]+ limit 10240p flows 1024 quantum [0-9]+ target 5ms interval 0us memory_limit 32Mb ecn drop_batch 64", + "matchCount": "1", + "teardown": ["$TC qdisc del dev $DUMMY handle 1: root"] + }, + { + "id": "3540", + "name": "Create FQ_CODEL with 3us interval, accepted (two ticks)", + "category": [ + "qdisc", + "fq_codel" + ], + "plugins": { + "requires": "nsPlugin" + }, + "setup": [], + "cmdUnderTest": "$TC qdisc add dev $DUMMY handle 1: root fq_codel interval 3us", + "expExitCode": "0", + "verifyCmd": "$TC qdisc show dev $DUMMY", + "matchPattern": "qdisc fq_codel 1: root refcnt [0-9]+ limit 10240p flows 1024 quantum [0-9]+ target 5ms interval 2us memory_limit 32Mb ecn drop_batch 64", + "matchCount": "1", + "teardown": ["$TC qdisc del dev $DUMMY handle 1: root"] + }, + { + "id": "49c5", + "name": "Create FQ_CODEL with 1024us interval boundary accepted", + "category": [ + "qdisc", + "fq_codel" + ], + "plugins": { + "requires": "nsPlugin" + }, + "setup": [], + "cmdUnderTest": "$TC qdisc add dev $DUMMY handle 1: root fq_codel interval 1024us", + "expExitCode": "0", + "verifyCmd": "$TC qdisc show dev $DUMMY", + "matchPattern": "qdisc fq_codel 1: root refcnt [0-9]+ limit 10240p flows 1024 quantum [0-9]+ target 5ms interval 1.02ms memory_limit 32Mb ecn drop_batch 64", + "matchCount": "1", + "teardown": ["$TC qdisc del dev $DUMMY handle 1: root"] + }, + { + "id": "3e0f", + "name": "Create FQ_CODEL with 1us target, accepted (target not in control law)", + "category": [ + "qdisc", + "fq_codel" + ], + "plugins": { + "requires": "nsPlugin" + }, + "setup": [], + "cmdUnderTest": "$TC qdisc add dev $DUMMY handle 1: root fq_codel target 1us", + "expExitCode": "0", + "verifyCmd": "$TC qdisc show dev $DUMMY", + "matchPattern": "qdisc fq_codel 1: root refcnt [0-9]+ limit 10240p flows 1024 quantum [0-9]+ target 0us interval 100ms memory_limit 32Mb ecn drop_batch 64", + "matchCount": "1", + "teardown": ["$TC qdisc del dev $DUMMY handle 1: root"] } ] diff --git a/tools/testing/selftests/tc-testing/tc-tests/qdiscs/hfsc.json b/tools/testing/selftests/tc-testing/tc-tests/qdiscs/hfsc.json index c98c339424d4..4f6bbb8b57f9 100644 --- a/tools/testing/selftests/tc-testing/tc-tests/qdiscs/hfsc.json +++ b/tools/testing/selftests/tc-testing/tc-tests/qdiscs/hfsc.json @@ -169,5 +169,39 @@ "teardown": [ "$TC qdisc del dev $DUMMY handle 1: root" ] + }, + { + "id": "8c39", + "name": "HFSC classify walk still reaches leaf after lateral drift", + "category": [ + "qdisc", + "hfsc" + ], + "plugins": { + "requires": "nsPlugin" + }, + "setup": [ + "ip link set lo up", + "$TC qdisc add dev lo handle 1: root hfsc default 30", + "$TC class add dev lo parent 1: classid 1:1 hfsc rt m2 100kbit", + "$TC class add dev lo parent 1:1 classid 1:10 hfsc rt m2 50kbit", + "$TC class add dev lo parent 1: classid 1:2 hfsc rt m2 100kbit", + "$TC filter add dev lo parent 1: protocol ip prio 1 u32 match u8 0 0 at 0 flowid 1:1", + "$TC filter add dev lo parent 1:1 protocol ip prio 1 u32 match u8 0 0 at 0 flowid 1:2", + "$TC class add dev lo parent 1:2 classid 1:20 hfsc rt m2 10kbit", + "$TC class add dev lo parent 1: classid 1:3 hfsc rt m2 100kbit", + "$TC filter add dev lo parent 1:2 protocol ip prio 1 u32 match u8 0 0 at 0 flowid 1:3", + "$TC class add dev lo parent 1:3 classid 1:30 hfsc rt m2 10kbit", + "$TC class add dev lo parent 1:3 classid 1:31 hfsc rt m2 100kbit", + "$TC filter add dev lo parent 1:3 protocol ip prio 1 u32 match u8 0 0 at 0 flowid 1:31" + ], + "cmdUnderTest": "ping -n -c 10 -W 1 127.0.0.1", + "expExitCode": "0", + "verifyCmd": "$TC -s class show dev lo", + "matchPattern": "class hfsc 1:31 parent 1:3 rt[^\\n]*\\n Sent [0-9]+ bytes [1-9][0-9]* pkt", + "matchCount": "1", + "teardown": [ + "$TC qdisc del dev lo handle 1: root" + ] } ] diff --git a/tools/testing/selftests/tc-testing/tc-tests/qdiscs/hhf_flows_limit.json b/tools/testing/selftests/tc-testing/tc-tests/qdiscs/hhf_flows_limit.json new file mode 100644 index 000000000000..44538b9266b6 --- /dev/null +++ b/tools/testing/selftests/tc-testing/tc-tests/qdiscs/hhf_flows_limit.json @@ -0,0 +1,128 @@ +[ + { + "id": "e3cc", + "name": "HHF hh_limit rejects value above 2*HH_FLOWS_CNT cap (4294967295)", + "category": [ + "qdisc", + "hhf" + ], + "plugins": { + "requires": "nsPlugin" + }, + "setup": [ + "$TC qdisc add dev $DUMMY handle 1: root hhf" + ], + "cmdUnderTest": "$TC qdisc change dev $DUMMY handle 1: root hhf hh_limit 4294967295", + "expExitCode": "2", + "verifyCmd": "$TC qdisc show dev $DUMMY", + "matchPattern": "qdisc hhf 1: root refcnt [0-9]+.*hh_limit 2048", + "matchCount": "1", + "teardown": [ + "$TC qdisc del dev $DUMMY handle 1: root" + ] + }, + { + "id": "f681", + "name": "HHF hh_limit rejects 65536 (above 2*HH_FLOWS_CNT cap)", + "category": [ + "qdisc", + "hhf" + ], + "plugins": { + "requires": "nsPlugin" + }, + "setup": [ + "$TC qdisc add dev $DUMMY handle 1: root hhf" + ], + "cmdUnderTest": "$TC qdisc change dev $DUMMY handle 1: root hhf hh_limit 65536", + "expExitCode": "2", + "verifyCmd": "$TC qdisc show dev $DUMMY", + "matchPattern": "qdisc hhf 1: root refcnt [0-9]+.*hh_limit 2048", + "matchCount": "1", + "teardown": [ + "$TC qdisc del dev $DUMMY handle 1: root" + ] + }, + { + "id": "223d", + "name": "HHF hh_limit accepts boundary value 2048 (2*HH_FLOWS_CNT)", + "category": [ + "qdisc", + "hhf" + ], + "plugins": { + "requires": "nsPlugin" + }, + "setup": [ + "$TC qdisc add dev $DUMMY handle 1: root hhf hh_limit 100" + ], + "cmdUnderTest": "$TC qdisc change dev $DUMMY handle 1: root hhf hh_limit 2048", + "expExitCode": "0", + "verifyCmd": "$TC qdisc show dev $DUMMY", + "matchPattern": "qdisc hhf 1: root refcnt [0-9]+.*hh_limit 2048", + "matchCount": "1", + "teardown": [ + "$TC qdisc del dev $DUMMY handle 1: root" + ] + }, + { + "id": "147f", + "name": "HHF hh_limit rejects first value above cap (2049)", + "category": [ + "qdisc", + "hhf" + ], + "plugins": { + "requires": "nsPlugin" + }, + "setup": [ + "$TC qdisc add dev $DUMMY handle 1: root hhf" + ], + "cmdUnderTest": "$TC qdisc change dev $DUMMY handle 1: root hhf hh_limit 2049", + "expExitCode": "2", + "verifyCmd": "$TC qdisc show dev $DUMMY", + "matchPattern": "qdisc hhf 1: root refcnt [0-9]+.*hh_limit 2048", + "matchCount": "1", + "teardown": [ + "$TC qdisc del dev $DUMMY handle 1: root" + ] + }, + { + "id": "4d4f", + "name": "HHF add-time hh_limit 500 is preserved (init does not clobber user value)", + "category": [ + "qdisc", + "hhf" + ], + "plugins": { + "requires": "nsPlugin" + }, + "setup": [], + "cmdUnderTest": "$TC qdisc add dev $DUMMY handle 1: root hhf hh_limit 500", + "expExitCode": "0", + "verifyCmd": "$TC qdisc show dev $DUMMY", + "matchPattern": "qdisc hhf 1: root refcnt [0-9]+.*hh_limit 500", + "matchCount": "1", + "teardown": [ + "$TC qdisc del dev $DUMMY handle 1: root" + ] + }, + { + "id": "ca99", + "name": "HHF add-time hh_limit 4294967295 is rejected (no qdisc installed)", + "category": [ + "qdisc", + "hhf" + ], + "plugins": { + "requires": "nsPlugin" + }, + "setup": [], + "cmdUnderTest": "$TC qdisc add dev $DUMMY handle 1: root hhf hh_limit 4294967295", + "expExitCode": "2", + "verifyCmd": "$TC qdisc show dev $DUMMY", + "matchPattern": "qdisc hhf 1: root", + "matchCount": "0", + "teardown": [] + } +] diff --git a/tools/testing/selftests/ublk/kublk.c b/tools/testing/selftests/ublk/kublk.c index 2400b4615766..5b19bfcfa8d7 100644 --- a/tools/testing/selftests/ublk/kublk.c +++ b/tools/testing/selftests/ublk/kublk.c @@ -1589,7 +1589,7 @@ fail_start: /* Signal listener thread to stop and wait for it */ if (linfo.stop_efd >= 0) { - write(linfo.stop_efd, &stop_val, sizeof(stop_val)); + (void)!write(linfo.stop_efd, &stop_val, sizeof(stop_val)); pthread_join(listener, NULL); close(linfo.stop_efd); ublk_shmem_sock_destroy(dinfo->dev_id, linfo.sock_fd); diff --git a/tools/testing/selftests/x86/Makefile b/tools/testing/selftests/x86/Makefile index 434065215d12..d478b13cc8d5 100644 --- a/tools/testing/selftests/x86/Makefile +++ b/tools/testing/selftests/x86/Makefile @@ -13,7 +13,7 @@ CAN_BUILD_WITH_NOPIE := $(shell ./check_cc.sh "$(CC)" trivial_program.c -no-pie) TARGETS_C_BOTHBITS := single_step_syscall sysret_ss_attrs syscall_nt test_mremap_vdso \ check_initial_reg_state sigreturn iopl ioperm \ test_vsyscall mov_ss_trap sigtrap_loop \ - syscall_arg_fault fsgsbase_restore sigaltstack + syscall_arg_fault fsgsbase_restore sigaltstack int_signal TARGETS_C_BOTHBITS += nx_stack TARGETS_C_32BIT_ONLY := entry_from_vm86 test_syscall_vdso unwind_vdso \ test_FCMOV test_FCOMI test_FISTTP \ diff --git a/tools/testing/selftests/x86/int_signal.c b/tools/testing/selftests/x86/int_signal.c new file mode 100644 index 000000000000..22676dac72b5 --- /dev/null +++ b/tools/testing/selftests/x86/int_signal.c @@ -0,0 +1,311 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* Check the signal context for INT instructions with IDT and FRED entry. */ +#define _GNU_SOURCE + +#include <cpuid.h> +#include <errno.h> +#include <stdbool.h> +#include <stddef.h> +#include <stdint.h> +#include <sys/ptrace.h> +#include <sys/user.h> +#include <sys/wait.h> +#include <unistd.h> +#include <ucontext.h> + +#include "helpers.h" + +#ifdef __x86_64__ +#define REG_IP REG_RIP +#define USER_IP rip +#define STACK_PTR "%rsp" +#else +#define REG_IP REG_EIP +#define USER_IP eip +#define STACK_PTR "%esp" +#endif + +/* + * Each instruction has normal and single-step entry points. Resume at the + * NOP after handling its signal, then expect a trace trap after that NOP + * when TF is set. Explicit labels avoid assuming the kernel's saved IP. + */ +#define PROBE(name, insn) \ + extern void name(void); \ + extern void name##_tf(void); \ + extern const char name##_end[], name##_step[]; \ + asm(".pushsection .text\n" \ + ".globl " #name "_tf\n" \ + ".type " #name "_tf, @function\n" \ + #name "_tf:\n" \ + "pushf\n" \ + "orl $0x100, (" STACK_PTR ")\n" \ + "popf\n" \ + ".globl " #name "\n" \ + ".type " #name ", @function\n" \ + #name ":\n" insn "\n" \ + ".globl " #name "_end\n" \ + #name "_end:\nnop\n" \ + ".globl " #name "_step\n" \ + #name "_step:\nret\n" \ + ".size " #name ", .-" #name "\n" \ + ".size " #name "_tf, .-" #name "_tf\n" \ + ".popsection\n") + +PROBE(int1, ".byte 0xcd, 0x01"); +PROBE(int29, ".byte 0xcd, 0x29"); +PROBE(int2c, ".byte 0xcd, 0x2c"); +PROBE(int2d, ".byte 0xcd, 0x2d"); +PROBE(prefixed_int2d, ".byte 0x66, 0xcd, 0x2d"); +PROBE(long_int2d, ".fill 13, 1, 0x2e\n.byte 0xcd, 0x2d"); +PROBE(int81, ".byte 0xcd, 0x81"); +PROBE(intff, ".byte 0xcd, 0xff"); +PROBE(short_int3, ".byte 0xcc"); +PROBE(long_int3, ".byte 0xcd, 0x03"); +PROBE(int4, ".byte 0xcd, 0x04"); +PROBE(ud2, ".byte 0x0f, 0x0b"); +PROBE(hlt, ".byte 0xf4"); + +struct test { + const char *name; + void (*run)(void); + void (*run_tf)(void); + const char *end, *step; + int signo, trap, error, ip_offset, flags, code; +}; + +#define TEST(name, sig, trap, error, offset, flags, code) \ + { #name, name, name##_tf, name##_end, name##_step, \ + sig, trap, error, offset, flags, code } + +#define GP(name, error) \ + TEST(name, SIGSEGV, 13, error, 0, X86_EFLAGS_RF, SI_KERNEL) + +static const struct test tests[] = { + GP(int1, 0x00a), + GP(int29, 0x14a), + GP(int2c, 0x162), + GP(int2d, 0x16a), + GP(prefixed_int2d, 0x16a), + GP(long_int2d, 0x16a), + GP(int81, 0x40a), + GP(intff, 0x7fa), + GP(hlt, 0), + TEST(short_int3, SIGTRAP, 3, 0, 1, 0, SI_KERNEL), + TEST(long_int3, SIGTRAP, 3, 0, 2, 0, SI_KERNEL), + TEST(int4, SIGSEGV, 4, 0, 2, 0, SI_KERNEL), + TEST(ud2, SIGILL, 6, 0, 0, X86_EFLAGS_RF, ILL_ILLOPN), +}; + +static const struct test *active; +static volatile sig_atomic_t seen, signo, trap, error, ip_offset, flags; +static volatile sig_atomic_t code, addr_ok, single_step, stepped, step_ok; + +static void handler(int sig, siginfo_t *info, void *context) +{ + ucontext_t *uc = context; + uintptr_t ip = uc->uc_mcontext.gregs[REG_IP]; + uintptr_t start = (uintptr_t)active->run; + uintptr_t end = (uintptr_t)active->end; + + if (seen && single_step && sig == SIGTRAP) { + if (stepped++) { + ksft_print_msg("%s: second trace trap at %#lx\n", + active->name, (unsigned long)ip); + _exit(KSFT_FAIL); + } + step_ok = ip == (uintptr_t)active->step && + uc->uc_mcontext.gregs[REG_TRAPNO] == 1 && + info->si_code == TRAP_TRACE; + uc->uc_mcontext.gregs[REG_EFL] &= ~X86_EFLAGS_TF; + return; + } + + if (seen || ip < start || ip > end) { + ksft_print_msg("%s: unexpected signal %d at %#lx\n", + active->name, sig, (unsigned long)ip); + _exit(KSFT_FAIL); + } + + signo = sig; + trap = uc->uc_mcontext.gregs[REG_TRAPNO]; + error = uc->uc_mcontext.gregs[REG_ERR]; + ip_offset = ip - start; + flags = uc->uc_mcontext.gregs[REG_EFL] & (X86_EFLAGS_RF | X86_EFLAGS_TF); + code = info->si_code; + /* force_sig() reports no address, force_sig_fault() reports the IP. */ + addr_ok = info->si_addr == (code == SI_KERNEL ? NULL : (void *)ip); + seen = 1; + uc->uc_mcontext.gregs[REG_IP] = end; +} + +static void wait_for_child(pid_t child, int *status) +{ + pid_t ret; + + do { + ret = waitpid(child, status, 0); + } while (ret < 0 && errno == EINTR); + if (ret != child) + ksft_exit_fail_perror("waitpid"); +} + +/* Resume the tracee and check where the next stop lands. */ +static bool resume_to(pid_t child, int *status, int request, int sig, + const void *ip, const char *what) +{ + struct user_regs_struct regs; + + if (ptrace(request, child, 0, 0)) + return false; + wait_for_child(child, status); + if (!WIFSTOPPED(*status)) { + ksft_print_msg("%s: tracee did not stop\n", what); + return false; + } + if (WSTOPSIG(*status) != sig) { + ksft_print_msg("%s: stopped with signal %d, expected %d\n", + what, WSTOPSIG(*status), sig); + return false; + } + if (ptrace(PTRACE_GETREGS, child, 0, ®s)) + return false; + if ((unsigned long)regs.USER_IP != (unsigned long)ip) { + ksft_print_msg("%s: stopped at %#lx, expected %#lx\n", what, + (unsigned long)regs.USER_IP, (unsigned long)ip); + return false; + } + return true; +} + +static bool set_ip(pid_t child, const void *ip, bool tf) +{ + struct user_regs_struct regs; + + if (ptrace(PTRACE_GETREGS, child, 0, ®s)) + return false; + regs.USER_IP = (unsigned long)ip; + if (tf) + regs.eflags |= X86_EFLAGS_TF; + return !ptrace(PTRACE_SETREGS, child, 0, ®s); +} + +/* + * Exercise the tracer paths that resume through the fault frame rather than + * sigreturn. A stale FRED software event flag on that frame traps before the + * NOP executes instead of after it. + */ +static void test_ptrace(void) +{ + bool into = false, step = false, cont = false; + pid_t child; + int status; + + child = fork(); + if (child < 0) + ksft_exit_fail_perror("fork"); + if (!child) { + if (ptrace(PTRACE_TRACEME, 0, 0, 0)) + _exit(KSFT_FAIL); + /* Start from a breakpoint frame, not the syscall frame of raise(). */ + asm volatile("int3"); + _exit(KSFT_FAIL); + } + + wait_for_child(child, &status); + if (!WIFSTOPPED(status) || WSTOPSIG(status) != SIGTRAP) + goto out; + if (ptrace(PTRACE_SETOPTIONS, child, 0, PTRACE_O_EXITKILL)) + goto out; + + /* Single-step into the INT. The fault must report the INT's address. */ + if (!set_ip(child, int2d, false)) + goto out; + into = resume_to(child, &status, PTRACE_SINGLESTEP, SIGSEGV, int2d, + "single-step into INT"); + if (!into) + goto out; + + /* Suppress SIGSEGV and single-step the NOP. */ + if (!set_ip(child, int2d_end, false)) + goto out; + step = resume_to(child, &status, PTRACE_SINGLESTEP, SIGTRAP, int2d_step, + "single-step after INT"); + if (!step) + goto out; + + /* Fault again, then suppress SIGSEGV and continue with TF set. */ + if (!set_ip(child, int2d, false)) + goto out; + if (!resume_to(child, &status, PTRACE_CONT, SIGSEGV, int2d, + "continue to INT")) + goto out; + if (!set_ip(child, int2d_end, true)) + goto out; + cont = resume_to(child, &status, PTRACE_CONT, SIGTRAP, int2d_step, + "continue with TF after INT"); +out: + if (WIFSTOPPED(status)) { + kill(child, SIGKILL); + wait_for_child(child, &status); + } + ksft_test_result(into, "ptrace single-step into INT faults at the INT\n"); + ksft_test_result(step, "ptrace single-step after suppressing SIGSEGV\n"); + ksft_test_result(cont, "ptrace continue with TF after suppressing SIGSEGV\n"); +} + +static bool cpu_has_fred(void) +{ + unsigned int eax, ebx, ecx, edx; + + if (__get_cpuid_max(0, NULL) < 7) + return false; + __cpuid_count(7, 1, eax, ebx, ecx, edx); + return eax & (1 << 17); +} + +int main(void) +{ + unsigned int i, tf; + int expected_flags, ok; + + ksft_print_header(); + ksft_set_plan(2 * ARRAY_SIZE(tests) + 3); + ksft_print_msg("CPU %s FRED\n", cpu_has_fred() ? "supports" : "lacks"); + sethandler(SIGSEGV, handler, 0); + sethandler(SIGTRAP, handler, 0); + sethandler(SIGILL, handler, 0); + + for (tf = 0; tf < 2; tf++) { + for (i = 0; i < ARRAY_SIZE(tests); i++) { + active = &tests[i]; + single_step = tf; + seen = signo = trap = error = ip_offset = flags = 0; + code = addr_ok = stepped = step_ok = 0; + expected_flags = active->flags | (tf ? X86_EFLAGS_TF : 0); + if (tf) + active->run_tf(); + else + active->run(); + + ok = seen && signo == active->signo && trap == active->trap && + error == active->error && ip_offset == active->ip_offset && + flags == expected_flags && code == active->code && addr_ok && + (!tf || (stepped && step_ok)); + ksft_test_result(ok, "%s%s\n", active->name, tf ? " with TF" : ""); + if (!ok) { + ksft_print_msg("got signal=%d trap=%d error=%#x ip=%d\n", + signo, trap, error, ip_offset); + ksft_print_msg("got flags=%#x code=%d addr_ok=%d step_ok=%d\n", + flags, code, addr_ok, step_ok); + ksft_print_msg("expected signal=%d trap=%d error=%#x ip=%d\n", + active->signo, active->trap, active->error, + active->ip_offset); + ksft_print_msg("expected flags=%#x code=%d\n", + expected_flags, active->code); + } + } + } + test_ptrace(); + ksft_finished(); +} diff --git a/tools/testing/vma/tests/vma.c b/tools/testing/vma/tests/vma.c index c8ef7b8cd46b..e973e0a6d1a8 100644 --- a/tools/testing/vma/tests/vma.c +++ b/tools/testing/vma/tests/vma.c @@ -40,7 +40,7 @@ static bool test_copy_vma(void) vma = alloc_and_link_vma(&mm, 0x1000, 0x2000, 1, vma_flags); vma_set_anonymous(vma); vma_orig = vma; - vma_new = copy_vma(&vma, 0x2000, 0x1000, 1, 1, &need_locks); + vma_new = copy_vma(&vma, 0x2000, 0x1000, 1, 1, &need_locks, false); ASSERT_EQ(vma_new, vma_orig); ASSERT_EQ(vma, vma_orig); ASSERT_EQ(vma_new->vm_start, 0x1000); @@ -53,7 +53,7 @@ static bool test_copy_vma(void) vma = alloc_and_link_vma(&mm, 0x2000, 0x3000, 2, vma_flags); vma_set_anonymous(vma); vma_orig = vma; - vma_new = copy_vma(&vma, 0x1000, 0x1000, 2, 2, &need_locks); + vma_new = copy_vma(&vma, 0x1000, 0x1000, 2, 2, &need_locks, false); ASSERT_EQ(vma_new, vma_orig); ASSERT_EQ(vma, vma_orig); ASSERT_EQ(vma_new->vm_start, 0x1000); @@ -71,7 +71,7 @@ static bool test_copy_vma(void) vma = alloc_and_link_vma(&mm, 0x3000, 0x4000, 3, vma_flags); vma_set_anonymous(vma); vma_orig = vma; - vma_new = copy_vma(&vma, 0x2000, 0x1000, 3, 3, &need_locks); + vma_new = copy_vma(&vma, 0x2000, 0x1000, 3, 3, &need_locks, false); ASSERT_NE(vma_new, vma_orig); ASSERT_EQ(vma_new, vma); ASSERT_EQ(vma_new->vm_start, 0x1000); @@ -82,7 +82,7 @@ static bool test_copy_vma(void) /* Move backwards and do not merge. */ vma = alloc_and_link_vma(&mm, 0x3000, 0x5000, 3, vma_flags); - vma_new = copy_vma(&vma, 0, 0x2000, 0, 3, &need_locks); + vma_new = copy_vma(&vma, 0, 0x2000, 0, 3, &need_locks, false); ASSERT_NE(vma_new, vma); ASSERT_EQ(vma_new->vm_start, 0); ASSERT_EQ(vma_new->vm_end, 0x2000); @@ -95,7 +95,7 @@ static bool test_copy_vma(void) vma = alloc_and_link_vma(&mm, 0, 0x2000, 0, vma_flags); vma_next = alloc_and_link_vma(&mm, 0x6000, 0x8000, 6, vma_flags); - vma_new = copy_vma(&vma, 0x4000, 0x2000, 4, 4, &need_locks); + vma_new = copy_vma(&vma, 0x4000, 0x2000, 4, 4, &need_locks, false); vma_assert_attached(vma_new); ASSERT_EQ(vma_new, vma_next); |
