diff options
author | Lennart Poettering <lennart@poettering.net> | 2016-11-23 22:21:40 +0100 |
---|---|---|
committer | Lennart Poettering <lennart@poettering.net> | 2016-12-14 00:54:10 +0100 |
commit | d2d6c096f6373a76f3b303a7a116e7cfe7139c4d (patch) | |
tree | 090a728bbf4f98d5758806f6c21f958a8d9e982c /src/core | |
parent | 8fceda937f3a177d9e27b403fb5e1b34138b05f5 (diff) |
core: add ability to define arbitrary bind mounts for services
This adds two new settings BindPaths= and BindReadOnlyPaths=. They allow
defining arbitrary bind mounts specific to particular services. This is
particularly useful for services with RootDirectory= set as this permits making
specific bits of the host directory available to chrooted services.
The two new settings follow the concepts nspawn already possess in --bind= and
--bind-ro=, as well as the .nspawn settings Bind= and BindReadOnly= (and these
latter options should probably be renamed to BindPaths= and BindReadOnlyPaths=
too).
Fixes: #3439
Diffstat (limited to 'src/core')
-rw-r--r-- | src/core/dbus-execute.c | 115 | ||||
-rw-r--r-- | src/core/execute.c | 16 | ||||
-rw-r--r-- | src/core/execute.h | 2 | ||||
-rw-r--r-- | src/core/load-fragment-gperf.gperf.m4 | 2 | ||||
-rw-r--r-- | src/core/load-fragment.c | 127 | ||||
-rw-r--r-- | src/core/load-fragment.h | 1 | ||||
-rw-r--r-- | src/core/namespace.c | 127 | ||||
-rw-r--r-- | src/core/namespace.h | 44 |
8 files changed, 407 insertions, 27 deletions
diff --git a/src/core/dbus-execute.c b/src/core/dbus-execute.c index 78b177e107..b3fc0ff5c3 100644 --- a/src/core/dbus-execute.c +++ b/src/core/dbus-execute.c @@ -675,6 +675,49 @@ static int property_get_output_fdname( return sd_bus_message_append(reply, "s", name); } +static int property_get_bind_paths( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + + ExecContext *c = userdata; + unsigned i; + bool ro; + int r; + + assert(bus); + assert(c); + assert(property); + assert(reply); + + ro = !!strstr(property, "ReadOnly"); + + r = sd_bus_message_open_container(reply, 'a', "(ssbt)"); + if (r < 0) + return r; + + for (i = 0; i < c->n_bind_mounts; i++) { + + if (ro != c->bind_mounts[i].read_only) + continue; + + r = sd_bus_message_append( + reply, "(ssbt)", + c->bind_mounts[i].source, + c->bind_mounts[i].destination, + c->bind_mounts[i].ignore_enoent, + c->bind_mounts[i].recursive ? MS_REC : 0); + if (r < 0) + return r; + } + + return sd_bus_message_close_container(reply); +} + const sd_bus_vtable bus_exec_vtable[] = { SD_BUS_VTABLE_START(0), SD_BUS_PROPERTY("Environment", "as", NULL, offsetof(ExecContext, environment), SD_BUS_VTABLE_PROPERTY_CONST), @@ -783,6 +826,8 @@ const sd_bus_vtable bus_exec_vtable[] = { SD_BUS_PROPERTY("MemoryDenyWriteExecute", "b", bus_property_get_bool, offsetof(ExecContext, memory_deny_write_execute), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("RestrictRealtime", "b", bus_property_get_bool, offsetof(ExecContext, restrict_realtime), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("RestrictNamespaces", "t", bus_property_get_ulong, offsetof(ExecContext, restrict_namespaces), SD_BUS_VTABLE_PROPERTY_CONST), + SD_BUS_PROPERTY("BindPaths", "a(ssbt)", property_get_bind_paths, 0, SD_BUS_VTABLE_PROPERTY_CONST), + SD_BUS_PROPERTY("BindReadOnlyPaths", "a(ssbt)", property_get_bind_paths, 0, SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_VTABLE_END }; @@ -1364,8 +1409,8 @@ int bus_exec_context_set_transient_property( if (r < 0) return r; - if (!isempty(path) && !path_is_absolute(path)) - return sd_bus_error_set_errnof(error, EINVAL, "Path %s is not absolute.", path); + if (!path_is_absolute(path)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Path %s is not absolute.", path); if (mode != UNIT_CHECK) { char *buf = NULL; @@ -1630,7 +1675,73 @@ int bus_exec_context_set_transient_property( } return 1; + } else if (STR_IN_SET(name, "BindPaths", "BindReadOnlyPaths")) { + unsigned empty = true; + + r = sd_bus_message_enter_container(message, 'a', "(ssbt)"); + if (r < 0) + return r; + + while ((r = sd_bus_message_enter_container(message, 'r', "ssbt")) > 0) { + const char *source, *destination; + int ignore_enoent; + uint64_t mount_flags; + + r = sd_bus_message_read(message, "ssbt", &source, &destination, &ignore_enoent, &mount_flags); + if (r < 0) + return r; + + r = sd_bus_message_exit_container(message); + if (r < 0) + return r; + + if (!path_is_absolute(source)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Source path %s is not absolute.", source); + if (!path_is_absolute(destination)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Destination path %s is not absolute.", source); + if (!IN_SET(mount_flags, 0, MS_REC)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Unknown mount flags."); + + if (mode != UNIT_CHECK) { + r = bind_mount_add(&c->bind_mounts, &c->n_bind_mounts, + &(BindMount) { + .source = strdup(source), + .destination = strdup(destination), + .read_only = !!strstr(name, "ReadOnly"), + .recursive = !!(mount_flags & MS_REC), + .ignore_enoent = ignore_enoent, + }); + if (r < 0) + return r; + + unit_write_drop_in_private_format( + u, mode, name, + "%s=%s%s:%s:%s", + name, + ignore_enoent ? "-" : "", + source, + destination, + (mount_flags & MS_REC) ? "rbind" : "norbind"); + } + + empty = false; + } + if (r < 0) + return r; + + r = sd_bus_message_exit_container(message); + if (r < 0) + return r; + + if (empty) { + bind_mount_free_many(c->bind_mounts, c->n_bind_mounts); + c->bind_mounts = NULL; + c->n_bind_mounts = 0; + } + + return 1; } + ri = rlimit_from_string(name); if (ri < 0) { soft = endswith(name, "Soft"); diff --git a/src/core/execute.c b/src/core/execute.c index 07ab067c05..2ee8c9a416 100644 --- a/src/core/execute.c +++ b/src/core/execute.c @@ -1826,6 +1826,9 @@ static bool exec_needs_mount_namespace( !strv_isempty(context->inaccessible_paths)) return true; + if (context->n_bind_mounts > 0) + return true; + if (context->mount_flags != 0) return true; @@ -2147,6 +2150,8 @@ static int apply_mount_namespace(Unit *u, const ExecContext *context, r = setup_namespace(root_dir, &ns_info, rw, context->read_only_paths, context->inaccessible_paths, + context->bind_mounts, + context->n_bind_mounts, tmp, var, context->protect_home, @@ -3086,6 +3091,8 @@ void exec_context_done(ExecContext *c) { c->read_write_paths = strv_free(c->read_write_paths); c->inaccessible_paths = strv_free(c->inaccessible_paths); + bind_mount_free_many(c->bind_mounts, c->n_bind_mounts); + if (c->cpuset) CPU_FREE(c->cpuset); @@ -3569,6 +3576,15 @@ void exec_context_dump(ExecContext *c, FILE* f, const char *prefix) { fputs("\n", f); } + if (c->n_bind_mounts > 0) + for (i = 0; i < c->n_bind_mounts; i++) { + fprintf(f, "%s%s: %s:%s:%s\n", prefix, + c->bind_mounts[i].read_only ? "BindReadOnlyPaths" : "BindPaths", + c->bind_mounts[i].source, + c->bind_mounts[i].destination, + c->bind_mounts[i].recursive ? "rbind" : "norbind"); + } + if (c->utmp_id) fprintf(f, "%sUtmpIdentifier: %s\n", diff --git a/src/core/execute.h b/src/core/execute.h index 951c8f4da3..84ab4339cf 100644 --- a/src/core/execute.h +++ b/src/core/execute.h @@ -161,6 +161,8 @@ struct ExecContext { char **read_write_paths, **read_only_paths, **inaccessible_paths; unsigned long mount_flags; + BindMount *bind_mounts; + unsigned n_bind_mounts; uint64_t capability_bounding_set; uint64_t capability_ambient_set; diff --git a/src/core/load-fragment-gperf.gperf.m4 b/src/core/load-fragment-gperf.gperf.m4 index 2610442b91..15f22a2681 100644 --- a/src/core/load-fragment-gperf.gperf.m4 +++ b/src/core/load-fragment-gperf.gperf.m4 @@ -89,6 +89,8 @@ $1.InaccessibleDirectories, config_parse_namespace_path_strv, 0, $1.ReadWritePaths, config_parse_namespace_path_strv, 0, offsetof($1, exec_context.read_write_paths) $1.ReadOnlyPaths, config_parse_namespace_path_strv, 0, offsetof($1, exec_context.read_only_paths) $1.InaccessiblePaths, config_parse_namespace_path_strv, 0, offsetof($1, exec_context.inaccessible_paths) +$1.BindPaths, config_parse_bind_paths, 0, offsetof($1, exec_context) +$1.BindReadOnlyPaths, config_parse_bind_paths, 0, offsetof($1, exec_context) $1.PrivateTmp, config_parse_bool, 0, offsetof($1, exec_context.private_tmp) $1.PrivateDevices, config_parse_bool, 0, offsetof($1, exec_context.private_devices) $1.ProtectKernelTunables, config_parse_bool, 0, offsetof($1, exec_context.protect_kernel_tunables) diff --git a/src/core/load-fragment.c b/src/core/load-fragment.c index a2e7097de0..f325d853c6 100644 --- a/src/core/load-fragment.c +++ b/src/core/load-fragment.c @@ -3891,6 +3891,132 @@ int config_parse_namespace_path_strv( return 0; } +int config_parse_bind_paths( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + ExecContext *c = data; + const char *p; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + assert(data); + + if (isempty(rvalue)) { + /* Empty assignment resets the list */ + bind_mount_free_many(c->bind_mounts, c->n_bind_mounts); + c->bind_mounts = NULL; + c->n_bind_mounts = 0; + return 0; + } + + p = rvalue; + for (;;) { + _cleanup_free_ char *source = NULL, *destination = NULL; + char *s = NULL, *d = NULL; + bool rbind = true, ignore_enoent = false; + + r = extract_first_word(&p, &source, ":" WHITESPACE, EXTRACT_QUOTES|EXTRACT_DONT_COALESCE_SEPARATORS); + if (r == 0) + break; + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_ERR, filename, line, r, "Failed to parse %s: %s", lvalue, rvalue); + return 0; + } + + s = source; + if (s[0] == '-') { + ignore_enoent = true; + s++; + } + + if (!utf8_is_valid(s)) { + log_syntax_invalid_utf8(unit, LOG_ERR, filename, line, s); + return 0; + } + if (!path_is_absolute(s)) { + log_syntax(unit, LOG_ERR, filename, line, 0, "Not an absolute source path, ignoring: %s", s); + return 0; + } + + path_kill_slashes(s); + + /* Optionally, the destination is specified. */ + if (p && p[-1] == ':') { + r = extract_first_word(&p, &destination, ":" WHITESPACE, EXTRACT_QUOTES|EXTRACT_DONT_COALESCE_SEPARATORS); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_ERR, filename, line, r, "Failed to parse %s: %s", lvalue, rvalue); + return 0; + } + if (r == 0) { + log_syntax(unit, LOG_ERR, filename, line, 0, "Missing argument after ':': %s", rvalue); + return 0; + } + + if (!utf8_is_valid(destination)) { + log_syntax_invalid_utf8(unit, LOG_ERR, filename, line, destination); + return 0; + } + if (!path_is_absolute(destination)) { + log_syntax(unit, LOG_ERR, filename, line, 0, "Not an absolute destination path, ignoring: %s", destination); + return 0; + } + + d = path_kill_slashes(destination); + + /* Optionally, there's also a short option string specified */ + if (p && p[-1] == ':') { + _cleanup_free_ char *options = NULL; + + r = extract_first_word(&p, &options, NULL, EXTRACT_QUOTES); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_ERR, filename, line, r, "Failed to parse %s: %s", lvalue, rvalue); + return 0; + } + + if (isempty(options) || streq(options, "rbind")) + rbind = true; + else if (streq(options, "norbind")) + rbind = false; + else { + log_syntax(unit, LOG_ERR, filename, line, 0, "Invalid option string, ignoring setting: %s", options); + return 0; + } + } + } else + d = s; + + r = bind_mount_add(&c->bind_mounts, &c->n_bind_mounts, + &(BindMount) { + .source = s, + .destination = d, + .read_only = !!strstr(lvalue, "ReadOnly"), + .recursive = rbind, + .ignore_enoent = ignore_enoent, + }); + if (r < 0) + return log_oom(); + } + + return 0; +} + int config_parse_no_new_privileges( const char* unit, const char *filename, @@ -4388,6 +4514,7 @@ void unit_dump_config_items(FILE *f) { { config_parse_sec, "SECONDS" }, { config_parse_nsec, "NANOSECONDS" }, { config_parse_namespace_path_strv, "PATH [...]" }, + { config_parse_bind_paths, "PATH[:PATH[:OPTIONS]] [...]" }, { config_parse_unit_requires_mounts_for, "PATH [...]" }, { config_parse_exec_mount_flags, "MOUNTFLAG [...]" }, { config_parse_unit_string_printf, "STRING" }, diff --git a/src/core/load-fragment.h b/src/core/load-fragment.h index 1cff815a50..bbac2d84b5 100644 --- a/src/core/load-fragment.h +++ b/src/core/load-fragment.h @@ -117,6 +117,7 @@ int config_parse_sec_fix_0(const char *unit, const char *filename, unsigned line int config_parse_user_group(const char *unit, const char *filename, unsigned line, const char *section, unsigned section_line, const char *lvalue, int ltype, const char *rvalue, void *data, void *userdata); int config_parse_user_group_strv(const char *unit, const char *filename, unsigned line, const char *section, unsigned section_line, const char *lvalue, int ltype, const char *rvalue, void *data, void *userdata); int config_parse_restrict_namespaces(const char *unit, const char *filename, unsigned line, const char *section, unsigned section_line, const char *lvalue, int ltype, const char *rvalue, void *data, void *userdata); +int config_parse_bind_paths(const char *unit, const char *filename, unsigned line, const char *section, unsigned section_line, const char *lvalue, int ltype, const char *rvalue, void *data, void *userdata); /* gperf prototypes */ const struct ConfigPerfItem* load_fragment_gperf_lookup(const char *key, unsigned length); diff --git a/src/core/namespace.c b/src/core/namespace.c index 746ebc45b0..834883267c 100644 --- a/src/core/namespace.c +++ b/src/core/namespace.c @@ -50,6 +50,8 @@ typedef enum MountMode { /* This is ordered by priority! */ INACCESSIBLE, + BIND_MOUNT, + BIND_MOUNT_RECURSIVE, READONLY, PRIVATE_TMP, PRIVATE_VAR_TMP, @@ -64,6 +66,8 @@ typedef struct MountEntry { bool has_prefix:1; /* Already is prefixed by the root dir? */ bool read_only:1; /* Shall this mount point be read-only? */ char *path_malloc; /* Use this instead of 'path' if we had to allocate memory */ + const char *source_const; /* The source path, for bind mounts */ + char *source_malloc; } MountEntry; /* @@ -166,6 +170,12 @@ static bool mount_entry_read_only(const MountEntry *p) { return p->read_only || IN_SET(p->mode, READONLY, INACCESSIBLE); } +static const char *mount_entry_source(const MountEntry *p) { + assert(p); + + return p->source_malloc ?: p->source_const; +} + static int append_access_mounts(MountEntry **p, char **strv, MountMode mode) { char **i; @@ -201,6 +211,25 @@ static int append_access_mounts(MountEntry **p, char **strv, MountMode mode) { return 0; } +static int append_bind_mounts(MountEntry **p, const BindMount *binds, unsigned n) { + unsigned i; + + assert(p); + + for (i = 0; i < n; i++) { + const BindMount *b = binds + i; + + *((*p)++) = (MountEntry) { + .path_const = b->destination, + .mode = b->recursive ? BIND_MOUNT_RECURSIVE : BIND_MOUNT, + .read_only = b->read_only, + .source_const = b->source, + }; + } + + return 0; +} + static int append_static_mounts(MountEntry **p, const MountEntry *mounts, unsigned n, bool ignore_protect) { unsigned i; @@ -568,27 +597,33 @@ fail: return r; } -static int mount_entry_chase(MountEntry *m, const char *root_directory) { +static int mount_entry_chase( + const char *root_directory, + MountEntry *m, + const char *path, + char **location) { + char *chased; int r; assert(m); /* Since mount() will always follow symlinks and we need to take the different root directory into account we - * chase the symlinks on our own first. */ + * chase the symlinks on our own first. This is called for the destination path, as well as the source path (if + * that applies). The result is stored in "location". */ - r = chase_symlinks(mount_entry_path(m), root_directory, 0, &chased); + r = chase_symlinks(path, root_directory, 0, &chased); if (r == -ENOENT && m->ignore) { - log_debug_errno(r, "Path %s does not exist, ignoring.", mount_entry_path(m)); + log_debug_errno(r, "Path %s does not exist, ignoring.", path); return 0; } if (r < 0) - return log_debug_errno(r, "Failed to follow symlinks on %s: %m", mount_entry_path(m)); + return log_debug_errno(r, "Failed to follow symlinks on %s: %m", path); - log_debug("Followed symlinks %s → %s.", mount_entry_path(m), chased); + log_debug("Followed symlinks %s → %s.", path, chased); - free(m->path_malloc); - m->path_malloc = chased; + free(*location); + *location = chased; return 1; } @@ -600,11 +635,12 @@ static int apply_mount( const char *var_tmp_dir) { const char *what; + bool rbind = true; int r; assert(m); - r = mount_entry_chase(m, root_directory); + r = mount_entry_chase(root_directory, m, mount_entry_path(m), &m->path_malloc); if (r <= 0) return r; @@ -633,7 +669,6 @@ static int apply_mount( case READONLY: case READWRITE: - r = path_is_mount_point(mount_entry_path(m), root_directory, 0); if (r < 0) return log_debug_errno(r, "Failed to determine whether %s is already a mount point: %m", mount_entry_path(m)); @@ -643,6 +678,19 @@ static int apply_mount( what = mount_entry_path(m); break; + case BIND_MOUNT: + rbind = false; + /* fallthrough */ + + case BIND_MOUNT_RECURSIVE: + /* Also chase the source mount */ + r = mount_entry_chase(root_directory, m, mount_entry_source(m), &m->source_malloc); + if (r <= 0) + return r; + + what = mount_entry_source(m); + break; + case PRIVATE_TMP: what = tmp_dir; break; @@ -660,7 +708,7 @@ static int apply_mount( assert(what); - if (mount(what, mount_entry_path(m), NULL, MS_BIND|MS_REC, NULL) < 0) + if (mount(what, mount_entry_path(m), NULL, MS_BIND|(rbind ? MS_REC : 0), NULL) < 0) return log_debug_errno(errno, "Failed to mount %s to %s: %m", what, mount_entry_path(m)); log_debug("Successfully mounted %s to %s", what, mount_entry_path(m)); @@ -695,6 +743,8 @@ static unsigned namespace_calculate_mounts( char** read_write_paths, char** read_only_paths, char** inaccessible_paths, + const BindMount *bind_mounts, + unsigned n_bind_mounts, const char* tmp_dir, const char* var_tmp_dir, ProtectHome protect_home, @@ -719,6 +769,7 @@ static unsigned namespace_calculate_mounts( strv_length(read_write_paths) + strv_length(read_only_paths) + strv_length(inaccessible_paths) + + n_bind_mounts + ns_info->private_dev + (ns_info->protect_kernel_tunables ? ELEMENTSOF(protect_kernel_tunables_table) : 0) + (ns_info->protect_control_groups ? 1 : 0) + @@ -732,6 +783,8 @@ int setup_namespace( char** read_write_paths, char** read_only_paths, char** inaccessible_paths, + const BindMount *bind_mounts, + unsigned n_bind_mounts, const char* tmp_dir, const char* var_tmp_dir, ProtectHome protect_home, @@ -751,6 +804,7 @@ int setup_namespace( read_write_paths, read_only_paths, inaccessible_paths, + bind_mounts, n_bind_mounts, tmp_dir, var_tmp_dir, protect_home, protect_system); @@ -772,6 +826,10 @@ int setup_namespace( if (r < 0) goto finish; + r = append_bind_mounts(&m, bind_mounts, n_bind_mounts); + if (r < 0) + goto finish; + if (tmp_dir) { *(m++) = (MountEntry) { .path_const = "/tmp", @@ -911,6 +969,53 @@ finish: return r; } +void bind_mount_free_many(BindMount *b, unsigned n) { + unsigned i; + + assert(b || n == 0); + + for (i = 0; i < n; i++) { + free(b[i].source); + free(b[i].destination); + } + + free(b); +} + +int bind_mount_add(BindMount **b, unsigned *n, const BindMount *item) { + _cleanup_free_ char *s = NULL, *d = NULL; + BindMount *c; + + assert(b); + assert(n); + assert(item); + + s = strdup(item->source); + if (!s) + return -ENOMEM; + + d = strdup(item->destination); + if (!d) + return -ENOMEM; + + c = realloc_multiply(*b, sizeof(BindMount), *n + 1); + if (!c) + return -ENOMEM; + + *b = c; + + c[(*n) ++] = (BindMount) { + .source = s, + .destination = d, + .read_only = item->read_only, + .recursive = item->recursive, + .ignore_enoent = item->ignore_enoent, + }; + + s = d = NULL; + return 0; +} + static int setup_one_tmp_dir(const char *id, const char *prefix, char **path) { _cleanup_free_ char *x = NULL; char bid[SD_ID128_STRING_MAX]; diff --git a/src/core/namespace.h b/src/core/namespace.h index 2c278fd457..de3edc419c 100644 --- a/src/core/namespace.h +++ b/src/core/namespace.h @@ -21,6 +21,7 @@ ***/ typedef struct NameSpaceInfo NameSpaceInfo; +typedef struct BindMount BindMount; #include <stdbool.h> @@ -51,20 +52,32 @@ struct NameSpaceInfo { bool protect_kernel_modules:1; }; -int setup_namespace(const char *chroot, - const NameSpaceInfo *ns_info, - char **read_write_paths, - char **read_only_paths, - char **inaccessible_paths, - const char *tmp_dir, - const char *var_tmp_dir, - ProtectHome protect_home, - ProtectSystem protect_system, - unsigned long mount_flags); - -int setup_tmp_dirs(const char *id, - char **tmp_dir, - char **var_tmp_dir); +struct BindMount { + char *source; + char *destination; + bool read_only:1; + bool recursive:1; + bool ignore_enoent:1; +}; + +int setup_namespace( + const char *root_directory, + const NameSpaceInfo *ns_info, + char **read_write_paths, + char **read_only_paths, + char **inaccessible_paths, + const BindMount *bind_mounts, + unsigned n_bind_mounts, + const char *tmp_dir, + const char *var_tmp_dir, + ProtectHome protect_home, + ProtectSystem protect_system, + unsigned long mount_flags); + +int setup_tmp_dirs( + const char *id, + char **tmp_dir, + char **var_tmp_dir); int setup_netns(int netns_storage_socket[2]); @@ -73,3 +86,6 @@ ProtectHome protect_home_from_string(const char *s) _pure_; const char* protect_system_to_string(ProtectSystem p) _const_; ProtectSystem protect_system_from_string(const char *s) _pure_; + +void bind_mount_free_many(BindMount *b, unsigned n); +int bind_mount_add(BindMount **b, unsigned *n, const BindMount *item); |