1

How do I set the UUID of an ExFat partition?

The linux tools I've tried (mlabel, gparted) only allow resetting the UUID to a different random one, not to a UUID I choose.

JanKanis
  • 287

1 Answers1

1

The tools I could find couldn't do this, so I wrote my own. See https://github.com/JanKanis/exfat_setuuid.

Later on I found that on Linux, tune.exfat and exfatlabel (part of exfatprogs) can also do it.

It can also display some low level ExFat configuration which can come in handy if you want to e.g. recreate an ExFat partition with a smaller size but with the same cluster sizes and FAT table offset, as those are sometimes optimized for the specific device.

In case the link ever breaks, here's the full file:

#!/usr/bin/env python3

import sys, os, re, argparse, pathlib, subprocess, weakref from collections import namedtuple

try: from humanfriendly import parse_size, format_size except ImportError: def parse_size(num, binary=False): return int(num) format_size = None

DEFAULT_BLOCK_SIZE = 512

Volume Boot Record / Volume Boot Region

BACKUP_VBR_OFFSET = 12

CHECKSUM_OFFSET = 11 # no. of sectors relative to start of VBR CHECKSUMMED_DATA_LENGTH = CHECKSUM_OFFSET

FILE_SYSTEM_NAME = b'EXFAT ' # mind the spaces BOOT_SIGNATURE = 0xAA55

def get_uuid_bytes(uuid_str): if not re.fullmatch('[0-9a-fA-F]{4}-[0-9a-fA-F]{4}', uuid_str): raise ValueError("invalid UUID, expected format: 12AB-E9FF")

bytes_str = [uuid_str[7:9], uuid_str[5:7], uuid_str[2:4], uuid_str[0:2]]
return bytes(int(x, 16) for x in bytes_str)

def uuid_str(uuid_num): b1, b2, b3, b4 = (format(b, '02X') for b in reversed(uuid_num.to_bytes(4, byteorder='little'))) return f"{b1+b2}-{b3+b4}"

def try_get_block_size(device): try: p = subprocess.run(['blockdev', '--getbsz', device], capture_output=True, encoding='utf8') if p.returncode == 0: return int(p.stdout) else: print("Unable to determine device block size, defaulting to 512: "+p.stderr) return None except (FileNotFoundError, ValueError): return None

class ExFatFS: def init(self, file, sector_size=None): self.inconsistent = False self.file = file self.vbr = VBR(self.file, 0, self, sector_size=sector_size) if sector_size is None: sector_size = self.vbr.sector_size self.backup_vbr = VBR(self.file, BACKUP_VBR_OFFSET*sector_size, self, sector_size=sector_size, is_backup=True)

def set_uuid(self, uuid):
    self.vbr.set_uuid(uuid)
    self.file.flush()
    os.fsync(self.file.fileno())  # Update main and backup VBR one by one for recoverability
    self.backup_vbr.set_uuid(uuid)
    self.file.flush()
    os.fsync(self.file.fileno())

def check(self):
    self.vbr.check()
    self.backup_vbr.check()
    for field, desc in VBR.fields.items():
        if field in {'volume_flags', 'percent_in_use'}:
            continue
        if not getattr(self.vbr, field) == getattr(self.backup_vbr, field):
            self.inconsistentFS(f"Invalid EXFAT filesystem: {field} in VBR does not equal {field} in backup VBR. Found {VBR.format_value(getattr(self.vbr, field), desc[1])} and {VBR.format_value(getattr(self.backup_vbr, field), desc[1])}")


def inconsistentFS(self, message):
    self.inconsistent = True
    print(message, file=sys.stderr)



D = Desc = namedtuple('Desc', 'offset unit size', defaults=[4]) class VBR:

