Skip to content

Rebuild process execution safely#19

Merged
joetannenbaum merged 35 commits into
2.xfrom
feat/rebuild-process-exec
Jun 30, 2026
Merged

Rebuild process execution safely#19
joetannenbaum merged 35 commits into
2.xfrom
feat/rebuild-process-exec

Conversation

@WendellAdriel

Copy link
Copy Markdown
Member

Why

The original cpx built its commands as shell strings, which let crafted package names slip into a shell, swallowed child exit codes so a failing tool could still make cpx exit zero, and mangled forwarded arguments. None of that is safe to wrap other binaries with or to run in CI.

What changed

This rebuilds the execution path around argv arrays instead of shell strings. Package binaries and Composer both run through a small ProcessRunner over reopened stdio, so child exit codes propagate all the way back to cpx and arguments reach the target untouched (including everything after --). A PackageInvocation value object carries the forwarded tokens verbatim, package targets are validated against Composer's official name grammar before they ever touch the filesystem, and cache cleanup uses filesystem APIs rather than rm -rf. Composer output now streams live instead of being buffered and thrown away.

The supporting classes are also regrouped into focused Cache, Composer, Input, Packages, Process, Runtime, and Support namespaces.

Why not symfony/process

cpx is a package runner, so the child process needs the real terminal: interactive prompts, REPLs like tinker, colored output, and a passthrough TTY. symfony/process is built to capture or pipe output, not to hand the child the inherited descriptors, so it gets in the way of true passthrough. Raw proc_open with the inherited stdio is the simplest thing that preserves it.

Note

The generated install scaffold still sets allow-plugins: true, which the cache/install hardening work should revisit.

@WendellAdriel WendellAdriel force-pushed the feat/rebuild-process-exec branch from 12cfdfb to 46dbddd Compare June 26, 2026 17:46
@WendellAdriel WendellAdriel marked this pull request as ready for review June 26, 2026 17:47
@WendellAdriel WendellAdriel force-pushed the feat/rebuild-process-exec branch from 46dbddd to d05fa86 Compare June 29, 2026 14:52
Comment thread src/Composer/ComposerRunner.php Outdated
Comment thread src/Input/PackageInvocation.php
{
$target = array_shift($tokens);

return new self(is_string($target) ? $target : '', $tokens);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This will ultimately throw an exception if you pass the blank string... is that intentional?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes, that's intentional!

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is that just to keep the exception centralized? Doing it this way as opposed to throwing it here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yeah, just to centralize!

Comment thread src/Packages/Package.php Outdated
Comment thread src/Packages/PackageAliases.php
@joetannenbaum

Copy link
Copy Markdown
Collaborator

@WendellAdriel just a couple of comments here, not much blocking but give it a look before I merge and let me know what you think.

Base automatically changed from feat/symfony-console to feat/base-tests June 30, 2026 08:57
@WendellAdriel WendellAdriel changed the base branch from feat/base-tests to 2.x June 30, 2026 10:15
WendellAdriel and others added 15 commits June 30, 2026 11:20
Help and version are provided by Symfony Console out of the box, so the
custom commands are gone. The check, format, and test commands are
removed too, along with the now-dead Console::exec() and ConsoleException
they relied on.
The find-autoloader, load-laravel-bootstrap, and alias-classes options
are now VALUE_NEGATABLE with real boolean defaults, so the command reads
true booleans and the default lives in one place. The file fallback
coerces its parsed value before handing it to Symfony, keeping
`--find-autoloader=false` working.
Introduce Cpx\Process\ProcessRunner as the single proc_open() boundary, executing argv arrays with inherited stdio or captured output and returning child exit codes. Add an architecture test that forbids proc_open() outside the runner and bans shell-exec helpers.
Replace the shell-string Cpx\Composer wrapper with Cpx\Composer\ComposerRunner, which builds composer invocations as argv tokens (including --working-dir) and surfaces stderr on failure.
Drop the legacy Cpx\Console string parser/executor in favor of an immutable Cpx\Input\PackageInvocation built from raw argv tokens, preserving --, repeated options, and shell metacharacters as literal tokens.
Replace the generic Utils class with Cpx\Support\Arr and Cpx\Support\Filesystem for the array mapping and recursive directory deletion that are still needed.
Move Metadata and PackageMetadata under Cpx\Cache and the PHP execution helpers under Cpx\Runtime. Replace Metadata's string mode flag with intent-named recordRun() and recordUpdate() methods.
Move Package, PackageAlias, PackageAliases, and PackageCommandRunner into Cpx\Packages and execute resolved binaries as argv arrays that return child exit codes. Reject path-traversal version constraints during parsing.
Update the Symfony commands, application wiring, and sandbox bootstrap to build PackageInvocations, call ProcessRunner and ComposerRunner with argv arrays, and propagate child process exit codes.
- Run subprocesses over reopened stdio so exit codes survive any test harness
- Stream Composer output live instead of buffering and discarding it
- Thread console output through Package and drop the printColor buffering
- Validate package targets against Composer's official name grammar
@WendellAdriel WendellAdriel force-pushed the feat/rebuild-process-exec branch from 05cbb09 to 77d67d4 Compare June 30, 2026 10:20
@joetannenbaum joetannenbaum merged commit e8670fc into 2.x Jun 30, 2026
3 checks passed
@WendellAdriel WendellAdriel deleted the feat/rebuild-process-exec branch June 30, 2026 15:18
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.

3 participants