Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CPP: Add query for detecting invalid uses of temporary unique pointers. #15078

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

alexet
Copy link
Contributor

@alexet alexet commented Dec 12, 2023

This adds a new query for finding cases like T* x = functionReturningUniquePtr().get() where a reference to the internals of a temporary unique pointer lasts longer than the unique pointer. This is based of the c_str query and shares some of the code from it.

Copy link
Contributor

github-actions bot commented Dec 12, 2023

QHelp previews:

cpp/ql/src/Security/CWE/CWE-416/UseOfUniquePointerAfterLifetimeEnds.qhelp

Use of unique pointer after lifetime ends

Calling get on a std::unique_ptr object returns a pointer to the underlying allocations. When the std::unique_ptr object is destroyed, the pointer returned by get is no longer valid. If the pointer is used after the std::unique_ptr object is destroyed, then the behavior is undefined.

Recommendation

Ensure that the pointer returned by get does not outlive the underlying std::unique_ptr object.

Example

The following example gets a std::unique_ptr object, and then converts the resulting unique pointer to a pointer using get so that it can be passed to the work function. However, the std::unique_ptr object is destroyed as soon as the call to get returns. This means that work is given a pointer to invalid memory.

#include <memory>
std::unique_ptr<T> getUniquePointer();
void work(const T*);

// BAD: the unique pointer is deallocated when `get` returns. So `work`
// is given a pointer to invalid memory.
void work_with_unique_ptr_bad() {
  const T* combined_string = getUniquePointer().get();
  work(combined_string);
}

The following example fixes the above code by ensuring that the pointer returned by the call to get does not outlive the underlying std::unique_ptr objects. This ensures that the pointer passed to work points to valid memory.

#include <memory>
std::unique_ptr<T> getUniquePointer();
void work(const T*);

// GOOD: the unique pointer outlives the call to `work`. So the pointer
// obtainted from `get` is valid.
void work_with_unique_ptr_good() {
  auto combined_string = getUniquePointer();
  work(combined_string.get());
}

References

Copy link
Contributor

@MathiasVP MathiasVP left a comment

Choose a reason for hiding this comment

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

I couldn't help taking a peek at the query, and this looks super good already!

@alexet alexet marked this pull request as ready for review December 12, 2023 16:44
@alexet alexet requested a review from a team as a code owner December 12, 2023 16:44
@alexet alexet marked this pull request as draft December 12, 2023 16:45
Copy link
Contributor

@MathiasVP MathiasVP left a comment

Choose a reason for hiding this comment

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

The code (and docs) LGTM! We might as well do a DCA run for good measure while we wait for the docs team to review the documentation.

Oh, we do need a change note, though

@alexet alexet marked this pull request as ready for review December 12, 2023 16:48
Fix spelling in query id

Co-authored-by: Mathias Vorreiter Pedersen <mathiasvp@github.com>
* Holds if the value of `e` outlives the enclosing full expression. For
* example, because the value is stored in a local variable.
*/
predicate outlivesFullExpr(Expr e) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I want to point out that this has changed as well as been moved so the changes need reviewing as well

Now that I think about it I should have probably moved in a separate commit to the actual changes. I don't mind doing that if you want.

Copy link
Contributor

@MathiasVP MathiasVP Dec 13, 2023

Choose a reason for hiding this comment

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

Oh, you're right. Thanks for pointing that out. I can see that there are some subtle changes here 🤔 Let me just think about the implication of this

Copy link
Contributor

@MathiasVP MathiasVP Dec 13, 2023

Choose a reason for hiding this comment

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

I think the inclusion of not e.getConversion*().hasLValueToRValueConversion() in all the cases is a bad idea. For instance, consider this example that we currently catch on main:

const char* s = "hello";
const char* s13 = b1 ? std::string("hello").c_str() : s;

this is bad because of temporary std::string string is destroyed, but because there's an lvalue-to-rvalue conversion on the other branch of the ternary we won't flag this case any longer.

Copy link
Contributor

Choose a reason for hiding this comment

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

... We should actually add this test to the UseOfStringAfterLifetimeEnds testcases

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I seems that the entire conditional is marked as having an hasLValueToRValueConversion as well as the branch s. But surely the conversion only needs to happen for s. The clang AST shows that. However our AST doesn't. Reading cppreference it suggest this is an rvalue so we don't need a a conversion here.

I can fix this by looking for this specific case but am I misunderstanding this case?

Copy link
Contributor

@MathiasVP MathiasVP Dec 13, 2023

Choose a reason for hiding this comment

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

No, I think you're understanding this correctly. I think this is case 6.1 in https://en.cppreference.com/w/cpp/language/operator_other#Conditional_operator. I think we should just exclude the ternary case from the not e.getConversion*().hasLValueToRValueConversion() conjunct.

Copy link
Contributor

@MathiasVP MathiasVP left a comment

Choose a reason for hiding this comment

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

Only a single suggestion, but otherwise this LGTM! Let's see if we can get a docs review in before your holidays start 🤞. Otherwise, I'll be happy to pick this up on Monday

@@ -190,6 +190,8 @@ const char* test1(bool b1, bool b2) {
char* s9;
s9 = std::string("hello").data(); // BAD

const char* s13 = b1 ? std::string("hello").c_str() : s1; // BAD
Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks for adding this test 🙇

cpp/ql/src/Security/CWE/CWE-416/Temporaries.qll Outdated Show resolved Hide resolved
@MathiasVP MathiasVP added the ready-for-doc-review This PR requires and is ready for review from the GitHub docs team. label Dec 15, 2023
alexet and others added 2 commits December 15, 2023 11:23
Co-authored-by: Mathias Vorreiter Pedersen <mathiasvp@github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C++ documentation ready-for-doc-review This PR requires and is ready for review from the GitHub docs team.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants