commit b8daff4719a017bf3f397759e6cd85245b1f6ceb
parent 324f8448f5eb6f6259e3870d714024379995936d
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Mon, 18 May 2026 13:55:42 -0700
Set executable permissions on linked outputs
Diffstat:
6 files changed, 119 insertions(+), 7 deletions(-)
diff --git a/driver/cc.c b/driver/cc.c
@@ -1574,6 +1574,13 @@ static int cc_run_link_exe(DriverEnv* env, const CcOptions* o,
out:
if (out_w) cfree_writer_close(out_w);
+ if (rc == 0 && o->output_path) {
+ if (driver_mark_executable_output(o->output_path) != 0) {
+ driver_errf(CC_TOOL, "failed to set executable mode: %s",
+ o->output_path);
+ rc = 1;
+ }
+ }
if (script && pipe)
cfree_link_script_free(cfree_pipeline_compiler(pipe), script);
if (pipe) driver_pipeline_free(pipe);
diff --git a/driver/driver.h b/driver/driver.h
@@ -151,6 +151,10 @@ CfreeWriter *driver_stdout_writer(DriverEnv *);
* loops don't slurp file contents on a hit. */
int driver_path_exists(const char *path);
+/* Set a linked binary output's final mode according to the active umask.
+ * Returns 0 on success, nonzero on chmod failure. */
+int driver_mark_executable_output(const char *path);
+
/* Diagnostic printing to host stderr. Format is `"<tool>: <fmt>\n"`. */
void driver_errf(const char *tool, const char *fmt, ...);
diff --git a/driver/env.c b/driver/env.c
@@ -1244,6 +1244,18 @@ int driver_path_exists(const char *path) {
return stat(path, &sb) == 0;
}
+int driver_mark_executable_output(const char *path) {
+ mode_t mask;
+ mode_t mode;
+
+ if (!path)
+ return 1;
+ mask = umask(0);
+ (void)umask(mask);
+ mode = (mode_t)(0777 & ~mask);
+ return chmod(path, mode) == 0 ? 0 : 1;
+}
+
void *driver_alloc(DriverEnv *e, size_t n) {
return e->heap->alloc(e->heap, n, _Alignof(max_align_t));
}
diff --git a/driver/ld.c b/driver/ld.c
@@ -1,5 +1,4 @@
#include <stdint.h>
-#include <sys/stat.h>
#include "driver.h"
#include "lib_resolve.h"
@@ -857,11 +856,15 @@ static int ld_run_link(LdOptions* o) {
out:
if (writer) cfree_writer_close(writer);
- /* Match GNU ld / lld: a successful link chmods the output to 0755
- * so the file is directly runnable. Done after closing the writer
- * so the bits are stable on disk. */
+ /* Match compiler/linker drivers: successful link outputs get executable
+ * file modes while still respecting the process umask. Done after closing
+ * the writer so the bits are stable on disk. */
if (rc == 0 && o->output_path) {
- (void)chmod(o->output_path, 0755);
+ if (driver_mark_executable_output(o->output_path) != 0) {
+ driver_errf(LD_TOOL, "failed to set executable mode: %s",
+ o->output_path);
+ rc = 1;
+ }
}
if (script && compiler) cfree_link_script_free(compiler, script);
if (compiler) driver_compiler_free(compiler);
diff --git a/test/driver/run.sh b/test/driver/run.sh
@@ -0,0 +1,81 @@
+#!/bin/sh
+# Driver-level permission checks for executable outputs.
+
+set -u
+
+script_dir=$(cd "$(dirname "$0")" && pwd)
+repo_root=$(cd "$script_dir/../.." && pwd)
+
+CFREE="${CFREE:-$repo_root/build/cfree}"
+
+if [ ! -x "$CFREE" ]; then
+ echo "driver: cfree binary not found at $CFREE" >&2
+ exit 2
+fi
+
+stat_mode() {
+ if mode=$(stat -f '%Lp' "$1" 2>/dev/null); then
+ printf '%s' "$mode"
+ else
+ stat -c '%a' "$1"
+ fi
+}
+
+work=$(mktemp -d "${TMPDIR:-/tmp}/cfree-driver-test.XXXXXX")
+trap 'rm -rf "$work"' EXIT
+
+cat > "$work/main.c" <<'SRC'
+int main(void) { return 0; }
+int _start(void) { return 0; }
+SRC
+
+pass=0
+fail=0
+
+check_mode() {
+ name=$1
+ path=$2
+ want=$3
+ got=$(stat_mode "$path")
+ if [ "$got" = "$want" ]; then
+ printf 'PASS %s\n' "$name"
+ pass=$((pass + 1))
+ else
+ printf 'FAIL %s (mode %s, want %s)\n' "$name" "$got" "$want"
+ fail=$((fail + 1))
+ fi
+}
+
+if (umask 077; "$CFREE" cc "$work/main.c" -o "$work/cc-exe") \
+ > "$work/cc.out" 2> "$work/cc.err"; then
+ check_mode "cc-executable-mode" "$work/cc-exe" 700
+else
+ printf 'FAIL cc-executable-mode (cfree cc failed)\n'
+ sed 's/^/ | /' "$work/cc.err"
+ fail=$((fail + 1))
+fi
+
+if (umask 077; "$CFREE" cc -c "$work/main.c" -o "$work/main.o") \
+ > "$work/cc-c.out" 2> "$work/cc-c.err"; then
+ : > "$work/ld-exe"
+ chmod 0644 "$work/ld-exe"
+ if (umask 077; "$CFREE" ld "$work/main.o" -o "$work/ld-exe") \
+ > "$work/ld.out" 2> "$work/ld.err"; then
+ check_mode "ld-executable-mode" "$work/ld-exe" 700
+ else
+ printf 'FAIL ld-executable-mode (cfree ld failed)\n'
+ sed 's/^/ | /' "$work/ld.err"
+ fail=$((fail + 1))
+ fi
+else
+ printf 'FAIL ld-executable-mode (cfree cc -c failed)\n'
+ sed 's/^/ | /' "$work/cc-c.err"
+ fail=$((fail + 1))
+fi
+
+total=$((pass + fail))
+if [ "$fail" -gt 0 ]; then
+ printf '\ndriver: %d/%d passed\n' "$pass" "$total"
+ exit 1
+fi
+printf '\ndriver: %d/%d passed\n' "$pass" "$total"
diff --git a/test/test.mk b/test/test.mk
@@ -1,5 +1,7 @@
# Data-driven tests. Included from the top-level Makefile.
#
+# - test-driver: narrow CLI behavior checks that do not belong to a specific
+# frontend/linker corpus. Depends on the cfree driver binary.
# - test-lex / test-pp: C frontend runners; depend on the cfree driver
# binary, which today fails to link (most of libcfree is header-only).
# - test-elf: ELF roundtrip harness in test/elf/; depends only on
@@ -25,9 +27,12 @@
# asm_parse / cfree_disasm_iter_* are still stubs; the harness builds
# and runs end-to-end so the wiring stays exercised. See doc/ASM.md.
-.PHONY: test test-lex test-pp test-pp-err test-elf test-ar test-ar-driver test-link test-cg-api test-toy test-opt test-dwarf test-debug test-parse test-parse-err test-asm test-wasm-front test-isa test-aa64-inline test-libc test-musl test-glibc test-lib-deps test-smoke-x64 test-smoke-rv64
+.PHONY: test test-driver test-lex test-pp test-pp-err test-elf test-ar test-ar-driver test-link test-cg-api test-toy test-opt test-dwarf test-debug test-parse test-parse-err test-asm test-wasm-front test-isa test-aa64-inline test-libc test-musl test-glibc test-lib-deps test-smoke-x64 test-smoke-rv64
-test: test-lex test-pp test-pp-err test-elf test-ar test-ar-driver test-link test-toy test-dwarf test-debug test-parse test-parse-err test-asm test-isa test-aa64-inline test-lib-deps
+test: test-driver test-lex test-pp test-pp-err test-elf test-ar test-ar-driver test-link test-toy test-dwarf test-debug test-parse test-parse-err test-asm test-isa test-aa64-inline test-lib-deps
+
+test-driver: bin
+ @CFREE=$(abspath $(BIN)) sh test/driver/run.sh
test-lex: bin
@CFREE=$(abspath $(BIN)) test/lex/run.sh