I am writing a GTK Rust application and I need to write to files. The file could possibly require root privileges, as such I have built some error detection that will attempt to acquire such permissions by using a file URI that starts with admin://.
My code looks as follows:
let admin_uri = format!(
"admin://{}",
file.path()
.expect("File does not have a path?")
.to_string_lossy()
);
let file = gio::File::for_uri(&admin_uri);
// Mount admin volume
let res = file
.mount_enclosing_volume_future(MountMountFlags::NONE, MountOperation::NONE)
.await;
if let Err(e) = res {
println!("Failed to mount admin volume: {e}");
}
println!("Writing with admin perms");
let res = file
.replace_contents_future(
content.clone(),
None,
false,
FileCreateFlags::NONE,
)
.await;
println!("Finished");
if let Err((_, e)) = res {
println!("Failed to write file with admin perms: {e}");
}
Now, this works, as I am prompted for password authentication and the file is correctly written even when it requires elevated privileges. However, after the file is written the application crashes due to an assertion error in the GString wrapper.
Backtrace
thread 'main' panicked at /home/aleb/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/glib-0.21.1/src/gstring.rs:2054:9:
assertion failed: !ptr.is_null()
stack backtrace:
0: __rustc::rust_begin_unwind
at /rustc/6b00bc3880198600130e1cf62b8f8a93494488cc/library/std/src/panicking.rs:697:5
1: core::panicking::panic_fmt
at /rustc/6b00bc3880198600130e1cf62b8f8a93494488cc/library/core/src/panicking.rs:75:14
2: core::panicking::panic
at /rustc/6b00bc3880198600130e1cf62b8f8a93494488cc/library/core/src/panicking.rs:145:5
3: <glib::gstring::GString as glib::translate::FromGlibPtrFull<*mut u8>>::from_glib_full
at /home/aleb/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/glib-0.21.1/src/gstring.rs:2054:9
4: glib::translate::from_glib_full
at /home/aleb/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/glib-0.21.1/src/translate.rs:1639:5
5: <glib::gstring::GString as glib::translate::FromGlibPtrFull<*mut i8>>::from_glib_full
at /home/aleb/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/glib-0.21.1/src/gstring.rs:2070:9
6: glib::translate::from_glib_full
at /home/aleb/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/glib-0.21.1/src/translate.rs:1639:5
7: gio::file::FileExtManual::replace_contents_async::replace_contents_async_trampoline
at /home/aleb/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/gio-0.21.1/src/file.rs:226:31
8: <unknown>
9: <unknown>
10: <unknown>
11: <unknown>
12: <unknown>
13: <unknown>
14: <unknown>
15: <unknown>
16: <unknown>
17: <unknown>
18: <unknown>
19: <unknown>
20: <unknown>
21: <unknown>
22: <unknown>
23: <unknown>
24: <unknown>
25: <unknown>
26: <unknown>
27: <unknown>
28: <unknown>
29: g_main_context_iteration
30: g_application_run
31: gio::application::ApplicationExtManual::run_with_args
at /home/aleb/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/gio-0.21.1/src/application.rs:25:13
32: gio::application::ApplicationExtManual::run
at /home/aleb/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/gio-0.21.1/src/application.rs:17:9
33: desktop_file_editor::main
at ./src/main.rs:34:5
34: core::ops::function::FnOnce::call_once
at /home/aleb/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ops/function.rs:250:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
Looking at the gtk-rs source, the assertion debug_assert!(!ptr.is_null()); from the following function fails:
impl FromGlibPtrFull<*mut u8> for GString {
#[inline]
unsafe fn from_glib_full(ptr: *mut u8) -> Self {
debug_assert!(!ptr.is_null());
let cstr = CStr::from_ptr(ptr as *const _);
// Check for valid UTF-8 here
debug_assert!(cstr.to_str().is_ok());
Self(Inner::Foreign {
ptr: ptr::NonNull::new_unchecked(ptr as *mut _),
len: cstr.to_bytes().len(),
})
}
}
This looks like the wrapper is trying to convert a pointer to a GString but the pointer is null.
From the backtrace I found an interesting place that seems to be the origin of the error: the line Ok((contents, from_glib_full(new_etag))) from the following function.
unsafe extern "C" fn replace_contents_async_trampoline<
B: AsRef<[u8]> + Send + 'static,
R: FnOnce(Result<(B, glib::GString), (B, glib::Error)>) + 'static,
>(
_source_object: *mut glib::gobject_ffi::GObject,
res: *mut ffi::GAsyncResult,
user_data: glib::ffi::gpointer,
) {
let user_data: Box<(glib::thread_guard::ThreadGuard<R>, B)> =
Box::from_raw(user_data as *mut _);
let (callback, contents) = *user_data;
let callback = callback.into_inner();
let mut error = ptr::null_mut();
let mut new_etag = ptr::null_mut();
let _ = ffi::g_file_replace_contents_finish(
_source_object as *mut _,
res,
&mut new_etag,
&mut error,
);
let result = if error.is_null() {
Ok((contents, from_glib_full(new_etag)))
} else {
Err((contents, from_glib_full(error)))
};
callback(result);
}
My understanding is that this function is trying to create a GString from the new_etag raw pointer, but the pointer is null. This could be because when I call the function file.replace_contents_future() I pass None as the etag argument (since I am not interested in the etag and I just want to edit the file). Could me passing None cause the etag pointer to be null, and the gtk-rs wrapper of gio::File forgets to check for a null value there?
I am not sure if this is me doing something wrong or if there is a bug in gtk-rs, regardless I need help. The curious tidbit is that this only happens when using an admin:// URI; when I do the same thing using the normal file path the panic does not happen.
Would love for someone to shine some more light on this issue.