Skip to content

Commit c9d3831

Browse files
authored
Merge pull request #63 from jsonjoy-com/nfs-server
NFS v4.0 server demo
2 parents b01e41f + 95c2607 commit c9d3831

File tree

97 files changed

+58223
-1441
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

97 files changed

+58223
-1441
lines changed

docs/nfs/missing-features.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Missing NFS Features Overview
2+
3+
The current code base implements NFSv4.0 semantics. Bringing it to feature parity with NFSv4.1 and NFSv4.2 requires the enhancements summarized below. Use this document as a high-level checklist for product managers, architects, and engineers.
4+
5+
## Legend
6+
7+
- **Status**: ✔️ complete · ⚠️ planned · ❌ missing
8+
- **Impact**: H (high), M (medium), L (low) effort/risk assessment
9+
10+
## NFSv4.1 Gaps
11+
12+
| Feature Area | Status | Impact | Notes |
13+
|--------------|:------:|:------:|-------|
14+
| Session protocol (`EXCHANGE_ID`, `CREATE_SESSION`, `SEQUENCE`, replay cache) || H | Must replace legacy `SETCLIENTID` handshake, manage slot tables, introduce session persistence, replay detection, and trunking support per RFC 5661 §18. |
15+
| Backchannel callbacks || M | Needed for delegations and pNFS recalls. Requires callback transport negotiation and request dispatch. |
16+
| pNFS layouts and device management || H | Implement `GETDEVICELIST`, `GETDEVICEINFO`, `LAYOUTGET/RETURN/COMMIT/ERROR/STATS`, plus registry for device IDs and layout drivers. |
17+
| Layout-aware client tooling || M | Client helpers must negotiate layouts, interpret device info, and choose data paths. |
18+
| Recovery improvements (`RECLAIM_COMPLETE`, `FREE_STATEID`, `TEST_STATEID`) || M | Enables robust crash recovery and state reclamation. |
19+
| Secret state verification (`SET_SSV`) || M | Required for secure session re-establishment. |
20+
| Attribute expansion (layout hints, fs status, etc.) || M | Update bitmaps and server responses to include new attributes; affects GETATTR/SETATTR flows. |
21+
| Observability for sessions/pNFS || L | Needed to monitor slot exhaustion, recalls, and layout churn. |
22+
23+
## NFSv4.2 Gaps
24+
25+
| Feature Area | Status | Impact | Notes |
26+
|--------------|:------:|:------:|-------|
27+
| Sparse file enhancements (`READ_PLUS`, `ALLOCATE`, `DEALLOCATE`) || H | Requires encoder/decoder updates and filesystem support for hole punching and zero-cost reads. |
28+
| Application I/O hints (`IO_ADVISE`, `SEEK`) || M | Guides caching and positioning; backend hooks to adjust behavior. |
29+
| Server-side clone/copy (`COPY`, `COPY_NOTIFY`, `OFFLOAD_STATUS`, `CLONE`, `OFFLOAD_CANCEL`) || H | Demands asynchronous copy manager, inter-server RPCSEC_GSSv3 support, and backend integration. |
30+
| Extended attributes (`GETXATTR`, `SETXATTR`, `LISTXATTR`, `REMOVEXATTR`) || M | Provide POSIX-like xattr functionality with size/error handling. |
31+
| Application Data Block (ADB) support || M | Adds data integrity metadata for READ_PLUS transfers. |
32+
| Labeled NFS (`sec_label`, MAC enforcement, operating modes) || H | Mandates policy integration, attribute handling, and mode negotiation. |
33+
| Observability for copy/sparse/xattr flows || L | Logging/metrics for new operations to aid debugging. |
34+
| Documentation updates | ⚠️ | L | Track new configuration steps, operational guidance, and compatibility matrices. |
35+
36+
## Cross-Cutting Concerns
37+
38+
- **Testing**: Integration suites must be expanded to cover sessions, pNFS, sparse files, server-side copy, xattrs, and labeled access control.
39+
- **Security**: RPCSEC_GSSv3 support is mandatory for secure copy-offload; policy checks must extend to sec_label and SSV workflows.
40+
- **Performance**: Slot table sizing, layout caching, and clone/copy offload should be tuned for high-throughput workloads.
41+
42+
## Next Steps
43+
44+
1. Execute the task breakdowns in `v4.1-implementation-plan.md` and `v4.2-implementation-plan.md`.
45+
2. Prioritize high-impact items (sessions, pNFS, server-side copy) to unlock interoperability with modern NFS clients.
46+
3. Coordinate with operations teams to validate required filesystem/backing service capabilities.
47+
4. Maintain an updated gap tracker as features graduate from planned to completed.

