processone / processone/ejabberd
WebAdmin Users page silently returns empty response (badarg in fxml:element_to_binary from malformed xmlcdata node)
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/ecsDocker 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
- Fresh ejabberd 26.03/26.04 instance, any storage backend,
mod_lasteither absent
or in a state where theget_lastcommand throwsunknown_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). - Register 5 (or any number ≤10) users.
- Visit
/admin/server/HOST/users/as an authenticated admin. - 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:
- Reproducing deterministically via
curlagainst a live instance. - Confirming zero log output at any level, including
debug. - Bisecting per-account via individual
/admin/.../user/USERNAME/pages — ruled out
any single account. - 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. - Using
erlang:trace_pattern({ejabberd_web_admin, '_', '_'}, [{'_',[],[{exception_trace}]}], [local])
(thedbgapplication wasn't available in the release image) to trace the full
call sequence, which showedejabberd_web_admin:process/2returning a complete,
valid{200, [html], XmlElement}tuple — i.e. the page-building logic itself
never errors. - Extracting that exact
XmlElementterm from the trace output and feeding it
directly tofxml:element_to_binary/1in an attached shell, reproducing
error:badargin 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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