/*
 * Copyright (C) 2005-2018 Junjiro R. Okajima
 *
 * This program, aufs is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/*
 * branch management
 */

#include <linux/compat.h>
#include <linux/statfs.h>
#include "aufs.h"

/*
 * free a single branch
 */
static void au_br_do_free(struct au_branch *br)
{
	int i;
	struct au_wbr *wbr;
	struct au_dykey **key;

	au_hnotify_fin_br(br);
	/* always, regardless the mount option */
	au_dr_hino_free(&br->br_dirren);

	if (br->br_xino.xi_file)
		fput(br->br_xino.xi_file);
	for (i = br->br_xino.xi_nondir.total - 1; i >= 0; i--)
		AuDebugOn(br->br_xino.xi_nondir.array[i]);
	kfree(br->br_xino.xi_nondir.array);

	AuDebugOn(au_br_count(br));
	au_br_count_fin(br);

	wbr = br->br_wbr;
	if (wbr) {
		for (i = 0; i < AuBrWh_Last; i++)
			dput(wbr->wbr_wh[i]);
		AuDebugOn(atomic_read(&wbr->wbr_wh_running));
		AuRwDestroy(&wbr->wbr_wh_rwsem);
	}

	if (br->br_fhsm) {
		au_br_fhsm_fin(br->br_fhsm);
		kfree(br->br_fhsm);
	}

	key = br->br_dykey;
	for (i = 0; i < AuBrDynOp; i++, key++)
		if (*key)
			au_dy_put(*key);
		else
			break;

	/* recursive lock, s_umount of branch's */
	lockdep_off();
	path_put(&br->br_path);
	lockdep_on();
	kfree(wbr);
	kfree(br);
}

/*
 * frees all branches
 */
void au_br_free(struct au_sbinfo *sbinfo)
{
	aufs_bindex_t bmax;
	struct au_branch **br;

	AuRwMustWriteLock(&sbinfo->si_rwsem);

	bmax = sbinfo->si_bbot + 1;
	br = sbinfo->si_branch;
	while (bmax--)
		au_br_do_free(*br++);
}

/*
 * find the index of a branch which is specified by @br_id.
 */
int au_br_index(struct super_block *sb, aufs_bindex_t br_id)
{
	aufs_bindex_t bindex, bbot;

	bbot = au_sbbot(sb);
	for (bindex = 0; bindex <= bbot; bindex++)
		if (au_sbr_id(sb, bindex) == br_id)
			return bindex;
	return -1;
}

/* ---------------------------------------------------------------------- */

/*
 * add a branch
 */

static int test_overlap(struct super_block *sb, struct dentry *h_adding,
			struct dentry *h_root)
{
	if (unlikely(h_adding == h_root
		     || au_test_loopback_overlap(sb, h_adding)))
		return 1;
	if (h_adding->d_sb != h_root->d_sb)
		return 0;
	return au_test_subdir(h_adding, h_root)
		|| au_test_subdir(h_root, h_adding);
}

/*
 * returns a newly allocated branch. @new_nbranch is a number of branches
 * after adding a branch.
 */
static struct au_branch *au_br_alloc(struct super_block *sb, int new_nbranch,
				     int perm)
{
	struct au_branch *add_branch;
	struct dentry *root;
	struct inode *inode;
	int err;

	err = -ENOMEM;
	add_branch = kzalloc(sizeof(*add_branch), GFP_NOFS);
	if (unlikely(!add_branch))
		goto out;
	add_branch->br_xino.xi_nondir.total = 8; /* initial size */
	add_branch->br_xino.xi_nondir.array
		= kcalloc(add_branch->br_xino.xi_nondir.total, sizeof(ino_t),
			  GFP_NOFS);
	if (unlikely(!add_branch->br_xino.xi_nondir.array))
		goto out_br;

	err = au_hnotify_init_br(add_branch, perm);
	if (unlikely(err))
		goto out_xinondir;

	if (au_br_writable(perm)) {
		/* may be freed separately at changing the branch permission */
		add_branch->br_wbr = kzalloc(sizeof(*add_branch->br_wbr),
					     GFP_NOFS);
		if (unlikely(!add_branch->br_wbr))
			goto out_hnotify;
	}

	if (au_br_fhsm(perm)) {
		err = au_fhsm_br_alloc(add_branch);
		if (unlikely(err))
			goto out_wbr;
	}

	root = sb->s_root;
	err = au_sbr_realloc(au_sbi(sb), new_nbranch, /*may_shrink*/0);
	if (!err)
		err = au_di_realloc(au_di(root), new_nbranch, /*may_shrink*/0);
	if (!err) {
		inode = d_inode(root);
		err = au_hinode_realloc(au_ii(inode), new_nbranch,
					/*may_shrink*/0);
	}
	if (!err)
		return add_branch; /* success */

out_wbr:
	kfree(add_branch->br_wbr);
out_hnotify:
	au_hnotify_fin_br(add_branch);
out_xinondir:
	kfree(add_branch->br_xino.xi_nondir.array);
out_br:
	kfree(add_branch);
out:
	return ERR_PTR(err);
}

/*
 * test if the branch permission is legal or not.
 */
static int test_br(struct inode *inode, int brperm, char *path)
{
	int err;

	err = (au_br_writable(brperm) && IS_RDONLY(inode));
	if (!err)
		goto out;

	err = -EINVAL;
	pr_err("write permission for readonly mount or inode, %s\n", path);

out:
	return err;
}

