Enable HTTPS on Linux, through naett's libcurl backend

naett has had a complete libcurl backend all along; we just never built it,
so Linux ran with HTTPS_NOT_AVAILABLE. That means no homebrew store over
HTTPS, and RetroAchievements talking to plain http://retroachievements.org.

libcurl is loaded with dlopen rather than linked, the same way we handle the
Vulkan loader, so it stays a soft dependency: we need the curl headers at
build time, but a build made here still starts on a machine without libcurl
installed - it just reports HTTPS as unavailable, exactly like today. Distro
packagers get the behavior they'd expect either way, and certificate
validation comes free from the system CA store.

New net::HTTPSAvailable() answers "did that work", and SDLMain folds it into
SYSPROP_SUPPORTS_HTTPS, which everything downstream already degrades on.

Four fixes to the backend itself, all noted in ext/naett/README-ppsspp.md:

- panic() called exit(1) on a pipe or curl_multi_perform failure. Taking the
  emulator down because a download failed isn't acceptable - the backend now
  disables itself and requests complete with naettGenericError.
- CURLINFO_RESPONSE_CODE writes a long into res->code, which is an int. Eight
  bytes into four, getting away with it only because the next field absorbs
  the zeroes.
- curl_easy_setopt is varargs and wants a long for these options; int literals
  and int variables are UB on LP64.
- naettPlatformCloseResponse called through a null function pointer when
  libcurl was missing. Found by testing that path, which segfaulted.

CI needs libcurl4-openssl-dev (curl-dev on Alpine) or it would quietly keep
building without HTTPS.
This commit is contained in:
Henrik Rydgård
2026-09-02 17:42:04 +02:00
parent 2096df52f5
commit a0873e5b0e
10 changed files with 268 additions and 23 deletions
+3 -3
View File
@@ -321,7 +321,7 @@ jobs:
if: runner.os == 'Linux' && matrix.extra != 'android' && matrix.extra != 'loongarch64'
run: |
sudo apt-get update -y -qq
sudo apt-get install libsdl3-dev libgl1-mesa-dev libglu1-mesa-dev libsdl3-ttf-dev libfontconfig1-dev
sudo apt-get install libsdl3-dev libgl1-mesa-dev libglu1-mesa-dev libsdl3-ttf-dev libfontconfig1-dev libcurl4-openssl-dev
- name: Install macOS SDL3 dependencies
if: runner.os == 'macOS' && matrix.id == 'macos'
@@ -475,7 +475,7 @@ jobs:
if: runner.os == 'Linux'
run: |
sudo apt-get update -y -qq
sudo apt-get install libsdl3-dev libgl1-mesa-dev libglu1-mesa-dev libsdl3-ttf-dev libfontconfig1-dev
sudo apt-get install libsdl3-dev libgl1-mesa-dev libglu1-mesa-dev libsdl3-ttf-dev libfontconfig1-dev libcurl4-openssl-dev
- name: Install macOS dependencies
if: runner.os == 'macOS'
@@ -524,7 +524,7 @@ jobs:
steps:
- name: Install Linux dependencies (Alpine)
run: |
apk add build-base wget git bash cmake python3 glu-dev sdl3-dev sdl3_ttf-dev
apk add build-base wget git bash cmake python3 glu-dev sdl3-dev sdl3_ttf-dev curl-dev
- uses: actions/checkout@v7
with:
+14 -1
View File
@@ -271,7 +271,20 @@ if(ANDROID)
set(USING_GLES2 ON)
endif()
if(NOT ANDROID AND NOT WIN32 AND (NOT APPLE OR IOS))
# naett wraps a native HTTPS implementation per platform: WinHTTP on Windows, the Java
# APIs on Android, NSURLSession on macOS, and libcurl elsewhere.
if(WIN32 OR ANDROID OR (APPLE AND NOT IOS))
# Nothing to do, these need no external library.
elseif(UNIX AND NOT APPLE AND NOT LIBRETRO)
# We only need the curl headers at build time - libcurl itself is loaded with dlopen
# (see ext/naett-lib/src/naett_curl.h), so it stays optional at runtime and a build made
# here still starts on a machine without it, just without HTTPS.
find_package(CURL)
if(NOT CURL_FOUND)
message(STATUS "libcurl headers not found - building without HTTPS support")
set(HTTPS_NOT_AVAILABLE ON)
endif()
else()
set(HTTPS_NOT_AVAILABLE ON)
endif()
+19 -1
View File
@@ -16,6 +16,12 @@
#ifndef HTTPS_NOT_AVAILABLE
#include "ext/naett-lib/naett.h"
// Note: PPSSPP_PLATFORM(LINUX) is also set on Android, which needs no loader.
#if PPSSPP_PLATFORM(LINUX) && !PPSSPP_PLATFORM(ANDROID)
// On Linux, naett goes through libcurl, which we load at runtime - so HTTPS support is
// only known once we've tried.
#include "ext/naett-lib/src/naett_curl.h"
#endif
#endif
#if PPSSPP_PLATFORM(ANDROID)
@@ -44,13 +50,25 @@ void Init() {
_assert_(gJvm != nullptr);
naettInit(gJvm);
#else
naettInit(NULL);
if (HTTPSAvailable()) {
naettInit(NULL);
}
#endif
#endif
g_naettInitialized = true;
}
}
bool HTTPSAvailable() {
#ifdef HTTPS_NOT_AVAILABLE
return false;
#elif PPSSPP_PLATFORM(LINUX) && !PPSSPP_PLATFORM(ANDROID)
return naettCurlLoad() != 0;
#else
return true;
#endif
}
void Shutdown() {
#ifdef _WIN32
if (g_wsaInitialized) {
+5
View File
@@ -12,6 +12,11 @@ namespace net {
void Init();
void Shutdown();
// Whether we have a working HTTPS backend. False if PPSSPP was built without one, and on
// Linux also if libcurl couldn't be loaded at runtime. Safe to call before Init().
// Platform ports should factor this into SYSPROP_SUPPORTS_HTTPS.
bool HTTPSAvailable();
enum class DNSType {
ANY = 0,
IPV4 = 1,
+2 -1
View File
@@ -1193,7 +1193,8 @@ bool System_GetPropertyBool(SystemProperty prop) {
return true; // FileUtil.cpp: OpenFileInEditor
#ifndef HTTPS_NOT_AVAILABLE
case SYSPROP_SUPPORTS_HTTPS:
return !g_Config.bDisableHTTPS;
// On Linux this also depends on whether libcurl could be loaded.
return !g_Config.bDisableHTTPS && net::HTTPSAvailable();
#endif
case SYSPROP_HAS_FOLDER_BROWSER:
case SYSPROP_HAS_FILE_BROWSER:
+5 -1
View File
@@ -14,10 +14,14 @@ elseif(ANDROID)
elseif(APPLE)
list(APPEND ALL_SOURCE_FILES ${SRC_DIR}/naett_osx.c)
elseif(UNIX)
list(APPEND ALL_SOURCE_FILES ${SRC_DIR}/naett_linux.c)
list(APPEND ALL_SOURCE_FILES ${SRC_DIR}/naett_linux.c ${SRC_DIR}/naett_curl.c)
endif()
add_library(naett STATIC ${ALL_SOURCE_FILES})
if(WIN32)
target_link_libraries(naett winhttp)
elseif(UNIX AND NOT APPLE AND NOT ANDROID)
# Headers only - libcurl itself is dlopen'd, see ../naett/src/naett_curl.h.
target_include_directories(naett PRIVATE ${CURL_INCLUDE_DIRS})
target_link_libraries(naett ${CMAKE_DL_LIBS})
endif()
+12
View File
@@ -21,3 +21,15 @@ Keep this list up to date - it's what makes it possible to move to a newer upstr
`exit`/`calloc`/`realloc`/`free`/`fprintf` but never included either header - in the amalgam it
got them from `naett_core.c` further up the concatenation. Building it on its own is an error
with a modern compiler.
- `src/naett_curl.h` / `src/naett_curl.c`: new, ours. Loads libcurl with `dlopen` instead of
linking against it, so libcurl stays a soft dependency at runtime. `naett_linux.c` includes
that header in place of `<curl/curl.h>`.
- `src/naett_linux.c`: replaced the `panic()` that called `exit(1)` on a pipe or
`curl_multi_perform` failure. Taking the whole emulator down because a download failed isn't
acceptable, so the backend now disables itself and requests complete with `naettGenericError`.
- `src/naett_linux.c`: `CURLINFO_RESPONSE_CODE` writes a `long`, and `res->code` is an `int` -
upstream passed `&res->code` straight to `curl_easy_getinfo`, writing 8 bytes into 4. Reads
into a `long` local now.
- `src/naett_linux.c`: `curl_easy_setopt` is varargs and takes a `long` for these options;
upstream passed `int` literals and `int` variables, which is UB on LP64 (and what curl's own
typecheck macros warn about). They're `1L`/`(long)` now.
+78
View File
@@ -0,0 +1,78 @@
// PPSSPP addition - not part of upstream naett. See naett_curl.h.
#if __linux__ && !__ANDROID__
#define NAETT_CURL_INTERNAL
#include "naett_curl.h"
#include <dlfcn.h>
#include <stdio.h>
NaettCurl g_curl;
// libcurl's SONAME has been libcurl.so.4 since 2007. The gnutls and nss flavors are
// separate sonames on Debian-likes and old Fedora respectively, and are ABI compatible.
// libcurl.so last, since that one only exists if the -dev package is installed.
static const char *g_curlLibNames[] = {
"libcurl.so.4",
"libcurl-gnutls.so.4",
"libcurl-nss.so.4",
"libcurl.so",
};
static int LoadSymbols(void *lib) {
// Every one of these has existed since libcurl 7.28 (2012), so a partial load means
// something is badly wrong rather than "the host libcurl is a bit old" - bail out
// entirely rather than crash later on a null pointer.
#define LOAD(field, name) \
g_curl.field = (__typeof__(g_curl.field))dlsym(lib, name); \
if (!g_curl.field) { \
fprintf(stderr, "naett: libcurl is missing %s\n", name); \
return 0; \
}
LOAD(global_init, "curl_global_init")
LOAD(easy_init, "curl_easy_init")
LOAD(easy_setopt, "curl_easy_setopt")
LOAD(easy_getinfo, "curl_easy_getinfo")
LOAD(easy_cleanup, "curl_easy_cleanup")
LOAD(multi_init, "curl_multi_init")
LOAD(multi_add_handle, "curl_multi_add_handle")
LOAD(multi_perform, "curl_multi_perform")
LOAD(multi_info_read, "curl_multi_info_read")
LOAD(multi_wait, "curl_multi_wait")
LOAD(slist_append, "curl_slist_append")
LOAD(slist_free_all, "curl_slist_free_all")
#undef LOAD
return 1;
}
int naettCurlLoad(void) {
static int loaded = -1;
if (loaded >= 0) {
return loaded;
}
loaded = 0;
for (size_t i = 0; i < sizeof(g_curlLibNames) / sizeof(g_curlLibNames[0]); i++) {
// RTLD_GLOBAL so that libcurl's own dependencies resolve normally.
void *lib = dlopen(g_curlLibNames[i], RTLD_NOW | RTLD_GLOBAL);
if (!lib) {
continue;
}
if (LoadSymbols(lib)) {
loaded = 1;
} else {
dlclose(lib);
}
break;
}
if (!loaded) {
fprintf(stderr, "naett: libcurl not found, HTTPS will be unavailable\n");
}
return loaded;
}
#endif
+76
View File
@@ -0,0 +1,76 @@
// PPSSPP addition - not part of upstream naett.
//
// Loads libcurl at runtime instead of linking against it, so that libcurl stays a soft
// dependency: a build made on a machine with the curl headers still starts on a machine
// without libcurl installed, just with HTTPS reported as unavailable. Same idea as
// Common/GPU/Vulkan/VulkanLoader.cpp.
//
// The curl headers are still needed at build time, for the types and the option enums.
// Only naett_linux.c and naett_curl.c want the redirect macros, so those are behind
// NAETT_CURL_INTERNAL; everyone else gets just the loader entry point and doesn't pull
// <curl/curl.h> into their translation unit.
#ifndef NAETT_CURL_H
#define NAETT_CURL_H
#ifdef __cplusplus
extern "C" {
#endif
// Loads libcurl if it isn't loaded yet, and returns 1 if it's available.
// The result is cached, so calling this repeatedly is cheap. Not thread safe - call it
// once during startup before any other thread can reach it (PPSSPP does so in net::Init).
int naettCurlLoad(void);
#ifdef __cplusplus
}
#endif
#ifdef NAETT_CURL_INTERNAL
#include <curl/curl.h>
// curl.h defines these as typechecking macros when built with GCC/Clang. We need the
// plain names to redirect, and we lose the typechecking, which is fine - the calls below
// are checked at build time in exactly the same way for anyone building against a normal
// libcurl.
#undef curl_easy_setopt
#undef curl_easy_getinfo
typedef struct {
CURLcode (*global_init)(long flags);
CURL *(*easy_init)(void);
CURLcode (*easy_setopt)(CURL *handle, CURLoption option, ...);
CURLcode (*easy_getinfo)(CURL *handle, CURLINFO info, ...);
void (*easy_cleanup)(CURL *handle);
CURLM *(*multi_init)(void);
CURLMcode (*multi_add_handle)(CURLM *multi, CURL *easy);
CURLMcode (*multi_perform)(CURLM *multi, int *runningHandles);
CURLMsg *(*multi_info_read)(CURLM *multi, int *msgsInQueue);
CURLMcode (*multi_wait)(CURLM *multi, struct curl_waitfd extraFDs[], unsigned int extraNFDs, int timeoutMS,
int *numFDs);
struct curl_slist *(*slist_append)(struct curl_slist *list, const char *data);
void (*slist_free_all)(struct curl_slist *list);
} NaettCurl;
extern NaettCurl g_curl;
#define curl_global_init g_curl.global_init
#define curl_easy_init g_curl.easy_init
#define curl_easy_setopt g_curl.easy_setopt
#define curl_easy_getinfo g_curl.easy_getinfo
#define curl_easy_cleanup g_curl.easy_cleanup
#define curl_multi_init g_curl.multi_init
#define curl_multi_add_handle g_curl.multi_add_handle
#define curl_multi_perform g_curl.multi_perform
#define curl_multi_info_read g_curl.multi_info_read
#define curl_multi_wait g_curl.multi_wait
#define curl_slist_append g_curl.slist_append
#define curl_slist_free_all g_curl.slist_free_all
#endif // NAETT_CURL_INTERNAL
#endif // NAETT_CURL_H
+54 -16
View File
@@ -2,7 +2,11 @@
#if __linux__ && !__ANDROID__
#include <curl/curl.h>
// PPSSPP: libcurl is dlopen'd rather than linked, see naett_curl.h. This header stands in
// for <curl/curl.h> and redirects the curl_* calls below through function pointers.
#define NAETT_CURL_INTERNAL
#include "naett_curl.h"
#include <assert.h>
#include <pthread.h>
#include <unistd.h>
@@ -15,9 +19,14 @@ static pthread_t workerThread;
static int handleReadFD = 0;
static int handleWriteFD = 0;
static void panic(const char* message) {
fprintf(stderr, "%s\n", message);
exit(1);
// PPSSPP: upstream had a panic() here that called exit(1). Taking the whole application
// down because a download failed isn't acceptable for us, so failures now leave the
// backend disabled and requests complete with naettGenericError instead.
static int workerRunning = 0;
static void fail(const char* message) {
fprintf(stderr, "naett: %s\n", message);
workerRunning = 0;
}
static void* curlWorker(void* data) {
@@ -34,10 +43,11 @@ static void* curlWorker(void* data) {
int newHandlePos = 0;
while (1) {
while (workerRunning) {
int status = curl_multi_perform(mc, &activeHandles);
if (status != CURLM_OK) {
panic("CURL processing failure");
fail("curl_multi_perform failed, shutting down the HTTP worker");
break;
}
struct CURLMsg* message = curl_multi_info_read(mc, &messagesLeft);
@@ -45,7 +55,12 @@ static void* curlWorker(void* data) {
CURL* handle = message->easy_handle;
InternalResponse* res = NULL;
curl_easy_getinfo(handle, CURLINFO_PRIVATE, (char**)&res);
curl_easy_getinfo(handle, CURLINFO_RESPONSE_CODE, &res->code);
// PPSSPP: CURLINFO_RESPONSE_CODE writes a long, and res->code is an int -
// upstream passed &res->code directly, which writes 8 bytes into 4 and only
// gets away with it because the next field happens to absorb the zeroes.
long responseCode = 0;
curl_easy_getinfo(handle, CURLINFO_RESPONSE_CODE, &responseCode);
res->code = (int)responseCode;
res->complete = 1;
curl_easy_cleanup(handle);
}
@@ -72,11 +87,19 @@ static void* curlWorker(void* data) {
}
void naettPlatformInit(naettInitData initData) {
if (!naettCurlLoad()) {
return;
}
curl_global_init(CURL_GLOBAL_ALL);
CURLM* mc = curl_multi_init();
if (!mc) {
fail("curl_multi_init failed");
return;
}
int fds[2];
if (pipe(fds) != 0) {
panic("Failed to open pipe");
fail("failed to open pipe");
return;
}
handleReadFD = fds[0];
handleWriteFD = fds[1];
@@ -87,7 +110,10 @@ void naettPlatformInit(naettInitData initData) {
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_create(&workerThread, &attr, curlWorker, mc);
workerRunning = 1;
if (pthread_create(&workerThread, &attr, curlWorker, mc) != 0) {
fail("failed to start the HTTP worker thread");
}
}
int naettPlatformInitRequest(InternalRequest* req) {
@@ -121,19 +147,19 @@ static void setupMethod(CURL* curl, const char* method) {
case METHOD('G', 'E', 'T'):
case METHOD('C', 'O', 'N'):
case METHOD('O', 'P', 'T'):
curl_easy_setopt(curl, CURLOPT_HTTPGET, 1);
curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L);
break;
case METHOD('P', 'O', 'S'):
case METHOD('P', 'A', 'T'):
case METHOD('D', 'E', 'L'):
curl_easy_setopt(curl, CURLOPT_POST, 1);
curl_easy_setopt(curl, CURLOPT_POST, 1L);
break;
case METHOD('P', 'U', 'T'):
curl_easy_setopt(curl, CURLOPT_UPLOAD, 1);
curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L);
break;
case METHOD('H', 'E', 'A'):
case METHOD('T', 'R', 'A'):
curl_easy_setopt(curl, CURLOPT_NOBODY, 1);
curl_easy_setopt(curl, CURLOPT_NOBODY, 1L);
break;
}
@@ -177,9 +203,17 @@ static size_t headerCallback(char* buffer, size_t size, size_t nitems, void* use
void naettPlatformMakeRequest(InternalResponse* res) {
InternalRequest* req = res->request;
if (!workerRunning) {
// No libcurl, or the worker died. Complete the request as failed rather than
// leaving the caller waiting forever on a request nobody is going to run.
res->code = naettGenericError;
res->complete = 1;
return;
}
CURL* c = curl_easy_init();
curl_easy_setopt(c, CURLOPT_URL, req->url);
curl_easy_setopt(c, CURLOPT_CONNECTTIMEOUT_MS, req->options.timeoutMS);
curl_easy_setopt(c, CURLOPT_CONNECTTIMEOUT_MS, (long)req->options.timeoutMS);
curl_easy_setopt(c, CURLOPT_READFUNCTION, readCallback);
curl_easy_setopt(c, CURLOPT_READDATA, res);
@@ -190,10 +224,10 @@ void naettPlatformMakeRequest(InternalResponse* res) {
curl_easy_setopt(c, CURLOPT_HEADERFUNCTION, headerCallback);
curl_easy_setopt(c, CURLOPT_HEADERDATA, res);
curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1);
curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L);
int bodySize = res->request->options.bodyReader(NULL, 0, res->request->options.bodyReaderData);
curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, bodySize);
curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)bodySize);
setupMethod(c, req->options.method);
@@ -228,6 +262,10 @@ void naettPlatformFreeRequest(InternalRequest* req) {
}
void naettPlatformCloseResponse(InternalResponse* res) {
if (!naettCurlLoad()) {
// Nothing was ever allocated by curl, and the function pointers are all null.
return;
}
curl_slist_free_all(res->headerList);
}