hashicorp/nomad · error

failed to parse hash file %s

Error message

failed to parse hash file %s

What it means

This error is returned by ResolvConf.UserModified when the contents of the hash file cannot be parsed as a valid digest via digest.Parse. The hash file is expected to contain a valid digest string; if it is empty, truncated, or contains garbage, the stored expected hash cannot be reconstructed and the comparison against the live resolv.conf cannot proceed.

Source

Thrown at lib/resolvconf/lib.go:420

}

// UserModified can be used to determine whether the resolv.conf file has been
// modified since it was generated. It returns false with no error if the file
// matches the hash, true with no error if the file no longer matches the hash,
// and false with an error if the result cannot be determined.
func UserModified(rcPath, rcHashPath string) (bool, error) {
	currRCHash, err := os.ReadFile(rcHashPath)
	if err != nil {
		// If the hash file doesn't exist, can only assume it hasn't been written
		// yet (so, the user hasn't modified the file it hashes).
		if errors.Is(err, fs.ErrNotExist) {
			return false, nil
		}
		return false, errors.Wrapf(err, "failed to read hash file %s", rcHashPath)
	}
	expected, err := digest.Parse(string(currRCHash))
	if err != nil {
		return false, errors.Wrapf(err, "failed to parse hash file %s", rcHashPath)
	}
	v := expected.Verifier()
	currRC, err := os.Open(rcPath)
	if err != nil {
		return false, errors.Wrapf(err, "failed to open %s to check for modifications", rcPath)
	}
	defer currRC.Close()
	if _, err := io.Copy(v, currRC); err != nil {
		return false, errors.Wrapf(err, "failed to hash %s to check for modifications", rcPath)
	}
	return !v.Verified(), nil
}

func (rc *ResolvConf) processLine(line string) {
	fields := strings.Fields(line)

	// Strip blank lines and comments.
	if len(fields) == 0 || fields[0][0] == '#' || fields[0][0] == ';' {

View on GitHub (pinned to 482b49bf1a)

Solutions

  1. Inspect the hash file contents (cat rcHashPath) — if empty or corrupt, delete it; the next Set/user flow will rewrite it and UserModified will report unmodified until then.
  2. Ensure the code that writes the hash file writes it atomically (write to temp file then rename) to avoid truncated contents.
  3. Check for concurrent writers to the same hash path (multiple processes/containers sharing the state dir) and isolate the path per instance.
  4. Upgrade/align library versions so the digest format written matches the format parsed.

Example fix

// before: corrupt/truncated hash file
// cat /var/run/resolvconf.hash  ->  "" (empty)
// UserModified() -> failed to parse hash file ...

// after
// rm /var/run/resolvconf.hash   // regenerate via library Set
// UserModified() -> (false, nil)
Defensive patterns

Strategy: validation

Validate before calling

data, err := os.ReadFile(rcHashPath)
if err != nil { return err }
if len(data) == 0 {
	// invalid digest: delete file so library regenerates it
	os.Remove(rcHashPath)
}

Type guard

func hashFileNonEmpty(path string) bool {
	fi, err := os.Stat(path)
	return err == nil && fi.Mode().IsRegular() && fi.Size() > 0
}

Try / catch

modified, err := rc.UserModified()
if err != nil && strings.Contains(err.Error(), "failed to parse hash file") {
	os.Remove(rcHashPath) // regenerate on next write
	modified = false
}

Prevention

When it happens

Trigger: Calling ResolvConf.UserModified when the hash file at rcHashPath exists and is readable but its contents are not a valid digest — e.g. an empty file, a partially written file from an interrupted Set, or a file overwritten by another tool.

Common situations: Crash or SIGKILL between file creation and full write of the hash; an init script or backup tool clobbering the hash file; manual editing of the state file; different library versions writing incompatible digest formats.

Understand the failure class

Related errors


AI-assisted analysis of hashicorp/nomad@482b49bf1a (2026-09-04). Data as JSON: /api/errors/e7f37d85b5257eb4. Report an issue: GitHub.