char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somrearrange libs into lib and extensions into extsf4a0f5b

main
27.0 KiB666 linesraw
1use super::paths::{follows_link, mutable_entry, resolve};
2use super::*;
3
4#[cfg(unix)]
5use std::os::fd::AsFd;
6
7#[cfg(target_os = "linux")]
8use std::os::fd::AsRawFd;
9
10#[cfg(unix)]
11fn times(secs: i64, nanos: u32) -> io::Result<rustix::fs::Timespec> {
12    if !(-8_640_000_000_000..=8_640_000_000_000).contains(&secs)
13        || (secs == 8_640_000_000_000 && nanos > 0)
14        || nanos >= 1_000_000_000
15    {
16        return Err(io::Error::new(
17            io::ErrorKind::InvalidInput,
18            "Invalid timestamp",
19        ));
20    }
21    Ok(rustix::fs::Timespec {
22        tv_sec: secs,
23        tv_nsec: nanos as _,
24    })
25}
26
27fn unchanged(directory: &Dir, name: &std::ffi::OsStr) -> io::Result<()> {
28    // Following ops resolve the referent before checking authority, so the
29    // leaf must still be that referent at use time; anything else raced.
30    if directory.symlink_metadata(name)?.is_symlink() {
31        return Err(io::Error::new(
32            io::ErrorKind::NotFound,
33            "Symbolic link changed or has a missing target",
34        ));
35    }
36    Ok(())
37}
38
39fn link_changed(error: io::Error) -> io::Error {
40    // A loop in the resolved path names no file; report it like a raced link.
41    #[cfg(unix)]
42    if error.raw_os_error() == Some(libc::ELOOP) {
43        return io::Error::new(
44            io::ErrorKind::NotFound,
45            "Symbolic link changed or has a missing target",
46        );
47    }
48    error
49}
50
51#[cfg(target_os = "linux")]
52fn fchmodat2_nr() -> Option<libc::c_long> {
53    // fchmodat2 is number 452 on the shared-table architectures and x86;
54    // x32 offsets it, and alpha/mips number it differently or not at all.
55    #[cfg(all(target_arch = "x86_64", target_pointer_width = "32"))]
56    {
57        Some(libc::SYS_fchmodat2)
58    }
59    #[cfg(any(
60        all(target_arch = "x86_64", target_pointer_width = "64"),
61        target_arch = "x86",
62        target_arch = "aarch64",
63        target_arch = "arm",
64        target_arch = "riscv64",
65        target_arch = "riscv32",
66        target_arch = "loongarch64",
67        target_arch = "m68k",
68        target_arch = "csky",
69        target_arch = "powerpc",
70        target_arch = "powerpc64",
71        target_arch = "s390x",
72        target_arch = "sparc",
73        target_arch = "sparc64"
74    ))]
75    {
76        Some(452)
77    }
78    #[cfg(not(any(
79        target_arch = "x86_64",
80        target_arch = "x86",
81        target_arch = "aarch64",
82        target_arch = "arm",
83        target_arch = "riscv64",
84        target_arch = "riscv32",
85        target_arch = "loongarch64",
86        target_arch = "m68k",
87        target_arch = "csky",
88        target_arch = "powerpc",
89        target_arch = "powerpc64",
90        target_arch = "s390x",
91        target_arch = "sparc",
92        target_arch = "sparc64"
93    )))]
94    {
95        None
96    }
97}
98
99#[cfg(target_os = "linux")]
100fn chmod_nofollow(
101    directory: std::os::fd::BorrowedFd<'_>,
102    name: &std::ffi::OsStr,
103    mode: u32,
104) -> io::Result<()> {
105    use std::os::unix::ffi::OsStrExt;
106    let name = std::ffi::CString::new(name.as_bytes())
107        .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "Invalid file name"))?;
108    // SAFETY: fchmodat2 takes a directory fd, a NUL-terminated leaf name,
109    // a mode, and flags; all are valid here.
110    let number = match fchmodat2_nr() {
111        // Architectures numbering fchmodat2 differently take the open path.
112        Some(number) => number,
113        None => return Err(io::Error::from_raw_os_error(libc::ENOSYS)),
114    };
115    let result = unsafe {
116        libc::syscall(
117            number,
118            directory.as_raw_fd(),
119            name.as_ptr(),
120            mode,
121            libc::AT_SYMLINK_NOFOLLOW,
122        )
123    };
124    if result == 0 {
125        Ok(())
126    } else {
127        Err(io::Error::last_os_error())
128    }
129}
130
131#[cfg(unix)]
132fn owner(
133    uid: Option<u32>,
134    gid: Option<u32>,
135) -> io::Result<(Option<rustix::fs::Uid>, Option<rustix::fs::Gid>)> {
136    // -1 (u32::MAX) means "no change" to the kernel; it must stay explicit null.
137    if uid.is_some_and(|id| id == u32::MAX) || gid.is_some_and(|id| id == u32::MAX) {
138        return Err(io::Error::new(
139            io::ErrorKind::InvalidInput,
140            "Invalid user or group id",
141        ));
142    }
143    if uid.is_none() && gid.is_none() {
144        return Err(io::Error::new(
145            io::ErrorKind::InvalidInput,
146            "Expected a user or group id",
147        ));
148    }
149    Ok((
150        uid.map(rustix::fs::Uid::from_raw),
151        gid.map(rustix::fs::Gid::from_raw),
152    ))
153}
154
155pub(super) fn ops(files: Arc<Files>) -> Vec<Op> {
156    let ops = files.clone();
157    let chmod = files.clone();
158    let chown = files.clone();
159    let lchown = files.clone();
160    let utime = files.clone();
161    let lutime = files.clone();
162    let link = files.clone();
163    let truncate = files.clone();
164    let fchmod = files.clone();
165    let fchown = files.clone();
166    let futime = files.clone();
167    let flock = files.clone();
168    let funlock = files.clone();
169    vec![
170        ops.io_ops(
171            "op_file_chmod",
172            "op_file_chmod_sync",
173            move |(path, mode): (String, u32)| {
174                let caps = chmod.caps.clone();
175                let pending = chmod.pool.run(move || {
176                    if mode > 0o7777 {
177                        return Err(io::Error::new(
178                            io::ErrorKind::InvalidInput,
179                            "Invalid file mode",
180                        ));
181                    }
182                    let target = resolve(&caps, &path, false, true).map_err(link_changed)?;
183                    let (directory, name) = mutable_entry(&target)?;
184                    authorize(&caps, &target, &path, true)?;
185                    unchanged(&directory, &name)?;
186                    #[cfg(target_os = "linux")]
187                    {
188                        // fchmodat2 with NOFOLLOW chmods the resolved leaf itself: a
189                        // link swapped in after the pre-check fails closed instead
190                        // of retargeting, and with no open, unreadable files,
191                        // locked directories, and device nodes need no access.
192                        match chmod_nofollow(directory.as_fd(), &name, mode) {
193                            Ok(()) => Ok(true),
194                            Err(error) if error.raw_os_error() == Some(libc::EOPNOTSUPP) => {
195                                // A link swapped in after the pre-check fails here
196                                // instead of retargeting.
197                                Err(io::Error::new(
198                                    io::ErrorKind::NotFound,
199                                    "Symbolic link changed or has a missing target",
200                                ))
201                            }
202                            Err(error)
203                                if matches!(
204                                    error.raw_os_error(),
205                                    Some(libc::ENOSYS) | Some(libc::EPERM)
206                                ) =>
207                            {
208                                // Kernels before 6.6 lack fchmodat2, and seccomp
209                                // filters may block it; open without following,
210                                // preferring read over write.
211                                let mut read = OpenOptions::new();
212                                read.read(true).follow(FollowSymlinks::No).nonblock(true);
213                                let mut write = OpenOptions::new();
214                                write.write(true).follow(FollowSymlinks::No).nonblock(true);
215                                let opened = directory
216                                    .open_with(&name, &read)
217                                    .or_else(|_| directory.open_with(&name, &write))
218                                    .map_err(link_changed)?;
219                                let metadata = opened.metadata()?;
220                                if !metadata.is_file() && !metadata.is_dir() {
221                                    return Err(io::Error::new(
222                                        io::ErrorKind::InvalidInput,
223                                        "Cannot change mode of non-regular file",
224                                    ));
225                                }
226                                rustix::fs::fchmod(
227                                    opened.as_fd(),
228                                    rustix::fs::Mode::from_raw_mode(mode),
229                                )?;
230                                Ok(true)
231                            }
232                            Err(error) => Err(error),
233                        }
234                    }
235                    #[cfg(all(unix, not(target_os = "linux")))]
236                    {
237                        rustix::fs::chmodat(
238                            directory.as_fd(),
239                            Path::new(&name),
240                            rustix::fs::Mode::from_raw_mode(mode),
241                            rustix::fs::AtFlags::SYMLINK_NOFOLLOW,
242                        )?;
243                        Ok(true)
244                    }
245                    #[cfg(not(unix))]
246                    {
247                        let _ = (&directory, &name);
248                        Err(io::Error::new(
249                            io::ErrorKind::Unsupported,
250                            "not yet implemented: file modes on this platform",
251                        ))
252                    }
253                });
254                async move { Ok::<_, Infallible>(receive(pending).await) }
255            },
256        ),
257        ops.io_ops(
258            "op_file_chown",
259            "op_file_chown_sync",
260            move |(path, uid, gid): (String, Option<u32>, Option<u32>)| {
261                let caps = chown.caps.clone();
262                let pending = chown.pool.run(move || {
263                    #[cfg(unix)]
264                    let owned = owner(uid, gid)?;
265                    let target = resolve(&caps, &path, false, true)?;
266                    let (directory, name) = mutable_entry(&target)?;
267                    authorize(&caps, &target, &path, true)?;
268                    unchanged(&directory, &name)?;
269                    #[cfg(unix)]
270                    {
271                        let (uid, gid) = owned;
272                        rustix::fs::chownat(
273                            directory.as_fd(),
274                            Path::new(&name),
275                            uid,
276                            gid,
277                            rustix::fs::AtFlags::SYMLINK_NOFOLLOW,
278                        )?;
279                        Ok(true)
280                    }
281                    #[cfg(not(unix))]
282                    {
283                        let _ = (&directory, &name, uid, gid);
284                        Err(io::Error::new(
285                            io::ErrorKind::Unsupported,
286                            "not yet implemented: file ownership on this platform",
287                        ))
288                    }
289                });
290                async move { Ok::<_, Infallible>(receive(pending).await) }
291            },
292        ),
293        ops.io_ops(
294            "op_file_lchown",
295            "op_file_lchown_sync",
296            move |(path, uid, gid): (String, Option<u32>, Option<u32>)| {
297                let caps = lchown.caps.clone();
298                let pending = lchown.pool.run(move || {
299                    #[cfg(unix)]
300                    let owned = owner(uid, gid)?;
301                    // A trailing slash names the referent, like path_stat.
302                    let target = resolve(&caps, &path, !follows_link(&path), true)?;
303                    let (directory, name) = mutable_entry(&target)?;
304                    authorize(&caps, &target, &path, true)?;
305                    #[cfg(unix)]
306                    {
307                        let (uid, gid) = owned;
308                        rustix::fs::chownat(
309                            directory.as_fd(),
310                            Path::new(&name),
311                            uid,
312                            gid,
313                            rustix::fs::AtFlags::SYMLINK_NOFOLLOW,
314                        )?;
315                        Ok(true)
316                    }
317                    #[cfg(not(unix))]
318                    {
319                        let _ = (&directory, &name, uid, gid);
320                        Err(io::Error::new(
321                            io::ErrorKind::Unsupported,
322                            "not yet implemented: file ownership on this platform",
323                        ))
324                    }
325                });
326                async move { Ok::<_, Infallible>(receive(pending).await) }
327            },
328        ),
329        ops.io_ops(
330            "op_file_utime",
331            "op_file_utime_sync",
332            move |(path, asec, anano, msec, mnano): (String, i64, u32, i64, u32)| {
333                let caps = utime.caps.clone();
334                let pending = utime.pool.run(move || {
335                    #[cfg(unix)]
336                    let times = rustix::fs::Timestamps {
337                        last_access: times(asec, anano)?,
338                        last_modification: times(msec, mnano)?,
339                    };
340                    let target = resolve(&caps, &path, false, true)?;
341                    let (directory, name) = mutable_entry(&target)?;
342                    authorize(&caps, &target, &path, true)?;
343                    unchanged(&directory, &name)?;
344                    #[cfg(unix)]
345                    {
346                        rustix::fs::utimensat(
347                            directory.as_fd(),
348                            Path::new(&name),
349                            &times,
350                            rustix::fs::AtFlags::SYMLINK_NOFOLLOW,
351                        )?;
352                        Ok(true)
353                    }
354                    #[cfg(not(unix))]
355                    {
356                        let _ = (&directory, &name, asec, anano, msec, mnano);
357                        Err(io::Error::new(
358                            io::ErrorKind::Unsupported,
359                            "not yet implemented: file times on this platform",
360                        ))
361                    }
362                });
363                async move { Ok::<_, Infallible>(receive(pending).await) }
364            },
365        ),
366        ops.io_ops(
367            "op_file_lutime",
368            "op_file_lutime_sync",
369            move |(path, asec, anano, msec, mnano): (String, i64, u32, i64, u32)| {
370                let caps = lutime.caps.clone();
371                let pending = lutime.pool.run(move || {
372                    #[cfg(unix)]
373                    let times = rustix::fs::Timestamps {
374                        last_access: times(asec, anano)?,
375                        last_modification: times(msec, mnano)?,
376                    };
377                    // A trailing slash names the referent, like path_stat.
378                    let target = resolve(&caps, &path, !follows_link(&path), true)?;
379                    let (directory, name) = mutable_entry(&target)?;
380                    authorize(&caps, &target, &path, true)?;
381                    #[cfg(unix)]
382                    {
383                        rustix::fs::utimensat(
384                            directory.as_fd(),
385                            Path::new(&name),
386                            &times,
387                            rustix::fs::AtFlags::SYMLINK_NOFOLLOW,
388                        )?;
389                        Ok(true)
390                    }
391                    #[cfg(not(unix))]
392                    {
393                        let _ = (&directory, &name, asec, anano, msec, mnano);
394                        Err(io::Error::new(
395                            io::ErrorKind::Unsupported,
396                            "not yet implemented: file times on this platform",
397                        ))
398                    }
399                });
400                async move { Ok::<_, Infallible>(receive(pending).await) }
401            },
402        ),
403        ops.io_ops(
404            "op_file_link",
405            "op_file_link_sync",
406            move |(source, destination): (String, String)| {
407                let caps = link.caps.clone();
408                let pending = link.pool.run(move || {
409                    // Link authorization must not race another managed link mutation.
410                    let _guard = super::links::MUTATIONS
411                        .lock()
412                        .unwrap_or_else(std::sync::PoisonError::into_inner);
413                    let from = resolve(&caps, &source, true, false)?;
414                    let to = resolve(&caps, &destination, true, true)?;
415                    let (from_dir, from_name) = parent(from.as_path())?;
416                    let (to_dir, to_name) = mutable_entry(&to)?;
417                    authorize(&caps, &from, &source, false)?;
418                    authorize(&caps, &from, &source, true)?;
419                    authorize(&caps, &to, &destination, true)?;
420                    // linkat would hard-link the link itself past its target checks.
421                    if from_dir.symlink_metadata(&from_name)?.is_symlink() {
422                        return Err(io::Error::new(
423                            io::ErrorKind::InvalidInput,
424                            "Source is a symbolic link",
425                        ));
426                    }
427                    from_dir.hard_link(from_name, &to_dir, to_name)?;
428                    Ok(true)
429                });
430                async move { Ok::<_, Infallible>(receive(pending).await) }
431            },
432        ),
433        ops.io_ops(
434            "op_file_path_truncate",
435            "op_file_path_truncate_sync",
436            move |(path, length): (String, u64)| {
437                let caps = truncate.caps.clone();
438                let pending = truncate.pool.run(move || {
439                    if length > SAFE_INTEGER {
440                        return Err(io::Error::new(
441                            io::ErrorKind::InvalidInput,
442                            "Unsafe truncate length",
443                        ));
444                    }
445                    let target = resolve(&caps, &path, false, true).map_err(link_changed)?;
446                    let (directory, name) = mutable_entry(&target)?;
447                    authorize(&caps, &target, &path, true)?;
448                    unchanged(&directory, &name)?;
449                    let mut flags = OpenOptions::new();
450                    flags.write(true).follow(FollowSymlinks::No).nonblock(true);
451                    let opened = directory.open_with(name, &flags)?;
452                    if !opened.metadata()?.is_file() {
453                        return Err(io::Error::new(
454                            io::ErrorKind::InvalidInput,
455                            "Cannot truncate non-regular file",
456                        ));
457                    }
458                    opened.set_len(length)?;
459                    Ok(true)
460                });
461                async move { Ok::<_, Infallible>(receive(pending).await) }
462            },
463        ),
464        ops.io_ops(
465            "op_file_fchmod",
466            "op_file_fchmod_sync",
467            move |(id, mode): (u64, u32)| {
468                let pending = fchmod.submit(id, move |opened| {
469                    if mode > 0o7777 {
470                        return Err(io::Error::new(
471                            io::ErrorKind::InvalidInput,
472                            "Invalid file mode",
473                        ));
474                    }
475                    opened.check(true)?;
476                    #[cfg(unix)]
477                    {
478                        rustix::fs::fchmod(
479                            opened.file.as_fd(),
480                            rustix::fs::Mode::from_raw_mode(mode),
481                        )?;
482                        Ok(true)
483                    }
484                    #[cfg(not(unix))]
485                    {
486                        Err(io::Error::new(
487                            io::ErrorKind::Unsupported,
488                            "not yet implemented: file modes on this platform",
489                        ))
490                    }
491                });
492                async move { Ok::<_, Infallible>(receive(pending).await) }
493            },
494        ),
495        ops.io_ops(
496            "op_file_fchown",
497            "op_file_fchown_sync",
498            move |(id, uid, gid): (u64, Option<u32>, Option<u32>)| {
499                let pending = fchown.submit(id, move |opened| {
500                    #[cfg(unix)]
501                    let owned = owner(uid, gid)?;
502                    opened.check(true)?;
503                    #[cfg(unix)]
504                    {
505                        let (uid, gid) = owned;
506                        rustix::fs::fchown(opened.file.as_fd(), uid, gid)?;
507                        Ok(true)
508                    }
509                    #[cfg(not(unix))]
510                    {
511                        let _ = (uid, gid);
512                        Err(io::Error::new(
513                            io::ErrorKind::Unsupported,
514                            "not yet implemented: file ownership on this platform",
515                        ))
516                    }
517                });
518                async move { Ok::<_, Infallible>(receive(pending).await) }
519            },
520        ),
521        ops.io_ops(
522            "op_file_futime",
523            "op_file_futime_sync",
524            move |(id, asec, anano, msec, mnano): (u64, i64, u32, i64, u32)| {
525                let pending = futime.submit(id, move |opened| {
526                    #[cfg(unix)]
527                    let times = rustix::fs::Timestamps {
528                        last_access: times(asec, anano)?,
529                        last_modification: times(msec, mnano)?,
530                    };
531                    opened.check(true)?;
532                    #[cfg(unix)]
533                    {
534                        rustix::fs::futimens(opened.file.as_fd(), &times)?;
535                        Ok(true)
536                    }
537                    #[cfg(not(unix))]
538                    {
539                        let _ = (asec, anano, msec, mnano);
540                        Err(io::Error::new(
541                            io::ErrorKind::Unsupported,
542                            "not yet implemented: file times on this platform",
543                        ))
544                    }
545                });
546                async move { Ok::<_, Infallible>(receive(pending).await) }
547            },
548        ),
549        ops.io_ops(
550            "op_file_flock",
551            "op_file_flock_sync",
552            move |(id, exclusive): (u64, bool)| {
553                let state = flock.clone();
554                async move {
555                    #[cfg(unix)]
556                    {
557                        // Only nonblocking attempts run on the handle queue, so a
558                        // waiting lock holds neither a pool thread nor the queue.
559                        let mut wait = std::time::Duration::from_millis(5);
560                        loop {
561                            let pending = state.submit(id, move |opened| {
562                                if exclusive {
563                                    opened.check(true)?;
564                                } else {
565                                    opened.check(false)?;
566                                }
567                                rustix::fs::flock(
568                                    opened.file.as_fd(),
569                                    if exclusive {
570                                        rustix::fs::FlockOperation::NonBlockingLockExclusive
571                                    } else {
572                                        rustix::fs::FlockOperation::NonBlockingLockShared
573                                    },
574                                )?;
575                                Ok(true)
576                            });
577                            match receive(pending).await {
578                                (value @ Some(_), None) => {
579                                    // A closed handle releases its locks; never
580                                    // report success past close.
581                                    let live = state.submit(id, |_| Ok::<_, io::Error>(true));
582                                    match receive(live).await {
583                                        (Some(_), None) => {
584                                            return Ok::<_, Infallible>((value, None));
585                                        }
586                                        (_, error) => return Ok((None, error)),
587                                    }
588                                }
589                                (None, Some(("Busy", _))) => {
590                                    let delay = {
591                                        let _enter = state.pool.runtime.enter();
592                                        tokio::time::sleep(wait)
593                                    };
594                                    delay.await;
595                                    wait = (wait * 2).min(std::time::Duration::from_millis(100));
596                                }
597                                done => return Ok(done),
598                            }
599                        }
600                    }
601                    #[cfg(not(unix))]
602                    {
603                        let pending = state.submit(id, move |opened| {
604                            if exclusive {
605                                opened.check(true)?;
606                            } else {
607                                opened.check(false)?;
608                            }
609                            let _ = exclusive;
610                            Err::<bool, _>(io::Error::new(
611                                io::ErrorKind::Unsupported,
612                                "not yet implemented: file locking on this platform",
613                            ))
614                        });
615                        Ok(receive(pending).await)
616                    }
617                }
618            },
619        ),
620        ops.io_ops("op_file_funlock", "op_file_funlock_sync", move |id: u64| {
621            // Releasing a lock grants nothing, so any live handle may unlock.
622            let pending = funlock.submit(id, |opened| {
623                #[cfg(unix)]
624                {
625                    rustix::fs::flock(opened.file.as_fd(), rustix::fs::FlockOperation::Unlock)?;
626                    Ok(true)
627                }
628                #[cfg(not(unix))]
629                {
630                    let _ = opened;
631                    Err(io::Error::new(
632                        io::ErrorKind::Unsupported,
633                        "not yet implemented: file locking on this platform",
634                    ))
635                }
636            });
637            async move { Ok::<_, Infallible>(receive(pending).await) }
638        }),
639    ]
640    .into_iter()
641    .flatten()
642    .collect()
643}
644
645#[cfg(all(test, unix))]
646mod tests {
647    use super::*;
648
649    #[test]
650    fn leaf_check_rejects_links_swapped_in_after_resolve() {
651        let root = tempfile::tempdir().unwrap();
652        std::fs::write(root.path().join("leaf"), b"data").unwrap();
653        let caps = Capabilities::new(avril_caps::Initial::new(false), root.path().to_owned());
654        let target = resolve(&caps, "leaf", false, true).unwrap();
655        let (directory, name) = parent(target.as_path()).unwrap();
656        unchanged(&directory, &name).unwrap();
657        std::fs::remove_file(root.path().join("leaf")).unwrap();
658        std::os::unix::fs::symlink("elsewhere", root.path().join("leaf")).unwrap();
659        let error = unchanged(&directory, &name).unwrap_err();
660        assert_eq!(error.kind(), io::ErrorKind::NotFound);
661        assert_eq!(
662            error.to_string(),
663            "Symbolic link changed or has a missing target"
664        );
665    }
666}