Feature/atomicserial marshal delegate#319
Merged
Merged
Conversation
…(Phase A: A1-A3) Introduce the per-package MarshalDelegate access broker so package-private @AtomicSerial classes can participate in atomic marshalling without reflective field access, setAccessible-on-fields, or opens (SOW-AtomicSerial-Delegate-Marshalling). A1 (jgdms-platform, org.apache.river.api.io): - MarshalDelegate: serialForm(Class) / serialize(Class,PutArg,Object) / create(Class,GetArg) / default packageName(). In-package dispatch, zero reflection -> needs no reflection permission under POLP. - MarshalDelegates.delegateFor(Class): loader-scoped resolution via org.apache.river.resource.Service, selecting the provider co-loaded with the target (same runtime package, SOW 3.1) AND matching its package name (one loader may host delegates for several packages; the OSGi path also ignores the loader, so this re-imposes scoping). ClassValue-cached with a NONE sentinel for negatives; ServiceConfigurationError is swallowed -> reflective fallback. A2 (JOSS engine): route the four reflective @AtomicSerial sites through the delegate, keeping the existing reflective path as fallback - ObjOutputStream.fields()/writeHierarchy(), AtomicMarshalInputStream.fields()/ instantiateAtomicSerialOrDiscard(). A3 (DER codec): same for SchemaGenerator.invokeSerialForm and ObjectCodec's invokeSerialize plus all three (GetArg) construction sites. No delegates are registered yet, so delegateFor returns null everywhere and the reflective fallback is always taken: behaviour is identical to before. Verified by a clean platform+der compile (release 21) and the jgdms-platform unit suite (297 tests, 0 failures) under JDK 21. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…proxy (Phase A: A4)
Hand-written MarshalDelegate implementations + META-INF/services registrations
for the three proxy packages whose non-public @AtomicSerial classes the engines
previously could only reach via setAccessible:
- org.apache.river.fiddler.proxy.FiddlerProxyMarshalDelegate (10 classes)
- org.apache.river.norm.proxy.NormProxyMarshalDelegate
abstract AbstractProxy (serialForm/serialize, no create -- never a leaf);
@stateless AdminProxy/NormProxy + their Constrainable* (empty serialForm,
no-op serialize, create via (GetArg)); stateful SetProxy/ConstrainableSetProxy/
GetLeasesResult/ProxyVerifier.
- org.apache.river.mercury.proxy.MercuryProxyMarshalDelegate (12 classes, incl.
@stateless ConstrainableMailboxAdminProxy/ConstrainableMailboxProxy)
Each dispatches serialForm/serialize/create in-package by exact concrete Class --
zero reflection, no setAccessible, no field access.
Verified under DirtyChai (OpenJDK 27-internal, Security-Manager-capable): all
three compile; Service discovery resolves each by defining-loader + package name;
a fiddler FiddlerRenewResults atomic round-trip passes through the delegate;
@stateless AdminProxy.serialForm dispatches to empty while stateful SetProxy /
RemoteEventData dispatch to non-empty.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…ve fallback (Phase A: A5) Adds an opt-in strict mode (MarshalDelegates.isStrict()/setStrict(); system property org.apache.river.api.io.marshalDelegate.strict, default off) that forbids the setAccessible escalation on the reflective fallback path. When strict, a non-public @AtomicSerial class / member / (GetArg) ctor with no MarshalDelegate is rejected with a clear "add a MarshalDelegate" message instead of being reached via setAccessible. A genuinely public class with public members still works with no delegate (the SOW position), so strict mode only forces delegates where setAccessible was actually required. Guards the 7 @AtomicSerial contract reflective sites (all on the no-delegate fallback): ObjOutputStream.fields()/writeHierarchy(), AtomicMarshalInputStream .fields(), AtomicSerial.Factory.instantiate (the (GetArg) ctor -- public and package-private cases), SchemaGenerator.invokeSerialForm, ObjectCodec .invokeSerialize, ObjectCodec.findGetArgConstructor. Each: if strict and the member needs setAccessible -> throw; else skip setAccessible in strict (legacy mode keeps it). Unrelated setAccessible sites (legacy field injection, readObject -style methods, @ReadInput readers) are untouched. This encodes the SOW §11 "fallback disabled, still green" guarantee in the engine rather than only testing it. The global stopgap is NOT removed -- many non-public @AtomicSerial classes still lack delegates (norm-service, mahalo, phoenix, api.io serializers, vr, mercury EventID/ServiceRegistration); strict mode is the forcing function to surface them. Verified under DirtyChai (OpenJDK 27-internal): platform+der compile; legacy mode unchanged (fiddler atomic round-trip + norm/mercury dispatch green); strict mode green for the delegated packages (delegate carries the full round-trip, no setAccessible) AND correctly blocks a non-delegated non-public class (org.apache.river.norm.ClientLeaseWrapper). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Reviews 24eb294 (A1-A3), 3af27e7 (A4), f1cd2c8 (A5) against SOW-AtomicSerial-Delegate-Marshalling.md (section 11). Verdict: faithful and high quality -- loader-scoped resolution, codec-agnostic dispatch in both engines, strict-mode fallback gating, and no field access are all correct. One must-fix gap: org.apache.river.mahalo.proxy has no delegate but has package-private @stateless ConstrainableTxnMgrProxy / ConstrainableTxnMgrAdminProxy, so strict-mode mahalo PrepareAndCommitExceptionTest fails on their (GetArg) ctor; fix is a MahaloProxyMarshalDelegate mirroring norm. Two minor non-blocking notes (ungated @AtomicExternal ctor; dead read-side fields(Class) dispatch). The strict-mode verification run is still to be demonstrated. Co-Authored-By: Claude Opus 4.8 <[email protected]>
… batch 2) NormMarshalDelegate + META-INF/services for the norm service's three package-private @AtomicSerial state classes (ClientLeaseWrapper, CreateLeaseSet, LeaseSet) -- all complete and concrete; CreateLeaseSet's (GetArg) ctor is protected (reachable in-package). Dispatches serialForm/serialize/create in-package, no reflection. Constraint observed while scoping the batch: a package delegate is resolved for EVERY @AtomicSerial class in its package (public and non-public), so a package is only delegatable when ALL its @AtomicSerial classes are complete. Hence org.apache.river.lookup.util and net.jini.core.transaction.server are blocked by incomplete PUBLIC classes (ConsistentMap lacks serialForm/serialize; ServerTransaction has serialize but no serialForm) -- deferred. Verified under DirtyChai (strict mode): delegateFor(ClientLeaseWrapper) resolves NormMarshalDelegate and dispatches serialForm; all four delegated packages pass; a non-delegated non-public class (ConsistentMapEntry) is correctly blocked. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…p.util delegate Refines the MarshalDelegate SPI so a delegate declares the exact classes it serves (Class<?>[] servedClasses() + default serves(Class)), and MarshalDelegates.delegateFor returns it only for those. A class no delegate serves -- a fully public @AtomicSerial class, or an incomplete/not-yet-migrated class sharing a delegated package -- resolves to null and uses the reflective path. This makes the SOW's "public @AtomicSerial classes need no delegate" literally true and unblocks MIXED packages: a delegate can serve just its non-public classes without being forced to handle (and thus blocked by) public or incomplete ones in the same package. - MarshalDelegate: add servedClasses()/serves(); resolver matches loader + serves() instead of loader + packageName(). - Retrofit the four existing delegates (fiddler.proxy, norm.proxy, mercury.proxy, norm) with SERVED arrays -- behaviour unchanged (their packages are fully complete). - New LookupUtilMarshalDelegate serves only package-private ConsistentMapEntry; public ConsistentSet and still-incomplete public ConsistentMap stay on the reflective path. (net.jini.core.transaction.server needs no delegate: TransactionManager is an interface, so nested TransactionConfig is implicitly public.) Verified under DirtyChai (strict mode): all six delegated paths resolve and dispatch; the fiddler atomic round-trip passes; serves() scoping confirmed (ConsistentMapEntry resolves its delegate while public ConsistentSet in the same package resolves to null); a non-delegated non-public class (mercury.EventID) is still blocked. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
….proxy (review must-fix) Addresses the Phase A review's one must-fix: org.apache.river.mahalo.proxy had no delegate, so under strict mode the package-private @stateless leaves ConstrainableTxnMgrProxy / ConstrainableTxnMgrAdminProxy fail on their package-private (GetArg) constructor (strictBlockCtor), breaking the §11 mahalo verification test. MahaloProxyMarshalDelegate serves all five @AtomicSerial classes of the package: ProxyVerifier, TxnMgrProxy, TxnMgrAdminProxy (public classes but with package-private (GetArg) ctors -> need in-package read construction) and the two @stateless Constrainable* leaves (empty serialForm, no-op serialize, create via their package-private ctor). + META-INF/services entry. Compiles clean under DirtyChai. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
… validates the contract (Phase B) New tools module marshal-delegate-processor: a javax.annotation.processing.Processor that, per @AtomicSerial round, groups classes by package and: - GENERATES one GeneratedMarshalDelegate per package serving only the classes that need in-package dispatch (non-public class, or non-public single-arg (GetArg) ctor), using the per-class serves()/servedClasses() model. Fully-public classes are left to the reflective path. @stateless levels get empty serialForm / no-op serialize; abstract levels get serialForm/serialize but no create. Emits the module's META-INF/services/org.apache.river.api.io.MarshalDelegate. Skips a package that already has a hand-written MarshalDelegate. - VALIDATES the contract (does NOT modify classes): warns on a non-@stateless class missing serialForm/serialize ("incomplete -- bring it up to spec"), and on a PRIVATE (GetArg) ctor ("cannot be called by a delegate; make it package-private"). This warning stream is the authoritative bring-up-to-spec list. Depends only on the JDK (java.compiler); references the JGDMS API by FQN string. Verified end-to-end under DirtyChai on synthetic inputs: serves PkgComplete / @stateless StatelessSub / public-class-with-pkgpriv-ctor; excludes public+public-ctor and the flagged Incomplete/PrivCtor; generated delegate compiles; warnings correct. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…-only mode Two fixes surfaced by a full-reactor dry run: - A PRIVATE nested @AtomicSerial class (UuidFactory.Impl, MapSerializer.Ent, Levels.LevelData, ...) cannot be named by a separate delegate, so generating "new Outer.Inner(arg)" failed to compile. The processor now excludes private classes from the served set and warns "make it package-private" (same rule as private (GetArg) constructors). - New -Amarshaldelegate.validateOnly=true option: run the contract checks and emit warnings without generating any sources or service files. This is the safe way to sweep the whole reactor for the bring-up-to-spec list (a generation bug in one package can no longer halt the sweep). Verified: validate-only sweep over the entire reactor completes with 0 module failures and reports 37 contract warnings (incomplete classes + private nested classes) -- the authoritative bring-up-to-spec list. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…lidate-only) Promotes the annotation processor to a top-level module (jgdms-marshal-delegate- processor, parent = root) listed FIRST in the reactor, so it builds before any @AtomicSerial module; downstream modules then resolve it from the reactor/local repo. The processor module overrides annotationProcessorPaths to empty (the parent path points back at itself) and keeps proc=none. Wires it into the parent maven-compiler-plugin config with -Amarshaldelegate.validateOnly=true: every module compile now runs the @AtomicSerial contract checks and warns on incomplete classes, private nested classes, and private (GetArg) constructors -- without generating anything. validateOnly keeps it a non-failing gate (warnings, not errors) for now. Verified from a clean local repo: mvn install of [processor, platform] builds the processor first, installs it, and platform's compile runs the gate (warnings emitted), BUILD SUCCESS, no annotationProcessorPaths resolution error. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
ParticipantHandle was @AtomicSerial but had no serialForm/serialize, and its
(GetArg) ctor read "preparedPart" -- the TRANSIENT field, never on the wire --
then rebuilt storedpart from it (always null), and read crashcount as int though
the field is long. The atomic path was broken.
Adds serialForm/serialize for the actual non-transient state {storedpart
(StorableObject), crashcount (long), prepstate (int)} -- the same fields the
legacy writeObject (defaultWriteObject) persists -- and rewrites the (GetArg)
ctor to read those and set the final fields directly. preparedPart stays
transient/null, restored later by restoreTransientState() as on the legacy path.
Removes the now-dead check(GetArg) helper.
Verified: mahalo-service compiles; the @AtomicSerial validation gate no longer
flags ParticipantHandle.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
ServiceRegistration was @AtomicSerial but had no serialForm/serialize, and its
(GetArg) ctor was multiply broken: it read "eventIterator" and "iteratorCondition"
(both transient, never on the wire), read "marshalledEventType" though the field
is marshalledEventTarget, and omitted expiration and remoteEventIteratorID.
Adds serialForm/serialize for the five non-transient fields {cookie (Uuid),
expiration (long), marshalledEventTarget (MarshalledObject), unknownEvents (Map),
remoteEventIteratorID (Uuid)} -- exactly what the legacy defaultWriteObject
persists (AbstractLeasedResource carries no serialized state) -- and rewrites the
(GetArg) ctor to read those (delegating to the private ctor for the final fields,
then setting non-final expiration/remoteEventIteratorID). Transients stay null,
restored later via restoreTransientState/setIterator/setCondition as today.
Verified: mercury-service compiles; the validation gate no longer flags
ServiceRegistration.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
EventID was @AtomicSerial but had no serialForm/serialize, and its atomic (GetArg)
ctor was broken: it read source via ((RO)arg.getReader()).source, but no @ReadInput
method registered the RO reader, so arg.getReader() was null (NPE). The RO inner
class was a dead, half-wired @ReadInput attempt that read source as a trailing
MarshalledObject -- a non-deterministic block-data read unsuitable for the atomic
(DER) codec.
Adds serialForm/serialize with source as a deterministic named field
{id (long), source (MarshalledObject)} -- the source wrapped in a MarshalledObject
for codebase preservation, mirroring the legacy writeObject/readObject (the
RemoteEventData pattern). The (GetArg) ctor reads id and reconstructs source via
readSource(), which tolerates an unavailable codebase (leaves source null) as the
legacy readObject did. Removes the dead RO class and its now-unused imports; keeps
the legacy writeObject/readObject for the non-atomic path.
Verified: mercury-service compiles; the validation gate no longer flags EventID.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
EntryRep had a complete atomic READ side (the @ReadInput getRO() + the (GetArg) ctor reading values/superclasses/hashes/hash/className/codebase/id, with integrity derived from the stream's integrity status -- deterministic, no block-data), but no serialForm/serialize, so it could not be atomically written. Adds the write side for the 7 non-transient fields (exactly what the (GetArg) ctor reads and legacy defaultWriteObject persists). transient expires/integrity/realClass are correctly excluded; integrity stays derived from the stream context via the existing @ReadInput RO. Purely additive -- no ctor or RO changes. Verified: outrigger-dl compiles; the validation gate no longer flags EntryRep. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
… (not MarshalledObject) The atomic serialForm for EventID.source used java.rmi.MarshalledObject (via convertToMarshalledObject()). A MarshalledInstance -> MarshalledObject -> MarshalledInstance round-trip narrows through java.rmi.MarshalledObject, which has no place for the DER schema fields (schemaBytes/schemaDigest) that MarshalledInstance / AtomicMarshalledInstance carry -- so it would silently drop the schema once added. Switches the atomic field to MarshalledInstance end-to-end (new AtomicMarshalledInstance(source) + mi.get(false)), the RemoteEventData pattern. The legacy writeObject/readObject path is unchanged (separate codec). See the API-wide MarshalledObject->MarshalledInstance migration task. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…and EventReg Both nested @AtomicSerial classes were private (a private nested class cannot be named by a separate MarshalDelegate); made them package-private. SvcReg was otherwise complete. EventReg was incomplete (no serialForm/serialize) and read its listener via a non-deterministic @ReadInput block-data RO reader. Adds serialForm/serialize for the seven non-transient fields plus listener, with listener as a named MarshalledInstance field (new AtomicMarshalledInstance / mi.get(false)) -- deterministic for the DER codec and schema-preserving (not MarshalledObject). Rewrites the (GetArg) ctor to read listener from the field via readListener(), removing the @ReadInput getReader() + RO inner class. newNotify stays transient (hardcoded true). Verified: reggie-service compiles; the validation gate no longer flags RegistrarImpl. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…delegatable) A private nested @AtomicSerial class cannot be named by a separate MarshalDelegate, so it can never be served and the setAccessible fallback could never be removed for it. These three are otherwise complete (UuidFactory.Impl is @stateless with a (GetArg) ctor; MapSerializer.Ent and org.apache.river.logging.Levels.LevelData have full serialForm/serialize/(GetArg)). Widen from private to package-private -- the minimal access a same-package delegate needs. Verified: jgdms-platform compiles; the @AtomicSerial validation gate no longer flags UuidFactory.Impl, MapSerializer.Ent, or Levels.LevelData. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…nts derived) au.net.zeus.jgdms.proxy.AdminProxy.ConstrainableAdminProxy was flagged incomplete (@AtomicSerial, a methodConstraints field, no serialForm/serialize). But its (GetArg) ctor derives methodConstraints from the deserialized server -- ((RemoteMethodControl) server).getConstraints() -- so its level contributes no wire data; it is @stateless, like its sibling ConstrainableSmartProxy. Adds @AtomicSerial.Stateless; no serialForm/ serialize needed and the ctor is unchanged (the cached field stays, repopulated on read). Verified: jgdms-lib-dl compiles; the gate no longer flags ConstrainableAdminProxy. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
ConsistentMap was @AtomicSerial with no serialForm/serialize, and its (GetArg) ctor read "entrySet" as a Map and re-wrapped it -- but the field (and legacy defaultWriteObject) is a Set<Map.Entry> of ConsistentMapEntry. Adds serialForm/ serialize for the entrySet Set (the entries are ConsistentMapEntry and the set is a ConsistentSet, both @AtomicSerial) and fixes the (GetArg) ctor to read it as a Set, delegating to the existing private ConsistentMap(Set) ctor. Matches the field and the legacy form. Verified: jgdms-lib-dl compiles; the gate no longer flags ConsistentMap. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…aints.MethodDesc
The outer BasicMethodConstraints already had serialForm/serialize; its nested
public MethodDesc had a (GetArg) ctor + checkSerial invariant + serialPersistentFields
but no serialForm/serialize. Adds them, mirroring serialPersistentFields exactly
{name(String), types(Class[], optional), constraints(InvocationConstraints)} and the
legacy defaultWriteObject/readObject form. Purely additive; ctor and validation unchanged.
Verified: jgdms-platform compiles; the gate no longer flags MethodDesc.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…med core class) RegistrationInfo was a private nested @AtomicSerial class with a (GetArg) ctor and an @ReadInput RO reader, but no serialForm/serialize -- so it could not be marshalled by the atomic write path. The atomic writer (ObjOutputStream.writeHierarchy) dispatches only to serialize(PutArg); its writeObject branch is disabled, so the trailing block-data listener the RO read had no atomic writer. Changes: - Make the class package-private (was private), so a MarshalDelegate can serve it when the setAccessible fallback is removed in 4.0.0. - Promote the transient, block-data 'listener' to a named MarshalledInstance serialForm field (the EventReg/EventID pattern); add serialForm/serialize for all 11 fields. - Rewrite check(GetArg) to unmarshal the listener from the named field (tolerating recovery failure exactly as the legacy readObject did); delete @ReadInput getRO()+RO. - Keep the legacy writeObject/readObject (block-data listener) for JOSS interop with previously persisted logs -- dual path, like BasicMethodConstraints. listener uses MarshalledInstance directly (no convertToMarshalledObject round-trip -> preserves DER schema). handback/discoveredRegsMap stay MarshalledObject (Task #10). The atomic path was non-functional before (no serialize) so there is no atomic data to break; old JOSS logs still read via the retained readObject. Verified: fiddler-service compiles; the gate no longer flags RegistrationInfo. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Re-review of feature/atomicserial-marshal-delegate after the Phase A review. Code verdict: approved (mahalo must-fix resolved; serves() scoping sound; Phase B processor correct; contract completions correct; normal-mode build green platform 297 / der 388 / jeri 96). Records the one gap that blocks strict-green: the platform package org.apache.river.api.io has no MarshalDelegate for its built-in @AtomicSerial serializer helpers (MapSerializer$Ent et al.), so strict mode fails on the first Map/collection in any object graph. mercury MercuryProxyEqualityTest and norm renewalservice/EqualsTest both fail under strict on this; the service-proxy delegates themselves resolve fine. Includes a reproduction recipe (incl. the qaHarness.prop globalvmargs injection gotcha) and notes the gap is still open at tip 68dc29b. Co-Authored-By: Claude Opus 4.8 <[email protected]>
…stream state through getObjectStreamContext()
The @AtomicSerial @ReadInput/ReadObject mechanism was a read-side hook for consuming
trailing writeObject block data, but its write-side counterpart was never finished
(ObjOutputStream.writeHierarchy's writeObject branch is dead code) and the atomic write
path discards block data. In practice all five getReader() users only ever used the
ReadObject as a back-door to the underlying stream's *state*, never to read block data:
- EntryRep, MarshalledWrapper (x2): the integrity-enforcement flag
- ProxySerializer: the stream's default/verifier class loaders
- BasicObjectEndpoint: a per-decode-unit token for batching client DGC dirty calls
Since getReader() returned null on the DER path, every one of these was JOSS-only or
latently broken under DER. This routes them all through the explicit, wire-format-
independent getObjectStreamContext() channel and removes the mechanism (4.0.0 breaking).
Phase A (additive): AtomicMarshalInputStream.getObjectStreamContext() now also surfaces
a DeserializationCompletion (adapting registerValidation) -- the JOSS analogue of the DER
completion element. Class loaders are deliberately NOT surfaced here (security-sensitive;
would be broadcast to every object in the graph).
Phase B (migrate users):
- EntryRep, MarshalledWrapper (lib-dl + compat): integrity via the existing
MarshalledWrapper.integrityEnforced(ObjectStreamContext) helper, passing arg.
- ProxySerializer: reads the {default,verifier} loaders from the package-private
GetArgImpl under doPrivileged -- narrower than the old @ReadInput (only trusted
same-package code, never broadcast).
- BasicObjectEndpoint: registers the batched DGC dirty on the DeserializationCompletion
context element on BOTH paths (the joss/reader split is gone); fixes the DER-path NPE.
Phase C (delete machinery): removed AtomicSerial.{ReadObject, @ReadInput, GetArg.getReader(),
Factory.streamReader()}; GetArgImpl/DerGetArg.getReader(); the AtomicMarshalInputStream
readers-map build; cleaned unused imports (RegistrarImpl et al.) and updated two tests.
Verified: full reactor compiles; BasicObjectEndpointDerDgcTest (4), GetArgIdempotencyTest (6)
and AtomicMarshalInputStreamTest (4) pass on the DirtyChai SM-capable JDK. (DerDecodeUnitContextTest
is JUnit5; its surefire provider is unavailable offline, so not run.)
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
… strict-mode io gap) The strict-mode verification review found that org.apache.river.api.io has no registered MarshalDelegate for its built-in @AtomicSerial serializer helpers (MapSerializer.Ent, ProxySerializer, the Set/List/File/Permission/URI/... serializers), so strict marshalling died on the first Map/collection in any object graph (InvalidClassException on MapSerializer$Ent). This is the review's #1 must-fix for strict-green. The MarshalDelegateProcessor already generates a correct per-package delegate; it was only running in validateOnly mode for the platform. This overrides the parent's compilerArgs (combine.self="override") to DROP -Amarshaldelegate.validateOnly for jgdms-platform only, so the processor generates and registers a per-package GeneratedMarshalDelegate for the 6 platform packages that need in-package dispatch (org.apache.river.api.io, net.jini.id, net.jini.security, net.jini.core.constraint, org.apache.river.discovery, org.apache.river.logging). All other modules stay in validateOnly (the gate still emits its worklist warnings). Adds GeneratedMarshalDelegateTest: verifies the generated delegate resolves and serves the package-private helpers (MapSerializer.Ent, ProxySerializer, Set/ListSerializer) and round-trips Map/List/Set. Green in normal mode AND under -Dorg.apache.river.api.io.marshalDelegate.strict=true (4/4, no setAccessible fallback) on the DirtyChai SM-capable JDK; full reactor compiles. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…rocessor merges service files Flip the parent compiler config from validate-only to generation mode by default, so the MarshalDelegateProcessor generates + registers a per-package MarshalDelegate for every module whose @AtomicSerial classes need in-package dispatch -- closing the strict-mode gap for the core service classes made package-private during the bring-up-to-spec pass (FiddlerImpl. RegistrationInfo, ParticipantHandle, EventID, reggie SvcReg/EventReg, outrigger EntryRep, ...). The 7 slimming-cluster modules whose @AtomicSerial classes are not yet complete pin -Amarshaldelegate.validateOnly=true in their own pom (generation would emit non-compiling dispatch for an incomplete class): jgdms-activation, jini-2.1-compat, service-starter, and the phoenix-activation phoenix/phoenix-common/phoenix-dl/phoenix-group submodules. They stay on the contract gate (still emitting the 22-warning worklist). jgdms-platform drops its now-redundant per-module override (inherits the generation default). Processor: writeServiceFile() now MERGES rather than overwrites -- it preserves any committed hand-written entries already in CLASS_OUTPUT (e.g. MercuryProxyMarshalDelegate, NormMarshalDelegate) and adds the generated ones, so generation composes with hand-written delegates instead of clobbering their registration. Verified: jgdms-lib-dl's service file now lists its hand-written LookupUtilMarshalDelegate plus 4 generated delegates; the hand-written-only modules are untouched. Verified: full reactor clean compile is green; slimming modules generate nothing and still warn; the generated fiddler-service delegate dispatches FiddlerImpl.RegistrationInfo correctly. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
- MarshalDelegate.packageName(): dead for resolution since the resolver moved to servedClasses()/serves(Class) identity matching; no delegate overrides it. Dropped. - AtomicMarshalInputStream.fields(Class) + its toOSF() helper: an unused serialForm-dispatch path (the live readFields uses the 2-arg fields(ObjectStreamClass,Class)); the only reference was a commented-out line. Dropped. EMPTY_CONSTRUCTOR_PARAM_TYPES stays (still used by the atomic ctor lookup); ObjOutputStream keeps its own toOSF. Verified: jgdms-platform compiles. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…ct mode Resolves the strict-verification review's note about the ungated setAccessible in AtomicExternal.Factory.instantiate. @AtomicExternal is the Externalizable-style escape hatch for the JOSS atomic implementation: the (ObjectInput) constructor and writeExternal read/write the stream with arbitrary logic, so there is no fixed serialForm/schema. It therefore cannot be expressed deterministically (no DER support) and cannot be served by a MarshalDelegate, so it is intentionally outside the strict MarshalDelegate gate (which governs only the @AtomicSerial DER/delegate path). Support is optional -- a framework may decline @AtomicExternal entirely; classes needing cross-path/deterministic marshalling should use @AtomicSerial with serialForm()/serialize() instead. External @AtomicExternal classes follow the Externalizable convention of being public; the platform's own JOSS-only primitive serializers (Double/Date/UID/...) are package-private with a public (ObjectInput) ctor and are instantiated only within this serialization package, never on the deterministic wire -- a deliberate same-package detail, so they stay package-private (no new public API surface). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…ion classes - ActivationGroupDescImpl.CommandEnvironmentImpl: its serialize was non-static (public void) so it did not satisfy the @AtomicSerial serialize(PutArg,T) contract; made it public static. serialForm/(GetArg) ctor were already correct. - ActivatableInvocationHandler: added serialForm/serialize for the three @serial fields {id(Object), uproxy(Remote), clientConstraints(MethodConstraints)}, matching the legacy defaultWriteObject. The existing (GetArg) ctor and validating readObject are unchanged; id stays Object (field/legacy slot type) and the ctor's ActivationID type-check is unchanged. Verified: jgdms-activation compiles; the gate no longer flags either class. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…or/permission classes
Adds serialForm/serialize to the five incomplete @AtomicSerial classes, each mirroring the
fields its existing (GetArg) constructor reads:
- ActivateWrapper.ActivateDesc: {className, importLocation[], exportLocation[], policy,
configurationArguments[]}.
- NonActivatableServiceDescriptor: {codebase, policy, classpath, implClassName,
serverConfigArgs[]}. The runtime-derived final Configuration field is intentionally not
in the atomic form (the ctor passes null and it is rebuilt from serverConfigArgs) -- the
serial form is independent of the legacy defaultWriteObject, as with Levels.LevelData.
- SharedActivatableServiceDescriptor: its own {sharedGroupLog, restart, host, port}; the
NonActivatableServiceDescriptor superclass serializes its own fields via the hierarchy walk.
- SharedActivationGroupDescriptor: {policy, classpath, log, serverCommand, serverOptions[],
serverProperties[], host, port}. serverProperties is a Properties field but the ctor reads
the deterministic String[] form (Properties/Hashtable iteration order is not DER-stable);
serialize flattens it to a key-sorted [k,v,...] array via propertiesToArray (the inverse of
the existing convertToProperties).
- SharedActivationPolicyPermission: {policy} -- the permission name (getName()); the ctor
rebuilds the derived policyPermission via init(policy), so the atomic form carries only the
name (deterministic; avoids serializing the duplicate Permission the legacy writeObject wrote).
Verified: service-starter compiles; the gate no longer flags any of the five.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…ommon value types
Adds serialForm/serialize mirroring each existing (GetArg) ctor's reads:
- AID.State (AID's serialization proxy): {activator(Activator), uid(UID)}.
- ConstrainableAID.State: {activator(Activator), uid(UID), constraints(MethodConstraints)}.
- ConstrainableAID.Verifier (TrustVerifier): {activator(RemoteMethodControl)}.
- ActivationGroupData: {config(String[])}.
Verified: phoenix-dl and phoenix-common compile; the gate no longer flags any of them.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…r proxy) New series entry on the resolution half of deserialization, verified against the code: the Warres (TR-2006-149) ambient-loader problem (latestUserDefinedLoader/TCCL -> undesired local resolution, type conflicts; fatal under OSGi); JERI's fix half one (MarshalInputStream carries an endpoint-assigned defaultLoader, resolveClass -> ClassLoading.loadClass(codebase, name, defaultLoader); the loader becomes the PARENT of the proxy's PreferredClassLoader, so shared API resolves through the parent = client bundle in OSGi while impl stays child-first/isolated); fix half two (each proxy + object tree in its own MarshalledInstance stream via MarshalledWrapper -- reggie Item.service -- giving type-identity / fault / security isolation); and why this is the OSGi answer (Service registry for cross-bundle SPIs + co-loaded loader scan), with an honest "uniquely among mainstream frameworks" framing. Also: REVIEW-NOTES §9 points the reviewing agent at the Service/OSGi fix (2102f48) and the OSGi-smoke-test ask. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
… refactor
Unit-tests the OSGi discovery fix without a live OSGi runtime, using Proxy-based
fakes for BundleContext/Bundle/BundleReference/ServiceReference (8 tests, green):
* ChainedIterator unions the two relationships (co-loaded scan then registry),
incl. empty sides.
* registryProviders returns the real service INSTANCES via getService (the bug
returned ServiceReferences / nothing), filters by isInstance (no CCE on a
foreign service under the same name), and is empty when none registered.
* contextFor scopes to the requesting loader's bundle, walks the parent chain
(proxy codebase loader -> client bundle parent), and falls back to the
platform context when the requester isn't in a bundle.
Testability refactor: extract OSGiServiceIterator.registryProviders(Class,
BundleContext) and open contextFor + Service.ChainedIterator to package-private.
Retires the "compile-green only" caveat for the discovery logic; the live
OSGi/SPI-Fly smoke test is still the reviewer's call (review notes Sec 9).
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Strengthen the versioning contrast: OSGi version ranges aren't just nominal/ asserted, their *resolution* into a consistent wiring (the uses-constraint problem) is NP-complete -- hence backtracking SAT-style resolvers that can grind or fail to find a wiring that exists. Content-addressing has no global solve: two versions are two loaders, and "compatible?" is a local schemaDigest compare. Applied to both the .md and the .adoc. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* images/wrong-loader-right-key.svg — DRAFT placeholder cartoon: the ambient
resolver (blindfolded, atop a wobbly call stack) delivers to the wrong
"LocalCopy" door -> ClassCastException; the JERI endpoint hands over the
right key (defaultLoader, parent = client bundle) -> the correct codebase
door. Replace with finished art; keep the caption.
* Diagram 7 (diagram7_class_resolution.puml + rendered .svg via PlantUML/Smetana)
contrasts standard ObjectInputStream resolveClass (stack-walk ->
latestUserDefinedLoader/TCCL -> wrong loader) against JERI MarshalInputStream
(endpoint-assigned defaultLoader -> ClassLoading.loadClass -> PreferredClassLoader
with parent = defaultLoader = client bundle).
* blog-post-7: cartoon after the thesis, Diagram 7 "See also" under "the fix,
half one".
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The versioning section was all-critique of JPMS; add the fair counterweight: JPMS's strong encapsulation was a genuine security advance — it shrank the platform's trusted, reflectively-reachable surface to java.base and walled off the internal APIs (sun.misc.Unsafe and kin) that were a perennial exploit source. Narrow the critique explicitly to *version* coexistence: a versioning critique, not a verdict on the module system. Applied to both .md and .adoc. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Full AsciiDoc of Part 7 (class resolution): cartoon as an image:: figure with caption, Diagram 7 inlined as a [plantuml,diagram7-class-resolution,svg] block (renders via asciidoctor-diagram at build), series index as an AsciiDoc table. Header comment lists the publication touch-ups (front matter, cross-link remapping, final cartoon art) -- same shape as the Part 6 .adoc. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Was "Jim Warres" (crossed with Jim Waldo); the author of SMLI TR-2006-149, "Class Loading Issues in Java RMI and Jini," is Michael Warres. Fixed in the .md and .adoc (surname-only references were already correct). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…float/char alignment §3.8 opaque-octet carve-out (certs/X.501 Names/signatures carried verbatim, never re-encoded; referenced from §7.3/§7.4.1/§7.6/§7.7.7/§9); §7.3 OF Certificate -> OF OCTET STRING; §7.4.1 normative tbs signature input; §7.6 X500Principal opaque + Float/Double/Character aligned to STD-008 §17.3; §5 rewritten to the built MarshallingFormat/AtomicDer reality (drop WireFormat/version-byte); clean SCAP break. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…+ PROXY gate
DerObjectStreamCodec: add STD-008 §15.2 context tags [7] enum (declaring class + constant name; getDeclaringClass gotcha) and [8] bare java.lang.reflect.Proxy (interface names + @AtomicSerial handler -> Proxy.newProxyInstance; DoS-bounded to 127; fail-secure), gated by DeSerializationPermission("PROXY"); shared encodeAtomicRecord/decodeAtomicRecord helpers. Tests: enum 5/5, proxy 4/4, opaque-octet carriage 5/5, StringMethodConstraints CNFE-avoidance 1/1.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
DER-native counterpart of platform ProxySerializer, in the downloadable jgdms-der module so DER proxy support downloads into a JGDMS 3.X node rather than depending on the 4.0 platform ProxySerializer. @AtomicSerial + implements Resolve; reuses 3.X-present CodebaseAccessor/ProxyCodebaseSpi; wraps the proxy in a DER MarshalledInstance. Stream loaders reached via package-private DerGetArg accessors, NOT getObjectStreamContext (a ClassLoader is a capability and must not be broadcast to every object in the graph). WIP: write seam + real stream->DerGetArg loader threading + round-trip test to come. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…code path
Wire the unmarshalling stream's {default, verifier} class loaders from
DerMarshalFactory.createMarshalInput down to DerGetArg's 5-arg ctor, so trusted
resolution code (DerProxySerializer) can reach them via the narrow package-private
DerGetArg channel rather than the capability-broadcasting getObjectStreamContext().
Flow: DerMarshalFactory.createMarshalInput -> DerMarshalInstanceInput(defaultLoader,
verifierLoader) -> MarshalledInstanceCodec.decodeMarshalledInstance(..,defaultLoader,
verifierLoader) -> ObjectCodec.decodeHierarchy(..,streamDefaultLoader,streamVerifierLoader)
-> new DerGetArg(storeMap,0,decodeUnit,streamDefaultLoader,streamVerifierLoader).
The token-carrying decodeMarshalledInstance/decodeHierarchy overloads are widened in
place; the stable 2-arg/3-arg public entry points (used by all existing tests) are
preserved as null-loader delegates, so no test callers change. The object-stream
(DerObjectStreamCodec) and nested-object paths pass null loaders for now; only the
top-level MarshalledInstance path carries real loaders, which is what the DER proxy
carrier needs.
31 main + 75 test sources compile clean; 105 decode-path regression tests green.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…tance over DER (no JOSS)
A proxy can now travel inside a DER MarshalledInstance with no Java Object
Serialization, both bare and via a downloadable-codebase carrier:
* DerMarshalInstanceOutput gains a substitute/raw flag (mirroring the JOSS
AtomicMarshalledInstance(obj, ctx, replace) guard) and dual-form encoding:
an object with an @AtomicSerial ancestor uses the schema-separated form;
anything else (a bare java.lang.reflect.Proxy, String, byte[], enum, ...)
is written as a self-describing object-stream item ([8] for a proxy) via
DerObjectStreamCodec, with empty schemaBytes as the sentinel. The seam
substitutes a downloadable DynamicProxyCodebaseAccessor / ProxyAccessor with
a DerProxySerializer carrier (gated by the registered ProxyCodebaseSpi).
DerMarshalInstanceInput decodes the object-stream form on empty schemaBytes
and applies readResolve to the root (JOSS parity), so a carrier resolves to
the real proxy.
* DerProxySerializer (Option B): carries the proxy's @AtomicSerial
InvocationHandler (the portable endpoint+constraints essence) instead of a
bare bootstrap Proxy field -- the DER @AtomicSerial codec has no bare-Proxy
field type, and a Rust/cross-language peer reads the handler record, not a
JVM proxy. The fixed-interface {CodebaseAccessor, RemoteMethodControl}
bootstrap proxy is rebuilt locally in readResolve via Proxy.newProxyInstance
and handed to ProxyCodebaseSpi.resolve (authenticate-before-download
preserved). Wire-equivalent to the platform ProxySerializer; stays
downloadable (no platform-4.0 dependency). Construction validates the
handler is @AtomicSerial (the [8] invariant). substitute/raw plumbed through
DerMarshalledInstance(obj, ctx, boolean) and DerMarshalFactory.
* ObjectCodec.encodeNested: a value whose runtime class is not itself
@AtomicSerial but extends an @AtomicSerial class is encoded as that
superclass (S3.10 wire-visibility, already applied at top level) -- lets a
final DerMarshalledInstance value travel as its @AtomicSerial
MarshalledInstance superclass and decode there via ServiceLoader dispatch
(@AtomicSerial is not @inherited).
* Nullable scalar reference value fields (boxed primitive, String, byte[])
now round-trip: encodeValue emits DER NULL for null; WireTypes.decode reads
NULL back as null. Required because a nested MarshalledInstance's
codebaseAnnotation is legitimately null.
New DerProxyMarshalledInstanceTest: bare proxy round-trips through a DER
MarshalledInstance; the carrier round-trips and readResolves to the proxy via
the registered PreferredProxyCodebaseProvider (empty codebase = local
resolve); a non-@AtomicSerial handler is rejected.
262/262 offline tests green across jgdms-der (the 5 DerReplacerSeamTest
failures are a pre-existing offline-harness limitation -- the @serializer
annotation processor that generates the serializer registry is not run by
plain javac -- identical on baseline with these changes stashed).
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…he thread-context loader
The DER decode path resolved class names with Thread.currentThread()
.getContextClassLoader() in three places -- ObjectCodec.loadClass (the
@AtomicSerial path), WireTypes.loadClass (enum/array field components), and
DerObjectStreamCodec ([1] @AtomicSerial / [7] enum / [8] bare proxy). That is
the Warres ambient-resolution failure (TR-2006-149, and blog-post-7): the TCCL
depends on whatever earlier code left it set to, so across codebases it picks
the wrong local copy, conflates same-named types from different codebases, or
cannot see a class in another OSGi bundle at all. It is wrong for a federated
deserializer.
Resolution is now a deterministic function of the loader the receiving ENDPOINT
assigned, via net.jini.loader.ClassLoading (preferred-class / OSGi-aware), never
the TCCL -- the JGDMS discipline ("the endpoint assigns the loader"). DER carries
no codebase annotation, so names resolve with codebase==null against the
endpoint's defaultLoader; [8] proxies go through ClassLoading.loadProxyClass.
Mechanism: a new der.getarg.ResolutionContext {defaultLoader,
verifyCodebaseIntegrity, verifierLoader} carrying loadClass/loadProxyClass is
threaded explicitly through the decode tree -- DerMarshalFactory.createMarshalInput
-> DerMarshalInstanceInput -> {MarshalledInstanceCodec.decodeMarshalledInstance,
DerMarshalInputStream} -> {ObjectCodec.decode{Hierarchy,Nested,NestedArray},
DerObjectStreamCodec, DerFieldStore -> WireTypes} -> DerGetArg. The encode side,
whose classes are already loaded, uses ResolutionContext.NONE; standalone
public decode overloads delegate with NONE (no test/API churn).
SECURITY (ClassLoader visibility): a ClassLoader is a capability and is NOT
widened. ResolutionContext exposes the loadClass/loadProxyClass OPERATIONS; the
raw loader stays inside it. It is held only in private fields (DerObjectStreamCodec,
DerMarshalInstanceInput) and a package-private DerGetArg field, threaded only as
trusted-internal method params, and is NEVER placed in getObjectStreamContext()
(which still returns only the decode-unit) -- so a hostile (GetArg) constructor in
the graph cannot reach it. DerProxySerializer keeps reaching {default,verifier} via
the existing package-private DerGetArg accessors. Audited: no public method returns
a ResolutionContext or ClassLoader.
New test resolvesAgainstEndpointLoaderNotThreadContext: a recording endpoint
loader is provably consulted to resolve both the [8] proxy interface and the [1]
@AtomicSerial handler. 262/262 offline jgdms-der tests green (the 5
DerReplacerSeamTest failures are the pre-existing offline-harness artifact --
identical on baseline).
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…the JERI invocation stream Two remaining DER proxy follow-ups. (2) Polymorphic-receiver typed get(). A typed MarshalledInstance.get(ServiceInterface.class) of a DerProxySerializer carrier was rejected because the carrier's own class is not assignable to the service interface -- the pre-construction assignability check fired before readResolve. DerMarshalInstanceInput.readObject now decodes at Object.class, applies readResolve, then enforces the requested type against the RESOLVED value (the same type guarantee, mirroring JOSS where readResolve runs before the typed get() sees the object). The no-type get()/Object.class path is unchanged. (3) Substitute downloadable proxies in the invocation arg/return stream, not just the MarshalledInstance path. DerObjectStreamCodec gains a write seam (initWriter) that substitutes a top-level DynamicProxyCodebaseAccessor / ProxyAccessor with a DerProxySerializer carrier -- the object-stream counterpart of AtomicMarshalOutputStream.defaultReplaceObject -- OFF by default (a bare serviceProxy inside a carrier must not re-substitute). DerMarshalOutputStream exposes it via a (out, context, streamLoader) ctor. AtomicDerInvocationHandler (client) and AtomicDerInvocationDispatcher (server) now build their DER streams with the endpoint loader and context: getProxyLoader(proxy.getClass()) on the client, getStreamLoader(impl) on the server -- both for write-side substitution AND read-side resolution (extending the endpoint-loader/Warres discipline to the invocation path; the read side previously used the no-arg streams = NONE/TCCL). With no registered ProxyCodebaseSpi that substitutes, a proxy still travels bare ([8]); a registered provider yields a carrier. New tests: carrier typed-get resolves to the proxy interface; a proxy round-trips through the substituting object stream with endpoint-loader resolution. 265/265 offline jgdms-der tests green (the 5 DerReplacerSeamTest failures are the pre-existing offline-harness artifact). The two AtomicDer* files compile clean against the changed der API. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…t/double/char The class javadoc still said writeFloat/writeDouble/writeChar throw UnsupportedOperationException "per STD-006 sec.7.6". They are in fact implemented with strict canonicalization (STD-008 sec.17.3 lifted that deferral) -- the methods delegate to the codec. Only writeBytes(String)/writeChars(String) throw (ambiguous legacy encoding). Comment-only; no behaviour change. (The input side DerMarshalInputStream and the object-granularity DerMarshalInstanceInput/Output docs were already correct.) Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
A DoS review of the read path found the low-level parser already well-hardened: DerReader.readTlvHeader validates (long)pos+len > end (overflow-safe) BEFORE any allocation, so an attacker's declared length can't drive over-allocation; nested @AtomicSerial fields are bounded by the cumulative MAX_NESTING=16; proxy interface count by MAX_PROXY_INTERFACES=127; INTEGER fields by intValueExact/longValueExact. Two higher-level vectors were unbounded: (A) Unbounded eager read. DerMarshalInputStream / DerMarshalInstanceInput read the whole input with InputStream.readAllBytes() (DER needs the full TLV buffer), so on an attacker-controlled stream -- notably the JERI invocation arg/return stream -- peak memory was unbounded. Now both use DerInputLimits.readAllBytesBounded, which buffers at most maxInputBytes (default 16 MiB, system-property overridable) and rejects a larger stream with an IOException, reading only one byte past the limit to detect it. (B) Cross-stream readResolve->get() recursion. A DerProxySerializer carrier resolves by unmarshalling its serviceProxy MarshalledInstance via get(), which is a fresh depth-0 decode -- so a chain of carriers-within-carriers recursed without bound (MAX_NESTING only bounds field nesting within ONE decode), and a modest crafted input could reach StackOverflowError. DerMarshalInstanceInput.readObject now bounds that recursion with a ScopedValue depth counter (ScopedValue, not ThreadLocal -- virtual threads; it propagates down the synchronous get() call stack) capped at maxMarshalledInstanceNesting (default 16), so a deep chain fails with a clean InvalidObjectException. A real downloadable proxy nests exactly one level, so the limit is far above legitimate use. Tests: DerInputLimitsTest (under/at/over the byte cap; operator-disabled cap); deeplyNestedCarriersFailCleanlyNotStackOverflow (24 nested carriers must throw a bounded IOException, not a StackOverflowError). 270/270 offline jgdms-der tests green (the 5 DerReplacerSeamTest failures are the offline-harness resource artifact). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The 16 MiB max-input cap was JVM-wide only (a system property). Make it
per-deployment via the JGDMS Configuration -> invocation-layer path:
* DerInputLimits is now an immutable value object {maxInputBytes,
maxMarshalledInstanceNesting} with a JVM-wide DEFAULT (still system-property
configurable) and a DerInputLimits.maxBytes(int) convenience. DerMarshalInputStream
gains a (in, resolution, limits) ctor; the existing ctors use DEFAULT.
* AtomicDerILFactory takes a DerInputLimits (new constructor
(serverConstraints, proxyOrServiceImplClass, limits)) and threads it into the
AtomicDerInvocationDispatcher, whose createMarshalInputStream applies it to the
client-argument stream. So a service sets its own cap from its config, e.g.
new AtomicDerILFactory(null, MyService.class, DerInputLimits.maxBytes(64<<20)).
Scope: the cap is injected on the SERVER (dispatcher) reading client arguments --
the primary attacker surface and the natural per-deployment injection point. The
client handler reads return values with the JVM default (the handler is serialized
into the proxy and travels from server to client, so it's a client-JVM concern; a
MarshalledInstance nested in args is transitively bounded by the server's
invocation-stream cap anyway). The nesting bound stays on the JVM default.
270/270 offline jgdms-der tests green (5 DerReplacerSeamTest = offline-harness
artifact); der main + the three jeri AtomicDer* classes compile clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…cDerInvocationHandler) Completes the symmetry started for the server (dispatcher) side: the client now bounds the invocation RETURN-value stream with its own per-deployment limit. The handler arrives at the client deserialized from the proxy, so the limit must be the CLIENT's own and must never be server-dictated. The handler gains a transient DerInputLimits field, absent from its (empty) serialForm() -- so it is never carried on the wire. It defaults to the JVM-wide DerInputLimits.DEFAULT (system-property configurable per client JVM); a client overrides it programmatically by re-wrapping a received handler -- e.g. in a ProxyPreparer -- via the new ctor AtomicDerInvocationHandler(other, DerInputLimits), which preserves the endpoint and client constraints (getClientConstraints) and applies the client's cap. The constraints-copy ctor carries the chosen cap across setConstraints. createMarshalInputStream passes it to the return-value DerMarshalInputStream. limits is excluded from equals/hashCode (it is a local client behaviour, not proxy identity). der unchanged; the three jeri AtomicDer* classes compile clean. The bounded read itself is covered by DerInputLimitsTest; an end-to-end client-cap assertion needs a JERI transport round-trip (integration test). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…alue cap
A client can now set its own per-deployment return-value DoS cap on a received
proxy through the standard proxy-control idiom -- no dependency on the
invocation-handler type:
Greeter g = (Greeter) ((DerInputLimitControl) proxy)
.setInputLimits(DerInputLimits.maxBytes(8 * 1024 * 1024));
New interface au.net.zeus.jgdms.der.DerInputLimitControl mirrors
RemoteMethodControl: setInputLimits returns a NEW proxy (does not mutate the
original) and getInputLimits reports the current cap. DER proxies implement it --
AtomicDerILFactory.getExtraProxyInterfaces adds DerInputLimitControl alongside
RemoteMethodControl/TrustEquivalence -- and AtomicDerInvocationHandler.invoke
intercepts its methods LOCALLY (by declaring class, exactly as the superclass
handles RemoteMethodControl), building the new proxy with a handler created via
the (other, DerInputLimits) ctor (endpoint + client constraints preserved). The
chosen limits are never serialized (the server cannot impose them); a proxy
applies DerInputLimits.DEFAULT until a client overrides it here.
Test AtomicDerInputLimitControlTest (stub endpoint, no transport -- the control
methods are local): default is DEFAULT; setInputLimits returns a new proxy
carrying the cap and leaves the original unchanged and still a service proxy;
null is rejected. der main + the jeri AtomicDer* classes compile clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
… (marshalling hot path)
A performance review found the per-object marshalling cost was dominated by work
that is a pure function of the class and was being recomputed on every encode and
on every decode digest-compare. Three fixes:
1. SchemaGenerator.generateChain is now memoised per class in a ClassValue. It was
recomputed per writeObject / per nested encode / per decode -- each call doing a
reflective serialForm() and SHA-256 digest(s) per @AtomicSerial class in the
hierarchy. The chain is immutable per class, so it is now computed once; negative
("no @AtomicSerial") results are cached too (as the message) and re-thrown fresh.
2. AtomicSerialSchemaRecord.sha256 clones a pristine MessageDigest prototype instead
of MessageDigest.getInstance("SHA-256") per call (clone copies the un-updated
initial state and skips the synchronized provider lookup; thread-confined per call;
falls back to getInstance if a provider's digest isn't Cloneable). Largely subsumed
by (1) for schema digests, but also helps other digest sites.
3. ObjectCodec caches the (AtomicSerial.GetArg) constructor and the static
serialize(PutArg, T) method per class in ClassValues -- the delegate-less reflection
fallback was doing getDeclaredConstructor / getDeclaredMethod per object. The common
case already uses a MarshalDelegate (no reflection); this speeds up classes without a
generated delegate (e.g. anything compiled without the annotation processor).
Accessibility behaviour is unchanged (no setAccessible added).
Verified good and left alone: MarshalDelegates.delegateFor is already ClassValue-cached;
DerWriter.writeSequence/writeTlv are single-pass O(n) (no quadratic concatenation);
DerReader.readTlvHeader bounds-checks before allocation.
New SchemaGeneratorCacheTest (memoisation via assertSame; deterministic cached failure).
The cached chains produce byte-for-byte-identical wire output -- confirmed by the full
272/272 offline round-trip suite passing with the cache active (5 DerReplacerSeamTest
failures are the pre-existing offline-harness resource artifact).
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…ctions resolves it The @AtomicSerial MarshalDelegate processor is wired into every module's annotationProcessorPaths but is not a normal dependency, so --also-make excluded it from the scoped CI reactor and jgdms-collections failed to resolve marshal-delegate-processor:3.1.1-SNAPSHOT. Add it to -pl in the "Build dependencies" step of the SPIFFE and JWT workflows. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…modules StringMethodConstraintsCnfeTest (jgdms-platform) and AtomicDerInputLimitControlTest (jgdms-jeri) were written in JUnit 5, but both modules are JUnit-4-only (junit:4.13.2, no Jupiter), so testCompile failed in CI. Convert them to JUnit 4 (the modules' existing framework): public class/method, message-first assertions, and replace the JUnit-5-only assertInstanceOf/assertDoesNotThrow with instanceof+cast / direct call. Semantics unchanged. (Project-wide JUnit 5 platform adoption is a separate follow-up.) Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
DerMarshalInstanceInput uses java.lang.ScopedValue for the nested-carrier DoS depth guard. ScopedValue is a preview API in JDK 21-24 and only final in JDK 25 (JEP 506), so --release 21 cannot compile it without --enable-preview (which would pin der bytecode to one JDK). Target release 25 instead. Consequence: jgdms-der now requires a JDK 25+ runtime (DirtyChai) and will not run on Java 21. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
SPIFFE exercises jgdms-jeri, which depends on jgdms-der. jgdms-der now targets release 25 (finalised java.lang.ScopedValue, JEP 506), so JDK 21 can neither compile (--release 25 unsupported) nor run (class-file v69) the jeri+der stack. Run SPIFFE on DirtyChai (JDK 25+) only. The JWT workflow keeps its Temurin-21 leg: jgdms-security-jwt depends only on jgdms-platform (release 21), not der. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…] over the wire) The legacy org.apache.river.api.security.RemotePolicy interface (replace(PermissionGrant[])) is superseded by RemotePolicyService, which transmits grants as policy-file text (String[]) and parses them locally with DefaultPolicyParser. RemotePolicy was never exported remotely, and its only implementer, RemotePolicyProvider, is used purely as a LOCAL dynamic Policy (PolicyUpdateListener applies locally-parsed grants to it via the concrete type). Removing it deletes the only path that would deserialise an attacker-supplied PermissionGrant[]/Permission off the wire into a policy implies() sink, and drops the dead basePolicyIsRemote forwarding branch in RemotePolicyProvider.implies(). Breaking change (public API) — appropriate for the 4.0 slimming release. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…oS hardening) readBlockDataLong, readNewArray, readNewProxyClassDesc and readNewLongString checked only the upper bound (arrayLenAllowedRemain / Byte.MAX_VALUE) against the attacker-supplied length/count, never `< 0`. A negative value passed the check, *inflated* the cumulative array budget (subtracting a negative), and then threw an uncaught NegativeArraySizeException at Array.newInstance / new byte[]/char[]/String[] instead of a clean, fail-secure InvalidObjectException. Add `< 0` guards so a malformed length is rejected before any allocation or budget arithmetic. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…init Root cause of the harness CNFE (net.jini.activation.arg.MarshalledObject): delegate discovery loaded, INITIALIZED, and instantiated EVERY registered MarshalDelegate provider just to call serves() on it, and the generated platform delegate's <clinit> eagerly LINKED its whole served set (Class[] literals) -- one of which transitively needs net.jini.activation.arg. MarshalledObject. Under a loader that cannot resolve that class the <clinit> failed, so delegate resolution threw for EVERY class, not just events/activation. Two complementary fixes: - Service.providerNames + MarshalDelegates.resolve: discover provider class NAMES and package-filter BEFORE loading, then load (initialize=false) and instantiate only the co-package delegate, with per-provider try/catch. An unrelated provider is never loaded/initialized; one that cannot be instantiated no longer breaks resolution of the others. - MarshalDelegateProcessor: the generated delegate references its served classes BY NAME (String[] SERVED_NAMES) with a name-based serves(), name-keyed serialForm/serialize/create dispatch, and a lazily-resolved servedClasses(). A generated delegate's <clinit> no longer links its served set; a served class is linked only when actually dispatched (when it is the class being marshalled, hence already loaded). Verified (Zulu 25 dist, DirtyChai runtime): platform 310 + GeneratedMarshal- DelegateTest 4/4 + ServiceOsgiTest 8/8 green; qa mercury MercuryProxyEquality- Test now PASSES end-to-end (previously hung on the CNFE), with 0 ClassNotFound/ NoClassDefFound in the run. Co-Authored-By: Claude Opus 4.8 <[email protected]>
…on the local class deSerializationPermitted() skipped the DeSerializationPermission check when the object appeared "stateless", but it decided that from the STREAM-supplied descriptor (osci.hasWriteObjectData / numObjFields / fields) -- attacker-controlled bytes. A crafted descriptor advertising zero object fields and no write method could bypass the gate for a class the local side considers stateful. Decide statelessness from the RESOLVED LOCAL class hierarchy instead: walk the local ObjectStreamClass chain and require the gate unless every level genuinely has no readObject/readObjectNoData and no non-primitive serialisable fields (and require it if a local descriptor is unavailable). hasReadObject/hasReadObjectNoData were already local-class based; only the field/write-method check trusted the stream. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.