Skip to main content

shadow_rs/host/syscall/handler/
fcntl.rs

1use std::ops::Range;
2
3use linux_api::errno::Errno;
4use linux_api::fcntl::{DescriptorFlags, FcntlCommand, FlockType, FlockWhence, OFlag, flock};
5use linux_api::unistd::LSeekWhence;
6use log::debug;
7use shadow_shim_helper_rs::syscall_types::ForeignPtr;
8
9use crate::cshadow;
10use crate::host::descriptor::{CompatFile, File, FileMode, FileStatus};
11use crate::host::fcntl_lock_table::{self, FileId, LockError, LockOwner};
12use crate::host::process::Process;
13use crate::host::syscall::handler::{SyscallContext, SyscallHandler};
14use crate::host::syscall::type_formatting::SyscallNonDeterministicArg;
15use crate::host::syscall::types::SyscallError;
16
17#[derive(Debug, Clone)]
18struct CommonFlockParams {
19    file_id: fcntl_lock_table::FileId,
20    // TODO: make this `std::range::Range` instead of `std::ops::Range` go get `Copy`,
21    // once we've updated to Rust 1.96.
22    range: Range<usize>,
23    owner: LockOwner,
24    access: FlockType,
25}
26
27/// Common code for interpreting `flock`
28fn flock_params(
29    file: &CompatFile,
30    process: &Process,
31    flock: &linux_api::fcntl::flock,
32) -> Result<CommonFlockParams, SyscallError> {
33    let requested_access = FlockType::try_from(flock.l_type).map_err(|_| Errno::EINVAL)?;
34    let access_compat = match requested_access {
35        FlockType::F_RDLCK => file.mode().contains(FileMode::READ),
36        FlockType::F_WRLCK => file.mode().contains(FileMode::WRITE),
37        FlockType::F_UNLCK => true,
38    };
39    if !access_compat {
40        log::debug!(
41            "requested access {requested_access:?} incompatible with file mode {:?}",
42            file.mode()
43        );
44        return Err(Errno::EBADF.into());
45    }
46    let file_id = FileId::from(&file.stat()?);
47    let whence = FlockWhence::try_from(flock.l_whence).map_err(|_| Errno::EINVAL)?;
48    let origin = match whence {
49        FlockWhence::SEEK_SET => 0,
50        FlockWhence::SEEK_CUR => file.lseek(0, LSeekWhence::SEEK_CUR)?,
51        FlockWhence::SEEK_END => file.stat()?.lst_size,
52    };
53    let Some(signed_start) = origin.checked_add(flock.l_start) else {
54        return Err(Errno::EINVAL.into());
55    };
56    let (start, open_end) = if flock.l_len == 0 {
57        // fcntl(2): Specifying 0 for l_len has the special meaning: lock all
58        // bytes starting at the location specified by l_whence and l_start
59        // through to the end of file, no matter how large the file grows.
60        let start = usize::try_from(signed_start).map_err(|_| Errno::EINVAL)?;
61        (start, FLOCK_LENGTH_0_OPEN_END)
62    } else if flock.l_len < 0 {
63        // fcntl(2): if l_len is negative, the interval described by lock
64        // covers bytes l_start+l_len  up  to  and  including  l_start-1.
65        let start = signed_start
66            .checked_add(flock.l_len)
67            .ok_or(Errno::EINVAL)
68            .and_then(|x| usize::try_from(x).map_err(|_| Errno::EINVAL))?;
69        let open_end = usize::try_from(signed_start).map_err(|_| Errno::EINVAL)?;
70        (start, open_end)
71    } else {
72        let start = usize::try_from(signed_start).map_err(|_| Errno::EINVAL)?;
73        // We know length is positive.
74        let len =
75            usize::try_from(flock.l_len).expect("Negative length should have been caught earlier");
76        let Some(open_end) = start.checked_add(len) else {
77            return Err(Errno::EOVERFLOW.into());
78        };
79        if open_end > FLOCK_MAX_VISIBLE_RANGE_OPEN_END {
80            return Err(Errno::EOVERFLOW.into());
81        }
82        (start, open_end)
83    };
84    if open_end <= start {
85        return Err(Errno::EINVAL.into());
86    }
87    let requested_owner = LockOwner::Process(process.id());
88    Ok(CommonFlockParams {
89        file_id,
90        range: start..open_end,
91        owner: requested_owner,
92        access: requested_access,
93    })
94}
95
96// Linux returns an overflow error when the calculated (open) end is greater
97// than i64::MAX + 1, so that's the maximum user-visible range-end we permit.
98//
99// Internally we use values greater than this, having special meaning (see
100// FLOCK_LENGTH_0_OPEN_END, below).
101const FLOCK_MAX_VISIBLE_RANGE_OPEN_END: usize = (i64::MAX as usize) + 1;
102/// fcntl(2): Specifying 0 for l_len has the special meaning: lock all bytes
103/// starting at the location specified by l_whence and l_start through to the
104/// end of file, no matter how large the file grows.
105///
106/// We need to "round-trip" this behavior - returning a length of 0 in `F_GETLK`
107/// for a lock that was set with length 0.
108///
109/// We do this by internally mapping length 0 locks to end at usize::MAX,
110/// which is beyond the end that can be specified otherwise
111/// (FLOCK_MAX_VISIBLE_RANGE_OPEN_END).
112const FLOCK_LENGTH_0_OPEN_END: usize = usize::MAX;
113const _: () = const {
114    assert!(FLOCK_LENGTH_0_OPEN_END > FLOCK_MAX_VISIBLE_RANGE_OPEN_END);
115};
116
117impl SyscallHandler {
118    log_syscall!(
119        fcntl,
120        /* rv */ std::ffi::c_long,
121        /* fd */ std::ffi::c_uint,
122        /* cmd */ FcntlCommand,
123        /* arg */ SyscallNonDeterministicArg<std::ffi::c_ulong>,
124    );
125    pub fn fcntl(
126        ctx: &mut SyscallContext,
127        fd: std::ffi::c_uint,
128        cmd: std::ffi::c_uint,
129        arg: std::ffi::c_ulong,
130    ) -> Result<std::ffi::c_long, SyscallError> {
131        // NOTE: this function should *not* run the C syscall handler if the cmd modifies the
132        // descriptor
133
134        // helper function to run the C syscall handler
135        let legacy_syscall_fn =
136            |ctx: &mut SyscallContext| Self::legacy_syscall(cshadow::syscallhandler_fcntl, ctx);
137
138        // get the descriptor, or return early if it doesn't exist
139        let mut desc_table = ctx.objs.thread.descriptor_table_borrow_mut(ctx.objs.host);
140        let desc = Self::get_descriptor_mut(&mut desc_table, fd)?;
141
142        let Ok(cmd) = FcntlCommand::try_from(cmd) else {
143            debug!("Bad fcntl command: {cmd}");
144            return Err(Errno::EINVAL.into());
145        };
146
147        Ok(match cmd {
148            FcntlCommand::F_SETLK | FcntlCommand::F_SETLKW => {
149                let flock_ptr = ForeignPtr::<()>::from(arg).cast::<flock>();
150                let flock = ctx.objs.process.memory_borrow().read(flock_ptr)?;
151                let file = desc.file();
152                let params = flock_params(file, ctx.objs.process, &flock)?;
153                let mut lock_table = ctx.objs.host.fcntl_lock_table_borrow_mut();
154                let res = lock_table.set_lock(
155                    params.file_id,
156                    params.range.clone(),
157                    &params.owner,
158                    params.access,
159                );
160                log::trace!("setlk[w] {params:?} -> {res:?}");
161                if let Err(e) = res {
162                    match e {
163                        LockError::ConflictingLock => {
164                            let errno = Errno::EACCES;
165                            if cmd == FcntlCommand::F_SETLKW {
166                                // TODO: block instead of returning an error.
167                                // <https://github.com/shadow/shadow/issues/3784>
168                                log::warn!(
169                                    "SETLKW({params:?}) should block, but blocking is unimplemented. Returning {errno:?}"
170                                );
171                            }
172                            return Err(errno.into());
173                        }
174                    }
175                }
176                0i64
177            }
178            FcntlCommand::F_GETLK => {
179                let flock_ptr = ForeignPtr::<()>::from(arg).cast::<flock>();
180                let flock = ctx.objs.process.memory_borrow().read(flock_ptr)?;
181                let file = desc.file();
182                let params = flock_params(file, ctx.objs.process, &flock)?;
183                let lock_table = ctx.objs.host.fcntl_lock_table_borrow();
184                let out_flock = match lock_table.get_coalesced_conflicting_lock(
185                    params.file_id,
186                    params.range.clone(),
187                    &params.owner,
188                    params.access,
189                ) {
190                    Some((conflict_owner, conflict_range, conflict_access)) => {
191                        let start = i64::try_from(conflict_range.start).unwrap_or_else(|_err| {
192                            panic!("Current lock range {conflict_range:?} start is out of range")
193                        });
194                        // length > i64::MAX is represented as 0.
195                        // Primarily this happens when the lock is *set* with
196                        // length 0 (see FLOCK_LENGTH_0_OPEN_END), but Linux
197                        // also uses 0 to represent the length of a coalesced
198                        // lock whose length doesn't fit in an i64.
199                        let length = i64::try_from(conflict_range.len()).unwrap_or(0);
200                        flock {
201                            l_type: conflict_access.into(),
202                            l_whence: FlockWhence::SEEK_SET.into(),
203                            l_start: start,
204                            l_len: length,
205                            l_pid: match conflict_owner {
206                                LockOwner::Process(process_id) => process_id.into(),
207                            },
208                        }
209                    }
210                    None => flock {
211                        l_type: FlockType::F_UNLCK.into(),
212                        ..flock
213                    },
214                };
215                ctx.objs
216                    .process
217                    .memory_borrow_mut()
218                    .write(flock_ptr, &out_flock)?;
219                log::trace!("getlk {params:?} -> {out_flock:?}");
220                0i64
221            }
222            FcntlCommand::F_OFD_SETLK | FcntlCommand::F_OFD_SETLKW | FcntlCommand::F_OFD_GETLK => {
223                match desc.file() {
224                    CompatFile::New(_) => {
225                        warn_once_then_debug!("fcntl({cmd:?}) unimplemented for {:?}", desc.file());
226                        return Err(Errno::ENOSYS.into());
227                    }
228                    CompatFile::Legacy(_) => {
229                        warn_once_then_debug!(
230                            "Using fcntl({cmd:?}) implementation that assumes no lock contention. \
231                            See https://github.com/shadow/shadow/issues/2258"
232                        );
233                        drop(desc_table);
234                        return legacy_syscall_fn(ctx);
235                    }
236                };
237            }
238            FcntlCommand::F_GETFL => {
239                let file = desc.file();
240                // combine the file status and access mode flags
241                let flags = file.status().as_o_flags() | file.mode().as_o_flags();
242                flags.bits().into()
243            }
244            FcntlCommand::F_SETFL => {
245                let status = i32::try_from(arg).or(Err(Errno::EINVAL))?;
246                let mut status = OFlag::from_bits(status).ok_or(Errno::EINVAL)?;
247
248                // remove access mode flags
249                status.remove(OFlag::O_RDONLY | OFlag::O_WRONLY | OFlag::O_RDWR | OFlag::O_PATH);
250                // remove file creation flags
251                status.remove(
252                    OFlag::O_CLOEXEC
253                        | OFlag::O_CREAT
254                        | OFlag::O_DIRECTORY
255                        | OFlag::O_EXCL
256                        | OFlag::O_NOCTTY
257                        | OFlag::O_NOFOLLOW
258                        | OFlag::O_TMPFILE
259                        | OFlag::O_TRUNC,
260                );
261
262                let old_flags = desc.file().status().as_o_flags();
263
264                // fcntl(2): "On Linux, this command can change only the O_APPEND, O_ASYNC, O_DIRECT,
265                // O_NOATIME, and O_NONBLOCK flags"
266                let update_mask = OFlag::O_APPEND
267                    | OFlag::O_ASYNC
268                    | OFlag::O_DIRECT
269                    | OFlag::O_NOATIME
270                    | OFlag::O_NONBLOCK;
271
272                // The proper way for the process to update its flags is to:
273                //   int flags = fcntl(fd, F_GETFL);
274                //   flags = flags | O_NONBLOCK; // add O_NONBLOCK
275                //   fcntl(fd, F_SETFL, flags);
276                // So if there are flags that we can't update, we should assume they are leftover
277                // from the F_GETFL and we shouldn't return an error. This includes `O_DSYNC` and
278                // `O_SYNC`, which fcntl(2) says:
279                //   "It is not possible to use F_SETFL to change the state of the O_DSYNC and O_SYNC
280                //   flags. Attempts to change the state of these flags are silently ignored."
281                // In other words, the following code should always be valid:
282                //   int flags = fcntl(fd, F_GETFL);
283                //   fcntl(fd, F_SETFL, flags); // set to the current existing flags
284
285                // keep the old flags that we can't change, and use the new flags that we can change
286                let status = (old_flags & !update_mask) | (status & update_mask);
287
288                let (status, remaining) = FileStatus::from_o_flags(status);
289
290                // check if there are flags that we don't support but Linux does
291                if !remaining.is_empty() {
292                    return Err(Errno::EINVAL.into());
293                }
294
295                desc.file().set_status(status);
296                0
297            }
298            FcntlCommand::F_GETFD => desc.flags().bits().into(),
299            FcntlCommand::F_SETFD => {
300                let flags = i32::try_from(arg).or(Err(Errno::EINVAL))?;
301                let flags = DescriptorFlags::from_bits(flags).ok_or(Errno::EINVAL)?;
302                desc.set_flags(flags);
303                0
304            }
305            FcntlCommand::F_DUPFD => {
306                let min_fd = arg.try_into().or(Err(Errno::EINVAL))?;
307
308                let new_desc = desc.dup(DescriptorFlags::empty());
309                let new_fd = desc_table
310                    .register_descriptor_with_min_fd(new_desc, min_fd)
311                    .or(Err(Errno::EINVAL))?;
312                new_fd.into()
313            }
314            FcntlCommand::F_DUPFD_CLOEXEC => {
315                let min_fd = arg.try_into().or(Err(Errno::EINVAL))?;
316
317                let new_desc = desc.dup(DescriptorFlags::FD_CLOEXEC);
318                let new_fd = desc_table
319                    .register_descriptor_with_min_fd(new_desc, min_fd)
320                    .or(Err(Errno::EINVAL))?;
321                new_fd.into()
322            }
323            FcntlCommand::F_GETPIPE_SZ => {
324                let file = match desc.file() {
325                    CompatFile::New(d) => d,
326                    // if it's a legacy file, use the C syscall handler instead
327                    CompatFile::Legacy(_) => {
328                        return legacy_syscall_fn(ctx);
329                    }
330                };
331
332                if let File::Pipe(pipe) = file.inner_file() {
333                    pipe.borrow().max_size().try_into().unwrap()
334                } else {
335                    return Err(Errno::EINVAL.into());
336                }
337            }
338            cmd => {
339                warn_once_then_debug!("Unhandled fcntl command: {cmd:?}");
340                return Err(Errno::EINVAL.into());
341            }
342        })
343    }
344}