I don't know if this bug is intentional or not.
A compiled binary's process.argv comes up one element short whenever an argument, resolved as a filesystem path relative to cwd, points to the same file on disk as the binary itself. This silently discards a real user argument — even code that correctly guards process.argv.length gets the wrong result, with no error of any kind.
Repro
hello.ts:
ts const argument = process.argv.length > 2 ? process.argv[2] : "something"; console.log(`hello ,${argument}`);
```
$ scriptc build hello.ts -o hello
$ ./hello world
hello ,world
$ ./hello hello
hello ,something
```
Expected hello ,hello on the second run. Instead the argument is dropped and the code falls back to its default, silently. There's no crash, no diagnostic — this is a correctness bug, not just a crash bug.
The trigger is path resolution, not string-literal matching, by renaming the binary:
$ scriptc build hello.ts -o foo $ ./foo hello hello ,hello
Same argument, different binary name → correct result. "hello" no longer resolves to the same file as argv[0].
(A version of hello.ts without the length guard traps instead of silently defaulting — scriptc: RangeError: array index 2 out of bounds (length 2) — same root cause, surfaced as a crash because it does an unguarded process.argv[2] read. Either way, the underlying array is already the wrong length before code runs.)
Root cause (traced to source)
scr_lib_init() calls scr_lib_same_executable_arg(argv[0], argv[1]) to detect Node-style self-re-exec, where a relaunched process's argv contains the executable path twice (spawn(execPath, [argv[1], ...args])). The check does:
- strcmp(a, b) — exact string match
- realpath(a) vs realpath(b) — resolved absolute path match
Step 2 is the problem. It resolves both argv[0] and the first user-supplied argument as filesystem paths and compares the results, with no way to distinguish "this is a re-exec marker" from "this is an ordinary user argument that happens to resolve to the running binary's own file." When they collide, scr_lib_skip_reexec_arg is incorrectly set to true, and scr_lib_arg_count()/scr_lib_arg() silently drop one element from process.argv, shifting everything after it.
Relevant runtime code:
```c
static bool scr_lib_same_executable_arg(const char *a, const char *b) {
if (strcmp(a, b) == 0)
return true;
char resolved_a[PATH_MAX], resolved_b[PATH_MAX];
const char *use_a = realpath(a, resolved_a) != NULL ? resolved_a : a;
const char *use_b = realpath(b, resolved_b) != NULL ? resolved_b : b;
return strcmp(use_a, use_b) == 0;
}
void scr_lib_init(int argc, char **argv) {
scr_lib_argc = argc;
scr_lib_argv = argv;
scr_lib_skip_reexec_arg = argc >= 2 && scr_lib_same_executable_arg(argv[0], argv[1]);
atexit(scr_lib_cleanup);
}
int scr_lib_arg_count(void) { return scr_lib_argc - (scr_lib_skip_reexec_arg ? 1 : 0); }
const char *scr_lib_arg(int i) {
return scr_lib_argv[i + (scr_lib_skip_reexec_arg && i > 0 ? 1 : 0)];
}
```
The actual trigger is "argument resolves via realpath to the same file as the running binary," not "argument text equals binary name." Any CLI that takes filenames as arguments is exposed to this if run from its own directory with a coincidentally matching filename — this isn't limited to deliberately passing the binary's own name.
This concludes what i found. And Thank you.
Environment
I don't know if this bug is intentional or not.
A compiled binary's process.argv comes up one element short whenever an argument, resolved as a filesystem path relative to cwd, points to the same file on disk as the binary itself. This silently discards a real user argument — even code that correctly guards process.argv.length gets the wrong result, with no error of any kind.
Repro
hello.ts:
ts const argument = process.argv.length > 2 ? process.argv[2] : "something"; console.log(`hello ,${argument}`); ```
$ scriptc build hello.ts -o hello
$ ./hello world
hello ,world
$ ./hello hello
hello ,something
```
Expected
hello ,helloon the second run. Instead the argument is dropped and the code falls back to its default, silently. There's no crash, no diagnostic — this is a correctness bug, not just a crash bug.The trigger is path resolution, not string-literal matching, by renaming the binary:
$ scriptc build hello.ts -o foo $ ./foo hello hello ,hello Same argument, different binary name → correct result. "hello" no longer resolves to the same file as argv[0].
(A version of hello.ts without the length guard traps instead of silently defaulting —
scriptc: RangeError: array index 2 out of bounds (length 2)— same root cause, surfaced as a crash because it does an unguarded process.argv[2] read. Either way, the underlying array is already the wrong length before code runs.)Root cause (traced to source)
scr_lib_init()callsscr_lib_same_executable_arg(argv[0], argv[1])to detect Node-style self-re-exec, where a relaunched process's argv contains the executable path twice (spawn(execPath, [argv[1], ...args])). The check does:Step 2 is the problem. It resolves both argv[0] and the first user-supplied argument as filesystem paths and compares the results, with no way to distinguish "this is a re-exec marker" from "this is an ordinary user argument that happens to resolve to the running binary's own file." When they collide, scr_lib_skip_reexec_arg is incorrectly set to true, and scr_lib_arg_count()/scr_lib_arg() silently drop one element from process.argv, shifting everything after it.
Relevant runtime code:
```c
static bool scr_lib_same_executable_arg(const char *a, const char *b) {
if (strcmp(a, b) == 0)
return true;
char resolved_a[PATH_MAX], resolved_b[PATH_MAX];
const char *use_a = realpath(a, resolved_a) != NULL ? resolved_a : a;
const char *use_b = realpath(b, resolved_b) != NULL ? resolved_b : b;
return strcmp(use_a, use_b) == 0;
}
void scr_lib_init(int argc, char **argv) {
scr_lib_argc = argc;
scr_lib_argv = argv;
scr_lib_skip_reexec_arg = argc >= 2 && scr_lib_same_executable_arg(argv[0], argv[1]);
atexit(scr_lib_cleanup);
}
int scr_lib_arg_count(void) { return scr_lib_argc - (scr_lib_skip_reexec_arg ? 1 : 0); }
const char *scr_lib_arg(int i) {
return scr_lib_argv[i + (scr_lib_skip_reexec_arg && i > 0 ? 1 : 0)];
}
```
The actual trigger is "argument resolves via realpath to the same file as the running binary," not "argument text equals binary name." Any CLI that takes filenames as arguments is exposed to this if run from its own directory with a coincidentally matching filename — this isn't limited to deliberately passing the binary's own name.
This concludes what i found. And Thank you.
Environment