WebServer.serve runs on a NanoHTTPD per-connection thread and writes the converted FIT to a deterministic path new File(mCacheDir, path + ".fit") (WebServer.java:91-94), then streams that same file (lines 112-115). Two overlapping GETs for the same .gpx — e.g. the watch's Comm request times out on a large track and the user retries while the first conversion is still running — both open a FileEncoder on the identical path; one response can stream the half-rewritten file with a src.length() that races the other writer. The watch imports a truncated/corrupt course.
Fix: convert to a unique temp file and atomically rename into place, or serialize conversions per source path (e.g. a ConcurrentHashMap<String,Object> lock), or convert into a byte array and serve from memory.
Found independently by two reviewers. Confidence MEDIUM (needs overlapping requests, which watch retries make plausible; corruption path is direct).
WebServer.serveruns on a NanoHTTPD per-connection thread and writes the converted FIT to a deterministic pathnew File(mCacheDir, path + ".fit")(WebServer.java:91-94), then streams that same file (lines 112-115). Two overlapping GETs for the same.gpx— e.g. the watch's Comm request times out on a large track and the user retries while the first conversion is still running — both open aFileEncoderon the identical path; one response can stream the half-rewritten file with asrc.length()that races the other writer. The watch imports a truncated/corrupt course.Fix: convert to a unique temp file and atomically rename into place, or serialize conversions per source path (e.g. a
ConcurrentHashMap<String,Object>lock), or convert into a byte array and serve from memory.Found independently by two reviewers. Confidence MEDIUM (needs overlapping requests, which watch retries make plausible; corruption path is direct).