Repository navigation
fix(ipc): bound the kernel IPC transport against abuse - #1379
Conversation
5b682d0 to
6b0170a
Compare
|
Thanks @Mihir-Rabari for the focused IPC hardening and the authenticated-wire tests. The contributor credit should remain with this change when it is rebased or split. Review result: unresolved — not ready to merge into current Before: the authenticated endpoint still inherited the transport defaults for message/metadata sizing, per-connection stream concurrency, and idle connection lifetime. A local process could therefore send oversized frames, open unbounded streams, or keep an idle connection around indefinitely. After: Required follow-up:
Hosted CI is currently green on Ubuntu, macOS, and Windows, including the dedicated authenticated IPC job, code quality, compatibility, and build/test checks. No Claude review or correctness feedback is attached. Related searches found no true duplicate; #1378 is the source issue, and #1225/#575/#657/#363 are adjacent, distinct work. |
bdcf620 to
bc9aa3b
Compare
5165b66 to
dca81c3
Compare
|
Rebased the six-file IPC change onto current dev. The client channel now applies maxInboundMetadataSize as well as maxInboundMessageSize, with an authenticated oversized-response-metadata regression and a normal-channel control. The first hosted run showed Netty reporting INTERNAL for the metadata refusal on Ubuntu/Windows; the test now accepts that transport refusal as well as RESOURCE_EXHAUSTED, and fresh authenticated IPC CI is running on the updated head. |
|
@claude review this PR - security-sensitive changes, part of the backlog sweep. Focus on auth, signing, path handling, secrets, and injection surfaces. |
|
Claude finished @fluck-boss's task in 3m 31s —— View job Security review - kernel IPC transport limits
Verdict: the change is sound and does not weaken auth. The metadata asymmetry from the previous review round is fixed ( Findings below, ordered by what I'd want fixed before merge. 1. Both message-cap tests pass against a mutant that deletes the caps
The oversized-metadata test gets this right and is the model to copy: it configures Given this repo's AGENTS.md standard ("mutation-verified: reversing each one fails a named test"), this is the one I'd hold the merge on. 2. The server-side (request) metadata cap has no test at allThe PR comment says the new client-side regression lands "alongside the existing server-side/request metadata coverage", but I could not find any. Grepping 3. The per-connection stream cap is bypassable by opening more connections, and the KDoc doesn't say so
That doesn't make the cap useless (it bounds a single accidental runaway, and it does advertise via SETTINGS), but it's a mitigation rather than a bound, and the "Deliberately not bounded here" paragraph at 4. Exceeding the stream cap is a silent client-side queue, not an error - and nothing in production sets a deadlinegRPC-java's client queues streams above the peer's Not a security hole, but a diagnosability trap worth a sentence in the KDoc, and an argument for either deadlines on kernel unary calls or a server-side log when the cap is hit. 5. Published-jar binary compatibilityAGENTS.md records that Checked and clear
Pre-existing, adjacent - flagging, not attributing to this PR
|
|
Verified the review claims against the real code:
|
|
@claude please review this exact head (a411903) for merge into dev. Check that the change is correct and complete for its stated goal, that tests cover the risk being fixed (a test that passes without the fix is a finding), and flag any security issues (secrets, injection, auth or approval bypass, path handling). Keep the review focused on this diff. |
|
Claude finished @fluck-boss's task in 5m 48s —— View job Security + correctness review -
|
| Line | Pinned by | Mutant (delete the line) |
|---|---|---|
applyToServer message |
oversized request is refused... (:60, 64 KiB cap, 64 KiB+1 payload) |
publish succeeds, assertFailsWith fails ✅ |
applyToServer metadata |
nothing | whole suite still green ❌ |
applyToServer concurrent calls |
a connection cannot exceed its concurrent stream cap... (:191, cap 2, flood 5) |
sampling loop at :205 observes 5 > 2 ✅ |
applyToServer idle |
an idle connection is reaped... (:241) |
channel stays READY, withTimeout fires ✅ |
applyToChannel message |
oversized response is refused at the child reader... (:111) |
client accepts 64 KiB+1, assertFailsWith fails ✅ |
applyToChannel metadata |
oversized response metadata... (:170, 1 KiB cap, 2 KiB header) |
default 8 KiB accepts it ✅ |
1. The server-side request-metadata cap has no test - hold on this
applyToServer sets .maxInboundMetadataSize(maxInboundMetadataBytes) (IpcTransportLimits.kt:53) and nothing exercises it. Grepping every reference to the four fields across modules/ and composeApp/ turns up maxInboundMetadataBytes exactly once outside the class itself, at IpcTransportLimitsTest.kt:170, and that is the client leg. Delete line 53 and the entire suite passes.
This is the same gap @fluck-boss confirmed and asked for in the comment above, and it is still absent at this head. It matters more than its size suggests, for two reasons:
- It is the asymmetric one. The client metadata cap defends against a hostile kernel; the server metadata cap defends against a hostile local peer, which is the threat model the class KDoc actually states ("the IPC endpoint is reachable by every process on the machine"). The leg any process on the box can drive is the unpinned one.
- The PR description says the client regression lands "alongside the existing server-side/request metadata coverage". There is no such coverage - the nearest neighbours are
IpcTlsIdentityTest's oversized endpoint material andIpcTransportTest's oversized version marker, neither of which touchesmaxInboundMetadataSize. So the merge record currently claims a bound that no test holds.
The mirror of the test at :139 is the shape: IpcTestServer(service, limits = IpcTransportLimits(maxInboundMetadataBytes = 512)), a ClientInterceptor that puts an oversized header on the request, and an assertion that the refusal is transport-level rather than UNAUTHENTICATED - which also proves the useful security property, that a header flood is not laundered into an identity failure.
2. require(maxConnectionIdleMillis > 0) is weaker than the effective contract
IpcTransportLimits.kt:46 accepts any positive value, but NettyServerBuilder.maxConnectionIdle does not honour the whole range: values below one second are clamped up to one second, and values at or beyond its "as large as infinite" threshold (on the order of 1000 days) are converted to disabled. So IpcTransportLimits(maxConnectionIdleMillis = 5) silently becomes 1000, and a caller reaching for a very large sentinel silently turns off the one bound this class was added to introduce - with a passing require either way.
The shipped default (120_000) and the test's 1_000 are both inside the honoured range, so nothing is broken today; this is about the constructor's promise to a future caller, in a module whose whole job is pinning bounds that the library "happens to ship defaults for". Worth tightening the require to a documented range, or naming the floor in the KDoc. Worth confirming the exact thresholds against the pinned grpc = "1.84.0" rather than taking my reading of NettyServerBuilder on trust - I could not run a build here.
Notes - non-blocking, but worth not carrying into the merge record as-is
The KDoc promises RESOURCE_EXHAUSTED more firmly than the wire delivers. IpcTransportLimits.kt:19 says "Oversize requests and responses fail with RESOURCE_EXHAUSTED", while the test comment at :76-84 documents - honestly, and clearly from experience - that the request leg can surface as UNAVAILABLE, CANCELLED or INTERNAL depending on where the teardown catches the writer. The test comment is the accurate one. A consumer coding catch (RESOURCE_EXHAUSTED) to tell "my frame was too big" from "the connection dropped" will be wrong on some platforms, so the class doc should say what the test learned. Same applies to the response-metadata assertion, which already has to accept INTERNAL (:176).
Correcting the previous round on auth ordering. Last round I wrote that the caps sit "before ProcessTokenServerInterceptor, so a header flood is refused pre-auth". That is right for metadata - HTTP/2 header-size enforcement precedes stream creation - and not right for messages: maxInboundMessageSize fires in MessageDeframer when the length prefix is read, which is after interceptCall has run and therefore after the token has been validated against the registry. The class KDoc's own wording ("before any service handler runs", :20) is the correct and defensible claim, and the security property that matters still holds - the body is never buffered and no handler executes. But the stronger pre-auth claim should not be inherited by this merge; an oversize request does cost one token validation.
One assertion in the stream test cannot fail. :231 asserts refusals.none { UNAUTHENTICATED || PERMISSION_DENIED } over a list that, under correct behaviour, is empty or holds only the CANCELLEDs produced by flood.cancel() - because gRPC's client queues streams above the peer's advertised MAX_CONCURRENT_STREAMS rather than failing them, so the excess streams are never refused client-side at all. The load-bearing proof in that test is the sampling loop at :204-210 against bus.activeSubscribers, which is genuinely discriminating and does prove the server-side bound. I agree with @fluck-boss that the queuing is standard SETTINGS behaviour and not a defect; the narrower point is that the comment at :230 ("refused abuse at the transport and never masqueraded as an auth failure") describes an observation the test did not make.
Connection count is still missing from the "deliberately not bounded" list (:25-30). That paragraph names connection age and keepalive rates as considered-and-excluded, which reads as an exhaustive account, and omits the one dimension that bypasses the cap directly above it: nothing bounds how many connections a peer opens, and maxConnectionIdle does not help against a flooder that always has an RPC in flight. Documenting it is a perfectly good answer - the cap still bounds an accidental runaway and advertises via SETTINGS - but leaving it unmentioned next to a list of rejected bounds implies it was covered.
Verified clear
- No bypass. Every production builder goes through
IpcAddressResolver:KernelBootstrap.kt:490(kernel server),ChildProcessBootstrap.kt:79/:87(child server and kernel client),SpawnIpcSecurity.kt:46(host client). All take the defaultIpcTransportLimits(), so all four get the caps. The remaining directNettyChannelBuilderuses are the threecomposeApp/src/desktopTestbridge tests plusIpcTlsIdentityTest- tests only. - Tests run on the real authenticated wire, and the service-level interceptor does not sidestep auth.
BossIpcServer.buildAndStartinstallsProcessIdentityInterceptorthroughbuilder.intercept(...)(:68), which wraps outside anythingServerInterceptors.interceptattaches to a service - so the oversized-headers harness at:142is layered inside a genuine token check, andchannelForissues a real token for every channel. TheIpcTestServerchange is a pure parameter addition withclientLimitsdefaulting to the server's, and the response-metadata test's use of a default-limits control channel on the same oversized-header server (:179) is a good touch: it proves the refusal came from the configured 1 KiB cap and not from the header's mere presence. IpcAddressResolverrewrite is behaviour-preserving.git diff --ignore-all-spaceshows only the two signatures, two KDoc lines, andreturn limits.applyTo*(builder)appended - the 130-line churn is re-indentation to hoist thewheninto a local. Address parsing, the loopback requirement,validateProcessIdentifierandsecureSocketFile's owner-onlychmodare untouched.- Secrets, injection, path handling. Nothing in the diff logs token, certificate or credential material, and nothing widens what is logged. No command, path, or query is built from peer input; the only peer-influenced string added anywhere is the test-only fixed literal
"x".repeat(2_048). No new file or socket path is constructed. - Idle reap has no health-surface fallout.
BossIpcClient.isConnected(:87) isstate == READYand a 2-minute GOAWAY will now flip it false, but grepping the tree shows no production consumer - the otherisConnectedhits are unrelated (NetworkMonitorService,CoreAuthService,PluginProcessMonitor). Reconnect-on-next-call is proven at:262. - Binary compatibility - correcting last round's point 5. I flagged the new trailing default parameters as a
NoSuchMethodErrorrisk for theboss-microkernel-runtimeconsumer. Checking rather than assuming: no module undermodules/applieslibs.plugins.mavenPublish(onlyplugin-platform/*does), andpublish-maven-central.ymllists no IPC package - so nothing in this repo publishesboss-ipc, there is no publish task to break, and the absence of an.apidump is consistent with that. The residual risk is only an out-of-band jar consumer; if one exists,@JvmOverloadson the two constructors makes it free, but this is not a merge blocker.
What I could not run
Gradle invocations are outside my allowed tools here (./gradlew :boss-ipc:compileKotlin was denied, and there is no populated Gradle cache in this checkout), so I could not execute :boss-ipc:test --tests 'ai.rever.boss.ipc.IpcTransportLimitsTest'. Every mutation verdict in the table above is read from the sources, not observed. The PluginUIServiceGrpc.java generic mismatch from the first round looks like a stale-generated-source artefact of that local tree rather than a product fault, consistent with hosted CI being green - the authenticated IPC job is the one to trust here.
· branch agent/flag-kernel-ipc-limits
fluck-boss
left a comment
There was a problem hiding this comment.
Thanks @Mihir-Rabari - Claude calls this correct and nearly complete. One item still open on a411903, the same one I flagged earlier as needing you: the server-side request-metadata cap (maxInboundMetadataBytes on the inbound leg) has no test. Add one - a server with a small cap and a client sending metadata one byte over, failing if the pin is removed. Optional while you're there: require(maxConnectionIdleMillis > 0) is weaker than the effective contract.
Changes requested + draft until then. Push, get CI green, mark ready for review and it goes straight to merge.
a411903 to
601aa2d
Compare
The microkernel IPC endpoint is reachable by every local process. Pinned TLS and per-call process tokens prove the endpoint and the caller, but nothing bounded what a peer could demand from the transport itself: one connection could hold unlimited concurrent streams (gRPC defaults to Integer.MAX_VALUE) and a connection with no RPCs in flight was never closed (idle eviction ships disabled), so a stalled or hostile peer kept its slot forever. The 4 MiB message and 8 KiB metadata caps existed only as undocumented library defaults. IpcTransportLimits now applies refuse-style bounds at both ends of the wire. Requests and responses over the cap fail with RESOURCE_EXHAUSTED before any handler runs. Excess concurrent streams on one connection are refused, and well-behaved clients additionally see the cap advertised in HTTP/2 SETTINGS. A connection idle for two minutes gets a graceful GO_AWAY that clients survive by reconnecting on demand. Total connection age is deliberately left unbounded because PluginUI and state watch streams are designed to stay open indefinitely, and active RPCs already exempt a connection from the idle reap. Adversarial tests pin each contract on the real authenticated transport: oversize request and response frames are refused while well-formed traffic on the same channel still succeeds, a five-stream flood on one connection never exceeds the configured cap server-side and recovers once streams close, and a stalled connection is reaped by the server with the next call transparently reconnecting. Post first-CI corrections: the oversize request test pinned observable surface that the transport's own teardown race owns, not the contract. First, the exact client-observed status of that mid-write teardown is platform timing - CI surfaced CANCELLED on ubuntu (write interrupted in flight) and INTERNAL on windows (netty escalates the teardown into a connection GOAWAY) while macos landed the server's RESOURCE_EXHAUSTED trailers. The refusal held on every leg (the oversized frame never reached a handler), so the pin is now the same transport-level guard the stream flood test holds. Second, the follow-up well-formed call raced the replacement transport that a GOAWAY teardown leaves behind - the second windows run showed a single-shot call surfacing that race as UNAVAILABLE without any lockout - so recovery is now proven across that bounded window and still fails on a genuine lockout when the deadline expires. Test-only changes.
… tests Both oversize tests sized their payloads against DEFAULT_MAX_INBOUND_MESSAGE_BYTES, which equals gRPC's built-in 4MiB cap: deleting the explicit pin would leave the library refusing the same payload and the tests would pass with the bound gone. Caps pinned at 64KiB with payloads one byte over make removal visible.
601aa2d to
3233d93
Compare
|
Both review points are addressed:
The branch was also rebased onto current dev (9e41738); range-diff confirms no other changes. The first hosted run (601aa2d) failed Hosted checks green on 3233d93. Marking ready for review. |
|
@claude please review this exact head (3233d93) for merge into dev. Check that the change is correct and complete for its stated goal, that tests cover the risk being fixed (a test that passes without the fix is a finding), that the earlier review findings on this PR are addressed, and flag any security issues (secrets, injection, auth or approval bypass, path handling). Keep the review focused on this diff. |
|
Claude finished @fluck-boss's task in 3m 56s —— View job Review of
|
| Builder line | Pinned by | Mutant (delete the line) |
|---|---|---|
applyToServer message |
oversized request is refused... (:62, cap 64 KiB, payload 64 KiB+1) |
4 MiB library default admits it, assertFailsWith fails ✅ |
applyToServer metadata |
oversized request metadata is refused at the server... (:113, cap 1 KiB, header value 1 KiB+1) |
8 KiB default admits it, assertFailsWith fails ✅ |
applyToServer concurrent calls |
a connection cannot exceed its concurrent stream cap... (:253, cap 2, flood 5) |
sampling loop at :271 observes 5 > 2 ✅ |
applyToServer idle |
an idle connection is reaped... (:306, 1 s window) |
channel stays READY, withTimeout fires ✅ |
applyToChannel message |
oversized response is refused at the child reader... (:173, reader cap 64 KiB, default-limits server) |
client accepts 64 KiB+1 ✅ |
applyToChannel metadata |
oversized response metadata... (:206, reader cap 1 KiB, 2 KiB header) |
8 KiB default accepts it ✅ |
The new server-metadata test: checked properly, it does discriminate
This is the one I held the merge on last round, so I sized it rather than eyeballing it. The server pins 1024 and the injected header value alone is 1025 bytes, so on HPACK accounting (name + value + 32) that single header is 1074 - over the configured cap by itself, which is what makes the test independent of how the transport counts everything else. The rest of the header list is small and bounded: the process token is 64 hex chars (ProcessTokenRegistry.issue formats 32 random bytes, ProcessTokenRegistry.kt:32) plus the usual pseudo-headers, te, content-type, user-agent - call it ~1.5 KiB total. Comfortably under gRPC's 8 KiB default, so deleting IpcTransportLimits.kt:59 admits the request and assertFailsWith fails. That is the property the last two rounds were missing.
Two details worth recording because they are easy to get wrong and this got both right:
- The refusal traces to the server's setting, not the client's.
channelFordefaultsclientLimitsto the server's limits, so the writer channel also carriesmaxInboundMetadataSize = 1024- but that bounds what the client will accept inbound, and gRPC does not bound outbound headers against it. What refuses the write is the server'sSETTINGS_MAX_HEADER_LIST_SIZE, which is exactly the line under test. No accidental double-pin. - The control call (
:166) opens a freshBossIpcClient, so it survives the case where netty escalates the header refusal into a connection-level GOAWAY. The comment claims only what that proves ("a peer sending ordinary headers is admitted by the same capped server"), not same-connection recovery.
The require floor (:49) matches grpc-netty's actual behaviour: NettyServerBuilder.maxConnectionIdle clamps anything under MIN_MAX_CONNECTION_IDLE_NANO (1 s) up to 1 s, so the old > 0 really did accept values that widened silently. The test's 1 s window sits exactly on the new floor and still passes construction.
Residual notes - none blocking, ordered by how much I'd want them
1. The idle window is now guarded at the floor and still open at the ceiling. maxConnectionIdle has a second silent transform at the top of its range: at or beyond AS_LARGE_AS_INFINITE (1000 days) it converts the value to disabled, not to a very long window. So IpcTransportLimits(maxConnectionIdleMillis = Long.MAX_VALUE) passes the new require and turns off the one bound this class was added to introduce. Nothing does this today (LONG_IDLE_MILLIS is 60 s), but it is the same class of silent widening that was just closed on the other end, in a class whose entire job is honest pins - a matching require(maxConnectionIdleMillis < MAX_CONNECTION_IDLE_MILLIS) with a 1000-day constant would make the contract symmetric. Worth confirming the exact threshold against the pinned grpc = "1.84.0" rather than my reading.
2. The class KDoc still promises RESOURCE_EXHAUSTED more firmly than the wire delivers. IpcTransportLimits.kt:19 says "Oversize requests and responses fail with RESOURCE_EXHAUSTED before any service handler runs", while the test comments at :81-91 and :152-157 document - from experience, and accurately - that the request leg surfaces as UNAVAILABLE, CANCELLED or INTERNAL depending on where the teardown catches the writer, and the response-metadata assertion at :243 has to accept INTERNAL. The test comment is the true one. A consumer coding catch (RESOURCE_EXHAUSTED) to tell "my frame was too big" from "the connection dropped" will be wrong on some platforms. One sentence in the KDoc closes the gap between what the class promises and what its own suite learned. Carried over from last round, still open.
3. Connection count is still absent from the "Deliberately not bounded here" paragraph (:25-30). That paragraph names connection age and keepalive rates as considered-and-excluded, which reads as an exhaustive account of what was thought about, and omits the one dimension that walks straight around the cap directly above it: nothing bounds how many connections a peer opens, and maxConnectionIdle does not help against a flooder that always has an RPC in flight (N connections x 128 streams). Documenting it is a perfectly good answer - the cap still bounds an accidental runaway and advertises via SETTINGS - but leaving it unlisted next to a list of rejected bounds implies it was covered. Carried over from two rounds ago, still open.
4. One assertion in the stream test cannot fail (:298). refusals.none { UNAUTHENTICATED || PERMISSION_DENIED } runs over a list that, under correct behaviour, is empty: gRPC's client queues streams above the peer's advertised MAX_CONCURRENT_STREAMS rather than failing them, and flood.cancel() raises CancellationException, which the handler's as? StatusException drops. The load-bearing proof in that test is the sampling loop at :271-277 against bus.activeSubscribers, which genuinely discriminates. I agree with @fluck-boss that the queuing is standard SETTINGS behaviour and not a defect; the narrow point is that the comment at :297 describes an observation the test did not make.
5. Binary compatibility - re-opening my own correction from last round. I checked and confirmed again that no module under modules/ applies a publish plugin and publish-maven-central.yml lists no IPC package, so there is no in-repo publish task to break. But this repo's own AGENTS.md states that boss-ipc/boss-ui-sdk "publish as upstream jars consumed by the standalone boss-microkernel-runtime repo", and boss-ipc/build.gradle.kts does carry group/version = "1.2.0" - so an out-of-band consumer plausibly exists. The new trailing default parameters delete the JVM descriptors BossIpcClient(String, IpcClientCredentials, List), BossIpcServer(String, ProcessTokenRegistry, IpcTlsIdentity), IpcAddressResolver.configureServerBuilder(String) and configureChannelBuilder(String); a consumer compiled against the published 1.2.0 jar gets NoSuchMethodError, not a compile error, and there is no .api dump here to catch it. @JvmOverloads on the two constructors and the two resolver functions makes this free. Worth a yes/no from whoever owns the runtime repo (source build or jar?) rather than an assumption either way.
Verified clear
- No bypass surface. Every production builder still goes through
IpcAddressResolver, and all four production instantiations take the defaultIpcTransportLimits():KernelBootstrap.kt:493,ChildProcessBootstrap.kt:79/:87,SpawnIpcSecurity.kt:46. The only remaining directNettyChannelBuilder/NettyServerBuilderuses in the tree are the fourcomposeApp/src/desktopTestbridge tests,IpcTlsIdentityTest, andIpcTestServer.channelWithToken- the intentional negative-test escape hatch. - Tests run on the real authenticated wire.
IpcTestServerissues a genuine token per channel, andBossIpcServer.buildAndStartinstallsProcessIdentityInterceptorviabuilder.intercept(...), which wraps outside anythingServerInterceptors.interceptattaches to a service - so both metadata harnesses are layered inside a real token check rather than around it. - Auth ordering. Metadata caps are enforced at HTTP/2 header decode, before stream creation and therefore before the token interceptor; message caps fire in
MessageDeframeron the length prefix, which is afterinterceptCallbut still before any handler and before the body is buffered. The KDoc's own wording ("before any service handler runs") is the defensible claim and is the one used. Refusals are deliberately kept offUNAUTHENTICATED/PERMISSION_DENIED, asserted in both request-side tests, so abuse cannot be laundered into an identity failure. - Secrets, injection, path handling. No token, certificate or credential material is logged, and the diff adds no logging at all. Nothing builds a command, path, file name or query from peer input; the only peer-influenced strings introduced anywhere are the two test-only fixed literals (
"x".repeat(...)). IpcAddressResolverrewrite is behaviour-preserving.git diff --ignore-all-spacereduces the 130-line churn to exactly two signatures, two KDoc lines and tworeturn limits.applyTo*(builder)statements. Address parsing, the loopback requirement,validateProcessIdentifierandsecureSocketFile's owner-onlychmodare untouched.- Idle reap has no health-surface fallout.
BossIpcClient.isConnectedisstate == READYand a 2-minute GOAWAY flips it false, but it has no production consumer. Reconnect-on-next-call is proven at:329. - Style gates. No line over 120 columns anywhere under
modules/boss-ipc/src, no trailing whitespace, no U+2014 in the new prose - consistent with the detektMaxLineLengthfix at85ffc16.
Pre-existing, adjacent - flagging, not attributing to this PR
configureServerBuilder still does an unconditional File(parsed.path()).delete() on a unix:// path that parseAddress does not confine to ipcDir, unlike the careful PROCESS_ID_REGEX guard on the generated path, and kernelAddress() reads BOSS_KERNEL_IPC_ADDR straight from the environment. Environment control largely implies process control so the severity is low, but unix:// deserves the confinement check tcp:// gets for loopback. Related: ipcDir is created with mkdirs() and no owner-only mode, where this repo's run-directory and policy writers pin 0600/owner-only. Both deserve their own issue rather than scope creep here.
What I could not run
Gradle invocations are outside my allowed tools in this environment, so :boss-ipc:test --tests 'ai.rever.boss.ipc.IpcTransportLimitsTest' was not executed - every mutation verdict in the table above is read from the sources and from grpc-netty's builder semantics, not observed. Hosted CI reported green on this head including the dedicated authenticated IPC job; that is the run to trust for execution.
· branch agent/flag-kernel-ipc-limits
|
Merging: ubuntu and windows are green on 3233d93. Claude's re-review finds both open items fixed and no blocking finding: every builder limit is now pinned by a test that fails if the line is removed, including the server-side metadata cap. My diff read agrees: the limits go through |
* fix(ipc): bound the kernel IPC transport against abuse The microkernel IPC endpoint is reachable by every local process. Pinned TLS and per-call process tokens prove the endpoint and the caller, but nothing bounded what a peer could demand from the transport itself: one connection could hold unlimited concurrent streams (gRPC defaults to Integer.MAX_VALUE) and a connection with no RPCs in flight was never closed (idle eviction ships disabled), so a stalled or hostile peer kept its slot forever. The 4 MiB message and 8 KiB metadata caps existed only as undocumented library defaults. IpcTransportLimits now applies refuse-style bounds at both ends of the wire. Requests and responses over the cap fail with RESOURCE_EXHAUSTED before any handler runs. Excess concurrent streams on one connection are refused, and well-behaved clients additionally see the cap advertised in HTTP/2 SETTINGS. A connection idle for two minutes gets a graceful GO_AWAY that clients survive by reconnecting on demand. Total connection age is deliberately left unbounded because PluginUI and state watch streams are designed to stay open indefinitely, and active RPCs already exempt a connection from the idle reap. Adversarial tests pin each contract on the real authenticated transport: oversize request and response frames are refused while well-formed traffic on the same channel still succeeds, a five-stream flood on one connection never exceeds the configured cap server-side and recovers once streams close, and a stalled connection is reaped by the server with the next call transparently reconnecting. Post first-CI corrections: the oversize request test pinned observable surface that the transport's own teardown race owns, not the contract. First, the exact client-observed status of that mid-write teardown is platform timing - CI surfaced CANCELLED on ubuntu (write interrupted in flight) and INTERNAL on windows (netty escalates the teardown into a connection GOAWAY) while macos landed the server's RESOURCE_EXHAUSTED trailers. The refusal held on every leg (the oversized frame never reached a handler), so the pin is now the same transport-level guard the stream flood test holds. Second, the follow-up well-formed call raced the replacement transport that a GOAWAY teardown leaves behind - the second windows run showed a single-shot call surfacing that race as UNAVAILABLE without any lockout - so recovery is now proven across that bounded window and still fails on a genuine lockout when the deadline expires. Test-only changes. * fix(ipc): cap response metadata at the client reader * test(ipc): accept transport-specific metadata refusal codes * test(ipc): pin message caps below the library default in the oversize tests Both oversize tests sized their payloads against DEFAULT_MAX_INBOUND_MESSAGE_BYTES, which equals gRPC's built-in 4MiB cap: deleting the explicit pin would leave the library refusing the same payload and the tests would pass with the bound gone. Caps pinned at 64KiB with payloads one byte over make removal visible. * fix(ipc): enforce grpc-netty's one-second idle floor in require (per review) * test(ipc): cover the server-side inbound metadata cap (per review) --------- Co-authored-by: Instinct Agent <agent@instinct.com>
Closes #1378
Cap maxInboundMessageSize on both ends of the kernel IPC wire from the
negotiated transport limits, so oversize frames die with a protocol-level
RESOURCE_EXHAUSTED at the deframer, before any handler runs or bytes are
buffered; refuse stream floods at the transport while streams are being
established, rather than masquerading the cap as an auth failure; and reap
connections idle past the deadline so their slots return to the pool.
The limits suite pins the contract against the real authenticated wire, both
halves of it: abuse is refused AND well-formed traffic keeps flowing before,
during and after each refusal - including across the transparent reconnect a
mid-write stream teardown forces.