Hacktoberfest 2026:メンテナが10月に向けて印を付けた、オープンで初心者向けの issue。 Hacktoberfest の issue を見る

groups, pinky: unknown numeric user name looked up by uid, unlike GNU

クローズ 初心者向け
#15,813 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る

メンテナーはふだん 1 日以内に返信

まだ誰も着手していません。

評価

難易度
2/5
見積もり時間
1〜3時間
初心者へのやさしさ
83/100
issue の種類
バグ
明瞭さ
明確に書かれている
活発さ
活発
技術スタック
rust
領域
backend, cli

調査の方向性

src/uucore/src/lib/features/entries.rs の Passwd::locate と、src/uu/groups/src/groups.rs および src/uu/pinky/src/platform/unix.rs にある名前のみを使う呼び出し箇所から始め、それらを異なる検索動作が必要な id および chown と比較してください。issue では、名前のみを使う検索を追加し、これら3箇所の呼び出しで使用することが提案されています。添付のスクリプトで再現し、対応するアカウントがない数値名は groups で失敗し、pinky では uid の詳細が明らかにならない一方、id は引き続き数値 ID を解決することを確認してください。

索引モデルが issue の本文から書いたものです。

説明

good first issue I - Incorrect oss-scanner U - groups U - pinky
============================================================================
ANT-2026-X3DMHZDB  [NOT SECURITY: ordinary bug]
groups, pinky: unknown numeric user name looked up by uid, unlike GNU
Location: src/uucore/src/lib/features/entries.rs:293  in Locate<&str>::locate (macro f!)()
Commit:   7bc1eb6d1d6fa2e1dc2c9d2277cf576c05f61e47
============================================================================

Not a security issue. This is a real, reproducible bug that is NOT a security issue: by your project's own policy nobody gains anything from it that they did not already have. It is not part of the vulnerability report, is in none of its counts, needs no embargo and no advisory, and can be filed and fixed in the open like any bug.

groups NAME and pinky -l NAME give an answer for a name that no account has, as long as the name is a number: the operand is quietly looked up again as a uid. On a system where uid 1000 is opsadmin and no account is named 1000, coreutils groups 1000 prints 1000 : opsadmin operators and exits 0. GNU coreutils 9.7 prints groups: '1000': no such user and exits 1. 0, +0 and 00 all resolve to root the same way.

The cause is Passwd::locate(&str) in src/uucore/src/lib/features/entries.rs (the f! macro, around line 293). It tries getpwnam first and, if that fails and the string parses as a uid_t, falls back to getpwuid. u32::from_str accepts a leading + and leading zeros. uucore::entries has no name-only lookup, so callers holding a name get the fallback whether they want it or not:

  • groups calls it for each operand (src/uu/groups/src/groups.rs:64).
  • pinky calls it for long-format operands and for utmp user names (src/uu/pinky/src/platform/unix.rs:326 and :231). As a result, pinky -l 1000 prints uid 1000's real name, home, shell and plan files where GNU prints In real life: ???.

chown already guards against this by checking passwd.name == user after the lookup. id relies on the fallback and matches GNU there, so the fallback can't simply be removed. The group list printed is always correct for the uid it lands on, and an account whose name really is all digits is still found by name first. The difference only shows up for a numeric operand that is nobody's login name but is an assigned uid, for example a uid passed where a name was expected.

The string lookup dates from 799804e455f5 ("Add uucore::entries", 2016) and has been present since 0.0.1. An attached script compares GNU and uutils groups on such an operand, and a patch adds a name-only locate_name for these three call sites.

This is an ordinary bug, not a security issue: groups and pinky -l differ from GNU only for an
operand that is no account's name, the group list they print is the correct one for the uid they fall
back to, and a script that trusts such a caller-chosen name is already open, under GNU, to anyone who
gives a real member's name.

Details

groups NAME and pinky -l NAME answer for a name that no account has when the name is a number: the
operand is silently looked up again as a uid. On a system where uid 1000 is opsadmin, a member of
operators, and no account is called 1000:

$ /usr/bin/groups 1000                 # GNU coreutils 9.7
groups: '1000': no such user           (exit 1)
$ coreutils groups 1000
1000 : opsadmin operators              (exit 0)
$ coreutils groups +0
+0 : root                              (exit 0)

coreutils pinky -l 1000 likewise prints the real name, home directory, shell, ~/.project and ~/.plan
of uid 1000, where GNU prints In real life: ??? and nothing else.

Both utilities call Passwd::locate(&str). Its string form is not a lookup by name; it is "name, and
if that fails and the string is a number, ID":

// src/uucore/src/lib/features/entries.rs:289   (macro f!, instantiated for Passwd and Group at :312-313)
                    let data = $fnam(cstring.as_ptr());          // getpwnam / getgrnam
                    if !data.is_null() {
                        return Ok($st::from_raw(ptr::read(data.cast_const())));
                    }
                    if let Ok(id) = k.parse::<$t>() {            // uid_t / gid_t
                        let data = $fid(id);                     // getpwuid / getgrgid
                        if !data.is_null() {
                            Ok($st::from_raw(ptr::read(data.cast_const())))

u32::from_str accepts a leading + and leading zeros, so 0, +0, 00 and 0033 all reach
getpwuid. The returned Passwd does not record which of the two lookups produced it, and
uucore::entries has no name-only entry point (usr2uid, usr2gid and grp2gid at :326-338 are thin
wrappers over the same function), so a caller that holds a name gets the fallback whether it wants it
or not.

groups passes each operand straight in and prints the operand, not the name of the account it found,
in front of the group list; the groups themselves are those of the account getpwuid returned:

// src/uu/groups/src/groups.rs:63
    for user in users {
        match Passwd::locate(user.as_str()) {
            Ok(p) => {
                let groups: Vec<String> =
                    p.belongs_to().into_iter().map(infallible_gid2grp).collect();
                writeln!(stdout(), "{user} : {}", groups.join(" "))?;

With groups 1000, getpwnam("1000") returns NULL, the parse succeeds and getpwuid(1000) returns
opsadmin: the line is 1000 : opsadmin operators, exit status 0. With groups nosuchuser the parse
fails and the Err arm reports no such user, exit 1, as GNU does. An existing account whose name
really is all digits is still found by name, because getpwnam is tried first.

pinky makes the same call in two places: src/uu/pinky/src/platform/unix.rs:326 for each operand of
the long format, and :231 for the user field of each utmp record in the short format.

chown met the same problem for the OWNER: form and guards against it locally by comparing the name
afterwards:

// src/uu/chown/src/chown.rs:225
        return match Passwd::locate(user) {
            // Lookup must occur through name, not ID, to be valid here.
            Ok(passwd) if passwd.name == user => Ok((Some(passwd.uid), Some(passwd.gid))),

groups and pinky have no such comparison. The fallback itself cannot simply be removed: id relies
on it (src/uu/id/src/id.rs:227), and there it matches GNU, whose id 33, id +33 and id 0033 all
print the entry of uid 33.

The name-or-ID string lookup was introduced with Locate<&str> in 799804e455f5 ("Add uucore::entries",
2016-08-14) and is present in every release since the first, 0.0.1.

Impact

Anyone who runs groups N or pinky -l N with an all-digit operand (also +N or with leading zeros)
that is not an account name but is an assigned uid gets an answer for the account with that uid, exit
status 0, where GNU reports no such user (exit 1) or In real life: ???. groups documents its
operand as USERNAME, so a shell script that uses groups -- "$name" to ask whether an account named
$name exists, or whether it is in a group, gets "yes" for such a string where GNU says no. Users meet
this rarely: the operand has to be entirely digits, be nobody's login name, and match an assigned uid
(0, +0 and 00 all match root); typically it happens when a uid is passed by mistake where a name
was expected. An account whose name really is all digits is still found by name first.

Nothing is misreported for a real account: the group list printed is correct for that uid, and
pinky -l N shows only what the passwd database and the home directory already expose. Neighbouring
tools behave the same way on GNU systems: GNU id 1000 and id -nG 1000, and glibc
getent passwd 1000 (and getent passwd +33), also resolve a number as a uid, so only groups and
pinky diverge from GNU here. The login hook in the reproducer is written only to show how a script's
decision changes; no existing script that relies on this pattern is known.

Reproducing

repro is a Python script (standard library only). It runs as root against the prebuilt
/src/target/debug/coreutils and the image's GNU coreutils 9.7 in /usr/bin. It comes with one file:

  • repro.d/operators_only.sh is the administrator's part: a login hook that is handed the name a client
    typed and prints GRANTED if groups -- "$1" lists operators, DENIED otherwise. It calls plain
    groups, so the same hook runs with GNU or with uutils depending only on PATH.

repro holds the setup, the input and the verdict:

  1. Set up. It puts a groups symlink to the multicall binary in a temporary directory (the usual
    multicall install), so that prepending that directory to PATH selects uutils. It creates group
    operators and account opsadmin in it with groupadd and useradd (default options), and removes
    both, and the temporary directory, before printing.
  2. Input. The input is one string: opsadmin's uid in decimal (1000 in this image).
    The script counts the accounts in the passwd database that have that string as their name (0).
  3. Observe. It runs groups -- 1000 with GNU and with uutils, and the login hook with 1000 under
    each PATH.

Expected output:

CONTROL  accounts named '1000': 0
CONTROL  GNU    groups 1000 -> exit 1, "groups: '1000': no such user\n"
OBSERVED uutils groups 1000 -> exit 0, '1000 : opsadmin operators\n'
CONTROL  login hook with GNU groups    -> DENIED
OBSERVED login hook with uutils groups -> GRANTED
BUG

BUG is printed only when no account is named 1000, GNU groups exits 1 and the hook denies with it,
while uutils groups exits 0 with the groups of uid 1000 (1000 : opsadmin operators) and the same hook
grants.

pinky is not part of the script; the same fallback shows there with
coreutils pinky -l 1000 (home directory and shell of uid 1000) against GNU pinky -l 1000
(In real life: ???).

Suggested fix

The attached patch gives uucore::entries a lookup that is by name only and uses it where the operand
is a name. In the f! macro it splits the existing string lookup in two:

// src/uucore/src/lib/features/entries.rs, replacing the Locate<&str> impl at :280-308
        impl $st {
            /// Fetch the entry whose name is `k`.
            ///
            /// Unlike `locate`, this never retries a numeric `k` as an ID.
            pub fn locate_name(k: &str) -> IOResult<Self> {
                let _guard = PW_LOCK.lock();
                // SAFETY: We're holding PW_LOCK.
                unsafe {
                    let cstring = CString::new(k)?;
                    let data = $fnam(cstring.as_ptr());
                    if !data.is_null() {
                        Ok($st::from_raw(ptr::read(data.cast_const())))
                    } else {
                        Err(IOError::new(ErrorKind::NotFound, format!("Not found: {k}")))
                    }
                }
            }
        }

        impl<'a> Locate<&'a str> for $st {
            fn locate(k: &'a str) -> IOResult<Self> {
                Self::locate_name(k).or_else(|e| match k.parse::<$t>() {
                    Ok(id) => Self::locate(id),
                    Err(_) => Err(e),
                })
            }
        }

and switches the three name-only call sites (dropping the then unused Locate import in both files):

--- a/src/uu/groups/src/groups.rs
+++ b/src/uu/groups/src/groups.rs
@@ -64 @@
-        match Passwd::locate(user.as_str()) {
+        match Passwd::locate_name(user.as_str()) {
--- a/src/uu/pinky/src/platform/unix.rs
+++ b/src/uu/pinky/src/platform/unix.rs
@@ -231 @@
-            let name = Passwd::locate(ut.user().as_ref())
+            let name = Passwd::locate_name(ut.user().as_ref())
@@ -326 @@
-            let Ok(pw) = Passwd::locate(name.as_str()) else {
+            let Ok(pw) = Passwd::locate_name(name.as_str()) else {

Locate<&str>::locate keeps its name-then-ID behaviour and its error messages, so id, chown,
chgrp, install -o/-g and chroot --userspec/--groups are not affected. The patch also documents
both lookups in the module doctest of entries.rs and adds test_groups_operand_is_not_a_uid
(tests/by-util/test_groups.rs: 0, +0, 00 give groups: '<name>': no such user, exit 1) and
test_long_format_operand_is_not_a_uid (tests/by-util/test_pinky.rs: pinky -l 0 prints ???).
With the patch, groups 0, +0, 00 and 33 report
no such user with exit status 1, pinky -l 0 prints ???, id 33, id +33, chown 33:33,
chgrp 33, install -o 33 -g +33 and chroot --userspec=65534:65534 behave as before, and the
reproducer prints OK.

Running the reproducer

bugs-not-security/ANT-2026-X3DMHZDB/repro, next to this file, with its companion files in bugs-not-security/ANT-2026-X3DMHZDB/repro.d/ (operators_only.sh) — the script reads them from /tmp/repro.d/. It was written by an automated agent and our check ran it as root in a privileged container: run it INSIDE A THROWAWAY VM, with the image oss-scan-7bc1eb6d1d6f built by bash build/build-image.sh, from the archive's top directory:

docker run --rm --network=none --privileged -u 0 -w /src -v "$PWD/bugs-not-security/ANT-2026-X3DMHZDB/repro:/tmp/repro:ro" -v "$PWD/bugs-not-security/ANT-2026-X3DMHZDB/repro.d:/tmp/repro.d:ro" oss-scan-7bc1eb6d1d6f /tmp/repro

It prints its evidence; the last line is BUG if the issue triggered, OK otherwise.

主要言語
Rust
スター
24.3k
フォーク
2.1k
平均マージ
1日 21時間
マージ済み PR(30日)
357

環境構築

Codespaces で開く

このプロジェクトの開発コンテナを、あなたの GitHub アカウントでブラウザ上に起動します。

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

uutils/coreutils のほかの issue

uutils/coreutils の issue をすべて見る

似ている issue

Rust の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。