/*
 * returns:
 * 0: success, the caller will add it
 * plus: success, it is already unified, the caller should ignore it
 * minus: error
 */
static int test_add(struct super_block *sb, struct au_opt_add *add, int remount)
{
	int err;
	aufs_bindex_t bbot, bindex;
	struct dentry *root, *h_dentry;
	struct inode *inode, *h_inode;

	root = sb->s_root;
	bbot = au_sbbot(sb);
	if (unlikely(bbot >= 0
		     && au_find_dbindex(root, add->path.dentry) >= 0)) {
		err = 1;
		if (!remount) {
			err = -EINVAL;
			pr_err("%s duplicated\n", add->pathname);
		}
		goto out;
	}

	err = -ENOSPC; /* -E2BIG; */
	if (unlikely(AUFS_BRANCH_MAX <= add->bindex
		     || AUFS_BRANCH_MAX - 1 <= bbot)) {
		pr_err("number of branches exceeded %s\n", add->pathname);
		goto out;
	}

	err = -EDOM;
	if (unlikely(add->bindex < 0 || bbot + 1 < add->bindex)) {
		pr_err("bad index %d\n", add->bindex);
		goto out;
	}

	inode = d_inode(add->path.dentry);
	err = -ENOENT;
	if (unlikely(!inode->i_nlink)) {
		pr_err("no existence %s\n", add->pathname);
		goto out;
	}

	err = -EINVAL;
	if (unlikely(inode->i_sb == sb)) {
		pr_err("%s must be outside\n", add->pathname);
		goto out;
	}

	if (unlikely(au_test_fs_unsuppoted(inode->i_sb))) {
		pr_err("unsupported filesystem, %s (%s)\n",
		       add->pathname, au_sbtype(inode->i_sb));
		goto out;
	}

	if (unlikely(inode->i_sb->s_stack_depth)) {
		pr_err("already stacked, %s (%s)\n",
		       add->pathname, au_sbtype(inode->i_sb));
		goto out;
	}

	err = test_br(d_inode(add->path.dentry), add->perm, add->pathname);
	if (unlikely(err))
		goto out;

	if (bbot < 0)
		return 0; /* success */

	err = -EINVAL;
	for (bindex = 0; bindex <= bbot; bindex++)
		if (unlikely(test_overlap(sb, add->path.dentry,
					  au_h_dptr(root, bindex)))) {
			pr_err("%s is overlapped\n", add->pathname);
			goto out;
		}

	err = 0;
	if (au_opt_test(au_mntflags(sb), WARN_PERM)) {
		h_dentry = au_h_dptr(root, 0);
		h_inode = d_inode(h_dentry);
		if ((h_inode->i_mode & S_IALLUGO) != (inode->i_mode & S_IALLUGO)
		    || !uid_eq(h_inode->i_uid, inode->i_uid)
		    || !gid_eq(h_inode->i_gid, inode->i_gid))
			pr_warn("uid/gid/perm %s %u/%u/0%o, %u/%u/0%o\n",
				add->pathname,
				i_uid_read(inode), i_gid_read(inode),
				(inode->i_mode & S_IALLUGO),
				i_uid_read(h_inode), i_gid_read(h_inode),
				(h_inode->i_mode & S_IALLUGO));
	}

out:
	return err;
}

/*
 * initialize or clean the whiteouts for an adding branch
 */
static int au_br_init_wh(struct super_block *sb, struct au_branch *br,
			 int new_perm)
{
	int err, old_perm;
	aufs_bindex_t bindex;
	struct inode *h_inode;
	struct au_wbr *wbr;
	struct au_hinode *hdir;
	struct dentry *h_dentry;

	err = vfsub_mnt_want_write(au_br_mnt(br));
	if (unlikely(err))
		goto out;

	wbr = br->br_wbr;
	old_perm = br->br_perm;
	br->br_perm = new_perm;
	hdir = NULL;
	h_inode = NULL;
	bindex = au_br_index(sb, br->br_id);
	if (0 <= bindex) {
		hdir = au_hi(d_inode(sb->s_root), bindex);
		au_hn_inode_lock_nested(hdir, AuLsc_I_PARENT);
	} else {
		h_dentry = au_br_dentry(br);
		h_inode = d_inode(h_dentry);
		inode_lock_nested(h_inode, AuLsc_I_PARENT);
	}
	if (!wbr)
		err = au_wh_init(br, sb);
	else {
		wbr_wh_write_lock(wbr);
		err = au_wh_init(br, sb);
		wbr_wh_write_unlock(wbr);
	}
	if (hdir)
		au_hn_inode_unlock(hdir);
	else
		inode_unlock(h_inode);
	vfsub_mnt_drop_write(au_br_mnt(br));
	br->br_perm = old_perm;

	if (!err && wbr && !au_br_writable(new_perm)) {
		kfree(wbr);
		br->br_wbr = NULL;
	}

out:
	return err;
}

static int au_wbr_init(struct au_branch *br, struct super_block *sb,
		       int perm)
{
	int err;
	struct kstatfs kst;
	struct au_wbr *wbr;

	wbr = br->br_wbr;
	au_rw_init(&wbr->wbr_wh_rwsem);
	atomic_set(&wbr->wbr_wh_running, 0);

	/*
	 * a limit for rmdir/rename a dir
	 * cf. AUFS_MAX_NAMELEN in include/uapi/linux/aufs_type.h
	 */
	err = vfs_statfs(&br->br_path, &kst);
	if (unlikely(err))
		goto out;
	err = -EINVAL;
	if (kst.f_namelen >= NAME_MAX)
		err = au_br_init_wh(sb, br, perm);
	else
		pr_err("%pd(%s), unsupported namelen %ld\n",
		       au_br_dentry(br),
		       au_sbtype(au_br_dentry(br)->d_sb), kst.f_namelen);

out:
	return err;
}

