linux/fs/udf/directory.c
Jan Kara d16076d9b6 udf: New directory iteration code
Add new support code for iterating directory entries. The code is also
more carefully verifying validity of on-disk directory entries to avoid
crashes on malicious media.

Signed-off-by: Jan Kara <jack@suse.cz>
2023-01-09 10:39:51 +01:00

638 lines
16 KiB
C

/*
* directory.c
*
* PURPOSE
* Directory related functions
*
* COPYRIGHT
* This file is distributed under the terms of the GNU General Public
* License (GPL). Copies of the GPL can be obtained from:
* ftp://prep.ai.mit.edu/pub/gnu/GPL
* Each contributing author retains all rights to their own work.
*/
#include "udfdecl.h"
#include "udf_i.h"
#include <linux/fs.h>
#include <linux/string.h>
#include <linux/bio.h>
#include <linux/crc-itu-t.h>
#include <linux/iversion.h>
static int udf_verify_fi(struct udf_fileident_iter *iter)
{
unsigned int len;
if (iter->fi.descTag.tagIdent != cpu_to_le16(TAG_IDENT_FID)) {
udf_err(iter->dir->i_sb,
"directory (ino %lu) has entry at pos %llu with incorrect tag %x\n",
iter->dir->i_ino, (unsigned long long)iter->pos,
le16_to_cpu(iter->fi.descTag.tagIdent));
return -EFSCORRUPTED;
}
len = udf_dir_entry_len(&iter->fi);
if (le16_to_cpu(iter->fi.lengthOfImpUse) & 3) {
udf_err(iter->dir->i_sb,
"directory (ino %lu) has entry at pos %llu with unaligned lenght of impUse field\n",
iter->dir->i_ino, (unsigned long long)iter->pos);
return -EFSCORRUPTED;
}
/*
* This is in fact allowed by the spec due to long impUse field but
* we don't support it. If there is real media with this large impUse
* field, support can be added.
*/
if (len > 1 << iter->dir->i_blkbits) {
udf_err(iter->dir->i_sb,
"directory (ino %lu) has too big (%u) entry at pos %llu\n",
iter->dir->i_ino, len, (unsigned long long)iter->pos);
return -EFSCORRUPTED;
}
if (iter->pos + len > iter->dir->i_size) {
udf_err(iter->dir->i_sb,
"directory (ino %lu) has entry past directory size at pos %llu\n",
iter->dir->i_ino, (unsigned long long)iter->pos);
return -EFSCORRUPTED;
}
if (udf_dir_entry_len(&iter->fi) !=
sizeof(struct tag) + le16_to_cpu(iter->fi.descTag.descCRCLength)) {
udf_err(iter->dir->i_sb,
"directory (ino %lu) has entry where CRC length (%u) does not match entry length (%u)\n",
iter->dir->i_ino,
(unsigned)le16_to_cpu(iter->fi.descTag.descCRCLength),
(unsigned)(udf_dir_entry_len(&iter->fi) -
sizeof(struct tag)));
return -EFSCORRUPTED;
}
return 0;
}
static int udf_copy_fi(struct udf_fileident_iter *iter)
{
struct udf_inode_info *iinfo = UDF_I(iter->dir);
int blksize = 1 << iter->dir->i_blkbits;
int err, off, len, nameoff;
/* Skip copying when we are at EOF */
if (iter->pos >= iter->dir->i_size) {
iter->name = NULL;
return 0;
}
if (iter->dir->i_size < iter->pos + sizeof(struct fileIdentDesc)) {
udf_err(iter->dir->i_sb,
"directory (ino %lu) has entry straddling EOF\n",
iter->dir->i_ino);
return -EFSCORRUPTED;
}
if (iinfo->i_alloc_type == ICBTAG_FLAG_AD_IN_ICB) {
memcpy(&iter->fi, iinfo->i_data + iinfo->i_lenEAttr + iter->pos,
sizeof(struct fileIdentDesc));
err = udf_verify_fi(iter);
if (err < 0)
return err;
iter->name = iinfo->i_data + iinfo->i_lenEAttr + iter->pos +
sizeof(struct fileIdentDesc) +
le16_to_cpu(iter->fi.lengthOfImpUse);
return 0;
}
off = iter->pos & (blksize - 1);
len = min_t(int, sizeof(struct fileIdentDesc), blksize - off);
memcpy(&iter->fi, iter->bh[0]->b_data + off, len);
if (len < sizeof(struct fileIdentDesc))
memcpy((char *)(&iter->fi) + len, iter->bh[1]->b_data,
sizeof(struct fileIdentDesc) - len);
err = udf_verify_fi(iter);
if (err < 0)
return err;
/* Handle directory entry name */
nameoff = off + sizeof(struct fileIdentDesc) +
le16_to_cpu(iter->fi.lengthOfImpUse);
if (off + udf_dir_entry_len(&iter->fi) <= blksize) {
iter->name = iter->bh[0]->b_data + nameoff;
} else if (nameoff >= blksize) {
iter->name = iter->bh[1]->b_data + (nameoff - blksize);
} else {
iter->name = iter->namebuf;
len = blksize - nameoff;
memcpy(iter->name, iter->bh[0]->b_data + nameoff, len);
memcpy(iter->name + len, iter->bh[1]->b_data,
iter->fi.lengthFileIdent - len);
}
return 0;
}
/* Readahead 8k once we are at 8k boundary */
static void udf_readahead_dir(struct udf_fileident_iter *iter)
{
unsigned int ralen = 16 >> (iter->dir->i_blkbits - 9);
struct buffer_head *tmp, *bha[16];
int i, num;
udf_pblk_t blk;
if (iter->loffset & (ralen - 1))
return;
if (iter->loffset + ralen > (iter->elen >> iter->dir->i_blkbits))
ralen = (iter->elen >> iter->dir->i_blkbits) - iter->loffset;
num = 0;
for (i = 0; i < ralen; i++) {
blk = udf_get_lb_pblock(iter->dir->i_sb, &iter->eloc,
iter->loffset + i);
tmp = udf_tgetblk(iter->dir->i_sb, blk);
if (tmp && !buffer_uptodate(tmp) && !buffer_locked(tmp))
bha[num++] = tmp;
else
brelse(tmp);
}
if (num) {
bh_readahead_batch(num, bha, REQ_RAHEAD);
for (i = 0; i < num; i++)
brelse(bha[i]);
}
}
static struct buffer_head *udf_fiiter_bread_blk(struct udf_fileident_iter *iter)
{
udf_pblk_t blk;
udf_readahead_dir(iter);
blk = udf_get_lb_pblock(iter->dir->i_sb, &iter->eloc, iter->loffset);
return udf_tread(iter->dir->i_sb, blk);
}
/*
* Updates loffset to point to next directory block; eloc, elen & epos are
* updated if we need to traverse to the next extent as well.
*/
static int udf_fiiter_advance_blk(struct udf_fileident_iter *iter)
{
iter->loffset++;
if (iter->loffset < iter->elen >> iter->dir->i_blkbits)
return 0;
iter->loffset = 0;
if (udf_next_aext(iter->dir, &iter->epos, &iter->eloc, &iter->elen, 1)
!= (EXT_RECORDED_ALLOCATED >> 30)) {
if (iter->pos == iter->dir->i_size) {
iter->elen = 0;
return 0;
}
udf_err(iter->dir->i_sb,
"extent after position %llu not allocated in directory (ino %lu)\n",
(unsigned long long)iter->pos, iter->dir->i_ino);
return -EFSCORRUPTED;
}
return 0;
}
static int udf_fiiter_load_bhs(struct udf_fileident_iter *iter)
{
int blksize = 1 << iter->dir->i_blkbits;
int off = iter->pos & (blksize - 1);
int err;
struct fileIdentDesc *fi;
/* Is there any further extent we can map from? */
if (!iter->bh[0] && iter->elen) {
iter->bh[0] = udf_fiiter_bread_blk(iter);
if (!iter->bh[0]) {
err = -ENOMEM;
goto out_brelse;
}
if (!buffer_uptodate(iter->bh[0])) {
err = -EIO;
goto out_brelse;
}
}
/* There's no next block so we are done */
if (iter->pos >= iter->dir->i_size)
return 0;
/* Need to fetch next block as well? */
if (off + sizeof(struct fileIdentDesc) > blksize)
goto fetch_next;
fi = (struct fileIdentDesc *)(iter->bh[0]->b_data + off);
/* Need to fetch next block to get name? */
if (off + udf_dir_entry_len(fi) > blksize) {
fetch_next:
udf_fiiter_advance_blk(iter);
iter->bh[1] = udf_fiiter_bread_blk(iter);
if (!iter->bh[1]) {
err = -ENOMEM;
goto out_brelse;
}
if (!buffer_uptodate(iter->bh[1])) {
err = -EIO;
goto out_brelse;
}
}
return 0;
out_brelse:
brelse(iter->bh[0]);
brelse(iter->bh[1]);
iter->bh[0] = iter->bh[1] = NULL;
return err;
}
int udf_fiiter_init(struct udf_fileident_iter *iter, struct inode *dir,
loff_t pos)
{
struct udf_inode_info *iinfo = UDF_I(dir);
int err = 0;
iter->dir = dir;
iter->bh[0] = iter->bh[1] = NULL;
iter->pos = pos;
iter->elen = 0;
iter->epos.bh = NULL;
iter->name = NULL;
if (iinfo->i_alloc_type == ICBTAG_FLAG_AD_IN_ICB)
return udf_copy_fi(iter);
if (inode_bmap(dir, iter->pos >> dir->i_blkbits, &iter->epos,
&iter->eloc, &iter->elen, &iter->loffset) !=
(EXT_RECORDED_ALLOCATED >> 30)) {
if (pos == dir->i_size)
return 0;
udf_err(dir->i_sb,
"position %llu not allocated in directory (ino %lu)\n",
(unsigned long long)pos, dir->i_ino);
return -EFSCORRUPTED;
}
err = udf_fiiter_load_bhs(iter);
if (err < 0)
return err;
err = udf_copy_fi(iter);
if (err < 0) {
udf_fiiter_release(iter);
return err;
}
return 0;
}
int udf_fiiter_advance(struct udf_fileident_iter *iter)
{
unsigned int oldoff, len;
int blksize = 1 << iter->dir->i_blkbits;
int err;
oldoff = iter->pos & (blksize - 1);
len = udf_dir_entry_len(&iter->fi);
iter->pos += len;
if (UDF_I(iter->dir)->i_alloc_type != ICBTAG_FLAG_AD_IN_ICB) {
if (oldoff + len >= blksize) {
brelse(iter->bh[0]);
iter->bh[0] = NULL;
/* Next block already loaded? */
if (iter->bh[1]) {
iter->bh[0] = iter->bh[1];
iter->bh[1] = NULL;
} else {
udf_fiiter_advance_blk(iter);
}
}
err = udf_fiiter_load_bhs(iter);
if (err < 0)
return err;
}
return udf_copy_fi(iter);
}
void udf_fiiter_release(struct udf_fileident_iter *iter)
{
iter->dir = NULL;
brelse(iter->bh[0]);
brelse(iter->bh[1]);
iter->bh[0] = iter->bh[1] = NULL;
}
static void udf_copy_to_bufs(void *buf1, int len1, void *buf2, int len2,
int off, void *src, int len)
{
int copy;
if (off >= len1) {
off -= len1;
} else {
copy = min(off + len, len1) - off;
memcpy(buf1 + off, src, copy);
src += copy;
len -= copy;
off = 0;
}
if (len > 0) {
if (WARN_ON_ONCE(off + len > len2 || !buf2))
return;
memcpy(buf2 + off, src, len);
}
}
static uint16_t udf_crc_fi_bufs(void *buf1, int len1, void *buf2, int len2,
int off, int len)
{
int copy;
uint16_t crc = 0;
if (off >= len1) {
off -= len1;
} else {
copy = min(off + len, len1) - off;
crc = crc_itu_t(crc, buf1 + off, copy);
len -= copy;
off = 0;
}
if (len > 0) {
if (WARN_ON_ONCE(off + len > len2 || !buf2))
return 0;
crc = crc_itu_t(crc, buf2 + off, len);
}
return crc;
}
static void udf_copy_fi_to_bufs(char *buf1, int len1, char *buf2, int len2,
int off, struct fileIdentDesc *fi,
uint8_t *impuse, uint8_t *name)
{
uint16_t crc;
int fioff = off;
int crcoff = off + sizeof(struct tag);
unsigned int crclen = udf_dir_entry_len(fi) - sizeof(struct tag);
udf_copy_to_bufs(buf1, len1, buf2, len2, off, fi,
sizeof(struct fileIdentDesc));
off += sizeof(struct fileIdentDesc);
if (impuse)
udf_copy_to_bufs(buf1, len1, buf2, len2, off, impuse,
le16_to_cpu(fi->lengthOfImpUse));
off += le16_to_cpu(fi->lengthOfImpUse);
if (name)
udf_copy_to_bufs(buf1, len1, buf2, len2, off, name,
fi->lengthFileIdent);
crc = udf_crc_fi_bufs(buf1, len1, buf2, len2, crcoff, crclen);
fi->descTag.descCRC = cpu_to_le16(crc);
fi->descTag.descCRCLength = cpu_to_le16(crclen);
fi->descTag.tagChecksum = udf_tag_checksum(&fi->descTag);
udf_copy_to_bufs(buf1, len1, buf2, len2, fioff, fi, sizeof(struct tag));
}
void udf_fiiter_write_fi(struct udf_fileident_iter *iter, uint8_t *impuse)
{
struct udf_inode_info *iinfo = UDF_I(iter->dir);
void *buf1, *buf2 = NULL;
int len1, len2 = 0, off;
int blksize = 1 << iter->dir->i_blkbits;
off = iter->pos & (blksize - 1);
if (iinfo->i_alloc_type == ICBTAG_FLAG_AD_IN_ICB) {
buf1 = iinfo->i_data + iinfo->i_lenEAttr;
len1 = iter->dir->i_size;
} else {
buf1 = iter->bh[0]->b_data;
len1 = blksize;
if (iter->bh[1]) {
buf2 = iter->bh[1]->b_data;
len2 = blksize;
}
}
udf_copy_fi_to_bufs(buf1, len1, buf2, len2, off, &iter->fi, impuse,
iter->name == iter->namebuf ? iter->name : NULL);
if (iinfo->i_alloc_type == ICBTAG_FLAG_AD_IN_ICB) {
mark_inode_dirty(iter->dir);
} else {
mark_buffer_dirty_inode(iter->bh[0], iter->dir);
if (iter->bh[1])
mark_buffer_dirty_inode(iter->bh[1], iter->dir);
}
inode_inc_iversion(iter->dir);
}
struct fileIdentDesc *udf_fileident_read(struct inode *dir, loff_t *nf_pos,
struct udf_fileident_bh *fibh,
struct fileIdentDesc *cfi,
struct extent_position *epos,
struct kernel_lb_addr *eloc, uint32_t *elen,
sector_t *offset)
{
struct fileIdentDesc *fi;
int i, num;
udf_pblk_t block;
struct buffer_head *tmp, *bha[16];
struct udf_inode_info *iinfo = UDF_I(dir);
fibh->soffset = fibh->eoffset;
if (iinfo->i_alloc_type == ICBTAG_FLAG_AD_IN_ICB) {
fi = udf_get_fileident(iinfo->i_data -
(iinfo->i_efe ?
sizeof(struct extendedFileEntry) :
sizeof(struct fileEntry)),
dir->i_sb->s_blocksize,
&(fibh->eoffset));
if (!fi)
return NULL;
*nf_pos += fibh->eoffset - fibh->soffset;
memcpy((uint8_t *)cfi, (uint8_t *)fi,
sizeof(struct fileIdentDesc));
return fi;
}
if (fibh->eoffset == dir->i_sb->s_blocksize) {
uint32_t lextoffset = epos->offset;
unsigned char blocksize_bits = dir->i_sb->s_blocksize_bits;
if (udf_next_aext(dir, epos, eloc, elen, 1) !=
(EXT_RECORDED_ALLOCATED >> 30))
return NULL;
block = udf_get_lb_pblock(dir->i_sb, eloc, *offset);
(*offset)++;
if ((*offset << blocksize_bits) >= *elen)
*offset = 0;
else
epos->offset = lextoffset;
brelse(fibh->sbh);
fibh->sbh = fibh->ebh = udf_tread(dir->i_sb, block);
if (!fibh->sbh)
return NULL;
fibh->soffset = fibh->eoffset = 0;
if (!(*offset & ((16 >> (blocksize_bits - 9)) - 1))) {
i = 16 >> (blocksize_bits - 9);
if (i + *offset > (*elen >> blocksize_bits))
i = (*elen >> blocksize_bits)-*offset;
for (num = 0; i > 0; i--) {
block = udf_get_lb_pblock(dir->i_sb, eloc,
*offset + i);
tmp = udf_tgetblk(dir->i_sb, block);
if (tmp && !buffer_uptodate(tmp) &&
!buffer_locked(tmp))
bha[num++] = tmp;
else
brelse(tmp);
}
if (num) {
bh_readahead_batch(num, bha, REQ_RAHEAD);
for (i = 0; i < num; i++)
brelse(bha[i]);
}
}
} else if (fibh->sbh != fibh->ebh) {
brelse(fibh->sbh);
fibh->sbh = fibh->ebh;
}
fi = udf_get_fileident(fibh->sbh->b_data, dir->i_sb->s_blocksize,
&(fibh->eoffset));
if (!fi)
return NULL;
*nf_pos += fibh->eoffset - fibh->soffset;
if (fibh->eoffset <= dir->i_sb->s_blocksize) {
memcpy((uint8_t *)cfi, (uint8_t *)fi,
sizeof(struct fileIdentDesc));
} else if (fibh->eoffset > dir->i_sb->s_blocksize) {
uint32_t lextoffset = epos->offset;
if (udf_next_aext(dir, epos, eloc, elen, 1) !=
(EXT_RECORDED_ALLOCATED >> 30))
return NULL;
block = udf_get_lb_pblock(dir->i_sb, eloc, *offset);
(*offset)++;
if ((*offset << dir->i_sb->s_blocksize_bits) >= *elen)
*offset = 0;
else
epos->offset = lextoffset;
fibh->soffset -= dir->i_sb->s_blocksize;
fibh->eoffset -= dir->i_sb->s_blocksize;
fibh->ebh = udf_tread(dir->i_sb, block);
if (!fibh->ebh)
return NULL;
if (sizeof(struct fileIdentDesc) > -fibh->soffset) {
int fi_len;
memcpy((uint8_t *)cfi, (uint8_t *)fi, -fibh->soffset);
memcpy((uint8_t *)cfi - fibh->soffset,
fibh->ebh->b_data,
sizeof(struct fileIdentDesc) + fibh->soffset);
fi_len = udf_dir_entry_len(cfi);
*nf_pos += fi_len - (fibh->eoffset - fibh->soffset);
fibh->eoffset = fibh->soffset + fi_len;
} else {
memcpy((uint8_t *)cfi, (uint8_t *)fi,
sizeof(struct fileIdentDesc));
}
}
/* Got last entry outside of dir size - fs is corrupted! */
if (*nf_pos > dir->i_size)
return NULL;
return fi;
}
struct fileIdentDesc *udf_get_fileident(void *buffer, int bufsize, int *offset)
{
struct fileIdentDesc *fi;
int lengthThisIdent;
uint8_t *ptr;
int padlen;
if ((!buffer) || (!offset)) {
udf_debug("invalidparms, buffer=%p, offset=%p\n",
buffer, offset);
return NULL;
}
ptr = buffer;
if ((*offset > 0) && (*offset < bufsize))
ptr += *offset;
fi = (struct fileIdentDesc *)ptr;
if (fi->descTag.tagIdent != cpu_to_le16(TAG_IDENT_FID)) {
udf_debug("0x%x != TAG_IDENT_FID\n",
le16_to_cpu(fi->descTag.tagIdent));
udf_debug("offset: %d sizeof: %lu bufsize: %d\n",
*offset, (unsigned long)sizeof(struct fileIdentDesc),
bufsize);
return NULL;
}
if ((*offset + sizeof(struct fileIdentDesc)) > bufsize)
lengthThisIdent = sizeof(struct fileIdentDesc);
else
lengthThisIdent = sizeof(struct fileIdentDesc) +
fi->lengthFileIdent + le16_to_cpu(fi->lengthOfImpUse);
/* we need to figure padding, too! */
padlen = lengthThisIdent % UDF_NAME_PAD;
if (padlen)
lengthThisIdent += (UDF_NAME_PAD - padlen);
*offset = *offset + lengthThisIdent;
return fi;
}
struct short_ad *udf_get_fileshortad(uint8_t *ptr, int maxoffset, uint32_t *offset,
int inc)
{
struct short_ad *sa;
if ((!ptr) || (!offset)) {
pr_err("%s: invalidparms\n", __func__);
return NULL;
}
if ((*offset + sizeof(struct short_ad)) > maxoffset)
return NULL;
else {
sa = (struct short_ad *)ptr;
if (sa->extLength == 0)
return NULL;
}
if (inc)
*offset += sizeof(struct short_ad);
return sa;
}
struct long_ad *udf_get_filelongad(uint8_t *ptr, int maxoffset, uint32_t *offset, int inc)
{
struct long_ad *la;
if ((!ptr) || (!offset)) {
pr_err("%s: invalidparms\n", __func__);
return NULL;
}
if ((*offset + sizeof(struct long_ad)) > maxoffset)
return NULL;
else {
la = (struct long_ad *)ptr;
if (la->extLength == 0)
return NULL;
}
if (inc)
*offset += sizeof(struct long_ad);
return la;
}