jail(3lua): add a jail.list() method

This is implemented as an iterator, reusing parts of the earlier logic
to populate jailparams from a passed in table.

The user may request any number of parameters to pull in while we're
searching, but we'll force jid and name to appear at a minimum.

Reviewed by:	freqlabs
Differential Revision:	https://reviews.freebsd.org/D26756
This commit is contained in:
Kyle Evans 2020-10-12 21:11:14 -05:00
parent 9eb5fd3599
commit 6a7647eccd
3 changed files with 385 additions and 39 deletions

View file

@ -32,6 +32,7 @@
.Sh NAME
.Nm getid ,
.Nm getname ,
.Nm list ,
.Nm allparams ,
.Nm getparams ,
.Nm setparams ,
@ -50,6 +51,7 @@ local jail = require('jail')
.It Dv jid, err = jail.getid(name)
.It Dv name, err = jail.getname(jid)
.It Dv params, err = jail.allparams()
.It Dv iter, jail_obj = jail.list([params])
.It Dv jid, res = jail.getparams(jid|name, params [, flags ] )
.It Dv jid, err = jail.setparams(jid|name, params, flags )
.It Dv jail.CREATE
@ -79,6 +81,21 @@ is the name of a jail or a jid in the form of a string.
Get the name of a jail as a string for the given
.Fa jid
.Pq an integer .
.It Dv iter, jail_obj = jail.list([params])
Returns an iterator over running jails on the system.
.Dv params
is a list of parameters to fetch for each jail as we iterate.
.Dv jid
and
.Dv name
will always be returned, and may be omitted from
.Dv params .
Additionally,
.Dv params
may be omitted or an empty table, but not nil.
.Pp
See
.Sx EXAMPLES .
.It Dv params, err = jail.allparams()
Get a list of all supported parameter names
.Pq as strings .
@ -167,6 +184,10 @@ function returns a jail identifier integer and a table of jail parameters
with parameter name strings as keys and strings for values on success, or
.Dv nil
and an error message string if an error occurred.
.Pp
The
.Fn list
function returns an iterator over the list of running jails.
.Sh EXAMPLES
Set the hostname of jail
.Dq foo
@ -193,6 +214,32 @@ if not jid then
end
print(res["host.hostname"])
.Ed
.Pp
Iterate over jails on the system:
.Bd -literal -offset indent
local jail = require('jail')
-- Recommended: just loop over it
for jparams in jail.list() do
print(jparams["jid"] .. " = " .. jparams["name"])
end
-- Request path and hostname, too
for jparams in jail.list({"path", "host.hostname"}) do
print(jparams["host.hostname"] .. " mounted at " .. jparams["path"])
end
-- Raw iteration protocol
local iter, jail_obj = jail.list()
-- Request the first params
local jparams = jail_obj:next()
while jparams do
print(jparams["jid"] .. " = " .. jparams["name"])
-- Subsequent calls may return nil
jparams = jail_obj:next()
end
.Ed
.Sh SEE ALSO
.Xr jail 2 ,
.Xr jail 3 ,

View file

@ -2,6 +2,7 @@
* SPDX-License-Identifier: BSD-2-Clause
*
* Copyright (c) 2020, Ryan Moeller <freqlabs@FreeBSD.org>
* Copyright (c) 2020, Kyle Evans <kevans@FreeBSD.org>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
@ -34,6 +35,7 @@ __FBSDID("$FreeBSD$");
#include <sys/jail.h>
#include <errno.h>
#include <jail.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
@ -41,8 +43,222 @@ __FBSDID("$FreeBSD$");
#include <lauxlib.h>
#include <lualib.h>
#define JAIL_METATABLE "jail iterator metatable"
/*
* Taken from RhodiumToad's lspawn implementation, let static analyzers make
* better decisions about the behavior after we raise an error.
*/
#if defined(LUA_VERSION_NUM) && defined(LUA_API)
LUA_API int (lua_error) (lua_State *L) __dead2;
#endif
#if defined(LUA_ERRFILE) && defined(LUALIB_API)
LUALIB_API int (luaL_argerror) (lua_State *L, int arg, const char *extramsg) __dead2;
LUALIB_API int (luaL_typeerror) (lua_State *L, int arg, const char *tname) __dead2;
LUALIB_API int (luaL_error) (lua_State *L, const char *fmt, ...) __dead2;
#endif
int luaopen_jail(lua_State *);
typedef bool (*getparam_filter)(const char *, void *);
static void getparam_table(lua_State *L, int paramindex,
struct jailparam *params, size_t paramoff, size_t *params_countp,
getparam_filter keyfilt, void *udata);
struct l_jail_iter {
struct jailparam *params;
size_t params_count;
int jid;
};
static bool
l_jail_filter(const char *param_name, void *data __unused)
{
/*
* Allowing lastjid will mess up our iteration over all jails on the
* system, as this is a special paramter that indicates where the search
* starts from. We'll always add jid and name, so just silently remove
* these.
*/
return (strcmp(param_name, "lastjid") != 0 &&
strcmp(param_name, "jid") != 0 &&
strcmp(param_name, "name") != 0);
}
static int
l_jail_iter_next(lua_State *L)
{
struct l_jail_iter *iter, **iterp;
struct jailparam *jp;
int serrno;
iterp = (struct l_jail_iter **)luaL_checkudata(L, 1, JAIL_METATABLE);
iter = *iterp;
luaL_argcheck(L, iter != NULL, 1, "closed jail iterator");
jp = iter->params;
/* Populate lastjid; we must keep it in params[0] for our sake. */
if (jailparam_import_raw(&jp[0], &iter->jid, sizeof(iter->jid))) {
jailparam_free(jp, iter->params_count);
free(jp);
free(iter);
*iterp = NULL;
return (luaL_error(L, "jailparam_import_raw: %s", jail_errmsg));
}
/* The list of requested params was populated back in l_list(). */
iter->jid = jailparam_get(jp, iter->params_count, 0);
if (iter->jid == -1) {
/*
* We probably got an ENOENT to signify the end of the jail
* listing, but just in case we didn't; stash it off and start
* cleaning up. We'll handle non-ENOENT errors later.
*/
serrno = errno;
jailparam_free(jp, iter->params_count);
free(iter->params);
free(iter);
*iterp = NULL;
if (serrno != ENOENT)
return (luaL_error(L, "jailparam_get: %s",
strerror(serrno)));
return (0);
}
/*
* Finally, we'll fill in the return table with whatever parameters the
* user requested, in addition to the ones we forced with exception to
* lastjid.
*/
lua_newtable(L);
for (size_t i = 0; i < iter->params_count; ++i) {
char *value;
jp = &iter->params[i];
if (strcmp(jp->jp_name, "lastjid") == 0)
continue;
value = jailparam_export(jp);
lua_pushstring(L, value);
lua_setfield(L, -2, jp->jp_name);
free(value);
}
return (1);
}
static int
l_jail_iter_close(lua_State *L)
{
struct l_jail_iter *iter, **iterp;
/*
* Since we're using this as the __gc method as well, there's a good
* chance that it's already been cleaned up by iterating to the end of
* the list.
*/
iterp = (struct l_jail_iter **)lua_touserdata(L, 1);
iter = *iterp;
if (iter == NULL)
return (0);
jailparam_free(iter->params, iter->params_count);
free(iter->params);
free(iter);
*iterp = NULL;
return (0);
}
static int
l_list(lua_State *L)
{
struct l_jail_iter *iter;
int nargs;
nargs = lua_gettop(L);
if (nargs >= 1)
luaL_checktype(L, 1, LUA_TTABLE);
iter = malloc(sizeof(*iter));
if (iter == NULL)
return (luaL_error(L, "malloc: %s", strerror(errno)));
/*
* lastjid, jid, name + length of the table. This may be too much if
* we have duplicated one of those fixed parameters.
*/
iter->params_count = 3 + (nargs != 0 ? lua_rawlen(L, 1) : 0);
iter->params = malloc(iter->params_count * sizeof(*iter->params));
if (iter->params == NULL) {
free(iter);
return (luaL_error(L, "malloc params: %s", strerror(errno)));
}
/* The :next() method will populate lastjid before jail_getparam(). */
if (jailparam_init(&iter->params[0], "lastjid") == -1) {
free(iter->params);
free(iter);
return (luaL_error(L, "jailparam_init: %s", jail_errmsg));
}
/* These two will get populated by jail_getparam(). */
if (jailparam_init(&iter->params[1], "jid") == -1) {
jailparam_free(iter->params, 1);
free(iter->params);
free(iter);
return (luaL_error(L, "jailparam_init: %s",
jail_errmsg));
}
if (jailparam_init(&iter->params[2], "name") == -1) {
jailparam_free(iter->params, 2);
free(iter->params);
free(iter);
return (luaL_error(L, "jailparam_init: %s",
jail_errmsg));
}
/*
* We only need to process additional arguments if we were given any.
* That is, we don't descend into getparam_table if we're passed nothing
* or an empty table.
*/
iter->jid = 0;
if (iter->params_count != 3)
getparam_table(L, 1, iter->params, 2, &iter->params_count,
l_jail_filter, NULL);
/*
* Part of the iterator magic. We give it an iterator function with a
* metatable defining next() and close() that can be used for manual
* iteration. iter->jid is how we track which jail we last iterated, to
* be supplied as "lastjid".
*/
lua_pushcfunction(L, l_jail_iter_next);
*(struct l_jail_iter **)lua_newuserdata(L,
sizeof(struct l_jail_iter **)) = iter;
luaL_getmetatable(L, JAIL_METATABLE);
lua_setmetatable(L, -2);
return (2);
}
static void
register_jail_metatable(lua_State *L)
{
luaL_newmetatable(L, JAIL_METATABLE);
lua_newtable(L);
lua_pushcfunction(L, l_jail_iter_next);
lua_setfield(L, -2, "next");
lua_pushcfunction(L, l_jail_iter_close);
lua_setfield(L, -2, "close");
lua_setfield(L, -2, "__index");
lua_pushcfunction(L, l_jail_iter_close);
lua_setfield(L, -2, "__gc");
lua_pop(L, 1);
}
static int
l_getid(lua_State *L)
{
@ -100,12 +316,71 @@ l_allparams(lua_State *L)
return (1);
}
static void
getparam_table(lua_State *L, int paramindex, struct jailparam *params,
size_t params_off, size_t *params_countp, getparam_filter keyfilt,
void *udata)
{
size_t params_count;
int skipped;
params_count = *params_countp;
skipped = 0;
for (size_t i = 1 + params_off; i < params_count; ++i) {
const char *param_name;
lua_rawgeti(L, -1, i - params_off);
param_name = lua_tostring(L, -1);
if (param_name == NULL) {
jailparam_free(params, i - skipped);
free(params);
luaL_argerror(L, paramindex,
"param names must be strings");
}
lua_pop(L, 1);
if (keyfilt != NULL && !keyfilt(param_name, udata)) {
++skipped;
continue;
}
if (jailparam_init(&params[i - skipped], param_name) == -1) {
jailparam_free(params, i - skipped);
free(params);
luaL_error(L, "jailparam_init: %s", jail_errmsg);
}
}
*params_countp -= skipped;
}
struct getparams_filter_args {
int filter_type;
};
static bool
l_getparams_filter(const char *param_name, void *udata)
{
struct getparams_filter_args *gpa;
gpa = udata;
/* Skip name or jid, whichever was given. */
if (gpa->filter_type == LUA_TSTRING) {
if (strcmp(param_name, "name") == 0)
return (false);
} else /* type == LUA_TNUMBER */ {
if (strcmp(param_name, "jid") == 0)
return (false);
}
return (true);
}
static int
l_getparams(lua_State *L)
{
const char *name;
struct jailparam *params;
size_t params_count, skipped;
size_t params_count;
struct getparams_filter_args gpa;
int flags, jid, type;
type = lua_type(L, 1);
@ -154,40 +429,8 @@ l_getparams(lua_State *L)
/*
* Set the remaining param names being requested.
*/
skipped = 0;
for (size_t i = 1; i < params_count; ++i) {
const char *param_name;
lua_rawgeti(L, -1, i);
param_name = lua_tostring(L, -1);
if (param_name == NULL) {
jailparam_free(params, i - skipped);
free(params);
return (luaL_argerror(L, 2,
"param names must be strings"));
}
lua_pop(L, 1);
/* Skip name or jid, whichever was given. */
if (type == LUA_TSTRING) {
if (strcmp(param_name, "name") == 0) {
++skipped;
continue;
}
} else /* type == LUA_TNUMBER */ {
if (strcmp(param_name, "jid") == 0) {
++skipped;
continue;
}
}
if (jailparam_init(&params[i - skipped], param_name) == -1) {
jailparam_free(params, i - skipped);
free(params);
return (luaL_error(L, "jailparam_init: %s",
jail_errmsg));
}
}
params_count -= skipped;
gpa.filter_type = type;
getparam_table(L, 2, params, 0, &params_count, l_getparams_filter, &gpa);
/*
* Get the values and convert to a table.
@ -366,6 +609,13 @@ static const struct luaL_Reg l_jail[] = {
* or nil, error (string) on error
*/
{"setparams", l_setparams},
/** Get a list of jail parameters for running jails on the system.
* @param params optional list of parameter names (table of
* strings)
* @return iterator (function), jail_obj (object) with next and
* close methods
*/
{"list", l_list},
{NULL, NULL}
};
@ -385,5 +635,7 @@ luaopen_jail(lua_State *L)
lua_pushinteger(L, JAIL_DYING);
lua_setfield(L, -2, "DYING");
register_jail_metatable(L);
return (1);
}

View file

@ -35,10 +35,23 @@ ucl = require("ucl")
name = "demo"
-- Create a persistent jail named "demo" with all other parameters default.
jid, err = jail.setparams(name, {persist = "true"}, jail.CREATE)
if not jid then
error(err)
local has_demo = false
-- Make sure we don't have a demo jail to start with; "jid" and "name" are
-- always present.
for jparams in jail.list() do
if jparams["name"] == name then
has_demo = true
break
end
end
if not has_demo then
-- Create a persistent jail named "demo" with all other parameters default.
jid, err = jail.setparams(name, {persist = "true"}, jail.CREATE)
if not jid then
error(err)
end
end
-- Get a list of all known jail parameter names.
@ -53,8 +66,42 @@ end
-- Display the jail's parameters as a pretty-printed JSON object.
print(ucl.to_json(res))
-- Confirm that we still have it for now.
has_demo = false
for jparams in jail.list() do
if jparams["name"] == name then
has_demo = true
break
end
end
if not has_demo then
print("demo does not exist")
end
-- Update the "persist" parameter to "false" to remove the jail.
jid, err = jail.setparams(name, {persist = "false"}, jail.UPDATE)
if not jid then
error(err)
end
-- Verify that the jail is no longer on the system.
local is_persistent = false
has_demo = false
for jparams in jail.list({"persist"}) do
if jparams["name"] == name then
has_demo = true
jid = jparams["jid"]
is_persistent = jparams["persist"] ~= "false"
end
end
-- In fact, it does remain until this process ends -- c'est la vie.
if has_demo then
io.write("demo still exists, jid " .. jid .. ", ")
if is_persistent then
io.write("persistent\n")
else
io.write("not persistent\n")
end
end