├── .gitignore ├── AUTHORS ├── luufs.8 ├── COPYING ├── README ├── Makefile ├── test.sh └── luufs.c /.gitignore: -------------------------------------------------------------------------------- 1 | luufs 2 | *.o 3 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Dima Krasner (dima@dimakrasner.com) 2 | -------------------------------------------------------------------------------- /luufs.8: -------------------------------------------------------------------------------- 1 | .TH luufs 8 2 | .SH NAME 3 | .B luufs 4 | \- mirror or merge directories 5 | .SH SYNOPSIS 6 | .B luufs 7 | RO [RW] TARGET 8 | .SH DESCRIPTION 9 | Mirrors a directory without allowing any changes or creates a directory which 10 | unifies the contents of two directories, while redirecting all changes to the 11 | second one. 12 | .SH "SEE ALSO" 13 | .B ls(1), chroot(8), umount(8) 14 | .SH AUTHOR 15 | Dima Krasner (dima@dimakrasner.com) 16 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, 2015 Dima Krasner 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | _ __ 2 | | |_ _ _ _ / _|___ 3 | | | | | | | | | |_/ __| 4 | | | |_| | |_| | _\__ \ 5 | |_|\__,_|\__,_|_| |___/ 6 | 7 | Overview 8 | ======== 9 | 10 | luufs is a lazy man's, user-mode union file system. 11 | 12 | It takes two directories and creates a magical directory which shows their 13 | unified contents. 14 | 15 | luufs is a "compile once, run anywhere" alternative for Aufs 16 | (http://aufs.sourceforge.net/) and Unionfs (http://unionfs.filesystems.org/), 17 | implemented in user-mode via FUSE (http://fuse.sourceforge.net/). 18 | 19 | However, luufs is very simple, so it does not fit in all use cases of more 20 | complex union file systems. It operates according to three rules: 21 | 1) The first directory is read-only and the second one is writeable. New files 22 | are created under the writeable directory, but read from both directories. 23 | 2) If a file exists under both directories, the one under the read-only 24 | directory is preferred. This improves security, as files (e.g /bin/login) 25 | cannot be overwritten using external access to the writeable directory. 26 | 3) Non-root processes cannot open new file descriptors via luufs (e.g open 27 | files), but can use existing file descriptors. 28 | 29 | Therefore, luufs can be used to secure servers: they can be trapped under a 30 | luufs mount point (using chroot), with a writeable directory mounted with the 31 | MS_NOEXEC and MS_NODEV flags. 32 | 33 | In addition, luufs has a read-only mirroring mode, in which a directory is 34 | mirrored and changes are disallowed. It is similar to a bind mount, but may be 35 | read-only even if the specified directory is writable. 36 | 37 | Legal Information 38 | ================= 39 | 40 | luufs is licensed under the MIT license, see COPYING for the license 41 | text. For a list of its authors and contributors, see AUTHORS. 42 | 43 | The ASCII art logo at the top was made using FIGlet (http://www.figlet.org/). 44 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # this file is part of luufs. 2 | # 3 | # Copyright (c) 2014, 2015 Dima Krasner 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | PROG = luufs 24 | 25 | CC ?= cc 26 | CFLAGS ?= -Wall -pedantic 27 | LDFLAGS ?= 28 | DESTDIR ?= / 29 | SBIN_DIR ?= sbin 30 | DOC_DIR ?= usr/share/doc/$(PROG) 31 | MAN_DIR ?= usr/share/man 32 | HAVE_WAIVE ?= 0 33 | 34 | CFLAGS += -std=gnu99 -D_GNU_SOURCE 35 | 36 | FUSE_CFLAGS = $(shell pkg-config --cflags fuse) 37 | ZLIB_CFLAGS = $(shell pkg-config --cflags zlib) 38 | FUSE_LIBS = $(shell pkg-config --libs fuse) 39 | ZLIB_LIBS = $(shell pkg-config --libs zlib) 40 | 41 | ifeq (0,$(HAVE_WAIVE)) 42 | LIBWAIVE_CFLAGS = 43 | LIBWAIVE_LIBS = 44 | else 45 | CFLAGS += -DHAVE_WAIVE 46 | LIBWAIVE_CFLAGS = $(shell pkg-config --cflags libwaive) 47 | LIBWAIVE_LIBS = $(shell pkg-config --libs libwaive) 48 | endif 49 | 50 | SRCS = $(wildcard *.c) 51 | OBJECTS = $(SRCS:.c=.o) 52 | HEADERS = $(wildcard *.h) 53 | 54 | %.o: %.c $(HEADERS) 55 | $(CC) -c -o $@ $< $(CFLAGS) $(FUSE_CFLAGS) $(ZLIB_CFLAGS) $(LIBWAIVE_CFLAGS) 56 | 57 | $(PROG): $(OBJECTS) 58 | $(CC) -o $@ $^ $(LDFLAGS) $(FUSE_LIBS) $(ZLIB_LIBS) $(LIBWAIVE_LIBS) 59 | 60 | test: $(PROG) 61 | sh test.sh 62 | 63 | clean: 64 | rm -f $(PROG) $(OBJECTS) 65 | 66 | install: $(PROG) 67 | install -D -m 755 $(PROG) $(DESTDIR)/$(SBIN_DIR)/$(PROG) 68 | install -D -m 644 $(PROG).8 $(DESTDIR)/$(MAN_DIR)/man8/$(PROG).8 69 | install -D -m 644 README $(DESTDIR)/$(DOC_DIR)/README 70 | install -m 644 AUTHORS $(DESTDIR)/$(DOC_DIR)/AUTHORS 71 | install -m 644 COPYING $(DESTDIR)/$(DOC_DIR)/COPYING 72 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # this file is part of luufs. 4 | # 5 | # Copyright (c) 2014, 2015 Dima Krasner 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | # THE SOFTWARE. 24 | 25 | start_test() { 26 | echo -n "$1 ..." 27 | sleep 1 28 | } 29 | 30 | end_test() { 31 | if [ 0 -eq $1 ] 32 | then 33 | echo " OK" 34 | else 35 | echo " failed" 36 | exit 1 37 | fi 38 | } 39 | 40 | cleanup() { 41 | umount -l union 2>/dev/null 42 | rm -rf union rw ro 2>/dev/null 43 | } 44 | 45 | mkdir ro rw union 46 | trap cleanup EXIT 47 | trap cleanup INT 48 | trap cleanup TERM 49 | 50 | here="$(pwd)" 51 | ./luufs "$here/ro" "$here/rw" "$here/union" & 52 | 53 | start_test "Missing file stat" 54 | ls union/dir 2>/dev/null 55 | [ 0 -eq $? ] && end_test 1 || end_test 0 56 | 57 | start_test "Duplicate file stat" 58 | cp /bin/sh ro/f 59 | cp /bin/sh rw/f 60 | echo >> rw/f 61 | good_size="$(du -b -D /bin/sh | awk '{print $1}')" 62 | size="$(du -b union/f | awk '{print $1}')" 63 | rm rw/f 64 | rm ro/f 65 | [ "$size" = "$good_size" ] && end_test 0 || end_test 1 66 | 67 | start_test "Directory creation" 68 | mkdir union/dir 69 | end_test $? 70 | 71 | start_test "Existing file stat" 72 | ls union/dir > /dev/null 73 | end_test $? 74 | 75 | start_test "Directory deletion" 76 | rmdir union/dir 77 | end_test $? 78 | 79 | start_test "File deletion" 80 | cp /bin/sh union/f 81 | rm union/f 82 | end_test $? 83 | 84 | start_test "Read-only file deletion" 85 | cp /bin/sh ro/f 86 | rm union/f 2>/dev/null 87 | ret=$? 88 | rm -f ro/f 89 | [ 0 -eq $ret ] && end_test 1 || end_test 0 90 | 91 | start_test "Writable file deletion" 92 | cp /bin/sh union/f 93 | rm union/f 94 | end_test $? 95 | 96 | start_test "Duplicate file deletion" 97 | cp /bin/sh ro/f 98 | cp /bin/sh rw/f 99 | rm union/f 2>/dev/null 100 | ret=$? 101 | rm -f rw/f 102 | rm -f ro/f 103 | [ 0 -eq $ret ] && end_test 1 || end_test 0 104 | 105 | start_test "Existing directory creation" 106 | mkdir rw/dir 107 | mkdir union/dir 2>/dev/null 108 | ret=$? 109 | rmdir rw/dir 110 | [ 0 -eq $ret ] && end_test 1 || end_test 0 111 | 112 | start_test "Missing directory deletion" 113 | rm union/dir 2>/dev/null 114 | [ 0 -eq $? ] && end_test 1 || end_test 0 115 | 116 | start_test "Read-only directory deletion" 117 | mkdir ro/dir 118 | rmdir union/dir 2>/dev/null 119 | ret=$? 120 | rmdir ro/dir 121 | [ 0 -eq $ret ] && end_test 1 || end_test 0 122 | 123 | start_test "Writeable directory deletion" 124 | mkdir union/dir 125 | rmdir union/dir 126 | end_test $? 127 | 128 | start_test "File truncation" 129 | cp /bin/sh union/f 130 | truncate --size 0 union/f 131 | ret=$? 132 | rm union/f 133 | end_test $ret 134 | 135 | start_test "Read-only file truncation" 136 | cp /bin/sh ro/f 137 | truncate --size 0 union/f 2>/dev/null 138 | ret=$? 139 | rm ro/f 140 | [ 0 -eq $ret ] && end_test 1 || end_test 0 141 | 142 | start_test "Missing file truncation" 143 | truncate --size 0 union/f 144 | ret=$? 145 | rm union/f 146 | end_test $ret 147 | 148 | start_test "Contents listing" 149 | mkdir ro/dir 150 | touch rw/file 151 | output="$(ls union/)" 152 | rmdir ro/dir 153 | rm -f rw/file 154 | case "$output" in 155 | *file*) 156 | case "$output" in 157 | *dir*) 158 | end_test 0 159 | ;; 160 | *) 161 | end_test 1 162 | ;; 163 | esac 164 | ;; 165 | *) 166 | end_test 1 167 | ;; 168 | esac 169 | 170 | start_test "Missing directory contents listing" 171 | ls union/dir 2>/dev/null 172 | [ 0 -eq $? ] && end_test 1 || end_test 0 173 | 174 | start_test "Duplicate file directory contents listing" 175 | mkdir ro/dir 176 | mkdir rw/dir 177 | output="$(ls union/)" 178 | rmdir rw/dir 179 | rmdir ro/dir 180 | [ "dir" = "$output" ] && end_test 0 || end_test 1 181 | 182 | start_test "Symlink creation" 183 | ln -s x union/y 184 | ret=$? 185 | rm union/y 186 | end_test $ret 187 | 188 | start_test "Existing symlink creation" 189 | ln -s x ro/y 190 | ln -s w union/y 2>/dev/null 191 | ret=$? 192 | rm ro/y 193 | [ 0 -eq $ret ] && end_test 1 || end_test 0 194 | 195 | start_test "Symlink deletion" 196 | ln -s x union/y 197 | rm union/y 198 | end_test $? 199 | 200 | start_test "Duplicate symlink dereferencing" 201 | ln -s a ro/y 202 | ln -s b rw/y 203 | output="$(readlink union/y)" 204 | rm rw/y 205 | rm ro/y 206 | [ "a" = "$output" ] && end_test 0 || end_test 1 207 | 208 | echo "All tests passed!" 209 | -------------------------------------------------------------------------------- /luufs.c: -------------------------------------------------------------------------------- 1 | /* 2 | * this file is part of luufs. 3 | * 4 | * Copyright (c) 2014, 2015 Dima Krasner 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | 37 | #include 38 | #define FUSE_USE_VERSION (26) 39 | #include 40 | 41 | #ifdef HAVE_WAIVE 42 | # include 43 | #endif 44 | 45 | #define DIRENT_MAX 255 46 | 47 | struct luufs_ctx { 48 | uLong init; 49 | int (*openat)(int, const char *, int, ...); 50 | int (*unlinkat)(int, const char *, int); 51 | int (*fchownat)(int, const char *, uid_t, gid_t, int); 52 | int (*mkdirat)(int, const char *, mode_t); 53 | int (*mknodat)(int, const char *, mode_t, dev_t); 54 | int (*renameat)(int, const char *, int, const char *); 55 | int (*symlinkat)(const char *, int, const char *); 56 | int (*utimensat)(int, const char *, const struct timespec[2], int); 57 | int (*fstatat)(int, const char *, struct stat *, int); 58 | int ro; 59 | int rw; 60 | }; 61 | 62 | struct luufs_dir_ctx { 63 | DIR *dirs[2]; 64 | int fds[2]; 65 | }; 66 | 67 | #define f_ro fds[0] 68 | #define f_rw fds[1] 69 | 70 | #define d_ro dirs[0] 71 | #define d_rw dirs[1] 72 | 73 | /* since luufs runs as root, no permission checks are performed (in other words: 74 | * the process that actually calls the *at() system calls is luufs, which runs 75 | * as root), so when the calling process runs as an unprivileged user, return 76 | * EPERM in errno */ 77 | #define LUUFS_CALL_HEAD() \ 78 | const struct luufs_ctx *ctx; \ 79 | const struct fuse_context *fuse_ctx; \ 80 | \ 81 | fuse_ctx = fuse_get_context(); \ 82 | ctx = (const struct luufs_ctx *) fuse_ctx->private_data; \ 83 | \ 84 | if ((0 != fuse_ctx->uid) || (0 != fuse_ctx->gid)) \ 85 | return -EPERM 86 | 87 | static int luufs_open(const char *name, struct fuse_file_info *fi) 88 | { 89 | struct stat stbuf; 90 | int fd; 91 | 92 | LUUFS_CALL_HEAD(); 93 | 94 | /* when a file is opened for reading, prefer the read-only directory */ 95 | if ((0 == (O_WRONLY & fi->flags)) && (0 == (O_RDWR & fi->flags))) { 96 | fd = ctx->openat(ctx->ro, &name[1], fi->flags); 97 | if (-1 != fd) 98 | goto ok; 99 | if (ENOENT != errno) 100 | return -errno; 101 | } 102 | 103 | /* return EROFS in errno if it's an attempt to overwrite a file under the 104 | * read-only directory */ 105 | if (0 == ctx->fstatat(ctx->ro, 106 | &name[1], 107 | &stbuf, 108 | AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW)) 109 | return -EROFS; 110 | if (ENOENT != errno) 111 | return -errno; 112 | 113 | fd = ctx->openat(ctx->rw, &name[1], fi->flags); 114 | if (-1 == fd) 115 | return -errno; 116 | 117 | ok: 118 | /* make sure the file descriptor does not exceed INT_MAX, to prevent 119 | * truncation when casting the uint64_t back to int later */ 120 | if (INT_MAX < fd) { 121 | (void) close(fd); 122 | return -EMFILE; 123 | } 124 | 125 | fi->fh = (uint64_t) fd; 126 | 127 | return 0; 128 | } 129 | 130 | static int luufs_create(const char *name, 131 | mode_t mode, 132 | struct fuse_file_info *fi) 133 | { 134 | struct stat stbuf; 135 | int ret; 136 | int fd; 137 | 138 | LUUFS_CALL_HEAD(); 139 | 140 | /* if the file already exists, return EEXIST in errno */ 141 | if (0 == ctx->fstatat(ctx->ro, 142 | &name[1], 143 | &stbuf, 144 | AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW)) { 145 | ret = -EEXIST; 146 | goto out; 147 | } 148 | 149 | fd = ctx->openat(ctx->rw, &name[1], O_CREAT | O_EXCL | fi->flags, mode); 150 | if (-1 == fd) { 151 | ret = -errno; 152 | goto out; 153 | } 154 | 155 | if (INT_MAX < fd) { 156 | ret = -EMFILE; 157 | goto close_fd; 158 | } 159 | 160 | /* change the file owner, using the calling process credentials */ 161 | if (-1 == fchown(fd, fuse_ctx->uid, fuse_ctx->gid)) { 162 | ret = -errno; 163 | goto close_fd; 164 | } 165 | 166 | fi->fh = (uint64_t) fd; 167 | 168 | return 0; 169 | 170 | close_fd: 171 | (void) close(fd); 172 | 173 | out: 174 | return ret; 175 | } 176 | 177 | static int luufs_close(const char *name, struct fuse_file_info *fi) 178 | { 179 | int fd; 180 | 181 | fd = (int) fi->fh; 182 | if (-1 == fd) 183 | return -EBADF; 184 | 185 | if (0 == close(fd)) { 186 | fi->fh = -1; 187 | return 0; 188 | } 189 | 190 | return -errno; 191 | } 192 | 193 | static int luufs_truncate(const char *name, off_t size) 194 | { 195 | struct stat stbuf; 196 | int fd; 197 | int ret; 198 | 199 | LUUFS_CALL_HEAD(); 200 | 201 | /* if the file exists under the read-only directory, return EROFS in 202 | * errno */ 203 | if (0 == ctx->fstatat(ctx->ro, 204 | &name[1], 205 | &stbuf, 206 | AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW)) { 207 | ret = -EROFS; 208 | goto out; 209 | } 210 | 211 | /* try to open the file - if it's missing, create it */ 212 | fd = ctx->openat(ctx->rw, &name[1], O_WRONLY); 213 | if (-1 == fd) { 214 | if (ENOENT == errno) { 215 | fd = ctx->openat(ctx->rw, &name[1], O_WRONLY | O_CREAT | O_EXCL); 216 | if (-1 != fd) 217 | goto trunc; 218 | } 219 | 220 | ret = -errno; 221 | goto out; 222 | } 223 | 224 | trunc: 225 | ret = ftruncate(fd, size); 226 | if (0 != ret) 227 | ret = -errno; 228 | 229 | (void) close(fd); 230 | 231 | out: 232 | return ret; 233 | } 234 | 235 | static int luufs_stat(const char *name, struct stat *stbuf) 236 | { 237 | LUUFS_CALL_HEAD(); 238 | 239 | /* try the read-only directory first */ 240 | if (0 == ctx->fstatat(ctx->ro, 241 | &name[1], 242 | stbuf, 243 | AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW)) 244 | return 0; 245 | if (ENOENT != errno) 246 | return -errno; 247 | 248 | if (0 == ctx->fstatat(ctx->rw, 249 | &name[1], 250 | stbuf, 251 | AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW)) 252 | return 0; 253 | 254 | return -errno; 255 | } 256 | 257 | static int luufs_access(const char *name, int mask) 258 | { 259 | struct stat stbuf; 260 | const char *namep; 261 | 262 | LUUFS_CALL_HEAD(); 263 | 264 | if (0 == strcmp("/", name)) 265 | namep = name; 266 | else 267 | namep = &name[1]; 268 | 269 | if (0 != (F_OK & mask)) 270 | return luufs_stat(namep, &stbuf); 271 | 272 | /* perform all access checks except W_OK on the read-only directory; musl 273 | * does not support AT_SYMLINK_NOFOLLOW so we don't pass this flag */ 274 | if (0 != (W_OK & mask)) { 275 | if (-1 == faccessat(ctx->rw, namep, mask, 0)) 276 | return -errno; 277 | } 278 | else { 279 | if (-1 == faccessat(ctx->ro, namep, mask, 0)) 280 | return -errno; 281 | } 282 | 283 | return 0; 284 | } 285 | 286 | static int luufs_read(const char *path, 287 | char *buf, 288 | size_t size, 289 | off_t off, 290 | struct fuse_file_info *fi) 291 | { 292 | ssize_t ret; 293 | int fd; 294 | 295 | fd = (int) fi->fh; 296 | if (-1 == fd) 297 | return -EBADF; 298 | 299 | ret = pread(fd, buf, size, off); 300 | if (-1 == ret) 301 | return -errno; 302 | 303 | return (int) ret; 304 | } 305 | 306 | static int luufs_write(const char *path, 307 | const char *buf, 308 | size_t size, 309 | off_t off, 310 | struct fuse_file_info *fi) 311 | { 312 | ssize_t ret; 313 | int fd; 314 | 315 | fd = (int) fi->fh; 316 | if (-1 == fd) 317 | return -EBADF; 318 | 319 | ret = pwrite(fd, buf, size, off); 320 | if (-1 == ret) 321 | return -errno; 322 | 323 | return (int) ret; 324 | } 325 | 326 | static int luufs_unlink(const char *name) 327 | { 328 | struct stat stbuf; 329 | 330 | LUUFS_CALL_HEAD(); 331 | 332 | /* if the file exists under the read-only directory, return EROFS in 333 | * errno */ 334 | if (0 == ctx->fstatat(ctx->ro, 335 | &name[1], 336 | &stbuf, 337 | AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW)) 338 | return -EROFS; 339 | if (ENOENT != errno) 340 | return -errno; 341 | 342 | if (0 == ctx->unlinkat(ctx->rw, &name[1], 0)) 343 | return 0; 344 | 345 | return -errno; 346 | } 347 | 348 | static int luufs_mkdir(const char *name, mode_t mode) 349 | { 350 | struct stat stbuf; 351 | 352 | LUUFS_CALL_HEAD(); 353 | 354 | /* if the directory exists under the read-only directory, return EEXIST in 355 | * errno */ 356 | if (0 == ctx->fstatat(ctx->ro, 357 | &name[1], 358 | &stbuf, 359 | AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW)) 360 | return -EEXIST; 361 | if (ENOENT != errno) 362 | return -errno; 363 | 364 | if (-1 == ctx->mkdirat(ctx->rw, &name[1], mode)) 365 | return -errno; 366 | 367 | if (-1 == ctx->fchownat(ctx->rw, 368 | &name[1], 369 | fuse_ctx->uid, 370 | fuse_ctx->gid, 371 | AT_SYMLINK_NOFOLLOW)) { 372 | (void) ctx->unlinkat(ctx->rw, &name[1], AT_REMOVEDIR); 373 | return -errno; 374 | } 375 | 376 | return 0; 377 | } 378 | 379 | static int luufs_rmdir(const char *name) 380 | { 381 | struct stat stbuf; 382 | 383 | LUUFS_CALL_HEAD(); 384 | 385 | /* if the directory exists under the read-only directory, return EROFS in 386 | * errno */ 387 | if (0 == ctx->fstatat(ctx->ro, 388 | &name[1], 389 | &stbuf, 390 | AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW)) 391 | return -EROFS; 392 | if (ENOENT != errno) 393 | return -errno; 394 | 395 | if (0 == ctx->unlinkat(ctx->rw, &name[1], AT_REMOVEDIR)) 396 | return 0; 397 | 398 | return -errno; 399 | } 400 | 401 | static int luufs_opendir(const char *name, struct fuse_file_info *fi) 402 | { 403 | int ret; 404 | int cmp; 405 | struct luufs_dir_ctx *dir_ctx; 406 | 407 | LUUFS_CALL_HEAD(); 408 | 409 | dir_ctx = malloc(sizeof(*dir_ctx)); 410 | if (NULL == dir_ctx) { 411 | ret = -ENOMEM; 412 | goto end; 413 | } 414 | 415 | ctx = (const struct luufs_ctx *) fuse_get_context()->private_data; 416 | 417 | cmp = strcmp("/", name); 418 | if (0 == cmp) 419 | dir_ctx->f_ro = dup(ctx->ro); 420 | else 421 | dir_ctx->f_ro = ctx->openat(ctx->ro, &name[1], O_DIRECTORY); 422 | if (-1 == dir_ctx->f_ro) { 423 | if (ENOENT != errno) { 424 | ret = -errno; 425 | goto free_ctx; 426 | } 427 | } 428 | 429 | if (-1 == ctx->rw) 430 | dir_ctx->f_rw = -1; 431 | else { 432 | if (0 == cmp) 433 | dir_ctx->f_rw = dup(ctx->rw); 434 | else 435 | dir_ctx->f_rw = ctx->openat(ctx->rw, &name[1], O_DIRECTORY); 436 | if (-1 == dir_ctx->f_rw) { 437 | ret = -errno; 438 | goto close_ro; 439 | } 440 | } 441 | 442 | if (-1 == dir_ctx->f_ro) 443 | dir_ctx->d_ro = NULL; 444 | else { 445 | dir_ctx->d_ro = fdopendir(dir_ctx->f_ro); 446 | if (NULL == dir_ctx->d_ro) { 447 | ret = -errno; 448 | goto close_rw; 449 | } 450 | } 451 | 452 | if (-1 == dir_ctx->f_rw) 453 | dir_ctx->d_rw = NULL; 454 | else { 455 | dir_ctx->d_rw = fdopendir(dir_ctx->f_rw); 456 | if (NULL == dir_ctx->d_rw) { 457 | ret = -errno; 458 | goto close_rw; 459 | } 460 | } 461 | 462 | fi->fh = (uint64_t) (uintptr_t) dir_ctx; 463 | 464 | return 0; 465 | 466 | (void) closedir(dir_ctx->d_ro); 467 | dir_ctx->f_ro = -1; 468 | 469 | close_rw: 470 | if (-1 != dir_ctx->f_rw) 471 | (void) close(dir_ctx->f_rw); 472 | 473 | close_ro: 474 | if (-1 != dir_ctx->f_ro) 475 | (void) close(dir_ctx->f_ro); 476 | 477 | free_ctx: 478 | free(dir_ctx); 479 | 480 | end: 481 | return ret; 482 | } 483 | 484 | static int luufs_closedir(const char *name, struct fuse_file_info *fi) 485 | { 486 | struct luufs_dir_ctx *dir_ctx; 487 | unsigned int i; 488 | 489 | dir_ctx = (struct luufs_dir_ctx *) (uintptr_t) fi->fh; 490 | if (NULL == dir_ctx) 491 | return -EBADF; 492 | 493 | for (i = 0; 2 > i; ++i) { 494 | if (NULL != dir_ctx->dirs[i]) { 495 | if (-1 == closedir(dir_ctx->dirs[i])) 496 | return -errno; 497 | } 498 | } 499 | 500 | free(dir_ctx); 501 | 502 | fi->fh = (uint64_t) (uintptr_t) NULL; 503 | 504 | return 0; 505 | } 506 | 507 | static int luufs_readdir(const char *path, 508 | void *buf, 509 | fuse_fill_dir_t filler, 510 | off_t offset, 511 | struct fuse_file_info *fi) 512 | { 513 | uLong *crc; 514 | struct dirent ent; 515 | struct stat stbuf; 516 | struct luufs_dir_ctx *dir_ctx; 517 | const struct luufs_ctx *ctx; 518 | struct dirent *entp; 519 | unsigned int i; 520 | unsigned int j; 521 | unsigned int k; 522 | int ret; 523 | 524 | dir_ctx = (struct luufs_dir_ctx *) (uintptr_t) fi->fh; 525 | if (NULL == dir_ctx) { 526 | ret = -EBADF; 527 | goto end; 528 | } 529 | 530 | crc = malloc(DIRENT_MAX * sizeof(*crc)); 531 | if (NULL == crc) { 532 | ret = -ENOMEM; 533 | goto end; 534 | } 535 | 536 | if (0 == offset) { 537 | for (i = 0; 2 > i; ++i) { 538 | if (NULL != dir_ctx->dirs[i]) 539 | rewinddir(dir_ctx->dirs[i]); 540 | } 541 | } 542 | 543 | ctx = (const struct luufs_ctx *) fuse_get_context()->private_data; 544 | 545 | ret = 0; 546 | j = 0; 547 | for (i = 0; 2 > i; ++i) { 548 | if (NULL == dir_ctx->dirs[i]) 549 | continue; 550 | 551 | do { 552 | next: 553 | if (0 != readdir_r(dir_ctx->dirs[i], &ent, &entp)) { 554 | ret = -errno; 555 | goto free_crc; 556 | } 557 | if (NULL == entp) { 558 | ret = 0; 559 | break; 560 | } 561 | 562 | if (DIRENT_MAX == j) { 563 | ret = -ENOMEM; 564 | goto free_crc; 565 | } 566 | 567 | crc[j] = crc32(ctx->init, 568 | (const Bytef *) entp->d_name, 569 | strlen(entp->d_name)); 570 | for (k = 0; j > k; ++k) { 571 | if (crc[k] == crc[j]) 572 | goto next; 573 | } 574 | 575 | if (0 != fstatat(dir_ctx->fds[i], 576 | entp->d_name, 577 | &stbuf, 578 | AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW)) { 579 | ret = -errno; 580 | goto free_crc; 581 | } 582 | 583 | if (1 == filler(buf, entp->d_name, &stbuf, 0)) { 584 | ret = -ENOMEM; 585 | goto free_crc; 586 | } 587 | 588 | ++j; 589 | } while (1); 590 | } 591 | 592 | free_crc: 593 | free(crc); 594 | 595 | end: 596 | return ret; 597 | } 598 | 599 | static int luufs_symlink(const char *to, const char *from) 600 | { 601 | struct stat stbuf; 602 | 603 | LUUFS_CALL_HEAD(); 604 | 605 | /* if the link source exists under the read-only directory, return EEXIST in 606 | * errno */ 607 | if (0 == ctx->fstatat(ctx->ro, 608 | &from[1], 609 | &stbuf, 610 | AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW)) 611 | return -EEXIST; 612 | if (ENOENT != errno) 613 | return -errno; 614 | 615 | if (-1 == ctx->symlinkat(to, ctx->rw, &from[1])) 616 | return -errno; 617 | 618 | if (-1 == ctx->fchownat(ctx->rw, 619 | &from[1], 620 | fuse_ctx->uid, 621 | fuse_ctx->gid, 622 | AT_SYMLINK_NOFOLLOW)) { 623 | (void) ctx->unlinkat(ctx->rw, &from[1], AT_REMOVEDIR); 624 | return -errno; 625 | } 626 | 627 | return 0; 628 | } 629 | 630 | static int luufs_readlink(const char *name, char *buf, size_t size) 631 | { 632 | int len; 633 | 634 | LUUFS_CALL_HEAD(); 635 | 636 | len = readlinkat(ctx->ro, &name[1], buf, size - 1); 637 | if (-1 != len) 638 | goto nul; 639 | if (ENOENT != errno) 640 | return -errno; 641 | 642 | len = readlinkat(ctx->rw, &name[1], buf, size - 1); 643 | if (-1 == len) 644 | return -errno; 645 | 646 | nul: 647 | buf[len] = '\0'; 648 | 649 | return 0; 650 | } 651 | 652 | static int luufs_mknod(const char *name, mode_t mode, dev_t dev) 653 | { 654 | struct stat stbuf; 655 | 656 | LUUFS_CALL_HEAD(); 657 | 658 | /* if the device exists under the read-only directory, return EROFS in 659 | * errno */ 660 | if (0 == ctx->fstatat(ctx->ro, 661 | &name[1], 662 | &stbuf, 663 | AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW)) 664 | return -EROFS; 665 | if (ENOENT != errno) 666 | return -errno; 667 | 668 | if (-1 == ctx->mknodat(ctx->rw, 669 | &name[1], 670 | mode, 671 | dev)) 672 | return -errno; 673 | 674 | return 0; 675 | } 676 | 677 | static int luufs_chmod(const char *name, mode_t mode) 678 | { 679 | struct stat stbuf; 680 | 681 | LUUFS_CALL_HEAD(); 682 | 683 | /* if the file exists under the read-only directory, return EROFS in 684 | * errno */ 685 | if (0 == ctx->fstatat(ctx->ro, 686 | &name[1], 687 | &stbuf, 688 | AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW)) 689 | return -EROFS; 690 | if (ENOENT != errno) 691 | return -errno; 692 | 693 | if (-1 == fchmodat(ctx->rw, &name[1], mode, AT_SYMLINK_NOFOLLOW)) 694 | return -errno; 695 | 696 | return 0; 697 | } 698 | 699 | static int luufs_chown(const char *name, uid_t uid, gid_t gid) 700 | { 701 | struct stat stbuf; 702 | 703 | LUUFS_CALL_HEAD(); 704 | 705 | /* if the file exists under the read-only directory, return EROFS in 706 | * errno */ 707 | if (0 == ctx->fstatat(ctx->ro, 708 | &name[1], 709 | &stbuf, 710 | AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW)) 711 | return -EROFS; 712 | if (ENOENT != errno) 713 | return -errno; 714 | 715 | if (-1 == ctx->fchownat(ctx->rw, 716 | &name[1], 717 | uid, 718 | gid, 719 | AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW)) 720 | return -errno; 721 | 722 | return 0; 723 | } 724 | 725 | static int luufs_utimens(const char *name, const struct timespec tv[2]) 726 | { 727 | struct stat stbuf; 728 | 729 | LUUFS_CALL_HEAD(); 730 | 731 | /* if the file exists under the read-only directory, return EROFS in 732 | * errno */ 733 | if (0 == ctx->fstatat(ctx->ro, 734 | &name[1], 735 | &stbuf, 736 | AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW)) 737 | return -EROFS; 738 | if (ENOENT != errno) 739 | return -errno; 740 | 741 | if (-1 == ctx->utimensat(ctx->rw, &name[1], tv, AT_SYMLINK_NOFOLLOW)) 742 | return -errno; 743 | 744 | return 0; 745 | } 746 | 747 | static int luufs_rename(const char *oldpath, const char *newpath) 748 | { 749 | struct stat stbuf; 750 | 751 | LUUFS_CALL_HEAD(); 752 | 753 | /* if the file belongs to the read-only directory, return EROFS in errno */ 754 | if (0 == ctx->fstatat(ctx->ro, 755 | &oldpath[1], 756 | &stbuf, 757 | AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW)) 758 | return -EROFS; 759 | if (ENOENT != errno) 760 | return -errno; 761 | 762 | /* if the destination exists under the read-only directory, return EEXIST in 763 | * errno */ 764 | if (0 == ctx->fstatat(ctx->ro, 765 | &newpath[1], 766 | &stbuf, 767 | AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW)) 768 | return -EEXIST; 769 | if (ENOENT != errno) 770 | return -errno; 771 | 772 | if (-1 == ctx->renameat(ctx->rw, 773 | &oldpath[1], 774 | ctx->rw, 775 | &newpath[1])) 776 | return -errno; 777 | 778 | return 0; 779 | } 780 | 781 | static struct fuse_operations luufs_oper = { 782 | .open = luufs_open, 783 | .create = luufs_create, 784 | .release = luufs_close, 785 | 786 | .truncate = luufs_truncate, 787 | 788 | .read = luufs_read, 789 | .write = luufs_write, 790 | 791 | .getattr = luufs_stat, 792 | .access = luufs_access, 793 | 794 | .unlink = luufs_unlink, 795 | 796 | .mkdir = luufs_mkdir, 797 | .rmdir = luufs_rmdir, 798 | 799 | .opendir = luufs_opendir, 800 | .releasedir = luufs_closedir, 801 | .readdir = luufs_readdir, 802 | 803 | .symlink = luufs_symlink, 804 | .readlink = luufs_readlink, 805 | 806 | .mknod = luufs_mknod, 807 | 808 | .chmod = luufs_chmod, 809 | .chown = luufs_chown, 810 | .utimens = luufs_utimens, 811 | .rename = luufs_rename 812 | }; 813 | 814 | static int mirror_dirs(const int src, const int dest) { 815 | struct stat stbuf; 816 | struct dirent ent; 817 | DIR *dir; 818 | struct dirent *entp; 819 | int nsrc; 820 | int ndest; 821 | int ret; 822 | 823 | dir = fdopendir(src); 824 | if (NULL == dir) { 825 | ret = -1; 826 | goto end; 827 | } 828 | 829 | do { 830 | if (0 != readdir_r(dir, &ent, &entp)) { 831 | ret = -1; 832 | break; 833 | } 834 | if (NULL == entp) { 835 | ret = 0; 836 | break; 837 | } 838 | 839 | if (DT_DIR != entp->d_type) 840 | continue; 841 | 842 | if ((0 == strcmp(".", entp->d_name)) || 843 | (0 == strcmp("..", entp->d_name))) 844 | continue; 845 | 846 | if (-1 == fstatat(src, entp->d_name, &stbuf, 0)) { 847 | ret = -1; 848 | break; 849 | } 850 | 851 | if (-1 == mkdirat(dest, entp->d_name, stbuf.st_mode)) { 852 | if (EEXIST != errno) { 853 | ret = -1; 854 | break; 855 | } 856 | } 857 | 858 | nsrc = openat(src, entp->d_name, O_DIRECTORY); 859 | if (-1 == nsrc) { 860 | ret = -1; 861 | break; 862 | } 863 | 864 | ndest = openat(dest, entp->d_name, O_DIRECTORY); 865 | if (-1 == ndest) { 866 | (void) close(nsrc); 867 | ret = -1; 868 | break; 869 | } 870 | 871 | if (-1 == mirror_dirs(nsrc, ndest)) { 872 | (void) close(ndest); 873 | (void) close(nsrc); 874 | ret = -1; 875 | break; 876 | } 877 | } while (1); 878 | 879 | (void) closedir(dir); 880 | 881 | end: 882 | return ret; 883 | } 884 | 885 | static int openat_stub(int dirfd, const char *pathname, int flags, ...) 886 | { 887 | va_list ap; 888 | int ret; 889 | 890 | if (0 != ((O_CREAT | O_WRONLY | O_RDWR) & flags)) 891 | return -EROFS; 892 | 893 | va_start(ap, flags); 894 | ret = openat(dirfd, pathname, flags, va_arg(ap, mode_t)); 895 | va_end(ap); 896 | return ret; 897 | } 898 | 899 | static int unlinkat_stub(int dirfd, const char *pathname, int flags) 900 | { 901 | errno = EROFS; 902 | return -1; 903 | } 904 | 905 | static int fchownat_stub(int dirfd, 906 | const char *pathname, 907 | uid_t owner, 908 | gid_t group, 909 | int flags) 910 | { 911 | errno = EROFS; 912 | return -1; 913 | } 914 | 915 | static int mkdirat_stub(int dirfd, const char *pathname, mode_t mode) 916 | { 917 | errno = EROFS; 918 | return -1; 919 | } 920 | 921 | static int mknodat_stub(int dirfd, const char *pathname, mode_t mode, dev_t dev) 922 | { 923 | errno = EROFS; 924 | return -1; 925 | } 926 | 927 | static int renameat_stub(int olddirfd, 928 | const char *oldpath, 929 | int newdirfd, 930 | const char *newpath) 931 | { 932 | errno = EROFS; 933 | return -1; 934 | } 935 | 936 | static int symlinkat_stub(const char *oldpath, 937 | int newdirfd, 938 | const char *newpath) 939 | { 940 | errno = EROFS; 941 | return -1; 942 | } 943 | 944 | static int utimensat_stub(int dirfd, 945 | const char *pathname, 946 | const struct timespec times[2], 947 | int flags) 948 | { 949 | errno = EROFS; 950 | return -1; 951 | } 952 | 953 | static int fstatat_stub(int dirfd, 954 | const char *pathname, 955 | struct stat *buf, 956 | int flags) 957 | { 958 | if (-1 == dirfd) { 959 | errno = ENOENT; 960 | return -1; 961 | } 962 | 963 | return fstatat(dirfd, pathname, buf, flags); 964 | } 965 | 966 | int main(int argc, char *argv[]) 967 | { 968 | char *fuse_argv[4]; 969 | struct luufs_ctx ctx; 970 | int ret; 971 | int fd; 972 | 973 | if ((3 != argc) && (4 != argc)) { 974 | (void) fprintf(stderr, "Usage: %s RO [RW] TARGET\n", argv[0]); 975 | ret = EXIT_FAILURE; 976 | goto out; 977 | } 978 | 979 | #ifdef HAVE_WAIVE 980 | if (-1 == waive(WAIVE_INET | WAIVE_PACKET | WAIVE_KILL)) { 981 | ret = EXIT_FAILURE; 982 | goto out; 983 | } 984 | #endif 985 | 986 | /* open both directories, so we can pass their file descriptors to the 987 | * *at() system calls later */ 988 | ctx.ro = open(argv[1], O_DIRECTORY); 989 | if (-1 == ctx.ro) { 990 | ret = EXIT_FAILURE; 991 | goto out; 992 | } 993 | 994 | if (3 == argc) { 995 | ctx.rw = -1; 996 | fuse_argv[1] = argv[2]; 997 | 998 | /* use stubs that fail with EROFS instead of real system calls that may 999 | * alter the read-only directory */ 1000 | ctx.openat = openat_stub; 1001 | ctx.unlinkat = unlinkat_stub; 1002 | ctx.fchownat = fchownat_stub; 1003 | ctx.mkdirat = mkdirat_stub; 1004 | ctx.mknodat = mknodat_stub; 1005 | ctx.renameat = renameat_stub; 1006 | ctx.symlinkat = symlinkat_stub; 1007 | ctx.utimensat = utimensat_stub; 1008 | ctx.fstatat = fstatat_stub; 1009 | } 1010 | else { 1011 | ctx.rw = open(argv[2], O_DIRECTORY); 1012 | if (-1 == ctx.rw) { 1013 | ret = EXIT_FAILURE; 1014 | goto close_ro; 1015 | } 1016 | 1017 | /* mirror the read-only directory tree under the writeable directory */ 1018 | fd = dup(ctx.ro); 1019 | if (-1 == fd) { 1020 | ret = EXIT_FAILURE; 1021 | goto close_ro; 1022 | } 1023 | ret = mirror_dirs(fd, ctx.rw); 1024 | if (-1 == ret) { 1025 | (void) close(fd); 1026 | ret = EXIT_FAILURE; 1027 | goto close_ro; 1028 | } 1029 | 1030 | ctx.openat = openat; 1031 | ctx.unlinkat = unlinkat; 1032 | ctx.fchownat = fchownat; 1033 | ctx.mkdirat = mkdirat; 1034 | ctx.mknodat = mknodat; 1035 | ctx.renameat = renameat; 1036 | ctx.symlinkat = symlinkat; 1037 | ctx.utimensat = utimensat; 1038 | ctx.fstatat = fstatat; 1039 | 1040 | fuse_argv[1] = argv[3]; 1041 | } 1042 | 1043 | ctx.init = crc32(0L, Z_NULL, 0); 1044 | fuse_argv[0] = argv[0]; 1045 | fuse_argv[2] = "-ononempty,suid,dev,allow_other,default_permissions"; 1046 | fuse_argv[3] = NULL; 1047 | ret = fuse_main(3, fuse_argv, &luufs_oper, &ctx); 1048 | 1049 | if (-1 != ctx.rw) 1050 | (void) close(ctx.rw); 1051 | 1052 | close_ro: 1053 | (void) close(ctx.ro); 1054 | 1055 | out: 1056 | return ret; 1057 | } 1058 | --------------------------------------------------------------------------------