Skip to content

Commit f86e7cc

Browse files
committed
Report a write the host refuses as an IOException from close, sync and force instead of losing it.
1 parent dc3f078 commit f86e7cc

6 files changed

Lines changed: 88 additions & 7 deletions

File tree

‎src/fs.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ export interface Backend {
2020
readRange?(path: string, off: number, len: number): Uint8Array | null;
2121
list(path: string): string[] | null;
2222
write?(path: string, data: Uint8Array): boolean;
23+
// the errno code (such as "ENOSPC") of the last write that failed
24+
writeError?: string;
2325
mkdir?(path: string): boolean;
2426
remove?(path: string): boolean;
2527
rename?(from: string, to: string): boolean;
@@ -139,6 +141,10 @@ export class VFS {
139141
return !!r && r[0].writable && !!r[0].write?.(r[1], data);
140142
}
141143

144+
writeError(path: string): string | undefined {
145+
return this.resolve(path)?.[0].writeError;
146+
}
147+
142148
mkdir(path: string): boolean {
143149
const r = this.resolve(path);
144150
return !!r && r[0].writable && !!r[0].mkdir?.(r[1]);

‎src/natives/io.ts‎

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,8 @@ export function fileOf(t: JThread, fd: number): OpenFile {
190190
return f;
191191
}
192192

193-
export function flush(t: JThread, f: OpenFile) {
193+
// Writes the file's buffered bytes; false if the host refused them.
194+
export function flush(t: JThread, f: OpenFile): boolean {
194195
if (f.dirty && f.writable && f.path) {
195196
const data = f.data.slice(0, f.size);
196197
let ok = t.vm.fs.write(f.path, data);
@@ -201,7 +202,25 @@ export function flush(t: JThread, f: OpenFile) {
201202
t.vm.fs.chmod(f.path, () => mode);
202203
}
203204
if (ok) f.dirty = false;
205+
return ok;
204206
}
207+
return true;
208+
}
209+
210+
const ERRNO: Record<string, string> = {
211+
ENOSPC: "No space left on device", EFBIG: "File too large", EDQUOT: "Disk quota exceeded",
212+
EACCES: "Permission denied", EPERM: "Operation not permitted", EROFS: "Read-only file system",
213+
ENOENT: "No such file or directory",
214+
};
215+
216+
// Where Java learns of a write that failed: HotSpot reports it from write(),
217+
// java-web, which buffers, from the flush, sync or close that writes it out.
218+
export function flushOrThrow(t: JThread, f: OpenFile) {
219+
if (!flush(t, f)) throw writeFailed(t, f.path);
220+
}
221+
222+
function writeFailed(t: JThread, path: string): PendingThrow {
223+
return new PendingThrow("java/io/IOException", ERRNO[t.vm.fs.writeError(path) ?? ""] ?? "Input/output error");
205224
}
206225

207226
export function newFd(t: JThread): number {
@@ -213,9 +232,11 @@ export function closeFd(t: JThread, fd: number) {
213232
const tab = table(t);
214233
const f = tab.files.get(fd);
215234
if (f) {
216-
flush(t, f);
235+
const ok = flush(t, f);
217236
tab.files.delete(fd);
218237
if (f.writable && f.path) t.vm.opts.fileClosed?.(f.path);
238+
// the descriptor is closed either way, as with close(2)
239+
if (!ok) throw writeFailed(t, f.path);
219240
}
220241
}
221242

@@ -308,7 +329,7 @@ export const natives: Natives = {
308329
"java/io/FileDescriptor.sync0()V": (t, [fdObj]) => {
309330
const fd = t.vm.getField(fdObj, "java/io/FileDescriptor", "fd");
310331
const f = table(t).files.get(fd);
311-
if (f) flush(t, f);
332+
if (f) flushOrThrow(t, f);
312333
},
313334
"java/io/FileDescriptor.close0()V": (t, [fdObj]) => {
314335
const fd = t.vm.getField(fdObj, "java/io/FileDescriptor", "fd");

‎src/natives/nio.ts‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type { JObject } from "../runtime.ts";
55
import { preCloseSock } from "../sockets.ts";
66
import { sockRead, sockReadv, sockWrite, sockWritev } from "./sockets.ts";
77
import { JThread, PendingThrow } from "../thread.ts";
8-
import { closeFd, endsInDot, fileOf, flush, openFile, removePath, renamePath, resolvePath, setMtime, syncPath } from "./io.ts";
8+
import { closeFd, endsInDot, fileOf, flush, flushOrThrow, openFile, removePath, renamePath, resolvePath, setMtime, syncPath } from "./io.ts";
99
import type { OpenFile } from "./io.ts";
1010
import type { Natives } from "./util.ts";
1111

@@ -338,8 +338,7 @@ Object.assign(natives, {
338338
[`${UFD}writev0(Ljava/io/FileDescriptor;JI)J`]: (t: JThread, [fd, addr, n]: any[]) =>
339339
sockWritev(t, fd, addr, n) ?? vector(t, addr, n, (base, len) => writeAt(t, fd, base, len, null)),
340340
[`${UFD}force0(Ljava/io/FileDescriptor;Z)I`]: (t: JThread, [fd]: any[]) => {
341-
const f = chanFile(t, fd);
342-
flush(t, f);
341+
flushOrThrow(t, chanFile(t, fd));
343342
return 0;
344343
},
345344
[`${UFD}seek0(Ljava/io/FileDescriptor;J)J`]: (t: JThread, [fd, off]: any[]) => {

‎src/node.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,13 @@ export class NodeBackend implements Backend {
6868
if (!this.writable) this.lists.set(p, names);
6969
return names;
7070
}
71+
writeError?: string;
7172
write(p: string, data: Uint8Array) {
7273
try {
7374
fs.writeFileSync(this.host(p), data);
7475
return true;
75-
} catch {
76+
} catch (e) {
77+
this.writeError = (e as NodeJS.ErrnoException).code;
7678
return false;
7779
}
7880
}

‎test/full/Full.java‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import java.io.FileOutputStream;
2+
import java.io.IOException;
3+
import java.nio.ByteBuffer;
4+
import java.nio.channels.FileChannel;
5+
import java.nio.file.Path;
6+
import java.nio.file.StandardOpenOption;
7+
8+
// Writes past the file size limit (ulimit -f), so the host refuses them. HotSpot
9+
// reports that from write(); java-web, which buffers, from close, sync or force.
10+
public class Full {
11+
public static void main(String[] args) throws Exception {
12+
Path p = Path.of(args[0]);
13+
try {
14+
try (var out = new FileOutputStream(p.toFile())) {
15+
out.write(new byte[8192]);
16+
}
17+
System.out.println("closed");
18+
} catch (IOException e) {
19+
System.out.println("close: " + e.getMessage());
20+
}
21+
try (var out = new FileOutputStream(p.toFile())) {
22+
out.write(new byte[8192]);
23+
out.getFD().sync();
24+
System.out.println("synced");
25+
} catch (IOException e) {
26+
System.out.println("sync: " + e.getMessage());
27+
}
28+
try (var ch = FileChannel.open(p, StandardOpenOption.WRITE)) {
29+
ByteBuffer b = ByteBuffer.allocate(8192);
30+
while (b.hasRemaining()) ch.write(b);
31+
ch.force(true);
32+
System.out.println("forced");
33+
} catch (IOException e) {
34+
System.out.println("force: " + e.getMessage());
35+
}
36+
}
37+
}

‎test/programs.test.ts‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,22 @@ test("-Xmx caps an allocation under the command", () => {
7676
assert.match(max.stdout, /^max 18874368$/m);
7777
});
7878

79+
// ulimit -f makes the host refuse writes past 4 KB; SIGXFSZ ignored, they fail with EFBIG
80+
test("writes the host refuses", { skip: process.platform !== "linux" }, () => {
81+
const classes = join(root, "build", "test-full");
82+
execFileSync(hotspot("javac"), ["-d", classes, join(root, "test", "full", "Full.java")]);
83+
const dir = mkdtempSync(join(tmpdir(), "java-web-full-"));
84+
const limited = (cmd: string, args: string[]) =>
85+
spawnSync("bash", ["-c", 'trap "" XFSZ; ulimit -f 4; exec "$@"', "bash", cmd, ...args, "-cp", classes, "Full", join(dir, "out.bin")], { encoding: "utf8" });
86+
try {
87+
const ref = limited(hotspot("java"), []);
88+
const got = limited(process.execPath, [join(root, "src", "node.ts")]);
89+
assert.deepEqual({ stdout: got.stdout, code: got.status }, { stdout: ref.stdout, code: ref.status });
90+
} finally {
91+
rmSync(dir, { recursive: true });
92+
}
93+
});
94+
7995
test("stdout closed by its reader", async () => {
8096
const classes = join(root, "build", "test-pipe");
8197
execFileSync(hotspot("javac"), ["-d", classes, join(root, "test", "pipe", "Pipe.java")]);

0 commit comments

Comments
 (0)