Package: dhcpcd-base
Version: 1:10.1.0-11
Severity: important
Forwarded: https://github.com/NetworkConfiguration/dhcpcd/pull/548
X-Debbugs-Cc: [email protected]

Dear Maintainer,

While working on dhcpcd<>ifupdown integration I found that interface
UP/DOWN events (cable re-plugging) can clear options that were passed when
asking the global manager (dhcpcd.service) to start the interface
initially.

This doesn't happen when using per-iface (as used by ifupdown=0.8.44) or
per-iface-per-af.

The attached patch fixes the issue and has more detailed rationale.

My preferred design for future-proof ifupdown<>dhcpcd.service
interoperability relies on this not being broken, I will be uploading an
NMU (1:10.3.0-1.1) to DELAYED/5 soon so my pending ifupdown=0.9 upload will
not regress or have to hack around this.

Thanks,
--Daniel

FYI: Find nomenclature and discussion in
https://lists.debian.org/debian-devel/2025/10/msg00201.html
From 112cc12b8dabef6ca41558016e97bfa03b71df0d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20Gr=C3=B6ber?= <[email protected]>
Date: Thu, 6 Nov 2025 14:50:15 +0100
Subject: manager: Fix loosing iface options on CARRIER

When an interface (re-)gains carrier dhcpcd_handlecarrier() runs
dhcpcd_initstate() to kick off profile re-selection. Previously this used
args originally passed when starting the manager (ctx->argv).

However interfaces started via the manager control
interface (dhcpcd_initstate1() in dhcpcd_handleargs()) may be started with
different args.

For example if we start a manager with

    dhcpcd -M --inactive

and then start only IPv4 on an interface with

    dhcpcd -4 iface0

a subsequent CARRIER event will reset the interface to what amounts to
"default config + `-M --inactive`" which in this case will enable ipv6
also!

To fix this we keep a copy of the arguments used to start an interface in
the manager (dhcpcd_handleargs()) code path around around (ifp->argv).

In the current implementation args passed for renew following the initial
interface start will not be persisted. This causes the interface to reset
to a state of "defaults + config + profile + start-cmdline".

For example (continuing the scenario above) after enabling ipv6 with -n:

    $ dhcpcd -6 -n iface0

A subsequent CARRIER event will disable ipv6 again as the effective
arguments remain `-4 iface0` as passed during interface start.

Note the per-interface daemon code path wasn't affected as ctx->args
already contains the interface start args.
---
 src/dhcpcd.c     | 21 +++++++++++++----
 src/dhcpcd.h     |  3 +++
 src/if-options.c | 60 ++++++++++++++++++++++++++++++++++++++++++++++++
 src/if-options.h |  3 +++
 src/if.c         |  2 ++
 5 files changed, 84 insertions(+), 5 deletions(-)

--- a/src/dhcpcd.c
+++ b/src/dhcpcd.c
@@ -729,7 +729,10 @@ static void
 dhcpcd_initstate(struct interface *ifp, unsigned long long options)
 {
 
-	dhcpcd_initstate1(ifp, ifp->ctx->argc, ifp->ctx->argv, options);
+	dhcpcd_initstate1(ifp,
+			  ifp->argc ?: ifp->ctx->argc,
+			  ifp->argv ?: ifp->ctx->argv,
+			  options);
 }
 
 static void
@@ -1364,7 +1367,7 @@ if_reboot(struct interface *ifp, int arg
 	oldopts = ifp->options->options;
 #endif
 	script_runreason(ifp, "RECONFIGURE");
-	dhcpcd_initstate1(ifp, argc, argv, 0);
+	dhcpcd_initstate1(ifp, argc, argv, 0); // control or main argv
 #ifdef INET
 	if (ifp->options->options & DHCPCD_DHCP)
 		dhcp_reboot_newopts(ifp, oldopts);
@@ -1419,8 +1422,16 @@ reconf_reboot(struct dhcpcd_ctx *ctx, in
 				ipv4_applyaddr(ifp);
 #endif
 		} else if (i != argc) {
+			/* iface wasnt found above -> it's new. start it. */
 			ifp->active = IF_ACTIVE_USER;
-			dhcpcd_initstate1(ifp, argc, argv, 0);
+			dhcpcd_initstate1(ifp, argc, argv, 0); // control cmd args
+
+			if (ifp->argv)
+				free_argv_copy(ifp->argv);
+			ifp->argv = copy_argv(argc, argv);
+			if (ifp->argv)
+				ifp->argc = argc;
+
 			run_preinit(ifp);
 			dhcpcd_prestartinterface(ifp);
 		}
@@ -1773,7 +1784,7 @@ dumperr:
 	}
 
 	reload_config(ctx);
