oldj/SwitchHosts · error · std::io::Error

InvalidData

InvalidData

Error message

payload too large: {len} bytes

What it means

helper_proto::validate_payload (src-tauri/src/helper_proto/mod.rs:132) rejects any hosts payload larger than MAX_PAYLOAD_BYTES (5 * 1024 * 1024, defined at mod.rs:48) before the privileged daemon writes /etc/hosts. write_system_hosts maps ProtoError::TooLarge into std::io::Error with ErrorKind::InvalidData and the message 'payload too large: {len} bytes'. It is a cheap-to-run DoS guard on a deliberately dumb byte-sink daemon: the size check runs first, before NUL and UTF-8 scans.

Source

Thrown at src-tauri/src/helper_proto/mod.rs:165

/// The system hosts file the daemon is allowed to write. A compile-time
/// constant: the IPC protocol carries NO path, so a client cannot
/// redirect the privileged write anywhere else.
#[cfg(unix)]
pub const SYSTEM_HOSTS_PATH: &str = "/etc/hosts";

/// Monotonic counter that makes each privileged write's temp file name
/// unique, so two concurrent writes within the daemon never share (and
/// clobber) one temp file.
#[cfg(unix)]
static TMP_SEQ: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);

/// Validate `content` and atomically overwrite the system hosts file as
/// `root:wheel` mode `0644`. Intended to run inside the privileged
/// daemon (which is `root`). Returns `InvalidData` if validation fails.
#[cfg(unix)]
pub fn write_system_hosts(content: &[u8]) -> std::io::Result<()> {
    validate_payload(content)
        .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?;
    write_atomic(
        std::path::Path::new(SYSTEM_HOSTS_PATH),
        content,
        Some((0, 0)),
    )
}

/// Atomic write core, factored out so it can be unit-tested
/// unprivileged: write to a sibling temp file, `fsync`, set mode `0644`,
/// optionally `chown` to `owner` (`Some((uid, gid))`; `None` skips the
/// chown so tests can run without root), then `rename(2)` over the
/// target. `rename` within the same directory is atomic, so a reader of
/// the hosts file always sees either the old or the new content, never a
/// partial write. On any failure the temp file is removed best-effort.
#[cfg(unix)]
fn write_atomic(
    target: &std::path::Path,
    content: &[u8],

View on GitHub (pinned to 6ecea88d92)

Solutions

  1. Check payload size before the privileged write: if content.len() > helper_proto::MAX_PAYLOAD_BYTES, split, trim, or refuse in the UI instead of sending to the daemon.
  2. Shrink the generated hosts file: deduplicate hostnames/IP pairs and drop comments/blank lines before applying; most blocklists compress far below 5 MiB after dedup.
  3. If 5 MiB is genuinely too small for your build, raise MAX_PAYLOAD_BYTES in src-tauri/src/helper_proto/mod.rs:48 and rebuild both the app and the swh_helper daemon (both sides must agree on the limit).
  4. Audit how the payload is assembled if it unexpectedly exceeds the cap — an entry-duplication bug in the merge code is a common root cause.

Example fix

// before
let res = helper_proto::write_system_hosts(&content); // may fail InvalidData at 5 MiB+

// after
if content.len() > helper_proto::MAX_PAYLOAD_BYTES {
    anyhow::bail!("hosts payload {} bytes exceeds {} byte cap",
        content.len(), helper_proto::MAX_PAYLOAD_BYTES);
}
helper_proto::write_system_hosts(&content)?;
Defensive patterns

Strategy: validation

Validate before calling

use crate::helper_proto::{validate_payload, MAX_PAYLOAD_BYTES};

// Client-side pre-check before invoking the privileged write:
if content.len() > MAX_PAYLOAD_BYTES {
    return Err(format!("hosts payload too large: {} bytes (cap {})",
        content.len(), MAX_PAYLOAD_BYTES));
}
// Full parity check with the daemon's own validator:
validate_payload(&content)?; // catches TooLarge, ContainsNul, NotUtf8 early

Type guard

// Narrow an io::Error back to this validation failure before deciding what to do
fn is_payload_too_large(err: &std::io::Error) -> bool {
    err.kind() == std::io::ErrorKind::InvalidData
        && err.to_string().contains("payload too large")
}

Try / catch

// Rust: map InvalidData from write_system_hosts distinctly from real IO failures
match helper_proto::write_system_hosts(&content) {
    Ok(()) => {}
    Err(e) if e.kind() == std::io::ErrorKind::InvalidData => {
        // payload rejected by validate_payload: fix/dedup content, never retry as-is
        return Err(anyhow::anyhow!("hosts payload rejected: {e}"));
    }
    Err(e) => {
        // genuine write failure (perms, disk) — may be retried after fixing cause
        return Err(e.into());
    }
}

Prevention

When it happens

Trigger: Calling helper_proto::write_system_hosts(content) (or sending the corresponding XPC/IPC write request from the app) with a payload whose bytes.len() > 5 MiB. Typical producers: merging large ad-block/tracker hosts blocklists into the final hosts content, a generation bug that duplicates entries, or accidentally feeding binary/non-hosts data into the payload.

Common situations: Users enabling very large remote hosts lists (common ad-block lists exceed 5 MB when combined); a loop in merge/apply code that appends entries repeatedly until the cap is exceeded; feed the daemon a file that isn't a hosts file (wrong path passed) so len balloons or content fails other checks after the size one.

Related errors


AI-assisted analysis of oldj/SwitchHosts@6ecea88d92 (2026-08-16). Data as JSON: /api/errors/a84ceb2fffd1d325. Report an issue: GitHub.