Repository navigation
Conversation
1b8a8f3 to
36088f5
Compare
706f6e5 to
15f6695
Compare
752b6ed to
600b945
Compare
kolyshkin
left a comment
There was a problem hiding this comment.
The second patch seems problematic: while it says "skip malformed and empty lines", in fact it returns an error where previously it was ignored.
I think we should merely skip lines that do not conform to the specification, otherwise there will be regressions.
Yes, I need to clean-up my patch set; temporarily pushed, because I think the second (add error return) patch needed updates to the tests.
This was the topic of discussion (so was reordering the patches to make the "skip" last); Old code wasn't good, because it would silently discard invalid UID/GID (numeric) values; it would use The trickier question is what to do if the file contains malformed line(s)? Currently (see above) we "half" ignore them (bad). Which, even with this patch, may still be true; We silently ignore truncated lines; // Ignore cases where we don't have enough fields to populate the arguments.
// Some configuration files like to misbehave.
if len(v) <= i {
break
}So a line like And we still consider empty fields; case *int64:
if len(p) != 0 {
n, err := strconv.ParseInt(string(p), 10, 64)
if err != nil {
return true, fmt.Errorf("parsing integer field %d: %w", i, err)
}
*e = n
}So The slightly less problematic case is ignoring completely malformed lines, and it's less problematic if such a record was not the one we were looking for (i.e., we wouldn't use it anyway), but of course, if it's malformed, it COULD have been the line we were looking for, and I think we pick "first match", so the malformed (or incoplete) line would shadow the valid one; If we skip such lines or error out, that would prevent that (but probably needs more than this patch). |
|
Looks like getent skips over malformed lines; if (ret == EINVAL)
/* Skip over malformed lines. */
continue;Older implementations; } while (*p == '\0' || *p == '#' /* Ignore empty and comment lines. */
/* Parse the line. If it is invalid, loop to
get the next line of the file to parse. */
|| ! parse_line (p, resbuf, (void *) buffer, buflen, &errno));But it looks like it does reject lines with empty numeric columns; INT_FIELD (result->pw_uid, ISCOLON, 0, 10,)
INT_FIELD (result->pw_gid, ISCOLON, 0, 10,)Except for STRING_FIELD (result->pw_name, ISCOLON, 0);
if (line[0] == '\0'
&& (result->pw_name[0] == '+' || result->pw_name[0] == '-'))
{
/* This a special case. We allow lines containing only a `+' sign
since this is used for nss_compat. All other services will
reject this entry later. Initialize all other fields now. */
result->pw_passwd = NULL;
result->pw_uid = 0;
result->pw_gid = 0;
result->pw_gecos = NULL;
result->pw_dir = NULL;
result->pw_shell = NULL;
} |
|
Quick AI summary;
So the overall behavior is:
That's a fairly common pattern in libc: the iteration APIs ( I think that's a strong precedent for |
|
And for the
|
dabc9bb to
680b45f
Compare
| case *int: | ||
| // "numbers", with conversion errors ignored because of some misbehaving configuration files. | ||
| *e, _ = strconv.Atoi(string(p)) | ||
| n, ok, err := parseNumeric(string(p)) |
There was a problem hiding this comment.
note to self; this should not use parseNumeric, because it's too permissive. Perhaps we should rename it to parseMaybeNumeric to be more clear on intent (it's used for usernames that may be numeric);
Lines 245 to 253 in 4a8455d
9fbaa29 to
ff1220b
Compare
ff1220b to
c0fdf59
Compare
c0fdf59 to
1c9c861
Compare
| case *int: | ||
| // "numbers", with conversion errors ignored because of some misbehaving configuration files. | ||
| *e, _ = strconv.Atoi(string(p)) | ||
| n, err := parseNumeric(string(p)) |
Split parseNumeric into a strict numeric parser and a parseMaybeNumeric wrapper that allows non-numeric values to be interpreted as user or group names. This allows passwd and group file parsing to reject invalid numeric IDs without changing the behavior of callers that accept names. Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
Skip empty and malformed records when parsing passwd and group files, instead of accepting incomplete records or aborting on invalid numeric fields. Update parseLine and parseParts to distinguish malformed records from other parsing errors, allowing callers to skip invalid entries while still propagating unexpected errors. Treat missing fields as empty, allowing empty string fields while rejecting missing or invalid numeric fields. This aligns with glibc, which skips malformed records when reading passwd and group files. Add tests for empty, incomplete, and malformed records, and document the behavior based on the corresponding glibc implementations. For reference: - https://sources.debian.org/src/glibc/2.42-13/nss/nss_fgetent_r.c/ - https://sources.debian.org/src/glibc/2.42-13/nss/fgetpwent_r.c/ Co-authored-by: Mickael Emirkanian <mickael.emirkanian@docker.com> Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
Treat out-of-range numeric fields as malformed records, consistent with missing and non-numeric fields. Use the ok return value from parseLine and parseParts to distinguish malformed records, which should be skipped, from other parsing errors, which should be propagated to the caller. Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
1c9c861 to
c876c95
Compare
There was a problem hiding this comment.
A few issues outside of the diff hunks (so not inline):
-
GetExecUser(user.Uid = users[0].Uid, and the same forGid) copies the parsed IDs without passing them through the newvalidateID. The description says out-of-range numeric fields make a record malformed, butparsePartsonly rejectsstrconvoverflow, so negative IDs and IDs abovemaxIDstill get through. For example,adm:x:-1:-1::/:withGetExecUser("adm", ...)returnsUid: -1, Gid: -1and a nil error. A caller that doessetresuid(-1, ...)with that changes nothing, so the process keeps its current (often root) credentials. On 64-bit,adm:x:2147483648:4:is also accepted.GetAdditionalGroupsdoes callvalidateID, so the two paths are inconsistent. I suggest validating the range forUid/Gid(either in the parsers, treating such records as malformed, or inGetExecUser). -
GetAdditionalGroups([]string{""}, r)panics with a nil pointer dereference:parseGroupArg("")returnsnil, nil, and then*groupArgis dereferenced without a check. This is not new, but this PR touches the function, and now groups with empty names are accepted (:x:0:is in the expected test output), which makes empty group strings more likely to reach this code.
This review was written by an AI assistant (Claude) on my behalf.
| { | ||
| Name: "adm", | ||
| Pass: "x", | ||
| Uid: 2147483648, |
There was a problem hiding this comment.
2147483648 overflows int on 32-bit platforms, so the test package does not compile there: GOARCH=386 go vet ./user fails with cannot use 2147483648 (untyped int constant) as int value in struct literal (overflows). CI only runs tests on amd64, so it doesn't catch this.
Even if this compiled, on 32-bit Atoi("2147483648") returns ErrRange and the record would be skipped, so the expected result is platform-dependent. Maybe move these cases into a separate test guarded by strconv.IntSize == 64, or use a value that is out of range on all platforms.
| { | ||
| Name: "invalid", | ||
| Pass: "x", | ||
| Gid: 2147483648, |
There was a problem hiding this comment.
Same as above: 2147483648 overflows int on 32-bit.
| // adm:x:3:4:adm:/var/adm:/bin/false | ||
| p := User{} | ||
| parseLine(line, &p.Name, &p.Pass, &p.Uid, &p.Gid, &p.Gecos, &p.Home, &p.Shell) | ||
| ok, err := parseLine(line, &p.Name, &p.Pass, &p.Uid, &p.Gid, &p.Gecos, &p.Home, &p.Shell) |
There was a problem hiding this comment.
ParsePasswdFilter still does not skip # comment lines, unlike ParseGroupFilter and unlike glibc, which this PR says it follows. A commented-out entry whose numeric fields are valid is returned as a real user: #olduser:x:0:0::/root:/bin/sh gives a user named #olduser with UID 0, which the default GetExecUser filter (u.Uid == user.Uid) can match. The new skipping only drops comment lines that happen to fail numeric parsing.
| p := User{} | ||
| parseLine(line, &p.Name, &p.Pass, &p.Uid, &p.Gid, &p.Gecos, &p.Home, &p.Shell) | ||
| ok, err := parseLine(line, &p.Name, &p.Pass, &p.Uid, &p.Gid, &p.Gecos, &p.Home, &p.Shell) | ||
| if !ok { |
There was a problem hiding this comment.
This changes the behavior of the public parsers without saying so: lines that used to be returned with zeroed numeric fields are now silently dropped. That covers passwd/group lines with empty or non-numeric IDs, and subuid/idmap lines with missing counts (e.g. name:100000 used to give Count: 0). It's probably the right thing to do, but it's worth calling out in the PR description / commit message, since callers that counted or validated entries lose that signal.
| if errors.Is(err, strconv.ErrRange) { | ||
| return false, fmt.Errorf("parsing integer field %d: %w", i, err) | ||
| } | ||
| return true, fmt.Errorf("parsing integer field %d: %w", i, err) |
There was a problem hiding this comment.
These return true, fmt.Errorf(...) branches (here and in the *int64 case) can't be reached: strconv.Atoi and strconv.ParseInt(s, 10, 64) only return ErrSyntax or ErrRange. So the (bool, error) contract, and the four if err != nil { return nil, fmt.Errorf("invalid line %q: %w", ...) } blocks in the callers, are dead code, and they make readers think some lines can abort parsing. Simpler: have parseParts return just ok bool.
| return false, fmt.Errorf("parsing integer field %d: %q is not numeric: %w", i, p, strconv.ErrSyntax) | ||
| } | ||
| if errors.Is(err, strconv.ErrRange) { | ||
| return false, fmt.Errorf("parsing integer field %d: %w", i, err) |
There was a problem hiding this comment.
For every malformed line this builds a formatted error (fmt.Errorf with %q in the ErrSyntax case), and every caller throws it away because it checks !ok first. For subuid/subgid files, which have no # skip, every comment line goes through this path. Returning just false (or a sentinel error) avoids the allocation.
| return true, fmt.Errorf("parsing integer field %d: %w", i, err) | ||
| } | ||
| *e = n | ||
| case *int64: |
There was a problem hiding this comment.
The *int and *int64 cases repeat the same error classification and differ only in the parse call. Maybe parse once with strconv.ParseInt(string(p), 10, 64) (or bitSize 0 for *int), handle the error once, then assign to the target type.
| // | ||
| // See https://sources.debian.org/src/glibc/2.42-13/nss/nss_fgetent_r.c/ | ||
| func parseLine(line []byte, v ...any) (ok bool, _ error) { | ||
| line = bytes.TrimSpace(line) |
There was a problem hiding this comment.
All callers already do line := bytes.TrimSpace(s.Bytes()); if len(line) == 0 { continue }, so this trim and empty check never do anything, and neither does len(parts) == 0 in parseParts (bytes.Split never returns an empty slice). I suggest keeping this check in one place.


user: split numeric ID parsing from name detection
Split parseNumeric into a strict numeric parser and a parseMaybeNumeric
wrapper that allows non-numeric values to be interpreted as user or group
names.
This allows passwd and group file parsing to reject invalid numeric IDs
without changing the behavior of callers that accept names.
user: skip empty and malformed records
Skip empty and malformed records when parsing passwd and group files,
instead of accepting incomplete records or aborting on invalid numeric
fields.
Update parseLine and parseParts to distinguish malformed records from
other parsing errors, allowing callers to skip invalid entries while
still propagating unexpected errors.
Treat missing fields as empty, allowing empty string fields while
rejecting missing or invalid numeric fields. This aligns with glibc,
which skips malformed records when reading passwd and group files.
Add tests for empty, incomplete, and malformed records, and document
the behavior based on the corresponding glibc implementations.
For reference:
user: distinguish malformed records from parsing errors
Treat out-of-range numeric fields as malformed records, consistent
with missing and non-numeric fields.
Use the ok return value from parseLine and parseParts to distinguish
malformed records, which should be skipped, from other parsing errors,
which should be propagated to the caller.