Linux kernel mirror (for testing) git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel os linux

fs: create and use seq_show_option for escaping

Many file systems that implement the show_options hook fail to correctly
escape their output which could lead to unescaped characters (e.g. new
lines) leaking into /proc/mounts and /proc/[pid]/mountinfo files. This
could lead to confusion, spoofed entries (resulting in things like
systemd issuing false d-bus "mount" notifications), and who knows what
else. This looks like it would only be the root user stepping on
themselves, but it's possible weird things could happen in containers or
in other situations with delegated mount privileges.

Here's an example using overlay with setuid fusermount trusting the
contents of /proc/mounts (via the /etc/mtab symlink). Imagine the use
of "sudo" is something more sneaky:

$ BASE="ovl"
$ MNT="$BASE/mnt"
$ LOW="$BASE/lower"
$ UP="$BASE/upper"
$ WORK="$BASE/work/ 0 0
none /proc fuse.pwn user_id=1000"
$ mkdir -p "$LOW" "$UP" "$WORK"
$ sudo mount -t overlay -o "lowerdir=$LOW,upperdir=$UP,workdir=$WORK" none /mnt
$ cat /proc/mounts
none /root/ovl/mnt overlay rw,relatime,lowerdir=ovl/lower,upperdir=ovl/upper,workdir=ovl/work/ 0 0
none /proc fuse.pwn user_id=1000 0 0
$ fusermount -u /proc
$ cat /proc/mounts
cat: /proc/mounts: No such file or directory

This fixes the problem by adding new seq_show_option and
seq_show_option_n helpers, and updating the vulnerable show_option
handlers to use them as needed. Some, like SELinux, need to be open
coded due to unusual existing escape mechanisms.

[akpm@linux-foundation.org: add lost chunk, per Kees]
[keescook@chromium.org: seq_show_option should be using const parameters]
Signed-off-by: Kees Cook <keescook@chromium.org>
Acked-by: Serge Hallyn <serge.hallyn@canonical.com>
Acked-by: Jan Kara <jack@suse.com>
Acked-by: Paul Moore <paul@paul-moore.com>
Cc: J. R. Okajima <hooanon05g@gmail.com>
Signed-off-by: Kees Cook <keescook@chromium.org>
Cc: <stable@vger.kernel.org>
Signed-off-by: Andrew Morton <akpm@linux-foundation.org>
Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>

authored by

Kees Cook and committed by
Linus Torvalds
a068acf2 46359295

+71 -30
+1 -1
fs/ceph/super.c
··· 479 479 if (fsopt->max_readdir_bytes != CEPH_MAX_READDIR_BYTES_DEFAULT) 480 480 seq_printf(m, ",readdir_max_bytes=%d", fsopt->max_readdir_bytes); 481 481 if (strcmp(fsopt->snapdir_name, CEPH_SNAPDIRNAME_DEFAULT)) 482 - seq_printf(m, ",snapdirname=%s", fsopt->snapdir_name); 482 + seq_show_option(m, "snapdirname", fsopt->snapdir_name); 483 483 484 484 return 0; 485 485 }
+3 -3
fs/cifs/cifsfs.c
··· 394 394 struct sockaddr *srcaddr; 395 395 srcaddr = (struct sockaddr *)&tcon->ses->server->srcaddr; 396 396 397 - seq_printf(s, ",vers=%s", tcon->ses->server->vals->version_string); 397 + seq_show_option(s, "vers", tcon->ses->server->vals->version_string); 398 398 cifs_show_security(s, tcon->ses); 399 399 cifs_show_cache_flavor(s, cifs_sb); 400 400 401 401 if (cifs_sb->mnt_cifs_flags & CIFS_MOUNT_MULTIUSER) 402 402 seq_puts(s, ",multiuser"); 403 403 else if (tcon->ses->user_name) 404 - seq_printf(s, ",username=%s", tcon->ses->user_name); 404 + seq_show_option(s, "username", tcon->ses->user_name); 405 405 406 406 if (tcon->ses->domainName) 407 - seq_printf(s, ",domain=%s", tcon->ses->domainName); 407 + seq_show_option(s, "domain", tcon->ses->domainName); 408 408 409 409 if (srcaddr->sa_family != AF_UNSPEC) { 410 410 struct sockaddr_in *saddr4;
+2 -2
fs/ext4/super.c
··· 1776 1776 } 1777 1777 1778 1778 if (sbi->s_qf_names[USRQUOTA]) 1779 - seq_printf(seq, ",usrjquota=%s", sbi->s_qf_names[USRQUOTA]); 1779 + seq_show_option(seq, "usrjquota", sbi->s_qf_names[USRQUOTA]); 1780 1780 1781 1781 if (sbi->s_qf_names[GRPQUOTA]) 1782 - seq_printf(seq, ",grpjquota=%s", sbi->s_qf_names[GRPQUOTA]); 1782 + seq_show_option(seq, "grpjquota", sbi->s_qf_names[GRPQUOTA]); 1783 1783 #endif 1784 1784 } 1785 1785
+3 -3
fs/gfs2/super.c
··· 1334 1334 if (is_ancestor(root, sdp->sd_master_dir)) 1335 1335 seq_puts(s, ",meta"); 1336 1336 if (args->ar_lockproto[0]) 1337 - seq_printf(s, ",lockproto=%s", args->ar_lockproto); 1337 + seq_show_option(s, "lockproto", args->ar_lockproto); 1338 1338 if (args->ar_locktable[0]) 1339 - seq_printf(s, ",locktable=%s", args->ar_locktable); 1339 + seq_show_option(s, "locktable", args->ar_locktable); 1340 1340 if (args->ar_hostdata[0]) 1341 - seq_printf(s, ",hostdata=%s", args->ar_hostdata); 1341 + seq_show_option(s, "hostdata", args->ar_hostdata); 1342 1342 if (args->ar_spectator) 1343 1343 seq_puts(s, ",spectator"); 1344 1344 if (args->ar_localflocks)
+2 -2
fs/hfs/super.c
··· 136 136 struct hfs_sb_info *sbi = HFS_SB(root->d_sb); 137 137 138 138 if (sbi->s_creator != cpu_to_be32(0x3f3f3f3f)) 139 - seq_printf(seq, ",creator=%.4s", (char *)&sbi->s_creator); 139 + seq_show_option_n(seq, "creator", (char *)&sbi->s_creator, 4); 140 140 if (sbi->s_type != cpu_to_be32(0x3f3f3f3f)) 141 - seq_printf(seq, ",type=%.4s", (char *)&sbi->s_type); 141 + seq_show_option_n(seq, "type", (char *)&sbi->s_type, 4); 142 142 seq_printf(seq, ",uid=%u,gid=%u", 143 143 from_kuid_munged(&init_user_ns, sbi->s_uid), 144 144 from_kgid_munged(&init_user_ns, sbi->s_gid));
+2 -2
fs/hfsplus/options.c
··· 218 218 struct hfsplus_sb_info *sbi = HFSPLUS_SB(root->d_sb); 219 219 220 220 if (sbi->creator != HFSPLUS_DEF_CR_TYPE) 221 - seq_printf(seq, ",creator=%.4s", (char *)&sbi->creator); 221 + seq_show_option_n(seq, "creator", (char *)&sbi->creator, 4); 222 222 if (sbi->type != HFSPLUS_DEF_CR_TYPE) 223 - seq_printf(seq, ",type=%.4s", (char *)&sbi->type); 223 + seq_show_option_n(seq, "type", (char *)&sbi->type, 4); 224 224 seq_printf(seq, ",umask=%o,uid=%u,gid=%u", sbi->umask, 225 225 from_kuid_munged(&init_user_ns, sbi->uid), 226 226 from_kgid_munged(&init_user_ns, sbi->gid));
+1 -1
fs/hostfs/hostfs_kern.c
··· 260 260 size_t offset = strlen(root_ino) + 1; 261 261 262 262 if (strlen(root_path) > offset) 263 - seq_printf(seq, ",%s", root_path + offset); 263 + seq_show_option(seq, root_path + offset, NULL); 264 264 265 265 if (append) 266 266 seq_puts(seq, ",append");
+2 -2
fs/ocfs2/super.c
··· 1563 1563 seq_printf(s, ",localflocks,"); 1564 1564 1565 1565 if (osb->osb_cluster_stack[0]) 1566 - seq_printf(s, ",cluster_stack=%.*s", OCFS2_STACK_LABEL_LEN, 1567 - osb->osb_cluster_stack); 1566 + seq_show_option_n(s, "cluster_stack", osb->osb_cluster_stack, 1567 + OCFS2_STACK_LABEL_LEN); 1568 1568 if (opts & OCFS2_MOUNT_USRQUOTA) 1569 1569 seq_printf(s, ",usrquota"); 1570 1570 if (opts & OCFS2_MOUNT_GRPQUOTA)
+3 -3
fs/overlayfs/super.c
··· 588 588 struct super_block *sb = dentry->d_sb; 589 589 struct ovl_fs *ufs = sb->s_fs_info; 590 590 591 - seq_printf(m, ",lowerdir=%s", ufs->config.lowerdir); 591 + seq_show_option(m, "lowerdir", ufs->config.lowerdir); 592 592 if (ufs->config.upperdir) { 593 - seq_printf(m, ",upperdir=%s", ufs->config.upperdir); 594 - seq_printf(m, ",workdir=%s", ufs->config.workdir); 593 + seq_show_option(m, "upperdir", ufs->config.upperdir); 594 + seq_show_option(m, "workdir", ufs->config.workdir); 595 595 } 596 596 return 0; 597 597 }
+5 -3
fs/reiserfs/super.c
··· 714 714 seq_puts(seq, ",acl"); 715 715 716 716 if (REISERFS_SB(s)->s_jdev) 717 - seq_printf(seq, ",jdev=%s", REISERFS_SB(s)->s_jdev); 717 + seq_show_option(seq, "jdev", REISERFS_SB(s)->s_jdev); 718 718 719 719 if (journal->j_max_commit_age != journal->j_default_max_commit_age) 720 720 seq_printf(seq, ",commit=%d", journal->j_max_commit_age); 721 721 722 722 #ifdef CONFIG_QUOTA 723 723 if (REISERFS_SB(s)->s_qf_names[USRQUOTA]) 724 - seq_printf(seq, ",usrjquota=%s", REISERFS_SB(s)->s_qf_names[USRQUOTA]); 724 + seq_show_option(seq, "usrjquota", 725 + REISERFS_SB(s)->s_qf_names[USRQUOTA]); 725 726 else if (opts & (1 << REISERFS_USRQUOTA)) 726 727 seq_puts(seq, ",usrquota"); 727 728 if (REISERFS_SB(s)->s_qf_names[GRPQUOTA]) 728 - seq_printf(seq, ",grpjquota=%s", REISERFS_SB(s)->s_qf_names[GRPQUOTA]); 729 + seq_show_option(seq, "grpjquota", 730 + REISERFS_SB(s)->s_qf_names[GRPQUOTA]); 729 731 else if (opts & (1 << REISERFS_GRPQUOTA)) 730 732 seq_puts(seq, ",grpquota"); 731 733 if (REISERFS_SB(s)->s_jquota_fmt) {
+2 -2
fs/xfs/xfs_super.c
··· 511 511 seq_printf(m, "," MNTOPT_LOGBSIZE "=%dk", mp->m_logbsize >> 10); 512 512 513 513 if (mp->m_logname) 514 - seq_printf(m, "," MNTOPT_LOGDEV "=%s", mp->m_logname); 514 + seq_show_option(m, MNTOPT_LOGDEV, mp->m_logname); 515 515 if (mp->m_rtname) 516 - seq_printf(m, "," MNTOPT_RTDEV "=%s", mp->m_rtname); 516 + seq_show_option(m, MNTOPT_RTDEV, mp->m_rtname); 517 517 518 518 if (mp->m_dalign > 0) 519 519 seq_printf(m, "," MNTOPT_SUNIT "=%d",
+35
include/linux/seq_file.h
··· 149 149 #endif 150 150 } 151 151 152 + /** 153 + * seq_show_options - display mount options with appropriate escapes. 154 + * @m: the seq_file handle 155 + * @name: the mount option name 156 + * @value: the mount option name's value, can be NULL 157 + */ 158 + static inline void seq_show_option(struct seq_file *m, const char *name, 159 + const char *value) 160 + { 161 + seq_putc(m, ','); 162 + seq_escape(m, name, ",= \t\n\\"); 163 + if (value) { 164 + seq_putc(m, '='); 165 + seq_escape(m, value, ", \t\n\\"); 166 + } 167 + } 168 + 169 + /** 170 + * seq_show_option_n - display mount options with appropriate escapes 171 + * where @value must be a specific length. 172 + * @m: the seq_file handle 173 + * @name: the mount option name 174 + * @value: the mount option name's value, cannot be NULL 175 + * @length: the length of @value to display 176 + * 177 + * This is a macro since this uses "length" to define the size of the 178 + * stack buffer. 179 + */ 180 + #define seq_show_option_n(m, name, value, length) { \ 181 + char val_buf[length + 1]; \ 182 + strncpy(val_buf, value, length); \ 183 + val_buf[length] = '\0'; \ 184 + seq_show_option(m, name, val_buf); \ 185 + } 186 + 152 187 #define SEQ_START_TOKEN ((void *)1) 153 188 /* 154 189 * Helpers for iteration over list_head-s in seq_files
+4 -3
kernel/cgroup.c
··· 1342 1342 if (root != &cgrp_dfl_root) 1343 1343 for_each_subsys(ss, ssid) 1344 1344 if (root->subsys_mask & (1 << ssid)) 1345 - seq_printf(seq, ",%s", ss->legacy_name); 1345 + seq_show_option(seq, ss->name, NULL); 1346 1346 if (root->flags & CGRP_ROOT_NOPREFIX) 1347 1347 seq_puts(seq, ",noprefix"); 1348 1348 if (root->flags & CGRP_ROOT_XATTR) ··· 1350 1350 1351 1351 spin_lock(&release_agent_path_lock); 1352 1352 if (strlen(root->release_agent_path)) 1353 - seq_printf(seq, ",release_agent=%s", root->release_agent_path); 1353 + seq_show_option(seq, "release_agent", 1354 + root->release_agent_path); 1354 1355 spin_unlock(&release_agent_path_lock); 1355 1356 1356 1357 if (test_bit(CGRP_CPUSET_CLONE_CHILDREN, &root->cgrp.flags)) 1357 1358 seq_puts(seq, ",clone_children"); 1358 1359 if (strlen(root->name)) 1359 - seq_printf(seq, ",name=%s", root->name); 1360 + seq_show_option(seq, "name", root->name); 1360 1361 return 0; 1361 1362 } 1362 1363
+5 -2
net/ceph/ceph_common.c
··· 517 517 struct ceph_options *opt = client->options; 518 518 size_t pos = m->count; 519 519 520 - if (opt->name) 521 - seq_printf(m, "name=%s,", opt->name); 520 + if (opt->name) { 521 + seq_puts(m, "name="); 522 + seq_escape(m, opt->name, ", \t\n\\"); 523 + seq_putc(m, ','); 524 + } 522 525 if (opt->key) 523 526 seq_puts(m, "secret=<hidden>,"); 524 527
+1 -1
security/selinux/hooks.c
··· 1100 1100 seq_puts(m, prefix); 1101 1101 if (has_comma) 1102 1102 seq_putc(m, '\"'); 1103 - seq_puts(m, opts->mnt_opts[i]); 1103 + seq_escape(m, opts->mnt_opts[i], "\"\n\\"); 1104 1104 if (has_comma) 1105 1105 seq_putc(m, '\"'); 1106 1106 }