* [RFC PATCH] smack: preserve low-integrity labels on copy via whitelist
@ 2026-08-26 3:33 Tang Peter
2026-09-12 15:48 ` Casey Schaufler
0 siblings, 1 reply; 3+ messages in thread
From: Tang Peter @ 2026-08-26 3:33 UTC (permalink / raw)
To: Casey Schaufler; +Cc: linux-security-module@vger.kernel.org, gaolong@yhkylin.cn
Subject: [RFC PATCH] smack: preserve low-integrity labels on copy via whitelist
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
^ permalink raw reply related [flat|nested] 3+ messages in thread* Re: [RFC PATCH] smack: preserve low-integrity labels on copy via whitelist
2026-08-26 3:33 [RFC PATCH] smack: preserve low-integrity labels on copy via whitelist Tang Peter
@ 2026-09-12 15:48 ` Casey Schaufler
0 siblings, 0 replies; 3+ messages in thread
From: Casey Schaufler @ 2026-09-12 15:48 UTC (permalink / raw)
To: Tang Peter
Cc: linux-security-module@vger.kernel.org, gaolong@yhkylin.cn,
Casey Schaufler
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
^ permalink raw reply [flat|nested] 3+ messages in thread
* Re: [RFC PATCH] smack: preserve low-integrity labels on copy via whitelist
@ 2026-09-22 11:07 Tang Peter
0 siblings, 0 replies; 3+ messages in thread
From: Tang Peter @ 2026-09-22 11:07 UTC (permalink / raw)
To: casey@schaufler-ca.com
Cc: gaolong@yhkylin.cn, linux-security-module@vger.kernel.org
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@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@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
^ permalink raw reply related [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-09-22 11:07 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-26 3:33 [RFC PATCH] smack: preserve low-integrity labels on copy via whitelist Tang Peter
2026-09-12 15:48 ` Casey Schaufler
-- strict thread matches above, loose matches on Subject: below --
2026-09-22 11:07 Tang Peter
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox