操作系统与网络:有界实验入口
只依赖 Python 3 和 JDK 17,无下载依赖、无外部服务,不更改系统 hosts、安全文件、shell 或配额。实现见本页 Java 实验源码、OS 资源探针与 U15 连接探针,公开入口为仓库命令 python3 labs/os-network/run.py(运行需本地检出仓库)。这是教学协议/预算/资源探针,不是完整服务器、性能基准或生产压测工具。
运行与退出
仓库根执行(macOS 自动用 /usr/libexec/java_home -v17 定位;其他系统仅给本命令传已有 JDK 的 JAVA_HOME。注意:本机 /usr/libexec/java_home -v17 当前解析到 GraalVM 17.0.12+8.1,其 JVMCI 额外占用文件描述符;fd 模式要求非 JVMCI 的 JDK 17(如 Oracle/OpenJDK jdk-17.jdk),建议显式 JAVA_HOME=... python3 labs/os-network/run.py fd)。U15 的 tls 模式会用同目录 JAVA_HOME 的 keytool 在唯一临时目录内生成教学 CA/证书(约 10 秒),全程不触系统钥匙串与 cacerts:
python3 labs/os-network/run.py all
python3 labs/os-network/run.py budget
python3 labs/os-network/run.py tcp
python3 labs/os-network/run.py selector
python3 labs/os-network/run.py http
python3 labs/os-network/run.py dns
python3 labs/os-network/run.py tcpstates
python3 labs/os-network/run.py tls
python3 labs/os-network/run.py reuse
python3 labs/os-network/run.py dnschain
python3 labs/os-network/run.py thread
python3 labs/os-network/run.py signal
python3 labs/os-network/run.py fd
python3 labs/os-network/run.py mem
python3 labs/os-network/run.py perm
python3 labs/os-network/run.py diskrunner 核对 JDK 主版本 17,并打印实际构建;javac 最长 30 秒,每个 Java 场景最长 15–90 秒(thread 到 90 秒、mem 到 60 秒,含探针驻留),异常或断言失败非零退出。所有类文件与临时产物在 TemporaryDirectory 中生成,成功/失败/超时都会清理;fd 模式用 resource.setrlimit 只对子进程降低 RLIMIT_NOFILE,perm 模式的 umask 只作用于子进程,不触碰用户 shell 或全局配置。
所有实际 socket 绑定/连接 127.0.0.1、随机端口 0(perm 的 bind80 探测固定 80 端口、只 bind 不 listen 不收发);连接超时 2 秒、socket 读/accept 超时 3 秒,Selector 循环有单调时钟截止,Future/闩锁等待有上限,资源以 try-with-resources/finally 关闭。仅使用少量字节,无外部负载和无界轮询。不要把这里改成生产地址运行。
输入、断言与证据范围
| 模式 | 输入与预期断言 | 教学回查 |
|---|---|---|
| budget | 假设各线程共享同一有效 affinity:子 2/父 1/affinity 4 → 1;本层 max/父 1.5 → 1.5;双 max/affinity 1 → 1;0.5 核仍是 0.5;4 个非法控制字符串拒绝 | CPU 预算 |
| tcp | “甲”“beta”逐字节、合帧与真实 loopback 得到相同两帧;空帧交付一次;-1/1025 长度拒绝;半头/声明100仅20字节 EOF 均截断;half-close 后收到42和干净EOF | 分帧/EOF |
| selector | 普通 select 保留 selected key 至显式移除;Consumer 不新增它;H=4/L=2/global=4,跨连接超额拒绝、暂停/恢复 OP_READ,pending 才 OP_WRITE;每轮 1 字节仍顺序正确;旧代际拒绝、取消释放一次且注销,关闭全部资源 | 选择器与背压 |
| http | HTTP/1.1 POST 完整读到 x 后记1并关闭,不发响应:客户端 IOException 而服务端仍记1;慢 body 先给200和A、闩锁阻止B,完整体等待超时,放行后只剩B和EOF;worker/body/socket 全部关闭 | HTTP 完成边界 |
| dns | 三个独立 JVM:正缓存-1保留旧值;负缓存-1保留失败;正负0立即读更新。仅私有hosts A=127.0.0.1→B=192.0.2.1,新解析B而旧socket仍能传输到A;同名系统属性600不覆盖安全属性0 | 名称与连接 |
| tcpstates(U15) | 环回五断言:半关闭后仍收回复+EOF;linger0 关闭后对端下一次 read 抛 reset(非 EOF);主动关闭方端口 netstat 呈 TIME_WAIT 且无 REUSEADDR 重绑定被拒(EADDRINUSE)/设后成功;服务端收 FIN 不 close 计出 ≥3 条 CLOSE_WAIT;对端 SIGKILL(有未读数据)后 write/read 得 Broken pipe/Connection reset。SO_KEEPALIVE 与 TCP_KEEPIDLE=600 仅验证选项可设,netstat/sysctl 为 macOS 只读观察 | 连接状态机 |
| tls(U15) | 临时 keytool CA+SAN 证书(私有 hosts 映射 good.test/bad.test→127.0.0.1):自有 truststore→200 TLSv1.3;默认信任库→PKIX path building failed;CA 签名过期证书(-startdate -400d)→validity check failed;SAN 不符→No subject alternative;debug 档抓 Produced ClientHello/Consuming ServerHello/Negotiated TLSv1.3 | TLS 握手 |
| reuse(U15) | 服务端循环读多请求:3 请求 1 次 accept(keep-alive 复用);手动 Connection: close 被 IllegalArgumentException 拒(受限头);服务端响应 Connection: close 后下一请求新建连接(accepts 1→2);不响应服务+700ms 请求预算→HttpTimeoutException(实测 ~700ms);关闭端口→ConnectException ~3ms | 复用与超时 |
| dnschain(U15) | 私有 hosts:chain.test→127.0.0.1 命中、missing.test→UnknownHostException(负TTL=0);hosts 文件缺失→每次查名失败且消息点名文件;只读观察 scutil --dns resolver#1 与 dscacheutil localhost(macOS) | 解析链路 |
| thread(U14) | 目标 5000 驻留线程直到 OOME:macOS 实测 4069 处 EAGAIN(kern.num_taskthreads=4096);断言 OOME_AT>=1000,阈值是单机观察值;未触限的宿主机输出 OBSERVE | 线程创建受限 |
| signal(U14) | 四相位:TERM→hook 运行+退出码 143;KILL→无 hook+-9;QUIT→Full thread dump 且进程存活;-Xrs+TERM→无 hook+-15 | 信号终止与 JVM 处理 |
| fd(U14) | 子进程 RLIMIT_NOFILE=80:不关流在 EMFILE 前成功打开 ~76 次(观察值);System.gc() 后重试 0 次成功(GC 不回收 FD);try-with-resources 200/200 成功。需非 JVMCI JDK | nofile 泄漏恢复 |
| mem(U14) | -Xmx512m 分配 400×256KB:RSS 增量>=50MB、RSS>heapUsed(观察值);jcmd NMT summary 打印 total/heap committed(RSS≠堆) | RSS 对账 |
| perm(U14) | 子进程 umask 027:createFile 请求 rwxrwxrwx 实得 rwxr-x---(umask 掩蔽请求值);chmod 000 拒属主读;bind80 记录平台观察(macOS ok / Linux 非 root 预期拒绝,推导) | umask 与低端口 |
| disk(U14) | 64MB 文件删除后 fd 仍开:可用空间不变、数据仍可读(释放时点是观察值);write+force 10 次打印 fsync 时长;iostat 采样一行 | deleted-but-open 与 fsync |
DNS 的文档地址 192.0.2.1 只作为解析结果,从不连接。-Djdk.net.hosts.file 是目标 JDK 的私有名称服务入口,不修改 /etc/hosts、不发送真实 DNS 查询。正负 -1 与 0 为确定性边界测试;未靠 sleep 等待有限 TTL 到期。U15 的 tcpstates/tls/dnschain 的所有实际监听/连接同样只在 127.0.0.1;good.test/bad.test/chain.test 均为私有 hosts 文件里的文档名,不出 JVM。
Selector 每轮 1 字节是应用额度限制,不声称内核发送缓冲被打满。队列/代际是单 loop 教学状态,未创建完整 worker 框架,也未测跨线程竞态/租户公平性。TCP 的分片断言由解析器直接输入保证,不声称 write 与网络包一一对应。HTTP 的服务计数是进程内提交标记,不是持久数据库;慢体 100 ms 等待只验证 B 尚未提供,不能当作吞吐或 request timeout 覆盖范围测试。U14 六模式(thread/signal/fd/mem/perm/disk)在本机(macOS 26.7.1 x86_64 + Oracle JDK 17.0.12+8-LTS-286)实测通过,证据记录含八次真实红绿;Linux 专属行为(pids.max、cgroup io/memory 控制器、CAP_*、OOMKilled)未实跑、按文档推导。
U15 四模式(tcpstates/tls/reuse/dnschain)实测通过,证据记录含五次真实红绿;重传/RTO、backlog 溢出、真实递归 DNS、撤销检查(OCSP/CRL)、mTLS、0-RTT、HTTP/2、真实连接超时路径(TEST-NET 在本网络实测建连成功,不可用作黑洞)均未实跑,按 RFC 9293/6298/8446、tcp(7)、resolv.conf(5) 与 jdk-17.0.12+7 源码推导并在正文逐项标注。TIME_WAIT 存留时长(~32.6s)、CLOSE_WAIT 计数、netstat/sysctl 数值为 macOS 单机观察值。
本次实际运行与未运行
本次已通过各模式公开命令:macOS 26.7.1,Oracle JDK 17.0.12+8-LTS-286(JAVA_HOME 显式指定;T12 时代记录的 Liberica 17.0.19 为当时环境);Selector 打印 KQueueSelectorProvider。教材原码固定 jdk-17.0.12+7,运行构建不同,不能将此运行说成在源码标签构建上实测。
实际完成标记如下(路径与临时目录不固定):
PASS budget: parent=1.0 unlimited-child=1.5 affinity=1.0 fractional=0.5 invalid=4 (model, not Linux)
PASS tcp: split/coalesced=UTF-8,2 frames; empty=1; invalid=2; truncated=2; half-close/reply/EOF
PASS selector: provider=KQueueSelectorProvider ordinary/consumer; high=4 low=2 global=4; partial-write/order; stale-result/cancel
PASS selector cleanup: all channels/selector closed
PASS http: client=IOException server-commit=1; headers/first-byte before full-body; AB then EOF; workers/body/sockets closed
PASS dns-positive: security-positive=-1 security-negative=0 system-same-name=600 (private hosts; no real DNS)
PASS dns-negative: security-positive=0 security-negative=-1 system-same-name=600 (private hosts; no real DNS)
PASS dns-zero: security-positive=0 security-negative=0 system-same-name=600 (private hosts; no real DNS)
PASS thread: native thread creation hit a real task-level limit at 4069 (kern.num_taskthreads=4096)
PASS signal: TERM=hook+143, KILL=no-hook+-9, QUIT=dump+survive, XRS+TERM=no-hook+-15
PASS fd: limit=80 -> EMFILE after 76 opens, GC frees none, closing fixes it
PASS mem: heapUsed=133MB RSS 31064->173936KB NMT total=339295KB heap=232448KB (RSS!=heap; single-host observation)
PASS perm: umask masks createFile request, 000 denies owner read, bind80=ok (platform-dependent; Linux derivation in article)
PASS disk: deleted-while-open keeps space+data; fsync timings are observations above
PASS tcpstates: FIN=reply+EOF; linger0=reset-on-next-read; active-closer=TIME_WAIT+rebind rules; un-closed FIN=CLOSE_WAIT x3; peer-SIGKILL=reset; keepalive=options only
PASS tls-pos: status=200 protocol=TLSv1.3 cipher=TLS_AES_256_GCM_SHA384
PASS tls-trust: ex=SSLHandshakeException msg=PKIX path building failed: ... unable to find valid certification path to requested target
PASS tls-expire: ex=SSLHandshakeException msg=PKIX path validation failed: ... validity check failed
PASS tls-san: ex=SSLHandshakeException msg=No subject alternative DNS name matching bad.test found.
PASS tls-debug: status=200 protocol=TLSv1.3 cipher=TLS_AES_256_GCM_SHA384
PASS tls: lab CA trust=200; default trust=PKIX path building failed; expired=validity check failed; SAN mismatch=No subject alternative; debug shows handshake phases
PASS reuse-keepalive: 3 requests=1 accept; Connection header rejected; server close forces reconnect
PASS reuse-reqtimeout: no-response request=HttpTimeoutException near budget; closed port=fast refusal
PASS reuse: keepalive reuse, restricted header, server-driven close, request timeout, fast refusal
PASS dns-chain: hosts-file wins over resolver; missing name fails; OS resolver config observed (read-only)
PASS dns-hostsmissing: nonexistent hosts file behaves as empty file -> UnknownHostException
PASS dnschain: hosts precedence, missing-name and missing-file failures, OS resolver observation
PASS runner: temporary classes/hosts removed; no external service or system configuration used未运行:真实 Linux cgroup(cpu/memory/io/pids 控制器与节流/OOM/记账)、容器内 AvailableProcessors/MaxRAMPercentage 感知、OOMKilled 退出链、CAP_NET_BIND_SERVICE 与非 root 低端口拒绝、EEVDF/CFS 实际调度行为、PSI /proc/pressure、权威/递归 DNS 与有限 TTL 传播、DNSSEC、内核 TCP 重传/RTO 与 backlog 溢出(环回无丢包)、真实连接超时(TEST-NET-1 在本网络实测建连成功,不可用作黑洞)、TLS 撤销检查(OCSP/CRL)、mTLS、0-RTT、HTTP/2、多连接池压测、内核拥塞与吞吐。budget 是纯算例,thread/signal/fd/mem/perm/disk 与 U15 四模式是 macOS 侧可观测对等物,不能替代 Linux /proc、cgroup 与容器运行时实测。U14 时代 TLS 隔离证书的验证步骤已在 U15 tls 模式落实为实跑。保留已有 JVM 故障实验,本票不修改它们,也不沿用其旧审计作为本轮执行结果。
Java 实验源码
站点直接嵌入同目录 OsNetworkLab.java 与 OsResourceProbe.java,避免产生构建输出中并不存在的源码下载链接;维护源文件只有一份,以下是实验代码而非 JDK 原码。
import java.io.*;
import java.net.*;
import java.net.http.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.security.Security;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.*;
/** Teaching probes, not a production server. All actual sockets use IPv4 loopback. */
public class OsNetworkLab {
static void check(boolean condition, String message) {
if (!condition) throw new AssertionError(message);
}
static double budget(int affinity, String... levels) {
if (affinity <= 0) throw new IllegalArgumentException("empty affinity");
double bound = affinity;
for (String level : levels) {
String[] fields = level.trim().split("\\s+");
if (fields.length != 2) throw new IllegalArgumentException("expected quota period");
long period = Long.parseLong(fields[1]);
if (period <= 0) throw new IllegalArgumentException("invalid period");
if (!fields[0].equals("max")) {
long quota = Long.parseLong(fields[0]);
if (quota <= 0) throw new IllegalArgumentException("invalid quota");
bound = Math.min(bound, (double) quota / period);
}
}
return bound;
}
static void budgetTest() {
check(budget(4, "200000 100000", "100000 100000") == 1.0, "parent cap");
check(budget(4, "max 100000", "150000 100000") == 1.5, "unlimited child");
check(budget(1, "max 100000", "max 100000") == 1.0, "affinity cap");
check(budget(8, "50000 100000") == 0.5, "fractional quota is not one CPU");
for (String invalid : List.of("100 0", "-1 100", "broken", "max -1")) {
try {
budget(4, invalid);
throw new AssertionError("accepted invalid cpu.max: " + invalid);
} catch (IllegalArgumentException expected) { }
}
System.out.println("PASS budget: parent=1.0 unlimited-child=1.5 affinity=1.0 fractional=0.5 invalid=4 (model, not Linux)");
}
static InetAddress loopback() throws UnknownHostException {
return InetAddress.getByAddress(new byte[]{127, 0, 0, 1});
}
static final class Pair implements AutoCloseable {
final ServerSocketChannel listener = ServerSocketChannel.open();
final SocketChannel client = SocketChannel.open();
SocketChannel server;
Pair() throws IOException {
try {
listener.bind(new InetSocketAddress(loopback(), 0));
listener.socket().setSoTimeout(3000);
client.socket().connect(listener.getLocalAddress(), 2000);
server = listener.socket().accept().getChannel();
client.socket().setSoTimeout(3000);
server.socket().setSoTimeout(3000);
} catch (IOException | RuntimeException e) {
close();
throw e;
}
}
public void close() throws IOException {
try { if (server != null) server.close(); }
finally { try { client.close(); } finally { listener.close(); } }
}
}
static byte[] frame(String value) {
byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
return ByteBuffer.allocate(4 + bytes.length).putInt(bytes.length).put(bytes).array();
}
static final class Framer {
final List<String> messages = new ArrayList<>();
final ByteBuffer header = ByteBuffer.allocate(4);
ByteBuffer payload;
void feed(byte[] input, int length) throws IOException {
for (int i = 0; i < length; i++) {
if (payload == null) {
header.put(input[i]);
if (header.hasRemaining()) continue;
int size = header.flip().getInt();
header.clear();
if (size < 0 || size > 1024) throw new IOException("unsafe frame length: " + size);
payload = ByteBuffer.allocate(size);
} else {
payload.put(input[i]);
}
if (!payload.hasRemaining()) {
messages.add(new String(payload.array(), StandardCharsets.UTF_8));
payload = null;
}
}
}
void eof() throws EOFException {
if (payload != null || header.position() != 0) throw new EOFException("truncated frame");
}
}
static void tcpTest() throws Exception {
byte[] first = frame("甲"), second = frame("beta");
byte[] together = ByteBuffer.allocate(first.length + second.length).put(first).put(second).array();
Framer split = new Framer(), joined = new Framer();
for (byte value : together) split.feed(new byte[]{value}, 1);
joined.feed(together, together.length);
split.eof(); joined.eof();
check(split.messages.equals(List.of("甲", "beta")), "one byte at a time");
check(joined.messages.equals(split.messages), "coalesced frames");
Framer empty = new Framer();
empty.feed(frame(""), 4); empty.eof();
check(empty.messages.equals(List.of("")), "empty frame progresses");
for (int length : new int[]{-1, 1025}) {
try {
new Framer().feed(ByteBuffer.allocate(4).putInt(length).array(), 4);
throw new AssertionError("accepted unsafe length");
} catch (IOException expected) { }
}
try (Pair pair = new Pair()) {
pair.client.socket().getOutputStream().write(together);
pair.client.shutdownOutput();
Framer received = new Framer();
byte[] chunk = new byte[2];
int n;
while ((n = pair.server.socket().getInputStream().read(chunk)) != -1) received.feed(chunk, n);
received.eof();
check(received.messages.equals(List.of("甲", "beta")), "loopback frames in order");
pair.server.socket().getOutputStream().write(42);
pair.server.shutdownOutput();
check(pair.client.socket().getInputStream().read() == 42, "half-close still reads reply");
check(pair.client.socket().getInputStream().read() == -1, "clean EOF");
}
for (byte[] truncated : List.of(new byte[]{0, 0}, ByteBuffer.allocate(24).putInt(100).array())) {
try (Pair pair = new Pair()) {
pair.client.socket().getOutputStream().write(truncated);
pair.client.shutdownOutput();
Framer received = new Framer();
byte[] chunk = new byte[3];
int n;
while ((n = pair.server.socket().getInputStream().read(chunk)) != -1) received.feed(chunk, n);
try { received.eof(); throw new AssertionError("accepted truncated stream"); }
catch (EOFException expected) { }
}
}
System.out.println("PASS tcp: split/coalesced=UTF-8,2 frames; empty=1; invalid=2; truncated=2; half-close/reply/EOF");
}
static final class BytesBudget { int used; final int cap = 4; }
/** Loop-owned teaching state: no worker threads mutate buffers or interestOps. */
static final class Outbound {
final SelectionKey key;
final BytesBudget global;
final ArrayDeque<ByteBuffer> queue = new ArrayDeque<>();
int pending, generation = 1;
boolean closed, paused;
Outbound(SelectionKey key, BytesBudget global) { this.key = key; this.global = global; }
boolean offer(int resultGeneration, byte[] bytes) {
if (closed || resultGeneration != generation || !key.isValid()) return false;
if (bytes.length == 0 || bytes.length > 4 - pending || bytes.length > global.cap - global.used) return false;
queue.add(ByteBuffer.wrap(bytes.clone()));
pending += bytes.length;
global.used += bytes.length;
updateInterest();
return true;
}
void updateInterest() {
if (pending >= 4) paused = true;
else if (pending <= 2) paused = false;
key.interestOps((paused ? 0 : SelectionKey.OP_READ) | (pending > 0 ? SelectionKey.OP_WRITE : 0));
}
void drainOneByte() throws IOException {
ByteBuffer buffer = queue.peek();
if (buffer == null) return;
int originalLimit = buffer.limit();
int written;
try {
buffer.limit(buffer.position() + 1); // Deliberate per-turn budget, not kernel saturation.
written = ((SocketChannel) key.channel()).write(buffer);
} finally { buffer.limit(originalLimit); }
pending -= written;
global.used -= written;
if (!buffer.hasRemaining()) queue.remove();
updateInterest();
}
void close() throws IOException {
if (closed) return;
closed = true;
generation++;
global.used -= pending;
pending = 0;
queue.clear();
key.cancel();
key.channel().close();
}
}
static void awaitSelected(Selector selector) throws IOException {
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(3);
while (selector.selectedKeys().isEmpty() && System.nanoTime() < deadline) selector.select(100);
check(!selector.selectedKeys().isEmpty(), "selection deadline");
}
static void selectorTest() throws Exception {
Pair pair = new Pair();
Selector selector = Selector.open();
try (pair; selector) {
pair.server.configureBlocking(false);
pair.client.configureBlocking(false);
SelectionKey key = pair.server.register(selector, SelectionKey.OP_READ);
pair.client.write(ByteBuffer.wrap(new byte[]{7, 8}));
awaitSelected(selector);
check(selector.selectedKeys().contains(key), "ordinary path keeps selected key");
check(pair.server.read(ByteBuffer.allocate(1)) == 1, "consume one byte");
check(selector.selectedKeys().contains(key), "read does not remove selected key");
selector.selectedKeys().remove(key);
int[] calls = {0};
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(3);
while (calls[0] == 0 && System.nanoTime() < deadline) {
selector.select(ready -> {
try {
if (ready.isReadable()) {
check(pair.server.read(ByteBuffer.allocate(1)) == 1, "consumer reads remaining byte");
calls[0]++;
}
} catch (IOException e) { throw new UncheckedIOException(e); }
}, 100);
}
check(calls[0] > 0 && selector.selectedKeys().isEmpty(), "consumer path does not add selected key");
BytesBudget global = new BytesBudget();
Outbound output = new Outbound(key, global);
SelectionKey otherKey = pair.client.register(selector, 0);
Outbound other = new Outbound(otherKey, global);
check(output.offer(1, new byte[]{10, 20, 30, 40}), "admit within budget");
check(!other.offer(1, new byte[]{50}), "global cap across connections");
check(output.pending == 4 && global.used == 4, "reserve exactly four bytes");
check((key.interestOps() & SelectionKey.OP_READ) == 0, "pause read at high water");
check((key.interestOps() & SelectionKey.OP_WRITE) != 0, "pending output needs write interest");
deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(3);
boolean resumed = false;
while (output.pending > 0 && System.nanoTime() < deadline) {
awaitSelected(selector);
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey ready = iterator.next(); iterator.remove();
if (ready == key && ready.isWritable()) output.drainOneByte();
}
if (output.pending == 2) {
check((key.interestOps() & SelectionKey.OP_READ) != 0, "resume read at low water");
resumed = true;
}
}
check(output.pending == 0 && global.used == 0 && resumed, "bounded drain and accounting");
check((key.interestOps() & SelectionKey.OP_WRITE) == 0, "no empty write interest");
pair.server.shutdownOutput();
otherKey.cancel(); selector.selectNow();
pair.client.configureBlocking(true);
check(Arrays.equals(pair.client.socket().getInputStream().readNBytes(4), new byte[]{10, 20, 30, 40}), "partial writes preserve order");
check(pair.client.socket().getInputStream().read() == -1, "drained EOF");
output.generation++; // A new request epoch on a still-open context.
check(!output.offer(1, new byte[]{9}) && global.used == 0, "stale result rejected while context remains open");
check(output.offer(2, new byte[]{1, 2}), "reserve before cancellation");
output.close(); output.close();
check(!output.offer(2, new byte[]{9}) && global.used == 0, "closed result rejected, release once");
selector.selectNow();
check(!key.isValid() && !selector.keys().contains(key), "cancelled registration removed");
System.out.println("PASS selector: provider=" + selector.provider().getClass().getSimpleName()
+ " ordinary/consumer; high=4 low=2 global=4; partial-write/order; stale-result/cancel");
}
check(!pair.client.isOpen() && !pair.server.isOpen() && !pair.listener.isOpen() && !selector.isOpen(), "resources closed");
System.out.println("PASS selector cleanup: all channels/selector closed");
}
static byte[] readRequest(Socket socket) throws IOException {
InputStream input = socket.getInputStream();
ByteArrayOutputStream header = new ByteArrayOutputStream();
int suffix = 0;
while (header.size() < 8192 && suffix != 0x0d0a0d0a) {
int value = input.read();
if (value < 0) throw new EOFException("request header truncated");
header.write(value);
suffix = (suffix << 8) | value;
}
if (suffix != 0x0d0a0d0a) throw new IOException("oversize header");
int size = 0;
for (String line : header.toString(StandardCharsets.US_ASCII).split("\r\n")) {
if (line.toLowerCase(Locale.ROOT).startsWith("content-length:")) size = Integer.parseInt(line.substring(15).trim());
}
if (size < 0 || size > 1024) throw new IOException("request body bound");
byte[] body = input.readNBytes(size);
if (body.length != size) throw new EOFException("request body truncated");
return body;
}
static ServerSocket httpListener() throws IOException {
ServerSocket server = new ServerSocket();
try {
server.bind(new InetSocketAddress(loopback(), 0));
server.setSoTimeout(3000);
return server;
} catch (IOException e) { server.close(); throw e; }
}
static void httpTest() throws Exception {
ExecutorService workers = Executors.newFixedThreadPool(2);
CountDownLatch releaseBody = new CountDownLatch(1);
HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1)
.proxy(ProxySelector.of(null)).connectTimeout(Duration.ofSeconds(2)).build();
try {
try (ServerSocket server = httpListener()) {
var commits = new java.util.concurrent.atomic.AtomicInteger();
Future<Void> committed = workers.submit(() -> {
try (Socket socket = server.accept()) {
socket.setSoTimeout(3000);
check(Arrays.equals(readRequest(socket), new byte[]{'x'}), "whole POST received before commit");
// Observable single-process side effect, not a durable database transaction.
commits.incrementAndGet();
// Deliberately close without a response after the effect has happened.
return null;
}
});
HttpRequest request = HttpRequest.newBuilder(URI.create("http://127.0.0.1:" + server.getLocalPort() + "/commit"))
.timeout(Duration.ofSeconds(3)).POST(HttpRequest.BodyPublishers.ofString("x")).build();
try {
client.send(request, HttpResponse.BodyHandlers.ofString());
throw new AssertionError("lost response must not succeed");
} catch (IOException expected) { }
committed.get(4, TimeUnit.SECONDS);
check(commits.get() == 1, "client failure did not undo server work");
}
try (ServerSocket server = httpListener()) {
Future<Void> sender = workers.submit(() -> {
try (Socket socket = server.accept()) {
socket.setSoTimeout(3000);
readRequest(socket);
socket.getOutputStream().write("HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nA".getBytes(StandardCharsets.US_ASCII));
socket.getOutputStream().flush();
check(releaseBody.await(4, TimeUnit.SECONDS), "body release deadline");
socket.getOutputStream().write('B');
}
return null;
});
HttpRequest request = HttpRequest.newBuilder(URI.create("http://127.0.0.1:" + server.getLocalPort() + "/slow"))
.timeout(Duration.ofSeconds(3)).build();
HttpResponse<InputStream> response = client.send(request, HttpResponse.BodyHandlers.ofInputStream());
check(response.statusCode() == 200, "headers available");
try (InputStream body = response.body()) {
check(body.read() == 'A', "first byte before body release");
CountDownLatch readerStarted = new CountDownLatch(1);
Future<byte[]> rest = workers.submit(() -> { readerStarted.countDown(); return body.readAllBytes(); });
check(readerStarted.await(1, TimeUnit.SECONDS), "reader scheduled");
try { rest.get(100, TimeUnit.MILLISECONDS); throw new AssertionError("body completed before final byte exists"); }
catch (TimeoutException expected) { }
releaseBody.countDown();
check(Arrays.equals(rest.get(3, TimeUnit.SECONDS), new byte[]{'B'}), "complete body only after release");
}
sender.get(3, TimeUnit.SECONDS);
}
} finally {
releaseBody.countDown();
workers.shutdownNow();
check(workers.awaitTermination(5, TimeUnit.SECONDS), "HTTP workers stopped");
}
System.out.println("PASS http: client=IOException server-commit=1; headers/first-byte before full-body; AB then EOF; workers/body/sockets closed");
}
static void dnsTest(String mode) throws Exception {
// Before any InetAddress use in this JVM; no Security or hosts change survives it.
Security.setProperty("networkaddress.cache.ttl", mode.equals("dns-positive") ? "-1" : "0");
Security.setProperty("networkaddress.cache.negative.ttl", mode.equals("dns-negative") ? "-1" : "0");
Path hosts = Path.of(System.getProperty("jdk.net.hosts.file"));
if (mode.equals("dns-positive")) {
check(InetAddress.getByName("lab.test").getHostAddress().equals("127.0.0.1"), "initial positive lookup");
Files.writeString(hosts, "192.0.2.1 lab.test\n");
check(InetAddress.getByName("lab.test").getHostAddress().equals("127.0.0.1"), "positive cache survives file change");
} else if (mode.equals("dns-negative")) {
expectUnknown("new.test");
Files.writeString(hosts, "127.0.0.1 lab.test new.test\n");
expectUnknown("new.test");
} else {
expectUnknown("new.test");
InetAddress initial = InetAddress.getByName("lab.test");
try (ServerSocket server = httpListener(); Socket connection = new Socket()) {
connection.connect(new InetSocketAddress(initial, server.getLocalPort()), 2000);
connection.setSoTimeout(3000);
try (Socket accepted = server.accept()) {
accepted.setSoTimeout(3000);
Files.writeString(hosts, "192.0.2.1 lab.test\n127.0.0.1 new.test\n");
check(InetAddress.getByName("lab.test").getHostAddress().equals("192.0.2.1"), "security TTL=0 overrides same-name -D=600");
check(InetAddress.getByName("new.test").getHostAddress().equals("127.0.0.1"), "negative zero sees repair");
// No connection is ever attempted to the documentation-only new address.
connection.getOutputStream().write(23);
check(accepted.getInputStream().read() == 23, "existing connection still works");
check(connection.getInetAddress().getHostAddress().equals("127.0.0.1"), "socket peer stays old address");
}
}
}
System.out.println("PASS " + mode + ": security-positive=" + Security.getProperty("networkaddress.cache.ttl")
+ " security-negative=" + Security.getProperty("networkaddress.cache.negative.ttl")
+ " system-same-name=" + System.getProperty("networkaddress.cache.ttl") + " (private hosts; no real DNS)");
}
static void expectUnknown(String host) throws Exception {
try { InetAddress.getByName(host); throw new AssertionError("expected negative lookup: " + host); }
catch (UnknownHostException expected) { }
}
public static void main(String[] args) throws Exception {
switch (args[0]) {
case "budget" -> budgetTest();
case "tcp" -> tcpTest();
case "selector" -> selectorTest();
case "http" -> httpTest();
case "dns-positive", "dns-negative", "dns-zero" -> dnsTest(args[0]);
default -> throw new IllegalArgumentException("Unknown case: " + args[0]);
}
}
}OS 资源探针源码
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.*;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.*;
import java.util.concurrent.CountDownLatch;
/**
* U14 OS-resource teaching probes (process/thread/signal/fd/memory/permission/disk).
* Each subcommand is a separate JVM launch orchestrated by run.py; every probe
* prints machine-readable lines and exits. Not a benchmark or production tool:
* allocations, thread counts, limits and temp files are bounded by run.py, which
* owns temp directories and cleanup.
*/
public class OsResourceProbe {
static final PrintStream OUT = System.out;
public static void main(String[] args) throws Exception {
String mode = args.length > 0 ? args[0] : "";
switch (mode) {
case "thread" -> thread(Integer.parseInt(args[1]));
case "signal" -> signal();
case "fd" -> fd(Integer.parseInt(args[1]), args[2], args[3]);
case "mem" -> mem();
case "perm" -> perm(args[1]);
case "disk" -> disk();
default -> { OUT.println("UNKNOWN_MODE=" + mode); System.exit(2); }
}
}
/** Park one thread per iteration until target or OOME; report both outcomes. */
static void thread(int target) throws Exception {
CountDownLatch latch = new CountDownLatch(1);
List<Thread> held = new ArrayList<>();
int created = 0;
try {
for (; created < target; created++) {
Thread t = new Thread(() -> {
try { latch.await(); } catch (InterruptedException e) { }
}, "parked-" + created);
t.setDaemon(true);
t.start();
held.add(t);
}
OUT.println("TARGET_REACHED created=" + created + " active=" + Thread.activeCount());
} catch (OutOfMemoryError e) {
OUT.println("OOME_AT created=" + created + " active=" + Thread.activeCount()
+ " msg=" + String.valueOf(e.getMessage()).replaceAll("\\s+", " "));
}
OUT.println("PID=" + ProcessHandle.current().pid());
latch.countDown();
}
/** Install a shutdown hook, announce pid, then park until signaled. */
static void signal() throws Exception {
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
OUT.println("HOOK-RAN");
OUT.flush();
try { Thread.sleep(300); } catch (InterruptedException e) { }
OUT.println("HOOK-DONE");
OUT.flush();
}));
OUT.println("READY pid=" + ProcessHandle.current().pid());
OUT.flush();
new CountDownLatch(1).await();
}
/**
* Open count files against filePath. hold=true keeps them open, calls
* System.gc() and retries opening more, printing whether GC freed any fd.
* hold=false closes each file immediately (try-with-resources).
*/
static void fd(int count, String filePath, String holdFlag) throws Exception {
boolean hold = holdFlag.equals("hold");
List<FileInputStream> held = new ArrayList<>();
int opened = 0;
try {
for (int i = 0; i < count; i++) {
FileInputStream in = new FileInputStream(filePath);
opened++;
if (hold) held.add(in); else in.close();
}
OUT.println((hold ? "OPENED_ALL" : "OPENED_ALL_CLOSED") + " opened=" + opened);
} catch (IOException e) {
OUT.println("EMFILE_AT opened=" + opened + " msg=" + e.getMessage());
}
if (hold) {
System.gc();
Thread.sleep(200);
int retry = 0;
try {
for (int i = 0; i < 20; i++) { held.add(new FileInputStream(filePath)); retry++; }
} catch (IOException e) {
// expected while fds are still held; GC must not reclaim them
}
OUT.println("GC_RETRY_OPENED=" + retry);
for (FileInputStream in : held) try { in.close(); } catch (IOException ignored) { }
}
}
/** Allocate ~100 MB in 256 KB chunks, report heap, then park for the runner to observe RSS/NMT. */
static void mem() throws Exception {
OUT.println("PHASE=before pid=" + ProcessHandle.current().pid()
+ " maxHeapMB=" + Runtime.getRuntime().maxMemory() / 1048576);
OUT.flush();
Thread.sleep(2000); // let the runner sample the baseline RSS before allocation starts
List<byte[]> held = new ArrayList<>();
for (int i = 0; i < 400; i++) held.add(new byte[262144]);
long used = (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / 1048576;
OUT.println("PHASE=after heapUsedMB=" + used + " chunks=" + held.size());
OUT.flush();
Thread.sleep(25000);
}
/**
* arg[1] = expected mode in symbolic rwx form after umask (runner computes it).
* 1) createFile requesting rwxrwxrwx must come out masked by the umask;
* 2) a 000-permission file must deny reads even for its owner;
* 3) binding port 80 is recorded as a platform-dependent observation.
*/
static void perm(String expectedAfterUmask) throws Exception {
Path p = Files.createTempFile("u14-perm", ".txt");
Files.delete(p);
Set<PosixFilePermission> requested = PosixFilePermissions.fromString("rwxrwxrwx");
Files.createFile(p, PosixFilePermissions.asFileAttribute(requested));
String actual = PosixFilePermissions.toString(Files.getPosixFilePermissions(p));
OUT.println("CREATED=" + actual + " EXPECTED=" + expectedAfterUmask);
if (!actual.equals(expectedAfterUmask)) {
OUT.println("UMASK_MISMATCH actual=" + actual);
}
Files.delete(p);
Path q = Files.createTempFile("u14-zero", ".txt");
Files.setPosixFilePermissions(q, PosixFilePermissions.fromString("---------"));
try {
Files.readString(q);
OUT.println("READ000=allowed");
} catch (AccessDeniedException e) {
OUT.println("READ000=denied");
} finally {
Files.delete(q);
}
try (java.net.ServerSocket ss = new java.net.ServerSocket()) {
ss.bind(new java.net.InetSocketAddress(80));
OUT.println("BIND80=ok");
} catch (Exception e) {
OUT.println("BIND80=denied:" + e.getClass().getSimpleName());
}
}
/**
* Write 64 MB, delete the path while the channel is open, show the store
* did not reclaim the space and the data is still readable; then time
* write+force 10 times (observation, not a contract).
*/
static void disk() throws Exception {
Path dir = Files.createTempDirectory("u14-disk");
Path p = dir.resolve("blob.tmp");
long base = Files.getFileStore(dir).getUsableSpace();
try (FileChannel ch = FileChannel.open(p,
StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.READ)) {
ByteBuffer buf = ByteBuffer.wrap(new byte[1048576]);
for (int i = 0; i < 64; i++) { buf.clear(); ch.write(buf); }
ch.force(true);
long withFile = Files.getFileStore(dir).getUsableSpace();
Files.delete(p);
long deletedOpen = Files.getFileStore(dir).getUsableSpace();
OUT.println("USABLE_MB withFile=" + withFile / 1048576
+ " deletedWhileOpen=" + deletedOpen / 1048576 + " base=" + base / 1048576);
ch.position(0);
ByteBuffer back = ByteBuffer.wrap(new byte[1048576]);
ch.read(back);
OUT.println("STILL_READABLE=" + (back.get(0) == 0));
long[] fsyncUs = new long[10];
for (int i = 0; i < fsyncUs.length; i++) {
buf.clear();
long t0 = System.nanoTime();
ch.write(buf);
ch.force(true);
fsyncUs[i] = (System.nanoTime() - t0) / 1000;
}
OUT.println("FSYNC_US=" + Arrays.toString(fsyncUs));
}
deleteTree(dir);
}
static void deleteTree(Path dir) throws IOException {
try (var stream = Files.newDirectoryStream(dir)) {
for (Path entry : stream) Files.delete(entry);
}
Files.delete(dir);
}
}U15 连接探针源码
站点直接嵌入同目录 ConnectionProbe.java:tcpstates 的环回状态机断言、tls 的五案例握手、reuse 的复用/超时计数、dnschain 的私有 hosts 链路;child-server 辅助进程只用于 SIGKILL 半开连接反例。
import java.io.*;
import java.net.*;
import java.net.http.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.security.KeyStore;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import javax.net.ssl.*;
/** U15 protocol-layer probes (loopback only). Teaching probes, not a production server. */
public class ConnectionProbe {
static void check(boolean condition, String message) {
if (!condition) throw new AssertionError(message);
}
static InetAddress loopback() throws UnknownHostException {
return InetAddress.getByAddress(new byte[]{127, 0, 0, 1});
}
static ExecutorService daemonPool(int size) {
return Executors.newFixedThreadPool(size, runnable -> {
Thread thread = new Thread(runnable);
thread.setDaemon(true);
return thread;
});
}
static boolean macOS() {
return System.getProperty("os.name", "").toLowerCase(Locale.ROOT).contains("mac");
}
/** macOS netstat rows for a loopback connection; empty on other platforms. */
static List<String> netstatRows() throws Exception {
if (!macOS()) return List.of();
Process p = new ProcessBuilder("/usr/sbin/netstat", "-an", "-p", "tcp").start();
String out = new String(p.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
p.waitFor(5, TimeUnit.SECONDS);
List<String> rows = new ArrayList<>();
for (String line : out.split("\n")) {
if (line.startsWith("tcp")) rows.add(line.trim());
}
return rows;
}
static int tcpStateCount(List<String> rows, int localPort, String state) {
int count = 0;
for (String row : rows) {
String[] tokens = row.split("\\s+");
if (tokens.length >= 6 && tokens[3].endsWith("." + localPort) && tokens[5].equals(state)) count++;
}
return count;
}
static String firstStateRow(List<String> rows, int localPort, int foreignPort, String state) {
for (String row : rows) {
String[] tokens = row.split("\\s+");
if (tokens.length >= 6 && tokens[3].endsWith("." + localPort)
&& tokens[4].endsWith("." + foreignPort) && tokens[5].equals(state)) return row;
}
return null;
}
static void tcpstates() throws Exception {
ExecutorService pool = daemonPool(2);
// 1) Clean FIN: half-close, reply still arrives, then EOF.
try (ServerSocket ss = new ServerSocket()) {
ss.bind(new InetSocketAddress(loopback(), 0));
Future<String> server = pool.submit(() -> {
try (Socket s = ss.accept()) {
BufferedReader r = new BufferedReader(new InputStreamReader(s.getInputStream(), StandardCharsets.US_ASCII));
String req = r.readLine();
s.getOutputStream().write(("resp:" + req + "\n").getBytes(StandardCharsets.US_ASCII));
s.shutdownOutput();
return "server-saw=" + req;
}
});
try (Socket c = new Socket()) {
c.connect(new InetSocketAddress(loopback(), ss.getLocalPort()), 2000);
c.getOutputStream().write("ping\n".getBytes(StandardCharsets.US_ASCII));
c.getOutputStream().flush();
c.shutdownOutput();
BufferedReader r = new BufferedReader(new InputStreamReader(c.getInputStream(), StandardCharsets.US_ASCII));
check(("resp:ping").equals(r.readLine()), "half-close still receives reply");
check(r.readLine() == null, "clean FIN gives EOF");
check(server.get(3, TimeUnit.SECONDS).equals("server-saw=ping"), "server saw request");
}
}
// 2) RST via SO_LINGER(0) close: peer's next read fails instead of returning -1.
try (ServerSocket ss = new ServerSocket()) {
ss.bind(new InetSocketAddress(loopback(), 0));
Future<String> server = pool.submit(() -> {
try (Socket s = ss.accept()) {
s.setSoTimeout(4000);
int first = s.getInputStream().read(); // byte already delivered before RST
try {
s.getInputStream().read();
return "server-second-read=no-exception";
} catch (IOException e) {
return "server-second-read=ex:" + e.getClass().getSimpleName() + ":" + e.getMessage();
} finally {
check(first == 'x', "first byte delivered");
}
}
});
try (Socket c = new Socket()) {
c.connect(new InetSocketAddress(loopback(), ss.getLocalPort()), 2000);
c.getOutputStream().write('x');
c.getOutputStream().flush();
Thread.sleep(300);
c.setSoLinger(true, 0); // close() now aborts with RST instead of FIN
c.close();
String result = server.get(4, TimeUnit.SECONDS);
check(result.contains("ex:") && result.toLowerCase(Locale.ROOT).contains("reset"),
"linger-0 close must surface as reset on peer's next read, got: " + result);
}
}
// 3) TIME_WAIT belongs to the active closer: bind/rebind semantics are observable.
int timeWaitPort;
try (ServerSocket ss = new ServerSocket()) {
ss.bind(new InetSocketAddress(loopback(), 0));
int serverPort = ss.getLocalPort();
CountDownLatch serverClosed = new CountDownLatch(1);
Future<String> server = pool.submit(() -> {
try (Socket s = ss.accept()) {
while (s.getInputStream().read() != -1) { /* drain until FIN */ }
s.close(); // passive close: server must NOT enter TIME_WAIT
serverClosed.countDown();
return "ok";
}
});
Socket c = new Socket();
c.bind(new InetSocketAddress(loopback(), 0));
timeWaitPort = c.getLocalPort();
c.connect(new InetSocketAddress(loopback(), serverPort), 2000);
c.getOutputStream().write('x');
c.getOutputStream().flush();
c.close(); // active close: this endpoint enters TIME_WAIT
check(server.get(3, TimeUnit.SECONDS).equals("ok"), "server drained and closed");
check(serverClosed.await(2, TimeUnit.SECONDS), "server closed latch");
List<String> rows = netstatRows();
if (macOS()) {
String row = firstStateRow(rows, timeWaitPort, serverPort, "TIME_WAIT");
check(row != null, "client endpoint must sit in TIME_WAIT after active close, rows=" + rows.size());
System.out.println("TCPSTATES netstat TIME_WAIT: " + row);
try {
Socket re = new Socket();
re.bind(new InetSocketAddress(loopback(), timeWaitPort));
re.close();
throw new AssertionError("binding a TIME_WAIT port without SO_REUSEADDR must fail");
} catch (BindException expected) {
System.out.println("TCPSTATES rebind-no-reuseaddr=denied (" + expected.getMessage() + ")");
}
Socket re = new Socket();
re.setReuseAddress(true);
re.bind(new InetSocketAddress(loopback(), timeWaitPort));
re.close();
System.out.println("TCPSTATES rebind-reuseaddr=ok");
} else {
System.out.println("OBSERVE tcpstates: netstat state assertions are macOS-only on this host");
}
}
// 4) CLOSE_WAIT accumulates when the app never closes after the peer's FIN.
try (ServerSocket ss = new ServerSocket()) {
ss.bind(new InetSocketAddress(loopback(), 0));
int serverPort = ss.getLocalPort();
List<Socket> held = new ArrayList<>();
for (int i = 0; i < 3; i++) {
Socket c = new Socket();
c.connect(new InetSocketAddress(loopback(), serverPort), 2000);
c.getOutputStream().write('x');
c.getOutputStream().flush();
c.shutdownOutput(); // peer FIN arrives; server app never closes below
held.add(ss.accept());
Thread.sleep(150);
}
List<String> rows = netstatRows();
if (macOS()) {
int closeWait = tcpStateCount(rows, serverPort, "CLOSE_WAIT");
check(closeWait >= 3, "expected >=3 CLOSE_WAIT rows for un-closed server sockets, got " + closeWait);
System.out.println("TCPSTATES close_wait_rows=" + closeWait + " (app did not close after FIN)");
} else {
System.out.println("OBSERVE tcpstates: CLOSE_WAIT assertion is macOS-only");
}
for (Socket s : held) s.close();
}
// 5) Half-open: SIGKILL of the peer with unread data surfaces as RST, not EOF.
Path classDir = Paths.get(ConnectionProbe.class.getProtectionDomain().getCodeSource().getLocation().toURI());
String java = Paths.get(System.getProperty("java.home"), "bin", "java").toString();
Process child = new ProcessBuilder(java, "-cp", classDir.toString(), "ConnectionProbe", "child-server")
.redirectErrorStream(true).start();
BufferedReader childOut = new BufferedReader(new InputStreamReader(child.getInputStream(), StandardCharsets.UTF_8));
String line = childOut.readLine();
check(line != null && line.startsWith("PORT="), "child server did not report port: " + line);
int childPort = Integer.parseInt(line.substring("PORT=".length()));
try (Socket c = new Socket()) {
c.connect(new InetSocketAddress(loopback(), childPort), 2000);
c.getOutputStream().write(new byte[8]); // child never reads them
c.getOutputStream().flush();
Thread.sleep(400); // let the bytes arrive in the child's receive buffer
child.destroyForcibly(); // SIGKILL: kernel closes, unread receive data -> RST
child.waitFor(5, TimeUnit.SECONDS);
Thread.sleep(400);
String writeOutcome;
try {
c.getOutputStream().write('y');
c.getOutputStream().flush();
writeOutcome = "ok";
} catch (IOException e) {
writeOutcome = "ex:" + e.getClass().getSimpleName() + ":" + e.getMessage();
}
try {
int n = c.getInputStream().read();
throw new AssertionError("half-open read must fail after peer death, got " + n);
} catch (IOException e) {
check(e.getMessage().toLowerCase(Locale.ROOT).contains("reset"),
"half-open read must be a reset, got: " + e.getMessage());
System.out.println("TCPSTATES half-open write=" + writeOutcome + " read=ex:" + e.getClass().getSimpleName() + ":" + e.getMessage());
}
}
// 6) Keepalive tuning is a socket option, not a fast failure guarantee (observation).
try (Socket c = new Socket()) {
c.setOption(StandardSocketOptions.SO_KEEPALIVE, true);
System.out.println("TCPSTATES so_keepalive=true (option accepted)");
try {
SocketOption<Integer> keepIdle = jdk.net.ExtendedSocketOptions.TCP_KEEPIDLE;
c.setOption(keepIdle, 600);
check(c.getOption(keepIdle) == 600, "TCP_KEEPIDLE roundtrip");
System.out.println("TCPSTATES tcp_keepidle=600s accepted by this JDK/platform (config accepted, probe behavior not asserted)");
} catch (Exception e) {
System.out.println("OBSERVE tcpstates: extended keepalive options unavailable: " + e.getClass().getSimpleName());
}
}
pool.shutdownNow();
check(pool.awaitTermination(5, TimeUnit.SECONDS), "probe pool stopped");
System.out.println("PASS tcpstates: FIN=reply+EOF; linger0=reset-on-next-read; active-closer=TIME_WAIT"
+ "+rebind rules; un-closed FIN=CLOSE_WAIT x3; peer-SIGKILL=reset; keepalive=options only");
}
/** Helper process: accepts one connection, never reads, dies on SIGKILL. */
static void childServer() throws Exception {
ServerSocket ss = new ServerSocket();
ss.bind(new InetSocketAddress(loopback(), 0));
System.out.println("PORT=" + ss.getLocalPort());
System.out.flush();
Socket s = ss.accept();
Thread.sleep(15000);
s.close();
}
// ---- TLS ----
static SSLContext trustContext(String trustPath) throws Exception {
KeyStore ts = KeyStore.getInstance("PKCS12");
try (InputStream in = new FileInputStream(trustPath)) { ts.load(in, "changeit".toCharArray()); }
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(ts);
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(null, tmf.getTrustManagers(), null);
return ctx;
}
static SSLContext keyContext(String storePath) throws Exception {
KeyStore ks = KeyStore.getInstance("PKCS12");
try (InputStream in = new FileInputStream(storePath)) { ks.load(in, "changeit".toCharArray()); }
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(ks, "changeit".toCharArray());
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(kmf.getKeyManagers(), null, null);
return ctx;
}
/**
* One HTTPS request against a single-connection loopback TLS server.
* args: case store trust host [expected]
*/
static void tlsCase(String[] args) throws Exception {
String kase = args[0], store = args[1], trust = args[2], host = args[3];
try (SSLServerSocket ss = (SSLServerSocket) keyContext(store).getServerSocketFactory().createServerSocket()) {
ss.bind(new InetSocketAddress(loopback(), 0));
ExecutorService pool = daemonPool(1);
Future<String> server = pool.submit(() -> {
try (SSLSocket s = (SSLSocket) ss.accept()) {
s.setSoTimeout(5000);
BufferedReader r = new BufferedReader(new InputStreamReader(s.getInputStream(), StandardCharsets.US_ASCII));
String req = r.readLine();
s.getOutputStream().write("HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nok"
.getBytes(StandardCharsets.US_ASCII));
return "server-served req=" + req;
} catch (Exception e) {
return "server-ex=" + e.getClass().getSimpleName();
}
});
HttpClient.Builder builder = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1)
.proxy(ProxySelector.of(null)).connectTimeout(Duration.ofSeconds(2));
if (!"null".equals(trust)) {
builder.sslContext(trustContext(trust)); // the literal string "null" = JVM default trust store
}
HttpClient client = builder.build();
HttpRequest request = HttpRequest.newBuilder(URI.create("https://" + host + ":" + ss.getLocalPort() + "/u15"))
.timeout(Duration.ofSeconds(5)).build();
String outcome;
try {
HttpResponse<String> resp = client.send(request, HttpResponse.BodyHandlers.ofString());
outcome = "status=" + resp.statusCode() + " protocol=" + resp.sslSession().orElseThrow().getProtocol()
+ " cipher=" + resp.sslSession().orElseThrow().getCipherSuite();
} catch (Exception e) {
Throwable cause = e.getCause() == null ? e : e.getCause();
outcome = "ex=" + cause.getClass().getSimpleName() + " msg=" + String.valueOf(cause.getMessage()).split("\n")[0];
}
System.out.println("U15TLS-BEGIN");
System.out.println("U15TLS-END " + outcome);
System.out.println("U15TLS-SERVER " + server.get(6, TimeUnit.SECONDS));
pool.shutdownNow();
check(pool.awaitTermination(5, TimeUnit.SECONDS), "tls pool stopped");
verifyTlsOutcome(kase, outcome);
System.out.println("PASS " + kase + ": " + outcome);
}
}
static void verifyTlsOutcome(String kase, String outcome) {
switch (kase) {
case "tls-pos" -> check(outcome.startsWith("status=200") && outcome.contains("protocol=TLS"),
"trusted lab CA must complete the handshake, got: " + outcome);
case "tls-trust" -> check(outcome.contains("SSLHandshakeException") && outcome.contains("PKIX path building failed"),
"default trust store must reject the lab CA, got: " + outcome);
case "tls-expire" -> check(outcome.contains("SSLHandshakeException") && outcome.contains("validity check failed"),
"CA-signed expired certificate must fail validity, got: " + outcome);
case "tls-san" -> check(outcome.contains("SSLHandshakeException") && outcome.contains("No subject alternative"),
"hostname not in SAN must fail hostname verification, got: " + outcome);
case "tls-debug" -> check(outcome.startsWith("status=200"), "debug run must still pass, got: " + outcome);
default -> throw new IllegalArgumentException("Unknown tls case: " + kase);
}
}
// ---- HTTP reuse & timeouts ----
static void reuseKeepalive() throws Exception {
ServerSocket ss = new ServerSocket();
ss.bind(new InetSocketAddress(loopback(), 0), 8);
int port = ss.getLocalPort();
AtomicInteger accepts = new AtomicInteger();
ExecutorService pool = daemonPool(1);
pool.submit(() -> {
while (!ss.isClosed()) {
Socket s = ss.accept();
accepts.incrementAndGet();
try (s) {
s.setSoTimeout(4000);
BufferedReader r = new BufferedReader(new InputStreamReader(s.getInputStream(), StandardCharsets.US_ASCII));
while (true) { // serve many requests per connection
String line;
StringBuilder head = new StringBuilder();
while (!(line = r.readLine()).isEmpty()) head.append(line).append('\n');
if (head.toString().contains("/bye")) { // server-driven close of THIS connection
s.getOutputStream().write("HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nok"
.getBytes(StandardCharsets.US_ASCII));
s.getOutputStream().flush();
break;
}
s.getOutputStream().write("HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: keep-alive\r\n\r\nok"
.getBytes(StandardCharsets.US_ASCII));
s.getOutputStream().flush();
}
} catch (Exception e) {
return null;
}
}
return null;
});
HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1)
.proxy(ProxySelector.of(null)).connectTimeout(Duration.ofSeconds(2)).build();
for (int i = 0; i < 3; i++) {
HttpRequest req = HttpRequest.newBuilder(URI.create("http://127.0.0.1:" + port + "/r" + i)).build();
HttpResponse<String> resp = client.send(req, HttpResponse.BodyHandlers.ofString());
check(resp.statusCode() == 200, "keep-alive request " + i);
}
check(accepts.get() == 1, "3 requests on keep-alive must reuse one connection, accepts=" + accepts.get());
System.out.println("REUSE accepts-after-3-keepalive-requests=" + accepts.get());
try {
client.send(HttpRequest.newBuilder(URI.create("http://127.0.0.1:" + port + "/hdr"))
.setHeader("Connection", "close").build(), HttpResponse.BodyHandlers.ofString());
throw new AssertionError("Connection is a restricted header for JDK HttpClient");
} catch (IllegalArgumentException expected) {
System.out.println("REUSE restricted-header ex=" + expected.getMessage());
}
client.send(HttpRequest.newBuilder(URI.create("http://127.0.0.1:" + port + "/bye")).build(),
HttpResponse.BodyHandlers.ofString());
client.send(HttpRequest.newBuilder(URI.create("http://127.0.0.1:" + port + "/after")).build(),
HttpResponse.BodyHandlers.ofString());
check(accepts.get() == 2, "server-driven Connection: close must force one new connection, accepts=" + accepts.get());
System.out.println("REUSE accepts-after-server-close=" + accepts.get());
pool.shutdownNow();
ss.close();
System.out.println("PASS reuse-keepalive: 3 requests=1 accept; Connection header rejected; server close forces reconnect");
}
static void reuseRequestTimeout() throws Exception {
ServerSocket ss = new ServerSocket();
ss.bind(new InetSocketAddress(loopback(), 0), 2);
ExecutorService pool = daemonPool(1);
pool.submit(() -> {
try (Socket held = ss.accept()) {
Thread.sleep(6000); // accept but never respond
} catch (Exception ignored) { }
return null;
});
HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1)
.proxy(ProxySelector.of(null)).connectTimeout(Duration.ofSeconds(2)).build();
long started = System.nanoTime();
try {
HttpRequest req = HttpRequest.newBuilder(URI.create("http://127.0.0.1:" + ss.getLocalPort() + "/slow"))
.timeout(Duration.ofMillis(700)).build();
client.send(req, HttpResponse.BodyHandlers.ofString());
throw new AssertionError("request with no response must time out");
} catch (HttpTimeoutException e) {
long elapsedMs = (System.nanoTime() - started) / 1_000_000;
check(elapsedMs >= 600 && elapsedMs < 3000,
"request timeout should fire near its budget, got " + elapsedMs + "ms");
System.out.println("REUSE request-timeout ms=" + elapsedMs + " ex=HttpTimeoutException:" + e.getMessage());
} finally {
pool.shutdownNow();
ss.close();
}
// Connection refused: nothing listens here.
long refusedStart = System.nanoTime();
try (ServerSocket occupied = new ServerSocket()) {
occupied.bind(new InetSocketAddress(loopback(), 0));
occupied.close();
int closedPort = occupied.getLocalPort();
try {
new Socket().connect(new InetSocketAddress(loopback(), closedPort), 2000);
throw new AssertionError("closed port must refuse connections");
} catch (ConnectException e) {
long refusedMs = (System.nanoTime() - refusedStart) / 1_000_000;
check(refusedMs < 2000, "connection refused must fail fast, took " + refusedMs + "ms");
System.out.println("REUSE connect-refused ms=" + refusedMs + " ex=ConnectException:" + e.getMessage());
}
}
System.out.println("PASS reuse-reqtimeout: no-response request=HttpTimeoutException near budget; closed port=fast refusal");
}
// ---- DNS chain ----
static void dnsChain() throws Exception {
long best = Long.MAX_VALUE;
for (int i = 0; i < 3; i++) {
long started = System.nanoTime();
InetAddress addr = InetAddress.getByName("chain.test");
best = Math.min(best, System.nanoTime() - started);
check(addr.getHostAddress().equals("127.0.0.1"), "hosts-file entry must win, got " + addr);
}
System.out.println("DNSCHAIN chain.test=127.0.0.1 lookup-min-ms=" + best / 1_000_000 + " (hosts-file re-read observation)");
try {
InetAddress.getByName("missing.test");
throw new AssertionError("name absent from hosts file must fail");
} catch (UnknownHostException expected) {
System.out.println("DNSCHAIN missing.test=UnknownHostException:" + expected.getMessage());
}
if (macOS()) {
Process p = new ProcessBuilder("/usr/sbin/scutil", "--dns").start();
String out = new String(p.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
p.waitFor(5, TimeUnit.SECONDS);
String[] lines = out.split("\n");
check(out.contains("nameserver"), "scutil --dns must list resolver nameservers");
System.out.println("DNSCHAIN scutil--dns (first resolver lines):");
Arrays.stream(lines).filter(l -> l.contains("resolver #1") || l.contains("nameserver[0]") || l.contains("search domain[0]"))
.limit(3).forEach(l -> System.out.println(" " + l.trim()));
Process q = new ProcessBuilder("/usr/bin/dscacheutil", "-q", "host", "-a", "name", "localhost").start();
String dsc = new String(q.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
q.waitFor(5, TimeUnit.SECONDS);
check(dsc.contains("127.0.0.1"), "system resolver answers localhost from hosts data, got: " + dsc);
System.out.println("DNSCHAIN dscacheutil localhost -> 127.0.0.1 (system-level hosts precedence, macOS)");
} else {
Path resolv = Paths.get("/etc/resolv.conf");
String out = Files.exists(resolv) ? Files.readString(resolv) : "";
check(out.contains("nameserver"), "resolv.conf must list nameservers on this platform");
System.out.println("DNSCHAIN /etc/resolv.conf first line: " + out.split("\n")[0]);
}
System.out.println("PASS dns-chain: hosts-file wins over resolver; missing name fails; OS resolver config observed (read-only)");
}
static void dnsHostsMissing() throws Exception {
try {
InetAddress.getByName("chain.test");
throw new AssertionError("nonexistent hosts file must fail every lookup");
} catch (UnknownHostException e) {
check(String.valueOf(e.getMessage()).contains("hosts file"),
"hosts-file failure must be named as such, got: " + e.getMessage());
System.out.println("DNSCHAIN missing-file UnknownHostException:" + e.getMessage());
}
System.out.println("PASS dns-hostsmissing: nonexistent hosts file behaves as empty file -> UnknownHostException");
}
public static void main(String[] args) throws Exception {
switch (args[0]) {
case "tcpstates" -> tcpstates();
case "child-server" -> childServer();
case "tls-pos", "tls-trust", "tls-expire", "tls-san", "tls-debug" -> tlsCase(args);
case "reuse-keepalive" -> reuseKeepalive();
case "reuse-reqtimeout" -> reuseRequestTimeout();
case "dns-chain" -> dnsChain();
case "dns-hostsmissing" -> dnsHostsMissing();
default -> throw new IllegalArgumentException("Unknown case: " + args[0]);
}
}
}Python runner 源码
同目录 run.py 是公开 CLI 实现。站点读源码即可检查超时、临时文件和子进程退出逻辑,运行仍使用前面的仓库命令。
#!/usr/bin/env python3
"""Public, bounded runner; compiles only into an automatically removed temp dir."""
import argparse
import os
import platform
import re
import resource
import signal
import subprocess
import tempfile
import time
from pathlib import Path
HERE = Path(__file__).resolve().parent
NETWORK_MODES = ("budget", "tcp", "selector", "http", "dns",
"tcpstates", "tls", "reuse", "dnschain")
RESOURCE_MODES = ("thread", "signal", "fd", "mem", "perm", "disk")
U15_MODES = ("tcpstates", "tls", "reuse", "dnschain")
def java_home(parser):
home = os.environ.get("JAVA_HOME")
if not home and platform.system() == "Darwin":
home = subprocess.check_output(["/usr/libexec/java_home", "-v17"], text=True, timeout=10).strip()
if not home:
parser.error("Set JAVA_HOME to an installed JDK 17 (no global configuration is changed).")
return home
def run_java(java, directory, main_class, args, timeout=60, jvm=None, preexec=None):
command = [java] + (jvm or []) + ["-ea", "-cp", directory, main_class] + list(args)
return subprocess.run(command, capture_output=True, text=True, timeout=timeout, preexec_fn=preexec)
def field(line, key):
match = re.search(rf"{key}=([^\s]+)", line)
return match.group(1) if match else None
def symbolic_mode(mode):
bits = ("r", "w", "x")
return "".join(bits[i % 3] if mode & (1 << (8 - i)) else "-" for i in range(9))
def macos_thread_limit():
try:
out = subprocess.check_output(["sysctl", "-n", "kern.num_taskthreads"], text=True, timeout=5)
return int(out.strip())
except Exception:
return None
def thread_mode(java, directory):
result = run_java(java, directory, "OsResourceProbe", ["thread", "5000"], timeout=90)
print(result.stdout, end="", flush=True)
result.check_returncode()
limit = macos_thread_limit()
lines = result.stdout.strip().splitlines()
oome = next((line for line in lines if line.startswith("OOME_AT")), None)
reached = next((line for line in lines if line.startswith("TARGET_REACHED")), None)
if oome:
created = int(field(oome, "created"))
assert created >= 1000, f"thread creation stopped too early: {oome}"
note = f" (kern.num_taskthreads={limit})" if limit and created < limit else ""
print(f"PASS thread: native thread creation hit a real task-level limit at {created}{note}", flush=True)
elif reached:
print("OBSERVE thread: target reached without hitting a thread limit on this host; "
"Linux pids.max/RLIMIT_NPROC semantics are derived in the article", flush=True)
else:
raise AssertionError("no OOME_AT/TARGET_REACHED marker from thread probe")
def signal_mode(java, directory):
def spawn(extra=()):
p = subprocess.Popen([java, "-ea", "-cp", str(directory), "OsResourceProbe", "signal", *extra],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
line = p.stdout.readline()
assert line.startswith("READY pid="), f"unexpected startup line: {line!r}"
time.sleep(0.3)
return p
# Phase 1: SIGTERM runs the hook, JVM exits itself, exit status encodes 128+15.
p = spawn()
p.send_signal(signal.SIGTERM)
out = p.communicate(timeout=20)[0]
assert "HOOK-RAN" in out and "HOOK-DONE" in out, f"hook did not run on SIGTERM: {out!r}"
assert p.returncode == 143, f"SIGTERM exit status {p.returncode} != 143"
# Phase 2: SIGKILL cannot be caught; no hook, status -9/137.
p = spawn()
p.send_signal(signal.SIGKILL)
out = p.communicate(timeout=20)[0]
assert "HOOK-RAN" not in out and "HOOK-DONE" not in out, f"hook ran on SIGKILL: {out!r}"
assert p.returncode == -9, f"SIGKILL returncode {p.returncode} != -9"
# Phase 3: SIGQUIT triggers the HotSpot thread dump and the JVM keeps running.
p = spawn()
p.send_signal(signal.SIGQUIT)
time.sleep(1.5)
alive = p.poll() is None
p.kill()
out = p.communicate(timeout=20)[0]
assert alive, "SIGQUIT must not terminate the JVM"
assert "Full thread dump" in out and '"main"' in out, "SIGQUIT thread dump missing"
# Phase 4: with -Xrs the JVM installs no INT/TERM handlers; SIGTERM just kills.
p = subprocess.Popen([java, "-ea", "-Xrs", "-cp", str(directory), "OsResourceProbe", "signal"],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
line = p.stdout.readline()
assert line.startswith("READY pid="), f"unexpected -Xrs startup line: {line!r}"
time.sleep(0.3)
p.send_signal(signal.SIGTERM)
out = p.communicate(timeout=20)[0]
assert "HOOK-RAN" not in out, f"-Xrs must not run the hook: {out!r}"
assert p.returncode == -15, f"-Xrs SIGTERM returncode {p.returncode} != -15"
print("PASS signal: TERM=hook+143, KILL=no-hook+-9, QUIT=dump+survive, XRS+TERM=no-hook+-15", flush=True)
def fd_mode(java, directory):
probe_path = HERE / "OsResourceProbe.java"
def lower():
resource.setrlimit(resource.RLIMIT_NOFILE, (80, 80))
hold = run_java(java, directory, "OsResourceProbe", ["fd", "200", str(probe_path), "hold"],
timeout=90, preexec=lower)
print(hold.stdout, end="", flush=True)
hold.check_returncode()
emfile = next((line for line in hold.stdout.splitlines() if line.startswith("EMFILE_AT")), None)
assert emfile, f"expected EMFILE under nofile=80, got: {hold.stdout!r}"
opened = int(field(emfile, "opened"))
assert 0 < opened < 200, f"unexpected open count {opened}"
retry = int(field(hold.stdout, "GC_RETRY_OPENED"))
assert retry == 0, f"System.gc() must not reclaim held fds, but {retry} opens succeeded"
close = run_java(java, directory, "OsResourceProbe", ["fd", "200", str(probe_path), "close"],
timeout=90, preexec=lower)
print(close.stdout, end="", flush=True)
close.check_returncode()
assert "OPENED_ALL_CLOSED opened=200" in close.stdout, f"close-each-open run failed: {close.stdout!r}"
print(f"PASS fd: limit=80 -> EMFILE after {opened} opens, GC frees none, closing fixes it", flush=True)
def ps_rss_kb(pid):
out = subprocess.check_output(["ps", "-o", "rss=", "-p", str(pid)], text=True)
return int(out.strip().split()[0])
def mem_mode(java, directory):
p = subprocess.Popen([java, "-ea", "-Xms64m", "-Xmx512m", "-XX:NativeMemoryTracking=summary",
"-cp", str(directory), "OsResourceProbe", "mem"],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
before = p.stdout.readline()
assert before.startswith("PHASE=before pid="), f"unexpected start: {before!r}"
pid = int(re.search(r"pid=(\d+)", before).group(1))
time.sleep(0.5)
rss_before = ps_rss_kb(pid)
after = p.stdout.readline()
assert after.startswith("PHASE=after"), f"unexpected phase line: {after!r}"
heap_used_mb = int(field(after, "heapUsedMB"))
time.sleep(1.0)
rss_after = ps_rss_kb(pid)
nmt_total_kb = nmt_heap_kb = None
try:
nmt = subprocess.run([str(Path(java).parent / "jcmd"), str(pid), "VM.native_memory", "summary"],
capture_output=True, text=True, timeout=30)
total = re.search(r"^Total: reserved=\d+KB, committed=(\d+)KB", nmt.stdout, re.M)
heap = re.search(r"Java Heap \(reserved=\d+KB, committed=(\d+)KB\)", nmt.stdout)
nmt_total_kb = int(total.group(1)) if total else None
nmt_heap_kb = int(heap.group(1)) if heap else None
except Exception as exc:
print(f"jcmd NMT unavailable: {exc}", flush=True)
p.kill()
p.communicate(timeout=10)
assert rss_after - rss_before >= 50 * 1024, f"RSS grew only {rss_after - rss_before} KB"
assert rss_after > heap_used_mb * 1024, "RSS must exceed heap used (JVM native memory exists)"
print(f"PASS mem: heapUsed={heap_used_mb}MB RSS {rss_before}->{rss_after}KB "
f"NMT total={nmt_total_kb}KB heap={nmt_heap_kb}KB (RSS!=heap; single-host observation)", flush=True)
def perm_mode(java, directory):
expected = symbolic_mode(0o777 & ~0o027) # requested rwxrwxrwx masked by umask 0o027
result = run_java(java, directory, "OsResourceProbe", ["perm", expected], timeout=60,
preexec=lambda: os.umask(0o027))
print(result.stdout, end="", flush=True)
result.check_returncode()
assert "UMASK_MISMATCH" not in result.stdout, f"umask was not applied: {result.stdout!r}"
assert "CREATED=" in result.stdout and "READ000=denied" in result.stdout, f"perm probe failed: {result.stdout!r}"
bind = "ok" if "BIND80=ok" in result.stdout else "denied"
print(f"PASS perm: umask masks createFile request, 000 denies owner read, bind80={bind} "
"(platform-dependent; Linux derivation in article)", flush=True)
def disk_mode(java, directory):
result = run_java(java, directory, "OsResourceProbe", ["disk"], timeout=90)
print(result.stdout, end="", flush=True)
result.check_returncode()
usable = next((line for line in result.stdout.splitlines() if line.startswith("USABLE_MB")), None)
assert usable, "missing USABLE_MB line"
with_file = int(field(usable, "withFile"))
deleted = int(field(usable, "deletedWhileOpen"))
assert deleted == with_file, f"space reclaimed while fd open: {usable}"
assert "STILL_READABLE=true" in result.stdout, "deleted-but-open file must stay readable"
try:
iostat = subprocess.run(["iostat", "-c", "2", "-w", "1"], capture_output=True, text=True, timeout=20)
if iostat.returncode == 0:
print(iostat.stdout.strip().splitlines()[-1], flush=True)
except Exception as exc:
print(f"iostat unavailable: {exc}", flush=True)
print("PASS disk: deleted-while-open keeps space+data; fsync timings are observations above", flush=True)
def sysctl_tcp_observation():
"""Read-only macOS TCP keepalive/timer sysctls; numbers are host observations."""
if platform.system() != "Darwin":
return
for name in ("net.inet.tcp.keepinit", "net.inet.tcp.keepidle", "net.inet.tcp.keepintvl"):
try:
out = subprocess.check_output(["sysctl", "-n", name], text=True, timeout=5).strip()
print(f"OBSERVE sysctl {name}={out} (macOS host value, not Linux)", flush=True)
except Exception:
pass
def tls_prepare(java_home, workdir):
"""Ephemeral lab CA/server/expired keystores via keytool; all inside workdir."""
keytool = str(Path(java_home) / "bin" / "keytool")
storepass = "changeit"
san = "SAN=dns:good.test,ip:127.0.0.1"
extensions = ["-ext", san, "-ext", "ku:c=digitalSignature,keyEncipherment", "-ext", "eku=serverAuth"]
def tool(*args):
subprocess.run([keytool, *args, f"-storepass", storepass], check=True, timeout=30,
capture_output=True, text=True, cwd=workdir)
def issue(store, alias, validity, startdate=None):
tool("-genkeypair", "-alias", alias, "-dname", "CN=good.test", "-keyalg", "RSA",
"-keysize", "2048", "-validity", validity, "-keystore", store, "-storetype", "PKCS12",
"-ext", san)
tool("-certreq", "-alias", alias, "-keystore", store, "-storetype", "PKCS12", "-file", f"{store}.csr")
gencert = ["-gencert", "-alias", "ca", "-keystore", "ca.p12", "-storetype", "PKCS12",
"-infile", f"{store}.csr", "-outfile", f"{store}.cer", "-validity", validity, *extensions]
if startdate:
gencert += ["-startdate", startdate]
tool(*gencert)
tool("-importcert", "-alias", "caroot", "-keystore", store, "-storetype", "PKCS12", "-file", "ca.cert", "-noprompt")
tool("-importcert", "-alias", alias, "-keystore", store, "-storetype", "PKCS12", "-file", f"{store}.cer", "-noprompt")
tool("-genkeypair", "-alias", "ca", "-dname", "CN=U15 Lab Test CA", "-ext", "bc:c", "-keyalg", "RSA",
"-keysize", "2048", "-validity", "2", "-keystore", "ca.p12", "-storetype", "PKCS12")
tool("-exportcert", "-alias", "ca", "-keystore", "ca.p12", "-storetype", "PKCS12", "-rfc", "-file", "ca.cert")
tool("-importcert", "-alias", "ca", "-keystore", "trust.p12", "-storetype", "PKCS12", "-file", "ca.cert", "-noprompt")
issue("server.p12", "server", "2")
issue("expired.p12", "server", "30", startdate="-400d")
hosts = Path(workdir) / "hosts"
hosts.write_text("127.0.0.1 good.test\n127.0.0.1 bad.test\n", encoding="utf-8")
return hosts
TLS_CASES = (
("tls-pos", "server.p12", "trust.p12", "good.test", ()),
("tls-trust", "server.p12", None, "good.test", ()),
("tls-expire", "expired.p12", "trust.p12", "good.test", ()),
("tls-san", "server.p12", "trust.p12", "bad.test", ()),
("tls-debug", "server.p12", "trust.p12", "good.test", ("-Djavax.net.debug=ssl:handshake",)),
)
def tls_mode(java, directory, java_home, workdir):
hosts = tls_prepare(java_home, workdir)
for kase, store, trust, host, extra in TLS_CASES:
command = [java, "-ea", "-Xmx64m", f"-Djdk.net.hosts.file={hosts}", *extra,
"-cp", directory, "ConnectionProbe", kase,
str(Path(workdir) / store), "null" if trust is None else str(Path(workdir) / trust), host]
result = subprocess.run(command, capture_output=True, text=True, timeout=60)
print(result.stdout, end="", flush=True)
if result.returncode != 0:
print(result.stderr, end="", flush=True)
result.check_returncode()
if f"PASS {kase}" not in result.stdout:
raise AssertionError(f"Missing assertion completion marker: {kase}")
if kase == "tls-debug":
# javax.net.debug writes to stderr in JDK 17; scan the combined output.
combined = result.stdout + result.stderr
for needle in ("Produced ClientHello", "Consuming ServerHello", "Negotiated protocol version: TLS"):
assert needle in combined, f"handshake debug missing {needle}"
picks = [line.strip() for line in combined.splitlines()
if any(t in line for t in ("Produced ClientHello", "Consuming ServerHello",
"Negotiated protocol version", "Fatal"))][:8]
for pick in picks:
print(f"DEBUG {pick[:180]}", flush=True)
sysctl_tcp_observation()
print("PASS tls: lab CA trust=200; default trust=PKIX path building failed; "
"expired=validity check failed; SAN mismatch=No subject alternative; debug shows handshake phases", flush=True)
def tcpstates_mode(java, directory):
sysctl_tcp_observation()
result = run_java(java, directory, "ConnectionProbe", ["tcpstates"], timeout=90)
print(result.stdout, end="", flush=True)
if result.returncode != 0:
print(result.stderr, end="", flush=True)
result.check_returncode()
assert "PASS tcpstates" in result.stdout, "missing tcpstates marker"
def reuse_mode(java, directory):
for kase in ("reuse-keepalive", "reuse-reqtimeout"):
result = run_java(java, directory, "ConnectionProbe", [kase], timeout=60)
print(result.stdout, end="", flush=True)
if result.returncode != 0:
print(result.stderr, end="", flush=True)
result.check_returncode()
if f"PASS {kase}" not in result.stdout:
raise AssertionError(f"Missing assertion completion marker: {kase}")
print("PASS reuse: keepalive reuse, restricted header, server-driven close, request timeout, fast refusal", flush=True)
def dnschain_mode(java, directory, workdir):
hosts = Path(workdir) / "hosts-chain"
hosts.write_text("127.0.0.1 chain.test\n", encoding="utf-8")
missing = Path(workdir) / "hosts-absent" # deliberately never created
for kase, hosts_file in (("dns-chain", hosts), ("dns-hostsmissing", missing)):
command = [java, "-ea", "-Xmx64m", f"-Djdk.net.hosts.file={hosts_file}",
"-Dnetworkaddress.cache.ttl=0", "-Dnetworkaddress.cache.negative.ttl=0",
"-cp", directory, "ConnectionProbe", kase]
result = subprocess.run(command, capture_output=True, text=True, timeout=30)
print(result.stdout, end="", flush=True)
if result.returncode != 0:
print(result.stderr, end="", flush=True)
result.check_returncode()
if f"PASS {kase}" not in result.stdout:
raise AssertionError(f"Missing assertion completion marker: {kase}")
print("PASS dnschain: hosts precedence, missing-name and missing-file failures, OS resolver observation", flush=True)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("mode", choices=["all", *NETWORK_MODES, *RESOURCE_MODES], nargs="?", default="all")
args = parser.parse_args()
home = java_home(parser)
java, javac = str(Path(home) / "bin/java"), str(Path(home) / "bin/javac")
version = subprocess.run([java, "-version"], capture_output=True, text=True, check=True, timeout=10)
print(version.stderr.strip(), flush=True)
if 'version "17.' not in version.stderr:
parser.error("This lab requires JDK 17; source baseline and runtime build are recorded separately.")
modes = [*NETWORK_MODES, *RESOURCE_MODES] if args.mode == "all" else [args.mode]
with tempfile.TemporaryDirectory(prefix="os-network-") as directory:
if any(mode in NETWORK_MODES for mode in modes):
subprocess.run([javac, "-encoding", "UTF-8", "-d", directory, str(HERE / "OsNetworkLab.java")],
check=True, timeout=30)
if any(mode in RESOURCE_MODES for mode in modes):
subprocess.run([javac, "-encoding", "UTF-8", "-d", directory, str(HERE / "OsResourceProbe.java")],
check=True, timeout=30)
if any(mode in U15_MODES for mode in modes):
subprocess.run([javac, "-encoding", "UTF-8", "-d", directory, str(HERE / "ConnectionProbe.java")],
check=True, timeout=30)
for mode in modes:
if mode in NETWORK_MODES and mode not in U15_MODES:
cases = ["dns-positive", "dns-negative", "dns-zero"] if mode == "dns" else [mode]
for case in cases:
hosts = Path(directory) / "hosts"
hosts.write_text("127.0.0.1 lab.test\n", encoding="utf-8")
command = [java, "-ea", "-Xmx64m", f"-Djdk.net.hosts.file={hosts}",
"-Dnetworkaddress.cache.ttl=600", "-cp", directory, "OsNetworkLab", case]
result = subprocess.run(command, capture_output=True, text=True, timeout=15)
print(result.stdout, end="", flush=True)
if result.stderr:
print(result.stderr, end="", flush=True)
result.check_returncode()
if f"PASS {case}" not in result.stdout:
raise AssertionError(f"Missing assertion completion marker: {case}")
elif mode == "tcpstates":
tcpstates_mode(java, directory)
elif mode == "tls":
tls_mode(java, directory, home, directory)
elif mode == "reuse":
reuse_mode(java, directory)
elif mode == "dnschain":
dnschain_mode(java, directory, directory)
elif mode == "thread":
thread_mode(java, directory)
elif mode == "signal":
signal_mode(java, directory)
elif mode == "fd":
fd_mode(java, directory)
elif mode == "mem":
mem_mode(java, directory)
elif mode == "perm":
perm_mode(java, directory)
elif mode == "disk":
disk_mode(java, directory)
print("PASS runner: temporary classes/hosts removed; no external service or system configuration used", flush=True)
if __name__ == "__main__":
main()