summaryrefslogtreecommitdiff
path: root/tools
diff options
context:
space:
mode:
authorPaolo Bonzini <pbonzini@redhat.com>2026-10-15 10:11:08 +0200
committerPaolo Bonzini <pbonzini@redhat.com>2026-10-15 10:11:08 +0200
commit8edffbc1f6272db167037e1a8c49b99e9cd04803 (patch)
treeb01a0cab46b5ce160820a85be5c1e98899860723 /tools
parent439d8f9cb01cb04f9b31910efd0ae8dbdfa4ad97 (diff)
parent34f4ce22cf25802d8026c60c13c2db81f8c8e3a0 (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')
-rw-r--r--tools/arch/riscv/include/asm/csr.h20
-rw-r--r--tools/lib/bpf/libbpf.c7
-rw-r--r--tools/lib/bpf/relo_core.c58
-rw-r--r--tools/objtool/Makefile8
-rw-r--r--tools/sched_ext/include/scx/common.bpf.h3
-rw-r--r--tools/sched_ext/scx_qmap.bpf.c165
-rw-r--r--tools/sched_ext/scx_qmap.h3
-rw-r--r--tools/testing/selftests/Makefile9
-rw-r--r--tools/testing/selftests/arm64/mte/check_gcr_el1_cswitch.c2
-rw-r--r--tools/testing/selftests/bpf/prog_tests/bpf_ma_ttrace.c60
-rw-r--r--tools/testing/selftests/bpf/prog_tests/bpf_mod_race.c34
-rw-r--r--tools/testing/selftests/bpf/prog_tests/cb_refs.c2
-rw-r--r--tools/testing/selftests/bpf/prog_tests/core_reloc_raw.c192
-rw-r--r--tools/testing/selftests/bpf/prog_tests/dynptr.c10
-rw-r--r--tools/testing/selftests/bpf/prog_tests/exceptions.c1
-rw-r--r--tools/testing/selftests/bpf/prog_tests/linked_list.c4
-rw-r--r--tools/testing/selftests/bpf/prog_tests/local_kptr_ownership.c296
-rw-r--r--tools/testing/selftests/bpf/prog_tests/percpu_alloc.c105
-rw-r--r--tools/testing/selftests/bpf/prog_tests/sock_destroy.c121
-rw-r--r--tools/testing/selftests/bpf/prog_tests/spin_lock.c2
-rw-r--r--tools/testing/selftests/bpf/prog_tests/tc_change_tail_pmtu.c125
-rw-r--r--tools/testing/selftests/bpf/prog_tests/tramp_prog_detach.c188
-rw-r--r--tools/testing/selftests/bpf/prog_tests/verifier.c4
-rw-r--r--tools/testing/selftests/bpf/progs/bpf_ma_ttrace.c50
-rw-r--r--tools/testing/selftests/bpf/progs/dynptr_success.c20
-rw-r--r--tools/testing/selftests/bpf/progs/exceptions.c30
-rw-r--r--tools/testing/selftests/bpf/progs/iters_state_safety.c22
-rw-r--r--tools/testing/selftests/bpf/progs/sock_destroy_prog.c30
-rw-r--r--tools/testing/selftests/bpf/progs/test_spin_lock_fail.c67
-rw-r--r--tools/testing/selftests/bpf/progs/test_tc_change_tail_pmtu.c129
-rw-r--r--tools/testing/selftests/bpf/progs/tramp_prog_detach.c56
-rw-r--r--tools/testing/selftests/bpf/progs/verifier_align.c6
-rw-r--r--tools/testing/selftests/bpf/progs/verifier_arena.c49
-rw-r--r--tools/testing/selftests/bpf/progs/verifier_async_cb_context.c106
-rw-r--r--tools/testing/selftests/bpf/progs/verifier_btf_flex_array.c56
-rw-r--r--tools/testing/selftests/bpf/progs/verifier_direct_packet_access.c178
-rw-r--r--tools/testing/selftests/bpf/progs/verifier_global_ptr_args.c53
-rw-r--r--tools/testing/selftests/bpf/progs/verifier_gotox.c48
-rw-r--r--tools/testing/selftests/bpf/progs/verifier_kfunc_perfmon.c75
-rw-r--r--tools/testing/selftests/bpf/progs/verifier_loops1.c36
-rw-r--r--tools/testing/selftests/bpf/progs/verifier_meta_access.c30
-rw-r--r--tools/testing/selftests/bpf/progs/verifier_sock.c38
-rw-r--r--tools/testing/selftests/bpf/progs/verifier_xdp_direct_packet_access.c35
-rw-r--r--tools/testing/selftests/bpf/testing_helpers.c28
-rw-r--r--tools/testing/selftests/bpf/testing_helpers.h2
-rwxr-xr-xtools/testing/selftests/cgroup/test_cpuset_prs.sh2
-rw-r--r--tools/testing/selftests/cgroup/test_memcontrol.c2
-rw-r--r--tools/testing/selftests/cgroup/test_zswap.c28
-rw-r--r--tools/testing/selftests/drivers/net/config1
-rwxr-xr-xtools/testing/selftests/drivers/net/mlxsw/port_range_occ.sh26
-rwxr-xr-xtools/testing/selftests/drivers/net/psp.py46
-rw-r--r--tools/testing/selftests/filesystems/.gitignore1
-rw-r--r--tools/testing/selftests/filesystems/Makefile2
-rw-r--r--tools/testing/selftests/filesystems/empty_mntns/.gitignore3
-rw-r--r--tools/testing/selftests/filesystems/empty_mntns/Makefile5
-rw-r--r--tools/testing/selftests/filesystems/empty_mntns/internal_sb_reconfigure_test.c108
-rw-r--r--tools/testing/selftests/filesystems/empty_mntns/nullfs_atime_test.c129
-rw-r--r--tools/testing/selftests/filesystems/empty_mntns/root_readdir_test.c45
-rw-r--r--tools/testing/selftests/filesystems/mntns_cleanup/.gitignore2
-rw-r--r--tools/testing/selftests/filesystems/mntns_cleanup/mntns_cleanup_test.c58
-rw-r--r--tools/testing/selftests/filesystems/mntns_unbindable/Makefile (renamed from tools/testing/selftests/filesystems/mntns_cleanup/Makefile)2
-rw-r--r--tools/testing/selftests/filesystems/mntns_unbindable/mntns_unbindable_test.c227
-rw-r--r--tools/testing/selftests/filesystems/mount_cycle/.gitignore8
-rw-r--r--tools/testing/selftests/filesystems/mount_cycle/Makefile12
-rw-r--r--tools/testing/selftests/filesystems/mount_cycle/config39
-rw-r--r--tools/testing/selftests/filesystems/mount_cycle/locked_handle_test.c377
-rw-r--r--tools/testing/selftests/filesystems/mount_cycle/loop_cycle_test.c1542
-rw-r--r--tools/testing/selftests/filesystems/mount_cycle/mount_cover_test.c579
-rw-r--r--tools/testing/selftests/filesystems/mount_cycle/nsfs_rbind_loop_test.c193
-rw-r--r--tools/testing/selftests/filesystems/mount_cycle/overmount_ns_file_test.c190
-rw-r--r--tools/testing/selftests/filesystems/mount_cycle/overmount_reparent_test.c181
-rw-r--r--tools/testing/selftests/filesystems/mount_cycle/settings1
-rw-r--r--tools/testing/selftests/filesystems/mount_cycle/unmounted_tree_test.c484
-rw-r--r--tools/testing/selftests/filesystems/open_tree_ns/.gitignore1
-rw-r--r--tools/testing/selftests/filesystems/open_tree_ns/Makefile2
-rw-r--r--tools/testing/selftests/filesystems/open_tree_ns/open_tree_ns_covered_test.c195
-rw-r--r--tools/testing/selftests/filesystems/overlayfs/.gitignore1
-rw-r--r--tools/testing/selftests/filesystems/overlayfs/Makefile1
-rw-r--r--tools/testing/selftests/filesystems/overlayfs/automount_in_layer.c174
-rw-r--r--tools/testing/selftests/filesystems/readdir_hold.h224
-rw-r--r--tools/testing/selftests/filesystems/rw_hint_test.c129
-rw-r--r--tools/testing/selftests/filesystems/umount_propagation/Makefile6
-rw-r--r--tools/testing/selftests/filesystems/umount_propagation/locked_mount_test.c432
-rw-r--r--tools/testing/selftests/filesystems/umount_propagation/shrink_submounts_test.c211
-rw-r--r--tools/testing/selftests/filesystems/umount_propagation/umount_propagation_test.c226
-rw-r--r--tools/testing/selftests/ftrace/test.d/kprobe/kprobe_non_uniq_symbol.tc2
-rw-r--r--tools/testing/selftests/hid/hid_bpf.c122
-rw-r--r--tools/testing/selftests/hid/hid_common.h25
-rw-r--r--tools/testing/selftests/hid/progs/hid.c2
-rw-r--r--tools/testing/selftests/kvm/Makefile.kvm1
-rw-r--r--tools/testing/selftests/kvm/hardware_disable_test.c6
-rw-r--r--tools/testing/selftests/kvm/include/loongarch/processor.h3
-rw-r--r--tools/testing/selftests/kvm/lib/loongarch/processor.c16
-rw-r--r--tools/testing/selftests/kvm/x86/nested_x2apic_test.c222
-rw-r--r--tools/testing/selftests/landlock/fs_test.c28
-rw-r--r--tools/testing/selftests/landlock/net_test.c295
-rw-r--r--tools/testing/selftests/landlock/trace.h59
-rw-r--r--tools/testing/selftests/landlock/trace_fs_test.c130
-rw-r--r--tools/testing/selftests/landlock/trace_test.c51
-rw-r--r--tools/testing/selftests/mm/hugetlb-mmap.c4
-rw-r--r--tools/testing/selftests/move_mount_set_group/move_mount_set_group_test.c74
-rw-r--r--tools/testing/selftests/nci/nci_dev.c45
-rw-r--r--tools/testing/selftests/net/.gitignore1
-rw-r--r--tools/testing/selftests/net/Makefile1
-rw-r--r--tools/testing/selftests/net/af_unix/scm_rights.c17
-rwxr-xr-xtools/testing/selftests/net/amt.sh29
-rw-r--r--tools/testing/selftests/net/config2
-rwxr-xr-xtools/testing/selftests/net/nl_nlctrl.py116
-rw-r--r--tools/testing/selftests/net/ovpn/common.sh13
-rw-r--r--tools/testing/selftests/net/ovpn/ovpn-cli.c54
-rwxr-xr-xtools/testing/selftests/net/ovpn/test.sh75
-rw-r--r--tools/testing/selftests/net/packetdrill/config2
-rw-r--r--tools/testing/selftests/net/packetdrill/tcp_old_ack_ts.pkt22
-rw-r--r--tools/testing/selftests/net/packetdrill/tcp_rfc5961_reject-old-ack.pkt29
-rw-r--r--tools/testing/selftests/net/tun.c136
-rw-r--r--tools/testing/selftests/net/udp_splice_checksum.c378
-rwxr-xr-xtools/testing/selftests/net/veth.sh46
-rw-r--r--tools/testing/selftests/sched_ext/Makefile3
-rw-r--r--tools/testing/selftests/sched_ext/dequeue_iter.bpf.c73
-rw-r--r--tools/testing/selftests/sched_ext/dequeue_iter.c79
-rw-r--r--tools/testing/selftests/sched_ext/dequeue_remote.bpf.c270
-rw-r--r--tools/testing/selftests/sched_ext/dequeue_remote.c204
-rw-r--r--tools/testing/selftests/sched_ext/enable_cmask.bpf.c217
-rw-r--r--tools/testing/selftests/sched_ext/enable_cmask.c138
-rw-r--r--tools/testing/selftests/tc-testing/tc-tests/actions/batch-delete.json115
-rw-r--r--tools/testing/selftests/tc-testing/tc-tests/filters/route.json46
-rw-r--r--tools/testing/selftests/tc-testing/tc-tests/filters/u32.json48
-rw-r--r--tools/testing/selftests/tc-testing/tc-tests/qdiscs/codel.json72
-rw-r--r--tools/testing/selftests/tc-testing/tc-tests/qdiscs/fq_codel.json72
-rw-r--r--tools/testing/selftests/tc-testing/tc-tests/qdiscs/hfsc.json34
-rw-r--r--tools/testing/selftests/tc-testing/tc-tests/qdiscs/hhf_flows_limit.json128
-rw-r--r--tools/testing/selftests/ublk/kublk.c2
-rw-r--r--tools/testing/selftests/x86/Makefile2
-rw-r--r--tools/testing/selftests/x86/int_signal.c311
-rw-r--r--tools/testing/vma/tests/vma.c10
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, &reg) || 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,
+ &param_ipaddr4_outer_src,
+ &param_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 *)&param_ipaddr4_outer_src,
+ sizeof(param_ipaddr4_outer_src));
+ sum += add_csum((const uint8_t *)&param_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, &param))
+ 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, &regs))
+ 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, &regs))
+ return false;
+ regs.USER_IP = (unsigned long)ip;
+ if (tf)
+ regs.eflags |= X86_EFLAGS_TF;
+ return !ptrace(PTRACE_SETREGS, child, 0, &regs);
+}
+
+/*
+ * 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);