Skip to content
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

termux-usb: fix issue with usb fd being closed before sent over socket #644

Merged
merged 1 commit into from
Mar 20, 2024

Conversation

dougvj
Copy link
Contributor

@dougvj dougvj commented Nov 29, 2023

This is a minor refactor of the WithAncillaryFd ResultReturner that achieves two goals:

  1. The usb fd is no longer closed before the message is sent over the socket. This resolves termux-usb -e broken with empty non-fd response #643

  2. We queue the fds to be sent (using setFileDescriptorsForSend) Before printing an output message to ensure the order is correct. This was working before by virtue of the buffering mechanism, but would break if there were no buffering.

@agnostic-apollo
Copy link
Member

Thanks for the pull. Some details for clarity...

The setFileDescriptorsForSend() docs state

The file descriptors will be sent with the next write of normal data, and will be delivered in a single ancillary message

Previously, in UsbApi, the fd was only temporarily stored in WithAncillaryFd, but not actually sent or passed to LocalSocket, and then a @ was written, possibly in attempts to send it, even though like I said, it wasn't passed to LocalSocket with setFileDescriptorsForSend() yet, so it was a pointless write, but worked due to autoflush on close() (check below). After this, when writeResult() returned, then fd was passed to LocalSocket, but still not sent, since no data was written after it.

} else {
resultWriter.writeResult(writer);
}
if(resultWriter instanceof WithAncillaryFd) {
int fd = ((WithAncillaryFd) resultWriter).getFd();
if (fd >= 0) {
pfds[0] = ParcelFileDescriptor.adoptFd(fd);
FileDescriptor[] fds = { pfds[0].getFileDescriptor() };
outputSocket.setFileDescriptorsForSend(fds);

Previously, before 3c1a6be, the PrintWriter in ResultReturner was using java try-with-resources, which automatically closes when try gets out of scope. Additionally, when new PrintWriter(outputSocket.getOutputStream() is called, it uses a BufferedWriter internally, which when closed, automatically flushes. What this would do is actually send the socket that was previously passed to LocalSocket. Moreover, PrintWriter was also closed before fd was closed since try-with-resources finished before, but after the commit, it was closed afterwards, causing your issue.

Note that calling flush() on SocketOutputStream does nothing, its not overridden by it, and base OutputStream does nothing.

You should passed PrintWriter to sendFd(), then after setFileDescriptorsForSend(fd), write @ (remove it from UsbApi), then run setFileDescriptorsForSend(null) to clear existing fd, otherwise it will get written for every write, even though we are currently not writing anything. Android will not clear it automatically. The @ character is used by termux-api, so do not change it.

Also fix typo in "otherwise we mysterious socket issues", preferably remove that part, and replace it completely with "Make sure we send the file descriptors before closing them by flushing them and data (@)". Also add comment before writing @ with reference to docs that "The file descriptors will be sent with the next write of normal data...".

You should also follow commit guidelines for format, check git history and https://github.com/termux/termux-app?tab=readme-ov-file#commit-messages-guidelines

Will need to update the commit title and description as well with this new info, probably should link this pull in the message.

@nohajc
Copy link

nohajc commented Feb 19, 2024

Hi, when can this be merged? It breaks at least one downstream project.

dougvj added a commit to dougvj/termux-api that referenced this pull request Feb 22, 2024
This is a minor refactor of the WithAncillaryFd ResultReturner that
achieves two goals:

1. The usb fd is no longer closed before the message is sent over the
   socket. This resolves termux#643

2. We queue the fds to be sent (using setFileDescriptorsForSend) and
   send and flush the output message in one operation to ensure the
   correct ordering

Explanation per @agnostic-apollo:

Previously, in UsbApi, the fd was only temporarily stored in WithAncillaryFd,
but not actually sent or passed to LocalSocket, and then a @ was written,
possibly in attempts to send it, even though like I said, it wasn't passed to
LocalSocket with setFileDescriptorsForSend() yet, so it was a pointless write,
but worked due to autoflush on close() (check below). After this, when
writeResult() returned, then fd was passed to LocalSocket, but still not sent,
since no data was written after it.

Previously, before 3c1a6be, the PrintWriter in ResultReturner was using java
try-with-resources, which automatically closes when try gets out of scope.
Additionally, when new PrintWriter(outputSocket.getOutputStream() is called, it
uses a BufferedWriter internally, which when closed, automatically flushes. What
this would do is actually send the socket that was previously passed to
LocalSocket. Moreover, PrintWriter was also closed before fd was closed since
try-with-resources finished before, but after the commit, it was closed
afterwards, causing your issue.

See termux#644
@dougvj
Copy link
Contributor Author

dougvj commented Feb 22, 2024

I made the changes requested and verified that the fix still works.

dougvj added a commit to dougvj/termux-api that referenced this pull request Feb 22, 2024
This is a minor refactor of the WithAncillaryFd ResultReturner that
achieves two goals:

1. The usb fd is no longer closed before the message is sent over the
   socket. This resolves termux#643

2. We queue the fds to be sent (using setFileDescriptorsForSend) and
   send and flush the output message in one operation to ensure the
   correct ordering

Explanation per @agnostic-apollo:

Previously, in UsbApi, the fd was only temporarily stored in WithAncillaryFd,
but not actually sent or passed to LocalSocket, and then a @ was written,
possibly in attempts to send it, even though like I said, it wasn't passed to
LocalSocket with setFileDescriptorsForSend() yet, so it was a pointless write,
but worked due to autoflush on close() (check below). After this, when
writeResult() returned, then fd was passed to LocalSocket, but still not sent,
since no data was written after it.

Previously, before 3c1a6be, the PrintWriter in ResultReturner was using java
try-with-resources, which automatically closes when try gets out of scope.
Additionally, when new PrintWriter(outputSocket.getOutputStream() is called, it
uses a BufferedWriter internally, which when closed, automatically flushes. What
this would do is actually send the socket that was previously passed to
LocalSocket. Moreover, PrintWriter was also closed before fd was closed since
try-with-resources finished before, but after the commit, it was closed
afterwards, causing your issue.

See termux#644
@agnostic-apollo
Copy link
Member

Looks good, thanks. Will merge it soon.

@nohajc
Copy link

nohajc commented Mar 18, 2024

Hi, any particular reason this hasn't been merged yet? Thanks.

@agnostic-apollo
Copy link
Member

Sorry, got unexpectedly busy with family stuff and then got sick, am back now, will merge it within next couple of days. Thanks for the ping.

agnostic-apollo pushed a commit to dougvj/termux-api that referenced this pull request Mar 19, 2024
This is a minor refactor of the WithAncillaryFd ResultReturner that
achieves two goals:

1. The usb fd is no longer closed before the message is sent over the
   socket. This resolves termux#643

2. We queue the fds to be sent (using setFileDescriptorsForSend) and
   send and flush the output message in one operation to ensure the
   correct ordering

Explanation per @agnostic-apollo:

Previously, in UsbApi, the fd was only temporarily stored in WithAncillaryFd,
but not actually sent or passed to LocalSocket, and then a @ was written,
possibly in attempts to send it, even though like I said, it wasn't passed to
LocalSocket with setFileDescriptorsForSend() yet, so it was a pointless write,
but worked due to autoflush on close() (check below). After this, when
writeResult() returned, then fd was passed to LocalSocket, but still not sent,
since no data was written after it.

Previously, before 3c1a6be, the PrintWriter in ResultReturner was using java
try-with-resources, which automatically closes when try gets out of scope.
Additionally, when new PrintWriter(outputSocket.getOutputStream() is called, it
uses a BufferedWriter internally, which when closed, automatically flushes. What
this would do is actually send the socket that was previously passed to
LocalSocket. Moreover, PrintWriter was also closed before fd was closed since
try-with-resources finished before, but after the commit, it was closed
afterwards, causing your issue.

See termux#644
This is a minor refactor of the WithAncillaryFd ResultReturner that achieves two goals:

1. The usb fd is no longer closed before the message is sent over the socket. This resolves termux#643

2. We queue the fd to be sent (using setFileDescriptorsForSend) and then write `@` as data and then flush the output message in a single operation so that fd is actually sent.

Writing `@` is required because as per docs "The file descriptors will be sent with the next write of normal data, and will be delivered in a single ancillary message.". The `@` character is not special, it is just the chosen character expected as the message by the native `termux-api` command when a fd is sent.
- https://developer.android.com/reference/android/net/LocalSocket#setFileDescriptorsForSend(java.io.FileDescriptor[])
- https://github.com/termux/termux-api-package/blob/e62bdadea3f26b60430bb85248f300fee68ecdcc/termux-api.c#L358

Explanation for why this got broken in 3c1a6be by @agnostic-apollo at termux#644 (comment):

Previously, in `UsbApi`, the fd was only temporarily stored in `WithAncillaryFd`, but not actually sent or passed to `LocalSocket`, and then a `@` was written, possibly in attempts to send it, even though like I said, it wasn't passed to `LocalSocket` with `setFileDescriptorsForSend()` yet, so it was a pointless write, but worked due to autoflush on `close()` (check below). After this, when `writeResult()` returned, then `fd` was passed to `LocalSocket`, but still not sent, since no data was written after it.

- https://github.com/termux/termux-api/blob/3bea194249586a7dcb143e66b46c1694cb6ca21a/app/src/main/java/com/termux/api/apis/UsbAPI.java#L69-L70
- https://github.com/termux/termux-api/blob/3c1a6be86ff0768fa8be029267fbe96dd7fbfb7f/app/src/main/java/com/termux/api/util/ResultReturner.java#L173-L181

Previously, before termux@3c1a6be, the `PrintWriter` in `ResultReturner` was using java `try-with-resources`, which automatically closes when `try` gets out of scope. Additionally, when `new PrintWriter(outputSocket.getOutputStream()` is called, it uses a `BufferedWriter` internally, which when closed, automatically flushes. What this would do is actually send the socket that was previously passed to `LocalSocket`. Moreover, `PrintWriter` was also closed before `fd` was closed since `try-with-resources` finished before, but after the commit, it was closed afterwards, causing the issue.

- https://cs.android.com/android/platform/superproject/+/android-14.0.0_r1:libcore/ojluni/src/main/java/java/io/PrintWriter.java;l=164
- https://cs.android.com/android/platform/superproject/+/android-14.0.0_r1:libcore/ojluni/src/main/java/java/io/BufferedWriter.java;l=268
- https://cs.android.com/android/platform/superproject/main/+/main:libcore/ojluni/src/main/java/java/io/Writer.java;l=412

Closes termux#643
@agnostic-apollo agnostic-apollo merged commit 4c6a519 into termux:master Mar 20, 2024
1 check passed
@agnostic-apollo
Copy link
Member

Thanks for working on the fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

termux-usb -e broken with empty non-fd response
3 participants