Files
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

336 lines
14 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 "unitcheck.h"
#if defined(USE_GNUTLS) || defined(USE_SCHANNEL) || defined(USE_MBEDTLS) || \
defined(USE_RUSTLS)
#include "vtls/x509asn1.h"
#include "vtls/vtls.h"
struct test_1667 {
unsigned char tag;
const char *asn1;
const size_t size;
const char *out;
CURLcode result_exp;
};
static bool test1667(const struct test_1667 *spec, size_t i,
struct dynbuf *dbuf)
{
CURLcode result;
bool ok = TRUE;
struct Curl_asn1Element elem;
curlx_dyn_reset(dbuf);
/* setup private struct for this invoke */
elem.tag = spec->tag;
elem.header = NULL;
elem.beg = spec->asn1;
elem.end = elem.beg + spec->size;
elem.eclass = 0;
elem.constructed = 0;
result = ASN1tostr(dbuf, &elem);
if(result != spec->result_exp) {
curl_mfprintf(stderr, "test %zu (type %u): expect result %d, got %d\n",
i, spec->tag, (int)spec->result_exp, (int)result);
if(!spec->result_exp) {
curl_mfprintf(stderr, "test %zu: expected output '%s'\n",
i, spec->out);
}
ok = FALSE;
}
else if(!result) {
/* use strlen on the pointer instead of curlx_dyn_len() because for some
of these type, the code explicitly adds a null-terminator which is then
counted as buffer size. */
size_t actual_len = strlen(curlx_dyn_ptr(dbuf));
if(strlen(spec->out) != actual_len) {
curl_mfprintf(stderr,
"test %zu (type %u): "
"unexpected length. Got %zu, expected %zu)\n",
i, spec->tag, actual_len, strlen(spec->out));
ok = FALSE;
}
if(strcmp(spec->out, curlx_dyn_ptr(dbuf))) {
size_t loop;
const uint8_t *data = curlx_dyn_uptr(dbuf);
const size_t len = curlx_dyn_len(dbuf);
curl_mfprintf(stderr,
"test %zu (type %u): "
"expected output '%s', got '%s' (length %zu)\n",
i, spec->tag, spec->out, data, len);
for(loop = 0; loop < len; loop++)
curl_mfprintf(stderr, "test %zu: index %zu byte %02x\n",
i, loop, data[loop]);
ok = FALSE;
}
}
return ok;
}
static CURLcode test_unit1667(const char *arg)
{
UNITTEST_BEGIN_SIMPLE
static const struct test_1667 test_specs[] = {
/* unsupported type > CURL_ASN1_BMP_STRING (30) */
{ 31, "abcde", 5, "", CURLE_BAD_FUNCTION_ARGUMENT },
{ 99, "abcde", 5, "", CURLE_BAD_FUNCTION_ARGUMENT },
/*
(many different) strings:
CURL_ASN1_UTF8_STRING
CURL_ASN1_NUMERIC_STRING
CURL_ASN1_PRINTABLE_STRING
CURL_ASN1_TELETEX_STRING
CURL_ASN1_IA5_STRING
CURL_ASN1_VISIBLE_STRING
CURL_ASN1_UNIVERSAL_STRING
CURL_ASN1_BMP_STRING
*/
{ CURL_ASN1_UTF8_STRING, "abcde", 5, "abcde", CURLE_OK },
/* a with ring, a with umlaut, o with umlaut in UTF-8 encoding */
{ CURL_ASN1_UTF8_STRING, "\xc3\xa5\xc3\xa4\xc3\xb6", 6,
"\xc3\xa5\xc3\xa4\xc3\xb6", CURLE_OK },
{ CURL_ASN1_NUMERIC_STRING, "abcde", 5, "abcde", CURLE_OK },
{ CURL_ASN1_PRINTABLE_STRING, "abcde", 5, "abcde", CURLE_OK },
{ CURL_ASN1_TELETEX_STRING, "abcde", 5, "abcde", CURLE_OK },
{ CURL_ASN1_IA5_STRING, "abcde", 5, "abcde", CURLE_OK },
{ CURL_ASN1_VISIBLE_STRING, "abcde", 5, "abcde", CURLE_OK },
{ CURL_ASN1_UNIVERSAL_STRING, "abcde", 5, "abcde",
CURLE_BAD_FUNCTION_ARGUMENT },
{ CURL_ASN1_UNIVERSAL_STRING, "abcd", 4, "abcd",
CURLE_WEIRD_SERVER_REPLY },
{ CURL_ASN1_UNIVERSAL_STRING, "\x00\x00\x12\x34", 4, "\xe1\x88\xb4",
CURLE_OK },
{ CURL_ASN1_BMP_STRING, "abcde", 5, "abcde", CURLE_BAD_FUNCTION_ARGUMENT },
/* Generalized Time */
/* GTime2str() is tested separately in unit test 1656 */
{ CURL_ASN1_GENERALIZED_TIME, "19851106210627.3", 16,
"1985-11-06 21:06:27.3", CURLE_OK },
{ CURL_ASN1_GENERALIZED_TIME, "19851106210627.3Z", 17,
"1985-11-06 21:06:27.3 GMT", CURLE_OK },
{ CURL_ASN1_GENERALIZED_TIME, "19851106210627.3-0500", 21,
"1985-11-06 21:06:27.3 UTC-0500", CURLE_OK },
/* Generalized Time: Invalid month (13). Still fine! */
{ CURL_ASN1_GENERALIZED_TIME, "20231301000000Z", 15,
"2023-13-01 00:00:00 GMT", CURLE_OK },
/* Generalized Time: Valid millisecond precision */
{ CURL_ASN1_GENERALIZED_TIME, "20230101000000.123Z", 19,
"2023-01-01 00:00:00.123 GMT", CURLE_OK },
/* UTC Time */
{ CURL_ASN1_UTC_TIME, "991231235959Z", 13, "1999-12-31 23:59:59 GMT",
CURLE_OK },
{ CURL_ASN1_UTC_TIME, "991231235959+0200", 17, "1999-12-31 23:59:59 +0200",
CURLE_OK },
{ CURL_ASN1_UTC_TIME, "991231235959-0200", 17, "1999-12-31 23:59:59 -0200",
CURLE_OK },
{ CURL_ASN1_UTC_TIME, "991231235959+999", 16, "1999-12-31 23:59:59 +999",
CURLE_OK },
/* Leap year check (Feb 29, 2024) */
{ CURL_ASN1_UTC_TIME, "240229120000Z", 13, "2024-02-29 12:00:00 GMT",
CURLE_OK },
/* Century roll-over (00-49 is 20xx, 50-99 is 19xx) */
{ CURL_ASN1_UTC_TIME, "491231235959Z", 13, "2049-12-31 23:59:59 GMT",
CURLE_OK },
{ CURL_ASN1_UTC_TIME, "500101000000Z", 13, "1950-01-01 00:00:00 GMT",
CURLE_OK },
/* Object Identifier (encodeOID() is tested in unit test 1666) */
/* 1.2.840.10040.4.1 */
{ CURL_ASN1_OBJECT_IDENTIFIER, "\x2A\x86\x48\xCE\x38\x04\x01", 7,
"dsa", CURLE_OK },
/* 1.2.840.113549.1.1.13 */
{ CURL_ASN1_OBJECT_IDENTIFIER, "\x2A\x86\x48\x86\xF7\x0D\x01\x01\x0D", 9,
"sha512WithRSAEncryption", CURLE_OK },
/* 1.2.840.10040.4.2 */
{ CURL_ASN1_OBJECT_IDENTIFIER, "\x2A\x86\x48\xCE\x38\x04\x02", 7,
"1.2.840.10040.4.2", CURLE_OK },
/* 2.16.840.1.101.3.4.2.3 */
{ CURL_ASN1_OBJECT_IDENTIFIER, "\x60\x86\x48\x01\x65\x03\x04\x02\x03", 9,
"sha512", CURLE_OK },
/* 2.999 -> (2*40) + 999 = 1079. 1079 in VLQ is 0x88 0x37 */
{ CURL_ASN1_OBJECT_IDENTIFIER, "\x88\x37\x03", 3, "2.999.3",
CURLE_OK },
/* 1.0 (Minimum possible OID length/value)
(1*40) + 0 = 40 (0x28) */
{ CURL_ASN1_OBJECT_IDENTIFIER, "\x28", 1, "1.0", CURLE_OK },
/* Malformed (Incomplete multi-byte SID, MSB set but no following byte) */
{ CURL_ASN1_OBJECT_IDENTIFIER, "\x88", 1, "",
CURLE_BAD_FUNCTION_ARGUMENT },
/* NULL */
{ CURL_ASN1_NULL, "", 0, "", CURLE_OK },
{ CURL_ASN1_NULL, "a", 1, "", CURLE_OK },
{ CURL_ASN1_NULL, "aa", 2, "", CURLE_OK },
/* Octet string */
{ CURL_ASN1_OCTET_STRING, "\x00\x00\x00\x04\x05", 5, "00:00:00:04:05:",
CURLE_OK },
{ CURL_ASN1_OCTET_STRING, "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff"
"\xff\xff", 14, "", CURLE_TOO_LARGE},
/* Bit string. The first byte is number of unused bits. */
{ CURL_ASN1_BIT_STRING, "\x00\x55", 2, "55:", CURLE_OK },
/* Invalid number of unused bits (> 7) */
{ CURL_ASN1_BIT_STRING, "\x08\x55", 2, "", CURLE_BAD_FUNCTION_ARGUMENT },
{ CURL_ASN1_BIT_STRING, "\x00\xaa\x55", 3, "aa:55:", CURLE_OK },
{ CURL_ASN1_BIT_STRING, "\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff"
"\xff\xff", 15, "", CURLE_TOO_LARGE},
/* 3 unused bits, data 0xF0 (11110000)
The '0' at the end of 11110... are the unused bits. */
{ CURL_ASN1_BIT_STRING, "\x03\xf0", 2, "f0:", CURLE_OK },
/* Integer */
{ CURL_ASN1_INTEGER, "\x00", 1, "0", CURLE_OK },
{ CURL_ASN1_INTEGER, "\x01", 1, "1", CURLE_OK },
{ CURL_ASN1_INTEGER, "\x09", 1, "9", CURLE_OK },
{ CURL_ASN1_INTEGER, "\x0a", 1, "10", CURLE_OK },
{ CURL_ASN1_INTEGER, "\xb", 1, "11", CURLE_OK },
{ CURL_ASN1_INTEGER, "\x11", 1, "17", CURLE_OK },
{ CURL_ASN1_INTEGER, "\x7f", 1, "127", CURLE_OK },
{ CURL_ASN1_INTEGER, "\x80", 1, "-128", CURLE_OK },
{ CURL_ASN1_INTEGER, "\x81", 1, "-127", CURLE_OK },
{ CURL_ASN1_INTEGER, "\x82", 1, "-126", CURLE_OK },
{ CURL_ASN1_INTEGER, "\xff", 1, "-1", CURLE_OK },
{ CURL_ASN1_INTEGER, "\x27\x0f", 2, "9999", CURLE_OK },
{ CURL_ASN1_INTEGER, "\x27\x10", 2, "0x2710", CURLE_OK },
{ CURL_ASN1_INTEGER, "\x7f\x81", 2, "0x7f81", CURLE_OK },
/* sign-extended, would be -32385 in decimal */
{ CURL_ASN1_INTEGER, "\x81\x7f", 2, "0xffff817f", CURLE_OK },
{ CURL_ASN1_INTEGER, "\xff\xff", 2, "-1", CURLE_OK },
{ CURL_ASN1_INTEGER, "\x00\x00", 2, "0", CURLE_OK },
{ CURL_ASN1_INTEGER, "\x01\x02\x03", 3, "0x10203", CURLE_OK },
{ CURL_ASN1_INTEGER, "\xff\x02\xff", 3, "0xffff02ff", CURLE_OK },
{ CURL_ASN1_INTEGER, "\x00\xff\x00", 3, "0xff00", CURLE_OK },
{ CURL_ASN1_INTEGER, "\x01\x02\x03\x04", 4, "0x1020304", CURLE_OK },
{ CURL_ASN1_INTEGER, "\xff\x02\x03\x04", 4, "0xff020304", CURLE_OK },
{ CURL_ASN1_INTEGER, "\x00\x00\x00\x04", 4, "4", CURLE_OK },
{ CURL_ASN1_INTEGER, "\x00\x00\x00\x04\x05", 5, "00:00:00:04:05:",
CURLE_OK },
{ CURL_ASN1_INTEGER, "\xff\x00\x00\x04\x05", 5, "ff:00:00:04:05:",
CURLE_OK },
{ CURL_ASN1_INTEGER, "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff",
11, "ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:", CURLE_OK },
{ CURL_ASN1_INTEGER, "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff"
"\xff", 12, "ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:", CURLE_OK },
{ CURL_ASN1_INTEGER, "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff"
"\xff\xff", 14, "", CURLE_TOO_LARGE},
{ CURL_ASN1_INTEGER, "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff"
"\xff\xff\xff", 15, "", CURLE_TOO_LARGE },
{ CURL_ASN1_INTEGER, "", 0, "", CURLE_BAD_FUNCTION_ARGUMENT },
/* Leading zero required if the MSB of the next byte is 1 (to keep it
positive) */
{ CURL_ASN1_INTEGER, "\x00\x80", 2, "128", CURLE_OK },
/* Enumerated works the same as Integer */
{ CURL_ASN1_ENUMERATED, "\x00", 1, "0", CURLE_OK },
{ CURL_ASN1_ENUMERATED, "\x01", 1, "1", CURLE_OK },
{ CURL_ASN1_ENUMERATED, "\x09", 1, "9", CURLE_OK },
{ CURL_ASN1_ENUMERATED, "\x0a", 1, "10", CURLE_OK },
{ CURL_ASN1_ENUMERATED, "\xb", 1, "11", CURLE_OK },
{ CURL_ASN1_ENUMERATED, "\x11", 1, "17", CURLE_OK },
{ CURL_ASN1_ENUMERATED, "\x7f", 1, "127", CURLE_OK },
{ CURL_ASN1_ENUMERATED, "\x80", 1, "-128", CURLE_OK },
{ CURL_ASN1_ENUMERATED, "\xff", 1, "-1", CURLE_OK },
{ CURL_ASN1_ENUMERATED, "\x7f\x81", 2, "0x7f81", CURLE_OK },
{ CURL_ASN1_ENUMERATED, "\xff\xff", 2, "-1", CURLE_OK },
{ CURL_ASN1_ENUMERATED, "\x00\x00", 2, "0", CURLE_OK },
{ CURL_ASN1_ENUMERATED, "\x01\x02\x03", 3, "0x10203", CURLE_OK },
{ CURL_ASN1_ENUMERATED, "\xff\x02\xff", 3, "0xffff02ff", CURLE_OK },
{ CURL_ASN1_ENUMERATED, "\x00\xff\x00", 3, "0xff00", CURLE_OK },
{ CURL_ASN1_ENUMERATED, "\x01\x02\x03\x04", 4, "0x1020304", CURLE_OK },
{ CURL_ASN1_ENUMERATED, "\xff\x02\x03\x04", 4, "0xff020304", CURLE_OK },
{ CURL_ASN1_ENUMERATED, "\x00\x00\x00\x04", 4, "4", CURLE_OK },
{ CURL_ASN1_ENUMERATED, "\x00\x00\x00\x04\x05", 5, "00:00:00:04:05:",
CURLE_OK },
{ CURL_ASN1_ENUMERATED, "\xff\x00\x00\x04\x05", 5, "ff:00:00:04:05:",
CURLE_OK },
{ CURL_ASN1_ENUMERATED, "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff",
11, "ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:", CURLE_OK },
{ CURL_ASN1_ENUMERATED, "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff"
"\xff", 12, "ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:", CURLE_OK },
{ CURL_ASN1_ENUMERATED, "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff"
"\xff\xff", 14, "", CURLE_TOO_LARGE},
{ CURL_ASN1_ENUMERATED, "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff"
"\xff\xff\xff", 15, "", CURLE_TOO_LARGE },
{ CURL_ASN1_ENUMERATED, "", 0, "", CURLE_BAD_FUNCTION_ARGUMENT },
/* Boolean */
{ CURL_ASN1_BOOLEAN, "\xff", 1, "TRUE", CURLE_OK },
{ CURL_ASN1_BOOLEAN, "\x01", 1, "", CURLE_BAD_FUNCTION_ARGUMENT },
{ CURL_ASN1_BOOLEAN, "\x02", 1, "", CURLE_BAD_FUNCTION_ARGUMENT },
{ CURL_ASN1_BOOLEAN, "\x00", 1, "FALSE", CURLE_OK },
{ CURL_ASN1_BOOLEAN, "\x01\x01", 2, "", CURLE_BAD_FUNCTION_ARGUMENT },
{ CURL_ASN1_BOOLEAN, "\x00\x01", 2, "", CURLE_BAD_FUNCTION_ARGUMENT },
{ CURL_ASN1_BOOLEAN, "", 0, "", CURLE_BAD_FUNCTION_ARGUMENT },
};
size_t i;
struct dynbuf dbuf;
bool all_ok = TRUE;
if(curl_global_init(CURL_GLOBAL_ALL) != CURLE_OK) {
curl_mfprintf(stderr, "curl_global_init() failed\n");
return TEST_ERR_MAJOR_BAD;
}
/* the real code uses CURL_X509_STR_MAX for maximum size, but we set a
smaller one here so that we can test running into the limit a little
easier */
curlx_dyn_init(&dbuf, 40);
for(i = 0; i < CURL_ARRAYSIZE(test_specs); ++i) {
if(!test1667(&test_specs[i], i, &dbuf))
all_ok = FALSE;
}
fail_unless(all_ok, "some tests of ASN1tostr() failed");
curlx_dyn_free(&dbuf);
curl_global_cleanup();
UNITTEST_END_SIMPLE
}
#else
static CURLcode test_unit1667(const char *arg)
{
UNITTEST_BEGIN_SIMPLE
puts("not tested since ASN1tostr() is not built in");
UNITTEST_END_SIMPLE
}
#endif