Radish alpha
H
rad:z3QDZAW2FAfuLvihrhiyDC9fAD8G9
HardenedBSD Package Manager
Radicle
Git
rcscript: rewrite entirely to be safer
Baptiste Daroussin committed 8 days ago
commit 2e3e34b18ae90b5b6f8e9f9b62bffcf21134b7af
parent 16bb26d
8 files changed +760 -89
modified libpkg/pkg_add.c
@@ -1408,12 +1408,16 @@ cleanup:
}

static int
-
pkg_add_cleanup_old(struct pkgdb *db, struct pkg *old, struct pkg *new, struct triggers *t, int flags)
+
pkg_add_cleanup_old(struct pkgdb *db, struct pkg *old, struct pkg *new,
+
    struct triggers *t, int flags, struct deferred_rc *rc)
{
	struct pkg_file *f;
	int ret = EPKG_OK;

-
	pkg_start_stop_rc_scripts(old, PKG_RC_STOP);
+
	if (rc != NULL)
+
		pkg_deferred_rc_add(rc, old, PKG_RC_STOP);
+
	else
+
		pkg_start_stop_rc_scripts(old, PKG_RC_STOP);

	/*
	 * Execute pre deinstall scripts
@@ -1525,7 +1529,7 @@ populate_config_file_contents(struct archive *a, struct archive_entry *ae,
static int
pkg_add_common(struct pkgdb *db, const char *path, unsigned flags,
    const char *reloc, struct pkg *remote,
-
    struct pkg *local, struct triggers *t)
+
    struct pkg *local, struct triggers *t, struct deferred_rc *rc)
{
	struct archive		*a;
	struct archive_entry	*ae;
@@ -1679,7 +1683,7 @@ pkg_add_common(struct pkgdb *db, const char *path, unsigned flags,
	if (local != NULL && (flags & PKG_ADD_SPLITTED_UPGRADE) == 0) {
		pkg_open_root_fd(local);
		pkg_debug(1, "Cleaning up old version");
-
		if (pkg_add_cleanup_old(db, local, pkg, t, flags) != EPKG_OK) {
+
		if (pkg_add_cleanup_old(db, local, pkg, t, flags, rc) != EPKG_OK) {
			retcode = EPKG_FATAL;
			goto cleanup;
		}
@@ -1717,11 +1721,13 @@ pkg_add_common(struct pkgdb *db, const char *path, unsigned flags,
	triggers_execute_perpackage(pkg, TRIGGER_PHASE_POST_INSTALL, (local != NULL));

	/*
-
	 * start the different related services if the users do want that
-
	 * and that the service is running
+
	 * Record rc scripts for (re)start at the end of the transaction,
+
	 * or start them immediately if running outside a transaction.
	 */
-

-
	pkg_start_stop_rc_scripts(pkg, PKG_RC_START);
+
	if (rc != NULL)
+
		pkg_deferred_rc_add(rc, pkg, PKG_RC_START);
+
	else
+
		pkg_start_stop_rc_scripts(pkg, PKG_RC_START);

	if ((flags & (PKG_ADD_UPGRADE | PKG_ADD_SPLITTED_UPGRADE)) !=
	    PKG_ADD_UPGRADE)
@@ -1787,26 +1793,28 @@ int
pkg_add(struct pkgdb *db, const char *path, unsigned flags,
    const char *location)
{
-
	return pkg_add_common(db, path, flags, location, NULL, NULL, NULL);
+
	return pkg_add_common(db, path, flags, location, NULL, NULL, NULL, NULL);
}

int
pkg_add_from_remote(struct pkgdb *db, const char *path, unsigned flags,
-
    const char *location, struct pkg *rp, struct triggers *t)
+
    const char *location, struct pkg *rp, struct triggers *t,
+
    struct deferred_rc *rc)
{
-
	return pkg_add_common(db, path, flags, location, rp, NULL, t);
+
	return pkg_add_common(db, path, flags, location, rp, NULL, t, rc);
}

int
pkg_add_upgrade(struct pkgdb *db, const char *path, unsigned flags,
    const char *location,
-
    struct pkg *rp, struct pkg *lp, struct triggers *t)
+
    struct pkg *rp, struct pkg *lp, struct triggers *t,
+
    struct deferred_rc *rc)
{
	if (pkgdb_ensure_loaded(db, lp,
	    PKG_LOAD_FILES|PKG_LOAD_SCRIPTS|PKG_LOAD_DIRS|PKG_LOAD_LUA_SCRIPTS) != EPKG_OK)
		return (EPKG_FATAL);

-
	return pkg_add_common(db, path, flags, location, rp, lp, t);
+
	return pkg_add_common(db, path, flags, location, rp, lp, t, rc);
}

static int
modified libpkg/pkg_delete.c
@@ -57,7 +57,7 @@

int
pkg_delete(struct pkg *pkg, struct pkg *rpkg, struct pkgdb *db, int flags,
-
    struct triggers *t)
+
    struct triggers *t, struct deferred_rc *rc)
{
	xstring		*message = NULL;
	int		 ret, cancel = 0;
@@ -82,12 +82,16 @@ pkg_delete(struct pkg *pkg, struct pkg *rpkg, struct pkgdb *db, int flags,
	}

	/*
-
	 * stop the different related services if the users do want that
-
	 * and that the service is running
+
	 * Record rc scripts for deferred stop at the end of the transaction,
+
	 * or stop them immediately if running outside a transaction.
	 */
	handle_rc = pkg_object_bool(pkg_config_get("HANDLE_RC_SCRIPTS"));
-
	if (handle_rc)
-
		pkg_start_stop_rc_scripts(pkg, PKG_RC_STOP);
+
	if (handle_rc) {
+
		if (rc != NULL)
+
			pkg_deferred_rc_add(rc, pkg, PKG_RC_STOP);
+
		else
+
			pkg_start_stop_rc_scripts(pkg, PKG_RC_STOP);
+
	}

	if ((flags & PKG_DELETE_NOSCRIPT) == 0) {
		bool noexec = ((flags & PKG_DELETE_NOEXEC) == PKG_DELETE_NOEXEC);
modified libpkg/pkg_jobs.c
@@ -91,6 +91,7 @@ pkg_jobs_new(struct pkg_jobs **j, pkg_jobs_t t, struct pkgdb *db)
	(*j)->solved = false;
	(*j)->pinning = true;
	(*j)->flags = PKG_FLAG_NONE;
+
	pkg_deferred_rc_init(&(*j)->rc);
	(*j)->conservative = pkg_object_bool(pkg_config_get("CONSERVATIVE_UPGRADE"));
	(*j)->triggers.dfd = -1;

@@ -202,6 +203,7 @@ pkg_jobs_free(struct pkg_jobs *j)
		close(j->triggers.dfd);
	if (j->triggers.schema != NULL)
		ucl_object_unref(j->triggers.schema);
+
	pkg_deferred_rc_free(&j->rc);
	pkghash_destroy(j->orphaned);
	pkghash_destroy(j->notorphaned);
	vec_free_and_free(&j->system_shlibs, free);
@@ -2041,9 +2043,9 @@ pkg_jobs_handle_install(struct pkg_solved *ps, struct pkg_jobs *j)
	if (new->type == PKG_GROUP_REMOTE)
		retcode = pkg_add_group(new);
	else if (old != NULL)
-
		retcode = pkg_add_upgrade(j->db, target, flags, NULL, new, old, &j->triggers);
+
		retcode = pkg_add_upgrade(j->db, target, flags, NULL, new, old, &j->triggers, &j->rc);
	else
-
		retcode = pkg_add_from_remote(j->db, target, flags, NULL, new, &j->triggers);
+
		retcode = pkg_add_from_remote(j->db, target, flags, NULL, new, &j->triggers, &j->rc);

	dbg(2, "end %s:", __func__);
	return (retcode);
@@ -2066,7 +2068,7 @@ pkg_jobs_handle_delete(struct pkg_solved *ps, struct pkg_jobs *j)
		rpkg = ps->xlink->items[0]->pkg;
	}
	return (pkg_delete(ps->items[0]->pkg, rpkg, j->db, flags,
-
	    &j->triggers));
+
	    &j->triggers, &j->rc));
}

static int
@@ -2167,6 +2169,7 @@ pkg_jobs_execute(struct pkg_jobs *j)

	pkg_plugins_hook_run(post, j, j->db);
	triggers_execute(&j->triggers);
+
	pkg_deferred_rc_execute(&j->rc);

cleanup:
	pkgdb_release_lock(j->db, PKGDB_LOCK_EXCLUSIVE);
modified libpkg/private/pkg.h
@@ -291,6 +291,35 @@ struct triggers {
	trigger_t *post_transaction;
};

+
/*
+
 * Deferred rc script handling.
+
 *
+
 * During a transaction we collect the names of rc scripts that need to be
+
 * stopped and/or started.  Old scripts are copied to a temporary directory
+
 * so they survive file replacement.  At the end of the transaction:
+
 *
+
 * - Upgraded services (in both stop and start sets) are restarted via
+
 *   "service <name> restart", letting the rc script handle the transition
+
 *   gracefully (e.g. sshd preserves active connections).
+
 * - Deleted services (stop only) are stopped using the saved old script.
+
 * - Newly installed services (start only) are started if enabled.
+
 *
+
 * Skipped entirely when operating on an alternate rootdir (pkg -r).
+
 */
+
struct deferred_rc_stop {
+
	char *name;		/* rc script basename, e.g. "nginx" */
+
	char *oldpath;		/* saved copy in tmpdir, or NULL */
+
};
+
typedef vec_t(struct deferred_rc_stop) rc_stop_t;
+

+
struct deferred_rc {
+
	char *tmpdir;		/* mkdtemp'd dir for saved old scripts */
+
	rc_stop_t to_stop;	/* services to stop (deletions & upgrades) */
+
	charv_t to_start;	/* service names to start (installs & upgrades) */
+
	pkghash *seen_stop;	/* dedup: names already in to_stop */
+
	pkghash *seen_start;	/* dedup: names already in to_start */
+
};
+

struct pkg_create {
	bool overwrite;
	bool expand_manifest;
@@ -683,7 +712,7 @@ typedef enum {
 * @return An error code.
 */
int pkg_delete(struct pkg *pkg, struct pkg *rpkg, struct pkgdb *db, int flags,
-
    struct triggers *);
+
    struct triggers *, struct deferred_rc *);
#define PKG_DELETE_UPGRADE	(1 << 1)	/* delete as a split upgrade */
#define PKG_DELETE_NOSCRIPT	(1 << 2)	/* don't run delete scripts */
#define PKG_DELETE_NOEXEC	(1 << 3)	/* don't run delete scripts which execute things*/
@@ -721,6 +750,10 @@ int pkg_repo_load_fingerprints(struct pkg_repo *repo);


int pkg_start_stop_rc_scripts(struct pkg *, pkg_rc_attr attr);
+
void pkg_deferred_rc_init(struct deferred_rc *);
+
void pkg_deferred_rc_free(struct deferred_rc *);
+
void pkg_deferred_rc_add(struct deferred_rc *, struct pkg *, pkg_rc_attr);
+
int pkg_deferred_rc_execute(struct deferred_rc *);

int pkg_script_run(struct pkg *, pkg_script type, bool upgrade, bool noexec);
int pkg_lua_script_run(struct pkg *, pkg_lua_script type, bool upgrade);
@@ -829,9 +862,11 @@ char *pkg_checksum_generate_fileat(int fd, const char *path,

int pkg_add_group(struct pkg *pkg);
int pkg_add_upgrade(struct pkgdb *db, const char *path, unsigned flags,
-
    const char *location, struct pkg *rp, struct pkg *lp, struct triggers *);
+
    const char *location, struct pkg *rp, struct pkg *lp, struct triggers *,
+
    struct deferred_rc *);
int pkg_add_from_remote(struct pkgdb *db, const char *path, unsigned flags,
-
    const char *location, struct pkg *rp, struct triggers *);
+
    const char *location, struct pkg *rp, struct triggers *,
+
    struct deferred_rc *);
