blob: ed8fbad94fff3e02da52713033e2e21a6bfd42b7 [file] [log] [blame]
/*
*
* Copyright (C) 2011 Novell Inc.
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 as published by
* the Free Software Foundation.
*/
#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/namei.h>
#include <linux/file.h>
#include <linux/xattr.h>
#include <linux/rbtree.h>
#include <linux/security.h>
#include "overlayfs.h"
struct ovl_cache_entry {
const char *name;
unsigned int len;
unsigned int type;
u64 ino;
bool is_whiteout;
struct list_head l_node;
struct rb_node node;
};
struct ovl_readdir_data {
struct rb_root *root;
struct list_head *list;
struct list_head *middle;
struct dentry *dir;
int count;
int err;
};
struct ovl_dir_file {
bool is_real;
bool is_cached;
struct list_head cursor;
u64 cache_version;
struct list_head cache;
struct file *realfile;
};
static struct ovl_cache_entry *ovl_cache_entry_from_node(struct rb_node *n)
{
return container_of(n, struct ovl_cache_entry, node);
}
static struct ovl_cache_entry *ovl_cache_entry_find(struct rb_root *root,
const char *name, int len)
{
struct rb_node *node = root->rb_node;
int cmp;
while (node) {
struct ovl_cache_entry *p = ovl_cache_entry_from_node(node);
cmp = strncmp(name, p->name, len);
if (cmp > 0)
node = p->node.rb_right;
else if (cmp < 0 || len < p->len)
node = p->node.rb_left;
else
return p;
}
return NULL;
}
static struct ovl_cache_entry *ovl_cache_entry_new(const char *name, int len,
u64 ino, unsigned int d_type)
{
struct ovl_cache_entry *p;
p = kmalloc(sizeof(*p) + len + 1, GFP_KERNEL);
if (p) {
char *name_copy = (char *) (p + 1);
memcpy(name_copy, name, len);
name_copy[len] = '\0';
p->name = name_copy;
p->len = len;
p->type = d_type;
p->ino = ino;
p->is_whiteout = false;
}
return p;
}
static int ovl_cache_entry_add_rb(struct ovl_readdir_data *rdd,
const char *name, int len, u64 ino,
unsigned int d_type)
{
struct rb_node **newp = &rdd->root->rb_node;
struct rb_node *parent = NULL;
struct ovl_cache_entry *p;
while (*newp) {
int cmp;
struct ovl_cache_entry *tmp;
parent = *newp;
tmp = ovl_cache_entry_from_node(*newp);
cmp = strncmp(name, tmp->name, len);
if (cmp > 0)
newp = &tmp->node.rb_right;
else if (cmp < 0 || len < tmp->len)
newp = &tmp->node.rb_left;
else
return 0;
}
p = ovl_cache_entry_new(name, len, ino, d_type);
if (p == NULL)
return -ENOMEM;
list_add_tail(&p->l_node, rdd->list);
rb_link_node(&p->node, parent, newp);
rb_insert_color(&p->node, rdd->root);
return 0;
}
static int ovl_fill_lower(void *buf, const char *name, int namelen,
loff_t offset, u64 ino, unsigned int d_type)
{
struct ovl_readdir_data *rdd = buf;
struct ovl_cache_entry *p;
rdd->count++;
p = ovl_cache_entry_find(rdd->root, name, namelen);
if (p) {
list_move_tail(&p->l_node, rdd->middle);
} else {
p = ovl_cache_entry_new(name, namelen, ino, d_type);
if (p == NULL)
rdd->err = -ENOMEM;
else
list_add_tail(&p->l_node, rdd->middle);
}
return rdd->err;
}
static void ovl_cache_free(struct list_head *list)
{
struct ovl_cache_entry *p;
struct ovl_cache_entry *n;
list_for_each_entry_safe(p, n, list, l_node)
kfree(p);
INIT_LIST_HEAD(list);
}
static int ovl_fill_upper(void *buf, const char *name, int namelen,
loff_t offset, u64 ino, unsigned int d_type)
{
struct ovl_readdir_data *rdd = buf;
rdd->count++;
return ovl_cache_entry_add_rb(rdd, name, namelen, ino, d_type);
}
static inline int ovl_dir_read(struct path *realpath,
struct ovl_readdir_data *rdd, filldir_t filler)
{
struct file *realfile;
int err;
realfile = ovl_path_open(realpath, O_RDONLY | O_DIRECTORY);
if (IS_ERR(realfile))
return PTR_ERR(realfile);
do {
rdd->count = 0;
rdd->err = 0;
err = vfs_readdir(realfile, filler, rdd);
if (err >= 0)
err = rdd->err;
} while (!err && rdd->count);
fput(realfile);
return 0;
}
static void ovl_dir_reset(struct file *file)
{
struct ovl_dir_file *od = file->private_data;
enum ovl_path_type type = ovl_path_type(file->f_path.dentry);
if (ovl_dentry_version_get(file->f_path.dentry) != od->cache_version) {
list_del_init(&od->cursor);
ovl_cache_free(&od->cache);
od->is_cached = false;
}
WARN_ON(!od->is_real && type != OVL_PATH_MERGE);
if (od->is_real && type == OVL_PATH_MERGE) {
fput(od->realfile);
od->realfile = NULL;
od->is_real = false;
}
}
static int ovl_dir_mark_whiteouts(struct ovl_readdir_data *rdd)
{
struct ovl_cache_entry *p;
struct dentry *dentry;
const struct cred *old_cred;
struct cred *override_cred;
override_cred = prepare_creds();
if (!override_cred) {
ovl_cache_free(rdd->list);
return -ENOMEM;
}
/*
* CAP_SYS_ADMIN for getxattr
* CAP_DAC_OVERRIDE for lookup
*/
cap_raise(override_cred->cap_effective, CAP_SYS_ADMIN);
cap_raise(override_cred->cap_effective, CAP_DAC_OVERRIDE);
old_cred = override_creds(override_cred);
mutex_lock(&rdd->dir->d_inode->i_mutex);
list_for_each_entry(p, rdd->list, l_node) {
if (p->type != DT_LNK)
continue;
dentry = lookup_one_len(p->name, rdd->dir, p->len);
if (IS_ERR(dentry))
continue;
p->is_whiteout = ovl_is_whiteout(dentry);
dput(dentry);
}
mutex_unlock(&rdd->dir->d_inode->i_mutex);
revert_creds(old_cred);
put_cred(override_cred);
return 0;
}
static inline int ovl_dir_read_merged(struct path *upperpath,
struct path *lowerpath,
struct ovl_readdir_data *rdd)
{
int err;
struct rb_root root = RB_ROOT;
struct list_head middle;
rdd->root = &root;
if (upperpath->dentry) {
rdd->dir = upperpath->dentry;
err = ovl_dir_read(upperpath, rdd, ovl_fill_upper);
if (err)
goto out;
err = ovl_dir_mark_whiteouts(rdd);
if (err)
goto out;
}
/*
* Insert lowerpath entries before upperpath ones, this allows
* offsets to be reasonably constant
*/
list_add(&middle, rdd->list);
rdd->middle = &middle;
err = ovl_dir_read(lowerpath, rdd, ovl_fill_lower);
list_del(&middle);
out:
rdd->root = NULL;
return err;
}
static void ovl_seek_cursor(struct ovl_dir_file *od, loff_t pos)
{
struct list_head *l;
loff_t off;
l = od->cache.next;
for (off = 0; off < pos; off++) {
if (l == &od->cache)
break;
l = l->next;
}
list_move_tail(&od->cursor, l);
}
static int ovl_readdir(struct file *file, void *buf, filldir_t filler)
{
struct ovl_dir_file *od = file->private_data;
int res;
if (!file->f_pos)
ovl_dir_reset(file);
if (od->is_real) {
res = vfs_readdir(od->realfile, filler, buf);
file->f_pos = od->realfile->f_pos;
return res;
}
if (!od->is_cached) {
struct path lowerpath;
struct path upperpath;
struct ovl_readdir_data rdd = { .list = &od->cache };
ovl_path_lower(file->f_path.dentry, &lowerpath);
ovl_path_upper(file->f_path.dentry, &upperpath);
res = ovl_dir_read_merged(&upperpath, &lowerpath, &rdd);
if (res) {
ovl_cache_free(rdd.list);
return res;
}
od->cache_version = ovl_dentry_version_get(file->f_path.dentry);
od->is_cached = true;
ovl_seek_cursor(od, file->f_pos);
}
while (od->cursor.next != &od->cache) {
int over;
loff_t off;
struct ovl_cache_entry *p;
p = list_entry(od->cursor.next, struct ovl_cache_entry, l_node);
off = file->f_pos;
if (!p->is_whiteout) {
over = filler(buf, p->name, p->len, off, p->ino,
p->type);
if (over)
break;
}
file->f_pos++;
list_move(&od->cursor, &p->l_node);
}
return 0;
}
static loff_t ovl_dir_llseek(struct file *file, loff_t offset, int origin)
{
loff_t res;
struct ovl_dir_file *od = file->private_data;
mutex_lock(&file->f_dentry->d_inode->i_mutex);
if (!file->f_pos)
ovl_dir_reset(file);
if (od->is_real) {
res = vfs_llseek(od->realfile, offset, origin);
file->f_pos = od->realfile->f_pos;
} else {
res = -EINVAL;
switch (origin) {
case SEEK_CUR:
offset += file->f_pos;
break;
case SEEK_SET:
break;
default:
goto out_unlock;
}
if (offset < 0)
goto out_unlock;
if (offset != file->f_pos) {
file->f_pos = offset;
if (od->is_cached)
ovl_seek_cursor(od, offset);
}
res = offset;
}
out_unlock:
mutex_unlock(&file->f_dentry->d_inode->i_mutex);
return res;
}
static int ovl_dir_fsync(struct file *file, loff_t start, loff_t end,
int datasync)
{
struct ovl_dir_file *od = file->private_data;
/* May need to reopen directory if it got copied up */
if (!od->realfile) {
struct path upperpath;
ovl_path_upper(file->f_path.dentry, &upperpath);
od->realfile = ovl_path_open(&upperpath, O_RDONLY);
if (IS_ERR(od->realfile))
return PTR_ERR(od->realfile);
}
return vfs_fsync_range(od->realfile, start, end, datasync);
}
static int ovl_dir_release(struct inode *inode, struct file *file)
{
struct ovl_dir_file *od = file->private_data;
list_del(&od->cursor);
ovl_cache_free(&od->cache);
if (od->realfile)
fput(od->realfile);
kfree(od);
return 0;
}
static int ovl_dir_open(struct inode *inode, struct file *file)
{
struct path realpath;
struct file *realfile;
struct ovl_dir_file *od;
enum ovl_path_type type;
od = kzalloc(sizeof(struct ovl_dir_file), GFP_KERNEL);
if (!od)
return -ENOMEM;
type = ovl_path_real(file->f_path.dentry, &realpath);
realfile = ovl_path_open(&realpath, file->f_flags);
if (IS_ERR(realfile)) {
kfree(od);
return PTR_ERR(realfile);
}
INIT_LIST_HEAD(&od->cache);
INIT_LIST_HEAD(&od->cursor);
od->is_cached = false;
od->realfile = realfile;
od->is_real = (type != OVL_PATH_MERGE);
file->private_data = od;
return 0;
}
const struct file_operations ovl_dir_operations = {
.read = generic_read_dir,
.open = ovl_dir_open,
.readdir = ovl_readdir,
.llseek = ovl_dir_llseek,
.fsync = ovl_dir_fsync,
.release = ovl_dir_release,
};
static int ovl_check_empty_dir(struct dentry *dentry, struct list_head *list)
{
int err;
struct path lowerpath;
struct path upperpath;
struct ovl_cache_entry *p;
struct ovl_readdir_data rdd = { .list = list };
ovl_path_upper(dentry, &upperpath);
ovl_path_lower(dentry, &lowerpath);
err = ovl_dir_read_merged(&upperpath, &lowerpath, &rdd);
if (err)
return err;
err = 0;
list_for_each_entry(p, list, l_node) {
if (p->is_whiteout)
continue;
if (p->name[0] == '.') {
if (p->len == 1)
continue;
if (p->len == 2 && p->name[1] == '.')
continue;
}
err = -ENOTEMPTY;
break;
}
return err;
}
static int ovl_remove_whiteouts(struct dentry *dir, struct list_head *list)
{
struct path upperpath;
struct dentry *upperdir;
struct ovl_cache_entry *p;
const struct cred *old_cred;
struct cred *override_cred;
int err;
ovl_path_upper(dir, &upperpath);
upperdir = upperpath.dentry;
override_cred = prepare_creds();
if (!override_cred)
return -ENOMEM;
/*
* CAP_DAC_OVERRIDE for lookup and unlink
* CAP_SYS_ADMIN for setxattr of "trusted" namespace
* CAP_FOWNER for unlink in sticky directory
*/
cap_raise(override_cred->cap_effective, CAP_DAC_OVERRIDE);
cap_raise(override_cred->cap_effective, CAP_SYS_ADMIN);
cap_raise(override_cred->cap_effective, CAP_FOWNER);
old_cred = override_creds(override_cred);
err = vfs_setxattr(upperdir, ovl_opaque_xattr, "y", 1, 0);
if (err)
goto out_revert_creds;
mutex_lock_nested(&upperdir->d_inode->i_mutex, I_MUTEX_PARENT);
list_for_each_entry(p, list, l_node) {
struct dentry *dentry;
int ret;
if (!p->is_whiteout)
continue;
dentry = lookup_one_len(p->name, upperdir, p->len);
if (IS_ERR(dentry)) {
printk(KERN_WARNING
"overlayfs: failed to lookup whiteout %.*s: %li\n",
p->len, p->name, PTR_ERR(dentry));
continue;
}
ret = vfs_unlink(upperdir->d_inode, dentry);
dput(dentry);
if (ret)
printk(KERN_WARNING
"overlayfs: failed to unlink whiteout %.*s: %i\n",
p->len, p->name, ret);
}
mutex_unlock(&upperdir->d_inode->i_mutex);
out_revert_creds:
revert_creds(old_cred);
put_cred(override_cred);
return err;
}
int ovl_check_empty_and_clear(struct dentry *dentry, enum ovl_path_type type)
{
int err;
LIST_HEAD(list);
err = ovl_check_empty_dir(dentry, &list);
if (!err && type == OVL_PATH_MERGE)
err = ovl_remove_whiteouts(dentry, &list);
ovl_cache_free(&list);
return err;
}