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

[project-vvm-async-api] "buffer"をRustの世界で保持し続ける #525

126 changes: 126 additions & 0 deletions crates/voicevox_core_c_api/src/drop_check.rs
Copy link
Member Author

Choose a reason for hiding this comment

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

あくまでチェックということで、MaybeUninitへの書き込みとCString::{from,into}_rawをここから外に出しました。

メソッド名および概念は"whitelist"(動詞兼名詞)と"blacklist"(動詞兼名詞)としてみました。

Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
use std::{
collections::BTreeSet,
ffi::{c_char, CStr, CString},
sync::Mutex,
};

/// dropして良い`*mut c_char`を把握し、チェックする。
///
/// `Mutex`による内部可変性を持ち、すべての操作は共有参照から行うことができる。
///
/// # Motivation
///
/// `CString`は`Box<impl Sized>`と同様Cの世界でもポインタ一つで実体を表すことができるため、こちら側
/// で管理すべきものは本来無い。しかしながら本クレートが提供するAPIには「解放不要」な文字列を返すも
/// のが含まれている。ユーザーが誤ってそのような文字列を解放するのは未定義動作 (undefined behavior)
/// であるため、綺麗にSEGVするとも限らない。`once_cell::sync::Lazy`由来の文字列の場合、最悪解放が成
/// 功してしまう。
///
/// この構造体はCの世界から帰ってきた`*mut c_char`を`CString`としてdropする際、それが本当にこちら側
/// が送り出した`CString`かどうかをチェックする。
///
/// Cの世界に`CString`を送り出す前に`whitelist`を通し、戻って来た`*mut c_char`を`CString`にしてdrop
/// する前に`check`に通す。
Comment on lines +22 to +23
Copy link
Member

@Hiroshiba Hiroshiba Jun 18, 2023

Choose a reason for hiding this comment

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

将来なにか関数増えたときにcheck通すの忘れそうですね。。
仕組みで解決できないので、まあ思い出すしか無さそう。

Copy link
Member Author

Choose a reason for hiding this comment

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

ですね... ベタ書きしたものをCの世界に向けて発射できる以上、どうしようもない気がします。

pub(crate) static C_STRING_DROP_CHECKER: CStringDropChecker = CStringDropChecker::new();

pub(crate) struct CStringDropChecker(Mutex<Inner>);

struct Inner {
owned_str_addrs: BTreeSet<usize>,
static_str_addrs: BTreeSet<usize>,
}

impl CStringDropChecker {
const fn new() -> Self {
Self(Mutex::new(Inner {
owned_str_addrs: BTreeSet::new(),
static_str_addrs: BTreeSet::new(),
}))
}

/// `CString`をホワイトリストに追加する。
///
/// Cの世界に`CString`を送り出す前にこの関数を挟む。
pub(crate) fn whitelist(&self, s: CString) -> CString {
let Inner {
owned_str_addrs, ..
} = &mut *self.0.lock().unwrap();

let duplicated = !owned_str_addrs.insert(s.as_ptr() as usize);
assert!(!duplicated, "duplicated");
s
}

/// `&'static CStr`をブラックリストに追加する。
///
/// Cの世界に`Lazy`由来の`&'static CStr`を送り出す前にこの関数を挟む。
///
/// ホワイトリストとブラックリストは重複しないと考えてよく、ブラックリストはエラーメセージの制御
/// のためのみに使われる。
pub(crate) fn blacklist(&self, s: &'static CStr) -> &'static CStr {
let Inner {
static_str_addrs, ..
} = &mut *self.0.lock().unwrap();

static_str_addrs.insert(s.as_ptr() as usize);
s
}

/// `*mut c_char`が`whitelist`を通ったものかどうかチェックする。
///
/// # Panics
///
/// `ptr`が`Self::whitelist`を経由したものではないならパニックする。
pub(crate) fn check(&self, ptr: *mut c_char) -> *mut c_char {
let Inner {
owned_str_addrs,
static_str_addrs,
..
} = &mut *self.0.lock().unwrap();

let addr = ptr as usize;
if !owned_str_addrs.remove(&addr) {
if static_str_addrs.contains(&addr) {
panic!(
"解放しようとしたポインタはvoicevox_core管理下のものですが、\
voicevox_coreがアンロードされるまで永続する文字列に対するものです。\
解放することはできません",
)
}
panic!(
"解放しようとしたポインタはvoicevox_coreの管理下にありません。\
誤ったポインタであるか、二重解放になっていることが考えられます",
);
}
ptr
}
}

#[cfg(test)]
mod tests {
use std::ffi::{c_char, CStr};

use super::CStringDropChecker;

#[test]
#[should_panic(
expected = "解放しようとしたポインタはvoicevox_coreの管理下にありません。誤ったポインタであるか、二重解放になっていることが考えられます"
)]
fn it_denies_unknown_char_ptr() {
let checker = CStringDropChecker::new();
let s = CStr::from_bytes_with_nul(b"\0").unwrap().to_owned();
checker.check(s.into_raw());
}

