Using FUSE library with Java; trying to replicate hello.c example
Asked Answered
W

2

11

I am trying to create bindings to the FUSE library using JNA, but I have hit a snag along the road. I have minimized the code as much as possible to make it digestible here.

The FUSE library comes with a few example filesystems written in C. The simplest of them is hello.c. The following is a minimized version of its code to simply a few prints in the filesystem functions:

hello.c:

/*
  FUSE: Filesystem in Userspace
  Copyright (C) 2001-2007  Miklos Szeredi <[email protected]>

  This program can be distributed under the terms of the GNU GPL.
  See the file COPYING.

  gcc -Wall hello.c -o hello `pkg-config fuse --cflags --libs`
*/
#define FUSE_USE_VERSION 26

#include <fuse.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>

static int hello_getattr(const char *path, struct stat *stbuf)
{
    printf("getattr was called\n");
    return 0;
}

static int hello_readdir(const char *path, void *buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info *fi)
{
    printf("readdir was called\n");
    return 0;
}

static int hello_open(const char *path, struct fuse_file_info *fi)
{
    printf("open was called\n");
    return 0;
}

static int hello_read(const char *path, char *buf, size_t size, off_t offset, struct fuse_file_info *fi)
{
    printf("read was called\n");
    return 0;
}

static struct fuse_operations hello_oper = {
    .getattr    = hello_getattr,
    .readdir    = hello_readdir,
    .open       = hello_open,
    .read       = hello_read,
};

int main(int argc, char *argv[])
{
    return fuse_main_real(argc, argv, &hello_oper, sizeof(hello_oper), NULL);
}

This can be compiled using gcc -Wall hello.c -o hello -D_FILE_OFFSET_BITS=64 -I/usr/include/fuse -pthread -lfuse -lrt -ldl

And invoked with ./hello.c -f /some/mount/point

The -f flag is to make it stay in the foreground so that you can see the printf()'s working.

All of this works well, you can see the printf()'s executing properly. I am trying to replicate the same thing in Java using JNA. Here is what I came up with:

FuseTemp.java:

import com.sun.jna.Callback;
import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
import com.sun.jna.Structure;

public class FuseTemp
{
    public static interface Fuse extends Library
    {
        int fuse_main_real(int argc, String[] argv, StructFuseOperations op, long size, Pointer user_data);
    }

    @SuppressWarnings("unused")
    public static class StructFuseOperations extends Structure
    {
        public static class ByReference extends StructFuseOperations implements Structure.ByReference
        {
        }

        public Callback getattr = new Callback()
        {
            public int callback(final String path, final Pointer stat)
            {
                System.out.println("getattr was called");
                return 0;
            }
        };
        public Callback readlink = null;
        public Callback mknod = null;
        public Callback mkdir = null;
        public Callback unlink = null;
        public Callback rmdir = null;
        public Callback symlink = null;
        public Callback rename = null;
        public Callback link = null;
        public Callback chmod = null;
        public Callback chown = null;
        public Callback truncate = null;
        public Callback utime = null;
        public Callback open = new Callback()
        {
            public int callback(final String path, final Pointer info)
            {
                System.out.println("open was called");
                return 0;
            }
        };
        public Callback read = new Callback()
        {
            public int callback(final String path, final Pointer buffer, final long size, final long offset, final Pointer fi)
            {
                System.out.println("read was called");
                return 0;
            }
        };
        public Callback write = null;
        public Callback statfs = null;
        public Callback flush = null;
        public Callback release = null;
        public Callback fsync = null;
        public Callback setxattr = null;
        public Callback getxattr = null;
        public Callback listxattr = null;
        public Callback removexattr = null;
        public Callback opendir = null;
        public Callback readdir = new Callback()
        {
            public int callback(final String path, final Pointer buffer, final Pointer filler, final long offset,
                    final Pointer fi)
            {
                System.out.println("readdir was called");
                return 0;
            }
        };
        public Callback releasedir = null;
        public Callback fsyncdir = null;
        public Callback init = null;
        public Callback destroy = null;
        public Callback access = null;
        public Callback create = null;
        public Callback ftruncate = null;
        public Callback fgetattr = null;
        public Callback lock = null;
        public Callback utimens = null;
        public Callback bmap = null;
        public int flag_nullpath_ok;
        public int flag_reserved;
        public Callback ioctl = null;
        public Callback poll = null;
    }

    public static void main(final String[] args)
    {
        final String[] actualArgs = { "-f", "/some/mount/point" };
        final Fuse fuse = (Fuse) Native.loadLibrary("fuse", Fuse.class);
        final StructFuseOperations.ByReference operations = new StructFuseOperations.ByReference();
        System.out.println("Mounting");
        final int result = fuse.fuse_main_real(actualArgs.length, actualArgs, operations, operations.size(), null);
        System.out.println("Result: " + result);
        System.out.println("Mounted");
    }
}

The definition of the the fuse_operations struct can be found here.

This can be compiled using: javac -cp path/to/jna.jar FuseTemp.java

And invoked using java -cp path/to/jna.jar:. FuseTemp

jna.jar is available here.

The error that comes up is: fusermount: failed to access mountpoint /some/mount/point: Permission denied.

I am executing both programs as the same user with the same permissions on the same mountpoint folder, and I am in the fuse group. I am using:

  • Linux kernel 3.0.0
  • FUSE 2.8.4
  • OpenJDK 1.6.0_23
  • JNA 3.4.0

So my question is: What exactly is different between these two programs (hello.c and FuseTemp.java), and how to make them do the same thing?

Thanks in advance.

Edit: Here is some additional info.

Initial stat of the mountpoint:

  File: `/some/mount/point'
  Size: 4096            Blocks: 8          IO Block: 4096   directory
Device: 803h/2051d      Inode: 540652      Links: 2
Access: (0777/drwxrwxrwx)  Uid: ( 1000/ myusername)   Gid: ( 1000/ myusername)

Output I get from running the Java program as regular user:

Mounting
fusermount: failed to access mountpoint /some/mount/point: Permission denied
Result: 1
Mounted
(program exits with return code 0)

After this, trying to execute stat gives the following error message:

stat: cannot stat/some/mount/point': Transport endpoint is not connected`

That is because the Java program isn't running anymore, so fuse cannot call its callbacks. To unmount, if I try fusermount -u /some/mount/point, I get:

fusermount: entry for /some/mountpoint not found in /etc/mtab

And if I try sudo fusermount -u /some/mount/point, the mountpoint is successfully unmounted and there is no output from fusermount. /etc/mtab is chmod'd 644 (-rw-r--r--) so my user can read it, but it doesn't contain /some/mount/point. After a successful unmount, the mountpoint is back to its old permissions (777 directory).

Now, running the java program as root:

Mounting
Result: 1
Mounted
(program exits with return code 0)

After that, stating /some/mount/point shows that is has not been modified, i.e. it is still a 777 directory.

I have also rewritten FuseTemp.java to include all Callbacks as Callbacks instead of Pointers. The behavior is the same, however.

I looked at fuse's source code and the error code 1 can be returned at multiple points throughout the execution. I will pinpoint where exactly is it failing on the fuse side and report back here.

Now for hello.c: running it as regular user, starting with the same permissions on /some/mount/point and passing it the arguments -f and /some/mount/point, the program doesn't print any output at first but keeps running. When running stat on the mountpoint, the program prints

getattr was called

like it should. stat returns an error, but that's simply because hello.c's getattr function doesn't give it any information, so no problems there. After executing fusermount -u /some/mount/point as regular user, the program exits with return code 0 and the unmount is successful.

Running it as root, starting with the same permissions on /some/mount/point and passing it the arguments -f and /some/mount/point, the program doesn't print any output at first but keeps running. When running stat on the mountpoint, I get a permission error because I am not root. When running stat on it as root, the program prints

getattr was called

like it should. Executing fusermount -u /some/mount/point as regular user yields

fusermount: entry for /some/mount/point not found in /etc/mtab

Executing fusermount as root, the program exits with return code 0 and the unmount is successful.

Witte answered 15/1, 2012 at 19:53 Comment(9)
Don't know but it is interesting to see the jar emits permission denied error. If you use linux command su before the java command, does it work?Button
Have you set the system to permit execution for your jar file using linux chmod command?Button
Using chmod +x on jna.jar does nothing (why would it? Java just needs to read it). However, running the program as root didn't cause the error to appear. Alas, it didn't seem to do anything: It just said "Mounting" and "Mounted" instantly. So it does go up to completion, but the fuse_main_real call returns instantly. The number it returns is 1. That is some progress, but the program needs to be runnable as a regular user like hello.c can.Witte
Actually, I asked such questions to confirm my suspicion. It looks like Java application is using its default security permission model when it comes to file system calls that fuse uses where under Linux, only root/system user account can interact with file system calls to modify the file system. Non-root/system users will get permission denied or a dialog box saying the same. In Windows Vista/7, it will trigger UAC if you are not in Administrators group.Button
It seems that your Java JNA program only works when it runs in superuser mode, albeit the return error of 1. So, you have the chance to improve/correct your JNA code to solve the error while in superuser mode.Button
Is the state of the mount point the same before each invocation? Do you need to un-mount prior to mounting?Condom
@ee: That seems kind of backwards. Java isn't calling any filesystem manipulation function here, it is simply calling functions in libfuse.so. It cannot "know" that the calls it is making affect the filesystem. As for when running in superuser mode, well, I can't get that to work either. Both the C and the Java program look like they do exactly the same thing to me.Witte
@technomage: I do need to unmount the folder once the program runs, even as non-root. So it is definitely doing something with libfuse. However, it only accepts to unmount it when I run fusermount -u as root; trying to unmount it as regular user complains that it is not in /etc/mtab. When in that "program has exited but filesystem is not unmounted" state, a stat command returns on the mounpoint the error "Transport endpoint not connected", which is expected (it cannot contact the program that created this filesystem). This behavior occurs both when running as root and non-root.Witte
I have posted a possible answer based on unclear expectation from you, but it will be wiser if you have updated in your post the proper sample output called from both the C and the Java program when running in both superuser and non-superuser mode before this discussion takes placeButton
W
7

Found it. While the error was really silly in retrospect, it wasn't easy to spot.

The solution: Fuse's fuse_main_real method's first argument is an argument list. In this list, it expects argument 0 to be the filesystem name, or some meaningful program name. Thus, instead of

final String[] actualArgs = { "-f", "/some/mount/point" };

It should have been

final String[] actualArgs = { "programName", "-f", "/some/mount/point" };

This also means that you can't use the argument list that Java gives you in your main method, since that doesn't include the program name either.

Why it matters: fuse actually does its own argument parsing and calls /bin/mount passing it the following arguments:

--no-canonicalize -i -f -t fuse.(arg 0) -o (options) (mountpoints) ...

As such, if you give if -f /some/mount/point as argument list, fuse will try to run:

/bin/mount --no-canonicalize -i -f -t fuse.-f -o rw,nosuid,nodev /some/mount/point

And mount doesn't like "fuse.-f" and will complain.

How it was found: Adding a bunch of printf() inside fuse's source code to figure out where exactly things were failing: in /lib/mount_util.c at line 82:

execl("/bin/mount", "/bin/mount", "--no-canonicalize", "-i",
      "-f", "-t", type, "-o", opts, fsname, mnt, NULL);

I apologise for assuming the error was due to it being Java-related or JNA-related or permissions-related. I will edit the question title and tags to reflect this. (In my defense, the error fuse was returning ("Permission denied") certainly wasn't helpful!)

Thank you for your assistance ee. and technomage, and again I apologise for taking away a chunk of your time because of what turned out to be a silly mistake.

Witte answered 15/1, 2012 at 19:53 Comment(1)
No problem, though... sharing is caring :)Button
B
2

Regarding the permission denied issue when running the jar...I am sure it is Java security permission thing is going on here to explain why no exception is caught when running in superuser mode but permission denied exception is caught when running in non-superuser mode.

From what I can understand, Java has a layer of security unlike the standard C program (except for some C libraries that may include security checks, just like .NET managed C++ libraries). Even though the file manipulation functions are coming from libfuse.so, it may also call Linux system kernel calls that may be executed within system kernel memory space. Since it is now running via Java where Java has to load/map all library functions including system calls into memory. If Java finds out the memory map occurs in system kernel memory space rather than user memory space during execution, it will refer its security manager to check against the current user state of the Java program.

Otherwise, the permission denied error may actually come from fuse trying to access a mount point that is restricted from the normal user which is an expected behavior. Then, this has nothing to do with Java. But, this error shall also occur in C program as well. But, from your post and comments, it doesn't tell that much.

However, running the program as root didn't cause the error to appear. Alas, it didn't seem to do anything: It just said "Mounting" and "Mounted" instantly. So it does go up to completion, but the fuse_main_real call returns instantly. The number it returns is 1. That is some progress, but the program needs to be runnable as a regular user like hello.c can.

On the other hand, based on your recent comment above, it seems that your function pointer (callback) fields in StructFuseOperations structure are not working to "fire up" any fuse event that fuse may invoke.

Note: I assume that the "erroneous" main Java program displays "Mounting" and "Mounted" and nothing else in between them which actually involves a call to fuse_main_real method that doesn't fire up any fuse event but a return code of 1 when running the program in the superuser mode. I haven't tried the code in the post since I don't have access to Linux OS right now.

