-
-
Notifications
You must be signed in to change notification settings - Fork 26
ADFA-3945: Handle reader-thread IOException on tar.xz extraction timeout #1412
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: stage
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -18,7 +18,6 @@ import java.io.FilterInputStream | |||||||||||||||
| import java.io.InputStream | ||||||||||||||||
| import java.io.OutputStream | ||||||||||||||||
| import java.util.concurrent.TimeUnit | ||||||||||||||||
| import kotlin.concurrent.thread | ||||||||||||||||
| import kotlin.system.measureTimeMillis | ||||||||||||||||
|
|
||||||||||||||||
| class IdeArchiveServiceImpl( | ||||||||||||||||
|
|
@@ -255,6 +254,21 @@ class IdeArchiveServiceImpl( | |||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| /** | ||||||||||||||||
| * Extracts a `.tar.xz` archive by invoking Termux `tar`. | ||||||||||||||||
| * | ||||||||||||||||
| * The child process's combined stdout/stderr is redirected to a temporary log file | ||||||||||||||||
| * ([ProcessBuilder.redirectOutput]) and read back after the process exits, rather than being | ||||||||||||||||
| * drained by a separate reader thread. This removes the concurrency hazard behind ADFA-3945 | ||||||||||||||||
| * (Sentry APPDEVFORALL-V0): when a timeout triggers [Process.destroyForcibly] the pipe is | ||||||||||||||||
| * closed, and a thread blocked in `read()` on it would throw an `InterruptedIOException` on a | ||||||||||||||||
| * bare thread with no handler, crashing the app. With OS-side redirection there is no in-flight | ||||||||||||||||
| * read to interrupt, and the process also cannot deadlock on a full pipe buffer. | ||||||||||||||||
| * | ||||||||||||||||
| * @param archiveFile the `.tar.xz` to extract. | ||||||||||||||||
| * @param outputDir the directory to extract into (must exist and be writable). | ||||||||||||||||
| * @return `true` only if `tar` completed within the timeout with exit code 0. | ||||||||||||||||
| */ | ||||||||||||||||
| private fun extractTarXzViaTermux(archiveFile: File, outputDir: File): Boolean { | ||||||||||||||||
| if (!archiveFile.exists()) { | ||||||||||||||||
| logger.debug("Archive not found: ${archiveFile.absolutePath}") | ||||||||||||||||
|
|
@@ -263,28 +277,25 @@ class IdeArchiveServiceImpl( | |||||||||||||||
|
|
||||||||||||||||
| logger.debug("Starting Termux tar extraction: ${archiveFile.absolutePath}") | ||||||||||||||||
|
|
||||||||||||||||
| val logFile = File.createTempFile("tarxz-extract", ".log", outputDir) | ||||||||||||||||
| return runCatching { | ||||||||||||||||
| val output = StringBuilder() | ||||||||||||||||
| var exitCode = -1 | ||||||||||||||||
|
|
||||||||||||||||
| val elapsed = measureTimeMillis { | ||||||||||||||||
| val process = ProcessBuilder( | ||||||||||||||||
| "$TERMUX_BIN_PATH/tar", "-xJf", archiveFile.absolutePath, | ||||||||||||||||
| "-C", outputDir.canonicalPath, "--no-same-owner" | ||||||||||||||||
| ).redirectErrorStream(true).apply { | ||||||||||||||||
| environment()["PATH"] = TERMUX_BIN_PATH | ||||||||||||||||
| }.start() | ||||||||||||||||
|
|
||||||||||||||||
| val reader = thread(name = "tar-xz-extract-output") { | ||||||||||||||||
| process.inputStream.bufferedReader().useLines { it.forEach(output::appendLine) } | ||||||||||||||||
| } | ||||||||||||||||
| ).redirectErrorStream(true) | ||||||||||||||||
| .redirectOutput(logFile) | ||||||||||||||||
| .apply { environment()["PATH"] = TERMUX_BIN_PATH } | ||||||||||||||||
| .start() | ||||||||||||||||
|
|
||||||||||||||||
| val completed = process.waitFor(2, TimeUnit.MINUTES) | ||||||||||||||||
| if (!completed) process.destroyForcibly() | ||||||||||||||||
| reader.join() | ||||||||||||||||
| exitCode = if (completed) process.exitValue() else -1 | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| val output = runCatching { logFile.readText() }.getOrDefault("") | ||||||||||||||||
| when (exitCode) { | ||||||||||||||||
| 0 -> { | ||||||||||||||||
| logger.debug("Extraction succeeded in ${elapsed}ms: $output") | ||||||||||||||||
|
|
@@ -298,6 +309,8 @@ class IdeArchiveServiceImpl( | |||||||||||||||
| }.getOrElse { e -> | ||||||||||||||||
| logger.error("Termux process error: ${e.message}") | ||||||||||||||||
| false | ||||||||||||||||
| }.also { | ||||||||||||||||
| logFile.delete() | ||||||||||||||||
|
Comment on lines
+312
to
+313
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win Handle failed deletion of the temporary extraction log. The log contains combined process output and currently remains silently if deletion fails. Suggested cleanup hardening- }.also {
- logFile.delete()
+ }.also {
+ if (logFile.exists() && !logFile.delete()) {
+ logger.warn("Failed to delete Termux extraction log: ${logFile.absolutePath}")
+ logFile.deleteOnExit()
+ }
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
|
|
||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Wait for the forcibly killed
tarprocess and clean it up on interruption.destroyForcibly()is asynchronous, so the method can return/read/delete the log whiletaris still exiting. Also, ifwaitFor(...)throwsInterruptedException, the started process is never destroyed.Suggested lifecycle handling
📝 Committable suggestion
🤖 Prompt for AI Agents