On Fri 09-10-26 23:51:56, Christian Brauner wrote:
> generic_ci_d_hash() and generic_ci_d_compare() read sb->s_encoding and
> dereference the unicode map while a pathwalk in rcu mode holds nothing
> but rcu_read_lock(). The filesystems that use them, ext4, f2fs and
> tmpfs, drop the map with utf8_unload() from ->put_super(), which runs
> when the last reference to the mount goes away. A lookup that is still
> inside such a filesystem at that point reads a freed map.
> 
> With a pathwalk held up in utf8_casefold_hash() while the last reference
> to a casefolded tmpfs goes away:
> 
>   BUG: KASAN: slab-use-after-free in utf8nlookup+0x74c/0x950
>   Read of size 8 at addr ff110001030fe658 by task walker/98
>   Call Trace:
>    <TASK>
>    utf8nlookup+0x74c/0x950
>    utf8byte+0x160/0x1560
>    utf8_casefold_hash+0x167/0x230
>    generic_ci_d_hash+0xdd/0x1c0
>    link_path_walk+0x11ad/0x1d30
>    path_lookupat+0x63/0xb00
>    filename_lookup+0x1f1/0x5b0
>    vfs_statx+0xd7/0x3b0
>   [...]
>   Allocated by task 94:
>    __kmalloc_cache_noprof+0x249/0x560
>    utf8_load+0x4e/0x570
>    shmem_parse_opt_casefold.isra.0+0x92/0x270
>   [...]
>   Freed by task 97:
>    kfree+0x1db/0x5b0
>    shmem_put_super+0x72/0x170
>    generic_shutdown_super+0x14c/0x530
>    kill_anon_super+0x3a/0x60
>    deactivate_locked_super+0x9c/0xf0
>    cleanup_mnt+0x9f/0x130
> 
> So far that has been hard to hit. umount(2) waits for a grace period
> in namespace_unlock() before it drops the mount, so only a mount whose
> last reference goes away elsewhere, like the close of a file or a chdir
> out of a lazily unmounted directory, can race a walker. So it is
> reachable.
> 
> Delay the free until a grace period has passed. The map and the data
> table it holds a reference to then outlive any pathwalk that found the
> superblock before the mount went away. The reference on the data table
> module is dropped from the rcu callback as well. utf8-core is built in
> whenever CONFIG_UNICODE is set, so there is no module exit that would
> have to wait for the callbacks.
> 
> Overlayfs points the s_encoding of an overlay at the map of its first
> layer without taking a reference of its own. That stays correct. The
> layer is held by the overlay, so its map is dropped only after the
> overlay is gone.
> 
> Fixes: b886ee3e778e ("ext4: Support case-insensitive file name lookups")
> Cc: [email protected]
> Signed-off-by: Christian Brauner (Amutable) <[email protected]>

Looks good. Feel free to add:

Reviewed-by: Jan Kara <[email protected]>

                                                                Honza

> ---
> Reproducer: delay.c holds a task named walker in utf8_casefold_hash() in
> rcu pathwalk mode for two seconds and repro.c drops the last reference
> to a lazily unmounted casefolded tmpfs meanwhile.
> 
>     mount -t tmpfs -o casefold tmpfs /mnt
>     chattr +F /mnt
>     echo hello > /mnt/Foo
>     insmod delay.ko
>     ./repro
> 
> Without this patch that gives the splat above, with it it runs clean.
> 
> delay.c:
> 
>     #include <linux/delay.h>
>     #include <linux/kprobes.h>
>     #include <linux/module.h>
> 
>     /* keep a task named "walker" in utf8_casefold_hash() in rcu pathwalk */
>     static int pre(struct kprobe *p, struct pt_regs *regs)
>     {
>       if (!strcmp(current->comm, "walker"))
>               mdelay(2000);
>       return 0;
>     }
> 
>     static struct kprobe kp = {
>       .symbol_name    = "utf8_casefold_hash",
>       .pre_handler    = pre,
>     };
> 
>     static int __init delay_init(void)
>     {
>       return register_kprobe(&kp);
>     }
> 
>     static void __exit delay_exit(void)
>     {
>       unregister_kprobe(&kp);
>     }
> 
>     module_init(delay_init);
>     module_exit(delay_exit);
>     MODULE_LICENSE("GPL");
> 
> repro.c:
> 
>     #define _GNU_SOURCE
>     #include <pthread.h>
>     #include <sched.h>
>     #include <string.h>
>     #include <sys/mman.h>
>     #include <sys/mount.h>
>     #include <sys/prctl.h>
>     #include <sys/stat.h>
>     #include <unistd.h>
> 
>     static void pin(int cpu)
>     {
>       cpu_set_t set;
> 
>       CPU_ZERO(&set);
>       CPU_SET(cpu, &set);
>       sched_setaffinity(0, sizeof(set), &set);
>     }
> 
>     static void *walker(void *arg)
>     {
>       struct stat st;
>       char *p;
> 
>       /*
>        * The delay spins. Empty this cpu's lru batches so that an
>        * lru_add_drain_all() on the other side has no work to wait for here.
>        */
>       pin(1);
>       p = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_PRIVATE | 
> MAP_ANONYMOUS, -1, 0);
>       memset(p, 1, 4096);
>       madvise(p, 4096, MADV_COLD);
> 
>       prctl(PR_SET_NAME, "walker");
>       stat("FOO", &st);
>       return NULL;
>     }
> 
>     int main(void)
>     {
>       struct stat st;
>       pthread_t t;
> 
>       pin(0);
>       chdir("/mnt");
>       stat("FOO", &st);
>       /* the cwd now holds the last reference to the mount */
>       umount2("/mnt", MNT_DETACH);
>       pthread_create(&t, NULL, walker, NULL);
>       sleep(1);
>       /* drop it, ->kill_sb() runs before chdir() returns */
>       chdir("/");
>       pthread_join(t, NULL);
>       return 0;
>     }
> ---
>  fs/unicode/utf8-core.c  | 21 +++++++++++++++++----
>  include/linux/unicode.h |  1 +
>  2 files changed, 18 insertions(+), 4 deletions(-)
> 
> diff --git a/fs/unicode/utf8-core.c b/fs/unicode/utf8-core.c
> index f313532f5e80..74a74a48dd8b 100644
> --- a/fs/unicode/utf8-core.c
> +++ b/fs/unicode/utf8-core.c
> @@ -3,6 +3,7 @@
>  #include <linux/kernel.h>
>  #include <linux/string.h>
>  #include <linux/slab.h>
> +#include <linux/rcupdate.h>
>  #include <linux/parser.h>
>  #include <linux/errno.h>
>  #include <linux/stringhash.h>
> @@ -183,12 +184,24 @@ struct unicode_map *utf8_load(unsigned int version)
>  }
>  EXPORT_SYMBOL(utf8_load);
>  
> +static void utf8_unload_rcu(struct rcu_head *head)
> +{
> +     struct unicode_map *um = container_of(head, struct unicode_map, rcu);
> +
> +     symbol_put(utf8_data_table);
> +     kfree(um);
> +}
> +
> +/*
> + * A pathwalk in rcu mode may still be in ->d_hash() or ->d_compare() of a
> + * filesystem that is being torn down and read the map through its
> + * superblock, so the map and the tables it points to have to outlive the
> + * grace period.
> + */
>  void utf8_unload(struct unicode_map *um)
>  {
> -     if (um) {
> -             symbol_put(utf8_data_table);
> -             kfree(um);
> -     }
> +     if (um)
> +             call_rcu(&um->rcu, utf8_unload_rcu);
>  }
>  EXPORT_SYMBOL(utf8_unload);
>  
> diff --git a/include/linux/unicode.h b/include/linux/unicode.h
> index 64fa44fe180c..3a8cb603eb93 100644
> --- a/include/linux/unicode.h
> +++ b/include/linux/unicode.h
> @@ -53,6 +53,7 @@ struct unicode_map {
>       unsigned int version;
>       const struct utf8data *ntab[UTF8_NMAX];
>       const struct utf8data_table *tables;
> +     struct rcu_head rcu;
>  };
>  
>  int utf8_validate(const struct unicode_map *um, const struct qstr *str);
> 
> -- 
> 2.53.0
> 
-- 
Jan Kara <[email protected]>
SUSE Labs, CR

Reply via email to