Commit c483bab0 authored by OGAWA Hirofumi's avatar OGAWA Hirofumi Committed by Linus Torvalds

fat: fix VFAT compat ioctls on 64-bit systems

If you compile and run the below test case in an msdos or vfat directory on
an x86-64 system with -m32 you'll get garbage in the kernel_dirent struct
followed by a SIGSEGV.

The patch fixes this.

Reported and initial fix by Bart Oldeman

#include <sys/types.h>
#include <sys/ioctl.h>
#include <dirent.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
struct kernel_dirent {
         long            d_ino;
         long		d_off;
         unsigned short  d_reclen;
         char            d_name[256]; /* We must not include limits.h! */
};
#define VFAT_IOCTL_READDIR_BOTH  _IOR('r', 1, struct kernel_dirent [2])
#define VFAT_IOCTL_READDIR_SHORT  _IOR('r', 2, struct kernel_dirent [2])

int main(void)
{
         int fd = open(".", O_RDONLY);
         struct kernel_dirent de[2];

         while (1) {
                 int i = ioctl(fd, VFAT_IOCTL_READDIR_BOTH, (long)de);
                 if (i == -1) break;
                 if (de[0].d_reclen == 0) break;
                 printf("SFN: reclen=%2d off=%d ino=%d, %-12s",
 		       de[0].d_reclen, de[0].d_off, de[0].d_ino, de[0].d_name);
 		if (de[1].d_reclen)
 		  printf("\tLFN: reclen=%2d off=%d ino=%d, %s",
 		    de[1].d_reclen, de[1].d_off, de[1].d_ino, de[1].d_name);
 		printf("\n");
         }
         return 0;
}
Signed-off-by: default avatarBart Oldeman <bartoldeman@users.sourceforge.net>
Signed-off-by: default avatarOGAWA Hirofumi <hirofumi@mail.parknet.co.jp>
Cc: <stable@kernel.org>
Signed-off-by: default avatarAndrew Morton <akpm@linux-foundation.org>
Signed-off-by: default avatarLinus Torvalds <torvalds@linux-foundation.org>
parent b247e8aa
...@@ -422,7 +422,7 @@ int fat_search_long(struct inode *inode, const unsigned char *name, ...@@ -422,7 +422,7 @@ int fat_search_long(struct inode *inode, const unsigned char *name,
EXPORT_SYMBOL_GPL(fat_search_long); EXPORT_SYMBOL_GPL(fat_search_long);
struct fat_ioctl_filldir_callback { struct fat_ioctl_filldir_callback {
struct dirent __user *dirent; void __user *dirent;
int result; int result;
/* for dir ioctl */ /* for dir ioctl */
const char *longname; const char *longname;
...@@ -647,62 +647,85 @@ static int fat_readdir(struct file *filp, void *dirent, filldir_t filldir) ...@@ -647,62 +647,85 @@ static int fat_readdir(struct file *filp, void *dirent, filldir_t filldir)
return __fat_readdir(inode, filp, dirent, filldir, 0, 0); return __fat_readdir(inode, filp, dirent, filldir, 0, 0);
} }
static int fat_ioctl_filldir(void *__buf, const char *name, int name_len, #define FAT_IOCTL_FILLDIR_FUNC(func, dirent_type) \
loff_t offset, u64 ino, unsigned int d_type) static int func(void *__buf, const char *name, int name_len, \
loff_t offset, u64 ino, unsigned int d_type) \
{ \
struct fat_ioctl_filldir_callback *buf = __buf; \
struct dirent_type __user *d1 = buf->dirent; \
struct dirent_type __user *d2 = d1 + 1; \
\
if (buf->result) \
return -EINVAL; \
buf->result++; \
\
if (name != NULL) { \
/* dirent has only short name */ \
if (name_len >= sizeof(d1->d_name)) \
name_len = sizeof(d1->d_name) - 1; \
\
if (put_user(0, d2->d_name) || \
put_user(0, &d2->d_reclen) || \
copy_to_user(d1->d_name, name, name_len) || \
put_user(0, d1->d_name + name_len) || \
put_user(name_len, &d1->d_reclen)) \
goto efault; \
} else { \
/* dirent has short and long name */ \
const char *longname = buf->longname; \
int long_len = buf->long_len; \
const char *shortname = buf->shortname; \
int short_len = buf->short_len; \
\
if (long_len >= sizeof(d1->d_name)) \
long_len = sizeof(d1->d_name) - 1; \
if (short_len >= sizeof(d1->d_name)) \
short_len = sizeof(d1->d_name) - 1; \
\
if (copy_to_user(d2->d_name, longname, long_len) || \
put_user(0, d2->d_name + long_len) || \
put_user(long_len, &d2->d_reclen) || \
put_user(ino, &d2->d_ino) || \
put_user(offset, &d2->d_off) || \
copy_to_user(d1->d_name, shortname, short_len) || \
put_user(0, d1->d_name + short_len) || \
put_user(short_len, &d1->d_reclen)) \
goto efault; \
} \
return 0; \
efault: \
buf->result = -EFAULT; \
return -EFAULT; \
}
FAT_IOCTL_FILLDIR_FUNC(fat_ioctl_filldir, dirent)
static int fat_ioctl_readdir(struct inode *inode, struct file *filp,
void __user *dirent, filldir_t filldir,
int short_only, int both)
{ {
struct fat_ioctl_filldir_callback *buf = __buf; struct fat_ioctl_filldir_callback buf;
struct dirent __user *d1 = buf->dirent; int ret;
struct dirent __user *d2 = d1 + 1;
buf.dirent = dirent;
if (buf->result) buf.result = 0;
return -EINVAL; mutex_lock(&inode->i_mutex);
buf->result++; ret = -ENOENT;
if (!IS_DEADDIR(inode)) {
if (name != NULL) { ret = __fat_readdir(inode, filp, &buf, filldir,
/* dirent has only short name */ short_only, both);
if (name_len >= sizeof(d1->d_name))
name_len = sizeof(d1->d_name) - 1;
if (put_user(0, d2->d_name) ||
put_user(0, &d2->d_reclen) ||
copy_to_user(d1->d_name, name, name_len) ||
put_user(0, d1->d_name + name_len) ||
put_user(name_len, &d1->d_reclen))
goto efault;
} else {
/* dirent has short and long name */
const char *longname = buf->longname;
int long_len = buf->long_len;
const char *shortname = buf->shortname;
int short_len = buf->short_len;
if (long_len >= sizeof(d1->d_name))
long_len = sizeof(d1->d_name) - 1;
if (short_len >= sizeof(d1->d_name))
short_len = sizeof(d1->d_name) - 1;
if (copy_to_user(d2->d_name, longname, long_len) ||
put_user(0, d2->d_name + long_len) ||
put_user(long_len, &d2->d_reclen) ||
put_user(ino, &d2->d_ino) ||
put_user(offset, &d2->d_off) ||
copy_to_user(d1->d_name, shortname, short_len) ||
put_user(0, d1->d_name + short_len) ||
put_user(short_len, &d1->d_reclen))
goto efault;
} }
return 0; mutex_unlock(&inode->i_mutex);
efault: if (ret >= 0)
buf->result = -EFAULT; ret = buf.result;
return -EFAULT; return ret;
} }
static int fat_dir_ioctl(struct inode * inode, struct file * filp, static int fat_dir_ioctl(struct inode *inode, struct file *filp,
unsigned int cmd, unsigned long arg) unsigned int cmd, unsigned long arg)
{ {
struct fat_ioctl_filldir_callback buf; struct dirent __user *d1 = (struct dirent __user *)arg;
struct dirent __user *d1; int short_only, both;
int ret, short_only, both;
switch (cmd) { switch (cmd) {
case VFAT_IOCTL_READDIR_SHORT: case VFAT_IOCTL_READDIR_SHORT:
...@@ -717,7 +740,6 @@ static int fat_dir_ioctl(struct inode * inode, struct file * filp, ...@@ -717,7 +740,6 @@ static int fat_dir_ioctl(struct inode * inode, struct file * filp,
return fat_generic_ioctl(inode, filp, cmd, arg); return fat_generic_ioctl(inode, filp, cmd, arg);
} }
d1 = (struct dirent __user *)arg;
if (!access_ok(VERIFY_WRITE, d1, sizeof(struct dirent[2]))) if (!access_ok(VERIFY_WRITE, d1, sizeof(struct dirent[2])))
return -EFAULT; return -EFAULT;
/* /*
...@@ -728,69 +750,48 @@ static int fat_dir_ioctl(struct inode * inode, struct file * filp, ...@@ -728,69 +750,48 @@ static int fat_dir_ioctl(struct inode * inode, struct file * filp,
if (put_user(0, &d1->d_reclen)) if (put_user(0, &d1->d_reclen))
return -EFAULT; return -EFAULT;
buf.dirent = d1; return fat_ioctl_readdir(inode, filp, d1, fat_ioctl_filldir,
buf.result = 0;
mutex_lock(&inode->i_mutex);
ret = -ENOENT;
if (!IS_DEADDIR(inode)) {
ret = __fat_readdir(inode, filp, &buf, fat_ioctl_filldir,
short_only, both); short_only, both);
}
mutex_unlock(&inode->i_mutex);
if (ret >= 0)
ret = buf.result;
return ret;
} }
#ifdef CONFIG_COMPAT #ifdef CONFIG_COMPAT
#define VFAT_IOCTL_READDIR_BOTH32 _IOR('r', 1, struct compat_dirent[2]) #define VFAT_IOCTL_READDIR_BOTH32 _IOR('r', 1, struct compat_dirent[2])
#define VFAT_IOCTL_READDIR_SHORT32 _IOR('r', 2, struct compat_dirent[2]) #define VFAT_IOCTL_READDIR_SHORT32 _IOR('r', 2, struct compat_dirent[2])
static long fat_compat_put_dirent32(struct dirent *d, FAT_IOCTL_FILLDIR_FUNC(fat_compat_ioctl_filldir, compat_dirent)
struct compat_dirent __user *d32)
{
if (!access_ok(VERIFY_WRITE, d32, sizeof(struct compat_dirent)))
return -EFAULT;
__put_user(d->d_ino, &d32->d_ino); static long fat_compat_dir_ioctl(struct file *filp, unsigned cmd,
__put_user(d->d_off, &d32->d_off);
__put_user(d->d_reclen, &d32->d_reclen);
if (__copy_to_user(d32->d_name, d->d_name, d->d_reclen))
return -EFAULT;
return 0;
}
static long fat_compat_dir_ioctl(struct file *file, unsigned cmd,
unsigned long arg) unsigned long arg)
{ {
struct compat_dirent __user *p = compat_ptr(arg); struct inode *inode = filp->f_path.dentry->d_inode;
int ret; struct compat_dirent __user *d1 = compat_ptr(arg);
mm_segment_t oldfs = get_fs(); int short_only, both;
struct dirent d[2];
switch (cmd) { switch (cmd) {
case VFAT_IOCTL_READDIR_BOTH32:
cmd = VFAT_IOCTL_READDIR_BOTH;
break;
case VFAT_IOCTL_READDIR_SHORT32: case VFAT_IOCTL_READDIR_SHORT32:
cmd = VFAT_IOCTL_READDIR_SHORT; short_only = 1;
both = 0;
break;
case VFAT_IOCTL_READDIR_BOTH32:
short_only = 0;
both = 1;
break; break;
default: default:
return -ENOIOCTLCMD; return -ENOIOCTLCMD;
} }
set_fs(KERNEL_DS); if (!access_ok(VERIFY_WRITE, d1, sizeof(struct compat_dirent[2])))
lock_kernel(); return -EFAULT;
ret = fat_dir_ioctl(file->f_path.dentry->d_inode, file, /*
cmd, (unsigned long) &d); * Yes, we don't need this put_user() absolutely. However old
unlock_kernel(); * code didn't return the right value. So, app use this value,
set_fs(oldfs); * in order to check whether it is EOF.
if (ret >= 0) { */
ret |= fat_compat_put_dirent32(&d[0], p); if (put_user(0, &d1->d_reclen))
ret |= fat_compat_put_dirent32(&d[1], p + 1); return -EFAULT;
}
return ret; return fat_ioctl_readdir(inode, filp, d1, fat_compat_ioctl_filldir,
short_only, both);
} }
#endif /* CONFIG_COMPAT */ #endif /* CONFIG_COMPAT */
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment