Skip to main content

shadow_rs/host/
fcntl_lock_table.rs

1use std::{
2    collections::{BTreeMap, BTreeSet},
3    ops::Range,
4};
5
6use linux_api::fcntl::FlockType;
7use rangemap::RangeMap;
8
9use crate::host::process::ProcessId;
10
11/// Error returned by a lock operation.
12#[derive(Debug, Copy, Clone, Eq, PartialEq)]
13pub enum LockError {
14    // lock couldn't be taken, because it's held by another owner.
15    ConflictingLock,
16}
17
18/// Owner of a record lock.
19// Intentionally not `Copy`; see TODO below.
20#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
21pub enum LockOwner {
22    /// For "process-associated" (posix) locks, as created via `fcntl` operation
23    /// `F_SETLK`.
24    // TODO: We might want to make the owner be the *descriptor table* to
25    // more-closely align with the (undocumented? unintentional?) Linux
26    // semantics for processes that shares a descriptor table. See
27    // <https://github.com/shadow/shadow/issues/3783>
28    Process(ProcessId),
29    // TODO: add enumerator for "open file description" locks, as created via
30    // `fcntl` operation `F_OFD_SETLK`. We might want this to involve some sort
31    // of pointer (e.g. Weak) to the file description. To support that, we don't
32    // make this type `Copy`.
33}
34
35/// Internal representation of a record lock.
36#[derive(Clone, Debug, Eq, PartialEq)]
37enum FcntlLock {
38    // Write-lock; one owner.
39    Write(LockOwner),
40    // Read-lock; multiple owners.
41    // Important to use a sorted set here, so that an "arbitrary" conflicting
42    // lock returned via a.g. `F_GETLK` is deterministic.
43    Read(BTreeSet<LockOwner>),
44}
45
46impl FcntlLock {
47    fn new(requester: &LockOwner, flock_type: FlockType) -> Option<FcntlLock> {
48        match flock_type {
49            FlockType::F_UNLCK => None,
50            FlockType::F_RDLCK => Some(FcntlLock::Read(BTreeSet::from_iter([requester.clone()]))),
51            FlockType::F_WRLCK => Some(FcntlLock::Write(requester.clone())),
52        }
53    }
54
55    fn access(&self) -> FlockType {
56        match self {
57            FcntlLock::Write(_) => FlockType::F_WRLCK,
58            FcntlLock::Read(_) => FlockType::F_RDLCK,
59        }
60    }
61
62    fn owners_contains(&self, o: &LockOwner) -> bool {
63        match self {
64            FcntlLock::Write(write_owner) => write_owner == o,
65            FcntlLock::Read(read_owners) => read_owners.contains(o),
66        }
67    }
68
69    /// Return the result of updating this lock with the given request, if possible.
70    /// Otherwise returns an error that describes one of the conflicting locks.
71    ///
72    /// Use `requested_type=FlockType::F_UNLCK` to unlock.
73    ///
74    /// Returns:
75    /// * `Ok(None)` when the result is "no lock"; i.e. when
76    ///   `requested_type` is `F_UNLCK`, and `requester` is the only owner of
77    ///   `self`.
78    /// * `Ok(Some(x))`, where x is the result of a successful update.
79    /// * `Err(x)`, where x is one of the current owners of a conflicting lock.
80    pub fn with_request_applied(
81        &self,
82        requester: &LockOwner,
83        requested_type: FlockType,
84    ) -> Result<Option<Self>, LockOwner> {
85        match self {
86            FcntlLock::Write(current_owner) => {
87                if current_owner == requester {
88                    // requester is already the exclusive owner.
89                    // Give them whatever they want.
90                    Ok(Self::new(requester, requested_type))
91                } else if requested_type == FlockType::F_UNLCK {
92                    // requester is releasing, but isn't an owner of this lock.
93                    // Return this lock unchanged.
94                    Ok(Some(self.clone()))
95                } else {
96                    // Conflict with current owner.
97                    Err(current_owner.clone())
98                }
99            }
100            FcntlLock::Read(current_owners) => {
101                match requested_type {
102                    FlockType::F_RDLCK => {
103                        // Add requester to the set of read-lock owners
104                        let mut s = current_owners.clone();
105                        s.insert(requester.clone());
106                        Ok(Some(FcntlLock::Read(s)))
107                    }
108                    FlockType::F_WRLCK => {
109                        match current_owners.iter().find(|o| o != &requester) {
110                            Some(conflicting_owner) => {
111                                // requesting a write lock, but a different owner
112                                // is holding a read lock.
113                                Err(conflicting_owner.clone())
114                            }
115                            None => {
116                                // requesting a write lock, and requester is the sole holder
117                                // of a read lock. Upgrade it to a write lock.
118                                Ok(FcntlLock::new(requester, FlockType::F_WRLCK))
119                            }
120                        }
121                    }
122                    FlockType::F_UNLCK => {
123                        // Requesting to unlock, and there's a read-lock.
124                        // Remove requester from set of read-lock owners.
125                        let new_owners = BTreeSet::from_iter(
126                            current_owners.iter().filter(|x| x != &requester).cloned(),
127                        );
128                        if new_owners.is_empty() {
129                            // No owners left -> unlocked.
130                            Ok(None)
131                        } else {
132                            Ok(Some(FcntlLock::Read(new_owners)))
133                        }
134                    }
135                }
136            }
137        }
138    }
139}
140
141/// Stable identifier for a file, which we use to look up record locks.
142/// Remains valid as long as the file has links in the file system and/or there
143/// are open file descriptors to the file.
144//
145// We implement this using device+inode, as returned by `fstat`.
146//
147// According to
148// [posix](https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/sys_stat.h.html):
149//
150//   A file identity is uniquely determined by the combination of st_dev and
151//   st_ino. At any given time in a system, distinct files shall have distinct
152//   file identities; hard links to the same file shall have the same file
153//   identity. Over time, these file identities can be reused for different files.
154//   For example, the st_ino value can be reused after the last link to a file is
155//   unlinked and the space occupied by the file has been freed, and the st_dev
156//   value associated with a file system can be reused if that file system is
157//   detached ("unmounted") and another is attached ("mounted").
158//
159// While the posix definition doesn't seem to clearly specify whether an inode
160// number can be reused when there are no more links in the file system, but the
161// file is still open, my understanding is that at least in Linux, files aren't
162// destroyed and their inode numbers made available for reuse until it is no
163// longer open by anyone. e.g.
164// [unlink(2)](https://man7.org/linux/man-pages/man2/unlink.2.html):
165//
166//   If the name was the last link to a file but any processes still
167//   have the file open, the file will remain in existence until the
168//   last file descriptor referring to it is closed.
169#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
170pub struct FileId {
171    device: u64,
172    inode: u64,
173}
174
175impl From<&linux_api::stat::stat> for FileId {
176    fn from(value: &linux_api::stat::stat) -> Self {
177        FileId {
178            device: value.lst_dev,
179            inode: value.lst_ino,
180        }
181    }
182}
183
184impl From<&libc::stat> for FileId {
185    fn from(value: &libc::stat) -> Self {
186        FileId {
187            device: value.st_dev,
188            inode: value.st_ino,
189        }
190    }
191}
192
193#[derive(Debug)]
194struct FcntlLockTableForOneFile {
195    locks: RangeMap<usize, FcntlLock>,
196    // Will need something here to track sleepers
197}
198
199impl FcntlLockTableForOneFile {
200    fn new() -> Self {
201        Self {
202            locks: Default::default(),
203        }
204    }
205
206    pub fn is_empty(&self) -> bool {
207        self.locks.is_empty()
208    }
209
210    /// Apply the requested lock to the given range. Similar semantics as
211    /// `fcntl` operations like `F_SETLK`.
212    ///
213    /// Returns an error if an incompatible lock exists.
214    pub fn set_lock(
215        &mut self,
216        requested_range: Range<usize>,
217        requester: &LockOwner,
218        requested_type: FlockType,
219    ) -> Result<(), LockError> {
220        // Process overlapping locks, collecting what the updated locks will look like.
221        let mut updated_results = Vec::<(Range<usize>, FcntlLock)>::new();
222        for (lock_range, lock) in self.locks.overlapping(&requested_range) {
223            match lock.with_request_applied(requester, requested_type) {
224                Ok(Some(updated_lock)) => {
225                    // Record the updated lock.
226                    let start = std::cmp::max(lock_range.start, requested_range.start);
227                    let end = std::cmp::min(lock_range.end, requested_range.end);
228                    updated_results.push((start..end, updated_lock));
229                }
230                Ok(None) => {
231                    // Result is no-lock. No need to record anything; we'll clear this below.
232                }
233                Err(_) => {
234                    // Conflicting lock; can't update.
235                    return Err(LockError::ConflictingLock);
236                }
237            }
238        }
239
240        // Set the whole range based on the requested lock.
241        match FcntlLock::new(requester, requested_type) {
242            Some(l) => {
243                self.locks.insert(requested_range.clone(), l);
244            }
245            None => {
246                self.locks.remove(requested_range.clone());
247            }
248        }
249
250        // Insert the updated locks, potentially overwriting what we just wrote
251        // into the whole range.
252        for (updated_range, updated_lock) in updated_results {
253            self.locks.insert(updated_range, updated_lock);
254        }
255
256        Ok(())
257    }
258
259    /// Return one of the locks that would conflict with the requested lock, if
260    /// any. The return range is *uncoalesced*, meaning that if a conflicting read lock
261    /// is returned, the bounds have not been expanded to include adjancent read locks
262    /// that also have the returned owner.
263    ///
264    /// Intended for `fcntl` operations like `F_GETLK`, but result is the raw
265    /// internal lock, if any.
266    fn get_uncoalesced_conflicting_lock(
267        &self,
268        requested_range: Range<usize>,
269        requester: &LockOwner,
270        requested_type: FlockType,
271    ) -> Option<(LockOwner, Range<usize>, FlockType)> {
272        for (lock_range, lock) in self.locks.overlapping(&requested_range) {
273            match lock.with_request_applied(requester, requested_type) {
274                Ok(_) => (), // No conflict,
275                Err(o) => return Some((o, lock_range.clone(), lock.access())),
276            }
277        }
278        None
279    }
280
281    /// Return one of the locks that would conflict with the requested lock, if
282    /// any. Intended for `fcntl` operations like `F_GETLK`.
283    ///
284    /// The returned range is coalesced for the returned owner: it includes all adjacent
285    /// range over which the owner has the returned access, even if parts of the range
286    /// have different ownership sets. (Reproducing the behavior of `F_GETLK` on Linux).
287    pub fn get_coalesced_conflicting_lock(
288        &self,
289        requested_range: Range<usize>,
290        requester: &LockOwner,
291        requested_type: FlockType,
292    ) -> Option<(LockOwner, Range<usize>, FlockType)> {
293        let (conflicting_owner, mut conflicting_range, conflicting_access) =
294            self.get_uncoalesced_conflicting_lock(requested_range, requester, requested_type)?;
295
296        let can_coalesce = |other_lock: &FcntlLock| -> bool {
297            other_lock.access() == conflicting_access
298                && other_lock.owners_contains(&conflicting_owner)
299        };
300
301        // Coalesce backwards
302        while let Some(x) = conflicting_range.start.checked_sub(1) {
303            let Some((prev_range, prev_lock)) = self.locks.get_key_value(&x) else {
304                break;
305            };
306            if !(can_coalesce(prev_lock)) {
307                break;
308            }
309            conflicting_range.start = prev_range.start
310        }
311
312        // Coalesce forwards
313        while let Some((next_range, next_lock)) = self.locks.get_key_value(&conflicting_range.end) {
314            if !(can_coalesce(next_lock)) {
315                break;
316            }
317            debug_assert_eq!(conflicting_range.end, next_range.start);
318            conflicting_range.end = next_range.end
319        }
320
321        Some((conflicting_owner, conflicting_range, conflicting_access))
322    }
323}
324
325/// All record locks for a Host.
326#[derive(Debug)]
327pub struct FcntlLockTable {
328    /// locks by FileId.
329    locks: BTreeMap<FileId, FcntlLockTableForOneFile>,
330}
331
332impl FcntlLockTable {
333    pub fn new() -> Self {
334        Self {
335            locks: BTreeMap::new(),
336        }
337    }
338
339    pub fn set_lock(
340        &mut self,
341        file_id: FileId,
342        requested_range: Range<usize>,
343        requested_owner: &LockOwner,
344        requested_access: FlockType,
345    ) -> Result<(), LockError> {
346        let file_locks = self
347            .locks
348            .entry(file_id)
349            .or_insert_with(FcntlLockTableForOneFile::new);
350        let res = file_locks.set_lock(requested_range.clone(), requested_owner, requested_access);
351        if res.is_err() {
352            log::debug!(
353                "failed to lock {file_id:?}.{requested_range:?}.{requested_owner:?}.{requested_access:?}"
354            );
355        }
356        if file_locks.is_empty() {
357            self.locks.remove(&file_id);
358        }
359        res
360    }
361
362    /// Return one of the locks that would conflict with the requested lock, if
363    /// any. Intended for `fcntl` operations like `F_GETLK`.
364    pub fn get_coalesced_conflicting_lock(
365        &self,
366        file_id: FileId,
367        requested_range: Range<usize>,
368        requested_owner: &LockOwner,
369        requested_access: FlockType,
370    ) -> Option<(LockOwner, Range<usize>, FlockType)> {
371        let file_locks = self.locks.get(&file_id)?;
372        let start = requested_range.start;
373        let end = requested_range.end;
374        let res = file_locks.get_coalesced_conflicting_lock(
375            requested_range,
376            requested_owner,
377            requested_access,
378        );
379        log::trace!(
380            "get_coalesced_conflicting_lock({file_locks:?}, {file_id:?}, {start}..{end}, {requested_owner:?}, {requested_access:?} -> {res:?})"
381        );
382        res
383    }
384
385    /// Drop all of the given owner's locks on the given file.
386    pub fn remove_owner(&mut self, file_id: FileId, requested_owner: &LockOwner) {
387        // F_UNLCK operation removes the specified owner where applicable for
388        // any part of the range, and has no effect for parts of the range where
389        // the owner doesn't hold a lock.
390        self.set_lock(
391            file_id,
392            usize::MIN..usize::MAX,
393            requested_owner,
394            FlockType::F_UNLCK,
395        )
396        .expect("F_UNLCK should always succeed");
397    }
398}
399
400impl Default for FcntlLockTable {
401    fn default() -> Self {
402        Self::new()
403    }
404}