Commit Diff


commit - 57176d45cacb98f1968daa8f8b2efd2735da2731
commit + b9f813fb4b7de1042370b529b9ccc036b208465b
blob - 313a4aa4f065a48741d91ffc8571f831898a4ff6
blob + c578e5c402a0b0258a157edccf97b8984e5ba8df
--- src/daemon.rs
+++ src/daemon.rs
@@ -6,7 +6,7 @@
 //! over a Unix socket using a line-oriented text protocol
 //! (see [`crate::protocol`]).
 
-use std::io::{BufRead, BufReader, Write};
+use std::io::{BufRead, BufReader, Read, Write};
 use std::path::Path;
 use std::sync::atomic::{AtomicBool, Ordering};
 use std::sync::mpsc;
@@ -196,7 +196,7 @@ fn republish(node: &mut Node, store: &PasteStore) {
                     .duration_since(std::time::UNIX_EPOCH)
                     .unwrap_or_default()
                     .as_secs();
-                let expires = paste.created_at + paste.ttl_secs;
+                let expires = paste.created_at.saturating_add(paste.ttl_secs);
                 let rem = expires.saturating_sub(now);
                 std::cmp::min(rem, u16::MAX as u64) as u16
             };
@@ -259,6 +259,10 @@ pub fn run_unix_listener(
     let _ = std::fs::remove_file(sock_path);
 }
 
+/// Maximum protocol line size (128 KiB covers the 64 KiB paste
+/// limit after base58 expansion plus command overhead).
+const MAX_LINE_SIZE: usize = 128 * 1024;
+
 /// Read requests line-by-line from a connected Unix socket
 /// client, forwarding each to the daemon main loop via `tx`.
 fn handle_client(
@@ -268,11 +272,29 @@ fn handle_client(
     stream.set_nonblocking(false)?;
     stream.set_read_timeout(Some(Duration::from_secs(60)))?;
 
-    let reader = BufReader::new(&stream);
+    let mut reader = BufReader::new(&stream);
     let mut writer = &stream;
+    let mut line = String::new();
 
-    for line in reader.lines() {
-        let line = line?;
+    loop {
+        line.clear();
+        // Limit read to MAX_LINE_SIZE to prevent a client from
+        // exhausting memory with an unbounded request line.
+        let n = (&mut reader).take(MAX_LINE_SIZE as u64).read_line(&mut line)?;
+        if n == 0 {
+            break;
+        }
+        if !line.ends_with('\n') && n >= MAX_LINE_SIZE {
+            let resp = protocol::format_response(&Response::Err(
+                "request too large".into(),
+            ));
+            writer.write_all(resp.as_bytes())?;
+            // Drain remaining bytes until newline
+            let mut discard = String::new();
+            let _ = reader.read_line(&mut discard);
+            continue;
+        }
+        let line = line.trim();
         let cmd = match protocol::parse_request(&line) {
             Ok(c) => c,
             Err(e) => {
blob - 302bd5893e29d915799aaae103fbcb444edfa327
blob + 45fb919d90f70615db9b181e4c17eebe0fcc696d
--- src/ops.rs
+++ src/ops.rs
@@ -118,7 +118,17 @@ pub fn get_paste(
         if vals.is_empty() {
             return Err(PasteError::NotFound);
         }
-        vals[0].clone()
+        // Verify DHT result: the content hash must match the
+        // requested key to prevent a malicious node from
+        // injecting arbitrary data.
+        match vals.iter().find(|v| {
+            Paste::from_bytes(v)
+                .map(|p| Paste::content_key(&p.content) == *hash)
+                .unwrap_or(false)
+        }) {
+            Some(v) => v.clone(),
+            None => return Err(PasteError::NotFound),
+        }
     };
 
     let paste = Paste::from_bytes(&data).ok_or(PasteError::InvalidKey)?;
blob - 50b32b17e3b00963bb84e866831305096f51d354
blob + 8bfe9792c1bc71763b585a56873a8a23890fb059
--- src/paste.rs
+++ src/paste.rs
@@ -81,7 +81,7 @@ impl Paste {
             .duration_since(std::time::UNIX_EPOCH)
             .unwrap_or_default()
             .as_secs();
-        now > self.created_at + self.ttl_secs
+        now > self.created_at.saturating_add(self.ttl_secs)
     }
 }
 
blob - 18a7641ac411d880b5f3701315fc6a049084a4bb
blob + 98c54817e574fabaf0b4650ad6da24831a411efc
--- src/store.rs
+++ src/store.rs
@@ -45,14 +45,12 @@ impl PasteStore {
 
     // ── Paste CRUD ──────────────────────────────────
 
-    /// Write a paste to disk. The key (32 bytes) is prepended
-    /// to the file so [`original_keys`] can reconstruct it.
+    /// Write a paste to disk atomically (write-to-temp + rename).
+    /// The key (32 bytes) is prepended to the file so
+    /// [`original_keys`] can reconstruct it.
     pub fn put_paste(&self, key: &[u8], value: &[u8]) -> std::io::Result<()> {
         let path = self.paste_path(key);
-        let mut f = fs::File::create(path)?;
-        f.write_all(key)?;
-        f.write_all(value)?;
-        Ok(())
+        atomic_write(&path, &[key, value])
     }
 
     /// Read a paste from disk. Returns `None` if the paste
@@ -179,6 +177,20 @@ impl PasteStore {
     }
 }
 
+/// Write data to `path` atomically: write to a temporary file in
+/// the same directory, then rename over the target. This prevents
+/// corruption if the process is killed mid-write.
+fn atomic_write(path: &Path, chunks: &[&[u8]]) -> std::io::Result<()> {
+    let parent = path.parent().unwrap_or(Path::new("."));
+    let tmp = parent.join(format!(".tmp.{}", std::process::id()));
+    let mut f = fs::File::create(&tmp)?;
+    for chunk in chunks {
+        f.write_all(chunk)?;
+    }
+    f.sync_all()?;
+    fs::rename(&tmp, path)
+}
+
 // ── tesseras-dht persistence traits ─────────────────
 
 impl tesseras_dht::persist::RoutingPersistence for PasteStore {
@@ -198,7 +210,7 @@ impl tesseras_dht::persist::RoutingPersistence for Pas
             buf.extend_from_slice(id);
             buf.extend_from_slice(addr_bytes);
         }
-        fs::write(&path, &buf).map_err(tesseras_dht::Error::Io)?;
+        atomic_write(&path, &[&buf]).map_err(tesseras_dht::Error::Io)?;
         log::info!("store: persisted {} routing contacts", contacts.len());
         Ok(())
     }