/* initialize a new branch */
static int au_br_init(struct au_branch *br, struct super_block *sb,
		      struct au_opt_add *add)
{
	int err;
	struct inode *h_inode;

	err = 0;
	spin_lock_init(&br->br_xino.xi_nondir.spin);
	init_waitqueue_head(&br->br_xino.xi_nondir.wqh);
	br->br_perm = add->perm;
	br->br_path = add->path; /* set first, path_get() later */
	spin_lock_init(&br->br_dykey_lock);
	au_br_count_init(br);
	atomic_set(&br->br_xino_running, 0);
	br->br_id = au_new_br_id(sb);
	AuDebugOn(br->br_id < 0);

	/* always, regardless the given option */
	err = au_dr_br_init(sb, br, &add->path);
	if (unlikely(err))
		goto out_err;

	if (au_br_writable(add->perm)) {
		err = au_wbr_init(br, sb, add->perm);
		if (unlikely(err))
			goto out_err;
	}

	if (au_opt_test(au_mntflags(sb), XINO)) {
		h_inode = d_inode(add->path.dentry);
		err = au_xino_br(sb, br, h_inode->i_ino,
				 au_sbr(sb, 0)->br_xino.xi_file, /*do_test*/1);
		if (unlikely(err)) {
			AuDebugOn(br->br_xino.xi_file);
			goto out_err;
		}
	}

	sysaufs_br_init(br);
	path_get(&br->br_path);
	goto out; /* success */

out_err:
	memset(&br->br_path, 0, sizeof(br->br_path));
out:
	return err;
}

static void au_br_do_add_brp(struct au_sbinfo *sbinfo, aufs_bindex_t bindex,
			     struct au_branch *br, aufs_bindex_t bbot,
			     aufs_bindex_t amount)
{
	struct au_branch **brp;

	AuRwMustWriteLock(&sbinfo->si_rwsem);

	brp = sbinfo->si_branch + bindex;
	memmove(brp + 1, brp, sizeof(*brp) * amount);
	*brp = br;
	sbinfo->si_bbot++;
	if (unlikely(bbot < 0))
		sbinfo->si_bbot = 0;
}

static void au_br_do_add_hdp(struct au_dinfo *dinfo, aufs_bindex_t bindex,
			     aufs_bindex_t bbot, aufs_bindex_t amount)
{
	struct au_hdentry *hdp;

	AuRwMustWriteLock(&dinfo->di_rwsem);

	hdp = au_hdentry(dinfo, bindex);
	memmove(hdp + 1, hdp, sizeof(*hdp) * amount);
	au_h_dentry_init(hdp);
	dinfo->di_bbot++;
	if (unlikely(bbot < 0))
		dinfo->di_btop = 0;
}

static void au_br_do_add_hip(struct au_iinfo *iinfo, aufs_bindex_t bindex,
			     aufs_bindex_t bbot, aufs_bindex_t amount)
{
	struct au_hinode *hip;

	AuRwMustWriteLock(&iinfo->ii_rwsem);

	hip = au_hinode(iinfo, bindex);
	memmove(hip + 1, hip, sizeof(*hip) * amount);
	au_hinode_init(hip);
	iinfo->ii_bbot++;
	if (unlikely(bbot < 0))
		iinfo->ii_btop = 0;
}

static void au_br_do_add(struct super_block *sb, struct au_branch *br,
			 aufs_bindex_t bindex)
{
	struct dentry *root, *h_dentry;
	struct inode *root_inode, *h_inode;
	aufs_bindex_t bbot, amount;

	root = sb->s_root;
	root_inode = d_inode(root);
	bbot = au_sbbot(sb);
	amount = bbot + 1 - bindex;
	h_dentry = au_br_dentry(br);
	au_sbilist_lock();
	au_br_do_add_brp(au_sbi(sb), bindex, br, bbot, amount);
	au_br_do_add_hdp(au_di(root), bindex, bbot, amount);
	au_br_do_add_hip(au_ii(root_inode), bindex, bbot, amount);
	au_set_h_dptr(root, bindex, dget(h_dentry));
	h_inode = d_inode(h_dentry);
	au_set_h_iptr(root_inode, bindex, au_igrab(h_inode), /*flags*/0);
	au_sbilist_unlock();
}

