Skip site navigation (1)Skip section navigation (2)
Date:      Tue, 21 Jul 2026 11:34:28 +0000
From:      Dag-Erling=?utf-8?Q? Sm=C3=B8rg?=rav <des@FreeBSD.org>
To:        src-committers@FreeBSD.org, dev-commits-src-all@FreeBSD.org, dev-commits-src-main@FreeBSD.org
Subject:   git: c93f3b55e67f - main - libutil: Reimplement getlocalbase()
Message-ID:  <6a5f5944.3a8ff.647c3596@gitrepo.freebsd.org>

index | next in thread | raw e-mail

The branch main has been updated by des:

URL: https://cgit.FreeBSD.org/src/commit/?id=c93f3b55e67fe465d93f3f54ae4408f131fec3aa

commit c93f3b55e67fe465d93f3f54ae4408f131fec3aa
Author:     Dag-Erling Smørgrav <des@FreeBSD.org>
AuthorDate: 2026-07-21 08:31:26 +0000
Commit:     Dag-Erling Smørgrav <des@FreeBSD.org>
CommitDate: 2026-07-21 08:31:49 +0000

    libutil: Reimplement getlocalbase()
    
    * Get rid of the pointless LOCALBASE_CTL_LEN mechanism
    
    * Apply minimal normalization to the paths obtained from the environment
      or sysctl variable
    
    * Turn the manual page into a manual page
    
    * Add tests
    
    MFC after:      1 week
    Reviewed by:    se
    Differential Revision:  https://reviews.freebsd.org/D58362
---
 lib/libutil/getlocalbase.3            | 111 ++++++++++++++++------------------
 lib/libutil/getlocalbase.c            | 100 ++++++++++++++++++------------
 lib/libutil/tests/Makefile            |   1 +
 lib/libutil/tests/getlocalbase_test.c |  92 ++++++++++++++++++++++++++++
 4 files changed, 206 insertions(+), 98 deletions(-)

diff --git a/lib/libutil/getlocalbase.3 b/lib/libutil/getlocalbase.3
index 5c71a87be8a3..ea02ae801bbb 100644
--- a/lib/libutil/getlocalbase.3
+++ b/lib/libutil/getlocalbase.3
@@ -3,6 +3,7 @@
 .\"
 .\" Copyright 2020 Scott Long
 .\" Copyright 2020 Stefan Eßer
+.\" Copyright (c) 2026 Dag-Erling Smørgrav
 .\"
 .\" Redistribution and use in source and binary forms, with or without
 .\" modification, are permitted provided that the following conditions
@@ -25,97 +26,89 @@
 .\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 .\" SUCH DAMAGE.
 .\"
-.Dd July 11, 2023
+.Dd July 20, 2026
 .Dt GETLOCALBASE 3
 .Os
 .Sh NAME
 .Nm getlocalbase
-.Nd "return the path to the local software directory"
+.Nd "return the path to the local software base directory"
 .Sh LIBRARY
 .Lb libutil
 .Sh SYNOPSIS
 .In libutil.h
-.Ft const char*
+.Ft "const char *"
 .Fn getlocalbase "void"
 .Sh DESCRIPTION
 The
-.Fn getlocalbase
-function returns the path to the local software base directory.
-Normally this is the
-.Pa /usr/local
-directory.
+.Nm
+function returns the path to the local software base directory,
+normally
+.Pa /usr/local .
+.Pp
 First the
 .Ev LOCALBASE
 environment variable is checked.
-If that does not exist then the
+If that variable is undefined or empty, the
 .Va user.localbase
 sysctl is checked.
-If that also does not exist then the value of the
-.Dv LOCALBASE_PATH
-compile-time variable is used.
-If that is undefined then the system default,
-.Pa _PATH_LOCALBASE
-is used.
+If that returns an empty string or an error occurs, the value of
+.Dv _PATH_LOCALBASE
+is used as a last resort.
 .Pp