fields = dict(
    # name (as in spec)              offset   unit      size (bytes, default 4)
    partition_offset =                D(64,  'sectors', 8),
    volume_length =                   D(72,  'sectors', 8),
    fat_offset =                      D(80,  'sectors'),
    fat_length =                      D(84,  'sectors'),
    cluster_heap_offset =             D(88,  'sectors'),
    cluster_count =                   D(92,  'clusters'),
    first_cluster_of_root_directory = D(96,  'clusters'),
    volume_serial_number =            D(100, 'uuid'),
    file_system_revision =            D(104, 'version', 2),
    volume_flags =                    D(106, 'flags', 2),
    bytes_per_sector_shift =          D(108, 'log2', 1),
    sectors_per_cluster_shift =       D(109, 'log2', 1),
    number_of_fats =                  D(110, 'number', 1),
    drive_select =                    D(111, 'number', 1),
    percent_in_use =                  D(112, 'number', 1),
    boot_signature =                  D(510, 'hex', 2),
)

calculated_fields = dict(
    **fields,
    checksum = D(None, 'hex'),
    bytes_per_sector = D(None, 'bytes'),
    bytes_per_cluster = D(None, 'bytes'),
    **{k+'_bytes': D(None, 'bytes') for k, v in fields.items() if v[1] == 'sectors'},
    cluster_heap_length_bytes = D(None, 'bytes'),
)


def __init__(self, file, offset, exfatfs, sector_size=None, is_backup=False):
    self.file = file
    self.offset = offset
    self.exfatfs = weakref.ref(exfatfs)
    self.sector_size = sector_size
    self.is_backup = is_backup

    self.read_data()


def read_data(self):
    self.file.seek(self.offset)
    self.vbr = self.file.read(512)

    self.file_system_name = self.vbr[3:3+8]
    self._readfields()


def _read(self, offset, length=4):
    return int.from_bytes(self.vbr[offset:offset+length], byteorder='little')


def _readfields(self):
    sectorfields = []
    for name, desc in self.fields.items():
        offset, unit, size = desc

        value = self._read(offset, size)
        if unit == 'sectors':
            sectorfields.append(name)

        setattr(self, name, value)

    self.bytes_per_sector = 2**self.bytes_per_sector_shift
    self.bytes_per_cluster = self.bytes_per_sector * 2**self.sectors_per_cluster_shift

    if self.sector_size is None:
        self.sector_size = self.bytes_per_sector
        print(f"Using filesystem reported sector size of {self.sector_size} bytes", file=sys.stderr)

    self.checksum = self.get_checksum(report_bad=False)

    for fieldname in sectorfields:
        f = fieldname+'_bytes'
        setattr(self, f, getattr(self, fieldname) * self.bytes_per_sector)

    self.cluster_heap_length_bytes = self.cluster_count * self.bytes_per_cluster


def get_checksum(self, report_bad=True):
    self.file.seek(self.offset+self.sector_size*CHECKSUM_OFFSET)
    checksum_block = self.file.read(self.sector_size)
    checksum_bytes = checksum_block[0:4]
    if report_bad and not checksum_block == checksum_bytes * (self.sector_size//4):
        self.inconsistentFS(f"Invalid EXFAT filesystem: Checksum block of {'backup ' if self.is_backup else ''}volume boot record is corrupt. Expecting a repetition of the checksum value.")
    return int.from_bytes(checksum_bytes, byteorder='little')


def calc_checksum(self):
    self.file.seek(self.offset)
    data = self.file.read(CHECKSUMMED_DATA_LENGTH*self.sector_size)

    checksum = 0
    for i, byte in enumerate(data):
        if i in (106, 107, 112):
            continue
        checksum = (0x80000000 if (checksum & 1) else 0) + (checksum >> 1) + byte
        checksum &= 0xffffffff
    return checksum


def write_checksum(self):
    checksum_sector = self.calc_checksum().to_bytes(4, byteorder='little') * (self.sector_size//4)
    self.file.seek(self.offset + self.sector_size*CHECKSUM_OFFSET)
    self.file.write(checksum_sector)


def check(self):
    self.file.seek(self.offset+11)
    must_be_zero = self.file.read(53)

    if self.file_system_name != FILE_SYSTEM_NAME:
        self.inconsistentFS(f"Invalid EXFAT filesystem: name in {self._backup_str()}volume boot record. Found {self.file_system_name}, expected {FILE_SYSTEM_NAME}")

    if must_be_zero != b'\x00'*53:
        self.inconsistentFS(f"Invalid EXFAT filesystem: MustBeZero field in {self._backup_str()}volume boot record is not all zeros.")

    if self.boot_signature != BOOT_SIGNATURE:
        self.inconsistentFS(f"Invalid EXFAT filesystem: boot signature in {self._backup_str()}volume boot record is not 0x{BOOT_SIGNATURE:X}. Found 0x{self.boot_signature:X}.")

    if self.bytes_per_sector != self.sector_size:
        self.inconsistentFS(f"Invalid EXFAT filesystem: Using a blocksize of {self.sector_size}, but {self._backup_str()}volume boot record says blocksize is {self.bytes_per_sector}")

    checksum = self.get_checksum()
    expected_checksum = self.calc_checksum()
    if checksum != expected_checksum:
        self.inconsistentFS(f"Invalid EXFAT filesystem: Invalid checksum for {self._backup_str()}volume boot record: expected {expected_checksum:x}, found {checksum:x}")


def base_write_uuid(self, uuid):
    self.file.seek(self.offset+self.fields['volume_serial_number'].offset)
    self.file.write(uuid)


def set_uuid(self, uuid):
    self.base_write_uuid(uuid)
    self.write_checksum()
    self.read_data()


def inconsistentFS(self, message):
    self.exfatfs().inconsistentFS(message)

def _backup_str(self):
    return 'backup ' if self.is_backup else ''


def __str__(self):
    s = "FSInfo:\n"
    for name, desc in self.calculated_fields.items():
        val = getattr(self, name)
        s += f"  {name}: {self.format_value(val, desc.unit)}\n"
    return s


@staticmethod
def format_value(val, unit):
    if unit == 'uuid':
        return uuid_str(val)
    if unit == 'hex':
        return f"0x{val:X}"
    if unit == 'version':
        return f"{val//256}.{val%256}"
    if unit == 'flags':
        return ','.join(name for name, flag in (('ActiveFat', 1), ('VolumeDirty', 2), ('MediaFailure', 4), ('ClearToZero', 8), (f'Reserved=0x{val&0xfff0:X}', 0xfff0)) if flag&val) or '(none)'
    if unit == 'bytes' and format_size:
        return f"{val} ({format_size(val, binary=True)})"
    return str(val)     


class InconsistentExFatException(Exception): pass

def main(): argp = argparse.ArgumentParser(description="This program shows low level configuration of an ExFat filesystem and checks the volume boot record (superblock) for consistecy. It also allows setting the UUID/serial number. Without options, will check consistency and show configuration.") argp.add_argument('device', type=pathlib.Path, help="The device file to use.") argp.add_argument('--write-uuid', dest='uuid', type=get_uuid_bytes, help="Write this UUID (serial number) to the filesystem superblock. Before writing, the program will verify the consistency of the filesystem superblock. WARNING: There should NEVER be more than one active filesystem with the same UUID on your system. This should only be used if you are replacing an old ExFat filesystem!") argp.add_argument('--read-device-sector-size', action='store_true', help='Use device block size (does not work on image files; this requires the blockdev program)') argp.add_argument('--sector-size', '-b', default=None, type=lambda x: parse_size(x, binary=True), help='The sector size of the file system. K-suffix is supported. If omitted, will read sector size from filesystem superblock.') argp.add_argument('--ignore-invalid', action='store_true', help='Report configuration even if filesystem is corrupt') args = argp.parse_args()

mode = 'rb' if args.uuid is None else 'rb+'
with open(args.device, mode) as file:
    if args.sector_size is None and args.read_device_sector_size:
        args.sector_size = try_get_block_size(args.device)

    fs = ExFatFS(file, sector_size=args.sector_size)
    fs.check()
    if fs.inconsistent:
        print(f"Error: Not an ExFat filesystem or filesystem is corrupted.", file=sys.stderr)
        if not args.ignore_invalid:
            sys.exit(1)

    if not args.uuid:
        print(fs.vbr)

    if not fs.inconsistent and args.uuid:
        fs.set_uuid(args.uuid)
        fs.check()
        print(f"Updated UUID to {uuid_str(int.from_bytes(args.uuid, byteorder='little'))}")


if name == 'main': main()

JanKanis
  • 287