Skip to content

system/popen: Avoid copying FILE#3511

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

system/popen: Avoid copying FILE#3511
nightt5879 wants to merge 4 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
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
Comment thread system/popen/popen.c Outdated
Comment thread system/popen/popen.c Outdated
Comment thread system/popen/popen.c Outdated
Comment thread system/popen/popen.c
Comment thread system/popen/popen.c Outdated
Move popen stream cleanup into the fopencookie close callback so pclose() can close the stream directly.

Also fix the invalid-mode error path, avoid closing the stream fd twice after fopencookie() takes ownership, and simplify the container free path.

Signed-off-by: Nightt <87569709+nightt5879@users.noreply.github.com>
@nightt5879
Copy link
Copy Markdown
Contributor Author

nightt5879 commented May 31, 2026

@xiaoxiang781216 Thanks, I addressed the latest review comments in commit 6a16a4d.

The follow-up changes:

  • changed the invalid-mode path to jump to errout_with_container, since no pipe fd exists there;
  • avoided closing the stream fd twice after fopencookie() has taken ownership;
  • removed the redundant NULL check before free();
  • moved fd close, waitpid(), and cookie free into popen_file_close(), so pclose() now calls fclose() directly.

One caveat I noticed while moving waitpid() into the fopencookie close callback: fclose() treats any non-OK close callback return as an EOF/error result. Because of that, popen_file_close() now returns OK after close() and waitpid() themselves succeed, and reports only close()/waitpid() failures as errors. Otherwise a normal nonzero shell termination status could be converted into an fclose() failure.

I re-tested with:

  • checkpatch.sh -f apps/system/popen/popen.c
  • sim:nsh build with CONFIG_SYSTEM_POPEN=y and CONFIG_TESTING_POPEN_TEST=y
  • popen_test, which prints successful

Comment thread system/popen/popen.c Outdated
Comment thread system/popen/popen.c
Let close() and waitpid() leave errno directly on failure instead of saving and restoring it manually.

Also document that fclose(stream) only closes retfd on the stream error path, so the child-side descriptors still need to be closed there.

Signed-off-by: Nightt <87569709+nightt5879@users.noreply.github.com>
@xiaoxiang781216
Copy link
Copy Markdown
Contributor

@nightt5879 please rebase your patch to fix the conflict, thanks.

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