ESP32-S3 USB MSC Remounts: Verify That the Formatting Flag Is Actually Used

A USB mass-storage product can set format_if_mount_failed = false and still contain a reachable formatting path. The important question is not whether that field exists in a configuration structure. It is whether the storage wrapper actually reads it before calling the formatter.

This source review concerns an ESP32-S3 integration using ESP-IDF v5.2.5 and Espressif’s esp_tinyusb component version 1.7.4. It is a deliberately pinned example, not a claim about every TinyUSB release or every ESP32. The official component registry identifies the corresponding repository commit. The storage implementation and public header downloaded from that registry artifact byte-match the immutable source linked below. No target firmware was compiled or exercised for this article.

USB configuration gives storage to the host; USB unmount or a qualifying SCSI eject requests a local FAT mount. In esp_tinyusb 1.7.4, the local mount's no-filesystem or internal-error branch can call the formatter even though the public mount configuration contains a formatting-policy flag.

A familiar structure is not an implemented contract

ESP-IDF’s v5.2.5 FAT VFS header defines esp_vfs_fat_mount_config_t, including the formatting-policy Boolean. The esp_tinyusb 1.7.4 public storage header embeds that structure in both its SPI-flash and SDMMC configuration types. That makes the following application intent look reasonable:

/* Intended policy, NOT proof that the wrapper enforces it. */
.mount_config = {
    .format_if_mount_failed = false,
    .max_files = 4,
}

However, the pinned storage implementation copies mount_config.max_files into its private state in both initialization functions. It does not copy or consult format_if_mount_failed. The shared local-mount helper makes its own formatting decision.

This is a wrapper-boundary issue. A configuration type can be reused while only some members are honored. Neither successful initialization nor the appearance of the field in a designated initializer establishes the data-preservation behavior of a different implementation layer. The same review must therefore cover both supported storage backends rather than assuming the SD-card path is special.

Follow the local mount, not just startup

The public API distinguishes storage mounted locally in firmware from storage exposed through USB. In the source, tud_mount_cb(), called when USB becomes configured, requests local unmounting. Conversely, tud_umount_cb() requests a local mount. The SCSI Start Stop Unit callback also requests local mounting when load_eject is true and start is false.

Consequently, the formatting decision is not confined to a provisioning routine at first boot. It may be revisited during an ownership transition back to the device. These are specific callback paths, not proof that every operating system emits the same sequence or that every cable removal produces the same observed behavior. Capture the actual callbacks on the target before describing its disconnect policy.

Two details matter when interpreting logs. First, local mounting returns early if the wrapper already considers FAT mounted. Second, the SCSI callback logs a local-mount failure but still returns true. A successful-looking host eject cannot by itself certify that firmware has mounted the volume successfully or preserved its contents.

Identify the exact conditional formatting path

The internal _mount() helper first calls f_mount() with immediate mounting requested. If the result is neither FR_NO_FILESYSTEM nor FR_INT_ERR, a failed mount returns an error without taking the format branch. Those two selected results instead lead to allocation of a 4096-byte work buffer and, if allocation succeeds, a call to f_mkfs().

This does not establish that formatting will succeed, or that any particular medium will produce either result. It establishes a conditional call path that the apparent application policy does not gate. The formatter is passed an empty drive string, with a source comment identifying the default volume. A multi-volume product must additionally review which volume that resolves to; do not assume that every formatting argument is identical to the earlier mount target.

The premount notification is not a documented authorization barrier either. Its callback type returns void. Merely observing the event, logging a warning, or returning from that callback does not supply a “deny format” result to the subsequent mount helper.

Make preservation a release criterion

For a product holding customer data, write the acceptance requirement before choosing the integration: reconnecting storage to firmware must not format it without an explicit, separately authorized initialization operation. Do not treat a failed mount as evidence that media are blank.

A practical review and test plan is:

  1. Preserve the component archive, source revision, dependencies.lock, SDK version and build configuration. Version 1.7.4 allows a range of underlying TinyUSB versions; pinning the wrapper alone does not reproduce the full firmware.
  2. Trace each enabled backend from its initializer through the shared mount helper. Record which configuration fields survive and every path reaching f_mkfs().
  3. On disposable media, test valid FAT, known blank media, an unsupported filesystem and deliberately damaged FAT metadata. Keep read-only images before and after each case.
  4. Exercise application-requested mounting, USB configuration/unmount events and qualifying SCSI eject separately. Record callback order, FatFs results, formatter entry and local ownership state.
  5. For a modified integration, verify that denial prevents formatter entry on every path, including the two selected error results. Prefer a test double or instrumentation before destructive-media testing.

A maintained fix, a reviewed local change, or a different storage integration may satisfy that requirement. None is proven merely by changing the configuration initializer. Re-audit the selected implementation and retain regression evidence when updating the component.

Validation status: the official registry artifact and immutable source were compared, and the field handling and callback branches were inspected. Compilation, USB event capture, fault injection, default-volume resolution and formatting behavior on physical storage remain unperformed. The finding is a source-level data-preservation concern, not a demonstrated exploit or a report of observed data loss.

Similar Posts