Class: LittleGhost::Sandbox::Mount

Inherits:
Object
  • Object
show all
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

Class Method Summary collapse

Instance Method Summary collapse

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.

Raises:



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.expand_path(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

#accessObject (readonly)

Either :read_only or :read_write.



44
45
46
# File 'lib/little_ghost/sandbox/mount.rb', line 44

def access
  @access
end

#protect_aliasesObject (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

#sourceObject (readonly)

Absolute host directory exposed by this mount.



40
41
42
# File 'lib/little_ghost/sandbox/mount.rb', line 40

def source
  @source
end

#targetObject (readonly)

Absolute path presented inside the sandbox.



42
43
44
# File 'lib/little_ghost/sandbox/mount.rb', line 42

def target
  @target
end

#toolsObject (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.

Raises:



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

Raises:



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.

Returns:

  • (Boolean)


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

#hashObject

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.

Returns:

  • (Boolean)


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.

Returns:

  • (Boolean)


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.

Returns:

  • (Boolean)


57
# File 'lib/little_ghost/sandbox/mount.rb', line 57

def tool_visible? = tools

#writable?Boolean

Indicates that the mount permits writes.

Returns:

  • (Boolean)


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.