docs/nfs/v4.1-implementation-plan.md

Whitespace-only changes.
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# NFSv4.2 Implementation Plan
2+
3+
This document extends the v4.1 upgrade effort with the additional requirements defined in RFC 7862. Each task lists a ready-to-use LLM prompt to accelerate implementation.
4+
5+
## Prerequisites
6+
7+
- Completed NFSv4.1 implementation (sessions, pNFS, recovery tooling) and passing integration suite.
8+
- Working knowledge of RFC 7862 sections describing server-side copy, sparse files, application I/O advice, ADB, and labeled NFS.
9+
- Filesystem backend capable of hole punching, cloning, and extended attributes (or mockable interfaces when unavailable).
10+
11+
## Phase 1 · Protocol Additions
12+
13+
| # | Task | Guidance | LLM Prompt |
14+
|---|------|----------|------------|
15+
| 1 | Expand opcode/attribute catalogs | Add v4.2 operations (`ALLOCATE`, `DEALLOCATE`, `READ_PLUS`, `COPY`, etc.) and new attributes (`clone_blksize`, `space_freed`, `change_attr_type`, `sec_label`, etc.) to constants, builders, and attribute maps. | `Update src/nfs/v4/constants.ts, builder.ts, and attributes.ts to include every opcode, error, and attribute introduced in RFC 7862. Ensure bitmap handling scales beyond current word count and extend unit tests covering the new ranges.` |
16+
| 2 | Implement message/struct support | Create classes and serializers for new requests/responses (COPY_NOTIFY, OFFLOAD_STATUS, IO_ADVISE, SEEK, xattr ops, etc.). | `Add TypeScript request/response classes for all NFSv4.2 operations, wiring them into the encoder/decoder stack. Provide fixtures and round-trip tests proving binary compatibility with RFC 7862 examples.` |
17+
18+
## Phase 2 · Sparse File & I/O Enhancements
19+
20+
| # | Task | Guidance | LLM Prompt |
21+
|---|------|----------|------------|
22+
| 3 | READ_PLUS pipeline | Implement READ_PLUS decoding/encoding, supporting data, hole, and sparse-aware elements. | `Implement READ_PLUS handling on both client and server: encode READ_PLUS responses with data and hole segments, decode them client-side, and add tests demonstrating sparse file transfers.` |
23+
| 4 | ALLOCATE/DEALLOCATE semantics | Map allocation and hole-punching requests to filesystem APIs, including quota checks and error handling. | `Wire ALLOCATE and DEALLOCATE operations into the filesystem adapter layer, calling underlying fallocate/punch-hole APIs or mocks. Validate behavior with unit tests and ensure stateids remain consistent.` |
24+
| 5 | IO_ADVISE support | Accept and persist application I/O hints, applying them to caching policies or forwarding to backend drivers. | `Implement IO_ADVISE request processing, store per-file advice, and expose hooks for storage backends to react. Cover key hint types (sequential, random, willneed, dontneed) with tests.` |
25+
26+
## Phase 3 · Server-Side Copy & Clone
27+
28+
| # | Task | Guidance | LLM Prompt |
29+
|---|------|----------|------------|
30+
| 6 | COPY & COPY_NOTIFY workflows | Coordinate inter/intra-server copy lifecycle, including asynchronous state management and error mapping. | `Create a CopyManager coordinating COPY_NOTIFY exchanges, COPY execution, and OFFLOAD_STATUS polling. Handle chunked progress, cancellation, and error propagation per RFC 7862 §4.` |
31+
| 7 | CLONE integration | Support instantaneous clones when backend allows and fall back to server-side copy otherwise. | `Implement the CLONE operation, checking backend capabilities before cloning ranges. If unsupported, fall back to COPY-based replication. Add tests covering both clone-success and copy-fallback paths.` |
32+
| 8 | OFFLOAD controls | Handle `OFFLOAD_CANCEL` and `OFFLOAD_STATUS`, ensuring clients can monitor/cancel long-running copies. | `Add handlers for OFFLOAD_STATUS and OFFLOAD_CANCEL that query the CopyManager and control in-progress copies. Provide tests simulating cancellation mid-transfer and status polling.` |
33+
34+
## Phase 4 · Extended Attributes & Data Integrity
35+
36+
| # | Task | Guidance | LLM Prompt |
37+
|---|------|----------|------------|
38+
| 9 | XATTR operations | Implement `GETXATTR`, `SETXATTR`, `LISTXATTR`, `REMOVEXATTR`, respecting size limits and ACL constraints. | `Implement the xattr operation family, integrating with filesystem adapters for storage/retrieval. Add validation for size limits and permissions, and cover error paths in tests.` |
39+
| 10 | Application Data Block (ADB) | Introduce data block descriptors, integrity metadata, and READ_PLUS examples demonstrating corruption detection. | `Implement the Application Data Block framework: define data block descriptors, support READ_PLUS segments carrying block metadata, and add tests showing detection of mismatched checksums.` |
40+
41+
## Phase 5 · Labeled NFS & Security
42+
43+
| # | Task | Guidance | LLM Prompt |
44+
|---|------|----------|------------|
45+
| 11 | Security label attribute | Add `sec_label` attribute support to GETATTR/SETATTR, including policy translation layers. | `Implement security label handling: parse and set sec_label attributes, integrate with policy engine hooks, and enforce MAC checks on access. Add tests covering label propagation.` |
46+
| 12 | Labeled operation modes | Support Full, Limited Server, and Guest modes, including discovery via attributes and layout considerations. | `Implement Labeled NFS modes: expose capabilities through attributes, gate operations by policy, and ensure layout recall respects label constraints. Cover mode transitions with tests.` |
47+
| 13 | RPCSEC_GSSv3 integration | Enable secure inter-server copy by negotiating RPCSEC_GSSv3 contexts and propagating credentials. | `Integrate RPCSEC_GSSv3 for inter-server copy workflows. Negotiate security contexts during COPY_NOTIFY, wrap copy RPCs, and add tests verifying failure when peers lack support.` |
48+
49+
## Phase 6 · Telemetry & Compliance
50+
51+
| # | Task | Guidance | LLM Prompt |
52+
|---|------|----------|------------|
53+
| 14 | Metrics & logging | Extend observability to cover sparse operations, copy lifecycle, xattr usage, and labeled-mode decisions. | `Extend the logging/metrics framework to emit events for READ_PLUS holes, copy progress, xattr mutations, and label enforcement. Create tests ensuring logs appear with expected fields.` |
54+
| 15 | Integration scenarios | Build end-to-end tests covering sparse IO, server-side copy, xattrs, and labeled NFS enforcement. | `Create integration tests exercising READ_PLUS on sparse files, COPY/CLONE operations, xattr round-trips, and labeled NFS access control. Use fixtures mirroring RFC 7862 examples.` |
55+
| 16 | Documentation & rollout | Update docs with deployment guidance, compatibility notes, and new configuration toggles. | `Document the NFSv4.2 feature set: write setup guides for sparse file support, copy offload, and labeled NFS. Update docs/nfs/ with troubleshooting tips and release notes.` |
56+
57+
## Exit Criteria
58+
59+
- All NFSv4.2 mandatory and recommended features implemented or explicitly flagged as unsupported.
60+
- Integration suite validates sparse file handling, copy offload, xattrs, and security labels.
61+
- Documentation and observability ensure operators can deploy and monitor the new capabilities.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,8 @@
6969
"@jsonjoy.com/json-pointer": "^1.0.2",
7070
"@jsonjoy.com/util": "^1.9.0",
7171
"hyperdyperid": "^1.2.0",
72-
"thingies": "^2.5.0"
72+
"thingies": "^2.5.0",
73+
"tree-dump": "^1.1.0"
7374
},
7475
"devDependencies": {
7576
"@msgpack/msgpack": "^3.0.0-beta2",
@@ -96,6 +97,7 @@
9697
"js-base64": "^3.7.2",
9798
"jsbi": "^4.3.0",
9899
"json-pack-napi": "^0.0.2",
100+
"memfs": "^4.49.0",
99101
"messagepack": "^1.1.12",
100102
"msgpack-lite": "^0.1.26",
101103
"msgpack5": "^6.0.2",

src/cbor/__tests__/CborEncoder.spec.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,3 +418,96 @@ describe('JsonPackValue', () => {
418418
});
419419
});
420420
});
421+
422+
describe('buffer reallocation stress tests', () => {
423+
test('strings with non-ASCII triggering fallback with small buffer', () => {
424+
const smallWriter = new Writer(64);
425+
const smallEncoder = new CborEncoder(smallWriter);
426+
for (let round = 0; round < 50; round++) {
427+
smallWriter.reset();
428+
for (let i = 0; i < 500; i++) {
429+
const str = 'test_' + i + '_\x00\x01\x02';
430+
const encoded = smallEncoder.encode(str);
431+
const decoded = decode(encoded);
432+
expect(decoded).toBe(str);
433+
}
434+
}
435+
});
436+
437+
test('very long strings that exceed ensureCapacity pre-allocation', () => {
438+
// Use a Writer with initial capacity smaller than what a single string will need
439+
const tinyWriter = new Writer(32);
440+
const tinyEncoder = new CborEncoder(tinyWriter);
441+
for (let round = 0; round < 20; round++) {
442+
tinyWriter.reset();
443+
// Create a string that's long enough to require more than the tiny buffer
444+
const str = 'x'.repeat(100) + '\x00\x01\x02' + 'y'.repeat(100);
445+
const encoded = tinyEncoder.encode(str);
446+
const decoded = decode(encoded);
447+
expect(decoded).toBe(str);
448+
}
449+
});
450+
451+
test('alternating short and long strings with non-ASCII', () => {
452+
const smallWriter = new Writer(64);
453+
const smallEncoder = new CborEncoder(smallWriter);
454+
for (let round = 0; round < 30; round++) {
455+
smallWriter.reset();
456+
for (let i = 0; i < 100; i++) {
457+
// Alternate between short strings with control chars and longer strings
458+
const str = i % 2 === 0 ? 'short_\x00\x01\x02_' + i : 'a'.repeat(50) + '\x03\x04' + 'b'.repeat(50);
459+
const encoded = smallEncoder.encode(str);
460+
const decoded = decode(encoded);
461+
expect(decoded).toBe(str);
462+
}
463+
}
464+
});
465+
466+
test('many iterations with long strings', () => {
467+
const smallWriter = new Writer(64);
468+
const smallEncoder = new CborEncoder(smallWriter);
469+
for (let round = 0; round < 10; round++) {
470+
smallWriter.reset();
471+
for (let i = 0; i < 1000; i++) {
472+
const str = 'a'.repeat(Math.floor(Math.random() * 32768));
473+
const encoded = smallEncoder.encode(str);
474+
const decoded = decode(encoded);
475+
expect(decoded).toBe(str);
476+
}
477+
}
478+
});
479+
480+
test('objects with many short strings', () => {
481+
const smallWriter = new Writer(64);
482+
const smallEncoder = new CborEncoder(smallWriter);
483+
for (let round = 0; round < 100; round++) {
484+
smallWriter.reset();
485+
const obj: Record<string, string> = {};
486+
for (let i = 0; i < 100; i++) {
487+
obj['key_' + i] = 'value_' + i;
488+
}
489+
const encoded = smallEncoder.encode(obj);
490+
const decoded = decode(encoded);
491+
expect(decoded).toEqual(obj);
492+
}
493+
});
494+
495+
test('mixed objects and strings with buffer growth', () => {
496+
const smallWriter = new Writer(64);
497+
const smallEncoder = new CborEncoder(smallWriter);
498+
for (let round = 0; round < 50; round++) {
499+
smallWriter.reset();
500+
const data = {
501+
str1: 'test_\x00\x01',
502+
nested: {
503+
str2: 'nested_\x02\x03',
504+
arr: ['a', 'b', 'c_\x04'],
505+
},
506+
str3: 'final_\x05\x06\x07',
507+
};
508+
const encoded = smallEncoder.encode(data);
509+
const decoded = decode(encoded);
510+
expect(decoded).toEqual(data);
511+
}
512+
});
513+
});

src/json/JsonEncoder.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,8 @@ export class JsonEncoder implements BinaryJsonEncoder, StreamingBinaryJsonEncode
146146
const length = str.length;
147147
writer.ensureCapacity(length * 4 + 2);
148148
if (length < 256) {
149-
let x = writer.x;
149+
const startX = writer.x;
150+
let x = startX;
150151
const uint8 = writer.uint8;
151152
uint8[x++] = 0x22; // "
152153
for (let i = 0; i < length; i++) {
@@ -158,15 +159,20 @@ export class JsonEncoder implements BinaryJsonEncoder, StreamingBinaryJsonEncode
158159
break;
159160
}
160161
if (code < 32 || code > 126) {
161-
writer.utf8(JSON.stringify(str));
162+
writer.x = startX;
163+
const jsonStr = JSON.stringify(str);
164+
writer.ensureCapacity(jsonStr.length * 4 + 4);
165+
writer.utf8(jsonStr);
162166
return;
163167
} else uint8[x++] = code;
164168
}
165169
uint8[x++] = 0x22; // "
166170
writer.x = x;
167171
return;
168172
}
169-
writer.utf8(JSON.stringify(str));
173+
const jsonStr = JSON.stringify(str);
174+
writer.ensureCapacity(jsonStr.length * 4 + 4);
175+
writer.utf8(jsonStr);
170176
}
171177

172178
public writeAsciiStr(str: string): void {

src/json/__tests__/JsonEncoder.spec.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,3 +180,76 @@ describe('nested object', () => {
180180
});
181181
});
182182
});
183+
184+
describe('buffer reallocation stress tests', () => {
185+
test('strings with non-ASCII triggering fallback (reproduces writer.x bug)', () => {
186+
// This specifically tests the bug where writer.x is not reset before fallback
187+
// When a short string (<256) contains non-ASCII, it triggers writer.utf8()
188+
// but writer.x has already been incremented by writing the opening quote
189+
for (let round = 0; round < 50; round++) {
190+
const smallWriter = new Writer(64);
191+
const smallEncoder = new JsonEncoder(smallWriter);
192+
193+
for (let i = 0; i < 500; i++) {
194+
// Create strings < 256 chars with non-ASCII character to trigger fallback
195+
const asciiPart = 'a'.repeat(Math.floor(Math.random() * 200));
196+
const value = {foo: asciiPart + '\u0001' + asciiPart}; // control char triggers fallback
197+
const encoded = smallEncoder.encode(value);
198+
const json = Buffer.from(encoded).toString('utf-8');
199+
const decoded = JSON.parse(json);
200+
expect(decoded).toEqual(value);
201+
}
202+
}
203+
});
204+
205+
test('many iterations with long strings (reproduces writer.utf8 bug)', () => {
206+
// Run multiple test rounds to increase chance of hitting the bug
207+
for (let round = 0; round < 10; round++) {
208+
const smallWriter = new Writer(64);
209+
const smallEncoder = new JsonEncoder(smallWriter);
210+
211+
for (let i = 0; i < 1000; i++) {
212+
const value = {
213+
foo: 'a'.repeat(Math.round(32000 * Math.random()) + 10),
214+
};
215+
const encoded = smallEncoder.encode(value);
216+
const json = Buffer.from(encoded).toString('utf-8');
217+
const decoded = JSON.parse(json);
218+
expect(decoded).toEqual(value);
219+
}
220+
}
221+
});
222+
223+
test('repeated long strings >= 256 chars (reproduces writer.utf8 bug)', () => {
224+
// Run multiple test rounds to increase chance of hitting the bug
225+
for (let round = 0; round < 20; round++) {
226+
const smallWriter = new Writer(64);
227+
const smallEncoder = new JsonEncoder(smallWriter);
228+
229+
for (let i = 0; i < 100; i++) {
230+
const length = 256 + Math.floor(Math.random() * 10000);
231+
const value = {foo: 'a'.repeat(length)};
232+
const encoded = smallEncoder.encode(value);
233+
const json = Buffer.from(encoded).toString('utf-8');
234+
const decoded = JSON.parse(json);
235+
expect(decoded).toEqual(value);
236+
}
237+
}
238+
});
239+
240+
test('many short strings with buffer growth (reproduces writer.utf8 bug)', () => {
241+
// Run multiple test rounds to increase chance of hitting the bug
242+
for (let round = 0; round < 10; round++) {
243+
const smallWriter = new Writer(64);
244+
const smallEncoder = new JsonEncoder(smallWriter);
245+
246+
for (let i = 0; i < 1000; i++) {
247+
const value = {foo: 'test' + i};
248+
const encoded = smallEncoder.encode(value);
249+
const json = Buffer.from(encoded).toString('utf-8');
250+
const decoded = JSON.parse(json);
251+
expect(decoded).toEqual(value);
252+
}
253+
}
254+
});
255+
});

0 commit comments

Comments
 (0)