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
7 changes: 7 additions & 0 deletions src/fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export interface Backend {
readRange?(path: string, off: number, len: number): Uint8Array | null;
list(path: string): string[] | null;
write?(path: string, data: Uint8Array): boolean;
// the errno code (such as "ENOSPC") of the last failed write to path
writeError?(path: string): string | undefined;
mkdir?(path: string): boolean;
remove?(path: string): boolean;
rename?(from: string, to: string): boolean;
Expand Down Expand Up @@ -139,6 +141,11 @@ export class VFS {
return !!r && r[0].writable && !!r[0].write?.(r[1], data);
}

writeError(path: string): string | undefined {
Comment thread
project516-review-bot[bot] marked this conversation as resolved.
Resolved
const r = this.resolve(path);
return r?.[0].writeError?.(r[1]);
}

mkdir(path: string): boolean {
const r = this.resolve(path);
return !!r && r[0].writable && !!r[0].mkdir?.(r[1]);
Expand Down
30 changes: 26 additions & 4 deletions src/natives/io.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,8 @@ export function fileOf(t: JThread, fd: number): OpenFile {
return f;
}

export function flush(t: JThread, f: OpenFile) {
// Writes the file's buffered bytes; false if the host refused them.
export function flush(t: JThread, f: OpenFile): boolean {
if (f.dirty && f.writable && f.path) {
const data = f.data.slice(0, f.size);
let ok = t.vm.fs.write(f.path, data);
Expand All @@ -201,7 +202,25 @@ export function flush(t: JThread, f: OpenFile) {
t.vm.fs.chmod(f.path, () => mode);
}
if (ok) f.dirty = false;
return ok;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}
return true;
}

const ERRNO: Record<string, string> = {
ENOSPC: "No space left on device", EFBIG: "File too large", EDQUOT: "Disk quota exceeded",
EACCES: "Permission denied", EPERM: "Operation not permitted", EROFS: "Read-only file system",
ENOENT: "No such file or directory",
};

// Where Java learns of a write that failed: HotSpot reports it from write(),
// java-web, which buffers, from the flush, sync or close that writes it out.
export function flushOrThrow(t: JThread, f: OpenFile) {
if (!flush(t, f)) throw writeFailed(t, f.path);
}

export function writeFailed(t: JThread, path: string): PendingThrow {
return new PendingThrow("java/io/IOException", ERRNO[t.vm.fs.writeError(path) ?? ""] ?? "Input/output error");
}

export function newFd(t: JThread): number {
Expand All @@ -213,9 +232,11 @@ export function closeFd(t: JThread, fd: number) {
const tab = table(t);
const f = tab.files.get(fd);
if (f) {
flush(t, f);
const ok = flush(t, f);
tab.files.delete(fd);
if (f.writable && f.path) t.vm.opts.fileClosed?.(f.path);
// the descriptor is closed either way, as with close(2)
if (!ok) throw writeFailed(t, f.path);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}
}

Expand Down Expand Up @@ -308,12 +329,13 @@ export const natives: Natives = {
"java/io/FileDescriptor.sync0()V": (t, [fdObj]) => {
const fd = t.vm.getField(fdObj, "java/io/FileDescriptor", "fd");
const f = table(t).files.get(fd);
if (f) flush(t, f);
if (f) flushOrThrow(t, f);
},
"java/io/FileDescriptor.close0()V": (t, [fdObj]) => {
const fd = t.vm.getField(fdObj, "java/io/FileDescriptor", "fd");
if (fd > 2) closeFd(t, fd);
// closed even when close reports a failed write
t.vm.setField(fdObj, "java/io/FileDescriptor", "fd", -1);
if (fd > 2) closeFd(t, fd);
},

[`${FOS}.open0(Ljava/lang/String;Z)V`]: (t, [self, path, append]) =>
Expand Down
18 changes: 10 additions & 8 deletions src/natives/nio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { JObject } from "../runtime.ts";
import { preCloseSock } from "../sockets.ts";
import { sockRead, sockReadv, sockWrite, sockWritev } from "./sockets.ts";
import { JThread, PendingThrow } from "../thread.ts";
import { closeFd, endsInDot, fileOf, flush, openFile, removePath, renamePath, resolvePath, setMtime, syncPath } from "./io.ts";
import { closeFd, endsInDot, fileOf, flush, flushOrThrow, openFile, removePath, renamePath, resolvePath, setMtime, syncPath, writeFailed } from "./io.ts";
import type { OpenFile } from "./io.ts";
import type { Natives } from "./util.ts";

Expand Down Expand Up @@ -267,15 +267,16 @@ function mappings(t: JThread): Map<number, Mapping> {
}

// Copies mapped bytes from addr on into the file, which stays the size it is.
function writeBack(t: JThread, m: Mapping, base: number, addr: number, len: number) {
// false if the host refused the bytes
function writeBack(t: JThread, m: Mapping, base: number, addr: number, len: number): boolean {
const at = m.pos + addr - base;
const n = Math.min(len, m.f.size - at);
if (n <= 0) return;
if (n <= 0) return true;
const save = m.f.pos;
m.f.pos = at;
m.f.write(t.vm.mem.u8.slice(addr, addr + n));
m.f.pos = save;
flush(t, m.f);
return flush(t, m.f);
}

function readAt(t: JThread, fdObj: JObject, addr: bigint, len: number, pos: number | null): number {
Expand Down Expand Up @@ -338,8 +339,7 @@ Object.assign(natives, {
[`${UFD}writev0(Ljava/io/FileDescriptor;JI)J`]: (t: JThread, [fd, addr, n]: any[]) =>
sockWritev(t, fd, addr, n) ?? vector(t, addr, n, (base, len) => writeAt(t, fd, base, len, null)),
[`${UFD}force0(Ljava/io/FileDescriptor;Z)I`]: (t: JThread, [fd]: any[]) => {
const f = chanFile(t, fd);
flush(t, f);
flushOrThrow(t, chanFile(t, fd));
return 0;
},
[`${UFD}seek0(Ljava/io/FileDescriptor;J)J`]: (t: JThread, [fd, off]: any[]) => {
Expand Down Expand Up @@ -387,7 +387,7 @@ Object.assign(natives, {
const from = Number(addr), to = from + Number(len);
for (const [base, m] of mappings(t)) {
const start = Math.max(from, base), end = Math.min(to, base + m.len);
if (start < end) writeBack(t, m, base, start, end - start);
if (start < end && !writeBack(t, m, base, start, end - start)) throw writeFailed(t, m.f.path);
}
},
"java/nio/MappedMemoryUtils.isLoaded0(JJJ)Z": () => 1,
Expand All @@ -407,7 +407,9 @@ Object.assign(natives, {
"sun/nio/ch/UnixDispatcher.init()V": () => undefined,
"sun/nio/ch/UnixDispatcher.preClose0(Ljava/io/FileDescriptor;)V": (t: JThread, [fd]: any[]) => preCloseSock(t.vm.state, fdNum(t, fd)),
"sun/nio/ch/UnixDispatcher.close0(Ljava/io/FileDescriptor;)V": (t: JThread, [fd]: any[]) => {
closeFd(t, fdNum(t, fd));
const n = fdNum(t, fd);
// closed even when close reports a failed write
t.vm.setField(fd, FD, "fd", -1);
closeFd(t, n);
},
});
8 changes: 7 additions & 1 deletion src/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,20 @@ export class NodeBackend implements Backend {
if (!this.writable) this.lists.set(p, names);
return names;
}
private writeErrors = new Map<string, string>();
write(p: string, data: Uint8Array) {
try {
fs.writeFileSync(this.host(p), data);
this.writeErrors.delete(p);
return true;
} catch {
} catch (e) {
this.writeErrors.set(p, (e as NodeJS.ErrnoException).code ?? "EIO");
return false;
}
}
writeError(p: string) {
return this.writeErrors.get(p);
}
denied(p: string) {
const host = this.host(p);
try {
Expand Down
37 changes: 37 additions & 0 deletions test/full/Full.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;

// Writes past the file size limit (ulimit -f), so the host refuses them. HotSpot
// reports that from write(); java-web, which buffers, from close, sync or force.
public class Full {
public static void main(String[] args) throws Exception {
Path p = Path.of(args[0]);
try {
try (var out = new FileOutputStream(p.toFile())) {
out.write(new byte[8192]);
}
System.out.println("closed");
} catch (IOException e) {
System.out.println("close: " + e.getMessage());
}
try (var out = new FileOutputStream(p.toFile())) {
out.write(new byte[8192]);
out.getFD().sync();
System.out.println("synced");
} catch (IOException e) {
System.out.println("sync: " + e.getMessage());
}
try (var ch = FileChannel.open(p, StandardOpenOption.WRITE)) {
ByteBuffer b = ByteBuffer.allocate(8192);
while (b.hasRemaining()) ch.write(b);
ch.force(true);
System.out.println("forced");
} catch (IOException e) {
System.out.println("force: " + e.getMessage());
}
}
}
16 changes: 16 additions & 0 deletions test/programs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,22 @@ test("-Xmx caps an allocation under the command", () => {
assert.match(max.stdout, /^max 18874368$/m);
});

// ulimit -f makes the host refuse writes past 4 KB; SIGXFSZ ignored, they fail with EFBIG
test("writes the host refuses", { skip: process.platform !== "linux" }, () => {
const classes = join(root, "build", "test-full");
execFileSync(hotspot("javac"), ["-d", classes, join(root, "test", "full", "Full.java")]);
const dir = mkdtempSync(join(tmpdir(), "java-web-full-"));
const limited = (cmd: string, args: string[]) =>
spawnSync("bash", ["-c", 'trap "" XFSZ; ulimit -f 4; exec "$@"', "bash", cmd, ...args, "-cp", classes, "Full", join(dir, "out.bin")], { encoding: "utf8" });
Comment thread
Project516 marked this conversation as resolved.
Dismissed
try {
const ref = limited(hotspot("java"), []);
const got = limited(process.execPath, [join(root, "src", "node.ts")]);
assert.deepEqual({ stdout: got.stdout, code: got.status }, { stdout: ref.stdout, code: ref.status });
} finally {
rmSync(dir, { recursive: true });
}
});

test("stdout closed by its reader", async () => {
const classes = join(root, "build", "test-pipe");
execFileSync(hotspot("javac"), ["-d", classes, join(root, "test", "pipe", "Pipe.java")]);
Expand Down
Loading