summaryrefslogtreecommitdiff
path: root/nix/libutil/archive.cc
diff options
context:
space:
mode:
authorReepca Russelstein <reepca@russelstein.xyz>2026-06-15 19:22:37 -0500
committerLudovic Courtès <ludo@gnu.org>2026-07-02 19:42:47 +0200
commit3e5c3217f334805531e5db1680d97490999253f7 (patch)
tree6689196ba72866ff210cb97b1a5c7f4e0dbc497a /nix/libutil/archive.cc
parentfc06ada0602983350646c741531ec2b1887f3407 (diff)
daemon: libutil: make nar parser much stricter.
The prior implementation of 'parse' was, to put it mildly, very accepting of invalid input. This made it very difficult to review its security properties, which ended up relying entirely on the fact that it still didn't allow "/" in directory entry names and that O_CREAT|O_EXCL implicitly causes symlinks to not be followed in the final component, and mkdir and symlink never follow symlinks in the final component, and all of the above throw exceptions when they fail. Some examples of what was allowed prior: 1. Empty archives: "nix-archive-1" "(" ")" 2. Archives with only a type: "nix-archive-1" "(" "type" "regular" ")" 3. Archives of regular files with multiple contents, which would be concatenated (e.g. this has contents "foobar"): "nix-archive-1" "(" "type" "regular" "contents" "foo" "contents" "bar" ")" 4. Archives of directories with multiple entries with the same name, and that aren't sorted: "nix-archive-1" "(" "type" "directory" "entry" "(" "name" "c" "node" "(" ")" ")" "entry" "(" "name" "b" "node" "(" ")" ")" "entry" "(" "name" "b" "node" "(" ")" ")" "entry" "(" "name" "a" "node" "(" ")" ")" ")" 5. Archives of directories with empty entries: "nix-archive-1" "(" "type" "directory" "entry" "(" ")" ")" 6. Archives of directories with entries with no name: "nix-archive-1" "(" "type" "directory" "entry" "(" "node" "(" ")" ")" ")" 7. Archives of directories with entries with no node: "nix-archive-1" "(" "type" "directory" "entry" "(" "name" "a" ")" ")" 7. Archives of directories with entries with multiple names: "nix-archive-1" "(" "type" "directory" "entry" "(" "name" "a" "node" "(" ")" "name" "b" ")" ")" 8. Archives of directories with entries with multiple nodes: "nix-archive-1" "(" "type" "directory" "entry" "(" "node" "(" ")" "node" "(" ")" ")" ")" 9. Archives of directories with entries with multiple names and multiple nodes: "nix-archive-1" "(" "type" "directory" "entry" "(" "name" "a" "node" "(" ")" "name" "b" "node" "(" ")" ")" ")" Such permissiveness rather defeats the "normalized" part of "Normalized ARchive". Additionally, 'parse' previously used recursion with no explicit depth limit. In practice Linux would still limit the depth to around 2048, since filenames longer than 4096 bytes yield ENAMETOOLONG, but no such restriction exists on Hurd. The permissiveness issue is resolved by rewriting 'parse', and the unbounded recursion issue is resolved by adding a nestLimit parameter so that no more than DIRECTORY_NESTING_LIMIT (currently 256) levels of recursion are used. An alternate implementation that doesn't use recursion, 'parse_unbounded', is included in case it is determined that the nesting limit should be removed, but currently only 'parse' is used. * nix/libutil/archive.cc (parse): rewrite to add nestLimit parameter. (parseDump): pass DIRECTORY_NESTING_LIMIT as nestLimit argument to 'parse'. (parse_unbounded): new function. Change-Id: Ia3c32cc645b609f6ae7f3bcafc491ec0073cbb51 Signed-off-by: Ludovic Courtès <ludo@gnu.org>
Diffstat (limited to 'nix/libutil/archive.cc')
-rw-r--r--nix/libutil/archive.cc192
1 files changed, 136 insertions, 56 deletions
diff --git a/nix/libutil/archive.cc b/nix/libutil/archive.cc
index 938fe8d4492..81c03f44d37 100644
--- a/nix/libutil/archive.cc
+++ b/nix/libutil/archive.cc
@@ -155,94 +155,169 @@ struct CaseInsensitiveCompare
155 } 155 }
156}; 156};
157 157
158/* Used to place a consistent upper bound on recursion depth */
159#define DIRECTORY_NESTING_LIMIT 256
158 160
159static void parse(ParseSink & sink, Source & source, const Path & path) 161static void parse(ParseSink & sink, Source & source, const Path & path,
162 int nestLimit)
160{ 163{
164 if (nestLimit <= 0) throw Error("nar directory nesting limit reached");
161 string s; 165 string s;
162 166
163 s = readString(source); 167 s = readString(source);
164 if (s != "(") throw badArchive("expected open tag"); 168 if (s != "(") throw badArchive("expected open tag");
165 169
166 enum { tpUnknown, tpRegular, tpDirectory, tpSymlink } type = tpUnknown; 170 s = readString(source);
167 171 if (s != "type") throw badArchive("expected type tag");
168 std::map<Path, int, CaseInsensitiveCompare> names;
169 172
170 while (1) { 173 s = readString(source);
171 checkInterrupt();
172 174
175 if (s == "regular") {
176 bool executable = false;
173 s = readString(source); 177 s = readString(source);
174 178 if (s == "executable") {
175 if (s == ")") { 179 executable = true;
176 break; 180 readString(source);
181 s = readString(source);
177 } 182 }
178 183
179 else if (s == "type") { 184 if (s != "contents") throw badArchive("expected contents tag");
180 if (type != tpUnknown)
181 throw badArchive("multiple type fields");
182 string t = readString(source);
183 185
184 if (t == "regular") { 186 sink.createRegularFile(path);
185 type = tpRegular; 187 if (executable) sink.isExecutable();
186 sink.createRegularFile(path); 188 parseContents(sink, source, path);
187 } 189 s = readString(source);
190 if (s != ")") throw badArchive("expected close tag");
191 }
192 else if (s == "symlink") {
193 s = readString(source);
194 if (s != "target") throw badArchive("expected target tag");
195 s = readString(source);
196 sink.createSymlink(path, s);
197 s = readString(source);
198 if (s != ")") throw badArchive("expected close tag");
199 }
200 else if (s == "directory") {
201 string prevName;
202 sink.createDirectory(path);
203 while (true) {
204 checkInterrupt();
205 s = readString(source);
206 if (s == ")") break;
207 if (s != "entry") throw badArchive("expected entry tag");
188 208
189 else if (t == "directory") { 209 s = readString(source);
190 sink.createDirectory(path); 210 if (s != "(") throw badArchive("expected entry open tag");
191 type = tpDirectory;
192 }
193 211
194 else if (t == "symlink") { 212 s = readString(source);
195 type = tpSymlink; 213 if (s != "name") throw badArchive("expected name tag");
196 }
197 214
198 else throw badArchive("unknown file type " + t); 215 s = readString(source);
216 string name = s;
217 if (name.empty() || name == "." || name == ".."
218 || name.find('/') != string::npos
219 || name.find((char) 0) != string::npos)
220 throw Error(std::format("NAR contains invalid file name `{}'", name));
221 if (!prevName.empty() && name <= prevName)
222 throw Error("NAR directory is not sorted");
199 223
200 } 224 s = readString(source);
225 if (s != "node") throw badArchive("expected node tag");
226
227 parse(sink, source, path + "/" + name, nestLimit - 1);
201 228
202 else if (s == "contents" && type == tpRegular) { 229 s = readString(source);
203 parseContents(sink, source, path); 230 if (s != ")") throw badArchive("expected entry close tag");
231 prevName = name;
204 } 232 }
233 }
234 else throw badArchive("unknown type");
235}
205 236
206 else if (s == "executable" && type == tpRegular) { 237/* Unbounded variant that doesn't recurse, uses path as its stack */
238static void parse_unbounded(ParseSink & sink, Source & source, Path & path)
239{
240 string prevName;
241 string s;
242start:
243 s = readString(source);
244 if (s != "(") throw badArchive("expected open tag");
245
246 s = readString(source);
247 if (s != "type") throw badArchive("expected type tag");
248
249 s = readString(source);
250
251 if (s == "regular") {
252 bool executable = false;
253 s = readString(source);
254 if (s == "executable") {
255 executable = true;
207 readString(source); 256 readString(source);
208 sink.isExecutable(); 257 s = readString(source);
209 } 258 }
210 259
211 else if (s == "entry" && type == tpDirectory) { 260 if (s != "contents") throw badArchive("expected contents tag");
212 string name, prevName; 261
262 sink.createRegularFile(path);
263 if (executable) sink.isExecutable();
264 parseContents(sink, source, path);
265 s = readString(source);
266 if (s != ")") throw badArchive("expected close tag");
267 }
268 else if (s == "symlink") {
269 s = readString(source);
270 if (s != "target") throw badArchive("expected target tag");
271 s = readString(source);
272 sink.createSymlink(path, s);
273 s = readString(source);
274 if (s != ")") throw badArchive("expected close tag");
275 }
276 else if (s == "directory") {
277 sink.createDirectory(path);
278 while (true) {
279 checkInterrupt();
280 s = readString(source);
281 if (s == ")") break;
282 if (s != "entry") throw badArchive("expected entry tag");
283
284 s = readString(source);
285 if (s != "(") throw badArchive("expected entry open tag");
213 286
214 s = readString(source); 287 s = readString(source);
215 if (s != "(") throw badArchive("expected open tag"); 288 if (s != "name") throw badArchive("expected name tag");
216 289
217 while (1) { 290 s = readString(source);
218 checkInterrupt(); 291 {
292 string name = s;
293 if (name.empty() || name == "." || name == ".."
294 || name.find('/') != string::npos
295 || name.find((char) 0) != string::npos)
296 throw Error(std::format("NAR contains invalid file name `{}'", name));
297 if (!prevName.empty() && name <= prevName)
298 throw Error("NAR directory is not sorted");
219 299
220 s = readString(source); 300 s = readString(source);
301 if (s != "node") throw badArchive("expected node tag");
221 302
222 if (s == ")") { 303 /* parse(sink, source, path + "/" + name); */
223 break; 304 path.append("/" + name);
224 } else if (s == "name") { 305 prevName = "";
225 name = readString(source); 306 goto start;
226 if (name.empty() || name == "." || name == ".." || name.find('/') != string::npos || name.find((char) 0) != string::npos)
227 throw Error(std::format("NAR contains invalid file name `{}'", name));
228 if (name <= prevName)
229 throw Error("NAR directory is not sorted");
230 prevName = name;
231 } else if (s == "node") {
232 if (s.empty()) throw badArchive("entry name missing");
233 parse(sink, source, path + "/" + name);
234 } else
235 throw badArchive("unknown field " + s);
236 } 307 }
237 }
238 308
239 else if (s == "target" && type == tpSymlink) { 309 continue_directory:
240 string target = readString(source); 310 s = readString(source);
241 sink.createSymlink(path, target); 311 if (s != ")") throw badArchive("expected entry close tag");
242 } 312 }
313 }
314 else throw badArchive("unknown type");
243 315
244 else 316 string::size_type slash_pos = path.rfind("/");
245 throw badArchive("unknown field " + s); 317 if (slash_pos != string::npos) {
318 prevName = string(path, slash_pos+1);
319 path.erase(slash_pos);
320 goto continue_directory;
246 } 321 }
247} 322}
248 323
@@ -258,7 +333,12 @@ void parseDump(ParseSink & sink, Source & source)
258 } 333 }
259 if (version != archiveVersion1) 334 if (version != archiveVersion1)
260 throw badArchive("input doesn't look like a normalized archive"); 335 throw badArchive("input doesn't look like a normalized archive");
261 parse(sink, source, ""); 336
337 parse(sink, source, "", DIRECTORY_NESTING_LIMIT);
338 /*
339 string file = "";
340 parse_unbounded(sink, source, file);
341 */
262} 342}
263 343
264 344