#2014 Connection stays paused after a stream resumption if a c2s-session-updated handler waits (async)
Reporter
damencho
Owner
Nobody
Created
Updated
Stars
★ (1)
Tags
Type-Defect
Priority-Medium
Status-New
damencho
on
What steps will reproduce the problem?
1. Load this module on any host:
local async = require "util.async";
module:hook_global("c2s-session-updated", function ()
local wait, done = async.waiter();
module:add_timer(0.1, done);
wait();
end);
2. Log in with a normal account (internal_hashed, over WebSocket in our test), enable
stream management with resume (XEP-0198), then drop the connection.
3. Reconnect and resume with <resume previd=...>. Prosody answers <resumed>.
4. Send any IQ, for example disco#info to the host.
What is the expected output?
The IQ is answered, as it is when the module is not loaded.
What do you see instead?
No reply. Prosody never logs receiving the IQ or anything else sent on that connection,
until the client gives up and reconnects. Debug log:
c2s... Session absorbed into c2s...
runnerX changed state from ready to waiting
runnerX changed state from waiting to ready
c2s... Session has no connection to resume
What version of the product are you using? On what operating system?
Prosody 13.0 nightly build 98 (75d09be04f13), prosodyim/prosody:13.0 Docker image
(Debian 13 trixie, aarch64, Lua 5.4.7). update_session() in core/sessionmanager.lua
is the same on trunk.
Please provide any additional information below.
Any c2s-session-updated handler that waits through util.async causes this, for example
an HTTP request done with async.waiter.
1. The <resume> stanza is processed on the async runner of the new connection's session
(from_session.thread, created in mod_c2s as runner(..., runner_callbacks, session),
so runner.data == from_session).
2. mod_smacks do_resume() -> sessionmanager.update_session(original_session, session)
moves conn, send, stream, etc. to the original session and fires
c2s-session-updated.
3. A handler of that event waits. The runner changes from ready to waiting, and
runner_callbacks:waiting() calls self.data.conn:pause(). That is the client
connection, which from_session still references at this point.
4. When the wait completes, update_session() continues and calls
retire_session(from_session). This clears from_session.conn.
5. The runner changes from waiting to ready. runner_callbacks:ready() finds
self.data.conn == nil and logs "Session has no connection to resume". Nothing
resumes the connection, and it now belongs to original_session.
Suggested fix: before update_session() fires c2s-session-updated, point the runner at
the session that now owns the connection:
local thread = from_session.thread;
if type(thread) == "table" and thread.data == from_session then
thread.data = to_session;
end
Then the pause/resume in runner_callbacks acts on the connection that to_session now
holds.
Workaround: Jitsi Meet hit this in a module that verifies a JWT presented by the
resuming connection, and fetches the signing public key over HTTP when it isn't cached.
We work around it by running the snippet above from our own c2s-session-updated hook,
at priority 1000 so it runs before any handler that can wait. With that hook loaded,
the reproduction above gets its reply:
https://github.com/jitsi/jitsi-meet/pull/18130
What steps will reproduce the problem? 1. Load this module on any host: local async = require "util.async"; module:hook_global("c2s-session-updated", function () local wait, done = async.waiter(); module:add_timer(0.1, done); wait(); end); 2. Log in with a normal account (internal_hashed, over WebSocket in our test), enable stream management with resume (XEP-0198), then drop the connection. 3. Reconnect and resume with <resume previd=...>. Prosody answers <resumed>. 4. Send any IQ, for example disco#info to the host. What is the expected output? The IQ is answered, as it is when the module is not loaded. What do you see instead? No reply. Prosody never logs receiving the IQ or anything else sent on that connection, until the client gives up and reconnects. Debug log: c2s... Session absorbed into c2s... runnerX changed state from ready to waiting runnerX changed state from waiting to ready c2s... Session has no connection to resume What version of the product are you using? On what operating system? Prosody 13.0 nightly build 98 (75d09be04f13), prosodyim/prosody:13.0 Docker image (Debian 13 trixie, aarch64, Lua 5.4.7). update_session() in core/sessionmanager.lua is the same on trunk. Please provide any additional information below. Any c2s-session-updated handler that waits through util.async causes this, for example an HTTP request done with async.waiter. 1. The <resume> stanza is processed on the async runner of the new connection's session (from_session.thread, created in mod_c2s as runner(..., runner_callbacks, session), so runner.data == from_session). 2. mod_smacks do_resume() -> sessionmanager.update_session(original_session, session) moves conn, send, stream, etc. to the original session and fires c2s-session-updated. 3. A handler of that event waits. The runner changes from ready to waiting, and runner_callbacks:waiting() calls self.data.conn:pause(). That is the client connection, which from_session still references at this point. 4. When the wait completes, update_session() continues and calls retire_session(from_session). This clears from_session.conn. 5. The runner changes from waiting to ready. runner_callbacks:ready() finds self.data.conn == nil and logs "Session has no connection to resume". Nothing resumes the connection, and it now belongs to original_session. Suggested fix: before update_session() fires c2s-session-updated, point the runner at the session that now owns the connection: local thread = from_session.thread; if type(thread) == "table" and thread.data == from_session then thread.data = to_session; end Then the pause/resume in runner_callbacks acts on the connection that to_session now holds. Workaround: Jitsi Meet hit this in a module that verifies a JWT presented by the resuming connection, and fetches the signing public key over HTTP when it isn't cached. We work around it by running the snippet above from our own c2s-session-updated hook, at priority 1000 so it runs before any handler that can wait. With that hook loaded, the reproduction above gets its reply: https://github.com/jitsi/jitsi-meet/pull/18130