Class: HomeFS
- Inherits:
-
Object
- Object
- HomeFS
- Defined in:
- lib/homefs/homefs.rb,
lib/homefs/debug.rb
Overview
An instance of HomeFS; this implements all FuseFS methods, and ultimately handles reading, writing, and inspection of files.
We rely on the context provided to us by FUSE to determine the calling user’s UID and GID.
Currently, HomeFS does not support POSIX locking (which has little effect in practice), and special files (those created by mknod/FIFOs). We do supported extended filesystem attributes, but only if you have the ‘ffi-xattr’ gem.
Defined Under Namespace
Classes: Wrapper
Instance Method Summary collapse
-
#access(context, path, mode) ⇒ void
abstract
Check access permissions.
-
#check_listable(uid, relpath) ⇒ void
This method raises an Errno::EACCES if the user given by uid cannot list the given directory, i.e., if the user does not have the execute and read permissions on the given directory.
-
#check_owner(uid, relpath) ⇒ void
This method raises an Errno::EACCES if the user given by uid is not the owner of the file described by relpath HomeFS (not the actual filesystem) path.
-
#check_readable(uid, relpath) ⇒ void
This method raises an Errno::EACCES if the user given by uid cannot read from the given path.
-
#check_writable(uid, relpath) ⇒ void
This method raises an Errno::EACCES if the user given by uid cannot write to the given path.
-
#chmod(context, path, mode) ⇒ void
abstract
Change file permissions.
-
#chown(context, path, uid, gid) ⇒ void
abstract
Change file ownership.
-
#create(context, path, mode, ffi) ⇒ void
abstract
Create and open a file If the file does not exist, first create it with the specified mode, and then open it.
-
#executable?(path, uid = nil) ⇒ Boolean
Test whether the user described by uid can execute the given path.
-
#fgetattr(context, path, ffi) ⇒ Stat
abstract
Get attributes of an open file.
-
#flush(context, path, ffi) ⇒ void
abstract
Possibly flush cached data BIG NOTE: This is not equivalent to fsync().
-
#fsync(context, path, datasync, ffi) ⇒ void
abstract
Synchronize file contents.
-
#ftruncate(context, path, size, ffi) ⇒ void
abstract
Change the size of an open file.
-
#getattr(context, path) ⇒ Stat
abstract
Get file attributes.
-
#homepath(uid, path = nil) ⇒ String
Return the path to the root of HomeFS relative to the underlying filesystem.
-
#init(info) ⇒ void
abstract
Called when filesystem is initialised.
-
#initialize(path, options = Hash.new) ⇒ HomeFS
constructor
Creates a new instance of HomeFS.
-
#link(context, from, to) ⇒ void
abstract
Create a hard link to file.
- #mkdir(context, path, mode) ⇒ Object
-
#mode_mask(file, mask, uid = nil) ⇒ Boolean
Check whether the given file has a mode that matches the given mask.
-
#open(context, path, ffi) ⇒ void
abstract
File open operation file open flags etc.
-
#opendir(context, path, name) ⇒ void
abstract
Open directory Unless the ‘default_permissions’ mount option is given, this method should check if opendir is permitted for this directory.
-
#read(context, path, size, offset, ffi) ⇒ String
abstract
Read data from an open file.
-
#read_group ⇒ void
Read or re-read /etc/group to update the internal table of group membership.
-
#read_passwd ⇒ void
Read or re-read /etc/passwd to update the internal table of home directories for each UID.
-
#readable?(path, uid = nil) ⇒ Boolean
Test whether the user described by uid can read from the given path.
- #readdir(context, path, filler, offset, ffi) ⇒ Object
-
#readlink(context, path, size) ⇒ String
abstract
Resolve target of symbolic link.
-
#release(context, path, ffi) ⇒ void
abstract
Release an open file Release is called when there are no more references to an open file: all file descriptors are closed and all memory mappings are unmapped.
- #rename(context, from, to) ⇒ Object
-
#symlink(context, to, from) ⇒ void
abstract
Create a symbolic link Create a symbolic link named “from” which, when evaluated, will lead to “to”.
-
#truncate(context, path, offset) ⇒ void
abstract
Change the size of a file.
-
#unlink(context, path) ⇒ void
abstract
Remove a file.
-
#utimens(context, path, actime, modtime) ⇒ void
abstract
Change access/modification times of a file.
-
#writable?(path, uid = nil) ⇒ Boolean
Test whether we can write to the given path.
-
#write(context, path, data, offset, ffi) ⇒ Integer
abstract
Write data to an open file.
Constructor Details
#initialize(path, options = Hash.new) ⇒ HomeFS
Creates a new instance of HomeFS. This instance should probably be passed to RFuse, as it does nothing on its own. The path is relative to the calling user’s home directory, that is, the user interacting with the filesystem through open(2), read(2), write(2), etc. the file system.
22 23 24 25 26 |
# File 'lib/homefs/homefs.rb', line 22 def initialize(path, = Hash.new) @relpath = path read_passwd read_group end |
Instance Method Details
#access(context, path, mode) ⇒ void
This method should usually only be called by FUSE, and not called directly
This method returns an undefined value.
Check access permissions
270 271 272 273 274 275 |
# File 'lib/homefs/homefs.rb', line 270 def access(context, path, mode) uid = context.uid unless mode_mask(homepath(uid, path), mode * 0111, uid) raise Errno::EACCES end end |
#check_listable(uid, relpath) ⇒ void
This method returns an undefined value.
This method raises an Errno::EACCES if the user given by uid cannot list the given directory, i.e., if the user does not have the execute and read permissions on the given directory. If the path given is not a directory, the dirname of the given path is tested. HomeFS (not the actual filesystem)
234 235 236 237 238 239 240 241 242 |
# File 'lib/homefs/homefs.rb', line 234 def check_listable(uid, relpath) hpath = homepath(uid, relpath) unless File.directory?(hpath) hpath = File.dirname(hpath) end unless readable?(hpath) && executable?(hpath) fail Errno::EACCES, relpath end end |
#check_owner(uid, relpath) ⇒ void
This method returns an undefined value.
This method raises an Errno::EACCES if the user given by uid is not the owner of the file described by relpath HomeFS (not the actual filesystem) path
253 254 255 256 257 258 |
# File 'lib/homefs/homefs.rb', line 253 def check_owner(uid, relpath) hpath = File.dirname(homepath(uid, relpath)) unless File.stat(hpath).uid == uid fail Errno::EACCES, relpath end end |
#check_readable(uid, relpath) ⇒ void
This method returns an undefined value.
This method raises an Errno::EACCES if the user given by uid cannot read from the given path. HomeFS (not the actual filesystem) path
218 219 220 221 222 223 |
# File 'lib/homefs/homefs.rb', line 218 def check_readable(uid, relpath) hpath = homepath(uid, relpath) unless readable?(hpath, uid) fail Errno::EACCES, relpath end end |
#check_writable(uid, relpath) ⇒ void
This method returns an undefined value.
This method raises an Errno::EACCES if the user given by uid cannot write to the given path. The check is the same as in #writable?. HomeFS (not the actual filesystem)
202 203 204 205 206 207 |
# File 'lib/homefs/homefs.rb', line 202 def check_writable(uid, relpath) hpath = homepath(uid, relpath) unless writable?(hpath, uid) fail Errno::EACCES, relpath end end |
#chmod(context, path, mode) ⇒ void
This method should usually only be called by FUSE, and not called directly
This method returns an undefined value.
Change file permissions
287 288 289 290 291 |
# File 'lib/homefs/homefs.rb', line 287 def chmod(context, path, mode) hpath = homepath(context.uid, path) check_owner(context.uid, path) File.chmod(mode, hpath) end |
#chown(context, path, uid, gid) ⇒ void
This method should usually only be called by FUSE, and not called directly
This method returns an undefined value.
Change file ownership
304 305 306 307 308 |
# File 'lib/homefs/homefs.rb', line 304 def chown(context, path, uid, gid) raise Errno::EACCES, path if context.uid != 0 hpath = homepath(context.uid, path) File.chown(uid, gid, hpath) end |
#create(context, path, mode, ffi) ⇒ void
This method should usually only be called by FUSE, and not called directly
This method returns an undefined value.
Create and open a file If the file does not exist, first create it with the specified mode,
and then open it.
323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 |
# File 'lib/homefs/homefs.rb', line 323 def create(context, path, mode, ffi) check_writable(context.uid, path) hpath = homepath(context.uid, path) # It is important that we create the file and chown it before we # set its mode to prevent a possible privilage escalation # race-condition. To be more specific, if this filesystem is # running as root (which it usually is), the file will be created # owned by root. If the mode were set when the file is created, # then an attacker could create a setuid file writable by anyone, # and then, in the 'real' filesystem (not HomeFS), quickly write a # small program that just executes /bin/bash. If the write # completed before handle.chown was called, the attacker could have # a setuid shell. handle = File.new(hpath, File::CREAT | File::WRONLY, 0600) handle.chown(context.uid, context.gid) handle.chmod(mode) ffi.fh = handle end |
#executable?(path, uid = nil) ⇒ Boolean
Test whether the user described by uid can execute the given path. If no file exists at the given path, an Errno::ENOENT is raised. method returns whether the file is executable by anyone.
191 192 193 |
# File 'lib/homefs/homefs.rb', line 191 def executable?(path, uid = nil) mode_mask(path, 0111, uid) end |
#fgetattr(context, path, ffi) ⇒ Stat
This method should usually only be called by FUSE, and not called directly
Get attributes of an open file
353 354 355 |
# File 'lib/homefs/homefs.rb', line 353 def fgetattr(context, path, ffi) ffi.fh.lstat end |
#flush(context, path, ffi) ⇒ void
This method should usually only be called by FUSE, and not called directly
This method returns an undefined value.
Possibly flush cached data BIG NOTE: This is not equivalent to fsync(). It’s not a request to sync dirty data. Flush is called on each close() of a file descriptor. So if a filesystem wants to return write errors in close() and the file has cached dirty data, this is a good place to write back data and return any errors. Since many applications ignore close() errors this is not always useful.
NOTE: The flush() method may be called more than once for each open(). This happens if more than one file descriptor refers to an opened file due to dup(), dup2() or fork() calls. It is not possible to determine if a flush is final, so each flush should be treated equally. Multiple write-flush sequences are relatively rare, so this shouldn’t be a problem.
Filesystems shouldn’t assume that flush will always be called after some writes, or that if will be called at all.
383 384 |
# File 'lib/homefs/homefs.rb', line 383 def flush(context, path, ffi) # We don't do any caching end |
#fsync(context, path, datasync, ffi) ⇒ void
This method should usually only be called by FUSE, and not called directly
This method returns an undefined value.
Synchronize file contents
398 399 400 401 402 403 404 |
# File 'lib/homefs/homefs.rb', line 398 def fsync(context, path, datasync, ffi) if datasync ffi.fh.fdatasync else ffi.fh.fsync end end |
#ftruncate(context, path, size, ffi) ⇒ void
This method should usually only be called by FUSE, and not called directly
This method returns an undefined value.
Change the size of an open file
417 418 419 |
# File 'lib/homefs/homefs.rb', line 417 def ftruncate(context, path, size, ffi) ffi.fh.truncate(size) end |
#getattr(context, path) ⇒ Stat
This method should usually only be called by FUSE, and not called directly
Get file attributes. Similar to stat(). The ‘st_dev’ and ‘st_blksize’ fields are ignored. The ‘st_ino’ field is ignored except if the ‘use_ino’ mount option is given.
433 434 435 436 |
# File 'lib/homefs/homefs.rb', line 433 def getattr(context, path) check_listable(context.uid, path) File.lstat(homepath(context.uid, path)) end |
#homepath(uid, path = nil) ⇒ String
Return the path to the root of HomeFS relative to the underlying filesystem. If path is specified, it is taken to be relative to the root of HomeFS.
90 91 92 93 94 95 96 97 98 99 100 |
# File 'lib/homefs/homefs.rb', line 90 def homepath(uid, path = nil) basepath = @homedirs[uid] # basepath shouldn't ever be nil, but fail gracefully just # in case. raise Errno::ENOENT if basepath == nil if path File.join(basepath, @relpath, path) else File.join(basepath, @relpath) end end |
#init(info) ⇒ void
This method should usually only be called by FUSE, and not called directly
This method returns an undefined value.
Called when filesystem is initialised
446 447 |
# File 'lib/homefs/homefs.rb', line 446 def init(context, info) end |
#link(context, from, to) ⇒ void
This method should usually only be called by FUSE, and not called directly
This method returns an undefined value.
Create a hard link to file
459 460 461 462 463 464 |
# File 'lib/homefs/homefs.rb', line 459 def link(context, from, to) hfrom = homepath(context.uid, from) check_writable(context.uid, from) File.link(hfrom, to) File.chown(context.uid, hfrom) if context.uid == 0 end |
#mkdir(context, path, mode) ⇒ Object
This method should usually only be called by FUSE, and not called directly
469 470 471 472 473 474 |
# File 'lib/homefs/homefs.rb', line 469 def mkdir(context, path, mode) check_writable(context.uid, path) hpath = homepath(context.uid, path) Dir.mkdir(hpath, mode) File.chown(context.uid, context.gid, hpath) if context.uid == 0 end |
#mode_mask(file, mask, uid = nil) ⇒ Boolean
Check whether the given file has a mode that matches the given mask. If check_ids is true, the third-least significant digit of the mask is set to zero if the calling user is not the owner of the file, and the second-least significant digit of the mask is zeroed if the calling user’s group is not the group of the file. This has the effect of only checking the owner’s permissions if we’re the owner, and only checking the group permissions if we’re in that group.
This method checks if any of the set bits in the mask are set in the file’s mode. It does not check if the mask matches the file’s mode, nor does it check if all set bits in the mask are set in the file’s mode.
The first three least significant digits of the mode, in octal, (that is, the three rightmost digits) control the read (4), write (2), and execute (1) permissions of the file. The next digit controls setuid (4), setgid (2), and the sticky bit (1) of the file.
It is recommended to see
man chmod
to get a more detailed description about file modes.
138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 |
# File 'lib/homefs/homefs.rb', line 138 def mode_mask(file, mask, uid = nil) stat = File.stat(file) fmode = stat.mode if uid fuid, fgid = stat.uid, stat.gid # Zero out the third digit (in octal). # We could use a constant here, but this works # for a mask of any length if uid != fuid mask &= ~(mask & 0700) end if !@groups[fgid].include?(uid) mask &= ~(mask & 0070) end end fmode & mask != 0 end |
#open(context, path, ffi) ⇒ void
This method should usually only be called by FUSE, and not called directly
This method returns an undefined value.
File open operation file open flags etc. The fh attribute may be used to store an arbitrary filehandle object which will be passed to all subsequent operations on this file
490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 |
# File 'lib/homefs/homefs.rb', line 490 def open(context, path, ffi) if ffi.flags & File::RDWR != 0 mask = 0666 elsif ffi.flags & File::WRONLY != 0 mask = 0222 else mask = 0444 end hpath = homepath(context.uid, path) unless mode_mask(hpath, mask, context.uid) fail Errno::EACCES, path end # We pass the flags straight into File.open because the constants # in RFuse::Fcntl and File::Constants have the same values, because # they both have the same values as the open syscall. If this were # not the case, we'd have the map the values of RFuse::Fcntl to # their equivalents in File::Constants ffi.fh = File.open(hpath, ffi.flags) end |
#opendir(context, path, name) ⇒ void
This method should usually only be called by FUSE, and not called directly
This method returns an undefined value.
Open directory Unless the ‘default_permissions’ mount option is given, this method should check if opendir is permitted for this directory. Optionally opendir may also return an arbitrary filehandle in the fuse_file_info structure, which will be available to #readdir, #fsyncdir, #releasedir.
526 527 528 529 |
# File 'lib/homefs/homefs.rb', line 526 def opendir(context, path, ffi) check_listable(context.uid, path) ffi.fh = Dir.new(homepath(context.uid, path)) end |
#read(context, path, size, offset, ffi) ⇒ String
This method should usually only be called by FUSE, and not called directly
Read data from an open file
544 545 546 547 548 549 550 551 |
# File 'lib/homefs/homefs.rb', line 544 def read(context, path, size, offset, ffi) if offset < 0 ffi.fh.seek(offset, :END) else ffi.fh.seek(offset, :SET) end ffi.fh.read(size) end |
#read_group ⇒ void
This method returns an undefined value.
Read or re-read /etc/group to update the internal table of group membership.
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
# File 'lib/homefs/homefs.rb', line 61 def read_group # Try to read from getent group = `getent group` if $? != 0 group = File.read('/etc/group') end @groups = Hash.new group.each_line.map { |line| next if line.strip.empty? _name, _, gid, members = line.split(':') members = members.strip.split(',').map {|member| @usernames[member]} [Integer(gid), members] }.reject { |line| line.nil? }.each { |gid, members| @groups[gid] = members } end |
#read_passwd ⇒ void
This method returns an undefined value.
Read or re-read /etc/passwd to update the internal table of home directories for each UID. This method does not try to guess as to which user IDs are owned by actual users of the system, and which are utility accounts such as ‘lp’ or ‘pulse’.
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
# File 'lib/homefs/homefs.rb', line 33 def read_passwd # Try to read from getent passwd = `getent passwd` if $? != 0 passwd = File.read('/etc/passwd') end @homedirs = Hash.new @usernames = Hash.new # Here we map each line in passwd to an array, # which Hash interprets as a key, value pair. passwd.each_line.map { |line| next if line.strip.empty? values = line.split(':') # Username, UID, home directory [values[0], Integer(values[2]), values[5]] }.reject { |line| line.nil? }.each { |username, uid, home| @homedirs[uid] = home @usernames[username] = uid } end |
#readable?(path, uid = nil) ⇒ Boolean
Test whether the user described by uid can read from the given path. If no file exists at the given path, an Errno::ENOENT is raised. method returns whether the file is readable by anyone.
179 180 181 |
# File 'lib/homefs/homefs.rb', line 179 def readable?(path, uid = nil) mode_mask(path, 0444, uid) end |
#readdir(context, path, filler, offset, ffi) ⇒ Object
This method should usually only be called by FUSE, and not called directly
556 557 558 559 560 561 562 563 564 |
# File 'lib/homefs/homefs.rb', line 556 def readdir(context, path, filler, offset, ffi) filler.push(".", nil, 0) filler.push("..", nil, 0) ffi.fh.pos = offset ffi.fh.each do |filename| next if filename == '.' || filename == '..' filler.push(filename, nil, 0) end end |
#readlink(context, path, size) ⇒ String
This method should usually only be called by FUSE, and not called directly
Resolve target of symbolic link
577 578 579 580 |
# File 'lib/homefs/homefs.rb', line 577 def readlink(context, path, size) # Is it okay that we return the 'real' path here? File.readlink(homepath(context.uid, path))[0 ... size] end |
#release(context, path, ffi) ⇒ void
This method should usually only be called by FUSE, and not called directly
This method returns an undefined value.
Release an open file Release is called when there are no more references to an open file: all file descriptors are closed and all memory mappings are unmapped.
For every #open call there will be exactly one #release call with the same flags and file descriptor. It is possible to have a file opened more than once, in which case only the last release will mean, that no more reads/writes will happen on the file.
598 599 600 |
# File 'lib/homefs/homefs.rb', line 598 def release(context, path, ffi) ffi.fh.close end |
#rename(context, from, to) ⇒ Object
This method should usually only be called by FUSE, and not called directly
605 606 607 608 609 610 611 612 |
# File 'lib/homefs/homefs.rb', line 605 def rename(context, from, to) hfrom = homepath(context.uid, from) hto = homepath(context.uid, to) check_readable(context.uid, from) check_writable(File.dirname(from), context.uid) check_writable(File.dirname(to), context.uid) FileUtils.mv(hfrom, hto, :force => true) end |
#symlink(context, to, from) ⇒ void
This method should usually only be called by FUSE, and not called directly
This method returns an undefined value.
Create a symbolic link Create a symbolic link named “from” which, when evaluated, will lead
to "to".
626 627 628 629 630 631 632 633 634 |
# File 'lib/homefs/homefs.rb', line 626 def symlink(context, to, from) check_writable(context.uid, from) hfrom = homepath(context.uid, from) File.symlink(to, hfrom) begin File.lchown(context.uid, context.gid, hfrom) rescue end end |
#truncate(context, path, offset) ⇒ void
This method should usually only be called by FUSE, and not called directly
This method returns an undefined value.
Change the size of a file
646 647 648 649 |
# File 'lib/homefs/homefs.rb', line 646 def truncate(context, path, offset) check_writable(context.uid, path) File.truncate(homepath(context.uid, path), offset) end |
#unlink(context, path) ⇒ void
This method should usually only be called by FUSE, and not called directly
This method returns an undefined value.
Remove a file
660 661 662 663 |
# File 'lib/homefs/homefs.rb', line 660 def unlink(context, path) check_writable(context.uid, File.dirname(path)) File.unlink(homepath(context.uid, path)) end |
#utimens(context, path, actime, modtime) ⇒ void
This method should usually only be called by FUSE, and not called directly
This method returns an undefined value.
Change access/modification times of a file
676 677 678 679 |
# File 'lib/homefs/homefs.rb', line 676 def utimens(context, path, actime, modtime) check_writable(context.uid, path) File.utime(actime, modtime, homepath(context.uid, path)) end |
#writable?(path, uid = nil) ⇒ Boolean
Test whether we can write to the given path. If no file exists at path, we test if the parent directory is either writable or has the sticky bit set. method returns whether the file is writable by anyone.
163 164 165 166 167 168 169 |
# File 'lib/homefs/homefs.rb', line 163 def writable?(path, uid = nil) if !File.exist?(path) mode_mask(File.dirname(path), 01222, uid) else mode_mask(path, 0222, uid) end end |
#write(context, path, data, offset, ffi) ⇒ Integer
This method should usually only be called by FUSE, and not called directly
Write data to an open file
694 695 696 697 698 699 700 701 |
# File 'lib/homefs/homefs.rb', line 694 def write(context, path, data, offset, ffi) if offset < 0 ffi.fh.seek(offset, :END) else ffi.fh.seek(offset, :SET) end ffi.fh.write(data) end |