int au_br_add(struct super_block *sb, struct au_opt_add *add, int remount)
{
	int err;
	aufs_bindex_t bbot, add_bindex;
	struct dentry *root, *h_dentry;
	struct inode *root_inode;
	struct au_branch *add_branch;

	root = sb->s_root;
	root_inode = d_inode(root);
	IMustLock(root_inode);
	IiMustWriteLock(root_inode);
	err = test_add(sb, add, remount);
	if (unlikely(err < 0))
		goto out;
	if (err) {
		err = 0;
		goto out; /* success */
	}

	bbot = au_sbbot(sb);
	add_branch = au_br_alloc(sb, bbot + 2, add->perm);
	err = PTR_ERR(add_branch);
	if (IS_ERR(add_branch))
		goto out;

	err = au_br_init(add_branch, sb, add);
	if (unlikely(err)) {
		au_br_do_free(add_branch);
		goto out;
	}

	add_bindex = add->bindex;
	if (!remount)
		au_br_do_add(sb, add_branch, add_bindex);
	else {
		sysaufs_brs_del(sb, add_bindex);
		au_br_do_add(sb, add_branch, add_bindex);
		sysaufs_brs_add(sb, add_bindex);
	}

	h_dentry = add->path.dentry;
	if (!add_bindex) {
		au_cpup_attr_all(root_inode, /*force*/1);
		sb->s_maxbytes = h_dentry->d_sb->s_maxbytes;
	} else
		au_add_nlink(root_inode, d_inode(h_dentry));

	/*
	 * this test/set prevents aufs from handling unnecesary notify events
	 * of xino files, in case of re-adding a writable branch which was
	 * once detached from aufs.
	 */
	if (au_xino_brid(sb) < 0
	    && au_br_writable(add_branch->br_perm)
	    && !au_test_fs_bad_xino(h_dentry->d_sb)
	    && add_branch->br_xino.xi_file
	    && add_branch->br_xino.xi_file->f_path.dentry->d_parent == h_dentry)
		au_xino_brid_set(sb, add_branch->br_id);

out:
	return err;
}

/* ---------------------------------------------------------------------- */

static unsigned long long au_farray_cb(struct super_block *sb, void *a,
				       unsigned long long max __maybe_unused,
				       void *arg)
{
	unsigned long long n;
	struct file **p, *f;
	struct hlist_bl_head *files;
	struct hlist_bl_node *pos;
	struct au_finfo *finfo;

	n = 0;
	p = a;
	files = &au_sbi(sb)->si_files;
	hlist_bl_lock(files);
	hlist_bl_for_each_entry(finfo, pos, files, fi_hlist) {
		f = finfo->fi_file;
		if (file_count(f)
		    && !special_file(file_inode(f)->i_mode)) {
			get_file(f);
			*p++ = f;
			n++;
			AuDebugOn(n > max);
		}
	}
	hlist_bl_unlock(files);

	return n;
}

static struct file **au_farray_alloc(struct super_block *sb,
				     unsigned long long *max)
{
	*max = au_nfiles(sb);
	return au_array_alloc(max, au_farray_cb, sb, /*arg*/NULL);
}

static void au_farray_free(struct file **a, unsigned long long max)
{
	unsigned long long ull;

	for (ull = 0; ull < max; ull++)
		if (a[ull])
			fput(a[ull]);
	kvfree(a);
}

/* ---------------------------------------------------------------------- */

/*
 * delete a branch
 */

/* to show the line number, do not make it inlined function */
#define AuVerbose(do_info, fmt, ...) do { \
	if (do_info) \
		pr_info(fmt, ##__VA_ARGS__); \
} while (0)

static int au_test_ibusy(struct inode *inode, aufs_bindex_t btop,
			 aufs_bindex_t bbot)
{
	return (inode && !S_ISDIR(inode->i_mode)) || btop == bbot;
}

static int au_test_dbusy(struct dentry *dentry, aufs_bindex_t btop,
			 aufs_bindex_t bbot)
{
	return au_test_ibusy(d_inode(dentry), btop, bbot);
}

/*
 * test if the branch is deletable or not.
 */
static int test_dentry_busy(struct dentry *root, aufs_bindex_t bindex,
			    unsigned int sigen, const unsigned int verbose)
{
	int err, i, j, ndentry;
	aufs_bindex_t btop, bbot;
	struct au_dcsub_pages dpages;
	struct au_dpage *dpage;
	struct dentry *d;

	err = au_dpages_init(&dpages, GFP_NOFS);
	if (unlikely(err))
		goto out;
	err = au_dcsub_pages(&dpages, root, NULL, NULL);
	if (unlikely(err))
		goto out_dpages;

	for (i = 0; !err && i < dpages.ndpage; i++) {
		dpage = dpages.dpages + i;
		ndentry = dpage->ndentry;
		for (j = 0; !err && j < ndentry; j++) {
			d = dpage->dentries[j];
			AuDebugOn(au_dcount(d) <= 0);
			if (!au_digen_test(d, sigen)) {
				di_read_lock_child(d, AuLock_IR);
				if (unlikely(au_dbrange_test(d))) {
					di_read_unlock(d, AuLock_IR);
					continue;
				}
			} else {
				di_write_lock_child(d);
				if (unlikely(au_dbrange_test(d))) {
					di_write_unlock(d);
					continue;
				}
				err = au_reval_dpath(d, sigen);
				if (!err)
					di_downgrade_lock(d, AuLock_IR);
				else {
					di_write_unlock(d);
					break;
				}
			}

			/* AuDbgDentry(d); */
			btop = au_dbtop(d);
			bbot = au_dbbot(d);
			if (btop <= bindex
			    && bindex <= bbot
			    && au_h_dptr(d, bindex)
			    && au_test_dbusy(d, btop, bbot)) {
				err = -EBUSY;
				AuVerbose(verbose, "busy %pd\n", d);
				AuDbgDentry(d);
			}
			di_read_unlock(d, AuLock_IR);
		}
	}

out_dpages:
	au_dpages_free(&dpages);
out:
	return err;
}