void pkg_delete_dir(struct pkg *pkg, struct pkg_dir *dir);
void pkg_delete_file(struct pkg *pkg, struct pkg_file *file);
int pkg_open_root_fd(struct pkg *pkg);
modified libpkg/private/pkg_jobs.h
@@ -140,6 +140,7 @@ struct pkg_jobs {
	bool ignore_compat32;
	void		*lockedpkgs;
	struct triggers triggers;
+
	struct deferred_rc rc;
	struct pkghash *orphaned;
	struct pkghash *notorphaned;
	charv_t system_shlibs;
modified libpkg/rcscripts.c
@@ -1,7 +1,6 @@
/*-
-
 * Copyright (c) 2011-2012 Baptiste Daroussin <bapt@FreeBSD.org>
-
 * All rights reserved.
-
 * 
+
 * Copyright (c) 2011-2026 Baptiste Daroussin <bapt@FreeBSD.org>
+
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
@@ -11,7 +10,7 @@
 * 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(S) ``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.
@@ -24,23 +23,124 @@
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

+
#include <sys/stat.h>
#include <sys/wait.h>

#include <errno.h>
#include <fcntl.h>
#include <spawn.h>
+
#include <stdio.h>
+
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "pkg.h"
#include "private/pkg.h"
#include "private/event.h"
-

-
static int rc_stop(const char *);
-
static int rc_start(const char *);
+
#include "xmalloc.h"
+
#include "pkghash.h"

extern char **environ;

+
/*
+
 * Run a command and return its exit status, or -1 on error.
+
 * If quiet is true, stdout and stderr are redirected to /dev/null.
+
 */
+
static int
+
run_cmd(const char *program, const char **argv, bool quiet)
+
{
+
	posix_spawn_file_actions_t actions, *actionsp = NULL;
+
	int error, pstat;
+
	pid_t pid;
+

+
	if (quiet) {
+
		if ((error = posix_spawn_file_actions_init(&actions)) != 0 ||
+
		    (error = posix_spawn_file_actions_addopen(&actions,
+
		    STDOUT_FILENO, "/dev/null", O_RDONLY, 0)) != 0 ||
+
		    (error = posix_spawn_file_actions_addopen(&actions,
+
		    STDERR_FILENO, "/dev/null", O_RDONLY, 0)) != 0) {
+
			posix_spawn_file_actions_destroy(&actions);
+
			errno = error;
+
			return (-1);
+
		}
+
		actionsp = &actions;
+
	}
+

+
	error = posix_spawn(&pid, program, actionsp, NULL,
+
	    __DECONST(char **, argv), environ);
+
	if (actionsp != NULL)
+
		posix_spawn_file_actions_destroy(actionsp);
+
	if (error != 0) {
+
		errno = error;
+
		return (-1);
+
	}
+

+
	while (waitpid(pid, &pstat, 0) == -1) {
+
		if (errno != EINTR)
+
			return (-1);
+
	}
+

+
	return (WEXITSTATUS(pstat));
+
}
+

+
/*
+
 * Run "service <name> <cmd>" quietly and return exit status.
+
 */
+
static int
+
service_cmd(const char *name, const char *cmd)
+
{
+
	const char *argv[4];
+

+
	argv[0] = "service";
+
	argv[1] = name;
+
	argv[2] = cmd;
+
	argv[3] = NULL;
+
	return (run_cmd("/usr/sbin/service", argv, true));
+
}
+

+
/*
+
 * Run "<script_path> <cmd>" quietly and return exit status.
+
 */
+
static int
+
script_cmd(const char *script_path, const char *cmd)
+
{
+
	const char *argv[3];
+

+
	argv[0] = script_path;
+
	argv[1] = cmd;
+
	argv[2] = NULL;
+
	return (run_cmd(script_path, argv, true));
+
}
+

+
static int
+
rc_stop(const char *rc_file)
+
{
+
	if (rc_file == NULL)
+
		return (0);
+

+
	/* use faststrop to avoid checking if the servie was running */
+
	return (service_cmd(rc_file, "faststop"));
+
}
+

+
static int
+
rc_stop_with_script(const char *script_path)
+
{
+
	if (script_path == NULL)
+
		return (0);
+

+
	/* use faststrop to avoid checking if the servie was running */
+
	return (script_cmd(script_path, "faststop"));
+
}
+

+
static int
+
rc_start(const char *rc_file)
+
{
+
	if (rc_file == NULL)
+
		return (0);
+

+
	return (service_cmd(rc_file, "start"));
+
}
+

int
pkg_start_stop_rc_scripts(struct pkg *pkg, pkg_rc_attr attr)
{
@@ -55,6 +155,10 @@ pkg_start_stop_rc_scripts(struct pkg *pkg, pkg_rc_attr attr)
	if (!handle_rc)
		return (ret);

+
	/* Do not manage rc scripts when operating on an alternate rootdir */
+
	if (ctx.pkg_rootdir != NULL)
+
		return (ret);
+

	snprintf(rc_d_path, sizeof(rc_d_path), "%s/etc/rc.d/", pkg->prefix);
	len = strlen(rc_d_path);

@@ -77,88 +181,223 @@ pkg_start_stop_rc_scripts(struct pkg *pkg, pkg_rc_attr attr)
}

static int
-
rc_stop(const char *rc_file)
+
copy_file(const char *src, const char *dst)
{
-
	int error, pstat;
-
	pid_t pid;
-
	posix_spawn_file_actions_t actions;
-
	const char *argv[4];
+
	int sfd, dfd;
+
	char buf[BUFSIZ];
+
	ssize_t n;
+
	struct stat sb;

-
	if (rc_file == NULL)
-
		return (0);
-

-
	argv[0] = "service";
-
	argv[1] = rc_file;
-
	argv[2] = "onestatus";
-
	argv[3] = NULL;
+
	sfd = open(src, O_RDONLY);
+
	if (sfd == -1)
+
		return (-1);

-
	if ((error = posix_spawn_file_actions_init(&actions)) != 0 ||
-
	    (error = posix_spawn_file_actions_addopen(&actions,
-
	    STDOUT_FILENO, "/dev/null", O_RDONLY, 0)) != 0 ||
-
	    (error = posix_spawn_file_actions_addopen(&actions,
-
	    STDERR_FILENO, "/dev/null", O_RDONLY, 0)) != 0 ||
-
	    (error = posix_spawn(&pid, "/usr/sbin/service", &actions, NULL,
-
	    __DECONST(char **, argv), environ)) != 0) {
-
		errno = error;
-
		pkg_errno("Cannot query service '%s'", rc_file);
+
	dfd = open(dst, O_WRONLY | O_CREAT | O_TRUNC, 0700);
+
	if (dfd == -1) {
+
		close(sfd);
		return (-1);
	}

-
	while (waitpid(pid, &pstat, 0) == -1) {
-
		if (errno != EINTR)
+
	for (;;) {
+
		n = read(sfd, buf, sizeof(buf));
+
		if (n == -1) {
+
			if (errno == EINTR)
+
				continue;
+
			close(sfd);
+
			close(dfd);
			return (-1);
+
		}
+
		if (n == 0)
+
			break;
+
		const char *p = buf;
+
		ssize_t remaining = n;
+
		while (remaining > 0) {
+
			ssize_t w = write(dfd, p, remaining);
+
			if (w == -1) {
+
				if (errno == EINTR)
+
					continue;
+
				close(sfd);
+
				close(dfd);
+
				return (-1);
+
			}
+
			p += w;
+
			remaining -= w;
+
		}
	}

-
	if (WEXITSTATUS(pstat) != 0)
-
		return (0);
+
	if (fstat(sfd, &sb) == 0)
+
		fchmod(dfd, sb.st_mode);

-
	posix_spawn_file_actions_destroy(&actions);
+
	close(sfd);
+
	close(dfd);
+
	return (n < 0 ? -1 : 0);
+
}

-
	argv[2] = "stop";
+
void
+
pkg_deferred_rc_init(struct deferred_rc *rc)
+
{
+
	memset(rc, 0, sizeof(*rc));
+
}

-
	if ((error = posix_spawn(&pid, "/usr/sbin/service", NULL, NULL,
-
	    __DECONST(char **, argv), environ)) != 0) {
-
		errno = error;
-
		pkg_errno("Cannot stop service '%s'", rc_file);
-
		return (-1);
-
	}
+
static void
+
deferred_rc_cleanup_cb(void *data)
+
{
+
	struct deferred_rc *rc = data;

-
	while (waitpid(pid, &pstat, 0) == -1) {
-
		if (errno != EINTR)
-
			return (-1);
+
	pkg_deferred_rc_free(rc);
+
}
+

+
static void
+
deferred_rc_stop_free(struct deferred_rc_stop *s)
+
{
+
	if (s->oldpath != NULL) {
+
		unlink(s->oldpath);
+
		free(s->oldpath);
	}
+
	free(s->name);
+
}

-
	return (WEXITSTATUS(pstat));
+
void
+
pkg_deferred_rc_free(struct deferred_rc *rc)
+
{
+
	if (rc == NULL)
+
		return;
+

+
	vec_foreach(rc->to_stop, i)
+
		deferred_rc_stop_free(&rc->to_stop.d[i]);
+
	vec_free(&rc->to_stop);
+

+
	vec_foreach(rc->to_start, i)
+
		free(rc->to_start.d[i]);
+
	vec_free(&rc->to_start);
+

+
	pkghash_destroy(rc->seen_stop);
+
	rc->seen_stop = NULL;
+
	pkghash_destroy(rc->seen_start);
+
	rc->seen_start = NULL;
+

+
	if (rc->tmpdir != NULL) {
+
		rmdir(rc->tmpdir);
+
		free(rc->tmpdir);
+
		rc->tmpdir = NULL;
+
	}
}

static int
-
rc_start(const char *rc_file)
+
deferred_rc_ensure_tmpdir(struct deferred_rc *rc)
{
-
	int error, pstat;
-
	pid_t pid;
-
	const char *argv[4];
-

-
	if (rc_file == NULL)
+
	if (rc->tmpdir != NULL)
		return (0);

-
	argv[0] = "service";
-
	argv[1] = rc_file;
-
	argv[2] = "quietstart";
-
	argv[3] = NULL;
+
	const char *tmpdir = getenv("TMPDIR");
+
	if (tmpdir == NULL)
+
		tmpdir = "/tmp";

-
	if ((error = posix_spawn(&pid, "/usr/sbin/service", NULL, NULL,
-
	    __DECONST(char **, argv), environ)) != 0) {
-
		errno = error;
-
		pkg_errno("Cannot start service '%s'", rc_file);
+
	char template[PATH_MAX];
+
	snprintf(template, sizeof(template), "%s/pkg-rc.XXXXXX", tmpdir);
+
	if (mkdtemp(template) == NULL) {
+
		pkg_errno("Cannot create temporary directory '%s'", template);
		return (-1);
	}
+
	rc->tmpdir = xstrdup(template);

-
	while (waitpid(pid, &pstat, 0) == -1) {
-
		if (errno != EINTR)
-
			return (-1);
-
	}
+
	pkg_register_cleanup_callback(deferred_rc_cleanup_cb, rc);

-
	return (WEXITSTATUS(pstat));
+
	return (0);
}

+
void
+
pkg_deferred_rc_add(struct deferred_rc *rc, struct pkg *pkg, pkg_rc_attr attr)
+
{
+
	struct pkg_file *file = NULL;
+
	char rc_d_path[PATH_MAX];
+
	size_t len;
+
	bool handle_rc;
+

+
	handle_rc = pkg_object_bool(pkg_config_get("HANDLE_RC_SCRIPTS"));
+
	if (!handle_rc)
+
		return;
+

+
	/* Do not manage rc scripts when operating on an alternate rootdir */
+
	if (ctx.pkg_rootdir != NULL)
+
		return;

+
	snprintf(rc_d_path, sizeof(rc_d_path), "%s/etc/rc.d/", pkg->prefix);
+
	len = strlen(rc_d_path);
+

+
	while (pkg_files(pkg, &file) == EPKG_OK) {
+
		if (strncmp(rc_d_path, file->path, len) != 0)
+
			continue;
+

+
		const char *rcname = file->path + len;
+

+
		switch (attr) {
+
		case PKG_RC_STOP:
+
			if (pkghash_get(rc->seen_stop, rcname) != NULL)
+
				break;
+
			pkghash_safe_add(rc->seen_stop, rcname, NULL, NULL);
+

+
			struct deferred_rc_stop entry = { .name = xstrdup(rcname) };
+
			if (deferred_rc_ensure_tmpdir(rc) == 0) {
+
				char saved[PATH_MAX];
+
				snprintf(saved, sizeof(saved), "%s/%s",
+
				    rc->tmpdir, rcname);
+
				if (copy_file(file->path, saved) == 0)
+
					entry.oldpath = xstrdup(saved);
+
				else
+
					pkg_debug(1,
+
					    "Failed to save rc script %s, "
+
					    "will use service(8) to stop",
+
					    file->path);
+
			}
+
			vec_push(&rc->to_stop, entry);
+
			break;
+
		case PKG_RC_START:
+
			if (pkghash_get(rc->seen_start, rcname) != NULL)
+
				break;
+
			pkghash_safe_add(rc->seen_start, rcname, NULL, NULL);
+
			vec_push(&rc->to_start, xstrdup(rcname));
+
			break;
+
		}
+
	}
+
}
+

+
int
+
pkg_deferred_rc_execute(struct deferred_rc *rc)
+
{
+
	int ret = 0;
+

+
	/*
+
	 * Upgrades (in both stop and start sets): "service <name> restart"
+
	 * so the rc script can handle the transition gracefully.
+
	 * Deletions (stop only): stop using the saved old script.
+
	 * New installs (start only): start if enabled.
+
	 */
+
	vec_foreach(rc->to_stop, i) {
+
		struct deferred_rc_stop *s = &rc->to_stop.d[i];
+
		if (pkghash_get(rc->seen_start, s->name) != NULL) {
+
			ret += service_cmd(s->name, "restart");
+
		} else {
+
			if (s->oldpath != NULL) {
+
				ret += rc_stop_with_script(s->oldpath);
+
			} else {
+
				ret += rc_stop(s->name);
+
			}
+
		}
+
	}
+

+
	/* Start only services that were not already restarted above */
+
	vec_foreach(rc->to_start, i) {
+
		char *name = rc->to_start.d[i];
+
		if (pkghash_get(rc->seen_stop, name) != NULL)
+
			continue;
+
		pkg_emit_notice("Starting %s", name);
+
		ret += rc_start(name);
+
	}
+

+
	if (rc->tmpdir != NULL)
+
		pkg_unregister_cleanup_callback(deferred_rc_cleanup_cb, rc);
+
	pkg_deferred_rc_free(rc);
+

+
	return (ret);
+
}
modified tests/Makefile.in
@@ -20,7 +20,8 @@ TESTS= \
	pkg_elf \
	hash \
	shlibs \
-
	kv
+
	kv \
+
	rcscripts

TESTS_SH= \
	frontend/audit.sh \
@@ -132,6 +133,7 @@ shlibs_OBJS= lib/shlibs.o
kv_OBJS=	lib/kv.o
pkg_OBJS=	lib/pkg.o
pkg_osvf_OBJS=	lib/pkg_osvf.o
+
rcscripts_OBJS=	lib/rcscripts.o

SRCS=	\
	$(packing_OBJS:.o=.c) \
@@ -153,7 +155,8 @@ SRCS= \
	$(hash_OBJS:.o=.c) \
	$(shlibs_OBJS:.o=.c) \
	$(kv_OBJS:.o=.c) \
-
	$(pkg_OBJS:.o=.c)
+
	$(pkg_OBJS:.o=.c) \
+
	$(rcscripts_OBJS:.o=.c)

include $(MK)/common.mk

added tests/lib/rcscripts.c
@@ -0,0 +1,378 @@
+
/*-
+
 * Copyright (c) 2026 Baptiste Daroussin <bapt@FreeBSD.org>
+
 *
+
 * SPDX-License-Identifier: BSD-2-Clause
+
 */
+

+
#include <sys/stat.h>
+

+
#include <atf-c.h>
+
#include <fcntl.h>
+
#include <stdio.h>
+
#include <stdlib.h>
+
#include <string.h>
+
#include <unistd.h>
+

+
#include <private/pkg.h>
+
#include <xmalloc.h>
+
#include <pkghash.h>
+

+
ATF_TC_WITHOUT_HEAD(deferred_rc_init_free);
+
ATF_TC_WITHOUT_HEAD(deferred_rc_free_null);
+
ATF_TC_WITHOUT_HEAD(deferred_rc_stop_entries);
+
ATF_TC_WITHOUT_HEAD(deferred_rc_start_entries);
+
ATF_TC_WITHOUT_HEAD(deferred_rc_dedup);
+
ATF_TC_WITHOUT_HEAD(deferred_rc_tmpdir_cleanup);
+
ATF_TC_WITHOUT_HEAD(deferred_rc_free_reuse);
+
ATF_TC_WITHOUT_HEAD(deferred_rc_stop_all_null_oldpath);
+
ATF_TC_WITHOUT_HEAD(deferred_rc_tmpdir_multiple_scripts);
+
ATF_TC_WITHOUT_HEAD(deferred_rc_seen_sets_independent);
+
ATF_TC_WITHOUT_HEAD(deferred_rc_mixed_stop_start);
+

+
ATF_TC_BODY(deferred_rc_init_free, tc)
+
{
+
	struct deferred_rc rc;
+

+
	pkg_deferred_rc_init(&rc);
+
	ATF_REQUIRE_EQ(rc.tmpdir, NULL);
+
	ATF_REQUIRE_EQ(rc.to_stop.len, 0);
+
	ATF_REQUIRE_EQ(rc.to_start.len, 0);
+
	ATF_REQUIRE_EQ(rc.seen_stop, NULL);
+
	ATF_REQUIRE_EQ(rc.seen_start, NULL);
+

+
	pkg_deferred_rc_free(&rc);
+
	ATF_REQUIRE_EQ(rc.tmpdir, NULL);
+
	ATF_REQUIRE_EQ(rc.to_stop.len, 0);
+
	ATF_REQUIRE_EQ(rc.to_start.len, 0);
+
}
+

+
ATF_TC_BODY(deferred_rc_free_null, tc)
+
{
+
	pkg_deferred_rc_free(NULL);
+
}
+

+
ATF_TC_BODY(deferred_rc_stop_entries, tc)
+
{
+
	struct deferred_rc rc;
+
	struct deferred_rc_stop s;
+

+
	pkg_deferred_rc_init(&rc);
+

+
	s.name = xstrdup("sshd");
+
	s.oldpath = xstrdup("/tmp/fakepath/sshd");
+
	vec_push(&rc.to_stop, s);
+

+
	s.name = xstrdup("nginx");
+
	s.oldpath = NULL;
+
	vec_push(&rc.to_stop, s);
+

+
	ATF_REQUIRE_EQ(rc.to_stop.len, 2);
+
	ATF_REQUIRE_STREQ(rc.to_stop.d[0].name, "sshd");
+
	ATF_REQUIRE(rc.to_stop.d[0].oldpath != NULL);
+
	ATF_REQUIRE_STREQ(rc.to_stop.d[1].name, "nginx");
+
	ATF_REQUIRE_EQ(rc.to_stop.d[1].oldpath, NULL);
+
	ATF_REQUIRE_EQ(rc.to_start.len, 0);
+

+
	/* Clean up without unlink since paths are fake */
+
	free(rc.to_stop.d[0].oldpath);
+
	rc.to_stop.d[0].oldpath = NULL;
+
	pkg_deferred_rc_free(&rc);
+
}
+

+
ATF_TC_BODY(deferred_rc_start_entries, tc)
+
{
+
	struct deferred_rc rc;
+

+
	pkg_deferred_rc_init(&rc);
+

+
	vec_push(&rc.to_start, xstrdup("postfix"));
+
	vec_push(&rc.to_start, xstrdup("dovecot"));
+

+
	ATF_REQUIRE_EQ(rc.to_start.len, 2);
+
	ATF_REQUIRE_STREQ(rc.to_start.d[0], "postfix");
+
	ATF_REQUIRE_STREQ(rc.to_start.d[1], "dovecot");
+
	ATF_REQUIRE_EQ(rc.to_stop.len, 0);
+

+
	pkg_deferred_rc_free(&rc);
+
}
+

+
ATF_TC_BODY(deferred_rc_dedup, tc)
+
{
+
	struct deferred_rc rc;
+
	struct deferred_rc_stop s;
+

+
	pkg_deferred_rc_init(&rc);
+

+
	/* Simulate what pkg_deferred_rc_add does for dedup */
+
	pkghash_safe_add(rc.seen_stop, "svc", NULL, NULL);
+
	s.name = xstrdup("svc");
+
	s.oldpath = NULL;
+
	vec_push(&rc.to_stop, s);
+

+
	/* Second add should be detected via seen_stop */
+
	ATF_REQUIRE(pkghash_get(rc.seen_stop, "svc") != NULL);
+
	/* Don't add again — this is what pkg_deferred_rc_add checks */
+
	ATF_REQUIRE_EQ(rc.to_stop.len, 1);
+

+
	/* Same for start */
+
	pkghash_safe_add(rc.seen_start, "svc2", NULL, NULL);
+
	vec_push(&rc.to_start, xstrdup("svc2"));
+

+
	ATF_REQUIRE(pkghash_get(rc.seen_start, "svc2") != NULL);
+
	ATF_REQUIRE_EQ(rc.to_start.len, 1);
+

+
	/* Cross-lookup: seen_start used to detect upgrades in execute */
+
	pkghash_safe_add(rc.seen_start, "svc", NULL, NULL);
+
	ATF_REQUIRE(pkghash_get(rc.seen_start, "svc") != NULL);
+

+
	pkg_deferred_rc_free(&rc);
+
}
+

+
ATF_TC_BODY(deferred_rc_tmpdir_cleanup, tc)
+
{
+
	struct deferred_rc rc;
+
	struct deferred_rc_stop s;
+
	char tdir[] = "/tmp/pkg-test-rc.XXXXXX";
+
	char script_path[PATH_MAX];
+
	char *saved_tmpdir;
+
	int fd;
+

+
	ATF_REQUIRE(mkdtemp(tdir) != NULL);
+

+
	pkg_deferred_rc_init(&rc);
+
	rc.tmpdir = xstrdup(tdir);
+

+
	snprintf(script_path, sizeof(script_path), "%s/fakesvc", tdir);
+
	fd = open(script_path, O_WRONLY | O_CREAT, 0755);
+
	ATF_REQUIRE(fd != -1);
+
	write(fd, "#!/bin/sh\n", 10);
+
	close(fd);
+

+
	s.name = xstrdup("fakesvc");
+
	s.oldpath = xstrdup(script_path);
+
	vec_push(&rc.to_stop, s);
+

+
	saved_tmpdir = xstrdup(rc.tmpdir);
+
	ATF_REQUIRE(access(script_path, F_OK) == 0);
+

+
	pkg_deferred_rc_free(&rc);
+

+
	ATF_REQUIRE_EQ_MSG(access(script_path, F_OK), -1,
+
	    "saved rc script should have been removed");
+
	ATF_REQUIRE_EQ_MSG(access(saved_tmpdir, F_OK), -1,
+
	    "tmpdir should have been removed");
+

+
	free(saved_tmpdir);
+
}
+

+
ATF_TC_BODY(deferred_rc_free_reuse, tc)
+
{
+
	struct deferred_rc rc;
+
	struct deferred_rc_stop s;
+

+
	pkg_deferred_rc_init(&rc);
+

+
	s.name = xstrdup("sshd");
+
	s.oldpath = NULL;
+
	vec_push(&rc.to_stop, s);
+
	vec_push(&rc.to_start, xstrdup("nginx"));
+

+
	pkghash_safe_add(rc.seen_stop, "sshd", NULL, NULL);
+
	pkghash_safe_add(rc.seen_start, "nginx", NULL, NULL);
+

+
	pkg_deferred_rc_free(&rc);
+

+
	/* Re-init and reuse the same struct */
+
	pkg_deferred_rc_init(&rc);
+
	ATF_REQUIRE_EQ(rc.tmpdir, NULL);
+
	ATF_REQUIRE_EQ(rc.to_stop.len, 0);
+
	ATF_REQUIRE_EQ(rc.to_start.len, 0);
+
	ATF_REQUIRE_EQ(rc.seen_stop, NULL);
+
	ATF_REQUIRE_EQ(rc.seen_start, NULL);
+

+
	s.name = xstrdup("postfix");
+
	s.oldpath = NULL;
+
	vec_push(&rc.to_stop, s);
+
	ATF_REQUIRE_EQ(rc.to_stop.len, 1);
+
	ATF_REQUIRE_STREQ(rc.to_stop.d[0].name, "postfix");
+

+
	pkg_deferred_rc_free(&rc);
+
}
+

+
ATF_TC_BODY(deferred_rc_stop_all_null_oldpath, tc)
+
{
+
	struct deferred_rc rc;
+
	struct deferred_rc_stop s;
+

+
	pkg_deferred_rc_init(&rc);
+

+
	s.name = xstrdup("sshd");
+
	s.oldpath = NULL;
+
	vec_push(&rc.to_stop, s);
+

+
	s.name = xstrdup("nginx");
+
	s.oldpath = NULL;
+
	vec_push(&rc.to_stop, s);
+

+
	s.name = xstrdup("postfix");
+
	s.oldpath = NULL;
+
	vec_push(&rc.to_stop, s);
+

+
	ATF_REQUIRE_EQ(rc.to_stop.len, 3);
+
	ATF_REQUIRE_EQ(rc.to_stop.d[0].oldpath, NULL);
+
	ATF_REQUIRE_EQ(rc.to_stop.d[1].oldpath, NULL);
+
	ATF_REQUIRE_EQ(rc.to_stop.d[2].oldpath, NULL);
+

+
	/* free should handle all-NULL oldpaths without issue */
+
	pkg_deferred_rc_free(&rc);
+
}
+

+
ATF_TC_BODY(deferred_rc_tmpdir_multiple_scripts, tc)
+
{
+
	struct deferred_rc rc;
+
	struct deferred_rc_stop s;
+
	char tdir[] = "/tmp/pkg-test-rc.XXXXXX";
+
	char path1[PATH_MAX], path2[PATH_MAX], path3[PATH_MAX];
+
	int fd;
+

+
	ATF_REQUIRE(mkdtemp(tdir) != NULL);
+

+
	pkg_deferred_rc_init(&rc);
+
	rc.tmpdir = xstrdup(tdir);
+

+
	/* Create three fake scripts in the tmpdir */
+
	snprintf(path1, sizeof(path1), "%s/svc_a", tdir);
+
	fd = open(path1, O_WRONLY | O_CREAT, 0755);
+
	ATF_REQUIRE(fd != -1);
+
	write(fd, "#!/bin/sh\n", 10);
+
	close(fd);
+

+
	snprintf(path2, sizeof(path2), "%s/svc_b", tdir);
+
	fd = open(path2, O_WRONLY | O_CREAT, 0755);
+
	ATF_REQUIRE(fd != -1);
+
	write(fd, "#!/bin/sh\n", 10);
+
	close(fd);
+

+
	snprintf(path3, sizeof(path3), "%s/svc_c", tdir);
+
	fd = open(path3, O_WRONLY | O_CREAT, 0755);
+
	ATF_REQUIRE(fd != -1);
+
	write(fd, "#!/bin/sh\n", 10);
+
	close(fd);
+

+
	s.name = xstrdup("svc_a");
+
	s.oldpath = xstrdup(path1);
+
	vec_push(&rc.to_stop, s);
+

+
	s.name = xstrdup("svc_b");
+
	s.oldpath = xstrdup(path2);
+
	vec_push(&rc.to_stop, s);
+

+
	s.name = xstrdup("svc_c");
+
	s.oldpath = xstrdup(path3);
+
	vec_push(&rc.to_stop, s);
+

+
	ATF_REQUIRE_EQ(rc.to_stop.len, 3);
+

+
	char *saved_tmpdir = xstrdup(rc.tmpdir);
+
	pkg_deferred_rc_free(&rc);
+

+
	/* All three scripts and the tmpdir should be gone */
+
	ATF_REQUIRE_EQ_MSG(access(path1, F_OK), -1,
+
	    "svc_a script should have been removed");
+
	ATF_REQUIRE_EQ_MSG(access(path2, F_OK), -1,
+
	    "svc_b script should have been removed");
+
	ATF_REQUIRE_EQ_MSG(access(path3, F_OK), -1,
+
	    "svc_c script should have been removed");
+
	ATF_REQUIRE_EQ_MSG(access(saved_tmpdir, F_OK), -1,
+
	    "tmpdir should have been removed");
+

+
	free(saved_tmpdir);
+
}
+

+
ATF_TC_BODY(deferred_rc_seen_sets_independent, tc)
+
{
+
	struct deferred_rc rc;
+

+
	pkg_deferred_rc_init(&rc);
+

+
	/* Add to seen_stop only */
+
	pkghash_safe_add(rc.seen_stop, "only_stop", NULL, NULL);
+
	/* Add to seen_start only */
+
	pkghash_safe_add(rc.seen_start, "only_start", NULL, NULL);
+

+
	/* Each should be in its own set but not the other */
+
	ATF_REQUIRE(pkghash_get(rc.seen_stop, "only_stop") != NULL);
+
	ATF_REQUIRE(pkghash_get(rc.seen_start, "only_stop") == NULL);
+

+
	ATF_REQUIRE(pkghash_get(rc.seen_start, "only_start") != NULL);
+
	ATF_REQUIRE(pkghash_get(rc.seen_stop, "only_start") == NULL);
+

+
	/* A name not in either set */
+
	ATF_REQUIRE(pkghash_get(rc.seen_stop, "unknown") == NULL);
+
	ATF_REQUIRE(pkghash_get(rc.seen_start, "unknown") == NULL);
+

+
	pkg_deferred_rc_free(&rc);
+
}
+

+
ATF_TC_BODY(deferred_rc_mixed_stop_start, tc)
+
{
+
	struct deferred_rc rc;
+
	struct deferred_rc_stop s;
+

+
	pkg_deferred_rc_init(&rc);
+

+
	/*
+
	 * Simulate an upgrade scenario: sshd appears in both stop and start.
+
	 * nginx is only stopped (deleted).
+
	 * postfix is only started (new install).
+
	 */
+
	pkghash_safe_add(rc.seen_stop, "sshd", NULL, NULL);
+
	s.name = xstrdup("sshd");
+
	s.oldpath = NULL;
+
	vec_push(&rc.to_stop, s);
+

+
	pkghash_safe_add(rc.seen_stop, "nginx", NULL, NULL);
+
	s.name = xstrdup("nginx");
+
	s.oldpath = NULL;
+
	vec_push(&rc.to_stop, s);
+

+
	pkghash_safe_add(rc.seen_start, "sshd", NULL, NULL);
+
	vec_push(&rc.to_start, xstrdup("sshd"));
+

+
	pkghash_safe_add(rc.seen_start, "postfix", NULL, NULL);
+
	vec_push(&rc.to_start, xstrdup("postfix"));
+

+
	ATF_REQUIRE_EQ(rc.to_stop.len, 2);
+
	ATF_REQUIRE_EQ(rc.to_start.len, 2);
+

+
	/* sshd is an upgrade: in both sets */
+
	ATF_REQUIRE(pkghash_get(rc.seen_stop, "sshd") != NULL);
+
	ATF_REQUIRE(pkghash_get(rc.seen_start, "sshd") != NULL);
+

+
	/* nginx is a deletion: stop only */
+
	ATF_REQUIRE(pkghash_get(rc.seen_stop, "nginx") != NULL);
+
	ATF_REQUIRE(pkghash_get(rc.seen_start, "nginx") == NULL);
+

+
	/* postfix is a new install: start only */
+
	ATF_REQUIRE(pkghash_get(rc.seen_stop, "postfix") == NULL);
+
	ATF_REQUIRE(pkghash_get(rc.seen_start, "postfix") != NULL);
+

+
	pkg_deferred_rc_free(&rc);
+
}
+

+
ATF_TP_ADD_TCS(tp)
+
{
+
	ATF_TP_ADD_TC(tp, deferred_rc_init_free);
+
	ATF_TP_ADD_TC(tp, deferred_rc_free_null);
+
	ATF_TP_ADD_TC(tp, deferred_rc_stop_entries);
+
	ATF_TP_ADD_TC(tp, deferred_rc_start_entries);
+
	ATF_TP_ADD_TC(tp, deferred_rc_dedup);
+
	ATF_TP_ADD_TC(tp, deferred_rc_tmpdir_cleanup);
+
	ATF_TP_ADD_TC(tp, deferred_rc_free_reuse);
+
	ATF_TP_ADD_TC(tp, deferred_rc_stop_all_null_oldpath);
+
	ATF_TP_ADD_TC(tp, deferred_rc_tmpdir_multiple_scripts);
+
	ATF_TP_ADD_TC(tp, deferred_rc_seen_sets_independent);
+
	ATF_TP_ADD_TC(tp, deferred_rc_mixed_stop_start);
+

+
	return (atf_no_error());
+
}