git: 44175ec8ce4d - stable/13 - jail(3lua): add a jail.list() method

From: Kyle Evans <kevans_at_FreeBSD.org>
Date: Wed, 06 Oct 2021 07:13:38 UTC
The branch stable/13 has been updated by kevans:

URL: https://cgit.FreeBSD.org/src/commit/?id=44175ec8ce4dd3ce76b3bd08eeb51b12edb7d5e3

commit 44175ec8ce4dd3ce76b3bd08eeb51b12edb7d5e3
Author:     Kyle Evans <kevans@FreeBSD.org>
AuthorDate: 2020-10-13 02:11:14 +0000
Commit:     Kyle Evans <kevans@FreeBSD.org>
CommitDate: 2021-10-06 07:13:23 +0000

    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.
    
    (cherry picked from commit 6a7647eccd3ef35189c63a61b0ec8865fd559839)
---
 lib/flua/libjail/jail.3lua      |  47 ++++++
 lib/flua/libjail/lua_jail.c     | 322 +++++++++++++++++++++++++++++++++++-----
 share/examples/flua/libjail.lua |  55 ++++++-
 3 files changed, 385 insertions(+), 39 deletions(-)

diff --git a/lib/flua/libjail/jail.3lua b/lib/flua/libjail/jail.3lua
index fb54b94844f5..aa1e0ec49616 100644
--- a/lib/flua/libjail/jail.3lua
+++ b/lib/flua/libjail/jail.3lua
@@ -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 ,
diff --git a/lib/flua/libjail/lua_jail.c b/lib/flua/libjail/lua_jail.c
index b66c60b43bc8..7bb0e13cceea 100644
--- a/lib/flua/libjail/lua_jail.c
+++ b/lib/flua/libjail/lua_jail.c
@@ -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);
 }
diff --git a/share/examples/flua/libjail.lua b/share/examples/flua/libjail.lua
index 3ff878460d2f..1761f5c86b24 100644
--- a/share/examples/flua/libjail.lua
+++ b/share/examples/flua/libjail.lua
@@ -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