Skip to content

fix: race condition on shared account info causes KeyError during download#4769

Open
waldh4ri wants to merge 1 commit into
pyload:developfrom
waldh4ri:fix/account-info-race-condition
Open

fix: race condition on shared account info causes KeyError during download#4769
waldh4ri wants to merge 1 commit into
pyload:developfrom
waldh4ri:fix/account-info-race-condition

Conversation

@waldh4ri

Copy link
Copy Markdown

Problem

BaseAccount.choose() clears self.info before resyncing it for the newly selected user:

self.user = user
self.info.clear()
self.req.close()
...
if not self.logged:
    self.relogin()

The account plugin instance is shared across every download thread for a given hoster (AccountManager.get_account_plugin caches a single instance per plugin). Downloader plugins read self.account.info["login"]["password"] directly, without holding the account's lock.

When one thread runs choose() and clears info while another thread is mid-download reading from it, the reader can observe an empty dict and crash:

File "pyload/plugins/downloaders/AlldebridCom.py", line 83, in handle_premium
    "apikey": self.account.info["login"]["password"],
              ~~~~~~~~~~~~~~~~~^^^^^^^^^^
KeyError: 'login'

This affects every downloader that reads account.info["login"][...] directly (AlldebridCom, PremiumizeMe, RealDebrid, TorboxApp, FikperCom, ZeveraCom, and others), and shows up intermittently under concurrent downloads, causing some files in a batch to fail while others succeed.

A prior change (add some more locks) added @lock to login(), logout(), and sync(), but the readers in the downloader plugins never acquire that lock, so the empty-dict window was never actually closed.

Fix

Replace self.info.clear() with self.sync(). sync() already runs a few lines later via the self.logged check, so the explicit clear is redundant. Calling sync() directly rebuilds info["login"]/info["data"] for the new user in place, so self.info transitions straight from the old user's data to the new user's data and is never left empty for a reader on another thread to observe.

@CLAassistant

CLAassistant commented Jul 18, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

…nload

BaseAccount.choose() cleared self.info before resyncing it for the
newly selected user. The account plugin instance is shared across all
download threads for a given hoster, and downloader plugins read
self.account.info["login"]["password"] without holding the account
lock. When one thread cleared info while another was mid-download,
the reader could see an empty dict and crash.

Replace the clear with sync(), which rebuilds info for the current
user directly, so readers never observe an empty dict.
@waldh4ri
waldh4ri force-pushed the fix/account-info-race-condition branch from cdedff7 to 3eb06a3 Compare July 18, 2026 16:00

else:
self.user = user
self.info.clear()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think calling clear() and then sync() would be better, please check and report

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would reintroduce the race (with a shorter window) and could lead to the same KeyError. self.info update mechanism must be atomic because we don't acquire self.account.lock before reading info in any of hoster.py/AlldebridCom.py/etc. sync update to info is atomic and never leave it in a blank state:

d = {"login": {}, "data": {}}
for k, v in u.items():
    ...
self.info.update(d)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants