From patchwork Thu Apr 2 17:12:09 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Adhemerval Zanella X-Patchwork-Id: 132634 Return-Path: X-Original-To: patchwork@sourceware.org Delivered-To: patchwork@sourceware.org Received: from vm01.sourceware.org (localhost [127.0.0.1]) by sourceware.org (Postfix) with ESMTP id 1330B4BA23F2 for ; Thu, 2 Apr 2026 17:13:02 +0000 (GMT) DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 1330B4BA23F2 Authentication-Results: sourceware.org; dkim=pass (2048-bit key, unprotected) header.d=linaro.org header.i=@linaro.org header.a=rsa-sha256 header.s=google header.b=ilrhIDr5 X-Original-To: libc-alpha@sourceware.org Delivered-To: libc-alpha@sourceware.org Received: from mail-vk1-xa29.google.com (mail-vk1-xa29.google.com [IPv6:2607:f8b0:4864:20::a29]) by sourceware.org (Postfix) with ESMTPS id 551FF4BA543C for ; Thu, 2 Apr 2026 17:12:30 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.2 sourceware.org 551FF4BA543C Authentication-Results: sourceware.org; dmarc=pass (p=none dis=none) header.from=linaro.org Authentication-Results: sourceware.org; spf=pass smtp.mailfrom=linaro.org ARC-Filter: OpenARC Filter v1.0.0 sourceware.org 551FF4BA543C Authentication-Results: server2.sourceware.org; arc=none smtp.remote-ip=2607:f8b0:4864:20::a29 ARC-Seal: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1775149950; cv=none; b=ueNhLDF8cvfvBGk9a5XqOaFt4mCszxFK5MONQ6u4fSFTxoyKSygyIhe4XbM1reDvlLkrG49SbMvLZg/F/1VyNRiC/ta9jwj/ExMeKKJggXSpgzJErVoc9FyQyxBLs8b4h99lF72xzAhsQEWxtJrj6C8+3k4DpZtI7DBouiuzjyw= ARC-Message-Signature: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1775149950; c=relaxed/simple; bh=pTI8j+F/V+kT4hUluxFDJ0YXzG4grCAnmg1+RNo4Xxs=; h=DKIM-Signature:From:To:Subject:Date:Message-ID:MIME-Version; b=oB7eAg4zNcLrnzeV7Bolmf/Ty5IgYPUJ9Pa3id1nc2TX9hvJGJ2ZbvnF9BzAvRVh6st6UBdu0BHBluxeRgQaYsxO+Vxx72UFcDy5USNkoI4GKcuW+Ct3ruXTfCr/7o6eL0vI0QUQFEKiYDJRZIy+m0/LumJB4SAUkpu6BXAjrDc= ARC-Authentication-Results: i=1; server2.sourceware.org DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 551FF4BA543C Received: by mail-vk1-xa29.google.com with SMTP id 71dfb90a1353d-5674d8be45eso457091e0c.1 for ; Thu, 02 Apr 2026 10:12:30 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=linaro.org; s=google; t=1775149949; x=1775754749; darn=sourceware.org; h=content-transfer-encoding:mime-version:message-id:date:subject:cc :to:from:from:to:cc:subject:date:message-id:reply-to; bh=/lFbh63m5I4vYEEvNS+x1Tc5rN+Uniu30RWWeaPW9v8=; b=ilrhIDr5Gb0JmlRuZdzoITR8rIP2Nry3jeGIsOdCFR4EOfgBuIC7ZJp9E4QwyPBqVF NRktPD573AmMjMj4OXxM/uGeZkXDYPlTFKJnQ25FnlgdDxSZPqwmNh5YMjuTv6Nmyscc ykfV7fWcF+bJVhNkpsnkN0sKl1gNWlwSaVbinShGFVZCRDzVytN2StPsCvTV8P5CwUoe jlnZev6HnsZ6gOaPEZUUZW2FqwuGW60kpv4eMLuv+41Z2D0vZEx3oD7MUzXO6Ooo4hzA D2yBTbpRC4DptVlymKlX3ZIGmqh+McOm2EnSKC3c0VCJ+VStOvRqZt8zK4BwrY12zI1M OO7A== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1775149949; x=1775754749; h=content-transfer-encoding:mime-version:message-id:date:subject:cc :to:from:x-gm-gg:x-gm-message-state:from:to:cc:subject:date :message-id:reply-to; bh=/lFbh63m5I4vYEEvNS+x1Tc5rN+Uniu30RWWeaPW9v8=; b=iS5mDuDumcKmOKtrAVrzs/20t0qPXkmYt6tNcGJJC+taREz1fi68d1uDkuO5KPJSRQ NKFR5CJyv3o+5K6wDXPmaRNgoMLsqQd80tpOdxNvKlRMlEcO1Kq/c2ASS6Qwy5tGxLcZ 9RVwzK4nC+1B+HK+y2KaqtvtzVhBC0iIq3lX44pUCVxTtTGBePVxMdkMgEq4lGrClWUW WJjajGp1/RDnK9t5IlS9FQ2b202dZzRx6r1jZpva3C3X1E6pM0R7goitR0nRCSbzyuGq M018AgwB+rMgLNtr2R8x+jpQAFsA60XCnq3cp89TPyflOKWHO1jTI9xFiv5cfiaJoY1y s/2Q== X-Gm-Message-State: AOJu0YxDlYj/EtZXLChfeumxIkTHgmbm7udkuY6nANeKB8uI/izjmJ8q eZxnZwK9ahEuw/wXLBdb9E/ti8w6I5fAUqQH+PAk7YELIGpU80Bs7GiwDUduMP0reSqeQtIpONQ WtOgQ X-Gm-Gg: ATEYQzyrMcGCophKrWx3ULAhdgRRu9FjaCwPMusL9vMuAqDJoASJKN8S/qxbsoxwWaz rrvtj7+cIdI5Rgg2ELUQrRwcBLMIF1Ur1/pohCqZ1m2cud9HFJTgQQ+zSvOULBMmmcys7wN/066 vXqFCXV5TRcSqbfeIsYoRkmnA2eomDonTnDaq8yVMCGH7Bw2iloCUYIOI0SqJ+9p4feo1qTOu3w Q2DbsOFVEwvKkXPTB+gtoFBGY823mv+CkiRVQfW4bwcaCU8opRPvX1gK8saWN99+QaDqK8dG70v TXtHyN4RNdPidPbpOy5ItV9N35l4Vmy/OAJwVXWse+Mz67QLAUDsgbDxgvu+uEP1w4Oe/LPOYKP hqOLQKiCsbh4SODHK/GTF2uvZsPzrJlJxAeewCx4ayGpKMfHjfeszj55jWW3JLJTunjK+gXTooc nFJwRL+0lhTrSfD480q4B7XF7tLGWqY6s1EnqbFs10XOJfKg== X-Received: by 2002:a05:6122:ca1:b0:56a:ef89:34fc with SMTP id 71dfb90a1353d-56d8a81bd48mr3291089e0c.6.1775149949256; Thu, 02 Apr 2026 10:12:29 -0700 (PDT) Received: from mandiga.. ([2804:1b3:a7c2:40e0:23f9:a1c4:77f8:d4bc]) by smtp.gmail.com with ESMTPSA id 71dfb90a1353d-56d9babf3e6sm4064002e0c.6.2026.04.02.10.12.27 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Thu, 02 Apr 2026 10:12:28 -0700 (PDT) From: Adhemerval Zanella To: libc-alpha@sourceware.org Cc: DJ Delorie Subject: [PATCH] io: Fix silent readdir failures in ftw/nftw (BZ 33085) Date: Thu, 2 Apr 2026 14:12:09 -0300 Message-ID: <20260402171224.17813-1-adhemerval.zanella@linaro.org> X-Mailer: git-send-email 2.43.0 MIME-Version: 1.0 X-Spam-Status: No, score=-12.7 required=5.0 tests=BAYES_00, DKIM_SIGNED, DKIM_VALID, DKIM_VALID_AU, DKIM_VALID_EF, GIT_PATCH_0, KAM_SHORT, RCVD_IN_DNSWL_NONE, SPF_HELO_NONE, SPF_NONE, TXREP autolearn=ham autolearn_force=no version=3.4.6 X-Spam-Checker-Version: SpamAssassin 3.4.6 (2021-04-09) on sourceware.org X-BeenThere: libc-alpha@sourceware.org X-Mailman-Version: 2.1.30 Precedence: list List-Id: Libc-alpha mailing list List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libc-alpha-bounces~patchwork=sourceware.org@sourceware.org The {n}ftw functions fails to distinguish between the end of a directory stream and an read error. The existing implementation treated all NULL returns as the end of the stream, silently swallowing file system or I/O errors and incorrectly reporting a successful traversal. This patch fixes the issue in two places within io/ftw.c: 1. In open_dir_stream (the fallback loop used when the file descriptor limit is exhausted and directory entries must be cached). 2. In ftw_dir (the main directory reading loop, specifically within FTW_STATE_STREAM_LOOP). The testcasuse uses the FUSE libsupport to trigger getdents failure in this two readdir calls. Checked on x86_64-linux-gnu and i686-linux-gnu. --- io/Makefile | 1 + io/ftw.c | 17 +++- io/tst-ftw-bz33085.c | 195 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 211 insertions(+), 2 deletions(-) create mode 100644 io/tst-ftw-bz33085.c diff --git a/io/Makefile b/io/Makefile index 80e50578b2..f0a5a0fa3a 100644 --- a/io/Makefile +++ b/io/Makefile @@ -200,6 +200,7 @@ tests := \ tst-fts-lfs \ tst-ftw-bz26353 \ tst-ftw-bz28126 \ + tst-ftw-bz33085 \ tst-ftw-lnk \ tst-futimens \ tst-futimes \ diff --git a/io/ftw.c b/io/ftw.c index 726c430eaf..e292a45aa6 100644 --- a/io/ftw.c +++ b/io/ftw.c @@ -246,8 +246,16 @@ open_dir_stream (int *dfdp, struct ftw_data *data, struct dir_data *dirp) struct dirent64 *d; size_t actsize = 0; - while ((d = __readdir64 (st)) != NULL) + while (1) { + errno = 0; + d = __readdir64 (st); + if (d == NULL) + { + if (errno != 0) + return -1; + break; + } size_t this_len = NAMLEN (d); if (actsize + this_len + 2 >= bufsize) { @@ -607,6 +615,7 @@ ftw_dir (struct ftw_data *data, const struct STRUCT_STAT *st) continue; } + errno = 0; struct dirent64 *d = __readdir64 (frame->dir.stream); if (d != NULL) { @@ -631,7 +640,11 @@ ftw_dir (struct ftw_data *data, const struct STRUCT_STAT *st) } } else - frame->state = FTW_STATE_CLEANUP; + { + frame->state = FTW_STATE_CLEANUP; + if (errno != 0) + result = -1; + } } else if (frame->state == FTW_STATE_CONTENT_LOOP) { diff --git a/io/tst-ftw-bz33085.c b/io/tst-ftw-bz33085.c new file mode 100644 index 0000000000..8ca96ba2f5 --- /dev/null +++ b/io/tst-ftw-bz33085.c @@ -0,0 +1,195 @@ +/* Test if nftw correctly handles readdir errors. + Copyright (C) 2026 Free Software Foundation, Inc. + This file is part of the GNU C Library. + + The GNU C Library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + The GNU C Library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with the GNU C Library; if not, see + . */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +/* The test stress the readdir calls from nftw, where failures should not + handled as end of stream. The first it at 'ftw_dir' + (FTW_STATE_STREAM_LOOP) for the default entries read. The another one is + at open_dir_stream where it is triggered when there is a file description + exaustion and the code< must close an existing stream to make room for the + new subdirectory stream. */ + +static _Atomic bool readdir_triggered = false; + +static void +fuse_thread (struct support_fuse *f, void *closure) +{ + struct fuse_in_header *inh; + + while ((inh = support_fuse_next (f)) != NULL) + { + switch (inh->opcode) + { + case FUSE_GETATTR: + { + /* We need to respond for both the root (1) and dir1 (2) */ + if (inh->nodeid == 1 || inh->nodeid == 2) + { + struct fuse_attr_out out = { 0 }; + out.attr_valid = 60; + out.attr.ino = inh->nodeid; + out.attr.mode = S_IFDIR | 0755; + out.attr.nlink = 3; /* Force nftw to look for subdirs */ + support_fuse_reply (f, &out, sizeof (out)); + } + else + support_fuse_reply_error (f, ENOENT); + break; + } + + case FUSE_LOOKUP: + { + const char *name = (const char *) (inh + 1); + if (strcmp (name, "dir1") == 0) + { + struct fuse_entry_out out = { 0 }; + out.nodeid = 2; + out.attr_valid = 60; + out.entry_valid = 60; + out.attr.ino = 2; + out.attr.mode = S_IFDIR | 0755; + out.attr.nlink = 2; + support_fuse_reply (f, &out, sizeof (out)); + } + else + support_fuse_reply_error (f, ENOENT); + break; + } + + case FUSE_OPENDIR: + { + struct fuse_open_out out = { 0 }; + out.fh = inh->nodeid; + support_fuse_reply (f, &out, sizeof (out)); + break; + } + + case FUSE_READDIR: + { + const struct fuse_read_in *rin = support_fuse_cast (READ, inh); + + if (inh->nodeid == 1) /* Reading the Root directory */ + { + if (rin->offset == 0) + { + /* First readdir, this happens in FTW_STATE_STREAM_LOOP. + We yield "dir1", prompting nftw to descend. */ + char buf[256] = { 0 }; + struct fuse_dirent *d = (struct fuse_dirent *) buf; + + d->ino = 2; + d->off = 1; + d->type = DT_DIR; + d->namelen = 4; + strcpy (d->name, "dir1"); + + size_t d_size = + FUSE_DIRENT_ALIGN (sizeof (struct fuse_dirent) + + d->namelen); + support_fuse_reply (f, buf, d_size); + } + else + { + /* Second readdir, this ONLY happens inside + open_dir_stream() when nftw tries to cache the + remaining entries before closing the stream to descend + into "dir1". */ + readdir_triggered = true; + support_fuse_reply_error (f, EIO); + } + } + else + /* Subdirectory logic (shouldn't be reached in this test) */ + support_fuse_reply_empty (f); + break; + } + + case FUSE_READDIRPLUS: + support_fuse_reply_error (f, EIO); + break; + + case FUSE_ACCESS: + case FUSE_RELEASEDIR: + support_fuse_reply_empty (f); + break; + + default: + support_fuse_reply_error (f, EIO); + } + } +} + +static int +nftw_cb (const char *fpath, const struct stat *sb, int typeflag, + struct FTW *ftwbuf) +{ + return 0; +} + +static int +do_test (void) +{ + support_fuse_init (); + struct support_fuse *f = support_fuse_mount (fuse_thread, NULL); + + { + /* This forces nftw to immediately exhaust its FD limit when it tries + to descend into 'dir1', forcing it into the open_dir_stream fallback + loop. */ + errno = 0; + readdir_triggered = false; + int ret = nftw (support_fuse_mountpoint (f), nftw_cb, 1, FTW_PHYS); + /* Assert that we successfully hit the caching __readdir64 block */ + TEST_VERIFY (readdir_triggered); + + /* Assert that nftw correctly aborted and propagated the EIO */ + TEST_COMPARE (ret, -1); + TEST_COMPARE (errno, EIO); + } + + { + /* Use a high descriptor count (10) so nftw doesn't fall back to + caching */ + errno = 0; + readdir_triggered = false; + int ret = nftw (support_fuse_mountpoint (f), nftw_cb, 10, FTW_PHYS); + /* Assert that the second readdir in the main loop was actually hit */ + TEST_VERIFY (readdir_triggered); + + /* Assert that nftw correctly aborts and propagates the EIO + This will fail until the `#if 0` block in ftw.c is patched) */ + TEST_COMPARE (ret, -1); + TEST_COMPARE (errno, EIO); + } + + support_fuse_unmount (f); + return 0; +} + +#include