-	/* XXX: Respect initial commandline options? */
+	/* Respect control cmd options! */
 	reconf_reboot(ctx, do_reboot, argc, argv, oifind);
 	return 0;
 }
@@ -2680,7 +2691,7 @@ start_manager:
 
 	TAILQ_FOREACH(ifp, ctx.ifaces, next) {
 		if (ifp->active)
-			dhcpcd_initstate1(ifp, argc, argv, 0);
+			dhcpcd_initstate1(ifp, argc, argv, 0); // main argv
 	}
 	if_learnaddrs(&ctx, ctx.ifaces, &ifaddrs);
 	if_freeifaddrs(&ctx, &ifaddrs);
--- a/src/dhcpcd.h
+++ b/src/dhcpcd.h
@@ -85,6 +85,9 @@ struct interface {
 	uint8_t ssid[IF_SSIDLEN];
 	unsigned int ssid_len;
 
+	int argc;
+	char **argv;
+
 	char profile[PROFILE_LEN];
 	struct if_options *options;
 	void *if_data[IF_DATA_MAX];
--- a/src/if-options.c
+++ b/src/if-options.c
@@ -44,6 +44,7 @@
 #include <string.h>
 #include <unistd.h>
 #include <time.h>
+#include <assert.h>
 
 #include "config.h"
 #include "common.h"
@@ -2986,6 +2987,65 @@ add_options(struct dhcpcd_ctx *ctx, cons
 	return r;
 }
 
+#define ARGV_COPY_MAGIC ((char *)0x5a54292d273f3d34)
+/*^ intentional truncation on 32bit arches */
+
+char **copy_argv(int argc, char **argv)
+{
+	int i;
+	size_t strslen = 0;
+	for (i = 0; i < argc; i++) {
+		strslen += strlen(argv[i]) + 1;
+	}
+	if (strslen == 0) // also handles argc < 0
+		return NULL;
+
+	unsigned nptrs = 1 + (unsigned)argc + 1;
+	size_t ptrslen =  nptrs * sizeof(char *);
+	void *buf = malloc(ptrslen + strslen);
+	char **ptrs = buf;
+	if (!buf)
+		return NULL;
+
+	ptrs[0] = ARGV_COPY_MAGIC;
+	ptrs[nptrs - 1] = NULL;
+
+	if (argc == 0)
+		goto out;
+
+	char *strsp = (char *)&ptrs[nptrs];
+	for (i = 0; i < argc; i++) {
+		size_t len = strlcpy(strsp, argv[i], strslen);
+		if (len >= strslen) // truncated
+			goto err;
+
+		ptrs[1 + i] = strsp;
+
+		strsp += len + 1;
+		if (strslen < len + 1)
+			goto err;
+		strslen -= len + 1;
+	}
+
+	assert(strslen == 0);
+	assert(ptrs[nptrs - 1] == NULL);
+out:
+	return &ptrs[1];
+
+err:
+	free(buf);
+	return NULL;
+}
+
+void free_argv_copy(char **argv)
+{
+	assert(argv[-1] == ARGV_COPY_MAGIC);
+	if (argv[-1] != ARGV_COPY_MAGIC) {
+		logerrx("%s: invalid argv", __func__);
+	} else
+		free(&argv[-1]);
+}
+
 void
 free_options(struct dhcpcd_ctx *ctx, struct if_options *ifo)
 {
--- a/src/if-options.h
+++ b/src/if-options.h
@@ -322,4 +322,7 @@ int add_options(struct dhcpcd_ctx *, con
 void free_dhcp_opt_embenc(struct dhcp_opt *);
 void free_options(struct dhcpcd_ctx *, struct if_options *);
 
+char **copy_argv(int argc, char **argv);
+void free_argv_copy(char **argv);
+
 #endif
--- a/src/if.c
+++ b/src/if.c
@@ -100,6 +100,8 @@ if_free(struct interface *ifp)
 #endif
 	rt_freeif(ifp);
 	free_options(ifp->ctx, ifp->options);
+	if (ifp->argv)
+		free_argv_copy(ifp->argv);
 	free(ifp);
 }
 

Attachment: signature.asc
Description: PGP signature

Reply via email to