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

Perl script fails when executed via argv[0] symlink but works when script is passed as argument #10

Closed
decoyjoe opened this issue Dec 11, 2024 · 6 comments

Comments

@decoyjoe
Copy link

First of all, thank you for an amazing project. Using APPerl, I was able to build a truly portable binary of a ZFS snapshot management tool called Sanoid, which I have hosted at decoyjoe/sanoid-portable.

However I'm seeing a strange issue when executing the portable binary using the argv[0] feature.

The binary contains a Perl script at /zip/bin/sanoid, so I create a symlink to automatically execute that script via argv[0]:

ln -s sanoid-portable sanoid

When I execute the binary using the symlink, it fails with an error related to launching subprocesses:

root@apps1:~# ./sanoid --take-snapshots --verbose --force-update
INFO: cache forcibly expired - updating from zfs list.
INFO: taking snapshots...
taking snapshot tank/app-data/app1@autosnap_2024-12-10_20:07:26_daily
Timed out waiting for subprocesses to start at /zip/lib/perl5/Capture/Tiny.pm line 271.
        Capture::Tiny::_wait_for_tees(HASH(0x100080a21ee0)) called at /zip/lib/perl5/Capture/Tiny.pm line 366
        Capture::Tiny::_capture_tee(0, 1, 0, 1, CODE(0x100080a22000)) called at /zip/bin/sanoid line 621
        main::take_snapshots(HASH(0x1000808bdd68), HASH(0x1000808bdee8), HASH(0x1000808ae000), HASH(0x1000808adfe8)) called at /zip/bin/sanoid line 84

If I rename the binary back to perl.com and execute the script by passing it as an argument to Perl, it works fine:

root@apps1:~# ln -s sanoid-portable perl.com
root@apps1:~# ./perl.com /zip/bin/sanoid --take-snapshots --verbose --force-update
INFO: cache forcibly expired - updating from zfs list.
INFO: taking snapshots...
taking snapshot tank/app-data/app1@autosnap_2024-12-10_20:09:12_daily
INFO: cache forcibly expired - updating from zfs list.
root@apps1:~#

Is this behavior a bug in the argv[0] functionality, or am I using APPerl (or Perl itself) incorrectly?

I'm not very familiar with Perl, so it's possible I'm misunderstanding something fundamental. Any guidance or insights would be greatly appreciated.

@G4Vi
Copy link
Owner

G4Vi commented Dec 11, 2024

Cool use of APPerl!

With cosmopolitan binaries, you can pass --strace to get log of the syscalls, so for your example I think you'd run:
./sanoid --strace --take-snapshots --verbose --force-update, cosmopolitan should remove --strace from the args before passing them to the program. Seeing what binaries it's trying to exec might provides some clues on what's happening.

Does the issue happen if you make copies instead of symlinks? I'm not familiar with sanoid, but maybe it tries to execute another copy of the currently running binary, but resolves the symlink itself and loses info on what binary is executing.

Another possibility is it's attempting to run a script with $^X. When taking advantage of argv[0] script execution, $^X doesn't point to a Perl interpreter as it's pointing to your script executing binary. This is a limitation of argv[0] script execution.

In any case, assuming it's not an APPerl bug, sanoid can be patched to run the subprocesses in a way compatible with APPerl, or if it works with ./perl.com /zip/bin/script, you could build APPerl as you're currently doing, but leave it named perl.com and instruct making shell scripts that run ./perl.com /zip/bin/script.

@decoyjoe
Copy link
Author

decoyjoe commented Dec 12, 2024

Thank you for the --strace tip, that was very helpful.

Following the code path, I found that Sanoid is using dagolden/Capture-Tiny to capture stderr output by calling tee_stderr here.

In looking at Capture-Tiny, turns out that library does indeed use $^X here. So that explains the issue, just as you noted.

Do you have any ideas of ways I can workaround this? I really like the elegance of being able to just symlink sanoid -> sanoid-portable. Requiring a shell script kind of kills the value and ease-of-use of a single, portable binary.

