Skip to content

[BUG] [v0.0.7] cortex-app-server storage.rs save_session() silently drops BufWriter flush errors — BufWriter::drop() discards I/O errors, session appears saved but file is empty #53292

@petar-vasilev

Description

@petar-vasilev

Project

cortex

Description

In cortex-app-server/src/storage.rs, save_session() uses BufWriter to write session JSON to disk, but the BufWriter is moved into serde_json::to_writer_pretty() and silently dropped:

pub fn save_session(&self, session: &StoredSession) -> std::io::Result<()> {
let path = self.session_path(&session.id);
let file = fs::File::create(&path)?;
file.lock_exclusive()?;

  let writer = BufWriter::new(&file);  // 8KB internal buffer
  // BUG: writer is moved into to_writer_pretty, then silently dropped
  let result = serde_json::to_writer_pretty(writer, session)
      .map_err(std::io::Error::other);
  // ^^^ writer's BufWriter::drop() runs here, calling flush_buf()
  // If flush_buf() fails (disk full, I/O error), the error is SWALLOWED:
  //   fn drop(&mut self) { let _r = self.flush_buf(); }  <-- _r ignores error!

  file.unlock()?;
  result?;  // BUG: result is Ok(()), not aware of the flushed failure!
  Ok(())

}

The Rust BufWriter documentation explicitly warns: 'It is critical to call flush() before the BufWriter is dropped, as the implementation in Drop will ignore any errors.' serde_json::to_writer_pretty does NOT call flush() explicitly before returning.

For small sessions (< 8KB, the default BufWriter capacity), ALL data stays in the internal buffer until Drop. If the OS write fails (disk full, NFS timeout, permission error), the error is silently discarded. save_session() returns Ok(()) while the session file is EMPTY on disk.

Error Message

Debug Logs

cortex-app-server/src/storage.rs:95-96 (BufWriter flush error swallowed):
    let writer = BufWriter::new(&file);  // 8KB buffer, no data written to disk yet
    let result = serde_json::to_writer_pretty(writer, session)  // writes to buffer
        .map_err(std::io::Error::other);  // <-- writer dropped here, flush_buf() error SWALLOWED

    // Rust std BufWriter::drop() source:
    // fn drop(&mut self) {
    //     if !self.buf.is_empty() {
    //         let _r = self.flush_buf();  // error explicitly ignored!
    //     }
    // }

    file.unlock()?;
    result?;  // result is Ok(()) -- session save SILENTLY FAILED

  warning: disk write failed (disk full) but save_session() returned Ok(())
  error: session file {id}.json is 0 bytes on disk -- session data lost
  error: on restart: load_session() -> Err(unexpected end of file)

System Information

OS: Ubuntu 22.04 LTS  |  Version: v0.0.7

Screenshots

https://github.com/petar-vasilev/screenshots/blob/main/a6013977a70a46e8ac6d628f5a9dcd4d.png

Steps to Reproduce

  1. Run: cortex app-server # session auto-save during AI conversation
  2. Fill disk to near-capacity (or introduce an I/O error via fault injection)
  3. Send a message to an active cortex AI session
  4. Server calls save_session() to persist the session
  5. serde_json::to_writer_pretty(writer, session) succeeds (writes to BufWriter buffer)
  6. BufWriter::drop() runs: calls flush_buf() which fails (disk full)
  7. flush_buf() error is swallowed: let _r = self.flush_buf();
  8. save_session() returns Ok(()) -- falsely indicates success
  9. Session file on disk is empty (0 bytes) -- data is permanently lost
  10. On server restart, load_session() fails with 'unexpected end of file'

Expected Behavior

save_session() should explicitly flush the BufWriter before dropping it, capturing and propagating any flush errors:

let mut writer = BufWriter::new(&file);
serde_json::to_writer_pretty(&mut writer, session)
.map_err(std::io::Error::other)?;
// Explicitly flush and propagate errors:
writer.flush()?;
// Now safe to drop writer (buffer is empty, flush_buf() in Drop is no-op)

file.unlock()?;
Ok(())

Actual Behavior

save_session() passes BufWriter by value to serde_json::to_writer_pretty(). The BufWriter is moved into and implicitly dropped inside the function call, which calls BufWriter::drop() -> flush_buf() with errors silently discarded. For sessions < 8KB, the entire session is in the BufWriter buffer until drop. Any I/O failure (disk full, NFS error) causes the session file to be empty while save_session() falsely returns Ok(()).

Additional Context

── Code Evidence ────────────────────────────────────────────────────
This is the classic Rust BufWriter footgun. The Rust std documentation for BufWriter explicitly warns against this pattern, but it requires explicit flush() calls to avoid.

The bug affects session persistence reliability. Users lose conversation history on server restart after any disk write failure.

Note: Large sessions (> 8KB, rare but possible for long conversations) do trigger partial disk writes during to_writer_pretty (when the buffer fills), so those I/O errors ARE captured by result. But the final flush of the last partial buffer still has the same silent-drop issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions