Stale reads when a file is modified while a read-only handle is open
- Dominant language
- C
- Stars
- 988
- Forks
- 355
- Avg merge
- 4d 2h
- Merged PRs (30d)
- 12
Description
Description
-
When a file with an existing open read-only handle is modified on disk, opening a subsequent read-only handle to the same file causes HDF5 to re-use the original low-level file descriptor along with the associated shared internal state (`H5F_shared_t`), including its metadata cache, page buffer cache, and global heap. Since these caches are now stale, reads through the new handle return incorrect data or fail with errors such as:
- `len not positive after adjustment for EOA`
- `bad heap index`
- `component not found`
#### Steps to reproduce a stale read:
1. Create an HDF5 file with a dataset having only fill values
2. Open the file for reading (keep the handle open)
3. Modify the file (e.g. overwrite the dataset with new values)
4. Again open the file for reading - reads return stale (pre-modification) data
5. Only after closing all handles and re-opening does the correct data appear.
A standalone c reproducer
```c
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define FILENAME "mask_test.h5"
#define DATASET "mask"
#define NROWS 3
#define NCOLS 2
#define CHUNK_ROWS 3
#define FILL_VALUE 0
static void print_data(const char *label, int data[NROWS][NCOLS])
{
printf("%s:\n", label);
for (int i = 0; i < NROWS; i++) {
printf(" [%2d] %d %d\n", i, data[i][0], data[i][1]);
}
}
int main(void)
{
herr_t status;
hid_t file_id, ds_id, space_id, dcpl_id;
hsize_t dims[2] = {NROWS, NCOLS};
hsize_t chunks[2] = {CHUNK_ROWS, NCOLS};
int fill = FILL_VALUE;
int data[NROWS][NCOLS];
/* Shared fapl for all read-only opens: disable file locking */
hid_t fapl_rdonly = H5Pcreate(H5P_FILE_ACCESS);
H5Pset_file_locking(fapl_rdonly, false, true); /* use_file_locking=false, ignore_when_disabled=true */
/* ------------------------------------------------------------------ */
/* 1. Create .h5 with a chunked dataset, fill=0, no data written */
/* ------------------------------------------------------------------ */
printf("=== Step 1: Create %s with chunked dataset '%s' ===\n", FILENAME, DATASET);
file_id = H5Fcreate(FILENAME, H5F_ACC_TRUNC, H5P_DEFAULT, H5P_DEFAULT);
space_id = H5Screate_simple(2, dims, NULL);
dcpl_id = H5Pcreate(H5P_DATASET_CREATE);
status = H5Pset_chunk(dcpl_id, 2, chunks);
if (status < 0) { fprintf(stderr, "H5Pset_chunk failed\n"); return 1; }
status = H5Pset_fill_value(dcpl_id, H5T_NATIVE_INT, &fill);
if (status < 0) { fprintf(stderr, "H5Pset_fill_value failed\n"); return 1; }
ds_id = H5Dcreate2(file_id, DATASET, H5T_NATIVE_INT, space_id,
H5P_DEFAULT, dcpl_id, H5P_DEFAULT);
if (ds_id < 0) { fprintf(stderr, "H5Dcreate2 failed\n"); return 1; }
H5Pclose(dcpl_id);
H5Sclose(space_id);
H5Dclose(ds_id);
H5Fclose(file_id);
printf("Created %s\n\n", FILENAME);
/* ------------------------------------------------------------------ */
/* 2. Verify reading returns fill value */
/* ------------------------------------------------------------------ */
printf("=== Step 2: Read from '%s', expect all zeros ===\n", DATASET);
hid_t file2_id = H5Fopen(FILENAME, H5F_ACC_RDONLY, fapl_rdonly);
hid_t ds2_id = H5Dopen2(file2_id, DATASET, H5P_DEFAULT);
memset(data, 0xff, sizeof(data)); /* poison buffer */
status = H5Dread(ds2_id, H5T_NATIVE_INT, H5S_ALL, H5S_ALL, H5P_DEFAULT, data);
if (status < 0) { fprintf(stderr, "H5Dread failed\n"); return 1; }
int all_fill = 1;
for (int i = 0; i < NROWS && all_fill; i++)
for (int j = 0; j < NCOLS && all_fill; j++)
if (data[i][j] != FILL_VALUE) all_fill = 0;
printf("Fill value check: %s\n\n", all_fill ? "PASS (all zeros)" : "FAIL");
H5Dclose(ds2_id);
H5Fclose(file2_id);
/* ------------------------------------------------------------------ */
/* 3. Open original file for reading (keep handle open) */
/* ------------------------------------------------------------------ */
printf("=== Step 3: Open %s read-only handle ===\n", FILENAME);
hid_t rdonly_file_id = H5Fopen(FILENAME, H5F_ACC_RDONLY, fapl_rdonly);
hid_t rdonly_ds_id = H5Dopen2(rdonly_file_id, DATASET, H5P_DEFAULT);
memset(data, 0xff, sizeof(data));
status = H5Dread(rdonly_ds_id, H5T_NATIVE_INT, H5S_ALL, H5S_ALL, H5P_DEFAULT, data);
if (status < 0) { fprintf(stderr, "H5Dread on rdonly handle failed\n"); return 1; }
print_data("read-only handle read (expect zeros)", data);
printf("\n");
/* ------------------------------------------------------------------ */
/* 4. Open same file r+ with core driver, write all 2's, close */
/* ------------------------------------------------------------------ */
printf("=== Step 4: Write all 2's to '%s' in-place (core driver) ===\n", DATASET);
for (int i = 0; i < NROWS; i++)
for (int j = 0; j < NCOLS; j++)
data[i][j] = 2;
/* core driver: load file into memory, write back to same file on close */
hid_t fapl_core = H5Pcreate(H5P_FILE_ACCESS);
H5Pset_fapl_core(fapl_core, 64 * 1024 * 1024, 1);
hid_t rw_file_id = H5Fopen(FILENAME, H5F_ACC_RDWR, fapl_core);
H5Pclose(fapl_core);
ds_id = H5Dopen2(rw_file_id, DATASET, H5P_DEFAULT);
status = H5Dwrite(ds_id, H5T_NATIVE_INT, H5S_ALL, H5S_ALL, H5P_DEFAULT, data);
if (status < 0) { fprintf(stderr, "H5Dwrite failed\n"); return 1; }
H5Dclose(ds_id);
H5Fclose(rw_file_id);
printf("In-place write complete\n\n");
/* ------------------------------------------------------------------ */
/* 5. Read via still-open original handle, then close both handles, */
/* re-open original fresh and read/print */
/* ------------------------------------------------------------------ */
printf("=== Step 5a: Open new handle to %s and read '%s' ===\n", FILENAME, DATASET);
hid_t orig_rdonly_file_id = H5Fopen(FILENAME, H5F_ACC_RDONLY, fapl_rdonly);
hid_t orig_rdonly_ds_id = H5Dopen2(orig_rdonly_file_id, DATASET, H5P_DEFAULT);
memset(data, 0xff, sizeof(data));
status = H5Dread(orig_rdonly_ds_id, H5T_NATIVE_INT, H5S_ALL, H5S_ALL, H5P_DEFAULT, data);
if (status < 0) { fprintf(stderr, "H5Dread via new original handle failed\n"); return 1; }
print_data("Read via new original handle (before closing)", data);
printf("\n");
printf("=== Step 5b: Close both read-only handles ===\n");
H5Dclose(orig_rdonly_ds_id);
H5Fclose(orig_rdonly_file_id);
H5Dclose(rdonly_ds_id);
H5Fclose(rdonly_file_id);
printf("Both read-only handles closed\n\n");
printf("=== Step 5c: Re-open %s and read '%s' ===\n", FILENAME, DATASET);
hid_t final_file_id = H5Fopen(FILENAME, H5F_ACC_RDONLY, fapl_rdonly);
hid_t final_ds_id = H5Dopen2(final_file_id, DATASET, H5P_DEFAULT);
memset(data, 0xff, sizeof(data));
status = H5Dread(final_ds_id, H5T_NATIVE_INT, H5S_ALL, H5S_ALL, H5P_DEFAULT, data);
if (status < 0) { fprintf(stderr, "H5Dread on final file failed\n"); return 1; }
print_data("Final read after re-open (expect all 2's)", data);
H5Dclose(final_ds_id);
H5Fclose(final_file_id);
H5Pclose(fapl_rdonly);
printf("\nDone.\n");
return 0;
}
```
Output
```
=== Step 1: Create mask_test.h5 with chunked dataset 'mask' ===
Created mask_test.h5
=== Step 2: Read from 'mask', expect all zeros ===
Fill value check: PASS (all zeros)
=== Step 3: Open mask_test.h5 read-only handle ===
read-only handle read (expect zeros):
[ 0] 0 0
[ 1] 0 0
[ 2] 0 0
=== Step 4: Write all 2's to 'mask' in-place (core driver) ===
In-place write complete
=== Step 5a: Open new handle to mask_test.h5 and read 'mask' ===
Read via new original handle (before closing):
[ 0] 0 0
[ 1] 0 0
[ 2] 0 0
=== Step 5b: Close both read-only handles ===
Both read-only handles closed
=== Step 5c: Re-open mask_test.h5 and read 'mask' ===
Final read after re-open (expect all 2's):
[ 0] 2 2
[ 1] 2 2
[ 2] 2 2
Done.
```
Expected Behavior
-
HDF5 should detect that the underlying file content has changed and invalidate its internal caches, rather than silently serving stale data through a reused shared structure.
Platform
-
HDF5 version: 2.0.1
OS and version: Linux
Compiler and version: GCC v8.5.0
Build system: ninja v1.11.1
Configure options: Default
MPI library and version: MPICH 4.3.2
Contributor guide
Assessment
This issue has not been assessed yet.