Skip to contentSkip to conversation

nfsd: fix rare symlink decoding bug

PATCH In review J. Bruce Fields 2 participants 7 messages Jun 23, 2014 – Jun 24, 2014

Subsystems (2)
lore
Share & tools
lore
#0 lore raw

PATCH 1/3 nfsd: fix rare symlink decoding bug

From: "J. Bruce Fields" <[email protected]>

An NFS operation that creates a new symlink includes the symlink data, which is xdr-encoded as a length followed by the data plus 0 to 3 bytes of zero-padding as required to reach a 4-byte boundary.

The vfs, on the other hand, wants null-terminated data.

The simple way to handle this would be by copying the data into a newly allocated buffer with space for the final null.

The current nfsd_symlink code tries to be more clever by skipping that step in the (likely) case where the byte following the string is already 0.

But that assumes that the byte following the string is ours to look at. In fact, it might be the first byte of a page that we can't read, or of some object that another task might modify.

Worse, the NFSv4 code tries to fix the problem by actually writing to that byte.

In the NFSv2/v3 cases this actually appears to be safe:

- nfs3svc_decode_symlinkargs explicitly null-terminates the data (after first checking its length and copying it to a new page).
- NFSv2 limits symlinks to 1k. The buffer holding the rpc request is always at least a page, and the link data (and previous fields) have maximum lengths that prevent the request from reaching the end of a page.

In the NFSv4 case the CREATE op is potentially just one part of a long compound so can end up on the end of a page if you're unlucky.

The minimal fix here is to copy and null-terminate in the NFSv4 case. The nfsd_symlink() interface here seems too fragile, though. It should really either do the copy itself every time or just require a null-terminated string.

---
 fs/nfsd/nfs4proc.c |   22 +++++++++++++---------
 1 file changed, 13 insertions(+), 9 deletions(-)
fs/nfsd/nfs4proc.c +13 -9
index 6851b00..453c907 100644
@@ -601,6 +601,8 @@ nfsd4_create(struct svc_rqst *rqstp, struct nfsd4_compound_state *cstate,
 {
 	struct svc_fh resfh;
 	__be32 status;
+	u32 len;
+	char *data;
 	dev_t rdev;
 
 	fh_init(&resfh, NFS4_FHSIZE);
@@ -617,19 +619,21 @@ nfsd4_create(struct svc_rqst *rqstp, struct nfsd4_compound_state *cstate,
 
 	switch (create->cr_type) {
 	case NF4LNK:
-		/* ugh! we have to null-terminate the linktext, or
-		 * vfs_symlink() will choke.  it is always safe to
-		 * null-terminate by brute force, since at worst we
-		 * will overwrite the first byte of the create namelen
-		 * in the XDR buffer, which has already been extracted
-		 * during XDR decode.
+		len = create->cr_linklen;
+		data = kmalloc(len + 1, GFP_KERNEL);
+		/*
+		 * Null-terminating in place isn't safe since
+		 * cr_linkname might end on a page boundary.
 		 */
-		create->cr_linkname[create->cr_linklen] = 0;
-
+		if (!data)
+			return nfserr_jukebox;
+		memcpy(data, create->cr_linkname, len + 1);
+		data[len] = '\0';
 		status = nfsd_symlink(rqstp, &cstate->current_fh,
 				      create->cr_name, create->cr_namelen,
-				      create->cr_linkname, create->cr_linklen,
+				      data, len,
 				      &resfh, &create->cr_iattr);
+		kfree(data);
 		break;
 
 	case NF4BLK:
Signature · 1 line
1.7.9.5
#1 lore raw

PATCH 2/3 nfsd: make NFSv2 null terminate symlink data

From: "J. Bruce Fields" <[email protected]>

It's simple enough for NFSv2 to null-terminate the symlink data.

A bit weird (it depends on knowing that we've already read the following byte, which is either padding or part of the mode), but no worse than the conditional kstrdup it otherwise relies on in nfsd_symlink().

---
 fs/nfsd/nfsproc.c |    5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)
fs/nfsd/nfsproc.c +4 -1
index 54c6b3d..aebe23c 100644
@@ -403,8 +403,11 @@ nfsd_proc_symlink(struct svc_rqst *rqstp, struct nfsd_symlinkargs *argp,
 
 	fh_init(&newfh, NFS_FHSIZE);
 	/*
-	 * Create the link, look up new file and set attrs.
+	 * Crazy hack: the request fits in a page, and already-decoded
+	 * attributes follow argp->tname, so it's safe to just write a
+	 * null to ensure it's null-terminated:
 	 */
+	argp->tname[argp->tlen] = '\0';
 	nfserr = nfsd_symlink(rqstp, &argp->ffh, argp->fname, argp->flen,
 						 argp->tname, argp->tlen,
 				 		 &newfh, &argp->attrs);
Signature · 1 line
1.7.9.5
#2 lore raw

PATCH 3/3 nfsd: let nfsd_symlink assume null-terminated data

In reply to J. Bruce Fields

From: "J. Bruce Fields" <[email protected]>

Currently nfsd_symlink has a weird hack to serve callers who don't null-terminate symlink data: it looks ahead at the next byte to see if it's zero, and copies it to a new buffer to null-terminate if not.

That means callers don't have to null-terminate, but they *do* have to ensure that the byte following the end of the data is theirs to read.

That's a bit subtle, and the NFSv4 code actually got this wrong.

So let's just throw out that code and let callers pass null-terminated strings; we've already fixed them to do that.

---
 fs/nfsd/nfs3proc.c |    2 +-
 fs/nfsd/nfs4proc.c |    3 +--
 fs/nfsd/nfsproc.c  |    2 +-
 fs/nfsd/vfs.c      |   17 +++--------------
 fs/nfsd/vfs.h      |    2 +-
 5 files changed, 7 insertions(+), 19 deletions(-)
fs/nfsd/nfs3proc.c +1 -1
index 4012899..4abf4dc 100644
@@ -286,7 +286,7 @@ nfsd3_proc_symlink(struct svc_rqst *rqstp, struct nfsd3_symlinkargs *argp,
 	fh_copy(&resp->dirfh, &argp->ffh);
 	fh_init(&resp->fh, NFS3_FHSIZE);
 	nfserr = nfsd_symlink(rqstp, &resp->dirfh, argp->fname, argp->flen,
-						   argp->tname, argp->tlen,
+						   argp->tname,
 						   &resp->fh, &argp->attrs);
 	RETURN_STATUS(nfserr);
 }
fs/nfsd/nfs4proc.c +1 -2
index 453c907..4ee72a3 100644
@@ -631,8 +631,7 @@ nfsd4_create(struct svc_rqst *rqstp, struct nfsd4_compound_state *cstate,
 		data[len] = '\0';
 		status = nfsd_symlink(rqstp, &cstate->current_fh,
 				      create->cr_name, create->cr_namelen,
-				      data, len,
-				      &resfh, &create->cr_iattr);
+				      data, &resfh, &create->cr_iattr);
 		kfree(data);
 		break;
 
fs/nfsd/nfsproc.c +1 -1
index aebe23c..583ed03 100644
@@ -409,7 +409,7 @@ nfsd_proc_symlink(struct svc_rqst *rqstp, struct nfsd_symlinkargs *argp,
 	 */
 	argp->tname[argp->tlen] = '\0';
 	nfserr = nfsd_symlink(rqstp, &argp->ffh, argp->fname, argp->flen,
-						 argp->tname, argp->tlen,
+						 argp->tname,
 				 		 &newfh, &argp->attrs);
 
 
fs/nfsd/vfs.c +3 -14
index 140c496..575fa9c 100644
@@ -1504,7 +1504,7 @@ out_nfserr:
 __be32
 nfsd_symlink(struct svc_rqst *rqstp, struct svc_fh *fhp,
 				char *fname, int flen,
-				char *path,  int plen,
+				char *path,
 				struct svc_fh *resfhp,
 				struct iattr *iap)
 {
@@ -1513,7 +1513,7 @@ nfsd_symlink(struct svc_rqst *rqstp, struct svc_fh *fhp,
 	int		host_err;
 
 	err = nfserr_noent;
-	if (!flen || !plen)
+	if (!flen || path[0] == '\0')
 		goto out;
 	err = nfserr_exist;
 	if (isdotent(fname, flen))
@@ -1534,18 +1534,7 @@ nfsd_symlink(struct svc_rqst *rqstp, struct svc_fh *fhp,
 	if (IS_ERR(dnew))
 		goto out_nfserr;
 
-	if (unlikely(path[plen] != 0)) {
-		char *path_alloced = kmalloc(plen+1, GFP_KERNEL);
-		if (path_alloced == NULL)
-			host_err = -ENOMEM;
-		else {
-			strncpy(path_alloced, path, plen);
-			path_alloced[plen] = 0;
-			host_err = vfs_symlink(dentry->d_inode, dnew, path_alloced);
-			kfree(path_alloced);
-		}
-	} else
-		host_err = vfs_symlink(dentry->d_inode, dnew, path);
+	host_err = vfs_symlink(dentry->d_inode, dnew, path);
 	err = nfserrno(host_err);
 	if (!err)
 		err = nfserrno(commit_metadata(fhp));
fs/nfsd/vfs.h +1 -1
index 91b6ae3..9b9eb91 100644
@@ -85,7 +85,7 @@ __be32 		nfsd_write(struct svc_rqst *, struct svc_fh *,struct file *,
 __be32		nfsd_readlink(struct svc_rqst *, struct svc_fh *,
 				char *, int *);
 __be32		nfsd_symlink(struct svc_rqst *, struct svc_fh *,
-				char *name, int len, char *path, int plen,
+				char *name, int len, char *path,
 				struct svc_fh *res, struct iattr *);
 __be32		nfsd_link(struct svc_rqst *, struct svc_fh *,
 				char *, int, struct svc_fh *);
Signature · 1 line
1.7.9.5
#3 lore raw
In reply to J. Bruce Fields
Quoted 70 lines · J. Bruce Fields

From: "J. Bruce Fields" <[email protected]>

An NFS operation that creates a new symlink includes the symlink data, which is xdr-encoded as a length followed by the data plus 0 to 3 bytes of zero-padding as required to reach a 4-byte boundary.

The vfs, on the other hand, wants null-terminated data.

The simple way to handle this would be by copying the data into a newly allocated buffer with space for the final null.

The current nfsd_symlink code tries to be more clever by skipping that step in the (likely) case where the byte following the string is already 0.

But that assumes that the byte following the string is ours to look at. In fact, it might be the first byte of a page that we can't read, or of some object that another task might modify.

Worse, the NFSv4 code tries to fix the problem by actually writing to that byte.

In the NFSv2/v3 cases this actually appears to be safe:

- nfs3svc_decode_symlinkargs explicitly null-terminates the data (after first checking its length and copying it to a new page).
- NFSv2 limits symlinks to 1k. The buffer holding the rpc request is always at least a page, and the link data (and previous fields) have maximum lengths that prevent the request from reaching the end of a page.

In the NFSv4 case the CREATE op is potentially just one part of a long compound so can end up on the end of a page if you're unlucky.

The minimal fix here is to copy and null-terminate in the NFSv4 case. The nfsd_symlink() interface here seems too fragile, though. It should really either do the copy itself every time or just require a null-terminated string.

---
 fs/nfsd/nfs4proc.c |   22 +++++++++++++---------
 1 file changed, 13 insertions(+), 9 deletions(-)
fs/nfsd/nfs4proc.c +3 -6
index 6851b00..453c907 100644
@@ -601,6 +601,8 @@ nfsd4_create(struct svc_rqst *rqstp, struct nfsd4_compound_state *cstate,
 {
        struct svc_fh resfh;
        __be32 status;
+       u32 len;
+       char *data;
        dev_t rdev;
 
        fh_init(&resfh, NFS4_FHSIZE);
@@ -617,19 +619,21 @@ nfsd4_create(struct svc_rqst *rqstp, struct nfsd4_compound_state *cstate,
 
        switch (create->cr_type) {
        case NF4LNK:
-               /* ugh! we have to null-terminate the linktext, or
-                * vfs_symlink() will choke.  it is always safe to
-                * null-terminate by brute force, since at worst we
-                * will overwrite the first byte of the create namelen
-                * in the XDR buffer, which has already been extracted
-                * during XDR decode.
+               len = create->cr_linklen;

fs/nfsd/nfs4proc.c nfsd4_create(struct svc_rqst *rqstp, struct nfsd4_compound_state *cstate, · line 630

Quoted 10 lines
fs/nfsd/nfs4proc.c +7 -2
+               data = kmalloc(len + 1, GFP_KERNEL);
+               /*
+                * Null-terminating in place isn't safe since
+                * cr_linkname might end on a page boundary.
                 */
-               create->cr_linkname[create->cr_linklen] = 0;
-
+               if (!data)
+                       return nfserr_jukebox;
+               memcpy(data, create->cr_linkname, len + 1);

Shouldn't that be a copy of 'len' bytes?

Quoted 17 lines
fs/nfsd/nfs4proc.c +3 -2
+               data[len] = '\0';
                status = nfsd_symlink(rqstp, &cstate->current_fh,
                                      create->cr_name, create->cr_namelen,
-                                     create->cr_linkname, create->cr_linklen,
+                                     data, len,
                                      &resfh, &create->cr_iattr);
+               kfree(data);
                break;
 
        case NF4BLK:
--

1.7.9.5

--
To unsubscribe from this list: send the line "unsubscribe linux-nfs" in the body of a message to [email protected]
More majordomo info at http://vger.kernel.org/majordomo-info.html

Signature · 5 lines
Trond Myklebust

Linux NFS client maintainer, PrimaryData

[email protected]
#4 lore raw
Quoted 13 lines · Trond Myklebust

Review

Quoted 11 lines · J. Bruce Fields
(quoted diff) +8 -2
+               len = create->cr_linklen;
+               data = kmalloc(len + 1, GFP_KERNEL);
+               /*
+                * Null-terminating in place isn't safe since
+                * cr_linkname might end on a page boundary.
                 */
-               create->cr_linkname[create->cr_linklen] = 0;
-
+               if (!data)
+                       return nfserr_jukebox;
+               memcpy(data, create->cr_linkname, len + 1);
Quoted 1 line

Shouldn't that be a copy of 'len' bytes?

Doh, yes.

After sending out I realized that we'd probably rather than do a single alloc&copy instead of two, so I'm thinking of doing the following (still testing).

But my first draft of that had the same problem that you pointed out here!

--b.

commit f719db9342235b3ebd4d65b9944fce9168177682

Author: J. Bruce Fields <[email protected]>
Date:   Thu Jun 19 16:44:48 2014 -0400

nfsd: fix rare symlink decoding bug

An NFS operation that creates a new symlink includes the symlink data, which is xdr-encoded as a length followed by the data plus 0 to 3 bytes of zero-padding as required to reach a 4-byte boundary.

The vfs, on the other hand, wants null-terminated data.

The simple way to handle this would be by copying the data into a newly allocated buffer with space for the final null.

The current nfsd_symlink code tries to be more clever by skipping that step in the (likely) case where the byte following the string is already 0.

But that assumes that the byte following the string is ours to look at. In fact, it might be the first byte of a page that we can't read, or of some object that another task might modify.

Worse, the NFSv4 code tries to fix the problem by actually writing to that byte.

In the NFSv2/v3 cases this actually appears to be safe:

- nfs3svc_decode_symlinkargs explicitly null-terminates the data (after first checking its length and copying it to a new page).
- NFSv2 limits symlinks to 1k. The buffer holding the rpc request is always at least a page, and the link data (and previous fields) have maximum lengths that prevent the request from reaching the end of a page.

In the NFSv4 case the CREATE op is potentially just one part of a long compound so can end up on the end of a page if you're unlucky.

The minimal fix here is to copy and null-terminate in the NFSv4 case. The nfsd_symlink() interface here seems too fragile, though. It should really either do the copy itself every time or just require a null-terminated string.

fs/nfsd/nfs4proc.c +0 -9
index 6851b00..8f029db 100644
@@ -617,15 +617,6 @@ nfsd4_create(struct svc_rqst *rqstp, struct nfsd4_compound_state *cstate,
 
 	switch (create->cr_type) {
 	case NF4LNK:
-		/* ugh! we have to null-terminate the linktext, or
-		 * vfs_symlink() will choke.  it is always safe to
-		 * null-terminate by brute force, since at worst we
-		 * will overwrite the first byte of the create namelen
-		 * in the XDR buffer, which has already been extracted
-		 * during XDR decode.
-		 */
-		create->cr_linkname[create->cr_linklen] = 0;
-
 		status = nfsd_symlink(rqstp, &cstate->current_fh,
 				      create->cr_name, create->cr_namelen,
 				      create->cr_linkname, create->cr_linklen,
fs/nfsd/nfs4xdr.c +12 -1
index 2d305a1..56bdf4a 100644
@@ -600,7 +600,18 @@ nfsd4_decode_create(struct nfsd4_compoundargs *argp, struct nfsd4_create *create
 		READ_BUF(4);
 		create->cr_linklen = be32_to_cpup(p++);
 		READ_BUF(create->cr_linklen);
-		SAVEMEM(create->cr_linkname, create->cr_linklen);
+		/*
+		 * The VFS will want a null-terminated string, and
+		 * null-terminating in place isn't safe since this might
+		 * end on a page boundary:
+		 */
+		create->cr_linkname =
+				kmalloc(create->cr_linklen + 1, GFP_KERNEL);
+		if (!create->cr_linkname)
+			return nfserr_jukebox;
+		memcpy(create->cr_linkname, p, create->cr_linklen);
+		create->cr_linkname[create->cr_linklen] = '\0';
+		defer_free(argp, kfree, create->cr_linkname);
 		break;
 	case NF4BLK:
 	case NF4CHR:
#5 lore raw
Quoted 14 lines · J. Bruce Fields
fs/nfsd/nfs4xdr.c +5 -1
index 2d305a1..56bdf4a 100644
@@ -600,7 +600,18 @@ nfsd4_decode_create(struct nfsd4_compoundargs *argp, struct nfsd4_create *create
                READ_BUF(4);
                create->cr_linklen = be32_to_cpup(p++);
                READ_BUF(create->cr_linklen);
-               SAVEMEM(create->cr_linkname, create->cr_linklen);
+               /*
+                * The VFS will want a null-terminated string, and
+                * null-terminating in place isn't safe since this might
+                * end on a page boundary:
+                */

fs/nfsd/nfs4xdr.c nfsd4_decode_create(struct nfsd4_compoundargs *argp, struct nfsd4_create *create · line 617

Quoted 10 lines
fs/nfsd/nfs4xdr.c +7 -0
+               create->cr_linkname =
+                               kmalloc(create->cr_linklen + 1, GFP_KERNEL);
+               if (!create->cr_linkname)
+                       return nfserr_jukebox;
+               memcpy(create->cr_linkname, p, create->cr_linklen);
+               create->cr_linkname[create->cr_linklen] = '\0';
+               defer_free(argp, kfree, create->cr_linkname);
                break;
        case NF4BLK:
        case NF4CHR:

Note that "defer_free()" does yet another allocation here in order to make space for a small 'struct tmpbuf' structure. Unlike the first allocation, there is no check for whether or not that second allocation is successful above, so you can easily end up with a silent memory leakage (ditto for a number of other callers of defer_free()).

Looking around at all the other users, wouldn't it perhaps make sense to replace defer_free() with a helper that does just a single allocation for both the memory buffer and the struct tmpbuf?

Cheers
Trond

Signature · 5 lines
Trond Myklebust

Linux NFS client maintainer, PrimaryData

[email protected]
#6 lore raw
Quoted 32 lines · Trond Myklebust
Quoted 14 lines · J. Bruce Fields
fs/nfsd/nfs4xdr.c +5 -1
index 2d305a1..56bdf4a 100644
@@ -600,7 +600,18 @@ nfsd4_decode_create(struct nfsd4_compoundargs *argp, struct nfsd4_create *create
                READ_BUF(4);
                create->cr_linklen = be32_to_cpup(p++);
                READ_BUF(create->cr_linklen);
-               SAVEMEM(create->cr_linkname, create->cr_linklen);
+               /*
+                * The VFS will want a null-terminated string, and
+                * null-terminating in place isn't safe since this might
+                * end on a page boundary:
+                */

fs/nfsd/nfs4xdr.c nfsd4_decode_create(struct nfsd4_compoundargs *argp, struct nfsd4_create *create · line 617

Quoted 10 lines
fs/nfsd/nfs4xdr.c +7 -0
+               create->cr_linkname =
+                               kmalloc(create->cr_linklen + 1, GFP_KERNEL);
+               if (!create->cr_linkname)
+                       return nfserr_jukebox;
+               memcpy(create->cr_linkname, p, create->cr_linklen);
+               create->cr_linkname[create->cr_linklen] = '\0';
+               defer_free(argp, kfree, create->cr_linkname);
                break;
        case NF4BLK:
        case NF4CHR:

Note that "defer_free()" does yet another allocation here in order to make space for a small 'struct tmpbuf' structure. Unlike the first allocation, there is no check for whether or not that second allocation is successful above, so you can easily end up with a silent memory leakage (ditto for a number of other callers of defer_free()).

Quoted 3 lines

Looking around at all the other users, wouldn't it perhaps make sense to replace defer_free() with a helper that does just a single allocation for both the memory buffer and the struct tmpbuf?

Yeah, thanks, working on it....

--b.