All of lore.kernel.org
 help / color / mirror / Atom feed
From: 성병찬 <tjdqudcks0424@naver.com>
To: "Pablo Neira Ayuso" <pablo@netfilter.org>
Cc: <fw@strlen.de>, <phil@nwl.cc>, <netfilter-devel@vger.kernel.org>,
	<netdev@vger.kernel.org>, <linux-kernel@vger.kernel.org>
Subject: Re: [BUG] netfilter: IPv6 conntrack fragment reassembly truncates header offset
Date: Tue, 29 Sep 2026 18:14:02 +0900	[thread overview]
Message-ID: <2aafeb116ccba6fbd3fa457d53d17be2@cweb006.nm> (raw)
In-Reply-To: <art-XRsR09GIvc4D@chamomile>

[-- Attachment #1: Type: text/plain, Size: 1330 bytes --]


Thanks for the information. I confirmed that Jérémy Jean's existing
patch is identical to the fix I tested. Please treat my patch as
superseded by that patch.

I have attached my self-contained reproducer as repro-v2.c. It only
uses IPv6 loopback (::1) and does not send traffic outside the test
system.

Build:

  cc -std=c11 -O2 -Wall -Wextra -Werror \
    -o repro-v2 repro-v2.c

The test requires root or CAP_NET_RAW. I ran it in a QEMU guest booted
with:

  nf_conntrack.enable_hooks=1

Run:

  ./repro-v2 1

Expected result on the unmodified kernel:

  iteration=1 control_received=yes
  iteration=1 boundary_received=no
  RESULT: differential observed (control delivered, boundary not delivered)

The reproducer exits with status 0 when the bug is reproduced.

Expected result with Jérémy Jean's patch applied:

  iteration=1 control_received=yes
  iteration=1 boundary_received=yes
  RESULT: control passed but expected boundary drop was not observed

In this case it exits with status 2 because the bug is no longer
reproduced.

I reproduced the unmodified result twice and tested the fixed result
twice on Linux v7.2.8.

The SHA-256 of the attached source is:

  511dd29a2c41d0c734bb1369a6ab538a85f2498d92c137802c6775240ca17903

You may add:

Tested-by: 성병찬 <tjdqudcks0424@naver.com>

Regards,
Sung Byeongchan

[-- Attachment #2: repro-v2.c --]
[-- Type: application/octet-stream, Size: 15352 bytes --]

#define _GNU_SOURCE

#include <arpa/inet.h>
#include <errno.h>
#include <netinet/in.h>
#include <poll.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <time.h>
#include <unistd.h>

/*
 * NFA-02 differential reproducer, version 2.
 *
 * Packets are injected as complete IPv6 packets through
 * AF_INET6/SOCK_RAW/IPV6_HDRINCL to ::1.  No link-layer header is supplied by
 * userspace.  Linux rawv6_send_hdrinc() sends these skbs through
 * NF_INET_LOCAL_OUT before loopback delivery, so netfilter reassembly happens
 * before the receive-side IPv6 extension-header parser initializes its skb
 * offsets.
 *
 * All offsets below are relative to IPv6 byte zero.
 *
 * control:
 *   IPv6 [0,40), Hop-by-Hop [40,248), Destination Options [248,256),
 *   Fragment [256,264), then 8 bytes of fragment data.
 *   The predecessor Next Header byte is exactly offset 248.
 *
 * boundary:
 *   IPv6 [0,40), Hop-by-Hop [40,256), Destination Options [256,264),
 *   Fragment [264,272), then 8 bytes of fragment data.
 *   The predecessor Next Header byte is exactly offset 256.
 *
 * Each UDP datagram is split into two fragments.  Fragment zero contains the
 * complete 8-byte UDP header and has M=1.  Fragment one has byte offset 8,
 * M=0, and contains the complete 8-byte UDP payload.
 */

enum {
	IPV6_LEN = 40,
	DEST_LEN = 8,
	FRAG_LEN = 8,
	UDP_LEN = 8,
	APP_PAYLOAD_LEN = 8,
	UDP_DATAGRAM_LEN = UDP_LEN + APP_PAYLOAD_LEN,
	CONTROL_HBH_LEN = 208,
	BOUNDARY_HBH_LEN = 216,
	CONTROL_PREDECESSOR_NHOFF = 248,
	BOUNDARY_PREDECESSOR_NHOFF = 256,
	CONTROL_FRAGMENT_OFFSET = 256,
	BOUNDARY_FRAGMENT_OFFSET = 264,
	MAX_PACKET_LEN = 512,
	DEST_PORT = 45555,
	MAX_ITERATIONS = 10,
	RECEIVE_TIMEOUT_MS = 1500,
	V6_NH_HOP = 0,
	V6_NH_UDP = 17,
	V6_NH_FRAGMENT = 44,
	V6_NH_DEST = 60,
	FRAGMENT_M_FLAG = 1,
	UNKNOWN_SKIP_OPTION = 0x1e,
	PADN_OPTION = 1,
};

static void write_be16(unsigned char *p, uint16_t value)
{
	p[0] = (unsigned char)(value >> 8);
	p[1] = (unsigned char)value;
}

static void write_be32(unsigned char *p, uint32_t value)
{
	p[0] = (unsigned char)(value >> 24);
	p[1] = (unsigned char)(value >> 16);
	p[2] = (unsigned char)(value >> 8);
	p[3] = (unsigned char)value;
}

static uint16_t read_be16(const unsigned char *p)
{
	return (uint16_t)(((uint16_t)p[0] << 8) | p[1]);
}

static uint32_t checksum_add(uint32_t sum, const unsigned char *data,
			     size_t len)
{
	while (len >= 2) {
		sum += ((uint32_t)data[0] << 8) | data[1];
		data += 2;
		len -= 2;
	}
	if (len != 0)
		sum += (uint32_t)data[0] << 8;
	return sum;
}

static uint16_t checksum_finish(uint32_t sum)
{
	uint16_t result;

	while (sum >> 16)
		sum = (sum & 0xffffU) + (sum >> 16);
	result = (uint16_t)~sum;
	return result == 0 ? 0xffffU : result;
}

static uint16_t udp6_checksum(const unsigned char *udp,
			      const unsigned char *payload)
{
	unsigned char loopback[16] = { 0 };
	uint32_t sum = 0;

	loopback[15] = 1;
	sum = checksum_add(sum, loopback, sizeof(loopback));
	sum = checksum_add(sum, loopback, sizeof(loopback));
	/* 32-bit UDP length contributes 0x0000 and 0x0010. */
	sum += UDP_DATAGRAM_LEN;
	/* Three zero bytes and Next Header 17 contribute 0x0000 and 0x0011. */
	sum += V6_NH_UDP;
	sum = checksum_add(sum, udp, UDP_LEN);
	sum = checksum_add(sum, payload, APP_PAYLOAD_LEN);
	return checksum_finish(sum);
}

static int build_udp(unsigned char udp[UDP_LEN], uint16_t source_port,
		     const unsigned char payload[APP_PAYLOAD_LEN])
{
	uint16_t checksum;

	memset(udp, 0, UDP_LEN);
	write_be16(udp, source_port);
	write_be16(udp + 2, DEST_PORT);
	write_be16(udp + 4, UDP_DATAGRAM_LEN);
	checksum = udp6_checksum(udp, payload);
	write_be16(udp + 6, checksum);
	return 0;
}

static int validate_udp(const unsigned char udp[UDP_LEN], uint16_t source_port,
			const unsigned char payload[APP_PAYLOAD_LEN])
{
	unsigned char check[UDP_LEN];
	uint16_t expected;
	uint16_t actual;

	if (read_be16(udp) != source_port ||
	    read_be16(udp + 2) != DEST_PORT ||
	    read_be16(udp + 4) != UDP_DATAGRAM_LEN)
		return -1;
	actual = read_be16(udp + 6);
	if (actual == 0)
		return -1;
	memcpy(check, udp, sizeof(check));
	check[6] = 0;
	check[7] = 0;
	expected = udp6_checksum(check, payload);
	return actual == expected ? 0 : -1;
}

static int fill_hop_by_hop(unsigned char *hbh, size_t hbh_len)
{
	size_t unknown_total;
	size_t unknown_data;
	size_t pad_offset;

	if (hbh_len != CONTROL_HBH_LEN && hbh_len != BOUNDARY_HBH_LEN)
		return -1;

	memset(hbh, 0, hbh_len);
	hbh[0] = V6_NH_DEST;
	hbh[1] = (unsigned char)(hbh_len / 8 - 1);

	/*
	 * One unknown option with action bits 00 (skip) plus one six-byte PadN.
	 * control: 200-byte unknown TLV + 6-byte PadN = 206 option bytes.
	 * boundary: 208-byte unknown TLV + 6-byte PadN = 214 option bytes.
	 */
	unknown_total = hbh_len - 8;
	unknown_data = unknown_total - 2;
	if (unknown_data > UINT8_MAX)
		return -1;
	hbh[2] = UNKNOWN_SKIP_OPTION;
	hbh[3] = (unsigned char)unknown_data;

	pad_offset = 2 + unknown_total;
	hbh[pad_offset] = PADN_OPTION;
	hbh[pad_offset + 1] = 4;
	return 0;
}

static void fill_destination_options(unsigned char dest[DEST_LEN])
{
	memset(dest, 0, DEST_LEN);
	dest[0] = V6_NH_FRAGMENT;
	dest[1] = 0;
	dest[2] = PADN_OPTION;
	dest[3] = 4;
}

static int build_fragment(unsigned char packet[MAX_PACKET_LEN],
			  size_t *packet_len, size_t hbh_len,
			  uint32_t identification, uint16_t fragment_field,
			  const unsigned char fragment_data[8])
{
	unsigned char *ip6 = packet;
	unsigned char *hbh = ip6 + IPV6_LEN;
	unsigned char *dest = hbh + hbh_len;
	unsigned char *frag = dest + DEST_LEN;
	unsigned char *data = frag + FRAG_LEN;
	size_t payload_len = hbh_len + DEST_LEN + FRAG_LEN + 8;
	size_t total_len = IPV6_LEN + payload_len;

	if (total_len > MAX_PACKET_LEN || payload_len > UINT16_MAX)
		return -1;
	memset(packet, 0, MAX_PACKET_LEN);

	/* IPv6: version 6, explicit payload length, HBH, hop limit 64, ::1 -> ::1. */
	ip6[0] = 0x60;
	write_be16(ip6 + 4, (uint16_t)payload_len);
	ip6[6] = V6_NH_HOP;
	ip6[7] = 64;
	ip6[8 + 15] = 1;
	ip6[24 + 15] = 1;

	if (fill_hop_by_hop(hbh, hbh_len) < 0)
		return -1;
	fill_destination_options(dest);

	frag[0] = V6_NH_UDP;
	frag[1] = 0;
	write_be16(frag + 2, fragment_field);
	write_be32(frag + 4, identification);
	memcpy(data, fragment_data, 8);

	*packet_len = total_len;
	return 0;
}

static int validate_fragment(const unsigned char *packet, size_t packet_len,
			     size_t hbh_len, uint16_t fragment_field,
			     const unsigned char fragment_data[8])
{
	const unsigned char *hbh = packet + IPV6_LEN;
	const unsigned char *dest = hbh + hbh_len;
	const unsigned char *frag = dest + DEST_LEN;
	const unsigned char *data = frag + FRAG_LEN;
	size_t expected_len = IPV6_LEN + hbh_len + DEST_LEN + FRAG_LEN + 8;

	if (packet_len != expected_len || packet[0] != 0x60 ||
	    read_be16(packet + 4) != packet_len - IPV6_LEN ||
	    packet[6] != V6_NH_HOP || packet[7] != 64)
		return -1;
	if (packet[8 + 15] != 1 || packet[24 + 15] != 1)
		return -1;
	if (hbh[0] != V6_NH_DEST || hbh[1] != hbh_len / 8 - 1)
		return -1;
	if (dest[0] != V6_NH_FRAGMENT || dest[1] != 0 ||
	    dest[2] != PADN_OPTION || dest[3] != 4)
		return -1;
	if (frag[0] != V6_NH_UDP || frag[1] != 0 ||
	    read_be16(frag + 2) != fragment_field)
		return -1;
	return memcmp(data, fragment_data, 8) == 0 ? 0 : -1;
}

static int send_one_packet(int fd, const unsigned char *packet,
			   size_t packet_len)
{
	struct sockaddr_in6 destination;
	ssize_t sent;

	memset(&destination, 0, sizeof(destination));
	destination.sin6_family = AF_INET6;
	destination.sin6_addr = in6addr_loopback;

	sent = sendto(fd, packet, packet_len, 0,
		      (const struct sockaddr *)&destination, sizeof(destination));
	if (sent < 0) {
		perror("sendto(AF_INET6/SOCK_RAW/IPV6_HDRINCL)");
		return -1;
	}
	if ((size_t)sent != packet_len) {
		fprintf(stderr, "short raw IPv6 send: %zd of %zu bytes\n",
			sent, packet_len);
		return -1;
	}
	return 0;
}

static int send_case(int fd, const char *name, size_t hbh_len,
		     uint32_t identification, uint16_t source_port,
		     const unsigned char payload[APP_PAYLOAD_LEN])
{
	unsigned char first[MAX_PACKET_LEN];
	unsigned char second[MAX_PACKET_LEN];
	unsigned char udp[UDP_LEN];
	size_t first_len;
	size_t second_len;

	if (build_udp(udp, source_port, payload) < 0 ||
	    validate_udp(udp, source_port, payload) < 0) {
		fprintf(stderr, "%s: UDP header/checksum self-check failed\n", name);
		return -1;
	}
	if (build_fragment(first, &first_len, hbh_len, identification,
			   FRAGMENT_M_FLAG, udp) < 0 ||
	    validate_fragment(first, first_len, hbh_len, FRAGMENT_M_FLAG,
			      udp) < 0) {
		fprintf(stderr, "%s: first-fragment self-check failed\n", name);
		return -1;
	}
	/* Wire value 8 means byte offset 8 because the low three bits are flags. */
	if (build_fragment(second, &second_len, hbh_len, identification,
			   8, payload) < 0 ||
	    validate_fragment(second, second_len, hbh_len, 8, payload) < 0) {
		fprintf(stderr, "%s: second-fragment self-check failed\n", name);
		return -1;
	}

	printf("%s: first_len=%zu second_len=%zu payload_len=%u "
	       "udp_checksum=0x%04x fragment_fields=0x0001,0x0008\n",
	       name, first_len, second_len, read_be16(first + 4),
	       read_be16(udp + 6));
	if (send_one_packet(fd, first, first_len) < 0)
		return -1;
	if (send_one_packet(fd, second, second_len) < 0)
		return -1;
	return 0;
}

static int monotonic_ms(uint64_t *value)
{
	struct timespec ts;

	if (clock_gettime(CLOCK_MONOTONIC, &ts) < 0) {
		perror("clock_gettime");
		return -1;
	}
	*value = (uint64_t)ts.tv_sec * 1000U +
		 (uint64_t)ts.tv_nsec / 1000000U;
	return 0;
}

static int drain_receiver(int fd)
{
	unsigned char buffer[256];

	for (;;) {
		ssize_t received = recv(fd, buffer, sizeof(buffer), 0);

		if (received >= 0)
			continue;
		if (errno == EAGAIN || errno == EWOULDBLOCK)
			return 0;
		perror("recv(drain)");
		return -1;
	}
}

/* Return 1 for a matching datagram, 0 for timeout, and -1 for syscall error. */
static int wait_for_payload(int fd,
			    const unsigned char expected[APP_PAYLOAD_LEN])
{
	uint64_t start;
	uint64_t now;
	struct pollfd pfd = { .fd = fd, .events = POLLIN };

	if (monotonic_ms(&start) < 0)
		return -1;
	for (;;) {
		unsigned char buffer[256];
		ssize_t received;
		int remaining;
		int ready;

		if (monotonic_ms(&now) < 0)
			return -1;
		if (now - start >= RECEIVE_TIMEOUT_MS)
			return 0;
		remaining = RECEIVE_TIMEOUT_MS - (int)(now - start);
		pfd.revents = 0;
		ready = poll(&pfd, 1, remaining);
		if (ready < 0) {
			perror("poll");
			return -1;
		}
		if (ready == 0)
			return 0;
		if ((pfd.revents & POLLIN) == 0) {
			fprintf(stderr, "unexpected poll revents: 0x%x\n", pfd.revents);
			return -1;
		}

		received = recv(fd, buffer, sizeof(buffer), 0);
		if (received < 0) {
			perror("recv");
			return -1;
		}
		if (received == APP_PAYLOAD_LEN &&
		    memcmp(buffer, expected, APP_PAYLOAD_LEN) == 0)
			return 1;
		fprintf(stderr, "ignored unrelated UDP datagram of %zd bytes\n",
			received);
	}
}

static int parse_iterations(const char *text, int *iterations)
{
	char *end = NULL;
	long value;

	errno = 0;
	value = strtol(text, &end, 10);
	if (errno != 0 || end == text || *end != '\0' ||
	    value < 1 || value > MAX_ITERATIONS)
		return -1;
	*iterations = (int)value;
	return 0;
}

static int close_checked(int fd, const char *name)
{
	if (close(fd) < 0) {
		fprintf(stderr, "close(%s): %s\n", name, strerror(errno));
		return -1;
	}
	return 0;
}

int main(int argc, char **argv)
{
	static const unsigned char control_payload[APP_PAYLOAD_LEN] =
		{ 'N', 'F', 'A', '2', 'V', '2', 'C', '!' };
	static const unsigned char boundary_payload[APP_PAYLOAD_LEN] =
		{ 'N', 'F', 'A', '2', 'V', '2', 'B', '!' };
	static const char loopback_device[] = "lo";
	struct sockaddr_in6 receive_address;
	int hdrincl = 1;
	int iterations = 1;
	int receiver = -1;
	int raw = -1;
	int result = EXIT_SUCCESS;
	int i;

	if (argc > 2 || (argc == 2 && parse_iterations(argv[1], &iterations) < 0)) {
		fprintf(stderr, "usage: %s [iterations:1-%d]\n", argv[0],
			MAX_ITERATIONS);
		return EXIT_FAILURE;
	}

	receiver = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK,
			  IPPROTO_UDP);
	if (receiver < 0) {
		perror("socket(AF_INET6, SOCK_DGRAM)");
		return EXIT_FAILURE;
	}
	memset(&receive_address, 0, sizeof(receive_address));
	receive_address.sin6_family = AF_INET6;
	receive_address.sin6_port = htons(DEST_PORT);
	receive_address.sin6_addr = in6addr_loopback;
	if (bind(receiver, (const struct sockaddr *)&receive_address,
		 sizeof(receive_address)) < 0) {
		perror("bind(UDP ::1)");
		result = EXIT_FAILURE;
		goto out;
	}

	raw = socket(AF_INET6, SOCK_RAW | SOCK_CLOEXEC, IPPROTO_RAW);
	if (raw < 0) {
		perror("socket(AF_INET6, SOCK_RAW, IPPROTO_RAW); CAP_NET_RAW required");
		result = EXIT_FAILURE;
		goto out;
	}
	if (setsockopt(raw, IPPROTO_IPV6, IPV6_HDRINCL, &hdrincl,
		       sizeof(hdrincl)) < 0) {
		perror("setsockopt(IPV6_HDRINCL)");
		result = EXIT_FAILURE;
		goto out;
	}
	if (setsockopt(raw, SOL_SOCKET, SO_BINDTODEVICE, loopback_device,
		       sizeof(loopback_device)) < 0) {
		perror("setsockopt(SO_BINDTODEVICE=lo)");
		result = EXIT_FAILURE;
		goto out;
	}

	printf("injection=AF_INET6/SOCK_RAW/IPPROTO_RAW/IPV6_HDRINCL "
	       "device=lo destination=[::1]:%d iterations=%d\n",
	       DEST_PORT, iterations);
	printf("control: hbh=208 predecessor_nhoff=%d fragment_offset=%d\n",
	       CONTROL_PREDECESSOR_NHOFF, CONTROL_FRAGMENT_OFFSET);
	printf("boundary: hbh=216 predecessor_nhoff=%d fragment_offset=%d "
	       "stored_u8=0\n",
	       BOUNDARY_PREDECESSOR_NHOFF, BOUNDARY_FRAGMENT_OFFSET);

	for (i = 0; i < iterations; i++) {
		uint32_t base_id = 0x4e460400U + (uint32_t)i * 2U;
		int observed;

		if (drain_receiver(receiver) < 0) {
			result = EXIT_FAILURE;
			goto out;
		}
		if (send_case(raw, "control", CONTROL_HBH_LEN, base_id,
			      (uint16_t)(40000 + i * 2), control_payload) < 0) {
			result = EXIT_FAILURE;
			goto out;
		}
		observed = wait_for_payload(receiver, control_payload);
		if (observed < 0) {
			result = EXIT_FAILURE;
			goto out;
		}
		printf("iteration=%d control_received=%s\n", i + 1,
		       observed ? "yes" : "no");
		if (!observed) {
			printf("iteration=%d boundary_received=not-tested\n", i + 1);
			printf("RESULT: control failed; boundary was not sent and is not interpretable\n");
			result = 3;
			goto out;
		}

		if (drain_receiver(receiver) < 0) {
			result = EXIT_FAILURE;
			goto out;
		}
		if (send_case(raw, "boundary", BOUNDARY_HBH_LEN, base_id + 1,
			      (uint16_t)(40001 + i * 2), boundary_payload) < 0) {
			result = EXIT_FAILURE;
			goto out;
		}
		observed = wait_for_payload(receiver, boundary_payload);
		if (observed < 0) {
			result = EXIT_FAILURE;
			goto out;
		}
		printf("iteration=%d boundary_received=%s\n", i + 1,
		       observed ? "yes" : "no");
		if (observed)
			result = 2;
	}

	if (result == EXIT_SUCCESS)
		printf("RESULT: differential observed (control delivered, boundary not delivered)\n");
	else if (result == 2)
		printf("RESULT: control passed but expected boundary drop was not observed\n");

out:
	if (raw >= 0 && close_checked(raw, "raw IPv6") < 0)
		result = EXIT_FAILURE;
	if (receiver >= 0 && close_checked(receiver, "UDP receiver") < 0)
		result = EXIT_FAILURE;
	return result;
}

  reply	other threads:[~2026-09-29  9:34 UTC|newest]

Thread overview: 5+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
     [not found] <31da96413a406da2116f44df2db84f@cweb009.nm>
2026-09-29  9:01 ` [BUG] netfilter: IPv6 conntrack fragment reassembly truncates header offset Pablo Neira Ayuso
2026-09-29  9:14   ` 성병찬 [this message]
2026-09-29  5:34 성병찬
2026-09-29  7:20 ` Florian Westphal
2026-09-29  9:07   ` Pablo Neira Ayuso

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=2aafeb116ccba6fbd3fa457d53d17be2@cweb006.nm \
    --to=tjdqudcks0424@naver.com \
    --cc=fw@strlen.de \
    --cc=linux-kernel@vger.kernel.org \
    --cc=netdev@vger.kernel.org \
    --cc=netfilter-devel@vger.kernel.org \
    --cc=pablo@netfilter.org \
    --cc=phil@nwl.cc \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.