Skip to content

[improve][misc][WIP] Detect "double release" and "use after release" bugs with recycled objects#22110

Draft
lhotari wants to merge 4 commits into
apache:masterfrom
lhotari:lh-detect-double-release-bugs
Draft

[improve][misc][WIP] Detect "double release" and "use after release" bugs with recycled objects#22110
lhotari wants to merge 4 commits into
apache:masterfrom
lhotari:lh-detect-double-release-bugs

Conversation

@lhotari

@lhotari lhotari commented Feb 23, 2024

Copy link
Copy Markdown
Member

Motivation

In Pulsar, users have reported issues that could be caused by "double release" or "use after release" bugs with recycled objects.

Here's are some issues that could potentially be caused by "double release" or "use after release" bugs:
#22035
#21892

Other example of such potential issue: #21421/#21933. It is possible that these issues are fixed by apache/bookkeeper#4196. The other root cause could be a "double release" or "use after release" bug which is corrupting the buffer and causing checksum calculation to fail.

Outside of Java, there's a known bug pattern called "double-free" or "Doubly freeing memory" (with malloc).
The "double release" bug pattern is a bit similar, but happens with the recycled object pattern using Netty's Recycler that Pulsar uses because of performance reasons.

There is also a "use after free" bug pattern. Something similar could be happen with Netty recycled objects that the object instance gets used after releasing.

The solution in this PR attempts to help detect "double release" and "use after release" bugs.

Modifications

  • get rid of any .setRefCnt(1) calls in production code since that could hide real issues
  • add additional detection feature that can be disabled by setting the system property -Dpulsar.refcount.check.on_access=false.
    • the refcount check will add a volatile read. This isn't a performance problem in most use cases. For production usage we could add -Dpulsar.refcount.check.on_access=false into the bin/pulsar script by default if we are afraid of the performance overhead. For all tests, we should be running with checks enabled so that we could find the source of problems.

Documentation

  • doc
  • doc-required
  • doc-not-needed
  • doc-complete

@lhotari lhotari added this to the 3.3.0 milestone Feb 23, 2024
@lhotari lhotari self-assigned this Feb 23, 2024
@github-actions github-actions Bot added the doc-not-needed Your PR changes do not impact docs label Feb 23, 2024
@lhotari lhotari changed the title WIP Detect double release bugs with recycled objects [improve][misc] WIP Detect double release bugs with recycled objects Feb 23, 2024
@lhotari lhotari marked this pull request as ready for review February 23, 2024 16:03
@lhotari lhotari changed the title [improve][misc] WIP Detect double release bugs with recycled objects [improve][misc] WIP Detect "double release" and "use after release" bugs with recycled objects Feb 23, 2024
@lhotari lhotari changed the title [improve][misc] WIP Detect "double release" and "use after release" bugs with recycled objects [improve][misc][WIP] Detect "double release" and "use after release" bugs with recycled objects Feb 23, 2024
@lhotari

lhotari commented Feb 23, 2024

Copy link
Copy Markdown
Member Author

After making the changes, there are a lot of unit test failures. I haven't had a chance to look into the details. This PR is still a very early proposal about how to start detecting "double release" and "use after release" bugs.

@BewareMyPower BewareMyPower left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The recycled objects are widely used in Pulsar, not only for EntryImpl. The 1st concern is that should we apply checks to all these places? For example, the client side could also use recycled objects.

The 2nd concern is, I'm afraid currently Pulsar allows a recycled object is accessed with a "null check". We need to investigate such cases.

Comment thread managed-ledger/src/main/java/org/apache/bookkeeper/mledger/impl/EntryImpl.java Outdated
@lhotari

lhotari commented Feb 26, 2024

Copy link
Copy Markdown
Member Author

The recycled objects are widely used in Pulsar, not only for EntryImpl. The 1st concern is that should we apply checks to all these places? For example, the client side could also use recycled objects.

@BewareMyPower Yes that's a good point. It looks like the AbstractCASReferenceCounted/AbstractReferenceCounted base class prevents a lot of the problems since there would be exceptions at some point if there would be execution paths where "double release" or "use after release" bugs existed.

The 2nd concern is, I'm afraid currently Pulsar allows a recycled object is accessed with a "null check". We need to investigate such cases.

Yes, those are cases where the problem is getting hidden.

It would be great to find a better way to track down the issues. That's why I started this PR which is more like an experiment to find some way that would work for detecting "double release" and "use after release" bugs and also raise awareness of such bug patterns with the recycled objects. These are bug patterns that most Java developers have never had to deal with because of Java's garbage collection. With recycled objects and Netty ByteBufs, that all changes.

@lhotari

lhotari commented Feb 26, 2024

Copy link
Copy Markdown
Member Author

I guess there's also the possibility of "double release" and "use after release" bugs with Netty ByteBufs.

In Netty, there's the leak detector for detecting when you don't release buffers, but there seems to be nothing for detecting the "use after release" bugs. I guess "double release" would be detected with the io.netty.buffer.AbstractReferenceCountedByteBuf base class which will throw an exception on double release.
It leaves the "use after release" bugs undetected.

It seems that the solution might be a Java Agent written with Byte Buddy etc. which would add additional checks with byte code instrumentation when the agent is activated. Thinking something like https://github.com/reactor/BlockHound but for a completely different purpose, to help detect "use after release" bugs.

@lhotari lhotari marked this pull request as draft February 26, 2024 06:03
@lhotari

lhotari commented Feb 26, 2024

Copy link
Copy Markdown
Member Author

The 2nd concern is, I'm afraid currently Pulsar allows a recycled object is accessed with a "null check". We need to investigate such cases.

Getting back to this one more time. Netty protects against most "use after release" ByteBuf bugs by setting the fields to null and the NPEs would be popping up as a sign of issues. Therefore it's extremely important that NPEs aren't suppressed with null checks.

@onobc onobc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nice work @lhotari

My comments are mostly questions - 1 minor suggestion on naming. Other than that LGTM.

Also, because it is coupled w/ the "emergency brake" property , it seems harmless to add in .

The remaining piece will be to find out where we are currently doing the null checks and remove them.

return entry;
}

private static EntryImpl getEntryFromRecycler() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

👍🏻 factoring this out makes it easier to follow (and add other common behaviors in one place later if needed - basic DRY stuff).

setRefCnt(1);
}

public static <T extends AbstractValidatingReferenceCounted> T getAndCheck(Recycler<T> recycler) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[NIT] It is not really doing a "check". In the ACRC it was called getEntryFromRecycler. I think it would be helpful to name them the same thing (sans "entry"), maybe getInstanceFromRecycler.

}

public final void resetRefCnt() {
setRefCnt(1);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It is unfortunate that setRefCnt is protected. Otherwise, this could have been done in a single util rather than inserted into the hierarchy (composition over inheritance). Also, the code in both hierarchies is almost identical.

  • The checkOnAccess var is just a static lookup.
  • The checkRefCount and getFromRecycler could be passed in the counter or recycler, respectively

Not the end of the world but my brain is having trouble leaving it alone.

public static ChunkedMessageCtx get(int totalChunks) {
ChunkedMessageCtx chunkedMessageCtx = RECYCLER.get();
chunkedMessageCtx.setRefCnt(totalChunks);
ChunkedMessageCtx chunkedMessageCtx = getAndCheck(RECYCLER);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is setting ref count to 1 and then retaining N-1 less performant than setting ref count to N?

OR

Is setting ref count to 1 and then retaining N-1 more functionally correct than setting ref count to N?

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants