Date: Sun, 22 Dec 2019 03:19:17 +0000 (UTC) From: Conrad Meyer <cem@FreeBSD.org> To: src-committers@freebsd.org, svn-src-all@freebsd.org, svn-src-head@freebsd.org Subject: svn commit: r355996 - in head/usr.sbin/fstyp: . tests Message-ID: <201912220319.xBM3JHPu058403@repo.freebsd.org>
next in thread | raw e-mail | index | archive | help
Author: cem Date: Sun Dec 22 03:19:17 2019 New Revision: 355996 URL: https://svnweb.freebsd.org/changeset/base/355996 Log: fstyp(8): Show exFAT volume labels with -l flag exfat is fundamentally the same design as fat32. The superblock differs marginally, and there are some additional optional features irrelevant to fstype(8); the structure of dirents has changed slightly to enable, among other things, larger files; the directory entries are no longer DOS 8.3 ASCII or local 8-bit encoding, but instead explicitly UCS-2-LE. (As a result, this change uses iconv to convert a found exfat volume label to the user's locale.) Locating the volume label is identical to FAT32: locate the root directory and walk through dirents until you find a volume label. Like FAT32, follow the FAT chain between root directory clusters as necessary. PR: 242225 Reported by: Victor Sudakov <vas AT sibptus.ru> Modified: head/usr.sbin/fstyp/exfat.c head/usr.sbin/fstyp/fstyp.c head/usr.sbin/fstyp/fstyp.h head/usr.sbin/fstyp/tests/fstyp_test.sh Modified: head/usr.sbin/fstyp/exfat.c ============================================================================== --- head/usr.sbin/fstyp/exfat.c Sun Dec 22 01:22:51 2019 (r355995) +++ head/usr.sbin/fstyp/exfat.c Sun Dec 22 03:19:17 2019 (r355996) @@ -27,6 +27,14 @@ #include <sys/cdefs.h> __FBSDID("$FreeBSD$"); +#include <sys/param.h> +#include <sys/endian.h> + +#include <assert.h> +#include <err.h> +#include <errno.h> +#include <iconv.h> +#include <stdbool.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> @@ -34,6 +42,10 @@ __FBSDID("$FreeBSD$"); #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]; @@ -55,19 +67,300 @@ struct exfat_vbr { 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 { + /* + * Count of "secondary" dirents following this one. + * + * A single logical entity may be composed of a + * sequence of several dirents, starting with a primary + * one; the rest are secondary dirents. + */ + 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 fstyp.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; + } + + if (show_label) + exfat_find_label(fp, ev, bytespersec, label, size); + +done: + free(cksect); free(ev); return (0); Modified: head/usr.sbin/fstyp/fstyp.c ============================================================================== --- head/usr.sbin/fstyp/fstyp.c Sun Dec 22 01:22:51 2019 (r355995) +++ head/usr.sbin/fstyp/fstyp.c Sun Dec 22 03:19:17 2019 (r355996) @@ -38,6 +38,8 @@ __FBSDID("$FreeBSD$"); #include <capsicum_helpers.h> #include <err.h> #include <errno.h> +#include <iconv.h> +#include <locale.h> #include <stdbool.h> #include <stddef.h> #include <stdio.h> @@ -50,24 +52,27 @@ __FBSDID("$FreeBSD$"); #define LABEL_LEN 256 +bool show_label = false; + 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 * @@ -159,7 +164,7 @@ int main(int argc, char **argv) { int ch, error, i, nbytes; - bool ignore_type = false, show_label = false, show_unmountable = false; + bool ignore_type = false, show_unmountable = false; char label[LABEL_LEN + 1], strvised[LABEL_LEN * 4 + 1]; char *path; FILE *fp; @@ -187,6 +192,26 @@ main(int argc, char **argv) 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) Modified: head/usr.sbin/fstyp/fstyp.h ============================================================================== --- head/usr.sbin/fstyp/fstyp.h Sun Dec 22 01:22:51 2019 (r355995) +++ head/usr.sbin/fstyp/fstyp.h Sun Dec 22 03:19:17 2019 (r355996) @@ -32,7 +32,14 @@ #ifndef FSTYP_H #define FSTYP_H +#include <stdbool.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" + +extern bool show_label; /* -l flag */ void *read_buf(FILE *fp, off_t off, size_t len); char *checked_strdup(const char *s); Modified: head/usr.sbin/fstyp/tests/fstyp_test.sh ============================================================================== --- head/usr.sbin/fstyp/tests/fstyp_test.sh Sun Dec 22 01:22:51 2019 (r355995) +++ head/usr.sbin/fstyp/tests/fstyp_test.sh Sun Dec 22 03:19:17 2019 (r355996) @@ -68,6 +68,15 @@ exfat_body() { 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" @@ -253,6 +262,7 @@ atf_init_test_cases() { 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
Want to link to this message? Use this URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?201912220319.xBM3JHPu058403>