diff --git a/sys/conf/files b/sys/conf/files --- a/sys/conf/files +++ b/sys/conf/files @@ -3818,6 +3818,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,341 @@ +/*- + * 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 + +/* + * 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.metaext & .metaint + * sysctlS. The soft limit may remain higher than the hard limit to ensure that + * previously set long meta strings can still be correctly interpreted by + * end-user interfaces like 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; + + if (req->newptr == NULL) { + /* read-only */ + sx_slock(&allprison_lock); + error = SYSCTL_OUT(req, &jm_maxbufsize_hard, + sizeof(jm_maxbufsize_hard)); + sx_sunlock(&allprison_lock); + } else { + /* read and write */ + sx_xlock(&allprison_lock); + error = SYSCTL_IN(req, &newmax, sizeof(newmax)); + if (error == 0 && newmax < 1) + error = EINVAL; + if (error == 0) { + jm_maxbufsize_hard = newmax; + if (jm_maxbufsize_hard >= jm_maxbufsize_soft) + jm_maxbufsize_soft = jm_maxbufsize_hard; + else if (TAILQ_EMPTY(&allprison)) + /* + * TODO: For now, this is the simplest way to + * avoid O(n) iteration over all prisons in + * cases of a large n. + */ + jm_maxbufsize_soft = jm_maxbufsize_hard; + } + 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 meta buffer size."); + + +/* 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, metaext, + CTLTYPE_STRING | CTLFLAG_RW | CTLFLAG_MPSAFE, NULL, 0, + jm_sysctl_param_meta, "A", "Jail external meta information"); +SYSCTL_PROC(_security_jail_param, OID_AUTO, metaint, + CTLTYPE_STRING | CTLFLAG_RW | CTLFLAG_MPSAFE, NULL, 0, + jm_sysctl_param_meta, "A", "Jail internal meta information"); + + +/* OSD -- general */ + +struct meta { + char *name; + u_int osd_slot; + osd_method_t methods[PR_MAXMETHOD]; +}; + +static int +jm_osd_method_set(void *obj, void *data, struct meta *meta) +{ + struct prison *pr = obj; + struct vfsoptlist *opts = data; + int len = 0; + char *osd_addr; + char *osd_addr_old; + int error; + + /* Check the option presence and its len before buf allocation */ + error = vfs_getopt(opts, meta->name, NULL, &len); + if (error == ENOENT) + return (0); + if (error != 0) + return (error); + if (len < 1) + return (EINVAL); + + sx_assert(&allprison_lock, SA_LOCKED); + if (len > jm_maxbufsize_hard) /* len includes '\0' char */ + return (EFBIG); + + /* Prepare a new buf */ + osd_addr = NULL; + if (len > 1) { + osd_addr = malloc(len, M_PRISON, M_WAITOK); + error = vfs_copyopt(opts, meta->name, osd_addr, len); + if (error != 0) { + free(osd_addr, M_PRISON); + return (error); + } + } + + /* Swap bufs */ + mtx_lock(&pr->pr_mtx); + osd_addr_old = osd_jail_get(pr, meta->osd_slot); + error = osd_jail_set(pr, meta->osd_slot, osd_addr); + mtx_unlock(&pr->pr_mtx); + + if (error != 0) + osd_addr_old = osd_addr; + + free(osd_addr_old, M_PRISON); + + return (error); +} + +static int +jm_osd_method_get(void *obj, void *data, struct meta *meta) +{ + struct prison *pr = obj; + struct vfsoptlist *opts = data; + char *osd_addr = NULL; + char empty = '\0'; + int error; + + /* Check the option presence to avoid unnecessary locking */ + error = vfs_getopt(opts, meta->name, NULL, NULL); + if (error == ENOENT) + return (0); + if (error != 0) + return (error); + + mtx_lock(&pr->pr_mtx); + osd_addr = osd_jail_get(pr, meta->osd_slot); + if (osd_addr == NULL) + error = vfs_setopts(opts, meta->name, &empty); + else + error = vfs_setopts(opts, meta->name, osd_addr); + mtx_unlock(&pr->pr_mtx); + + return (error); +} + +static int +jm_osd_method_check(void *obj __unused, void *data, struct meta *meta) +{ + struct vfsoptlist *opts = data; + char *value = NULL; + int error; + int len = 0; + + /* Check the option presence */ + error = vfs_getopt(opts, meta->name, (void **)&value, &len); + if (error == ENOENT) + return (0); + if (error != 0) + return (error); + + if (len < 1) + return (EINVAL); + if (value == NULL) + return (EINVAL); + + return (0); +} + +static void +jm_osd_destructor(void *osd_addr) +{ + free(osd_addr, M_PRISON); +} + + +/* OSD -- metaext */ + +static struct meta metaext; + +static inline int +jm_osd_method_set_metaext(void *obj, void *data) +{ + return (jm_osd_method_set(obj, data, &metaext)); +} + +static inline int +jm_osd_method_get_metaext(void *obj, void *data) +{ + return (jm_osd_method_get(obj, data, &metaext)); +} + +static inline int +jm_osd_method_check_metaext(void *obj __unused, void *data) +{ + return (jm_osd_method_check(obj, data, &metaext)); +} + +static struct meta metaext = { + .name = "metaext", + .osd_slot = 0, + .methods = { + [PR_METHOD_SET] = jm_osd_method_set_metaext, + [PR_METHOD_GET] = jm_osd_method_get_metaext, + [PR_METHOD_CHECK] = jm_osd_method_check_metaext, + } +}; + + +/* OSD -- metaint */ + +static struct meta metaint; + +static inline int +jm_osd_method_set_metaint(void *obj, void *data) +{ + return (jm_osd_method_set(obj, data, &metaint)); +} + +static inline int +jm_osd_method_get_metaint(void *obj, void *data) +{ + return (jm_osd_method_get(obj, data, &metaint)); +} + +static inline int +jm_osd_method_check_metaint(void *obj __unused, void *data) +{ + return (jm_osd_method_check(obj, data, &metaint)); +} + +static struct meta metaint = { + .name = "metaint", + .osd_slot = 0, + .methods = { + [PR_METHOD_SET] = jm_osd_method_set_metaint, + [PR_METHOD_GET] = jm_osd_method_get_metaint, + [PR_METHOD_CHECK] = jm_osd_method_check_metaint, + } +}; + + +/* A jail can read its internal meta */ + +static int +jm_sysctl_metaint(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, metaint.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, metaint, + CTLTYPE_STRING | CTLFLAG_RD | CTLFLAG_MPSAFE, + 0, 0, jm_sysctl_metaint, "A", "Jail internal meta information"); + + +/* Setup and tear down */ + +static int +jm_sysinit(void *arg __unused) +{ + metaext.osd_slot = osd_jail_register(jm_osd_destructor, metaext.methods); + metaint.osd_slot = osd_jail_register(jm_osd_destructor, metaint.methods); + + return (0); +} + +static int +jm_sysuninit(void *arg __unused) +{ + osd_jail_deregister(metaext.osd_slot); + osd_jail_deregister(metaint.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(module, param, type, fmt, descr) \ diff --git a/tests/sys/kern/Makefile b/tests/sys/kern/Makefile --- a/tests/sys/kern/Makefile +++ b/tests/sys/kern/Makefile @@ -57,6 +57,7 @@ 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 --- /dev/null +++ b/tests/sys/kern/jailmeta.sh @@ -0,0 +1,401 @@ +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2024 SkunkWerks GmbH +# +# This software was developed by Igor Ostapenko +# under sponsorship from SkunkWerks GmbH. +# + +setup() +{ + if [ $(sysctl -n security.jail.meta_maxbufsize) -lt 10 ]; then + atf_skip "sysctl security.jail.meta_maxbufsize must be 10+ for testing." + fi +} + +atf_test_case "jail_create" "cleanup" +jail_create_head() +{ + atf_set descr 'Test that meta 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 -j jail1 + + atf_check -s exit:0 \ + jail -c name=jail1 persist metaext="a b c" metaint="C B A" + + atf_check -s exit:0 -o inline:"a b c\n" \ + jls -j jail1 metaext + atf_check -s exit:0 -o inline:"C B A\n" \ + jls -j jail1 metaint +} +jail_create_cleanup() +{ + jail -r jail1 + return 0 +} + +atf_test_case "jail_modify" "cleanup" +jail_modify_head() +{ + atf_set descr 'Test that meta 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 -j jail1 + + atf_check -s exit:0 \ + jail -c name=jail1 persist metaext="a b c" metaint="internal" + + atf_check -s exit:0 -o inline:"a b c\n" \ + jls -j jail1 metaext + atf_check -s exit:0 -o inline:"internal\n" \ + jls -j jail1 metaint + + atf_check -s exit:0 \ + jail -m name=jail1 metaext="t1=A t2=B" metaint="internal2" + + atf_check -s exit:0 -o inline:"t1=A t2=B\n" \ + jls -j jail1 metaext + atf_check -s exit:0 -o inline:"internal2\n" \ + jls -j jail1 metaint +} +jail_modify_cleanup() +{ + jail -r jail1 + return 0 +} + +atf_test_case "jail_add" "cleanup" +jail_add_head() +{ + atf_set descr 'Test that meta 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 -j jail1 + + atf_check -s exit:0 \ + jail -c name=jail1 persist host.hostname=jail1 + + atf_check -s exit:0 -o inline:'""\n' \ + jls -j jail1 metaext + atf_check -s exit:0 -o inline:'""\n' \ + jls -j jail1 metaint + + atf_check -s exit:0 \ + jail -m name=jail1 metaext="$(jot 3 1 3)" metaint="$(jot 2 11 12)" + + atf_check -s exit:0 -o inline:"1\n2\n3\n" \ + jls -j jail1 metaext + atf_check -s exit:0 -o inline:"11\n12\n" \ + jls -j jail1 metaint +} +jail_add_cleanup() +{ + jail -r jail1 + return 0 +} + +atf_test_case "jail_reset" "cleanup" +jail_reset_head() +{ + atf_set descr 'Test that meta 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 -j jail1 + + atf_check -s exit:0 \ + jail -c name=jail1 persist metaext="123" metaint="456" + + atf_check -s exit:0 -o inline:"123\n" \ + jls -j jail1 metaext + atf_check -s exit:0 -o inline:"456\n" \ + jls -j jail1 metaint + + atf_check -s exit:0 \ + jail -m name=jail1 metaext= metaint= + + atf_check -s exit:0 -o inline:'""\n' \ + jls -j jail1 metaext + atf_check -s exit:0 -o inline:'""\n' \ + jls -j jail1 metaint +} +jail_reset_cleanup() +{ + jail -r jail1 + return 0 +} + +atf_test_case "jls_libxo" "cleanup" +jls_libxo_head() +{ + atf_set descr 'Test that meta 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 -j jail1 + + atf_check -s exit:0 \ + jail -c name=jail1 persist metaext="a b c" metaint="1 2 3" + + atf_check -s exit:0 -o inline:'{"__version": "2", "jail-information": {"jail": [{"name":"jail1","metaext":"a b c"}]}}\n' \ + jls -j jail1 --libxo json name metaext + atf_check -s exit:0 -o inline:'{"__version": "2", "jail-information": {"jail": [{"metaint":"1 2 3"}]}}\n' \ + jls -j jail1 --libxo json metaint +} +jls_libxo_cleanup() +{ + jail -r jail1 + return 0 +} + +atf_test_case "flua_create" "cleanup" +flua_create_head() +{ + atf_set descr 'Test that meta 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 -j jail1 + + atf_check -s exit:0 \ + /usr/libexec/flua -ljail -e 'jail.setparams("jail1", {["metaext"]="t1 t2=v2", ["metaint"]="secret", ["persist"]="true"}, jail.CREATE)' + + atf_check -s exit:0 -o inline:"t1 t2=v2\n" \ + /usr/libexec/flua -ljail -e 'jid, res = jail.getparams("jail1", {"metaext"}); print(res["metaext"])' + atf_check -s exit:0 -o inline:"secret\n" \ + /usr/libexec/flua -ljail -e 'jid, res = jail.getparams("jail1", {"metaint"}); print(res["metaint"])' +} +flua_create_cleanup() +{ + jail -r jail1 + return 0 +} + +atf_test_case "flua_modify" "cleanup" +flua_modify_head() +{ + atf_set descr 'Test that meta 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 -j jail1 + + atf_check -s exit:0 \ + jail -c name=jail1 persist metaext="ABC" metaint="123" + + atf_check -s exit:0 -o inline:"ABC\n" \ + jls -j jail1 metaext + atf_check -s exit:0 -o inline:"123\n" \ + jls -j jail1 metaint + + atf_check -s exit:0 \ + /usr/libexec/flua -ljail -e 'jail.setparams("jail1", {["metaext"]="t1 t2=v", ["metaint"]="4"}, jail.UPDATE)' + + atf_check -s exit:0 -o inline:"t1 t2=v\n" \ + jls -j jail1 metaext + atf_check -s exit:0 -o inline:"4\n" \ + jls -j jail1 metaint +} +flua_modify_cleanup() +{ + jail -r jail1 + return 0 +} + +atf_test_case "readable_from_jail" "cleanup" +readable_from_jail_head() +{ + atf_set descr 'Test that a jail can read its internal meta parameter via sysctl(8)' + atf_set require.user root + atf_set execenv jail +} +readable_from_jail_body() +{ + setup + + atf_check -s not-exit:0 -e match:"not found" -o ignore \ + jls -j jail1 + + atf_check -s exit:0 \ + jail -c name=jail1 persist metaext="a b c" metaint="internal data" + + atf_check -s exit:0 -o inline:"a b c\n" \ + jls -j jail1 metaext + atf_check -s exit:0 -o inline:"internal data\n" \ + jls -j jail1 metaint + + atf_check -s exit:0 -o inline:"internal data\n" \ + jexec jail1 sysctl -n security.jail.metaint +} +readable_from_jail_cleanup() +{ + jail -r jail1 + return 0 +} + +atf_test_case "not_inheritable" "cleanup" +not_inheritable_head() +{ + atf_set descr 'Test that a jail does not inherit meta parameter 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 metaext="parent-ext" metaint="parent-int" + + jexec parent jail -c name=child persist + + atf_check -s exit:0 -o inline:"parent-ext\n" \ + jls -j parent metaext + atf_check -s exit:0 -o inline:'""\n' \ + jls -j parent.child metaext + + atf_check -s exit:0 -o inline:"parent-int\n" \ + jexec parent sysctl -n security.jail.metaint + atf_check -s exit:0 -o inline:"\n" \ + jexec parent.child sysctl -n security.jail.metaint +} +not_inheritable_cleanup() +{ + jail -r parent.child + jail -r parent + return 0 +} + +atf_test_case "maxbufsize" "cleanup" +maxbufsize_head() +{ + atf_set descr 'Test that meta buffer maximum size can be changed via sysctl from prison0' + 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 metaext="$(printf %$((origmax-1))s)" + atf_check -s exit:0 -o inline:"${origmax}\n" \ + jls -j $jn metaext | wc -c + # + atf_check -s exit:0 \ + jail -m name=$jn metaint="$(printf %$((origmax-1))s)" + atf_check -s exit:0 -o inline:"${origmax}\n" \ + jls -j $jn metaint | wc -c + + # should not allow exceeding current max + atf_check -s not-exit:0 -e match:"too large" \ + jail -m name=$jn metaext="$(printf %${origmax}s)" + # + atf_check -s not-exit:0 -e match:"too large" \ + jail -m name=$jn metaint="$(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 metaext="$(printf %${origmax}s)" + atf_check -s exit:0 -o inline:"${origmax}\n" \ + jls -j $jn metaext | wc -c + # + atf_check -s exit:0 \ + jail -m name=$jn metaint="$(printf %${origmax}s)" + atf_check -s exit:0 -o inline:"${origmax}\n" \ + jls -j $jn metaint | 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 metaext="$(printf %${origmax}s)" + # + atf_check -s not-exit:0 -e match:"too large" \ + jail -m name=$jn metaint="$(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.metaext)" + atf_check_equal "${newmax}" "$(sysctl -n security.jail.param.metaint)" + atf_check -s exit:0 -o inline:"${origmax}\n" \ + jls -j $jn metaext | wc -c + # + atf_check -s exit:0 -o inline:"${origmax}\n" \ + jls -j $jn metaint | wc -c +} +maxbufsize_cleanup() +{ + jail -r jailmeta_maxbufsize + 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 "readable_from_jail" + atf_add_test_case "not_inheritable" + + atf_add_test_case "maxbufsize" +} 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,21 @@ The number for the jail's .Va kern.osreldate and uname -K. +.It Va metaext , Va metaint +Any string associated with the jail. +Its buffer size is limited by the global +.Va security.jail.meta_maxbufsize +sysctl, which can only be tuned by the non-jailed root user. +The +.Va metaext +is an external meta information which can be read only by parent jail(s). +The +.Va metaint +is an internal meta information which can be read by both, +the jail and its parents. +The jail can read it via +.Va security.jail.metaint +sysctl. .It Va allow.* Some restrictions of the jail environment may be set on a per-jail basis.