CVE-2014-9731: DEPRECATED: Code (CWE-17)
value: The UDF filesystem implementation in the Linux kernel before 3.18.2 does not ensure that space is available for storing a symlink target's name along with a trailing \0 character, which allows local users to obtain sensitive information via a crafted filesystem image, related to fs/udf/symlink.c and fs/udf/unicode.c.
CWE description: This entry has been deprecated.  It was originally used for organizing the Development View (CWE-699) and some other views, but it introduced unnecessary complexity and depth to the resulting tree.
Published: 2015-08-31T10:59Z
Repository: https://github.com/torvalds/linux
Language: C
Files changed: fs/udf/dir.c, fs/udf/namei.c, fs/udf/symlink.c, fs/udf/udfdecl.h, fs/udf/unicode.c
Commit date: 2014-12-19 14:12:08 +0100
Commit message: udf: Check path length when reading symlink

Symlink reading code does not check whether the resulting path fits into
the page provided by the generic code. This isn't as easy as just
checking the symlink size because of various encoding conversions we
perform on path. So we have to check whether there is still enough space
in the buffer on the fly.

CC: stable@vger.kernel.org
Reported-by: Carl Henrik Lunde <chlunde@ping.uio.no>
Signed-off-by: Jan Kara <jack@suse.cz>

Diff: diff --git a/fs/udf/dir.c b/fs/udf/dir.c
index a012c51caffd2a..a7690b46ce0a18 100644
--- a/fs/udf/dir.c
+++ b/fs/udf/dir.c
@@ -165,11 +165,12 @@ static int udf_readdir(struct file *file, struct dir_context *ctx)
 			if (!dir_emit_dotdot(file, ctx))
 				goto out;
 			continue;
 		}
 
-		flen = udf_get_filename(dir->i_sb, nameptr, fname, lfi);
+		flen = udf_get_filename(dir->i_sb, nameptr, lfi, fname,
+					UDF_NAME_LEN);
 		if (!flen)
 			continue;
 
 		tloc = lelb_to_cpu(cfi.icb.extLocation);
 		iblock = udf_get_lb_pblock(dir->i_sb, &tloc, 0);
diff --git a/fs/udf/namei.c b/fs/udf/namei.c
index c12e260fd6c417..6ff19b54b51f28 100644
--- a/fs/udf/namei.c
+++ b/fs/udf/namei.c
@@ -231,11 +231,12 @@ static struct fileIdentDesc *udf_find_entry(struct inode *dir,
 			goto out_ok;
 
 		if (!lfi)
 			continue;
 
-		flen = udf_get_filename(dir->i_sb, nameptr, fname, lfi);
+		flen = udf_get_filename(dir->i_sb, nameptr, lfi, fname,
+					UDF_NAME_LEN);
 		if (flen && udf_match(flen, fname, child->len, child->name))
 			goto out_ok;
 	}
 
 out_err:
diff --git a/fs/udf/symlink.c b/fs/udf/symlink.c
index c3aa6fafd6cf69..0f1b3a2654b945 100644
--- a/fs/udf/symlink.c
+++ b/fs/udf/symlink.c
@@ -28,17 +28,20 @@
 #include <linux/stat.h>
 #include <linux/pagemap.h>
 #include <linux/buffer_head.h>
 #include "udf_i.h"
 
