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

Fix a use-after-free bug in our Sqlite backend #2663

Merged
merged 5 commits into from Mar 5, 2021

Conversation

weiznich
Copy link
Member

@weiznich weiznich commented Mar 4, 2021

We've missused sqlite3_column_name. The
SQLite documentation
states that the following:

The returned string pointer is valid until either the prepared statement
is destroyed by sqlite3_finalize() or until the statement is automatically
reprepared by the first call to sqlite3_step() for a particular
run or until the next call to sqlite3_column_name()
or sqlite3_column_name16() on the same column.

As part of our query_by_name infrastructure we've first received all
field names for the prepared statement and stored them as string slices
for later use. After that we called sqlite3_step() for the first time,
which invalids the pointer and therefore the stored string slice.
I've opted to fix this by just populating the field name map after the
first call to sqlite3_step() as a minimal fix for this issue for the
1.x release series. We need to investigate further if and how a similar
issue can occur with the current master branch. The corresponding code
changed quite a lot and at least this particular behaviour is not
possible anymore.

cc @diesel-rs/contributors and @diesel-rs/core: I've plan to release this change tomorrow. Additionally I see if we need to get a RUSTSEC advisory for this or not.

Fixes #2662

stmt.field_name(i).map(|column| {
let column = column
.to_str()
dbg!(i);
Copy link
Member

Choose a reason for hiding this comment

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

dbg

let column = column
.to_str()
dbg!(i);
dbg!(self.stmt.field_name(i)).map(|column| {
Copy link
Member

Choose a reason for hiding this comment

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

dbg

.to_str()
dbg!(i);
dbg!(self.stmt.field_name(i)).map(|column| {
let column = dbg!(column
Copy link
Member

Choose a reason for hiding this comment

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

dbg

Comment on lines +65 to +66
/// > reprepared by the first call to sqlite3_step() for a particular
/// > run or until the next call to sqlite3_column_name()
Copy link
Member

Choose a reason for hiding this comment

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

I don't understand what "for a particular run" means in this context. I'd be able to review more usefully if I understood precisely what this means :)

Copy link
Member Author

Choose a reason for hiding this comment

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

That's the exact wording of the SQLite documentation for this function. I interpret this in such a way that the pointer for a given column name can change if you execute a prepared statement and after the executing call sqlite3_step() the first time for this newly executed statement. (sqlite3_step() is basically Iterator::next() to get a new row from the result set). So we should fine as long as we call sqlite3_column_name() after the first call to sqlite3_step() for a particular run.

Copy link
Member

Choose a reason for hiding this comment

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

Hmm so if I understand correctly "for a particular run" means for us "for an instance of StatementUse".

Copy link
Member Author

Choose a reason for hiding this comment

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

Thats correct 👍

/// > run or until the next call to sqlite3_column_name()
/// > or sqlite3_column_name16() on the same column.
///
/// https://www.sqlite.org/c3ref/column_name.html
unsafe fn field_name<'a>(&self, idx: usize) -> Option<&'a CStr> {
Copy link
Member

@Ten0 Ten0 Mar 4, 2021

Choose a reason for hiding this comment

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

The doc states that "The lifetime of the returned CStr is shorter than self.", but it looks like this signature actually allows extending the lifetime.

fn field_name(&self, idx: usize) -> Option<&CStr>;

or (if we want to be more explicit)

fn field_name<'a, 'b: 'a>(&'a self, idx: usize) -> Option<&'b CStr>;

would solve part of the issue.

row.map(|row| {
let row = row.into_named(&self.column_indices);
let row = row.into_named(self.column_indices.as_ref().expect("it's there because we populated it above"));
Copy link
Member

@Ten0 Ten0 Mar 4, 2021

Choose a reason for hiding this comment

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

So this seems to not break memory safety specifically because this call to into_named enforces downgrading the lifetime of 'a in HashMap<&'a str, usize> to the lifetime of self.
Maybe we could explicit it there in order to contain why this works closer? (It looks that currently it wouldn't be hard to change the signature of into_named, see it compile and call it a day.)

impl<'a, T> Iterator for NamedStatementIterator<'a, T>
where
    T: QueryableByName<Sqlite>,
{
    type Item = QueryResult<T>;

    fn next<'s>(&'s mut self) -> Option<Self::Item> {
        let row = match self.stmt.step() {
            Ok(row) => row,
            Err(e) => return Some(Err(e)),
        };
        if self.column_indices.is_none() {
            if let Err(e) = self.populate_column_indices() {
                return Some(Err(e));
            }
        }
        row.map(|row| {
            // explicitly downgrade the lifetime to a correct one
            let column_indices: &'s HashMap<&'s str, usize> = self.column_indices.as_ref().expect("it's there because we populated it above");
            let row = row.into_named(column_indices);
            T::build(&row).map_err(DeserializationError)
        })
    }
}

Additionally it looks like we could add a doc comment on column_indices of to specify that 'a is not precisely correct in that context.

Copy link
Member Author

Choose a reason for hiding this comment

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

This is not possible, it results in the following error message:

error[E0597]: `self` does not live long enough
  --> diesel/src/sqlite/connection/statement_iterator.rs:94:34
   |
83 |     fn next<'s>(&'s mut self) -> Option<Self::Item> {
   |             -- lifetime `'s` defined here
...
93 |         row.map(|row| {
   |                 ----- value captured here
94 |             let indices: &'s _ = self.column_indices.as_ref().expect("it's there because we populated it above");
   |                          -----   ^^^^ borrowed value does not live long enough
   |                          |
   |                          type annotation requires that `self` is borrowed for `'s`
...
98 |     }
   |     - `self` dropped here while still borrowed

That written: I'm quite sure no one will actually touch this code again, as this is already rewritten on the master branch in such a way that this cannot happen there. (Though we should check if we could trigger there the multiple calls to sqlite3_column_name thing there.)

We've missused `sqlite3_column_name`. The
[SQLite](https://www.sqlite.org/c3ref/column_name.html) documentation
states that the following:

> The returned string pointer is valid until either the prepared statement
> is destroyed by sqlite3_finalize() or until the statement is automatically
> reprepared by the first call to sqlite3_step() for a particular
> run or until the next call to sqlite3_column_name()
> or sqlite3_column_name16() on the same column.

As part of our `query_by_name` infrastructure we've first received all
field names for the prepared statement and stored them as string slices
for later use. After that we called `sqlite3_step()` for the first time,
which invalids the pointer and therefore the stored string slice.
I've opted to fix this by just populating the field name map after the
first call to `sqlite3_step()` as a minimal fix for this issue for the
1.x release series. We need to investigate further if and how a similar
issue can occur with the current master branch. The corresponding code
changed quite a lot and at least this particular behaviour is not
possible anymore.
@weiznich weiznich merged commit ae72835 into diesel-rs:1.4.x Mar 5, 2021
weiznich added a commit to weiznich/diesel that referenced this pull request Mar 12, 2021
This is a full fix for diesel-rs#2663.

> The returned string pointer is valid until either the prepared statement
> is destroyed by sqlite3_finalize() or until the statement is automatically
> reprepared by the first call to sqlite3_step() for a particular
> run or until the next call to sqlite3_column_name()
> or sqlite3_column_name16() on the same column.

As concequence this means we cannot just call `sqlite3_column_name` in
`Field::field_name()`. To fix this issue I decided to get all column
names pointers once after calling `step()` the first time and cache them
as part of the specific Statement instance. This introduces a lot of
lifetimes to be very explicit about what is valid for which time.
bors bot added a commit to comit-network/comit-rs that referenced this pull request Mar 17, 2021
3539: Bump diesel from 1.4.5 to 1.4.6 r=mergify[bot] a=dependabot[bot]

Bumps [diesel](https://github.com/diesel-rs/diesel) from 1.4.5 to 1.4.6.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/diesel-rs/diesel/releases">diesel's releases</a>.</em></p>
<blockquote>
<h2>1.4.6</h2>
<h3>Fixed</h3>
<ul>
<li>Fixed a use-after-free issue in the <code>QueryableByName</code> implementation
of our <code>Sqlite</code> backend</li>
<li>Updated several dependencies</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/diesel-rs/diesel/blob/v1.4.6/CHANGELOG.md">diesel's changelog</a>.</em></p>
<blockquote>
<h2>[1.4.6] - 2021-03-05</h2>
<h3>Fixed</h3>
<ul>
<li>Fixed a use-after-free issue in the <code>QueryableByName</code> implementation
of our <code>Sqlite</code> backend</li>
<li>Updated several dependencies</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/diesel-rs/diesel/commit/b0c8e99b3e16a81a99c23af1ea90963d2ec7477d"><code>b0c8e99</code></a> Release diesel 1.4.6</li>
<li><a href="https://github.com/diesel-rs/diesel/commit/ae72835078156a3ff3b5b17e2a235189ef892af7"><code>ae72835</code></a> Merge pull request <a href="https://github.com/diesel-rs/diesel/issues/2663">#2663</a> from weiznich/bugfix/fix_use_after_free</li>
<li><a href="https://github.com/diesel-rs/diesel/commit/cead1a3b5d14ec90d356306b168ba0940167d36b"><code>cead1a3</code></a> Downgrade max bigdecimal version as the new version is incompatible</li>
<li><a href="https://github.com/diesel-rs/diesel/commit/2a612f078c625fa4c63012ec1f56e68141720229"><code>2a612f0</code></a> Run cargo fmt</li>
<li><a href="https://github.com/diesel-rs/diesel/commit/d699f1499f3cc1c6920cb6aebc72a3aff1880c8f"><code>d699f14</code></a> Bump some dependencies</li>
<li><a href="https://github.com/diesel-rs/diesel/commit/d2192118eba79ff08044455eabc784525de8d854"><code>d219211</code></a> Address review comments</li>
<li><a href="https://github.com/diesel-rs/diesel/commit/d4133288261612d43853245aa886e1a3a0d2dbfe"><code>d413328</code></a> Fix a use-after-free bug in our Sqlite backend</li>
<li><a href="https://github.com/diesel-rs/diesel/commit/8e9e639983b78fce456fd5cf5ea8aa1f0556ff66"><code>8e9e639</code></a> More fixes for mysql setup</li>
<li><a href="https://github.com/diesel-rs/diesel/commit/453cfb8eadbc39d26bbad928e3889864d4e9a707"><code>453cfb8</code></a> Remove clippy from ci on release branch</li>
<li><a href="https://github.com/diesel-rs/diesel/commit/d856d35fd5f134d3405e1f1324419ed3612fe0d1"><code>d856d35</code></a> Fix mysql instalation on macos</li>
<li>Additional commits viewable in <a href="https://github.com/diesel-rs/diesel/compare/v1.4.5...v1.4.6">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=diesel&package-manager=cargo&previous-version=1.4.5&new-version=1.4.6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually


</details>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
matthiasbeyer pushed a commit to science-computing/butido that referenced this pull request Apr 20, 2021
matthiasbeyer pushed a commit to science-computing/butido that referenced this pull request Apr 20, 2021
See also:
    https://rustsec.org/advisories/RUSTSEC-2021-0037.html
    diesel-rs/diesel#2663

Signed-off-by: Matthias Beyer <matthias.beyer@atos.net>
(cherry picked from commit 38b6ded)
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.

None yet

2 participants