[<prev] [next>] [<thread-prev] [thread-next>] [day] [month] [year] [list]
Message-ID: <20071231200247.GA30373@sergelap.austin.ibm.com>
Date: Mon, 31 Dec 2007 14:02:47 -0600
From: "Serge E. Hallyn" <serue@...ibm.com>
To: Tetsuo Handa <penguin-kernel@...ove.SAKURA.ne.jp>
Cc: linux-fsdevel@...r.kernel.org, linux-kernel@...r.kernel.org
Subject: Re: [PATCH][RFC] Simple tamper-proof device filesystem.
Quoting Tetsuo Handa (penguin-kernel@...ove.SAKURA.ne.jp):
> Hello.
>
> Thank you for attending discussion for previous posting
> (starting from http://lkml.org/lkml/2007/12/16/23 ).
>
> The previous posting was for feasibility test to know
> whether this kind of trivial filesystem is acceptable for mainline.
>
> Now, it seems that there is a little chance for accepting.
> Therefore I rebased the patch using the -mm tree.
>
> Regards.
> ----------
> Subject: Simple tamper-proof device filesystem.
>
> The goal of this filesystem is to guarantee that
> "applications using well-known device locations under /dev
> get the device they want" (e.g. an application that accesses /dev/null can
> always get a character special device with major=1 and minor=3).
>
> This idea sounds silly? Indeed, if you think the root can do whatever
> he/she wants do do. But this filesystem makes sense when used with
> access control mechanisms like MAC (mandatory access control).
> I want to use this filesystem in case where a process with root privilege was
> hijacked but the behavior of the hijacked process is still restricted by MAC.
>
> Why not use FUSE?
>
> Because /dev has to be available through the lifetime of the kernel.
> It is not acceptable if /dev stops working due to SIGKILL or OOM-killer.
>
> Why not use SELinux?
>
> Because SELinux doesn't guarantee filename and its attribute.
> As far as I know, no MAC implementation can handle filename and its attribute.
> I guess this is because
>
> Filename and its attributes pairs are conventionally considered as
> constant and reliable.
>
> It makes the MAC's policy syntax complicated to describe this attribute
> enforcement information in MAC's policy.
>
> I want to add functionality that the MACs are missing.
> Instead of adding this functionality per MAC,
> I propose to add it as ground work, to be combined with any MAC.
>
> Why not drop CAP_MKNOD?
>
> Dropping CAP_MKNOD is not enough for emulating this filesystem because
> a process can still rename()/unlink() to break filename and its attributes
> handling (e.g. mv /dev/sda1 /dev/sda1.tmp; mv /dev/sda2 /dev/sda1;
> mv /dev/sda1.tmp /dev/sda2 or unlink /dev/null; touch /dev/null ).
>
> This time, I'm implementing this filesystem as an extension to tmpfs
> because what this filesystem does are nothing but check filename and
> its attributes in addition to what tmpfs does.
>
> Signed-off-by: Tetsuo Handa <penguin-kernel@...ove.SAKURA.ne.jp>
> ---
> fs/ramfs/inode.c | 101 ++++-
> fs/ramfs/syaoran.h | 1066 +++++++++++++++++++++++++++++++++++++++++++++++++++++
> 2 files changed, 1160 insertions(+), 7 deletions(-)
>
> --- linux-2.6-mm.orig/fs/ramfs/inode.c
> +++ linux-2.6-mm/fs/ramfs/inode.c
> @@ -35,6 +35,7 @@
> #include <linux/sched.h>
> #include <asm/uaccess.h>
> #include "internal.h"
> +#include "syaoran.h"
>
> /* some random number */
> #define RAMFS_MAGIC 0x858458f6
> @@ -49,7 +50,8 @@ static struct backing_dev_info ramfs_bac
> BDI_CAP_READ_MAP | BDI_CAP_WRITE_MAP | BDI_CAP_EXEC_MAP,
> };
>
> -struct inode *ramfs_get_inode(struct super_block *sb, int mode, dev_t dev)
> +struct inode *__ramfs_get_inode(struct super_block *sb, int mode, dev_t dev,
> + const int mac)
> {
> struct inode * inode = new_inode(sb);
>
> @@ -65,10 +67,19 @@ struct inode *ramfs_get_inode(struct sup
> switch (mode & S_IFMT) {
> default:
> init_special_inode(inode, mode, dev);
> + if (mac) {
> + if (S_ISBLK(mode))
> + inode->i_fop = &wrapped_def_blk_fops;
> + else if (S_ISCHR(mode))
> + inode->i_fop = &wrapped_def_chr_fops;
> + inode->i_op = &syaoran_file_inode_operations;
> + }
> break;
> case S_IFREG:
> inode->i_op = &ramfs_file_inode_operations;
> inode->i_fop = &ramfs_file_operations;
> + if (mac)
> + inode->i_op = &syaoran_file_inode_operations;
> break;
> case S_IFDIR:
> inode->i_op = &ramfs_dir_inode_operations;
> @@ -79,12 +90,19 @@ struct inode *ramfs_get_inode(struct sup
> break;
> case S_IFLNK:
> inode->i_op = &page_symlink_inode_operations;
> + if (mac)
> + inode->i_op = &syaoran_symlink_inode_operations;
> break;
> }
> }
> return inode;
> }
>
> +struct inode *ramfs_get_inode(struct super_block *sb, int mode, dev_t dev)
> +{
> + return __ramfs_get_inode(sb, mode, dev, 0);
> +}
> +
> /*
> * File creation. Allocate an inode, and we're done..
> */
> @@ -92,9 +110,17 @@ struct inode *ramfs_get_inode(struct sup
> static int
> ramfs_mknod(struct inode *dir, struct dentry *dentry, int mode, dev_t dev)
> {
> - struct inode * inode = ramfs_get_inode(dir->i_sb, mode, dev);
> + struct inode *inode;
> int error = -ENOSPC;
>
> + /*** SYAORAN start. ***/
> + if (dir->i_sb->s_op == &syaoran_ops) {
> + if (syaoran_may_create_node(dentry, mode, dev) < 0)
> + return -EPERM;
> + inode = syaoran_get_inode(dir->i_sb, mode, dev);
> + /*** SYAORAN end. ***/
> + } else
> + inode = ramfs_get_inode(dir->i_sb, mode, dev);
> if (inode) {
> if (dir->i_mode & S_ISGID) {
> inode->i_gid = dir->i_gid;
> @@ -127,7 +153,14 @@ static int ramfs_symlink(struct inode *
> struct inode *inode;
> int error = -ENOSPC;
>
> - inode = ramfs_get_inode(dir->i_sb, S_IFLNK|S_IRWXUGO, 0);
> + /*** SYAORAN start. ***/
> + if (dir->i_sb->s_op == &syaoran_ops) {
> + if (syaoran_may_create_node(dentry, S_IFLNK, 0) < 0)
> + return -EPERM;
> + inode = syaoran_get_inode(dir->i_sb, S_IFLNK|S_IRWXUGO, 0);
> + /*** SYAORAN end. ***/
> + } else
> + inode = ramfs_get_inode(dir->i_sb, S_IFLNK|S_IRWXUGO, 0);
> if (inode) {
> int l = strlen(symname)+1;
> error = page_symlink(inode, symname, l);
> @@ -146,13 +179,14 @@ static int ramfs_symlink(struct inode *
> static const struct inode_operations ramfs_dir_inode_operations = {
> .create = ramfs_create,
> .lookup = simple_lookup,
> - .link = simple_link,
> - .unlink = simple_unlink,
> + .link = ramfs_link,
> + .unlink = ramfs_unlink,
> .symlink = ramfs_symlink,
> .mkdir = ramfs_mkdir,
> - .rmdir = simple_rmdir,
> + .rmdir = ramfs_rmdir,
> .mknod = ramfs_mknod,
> - .rename = simple_rename,
> + .rename = ramfs_rename,
> + .setattr = ramfs_setattr,
> };
>
> static const struct super_operations ramfs_ops = {
> @@ -184,6 +218,35 @@ static int ramfs_fill_super(struct super
> return 0;
> }
>
> +static int syaoran_fill_super(struct super_block *sb, void *data, int silent)
> +{
> + struct inode *inode;
> + struct dentry *root;
> + int error;
> +
> + sb->s_maxbytes = MAX_LFS_FILESIZE;
> + sb->s_blocksize = PAGE_CACHE_SIZE;
> + sb->s_blocksize_bits = PAGE_CACHE_SHIFT;
> + sb->s_magic = SYAORAN_MAGIC;
> + sb->s_op = &syaoran_ops;
> + sb->s_time_gran = 1;
> + error = syaoran_initialize(sb, data);
> + if (error < 0)
> + return error;
> + inode = syaoran_get_inode(sb, S_IFDIR | 0755, 0);
> + if (!inode)
> + return -ENOMEM;
> +
> + root = d_alloc_root(inode);
> + if (!root) {
> + iput(inode);
> + return -ENOMEM;
> + }
> + sb->s_root = root;
> + syaoran_make_initial_nodes(sb);
> + return 0;
> +}
> +
> int ramfs_get_sb(struct file_system_type *fs_type,
> int flags, const char *dev_name, void *data, struct vfsmount *mnt)
> {
> @@ -197,6 +260,13 @@ static int rootfs_get_sb(struct file_sys
> mnt);
> }
>
> +static int syaoran_get_sb(struct file_system_type *fs_type, int flags,
> + const char *dev_name, void *data,
> + struct vfsmount *mnt)
> +{
> + return get_sb_nodev(fs_type, flags, data, syaoran_fill_super, mnt);
> +}
> +
> static struct file_system_type ramfs_fs_type = {
> .name = "ramfs",
> .get_sb = ramfs_get_sb,
> @@ -207,6 +277,11 @@ static struct file_system_type rootfs_fs
> .get_sb = rootfs_get_sb,
> .kill_sb = kill_litter_super,
> };
> +static struct file_system_type syaoran_fs_type = {
> + .name = "syaoran",
> + .get_sb = syaoran_get_sb,
> + .kill_sb = kill_litter_super,
> +};
>
> static int __init init_ramfs_fs(void)
> {
> @@ -237,3 +312,15 @@ int __init init_rootfs(void)
> }
>
> MODULE_LICENSE("GPL");
> +
> +static int __init init_syaoran_fs(void)
> +{
> + return register_filesystem(&syaoran_fs_type);
> +}
> +
> +static void __exit exit_syaoran_fs(void)
> +{
> + unregister_filesystem(&syaoran_fs_type);
> +}
> +module_init(init_syaoran_fs);
> +module_exit(exit_syaoran_fs);
> --- /dev/null
> +++ linux-2.6-mm/fs/ramfs/syaoran.h
> @@ -0,0 +1,1066 @@
> +/*
> + * fs/ramfs/syaoran.h
> + *
> + * Implementation of the Tamper-Proof Device Filesystem.
> + *
> + * Copyright (C) 2005-2007 NTT DATA CORPORATION
> + *
> + * Version: 1.5.3-pre 2007/12/23
> + */
> +
> +#include <linux/namei.h>
> +#include <linux/mm.h>
> +#include <linux/quotaops.h>
> +
> +#define list_for_each_cookie(pos, cookie, head) \
> + for ((cookie) || ((cookie) = (head)), pos = (cookie)->next; \
> + prefetch(pos->next), pos != (head) || ((cookie) = NULL); \
> + (cookie) = pos, pos = pos->next)
> +
> +/* The following constants are used to restrict operations.*/
> +
> +#define MAY_CREATE 1 /* This file is allowed to be mknod()ed. */
> +#define MAY_DELETE 2 /* This file is allowed to be unlink()ed. */
> +#define MAY_CHMOD 4 /* This file is allowed to be chmod()ed. */
> +#define MAY_CHOWN 8 /* This file is allowed to be chown()ed. */
> +#define DEVICE_USED 16 /* This block or character device file is used. */
> +#define NO_CREATE_AT_MOUNT 32 /* Don't create this file at mount(). */
> +
> +/* some random number */
> +#define SYAORAN_MAGIC 0x2F646576 /* = '/dev' */
> +
> +static struct inode_operations syaoran_file_inode_operations;
> +static struct inode_operations syaoran_symlink_inode_operations;
> +
> +static void syaoran_put_super(struct super_block *sb);
> +static int syaoran_initialize(struct super_block *sb, void *data);
> +static void syaoran_make_initial_nodes(struct super_block *sb);
> +static int syaoran_may_create_node(struct dentry *dentry, int mode, int dev);
> +static int syaoran_may_modify_node(struct dentry *dentry, unsigned int flags);
> +static int syaoran_create_tracelog(struct super_block *sb,
> + const char *filename);
> +static struct inode *__ramfs_get_inode(struct super_block *sb, int mode,
> + dev_t dev, int mac);
> +
> +static struct inode *syaoran_get_inode(struct super_block *sb, int mode,
> + dev_t dev)
> +{
> + return __ramfs_get_inode(sb, mode, dev, 1);
To integrate this nicer into tmpfs, at least define TMPFS_IS_MAC as 1
and TMPFS_NOT_MAC as 0 and pass those values instead of just 1 and 0.
> +}
> +
> +static struct super_operations syaoran_ops = {
> + .statfs = simple_statfs,
> + .drop_inode = generic_delete_inode,
> + .put_super = syaoran_put_super,
> +};
> +
> +/* Wraps blkdev_open() to trace open operation for block devices. */
> +static int (*org_blkdev_open) (struct inode *inode, struct file *filp);
> +static struct file_operations wrapped_def_blk_fops;
Again, I should think you'd actually want to take blkdev_open() from
fs/block_dev.c and chrdev_open() from fs/char_dev.c. Surely your
method of grabbing it here is not acceptable for upstream code.
That's all I've got for now - though if you'd just break up some of
these functions - especially syaoran_initialize() with it's set of {}
blocks, it would help.
thanks,
-serge
> +
> +static int wrapped_blkdev_open(struct inode *inode, struct file *filp)
> +{
> + int error = org_blkdev_open(inode, filp);
> + if (error != -ENXIO)
> + syaoran_may_modify_node(filp->f_dentry, DEVICE_USED);
> + return error;
> +}
> +
> +/* Wraps chrdev_open() to trace open operation for character devices. */
> +static int (*org_chrdev_open) (struct inode *inode, struct file *filp);
> +static struct file_operations wrapped_def_chr_fops;
> +
> +static int wrapped_chrdev_open(struct inode *inode, struct file *filp)
> +{
> + int error = org_chrdev_open(inode, filp);
> + if (error != -ENXIO)
> + syaoran_may_modify_node(filp->f_dentry, DEVICE_USED);
> + return error;
> +}
> +
> +struct dev_entry {
> + struct list_head list;
> + /* Binary form of pathname under mount point. Never NULL. */
> + char *name;
> + /*
> + * Mode and permissions. setuid/setgid/sticky bits are not supported.
> + */
> + mode_t mode;
> + uid_t uid;
> + gid_t gid;
> + dev_t kdev;
> + /*
> + * Binary form of initial contents for the symlink.
> + * NULL if not symlink.
> + */
> + char *symlink_data;
> + /* File access control flags. */
> + unsigned int flags;
> + /* Text form of pathname under mount point. Never NULL. */
> + const char *printable_name;
> + /*
> + * Text form of initial contents for the symlink.
> + * NULL if not symlink.
> + */
> + const char *printable_symlink_data;
> +};
> +
> +struct syaoran_sb_info {
> + struct list_head list;
> + bool initialize_done; /* False if initialization is in progress. */
> + bool is_permissive_mode; /* True if permissive mode. */
> +};
> +
> +static void syaoran_put_super(struct super_block *sb)
> +{
> + struct syaoran_sb_info *info;
> + struct dev_entry *entry;
> + struct dev_entry *tmp;
> + if (!sb)
> + return;
> + info = (struct syaoran_sb_info *) sb->s_fs_info;
> + if (!info)
> + return;
> + list_for_each_entry_safe(entry, tmp, &info->list, list) {
> + kfree(entry->name);
> + kfree(entry->symlink_data);
> + kfree(entry->printable_name);
> + kfree(entry->printable_symlink_data);
> + list_del(&entry->list);
> + /* printk("Entry removed.\n"); */
> + kfree(entry);
> + }
> + kfree(info);
> + sb->s_fs_info = NULL;
> + printk(KERN_DEBUG "%s: Unused memory freed.\n", __FUNCTION__);
> +}
> +
> +/* Get absolute pathname from mount point. */
> +static int get_local_absolute_path(struct dentry *dentry, char *buffer,
> + int buflen)
> +{
> + char *start = buffer;
> + char *end = buffer + buflen;
> + int namelen;
> +
> + if (buflen < 256)
> + goto out;
> +
> + *--end = '\0';
> + buflen--;
> + for (;;) {
> + struct dentry *parent;
> + if (IS_ROOT(dentry))
> + break;
> + parent = dentry->d_parent;
> + namelen = dentry->d_name.len;
> + buflen -= namelen + 1;
> + if (buflen < 0)
> + goto out;
> + end -= namelen;
> + memcpy(end, dentry->d_name.name, namelen);
> + *--end = '/';
> + dentry = parent;
> + }
> + if (*end == '/') {
> + buflen++;
> + end++;
> + }
> + namelen = dentry->d_name.len;
> + buflen -= namelen;
> + if (buflen < 0)
> + goto out;
> + end -= namelen;
> + memcpy(end, dentry->d_name.name, namelen);
> + memmove(start, end, strlen(end) + 1);
> + return 0;
> +out:
> + return -ENOMEM;
> +}
> +
> +/* Get absolute pathname of the given dentry from mount point. */
> +static int local_realpath_from_dentry(struct dentry *dentry, char *newname,
> + int newname_len)
> +{
> + int error;
> + struct dentry *d_dentry;
> + if (!dentry || !newname || newname_len <= 0)
> + return -EINVAL;
> + d_dentry = dget(dentry);
> + /***** CRITICAL SECTION START *****/
> + spin_lock(&dcache_lock);
> + error = get_local_absolute_path(d_dentry, newname, newname_len);
> + spin_unlock(&dcache_lock);
> + /***** CRITICAL SECTION END *****/
> + dput(d_dentry);
> + return error;
> +}
> +
> +static int syaoran_check_flags(struct syaoran_sb_info *info,
> + struct dentry *dentry, int mode, int dev,
> + unsigned int flags)
> +{
> + int error = -EPERM;
> + struct dev_entry *entry;
> + /*
> + * Since local_realpath_from_dentry() holds dcache_lock,
> + * allocating buffer using kmalloc() won't help improving concurrency.
> + * Therefore, I use static buffer here.
> + */
> + static char filename[PAGE_SIZE];
> + static DEFINE_SPINLOCK(lock);
> + spin_lock(&lock);
> + memset(filename, 0, sizeof(filename));
> + if (local_realpath_from_dentry(dentry, filename, sizeof(filename) - 1))
> + goto out;
> + list_for_each_entry(entry, &info->list, list) {
> + if ((mode & S_IFMT) != (entry->mode & S_IFMT))
> + continue;
> + if ((S_ISBLK(mode) || S_ISCHR(mode)) && dev != entry->kdev)
> + continue;
> + if (strcmp(entry->name, filename + 1))
> + continue;
> + if (info->is_permissive_mode) {
> + entry->flags |= flags;
> + error = 0;
> + } else {
> + if ((entry->flags & flags) == flags)
> + error = 0;
> + }
> + break;
> + }
> +out:
> + if (error && strlen(filename) < (sizeof(filename) / 4) - 16) {
> + const char *name;
> + const uid_t uid = current->fsuid;
> + const gid_t gid = current->fsgid;
> + const mode_t perm = mode & 0777;
> + flags &= ~DEVICE_USED;
> + {
> + char *end = filename + sizeof(filename) - 1;
> + const char *cp = strchr(filename, '\0') - 1;
> + while (cp > filename) {
> + const unsigned char c = *cp--;
> + if (c == '\\') {
> + *--end = '\\';
> + *--end = '\\';
> + } else if (c > ' ' && c < 127) {
> + *--end = c;
> + } else {
> + *--end = (c & 7) + '0';
> + *--end = ((c >> 3) & 7) + '0';
> + *--end = (c >> 6) + '0';
> + *--end = '\\';
> + }
> + }
> + name = end;
> + }
> + switch (mode & S_IFMT) {
> + case S_IFCHR:
> + printk(KERN_DEBUG
> + "SYAORAN-ERROR: %s %3o %3u %3u %2u %c %3u %3u\n",
> + name, perm, uid, gid, flags, 'c',
> + MAJOR(dev), MINOR(dev));
> + break;
> + case S_IFBLK:
> + printk(KERN_DEBUG
> + "SYAORAN-ERROR: %s %3o %3u %3u %2u %c %3u %3u\n",
> + name, perm, uid, gid, flags, 'b',
> + MAJOR(dev), MINOR(dev));
> + break;
> + case S_IFIFO:
> + printk(KERN_DEBUG
> + "SYAORAN-ERROR: %s %3o %3u %3u %2u %c\n",
> + name, perm, uid, gid, flags, 'p');
> + break;
> + case S_IFSOCK:
> + printk(KERN_DEBUG
> + "SYAORAN-ERROR: %s %3o %3u %3u %2u %c\n",
> + name, perm, uid, gid, flags, 's');
> + break;
> + case S_IFDIR:
> + printk(KERN_DEBUG
> + "SYAORAN-ERROR: %s %3o %3u %3u %2u %c\n",
> + name, perm, uid, gid, flags, 'd');
> + break;
> + case S_IFLNK:
> + printk(KERN_DEBUG
> + "SYAORAN-ERROR: %s %3o %3u %3u %2u %c %s\n",
> + name, perm, uid, gid, flags, 'l', "unknown");
> + break;
> + case S_IFREG:
> + printk(KERN_DEBUG
> + "SYAORAN-ERROR: %s %3o %3u %3u %2u %c\n",
> + name, perm, uid, gid, flags, 'f');
> + break;
> + }
> + }
> + spin_unlock(&lock);
> + return error;
> +}
> +
> +/* Check whether the given dentry is allowed to mknod. */
> +static int syaoran_may_create_node(struct dentry *dentry, int mode, int dev)
> +{
> + struct syaoran_sb_info *info =
> + (struct syaoran_sb_info *) dentry->d_sb->s_fs_info;
> + if (!info) {
> + printk(KERN_DEBUG "%s: dentry->d_sb->s_fs_info == NULL\n",
> + __FUNCTION__);
> + return -EPERM;
> + }
> + if (!info->initialize_done)
> + return 0;
> + return syaoran_check_flags(info, dentry, mode, dev, MAY_CREATE);
> +}
> +
> +/* Check whether the given dentry is allowed to chmod/chown/unlink. */
> +static int syaoran_may_modify_node(struct dentry *dentry, unsigned int flags)
> +{
> + struct syaoran_sb_info *info =
> + (struct syaoran_sb_info *) dentry->d_sb->s_fs_info;
> + if (!info) {
> + printk(KERN_DEBUG "%s: dentry->d_sb->s_fs_info == NULL\n",
> + __FUNCTION__);
> + return -EPERM;
> + }
> + if (flags == DEVICE_USED && !info->is_permissive_mode)
> + return 0;
> + if (!dentry->d_inode)
> + return -ENOENT;
> + return syaoran_check_flags(info, dentry, dentry->d_inode->i_mode,
> + dentry->d_inode->i_rdev, flags);
> +}
> +
> +static int ramfs_link(struct dentry *old_dentry, struct inode *dir,
> + struct dentry *dentry)
> +{
> + struct inode *inode;
> + if (dir->i_sb->s_op != &syaoran_ops)
> + goto ok;
> + /*** SYAORAN start. ***/
> + inode = old_dentry->d_inode;
> + if (!inode ||
> + syaoran_may_create_node(dentry, inode->i_mode, inode->i_rdev))
> + return -EPERM;
> + /*** SYAORAN end. ***/
> +ok:
> + return simple_link(old_dentry, dir, dentry);
> +}
> +
> +static int ramfs_unlink(struct inode *dir, struct dentry *dentry)
> +{
> + if (dir->i_sb->s_op != &syaoran_ops)
> + goto ok;
> + /*** SYAORAN start. ***/
> + if (syaoran_may_modify_node(dentry, MAY_DELETE))
> + return -EPERM;
> + /*** SYAORAN end. ***/
> +ok:
> + return simple_unlink(dir, dentry);
> +}
> +
> +static int ramfs_rename(struct inode *old_dir, struct dentry *old_dentry,
> + struct inode *new_dir, struct dentry *new_dentry)
> +{
> + struct inode *inode;
> + if (old_dir->i_sb->s_op != &syaoran_ops)
> + goto ok;
> + /*** SYAORAN start. ***/
> + inode = old_dentry->d_inode;
> + if (!inode || syaoran_may_modify_node(old_dentry, MAY_DELETE) ||
> + syaoran_may_create_node(new_dentry, inode->i_mode, inode->i_rdev))
> + return -EPERM;
> + /*** SYAORAN end. ***/
> +ok:
> + return simple_rename(old_dir, old_dentry, new_dir, new_dentry);
> +}
> +
> +static int ramfs_rmdir(struct inode *dir, struct dentry *dentry)
> +{
> + if (dir->i_sb->s_op != &syaoran_ops)
> + goto ok;
> + /*** SYAORAN start. ***/
> + if (syaoran_may_modify_node(dentry, MAY_DELETE))
> + return -EPERM;
> + /*** SYAORAN end. ***/
> +ok:
> + return simple_rmdir(dir, dentry);
> +}
> +
> +/*
> + * Original tmpfs doesn't set ramfs_dir_inode_operations.setattr field.
> + * Now I'm setting the field to share tmpfs/rootfs/SYAORAN code.
> + * Side effect is that the checking order of notify_change() has changed from
> + * inode_change_ok() -> security_inode_setattr() ->
> + * DQUOT_TRANSFER() -> inode_setattr()
> + * to
> + * security_inode_setattr() -> inode_change_ok() ->
> + * DQUOT_TRANSFER() -> inode_setattr()
> + *
> + * Is this change problematic? If problematic, I'll stop sharing the field.
> + */
> +static int ramfs_setattr(struct dentry *dentry, struct iattr *attr)
> +{
> + unsigned int ia_valid;
> + unsigned int flags = 0;
> + struct inode *inode = dentry->d_inode;
> + int error = inode_change_ok(inode, attr);
> + if (inode->i_sb->s_op != &syaoran_ops)
> + goto ok;
> + /*** SYAORAN start. ***/
> + ia_valid = attr->ia_valid;
> + if (ia_valid & (ATTR_UID | ATTR_GID))
> + flags |= MAY_CHOWN;
> + if (ia_valid & ATTR_MODE)
> + flags |= MAY_CHMOD;
> + if (syaoran_may_modify_node(dentry, flags))
> + return -EPERM;
> + /*** SYAORAN end. ***/
> +ok:
> + if (!error) {
> + if ((ia_valid & ATTR_UID && attr->ia_uid != inode->i_uid) ||
> + (ia_valid & ATTR_GID && attr->ia_gid != inode->i_gid))
> + error = DQUOT_TRANSFER(inode, attr) ? -EDQUOT : 0;
> + if (!error)
> + error = inode_setattr(inode, attr);
> + }
> + return error;
> +}
> +
> +static struct inode_operations syaoran_file_inode_operations = {
> + .getattr = simple_getattr,
> + .setattr = ramfs_setattr,
> +};
> +
> +static struct inode_operations syaoran_symlink_inode_operations = {
> + .readlink = generic_readlink,
> + .follow_link = page_follow_link_light,
> + .put_link = page_put_link,
> + .setattr = ramfs_setattr,
> +};
> +
> +/*
> + * The following codes are used for processing the policy file and
> + * creating initial nodes.
> + */
> +
> +/* lookup_create() without nameidata. Called only while initialization. */
> +static struct dentry *lookup_create2(const char *name, struct dentry *base,
> + const bool is_dir)
> +{
> + struct dentry *dentry;
> + const int len = name ? strlen(name) : 0;
> + mutex_lock(&base->d_inode->i_mutex);
> + dentry = lookup_one_len(name, base, len);
> + if (IS_ERR(dentry))
> + goto fail;
> + if (!is_dir && name[len] && !dentry->d_inode)
> + goto enoent;
> + return dentry;
> +enoent:
> + dput(dentry);
> + dentry = ERR_PTR(-ENOENT);
> +fail:
> + return dentry;
> +}
> +
> +/* mkdir(). Called only while initialization. */
> +static int fs_mkdir(const char *pathname, struct dentry *base, int mode,
> + uid_t user, gid_t group)
> +{
> + struct dentry *dentry = lookup_create2(pathname, base, 1);
> + int error = PTR_ERR(dentry);
> + if (!IS_ERR(dentry)) {
> + error = vfs_mkdir(base->d_inode, dentry, mode);
> + if (!error) {
> + lock_kernel();
> + dentry->d_inode->i_uid = user;
> + dentry->d_inode->i_gid = group;
> + unlock_kernel();
> + }
> + dput(dentry);
> + }
> + mutex_unlock(&base->d_inode->i_mutex);
> + return error;
> +}
> +
> +/* mknod(). Called only while initialization. */
> +static int fs_mknod(const char *filename, struct dentry *base, int mode,
> + dev_t dev, uid_t user, gid_t group)
> +{
> + struct dentry *dentry;
> + int error;
> + switch (mode & S_IFMT) {
> + case S_IFCHR:
> + case S_IFBLK:
> + case S_IFIFO:
> + case S_IFSOCK:
> + case S_IFREG:
> + break;
> + default:
> + return -EPERM;
> + }
> + dentry = lookup_create2(filename, base, 0);
> + error = PTR_ERR(dentry);
> + if (!IS_ERR(dentry)) {
> + error = vfs_mknod(base->d_inode, dentry, mode, dev);
> + if (!error) {
> + lock_kernel();
> + dentry->d_inode->i_uid = user;
> + dentry->d_inode->i_gid = group;
> + unlock_kernel();
> + }
> + dput(dentry);
> + }
> + mutex_unlock(&base->d_inode->i_mutex);
> + return error;
> +}
> +
> +/* symlink(). Called only while initialization. */
> +static int fs_symlink(const char *pathname, struct dentry *base,
> + char *oldname, int mode, uid_t user, gid_t group)
> +{
> + struct dentry *dentry = lookup_create2(pathname, base, 0);
> + int error = PTR_ERR(dentry);
> + if (!IS_ERR(dentry)) {
> + error = vfs_symlink(base->d_inode, dentry, oldname, S_IALLUGO);
> + if (!error) {
> + lock_kernel();
> + dentry->d_inode->i_mode = mode;
> + dentry->d_inode->i_uid = user;
> + dentry->d_inode->i_gid = group;
> + unlock_kernel();
> + }
> + dput(dentry);
> + }
> + mutex_unlock(&base->d_inode->i_mutex);
> + return error;
> +}
> +
> +/*
> + * Format string.
> + * Leading and trailing whitespaces are removed.
> + * Multiple whitespaces are packed into single space.
> + */
> +static void syaoran_normalize_line(unsigned char *buffer)
> +{
> + unsigned char *sp = buffer;
> + unsigned char *dp = buffer;
> + bool first = 1;
> + while (*sp && (*sp <= ' ' || *sp >= 127))
> + sp++;
> + while (*sp) {
> + if (!first)
> + *dp++ = ' ';
> + first = 0;
> + while (*sp > ' ' && *sp < 127)
> + *dp++ = *sp++;
> + while (*sp && (*sp <= ' ' || *sp >= 127))
> + sp++;
> + }
> + *dp = '\0';
> +}
> +
> +/* Convert text form of filename into binary form. */
> +static void syaoran_unescape(char *filename)
> +{
> + char *cp = filename;
> + char c, d, e;
> + if (!cp)
> + return;
> + while ((c = *filename++) != '\0') {
> + if (c != '\\') {
> + *cp++ = c;
> + continue;
> + }
> + if ((c = *filename++) == '\\') {
> + *cp++ = c;
> + continue;
> + }
> + if (c < '0' || c > '3')
> + break;
> + d = *filename++;
> + if (d < '0' || d > '7')
> + break;
> + e = *filename++;
> + if (e < '0' || e > '7')
> + break;
> + *(unsigned char *) cp++ = (unsigned char)
> + (((unsigned char) (c - '0') << 6) +
> + ((unsigned char) (d - '0') << 3) +
> + (unsigned char) (e - '0'));
> + }
> + *cp = '\0';
> +}
> +
> +static inline char *strdup(const char *data)
> +{
> + return kstrdup(data, GFP_KERNEL);
> +}
> +
> +static int register_node_info(char *buffer, struct super_block *sb)
> +{
> + enum {
> + ARG_FILENAME = 0,
> + ARG_PERMISSION = 1,
> + ARG_UID = 2,
> + ARG_GID = 3,
> + ARG_FLAGS = 4,
> + ARG_DEV_TYPE = 5,
> + ARG_SYMLINK_DATA = 6,
> + ARG_DEV_MAJOR = 6,
> + ARG_DEV_MINOR = 7,
> + MAX_ARG = 8
> + };
> + char *args[MAX_ARG];
> + int i;
> + int error = -EINVAL;
> + unsigned int perm, uid, gid, flags, major = 0, minor = 0;
> + struct syaoran_sb_info *info = (struct syaoran_sb_info *) sb->s_fs_info;
> + struct dev_entry *entry;
> + memset(args, 0, sizeof(args));
> + args[0] = buffer;
> + for (i = 1; i < MAX_ARG; i++) {
> + args[i] = strchr(args[i - 1] + 1, ' ');
> + if (!args[i])
> + break;
> + *args[i]++ = '\0';
> + }
> + /*
> + printk("<%s> <%s> <%s> <%s> <%s> <%s> <%s> <%s>\n",
> + args[0], args[1], args[2], args[3], args[4], args[5],
> + args[6], args[7]);
> + */
> + if (!args[ARG_FILENAME] || !args[ARG_PERMISSION] || !args[ARG_UID] ||
> + !args[ARG_GID] || !args[ARG_DEV_TYPE] || !args[ARG_FLAGS])
> + goto out;
> + if (sscanf(args[ARG_PERMISSION], "%o", &perm) != 1 || !(perm <= 0777)
> + || sscanf(args[ARG_UID], "%u", &uid) != 1
> + || sscanf(args[ARG_GID], "%u", &gid) != 1
> + || sscanf(args[ARG_FLAGS], "%u", &flags) != 1
> + || *(args[ARG_DEV_TYPE] + 1))
> + goto out;
> + switch (*args[ARG_DEV_TYPE]) {
> + case 'c':
> + perm |= S_IFCHR;
> + if (!args[ARG_DEV_MAJOR]
> + || sscanf(args[ARG_DEV_MAJOR], "%u", &major) != 1
> + || !args[ARG_DEV_MINOR]
> + || sscanf(args[ARG_DEV_MINOR], "%u", &minor) != 1)
> + goto out;
> + break;
> + case 'b':
> + perm |= S_IFBLK;
> + if (!args[ARG_DEV_MAJOR]
> + || sscanf(args[ARG_DEV_MAJOR], "%u", &major) != 1
> + || !args[ARG_DEV_MINOR]
> + || sscanf(args[ARG_DEV_MINOR], "%u", &minor) != 1)
> + goto out;
> + break;
> + case 'l':
> + perm |= S_IFLNK;
> + if (!args[ARG_SYMLINK_DATA])
> + goto out;
> + break;
> + case 'd':
> + perm |= S_IFDIR;
> + break;
> + case 's':
> + perm |= S_IFSOCK;
> + break;
> + case 'p':
> + perm |= S_IFIFO;
> + break;
> + case 'f':
> + perm |= S_IFREG;
> + break;
> + default:
> + goto out;
> + }
> + error = -ENOMEM;
> + entry = kzalloc(sizeof(*entry), GFP_KERNEL);
> + if (!entry)
> + goto out;
> + if (S_ISLNK(perm)) {
> + entry->printable_symlink_data = strdup(args[ARG_SYMLINK_DATA]);
> + if (!entry->printable_symlink_data)
> + goto out_freemem;
> + }
> + entry->printable_name = strdup(args[ARG_FILENAME]);
> + if (!entry->printable_name)
> + goto out_freemem;
> + if (S_ISLNK(perm)) {
> + entry->symlink_data = strdup(entry->printable_symlink_data);
> + if (!entry->symlink_data)
> + goto out_freemem;
> + syaoran_unescape(entry->symlink_data);
> + }
> + entry->name = strdup(entry->printable_name);
> + if (!entry->name)
> + goto out_freemem;
> + syaoran_unescape(entry->name);
> + /*
> + * Drop trailing '/', for GetLocalAbsolutePath() doesn't append
> + * trailing '/'.
> + */
> + i = strlen(entry->name);
> + if (i && entry->name[i - 1] == '/')
> + entry->name[i - 1] = '\0';
> + entry->mode = perm;
> + entry->uid = uid;
> + entry->gid = gid;
> + entry->kdev = S_ISCHR(perm) || S_ISBLK(perm) ? MKDEV(major, minor) : 0;
> + entry->flags = flags;
> + list_add_tail(&entry->list, &info->list);
> + /* printk("Entry added.\n"); */
> + error = 0;
> +out:
> + return error;
> +out_freemem:
> + kfree(entry->printable_symlink_data);
> + kfree(entry->printable_name);
> + kfree(entry->symlink_data);
> + kfree(entry);
> + goto out;
> +}
> +
> +static int read_config_file(struct file *file, struct super_block *sb)
> +{
> + char *buffer;
> + int error = -ENOMEM;
> + if (!file)
> + return -EINVAL;
> + buffer = kzalloc(PAGE_SIZE, GFP_KERNEL);
> + if (buffer) {
> + int len;
> + char *cp;
> + unsigned long offset = 0;
> + while ((len = kernel_read(file, offset, buffer, PAGE_SIZE)) > 0
> + && (cp = memchr(buffer, '\n', len)) != NULL) {
> + *cp = '\0';
> + offset += cp - buffer + 1;
> + syaoran_normalize_line(buffer);
> + if (register_node_info(buffer, sb) == -ENOMEM)
> + goto out;
> + }
> + error = 0;
> + }
> +out:
> + kfree(buffer);
> + return error;
> +}
> +
> +static void make_node(struct dev_entry *entry, struct dentry *root)
> +{
> + struct dentry *base = dget(root);
> + char *filename = entry->name;
> + char *name = filename;
> + unsigned int c;
> + const mode_t perm = entry->mode;
> + const uid_t uid = entry->uid;
> + const gid_t gid = entry->gid;
> + goto start;
> + while ((c = *(unsigned char *) filename) != '\0') {
> + if (c == '/') {
> + struct dentry *new_base;
> + const int len = filename - name;
> + *filename = '\0';
> + mutex_lock(&base->d_inode->i_mutex);
> + new_base = lookup_one_len(name, base, len);
> + mutex_unlock(&base->d_inode->i_mutex);
> + dput(base);
> + *filename = '/';
> + filename++;
> + if (IS_ERR(new_base))
> + return;
> + if (!new_base->d_inode ||
> + !S_ISDIR(new_base->d_inode->i_mode)) {
> + dput(new_base);
> + return;
> + }
> + base = new_base;
> +start:
> + name = filename;
> + } else {
> + filename++;
> + }
> + }
> + filename = (char *) name;
> + if (S_ISLNK(perm)) {
> + fs_symlink(filename, base, entry->symlink_data, perm, uid, gid);
> + } else if (S_ISDIR(perm)) {
> + fs_mkdir(filename, base, perm ^ S_IFDIR, uid, gid);
> + } else if (S_ISSOCK(perm) || S_ISFIFO(perm) || S_ISREG(perm)) {
> + fs_mknod(filename, base, perm, 0, uid, gid);
> + } else if (S_ISCHR(perm) || S_ISBLK(perm)) {
> + fs_mknod(filename, base, perm, entry->kdev, uid, gid);
> + }
> + dput(base);
> +}
> +
> +/* Create files according to the policy file. */
> +static void syaoran_make_initial_nodes(struct super_block *sb)
> +{
> + struct syaoran_sb_info *info;
> + struct dev_entry *entry;
> + if (!sb)
> + return;
> + info = (struct syaoran_sb_info *) sb->s_fs_info;
> + if (!info)
> + return;
> + if (info->is_permissive_mode) {
> + syaoran_create_tracelog(sb, ".syaoran");
> + syaoran_create_tracelog(sb, ".syaoran_all");
> + }
> + list_for_each_entry(entry, &info->list, list) {
> + if ((entry->flags & NO_CREATE_AT_MOUNT) == 0)
> + make_node(entry, sb->s_root);
> + }
> + info->initialize_done = 1;
> +}
> +
> +/* Read policy file. */
> +static int syaoran_initialize(struct super_block *sb, void *data)
> +{
> + int error = -EINVAL;
> + static bool first = 1;
> + if (first) {
> + first = 0;
> + printk(KERN_INFO "SYAORAN: 1.5.3-pre 2007/12/23\n");
> + }
> + {
> + struct inode *inode = new_inode(sb);
> + if (!inode)
> + return -EINVAL;
> + /* Create /dev/ram0 to get the value of blkdev_open(). */
> + init_special_inode(inode, S_IFBLK | 0666, MKDEV(1, 0));
> + wrapped_def_blk_fops = *inode->i_fop;
> + iput(inode);
> + org_blkdev_open = wrapped_def_blk_fops.open;
> + wrapped_def_blk_fops.open = wrapped_blkdev_open;
> + }
> + {
> + struct inode *inode = new_inode(sb);
> + if (!inode)
> + return -EINVAL;
> + /* Create /dev/null to get the value of chrdev_open(). */
> + init_special_inode(inode, S_IFCHR | 0666, MKDEV(1, 3));
> + wrapped_def_chr_fops = *inode->i_fop;
> + iput(inode);
> + org_chrdev_open = wrapped_def_chr_fops.open;
> + wrapped_def_chr_fops.open = wrapped_chrdev_open;
> + }
> + if (data) {
> + struct file *f;
> + char *filename = (char *) data;
> + bool is_permissive_mode = 0;
> + if (strncmp(filename, "accept=", 7) == 0) {
> + filename += 7;
> + is_permissive_mode = 1;
> + } else if (strncmp(filename, "enforce=", 8) == 0) {
> + filename += 8;
> + is_permissive_mode = 0;
> + } else {
> + printk(KERN_INFO
> + "SYAORAN: Missing 'accept=' or 'enforce='.\n");
> + return -EINVAL;
> + }
> + f = open_pathname(AT_FDCWD, filename, O_RDONLY, 0600);
> + if (!IS_ERR(f)) {
> + struct syaoran_sb_info *p;
> + if (!S_ISREG(f->f_dentry->d_inode->i_mode))
> + goto out;
> + p = kzalloc(sizeof(*p), GFP_KERNEL);
> + if (!p)
> + goto out;
> + p->is_permissive_mode = is_permissive_mode;
> + sb->s_fs_info = p;
> + INIT_LIST_HEAD(&((struct syaoran_sb_info *)
> + sb->s_fs_info)->list);
> + printk(KERN_INFO "SYAORAN: Reading '%s'\n", filename);
> + error = read_config_file(f, sb);
> +out:
> + if (error)
> + printk(KERN_INFO "SYAORAN: Can't read '%s'\n",
> + filename);
> + filp_close(f, NULL);
> + } else {
> + printk(KERN_INFO "SYAORAN: Can't open '%s'\n",
> + filename);
> + }
> + } else {
> + printk(KERN_INFO "SYAORAN: Missing config-file path.\n");
> + }
> + return error;
> +}
> +
> +/*
> + * The following structure and codes are used for transferring data
> + * to interfaces files.
> + */
> +
> +struct syaoran_read_struct {
> + char *buf; /* Buffer for reading. */
> + int avail; /* Bytes available for reading. */
> + struct super_block *sb; /* The super_block of this partition. */
> + struct dev_entry *entry; /* The entry currently reading from. */
> + _Bool read_all; /* Dump all entries? */
> + struct list_head *pos; /* Current position. */
> +};
> +
> +static void syaoran_read_table(struct syaoran_read_struct *head, char *buf,
> + int count)
> +{
> + struct super_block *sb = head->sb;
> + struct syaoran_sb_info *info =
> + (struct syaoran_sb_info *) sb->s_fs_info;
> + struct list_head *pos;
> + const _Bool read_all = head->read_all;
> + if (!info)
> + return;
> + if (!head->pos)
> + return;
> + list_for_each_cookie(pos, head->pos, &info->list) {
> + struct dev_entry *entry =
> + list_entry(pos, struct dev_entry, list);
> + const unsigned int flags =
> + read_all ? entry->flags : entry->flags & ~DEVICE_USED;
> + const char *name = entry->printable_name;
> + const uid_t uid = entry->uid;
> + const gid_t gid = entry->gid;
> + const mode_t perm = entry->mode & 0777;
> + int len = 0;
> + switch (entry->mode & S_IFMT) {
> + case S_IFCHR:
> + if (!head->read_all && !(entry->flags & DEVICE_USED))
> + break;
> + len = snprintf(buf, count,
> + "%-20s %3o %3u %3u %2u %c %3u %3u\n",
> + name, perm, uid, gid, flags, 'c',
> + MAJOR(entry->kdev), MINOR(entry->kdev));
> + break;
> + case S_IFBLK:
> + if (!head->read_all && !(entry->flags & DEVICE_USED))
> + break;
> + len = snprintf(buf, count,
> + "%-20s %3o %3u %3u %2u %c %3u %3u\n",
> + name, perm, uid, gid, flags, 'b',
> + MAJOR(entry->kdev), MINOR(entry->kdev));
> + break;
> + case S_IFIFO:
> + len = snprintf(buf, count,
> + "%-20s %3o %3u %3u %2u %c\n",
> + name, perm, uid, gid, flags, 'p');
> + break;
> + case S_IFSOCK:
> + len = snprintf(buf, count,
> + "%-20s %3o %3u %3u %2u %c\n",
> + name, perm, uid, gid, flags, 's');
> + break;
> + case S_IFDIR:
> + len = snprintf(buf, count,
> + "%-20s %3o %3u %3u %2u %c\n",
> + name, perm, uid, gid, flags, 'd');
> + break;
> + case S_IFLNK:
> + len = snprintf(buf, count,
> + "%-20s %3o %3u %3u %2u %c %s\n",
> + name, perm, uid, gid, flags, 'l',
> + entry->printable_symlink_data);
> + break;
> + case S_IFREG:
> + len = snprintf(buf, count,
> + "%-20s %3o %3u %3u %2u %c\n",
> + name, perm, uid, gid, flags, 'f');
> + break;
> + }
> + if (len < 0 || count <= len)
> + break;
> + count -= len;
> + buf += len;
> + head->avail += len;
> + }
> +}
> +
> +static int syaoran_trace_open(struct inode *inode, struct file *file)
> +{
> + struct syaoran_read_struct *head =
> + kzalloc(sizeof(*head), GFP_KERNEL);
> + if (!head)
> + return -ENOMEM;
> + head->sb = inode->i_sb;
> + head->read_all =
> + (strcmp(file->f_dentry->d_name.name, ".syaoran_all") == 0);
> + head->pos = &((struct syaoran_sb_info *) head->sb->s_fs_info)->list;
> + head->buf = kzalloc(PAGE_SIZE * 2, GFP_KERNEL);
> + if (!head->buf) {
> + kfree(head);
> + return -ENOMEM;
> + }
> + file->private_data = head;
> + return 0;
> +}
> +
> +static int syaoran_trace_release(struct inode *inode, struct file *file)
> +{
> + struct syaoran_read_struct *head = file->private_data;
> + kfree(head->buf);
> + kfree(head);
> + file->private_data = NULL;
> + return 0;
> +}
> +
> +static ssize_t syaoran_trace_read(struct file *file, char __user *buf,
> + size_t count, loff_t *ppos)
> +{
> + struct syaoran_read_struct *head =
> + (struct syaoran_read_struct *) file->private_data;
> + int len = head->avail;
> + char *cp = head->buf;
> + if (!access_ok(VERIFY_WRITE, buf, count))
> + return -EFAULT;
> + syaoran_read_table(head, cp + len, PAGE_SIZE * 2 - len);
> + len = head->avail;
> + if (len > count)
> + len = count;
> + if (len > 0) {
> + if (copy_to_user(buf, cp, len))
> + return -EFAULT;
> + head->avail -= len;
> + memmove(cp, cp + len, head->avail);
> + }
> + return len;
> +}
> +
> +static struct file_operations syaoran_trace_operations = {
> + .open = syaoran_trace_open,
> + .release = syaoran_trace_release,
> + .read = syaoran_trace_read,
> +};
> +
> +/* Create interface files for reading status. */
> +static int syaoran_create_tracelog(struct super_block *sb, const char *filename)
> +{
> + struct inode *inode;
> + struct dentry *base = dget(sb->s_root);
> + struct dentry *dentry = lookup_create2(filename, base, 0);
> + int error = PTR_ERR(dentry);
> + if (IS_ERR(dentry))
> + goto out;
> + inode = syaoran_get_inode(sb, S_IFREG | 0400, 0);
> + if (!inode)
> + error = -ENOSPC;
> + else {
> + /* Override file operation. */
> + inode->i_fop = &syaoran_trace_operations;
> + d_instantiate(dentry, inode);
> + dget(dentry); /* Extra count - pin the dentry in core */
> + error = 0;
> + }
> + dput(dentry);
> +out:
> + mutex_unlock(&base->d_inode->i_mutex);
> + dput(base);
> + return error;
> +}
> -
> To unsubscribe from this list: send the line "unsubscribe linux-fsdevel" in
> the body of a message to majordomo@...r.kernel.org
> More majordomo info at http://vger.kernel.org/majordomo-info.html
--
To unsubscribe from this list: send the line "unsubscribe linux-kernel" in
the body of a message to majordomo@...r.kernel.org
More majordomo info at http://vger.kernel.org/majordomo-info.html
Please read the FAQ at http://www.tux.org/lkml/
Powered by blists - more mailing lists