-static void udf_pc_to_char(struct super_block *sb, unsigned char *from,
-			   int fromlen, unsigned char *to)
+static int udf_pc_to_char(struct super_block *sb, unsigned char *from,
+			  int fromlen, unsigned char *to, int tolen)
 {
 	struct pathComponent *pc;
 	int elen = 0;
+	int comp_len;
 	unsigned char *p = to;
 
+	/* Reserve one byte for terminating \0 */
+	tolen--;
 	while (elen < fromlen) {
 		pc = (struct pathComponent *)(from + elen);
 		switch (pc->componentType) {
 		case 1:
 			/*
@@ -47,34 +50,50 @@ static void udf_pc_to_char(struct super_block *sb, unsigned char *from,
 			 */
 			if (pc->lengthComponentIdent > 0)
 				break;
 			/* Fall through */
 		case 2:
+			if (tolen == 0)
+				return -ENAMETOOLONG;
 			p = to;
 			*p++ = '/';
+			tolen--;
 			break;
 		case 3:
+			if (tolen < 3)
+				return -ENAMETOOLONG;
 			memcpy(p, "../", 3);
 			p += 3;
+			tolen -= 3;
 			break;
 		case 4:
+			if (tolen < 2)
+				return -ENAMETOOLONG;
 			memcpy(p, "./", 2);
 			p += 2;
+			tolen -= 2;
 			/* that would be . - just ignore */
 			break;
 		case 5:
-			p += udf_get_filename(sb, pc->componentIdent, p,
-					      pc->lengthComponentIdent);
+			comp_len = udf_get_filename(sb, pc->componentIdent,
+						    pc->lengthComponentIdent,
+						    p, tolen);
+			p += comp_len;
+			tolen -= comp_len;
+			if (tolen == 0)
+				return -ENAMETOOLONG;
 			*p++ = '/';
+			tolen--;
 			break;
 		}
 		elen += sizeof(struct pathComponent) + pc->lengthComponentIdent;
 	}
 	if (p > to + 1)
 		p[-1] = '\0';
 	else
 		p[0] = '\0';
+	return 0;
 }
 
 static int udf_symlink_filler(struct file *file, struct page *page)
 {
 	struct inode *inode = page->mapping->host;
@@ -106,12 +125,14 @@ static int udf_symlink_filler(struct file *file, struct page *page)
 		}
 
 		symlink = bh->b_data;
 	}
 
-	udf_pc_to_char(inode->i_sb, symlink, inode->i_size, p);
+	err = udf_pc_to_char(inode->i_sb, symlink, inode->i_size, p, PAGE_SIZE);
 	brelse(bh);
+	if (err)
+		goto out_unlock_inode;
 
 	up_read(&iinfo->i_data_sem);
 	SetPageUptodate(page);
 	kunmap(page);
 	unlock_page(page);
diff --git a/fs/udf/udfdecl.h b/fs/udf/udfdecl.h
index 1cc3c993ebd04f..47bb3f5ca360d4 100644
--- a/fs/udf/udfdecl.h
+++ b/fs/udf/udfdecl.h
@@ -209,11 +209,12 @@ udf_get_lb_pblock(struct super_block *sb, struct kernel_lb_addr *loc,
 	return udf_get_pblock(sb, loc->logicalBlockNum,
 			loc->partitionReferenceNum, offset);
 }
 
 /* unicode.c */
-extern int udf_get_filename(struct super_block *, uint8_t *, uint8_t *, int);
+extern int udf_get_filename(struct super_block *, uint8_t *, int, uint8_t *,
+			    int);
 extern int udf_put_filename(struct super_block *, const uint8_t *, uint8_t *,
 			    int);
 extern int udf_build_ustr(struct ustr *, dstring *, int);
 extern int udf_CS0toUTF8(struct ustr *, const struct ustr *);
 
diff --git a/fs/udf/unicode.c b/fs/udf/unicode.c
index afd470e588ffbb..b84fee372734bd 100644
--- a/fs/udf/unicode.c
+++ b/fs/udf/unicode.c
@@ -26,11 +26,12 @@
 #include <linux/crc-itu-t.h>
 #include <linux/slab.h>
 
 #include "udf_sb.h"
 
-static int udf_translate_to_linux(uint8_t *, uint8_t *, int, uint8_t *, int);
+static int udf_translate_to_linux(uint8_t *, int, uint8_t *, int, uint8_t *,
+				  int);
 
 static int udf_char_to_ustr(struct ustr *dest, const uint8_t *src, int strlen)
 {
 	if ((!dest) || (!src) || (!strlen) || (strlen > UDF_NAME_LEN - 2))
 		return 0;
@@ -331,12 +332,12 @@ static int udf_NLStoCS0(struct nls_table *nls, dstring *ocu, struct ustr *uni,
 
 	ocu[length - 1] = (uint8_t)u_len + 1;
 	return u_len + 1;
 }
 
-int udf_get_filename(struct super_block *sb, uint8_t *sname, uint8_t *dname,
-		     int flen)
+int udf_get_filename(struct super_block *sb, uint8_t *sname, int slen,
+		     uint8_t *dname, int dlen)
 {
 	struct ustr *filename, *unifilename;
 	int len = 0;
 
 	filename = kmalloc(sizeof(struct ustr), GFP_NOFS);
@@ -345,11 +346,11 @@ int udf_get_filename(struct super_block *sb, uint8_t *sname, uint8_t *dname,
 
 	unifilename = kmalloc(sizeof(struct ustr), GFP_NOFS);
 	if (!unifilename)
 		goto out1;
 
-	if (udf_build_ustr_exact(unifilename, sname, flen))
+	if (udf_build_ustr_exact(unifilename, sname, slen))
 		goto out2;
 
 	if (UDF_QUERY_FLAG(sb, UDF_FLAG_UTF8)) {
 		if (!udf_CS0toUTF8(filename, unifilename)) {
 			udf_debug("Failed in udf_get_filename: sname = %s\n",
@@ -364,11 +365,12 @@ int udf_get_filename(struct super_block *sb, uint8_t *sname, uint8_t *dname,
 			goto out2;
 		}
 	} else
 		goto out2;
 
-	len = udf_translate_to_linux(dname, filename->u_name, filename->u_len,
+	len = udf_translate_to_linux(dname, dlen,
+				     filename->u_name, filename->u_len,
 				     unifilename->u_name, unifilename->u_len);
 out2:
 	kfree(unifilename);
 out1:
 	kfree(filename);
@@ -401,14 +403,16 @@ int udf_put_filename(struct super_block *sb, const uint8_t *sname,
 
 #define ILLEGAL_CHAR_MARK	'_'
 #define EXT_MARK		'.'
 #define CRC_MARK		'#'
 #define EXT_SIZE 		5
+/* Number of chars we need to store generated CRC to make filename unique */
+#define CRC_LEN			5
 
-static int udf_translate_to_linux(uint8_t *newName, uint8_t *udfName,
-				  int udfLen, uint8_t *fidName,
-				  int fidNameLen)
+static int udf_translate_to_linux(uint8_t *newName, int newLen,
+				  uint8_t *udfName, int udfLen,
+				  uint8_t *fidName, int fidNameLen)
 {
 	int index, newIndex = 0, needsCRC = 0;
 	int extIndex = 0, newExtIndex = 0, hasExt = 0;
 	unsigned short valueCRC;
 	uint8_t curr;
@@ -437,11 +441,11 @@ static int udf_translate_to_linux(uint8_t *newName, uint8_t *udfName,
 					hasExt = 1;
 					extIndex = index;
 					newExtIndex = newIndex;
 				}
 			}
-			if (newIndex < 256)
+			if (newIndex < newLen)
 				newName[newIndex++] = curr;
 			else
 				needsCRC = 1;
 		}
 	}
@@ -465,17 +469,17 @@ static int udf_translate_to_linux(uint8_t *newName, uint8_t *udfName,
 						 udfName[extIndex + index + 2] == 0)))
 						index++;
 				}
 				ext[localExtIndex++] = curr;
 			}
-			maxFilenameLen = 250 - localExtIndex;
+			maxFilenameLen = newLen - CRC_LEN - localExtIndex;
 			if (newIndex > maxFilenameLen)
 				newIndex = maxFilenameLen;
 			else
 				newIndex = newExtIndex;
-		} else if (newIndex > 250)
-			newIndex = 250;
+		} else if (newIndex > newLen - CRC_LEN)
+			newIndex = newLen - CRC_LEN;
 		newName[newIndex++] = CRC_MARK;
 		valueCRC = crc_itu_t(0, fidName, fidNameLen);
 		newName[newIndex++] = hex_asc_upper_hi(valueCRC >> 8);
 		newName[newIndex++] = hex_asc_upper_lo(valueCRC >> 8);
 		newName[newIndex++] = hex_asc_upper_hi(valueCRC);

