diff --git a/cmd/hakurei/command.go b/cmd/hakurei/command.go index 6e5b446a..78090161 100644 --- a/cmd/hakurei/command.go +++ b/cmd/hakurei/command.go @@ -38,8 +38,9 @@ var errSuccess = errors.New("success") func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErrs, out io.Writer) command.Command { var ( - flagVerbose bool - flagJSON bool + flagVerbose bool + flagInsecure bool + flagJSON bool ) c := command.New(out, log.Printf, "hakurei", func([]string) error { msg.SwapVerbose(flagVerbose) @@ -57,6 +58,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr return nil }). Flag(&flagVerbose, "v", command.BoolFlag(false), "Increase log verbosity"). + Flag(&flagInsecure, "insecure", command.BoolFlag(false), "Allow use of insecure compatibility options"). Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output in JSON when applicable") c.Command("shim", command.UsageInternal, func([]string) error { outcome.Shim(msg); return errSuccess }) @@ -75,7 +77,12 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr config.Container.Args = append(config.Container.Args, args[1:]...) } - outcome.Main(ctx, msg, config, flagIdentifierFile) + var flags int + if flagInsecure { + flags |= hst.VAllowInsecure + } + + outcome.Main(ctx, msg, config, flags, flagIdentifierFile) panic("unreachable") }). Flag(&flagIdentifierFile, "identifier-fd", command.IntFlag(-1), @@ -282,7 +289,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr } } - outcome.Main(ctx, msg, &config, -1) + outcome.Main(ctx, msg, &config, 0, -1) panic("unreachable") }). Flag(&flagDBusConfigSession, "dbus-config", command.StringFlag("builtin"), diff --git a/cmd/hakurei/command_test.go b/cmd/hakurei/command_test.go index a7a5027e..b90688e5 100644 --- a/cmd/hakurei/command_test.go +++ b/cmd/hakurei/command_test.go @@ -20,7 +20,7 @@ func TestHelp(t *testing.T) { }{ { "main", []string{}, ` -Usage: hakurei [-h | --help] [-v] [--json] COMMAND [OPTIONS] +Usage: hakurei [-h | --help] [-v] [--insecure] [--json] COMMAND [OPTIONS] Commands: run Load and start container from configuration file diff --git a/cmd/hakurei/print.go b/cmd/hakurei/print.go index 7ce51f47..780dc55b 100644 --- a/cmd/hakurei/print.go +++ b/cmd/hakurei/print.go @@ -56,7 +56,7 @@ func printShowInstance( t := newPrinter(output) defer t.MustFlush() - if err := config.Validate(); err != nil { + if err := config.Validate(hst.VAllowInsecure); err != nil { valid = false if m, ok := message.GetMessage(err); ok { mustPrint(output, "Error: "+m+"!\n\n") diff --git a/hst/config.go b/hst/config.go index 6b7e26f4..927f60ab 100644 --- a/hst/config.go +++ b/hst/config.go @@ -140,21 +140,29 @@ var ( ErrInsecure = errors.New("configuration is insecure") ) +const ( + // VAllowInsecure allows use of compatibility options considered insecure + // under any configuration, to work around ecosystem-wide flaws. + VAllowInsecure = 1 << iota +) + // Validate checks [Config] and returns [AppError] if an invalid value is encountered. -func (config *Config) Validate() error { +func (config *Config) Validate(flags int) error { + const step = "validate configuration" + if config == nil { - return &AppError{Step: "validate configuration", Err: ErrConfigNull, + return &AppError{Step: step, 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, + return &AppError{Step: step, Err: ErrIdentityBounds, Msg: "identity " + strconv.Itoa(config.Identity) + " out of range"} } if config.SchedPolicy < 0 || config.SchedPolicy > ext.SCHED_LAST { - return &AppError{Step: "validate configuration", Err: ErrSchedPolicyBounds, + return &AppError{Step: step, Err: ErrSchedPolicyBounds, Msg: "scheduling policy " + strconv.Itoa(int(config.SchedPolicy)) + " out of range"} @@ -168,34 +176,51 @@ func (config *Config) Validate() error { } if config.Container == nil { - return &AppError{Step: "validate configuration", Err: ErrConfigNull, + return &AppError{Step: step, Err: ErrConfigNull, Msg: "configuration missing container state"} } if config.Container.Home == nil { - return &AppError{Step: "validate configuration", Err: ErrConfigNull, + return &AppError{Step: step, Err: ErrConfigNull, Msg: "container configuration missing path to home directory"} } if config.Container.Shell == nil { - return &AppError{Step: "validate configuration", Err: ErrConfigNull, + return &AppError{Step: step, Err: ErrConfigNull, Msg: "container configuration missing path to shell"} } if config.Container.Path == nil { - return &AppError{Step: "validate configuration", Err: ErrConfigNull, + return &AppError{Step: step, 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, + return &AppError{Step: step, 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, + et := config.Enablements.Unwrap() + if !config.DirectPulse && et&EPulse != 0 { + return &AppError{Step: step, Err: ErrInsecure, Msg: "enablement PulseAudio is insecure and no longer supported"} } + if flags&VAllowInsecure == 0 { + switch { + case et&EWayland != 0 && config.DirectWayland: + return &AppError{Step: step, Err: ErrInsecure, + Msg: "direct_wayland is insecure and no longer supported"} + + case et&EPipeWire != 0 && config.DirectPipeWire: + return &AppError{Step: step, Err: ErrInsecure, + Msg: "direct_pipewire is insecure and no longer supported"} + + case et&EPulse != 0 && config.DirectPulse: + return &AppError{Step: step, Err: ErrInsecure, + Msg: "direct_pulse is insecure and no longer supported"} + } + } + return nil } diff --git a/hst/config_test.go b/hst/config_test.go index b9e44d8b..22d764b5 100644 --- a/hst/config_test.go +++ b/hst/config_test.go @@ -14,65 +14,109 @@ func TestConfigValidate(t *testing.T) { testCases := []struct { name string config *hst.Config + flags int wantErr error }{ - {"nil", nil, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull, + {"nil", nil, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull, Msg: "invalid configuration"}}, - {"identity lower", &hst.Config{Identity: -1}, &hst.AppError{Step: "validate configuration", Err: hst.ErrIdentityBounds, + + {"identity lower", &hst.Config{Identity: -1}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrIdentityBounds, Msg: "identity -1 out of range"}}, - {"identity upper", &hst.Config{Identity: 10000}, &hst.AppError{Step: "validate configuration", Err: hst.ErrIdentityBounds, + {"identity upper", &hst.Config{Identity: 10000}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrIdentityBounds, Msg: "identity 10000 out of range"}}, - {"sched lower", &hst.Config{SchedPolicy: -1}, &hst.AppError{Step: "validate configuration", Err: hst.ErrSchedPolicyBounds, + + {"sched lower", &hst.Config{SchedPolicy: -1}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrSchedPolicyBounds, Msg: "scheduling policy -1 out of range"}}, - {"sched upper", &hst.Config{SchedPolicy: 0xcafe}, &hst.AppError{Step: "validate configuration", Err: hst.ErrSchedPolicyBounds, + {"sched upper", &hst.Config{SchedPolicy: 0xcafe}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrSchedPolicyBounds, Msg: "scheduling policy 51966 out of range"}}, - {"dbus session", &hst.Config{SessionBus: &hst.BusConfig{See: []string{""}}}, + + {"dbus session", &hst.Config{SessionBus: &hst.BusConfig{See: []string{""}}}, 0, &hst.BadInterfaceError{Interface: "", Segment: "session"}}, - {"dbus system", &hst.Config{SystemBus: &hst.BusConfig{See: []string{""}}}, + {"dbus system", &hst.Config{SystemBus: &hst.BusConfig{See: []string{""}}}, 0, &hst.BadInterfaceError{Interface: "", Segment: "system"}}, - {"container", &hst.Config{}, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull, + + {"container", &hst.Config{}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull, Msg: "configuration missing container state"}}, - {"home", &hst.Config{Container: &hst.ContainerConfig{}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull, + {"home", &hst.Config{Container: &hst.ContainerConfig{}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull, Msg: "container configuration missing path to home directory"}}, {"shell", &hst.Config{Container: &hst.ContainerConfig{ Home: fhs.AbsTmp, - }}, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull, + }}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull, Msg: "container configuration missing path to shell"}}, {"path", &hst.Config{Container: &hst.ContainerConfig{ Home: fhs.AbsTmp, Shell: fhs.AbsTmp, - }}, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull, + }}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull, Msg: "container configuration missing path to initial program"}}, + {"env equals", &hst.Config{Container: &hst.ContainerConfig{ Home: fhs.AbsTmp, Shell: fhs.AbsTmp, Path: fhs.AbsTmp, Env: map[string]string{"TERM=": ""}, - }}, &hst.AppError{Step: "validate configuration", Err: hst.ErrEnviron, + }}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrEnviron, Msg: `invalid environment variable "TERM="`}}, {"env NUL", &hst.Config{Container: &hst.ContainerConfig{ Home: fhs.AbsTmp, Shell: fhs.AbsTmp, Path: fhs.AbsTmp, Env: map[string]string{"TERM\x00": ""}, - }}, &hst.AppError{Step: "validate configuration", Err: hst.ErrEnviron, + }}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrEnviron, Msg: `invalid environment variable "TERM\x00"`}}, + {"insecure pulse", &hst.Config{Enablements: hst.NewEnablements(hst.EPulse), Container: &hst.ContainerConfig{ Home: fhs.AbsTmp, Shell: fhs.AbsTmp, Path: fhs.AbsTmp, - }}, &hst.AppError{Step: "validate configuration", Err: hst.ErrInsecure, + }}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrInsecure, Msg: "enablement PulseAudio is insecure and no longer supported"}}, + + {"direct wayland", &hst.Config{Enablements: hst.NewEnablements(hst.EWayland), DirectWayland: true, Container: &hst.ContainerConfig{ + Home: fhs.AbsTmp, + Shell: fhs.AbsTmp, + Path: fhs.AbsTmp, + }}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrInsecure, + Msg: "direct_wayland is insecure and no longer supported"}}, + {"direct wayland allow", &hst.Config{Enablements: hst.NewEnablements(hst.EWayland), DirectWayland: true, Container: &hst.ContainerConfig{ + Home: fhs.AbsTmp, + Shell: fhs.AbsTmp, + Path: fhs.AbsTmp, + }}, hst.VAllowInsecure, nil}, + + {"direct pipewire", &hst.Config{Enablements: hst.NewEnablements(hst.EPipeWire), DirectPipeWire: true, Container: &hst.ContainerConfig{ + Home: fhs.AbsTmp, + Shell: fhs.AbsTmp, + Path: fhs.AbsTmp, + }}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrInsecure, + Msg: "direct_pipewire is insecure and no longer supported"}}, + {"direct pipewire allow", &hst.Config{Enablements: hst.NewEnablements(hst.EPipeWire), DirectPipeWire: true, Container: &hst.ContainerConfig{ + Home: fhs.AbsTmp, + Shell: fhs.AbsTmp, + Path: fhs.AbsTmp, + }}, hst.VAllowInsecure, nil}, + + {"direct pulse", &hst.Config{Enablements: hst.NewEnablements(hst.EPulse), DirectPulse: true, Container: &hst.ContainerConfig{ + Home: fhs.AbsTmp, + Shell: fhs.AbsTmp, + Path: fhs.AbsTmp, + }}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrInsecure, + Msg: "direct_pulse is insecure and no longer supported"}}, + {"direct pulse allow", &hst.Config{Enablements: hst.NewEnablements(hst.EPulse), DirectPulse: true, Container: &hst.ContainerConfig{ + Home: fhs.AbsTmp, + Shell: fhs.AbsTmp, + Path: fhs.AbsTmp, + }}, hst.VAllowInsecure, nil}, + {"valid", &hst.Config{Container: &hst.ContainerConfig{ Home: fhs.AbsTmp, Shell: fhs.AbsTmp, Path: fhs.AbsTmp, - }}, nil}, + }}, 0, nil}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { t.Parallel() - if err := tc.config.Validate(); !reflect.DeepEqual(err, tc.wantErr) { + if err := tc.config.Validate(tc.flags); !reflect.DeepEqual(err, tc.wantErr) { t.Errorf("Validate: error = %#v, want %#v", err, tc.wantErr) } }) diff --git a/internal/outcome/finalise.go b/internal/outcome/finalise.go index 17007f07..d74c648c 100644 --- a/internal/outcome/finalise.go +++ b/internal/outcome/finalise.go @@ -32,7 +32,14 @@ type outcome struct { syscallDispatcher } -func (k *outcome) finalise(ctx context.Context, msg message.Msg, id *hst.ID, config *hst.Config) error { +// finalise prepares an outcome for main. +func (k *outcome) finalise( + ctx context.Context, + msg message.Msg, + id *hst.ID, + config *hst.Config, + flags int, +) error { if ctx == nil || id == nil { // unreachable panic("invalid call to finalise") @@ -43,7 +50,7 @@ func (k *outcome) finalise(ctx context.Context, msg message.Msg, id *hst.ID, con } k.ctx = ctx - if err := config.Validate(); err != nil { + if err := config.Validate(flags); err != nil { return err } diff --git a/internal/outcome/run.go b/internal/outcome/run.go index 8bf666b2..dfa2cf63 100644 --- a/internal/outcome/run.go +++ b/internal/outcome/run.go @@ -18,7 +18,13 @@ import ( func IsPollDescriptor(fd uintptr) bool // Main runs an app according to [hst.Config] and terminates. Main does not return. -func Main(ctx context.Context, msg message.Msg, config *hst.Config, fd int) { +func Main( + ctx context.Context, + msg message.Msg, + config *hst.Config, + flags int, + fd int, +) { // avoids runtime internals or standard streams if fd >= 0 { if IsPollDescriptor(uintptr(fd)) || fd < 3 { @@ -34,7 +40,7 @@ func Main(ctx context.Context, msg message.Msg, config *hst.Config, fd int) { k := outcome{syscallDispatcher: direct{msg}} finaliseTime := time.Now() - if err := k.finalise(ctx, msg, &id, config); err != nil { + if err := k.finalise(ctx, msg, &id, config, flags); err != nil { printMessageError(msg.GetLogger().Fatalln, "cannot seal app:", err) panic("unreachable") } diff --git a/internal/store/data.go b/internal/store/data.go index d8f2bc6d..00a23443 100644 --- a/internal/store/data.go +++ b/internal/store/data.go @@ -41,7 +41,7 @@ func entryDecode(r io.Reader, p *hst.State) (hst.Enablement, error) { return et, err } else if err = gob.NewDecoder(r).Decode(&p); err != nil { return et, &hst.AppError{Step: "decode state body", Err: err} - } else if err = p.Config.Validate(); err != nil { + } else if err = p.Config.Validate(hst.VAllowInsecure); err != nil { return et, err } else if p.Enablements.Unwrap() != et { return et, &hst.AppError{Step: "validate state enablement", Err: os.ErrInvalid, diff --git a/nixos.nix b/nixos.nix index 5b60aea4..d59ffc4e 100644 --- a/nixos.nix +++ b/nixos.nix @@ -265,7 +265,7 @@ in ''; in pkgs.writeShellScriptBin app.name '' - exec hakurei${if app.verbose then " -v" else ""} run ${checkedConfig "hakurei-app-${app.name}.json" conf} $@ + exec hakurei${if app.verbose then " -v" else ""}${if app.insecureWayland then " --insecure" else ""} run ${checkedConfig "hakurei-app-${app.name}.json" conf} $@ '' ) ]