diff --git a/sys/netpfil/pf/pf.c b/sys/netpfil/pf/pf.c --- a/sys/netpfil/pf/pf.c +++ b/sys/netpfil/pf/pf.c @@ -9980,6 +9980,21 @@ pd->act.dnpipe = tmp; } + /* + * If the output interface does not accept unmapped mbufs, convert + * them to mapped mbufs. + */ + if ((ifp->if_capenable & IFCAP_MEXTPG) == 0) { + error = mb_unmapped_to_ext(m0, &md); + if (error) + goto done; + /* + * The first mbuf should not be reallocated because it is + * always mapped. + */ + MPASS(m0 == md); + } + /* * If small enough for interface, or the interface will take * care of the fragmentation for us, we can just send directly. @@ -10321,6 +10336,11 @@ } if ((u_long)m0->m_pkthdr.len <= ifp->if_mtu) { + if ((ifp->if_capenable & IFCAP_MEXTPG) == 0) { + if (mb_unmapped_to_ext(m0, &md) != 0) + goto done; + MPASS(m0 == md); + } md = m0; pf_dummynet_route(pd, s, r, ifp, sintosa(&dst), &md); if (md != NULL) { @@ -11585,6 +11605,7 @@ { struct pfi_kkif *kif; u_short action, reason = 0; + struct mbuf *m; struct m_tag *mtag; struct pf_krule *a = NULL, *r = &V_pf_default_rule; struct pf_kstate *s = NULL; @@ -11593,6 +11614,7 @@ struct pf_krule_slist match_rules; struct pf_pdesc pd; int use_2nd_queue = 0; + bool mapped_done = false; uint16_t tag; PF_RULES_RLOCK_TRACKER; @@ -11620,6 +11642,13 @@ } if (__predict_false(! M_WRITABLE(*m0))) { + /* Need to convert unmapped mbufs before calling m_unshare(). */ + if (mb_unmapped_to_ext(*m0, &m) != 0) { + *m0 = NULL; + return (PF_DROP); + } + MPASS(*m0 == m); + mapped_done = true; *m0 = m_unshare(*m0, M_NOWAIT); if (*m0 == NULL) { return (PF_DROP); @@ -11639,6 +11668,16 @@ *m0 = NULL; return (PF_PASS); } + + if (!mapped_done && (ifp->if_capenable & IFCAP_MEXTPG) == 0) { + if (mb_unmapped_to_ext(*m0, &m) != 0) { + /* *m0 freed in mb_unmapped_to_ext(). */ + *m0 = NULL; + return (PF_PASS); + } + MPASS(*m0 == m); + } + (ifp->if_output)(ifp, *m0, sintosa(&pd.pf_mtag->dst), NULL); *m0 = NULL; return (PF_PASS); diff --git a/tests/sys/netpfil/pf/Makefile b/tests/sys/netpfil/pf/Makefile --- a/tests/sys/netpfil/pf/Makefile +++ b/tests/sys/netpfil/pf/Makefile @@ -56,7 +56,8 @@ synproxy \ table \ tcp \ - tos + tos \ + unmapped_mbuf ATF_TESTS_PYTEST+= frag4.py ATF_TESTS_PYTEST+= frag6.py @@ -71,6 +72,10 @@ ATF_TESTS_PYTEST+= sctp.py ATF_TESTS_PYTEST+= tcp.py +BINDIR= ${TESTSDIR} +PROGS+= sendfile_helper +LIBADD.sendfile_helper = md + # Allow tests to run in parallel in their own jails TEST_METADATA+= execenv="jail" TEST_METADATA+= execenv_jail_params="vnet allow.raw_sockets allow.read_msgbuf" diff --git a/tests/sys/netpfil/pf/sendfile_helper.c b/tests/sys/netpfil/pf/sendfile_helper.c new file mode 100644 --- /dev/null +++ b/tests/sys/netpfil/pf/sendfile_helper.c @@ -0,0 +1,136 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2026 KUROSAWA Takahiro + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static void +usage(void) +{ + fprintf(stderr, "Usage: sendfile_helper host port filesize\n"); + exit(1); +} + +static void +getfile(size_t fsize, int *fdp, char **hashp) +{ + char tmpf[] = "/tmp/sendfileXXXXXXXX"; + void *addr; + char *hash; + ssize_t ssz; + int fd; + + if ((fd = mkstemp(tmpf)) < 0) + err(1, "mkstemp"); + unlink(tmpf); + if (ftruncate(fd, fsize) < 0) + err(1, "ftruncate"); + + if ((addr = mmap(NULL, fsize, PROT_READ|PROT_WRITE, + MAP_SHARED, fd, 0)) == (void *)-1) + err(1, "mmap"); + if ((ssz = getrandom(addr, fsize, 0)) < 0) + err(1, "getrandom"); + else if ((size_t)ssz != fsize) + errx(1, "getrandom() size mismatch"); + if ((hash = SHA256_Data(addr, fsize, NULL)) == NULL) + err(1, "SHA256_Data"); + + *fdp = fd; + *hashp = hash; +} + +int +main(int argc, char *argv[]) +{ + struct addrinfo hints, *res, *res0; + struct sf_hdtr hdtr; + struct iovec iov; + size_t fsize; + char *hash; + int opt; + int error; + int fd; + int s; + + if (argc != 4) + usage(); + + fsize = atoi(argv[3]); + if (fsize == 0) + errx(1, "Wrong file size: %s.", argv[3]); + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + if ((error = getaddrinfo(argv[1], argv[2], &hints, &res0)) != 0) + errx(1, "%s:%s: %s.", argv[1], argv[2], gai_strerror(error)); + s = -1; + for (res = res0; res != NULL; res = res->ai_next) { + s = socket(res->ai_family, res->ai_socktype, + res->ai_protocol); + if (s < 0) + err(1, "socket"); + if (connect(s, res->ai_addr, res->ai_addrlen) < 0) { + warn("connect(%s:%s)", argv[1], argv[2]); + close(s); + s = -1; + } else + break; + } + if (s < 0) + exit(1); + + opt = 1; + if (setsockopt(s, IPPROTO_TCP, TCP_NODELAY, &opt, sizeof(opt)) < 0) + warn("setsockopt(TCP_NODELAY, 1)"); + + getfile(fsize, &fd, &hash); + + iov.iov_base = hash; + iov.iov_len = strlen(hash); + memset(&hdtr, 0, sizeof(hdtr)); + hdtr.headers = &iov; + hdtr.hdr_cnt = 1; + if (sendfile(fd, s, 0, 0, &hdtr, NULL, 0) < 0) + err(1, "sendfile"); + + return 0; +} diff --git a/tests/sys/netpfil/pf/unmapped_mbuf.sh b/tests/sys/netpfil/pf/unmapped_mbuf.sh new file mode 100644 --- /dev/null +++ b/tests/sys/netpfil/pf/unmapped_mbuf.sh @@ -0,0 +1,220 @@ +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2026 KUROSAWA Takahiro +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. + +. $(atf_get_srcdir)/utils.subr + +common_dir=$(atf_get_srcdir)/../common + +ovpn_dir=$(atf_get_srcdir)/../../net/if_ovpn +. ${ovpn_dir}/utils.subr + +ifa_v4() { + local j=$1 + local iface=$2 + + jexec $j ifconfig $iface | awk '/^[[:space:]]inet /{print $2}' +} + +ifa_v6() { + local j=$1 + local iface=$2 + + jexec $j ifconfig $iface | awk '/^[[:space:]]inet6 2001/{print $2}' +} + +# pf must call mb_unmapped_to_ext() before passing an output mbuf to a network +# interface if it does not accept unmapped mbufs as ip_output() does. These tests +# make sure that unmapped mbufs are correctly converted to mapped ones before pf +# passes mbufs to interfaces. +# Use ovpn(4) because it does not accept unmapped mbufs and actually crashes if +# unmapped mbufs are passed to ovpn_output(). +setup_ovpn_peers() { + pft_init + ovpn_init + + l=$(vnet_mkepair) + + vnet_mkjail a ${l}a + jexec a ifconfig ${l}a 192.0.2.1/24 up + vnet_mkjail b ${l}b + jexec b ifconfig ${l}b 192.0.2.2/24 up + + # Sanity check + atf_check -s exit:0 -o ignore jexec a ping -c 1 192.0.2.2 + + ovpn_start a " + dev ovpn0 + dev-type tun + proto udp4 + + cipher AES-256-GCM + auth SHA256 + + local 192.0.2.1 + server 198.51.100.0 255.255.255.0 + server-ipv6 2001:db8::/64 + ca ${ovpn_dir}/ca.crt + cert ${ovpn_dir}/server.crt + key ${ovpn_dir}/server.key + dh ${ovpn_dir}/dh.pem + + mode server + script-security 2 + auth-user-pass-verify /usr/bin/true via-env + topology subnet + + keepalive 2 10 + + management 192.0.2.1 1234 + " + ovpn_start b " + dev tun0 + dev-type tun + + client + + remote 192.0.2.1 + auth-user-pass ${ovpn_dir}/user.pass + + ca ${ovpn_dir}/ca.crt + cert ${ovpn_dir}/client.crt + key ${ovpn_dir}/client.key + dh ${ovpn_dir}/dh.pem + + keepalive 2 10 + " + + # Give the tunnel time to come up + sleep 10 + + atf_check -s exit:0 -o ignore jexec b ping -c 1 198.51.100.1 + atf_check -s exit:0 -o ignore jexec a ping -c 1 $(ifa_v6 b tun0) +} + +atf_test_case "v4" "cleanup" +v4_head() { + atf_set descr 'unmapped mbuf IPv4 test' + atf_set require.user root + atf_set require.kmods if_ovpn + atf_set require.progs openvpn +} + +v4_body() { + pft_init + setup_ovpn_peers + + ovpn_a=$(jexec a ifconfig -g openvpn) + b_v4=$(ifa_v4 b tun0) + jexec a pfctl -e + pft_set_rules a "pass out on $ovpn_a route-to ($ovpn_a $b_v4) all" + jexec b timeout 30s nc -4 -d -l 2345 > out & + sleep 1 + atf_check -s exit:0 -o ignore \ + jexec a $(atf_get_srcdir)/sendfile_helper $b_v4 2345 512 + wait + # First 64 bytes are SHA256 hash of the following contents. + atf_check_equal $(dd bs=64 count=1 if=out) \ + $(dd bs=64 skip=1 if=out | sha256 -q) +} + +v4_cleanup() { + pft_cleanup + ovpn_cleanup + rm -f out +} + +atf_test_case "v6" "cleanup" +v6_head() { + atf_set descr 'unmapped mbuf IPv6 test' + atf_set require.user root + atf_set require.kmods if_ovpn + atf_set require.progs openvpn +} + +v6_body() { + pft_init + setup_ovpn_peers + + ovpn_a=$(jexec a ifconfig -g openvpn) + b_v6=$(ifa_v6 b tun0) + jexec a pfctl -e + pft_set_rules a "pass out on $ovpn_a route-to ($ovpn_a $b_v6) all" + jexec b timeout 30s nc -6 -d -l 2345 > out & + sleep 1 + atf_check -s exit:0 -o ignore \ + jexec a $(atf_get_srcdir)/sendfile_helper $b_v6 2345 512 + wait + # First 64 bytes are SHA256 hash of the following contents. + atf_check_equal $(dd bs=64 count=1 if=out) \ + $(dd bs=64 skip=1 if=out | sha256 -q) +} + +v6_cleanup() { + pft_cleanup + ovpn_cleanup + rm -f out +} + +atf_test_case "dummynet" "cleanup" +dummynet_head() { + atf_set descr 'unmapped mbuf dummynet test' + atf_set require.user root + atf_set require.kmods if_ovpn dummynet + atf_set require.progs openvpn +} + +dummynet_body() { + pft_init + setup_ovpn_peers + + ovpn_a=$(jexec a ifconfig -g openvpn) + b_v4=$(ifa_v4 b tun0) + atf_check -s exit:0 -o ignore \ + jexec a dnctl pipe 1 config delay 100 + jexec a pfctl -e + pft_set_rules a "pass out on $ovpn_a route-to ($ovpn_a $b_v4) all dnpipe 1" + jexec b timeout 30s nc -4 -d -l 2345 > out & + sleep 1 + atf_check -s exit:0 -o ignore \ + jexec a $(atf_get_srcdir)/sendfile_helper $b_v4 2345 512 + wait + # First 64 bytes are SHA256 hash of the following contents. + atf_check_equal $(dd bs=64 count=1 if=out) \ + $(dd bs=64 skip=1 if=out | sha256 -q) +} + +dummynet_cleanup() { + pft_cleanup + ovpn_cleanup + rm -f out +} + +atf_init_test_cases() +{ + atf_add_test_case "v4" + atf_add_test_case "v6" + atf_add_test_case "dummynet" +}