blob: e9e10cb9734ad298ba95611822e011fa7a0686cf [file] [log] [blame]
// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use crate::execution::loop_entry::enter_syscall_loop;
use crate::ptrace::{PtraceCoreState, ptrace_attach_from_state};
use crate::task::{CurrentTask, DelayedReleaser, ExitStatus, TaskBuilder};
use anyhow::Error;
use starnix_logging::{log_error, log_warn};
use starnix_sync::{LockBefore, Locked, TaskRelease, Unlocked};
use starnix_types::ownership::WeakRef;
use starnix_uapi::errors::Errno;
use starnix_uapi::{errno, error};
use std::os::unix::thread::JoinHandleExt;
use std::sync::Arc;
use std::sync::mpsc::sync_channel;
pub fn execute_task_with_prerun_result<L, F, R, G>(
locked: &mut Locked<L>,
task_builder: TaskBuilder,
pre_run: F,
task_complete: G,
ptrace_state: Option<PtraceCoreState>,
) -> Result<R, Errno>
where
L: LockBefore<TaskRelease>,
F: FnOnce(&mut Locked<Unlocked>, &mut CurrentTask) -> Result<R, Errno> + Send + Sync + 'static,
R: Send + Sync + 'static,
G: FnOnce(Result<ExitStatus, Error>) + Send + Sync + 'static,
{
let (sender, receiver) = sync_channel::<Result<R, Errno>>(1);
execute_task(
locked,
task_builder,
move |current_task, locked| match pre_run(current_task, locked) {
Err(errno) => {
let _ = sender.send(Err(errno.clone()));
Err(errno)
}
Ok(value) => sender.send(Ok(value)).map_err(|error| {
log_error!("Unable to send `pre_run` result: {error:?}");
errno!(EINVAL)
}),
},
task_complete,
ptrace_state,
)?;
receiver.recv().map_err(|e| {
log_error!("Unable to retrieve result from `pre_run`: {e:?}");
errno!(EINVAL)
})?
}
pub fn execute_task<L, F, G>(
locked: &mut Locked<L>,
task_builder: TaskBuilder,
pre_run: F,
task_complete: G,
ptrace_state: Option<PtraceCoreState>,
) -> Result<(), Errno>
where
L: LockBefore<TaskRelease>,
F: FnOnce(&mut Locked<Unlocked>, &mut CurrentTask) -> Result<(), Errno> + Send + Sync + 'static,
G: FnOnce(Result<ExitStatus, Error>) + Send + Sync + 'static,
{
// Set the process handle to the new task's process, so the new thread is spawned in that
// process.
let process_handle = task_builder.task.thread_group().process.raw_handle();
// SAFETY: thrd_set_zx_process is a safe function that only sets the process handle for the
// current thread. Which handle is used here is only for diagnostics.
let old_process_handle = unsafe { thrd_set_zx_process(process_handle) };
scopeguard::defer! {
// SAFETY: thrd_set_zx_process is a safe function that only sets the process handle for the
// current thread. Which handle is used here is only for diagnostics.
unsafe {
thrd_set_zx_process(old_process_handle);
};
};
let weak_task = WeakRef::from(&task_builder.task);
let ref_task = weak_task.upgrade().unwrap();
if let Some(ptrace_state) = ptrace_state {
let _ = ptrace_attach_from_state(
locked.cast_locked::<TaskRelease>(),
&task_builder.task,
ptrace_state,
);
}
// Hold a lock on the task's thread slot until we have a chance to initialize it.
let mut task_thread_guard = ref_task.thread.write();
// Spawn the process' thread. Note, this closure ends up executing in the process referred to by
// `process_handle`.
let (sender, receiver) = sync_channel::<TaskBuilder>(1);
let result = std::thread::Builder::new().name("user-thread".to_string()).spawn(move || {
// It's safe to create a new lock context since we are on a new thread.
#[allow(
clippy::undocumented_unsafe_blocks,
reason = "Force documented unsafe blocks in Starnix"
)]
let locked = unsafe { Unlocked::new() };
// Note, cross-process shared resources allocated in this function that aren't freed by the
// Zircon kernel upon thread and/or process termination (like mappings in the shared region)
// should be freed using the delayed finalizer mechanism and Task drop.
let mut current_task: CurrentTask = receiver
.recv()
.expect("caller should always send task builder before disconnecting")
.into();
// We don't need the receiver anymore. If we don't drop the receiver now, we'll keep it
// allocated for the lifetime of the thread.
std::mem::drop(receiver);
let pre_run_result = { pre_run(locked, &mut current_task) };
if pre_run_result.is_err() {
// Only log if the pre run didn't exit the task. Otherwise, consider this is expected
// by the caller.
if current_task.exit_status().is_none() {
log_error!("Pre run failed from {pre_run_result:?}. The task will not be run.");
}
// Drop the task_complete callback to ensure that the closure isn't holding any
// releasables.
std::mem::drop(task_complete);
} else {
let exit_status = enter_syscall_loop(locked, &mut current_task);
current_task.write().set_exit_status(exit_status.clone());
task_complete(Ok(exit_status));
}
// `release` must be called as the absolute last action on this thread to ensure that
// any deferred release are done before it.
current_task.release(locked);
// Ensure that no releasables are registered after this point as we unwind the stack.
DelayedReleaser::finalize();
});
let join_handle = match result {
Ok(handle) => handle,
Err(e) => {
task_builder.release(locked);
match e.kind() {
std::io::ErrorKind::WouldBlock => return error!(EAGAIN),
other => panic!("unexpected error on thread spawn: {other}"),
}
}
};
// Update the thread and task information before sending the task_builder to the spawned thread.
// This will make sure the mapping between linux tid and fuchsia koid is set before trace events
// are emitted from the linux code.
// Set the task's thread handle
let pthread = join_handle.as_pthread_t();
#[allow(
clippy::undocumented_unsafe_blocks,
reason = "Force documented unsafe blocks in Starnix"
)]
let raw_thread_handle =
unsafe { zx::Unowned::<'_, zx::Thread>::from_raw_handle(thrd_get_zx_handle(pthread)) };
*task_thread_guard = Some(Arc::new(
raw_thread_handle
.duplicate(zx::Rights::SAME_RIGHTS)
.expect("must have RIGHT_DUPLICATE on handle we created"),
));
// Now that the task has a thread handle, update the thread's role using the policy configured.
drop(task_thread_guard);
if let Err(err) = ref_task.sync_scheduler_state_to_role() {
log_warn!(err:?; "Couldn't update freshly spawned thread's profile.");
}
// Record the thread and process ids for tracing after the task_thread is unlocked.
ref_task.record_pid_koid_mapping();
// Wait to send the `TaskBuilder` to the spawned thread until we know that it
// spawned successfully, as we need to ensure the builder is always explicitly
// released.
sender
.send(task_builder)
.expect("receiver should not be disconnected because thread spawned successfully");
Ok(())
}
unsafe extern "C" {
/// Sets the process handle used to create new threads, for the current thread.
fn thrd_set_zx_process(handle: zx::sys::zx_handle_t) -> zx::sys::zx_handle_t;
// Gets the thread handle underlying a specific thread.
// In C the 'thread' parameter is thrd_t which on Fuchsia is the same as pthread_t.
fn thrd_get_zx_handle(thread: u64) -> zx::sys::zx_handle_t;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ptrace::StopState;
use crate::signals::SignalInfo;
use crate::testing::*;
use starnix_uapi::signals::{SIGCONT, SIGSTOP};
#[::fuchsia::test]
async fn test_block_while_stopped_stop_and_continue() {
spawn_kernel_and_run(async |locked, task| {
// block_while_stopped must immediately returned if the task is not stopped.
task.block_while_stopped(locked);
// Stop the task.
task.thread_group().set_stopped(
StopState::GroupStopping,
Some(SignalInfo::default(SIGSTOP)),
false,
);
let thread = std::thread::spawn({
let task = task.weak_task();
move || {
let task = task.upgrade().expect("task must be alive");
// Wait for the task to have a waiter.
while !task.read().is_blocked() {
std::thread::sleep(std::time::Duration::from_millis(10));
}
// Continue the task.
task.thread_group().set_stopped(
StopState::Waking,
Some(SignalInfo::default(SIGCONT)),
false,
);
}
});
// Block until continued.
task.block_while_stopped(locked);
// Join the thread, which will ensure set_stopped terminated.
thread.join().expect("joined");
// The task should not be blocked anymore.
task.block_while_stopped(locked);
})
.await;
}
#[::fuchsia::test]
async fn test_block_while_stopped_stop_and_exit() {
spawn_kernel_and_run(async |locked, task| {
// block_while_stopped must immediately returned if the task is neither stopped nor exited.
task.block_while_stopped(locked);
// Stop the task.
task.thread_group().set_stopped(
StopState::GroupStopping,
Some(SignalInfo::default(SIGSTOP)),
false,
);
let thread = std::thread::spawn({
let task = task.weak_task();
move || {
#[allow(
clippy::undocumented_unsafe_blocks,
reason = "Force documented unsafe blocks in Starnix"
)]
let locked = unsafe { Unlocked::new() };
let task = task.upgrade().expect("task must be alive");
// Wait for the task to have a waiter.
while !task.read().is_blocked() {
std::thread::sleep(std::time::Duration::from_millis(10));
}
// exit the task.
task.thread_group().exit(locked, ExitStatus::Exit(1), None);
}
});
// Block until continued.
task.block_while_stopped(locked);
// Join the task, which will ensure thread_group.exit terminated.
thread.join().expect("joined");
// The task should not be blocked because it is stopped.
task.block_while_stopped(locked);
})
.await;
}
}