[Bug]: Linux service units cannot load when T3 home contains a quote, backslash, or newline #35945
Send to my agentSummary#
A Linux background service cannot load when its T3 home contains a literal double quote, backslash, or newline. The renderer accepts these directory names and emits a unit that systemd rejects, so quoting the executable path does not make this service configuration usable. This was reproduced by passing the actual pure renderer's output to systemd's offline verifier, with ordinary, space, percent, and dollar-sign paths passing as controls.
Before submitting#
- I searched existing issues and did not find a duplicate. Related installer history is listed under References; targeted open and closed issue/PR searches found no report of this service-unit loading failure.
- I included enough detail to reproduce or investigate the problem.
- Removed credentials, tokens, private paths, and sensitive data.
Area#
apps/server
Actual behavior#
At the pinned commit, renderBootServiceUnit produces ExecStart with the versioned executable under the supplied T3 home. systemd-analyze verify --man=no rejects the quote and backslash cases with Executable name contains special characters and a fatal unit-configuration error. The newline case splits the generated configuration into physical lines, produces unbalanced quoting, and leaves the service without an ExecStart.
The four control units pass verification. The same renderer correctly preserves spaces, literal %h, and ${UNSET} in those controls. This is an offline unit-loading reproduction, not an executed t3 service install or a full server startup.
Steps to reproduce#
- Use a clean checkout at the commit below and save the script below as
repro.mjsoutside the checkout. - On Linux with Node 26 and
systemd-analyze, runnode --disable-warning=ExperimentalWarning repro.mjs /path/to/t3code. - Observe the control units pass and the quote, backslash, and newline units fail.
The final script was run once and all three affected inputs failed deterministically in that run. Earlier exploratory renderer/verifier calls showed the same rejection. The script creates disposable data directories and harmless executable fixtures, invokes the exact extracted renderer and its helpers without application dependencies, verifies the generated units without starting a manager or service, and removes its scratch tree. Exit 1 means the defect was reproduced, exit 0 means all units loaded, and exit 2 means setup or a control failed. For the observed run, Node ran on the host and the optional third argument selected an existing local Ubuntu container image with systemd-analyze; the script used no network and mounted only its scratch directory. On Linux, omit that third argument and use the local verifier.
Reproduction script: repro.mjs
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { spawnSync } from 'node:child_process';
import { stripTypeScriptTypes } from 'node:module';
const checkout = path.resolve(process.argv[2] ?? '.');
const image = process.argv[3]; // Optional existing Linux image with systemd-analyze.
const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 't3-unit-repro-'));
let result = 2;
try {
for (const name of ['home', 'tmp', 'config', 'runtime', 'state', 'local'])
fs.mkdirSync(path.join(scratch, name));
const env = { ...process.env, HOME: path.join(scratch, 'home'),
USERPROFILE: path.join(scratch, 'home'), LOCALAPPDATA: path.join(scratch, 'local'),
TMPDIR: path.join(scratch, 'tmp'), XDG_CONFIG_HOME: path.join(scratch, 'config'),
XDG_RUNTIME_DIR: path.join(scratch, 'runtime'), T3CODE_HOME: path.join(scratch, 'state'),
SYSTEMD_LOG_LEVEL: 'debug' };
const run = (command, args) => spawnSync(command, args, { env, encoding: 'utf8' });
console.log(`Node ${process.version}`);
console.log(run('git', ['--version']).stdout.trim());
console.log(`Commit ${run('git', ['-C', checkout, 'rev-parse', 'HEAD']).stdout.trim()}`);
if (image) console.log(run('docker', ['--version']).stdout.trim());
const analyze = (args) => image
? run('docker', ['run', '--rm', '--network', 'none',
'-v', `${scratch}:${scratch}:ro`, '-e', 'SYSTEMD_LOG_LEVEL=debug',
'-e', `HOME=${env.HOME}`, '-e', `TMPDIR=${env.TMPDIR}`,
image, 'systemd-analyze', ...args])
: run('systemd-analyze', args);
const version = analyze(['--version']);
if (version.status !== 0) throw new Error('systemd-analyze unavailable');
console.log(version.stdout.split('\n')[0]);
const source = fs.readFileSync(path.join(checkout, 'apps/server/src/cloud/bootService.ts'), 'utf8');
const first = source.indexOf('const BOOT_SERVICE_NAME =');
const last = source.indexOf('function escapeXmlText');
if (first < 0 || last <= first) throw new Error('renderer extraction boundary changed');
// Execute the exact pure renderer and its helpers without installing the application.
const module = 'const SERVICE_UNIT_ENV="T3_BOOT_SERVICE_UNIT";\n' + source.slice(first, last);
const code = stripTypeScriptTypes(module);
const { renderBootServiceUnit } = await import('data:text/javascript;base64,' + Buffer.from(code).toString('base64'));
let failed = 0;
for (const [name, suffix, control] of [
['plain', 'plain', true], ['spaces', 'T3 Data', true],
['percent', '100%h', true], ['dollar', '${UNSET}', true],
['quote', 'T3 "Data"', false], ['backslash', 'T3\\Data', false],
['newline', 'T3\nData', false],
]) {
const baseDir = path.join(scratch, suffix);
const executable = path.join(baseDir, 'runtime/versions/1.2.3/t3');
fs.mkdirSync(path.dirname(executable), { recursive: true });
fs.mkdirSync(path.join(baseDir, 'userdata/logs'), { recursive: true });
fs.writeFileSync(executable, '#!/bin/sh\nexit 0\n', { mode: 0o755 });
const unitPath = path.join(scratch, `${name}.service`);
fs.writeFileSync(unitPath, renderBootServiceUnit({ program: [executable, '__service-launcher'],
baseDir, logPath: path.join(baseDir, 'userdata/logs/boot-service.log'), unitPath }));
const checked = analyze(['verify', '--man=no', unitPath]);
const output = checked.stdout + checked.stderr;
const diagnostics = output.split('\n').filter(line =>
/Executable (?:name|path) contains special characters|Unbalanced quoting|Service has no ExecStart=|fatal error|bad unit file setting/.test(line));
console.log(`${name}: verify exit ${checked.status}`);
for (const line of diagnostics) console.log(line.replaceAll(scratch, '$SCRATCH'));
if (control && checked.status !== 0) throw new Error(`${name} control failed`);
if (!control && checked.status !== 0) {
if (diagnostics.length === 0) throw new Error(`${name} failed for an unrelated reason`);
failed++;
}
}
result = failed ? 1 : 0;
} catch (error) {
console.log(`Setup: ${error.message}`);
} finally {
fs.rmSync(scratch, { recursive: true, force: true });
}
console.log(result === 1 ? 'RESULT: defect reproduced' : result === 0 ? 'RESULT: expected behavior' : 'RESULT: inconclusive');
process.exitCode = result;
Expected behavior#
A service configured for a T3 home accepted by the CLI and renderer should have a loadable unit that invokes the intended runtime, or should reject an unsupported directory before committing an unusable service definition. The CLI accepts an explicit data directory through apps/server/src/cli/config.ts#L44-L49, and the existing renderer/parser round-trip case explicitly includes quotes and a backslash in apps/server/src/cloud/bootService.test.ts#L121-L141. The installation path builds the program directly under that directory in apps/server/src/cloud/bootService.ts#L966-L973.
The systemd command-line contract applies its own parsing rules. Quoting a path does not override the executable-path validation: the published systemd v262.2 parser still rejects unsafe executable-path characters after unquoting and specifier expansion. The systemd syntax contract describes escaped control characters; a raw newline is a configuration-line boundary.
Impact#
Minor bug or occasional failure.
Impact: P2 - This prevents Linux background-service startup for a bounded set of valid filesystem directory names. Offline verification directly establishes that the generated units cannot load; no user data loss or live manager mutation was observed.
For a new installation, an ordinary directory is a practical bypass, and its unit passes the same verifier. Moving an existing environment to another directory was not tested. A full published T3 runtime was not executed under these paths, so independent runtime-loader limitations are outside this report. The observed systemd version is 255; newer executable-path validation was inspected in published v262.2 source, but v262.2 was not executed.
Version or commit#
T3 Code 59e64eef881632a23f89c4138bcf37f23d92bf93; server package version 0.1.9. GitHub's latest stable release endpoint returned v0.1.9 during this investigation.
Environment#
Node v26.12.1, Git 2.56.0, Docker 29.9.2; the offline verifier ran in an existing Ubuntu 24.04.5 LTS arm64 container with systemd 255 (255.4-1ubuntu8.18). No systemd manager, real T3 service, provider, credentials, or live account state was used.
Interface#
Package or setup: Linux background-service unit rendering used by t3 service install --base-dir.
Earliest failure stage#
Setup or configuration
Logs or stack traces#
The exact reproduction script emitted the following output. Random temporary directory prefixes are replaced by $SCRATCH by the script itself; no personal paths were retained.
Node v26.12.1
git version 2.56.0
Commit 59e64eef881632a23f89c4138bcf37f23d92bf93
Docker version 29.9.2, build a82e528901
systemd 255 (255.4-1ubuntu8.18)
plain: verify exit 0
spaces: verify exit 0
percent: verify exit 0
dollar: verify exit 0
quote: verify exit 1
$SCRATCH/quote.service:11: Executable name contains special characters: $SCRATCH/T3 "Data"/runtime/versions/1.2.3/t3
quote.service: Unit configuration has fatal error, unit will not be started.
Unit quote.service has a bad unit file setting.
backslash: verify exit 1
$SCRATCH/backslash.service:11: Executable name contains special characters: $SCRATCH/T3\Data/runtime/versions/1.2.3/t3
backslash.service: Unit configuration has fatal error, unit will not be started.
Unit backslash.service has a bad unit file setting.
newline: verify exit 1
$SCRATCH/newline.service:12: Unbalanced quoting, ignoring: ""$SCRATCH/T3"
newline.service: Service has no ExecStart=, ExecStop=, or SuccessAction=. Refusing.
Unit newline.service has a bad unit file setting.
RESULT: defect reproduced
The renderer quotes backslashes and double quotes but writes the resulting path directly as the executable in apps/server/src/cloud/bootService.ts#L68-L77 and apps/server/src/cloud/bootService.ts#L103-L110. The actual installation writes that rendered unit before activation in apps/server/src/cloud/bootService.ts#L1371-L1385. Existing round-trip tests exercise T3's parser and do not establish whether systemd accepts the definition.
References#
- https://github.com/pingdotgg/t3code/pull/22415 — addresses checksum parsing for backslash and newline T3 homes, before service-unit rendering; its replacement is below.
- https://github.com/pingdotgg/t3code/pull/25240 — fixes installer checksum handling for these homes; its discussion separately records a published backslash-path runtime-loader failure. This report concerns the generated unit being rejected before that executable can run, and also reproduces quote and newline paths.
tanstack-source added labelsbug
juliusmarminge closed this
Sign in to join this conversation.