shadow1ng/fscan · error
short fragment header: %w
Error message
short fragment header: %w
What it means
RPC-over-TCP frames every message with a 4-byte record-mark header. readRPCFragment uses io.ReadFull to read those 4 bytes; if the connection delivers fewer (EOF, reset, timeout), it wraps the underlying error as "short fragment header: %w". This is the transport-level guard that keeps the parser from interpreting garbage as a frame size.
Source
Thrown at plugins/services/nfs.go:228
}
offset += int(groupLen)
if pad := (4 - groupLen%4) % 4; pad > 0 {
if int(pad) > len(data)-offset {
break
}
offset += int(pad)
}
}
}
return exports
}
func readRPCFragment(conn interface {
Read([]byte) (int, error)
}, maxPayload int) ([]byte, error) {
var header [4]byte
if _, err := io.ReadFull(conn, header[:]); err != nil {
return nil, fmt.Errorf("short fragment header: %w", err)
}
size := int(binary.BigEndian.Uint32(header[:]) & 0x7fffffff)
if size <= 0 || size > maxPayload {
return nil, fmt.Errorf("invalid fragment size: %d", size)
}
payload := make([]byte, size)
if _, err := io.ReadFull(conn, payload); err != nil {
return nil, fmt.Errorf("short fragment payload: %w", err)
}
return payload, nil
}
func (p *NFSPlugin) buildRPCCall(xid, program, version, procedure uint32, data []byte) []byte {
authNone := []byte{0, 0, 0, 0, 0, 0, 0, 0} // AUTH_NONE flavor=0, len=0
buf := make([]byte, 0, 40+len(data))
buf = binary.BigEndian.AppendUint32(buf, xid)
buf = binary.BigEndian.AppendUint32(buf, 0) // CALLView on GitHub (pinned to 95cc12e753)
Solutions
- Unwrap the cause (%w) — EOF/reset/timeout tells you which fix applies
- Check the host actually runs NFS/mountd (`rpcinfo -p <host>`) before scanning
- Retry with backoff; transient resets are common on busy networks
- Verify the port number: mountd's port is dynamic — query the portmapper instead of hardcoding
Example fix
// before
conn, err := net.DialTimeout("tcp", host+":2049", 2*time.Second)
// after
mountdPort, err := portmapperGetPort(host, 100005)
if err != nil {
return fmt.Errorf("no mountd on %s: %w", host, err)
}
conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", host, mountdPort), 2*time.Second) Defensive patterns
Strategy: retry
Validate before calling
conn, err := net.DialTimeout("tcp", addr, timeout)
if err != nil { return fmt.Errorf("target unreachable: %w", err) } Type guard
null
Try / catch
payload, err := readRPCFragment(conn, 4096)
if err != nil {
var nerr net.Error
if errors.As(err, &nerr) && nerr.Timeout() {
return retryWithBackoff(addr, 3)
}
return fmt.Errorf("rpc transport failed: %w", err)
} Prevention
- Dial with a timeout and check connectivity before RPC calls
- Retry transient short reads with exponential backoff
- Verify the port with rpcinfo/portmapper before sending RPC frames
When it happens
Trigger: Calling rpcNullCall, getExports, or TestNFSReadRPCFragmentRejectsInvalidSize when the peer closes the connection before sending a full 4-byte header — connection refused/reset, immediate EOF on a non-RPC port, or a read timeout.
Common situations: Scanning a host that has no NFS service (connection closed instantly); port filtered by a firewall that accepts then resets; TLS-wrapped port responding with binary handshake data then closing; network blip mid-scan.
Understand the failure class
Background: ECONNREFUSED and "connection refused" / "could not connect to server" errors: what they mean and how to fix them — this error's family across 44 libraries.
Related errors
AI-assisted analysis of shadow1ng/fscan@95cc12e753 (2026-09-06).
Data as JSON: /api/errors/06b2b56237bedce0.
Report an issue: GitHub.