I also don't quite want to submit a patch to the upstream Sanoid tool just to make it work with my portable binary. I'd like that tool to work as-is in a portable fashion.

Although I'm not opposed to monkey patching Sanoid during the APPerl build if necessary to make it work.

I'm also wondering if I could somehow leverage the default_script feature to work around this. I already use a default script to write a help message, so perhaps I can put some logic in there to workaround this issue? The exec looks like this from the strace:

SYS �[1;33m229239�[0m �[1;33m229239�[0m         93'099'508 execve("/root/sanoid", {"/root/sanoid", "-C0", "-e", "use Fcntl;
$SIG{HUP}=sub{exit};
if ( my $fn=shift ) {
    sysopen(my $fh, qq{$fn}, O_WRONLY|O_CREAT|O_EXCL) or die $!;
    print {$fh} $$;
    close $fh;
}
my $buf; while (sysread(STDIN, $buf, 2048)) {
    syswrite(STDOUT, $buf); syswrite(STDERR, $buf);
}
"

My idea perhaps is to not use the APPerl argv[0] feature and instead always execute the default_script. Then write some logic in there that determines whether to run sanoid or perl depending on the arguments passed to it. I suppose that would be me simply writing my own argv[0] implementation.

I'd really like to get my project to work with the argv[0] feature if at all possible, so I'm open to any and all ideas you may have.

@G4Vi
Copy link
Owner

G4Vi commented Dec 12, 2024

APPerl should ignore argv[0] script execution and run Perl if the APPERL_SCRIPTNAME environment variable is set to perl. See the script choosing logic here:

+ // check for APPERL_SCRIPTNAME or argv[0] or default script execution
+ do {
+ const char *envscriptname = getenv("APPERL_SCRIPTNAME");
+ const char *programname = envscriptname ? envscriptname : argv[0];
+ const char *slash = strrchr(programname, '/');
+ if(slash != NULL)
+ {
+ programname = slash + 1;
+ }
+ const char *dot = strrchr(programname, '.');
+ const unsigned namelen = dot ? dot - programname : strlen(programname);
+
+ // shortcut for normal execution
+ if((namelen == 4) && (memcmp("perl", programname, 4) == 0))
+ {
+ break;
+ }
+
+ // /zip/bin/ script execution
+ #define SCRIPTPATH "/zip/bin/"
+ static char name[256] = SCRIPTPATH;
+ if(sizeof(SCRIPTPATH)+namelen <= sizeof(name))
+ {
+ memcpy(name + sizeof(SCRIPTPATH) - 1, programname, namelen);
+ name[sizeof(SCRIPTPATH)-1+namelen] = '\0';
+ struct stat st;
+ if((stat(name, &st) == 0) && S_ISREG(st.st_mode))
+ {
+ scriptname = name;
+ break;
+ }
+ }
+ #undef SCRIPTPATH
+
+ // default script
+ #define DEFAULT_SCRIPT_SENTINEL "APPERL_DEFAULT_SCRIPT"
+ volatile static const char default_script[sizeof(DEFAULT_SCRIPT_SENTINEL)+256] = DEFAULT_SCRIPT_SENTINEL;
+ if(default_script[sizeof(DEFAULT_SCRIPT_SENTINEL)])
+ {
+ scriptname = &default_script[sizeof(DEFAULT_SCRIPT_SENTINEL)];
+ break;
+ }
+ #undef DEFAULT_SCRIPT_SENTINEL
+ } while(0);

The patch to set the environment variable could probably applied to Capture-Tiny or Sanoid as long as it's applied before exec, if you didn't want to patch either of those distributions, making a wrapper default_script as you suggested could work too.

@decoyjoe
Copy link
Author

That worked perfectly. I was able to add a one-line patch to the build that added $ENV{APPERL_SCRIPTNAME} = 'perl'; to the top of the sanoid script so that any future $^X invocations would launch perl instead of /zip/bin/sanoid again.

That got me thinking, how is the perl executable located? /zip/bin doesn't seem to be in the PATH environment variable so I assume there must be some special APPerl logic to launch the bundled perl executable?

I would like to do something similar, where I bundle some additional tools in /zip/bin that syncoid (sanoid related script) relies on. I built an APE version of pv (using superconfigure) and updated the APPerl build to include the pv binary at /zip/bin/pv and patched syncoid to use the absolute path /zip/bin/pv. However syncoid fails to find it with command -v /zip/bin/pv and fails to execute it with the full path /zip/bin/pv.

With strace I was able to confirm that perl can stat the file:

fstatat(AT_FDCWD, "/zip/bin/pv", [{.st_size=1'177'661, .st_blocks=548'864/512, .st_mode=0100644, .st_dev=0x14ccab, .st_ino=0x18a352c, .st_blksize=65'536}], 0) → 0 ENOENT

But can't exec it:

execve("/zip/bin/pv") failed -1 ENOENT

Is there a special way that other binaries located in the PKZIP need to be executed? Is this even possible to do with APPerl/Cosmopolitan?

@G4Vi
Copy link
Owner

G4Vi commented Dec 15, 2024

was able to add a one-line patch to the build that added $ENV{APPERL_SCRIPTNAME} = 'perl'; to the top of the sanoid script so that any future $^X invocations would launch perl instead of /zip/bin/sanoid again.

Excellent, I'm glad that works! It would nice to include this by default with APPerl, but it would interfere with running other APPerl executables from APPerl.

That got me thinking, how is the perl executable located? ``

An APPerl executable only consists of one executable, perl, patched with the embedded script execution logic, I linked. When APPERL_SCRIPTNAME or (if not set) argv[0] is set to perl, rather than attempting to run a script from /zip/bin, it just continues into normal argv parsing. From your example, I'd expect $^X to be pointing to /root/sanoid not /zip/bin/sanoid as the latter is just the script not the Perl executable. In a non APPerl scenario, $^X would be something like /usr/bin/perl. In summary, even with APPerl, Perl always knows where Perl is, in $^X, however, APPerl's embedded script execution can interfere.

However syncoid fails to find it with command -v /zip/bin/pv

Shelling out to run command -v won't work as /zip only exists within the running APPerl executable.

Is there a special way that other binaries located in the PKZIP need to be executed? Is this even possible to do with APPerl/Cosmopolitan?

Unfortunately, Cosmopolitan doesn't currently support exec from /zip. Getting it to work cross-platform is tricky as not all OSs allow running executables from memory. This is the current status, jart/cosmopolitan#888, but I don't foresee picking it back up anytime soon.

As a workaround, you should be able to copy it to disk and then execute it, to copy it should be something like:

use File::Copy 'cp';
cp('/zip/bin/pv', '/tmp');

I'm not sure if the executable bit is preserved by /zip, you may need to chmod it after the copy.

@decoyjoe
Copy link
Author

From your example, I'd expect $^X to be pointing to /root/sanoid not /zip/bin/sanoid as the latter is just the script not the Perl executable. In a non APPerl scenario, $^X would be something like /usr/bin/perl. In summary, even with APPerl, Perl always knows where Perl is, in $^X, however, APPerl's embedded script execution can interfere.

That's correct and makes perfect sense. I was getting hung up on the PKZIP content and forgetting that it's still an executable binary that launches Perl.

Shelling out to run command -v won't work as /zip only exists within the running APPerl executable.

Of course, makes sense. I again got too lost trying to work in the zip.

Unfortunately, Cosmopolitan doesn't currently support exec from /zip. Getting it to work cross-platform is tricky as not all OSs allow running executables from memory. This is the current status, jart/cosmopolitan#888, but I don't foresee picking it back up anytime soon.

I did think I was perhaps a bit too ambitious with my usage of cosmopolitan/APE but that confirms it, I just got a bit too excited with what I wanted to do 😄


Anyways, thanks again for all the help and for the awesome project!

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

No branches or pull requests

2 participants