Skip to content

[-Wunsafe-buffer-usage][BoundsSafety] Extend safe patterns of arguments to counted_by parameters #9971

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

Merged
merged 4 commits into from
Feb 12, 2025

Conversation

ziqingluo-90
Copy link

Add support of

  • overloaded subscript operator of hardened view/containers, and
  • calls to const member functions of 0 arguments

Now it can match patterns like:

std::span<char> D, S;
std::array<std::span<char>, 10> A;

memcpy(D.first(S.size()).data(), S.data(), S.size());
memcpy(D.first(A[x].size()).data(), A[x].data(), A[x].size());

(rdar://143986251)

…ts to counted_by parameters

Add support of
- overloaded subscript operator of hardened view/containers, and
- calls to const member functions of 0 arguments

Now it can match patterns like:

```
std::span<char> D, S;
std::array<std::span<char>, 10> A;

memcpy(D.first(S.size()).data(), S.data(), S.size());
memcpy(D.first(A[x].size()).data(), A[x].data(), A[x].size());
```

(rdar://143986251)
@ziqingluo-90
Copy link
Author

CC: @jkorous-apple @dtarditi

@ziqingluo-90
Copy link
Author

@swift-ci test
@swift-ci test llvm

Visit(SelfOpCall->getArg(1), OtherOpCall->getArg(1));
}
return false;
}

Choose a reason for hiding this comment

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

Why do we check for overloaded operator[] and not just any operator[]? Why do we need to check for hardened containers/view instead of any container?
Is this related to how the analysis work and gadgets are matched? Say, if we allow operator[] on a pointer here and consider the call safe, then other gadget for pointer arithmetic won't match?

Copy link
Author

Choose a reason for hiding this comment

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

I think you are right. It should be just any operator[].

