Class: LittleGhost::Sandbox::Mount
- Inherits:
-
Object
- Object
- LittleGhost::Sandbox::Mount
- Defined in:
- lib/little_ghost/sandbox/mount.rb
Overview
Maps one trusted host directory into the sandbox's virtual filesystem.
Process and Filesystem Tool visibility are separate: tools: false keeps the
mount available to child processes while denying it to the filesystem
broker. protect_aliases preserves restrictive access when the same host
files are reachable through a broader mount.
Constant Summary collapse
- ACCESS_MODES =
:nodoc:
%i[read_only read_write].freeze
Instance Attribute Summary collapse
-
#access ⇒ Object
readonly
Either
:read_onlyor:read_write. -
#protect_aliases ⇒ Object
readonly
Indicates whether the mount's access applies through physical aliases.
-
#source ⇒ Object
readonly
Absolute host directory exposed by this mount.
-
#target ⇒ Object
readonly
Absolute path presented inside the sandbox.
-
#tools ⇒ Object
readonly
Indicates whether direct filesystem tools may traverse this mount.
Class Method Summary collapse
-
.coerce(value, root: nil) ⇒ Object
Returns
valueunchanged or builds a mount from a Hash. - .normalize_virtual_path(path) ⇒ Object
Instance Method Summary collapse
-
#==(other) ⇒ Object
(also: #eql?)
Mounts compare by their normalized mapping and access semantics.
-
#covers?(path) ⇒ Boolean
Indicates whether
pathis this mount target or one of its descendants. -
#hash ⇒ Object
Hashes the normalized mapping and access semantics.
-
#initialize(source:, target:, access: :read_only, protect_aliases: false, tools: true, root: nil) ⇒ Mount
constructor
Maps
sourceat the absolute virtualtargetwith the selected access. -
#narrow(target: self.target, access: self.access) ⇒ Object
Returns a copy narrowed to
targetandaccess. -
#protect_aliases? ⇒ Boolean
Indicates that physical aliases retain this mount's access.
-
#read_only? ⇒ Boolean
Indicates that the mount does not permit writes.
-
#tool_visible? ⇒ Boolean
Indicates that direct filesystem tools may traverse this mount.
-
#writable? ⇒ Boolean
Indicates that the mount permits writes.
Constructor Details
#initialize(source:, target:, access: :read_only, protect_aliases: false, tools: true, root: nil) ⇒ Mount
Maps source at the absolute virtual target with the selected access.
source is trusted host configuration, not model input.
25 26 27 28 29 30 31 32 33 34 35 36 37 |
# File 'lib/little_ghost/sandbox/mount.rb', line 25 def initialize(source:, target:, access: :read_only, protect_aliases: false, tools: true, root: nil) source = File.(String(source), root) target = normalize_target(target) access = access.to_sym raise PolicyError, "mount access must be :read_only or :read_write" unless ACCESS_MODES.include?(access) @source = source.freeze @target = target.freeze @access = access @protect_aliases = !!protect_aliases @tools = !!tools freeze end |
Instance Attribute Details
#access ⇒ Object (readonly)
Either :read_only or :read_write.
44 45 46 |
# File 'lib/little_ghost/sandbox/mount.rb', line 44 def access @access end |
#protect_aliases ⇒ Object (readonly)
Indicates whether the mount's access applies through physical aliases.
46 47 48 |
# File 'lib/little_ghost/sandbox/mount.rb', line 46 def protect_aliases @protect_aliases end |
#source ⇒ Object (readonly)
Absolute host directory exposed by this mount.
40 41 42 |
# File 'lib/little_ghost/sandbox/mount.rb', line 40 def source @source end |
#target ⇒ Object (readonly)
Absolute path presented inside the sandbox.
42 43 44 |
# File 'lib/little_ghost/sandbox/mount.rb', line 42 def target @target end |
#tools ⇒ Object (readonly)
Indicates whether direct filesystem tools may traverse this mount.
48 49 50 |
# File 'lib/little_ghost/sandbox/mount.rb', line 48 def tools @tools end |
Class Method Details
.coerce(value, root: nil) ⇒ Object
Returns value unchanged or builds a mount from a Hash.
16 17 18 19 20 21 |
# File 'lib/little_ghost/sandbox/mount.rb', line 16 def self.coerce(value, root: nil) return value if value.is_a?(self) raise PolicyError, "mount must be a Hash or Sandbox::Mount" unless value.is_a?(Hash) new(**value.transform_keys(&:to_sym), root:) end |
.normalize_virtual_path(path) ⇒ Object
103 104 105 106 107 108 109 110 |
# File 'lib/little_ghost/sandbox/mount.rb', line 103 def self.normalize_virtual_path(path) value = String(path) raise PolicyError, "mount target contains a null byte" if value.include?("\0") raise PolicyError, "mount target must be absolute" unless value.start_with?(File::SEPARATOR) raise PolicyError, "mount target cannot contain traversal" if value.split(File::SEPARATOR).include?("..") Pathname.new(value).cleanpath.to_s end |
Instance Method Details
#==(other) ⇒ Object Also known as: eql?
Mounts compare by their normalized mapping and access semantics.
92 93 94 95 96 |
# File 'lib/little_ghost/sandbox/mount.rb', line 92 def ==(other) other.is_a?(self.class) && [comparison_source, target, access, protect_aliases?, tool_visible?] == [other.send(:comparison_source), other.target, other.access, other.protect_aliases?, other.tool_visible?] end |
#covers?(path) ⇒ Boolean
Indicates whether path is this mount target or one of its descendants.
60 61 62 63 |
# File 'lib/little_ghost/sandbox/mount.rb', line 60 def covers?(path) path = self.class.send(:normalize_virtual_path, path) path == target || path.start_with?("#{target}/") end |
#hash ⇒ Object
Hashes the normalized mapping and access semantics.
101 |
# File 'lib/little_ghost/sandbox/mount.rb', line 101 def hash = [comparison_source, target, access, protect_aliases?, tool_visible?].hash |
#narrow(target: self.target, access: self.access) ⇒ Object
Returns a copy narrowed to target and access. Widening raises
CapabilityError.
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
# File 'lib/little_ghost/sandbox/mount.rb', line 67 def narrow(target: self.target, access: self.access) target = self.class.send(:normalize_virtual_path, target) raise CapabilityError, "sandbox scope cannot expose a path outside its parent mount" unless covers?(target) if read_only? && access.to_sym == :read_write raise CapabilityError, "sandbox scope cannot make a read-only mount writable" end relative = target.delete_prefix(self.target).delete_prefix("/") parent_root = File.realpath(source) child_root = relative.empty? ? parent_root : File.realpath(File.join(parent_root, relative)) unless child_root == parent_root || child_root.start_with?("#{parent_root}#{File::SEPARATOR}") raise CapabilityError, "sandbox scope mount source escapes its parent" end self.class.new( source: child_root, target:, access:, protect_aliases: protect_aliases?, tools: tool_visible? ) rescue Errno::ENOENT, Errno::EACCES raise CapabilityError, "sandbox scope mount source is unavailable" end |
#protect_aliases? ⇒ Boolean
Indicates that physical aliases retain this mount's access.
55 56 |
# File 'lib/little_ghost/sandbox/mount.rb', line 55 def protect_aliases? = protect_aliases # Indicates that direct filesystem tools may traverse this mount. |
#read_only? ⇒ Boolean
Indicates that the mount does not permit writes.
51 52 |
# File 'lib/little_ghost/sandbox/mount.rb', line 51 def read_only? = access == :read_only # Indicates that the mount permits writes. |
#tool_visible? ⇒ Boolean
Indicates that direct filesystem tools may traverse this mount.
57 |
# File 'lib/little_ghost/sandbox/mount.rb', line 57 def tool_visible? = tools |
#writable? ⇒ Boolean
Indicates that the mount permits writes.
53 54 |
# File 'lib/little_ghost/sandbox/mount.rb', line 53 def writable? = access == :read_write # Indicates that physical aliases retain this mount's access. |