From: Erez Zadok <ezk@xxxxxxxxxxxxx> Signed-off-by: Erez Zadok <ezk@xxxxxxxxxxxxx> Signed-off-by: Josef 'Jeff' Sipek <jsipek@xxxxxxxxxxxxx> --- fs/unionfs/main.c | 52 +++-- fs/unionfs/super.c | 612 +++++++++++++++++++++++++++++++++++++++++++++++++++- fs/unionfs/union.h | 6 + 3 files changed, 646 insertions(+), 24 deletions(-) diff --git a/fs/unionfs/main.c b/fs/unionfs/main.c index ed264c7..1c93b13 100644 --- a/fs/unionfs/main.c +++ b/fs/unionfs/main.c @@ -181,7 +181,7 @@ void unionfs_reinterpose(struct dentry *dentry) * 2) it exists * 3) is a directory */ -static int check_branch(struct nameidata *nd) +int check_branch(struct nameidata *nd) { if (!strcmp(nd->dentry->d_sb->s_type->name, "unionfs")) return -EINVAL; @@ -211,20 +211,29 @@ static int is_branch_overlap(struct dentry *dent1, struct dentry *dent2) return (dent == dent1); } -/* parse branch mode */ -static int parse_branch_mode(char *name) +/* + * Parse branch mode helper function + */ +int __parse_branch_mode(const char *name) { - int perms; - int l = strlen(name); - if (!strcmp(name + l - 3, "=ro")) { - perms = MAY_READ; - name[l - 3] = '\0'; - } else if (!strcmp(name + l - 3, "=rw")) { - perms = MAY_READ | MAY_WRITE; - name[l - 3] = '\0'; - } else - perms = MAY_READ | MAY_WRITE; + if (!name) + return 0; + if (!strcmp(name, "ro")) + return MAY_READ; + if (!strcmp(name, "rw")) + return (MAY_READ | MAY_WRITE); + return 0; +} +/* + * Parse "ro" or "rw" options, but default to "rw" of no mode options + * was specified. + */ +int parse_branch_mode(const char *name) +{ + int perms = __parse_branch_mode(name); + if (perms == 0) + perms = MAY_READ | MAY_WRITE; return perms; } @@ -271,18 +280,22 @@ static int parse_dirs_option(struct super_block *sb, struct unionfs_dentry_info goto out; } - /* now parsing the string b1:b2=rw:b3=ro:b4 */ + /* now parsing a string such as "b1:b2=rw:b3=ro:b4" */ branches = 0; while ((name = strsep(&options, ":")) != NULL) { int perms; + char *mode = strchr(name, '='); - if (!*name) + if (!name || !*name) continue; branches++; - /* strip off =rw or =ro if it is specified. */ - perms = parse_branch_mode(name); + /* strip off '=' if any */ + if (mode) + *mode++ = '\0'; + + perms = parse_branch_mode(mode); if (!bindex && !(perms & MAY_WRITE)) { err = -EINVAL; goto out; @@ -305,8 +318,11 @@ static int parse_dirs_option(struct super_block *sb, struct unionfs_dentry_info hidden_root_info->lower_paths[bindex].dentry = nd.dentry; hidden_root_info->lower_paths[bindex].mnt = nd.mnt; + unionfs_write_lock(sb); set_branchperms(sb, bindex, perms); set_branch_count(sb, bindex, 0); + new_branch_id(sb, bindex); + unionfs_write_unlock(sb); if (hidden_root_info->bstart < 0) hidden_root_info->bstart = bindex; @@ -387,7 +403,7 @@ static struct unionfs_dentry_info *unionfs_parse_options(struct super_block *sb, char *endptr; int intval; - if (!*optname) + if (!optname || !*optname) continue; optarg = strchr(optname, '='); diff --git a/fs/unionfs/super.c b/fs/unionfs/super.c index 037c47d..7f0d174 100644 --- a/fs/unionfs/super.c +++ b/fs/unionfs/super.c @@ -143,14 +143,614 @@ static int unionfs_statfs(struct dentry *dentry, struct kstatfs *buf) return err; } -/* We don't support a standard text remount. Eventually it would be nice to - * support a full-on remount, so that you can have all of the directories - * change at once, but that would require some pretty complicated matching - * code. +/* handle mode changing during remount */ +static int do_remount_mode_option(char *optarg, int cur_branches, + struct unionfs_data *new_data, + struct path *new_lower_paths) +{ + int err = -EINVAL; + int perms, idx; + char *modename = strchr(optarg, '='); + struct nameidata nd; + + /* by now, optarg contains the branch name */ + if (!*optarg) { + printk("unionfs: no branch specified for mode change.\n"); + goto out; + } + if (!modename) { + printk("unionfs: branch \"%s\" requires a mode.\n", optarg); + goto out; + } + *modename++ = '\0'; + perms = __parse_branch_mode(modename); + if (perms == 0) { + printk("unionfs: invalid mode \"%s\" for \"%s\".\n", + modename, optarg); + goto out; + } + + /* + * Find matching branch index. For now, this assumes that nothing + * has been mounted on top of this Unionfs stack. Once we have /odf + * and cache-coherency resolved, we'll address the branch-path + * uniqueness. + */ + err = path_lookup(optarg, LOOKUP_FOLLOW, &nd); + if (err) { + printk(KERN_WARNING "unionfs: error accessing " + "hidden directory \"%s\" (error %d)\n", + optarg, err); + goto out; + } + for (idx=0; idx<cur_branches; idx++) + if (nd.mnt == new_lower_paths[idx].mnt && + nd.dentry == new_lower_paths[idx].dentry) + break; + path_release(&nd); /* no longer needed */ + if (idx == cur_branches) { + err = -ENOENT; /* err may have been reset above */ + printk(KERN_WARNING "unionfs: branch \"%s\" " + "not found\n", optarg); + goto out; + } + /* check/change mode for existing branch */ + /* we don't warn if perms==branchperms */ + new_data[idx].branchperms = perms; + err = 0; +out: + return err; +} + +/* handle branch deletion during remount */ +static int do_remount_del_option(char *optarg, int cur_branches, + struct unionfs_data *new_data, + struct path *new_lower_paths) +{ + int err = -EINVAL; + int idx; + struct nameidata nd; + /* optarg contains the branch name to delete */ + + /* + * Find matching branch index. For now, this assumes that nothing + * has been mounted on top of this Unionfs stack. Once we have /odf + * and cache-coherency resolved, we'll address the branch-path + * uniqueness. + */ + err = path_lookup(optarg, LOOKUP_FOLLOW, &nd); + if (err) { + printk(KERN_WARNING "unionfs: error accessing " + "hidden directory \"%s\" (error %d)\n", + optarg, err); + goto out; + } + for (idx=0; idx < cur_branches; idx++) + if (nd.mnt == new_lower_paths[idx].mnt && + nd.dentry == new_lower_paths[idx].dentry) + break; + path_release(&nd); /* no longer needed */ + if (idx == cur_branches) { + printk(KERN_WARNING "unionfs: branch \"%s\" " + "not found\n", optarg); + err = -ENOENT; + goto out; + } + /* check if there are any open files on the branch to be deleted */ + if (atomic_read(&new_data[idx].open_files) > 0) { + err = -EBUSY; + goto out; + } + + /* + * Now we have to delete the branch. First, release any handles it + * has. Then, move the remaining array indexes past "idx" in + * new_data and new_lower_paths one to the left. Finally, adjust + * cur_branches. + */ + pathput(&new_lower_paths[idx]); + + if (idx < cur_branches - 1) { + /* if idx==cur_branches-1, we delete last branch: easy */ + memmove(&new_data[idx], &new_data[idx+1], + (cur_branches - 1 - idx) * sizeof(struct unionfs_data)); + memmove(&new_lower_paths[idx], &new_lower_paths[idx+1], + (cur_branches - 1 - idx) * sizeof(struct path)); + } + + err = 0; +out: + return err; +} + +/* handle branch insertion during remount */ +static int do_remount_add_option(char *optarg, int cur_branches, + struct unionfs_data *new_data, + struct path *new_lower_paths, + int *high_branch_id) +{ + int err = -EINVAL; + int perms; + int idx = 0; /* default: insert at beginning */ + char *new_branch , *modename = NULL; + struct nameidata nd; + + /* + * optarg can be of several forms: + * + * /bar:/foo insert /foo before /bar + * /bar:/foo=ro insert /foo in ro mode before /bar + * /foo insert /foo in the beginning (prepend) + * :/foo insert /foo at the end (append) + */ + if (*optarg == ':') { /* append? */ + new_branch = optarg + 1; /* skip ':' */ + idx = cur_branches; + goto found_insertion_point; + } + new_branch = strchr(optarg, ':'); + if (!new_branch) { /* prepend? */ + new_branch = optarg; + goto found_insertion_point; + } + *new_branch++ = '\0'; /* holds path+mode of new branch */ + + /* + * Find matching branch index. For now, this assumes that nothing + * has been mounted on top of this Unionfs stack. Once we have /odf + * and cache-coherency resolved, we'll address the branch-path + * uniqueness. + */ + err = path_lookup(optarg, LOOKUP_FOLLOW, &nd); + if (err) { + printk(KERN_WARNING "unionfs: error accessing " + "hidden directory \"%s\" (error %d)\n", + optarg, err); + goto out; + } + for (idx=0; idx < cur_branches; idx++) + if (nd.mnt == new_lower_paths[idx].mnt && + nd.dentry == new_lower_paths[idx].dentry) + break; + path_release(&nd); /* no longer needed */ + if (idx == cur_branches) { + printk(KERN_WARNING "unionfs: branch \"%s\" " + "not found\n", optarg); + err = -ENOENT; + goto out; + } + + /* + * At this point idx will hold the index where the new branch should + * be inserted before. + */ +found_insertion_point: + /* find the mode for the new branch */ + if (new_branch) + modename = strchr(new_branch, '='); + if (modename) + *modename++ = '\0'; + perms = parse_branch_mode(modename); + + if (!new_branch || !*new_branch) { + printk(KERN_WARNING "unionfs: null new branch\n"); + err = -EINVAL; + goto out; + } + err = path_lookup(new_branch, LOOKUP_FOLLOW, &nd); + if (err) { + printk(KERN_WARNING "unionfs: error accessing " + "hidden directory \"%s\" (error %d)\n", + new_branch, err); + goto out; + } + /* it's probably safe to check_mode the new branch to insert */ + if ((err = check_branch(&nd))) { + printk(KERN_WARNING "unionfs: hidden directory " + "\"%s\" is not a valid branch\n", optarg); + path_release(&nd); + goto out; + } + + /* + * Now we have to insert the new branch. But first, move the bits + * to make space for the new branch, if needed. Finally, adjust + * cur_branches. + * We don't release nd here; it's kept until umount/remount. + */ + if (idx < cur_branches) { + /* if idx==cur_branches, we append: easy */ + memmove(&new_data[idx+1], &new_data[idx], + (cur_branches - idx) * sizeof(struct unionfs_data)); + memmove(&new_lower_paths[idx+1], &new_lower_paths[idx], + (cur_branches - idx) * sizeof(struct path)); + } + new_lower_paths[idx].dentry = nd.dentry; + new_lower_paths[idx].mnt = nd.mnt; + + new_data[idx].sb = nd.dentry->d_sb; + atomic_set(&new_data[idx].open_files, 0); + new_data[idx].branchperms = perms; + new_data[idx].branch_id = ++*high_branch_id; /* assign new branch ID */ + + err = 0; +out: + return err; +} + + +/* + * Support branch management options on remount. + * + * See Documentation/filesystems/unionfs/ for details. + * + * @flags: numeric mount options + * @options: mount options string + * + * This function can rearrange a mounted union dynamically, adding and + * removing branches, including changing branch modes. Clearly this has to + * be done safely and atomically. Luckily, the VFS already calls this + * function with lock_super(sb) and lock_kernel() held, preventing + * concurrent mixing of new mounts, remounts, and unmounts. Moreover, + * do_remount_sb(), our caller function, already called shrink_dcache_sb(sb) + * to purge dentries/inodes from our superblock, and also called + * fsync_super(sb) to purge any dirty pages. So we're good. + * + * XXX: however, our remount code may also need to invalidate mapped pages + * so as to force them to be re-gotten from the (newly reconfigured) lower + * branches. This has to wait for proper mmap and cache coherency support + * in the VFS. + * */ -static int unionfs_remount_fs(struct super_block *sb, int *flags, char *data) +static int unionfs_remount_fs(struct super_block *sb, int *flags, + char *options) { - return -ENOSYS; + int err = 0; + int i; + char *optionstmp, *tmp_to_free; /* kstrdup'ed of "options" */ + char *optname; + int cur_branches; /* no. of current branches */ + int new_branches; /* no. of branches actually left in the end */ + int add_branches; /* est. no. of branches to add */ + int del_branches; /* est. no. of branches to del */ + int max_branches; /* max possible no. of branches */ + struct unionfs_data *new_data = NULL, *tmp_data = NULL; + struct path *new_lower_paths = NULL, *tmp_lower_paths = NULL; + int new_high_branch_id; /* new high branch ID */ + + unionfs_write_lock(sb); + + /* + * The VFS will take care of "ro" and "rw" flags, so anything else + * is an error. So we need to check if any other flags may have + * been passed (none are allowed/supported as of now). + */ + if ((*flags & ~MS_RDONLY) != 0) { + printk(KERN_WARNING + "unionfs: remount flags 0x%x unsupported\n", *flags); + err = -EINVAL; + goto out_error; + } + + /* + * If 'options' is NULL, it's probably because the user just changed + * the union to a "ro" or "rw" and the VFS took care of it. So + * nothing to do and we're done. + */ + if (options[0] == '\0') + goto out_error; + + /* + * Find out how many branches we will have in the end, counting + * "add" and "del" commands. Copy the "options" string because + * strsep modifies the string and we need it later. + */ + optionstmp = tmp_to_free = kstrdup(options, GFP_KERNEL); + if (!optionstmp) { + err = -ENOMEM; + goto out_free; + } + new_branches = cur_branches = sbmax(sb); /* current no. branches */ + add_branches = del_branches = 0; + new_high_branch_id = sbhbid(sb); /* save current high_branch_id */ + while ((optname = strsep(&optionstmp, ",")) != NULL) { + char *optarg; + + if (!optname || !*optname) + continue; + + optarg = strchr(optname, '='); + if (optarg) + *optarg++ = '\0'; + + if (!strcmp("add", optname)) + add_branches++; + else if (!strcmp("del", optname)) + del_branches++; + } + kfree(tmp_to_free); + /* after all changes, will we have at least one branch left? */ + if ((new_branches + add_branches - del_branches) < 1) { + printk(KERN_WARNING + "unionfs: no branches left after remount\n"); + err = -EINVAL; + goto out_free; + } + + /* + * Since we haven't actually parsed all the add/del options, nor + * have we checked them for errors, we don't know for sure how many + * branches we will have after all changes have taken place. In + * fact, the total number of branches left could be less than what + * we have now. So we need to allocate space for a temporary + * placeholder that is at least as large as the maximum number of + * branches we *could* have, which is the current number plus all + * the additions. Once we're done with these temp placeholders, we + * may have to re-allocate the final size, copy over from the temp, + * and then free the temps (done near the end of this function). + */ + max_branches = cur_branches + add_branches; + /* allocate space for new pointers to hidden dentry */ + tmp_data = kcalloc(max_branches, + sizeof(struct unionfs_data), GFP_KERNEL); + if (!tmp_data) { + err = -ENOMEM; + goto out_free; + } + /* allocate space for new pointers to lower paths */ + tmp_lower_paths = kcalloc(max_branches, + sizeof(struct path), GFP_KERNEL); + if (!tmp_lower_paths) { + err = -ENOMEM; + goto out_free; + } + /* copy current info into new placeholders, incrementing refcnts */ + memcpy(tmp_data, UNIONFS_SB(sb)->data, + cur_branches * sizeof(struct unionfs_data)); + memcpy(tmp_lower_paths, UNIONFS_D(sb->s_root)->lower_paths, + cur_branches * sizeof(struct path)); + for (i=0; i<cur_branches; i++) + pathget(&tmp_lower_paths[i]); /* drop refs at end of fxn */ + + /******************************************************************* + * For each branch command, do path_lookup on the requested branch, + * and apply the change to a temp branch list. To handle errors, we + * already dup'ed the old arrays (above), and increased the refcnts + * on various f/s objects. So now we can do all the path_lookups + * and branch-management commands on the new arrays. If it fail mid + * way, we free the tmp arrays and *put all objects. If we succeed, + * then we free old arrays and *put its objects, and then replace + * the arrays with the new tmp list (we may have to re-allocate the + * memory because the temp lists could have been larger than what we + * actually needed). + *******************************************************************/ + + while ((optname = strsep(&options, ",")) != NULL) { + char *optarg; + + if (!optname || !*optname) + continue; + /* + * At this stage optname holds a comma-delimited option, but + * without the commas. Next, we need to break the string on + * the '=' symbol to separate CMD=ARG, where ARG itself can + * be KEY=VAL. For example, in mode=/foo=rw, CMD is "mode", + * KEY is "/foo", and VAL is "rw". + */ + optarg = strchr(optname, '='); + if (optarg) + *optarg++ = '\0'; + /* incgen remount option (instead of old ioctl) */ + if (!strcmp("incgen", optname)) { + err = 0; + goto out_no_change; + } + + /* + * All of our options take an argument now. (Insert ones + * that don't above this check.) So at this stage optname + * contains the CMD part and optarg contains the ARG part. + */ + if (!optarg || !*optarg) { + printk("unionfs: all remount options require " + "an argument (%s).\n", optname); + err = -EINVAL; + goto out_release; + } + + if (!strcmp("add", optname)) { + err = do_remount_add_option(optarg, new_branches, + tmp_data, + tmp_lower_paths, + &new_high_branch_id); + if (err) + goto out_release; + new_branches++; + if (new_branches > UNIONFS_MAX_BRANCHES) { + printk("unionfs: command exceeds %d branches\n", + UNIONFS_MAX_BRANCHES); + err = -E2BIG; + goto out_release; + } + continue; + } + if (!strcmp("del", optname)) { + err = do_remount_del_option(optarg, new_branches, + tmp_data, + tmp_lower_paths); + if (err) + goto out_release; + new_branches--; + continue; + } + if (!strcmp("mode", optname)) { + err = do_remount_mode_option(optarg, new_branches, + tmp_data, + tmp_lower_paths); + if (err) + goto out_release; + continue; + } + + /* + * When you use "mount -o remount,ro", mount(8) will + * reportedly pass the original dirs= string from + * /proc/mounts. So for now, we have to ignore dirs= and + * not consider it an error, unless we want to allow users + * to pass dirs= in remount. Note that to allow the VFS to + * actually process the ro/rw remount options, we have to + * return 0 from this function. + */ + if (!strcmp("dirs", optname)) { + printk(KERN_WARNING + "unionfs: remount ignoring option \"%s\".\n", + optname); + continue; + } + + err = -EINVAL; + printk(KERN_WARNING + "unionfs: unrecognized option \"%s\"\n", optname); + goto out_release; + } + +out_no_change: + + /****************************************************************** + * WE'RE ALMOST DONE: see if we need to allocate a small-sized new + * vector, copy the vectors to their correct place, release the + * refcnt of the older ones, and return. + * Also handle invalidating any pgaes that will have to be re-read. + *******************************************************************/ + + /* + * Allocate space for actual pointers, if needed. By the time we + * finish this block of code, new_branches and new_lower_paths will + * have the correct size. None of this code below would be needed + * if the kernel had a realloc() function, at least one capable of + * shrinking/truncating an allocation's size (hint, hint). + */ + if (new_branches < max_branches) { + + /* allocate space for new pointers to hidden dentry */ + new_data = kcalloc(new_branches, + sizeof(struct unionfs_data), GFP_KERNEL); + if (!new_data) { + err = -ENOMEM; + goto out_release; + } + /* allocate space for new pointers to lower paths */ + new_lower_paths = kcalloc(new_branches, + sizeof(struct path), GFP_KERNEL); + if (!new_lower_paths) { + err = -ENOMEM; + goto out_release; + } + /* + * copy current info into new placeholders, incrementing + * refcounts. + */ + memcpy(new_data, tmp_data, + new_branches * sizeof(struct unionfs_data)); + memcpy(new_lower_paths, tmp_lower_paths, + new_branches * sizeof(struct path)); + /* + * Since we already hold various refcnts on the objects, we + * don't need to redo it here. Just free the older memory + * and re-point the pointers. + */ + kfree(tmp_data); + kfree(tmp_lower_paths); + /* no need to nullify pointers here */ + } else { + /* number of branches didn't change, no need to re-alloc */ + new_data = tmp_data; + new_lower_paths = tmp_lower_paths; + } + + /* + * OK, just before we actually put the new set of branches in place, + * we need to ensure that our own f/s has no dirty objects left. + * Luckily, do_remount_sb() already calls shrink_dcache_sb(sb) and + * fsync_super(sb), taking care of dentries, inodes, and dirty + * pages. So all that's left is for us to invalidate any leftover + * (non-dirty) pages to ensure that they will be re-read from the + * new lower branches (and to support mmap). + */ + + /* + * No we call drop_pagecache_sb() to invalidate all pages in this + * super. This function calls invalidate_inode_pages(mapping), + * which calls invalidate_mapping_pages(): the latter, however, will + * not invalidate pages which are dirty, locked, under writeback, or + * mapped into pagetables. We shouldn't have to worry about dirty + * or under-writeback pages, because do_remount_sb() called + * fsync_super() which would not have returned until all dirty pages + * were flushed. + * + * But do w have to worry about locked pages? Is there any chance + * that in here we'll get locked pages? + * + * XXX: what about pages mapped into pagetables? Are these pages + * which user processes may have mmap(2)'ed? If so, then we need to + * invalidate those too, no? Maybe we'll have to write our own + * version of invalidate_mapping_pages() which also handled mapped + * pages. + * + * XXX: Alternatively, maybe we should call truncate_inode_pages(), + * which use two passes over the pages list, and will truncate all + * pages. + */ + drop_pagecache_sb(sb); + + /* copy new vectors into their correct place */ + tmp_data = UNIONFS_SB(sb)->data; + UNIONFS_SB(sb)->data = new_data; + new_data = NULL; /* so don't free good pointers below */ + tmp_lower_paths = UNIONFS_D(sb->s_root)->lower_paths; + UNIONFS_D(sb->s_root)->lower_paths = new_lower_paths; + new_lower_paths = NULL; /* so don't free good pointers below */ + + /* update our unionfs_sb_info and root dentry index of last branch */ + i = sbmax(sb); /* save no. of branches to release at end */ + sbend(sb) = new_branches - 1; + set_dbend(sb->s_root, new_branches - 1); + UNIONFS_D(sb->s_root)->bcount = new_branches; + new_branches = i; /* no. of branches to release below */ + + /* maxbytes may have changed */ + sb->s_maxbytes = unionfs_lower_super_idx(sb, 0)->s_maxbytes; + /* update high branch ID */ + sbhbid(sb) = new_high_branch_id; + + /* update our sb->generation for revalidating objects */ + i = atomic_inc_return(&UNIONFS_SB(sb)->generation); + atomic_set(&UNIONFS_D(sb->s_root)->generation, i); + atomic_set(&UNIONFS_I(sb->s_root->d_inode)->generation, i); + printk("unionfs: new generation number %d\n", i); + err = 0; /* reset to success */ + + /* + * The code above falls through to the next label, and releases the + * refcnts of the older ones (stored in tmp_*): if we fell through + * here, it means success. However, if we jump directly to this + * label from any error above, then an error occurred after we + * grabbed various refcnts, and so we have to release the + * temporarily constructed structures. + */ +out_release: + /* no need to cleanup/release anything in tmp_data */ + if (tmp_lower_paths) + for (i=0; i<new_branches; i++) + pathput(&tmp_lower_paths[i]); +out_free: + kfree(tmp_lower_paths); + kfree(tmp_data); + kfree(new_lower_paths); + kfree(new_data); +out_error: + unionfs_write_unlock(sb); + return err; } /* diff --git a/fs/unionfs/union.h b/fs/unionfs/union.h index 7bd6306..715a3ad 100644 --- a/fs/unionfs/union.h +++ b/fs/unionfs/union.h @@ -60,6 +60,9 @@ /* number of times we try to get a unique temporary file name */ #define GET_TMPNAM_MAX_RETRY 5 +/* maximum number of branches we support, to avoid memory blowup */ +#define UNIONFS_MAX_BRANCHES 128 + /* Operations vectors defined in specific files. */ extern struct file_operations unionfs_main_fops; extern struct file_operations unionfs_dir_fops; @@ -408,6 +411,9 @@ static inline int is_robranch(const struct dentry *dentry) * EXTERNALS: */ extern char *alloc_whname(const char *name, int len); +extern int check_branch(struct nameidata *nd); +extern int __parse_branch_mode(const char *name); +extern int parse_branch_mode(const char *name); /* These two functions are here because it is kind of daft to copy and paste the * contents of the two functions to 32+ places in unionfs -- 1.5.0.3.268.g3dda - To unsubscribe from this list: send the line "unsubscribe linux-fsdevel" in the body of a message to majordomo@xxxxxxxxxxxxxxx More majordomo info at http://vger.kernel.org/majordomo-info.html