Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
git clone -q --filter=blob:none --sparse --depth 1 --branch "jdk-${release%+*}-ga" https://github.com/openjdk/jdk25u.git build/ref/jdk25u
dirs=$(grep -v '^#' test/jtreg/passing.txt | cut -d/ -f1-2 | sort -u | sed 's|^|test/jdk/|')
git -C build/ref/jdk25u sparse-checkout set test/lib test/jdk/lib test/jdk/tools/lib $dirs
node scripts/jtreg/run.ts build/ref/jdk25u --only-passing --jobs 4
node scripts/jtreg/run.ts build/ref/jdk25u --only-passing --jobs 4 --timeout 600
- run: pnpm build
- name: Node entry on the built dist/
run: node --expose-gc --test test/image.test.ts
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@
"dist/jdk/jdk.localedata.txt",
"dist/jdk/legal/"
],
"publishConfig": {
"access": "public"
},
"engines": {
"node": ">=23.6"
},
Expand Down
21 changes: 21 additions & 0 deletions src/fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ export interface Backend {
setMtime?(path: string, ms: number): boolean;
// whether the host refuses to write the file, or to create it when it is missing
denied?(path: string): boolean;
// sets the permission bits to f(current bits)
chmod?(path: string, f: (mode: number) => number): boolean;
// whether the host allows reading (4) and executing (1), as access(2)
access?(path: string, mode: number): boolean;
}

export function normalize(path: string): string {
Expand Down Expand Up @@ -165,6 +169,23 @@ export class VFS {
const r = this.resolve(path);
return !!r && r[0].writable && !!r[0].denied?.(r[1]);
}

// undefined when the backend has no permission bits
chmod(path: string, f: (mode: number) => number): boolean | undefined {
if (DEVICES.has(normalize(path))) return false;
const r = this.resolve(path);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
if (!r || !r[0].chmod) return undefined;
return r[0].writable && r[0].chmod(r[1], f);
}

// access(2) on an existing path: mode is 4 read, 2 write, 1 execute
access(path: string, mode: number): boolean {
if (DEVICES.has(normalize(path))) return true;
const r = this.resolve(path);
if (!r) return false;
if (mode & 2 && (!r[0].writable || r[0].denied?.(r[1]))) return false;
return !(mode & 5) || (r[0].access?.(r[1], mode & 5) ?? true);
}
}

// Writable in-memory tree, for /tmp, /files and strings added by the page.
Expand Down
26 changes: 20 additions & 6 deletions src/natives/io.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,15 @@ export function fileOf(t: JThread, fd: number): OpenFile {

export function flush(t: JThread, f: OpenFile) {
if (f.dirty && f.writable && f.path) {
t.vm.fs.write(f.path, f.data.slice(0, f.size));
f.dirty = false;
const data = f.data.slice(0, f.size);
let ok = t.vm.fs.write(f.path, data);
// an open file keeps its write access when the file's mode changes, as an fd does
let mode = 0;
if (!ok && t.vm.fs.chmod(f.path, (m) => (mode = m) | 0o200)) {
ok = t.vm.fs.write(f.path, data);
t.vm.fs.chmod(f.path, () => mode);
}
if (ok) f.dirty = false;
}
}

Expand Down Expand Up @@ -278,6 +285,11 @@ function attrs(t: JThread, file: JObject): number {
return 1 | (st.dir ? 4 : st.device ? 0 : 2);
}

// a backend without permission bits takes any change to an existing file
function chmod(t: JThread, file: JObject, f: (mode: number) => number): boolean {
return t.vm.fs.chmod(resolvePath(t, pathOf(t, file)), f) ?? attrs(t, file) !== 0;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}

function pathOf(t: JThread, file: JObject): string {
return t.vm.jsString(t.vm.getField(file, "java/io/File", "path"));
}
Expand Down Expand Up @@ -375,16 +387,18 @@ export const natives: Natives = {
[`${UFS}.getBooleanAttributes0(Ljava/io/File;)I`]: (t, [, f]) => attrs(t, f),
[`${UFS}.checkAccess0(Ljava/io/File;I)Z`]: (t, [, f, access]) => {
const abs = resolvePath(t, pathOf(t, f));
if (!t.vm.fs.stat(abs)) return 0;
return (access & 2) === 0 || (t.vm.fs.writable(abs) && !t.vm.fs.denied(abs));
return !!t.vm.fs.stat(abs) && t.vm.fs.access(abs, access);
},
[`${UFS}.getLastModifiedTime0(Ljava/io/File;)J`]: (t, [, f]) => BigInt(Math.floor(t.vm.fs.stat(resolvePath(t, pathOf(t, f)))?.mtime ?? 0)),
[`${UFS}.getLength0(Ljava/io/File;)J`]: (t, [, f]) => {
const abs = resolvePath(t, pathOf(t, f));
syncPath(t, abs);
return BigInt(t.vm.fs.stat(abs)?.size ?? 0);
},
[`${UFS}.setPermission0(Ljava/io/File;IZZ)Z`]: (t, [, f]) => attrs(t, f) !== 0,
[`${UFS}.setPermission0(Ljava/io/File;IZZ)Z`]: (t, [, f, access, enable, ownerOnly]) => {
const bits = ownerOnly ? access << 6 : access * 0o111;
return chmod(t, f, (m) => (enable ? m | bits : m & ~bits));
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
},
[`${UFS}.createFileExclusively0(Ljava/lang/String;)Z`]: (t, [, p]) => {
const abs = resolvePath(t, t.vm.jsString(p));
if (t.vm.fs.stat(abs)) return 0;
Expand All @@ -401,7 +415,7 @@ export const natives: Natives = {
[`${UFS}.rename0(Ljava/io/File;Ljava/io/File;)Z`]: (t, [, a, b]) =>
renamePath(t, resolvePath(t, pathOf(t, a)), resolvePath(t, pathOf(t, b))),
[`${UFS}.setLastModifiedTime0(Ljava/io/File;J)Z`]: (t, [, f, ms]) => setMtime(t, resolvePath(t, pathOf(t, f)), Number(ms)),
[`${UFS}.setReadOnly0(Ljava/io/File;)Z`]: (t, [, f]) => attrs(t, f) !== 0,
[`${UFS}.setReadOnly0(Ljava/io/File;)Z`]: (t, [, f]) => chmod(t, f, (m) => m & ~0o222),
[`${UFS}.getSpace0(Ljava/io/File;I)J`]: () => 0n,
[`${UFS}.getNameMax0(Ljava/lang/String;)J`]: () => 255n,
};
2 changes: 1 addition & 1 deletion src/natives/nio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export const natives: Natives = {
[`${N}access0(JI)I`]: (t, [p, amode]) => {
const abs = path(t, p);
if (!t.vm.fs.stat(abs)) return ENOENT;
return amode & 2 && (!t.vm.fs.writable(abs) || t.vm.fs.denied(abs)) ? EACCES : 0;
return t.vm.fs.access(abs, amode) ? 0 : EACCES;
},
[`${N}open0(JII)I`]: (t, [p, flags]) => {
const abs = path(t, p);
Expand Down
16 changes: 16 additions & 0 deletions src/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,22 @@ export class NodeBackend implements Backend {
return (e as NodeJS.ErrnoException).code === "EACCES" || (e as NodeJS.ErrnoException).code === "EPERM";
}
}
chmod(p: string, f: (mode: number) => number) {
try {
fs.chmodSync(this.host(p), f(fs.statSync(this.host(p)).mode & 0o7777));
return true;
} catch {
return false;
}
}
access(p: string, mode: number) {
try {
fs.accessSync(this.host(p), mode);
return true;
} catch {
return false;
}
}
setMtime(p: string, ms: number) {
try {
fs.utimesSync(this.host(p), fs.statSync(this.host(p)).atime, ms / 1000);
Expand Down
15 changes: 14 additions & 1 deletion test/permissions/Permissions.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
import java.nio.file.Files;
import java.nio.file.Path;

// Writes the host refuses: args are a read-only file and a read-only directory.
// Writes the host refuses: args are a read-only file, a read-only directory and a
// writable file.
public class Permissions {
public static void main(String[] args) throws Exception {
Path file = Path.of(args[0]), dir = Path.of(args[1]);
Expand All @@ -30,5 +31,17 @@ public static void main(String[] args) throws Exception {
System.out.println(e.getClass().getName());
}
System.out.println(Files.isWritable(dir) + " " + Files.exists(dir.resolve("new.txt")));
File w = new File(args[2]);
System.out.println(w.setReadOnly() + " " + w.canWrite());
System.out.println(w.setWritable(true) + " " + w.canWrite());
System.out.println(w.canExecute() + " " + w.setExecutable(true) + " " + w.canExecute());
try (var out = new FileOutputStream(w)) {
out.write('a');
w.setReadOnly();
out.write('b');
}
w.setWritable(true);
System.out.println(Files.readString(w.toPath()));
System.out.println(w.setReadable(false) + " " + w.canRead() + " " + Files.isReadable(w.toPath()));
}
}
9 changes: 6 additions & 3 deletions test/programs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,14 +122,17 @@ test("permission denied", { skip: process.getuid?.() === 0 }, async () => {
const classes = join(root, "build", "test-permissions");
execFileSync(hotspot("javac"), ["-d", classes, join(root, "test", "permissions", "Permissions.java")]);
const dir = mkdtempSync(join(tmpdir(), "java-web-perm-"));
const file = join(dir, "read-only.txt"), locked = join(dir, "locked");
const file = join(dir, "read-only.txt"), locked = join(dir, "locked"), open = join(dir, "open.txt");
writeFileSync(file, "x");
chmodSync(file, 0o444);
mkdirSync(locked, { mode: 0o555 });
try {
const ref = spawnSync(hotspot("java"), ["-cp", classes, "Permissions", file, locked], { encoding: "utf8" });
writeFileSync(open, "x");
const ref = spawnSync(hotspot("java"), ["-cp", classes, "Permissions", file, locked, open], { encoding: "utf8" });
writeFileSync(open, "x");
chmodSync(open, 0o644);
const stdout: Uint8Array[] = [];
const code = await runNode({ classpath: [classes], mainClass: "Permissions", args: [file, locked], stdout: (b) => stdout.push(b) });
const code = await runNode({ classpath: [classes], mainClass: "Permissions", args: [file, locked, open], stdout: (b) => stdout.push(b) });
assert.equal(Buffer.concat(stdout).toString("utf8"), ref.stdout);
assert.equal(code, ref.status);
} finally {
Expand Down
Loading