static int test_inode_busy(struct super_block *sb, aufs_bindex_t bindex,
			   unsigned int sigen, const unsigned int verbose)
{
	int err;
	unsigned long long max, ull;
	struct inode *i, **array;
	aufs_bindex_t btop, bbot;

	array = au_iarray_alloc(sb, &max);
	err = PTR_ERR(array);
	if (IS_ERR(array))
		goto out;

	err = 0;
	AuDbg("b%d\n", bindex);
	for (ull = 0; !err && ull < max; ull++) {
		i = array[ull];
		if (unlikely(!i))
			break;
		if (i->i_ino == AUFS_ROOT_INO)
			continue;

		/* AuDbgInode(i); */
		if (au_iigen(i, NULL) == sigen)
			ii_read_lock_child(i);
		else {
			ii_write_lock_child(i);
			err = au_refresh_hinode_self(i);
			au_iigen_dec(i);
			if (!err)
				ii_downgrade_lock(i);
			else {
				ii_write_unlock(i);
				break;
			}
		}

		btop = au_ibtop(i);
		bbot = au_ibbot(i);
		if (btop <= bindex
		    && bindex <= bbot
		    && au_h_iptr(i, bindex)
		    && au_test_ibusy(i, btop, bbot)) {
			err = -EBUSY;
			AuVerbose(verbose, "busy i%lu\n", i->i_ino);
			AuDbgInode(i);
		}
		ii_read_unlock(i);
	}
	au_iarray_free(array, max);

out:
	return err;
}

static int test_children_busy(struct dentry *root, aufs_bindex_t bindex,
			      const unsigned int verbose)
{
	int err;
	unsigned int sigen;

	sigen = au_sigen(root->d_sb);
	DiMustNoWaiters(root);
	IiMustNoWaiters(d_inode(root));
	di_write_unlock(root);
	err = test_dentry_busy(root, bindex, sigen, verbose);
	if (!err)
		err = test_inode_busy(root->d_sb, bindex, sigen, verbose);
	di_write_lock_child(root); /* aufs_write_lock() calls ..._child() */

	return err;
}

static int test_dir_busy(struct file *file, aufs_bindex_t br_id,
			 struct file **to_free, int *idx)
{
	int err;
	unsigned char matched, root;
	aufs_bindex_t bindex, bbot;
	struct au_fidir *fidir;
	struct au_hfile *hfile;

	err = 0;
	root = IS_ROOT(file->f_path.dentry);
	if (root) {
		get_file(file);
		to_free[*idx] = file;
		(*idx)++;
		goto out;
	}

	matched = 0;
	fidir = au_fi(file)->fi_hdir;
	AuDebugOn(!fidir);
	bbot = au_fbbot_dir(file);
	for (bindex = au_fbtop(file); bindex <= bbot; bindex++) {
		hfile = fidir->fd_hfile + bindex;
		if (!hfile->hf_file)
			continue;

		if (hfile->hf_br->br_id == br_id) {
			matched = 1;
			break;
		}
	}
	if (matched)
		err = -EBUSY;

out:
	return err;
}

static int test_file_busy(struct super_block *sb, aufs_bindex_t br_id,
			  struct file **to_free, int opened)
{
	int err, idx;
	unsigned long long ull, max;
	aufs_bindex_t btop;
	struct file *file, **array;
	struct dentry *root;
	struct au_hfile *hfile;

	array = au_farray_alloc(sb, &max);
	err = PTR_ERR(array);
	if (IS_ERR(array))
		goto out;

	err = 0;
	idx = 0;
	root = sb->s_root;
	di_write_unlock(root);
	for (ull = 0; ull < max; ull++) {
		file = array[ull];
		if (unlikely(!file))
			break;

		/* AuDbg("%pD\n", file); */
		fi_read_lock(file);
		btop = au_fbtop(file);
		if (!d_is_dir(file->f_path.dentry)) {
			hfile = &au_fi(file)->fi_htop;
			if (hfile->hf_br->br_id == br_id)
				err = -EBUSY;
		} else
			err = test_dir_busy(file, br_id, to_free, &idx);
		fi_read_unlock(file);
		if (unlikely(err))
			break;
	}
	di_write_lock_child(root);
	au_farray_free(array, max);
	AuDebugOn(idx > opened);

out:
	return err;
}

static void br_del_file(struct file **to_free, unsigned long long opened,
			  aufs_bindex_t br_id)
{
	unsigned long long ull;
	aufs_bindex_t bindex, btop, bbot, bfound;
	struct file *file;
	struct au_fidir *fidir;
	struct au_hfile *hfile;

	for (ull = 0; ull < opened; ull++) {
		file = to_free[ull];
		if (unlikely(!file))
			break;

		/* AuDbg("%pD\n", file); */
		AuDebugOn(!d_is_dir(file->f_path.dentry));
		bfound = -1;
		fidir = au_fi(file)->fi_hdir;
		AuDebugOn(!fidir);
		fi_write_lock(file);
		btop = au_fbtop(file);
		bbot = au_fbbot_dir(file);
		for (bindex = btop; bindex <= bbot; bindex++) {
			hfile = fidir->fd_hfile + bindex;
			if (!hfile->hf_file)
				continue;

			if (hfile->hf_br->br_id == br_id) {
				bfound = bindex;
				break;
			}
		}
		AuDebugOn(bfound < 0);
		au_set_h_fptr(file, bfound, NULL);
		if (bfound == btop) {
			for (btop++; btop <= bbot; btop++)
				if (au_hf_dir(file, btop)) {
					au_set_fbtop(file, btop);
					break;
				}
		}
		fi_write_unlock(file);
	}
}

static void au_br_do_del_brp(struct au_sbinfo *sbinfo,
			     const aufs_bindex_t bindex,
			     const aufs_bindex_t bbot)
{
	struct au_branch **brp, **p;

	AuRwMustWriteLock(&sbinfo->si_rwsem);

	brp = sbinfo->si_branch + bindex;
	if (bindex < bbot)
		memmove(brp, brp + 1, sizeof(*brp) * (bbot - bindex));
	sbinfo->si_branch[0 + bbot] = NULL;
	sbinfo->si_bbot--;

	p = au_krealloc(sbinfo->si_branch, sizeof(*p) * bbot, AuGFP_SBILIST,
			/*may_shrink*/1);
	if (p)
		sbinfo->si_branch = p;
	/* harmless error */
}

static void au_br_do_del_hdp(struct au_dinfo *dinfo, const aufs_bindex_t bindex,
			     const aufs_bindex_t bbot)
{
	struct au_hdentry *hdp, *p;

	AuRwMustWriteLock(&dinfo->di_rwsem);

	hdp = au_hdentry(dinfo, bindex);
	if (bindex < bbot)
		memmove(hdp, hdp + 1, sizeof(*hdp) * (bbot - bindex));
	/* au_h_dentry_init(au_hdentry(dinfo, bbot); */
	dinfo->di_bbot--;

	p = au_krealloc(dinfo->di_hdentry, sizeof(*p) * bbot, AuGFP_SBILIST,
			/*may_shrink*/1);
	if (p)
		dinfo->di_hdentry = p;
	/* harmless error */
}

static void au_br_do_del_hip(struct au_iinfo *iinfo, const aufs_bindex_t bindex,
			     const aufs_bindex_t bbot)
{
	struct au_hinode *hip, *p;

	AuRwMustWriteLock(&iinfo->ii_rwsem);

	hip = au_hinode(iinfo, bindex);
	if (bindex < bbot)
		memmove(hip, hip + 1, sizeof(*hip) * (bbot - bindex));
	/* au_hinode_init(au_hinode(iinfo, bbot)); */
	iinfo->ii_bbot--;

	p = au_krealloc(iinfo->ii_hinode, sizeof(*p) * bbot, AuGFP_SBILIST,
			/*may_shrink*/1);
	if (p)
		iinfo->ii_hinode = p;
	/* harmless error */
}

static void au_br_do_del(struct super_block *sb, aufs_bindex_t bindex,
			 struct au_branch *br)
{
	aufs_bindex_t bbot;
	struct au_sbinfo *sbinfo;
	struct dentry *root, *h_root;
	struct inode *inode, *h_inode;
	struct au_hinode *hinode;

	SiMustWriteLock(sb);

	root = sb->s_root;
	inode = d_inode(root);
	sbinfo = au_sbi(sb);
	bbot = sbinfo->si_bbot;

	h_root = au_h_dptr(root, bindex);
	hinode = au_hi(inode, bindex);
	h_inode = au_igrab(hinode->hi_inode);
	au_hiput(hinode);

	au_sbilist_lock();
	au_br_do_del_brp(sbinfo, bindex, bbot);
	au_br_do_del_hdp(au_di(root), bindex, bbot);
	au_br_do_del_hip(au_ii(inode), bindex, bbot);
	au_sbilist_unlock();

	/* ignore an error */
	au_dr_br_fin(sb, br); /* always, regardless the mount option */

	dput(h_root);
	iput(h_inode);
	au_br_do_free(br);
}

static unsigned long long empty_cb(struct super_block *sb, void *array,
				   unsigned long long max, void *arg)
{
	return max;
}

int au_br_del(struct super_block *sb, struct au_opt_del *del, int remount)
{
	int err, rerr, i;
	unsigned long long opened;
	unsigned int mnt_flags;
	aufs_bindex_t bindex, bbot, br_id;
	unsigned char do_wh, verbose;
	struct au_branch *br;
	struct au_wbr *wbr;
	struct dentry *root;
	struct file **to_free;

	err = 0;
	opened = 0;
	to_free = NULL;
	root = sb->s_root;
	bindex = au_find_dbindex(root, del->h_path.dentry);
	if (bindex < 0) {
		if (remount)
			goto out; /* success */
		err = -ENOENT;
		pr_err("%s no such branch\n", del->pathname);
		goto out;
	}
	AuDbg("bindex b%d\n", bindex);

	err = -EBUSY;
	mnt_flags = au_mntflags(sb);
	verbose = !!au_opt_test(mnt_flags, VERBOSE);
	bbot = au_sbbot(sb);
	if (unlikely(!bbot)) {
		AuVerbose(verbose, "no more branches left\n");
		goto out;
	}
	br = au_sbr(sb, bindex);
	AuDebugOn(!path_equal(&br->br_path, &del->h_path));

	br_id = br->br_id;
	opened = au_br_count(br);
	if (unlikely(opened)) {
		to_free = au_array_alloc(&opened, empty_cb, sb, NULL);
		err = PTR_ERR(to_free);
		if (IS_ERR(to_free))
			goto out;

		err = test_file_busy(sb, br_id, to_free, opened);
		if (unlikely(err)) {
			AuVerbose(verbose, "%llu file(s) opened\n", opened);
			goto out;
		}
	}

	wbr = br->br_wbr;
	do_wh = wbr && (wbr->wbr_whbase || wbr->wbr_plink || wbr->wbr_orph);
	if (do_wh) {
		/* instead of WbrWhMustWriteLock(wbr) */
		SiMustWriteLock(sb);
		for (i = 0; i < AuBrWh_Last; i++) {
			dput(wbr->wbr_wh[i]);
			wbr->wbr_wh[i] = NULL;
		}
	}

	err = test_children_busy(root, bindex, verbose);
	if (unlikely(err)) {
		if (do_wh)
			goto out_wh;
		goto out;
	}

	err = 0;
	if (to_free) {
		/*
		 * now we confirmed the branch is deletable.
		 * let's free the remaining opened dirs on the branch.
		 */
		di_write_unlock(root);
		br_del_file(to_free, opened, br_id);
		di_write_lock_child(root);
	}

	if (!remount)
		au_br_do_del(sb, bindex, br);
	else {
		sysaufs_brs_del(sb, bindex);
		au_br_do_del(sb, bindex, br);
		sysaufs_brs_add(sb, bindex);
	}

	if (!bindex) {
		au_cpup_attr_all(d_inode(root), /*force*/1);
		sb->s_maxbytes = au_sbr_sb(sb, 0)->s_maxbytes;
	} else
		au_sub_nlink(d_inode(root), d_inode(del->h_path.dentry));
	if (au_opt_test(mnt_flags, PLINK))
		au_plink_half_refresh(sb, br_id);

	if (au_xino_brid(sb) == br_id)
		au_xino_brid_set(sb, -1);
	goto out; /* success */

out_wh:
	/* revert */
	rerr = au_br_init_wh(sb, br, br->br_perm);
	if (rerr)
		pr_warn("failed re-creating base whiteout, %s. (%d)\n",
			del->pathname, rerr);
out:
	if (to_free)
		au_farray_free(to_free, opened);
	return err;
}

/* ---------------------------------------------------------------------- */

static int au_ibusy(struct super_block *sb, struct aufs_ibusy __user *arg)
{
	int err;
	aufs_bindex_t btop, bbot;
	struct aufs_ibusy ibusy;
	struct inode *inode, *h_inode;

	err = -EPERM;
	if (unlikely(!capable(CAP_SYS_ADMIN)))
		goto out;

	err = copy_from_user(&ibusy, arg, sizeof(ibusy));
	if (!err)
		err = !access_ok(VERIFY_WRITE, &arg->h_ino, sizeof(arg->h_ino));
	if (unlikely(err)) {
		err = -EFAULT;
		AuTraceErr(err);
		goto out;
	}

	err = -EINVAL;
	si_read_lock(sb, AuLock_FLUSH);
	if (unlikely(ibusy.bindex < 0 || ibusy.bindex > au_sbbot(sb)))
		goto out_unlock;

	err = 0;
	ibusy.h_ino = 0; /* invalid */
	inode = ilookup(sb, ibusy.ino);
	if (!inode
	    || inode->i_ino == AUFS_ROOT_INO
	    || au_is_bad_inode(inode))
		goto out_unlock;

	ii_read_lock_child(inode);
	btop = au_ibtop(inode);
	bbot = au_ibbot(inode);
	if (btop <= ibusy.bindex && ibusy.bindex <= bbot) {
		h_inode = au_h_iptr(inode, ibusy.bindex);
		if (h_inode && au_test_ibusy(inode, btop, bbot))
			ibusy.h_ino = h_inode->i_ino;
	}
	ii_read_unlock(inode);
	iput(inode);

out_unlock:
	si_read_unlock(sb);
	if (!err) {
		err = __put_user(ibusy.h_ino, &arg->h_ino);
		if (unlikely(err)) {
			err = -EFAULT;
			AuTraceErr(err);
		}
	}
out:
	return err;
}

long au_ibusy_ioctl(struct file *file, unsigned long arg)
{
	return au_ibusy(file->f_path.dentry->d_sb, (void __user *)arg);
}

#ifdef CONFIG_COMPAT
long au_ibusy_compat_ioctl(struct file *file, unsigned long arg)
{
	return au_ibusy(file->f_path.dentry->d_sb, compat_ptr(arg));
}
#endif

/* ---------------------------------------------------------------------- */

/*
 * change a branch permission
 */

static void au_warn_ima(void)
{
#ifdef CONFIG_IMA
	/* since it doesn't support mark_files_ro() */
	AuWarn1("RW -> RO makes IMA to produce wrong message\n");
#endif
}

static int do_need_sigen_inc(int a, int b)
{
	return au_br_whable(a) && !au_br_whable(b);
}

static int need_sigen_inc(int old, int new)
{
	return do_need_sigen_inc(old, new)
		|| do_need_sigen_inc(new, old);
}

static int au_br_mod_files_ro(struct super_block *sb, aufs_bindex_t bindex)
{
	int err, do_warn;
	unsigned int mnt_flags;
	unsigned long long ull, max;
	aufs_bindex_t br_id;
	unsigned char verbose, writer;
	struct file *file, *hf, **array;
	struct au_hfile *hfile;

	mnt_flags = au_mntflags(sb);
	verbose = !!au_opt_test(mnt_flags, VERBOSE);

	array = au_farray_alloc(sb, &max);
	err = PTR_ERR(array);
	if (IS_ERR(array))
		goto out;

	do_warn = 0;
	br_id = au_sbr_id(sb, bindex);
	for (ull = 0; ull < max; ull++) {
		file = array[ull];
		if (unlikely(!file))
			break;

		/* AuDbg("%pD\n", file); */
		fi_read_lock(file);
		if (unlikely(au_test_mmapped(file))) {
			err = -EBUSY;
			AuVerbose(verbose, "mmapped %pD\n", file);
			AuDbgFile(file);
			FiMustNoWaiters(file);
			fi_read_unlock(file);
			goto out_array;
		}

		hfile = &au_fi(file)->fi_htop;
		hf = hfile->hf_file;
		if (!d_is_reg(file->f_path.dentry)
		    || !(file->f_mode & FMODE_WRITE)
		    || hfile->hf_br->br_id != br_id
		    || !(hf->f_mode & FMODE_WRITE))
			array[ull] = NULL;
		else {
			do_warn = 1;
			get_file(file);
		}

		FiMustNoWaiters(file);
		fi_read_unlock(file);
		fput(file);
	}

	err = 0;
	if (do_warn)
		au_warn_ima();

	for (ull = 0; ull < max; ull++) {
		file = array[ull];
		if (!file)
			continue;

		/* todo: already flushed? */
		/*
		 * fs/super.c:mark_files_ro() is gone, but aufs keeps its
		 * approach which resets f_mode and calls mnt_drop_write() and
		 * file_release_write() for each file, because the branch
		 * attribute in aufs world is totally different from the native
		 * fs rw/ro mode.
		*/
		/* fi_read_lock(file); */
		hfile = &au_fi(file)->fi_htop;
		hf = hfile->hf_file;
		/* fi_read_unlock(file); */
		spin_lock(&hf->f_lock);
		writer = !!(hf->f_mode & FMODE_WRITER);
		hf->f_mode &= ~(FMODE_WRITE | FMODE_WRITER);
		spin_unlock(&hf->f_lock);
		if (writer) {
			put_write_access(file_inode(hf));
			__mnt_drop_write(hf->f_path.mnt);
		}
	}

out_array:
	au_farray_free(array, max);
out:
	AuTraceErr(err);
	return err;
}

int au_br_mod(struct super_block *sb, struct au_opt_mod *mod, int remount,
	      int *do_refresh)
{
	int err, rerr;
	aufs_bindex_t bindex;
	struct dentry *root;
	struct au_branch *br;
	struct au_br_fhsm *bf;

	root = sb->s_root;
	bindex = au_find_dbindex(root, mod->h_root);
	if (bindex < 0) {
		if (remount)
			return 0; /* success */
		err = -ENOENT;
		pr_err("%s no such branch\n", mod->path);
		goto out;
	}
	AuDbg("bindex b%d\n", bindex);

	err = test_br(d_inode(mod->h_root), mod->perm, mod->path);
	if (unlikely(err))
		goto out;

	br = au_sbr(sb, bindex);
	AuDebugOn(mod->h_root != au_br_dentry(br));
	if (br->br_perm == mod->perm)
		return 0; /* success */

	/* pre-allocate for non-fhsm --> fhsm */
	bf = NULL;
	if (!au_br_fhsm(br->br_perm) && au_br_fhsm(mod->perm)) {
		err = au_fhsm_br_alloc(br);
		if (unlikely(err))
			goto out;
		bf = br->br_fhsm;
		br->br_fhsm = NULL;
	}

	if (au_br_writable(br->br_perm)) {
		/* remove whiteout base */
		err = au_br_init_wh(sb, br, mod->perm);
		if (unlikely(err))
			goto out_bf;

		if (!au_br_writable(mod->perm)) {
			/* rw --> ro, file might be mmapped */
			DiMustNoWaiters(root);
			IiMustNoWaiters(d_inode(root));
			di_write_unlock(root);
			err = au_br_mod_files_ro(sb, bindex);
			/* aufs_write_lock() calls ..._child() */
			di_write_lock_child(root);

			if (unlikely(err)) {
				rerr = -ENOMEM;
				br->br_wbr = kzalloc(sizeof(*br->br_wbr),
						     GFP_NOFS);
				if (br->br_wbr)
					rerr = au_wbr_init(br, sb, br->br_perm);
				if (unlikely(rerr)) {
					AuIOErr("nested error %d (%d)\n",
						rerr, err);
					br->br_perm = mod->perm;
				}
			}
		}
	} else if (au_br_writable(mod->perm)) {
		/* ro --> rw */
		err = -ENOMEM;
		br->br_wbr = kzalloc(sizeof(*br->br_wbr), GFP_NOFS);
		if (br->br_wbr) {
			err = au_wbr_init(br, sb, mod->perm);
			if (unlikely(err)) {
				kfree(br->br_wbr);
				br->br_wbr = NULL;
			}
		}
	}
	if (unlikely(err))
		goto out_bf;

	if (au_br_fhsm(br->br_perm)) {
		if (!au_br_fhsm(mod->perm)) {
			/* fhsm --> non-fhsm */
			au_br_fhsm_fin(br->br_fhsm);
			kfree(br->br_fhsm);
			br->br_fhsm = NULL;
		}
	} else if (au_br_fhsm(mod->perm))
		/* non-fhsm --> fhsm */
		br->br_fhsm = bf;

	*do_refresh |= need_sigen_inc(br->br_perm, mod->perm);
	br->br_perm = mod->perm;
	goto out; /* success */

out_bf:
	kfree(bf);
out:
	AuTraceErr(err);
	return err;
}

/* ---------------------------------------------------------------------- */

int au_br_stfs(struct au_branch *br, struct aufs_stfs *stfs)
{
	int err;
	struct kstatfs kstfs;

	err = vfs_statfs(&br->br_path, &kstfs);
	if (!err) {
		stfs->f_blocks = kstfs.f_blocks;
		stfs->f_bavail = kstfs.f_bavail;
		stfs->f_files = kstfs.f_files;
		stfs->f_ffree = kstfs.f_ffree;
	}

	return err;
}
