From patchwork Thu Jul 16 05:56:32 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Babanpreet Singh X-Patchwork-Id: 92626 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 1600FC44511 for ; Thu, 16 Jul 2026 05:56:44 +0000 (UTC) Received: from mail-pl1-f176.google.com (mail-pl1-f176.google.com [209.85.214.176]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.6531.1784181396597703964 for ; Wed, 15 Jul 2026 22:56:36 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=@gmail.com header.s=20251104 header.b=EP6ZbgPU; spf=pass (domain: gmail.com, ip: 209.85.214.176, mailfrom: bbnpreetsingh@gmail.com) Received: by mail-pl1-f176.google.com with SMTP id d9443c01a7336-2ceb096e675so55796665ad.0 for ; Wed, 15 Jul 2026 22:56:36 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1784181396; x=1784786196; darn=lists.yoctoproject.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to:content-type; bh=bIxfU2MysL8f6MpK9jdQK0hfsaPk0NjankeRFgRPEUs=; b=EP6ZbgPUjv7z9spKieeEz6uyEeLQnbXN980vunVyXfdSz39QqhO5e7if5Aebd48DNm 1u8kUAoK31Nb407/OVSKzXDBrzJXnooWsTwfWcGcL+4ayH3Ot3xPcrAHOC77nKteGZqO +i0yPeHfaM5tnPSMBkcZCzgNgFUn765kOEzlEGyopfSziN6e0V/lBQ76AKGJO7rkgUul pWq8/AcXhbs53a+dnCKLY6+lBMfQXisqCZBne4I6JPKvhfUVRLTLRO9YsNEa907RmG3u 1LYCN9+8j2cCw4smr6jp0N1Mf/btdiUO83zC+Lk2EszxIcP2mX5bQJvZ5wV1h6PC6tpm wUTg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1784181396; x=1784786196; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to:content-type; bh=bIxfU2MysL8f6MpK9jdQK0hfsaPk0NjankeRFgRPEUs=; b=mvtwFaNHmgmYkMZwhovViGYt3OxYXpNjpMHw1Fvb+o7EqXoC6YsPQ1XbweW10uypVy /IVOUDUexAyhV/A/Oxc10aUeEBtCzhpUrfdzm9G5uq9bipnnrjAMakagkAuGHa2cnszy jlz/+GzpPD+dzRvc5FTtcnpsY6WCwmHZE4lLl+dRgUq96DwbEAarROESIttUTgkwtRGg mLqOh/C9LIW1gc2qOdYEriGhfOom7IYIp1Ni5VBG3Mrl2pF7iAWGmoa3lysHL3BpY6pp D6Nt1X/Fq4D4/Z3FWrd0FkLTVTbebQeK8qNsqRzbII0HvsLIZSjeBjZbD0I9HjS1TGe3 FHqQ== X-Gm-Message-State: AOJu0YweYNddb3SDc6THFgD6l2GGJXkeH/NfpbfRo8J83v5DCQEIWSdj w/l2noMnFysC2KiINXyDCdMiBNJyLxni/ib5Ug8BdtTFgMzZ1zlvproxNsjKySPj5qQ= X-Gm-Gg: AfdE7cmw0Cwcpgzzr96y4jWT04qIaVKMDW3tFFNA8hZR6MlEOfdC/pC++cXdlleXhVV zmgjZ6WP2aaZB0SE/brpn++47CYn98qoW09j3AHUhhHRz5pWVEYzfaBeJ7VslH4LXBNZjEsykHm czJaXXELjAmZmr9Fra7R/p0e6X6eTU3IGdPH7IHYBqf77xVoe9hrggHXf7cFmu3G7MbGyZh9hJZ csrA4sq38XPFPGyiDv5j4+kCR3+MAqaoCeZbrbnfDMDeEoEWViOEFc7xvwfIrAmaSGOfzIcgaeY lUMyqP/yufPrtV+PR7aOM44lgKXbQ9jZUQ0qg2YSVItIVx4a4//7nLIoaYpC8cNzRIYtdAb1CDp r+LsFa0/+qlA+aF7PDoF9awWr46ilhAkDtYCWeGUjQcQPZghHBT0hwoHZsJ7cjrWdhUq2UDMkfp x3cA== X-Received: by 2002:a17:902:ec8e:b0:2ca:ce91:f027 with SMTP id d9443c01a7336-2cf03d37947mr50158105ad.36.1784181395916; Wed, 15 Jul 2026 22:56:35 -0700 (PDT) Received: from ydev.. ([108.180.130.139]) by smtp.gmail.com with ESMTPSA id d9443c01a7336-2cf100e44ecsm14124585ad.12.2026.07.15.22.56.35 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Wed, 15 Jul 2026 22:56:35 -0700 (PDT) From: Babanpreet Singh To: yocto-patches@lists.yoctoproject.org Cc: Paul Barker , Richard Purdie , Mark Hatle , Randy MacLeod , Vincent Haupert , Babanpreet Singh Subject: [pseudo] [PATCH v2 1/2] ports/linux/guts: Implement close_range() instead of returning ENOSYS Date: Thu, 16 Jul 2026 05:56:32 +0000 Message-ID: <20260716055633.7-2-bbnpreetsingh@gmail.com> X-Mailer: git-send-email 2.43.0 In-Reply-To: <20260716055633.7-1-bbnpreetsingh@gmail.com> References: <20260715054142.7-1-bbnpreetsingh@gmail.com> <20260716055633.7-1-bbnpreetsingh@gmail.com> MIME-Version: 1.0 List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Thu, 16 Jul 2026 05:56:44 -0000 X-Groupsio-URL: https://lists.yoctoproject.org/g/yocto-patches/message/4491 The close_range() wrapper added in 35433e6 ("ports/linux/guts: Add close_range wrapper for glibc 2.34") returns ENOSYS on the assumption that callers handle that. systemd v260 no longer does: it removed its /proc/self/fd fallback and treats a close_range() failure as fatal, so every fork+exec under pseudo aborts: Failed to close all file descriptors: Function not implemented '(mkfs)' failed with exit status 1. A client side op closes the low descriptors one at a time, skipping the ones pseudo needs for itself, and returns the first fd above pseudo's own so the caller can pass the rest of the range to the kernel directly. CLOSE_RANGE_UNSHARE is handled by calling unshare(CLONE_FILES) before closing anything, so the closes only affect the caller and not other processes sharing the descriptor table. Unknown flags and an inverted range are rejected with EINVAL before anything is closed. A range starting entirely above INT_MAX cannot contain any of pseudo's fds and is also passed straight through. [YOCTO #16339] AI-Generated: Uses Claude (claude-opus-4-8) Signed-off-by: Babanpreet Singh --- enums/op.in | 1 + ports/linux/guts/close_range.c | 54 ++++++++++++++++++++++++++---- ports/linux/portdefs.h | 16 +++++++++ pseudo_client.c | 60 ++++++++++++++++++++++++++++++++++ 4 files changed, 124 insertions(+), 7 deletions(-) diff --git a/enums/op.in b/enums/op.in index 5b5e21b..5013892 100644 --- a/enums/op.in +++ b/enums/op.in @@ -28,3 +28,4 @@ set-xattr, 0 create-xattr, 1 replace-xattr, 1 closefrom, 0 +close-range, 0 diff --git a/ports/linux/guts/close_range.c b/ports/linux/guts/close_range.c index 4bd2fe1..c3ca71d 100644 --- a/ports/linux/guts/close_range.c +++ b/ports/linux/guts/close_range.c @@ -6,14 +6,54 @@ * int close_range(unsigned int lowfd, unsigned int maxfd, int flags) * int rc = -1; */ + pseudo_msg_t *msg; - (void) lowfd; - (void) maxfd; - (void) flags; - /* for now pretend the kernel doesn't support it regardless - which users are supposed to be able to handle */ - errno = ENOSYS; - rc = -1; + /* The kernel rejects both of these outright and closes nothing when + * it does, so validate before touching anything. + */ + if (flags & ~(CLOSE_RANGE_UNSHARE | CLOSE_RANGE_CLOEXEC)) { + errno = EINVAL; + return -1; + } + if (lowfd > maxfd) { + errno = EINVAL; + return -1; + } + + /* CLOSE_RANGE_UNSHARE has to take effect before anything is closed: + * while the descriptor table is still shared, closing a descriptor + * would close it for everyone sharing the table, not just for us. + */ + if (flags & CLOSE_RANGE_UNSHARE) { + if (unshare(CLONE_FILES) == -1) + return -1; + flags &= ~CLOSE_RANGE_UNSHARE; + } + + /* CLOSE_RANGE_CLOEXEC closes nothing, it only marks descriptors, and + * pseudo's own are close-on-exec already (pseudo_fd() sets that on + * every one of them), so there is nothing here to protect. + */ + if (flags & CLOSE_RANGE_CLOEXEC) + return real_close_range(lowfd, maxfd, flags); + + /* Descriptors are ints, so a range starting above INT_MAX cannot hold + * any of pseudo's own and there is nothing to step around. Worth its + * own case because pseudo_client_op() takes the low end as an int. + */ + if (lowfd > INT_MAX) + return real_close_range(lowfd, maxfd, flags); + + /* Same shape as closefrom(): the client op closes the descriptors its + * own are mixed in with by hand, stepping around the ones pseudo needs + * to keep, and hands back the first fd the kernel can safely be turned + * loose on. + */ + msg = pseudo_client_op(OP_CLOSE_RANGE, 0, lowfd, -1, 0, 0, maxfd); + if (maxfd >= (unsigned int) msg->fd) + rc = real_close_range(msg->fd, maxfd, flags); + else + rc = 0; /* return rc; * } diff --git a/ports/linux/portdefs.h b/ports/linux/portdefs.h index 19bb232..1f1a41a 100644 --- a/ports/linux/portdefs.h +++ b/ports/linux/portdefs.h @@ -35,6 +35,22 @@ GLIBC_COMPAT_SYMBOL(memcpy,2.0); #include #include +/* close_range()'s flags, and unshare(), are only declared by glibc under + * _GNU_SOURCE, which pseudo does not build with. is + * not an option either: it is absent on hosts with pre-5.9 kernel headers, + * the same problem SYS_openat2 has below. Both values are kernel ABI. + */ +#ifndef CLOSE_RANGE_UNSHARE +#define CLOSE_RANGE_UNSHARE (1U << 1) +#endif +#ifndef CLOSE_RANGE_CLOEXEC +#define CLOSE_RANGE_CLOEXEC (1U << 2) +#endif +#ifndef CLONE_FILES +#define CLONE_FILES 0x00000400 +#endif +extern int unshare(int flags); + #ifndef _STAT_VER #if defined (__aarch64__) || defined (__riscv) #define _STAT_VER 0 diff --git a/pseudo_client.c b/pseudo_client.c index 7041366..1acd948 100644 --- a/pseudo_client.c +++ b/pseudo_client.c @@ -993,6 +993,28 @@ pseudo_client_closefrom(int fd) { } } +/* Like pseudo_client_closefrom(), but bounded: close_range() has a top end, + * so entries above it have to be left alone. + */ +static void +pseudo_client_close_range(int lowfd, unsigned int maxfd) { + int i, top; + + if (lowfd < 0 || lowfd >= nfds) + return; + + top = (maxfd >= (unsigned int) nfds) ? nfds - 1 : (int) maxfd; + for (i = lowfd; i <= top; ++i) { + free(fd_paths[i]); + fd_paths[i] = 0; + + if (i < linked_nfds) { + free(linked_fd_paths[i]); + linked_fd_paths[i] = 0; + } + } +} + /* spawn server */ static int client_spawn_server(void) { @@ -1633,6 +1655,7 @@ pseudo_client_op(pseudo_op_t op, int access, int fd, int dirfd, const char *path static size_t alloced_len = 0; int strip_slash; int startfd, i; + unsigned int close_range_maxfd = 0; #ifdef PSEUDO_PROFILING struct timeval tv1_op, tv2_op; @@ -1753,6 +1776,13 @@ pseudo_client_op(pseudo_op_t op, int access, int fd, int dirfd, const char *path } #endif + if (op == OP_CLOSE_RANGE) { + va_list ap; + va_start(ap, buf); + close_range_maxfd = va_arg(ap, unsigned int); + va_end(ap); + } + if (op == OP_RENAME) { va_list ap; if (!path) { @@ -1959,6 +1989,36 @@ pseudo_client_op(pseudo_op_t op, int access, int fd, int dirfd, const char *path msg.fd = startfd; do_request = 0; break; + case OP_CLOSE_RANGE: + /* no request needed */ + startfd = fd; + if (pseudo_util_debug_fd > startfd) + startfd = pseudo_util_debug_fd + 1; + if (pseudo_localstate_dir_fd > startfd) + startfd = pseudo_localstate_dir_fd + 1; + if (pseudo_pwd_fd > startfd) + startfd = pseudo_pwd_fd + 1; + if (pseudo_grp_fd > startfd) + startfd = pseudo_grp_fd + 1; + if (connect_fd > startfd) + startfd = connect_fd + 1; + /* the fds below startfd are the ones our own are mixed in + * with, so close those by hand and skip the ones we need + */ + for (i = fd; i < startfd && (unsigned int) i <= close_range_maxfd; ++i) { + if (i == pseudo_util_debug_fd || i == pseudo_localstate_dir_fd || i == pseudo_pwd_fd || + i == pseudo_grp_fd || i == connect_fd) + continue; + pseudo_client_close(i); + close(i); + } + if (close_range_maxfd >= (unsigned int) startfd) + pseudo_client_close_range(startfd, close_range_maxfd); + /* tell the caller to start at startfd instead of fd */ + result = &msg; + msg.fd = startfd; + do_request = 0; + break; case OP_CLOSE: /* no request needed */ if (fd >= 0) { From patchwork Thu Jul 16 05:56:33 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Babanpreet Singh X-Patchwork-Id: 92625 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 08D37C44510 for ; Thu, 16 Jul 2026 05:56:44 +0000 (UTC) Received: from mail-pl1-f169.google.com (mail-pl1-f169.google.com [209.85.214.169]) by mx.groups.io with SMTP id smtpd.msgproc01-g2.6455.1784181397384671808 for ; Wed, 15 Jul 2026 22:56:37 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=@gmail.com header.s=20251104 header.b=SGbv8SK4; spf=pass (domain: gmail.com, ip: 209.85.214.169, mailfrom: bbnpreetsingh@gmail.com) Received: by mail-pl1-f169.google.com with SMTP id d9443c01a7336-2cace91f112so59268195ad.0 for ; Wed, 15 Jul 2026 22:56:37 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1784181397; x=1784786197; darn=lists.yoctoproject.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to:content-type; bh=rimp8WitE9zbsIDrFZlsS+wNK/4vZn0uQjicKbM1kPg=; b=SGbv8SK4DmJZT9xR1Qe5LRret6BESCZTaWIIQ94Jhnl/GqzmbEa9mke9qzf1CyBVin hUZvjtrpgdWtUTJOQzrHGzVdDb+WexkS2cS8QzNKOlxG1NfV+zAFfPrXcbU5RWKrZfi0 BIqjcPTZzamaaB8gJo6jXd3n7dqdfv/48BCu1gonb36UMkt2cXH0kqKYsrwiSfMxUkzm JuFH9CIIrIoA8PHCo//qvfhm+msNkq2cxMyJCGMOKClor1IXIpJG5egGqiFNbVfmd8H8 YqoHlLMzigdjb1LMWJn6E91bj/1BxxEdFWOcwis9AaAlN80wi1n/pd/OILjU0ePzWiKk xh5Q== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1784181397; x=1784786197; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to:content-type; bh=rimp8WitE9zbsIDrFZlsS+wNK/4vZn0uQjicKbM1kPg=; b=J0ubMlsz5IFeywIJpBgd0dlPFmTO7qeOXkCQvJiVnCVazb8F5ibvvM+mN6EZ3kHF1a 01UIKMxpBHUdTkOuwpaWjpS0vdnVJmdUuZKvz5w9062APlS4GUzRW7vbhNZ5QCMo8Chg l+4br1t634y3O9JxmxYSpLhHYlLC2Cz08RaZGwMJ5Zl2ksajsWgfNV/xWHSYCyZajqdD UBkVRZ0i4PWuOE4O0rDbgFEJ4dL6Wi1HWUAuKT/z0OJYRLlDzWzxQ7+asUC0xEdNj+71 9trVSGrS2eDTuAfXx2o0ZvIUyfAzK14rXlMXCy5xVJPIc06z7nVKJg03Q6mJOfWfs//i 84OA== X-Gm-Message-State: AOJu0Yw7PpVecTIX0Ci+50pPdWxb3mWNl/84O36oRgANcdUI33kRO3Em MEvbHmoGNrEuD3JN1dx8cwPt4OA3FlDuAQWm82WCSpB5/8OjcIclNmxB8Q4xUXJZZTU= X-Gm-Gg: AfdE7cnAqEGeawJHqmTnggO3SewQIBas8pXo43yKZknhFmyzeLl3cFBwi4UJvvt/PgZ cDEzrHWS3epQRNbbQMudGyWwwuSLAv+7+fH4eXAzFuqlCHN9vnw9AJJmmBjrl8fcPJp1b+qizGP aNGrH3X0scvHUQ4IEaN9akJ57OcwIPZITBdL6H9d6vGTMfPBfKeueD/FXmAstl+VB36zZgDBCgF YVNdy8J8iw2xXaWu3jNUdIp80XtOSaDCacxeqKD1jGsD6c/ROo/CEPSYOjsTS6ID241aaSNJtUk LAZlKpTAtK1AakfMC+qwjLyNRyb3V3dXo1t2Uo6+SAMMzEaZZpuxJi/iP3kEI7eb0Va81Y+ZNqw bO/LqVZSIX/qGhUi/TlJnKfxEF3ExKTER4IRrEISHLZoFI+4p+44+Bsu6PoSNjf9zZgylFYCKqr a6IQ== X-Received: by 2002:a17:903:b8e:b0:2c0:a555:80d6 with SMTP id d9443c01a7336-2cf03c762e3mr50483315ad.2.1784181396627; Wed, 15 Jul 2026 22:56:36 -0700 (PDT) Received: from ydev.. ([108.180.130.139]) by smtp.gmail.com with ESMTPSA id d9443c01a7336-2cf100e44ecsm14124585ad.12.2026.07.15.22.56.36 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Wed, 15 Jul 2026 22:56:36 -0700 (PDT) From: Babanpreet Singh To: yocto-patches@lists.yoctoproject.org Cc: Paul Barker , Richard Purdie , Mark Hatle , Randy MacLeod , Vincent Haupert , Babanpreet Singh Subject: [pseudo] [PATCH v2 2/2] tests: Add close_range() test Date: Thu, 16 Jul 2026 05:56:33 +0000 Message-ID: <20260716055633.7-3-bbnpreetsingh@gmail.com> X-Mailer: git-send-email 2.43.0 In-Reply-To: <20260716055633.7-1-bbnpreetsingh@gmail.com> References: <20260715054142.7-1-bbnpreetsingh@gmail.com> <20260716055633.7-1-bbnpreetsingh@gmail.com> MIME-Version: 1.0 List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Thu, 16 Jul 2026 05:56:44 -0000 X-Groupsio-URL: https://lists.yoctoproject.org/g/yocto-patches/message/4492 Covers the ENOSYS regression itself, that the wrapper agrees with the kernel about bad arguments, and the part that actually needs the care: pseudo keeps its own descriptors in the range, including the connection to its server, and has to come out of a close_range(3, ~0U, 0) still tracking ownership. CLOSE_RANGE_UNSHARE is exercised against a child cloned with CLONE_FILES: the child closes a shared descriptor with the flag set, and the parent must still hold that descriptor afterwards, which is what the wrapper's unshare-before-closing order exists to preserve. A second child then closes the same descriptor without the flag and the parent must see it gone. close_range() needs a 5.9 kernel and CLOSE_RANGE_CLOEXEC a 5.11 one, so the shell wrapper probes for each with PSEUDO_DISABLED=1 before running. On 5.9 and 5.10 the syscall probe succeeds, the flag probe fails, and only the CLOSE_RANGE_CLOEXEC part of the test is left out. [YOCTO #16339] AI-Generated: Uses Claude (claude-opus-4-8) Signed-off-by: Babanpreet Singh --- test/test-close-range.c | 283 +++++++++++++++++++++++++++++++++++++++ test/test-close-range.sh | 24 ++++ 2 files changed, 307 insertions(+) create mode 100644 test/test-close-range.c create mode 100755 test/test-close-range.sh diff --git a/test/test-close-range.c b/test/test-close-range.c new file mode 100644 index 0000000..482c85e --- /dev/null +++ b/test/test-close-range.c @@ -0,0 +1,283 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-only + * + * close_range() was stubbed out to return ENOSYS unconditionally, on the + * assumption that callers all cope with that. Current systemd does not: it + * dropped its /proc fallback once its kernel baseline reached 5.10, so every + * fork+exec under pseudo failed. Check the wrapper works, that it agrees + * with the kernel about bad arguments, that CLOSE_RANGE_UNSHARE keeps a + * process sharing the descriptor table unaffected, and that pseudo still + * functions after a caller closes every descriptor from 3 up. + */ +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* is missing on hosts with pre-5.9 kernel headers */ +#ifndef CLOSE_RANGE_UNSHARE +#define CLOSE_RANGE_UNSHARE (1U << 1) +#endif +#ifndef CLOSE_RANGE_CLOEXEC +#define CLOSE_RANGE_CLOEXEC (1U << 2) +#endif + +/* matches test-openat2-syscall: the shell wrapper turns 77 into "skipped" */ +#define SKIP 77 + +#define TESTFILE "test-close-range-file" + +static int open_null(void) { + int fd = open("/dev/null", O_RDONLY); + if (fd < 0) + perror("open /dev/null"); + return fd; +} + +static int is_closed(int fd) { + return fcntl(fd, F_GETFD) == -1 && errno == EBADF; +} + +/* Is close_range() there at all? The wrapper runs this with PSEUDO_DISABLED + * set, so the answer is the kernel's and a stubbed wrapper cannot pass + * itself off as an old kernel. + */ +static int probe(void) { + int fd = open_null(); + + if (fd < 0) + return 1; + if (close_range(fd, fd, 0) == -1) { + int err = errno; + close(fd); + return (err == ENOSYS) ? SKIP : 1; + } + return 0; +} + +/* CLOSE_RANGE_CLOEXEC is newer than the syscall: 5.11 rather than 5.9. Probe + * for it separately, the same way, so that on a 5.9 or 5.10 kernel only the + * CLOSE_RANGE_CLOEXEC part of the test is left out, not the whole thing. + */ +static int probe_cloexec(void) { + int fd = open_null(); + int rc; + + if (fd < 0) + return 1; + rc = close_range(fd, fd, CLOSE_RANGE_CLOEXEC); + if (rc == -1) { + int err = errno; + close(fd); + return (err == EINVAL || err == ENOSYS) ? SKIP : 1; + } + close(fd); + return 0; +} + +/* for the CLONE_FILES children: close one descriptor, with or without + * CLOSE_RANGE_UNSHARE, and report whether it is gone for the child itself. + */ +struct child_close { + int fd; + int flags; +}; + +static char child_stack[65536]; + +static int child_close_one(void *arg) { + struct child_close *cc = arg; + + if (close_range(cc->fd, cc->fd, cc->flags) == -1) + return 2; + return is_closed(cc->fd) ? 0 : 3; +} + +int main(int argc, char **argv) { + struct child_close cc; + struct stat st; + int a, b, c; + int status; + int test_cloexec = 1; + pid_t pid; + + if (argc > 1 && !strcmp(argv[1], "--probe")) + return probe(); + if (argc > 1 && !strcmp(argv[1], "--probe-cloexec")) + return probe_cloexec(); + if (argc > 1 && !strcmp(argv[1], "--no-cloexec")) + test_cloexec = 0; + + /* the reported bug: this used to fail with ENOSYS every time */ + a = open_null(); + if (a < 0) + return 1; + if (close_range(a, a, 0) == -1) { + fprintf(stderr, "close_range(%d, %d, 0): %s\n", a, a, strerror(errno)); + return 1; + } + if (!is_closed(a)) { + fprintf(stderr, "close_range() left fd %d open\n", a); + return 1; + } + + /* a bounded range closes all of itself */ + a = open_null(); + b = open_null(); + c = open_null(); + if (a < 0 || b < 0 || c < 0) + return 1; + if (close_range(a, c, 0) == -1) { + perror("close_range(a, c, 0)"); + return 1; + } + if (!is_closed(a) || !is_closed(b) || !is_closed(c)) { + fprintf(stderr, "close_range(%d, %d, 0) left descriptors open\n", a, c); + return 1; + } + + /* CLOSE_RANGE_CLOEXEC marks descriptors, it does not close them. The + * flag needs a 5.11 kernel; the shell wrapper passes --no-cloexec + * when the probe says this kernel has the syscall but not the flag. + */ + if (test_cloexec) { + a = open_null(); + if (a < 0) + return 1; + if (close_range(a, a, CLOSE_RANGE_CLOEXEC) == -1) { + perror("close_range(a, a, CLOSE_RANGE_CLOEXEC)"); + return 1; + } + if (is_closed(a)) { + fprintf(stderr, "CLOSE_RANGE_CLOEXEC closed fd %d\n", a); + return 1; + } + if (!(fcntl(a, F_GETFD) & FD_CLOEXEC)) { + fprintf(stderr, "CLOSE_RANGE_CLOEXEC did not set FD_CLOEXEC on fd %d\n", a); + return 1; + } + close(a); + } + + /* bad arguments are refused, and a refused call closes nothing */ + a = open_null(); + if (a < 0) + return 1; + if (close_range(a, a, 0x80) != -1 || errno != EINVAL) { + fprintf(stderr, "close_range() with an unknown flag should fail EINVAL\n"); + return 1; + } + if (is_closed(a)) { + fprintf(stderr, "a rejected close_range() closed fd %d anyway\n", a); + return 1; + } + if (close_range(a + 1, a, 0) != -1 || errno != EINVAL) { + fprintf(stderr, "close_range() with lowfd > maxfd should fail EINVAL\n"); + return 1; + } + close(a); + + /* a range entirely above INT_MAX holds no descriptors at all, but it + * still has to be accepted rather than mangled on the way through + */ + if (close_range((unsigned int) INT_MAX + 1, ~0U, 0) == -1) { + fprintf(stderr, "close_range(INT_MAX+1, ~0U, 0): %s\n", strerror(errno)); + return 1; + } + + /* CLOSE_RANGE_UNSHARE unshares the descriptor table before closing: + * when a child cloned with CLONE_FILES closes our shared descriptor + * with the flag set, we must still hold that descriptor afterwards. + * This is what the wrapper's unshare-first order preserves; closing + * by hand before unsharing would go through the still-shared table. + */ + a = open_null(); + if (a < 0) + return 1; + cc.fd = a; + cc.flags = CLOSE_RANGE_UNSHARE; + pid = clone(child_close_one, child_stack + sizeof(child_stack), + CLONE_FILES | SIGCHLD, &cc); + if (pid == -1) { + perror("clone(CLONE_FILES)"); + return 1; + } + if (waitpid(pid, &status, 0) == -1) { + perror("waitpid"); + return 1; + } + if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { + fprintf(stderr, "CLOSE_RANGE_UNSHARE failed in the child (status %d)\n", + status); + return 1; + } + if (is_closed(a)) { + fprintf(stderr, "child's CLOSE_RANGE_UNSHARE closed the parent's fd %d\n", a); + return 1; + } + + /* the control for the test above: the same close without the flag + * happens in the shared table, so this time fd a must be gone for + * us as well. A clone that quietly stopped sharing the table would + * pass the test above no matter what the wrapper did; it fails here + * instead. + */ + cc.flags = 0; + pid = clone(child_close_one, child_stack + sizeof(child_stack), + CLONE_FILES | SIGCHLD, &cc); + if (pid == -1) { + perror("clone(CLONE_FILES)"); + return 1; + } + if (waitpid(pid, &status, 0) == -1) { + perror("waitpid"); + return 1; + } + if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { + fprintf(stderr, "close_range() failed in the CLONE_FILES child (status %d)\n", + status); + return 1; + } + if (!is_closed(a)) { + fprintf(stderr, "close in a CLONE_FILES child did not reach the parent's fd %d\n", a); + return 1; + } + + /* And the point of the care in the wrapper: pseudo keeps descriptors + * of its own in this range and has to come out of it still working. + */ + a = open(TESTFILE, O_CREAT | O_WRONLY, 0644); + if (a < 0) { + perror("open " TESTFILE); + return 1; + } + close(a); + if (chown(TESTFILE, 0, 0) == -1) { + perror("chown " TESTFILE); + return 1; + } + if (close_range(3, ~0U, 0) == -1) { + fprintf(stderr, "close_range(3, ~0U, 0): %s\n", strerror(errno)); + return 1; + } + if (stat(TESTFILE, &st) == -1) { + perror("stat after close_range"); + return 1; + } + if (st.st_uid != 0 || st.st_gid != 0) { + fprintf(stderr, "pseudo lost track after close_range: uid %d, gid %d\n", + (int) st.st_uid, (int) st.st_gid); + return 1; + } + unlink(TESTFILE); + + return 0; +} diff --git a/test/test-close-range.sh b/test/test-close-range.sh new file mode 100755 index 0000000..578102c --- /dev/null +++ b/test/test-close-range.sh @@ -0,0 +1,24 @@ +#!/bin/bash +# +# SPDX-License-Identifier: LGPL-2.1-only +# + +# close_range() needs a 5.9 kernel. Ask with pseudo out of the way, so that a +# wrapper which has gone back to returning ENOSYS cannot pass itself off as an +# old kernel and quietly skip the test it is supposed to fail. +PSEUDO_DISABLED=1 ./test/test-close-range --probe +case $? in +0) ;; +77) exit 255 ;; +*) exit 1 ;; +esac + +# CLOSE_RANGE_CLOEXEC needs 5.11. On a 5.9 or 5.10 kernel the syscall probe +# above succeeds but the flag does not exist yet, so probe for it the same +# way and leave only the CLOSE_RANGE_CLOEXEC part of the test out. +PSEUDO_DISABLED=1 ./test/test-close-range --probe-cloexec +case $? in +0) ./test/test-close-range ;; +77) ./test/test-close-range --no-cloexec ;; +*) exit 1 ;; +esac