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

Bounds-checking debug assertions for array access #1514

Merged

Conversation

NotGyro
Copy link
Contributor

@NotGyro NotGyro commented Feb 2, 2024

This introduces several debug_assert!() calls around any attempt to dereference an element in a Postgres array, in order to catch memory bugs. It was written to resolve #1195.

If RawArray's end_ptr() ever pointed at something other than one byte past the end of the array's buffer, this might fail to catch memory bugs. However, it should be pointing to the end of the ArrayType's varlena, which includes the ArrayType header, its null bitmap, and its data, so this should work.

@workingjubilee
Copy link
Member

workingjubilee commented Feb 3, 2024

Thanks! First impression is this looks good, though I have to meditate on some of the possible pointer-inbounds edge cases here and figure out if any of them matter for this check.

Comment on lines 280 to 293
/// Returns true if the pointer provided is within the bounds of the array.
/// Primarily intended for use with debug_assert!()s.
/// Note that this will return false for the 1-past-end, which is a useful
/// position for a pointer to be in, but not valid to dereference.
#[inline]
pub(crate) fn is_within_bounds(&self, ptr: *const u8) -> bool {
(self.raw.data_ptr() <= ptr) && (ptr < self.raw.end_ptr())
}
/// Similar to [`Self::is_within_bounds()`], but also returns true for the
/// 1-past-end position.
#[inline]
pub(crate) fn is_within_bounds_inclusive(&self, ptr: *const u8) -> bool {
(self.raw.data_ptr() <= ptr) && (ptr <= self.raw.end_ptr())
}
Copy link
Member

Choose a reason for hiding this comment

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

Something I am currently thinking through is whether these are resilient in the face of UB, LLVM optimizations, and arithmetic offset wrapping values around the integer addressing space.

Copy link
Member

Choose a reason for hiding this comment

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

For RawArray::end_ptr, the implementation is:

    /// "one past the end" pointer for the entire array's bytes
    pub(crate) fn end_ptr(&self) -> *const u8 {
        let ptr = self.ptr.as_ptr().cast::<u8>();
        ptr.wrapping_add(unsafe { crate::varlena::varsize_any(ptr.cast()) })
    }

Notably, we should not trust varsize_any to be correct, but the ptr.wrapping_add returns a pointer that may be invalid, but is not, according to my understanding of LLVM's mechanics, inherently "bad" to compare against (where "bad" is much more nebulous than "UB"). But I am slightly suspicious because pointer comparisons are ill-defined in too many cases...

Copy link
Member

@workingjubilee workingjubilee Feb 6, 2024

Choose a reason for hiding this comment

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

Okay!

I think we have to trust the implementation of varsize_any anyways, thinking about it. Ideally, we should set up more tests for it. Opened #1539 for that.

After recaching my memory from the original issue and PR, I now remember: What we really need is to defend against LLVM deciding pointers with the same address are unequal, as that means we can't rely on comparison semantics being coherent:

This means that we need to cast the pointer addresses to usize, and do the comparisons purely on integral values.

Choose a reason for hiding this comment

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

Terse version:

Casting to usize won't save you, but I'll bet that you don't need this anyway, for several reasons. In the remote case you do, a core::hint::black_box() will serve you much better, and will be much more readable.


Verbose version:

Forgive me for intruding, but I'm worried you might be misremembering some small details of the bug you're worried about. Chief amongst them is the fact that casting the pointer addresses to usize does nothing to alleviate this problem. This example makes this absolutely clear.

That said, I'm not sure how much of a problem this is for your use-case. There are two reasons for this, one incidental and one fundamental.

The incidental one: In every single reproduction of this issue that I've managed to find, the pointers only miscompare when they're dangling. This pull-request appears to only compare non-dangling pointers, so it ought to dodge this specific issue.

The fundamental one: The main problem is that LLVM (and GCC, for that matter) think that two pointers can never be equal if they “come from” different allocations. If the pointers you're comparing are both “coming from” the same allocation –as appears to be the case as best as I can tell– then comparison will never misbehave.

The solution I propose: Rust has a specific function whose entire purpose is to tell the compiler “please pessimise this as best as you can”; that function is black_box, found in core::hint and std::hint. I've yet to encounter a reproduction where this didn't immediately fix the issue. (There's also (a^b).to_ne_bytes().into_iter().map(|x| x.count_ones()).sum() == 0, but that's both slow and plain unreadable.)

One thing to note: I can't help but notice, due to your title, that this entire discussion happens for some debug assertions. Thus far, this issue has only ever been encountered in release mode. If you manage to reproduce it with debug assertions enabled, that would be important information that should be noted in the issue that tracks it.

Choose a reason for hiding this comment

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

@workingjubilee …was the above useful, after all?

Copy link
Member

Choose a reason for hiding this comment

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

@DoubleHyphen

  1. Debug assertions and optimization levels are not values that are intrinsically connected, so I would prefer not to reason on that grounds.
  2. std::hint::black_box is not "please pessimize this as much as you can", it has a specific and more narrow behavior than that.
  3. I have in fact noticed usize-casting interfering, though it has been a while so I don't have the reproduction code for such. Shrug.

