The branch main has been updated by igoro:

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

commit 30e6e008bc06385a66756bebb41676f4f9017eca
Author:     Igor Ostapenko <ig...@freebsd.org>
AuthorDate: 2025-03-31 09:08:43 +0000
Commit:     Igor Ostapenko <ig...@freebsd.org>
CommitDate: 2025-03-31 09:17:03 +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
---
 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 8e5b420d677b..30282e67866c 100644
--- a/lib/libjail/jail.c
+++ b/lib/libjail/jail.c
@@ -59,6 +59,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];
 
@@ -521,6 +522,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.
@@ -907,22 +913,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;
@@ -943,6 +968,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;
@@ -1119,3 +1150,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 f364b090b3f9..9632db795775 100644
--- a/libexec/flua/libjail/lua_jail.c
+++ b/libexec/flua/libjail/lua_jail.c
@@ -445,9 +445,16 @@ l_getparams(lua_State *L)
        for (size_t i = 0; i < params_count; ++i) {
                char *value;
 
-               value = jailparam_export(&params[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(&params[i]);
+                       lua_pushstring(L, value);
+                       free(value);
+               }
+
                lua_setfield(L, -2, params[i].jp_name);
        }
 
@@ -535,7 +542,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 9173fc9b81e5..3be4c1d8e3dd 100644
--- a/sys/conf/files
+++ b/sys/conf/files
@@ -3792,6 +3792,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 "${NOSAN_C} ${MSAN_CFLAGS}"
 kern/kern_khelp.c              standard
diff --git a/sys/kern/kern_jail.c b/sys/kern/kern_jail.c
index 6ffeab59112b..37c0bd49490f 100644
--- a/sys/kern/kern_jail.c
+++ b/sys/kern/kern_jail.c
@@ -2552,6 +2552,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);
@@ -4272,7 +4281,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 <ig...@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 = &empty;
+               }
+
+               /* 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 = &empty;
+               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 72799dbf172f..90fcf8cd5a47 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;
 
@@ -376,6 +379,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 be05f5d01faa..900c9a5b3bbe 100644
--- a/tests/sys/kern/Makefile
+++ b/tests/sys/kern/Makefile
@@ -59,6 +59,7 @@ TEST_METADATA.sigsys+=        is_exclusive="true"
 
 ATF_TESTS_SH+= coredump_phnum_test
 ATF_TESTS_SH+= logsigexit_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 <ig...@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
+
+       atf_check -s not-exit:0 -e match:"not found" -o ignore \
+           jls -jj
+
*** 602 LINES SKIPPED ***

Reply via email to