Skip to content

Commit 02c23c4

Browse files
libct/user: fix parsing long /etc/group lines
Lines in /etc/group longer than 64 characters breaks the current implementation of group parser. This is caused by bufio.Scanner buffer limit. Fix by re-using the fix for a similar problem in golang os/user, namely https://go-review.googlesource.com/c/go/+/283601. Add some tests. Co-authored-by: Andrey Bokhanko <[email protected]> Signed-off-by: Kir Kolyshkin <[email protected]>
1 parent 226dfab commit 02c23c4

File tree

2 files changed

+82
-21
lines changed

2 files changed

+82
-21
lines changed

libcontainer/user/user.go

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -180,15 +180,53 @@ func ParseGroupFilter(r io.Reader, filter func(Group) bool) ([]Group, error) {
180180
if r == nil {
181181
return nil, errors.New("nil source for group-formatted data")
182182
}
183+
rd := bufio.NewReader(r)
184+
out := []Group{}
185+
186+
// Read the file line-by-line.
187+
for {
188+
var (
189+
isPrefix bool
190+
wholeLine []byte
191+
err error
192+
)
193+
194+
// Read the next line. We do so in chunks (as much as reader's
195+
// buffer is able to keep), check if we read enough columns
196+
// already on each step and store final result in wholeLine.
197+
for {
198+
var line []byte
199+
line, isPrefix, err = rd.ReadLine()
183200

184-
var (
185-
s = bufio.NewScanner(r)
186-
out = []Group{}
187-
)
201+
if err != nil {
202+
// We should return no error if EOF is reached
203+
// without a match.
204+
if err == io.EOF {
205+
err = nil
206+
}
207+
return out, err
208+
}
188209

189-
for s.Scan() {
190-
text := bytes.TrimSpace(s.Bytes())
191-
if len(text) == 0 {
210+
// Simple common case: line is short enough to fit in a
211+
// single reader's buffer.
212+
if !isPrefix && len(wholeLine) == 0 {
213+
wholeLine = line
214+
break
215+
}
216+
217+
wholeLine = append(wholeLine, line...)
218+
219+
// Check if we read the whole line already.
220+
if !isPrefix {
221+
break
222+
}
223+
}
224+
225+
// There's no spec for /etc/passwd or /etc/group, but we try to follow
226+
// the same rules as the glibc parser, which allows comments and blank
227+
// space at the beginning of a line.
228+
wholeLine = bytes.TrimSpace(wholeLine)
229+
if len(wholeLine) == 0 || wholeLine[0] == '#' {
192230
continue
193231
}
194232

@@ -198,17 +236,12 @@ func ParseGroupFilter(r io.Reader, filter func(Group) bool) ([]Group, error) {
198236
// root:x:0:root
199237
// adm:x:4:root,adm,daemon
200238
p := Group{}
201-
parseLine(text, &p.Name, &p.Pass, &p.Gid, &p.List)
239+
parseLine(wholeLine, &p.Name, &p.Pass, &p.Gid, &p.List)
202240

203241
if filter == nil || filter(p) {
204242
out = append(out, p)
205243
}
206244
}
207-
if err := s.Err(); err != nil {
208-
return nil, err
209-
}
210-
211-
return out, nil
212245
}
213246

214247
type ExecUser struct {

libcontainer/user/user_test.go

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package user
22

33
import (
4+
"fmt"
45
"io"
56
"reflect"
67
"sort"
@@ -82,12 +83,12 @@ func TestUserParseGroup(t *testing.T) {
8283
root:x:0:root
8384
adm:x:4:root,adm,daemon
8485
this is just some garbage data
85-
`), nil)
86+
`+largeGroup()), nil)
8687
if err != nil {
8788
t.Fatalf("Unexpected error: %v", err)
8889
}
89-
if len(groups) != 3 {
90-
t.Fatalf("Expected 3 groups, got %v", len(groups))
90+
if len(groups) != 4 {
91+
t.Fatalf("Expected 4 groups, got %v", len(groups))
9192
}
9293
if groups[0].Gid != 0 || groups[0].Name != "root" || len(groups[0].List) != 1 {
9394
t.Fatalf("Expected groups[0] to be 0 - root - 1 member, got %v - %v - %v", groups[0].Gid, groups[0].Name, len(groups[0].List))
@@ -103,16 +104,18 @@ root:x:0:0:root user:/root:/bin/bash
103104
adm:x:42:43:adm:/var/adm:/bin/false
104105
111:x:222:333::/var/garbage
105106
odd:x:111:112::/home/odd:::::
107+
user7456:x:7456:100:Vasya:/home/user7456
106108
this is just some garbage data
107109
`
108-
const groupContent = `
110+
groupContent := `
109111
root:x:0:root
110112
adm:x:43:
111-
grp:x:1234:root,adm
113+
grp:x:1234:root,adm,user7456
112114
444:x:555:111
113115
odd:x:444:
114116
this is just some garbage data
115-
`
117+
` + largeGroup()
118+
116119
defaultExecUser := ExecUser{
117120
Uid: 8888,
118121
Gid: 8888,
@@ -216,6 +219,16 @@ this is just some garbage data
216219
Home: "/home/odd",
217220
},
218221
},
222+
// Test for #3036.
223+
{
224+
ref: "7456",
225+
expected: ExecUser{
226+
Uid: 7456,
227+
Gid: 100,
228+
Sgids: []int{1234, 1000}, // 1000 is largegroup GID
229+
Home: "/home/user7456",
230+
},
231+
},
219232
}
220233

221234
for _, test := range tests {
@@ -388,13 +401,13 @@ func TestGetAdditionalGroups(t *testing.T) {
388401
hasError bool
389402
}
390403

391-
const groupContent = `
404+
groupContent := `
392405
root:x:0:root
393406
adm:x:43:
394407
grp:x:1234:root,adm
395408
adm:x:4343:root,adm-duplicate
396409
this is just some garbage data
397-
`
410+
` + largeGroup()
398411
tests := []foo{
399412
{
400413
// empty group
@@ -444,6 +457,11 @@ this is just some garbage data
444457
expected: nil,
445458
hasError: true,
446459
},
460+
{
461+
// group with very long list of users
462+
groups: []string{"largegroup"},
463+
expected: []int{1000},
464+
},
447465
}
448466

449467
for _, test := range tests {
@@ -500,3 +518,13 @@ func TestGetAdditionalGroupsNumeric(t *testing.T) {
500518
}
501519
}
502520
}
521+
522+
// Generate a proper "largegroup" entry for group tests.
523+
func largeGroup() (res string) {
524+
var b strings.Builder
525+
b.WriteString("largegroup:x:1000:user1")
526+
for i := 2; i <= 7500; i++ {
527+
fmt.Fprintf(&b, ",user%d", i)
528+
}
529+
return b.String()
530+
}

0 commit comments

Comments
 (0)