Page MenuHomeFreeBSD

if_output: validate sa_len before reading sa_data for BPF address family
Needs ReviewPublic

Authored by ing.castellanosdz_gmail.com on Mon, Aug 24, 6:37 AM.
Tags
None
Referenced Files
F172188026: D59140.id186596.diff
Wed, Sep 16, 5:34 PM
Unknown Object (File)
Wed, Sep 16, 5:29 AM
Unknown Object (File)
Wed, Sep 16, 4:41 AM
Unknown Object (File)
Tue, Sep 15, 11:16 PM
Unknown Object (File)
Tue, Sep 15, 8:00 PM
Unknown Object (File)
Tue, Sep 15, 5:24 PM
Unknown Object (File)
Tue, Sep 15, 2:37 AM
Unknown Object (File)
Mon, Sep 14, 5:26 PM

Details

Reviewers
jhb
bz
glebius
Summary

Original observation: write(2) to BPF descriptor on lo0 returned
EAFNOSUPPORT. Initial attribution: missing sa_len initialization in
if_output drivers.

After review by glebius: the reproducer violated the DLT_NULL contract
(missing 32-bit AF header). The original attribution was not sustained.
The sa_len check in the individual drivers is not required because the
BPF/ifnet interface contract is already established upstream.

Following that review, this revision is updated to add a defensive
assertion in bpf_movein() per glebius's suggestion:

MPASS(hlen <= sizeof(sockp->sa_data));

This makes the existing contract between bpf and ifnet explicit and
machine-checkable.

Test Plan

Original observation (FreeBSD 14.4-RELEASE-p8):
write(2) to BPF descriptor on lo0 returned EAFNOSUPPORT.
Reproducer: repro-bpf-sa-len.c — pkt[] without DLT_NULL header.

Initial analysis:
bpfwrite() path confirmed: bzero(&dst) at line 1211, sa_len=0 after
bpf_movein(DLT_RAW), activates AF_UNSPEC branch in each driver.
Pattern was verified in HEAD across all five files during the source audit.
This analysis supported the initial attribution — subsequently not sustained.

After review by glebius (2026-09-12):
The reproducer violated the DLT_NULL contract (missing 32-bit AF header).
Corrected reproducer verified on FreeBSD 14.5-RELEASE: injection succeeds.

Current patch:
MPASS(hlen <= sizeof(sockp->sa_data)) added in bpf_movein() as defensive
assertion making the BPF/ifnet contract explicit and machine-checkable.
MFC after: 2 weeks

Diff Detail

Repository
rG FreeBSD src repository
Lint
Lint Skipped
Unit
Tests Skipped

Event Timeline

Updating D59140: if_output: validate sa_len before reading sa_data for BPF address family

Rebased against stable/14 as of 2026-09-10. Line offsets updated for
if_tuntap.c (1455), if_me.c (544), if_gif.c (409), if_disc.c (181),
if_loop.c (229). Logic and fix unchanged from original D59140.

I wonder if bpfwrite() can be fixed instead? Can you please share a reproducing bpf writing program?

Thank you for the question, glebius.

The reproducer is below. Compile and run as root on any interface that uses DLT_RAW (lo0 works):

cc -o repro-bpf-sa-len repro-bpf-sa-len.c
./repro-bpf-sa-len lo0

Output on FreeBSD 14.5-RELEASE:

write: Address family not supported by protocol family

This confirms the path: bpfwrite() -> bpf_movein() -> if_output with sa_len=0.

Regarding fixing bpfwrite() instead:

bpfwrite() already calls bzero(&dst, sizeof(dst)) before bpf_movein(), so sa_len=0 is the correct initial state for most DLT types. bpf_movein() sets sa_family for each DLT case but initializes sa_len in only one case: DLT_IEEE802_11_RADIO sets sa_len=12 with a XXX comment. All other DLT cases (DLT_RAW, DLT_EN10MB, DLT_NULL, DLT_PPP, DLT_FDDI, DLT_ATM_RFC1483) leave sa_len=0.

Fixing in bpfwrite()/bpf_movein() would require adding the correct sa_len value for each DLT case, which is more invasive and requires knowledge of the expected header size per link type. The fix in if_output is a defensive check at the point of use: if sa_len is too small to contain a valid af value, fall back to af=0 rather than reading past the end of sa_data.

I am open to the alternative approach if you prefer the fix to live in bpfwrite(). Happy to prepare a patch there instead if that is the direction you recommend.

Reproducer source:

/*
 * repro-bpf-sa-len.c
 * Reproduces sa_len=0 path in bpfwrite() -> if_output on FreeBSD.
 * Compile: cc -o repro-bpf-sa-len repro-bpf-sa-len.c
 * Run as root: ./repro-bpf-sa-len <interface>
 */
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <net/bpf.h>
#include <net/if.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

int
main(int argc, char *argv[])
{
        int fd;
        struct ifreq ifr;
        unsigned char pkt[] = {
                0x45, 0x00, 0x00, 0x14,
                0x00, 0x01, 0x00, 0x00,
                0x40, 0x00, 0x00, 0x00,
                0x7f, 0x00, 0x00, 0x01,
                0x7f, 0x00, 0x00, 0x01
        };

        if (argc < 2) {
                fprintf(stderr, "usage: %s <interface>\n", argv[0]);
                return 1;
        }

        fd = open("/dev/bpf", O_RDWR);
        if (fd < 0) { perror("open /dev/bpf"); return 1; }

        memset(&ifr, 0, sizeof(ifr));
        strlcpy(ifr.ifr_name, argv[1], sizeof(ifr.ifr_name));
        if (ioctl(fd, BIOCSETIF, &ifr) < 0) {
                perror("BIOCSETIF"); close(fd); return 1;
        }

        ssize_t n = write(fd, pkt, sizeof(pkt));
        if (n < 0)
                perror("write");
        else
                printf("wrote %zd bytes -- sa_len=0 path triggered\n", n);

        close(fd);
        return 0;
}
glebius requested changes to this revision.EditedSat, Sep 12, 12:45 PM

There is actually no problem here. The reproducing program is not correct. A write(2) on a bpf descriptor shall include a header of appropriate length for this descriptors DLT. The DLT can be determined by BIOCGDLT ioctl on the descriptor. On all these interfaces DLT_NULL will be returned. DLT_NULL implies that a packet written shall be prepended with a 32-bit word in host byte order that has the address family number. Since you program has IPv4 packet, that should be AF_INET in host byte order:

unsigned char pkt[] = {
        0x02, 0x00, 0x00, 0x00,
        0x45, 0x00, 0x00, 0x14,
        0x00, 0x01, 0x00, 0x00,
        0x40, 0x00, 0x00, 0x00,
        0x7f, 0x00, 0x00, 0x01,
        0x7f, 0x00, 0x00, 0x01
};

With the above corrected pkt[] the program correctly injects packet on lo0.

The sa_len doesn't need to be initialized. This is a contract between bpf and interfaces. Both bpf and interfaces shall know how much data there is in sa_data. This length is constant for every interface type and bpf is supposed to know it. I agree this is not a beautiful code. We inherited it from the original BSD. What could be done is adding an extra assertion in bpf_ifnet_write():

MPASS(hlen <= sizeof(sa->sa_data));
This revision now requires changes to proceed.Sat, Sep 12, 12:45 PM

Thanks for the correction on DLT_NULL and for reviewing this carefully. You are right on both points.

The original reproducer was missing the 32-bit address-family header required for DLT_NULL. With AF_INET prepended in host byte order the packet injects correctly on lo0. I will verify this locally and update the reproducer in the experiment record.

On the patch itself: I understand now that bpf_ifnet_write() already ensures dst.sa_data contains the correct hlen bytes before calling if_output, so the sa_len check in the individual drivers is defending against a condition that cannot arise via that path. The contract is enforced upstream.

Regarding the MPASS(hlen <= sizeof(sa->sa_data)) suggestion: I would be glad to update D59140 to add that assertion in bpf_ifnet_write() as a hardening measure, replacing the current approach. Should I prepare that as an update to this revision or as a new diff?

Regarding the MPASS(hlen <= sizeof(sa->sa_data)) suggestion: I would be glad to update D59140 to add that assertion in bpf_ifnet_write() as a hardening measure, replacing the current approach. Should I prepare that as an update to this revision or as a new diff?

Yes, IMHO better to reuse this revision number so that all discussion stays in one place for history. However, you will need to update both the revision title and summary.

ing.castellanosdz_gmail.com edited the summary of this revision. (Show Details)

Updated per glebius's suggestion: replaced the sa_len check in individual
drivers with a defensive assertion in bpf_movein() asserting
MPASS(hlen <= sizeof(sockp->sa_data)) before the bcopy. Original
attribution (missing sa_len initialization) was not sustained after review.

Updated as suggested: replaced the sa_len check in individual drivers
with MPASS(hlen <= sizeof(sockp->sa_data)) in bpf_movein() before the
bcopy. Summary updated to reflect the evolution of the revision.