Vulnerable code: 		flen = udf_get_filename(dir->i_sb, nameptr, fname, lfi);
		flen = udf_get_filename(dir->i_sb, nameptr, fname, lfi);
static void udf_pc_to_char(struct super_block *sb, unsigned char *from,
			   int fromlen, unsigned char *to)
			p += udf_get_filename(sb, pc->componentIdent, p,
					      pc->lengthComponentIdent);
	udf_pc_to_char(inode->i_sb, symlink, inode->i_size, p);
extern int udf_get_filename(struct super_block *, uint8_t *, uint8_t *, int);
static int udf_translate_to_linux(uint8_t *, uint8_t *, int, uint8_t *, int);
int udf_get_filename(struct super_block *sb, uint8_t *sname, uint8_t *dname,
		     int flen)
	if (udf_build_ustr_exact(unifilename, sname, flen))
	len = udf_translate_to_linux(dname, filename->u_name, filename->u_len,
static int udf_translate_to_linux(uint8_t *newName, uint8_t *udfName,
				  int udfLen, uint8_t *fidName,
				  int fidNameLen)
			if (newIndex < 256)
			maxFilenameLen = 250 - localExtIndex;
		} else if (newIndex > 250)
			newIndex = 250;
Fixed code: 		flen = udf_get_filename(dir->i_sb, nameptr, lfi, fname,
					UDF_NAME_LEN);
		flen = udf_get_filename(dir->i_sb, nameptr, lfi, fname,
					UDF_NAME_LEN);
static int udf_pc_to_char(struct super_block *sb, unsigned char *from,
			  int fromlen, unsigned char *to, int tolen)
	int comp_len;
	/* Reserve one byte for terminating \0 */
	tolen--;
			if (tolen == 0)
				return -ENAMETOOLONG;
			tolen--;
			if (tolen < 3)
				return -ENAMETOOLONG;
			tolen -= 3;
			if (tolen < 2)
				return -ENAMETOOLONG;
			tolen -= 2;
			comp_len = udf_get_filename(sb, pc->componentIdent,
						    pc->lengthComponentIdent,
						    p, tolen);
			p += comp_len;
			tolen -= comp_len;
			if (tolen == 0)
				return -ENAMETOOLONG;
			tolen--;
	return 0;
	err = udf_pc_to_char(inode->i_sb, symlink, inode->i_size, p, PAGE_SIZE);
	if (err)
		goto out_unlock_inode;
extern int udf_get_filename(struct super_block *, uint8_t *, int, uint8_t *,
			    int);
static int udf_translate_to_linux(uint8_t *, int, uint8_t *, int, uint8_t *,
				  int);
int udf_get_filename(struct super_block *sb, uint8_t *sname, int slen,
		     uint8_t *dname, int dlen)
	if (udf_build_ustr_exact(unifilename, sname, slen))
	len = udf_translate_to_linux(dname, dlen,
				     filename->u_name, filename->u_len,
/* Number of chars we need to store generated CRC to make filename unique */
#define CRC_LEN			5
static int udf_translate_to_linux(uint8_t *newName, int newLen,
				  uint8_t *udfName, int udfLen,
				  uint8_t *fidName, int fidNameLen)
			if (newIndex < newLen)
			maxFilenameLen = newLen - CRC_LEN - localExtIndex;
		} else if (newIndex > newLen - CRC_LEN)
			newIndex = newLen - CRC_LEN;