fix: a TOCTOU race condition while creating temporary directories (#4008)

This commit is contained in:
三咲雅 misaki masa 2026-05-28 18:07:31 +08:00 committed by GitHub
parent 7a51626505
commit 6c30430666
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 97 additions and 27 deletions

View file

@ -0,0 +1,29 @@
use std::ops::Deref;
use mlua::{AnyUserData, UserData, UserDataMethods};
use super::{Lives, PtrCell};
pub(super) struct Behavior {
inner: PtrCell<yazi_scheduler::Behavior>,
}
impl Deref for Behavior {
type Target = yazi_scheduler::Behavior;
fn deref(&self) -> &Self::Target { &self.inner }
}
impl Behavior {
pub(super) fn make(inner: &yazi_scheduler::Behavior) -> mlua::Result<AnyUserData> {
let inner = PtrCell::from(inner);
Lives::scoped_userdata(Self { inner })
}
}
impl UserData for Behavior {
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_method("reset", |_, me, ()| Ok(me.reset()));
}
}

View file

@ -1 +1 @@
yazi_macro::mod_flat!(core file files filter finder folder lives mode preference preview ptr selected tab tabs task tasks which yanked);
yazi_macro::mod_flat!(behavior core file files filter finder folder lives mode preference preview ptr selected tab tabs task tasks which yanked);

View file

@ -4,13 +4,14 @@ use mlua::{AnyUserData, LuaSerdeExt, UserData, UserDataFields, Value};
use yazi_binding::{SER_OPT, cached_field};
use super::{Lives, PtrCell};
use crate::lives::TaskSnap;
use crate::lives::{Behavior, TaskSnap};
pub(super) struct Tasks {
inner: PtrCell<yazi_core::tasks::Tasks>,
v_snaps: Option<Value>,
v_summary: Option<Value>,
v_behavior: Option<Value>,
v_snaps: Option<Value>,
v_summary: Option<Value>,
}
impl Deref for Tasks {
@ -24,8 +25,9 @@ impl Tasks {
Lives::scoped_userdata(Self {
inner: inner.into(),
v_snaps: None,
v_summary: None,
v_behavior: None,
v_snaps: None,
v_summary: None,
})
}
}
@ -34,6 +36,8 @@ impl UserData for Tasks {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("cursor", |_, me| Ok(me.cursor));
cached_field!(fields, behavior, |_, me| Behavior::make(&me.scheduler.behavior));
cached_field!(fields, snaps, |lua, me| {
let tbl = lua.create_table_with_capacity(me.snaps.len(), 0)?;
for snap in &me.snaps {

View file

@ -16,6 +16,7 @@ impl Actor for Spawn {
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
succ!(match form.opt {
TaskOpt::Cut(r#in) => cx.tasks.scheduler.file_cut(r#in),
TaskOpt::Plugin(r#in) => cx.tasks.scheduler.plugin_entry(r#in),
})
}

View file

@ -1,11 +1,9 @@
#[cfg(unix)]
use std::os::unix::fs::DirBuilderExt;
use std::{fs::DirBuilder, path::PathBuf};
use std::path::PathBuf;
use anyhow::{Context, Result};
use serde::{Deserialize, Deserializer, Serialize};
use yazi_codegen::DeserializeOver2;
use yazi_fs::Xdg;
use yazi_fs::{Xdg, create_owned_dir_blocking};
use yazi_shared::{SStr, timestamp_us};
use yazi_shim::toml::DeserializeOverHook;
@ -51,12 +49,7 @@ impl Preview {
impl DeserializeOverHook for Preview {
fn deserialize_over_hook(self) -> Result<Self, toml::de::Error> {
#[cfg(unix)]
let result = DirBuilder::new().mode(0o700).recursive(true).create(&self.cache_dir);
#[cfg(not(unix))]
let result = DirBuilder::new().recursive(true).create(&self.cache_dir);
result
create_owned_dir_blocking(&self.cache_dir)
.context(format!("Failed to create cache directory: {}", self.cache_dir.display()))
.map_err(serde::de::Error::custom)?;

View file

@ -53,18 +53,14 @@ impl Stream {
#[cfg(unix)]
async fn socket_file() -> io::Result<&'static std::path::PathBuf> {
use tokio::{fs::DirBuilder, sync::OnceCell};
use yazi_fs::Xdg;
use tokio::sync::OnceCell;
use yazi_fs::{Xdg, create_owned_dir};
static ONCE: tokio::sync::OnceCell<std::path::PathBuf> = OnceCell::const_new();
static ONCE: OnceCell<std::path::PathBuf> = OnceCell::const_new();
ONCE
.get_or_try_init(|| async move {
let p = Xdg::runtime_dir();
#[cfg(unix)]
DirBuilder::new().mode(0o700).recursive(true).create(p).await?;
#[cfg(not(unix))]
DirBuilder::new().recursive(true).create(p).await?;
create_owned_dir(p).await?;
Ok(p.join(".dds.sock"))
})

View file

@ -1,4 +1,5 @@
use tokio::io;
use std::{io, path::Path};
use yazi_shared::url::{Component, UrlBuf, UrlLike};
#[inline]
@ -74,3 +75,47 @@ fn test_max_common_root() {
assert(&["/aa/bb/cc", "/aa/dd/ee"], "/aa");
assert(&["/aa/bb/cc", "/aa/bb/cc/dd/ee", "/aa/bb/cc/ff"], "/aa/bb");
}
pub async fn create_owned_dir(p: &Path) -> io::Result<()> {
let p = p.to_owned();
tokio::task::spawn_blocking(move || create_owned_dir_blocking(&p)).await?
}
pub fn create_owned_dir_blocking(p: &Path) -> io::Result<()> {
#[cfg(unix)]
{
use std::{fs::{DirBuilder, OpenOptions}, mem, os::unix::{fs::{DirBuilderExt, OpenOptionsExt}, io::AsRawFd}};
use libc::{O_DIRECTORY, O_NOFOLLOW};
use uzers::Users;
use yazi_shared::USERS_CACHE;
DirBuilder::new().mode(0o700).recursive(true).create(p)?;
let dir = OpenOptions::new().read(true).custom_flags(O_DIRECTORY | O_NOFOLLOW).open(p)?;
let mut stat: libc::stat = unsafe { mem::zeroed() };
if unsafe { libc::fstat(dir.as_raw_fd(), &mut stat) } != 0 {
return Err(io::Error::last_os_error());
}
// Reject directories not owned by the current user.
let uid = USERS_CACHE.get_current_uid();
if stat.st_uid != uid {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
format!("directory {:?} is owned by uid {} but current uid is {}", p, stat.st_uid, uid),
));
}
// Enforce mode 0o700 via the fd.
if unsafe { libc::fchmod(dir.as_raw_fd(), 0o700) } != 0 {
return Err(io::Error::last_os_error());
}
Ok(())
}
#[cfg(not(unix))]
{
std::fs::DirBuilder::new().recursive(true).create(p)
}
}

View file

@ -13,7 +13,7 @@ function Tip:layout()
:direction(ui.Layout.VERTICAL)
:constraints({
ui.Constraint.Fill(1),
ui.Constraint.Length(1),
ui.Constraint.Length(3),
ui.Constraint.Fill(1),
})
:split(self._area)
@ -24,6 +24,7 @@ function Tip:reflow() return {} end
function Tip:redraw()
return {
ui.Clear(self._chunks[2]),
ui.Text(self._text):area(self._chunks[2]):align(ui.Align.CENTER):fg("black"):bg("yellow"):bold(),
ui.Text(""):area(self._chunks[2]):align(ui.Align.CENTER):bg("yellow"),
ui.Text(self._text):area(self._chunks[2]:pad(ui.Pad.xy(1, 1))):align(ui.Align.CENTER):fg("black"):bold(),
}
end

View file

@ -12,6 +12,7 @@ function M.selected_uri_list()
end
function M.cut_uri_list(list)
cx.tasks.behavior:reset()
for line in list:gmatch("[^\r\n]+") do
if line:sub(1, 7) ~= "file://" then
goto continue