{"record":{"id":"4598e292dce2bbd4","repo":"router-for-me/CLIProxyAPI","slug":"invalid-fingerprint-boundary","errorCode":null,"errorMessage":"invalid fingerprint boundary","messagePattern":"invalid fingerprint boundary","errorType":"http","errorClass":null,"httpStatus":500,"severity":"warning","filePath":"internal/api/handlers/management/logs.go","lineNumber":1035,"sourceCode":"\tif boundary < 0 {\n\t\treturn \"\", fmt.Errorf(\"invalid fingerprint boundary\")\n\t}\n\tfile, errOpen := os.Open(path)\n\tif errOpen != nil {\n\t\treturn \"\", errOpen\n\t}\n\tdefer func() {\n\t\t_ = file.Close()\n\t}()\n\tinfo, errStat := file.Stat()\n\tif errStat != nil {\n\t\treturn \"\", errStat\n\t}\n\tif info.IsDir() {\n\t\treturn \"\", fmt.Errorf(\"invalid log file\")\n\t}\n\tif boundary > info.Size() {\n\t\treturn \"\", fmt.Errorf(\"invalid fingerprint boundary\")\n\t}\n\n\thash := sha256.New()\n\t_, _ = fmt.Fprintf(hash, \"log-cursor-v1:%d:\", boundary)\n\tfirstLen := minInt64(boundary, logCursorFingerprintMax)\n\tif errRead := writeFileRange(hash, file, 0, firstLen); errRead != nil {\n\t\treturn \"\", errRead\n\t}\n\ttailLen := minInt64(boundary, logCursorFingerprintMax)\n\ttailStart := boundary - tailLen\n\t_, _ = fmt.Fprintf(hash, \":%d:\", tailStart)\n\tif errRead := writeFileRange(hash, file, tailStart, tailLen); errRead != nil {\n\t\treturn \"\", errRead\n\t}\n\tsum := hash.Sum(nil)\n\treturn base64.RawURLEncoding.EncodeToString(sum[:12]), nil\n}\n","sourceCodeStart":1017,"sourceCodeEnd":1053,"githubUrl":"https://github.com/router-for-me/CLIProxyAPI/blob/78f0c4079e3e6273d65d03b5549cffc898703264/internal/api/handlers/management/logs.go#L1017-L1053","documentation":"Returned by logFileFingerprint (logs.go:1035) when the boundary (the byte position up to which the file is fingerprinted) exceeds the file's current size. newLogCursor computes the boundary from the offset/size it just Stat'ed; if the file shrinks between that Stat and the Open/Stat inside logFileFingerprint, the boundary is now past EOF and the check trips. It is a time-of-check/time-of-use race during rotation or truncation.","triggerScenarios":"Log rotation or copytruncate fires exactly between newLogCursor's os.Stat and logFileFingerprint's open+stat, shrinking the file; rapid manual truncation while a tail client paginates.","commonSituations":"logrotate with copytruncate running on a busy server; a disk-cleanup job that truncates logs on a schedule that collides with dashboard polling.","solutions":["Retry the same log request once — the race is transient and the retry will compute a fresh boundary against the new file size","Switch rotation from copytruncate to rename+create so the active file is replaced atomically instead of being truncated in place","If it persists, check that no process is continuously truncating the log file","Start a fresh read without a cursor to resynchronize state"],"exampleFix":"# before: copytruncate shrinks the file mid-fingerprint\n/var/log/cliproxy/cliproxy.log {\n  copytruncate\n  weekly\n}\n\n# after: rename-based rotation avoids in-place truncation\n/var/log/cliproxy/cliproxy.log {\n  create 0644 root root\n  postrotate\n    systemctl reload cliproxy || true\n  endscript\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"for attempt := 0; attempt < 2; attempt++ {\n\tcursor, err := newLogCursor(path, offset, latest)\n\tif err == nil {\n\t\tbreak\n\t}\n\tif strings.Contains(err.Error(), \"invalid fingerprint boundary\") && attempt == 0 {\n\t\tcontinue // rotation raced us; recompute offset against new size\n\t}\n\treturn err\n}","preventionTips":["Use atomic (rename-based) rotation so files are never truncated in place while being read","Schedule log rotation and aggressive log-trimming jobs away from heavy dashboard polling windows","Retry once on boundary-size errors before surfacing them to users"],"tags":["logs","race-condition","rotation","fingerprint","toctou"],"backgroundTag":null,"analyzedSha":"78f0c4079e3e6273d65d03b5549cffc898703264","analyzedAt":"2026-08-15T12:26:37.444Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}