Skip to content

Issue 277 reed-solomon#316

Open
adela-marin wants to merge 9 commits into
xoriors:mainfrom
adela-marin:issue-277-reed-solomon
Open

Issue 277 reed-solomon#316
adela-marin wants to merge 9 commits into
xoriors:mainfrom
adela-marin:issue-277-reed-solomon

Conversation

@adela-marin
Copy link
Copy Markdown
Collaborator

@adela-marin adela-marin commented Dec 6, 2025

Use Reed-Solomon error correction (issue #277 )

Description

Add a minimal Reed-Solomon error-correction example algorithm to see how the data can be reconstructed. It provides a mechanism to create parity shards for files so the project can tolerate disk/device failures.


Type of change

  • New feature (non-breaking change which adds functionality)

Checklist:

  • I have performed a self-review of my code
  • I have tested my code on different platforms (if applicable)
  • I have commented my code, particularly in hard-to-understand areas
  • I have added necessary documentation (if appropriate)
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

Add `reed-solomon-erasure = \"5\"` to provide Reed–Solomon encoding/decoding support for future error-correction feature.
Add `reed-solomon-erasure = \"5\"` so the Reed–Solomon helper is available as `crate::crypto::rs`."
@adela-marin adela-marin marked this pull request as draft December 6, 2025 09:59
@adela-marin adela-marin changed the base branch from cdl to main December 6, 2025 10:35
@radumarias radumarias marked this pull request as ready for review December 13, 2025 09:52
Copy link
Copy Markdown
Member

@radumarias radumarias left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review from claude code

▐▛███▜▌ Claude Code v2.0.65
▝▜█████▛▘ Opus 4.5 · Claude API
▘▘ ▝▝ ~/dev/xorio/rencfs

Code Review: PR #316 - Reed-Solomon Error Correction

Overview

This PR implements Reed-Solomon error correction for rencfs (Issue #277), allowing data recovery from parity shards when the main file is corrupted or missing. The implementation adds:

  • New module: src/crypto/rs.rs - Core RS encoding/decoding logic
  • Integration: RS encoding on file release, reconstruction on file open
  • Configuration: Optional RsConfig struct with data/parity shard counts
  • Example: examples/rs_example.rs demonstrating usage
  • Tests: Unit test for the RS encoder and integration test for full workflow

Changes: +398 additions, -7 deletions across 11 files


Code Quality Analysis

✅ Strengths

  1. Opt-in design: RS is optional via Option, respecting users who don't want storage overhead
  2. Clean separation: RS logic isolated in its own module (src/crypto/rs.rs)
  3. Comprehensive test: Integration test simulates file corruption and recovery
  4. Length prefix encoding: Smart approach to handle arbitrary data sizes

❌ Issues & Concerns

  1. Critical: Incorrect Data Sharding Model (src/encryptedfs.rs:1831-1893)

The reconstruction logic assumes the original file is shard 0, but the encoding splits data across ALL data shards:

// Encoding: data split across data_shards (0, 1, 2)
// Reconstruction: only loads original file as shard[0], nothing for shards 1, 2
shards_opt[0] = Some(data); // Only shard 0!

Problem: With data_shards=3, reconstruction requires 3 shards minimum, but the code only provides 1 data shard + parity shards. This means reconstruction will always fail for the current implementation.

Recommendation: Either:

  • Store all data shards separately (not just the original file)
  • Or use data_shards=1 model where the original file IS the single data shard
  1. Memory Leak via Box::leak (src/encryptedfs.rs:1880-1888)

return Err(FsError::Other(Box::leak(format!("...").into_boxed_str())));

Using Box::leak creates permanent memory leaks. The project already has FsError::Other(&'static str) which expects static strings.

Recommendation: Add a new error variant for RS failures:
#[error("Reed-Solomon error: {0}")]
ReedSolomonError(String),

  1. Blocking I/O in Async Context (src/encryptedfs.rs:1531-1565, 1838-1893)

if let Ok(content) = std::fs::read(&content_path) { // Blocking!

Using synchronous std::fs::read/write in async methods blocks the Tokio runtime.

Recommendation: Use tokio::fs::read / tokio::fs::write with .await

  1. Missing Error Propagation (src/encryptedfs.rs:1553-1559)

if let Err(e) = std::fs::write(&shard_path, shard) {
warn!("Failed to write parity shard {:?}: {}", shard_path, e);
}

Encoding failures are silently logged but not propagated. User may think data is protected when it isn't.

Recommendation: Consider making parity write failures a hard error or at least return a warning to the caller.

  1. Inconsistent use of warn! vs error!
  • Line 1559: warn! for parity write failure
  • Line 1563: warn! for encode failure
  • Line 1874: error! for reconstruction failure

These all seem like similar severity levels; consider consistent logging.


Security Considerations

⚠️ Concerns

  1. Parity shards are unencrypted data fragments: The encrypted content is split into shards. While the content is already encrypted, the existence of .parity.X files leaks metadata about which files have RS protection enabled.
  2. No integrity verification on shards: Maliciously crafted parity shards could potentially cause reconstruction to produce garbage data. Consider adding checksums.
  3. Shard files not cleaned up on delete: When a file is deleted, the corresponding .parity.X files should also be removed to avoid dangling files and potential data leakage.

Test Coverage

✅ Good

  • Unit test for RsEncoder::encode/reconstruct
  • Integration test test_reed_solomon_error_correction covers full workflow

❌ Missing

  • No test for partial corruption (file exists but is corrupted)
  • No test for insufficient shards scenario
  • No test for parity shard cleanup on file deletion
  • No test for set_len (truncation) interaction with RS
  • No negative test for when data_shards > 1 (which will fail with current impl)

Performance Implications

  1. Every file release triggers RS encoding + parity writes - could be slow for large files
  2. Synchronous reads of entire file content into memory (std::fs::read) - problematic for large files
  3. Older parking_lot version (0.11.2) pulled in by reed-solomon-erasure - adds duplicate dependency

Specific Recommendations

Must Fix

  1. Fix the sharding model: Current implementation won't work with data_shards > 1. Either:
    - Document that only data_shards=1 is supported, or
    - Store all data shards separately
  2. Remove Box::leak: Add proper error variant to avoid memory leaks
  3. Use async I/O: Replace std::fs::{read,write} with tokio::fs

Should Fix

  1. Add parity cleanup in remove_file / deletion logic
  2. Handle large files with streaming instead of loading entire file into memory
  3. Propagate encoding errors to caller instead of silent logging

Nice to Have

  1. Add more comprehensive tests for edge cases
  2. Consider adding a checksum to each shard for integrity verification
  3. Add CLI flag to enable/configure RS settings

Summary

Category Rating
Code Correctness ⚠️ Has critical bug
Code Style ✅ Good
Security ⚠️ Some concerns
Tests ⚠️ Needs more coverage
Performance ⚠️ Blocking I/O issues

Verdict: The PR shows good understanding of Reed-Solomon concepts and clean code structure, but has a critical bug in the sharding model that would cause reconstruction to fail in practice. The memory leak from Box::leak should also be fixed before merge.

Recommend requesting changes to address the critical issues before approval.

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

 
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
? for shortcuts 51102 tokens
globalVersion: 2.0.65 · latestVersion: 2.0.69

@radumarias
Copy link
Copy Markdown
Member

  • ▐▛███▜▌ * Claude Code v2.0.69
  • ▝▜█████▛▘ * Opus 4.5 · Claude API
  • ▘▘ ▝▝ * ~/dev/xorio/rencfs

/pr-review-toolkit:review-pr is running…

Code Review: Reed-Solomon Error Correction PR

Files Reviewed

- /home/radu/dev/xorio/rencfs/src/crypto/rs.rs (new file)
- /home/radu/dev/xorio/rencfs/src/encryptedfs.rs (RS integration)
- /home/radu/dev/xorio/rencfs/src/encryptedfs/test.rs (test additions)
- /home/radu/dev/xorio/rencfs/examples/rs_example.rs (new example)
- Various updated call sites (examples, mount/linux.rs, test_common.rs)

---
Critical Issues (Confidence 90-100)

1. Memory Leak via Box::leak (Confidence: 95)

File: /home/radu/dev/xorio/rencfs/src/encryptedfs.rs
Lines: 1891, 1897, 1903-1906

return Err(FsError::Other(Box::leak(format!("Failed to reconstruct file: {}", e).into_boxed_str())));

Problem: Using Box::leak creates a permanent memory leak every time an error occurs. The FsError::Other variant takes a &'static 
str, and leaking heap-allocated strings to create static references is a memory leak pattern. In a long-running filesystem,
repeated reconstruction failures would leak memory indefinitely.

CLAUDE.md Relevance: The project emphasizes "Memory safety through mlock(2), mprotect, and zeroize" and careful memory handling.
Intentional memory leaks contradict this security-focused design.

Fix Suggestion: Add a new FsError variant for reconstruction errors:
// In FsError enum:
#[error("Reed-Solomon reconstruction failed: {0}")]
RsReconstructionFailed(String),
Then use return Err(FsError::RsReconstructionFailed(format!("...")));

---
2. Fundamental Design Flaw: RS Encoding Logic Mismatch (Confidence: 98)

File: /home/radu/dev/xorio/rencfs/src/encryptedfs.rs
Lines: 1535-1566 (encoding) and 1841-1908 (reconstruction)

Problem: The RS implementation has a fundamental design inconsistency:

During encoding (release):
- The entire encrypted file is treated as a single payload
- This payload is split into data_shards pieces
- The encoder generates parity_shards additional shards
- Only the parity shards are written to disk (lines 1551-1557)
- The original file remains unchanged (it IS the "data shards" combined)

During reconstruction (open):
- The code expects data_shards + parity_shards total shards
- It puts the main file into shards_opt[0] only (line 1860)
- Data shards 1 through data_shards-1 are never populated (they remain None)
- The check available_shards >= rs_config.data_shards (line 1880) uses wrong logic

Example with default config (3 data, 2 parity):
- Encoding: File split into 3 data shards, 2 parity shards written
- Reconstruction: Only shard[0] (main file if exists) + shard[3], shard[4] (parity) are loaded
- Shards[1] and shards[2] (data shards 2 and 3) are NEVER loaded from anywhere

This means reconstruction will always fail when the main file is missing because the code stores the complete file as-is but
expects it to be reconstructed from 3 separate data shards that were never stored.

Test passes by coincidence: The test deletes the main file but still has 2 parity shards. With this flawed logic, available_shards 
= 2 (both parity shards), which is less than data_shards = 3, so it should fail... unless the reconstruction incorrectly succeeds
due to other issues.

Fix: The entire approach needs rethinking. Either:
1. Store all data shards separately (not just parity), or
2. Use a simpler approach where the file IS shard 0 and you only need parity shards for error detection/correction (not full
reconstruction from missing data shards)

---
3. Parity Shards Not Deleted on File Removal (Confidence: 92)

File: /home/radu/dev/xorio/rencfs/src/encryptedfs.rs
Lines: 966-1026 (remove_file function)

Problem: When a file is deleted via remove_file(), only the main content file is deleted (line 1006). The parity shard files
({ino}.parity.0, {ino}.parity.1, etc.) are never cleaned up, leading to:
1. Orphaned files accumulating on disk
2. Potential data leakage if parity shards contain recoverable information
3. Disk space waste

Fix: Add cleanup of parity shards in remove_file():
// After fs::remove_file(self_clone.contents_path(attr.ino))?;
if let Some(ref rs_config) = self_clone.rs_config {
    for i in 0..rs_config.parity_shards {
        let parity_path = self_clone.contents_path(attr.ino)
            .parent().unwrap()
            .join(format!("{}.parity.{}", attr.ino, i));
        let _ = fs::remove_file(parity_path);  // ignore errors
    }
}

---
Important Issues (Confidence 80-89)

4. Synchronous File I/O in Async Context (Confidence: 85)

File: /home/radu/dev/xorio/rencfs/src/encryptedfs.rs
Lines: 1541, 1555, 1847, 1858, 1872, 1889

Problem: Multiple uses of std::fs::read(), std::fs::write(), and std::fs::metadata() within async functions. While the existing
codebase does use std::fs in places, these new operations can be blocking during RS encoding/decoding of large files.

Partial Mitigation: The existing codebase pattern shows synchronous I/O is sometimes acceptable here. However, for large files, RS
encoding could block the async runtime.

Suggestion: Consider wrapping in tokio::task::spawn_blocking for large operations, or document this as a known limitation.

---
5. Missing Input Validation for RsConfig (Confidence: 88)

File: /home/radu/dev/xorio/rencfs/src/crypto/rs.rs
Lines: 20-26 (RsEncoder::new)

Problem: No validation that data_shards > 0 and parity_shards > 0. Passing 0 for either will cause panics or unexpected behavior in
 the underlying reed_solomon_erasure library.

pub fn new(data_shards: usize, parity_shards: usize) -> Self {
    Self {
        data_shards,
        parity_shards,
    }
}

Fix: Add validation:
pub fn new(data_shards: usize, parity_shards: usize) -> Result<Self, &'static str> {
    if data_shards == 0 {
        return Err("data_shards must be > 0");
    }
    if parity_shards == 0 {
        return Err("parity_shards must be > 0");
    }
    Ok(Self { data_shards, parity_shards })
}

---
6. Test Uses Private Method contents_path (Confidence: 82)

File: /home/radu/dev/xorio/rencfs/src/encryptedfs/test.rs
Lines: 2533, 2538, 2543, 2556

Problem: The test directly accesses fs.contents_path(attr.ino) which is a private method (defined without pub at line 2527 of
encryptedfs.rs). Tests in the same module can access private members, but this creates a brittle test that depends on internal
implementation details.

Suggestion: Either make this a documented internal testing pattern or expose a test-only helper.

---
7. Silent Failure on RS Encoding Errors (Confidence: 81)

File: /home/radu/dev/xorio/rencfs/src/encryptedfs.rs
Lines: 1561-1563

Err(e) => {
    warn!("RS encoding failed for inode {}: {}", ino, e);
}

Problem: When RS encoding fails during file release, the operation continues successfully. The user has no indication that their
file lacks error correction protection. This could give false confidence that data is protected.

Suggestion: Consider returning an error or providing a way for users to know RS protection status. At minimum, upgrade to error!
logging level.

---
8. Read-Only Mode Can Write During Reconstruction (Confidence: 84)

File: /home/radu/dev/xorio/rencfs/src/encryptedfs.rs
Lines: 1889

Problem: During open(), if a file needs reconstruction, the code writes the reconstructed data back to disk (line 1889) even in
read-only mode. The read_only check happens earlier for writes but the reconstruction path bypasses this.

// Line 1829-1830 checks read_only for write flag
if write && self.read_only {
    return Err(FsError::ReadOnly);
}
// But reconstruction at line 1889 writes unconditionally
if let Err(e) = std::fs::write(&content_path, &recovered) {

Fix: Check self.read_only before writing reconstructed data.

---
Summary

The PR introduces Reed-Solomon error correction, which is a valuable feature for data integrity. However, there are several
critical issues that need to be addressed before merging:

1. Memory leaks through Box::leak pattern
2. Fundamental design flaw in how data shards vs parity shards are stored and reconstructed
3. Resource leakage with orphaned parity shard files on file deletion
4. Input validation missing for RS configuration
5. Read-only mode violation during reconstruction

The most significant issue (#2) appears to be a conceptual misunderstanding of how Reed-Solomon should be applied in this context.
The current implementation stores only parity shards but expects to reconstruct from data shards that were never persisted
separately.

● PR Review Summary: Reed-Solomon Error Correction (issue-277-reed-solomon)

Overview

This PR implements Reed-Solomon error correction for file recovery, adding ~400 lines across 11 files. The feature is opt-in via an RsConfig parameter.


Critical Issues (4 found) — Must fix before merge

# Agent Issue Location
1 code-reviewer Fundamental design flaw: RS reconstruction logic is broken. Only parity shards are stored, but reconstruction expects separate data shards that don't exist. The file is stored as-is but reconstruction tries to treat it as "shard 0" of many. src/encryptedfs.rs:1841-1908
2 code-reviewer Memory leak via Box::leak: Every reconstruction error leaks heap memory. In a long-running FS, this accumulates unbounded. src/encryptedfs.rs:1891,1897,1903-1906
3 silent-failure-hunter Silent failure on write path: If RS encoding fails, only warn! is logged and write appears successful. User believes file is protected when it isn't. src/encryptedfs.rs:1541-1565
4 code-reviewer Orphaned parity shards: When files are deleted, parity shard files ({ino}.parity.N) are never cleaned up — disk space leak and potential data leakage. src/encryptedfs.rs:966-1026

Important Issues (8 found) — Should fix

# Agent Issue Location
5 silent-failure-hunter Parity shard write failures are logged but silently ignored, resulting in partial protection src/encryptedfs.rs:1555-1557
6 type-design-analyzer No validation for RsConfig — allows data_shards=0 or parity_shards=0 src/crypto/rs.rs:5-26
7 type-design-analyzer Error type is Box — inconsistent with codebase pattern of thiserror enums src/crypto/rs.rs
8 code-reviewer Read-only mode bypassed during reconstruction — writes to disk even when read_only=true src/encryptedfs.rs:1889
9 silent-failure-hunter Silent failure reading parity shards — file exists but can't be read, silently skipped src/encryptedfs.rs:1871-1877
10 pr-test-analyzer Missing test for unrecoverable scenario (>2 shards missing) src/crypto/rs.rs
11 pr-test-analyzer Missing test for empty data handling src/crypto/rs.rs
12 comment-analyzer Misleading comment "original file is split across these" — file isn't actually split src/crypto/rs.rs:9

Suggestions (6 found) — Nice to have

# Agent Suggestion Location
13 type-design-analyzer Make RsConfig fields private with validated constructor src/crypto/rs.rs
14 type-design-analyzer Add RsEncoder::from_config() to connect types src/crypto/rs.rs
15 pr-test-analyzer Add tests for large data (1MB+) src/crypto/rs.rs
16 pr-test-analyzer Add tests for invalid shard counts src/crypto/rs.rs
17 code-reviewer Consider tokio::task::spawn_blocking for large file RS operations src/encryptedfs.rs
18 comment-analyzer Add doc comments for RsEncoder methods src/crypto/rs.rs

Strengths

  • Clean integration: The Option pattern makes RS opt-in without breaking existing APIs
  • Good test structure: The integration test properly verifies end-to-end flow
  • Correct length preservation: Smart use of 8-byte length prefix for exact reconstruction
  • Proper error propagation in read path: Reconstruction errors during open() are correctly returned (unlike write path)

Recommended Action Plan

Phase 1: Fix Critical Issues (Blocking)

  1. Re-design RS storage model — Either:
    - Store all data shards separately (significant change), OR
    - Simplify to 1 data shard (the file itself) + N parity shards and update comments/docs
  2. Replace Box::leak with proper error variant:
    #[error("RS reconstruction failed: {0}")]
    RsReconstructionFailed(String),
  3. Add parity shard cleanup in remove_file()
  4. Decide on write-path failure behavior: Either propagate error or document that RS is best-effort

Phase 2: Address Important Issues

  1. Add RsConfig validation (reject 0 values)
  2. Create RsError enum with thiserror
  3. Check read_only before reconstruction writes
  4. Add missing tests for edge cases

Phase 3: Polish

  1. Improve comments accuracy
  2. Add documentation
  3. Consider spawn_blocking for large files

Re-run Review After Fixes

After addressing critical issues, run:
/pr-review-toolkit:review-pr code errors

Critical fixes:
- Eliminate Box::leak memory leaks (add FsError::ReedSolomonError)
- Replace blocking std::fs with async tokio::fs
- Delete parity shards on file removal to prevent data leakage

Important fixes:
- Add input validation for RsEncoder (data_shards/parity_shards > 0)
- Document data_shards=1 limitation with debug_assert
- Respect read-only mode during reconstruction
- Only reconstruct missing files, not empty ones
Copilot AI review requested due to automatic review settings December 13, 2025 12:30
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds Reed-Solomon error correction functionality to enable file recovery from parity shards in case of data corruption or loss. The implementation uses the reed-solomon-erasure crate to create parity shards that can reconstruct missing or corrupted encrypted files.

Key changes:

  • Introduced a new RsEncoder module with encode/reconstruct capabilities for Reed-Solomon error correction
  • Extended EncryptedFs to support optional Reed-Solomon configuration, automatically creating parity shards on file write and reconstructing files on read when needed
  • Updated all EncryptedFs::new() call sites to include the new optional rs_config parameter

Reviewed changes

Copilot reviewed 11 out of 12 changed files in this pull request and generated 23 comments.

Show a summary per file
File Description
src/crypto/rs.rs New module implementing Reed-Solomon encoding and reconstruction logic
src/encryptedfs.rs Added rs_config field, parity shard creation on release(), reconstruction on open(), and parity cleanup on deletion
src/encryptedfs/test.rs Added comprehensive test for Reed-Solomon reconstruction scenario
src/test_common.rs Updated EncryptedFs::new() call to include None for rs_config parameter
src/mount/linux.rs Updated EncryptedFs::new() call to include None for rs_config parameter
examples/rs_example.rs New standalone example demonstrating Reed-Solomon encode/reconstruct
examples/file_handling.rs Updated EncryptedFs::new() call to include None for rs_config parameter
examples/encryptedfs.rs Updated EncryptedFs::new() call to include None for rs_config parameter
src/crypto.rs Added rs module export and removed unused error import from tracing
src/crypto/read.rs Removed redundant pub(crate) use of decrypt_block macro
Cargo.toml Added reed-solomon-erasure dependency
Cargo.lock Updated with new dependencies for reed-solomon-erasure

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/encryptedfs.rs
let mut available_shards = if shards_opt[0].is_some() { 1 } else { 0 };
for i in 0..rs_config.parity_shards {
let shard_path = content_path.parent()
.unwrap()
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error handling issue: Using .unwrap() on parent() can panic. Consider using .ok_or() or .expect() with a descriptive message to make debugging easier if this path operation ever fails.

Suggested change
.unwrap()
.ok_or_else(|| FsError::ReedSolomonError(format!(
"Cannot determine parent directory for content path: {:?}", content_path
)))?

Copilot uses AI. Check for mistakes.
Comment thread src/encryptedfs.rs Outdated
// Write parity shards (skip data shards which are the original file)
for (i, shard) in shards.iter().enumerate().skip(rs_config.data_shards) {
let shard_path = content_path.parent()
.unwrap()
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error handling issue: Using .unwrap() on parent() can panic. Consider using .ok_or() or .expect() with a descriptive message to make debugging easier if this path operation ever fails.

Suggested change
.unwrap()
.expect(&format!("content_path {:?} has no parent directory", content_path))

Copilot uses AI. Check for mistakes.
Comment thread src/crypto/rs.rs
Comment on lines +10 to +15
pub data_shards: usize,
/// Number of parity shards to create (for recovery).
pub parity_shards: usize,
}

pub struct RsEncoder {
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing validation: RsConfig lacks validation in its creation. The fields data_shards and parity_shards should be validated to ensure they are greater than 0 and that data_shards == 1 (as per the current implementation constraint). Consider adding a constructor method that validates these constraints or implementing validation in the EncryptedFs::new method.

Suggested change
pub data_shards: usize,
/// Number of parity shards to create (for recovery).
pub parity_shards: usize,
}
pub struct RsEncoder {
data_shards: usize,
/// Number of parity shards to create (for recovery).
parity_shards: usize,
}

Copilot uses AI. Check for mistakes.
Comment thread src/crypto/rs.rs
Comment on lines +34 to +100
let mut payload = Vec::with_capacity(8 + data.len());
payload.extend_from_slice(&(data.len() as u64).to_le_bytes());
payload.extend_from_slice(data);

let shard_size = (payload.len() + self.data_shards - 1) / self.data_shards;
let total_shards = self.data_shards + self.parity_shards;

let mut shards: Vec<Vec<u8>> = vec![vec![0u8; shard_size]; total_shards];

for i in 0..self.data_shards {
let start = i * shard_size;
let end = std::cmp::min(start + shard_size, payload.len());
if start < payload.len() {
shards[i][..end - start].copy_from_slice(&payload[start..end]);
}
}

let mut shard_refs: Vec<&mut [u8]> = shards.iter_mut().map(|v| v.as_mut_slice()).collect();
r.encode(&mut shard_refs)?;

Ok(shards)
}

pub fn reconstruct(
&self,
shards_opt: &mut [Option<Vec<u8>>],
) -> Result<Vec<u8>, Box<dyn Error>> {
let r = ReedSolomon::<Gf8>::new(self.data_shards, self.parity_shards)?;
let total_shards = self.data_shards + self.parity_shards;

if shards_opt.len() != total_shards {
return Err("shards length mismatch".into());
}

let shard_len = shards_opt
.iter()
.find_map(|s| s.as_ref().map(|v| v.len()))
.ok_or("no shards available")?;

for slot in shards_opt.iter_mut() {
if let Some(v) = slot {
if v.len() < shard_len {
v.resize(shard_len, 0u8);
} else if v.len() > shard_len {
return Err("inconsistent shard lengths".into());
}
}
}

r.reconstruct(shards_opt)?;

let mut payload = Vec::with_capacity(shard_len * self.data_shards);
for i in 0..self.data_shards {
let slice = shards_opt[i]
.as_ref()
.ok_or("missing shard after reconstruct")?;
payload.extend_from_slice(slice);
}

if payload.len() < 8 {
return Err("payload too small".into());
}
let orig_len = u64::from_le_bytes(payload[0..8].try_into().unwrap()) as usize;
if 8 + orig_len > payload.len() {
return Err("original length exceeds reconstructed payload".into());
}
Ok(payload[8..8 + orig_len].to_vec())
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Magic number: The value 8 appears multiple times (lines 34, 35, 93, 96, 100) representing the size of the u64 length prefix. This should be defined as a named constant (e.g., LENGTH_PREFIX_SIZE) to improve maintainability and make the code's intent clearer.

Copilot uses AI. Check for mistakes.
Comment thread src/encryptedfs/test.rs
Comment on lines +2486 to +2557
#[tokio::test]
#[traced_test]
async fn test_reed_solomon_error_correction() {
// Create filesystem with Reed-Solomon enabled (3 data shards, 2 parity shards)
// Currently only `data_shards = 1` is supported.
// The original encrypted file acts as the single data shard.
// Parity shards are used only for recovery.
let rs_config = Some(crate::crypto::rs::RsConfig {
data_shards: 1,
parity_shards: 2,
});

let temp_dir = tempfile::tempdir().unwrap();
let data_dir = temp_dir.path().to_path_buf();

let fs = EncryptedFs::new(
data_dir.clone(),
Box::new(PasswordProviderImpl {}),
Cipher::ChaCha20Poly1305,
rs_config,
false,
)
.await
.unwrap();

// Create and write a test file
let test_file = SecretString::from_str("test-rs-file").unwrap();
let (fh, attr) = fs
.create(
ROOT_INODE,
&test_file,
create_attr(FileType::RegularFile),
false,
true,
)
.await
.unwrap();

let original_data = b"Hello, Reed-Solomon! This is test data for error correction.";
write_all_bytes_to_fs(&fs, attr.ino, 0, original_data, fh).await.unwrap();

// Release the file handle (this should trigger RS encoding and create parity shards)
fs.release(fh).await.unwrap();

// Verify the main content file exists
let content_path = fs.contents_path(attr.ino);
assert!(content_path.exists(), "Main content file should exist");

// Verify parity shard files were created
for i in 0..2 {
let parity_path = content_path.parent().unwrap().join(format!("{}.parity.{}", attr.ino, i));
assert!(parity_path.exists(), "Parity shard {} should exist", i);
}

// Simulate file corruption by deleting the main content file
std::fs::remove_file(&content_path).unwrap();
assert!(!content_path.exists(), "Main content file should be deleted");

// Try to read the file - this should trigger reconstruction from parity shards
let read_fh = fs.open(attr.ino, true, false).await.unwrap();

// Read the reconstructed content
let mut buffer = vec![0u8; original_data.len()];
let bytes_read = fs.read(attr.ino, 0, &mut buffer, read_fh).await.unwrap();
assert_eq!(bytes_read, original_data.len(), "Should read all original bytes");
assert_eq!(&buffer[..bytes_read], original_data, "Reconstructed data should match original");

// Verify the main file was restored
assert!(content_path.exists(), "Main content file should be restored after reconstruction");

fs.release(read_fh).await.unwrap();
}
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test coverage: The test only covers the happy path where file reconstruction succeeds. It should also test error scenarios such as: insufficient parity shards available for reconstruction, corrupted parity shards, missing all shards, and the read-only mode preventing reconstruction write-back.

Copilot uses AI. Check for mistakes.
Comment thread src/encryptedfs.rs Outdated
debug!("RS: Wrote {} parity shards for inode {}", rs_config.parity_shards, ctx.ino);
}
Err(e) => {
warn!("{}", e);
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent error handling: When RS encoding fails (line 1570), the error is converted to a String, then later at line 1589 it's only logged as a warning. This means encoding failures are silently ignored and parity shards won't be created. Consider either propagating this error or at least using error! level logging and documenting this behavior, as users may expect parity shards to always be created when RS is enabled.

Suggested change
warn!("{}", e);
// RS encoding failed: parity shards will NOT be created for this file.
error!("RS: Parity shard creation failed: {}. Parity shards will NOT be created for inode {}.", e, ctx.ino);

Copilot uses AI. Check for mistakes.
Comment thread src/encryptedfs/test.rs Outdated
Comment on lines +2489 to +2556
// Create filesystem with Reed-Solomon enabled (3 data shards, 2 parity shards)
// Currently only `data_shards = 1` is supported.
// The original encrypted file acts as the single data shard.
// Parity shards are used only for recovery.
let rs_config = Some(crate::crypto::rs::RsConfig {
data_shards: 1,
parity_shards: 2,
});

let temp_dir = tempfile::tempdir().unwrap();
let data_dir = temp_dir.path().to_path_buf();

let fs = EncryptedFs::new(
data_dir.clone(),
Box::new(PasswordProviderImpl {}),
Cipher::ChaCha20Poly1305,
rs_config,
false,
)
.await
.unwrap();

// Create and write a test file
let test_file = SecretString::from_str("test-rs-file").unwrap();
let (fh, attr) = fs
.create(
ROOT_INODE,
&test_file,
create_attr(FileType::RegularFile),
false,
true,
)
.await
.unwrap();

let original_data = b"Hello, Reed-Solomon! This is test data for error correction.";
write_all_bytes_to_fs(&fs, attr.ino, 0, original_data, fh).await.unwrap();

// Release the file handle (this should trigger RS encoding and create parity shards)
fs.release(fh).await.unwrap();

// Verify the main content file exists
let content_path = fs.contents_path(attr.ino);
assert!(content_path.exists(), "Main content file should exist");

// Verify parity shard files were created
for i in 0..2 {
let parity_path = content_path.parent().unwrap().join(format!("{}.parity.{}", attr.ino, i));
assert!(parity_path.exists(), "Parity shard {} should exist", i);
}

// Simulate file corruption by deleting the main content file
std::fs::remove_file(&content_path).unwrap();
assert!(!content_path.exists(), "Main content file should be deleted");

// Try to read the file - this should trigger reconstruction from parity shards
let read_fh = fs.open(attr.ino, true, false).await.unwrap();

// Read the reconstructed content
let mut buffer = vec![0u8; original_data.len()];
let bytes_read = fs.read(attr.ino, 0, &mut buffer, read_fh).await.unwrap();
assert_eq!(bytes_read, original_data.len(), "Should read all original bytes");
assert_eq!(&buffer[..bytes_read], original_data, "Reconstructed data should match original");

// Verify the main file was restored
assert!(content_path.exists(), "Main content file should be restored after reconstruction");

fs.release(read_fh).await.unwrap();
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test pattern inconsistency: This test doesn't follow the run_test pattern used by other tests in the file (e.g., test_read_only_write). Consider refactoring to use the run_test helper for consistency, proper cleanup, and to leverage the existing test infrastructure.

Suggested change
// Create filesystem with Reed-Solomon enabled (3 data shards, 2 parity shards)
// Currently only `data_shards = 1` is supported.
// The original encrypted file acts as the single data shard.
// Parity shards are used only for recovery.
let rs_config = Some(crate::crypto::rs::RsConfig {
data_shards: 1,
parity_shards: 2,
});
let temp_dir = tempfile::tempdir().unwrap();
let data_dir = temp_dir.path().to_path_buf();
let fs = EncryptedFs::new(
data_dir.clone(),
Box::new(PasswordProviderImpl {}),
Cipher::ChaCha20Poly1305,
rs_config,
false,
)
.await
.unwrap();
// Create and write a test file
let test_file = SecretString::from_str("test-rs-file").unwrap();
let (fh, attr) = fs
.create(
ROOT_INODE,
&test_file,
create_attr(FileType::RegularFile),
false,
true,
)
.await
.unwrap();
let original_data = b"Hello, Reed-Solomon! This is test data for error correction.";
write_all_bytes_to_fs(&fs, attr.ino, 0, original_data, fh).await.unwrap();
// Release the file handle (this should trigger RS encoding and create parity shards)
fs.release(fh).await.unwrap();
// Verify the main content file exists
let content_path = fs.contents_path(attr.ino);
assert!(content_path.exists(), "Main content file should exist");
// Verify parity shard files were created
for i in 0..2 {
let parity_path = content_path.parent().unwrap().join(format!("{}.parity.{}", attr.ino, i));
assert!(parity_path.exists(), "Parity shard {} should exist", i);
}
// Simulate file corruption by deleting the main content file
std::fs::remove_file(&content_path).unwrap();
assert!(!content_path.exists(), "Main content file should be deleted");
// Try to read the file - this should trigger reconstruction from parity shards
let read_fh = fs.open(attr.ino, true, false).await.unwrap();
// Read the reconstructed content
let mut buffer = vec![0u8; original_data.len()];
let bytes_read = fs.read(attr.ino, 0, &mut buffer, read_fh).await.unwrap();
assert_eq!(bytes_read, original_data.len(), "Should read all original bytes");
assert_eq!(&buffer[..bytes_read], original_data, "Reconstructed data should match original");
// Verify the main file was restored
assert!(content_path.exists(), "Main content file should be restored after reconstruction");
fs.release(read_fh).await.unwrap();
let rs_config = Some(crate::crypto::rs::RsConfig {
data_shards: 1,
parity_shards: 2,
});
run_test(
Some(Cipher::ChaCha20Poly1305),
rs_config,
false,
|fs| Box::pin(async move {
// Create and write a test file
let test_file = SecretString::from_str("test-rs-file").unwrap();
let (fh, attr) = fs
.create(
ROOT_INODE,
&test_file,
create_attr(FileType::RegularFile),
false,
true,
)
.await
.unwrap();
let original_data = b"Hello, Reed-Solomon! This is test data for error correction.";
write_all_bytes_to_fs(&fs, attr.ino, 0, original_data, fh).await.unwrap();
// Release the file handle (this should trigger RS encoding and create parity shards)
fs.release(fh).await.unwrap();
// Verify the main content file exists
let content_path = fs.contents_path(attr.ino);
assert!(content_path.exists(), "Main content file should exist");
// Verify parity shard files were created
for i in 0..2 {
let parity_path = content_path.parent().unwrap().join(format!("{}.parity.{}", attr.ino, i));
assert!(parity_path.exists(), "Parity shard {} should exist", i);
}
// Simulate file corruption by deleting the main content file
std::fs::remove_file(&content_path).unwrap();
assert!(!content_path.exists(), "Main content file should be deleted");
// Try to read the file - this should trigger reconstruction from parity shards
let read_fh = fs.open(attr.ino, true, false).await.unwrap();
// Read the reconstructed content
let mut buffer = vec![0u8; original_data.len()];
let bytes_read = fs.read(attr.ino, 0, &mut buffer, read_fh).await.unwrap();
assert_eq!(bytes_read, original_data.len(), "Should read all original bytes");
assert_eq!(&buffer[..bytes_read], original_data, "Reconstructed data should match original");
// Verify the main file was restored
assert!(content_path.exists(), "Main content file should be restored after reconstruction");
fs.release(read_fh).await.unwrap();
}),
)
.await;

Copilot uses AI. Check for mistakes.
Comment thread src/crypto/rs.rs Outdated
Comment on lines +30 to +55
pub fn encode(&self, data: &[u8]) -> Result<Vec<Vec<u8>>, Box<dyn Error>> {
let r = ReedSolomon::<Gf8>::new(self.data_shards, self.parity_shards)?;

// prefix with original length so we can trim when reconstructing
let mut payload = Vec::with_capacity(8 + data.len());
payload.extend_from_slice(&(data.len() as u64).to_le_bytes());
payload.extend_from_slice(data);

let shard_size = (payload.len() + self.data_shards - 1) / self.data_shards;
let total_shards = self.data_shards + self.parity_shards;

let mut shards: Vec<Vec<u8>> = vec![vec![0u8; shard_size]; total_shards];

for i in 0..self.data_shards {
let start = i * shard_size;
let end = std::cmp::min(start + shard_size, payload.len());
if start < payload.len() {
shards[i][..end - start].copy_from_slice(&payload[start..end]);
}
}

let mut shard_refs: Vec<&mut [u8]> = shards.iter_mut().map(|v| v.as_mut_slice()).collect();
r.encode(&mut shard_refs)?;

Ok(shards)
}
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing documentation: The encode method lacks documentation. It should document what it does, explain the length-prefixing behavior, describe the returned shard structure, and document potential errors.

Copilot uses AI. Check for mistakes.
Comment thread src/encryptedfs.rs Outdated
Comment on lines +1576 to +1578
let shard_path = content_path.parent()
.unwrap()
.join(format!("{}.parity.{}", ctx.ino, i - rs_config.data_shards));
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security issue: The parity shard path construction uses format! with user-controlled inode number without validation. While inode is typically an internal number, ensure this cannot be exploited for path traversal. Consider using a more explicit path join operation or validating the inode format.

Suggested change
let shard_path = content_path.parent()
.unwrap()
.join(format!("{}.parity.{}", ctx.ino, i - rs_config.data_shards));
let shard_filename = format!("{}.parity.{}", ctx.ino.to_string(), (i - rs_config.data_shards).to_string());
let shard_path = content_path.parent()
.unwrap()
.join(shard_filename);

Copilot uses AI. Check for mistakes.
Comment thread src/encryptedfs.rs
Comment on lines +1558 to +1594
let content_path = self.contents_path(ctx.ino);
debug!("RS: Attempting to encode file for inode {} at {:?}", ctx.ino, content_path);

match tokio::fs::read(&content_path).await {
Ok(content) => {
debug!("RS: Read {} bytes from {:?}", content.len(), content_path);
let encoder = crate::crypto::rs::RsEncoder::new(
rs_config.data_shards,
rs_config.parity_shards,
);

let shards = encoder.encode(&content)
.map_err(|e| format!("RS encoding failed for inode {}: {}", ctx.ino, e));

match shards {
Ok(shards) => {
// Write parity shards (skip data shards which are the original file)
for (i, shard) in shards.iter().enumerate().skip(rs_config.data_shards) {
let shard_path = content_path.parent()
.unwrap()
.join(format!("{}.parity.{}", ctx.ino, i - rs_config.data_shards));
if let Err(e) = tokio::fs::write(&shard_path, shard).await {
warn!("RS: Failed to write parity shard {:?}: {}", shard_path, e);
} else {
debug!("RS: Wrote parity shard {} for inode {} at {:?}",
i - rs_config.data_shards, ctx.ino, shard_path);
}
}
debug!("RS: Wrote {} parity shards for inode {}", rs_config.parity_shards, ctx.ino);
}
Err(e) => {
warn!("{}", e);
}
}
}
Err(e) => {
debug!("RS: Failed to read content file {:?}: {}", content_path, e);
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Performance issue: The entire file content is read into memory (line 1561) for Reed-Solomon encoding on every file release. For large files, this could cause significant memory pressure and I/O overhead. Consider implementing streaming/chunked encoding or adding a file size threshold to avoid encoding very large files.

Suggested change
let content_path = self.contents_path(ctx.ino);
debug!("RS: Attempting to encode file for inode {} at {:?}", ctx.ino, content_path);
match tokio::fs::read(&content_path).await {
Ok(content) => {
debug!("RS: Read {} bytes from {:?}", content.len(), content_path);
let encoder = crate::crypto::rs::RsEncoder::new(
rs_config.data_shards,
rs_config.parity_shards,
);
let shards = encoder.encode(&content)
.map_err(|e| format!("RS encoding failed for inode {}: {}", ctx.ino, e));
match shards {
Ok(shards) => {
// Write parity shards (skip data shards which are the original file)
for (i, shard) in shards.iter().enumerate().skip(rs_config.data_shards) {
let shard_path = content_path.parent()
.unwrap()
.join(format!("{}.parity.{}", ctx.ino, i - rs_config.data_shards));
if let Err(e) = tokio::fs::write(&shard_path, shard).await {
warn!("RS: Failed to write parity shard {:?}: {}", shard_path, e);
} else {
debug!("RS: Wrote parity shard {} for inode {} at {:?}",
i - rs_config.data_shards, ctx.ino, shard_path);
}
}
debug!("RS: Wrote {} parity shards for inode {}", rs_config.parity_shards, ctx.ino);
}
Err(e) => {
warn!("{}", e);
}
}
}
Err(e) => {
debug!("RS: Failed to read content file {:?}: {}", content_path, e);
const RS_MAX_FILE_SIZE: u64 = 10 * 1024 * 1024; // 10 MB
let content_path = self.contents_path(ctx.ino);
debug!("RS: Attempting to encode file for inode {} at {:?}", ctx.ino, content_path);
match tokio::fs::metadata(&content_path).await {
Ok(metadata) => {
if metadata.len() > RS_MAX_FILE_SIZE {
warn!("RS: Skipping Reed-Solomon encoding for inode {} at {:?} (file size {} bytes exceeds threshold of {} bytes)",
ctx.ino, content_path, metadata.len(), RS_MAX_FILE_SIZE);
} else {
match tokio::fs::read(&content_path).await {
Ok(content) => {
debug!("RS: Read {} bytes from {:?}", content.len(), content_path);
let encoder = crate::crypto::rs::RsEncoder::new(
rs_config.data_shards,
rs_config.parity_shards,
);
let shards = encoder.encode(&content)
.map_err(|e| format!("RS encoding failed for inode {}: {}", ctx.ino, e));
match shards {
Ok(shards) => {
// Write parity shards (skip data shards which are the original file)
for (i, shard) in shards.iter().enumerate().skip(rs_config.data_shards) {
let shard_path = content_path.parent()
.unwrap()
.join(format!("{}.parity.{}", ctx.ino, i - rs_config.data_shards));
if let Err(e) = tokio::fs::write(&shard_path, shard).await {
warn!("RS: Failed to write parity shard {:?}: {}", shard_path, e);
} else {
debug!("RS: Wrote parity shard {} for inode {} at {:?}",
i - rs_config.data_shards, ctx.ino, shard_path);
}
}
debug!("RS: Wrote {} parity shards for inode {}", rs_config.parity_shards, ctx.ino);
}
Err(e) => {
warn!("{}", e);
}
}
}
Err(e) => {
debug!("RS: Failed to read content file {:?}: {}", content_path, e);
}
}
}
}
Err(e) => {
debug!("RS: Failed to stat content file {:?}: {}", content_path, e);

Copilot uses AI. Check for mistakes.
@adela-marin adela-marin force-pushed the issue-277-reed-solomon branch 2 times, most recently from eeb16e5 to 2d7ffa8 Compare March 10, 2026 15:25
@adela-marin adela-marin force-pushed the issue-277-reed-solomon branch from 2d7ffa8 to a3d52c4 Compare March 10, 2026 15:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants