Skip to content

libext: real fsync + page-cache bridge for ext2/3/4#1431

Open
gburd wants to merge 2 commits into
cloudius-systems:masterfrom
gburd:pr/ext4-fsync-cache
Open

libext: real fsync + page-cache bridge for ext2/3/4#1431
gburd wants to merge 2 commits into
cloudius-systems:masterfrom
gburd:pr/ext4-fsync-cache

Conversation

@gburd

@gburd gburd commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

What

Three fixes to the ext (lwext4) filesystem module toward the Postgres-over-ext
goal.

  1. fsync durability. ext mounts with lwext4 block-cache write-back enabled,
    so a write only reaches the disk when the block cache is flushed -- but
    vop_fsync was vop_nullop, so fsync(2)/fdatasync(2) on an ext file
    persisted nothing. ext_fsync() now flushes the device's block cache (like
    ext_sync does at unmount), making written data durable.

  2. Page-cache bridge (vop_cache). The vop_cache slot was null, so mmap
    faults on ext files hit the block layer on every fault, unlike ROFS and ZFS
    which populate the shared page cache. ext_map_cached_page() reads one
    page-aligned page into a freshly allocated page and hands it to
    pagecache::map_read_cached_page(), warming the read cache so subsequent
    faults and readahead are served from it. Allocate-and-copy for now (lwext4's
    block-cache buffers are not page-aligned/shareable the way ROFS's read-around
    cache is); a zero-copy borrow-and-pin bridge like the ZFS ARC one can follow.

  3. Inode deletion time. The inode-delete path freed the inode from the
    bitmap but never set its on-disk dtime, so after OSv deleted a file, Linux
    e2fsck flagged "deleted inode has zero dtime". ext_mark_inode_deleted()
    now sets it before each ext4_fs_free_inode() (lwext4's own
    ext4_inode_set_del_time is not exported from liblwext4.so, so the field is
    set directly with the byte-order-invariant 0xffffffff).

Because the module builds with -fno-rtti and <osv/pagecache.hh> pulls in
<osv/trace.hh> (typeid), the two page-cache symbols are declared minimally
instead of including the heavy header (the uio carries the hashkey opaquely).

Testing

tests/tst-ext4-rw.cc: mmap a pre-populated ext4 file and verify the pattern
survives the vop_cache bridge (first fault + cached re-read), then write a
file, fsync it, and read it back (plus fdatasync and a fresh re-open).
Verified on OSv under KVM with an ext4 second disk (mkfs.ext4 -b 4096 -O ^64bit,^metadata_csum, which lwext4 supports). After the run -- which
creates, fsyncs, and deletes a file -- Linux e2fsck -n -f on the disk exits 0
(clean), confirming both the fsync durability and the dtime fix.

gburd added 2 commits July 15, 2026 10:39
Two gaps in the ext (lwext4) filesystem module for the Postgres-over-ext goal.

1. fsync durability.  ext mounts with lwext4 block-cache write-back enabled, so
   a write only reaches the disk when the block cache is flushed -- but
   vop_fsync was vop_nullop, so fsync(2)/fdatasync(2) on an ext file persisted
   nothing.  Implement ext_fsync() to flush the device's block cache (like
   ext_sync does at unmount), making written data durable.  The cache is shared
   per device, so this persists the file along with any other dirty buffers,
   which is correct if slightly more than the theoretical per-inode minimum.

2. Page-cache bridge (vop_cache).  The vop_cache slot was null, so mmap faults
   on ext files went through the block layer on every fault, unlike ROFS and
   ZFS which populate the shared page cache.  Add ext_map_cached_page(): on a
   VOP_CACHE call it reads one page-aligned page of file data into a freshly
   allocated page and hands it to pagecache::map_read_cached_page(), warming the
   read cache so subsequent faults and readahead are served from it.  This is an
   allocate-and-copy bridge (lwext4's block-cache buffers are not
   page-aligned/shareable the way ROFS's read-around cache is); a zero-copy
   borrow-and-pin bridge like the ZFS ARC one can follow if it shows up hot.

Because the module is built with -fno-rtti and <osv/pagecache.hh> pulls in
<osv/trace.hh> (which uses typeid), the two page-cache symbols we need are
declared minimally instead of including the heavy header (the uio carries the
hashkey opaquely, so its layout is never needed).

Add tests/tst-ext4-rw.cc: mmap a pre-populated ext4 file and verify the pattern
survives the vop_cache bridge (first fault + cached re-read), then write a file,
fsync it, and read it back (plus fdatasync and a fresh re-open).  Verified on
OSv under KVM with an ext4 second disk (created with mkfs.ext4 -b 4096
-O ^64bit,^metadata_csum, which lwext4 supports).

Known pre-existing limitation (not introduced here, out of scope for this PR):
libext's inode-delete path does not set the inode dtime, so Linux e2fsck flags
"deleted inode has zero dtime" on a disk after OSv deletes a file.  A fresh disk
that OSv only reads/writes+fsyncs (no delete) fscks clean.  Tracked as a
follow-up in the ext write-path correctness work.
Follow-up to the fsync/page-cache work: libext's inode-delete path freed the
inode from the bitmap but never set its on-disk deletion time (dtime), so after
OSv created and deleted a file, Linux e2fsck flagged "Deleted inode NN has zero
dtime" and reported the filesystem as still having errors.

lwext4's own delete path marks the inode with ext4_inode_set_del_time(inode,
-1L) before ext4_fs_free_inode(), but that symbol is not exported from
liblwext4.so.  Add a small ext_mark_inode_deleted() helper that sets
inode->deletion_time = 0xffffffff directly (byte-order invariant, so no
to_le32() needed) and call it before each ext4_fs_free_inode() in the module
(unlink, rmdir, delete-on-last-close, delete-outstanding-on-unmount, and the
dir_link allocation-rollback path).

Verified: after OSv boots an ext4 second disk, mmaps/reads a file, writes and
fsyncs another, then deletes it, `e2fsck -n -f` on the disk now exits 0 (clean),
where before it reported the zero-dtime error.  tst-ext4-rw still passes.
@gburd
gburd force-pushed the pr/ext4-fsync-cache branch from c51b724 to 6c7cd9a Compare July 15, 2026 14:44
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