union-mount: Implement union-aware truncate()

Original-author: Valerie Aurora <vaurora@redhat.com>
Signed-off-by: David Howells <dhowells@redhat.com> (Further development)
diff --git a/fs/namei.c b/fs/namei.c
index bab3a57..5cc70c7 100644
--- a/fs/namei.c
+++ b/fs/namei.c
@@ -1237,7 +1237,8 @@
 			goto out_err;
 	}
 
-	if (nd->flags & (LOOKUP_COPY_UP_TRUNC | LOOKUP_OPEN | LOOKUP_CREATE)) {
+	if (nd->flags & (LOOKUP_COPY_UP_TRUNC | LOOKUP_OPEN | LOOKUP_CREATE |
+			 LOOKUP_FOR_TRUNCATE)) {
 		err = __inode_permission(lower.dentry->d_inode, MAY_WRITE);
 		if (err < 0)
 			goto out_err;
diff --git a/fs/open.c b/fs/open.c
index dbaac6a..5ef49c0 100644
--- a/fs/open.c
+++ b/fs/open.c
@@ -65,14 +65,19 @@
 static long do_sys_truncate(const char __user *pathname, loff_t length)
 {
 	struct path path;
+	struct nameidata nd;
+	struct vfsmount *mnt;
 	struct inode *inode;
+	char *tmp;
 	int error;
 
 	error = -EINVAL;
 	if (length < 0)	/* sorry, but loff_t says... */
 		goto out;
 
-	error = user_path(pathname, &path);
+	error = user_path_nd(AT_FDCWD, pathname,
+			     LOOKUP_COPY_UP | LOOKUP_FOR_TRUNCATE,
+			     &nd, &path, &tmp);
 	if (error)
 		goto out;
 	inode = path.dentry->d_inode;
@@ -86,18 +91,45 @@
 	if (!S_ISREG(inode->i_mode))
 		goto dput_and_out;
 
-	error = mnt_want_write(path.mnt);
+	/* If we're looking at the lower layer of a union mount, then we need
+	 * to create the file on the upperfs and truncate that.
+	 */
+	if (IS_MNT_LOWER(path.mnt))
+		mnt = nd.path.mnt;
+	else
+		mnt = path.mnt;
+
+	error = mnt_want_write(mnt);
 	if (error)
 		goto dput_and_out;
 
-	error = inode_permission(inode, MAY_WRITE);
-	if (error)
-		goto mnt_drop_write_and_out;
+	if (unlikely(IS_MNT_UNION(mnt))) {
+		/* We have to be able to write to the upperfs. */
+		error = -EROFS;
+		if (mnt->mnt_sb->s_flags & MS_RDONLY)
+			goto mnt_drop_write_and_out;
+
+		/* But the lowerfs inode must offer write permission - if the
+		 * lowerfs was mounted writably. */
+		error = __inode_permission(inode, MAY_WRITE);
+		if (error)
+			goto mnt_drop_write_and_out;
+	} else {
+		error = inode_permission(inode, MAY_WRITE);
+		if (error)
+			goto mnt_drop_write_and_out;
+	}
 
 	error = -EPERM;
 	if (IS_APPEND(inode))
 		goto mnt_drop_write_and_out;
 
+	error = union_copyup_len(&nd, &path, length);
+	if (error)
+		goto mnt_drop_write_and_out;
+
+	/* path may have changed after copyup */
+	inode = path.dentry->d_inode;
 	error = get_write_access(inode);
 	if (error)
 		goto mnt_drop_write_and_out;
@@ -119,9 +151,11 @@
 put_write_and_out:
 	put_write_access(inode);
 mnt_drop_write_and_out:
-	mnt_drop_write(path.mnt);
+	mnt_drop_write(mnt);
 dput_and_out:
 	path_put(&path);
+	path_put(&nd.path);
+	putname(tmp);
 out:
 	return error;
 }
diff --git a/fs/union.c b/fs/union.c
index 2f1d95c..52f54e0 100644
--- a/fs/union.c
+++ b/fs/union.c
@@ -595,7 +595,7 @@
 	return error;
 }
 
-#if 0
+#if 1
 /**
  * __union_copyup_len - Copy up a file and len bytes of data
  * @nd: nameidata for topmost parent dir
diff --git a/fs/union.h b/fs/union.h
index b77b3a4..19c5228 100644
--- a/fs/union.h
+++ b/fs/union.h
@@ -182,6 +182,7 @@
 {
 	return do_union_copyup_len(nd, path, true, 0);
 }
+#endif
 
 /*
  * Helper function to copy up part of a file for truncate and O_TRUNC.
@@ -190,4 +191,3 @@
 {
 	return do_union_copyup_len(nd, path, false, len);
 }
-#endif
diff --git a/include/linux/namei.h b/include/linux/namei.h
index c4802ef..9648361 100644
--- a/include/linux/namei.h
+++ b/include/linux/namei.h
@@ -56,6 +56,7 @@
 #define LOOKUP_EMPTY		0x4000
 #define LOOKUP_COPY_UP		0x8000 /* Copy up from lower mount if unionmounted */
 #define LOOKUP_COPY_UP_TRUNC	0x10000 /* Copy up truncated to 0 (need file attributes) */
+#define LOOKUP_FOR_TRUNCATE	0x20000 /* truncate() is doing the lookup */
 
 extern int user_path_at(int, const char __user *, unsigned, struct path *);
 extern int user_path_at_empty(int, const char __user *, unsigned, struct path *, int *empty);