Skip to content

Commit 7b5d67e

Browse files
author
Vincent Rouillé
committed
Simplify and fix UB in boot API
- remove UB `boot_async` method - rename `boot` to `run` method - add back the old but now unsafe `boot` method - add migration guide from 0.4 to 0.5 #202
1 parent cf5ceb6 commit 7b5d67e

File tree

14 files changed

+152
-137
lines changed

14 files changed

+152
-137
lines changed

foundationdb-bench/src/bin/fdb-bench.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ fn main() {
146146
let opt = Opt::from_args();
147147
info!("opt: {:?}", opt);
148148

149-
fdb::boot(|| {
149+
fdb::run(|| {
150150
let db = Arc::new(
151151
futures::executor::block_on(fdb::Database::new_compat(None))
152152
.expect("failed to get database"),

foundationdb-bindingtester/src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1586,7 +1586,7 @@ fn main() {
15861586
.set_runtime_version(api_version)
15871587
.build()
15881588
.expect("failed to initialize FoundationDB API")
1589-
.boot(|| {
1589+
.run(|| {
15901590
let db = Arc::new(
15911591
futures::executor::block_on(fdb::Database::new_compat(cluster_path))
15921592
.expect("failed to get database"),

foundationdb/README.md

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,48 @@ async fn async_main() -> foundationdb::FdbResult<()> {
6565
Ok(())
6666
}
6767

68-
foundationdb::boot(|| {
68+
foundationdb::run(|| {
6969
futures::executor::block_on(async_main()).expect("failed to run");
7070
});
7171
```
7272

73+
## Migration from 0.4 to 0.5
74+
75+
The initialization of foundationdb API has changed due to undefined behavior being possible with only safe code (issues #170, #181, pulls #179, #182).
76+
77+
Previously you had to wrote:
78+
79+
```rust
80+
let network = foundationdb::boot().expect("failed to initialize Fdb");
81+
82+
futures::executor::block_on(async_main()).expect("failed to run");
83+
// cleanly shutdown the client
84+
drop(network);
85+
```
86+
87+
This can be converted to:
88+
89+
```rust
90+
foundationdb::boot(|| {
91+
futures::executor::block_on(async_main()).expect("failed to run");
92+
}).expect("failed to boot fdb");
93+
```
94+
95+
or
96+
97+
```rust
98+
#[tokio::main]
99+
async fn main() {
100+
// Safe because drop is called before the program exits
101+
let network = unsafe { foundationdb::boot() }.expect("failed to initialize Fdb");
102+
103+
// Have fun with the FDB API
104+
105+
// shutdown the client
106+
drop(network);
107+
}
108+
```
109+
73110
## API stability
74111

75112
_WARNING_ Until the 1.0 release of this library, the API may be in constant flux.

foundationdb/examples/class-scheduling.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,7 @@ async fn run_sim(db: &Database, students: usize, ops_per_student: usize) {
364364
}
365365

366366
fn main() {
367-
fdb::boot(|| {
367+
fdb::run(|| {
368368
let db = futures::executor::block_on(fdb::Database::new_compat(None))
369369
.expect("failed to get database");
370370
futures::executor::block_on(init(&db, &*ALL_CLASSES));

foundationdb/src/api.rs

Lines changed: 72 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@ use std::sync::atomic::{AtomicBool, Ordering};
1818
use std::sync::{Arc, Condvar, Mutex};
1919
use std::thread;
2020

21-
use futures::prelude::*;
22-
2321
use crate::options::NetworkOption;
2422
use crate::{error, FdbResult};
2523
use foundationdb_sys as fdb_sys;
@@ -92,7 +90,7 @@ impl Default for FdbApiBuilder {
9290
/// use foundationdb::api::FdbApiBuilder;
9391
///
9492
/// let network_builder = FdbApiBuilder::default().build().expect("fdb api initialized");
95-
/// network_builder.boot(|| {
93+
/// network_builder.run(|| {
9694
/// // do some work with foundationDB
9795
/// }).expect("fdb network to run");
9896
/// ```
@@ -110,7 +108,7 @@ impl NetworkBuilder {
110108
/// Finalizes the initialization of the Network
111109
///
112110
/// It's not recommended to use this method unless you really know what you are doing.
113-
/// Otherwise, the `boot` method is the **safe** and easiest way to do it.
111+
/// Otherwise, the `run` method is the **safe** and easiest way to do it.
114112
///
115113
/// In order to start the network you have to call the unsafe `NetworkRunner::run()` method.
116114
/// This method starts the foundationdb network runloop, once started, the `NetworkStop::stop()`
@@ -145,89 +143,82 @@ impl NetworkBuilder {
145143
}
146144

147145
/// Execute `f` with the FoundationDB Client API ready, this can only be called once per process.
146+
/// Once this method returns, the FoundationDB Client API is stopped and cannot be restarted.
148147
///
149148
/// # Examples
150149
///
151150
/// ```rust
152151
/// use foundationdb::api::FdbApiBuilder;
153152
///
154153
/// let network_builder = FdbApiBuilder::default().build().expect("fdb api initialized");
155-
/// network_builder.boot(|| {
154+
/// network_builder.run(|| {
156155
/// // do some interesting things with the API...
157156
/// });
158157
/// ```
159-
pub fn boot<T>(self, f: impl (FnOnce() -> T) + panic::UnwindSafe) -> FdbResult<T> {
160-
let (runner, cond) = self.build()?;
161-
162-
let net_thread = thread::spawn(move || {
163-
unsafe { runner.run() }.expect("failed to run");
164-
});
165-
166-
let network = cond.wait();
158+
pub fn run<T>(self, f: impl FnOnce() -> T) -> FdbResult<T> {
159+
// Safe because the returned object is dropped before this function returns
160+
let must_drop = unsafe { self.boot()? };
167161

168-
let res = panic::catch_unwind(f);
162+
let t = f();
169163

170-
if let Err(err) = network.stop() {
171-
eprintln!("failed to stop network: {}", err);
172-
// Not aborting can probably cause undefined behavior
173-
std::process::abort();
174-
}
175-
net_thread.join().expect("failed to join fdb thread");
164+
drop(must_drop);
176165

177-
match res {
178-
Err(payload) => panic::resume_unwind(payload),
179-
Ok(v) => Ok(v),
180-
}
166+
Ok(t)
181167
}
182168

183-
/// Async execute `f` with the FoundationDB Client API ready, this can only be called once per process.
169+
/// Initialize the FoundationDB Client API, this can only be called once per process.
170+
///
171+
/// # Returns
172+
///
173+
/// A `NetworkAutoStop` handle which must be dropped before the program exits.
174+
///
175+
/// # Safety
176+
///
177+
/// This method used to be safe in version `0.4`. But because `drop` on the returned object
178+
/// might not be called before the program exits, it was found unsafe.
179+
/// You should prefer the safe `run` variant.
180+
/// If you still want to use this, you *MUST* ensure drop is called on the returned object
181+
/// before the program exits. This is not required if the program is aborted.
184182
///
185183
/// # Examples
186184
///
187185
/// ```rust
188186
/// use foundationdb::api::FdbApiBuilder;
189187
///
190188
/// let network_builder = FdbApiBuilder::default().build().expect("fdb api initialized");
191-
/// network_builder.boot_async(|| async {
189+
/// let network = unsafe { network_builder.boot() }.expect("fdb network running");
190+
/// // do some interesting things with the API...
191+
/// drop(network);
192+
/// ```
193+
///
194+
/// ```rust
195+
/// use foundationdb::api::FdbApiBuilder;
196+
///
197+
/// #[tokio::main]
198+
/// async fn main() {
199+
/// let network_builder = FdbApiBuilder::default().build().expect("fdb api initialized");
200+
/// let network = unsafe { network_builder.boot() }.expect("fdb network running");
192201
/// // do some interesting things with the API...
193-
/// });
202+
/// drop(network);
203+
/// }
194204
/// ```
195-
pub async fn boot_async<F, Fut, T>(self, f: F) -> FdbResult<T>
196-
where
197-
F: (FnOnce() -> Fut) + panic::UnwindSafe,
198-
Fut: Future<Output = T> + panic::UnwindSafe,
199-
{
205+
pub unsafe fn boot(self) -> FdbResult<NetworkAutoStop> {
200206
let (runner, cond) = self.build()?;
201207

202-
let net_thread = thread::spawn(move || {
203-
unsafe { runner.run() }.expect("failed to run");
204-
});
208+
let net_thread = runner.spawn();
205209

206210
let network = cond.wait();
207211

208-
let res = panic::catch_unwind(f);
209-
let res = match res {
210-
Ok(fut) => fut.catch_unwind().await,
211-
Err(err) => Err(err),
212-
};
213-
214-
if let Err(err) = network.stop() {
215-
eprintln!("failed to stop network: {}", err);
216-
// Not aborting can probably cause undefined behavior
217-
std::process::abort();
218-
}
219-
net_thread.join().expect("failed to join fdb thread");
220-
221-
match res {
222-
Err(payload) => panic::resume_unwind(payload),
223-
Ok(v) => Ok(v),
224-
}
212+
Ok(NetworkAutoStop {
213+
handle: Some(net_thread),
214+
network: Some(network),
215+
})
225216
}
226217
}
227218

228219
/// A foundationDB network event loop runner
229220
///
230-
/// Most of the time you should never need to use this directly and use `NetworkBuilder::boot()`.
221+
/// Most of the time you should never need to use this directly and use `NetworkBuilder::run()`.
231222
pub struct NetworkRunner {
232223
cond: Arc<(Mutex<bool>, Condvar)>,
233224
}
@@ -257,11 +248,17 @@ impl NetworkRunner {
257248

258249
error::eval(unsafe { fdb_sys::fdb_run_network() })
259250
}
251+
252+
unsafe fn spawn(self) -> thread::JoinHandle<()> {
253+
thread::spawn(move || {
254+
self.run().expect("failed to run network thread");
255+
})
256+
}
260257
}
261258

262259
/// A condition object that can wait for the associated `NetworkRunner` to actually run.
263260
///
264-
/// Most of the time you should never need to use this directly and use `NetworkBuilder::boot()`.
261+
/// Most of the time you should never need to use this directly and use `NetworkBuilder::run()`.
265262
pub struct NetworkWait {
266263
cond: Arc<(Mutex<bool>, Condvar)>,
267264
}
@@ -284,7 +281,7 @@ impl NetworkWait {
284281

285282
/// Allow to stop the associated and running `NetworkRunner`.
286283
///
287-
/// Most of the time you should never need to use this directly and use `NetworkBuilder::boot()`.
284+
/// Most of the time you should never need to use this directly and use `NetworkBuilder::run()`.
288285
pub struct NetworkStop {
289286
_private: (),
290287
}
@@ -296,6 +293,26 @@ impl NetworkStop {
296293
}
297294
}
298295

296+
/// Stop the associated `NetworkRunner` and thread if dropped
297+
pub struct NetworkAutoStop {
298+
network: Option<NetworkStop>,
299+
handle: Option<std::thread::JoinHandle<()>>,
300+
}
301+
impl Drop for NetworkAutoStop {
302+
fn drop(&mut self) {
303+
if let Err(err) = self.network.take().unwrap().stop() {
304+
eprintln!("failed to stop network: {}", err);
305+
// Not aborting can probably cause undefined behavior
306+
std::process::abort();
307+
}
308+
self.handle
309+
.take()
310+
.unwrap()
311+
.join()
312+
.expect("failed to join fdb thread");
313+
}
314+
}
315+
299316
#[cfg(test)]
300317
mod tests {
301318
use super::*;

foundationdb/src/lib.rs

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@
7070
//! Ok(())
7171
//! }
7272
//!
73-
//! foundationdb::boot(|| {
73+
//! foundationdb::run(|| {
7474
//! futures::executor::block_on(async_main()).expect("failed to run");
7575
//! });
7676
//! ```
@@ -109,38 +109,53 @@ pub use crate::transaction::*;
109109
/// # Examples
110110
///
111111
/// ```rust
112-
/// foundationdb::boot(|| {
112+
/// foundationdb::run(|| {
113113
/// // do some interesting things with the API...
114114
/// });
115115
/// ```
116-
pub fn boot<T>(f: impl (FnOnce() -> T) + std::panic::UnwindSafe) -> T {
116+
pub fn run<T>(f: impl FnOnce() -> T) -> T {
117117
api::FdbApiBuilder::default()
118118
.build()
119119
.expect("foundationdb API to be initialized")
120-
.boot(f)
120+
.run(f)
121121
.expect("foundationdb network to be setup")
122122
}
123123

124-
/// Async execute `f` with the FoundationDB Client API ready, this can only be called once per process.
124+
/// Initialize the FoundationDB Client API, this can only be called once per process.
125+
///
126+
/// # Returns
127+
///
128+
/// A `NetworkAutoStop` handle which must be dropped before the program exits.
129+
///
130+
/// # Safety
131+
///
132+
/// This method used to be safe in version `0.4`. But because `drop` on the returned object
133+
/// might not be called before the program exits, it was found unsafe.
134+
/// You should prefer the safe `run` variant.
135+
/// If you still want to use this, you *MUST* ensure drop is called on the returned object
136+
/// before the program exits. This is not required if the program is aborted.
125137
///
126138
/// # Examples
127139
///
128140
/// ```rust
129-
/// foundationdb::boot_async(|| async {
141+
/// let network = unsafe { foundationdb::boot() };
142+
/// // do some interesting things with the API...
143+
/// drop(network);
144+
/// ```
145+
///
146+
/// ```rust
147+
/// #[tokio::main]
148+
/// async fn main() {
149+
/// let network = unsafe { foundationdb::boot() };
130150
/// // do some interesting things with the API...
131-
/// });
151+
/// drop(network);
152+
/// }
132153
/// ```
133-
pub async fn boot_async<F, Fut, T>(f: F) -> T
134-
where
135-
F: (FnOnce() -> Fut) + std::panic::UnwindSafe,
136-
Fut: std::future::Future<Output = T> + std::panic::UnwindSafe,
137-
{
138-
api::FdbApiBuilder::default()
154+
pub unsafe fn boot() -> api::NetworkAutoStop {
155+
let network_builder = api::FdbApiBuilder::default()
139156
.build()
140-
.expect("foundationdb API to be initialized")
141-
.boot_async(f)
142-
.await
143-
.expect("foundationdb network to be setup")
157+
.expect("foundationdb API to be initialized");
158+
network_builder.boot().expect("fdb network running")
144159
}
145160

146161
/// Returns the default Fdb cluster configuration file path

foundationdb/tests/atomic.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ mod common;
1212

1313
#[test]
1414
fn test_atomic() {
15-
boot(|| futures::executor::block_on(test_atomic_async()).expect("failed to run"));
15+
run(|| futures::executor::block_on(test_atomic_async()).expect("failed to run"));
1616
}
1717

1818
async fn atomic_add(db: &Database, key: &[u8], value: i64) -> FdbResult<()> {

foundationdb/tests/future.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ where
3232

3333
#[test]
3434
fn test_future_discard() {
35-
boot(|| futures::executor::block_on(test_future_discard_async()).expect("failed to run"));
35+
run(|| futures::executor::block_on(test_future_discard_async()).expect("failed to run"));
3636
}
3737

3838
async fn test_future_discard_async() -> FdbResult<()> {

foundationdb/tests/get.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ mod common;
1313

1414
#[test]
1515
fn test_get() {
16-
boot(|| {
16+
run(|| {
1717
futures::executor::block_on(test_set_get_async()).expect("failed to run");
1818
futures::executor::block_on(test_get_multi_async()).expect("failed to run");
1919
futures::executor::block_on(test_set_conflict_async()).expect("failed to run");

0 commit comments

Comments
 (0)