diff --git a/lib/libjail/jail.h b/lib/libjail/jail.h --- 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/lib/libjail/jail.c b/lib/libjail/jail.c --- a/lib/libjail/jail.c +++ b/lib/libjail/jail.c @@ -59,6 +59,7 @@ 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]; @@ -907,22 +908,41 @@ * 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 +963,12 @@ 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 +1145,26 @@ 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/sys/conf/files b/sys/conf/files --- a/sys/conf/files +++ b/sys/conf/files @@ -3805,6 +3805,7 @@ 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 --- a/sys/kern/kern_jail.c +++ b/sys/kern/kern_jail.c @@ -4254,7 +4254,7 @@ /* * 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 --- /dev/null +++ b/sys/kern/kern_jailmeta.c @@ -0,0 +1,706 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2024 SkunkWerks GmbH + * + * This software was developed by Igor Ostapenko + * under sponsorship from SkunkWerks GmbH. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* + * 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; + + /* only reading */ + + 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"); + + +/* Allowed chars */ + +#define NCHARS 256 +BITSET_DEFINE(charbitset, NCHARS); +static struct charbitset allowedchars; + +static int +jm_sysctl_meta_allowedchars(SYSCTL_HANDLER_ARGS) +{ + int error; + unsigned char chars[NCHARS]; + int len = 0; + const bool readonly = req->newptr == NULL; + + readonly ? sx_slock(&allprison_lock) : sx_xlock(&allprison_lock); + + if (!BIT_ISFULLSET(NCHARS, &allowedchars)) + for (size_t i = 1; i < NCHARS; i++) { + if (!BIT_ISSET(NCHARS, i, &allowedchars)) + continue; + chars[len] = i; + len++; + } + chars[len] = 0; + + error = sysctl_handle_string(oidp, chars, arg2, req); + + if (!readonly && error == 0) { + if (chars[0] == 0) { + BIT_FILL(NCHARS, &allowedchars); + } else { + BIT_ZERO(NCHARS, &allowedchars); + for (size_t i = 0; i < NCHARS; i++) { + if (chars[i] == 0) + break; + BIT_SET(NCHARS, chars[i], &allowedchars); + } + } + } + + readonly ? sx_sunlock(&allprison_lock) : sx_xunlock(&allprison_lock); + + return (error); +} +SYSCTL_PROC(_security_jail, OID_AUTO, meta_allowedchars, + CTLTYPE_STRING | CTLFLAG_RW | CTLFLAG_MPSAFE, NULL, NCHARS, + jm_sysctl_meta_allowedchars, "A", + "The single-byte chars allowed to be used for meta and env" + " (empty string means all chars are allowed)"); + + +/* 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"); + + +/* OSD -- generic logic for any metadata buffer */ + +struct meta { + char *name; + u_int osd_slot; + osd_method_t methods[PR_MAXMETHOD]; +}; + +/* + * A chain of hunks depicts the final buffer after all manipulations. + */ +struct hunk { + char *p; + size_t len; /* may include \0 char or not */ + 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; + + if (h == NULL) + return (n); + + 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\n */ + for (end = begin; (end + 1) - h->p < h->len; end++) + if (*end == '\0' || *end == '\n') + break; + + /* Pick up 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 prefix */ + if (strstr(opt->name, meta->name) != opt->name) + continue; + /* Consider only full or .* 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; + } + /* vfsopt is expected to provide NULL-terminated strings */ + if (((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; + } + + /* 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, + /* avoid empty NULL-terminated string */ + (opt->len > 1) ? opt->len : 0); + continue; + } + + /* Add or replace existing 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); + 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 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 */ + /* Check allowed chars */ + for (size_t i = 0; i < osdlen; i++) { + if (osd[i] == 0) + continue; + if (!BIT_ISSET(NCHARS, osd[i], &allowedchars)) { + error = EINVAL; + goto end; + } + } + } + + /* Compare and swap 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 { + /* someone else was quicker - re-apply opts then */ + 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++; + if (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; + + opt->seen = 1; + + 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; + } + continue; + } + + /* Extract a specific key=value\n */ + 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; + } + } + p += keylen; + } + } + + 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 -- meta */ + +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 = "meta", + .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 -- env */ + +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 = "env", + .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) +{ + /* Default set of allowed chars */ + + BIT_ZERO(NCHARS, &allowedchars); + + /* Base64 */ + for (size_t i = 0x41; i <= 0x5A; i++) /* A-Z */ + BIT_SET(NCHARS, i, &allowedchars); + for (size_t i = 0x61; i <= 0x7A; i++) /* a-z */ + BIT_SET(NCHARS, i, &allowedchars); + for (size_t i = 0x30; i <= 0x39; i++) /* 0-9 */ + BIT_SET(NCHARS, i, &allowedchars); + BIT_SET(NCHARS, 0x2B, &allowedchars); /* + */ + BIT_SET(NCHARS, 0x2F, &allowedchars); /* / */ + BIT_SET(NCHARS, 0x3D, &allowedchars); /* = */ + + /* key=value\n format */ + BIT_SET(NCHARS, 0x0A, &allowedchars); /* LF */ + BIT_SET(NCHARS, 0x0D, &allowedchars); /* CR */ + + /* Extra */ + BIT_SET(NCHARS, 0x09, &allowedchars); /* HT */ + BIT_SET(NCHARS, 0x20, &allowedchars); /* SP */ + BIT_SET(NCHARS, 0x2C, &allowedchars); /* , */ + BIT_SET(NCHARS, 0x2D, &allowedchars); /* - */ + BIT_SET(NCHARS, 0x2E, &allowedchars); /* . */ + BIT_SET(NCHARS, 0x3A, &allowedchars); /* : */ + BIT_SET(NCHARS, 0x40, &allowedchars); /* @ */ + BIT_SET(NCHARS, 0x5F, &allowedchars); /* _ */ + + + 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 --- a/sys/sys/jail.h +++ b/sys/sys/jail.h @@ -376,6 +376,7 @@ /* * 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 --- a/tests/sys/kern/Makefile +++ b/tests/sys/kern/Makefile @@ -59,6 +59,8 @@ ATF_TESTS_SH+= coredump_phnum_test ATF_TESTS_SH+= logsigexit_test +ATF_TESTS_SH+= jailmeta +TEST_METADATA.jailmeta+= is_exclusive="true" 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 --- /dev/null +++ b/tests/sys/kern/jailmeta.sh @@ -0,0 +1,631 @@ +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2024 SkunkWerks GmbH +# +# This software was developed by Igor Ostapenko +# 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 + + # Check if chars required for testing are allowed + allowed="$(sysctl -b security.jail.meta_allowedchars | hexdump -e '1/1 "%02x\n"')" + # ABCabctv =0-9\t\n + for b in 41 42 43 61 62 63 74 76 20 30 31 32 33 34 35 36 37 38 39 09 0a + do + if ! echo $allowed | grep -q $b; then + atf_skip "sysctl security.jail.meta_allowedchars is not wide enough for testing" + fi + done +} + +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 + + atf_check -s exit:0 \ + jail -c name=j persist meta="a b c" env="CAB" + + atf_check -s exit:0 -o inline:"a b c\n" \ + jls -jj meta + atf_check -s exit:0 -o inline:"CAB\n" \ + jls -jj env + + atf_check -s exit:0 \ + jail -m name=j meta="t1=A t2=B" env="CAB2" + + atf_check -s exit:0 -o inline:"t1=A t2=B\n" \ + jls -jj meta + atf_check -s exit:0 -o inline:"CAB2\n" \ + jls -jj env +} +jail_modify_cleanup() +{ + jail -r j + return 0 +} + +atf_test_case "jail_add" "cleanup" +jail_add_head() +{ + atf_set descr 'Test that metadata can be added to an existing jail with jail(8)' + atf_set require.user root + atf_set execenv jail +} +jail_add_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 host.hostname=jail1 + + atf_check -s exit:0 -o inline:'""\n' \ + jls -jj meta + atf_check -s exit:0 -o inline:'""\n' \ + jls -jj env + + atf_check -s exit:0 \ + jail -m name=j meta="$(jot 3 1 3)" env="$(jot 2 11 12)" + + atf_check -s exit:0 -o inline:"1\n2\n3\n" \ + jls -jj meta + atf_check -s exit:0 -o inline:"11\n12\n" \ + jls -jj env +} +jail_add_cleanup() +{ + jail -r j + return 0 +} + +atf_test_case "jail_reset" "cleanup" +jail_reset_head() +{ + atf_set descr 'Test that metadata can be reset to an empty string with jail(8)' + atf_set require.user root + atf_set execenv jail +} +jail_reset_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="123" env="456" + + atf_check -s exit:0 -o inline:"123\n" \ + jls -jj meta + atf_check -s exit:0 -o inline:"456\n" \ + jls -jj env + + atf_check -s exit:0 \ + jail -m name=j meta= env= + + atf_check -s exit:0 -o inline:'""\n' \ + jls -jj meta + atf_check -s exit:0 -o inline:'""\n' \ + jls -jj env +} +jail_reset_cleanup() +{ + jail -r j + return 0 +} + +atf_test_case "jls_libxo" "cleanup" +jls_libxo_head() +{ + atf_set descr 'Test that metadata can be read with jls(8) using libxo' + atf_set require.user root + atf_set execenv jail +} +jls_libxo_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="1 2 3" + + atf_check -s exit:0 -o inline:'{"__version": "2", "jail-information": {"jail": [{"name":"j","meta":"a b c"}]}}\n' \ + jls -jj --libxo json name meta + atf_check -s exit:0 -o inline:'{"__version": "2", "jail-information": {"jail": [{"env":"1 2 3"}]}}\n' \ + jls -jj --libxo json env +} +jls_libxo_cleanup() +{ + jail -r j + return 0 +} + +atf_test_case "flua_create" "cleanup" +flua_create_head() +{ + atf_set descr 'Test that metadata can be set upon jail creation with flua' + atf_set require.user root + atf_set execenv jail +} +flua_create_body() +{ + setup + + atf_check -s not-exit:0 -e match:"not found" -o ignore \ + jls -jj + + atf_check -s exit:0 \ + /usr/libexec/flua -ljail -e 'jail.setparams("j", {["meta"]="t1 t2=v2", ["env"]="BAC", ["persist"]="true"}, jail.CREATE)' + + atf_check -s exit:0 -o inline:"t1 t2=v2\n" \ + /usr/libexec/flua -ljail -e 'jid, res = jail.getparams("j", {"meta"}); print(res["meta"])' + atf_check -s exit:0 -o inline:"BAC\n" \ + /usr/libexec/flua -ljail -e 'jid, res = jail.getparams("j", {"env"}); print(res["env"])' +} +flua_create_cleanup() +{ + jail -r j + return 0 +} + +atf_test_case "flua_modify" "cleanup" +flua_modify_head() +{ + atf_set descr 'Test that metadata can be changed with flua after jail creation' + atf_set require.user root + atf_set execenv jail +} +flua_modify_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="ABC" env="123" + + atf_check -s exit:0 -o inline:"ABC\n" \ + jls -jj meta + atf_check -s exit:0 -o inline:"123\n" \ + jls -jj env + + atf_check -s exit:0 \ + /usr/libexec/flua -ljail -e 'jail.setparams("j", {["meta"]="t1 t2=v", ["env"]="4"}, jail.UPDATE)' + + atf_check -s exit:0 -o inline:"t1 t2=v\n" \ + jls -jj meta + atf_check -s exit:0 -o inline:"4\n" \ + jls -jj env +} +flua_modify_cleanup() +{ + jail -r j + return 0 +} + +atf_test_case "env_readable_by_jail" "cleanup" +env_readable_by_jail_head() +{ + atf_set descr 'Test that a jail can read its own env parameter via sysctl(8)' + atf_set require.user root + atf_set execenv jail +} +env_readable_by_jail_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="CBA" + + atf_check -s exit:0 -o inline:"a b c\n" \ + jls -jj meta + atf_check -s exit:0 -o inline:"CBA\n" \ + jls -jj env + + atf_check -s exit:0 -o inline:"CBA\n" \ + jexec j sysctl -n security.jail.env +} +env_readable_by_jail_cleanup() +{ + jail -r j + return 0 +} + +atf_test_case "not_inheritable" "cleanup" +not_inheritable_head() +{ + atf_set descr 'Test that a jail does not inherit metadata from its parent jail' + atf_set require.user root + atf_set execenv jail +} +not_inheritable_body() +{ + setup + + atf_check -s not-exit:0 -e match:"not found" -o ignore \ + jls -j parent + + atf_check -s exit:0 \ + jail -c name=parent children.max=1 persist meta="abc" env="cba" + + jexec parent jail -c name=child persist + + atf_check -s exit:0 -o inline:"abc\n" \ + jls -j parent meta + atf_check -s exit:0 -o inline:'""\n' \ + jls -j parent.child meta + + atf_check -s exit:0 -o inline:"cba\n" \ + jexec parent sysctl -n security.jail.env + atf_check -s exit:0 -o inline:"\n" \ + jexec parent.child sysctl -n security.jail.env +} +not_inheritable_cleanup() +{ + jail -r parent.child + jail -r parent + return 0 +} + +atf_test_case "maxbufsize" "cleanup" +maxbufsize_head() +{ + atf_set descr 'Test that metadata buffer maximum size can be changed' + atf_set require.user root +} +maxbufsize_body() +{ + setup + + jn=jailmeta_maxbufsize + + atf_check -s not-exit:0 -e match:"not found" -o ignore \ + jls -j $jn + + # the size counts string length and the trailing \0 char + origmax=$(sysctl -n security.jail.meta_maxbufsize) + + # must be fine with current max + atf_check -s exit:0 \ + jail -c name=$jn persist meta="$(printf %$((origmax-1))s)" + atf_check -s exit:0 -o inline:"${origmax}\n" \ + jls -j $jn meta | wc -c + # + atf_check -s exit:0 \ + jail -m name=$jn env="$(printf %$((origmax-1))s)" + atf_check -s exit:0 -o inline:"${origmax}\n" \ + jls -j $jn env | wc -c + + # should not allow exceeding current max + atf_check -s not-exit:0 -e match:"too large" \ + jail -m name=$jn meta="$(printf %${origmax}s)" + # + atf_check -s not-exit:0 -e match:"too large" \ + jail -m name=$jn env="$(printf %${origmax}s)" + + # should allow the same size with increased max + newmax=$((origmax + 1)) + sysctl security.jail.meta_maxbufsize=$newmax + atf_check -s exit:0 \ + jail -m name=$jn meta="$(printf %${origmax}s)" + atf_check -s exit:0 -o inline:"${origmax}\n" \ + jls -j $jn meta | wc -c + # + atf_check -s exit:0 \ + jail -m name=$jn env="$(printf %${origmax}s)" + atf_check -s exit:0 -o inline:"${origmax}\n" \ + jls -j $jn env | wc -c + + # decrease back to the original max + sysctl security.jail.meta_maxbufsize=$origmax + atf_check -s not-exit:0 -e match:"too large" \ + jail -m name=$jn meta="$(printf %${origmax}s)" + # + atf_check -s not-exit:0 -e match:"too large" \ + jail -m name=$jn env="$(printf %${origmax}s)" + + # the previously set long meta is still readable as is + # due to the soft limit remains higher than the hard limit + atf_check_equal '${newmax}' '$(sysctl -n security.jail.param.meta)' + atf_check_equal '${newmax}' '$(sysctl -n security.jail.param.env)' + atf_check -s exit:0 -o inline:"${origmax}\n" \ + jls -j $jn meta | wc -c + # + atf_check -s exit:0 -o inline:"${origmax}\n" \ + jls -j $jn env | wc -c +} +maxbufsize_cleanup() +{ + jail -r jailmeta_maxbufsize + return 0 +} + +atf_test_case "allowedchars" "cleanup" +allowedchars_head() +{ + atf_set descr 'Test that the set of allowed chars can be changed' + atf_set require.user root +} +allowedchars_body() +{ + setup + + jn=jailmeta_allowedchars + atf_check -s not-exit:0 -e match:"not found" -o ignore \ + jls -j $jn + atf_check -s exit:0 \ + jail -c name=$jn persist + + # Save the original value + sysctl -b security.jail.meta_allowedchars > meta_allowedchars.bin + + # All chars + sysctl security.jail.meta_allowedchars= + printf $(jot -w '\%o' -s '' -n 127 1 127) > 7bit.bin + atf_check -s exit:0 \ + jail -m name=$jn meta="$(cat 7bit.bin)" env="$(cat 7bit.bin)" + jls -j $jn meta > meta.bin + jls -j $jn env > env.bin + printf '\n' >> 7bit.bin # jls adds a newline + atf_check -s exit:0 diff 7bit.bin meta.bin + atf_check -s exit:0 diff 7bit.bin env.bin + + # Limited set + sysctl security.jail.meta_allowedchars="$(printf 'AB\1\2_\3\11C')" + # should be okay if within the limits + atf_check -s exit:0 \ + jail -m name=$jn meta="$(printf 'C\11A\3')" env="$(printf '\1A\2B\3')" + # should error and not change env + atf_check -s not-exit:0 -o ignore -e ignore \ + jail -m name=$jn meta="$(printf 'XC\11A\3')" env="$(printf '_\1A\2B\3')" + # should error and not change meta + atf_check -s not-exit:0 -o ignore -e ignore \ + jail -m name=$jn meta="$(printf '_C\11A\3')" env="$(printf '\1A\2B\3x')" + # should stay intact after errors + atf_check -s exit:0 -o inline:"43094103" \ + jls -j $jn meta | hexdump -e '1/1 "%02x"' + atf_check -s exit:0 -o inline:"0141024303" \ + jls -j $jn env | hexdump -e '1/1 "%02x"' + +} +allowedchars_cleanup() +{ + # Restore the original value + test -f meta_allowedchars.bin \ + && sysctl security.jail.meta_allowedchars="'$(cat meta_allowedchars.bin)'" + rm *.bin + + jail -r jailmeta_allowedchars + return 0 +} + +atf_test_case "keyvalue" "cleanup" +keyvalue_head() +{ + atf_set descr 'Test that metadata can be handled as a set of key=value\n strings using jail(8) and jls(8)' + atf_set require.user root + atf_set execenv jail +} +keyvalue_generic() +{ + local meta=$1 + + atf_check -sexit:0 -oinline:'""\n' jls -jj $meta + + # Should be able to extract a key added manually + atf_check -sexit:0 jail -m name=j $meta="a=1" + atf_check -sexit:0 -oinline:'a=1\n' jls -jj $meta + atf_check -sexit:0 -oinline:'1\n' jls -jj $meta.a + atf_check -sexit:0 jail -m name=j $meta="$(printf 'a=2\nb=3')" + atf_check -sexit:0 -oinline:'a=2\nb=3\n' jls -jj $meta + atf_check -sexit:0 -oinline:'2\n' jls -jj $meta.a + atf_check -sexit:0 -oinline:'3\n' jls -jj $meta.b + + # Should provide an empty string for a non-found key + atf_check -sexit:0 -oinline:'""\n' jls -jj $meta.c + + # Should be able to lookup multiple keys at once + atf_check -sexit:0 -oinline:'3 2\n' jls -jj $meta.b $meta.a + + # Should be able to lookup keys and the whole buffer at once + atf_check -sexit:0 -oinline:'3 a=2\nb=3 2\n' jls -jj $meta.b $meta $meta.a + + # Should be able to lookup a key with libxo-based output + s='{"__version": "2", "jail-information": {"jail": [{"'$meta'.b":"3","'$meta'.c":""}]}}\n' + atf_check -s exit:0 -o inline:"$s" jls -jj --libxo json $meta.b $meta.c + + # Should be able to lookup a key with flua + atf_check -s exit:0 -o inline:"2\n" \ + /usr/libexec/flua -ljail -e 'jid, res = jail.getparams("j", {"'$meta'.a"}); print(res["'$meta'.a"])' + + # Should be fine if a buffer is empty + atf_check -sexit:0 jail -m name=j $meta= + atf_check -sexit:0 -oinline:'"" "" ""\n' jls -jj $meta.c $meta $meta.a + + # Should allow adding a new key + atf_check -sexit:0 jail -m name=j $meta.a=1 + atf_check -sexit:0 -oinline:'1\n' jls -jj $meta.a + atf_check -sexit:0 -oinline:'a=1\n' jls -jj $meta + + # Should allow adding multiple new keys at once + atf_check -sexit:0 jail -m name=j $meta.c=3 $meta.b=2 + atf_check -sexit:0 -oinline:'3\n' jls -jj $meta.c + atf_check -sexit:0 -oinline:'2\n' jls -jj $meta.b + atf_check -sexit:0 -oinline:'b=2\nc=3\na=1\n' jls -jj $meta + + # Should replace existing keys + atf_check -sexit:0 jail -m name=j $meta.a=A $meta.c=C + atf_check -sexit:0 -oinline:'A\n' jls -jj $meta.a + atf_check -sexit:0 -oinline:'C\n' jls -jj $meta.c + atf_check -sexit:0 -oinline:'c=C\na=A\nb=2\n' jls -jj $meta + + # Should treat empty value correctly + atf_check -sexit:0 jail -m name=j $meta.b $meta.a= + atf_check -sexit:0 -oinline:'""\n' jls -jj $meta.a + atf_check -sexit:0 -oinline:'""\n' jls -jj $meta.b + atf_check -sexit:0 -oinline:'a=\nb=\nc=C\n' jls -jj $meta + + # Should allow changing the whole buffer and per key at once (order matters) + atf_check -sexit:0 jail -m name=j $meta.a=1 $meta=ttt $meta.b=2 + atf_check -sexit:0 -oinline:'""\n' jls -jj $meta.a + atf_check -sexit:0 -oinline:'2\n' jls -jj $meta.b + atf_check -sexit:0 -oinline:'b=2\nttt\n' jls -jj $meta + + # Should treat only the first equal sign as syntax + atf_check -sexit:0 jail -m name=j $meta.b== + atf_check -sexit:0 -oinline:'=\n' jls -jj $meta.b + atf_check -sexit:0 -oinline:'b==\nttt\n' jls -jj $meta + + # Should allow adding or modifying keys with flua + atf_check -s exit:0 \ + /usr/libexec/flua -ljail -e 'jail.setparams("j", {["'$meta.b'"]="ttt", ["'$meta'.c"]="C"}, jail.UPDATE)' + atf_check -sexit:0 -oinline:'ttt\n' jls -jj $meta.b + atf_check -sexit:0 -oinline:'C\n' jls -jj $meta.c +} +keyvalue_body() +{ + setup + + atf_check -s exit:0 \ + jail -c name=j persist meta env + + keyvalue_generic "meta" + keyvalue_generic "env" +} +keyvalue_cleanup() +{ + jail -r j + return 0 +} + +atf_test_case "keyvalue_contention" "cleanup" +keyvalue_contention_head() +{ + atf_set descr 'Try to stress metadata read/write mechanism with some contention' + atf_set require.user root + atf_set execenv jail + atf_set timeout 30 +} +keyvalue_stresser() +{ + local jailname=$1 + local modifier=$2 + + while true + do + jail -m name=$jailname $modifier + done +} +keyvalue_contention_body() +{ + setup + + atf_check -s exit:0 jail -c name=j persist meta env + + keyvalue_stresser "j" "meta.a=1" & + apid=$! + keyvalue_stresser "j" "meta.b=2" & + bpid=$! + keyvalue_stresser "j" "env.c=3" & + cpid=$! + keyvalue_stresser "j" "env.d=4" & + dpid=$! + + for it in $(jot 8) + do + sleep 1 + jail -m name=j meta='meta=META' env='env=ENV' + atf_check -sexit:0 -oinline:'META\n' jls -jj meta.meta + atf_check -sexit:0 -oinline:'ENV\n' jls -jj env.env + atf_check -sexit:0 -oinline:'1\n' jls -jj meta.a + atf_check -sexit:0 -oinline:'2\n' jls -jj meta.b + atf_check -sexit:0 -oinline:'3\n' jls -jj env.c + atf_check -sexit:0 -oinline:'4\n' jls -jj env.d + done + + # TODO: Think of adding a stresser on the kernel side which does + # osd_set() w/o allprison lock. It could test the compare + # and swap mechanism in jm_osd_method_set(). + + kill -9 $apid $bpid $cpid $dpid +} +keyvalue_contention_cleanup() +{ + jail -r j + return 0 +} + +atf_init_test_cases() +{ + atf_add_test_case "jail_create" + atf_add_test_case "jail_modify" + atf_add_test_case "jail_add" + atf_add_test_case "jail_reset" + + atf_add_test_case "jls_libxo" + + atf_add_test_case "flua_create" + atf_add_test_case "flua_modify" + + atf_add_test_case "env_readable_by_jail" + atf_add_test_case "not_inheritable" + + atf_add_test_case "maxbufsize" + atf_add_test_case "allowedchars" + + atf_add_test_case "keyvalue" + atf_add_test_case "keyvalue_contention" +} diff --git a/usr.sbin/jail/jail.8 b/usr.sbin/jail/jail.8 --- a/usr.sbin/jail/jail.8 +++ b/usr.sbin/jail/jail.8 @@ -513,6 +513,36 @@ The number for the jail's .Va kern.osreldate and uname -K. +.It Va meta , Va env +An arbitrary string associated with the jail. +Its maximum buffer size is controlled by the global +.Va security.jail.meta_maxbufsize +sysctl, which can only be adjusted by the non-jailed root user. +While the +.Va meta +is hidden from the jail, the +.Va env +is readable through the +.Va security.jail.env +sysctl. +The set of allowed single-byte characters for both buffers is limited by the +global +.Va security.jail.meta_allowedchars +sysctl, which is also tunable by the non-jailed root user. +All characters are allowed if it is set to an empty string. +.Pp +Each buffer can be treated as a set of key=value\\n strings. +In order to add or replace a specific key the +.Va meta.keyname=value +or +.Va env.keyname=value +parameter notations must be used. +The +.Va meta.keyname +or +.Va env.keyname +notations are used for reading. +Multiple keys can be queried or modified with a single command. .It Va allow.* Some restrictions of the jail environment may be set on a per-jail basis.