socketclient: connection refused socks5 reply does lead to CPU 100% instead of correct error reporting
Description
Using the following two attached snippets to reproduce it. It is reproducible via libsoup originally in WebKit (Epiphany too) but also without WebKit when the Socks server which is configured returns a non-success reply, e.g. connection refused. The CPU then spins to 100% forever. I tested it also on the current master with both libraries and the problem seems to be also persistent there.
It looks like #2233 (closed) with #1728 (closed) was related to it and tried to fix it. But the error does not get raised correctly in my scenario. (cc @mcatanzaro)
Versions:
- Ubuntu 20.04/Ubuntu 21.10
- libsoup and glib tip-of-tree / master / 2.68
- WebKit: master
Some investigation:
- The error get's "successfully" catched there: https://gitlab.gnome.org/GNOME/glib/-/blob/master/gio/gsocks5proxy.c#L946-954
- After that the error is stored inside
data->error_info->tmp_error
and it goes into this if where the error handling should happen
Affected
- WebKit
- Epiphany
- libsoup, see the reproducible
Reproducible
Socks client:
/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- */
/*
* Copyright (C) 2001-2003, Ximian, Inc.
* Copyright (C) 2013 Igalia, S.L.
*/
#include <stdio.h>
#include <stdlib.h>
#include <libsoup/soup.h>
#include <gnu/libc-version.h>
static SoupSession *session;
static GMainLoop *loop;
static gboolean debug, head;
static const char *proxy;
#define OUTPUT_BUFFER_SIZE 8192
static void
on_read_ready (GObject *source, GAsyncResult *result, gpointer user_data)
{
GInputStream *in = G_INPUT_STREAM (source);
GError *error = NULL;
gsize bytes_read = 0;
char *output_buffer = user_data;
g_input_stream_read_all_finish (in, result, &bytes_read, &error);
if (bytes_read) {
g_print ("%.*s", (int)bytes_read, output_buffer);
}
if (error) {
g_printerr ("\nFailed to read stream: %s\n", error->message);
g_error_free (error);
g_free (output_buffer);
g_main_loop_quit (loop);
} else if (!bytes_read) {
g_print ("\n");
g_free (output_buffer);
g_main_loop_quit (loop);
} else {
g_input_stream_read_all_async (in, output_buffer, OUTPUT_BUFFER_SIZE,
G_PRIORITY_DEFAULT, NULL, on_read_ready, output_buffer);
}
}
static void
on_request_sent (GObject *source, GAsyncResult *result, gpointer user_data)
{
g_print("on_request_sent\n");
GError *error = NULL;
GInputStream *in = soup_session_send_finish (SOUP_SESSION (source), result, &error);
if (error) {
g_printerr ("Failed to send request: %s\n", error->message);
g_error_free (error);
g_main_loop_quit (loop);
return;
}
char *output_buffer = g_new (char, OUTPUT_BUFFER_SIZE);
g_input_stream_read_all_async (in, output_buffer, OUTPUT_BUFFER_SIZE,
G_PRIORITY_DEFAULT, NULL, on_read_ready, output_buffer);
g_object_unref (in);
}
static GOptionEntry entries[] = {
{ "debug", 'd', 0,
G_OPTION_ARG_NONE, &debug,
"Show HTTP headers", NULL },
{ "head", 'h', 0,
G_OPTION_ARG_NONE, &head,
"Do HEAD rather than GET", NULL },
{ "proxy", 'p', 0,
G_OPTION_ARG_STRING, &proxy,
"Use URL as an HTTP proxy", "URL" },
{ NULL }
};
int
main (int argc, char **argv)
{
GOptionContext *opts;
const char *url;
SoupMessage *msg;
GUri *parsed;
GError *error = NULL;
opts = g_option_context_new (NULL);
g_option_context_add_main_entries (opts, entries, NULL);
if (!g_option_context_parse (opts, &argc, &argv, &error)) {
g_printerr ("Could not parse arguments: %s\n",
error->message);
g_printerr ("%s",
g_option_context_get_help (opts, TRUE, NULL));
exit (1);
}
if (argc != 2) {
char *help = g_option_context_get_help (opts, TRUE, NULL);
g_printerr ("%s", help);
g_free (help);
exit (1);
}
g_option_context_free (opts);
/* Validate the URL */
url = argv[1];
parsed = g_uri_parse (url, SOUP_HTTP_URI_FLAGS, &error);
if (!parsed) {
g_printerr ("Could not parse '%s' as a URL: %s\n", url, error->message);
exit (1);
}
g_uri_unref (parsed);
/* Build the session with all of the features we need */
session = soup_session_new_with_options ("user-agent", "get ",
"accept-language-auto", TRUE,
"timeout", 15,
NULL);
soup_session_add_feature_by_type (session, SOUP_TYPE_CONTENT_DECODER);
soup_session_add_feature_by_type (session, SOUP_TYPE_COOKIE_JAR);
if (debug) {
SoupLogger *logger = soup_logger_new (SOUP_LOGGER_LOG_HEADERS);
soup_session_add_feature (session, SOUP_SESSION_FEATURE (logger));
g_object_unref (logger);
}
if (proxy) {
GProxyResolver *resolver;
GUri *proxy_uri = g_uri_parse (proxy, SOUP_HTTP_URI_FLAGS, &error);
if (!proxy_uri) {
g_printerr ("Could not parse '%s' as URI: %s\n",
proxy, error->message);
g_error_free (error);
g_object_unref (session);
exit (1);
}
resolver = g_simple_proxy_resolver_new (proxy, NULL);
soup_session_set_proxy_resolver (session, resolver);
g_uri_unref (proxy_uri);
g_object_unref (resolver);
}
/* Send the request */
msg = soup_message_new (head ? "HEAD" : "GET", url);
soup_session_send_async (session, msg, G_PRIORITY_DEFAULT, NULL,
on_request_sent, NULL);
g_object_unref (msg);
/* Run the loop */
loop = g_main_loop_new (NULL, FALSE);
g_print ("before\n");
g_main_loop_run (loop);
g_print ("after\n");
g_main_loop_unref (loop);
g_object_unref (session);
return 0;
}
Socks server: (npm install socksv5 && node server.js
to start it)
var socks = require('socksv5');
var srv = socks.createServer(function(info, accept, deny) {
console.log('request', info);
deny();
});
srv.listen(1080, 'localhost', function() {
console.log('SOCKS server listening on port 1080');
});
srv.useAuth(socks.auth.None());
Replace the normal get example with the attached get example and execute it while having the socks server running.
./_build/examples/get -d -p socks://localhost:1080 http://example.com
The target URL does not matter, since on the server side we deny all the requests which send a socks 0x2 reply bit. With 0x5 the same happens. See here: https://datatracker.ietf.org/doc/html/rfc1928#section-6