Verify libtacky bugs from the AI interop runs (v1.2.0-rc2, 71 found) #1
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
The AI interop runs of 2026-11-04 to 2026-11-07 tested libtacky v1.2.0-rc2 (tackyd-json) against slixmpp on Prosody, ejabberd and anotherim-server. They found 71 distinct libtacky bugs: 13 security/privacy, 33 correctness and 25 minor/SHOULD. Upstream v1.2.0 (
1dae0a6c, develop HEAD) fixes 16 of them, partly fixes 1 and probably fixes 1. The other 53 are not fixed upstream or have an unclear status. Duplicates across servers and runs are merged below.How to verify a bug: open each linked run dir, read
notesandbuginrun.toml, then find the stanza inclient.log(libtacky's wire).client-events.logholds the API events andclient-debug.logthe debug log.peer-slixmpp.logandserver.logshow the other side. Source refs arefile:lineat tagv1.2.0-rc2. "Checked" means the code at1dae0a6cwas compared. "Commit log only" means no matching upstream commit was found but the code was not re-read. Tick the box once a human has seen it. Bugs marked Needs live check can only be settled against a live server (the three test servers are being restarted); they say what to check.Links: runs
https://dev.another.im/another-im/interop/src/branch/ai-runs-2026-11-07/runs/<dir>, commitshttps://dev.another.im/another-im/libtacky/commit/<sha>.Contents
geoloc stopovertaken by a stale republishexpr(Inf)parse_urisplits a decoded?(link spoofing), accepts portsby=own account)<Ack>, XML console)fromthrows, message lost<close/>, 60 s stall (callback arity)notify <Notify><advanced/>dropped on rewrite<delete/>stops sharing for goodexpr(1E…→Inf)<file/>lacks<hash/>and<desc/>urn:ietf:params:xml:ns:vcard-4.0urn:xmpp:eme:0;name=on listed EME nspubsub#notify_retractfalsemessage sendneither validates nor prepares JIDsaccount unregisternever answers on ejabberd; redial loop<message/>in urn:xmpp:forward:0 namespaceiap/happy_eyeballssettings never reach the connectionbgerror)adhoc listkeeps foreign command nodesthread_parent == threadaccepted<attachments>read as attachmentsget_invoicewithout disco<r/>unanswered ~50 min without declaring the stream dead<count>hiddenuntrust_certificatefor a registration domain fails<nick>accept_inviteleaves its autojoin bookmarkstanza-id by=(untested)Security / privacy
S1. Call invites (XEP-0482) and meeting messages (XEP-0483) bypass OMEMO
omemo set_enableddoc says "a send fails closed rather than going out in the clear".explicit:true).meetings invite/accept/reject/retract/leaveeach wrote a plain<message type='chat'>with no<encrypted/>. Example from client.log line 144:<message id='meet-…' to='ai3b@…' type='chat'><invite xmlns='urn:xmpp:call-invites:0'><external uri='https://meet.example.invalid/…'/>…<body>You are invited to join … at https://…</body>. Muji call invites likewise put the room JID in the clear.send_updatedoes under OMEMO).tacky/xmpp/modules/meetings.tcl:273-284Sendcalls$client write.tacky/xmpp/modules/groupcall.tcl:1910-1925SendToChatcalls$client write. Both bypass the message/OMEMO send path.<invite/>inside), or refuse whenomemo chat_stateis enabled and the payload cannot be sealed.{MEETINGS ENCRYPTED}), as our patch did. Group-call invites and answers are not refused but sealed:groupcall Deliverputs them in an OMEMO 2 envelope (omemo encrypt_envelope, new) with only the EME marker and<store/>outside, and withholds them ({TACKY GROUPCALL ENCRYPTED}, or a<Warning>for an answer) when no OMEMO 2 device can read them. So an OMEMO 0.3-only peer (Dino today) gets no call invite in an OMEMO chat.S2. Chat state, receipt request and markable in clear beside OMEMO 2 ciphertext
<envelope/>". libtacky's features.md row says "an encrypted message's outer elements are only the server's".…<encrypted xmlns='urn:xmpp:omemo:2'>…</encrypted><body>I sent you an OMEMO encrypted message…</body><encryption xmlns='urn:xmpp:eme:0' …/><store/><active xmlns='http://jabber.org/protocol/chatstates'/><request xmlns='urn:xmpp:receipts'/><markable xmlns='urn:xmpp:chat-markers:0'/></message>. The same happens on replies and corrections. Receipts, displayed markers, reactions and retractions also go out in the clear (INFO R19).<content/>, or not sent. Only Table 2 elements and the fallback body belong outside.tacky/xmpp/modules/message.tcl:3443-3451(BuildMessageStanza) appendsactive/request/markableunconditionally, outside the encrypted payload.encWireis set, put these into the envelope's extras (as is done for reply pointers), or omit them. Then do the same for receipts, markers, reactions and retractions.BuildMessageStanzahands<active/>,<request/>and<markable/>toencrypt_messagewith the pointers, so they ride inside the :2 envelope. They stay outside only when a 0.3 element is written. Bodyless signals sent on their own (receipts, displayed markers, chat states, reactions, retractions) are still in the clear; features.md now says so.S3. File name, size and type in clear beside legacy OMEMO ciphertext
…</encrypted><body>…</body><encryption …/><store/><reference xmlns='urn:xmpp:reference:0' type='data'><media-sharing xmlns='urn:xmpp:sims:1'><file …><media-type>application/octet-stream</media-type><name>f-87e2cf27.bin</name><size>2000</size></file></media-sharing></reference>….legacyOutside,message.tcl:3288-3299,:3340,:3416-3421,LegacySimsNode~:3500) so that legacy readers see it. It is not stated in features.md or HANDBOOK.LegacySimsNodebeside ciphertext. If the legacy UX matters, gate it behind a setting and document the leak.legacy_file_metadata:0drops the SIMS metadata beside a 0.3 ciphertext, but unset means on, so by default the file name, size and type still go out in the clear. Our patch removedLegacySimsNodebeside ciphertext outright. The leak is now documented (features.md, HANDBOOK).S4. A "room join" at a contact's bare JID silently downgrades the 1:1 chat to plaintext
run.toml. The store rows were since repaired by hand.aiNb@…as a room leftmodule_state muc_features/<contact> = "-". From then onomemo chat_statefor that contact saysenabled:false reason room_not_eligible, overriding the user's explicit ON. This persists, and there is no API to forget it.tacky/xmpp/modules/omemo.tcl:5188-5195IsRoomtrustsmuc PrivacyOf"known" (muc.tcl:718), which reads the stalemuc_featuresrow.muc_featuresonly after disco#info shows aconferenceidentity. InIsRoom, prefer roster/contact evidence. Add a forget call.S5. CAPTCHA inline BoB data taken without hash, size or type check
<data/>taken only when its bytes hash to its cid".<uri>cid:sha1+40ecc4…@bob.xmpp.org</uri>and<data cid='sha1+40ecc4…'>hold bytes hashing to0aebef….captcha <Challenge>still carriesmedia.ocrwith those bytes. The ad-hoc path in the same run logsadhoc: <data cid=…> dropped: the data does not hash to its cid. The notes add that unbounded size andimage/svg+xmlwithonloadalso pass, which is an XSS risk if a host renders SVG. Challenges for stanzas never sent, and from a foreignfrom, also reach the host (SHOULD ignore).tacky/xmpp/modules/captcha.tcl:224-238only base64-decodes the data. Compareadhoc.tcl:312, which uses::tacky::bob::check $cid … $MEDIA_MAXplus a raster-only type filter.bob::checkand raster allowlist asadhoc.tcl:312-319. Drop challenges whose referenced stanza id we never sent.S6. spans-0: any range marked as fallback is stripped, so a sender can hide text
docs/protocol). Recorded as INFO, not as a doc violation.<fallback for='urn:tacky:spans:0'>range is removed without checking that it is(href). Text such as "PS: wire the money…" is shown by other clients but hidden from libtacky users. A range past the body end is clipped. A childless<fallback for=spans/>drops the whole message. Forwards and quotes lose the hidden URL.tacky/xmpp/modules/message.tcl:8864-8867(fallback ranges for spans) and:9521-9522.(<href>)directly after a link span. Ignore other ranges and a childless spans fallback.S7. XEP-0100: a user's full JID claiming gateway identity gets added to the roster
adhoc execute node=registeratai2b@ejabberd.asxp.io/gw-…(disco identitygateway/legacy), libtacky sent<iq type='set'><query xmlns='jabber:iq:roster'><item jid='ai2b@…/gw-…'/></query></iq>plus<presence type='subscribe'/>. On ejabberd this replaced the existing bare contact (subscriptionbothbecameto, losingfrom). Where pre-approval is used, it also gives our presence away.tacky/xmpp/modules/adhoc.tcl:340-345GatewayRegistered, thensearch Linked(search.tcl:675), thenroster.tcl:600IfGateway. None of them checks that the JID has no node or resource.GatewayRegistered/IfGateway, return early unlessjid usernameandjid resourceare both empty, asgateway registeralready refuses ("is not a remote service").S8.
geoloc stopovertaken by a stale republish afterset_allowed<geoloc/>), §8 (access control).set_allowed [],set_allowed [peer]andstopwere made back to back. iq 78 publishes the empty<geoloc/>and is acked. Then iq 80 sets the peer as member, and iq 81 publishes<item id='current'><geoloc>…<lat>12.3456</lat>…</geoloc></item>again. The peer reads the location, andgeoloc getsaysactive true.tacky/xmpp/modules/geoloc.tcl:257-266AfterListChangerepublishesStoredownlocfor added members without checking that sharing is still active. The refill ofStoredwas not traced.AfterListChange, return if the own state is stopped/inactive. Serialisestopafter pending list changes.set_allowed [],set_allowed [peer],stopback to back; read the geoloc node as the peer 8 s later (must be empty) — on rc2 and on 1dae0a6c.S9. OTR ssid and fingerprint mangled by Tcl
expr02802368162e5437was reported as"ssid": "Inf"inotr <State>(client-events.log:89).tacky/xmpp/otr.tcl:277-278buildsfingerprintandssidinsideexpr {… ? "" : [hex …]}. Tcl canonicalises numeric-looking results (…e…becomes Inf; all digits lose leading zeros).if {$x eq ""} {set s ""} else {set s [string tolower [binary encode hex $x]]}, notexpr. Same class as C11.S10.
account parse_uri: a decoded?splits the address (link spoofing); ports acceptedxmpp:x%3Fy…@example.invalid?subscribegives jidx, soxmpp:target.example%3Fz@innocent.example?subscribesubscribes to target.example.?invite;jid=a%3Fb@…givesa.xmpp:ai1b@prosody.asxp.io:5222?messageis accepted with the port in the domain.tacky/xmpp/jid.tcl, the rc2 pattern refused?in a localpart, sojid baresplit at it. Used fromaccount.tcl:1791LinkUri.jid validdoes not refuse:in a domain.?split is fixed upstream. Also refuse a:port(and?) in the decoded domain, and add URI tables totest_link.tcl.%3Flink now namesx?y@…; does…:5222?messagestill parse??split was fixed in v1.2.0 by cd3e39c5.S11. XEP-0227 export: every PEP node exported as open; own vcard-temp written as a PEP node
<user>holds<pubsub#owner><configure node='vcard-temp'>(access open) plus items, and no<vCard xmlns='vcard-temp'/>child. On Prosody theurn:xmpp:vcard4node is exported asopenalthough the server has it aspresence. An importing server would publish the vCard to everyone.tacky/xmpp/modules/backup.tcl:584hard-codesopenexcept for bookmarks.WriteUserexports thevcard-temppseudo-node (vcard.tclKeep).S12. A peer can choose the stanza-id of its own message (forged
by=our account)<stanza-id by=ai1a id=forged-me-…/>came before Prosody's own, and libtacky storedserver_id=forged-me-…even after MAM catch-up. Replies, retractions and markers then name the peer's choice.tacky/xmpp/modules/message.tcl:9474ArchiveStanzaIdtakes the first element withby=our account.S13. Secrets reach host-facing output
sm <Ack>hands the host the raw acked stanzas, includingchange_password's new password (it reached client-events.log; the scenario redacts it). (b) The XML console logs the invite form URI with its preauth token unredacted, becauseredact::uriParamsis used only by jlog.jabber:iq:register<password/>and SASL/FAST data from<Ack>payloads. Runredact::uriParamson debugtap output.Correctness
C1. Invalid
<jinglepub from>throws; the whole message is lost (and fails again on every MAM catch-up)<jinglepub from='@@@/evil-….invalid@@' …>causes client-debug.logbgerror: Invalid JID: @@@/evil-….invalid@@. No row and no<New>is created. Any contact can make a message vanish.tacky/xmpp/modules/message.tcl:11132(ExtractSfsAttachments:jid norm [jid bare $pubFrom]with nocatch).if {[catch {jid norm [jid bare $pubFrom]} b]} { continue }, which drops that source and keeps the message.C2. IBB: a data error never closes the bytestream (callback arity), 60 s stall
<data seq='1'>is answered<unexpected-request/>, then client-debug.log showsibb callback failed: wrong # args: should be "...jinglefile OnIbbSent sid n result". No<close/>is sent, and the session ends only after the 60 s stall timer. As receiver, an out-of-sequence block terminates the Jingle session but sends no IBB<close/>.tacky/xmpp/modules/ibb.tcl:94,126callsCallback $cmd err <text>(two args).jinglefile.tcl:2711OnIbbOpened {sid result}and:2741OnIbbSent {sid n result}take one.ibb.tclpass one list ([list err $text]), or change the jinglefile methods to{sid n status args}. On error, send<close/>before unsettingOut(sid). Send<close/>on the receiver's bad-seq path.C3. Mention offsets point at the wrong text after XML-forbidden characters are stripped
'… ctl\x01 nonchar lone\uD83D @Bee end'with mention 30..34 goes out with U+0001 and U+FFFE removed, but<reference begin='30' end='34'/>now coversee e.tacky/xmpp/modules/message.tcl:3798-3800:XmlTextruns, thenCheckMentionsuses the cleaned body without shifting offsets. Edits and captions behave the same.C4. A reply fallback naming only
<subject/>is read as covering the whole body, and the reply is dropped<fallback/>covers everything).<fallback for='urn:xmpp:reply:0'><subject start='0' end='3'/></fallback>no row and no<New>are created. The same reply without<fallback/>is stored.tacky/xmpp/modules/reply.tcl:51-55:if {[llength $bodies] == 0} { lappend ranges [list 0 $len] }.[llength [dict get $fb children]] == 0.C5. service-0 notices raise
notify <Notify>docs/protocol/service-0.md§4 ("never unread, never notified").notify <Notify> {"id": "svc-pin-…", "body": "Peer did pin …"}are raised. The unread count stays correct.tacky/xmpp/modules/notify.tcl:287-293OnNewskips only content typescallandomemo_session.service(and any other notice content types).OnNewskips content typeservicebesideomemo_session; ours skipped every notice row. Timer, ttl and screenshot notices still raise<Notify>, by upstream's choice.C6. In a room, a caption is folded by origin-id rather than the room's stanza-id
<attach-to id='go2-…'/>naming the file's origin-id is folded (message <Edited> content.caption …) instead of being shown as a normal message. The author check still applies, so there is no spoofing.tacky/xmpp/modules/message.tcl:9224FoldCaption, thenmessagestore.tcl:2448rowsByTarget, which matches server_id, own_id and origin_id in rooms too.?joinchats, match attach-to againstserver_idonly.C7. XEP-0482 reject uses the wrong ids
<invite id>is declined locally, but no<reject/>is sent. The expected answer is<reject id='<origin-id>'/>. (2) In a room, the decline sends<reject id='ci7-…'/>(the invite@id) instead of the room's stanza-id. libtacky's own invites have a message id that differs from invite@id and no origin-id, so 0.1.0-only peers never match.tacky/xmpp/modules/groupcall.tcl:376-391declineuses onlyinvite id.declinefalls back toCallInviteRef(stanza-id in a room, else origin-id, own id, wire id) and answers are matched the same way (CallInvitesByRef). Upstream did not set origin-id = invite@id on our own invites (re-invites share one<invite id/>); a 0.1.0 peer's answer naming our random message id is matched byown_idinstead.C8. XEP-0492:
<advanced/>dropped when the attribute-free element is rewritten<advanced/>; SHOULD add a fallback).<notify><always><advanced><rule…/></advanced></always>…</notify>passed throughset_notification -value on-mentionbecomes…<on-mention/></notify>with no<advanced/>. Two more problems: (a) a scoped write with no setting adds no attribute-free fallback (SHOULD); (b)notify getanswers{muted: 0, mentions: 1}regardless of the bookmark setting, while<Notify>alerts do follow it.tacky/xmpp/modules/bookmarks.tcl:841-890NotifyMerge, around 867/882: the matching element is skipped and the new one getschildren {}.<advanced/>children over to the new one. Add the fallback on scoped writes. Makenotify getread the bookmark.NotifyMergecarries<advanced/>over. Not done: (a) the attribute-free fallback on a scoped write (SHOULD). (b) upstream saysnotify getalready reads the bookmark once a?joinchat is in scope (not a bug).C9. Geoloc on Prosody: the owner's node
<delete/>event ends sharing;set_allowedafter item-not-found is marked syncedset_allowed []sends iq 41<delete node=geoloc/>. Prosody pushes the owner<delete/>event, after whichgeoloc <Changed> active falsefires and the node is never republished.set_allowed [ai1b]then reads affiliations, getsitem-not-found, and setsgeoloc_allowed_dirty 0.stoppublishes under a whitelist without making ai1b a member, so ai1b reads<forbidden/>. Nothing leaks.tacky/xmpp/modules/geoloc.tcl:445-451OnNotificationtreats our owndeleteas a stop.:372clears the dirty flag after an error-ish sync.set_allowed []thenset_allowed [peer]: does the owner get its own<delete/>event, and does a peer read item-not-found/forbidden afterwards.C10. XEP-0470 attachments node made without
pubsub#max_items(Prosody keeps 1, so our own reaction is evicted)item id='ai1a@…') and the peer's, the node holds onlyai1b.tacky/xmpp/modules/microblog.tcl:688-690OnPostPublishedcreates the node withaccess_model/publish_model/persist_itemsonly.pubsub#max_items max(ascreate_feeddoes at:1140-1143).C11.
pubsub publish/createreturn a numeric-looking id altered (1E…→Inf,0x10→16)70262E937439→Inf).tacky/xmpp/modules/pubsub.tcl:1044expr {$made eq "" ? $node : $made}and:1158Done $command [expr {$given ne "" ? $given : $id}].if/else. Also grep for otherexpr {… ? $str : $str}on ids (see S9).expr ?:across 13 modules now usesif; ours touchedpubsub.tclonly. Left: the create error text (pubsub.tcl:1072,Could not create [expr {...? "a node" : $node}]) still goes throughexpr(cosmetic).C12. XEP-0153: the kept photo hash is advertised before the own vCard is downloaded
<x xmlns='vcard-temp:x:update'><photo>74f5…</photo></x>, and the own<vCard/>get goes out after it.tacky/xmpp/modules/presencemod.tcl:155-169(PhotoKnown/LoadPhoto, deliberately, for an Android cold start).<x/>until the vCard is fetched, or document the deviation in the row.fix/C12-photo-hash-before-vcard(empty<x/>until the vCard is fetched) was not taken.C13. SIMS
<file/>lacks<hash/>and<desc/><media-sharing><file><media-type/><name/><size/><thumbnail/></file>…has no hash, while the XEP-0447<file/>beside it has the sha-256 and desc.tacky/xmpp/modules/message.tcl:3462ShareNodes.<hash xmlns='urn:xmpp:hashes:2'/>and<desc/>in the SIMS file.C14.
send_albumsource messages carry a url body without the sfs fallback marker<fallback for='urn:xmpp:sfs:0'>).tacky/xmpp/modules/message.tcl:4683SourceWrite.j fallback -ns urn:xmpp:fallback:0 -for urn:xmpp:sfs:0 { j body }, assend_filedoes.C15. Outgoing inline thumbnails have no size cap (a 70 KB thumb gives a ~95 KB stanza)
tacky/xmpp/modules/file.tcl:2763thumb_data_partschecks only syntax and type.thumb_data_uri(:2756) does checkthumb_max_bytes.thumb_data_parts, return""when the decoded size is over[thumb_max_bytes].thumb_data_parts; comment and test reworded).C16. disco#info lacks
urn:ietf:params:xml:ns:vcard-4.0tacky/xmpp/modules/vcard.tcl:783adds only${NODE4}+notify.$client caps addFeature urn:ietf:params:xml:ns:vcard-4.0.C17. disco#info lacks
urn:xmpp:eme:0;name=sent for listed protocolscaps addFeature urn:xmpp:eme:0.message.tcl:3375puts-name OMEMOon theurn:xmpp:omemo:2hint.namefor listed namespaces.C18.
microblog publishleavespubsub#notify_retractfalse on a fresh nodetacky/xmpp/modules/microblog.tcl:671-675pubOptions(access_model, persist_items, max_items only).pubsub#notify_retract true pubsub#send_last_published_item never. The notes also say<script>content in html entries is passed to the host verbatim; check that the HANDBOOK warns hosts.C19. messagestore
IsDuplicatemerges two different messages (different@id, no stanza-id, same body/sender/second)tacky/xmpp/modules/messagestore.tcl:511IsDuplicate.@ids differ as duplicates.IsDuplicatetreats two stanzas that each name an id and share none as two messages, matching origin-id and@idacross each other; ours comparedwire_idwithwire_idonly.C20. Forged XEP-0425 moderation from an occupant stored as a message showing its fallback
:1<retract><moderated/>form and the legacyapply-toform became rows (message <New> 'mod-… forged fallback occupant 1').message <New>for the forged:1and:0forms.:0notice in v1.2.1 by 95ef9888 (upstream's own change; no branch of ours): a:0moderation outside a room is dropped.C21. Stream-error replay loop: the stanza that killed the stream is replayed on every new stream, with no backoff
<invalid-from/>errors and reconnects in 45 s, alwaysretry_in 1000 attempt 1.C22. On SASL2/Bind 2, post-auth stream features are ignored (no roster
ver,preapproverefused, no pre-approval)tacky/xmpp/modules/roster.tcl:251-280(StreamHasandPostAuthFeaturesUnknownsee only the Bind 2 inline list).accept_invite(ce96d4c5, commit): approval on arrival only, because ejabberd 26.09 left gateways atto. The row now says so, so the XEP-0379 part is a documented deviation. See also issue #22.C23.
message sendneither validates nor prepares JIDs (forbidden chars accepted;AI1B@…, fullwidth or final-dot JIDs become separate chats; sends stuck pending)ai2b@ejabberd.asxp.io.), so a store cleanup is needed.C24. Prosody: a vCard4 publish refused with precondition-not-met is retried unchanged after a newer one, wiping the name
C25.
account unregisternever answers on ejabberd-family servers and the removed account redialsiq.tcl:151-165. The following<conflict/>"User removed" is read as a resource clash, leading to a not-authorized loop. Minor:<remove/>carriedto=domain(SHOULD NOT).to.C26. Forwarded inner
<message/>has noxmlns='jabber:client'message.tcl:7018ForwardNodes. Minor:message forwardreturns""although the HANDBOOK says it returns a string. Its upstream status is unclear.C27. XEP-0440 rules 4/5 not enforced on SASL2
sasl.tclSelectWith.C28. An IQ result over the XEP-0478 limit is dropped instead of answered with policy-violation
C29. The
iap(XEP-0509) andhappy_eyeballs(XEP-0495) installation settings never reach the connectiontacky/xmpp/client/client.tcl:1093,1099callstacky_setting_geton the backend (no-owner), sosetting.tcl:221-233returns the default.C30. A MUC decline is sent to the room itself when the room names the inviter by occupant JID
message.tcl:10739-10747ParseInvitetakes the bare JID. Note: the scenario's R14 check should accept the inviter as given.C31. The Jingle file sender tears down on
<received/>and refuses the receiver'ssession-terminate(unknown-session)jinglefile.tcl:1839HandleSessionInfo, which goes Done, then Teardown.C32. XEP-0424: a retraction naming the
@idis ignored when origin-id differs; refused retractions are stored as messages with the fallback bodyC33. XEP-0444: the reaction set is not reverted on a not-acceptable error
message.tcl:7306SetReactions(stores the set optimistically, with no error path).C3.1. A draft holding a character XML cannot carry is never synced
009c9884, libtacky56d29734) on anotherim-server, 2026-11-08: pastehi\x01\x02into the composer (comment).chats draftkeeps the text locally, thenPublishDraftfails:bgerror: text contains U+0001, which XML cannot carry. The draft never reaches the other devices (urn:tacky:drafts:0 / sealed:1), and the error escapes the module.tacky/xmpp/modules/chatsync.tcl:972-985PublishDraftwrites<draft><body>through::tacky::core::xml::writewithoutXmlText, unlikemessage send.XmlTextbeforePublishDraft, or catch and log the write error. Test with U+0001.chatsync.tcl:983PublishDraftwrites the text as is). Upstream says 7dddfd32 for 1.2.2; not on the forge yet.C34. A destroyed room comes back on the next sign-in
009c9884, libtacky56d29734+ C3/S2) on anotherim-server, 2026-11-08 (comment). Playbook:fixes/C34/.muc destroy_roomsucceeds, but the XEP-0402 bookmark keepsautojoin='true'. On the next sign-in disco#info answersitem-not-found, libtacky joins anyway, and the server creates a new room (status 201/110, owner): empty, default config, same address.tacky/xmpp/modules/muc.tcl:2290destroy_roomonly sends the iq; nothing acts on<Destroyed>(muc.tcl:3058,:3941); autojoin ignores a disco#infoitem-not-found.item-not-found(report it), and clear autojoin after a successfuldestroy_room. Host side (anotherim-jsui/src/state/muc-room.ts:204): show the row as destroyed and offer Remove.fix/C34-destroyed-room-autojoin).Minor / SHOULD
M1.
file slot/message upload_slotrequest slots for empty files (OMEMO adds the 16-byte tag first)M2.
adhoc listreturns command nodes whosejidis not the responder'sadhoc.tcl:214-216.if {$ij ne "" && [jid norm $ij] ne [jid norm $service]} continue.jid comparedforjid norm, no debug line).M3.
message sendacceptsthread_parent == threadmessage.tcl:3271-3275(andsendat:3814).CheckThreadParentrefusesthread_parent == threadin send, send_file and send_album, and the builder never writes such a parent; unlike ours it does not refuse a parent without a thread.M4.
<attachments>in a foreign namespace is read as an attachmentmicroblog.tcl:1009(and:925) checks onlytag eq "attachments".xmlns eq $NS_ATTACH.dict getdeffor the namespace).M5.
payment get_invoicesends<get-invoice/>without a disco check#invoice-request.payment.tcl:508.get_invoiceasks disco#info first and sends only to an entity advertising#invoice-request.M6. XEP-0472 side issues
access_model=presence(XEP-0470 §4.1.1 SHOULD match).itemreply=publisher.http://URLs surface in<Post>.M7. XEP-0501 story entry lacks
atom:authorM8. A forged roster push gets no IQ reply
service-unavailable, nothing stored.M9. XEP-0198
<r/>unanswered for ~50 min (network outage 05:31-06:20 UTC) without the stream being declared dead<r/>timeout.<r/>); no change in v1.2.1.M10. XEP-0410 self-pings rooms already left, during the resume replay (Prosody ordering)
M11.
mam queryhides RSM<count>; discoTruncatedcount is empty although page 1 had<count>mam queryanswerscountfrom::tacky::rsm::parse(""when absent; ours left the key out), disco<Truncated>keeps page 1's count.M12.
account untrust_certificate -acc <registration domain>fails with "Account doesn't exist" (trust works; HANDBOOK CERTPIN names both)account trust_certificatethenuntrust_certificatewith-acc <registration domain>during an IBR flow, on 1dae0a6c.M13. tackyd-json v1.2.0-rc2 build:
::tacky::versionunset0.1and the caps node ishttps://tacky.example.::tacky::LibraryVersionanswered, caps node is the DOAP home.M14. OTR whitespace tag kept in the stored plaintext body
M15. Duplicate vCard gets for a MUC avatar (after 104 + x:update, and on join)
avatar FetchVCardkeeps one get in flight per JID and hash; a 104 and our own set ask regardless (-force).M16. Republishing another device's bookmark adds a
<nick>it lackedM17. Stale store state after server-side deletes
notify_delete false) stays in the store for good.M18. Replayed or equal-stamp sealed items are dropped without a debug line (other drops are logged)
pte_sealnow log older and equal-stamp drops. The draft path (chatsync.tcl:1128,:1131, theDrafts/Clearedchecks) still returns silently; our branch logged those too.M19. The own call-invite row reads
state joinedalthough no call started (no media backend)joinedonly while this device is in the call,pendingotherwise,elsewherefor another device's.M20. tls-server-end-point is computed for Ed25519 certificates, where RFC 5929 leaves it undefined
M21. PIE import oddities
pie-0.mdlacks<quick/>.<row/>facet. An enabled account coming up during the import is by design.M22. A refused
muc accept_inviteleaves its autojoin bookmarkM23. OMEMO list and store hygiene
publish_listdrops OMEMO 2labelattributes on republish.forget_inactiveis unsafe when contacts have older inactive rows; a per-device forget is needed.trust_listactivereads false for devices on current lists (check the meaning in the HANDBOOK).trust_listactiveis documented (listed on a list read on this connection). Left as decisions: a per-device forget, own-device rows piling up on fresh stores.M24. XEP-0377 report: the row says
stanza-id by=is the reported JID, but §6 says to copy the received<stanza-id/>mod_spam_reportingor a fake.M25. INFO-level oddities
live_stoppedis not set at expiry, although the HANDBOOK suggests it is.<fallback for=urn:fluux:poll:0/>has no<body/>, unlike libtacky's ownpoll-fluux-0.md§3.1.subscribers []and noSubscriberChanged.rdf:about-in-body check is a substring match (untested).<body/>3dad47ab, XEP-0511 whole-link check c4d00bc9, pep-expiry year bound and replaced-item pending e364005c, live-location bad move dropped andlive_stoppeddoc e9379baf, MUC/Sub nick-only subscribers 2336d35a. Left: the own device's correction of a peer's share still stored as an outgoing live row.Server-side issues (not libtacky)
<blocked xmlns='urn:xmpp:blocking:errors'/>. Runs: anotherim 221228, ejabberd 221627.<stanza-id by=recipient/>ahead of its own and keeps it in MAM. libtacky is hardened (S12). Run: prosody 222405.y("Invalid channel binding"). libtacky works around it withn(20f0aecc, commit). Runs: ejabberd 230523, anotherim 233905.mod_muc_room.erlget_occupant_initial_role→ stored role). Report to ProcessOne. Runs: ejabberd 014838, anotherim 015135.failalthough libtacky passed R1-R10; relabel it, or make the scenario chunk writes. Run: prosody 113200.127.0.0.2:7777, and public port 7777 accepts then closes. This is a config fix and the user's decision.Needs live check: disco#info of proxy65 on ejabberd.asxp.io and a connect to public :7777.
tls-exporter(XEP-0440 says servers MUST advertisetls-server-end-point). Maybe a config or version matter; not checked.Needs live check: read the stream features on prosody.asxp.io (no sign-in needed) and the channel-binding config.
Needs live check: Prosody
limits/c2s_stanza_size_limitconfig and a 20 KB stanza from a test account. arrives as a space (INFO). Run: contact-0 prosody 104729.xep_0202cannot answer time queries (set_tzo ValueError). Thexep_0080timestamp is broken. Iteratingpubsub_eventitems in a handler hangs at 100% CPU.Harness / scenario issues already fixed in the interop repo
peer.py: each Peer's wire log now holds only its own stream (commit "ai: keep each Peer's wire log to its own stream"). Runs before ~11:15 UTC on 2026-11-07 still have mixedpeer-slixmpp*.log.common.wire():splitlines()broke on raw U+0085 in libtacky JSON lines. Fixed in2633218. Other scenarios with their ownsplitlines()loops carry the same latent bug.nix/https443.nixredirected container egress on port 443 too. Fixed, so a fresh anotherim server can build its image.nix develop .#aicopied the repo into the store on every tree change, which filled the disk. Fixed withtools/ai/aish(pinned rev). Old copies are still in /nix/store.roster revoke_invite) and decide whether to rewrite history.interop-logrotated-files fix: deployed to ejabberd only (Prosody and anotherim were not redeployed).muc_features/ai{1,2,3}brows deleted (see S4). Backups are in the scratchpadstore-bak/.Not bugs: documented deviations or judgement calls
accept_invitesends no server pre-approval; it approves on arrival (upstream ce96d4c5, row updated).<received/>marker kept beside the XEP-0184 receipt (upstream 2dd5a78f documents why).preserve_history(default).<ephemeral/>in clear beside OMEMO 0.3. Documented. The OMEMO 2 case was fixed in d78fd98c (#17).application/webxdc+zipin clear, as XEP-0363 requires.pubsub#typeanditem_expireunset (the row says so).rpad/fromare refused (INFO, stricter than the spec).legacyfor legacy-field forms (INFO).sealed-1.md.fast_tokenneeds token-mechanism + user-agent-id.omemo.tclATM comment (~7532) say trust messages go to every usable device, but the code sends to authenticated devices only.<Invite>and the stored row carry it.message forward's documented return value.6 comments
Live re-checks, 2026-11-07 (rc2 vs upstream v1.2.0
1dae0a6c)All three test servers were restarted; every check is an interop run (commits
3ededa3,febb903,68490c1).geoloc stopovertaken by stale republishset_allowed []thenstopalso republishesparse_uri%3Fsplit fixed;:portstill accepted, and a%3Fin a domainpart<delete/>ends geoloc sharing (Prosody):0notice from a contact is still stored as text (INFO)<r/>never ends the stream<r/>; idle cut detected by the probeuntrust_certificate -acc <registration domain>Server-side:
127.0.0.2:7777(mod_proxy65: { access: local, max_connections: 5, port: 7777 }, noip/hostname). Test-config fix: addipandhostname: ejabberd.asxp.io.tls-exporter; ejabberd and anotherim-server advertisetls-exporterandtls-server-end-point.data:URI is one 20 KB attribute stalls the sender's stream (both directions, prosody 174914). Prosody's debug.log shows every byte RECV'd but no stanza dispatched until the client closed, no error. Prosody 13.0.7 with expat 2.9.0: expat ≥ 2.6 reparse deferral on a single large token is the likely cause. mod_limits is not loaded. The right error would be RFC 6120 §4.9.3.14<policy-violation/>.Fix branches and step-by-step playbooks for the security bugs are being prepared under
fixes/<ID>/in this repo.Fix playbooks ready (PR #2)
Every bug here now has a playbook in
fixes/on PR #2's branch. Each one covers reproducing the bug, verifying it by hand, the libtacky issue body, reviewing and testing the fix branch, retesting, and the upstream PR body.../libtacky-fixes, signed, test fails without it, v1.2.0 fail / fix pass runs)Corrections to the list above:
Branches that touch the same code: S8 with C9, and S6 with C4. Whichever merges second needs a rebase.
C3 verified by a human (2026-11-08): reproduced in anotherim-js 0.8.0-beta5 by pasting
hi\x01\x02before a picked@Nick. The highlight shifted two characters. This happened with OMEMO on, so encrypted chats are affected as well.fix/C3-mention-offsets-after-strip, rebased on develop56d29734)New: C3.1, and a second C3 symptom (found by hand in anotherim-js, 2026-11-08)
Setup: anotherim-js upstream
develop(009c9884,XML_LOG=1) with libtackydevelop56d29734, on anotherim-server as ai3a.C3.1: a draft with a character XML cannot carry is never synced (correctness)
chats draftkeeps the text locally (dict set Drafts,SaveDraft).PublishDraft(tacky/xmpp/modules/chatsync.tcl:972-985) then writes<draft><body>through::tacky::core::xml::writewithout cleaning it, asmessage senddoes withXmlText. The writer refuses the text, and the error escapes as abgerror:Effect: the draft never reaches the user's other devices (urn:tacky:drafts:0 / sealed:1), and the error escapes the module as a
bgerrorinstead of being handled there. To trigger it, pastehi\x01\x02into the composer.Suggested fix: run the draft text through
XmlText(or the same cleaning) beforePublishDraft, or catch the write error and log it. Add a test that publishes a draft holding U+0001.C3, second symptom: the message is not sent at all
If the mention is the last thing in the message, the two stripped characters push its end past the end of the cleaned body.
message sendthen refuses the whole send, because a mention out of the body is an error and nothing is sent. No stanza goes out (the XML log has none). In anotherim-js the message just doesn't send. The C3 fix (libtacky#25) moves the offsets, so the send succeeds.Side note, anotherim-js: media fetched from received messages
The test build's log shows repeated
frontend.media: fetch failed host=example.invalidwarnings. anotherim-js fetches URLs that appear in received messages, here from old test-run messages, on its own. With a real host, this would show the sender the user's IP address and that the user is online. Check which setting governs it and whether it is on by default.New: C34, a destroyed room comes back on the next sign-in (found by hand in anotherim-js, 2026-11-08)
Setup: anotherim-js upstream
develop(009c9884,XML_LOG=1) with libtackydevelop56d29734plus C3/S2 (bdb54e89), on anotherim-server as ai3a. ai3a, the owner, destroyedc3-test@conference.anotherim.asxp.iofrom the room menu.What happens:
muc destroy_roomsends<destroy/>, and the server confirms with presence<destroy/>,affiliation='none'. The room is gone.autojoin='true'.destroy_room(tacky/xmpp/modules/muc.tcl:2290) only sends the iq.<Destroyed>event (muc.tcl:3058,:3941) is not acted on bybookmarks.ui/src/state/muc-room.ts:204) only clears the room's state, so the row stays in the list.item-not-found"Conference room does not exist".201and110, affiliationowner.Effect: a room destroyed on purpose is quietly recreated by the owner's own client on every sign-in, empty, with the server's default configuration, under the same address. Anyone who knew the address can join it. The list row also stays after the destroy.
Spec: XEP-0045 §10.9 (destroying a room; the
<destroy/>with an optional alternate venue), XEP-0402 (autojoin bookmarks).Who should fix what:
libtacky (the real fix):
item-not-found"Conference room does not exist", and joined anyway. The server took that join as "create".item-not-found(report it instead), and clearautojoinafter a successfuldestroy_room. Then a bookmark left behind cannot turn a destroy into a recreate.anotherim-js (UX, a separate small item):
destroy_roompromises nothing about bookmarks.muc <Destroyed>, show the row as "destroyed" and offer Remove (bookmarks remove). Offer the alternate venue whenaltRoomis set.ui/src/state/muc-room.ts:204only clears the room's state, so the row stays as if nothing happened.Evidence: the test build's XML logs (not committed):
aim-fix-xml.loglines 1365-1371 (destroy) andaim-fix-xml-2.loglines 86, 96, 142, 144 (rejoin and recreate).Verified against libtacky v1.2.1 (https://dev.another.im/another-im/libtacky/releases/tag/v1.2.1), built on 1.2.0. One comment covering the whole list.
Security / privacy
groupcall start/inviteanswer{TACKY GROUPCALL ENCRYPTED}for an OMEMO 0.3-only peer); meeting messages are refused the same way (the URL is a bearer secret,{TACKY MEETINGS ENCRYPTED}).<active/>, the receipt request and<markable/>now go inside an OMEMO 2 message's envelope instead of beside it in the clear.legacy_file_metadata(default/unset = on, as before) by the user's ruling, not removed outright. Off (new in 1.2.1) drops the SIMS file metadata that used to sit beside OMEMO 0.3 ciphertext.stop.expr, so a numeric-looking value is no longer turned intoInfor stripped of leading zeros.parse_uri/jid validnow refuse both a decoded?in an address and a:porton the domain (the?-split half was already fixed earlier, commit cd3e39c5).<Ack>output or the XML console log.Correctness
fromno longer throws; that source is dropped and the message is kept.<subject/>no longer swallows the whole body; the reply is kept.service: service-0 notices no longer raisenotify <Notify>. Other notice types (timer, ttl, screenshot) still alert; that is a design call, not part of this bug.<reject/>names (falls back through origin-id, message id, then stanza-id in rooms); our own invites' id scheme was deliberately left as is (re-invites share one<invite id/>on purpose) rather than changed.<advanced/>is kept when the attribute-free element is rewritten. Not done: the SHOULD fallback on a scoped write with no setting. Thenotify get-ignores-bookmark report was not a bug on the base code (it already reads the bookmark once a?joinchat is in scope).<delete/>is no longer read as a stop; a rig case for this is in the test suite.pubsub#max_items max, so Prosody's default no longer evicts our own reaction.expr {... ? ... : ...}) go throughif/dict getdefinstead, same class as S9.<file/>carries<hash/>and<desc/>.send_albumsource messages carry the sfs fallback marker.urn:ietf:params:xml:ns:vcard-4.0.urn:xmpp:eme:0;name=is dropped for listed EME methods.microblog publishsetspubsub#notify_retracttrue on a fresh node.@ids are no longer merged as one duplicate by content alone.:0moderation outside a room is dropped. The:1/room-relayed case was already fixed earlier (commit 292016a5).accept_inviteapproves on arrival rather than ahead of time, because ejabberd 26.09 leaves gateways at subscriptionto.Minor / SHOULD
adhoc listdrops command nodes whose jid is not the responder's.thread_parent == threadis refused.<attachments>is read only from its own namespace.get_invoicechecks disco for#invoice-requestfirst.itemreply,xmpp:-only comments/reply_to, an edit no longer re-creating the attachments node).atom:author.service-unavailable.<r/>, as expected. No change needed.mam queryanswers the RSM<count>; disco's Truncated count keeps the page-1 value.trust_certificate/untrust_certificateare symmetric in code; a live re-check found it works while a registration session is open, which is by design otherwise.0.1; the caps node is the library's home.joinedonly while this device is actually in the call.tls-server-end-pointis the choice other XMPP implementations make, since RFC 5929 has no rule for those algorithms; the docs row says so now.accept_invitejoin restores or removes its autojoin bookmark instead of leaving it behind.forget_inactive), and pruning own-device rows that accumulate on fresh-store runs.trust_list active's per-connection meaning is documented, not changed.stanza-id by=question reads differently from the XEP's own text versus its example and is untestable on the three servers; left for the user (or the XEP's author) to decide.<body/>, a whole-link check for XEP-0511, a pep-expiry year bound plus a pending replaced item, a dropped bad live-location move, and nick-only MUC/Sub subscribers.From comment 1014 (found after the above fixes landed)
bgerror): fixed, but not in 1.2.1 — targeted at 1.2.2, commit 7dddfd32, not released yet.Follow-up to our comment 1028 (v1.2.1).
C3.1 (a draft holding a character XML cannot carry) is now fixed and released, in libtacky 1.2.2, commit 7dddfd32: https://dev.another.im/another-im/libtacky/releases/tag/v1.2.2
C34 (comment 1015, a destroyed room recreated by autojoin) is fixed in libtacky 1.2.2 and anotherim-js 0.8.0-rc8: libtacky issue 26, anotherim-js issue 95.
The media auto-download note from comment 1014: pictures and stickers now auto-download only from contacts. The setting takes contacts (default), never or anyone; the old everyone value is read as contacts and rewritten to contacts when the store opens. A room occupant is not a contact for this; a service on the account's own server, such as the sticker catalogue, counts as one.
Side note: the C34 change exposed a catch-up walk-order regression, which the release gate caught and fixed before the 1.2.2 tag (commit d04ed76a). Nothing to report there.