native_ossl/bio.rs
1//! BIO wrappers — `MemBio`, `MemBioBuf<'a>`, `Bio`, `BorrowedBio<'_>`.
2//!
3//! BIOs are OpenSSL's generic I/O abstraction. This module exposes four types:
4//!
5//! - [`MemBio`] — a writable, growable in-memory BIO (`BIO_s_mem()`). Used for
6//! encoding output (PEM, DER). Call `data()` after writing to read the result
7//! as a `&[u8]` slice without copying.
8//!
9//! - [`MemBioBuf<'a>`] — a read-only view of a caller-supplied slice
10//! (`BIO_new_mem_buf()`). Zero-copy input path for PEM parsing.
11//!
12//! - [`Bio`] — shared ownership wrapper around a raw `BIO*`. Used when OpenSSL
13//! needs a `BIO` that outlives the immediate call (e.g. TLS `SSL_set_bio`).
14//! Supports read/write I/O and BIO chain operations.
15//!
16//! - [`BorrowedBio<'_>`] — a non-owning view of a `BIO*` returned by chain
17//! walk operations (`BIO_next`, `BIO_find_type`). Does **not** free on drop.
18
19use crate::error::ErrorStack;
20use native_ossl_sys as sys;
21use std::marker::PhantomData;
22use std::mem::ManuallyDrop;
23use std::os::raw::c_int;
24use std::ptr;
25
26// ── MemBio — writable in-memory BIO ──────────────────────────────────────────
27
28/// A writable, growable in-memory BIO.
29///
30/// Data written to this BIO accumulates in an internal buffer managed by
31/// OpenSSL. After writing, `data()` returns a borrowed slice without copying.
32pub struct MemBio {
33 ptr: *mut sys::BIO,
34}
35
36impl MemBio {
37 /// Create a new empty writable `BIO_s_mem()` BIO.
38 ///
39 /// # Errors
40 ///
41 /// Returns `Err` if OpenSSL cannot allocate the BIO.
42 pub fn new() -> Result<Self, ErrorStack> {
43 let method = unsafe { sys::BIO_s_mem() };
44 if method.is_null() {
45 return Err(ErrorStack::drain());
46 }
47 let ptr = unsafe { sys::BIO_new(method) };
48 if ptr.is_null() {
49 return Err(ErrorStack::drain());
50 }
51 Ok(MemBio { ptr })
52 }
53
54 /// Write bytes into the BIO's internal buffer.
55 ///
56 /// # Errors
57 ///
58 /// Returns `Err` if the write fails.
59 pub fn write(&mut self, data: &[u8]) -> Result<(), ErrorStack> {
60 let mut written: usize = 0;
61 let rc = unsafe {
62 sys::BIO_write_ex(
63 self.ptr,
64 data.as_ptr().cast(),
65 data.len(),
66 std::ptr::addr_of_mut!(written),
67 )
68 };
69 if rc != 1 || written != data.len() {
70 return Err(ErrorStack::drain());
71 }
72 Ok(())
73 }
74
75 /// Borrow the current contents of the BIO's buffer as a `&[u8]`.
76 ///
77 /// The slice is valid until the next write operation or until `self` is dropped.
78 /// This is a zero-copy view — no allocation occurs.
79 #[must_use]
80 pub fn data(&self) -> &[u8] {
81 let mut ptr: *mut std::os::raw::c_char = ptr::null_mut();
82 // BIO_get_mem_data is the C macro equivalent of:
83 // BIO_ctrl(b, BIO_CTRL_INFO, 0, (char**)(pp))
84 // BIO_CTRL_INFO = 3.
85 let len = unsafe {
86 sys::BIO_ctrl(
87 self.ptr,
88 3, // BIO_CTRL_INFO
89 0,
90 (&raw mut ptr).cast::<std::os::raw::c_void>(),
91 )
92 };
93 if len <= 0 || ptr.is_null() {
94 return &[];
95 }
96 let n = usize::try_from(len).unwrap_or(0);
97 unsafe { std::slice::from_raw_parts(ptr.cast::<u8>(), n) }
98 }
99
100 /// Move the buffer contents into a freshly allocated `Vec<u8>`.
101 ///
102 /// Prefer `data()` when a borrow suffices.
103 #[must_use]
104 pub fn into_vec(self) -> Vec<u8> {
105 self.data().to_vec()
106 }
107
108 /// Return the raw `BIO*` pointer.
109 ///
110 /// The pointer is valid for the lifetime of `self`.
111 #[must_use]
112 #[allow(dead_code)] // used by x509/ssl modules added in Phase 7-8
113 pub(crate) fn as_ptr(&mut self) -> *mut sys::BIO {
114 self.ptr
115 }
116}
117
118impl Drop for MemBio {
119 fn drop(&mut self) {
120 unsafe { sys::BIO_free_all(self.ptr) };
121 }
122}
123
124// SAFETY: BIO_s_mem() BIOs do not reference external state.
125unsafe impl Send for MemBio {}
126
127// ── MemBioBuf — read-only view into a caller slice ───────────────────────────
128
129/// A read-only BIO wrapping a borrowed byte slice (`BIO_new_mem_buf()`).
130///
131/// Zero-copy: no data is copied from the slice. The `BIO*` pointer reads
132/// directly from the caller's memory. The lifetime `'a` ties the BIO to the
133/// source slice.
134pub struct MemBioBuf<'a> {
135 ptr: *mut sys::BIO,
136 _data: PhantomData<&'a [u8]>,
137}
138
139impl<'a> MemBioBuf<'a> {
140 /// Create a read-only BIO backed by `data`.
141 ///
142 /// OpenSSL reads from `data` directly; no copy occurs.
143 ///
144 /// # Errors
145 ///
146 /// Returns `Err` if OpenSSL cannot allocate the BIO wrapper, or if
147 /// `data.len()` exceeds `i32::MAX`.
148 pub fn new(data: &'a [u8]) -> Result<Self, ErrorStack> {
149 // BIO_new_mem_buf reads from the caller's slice directly.
150 // -1 means use data.len() (NUL-terminated string convention is not used here
151 // because we pass the explicit length).
152 let len = i32::try_from(data.len()).map_err(|_| ErrorStack::drain())?;
153 let ptr = unsafe { sys::BIO_new_mem_buf(data.as_ptr().cast(), len) };
154 if ptr.is_null() {
155 return Err(ErrorStack::drain());
156 }
157 Ok(MemBioBuf {
158 ptr,
159 _data: PhantomData,
160 })
161 }
162
163 /// Return the raw `BIO*` pointer.
164 #[must_use]
165 #[allow(dead_code)] // used by x509/ssl modules added in Phase 7-8
166 pub(crate) fn as_ptr(&self) -> *mut sys::BIO {
167 self.ptr
168 }
169}
170
171impl Drop for MemBioBuf<'_> {
172 fn drop(&mut self) {
173 unsafe { sys::BIO_free(self.ptr) };
174 }
175}
176
177// SAFETY: the slice reference `'a` bounds the BIO's use; it cannot outlive the slice.
178unsafe impl Send for MemBioBuf<'_> {}
179
180// ── Bio — shared ownership BIO ────────────────────────────────────────────────
181
182/// Shared ownership wrapper around a `BIO*`.
183///
184/// Used where OpenSSL takes ownership of a BIO (e.g. `SSL_set_bio`) or where
185/// the same BIO must be reachable from multiple Rust values. Implemented with
186/// `BIO_up_ref` / `BIO_free`.
187pub struct Bio {
188 ptr: *mut sys::BIO,
189}
190
191impl Bio {
192 /// Create a linked in-memory BIO pair suitable for in-process TLS.
193 ///
194 /// Returns `(bio1, bio2)` where data written to `bio1` is readable from
195 /// `bio2` and vice-versa. Pass each half to [`crate::ssl::Ssl::set_bio_duplex`] on
196 /// the client and server `Ssl` objects respectively.
197 ///
198 /// # Errors
199 ///
200 /// Returns `Err` if OpenSSL fails to allocate the pair.
201 pub fn new_pair() -> Result<(Self, Self), crate::error::ErrorStack> {
202 let mut b1: *mut sys::BIO = std::ptr::null_mut();
203 let mut b2: *mut sys::BIO = std::ptr::null_mut();
204 let rc = unsafe {
205 sys::BIO_new_bio_pair(std::ptr::addr_of_mut!(b1), 0, std::ptr::addr_of_mut!(b2), 0)
206 };
207 if rc != 1 {
208 return Err(crate::error::ErrorStack::drain());
209 }
210 Ok((Bio { ptr: b1 }, Bio { ptr: b2 }))
211 }
212
213 /// Wrap a raw `BIO*` transferring ownership to this `Bio`.
214 ///
215 /// # Safety
216 ///
217 /// `ptr` must be a valid, non-null `BIO*` that the caller is giving up ownership of.
218 #[must_use]
219 #[allow(dead_code)] // used by ssl tests
220 pub(crate) unsafe fn from_ptr_owned(ptr: *mut sys::BIO) -> Self {
221 Bio { ptr }
222 }
223
224 /// Return the raw `BIO*` pointer. Valid for the lifetime of `self`.
225 #[must_use]
226 pub(crate) fn as_ptr(&self) -> *mut sys::BIO {
227 self.ptr
228 }
229
230 // ── I/O methods ──────────────────────────────────────────────────────────
231
232 /// Read bytes from the BIO into `buf`.
233 ///
234 /// Returns the number of bytes actually read, which may be less than
235 /// `buf.len()` if fewer bytes are available.
236 ///
237 /// # Errors
238 ///
239 /// Returns `Err` on I/O error or EOF (when `BIO_read` returns -1).
240 pub fn read(&mut self, buf: &mut [u8]) -> Result<usize, ErrorStack> {
241 let len = i32::try_from(buf.len()).unwrap_or(i32::MAX);
242 // SAFETY: `self.ptr` is a valid, non-null BIO*; `buf` is a live mutable slice.
243 let n = unsafe { sys::BIO_read(self.ptr, buf.as_mut_ptr().cast(), len) };
244 if n < 0 {
245 return Err(ErrorStack::drain());
246 }
247 // n >= 0 is guaranteed by the check above; unwrap is infallible.
248 Ok(usize::try_from(n).unwrap_or(0))
249 }
250
251 /// Read bytes from the BIO, reporting the exact number of bytes read.
252 ///
253 /// On success returns the number of bytes placed into `buf`.
254 ///
255 /// # Errors
256 ///
257 /// Returns `Err` if `BIO_read_ex` reports failure (returns 0).
258 pub fn read_ex(&mut self, buf: &mut [u8]) -> Result<usize, ErrorStack> {
259 let mut readbytes: usize = 0;
260 // SAFETY: `self.ptr` is valid; `buf` is a live mutable slice; `readbytes` is a
261 // live stack variable whose address remains valid for the duration of the call.
262 crate::ossl_call!(sys::BIO_read_ex(
263 self.ptr,
264 buf.as_mut_ptr().cast(),
265 buf.len(),
266 &raw mut readbytes
267 ))?;
268 Ok(readbytes)
269 }
270
271 /// Write `buf` into the BIO.
272 ///
273 /// Returns the number of bytes actually written.
274 ///
275 /// # Errors
276 ///
277 /// Returns `Err` on I/O error (when `BIO_write` returns -1).
278 pub fn write(&mut self, buf: &[u8]) -> Result<usize, ErrorStack> {
279 let len = i32::try_from(buf.len()).unwrap_or(i32::MAX);
280 // SAFETY: `self.ptr` is a valid, non-null BIO*; `buf` is a live immutable slice.
281 let n = unsafe { sys::BIO_write(self.ptr, buf.as_ptr().cast(), len) };
282 if n < 0 {
283 return Err(ErrorStack::drain());
284 }
285 // n >= 0 is guaranteed by the check above; unwrap is infallible.
286 Ok(usize::try_from(n).unwrap_or(0))
287 }
288
289 // ── Chain management ─────────────────────────────────────────────────────
290
291 /// Append `next` after `self` in the BIO chain.
292 ///
293 /// Ownership of `next` is transferred into the chain; it must **not** be
294 /// freed separately. Returns `self` with the chain extended.
295 ///
296 /// Corresponds to `BIO_push(self, next)`.
297 #[must_use]
298 pub fn push(self, next: Bio) -> Bio {
299 // Prevent Rust from calling BIO_free on `next` — the chain now owns it.
300 let next_raw = ManuallyDrop::new(next).ptr;
301 // Prevent Rust from calling BIO_free on `self` — we return the new owner.
302 let self_raw = ManuallyDrop::new(self).ptr;
303 // SAFETY: both pointers are valid, non-null BIO*s. BIO_push transfers
304 // ownership of `next_raw` into the chain headed by `self_raw`.
305 let result = unsafe { sys::BIO_push(self_raw, next_raw) };
306 // BIO_push always returns its first argument; wrap it as the owning Bio.
307 Bio { ptr: result }
308 }
309
310 /// Remove `self` from its chain and return the rest of the chain.
311 ///
312 /// After this call `self` is a standalone BIO. Returns `None` if `self`
313 /// was the only (or last) element in the chain.
314 ///
315 /// Corresponds to `BIO_pop(self)`.
316 #[must_use]
317 pub fn pop(&mut self) -> Option<Bio> {
318 // SAFETY: `self.ptr` is a valid, non-null BIO*. BIO_pop detaches `self`
319 // from the chain and returns the next BIO (now an independent owner).
320 let next = unsafe { sys::BIO_pop(self.ptr) };
321 if next.is_null() {
322 None
323 } else {
324 Some(Bio { ptr: next })
325 }
326 }
327
328 /// Return a borrowed view of the next BIO in the chain without consuming `self`.
329 ///
330 /// The returned [`BorrowedBio`] is valid for the lifetime of `self` and does
331 /// **not** free the underlying pointer when dropped.
332 ///
333 /// Returns `None` if `self` is the last (or only) BIO in the chain.
334 ///
335 /// Corresponds to `BIO_next(self)`.
336 #[must_use]
337 pub fn next(&self) -> Option<BorrowedBio<'_>> {
338 // SAFETY: `self.ptr` is a valid, non-null BIO*. BIO_next returns a borrowed
339 // pointer whose lifetime is tied to the chain — expressed here via `'_`.
340 let next = unsafe { sys::BIO_next(self.ptr) };
341 if next.is_null() {
342 None
343 } else {
344 Some(BorrowedBio {
345 inner: ManuallyDrop::new(Bio { ptr: next }),
346 _marker: PhantomData,
347 })
348 }
349 }
350
351 /// Return the number of bytes available for reading from the BIO.
352 ///
353 /// Corresponds to the `BIO_pending` C macro, implemented via
354 /// `BIO_ctrl(b, BIO_CTRL_PENDING, 0, NULL)`.
355 ///
356 /// For a mem BIO this is the number of unread bytes in the buffer.
357 /// For other BIO types the value is type-specific.
358 #[must_use]
359 pub fn pending(&self) -> usize {
360 // SAFETY:
361 // - self.ptr is non-null (constructor invariant; never stores null)
362 // - BIO_ctrl with BIO_CTRL_PENDING=10 is a read-only query; &self is sufficient
363 // - no aliasing concern: &self ensures no concurrent mutation
364 let n = unsafe {
365 sys::BIO_ctrl(
366 self.ptr,
367 10, // BIO_CTRL_PENDING
368 0,
369 std::ptr::null_mut(),
370 )
371 };
372 usize::try_from(n).unwrap_or(0)
373 }
374
375 /// Return the number of bytes still to be written (pending in the write buffer).
376 ///
377 /// Corresponds to the `BIO_wpending` C macro, implemented via
378 /// `BIO_ctrl(b, BIO_CTRL_WPENDING, 0, NULL)`.
379 ///
380 /// For most BIO types this is 0 (writes are synchronous). For filter
381 /// BIOs it reflects bytes buffered but not yet flushed downstream.
382 #[must_use]
383 pub fn wpending(&self) -> usize {
384 // SAFETY:
385 // - self.ptr is non-null (constructor invariant; never stores null)
386 // - BIO_ctrl with BIO_CTRL_WPENDING=13 is a read-only query; &self is sufficient
387 // - no aliasing concern: &self ensures no concurrent mutation
388 let n = unsafe {
389 sys::BIO_ctrl(
390 self.ptr,
391 13, // BIO_CTRL_WPENDING
392 0,
393 std::ptr::null_mut(),
394 )
395 };
396 usize::try_from(n).unwrap_or(0)
397 }
398
399 /// Search the chain for the first BIO of the given `bio_type`.
400 ///
401 /// `bio_type` is one of the `BIO_TYPE_*` integer constants from OpenSSL
402 /// (e.g. `BIO_TYPE_MEM = 8`, `BIO_TYPE_BIO = 19`).
403 ///
404 /// Returns a borrowed view of the matching BIO, or `None` if none is found.
405 ///
406 /// Corresponds to `BIO_find_type(self, bio_type)`.
407 ///
408 /// # Errors
409 ///
410 /// Returns `None` if no BIO of the requested type exists in the chain.
411 #[must_use]
412 pub fn find_type(&self, bio_type: c_int) -> Option<BorrowedBio<'_>> {
413 // SAFETY: `self.ptr` is a valid, non-null BIO*; `bio_type` is a plain integer.
414 // The returned pointer is borrowed from the chain; lifetime tied to `self`.
415 let found = unsafe { sys::BIO_find_type(self.ptr, bio_type) };
416 if found.is_null() {
417 None
418 } else {
419 Some(BorrowedBio {
420 inner: ManuallyDrop::new(Bio { ptr: found }),
421 _marker: PhantomData,
422 })
423 }
424 }
425}
426
427impl Clone for Bio {
428 fn clone(&self) -> Self {
429 unsafe { sys::BIO_up_ref(self.ptr) };
430 Bio { ptr: self.ptr }
431 }
432}
433
434impl Drop for Bio {
435 fn drop(&mut self) {
436 unsafe { sys::BIO_free(self.ptr) };
437 }
438}
439
440// SAFETY: `BIO_up_ref` / `BIO_free` are thread-safe for memory BIOs.
441unsafe impl Send for Bio {}
442unsafe impl Sync for Bio {}
443
444// ── BorrowedBio — non-owning chain view ──────────────────────────────────────
445
446/// A non-owning view of a `BIO*` returned by chain walk operations.
447///
448/// Obtained from [`Bio::next`] or [`Bio::find_type`]. The underlying pointer
449/// is **not** freed when this value is dropped — ownership remains with the
450/// chain.
451///
452/// The lifetime `'a` is tied to the [`Bio`] that produced this value, ensuring
453/// the borrowed pointer cannot outlive the chain it belongs to.
454pub struct BorrowedBio<'a> {
455 /// Wrap in `ManuallyDrop` so the `Bio` destructor is never called.
456 inner: ManuallyDrop<Bio>,
457 _marker: PhantomData<&'a Bio>,
458}
459
460impl BorrowedBio<'_> {
461 /// Return the raw `BIO*` pointer.
462 ///
463 /// The pointer is valid for the lifetime of the `Bio` chain this was
464 /// borrowed from.
465 #[must_use]
466 pub fn as_ptr(&self) -> *mut sys::BIO {
467 self.inner.ptr
468 }
469}
470
471// SAFETY: no destructor — ManuallyDrop prevents BIO_free being called.
472// The lifetime `'a` statically prevents the borrowed BIO* from outliving its chain.
473unsafe impl Send for BorrowedBio<'_> {}
474
475// ── Tests ─────────────────────────────────────────────────────────────────────
476
477#[cfg(test)]
478mod tests {
479 use super::*;
480
481 #[test]
482 fn mem_bio_write_and_data() {
483 let mut bio = MemBio::new().unwrap();
484 bio.write(b"hello").unwrap();
485 bio.write(b" world").unwrap();
486 assert_eq!(bio.data(), b"hello world");
487 }
488
489 #[test]
490 fn mem_bio_empty() {
491 let bio = MemBio::new().unwrap();
492 assert_eq!(bio.data(), b"");
493 }
494
495 #[test]
496 fn mem_bio_buf_zero_copy() {
497 let source = b"PEM data goes here";
498 let bio = MemBioBuf::new(source).unwrap();
499 // Verify the BIO's internal read pointer equals the source slice pointer.
500 let mut char_ptr: *mut std::os::raw::c_char = ptr::null_mut();
501 // BIO_get_mem_data via BIO_ctrl(BIO_CTRL_INFO=3).
502 let len = unsafe {
503 sys::BIO_ctrl(
504 bio.as_ptr(),
505 3, // BIO_CTRL_INFO
506 0,
507 (&raw mut char_ptr).cast::<std::os::raw::c_void>(),
508 )
509 };
510 assert_eq!(usize::try_from(len).unwrap(), source.len());
511 // The data pointer must be the same as the source slice's pointer.
512 assert_eq!(char_ptr.cast::<u8>().cast_const(), source.as_ptr());
513 }
514
515 #[test]
516 fn bio_clone_shares_object() {
517 // Create a MemBio and wrap its underlying pointer in a Bio to test Clone.
518 let mut mem = MemBio::new().unwrap();
519 mem.write(b"test").unwrap();
520
521 // Build a Bio using the MemBio's pointer (up_ref first to share ownership).
522 let raw = mem.as_ptr();
523 unsafe { sys::BIO_up_ref(raw) };
524 let bio = unsafe { Bio::from_ptr_owned(raw) };
525 let bio2 = bio.clone();
526
527 // Both should point to the same BIO object.
528 assert_eq!(bio.as_ptr(), bio2.as_ptr());
529 }
530
531 #[test]
532 fn bio_mem_write_then_read() {
533 // Create a mem BIO, write bytes, read them back via Bio::read.
534 // SAFETY: BIO_new / BIO_s_mem are the canonical way to create a mem BIO.
535 let raw = unsafe { sys::BIO_new(sys::BIO_s_mem()) };
536 assert!(!raw.is_null());
537 let mut bio = unsafe { Bio::from_ptr_owned(raw) };
538
539 let payload = b"hello, BIO";
540 let written = bio.write(payload).unwrap();
541 assert_eq!(written, payload.len());
542
543 let mut buf = [0u8; 64];
544 let nread = bio.read(&mut buf).unwrap();
545 assert_eq!(nread, payload.len());
546 assert_eq!(&buf[..nread], payload);
547 }
548
549 #[test]
550 fn bio_read_ex() {
551 // Same as bio_mem_write_then_read but uses read_ex to verify readbytes count.
552 // SAFETY: BIO_new / BIO_s_mem create a valid mem BIO.
553 let raw = unsafe { sys::BIO_new(sys::BIO_s_mem()) };
554 assert!(!raw.is_null());
555 let mut bio = unsafe { Bio::from_ptr_owned(raw) };
556
557 let payload = b"read_ex test";
558 bio.write(payload).unwrap();
559
560 let mut buf = [0u8; 64];
561 let nread = bio.read_ex(&mut buf).unwrap();
562 assert_eq!(nread, payload.len());
563 assert_eq!(&buf[..nread], payload);
564 }
565
566 #[test]
567 fn bio_pending_on_mem_bio() {
568 // Write known bytes into a mem BIO; pending() must return the byte count.
569 // SAFETY: BIO_new / BIO_s_mem create a valid mem BIO.
570 let raw = unsafe { sys::BIO_new(sys::BIO_s_mem()) };
571 assert!(!raw.is_null());
572 let mut bio = unsafe { Bio::from_ptr_owned(raw) };
573
574 let payload = b"hello, pending";
575 bio.write(payload).unwrap();
576
577 // pending() should equal the number of unread bytes.
578 assert_eq!(bio.pending(), payload.len());
579
580 // wpending() for a mem BIO is 0 (synchronous writes).
581 assert_eq!(bio.wpending(), 0);
582
583 // After reading all bytes, pending() should drop to 0.
584 let mut buf = vec![0u8; payload.len()];
585 let n = bio.read(&mut buf).unwrap();
586 assert_eq!(n, payload.len());
587 assert_eq!(bio.pending(), 0);
588 }
589
590 #[test]
591 fn bio_chain_push_next_pop() {
592 // Create two independent mem BIOs, push one after the other, verify
593 // next() sees the second, then pop() detaches it.
594 // SAFETY: BIO_new / BIO_s_mem create valid mem BIOs.
595 let ptr1 = unsafe { sys::BIO_new(sys::BIO_s_mem()) };
596 let ptr2 = unsafe { sys::BIO_new(sys::BIO_s_mem()) };
597 assert!(!ptr1.is_null());
598 assert!(!ptr2.is_null());
599
600 let bio1 = unsafe { Bio::from_ptr_owned(ptr1) };
601 let bio2 = unsafe { Bio::from_ptr_owned(ptr2) };
602
603 // Remember ptr2 before ownership moves into the chain.
604 let raw2 = bio2.as_ptr();
605
606 // Push bio2 after bio1; bio1 takes ownership of the chain.
607 let mut chain = bio1.push(bio2);
608
609 // next() should return a borrowed view of bio2.
610 {
611 let next = chain.next().expect("chain must have a next BIO");
612 assert_eq!(next.as_ptr(), raw2);
613 } // `next` (BorrowedBio) goes out of scope here, releasing the borrow.
614
615 // pop() detaches chain from its successor; returns Some(bio2_owner).
616 let detached = chain.pop().expect("pop must return the detached tail");
617 assert_eq!(detached.as_ptr(), raw2);
618
619 // After pop, chain has no successor.
620 assert!(chain.next().is_none());
621 }
622}