if (MD->isConst() && MD->param_empty()) {
if (auto *OtherCall =
dyn_cast<CXXMemberCallExpr>(Other->IgnoreParenImpCasts())) {
if (OtherCall->getMethodDecl() == MD)

Choose a reason for hiding this comment

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

Is the pointer MD and OtherCall->getMethodDecl() different if they are in different template instances?

Copy link
Author

Choose a reason for hiding this comment

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

They should be different. I will add a test for it.

Copy link
Author

Choose a reason for hiding this comment

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

hmm... having an integrated test for the case where only methods are different is actually difficult. Because if methods are from different template instances, the implicit objects cannot be identical. Eventually, the test does not tell which condition fails the compatibility checking.

const T *data() const noexcept;
T *data() noexcept;
size_t size() const noexcept;
T& operator[](size_t idx) const {return *data_ptr;}

Choose a reason for hiding this comment

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

  1. I think just a declaration without the implementation would be fine and consistent with above declarations. Then, you could remove T * data_ptr above.
  2. const operator[] should return const T &. You probably can drop const from operator[] to make the tests work.
  3. Nit: clang-format T& operator[] -> T &operator[].

Copy link
Author

@ziqingluo-90 ziqingluo-90 Feb 10, 2025

Choose a reason for hiding this comment

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

It seems to require a definition when the template parameter T has no linkage: https://godbolt.org/z/4MP4afqq5. I didn't know this before. I will fix it.

@@ -40,6 +42,7 @@ struct span {
span<T> first(size_t count) const noexcept;
span<T> last(size_t count) const noexcept;
span<T> subspan(size_t offset, size_t count) const noexcept;
T& operator[](size_t idx) const {return *data();};

Choose a reason for hiding this comment

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

Same here.

cb_cint(S.first(ttp->tp->a[x]).data(), ttp->tp->a[x]);
cb_cint(S.first(t.a[x]).data(), tp->a[x]); // expected-warning{{unsafe assignment to function parameter of count-attributed type}}
cb_cint(S.first(tt.t.a[x]).data(), tt.tp->a[x]); // expected-warning{{unsafe assignment to function parameter of count-attributed type}}
cb_cint(S.first(ttp->t.a[x]).data(), ttp->tp->a[x]); // expected-warning{{unsafe assignment to function parameter of count-attributed type}}

Choose a reason for hiding this comment

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

Could you add a few tests where subscript is in a member base? For example:

    std::array<TT, 10> arr;
    cb_cint(S.first(arr[1].t.a[x]).data(), arr[1].t.a[x]);
    cb_cint(S.first(arr[1].t.a[x]).data(), arr[2].t.a[x]); // warn

if (const auto *OtherME =
dyn_cast<MemberExpr>(Other->IgnoreParenImpCasts())) {
return Self->getMemberDecl() == OtherME->getMemberDecl() &&
Visit(Self->getBase(), OtherME->getBase());

Choose a reason for hiding this comment

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

Will this work if the member base is CXXThisExpr? For example:

struct Foo {
  std::array<int, 10> A;

  void foo(std::span<int> S) {
    cb_cint(S.first(A[5]).data(), A[5]);
  }
};

We probably can implement VisitCXXThisExpr to make it work. Moreover, if this works, we might be able to remove isSameMemberBase.

Copy link
Author

Choose a reason for hiding this comment

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

Yeah, I should have added tests of this case. I think it works. But why we can remove isSameMemberBase?

Choose a reason for hiding this comment

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

We are using here Visit(Self->getBase(), OtherME->getBase()) to check if the base is the same. Couldn't we replace isSameMemberBase(Self->getBase(), MemberBase) above by Visit(Self->getBase(), MemberBase) to achieve the same?

Copy link
Author

Choose a reason for hiding this comment

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

I see what you mean. I agree.

@ziqingluo-90
Copy link
Author

@swift-ci test
@swift-ci test llvm


if (auto *MD = dyn_cast<CXXMethodDecl>(SelfOpCall->getCalleeDecl()))
if (!MD->isConst())
return false;

Choose a reason for hiding this comment

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

If I understand correctly, we check MD->isConst() to ensure that both calls return the same value, and thus we can assume that the pattern is always safe.

Can dyn_cast<CXXMethodDecl>(SelfOpCall->getCalleeDecl()) be nullptr? If so, we skip the MD->isConst() check and we might consider as safe something we don't want. Maybe we can change this to:

    const auto *MD = dyn_cast<CXXMethodDecl>(SelfOpCall->getCalleeDecl());
    if (!MD || !MD->isConst())
      return false;

Copy link
Author

Choose a reason for hiding this comment

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

Thanks for the catch. It is my bad code! Yeah, I think it should be in the form you suggested.
I just learned that operator[] can be static: https://en.cppreference.com/w/cpp/language/operators.
So we do need the dyn_cast.

@ziqingluo-90 ziqingluo-90 merged commit 49f126f into swiftlang:next Feb 12, 2025
@ziqingluo-90 ziqingluo-90 deleted the dev/ziqing/PR-143986251 branch February 25, 2025 19:35
ziqingluo-90 added a commit that referenced this pull request Mar 5, 2025
#10156)

* [-Wunsafe-buffer-usage][BoundsSafety] Extend safe patterns of arguments to counted_by parameters (#9971)

Add support of
- overloaded subscript operator of hardened view/containers, and
- calls to const member functions of 0 arguments

Now it can match patterns like:

```
std::span<char> D, S;
std::array<std::span<char>, 10> A;

memcpy(D.first(S.size()).data(), S.data(), S.size());
memcpy(D.first(A[x].size()).data(), A[x].data(), A[x].size());
```

(rdar://143986251)

* [Safe Buffers][BoundsSafety] Fix a bug in the interop analysis that can cause infinite loops

The interop analysis substitutes formal parameters to arguments in AST
visiting.  Conceptually, the substitution should be done exactly once.
But the previous implementation never checked if the substitution has
happened.  In cases where the argument contains DREs referring to the
Decl of the corresponding formal parameter, the analysis enters an
infinite loop.  A typical example is the following.

```
void f(int * __counted_by(n) p, size_t n) {
  f(p, n);
}
```

This commit fixes the issue.

(rdar://145705060)

(cherry picked from commit 31f72f7)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants