fix apparmor dereferencing potentially freed dentry, sanitize __d_path() API

__d_path() API is asking for trouble and in case of apparmor d_namespace_path()
getting just that.  The root cause is that when __d_path() misses the root
it had been told to look for, it stores the location of the most remote ancestor
in *root.  Without grabbing references.  Sure, at the moment of call it had
been pinned down by what we have in *path.  And if we raced with umount -l, we
could have very well stopped at vfsmount/dentry that got freed as soon as
prepend_path() dropped vfsmount_lock.

It is safe to compare these pointers with pre-existing (and known to be still
alive) vfsmount and dentry, as long as all we are asking is "is it the same
address?".  Dereferencing is not safe and apparmor ended up stepping into
that.  d_namespace_path() really wants to examine the place where we stopped,
even if it's not connected to our namespace.  As the result, it looked
at ->d_sb->s_magic of a dentry that might've been already freed by that point.
All other callers had been careful enough to avoid that, but it's really
a bad interface - it invites that kind of trouble.

The fix is fairly straightforward, even though it's bigger than I'd like:
	* prepend_path() root argument becomes const.
	* __d_path() is never called with NULL/NULL root.  It was a kludge
to start with.  Instead, we have an explicit function - d_absolute_root().
Same as __d_path(), except that it doesn't get root passed and stops where
it stops.  apparmor and tomoyo are using it.
	* __d_path() returns NULL on path outside of root.  The main
caller is show_mountinfo() and that's precisely what we pass root for - to
skip those outside chroot jail.  Those who don't want that can (and do)
use d_path().
	* __d_path() root argument becomes const.  Everyone agrees, I hope.
	* apparmor does *NOT* try to use __d_path() or any of its variants
when it sees that path->mnt is an internal vfsmount.  In that case it's
definitely not mounted anywhere and dentry_path() is exactly what we want
there.  Handling of sysctl()-triggered weirdness is moved to that place.
	* if apparmor is asked to do pathname relative to chroot jail
and __d_path() tells it we it's not in that jail, the sucker just calls
d_absolute_path() instead.  That's the other remaining caller of __d_path(),
BTW.
        * seq_path_root() does _NOT_ return -ENAMETOOLONG (it's stupid anyway -
the normal seq_file logics will take care of growing the buffer and redoing
the call of ->show() just fine).  However, if it gets path not reachable
from root, it returns SEQ_SKIP.  The only caller adjusted (i.e. stopped
ignoring the return value as it used to do).

Reviewed-by: John Johansen <john.johansen@canonical.com>
ACKed-by: John Johansen <john.johansen@canonical.com>
Signed-off-by: Al Viro <viro@zeniv.linux.org.uk>
Cc: stable@vger.kernel.org
This commit is contained in:
Al Viro 2011-12-05 08:43:34 -05:00
parent 5611cc4572
commit 02125a8264
7 changed files with 102 additions and 71 deletions

View File

@ -2439,16 +2439,14 @@ static int prepend_name(char **buffer, int *buflen, struct qstr *name)
/** /**
* prepend_path - Prepend path string to a buffer * prepend_path - Prepend path string to a buffer
* @path: the dentry/vfsmount to report * @path: the dentry/vfsmount to report
* @root: root vfsmnt/dentry (may be modified by this function) * @root: root vfsmnt/dentry
* @buffer: pointer to the end of the buffer * @buffer: pointer to the end of the buffer
* @buflen: pointer to buffer length * @buflen: pointer to buffer length
* *
* Caller holds the rename_lock. * Caller holds the rename_lock.
*
* If path is not reachable from the supplied root, then the value of
* root is changed (without modifying refcounts).
*/ */
static int prepend_path(const struct path *path, struct path *root, static int prepend_path(const struct path *path,
const struct path *root,
char **buffer, int *buflen) char **buffer, int *buflen)
{ {
struct dentry *dentry = path->dentry; struct dentry *dentry = path->dentry;
@ -2483,10 +2481,10 @@ static int prepend_path(const struct path *path, struct path *root,
dentry = parent; dentry = parent;
} }
out:
if (!error && !slash) if (!error && !slash)
error = prepend(buffer, buflen, "/", 1); error = prepend(buffer, buflen, "/", 1);
out:
br_read_unlock(vfsmount_lock); br_read_unlock(vfsmount_lock);
return error; return error;
@ -2500,15 +2498,17 @@ global_root:
WARN(1, "Root dentry has weird name <%.*s>\n", WARN(1, "Root dentry has weird name <%.*s>\n",
(int) dentry->d_name.len, dentry->d_name.name); (int) dentry->d_name.len, dentry->d_name.name);
} }
root->mnt = vfsmnt; if (!slash)
root->dentry = dentry; error = prepend(buffer, buflen, "/", 1);
if (!error)
error = vfsmnt->mnt_ns ? 1 : 2;
goto out; goto out;
} }
/** /**
* __d_path - return the path of a dentry * __d_path - return the path of a dentry
* @path: the dentry/vfsmount to report * @path: the dentry/vfsmount to report
* @root: root vfsmnt/dentry (may be modified by this function) * @root: root vfsmnt/dentry
* @buf: buffer to return value in * @buf: buffer to return value in
* @buflen: buffer length * @buflen: buffer length
* *
@ -2519,10 +2519,10 @@ global_root:
* *
* "buflen" should be positive. * "buflen" should be positive.
* *
* If path is not reachable from the supplied root, then the value of * If the path is not reachable from the supplied root, return %NULL.
* root is changed (without modifying refcounts).
*/ */
char *__d_path(const struct path *path, struct path *root, char *__d_path(const struct path *path,
const struct path *root,
char *buf, int buflen) char *buf, int buflen)
{ {
char *res = buf + buflen; char *res = buf + buflen;
@ -2533,7 +2533,28 @@ char *__d_path(const struct path *path, struct path *root,
error = prepend_path(path, root, &res, &buflen); error = prepend_path(path, root, &res, &buflen);
write_sequnlock(&rename_lock); write_sequnlock(&rename_lock);
if (error) if (error < 0)
return ERR_PTR(error);
if (error > 0)
return NULL;
return res;
}
char *d_absolute_path(const struct path *path,
char *buf, int buflen)
{
struct path root = {};
char *res = buf + buflen;
int error;
prepend(&res, &buflen, "\0", 1);
write_seqlock(&rename_lock);
error = prepend_path(path, &root, &res, &buflen);
write_sequnlock(&rename_lock);
if (error > 1)
error = -EINVAL;
if (error < 0)
return ERR_PTR(error); return ERR_PTR(error);
return res; return res;
} }
@ -2541,8 +2562,9 @@ char *__d_path(const struct path *path, struct path *root,
/* /*
* same as __d_path but appends "(deleted)" for unlinked files. * same as __d_path but appends "(deleted)" for unlinked files.
*/ */
static int path_with_deleted(const struct path *path, struct path *root, static int path_with_deleted(const struct path *path,
char **buf, int *buflen) const struct path *root,
char **buf, int *buflen)
{ {
prepend(buf, buflen, "\0", 1); prepend(buf, buflen, "\0", 1);
if (d_unlinked(path->dentry)) { if (d_unlinked(path->dentry)) {
@ -2579,7 +2601,6 @@ char *d_path(const struct path *path, char *buf, int buflen)
{ {
char *res = buf + buflen; char *res = buf + buflen;
struct path root; struct path root;
struct path tmp;
int error; int error;
/* /*
@ -2594,9 +2615,8 @@ char *d_path(const struct path *path, char *buf, int buflen)
get_fs_root(current->fs, &root); get_fs_root(current->fs, &root);
write_seqlock(&rename_lock); write_seqlock(&rename_lock);
tmp = root; error = path_with_deleted(path, &root, &res, &buflen);
error = path_with_deleted(path, &tmp, &res, &buflen); if (error < 0)
if (error)
res = ERR_PTR(error); res = ERR_PTR(error);
write_sequnlock(&rename_lock); write_sequnlock(&rename_lock);
path_put(&root); path_put(&root);
@ -2617,7 +2637,6 @@ char *d_path_with_unreachable(const struct path *path, char *buf, int buflen)
{ {
char *res = buf + buflen; char *res = buf + buflen;
struct path root; struct path root;
struct path tmp;
int error; int error;
if (path->dentry->d_op && path->dentry->d_op->d_dname) if (path->dentry->d_op && path->dentry->d_op->d_dname)
@ -2625,9 +2644,8 @@ char *d_path_with_unreachable(const struct path *path, char *buf, int buflen)
get_fs_root(current->fs, &root); get_fs_root(current->fs, &root);
write_seqlock(&rename_lock); write_seqlock(&rename_lock);
tmp = root; error = path_with_deleted(path, &root, &res, &buflen);
error = path_with_deleted(path, &tmp, &res, &buflen); if (error > 0)
if (!error && !path_equal(&tmp, &root))
error = prepend_unreachable(&res, &buflen); error = prepend_unreachable(&res, &buflen);
write_sequnlock(&rename_lock); write_sequnlock(&rename_lock);
path_put(&root); path_put(&root);
@ -2758,19 +2776,18 @@ SYSCALL_DEFINE2(getcwd, char __user *, buf, unsigned long, size)
write_seqlock(&rename_lock); write_seqlock(&rename_lock);
if (!d_unlinked(pwd.dentry)) { if (!d_unlinked(pwd.dentry)) {
unsigned long len; unsigned long len;
struct path tmp = root;
char *cwd = page + PAGE_SIZE; char *cwd = page + PAGE_SIZE;
int buflen = PAGE_SIZE; int buflen = PAGE_SIZE;
prepend(&cwd, &buflen, "\0", 1); prepend(&cwd, &buflen, "\0", 1);
error = prepend_path(&pwd, &tmp, &cwd, &buflen); error = prepend_path(&pwd, &root, &cwd, &buflen);
write_sequnlock(&rename_lock); write_sequnlock(&rename_lock);
if (error) if (error < 0)
goto out; goto out;
/* Unreachable from current root */ /* Unreachable from current root */
if (!path_equal(&tmp, &root)) { if (error > 0) {
error = prepend_unreachable(&cwd, &buflen); error = prepend_unreachable(&cwd, &buflen);
if (error) if (error)
goto out; goto out;

View File

@ -1048,15 +1048,12 @@ static int show_mountinfo(struct seq_file *m, void *v)
if (err) if (err)
goto out; goto out;
seq_putc(m, ' '); seq_putc(m, ' ');
seq_path_root(m, &mnt_path, &root, " \t\n\\");
if (root.mnt != p->root.mnt || root.dentry != p->root.dentry) { /* mountpoints outside of chroot jail will give SEQ_SKIP on this */
/* err = seq_path_root(m, &mnt_path, &root, " \t\n\\");
* Mountpoint is outside root, discard that one. Ugly, if (err)
* but less so than trying to do that in iterator in a goto out;
* race-free way (due to renames).
*/
return SEQ_SKIP;
}
seq_puts(m, mnt->mnt_flags & MNT_READONLY ? " ro" : " rw"); seq_puts(m, mnt->mnt_flags & MNT_READONLY ? " ro" : " rw");
show_mnt_opts(m, mnt); show_mnt_opts(m, mnt);
@ -2776,3 +2773,8 @@ void kern_unmount(struct vfsmount *mnt)
} }
} }
EXPORT_SYMBOL(kern_unmount); EXPORT_SYMBOL(kern_unmount);
bool our_mnt(struct vfsmount *mnt)
{
return check_mnt(mnt);
}

View File

@ -449,8 +449,6 @@ EXPORT_SYMBOL(seq_path);
/* /*
* Same as seq_path, but relative to supplied root. * Same as seq_path, but relative to supplied root.
*
* root may be changed, see __d_path().
*/ */
int seq_path_root(struct seq_file *m, struct path *path, struct path *root, int seq_path_root(struct seq_file *m, struct path *path, struct path *root,
char *esc) char *esc)
@ -463,6 +461,8 @@ int seq_path_root(struct seq_file *m, struct path *path, struct path *root,
char *p; char *p;
p = __d_path(path, root, buf, size); p = __d_path(path, root, buf, size);
if (!p)
return SEQ_SKIP;
res = PTR_ERR(p); res = PTR_ERR(p);
if (!IS_ERR(p)) { if (!IS_ERR(p)) {
char *end = mangle_path(buf, p, esc); char *end = mangle_path(buf, p, esc);
@ -474,7 +474,7 @@ int seq_path_root(struct seq_file *m, struct path *path, struct path *root,
} }
seq_commit(m, res); seq_commit(m, res);
return res < 0 ? res : 0; return res < 0 && res != -ENAMETOOLONG ? res : 0;
} }
/* /*

View File

@ -339,7 +339,8 @@ extern int d_validate(struct dentry *, struct dentry *);
*/ */
extern char *dynamic_dname(struct dentry *, char *, int, const char *, ...); extern char *dynamic_dname(struct dentry *, char *, int, const char *, ...);
extern char *__d_path(const struct path *path, struct path *root, char *, int); extern char *__d_path(const struct path *, const struct path *, char *, int);
extern char *d_absolute_path(const struct path *, char *, int);
extern char *d_path(const struct path *, char *, int); extern char *d_path(const struct path *, char *, int);
extern char *d_path_with_unreachable(const struct path *, char *, int); extern char *d_path_with_unreachable(const struct path *, char *, int);
extern char *dentry_path_raw(struct dentry *, char *, int); extern char *dentry_path_raw(struct dentry *, char *, int);

View File

@ -1942,6 +1942,7 @@ extern int fd_statfs(int, struct kstatfs *);
extern int statfs_by_dentry(struct dentry *, struct kstatfs *); extern int statfs_by_dentry(struct dentry *, struct kstatfs *);
extern int freeze_super(struct super_block *super); extern int freeze_super(struct super_block *super);
extern int thaw_super(struct super_block *super); extern int thaw_super(struct super_block *super);
extern bool our_mnt(struct vfsmount *mnt);
extern int current_umask(void); extern int current_umask(void);

View File

@ -57,23 +57,44 @@ static int prepend(char **buffer, int buflen, const char *str, int namelen)
static int d_namespace_path(struct path *path, char *buf, int buflen, static int d_namespace_path(struct path *path, char *buf, int buflen,
char **name, int flags) char **name, int flags)
{ {
struct path root, tmp;
char *res; char *res;
int connected, error = 0; int error = 0;
int connected = 1;
/* Get the root we want to resolve too, released below */ if (path->mnt->mnt_flags & MNT_INTERNAL) {
if (flags & PATH_CHROOT_REL) { /* it's not mounted anywhere */
/* resolve paths relative to chroot */ res = dentry_path(path->dentry, buf, buflen);
get_fs_root(current->fs, &root); *name = res;
} else { if (IS_ERR(res)) {
/* resolve paths relative to namespace */ *name = buf;
root.mnt = current->nsproxy->mnt_ns->root; return PTR_ERR(res);
root.dentry = root.mnt->mnt_root; }
path_get(&root); if (path->dentry->d_sb->s_magic == PROC_SUPER_MAGIC &&
strncmp(*name, "/sys/", 5) == 0) {
/* TODO: convert over to using a per namespace
* control instead of hard coded /proc
*/
return prepend(name, *name - buf, "/proc", 5);
}
return 0;
} }
tmp = root; /* resolve paths relative to chroot?*/
res = __d_path(path, &tmp, buf, buflen); if (flags & PATH_CHROOT_REL) {
struct path root;
get_fs_root(current->fs, &root);
res = __d_path(path, &root, buf, buflen);
if (res && !IS_ERR(res)) {
/* everything's fine */
*name = res;
path_put(&root);
goto ok;
}
path_put(&root);
connected = 0;
}
res = d_absolute_path(path, buf, buflen);
*name = res; *name = res;
/* handle error conditions - and still allow a partial path to /* handle error conditions - and still allow a partial path to
@ -84,7 +105,10 @@ static int d_namespace_path(struct path *path, char *buf, int buflen,
*name = buf; *name = buf;
goto out; goto out;
} }
if (!our_mnt(path->mnt))
connected = 0;
ok:
/* Handle two cases: /* Handle two cases:
* 1. A deleted dentry && profile is not allowing mediation of deleted * 1. A deleted dentry && profile is not allowing mediation of deleted
* 2. On some filesystems, newly allocated dentries appear to the * 2. On some filesystems, newly allocated dentries appear to the
@ -97,10 +121,7 @@ static int d_namespace_path(struct path *path, char *buf, int buflen,
goto out; goto out;
} }
/* Determine if the path is connected to the expected root */ /* If the path is not connected to the expected root,
connected = tmp.dentry == root.dentry && tmp.mnt == root.mnt;
/* If the path is not connected,
* check if it is a sysctl and handle specially else remove any * check if it is a sysctl and handle specially else remove any
* leading / that __d_path may have returned. * leading / that __d_path may have returned.
* Unless * Unless
@ -112,17 +133,9 @@ static int d_namespace_path(struct path *path, char *buf, int buflen,
* namespace root. * namespace root.
*/ */
if (!connected) { if (!connected) {
/* is the disconnect path a sysctl? */ if (!(flags & PATH_CONNECT_PATH) &&
if (tmp.dentry->d_sb->s_magic == PROC_SUPER_MAGIC &&
strncmp(*name, "/sys/", 5) == 0) {
/* TODO: convert over to using a per namespace
* control instead of hard coded /proc
*/
error = prepend(name, *name - buf, "/proc", 5);
} else if (!(flags & PATH_CONNECT_PATH) &&
!(((flags & CHROOT_NSCONNECT) == CHROOT_NSCONNECT) && !(((flags & CHROOT_NSCONNECT) == CHROOT_NSCONNECT) &&
(tmp.mnt == current->nsproxy->mnt_ns->root && our_mnt(path->mnt))) {
tmp.dentry == tmp.mnt->mnt_root))) {
/* disconnected path, don't return pathname starting /* disconnected path, don't return pathname starting
* with '/' * with '/'
*/ */
@ -133,8 +146,6 @@ static int d_namespace_path(struct path *path, char *buf, int buflen,
} }
out: out:
path_put(&root);
return error; return error;
} }

View File

@ -101,9 +101,8 @@ static char *tomoyo_get_absolute_path(struct path *path, char * const buffer,
{ {
char *pos = ERR_PTR(-ENOMEM); char *pos = ERR_PTR(-ENOMEM);
if (buflen >= 256) { if (buflen >= 256) {
struct path ns_root = { };
/* go to whatever namespace root we are under */ /* go to whatever namespace root we are under */
pos = __d_path(path, &ns_root, buffer, buflen - 1); pos = d_absolute_path(path, buffer, buflen - 1);
if (!IS_ERR(pos) && *pos == '/' && pos[1]) { if (!IS_ERR(pos) && *pos == '/' && pos[1]) {
struct inode *inode = path->dentry->d_inode; struct inode *inode = path->dentry->d_inode;
if (inode && S_ISDIR(inode->i_mode)) { if (inode && S_ISDIR(inode->i_mode)) {