#[test]
#[should_panic(
expected = "解放しようとしたポインタはvoicevox_core管理下のものですが、voicevox_coreがアンロードされるまで永続する文字列に対するものです。解放することはできません"
)]
fn it_denies_known_static_char_ptr() {
let checker = CStringDropChecker::new();
checker.blacklist(STATIC);
checker.check(STATIC.as_ptr() as *mut c_char);

static STATIC: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(b"\0") };
}
}
193 changes: 0 additions & 193 deletions crates/voicevox_core_c_api/src/helpers.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use std::alloc::Layout;
use std::collections::{BTreeMap, BTreeSet};
use std::fmt::Debug;

use const_default::ConstDefault;
Expand Down Expand Up @@ -177,194 +175,3 @@ impl ConstDefault for VoicevoxSynthesisOptions {
}
};
}

// libcのmallocで追加のアロケーションを行うことなく、`Vec<u8>`や`Vec<f32>`の内容を直接Cの世界に貸し出す。

/// Rustの世界の`Box<[impl Copy]>`をCの世界に貸し出すため、アドレスとレイアウトを管理するもの。
///
/// `Mutex`による内部可変性を持ち、すべての操作は`&self`から行うことができる。
pub(crate) struct BufferManager(Mutex<BufferManagerInner>);

struct BufferManagerInner {
address_to_layout_table: BTreeMap<usize, Layout>,
owned_str_addrs: BTreeSet<usize>,
static_str_addrs: BTreeSet<usize>,
}

impl BufferManager {
pub const fn new() -> Self {
Self(Mutex::new(BufferManagerInner {
address_to_layout_table: BTreeMap::new(),
owned_str_addrs: BTreeSet::new(),
static_str_addrs: BTreeSet::new(),
}))
}

pub fn vec_into_raw<T: Copy>(&self, vec: Vec<T>) -> (*mut T, usize) {
let BufferManagerInner {
address_to_layout_table,
..
} = &mut *self.0.lock().unwrap();

let slice = Box::leak(vec.into_boxed_slice());
let layout = Layout::for_value(slice);
let len = slice.len();
let ptr = slice.as_mut_ptr();
let addr = ptr as usize;

let not_occupied = address_to_layout_table.insert(addr, layout).is_none();

assert!(not_occupied, "すでに値が入っている状態はおかしい");

(ptr, len)
}

/// `vec_into_raw`でC API利用側に貸し出したポインタに対し、デアロケートする。
///
/// # Safety
///
/// - `buffer_ptr`は`vec_into_raw`で取得したものであること。
pub unsafe fn dealloc_slice<T: Copy>(&self, buffer_ptr: *const T) {
let BufferManagerInner {
address_to_layout_table,
..
} = &mut *self.0.lock().unwrap();

let addr = buffer_ptr as usize;
let layout = address_to_layout_table.remove(&addr).expect(
"解放しようとしたポインタはvoicevox_coreの管理下にありません。\
誤ったポインタであるか、二重解放になっていることが考えられます",
);

if layout.size() > 0 {
// `T: Copy`より、`T: !Drop`であるため`drop_in_place`は不要

// SAFETY:
// - `addr`と`layout`は対応したものである
// - `layout.size() > 0`より、`addr`はダングリングではない有効なポインタである
std::alloc::dealloc(addr as *mut u8, layout);
}
}

pub fn c_string_into_raw(&self, s: CString) -> *mut c_char {
let BufferManagerInner {
owned_str_addrs, ..
} = &mut *self.0.lock().unwrap();

let ptr = s.into_raw();
owned_str_addrs.insert(ptr as _);
ptr
}

/// `c_string_into_raw`でC API利用側に貸し出したポインタに対し、デアロケートする。
///
/// # Safety
///
/// - `ptr`は`c_string_into_raw`で取得したものであること。
pub unsafe fn dealloc_c_string(&self, ptr: *mut c_char) {
let BufferManagerInner {
owned_str_addrs,
static_str_addrs,
..
} = &mut *self.0.lock().unwrap();

if !owned_str_addrs.remove(&(ptr as _)) {
if static_str_addrs.contains(&(ptr as _)) {
panic!(
"解放しようとしたポインタはvoicevox_core管理下のものですが、\
voicevox_coreがアンロードされるまで永続する文字列に対するものです。\
解放することはできません",
)
}
panic!(
"解放しようとしたポインタはvoicevox_coreの管理下にありません。\
誤ったポインタであるか、二重解放になっていることが考えられます",
);
}
drop(CString::from_raw(ptr));
}

pub fn memorize_static_str(&self, ptr: *const c_char) -> *const c_char {
let BufferManagerInner {
static_str_addrs, ..
} = &mut *self.0.lock().unwrap();

static_str_addrs.insert(ptr as _);
ptr
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn buffer_manager_works() {
let mut buffer_manager = BufferManager::new();

rent_and_dealloc(&mut buffer_manager, vec::<()>(0, &[]));
rent_and_dealloc(&mut buffer_manager, vec(0, &[()]));
rent_and_dealloc(&mut buffer_manager, vec(2, &[()]));

rent_and_dealloc(&mut buffer_manager, vec::<u8>(0, &[]));
rent_and_dealloc(&mut buffer_manager, vec(0, &[0u8]));
rent_and_dealloc(&mut buffer_manager, vec(2, &[0u8]));

rent_and_dealloc(&mut buffer_manager, vec::<f32>(0, &[]));
rent_and_dealloc(&mut buffer_manager, vec(0, &[0f32]));
rent_and_dealloc(&mut buffer_manager, vec(2, &[0f32]));

fn rent_and_dealloc(buffer_manager: &mut BufferManager, vec: Vec<impl Copy>) {
let expected_len = vec.len();
let (ptr, len) = buffer_manager.vec_into_raw(vec);
assert_eq!(expected_len, len);
unsafe {
buffer_manager.dealloc_slice(ptr);
}
}

fn vec<T: Copy>(initial_cap: usize, elems: &[T]) -> Vec<T> {
let mut vec = Vec::with_capacity(initial_cap);
vec.extend_from_slice(elems);
vec
}
}

#[test]
#[should_panic(
expected = "解放しようとしたポインタはvoicevox_coreの管理下にありません。誤ったポインタであるか、二重解放になっていることが考えられます"
)]
fn buffer_manager_denies_unknown_slice_ptr() {
let buffer_manager = BufferManager::new();
unsafe {
let x = 42;
buffer_manager.dealloc_slice(&x as *const i32);
}
}

#[test]
#[should_panic(
expected = "解放しようとしたポインタはvoicevox_coreの管理下にありません。誤ったポインタであるか、二重解放になっていることが考えられます"
)]
fn buffer_manager_denies_unknown_char_ptr() {
let buffer_manager = BufferManager::new();
unsafe {
let s = CStr::from_bytes_with_nul(b"\0").unwrap().to_owned();
buffer_manager.dealloc_c_string(s.into_raw());
}
}

#[test]
#[should_panic(
expected = "解放しようとしたポインタはvoicevox_core管理下のものですが、voicevox_coreがアンロードされるまで永続する文字列に対するものです。解放することはできません"
)]
fn buffer_manager_denies_known_static_char_ptr() {
let buffer_manager = BufferManager::new();
unsafe {
buffer_manager.memorize_static_str(STATIC.as_ptr() as _);
buffer_manager.dealloc_c_string(STATIC.as_ptr() as *mut c_char);
}

static STATIC: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(b"\0") };
}
}
Loading