Skip to content

fx_file_first_physical_cluster of an already-open read handle is not updated after the first write allocates a cluster to an empty file #97

Description

@leejugy

Title

fx_file_first_physical_cluster of an already-open read handle is not updated after the first write allocates a cluster to an empty file

Describe the bug

When the same zero-length file is opened with separate FX_FILE handles for reading and writing, the first write through the write handle allocates a new physical cluster.

At this point, FileX correctly updates the following fields in the already-open read handle:

  • fx_file_current_file_size
  • fx_file_total_clusters
  • fx_file_last_physical_cluster
  • fx_file_current_available_size
  • fx_file_dir_entry.fx_dir_entry_cluster

However, fx_file_first_physical_cluster in the read handle remains 0.

If fx_file_seek() is then called on the read handle, FileX recalculates the current physical cluster and logical sector based on fx_file_first_physical_cluster. Because the read handle still has a first cluster value of 0, FileX reads from an incorrect logical sector instead of the cluster that contains the actual file data.

The following test reproduces the issue:

void test_cluster_failure(FX_MEDIA *media) 
{
    FX_FILE wr_file = {
        0, 
    };

    FX_FILE rd_file = {
        0,
    };

    uint8_t send_data[512] = {
        0,
    };

    uint8_t read_data[512] = {
        0,
    };
    ULONG actual_size = 0;

    memset(send_data, 'a', sizeof(send_data));
    fx_file_delete(media, "/test.txt");
    fx_file_create(media, "/test.txt");
    fx_file_open(media, &wr_file, "/test.txt", FX_OPEN_FOR_WRITE);
    fx_file_open(media, &rd_file, "/test.txt", FX_OPEN_FOR_READ_FAST);

    fx_file_write(&wr_file, send_data, sizeof(send_data));
    fx_file_write(&wr_file, send_data, sizeof(send_data));
    fx_media_flush(media);
    fx_file_seek(&rd_file, 512);
    fx_file_read(&rd_file, read_data, sizeof(read_data), &actual_size);

    fx_file_close(&wr_file);
    fx_file_close(&rd_file);
}

Expected: read_data is entirely filled with 'a'.

Actual: all bytes in read_data are 0x00.

actual_size is correctly returned as 512.

Immediately after fx_file_read(), the debugger shows the following state:

rd_file:
    fx_file_total_clusters           = 1
    fx_file_first_physical_cluster   = 0
    fx_file_last_physical_cluster    = 101
    fx_file_current_physical_cluster = 0
    fx_file_current_logical_sector   = 32737
    fx_file_current_file_offset      = 512
    fx_file_current_file_size        = 1024
    fx_file_current_available_size   = 8192

wr_file:
    fx_file_total_clusters           = 1
    fx_file_first_physical_cluster   = 101
    fx_file_last_physical_cluster    = 101
    fx_file_current_physical_cluster = 101
    fx_file_current_logical_sector   = 34354
    fx_file_current_file_offset      = 1024
    fx_file_current_file_size        = 1024
    fx_file_current_available_size   = 8192

The cluster size is 8192 bytes and the sector size is 512 bytes, so each cluster contains 16 sectors.

This suggests that the read handle calculates the logical sector using cluster 0 instead of the actual first cluster, which is 101.

As a result, data read from the already-open read handle is incorrect.

After closing and reopening the file, fx_file_first_physical_cluster is initialized correctly from the directory entry, and the read operation works as expected.

Environment

  • Target device: STM32H563ZIT6
  • RTOS: Eclipse ThreadX 6.4.0
  • File system: Eclipse FileX 6.4.0
  • Storage: USB Mass Storage device through USBX Host MSC
  • Toolchain: arm-none-eabi-gcc [STM32CubeCLT_1.16.0]
  • IDE/build environment: Visual Studio Code (STM32CubeIDE extension for VS Code)

Diagnosis and attempted workarounds

I confirmed the following:

  1. After the write, the file size and current offset in the read handle are updated correctly.
  2. fx_media_flush() is called after write operations and other file modifications, but it does not resolve the issue.
  3. Closing and reopening the file makes the read operation work correctly.
  4. Manually copying fx_file_first_physical_cluster from the write handle to the read handle makes subsequent seek and read operations work correctly.
  5. The issue appears to be related to synchronization of the first-cluster metadata between already-open FX_FILE objects, rather than storage flushing or data caching.

To Reproduce

  1. Create a new file and open two FX_FILE handles for it.
  2. Write data to the new file.
  3. Move the read offset to a position greater than 0 using fx_file_seek().
  4. Observe the data returned by the subsequent read operation.
void test_cluster_failure(FX_MEDIA *media) 
{
    FX_FILE wr_file = {
        0, 
    };

    FX_FILE rd_file = {
        0,
    };

    uint8_t send_data[512] = {
        0,
    };

    uint8_t read_data[512] = {
        0,
    };
    ULONG actual_size = 0;

    memset(send_data, 'a', sizeof(send_data));
    fx_file_delete(media, "/test.txt");
    fx_file_create(media, "/test.txt");
    fx_file_open(media, &wr_file, "/test.txt", FX_OPEN_FOR_WRITE);
    fx_file_open(media, &rd_file, "/test.txt", FX_OPEN_FOR_READ_FAST);

    fx_file_write(&wr_file, send_data, sizeof(send_data));
    fx_file_write(&wr_file, send_data, sizeof(send_data));
    fx_media_flush(media);
    fx_file_seek(&rd_file, 512);
    fx_file_read(&rd_file, read_data, sizeof(read_data), &actual_size);

    fx_file_close(&wr_file);
    fx_file_close(&rd_file);
}

actual_size is correctly returned as 512, and all API calls return FX_SUCCESS.

Observed result:

rd_file:
    fx_file_total_clusters           = 1
    fx_file_first_physical_cluster   = 0
    fx_file_last_physical_cluster    = 101
    fx_file_current_physical_cluster = 0

wr_file:
    fx_file_total_clusters           = 1
    fx_file_first_physical_cluster   = 101
    fx_file_last_physical_cluster    = 101
    fx_file_current_physical_cluster = 101

All bytes in read_data are 0x00.

If both handles are closed and the file is reopened, reading from the same offset returns the correct data.

Expected behavior

When the first cluster is allocated to a zero-length file, the first-cluster information in other already-open read handles for the same file should also be updated.

The expected state is:

rd_file.fx_file_first_physical_cluster = 101
wr_file.fx_file_first_physical_cluster = 101

Subsequent calls to fx_file_seek() on the read handle should calculate the correct logical sector using the actual first cluster of the file.

When FileX updates other open handles after extending an empty file, the following field also appears to need synchronization:

search_ptr->fx_file_first_physical_cluster =
    file_ptr->fx_file_first_physical_cluster;

Impact

This issue blocks my work on porting a SQLite VFS on top of FileX.

The current implementation maintains separate read and write handles for the same database file. When a new database file is created and the first write occurs, the first-cluster information in the read handle is not updated. As a result, SQLite cannot correctly read back database pages that were just written.

This causes the following problems:

  • A newly created SQLite database cannot be read correctly immediately after creation.
  • After creating a table or inserting data, database pages are read from the wrong sector.
  • The file must be closed and reopened before it can be read correctly.
  • It is difficult to implement the file-handle lifetime and behavior required by the SQLite VFS.

This is currently a showstopper for the project.

Logs and console output

The relevant FX_FILE state is included above.

Additional context

This issue does not occur with an existing file that already has an allocated cluster.

When an existing file is opened, both the read and write handles obtain the same first cluster from the directory entry.

The issue occurs under the following conditions:

  1. A zero-length file is created.
  2. Both a read handle and a write handle are opened.
  3. The first write is performed through the write handle.
  4. The first physical cluster is allocated to the file.
  5. fx_file_first_physical_cluster is updated in the write handle, but remains 0 in the read handle.
  6. A subsequent seek on the read handle uses cluster 0 as the base cluster.

fx_media_flush() correctly flushes the FAT, directory entry, and storage cache, but it does not reinitialize the FX_FILE structure of an already-open read handle, so it does not resolve this issue.

