exec: binfmt_misc: fix use-after-free, kill iname[BINPRM_BUF_SIZE]
Subsystems (2)
cover exec: binfmt_misc: fix use-after-free, kill iname[BINPRM_BUF_SIZE]
Note: 5/5 depends on
-extern int bprm_change_interp(char *interp, struct linux_binprm *bprm); +extern int bprm_change_interp(const char *interp, struct linux_binprm *bprm);
change in
[PATCH] exec: load_script: kill the onstack interp[BINPRM_BUF_SIZE] array https://marc.info/?l=linux-kernel&m=150575251328591
I sent before.
Looks like this code was always wrong, then 948b701a607f ("binfmt_misc: add persistent opened binary handler for containers") added more problems.
Oleg.
fs/binfmt_misc.c | 56 ++++++++++++++++++++++++++------------------------------ 1 file changed, 26 insertions(+), 30 deletions(-)
PATCH 1/5 exec: binfmt_misc: don't nullify Node->dentry in kill_node()
kill_node() nullifies/checks Node->dentry to avoid double free. This complicates the next changes and this is very confusing:
- we do not need to check dentry != NULL under entries_lock, kill_node() is always called under inode_lock(d_inode(root)) and we rely on this inode_lock() anyway, without this lock the MISC_FMT_OPEN_FILE cleanup could race with itself.
- if kill_inode() was already called and ->dentry == NULL we should not even try to close e->interp_file.
We can change bm_entry_write() to simply check !list_empty(list) before kill_node. Again, we rely on inode_lock(), in particular it saves us from the race with bm_status_write(), another caller of kill_node().
--- fs/binfmt_misc.c | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-)
fs/binfmt_misc.c +9 -13
@@ -600,11 +600,7 @@ static void kill_node(Node *e)struct dentry *dentry;write_lock (&entries_lock); - dentry = e->dentry; -if (dentry) { -list_del_init (&e->list); - e->dentry =NULL ; - } +list_del_init (&e->list);write_unlock (&entries_lock);if ((e->flags &MISC_FMT_OPEN_FILE ) && e->interp_file) {
@@ -612,12 +608,11 @@ static void kill_node(Node *e) e->interp_file =NULL ; } -if (dentry) { -drop_nlink (d_inode (dentry)); -d_drop (dentry); -dput (dentry); -simple_release_fs (&bm_mnt, &entry_count); - } + dentry = e->dentry; +drop_nlink (d_inode (dentry)); +d_drop (dentry); +dput (dentry); +simple_release_fs (&bm_mnt, &entry_count); }/* /<entry> */
@@ -662,7 +657,8 @@ static ssize_t bm_entry_write(struct file *file, const char __user *buffer, root =file_inode (file)->i_sb->s_root;inode_lock (d_inode (root)); -kill_node (e); +if (!list_empty (&e->list)) +kill_node (e);inode_unlock (d_inode (root));break ;
@@ -791,7 +787,7 @@ static ssize_t bm_status_write(struct file *file, const char __user *buffer,inode_lock (d_inode (root));while (!list_empty (&entries)) -kill_node (list_entry (entries.next, Node, list)); +kill_node (list_first_entry (&entries, Node, list));inode_unlock (d_inode (root));break ;
Signature · 1 line
2.5.0
PATCH 3/5 exec: binfmt_misc: remove the confusing e->interp_file != NULL checks
In reply to Oleg NesterovIf MISC_FMT_OPEN_FILE flag is set e->interp_file must be valid or we have a bug which should not be silently ignored.
--- fs/binfmt_misc.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-)
fs/binfmt_misc.c +2 -2
@@ -205,7 +205,7 @@ static int load_misc_binary(struct linux_binprm *bprm)if (retval <0 ) goto error; -if (fmt->flags &MISC_FMT_OPEN_FILE && fmt->interp_file) { +if (fmt->flags &MISC_FMT_OPEN_FILE ) { interp_file =filp_clone_open (fmt->interp_file);if (!IS_ERR (interp_file))deny_write_access (interp_file);
@@ -593,7 +593,7 @@ static void bm_evict_inode(struct inode *inode) {Node *e = inode->i_private; -if ((e->flags &MISC_FMT_OPEN_FILE ) && e->interp_file) +if (e->flags &MISC_FMT_OPEN_FILE )filp_close (e->interp_file,NULL );clear_inode (inode);
Signature · 1 line
2.5.0
PATCH 5/5 exec: binfmt_misc: kill the onstack iname[BINPRM_BUF_SIZE] array
In reply to Oleg NesterovAfter the previous change "fmt" can't go away, we can kill iname/iname_addr and use fmt->interpreter.
--- fs/binfmt_misc.c | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-)
fs/binfmt_misc.c +5 -9
@@ -54,7 +54,7 @@ typedef struct {int size;/* size of magic/mask */ char *magic;/* magic or filename extension */ char *mask;/* mask, NULL for exact match */ -char *interpreter;/* filename of interpreter */ +const char *interpreter;/* filename of interpreter */ char *name;struct dentry *dentry;struct file *interp_file;
@@ -131,8 +131,6 @@ static int load_misc_binary(struct linux_binprm *bprm) {Node *fmt;struct file *interp_file =NULL ; -char iname[BINPRM_BUF_SIZE ]; -const char *iname_addr = iname;int retval;int fd_binary =-1 ;
@@ -143,10 +141,8 @@ static int load_misc_binary(struct linux_binprm *bprm)/* to keep locking time low, we copy the interpreter string */ read_lock (&entries_lock); fmt =check_file (bprm); -if (fmt) { +if (fmt)dget (fmt->dentry); -strlcpy (iname, fmt->interpreter,BINPRM_BUF_SIZE ); - }read_unlock (&entries_lock);if (!fmt)return retval;
@@ -198,13 +194,13 @@ static int load_misc_binary(struct linux_binprm *bprm) bprm->argc++;/* add the interp as argv[0] */ - retval =copy_strings_kernel (1 , &iname_addr, bprm); + retval =copy_strings_kernel (1 , &fmt->interpreter, bprm);if (retval <0 ) goto error; bprm->argc++;/* Update interp in case binfmt_script needs it. */ - retval =bprm_change_interp (iname, bprm); + retval =bprm_change_interp (fmt->interpreter, bprm);if (retval <0 ) goto error;
@@ -213,7 +209,7 @@ static int load_misc_binary(struct linux_binprm *bprm)if (!IS_ERR (interp_file))deny_write_access (interp_file); }else { - interp_file =open_exec (iname); + interp_file =open_exec (fmt->interpreter); } retval =PTR_ERR (interp_file);if (IS_ERR (interp_file))
Signature · 1 line
2.5.0
PATCH 4/5 exec: binfmt_misc: fix race between load_misc_binary() and kill_node()
In reply to Oleg Nesterovload_misc_binary() makes a local copy of fmt->interpreter under entries_lock to avoid the race with kill_node() but this is not enough; the whole Node can be freed after we drop entries_lock, not only the ->interpreter string.
Add dget/dput(fmt->dentry) to ensure bm_evict_inode() can't destroy/free this Node.
--- fs/binfmt_misc.c | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-)
fs/binfmt_misc.c +8 -4
@@ -138,20 +138,23 @@ static int load_misc_binary(struct linux_binprm *bprm) retval = -ENOEXEC ;if (!enabled) - goto ret; +return retval;/* to keep locking time low, we copy the interpreter string */ read_lock (&entries_lock); fmt =check_file (bprm); -if (fmt) +if (fmt) { +dget (fmt->dentry);strlcpy (iname, fmt->interpreter,BINPRM_BUF_SIZE ); + }read_unlock (&entries_lock);if (!fmt) - goto ret; +return retval;/* Need to be able to load the file after exec */ + retval = -ENOENT ;if (bprm->interp_flags &BINPRM_FLAGS_PATH_INACCESSIBLE ) -return -ENOENT ; + goto ret;if (!(fmt->flags &MISC_FMT_PRESERVE_ARGV0 )) { retval =remove_arg_zero (bprm);
@@ -235,6 +238,7 @@ static int load_misc_binary(struct linux_binprm *bprm) goto error; ret: +dput (fmt->dentry);return retval; error:if (fd_binary >0 )
Signature · 1 line
2.5.0
PATCH 2/5 exec: binfmt_misc: shift filp_close(interp_file) from kill_node() to bm_evict_inode()
In reply to Oleg Nesterovto ensure that load_misc_binary() can't use the partially destroyed Node, see also the next patch.
The current logic looks wrong in any case, once we close interp_file it doesn't make any sense to delay kfree(inode->i_private), this Node is no longer valid. Even if the MISC_FMT_OPEN_FILE/interp_file checks were not racy (they are), load_misc_binary() should not try to reopen ->interpreter if MISC_FMT_OPEN_FILE is set but ->interp_file is NULL.
And I can't understand why do we use filp_close(), not fput().
--- fs/binfmt_misc.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-)
fs/binfmt_misc.c +6 -6
@@ -591,8 +591,13 @@ static struct inode *bm_get_inode(struct super_block *sb, int mode)static void bm_evict_inode (struct inode *inode) { +Node *e = inode->i_private; + +if ((e->flags &MISC_FMT_OPEN_FILE ) && e->interp_file) +filp_close (e->interp_file,NULL ); +clear_inode (inode); -kfree (inode->i_private); +kfree (e); }static void kill_node (Node *e)
@@ -603,11 +608,6 @@ static void kill_node(Node *e)list_del_init (&e->list);write_unlock (&entries_lock); -if ((e->flags &MISC_FMT_OPEN_FILE ) && e->interp_file) { -filp_close (e->interp_file,NULL ); - e->interp_file =NULL ; - } - dentry = e->dentry;drop_nlink (d_inode (dentry));d_drop (dentry);
Signature · 1 line
2.5.0
Quoted 12 lines · Oleg Nesterov
Note: 5/5 depends on
-extern int bprm_change_interp(char *interp, struct linux_binprm *bprm); +extern int bprm_change_interp(const char *interp, struct linux_binprm *bprm);change in
[PATCH] exec:
load_script: kill the onstack interp[BINPRM_BUF_SIZE] array
https://marc.info/?l=linux-kernel&m=150575251328591I sent before.
Quoted 7 lines
Looks like this code was always wrong, then
948b701a607f("binfmt_misc: add persistent opened binary handler for containers") added more problems.Oleg.
fs/binfmt_misc.c | 56 ++++++++++++++++++++++++++------------------------------ 1 file changed, 26 insertions(+), 30 deletions(-)
This all looks correct to me, thanks!
-Kees
Signature · 2 lines
Kees Cook Pixel Security