]> Cypherpunks repositories - gostls13.git/commitdiff
os/user: make user.LookupGroupId function work for large entries
authorAndrey Bokhanko <andreybokhanko@gmail.com>
Thu, 14 Jan 2021 15:30:14 +0000 (15:30 +0000)
committerIan Lance Taylor <iant@golang.org>
Wed, 17 Mar 2021 03:08:07 +0000 (03:08 +0000)
The existing implementation of user.LookupGroupId function works
incorrectly with very large (>64K symbols) entries in /etc/group file.
This patch fixes this.

Fixes #43636

Change-Id: I453321f1ab15fd4d0002f97fcec7d0789e1e0da5
Reviewed-on: https://go-review.googlesource.com/c/go/+/283601
Run-TryBot: Ian Lance Taylor <iant@golang.org>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Ian Lance Taylor <iant@golang.org>
Trust: Emmanuel Odeke <emmanuel@orijtech.com>

src/os/user/lookup_unix.go
src/os/user/lookup_unix_test.go

index ed8c2dee9ddfa76628c642a3f7379fd8e77a36a1..97c611fad42fdf264af9bbef9259d10b5c2662a0 100644 (file)
@@ -33,23 +33,72 @@ type lineFunc func(line []byte) (v interface{}, err error)
 // readColonFile parses r as an /etc/group or /etc/passwd style file, running
 // fn for each row. readColonFile returns a value, an error, or (nil, nil) if
 // the end of the file is reached without a match.
-func readColonFile(r io.Reader, fn lineFunc) (v interface{}, err error) {
-       bs := bufio.NewScanner(r)
-       for bs.Scan() {
-               line := bs.Bytes()
+//
+// readCols is the minimum number of colon-separated fields that will be passed
+// to fn; in a long line additional fields may be silently discarded.
+func readColonFile(r io.Reader, fn lineFunc, readCols int) (v interface{}, err error) {
+       rd := bufio.NewReader(r)
+
+       // Read the file line-by-line.
+       for {
+               var isPrefix bool
+               var wholeLine []byte
+
+               // Read the next line. We do so in chunks (as much as reader's
+               // buffer is able to keep), check if we read enough columns
+               // already on each step and store final result in wholeLine.
+               for {
+                       var line []byte
+                       line, isPrefix, err = rd.ReadLine()
+
+                       if err != nil {
+                               // We should return (nil, nil) if EOF is reached
+                               // without a match.
+                               if err == io.EOF {
+                                       err = nil
+                               }
+                               return nil, err
+                       }
+
+                       // Simple common case: line is short enough to fit in a
+                       // single reader's buffer.
+                       if !isPrefix && len(wholeLine) == 0 {
+                               wholeLine = line
+                               break
+                       }
+
+                       wholeLine = append(wholeLine, line...)
+
+                       // Check if we read the whole line (or enough columns)
+                       // already.
+                       if !isPrefix || bytes.Count(wholeLine, []byte{':'}) >= readCols {
+                               break
+                       }
+               }
+
                // There's no spec for /etc/passwd or /etc/group, but we try to follow
                // the same rules as the glibc parser, which allows comments and blank
                // space at the beginning of a line.
-               line = bytes.TrimSpace(line)
-               if len(line) == 0 || line[0] == '#' {
+               wholeLine = bytes.TrimSpace(wholeLine)
+               if len(wholeLine) == 0 || wholeLine[0] == '#' {
                        continue
                }
-               v, err = fn(line)
+               v, err = fn(wholeLine)
                if v != nil || err != nil {
                        return
                }
+
+               // If necessary, skip the rest of the line
+               for ; isPrefix; _, isPrefix, err = rd.ReadLine() {
+                       if err != nil {
+                               // We should return (nil, nil) if EOF is reached without a match.
+                               if err == io.EOF {
+                                       err = nil
+                               }
+                               return nil, err
+                       }
+               }
        }
-       return nil, bs.Err()
 }
 
 func matchGroupIndexValue(value string, idx int) lineFunc {
@@ -80,7 +129,7 @@ func matchGroupIndexValue(value string, idx int) lineFunc {
 }
 
 func findGroupId(id string, r io.Reader) (*Group, error) {
-       if v, err := readColonFile(r, matchGroupIndexValue(id, 2)); err != nil {
+       if v, err := readColonFile(r, matchGroupIndexValue(id, 2), 3); err != nil {
                return nil, err
        } else if v != nil {
                return v.(*Group), nil
@@ -89,7 +138,7 @@ func findGroupId(id string, r io.Reader) (*Group, error) {
 }
 
 func findGroupName(name string, r io.Reader) (*Group, error) {
-       if v, err := readColonFile(r, matchGroupIndexValue(name, 0)); err != nil {
+       if v, err := readColonFile(r, matchGroupIndexValue(name, 0), 3); err != nil {
                return nil, err
        } else if v != nil {
                return v.(*Group), nil
@@ -144,7 +193,7 @@ func findUserId(uid string, r io.Reader) (*User, error) {
        if e != nil {
                return nil, errors.New("user: invalid userid " + uid)
        }
-       if v, err := readColonFile(r, matchUserIndexValue(uid, 2)); err != nil {
+       if v, err := readColonFile(r, matchUserIndexValue(uid, 2), 6); err != nil {
                return nil, err
        } else if v != nil {
                return v.(*User), nil
@@ -153,7 +202,7 @@ func findUserId(uid string, r io.Reader) (*User, error) {
 }
 
 func findUsername(name string, r io.Reader) (*User, error) {
-       if v, err := readColonFile(r, matchUserIndexValue(name, 0)); err != nil {
+       if v, err := readColonFile(r, matchUserIndexValue(name, 0), 6); err != nil {
                return nil, err
        } else if v != nil {
                return v.(*User), nil
index c697802171f7e44cb18ab74a141aeb1214ba6381..060cfe186f5ced2d288611965300d1163bda5571 100644 (file)
@@ -9,12 +9,13 @@
 package user
 
 import (
+       "fmt"
        "reflect"
        "strings"
        "testing"
 )
 
-const testGroupFile = `# See the opendirectoryd(8) man page for additional 
+var testGroupFile = `# See the opendirectoryd(8) man page for additional 
 # information about Open Directory.
 ##
 nobody:*:-2:
@@ -30,7 +31,7 @@ daemon:*:1:root
 # comment:*:4:found
      # comment:*:4:found
 kmem:*:2:root
-`
+` + largeGroup()
 
 var groupTests = []struct {
        in   string
@@ -49,9 +50,20 @@ var groupTests = []struct {
        {testGroupFile, "invalidgid", ""},
        {testGroupFile, "indented", "7"},
        {testGroupFile, "# comment", ""},
+       {testGroupFile, "largegroup", "1000"},
        {"", "emptyfile", ""},
 }
 
+// Generate a proper "largegroup" entry for testGroupFile string
+func largeGroup() (res string) {
+       var b strings.Builder
+       b.WriteString("largegroup:x:1000:user1")
+       for i := 2; i <= 7500; i++ {
+               fmt.Fprintf(&b, ",user%d", i)
+       }
+       return b.String()
+}
+
 func TestFindGroupName(t *testing.T) {
        for _, tt := range groupTests {
                got, err := findGroupName(tt.name, strings.NewReader(tt.in))