The current workaround is to manually synchronize the first cluster after the first write:

rd_file.fx_file_first_physical_cluster =
    wr_file.fx_file_first_physical_cluster;

With this synchronization, subsequent seek and read operations work correctly without closing and reopening the file.

The following code in fx_file_write.c updates other already-open handles for the same file:

open_count =  media_ptr -> fx_media_opened_file_count;
search_ptr =  media_ptr -> fx_media_opened_file_list;
while (open_count)
{

    /* Is this file the same file opened for reading?  */
    if ((search_ptr != file_ptr) &&
        (search_ptr -> fx_file_dir_entry.fx_dir_entry_log_sector ==
         file_ptr -> fx_file_dir_entry.fx_dir_entry_log_sector) &&
        (search_ptr -> fx_file_dir_entry.fx_dir_entry_byte_offset ==
         file_ptr -> fx_file_dir_entry.fx_dir_entry_byte_offset))
    {

        /* Yes, the same file is opened for reading.  */

        /* Setup the new size.  */
        search_ptr -> fx_file_current_file_size =  file_ptr -> fx_file_current_file_offset;

        /* Setup the new directory entry.  */
        search_ptr -> fx_file_dir_entry.fx_dir_entry_cluster =      file_ptr -> fx_file_dir_entry.fx_dir_entry_cluster;
        search_ptr -> fx_file_dir_entry.fx_dir_entry_file_size =    file_ptr -> fx_file_dir_entry.fx_dir_entry_file_size;
        search_ptr -> fx_file_dir_entry.fx_dir_entry_log_sector =   file_ptr -> fx_file_dir_entry.fx_dir_entry_log_sector;
        search_ptr -> fx_file_dir_entry.fx_dir_entry_byte_offset =  file_ptr -> fx_file_dir_entry.fx_dir_entry_byte_offset;

        /* Setup the last cluster. This really isn't used during reading, but it is nice to keep things
           consistent.  */
        search_ptr -> fx_file_last_physical_cluster =  file_ptr -> fx_file_last_physical_cluster;

        /* Update the available clusters as well.  */
        search_ptr -> fx_file_current_available_size =  file_ptr -> fx_file_current_available_size;

        /* Determine if an empty file was previously opened.  */
        if (search_ptr -> fx_file_total_clusters == 0)
        {

            /* Setup initial parameters.  */
            search_ptr -> fx_file_total_clusters =            file_ptr -> fx_file_total_clusters;
            search_ptr -> fx_file_current_physical_cluster =  file_ptr -> fx_file_first_physical_cluster;
            search_ptr -> fx_file_current_relative_cluster =  0;
            search_ptr -> fx_file_current_logical_sector =    ((ULONG)media_ptr -> fx_media_data_sector_start) +
                (((ULONG64)(file_ptr -> fx_file_first_physical_cluster - FX_FAT_ENTRY_START)) *
                 ((ULONG)media_ptr -> fx_media_sectors_per_cluster));
            search_ptr -> fx_file_current_relative_sector =   0;
            search_ptr -> fx_file_current_logical_offset =    0;
            search_ptr -> fx_file_current_file_offset =       0;
        }
    }

    /* Adjust the pointer and decrement the search count.  */
    search_ptr =  search_ptr -> fx_file_opened_next;
    open_count--;
}
#endif

This code does not update search_ptr->fx_file_first_physical_cluster.

The issue becomes visible when fx_file_seek() is called with an offset of 512:

else
{
    /* we should directly access the desired cluster */
    file_ptr -> fx_file_current_relative_cluster =
        (ULONG)(byte_offset / bytes_per_cluster);

    /* This line may cause the failure because
       fx_file_first_physical_cluster is still 0 in the read handle. */
    file_ptr -> fx_file_current_physical_cluster =
        file_ptr -> fx_file_first_physical_cluster +
        file_ptr -> fx_file_current_relative_cluster; 

    bytes_remaining = byte_offset % bytes_per_cluster;
}

Therefore, it appears that _fx_file_write() should also update fx_file_first_physical_cluster in other already-open handles when the first cluster is allocated to an empty file.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions