Skip to content

fix(fuse): complete open handle before unlinking path#966

Open
ljy1-lixing wants to merge 1 commit into
CurvineIO:mainfrom
ljy1-lixing:fix/fuse-unlink-open-handle-race
Open

fix(fuse): complete open handle before unlinking path#966
ljy1-lixing wants to merge 1 commit into
CurvineIO:mainfrom
ljy1-lixing:fix/fuse-unlink-open-handle-race

Conversation

@ljy1-lixing

Copy link
Copy Markdown
Contributor

Summary

Fix a FUSE unlink race where a path could remain visible when it was unlinked immediately after closing an open file.

When a file was closed and then unlinked right away, the kernel could send unlink before the FUSE release for the closed handle had finished. Before this change, Curvine saw that the file still had an open handle and deferred backend deletion. That left the old backend path visible temporarily.

A subsequent open with create semantics for the same name could then resolve the old file during lookup instead of creating a new file, causing intermittent open failures or stale-path behavior.

Root Cause

Curvine's unlink path treated an inode with open handles as a delayed-delete case. That preserved the backend path until the final handle was released.

This is unsafe for same-name recreation because POSIX unlink semantics require the directory entry to disappear when unlink() returns. Existing open handles may continue to complete their own file state, but they must not keep the unlinked pathname visible or block a new file with the same name.

Changes

  • In FUSE unlink, detect an existing open handle for the target path.
  • Complete the open handle before deleting the backend path.
  • Delete the backend path immediately after the handle is completed.
  • Expose NodeState::find_open_handle_by_path() so the unlink path can find the active handle by pathname.

Verification

  • Repeated same-name close, unlink, and reopen/create workflow passes after the fix.

## Summary

Fix a FUSE unlink race where a path could remain visible when it was unlinked immediately after closing an open file.

When a file was closed and then unlinked right away, the kernel could send `unlink` before the FUSE `release` for the closed handle had
finished. Before this change, Curvine saw that the file still had an open handle and deferred backend deletion. That left the old
backend path visible temporarily.

A subsequent open with create semantics for the same name could then resolve the old file during lookup instead of creating a new file,
causing intermittent open failures or stale-path behavior.

## Root Cause

Curvine's unlink path treated an inode with open handles as a delayed-delete case. That preserved the backend path until the final
handle was released.

This is unsafe for same-name recreation because POSIX unlink semantics require the directory entry to disappear when `unlink()` returns.
Existing open handles may continue to complete their own file state, but they must not keep the unlinked pathname visible or block a new
file with the same name.

## Changes

- In FUSE `unlink`, detect an existing open handle for the target path.
- Complete the open handle before deleting the backend path.
- Delete the backend path immediately after the handle is completed.
- Expose `NodeState::find_open_handle_by_path()` so the unlink path can find the active handle by pathname.
- Remove temporary debug logging used during diagnosis.

## Verification

- Repeated same-name close, unlink, and reopen/create workflow passes after the fix.
- `cargo fmt --check` passes.
- `cargo test -p curvine-fuse ...` could not run locally because `protoc` is not installed.
if self.state.should_delete_now(parent_ino, Some(name))? {
let completed_open_handle = if let Some(handle) = self.state.find_open_handle_by_path(&path)
{
handle.complete(None).await?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This completes the matched open handle during unlink, but unlink can run while the file descriptor is still genuinely open and may still receive writes, flushes, or reads before release. Completing the backend stream here can commit/close the writer early, so later operations on the same fd may fail or write after completion. Please avoid completing arbitrary open handles from unlink; the same-name recreation fix needs to remove the directory entry without changing the lifecycle of still-open fds, or only act on handles that are known to already be in the release/closed path.

@ljy1-lixing ljy1-lixing closed this Jul 2, 2026
@ljy1-lixing ljy1-lixing reopened this Jul 2, 2026
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.

2 participants