Repository: juicedata/juicefs
Pull request #2346 (2022-07-10 04:20:22)

fuse,meta: support GETFLAGS/SETFLAGS ioctl

qingyunha: close #775

Hi, I have a init implement for support ioctl. It's not complete and I hope your help, see if this is the right direction
codecov-commenter: # [Codecov](https://codecov.io/gh/juicedata/juicefs/pull/2346?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=juicedata) Report
> Merging [#2346](https://codecov.io/gh/juicedata/juicefs/pull/2346?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=juicedata) (0c29c63) into [main](https://codecov.io/gh/juicedata/juicefs/commit/8103c5557d38f71de5a451d4c8633d42b4641f2a?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=juicedata) (8103c55) will **decrease** coverage by `0.01%`.
> The diff coverage is `45.45%`.

```diff
@@            Coverage Diff             @@
##             main    #2346      +/-   ##
==========================================
- Coverage   61.66%   61.64%   -0.02%     
==========================================
  Files         137      137              
  Lines       25439    25490      +51     
==========================================
+ Hits        15687    15714      +27     
- Misses       7924     7941      +17     
- Partials     1828     1835       +7     
```


| [Impacted Files](https://codecov.io/gh/juicedata/juicefs/pull/2346?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=juicedata) | Coverage Δ | |
|---|---|---|
| [pkg/fuse/fuse.go](https://codecov.io/gh/juicedata/juicefs/pull/2346/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=juicedata#diff-cGtnL2Z1c2UvZnVzZS5nbw==) | `78.26% <0.00%> (-1.24%)` | :arrow_down: |
| [pkg/meta/interface.go](https://codecov.io/gh/juicedata/juicefs/pull/2346/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=juicedata#diff-cGtnL21ldGEvaW50ZXJmYWNlLmdv) | `52.98% <ø> (ø)` | |
| [pkg/vfs/vfs\_unix.go](https://codecov.io/gh/juicedata/juicefs/pull/2346/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=juicedata#diff-cGtnL3Zmcy92ZnNfdW5peC5nbw==) | `91.79% <0.00%> (-6.02%)` | :arrow_down: |
| [pkg/meta/base.go](https://codecov.io/gh/juicedata/juicefs/pull/2346/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=juicedata#diff-cGtnL21ldGEvYmFzZS5nbw==) | `70.78% <52.00%> (-0.67%)` | :arrow_down: |
| [pkg/utils/buffer.go](https://codecov.io/gh/juicedata/juicefs/pull/2346/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=juicedata#diff-cGtnL3V0aWxzL2J1ZmZlci5nbw==) | `94.20% <66.66%> (ø)` | |
| [pkg/meta/redis.go](https://codecov.io/gh/juicedata/juicefs/pull/2346/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=juicedata#diff-cGtnL21ldGEvcmVkaXMuZ28=) | `71.90% <100.00%> (+0.07%)` | :arrow_up: |
| [pkg/meta/sql.go](https://codecov.io/gh/juicedata/juicefs/pull/2346/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=juicedata#diff-cGtnL21ldGEvc3FsLmdv) | `65.62% <100.00%> (-0.05%)` | :arrow_down: |
| [pkg/meta/tkv.go](https://codecov.io/gh/juicedata/juicefs/pull/2346/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=juicedata#diff-cGtnL21ldGEvdGt2Lmdv) | `76.04% <100.00%> (-0.07%)` | :arrow_down: |
| [pkg/vfs/writer.go](https://codecov.io/gh/juicedata/juicefs/pull/2346/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=juicedata#diff-cGtnL3Zmcy93cml0ZXIuZ28=) | `78.64% <0.00%> (-1.02%)` | :arrow_down: |
| [pkg/fs/fs.go](https://codecov.io/gh/juicedata/juicefs/pull/2346/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=juicedata#diff-cGtnL2ZzL2ZzLmdv) | `77.46% <0.00%> (-0.77%)` | :arrow_down: |
| ... and [7 more](https://codecov.io/gh/juicedata/juicefs/pull/2346/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=juicedata) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/juicedata/juicefs/pull/2346?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=juicedata).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=juicedata)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/juicedata/juicefs/pull/2346?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=juicedata). Last update [8103c55...0c29c63](https://codecov.io/gh/juicedata/juicefs/pull/2346?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=juicedata). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=juicedata).

davies: @qingyunha This looks pretty good, some details could be tuned.
zhijian-pro: `doRename` should check flags
```
if (sattr.Flags&FlagAppend) != 0 || (sattr.Flags&FlagImmutable) != 0 || (dattr.Flags&FlagImmutable) != 0 || (iattr.Flags&FlagAppend) != 0 || (iattr.Flags&FlagImmutable) != 0 {
	return syscall.EPERM
}
```
SandyXSD: @qingyunha Do you have time to continue this PR? If not, we can take is over.
qingyunha: Sorry for the delay. I still want to try to implement this feature, perhaps this weekend.
SandyXSD: Sure
qingyunha: Updated.
SandyXSD: @qingyunha Please rebase the PR and resolve the conflicts.
SandyXSD: Failed test:
```
 base_test.go:1570: fallocate f : operation not permitted
```
qingyunha: > Failed test:
> 
> ```
>  base_test.go:1570: fallocate f : operation not permitted
> ```

Done.

Patch:
diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml
new file mode 100644
index 00000000..5b3f69d2
--- /dev/null
+++ b/.github/workflows/dependency-review.yml
@@ -0,0 +1,22 @@
+# Dependency Review Action
+#
+# This Action will scan dependency manifest files that change as part of a Pull Request, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging.
+#
+# Source repository: https://github.com/actions/dependency-review-action
+# Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement
+name: 'Dependency Review'
+on: [pull_request]
+
+permissions:
+  contents: read
+
+jobs:
+  dependency-review:
+    runs-on: ubuntu-latest
+    steps:
+      - name: 'Checkout Repository'
+        uses: actions/checkout@v3
+      - name: 'Dependency Review'
+        uses: actions/dependency-review-action@v2
+#        with:
+#          fail-on-severity: high
diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml
new file mode 100644
index 00000000..574fc808
--- /dev/null
+++ b/.github/workflows/scorecards.yml
@@ -0,0 +1,75 @@
+name: Scorecards security
+on:
+  # Only the default branch is supported.
+  branch_protection_rule:
+  schedule:
+    - cron: '30 19 * * 0'
+  push:
+    branches:
+      - "main"
+      - 'release-**'
+    paths-ignore:
+      - 'docs/**'
+      - '**.md'
+  pull_request:
+    branches:
+      - "main"
+      - 'release-**'
+    paths-ignore:
+      - 'docs/**'
+      - '**.md'
+
+
+# Declare default permissions as read only.
+permissions: read-all
+
+jobs:
+  analysis:
+    name: Scorecards analysis
+    runs-on: ubuntu-latest
+    permissions:
+      # Needed to upload the results to code-scanning dashboard.
+      security-events: write
+      # Used to receive a badge. (Upcoming feature)
+      id-token: write
+      # Needs for private repositories.
+      contents: read
+      actions: read
+    
+    steps:
+      - name: "Checkout code"
+        uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # v3.0.0
+        with:
+          persist-credentials: false
+
+      - name: "Run analysis"
+        uses: ossf/scorecard-action@3e15ea8318eee9b333819ec77a36aca8d39df13e # v1.1.1
+        with:
+          results_file: results.sarif
+          results_format: sarif
+          # (Optional) Read-only PAT token. Uncomment the `repo_token` line below if:
+          # - you want to enable the Branch-Protection check on a *public* repository, or
+          # - you are installing Scorecards on a *private* repository
+          # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat.
+          # repo_token: ${{ secrets.SCORECARD_READ_TOKEN }}
+
+          # Publish the results for public repositories to enable scorecard badges. For more details, see
+          # https://github.com/ossf/scorecard-action#publishing-results. 
+          # For private repositories, `publish_results` will automatically be set to `false`, regardless 
+          # of the value entered here.
+          publish_results: true
+
+      # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
+      # format to the repository Actions tab.
+      - name: "Upload artifact"
+        uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535 # v3.0.0
+        with:
+          name: SARIF file
+          path: results.sarif
+          retention-days: 5
+      
+      # Upload the results to GitHub's code scanning dashboard.
+      - name: "Upload to code-scanning"
+        uses: github/codeql-action/upload-sarif@5f532563584d71fdef14ee64d17bafb34f751ce5 # v1.0.26
+        with:
+          sarif_file: results.sarif
diff --git a/cmd/main.go b/cmd/main.go
index 4ca00a2e..4a5c1904 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -79,7 +79,7 @@ func Main(args []string) error {
 	if calledViaMount(args) {
 		args = handleSysMountArgs(args)
 		if len(args) < 1 {
-			args = []string{"--help"}
+			args = []string{"mount", "--help"}
 		}
 	}
 	return app.Run(reorderOptions(app, args))
@@ -96,7 +96,7 @@ func handleSysMountArgs(args []string) []string {
 		"direntrycacheto": "dir-entry-cache",
 	}
 	newArgs := []string{"juicefs", "mount", "-d"}
-	if len(args) < 4 {
+	if len(args) < 3 {
 		return nil
 	}
 	mountOptions := args[3:]
diff --git a/go.mod b/go.mod
index f3c36f65..9750ee86 100644
--- a/go.mod
+++ b/go.mod
@@ -233,7 +233,7 @@ require (
 
 replace github.com/minio/minio v0.0.0-20210206053228-97fe57bba92c => github.com/juicedata/minio v0.0.0-20220613143934-cee0571a1b03
 
-replace github.com/hanwen/go-fuse/v2 v2.1.1-0.20210611132105-24a1dfe6b4f8 => github.com/juicedata/go-fuse/v2 v2.1.1-0.20220720062817-183cb227b353
+replace github.com/hanwen/go-fuse/v2 v2.1.1-0.20210611132105-24a1dfe6b4f8 => github.com/juicedata/go-fuse/v2 v2.1.1-0.20220822092405-ece777deb343
 
 replace github.com/dgrijalva/jwt-go v3.2.0+incompatible => github.com/golang-jwt/jwt v3.2.1+incompatible
 
diff --git a/go.sum b/go.sum
index 6180d4c8..aa49fa07 100644
--- a/go.sum
+++ b/go.sum
@@ -598,8 +598,8 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1
 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
 github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
-github.com/juicedata/go-fuse/v2 v2.1.1-0.20220720062817-183cb227b353 h1:cIwY+HZQiRQAYoCItVNCkpFDyPahgZzJeNM3rMgZSOE=
-github.com/juicedata/go-fuse/v2 v2.1.1-0.20220720062817-183cb227b353/go.mod h1:oRyA5eK+pvJyv5otpO/DgccS8y/RvYMaO00GgRLGryc=
+github.com/juicedata/go-fuse/v2 v2.1.1-0.20220822092405-ece777deb343 h1:fbMz7gSlX5eYwuWNB3Z1+dK2q2Ekg5tw6uSYYTOdZrw=
+github.com/juicedata/go-fuse/v2 v2.1.1-0.20220822092405-ece777deb343/go.mod h1:oRyA5eK+pvJyv5otpO/DgccS8y/RvYMaO00GgRLGryc=
 github.com/juicedata/godaemon v0.0.0-20210629045518-3da5144a127d h1:kpQMvNZJKGY3PTt7OSoahYc4nM0HY67SvK0YyS0GLwA=
 github.com/juicedata/godaemon v0.0.0-20210629045518-3da5144a127d/go.mod h1:dlxKkLh3qAIPtgr2U/RVzsZJDuXA1ffg+Njikfmhvgw=
 github.com/juicedata/minio v0.0.0-20220613143934-cee0571a1b03 h1:+0jiEkr6qE5Zm2+LilAv5pV/+V+V1XZ5KUv0pmXZHhQ=
diff --git a/pkg/fuse/fuse.go b/pkg/fuse/fuse.go
index b3ce3fc6..2f0336cc 100644
--- a/pkg/fuse/fuse.go
+++ b/pkg/fuse/fuse.go
@@ -421,6 +421,13 @@ func (fs *fileSystem) StatFs(cancel <-chan struct{}, in *fuse.InHeader, out *fus
 	return 0
 }
 
+func (fs *fileSystem) Ioctl(cancel <-chan struct{}, in *fuse.IoctlIn, out *fuse.IoctlOut, bufIn, bufOut []byte) (status fuse.Status) {
+	ctx := newContext(cancel, &in.InHeader)
+	defer releaseContext(ctx)
+	out.Result = int32(fs.v.Ioctl(ctx, Ino(in.NodeId), in.Cmd, in.Arg, bufIn, bufOut))
+	return 0
+}
+
 // Serve starts a server to serve requests from FUSE.
 func Serve(v *vfs.VFS, options string, xattrs bool) error {
 	if err := syscall.Setpriority(syscall.PRIO_PROCESS, os.Getpid(), -19); err != nil {
diff --git a/pkg/meta/base.go b/pkg/meta/base.go
index 85439cc5..5d89d835 100644
--- a/pkg/meta/base.go
+++ b/pkg/meta/base.go
@@ -834,6 +834,19 @@ func (m *baseMeta) Open(ctx Context, inode Ino, flags uint32, attr *Attr) syscal
 	if attr != nil && !attr.Full {
 		err = m.GetAttr(ctx, inode, attr)
 	}
+	if attr.Flags&FlagImmutable != 0 {
+		if flags&(syscall.O_WRONLY|syscall.O_RDWR) != 0 {
+			return syscall.EPERM
+		}
+	}
+	if attr.Flags&FlagAppend != 0 {
+		if (flags&(syscall.O_WRONLY|syscall.O_RDWR)) != 0 && (flags&syscall.O_APPEND) == 0 {
+			return syscall.EPERM
+		}
+		if flags&syscall.O_TRUNC != 0 {
+			return syscall.EPERM
+		}
+	}
 	if err == 0 {
 		m.of.Open(inode, attr)
 	}
diff --git a/pkg/meta/interface.go b/pkg/meta/interface.go
index 3cbea3a3..e67e17b1 100644
--- a/pkg/meta/interface.go
+++ b/pkg/meta/interface.go
@@ -75,6 +75,12 @@ const (
 	SetAttrCtime
 	SetAttrAtimeNow
 	SetAttrMtimeNow
+	SetAttrFlag = 1 << 15
+)
+
+const (
+	FlagImmutable = 1 << iota
+	FlagAppend
 )
 
 const MaxName = 255
@@ -99,7 +105,7 @@ type MsgCallback func(...interface{}) error
 
 // Attr represents attributes of a node.
 type Attr struct {
-	Flags     uint8  // reserved flags
+	Flags     uint8  // flags
 	Typ       uint8  // type of a node
 	Mode      uint16 // permission mode
 	Uid       uint32 // owner id
diff --git a/pkg/meta/redis.go b/pkg/meta/redis.go
index 99114455..8aa1640a 100644
--- a/pkg/meta/redis.go
+++ b/pkg/meta/redis.go
@@ -930,6 +930,12 @@ func (m *redisMeta) Fallocate(ctx Context, inode Ino, mode uint8, off uint64, si
 		if t.Typ != TypeFile {
 			return syscall.EPERM
 		}
+		if (t.Flags & FlagImmutable) != 0 {
+			return syscall.EPERM
+		}
+		if (t.Flags&FlagAppend) != 0 && (mode&^fallocKeepSize) != 0 {
+			return syscall.EPERM
+		}
 		length := t.Length
 		if off+size > t.Length {
 			if mode&fallocKeepSize == 0 {
@@ -1037,6 +1043,10 @@ func (m *redisMeta) SetAttr(ctx Context, inode Ino, set uint16, sugidclearmode u
 			cur.Mtimensec = uint32(now.Nanosecond())
 			changed = true
 		}
+		if set&SetAttrFlag != 0 {
+			cur.Flags = attr.Flags
+			changed = true
+		}
 		if !changed {
 			*attr = cur
 			return nil
@@ -1106,6 +1116,9 @@ func (m *redisMeta) doMknod(ctx Context, parent Ino, name string, _type uint8, m
 		if pattr.Typ != TypeDirectory {
 			return syscall.ENOTDIR
 		}
+		if (pattr.Flags & FlagImmutable) != 0 {
+			return syscall.EPERM
+		}
 
 		buf, err := tx.HGet(ctx, m.entryKey(parent), name).Bytes()
 		if err != nil && err != redis.Nil {
@@ -1229,6 +1242,9 @@ func (m *redisMeta) doUnlink(ctx Context, parent Ino, name string) syscall.Errno
 		if pattr.Typ != TypeDirectory {
 			return syscall.ENOTDIR
 		}
+		if (pattr.Flags&FlagAppend) != 0 || (pattr.Flags&FlagImmutable) != 0 {
+			return syscall.EPERM
+		}
 		var updateParent bool
 		now := time.Now()
 		if !isTrash(parent) && now.Sub(time.Unix(pattr.Mtime, int64(pattr.Mtimensec))) >= minUpdateTime {
@@ -1243,6 +1259,9 @@ func (m *redisMeta) doUnlink(ctx Context, parent Ino, name string) syscall.Errno
 			if ctx.Uid() != 0 && pattr.Mode&01000 != 0 && ctx.Uid() != pattr.Uid && ctx.Uid() != attr.Uid {
 				return syscall.EACCES
 			}
+			if (attr.Flags&FlagAppend) != 0 || (attr.Flags&FlagImmutable) != 0 {
+				return syscall.EPERM
+			}
 			attr.Ctime = now.Unix()
 			attr.Ctimensec = uint32(now.Nanosecond())
 			if trash == 0 {
@@ -1349,6 +1368,9 @@ func (m *redisMeta) doRmdir(ctx Context, parent Ino, name string) syscall.Errno
 		if pattr.Typ != TypeDirectory {
 			return syscall.ENOTDIR
 		}
+		if (pattr.Flags&FlagAppend) != 0 || (pattr.Flags&FlagImmutable) != 0 {
+			return syscall.EPERM
+		}
 		now := time.Now()
 		pattr.Nlink--
 		pattr.Mtime = now.Unix()
@@ -1482,6 +1504,9 @@ func (m *redisMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentD
 			return syscall.ENOTDIR
 		}
 		m.parseAttr([]byte(rs[2].(string)), &iattr)
+		if (sattr.Flags&FlagAppend) != 0 || (sattr.Flags&FlagImmutable) != 0 || (dattr.Flags&FlagImmutable) != 0 || (iattr.Flags&FlagAppend) != 0 || (iattr.Flags&FlagImmutable) != 0 {
+			return syscall.EPERM
+		}
 
 		var supdate, dupdate bool
 		now := time.Now()
@@ -1491,6 +1516,9 @@ func (m *redisMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentD
 				trash = 0
 			}
 			m.parseAttr([]byte(rs[3].(string)), &tattr)
+			if (tattr.Flags&FlagAppend) != 0 || (tattr.Flags&FlagImmutable) != 0 {
+				return syscall.EPERM
+			}
 			tattr.Ctime = now.Unix()
 			tattr.Ctimensec = uint32(now.Nanosecond())
 			if exchange {
@@ -1666,6 +1694,9 @@ func (m *redisMeta) doLink(ctx Context, inode, parent Ino, name string, attr *At
 		if pattr.Typ != TypeDirectory {
 			return syscall.ENOTDIR
 		}
+		if pattr.Flags&FlagImmutable != 0 {
+			return syscall.EPERM
+		}
 		var updateParent bool
 		now := time.Now()
 		if now.Sub(time.Unix(pattr.Mtime, int64(pattr.Mtimensec))) >= minUpdateTime {
@@ -1679,6 +1710,9 @@ func (m *redisMeta) doLink(ctx Context, inode, parent Ino, name string, attr *At
 		if iattr.Typ == TypeDirectory {
 			return syscall.EPERM
 		}
+		if (iattr.Flags&FlagAppend) != 0 || (iattr.Flags&FlagImmutable) != 0 {
+			return syscall.EPERM
+		}
 		oldParent := iattr.Parent
 		iattr.Parent = 0
 		iattr.Ctime = now.Unix()
@@ -2053,6 +2087,9 @@ func (m *redisMeta) CopyFileRange(ctx Context, fin Ino, offIn uint64, fout Ino,
 		if attr.Typ != TypeFile {
 			return syscall.EINVAL
 		}
+		if (attr.Flags&FlagImmutable) != 0 || (attr.Flags&FlagAppend) != 0 {
+			return syscall.EPERM
+		}
 
 		newleng := offOut + size
 		if newleng > attr.Length {
diff --git a/pkg/meta/sql.go b/pkg/meta/sql.go
index bd25d6b4..a41f28ae 100644
--- a/pkg/meta/sql.go
+++ b/pkg/meta/sql.go
@@ -867,12 +867,16 @@ func (m *dbMeta) SetAttr(ctx Context, inode Ino, set uint16, sugidclearmode uint
 			cur.Mtime = now
 			changed = true
 		}
+		if set&SetAttrFlag != 0 {
+			cur.Flags = attr.Flags
+			changed = true
+		}
 		m.parseAttr(&cur, attr)
 		if !changed {
 			return nil
 		}
 		cur.Ctime = now
-		_, err = s.Cols("mode", "uid", "gid", "atime", "mtime", "ctime").Update(&cur, &node{Inode: inode})
+		_, err = s.Cols("flags", "mode", "uid", "gid", "atime", "mtime", "ctime").Update(&cur, &node{Inode: inode})
 		if err == nil {
 			m.parseAttr(&cur, attr)
 		}
@@ -1013,6 +1017,12 @@ func (m *dbMeta) Fallocate(ctx Context, inode Ino, mode uint8, off uint64, size
 		if n.Type != TypeFile {
 			return syscall.EPERM
 		}
+		if (n.Flags & FlagImmutable) != 0 {
+			return syscall.EPERM
+		}
+		if (n.Flags&FlagAppend) != 0 && (mode&^fallocKeepSize) != 0 {
+			return syscall.EPERM
+		}
 		length := n.Length
 		if off+size > n.Length {
 			if mode&fallocKeepSize == 0 {
@@ -1120,6 +1130,9 @@ func (m *dbMeta) doMknod(ctx Context, parent Ino, name string, _type uint8, mode
 		if pn.Type != TypeDirectory {
 			return syscall.ENOTDIR
 		}
+		if (pn.Flags & FlagImmutable) != 0 {
+			return syscall.EPERM
+		}
 		var e = edge{Parent: parent, Name: []byte(name)}
 		ok, err = s.ForUpdate().Get(&e)
 		if err != nil {
@@ -1219,6 +1232,9 @@ func (m *dbMeta) doUnlink(ctx Context, parent Ino, name string) syscall.Errno {
 		if pn.Type != TypeDirectory {
 			return syscall.ENOTDIR
 		}
+		if (pn.Flags&FlagAppend) != 0 || (pn.Flags&FlagImmutable) != 0 {
+			return syscall.EPERM
+		}
 		var e = edge{Parent: parent, Name: []byte(name)}
 		ok, err = s.ForUpdate().Get(&e)
 		if err != nil {
@@ -1249,6 +1265,9 @@ func (m *dbMeta) doUnlink(ctx Context, parent Ino, name string) syscall.Errno {
 			if ctx.Uid() != 0 && pn.Mode&01000 != 0 && ctx.Uid() != pn.Uid && ctx.Uid() != n.Uid {
 				return syscall.EACCES
 			}
+			if (n.Flags&FlagAppend) != 0 || (n.Flags&FlagImmutable) != 0 {
+				return syscall.EPERM
+			}
 			n.Ctime = now
 			if trash == 0 {
 				n.Nlink--
@@ -1350,6 +1369,9 @@ func (m *dbMeta) doRmdir(ctx Context, parent Ino, name string) syscall.Errno {
 		if pn.Type != TypeDirectory {
 			return syscall.ENOTDIR
 		}
+		if pn.Flags&FlagImmutable != 0 || pn.Flags&FlagAppend != 0 {
+			return syscall.EPERM
+		}
 		var e = edge{Parent: parent, Name: []byte(name)}
 		ok, err = s.ForUpdate().Get(&e)
 		if err != nil {
@@ -1465,6 +1487,9 @@ func (m *dbMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentDst
 		if spn.Type != TypeDirectory || dpn.Type != TypeDirectory {
 			return syscall.ENOTDIR
 		}
+		if (spn.Flags&FlagAppend) != 0 || (spn.Flags&FlagImmutable) != 0 || (dpn.Flags&FlagImmutable) != 0 {
+			return syscall.EPERM
+		}
 		var se = edge{Parent: parentSrc, Name: []byte(nameSrc)}
 		ok, err := s.ForUpdate().Get(&se)
 		if err != nil {
@@ -1495,6 +1520,9 @@ func (m *dbMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentDst
 		if !ok {
 			return syscall.ENOENT
 		}
+		if (sn.Flags&FlagAppend) != 0 || (sn.Flags&FlagImmutable) != 0 {
+			return syscall.EPERM
+		}
 
 		var de = edge{Parent: parentDst, Name: []byte(nameDst)}
 		ok, err = s.ForUpdate().Get(&de)
@@ -1525,6 +1553,9 @@ func (m *dbMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentDst
 				logger.Warnf("no attribute for inode %d (%d, %s)", dino, parentDst, de.Name)
 				trash = 0
 			}
+			if (dn.Flags&FlagAppend) != 0 || (dn.Flags&FlagImmutable) != 0 {
+				return syscall.EPERM
+			}
 			dn.Ctime = now
 			if exchange {
 				if parentSrc != parentDst {
@@ -1708,6 +1739,9 @@ func (m *dbMeta) doLink(ctx Context, inode, parent Ino, name string, attr *Attr)
 		if pn.Type != TypeDirectory {
 			return syscall.ENOTDIR
 		}
+		if pn.Flags&FlagImmutable != 0 {
+			return syscall.EPERM
+		}
 		var e = edge{Parent: parent, Name: []byte(name)}
 		ok, err = s.ForUpdate().Get(&e)
 		if err != nil {
@@ -1728,6 +1762,9 @@ func (m *dbMeta) doLink(ctx Context, inode, parent Ino, name string, attr *Attr)
 		if n.Type == TypeDirectory {
 			return syscall.EPERM
 		}
+		if (n.Flags&FlagAppend) != 0 || (n.Flags&FlagImmutable) != 0 {
+			return syscall.EPERM
+		}
 
 		var updateParent bool
 		now := time.Now().UnixNano() / 1e3
@@ -2059,6 +2096,9 @@ func (m *dbMeta) CopyFileRange(ctx Context, fin Ino, offIn uint64, fout Ino, off
 		if nout.Type != TypeFile {
 			return syscall.EINVAL
 		}
+		if (nout.Flags&FlagImmutable) != 0 || (nout.Flags&FlagAppend) != 0 {
+			return syscall.EPERM
+		}
 
 		newleng := offOut + size
 		if newleng > nout.Length {
diff --git a/pkg/meta/tkv.go b/pkg/meta/tkv.go
index 25b43ef5..f3827cef 100644
--- a/pkg/meta/tkv.go
+++ b/pkg/meta/tkv.go
@@ -794,6 +794,10 @@ func (m *kvMeta) SetAttr(ctx Context, inode Ino, set uint16, sugidclearmode uint
 			cur.Mtimensec = uint32(now.Nanosecond())
 			changed = true
 		}
+		if set&SetAttrFlag != 0 {
+			cur.Flags = attr.Flags
+			changed = true
+		}
 		if !changed {
 			*attr = cur
 			return nil
@@ -911,6 +915,12 @@ func (m *kvMeta) Fallocate(ctx Context, inode Ino, mode uint8, off uint64, size
 		if t.Typ != TypeFile {
 			return syscall.EPERM
 		}
+		if (t.Flags & FlagImmutable) != 0 {
+			return syscall.EPERM
+		}
+		if (t.Flags&FlagAppend) != 0 && (mode&^fallocKeepSize) != 0 {
+			return syscall.EPERM
+		}
 		length := t.Length
 		if off+size > t.Length {
 			if mode&fallocKeepSize == 0 {
@@ -1007,6 +1017,9 @@ func (m *kvMeta) doMknod(ctx Context, parent Ino, name string, _type uint8, mode
 		if pattr.Typ != TypeDirectory {
 			return syscall.ENOTDIR
 		}
+		if (pattr.Flags & FlagImmutable) != 0 {
+			return syscall.EPERM
+		}
 
 		buf := tx.get(m.entryKey(parent, name))
 		var foundIno Ino
@@ -1114,12 +1127,20 @@ func (m *kvMeta) doUnlink(ctx Context, parent Ino, name string) syscall.Errno {
 		if pattr.Typ != TypeDirectory {
 			return syscall.ENOTDIR
 		}
+		if (pattr.Flags&FlagAppend) != 0 || (pattr.Flags&FlagImmutable) != 0 {
+			return syscall.EPERM
+		}
+		attr = Attr{}
+		opened = false
 		now := time.Now()
 		if rs[1] != nil {
 			m.parseAttr(rs[1], &attr)
 			if ctx.Uid() != 0 && pattr.Mode&01000 != 0 && ctx.Uid() != pattr.Uid && ctx.Uid() != attr.Uid {
 				return syscall.EACCES
 			}
+			if (attr.Flags&FlagAppend) != 0 || (attr.Flags&FlagImmutable) != 0 {
+				return syscall.EPERM
+			}
 			attr.Ctime = now.Unix()
 			attr.Ctimensec = uint32(now.Nanosecond())
 			if trash == 0 {
@@ -1223,6 +1244,9 @@ func (m *kvMeta) doRmdir(ctx Context, parent Ino, name string) syscall.Errno {
 		if pattr.Typ != TypeDirectory {
 			return syscall.ENOTDIR
 		}
+		if (pattr.Flags&FlagAppend) != 0 || (pattr.Flags&FlagImmutable) != 0 {
+			return syscall.EPERM
+		}
 		if tx.exist(m.entryKey(inode, "")) {
 			return syscall.ENOTEMPTY
 		}
@@ -1314,6 +1338,9 @@ func (m *kvMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentDst
 			return syscall.ENOTDIR
 		}
 		m.parseAttr(rs[2], &iattr)
+		if (sattr.Flags&FlagAppend) != 0 || (sattr.Flags&FlagImmutable) != 0 || (dattr.Flags&FlagImmutable) != 0 || (iattr.Flags&FlagAppend) != 0 || (iattr.Flags&FlagImmutable) != 0 {
+			return syscall.EPERM
+		}
 
 		dbuf := tx.get(m.entryKey(parentDst, nameDst))
 		if dbuf == nil && m.conf.CaseInsensi {
@@ -1335,6 +1362,9 @@ func (m *kvMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentDst
 				trash = 0
 			}
 			m.parseAttr(a, &tattr)
+			if (tattr.Flags&FlagAppend) != 0 || (tattr.Flags&FlagImmutable) != 0 {
+				return syscall.EPERM
+			}
 			tattr.Ctime = now.Unix()
 			tattr.Ctimensec = uint32(now.Nanosecond())
 			if exchange {
@@ -1497,10 +1527,16 @@ func (m *kvMeta) doLink(ctx Context, inode, parent Ino, name string, attr *Attr)
 		if pattr.Typ != TypeDirectory {
 			return syscall.ENOTDIR
 		}
+		if pattr.Flags&FlagImmutable != 0 {
+			return syscall.EPERM
+		}
 		m.parseAttr(rs[1], &iattr)
 		if iattr.Typ == TypeDirectory {
 			return syscall.EPERM
 		}
+		if (iattr.Flags&FlagAppend) != 0 || (iattr.Flags&FlagImmutable) != 0 {
+			return syscall.EPERM
+		}
 		buf := tx.get(m.entryKey(parent, name))
 		if buf != nil || buf == nil && m.conf.CaseInsensi && m.resolveCase(ctx, parent, name) != nil {
 			return syscall.EEXIST
@@ -1744,6 +1780,9 @@ func (m *kvMeta) CopyFileRange(ctx Context, fin Ino, offIn uint64, fout Ino, off
 		if attr.Typ != TypeFile {
 			return syscall.EINVAL
 		}
+		if (attr.Flags&FlagImmutable) != 0 || (attr.Flags&FlagAppend) != 0 {
+			return syscall.EPERM
+		}
 
 		newleng := offOut + size
 		if newleng > attr.Length {
diff --git a/pkg/utils/buffer.go b/pkg/utils/buffer.go
index 4cc7e79d..46986ade 100644
--- a/pkg/utils/buffer.go
+++ b/pkg/utils/buffer.go
@@ -145,11 +145,11 @@ func (b *Buffer) Bytes() []byte {
 	return b.buf
 }
 
-var nativeEndian binary.ByteOrder
+var NativeEndian binary.ByteOrder
 
 // NewNativeBuffer utility to create *Buffer of given size with nativeEndian
 func NewNativeBuffer(buf []byte) *Buffer {
-	return &Buffer{nativeEndian, 0, buf}
+	return &Buffer{NativeEndian, 0, buf}
 }
 
 func init() {
@@ -158,9 +158,9 @@ func init() {
 
 	switch buf {
 	case [2]byte{0xCD, 0xAB}:
-		nativeEndian = binary.LittleEndian
+		NativeEndian = binary.LittleEndian
 	case [2]byte{0xAB, 0xCD}:
-		nativeEndian = binary.BigEndian
+		NativeEndian = binary.BigEndian
 	default:
 		panic("Could not determine native endianness.")
 	}
diff --git a/pkg/vfs/vfs_unix.go b/pkg/vfs/vfs_unix.go
index 62d6a6fb..291a90ce 100644
--- a/pkg/vfs/vfs_unix.go
+++ b/pkg/vfs/vfs_unix.go
@@ -26,6 +26,7 @@ import (
 	"syscall"
 
 	"github.com/juicedata/juicefs/pkg/meta"
+	"github.com/juicedata/juicefs/pkg/utils"
 
 	"golang.org/x/sys/unix"
 )
@@ -290,3 +291,64 @@ func (v *VFS) Flock(ctx Context, ino Ino, fh uint64, owner uint64, typ uint32, b
 	}
 	return
 }
+
+func (v *VFS) Ioctl(ctx Context, ino Ino, cmd uint32, arg uint64, bufIn, bufOut []byte) (err syscall.Errno) {
+	const (
+		FS_IOC_GETFLAGS    = 0x80086601
+		FS_IOC_SETFLAGS    = 0x40086602
+		FS_IOC_GETFLAGS_32 = 0x80046601
+		FS_IOC_SETFLAGS_32 = 0x40046602
+	)
+	const (
+		FS_IMMUTABLE_FL = 0x00000010
+		FS_APPEND_FL    = 0x00000020
+	)
+	defer func() { logit(ctx, "ioctl (%d,0x%X,0x%X,%v,%v): %s", ino, cmd, arg, bufIn, bufOut, strerr(err)) }()
+	switch cmd {
+	default:
+		err = syscall.ENOTTY
+		return
+	case FS_IOC_SETFLAGS, FS_IOC_GETFLAGS, FS_IOC_SETFLAGS_32, FS_IOC_GETFLAGS_32:
+	}
+	if IsSpecialNode(ino) {
+		err = syscall.EPERM
+		return
+	}
+	var attr = &Attr{}
+	if cmd>>30 == 1 { // set
+		var iflag uint64
+		if len(bufIn) == 8 {
+			iflag = utils.NativeEndian.Uint64(bufIn)
+		} else if len(bufIn) == 4 {
+			iflag = uint64(utils.NativeEndian.Uint32(bufIn))
+		} else {
+			err = syscall.EINVAL
+			return
+		}
+		if (iflag & FS_IMMUTABLE_FL) != 0 {
+			attr.Flags |= meta.FlagImmutable
+		}
+		if (iflag & FS_APPEND_FL) != 0 {
+			attr.Flags |= meta.FlagAppend
+		}
+		err = v.Meta.SetAttr(ctx, ino, meta.SetAttrFlag, 0, attr)
+	} else {
+		err = v.Meta.GetAttr(ctx, ino, attr)
+		var iflag uint64
+		if (attr.Flags & meta.FlagImmutable) != 0 {
+			iflag |= FS_IMMUTABLE_FL
+		}
+		if (attr.Flags & meta.FlagAppend) != 0 {
+			iflag |= FS_APPEND_FL
+		}
+		if len(bufOut) == 8 {
+			utils.NativeEndian.PutUint64(bufOut, iflag)
+		} else if len(bufOut) == 4 {
+			utils.NativeEndian.PutUint32(bufOut, uint32(iflag))
+		} else {
+			err = syscall.EINVAL
+			return
+		}
+	}
+	return
+}
diff --git a/sdk/java/src/main/java/io/juicefs/JuiceFileSystemImpl.java b/sdk/java/src/main/java/io/juicefs/JuiceFileSystemImpl.java
index 8fa3e5a1..d66487c6 100644
--- a/sdk/java/src/main/java/io/juicefs/JuiceFileSystemImpl.java
+++ b/sdk/java/src/main/java/io/juicefs/JuiceFileSystemImpl.java
@@ -24,6 +24,8 @@ import jnr.ffi.LibraryLoader;
 import jnr.ffi.Memory;
 import jnr.ffi.Pointer;
 import jnr.ffi.Runtime;
+import jnr.ffi.annotations.In;
+import jnr.ffi.annotations.Out;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 import org.apache.hadoop.HadoopIllegalArgumentException;
@@ -39,11 +41,9 @@ import org.apache.hadoop.io.MD5Hash;
 import org.apache.hadoop.security.AccessControlException;
 import org.apache.hadoop.security.UserGroupInformation;
 import org.apache.hadoop.util.DataChecksum;
-import org.apache.hadoop.util.DirectBufferPool;
 import org.apache.hadoop.util.Progressable;
 import org.apache.hadoop.util.VersionInfo;
 import org.json.JSONObject;
-import sun.nio.ch.DirectBuffer;
 
 import java.io.*;
 import java.lang.reflect.Constructor;
@@ -89,7 +89,6 @@ public class JuiceFileSystemImpl extends FileSystem {
   private ScheduledExecutorService nodesFetcherThread;
   private ScheduledExecutorService refreshUidThread;
   private Map<String, FileStatus> lastFileStatus = new HashMap<>();
-  private static final DirectBufferPool bufferPool = new DirectBufferPool();
   private boolean metricsEnable = false;
 
   /*
@@ -115,9 +114,9 @@ public class JuiceFileSystemImpl extends FileSystem {
 
     long jfs_lseek(long pid, int fd, long pos, int whence);
 
-    int jfs_pread(long pid, int fd, Pointer b, int len, long offset);
+    int jfs_pread(long pid, int fd, @Out ByteBuffer b, int len, long offset);
 
-    int jfs_write(long pid, int fd, Pointer b, int len);
+    int jfs_write(long pid, int fd, @In byte[] b, int len);
 
     int jfs_flush(long pid, int fd);
 
@@ -733,7 +732,7 @@ public class JuiceFileSystemImpl extends FileSystem {
     public FileInputStream(Path f, int fd, int size) throws IOException {
       path = f;
       this.fd = fd;
-      buf = bufferPool.getBuffer(size);
+      buf = ByteBuffer.allocate(size);
       buf.limit(0);
       position = 0;
     }
@@ -780,34 +779,7 @@ public class JuiceFileSystemImpl extends FileSystem {
     public synchronized int read(byte[] b, int off, int len) throws IOException {
       if (off < 0 || len < 0 || b.length - off < len)
         throw new IndexOutOfBoundsException();
-      if (len == 0)
-        return 0;
-      if (buf == null)
-        throw new IOException("stream was closed");
-      if (!buf.hasRemaining() && len <= buf.capacity() && !refill())
-        return -1; // No bytes were read before EOF.
-
-      int read = Math.min(buf.remaining(), len);
-      if (read > 0) {
-        buf.get(b, off, read);
-        statistics.incrementBytesRead(read);
-        off += read;
-        len -= read;
-      }
-      if (len == 0)
-        return read;
-      int more = read(position, b, off, len);
-      if (more <= 0) {
-        if (read > 0) {
-          return read;
-        } else {
-          return -1;
-        }
-      }
-      position += more;
-      buf.position(0);
-      buf.limit(0);
-      return read + more;
+      return read(ByteBuffer.wrap(b, off, len));
     }
 
     private boolean refill() throws IOException {
@@ -817,7 +789,6 @@ public class JuiceFileSystemImpl extends FileSystem {
         buf.limit(0);
         return false; // EOF
       }
-      statistics.incrementBytesRead(-read);
       buf.position(0);
       buf.limit(read);
       position += read;
@@ -826,27 +797,10 @@ public class JuiceFileSystemImpl extends FileSystem {
 
     @Override
     public synchronized int read(long pos, byte[] b, int off, int len) throws IOException {
-      if (len == 0)
-        return 0;
-      if (buf == null)
-        throw new IOException("stream was closed");
-      if (pos < 0)
-        throw new EOFException("position is negative");
       if (b == null || off < 0 || len < 0 || b.length - off < len) {
         throw new IllegalArgumentException("arguments: " + off + " " + len);
       }
-      if (len > 128 << 20) {
-        len = 128 << 20;
-      }
-      Pointer tmp = Memory.allocate(Runtime.getRuntime(lib), len);
-      int got = lib.jfs_pread(Thread.currentThread().getId(), fd, tmp, len, pos);
-      if (got == 0)
-        return -1;
-      if (got == EINVAL)
-        throw new IOException("stream was closed");
-      if (got < 0)
-        throw error(got, path);
-      tmp.get(0, b, off, got);
+      int got = read(pos, ByteBuffer.wrap(b, off, len));
       statistics.incrementBytesRead(got);
       return got;
     }
@@ -866,39 +820,31 @@ public class JuiceFileSystemImpl extends FileSystem {
         got++;
       }
       statistics.incrementBytesRead(got);
-      if (!b.hasRemaining())
-        return got;
       int more = read(position, b);
       if (more <= 0)
         return got > 0 ? got : -1;
       position += more;
+      statistics.incrementBytesRead(more);
       buf.position(0);
       buf.limit(0);
       return got + more;
     }
 
-    public synchronized int read(long pos, ByteBuffer b) throws IOException {
+    private synchronized int read(long pos, ByteBuffer b) throws IOException {
+      if (pos < 0)
+        throw new EOFException("position is negative");
       if (!b.hasRemaining())
         return 0;
       int got;
-      if (b.hasArray()) {
-        got = read(pos, b.array(), b.position(), b.remaining());
-        if (got <= 0)
-          return got;
-      } else {
-        assert b.isDirect();
-        long address = ((DirectBuffer) b).address() + b.position();
-        Pointer destPtr = Runtime.getRuntime(lib).getMemoryManager().newPointer(address);
-        got = lib.jfs_pread(Thread.currentThread().getId(), fd, destPtr, b.remaining(), pos);
-        if (got == EINVAL)
-          throw new IOException("stream was closed");
-        if (got < 0)
-          throw error(got, path);
-        if (got == 0)
-          return -1;
-        statistics.incrementBytesRead(got);
-      }
-      b.position(b.position() + got);
+      int startPos = b.position();
+      got = lib.jfs_pread(Thread.currentThread().getId(), fd, b, b.remaining(), pos);
+      if (got == EINVAL)
+        throw new IOException("stream was closed");
+      if (got < 0)
+        throw error(got, path);
+      if (got == 0)
+        return -1;
+      b.position(startPos + got);
       return got;
     }
 
@@ -939,7 +885,6 @@ public class JuiceFileSystemImpl extends FileSystem {
       if (buf == null) {
         return; // already closed
       }
-      bufferPool.returnBuffer(buf);
       buf = null;
       int r = lib.jfs_close(Thread.currentThread().getId(), fd);
       fd = 0;
@@ -1009,9 +954,7 @@ public class JuiceFileSystemImpl extends FileSystem {
       if (b.length - off < len) {
         throw new IndexOutOfBoundsException();
       }
-      Pointer buf = Memory.allocate(Runtime.getRuntime(lib), len);
-      buf.put(0, b, off, len);
-      int done = lib.jfs_write(Thread.currentThread().getId(), fd, buf, len);
+      int done = lib.jfs_write(Thread.currentThread().getId(), fd, b, len);
       if (done == EINVAL)
         throw new IOException("stream was closed");
       if (done < 0)
@@ -1023,9 +966,7 @@ public class JuiceFileSystemImpl extends FileSystem {
 
     @Override
     public void write(int b) throws IOException {
-      Pointer buf = Memory.allocate(Runtime.getRuntime(lib), 1);
-      buf.putByte(0, (byte) b);
-      int done = lib.jfs_write(Thread.currentThread().getId(), fd, buf, 1);
+      int done = lib.jfs_write(Thread.currentThread().getId(), fd, new byte[]{(byte) b}, 1);
       if (done == EINVAL)
         throw new IOException("stream was closed");
       if (done < 0)


Test patch:
diff --git a/pkg/meta/base_test.go b/pkg/meta/base_test.go
index 43ae398e..0c4055b1 100644
--- a/pkg/meta/base_test.go
+++ b/pkg/meta/base_test.go
@@ -69,6 +69,7 @@ func testMeta(t *testing.T, m Meta) {
 	testCopyFileRange(t, m)
 	testCloseSession(t, m)
 	testConcurrentDir(t, m)
+	testAttrFlags(t, m)
 	base := m.getBase()
 	base.conf.OpenCache = time.Second
 	base.of.expire = time.Second
@@ -1426,3 +1427,160 @@ func testConcurrentDir(t *testing.T, m Meta) {
 	}
 	g.Wait()
 }
+
+func testAttrFlags(t *testing.T, m Meta) {
+	ctx := Background
+	var attr = &Attr{}
+	var inode Ino
+	if st := m.Create(ctx, 1, "f", 0644, 022, 0, &inode, nil); st != 0 {
+		t.Fatalf("create f: %s", st)
+	}
+	attr.Flags = FlagAppend
+	if st := m.SetAttr(ctx, inode, SetAttrFlag, 0, attr); st != 0 {
+		t.Fatalf("setattr f: %s", st)
+	}
+	if st := m.Open(ctx, inode, syscall.O_WRONLY, attr); st != syscall.EPERM {
+		t.Fatalf("open f: %s", st)
+	}
+	if st := m.Open(ctx, inode, syscall.O_WRONLY|syscall.O_APPEND, attr); st != 0 {
+		t.Fatalf("open f: %s", st)
+	}
+	attr.Flags = FlagAppend | FlagImmutable
+	if st := m.SetAttr(ctx, inode, SetAttrFlag, 0, attr); st != 0 {
+		t.Fatalf("setattr f: %s", st)
+	}
+	if st := m.Open(ctx, inode, syscall.O_WRONLY, attr); st != syscall.EPERM {
+		t.Fatalf("open f: %s", st)
+	}
+	if st := m.Open(ctx, inode, syscall.O_WRONLY|syscall.O_APPEND, attr); st != syscall.EPERM {
+		t.Fatalf("open f: %s", st)
+	}
+
+	var d Ino
+	if st := m.Mkdir(ctx, 1, "d", 0640, 022, 0, &d, attr); st != 0 {
+		t.Fatalf("mkdir d: %s", st)
+	}
+	attr.Flags = FlagAppend
+	if st := m.SetAttr(ctx, d, SetAttrFlag, 0, attr); st != 0 {
+		t.Fatalf("setattr d: %s", st)
+	}
+	if st := m.Create(ctx, d, "f", 0644, 022, 0, &inode, nil); st != 0 {
+		t.Fatalf("create f: %s", st)
+	}
+	if st := m.Unlink(ctx, d, "f"); st != syscall.EPERM {
+		t.Fatalf("unlink f: %s", st)
+	}
+	attr.Flags = FlagAppend | FlagImmutable
+	if st := m.SetAttr(ctx, d, SetAttrFlag, 0, attr); st != 0 {
+		t.Fatalf("setattr d: %s", st)
+	}
+	if st := m.Create(ctx, d, "f2", 0644, 022, 0, &inode, nil); st != syscall.EPERM {
+		t.Fatalf("create f2: %s", st)
+	}
+
+	var Immutable Ino
+	if st := m.Mkdir(ctx, 1, "ImmutFile", 0640, 022, 0, &Immutable, attr); st != 0 {
+		t.Fatalf("mkdir d: %s", st)
+	}
+	attr.Flags = FlagImmutable
+	if st := m.SetAttr(ctx, Immutable, SetAttrFlag, 0, attr); st != 0 {
+		t.Fatalf("setattr d: %s", st)
+	}
+	if st := m.Create(ctx, Immutable, "f2", 0644, 022, 0, &inode, nil); st != syscall.EPERM {
+		t.Fatalf("create f2: %s", st)
+	}
+
+	var src1, dst1, mfile Ino
+	attr.Flags = 0
+	if st := m.Mkdir(ctx, 1, "src1", 0640, 022, 0, &src1, attr); st != 0 {
+		t.Fatalf("mkdir src1: %s", st)
+	}
+	if st := m.Create(ctx, src1, "mfile", 0644, 022, 0, &mfile, nil); st != 0 {
+		t.Fatalf("create mfile: %s", st)
+	}
+	if st := m.Mkdir(ctx, 1, "dst1", 0640, 022, 0, &dst1, attr); st != 0 {
+		t.Fatalf("mkdir dst1: %s", st)
+	}
+
+	attr.Flags = FlagAppend
+	if st := m.SetAttr(ctx, src1, SetAttrFlag, 0, attr); st != 0 {
+		t.Fatalf("setattr d: %s", st)
+	}
+	if st := m.Rename(ctx, src1, "mfile", dst1, "mfile", 0, &mfile, attr); st != syscall.EPERM {
+		t.Fatalf("rename d: %s", st)
+	}
+
+	attr.Flags = FlagImmutable
+	if st := m.SetAttr(ctx, src1, SetAttrFlag, 0, attr); st != 0 {
+		t.Fatalf("setattr d: %s", st)
+	}
+	if st := m.Rename(ctx, src1, "mfile", dst1, "mfile", 0, &mfile, attr); st != syscall.EPERM {
+		t.Fatalf("rename d: %s", st)
+	}
+
+	if st := m.SetAttr(ctx, dst1, SetAttrFlag, 0, attr); st != 0 {
+		t.Fatalf("setattr d: %s", st)
+	}
+	if st := m.Rename(ctx, src1, "mfile", dst1, "mfile", 0, &mfile, attr); st != syscall.EPERM {
+		t.Fatalf("rename d: %s", st)
+	}
+
+	var delFile Ino
+	if st := m.Create(ctx, 1, "delfile", 0644, 022, 0, &delFile, nil); st != 0 {
+		t.Fatalf("create f: %s", st)
+	}
+	attr.Flags = FlagImmutable | FlagAppend
+	if st := m.SetAttr(ctx, delFile, SetAttrFlag, 0, attr); st != 0 {
+		t.Fatalf("setattr d: %s", st)
+	}
+	if st := m.Unlink(ctx, 1, "delfile"); st != syscall.EPERM {
+		t.Fatalf("unlink f: %s", st)
+	}
+
+	var fallocFile Ino
+	if st := m.Create(ctx, 1, "fallocfile", 0644, 022, 0, &fallocFile, nil); st != 0 {
+		t.Fatalf("create f: %s", st)
+	}
+	attr.Flags = FlagAppend
+	if st := m.SetAttr(ctx, fallocFile, SetAttrFlag, 0, attr); st != 0 {
+		t.Fatalf("setattr f: %s", st)
+	}
+	if st := m.Fallocate(ctx, fallocFile, fallocKeepSize, 0, 1024); st != 0 {
+		t.Fatalf("fallocate f: %s", st)
+	}
+	if st := m.Fallocate(ctx, fallocFile, fallocKeepSize|fallocZeroRange, 0, 1024); st != syscall.EPERM {
+		t.Fatalf("fallocate f: %s", st)
+	}
+	attr.Flags = FlagImmutable
+	if st := m.SetAttr(ctx, fallocFile, SetAttrFlag, 0, attr); st != 0 {
+		t.Fatalf("setattr f: %s", st)
+	}
+	if st := m.Fallocate(ctx, fallocFile, fallocKeepSize, 0, 1024); st != syscall.EPERM {
+		t.Fatalf("fallocate f: %s", st)
+	}
+
+	var copysrcFile, copydstFile Ino
+	if st := m.Create(ctx, 1, "copysrcfile", 0644, 022, 0, &copysrcFile, nil); st != 0 {
+		t.Fatalf("create f: %s", st)
+	}
+	if st := m.Create(ctx, 1, "copydstfile", 0644, 022, 0, &copydstFile, nil); st != 0 {
+		t.Fatalf("create f: %s", st)
+	}
+	if st := m.Fallocate(ctx, copysrcFile, 0, 0, 1024); st != 0 {
+		t.Fatalf("fallocate f: %s", st)
+	}
+	attr.Flags = FlagAppend
+	if st := m.SetAttr(ctx, copydstFile, SetAttrFlag, 0, attr); st != 0 {
+		t.Fatalf("setattr f: %s", st)
+	}
+	if st := m.CopyFileRange(ctx, copysrcFile, 0, copydstFile, 0, 1024, 0, nil); st != syscall.EPERM {
+		t.Fatalf("copy_file_range f: %s", st)
+	}
+	attr.Flags = FlagImmutable
+	if st := m.SetAttr(ctx, copydstFile, SetAttrFlag, 0, attr); st != 0 {
+		t.Fatalf("setattr f: %s", st)
+	}
+	if st := m.CopyFileRange(ctx, copysrcFile, 0, copydstFile, 0, 1024, 0, nil); st != syscall.EPERM {
+		t.Fatalf("copy_file_range f: %s", st)
+	}
+}
diff --git a/sdk/java/src/test/java/io/juicefs/JuiceFileSystemTest.java b/sdk/java/src/test/java/io/juicefs/JuiceFileSystemTest.java
index 4953a2d0..206cccd9 100644
--- a/sdk/java/src/test/java/io/juicefs/JuiceFileSystemTest.java
+++ b/sdk/java/src/test/java/io/juicefs/JuiceFileSystemTest.java
@@ -349,6 +349,21 @@ public class JuiceFileSystemTest extends TestCase {
         throw new IOException("Reached the end of stream. Still have: " + buf.remaining() + " bytes left");
       }
     }
+
+    Path directReadFile = new Path("/direct_file");
+    FSDataOutputStream ou = fs.create(directReadFile);
+    ou.write("hello world".getBytes());
+    ou.close();
+    FSDataInputStream dto = fs.open(directReadFile);
+    ByteBuffer directBuf = ByteBuffer.allocateDirect(11);
+    directBuf.put("hello ".getBytes());
+    dto.seek(6);
+    dto.read(directBuf);
+    byte[] rest = new byte[11];
+    directBuf.flip();
+    directBuf.get(rest, 0, rest.length);
+    assertEquals("hello world", new String(rest));
+
     /*
      * FSDataOutputStream out = fs.create(new Path("/bigfile"), true); byte[] arr =
      * new byte[1<<20]; for (int i=0; i<1024; i++) { out.write(arr); } out.close();
@@ -407,6 +422,10 @@ public class JuiceFileSystemTest extends TestCase {
     in.read(3000, new byte[6000], 0, 3000);
     assertEquals(readSize * 2 + 3000 + 3000, statistics.getBytesRead());
 
+    in.read(new byte[3000], 0, 3000);
+    assertEquals(readSize * 2 + 3000 + 3000 + 3000, statistics.getBytesRead());
+
+    in.close();
   }
 
   public void testChecksum() throws IOException {