-The contents of the string returned by the
-.Fn getlocalbase
-function shall not be modified.
-.Sh IMPLEMENTATION NOTES
-Calls to
-.Fn getlocalbase
-will perform a setugid check on the running binary before checking the
-environment.
+If the value obtained through these means is not a valid absolute path
+shorter than
+.Dv MAXPATHLEN ,
+a constant string which has been deliberately chosen to cause any file
+system operation using it to fail is returned instead.
 .Pp
-The address returned by
-.Fn getlocalbase
-will point into the executing processes environment if it is the result of
-.Fn getenv "LOCALBASE" ,
-to a static buffer if it is the result of
-.Fn sysctl "user.localbase" ,
-and to a constant string if the compiled in default value is returned.
+The contents of the string returned by
+.Nm
+shall not be modified by the caller.
+.Sh IMPLEMENTATION NOTES
+The
+.Ev LOCALBASE
+environment variable will only be used if the process calling
+.Nm
+is not setugid.
 .Pp
-The same value will be returned on successive calls during the run-time
-of the program, ignoring any changes to the environment variable or the
-sysctl value that might have been made.
+Successive calls to
+.Nm
+will return the same value throughout the lifetime of the process,
+regardless of any subsequent changes to the environment or sysctl
+variables.
 .Pp
 The
-.Fn getlocalbase
-function can be compiled with a non-default value of LOCALBASE_CTL_LEN.
-A value of 0 will disable fetching of the sysctl value, a value less than
-MAXPATHLEN will put a limit on the maximum string length supported for
-this sysctl value.
-If built with a non-default value of LOCALBASE_CTL_LEN, a value of the
-user.localbase sysctl variable longer than this value will make
-.Fn getlocalbase
-return a valid string that is not a valid path prefix in any filesystem.
+.Nm
+function is thread-safe if and only if it has been called at least
+once already.
 .Sh RETURN VALUES
 The
 .Fn getlocalbase
-function returns a pointer to a string, whose length may exceed MAXPATHLEN,
-if it has been obtained from the environment.
+function returns a pointer to a null-terminated string.
 .Sh ENVIRONMENT
-The
-.Fn getlocalbase
-library function retrieves the
-.Ev LOCALBASE
-environment variable.
-.Sh ERRORS
-The
-.Fn getlocalbase
-function always succeeds and returns a valid pointer to a string.
+.Bl -tag -width ".Ev LOCALBASE"
+.It Ev LOCALBASE
+Path to the local software base directory
+.El
 .Sh SEE ALSO
-.Xr env 1 ,
-.Xr src.conf 5 ,
+.Xr environ 7 ,
 .Xr sysctl 8
 .Sh HISTORY
 The
 .Nm
-library function first appeared in
+function first appeared in
 .Fx 13.0 .
 .Sh AUTHORS
 .An -nosplit
-This
-manual page was written by
+The
+.Nm
+function was originally written by
+.An Stefan Eßer Aq Mt se@FreeBSD.org
+and was later reimplemented by
+.An Dag-Erling Sm\(/orgrav Aq Mt des@FreeBSD.org .
+This manual page was originally written by
 .An Scott Long Aq Mt scottl@FreeBSD.org
 and
-.An Stefan Eßer Aq Mt se@FreeBSD.org .
+.An Stefan Eßer Aq Mt se@FreeBSD.org
+and was later substantially rewritten by
+.An Dag-Erling Sm\(/orgrav Aq Mt des@FreeBSD.org .
diff --git a/lib/libutil/getlocalbase.c b/lib/libutil/getlocalbase.c
index e8aabbab1e14..d48e0dbb33fd 100644
--- a/lib/libutil/getlocalbase.c
+++ b/lib/libutil/getlocalbase.c
@@ -1,7 +1,7 @@
-/*-
+/*
  * SPDX-License-Identifier: BSD-2-Clause
  *
- * Copyright 2020 Stefan Eßer <se@freebsd.org>
+ * Copyright (c) 2026 Dag-Erling Smørgrav
  *
  * Redistribution and use in source and binary forms, with or without
  * modification, are permitted provided that the following conditions
@@ -27,61 +27,83 @@
 
 #include <sys/param.h>
 #include <sys/sysctl.h>
-#include <sys/limits.h>
+
 #include <errno.h>
-#include <stdlib.h>
-#include <paths.h>
 #include <libutil.h>
+#include <paths.h>
+#include <stdbool.h>
+#include <stdlib.h>
 #include <unistd.h>
 
-#ifndef LOCALBASE_PATH
-#define LOCALBASE_PATH _PATH_LOCALBASE
+#ifndef _PATH_LOCALBASE
+#define _PATH_LOCALBASE "/usr/local"
 #endif
 
-#ifndef LOCALBASE_CTL_LEN
-#define LOCALBASE_CTL_LEN MAXPATHLEN
-#endif
+/*
+ * Used in case of error; guaranteed to not be the start of a valid path
+ * name.  Deliberately includes a trailing path separator so that any
+ * access will fail with ENOTDIR.
+ */
+#define INVALID_PREFIX "/dev/null/"
 
-/* Any prefix guaranteed to not be the start of a valid path name */
-#define ILLEGAL_PREFIX "/dev/null/"
+/*
+ * Copies a path from src to dst, which may point to the same buffer,
+ * while deduplicating path separators.  Returns false if the path is not
+ * absolute or the result (including the terminating NUL) exceeds len
+ * characters.
+ */
+static bool
+normalize(const char *src, char *dst, size_t len)
+{
+	if (*src != '/' || len == 0)
+		return (false);
+	while (*src == '/')
+		src++;
+	do {
+		*dst++ = '/';
+		if (--len == 0)
+			return (false);
+		while (*src != '\0' && *src != '/') {
+			*dst++ = *src++;
+			if (--len == 0)
+				return (false);
+		}
+		while (*src == '/')
+			src++;
+	} while (*src != '\0');
+	*dst = '\0';
+	return (true);
+}
 
 const char *
 getlocalbase(void)
 {
-#if LOCALBASE_CTL_LEN > 0
-	int localbase_oid[2] = {CTL_USER, USER_LOCALBASE};
-	static char localpath[LOCALBASE_CTL_LEN];
-	size_t localpathlen = LOCALBASE_CTL_LEN;
-#endif
-	char *tmppath;
+	static const int oid[2] = { CTL_USER, USER_LOCALBASE };
+	static char buf[MAXPATHLEN];
 	static const char *localbase = NULL;
+	char *path;
+	size_t len = sizeof(buf);
 
 	if (localbase != NULL)
 		return (localbase);
 
-	if (issetugid() == 0) {
-		tmppath = getenv("LOCALBASE");
-		if (tmppath != NULL && tmppath[0] != '\0') {
-			localbase = tmppath;
-			return (localbase);
-		}
+	/* If not privileged, try the environment first */
+	if ((path = secure_getenv("LOCALBASE")) != NULL && *path != '\0') {
+		if (!normalize(path, buf, sizeof(buf)))
+			return (localbase = INVALID_PREFIX);
+		return (localbase = buf);
 	}
 
-#if LOCALBASE_CTL_LEN > 0
-	if (sysctl(localbase_oid, 2, localpath, &localpathlen, NULL, 0) != 0) {
-		if (errno != ENOMEM)
-			localbase = LOCALBASE_PATH;
-		else
-			localbase = ILLEGAL_PREFIX;
-	} else {
-		if (localpath[0] != '\0')
-			localbase = localpath;
-		else
-			localbase = LOCALBASE_PATH;
+	/* Next, try the sysctl variable */
+	if (sysctl(oid, 2, buf, &len, NULL, 0) != 0) {
+		if (errno == ENOMEM)
+			return (localbase = INVALID_PREFIX);
+	} else if (*buf != '\0') {
+		if (!normalize(buf, buf, sizeof(buf)))
+			return (localbase = INVALID_PREFIX);
+		return (localbase = buf);
 	}
-#else
-	localbase = LOCALBASE_PATH;
-#endif
 
-	return (localbase);
+	/* Fall back to the hardcoded default */
+	return (localbase = _PATH_LOCALBASE);
 }
diff --git a/lib/libutil/tests/Makefile b/lib/libutil/tests/Makefile
index cdcf3466aec8..2523c02eb33d 100644
--- a/lib/libutil/tests/Makefile
+++ b/lib/libutil/tests/Makefile
@@ -7,6 +7,7 @@ TAP_TESTS_C+=	trimdomain-nodomain_test
 ATF_TESTS_C+=	cpuset_test
 ATF_TESTS_C+=	expand_number_test
 ATF_TESTS_C+=	forkpty_test
+ATF_TESTS_C+=	getlocalbase_test
 
 WARNS?=		2
 LIBADD+=	util
diff --git a/lib/libutil/tests/getlocalbase_test.c b/lib/libutil/tests/getlocalbase_test.c
new file mode 100644
index 000000000000..4f90ddf26cc5
--- /dev/null
+++ b/lib/libutil/tests/getlocalbase_test.c
@@ -0,0 +1,92 @@
+/*-
+ * Copyright (c) 2026 Dag-Erling Smørgrav
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <sys/param.h>
+#include <sys/wait.h>
+
+#include <libutil.h>
+#include <paths.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <atf-c.h>
+
+#define INVALID_PREFIX "/dev/null/"
+
+#define X31 "/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+#define X32 "/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+#define X64 X32 X32
+#define X128 X64 X64
+#define X256 X128 X128
+#define X512 X256 X256
+#define X1024 X512 X512
+#define X1023 X512 X256 X128 X64 X32 X31
+
+static struct {
+	const char *in, *out;
+} tests[] = {
+	{ "", _PATH_LOCALBASE },
+	{ "/", "/" },
+	{ "//", "/" },
+	{ "///", "/" },
+	{ "foo", INVALID_PREFIX },
+	{ "/foo", "/foo" },
+	{ "//foo", "/foo" },
+	{ "/foo/", "/foo" },
+	{ "/foo//", "/foo" },
+	{ "//foo//", "/foo" },
+	{ "/foo/bar", "/foo/bar" },
+	{ "/foo/bar/", "/foo/bar" },
+	{ "/foo//bar", "/foo/bar" },
+	{ "/foo//bar/", "/foo/bar" },
+	{ "//foo/bar", "/foo/bar" },
+	{ "//foo/bar/", "/foo/bar" },
+	{ "//foo//bar", "/foo/bar" },
+	{ "//foo//bar/", "/foo/bar" },
+	{ _PATH_LOCALBASE, _PATH_LOCALBASE },
+	{ X31, X31 },
+	{ X32, X32 },
+	{ X64, X64 },
+	{ X128, X128 },
+	{ X256, X256 },
+	{ X512, X512 },
+	{ X1023, X1023 },
+	{ X1024, INVALID_PREFIX },
+	{ 0 }
+};
+_Static_assert(sizeof(X1023) == MAXPATHLEN, "Unexpected MAXPATHLEN value");
+
+ATF_TC(environment);
+ATF_TC_HEAD(environment, tc)
+{
+	atf_tc_set_md_var(tc, "descr", "Tests getting the local base from the "
+	    "environment and path normalization");
+}
+
+ATF_TC_BODY(environment, tc)
+{
+	pid_t pid;
+	int i, wstatus;
+
+	for (i = 0; tests[i].in != NULL; i++) {
+		ATF_REQUIRE((pid = fork()) >= 0);
+		if (pid == 0) {
+			ATF_REQUIRE_EQ(setenv("LOCALBASE", tests[i].in, 1), 0);
+			ATF_REQUIRE_STREQ(getlocalbase(), tests[i].out);
+			_exit(0);
+		}
+		ATF_REQUIRE_EQ(waitpid(0, &wstatus, 0), pid);
+		ATF_CHECK(WIFEXITED(wstatus));
+		ATF_CHECK_EQ(WEXITSTATUS(wstatus), 0);
+	}
+}
+
+ATF_TP_ADD_TCS(tp)
+{
+	ATF_TP_ADD_TC(tp, environment);
+	return (atf_no_error());
+}


home | help

Want to link to this message? Use this
URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?6a5f5944.3a8ff.647c3596>