NAH 3.0.0
Native Application Host - Library API Reference
Loading...
Searching...
No Matches
nah_exec.h
Go to the documentation of this file.
1/*
2 * NAH Exec - Contract Execution for NAH
3 *
4 * This file provides low-level, unsandboxed process spawning for a
5 * LaunchContract. Hosts that need isolation should execute the contract with
6 * their own platform launcher instead.
7 * Platform-specific implementations for Unix and Windows.
8 *
9 * SPDX-License-Identifier: MIT
10 */
11
12#ifndef NAH_EXEC_H
13#define NAH_EXEC_H
14
15#ifdef __cplusplus
16
17#include "nah_core.h"
18
19#include <algorithm>
20#include <cstdlib>
21#include <string>
22#include <vector>
23
24#ifdef _WIN32
25#include <windows.h>
26#include <process.h>
27#else
28#include <unistd.h>
29#include <sys/wait.h>
30#include <cstring>
31#endif
32
33namespace nah {
34namespace exec {
35
36// ============================================================================
37// EXECUTION RESULT
38// ============================================================================
39
40struct ExecResult {
41 bool ok = false;
42 int exit_code = -1;
43 std::string error;
44};
45
46inline std::string validate_contract(const core::LaunchContract& contract) {
47 const auto contains_nul = [](const std::string& value) {
48 return value.find('\0') != std::string::npos;
49 };
50 if (contract.execution.binary.empty()) return "execution binary is empty";
51 if (contains_nul(contract.execution.binary)) return "execution binary contains NUL";
52 if (contains_nul(contract.execution.cwd)) return "working directory contains NUL";
53 for (const auto& argument : contract.execution.arguments) {
54 if (contains_nul(argument)) return "execution argument contains NUL";
55 }
56 for (const auto& [key, value] : contract.environment) {
57 if (key.empty() || key.find('=') != std::string::npos || contains_nul(key)) {
58 return "invalid environment key";
59 }
60 if (contains_nul(value)) return "environment value contains NUL: " + key;
61 }
62 if (!contract.execution.library_paths.empty()) {
63 const auto& key = contract.execution.library_path_env_key;
64 if (key.empty() || key.find('=') != std::string::npos || contains_nul(key)) {
65 return "invalid library path environment key";
66 }
67 for (const auto& path : contract.execution.library_paths) {
68 if (contains_nul(path)) return "library path contains NUL";
69 }
70 }
71 return {};
72}
73
74// ============================================================================
75// ENVIRONMENT BUILDING
76// ============================================================================
77
83inline std::vector<std::string> build_environment(const core::LaunchContract& contract) {
84 std::vector<std::string> env;
85
86 // Add all contract environment variables
87 for (const auto& [key, value] : contract.environment) {
88 env.push_back(key + "=" + value);
89 }
90
91 // Build library path
92 if (!contract.execution.library_paths.empty()) {
93 std::string lib_path;
94 char sep = core::get_path_separator();
95
96 for (size_t i = 0; i < contract.execution.library_paths.size(); i++) {
97 if (i > 0) lib_path += sep;
98 lib_path += contract.execution.library_paths[i];
99 }
100
101 // Check if key already exists in environment
102 std::string lib_key = contract.execution.library_path_env_key;
103 bool found = false;
104 for (auto& e : env) {
105 if (e.find(lib_key + "=") == 0) {
106 // Prepend to existing value
107 std::string existing = e.substr(lib_key.size() + 1);
108 e = lib_key + "=" + lib_path + sep + existing;
109 found = true;
110 break;
111 }
112 }
113
114 if (!found) {
115 env.push_back(lib_key + "=" + lib_path);
116 }
117 }
118
119 std::sort(env.begin(), env.end());
120 return env;
121}
122
123// ============================================================================
124// COMMAND LINE BUILDING
125// ============================================================================
126
130inline std::vector<std::string> build_argv(const core::LaunchContract& contract) {
131 std::vector<std::string> argv;
132 argv.push_back(contract.execution.binary);
133 for (const auto& arg : contract.execution.arguments) {
134 argv.push_back(arg);
135 }
136 return argv;
137}
138
139// ============================================================================
140// UNIX EXECUTION
141// ============================================================================
142
143#ifndef _WIN32
144
150inline ExecResult execute_unix(const core::LaunchContract& contract) {
151 ExecResult result;
152
153 if (const auto error = validate_contract(contract); !error.empty()) {
154 result.error = error;
155 return result;
156 }
157
158 auto argv_strings = build_argv(contract);
159 auto env_strings = build_environment(contract);
160
161 // Build C-style arrays
162 std::vector<char*> argv;
163 for (auto& s : argv_strings) {
164 argv.push_back(const_cast<char*>(s.c_str()));
165 }
166 argv.push_back(nullptr);
167
168 std::vector<char*> envp;
169 for (auto& s : env_strings) {
170 envp.push_back(const_cast<char*>(s.c_str()));
171 }
172 envp.push_back(nullptr);
173
174 pid_t pid = fork();
175
176 if (pid == -1) {
177 result.error = "fork failed: " + std::string(strerror(errno));
178 return result;
179 }
180
181 if (pid == 0) {
182 // Child process
183
184 // Change directory
185 if (!contract.execution.cwd.empty()) {
186 if (chdir(contract.execution.cwd.c_str()) != 0) {
187 _exit(127);
188 }
189 }
190
191 // Execute
192 execve(contract.execution.binary.c_str(), argv.data(), envp.data());
193
194 // If execve returns, it failed
195 _exit(127);
196 }
197
198 int status;
199 if (waitpid(pid, &status, 0) == -1) {
200 result.error = "waitpid failed: " + std::string(strerror(errno));
201 return result;
202 }
203
204 if (WIFEXITED(status)) {
205 result.exit_code = WEXITSTATUS(status);
206 result.ok = true;
207 } else if (WIFSIGNALED(status)) {
208 result.exit_code = 128 + WTERMSIG(status);
209 result.ok = true;
210 } else {
211 result.error = "process terminated abnormally";
212 }
213
214 return result;
215}
216
222inline ExecResult exec_replace_unix(const core::LaunchContract& contract) {
223 ExecResult result;
224
225 if (const auto error = validate_contract(contract); !error.empty()) {
226 result.error = error;
227 return result;
228 }
229
230 auto argv_strings = build_argv(contract);
231 auto env_strings = build_environment(contract);
232
233 std::vector<char*> argv;
234 for (auto& s : argv_strings) {
235 argv.push_back(const_cast<char*>(s.c_str()));
236 }
237 argv.push_back(nullptr);
238
239 std::vector<char*> envp;
240 for (auto& s : env_strings) {
241 envp.push_back(const_cast<char*>(s.c_str()));
242 }
243 envp.push_back(nullptr);
244
245 // Change directory
246 if (!contract.execution.cwd.empty()) {
247 if (chdir(contract.execution.cwd.c_str()) != 0) {
248 result.error = "chdir failed: " + std::string(strerror(errno));
249 return result;
250 }
251 }
252
253 // Replace process
254 execve(contract.execution.binary.c_str(), argv.data(), envp.data());
255
256 // If we get here, execve failed
257 result.error = "execve failed: " + std::string(strerror(errno));
258 return result;
259}
260
261#endif // !_WIN32
262
263// ============================================================================
264// WINDOWS EXECUTION
265// ============================================================================
266
267#ifdef _WIN32
268
272inline std::string build_command_line(const std::vector<std::string>& argv) {
273 std::string cmd;
274 for (size_t i = 0; i < argv.size(); i++) {
275 if (i > 0) cmd += " ";
276 const auto& argument = argv[i];
277 const bool quote = argument.empty() || argument.find_first_of(" \t\"") != std::string::npos;
278 if (!quote) { cmd += argument; continue; }
279 cmd += '"';
280 std::size_t backslashes = 0;
281 for (const char character : argument) {
282 if (character == '\\') { ++backslashes; continue; }
283 if (character == '"') {
284 cmd.append(backslashes * 2 + 1, '\\');
285 cmd += '"';
286 } else {
287 cmd.append(backslashes, '\\');
288 cmd += character;
289 }
290 backslashes = 0;
291 }
292 cmd.append(backslashes * 2, '\\');
293 cmd += '"';
294 }
295 return cmd;
296}
297
301inline std::string build_environment_block(const std::vector<std::string>& env) {
302 std::string block;
303 for (const auto& e : env) {
304 block += e;
305 block += '\0';
306 }
307 block += '\0'; // Double null terminator
308 return block;
309}
310
314namespace detail {
315inline ExecResult spawn_windows(const core::LaunchContract& contract, bool wait_for_exit) {
316 ExecResult result;
317
318 if (const auto error = validate_contract(contract); !error.empty()) {
319 result.error = error;
320 return result;
321 }
322
323 auto argv = build_argv(contract);
324 auto env = build_environment(contract);
325
326 std::string cmd_line = build_command_line(argv);
327 std::string env_block = build_environment_block(env);
328
329 STARTUPINFOA si = {0};
330 si.cb = sizeof(si);
331
332 PROCESS_INFORMATION pi = {0};
333
334 BOOL success = CreateProcessA(
335 contract.execution.binary.c_str(),
336 cmd_line.data(),
337 nullptr, // Process security attributes
338 nullptr, // Thread security attributes
339 FALSE, // Inherit handles
340 0, // Creation flags
341 env_block.data(),
342 contract.execution.cwd.empty() ? nullptr : contract.execution.cwd.c_str(),
343 &si,
344 &pi
345 );
346
347 if (!success) {
348 result.error = "CreateProcess failed: " + std::to_string(GetLastError());
349 return result;
350 }
351
352 if (wait_for_exit) {
353 if (WaitForSingleObject(pi.hProcess, INFINITE) == WAIT_FAILED) {
354 result.error = "WaitForSingleObject failed: " + std::to_string(GetLastError());
355 CloseHandle(pi.hProcess);
356 CloseHandle(pi.hThread);
357 return result;
358 }
359
360 DWORD exit_code;
361 if (GetExitCodeProcess(pi.hProcess, &exit_code)) {
362 result.exit_code = static_cast<int>(exit_code);
363 result.ok = true;
364 } else {
365 result.error = "GetExitCodeProcess failed";
366 }
367 } else {
368 result.ok = true;
369 result.exit_code = 0;
370 }
371
372 CloseHandle(pi.hProcess);
373 CloseHandle(pi.hThread);
374
375 return result;
376}
377} // namespace detail
378
379#endif // _WIN32
380
381// ============================================================================
382// CROSS-PLATFORM API
383// ============================================================================
384
393inline ExecResult execute(const core::LaunchContract& contract) {
394#ifdef _WIN32
395 return detail::spawn_windows(contract, true);
396#else
397 return execute_unix(contract);
398#endif
399}
400
409inline ExecResult exec_replace(const core::LaunchContract& contract) {
410#ifdef _WIN32
411 auto result = detail::spawn_windows(contract, false);
412 if (result.ok) {
413 ExitProcess(0);
414 }
415 return result;
416#else
417 return exec_replace_unix(contract);
418#endif
419}
420
421} // namespace exec
422} // namespace nah
423
424#endif // __cplusplus
425
426#endif // NAH_EXEC_H