diff options
| author | Ophestra <cat@gensokyo.uk> | 2025-08-28 01:07:51 +0900 |
|---|---|---|
| committer | Ophestra <cat@gensokyo.uk> | 2025-08-28 01:07:51 +0900 |
| commit | b14690aa77b3d5f86c4d1b1965675d6d82789e32 (patch) | |
| tree | 5765f705fd69dae7a65a0666451ac71a729a6aa8 /internal/app/container.go | |
| parent | d0b6852cd7db2bb1214c3267719d3ac108c0a53e (diff) | |
internal/app: remove seal interface
This further cleans up the package for the restructure.
Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'internal/app/container.go')
| -rw-r--r-- | internal/app/container.go | 252 |
1 files changed, 252 insertions, 0 deletions
diff --git a/internal/app/container.go b/internal/app/container.go new file mode 100644 index 00000000..cdefe170 --- /dev/null +++ b/internal/app/container.go @@ -0,0 +1,252 @@ +package app + +import ( + "errors" + "fmt" + "io/fs" + "maps" + "path" + "syscall" + + "hakurei.app/container" + "hakurei.app/container/seccomp" + "hakurei.app/hst" + "hakurei.app/internal/hlog" + "hakurei.app/internal/sys" + "hakurei.app/system/dbus" +) + +// in practice there should be less than 30 system mount points +const preallocateOpsCount = 1 << 5 + +// newContainer initialises [container.Params] via [hst.ContainerConfig]. +// Note that remaining container setup must be queued by the caller. +func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid *int) (*container.Params, map[string]string, error) { + if s == nil { + return nil, nil, hlog.WrapErr(syscall.EBADE, "invalid container configuration") + } + + params := &container.Params{ + Hostname: s.Hostname, + SeccompFlags: s.SeccompFlags, + SeccompPresets: s.SeccompPresets, + RetainSession: s.Tty, + HostNet: s.HostNet, + HostAbstract: s.HostAbstract, + + // the container is canceled when shim is requested to exit or receives an interrupt or termination signal; + // this behaviour is implemented in the shim + ForwardCancel: s.WaitDelay >= 0, + } + + as := &hst.ApplyState{ + AutoEtcPrefix: prefix, + } + { + ops := make(container.Ops, 0, preallocateOpsCount+len(s.Filesystem)) + params.Ops = &ops + as.Ops = &ops + } + + if s.Multiarch { + params.SeccompFlags |= seccomp.AllowMultiarch + } + + if !s.SeccompCompat { + params.SeccompPresets |= seccomp.PresetExt + } + if !s.Devel { + params.SeccompPresets |= seccomp.PresetDenyDevel + } + if !s.Userns { + params.SeccompPresets |= seccomp.PresetDenyNS + } + if !s.Tty { + params.SeccompPresets |= seccomp.PresetDenyTTY + } + + if s.MapRealUID { + params.Uid = os.Getuid() + *uid = params.Uid + params.Gid = os.Getgid() + *gid = params.Gid + } else { + *uid = container.OverflowUid() + *gid = container.OverflowGid() + } + + filesystem := s.Filesystem + var autoroot *hst.FSBind + // valid happens late, so root mount gets it here + if len(filesystem) > 0 && filesystem[0].Valid() && filesystem[0].Path().String() == container.FHSRoot { + // if the first element targets /, it is inserted early and excluded from path hiding + rootfs := filesystem[0].FilesystemConfig + filesystem = filesystem[1:] + rootfs.Apply(as) + + // autoroot requires special handling during path hiding + if b, ok := rootfs.(*hst.FSBind); ok && b.IsAutoRoot() { + autoroot = b + } + } + + params. + Proc(container.AbsFHSProc). + Tmpfs(hst.AbsTmp, 1<<12, 0755) + + if !s.Device { + params.DevWritable(container.AbsFHSDev, true) + } else { + params.Bind(container.AbsFHSDev, container.AbsFHSDev, container.BindWritable|container.BindDevice) + } + + /* retrieve paths and hide them if they're made available in the sandbox; + + this feature tries to improve user experience of permissive defaults, and + to warn about issues in custom configuration; it is NOT a security feature + and should not be treated as such, ALWAYS be careful with what you bind */ + var hidePaths []string + sc := os.Paths() + hidePaths = append(hidePaths, sc.RuntimePath.String(), sc.SharePath.String()) + _, systemBusAddr := dbus.Address() + if entries, err := dbus.Parse([]byte(systemBusAddr)); err != nil { + return nil, nil, err + } else { + // there is usually only one, do not preallocate + for _, entry := range entries { + if entry.Method != "unix" { + continue + } + for _, pair := range entry.Values { + if pair[0] == "path" { + if path.IsAbs(pair[1]) { + // get parent dir of socket + dir := path.Dir(pair[1]) + if dir == "." || dir == container.FHSRoot { + os.Printf("dbus socket %q is in an unusual location", pair[1]) + } + hidePaths = append(hidePaths, dir) + } else { + os.Printf("dbus socket %q is not absolute", pair[1]) + } + } + } + } + } + hidePathMatch := make([]bool, len(hidePaths)) + for i := range hidePaths { + if err := evalSymlinks(os, &hidePaths[i]); err != nil { + return nil, nil, err + } + } + + var hidePathSourceCount int + for i, c := range filesystem { + if !c.Valid() { + return nil, nil, fmt.Errorf("invalid filesystem at index %d", i) + } + c.Apply(as) + + // fs counter + hidePathSourceCount += len(c.Host()) + } + + // AutoRootOp is a collection of many BindMountOp internally + var autoRootEntries []fs.DirEntry + if autoroot != nil { + if d, err := os.ReadDir(autoroot.Source.String()); err != nil { + return nil, nil, err + } else { + // autoroot counter + hidePathSourceCount += len(d) + autoRootEntries = d + } + } + + hidePathSource := make([]*container.Absolute, 0, hidePathSourceCount) + + // fs append + for _, c := range filesystem { + // all entries already checked above + hidePathSource = append(hidePathSource, c.Host()...) + } + + // autoroot append + if autoroot != nil { + for _, ent := range autoRootEntries { + name := ent.Name() + if container.IsAutoRootBindable(name) { + hidePathSource = append(hidePathSource, autoroot.Source.Append(name)) + } + } + } + + // evaluated path, input path + hidePathSourceEval := make([][2]string, len(hidePathSource)) + for i, a := range hidePathSource { + if a == nil { + // unreachable + return nil, nil, syscall.ENOTRECOVERABLE + } + + hidePathSourceEval[i] = [2]string{a.String(), a.String()} + if err := evalSymlinks(os, &hidePathSourceEval[i][0]); err != nil { + return nil, nil, err + } + } + + for _, p := range hidePathSourceEval { + for i := range hidePaths { + // skip matched entries + if hidePathMatch[i] { + continue + } + + if ok, err := deepContainsH(p[0], hidePaths[i]); err != nil { + return nil, nil, err + } else if ok { + hidePathMatch[i] = true + os.Printf("hiding path %q from %q", hidePaths[i], p[1]) + } + } + } + + // cover matched paths + for i, ok := range hidePathMatch { + if ok { + if a, err := container.NewAbs(hidePaths[i]); err != nil { + var absoluteError *container.AbsoluteError + if !errors.As(err, &absoluteError) { + return nil, nil, err + } + if absoluteError == nil { + return nil, nil, syscall.ENOTRECOVERABLE + } + return nil, nil, fmt.Errorf("invalid path hiding candidate %q", absoluteError.Pathname) + } else { + params.Tmpfs(a, 1<<13, 0755) + } + } + } + + // no more ContainerConfig paths beyond this point + if !s.Device { + params. + Remount(container.AbsFHSDev, syscall.MS_RDONLY). + Tmpfs(container.AbsFHSDev.Append("shm"), 0, 01777) + } + + return params, maps.Clone(s.Env), nil +} + +func evalSymlinks(os sys.State, v *string) error { + if p, err := os.EvalSymlinks(*v); err != nil { + if !errors.Is(err, fs.ErrNotExist) { + return err + } + os.Printf("path %q does not yet exist", *v) + } else { + *v = p + } + return nil +} |
