| From a068acf2ee77693e0bf39d6e07139ba704f461c3 Mon Sep 17 00:00:00 2001 |
| From: Kees Cook <keescook@chromium.org> |
| Date: Fri, 4 Sep 2015 15:44:57 -0700 |
| Subject: fs: create and use seq_show_option for escaping |
| |
| commit a068acf2ee77693e0bf39d6e07139ba704f461c3 upstream. |
| |
| 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> |
| Signed-off-by: Andrew Morton <akpm@linux-foundation.org> |
| Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org> |
| [lizf: Backported to 3.4: |
| - adjust context |
| - one more place in ceph needs to be changed |
| - drop changes to overlayfs |
| - drop showing vers in cifs] |
| Signed-off-by: Zefan Li <lizefan@huawei.com> |
| --- |
| fs/ceph/super.c | 8 +++++--- |
| fs/cifs/cifsfs.c | 4 ++-- |
| fs/ext4/super.c | 4 ++-- |
| fs/gfs2/super.c | 6 +++--- |
| fs/hfs/super.c | 4 ++-- |
| fs/hfsplus/options.c | 4 ++-- |
| fs/hostfs/hostfs_kern.c | 2 +- |
| fs/ocfs2/super.c | 4 ++-- |
| fs/reiserfs/super.c | 8 +++++--- |
| fs/xfs/xfs_super.c | 4 ++-- |
| include/linux/seq_file.h | 35 +++++++++++++++++++++++++++++++++++ |
| kernel/cgroup.c | 7 ++++--- |
| security/selinux/hooks.c | 2 +- |
| 13 files changed, 66 insertions(+), 26 deletions(-) |
| |
| --- a/fs/ceph/super.c |
| +++ b/fs/ceph/super.c |
| @@ -383,8 +383,10 @@ static int ceph_show_options(struct seq_ |
| if (opt->flags & CEPH_OPT_NOCRC) |
| seq_puts(m, ",nocrc"); |
| |
| - if (opt->name) |
| - seq_printf(m, ",name=%s", opt->name); |
| + if (opt->name) { |
| + seq_puts(m, ",name="); |
| + seq_escape(m, opt->name, ", \t\n\\"); |
| + } |
| if (opt->key) |
| seq_puts(m, ",secret=<hidden>"); |
| |
| @@ -429,7 +431,7 @@ static int ceph_show_options(struct seq_ |
| if (fsopt->max_readdir_bytes != CEPH_MAX_READDIR_BYTES_DEFAULT) |
| seq_printf(m, ",readdir_max_bytes=%d", fsopt->max_readdir_bytes); |
| if (strcmp(fsopt->snapdir_name, CEPH_SNAPDIRNAME_DEFAULT)) |
| - seq_printf(m, ",snapdirname=%s", fsopt->snapdir_name); |
| + seq_show_option(m, "snapdirname", fsopt->snapdir_name); |
| return 0; |
| } |
| |
| --- a/fs/cifs/cifsfs.c |
| +++ b/fs/cifs/cifsfs.c |
| @@ -373,10 +373,10 @@ cifs_show_options(struct seq_file *s, st |
| if (cifs_sb->mnt_cifs_flags & CIFS_MOUNT_MULTIUSER) |
| seq_printf(s, ",multiuser"); |
| else if (tcon->ses->user_name) |
| - seq_printf(s, ",username=%s", tcon->ses->user_name); |
| + seq_show_option(s, "username", tcon->ses->user_name); |
| |
| if (tcon->ses->domainName) |
| - seq_printf(s, ",domain=%s", tcon->ses->domainName); |
| + seq_show_option(s, "domain", tcon->ses->domainName); |
| |
| if (srcaddr->sa_family != AF_UNSPEC) { |
| struct sockaddr_in *saddr4; |
| --- a/fs/ext4/super.c |
| +++ b/fs/ext4/super.c |
| @@ -1682,10 +1682,10 @@ static inline void ext4_show_quota_optio |
| } |
| |
| if (sbi->s_qf_names[USRQUOTA]) |
| - seq_printf(seq, ",usrjquota=%s", sbi->s_qf_names[USRQUOTA]); |
| + seq_show_option(seq, "usrjquota", sbi->s_qf_names[USRQUOTA]); |
| |
| if (sbi->s_qf_names[GRPQUOTA]) |
| - seq_printf(seq, ",grpjquota=%s", sbi->s_qf_names[GRPQUOTA]); |
| + seq_show_option(seq, "grpjquota", sbi->s_qf_names[GRPQUOTA]); |
| |
| if (test_opt(sb, USRQUOTA)) |
| seq_puts(seq, ",usrquota"); |
| --- a/fs/gfs2/super.c |
| +++ b/fs/gfs2/super.c |
| @@ -1298,11 +1298,11 @@ static int gfs2_show_options(struct seq_ |
| if (is_ancestor(root, sdp->sd_master_dir)) |
| seq_printf(s, ",meta"); |
| if (args->ar_lockproto[0]) |
| - seq_printf(s, ",lockproto=%s", args->ar_lockproto); |
| + seq_show_option(s, "lockproto", args->ar_lockproto); |
| if (args->ar_locktable[0]) |
| - seq_printf(s, ",locktable=%s", args->ar_locktable); |
| + seq_show_option(s, "locktable", args->ar_locktable); |
| if (args->ar_hostdata[0]) |
| - seq_printf(s, ",hostdata=%s", args->ar_hostdata); |
| + seq_show_option(s, "hostdata", args->ar_hostdata); |
| if (args->ar_spectator) |
| seq_printf(s, ",spectator"); |
| if (args->ar_localflocks) |
| --- a/fs/hfs/super.c |
| +++ b/fs/hfs/super.c |
| @@ -138,9 +138,9 @@ static int hfs_show_options(struct seq_f |
| struct hfs_sb_info *sbi = HFS_SB(root->d_sb); |
| |
| if (sbi->s_creator != cpu_to_be32(0x3f3f3f3f)) |
| - seq_printf(seq, ",creator=%.4s", (char *)&sbi->s_creator); |
| + seq_show_option_n(seq, "creator", (char *)&sbi->s_creator, 4); |
| if (sbi->s_type != cpu_to_be32(0x3f3f3f3f)) |
| - seq_printf(seq, ",type=%.4s", (char *)&sbi->s_type); |
| + seq_show_option_n(seq, "type", (char *)&sbi->s_type, 4); |
| seq_printf(seq, ",uid=%u,gid=%u", sbi->s_uid, sbi->s_gid); |
| if (sbi->s_file_umask != 0133) |
| seq_printf(seq, ",file_umask=%o", sbi->s_file_umask); |
| --- a/fs/hfsplus/options.c |
| +++ b/fs/hfsplus/options.c |
| @@ -211,9 +211,9 @@ int hfsplus_show_options(struct seq_file |
| struct hfsplus_sb_info *sbi = HFSPLUS_SB(root->d_sb); |
| |
| if (sbi->creator != HFSPLUS_DEF_CR_TYPE) |
| - seq_printf(seq, ",creator=%.4s", (char *)&sbi->creator); |
| + seq_show_option_n(seq, "creator", (char *)&sbi->creator, 4); |
| if (sbi->type != HFSPLUS_DEF_CR_TYPE) |
| - seq_printf(seq, ",type=%.4s", (char *)&sbi->type); |
| + seq_show_option_n(seq, "type", (char *)&sbi->type, 4); |
| seq_printf(seq, ",umask=%o,uid=%u,gid=%u", sbi->umask, |
| sbi->uid, sbi->gid); |
| if (sbi->part >= 0) |
| --- a/fs/hostfs/hostfs_kern.c |
| +++ b/fs/hostfs/hostfs_kern.c |
| @@ -264,7 +264,7 @@ static int hostfs_show_options(struct se |
| size_t offset = strlen(root_ino) + 1; |
| |
| if (strlen(root_path) > offset) |
| - seq_printf(seq, ",%s", root_path + offset); |
| + seq_show_option(seq, root_path + offset, NULL); |
| |
| return 0; |
| } |
| --- a/fs/ocfs2/super.c |
| +++ b/fs/ocfs2/super.c |
| @@ -1578,8 +1578,8 @@ static int ocfs2_show_options(struct seq |
| seq_printf(s, ",localflocks,"); |
| |
| if (osb->osb_cluster_stack[0]) |
| - seq_printf(s, ",cluster_stack=%.*s", OCFS2_STACK_LABEL_LEN, |
| - osb->osb_cluster_stack); |
| + seq_show_option_n(s, "cluster_stack", osb->osb_cluster_stack, |
| + OCFS2_STACK_LABEL_LEN); |
| if (opts & OCFS2_MOUNT_USRQUOTA) |
| seq_printf(s, ",usrquota"); |
| if (opts & OCFS2_MOUNT_GRPQUOTA) |
| --- a/fs/reiserfs/super.c |
| +++ b/fs/reiserfs/super.c |
| @@ -645,18 +645,20 @@ static int reiserfs_show_options(struct |
| seq_puts(seq, ",acl"); |
| |
| if (REISERFS_SB(s)->s_jdev) |
| - seq_printf(seq, ",jdev=%s", REISERFS_SB(s)->s_jdev); |
| + seq_show_option(seq, "jdev", REISERFS_SB(s)->s_jdev); |
| |
| if (journal->j_max_commit_age != journal->j_default_max_commit_age) |
| seq_printf(seq, ",commit=%d", journal->j_max_commit_age); |
| |
| #ifdef CONFIG_QUOTA |
| if (REISERFS_SB(s)->s_qf_names[USRQUOTA]) |
| - seq_printf(seq, ",usrjquota=%s", REISERFS_SB(s)->s_qf_names[USRQUOTA]); |
| + seq_show_option(seq, "usrjquota", |
| + REISERFS_SB(s)->s_qf_names[USRQUOTA]); |
| else if (opts & (1 << REISERFS_USRQUOTA)) |
| seq_puts(seq, ",usrquota"); |
| if (REISERFS_SB(s)->s_qf_names[GRPQUOTA]) |
| - seq_printf(seq, ",grpjquota=%s", REISERFS_SB(s)->s_qf_names[GRPQUOTA]); |
| + seq_show_option(seq, "grpjquota", |
| + REISERFS_SB(s)->s_qf_names[GRPQUOTA]); |
| else if (opts & (1 << REISERFS_GRPQUOTA)) |
| seq_puts(seq, ",grpquota"); |
| if (REISERFS_SB(s)->s_jquota_fmt) { |
| --- a/fs/xfs/xfs_super.c |
| +++ b/fs/xfs/xfs_super.c |
| @@ -523,9 +523,9 @@ xfs_showargs( |
| seq_printf(m, "," MNTOPT_LOGBSIZE "=%dk", mp->m_logbsize >> 10); |
| |
| if (mp->m_logname) |
| - seq_printf(m, "," MNTOPT_LOGDEV "=%s", mp->m_logname); |
| + seq_show_option(m, MNTOPT_LOGDEV, mp->m_logname); |
| if (mp->m_rtname) |
| - seq_printf(m, "," MNTOPT_RTDEV "=%s", mp->m_rtname); |
| + seq_show_option(m, MNTOPT_RTDEV, mp->m_rtname); |
| |
| if (mp->m_dalign > 0) |
| seq_printf(m, "," MNTOPT_SUNIT "=%d", |
| --- a/include/linux/seq_file.h |
| +++ b/include/linux/seq_file.h |
| @@ -127,6 +127,41 @@ int seq_put_decimal_ull(struct seq_file |
| int seq_put_decimal_ll(struct seq_file *m, char delimiter, |
| long long num); |
| |
| +/** |
| + * seq_show_options - display mount options with appropriate escapes. |
| + * @m: the seq_file handle |
| + * @name: the mount option name |
| + * @value: the mount option name's value, can be NULL |
| + */ |
| +static inline void seq_show_option(struct seq_file *m, const char *name, |
| + const char *value) |
| +{ |
| + seq_putc(m, ','); |
| + seq_escape(m, name, ",= \t\n\\"); |
| + if (value) { |
| + seq_putc(m, '='); |
| + seq_escape(m, value, ", \t\n\\"); |
| + } |
| +} |
| + |
| +/** |
| + * seq_show_option_n - display mount options with appropriate escapes |
| + * where @value must be a specific length. |
| + * @m: the seq_file handle |
| + * @name: the mount option name |
| + * @value: the mount option name's value, cannot be NULL |
| + * @length: the length of @value to display |
| + * |
| + * This is a macro since this uses "length" to define the size of the |
| + * stack buffer. |
| + */ |
| +#define seq_show_option_n(m, name, value, length) { \ |
| + char val_buf[length + 1]; \ |
| + strncpy(val_buf, value, length); \ |
| + val_buf[length] = '\0'; \ |
| + seq_show_option(m, name, val_buf); \ |
| +} |
| + |
| #define SEQ_START_TOKEN ((void *)1) |
| /* |
| * Helpers for iteration over list_head-s in seq_files |
| --- a/kernel/cgroup.c |
| +++ b/kernel/cgroup.c |
| @@ -1071,15 +1071,16 @@ static int cgroup_show_options(struct se |
| |
| mutex_lock(&cgroup_root_mutex); |
| for_each_subsys(root, ss) |
| - seq_printf(seq, ",%s", ss->name); |
| + seq_show_option(seq, ss->name, NULL); |
| if (test_bit(ROOT_NOPREFIX, &root->flags)) |
| seq_puts(seq, ",noprefix"); |
| if (strlen(root->release_agent_path)) |
| - seq_printf(seq, ",release_agent=%s", root->release_agent_path); |
| + seq_show_option(seq, "release_agent", |
| + root->release_agent_path); |
| if (clone_children(&root->top_cgroup)) |
| seq_puts(seq, ",clone_children"); |
| if (strlen(root->name)) |
| - seq_printf(seq, ",name=%s", root->name); |
| + seq_show_option(seq, "name", root->name); |
| mutex_unlock(&cgroup_root_mutex); |
| return 0; |
| } |
| --- a/security/selinux/hooks.c |
| +++ b/security/selinux/hooks.c |
| @@ -1012,7 +1012,7 @@ static void selinux_write_opts(struct se |
| seq_puts(m, prefix); |
| if (has_comma) |
| seq_putc(m, '\"'); |
| - seq_puts(m, opts->mnt_opts[i]); |
| + seq_escape(m, opts->mnt_opts[i], "\"\n\\"); |
| if (has_comma) |
| seq_putc(m, '\"'); |
| } |