Files
jak-project/third-party/curl/tests/libtest/lib518.c
T
Alexander J. Semenuk 5d5e35fb9b fix: Windows toolchain compatibility (curl 8.21 re-vendor, endless reconfigure loop) (#4355)
## Problem

Windows builds break with a current local toolchain (Scoop LLVM 22.1.8,
CMake 4.4.0, VS 2026), in two independent ways:

1. The build stops at curl's deliberate guard: `#error "no non-blocking
method was found/used/set"` in `third-party/curl/lib/nonblock.c`.
2. From the second configure onward, `cmake --build` re-runs CMake in an
endless loop (observed 42 consecutive reconfigure cycles in a single
build). Likely the same mechanism behind the "endlessly building" VS
2026 note in `docs/setup/dev/vs.md`.

## Root cause

1. `third-party/curl/CMake/CurlTests.c` passes `int *` to
`ioctlsocket()`, whose third parameter is `u_long *`. Clang 22 promotes
`-Wincompatible-pointer-types` to a hard error in C, so the
`HAVE_IOCTLSOCKET_FIONBIO` try_compile silently fails and
`curl_config.h` never defines it. Upstream CI does not see this because
the windows-2022 runner image ships an older LLVM. GCC 14 promotes the
same warning to a hard error, which is very likely the `CurlTests.c.obj`
failure reported from MSYS2 in open-goal/jak-project#3551. Upstream curl
hit the identical problem with GCC 14 and fixed the probe in curl 8.8.0
(curl/curl#13578).
2. The root CMakeLists copies the build tree's `compile_commands.json`
into `<src>/build/` for clangd using `configure_file()`, which registers
its input as a configure dependency. CMake rewrites
`compile_commands.json` late in every generation, after
`CTestTestfile.cmake` and `cmake_install.cmake` (outputs of the same
Ninja regen rule), so once the dependency is registered the rule is
deterministically dirty and every `ninja` invocation re-runs CMake. A
pristine first configure is safe (the file does not exist yet, so the
`if(EXISTS ...)` guard skips the copy), which is why the loop looks
machine- or IDE-specific.

## Fix

1. Per review, re-vendor `third-party/curl` at the `curl-8_21_0` tag
(previously `curl-8_3_0`), which carries the upstream probe fix plus two
years of upstream development; `vendor.yaml` updated to match.
Adjustments the version jump forced:
- curl 8.15 removed the native macOS Secure Transport backend
(`CURL_USE_SECTRANSP`), so macOS now builds curl against OpenSSL like
Linux. The two macOS workflows install Homebrew `openssl@3` and export
`OPENSSL_ROOT_DIR` (keg-only), and the macOS setup docs gained the same
two lines.
- `CURL_BROTLI` / `CURL_ZSTD` switched to AUTO-detection in curl 8.10;
pinned OFF to keep the previous no-compression behavior and avoid
silently linking whatever the CI images happen to have.
- curl's new top-level `BUILD_EXAMPLES` cache option (default ON) leaked
into discord-rpc's identically named option and broke configure at a
nonexistent `examples/send-presence` directory; pinned OFF ahead of the
third-party subdirectories.

The diff is dominated by the mechanical tag-tree swap under
`third-party/curl` (linguist-vendored, collapsed in review). The
hand-written changes are `CMakeLists.txt`, the two macOS workflows,
`docs/setup/system/macos.md`, and `vendor.yaml`.
2. Swap `configure_file()` for `file(COPY ...)`: the same clangd copy
with no configure dependency registered. (`file(COPY_FILE ...
ONLY_IF_DIFFERENT)` would be cleaner still but requires CMake 3.21,
above the declared `cmake_minimum_required(VERSION 3.10)`.)

## Test plan

- [x] Fresh `cmake --preset Release-windows-clang` (LLVM 22, no cache
seeding) completes and logs `Enabled SSL backends: Schannel`; the
FIONBIO probe passes without the previous `#error`
- [x] Full Windows Release build from scratch in the branch worktree
(all 1422 targets)
- [x] goalc-test suite: 1509 passed, 0 failed
- [x] Second consecutive configure with `compile_commands.json` present:
the regen rule in `build.ninja` has no `compile_commands.json` input;
`<src>/build/compile_commands.json` is still refreshed for clangd
- [x] Repeated `ninja` invocations after a full build no longer re-run
CMake
- [x] macOS Intel and ARM CI green (first exercise of the OpenSSL
backend switch)

---

I work off a self-hosted forge, so this GitHub account is quiet; the
configure logs and ninja dirty-node traces from the investigation are
available if anyone wants the raw data.

(AI-assisted)
2026-07-27 19:19:18 -04:00

474 lines
15 KiB
C
Vendored
Generated

/***************************************************************************
* _ _ ____ _
* Project ___| | | | _ \| |
* / __| | | | |_) | |
* | (__| |_| | _ <| |___
* \___|\___/|_| \_\_____|
*
* Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
*
* This software is licensed as described in the file COPYING, which
* you should have received as part of this distribution. The terms
* are also available at https://curl.se/docs/copyright.html.
*
* You may opt to use, copy, modify, merge, publish, distribute and/or sell
* copies of the Software, and permit persons to whom the Software is
* furnished to do so, under the terms of the COPYING file.
*
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
* KIND, either express or implied.
*
* SPDX-License-Identifier: curl
*
***************************************************************************/
#include "first.h"
#if defined(HAVE_GETRLIMIT) && defined(HAVE_SETRLIMIT)
#define T518_SAFETY_MARGIN 16
#define NUM_OPEN (FD_SETSIZE + 10)
#define NUM_NEEDED (NUM_OPEN + T518_SAFETY_MARGIN)
#if defined(_WIN32) || defined(MSDOS)
#define DEV_NULL "NUL"
#else
#define DEV_NULL "/dev/null"
#endif
static int *t518_testfd = NULL;
static struct rlimit t518_num_open;
static char t518_msgbuff[256];
static void t518_store_errmsg(const char *msg, int err)
{
if(!err)
curl_msnprintf(t518_msgbuff, sizeof(t518_msgbuff), "%s", msg);
else {
char errbuf[STRERROR_LEN];
curl_msnprintf(t518_msgbuff, sizeof(t518_msgbuff), "%s, errno %d, %s", msg,
err, curlx_strerror(err, errbuf, sizeof(errbuf)));
}
}
static void t518_close_file_descriptors(void)
{
for(t518_num_open.rlim_cur = 0;
t518_num_open.rlim_cur < t518_num_open.rlim_max;
t518_num_open.rlim_cur++)
if(t518_testfd[t518_num_open.rlim_cur] > 0)
curlx_close(t518_testfd[t518_num_open.rlim_cur]);
curlx_safefree(t518_testfd);
}
static int t518_fopen_works(void)
{
FILE *fpa[3];
int i;
int ret = 1;
for(i = 0; i < 3; i++) {
fpa[i] = NULL;
}
for(i = 0; i < 3; i++) {
fpa[i] = curlx_fopen(DEV_NULL, FOPEN_READTEXT);
if(!fpa[i]) {
t518_store_errmsg("fopen failed", errno);
curl_mfprintf(stderr, "%s\n", t518_msgbuff);
ret = 0;
break;
}
}
for(i = 0; i < 3; i++) {
if(fpa[i])
curlx_fclose(fpa[i]);
}
return ret;
}
static int t518_test_rlimit(int keep_open)
{
rlim_t nitems, i;
int *memchunk = NULL;
struct rlimit rl;
char strbuff[256];
char strbuff1[81];
char strbuff2[81];
/* get initial open file limits */
if(getrlimit(RLIMIT_NOFILE, &rl)) {
t518_store_errmsg("getrlimit() failed", errno);
curl_mfprintf(stderr, "%s\n", t518_msgbuff);
return -1;
}
/* show initial open file limits */
tutil_rlim2str(strbuff, sizeof(strbuff), rl.rlim_cur);
curl_mfprintf(stderr, "initial soft limit: %s\n", strbuff);
tutil_rlim2str(strbuff, sizeof(strbuff), rl.rlim_max);
curl_mfprintf(stderr, "initial hard limit: %s\n", strbuff);
/* show our constants */
curl_mfprintf(stderr, "test518 FD_SETSIZE: %d\n", FD_SETSIZE);
curl_mfprintf(stderr, "test518 NUM_OPEN : %d\n", NUM_OPEN);
curl_mfprintf(stderr, "test518 NUM_NEEDED: %d\n", NUM_NEEDED);
/*
* if soft limit and hard limit are different we ask the
* system to raise soft limit all the way up to the hard
* limit. Due to some other system limit the soft limit
* might not be raised up to the hard limit. So from this
* point the resulting soft limit is our limit. Trying to
* open more than soft limit file descriptors does fail.
*/
if(rl.rlim_cur != rl.rlim_max) {
#ifdef OPEN_MAX
if((rl.rlim_cur > 0) &&
(rl.rlim_cur < OPEN_MAX)) {
curl_mfprintf(stderr, "raising soft limit up to OPEN_MAX\n");
rl.rlim_cur = OPEN_MAX;
if(setrlimit(RLIMIT_NOFILE, &rl)) {
/* on failure do not abort, only issue a warning */
t518_store_errmsg("setrlimit() failed", errno);
curl_mfprintf(stderr, "%s\n", t518_msgbuff);
t518_msgbuff[0] = '\0';
}
}
#endif
curl_mfprintf(stderr, "raising soft limit up to hard limit\n");
rl.rlim_cur = rl.rlim_max;
if(setrlimit(RLIMIT_NOFILE, &rl)) {
/* on failure do not abort, only issue a warning */
t518_store_errmsg("setrlimit() failed", errno);
curl_mfprintf(stderr, "%s\n", t518_msgbuff);
t518_msgbuff[0] = '\0';
}
/* get current open file limits */
if(getrlimit(RLIMIT_NOFILE, &rl)) {
t518_store_errmsg("getrlimit() failed", errno);
curl_mfprintf(stderr, "%s\n", t518_msgbuff);
return -3;
}
/* show current open file limits */
tutil_rlim2str(strbuff, sizeof(strbuff), rl.rlim_cur);
curl_mfprintf(stderr, "current soft limit: %s\n", strbuff);
tutil_rlim2str(strbuff, sizeof(strbuff), rl.rlim_max);
curl_mfprintf(stderr, "current hard limit: %s\n", strbuff);
} /* (rl.rlim_cur != rl.rlim_max) */
/*
* test 518 is all about testing libcurl functionality
* when more than FD_SETSIZE file descriptors are open.
* This means that if for any reason we are not able to
* open more than FD_SETSIZE file descriptors then test
* 518 should not be run.
*/
/*
* verify that soft limit is higher than NUM_NEEDED,
* which is the number of file descriptors we would
* try to open plus T518_SAFETY_MARGIN to not exhaust the
* file descriptor pool
*/
t518_num_open.rlim_cur = NUM_NEEDED;
if((rl.rlim_cur > 0) &&
#ifdef RLIM_INFINITY
(rl.rlim_cur != RLIM_INFINITY) &&
#endif
(rl.rlim_cur <= t518_num_open.rlim_cur)) {
tutil_rlim2str(strbuff2, sizeof(strbuff2), rl.rlim_cur);
tutil_rlim2str(strbuff1, sizeof(strbuff1), t518_num_open.rlim_cur);
curl_msnprintf(strbuff, sizeof(strbuff), "fds needed %s > system limit %s",
strbuff1, strbuff2);
t518_store_errmsg(strbuff, 0);
curl_mfprintf(stderr, "%s\n", t518_msgbuff);
return -4;
}
/*
* reserve a chunk of memory before opening file descriptors to
* avoid a low memory condition once the file descriptors are
* open. System conditions that could make the test fail should
* be addressed in the precheck phase. This chunk of memory shall
* be always curlx_free()ed before exiting the t518_test_rlimit()
* function so that it becomes available to the test.
*/
for(nitems = i = 1; nitems <= i; i *= 2)
nitems = i;
if(nitems > 0x7fff)
nitems = 0x40000;
do {
t518_num_open.rlim_max = sizeof(*memchunk) * nitems;
tutil_rlim2str(strbuff, sizeof(strbuff), t518_num_open.rlim_max);
curl_mfprintf(stderr, "allocating memchunk %s byte array\n", strbuff);
memchunk = curlx_malloc(sizeof(*memchunk) * (size_t)nitems);
if(!memchunk) {
curl_mfprintf(stderr, "memchunk, malloc() failed\n");
nitems /= 2;
}
} while(nitems && !memchunk);
if(!memchunk) {
t518_store_errmsg("memchunk, malloc() failed", errno);
curl_mfprintf(stderr, "%s\n", t518_msgbuff);
return -5;
}
/* initialize it to fight lazy allocation */
curl_mfprintf(stderr, "initializing memchunk array\n");
for(i = 0; i < nitems; i++)
memchunk[i] = -1;
/* set the number of file descriptors we try to open */
t518_num_open.rlim_max = NUM_OPEN;
/* verify that we do not overflow size_t in curlx_malloc() */
if((size_t)(t518_num_open.rlim_max) > ((size_t)-1) / sizeof(*t518_testfd)) {
tutil_rlim2str(strbuff1, sizeof(strbuff1), t518_num_open.rlim_max);
curl_msnprintf(strbuff, sizeof(strbuff),
"unable to allocate an array for %s "
"file descriptors, would overflow size_t", strbuff1);
t518_store_errmsg(strbuff, 0);
curl_mfprintf(stderr, "%s\n", t518_msgbuff);
curlx_free(memchunk);
return -6;
}
/* allocate array for file descriptors */
tutil_rlim2str(strbuff, sizeof(strbuff), t518_num_open.rlim_max);
curl_mfprintf(stderr, "allocating array for %s file descriptors\n", strbuff);
t518_testfd = curlx_malloc(sizeof(*t518_testfd) *
(size_t)(t518_num_open.rlim_max));
if(!t518_testfd) {
t518_store_errmsg("testfd, malloc() failed", errno);
curl_mfprintf(stderr, "%s\n", t518_msgbuff);
curlx_free(memchunk);
return -7;
}
/* initialize it to fight lazy allocation */
curl_mfprintf(stderr, "initializing testfd array\n");
for(t518_num_open.rlim_cur = 0;
t518_num_open.rlim_cur < t518_num_open.rlim_max;
t518_num_open.rlim_cur++)
t518_testfd[t518_num_open.rlim_cur] = -1;
tutil_rlim2str(strbuff, sizeof(strbuff), t518_num_open.rlim_max);
curl_mfprintf(stderr, "trying to open %s file descriptors\n", strbuff);
/* open a dummy descriptor */
t518_testfd[0] = curlx_open(DEV_NULL, O_RDONLY);
if(t518_testfd[0] < 0) {
curl_msnprintf(strbuff, sizeof(strbuff), "opening of %s failed", DEV_NULL);
t518_store_errmsg(strbuff, errno);
curl_mfprintf(stderr, "%s\n", t518_msgbuff);
curlx_safefree(t518_testfd);
curlx_free(memchunk);
return -8;
}
/* create a bunch of file descriptors */
for(t518_num_open.rlim_cur = 1;
t518_num_open.rlim_cur < t518_num_open.rlim_max;
t518_num_open.rlim_cur++) {
t518_testfd[t518_num_open.rlim_cur] = dup(t518_testfd[0]);
if(t518_testfd[t518_num_open.rlim_cur] < 0) {
t518_testfd[t518_num_open.rlim_cur] = -1;
tutil_rlim2str(strbuff1, sizeof(strbuff1), t518_num_open.rlim_cur);
curl_msnprintf(strbuff, sizeof(strbuff), "dup() attempt %s failed",
strbuff1);
curl_mfprintf(stderr, "%s\n", strbuff);
tutil_rlim2str(strbuff1, sizeof(strbuff1), t518_num_open.rlim_cur);
curl_msnprintf(strbuff, sizeof(strbuff),
"fds system limit seems close to %s", strbuff1);
curl_mfprintf(stderr, "%s\n", strbuff);
t518_num_open.rlim_max = NUM_NEEDED;
tutil_rlim2str(strbuff2, sizeof(strbuff2), t518_num_open.rlim_max);
tutil_rlim2str(strbuff1, sizeof(strbuff1), t518_num_open.rlim_cur);
curl_msnprintf(strbuff, sizeof(strbuff),
"fds needed %s > system limit %s", strbuff2, strbuff1);
t518_store_errmsg(strbuff, 0);
curl_mfprintf(stderr, "%s\n", t518_msgbuff);
for(t518_num_open.rlim_cur = 0;
t518_testfd[t518_num_open.rlim_cur] >= 0;
t518_num_open.rlim_cur++)
curlx_close(t518_testfd[t518_num_open.rlim_cur]);
curlx_safefree(t518_testfd);
curlx_free(memchunk);
return -9;
}
}
tutil_rlim2str(strbuff, sizeof(strbuff), t518_num_open.rlim_max);
curl_mfprintf(stderr, "%s file descriptors open\n", strbuff);
#if !defined(HAVE_POLL) && !defined(USE_WINSOCK)
/*
* when using select() instead of poll() we cannot test
* libcurl functionality with a socket number equal or
* greater than FD_SETSIZE. In any case, macro VERIFY_SOCK
* in lib/select.c enforces this check and protects libcurl
* from a possible crash. The effect of this protection
* is that test 518 always fails, since the actual
* call to select() never takes place. We skip test 518
* with an indication that select limit would be exceeded.
*/
t518_num_open.rlim_cur = FD_SETSIZE - T518_SAFETY_MARGIN;
if(t518_num_open.rlim_max > t518_num_open.rlim_cur) {
curl_msnprintf(strbuff, sizeof(strbuff), "select limit is FD_SETSIZE %d",
FD_SETSIZE);
t518_store_errmsg(strbuff, 0);
curl_mfprintf(stderr, "%s\n", t518_msgbuff);
t518_close_file_descriptors();
curlx_free(memchunk);
return -10;
}
t518_num_open.rlim_cur = FD_SETSIZE - T518_SAFETY_MARGIN;
for(rl.rlim_cur = 0;
rl.rlim_cur < t518_num_open.rlim_max;
rl.rlim_cur++) {
if((t518_testfd[rl.rlim_cur] > 0) &&
((unsigned int)t518_testfd[rl.rlim_cur] > t518_num_open.rlim_cur)) {
curl_msnprintf(strbuff, sizeof(strbuff), "select limit is FD_SETSIZE %d",
FD_SETSIZE);
t518_store_errmsg(strbuff, 0);
curl_mfprintf(stderr, "%s\n", t518_msgbuff);
t518_close_file_descriptors();
curlx_free(memchunk);
return -11;
}
}
#endif /* !HAVE_POLL && !USE_WINSOCK */
/*
* Old or 'backwards compatible' implementations of stdio do not allow
* handling of streams with an underlying file descriptor number greater
* than 255, even when allowing high numbered file descriptors for sockets.
* At this point we have a big number of file descriptors which have been
* opened using dup(), so lets test the stdio implementation and discover
* if it is capable of fopen()ing some additional files.
*/
if(!t518_fopen_works()) {
tutil_rlim2str(strbuff1, sizeof(strbuff1), t518_num_open.rlim_max);
curl_msnprintf(strbuff, sizeof(strbuff), "fopen fails with %s fds open",
strbuff1);
curl_mfprintf(stderr, "%s\n", t518_msgbuff);
curl_msnprintf(strbuff, sizeof(strbuff),
"fopen fails with lots of fds open");
t518_store_errmsg(strbuff, 0);
t518_close_file_descriptors();
curlx_free(memchunk);
return -12;
}
/* free the chunk of memory we were reserving so that it
becomes available to the test */
curlx_free(memchunk);
/* close file descriptors unless instructed to keep them */
if(!keep_open) {
t518_close_file_descriptors();
}
return 0;
}
static CURLcode test_lib518(const char *URL)
{
CURLcode result;
CURL *curl;
if(!strcmp(URL, "check")) {
/* used by the test script to ask if we can run this test or not */
if(t518_test_rlimit(FALSE)) {
curl_mfprintf(stdout, "test_rlimit problem: %s\n", t518_msgbuff);
return TEST_ERR_FAILURE;
}
return CURLE_OK; /* sure, run this! */
}
if(t518_test_rlimit(TRUE)) {
/* failure */
return TEST_ERR_MAJOR_BAD;
}
/* run the test with the bunch of open file descriptors
and close them all once the test is over */
if(curl_global_init(CURL_GLOBAL_ALL) != CURLE_OK) {
curl_mfprintf(stderr, "curl_global_init() failed\n");
t518_close_file_descriptors();
return TEST_ERR_MAJOR_BAD;
}
curl = curl_easy_init();
if(!curl) {
curl_mfprintf(stderr, "curl_easy_init() failed\n");
t518_close_file_descriptors();
curl_global_cleanup();
return TEST_ERR_MAJOR_BAD;
}
easy_setopt(curl, CURLOPT_URL, URL);
easy_setopt(curl, CURLOPT_HEADER, 1L);
result = curl_easy_perform(curl);
test_cleanup:
t518_close_file_descriptors();
curl_easy_cleanup(curl);
curl_global_cleanup();
return result;
}
#else /* HAVE_GETRLIMIT && HAVE_SETRLIMIT */
static CURLcode test_lib518(const char *URL)
{
(void)URL;
curl_mprintf("system lacks necessary system function(s)");
return TEST_ERR_MAJOR_BAD; /* skip test */
}
#endif /* HAVE_GETRLIMIT && HAVE_SETRLIMIT */