Commit d44bae94 authored by Karel Koci's avatar Karel Koci 🤘

Allow embedding of busybox binary to updater

This allows us to not depend on system binaries in updater it self. And
is one step to make updater self contained just so that it will work on
even pretty much broken system.
parent 59e59e08
......@@ -4,6 +4,9 @@ LUA_NAME := $(shell for lua in lua5.1 lua-5.1 lua51 lua ; do if pkg-config $$lua
VALGRIND:=IN_VALGRIND=1 valgrind --leak-check=full --show-leak-kinds=all --track-fds=yes --trace-children=no --child-silent-after-fork=yes --error-exitcode=1
# For picosat, it otherwise needs some headers not available on musl for a feature we don't need. And we need trace enabled.
EXTRA_DEFINES := NGETRUSAGE TRACE UPDATER_VERSION='"$(UPDATER_VERSION)"'
ifdef BUSYBOX_EXEC
EXTRA_DEFINES += BUSYBOX_EMBED
endif
include $(S)/build/Makefile.top
......
......@@ -12,6 +12,11 @@ $(O)/.gen/src/lib/lautoload.embedlist: SUFFIX := .lua
$(O)/.gen/src/lib/lcoverage.embedlist: $(S)/src/lib/coverage.lua
$(O)/.gen/src/lib/lcoverage.embedlist: SUFFIX := .lua
ifdef BUSYBOX_EXEC
# We have to ignore this if not provided to avoid error
$(O)/.gen/src/lib/busybox_exec.embedlist:$(BUSYBOX_EXEC)
endif
libupdater_MODULES := \
arguments \
inject \
......@@ -26,6 +31,9 @@ libupdater_MODULES := \
ifdef COV
libupdater_MODULES += lcoverage.embed
endif
ifdef BUSYBOX_EXEC
libupdater_MODULES += busybox_exec.embed
endif
libupdater_MODULES_3RDPARTY := \
md5 \
......
......@@ -31,7 +31,7 @@ local math = math
local io = io
local unpack = unpack
local events_wait = events_wait
local run_command = run_command
local run_util = run_util
module "utils"
......@@ -104,11 +104,11 @@ end
-- Run rm -rf on all dirs in the provided table
function cleanup_dirs(dirs)
if next(dirs) then
events_wait(run_command(function (ecode, _, _, stderr)
events_wait(run_util(function (ecode, _, _, stderr)
if ecode ~= 0 then
error("rm -rf failed: " .. stderr)
end
end, nil, nil, -1, -1, "/bin/rm", "-rf", unpack(dirs)));
end, nil, nil, -1, -1, "rm", "-rf", unpack(dirs)));
end
end
......
......@@ -39,6 +39,7 @@ local getcwd = getcwd
local mkdtemp = mkdtemp
local chdir = chdir
local run_command = run_command
local run_util = run_util
local events_wait = events_wait
local stat = stat
local lstat = lstat
......@@ -426,11 +427,11 @@ function pkg_unpack(package, tmp_dir)
local err
-- Unpack the ipk into s1dir, getting control.tar.gz and data.tar.gz
local function stage1()
events_wait(run_command(function (ecode, _, _, stderr)
events_wait(run_util(function (ecode, _, _, stderr)
if ecode ~= 0 then
err = "Stage 1 unpack failed: " .. stderr
end
end, function () chdir(s1dir) end, package, cmd_timeout, cmd_kill_timeout, "/bin/sh", "-c", "/bin/gzip -dc | /bin/tar x"))
end, function () chdir(s1dir) end, package, cmd_timeout, cmd_kill_timeout, "sh", "-c", "gzip -dc | tar x"))
-- TODO: Sanity check debian-binary
return err == nil
end
......@@ -438,11 +439,11 @@ function pkg_unpack(package, tmp_dir)
local function unpack_archive(what)
local archive = s1dir .. "/" .. what .. ".tar.gz"
local dir = s2dir .. "/" .. what
return run_command(function (ecode, _, _, stderr)
return run_util(function (ecode, _, _, stderr)
if ecode ~= 0 then
err = "Stage 2 unpack of " .. what .. " failed: " .. stderr
end
end, nil, package, cmd_timeout, cmd_kill_timeout, "/bin/sh", "-c", "mkdir -p '" .. dir .. "' && cd '" .. dir .. "' && /bin/gzip -dc <'" .. archive .. "' | /bin/tar xp")
end, nil, package, cmd_timeout, cmd_kill_timeout, "sh", "-c", "mkdir -p '" .. dir .. "' && cd '" .. dir .. "' && gzip -dc <'" .. archive .. "' | tar xp")
end
local function stage2()
events_wait(unpack_archive("control"), unpack_archive("data"))
......@@ -454,11 +455,11 @@ function pkg_unpack(package, tmp_dir)
local events = {}
local function remove(dir)
-- TODO: Would it be better to remove from within our code, without calling rm?
table.insert(events, run_command(function (ecode, _, _, stderr)
table.insert(events, run_util(function (ecode, _, _, stderr)
if ecode ~= 0 then
WARN("Failed to clean up work directory ", dir, ": ", stderr)
end
end, nil, nil, cmd_timeout, cmd_kill_timeout, "/bin/rm", "-rf", dir))
end, nil, nil, cmd_timeout, cmd_kill_timeout, "rm", "-rf", dir))
end
-- Intermediate work space, not needed by the caller
remove(s1dir)
......@@ -503,7 +504,7 @@ function pkg_examine(dir)
err = stderr
end
end
local event = run_command(cback, function () chdir(data_dir) end, nil, cmd_timeout, cmd_kill_timeout, ...)
local event = run_util(cback, function () chdir(data_dir) end, nil, cmd_timeout, cmd_kill_timeout, ...)
table.insert(events, event)
end
local function find_result(text)
......@@ -515,9 +516,9 @@ function pkg_examine(dir)
end
local files, dirs
-- One for non-directories
launch(function (text) files = slashes_sanitize(find_result(text)) end, "/usr/bin/find", "!", "-type", "d", "-print0")
launch(function (text) files = slashes_sanitize(find_result(text)) end, "find", "!", "-type", "d", "-print0")
-- One for directories
launch(function (text) dirs = slashes_sanitize(find_result(text)) end, "/usr/bin/find", "-type", "d", "-print0")
launch(function (text) dirs = slashes_sanitize(find_result(text)) end, "find", "-type", "d", "-print0")
-- Get list of config files, if there are any
local control_dir = dir .. "/control"
local cidx = io.open(control_dir .. "/conffiles")
......@@ -932,10 +933,10 @@ function pkg_merge_control(dir, name, files)
WARN("Control file " .. fname .. " is not a file, skipping")
else
DBG("Putting control file " .. fname .. " into place")
table.insert(events, run_command(function (ecode, _, _, stderr)
table.insert(events, run_util(function (ecode, _, _, stderr)
ec = ecode
err = stderr
end, nil, nil, cmd_timeout, cmd_kill_timeout, "/bin/cp", "-Lpf", dir .. "/" .. fname, info_dir .. "/" .. name .. '.' .. fname))
end, nil, nil, cmd_timeout, cmd_kill_timeout, "cp", "-Lpf", dir .. "/" .. fname, info_dir .. "/" .. name .. '.' .. fname))
end
if ec ~= 0 then
error(err)
......
......@@ -39,6 +39,7 @@ local string = string
local events_wait = events_wait
local download = download
local run_command = run_command
local run_util = run_util
local utils = require "utils"
local DBG = DBG
local uri_internal_get = uri_internal_get
......@@ -418,7 +419,7 @@ function new(context, uri, verification)
sigval(stdout)
end
end
events_wait(run_command(gzip_done, nil, result.content, -1, -1, '/bin/gzip', '-c', '-d'))
events_wait(run_util(gzip_done, nil, result.content, -1, -1, 'gzip', '-c', '-d'))
end
if not found then
local msg = "Signature validation failed"
......
......@@ -29,7 +29,7 @@ local unpack = unpack
local table = table
local string = string
local events_wait = events_wait
local run_command = run_command
local run_util = run_util
local mkdtemp = mkdtemp
local DBG = DBG
local WARN = WARN
......@@ -116,7 +116,7 @@ function get_repos()
elseif answer:sub(1, 2) == string.char(0x1F, 0x8B) then
-- It starts with gzip magic - we want to decompress it
DBG("Index " .. name .. " is compressed, decompressing")
table.insert(extract_events, run_command(decompressed, nil, answer, -1, -1, '/bin/gzip', '-dc'))
table.insert(extract_events, run_util(decompressed, nil, answer, -1, -1, 'gzip', '-dc'))
else
parse(answer)
end
......
......@@ -19,6 +19,7 @@
#include "events.h"
#include "util.h"
#include "embed_types.h"
#include <event2/event.h>
#include <event2/bufferevent.h>
......@@ -27,6 +28,8 @@
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <libgen.h>
#include <errno.h>
#include <stdbool.h>
#include <string.h>
......@@ -103,6 +106,15 @@ struct events {
struct wait_id *pending;
};
#ifdef BUSYBOX_EMBED
/*
* Function used for initialization of run_util functions (exports busybox to /tmp
* It is using reference counting, so for last call to run_util_clean it also
* cleans variables and files needed by run_util.
*/
static void run_util_init(void);
#endif
struct events *events_new(void) {
// We do a lot of writing to pipes and stuff. We don't want to be killed by a SIGPIPE from these, we shall handle errors of writing.
ASSERT_MSG(sigaction(SIGPIPE, &(struct sigaction) {
......@@ -120,6 +132,9 @@ struct events *events_new(void) {
ASSERT_MSG(result->base, "Failed to allocate the libevent event loop");
event_config_free(config);
result->downloads_max = DOWNLOAD_SLOTS;
#ifdef BUSYBOX_EMBED
run_util_init();
#endif
return result;
}
......@@ -260,25 +275,32 @@ struct wait_id watch_child(struct events *events, child_callback_t callback, voi
return child_id(pid);
}
struct wait_id run_command(struct events *events, command_callback_t callback, post_fork_callback_t post_fork, void *data, size_t input_size, const char *input, int term_timeout, int kill_timeout, const char *command, ...) {
struct wait_id run_command_v(struct events *events, command_callback_t callback, post_fork_callback_t post_fork, void *data, size_t input_size, const char *input, int term_timeout, int kill_timeout, const char *command, va_list args) {
size_t param_count = 1; // For the NULL terminator
va_list args;
va_list args_copy;
va_copy(args_copy, args); // for counting use copy
// Count how many parameters there are
va_start(args, command);
while (va_arg(args, const char *) != NULL)
while (va_arg(args_copy, const char *) != NULL)
param_count ++;
va_end(args);
va_end(args_copy);
// Prepare the array on stack and fill with the parameters
const char *params[param_count];
size_t i = 0;
va_start(args, command);
// Copies the terminating NULL as well.
while((params[i ++] = va_arg(args, const char *)) != NULL)
; // No body of the while. Everything is done in the conditional.
va_end(args);
// In new subprocess args are not closed, but because of exec whole stack is dropped so no harm there.
return run_command_a(events, callback, post_fork, data, input_size, input, term_timeout, kill_timeout, command, params);
}
struct wait_id run_command(struct events *events, command_callback_t callback, post_fork_callback_t post_fork, void *data, size_t input_size, const char *input, int term_timeout, int kill_timeout, const char *command, ...) {
va_list args;
va_start(args, command);
struct wait_id r = run_command_v(events, callback, post_fork, data, input_size, input, term_timeout, kill_timeout, command, args);
va_end(args);
return r;
}
static void run_child(post_fork_callback_t post_fork, void *data, const char *command, const char **params, int in_pipe[2], int out_pipe[2], int err_pipe[2]) {
// TODO: Close all other FDs
ASSERT(close(in_pipe[1]) != -1);
......@@ -509,6 +531,120 @@ struct wait_id run_command_a(struct events *events, command_callback_t callback,
}
}
#ifdef BUSYBOX_EMBED
const char run_util_tmp_template[] = "/tmp/updater-busybox-XXXXXX";
const char run_util_busybox_name[] = "busybox";
// Path of extracted busybox.
// sizeof returns whole size of array (so including '\0'). Using two sizeof creates
// this way two additional bytes in array, one is used for '\0' and second one for '/'
char run_util_busybox[sizeof(run_util_tmp_template) + sizeof(run_util_busybox_name)];
int run_util_init_counter; // Reference counter
extern struct file_index_element busybox_exec[];
static void run_util_init(void) {
run_util_init_counter++;
if (run_util_init_counter > 1)
return;
strcpy(run_util_busybox, run_util_tmp_template); // Copy string from constant template to used string
// Busybox executable have to be named as busybox otherwise it doesn't work as expected. So we put it to temporally directory
ASSERT(mkdtemp(run_util_busybox)); // mkdtemp edits run_util_busybox (replaces XXXXXX).
run_util_busybox[sizeof(run_util_tmp_template) - 1] = '/'; // We append slash replacing \0
strcpy(run_util_busybox + sizeof(run_util_tmp_template), run_util_busybox_name); // Copy busybox executable name to string.
DBG("Dumping busybox to: %s", run_util_busybox);
int f;
ASSERT_MSG((f = open(run_util_busybox, O_WRONLY | O_CREAT, S_IXUSR | S_IRUSR)) != -1, "Busybox file open failed: %s", strerror(errno));
size_t written = 0;
while (written < busybox_exec[0].size) {
int wrtn;
ASSERT_MSG((wrtn = write(f, busybox_exec[0].data, busybox_exec[0].size)) != -1 || errno == EINTR, "Busybox write failed: %s", strerror(errno));
if (wrtn == -1)
wrtn = 0;
written += wrtn;
}
ASSERT(!close(f));
}
static void run_util_clean(void) {
run_util_init_counter--;
if (run_util_init_counter > 0)
return;
DBG("Removing temporally busybox from: %s", run_util_busybox);
if (remove(run_util_busybox)) {
WARN("Busybox cleanup failed: %s", strerror(errno));
} else if (rmdir(dirname(run_util_busybox))) {
WARN("Busybox directory cleanup failed: %s", strerror(errno));
}
}
struct wait_id run_util_a(struct events* events, command_callback_t callback, post_fork_callback_t post_fork, void *data, size_t input_size, const char *input, int term_timeout, int kill_timeout, const char *function, const char **params) {
size_t params_count = 1; // One more to also count NULL terminator
for (const char **p = params; *p != NULL; p++)
params_count++;
const char *new_params[params_count + 1]; // One more for busybox function
new_params[0] = function;
memcpy(new_params + 1, params, params_count * sizeof *params); // Copies terminating NULL as well
return run_command_a(events, callback, post_fork, data, input_size, input, term_timeout, kill_timeout, run_util_busybox, new_params);
}
struct wait_id run_util(struct events* events, command_callback_t callback, post_fork_callback_t post_fork, void *data, size_t input_size, const char *input, int term_timeout, int kill_timeout, const char *function, ...) {
size_t param_count = 1; // One more for terminating NULL
va_list args;
va_start(args, function);
while (va_arg(args, const char *) != NULL)
param_count ++;
va_end(args);
const char *params[param_count + 1]; // One more for busybox function
params[0] = function;
size_t i = 1;
va_start(args, function);
while((params[i ++] = va_arg(args, const char *)) != NULL) // Copies the terminating NULL as well.
;
va_end(args);
return run_command_a(events, callback, post_fork, data, input_size, input, term_timeout, kill_timeout, run_util_busybox, params);
}
#else /* BUSYBOX_EMBED */
// When we are not using busybox these are system paths where we can find those tools
struct {
const char *fnc, *cmd;
} run_util_command[] = {
{"mv", "/bin/mv"},
{"cp", "/bin/cp"},
{"rm", "/bin/rm"},
{"gzip", "/bin/gzip"},
{"find", "/usr/bin/find"},
{"sh", "/bin/sh"},
{"mktemp", "/bin/mktemp"},
{NULL, NULL} // This must be NULL terminated
};
static const char *run_util_get_cmd(const char *fnc) {
size_t i = 0;
while (run_util_command[i].fnc != NULL) {
if (!strcmp(run_util_command[i].fnc, fnc))
return run_util_command[i].cmd;
i++;
}
DIE("run_util called with unsupported function: %s", fnc);
}
struct wait_id run_util(struct events* events, command_callback_t callback, post_fork_callback_t post_fork, void *data, size_t input_size, const char *input, int term_timeout, int kill_timeout, const char *function, ...) {
va_list args;
va_start(args, function);
struct wait_id r = run_command_v(events, callback, post_fork, data, input_size, input, term_timeout, kill_timeout, run_util_get_cmd(function), args);
va_end(args);
return r;
}
struct wait_id run_util_a(struct events* events, command_callback_t callback, post_fork_callback_t post_fork, void *data, size_t input_size, const char *input, int term_timeout, int kill_timeout, const char *function, const char **params) {
return run_command_a(events, callback, post_fork, data, input_size, input, term_timeout, kill_timeout, run_util_get_cmd(function), params);
}
#endif /* BUSYBOX_EMBED */
static ssize_t download_index_lookup(struct events *events, uint64_t id) {
for (size_t i = 0; i < events->download_count; i++) {
if (events->downloads[i]->id == id)
......@@ -865,4 +1001,7 @@ void events_destroy(struct events *events) {
free(events->downloads);
free(events->pending);
free(events);
#ifdef BUSYBOX_EMBED
run_util_clean();
#endif
}
......@@ -24,6 +24,7 @@
#include <unistd.h>
#include <stdint.h>
#include <stdarg.h>
struct events;
struct watched_command *command;
......@@ -131,6 +132,25 @@ typedef void (*post_fork_callback_t)(void *data);
struct wait_id run_command(struct events *events, command_callback_t callback, post_fork_callback_t post_fork, void *data, size_t input_size, const char *input, int term_timeout, int kill_timeout, const char *command, ...) __attribute__((nonnull(1, 2, 9)));
// Exactly the same as run_command, but with array for parameters.
struct wait_id run_command_a(struct events *events, command_callback_t callback, post_fork_callback_t post_fork, void *data, size_t input_size, const char *input, int term_timeout, int kill_timeout, const char *command, const char **params) __attribute__((nonnull(1, 2, 9)));
// Exactly the same as run_command, but with va_list for parameters. (va_list
// isn't closed in function, so it have to be closed afterwards)
struct wait_id run_command_v(struct events *events, command_callback_t callback, post_fork_callback_t post_fork, void *data, size_t input_size, const char *input, int term_timeout, int kill_timeout, const char *command, va_list params) __attribute__((nonnull(1, 2)));
/*
* These functions are same as run_command except that you don't have to specify
* exact path to command and instead you specify busybox function (second argument
* to busybox binary call). This ensures that when we have busybox embedded then
* it is used and system binaries aren't, but when we don't we wont fail.
*
* These were added because we have to be able to run on broken system (when we
* break it while we are moving files around). So updater is able to embed busybox
* executable and use it instead of system utilities.
*
* Note when you are using any new function, you should add it to data structure
* run_util_command in events.c.
*/
struct wait_id run_util(struct events* events, command_callback_t callback, post_fork_callback_t post_fork, void *data, size_t input_size, const char *input, int term_timeout, int kill_timeout, const char *function, ...) __attribute__((nonnull(1, 2)));
struct wait_id run_util_a(struct events* events, command_callback_t callback, post_fork_callback_t post_fork, void *data, size_t input_size, const char *input, int term_timeout, int kill_timeout, const char *function, const char **params) __attribute__((nonnull(1, 2)));
/*
* A callback called after download finished.
......
......@@ -282,7 +282,7 @@ static void push_wid(lua_State *L, const struct wait_id *id) {
lua_setmetatable(L, -2);
}
static int lua_run_command(lua_State *L) {
static int lua_run_generic(lua_State *L, bool utils) {
// Flush the lua output (it seems to buffered separately)
do_flush(L, "stdout");
do_flush(L, "stderr");
......@@ -296,7 +296,10 @@ static int lua_run_command(lua_State *L) {
int term_timeout = luaL_checkinteger(L, 4);
int kill_timeout = luaL_checkinteger(L, 5);
const char *command = luaL_checkstring(L, 6);
DBG("Command %s", command);
if (utils) {
DBG("Util command %s", command);
} else
DBG("Command %s", command);
// The rest of the args are args for the command ‒ get them into an array
const size_t arg_count = lua_gettop(L) - 6;
const char *args[arg_count + 1];
......@@ -314,12 +317,24 @@ static int lua_run_command(lua_State *L) {
const char *input = NULL;
if (lua_isstring(L, 3))
input = lua_tolstring(L, 3, &input_size);
struct wait_id id = run_command_a(events, command_terminated, command_postfork, data, input_size, input, term_timeout, kill_timeout, command, args);
struct wait_id id;
if (utils)
id = run_util_a(events, command_terminated, command_postfork, data, input_size, input, term_timeout, kill_timeout, command, args);
else
id = run_command_a(events, command_terminated, command_postfork, data, input_size, input, term_timeout, kill_timeout, command, args);
push_wid(L, &id);
// Return 1 value ‒ the wait_id
return 1;
}
static int lua_run_command(lua_State *L) {
return lua_run_generic(L, false);
}
static int lua_run_util(lua_State *L) {
return lua_run_generic(L, true);
}
struct lua_download_data {
lua_State *L;
char *callback;
......@@ -472,7 +487,7 @@ static int lua_move(lua_State *L) {
struct events *events = extract_registry(L, "events");
ASSERT(events);
struct mv_result_data mv_result_data = { .err = NULL };
struct wait_id id = run_command(events, mv_result, NULL, &mv_result_data, 0, NULL, -1, -1, "/bin/mv", "-f", old, new, (const char *)NULL);
struct wait_id id = run_util(events, mv_result, NULL, &mv_result_data, 0, NULL, -1, -1, "mv", "-f", old, new, (const char *)NULL);
events_wait(events, 1, &id);
if (mv_result_data.status) {
lua_pushfstring(L, "Failed to move '%s' to '%s': %s (ecode %d)", old, new, mv_result_data.err, mv_result_data.status);
......@@ -722,6 +737,7 @@ static const struct injected_func injected_funcs[] = {
{ lua_state_log_enabled, "state_log_enabled" },
{ lua_state_dump, "state_dump" },
{ lua_run_command, "run_command" },
{ lua_run_util, "run_util" },
{ lua_download, "download" },
{ lua_events_wait, "events_wait" },
/*
......
......@@ -82,6 +82,13 @@ Currently, there's no way to cancel running command from lua, since
that would make the wrapper code needlessly complex (while there seems
to be no need to cancel them currently).
For calling standard shell utility, or in fact every busybox function,
`run_util` is provided. It is same as `run_command` except it accepts
as `command` parameter one of supported busybox function names. So you
pass for example `rm` instead of `/bin/rm`. This is preferred way of
calling them, because when busybox is embedded then we don't rely on
system utilities and so it should be more reliable.
The other event is `download`:
function callback(status, content)
......
......@@ -78,6 +78,32 @@ function test_run_command()
end
end
-- This just tests one call using run_util. It is just function wrapper around
-- run_command so no extensive testing is required.
function test_run_util()
local called = 0
local tempfile
local id = run_util(function (ecode, killed, stdout, stderr)
assert_equal(0, ecode)
assert_equal("TERMINATED", killed)
tempfile = stdout
assert_equal('', stderr)
called = called + 1
end, nil, nil, 1000, 5000, "mktemp")
events_wait(id)
assert_equal(1, called)
called = 0
local id = run_util(function (ecode, killed, stdout, stderr)
assert_equal(0, ecode)
assert_equal("TERMINATED", killed)
assert_equal('', stdout)
assert_equal('', stderr)
called = called + 1
end, nil, nil, 1000, 5000, "rm", "-f", tempfile)
events_wait(id)
assert_equal(1, called)
end
function test_download()
local cert = (os.getenv("S") or ".") .. "/tests/data/updater.pem"
local called1 = 0
......
......@@ -8,7 +8,7 @@ globals = {
-- lua51 doesn't contains?
"_M",
-- From interpreter.c
"log", "state_log_enabled", "state_dump", "run_command", "download", "events_wait", "mkdtemp", "chdir", "getcwd", "mkdir", "move", "ls", "stat", "lstat", "sync", "setenv", "md5", "sha256", "reexec", "uri_internal_get", "system_reboot", "get_updater_version",
"log", "state_log_enabled", "state_dump", "run_command", "run_util", "download", "events_wait", "mkdtemp", "chdir", "getcwd", "mkdir", "move", "ls", "stat", "lstat", "sync", "setenv", "md5", "sha256", "reexec", "uri_internal_get", "system_reboot", "get_updater_version",
-- From logging
"ERROR", "WARN", "INFO", "DBG", "DIE", "log_event", "c_pcall_error_handler",
-- From dumper
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment