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

@ -11,6 +11,10 @@
#include "errno.h"
#include "x86.h"
#include "fsstat.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "fs.h"
#include "file.h"
// Binaries with entry points at 0x100000+ are musl/Linux-ABI.
// SchOS-native binaries (init) are linked at 0x4000.
@ -56,11 +60,12 @@ exec(char *path, char **argv, char **envp)
begin_op();
if((ip = namei(path)) == 0){
int err;
if((ip = nameifollow(0, path, &err)) == 0){
end_op();
// Must be ENOENT, not a bare -1: execvp() relies on this exact errno to
// know it should try the next $PATH component rather than give up.
return -ENOENT;
return -err;
}
ilock(ip);

View file

@ -44,6 +44,7 @@
#include "fsstat.h"
#include "file.h"
#include "fat.h"
#include "errno.h"
extern uint uptime(void);
@ -1616,6 +1617,11 @@ int
mount_add(uint dev, uint64 inum, uint fsdev, uint64 root,
const char *path, const char *source, const char *type)
{
// The lookup that found the mount point already crossed any mount on it,
// so mounting devtmpfs on /dev a second time lands on devtmpfs's own root.
// That stacked it on itself and listed it twice in /proc/mounts.
if (dev == fsdev && inum == root)
return -1;
acquire(&mtable.lock);
for (int i = 0; i < NMOUNT; i++)
if (mtable.m[i].used && mtable.m[i].dev == dev &&
@ -1742,24 +1748,30 @@ mount_cross(struct inode *ip)
// ownership of its own reference) rather than always `myproc()->cwd`. This is
// what makes a real (non-AT_FDCWD) dirfd argument to the *at() syscalls
// actually work: an absolute path still resolves from root either way.
//
// Symlinks are followed the way Linux follows them. One in the middle of the
// path always is; the last component only when `follow` says so, since
// lstat(), readlink(), unlink() and friends want the link itself. A relative
// target is resolved from the directory that holds the link, not from the
// caller's cwd, and more than MAXSYMLINKS of them in one lookup is ELOOP.
//
// The link is read with no directory locked: the walk only ever holds a
// reference to the directory it is in, so re-entering it costs nothing.
#define MAXSYMLINKS 40
static struct inode *
namex_from(struct inode *start, char *path, int nameiparent, char *name)
namex_from(struct inode *start, char *path, int nameiparent, char *name,
int follow, int *err)
{
struct inode *ip, *next;
int links = 0;
// Collapse consecutive slashes into a safe local copy.
// A private copy, because following a link rewrites what is left of it.
char safe[MAXPATH];
int i = 0, j = 0;
while (path[i] != '\0' && j < MAXPATH - 1) {
if (path[i] == '/') {
safe[j++] = '/';
while (path[i] == '/') i++;
} else {
safe[j++] = path[i++];
}
}
safe[j] = '\0';
safestrcpy(safe, path, MAXPATH);
char *spath = safe;
if (err)
*err = ENOENT;
// Root is a SchFS object, not a v1 inode. ops_for(SCHFS_DEV) then routes
// every operation on it through schfs_ops, which is the whole point: the v1
@ -1786,6 +1798,8 @@ namex_from(struct inode *start, char *path, int nameiparent, char *name)
ilock(ip);
if (ip->type != T_DIR) {
iunlockput(ip);
if (err)
*err = ENOTDIR;
return 0;
}
if (nameiparent && *spath == '\0') {
@ -1796,8 +1810,41 @@ namex_from(struct inode *start, char *path, int nameiparent, char *name)
iunlockput(ip);
return 0;
}
iunlockput(ip);
ip = mount_cross(next);
iunlock(ip);
next = mount_cross(next);
ilock(next);
if (next->type != T_SYMLINK || (*spath == '\0' && (!follow || nameiparent))) {
iunlock(next);
iput(ip);
ip = next;
continue;
}
// A link: put its target in front of what is left of the path and carry
// on from the directory it was found in, or from root.
char tgt[MAXPATH];
int len = (++links > MAXSYMLINKS) ? -1
: readi(next, 0, (uint64)tgt, 0, MAXPATH - 1);
iunlockput(next);
int rest = strlen(spath);
if (len <= 0 || len + 1 + rest >= MAXPATH) {
iput(ip);
if (err)
*err = len < 0 ? ELOOP : len == 0 ? ENOENT : ENAMETOOLONG;
return 0;
}
tgt[len] = '\0';
if (rest) {
tgt[len] = '/';
memmove(tgt + len + 1, spath, rest + 1);
}
memmove(safe, tgt, strlen(tgt) + 1);
spath = safe;
if (*spath == '/') {
iput(ip);
ip = mount_cross(iget(SCHFS_DEV, schfs_kroot(SCHFS_DEV)));
}
}
if (nameiparent) {
@ -1810,7 +1857,7 @@ namex_from(struct inode *start, char *path, int nameiparent, char *name)
static struct inode *
namex(char *path, int nameiparent, char *name)
{
return namex_from(myproc()->cwd, path, nameiparent, name);
return namex_from(myproc()->cwd, path, nameiparent, name, 0, 0);
}
struct inode *
@ -1832,50 +1879,23 @@ struct inode *
nameifrom(struct inode *start, char *path)
{
char name[DIRSIZ];
return namex_from(start, path, 0, name);
return namex_from(start, path, 0, name, 0, 0);
}
struct inode *
nameiparentfrom(struct inode *start, char *path, char *name)
{
return namex_from(start, path, 1, name);
return namex_from(start, path, 1, name, 0, 0);
}
// ===========================================================================
// Symlink traversal
//
// Fixed from original:
// - Buffer was char path[MAXPATH+1]. Original called readi(..., MAXPATH)
// then a second readi(... path+len, len, MAXPATH+1) to assert nothing
// more was there. The second read could write 1 byte past the buffer
// if len == MAXPATH.
// - Now: read at most MAXPATH-1 bytes, always NUL-terminate.
// ===========================================================================
// The lookup open(), stat(), chdir() and exec() want: a final symlink is
// followed too, and on failure `*err` says why (ENOENT, ENOTDIR, ELOOP,
// ENAMETOOLONG), so a loop is not reported as a missing file.
struct inode *
divesymlink(struct inode *isym)
nameifollow(struct inode *start, char *path, int *err)
{
struct inode *ip = isym;
char path[MAXPATH];
int deep = 0;
do {
int len = readi(ip, 0, (uint64)path, 0, MAXPATH - 1);
if (len < 0) len = 0;
path[len] = '\0';
iunlockput(ip);
if (++deep > 10)
return 0; // symlink loop
if ((ip = namei(path)) == 0)
return 0;
ilock(ip);
} while (ip->type == T_SYMLINK);
return ip;
char name[DIRSIZ];
return namex_from(start ? start : myproc()->cwd, path, 0, name, 1, err);
}
// ===========================================================================

View file

@ -103,6 +103,8 @@ static const struct global_file globals[] = {
{ "bootdebug", 30 },
// What the slab allocator is holding. See include/kmem.h.
{ "slab", 20 },
// Disks and partitions, in Linux's format. See blkdev_info().
{ "partitions", 21 },
{ "kmsg", 17 },
};
#define NGLOBALS ((int)(sizeof(globals) / sizeof(globals[0])))