Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ The following emojis are used to highlight certain changes:

- `routing/http/client`: `WithProviderInfoFunc` option resolves provider addresses at provide-time instead of client construction time. This only impacts legacy HTTP-only custom routing setups that depend on [IPIP-526](https://github.com/ipfs/specs/pull/526) and were sending unresolved `0.0.0.0` addresses in provider records instead of actual interface addresses. [#1115](https://github.com/ipfs/boxo/pull/1115)
- `chunker`: added `Register` function to allow custom chunkers to be registered for use with `FromString`.
- `mfs`: added `Directory.Mode()` and `Directory.ModTime()` getters to match the existing `File.Mode()` and `File.ModTime()` API. [#1131](https://github.com/ipfs/boxo/pull/1131)

### Changed

Expand All @@ -36,6 +37,7 @@ The following emojis are used to highlight certain changes:

- `bitswap/server`: incoming identity CIDs in wantlist messages are now silently ignored instead of killing the connection to the remote peer. Some IPFS implementations naively send identity CIDs, and disconnecting them for it caused unnecessary churn. [#1117](https://github.com/ipfs/boxo/pull/1117)
- `bitswap/network`: `ExtractHTTPAddress` now infers default ports for portless HTTP multiaddrs (e.g. `/dns/host/https` without `/tcp/443`). [#1123](https://github.com/ipfs/boxo/pull/1123)
- `mfs`: concurrent `Flush` and `Close` on the same file descriptor no longer panic, fixing a crash seen under FUSE. `Flush` after `Close` now returns `ErrClosed`. [#1131](https://github.com/ipfs/boxo/pull/1131)
- `mfs`: preserve `CidBuilder` and `SizeEstimationMode` across `setNodeData()`, `Mkdir()` and `NewRoot()`. [#1125](https://github.com/ipfs/boxo/pull/1125)

### Security
Expand Down
28 changes: 28 additions & 0 deletions mfs/dir.go
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,20 @@ func (d *Directory) getNode(cacheClean bool) (ipld.Node, error) {
return nd.Copy(), err
}

// Mode returns the directory's POSIX permission bits from UnixFS metadata.
// Returns 0 when no mode is stored.
func (d *Directory) Mode() (os.FileMode, error) {
nd, err := d.GetNode()
if err != nil {
return 0, err
}
fsn, err := ft.ExtractFSNode(nd)
if err != nil {
return 0, err
}
return fsn.Mode() & 0xFFF, nil
}

func (d *Directory) SetMode(mode os.FileMode) error {
nd, err := d.GetNode()
if err != nil {
Expand All @@ -556,6 +570,20 @@ func (d *Directory) SetMode(mode os.FileMode) error {
return nil
}

// ModTime returns the directory's last modification time from UnixFS metadata.
// Returns zero time when no mtime is stored.
func (d *Directory) ModTime() (time.Time, error) {
nd, err := d.GetNode()
if err != nil {
return time.Time{}, err
}
fsn, err := ft.ExtractFSNode(nd)
if err != nil {
return time.Time{}, err
}
return fsn.ModTime(), nil
}

func (d *Directory) SetModTime(ts time.Time) error {
nd, err := d.GetNode()
if err != nil {
Expand Down
14 changes: 14 additions & 0 deletions mfs/fd.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"io"
"sync"

mod "github.com/ipfs/boxo/ipld/unixfs/mod"
ipld "github.com/ipfs/go-ipld-format"
Expand Down Expand Up @@ -45,6 +46,12 @@ type fileDescriptor struct {
mod *mod.DagModifier
flags Flags

// mu serializes Flush and Close to prevent concurrent access to
// the underlying DagModifier, which is not safe for concurrent use.
// Without this, a caller that invokes Flush and Close from separate
// goroutines (e.g. FUSE dispatching FLUSH and RELEASE concurrently)
// can trigger a data race inside DagModifier.Sync.
mu sync.Mutex
state state
}

Expand Down Expand Up @@ -110,6 +117,8 @@ func (fi *fileDescriptor) CtxReadFull(ctx context.Context, b []byte) (int, error
// Close flushes, then propagates the modified dag node up the directory structure
// and signals a republish to occur
func (fi *fileDescriptor) Close() error {
fi.mu.Lock()
defer fi.mu.Unlock()
if fi.state == stateClosed {
return ErrClosed
}
Expand All @@ -128,6 +137,11 @@ func (fi *fileDescriptor) Close() error {
// the entry in the parent directory (setting `fullSync` to
// propagate the update all the way to the root).
func (fi *fileDescriptor) Flush() error {
fi.mu.Lock()
defer fi.mu.Unlock()
if fi.state == stateClosed {
return ErrClosed
}
return fi.flushUp(true)
}

Expand Down
59 changes: 59 additions & 0 deletions mfs/mfs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1447,6 +1447,65 @@ func TestConcurrentWrites(t *testing.T) {
}
}

// TestConcurrentFlushAndClose verifies that calling Flush and Close on the
// same fileDescriptor from different goroutines does not panic or corrupt
// data. This guards against the race where a FUSE daemon dispatches FLUSH
// and RELEASE concurrently for the same file handle (e.g. when FLUSH is
// interrupted by SIGURG and the kernel sends RELEASE while the flush
// goroutine is still running).
func TestConcurrentFlushAndClose(t *testing.T) {
ctx := t.Context()
ds, rt := setupRoot(ctx, t)
dir := rt.GetDirectory()

nd := dag.NodeWithData(ft.FilePBData(nil, 0))
prov := new(fakeProvider)

// Run many iterations to increase the chance of hitting the race.
for range 200 {
fi, err := NewFile("test", nd, dir, ds, prov)
if err != nil {
t.Fatal(err)
}

fd, err := fi.Open(Flags{Read: true, Write: true, Sync: true})
if err != nil {
t.Fatal(err)
}

if _, err := fd.Write([]byte("hello")); err != nil {
t.Fatal(err)
}

// Launch Flush and Close concurrently. One of them will
// acquire the internal mutex first; the other must wait
// and then observe a consistent state (either flushed or
// closed). Neither should panic.
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
// Flush may succeed or return ErrClosed if Close wins.
err := fd.Flush()
if err != nil && !errors.Is(err, ErrClosed) {
t.Errorf("Flush: unexpected error: %v", err)
}
}()
go func() {
defer wg.Done()
if err := fd.Close(); err != nil {
t.Errorf("Close: %v", err)
}
}()
wg.Wait()

// A second Close must return ErrClosed.
if err := fd.Close(); !errors.Is(err, ErrClosed) {
t.Fatalf("expected ErrClosed on double close, got: %v", err)
}
}
}

func TestFileDescriptors(t *testing.T) {
ctx := t.Context()

Expand Down
Loading