Skip to content

Commit d2ce0de

Browse files
authored
Add POSIX special file support (#259)
This commit adds support for POSIX special files throughout the filesystem stack. The FileSystem trait now includes a mknod() method that accepts mode (containing file type and permissions) and rdev (device major/minor numbers for character and block devices). A new rdev field in the Stats struct stores device numbers, and new file type constants (S_IFIFO, S_IFCHR, S_IFBLK, S_IFSOCK) are exported for use by implementations. The FUSE layer implements mknod() by delegating to the FileSystem trait and properly returning entry attributes after creation. The fillattr() function now maps all file types (FIFO, character device, block device, socket) to their corresponding FUSE FileType variants instead of treating everything as regular files. For AgentFS (SQLite-based storage), the fs_inode table gains an rdev column to persist device numbers. The mknod implementation creates inodes with the appropriate mode bits and device numbers, following the same pattern as file and directory creation. HostFS passes through to the libc mknod() call for native filesystem support. OverlayFS implements copy-on-write semantics for special files, checking both layers and handling whiteouts before creating in the delta layer. Also add some syscall tests verify mknod() and mkfifo() behavior including FIFO creation, permission handling, error cases (EEXIST, ENOENT), and device node creation when CAP_MKNOD is available.
2 parents 5018e31 + c00a8d4 commit d2ce0de

File tree

12 files changed

+524
-20
lines changed

12 files changed

+524
-20
lines changed

SPEC.md

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Agent Filesystem Specification
22

3-
**Version:** 0.3
3+
**Version:** 0.4
44

55
## Introduction
66

@@ -170,7 +170,8 @@ CREATE TABLE fs_inode (
170170
size INTEGER NOT NULL DEFAULT 0,
171171
atime INTEGER NOT NULL,
172172
mtime INTEGER NOT NULL,
173-
ctime INTEGER NOT NULL
173+
ctime INTEGER NOT NULL,
174+
rdev INTEGER NOT NULL DEFAULT 0
174175
)
175176
```
176177

@@ -185,6 +186,7 @@ CREATE TABLE fs_inode (
185186
- `atime` - Last access time (Unix timestamp, seconds)
186187
- `mtime` - Last modification time (Unix timestamp, seconds)
187188
- `ctime` - Creation/change time (Unix timestamp, seconds)
189+
- `rdev` - Device number for character and block devices (major/minor encoded)
188190

189191
**Mode Encoding:**
190192

@@ -196,6 +198,10 @@ File type (upper bits):
196198
0o100000 - Regular file (S_IFREG)
197199
0o040000 - Directory (S_IFDIR)
198200
0o120000 - Symbolic link (S_IFLNK)
201+
0o010000 - FIFO/named pipe (S_IFIFO)
202+
0o020000 - Character device (S_IFCHR)
203+
0o060000 - Block device (S_IFBLK)
204+
0o140000 - Socket (S_IFSOCK)
199205
200206
Permissions (lower 12 bits):
201207
0o000777 - Permission bits (rwxrwxrwx)
@@ -417,7 +423,7 @@ To read `length` bytes starting at byte offset `offset`:
417423
1. Resolve path to inode
418424
2. Query inode (includes link count):
419425
```sql
420-
SELECT ino, mode, nlink, uid, gid, size, atime, mtime, ctime
426+
SELECT ino, mode, nlink, uid, gid, size, atime, mtime, ctime, rdev
421427
FROM fs_inode WHERE ino = ?
422428
```
423429

@@ -685,6 +691,13 @@ Such extensions SHOULD use separate tables to maintain referential integrity.
685691

686692
## Revision History
687693

694+
### Version 0.4
695+
696+
- Added POSIX special file support (FIFOs, character devices, block devices, sockets)
697+
- Added `rdev` column to `fs_inode` table for device major/minor numbers
698+
- Added `S_IFIFO`, `S_IFCHR`, `S_IFBLK`, `S_IFSOCK` file type constants to Mode Encoding
699+
- Updated stat query to include `rdev` field
700+
688701
### Version 0.3
689702

690703
- Added `fs_origin` table to Overlay Filesystem for tracking copy-up origin inodes

cli/src/fuse.rs

Lines changed: 73 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use crate::fuser::{
88
Request,
99
};
1010
use agentfs_sdk::error::Error as SdkError;
11+
use agentfs_sdk::filesystem::{S_IFBLK, S_IFCHR, S_IFDIR, S_IFIFO, S_IFLNK, S_IFMT, S_IFSOCK};
1112
use agentfs_sdk::{BoxedFile, FileSystem, Stats};
1213
use parking_lot::Mutex;
1314
use std::{
@@ -548,6 +549,67 @@ impl Filesystem for AgentFSFuse {
548549
reply.ok();
549550
}
550551

552+
/// Creates a special file node (FIFO, device, socket, or regular file).
553+
///
554+
/// Creates a file node at `name` under `parent` with the specified mode
555+
/// and device number, then stats it to return proper attributes.
556+
fn mknod(
557+
&mut self,
558+
req: &Request,
559+
parent: u64,
560+
name: &OsStr,
561+
mode: u32,
562+
_umask: u32,
563+
rdev: u32,
564+
reply: ReplyEntry,
565+
) {
566+
tracing::debug!(
567+
"FUSE::mknod: parent={}, name={:?}, mode={:o}, rdev={}",
568+
parent,
569+
name,
570+
mode,
571+
rdev
572+
);
573+
let Some(path) = self.lookup_path(parent, name) else {
574+
reply.error(libc::ENOENT);
575+
return;
576+
};
577+
578+
let uid = req.uid();
579+
let gid = req.gid();
580+
let fs = self.fs.clone();
581+
let (result, path) = self.runtime.block_on(async move {
582+
let result = fs.mknod(&path, mode, rdev as u64, uid, gid).await;
583+
(result, path)
584+
});
585+
586+
if let Err(e) = result {
587+
reply.error(error_to_errno(&e));
588+
return;
589+
}
590+
591+
// Get the new node's stats
592+
let fs = self.fs.clone();
593+
let (stat_result, path) = self.runtime.block_on(async move {
594+
let result = fs.stat(&path).await;
595+
(result, path)
596+
});
597+
598+
match stat_result {
599+
Ok(Some(stats)) => {
600+
let attr = fillattr(&stats);
601+
self.add_path(attr.ino, path);
602+
reply.entry(&TTL, &attr, 0);
603+
}
604+
Ok(None) => {
605+
reply.error(libc::ENOENT);
606+
}
607+
Err(e) => {
608+
reply.error(error_to_errno(&e));
609+
}
610+
}
611+
}
612+
551613
/// Creates a new directory.
552614
///
553615
/// Creates a directory at `name` under `parent`, then stats it to return
@@ -1271,15 +1333,18 @@ impl AgentFSFuse {
12711333
/// The uid and gid parameters override the stored values to ensure proper
12721334
/// file ownership reporting (avoids "dubious ownership" errors from git).
12731335
fn fillattr(stats: &Stats) -> FileAttr {
1274-
let kind = if stats.is_directory() {
1275-
FileType::Directory
1276-
} else if stats.is_symlink() {
1277-
FileType::Symlink
1278-
} else {
1279-
FileType::RegularFile
1336+
let file_type = stats.mode & S_IFMT;
1337+
let kind = match file_type {
1338+
S_IFDIR => FileType::Directory,
1339+
S_IFLNK => FileType::Symlink,
1340+
S_IFIFO => FileType::NamedPipe,
1341+
S_IFCHR => FileType::CharDevice,
1342+
S_IFBLK => FileType::BlockDevice,
1343+
S_IFSOCK => FileType::Socket,
1344+
_ => FileType::RegularFile,
12801345
};
12811346

1282-
let size = if stats.is_directory() {
1347+
let size = if file_type == S_IFDIR {
12831348
4096_u64 // Standard directory size
12841349
} else {
12851350
stats.size as u64
@@ -1298,7 +1363,7 @@ fn fillattr(stats: &Stats) -> FileAttr {
12981363
nlink: stats.nlink,
12991364
uid: stats.uid,
13001365
gid: stats.gid,
1301-
rdev: 0,
1366+
rdev: stats.rdev as u32,
13021367
flags: 0,
13031368
blksize: 512,
13041369
}

cli/tests/syscall/Makefile

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ SRCS = main.c \
1919
test-link.c \
2020
test-unlink.c \
2121
test-copyup-inode-stability.c \
22-
test-rename.c
22+
test-rename.c \
23+
test-mknod.c \
24+
test-mkfifo.c
2325

2426
# Object files
2527
OBJS = $(SRCS:.c=.o)

cli/tests/syscall/main.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ int main(int argc, char *argv[]) {
3535
{"unlink", test_unlink},
3636
{"copyup_inode_stability", test_copyup_inode_stability},
3737
{"rename", test_rename},
38+
{"mknod", test_mknod},
39+
{"mkfifo", test_mkfifo},
3840
};
3941

4042
int num_tests = sizeof(tests) / sizeof(tests[0]);

cli/tests/syscall/test-common.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,5 +53,7 @@ int test_unlink(const char *base_path);
5353
int test_copyup_inode_stability(const char *base_path);
5454
int test_rename(const char *base_path);
5555
int test_chown(const char *base_path);
56+
int test_mknod(const char *base_path);
57+
int test_mkfifo(const char *base_path);
5658

5759
#endif /* TEST_COMMON_H */

cli/tests/syscall/test-mkfifo.c

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
#define _GNU_SOURCE
2+
#include "test-common.h"
3+
#include <sys/stat.h>
4+
#include <fcntl.h>
5+
#include <unistd.h>
6+
7+
int test_mkfifo(const char *base_path) {
8+
char path[512];
9+
struct stat st;
10+
int result;
11+
mode_t old_umask;
12+
13+
/* Save and clear umask for predictable permission tests */
14+
old_umask = umask(0);
15+
16+
/* Test 1: Create a FIFO (named pipe) using mkfifo */
17+
snprintf(path, sizeof(path), "%s/test_fifo_mkfifo", base_path);
18+
unlink(path); /* Clean up any previous test file */
19+
20+
result = mkfifo(path, 0644);
21+
TEST_ASSERT_ERRNO(result == 0, "mkfifo creation should succeed");
22+
23+
result = stat(path, &st);
24+
TEST_ASSERT_ERRNO(result == 0, "stat on FIFO should succeed");
25+
TEST_ASSERT(S_ISFIFO(st.st_mode), "created node should be a FIFO");
26+
TEST_ASSERT((st.st_mode & 0777) == 0644, "FIFO should have correct permissions (0644)");
27+
28+
unlink(path);
29+
30+
/* Test 2: mkfifo with existing path should fail with EEXIST */
31+
snprintf(path, sizeof(path), "%s/test.txt", base_path);
32+
33+
result = mkfifo(path, 0644);
34+
TEST_ASSERT(result < 0, "mkfifo on existing file should fail");
35+
TEST_ASSERT(errno == EEXIST, "errno should be EEXIST for existing path");
36+
37+
/* Test 3: mkfifo in non-existent directory should fail with ENOENT */
38+
snprintf(path, sizeof(path), "%s/nonexistent_dir/test_mkfifo", base_path);
39+
40+
result = mkfifo(path, 0644);
41+
TEST_ASSERT(result < 0, "mkfifo in non-existent directory should fail");
42+
TEST_ASSERT(errno == ENOENT, "errno should be ENOENT for non-existent directory");
43+
44+
/* Test 4: Create a FIFO with different permissions (0755) */
45+
snprintf(path, sizeof(path), "%s/test_fifo_perms_755", base_path);
46+
unlink(path);
47+
48+
result = mkfifo(path, 0755);
49+
TEST_ASSERT_ERRNO(result == 0, "mkfifo with 0755 should succeed");
50+
51+
result = stat(path, &st);
52+
TEST_ASSERT_ERRNO(result == 0, "stat on FIFO should succeed");
53+
TEST_ASSERT((st.st_mode & 0777) == 0755, "FIFO should have 0755 permissions");
54+
55+
unlink(path);
56+
57+
/* Test 5: Create a FIFO with restrictive permissions (0600) */
58+
snprintf(path, sizeof(path), "%s/test_fifo_perms_600", base_path);
59+
unlink(path);
60+
61+
result = mkfifo(path, 0600);
62+
TEST_ASSERT_ERRNO(result == 0, "mkfifo with 0600 should succeed");
63+
64+
result = stat(path, &st);
65+
TEST_ASSERT_ERRNO(result == 0, "stat on FIFO should succeed");
66+
TEST_ASSERT((st.st_mode & 0777) == 0600, "FIFO should have 0600 permissions");
67+
68+
unlink(path);
69+
70+
/* Test 6: Create FIFO in subdirectory */
71+
char subdir_path[512];
72+
snprintf(subdir_path, sizeof(subdir_path), "%s/mkfifo_subdir", base_path);
73+
mkdir(subdir_path, 0755);
74+
75+
snprintf(path, sizeof(path), "%s/mkfifo_subdir/test_fifo", base_path);
76+
unlink(path);
77+
78+
result = mkfifo(path, 0644);
79+
TEST_ASSERT_ERRNO(result == 0, "mkfifo in subdirectory should succeed");
80+
81+
result = stat(path, &st);
82+
TEST_ASSERT_ERRNO(result == 0, "stat on FIFO in subdirectory should succeed");
83+
TEST_ASSERT(S_ISFIFO(st.st_mode), "created node in subdir should be a FIFO");
84+
85+
unlink(path);
86+
rmdir(subdir_path);
87+
88+
/* Test 7: Create FIFO and verify it has size 0 */
89+
snprintf(path, sizeof(path), "%s/test_fifo_size", base_path);
90+
unlink(path);
91+
92+
result = mkfifo(path, 0644);
93+
TEST_ASSERT_ERRNO(result == 0, "mkfifo should succeed");
94+
95+
result = stat(path, &st);
96+
TEST_ASSERT_ERRNO(result == 0, "stat on FIFO should succeed");
97+
TEST_ASSERT(st.st_size == 0, "FIFO should have size 0");
98+
99+
unlink(path);
100+
101+
/* Test 8: Create FIFO and verify link count is 1 */
102+
snprintf(path, sizeof(path), "%s/test_fifo_nlink", base_path);
103+
unlink(path);
104+
105+
result = mkfifo(path, 0644);
106+
TEST_ASSERT_ERRNO(result == 0, "mkfifo should succeed");
107+
108+
result = stat(path, &st);
109+
TEST_ASSERT_ERRNO(result == 0, "stat on FIFO should succeed");
110+
TEST_ASSERT(st.st_nlink == 1, "FIFO should have nlink == 1");
111+
112+
unlink(path);
113+
114+
/* Test 9: Create FIFO with world-writable permissions (0666) */
115+
snprintf(path, sizeof(path), "%s/test_fifo_666", base_path);
116+
unlink(path);
117+
118+
result = mkfifo(path, 0666);
119+
TEST_ASSERT_ERRNO(result == 0, "mkfifo with 0666 should succeed");
120+
121+
result = stat(path, &st);
122+
TEST_ASSERT_ERRNO(result == 0, "stat on FIFO should succeed");
123+
TEST_ASSERT((st.st_mode & 0777) == 0666, "FIFO should have 0666 permissions");
124+
125+
unlink(path);
126+
127+
/* Test 10: Verify FIFO shows up in directory listing */
128+
snprintf(path, sizeof(path), "%s/test_fifo_readdir", base_path);
129+
unlink(path);
130+
131+
result = mkfifo(path, 0644);
132+
TEST_ASSERT_ERRNO(result == 0, "mkfifo should succeed");
133+
134+
/* Verify file exists via lstat too */
135+
result = lstat(path, &st);
136+
TEST_ASSERT_ERRNO(result == 0, "lstat on FIFO should succeed");
137+
TEST_ASSERT(S_ISFIFO(st.st_mode), "lstat should report FIFO type");
138+
139+
unlink(path);
140+
141+
/* Restore original umask */
142+
umask(old_umask);
143+
144+
return 0;
145+
}

0 commit comments

Comments
 (0)