From 274686d10d3386a41f8fb6bc3363269a734afe0e Mon Sep 17 00:00:00 2001 From: Ophestra Date: Wed, 29 Oct 2025 03:40:09 +0900 Subject: internal/validate: relocate from app These are free of the dispatcher from internal/app. This change relocates them into their own package. Signed-off-by: Ophestra --- internal/app/outcome.go | 2 +- internal/app/path.go | 15 ------- internal/app/path_test.go | 88 ------------------------------------- internal/app/spaccount.go | 3 +- internal/app/spcontainer.go | 3 +- internal/app/sysconf.go | 8 ---- internal/app/sysconf_test.go | 17 ------- internal/app/username.go | 12 ----- internal/app/username_test.go | 28 ------------ internal/validate/sysconf.go | 8 ++++ internal/validate/sysconf_test.go | 21 +++++++++ internal/validate/username.go | 12 +++++ internal/validate/username_test.go | 30 +++++++++++++ internal/validate/validate.go | 20 +++++++++ internal/validate/validate_test.go | 90 ++++++++++++++++++++++++++++++++++++++ 15 files changed, 186 insertions(+), 171 deletions(-) delete mode 100644 internal/app/path.go delete mode 100644 internal/app/path_test.go delete mode 100644 internal/app/sysconf.go delete mode 100644 internal/app/sysconf_test.go delete mode 100644 internal/app/username.go delete mode 100644 internal/app/username_test.go create mode 100644 internal/validate/sysconf.go create mode 100644 internal/validate/sysconf_test.go create mode 100644 internal/validate/username.go create mode 100644 internal/validate/username_test.go create mode 100644 internal/validate/validate.go create mode 100644 internal/validate/validate_test.go (limited to 'internal') diff --git a/internal/app/outcome.go b/internal/app/outcome.go index f462dfd5..237095af 100644 --- a/internal/app/outcome.go +++ b/internal/app/outcome.go @@ -43,7 +43,7 @@ type outcomeState struct { Identity int // Copied from Identity. identity *stringPair[int] - // Returned by [Hsu.MustIDMsg]. + // Returned by [Hsu.MustID]. UserID int // Target init namespace uid resolved from UserID and identity. uid *stringPair[int] diff --git a/internal/app/path.go b/internal/app/path.go deleted file mode 100644 index 493dfe39..00000000 --- a/internal/app/path.go +++ /dev/null @@ -1,15 +0,0 @@ -package app - -import ( - "path/filepath" - "strings" -) - -func deepContainsH(basepath, targpath string) (bool, error) { - const upper = ".." + string(filepath.Separator) - - rel, err := filepath.Rel(basepath, targpath) - return err == nil && - rel != ".." && - !strings.HasPrefix(rel, upper), err -} diff --git a/internal/app/path_test.go b/internal/app/path_test.go deleted file mode 100644 index 1f2d8fa6..00000000 --- a/internal/app/path_test.go +++ /dev/null @@ -1,88 +0,0 @@ -package app - -import ( - "testing" -) - -func TestDeepContainsH(t *testing.T) { - t.Parallel() - - testCases := []struct { - name string - basepath string - targpath string - want bool - wantErr bool - }{ - { - name: "empty", - want: true, - }, - { - name: "equal abs", - basepath: "/run", - targpath: "/run", - want: true, - }, - { - name: "equal rel", - basepath: "./run", - targpath: "run", - want: true, - }, - { - name: "contains abs", - basepath: "/run", - targpath: "/run/dbus", - want: true, - }, - { - name: "inverse contains abs", - basepath: "/run/dbus", - targpath: "/run", - want: false, - }, - { - name: "contains rel", - basepath: "../run", - targpath: "../run/dbus", - want: true, - }, - { - name: "inverse contains rel", - basepath: "../run/dbus", - targpath: "../run", - want: false, - }, - { - name: "weird abs", - basepath: "/run/dbus", - targpath: "/run/dbus/../current-system", - want: false, - }, - { - name: "weird rel", - basepath: "../run/dbus", - targpath: "../run/dbus/../current-system", - want: false, - }, - - { - name: "invalid mix", - basepath: "/run", - targpath: "./run", - wantErr: true, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - if got, err := deepContainsH(tc.basepath, tc.targpath); (err != nil) != tc.wantErr { - t.Errorf("deepContainsH() error = %v, wantErr %v", err, tc.wantErr) - } else if got != tc.want { - t.Errorf("deepContainsH() = %v, want %v", got, tc.want) - } - }) - } -} diff --git a/internal/app/spaccount.go b/internal/app/spaccount.go index 7da54366..c9f08fe9 100644 --- a/internal/app/spaccount.go +++ b/internal/app/spaccount.go @@ -6,6 +6,7 @@ import ( "syscall" "hakurei.app/container/fhs" + "hakurei.app/internal/validate" ) func init() { gob.Register(spAccountOp{}) } @@ -21,7 +22,7 @@ func (s spAccountOp) toSystem(state *outcomeStateSys) error { } // default is applied in toContainer - if state.Container.Username != "" && !isValidUsername(state.Container.Username) { + if state.Container.Username != "" && !validate.IsValidUsername(state.Container.Username) { return newWithMessage(fmt.Sprintf("invalid user name %q", state.Container.Username)) } return nil diff --git a/internal/app/spcontainer.go b/internal/app/spcontainer.go index 65071f7c..d411cb9f 100644 --- a/internal/app/spcontainer.go +++ b/internal/app/spcontainer.go @@ -16,6 +16,7 @@ import ( "hakurei.app/container/fhs" "hakurei.app/container/seccomp" "hakurei.app/hst" + "hakurei.app/internal/validate" "hakurei.app/message" "hakurei.app/system" "hakurei.app/system/acl" @@ -243,7 +244,7 @@ func (s *spFilesystemOp) toSystem(state *outcomeStateSys) error { continue } - if ok, err := deepContainsH(p[0], hidePaths[i]); err != nil { + if ok, err := validate.DeepContainsH(p[0], hidePaths[i]); err != nil { return &hst.AppError{Step: "determine path hiding outcome", Err: err} } else if ok { hidePathMatch[i] = true diff --git a/internal/app/sysconf.go b/internal/app/sysconf.go deleted file mode 100644 index 4c5714a9..00000000 --- a/internal/app/sysconf.go +++ /dev/null @@ -1,8 +0,0 @@ -package app - -//#include -import "C" - -const _SC_LOGIN_NAME_MAX = C._SC_LOGIN_NAME_MAX - -func sysconf(name C.int) int { return int(C.sysconf(name)) } diff --git a/internal/app/sysconf_test.go b/internal/app/sysconf_test.go deleted file mode 100644 index fb0c7f8a..00000000 --- a/internal/app/sysconf_test.go +++ /dev/null @@ -1,17 +0,0 @@ -package app - -import "testing" - -const ( - _POSIX_LOGIN_NAME_MAX = 9 -) - -func TestSysconf(t *testing.T) { - t.Parallel() - - t.Run("LOGIN_NAME_MAX", func(t *testing.T) { - if got := sysconf(_SC_LOGIN_NAME_MAX); got < _POSIX_LOGIN_NAME_MAX { - t.Errorf("sysconf(_SC_LOGIN_NAME_MAX): %d < _POSIX_LOGIN_NAME_MAX", got) - } - }) -} diff --git a/internal/app/username.go b/internal/app/username.go deleted file mode 100644 index 566daf5f..00000000 --- a/internal/app/username.go +++ /dev/null @@ -1,12 +0,0 @@ -package app - -import "regexp" - -// nameRegex is the default NAME_REGEX value from adduser. -var nameRegex = regexp.MustCompilePOSIX(`^[a-zA-Z][a-zA-Z0-9_-]*\$?$`) - -// isValidUsername returns whether the argument is a valid username -func isValidUsername(username string) bool { - return len(username) < sysconf(_SC_LOGIN_NAME_MAX) && - nameRegex.MatchString(username) -} diff --git a/internal/app/username_test.go b/internal/app/username_test.go deleted file mode 100644 index 1b2e0459..00000000 --- a/internal/app/username_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package app - -import ( - "strings" - "testing" -) - -func TestIsValidUsername(t *testing.T) { - t.Parallel() - - t.Run("long", func(t *testing.T) { - if isValidUsername(strings.Repeat("a", sysconf(_SC_LOGIN_NAME_MAX))) { - t.Errorf("isValidUsername unexpected true") - } - }) - - t.Run("regexp", func(t *testing.T) { - if isValidUsername("0") { - t.Errorf("isValidUsername unexpected true") - } - }) - - t.Run("valid", func(t *testing.T) { - if !isValidUsername("alice") { - t.Errorf("isValidUsername unexpected false") - } - }) -} diff --git a/internal/validate/sysconf.go b/internal/validate/sysconf.go new file mode 100644 index 00000000..0960e9dd --- /dev/null +++ b/internal/validate/sysconf.go @@ -0,0 +1,8 @@ +package validate + +//#include +import "C" + +const SC_LOGIN_NAME_MAX = C._SC_LOGIN_NAME_MAX + +func Sysconf(name C.int) int { return int(C.sysconf(name)) } diff --git a/internal/validate/sysconf_test.go b/internal/validate/sysconf_test.go new file mode 100644 index 00000000..95502d34 --- /dev/null +++ b/internal/validate/sysconf_test.go @@ -0,0 +1,21 @@ +package validate_test + +import ( + "testing" + + "hakurei.app/internal/validate" +) + +const ( + _POSIX_LOGIN_NAME_MAX = 9 +) + +func TestSysconf(t *testing.T) { + t.Parallel() + + t.Run("LOGIN_NAME_MAX", func(t *testing.T) { + if got := validate.Sysconf(validate.SC_LOGIN_NAME_MAX); got < _POSIX_LOGIN_NAME_MAX { + t.Errorf("sysconf(_SC_LOGIN_NAME_MAX): %d < _POSIX_LOGIN_NAME_MAX", got) + } + }) +} diff --git a/internal/validate/username.go b/internal/validate/username.go new file mode 100644 index 00000000..2249cd27 --- /dev/null +++ b/internal/validate/username.go @@ -0,0 +1,12 @@ +package validate + +import "regexp" + +// nameRegex is the default NAME_REGEX value from adduser. +var nameRegex = regexp.MustCompilePOSIX(`^[a-zA-Z][a-zA-Z0-9_-]*\$?$`) + +// IsValidUsername returns whether the argument is a valid username. +func IsValidUsername(username string) bool { + return len(username) < Sysconf(SC_LOGIN_NAME_MAX) && + nameRegex.MatchString(username) +} diff --git a/internal/validate/username_test.go b/internal/validate/username_test.go new file mode 100644 index 00000000..1970fa4a --- /dev/null +++ b/internal/validate/username_test.go @@ -0,0 +1,30 @@ +package validate_test + +import ( + "strings" + "testing" + + "hakurei.app/internal/validate" +) + +func TestIsValidUsername(t *testing.T) { + t.Parallel() + + t.Run("long", func(t *testing.T) { + if validate.IsValidUsername(strings.Repeat("a", validate.Sysconf(validate.SC_LOGIN_NAME_MAX))) { + t.Errorf("IsValidUsername unexpected true") + } + }) + + t.Run("regexp", func(t *testing.T) { + if validate.IsValidUsername("0") { + t.Errorf("IsValidUsername unexpected true") + } + }) + + t.Run("valid", func(t *testing.T) { + if !validate.IsValidUsername("alice") { + t.Errorf("IsValidUsername unexpected false") + } + }) +} diff --git a/internal/validate/validate.go b/internal/validate/validate.go new file mode 100644 index 00000000..a4e82753 --- /dev/null +++ b/internal/validate/validate.go @@ -0,0 +1,20 @@ +// Package validate provides functions for validating string values of various types. +package validate + +import ( + "path/filepath" + "strings" +) + +// DeepContainsH returns whether basepath is equivalent to or is the parent of targpath. +// +// This is used for path hiding warning behaviour, the purpose of which is to improve +// user experience and is *not* a security feature and must not be treated as such. +func DeepContainsH(basepath, targpath string) (bool, error) { + const upper = ".." + string(filepath.Separator) + + rel, err := filepath.Rel(basepath, targpath) + return err == nil && + rel != ".." && + !strings.HasPrefix(rel, upper), err +} diff --git a/internal/validate/validate_test.go b/internal/validate/validate_test.go new file mode 100644 index 00000000..338e5693 --- /dev/null +++ b/internal/validate/validate_test.go @@ -0,0 +1,90 @@ +package validate_test + +import ( + "testing" + + "hakurei.app/internal/validate" +) + +func TestDeepContainsH(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + basepath string + targpath string + want bool + wantErr bool + }{ + { + name: "empty", + want: true, + }, + { + name: "equal abs", + basepath: "/run", + targpath: "/run", + want: true, + }, + { + name: "equal rel", + basepath: "./run", + targpath: "run", + want: true, + }, + { + name: "contains abs", + basepath: "/run", + targpath: "/run/dbus", + want: true, + }, + { + name: "inverse contains abs", + basepath: "/run/dbus", + targpath: "/run", + want: false, + }, + { + name: "contains rel", + basepath: "../run", + targpath: "../run/dbus", + want: true, + }, + { + name: "inverse contains rel", + basepath: "../run/dbus", + targpath: "../run", + want: false, + }, + { + name: "weird abs", + basepath: "/run/dbus", + targpath: "/run/dbus/../current-system", + want: false, + }, + { + name: "weird rel", + basepath: "../run/dbus", + targpath: "../run/dbus/../current-system", + want: false, + }, + + { + name: "invalid mix", + basepath: "/run", + targpath: "./run", + wantErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + if got, err := validate.DeepContainsH(tc.basepath, tc.targpath); (err != nil) != tc.wantErr { + t.Errorf("DeepContainsH: error = %v, wantErr %v", err, tc.wantErr) + } else if got != tc.want { + t.Errorf("DeepContainsH: = %v, want %v", got, tc.want) + } + }) + } +} -- cgit v1.3.1