Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions os/StarryOS/kernel/src/file/pidfd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,18 @@ use core::{

use ax_errno::{AxError, AxResult};
use axpoll::{IoEvents, PollSet, Pollable};
use starry_process::Pid;

use crate::{
file::FileLike,
task::{ProcessData, Thread},
task::{ProcessData, Thread, get_process_data},
};

pub struct PidFd {
proc_data: Weak<ProcessData>,
exit_event: Arc<PollSet>,
thread_exit: Option<Arc<AtomicBool>>,
tid: Option<Pid>,

non_blocking: AtomicBool,
}
Expand All @@ -28,21 +30,31 @@ impl PidFd {
proc_data: Arc::downgrade(proc_data),
exit_event: proc_data.exit_event.clone(),
thread_exit: None,
tid: None,

non_blocking: AtomicBool::new(false),
}
}

pub fn new_thread(thread: &Thread) -> Self {
pub fn new_thread(thread: &Thread, tid: Pid) -> Self {
Self {
proc_data: Arc::downgrade(&thread.proc_data),
exit_event: thread.exit_event.clone(),
thread_exit: Some(thread.exit.clone()),
tid: Some(tid),

non_blocking: AtomicBool::new(false),
}
}

pub fn is_thread(&self) -> bool {
self.tid.is_some()
}

pub fn tid(&self) -> Option<Pid> {
self.tid
}

pub fn process_data(&self) -> AxResult<Arc<ProcessData>> {
// For threads, the pidfd is invalid once the thread exits, even if its
// process is still alive.
Expand All @@ -51,7 +63,12 @@ impl PidFd {
{
return Err(AxError::NoSuchProcess);
}
self.proc_data.upgrade().ok_or(AxError::NoSuchProcess)
let proc_data = self.proc_data.upgrade().ok_or(AxError::NoSuchProcess)?;
// `ProcessData` may outlive `waitpid` while the pid is no longer in
// `PROCESS_TABLE`. Linux pidfd ops on a reaped pid return ESRCH instead
// of falling through to EBADF from an empty fd table.
get_process_data(proc_data.proc.pid())?;
Ok(proc_data)
}
}
impl FileLike for PidFd {
Expand Down
148 changes: 122 additions & 26 deletions os/StarryOS/kernel/src/syscall/fs/pidfd.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
use alloc::sync::Arc;

use ax_errno::{AxError, AxResult};
use ax_task::current;
use bitflags::bitflags;
use linux_raw_sys::general::SI_USER;
use starry_signal::SignalInfo;
use linux_raw_sys::general::{SI_TKILL, SI_USER};
use starry_signal::{SignalInfo, Signo};
use starry_vm::VmPtr;

use crate::{
file::{FD_TABLE, FileLike, PidFd, add_file_like},
syscall::signal::{check_kill_permission, make_queue_signal_info, make_siginfo},
task::{AsThread, get_process_data, get_task, send_signal_to_process},
syscall::signal::check_kill_permission,
task::{
AsThread, get_process_data, get_task, send_signal_to_process, send_signal_to_process_group,
send_signal_to_thread,
},
};

bitflags! {
Expand All @@ -17,20 +24,56 @@ bitflags! {
}
}

bitflags! {
#[derive(Debug, Clone, Copy, Default)]
struct PidFdSignalFlags: u32 {
const THREAD = 1 << 0;
const THREAD_GROUP = 1 << 1;
const PROCESS_GROUP = 1 << 2;
}
}

#[derive(Clone, Copy, PartialEq, Eq)]
enum PidFdSignalScope {
Thread,
ThreadGroup,
ProcessGroup,
}

fn parse_signo(signo: u32) -> AxResult<Signo> {
Signo::from_repr(signo as u8).ok_or(AxError::InvalidInput)
}

fn make_pidfd_siginfo(signo: Signo, scope: PidFdSignalScope) -> SignalInfo {
let code = if scope == PidFdSignalScope::Thread {
SI_TKILL
} else {
SI_USER as _

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

get_task(pid) 找到非 leader 线程时,返回 AxError::NotFoundENOENT

Linux 内核使用 pid_task(pid, PIDTYPE_TGID) 查找,非 leader 线程的 tid 无法匹配 TGID,返回 ESRCH。虽然语义接近,但 errno 不一致。

次要问题,可在后续统一。

};
let curr = current();
let thread = curr.as_thread();
SignalInfo::new_user(signo, code, thread.proc_data.proc.pid(), thread.cred().uid)
}

pub fn sys_pidfd_open(pid: u32, flags: u32) -> AxResult<isize> {
debug!("sys_pidfd_open <= pid: {pid}, flags: {flags}");

let flags = PidFdFlags::from_bits(flags).ok_or(AxError::InvalidInput)?;

// Linux pidfd_open(2): EINVAL if pid is not valid. PID 0 is not a
// userspace-visible process id for this syscall (do not alias to self).
if pid == 0 {
// Linux pidfd_open(2): EINVAL if pid is not valid (includes pid <= 0).
if (pid as i32) <= 0 {
return Err(AxError::InvalidInput);
}

let fd = if flags.contains(PidFdFlags::THREAD) {
PidFd::new_thread(get_task(pid)?.as_thread())
PidFd::new_thread(get_task(pid)?.as_thread(), pid)
} else {
// Without PIDFD_THREAD the target must be a thread-group leader.
if let Ok(task) = get_task(pid)
&& task.as_thread().proc_data.proc.pid() != pid
{
return Err(AxError::NotFound);
}
PidFd::new_process(&get_process_data(pid)?)
};
if flags.contains(PidFdFlags::NONBLOCK) {
Expand All @@ -49,16 +92,28 @@ pub fn sys_pidfd_getfd(pidfd: i32, target_fd: i32, flags: u32) -> AxResult<isize

let pidfd = PidFd::from_fd(pidfd)?;
let proc_data = pidfd.process_data()?;
check_kill_permission(proc_data.proc.pid())?;
FD_TABLE
.scope(&proc_data.scope.read())
.read()
.get(target_fd as usize)
.ok_or(AxError::BadFileDescriptor)
.and_then(|fd| {
let fd = add_file_like(fd.inner.clone(), true)?;
Ok(fd as isize)
})
let curr_proc_data = current().as_thread().proc_data.clone();
let is_current = Arc::ptr_eq(&proc_data, &curr_proc_data);
if !is_current {
// Linux __pidfd_fget() uses ptrace_may_access(PTRACE_MODE_ATTACH_REALCREDS).
// Until Starry has that, require at least kill-style credentials on the target.
check_kill_permission(proc_data.proc.pid())?;
}
let fd_entry = if is_current {
// Use the live fd table for the current process. `proc_data.scope` is only
// refreshed on clone/dup paths; syscalls like pipe() update ActiveScope only.
FD_TABLE.read().get(target_fd as usize).cloned()
} else {
FD_TABLE
.scope(&proc_data.scope.read())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

旧代码在 fd 查找前调用了 check_kill_permission(proc_data.proc.pid())。新代码移除了该权限检查。

Linux pidfd_getfd 要求 ptrace_may_access(PTRACE_MODE_ATTACH_REALCREDS) 权限,比 kill 权限更严格。虽然旧代码的 kill-permission 检查不是完全正确的语义,但完全移除意味着任意进程可以通过 pidfd_getfd 获取其他进程的 fd。

建议在后续 PR 中至少恢复 check_kill_permission,或实现更精确的 ptrace 权限检查。当前不阻塞合入。

.read()
.get(target_fd as usize)
.cloned()
};
fd_entry.ok_or(AxError::BadFileDescriptor).and_then(|fd| {
let fd = add_file_like(fd.inner.clone(), true)?;
Ok(fd as isize)
})
}

pub fn sys_pidfd_send_signal(
Expand All @@ -67,19 +122,60 @@ pub fn sys_pidfd_send_signal(
sig: *mut SignalInfo,
flags: u32,
) -> AxResult<isize> {
if flags != 0 {
let flags = PidFdSignalFlags::from_bits(flags).ok_or(AxError::InvalidInput)?;
if flags.bits().count_ones() > 1 {
return Err(AxError::InvalidInput);
}

let pidfd = PidFd::from_fd(pidfd)?;
let pid = pidfd.process_data()?.proc.pid();
check_kill_permission(pid)?;
let pidfd_obj = PidFd::from_fd(pidfd)?;
let proc_data = pidfd_obj.process_data()?;
let target_pid = proc_data.proc.pid();

let sig = if sig.is_null() {
make_siginfo(signo, SI_USER as _)?
let scope = if flags.contains(PidFdSignalFlags::THREAD)
|| (flags.is_empty() && pidfd_obj.is_thread())
{
PidFdSignalScope::Thread
} else if flags.contains(PidFdSignalFlags::PROCESS_GROUP) {
PidFdSignalScope::ProcessGroup
} else {
make_queue_signal_info(pid, signo, sig)?
PidFdSignalScope::ThreadGroup
};
send_signal_to_process(pid, sig)?;

let kinfo = if signo == 0 {
None
} else if sig.is_null() {
let signo = parse_signo(signo)?;
Some(make_pidfd_siginfo(signo, scope))
} else {
let signo_parsed = parse_signo(signo)?;
let info = unsafe { sig.vm_read_uninit()?.assume_init() };
if info.signo() != signo_parsed {
return Err(AxError::InvalidInput);
}
if current().as_thread().proc_data.proc.pid() != target_pid
&& (info.code() >= 0 || info.code() == SI_TKILL)
{
return Err(AxError::OperationNotPermitted);
}
Some(info)
};

match scope {
PidFdSignalScope::Thread => {
let tid = pidfd_obj.tid().ok_or(AxError::InvalidInput)?;
check_kill_permission(tid)?;
send_signal_to_thread(Some(target_pid), tid, kinfo)?;
}
PidFdSignalScope::ThreadGroup => {
check_kill_permission(target_pid)?;
send_signal_to_process(target_pid, kinfo)?;
}
PidFdSignalScope::ProcessGroup => {
let pgid = proc_data.proc.group().pgid();
check_kill_permission(pgid)?;
send_signal_to_process_group(pgid, kinfo)?;
}
}

Ok(0)
}
2 changes: 1 addition & 1 deletion os/StarryOS/kernel/src/syscall/task/clone.rs
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ impl CloneArgs {
}
if flags.contains(CloneFlags::PIDFD) && pidfd != 0 {
let pidfd_obj = if flags.contains(CloneFlags::THREAD) {
PidFd::new_thread(&thr)
PidFd::new_thread(&thr, tid)
} else {
PidFd::new_process(&new_proc_data)
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#pragma once

/* 必须在最前面定义,确保 pipe2/gettid 等可用 */
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif

/*
* StarryOS Syscall Test Framework
*
* 极简独立测试框架:每个文件测一个 syscall,独立编译运行。
* 目标:出错时精确定位到 源文件:行号 -> 哪个调用 -> 什么结果
*
* 用法:
* TEST_START("测试名");
* CHECK(call == expected, "描述");
* CHECK_ERR(call, EBADF, "描述");
* TEST_DONE();
*/

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static int __pass = 0;
static int __fail = 0;

/* ---- 核心: 带文件名+行号的检查宏 ---- */

/* 检查条件为真 */
#define CHECK(cond, msg) do { \
if (cond) { \
printf(" PASS | %s:%d | %s\n", __FILE__, __LINE__, msg); \
__pass++; \
} else { \
printf(" FAIL | %s:%d | %s | errno=%d (%s)\n", \
__FILE__, __LINE__, msg, errno, strerror(errno)); \
__fail++; \
} \
} while (0)

/* 检查 syscall 返回特定值 */
#define CHECK_RET(call, expected, msg) do { \
errno = 0; \
long _r = (long)(call); \
long _e = (long)(expected); \
if (_r == _e) { \
printf(" PASS | %s:%d | %s (ret=%ld)\n", \
__FILE__, __LINE__, msg, _r); \
__pass++; \
} else { \
printf(" FAIL | %s:%d | %s | expected=%ld got=%ld | errno=%d (%s)\n", \
__FILE__, __LINE__, msg, _e, _r, errno, strerror(errno)); \
__fail++; \
} \
} while (0)

/* 检查 syscall 失败且 errno 符合预期 */
#define CHECK_ERR(call, exp_errno, msg) do { \
errno = 0; \
long _r = (long)(call); \
if (_r == -1 && errno == (exp_errno)) { \
printf(" PASS | %s:%d | %s (errno=%d as expected)\n", \
__FILE__, __LINE__, msg, errno); \
__pass++; \
} else { \
printf(" FAIL | %s:%d | %s | expected errno=%d got ret=%ld errno=%d (%s)\n", \
__FILE__, __LINE__, msg, (int)(exp_errno), _r, errno, \
strerror(errno)); \
__fail++; \
} \
} while (0)

/* ---- 测试边界 ---- */
#define TEST_START(name) \
printf("================================================\n"); \
printf(" TEST: %s\n", name); \
printf(" FILE: %s\n", __FILE__); \
printf("================================================\n")

#define TEST_DONE() \
printf("------------------------------------------------\n"); \
printf(" DONE: %d pass, %d fail\n", __pass, __fail); \
printf("================================================\n\n"); \
return __fail > 0 ? 1 : 0
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
cmake_minimum_required(VERSION 3.20)
project(test-pidfd-getfd C)
set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_C_EXTENSIONS OFF)
add_executable(test-pidfd-getfd src/main.c)
target_include_directories(test-pidfd-getfd PRIVATE src ${CMAKE_CURRENT_SOURCE_DIR}/../../common)
target_compile_options(test-pidfd-getfd PRIVATE -Wall -Wextra -Werror)
install(TARGETS test-pidfd-getfd RUNTIME DESTINATION usr/bin/starry-test-suit)
Loading
Loading