diff --git a/cmd/ww/main.c b/cmd/ww/main.c index 233b2736..8f6346d0 100644 --- a/cmd/ww/main.c +++ b/cmd/ww/main.c @@ -373,6 +373,7 @@ source_has_test_decl(const char *path) #define SEP_ROLE_TEST_SUPPORT 1 #define SEP_ROLE_GENERATED_MAIN 2 #define SEP_TEST_SUPPORT_MODULE "__wwtest" +#define SEP_LOAD_INTERNAL -3 /* Package-graph storage grows geometrically. Counts remain signed ints * because they are stable action/context indices throughout the existing @@ -994,6 +995,84 @@ sep_forbidden_command_import(const struct sepgraph *g, int importer, int dep) && strcmp(from->canon, to->canon) == 0); } +static int +sep_internal_parent_count(const char *path, size_t *parents) +{ + const char *p = path; + size_t components = 0; + size_t final = 0; + int found = 0; + while (*p != '\0') { + while (*p == '.') p++; + if (*p == '\0') break; + const char *end = strchr(p, '.'); + size_t n = end != NULL ? (size_t)(end - p) : strlen(p); + if (n == sizeof "internal" - 1 + && memcmp(p, "internal", n) == 0) { + final = components; + found = 1; + } + components++; + if (end == NULL) break; + p = end + 1; + } + if (!found) return 0; + *parents = components - final; + return 1; +} + +static int +sep_internal_import_allowed(const struct seppkg *from, + const char *target_path, const char *target_entry) +{ + size_t parents; + if (!sep_internal_parent_count(target_path, &parents)) return 1; + const char *importer = from->canon; + char *owned = NULL; + if (!from->is_dir) { + const char *slash = strrchr(from->canon, '/'); + if (slash == NULL) return -1; + size_t n = slash == from->canon ? 1 : (size_t)(slash - from->canon); + owned = strndup(from->canon, n); + if (owned == NULL) return sep_fail_nomem(); + importer = owned; + } + size_t boundary = strlen(target_entry); + while (boundary > 1 && target_entry[boundary - 1] == '/') boundary--; + for (size_t i = 0; i < parents; i++) { + while (boundary > 0 && target_entry[boundary - 1] != '/') boundary--; + while (boundary > 1 && target_entry[boundary - 1] == '/') boundary--; + } + char *lexical = boundary == 0 + ? strdup(".") : strndup(target_entry, boundary); + if (lexical == NULL) { + free(owned); + return sep_fail_nomem(); + } + errno = 0; + char *owner = realpath(lexical, NULL); + free(lexical); + if (owner == NULL) { + if (errno == ENOMEM) + sep_fail_nomem(); + else + fprintf(stderr, "ww: cannot canonicalize package %s\n", + target_entry); + free(owned); + return -1; + } + size_t n = strlen(importer); + boundary = strlen(owner); + int allowed = (n == boundary + && memcmp(importer, owner, boundary) == 0) + || (boundary == 1 && owner[0] == '/' && importer[0] == '/') + || (n > boundary && memcmp(importer, owner, boundary) == 0 + && importer[boundary] == '/'); + free(owner); + free(owned); + return allowed; +} + static int sep_command_compiler_marker(const struct sepgraph *g, int pi) { @@ -1718,6 +1797,8 @@ sep_scan_file(struct sepgraph *g, int pi, const char *file, else located = locate_import(searchpath, path_form, ipath, sizeof ipath); + const char *visibility_entry = bound != NULL + ? g->pkg[pi].entry : ipath; if (!located) { const char *dot = strrchr(name, '.'); const char *leaf = dot ? dot + 1 : name; @@ -1751,13 +1832,7 @@ sep_scan_file(struct sepgraph *g, int pi, const char *file, rc = -1; break; } - if (sep_binding_add(bindings, 'D', name, canon) < 0) { - free(canon); - rc = -1; - break; - } int self = strcmp(canon, g->pkg[pi].canon) == 0; - free(canon); if (self && (sep_external_production_name(&g->pkg[pi], name, 1) || (g->pkg[pi].variant == SEP_VARIANT_EXTERNAL @@ -1768,6 +1843,7 @@ sep_scan_file(struct sepgraph *g, int pi, const char *file, ? g->pkg[pi].path : g->pkg[pi].canon; errorf(u->pos, "self-import: package '%s' cannot import itself", owner[0] ? owner : "(root)"); + free(canon); rc = -1; break; } @@ -1775,7 +1851,23 @@ sep_scan_file(struct sepgraph *g, int pi, const char *file, * canonical production action for this directory. Discovery role and * the external product's artifact name never create another action. */ int di = sep_find_or_add(g, name, ipath, 1); - if (di < 0) { rc = -1; break; } + if (di < 0) { free(canon); rc = -1; break; } + int allowed = sep_internal_import_allowed(&g->pkg[pi], + g->pkg[di].path, visibility_entry); + if (allowed < 0) { free(canon); rc = -1; break; } + if (!allowed) { + errorf(u->pos, "use of internal package %s not allowed", + g->pkg[di].path); + free(canon); + rc = SEP_LOAD_INTERNAL; + break; + } + if (sep_binding_add(bindings, 'D', name, canon) < 0) { + free(canon); + rc = -1; + break; + } + free(canon); if (sep_add_dep(g, pi, di) < 0) { rc = -1; break; } } } @@ -1989,7 +2081,7 @@ sep_prepare_pkg_context(struct sepgraph *g, int pi, int context) if (rc < 0) { g->pkg[pi].context_state[context] = 2; g->pkg[pi].failed = 1; - return -1; + return rc; } for (int i = 1; i < g->pkg[pi].ndeps; i++) { int v = g->pkg[pi].deps[i]; @@ -2026,6 +2118,7 @@ sep_load_pkg(struct sepgraph *g, int pi, int context) context = g->support_context; struct seploadframe *frames = NULL; int nframe = 0, framecap = 0; + int result = -1; if (sep_reserve((void **)&frames, &framecap, 1, sizeof *frames) < 0) return -2; @@ -2045,8 +2138,11 @@ sep_load_pkg(struct sepgraph *g, int pi, int context) nframe--; continue; } - if (sep_prepare_pkg_context(g, f->pkg, f->context) < 0) + int prepared = sep_prepare_pkg_context(g, f->pkg, f->context); + if (prepared < 0) { + result = prepared; goto failed; + } f->next_dep = 0; } if (f->pending_dep >= 0) { @@ -2094,7 +2190,7 @@ failed: for (int i = 0; i < nframe; i++) g->pkg[frames[i].pkg].failed = 1; free(frames); - return sep_fatal_allocation ? -2 : -1; + return sep_fatal_allocation ? -2 : result; } static int @@ -3016,6 +3112,7 @@ build_one_sep_impl(const char *src, int entry_is_dir, } int lr = sep_load_pkg(g, root, products[i].context); if (lr == -2) return 1; + if (lr == SEP_LOAD_INTERNAL) return 1; if (lr < 0) { g->pkg[root].failed = 1; continue; @@ -3034,6 +3131,7 @@ build_one_sep_impl(const char *src, int entry_is_dir, if (support >= 0 && support != variant) { int lr = sep_load_pkg(g, support, products[i].context); if (lr == -2) return 1; + if (lr == SEP_LOAD_INTERNAL) return 1; if (lr < 0) g->pkg[variant].failed = 1; } } diff --git a/lib/os/os.ww b/lib/os/os.ww index cf0983e4..92c7022b 100644 --- a/lib/os/os.ww +++ b/lib/os/os.ww @@ -46,6 +46,7 @@ type nr = enum i64 { RMDIR = 84, UNLINK = 87, SYMLINK = 88, + READLINK = 89, SETPGID = 109, GETDENTS64 = 217, NEWFSTATAT = 262, @@ -132,7 +133,7 @@ export def SFD_NONBLOCK: i32 = O_NONBLOCK; export def SFD_CLOEXEC: i32 = O_CLOEXEC; fn kpath(p: str) *u8 = { - if (p.len + 1 >= PATH_MAX) { return nil: *u8; }; // ENAMETOOLONG + if (p.len + 1 > PATH_MAX) { return nil: *u8; }; // ENAMETOOLONG let i: i32 = 0; for (i < p.len) { pathbuf[i] = p[i]; i += 1; }; pathbuf[p.len] = 0u8; @@ -347,7 +348,7 @@ export fn rmdir(path: str) i32 = { export fn rename(oldpath: str, newpath: str) i32 = { let p: *u8 = kpath(oldpath); if (p == nil: *u8) { return -36i32; }; - if (newpath.len + 1 >= PATH_MAX) { return -36i32; }; + if (newpath.len + 1 > PATH_MAX) { return -36i32; }; let i: i32 = 0; for (i < newpath.len) { pathbuf2[i] = newpath[i]; i += 1; }; pathbuf2[newpath.len] = 0u8; @@ -364,13 +365,21 @@ export fn rename(oldpath: str, newpath: str) i32 = { export fn symlink(target: str, path: str) i32 = { let p: *u8 = kpath(target); if (p == nil: *u8) { return -36i32; }; - if (path.len + 1 >= PATH_MAX) { return -36i32; }; + if (path.len + 1 > PATH_MAX) { return -36i32; }; let i: i32 = 0; for (i < path.len) { pathbuf2[i] = path[i]; i += 1; }; pathbuf2[path.len] = 0u8; return syscall2(nr.SYMLINK, p: i64, (&pathbuf2[0]): i64): i32; }; +// readlink — read a symlink target without appending NUL. The caller owns the +// buffer and uses the returned byte count, matching readlink(2). +export fn readlink(path: str, buf: *u8, n: u64) i64 = { + let p: *u8 = kpath(path); + if (p == nil: *u8) { return -36i64; }; + return syscall3(nr.READLINK, p: i64, buf: i64, n: i64); +}; + // mkdirs — recursive mkdir. Creates `path` and any non-existent // parent directories with the given mode. EEXIST is silently // accepted (matches Hare's `errors::exists` skip in os::mkdirs); diff --git a/lib/os/stat_test.ww b/lib/os/stat_test.ww index 469af6f2..8347a7d5 100644 --- a/lib/os/stat_test.ww +++ b/lib/os/stat_test.ww @@ -157,6 +157,25 @@ fn istype(m: os.mode, t: os.mode) bool = { rmtree(&t); }; +@test fn test_readlink_verbatim() void = { + let t: tree; + mktree(&t); + let buf: [16]u8; + let i: i32 = 0; + for (i < 16) { buf[i] = 90u8; i += 1; }; // 'Z' + let n: i64 = os.readlink(t.symlink, &buf[0], 16u64); + assert(!(n != 9i64)); + let want: str = "./regfile"; + i = 0; + for (i < want.len) { + assert(!(buf[i] != want[i])); + i += 1; + }; + let ni: i32 = n: i32; + assert(!(buf[ni] != 90u8)); + rmtree(&t); +}; + @test fn test_fstat_regfile() void = { let t: tree; mktree(&t); @@ -196,7 +215,7 @@ fn istype(m: os.mode, t: os.mode) bool = { }; // kpath copies into a single [PATH_MAX]u8 buffer and reserves one byte -// for the NUL terminator (`p.len + 1 >= PATH_MAX` → reject). The +// for the NUL terminator (`p.len + 1 > PATH_MAX` → reject). The // rejection surfaces as `oserror = -36` (ENAMETOOLONG) on (... | // oserror)-returning wrappers, and as `false` on [[os.exists]] // (Hare's os::exists doc: "true if a node exists at the given path, diff --git a/selfhost/cmd/ww/main.ww b/selfhost/cmd/ww/main.ww index c53f2322..cefe89b9 100644 --- a/selfhost/cmd/ww/main.ww +++ b/selfhost/cmd/ww/main.ww @@ -522,6 +522,7 @@ def SEP_ROLE_NORMAL: i32 = 0; def SEP_ROLE_TEST_SUPPORT: i32 = 1; def SEP_ROLE_GENERATED_MAIN: i32 = 2; def SEP_TEST_SUPPORT_MODULE: str = "__wwtest"; +def SEP_LOAD_INTERNAL: i32 = -3; def SEP_INITIAL_CAP: i32 = 8; def SEP_COUNT_MAX: i32 = 2147483647; @@ -1345,6 +1346,115 @@ fn sepjoinpath(dir: *u8, name: *u8) *u8 = { return sepjoinpathlit(dir, pathstr(name)); }; +fn sepphysicaljoin(dir: *u8, name: str) *u8 = { + if (cstrlen(dir) == 1u64 && dir[0u64] == '/': u8) { + return sepappendlit(dir, name); + }; + return sepjoinpathlit(dir, name); +}; + +fn seppendingpath(target: str, rest: *u8, requiredir: bool) *u8 = { + let restlen: u64 = cstrlen(rest); + let need: u64 = 0u64; + if (!sepaddbytes(&need, target.len: u64) + || (target.len != 0 && (restlen != 0 || requiredir) + && !sepaddbytes(&need, 1u64)) + || !sepaddbytes(&need, restlen) + || !sepaddbytes(&need, 1u64)) { return nil; }; + let out: []u8; + if (!sepmakebytes(need, &out)) { return nil; }; + let off: u64 = strinto(out.ptr, 0u64, target); + if (target.len != 0 && (restlen != 0 || requiredir)) { + off = byteinto(out.ptr, off, '/': u8); + }; + off = cstrinto(out.ptr, off, rest); + cstrseal(out.ptr, off); + return out.ptr; +}; + +fn sepphysicalparent(path: *u8) *u8 = { + let n: u64 = cstrlen(path); + for (n > 1u64 && path[n - 1u64] == '/': u8) { n -= 1u64; }; + for (n > 1u64 && path[n - 1u64] != '/': u8) { n -= 1u64; }; + if (n > 1u64) { n -= 1u64; }; + return sepdupcstr(path, n); +}; + +fn sepcanonicalfile(path: *u8) *u8 = { + if (path[0u64] == 0u8) { return nil; }; + let pending: *u8 = sepdupcstr(path, cstrlen(path)); + if (pending == nil) { return nil; }; + let resolved: *u8 = nil; + if (path[0u64] == '/': u8) { + resolved = sepdupcstr("/".ptr, 1u64); + } else { + let cwd: []u8; + if (!sepmakebytes(os.PATH_MAX: u64, &cwd)) { return nil; }; + let n: i64 = os.getcwd(cwd.ptr, cwd.len: u64); + if (n <= 1i64 || n > cwd.len: i64) { return nil; }; + resolved = sepdupcstr(cwd.ptr, (n - 1i64): u64); + }; + if (resolved == nil) { return nil; }; + let links: i32 = 0; + for (true) { + let total: u64 = cstrlen(pending); + let start: u64 = 0u64; + for (start < total && pending[start] == '/': u8) { start += 1u64; }; + if (start == total) { return resolved; }; + let end: u64 = start; + for (end < total && pending[end] != '/': u8) { end += 1u64; }; + let followed: bool = end < total; + let rest: u64 = end; + for (rest < total && pending[rest] == '/': u8) { rest += 1u64; }; + let requiredir: bool = followed && rest == total; + let name: str; + name.ptr = pending + start; + name.len = (end - start): i32; + if (name.len == 1 && name[0] == '.': u8) { + pending = pending + rest; + continue; + }; + if (name.len == 2 && name[0] == '.': u8 && name[1] == '.': u8) { + resolved = sepphysicalparent(resolved); + if (resolved == nil) { return nil; }; + pending = pending + rest; + continue; + }; + let candidate: *u8 = sepphysicaljoin(resolved, name); + if (candidate == nil) { return nil; }; + let fi: os.filestat; + match (os.lstat(&fi, pathstr(candidate))) { + case void => void; + case let e: os.oserror => return nil; + }; + let typ: u32 = (fi.mode: u32) & 61440u32; + if (typ == os.mode.LINK: u32) { + if (links == 40) { return nil; }; + links += 1; + let target: []u8; + if (!sepmakebytes(os.PATH_MAX: u64, &target)) { return nil; }; + let n: i64 = os.readlink(pathstr(candidate), target.ptr, + os.PATH_MAX: u64); + if (n < 0i64 || n >= os.PATH_MAX: i64) { return nil; }; + let ni: i32 = n: i32; + target[ni] = 0u8; + let targetname: str; + targetname.ptr = target.ptr; + targetname.len = ni; + if (ni > 0 && target[0] == '/': u8) { + resolved = sepdupcstr("/".ptr, 1u64); + if (resolved == nil) { return nil; }; + }; + pending = seppendingpath(targetname, pending + rest, requiredir); + if (pending == nil) { return nil; }; + continue; + }; + if (followed && typ != os.mode.DIR: u32) { return nil; }; + resolved = candidate; + pending = pending + rest; + }; +}; + fn sepdirectoryvariant(variant: i32) bool = { return variant == SEP_VARIANT_PRODUCTION || variant == SEP_VARIANT_SAME_TEST @@ -1422,6 +1532,106 @@ fn sepforbiddencommandimport(g: *sepgraph, importer: i32, dep: i32) bool = { && cstreq(from.canon, to.canon)); }; +fn sepinternalparentcount(path: *u8, parents: *u64) bool = { + let total: u64 = cstrlen(path); + let p: u64 = 0u64; + let components: u64 = 0u64; + let final: u64 = 0u64; + let found: bool = false; + for (p < total) { + for (p < total && path[p] == '.': u8) { p += 1u64; }; + if (p == total) { break; }; + let end: u64 = p; + for (end < total && path[end] != '.': u8) { end += 1u64; }; + if (end - p == "internal".len: u64 + && bytecmp(path + p, end - p, "internal".ptr, + "internal".len: u64) == 0) { + final = components; + found = true; + }; + components += 1u64; + p = end; + }; + if (!found) { return false; }; + *parents = components - final; + return true; +}; + +fn seprawimporterdir(p: *seppkg) *u8 = { + let total: u64 = cstrlen(p.canon); + let slash: u64 = total; + let i: u64 = 0u64; + for (i < total) { + if (p.canon[i] == '/': u8) { slash = i; }; + i += 1u64; + }; + if (slash == total) { return nil; }; + if (slash == 0u64) { slash = 1u64; }; + return sepdupcstr(p.canon, slash); +}; + +fn sepcanonicalinternalowner(path: *u8, parents: u64) *u8 = { + let boundary: u64 = cstrlen(path); + for (boundary > 1u64 && path[boundary - 1u64] == '/': u8) { + boundary -= 1u64; + }; + let pi: u64 = 0u64; + for (pi < parents) { + for (boundary > 0u64 && path[boundary - 1u64] != '/': u8) { + boundary -= 1u64; + }; + for (boundary > 1u64 && path[boundary - 1u64] == '/': u8) { + boundary -= 1u64; + }; + pi += 1u64; + }; + let lexical: *u8 = nil; + if (boundary == 0u64) { + lexical = sepdupcstr(".".ptr, 1u64); + } else { + lexical = sepdupcstr(path, boundary); + }; + if (lexical == nil) { return nil; }; + return canonicaldir(pathstr(lexical)); +}; + +fn sepinternalimportallowed(from: *seppkg, targetpath: *u8, + targetentry: *u8) i32 = { + let parents: u64 = 0u64; + if (!sepinternalparentcount(targetpath, &parents)) { return 1; }; + let importer: *u8 = from.canon; + if (from.isdir == 0) { + importer = seprawimporterdir(from); + if (importer == nil) { + if (!sepfatalallocation) { + cerr("ww: cannot canonicalize package "); + cerr(pathstr(from.entry)); cerr("\n"); + }; + return -1; + }; + }; + let owner: *u8 = sepcanonicalinternalowner(targetentry, parents); + if (owner == nil) { + if (!sepfatalallocation) { + cerr("ww: cannot canonicalize package "); + cerr(pathstr(targetentry)); cerr("\n"); + }; + return -1; + }; + let boundary: u64 = cstrlen(owner); + let n: u64 = cstrlen(importer); + if (n == boundary + && bytecmp(importer, boundary, owner, boundary) == 0) { + return 1; + }; + if (boundary == 1u64 && owner[0u64] == '/': u8 + && importer[0u64] == '/': u8) { return 1; }; + if (n > boundary + && bytecmp(importer, boundary, owner, boundary) == 0 + && importer[boundary] == '/': u8) { return 1; }; + return 0; +}; + fn sepcommandcompilermarker(g: *sepgraph, pi: i32) bool = { return sepcommanddeclaredname(&g.pkg[pi]) && !g.pkg[pi].linkentry; }; @@ -1494,12 +1704,14 @@ fn sepfindoraddvariant(g: *sepgraph, path: *u8, entry: *u8, let canon: *u8 = nil; if (isdir != 0) { canon = canonicaldir(pathstr(entry)); - if (canon == nil) { - if (sepfatalallocation) { return -1; }; - cerr("ww: cannot canonicalize package "); - cerr(pathstr(entry)); cerr("\n"); - return -1; - }; + } else { + canon = sepcanonicalfile(entry); + }; + if (canon == nil) { + if (sepfatalallocation) { return -1; }; + cerr("ww: cannot canonicalize package "); + cerr(pathstr(entry)); cerr("\n"); + return -1; }; let incoming: *u8 = nil; if (isdir != 0 && path[0u64] != 0u8) { @@ -2112,19 +2324,19 @@ fn sepscanfile(g: *sepgraph, pi: i32, file: *u8, searchpath: *u8, }; let externalproduction: bool = false; let ipath: *u8 = nil; + let visibilityentry: *u8 = nil; if (g.pkg[pi].variant == SEP_VARIANT_EXTERNAL && g.pkg[pi].importbase != nil && cstrlen(g.pkg[pi].importbase) == idn && bytecmp(g.pkg[pi].importbase, idn, idp, idn) == 0) { ipath = g.pkg[pi].canon; + visibilityentry = g.pkg[pi].entry; }; if (ipath == nil) { ipath = locateimport(searchpath, idp, idn); + visibilityentry = ipath; }; if (ipath != nil) { - if (!sepbindadd(bindings, 'D': u8, u.usepath, ipath)) { - return -1; - }; let self: bool = os.samefile(pathstr(ipath), pathstr(g.pkg[pi].entry)); if (self && (sepexternalname(&g.pkg[pi], idp, idn, true) @@ -2150,6 +2362,18 @@ fn sepscanfile(g: *sepgraph, pi: i32, file: *u8, searchpath: *u8, // action. Discovery role and product artifact never create another. let di: i32 = sepfindoradd(g, nm.ptr, ipath, 1); if (di < 0) { return -1; }; + let allowed: i32 = sepinternalimportallowed(&g.pkg[pi], + g.pkg[di].path, visibilityentry); + if (allowed < 0) { return -1; }; + if (allowed == 0) { + cerrpos(u.file, u.line, u.col); + cerr(": error: use of internal package "); + cerr(pathstr(g.pkg[di].path)); cerr(" not allowed\n"); + return SEP_LOAD_INTERNAL; + }; + if (!sepbindadd(bindings, 'D': u8, u.usepath, ipath)) { + return -1; + }; if (!sepadddep(g, pi, di)) { return -1; }; } else { let lstart: u64 = 0u64; @@ -2510,7 +2734,8 @@ fn seploadpkg(g: *sepgraph, pi: i32, context: i32) i32 = { nframe -= 1; continue; }; - if (seppreparepkgcontext(g, f.pkg, f.context) < 0) { + let prepared: i32 = seppreparepkgcontext(g, f.pkg, f.context); + if (prepared < 0) { let fi: i32 = 0; for (fi < nframe) { g.pkg[frames[fi].pkg].failed = true; @@ -2519,7 +2744,7 @@ fn seploadpkg(g: *sepgraph, pi: i32, context: i32) i32 = { if (sepfatalallocation) { return sepfinishloadframes(frames, -2); }; - return sepfinishloadframes(frames, -1); + return sepfinishloadframes(frames, prepared); }; f.nextdep = 0; }; @@ -3771,6 +3996,7 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, let loadresult: i32 = seploadpkg(g, root, products[producti].context); if (loadresult == -2) { return 1; }; + if (loadresult == SEP_LOAD_INTERNAL) { return 1; }; if (loadresult < 0) { g.pkg[root].failed = true; producti += 1; @@ -3793,6 +4019,7 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, let loadresult: i32 = seploadpkg(g, support, products[producti].context); if (loadresult == -2) { return 1; }; + if (loadresult == SEP_LOAD_INTERNAL) { return 1; }; if (loadresult < 0) { g.pkg[variant].failed = true; };