[RFC PATCH] smack: preserve low-integrity labels on copy via whitelist
Tang Peter
Peter_Towne at hotmail.com
Tue Sep 22 11:07:21 UTC 2026
Subject: Re: [RFC PATCH] smack: preserve low-integrity labels on copy via whitelist
Hi Casey,
Sorry for the slow reply.
Thanks for taking another look.
I've put the whole mechanism under a new configuration option,
CONFIG_SECURITY_SMACK_COPYWHITELIST, which defaults to no. With the
option off, the code compiles out entirely and the default Smack
behavior is unchanged -- no task_smack field, no whitelist, no
preserve-whitelist smackfs node, and smack_file_open()/init_security
keep their existing semantics.
The updated patch (amended in place) is below. I'm happy to adjust
the config option name or the Kconfig help text if you'd prefer
different wording.
Regards,
Tang Pengke
From 246625e74cb374bdc10eb3872359b896f118f135 Mon Sep 17 00:00:00 2001
From: Tang Pengke <peter_towne at hotmail.com>
Date: Tue, 25 Aug 2026 10:13:40 +0800
Subject: [PATCH] smack: preserve low-integrity labels across file copy via
whitelist
Files copied from an untrusted channel (USB, network, serial) inherit
the creating process's Smack label, losing the source's label. That
breaks provenance and lets a file from an untrusted source silently
acquire a trusted label.
Record a "preserve" label in task_smack when a process opens a
whitelisted regular file read-only and the read check passes. The next
inode it creates inherits that label instead of the process label,
unless transmutation applies. The record is one-shot: consumed on
creation and cleared on fork and exec, so it never outlives a single
copy operation.
The preservable labels form a whitelist exposed through
/smack/preserve-whitelist. Only untrusted-channel / low-integrity
labels should be added. Recording happens only after a successful
read check, so a process cannot create a file with a label it could
not read (which would be a write-down primitive), and because the
whitelist holds only low labels the mechanism can only ever preserve a
low label, never a system trusted or sensitive one. An empty
whitelist is also the fast path in smack_file_open().
The whole mechanism is guarded by CONFIG_SECURITY_SMACK_COPYWHITELIST,
which defaults to no, so the default Smack behavior is unchanged.
Signed-off-by: Tang Pengke <peter_towne at hotmail.com>
---
security/smack/Kconfig | 21 ++++++++
security/smack/smack.h | 30 +++++++++++
security/smack/smack_access.c | 77 +++++++++++++++++++++++++++
security/smack/smack_lsm.c | 53 +++++++++++++++++++
security/smack/smackfs.c | 99 +++++++++++++++++++++++++++++++++++
5 files changed, 280 insertions(+)
diff --git a/security/smack/Kconfig b/security/smack/Kconfig
index 5a8dfad46..134449fb6 100644
--- a/security/smack/Kconfig
+++ b/security/smack/Kconfig
@@ -53,3 +53,24 @@ config SECURITY_SMACK_APPEND_SIGNALS
to differentiate between delivering a network packet and
delivering a signal in the Smack rules.
If you are unsure how to answer this question, answer N.
+
+config SECURITY_SMACK_COPYWHITELIST
+ bool "Preserve low-integrity labels when copying files"
+ depends on SECURITY_SMACK
+ default n
+ help
+ Enable a whitelist of labels that are preserved when a file is
+ copied. When a process reads a file whose label is on the
+ whitelist and then creates a new file, the new file inherits
+ the source label instead of the process label. This is useful
+ for tracking the provenance of data copied from untrusted
+ sources such as removable media or network mounts, where the
+ filesystem cannot store a per-file label.
+
+ Only low-integrity labels should be placed on the whitelist,
+ so that a process can only ever preserve a low label and never
+ a trusted or sensitive one. The whitelist is managed through
+ the /smack/preserve-whitelist interface.
+
+ If you are unsure how to answer this question, answer N.
+
diff --git a/security/smack/smack.h b/security/smack/smack.h
index 9b9eb262f..bb16c197f 100644
--- a/security/smack/smack.h
+++ b/security/smack/smack.h
@@ -73,6 +73,25 @@ struct smack_known {
struct mutex smk_rules_lock; /* lock for rules */
};
+#ifdef CONFIG_SECURITY_SMACK_COPYWHITELIST
+/*
+ * 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;
+};
+#endif
+
/*
* 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 +140,9 @@ 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 */
+#ifdef CONFIG_SECURITY_SMACK_COPYWHITELIST
+ struct smack_known *smk_preserve; /* label to inherit on next create */
+#endif
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 +334,10 @@ 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);
+#ifdef CONFIG_SECURITY_SMACK_COPYWHITELIST
+bool smk_preserve_allowed(struct smack_known *skp);
+void smk_preserve_set(struct smack_known *skp, bool on);
+#endif
/*
* Shared data.
@@ -326,6 +352,10 @@ extern struct smack_known *smack_unconfined;
#endif
extern int smack_ptrace_rule;
extern struct lsm_blob_sizes smack_blob_sizes;
+#ifdef CONFIG_SECURITY_SMACK_COPYWHITELIST
+extern struct list_head smk_preserve_wl;
+extern struct mutex smk_preserve_wl_lock;
+#endif
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..5bb75e2ec 100644
--- a/security/smack/smack_access.c
+++ b/security/smack/smack_access.c
@@ -53,6 +53,83 @@ static u32 smack_next_secid = 10;
int log_policy = SMACK_AUDIT_DENIED;
#endif /* CONFIG_AUDIT */
+#ifdef CONFIG_SECURITY_SMACK_COPYWHITELIST
+/*
+ * 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);
+}
+#endif /* CONFIG_SECURITY_SMACK_COPYWHITELIST */
+
/**
* 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..e7c3a8473 100644
--- a/security/smack/smack_lsm.c
+++ b/security/smack/smack_lsm.c
@@ -325,6 +325,9 @@ static void init_task_smack(struct task_smack *tsp, struct smack_known *task,
{
tsp->smk_task = task;
tsp->smk_forked = forked;
+#ifdef CONFIG_SECURITY_SMACK_COPYWHITELIST
+ tsp->smk_preserve = NULL;
+#endif
INIT_LIST_HEAD(&tsp->smk_rules);
INIT_LIST_HEAD(&tsp->smk_relabel);
mutex_init(&tsp->smk_rules_lock);
@@ -906,6 +909,10 @@ static int smack_bprm_creds_for_exec(struct linux_binprm *bprm)
struct superblock_smack *sbsp;
int rc;
+#ifdef CONFIG_SECURITY_SMACK_COPYWHITELIST
+ bsp->smk_preserve = NULL;
+#endif
+
isp = smack_inode(inode);
if (isp->smk_task == NULL || isp->smk_task == bsp->smk_task)
return 0;
@@ -1066,6 +1073,19 @@ static int smack_inode_init_security(struct inode *inode, struct inode *dir,
}
}
+#ifdef CONFIG_SECURITY_SMACK_COPYWHITELIST
+ /*
+ * 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;
+ }
+#endif
+
if (rc == 0)
if (xattr_dupval(xattrs, xattr_count,
XATTR_SMACK_SUFFIX,
@@ -2068,6 +2088,39 @@ 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;
+
+#ifdef CONFIG_SECURITY_SMACK_COPYWHITELIST
+ /*
+ * 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;
+ }
+#endif
return rc;
}
diff --git a/security/smack/smackfs.c b/security/smack/smackfs.c
index 6e62dcb36..b1a350365 100644
--- a/security/smack/smackfs.c
+++ b/security/smack/smackfs.c
@@ -62,6 +62,9 @@ enum smk_inos {
SMK_NET6ADDR = 23, /* single label IPv6 hosts */
#endif /* CONFIG_IPV6 */
SMK_RELABEL_SELF = 24, /* relabel possible without CAP_MAC_ADMIN */
+#ifdef CONFIG_SECURITY_SMACK_COPYWHITELIST
+ SMK_PRESERVE_WL = 25, /* preserve whitelist labels */
+#endif
};
/*
@@ -2867,6 +2870,97 @@ static const struct file_operations smk_ptrace_ops = {
.llseek = default_llseek,
};
+#ifdef CONFIG_SECURITY_SMACK_COPYWHITELIST
+/*
+ * 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,
+};
+#endif /* CONFIG_SECURITY_SMACK_COPYWHITELIST */
+
/**
* smk_fill_super - fill the smackfs superblock
* @sb: the empty superblock
@@ -2933,6 +3027,11 @@ 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},
+#ifdef CONFIG_SECURITY_SMACK_COPYWHITELIST
+ [SMK_PRESERVE_WL] = {
+ "preserve-whitelist", &smk_preserve_wl_ops,
+ S_IRUGO|S_IWUSR},
+#endif
/* last one */
{""}
};
--
2.43.0
More information about the Linux-security-module-archive
mailing list