Compare commits

...

3 Commits

Author SHA1 Message Date
a878b1e629 feat(ui): add cons_show_warning() (issue #87)
All checks were successful
CI Code / Check coding style (pull_request) Successful in 25s
CI Code / Check spelling (pull_request) Successful in 14s
CI Code / Code Coverage (pull_request) Successful in 3m16s
CI Code / Linux (ubuntu) (pull_request) Successful in 5m7s
CI Code / Linux (arch) (pull_request) Successful in 6m50s
CI Code / Linux (debian) (pull_request) Successful in 8m26s
The console had cons_show() for information and cons_show_error() for
failures, but nothing in between, so callers reporting a condition the
user should notice had to pick between hiding it and dressing it up as
an error.

cons_show_warning() prints through the new THEME_WARNING item, yellow
by default, and prefixes the text with "Warning: ". Themes that do not
set the "warning" colour fall back to that default, so the bundled
themes need no change. Logging stays with the caller, as it does for
the other console functions.
2026-08-01 12:59:28 +03:00
d914e42ff6 fix(xmpp): treat disco#info result without 'from' as from the server
All checks were successful
CI Code / Check coding style (pull_request) Successful in 23s
CI Code / Check spelling (pull_request) Successful in 14s
CI Code / Linux (debian) (pull_request) Successful in 4m57s
CI Code / Linux (arch) (pull_request) Successful in 6m23s
CI Code / Linux (ubuntu) (pull_request) Successful in 7m59s
CI Code / Code Coverage (pull_request) Successful in 9m40s
CI Code / Check spelling (push) Successful in 15s
CI Code / Check coding style (push) Successful in 25s
CI Code / Code Coverage (push) Successful in 3m4s
CI Code / Linux (arch) (push) Successful in 6m25s
Publish Docker image / Push Docker image to Docker Hub (push) Successful in 7m29s
CI Code / Linux (ubuntu) (push) Successful in 8m21s
CI Code / Linux (debian) (push) Successful in 8m39s
RFC 6120 §8.1.2.1: a stanza received over a c2s stream without a 'from'
attribute must be treated as coming from the server itself. The
on-connect disco#info handler passed the absent attribute as NULL into
connection_features_received(), where g_str_hash() dereferenced the NULL
key and crashed (remotely triggerable DoS on connect).

Substitute connection_get_domain() at both disco#info handler
boundaries, and make connection_features_received() and
connection_get_features() NULL-safe as defense in depth. Add a stabber
regression test answering the on-connect disco#info with a from-less
result.

Fixes #168
2026-07-28 22:12:48 +03:00
964f73d0ad fix(editor): follow live terminal size in external editor
All checks were successful
CI Code / Check spelling (pull_request) Successful in 14s
CI Code / Check coding style (pull_request) Successful in 22s
CI Code / Linux (debian) (pull_request) Successful in 4m24s
CI Code / Code Coverage (pull_request) Successful in 3m8s
CI Code / Linux (ubuntu) (pull_request) Successful in 8m41s
CI Code / Linux (arch) (pull_request) Successful in 13m2s
CI Code / Check spelling (push) Successful in 15s
CI Code / Check coding style (push) Successful in 23s
CI Code / Code Coverage (push) Successful in 3m13s
Publish Docker image / Push Docker image to Docker Hub (push) Successful in 3m35s
CI Code / Linux (debian) (push) Successful in 5m0s
CI Code / Linux (ubuntu) (push) Successful in 5m5s
CI Code / Linux (arch) (push) Successful in 6m30s
The compose editor is spawned via fork+execvp and inherits profanity's
LINES/COLUMNS, which hold the size captured at startup and are never
refreshed on resize. A curses editor (nano, vim, ...) honors them over
the real window, so it renders at the launch-time size after a resize.

Drop LINES/COLUMNS from the child's environment before forking so its
curses falls back to ioctl(TIOCGWINSZ). Assembling the env in the parent
lets the child only reassign environ instead of calling unsetenv()
between fork and exec, keeping it clear of unsetenv()'s allocator work in
the multithreaded fork->exec window. profanity itself is unaffected.
2026-07-25 15:19:22 +00:00
12 changed files with 118 additions and 4 deletions

View File

@@ -87,6 +87,7 @@ theme_init(const char* const theme_name)
g_hash_table_insert(defaults, strdup("main.help.header"), strdup("default")); g_hash_table_insert(defaults, strdup("main.help.header"), strdup("default"));
g_hash_table_insert(defaults, strdup("main.trackbar"), strdup("default")); g_hash_table_insert(defaults, strdup("main.trackbar"), strdup("default"));
g_hash_table_insert(defaults, strdup("error"), strdup("red")); g_hash_table_insert(defaults, strdup("error"), strdup("red"));
g_hash_table_insert(defaults, strdup("warning"), strdup("yellow"));
g_hash_table_insert(defaults, strdup("incoming"), strdup("yellow")); g_hash_table_insert(defaults, strdup("incoming"), strdup("yellow"));
g_hash_table_insert(defaults, strdup("mention"), strdup("yellow")); g_hash_table_insert(defaults, strdup("mention"), strdup("yellow"));
g_hash_table_insert(defaults, strdup("trigger"), strdup("yellow")); g_hash_table_insert(defaults, strdup("trigger"), strdup("yellow"));
@@ -703,6 +704,9 @@ theme_attrs(theme_item_t attrs)
case THEME_ERROR: case THEME_ERROR:
_theme_prep_fgnd("error", lookup_str, &bold); _theme_prep_fgnd("error", lookup_str, &bold);
break; break;
case THEME_WARNING:
_theme_prep_fgnd("warning", lookup_str, &bold);
break;
case THEME_INCOMING: case THEME_INCOMING:
_theme_prep_fgnd("incoming", lookup_str, &bold); _theme_prep_fgnd("incoming", lookup_str, &bold);
break; break;

View File

@@ -23,6 +23,7 @@ typedef enum {
THEME_SPLASH, THEME_SPLASH,
THEME_HELP_HEADER, THEME_HELP_HEADER,
THEME_ERROR, THEME_ERROR,
THEME_WARNING,
THEME_INCOMING, THEME_INCOMING,
THEME_MENTION, THEME_MENTION,
THEME_TRIGGER, THEME_TRIGGER,

View File

@@ -25,6 +25,8 @@
#include "ui/ui.h" #include "ui/ui.h"
#include "xmpp/xmpp.h" #include "xmpp/xmpp.h"
extern char** environ;
typedef struct EditorContext typedef struct EditorContext
{ {
gchar* filename; gchar* filename;
@@ -140,6 +142,22 @@ launch_editor(gchar* initial_content, void (*callback)(gchar* content, void* dat
GSource* sigchld_warmup = g_child_watch_source_new(getpid()); GSource* sigchld_warmup = g_child_watch_source_new(getpid());
g_source_unref(sigchld_warmup); g_source_unref(sigchld_warmup);
// Build the editor's env without LINES/COLUMNS pre-fork, so the child only
// reassigns environ instead of calling unsetenv() between fork and exec.
// The editor's (n)curses then reads the live window via ioctl(TIOCGWINSZ).
gsize env_len = 0;
while (environ[env_len]) {
env_len++;
}
gchar** editor_env = g_new0(gchar*, env_len + 1);
gsize env_kept = 0;
for (gsize i = 0; i < env_len; i++) {
if (g_str_has_prefix(environ[i], "LINES=") || g_str_has_prefix(environ[i], "COLUMNS=")) {
continue;
}
editor_env[env_kept++] = environ[i];
}
pid_t pid = fork(); pid_t pid = fork();
if (pid == -1) { if (pid == -1) {
log_error("[Editor] Failed to fork: %s", strerror(errno)); log_error("[Editor] Failed to fork: %s", strerror(errno));
@@ -148,12 +166,14 @@ launch_editor(gchar* initial_content, void (*callback)(gchar* content, void* dat
ui_resize(); ui_resize();
cons_show_error("Failed to start editor: %s", strerror(errno)); cons_show_error("Failed to start editor: %s", strerror(errno));
g_strfreev(editor_argv); g_strfreev(editor_argv);
g_free(ctx->filename); g_free(editor_env);
g_free(ctx); g_free(ctx);
return TRUE; return TRUE;
} else if (pid == 0) { } else if (pid == 0) {
// Child process: Inherits TTY from parent // Child process: Inherits TTY from parent
environ = editor_env; // live TIOCGWINSZ size, not the inherited LINES/COLUMNS
// SIGTSTP=SIG_DFL lets vim's :stop / Ctrl-Z work; profanity catches // SIGTSTP=SIG_DFL lets vim's :stop / Ctrl-Z work; profanity catches
// the STOPPED state via editor_check_stopped() and drops to the shell. // the STOPPED state via editor_check_stopped() and drops to the shell.
signal(SIGINT, SIG_DFL); signal(SIGINT, SIG_DFL);
@@ -170,6 +190,7 @@ launch_editor(gchar* initial_content, void (*callback)(gchar* content, void* dat
editor_pid = pid; editor_pid = pid;
g_child_watch_add((GPid)pid, _editor_exit_cb, ctx); g_child_watch_add((GPid)pid, _editor_exit_cb, ctx);
g_strfreev(editor_argv); g_strfreev(editor_argv);
g_free(editor_env); // array only; strings are borrowed from environ
return FALSE; return FALSE;
} }

View File

@@ -155,6 +155,20 @@ cons_show_error(const char* const msg, ...)
cons_alert(NULL); cons_alert(NULL);
} }
void
cons_show_warning(const char* const msg, ...)
{
va_list arg;
va_start(arg, msg);
GString* fmt_msg = g_string_new(NULL);
g_string_vprintf(fmt_msg, msg, arg);
win_println(wins_get_console(), THEME_WARNING, "-", "Warning: %s", fmt_msg->str);
g_string_free(fmt_msg, TRUE);
va_end(arg);
cons_alert(NULL);
}
void void
cons_show_tlscert_summary(const TLSCertificate* cert) cons_show_tlscert_summary(const TLSCertificate* cert)
{ {

View File

@@ -252,6 +252,8 @@ G_GNUC_PRINTF(1, 2)
void cons_debug(const char* const msg, ...); void cons_debug(const char* const msg, ...);
G_GNUC_PRINTF(1, 2) G_GNUC_PRINTF(1, 2)
void cons_show_error(const char* const cmd, ...); void cons_show_error(const char* const cmd, ...);
G_GNUC_PRINTF(1, 2)
void cons_show_warning(const char* const msg, ...);
void cons_show_contacts(GSList* list); void cons_show_contacts(GSList* list);
void cons_show_roster(GSList* list); void cons_show_roster(GSList* list);
void cons_show_roster_group(const char* const group, GSList* list); void cons_show_roster_group(const char* const group, GSList* list);

View File

@@ -749,11 +749,22 @@ connection_get_user(void)
return connection_get_jid()->localpart; return connection_get_jid()->localpart;
} }
// NULL 'from' means the server (RFC 6120 §8.1.2.1)
static const char*
_get_from_via_jid(const char* const jid)
{
return jid ? jid : conn.domain;
}
void void
connection_features_received(const char* const jid) connection_features_received(const char* const jid)
{ {
log_info("[CONNECTION] connection_features_received %s", jid); const char* key = _get_from_via_jid(jid);
if (g_hash_table_remove(conn.requested_features, jid) && g_hash_table_size(conn.requested_features) == 0) { if (!key) {
return;
}
log_info("[CONNECTION] connection_features_received %s", key);
if (g_hash_table_remove(conn.requested_features, key) && g_hash_table_size(conn.requested_features) == 0) {
sv_ev_connection_features_received(); sv_ev_connection_features_received();
} }
} }
@@ -761,7 +772,11 @@ connection_features_received(const char* const jid)
GHashTable* GHashTable*
connection_get_features(const char* const jid) connection_get_features(const char* const jid)
{ {
return g_hash_table_lookup(conn.features_by_jid, jid); const char* key = _get_from_via_jid(jid);
if (!key || !conn.features_by_jid) {
return NULL;
}
return g_hash_table_lookup(conn.features_by_jid, key);
} }
GList* GList*

View File

@@ -2314,6 +2314,7 @@ _disco_info_response_id_handler(xmpp_stanza_t* const stanza, void* const userdat
log_debug("Received disco#info response from: %s", from); log_debug("Received disco#info response from: %s", from);
} else { } else {
log_debug("Received disco#info response"); log_debug("Received disco#info response");
from = connection_get_domain(); // RFC 6120 §8.1.2.1: no 'from' means the server itself
} }
// handle error responses // handle error responses
@@ -2397,6 +2398,7 @@ _disco_info_response_id_handler_onconnect(xmpp_stanza_t* const stanza, void* con
log_debug("Received disco#info response from: %s", from); log_debug("Received disco#info response from: %s", from);
} else { } else {
log_debug("Received disco#info response"); log_debug("Received disco#info response");
from = connection_get_domain(); // RFC 6120 §8.1.2.1: no 'from' means the server itself
} }
// handle error responses // handle error responses

View File

@@ -173,6 +173,7 @@ main(int argc, char* argv[])
PROF_FUNC_TEST(disco_info_without_name), PROF_FUNC_TEST(disco_info_without_name),
PROF_FUNC_TEST(disco_items_without_name), PROF_FUNC_TEST(disco_items_without_name),
PROF_FUNC_TEST(disco_info_service_unavailable), PROF_FUNC_TEST(disco_info_service_unavailable),
PROF_FUNC_TEST(disco_info_result_no_from),
/* Roster management - add/remove/rename contacts */ /* Roster management - add/remove/rename contacts */
PROF_FUNC_TEST(sends_new_item), PROF_FUNC_TEST(sends_new_item),

View File

@@ -396,6 +396,35 @@ disco_items_without_name(void **state)
prof_timeout_reset(); prof_timeout_reset();
} }
void
disco_info_result_no_from(void **state)
{
/*
* Test that a disco#info result without a 'from' attribute is treated as
* coming from the server itself (RFC 6120 §8.1.2.1). The on-connect
* disco#info handler used to crash on such responses (issue #168).
*/
stbbr_for_query("http://jabber.org/protocol/disco#info",
"<iq to='stabber@localhost/profanity' type='result'>"
"<query xmlns='http://jabber.org/protocol/disco#info'>"
"<identity category='server' type='im' name='NoFromServer'/>"
"<feature var='urn:xmpp:ping'/>"
"</query>"
"</iq>"
);
/* the on-connect disco#info gets the same from-less response */
prof_connect();
prof_input("/disco info");
prof_timeout(10);
/* client survived and attributed the response to the server */
assert_true(prof_output_exact("Service discovery info for localhost"));
assert_true(prof_output_regex("NoFromServer.*im.*server"));
prof_timeout_reset();
}
void void
disco_info_service_unavailable(void **state) disco_info_service_unavailable(void **state)
{ {

View File

@@ -17,3 +17,4 @@ void disco_info_multiple_identities(void **state);
void disco_info_without_name(void **state); void disco_info_without_name(void **state);
void disco_items_without_name(void **state); void disco_items_without_name(void **state);
void disco_info_service_unavailable(void **state); void disco_info_service_unavailable(void **state);
void disco_info_result_no_from(void **state);

View File

@@ -49,6 +49,18 @@ expect_any_cons_show_error(void)
expect_any(cons_show_error, output); expect_any(cons_show_error, output);
} }
void
expect_cons_show_warning(char* expected)
{
expect_string(cons_show_warning, output, expected);
}
void
expect_any_cons_show_warning(void)
{
expect_any(cons_show_warning, output);
}
void void
expect_win_println(char* message) expect_win_println(char* message)
{ {
@@ -845,6 +857,16 @@ cons_show_error(const char* const cmd, ...)
va_end(args); va_end(args);
} }
void
cons_show_warning(const char* const msg, ...)
{
va_list args;
va_start(args, msg);
vsnprintf(output, sizeof(output), msg, args);
check_expected(output);
va_end(args);
}
void void
cons_show_contacts(GSList* list) cons_show_contacts(GSList* list)
{ {

View File

@@ -10,4 +10,6 @@ void expect_cons_show(char* expected);
void expect_any_cons_show(void); void expect_any_cons_show(void);
void expect_cons_show_error(char* expected); void expect_cons_show_error(char* expected);
void expect_any_cons_show_error(void); void expect_any_cons_show_error(void);
void expect_cons_show_warning(char* expected);
void expect_any_cons_show_warning(void);
void expect_win_println(char* message); void expect_win_println(char* message);