ww: enforce internal package visibility

This commit is contained in:
2026-08-13 15:37:51 +09:00
parent af0f56dfe5
commit 9a5dbac84f
4 changed files with 378 additions and 25 deletions

View File

@@ -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;
}
}

View File

@@ -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);

View File

@@ -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,

View File

@@ -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;
};