Skip to content
Open
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
10 changes: 10 additions & 0 deletions jfuse-api/src/main/java/org/cryptomator/jfuse/api/Stat.java
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,16 @@ public interface Stat {
*/
TimeSpec birthTime();

/**
* Backup time (macOS-specific, via macFUSE darwin extensions).
* Returns {@code null} on platforms that don't support it.
*
* @return backup time value, or {@code null} if unsupported
*/
default TimeSpec backupTime() {
return null;
}

/**
* Set {@link #getMode() mode}.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,12 @@ public static void main(String[] args) {
}
try (var fuse = builder.build(new PosixMirrorFileSystem(mirrored, builder.errno()))) {
LOG.info("Mounting at {}...", mountPoint);
fuse.mount("jfuse", mountPoint, "-s", "-obackend=fskit");
var backend = System.getProperty("fuse.backend", "");
if (backend.isEmpty()) {
fuse.mount("jfuse", mountPoint, "-s");
} else {
fuse.mount("jfuse", mountPoint, "-s", "-obackend=" + backend);
}
Comment on lines -40 to +45
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

undo the system property change. This lib already exposes a way to pass fuse mount flags, which is sufficient to specify the backend.

LOG.info("Mounted to {}.", mountPoint);
LOG.info("Enter a anything to unmount...");
System.in.read();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,29 @@

import org.cryptomator.jfuse.api.DirFiller;
import org.cryptomator.jfuse.api.Stat;
import org.cryptomator.jfuse.mac.extr.fuse3.fuse_darwin_attr;
import org.cryptomator.jfuse.mac.extr.fuse3.fuse_fill_dir_t;
import org.cryptomator.jfuse.mac.extr.fuse3.stat;

import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
import java.util.function.Consumer;

record DirFillerImpl(MemorySegment buf, MemorySegment callback, Arena arena) implements DirFiller {
record DirFillerImpl(MemorySegment buf, MemorySegment callback, Arena arena, boolean darwinExtensions) implements DirFiller {

@Override
public int fill(String name, Consumer<Stat> statFiller, long offset, int flags) {
var statSegment = stat.allocate(arena);
statFiller.accept(new StatImpl(statSegment));
return fuse_fill_dir_t.invoke(callback, buf, arena.allocateFrom(name), statSegment, offset, flags);
MemorySegment segment;
Stat statWrapper;
if (darwinExtensions) {
segment = fuse_darwin_attr.allocate(arena);
statWrapper = new StatImpl(segment);
} else {
segment = stat.allocate(arena);
statWrapper = new StatCompatImpl(segment);
}
Comment on lines +19 to +25
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

do we really need the compat version? since this impl is macOS-specific, can't we always use fuse_darwin_attr? (have you researched behaviour with fuse-t and older versions of macFUSE?)

statFiller.accept(statWrapper);
return fuse_fill_dir_t.invoke(callback, buf, arena.allocateFrom(name), segment, offset, flags);
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ class FuseFunctions {
private FuseFunctions() {
var lookup = SymbolLookup.loaderLookup();
var linker = Linker.nativeLinker();
this.fuse_parse_cmdline = lookup.find("fuse_parse_cmdline")
// Prefer fuse_parse_cmdline_312 (macFUSE 5.2.0+), fall back to fuse_parse_cmdline
this.fuse_parse_cmdline = lookup.find("fuse_parse_cmdline_312")
.or(() -> lookup.find("fuse_parse_cmdline"))
Comment on lines -36 to +38
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

normally the unversioned method is an alias for the newest versioned method. We usually use versioned methods for the reverse case: When we want to explicitly use an older version. Are you sure this is correct?

.map(symbol -> linker.downcallHandle(symbol, PARSE_CMDLINE_DESCRIPTOR))
.orElseThrow(() -> new UnsatisfiedLinkError("unresolved symbol fuse_parse_cmdline"));
this.fuse_set_feature_flag = lookup.find("fuse_set_feature_flag")
Expand Down
51 changes: 38 additions & 13 deletions jfuse-mac/src/main/java/org/cryptomator/jfuse/mac/FuseImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import org.cryptomator.jfuse.api.Fuse;
import org.cryptomator.jfuse.api.FuseConnInfo;
import org.cryptomator.jfuse.api.Stat;
import org.cryptomator.jfuse.api.FuseMount;
import org.cryptomator.jfuse.api.FuseMountFailedException;
import org.cryptomator.jfuse.api.FuseOperations;
Expand All @@ -17,27 +18,44 @@
import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.ValueLayout;
import java.util.ArrayList;
import java.util.List;

final class FuseImpl extends Fuse {

private volatile boolean darwinExtensions;

public FuseImpl(FuseOperations fuseOperations) {
super(fuseOperations, fuse_operations::allocate);
}

@Override
protected FuseMount mount(List<String> args) throws FuseMountFailedException {
var fuseArgs = parseArgs(args);
// macFUSE 5.2.0 crashes in fuse_darwin_mount if volname is not set (strdup of NULL)
var effectiveArgs = ensureVolname(args);
var fuseArgs = parseArgs(effectiveArgs);
var fuse = createFuseFS(fuseArgs);
if (fuse_h.fuse_mount(fuse, fuseArgs.mountPoint()) != 0) {
throw new FuseMountFailedException("fuse_mount failed");
}
// Defer fuse_mount to loop() — macFUSE FSKit requires mount+loop on the same thread
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

not an option, leaks resource.

why should mount + loop be on different threads in the old implemenation? there are no different threads before fuse_loop_mt

return new FuseMountImpl(fuse, fuseArgs);
}

private static List<String> ensureVolname(List<String> args) {
for (var arg : args) {
if (arg.contains("volname=")) {
return args;
}
}
var result = new ArrayList<>(args);
var progName = args.isEmpty() ? "jfuse" : args.getFirst();
result.add("-ovolname=" + progName);
return result;
}

@VisibleForTesting
MemorySegment createFuseFS(FuseArgs fuseArgs) throws FuseMountFailedException {
var fuse = FuseNewHelper.getInstance().fuse_new(fuseArgs.args(), fuseOperationsStruct, fuseOperationsStruct.byteSize(), MemorySegment.NULL);
var helper = FuseNewHelper.getInstance();
this.darwinExtensions = helper.darwinExtensions();
var fuse = helper.fuse_new(fuseArgs.args(), fuseOperationsStruct, fuseOperationsStruct.byteSize(), MemorySegment.NULL);
if (MemorySegment.NULL.equals(fuse)) {
throw new FuseMountFailedException("fuse_new failed");
}
Expand All @@ -46,7 +64,6 @@ MemorySegment createFuseFS(FuseArgs fuseArgs) throws FuseMountFailedException {

@VisibleForTesting
FuseArgs parseArgs(List<String> cmdLineArgs) throws IllegalArgumentException {
System.out.println("DEBUG: cmdLineArgs = " + cmdLineArgs); // Add this
var args = fuse_args.allocate(fuseArena);
var argc = cmdLineArgs.size();
var argv = fuseArena.allocate(ValueLayout.ADDRESS, argc + 1L);
Expand Down Expand Up @@ -112,9 +129,14 @@ MemorySegment init(MemorySegment conn, MemorySegment cfg) {
var connInfo = new FuseConnInfoImpl(conn);
if (fuse_h.fuse_version() >= 317) {
connInfo = new FuseConnInfoImpl317(conn);
connInfo.setFeatureFlag(FuseConnInfo.FUSE_CAP_READDIRPLUS);
} else {
connInfo.setWant(connInfo.want() | FuseConnInfo.FUSE_CAP_READDIRPLUS);
}
// only request READDIRPLUS if the kernel advertises support
if ((connInfo.capable() & FuseConnInfo.FUSE_CAP_READDIRPLUS) != 0) {
if (fuse_h.fuse_version() >= 317) {
connInfo.setFeatureFlag(FuseConnInfo.FUSE_CAP_READDIRPLUS);
} else {
connInfo.setWant(connInfo.want() | FuseConnInfo.FUSE_CAP_READDIRPLUS);
}
}
var config = new FuseConfigImpl(cfg);
fuseOperations.init(connInfo, config);
Expand Down Expand Up @@ -157,14 +179,18 @@ int fsyncdir(MemorySegment path, int datasync, MemorySegment fi) {
return fuseOperations.fsyncdir(MemoryUtils.toUtf8StringOrNull(path), datasync, new FileInfoImpl(fi));
}

private Stat wrapStat(MemorySegment segment) {
return darwinExtensions ? new StatImpl(segment) : new StatCompatImpl(segment);
}

@VisibleForTesting
int getattr(MemorySegment path, MemorySegment stat, MemorySegment fi) {
return fuseOperations.getattr(path.getString(0), new StatImpl(stat), FileInfoImpl.ofNullable(fi));
return fuseOperations.getattr(path.getString(0), wrapStat(stat), FileInfoImpl.ofNullable(fi));
}

@VisibleForTesting
int fgetattr(MemorySegment path, MemorySegment stat, MemorySegment fi) {
return fuseOperations.getattr(path.getString(0), new StatImpl(stat), new FileInfoImpl(fi));
return fuseOperations.getattr(path.getString(0), wrapStat(stat), new FileInfoImpl(fi));
}

@VisibleForTesting
Expand Down Expand Up @@ -209,7 +235,7 @@ private int read(MemorySegment path, MemorySegment buf, long size, long offset,

private int readdir(MemorySegment path, MemorySegment buf, MemorySegment filler, long offset, MemorySegment fi, int flags) {
try (var arena = Arena.ofConfined()) {
return fuseOperations.readdir(path.getString(0), new DirFillerImpl(buf, filler, arena), offset, new FileInfoImpl(fi), flags);
return fuseOperations.readdir(path.getString(0), new DirFillerImpl(buf, filler, arena, darwinExtensions), offset, new FileInfoImpl(fi), flags);
}
}

Expand Down Expand Up @@ -260,7 +286,6 @@ private int unlink(MemorySegment path) {
int utimens(MemorySegment path, MemorySegment times, MemorySegment fi) {
try (var arena = Arena.ofConfined()) {
if (MemorySegment.NULL.equals(times)) {
// set both times to current time
var segment = timespec.allocate(arena);
timespec.tv_sec(segment, 0);
timespec.tv_nsec(segment, stat_h.UTIME_NOW());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.cryptomator.jfuse.mac;

import org.cryptomator.jfuse.api.FuseMount;
import org.cryptomator.jfuse.api.FuseMountFailedException;
import org.cryptomator.jfuse.mac.extr.fuse3.fuse_h;
import org.cryptomator.jfuse.mac.extr.fuse3.fuse_loop_config_v1;

Expand All @@ -14,20 +15,20 @@ record FuseMountImpl(MemorySegment fuse, FuseArgs fuseArgs) implements FuseMount

@Override
public int loop() {
// depends on fuse version: https://github.com/libfuse/libfuse/blob/fuse-3.12.0/include/fuse.h#L1011-L1050
// macFUSE FSKit backend requires fuse_mount and fuse_loop on the same thread
if (fuse_h.fuse_mount(fuse, fuseArgs.mountPoint()) != 0) {
throw new RuntimeException(new FuseMountFailedException("fuse_mount failed"));
}
Comment on lines +19 to +21
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Destroy FUSE handle when mount fails in loop

When fuse_mount fails, loop() throws immediately and the fuse handle created earlier by fuse_new is never destroyed. With the new flow (mounting moved into loop()), that failure happens before Fuse.mount(...) stores the FuseMount instance, so close() cannot reach it to call destroy(). Repeated mount failures (e.g., invalid mount point/permissions) will leak native FUSE state in the process.

Useful? React with 👍 / 👎.

if (!fuseArgs.multithreaded() || fuse_h.fuse_version() < FUSE_3_2) {
// FUSE 3.1: to keep things simple, we just don't support fuse_loop_mt
return fuse_h.fuse_loop(fuse);
} else if (fuse_h.fuse_version() < FUSE_3_12) {
// FUSE 3.2
try (var arena = Arena.ofConfined()) {
var loopCfg = fuse_loop_config_v1.allocate(arena);
fuse_loop_config_v1.clone_fd(loopCfg, fuseArgs.cloneFd());
fuse_loop_config_v1.max_idle_threads(loopCfg, fuseArgs.maxIdleThreads());
return fuse_h.fuse_loop_mt(fuse, loopCfg);
}
} else {
// FUSE 3.12
var loopCfg = fuse_h.fuse_loop_cfg_create();
try {
fuse_h.fuse_loop_cfg_set_clone_fd(loopCfg, fuseArgs.cloneFd());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import org.cryptomator.jfuse.mac.extr.fuse3.fuse_h;

import java.lang.foreign.Arena;
import java.lang.foreign.FunctionDescriptor;
import java.lang.foreign.Linker;
import java.lang.foreign.MemoryLayout;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.SymbolLookup;
import java.lang.foreign.ValueLayout;
Expand All @@ -16,38 +18,79 @@
* This class is necessary due to changes in libfuse 3.17.1 and onwards: The function {@code fuse_new} is _not_ available anymore as unversioned symbol.
* One can call the function either by also specifying the version or call the unversioned symbol {@code fuse_new_31}.
* <p>
* Versioned symbols require a custom SymbolLookup (see PR #117), but libraries loaded via {@link System#loadLibrary(String)} cannot be accessed.
* Hence, this would require to _always_ set the libPath, loosing compatiblity to more exotic linux OSs with custom lib locations.
* To circumvent this issue, we call {@code fuse_version} to decide which (unversioned) symbol name we have to use.
* On macFUSE, we call {@code _fuse_new_31} (the 5-arg internal variant) directly, passing a properly
* initialized {@code libfuse_version} struct with {@code darwin_extensions_enabled=0}.
* This is required for FSKit backend support — the 4-arg wrapper zeros the version struct,
* which has the same effect, but calling the internal symbol directly makes the intent explicit
* and matches what fuse_main_real_versioned does.
Comment on lines +21 to +25
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Documentation mismatch: Javadoc says darwin_extensions_enabled=0 but code sets flags=1.

Line 22 states darwin_extensions_enabled=0, but line 79 sets the flags field to 1, enabling darwin extensions. Update the Javadoc to match the actual behavior.

📝 Proposed fix
 * On macFUSE, we call {`@code` _fuse_new_31} (the 5-arg internal variant) directly, passing a properly
- * initialized {`@code` libfuse_version} struct with {`@code` darwin_extensions_enabled=0}.
+ * initialized {`@code` libfuse_version} struct with {`@code` darwin_extensions_enabled=1}.
 * This is required for FSKit backend support — the 4-arg wrapper zeros the version struct,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@jfuse-mac/src/main/java/org/cryptomator/jfuse/mac/FuseNewHelper.java` around
lines 21 - 25, Javadoc and code disagree: the class FuseNewHelper initializes
the libfuse_version struct with flags=1 (enabling darwin extensions) but the
Javadoc claims darwin_extensions_enabled=0; update the Javadoc comment above
FuseNewHelper (and the explanatory block mentioning _fuse_new_31 and
libfuse_version) to state that darwin extensions are enabled (flags=1) and
explain that we explicitly set flags=1 to enable macFUSE/Darwin extensions to
match the actual initialization performed in the code (libfuse_version.flags =
1).

*/
public class FuseNewHelper {

private static final AtomicReference<FuseNewHelper> INSTANCE = new AtomicReference<>(null);
private static final SymbolLookup SYMBOL_LOOKUP = SymbolLookup.loaderLookup().or(Linker.nativeLinker().defaultLookup());
private static final FunctionDescriptor DESC = FunctionDescriptor.of(

// 5-arg _fuse_new_31: (fuse_args*, fuse_operations*, size_t, libfuse_version*, void*) -> fuse*
private static final FunctionDescriptor DESC_5ARG = FunctionDescriptor.of(
fuse_h.C_POINTER,
fuse_h.C_POINTER,
fuse_h.C_POINTER,
fuse_h.C_LONG,
fuse_h.C_POINTER,
fuse_h.C_POINTER
);

// 4-arg fallback: (fuse_args*, fuse_operations*, size_t, void*) -> fuse*
private static final FunctionDescriptor DESC_4ARG = FunctionDescriptor.of(
fuse_h.C_POINTER,
fuse_h.C_POINTER,
fuse_h.C_POINTER,
fuse_h.C_LONG,
fuse_h.C_POINTER
);

// libfuse_version struct: {uint32 major, uint32 minor, uint32 hotfix, uint32 flags}
private static final MemoryLayout LIBFUSE_VERSION_LAYOUT = MemoryLayout.structLayout(
ValueLayout.JAVA_INT.withName("major"),
ValueLayout.JAVA_INT.withName("minor"),
ValueLayout.JAVA_INT.withName("hotfix"),
ValueLayout.JAVA_INT.withName("flags")
);

private final MethodHandle fuse_new;
private final boolean fiveArg;
private final boolean darwinExtensions;

private FuseNewHelper(String symbolName) {
private FuseNewHelper(String symbolName, boolean fiveArg) {
this.fiveArg = fiveArg;
this.darwinExtensions = fiveArg;
this.fuse_new = Linker.nativeLinker().downcallHandle(
findOrThrow(symbolName),
DESC);
fiveArg ? DESC_5ARG : DESC_4ARG);
}

public MemorySegment fuse_new(MemorySegment args, MemorySegment op, long op_size, MemorySegment private_data) {
try {
return (MemorySegment) fuse_new.invokeExact(args, op, op_size, private_data);
if (fiveArg) {
try (var arena = Arena.ofConfined()) {
var version = arena.allocate(LIBFUSE_VERSION_LAYOUT);
version.set(ValueLayout.JAVA_INT, 0, 3); // major
version.set(ValueLayout.JAVA_INT, 4, 18); // minor
version.set(ValueLayout.JAVA_INT, 8, 2); // hotfix
version.set(ValueLayout.JAVA_INT, 12, 1); // darwin_extensions_enabled=1
return (MemorySegment) fuse_new.invokeExact(args, op, op_size, version, private_data);
}
} else {
return (MemorySegment) fuse_new.invokeExact(args, op, op_size, private_data);
}
} catch (Throwable ex) {
throw new AssertionError("should not reach here", ex);
}
}

public boolean darwinExtensions() {
return darwinExtensions;
}

public synchronized static FuseNewHelper getInstance() {
if (INSTANCE.get() == null) {
INSTANCE.set(createInstance());
Expand All @@ -57,9 +100,14 @@ public synchronized static FuseNewHelper getInstance() {

private static FuseNewHelper createInstance() throws IllegalStateException {
if (getLibVersion() < 317) {
return new FuseNewHelper("fuse_new");
return new FuseNewHelper("fuse_new", false);
} else {
return new FuseNewHelper("fuse_new_31");
// Prefer _fuse_new_31 (5-arg internal) if available, fall back to fuse_new_31 (4-arg wrapper)
if (SYMBOL_LOOKUP.find("_fuse_new_31").isPresent()) {
return new FuseNewHelper("_fuse_new_31", true);
Comment on lines +105 to +107
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this sounds like internal API. Is this the official, documented way to enable darwin_extensions?

} else {
return new FuseNewHelper("fuse_new_31", false);
}
}
}

Expand Down
Loading