processone / processone/ejabberd

WebAdmin Users page silently returns empty response (badarg in fxml:element_to_binary from malformed xmlcdata node)

Open Beginner friendly
#4,597 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Erlang
Stars
6.7k
Forks
1.6k
PR merge metrics
No merged PRs in 30d

Description

Environment

  • ejabberd version: 26.03 (also reproduced on 26.04, the latest stable at time of filing)
  • Erlang/OTP: 26 (erts-14.2.5.12)
  • Installed from: official ejabberd/ecs Docker image
  • Storage: Mnesia (auth/roster/offline), PostgreSQL for mod_mam (db_type: sql)

Summary

The WebAdmin "Users" list page (/admin/server/HOST/users/) returns a completely
empty HTTP response
— the TCP connection is closed with zero bytes sent, not even
headers (curl: "Empty reply from server", browser: ERR_EMPTY_RESPONSE) — whenever
the vhost has a small number of registered users (≤10, so the single-page
list_users/6 path is used rather than the paginated "diapason" view) and the
get_last/get_offline_count/get_roster_count WebAdmin commands are unavailable
(e.g. not registered/exposed in this build — they log
Problem preparing command get_last: {throw,{error,unknown_command}} on every
WebAdmin page load in our environment, unrelated to this bug but a precondition for
triggering it).

No exception, crash report, or log line of any kind is produced anywhere — not at
debug loglevel, not in the Erlang shell, not via docker logs — which made this very
hard to diagnose. Individual per-user pages
(/admin/server/HOST/user/USERNAME/) work fine; only the combined list breaks.

Root cause (confirmed via erlang:trace_pattern/3 + isolated reproduction)

ejabberd_web_admin:list_users_element/4, the {row, last, ...} clause
(src/ejabberd_web_admin.erl, currently around line 833-839 on master):

list_users_element(_, row, last, {R, Username, Host}) ->
    [?C(element(1,
                make_command_raw_value(get_last, R, [{<<"user">>, Username}, {<<"host">>, Host}]))),
     ?C(element(2,
                make_command_raw_value(get_last,
                                       R,
                                       [{<<"user">>, Username}, {<<"host">>, Host}])))];

This assumes make_command_raw_value(get_last, ...) always returns a 2-tuple of raw
values (e.g. {Timestamp, Status}) from a successful command execution, and blindly
extracts element(1, _) / element(2, _).

But make_command_raw_value/3's documented return type (per its own -spec) also
includes the fallback shape {xmlcdata, binary()} — used when the underlying command
isn't available/fails. When get_last is unavailable, this fallback is exactly what's
returned: {xmlcdata, <<>>}.

Calling element(1, {xmlcdata, <<>>}) in that case yields the atom xmlcdata
(the tuple's own tag!) — not a timestamp. Wrapped by the ?C/1 macro
(?C(X) -> {xmlcdata, X}), this produces a malformed XML node:
{xmlcdata, xmlcdata} — i.e. an xmlcdata node whose content is the bare atom
xmlcdata instead of a binary string.

This malformed node ends up embedded in the full page's XML tree — confirmed via
tracing that list_users_element/4 returns [{xmlcdata,xmlcdata},{xmlcdata,<<>>}]
individually for multiple users (verified for at least 3 of our 5 test accounts in
the trace output, and presumably all, since the underlying get_last unavailability
is not user-specific). When ejabberd_http:make_xhtml_output/4 calls
fxml:element_to_binary/1 to serialize the whole page to send it, this NIF-backed
serializer raises error:badarg on the malformed node — with no catch/log anywhere
between there and the socket, so the connection is simply dropped with nothing sent.

Verified directly: extracted the actual XML tree built for our 5-user Users page
(via erlang:trace_pattern) and fed it straight to fxml:element_to_binary/1 in a
try/catch in an attached shell — reproduces error:badarg in isolation, top stack
frame {fxml,element_to_binary,[<the tree>]}.

Why individual user pages aren't affected

user_info/3 (used for /admin/server/HOST/user/USERNAME/) doesn't go through
list_users_element/4's row, last clause, so it doesn't hit this particular
malformed-tuple construction, even under the same "get_last unavailable" condition.

Minimal reproduction

  1. Fresh ejabberd 26.03/26.04 instance, any storage backend, mod_last either absent
    or in a state where the get_last command throws unknown_command (this is what
    we hit — worth checking under what conditions this precondition is more broadly
    true, since it may point at a second, related bug in command registration).
  2. Register 5 (or any number ≤10) users.
  3. Visit /admin/server/HOST/users/ as an authenticated admin.
  4. Response: connection closes with zero bytes sent. No log output anywhere, even at
    loglevel: debug.

Suggested fix

In list_users_element/4's row, last clause, check the shape of
make_command_raw_value(get_last, ...)'s return value before calling element/2 on
it — e.g. match {Value1, Value2} explicitly and fall back to {<<"">>, <<"">>} (or
similar) for any other shape (including the {xmlcdata, _} fallback), instead of
assuming a successful 2-tuple unconditionally.

Debugging notes

We reached this root cause by:

  1. Reproducing deterministically via curl against a live instance.
  2. Confirming zero log output at any level, including debug.
  3. Bisecting per-account via individual /admin/.../user/USERNAME/ pages — ruled out
    any single account.
  4. Building a fully isolated, disposable ejabberd+Postgres stack (no shared data with
    any production instance) to confirm this is a genuine upstream bug, independent of
    local config — reproduced on both 26.03 and 26.04.
  5. Using erlang:trace_pattern({ejabberd_web_admin, '_', '_'}, [{'_',[],[{exception_trace}]}], [local])
    (the dbg application wasn't available in the release image) to trace the full
    call sequence, which showed ejabberd_web_admin:process/2 returning a complete,
    valid
    {200, [html], XmlElement} tuple — i.e. the page-building logic itself
    never errors.
  6. Extracting that exact XmlElement term from the trace output and feeding it
    directly to fxml:element_to_binary/1 in an attached shell, reproducing
    error:badarg in isolation and confirming the malformed {xmlcdata, xmlcdata}
    node as the cause.

Happy to provide the full trace output or test further if useful.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in src/ejabberd_web_admin.erl at list_users_element/4, specifically the row, last clause, and inspect the documented return shapes of make_command_raw_value/3. Reproduce the small-user WebAdmin Users page with get_last unavailable, then verify that the page returns valid XML and no longer closes the connection with an empty response.

Written by the indexing model from the issue text.

Assessment

Tech stack
erlang
Domain
backend
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
78/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.