git: 527027da391d - stable/14 - jail: Add meta and env parameters
- Go to: [ bottom of page ] [ top of archives ] [ this month ]
Date: Thu, 11 Sep 2025 18:50:26 UTC
The branch stable/14 has been updated by emaste:
URL: https://cgit.FreeBSD.org/src/commit/?id=527027da391d9ab75530927076f336330834dc11
commit 527027da391d9ab75530927076f336330834dc11
Author: Igor Ostapenko <igoro@FreeBSD.org>
AuthorDate: 2025-03-31 09:08:43 +0000
Commit: Ed Maste <emaste@FreeBSD.org>
CommitDate: 2025-09-11 15:09:59 +0000
jail: Add meta and env parameters
Each one is an arbitrary string associated with a jail. It can be set
upon jail creation or added/modified later:
> jail -cm ... meta="tag1=value1 tag2=value2" env="configuration"
The values are not inherited from the parent jail.
A parent jail can read both metadata parameters, while a child jail can
read only env via security.jail.env sysctl.
The maximum size of meta or env per jail is controlled by the
global security.jail.meta_maxbufsize sysctl. Decreasing it does not
alter the existing meta information.
Each metadata buffer can be handled as a set of key=value\n strings:
> jail -cm ... meta="$(echo k1=v1; echo k2=v2)" env.1=one
> jls meta.k2 env.1 meta.k1
While meta.k1= resets the value to an empty string, the meta.k1 without
the equal sign removes the given key.
Relnotes: yes
Reviewed by: jamie
Tested by: dch
Sponsored by: SkunkWerks GmbH
Differential Revision: https://reviews.freebsd.org/D47668
(cherry picked from commit 30e6e008bc06385a66756bebb41676f4f9017eca)
---
lib/libjail/jail.c | 84 +++++-
lib/libjail/jail.h | 1 +
libexec/flua/libjail/lua_jail.c | 16 +-
sys/conf/files | 1 +
sys/kern/kern_jail.c | 11 +-
sys/kern/kern_jailmeta.c | 621 ++++++++++++++++++++++++++++++++++++++++
sys/sys/jail.h | 4 +
tests/sys/kern/Makefile | 1 +
tests/sys/kern/jailmeta.sh | 588 +++++++++++++++++++++++++++++++++++++
usr.sbin/jail/jail.8 | 36 +++
usr.sbin/jls/jls.c | 8 +
11 files changed, 1351 insertions(+), 20 deletions(-)
diff --git a/lib/libjail/jail.c b/lib/libjail/jail.c
index 2af210ebb198..030f75a3476a 100644
--- a/lib/libjail/jail.c
+++ b/lib/libjail/jail.c
@@ -60,6 +60,7 @@ static int jailparam_type(struct jailparam *jp);
static int kldload_param(const char *name);
static char *noname(const char *name);
static char *nononame(const char *name);
+static char *kvname(const char *name);
char jail_errmsg[JAIL_ERRMSGLEN];
@@ -522,6 +523,11 @@ jailparam_set(struct jailparam *jp, unsigned njp, int flags)
jiov[i - 1].iov_len = strlen(nname) + 1;
}
+ } else if (jp[j].jp_flags & JP_KEYVALUE &&
+ jp[j].jp_value == NULL) {
+ /* No value means key removal. */
+ jiov[i].iov_base = NULL;
+ jiov[i].iov_len = 0;
} else {
/*
* Try to fill in missing values with an empty string.
@@ -908,22 +914,41 @@ jailparam_type(struct jailparam *jp)
* the "no" counterpart to a boolean.
*/
nname = nononame(name);
- if (nname == NULL) {
- unknown_parameter:
- snprintf(jail_errmsg, JAIL_ERRMSGLEN,
- "unknown parameter: %s", jp->jp_name);
- errno = ENOENT;
- return (-1);
+ if (nname != NULL) {
+ snprintf(desc.s, sizeof(desc.s), SJPARAM ".%s", nname);
+ miblen = sizeof(mib) - 2 * sizeof(int);
+ if (sysctl(mib, 2, mib + 2, &miblen, desc.s,
+ strlen(desc.s)) >= 0) {
+ name = alloca(strlen(nname) + 1);
+ strcpy(name, nname);
+ free(nname);
+ jp->jp_flags |= JP_NOBOOL;
+ goto mib_desc;
+ }
+ free(nname);
}
- name = alloca(strlen(nname) + 1);
- strcpy(name, nname);
- free(nname);
- snprintf(desc.s, sizeof(desc.s), SJPARAM ".%s", name);
- miblen = sizeof(mib) - 2 * sizeof(int);
- if (sysctl(mib, 2, mib + 2, &miblen, desc.s,
- strlen(desc.s)) < 0)
- goto unknown_parameter;
- jp->jp_flags |= JP_NOBOOL;
+ /*
+ * It might be an assumed sub-node of a fmt='A,keyvalue' sysctl.
+ */
+ nname = kvname(name);
+ if (nname != NULL) {
+ snprintf(desc.s, sizeof(desc.s), SJPARAM ".%s", nname);
+ miblen = sizeof(mib) - 2 * sizeof(int);
+ if (sysctl(mib, 2, mib + 2, &miblen, desc.s,
+ strlen(desc.s)) >= 0) {
+ name = alloca(strlen(nname) + 1);
+ strcpy(name, nname);
+ free(nname);
+ jp->jp_flags |= JP_KEYVALUE;
+ goto mib_desc;
+ }
+ free(nname);
+ }
+unknown_parameter:
+ snprintf(jail_errmsg, JAIL_ERRMSGLEN,
+ "unknown parameter: %s", jp->jp_name);
+ errno = ENOENT;
+ return (-1);
}
mib_desc:
mib[1] = 4;
@@ -944,6 +969,12 @@ jailparam_type(struct jailparam *jp)
else if ((desc.i & CTLTYPE) != CTLTYPE_NODE)
goto unknown_parameter;
}
+ /* Make sure it is a valid keyvalue param. */
+ if (jp->jp_flags & JP_KEYVALUE) {
+ if ((desc.i & CTLTYPE) != CTLTYPE_STRING ||
+ strcmp(desc.s, "A,keyvalue") != 0)
+ goto unknown_parameter;
+ }
/* See if this is an array type. */
p = strchr(desc.s, '\0');
isarray = 0;
@@ -1120,3 +1151,26 @@ nononame(const char *name)
strcpy(nname, name + 2);
return (nname);
}
+
+static char *
+kvname(const char *name)
+{
+ const char *p;
+ char *kvname;
+ size_t len;
+
+ p = strchr(name, '.');
+ if (p == NULL)
+ return (NULL);
+
+ len = p - name;
+ kvname = malloc(len + 1);
+ if (kvname == NULL) {
+ strerror_r(errno, jail_errmsg, JAIL_ERRMSGLEN);
+ return (NULL);
+ }
+ strncpy(kvname, name, len);
+ kvname[len] = '\0';
+
+ return (kvname);
+}
diff --git a/lib/libjail/jail.h b/lib/libjail/jail.h
index 27f07cd98802..6ce79b1b0528 100644
--- a/lib/libjail/jail.h
+++ b/lib/libjail/jail.h
@@ -33,6 +33,7 @@
#define JP_BOOL 0x02
#define JP_NOBOOL 0x04
#define JP_JAILSYS 0x08
+#define JP_KEYVALUE 0x10
#define JAIL_ERRMSGLEN 1024
diff --git a/libexec/flua/libjail/lua_jail.c b/libexec/flua/libjail/lua_jail.c
index b463f5c894ee..7345034aa5f3 100644
--- a/libexec/flua/libjail/lua_jail.c
+++ b/libexec/flua/libjail/lua_jail.c
@@ -446,9 +446,16 @@ l_getparams(lua_State *L)
for (size_t i = 0; i < params_count; ++i) {
char *value;
- value = jailparam_export(¶ms[i]);
- lua_pushstring(L, value);
- free(value);
+ if (params[i].jp_flags & JP_KEYVALUE &&
+ params[i].jp_valuelen == 0) {
+ /* Communicate back a missing key. */
+ lua_pushnil(L);
+ } else {
+ value = jailparam_export(¶ms[i]);
+ lua_pushstring(L, value);
+ free(value);
+ }
+
lua_setfield(L, -2, params[i].jp_name);
}
@@ -536,7 +543,8 @@ l_setparams(lua_State *L)
}
value = lua_tostring(L, -1);
- if (value == NULL) {
+ /* Allow passing NULL for key removal. */
+ if (value == NULL && !(params[i].jp_flags & JP_KEYVALUE)) {
jailparam_free(params, i + 1);
free(params);
return (luaL_argerror(L, 2,
diff --git a/sys/conf/files b/sys/conf/files
index 30559540a5c0..66d2916ed174 100644
--- a/sys/conf/files
+++ b/sys/conf/files
@@ -3772,6 +3772,7 @@ kern/kern_hhook.c standard
kern/kern_idle.c standard
kern/kern_intr.c standard
kern/kern_jail.c standard
+kern/kern_jailmeta.c standard
kern/kern_kcov.c optional kcov \
compile-with "${NORMAL_C:N-fsanitize*} ${NORMAL_C:M-fsanitize=kernel-memory}"
kern/kern_khelp.c standard
diff --git a/sys/kern/kern_jail.c b/sys/kern/kern_jail.c
index 0af1bcd775d0..c449aba9b56a 100644
--- a/sys/kern/kern_jail.c
+++ b/sys/kern/kern_jail.c
@@ -2519,6 +2519,15 @@ kern_jail_get(struct thread *td, struct uio *optuio, int flags)
/* By now, all parameters should have been noted. */
TAILQ_FOREACH(opt, opts, link) {
+ if (!opt->seen &&
+ (strstr(opt->name, JAIL_META_PRIVATE ".") == opt->name ||
+ strstr(opt->name, JAIL_META_SHARED ".") == opt->name)) {
+ /* Communicate back a missing key. */
+ free(opt->value, M_MOUNT);
+ opt->value = NULL;
+ opt->len = 0;
+ continue;
+ }
if (!opt->seen && strcmp(opt->name, "errmsg")) {
error = EINVAL;
vfs_opterror(opts, "unknown parameter: %s", opt->name);
@@ -4213,7 +4222,7 @@ prison_path(struct prison *pr1, struct prison *pr2)
/*
* Jail-related sysctls.
*/
-static SYSCTL_NODE(_security, OID_AUTO, jail, CTLFLAG_RW | CTLFLAG_MPSAFE, 0,
+SYSCTL_NODE(_security, OID_AUTO, jail, CTLFLAG_RW | CTLFLAG_MPSAFE, 0,
"Jails");
#if defined(INET) || defined(INET6)
diff --git a/sys/kern/kern_jailmeta.c b/sys/kern/kern_jailmeta.c
new file mode 100644
index 000000000000..4e37eccad03a
--- /dev/null
+++ b/sys/kern/kern_jailmeta.c
@@ -0,0 +1,621 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2024 SkunkWerks GmbH
+ *
+ * This software was developed by Igor Ostapenko <igoro@FreeBSD.org>
+ * under sponsorship from SkunkWerks GmbH.
+ */
+
+#include <sys/param.h>
+#include <sys/_bitset.h>
+#include <sys/bitset.h>
+#include <sys/lock.h>
+#include <sys/sx.h>
+#include <sys/kernel.h>
+#include <sys/mount.h>
+#include <sys/malloc.h>
+#include <sys/jail.h>
+#include <sys/osd.h>
+#include <sys/proc.h>
+
+/*
+ * Buffer limit.
+ *
+ * The hard limit is the actual value used during setting or modification. The
+ * soft limit is used solely by the security.jail.param.meta and .env sysctl. If
+ * the hard limit is decreased, the soft limit may remain higher to ensure that
+ * previously set meta strings can still be correctly interpreted by end-user
+ * interfaces, such as jls(8).
+ */
+
+static uint32_t jm_maxbufsize_hard = 4096;
+static uint32_t jm_maxbufsize_soft = 4096;
+
+static int
+jm_sysctl_meta_maxbufsize(SYSCTL_HANDLER_ARGS)
+{
+ int error;
+ uint32_t newmax = 0;
+
+ /* Reading only. */
+
+ if (req->newptr == NULL) {
+ sx_slock(&allprison_lock);
+ error = SYSCTL_OUT(req, &jm_maxbufsize_hard,
+ sizeof(jm_maxbufsize_hard));
+ sx_sunlock(&allprison_lock);
+
+ return (error);
+ }
+
+ /* Reading and writing. */
+
+ sx_xlock(&allprison_lock);
+
+ error = SYSCTL_OUT(req, &jm_maxbufsize_hard,
+ sizeof(jm_maxbufsize_hard));
+ if (error != 0)
+ goto end;
+
+ error = SYSCTL_IN(req, &newmax, sizeof(newmax));
+ if (error != 0)
+ goto end;
+
+ jm_maxbufsize_hard = newmax;
+ if (jm_maxbufsize_hard >= jm_maxbufsize_soft) {
+ jm_maxbufsize_soft = jm_maxbufsize_hard;
+ } else if (TAILQ_EMPTY(&allprison)) {
+ /*
+ * For now, this is the simplest way to
+ * avoid O(n) iteration over all prisons in
+ * case of a large n.
+ */
+ jm_maxbufsize_soft = jm_maxbufsize_hard;
+ }
+
+end:
+ sx_xunlock(&allprison_lock);
+ return (error);
+}
+SYSCTL_PROC(_security_jail, OID_AUTO, meta_maxbufsize,
+ CTLTYPE_U32 | CTLFLAG_RW | CTLFLAG_MPSAFE, NULL, 0,
+ jm_sysctl_meta_maxbufsize, "IU",
+ "Maximum buffer size of each meta and env");
+
+
+/* Jail parameter announcement. */
+
+static int
+jm_sysctl_param_meta(SYSCTL_HANDLER_ARGS)
+{
+ uint32_t soft;
+
+ sx_slock(&allprison_lock);
+ soft = jm_maxbufsize_soft;
+ sx_sunlock(&allprison_lock);
+
+ return (sysctl_jail_param(oidp, arg1, soft, req));
+}
+SYSCTL_PROC(_security_jail_param, OID_AUTO, meta,
+ CTLTYPE_STRING | CTLFLAG_RW | CTLFLAG_MPSAFE, NULL, 0,
+ jm_sysctl_param_meta, "A,keyvalue",
+ "Jail meta information hidden from the jail");
+SYSCTL_PROC(_security_jail_param, OID_AUTO, env,
+ CTLTYPE_STRING | CTLFLAG_RW | CTLFLAG_MPSAFE, NULL, 0,
+ jm_sysctl_param_meta, "A,keyvalue",
+ "Jail meta information readable by the jail");
+
+
+/* Generic OSD-based logic for any metadata buffer. */
+
+struct meta {
+ char *name;
+ u_int osd_slot;
+ osd_method_t methods[PR_MAXMETHOD];
+};
+
+/* A chain of hunks representing the final buffer after all manipulations. */
+struct hunk {
+ char *p; /* a buf reference */
+ size_t len; /* number of bytes referred */
+ char *owned; /* must be freed */
+ struct hunk *next;
+};
+
+static inline struct hunk *
+jm_h_alloc(void)
+{
+ /* All fields are zeroed. */
+ return (malloc(sizeof(struct hunk), M_PRISON, M_WAITOK | M_ZERO));
+}
+
+static inline struct hunk *
+jm_h_prepend(struct hunk *h, char *p, size_t len)
+{
+ struct hunk *n;
+
+ n = jm_h_alloc();
+ n->p = p;
+ n->len = len;
+ n->next = h;
+ return (n);
+}
+
+static inline void
+jm_h_cut_line(struct hunk *h, char *begin)
+{
+ struct hunk *rem;
+ char *end;
+
+ /* Find the end of key=value. */
+ for (end = begin; (end + 1) < (h->p + h->len); end++)
+ if (*end == '\0' || *end == '\n')
+ break;
+
+ /* Pick up a non-empty remainder. */
+ if ((end + 1) < (h->p + h->len) && *(end + 1) != '\0') {
+ rem = jm_h_alloc();
+ rem->p = end + 1;
+ rem->len = h->p + h->len - rem->p;
+
+ /* insert */
+ rem->next = h->next;
+ h->next = rem;
+ }
+
+ /* Shorten this hunk. */
+ h->len = begin - h->p;
+}
+
+static inline void
+jm_h_cut_occurrences(struct hunk *h, const char *key, size_t keylen)
+{
+ char *p = h->p;
+
+#define nexthunk() \
+ do { \
+ h = h->next; \
+ p = (h == NULL) ? NULL : h->p; \
+ } while (0)
+
+ while (p != NULL) {
+ p = strnstr(p, key, h->len - (p - h->p));
+ if (p == NULL) {
+ nexthunk();
+ continue;
+ }
+ if ((p == h->p || *(p - 1) == '\n') && p[keylen] == '=') {
+ jm_h_cut_line(h, p);
+ nexthunk();
+ continue;
+ }
+ /* Continue with this hunk. */
+ p += keylen;
+ /* Empty? The next hunk then. */
+ if ((p - h->p) >= h->len)
+ nexthunk();
+ }
+}
+
+static inline size_t
+jm_h_len(struct hunk *h)
+{
+ size_t len = 0;
+ while (h != NULL) {
+ len += h->len;
+ h = h->next;
+ }
+ return (len);
+}
+
+static inline void
+jm_h_assemble(char *dst, struct hunk *h)
+{
+ while (h != NULL) {
+ if (h->len > 0) {
+ memcpy(dst, h->p, h->len);
+ dst += h->len;
+ /* If not the last hunk then concatenate with \n. */
+ if (h->next != NULL && *(dst - 1) == '\0')
+ *(dst - 1) = '\n';
+ }
+ h = h->next;
+ }
+}
+
+static inline struct hunk *
+jm_h_freechain(struct hunk *h)
+{
+ struct hunk *n = h;
+ while (n != NULL) {
+ h = n;
+ n = h->next;
+ free(h->owned, M_PRISON);
+ free(h, M_PRISON);
+ }
+
+ return (NULL);
+}
+
+static int
+jm_osd_method_set(void *obj, void *data, const struct meta *meta)
+{
+ struct prison *pr = obj;
+ struct vfsoptlist *opts = data;
+ struct vfsopt *opt;
+
+ char *origosd;
+ char *origosd_copy;
+ char *oldosd;
+ char *osd;
+ size_t osdlen;
+ struct hunk *h;
+ char *key;
+ size_t keylen;
+ int error;
+ int repeats = 0;
+ bool repeat;
+
+ sx_assert(&allprison_lock, SA_XLOCKED);
+
+again:
+ origosd = NULL;
+ origosd_copy = NULL;
+ osd = NULL;
+ h = NULL;
+ error = 0;
+ repeat = false;
+ TAILQ_FOREACH(opt, opts, link) {
+ /* Look for options with <metaname> prefix. */
+ if (strstr(opt->name, meta->name) != opt->name)
+ continue;
+ /* Consider only full <metaname> or <metaname>.* ones. */
+ if (opt->name[strlen(meta->name)] != '.' &&
+ opt->name[strlen(meta->name)] != '\0')
+ continue;
+ opt->seen = 1;
+
+ /* The very first preconditions. */
+ if (opt->len < 0)
+ continue;
+ if (opt->len > jm_maxbufsize_hard) {
+ error = EFBIG;
+ break;
+ }
+ /* NULL-terminated strings are expected from vfsopt. */
+ if (opt->value != NULL &&
+ ((char *)opt->value)[opt->len - 1] != '\0') {
+ error = EINVAL;
+ break;
+ }
+
+ /* Work with our own copy of existing metadata. */
+ if (h == NULL) {
+ h = jm_h_alloc(); /* zeroed */
+ mtx_lock(&pr->pr_mtx);
+ origosd = osd_jail_get(pr, meta->osd_slot);
+ if (origosd != NULL) {
+ origosd_copy = malloc(strlen(origosd) + 1,
+ M_PRISON, M_NOWAIT);
+ if (origosd_copy == NULL)
+ error = ENOMEM;
+ else {
+ h->p = origosd_copy;
+ h->len = strlen(origosd) + 1;
+ memcpy(h->p, origosd, h->len);
+ }
+ }
+ mtx_unlock(&pr->pr_mtx);
+ if (error != 0)
+ break;
+ }
+
+ /* 1) Change the whole metadata. */
+ if (strcmp(opt->name, meta->name) == 0) {
+ if (opt->len > jm_maxbufsize_hard) {
+ error = EFBIG;
+ break;
+ }
+ h = jm_h_freechain(h);
+ h = jm_h_prepend(h,
+ (opt->value != NULL) ? opt->value : "",
+ /* avoid empty NULL-terminated string */
+ (opt->len > 1) ? opt->len : 0);
+ continue;
+ }
+
+ /* 2) Or add/replace/remove a specific key=value. */
+ key = opt->name + strlen(meta->name) + 1;
+ keylen = strlen(key);
+ if (keylen < 1) {
+ error = EINVAL;
+ break;
+ }
+ jm_h_cut_occurrences(h, key, keylen);
+ if (opt->value == NULL)
+ continue; /* key removal */
+ h = jm_h_prepend(h, NULL, 0);
+ h->len = keylen + 1 + opt->len; /* key=value\0 */
+ h->owned = malloc(h->len, M_PRISON, M_WAITOK | M_ZERO);
+ h->p = h->owned;
+ memcpy(h->p, key, keylen);
+ h->p[keylen] = '=';
+ memcpy(h->p + keylen + 1, opt->value, opt->len);
+ }
+
+ if (h == NULL || error != 0)
+ goto end;
+
+ /* Assemble the final contiguous buffer. */
+ osdlen = jm_h_len(h);
+ if (osdlen > jm_maxbufsize_hard) {
+ error = EFBIG;
+ goto end;
+ }
+ if (osdlen > 1) {
+ osd = malloc(osdlen, M_PRISON, M_WAITOK);
+ jm_h_assemble(osd, h);
+ osd[osdlen - 1] = '\0'; /* sealed */
+ }
+
+ /* Compare and swap the buffers. */
+ mtx_lock(&pr->pr_mtx);
+ oldosd = osd_jail_get(pr, meta->osd_slot);
+ if (oldosd == origosd) {
+ error = osd_jail_set(pr, meta->osd_slot, osd);
+ } else {
+ /*
+ * The osd(9) framework requires protection only for pr_osd,
+ * which is covered by pr_mtx. Therefore, other code might
+ * legally alter jail metadata without allprison_lock. It
+ * means that here we could override data just added by other
+ * thread. This extra caution with retry mechanism aims to
+ * prevent user data loss in such potential cases.
+ */
+ error = EAGAIN;
+ repeat = true;
+ }
+ mtx_unlock(&pr->pr_mtx);
+ if (error == 0)
+ osd = oldosd;
+
+end:
+ jm_h_freechain(h);
+ free(osd, M_PRISON);
+ free(origosd_copy, M_PRISON);
+
+ if (repeat && ++repeats < 3)
+ goto again;
+
+ return (error);
+}
+
+static int
+jm_osd_method_get(void *obj, void *data, const struct meta *meta)
+{
+ struct prison *pr = obj;
+ struct vfsoptlist *opts = data;
+ struct vfsopt *opt;
+ char *osd = NULL;
+ char empty = '\0';
+ int error = 0;
+ bool locked = false;
+ const char *key;
+ size_t keylen;
+ const char *p;
+
+ sx_assert(&allprison_lock, SA_SLOCKED);
+
+ TAILQ_FOREACH(opt, opts, link) {
+ if (strstr(opt->name, meta->name) != opt->name)
+ continue;
+ if (opt->name[strlen(meta->name)] != '.' &&
+ opt->name[strlen(meta->name)] != '\0')
+ continue;
+
+ if (!locked) {
+ mtx_lock(&pr->pr_mtx);
+ locked = true;
+ osd = osd_jail_get(pr, meta->osd_slot);
+ if (osd == NULL)
+ osd = ∅
+ }
+
+ /* Provide full metadata. */
+ if (strcmp(opt->name, meta->name) == 0) {
+ if (strlcpy(opt->value, osd, opt->len) >= opt->len) {
+ error = EINVAL;
+ break;
+ }
+ opt->seen = 1;
+ continue;
+ }
+
+ /* Extract a specific key=value. */
+ p = osd;
+ key = opt->name + strlen(meta->name) + 1;
+ keylen = strlen(key);
+ while ((p = strstr(p, key)) != NULL) {
+ if ((p == osd || *(p - 1) == '\n')
+ && p[keylen] == '=') {
+ if (strlcpy(opt->value, p + keylen + 1,
+ MIN(opt->len, strchr(p + keylen + 1, '\n') -
+ (p + keylen + 1) + 1)) >= opt->len) {
+ error = EINVAL;
+ break;
+ }
+ opt->seen = 1;
+ }
+ p += keylen;
+ }
+ if (error != 0)
+ break;
+ }
+
+ if (locked)
+ mtx_unlock(&pr->pr_mtx);
+
+ return (error);
+}
+
+static int
+jm_osd_method_check(void *obj __unused, void *data, const struct meta *meta)
+{
+ struct vfsoptlist *opts = data;
+ struct vfsopt *opt;
+
+ TAILQ_FOREACH(opt, opts, link) {
+ if (strstr(opt->name, meta->name) != opt->name)
+ continue;
+ if (opt->name[strlen(meta->name)] != '.' &&
+ opt->name[strlen(meta->name)] != '\0')
+ continue;
+ opt->seen = 1;
+ }
+
+ return (0);
+}
+
+static void
+jm_osd_destructor(void *osd)
+{
+ free(osd, M_PRISON);
+}
+
+
+/* OSD for "meta" param */
+
+static struct meta meta;
+
+static inline int
+jm_osd_method_set_meta(void *obj, void *data)
+{
+ return (jm_osd_method_set(obj, data, &meta));
+}
+
+static inline int
+jm_osd_method_get_meta(void *obj, void *data)
+{
+ return (jm_osd_method_get(obj, data, &meta));
+}
+
+static inline int
+jm_osd_method_check_meta(void *obj, void *data)
+{
+ return (jm_osd_method_check(obj, data, &meta));
+}
+
+static struct meta meta = {
+ .name = JAIL_META_PRIVATE,
+ .osd_slot = 0,
+ .methods = {
+ [PR_METHOD_SET] = jm_osd_method_set_meta,
+ [PR_METHOD_GET] = jm_osd_method_get_meta,
+ [PR_METHOD_CHECK] = jm_osd_method_check_meta,
+ }
+};
+
+
+/* OSD for "env" param */
+
+static struct meta env;
+
+static inline int
+jm_osd_method_set_env(void *obj, void *data)
+{
+ return (jm_osd_method_set(obj, data, &env));
+}
+
+static inline int
+jm_osd_method_get_env(void *obj, void *data)
+{
+ return (jm_osd_method_get(obj, data, &env));
+}
+
+static inline int
+jm_osd_method_check_env(void *obj, void *data)
+{
+ return (jm_osd_method_check(obj, data, &env));
+}
+
+static struct meta env = {
+ .name = JAIL_META_SHARED,
+ .osd_slot = 0,
+ .methods = {
+ [PR_METHOD_SET] = jm_osd_method_set_env,
+ [PR_METHOD_GET] = jm_osd_method_get_env,
+ [PR_METHOD_CHECK] = jm_osd_method_check_env,
+ }
+};
+
+
+/* A jail can read its "env". */
+
+static int
+jm_sysctl_env(SYSCTL_HANDLER_ARGS)
+{
+ struct prison *pr;
+ char empty = '\0';
+ char *tmpbuf;
+ size_t outlen;
+ int error = 0;
+
+ pr = req->td->td_ucred->cr_prison;
+
+ mtx_lock(&pr->pr_mtx);
+ arg1 = osd_jail_get(pr, env.osd_slot);
+ if (arg1 == NULL) {
+ tmpbuf = ∅
+ outlen = 1;
+ } else {
+ outlen = strlen(arg1) + 1;
+ if (req->oldptr != NULL) {
+ tmpbuf = malloc(outlen, M_PRISON, M_NOWAIT);
+ error = (tmpbuf == NULL) ? ENOMEM : 0;
+ if (error == 0)
+ memcpy(tmpbuf, arg1, outlen);
+ }
+ }
+ mtx_unlock(&pr->pr_mtx);
+
+ if (error != 0)
+ return (error);
+
+ if (req->oldptr == NULL)
+ SYSCTL_OUT(req, NULL, outlen);
+ else {
+ SYSCTL_OUT(req, tmpbuf, outlen);
+ if (tmpbuf != &empty)
+ free(tmpbuf, M_PRISON);
+ }
+
+ return (error);
+}
+SYSCTL_PROC(_security_jail, OID_AUTO, env,
+ CTLTYPE_STRING | CTLFLAG_RD | CTLFLAG_MPSAFE,
+ 0, 0, jm_sysctl_env, "A", "Meta information provided by parent jail");
+
+
+/* Setup and tear down. */
+
+static int
+jm_sysinit(void *arg __unused)
+{
+ meta.osd_slot = osd_jail_register(jm_osd_destructor, meta.methods);
+ env.osd_slot = osd_jail_register(jm_osd_destructor, env.methods);
+
+ return (0);
+}
+
+static int
+jm_sysuninit(void *arg __unused)
+{
+ osd_jail_deregister(meta.osd_slot);
+ osd_jail_deregister(env.osd_slot);
+
+ return (0);
+}
+
+SYSINIT(jailmeta, SI_SUB_DRIVERS, SI_ORDER_ANY, jm_sysinit, NULL);
+SYSUNINIT(jailmeta, SI_SUB_DRIVERS, SI_ORDER_ANY, jm_sysuninit, NULL);
diff --git a/sys/sys/jail.h b/sys/sys/jail.h
index 6701587f82cb..0291d99ec5af 100644
--- a/sys/sys/jail.h
+++ b/sys/sys/jail.h
@@ -141,6 +141,9 @@ MALLOC_DECLARE(M_PRISON);
#define DEFAULT_HOSTUUID "00000000-0000-0000-0000-000000000000"
#define OSRELEASELEN 32
+#define JAIL_META_PRIVATE "meta"
+#define JAIL_META_SHARED "env"
+
struct racct;
struct prison_racct;
@@ -374,6 +377,7 @@ extern struct sx allprison_lock;
/*
* Sysctls to describe jail parameters.
*/
+SYSCTL_DECL(_security_jail);
SYSCTL_DECL(_security_jail_param);
#define SYSCTL_JAIL_PARAM_DECL(name) \
diff --git a/tests/sys/kern/Makefile b/tests/sys/kern/Makefile
index ba53e8ffe40d..1e33430e84fd 100644
--- a/tests/sys/kern/Makefile
+++ b/tests/sys/kern/Makefile
@@ -58,6 +58,7 @@ ATF_TESTS_C+= sigsys
TEST_METADATA.sigsys+= is_exclusive="true"
ATF_TESTS_SH+= coredump_phnum_test
+ATF_TESTS_SH+= jailmeta
ATF_TESTS_SH+= sonewconn_overflow
TEST_METADATA.sonewconn_overflow+= required_programs="python"
TEST_METADATA.sonewconn_overflow+= required_user="root"
diff --git a/tests/sys/kern/jailmeta.sh b/tests/sys/kern/jailmeta.sh
new file mode 100644
index 000000000000..9a63f958231f
--- /dev/null
+++ b/tests/sys/kern/jailmeta.sh
@@ -0,0 +1,588 @@
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2024 SkunkWerks GmbH
+#
+# This software was developed by Igor Ostapenko <igoro@FreeBSD.org>
+# under sponsorship from SkunkWerks GmbH.
+#
+
+setup()
+{
+ # Check if we have enough buffer space for testing
+ if [ $(sysctl -n security.jail.meta_maxbufsize) -lt 128 ]; then
+ atf_skip "sysctl security.jail.meta_maxbufsize must be 128+ for testing."
+ fi
+}
+
+atf_test_case "jail_create" "cleanup"
+jail_create_head()
+{
+ atf_set descr 'Test that metadata can be set upon jail creation with jail(8)'
+ atf_set require.user root
+ atf_set execenv jail
+}
+jail_create_body()
+{
+ setup
+
+ atf_check -s not-exit:0 -e match:"not found" -o ignore \
+ jls -jj
+
+ atf_check -s exit:0 \
+ jail -c name=j persist meta="a b c" env="C B A"
+
+ atf_check -s exit:0 -o inline:"a b c\n" \
+ jls -jj meta
+ atf_check -s exit:0 -o inline:"C B A\n" \
+ jls -jj env
+}
+jail_create_cleanup()
+{
+ jail -r j
+ return 0
+}
+
+atf_test_case "jail_modify" "cleanup"
+jail_modify_head()
+{
+ atf_set descr 'Test that metadata can be modified after jail creation with jail(8)'
+ atf_set require.user root
+ atf_set execenv jail
+}
+jail_modify_body()
+{
+ setup
+
*** 605 LINES SKIPPED ***