From c1376370079e2d594615bcb4124b620d2e033fe6 Mon Sep 17 00:00:00 2001 From: Auke Kok Date: Thu, 14 May 2026 14:42:26 -0700 Subject: [PATCH] We can't cross-mount ipv4/ipv6. I thought this would just work between ipv4 and ipv6 based quorum members, but it turns out it will only work one way by default. While we could make this work (multiple sockets, special sockopts) it is highly unlikely and very undesirable. Much stronger feels to just disallow it explicitly and reject mixed v4/v6 configurations outright (mkfs/change-quorum, and mount) to avoid this. I can't imagine this doing any good for users' fencing setups. The test cases added validate the 2 easy userspace checks. The mount check isn't easily testable because we disallow userspace from creating such a failure path. One additional test section tests the migration path from v4->v6->v4 so there's at least some test that checks that this actually works. Signed-off-by: Auke Kok --- kmod/src/quorum.c | 17 ++++++++ tests/golden/basic-inetaddr | 7 ++++ tests/sequence | 1 + tests/tests/basic-inetaddr.sh | 78 +++++++++++++++++++++++++++++++++++ utils/src/quorum.c | 17 ++++++++ 5 files changed, 120 insertions(+) create mode 100644 tests/golden/basic-inetaddr create mode 100644 tests/tests/basic-inetaddr.sh diff --git a/kmod/src/quorum.c b/kmod/src/quorum.c index 9ef19276..08d5b9d3 100644 --- a/kmod/src/quorum.c +++ b/kmod/src/quorum.c @@ -1244,6 +1244,7 @@ static int verify_quorum_slots(struct super_block *sb, struct quorum_info *qinf, struct sockaddr_in *other4; struct sockaddr_in6 *sin6; struct sockaddr_in6 *other6; + __le16 family = cpu_to_le16(SCOUTFS_AF_NONE); int found = 0; int ret; int i; @@ -1255,6 +1256,14 @@ static int verify_quorum_slots(struct super_block *sb, struct quorum_info *qinf, continue; if (quorum_slot_ipv4(qconf, i)) { + if (family == cpu_to_le16(SCOUTFS_AF_NONE)) { + family = cpu_to_le16(SCOUTFS_AF_IPV4); + } else if (family != cpu_to_le16(SCOUTFS_AF_IPV4)) { + scoutfs_err(sb, "quorum slot #%d is IPv4 but earlier slots are IPv6; mixed IPv4/IPv6 quorum is not supported", + i); + return -EINVAL; + } + scoutfs_quorum_slot_sin(qconf, i, &sin); sin4 = (struct sockaddr_in *)&sin; @@ -1287,6 +1296,14 @@ static int verify_quorum_slots(struct super_block *sb, struct quorum_info *qinf, found++; } else if (quorum_slot_ipv6(qconf, i)) { + if (family == cpu_to_le16(SCOUTFS_AF_NONE)) { + family = cpu_to_le16(SCOUTFS_AF_IPV6); + } else if (family != cpu_to_le16(SCOUTFS_AF_IPV6)) { + scoutfs_err(sb, "quorum slot #%d is IPv6 but earlier slots are IPv4; mixed IPv4/IPv6 quorum is not supported", + i); + return -EINVAL; + } + quorum_slot_sin(qconf, i, &sin); sin6 = (struct sockaddr_in6 *)&sin; diff --git a/tests/golden/basic-inetaddr b/tests/golden/basic-inetaddr new file mode 100644 index 00000000..02d4d06b --- /dev/null +++ b/tests/golden/basic-inetaddr @@ -0,0 +1,7 @@ +== mkfs rejects mixed v4/v6 quorum +rc: 64 +== mkfs all-v4, mount three members, cross-mount signature visible +== change-quorum-config rejects mixed v4/v6 quorum +rc: 64 +== switch v4 -> v6, signature survives, cross-mount write again +== switch v6 -> v4, signatures survive diff --git a/tests/sequence b/tests/sequence index 909e665a..671bcb80 100644 --- a/tests/sequence +++ b/tests/sequence @@ -1,6 +1,7 @@ export-get-name-parent.sh basic-block-counts.sh basic-bad-mounts.sh +basic-inetaddr.sh basic-posix-acl.sh basic-acl-consistency.sh inode-items-updated.sh diff --git a/tests/tests/basic-inetaddr.sh b/tests/tests/basic-inetaddr.sh new file mode 100644 index 00000000..67131896 --- /dev/null +++ b/tests/tests/basic-inetaddr.sh @@ -0,0 +1,78 @@ +# +# Test that mixed ipv4/6 fails through mkfs/quorum change and that +# users can migrate from ipv4 to v6 and back. +# + +t_require_commands dmsetup blockdev cmp + +P0=$T_SCRATCH_PORT +P1=$((T_SCRATCH_PORT + 1)) +P2=$((T_SCRATCH_PORT + 2)) +SIG=$T_TMP.sig +seq 1 4096 > "$SIG" + +trap ' + umount $T_TMPDIR/m0 $T_TMPDIR/m1 $T_TMPDIR/m2 2>/dev/null + dmsetup remove _bia_m0 _bia_m1 _bia_m2 _bia_d0 _bia_d1 _bia_d2 2>/dev/null +' EXIT + +mkdir -p "$T_TMPDIR/m0" "$T_TMPDIR/m1" "$T_TMPDIR/m2" +for nv in "m0 $T_EX_META_DEV" "m1 $T_EX_META_DEV" "m2 $T_EX_META_DEV" \ + "d0 $T_EX_DATA_DEV" "d1 $T_EX_DATA_DEV" "d2 $T_EX_DATA_DEV"; do + set -- $nv + t_quiet dmsetup create _bia_$1 --table "0 $(blockdev --getsz $2) linear $2 0" +done + +mnt() { +mount -t scoutfs \ + -o metadev_path=/dev/mapper/_bia_m$1,quorum_slot_nr=$1 \ + /dev/mapper/_bia_d$1 "$T_TMPDIR/m$1" +} +mount_all() { + mnt 0 & + mnt 1 & + mnt 2 & + wait +} +umount_all() { + umount $T_TMPDIR/m0 & + umount $T_TMPDIR/m1 & + umount $T_TMPDIR/m2 & + wait +} +verify() { + cmp -s "$SIG" "$T_TMPDIR/m0/sig" && + cmp -s "$SIG" "$T_TMPDIR/m1/sig" && + cmp -s "$SIG" "$T_TMPDIR/m2/sig" || t_fail "$1" +} + +echo "== mkfs rejects mixed v4/v6 quorum" +t_rc scoutfs mkfs -f -Q 0,127.0.0.1,$P0 -Q 1,::1,$P1 -Q 2,127.0.0.1,$P2 /dev/mapper/_bia_m0 /dev/mapper/_bia_d0 + +echo "== mkfs all-v4, mount three members, cross-mount signature visible" +t_quiet scoutfs mkfs -f -Q 0,127.0.0.1,$P0 -Q 1,127.0.0.1,$P1 -Q 2,127.0.0.1,$P2 /dev/mapper/_bia_m0 /dev/mapper/_bia_d0 +mount_all +cp "$SIG" "$T_TMPDIR/m0/sig" +verify "v4 initial" +umount_all + +echo "== change-quorum-config rejects mixed v4/v6 quorum" +t_rc scoutfs change-quorum-config --offline -Q 0,127.0.0.1,$P0 -Q 1,::1,$P1 -Q 2,127.0.0.1,$P2 /dev/mapper/_bia_m0 + +echo "== switch v4 -> v6, signature survives, cross-mount write again" +t_quiet scoutfs change-quorum-config --offline -Q 0,::1,$P0 -Q 1,::1,$P1 -Q 2,::1,$P2 /dev/mapper/_bia_m0 +mount_all +verify "after v4->v6" +cp "$SIG" "$T_TMPDIR/m1/sig-v6" +cmp -s "$SIG" "$T_TMPDIR/m0/sig-v6" || t_fail "v6 cross-mount write not visible on m0" +cmp -s "$SIG" "$T_TMPDIR/m2/sig-v6" || t_fail "v6 cross-mount write not visible on m2" +umount_all + +echo "== switch v6 -> v4, signatures survive" +t_quiet scoutfs change-quorum-config --offline -Q 0,127.0.0.1,$P0 -Q 1,127.0.0.1,$P1 -Q 2,127.0.0.1,$P2 /dev/mapper/_bia_m0 +mount_all +verify "after v6->v4" +cmp -s "$SIG" "$T_TMPDIR/m0/sig-v6" || t_fail "after v6->v4 sig-v6 lost" +umount_all + +t_pass diff --git a/utils/src/quorum.c b/utils/src/quorum.c index b2b13565..3748b120 100644 --- a/utils/src/quorum.c +++ b/utils/src/quorum.c @@ -20,11 +20,20 @@ bool valid_quorum_slots(struct scoutfs_quorum_slot *slots) bool valid = true; char *addr; char ip6addr[INET6_ADDRSTRLEN]; + __le16 family = cpu_to_le16(SCOUTFS_AF_NONE); int i; int j; for (i = 0; i < SCOUTFS_QUORUM_MAX_SLOTS; i++) { if (slots[i].addr.v4.family == cpu_to_le16(SCOUTFS_AF_IPV4)) { + if (family == cpu_to_le16(SCOUTFS_AF_NONE)) { + family = cpu_to_le16(SCOUTFS_AF_IPV4); + } else if (family != cpu_to_le16(SCOUTFS_AF_IPV4)) { + fprintf(stderr, "quorum slot nr %u is IPv4 but earlier slots are IPv6; mixed IPv4/IPv6 quorum is not supported\n", + i); + valid = false; + } + for (j = i + 1; j < SCOUTFS_QUORUM_MAX_SLOTS; j++) { if (slots[i].addr.v4.addr == slots[j].addr.v4.addr && slots[i].addr.v4.port == slots[j].addr.v4.port) { @@ -38,6 +47,14 @@ bool valid_quorum_slots(struct scoutfs_quorum_slot *slots) } } } else if (slots[i].addr.v6.family == cpu_to_le16(SCOUTFS_AF_IPV6)) { + if (family == cpu_to_le16(SCOUTFS_AF_NONE)) { + family = cpu_to_le16(SCOUTFS_AF_IPV6); + } else if (family != cpu_to_le16(SCOUTFS_AF_IPV6)) { + fprintf(stderr, "quorum slot nr %u is IPv6 but earlier slots are IPv4; mixed IPv4/IPv6 quorum is not supported\n", + i); + valid = false; + } + for (j = i + 1; j < SCOUTFS_QUORUM_MAX_SLOTS; j++) { if ((IN6_ARE_ADDR_EQUAL(slots[i].addr.v6.addr, slots[j].addr.v6.addr)) && (slots[i].addr.v6.port == slots[j].addr.v6.port)) {