[RFC PATCH] smack: preserve low-integrity labels on copy via whitelist
Casey Schaufler
casey at schaufler-ca.com
Sat Sep 12 15:48:25 UTC 2026
On 8/25/2026 8:33 PM, Tang Peter wrote:
> Subject: [RFC PATCH] smack: preserve low-integrity labels on copy via whitelist
I haven't been ignoring this, sorry for the delay.
I can't say that I like it all that much, but I can see where it
could have value. It's not all that weirder than the relabel-self
behavior. So, if you put it under a configuration option, say
CONFIG_SECURITY_SMACK_COPYWHITELIST which would default to "no"
I will have another look. I'm not promising I'll take it at this
point, but I will look even more closely.
>
> Hi Casey,
>
> I did try SMACK64EXEC, and it works for a dedicated copy tool. But two
> cases on this device fall outside it:
>
> 1. The application itself reads USB content. A Qt app opens files on
> the stick directly (to import / preview) and later writes its own
> files. It isn't a cp_usbarea-style tool, so its output inherits the
> app's process label, not USB_Area — the provenance is lost through
> normal, legitimate use, not through a copy command.
>
> 2. An attacker won't use my cp_usbarea. If untrusted code reads the
> USB file and writes its own file directly, SMACK64EXEC doesn't get
> in the way — the copy tool only constrains the paths that
> voluntarily use it.
>
> So SMACK64EXEC constrains the "compliant" path, but this device also has
> non-compliant paths (the app's own reads, and arbitrary writes). That's
> what pushed me toward a whitelist enforced in the kernel on every read
> rather than only when someone uses the right tool.
>
> Here is the implementation, rebased onto current mainline and
> build-tested against 7.2.0. Compared with the earlier /lib/ path-filter
> version, two things changed:
>
> 1. The gate is a label whitelist, not a path prefix. Only labels added
> to /smack/preserve-whitelist are ever preserved, so the /lib/ and
> /usr/lib/ filter is gone: a shared-library label is simply not on
> the whitelist and can never pollute the slot.
>
> 2. The record is taken only after a passing MAY_READ check, so a
> process that cannot read a file cannot create a file bearing its
> label (which would widen the write-down further).
>
> The mechanism in short:
>
> - smack_file_open(): after a passing read check, a regular file opened
> O_RDONLY whose label is whitelisted records it in
> task_smack::smk_preserve.
>
> - smack_inode_init_security(): if no transmutation applies and a
> preserve label is recorded, use it for the new inode and clear it.
> TRANSMUTE still takes priority.
>
> - One-shot slot: consumed on create, cleared on fork and exec.
>
> - /smack/preserve-whitelist is the single source of truth; an empty
> list is the lock-free fast path in smack_file_open().
>
> I understand none of this removes the write-down primitive — it only
> narrows the target to the whitelisted channel labels. That is a
> deliberate device-side tradeoff for our deployment, where the only
> subject able to read the sensitive label is the trusted admin and the
> operator performing the copies has no read access to it, so the
> cross-sequence laundering you described is not reachable here. I am not
> asking to merge this; I'd value your read on whether the implementation
> has problems I've missed, and on whether there is a way to get the
> "enforced on every read" property without the write-down at all.
>
> The patch is a single commit, 235 insertions over four files, checkpatch
> clean apart from the symbolic-permission warning (kept to match the rest
> of smackfs).
>
> Regards,
> Tang Pengke
>
> ---
> security/smack/smack.h | 22 +++++++++
> security/smack/smack_access.c | 75 ++++++++++++++++++++++++++++
> security/smack/smack_lsm.c | 45 +++++++++++++++++
> security/smack/smackfs.c | 93 +++++++++++++++++++++++++++++++++++
> 4 files changed, 235 insertions(+)
>
> diff --git a/security/smack/smack.h b/security/smack/smack.h
> index 9b9eb262f..0b0a2ba42 100644
> --- a/security/smack/smack.h
> +++ b/security/smack/smack.h
> @@ -73,6 +73,23 @@ struct smack_known {
> struct mutex smk_rules_lock; /* lock for rules */
> };
>
> +/*
> + * An entry in the preserve whitelist.
> + *
> + * The whitelist is the single source of truth for which labels are
> + * preservable. Its emptiness is the fast path in smack_file_open(),
> + * and membership is tested by walking the list.
> + *
> + * Only untrusted-channel / low-integrity labels should be added, so a
> + * process can only ever preserve a low label and never a system trusted
> + * label (which would be a write-down primitive and let shared-library
> + * loads pollute the record).
> + */
> +struct smk_preserve_wl {
> + struct list_head list;
> + struct smack_known *skp;
> +};
> +
> /*
> * Maximum number of bytes for the levels in a CIPSO IP option.
> * Why 23? CIPSO is constrained to 30, so a 32 byte buffer is
> @@ -121,6 +138,7 @@ struct task_smack {
> struct smack_known *smk_task; /* label for access control */
> struct smack_known *smk_forked; /* label when forked */
> struct smack_known *smk_transmuted;/* label when transmuted */
> + struct smack_known *smk_preserve; /* label to inherit on next create */
> struct list_head smk_rules; /* per task access rules */
> struct mutex smk_rules_lock; /* lock for the rules */
> struct list_head smk_relabel; /* transit allowed labels */
> @@ -312,6 +330,8 @@ bool smack_privileged(int cap);
> bool smack_privileged_cred(int cap, const struct cred *cred);
> void smk_destroy_label_list(struct list_head *list);
> int smack_populate_secattr(struct smack_known *skp);
> +bool smk_preserve_allowed(struct smack_known *skp);
> +void smk_preserve_set(struct smack_known *skp, bool on);
>
> /*
> * Shared data.
> @@ -326,6 +346,8 @@ extern struct smack_known *smack_unconfined;
> #endif
> extern int smack_ptrace_rule;
> extern struct lsm_blob_sizes smack_blob_sizes;
> +extern struct list_head smk_preserve_wl;
> +extern struct mutex smk_preserve_wl_lock;
>
> extern struct smack_known smack_known_floor;
> extern struct smack_known smack_known_hat;
> diff --git a/security/smack/smack_access.c b/security/smack/smack_access.c
> index 350b88d58..961d47545 100644
> --- a/security/smack/smack_access.c
> +++ b/security/smack/smack_access.c
> @@ -53,6 +53,81 @@ static u32 smack_next_secid = 10;
> int log_policy = SMACK_AUDIT_DENIED;
> #endif /* CONFIG_AUDIT */
>
> +/*
> + * Preserve whitelist: only labels on this list are preserved on copy.
> + *
> + * The list is the single source of truth for "which labels are
> + * preservable". It also lets smack_file_open() take a plain
> + * list_empty() fast path.
> + *
> + * Only untrusted-channel / low-integrity labels should be added, so
> + * that a process can only ever preserve a low label -- never a system
> + * trusted or sensitive label -- which would otherwise be a write-down
> + * primitive (and would let shared-library loads pollute the record).
> + *
> + * Entries hold raw smack_known pointers. SMACK global labels are never
> + * freed at runtime (smk_destroy_label_list frees list elements, not the
> + * label objects), so these pointers stay valid for the life of the
> + * system.
> + */
> +LIST_HEAD(smk_preserve_wl);
> +DEFINE_MUTEX(smk_preserve_wl_lock);
> +
> +bool smk_preserve_allowed(struct smack_known *skp)
> +{
> + struct smk_preserve_wl *e;
> +
> + mutex_lock(&smk_preserve_wl_lock);
> + list_for_each_entry(e, &smk_preserve_wl, list) {
> + if (e->skp == skp) {
> + mutex_unlock(&smk_preserve_wl_lock);
> + return true;
> + }
> + }
> + mutex_unlock(&smk_preserve_wl_lock);
> + return false;
> +}
> +
> +/*
> + * Add or remove a label from the preserve whitelist.
> + *
> + * The sysfs interface (preserve-whitelist) calls this with on=true for
> + * a plain label, and on=false for a label prefixed with '-'.
> + */
> +void smk_preserve_set(struct smack_known *skp, bool on)
> +{
> + struct smk_preserve_wl *e, *new = NULL;
> +
> + if (on) {
> + new = kzalloc_obj(*new, GFP_KERNEL);
> + if (!new)
> + return;
> + new->skp = skp;
> + }
> +
> + mutex_lock(&smk_preserve_wl_lock);
> + if (on) {
> + list_for_each_entry(e, &smk_preserve_wl, list) {
> + if (e->skp == skp) {
> + kfree(new);
> + new = NULL;
> + break;
> + }
> + }
> + if (new)
> + list_add_tail(&new->list, &smk_preserve_wl);
> + } else {
> + list_for_each_entry(e, &smk_preserve_wl, list) {
> + if (e->skp == skp) {
> + list_del(&e->list);
> + kfree(e);
> + break;
> + }
> + }
> + }
> + mutex_unlock(&smk_preserve_wl_lock);
> +}
> +
> /**
> * smk_access_entry - look up matching access rule
> * @subject_label: a pointer to the subject's Smack label
> diff --git a/security/smack/smack_lsm.c b/security/smack/smack_lsm.c
> index ff115068c..b2a702668 100644
> --- a/security/smack/smack_lsm.c
> +++ b/security/smack/smack_lsm.c
> @@ -325,6 +325,7 @@ static void init_task_smack(struct task_smack *tsp, struct smack_known *task,
> {
> tsp->smk_task = task;
> tsp->smk_forked = forked;
> + tsp->smk_preserve = NULL;
> INIT_LIST_HEAD(&tsp->smk_rules);
> INIT_LIST_HEAD(&tsp->smk_relabel);
> mutex_init(&tsp->smk_rules_lock);
> @@ -906,6 +907,8 @@ static int smack_bprm_creds_for_exec(struct linux_binprm *bprm)
> struct superblock_smack *sbsp;
> int rc;
>
> + bsp->smk_preserve = NULL;
> +
> isp = smack_inode(inode);
> if (isp->smk_task == NULL || isp->smk_task == bsp->smk_task)
> return 0;
> @@ -1066,6 +1069,17 @@ static int smack_inode_init_security(struct inode *inode, struct inode *dir,
> }
> }
>
> + /*
> + * If a whitelisted label was recorded on file open and no
> + * transmutation applies, inherit it so the new file keeps the
> + * source's (low-integrity) label across the copy.
> + */
> + if (!trans_cred && tsp->smk_preserve != NULL &&
> + !(trans_rule && smk_inode_transmutable(dir))) {
> + issp->smk_inode = tsp->smk_preserve;
> + tsp->smk_preserve = NULL;
> + }
> +
> if (rc == 0)
> if (xattr_dupval(xattrs, xattr_count,
> XATTR_SMACK_SUFFIX,
> @@ -2068,6 +2082,37 @@ static int smack_file_open(struct file *file)
> smk_ad_setfield_u_fs_path(&ad, file->f_path);
> rc = smk_tskacc(tsp, smk_of_inode(inode), MAY_READ, &ad);
> rc = smk_bu_credfile(file->f_cred, file, MAY_READ, rc);
> + if (rc)
> + return rc;
> +
> + /*
> + * Record a whitelisted label after a successful read check so the
> + * next file create (smack_inode_init_security) can inherit it.
> + *
> + * The record must happen only after the read check passes;
> + * otherwise a process without read access to the source could
> + * create a file with that label without ever reading it, which
> + * would be a write-down primitive.
> + *
> + * The whitelist must only hold untrusted-channel / low-integrity
> + * labels (peripheral, network, serial, and other external
> + * sources), never system trusted labels (system binaries, shared
> + * libraries, etc.) -- otherwise shared-library loads would pollute
> + * the record. Because of this constraint no library path
> + * filtering is needed here: the whitelist check already excludes
> + * every non-whitelisted file, including shared libraries.
> + *
> + * The whitelist is read-only after configuration, so the
> + * list_empty() fast path needs no locking.
> + */
> + if (!list_empty(&smk_preserve_wl) &&
> + (file->f_flags & O_ACCMODE) == O_RDONLY &&
> + S_ISREG(inode->i_mode)) {
> + struct smack_known *skp = smk_of_inode(inode);
> +
> + if (smk_preserve_allowed(skp))
> + tsp->smk_preserve = skp;
> + }
>
> return rc;
> }
> diff --git a/security/smack/smackfs.c b/security/smack/smackfs.c
> index 6e62dcb36..568c40ffc 100644
> --- a/security/smack/smackfs.c
> +++ b/security/smack/smackfs.c
> @@ -62,6 +62,7 @@ enum smk_inos {
> SMK_NET6ADDR = 23, /* single label IPv6 hosts */
> #endif /* CONFIG_IPV6 */
> SMK_RELABEL_SELF = 24, /* relabel possible without CAP_MAC_ADMIN */
> + SMK_PRESERVE_WL = 25, /* preserve whitelist labels */
> };
>
> /*
> @@ -2867,6 +2868,95 @@ static const struct file_operations smk_ptrace_ops = {
> .llseek = default_llseek,
> };
>
> +/*
> + * Seq_file operations for /smack/preserve-whitelist.
> + * Iterates the whitelist and shows each preservable label.
> + */
> +static void *preserve_wl_seq_start(struct seq_file *s, loff_t *pos)
> +{
> + mutex_lock(&smk_preserve_wl_lock);
> + return seq_list_start(&smk_preserve_wl, *pos);
> +}
> +
> +static void *preserve_wl_seq_next(struct seq_file *s, void *v, loff_t *pos)
> +{
> + return seq_list_next(v, &smk_preserve_wl, pos);
> +}
> +
> +static void preserve_wl_seq_stop(struct seq_file *s, void *v)
> +{
> + mutex_unlock(&smk_preserve_wl_lock);
> +}
> +
> +static int preserve_wl_seq_show(struct seq_file *s, void *v)
> +{
> + struct smk_preserve_wl *e = list_entry(v, struct smk_preserve_wl, list);
> +
> + seq_printf(s, "%s\n", e->skp->smk_known);
> + return 0;
> +}
> +
> +static const struct seq_operations preserve_wl_seq_ops = {
> + .start = preserve_wl_seq_start,
> + .next = preserve_wl_seq_next,
> + .stop = preserve_wl_seq_stop,
> + .show = preserve_wl_seq_show,
> +};
> +
> +static int smk_open_preserve_wl(struct inode *inode, struct file *file)
> +{
> + return seq_open(file, &preserve_wl_seq_ops);
> +}
> +
> +static ssize_t smk_write_preserve_wl(struct file *file, const char __user *buf,
> + size_t count, loff_t *ppos)
> +{
> + struct smack_known *skp;
> + char *data, *label;
> + bool on = true;
> +
> + if (!smack_privileged(CAP_MAC_ADMIN))
> + return -EPERM;
> +
> + data = memdup_user_nul(buf, count);
> + if (IS_ERR(data))
> + return PTR_ERR(data);
> +
> + label = strim(data);
> + if (!label[0]) {
> + kfree(data);
> + return -EINVAL;
> + }
> +
> + /* A leading '-' removes the label (same convention as revoke-subject). */
> + if (label[0] == '-') {
> + on = false;
> + label = strim(label + 1);
> + if (!label[0]) {
> + kfree(data);
> + return -EINVAL;
> + }
> + }
> +
> + skp = smk_find_entry(label);
> + if (!skp) {
> + kfree(data);
> + return -ENOENT;
> + }
> +
> + smk_preserve_set(skp, on);
> + kfree(data);
> + return count;
> +}
> +
> +static const struct file_operations smk_preserve_wl_ops = {
> + .open = smk_open_preserve_wl,
> + .read = seq_read,
> + .write = smk_write_preserve_wl,
> + .llseek = seq_lseek,
> + .release = seq_release,
> +};
> +
> /**
> * smk_fill_super - fill the smackfs superblock
> * @sb: the empty superblock
> @@ -2933,6 +3023,9 @@ static int smk_fill_super(struct super_block *sb, struct fs_context *fc)
> [SMK_RELABEL_SELF] = {
> "relabel-self", &smk_relabel_self_ops,
> S_IRUGO|S_IWUGO},
> + [SMK_PRESERVE_WL] = {
> + "preserve-whitelist", &smk_preserve_wl_ops,
> + S_IRUGO|S_IWUSR},
> /* last one */
> {""}
> };
> --
> 2.43.0
More information about the Linux-security-module-archive
mailing list