"Last seen complete" column lies #17354

Open
opened 2026-02-22 03:48:18 -05:00 by deekerman · 13 comments
Owner

Originally created by @cheater on GitHub (Dec 11, 2025).

qBittorrent & operating system versions

5.1.4 Win11 x64

What is the problem?

I just added this torrent, it has only one leech at 45%, but "last seen complete" column is showing a date 1 minute ago (when I added the torrent). In this situation it should be empty.

Image

Steps to reproduce

No response

Additional context

No response

Originally created by @cheater on GitHub (Dec 11, 2025). ### qBittorrent & operating system versions 5.1.4 Win11 x64 ### What is the problem? I just added this torrent, it has only one leech at 45%, but "last seen complete" column is showing a date 1 minute ago (when I added the torrent). In this situation it should be empty. <img width="2768" height="452" alt="Image" src="https://github.com/user-attachments/assets/dec63991-54ed-4a27-affc-bf9bd13b4a65" /> ### Steps to reproduce _No response_ ### Additional context _No response_
Author
Owner

@schnurlos commented on GitHub (Dec 13, 2025):

The one "last seen" could be an IP you have banned or got blocked by qbittorrent.

@schnurlos commented on GitHub (Dec 13, 2025): The one "last seen" could be an IP you have banned or got blocked by qbittorrent.
Author
Owner

@cheater commented on GitHub (Dec 13, 2025):

"or got blocked by qbittorrent" - what does that mean?

I've never banned any peers

@cheater commented on GitHub (Dec 13, 2025): "or got blocked by qbittorrent" - what does that mean? I've never banned any peers
Author
Owner

@thalieht commented on GitHub (Dec 13, 2025):

the time when we, or one of our peers, last saw a complete copy of
this torrent.

"Last seen complete" column lies

You can't know that.

@thalieht commented on GitHub (Dec 13, 2025): >the time when we, **or one of our peers**, last saw a complete copy of this torrent. >"Last seen complete" column lies You can't know that.
Author
Owner

@LookForward2 commented on GitHub (Jan 4, 2026):

I just added this torrent, it has only one leech at 45%, but "last seen complete" column is showing a date 1 minute ago (when I added the torrent). In this situation it should be empty.

In a DHT network there are torrent clients that were modified (patched) to announce allegedly they are seeds but in fact they are not. I've seen lots of such fake seeds have IPs from one IP sub-network. Some company seems doing it on purpose

@LookForward2 commented on GitHub (Jan 4, 2026): > I just added this torrent, it has only one leech at 45%, but "last seen complete" column is showing a date 1 minute ago (when I added the torrent). In this situation it should be empty. In a DHT network there are torrent clients that were modified (patched) to announce allegedly they are seeds but in fact they are not. I've seen lots of such fake seeds have IPs from one IP sub-network. Some company seems doing it on purpose
Author
Owner

@peterdrier commented on GitHub (Jan 13, 2026):

I watched this for a good half hour once and I think I figured it out.

When new connections come in / are created, during their initial handshake, they seem to display 100% done for a split second before they actually show how much they really have. It's enough to make the last seen complete reset to now, but the remote site doesn't actually have the complete file (or immediately leaves..)

My gut says it's either a default value logic bug. i.e. defaults to 100% to avoid a divide by 0 type error at init. Or possibly a rogue agent out there connecting, lying about the completeness, and then immediately disconnecting.

If the latter, perhaps a check in the "last seen" update logic, to only allow updates to now() when the connection is providing file bytes? i.e. Only update the last seen when a remote client has started downloading to me, and they're at 100%. As it's purely an informational field, being overly aggressive about not updating it to now shouldn't harm anything.

@peterdrier commented on GitHub (Jan 13, 2026): I watched this for a good half hour once and I think I figured it out. When new connections come in / are created, during their initial handshake, they seem to display 100% done for a split second before they actually show how much they really have. It's enough to make the last seen complete reset to now, but the remote site doesn't actually have the complete file (or immediately leaves..) My gut says it's either a default value logic bug. i.e. defaults to 100% to avoid a divide by 0 type error at init. Or possibly a rogue agent out there connecting, lying about the completeness, and then immediately disconnecting. If the latter, perhaps a check in the "last seen" update logic, to only allow updates to now() when the connection is providing file bytes? i.e. Only update the last seen when a remote client has started downloading to me, and they're at 100%. As it's purely an informational field, being overly aggressive about not updating it to now shouldn't harm anything.
Author
Owner

@peterdrier commented on GitHub (Jan 13, 2026):

Seems this is actually a bug in libtorrent, specifically peer_connection.cpp:

The original, flawed logic in the is_seed() function was:
1 return m_num_pieces == m_have_piece.size()
2 && m_num_pieces > 0 && t && t->valid_metadata();

Here's why this was incorrect:

  • m_have_piece.size(): This returns the total capacity of the peer's bitfield, which is the total number of pieces in
    the torrent. It does not tell you how many of those pieces the peer actually has.
  • m_num_pieces: This variable also holds the total number of pieces in the torrent.

During the initial handshake, the m_have_piece bitfield might be initialized to its full size, but with all bits set
to 0 (indicating the peer has no pieces yet). At that moment, m_num_pieces == m_have_piece.size() would evaluate to
true (e.g., 1000 == 1000), even though the peer had none of the actual pieces. This would incorrectly identify the
peer as a seed.

The fix, using m_have_piece.all_set(), is much more accurate:

1 return m_have_piece.all_set()
2 && m_num_pieces > 0 && t && t->valid_metadata();

m_have_piece.all_set() checks if every single bit in the m_have_piece bitfield is set to 1. A bit being 1 signifies
that the peer has that corresponding piece. Therefore, m_have_piece.all_set() only returns true if and only if the
peer has all the pieces of the torrent.

I'll see if I can get a patch going on the libtorrent side...

@peterdrier commented on GitHub (Jan 13, 2026): Seems this is actually a bug in libtorrent, specifically peer_connection.cpp: The original, flawed logic in the is_seed() function was: 1 return m_num_pieces == m_have_piece.size() 2 && m_num_pieces > 0 && t && t->valid_metadata(); Here's why this was incorrect: * m_have_piece.size(): This returns the total capacity of the peer's bitfield, which is the total number of pieces in the torrent. It does not tell you how many of those pieces the peer actually has. * m_num_pieces: This variable also holds the total number of pieces in the torrent. During the initial handshake, the m_have_piece bitfield might be initialized to its full size, but with all bits set to 0 (indicating the peer has no pieces yet). At that moment, m_num_pieces == m_have_piece.size() would evaluate to true (e.g., 1000 == 1000), even though the peer had none of the actual pieces. This would incorrectly identify the peer as a seed. The fix, using m_have_piece.all_set(), is much more accurate: 1 return m_have_piece.all_set() 2 && m_num_pieces > 0 && t && t->valid_metadata(); m_have_piece.all_set() checks if every single bit in the m_have_piece bitfield is set to 1. A bit being 1 signifies that the peer has that corresponding piece. Therefore, m_have_piece.all_set() only returns true if and only if the peer has all the pieces of the torrent. I'll see if I can get a patch going on the libtorrent side...
Author
Owner

@cheater commented on GitHub (Jan 15, 2026):

thank you for looking into this

On Tue, Jan 13, 2026 at 5:58 PM Peter Drier @.***>
wrote:

peterdrier left a comment (qbittorrent/qBittorrent#23592)
https://github.com/qbittorrent/qBittorrent/issues/23592#issuecomment-3745407145

Seems this is actually a bug in libtorrent, specifically
peer_connection.cpp:

The original, flawed logic in the is_seed() function was:
1 return m_num_pieces == m_have_piece.size()
2 && m_num_pieces > 0 && t && t->valid_metadata();

Here's why this was incorrect:

  • m_have_piece.size(): This returns the total capacity of the peer's
    bitfield, which is the total number of pieces in
    the torrent. It does not tell you how many of those pieces the peer
    actually has.
  • m_num_pieces: This variable also holds the total number of pieces in
    the torrent.

During the initial handshake, the m_have_piece bitfield might be
initialized to its full size, but with all bits set
to 0 (indicating the peer has no pieces yet). At that moment, m_num_pieces
== m_have_piece.size() would evaluate to
true (e.g., 1000 == 1000), even though the peer had none of the actual
pieces. This would incorrectly identify the
peer as a seed.

The fix, using m_have_piece.all_set(), is much more accurate:

1 return m_have_piece.all_set()
2 && m_num_pieces > 0 && t && t->valid_metadata();

m_have_piece.all_set() checks if every single bit in the m_have_piece
bitfield is set to 1. A bit being 1 signifies
that the peer has that corresponding piece. Therefore,
m_have_piece.all_set() only returns true if and only if the
peer has all the pieces of the torrent.

I'll see if I can get a patch going on the libtorrent side...


Reply to this email directly, view it on GitHub
https://github.com/qbittorrent/qBittorrent/issues/23592#issuecomment-3745407145,
or unsubscribe
https://github.com/notifications/unsubscribe-auth/AABPWPR2X4EXTNGIPZHCOZD4GUPZXAVCNFSM6AAAAACOYEUCXWVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZTONBVGQYDOMJUGU
.
You are receiving this because you authored the thread.Message ID:
@.***>

@cheater commented on GitHub (Jan 15, 2026): thank you for looking into this On Tue, Jan 13, 2026 at 5:58 PM Peter Drier ***@***.***> wrote: > *peterdrier* left a comment (qbittorrent/qBittorrent#23592) > <https://github.com/qbittorrent/qBittorrent/issues/23592#issuecomment-3745407145> > > Seems this is actually a bug in libtorrent, specifically > peer_connection.cpp: > > The original, flawed logic in the is_seed() function was: > 1 return m_num_pieces == m_have_piece.size() > 2 && m_num_pieces > 0 && t && t->valid_metadata(); > > Here's why this was incorrect: > > - m_have_piece.size(): This returns the total capacity of the peer's > bitfield, which is the total number of pieces in > the torrent. It does not tell you how many of those pieces the peer > actually has. > - m_num_pieces: This variable also holds the total number of pieces in > the torrent. > > During the initial handshake, the m_have_piece bitfield might be > initialized to its full size, but with all bits set > to 0 (indicating the peer has no pieces yet). At that moment, m_num_pieces > == m_have_piece.size() would evaluate to > true (e.g., 1000 == 1000), even though the peer had none of the actual > pieces. This would incorrectly identify the > peer as a seed. > > The fix, using m_have_piece.all_set(), is much more accurate: > > 1 return m_have_piece.all_set() > 2 && m_num_pieces > 0 && t && t->valid_metadata(); > > m_have_piece.all_set() checks if every single bit in the m_have_piece > bitfield is set to 1. A bit being 1 signifies > that the peer has that corresponding piece. Therefore, > m_have_piece.all_set() only returns true if and only if the > peer has all the pieces of the torrent. > > I'll see if I can get a patch going on the libtorrent side... > > — > Reply to this email directly, view it on GitHub > <https://github.com/qbittorrent/qBittorrent/issues/23592#issuecomment-3745407145>, > or unsubscribe > <https://github.com/notifications/unsubscribe-auth/AABPWPR2X4EXTNGIPZHCOZD4GUPZXAVCNFSM6AAAAACOYEUCXWVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZTONBVGQYDOMJUGU> > . > You are receiving this because you authored the thread.Message ID: > ***@***.***> >
Author
Owner

@peterdrier commented on GitHub (Jan 28, 2026):

Turns out it's a straight up malicious actor coming from a russian ip address block, who seems to be scanning the DHT network, and pinging various torrents every few minutes. Not sure if this is a way of scraping what's going on downloads wise, an intentionally hack of the last seen complete field, or what.

The code above wasn't actually a fix, but have found another change for libtorrent (hopefully makes the 2.0 rc) which will improve the situation.

@peterdrier commented on GitHub (Jan 28, 2026): Turns out it's a straight up malicious actor coming from a russian ip address block, who seems to be scanning the DHT network, and pinging various torrents every few minutes. Not sure if this is a way of scraping what's going on downloads wise, an intentionally hack of the last seen complete field, or what. The code above wasn't actually a fix, but have found another change for libtorrent (hopefully makes the 2.0 rc) which will improve the situation.
Author
Owner

@cheater commented on GitHub (Jan 29, 2026):

thanks for the update.

there are a few options that come to my mind on how to alleviate this:

  1. ip banlist for "last seen complete" feature
  2. "slow registration" - only register as last seen complete when the seed hangs around
  3. zero-trust design - torrent only marked last seen complete if download tests succeed (in its simplest form: a random - not sequential from the start - block downloads successfully). if download of any advertised block from a peer does not succeed, they are banned from completion data for this torrent (what should the criteria be for "download did not succeed"?)

what did the libtorrent change do?

@cheater commented on GitHub (Jan 29, 2026): thanks for the update. there are a few options that come to my mind on how to alleviate this: 1. ip banlist for "last seen complete" feature 2. "slow registration" - only register as last seen complete when the seed hangs around 3. zero-trust design - torrent only marked last seen complete if download tests succeed (in its simplest form: a random - not sequential from the start - block downloads successfully). if download of any advertised block from a peer does not succeed, they are banned from completion data for this torrent (what should the criteria be for "download did not succeed"?) what did the libtorrent change do?
Author
Owner

@cheater commented on GitHub (Jan 29, 2026):

you previously thought this was a bug in libtorrent, but now you believe it's a malicious peer. is what made you believe it was a bug in libtorrent still valid after the discovery of the malicious peer? and do you still believe there is a bug?

@cheater commented on GitHub (Jan 29, 2026): you previously thought this was a bug in libtorrent, but now you believe it's a malicious peer. is what made you believe it was a bug in libtorrent still valid after the discovery of the malicious peer? and do you still believe there is a bug?
Author
Owner

@peterdrier commented on GitHub (Feb 1, 2026):

bug was believed to be a race condition at connection. That turned out not to be the case, and instead was a malicious actor, basically spamming the DHT network with 100% availability pings (possibly while scanning the network itself?)

bug has a specific definition, which this isn't. Conceptually though, this is undesired behavior. So changing the definition of how last_seen_complete functions internally is a way forward, excluding the behavior of the malicious behavior. the maintainer of libtorrent agreed, and given enough cycles the change will get into a release.

@peterdrier commented on GitHub (Feb 1, 2026): bug was believed to be a race condition at connection. That turned out not to be the case, and instead was a malicious actor, basically spamming the DHT network with 100% availability pings (possibly while scanning the network itself?) bug has a specific definition, which this isn't. Conceptually though, this is undesired behavior. So changing the definition of how last_seen_complete functions internally is a way forward, excluding the behavior of the malicious behavior. the maintainer of libtorrent agreed, and given enough cycles the change will get into a release.
Author
Owner

@cheater commented on GitHub (Feb 1, 2026):

amazing, thank you for elaborating. I really appreciate your work.

@cheater commented on GitHub (Feb 1, 2026): amazing, thank you for elaborating. I really appreciate your work.
Author
Owner

@cheater commented on GitHub (Feb 21, 2026):

I wonder if this is related? https://www.youtube.com/watch?v=VZ-7t1fN7eI

@cheater commented on GitHub (Feb 21, 2026): I wonder if this is related? https://www.youtube.com/watch?v=VZ-7t1fN7eI
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/qBittorrent#17354
No description provided.