Comment on lines +244 to +251
// Ensure we are not attempting to dereference a pointer
// outside of the array.
debug_assert!(self.is_within_bounds(ptr));
// Prevent a datum that begins inside the array but would end
// outside the array from being dereferenced.
debug_assert!(self.is_within_bounds_inclusive(
ptr.wrapping_add(unsafe { self.slide_impl.hop_size(ptr) })
));
Copy link
Member

Choose a reason for hiding this comment

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

If we used a fn from the .offset family instead of .wrapping_offset then it's simply UB and any such check is elided, but hop_size does not do this inherently...

@NotGyro
Copy link
Contributor Author

NotGyro commented Feb 6, 2024

Opened related issue #1540

@workingjubilee workingjubilee merged commit 33683fa into pgcentralfoundation:develop Feb 6, 2024
8 checks passed
workingjubilee added a commit that referenced this pull request Mar 1, 2024
Welcome to pgrx 0.12.0-alpha.1!

Say the magic words with me!

```shell
cargo install cargo-pgrx --locked --version 0.12.0-alpha.1
```

# Breaking Changes

## No more dlopen!

Perhaps the most exciting change this round is @usamoi's contribution in
#1468 which means that
we no longer perform a `dlopen` in order to generate the schema. The
cost, such as it is, is that your pgrx extensions now require a
`src/bin/pgrx_embed.rs`, which will be used to generate the schema. This
has much less cross-platform issues and will enable supporting things
like `cargo binstall` down the line.

It may be a bit touchy on first-time setup for transitioning older
repos. If necessary, you may have to directly add a
`src/bin/pgrx_embed.rs` and add the following code (which should be the
only code in the file, though you can add comments if you like?):

```rust
::pgrx::pgrx_embed!();
```

Your Cargo.toml will also want to update its crate-type key for the
library:
```toml
[lib]
crate-type = ["cdylib", "lib"]
```

## Library Code

- pgrx-pg-sys will now use `ManuallyDropUnion` thanks to @NotGyro in
#1547
- VARHDRSZ `const`s are no longer `fn`, thanks to @workingjubilee in
#1584
- We no longer have `Interval::is_finite` since
#1594
- We translate more `*_tree_walker` functions to the same signature
their `*_impl` version in Postgres 16 has:
#1596
- Thanks to @eeeebbbbrrrr in
#1591 we no longer have
the `pg_sql_graph_magic!()` macro, which should help with more things in
the future!

# What's New

We have quite a lot of useful additions to our API:

- `SpiClient::prepare_mut` was added thanks to @XeniaLu in
#1275
- @usamoi also contributed bindings subscripting code in
#1562
- For `#[pg_test]`, you have been able to use `#[should_panic(expected =
"string")]` to anticipate a panic that contains that string in that
test. For various reasons, `#[pg_test(error = "string")]` is much the
same. Now, you can also use `#[pg_test(expected = "string")]`, in the
hopes that is easier to stumble across, as of
#1570

## `Result<composite_type!("..."), E>` support

- In #1560 @NotGyro
contributed support for using `Result<composite_type!("Name"), E>`, as a
case that had not been handled before.

## Significantly expanded docs
Thanks to @rjuju, @NotGyro, and @workingjubilee, we now have
significantly expanded docs for cargo-pgrx and pgrx in general. Some of
these are in the API docs on https://docs.rs or the READMEs, but there's
also a guide, now! It's not currently published, but is available as an
[mdbook](https://github.com/rust-lang/mdBook) in the repo.

Some diagnostic information that is also arguably documentation, like
comments and the suggestion to `cargo install`, have also been improved,
thanks to @workingjubilee in
- #1579
- #1573

## `#[pg_cast]`

An experimental macro for a `CREATE CAST` was contributed by @xwkuang5
in #1445!

## Legal Stuff

Thanks to @the-kenny in
#1490 and
@workingjubilee in
#1504, it was brought to
our attention that some dependencies had unusual legal requirements. So
we fixed this with CI! We now check our code included into pgrx-using
binaries is MIT/Apache 2.0 licensed, as is common across crates.io,
using `cargo deny`!. The build tools will have more flexible legal
requirements (partly due to the use of Mozilla Public License code in
rustls).

# Internal Changes
Many internal cleanups were done thanks to
- @workingjubilee in too many PRs to count!
- @thomcc found a needless condition in
#1501
- @nyurik in too many PRs to count!

In particular:
- we now actually `pfree` our `Array`s we detoasted as-of
#1571
- creating a `RawArray` is now low-overhead due to
#1587

## Soundness Fixes
We had a number of soundness issues uncovered or have added more tests
to catch them.
- Bounds-checking debug assertions for array access by @NotGyro in
#1514
- Fix unsound `&` and `&mut` in `fcinfo.rs` by @workingjubilee in
#1595

## Less Deps
Part of the cleanup by @workingjubilee was reducing the number of deps
we compile:
* cargo-pgrx: reduce trivial dep usages in
#1499
* Update 2 syn in #1557

Hopefully it will reduce compile time and disk usage!

## New Contributors
* @the-kenny made their first contribution in
#1490
* @xwkuang5 made their first contribution in
#1445
* @rjuju made their first contribution in
#1516
* @nyurik made their first contribution in
#1533
* @NotGyro made their first contribution in
#1514
* @XeniaLu made their first contribution in
#1275

**Full Changelog**:
v0.12.0-alpha.0...v0.12.0-alpha.1
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.

Introduce debug_assert!s before ptr deref
3 participants