Skip to content

Commit 85a71bb

Browse files
authored
Merge commit from fork
Merge fixes
2 parents ee79b0e + 2c56c3d commit 85a71bb

3 files changed

Lines changed: 172 additions & 44 deletions

File tree

user/user.go

Lines changed: 19 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ func ParseGroup(group io.Reader) ([]Group, error) {
157157
}
158158

159159
func ParseGroupFileFilter(path string, filter func(Group) bool) ([]Group, error) {
160-
group, err := os.Open(path)
160+
group, err := openUserFile(path)
161161
if err != nil {
162162
return nil, err
163163
}
@@ -169,52 +169,22 @@ func ParseGroupFilter(r io.Reader, filter func(Group) bool) ([]Group, error) {
169169
if r == nil {
170170
return nil, errors.New("nil source for group-formatted data")
171171
}
172-
rd := bufio.NewReader(r)
173-
out := []Group{}
174-
175-
// Read the file line-by-line.
176-
for {
177-
var (
178-
isPrefix bool
179-
wholeLine []byte
180-
err error
181-
)
182-
183-
// Read the next line. We do so in chunks (as much as reader's
184-
// buffer is able to keep), check if we read enough columns
185-
// already on each step and store final result in wholeLine.
186-
for {
187-
var line []byte
188-
line, isPrefix, err = rd.ReadLine()
189-
if err != nil {
190-
// We should return no error if EOF is reached
191-
// without a match.
192-
if err == io.EOF {
193-
err = nil
194-
}
195-
return out, err
196-
}
197172

198-
// Simple common case: line is short enough to fit in a
199-
// single reader's buffer.
200-
if !isPrefix && len(wholeLine) == 0 {
201-
wholeLine = line
202-
break
203-
}
173+
var (
174+
s = bufio.NewScanner(r)
175+
out = []Group{}
176+
)
204177

205-
wholeLine = append(wholeLine, line...)
206-
207-
// Check if we read the whole line already.
208-
if !isPrefix {
209-
break
210-
}
211-
}
178+
// A group's user_list may be arbitrarily long, so allow lines that are
179+
// much larger than bufio.Scanner's default maximum token size (64 KiB).
180+
s.Buffer(nil, 1024*1024)
212181

182+
for s.Scan() {
213183
// There's no spec for /etc/passwd or /etc/group, but we try to follow
214184
// the same rules as the glibc parser, which allows comments and blank
215185
// space at the beginning of a line.
216-
wholeLine = bytes.TrimSpace(wholeLine)
217-
if len(wholeLine) == 0 || wholeLine[0] == '#' {
186+
line := bytes.TrimSpace(s.Bytes())
187+
if len(line) == 0 || line[0] == '#' {
218188
continue
219189
}
220190

@@ -224,12 +194,17 @@ func ParseGroupFilter(r io.Reader, filter func(Group) bool) ([]Group, error) {
224194
// root:x:0:root
225195
// adm:x:4:root,adm,daemon
226196
p := Group{}
227-
parseLine(wholeLine, &p.Name, &p.Pass, &p.Gid, &p.List)
197+
parseLine(line, &p.Name, &p.Pass, &p.Gid, &p.List)
228198

229199
if filter == nil || filter(p) {
230200
out = append(out, p)
231201
}
232202
}
203+
if err := s.Err(); err != nil {
204+
return nil, err
205+
}
206+
207+
return out, nil
233208
}
234209

235210
type ExecUser struct {
@@ -246,12 +221,12 @@ type ExecUser struct {
246221
func GetExecUserPath(userSpec string, defaults *ExecUser, passwdPath, groupPath string) (*ExecUser, error) {
247222
var passwd, group io.Reader
248223

249-
if passwdFile, err := os.Open(passwdPath); err == nil {
224+
if passwdFile, err := openUserFile(passwdPath); err == nil {
250225
passwd = passwdFile
251226
defer passwdFile.Close()
252227
}
253228

254-
if groupFile, err := os.Open(groupPath); err == nil {
229+
if groupFile, err := openUserFile(groupPath); err == nil {
255230
group = groupFile
256231
defer groupFile.Close()
257232
}

user/user_test.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
package user
22

33
import (
4+
"bytes"
45
"fmt"
56
"io"
7+
"os"
8+
"path/filepath"
69
"reflect"
10+
"slices"
711
"sort"
812
"strconv"
913
"strings"
@@ -98,6 +102,91 @@ this is just some garbage data
98102
}
99103
}
100104

105+
// TestParseGroupFileCapsReads asserts the boundary behavior of the read cap:
106+
// well below, ending exactly at, and past maxUserFileBytes.
107+
func TestParseGroupFileCapsReads(t *testing.T) {
108+
beyond := []byte("\nbeyond:x:42:\n")
109+
110+
for _, tc := range []struct {
111+
name string
112+
padBytes int
113+
expGIDs []int
114+
expErr string
115+
}{
116+
{
117+
name: "pad below cap, beyond is parsed",
118+
padBytes: 100,
119+
expGIDs: []int{42},
120+
},
121+
{
122+
name: "beyond ends exactly at cap, is parsed",
123+
padBytes: maxUserFileBytes - len(beyond),
124+
expGIDs: []int{42},
125+
},
126+
{
127+
name: "pad past cap, read errors out",
128+
padBytes: maxUserFileBytes,
129+
expErr: "file exceeds",
130+
},
131+
} {
132+
t.Run(tc.name, func(t *testing.T) {
133+
tmpDir := t.TempDir()
134+
fileName := filepath.Join(tmpDir, "etc-group")
135+
136+
data := append(bytes.Repeat([]byte{0}, tc.padBytes), beyond...)
137+
err := os.WriteFile(fileName, data, 0o644)
138+
if err != nil {
139+
t.Fatal(err)
140+
}
141+
gids, err := ParseGroupFile(fileName)
142+
if tc.expErr != "" {
143+
if err == nil {
144+
t.Fatal("expected error")
145+
}
146+
if !strings.Contains(err.Error(), tc.expErr) {
147+
t.Fatalf("unexpected error: %s", err)
148+
}
149+
return
150+
}
151+
if err != nil {
152+
t.Fatal(err)
153+
}
154+
haveGids := make([]int, 0, len(gids))
155+
for _, g := range gids {
156+
haveGids = append(haveGids, g.Gid)
157+
}
158+
if !slices.Equal(haveGids, tc.expGIDs) {
159+
t.Fatalf("unexpected gids: got %v, want %v", gids, tc.expGIDs)
160+
}
161+
})
162+
}
163+
}
164+
165+
// TestTestParseGroupFileCapsReadsonRegularFile verifies that non-regular files
166+
// are refused.
167+
func TestTestParseGroupFileCapsReadsonRegularFile(t *testing.T) {
168+
fileName := t.TempDir()
169+
_, err := ParseGroupFile(fileName)
170+
if err == nil {
171+
t.Fatal("expected error")
172+
}
173+
if !strings.Contains(err.Error(), "not a regular file") {
174+
t.Fatalf("unexpected error: %s", err)
175+
}
176+
}
177+
178+
func TestParseGroupFilterDevZero(t *testing.T) {
179+
dn, err := os.Open("/dev/zero")
180+
if err != nil {
181+
t.Fatal(err)
182+
}
183+
const expErr = "bufio.Scanner: token too long"
184+
_, err = ParseGroupFilter(dn, nil)
185+
if err == nil || !strings.Contains(err.Error(), expErr) {
186+
t.Fatalf("want %q, got %v", expErr, err)
187+
}
188+
}
189+
101190
func TestGetExecUser(t *testing.T) {
102191
const passwdContent = `
103192
root:x:0:0:root user:/root:/bin/bash

user/user_utils.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package user
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"io"
7+
"os"
8+
)
9+
10+
// maxUserFileBytes caps how much data is read from any user-database file.
11+
// User database files are expected to be relatively small. 10 MiB provides
12+
// generous headroom while bounding memory usage.
13+
const maxUserFileBytes = 10 << 20
14+
15+
// openUserFile attempts to open a user-database file with a limitedFile
16+
// capped at maxUserFileBytes. It produces an error if the given path is
17+
// a non-regular file.
18+
func openUserFile(path string) (*limitedFile, error) {
19+
f, err := os.Open(path)
20+
if err != nil {
21+
return nil, err
22+
}
23+
24+
info, err := f.Stat()
25+
if err != nil {
26+
_ = f.Close()
27+
return nil, err
28+
}
29+
if !info.Mode().IsRegular() {
30+
_ = f.Close()
31+
return nil, &os.PathError{
32+
Op: "open",
33+
Path: path,
34+
Err: errors.New("not a regular file"),
35+
}
36+
}
37+
38+
return &limitedFile{
39+
File: f,
40+
// Allow one byte past the cap so an overflow surfaces as an
41+
// error rather than a silent EOF that the parser would treat as
42+
// a clean end-of-file (and miss any entries past the cap).
43+
LimitedReader: &io.LimitedReader{R: f, N: maxUserFileBytes + 1},
44+
name: path,
45+
}, nil
46+
}
47+
48+
type limitedFile struct {
49+
*os.File
50+
*io.LimitedReader
51+
name string
52+
}
53+
54+
func (l *limitedFile) Read(p []byte) (int, error) {
55+
n, err := l.LimitedReader.Read(p)
56+
if l.LimitedReader.N == 0 {
57+
return n, &os.PathError{
58+
Op: "read",
59+
Path: l.name,
60+
Err: fmt.Errorf("file exceeds %d bytes", maxUserFileBytes),
61+
}
62+
}
63+
return n, err
64+
}

0 commit comments

Comments
 (0)