fs: follow symlinks like Linux; Linux mknod; /proc/partitions

Symlinks are followed in namex_from() now, in the middle of a path as
well as at the end, and a relative target resolves from the directory
that holds the link rather than from the caller's cwd. `ln -s f d1/s;
cat d1/s` said "Symbolic link loop" because it looked for ./f. A loop is
ELOOP after 40 links, a dangling link ENOENT, and open(O_CREAT) through
a link to a file opens the file. divesymlink() is gone.

mknod from a Linux program passes (mode, dev) where the native call
passes (major, minor), and was read as the native one: `mknod n c 1 2`
made node 432,258, and mkfifo made a character device. Both are decoded
now; a FIFO is EPERM, and an existing name is EEXIST.

/proc/mounts listed devtmpfs twice: once from a fixed line and once
from the mount table. The fixed line is gone, and mount_add() refuses
a filesystem mounted on its own root, which is where a second devtmpfs
mount lands. A read-only FAT mount says ro.

/proc/partitions exists, in Linux's format, and /proc/filesystems lists
vfat.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
This commit is contained in:
Nexoniarz 2026-10-23 10:50:41 +02:00
commit 7a1e63a553
8 changed files with 274 additions and 136 deletions

View file

@ -54,6 +54,9 @@ struct linux_stat {
#define LINUX_S_IFREG 0100000
#define LINUX_S_IFLNK 0120000
#define LINUX_S_IFSOCK 0140000
#define LINUX_S_IFMT 0170000
#define LINUX_S_IFBLK 0060000
#define LINUX_S_IFIFO 0010000
// Linux's dev_t: 12 bits of major and 8 of minor packed into the low 32, with
// the overflow of each above that. Spelled out in full rather than as
@ -162,6 +165,17 @@ at_namei(int dirfd, char *path)
return nameifrom(start, path);
}
// at_namei() that follows a final symlink as well, and says why it failed.
static struct inode *
at_nameif(int dirfd, char *path, int *err)
{
*err = ENOENT;
if (!path) return 0;
struct inode *start = (path[0] == '/') ? myproc()->cwd : at_dir(dirfd);
if (!start) { *err = EBADF; return 0; }
return nameifollow(start, path, err);
}
static struct inode *
at_nameiparent(int dirfd, char *path, char *name)
{
@ -206,7 +220,9 @@ createat_x(int dirfd, char *path, short type, short major, short minor,
// `> /dev/null` did not work, despite /dev/null existing.
// mkdir/mknod ask for T_DIR/T_DEV and still correctly fail here,
// which is what lets them report EEXIST.
if (type == T_FILE && ip->type != T_DIR)
// A symlink found here dangles: open() already followed every link
// that leads somewhere. Writing into it would rewrite its target.
if (type == T_FILE && ip->type != T_DIR && ip->type != T_SYMLINK)
return ip;
iunlockput(ip);
return 0;
@ -479,33 +495,33 @@ addr_t sys_pipe2(void)
// Filesystem ops
// -----------------------------------------------------------------------
// Resolve a symlink opened as the final path component.
//
// divesymlink() has been in kernel/fs/fs.c since the fork and nothing ever
// called it, so a symlink behaved as a small file containing its own target:
// `cat link` printed the target's *name* rather than its contents, and every
// program that follows a symlink to find something followed it nowhere.
//
// Only the last component. A symlink in the middle of a path -- /a/link/b --
// is still not resolved; that belongs in namex(), which would have to re-enter
// path lookup while holding a directory lock, and getting that wrong is a
// deadlock rather than a wrong answer. Final-component resolution is what
// open(), exec() and stat() need, which is nearly everything that matters.
//
// Takes a locked inode and returns a locked inode, or 0 with everything
// released -- the same contract divesymlink() already had.
// The lookup for opening something that exists. A final symlink is followed,
// from the directory it is in, unless O_NOFOLLOW, which makes one an ELOOP the
// way POSIX has it (this kernel has no O_PATH to open the link itself).
// Returns the inode unlocked, or 0 with *err set.
static struct inode *
follow_final(struct inode *ip, int omode)
open_lookup(int dirfd, char *path, int omode, int *err)
{
if (ip->type != T_SYMLINK)
return ip;
if (omode & O_NOFOLLOW) {
// POSIX: opening a symlink itself is an error unless O_PATH, which
// this kernel does not have. Saying so beats handing back the link.
iunlockput(ip);
return 0;
}
return divesymlink(ip);
if (!(omode & O_NOFOLLOW))
return at_nameif(dirfd, path, err);
struct inode *ip = at_namei(dirfd, path);
*err = ENOENT;
if (!ip) return 0;
ilock(ip);
if (ip->type == T_SYMLINK) { iunlockput(ip); *err = ELOOP; return 0; }
iunlock(ip);
return ip;
}
// open(O_CREAT) on a name that exists through a symlink opens the target, so
// `echo hi > link` writes where the link points. 0 if there is nothing there
// (or O_EXCL), and the caller creates.
static struct inode *
open_existing(int dirfd, char *path, int omode)
{
int err;
if (omode & (O_EXCL | O_NOFOLLOW)) return 0;
return at_nameif(dirfd, path, &err);
}
addr_t sys_open(void)
@ -523,8 +539,12 @@ addr_t sys_open(void)
if (ex) { iput(ex); end_op(); return -EEXIST; }
}
int cerr = 0;
ip = createat_x(AT_FDCWD, path,
(omode & O_EXTENT) ? T_EXTENT : T_FILE, 0, 0, &cerr);
if ((ip = open_existing(AT_FDCWD, path, omode)) != 0) {
ilock(ip);
if (ip->type == T_DIR) { iunlockput(ip); end_op(); return -EISDIR; }
} else
ip = createat_x(AT_FDCWD, path,
(omode & O_EXTENT) ? T_EXTENT : T_FILE, 0, 0, &cerr);
// A refusal because the filesystem is read-only is not "no such file".
// A shell prints the same sentence for every create failure, so
// returning ENOENT here made writing to a snapshot look like a missing
@ -533,9 +553,9 @@ addr_t sys_open(void)
if ((omode & O_TRUNC) && (ip->type == T_FILE || ip->type == T_EXTENT))
itrunc(ip);
} else {
if (!(ip = namei(path))) { end_op(); return -ENOENT; }
int err;
if (!(ip = open_lookup(AT_FDCWD, path, omode, &err))) { end_op(); return -err; }
ilock(ip);
if (!(ip = follow_final(ip, omode))) { end_op(); return -ELOOP; }
// Directories may only be opened for reading, but callers (e.g.
// opendir()) legitimately OR in flags like O_DIRECTORY/O_CLOEXEC/
// O_NONBLOCK alongside O_RDONLY — only the access-mode bits matter.
@ -577,14 +597,17 @@ addr_t sys_openat(void)
if (ex) { iput(ex); end_op(); return -EEXIST; }
}
int cerr = 0;
if (!(ip = createat_x(dirfd, path, T_FILE, 0, 0, &cerr))) {
if ((ip = open_existing(dirfd, path, omode)) != 0) {
ilock(ip);
if (ip->type == T_DIR) { iunlockput(ip); end_op(); return -EISDIR; }
} else if (!(ip = createat_x(dirfd, path, T_FILE, 0, 0, &cerr))) {
end_op();
return cerr ? -cerr : -ENOENT;
}
} else {
if (!(ip = at_namei(dirfd, path))) { end_op(); return -ENOENT; }
int err;
if (!(ip = open_lookup(dirfd, path, omode, &err))) { end_op(); return -err; }
ilock(ip);
if (!(ip = follow_final(ip, omode))) { end_op(); return -ELOOP; }
// Directories may only be opened for reading, but callers (e.g.
// opendir()) legitimately OR in flags like O_DIRECTORY/O_CLOEXEC/
// O_NONBLOCK alongside O_RDONLY — only the access-mode bits matter.
@ -656,13 +679,13 @@ do_stat(int dirfd, char *path, char *st, int follow)
{
struct inode *ip;
int err = ENOENT;
begin_op();
if (!(ip = at_namei(dirfd, path))) { end_op(); return -ENOENT; }
ilock(ip);
if (follow && ip->type == T_SYMLINK && !(ip = divesymlink(ip))) {
if (!(ip = follow ? at_nameif(dirfd, path, &err) : at_namei(dirfd, path))) {
end_op();
return -ELOOP;
return -err;
}
ilock(ip);
if (myproc()->syscall_map) stati_linux(ip, (struct linux_stat*)st);
else stati(ip, (struct stat*)st);
iunlockput(ip);
@ -692,9 +715,10 @@ addr_t sys_access(void)
{
char *path;
if (argstr(0, &path) < 0) return -1;
int err;
begin_op();
struct inode *ip = namei(path);
if (!ip) { end_op(); return -ENOENT; }
struct inode *ip = at_nameif(AT_FDCWD, path, &err);
if (!ip) { end_op(); return -err; }
iput(ip); end_op();
return 0;
}
@ -705,7 +729,8 @@ addr_t sys_chdir(void)
struct inode *ip;
begin_op();
if (argstr(0, &path) < 0) { end_op(); return -EFAULT; }
if (!(ip = namei(path))) { end_op(); return -ENOENT; }
int err;
if (!(ip = at_nameif(AT_FDCWD, path, &err))) { end_op(); return -err; }
ilock(ip);
// ENOTDIR, not a bare -1. `cd` into a file must fail, and it did, but the
// shell reported "Operation not permitted" -- which reads like a
@ -775,14 +800,65 @@ addr_t sys_mkdir(void)
return 0;
}
// What a Linux mknod(mode, dev) asks for, as a type here. Character and block
// devices are both T_DEV (the major says which driver); a regular file is what
// mknod(S_IFREG) has always meant. Named pipes and sockets have no on-disk
// form here yet, and Linux's answer for a filesystem that cannot hold one is
// EPERM. Returns the type, or a negative errno.
static int
mknod_type(uint32 mode)
{
switch (mode & LINUX_S_IFMT) {
case 0:
case LINUX_S_IFREG: return T_FILE;
case LINUX_S_IFCHR:
case LINUX_S_IFBLK: return T_DEV;
default: return -EPERM;
}
}
static addr_t
do_mknod(int dirfd, char *path, uint32 mode, uint64 dev)
{
int type = mknod_type(mode);
if (type < 0) return type;
// Linux's dev_t: 12 bits of major in 8..19 and the rest from bit 32, the
// minor's low byte in 0..7 and the rest from bit 20. The other half of
// linux_makedev().
int major = (int)(((dev >> 8) & 0xfff) | ((dev >> 32) & ~0xfffull));
int minor = (int)((dev & 0xff) | ((dev >> 12) & ~0xffull));
if (type == T_FILE) major = minor = 0;
begin_op();
// createat_x() opens an existing file when asked for one, as open(O_CREAT)
// wants. mknod() must not.
struct inode *ex = at_namei(dirfd, path);
if (ex) { iput(ex); end_op(); return -EEXIST; }
int cerr = 0;
struct inode *ip = createat_x(dirfd, path, type, (short)major,
(short)minor, &cerr);
if (!ip) { end_op(); return cerr ? -cerr : -EEXIST; }
iunlockput(ip); end_op();
return 0;
}
// The native call is mknod(path, major, minor); a Linux program's is
// mknod(path, mode, dev), on the same slot through the syscall map. Reading
// the Linux one as the native one made `mknod n c 1 2` a node numbered from
// the mode bits: 432, 258.
addr_t sys_mknod(void)
{
struct inode *ip;
char *path;
int major, minor;
if (argstr(0, &path) < 0 || argint(1, &major) < 0 || argint(2, &minor) < 0)
return -EFAULT;
if (myproc()->syscall_map) {
uint64 dev;
argaddr(2, &dev);
return do_mknod(AT_FDCWD, path, (uint32)major, dev);
}
begin_op();
if (argstr(0, &path) < 0 || argint(1, &major) < 0 || argint(2, &minor) < 0 ||
!(ip = create(path, T_DEV, major, minor))) { end_op(); return -1; }
if (!(ip = create(path, T_DEV, major, minor))) { end_op(); return -1; }
iunlockput(ip); end_op();
return 0;
}
@ -953,8 +1029,9 @@ addr_t sys_truncate(void)
struct inode *ip;
if (argstr(0, &path) < 0 || argaddr(1, &length) < 0) return -1;
if ((int64)length < 0) return -EINVAL;
int err;
begin_op();
if (!(ip = namei(path))) { end_op(); return -ENOENT; }
if (!(ip = at_nameif(AT_FDCWD, path, &err))) { end_op(); return -err; }
ilock(ip);
itrunc_to(ip, (uint64)length);
iunlockput(ip); end_op();
@ -1651,10 +1728,12 @@ addr_t sys_utimensat(void)
} else {
char *path;
if (argstr(1, &path) < 0) return -EFAULT;
int err = ENOENT;
begin_op();
ip = at_namei(dirfd, path);
ip = (flags & AT_SYMLINK_NOFOLLOW) ? at_namei(dirfd, path)
: at_nameif(dirfd, path, &err);
end_op();
if (!ip) return -ENOENT;
if (!ip) return -err;
}
uint64 now = 0, us = 0;
@ -1747,9 +1826,10 @@ addr_t sys_statfs(void)
// Resolve the path: which filesystem it lands on is the whole question,
// and mount crossing happens during resolution.
int err;
begin_op();
struct inode *ip = namei(path);
if (!ip) { end_op(); return -ENOENT; }
struct inode *ip = at_nameif(AT_FDCWD, path, &err);
if (!ip) { end_op(); return -err; }
uint dev = ip->dev;
iput(ip);
end_op();
@ -1806,17 +1886,10 @@ addr_t sys_mknodat(void)
uint64 dev;
argint(0, &dirfd);
if (argstr(1, &path) < 0) return -1;
argint(2, &(int){0}); // mode — ignored
int mode;
argint(2, &mode);
argaddr(3, &dev);
int major = (dev >> 8) & 0xff;
int minor = dev & 0xff;
begin_op();
int cerr = 0;
struct inode *ip = createat_x(dirfd, path, T_DEV, (short)major,
(short)minor, &cerr);
if (!ip) { end_op(); return cerr ? -cerr : -1; }
iunlockput(ip); end_op();
return 0;
return do_mknod(dirfd, path, (uint32)mode, dev);
}
addr_t sys_unlinkat(void)
@ -1970,9 +2043,10 @@ addr_t sys_faccessat(void)
if (argstr(1, &path) < 0) return -1;
argint(2, &(int){0}); // amode — ignored
// arg3 flags — ignored
int err;
begin_op();
struct inode *ip = at_namei(dirfd, path);
if (!ip) { end_op(); return -ENOENT; }
struct inode *ip = at_nameif(dirfd, path, &err);
if (!ip) { end_op(); return -err; }
iput(ip); end_op();
return 0;
}