From 1d9cf71b6a4ebbe60e8997302d63f3eccb7877d8 Mon Sep 17 00:00:00 2001 From: Conrad Meyer Date: Wed, 27 Nov 2019 12:40:18 -0800 Subject: [PATCH] fstyp(8): Read exFAT volume labels The superblock is slightly different from FAT32 and the dirent structure is a little different, but basically it's the same process: find the root directory data cluster, walk it (including chasing the FAT chain for the next data cluster if needed) until we find the special dirent indicating a volume label, and then return it to the generic fstyp code. Unlike msdosfs, exFAT labels are UCS-2LE, so this change uses iconv to convert it to the user's locale. It isn't clear to me if any of it is covered by the OIN (previously: Microsoft) patents on exFAT, as I have no familiarity with them. If one squints a little and considers 'fstyp' a Linux Component (OIN term), the patent coverage might not be an issue. (fstyp could easily run on a Linux System with a Linux Kernel.) --- usr.sbin/fstyp/exfat.c | 290 ++++++++++++++++++++++++++++- usr.sbin/fstyp/fstyp.c | 41 +++- usr.sbin/fstyp/fstyp.h | 2 + usr.sbin/fstyp/tests/fstyp_test.sh | 10 + 4 files changed, 332 insertions(+), 11 deletions(-) diff --git a/usr.sbin/fstyp/exfat.c b/usr.sbin/fstyp/exfat.c index 9c379df0c3f6..d2bbc53e4654 100644 --- a/usr.sbin/fstyp/exfat.c +++ b/usr.sbin/fstyp/exfat.c @@ -25,17 +25,29 @@ */ #include __FBSDID("$FreeBSD$"); +#include +#include + +#include +#include +#include +#include +#include #include #include #include #include #include "fstyp.h" +/* + * https://docs.microsoft.com/en-us/windows/win32/fileio/exfat-specification + */ + struct exfat_vbr { char ev_jmp[3]; char ev_fsname[8]; char ev_zeros[53]; uint64_t ev_part_offset; @@ -53,23 +65,297 @@ struct exfat_vbr { uint8_t ev_num_fats; uint8_t ev_drive_sel; uint8_t ev_percent_used; } __packed; +struct exfat_dirent { + uint8_t xde_type; +#define XDE_TYPE_INUSE_MASK 0x80 /* 1=in use */ +#define XDE_TYPE_INUSE_SHIFT 7 +#define XDE_TYPE_CATEGORY_MASK 0x40 /* 0=primary */ +#define XDE_TYPE_CATEGORY_SHIFT 6 +#define XDE_TYPE_IMPORTNC_MASK 0x20 /* 0=critical */ +#define XDE_TYPE_IMPORTNC_SHIFT 5 +#define XDE_TYPE_CODE_MASK 0x1f +/* InUse=0, ..., TypeCode=0: EOD. */ +#define XDE_TYPE_EOD 0x00 +#define XDE_TYPE_ALLOC_BITMAP (XDE_TYPE_INUSE_MASK | 0x01) +#define XDE_TYPE_UPCASE_TABLE (XDE_TYPE_INUSE_MASK | 0x02) +#define XDE_TYPE_VOL_LABEL (XDE_TYPE_INUSE_MASK | 0x03) +#define XDE_TYPE_FILE (XDE_TYPE_INUSE_MASK | 0x05) +#define XDE_TYPE_VOL_GUID (XDE_TYPE_INUSE_MASK | XDE_TYPE_IMPORTNC_MASK) +#define XDE_TYPE_STREAM_EXT (XDE_TYPE_INUSE_MASK | XDE_TYPE_CATEGORY_MASK) +#define XDE_TYPE_FILE_NAME (XDE_TYPE_INUSE_MASK | XDE_TYPE_CATEGORY_MASK | 0x01) +#define XDE_TYPE_VENDOR (XDE_TYPE_INUSE_MASK | XDE_TYPE_CATEGORY_MASK | XDE_TYPE_IMPORTNC_MASK) +#define XDE_TYPE_VENDOR_ALLOC (XDE_TYPE_INUSE_MASK | XDE_TYPE_CATEGORY_MASK | XDE_TYPE_IMPORTNC_MASK | 0x01) + union { + uint8_t xde_generic_[19]; + struct exde_primary { + // XXX: count of following dirents, "secondary" + uint8_t xde_secondary_count_; + uint16_t xde_set_chksum_; + uint16_t xde_prim_flags_; + uint8_t xde_prim_generic_[14]; + } __packed xde_primary_; + struct exde_secondary { + uint8_t xde_sec_flags_; + uint8_t xde_sec_generic_[18]; + } __packed xde_secondary_; + } u; + uint32_t xde_first_cluster; + uint64_t xde_data_len; +} __packed; +#define xde_generic u.xde_generic_ +#define xde_secondary_count u.xde_primary_.xde_secondary_count +#define xde_set_chksum u.xde_primary_.xde_set_chksum_ +#define xde_prim_flags u.xde_primary_.xde_prim_flags_ +#define xde_sec_flags u.xde_secondary_.xde_sec_flags_ +_Static_assert(sizeof(struct exfat_dirent) == 32, "spec"); + +struct exfat_de_label { + uint8_t xdel_type; /* XDE_TYPE_VOL_LABEL */ + uint8_t xdel_char_cnt; /* Length of UCS-2 label */ + uint16_t xdel_vol_lbl[11]; + uint8_t xdel_reserved[8]; +} __packed; +_Static_assert(sizeof(struct exfat_de_label) == 32, "spec"); + +#define MAIN_BOOT_REGION_SECT 0 +#define BACKUP_BOOT_REGION_SECT 12 + +#define SUBREGION_CHKSUM_SECT 11 + +#define FIRST_CLUSTER 2 +#define BAD_BLOCK_SENTINEL 0xfffffff7u +#define END_CLUSTER_SENTINEL 0xffffffffu + +static inline void * +read_sectn(FILE *fp, off_t sect, unsigned count, unsigned bytespersec) +{ + return (read_buf(fp, sect * bytespersec, bytespersec * count)); +} + +static inline void * +read_sect(FILE *fp, off_t sect, unsigned bytespersec) +{ + return (read_sectn(fp, sect, 1, bytespersec)); +} + +/* + * Compute the byte-by-byte multi-sector checksum of the given boot region + * (MAIN or BACKUP), for a given bytespersec (typically 512 or 4096). + * + * Endian-safe; result is host endian. + */ +static int +exfat_compute_boot_chksum(FILE *fp, unsigned region, unsigned bytespersec, + uint32_t *result) +{ + unsigned char *sector; + unsigned n, sect; + uint32_t checksum; + + checksum = 0; + for (sect = 0; sect < 11; sect++) { + sector = read_sect(fp, region + sect, bytespersec); + if (sector == NULL) + return (ENXIO); + for (n = 0; n < bytespersec; n++) { + if (sect == 0) { + switch (n) { + case 106: + case 107: + case 112: + continue; + } + } + checksum = ((checksum & 1) ? 0x80000000u : 0u) + + (checksum >> 1) + (uint32_t)sector[n]; + } + free(sector); + } + + *result = checksum; + return (0); +} + +static void +convert_label(const uint16_t *ucs2label /* LE */, unsigned ucs2len, char + *label_out, size_t label_sz) +{ + const char *label; + char *label_out_orig; + iconv_t cd; + size_t srcleft, rc; + + /* Currently hardcoded in fstype.c as 256 or so. */ + assert(label_sz > 1); + + if (ucs2len == 0) { + /* + * Kind of seems bogus, but the spec allows an empty label + * entry with the same meaning as no label. + */ + return; + } + + if (ucs2len > 11) { + warnx("exfat: Bogus volume label length: %u", ucs2len); + return; + } + + /* dstname="" means convert to the current locale. */ + cd = iconv_open("", EXFAT_ENC); + if (cd == (iconv_t)-1) { + warn("exfat: Could not open iconv"); + return; + } + + label_out_orig = label_out; + + /* Dummy up the byte pointer and byte length iconv's API wants. */ + label = (const void *)ucs2label; + srcleft = ucs2len * sizeof(*ucs2label); + + rc = iconv(cd, __DECONST(char **, &label), &srcleft, &label_out, + &label_sz); + if (rc == (size_t)-1) { + warn("exfat: iconv()"); + *label_out_orig = '\0'; + } else { + /* NUL-terminate result (iconv advances label_out). */ + if (label_sz == 0) + label_out--; + *label_out = '\0'; + } + + iconv_close(cd); +} + +/* + * Using the FAT table, look up the next cluster in this chain. + */ +static uint32_t +exfat_fat_next(FILE *fp, const struct exfat_vbr *ev, unsigned BPS, + uint32_t cluster) +{ + uint32_t fat_offset_sect, clsect, clsectoff; + uint32_t *fatsect, nextclust; + + fat_offset_sect = le32toh(ev->ev_fat_offset); + clsect = fat_offset_sect + (cluster / (BPS / sizeof(cluster))); + clsectoff = (cluster % (BPS / sizeof(cluster))); + + /* XXX This is pretty wasteful without a block cache for the FAT. */ + fatsect = read_sect(fp, clsect, BPS); + nextclust = le32toh(fatsect[clsectoff]); + free(fatsect); + + return (nextclust); +} + +static void +exfat_find_label(FILE *fp, const struct exfat_vbr *ev, unsigned BPS, + char *label_out, size_t label_sz) +{ + uint32_t rootdir_cluster, sects_per_clust, cluster_offset_sect; + off_t rootdir_sect; + struct exfat_dirent *declust, *it; + + cluster_offset_sect = le32toh(ev->ev_cluster_offset); + rootdir_cluster = le32toh(ev->ev_rootdir_cluster); + sects_per_clust = (1u << ev->ev_log_sect_per_clust); + + if (rootdir_cluster < FIRST_CLUSTER) { + warnx("%s: invalid rootdir cluster %u < %d", __func__, + rootdir_cluster, FIRST_CLUSTER); + return; + } + + + for (; rootdir_cluster != END_CLUSTER_SENTINEL; + rootdir_cluster = exfat_fat_next(fp, ev, BPS, rootdir_cluster)) { + if (rootdir_cluster == BAD_BLOCK_SENTINEL) { + warnx("%s: Bogus bad block in root directory chain", + __func__); + return; + } + + rootdir_sect = (rootdir_cluster - FIRST_CLUSTER) * + sects_per_clust + cluster_offset_sect; + declust = read_sectn(fp, rootdir_sect, sects_per_clust, BPS); + for (it = declust; + it < declust + (sects_per_clust * BPS / sizeof(*it)); it++) { + bool eod = false; + + /* + * Simplistic directory traversal doesn't do any + * validation of MUST requirements in spec. + */ + switch (it->xde_type) { + case XDE_TYPE_EOD: + eod = true; + break; + case XDE_TYPE_VOL_LABEL: { + struct exfat_de_label *lde = (void*)it; + convert_label(lde->xdel_vol_lbl, + lde->xdel_char_cnt, label_out, label_sz); + free(declust); + return; + } + } + + if (eod) + break; + } + free(declust); + } +} + int fstyp_exfat(FILE *fp, char *label, size_t size) { struct exfat_vbr *ev; + uint32_t *cksect; + unsigned bytespersec; + uint32_t chksum; + int error; + cksect = NULL; ev = (struct exfat_vbr *)read_buf(fp, 0, 512); if (ev == NULL || strncmp(ev->ev_fsname, "EXFAT ", 8) != 0) goto fail; + if (ev->ev_log_bytes_per_sect < 9 || ev->ev_log_bytes_per_sect > 12) { + warnx("exfat: Invalid BytesPerSectorShift"); + goto done; + } + + bytespersec = (1u << ev->ev_log_bytes_per_sect); + + error = exfat_compute_boot_chksum(fp, MAIN_BOOT_REGION_SECT, + bytespersec, &chksum); + if (error != 0) + goto done; + + cksect = read_sect(fp, MAIN_BOOT_REGION_SECT + SUBREGION_CHKSUM_SECT, + bytespersec); + /* - * Reading the volume label requires walking the root directory to look - * for a special label file. Left as an exercise for the reader. + * Technically the entire sector should be full of repeating 4-byte + * checksum pattern, but we only verify the first. */ + if (chksum != le32toh(cksect[0])) { + warnx("exfat: Found checksum 0x%08x != computed 0x%08x", + le32toh(cksect[0]), chksum); + goto done; + } + + exfat_find_label(fp, ev, bytespersec, label, size); + +done: + free(cksect); free(ev); return (0); fail: free(ev); diff --git a/usr.sbin/fstyp/fstyp.c b/usr.sbin/fstyp/fstyp.c index 31f7b34bde40..dabaf1c88bb2 100644 --- a/usr.sbin/fstyp/fstyp.c +++ b/usr.sbin/fstyp/fstyp.c @@ -36,10 +36,12 @@ __FBSDID("$FreeBSD$"); #include #include #include #include #include +#include +#include #include #include #include #include #include @@ -54,22 +56,23 @@ typedef int (*fstyp_function)(FILE *, char *, size_t); static struct { const char *name; fstyp_function function; bool unmountable; + char *precache_encoding; } fstypes[] = { - { "cd9660", &fstyp_cd9660, false }, - { "exfat", &fstyp_exfat, false }, - { "ext2fs", &fstyp_ext2fs, false }, - { "geli", &fstyp_geli, true }, - { "msdosfs", &fstyp_msdosfs, false }, - { "ntfs", &fstyp_ntfs, false }, - { "ufs", &fstyp_ufs, false }, + { "cd9660", &fstyp_cd9660, false, NULL }, + { "exfat", &fstyp_exfat, false, EXFAT_ENC }, + { "ext2fs", &fstyp_ext2fs, false, NULL }, + { "geli", &fstyp_geli, true, NULL }, + { "msdosfs", &fstyp_msdosfs, false, NULL }, + { "ntfs", &fstyp_ntfs, false, NULL }, + { "ufs", &fstyp_ufs, false, NULL }, #ifdef HAVE_ZFS - { "zfs", &fstyp_zfs, true }, + { "zfs", &fstyp_zfs, true, NULL }, #endif - { NULL, NULL, NULL } + { NULL, NULL, NULL, NULL } }; void * read_buf(FILE *fp, off_t off, size_t len) { @@ -186,10 +189,30 @@ main(int argc, char **argv) if (argc != 1) usage(); path = argv[0]; + if (setlocale(LC_CTYPE, "") == NULL) + err(1, "setlocale"); + caph_cache_catpages(); + + /* Cache iconv conversion data before entering capability mode. */ + if (show_label) { + for (i = 0; i < nitems(fstypes); i++) { + iconv_t cd; + + if (fstypes[i].precache_encoding == NULL) + continue; + cd = iconv_open("", fstypes[i].precache_encoding); + if (cd == (iconv_t)-1) + err(1, "%s: iconv_open %s", fstypes[i].name, + fstypes[i].precache_encoding); + /* Iconv keeps a small cache of unused encodings. */ + iconv_close(cd); + } + } + fp = fopen(path, "r"); if (fp == NULL) err(1, "%s", path); if (caph_enter() < 0) diff --git a/usr.sbin/fstyp/fstyp.h b/usr.sbin/fstyp/fstyp.h index f858b0c53827..b3be13235d22 100644 --- a/usr.sbin/fstyp/fstyp.h +++ b/usr.sbin/fstyp/fstyp.h @@ -31,10 +31,12 @@ #ifndef FSTYP_H #define FSTYP_H #define MIN(a,b) (((a)<(b))?(a):(b)) +/* The spec doesn't seem to permit UTF-16 surrogates. Definitely LE. */ +#define EXFAT_ENC "UCS-2LE" void *read_buf(FILE *fp, off_t off, size_t len); char *checked_strdup(const char *s); void rtrim(char *label, size_t size); diff --git a/usr.sbin/fstyp/tests/fstyp_test.sh b/usr.sbin/fstyp/tests/fstyp_test.sh index 5103ba042746..8f76424f5f75 100755 --- a/usr.sbin/fstyp/tests/fstyp_test.sh +++ b/usr.sbin/fstyp/tests/fstyp_test.sh @@ -66,10 +66,19 @@ exfat_head() { exfat_body() { bzcat $(atf_get_srcdir)/dfr-01-xfat.img.bz2 > exfat.img atf_check -s exit:0 -o inline:"exfat\n" fstyp -u exfat.img } +atf_test_case exfat_label +exfat_label_head() { + atf_set "descr" "fstyp(8) can read exFAT labels" +} +exfat_label_body() { + bzcat $(atf_get_srcdir)/dfr-01-xfat.img.bz2 > exfat.img + atf_check -s exit:0 -o inline:"exfat exFat\n" fstyp -u -l exfat.img +} + atf_test_case empty empty_head() { atf_set "descr" "fstyp(8) should fail on an empty file" } empty_body() { @@ -251,10 +260,11 @@ atf_init_test_cases() { atf_add_test_case cd9660 atf_add_test_case cd9660_label atf_add_test_case dir atf_add_test_case empty atf_add_test_case exfat + atf_add_test_case exfat_label atf_add_test_case ext2 atf_add_test_case ext3 atf_add_test_case ext4 atf_add_test_case ext4_label atf_add_test_case fat12 -- 2.23.0