use alloc::{
collections::BTreeMap,
sync::{Arc, Weak},
vec::Vec,
};
use core::ffi::c_int;
use linux_raw_sys::general::{
F_GETLK, F_OFD_GETLK, F_OFD_SETLK, F_OFD_SETLKW, F_RDLCK, F_SETLK, F_SETLKW, F_UNLCK, F_WRLCK,
LOCK_EX, LOCK_NB, LOCK_SH, LOCK_UN, O_ACCMODE, O_RDONLY, O_RDWR, O_WRONLY, SEEK_CUR, SEEK_END,
SEEK_SET, flock64,
};
use crate::{
Errno, StarryError, StarryResult,
file::{File, FileLike, get_file_like},
mm::UserPtr,
sync::RwLock,
task::{PidIdentityId, PidNamespaceId, PidSnapshot, futex::WaitQueue},
};
type InodeKey = (u64, u64); type OfdAddr = usize;
const OFD_PID_REPORTED: i32 = -1;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum LockKind {
Read,
Write,
}
#[derive(Clone, Debug)]
enum FOwner {
Posix {
owner: PidSnapshot,
},
Ofd {
addr: OfdAddr,
weak: Weak<dyn FileLike>,
},
}
impl FOwner {
fn same_as(&self, other: &FOwner) -> bool {
match (self, other) {
(FOwner::Posix { owner: a }, FOwner::Posix { owner: b }) => {
a.identity_id() == b.identity_id()
}
(FOwner::Ofd { addr: a, .. }, FOwner::Ofd { addr: b, .. }) => a == b,
_ => false,
}
}
fn report_pid(&self, observer: PidNamespaceId) -> i32 {
match self {
FOwner::Posix { owner } => owner
.visible_number(observer)
.map_or(0, |number| number.get() as i32),
FOwner::Ofd { .. } => OFD_PID_REPORTED,
}
}
fn is_dead(&self) -> bool {
match self {
FOwner::Posix { .. } => false,
FOwner::Ofd { weak, .. } => weak.strong_count() == 0,
}
}
}
#[derive(Debug)]
struct FLockEntry {
start: i64,
end: i64,
kind: LockKind,
owner: FOwner,
}
static FCNTL_LOCKS: RwLock<BTreeMap<InodeKey, Vec<FLockEntry>>> = RwLock::new(BTreeMap::new());
static LOCK_WAITERS: RwLock<BTreeMap<InodeKey, Arc<WaitQueue>>> = RwLock::new(BTreeMap::new());
static POSIX_LOCK_WAITS: RwLock<BTreeMap<PidIdentityId, Vec<WaitingLock>>> =
RwLock::new(BTreeMap::new());
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct WaitingLock {
key: InodeKey,
start: i64,
end: i64,
kind: LockKind,
}
type PosixLockWaitTable = BTreeMap<PidIdentityId, Vec<WaitingLock>>;
struct PosixLockWaitGuard {
owner: PidIdentityId,
request: WaitingLock,
}
impl PosixLockWaitGuard {
fn try_new(
waiter: PidIdentityId,
request: WaitingLock,
owner: &FOwner,
) -> Result<Option<Self>, Errno> {
let mut table = FCNTL_LOCKS.write();
let Some(entries) = table.get_mut(&request.key) else {
return Ok(None);
};
entries.retain(|e| !e.owner.is_dead());
let still_blocked =
find_conflict(entries, owner, request.start, request.end, request.kind).is_some();
if entries.is_empty() {
table.remove(&request.key);
}
if !still_blocked {
return Ok(None);
}
let mut waits = POSIX_LOCK_WAITS.write();
if posix_lock_deadlock_would_occur(&table, &waits, waiter, request) {
return Err(Errno::EDEADLK);
}
waits.entry(waiter).or_default().push(request);
Ok(Some(Self {
owner: waiter,
request,
}))
}
}
impl Drop for PosixLockWaitGuard {
fn drop(&mut self) {
let mut waits = POSIX_LOCK_WAITS.write();
let Some(requests) = waits.get_mut(&self.owner) else {
return;
};
if let Some(index) = requests.iter().position(|request| *request == self.request) {
requests.swap_remove(index);
}
if requests.is_empty() {
waits.remove(&self.owner);
}
}
}
#[derive(Debug)]
struct FlockEntry {
addr: OfdAddr,
weak: Weak<dyn FileLike>,
kind: LockKind,
owner: PidIdentityId,
}
static FLOCK_LOCKS: RwLock<BTreeMap<InodeKey, Vec<FlockEntry>>> = RwLock::new(BTreeMap::new());
static FLOCK_WAITERS: RwLock<BTreeMap<InodeKey, Arc<WaitQueue>>> = RwLock::new(BTreeMap::new());
fn ofd_addr(arc: &Arc<dyn FileLike>) -> OfdAddr {
Arc::as_ptr(arc) as *const () as usize
}
fn current_process_identity_id(current: &crate::task::UserTaskRef) -> PidIdentityId {
current.as_thread().proc_data.identity().id()
}
fn current_process_pid_snapshot(current: &crate::task::UserTaskRef) -> PidSnapshot {
current.as_thread().proc_data.identity().snapshot()
}
fn lockable(fd: c_int) -> StarryResult<(InodeKey, Arc<dyn FileLike>)> {
let f = get_file_like(fd)?;
let key = f.inode_key().ok_or(StarryError::BadFileDescriptor)?;
Ok((key, f))
}
fn resolve_l_start(file: &Arc<dyn FileLike>, l_whence: i16, l_start: i64) -> StarryResult<i64> {
let whence = l_whence as u32;
if whence == SEEK_SET {
return Ok(l_start);
}
if whence != SEEK_CUR && whence != SEEK_END {
return Err(StarryError::InvalidInput);
}
let regular = file
.downcast_ref::<File>()
.ok_or(StarryError::InvalidInput)?;
let base = if whence == SEEK_CUR {
regular
.inner()
.position()
.ok_or(StarryError::InvalidInput)?
} else {
regular
.inner()
.location()
.len()
.map_err(|_| StarryError::InvalidInput)?
};
let base_i64 = i64::try_from(base).map_err(|_| StarryError::InvalidInput)?;
base_i64
.checked_add(l_start)
.ok_or(StarryError::InvalidInput)
}
fn flock_range(l_start: i64, l_len: i64) -> StarryResult<(i64, i64)> {
if l_len == 0 {
if l_start < 0 {
return Err(StarryError::InvalidInput);
}
return Ok((l_start, i64::MAX));
}
let (start, end) = if l_len > 0 {
let end = l_start
.checked_add(l_len)
.ok_or(StarryError::InvalidInput)?;
(l_start, end)
} else {
let start = l_start
.checked_add(l_len)
.ok_or(StarryError::InvalidInput)?;
(start, l_start)
};
if start < 0 {
return Err(StarryError::InvalidInput);
}
Ok((start, end))
}
fn ranges_overlap(a_start: i64, a_end: i64, b_start: i64, b_end: i64) -> bool {
a_start < b_end && b_start < a_end
}
fn kinds_conflict(a: LockKind, b: LockKind) -> bool {
!(a == LockKind::Read && b == LockKind::Read)
}
fn fd_supports_kind(file: &Arc<dyn FileLike>, kind: LockKind) -> bool {
let acc = file.open_flags() & O_ACCMODE;
match kind {
LockKind::Read => acc == O_RDONLY || acc == O_RDWR,
LockKind::Write => acc == O_WRONLY || acc == O_RDWR,
}
}
fn clear_owner_overlap(
entries: &mut Vec<FLockEntry>,
owner: &FOwner,
start: i64,
end: i64,
) -> bool {
let mut changed = false;
let mut i = 0;
while i < entries.len() {
let e = &entries[i];
if !e.owner.same_as(owner) || !ranges_overlap(e.start, e.end, start, end) {
i += 1;
continue;
}
changed = true;
let (es, ee, ek) = (e.start, e.end, e.kind);
let snap_owner = e.owner.clone();
entries.swap_remove(i);
if es < start {
entries.push(FLockEntry {
start: es,
end: start,
kind: ek,
owner: snap_owner.clone(),
});
}
if ee > end {
entries.push(FLockEntry {
start: end,
end: ee,
kind: ek,
owner: snap_owner,
});
}
}
changed
}
fn find_conflict<'a>(
entries: &'a mut Vec<FLockEntry>,
requester: &FOwner,
start: i64,
end: i64,
kind: LockKind,
) -> Option<&'a FLockEntry> {
entries.retain(|e| !e.owner.is_dead());
entries.iter().find(|e| {
!e.owner.same_as(requester)
&& ranges_overlap(e.start, e.end, start, end)
&& kinds_conflict(e.kind, kind)
})
}
fn push_posix_conflict_pids(
entries: &[FLockEntry],
requester: PidIdentityId,
start: i64,
end: i64,
kind: LockKind,
out: &mut Vec<PidIdentityId>,
) {
for entry in entries {
if !ranges_overlap(entry.start, entry.end, start, end) || !kinds_conflict(entry.kind, kind)
{
continue;
}
let FOwner::Posix { owner } = &entry.owner else {
continue;
};
let blocker = owner.identity_id();
if blocker != requester && !out.contains(&blocker) {
out.push(blocker);
}
}
}
fn posix_lock_deadlock_would_occur(
table: &BTreeMap<InodeKey, Vec<FLockEntry>>,
waits: &PosixLockWaitTable,
requester: PidIdentityId,
request: WaitingLock,
) -> bool {
let mut stack = Vec::new();
let mut seen = Vec::new();
if let Some(entries) = table.get(&request.key) {
push_posix_conflict_pids(
entries,
requester,
request.start,
request.end,
request.kind,
&mut stack,
);
}
while let Some(blocker) = stack.pop() {
if blocker == requester {
return true;
}
if seen.contains(&blocker) {
continue;
}
seen.push(blocker);
let Some(blocker_waits) = waits.get(&blocker) else {
continue;
};
for blocked_request in blocker_waits {
if let Some(entries) = table.get(&blocked_request.key) {
push_posix_conflict_pids(
entries,
blocker,
blocked_request.start,
blocked_request.end,
blocked_request.kind,
&mut stack,
);
}
}
}
false
}
fn lock_waiters(key: InodeKey) -> Arc<WaitQueue> {
if let Some(wq) = LOCK_WAITERS.read().get(&key) {
return wq.clone();
}
LOCK_WAITERS
.write()
.entry(key)
.or_insert_with(|| Arc::new(WaitQueue::new()))
.clone()
}
pub fn wake_lock_waiters(key: InodeKey) {
let wq = LOCK_WAITERS.read().get(&key).cloned();
if let Some(wq) = wq {
wq.wake(usize::MAX, !0);
}
}
fn flock_waiters(key: InodeKey) -> Arc<WaitQueue> {
if let Some(wq) = FLOCK_WAITERS.read().get(&key) {
return wq.clone();
}
FLOCK_WAITERS
.write()
.entry(key)
.or_insert_with(|| Arc::new(WaitQueue::new()))
.clone()
}
pub fn wake_flock_waiters(key: InodeKey) {
let wq = FLOCK_WAITERS.read().get(&key).cloned();
if let Some(wq) = wq {
wq.wake(usize::MAX, !0);
}
}
fn make_owner(current: &crate::task::UserTaskRef, ofd: bool, file: &Arc<dyn FileLike>) -> FOwner {
if ofd {
FOwner::Ofd {
addr: ofd_addr(file),
weak: Arc::downgrade(file),
}
} else {
FOwner::Posix {
owner: current_process_pid_snapshot(current),
}
}
}
enum SetlkAttempt {
Done { woke_others: bool },
Conflict,
}
fn try_setlk_once(
key: InodeKey,
owner: FOwner,
start: i64,
end: i64,
kind: Option<LockKind>,
) -> SetlkAttempt {
let mut table = FCNTL_LOCKS.write();
let entries = table.entry(key).or_default();
entries.retain(|e| !e.owner.is_dead());
let attempt = match kind {
None => {
let woke_others = clear_owner_overlap(entries, &owner, start, end);
SetlkAttempt::Done { woke_others }
}
Some(k) => {
if find_conflict(entries, &owner, start, end, k).is_some() {
SetlkAttempt::Conflict
} else {
let woke_others = clear_owner_overlap(entries, &owner, start, end);
entries.push(FLockEntry {
start,
end,
kind: k,
owner,
});
SetlkAttempt::Done { woke_others }
}
}
};
if entries.is_empty() {
table.remove(&key);
}
attempt
}
pub fn fcntl_setlk(
current: &crate::task::UserTaskRef,
fd: c_int,
arg: usize,
ofd: bool,
wait: bool,
) -> crate::StarryResult<isize> {
let fl = unsafe { UserPtr::<flock64>::from(arg).read_abi(current)? };
if ofd && fl.l_pid != 0 {
return Err(StarryError::InvalidInput);
}
let (key, file) = lockable(fd)?;
let abs_start = resolve_l_start(&file, fl.l_whence, fl.l_start)?;
let (start, end) = flock_range(abs_start, fl.l_len)?;
let kind = match fl.l_type as u32 {
F_UNLCK => None,
F_RDLCK => Some(LockKind::Read),
F_WRLCK => Some(LockKind::Write),
_ => return Err(StarryError::InvalidInput),
};
if let Some(k) = kind
&& !fd_supports_kind(&file, k)
{
return Err(StarryError::BadFileDescriptor);
}
loop {
let owner = make_owner(current, ofd, &file);
match try_setlk_once(key, owner, start, end, kind) {
SetlkAttempt::Done { woke_others } => {
if woke_others {
wake_lock_waiters(key);
}
return Ok(0);
}
SetlkAttempt::Conflict => {
if !wait {
return Err(StarryError::WouldBlock);
}
let want = kind.unwrap();
let waiting = WaitingLock {
key,
start,
end,
kind: want,
};
let waiter = (!ofd).then(|| current_process_identity_id(current));
let mut wait_guard = None;
let mut deadlock = false;
let wq = lock_waiters(key);
wq.wait_if(current, !0u32, None, || {
let owner = make_owner(current, ofd, &file);
if let Some(waiter) = waiter {
match PosixLockWaitGuard::try_new(waiter, waiting, &owner) {
Ok(Some(guard)) => wait_guard = Some(guard),
Ok(None) => return false,
Err(Errno::EDEADLK) => {
deadlock = true;
return false;
}
Err(_) => unreachable!("try_new only reports EDEADLK"),
}
} else {
let mut table = FCNTL_LOCKS.write();
let Some(entries) = table.get_mut(&key) else {
return false;
};
entries.retain(|e| !e.owner.is_dead());
let still_blocked =
find_conflict(entries, &owner, start, end, want).is_some();
if entries.is_empty() {
table.remove(&key);
}
if !still_blocked {
return false;
}
}
true
})?;
drop(wait_guard);
if deadlock {
return Err(StarryError::from(Errno::EDEADLK));
}
}
}
}
}
pub fn fcntl_getlk(
current: &crate::task::UserTaskRef,
fd: c_int,
arg: usize,
ofd: bool,
) -> crate::StarryResult<isize> {
let user_fl = UserPtr::<flock64>::from(arg);
let mut fl = unsafe { user_fl.read_abi(current)? };
if ofd && fl.l_pid != 0 {
return Err(StarryError::InvalidInput);
}
let req_kind = match fl.l_type as u32 {
F_RDLCK => LockKind::Read,
F_WRLCK => LockKind::Write,
_ => return Err(StarryError::InvalidInput),
};
let (key, file) = lockable(fd)?;
let abs_start = resolve_l_start(&file, fl.l_whence, fl.l_start)?;
let (start, end) = flock_range(abs_start, fl.l_len)?;
let requester = if ofd {
FOwner::Ofd {
addr: ofd_addr(&file),
weak: Arc::downgrade(&file),
}
} else {
FOwner::Posix {
owner: current_process_pid_snapshot(current),
}
};
let observer = current.as_thread().active_pid_namespace().id();
let mut table = FCNTL_LOCKS.write();
let (report, empty_after) = {
let entries = table.entry(key).or_default();
let report = find_conflict(entries, &requester, start, end, req_kind).map(|e| {
(
e.kind,
e.owner.report_pid(observer),
e.start,
if e.end == i64::MAX {
0
} else {
e.end - e.start
},
)
});
(report, entries.is_empty())
};
if empty_after {
table.remove(&key);
}
drop(table);
if let Some((kind, owner, l_start, l_len)) = report {
fl.l_type = (if kind == LockKind::Read {
F_RDLCK
} else {
F_WRLCK
}) as i16;
fl.l_whence = SEEK_SET as i16;
fl.l_start = l_start;
fl.l_len = l_len;
fl.l_pid = owner;
} else {
fl.l_type = F_UNLCK as i16;
}
write_flock64_outputs(current, user_fl, &fl)?;
Ok(0)
}
fn write_flock64_outputs(
current: &crate::task::UserTaskRef,
user_fl: UserPtr<flock64>,
fl: &flock64,
) -> crate::StarryResult<()> {
let base = user_fl.address().as_usize();
UserPtr::<i16>::from(base + core::mem::offset_of!(flock64, l_type))
.write(current, fl.l_type)?;
UserPtr::<i16>::from(base + core::mem::offset_of!(flock64, l_whence))
.write(current, fl.l_whence)?;
UserPtr::<i64>::from(base + core::mem::offset_of!(flock64, l_start))
.write(current, fl.l_start)?;
UserPtr::<i64>::from(base + core::mem::offset_of!(flock64, l_len)).write(current, fl.l_len)?;
UserPtr::<i32>::from(base + core::mem::offset_of!(flock64, l_pid)).write(current, fl.l_pid)
}
pub fn dispatch_fcntl(
current: &crate::task::UserTaskRef,
fd: c_int,
cmd: c_int,
arg: usize,
) -> Option<crate::StarryResult<isize>> {
let cmd = cmd as u32;
Some(match cmd {
F_SETLK => fcntl_setlk(current, fd, arg, false, false),
F_SETLKW => fcntl_setlk(current, fd, arg, false, true),
F_OFD_SETLK => fcntl_setlk(current, fd, arg, true, false),
F_OFD_SETLKW => fcntl_setlk(current, fd, arg, true, true),
F_GETLK => fcntl_getlk(current, fd, arg, false),
F_OFD_GETLK => fcntl_getlk(current, fd, arg, true),
_ => return None,
})
}
pub fn release_pid_locks(owner: PidIdentityId) {
let mut affected: Vec<InodeKey> = Vec::new();
{
let mut table = FCNTL_LOCKS.write();
table.retain(|inode, entries| {
let before = entries.len();
entries.retain(|e| match &e.owner {
FOwner::Posix { owner: candidate } => candidate.identity_id() != owner,
FOwner::Ofd { .. } => true,
});
if entries.len() != before {
affected.push(*inode);
}
!entries.is_empty()
});
}
for key in affected {
wake_lock_waiters(key);
}
}
pub fn release_inode_posix_locks(owner: PidIdentityId, key: (u64, u64)) {
let woke_someone = {
let mut table = FCNTL_LOCKS.write();
let Some(entries) = table.get_mut(&key) else {
return;
};
let before = entries.len();
entries.retain(|e| match &e.owner {
FOwner::Posix { owner: candidate } => candidate.identity_id() != owner,
FOwner::Ofd { .. } => true,
});
let changed = entries.len() != before;
if entries.is_empty() {
table.remove(&key);
}
changed
};
if woke_someone {
wake_lock_waiters(key);
}
}
enum FlockAttempt {
Done,
Conflict,
}
fn try_flock_once(
current: &crate::task::UserTaskRef,
key: InodeKey,
addr: OfdAddr,
file: &Arc<dyn FileLike>,
kind: Option<LockKind>,
) -> (FlockAttempt, bool) {
let mut table = FLOCK_LOCKS.write();
let entries = table.entry(key).or_default();
let before = entries.len();
entries.retain(|e| e.weak.strong_count() != 0);
let owner = current_process_identity_id(current);
entries.retain(|entry| !(entry.owner == owner && entry.weak.strong_count() < 1));
let outcome = match kind {
None => {
entries.retain(|e| e.addr != addr);
FlockAttempt::Done
}
Some(want) => {
entries.retain(|e| e.addr != addr);
let blocked = entries.iter().any(|e| kinds_conflict(e.kind, want));
if blocked {
FlockAttempt::Conflict
} else {
entries.push(FlockEntry {
addr,
weak: Arc::downgrade(file),
kind: want,
owner: current_process_identity_id(current),
});
FlockAttempt::Done
}
}
};
let mutated = entries.len() != before;
if entries.is_empty() {
table.remove(&key);
}
(outcome, mutated)
}
pub fn release_flock_lock(key: InodeKey, file: &Arc<dyn FileLike>) {
let addr = ofd_addr(file);
let mutated = {
let mut table = FLOCK_LOCKS.write();
let Some(entries) = table.get_mut(&key) else {
return;
};
let before = entries.len();
entries.retain(|e| e.addr != addr);
let changed = entries.len() != before;
if entries.is_empty() {
table.remove(&key);
}
changed
};
if mutated {
wake_flock_waiters(key);
}
}
pub fn release_pid_flock_locks(owner: PidIdentityId) {
let mut affected: Vec<InodeKey> = Vec::new();
{
let mut table = FLOCK_LOCKS.write();
table.retain(|inode, entries| {
let before = entries.len();
entries.retain(|entry| entry.owner != owner);
if entries.len() != before {
affected.push(*inode);
}
!entries.is_empty()
});
}
for key in affected {
wake_flock_waiters(key);
}
}
pub fn flock_op(
current: &crate::task::UserTaskRef,
fd: c_int,
operation: c_int,
) -> crate::StarryResult<isize> {
let op = operation as u32;
let nonblock = op & LOCK_NB != 0;
let kind = match op & !LOCK_NB {
LOCK_SH => Some(LockKind::Read),
LOCK_EX => Some(LockKind::Write),
LOCK_UN => None,
_ => return Err(StarryError::InvalidInput),
};
let (key, file) = lockable(fd)?;
let addr = ofd_addr(&file);
loop {
let (outcome, mutated) = try_flock_once(current, key, addr, &file, kind);
if mutated {
wake_flock_waiters(key);
}
match outcome {
FlockAttempt::Done => return Ok(0),
FlockAttempt::Conflict => {
if nonblock {
return Err(StarryError::WouldBlock);
}
let want = kind.unwrap();
let wq = flock_waiters(key);
wq.wait_if(current, !0u32, None, || {
let table = FLOCK_LOCKS.read();
let Some(entries) = table.get(&key) else {
return false;
};
entries.iter().any(|e| {
e.weak.strong_count() != 0 && e.addr != addr && kinds_conflict(e.kind, want)
})
})?;
}
}
}
}