diff --git a/criu/apparmor.c b/criu/apparmor.c index d5c8b7638..85005ed41 100644 --- a/criu/apparmor.c +++ b/criu/apparmor.c @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -45,7 +46,7 @@ static AaNamespace *new_namespace(char *name, AaNamespace *parent) void *m; AaNamespace *ret; - ret = xmalloc(sizeof(*ret)); + ret = xzalloc(sizeof(*ret)); if (!ret) return NULL; aa_namespace__init(ret); @@ -350,6 +351,267 @@ err: return -1; } +/* An AA profile that allows everything that the parasite needs to do */ +#define PARASITE_PROFILE \ + ("profile %s {\n" \ + " /** rwmlkix,\n" \ + " unix,\n" \ + " capability,\n" \ + " signal,\n" \ + "}\n") + +char policydir[PATH_MAX] = ".criu.temp-aa-policy.XXXXXX"; + +static void *get_suspend_policy(char *name, off_t *len) +{ + char policy[1024], file[PATH_MAX], cache[PATH_MAX], clean_name[PATH_MAX]; + void *ret = NULL; + int n, fd, policy_len, i; + struct stat sb; + char *cmd[] = { + "apparmor_parser", "-QWL", cache, file, NULL, + }; + + *len = 0; + + policy_len = snprintf(policy, sizeof(policy), PARASITE_PROFILE, name); + if (policy_len < 0 || policy_len >= sizeof(policy)) { + pr_err("policy name %s too long\n", name); + return NULL; + } + + /* policy names can have /s, but file paths can't */ + for (i = 0; name[i]; i++) { + if (i == PATH_MAX) { + pr_err("name %s too long\n", name); + return NULL; + } + + clean_name[i] = name[i] == '/' ? '.' : name[i]; + } + clean_name[i] = 0; + + n = snprintf(file, sizeof(file), "%s/%s", policydir, clean_name); + if (n < 0 || n >= sizeof(policy)) { + pr_err("policy name %s too long\n", clean_name); + return NULL; + } + + n = snprintf(cache, sizeof(cache), "%s/cache", policydir); + if (n < 0 || n >= sizeof(policy)) { + pr_err("policy dir too long\n"); + return NULL; + } + + fd = open(file, O_CREAT | O_WRONLY, 0600); + if (fd < 0) { + pr_perror("couldn't create %s", file); + return NULL; + } + + n = write(fd, policy, policy_len); + close(fd); + if (n < 0 || n != policy_len) { + pr_perror("couldn't write policy for %s", file); + return NULL; + } + + n = cr_system(-1, -1, -1, cmd[0], cmd, 0); + if (n < 0 || !WIFEXITED(n) || WEXITSTATUS(n)) { + pr_err("apparmor parsing failed %d\n", n); + return NULL; + } + + n = snprintf(file, sizeof(file), "%s/cache/%s", policydir, clean_name); + if (n < 0 || n >= sizeof(policy)) { + pr_err("policy name %s too long\n", clean_name); + return NULL; + } + + fd = open(file, O_RDONLY); + if (fd < 0) { + pr_perror("couldn't open %s", file); + return NULL; + } + + if (fstat(fd, &sb) < 0) { + pr_perror("couldn't stat fd"); + goto out; + } + + ret = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0); + if (ret == MAP_FAILED) { + pr_perror("mmap of %s failed", file); + goto out; + } + + *len = sb.st_size; +out: + close(fd); + return ret; +} + +#define NEXT_AA_TOKEN(pos) \ + while (*pos) { \ + if (*pos == '/' && *(pos + 1) && *(pos + 1) == '/' && *(pos + 2) && *(pos + 2) == '&') { \ + pos += 3; \ + break; \ + } \ + if (*pos == ':' && *(pos + 1) && *(pos + 1) == '/' && *(pos + 2) && *(pos + 2) == '/') { \ + pos += 3; \ + break; \ + } \ + pos++; \ + } + +static int write_aa_policy(AaNamespace *ns, char *path, int offset, char *rewrite, bool suspend) +{ + int i, my_offset, ret; + char *rewrite_pos = rewrite, namespace[PATH_MAX]; + + if (rewrite && suspend) { + pr_err("requesting aa rewriting and suspension at the same time is not supported\n"); + return -1; + } + + if (!rewrite) { + strncpy(namespace, ns->name, sizeof(namespace) - 1); + } else { + NEXT_AA_TOKEN(rewrite_pos); + + switch (*rewrite_pos) { + case ':': { + char tmp, *end; + + end = strchr(rewrite_pos + 1, ':'); + if (!end) { + pr_err("invalid namespace %s\n", rewrite_pos); + return -1; + } + + tmp = *end; + *end = 0; + strlcpy(namespace, rewrite_pos + 1, sizeof(namespace)); + *end = tmp; + + break; + } + default: + strlcpy(namespace, ns->name, sizeof(namespace)); + for (i = 0; i < ns->n_policies; i++) { + if (strcmp(ns->policies[i]->name, rewrite_pos)) + pr_warn("binary rewriting of apparmor policies not supported right now, not renaming %s to %s\n", + ns->policies[i]->name, rewrite_pos); + } + } + } + + my_offset = snprintf(path + offset, PATH_MAX - offset, "/namespaces/%s", ns->name); + if (my_offset < 0 || my_offset >= PATH_MAX - offset) { + pr_err("snprintf'd too many characters\n"); + return -1; + } + + if (!suspend && mkdir(path, 0755) < 0 && errno != EEXIST) { + pr_perror("failed to create namespace %s", path); + goto fail; + } + + for (i = 0; i < ns->n_namespaces; i++) { + if (write_aa_policy(ns, path, offset + my_offset, rewrite_pos, suspend) < 0) + goto fail; + } + + ret = snprintf(path + offset + my_offset, sizeof(path) - offset - my_offset, "/.replace"); + if (ret < 0 || ret >= sizeof(path) - offset - my_offset) { + pr_err("snprintf failed\n"); + goto fail; + } + + for (i = 0; i < ns->n_policies; i++) { + AaPolicy *p = ns->policies[i]; + void *data = p->blob.data; + int fd, n; + off_t len = p->blob.len; + + fd = open(path, O_WRONLY); + if (fd < 0) { + pr_perror("couldn't open apparmor load file %s", path); + goto fail; + } + + if (suspend) { + pr_info("suspending policy %s\n", p->name); + data = get_suspend_policy(p->name, &len); + if (!data) { + close(fd); + goto fail; + } + } + + n = write(fd, data, len); + close(fd); + if (suspend && munmap(data, len) < 0) { + pr_perror("failed to munmap"); + goto fail; + } + + if (n != len) { + pr_perror("write AA policy %s in %s failed", p->name, namespace); + goto fail; + } + + if (!suspend) + pr_info("wrote aa policy %s: %s %d\n", path, p->name, n); + } + + return 0; + +fail: + if (!suspend) { + path[offset + my_offset] = 0; + rmdir(path); + } + + pr_err("failed to write policy in AA namespace %s\n", namespace); + return -1; +} + +static int do_suspend(bool suspend) +{ + int i; + + for (i = 0; i < n_namespaces; i++) { + AaNamespace *ns = namespaces[i]; + char path[PATH_MAX] = AA_SECURITYFS_PATH "/policy"; + + if (write_aa_policy(ns, path, strlen(path), opts.lsm_profile, false) < 0) + return -1; + } + + return 0; +} + +int suspend_aa(void) +{ + int ret; + if (!mkdtemp(policydir)) { + pr_perror("failed to make AA policy dir"); + return -1; + } + + ret = do_suspend(true); + if (rm_rf(policydir) < 0) + pr_err("failed removing policy dir %s\n", policydir); + + return ret; +} + +int unsuspend_aa(void) +{ + return do_suspend(false); +} + int dump_aa_namespaces(void) { ApparmorEntry *ae = NULL; @@ -411,97 +673,6 @@ bool check_aa_ns_dumping(void) return major >= 1 && minor >= 2; } -static int restore_aa_namespace(AaNamespace *ns, char *path, int offset) -{ - pid_t pid; - int status; - - pid = fork(); - if (pid < 0) { - pr_perror("fork failed"); - return -1; - } - - if (!pid) { - int i, my_offset, ret, fd; - char buf[1024]; - - ret = snprintf(buf, sizeof(buf), "changeprofile :%s:", ns->name); - if (ret < 0 || ret >= sizeof(buf)) { - pr_err("profile %s too big\n", ns->name); - exit(1); - } - - my_offset = snprintf(path + offset, PATH_MAX - offset, "/namespaces/%s", ns->name); - if (my_offset < 0 || my_offset >= PATH_MAX - offset) { - pr_err("snprintf'd too many characters\n"); - exit(1); - } - - if (mkdir(path, 0755) < 0) { - if (errno == EEXIST) { - pr_warn("apparmor namespace %s already exists, restoring into it\n", path); - } else { - pr_perror("failed to create namespace %s", path); - exit(1); - } - } - - fd = open_proc_rw(PROC_SELF, "attr/current"); - if (fd < 0) { - pr_perror("couldn't open attr/current"); - goto fail; - } - - errno = 0; - ret = write(fd, buf, strlen(buf)); - close(fd); - if (ret != strlen(buf)) { - pr_perror("failed to change aa namespace %s", buf); - goto fail; - } - - for (i = 0; i < ns->n_namespaces; i++) { - if (restore_aa_namespace(ns, path, offset + my_offset) < 0) - goto fail; - } - - for (i = 0; i < ns->n_policies; i++) { - int fd, n; - AaPolicy *p = ns->policies[i]; - - fd = open(AA_SECURITYFS_PATH "/.replace", O_WRONLY); - if (fd < 0) { - pr_perror("couldn't open apparmor load file"); - goto fail; - } - - n = write(fd, p->blob.data, p->blob.len); - close(fd); - if (n != p->blob.len) { - pr_perror("write AA policy failed"); - goto fail; - } - } - - exit(0); - fail: - rmdir(path); - exit(1); - } - - if (waitpid(pid, &status, 0) < 0) { - pr_perror("waitpid failed"); - return -1; - } - - if (WIFEXITED(status) && WEXITSTATUS(status) == 0) - return 0; - - pr_err("failed to restore aa namespace, worker exited: %d\n", status); - return -1; -} - int prepare_apparmor_namespaces(void) { struct cr_img *img; @@ -528,7 +699,7 @@ int prepare_apparmor_namespaces(void) for (i = 0; i < ae->n_namespaces; i++) { char path[PATH_MAX] = AA_SECURITYFS_PATH "/policy"; - if (restore_aa_namespace(ae->namespaces[i], path, strlen(path)) < 0) { + if (write_aa_policy(ae->namespaces[i], path, strlen(path), opts.lsm_profile, false) < 0) { ret = -1; goto out; } @@ -539,3 +710,74 @@ out: apparmor_entry__free_unpacked(ae, NULL); return ret; } + +int render_aa_profile(char **out, const char *cur) +{ + const char *pos; + int n_namespaces = 0, n_profiles = 0; + bool last_namespace = false; + + /* no rewriting necessary */ + if (!opts.lsm_supplied) { + *out = xsprintf("changeprofile %s", cur); + if (!*out) + return -1; + + return 0; + } + + /* user asked to re-write to an unconfined profile */ + if (!opts.lsm_profile) { + *out = NULL; + return 0; + } + + pos = opts.lsm_profile; + while (*pos) { + switch (*pos) { + case ':': + n_namespaces++; + break; + default: + n_profiles++; + } + + NEXT_AA_TOKEN(pos); + } + + /* special case: there is no namespacing or stacking; we can just + * changeprofile to the rewritten string + */ + if (n_profiles == 1 && n_namespaces == 0) { + *out = xsprintf("changeprofile %s", opts.lsm_profile); + if (!*out) + return -1; + + pr_info("rewrote apparmor profile from %s to %s\n", cur, *out); + return 0; + } + + pos = cur; + while (*pos) { + switch (*pos) { + case ':': + n_namespaces--; + last_namespace = true; + break; + default: + n_profiles--; + } + + NEXT_AA_TOKEN(pos); + + if (n_profiles == 0 && n_namespaces == 0) + break; + } + + *out = xsprintf("changeprofile %s//%s%s", opts.lsm_profile, last_namespace ? "" : "&", pos); + if (!*out) + return -1; + + pr_info("rewrote apparmor profile from %s to %s\n", cur, *out); + return 0; +} diff --git a/criu/cr-check.c b/criu/cr-check.c index 6583b6ea0..8fab51ca4 100644 --- a/criu/cr-check.c +++ b/criu/cr-check.c @@ -45,12 +45,16 @@ #include "tun.h" #include "namespaces.h" #include "pstree.h" +#include "lsm.h" +#include "apparmor.h" #include "cr_options.h" #include "libnetlink.h" #include "net.h" #include "restorer.h" #include "uffd.h" +#include "images/inventory.pb-c.h" + static char *feature_name(int (*func)(void)); static int check_tty(void) @@ -98,6 +102,14 @@ out: return ret; } +static int check_apparmor_stacking(void) +{ + if (!check_aa_ns_dumping()) + return -1; + + return 0; +} + static int check_map_files(void) { int ret; @@ -1466,6 +1478,7 @@ int cr_check(void) ret |= check_newifindex(); ret |= check_pidfd_store(); ret |= check_ns_pid(); + ret |= check_apparmor_stacking(); } /* @@ -1576,6 +1589,7 @@ static struct feature_list feature_list[] = { { "has_ipt_legacy", check_ipt_legacy }, { "pidfd_store", check_pidfd_store }, { "ns_pid", check_ns_pid }, + { "apparmor_stacking", check_apparmor_stacking }, { NULL, NULL }, }; diff --git a/criu/cr-restore.c b/criu/cr-restore.c index dec4e6097..9d2d957f8 100644 --- a/criu/cr-restore.c +++ b/criu/cr-restore.c @@ -3236,8 +3236,6 @@ static struct thread_creds_args *rst_prep_creds_args(CredsEntry *ce, unsigned lo char *rendered = NULL, *profile; profile = ce->lsm_profile; - if (opts.lsm_supplied) - profile = opts.lsm_profile; if (validate_lsm(profile) < 0) return ERR_PTR(-EINVAL); diff --git a/criu/include/lsm.h b/criu/include/lsm.h index 81676e3bf..b4891b4b3 100644 --- a/criu/include/lsm.h +++ b/criu/include/lsm.h @@ -28,7 +28,8 @@ int validate_lsm(char *profile); /* * Render the profile name in the way that the LSM wants it written to - * /proc//attr/current. + * /proc//attr/current, according to whatever is in the images and + * specified by --lsm-profile. */ int render_lsm_profile(char *profile, char **val); diff --git a/criu/lsm.c b/criu/lsm.c index 6f7e6d8d2..f49cc976e 100644 --- a/criu/lsm.c +++ b/criu/lsm.c @@ -318,6 +318,10 @@ int collect_and_suspend_lsm(void) /* now, suspend the LSM; this is where code that implements something * like PTRACE_O_SUSPEND_LSM should live. */ switch (kdat.lsm) { + case LSMTYPE__APPARMOR: + if (suspend_aa() < 0) + return -1; + break; default: pr_warn("don't know how to suspend LSM %d\n", kdat.lsm); } @@ -327,6 +331,9 @@ int collect_and_suspend_lsm(void) int unsuspend_lsm(void) { + if (kdat.lsm == LSMTYPE__APPARMOR && unsuspend_aa()) + return -1; + return 0; } @@ -357,12 +364,7 @@ int render_lsm_profile(char *profile, char **val) switch (kdat.lsm) { case LSMTYPE__APPARMOR: - if (strcmp(profile, "unconfined") != 0 && asprintf(val, "changeprofile %s", profile) < 0) { - pr_err("allocating lsm profile failed\n"); - *val = NULL; - return -1; - } - break; + return render_aa_profile(val, profile); case LSMTYPE__SELINUX: if (asprintf(val, "%s", profile) < 0) { *val = NULL; diff --git a/include/apparmor.h b/include/apparmor.h index bccf928d5..5e707bff4 100644 --- a/include/apparmor.h +++ b/include/apparmor.h @@ -4,8 +4,18 @@ int collect_aa_namespace(char *profile); int dump_aa_namespaces(void); +/* + * This is an operation similar to PTRACE_O_SUSPEND_SECCOMP but for apparmor, + * done entirely from userspace. All the namespaces to be dumped should be + * collected via collect_aa_namespaces() before calling this. + */ +int suspend_aa(void); +int unsuspend_aa(void); + bool check_aa_ns_dumping(void); int prepare_apparmor_namespaces(void); +int render_aa_profile(char **out, const char *cur); + #endif /* __CR_APPARMOR_H__ */