From 46359623916c01d167fdfe1121c6f3eba2ded81e Mon Sep 17 00:00:00 2001 From: Andrew LeFevre Date: Wed, 8 Oct 2025 12:27:13 -0400 Subject: [PATCH 1/5] add GetSeUserByName Signed-off-by: Andrew LeFevre --- go-selinux/selinux.go | 7 ++ go-selinux/selinux_linux.go | 111 +++++++++++++++++++++++++ go-selinux/selinux_linux_test.go | 137 +++++++++++++++++++++++++++++++ go-selinux/selinux_stub.go | 4 + 4 files changed, 259 insertions(+) diff --git a/go-selinux/selinux.go b/go-selinux/selinux.go index 9f0740e..e60cbd8 100644 --- a/go-selinux/selinux.go +++ b/go-selinux/selinux.go @@ -305,6 +305,13 @@ func DisableSecOpt() []string { return []string{"disable"} } +// GetSeUserByName retrieves the SELinux username and security level for a given +// Linux username. The username and security level is based on the +// /etc/selinux/{SELINUXTYPE}/seusers file. +func GetSeUserByName(username string) (seUser string, level string, err error) { + return getSeUserByName(username) +} + // GetDefaultContextWithLevel gets a single context for the specified SELinux user // identity that is reachable from the specified scon context. The context is based // on the per-user /etc/selinux/{SELINUXTYPE}/contexts/users/ if it exists, diff --git a/go-selinux/selinux_linux.go b/go-selinux/selinux_linux.go index 0875205..f21c541 100644 --- a/go-selinux/selinux_linux.go +++ b/go-selinux/selinux_linux.go @@ -1181,6 +1181,117 @@ func dupSecOpt(src string) ([]string, error) { return dup, nil } +// checkGroup returns true if group's GID is in the list of GIDs gids. +func checkGroup(group string, gids []string, lookupGroup func(string) (*user.Group, error)) bool { + grp, err := lookupGroup(group) + if err != nil { + return false + } + + for _, gid := range gids { + if grp.Gid == gid { + return true + } + } + return false +} + +// getSeUserFromReader reads the seusers file: https://www.man7.org/linux/man-pages/man5/seusers.5.html +func getSeUserFromReader(username string, gids []string, r io.Reader, lookupGroup func(string) (*user.Group, error)) (seUser string, level string, err error) { + var defaultSeUser, defaultLevel string + var groupSeUser, groupLevel string + + lineNum := -1 + scanner := bufio.NewScanner(r) + for scanner.Scan() { + line := scanner.Text() + lineNum++ + + // remove any trailing comments, then extra whitespace + parts := strings.SplitN(line, "#", 2) + line = strings.TrimSpace(parts[0]) + if line == "" { + continue + } + + parts = strings.SplitN(line, ":", 3) + if len(parts) < 2 { + return "", "", fmt.Errorf("line %d: malformed line", lineNum) + } + userField := parts[0] + if userField == "" { + return "", "", fmt.Errorf("line %d: user_id or group_id is empty", lineNum) + } + seUserField := parts[1] + if seUserField == "" { + return "", "", fmt.Errorf("line %d: seuser_id is empty", lineNum) + } + var levelField string + // level is optional + if len(parts) > 2 { + levelField = parts[2] + } + + // we found a match, return it + if userField == username { + return seUserField, levelField, nil + } + + // if the first field starts with '%' it's a group, check if + // the user is a member of that group and set the group + // SELinux user and level if so + if userField[0] == '%' && groupSeUser == "" { + if checkGroup(userField[1:], gids, lookupGroup) { + groupSeUser = seUserField + groupLevel = levelField + } + } else if userField == "__default__" && defaultSeUser == "" { + defaultSeUser = seUserField + defaultLevel = levelField + } + } + if err := scanner.Err(); err != nil { + return "", "", fmt.Errorf("failed to read seusers file: %w", err) + } + + if groupSeUser != "" { + return groupSeUser, groupLevel, nil + } + if defaultSeUser != "" { + return defaultSeUser, defaultLevel, nil + } + + return "", "", fmt.Errorf("could not find SELinux user for %q login", username) +} + +// getSeUserByName returns an SELinux user and MLS level that is +// mapped to a given Linux user. +func getSeUserByName(username string) (seUser string, level string, err error) { + seUsersConf := filepath.Join(policyRoot(), "seusers") + confFile, err := os.Open(seUsersConf) + if err != nil { + return "", "", fmt.Errorf("failed to open seusers file: %w", err) + } + defer confFile.Close() + + usr, err := user.Lookup(username) + if err != nil { + return "", "", fmt.Errorf("failed to lookup user %q", username) + } + gids, err := usr.GroupIds() + if err != nil { + return "", "", fmt.Errorf("failed to find user %q's groups", username) + } + gids = append([]string{usr.Gid}, gids...) + + seUser, level, err = getSeUserFromReader(username, gids, confFile, user.LookupGroup) + if err != nil { + return "", "", fmt.Errorf("failed to parse seusers file: %w", err) + } + + return seUser, level, nil +} + // findUserInContext scans the reader for a valid SELinux context // match that is verified with the verifier. Invalid contexts are // skipped. It returns a matched context or an empty string if no diff --git a/go-selinux/selinux_linux_test.go b/go-selinux/selinux_linux_test.go index 71aa0b8..53fff46 100644 --- a/go-selinux/selinux_linux_test.go +++ b/go-selinux/selinux_linux_test.go @@ -580,6 +580,143 @@ func TestGlbLub(t *testing.T) { } } +func TestGetSeUser(t *testing.T) { + lookupGroup := func(string) (*user.Group, error) { + return &user.Group{ + Gid: "42", + Name: "group", + }, nil + } + + tests := []struct { + name string + username string + gids []string + seUserBuf string + seUser string + level string + expectedErr string + }{ + { + name: "one entry match", + username: "bob", + seUserBuf: "bob:staff_u:s0", + seUser: "staff_u", + level: "s0", + }, + { + name: "match with no level", + username: "bob", + seUserBuf: "bob:staff_u", + seUser: "staff_u", + }, + { + name: "match", + username: "bob", + seUserBuf: ` +system_u:system_u:s0-s15:c0.c255 +root:root:s0-s15:c0.c255 +bob:staff_u:s0-s15:c0.c255`, + seUser: "staff_u", + level: "s0-s15:c0.c255", + }, + { + name: "match with comment", + username: "bob", + seUserBuf: ` +system_u:system_u:s0-s15:c0.c255 +# foobar +root:root:s0-s15:c0.c255 +bob:staff_u:s0-s15:c0.c255 #baz`, + seUser: "staff_u", + level: "s0-s15:c0.c255", + }, + { + name: "no match", + username: "bob", + seUserBuf: ` +system_u:system_u:s0-s15:c0.c255 +root:root:s0-s15:c0.c255`, + expectedErr: `could not find SELinux user for "bob" login`, + }, + { + name: "group match", + username: "bob", + gids: []string{"42"}, + seUserBuf: ` +system_u:system_u:s0-s15:c0.c255 +root:root:s0-s15:c0.c255 +%group:staff_u:s0`, + seUser: "staff_u", + level: "s0", + }, + { + name: "no group match", + username: "bob", + gids: []string{"99"}, + seUserBuf: ` +system_u:system_u:s0-s15:c0.c255 +root:root:s0-s15:c0.c255 +%group:staff_u:s0`, + expectedErr: `could not find SELinux user for "bob" login`, + }, + { + name: "malformed line", + username: "bob", + seUserBuf: ` +system_u:system_u:s0-s15:c0.c255 +root:root:s0-s15:c0.c255 +foobar +bob:staff_u:s0-s15:c0.c255`, + expectedErr: "line 3: malformed line", + }, + { + name: "empty user", + username: "bob", + seUserBuf: ` +system_u:system_u:s0-s15:c0.c255 +root:root:s0-s15:c0.c255 +:seuser_u +bob:staff_u:s0-s15:c0.c255`, + expectedErr: "line 3: user_id or group_id is empty", + }, + { + name: "empty seuser", + username: "bob", + seUserBuf: ` +system_u:system_u:s0-s15:c0.c255 +root:root:s0-s15:c0.c255 +user::s0 +bob:staff_u:s0-s15:c0.c255`, + expectedErr: "line 3: seuser_id is empty", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + r := bytes.NewBufferString(tt.seUserBuf) + seUser, level, err := getSeUserFromReader(tt.username, tt.gids, r, lookupGroup) + if tt.expectedErr != "" { + if err == nil { + t.Fatal("expected an error but got nil") + } else if err.Error() != tt.expectedErr { + t.Fatalf("got error: %q but expected %q", err.Error(), tt.expectedErr) + } + } else if tt.expectedErr == "" && err != nil { + t.Fatalf("err should not exist but is: %v", err) + } + + if seUser != tt.seUser { + t.Fatalf("got seUser: %q but expected %q", seUser, tt.seUser) + } + if level != tt.level { + t.Fatalf("got level: %q but expected %q", level, tt.level) + } + }) + } +} + func TestContextWithLevel(t *testing.T) { want := "bob:sysadm_r:sysadm_t:SystemLow-SystemHigh" diff --git a/go-selinux/selinux_stub.go b/go-selinux/selinux_stub.go index 0889fbe..4105c4c 100644 --- a/go-selinux/selinux_stub.go +++ b/go-selinux/selinux_stub.go @@ -146,6 +146,10 @@ func dupSecOpt(string) ([]string, error) { return nil, nil } +func getSeUserByName(string) (string, string, error) { + return "", "", nil +} + func getDefaultContextWithLevel(string, string, string) (string, error) { return "", nil } From 3a2844421ea5825f69483ecc248450e84cf12b9c Mon Sep 17 00:00:00 2001 From: Andrew LeFevre Date: Wed, 8 Oct 2025 12:28:53 -0400 Subject: [PATCH 2/5] fallback to failsafe context in GetDefaultContextWithLevel Signed-off-by: Andrew LeFevre --- go-selinux/selinux.go | 3 +- go-selinux/selinux_linux.go | 66 +++++++++++++++++++++++++++----- go-selinux/selinux_linux_test.go | 37 +++++++++++------- 3 files changed, 82 insertions(+), 24 deletions(-) diff --git a/go-selinux/selinux.go b/go-selinux/selinux.go index e60cbd8..d025272 100644 --- a/go-selinux/selinux.go +++ b/go-selinux/selinux.go @@ -316,7 +316,8 @@ func GetSeUserByName(username string) (seUser string, level string, err error) { // identity that is reachable from the specified scon context. The context is based // on the per-user /etc/selinux/{SELINUXTYPE}/contexts/users/ if it exists, // and falls back to the global /etc/selinux/{SELINUXTYPE}/contexts/default_contexts -// file. +// file and finally the global /etc/selinux/{SELINUXTYPE}/contexts/failsafe_context +// file if no match can be found anywhere else. func GetDefaultContextWithLevel(user, level, scon string) (string, error) { return getDefaultContextWithLevel(user, level, scon) } diff --git a/go-selinux/selinux_linux.go b/go-selinux/selinux_linux.go index f21c541..7261c88 100644 --- a/go-selinux/selinux_linux.go +++ b/go-selinux/selinux_linux.go @@ -27,6 +27,7 @@ const ( selinuxDir = "/etc/selinux/" selinuxUsersDir = "contexts/users" defaultContexts = "contexts/default_contexts" + failsafeContext = "contexts/failsafe_context" selinuxConfig = selinuxDir + "config" selinuxfsMount = "/sys/fs/selinux" selinuxTypeTag = "SELINUXTYPE" @@ -57,6 +58,7 @@ type defaultSECtx struct { userRdr io.Reader verifier func(string) error defaultRdr io.Reader + failsafeRdr io.Reader user, level, scon string } @@ -1349,6 +1351,33 @@ func findUserInContext(context Context, r io.Reader, verifier func(string) error return "", nil } +// getFailsafeContext returns the context in the failsafe_context file: +// https://www.man7.org/linux/man-pages/man5/failsafe_context.5.html +func getFailsafeContext(context Context, r io.Reader, verifier func(string) error) (string, error) { + conn := make([]byte, 256) + limReader := io.LimitReader(r, int64(len(conn))) + _, err := limReader.Read(conn) + if err != nil { + return "", fmt.Errorf("failed to read failsafe context: %w", err) + } + + conn = bytes.TrimSpace(conn) + toConns := strings.SplitN(string(conn), ":", 4) + if len(toConns) != 3 { + return "", nil + } + + context["role"] = toConns[0] + context["type"] = toConns[1] + + outConn := context.get() + if err := verifier(outConn); err != nil { + return "", nil + } + + return outConn, nil +} + func getDefaultContextFromReaders(c *defaultSECtx) (string, error) { if c.verifier == nil { return "", ErrVerifierNil @@ -1365,7 +1394,7 @@ func getDefaultContextFromReaders(c *defaultSECtx) (string, error) { conn, err := findUserInContext(context, c.userRdr, c.verifier) if err != nil { - return "", err + return "", fmt.Errorf("failed to read %q's user context file: %w", c.user, err) } if conn != "" { @@ -1374,7 +1403,16 @@ func getDefaultContextFromReaders(c *defaultSECtx) (string, error) { conn, err = findUserInContext(context, c.defaultRdr, c.verifier) if err != nil { - return "", err + return "", fmt.Errorf("failed to read default user context file: %w", err) + } + + if conn != "" { + return conn, nil + } + + conn, err = getFailsafeContext(context, c.failsafeRdr, c.verifier) + if err != nil { + return "", fmt.Errorf("failed to read failsafe_context: %w", err) } if conn != "" { @@ -1388,24 +1426,32 @@ func getDefaultContextWithLevel(user, level, scon string) (string, error) { userPath := filepath.Join(policyRoot(), selinuxUsersDir, user) fu, err := os.Open(userPath) if err != nil { - return "", err + return "", fmt.Errorf("failed to open %q's user context file: %w", user, err) } defer fu.Close() defaultPath := filepath.Join(policyRoot(), defaultContexts) fd, err := os.Open(defaultPath) if err != nil { - return "", err + return "", fmt.Errorf("failed to open default user context file: %w", err) } defer fd.Close() + failsafePath := filepath.Join(policyRoot(), failsafeContext) + fs, err := os.Open(failsafePath) + if err != nil { + return "", fmt.Errorf("failed to open failsafe user context file: %w", err) + } + defer fs.Close() + c := defaultSECtx{ - user: user, - level: level, - scon: scon, - userRdr: fu, - defaultRdr: fd, - verifier: securityCheckContext, + user: user, + level: level, + scon: scon, + userRdr: fu, + defaultRdr: fd, + failsafeRdr: fs, + verifier: securityCheckContext, } return getDefaultContextFromReaders(&c) diff --git a/go-selinux/selinux_linux_test.go b/go-selinux/selinux_linux_test.go index 53fff46..91fed5c 100644 --- a/go-selinux/selinux_linux_test.go +++ b/go-selinux/selinux_linux_test.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "os" + "os/user" "path/filepath" "runtime" "strconv" @@ -694,7 +695,6 @@ bob:staff_u:s0-s15:c0.c255`, for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - r := bytes.NewBufferString(tt.seUserBuf) seUser, level, err := getSeUserFromReader(tt.username, tt.gids, r, lookupGroup) if tt.expectedErr != "" { @@ -724,6 +724,7 @@ func TestContextWithLevel(t *testing.T) { foo_r:foo_t:s0 sysadm_r:sysadm_t:s0 staff_r:staff_t:s0 baz_r:baz_t:s0 sysadm_r:sysadm_t:s0 ` + goodFailsafeBuff := "unconfined_r:unconfined_t:s0" verifier := func(con string) error { if con != want { @@ -734,7 +735,7 @@ staff_r:staff_t:s0 baz_r:baz_t:s0 sysadm_r:sysadm_t:s0 } tests := []struct { - name, userBuff, defaultBuff string + name, userBuff, defaultBuff, failsafeBuff string }{ { name: "match exists in user context file", @@ -743,7 +744,8 @@ foo_r:foo_t:s0 sysadm_r:sysadm_t:s0 staff_r:staff_t:s0 baz_r:baz_t:s0 sysadm_r:sysadm_t:s0 `, - defaultBuff: goodDefaultBuff, + defaultBuff: goodDefaultBuff, + failsafeBuff: goodFailsafeBuff, }, { name: "match exists in default context file, but not in user file", @@ -751,7 +753,8 @@ staff_r:staff_t:s0 baz_r:baz_t:s0 sysadm_r:sysadm_t:s0 foo_r:foo_t:s0 sysadm_r:sysadm_t:s0 fake_r:fake_t:s0 baz_r:baz_t:s0 sysadm_r:sysadm_t:s0 `, - defaultBuff: goodDefaultBuff, + defaultBuff: goodDefaultBuff, + failsafeBuff: goodFailsafeBuff, }, } @@ -785,17 +788,25 @@ fake_r:fake_t:s0 baz_r:baz_t:s0 sysadm_r:sysadm_t:s0 dne_r:dne_t:s0 baz_r:baz_t:s0 sysadm_r:sysadm_t:s0 ` c := defaultSECtx{ - user: "bob", - level: "SystemLow-SystemHigh", - scon: "system_u:staff_r:staff_t:s0", - userRdr: bytes.NewBufferString(badUserBuff), - defaultRdr: bytes.NewBufferString(badDefaultBuff), - verifier: verifier, + user: "bob", + level: "SystemLow-SystemHigh", + scon: "system_u:staff_r:staff_t:s0", + userRdr: bytes.NewBufferString(badUserBuff), + defaultRdr: bytes.NewBufferString(badDefaultBuff), + failsafeRdr: bytes.NewBufferString(goodFailsafeBuff), + verifier: func(s string) error { + return nil + }, + } + + got, err := getDefaultContextFromReaders(&c) + if err != nil { + t.Fatalf("err should not exist but is: %v", err) } - _, err := getDefaultContextFromReaders(&c) - if err == nil { - t.Fatalf("err was expected") + const want string = "bob:unconfined_r:unconfined_t:SystemLow-SystemHigh" + if got != want { + t.Fatalf("got context: %q but expected %q", got, want) } }) } From fb43b054e238c1fa8d909f9132dbf0055ea275b3 Mon Sep 17 00:00:00 2001 From: Andrew LeFevre Date: Fri, 10 Oct 2025 10:33:41 -0400 Subject: [PATCH 3/5] address feedback on getseuserbyname --- go-selinux/selinux_linux.go | 23 +++++++++++------------ go-selinux/selinux_linux_test.go | 21 +++++++++++++++++++++ 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/go-selinux/selinux_linux.go b/go-selinux/selinux_linux.go index 7261c88..471acb1 100644 --- a/go-selinux/selinux_linux.go +++ b/go-selinux/selinux_linux.go @@ -1210,28 +1210,27 @@ func getSeUserFromReader(username string, gids []string, r io.Reader, lookupGrou lineNum++ // remove any trailing comments, then extra whitespace - parts := strings.SplitN(line, "#", 2) - line = strings.TrimSpace(parts[0]) + line, _, _ = strings.Cut(line, "#") + line = strings.TrimSpace(line) if line == "" { continue } - parts = strings.SplitN(line, ":", 3) - if len(parts) < 2 { + userField, rest, ok := strings.Cut(line, ":") + if !ok { return "", "", fmt.Errorf("line %d: malformed line", lineNum) } - userField := parts[0] if userField == "" { return "", "", fmt.Errorf("line %d: user_id or group_id is empty", lineNum) } - seUserField := parts[1] + seUserField, rest, ok := strings.Cut(rest, ":") if seUserField == "" { return "", "", fmt.Errorf("line %d: seuser_id is empty", lineNum) } var levelField string // level is optional - if len(parts) > 2 { - levelField = parts[2] + if ok { + levelField = rest } // we found a match, return it @@ -1268,7 +1267,7 @@ func getSeUserFromReader(username string, gids []string, r io.Reader, lookupGrou // getSeUserByName returns an SELinux user and MLS level that is // mapped to a given Linux user. -func getSeUserByName(username string) (seUser string, level string, err error) { +func getSeUserByName(username string) (string, string, error) { seUsersConf := filepath.Join(policyRoot(), "seusers") confFile, err := os.Open(seUsersConf) if err != nil { @@ -1278,15 +1277,15 @@ func getSeUserByName(username string) (seUser string, level string, err error) { usr, err := user.Lookup(username) if err != nil { - return "", "", fmt.Errorf("failed to lookup user %q", username) + return "", "", err } gids, err := usr.GroupIds() if err != nil { - return "", "", fmt.Errorf("failed to find user %q's groups", username) + return "", "", err } gids = append([]string{usr.Gid}, gids...) - seUser, level, err = getSeUserFromReader(username, gids, confFile, user.LookupGroup) + seUser, level, err := getSeUserFromReader(username, gids, confFile, user.LookupGroup) if err != nil { return "", "", fmt.Errorf("failed to parse seusers file: %w", err) } diff --git a/go-selinux/selinux_linux_test.go b/go-selinux/selinux_linux_test.go index 91fed5c..97e35c8 100644 --- a/go-selinux/selinux_linux_test.go +++ b/go-selinux/selinux_linux_test.go @@ -691,6 +691,27 @@ user::s0 bob:staff_u:s0-s15:c0.c255`, expectedErr: "line 3: seuser_id is empty", }, + { + name: "one entry match with whitespace", + username: "bob", + seUserBuf: " bob:staff_u:s0 ", + seUser: "staff_u", + level: "s0", + }, + { + name: "one entry match with trailing comment", + username: "bob", + seUserBuf: "bob:staff_u:s0#comment", + seUser: "staff_u", + level: "s0", + }, + { + name: "one entry match with whitespace and trailing comment", + username: "bob", + seUserBuf: " bob:staff_u:s0 #comment ", + seUser: "staff_u", + level: "s0", + }, } for _, tt := range tests { From 6d93352ca59abc019e79c2cda36411e2335fb9d9 Mon Sep 17 00:00:00 2001 From: Andrew LeFevre Date: Fri, 10 Oct 2025 11:28:29 -0400 Subject: [PATCH 4/5] open context files lazily --- go-selinux/selinux_linux.go | 69 ++++++++++++++++++-------------- go-selinux/selinux_linux_test.go | 35 ++++++++++------ 2 files changed, 62 insertions(+), 42 deletions(-) diff --git a/go-selinux/selinux_linux.go b/go-selinux/selinux_linux.go index 471acb1..395b861 100644 --- a/go-selinux/selinux_linux.go +++ b/go-selinux/selinux_linux.go @@ -54,11 +54,19 @@ type mlsRange struct { high *level } +type openReaderCloser func() (io.ReadCloser, error) + +func createOpener(path string) openReaderCloser { + return func() (io.ReadCloser, error) { + return os.Open(path) + } +} + type defaultSECtx struct { - userRdr io.Reader + openUserRdr openReaderCloser verifier func(string) error - defaultRdr io.Reader - failsafeRdr io.Reader + openDefaultRdr openReaderCloser + openFailsafeRdr openReaderCloser user, level, scon string } @@ -1391,7 +1399,13 @@ func getDefaultContextFromReaders(c *defaultSECtx) (string, error) { context["user"] = c.user context["level"] = c.level - conn, err := findUserInContext(context, c.userRdr, c.verifier) + userRdr, err := c.openUserRdr() + if err != nil { + return "", fmt.Errorf("failed to open user context file: %w", err) + } + defer userRdr.Close() + + conn, err := findUserInContext(context, userRdr, c.verifier) if err != nil { return "", fmt.Errorf("failed to read %q's user context file: %w", c.user, err) } @@ -1400,7 +1414,13 @@ func getDefaultContextFromReaders(c *defaultSECtx) (string, error) { return conn, nil } - conn, err = findUserInContext(context, c.defaultRdr, c.verifier) + defaultRdr, err := c.openDefaultRdr() + if err != nil { + return "", fmt.Errorf("failed to open default context file: %w", err) + } + defer defaultRdr.Close() + + conn, err = findUserInContext(context, defaultRdr, c.verifier) if err != nil { return "", fmt.Errorf("failed to read default user context file: %w", err) } @@ -1409,7 +1429,13 @@ func getDefaultContextFromReaders(c *defaultSECtx) (string, error) { return conn, nil } - conn, err = getFailsafeContext(context, c.failsafeRdr, c.verifier) + failsafeRdr, err := c.openFailsafeRdr() + if err != nil { + return "", fmt.Errorf("failed to open failsafe context file: %w", err) + } + defer failsafeRdr.Close() + + conn, err = getFailsafeContext(context, failsafeRdr, c.verifier) if err != nil { return "", fmt.Errorf("failed to read failsafe_context: %w", err) } @@ -1423,34 +1449,17 @@ func getDefaultContextFromReaders(c *defaultSECtx) (string, error) { func getDefaultContextWithLevel(user, level, scon string) (string, error) { userPath := filepath.Join(policyRoot(), selinuxUsersDir, user) - fu, err := os.Open(userPath) - if err != nil { - return "", fmt.Errorf("failed to open %q's user context file: %w", user, err) - } - defer fu.Close() - defaultPath := filepath.Join(policyRoot(), defaultContexts) - fd, err := os.Open(defaultPath) - if err != nil { - return "", fmt.Errorf("failed to open default user context file: %w", err) - } - defer fd.Close() - failsafePath := filepath.Join(policyRoot(), failsafeContext) - fs, err := os.Open(failsafePath) - if err != nil { - return "", fmt.Errorf("failed to open failsafe user context file: %w", err) - } - defer fs.Close() c := defaultSECtx{ - user: user, - level: level, - scon: scon, - userRdr: fu, - defaultRdr: fd, - failsafeRdr: fs, - verifier: securityCheckContext, + user: user, + level: level, + scon: scon, + openUserRdr: createOpener(userPath), + openDefaultRdr: createOpener(defaultPath), + openFailsafeRdr: createOpener(failsafePath), + verifier: securityCheckContext, } return getDefaultContextFromReaders(&c) diff --git a/go-selinux/selinux_linux_test.go b/go-selinux/selinux_linux_test.go index 97e35c8..74552b9 100644 --- a/go-selinux/selinux_linux_test.go +++ b/go-selinux/selinux_linux_test.go @@ -5,6 +5,7 @@ import ( "bytes" "errors" "fmt" + "io" "os" "os/user" "path/filepath" @@ -782,12 +783,16 @@ fake_r:fake_t:s0 baz_r:baz_t:s0 sysadm_r:sysadm_t:s0 for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := defaultSECtx{ - user: "bob", - level: "SystemLow-SystemHigh", - scon: "system_u:staff_r:staff_t:s0", - userRdr: bytes.NewBufferString(tt.userBuff), - defaultRdr: bytes.NewBufferString(tt.defaultBuff), - verifier: verifier, + user: "bob", + level: "SystemLow-SystemHigh", + scon: "system_u:staff_r:staff_t:s0", + openUserRdr: func() (io.ReadCloser, error) { + return io.NopCloser(bytes.NewBufferString(tt.userBuff)), nil + }, + openDefaultRdr: func() (io.ReadCloser, error) { + return io.NopCloser(bytes.NewBufferString(tt.defaultBuff)), nil + }, + verifier: verifier, } got, err := getDefaultContextFromReaders(&c) @@ -809,12 +814,18 @@ fake_r:fake_t:s0 baz_r:baz_t:s0 sysadm_r:sysadm_t:s0 dne_r:dne_t:s0 baz_r:baz_t:s0 sysadm_r:sysadm_t:s0 ` c := defaultSECtx{ - user: "bob", - level: "SystemLow-SystemHigh", - scon: "system_u:staff_r:staff_t:s0", - userRdr: bytes.NewBufferString(badUserBuff), - defaultRdr: bytes.NewBufferString(badDefaultBuff), - failsafeRdr: bytes.NewBufferString(goodFailsafeBuff), + user: "bob", + level: "SystemLow-SystemHigh", + scon: "system_u:staff_r:staff_t:s0", + openUserRdr: func() (io.ReadCloser, error) { + return io.NopCloser(bytes.NewBufferString(badUserBuff)), nil + }, + openDefaultRdr: func() (io.ReadCloser, error) { + return io.NopCloser(bytes.NewBufferString(badDefaultBuff)), nil + }, + openFailsafeRdr: func() (io.ReadCloser, error) { + return io.NopCloser(bytes.NewBufferString(goodFailsafeBuff)), nil + }, verifier: func(s string) error { return nil }, From d867b12d6c143b993211aa80bedbb77d7a5a497b Mon Sep 17 00:00:00 2001 From: Andrew LeFevre Date: Fri, 10 Oct 2025 11:29:29 -0400 Subject: [PATCH 5/5] use `bufio.NewReader` instead of `bufio.NewScanner` --- go-selinux/selinux_linux.go | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/go-selinux/selinux_linux.go b/go-selinux/selinux_linux.go index 395b861..dcf0f4b 100644 --- a/go-selinux/selinux_linux.go +++ b/go-selinux/selinux_linux.go @@ -1212,13 +1212,18 @@ func getSeUserFromReader(username string, gids []string, r io.Reader, lookupGrou var groupSeUser, groupLevel string lineNum := -1 - scanner := bufio.NewScanner(r) - for scanner.Scan() { - line := scanner.Text() + reader := bufio.NewReader(r) + for { + lineBytes, readErr := reader.ReadBytes('\n') + if readErr != nil { + if !errors.Is(readErr, io.EOF) { + return "", "", fmt.Errorf("failed to read seusers file: %w", readErr) + } + } lineNum++ // remove any trailing comments, then extra whitespace - line, _, _ = strings.Cut(line, "#") + line, _, _ := strings.Cut(string(lineBytes), "#") line = strings.TrimSpace(line) if line == "" { continue @@ -1258,9 +1263,10 @@ func getSeUserFromReader(username string, gids []string, r io.Reader, lookupGrou defaultSeUser = seUserField defaultLevel = levelField } - } - if err := scanner.Err(); err != nil { - return "", "", fmt.Errorf("failed to read seusers file: %w", err) + + if errors.Is(readErr, io.EOF) { + break + } } if groupSeUser != "" {