1 /*
2 * Copyright (c) 2025 Antmicro <www.antmicro.com>
3 *
4 * SPDX-License-Identifier: Apache-2.0
5 */
6
7 #include <zephyr/init.h>
8 #include <zephyr/fs/fs.h>
9 #include <zephyr/fs/fs_sys.h>
10 #include <zephyr/fs/virtiofs.h>
11 #include <zephyr/posix/fcntl.h>
12 #include "../fs_impl.h"
13 #include <string.h>
14 #include "virtiofs.h"
15
16 #define MODE_FTYPE_MASK 0170000
17 #define MODE_FTYPE_DIR 040000
18
19 #define DT_DIR 4
20 #define DT_REG 8
21
22 struct virtiofs_file {
23 uint64_t fh;
24 uint64_t nodeid;
25 uint64_t offset;
26 uint32_t open_flags;
27 };
28
29 struct virtiofs_dir {
30 uint64_t fh;
31 uint64_t nodeid;
32 uint64_t offset;
33 };
34
35 K_MEM_SLAB_DEFINE_STATIC(
36 file_struct_slab, sizeof(struct virtiofs_file), CONFIG_VIRTIOFS_MAX_FILES, sizeof(void *)
37 );
38 K_MEM_SLAB_DEFINE_STATIC(
39 dir_struct_slab, sizeof(struct virtiofs_dir), CONFIG_VIRTIOFS_MAX_FILES, sizeof(void *)
40 );
41
zephyr_mode_to_posix(int m)42 static int zephyr_mode_to_posix(int m)
43 {
44 int mode = (m & FS_O_CREATE) ? O_CREAT : 0;
45
46 mode |= (m & FS_O_APPEND) ? O_APPEND : 0;
47 mode |= (m & FS_O_TRUNC) ? O_TRUNC : 0;
48
49 switch (m & FS_O_MODE_MASK) {
50 case FS_O_READ:
51 mode |= O_RDONLY;
52 break;
53 case FS_O_WRITE:
54 mode |= O_WRONLY;
55 break;
56 case FS_O_RDWR:
57 mode |= O_RDWR;
58 break;
59 default:
60 break;
61 }
62
63 return mode;
64 }
65
virtiofs_strip_prefix(const char * path,const struct fs_mount_t * mp)66 static const char *virtiofs_strip_prefix(const char *path, const struct fs_mount_t *mp)
67 {
68 const char *virtiofs_path = fs_impl_strip_prefix(path, mp);
69
70 if (virtiofs_path[0] == '/') {
71 virtiofs_path++;
72 }
73 return virtiofs_path;
74 }
75
strip_path(const char * fpath)76 static const char *strip_path(const char *fpath)
77 {
78 const char *c = fpath + strlen(fpath);
79
80 for (; c > fpath; c--) {
81 if (*c == '/') {
82 c++;
83 break;
84 }
85 }
86
87 return c;
88 }
89
90 /*
91 * despite the similarity of fuse/virtiofs to posix fs functions there are some notable differences:
92 * - open() is split into lookup+open in case of existing files and lookup+create in case of
93 * O_CREATE
94 * - opendir() is split into lookup+opendir
95 * - lookups are non-recursive, we have to traverse through each directory in the path
96 * - close()/closedir() is split into release+forget/releasedir+forget
97 * - read()/write()/readdir() takes offset as a parameter
98 * - there is sort of reverse stat() - settatr, that can be used to i.e. truncate the file
99 */
100
virtiofs_zfs_open_existing(struct fs_file_t * filp,struct fuse_entry_out lookup_ret,int flags)101 static int virtiofs_zfs_open_existing(
102 struct fs_file_t *filp, struct fuse_entry_out lookup_ret, int flags)
103 {
104 struct fuse_open_out open_ret;
105 struct virtiofs_file *file;
106
107 int ret = virtiofs_open(
108 filp->mp->storage_dev, lookup_ret.nodeid, zephyr_mode_to_posix(flags), &open_ret,
109 FUSE_FILE
110 );
111 if (ret == 0) {
112 ret = k_mem_slab_alloc(&file_struct_slab, (void **)&file, K_NO_WAIT);
113 if (ret != 0) {
114 virtiofs_release(
115 filp->mp->storage_dev, lookup_ret.nodeid, open_ret.fh, FUSE_FILE
116 );
117 return ret;
118 }
119
120 file->fh = open_ret.fh;
121 file->nodeid = lookup_ret.nodeid;
122 file->offset = 0;
123 file->open_flags = flags;
124
125 filp->filep = file;
126 }
127
128 return ret;
129 }
130
virtiofs_zfs_open_create(struct fs_file_t * filp,int flags,const char * path,uint64_t parent_inode)131 static int virtiofs_zfs_open_create(
132 struct fs_file_t *filp, int flags, const char *path, uint64_t parent_inode)
133 {
134 struct fuse_create_out create_ret;
135 const char *fname = strip_path(path);
136 struct virtiofs_file *file;
137
138 int ret = virtiofs_create(
139 filp->mp->storage_dev, parent_inode, fname,
140 zephyr_mode_to_posix(flags), CONFIG_VIRTIOFS_CREATE_MODE_VALUE, &create_ret
141 );
142 if (ret == 0) {
143 ret = k_mem_slab_alloc(&file_struct_slab, (void **)&file, K_NO_WAIT);
144 if (ret != 0) {
145 virtiofs_release(
146 filp->mp->storage_dev, create_ret.entry_out.nodeid,
147 create_ret.open_out.fh, FUSE_FILE
148 );
149 return ret;
150 }
151
152 file->fh = create_ret.open_out.fh;
153 file->nodeid = create_ret.entry_out.nodeid;
154 file->offset = 0;
155 file->open_flags = flags;
156
157 filp->filep = file;
158 }
159
160 return ret;
161 }
162
virtiofs_zfs_open(struct fs_file_t * filp,const char * fs_path,fs_mode_t flags)163 static int virtiofs_zfs_open(struct fs_file_t *filp, const char *fs_path, fs_mode_t flags)
164 {
165 int ret = 0;
166 struct fuse_entry_out lookup_ret;
167 const char *path = virtiofs_strip_prefix(fs_path, filp->mp);
168 uint64_t parent_inode = FUSE_ROOT_INODE;
169
170 ret = virtiofs_lookup(
171 filp->mp->storage_dev, FUSE_ROOT_INODE, path, &lookup_ret, &parent_inode
172 );
173 if (ret == 0) {
174 ret = virtiofs_zfs_open_existing(filp, lookup_ret, flags & ~FS_O_CREATE);
175 } else if ((flags & FS_O_CREATE) && parent_inode != 0) {
176 ret = virtiofs_zfs_open_create(filp, flags, path, parent_inode);
177 } else {
178 if (parent_inode != 0) {
179 virtiofs_forget(filp->mp->storage_dev, parent_inode, 1);
180 }
181 return ret;
182 }
183
184 if (parent_inode != 0) {
185 virtiofs_forget(filp->mp->storage_dev, parent_inode, 1);
186 }
187
188 if (ret != 0) {
189 virtiofs_forget(filp->mp->storage_dev, lookup_ret.nodeid, 1);
190 }
191
192 return ret;
193 }
194
virtiofs_zfs_close(struct fs_file_t * filp)195 static int virtiofs_zfs_close(struct fs_file_t *filp)
196 {
197 struct virtiofs_file *file = filp->filep;
198 uint64_t nodeid = file->nodeid;
199 int ret = virtiofs_release(filp->mp->storage_dev, file->nodeid, file->fh, FUSE_FILE);
200
201 if (ret == 0) {
202 k_mem_slab_free(&file_struct_slab, file);
203 } else {
204 return ret;
205 }
206
207 virtiofs_forget(filp->mp->storage_dev, nodeid, 1);
208
209 return 0;
210 }
211
virtiofs_zfs_read(struct fs_file_t * filp,void * dest,size_t nbytes)212 static ssize_t virtiofs_zfs_read(struct fs_file_t *filp, void *dest, size_t nbytes)
213 {
214 struct virtiofs_file *file = filp->filep;
215 int read_c = virtiofs_read(
216 filp->mp->storage_dev, file->nodeid, file->fh, file->offset, nbytes, dest
217 );
218
219 if (read_c >= 0) {
220 file->offset += read_c;
221 }
222
223 return read_c;
224 }
225
226 #define FUSE_SEEK_SET 0
227 #define FUSE_SEEK_CUR 1
228 #define FUSE_SEEK_END 2
229
zephyr_whence_to_posix(int whence)230 static int zephyr_whence_to_posix(int whence)
231 {
232 switch (whence) {
233 case SEEK_SET:
234 return FUSE_SEEK_SET;
235 case SEEK_CUR:
236 return FUSE_SEEK_CUR;
237 case SEEK_END:
238 return FUSE_SEEK_END;
239 default:
240 return whence;
241 }
242 }
243
virtio_zfs_lseek(struct fs_file_t * filp,off_t off,int whence)244 static int virtio_zfs_lseek(struct fs_file_t *filp, off_t off, int whence)
245 {
246 struct virtiofs_file *file = filp->filep;
247 struct fuse_lseek_out lseek_ret;
248 uint64_t off_arg = off;
249
250 whence = zephyr_whence_to_posix(whence);
251
252 /*
253 * SEEK_CUR is kind of broken with FUSE_LSEEK as reads/writes don't update the file
254 * offset on the host side so if we never used FUSE_LSEEK since opening the file, but
255 * did some reads/writes in the meantime and then used FUSE_LSEEK with SEEK_CUR+x,
256 * the returned offset would've been x instead of sum of read/written bytes + x. One
257 * solution is to pair each read/write with lseek(SEEK_CUR, read_c/write_c) to keep
258 * the offset updated on the host side, but we just don't use SEEK_CUR+x and instead
259 * use SEEK_SET with file->offset+x. Essentially the only thing FUSE_LSEEK provides
260 * for us is bounds checking and easier handling of SEEK_END (otherwise we would have
261 * to use other fuse call to determine file size)
262 */
263 if (whence == FUSE_SEEK_CUR) {
264 whence = FUSE_SEEK_SET;
265 off_arg = file->offset + off;
266 }
267
268 int ret = virtiofs_lseek(
269 filp->mp->storage_dev, file->nodeid, file->fh, off_arg, whence, &lseek_ret
270 );
271
272 if (ret != 0) {
273 return ret;
274 }
275
276 file->offset = lseek_ret.offset;
277
278 return file->offset;
279 }
280
virtio_zfs_write(struct fs_file_t * filp,const void * src,size_t nbytes)281 static ssize_t virtio_zfs_write(struct fs_file_t *filp, const void *src, size_t nbytes)
282 {
283 struct virtiofs_file *file = filp->filep;
284 struct virtiofs_fs_data *fs_data = filp->mp->fs_data;
285 const uint8_t *curr_addr = src;
286 int write_c = 0;
287
288 while (nbytes > 0) {
289 /*
290 * max write size is limited to max_write from fuse_init_out received during fs
291 * initalization, so we have to split bigger writes into multiple smaller ones.
292 * If we try to write more the recent virtiofsd it will return 12 (Not enough
293 * space), but the older one will assert, rendering fs unusable until restart.
294 */
295 size_t curr_size = MIN(nbytes, fs_data->max_write);
296
297 /*
298 * while FUSE_WRITE will always write to the end if O_APPEND was passed when opening
299 * file (ignoring offset param) the file offset itself will remain unmodified on
300 * zephyr side, so we have to update it here
301 */
302 if (file->open_flags & FS_O_APPEND) {
303 virtio_zfs_lseek(filp, 0, SEEK_END);
304 }
305
306 int ret = virtiofs_write(
307 filp->mp->storage_dev, file->nodeid, file->fh, file->offset + write_c,
308 curr_size, curr_addr
309 );
310
311 if (ret >= 0) {
312 write_c += ret;
313 } else {
314 /*
315 * according to fs_write comment in fs.h zephyr handles partial
316 * failures like that
317 */
318 if (write_c > 0) {
319 errno = ret;
320 file->offset += write_c;
321 return write_c;
322 } else {
323 return ret;
324 }
325 }
326
327 nbytes -= curr_size;
328 curr_addr += curr_size;
329 }
330
331 file->offset += write_c;
332 return write_c;
333 }
334
virtiofs_zfs_tell(struct fs_file_t * filp)335 static off_t virtiofs_zfs_tell(struct fs_file_t *filp)
336 {
337 struct virtiofs_file *file = filp->filep;
338
339 return file->offset;
340 }
341
virtiofs_zfs_truncate(struct fs_file_t * filp,off_t length)342 static int virtiofs_zfs_truncate(struct fs_file_t *filp, off_t length)
343 {
344 struct virtiofs_file *file = filp->filep;
345 struct fuse_setattr_in attrs;
346 struct fuse_attr_out setattr_ret;
347
348 attrs.fh = file->fh;
349 attrs.size = length;
350 attrs.valid = FATTR_SIZE;
351
352 return virtiofs_setattr(filp->mp->storage_dev, file->nodeid, &attrs, &setattr_ret);
353 }
354
virtiofs_zfs_sync(struct fs_file_t * filp)355 static int virtiofs_zfs_sync(struct fs_file_t *filp)
356 {
357 struct virtiofs_file *file = filp->filep;
358
359 return virtiofs_fsync(filp->mp->storage_dev, file->nodeid, file->fh);
360 }
361
virtiofs_zfs_mkdir(struct fs_mount_t * mountp,const char * name)362 static int virtiofs_zfs_mkdir(struct fs_mount_t *mountp, const char *name)
363 {
364 const char *path = virtiofs_strip_prefix(name, mountp);
365 struct fuse_entry_out lookup_ret;
366 uint64_t parent_inode = FUSE_ROOT_INODE;
367 int ret = virtiofs_lookup(
368 mountp->storage_dev, FUSE_ROOT_INODE, path, &lookup_ret, &parent_inode
369 );
370
371 if (parent_inode != 0) {
372 ret = virtiofs_mkdir(
373 mountp->storage_dev, parent_inode, strip_path(name),
374 CONFIG_VIRTIOFS_CREATE_MODE_VALUE
375 );
376 virtiofs_forget(mountp->storage_dev, parent_inode, 1);
377 }
378
379 return ret;
380 }
381
virtiofs_zfs_opendir(struct fs_dir_t * dirp,const char * fs_path)382 static int virtiofs_zfs_opendir(struct fs_dir_t *dirp, const char *fs_path)
383 {
384 int ret = 0;
385 struct virtiofs_dir *dir;
386 struct fuse_entry_out lookup_ret;
387 const char *path = virtiofs_strip_prefix(fs_path, dirp->mp);
388
389 if (path[0] == '\0') {
390 /* looking up for "" or "/" yields nothing, so we have to use "." for root dir */
391 path = ".";
392 }
393
394 ret = virtiofs_lookup(dirp->mp->storage_dev, FUSE_ROOT_INODE, path, &lookup_ret, NULL);
395 if (ret == 0) {
396 struct fuse_open_out open_ret;
397
398 ret = virtiofs_open(
399 dirp->mp->storage_dev, lookup_ret.nodeid, O_RDONLY, &open_ret, FUSE_DIR
400 );
401 if (ret == 0) {
402 ret = k_mem_slab_alloc(&dir_struct_slab, (void **)&dir, K_NO_WAIT);
403 if (ret != 0) {
404 virtiofs_forget(dirp->mp->storage_dev, lookup_ret.nodeid, 1);
405 return ret;
406 }
407
408 dir->fh = open_ret.fh;
409 dir->nodeid = lookup_ret.nodeid;
410 dir->offset = 0;
411 dirp->dirp = dir;
412 }
413 } else {
414 virtiofs_forget(dirp->mp->storage_dev, lookup_ret.nodeid, 1);
415 }
416
417 return ret;
418 }
419
virtiofs_zfs_readdir(struct fs_dir_t * dirp,struct fs_dirent * entry)420 static int virtiofs_zfs_readdir(struct fs_dir_t *dirp, struct fs_dirent *entry)
421 {
422 struct virtiofs_dir *dir = dirp->dirp;
423 struct fuse_dirent de;
424
425 int read_c = virtiofs_readdir(
426 dirp->mp->storage_dev, dir->nodeid, dir->fh, dir->offset,
427 (uint8_t *)&de, sizeof(de), (uint8_t *)&entry->name, sizeof(entry->name)
428 );
429
430 /* end of dir */
431 if (read_c == 0) {
432 entry->name[0] = '\0';
433 return 0;
434 }
435
436 if (read_c < sizeof(de) || de.namelen >= sizeof(entry->name) - 1) {
437 return -EIO;
438 }
439
440 /*
441 * usually name is already null terminated, but sometimes name of the last entry
442 * in directory is not (maybe also in other cases), so we null terminate it here
443 */
444 entry->name[de.namelen] = '\0';
445
446 dir->offset = de.off;
447
448 if (de.type == DT_REG) {
449 struct fuse_entry_out lookup_ret;
450 int ret = virtiofs_lookup(
451 dirp->mp->storage_dev, dir->nodeid, entry->name, &lookup_ret, NULL
452 );
453
454 if (ret != 0) {
455 return ret;
456 }
457
458 virtiofs_forget(dirp->mp->storage_dev, lookup_ret.nodeid, 1);
459
460 entry->type = FS_DIR_ENTRY_FILE;
461 entry->size = lookup_ret.attr.size;
462 } else if (de.type == DT_DIR) {
463 entry->type = FS_DIR_ENTRY_DIR;
464 entry->size = 0;
465 } else {
466 return -ENOTSUP;
467 }
468
469 return 0;
470 }
471
virtiofs_zfs_closedir(struct fs_dir_t * dirp)472 static int virtiofs_zfs_closedir(struct fs_dir_t *dirp)
473 {
474 struct virtiofs_dir *dir = dirp->dirp;
475 uint64_t nodeid = dir->nodeid;
476 int ret = virtiofs_release(dirp->mp->storage_dev, dir->nodeid, dir->fh, FUSE_DIR);
477
478 if (ret == 0) {
479 k_mem_slab_free(&dir_struct_slab, dir);
480 } else {
481 return ret;
482 }
483
484 virtiofs_forget(dirp->mp->storage_dev, nodeid, 1);
485
486 return 0;
487 }
488
virtiofs_zfs_mount(struct fs_mount_t * mountp)489 static int virtiofs_zfs_mount(struct fs_mount_t *mountp)
490 {
491 struct fuse_init_out out;
492 int ret = virtiofs_init(mountp->storage_dev, &out);
493
494 if (ret == 0) {
495 struct virtiofs_fs_data *fs_data = mountp->fs_data;
496
497 fs_data->max_write = out.max_write;
498 }
499
500 return ret;
501 }
502
virtiofs_zfs_unmount(struct fs_mount_t * mountp)503 static int virtiofs_zfs_unmount(struct fs_mount_t *mountp)
504 {
505 return virtiofs_destroy(mountp->storage_dev);
506 }
507
virtiofs_zfs_stat(struct fs_mount_t * mountp,const char * fs_path,struct fs_dirent * entry)508 static int virtiofs_zfs_stat(
509 struct fs_mount_t *mountp, const char *fs_path, struct fs_dirent *entry)
510 {
511 const char *path = virtiofs_strip_prefix(fs_path, mountp);
512 const char *name = strip_path(fs_path);
513
514 if (strlen(name) + 1 > sizeof(entry->name)) {
515 return -ENOBUFS;
516 }
517
518 struct fuse_entry_out lookup_ret;
519 int ret = virtiofs_lookup(mountp->storage_dev, FUSE_ROOT_INODE, path, &lookup_ret, NULL);
520
521 if (ret != 0) {
522 return ret;
523 }
524
525 strcpy((char *)&entry->name, name);
526
527 if ((lookup_ret.attr.mode & MODE_FTYPE_MASK) == MODE_FTYPE_DIR) {
528 entry->type = FS_DIR_ENTRY_DIR;
529 entry->size = 0;
530 } else {
531 entry->type = FS_DIR_ENTRY_FILE;
532 entry->size = lookup_ret.attr.size;
533 }
534
535 virtiofs_forget(mountp->storage_dev, lookup_ret.nodeid, 1);
536
537 return 0;
538 }
539
virtiofs_zfs_unlink(struct fs_mount_t * mountp,const char * name)540 static int virtiofs_zfs_unlink(struct fs_mount_t *mountp, const char *name)
541 {
542 const char *path = virtiofs_strip_prefix(name, mountp);
543 struct fs_dirent d;
544 int ret = virtiofs_zfs_stat(mountp, name, &d);
545
546 if (ret != 0) {
547 return ret;
548 }
549
550 if (d.type == FS_DIR_ENTRY_FILE) {
551 #ifdef CONFIG_VIRTIOFS_VIRTIOFSD_UNLINK_QUIRK
552 struct fuse_entry_out lookup_ret;
553 /*
554 * Even if unlink doesn't take nodeid as a param it still fails with -EIO if the
555 * file wasn't looked up using some virtiofsd versions. It happens at least with
556 * the one from Debian's package (Debian 1:7.2+dfsg-7+deb12u7). Virtiofsd 1.12.0
557 * built from sources doesn't need it
558 */
559 ret = virtiofs_lookup(
560 mountp->storage_dev, FUSE_ROOT_INODE, path, &lookup_ret, NULL
561 );
562
563 if (ret != 0) {
564 return ret;
565 }
566 #endif
567
568 ret = virtiofs_unlink(mountp->storage_dev, path, FUSE_FILE);
569
570 #ifdef CONFIG_VIRTIOFS_VIRTIOFSD_UNLINK_QUIRK
571 virtiofs_forget(mountp->storage_dev, lookup_ret.nodeid, 1);
572 #endif
573 } else {
574 ret = virtiofs_unlink(mountp->storage_dev, path, FUSE_DIR);
575 }
576
577 return ret;
578 }
579
virtiofs_zfs_rename(struct fs_mount_t * mountp,const char * from,const char * to)580 static int virtiofs_zfs_rename(struct fs_mount_t *mountp, const char *from, const char *to)
581 {
582 const char *old_path = virtiofs_strip_prefix(from, mountp);
583 const char *new_path = virtiofs_strip_prefix(to, mountp);
584 uint64_t old_dir = FUSE_ROOT_INODE;
585 uint64_t new_dir = FUSE_ROOT_INODE;
586 struct fuse_entry_out old_lookup_ret;
587 struct fuse_entry_out new_lookup_ret;
588 int ret;
589
590 ret = virtiofs_lookup(
591 mountp->storage_dev, FUSE_ROOT_INODE, old_path, &old_lookup_ret, &old_dir
592 );
593
594 if (ret != 0) {
595 if (old_dir != 0 && old_dir != FUSE_ROOT_INODE) {
596 virtiofs_forget(mountp->storage_dev, old_dir, 1);
597 }
598 return ret;
599 }
600
601 ret = virtiofs_lookup(
602 mountp->storage_dev, FUSE_ROOT_INODE, new_path, &new_lookup_ret, &new_dir
603 );
604
605 /* there is no immediate parent of object's new path */
606 if (ret != 0 && new_dir == 0) {
607 virtiofs_forget(mountp->storage_dev, old_lookup_ret.nodeid, 1);
608 return ret;
609 }
610
611 ret = virtiofs_rename(
612 mountp->storage_dev,
613 old_dir, strip_path(old_path),
614 new_dir, strip_path(new_path)
615 );
616
617 virtiofs_forget(mountp->storage_dev, old_lookup_ret.nodeid, 1);
618 virtiofs_forget(mountp->storage_dev, new_lookup_ret.nodeid, 1);
619 virtiofs_forget(mountp->storage_dev, old_dir, 1);
620 virtiofs_forget(mountp->storage_dev, new_dir, 1);
621
622 return ret;
623 }
624
virtiofs_zfs_statvfs(struct fs_mount_t * mountp,const char * fs_path,struct fs_statvfs * stat)625 static int virtiofs_zfs_statvfs(
626 struct fs_mount_t *mountp, const char *fs_path, struct fs_statvfs *stat)
627 {
628 struct fuse_kstatfs statfs_out;
629 int ret = virtiofs_statfs(mountp->storage_dev, &statfs_out);
630
631 if (ret != 0) {
632 return ret;
633 }
634
635 stat->f_bsize = statfs_out.bsize;
636 stat->f_frsize = statfs_out.frsize;
637 stat->f_blocks = statfs_out.blocks;
638 stat->f_bfree = statfs_out.bfree;
639
640 return 0;
641 }
642
643 static const struct fs_file_system_t virtiofs_ops = {
644 .open = virtiofs_zfs_open,
645 .close = virtiofs_zfs_close,
646 .read = virtiofs_zfs_read,
647 .write = virtio_zfs_write,
648 .lseek = virtio_zfs_lseek,
649 .tell = virtiofs_zfs_tell,
650 .truncate = virtiofs_zfs_truncate,
651 .sync = virtiofs_zfs_sync,
652 .mkdir = virtiofs_zfs_mkdir,
653 .opendir = virtiofs_zfs_opendir,
654 .readdir = virtiofs_zfs_readdir,
655 .closedir = virtiofs_zfs_closedir,
656 .mount = virtiofs_zfs_mount,
657 .unmount = virtiofs_zfs_unmount,
658 .unlink = virtiofs_zfs_unlink,
659 .rename = virtiofs_zfs_rename,
660 .stat = virtiofs_zfs_stat,
661 .statvfs = virtiofs_zfs_statvfs
662 };
663
virtiofs_register(void)664 static int virtiofs_register(void)
665 {
666 return fs_register(FS_VIRTIOFS, &virtiofs_ops);
667 }
668
669 SYS_INIT(virtiofs_register, POST_KERNEL, 99);
670