Skip to content

system/popen: Avoid copying FILE#3511

Open
nightt5879 wants to merge 2 commits into
apache:masterfrom
nightt5879:nightt5879/fix-popen-file-copy-2937
Open

system/popen: Avoid copying FILE#3511
nightt5879 wants to merge 2 commits into
apache:masterfrom
nightt5879:nightt5879/fix-popen-file-copy-2937

Conversation

@nightt5879
Copy link
Copy Markdown
Contributor

Summary

Fixes #2937.

system/popen was embedding a copied FILE object in its private tracking container and copying it back in pclose(). That depends on libc FILE layout details.

This PR makes popen() return the actual stream from fdopen() and keeps a small private FILE * -> pid tracking list so pclose() can find the spawned shell process without copying FILE.

Scope:

  • Does not change supported popen() modes.
  • Does not change the spawn/file-action setup.
  • Keeps pclose() responsible for closing the stream and waiting for the shell pid.
  • Returns EINVAL if pclose() cannot find the stream in the private popen list.

Impact

popen()/pclose() no longer rely on libc FILE internals.

  • New feature: NO
  • User adaptation required: NO
  • Build process change: NO
  • Hardware/architecture/board change: NO
  • Documentation update required: NO
  • Security impact: NO
  • Compatibility impact: NO intended compatibility impact

Testing

Host:

  • Windows with WSL Ubuntu 24.04
  • Target: sim:nsh

Checks:

  • git diff --check: pass
  • checkpatch.sh -g HEAD~1..HEAD: pass
  • sim:nsh build with CONFIG_SYSTEM_POPEN=y and CONFIG_EXAMPLES_POPEN=y: pass
    • confirmed CC: popen.c
    • result: SIM elf with dynamic libs archive in nuttx.tgz
  • printf 'popen\npoweroff\n' | timeout 30s ./nuttx: pass
    • confirmed popen("help") output is read
    • confirmed execution reaches pclose()

@nightt5879 nightt5879 marked this pull request as ready for review May 29, 2026 07:34
acassis
acassis previously approved these changes May 29, 2026
@acassis
Copy link
Copy Markdown
Contributor

acassis commented May 29, 2026

@nightt5879 it would be nice to have some test coverage to test it too.

@nightt5879
Copy link
Copy Markdown
Contributor Author

nightt5879 commented May 29, 2026

Thanks, I added a small testing/libc/popen test in commit 23a698f. @acassis

The test opens two popen(..., "r") streams, verifies the output read from both streams, and closes the first stream before the second. That covers reading from the returned popen() stream and also exercises the private stream-to-pid lookup/removal path added by this PR when the stream is not the first entry in the tracking list.

I verified the added test with:

  • checkpatch.sh -g HEAD~1..HEAD
  • sim:nsh build with CONFIG_SYSTEM_POPEN=y and CONFIG_TESTING_POPEN_TEST=y
  • printf 'popen_test\npoweroff\n' | timeout 30s ./nuttx

The simulator run prints popen_test: successful.

@acassis
Copy link
Copy Markdown
Contributor

acassis commented May 29, 2026

Thanks, I added a small testing/libc/popen test in commit 23a698f. @acassis

The test opens two popen(..., "r") streams, verifies the output read from both streams, and closes the first stream before the second. That covers reading from the returned popen() stream and also exercises the private stream-to-pid lookup/removal path added by this PR when the stream is not the first entry in the tracking list.

I verified the added test with:

* `checkpatch.sh -g HEAD~1..HEAD`

* `sim:nsh` build with `CONFIG_SYSTEM_POPEN=y` and `CONFIG_TESTING_POPEN_TEST=y`

* `printf 'popen_test\npoweroff\n' | timeout 30s ./nuttx`

The simulator run prints popen_test: successful.

My bad, I looked only the first commit. So, everything is fine!

@nightt5879
Copy link
Copy Markdown
Contributor Author

Thanks, I added a small testing/libc/popen test in commit 23a698f. @acassis
The test opens two popen(..., "r") streams, verifies the output read from both streams, and closes the first stream before the second. That covers reading from the returned popen() stream and also exercises the private stream-to-pid lookup/removal path added by this PR when the stream is not the first entry in the tracking list.
I verified the added test with:

* `checkpatch.sh -g HEAD~1..HEAD`

* `sim:nsh` build with `CONFIG_SYSTEM_POPEN=y` and `CONFIG_TESTING_POPEN_TEST=y`

* `printf 'popen_test\npoweroff\n' | timeout 30s ./nuttx`

The simulator run prints popen_test: successful.

My bad, I looked only the first commit. So, everything is fine!

Just to clarify: I added the additional commit after seeing your suggestion.
Thanks for reviewing it again!

Comment thread system/popen/popen.c Outdated
****************************************************************************/

static sem_t g_popen_sem = SEM_INITIALIZER(1);
static FAR struct popen_file_s *g_popen_files;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but it isn't good (or even worse) to add a global list to fix a minor issue.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review. I trust your judgment here.

The global list was added because once popen() returns the real fdopen() stream, pclose(FILE *stream) still needs a way to recover the shell pid for waitpid().

The alternatives I can see are:

  1. keep external stream/fd -> pid state, as this PR does;
  2. store popen metadata in NuttX FILE/stdio internals;
  3. keep the old copied-FILE approach;
  4. make a broader libc/helper API change.

I chose (1) to avoid copying FILE or modifying libc internals, but I understand this may not be the tradeoff you want for this issue.

What approach would you suggest here?

Copy link
Copy Markdown
Contributor

@xiaoxiang781216 xiaoxiang781216 May 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@michallenc added fopencookie before, you may attach the private data to FILE by this function:
https://github.com/apache/nuttx/pulls?q=fopencookie

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the pointer. I followed this direction and reworked the implementation to use fopencookie(): popen now stores the pipe fd and shell pid in the stream's private cookie instead of a global FILE-to-pid list. The cookie callbacks forward read/write/close to the fd, and pclose() retrieves the pid from the cookie before fclose()/waitpid(). The popen test remains in the second commit and passes on sim:nsh.

Store the popen fd and shell pid in a fopencookie private cookie instead of embedding a copied FILE object in the popen container.

The stream callbacks forward I/O to the pipe fd, and pclose() reads the shell pid from the stream cookie before fclose() closes the fd.

Fixes apache#2937.

Signed-off-by: Nightt <87569709+nightt5879@users.noreply.github.com>
Add a small popen test that opens two read streams, verifies their output, and closes the first stream before the second.

This covers reading from independent popen streams and pclose() handling for multiple concurrently opened popen streams.

Signed-off-by: Nightt <87569709+nightt5879@users.noreply.github.com>
@nightt5879 nightt5879 force-pushed the nightt5879/fix-popen-file-copy-2937 branch from 23a698f to 9549d3b Compare May 30, 2026 05:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] system/popen memcpy FILE

3 participants