get_joliet_filename() writes into a buffer allocated by its caller but is not told how big that buffer is. It hardcodes PAGE_SIZE as the output limit for utf16s_to_utf8s() and gives uni16_to_x8() no limit at all. PAGE_SIZE was never the right bound. Before commit b2eb2e288604 ("isofs: Drop support of directory entries straddling blocks") both callers allocated a page, but only its first 1024 bytes were for the name, the rest holding a copy of a directory record that straddled a block. It could not overflow only because a directory record holds at most 222 bytes of name (255 bytes, 33 of them fixed), which no converter expands beyond 1024 bytes. It also keeps the converted length in an unsigned char. 222 bytes of name are 111 UTF-16 units, and the UTF-8 converter, used for iocharset=utf8 and when CONFIG_NLS_DEFAULT is "utf8", needs three bytes for each CJK, Thai or Devanagari character, so such a name can take up to 333 bytes. Past 255 the length wraps. readdir then reports a short name that usually ends in the middle of a UTF-8 sequence, and lookup finds the file only under that name, never under its real one; a name that wraps to exactly 0 bytes is not listed at all. Such names are out of spec, since Joliet allows 64 units, but common tools write them: PowerISO 9.5 and UltraISO 9.76 both keep up to 110 units by default. Windows, for which Joliet was made, returns them whole, including 111-unit names in records that leave out the padding byte. Return them whole here too. Pass the buffer size down and have both converters respect it. utf16s_to_utf8s() stops before a character that does not fit, and uni16_to_x8() now hands uni2char() the space that actually remains and stops on -ENAMETOOLONG, as fs/hfsplus/unicode.c does. Both callers pass JOLIET_NAME_MAX + 1, room for 111 units at three bytes each plus the terminator; no character set needs more than three bytes for a UTF-16 unit, and isofs_dir_record_valid() already rejects a record whose name_len claims more than the record holds, so no name is cut. Make the length an int, which is what both converters return. These names are longer than NAME_MAX. POSIX lets the limit vary by filesystem, reported by pathconf(_PC_NAME_MAX), and the VFS limits a name only by PATH_MAX (verify_dirent_name() in fs/readdir.c). vfat, exfat, hfsplus and ntfs3 already return names of up to 255 UTF-16 units, 765 bytes of UTF-8, and such names break the same things there: copying the file to a filesystem with a 255-byte limit, such as ext4 or tmpfs, fails with ENAMETOOLONG, and cp, tar, rsync and Python's shutil report that file and carry on with the rest, so the copy is incomplete but says so. glibc's readdir() returns the names, but the deprecated readdir_r() skips them and fails with ENAMETOOLONG, and an inotify reader with the buffer size inotify(7) suggests gets EINVAL. In the kernel, fanotify reports events on such a file without its name and warns once in fanotify_info_copy_name(), and a directory with such a name cannot be reconnected from a file handle, because the generic get_name() in fs/exportfs, which isofs uses, skips names longer than NAME_MAX. Cutting at NAME_MAX would avoid these, but would list names that Windows does not show and that can coincide within a directory. Every name of at most 255 bytes is returned exactly as before. Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2") Signed-off-by: Matthias Goergens --- fs/isofs/dir.c | 4 +++- fs/isofs/isofs.h | 11 ++++++++++- fs/isofs/joliet.c | 27 ++++++++++++++++++++------- fs/isofs/namei.c | 3 ++- 4 files changed, 35 insertions(+), 10 deletions(-) diff --git a/fs/isofs/dir.c b/fs/isofs/dir.c index 5e541e765f54..eebea133094d 100644 --- a/fs/isofs/dir.c +++ b/fs/isofs/dir.c @@ -195,7 +195,9 @@ static int do_isofs_readdir(struct inode *inode, struct file *file, if (map) { #ifdef CONFIG_JOLIET if (sbi->s_joliet_level) { - len = get_joliet_filename(de, tmpname, inode); + len = get_joliet_filename(de, tmpname, + JOLIET_NAME_MAX + 1, + inode); p = tmpname; } else #endif diff --git a/fs/isofs/isofs.h b/fs/isofs/isofs.h index 79ca0256843a..47c43a3c6a61 100644 --- a/fs/isofs/isofs.h +++ b/fs/isofs/isofs.h @@ -121,7 +121,16 @@ bool isofs_dir_record_valid(struct iso_directory_record *de, unsigned long offset, unsigned long bufsize); -int get_joliet_filename(struct iso_directory_record *, unsigned char *, struct inode *); +/* + * The longest name the Joliet converter returns, in bytes. A directory + * record is at most 255 bytes long, which leaves room for 111 UTF-16 units + * of name, and no character set needs more than three bytes for one unit. + */ +#define JOLIET_NAME_MAX \ + ((255 - sizeof(struct iso_directory_record)) / 2 * 3) + +int get_joliet_filename(struct iso_directory_record *de, unsigned char *outname, + int outsize, struct inode *inode); int get_acorn_filename(struct iso_directory_record *, char *, struct inode *); extern struct dentry *isofs_lookup(struct inode *, struct dentry *, unsigned int flags); diff --git a/fs/isofs/joliet.c b/fs/isofs/joliet.c index c0f04a1e7f69..d37e67c5e36f 100644 --- a/fs/isofs/joliet.c +++ b/fs/isofs/joliet.c @@ -15,19 +15,26 @@ * Convert Unicode 16 to UTF-8 or ASCII. */ static int -uni16_to_x8(unsigned char *ascii, __be16 *uni, int len, struct nls_table *nls) +uni16_to_x8(unsigned char *ascii, __be16 *uni, int len, struct nls_table *nls, + int outsize) { __be16 *ip, ch; - unsigned char *op; + unsigned char *op, *end; ip = uni; op = ascii; + end = ascii + outsize - 1; /* leave room for the terminator */ while ((ch = get_unaligned(ip)) && len) { int llen; - llen = nls->uni2char(be16_to_cpu(ch), op, NLS_MAX_CHARSET_SIZE); + + if (op >= end) + break; + llen = nls->uni2char(be16_to_cpu(ch), op, end - op); if (llen > 0) op += llen; + else if (llen == -ENAMETOOLONG) + break; else *op++ = '?'; ip++; @@ -38,21 +45,27 @@ uni16_to_x8(unsigned char *ascii, __be16 *uni, int len, struct nls_table *nls) return (op - ascii); } +/* + * Convert the Joliet name of @de into @outname, a buffer of @outsize bytes. + * The result is at most @outsize - 1 bytes long; a longer name is cut at a + * character boundary. + */ int -get_joliet_filename(struct iso_directory_record * de, unsigned char *outname, struct inode * inode) +get_joliet_filename(struct iso_directory_record *de, unsigned char *outname, + int outsize, struct inode *inode) { struct nls_table *nls; - unsigned char len = 0; + int len = 0; nls = ISOFS_SB(inode->i_sb)->s_nls_iocharset; if (!nls) { len = utf16s_to_utf8s((const wchar_t *) de->name, de->name_len[0] >> 1, UTF16_BIG_ENDIAN, - outname, PAGE_SIZE); + outname, outsize - 1); } else { len = uni16_to_x8(outname, (__be16 *) de->name, - de->name_len[0] >> 1, nls); + de->name_len[0] >> 1, nls, outsize); } if ((len > 2) && (outname[len-2] == ';') && (outname[len-1] == '1')) len -= 2; diff --git a/fs/isofs/namei.c b/fs/isofs/namei.c index e1c571478e8f..c65cc78583ba 100644 --- a/fs/isofs/namei.c +++ b/fs/isofs/namei.c @@ -109,7 +109,8 @@ isofs_find_entry(struct inode *dir, struct dentry *dentry, dpnt = tmpname; #ifdef CONFIG_JOLIET } else if (sbi->s_joliet_level) { - dlen = get_joliet_filename(de, tmpname, dir); + dlen = get_joliet_filename(de, tmpname, + JOLIET_NAME_MAX + 1, dir); dpnt = tmpname; #endif } else if (sbi->s_mapping == 'a') { -- 2.55.0