All checks were successful
Test / ShareFS (push) Successful in 43s
Test / Hakurei (push) Successful in 53s
Test / Sandbox (push) Successful in 49s
Test / Sandbox (race detector) (push) Successful in 48s
Test / Hakurei (race detector) (push) Successful in 53s
Test / Create distribution (push) Successful in 35s
Test / Flake checks (push) Successful in 1m18s
This is useful when limits are configured to allow it. Signed-off-by: Ophestra <cat@gensokyo.uk>
239 lines
9.0 KiB
Go
239 lines
9.0 KiB
Go
package hst
|
|
|
|
import (
|
|
"errors"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"hakurei.app/container/check"
|
|
"hakurei.app/container/std"
|
|
)
|
|
|
|
// Config configures an application container.
|
|
type Config struct {
|
|
// Reverse-DNS style configured arbitrary identifier string.
|
|
//
|
|
// This value is passed as is to Wayland security-context-v1 and used as
|
|
// part of defaults in D-Bus session proxy. The zero value causes a default
|
|
// value to be derived from the container instance.
|
|
ID string `json:"id,omitempty"`
|
|
|
|
// System services to make available in the container.
|
|
Enablements *Enablements `json:"enablements,omitempty"`
|
|
|
|
// Session D-Bus proxy configuration.
|
|
//
|
|
// Has no effect if [EDBus] but is not set in Enablements. The zero value
|
|
// assumes built-in defaults derived from ID.
|
|
SessionBus *BusConfig `json:"session_bus,omitempty"`
|
|
// System D-Bus proxy configuration.
|
|
//
|
|
// Has no effect if [EDBus] but is not set in Enablements. The zero value
|
|
// disables system bus proxy.
|
|
SystemBus *BusConfig `json:"system_bus,omitempty"`
|
|
|
|
// Direct access to Wayland socket, no attempt is made to attach
|
|
// security-context-v1 and the bare socket is made available to the
|
|
// container.
|
|
//
|
|
// This option is unsupported and will most likely enable full control over
|
|
// the Wayland session from within the container. Do not set this to true
|
|
// unless you are sure you know what you are doing.
|
|
DirectWayland bool `json:"direct_wayland,omitempty"`
|
|
|
|
// Direct access to the PipeWire socket established via SecurityContext::Create,
|
|
// no attempt is made to start the pipewire-pulse server.
|
|
//
|
|
// The SecurityContext machinery is fatally flawed, it unconditionally sets
|
|
// read and execute bits on all objects for clients with the lowest achievable
|
|
// privilege level (by setting PW_KEY_ACCESS to "restricted" or by satisfying
|
|
// all conditions of [the /.flatpak-info hack]). This enables them to call
|
|
// any method targeting any object, and since Registry::Destroy checks for
|
|
// the read and execute bit, allows the destruction of any object other than
|
|
// PW_ID_CORE as well.
|
|
//
|
|
// This behaviour is implemented separately in media-session and wireplumber,
|
|
// with the wireplumber implementation in Lua via an embedded Lua vm. In all
|
|
// known setups, wireplumber is in use, and in that case, no option for
|
|
// configuring this behaviour exists, without replacing the Lua script.
|
|
// Also, since PipeWire relies on these permissions to work, reducing them
|
|
// was never possible in the first place.
|
|
//
|
|
// Currently, the only other sandboxed use case is flatpak, which is not
|
|
// aware of PipeWire and blindly exposes the bare PulseAudio socket to the
|
|
// container (behaves like DirectPulse). This socket is backed by the
|
|
// pipewire-pulse compatibility daemon, which obtains client pid via the
|
|
// SO_PEERCRED option. The PipeWire daemon, pipewire-pulse daemon and the
|
|
// session manager daemon then separately performs [the /.flatpak-info hack].
|
|
// Under such use case, since the client has no direct access to PipeWire,
|
|
// insecure parts of the protocol are obscured by the absence of an
|
|
// equivalent API in PulseAudio, or pipewire-pulse simply not implementing
|
|
// them.
|
|
//
|
|
// Hakurei does not rely on [the /.flatpak-info hack]. Instead, a socket is
|
|
// sets up via SecurityContext. A pipewire-pulse server connected through it
|
|
// achieves the same permissions as flatpak does via [the /.flatpak-info hack]
|
|
// and is maintained for the life of the container.
|
|
//
|
|
// This option is unsupported and enables a denial-of-service attack as the
|
|
// sandboxed client is able to destroy any client object and thus
|
|
// disconnecting them from PipeWire, or destroy the SecurityContext object,
|
|
// preventing any further container creation.
|
|
//
|
|
// Do not set this to true, it is insecure under any configuration.
|
|
//
|
|
// [the /.flatpak-info hack]: https://git.gensokyo.uk/security/hakurei/issues/21
|
|
DirectPipeWire bool `json:"direct_pipewire,omitempty"`
|
|
|
|
// Direct access to PulseAudio socket, no attempt is made to establish
|
|
// pipewire-pulse server via a PipeWire socket with a SecurityContext
|
|
// attached, and the bare socket is made available to the container.
|
|
//
|
|
// This option is unsupported and enables arbitrary code execution as the
|
|
// PulseAudio server.
|
|
//
|
|
// Do not set this to true, it is insecure under any configuration.
|
|
DirectPulse bool `json:"direct_pulse,omitempty"`
|
|
|
|
// Extra acl updates to perform before setuid.
|
|
ExtraPerms []ExtraPermConfig `json:"extra_perms,omitempty"`
|
|
|
|
// Numerical application id, passed to hsu, used to derive init user
|
|
// namespace credentials.
|
|
Identity int `json:"identity"`
|
|
// Init user namespace supplementary groups inherited by all container processes.
|
|
Groups []string `json:"groups"`
|
|
|
|
// Scheduling policy to set for the container.
|
|
//
|
|
// The zero value retains the current scheduling policy.
|
|
SchedPolicy std.SchedPolicy `json:"sched_policy,omitempty"`
|
|
// Scheduling priority to set for the container.
|
|
//
|
|
// The zero value implies the minimum priority of the current SchedPolicy.
|
|
// Has no effect if SchedPolicy is zero.
|
|
SchedPriority std.Int `json:"sched_priority,omitempty"`
|
|
|
|
// High level configuration applied to the underlying [container].
|
|
Container *ContainerConfig `json:"container"`
|
|
}
|
|
|
|
var (
|
|
// ErrConfigNull is returned by [Config.Validate] for an invalid configuration
|
|
// that contains a null value for any field that must not be null.
|
|
ErrConfigNull = errors.New("unexpected null in config")
|
|
|
|
// ErrIdentityBounds is returned by [Config.Validate] for an out of bounds
|
|
// [Config.Identity] value.
|
|
ErrIdentityBounds = errors.New("identity out of bounds")
|
|
|
|
// ErrSchedPolicyBounds is returned by [Config.Validate] for an out of bounds
|
|
// [Config.SchedPolicy] value.
|
|
ErrSchedPolicyBounds = errors.New("scheduling policy out of bounds")
|
|
|
|
// ErrEnviron is returned by [Config.Validate] if an environment variable
|
|
// name contains '=' or NUL.
|
|
ErrEnviron = errors.New("invalid environment variable name")
|
|
|
|
// ErrInsecure is returned by [Config.Validate] if the configuration is
|
|
// considered insecure.
|
|
ErrInsecure = errors.New("configuration is insecure")
|
|
)
|
|
|
|
// Validate checks [Config] and returns [AppError] if an invalid value is encountered.
|
|
func (config *Config) Validate() error {
|
|
if config == nil {
|
|
return &AppError{Step: "validate configuration", Err: ErrConfigNull,
|
|
Msg: "invalid configuration"}
|
|
}
|
|
|
|
// this is checked again in hsu
|
|
if config.Identity < IdentityStart || config.Identity > IdentityEnd {
|
|
return &AppError{Step: "validate configuration", Err: ErrIdentityBounds,
|
|
Msg: "identity " + strconv.Itoa(config.Identity) + " out of range"}
|
|
}
|
|
|
|
if config.SchedPolicy < 0 || config.SchedPolicy > std.SCHED_LAST {
|
|
return &AppError{Step: "validate configuration", Err: ErrSchedPolicyBounds,
|
|
Msg: "scheduling policy " +
|
|
strconv.Itoa(int(config.SchedPolicy)) +
|
|
" out of range"}
|
|
}
|
|
|
|
if err := config.SessionBus.CheckInterfaces("session"); err != nil {
|
|
return err
|
|
}
|
|
if err := config.SystemBus.CheckInterfaces("system"); err != nil {
|
|
return err
|
|
}
|
|
|
|
if config.Container == nil {
|
|
return &AppError{Step: "validate configuration", Err: ErrConfigNull,
|
|
Msg: "configuration missing container state"}
|
|
}
|
|
if config.Container.Home == nil {
|
|
return &AppError{Step: "validate configuration", Err: ErrConfigNull,
|
|
Msg: "container configuration missing path to home directory"}
|
|
}
|
|
if config.Container.Shell == nil {
|
|
return &AppError{Step: "validate configuration", Err: ErrConfigNull,
|
|
Msg: "container configuration missing path to shell"}
|
|
}
|
|
if config.Container.Path == nil {
|
|
return &AppError{Step: "validate configuration", Err: ErrConfigNull,
|
|
Msg: "container configuration missing path to initial program"}
|
|
}
|
|
|
|
for key := range config.Container.Env {
|
|
if strings.IndexByte(key, '=') != -1 || strings.IndexByte(key, 0) != -1 {
|
|
return &AppError{Step: "validate configuration", Err: ErrEnviron,
|
|
Msg: "invalid environment variable " + strconv.Quote(key)}
|
|
}
|
|
}
|
|
|
|
if et := config.Enablements.Unwrap(); !config.DirectPulse && et&EPulse != 0 {
|
|
return &AppError{Step: "validate configuration", Err: ErrInsecure,
|
|
Msg: "enablement PulseAudio is insecure and no longer supported"}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ExtraPermConfig describes an acl update to perform before setuid.
|
|
type ExtraPermConfig struct {
|
|
// Whether to create Path as a directory if it does not exist.
|
|
Ensure bool `json:"ensure,omitempty"`
|
|
// Pathname to act on.
|
|
Path *check.Absolute `json:"path"`
|
|
// Whether to set ACL_READ for the target user.
|
|
Read bool `json:"r,omitempty"`
|
|
// Whether to set ACL_WRITE for the target user.
|
|
Write bool `json:"w,omitempty"`
|
|
// Whether to set ACL_EXECUTE for the target user.
|
|
Execute bool `json:"x,omitempty"`
|
|
}
|
|
|
|
// String returns a checked string representation of [ExtraPermConfig].
|
|
func (e *ExtraPermConfig) String() string {
|
|
if e == nil || e.Path == nil {
|
|
return "<invalid>"
|
|
}
|
|
buf := make([]byte, 0, 5+len(e.Path.String()))
|
|
buf = append(buf, '-', '-', '-')
|
|
if e.Ensure {
|
|
buf = append(buf, '+')
|
|
}
|
|
buf = append(buf, ':')
|
|
buf = append(buf, []byte(e.Path.String())...)
|
|
if e.Read {
|
|
buf[0] = 'r'
|
|
}
|
|
if e.Write {
|
|
buf[1] = 'w'
|
|
}
|
|
if e.Execute {
|
|
buf[2] = 'x'
|
|
}
|
|
return string(buf)
|
|
}
|