Update: from this point onwards, the discussion about callback padding in a JNA structure is no longer valid after the recent post update made by OP: https://stackoverflow.com/revisions/e28dc30b-9b71-4d65-8f8a-cfc7a3d5231e/view-source

Based on the given link, fuse_operations Struct Reference, you only focus on a few fields of the C structure as follows:

static struct fuse_operations hello_oper = {
    int (getattr*)(const char *path, struct stat *stbuf);
    /** some 12 skipped callbacks in between **/
    int (open*)(const char *path, struct fuse_file_info *fi);
    int (read*)(const char *path, char *buf, size_t size, off_t offset, struct fuse_file_info *fi)
    /** some 10 skipped callbacks in between **/
    int (readdir*)(const char *path, void *buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info *fi);
    /** some 11 skipped callbacks in between **/
    unsigned int flag_nullpath_ok;
    unsigned int flag_reserved;
    /** some 2 skipped callbacks in between **/
};

However, it seems that you are trying to skip a few callback fields with padding. Therefore, to maintain the order of how the callback fields are laid out in fuse_operations structure, you apply the Pointer type to each callback field that you have skipped. However, by assuming a simple Pointer field for these skipped structure fields, you have removed the vital information about the callback for each field: its callback signature.

From JNA API Overview:

Callbacks (Function Pointers)

JNA supports supplying Java callbacks to native code. You must define an interface that extends the Callback interface, and define a single callback method with a signature that matches the function pointer required by the native code. The name of the method may be something other than "callback" only if there is only a single method in the interface which extends Callback or the class which implements Callback. The arguments and return value follow the same rules as for a direct function invocation.

If the callback returns a String or String[], the returned memory will be valid until the returned object is GC'd.

Following is what is suggested in the overview:

// Original C code
struct _functions {
  int (*open)(const char*,int);
  int (*close)(int);
};

// Equivalent JNA mapping
public class Functions extends Structure {
  public static interface OpenFunc extends Callback {
    int invoke(String name, int options);
  }
  public static interface CloseFunc extends Callback {
    int invoke(int fd);
  }
  public OpenFunc open;
  public CloseFunc close;
}
...
Functions funcs = new Functions();
lib.init(funcs);
int fd = funcs.open.invoke("myfile", 0);
funcs.close.invoke(fd);

However, it doesn't suggest a way to properly skip the callbacks with padding technique in a structure especially when it is too large and you don't want to define every callback that you are not interested with. Maybe, it is not warranted and may cause undefined behavior like what you are facing...

Probably, instead of Pointer for each callback field that you want to pad, you can use Callback field, maintain its field name as in the specification. You may or may not initialize it with the null value (I haven't tried this; possibly it may not work).

Update:

It seems that my suggestion above can work based on the unrelated JNA solution by tgdavies in C callback with JNA makes JRE crash where he padded those callback fields he wasn't interested with simple Callback type but the matching callback field names remained intact in the sp_session_callbacks Structure.

I guess, because of the improper fuse_operations structure, fuse_main_real is unable to fire up the expected fuse event that you are interested with.

Button answered 15/1, 2012 at 19:53 Comment(5)
Actually I had a full structure without any padding or null Callbacks in the beginning and still had that problem. I had trimmed them down to padding Pointers to make the code more digestible on stackoverflow. I have edited the question to have the full structure with all callbacks. Some of them are null (the ones that are not defined in hello.c), but the behavior when they are all defined is the same (nothing from those callbacks ever gets printed). Also added output and permissions and all that.Witte
@EtiennePerot Thank you for adding more information (proper output samples)Button
Using Pointer or Callback for fields is not relevant. What is relevant, is that all fields must be defined in the Java object, and their alignments, offsets, and overall structure size must match the native code. The only time the callback signature must match is when you are going to assign it with a Java object or intend to call a native-specified callback from Java.Condom
@EtiennePerot Points to try: (1) First, disable all the callbacks by passing null to StructFuseOperations argument in fuse_main_real() so that we can tell whether fuse is working or not (still return error code 1?); (2) Then, try to activate the first callback in the structure, getatrr. If it works, then we can activate the rest of the callbacks one-by-one. (3) For parameters that have struct * as argument, I will prefer if you can complete their actual Structure declaration rather than just a Pointer.Button
Using null as argument gave exactly the same result as described in the first question. It must be something more fundamental than that... I'll keep looking.Witte

© 2022 - 2024 — McMap. All rights reserved.