Skip to content

daxfs: Read holes as zeros instead of returning short - #17

Open
congwang-mk wants to merge 1 commit into
mainfrom
fix-hole-reads
Open

daxfs: Read holes as zeros instead of returning short#17
congwang-mk wants to merge 1 commit into
mainfrom
fix-hole-reads

Conversation

@congwang-mk

Copy link
Copy Markdown
Contributor

Problem

A file extended by truncate has no overlay pages behind the new range and no base image data either. daxfs_read_iter() treated that as end of file and stopped, so read() reported EOF in the middle of a file that stat() says is larger:

: > f; truncate -s 16384 f
wc -c < f    ->  16384
dd if=f      ->  0 bytes

POSIX requires a hole to read as zeros. The mmap path already does this, since daxfs_copy_page() zero-fills when there is no source, so read() and mmap() disagreed about the same range. This is also why the fio benchmarking in #15 had to lay files out with dd rather than truncating them into place.

Fix

Zero-fill the hole a page at a time and continue the loop, so a read spanning a hole still finds the data after it rather than stopping at the gap.

The part that needed care: the caller could not tell a hole from a failure. daxfs_base_file_data() returned NULL both when nothing backed the range and when daxfs_pcache_get_page() failed with -EIO, -EFBIG or -ENOENT. Blindly zero-filling would have converted a pcache read error into silent zeros, which is a worse bug than the short read it replaces.

So the function now returns ERR_PTR for failures and keeps NULL for holes, and the contract is documented at the definition. daxfs_read_iter() propagates the error (or returns the short count if it already copied something) and zero-fills only for a genuine NULL.

The other five callers are updated for the new contract, which is not optional: they previously passed the NULL to daxfs_copy_page(), which zero-fills, and with an ERR_PTR that would have memcpy'd from an error pointer.

  • daxfs_write_prealloc() leaves the page unpublished so the per-page path retries and surfaces the error
  • daxfs_write_iter() fails the write rather than COW zeros over unreadable data
  • daxfs_dax_fault() and daxfs_dax_pfn_mkwrite() fall through to VM_FAULT_SIGBUS, taking care to do it via the existing !data check after sb_end_pagefault() rather than returning from inside the critical section

Testing

New regression test in tests/test_overlay.sh: a truncate-extended file must report the full size from read(), the range must be zeros, and a read spanning the hole must still find data written after it.

Verified it catches the bug rather than passing vacuously:

without the fix:   FAIL: Hole reads
                   Tests run: 21, passed: 20, failed: 1

with the fix:      PASS: Holes read as zeros, not short
                   Tests run: 21, passed: 21, failed: 0

Also checked, since this touches the hot read loop:

Note

#16 is open against main and also adds a test after test_overlay_truncate in tests/test_overlay.sh. Whichever merges second will need a trivial rebase for the test registration list; the C changes do not overlap.

A file extended by truncate has no overlay pages behind the new range and
no base image data either. daxfs_read_iter() treated that as end of file
and stopped, so read() reported EOF in the middle of a file that stat()
says is larger:

  : > f; truncate -s 16384 f
  wc -c < f   ->  16384
  dd if=f     ->  0 bytes

POSIX requires a hole to read as zeros. The mmap path already does this,
since daxfs_copy_page() zero-fills when there is no source, so read() and
mmap() disagreed about the same range.

Zero-fill the hole a page at a time and keep going, so a read spanning a
hole still finds the data after it rather than stopping at the gap.

This needs the caller to tell a hole apart from a failure, which
daxfs_base_file_data() could not express: it returned NULL both when
nothing backed the range and when daxfs_pcache_get_page() failed. Blindly
zeroing would have turned a pcache -EIO or -EFBIG into silent zeros,
which is worse than the short read it replaces. Return ERR_PTR for
failures and keep NULL for holes.

Update the other callers for the new contract. The COW paths in
daxfs_write_prealloc(), daxfs_write_iter(), daxfs_dax_fault() and
daxfs_dax_pfn_mkwrite() previously fed the NULL to daxfs_copy_page(),
which zero-fills; with an ERR_PTR that would have memcpy'd from an error
pointer. They now leave the page unpublished or fail the fault rather
than COW zeros over data they could not read.

Signed-off-by: Cong Wang <cwang@multikernel.io>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant