git: c93f3b55e67f - main - libutil: Reimplement getlocalbase()
- Go to: [ bottom of page ] [ top of archives ] [ this month ]
Date: Tue, 21 Jul 2026 11:34:28 UTC
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());
+}