Commit 0116ce0829 for bind

commit 0116ce082922f975585c414746a0391545c014c4
Author: Alessio Podda <alessio@isc.org>
Date:   Tue Oct 27 14:55:22 2026 +0100

    Use tagged configuration tokens with absolute source lines

    We want to stop using the ISC_LEXOPT_EOL hint to harmonize
    parse_btokens with the rest of config parsing.

    To do so, we introduce a new tagged representation for tokens that
    can contain metadata, such as the line count, and pull the line
    count from the lexer instead of giving us a special EOL token.

diff --git a/bin/named/include/named/server.h b/bin/named/include/named/server.h
index d3aea2f097..c1a0a4952c 100644
--- a/bin/named/include/named/server.h
+++ b/bin/named/include/named/server.h
@@ -426,4 +426,4 @@ named_server_getmemprof(void);
 isc_result_t
 named_register_one_plugin(const cfg_obj_t *config, const cfg_obj_t *obj,
 			  cfg_aclconfctx_t *aclctx, const char *plugin_path,
-			  const char *const *parameters, void *callback_data);
+			  const cfg_token_t *parameters, void *callback_data);
diff --git a/bin/named/server.c b/bin/named/server.c
index 19574da0c5..96e3445ca6 100644
--- a/bin/named/server.c
+++ b/bin/named/server.c
@@ -3556,7 +3556,7 @@ create_mapped_acl(void) {
 isc_result_t
 named_register_one_plugin(const cfg_obj_t *config, const cfg_obj_t *obj,
 			  cfg_aclconfctx_t *aclctx, const char *plugin_path,
-			  const char *const *parameters, void *callback_data) {
+			  const cfg_token_t *parameters, void *callback_data) {
 	char full_path[PATH_MAX];
 	isc_result_t result;
 	ns_hook_data_t *hookdata = callback_data;
diff --git a/bin/plugins/filter-a.c b/bin/plugins/filter-a.c
index 5a99cd3f64..ade56aea83 100644
--- a/bin/plugins/filter-a.c
+++ b/bin/plugins/filter-a.c
@@ -192,7 +192,7 @@ parse_filter_a_on(cfg_tokens_t *tok, const char *name, filter_a_t *dstp) {
 }

 static isc_result_t
-parse_parameters(const char *const *parameters, const char *cfg_file,
+parse_parameters(const cfg_token_t *parameters, const char *cfg_file,
 		 unsigned long cfg_line, filter_params_t *params) {
 	isc_result_t result = ISC_R_SUCCESS;
 	cfg_tokens_t tok;
@@ -284,7 +284,7 @@ cleanup:
 }

 static isc_result_t
-configure_instance(filter_instance_t *inst, const char *const *parameters,
+configure_instance(filter_instance_t *inst, const cfg_token_t *parameters,
 		   const void *cfg, const char *cfg_file,
 		   unsigned long cfg_line, isc_mem_t *mctx, void *aclctx) {
 	isc_result_t result = ISC_R_SUCCESS;
@@ -324,7 +324,7 @@ cleanup:
  * register hook functions into the view hook table.
  */
 isc_result_t
-plugin_register(const char *const *parameters, const void *cfg,
+plugin_register(const cfg_token_t *parameters, const void *cfg,
 		const char *cfg_file, unsigned long cfg_line, isc_mem_t *mctx,
 		void *aclctx, ns_hooktable_t *hooktable,
 		const ns_pluginctx_t *ctx, void **instp) {
@@ -366,7 +366,7 @@ cleanup:
 }

 isc_result_t
-plugin_check(const char *const *parameters, const void *cfg,
+plugin_check(const cfg_token_t *parameters, const void *cfg,
 	     const char *cfg_file, unsigned long cfg_line, isc_mem_t *mctx,
 	     void *aclctx, const ns_pluginctx_t *ctx ISC_ATTR_UNUSED) {
 	isc_result_t result = ISC_R_SUCCESS;
diff --git a/bin/plugins/filter-aaaa.c b/bin/plugins/filter-aaaa.c
index 9b01918c0a..557d6ffe71 100644
--- a/bin/plugins/filter-aaaa.c
+++ b/bin/plugins/filter-aaaa.c
@@ -192,7 +192,7 @@ parse_filter_aaaa_on(cfg_tokens_t *tok, const char *name, filter_aaaa_t *dstp) {
 }

 static isc_result_t
-parse_parameters(const char *const *parameters, const char *cfg_file,
+parse_parameters(const cfg_token_t *parameters, const char *cfg_file,
 		 unsigned long cfg_line, filter_params_t *params) {
 	isc_result_t result = ISC_R_SUCCESS;
 	cfg_tokens_t tok;
@@ -286,7 +286,7 @@ cleanup:
 }

 static isc_result_t
-configure_instance(filter_instance_t *inst, const char *const *parameters,
+configure_instance(filter_instance_t *inst, const cfg_token_t *parameters,
 		   const void *cfg, const char *cfg_file,
 		   unsigned long cfg_line, isc_mem_t *mctx, void *aclctx) {
 	isc_result_t result = ISC_R_SUCCESS;
@@ -326,7 +326,7 @@ cleanup:
  * register hook functions into the view hook table.
  */
 isc_result_t
-plugin_register(const char *const *parameters, const void *cfg,
+plugin_register(const cfg_token_t *parameters, const void *cfg,
 		const char *cfg_file, unsigned long cfg_line, isc_mem_t *mctx,
 		void *aclctx, ns_hooktable_t *hooktable,
 		const ns_pluginctx_t *ctx, void **instp) {
@@ -369,7 +369,7 @@ cleanup:
 }

 isc_result_t
-plugin_check(const char *const *parameters, const void *cfg,
+plugin_check(const cfg_token_t *parameters, const void *cfg,
 	     const char *cfg_file, unsigned long cfg_line, isc_mem_t *mctx,
 	     void *aclctx, const ns_pluginctx_t *ctx ISC_ATTR_UNUSED) {
 	isc_result_t result = ISC_R_SUCCESS;
diff --git a/bin/plugins/synthrecord.c b/bin/plugins/synthrecord.c
index 337e7bbd2f..f5faf08e09 100644
--- a/bin/plugins/synthrecord.c
+++ b/bin/plugins/synthrecord.c
@@ -408,7 +408,7 @@ typedef struct synthrecord_params {
 } synthrecord_params_t;

 static isc_result_t
-synthrecord_parseparams(const char *const *parameters, const char *cfgfile,
+synthrecord_parseparams(const cfg_token_t *parameters, const char *cfgfile,
 			unsigned long cfgline, synthrecord_params_t *params) {
 	isc_result_t result = ISC_R_SUCCESS;
 	cfg_tokens_t *tok = &params->tokens;
@@ -582,7 +582,7 @@ synthrecord_parseallowsynth(synthrecord_t *inst, const cfg_obj_t *cfg,
 }

 static isc_result_t
-synthrecord_parseconfig(synthrecord_t *inst, const char *const *parameters,
+synthrecord_parseconfig(synthrecord_t *inst, const cfg_token_t *parameters,
 			const cfg_obj_t *cfg, const char *cfgfile,
 			unsigned long cfgline, cfg_aclconfctx_t *aclctx,
 			const dns_name_t *zname) {
@@ -607,7 +607,7 @@ cleanup:
 }

 isc_result_t
-plugin_register(const char *const *parameters, const void *cfg,
+plugin_register(const cfg_token_t *parameters, const void *cfg,
 		const char *cfgfile, unsigned long cfgline, isc_mem_t *mctx,
 		void *aclctx, ns_hooktable_t *hooktable,
 		const ns_pluginctx_t *ctx, void **instp) {
@@ -646,7 +646,7 @@ plugin_register(const char *const *parameters, const void *cfg,
 }

 isc_result_t
-plugin_check(const char *const *parameters, const void *cfg,
+plugin_check(const cfg_token_t *parameters, const void *cfg,
 	     const char *cfgfile, unsigned long cfgline, isc_mem_t *mctx,
 	     void *aclctx, const ns_pluginctx_t *ctx) {
 	isc_result_t result;
diff --git a/bin/tests/system/dyndb/driver/driver.c b/bin/tests/system/dyndb/driver/driver.c
index a5f604176f..e3d2b9dea6 100644
--- a/bin/tests/system/dyndb/driver/driver.c
+++ b/bin/tests/system/dyndb/driver/driver.c
@@ -85,7 +85,7 @@ dns_dyndb_version_t dyndb_version;
  *                        (for one dyndb section).
  */
 isc_result_t
-dyndb_init(isc_mem_t *mctx, const char *name, const char *const *parameters,
+dyndb_init(isc_mem_t *mctx, const char *name, const cfg_token_t *parameters,
 	   const char *file, unsigned long line, const dns_dyndbctx_t *dctx,
 	   void **instp) {
 	isc_result_t result = ISC_R_SUCCESS;
@@ -99,7 +99,7 @@ dyndb_init(isc_mem_t *mctx, const char *name, const char *const *parameters,
 	REQUIRE(dctx != NULL);

 	/* Every parameter is a plain string: build argv from them. */
-	for (const char *const *t = parameters; *t != NULL; t++) {
+	for (const cfg_token_t *t = parameters; t->kind != CFG_TOKEN_EOF; t++) {
 		size++;
 	}
 	argv = isc_mem_cget(mctx, size + 1, sizeof(*argv));
diff --git a/bin/tests/system/hooks/driver/test-async.c b/bin/tests/system/hooks/driver/test-async.c
index 15aab8923b..806594023c 100644
--- a/bin/tests/system/hooks/driver/test-async.c
+++ b/bin/tests/system/hooks/driver/test-async.c
@@ -115,7 +115,7 @@ logmsg(const char *fmt, ...) {
  * register hook functions into the view hook table.
  */
 isc_result_t
-plugin_register(const char *const *parameters, const void *cfg,
+plugin_register(const cfg_token_t *parameters, const void *cfg,
 		const char *cfg_file, unsigned long cfg_line, isc_mem_t *mctx,
 		void *aclctx, ns_hooktable_t *hooktable,
 		const ns_pluginctx_t *ctx, void **instp) {
@@ -148,7 +148,7 @@ plugin_register(const char *const *parameters, const void *cfg,
 }

 isc_result_t
-plugin_check(const char *const *parameters, const void *cfg,
+plugin_check(const cfg_token_t *parameters, const void *cfg,
 	     const char *cfg_file, unsigned long cfg_line, isc_mem_t *mctx,
 	     void *aclctx, const ns_pluginctx_t *ctx) {
 	UNUSED(parameters);
diff --git a/bin/tests/system/hooks/driver/test-syncplugin.c b/bin/tests/system/hooks/driver/test-syncplugin.c
index 2248fd93b6..8bcf8f72c6 100644
--- a/bin/tests/system/hooks/driver/test-syncplugin.c
+++ b/bin/tests/system/hooks/driver/test-syncplugin.c
@@ -66,7 +66,7 @@ typedef struct {
 } syncplugin_params_t;

 static isc_result_t
-syncplugin__parse_params(const char *const *parameters, const char *cfgfile,
+syncplugin__parse_params(const cfg_token_t *parameters, const char *cfgfile,
 			 unsigned long cfgline, syncplugin_params_t *params) {
 	cfg_tokens_t *tok = &params->tokens;

@@ -122,7 +122,7 @@ syncplugin__parse_rcode(const char *rcodestr, uint8_t *rcode) {
 }

 isc_result_t
-plugin_register(const char *const *parameters, const void *cfg,
+plugin_register(const cfg_token_t *parameters, const void *cfg,
 		const char *cfgfile, unsigned long cfgline, isc_mem_t *mctx,
 		void *aclctx, ns_hooktable_t *hooktable,
 		const ns_pluginctx_t *ctx, void **instp) {
@@ -216,7 +216,7 @@ cleanup:
 }

 isc_result_t
-plugin_check(const char *const *parameters, const void *cfg,
+plugin_check(const cfg_token_t *parameters, const void *cfg,
 	     const char *cfgfile, unsigned long cfgline, isc_mem_t *mctx,
 	     void *aclctx, const ns_pluginctx_t *ctx) {
 	UNUSED(parameters);
diff --git a/lib/dns/dyndb.c b/lib/dns/dyndb.c
index 6ce26d0d19..677823f723 100644
--- a/lib/dns/dyndb.c
+++ b/lib/dns/dyndb.c
@@ -196,7 +196,7 @@ unload_library(dyndb_implementation_t **impp) {

 isc_result_t
 dns_dyndb_load(const char *libname, const char *name,
-	       const char *const *parameters, const char *file,
+	       const cfg_token_t *parameters, const char *file,
 	       unsigned long line, isc_mem_t *mctx,
 	       const dns_dyndbctx_t *dctx) {
 	isc_result_t result;
diff --git a/lib/dns/include/dns/dyndb.h b/lib/dns/include/dns/dyndb.h
index 113988355d..d1a6830982 100644
--- a/lib/dns/include/dns/dyndb.h
+++ b/lib/dns/include/dns/dyndb.h
@@ -19,6 +19,8 @@

 #include <dns/types.h>

+typedef struct cfg_token cfg_token_t;
+
 /*!
  * \brief
  * Context for initializing a dyndb module.
@@ -52,13 +54,13 @@ struct dns_dyndbctx {
  * if not, set DNS_DYNDB_AGE to 0.
  */
 #ifndef DNS_DYNDB_VERSION
-#define DNS_DYNDB_VERSION 4
+#define DNS_DYNDB_VERSION 5
 #define DNS_DYNDB_AGE	  0
 #endif /* ifndef DNS_DYNDB_VERSION */

 typedef isc_result_t
 dns_dyndb_register_t(isc_mem_t *mctx, const char *name,
-		     const char *const *parameters, const char *file,
+		     const cfg_token_t *parameters, const char *file,
 		     unsigned long line, const dns_dyndbctx_t *dctx,
 		     void **instp);
 /*%
@@ -99,7 +101,7 @@ dns_dyndb_version_t(unsigned int *flags);

 isc_result_t
 dns_dyndb_load(const char *libname, const char *name,
-	       const char *const *parameters, const char *file,
+	       const cfg_token_t *parameters, const char *file,
 	       unsigned long line, isc_mem_t *mctx, const dns_dyndbctx_t *dctx);
 /*%
  * Load a dyndb module.
diff --git a/lib/isccfg/check.c b/lib/isccfg/check.c
index 34de1e5bfe..f8434aeb61 100644
--- a/lib/isccfg/check.c
+++ b/lib/isccfg/check.c
@@ -3224,7 +3224,7 @@ struct check_one_plugin_data {
 static isc_result_t
 check_one_plugin(const cfg_obj_t *config, const cfg_obj_t *obj,
 		 cfg_aclconfctx_t *aclctx, const char *plugin_path,
-		 const char *const *parameters, void *callback_data) {
+		 const cfg_token_t *parameters, void *callback_data) {
 	struct check_one_plugin_data *data = callback_data;
 	char full_path[PATH_MAX];
 	isc_result_t result = ISC_R_SUCCESS;
diff --git a/lib/isccfg/include/isccfg/cfg.h b/lib/isccfg/include/isccfg/cfg.h
index 3493cda7ef..51ae849ed2 100644
--- a/lib/isccfg/include/isccfg/cfg.h
+++ b/lib/isccfg/include/isccfg/cfg.h
@@ -40,6 +40,7 @@
  ***/

 typedef struct cfg_aclconfctx cfg_aclconfctx_t;
+typedef struct cfg_token      cfg_token_t;

 /*%
  * A configuration parser.
@@ -365,10 +366,10 @@ cfg_obj_istokens(const cfg_obj_t *obj);
  * Return true iff 'obj' is a bracketed token list.
  */

-const char *const *
+const cfg_token_t *
 cfg_obj_astokens(const cfg_obj_t *obj);
 /*%<
- * Returns the value of a bracketed token list as a NULL-terminated
+ * Returns the value of a bracketed token list as a CFG_TOKEN_EOF-terminated
  * array of tokens, encoded as described in <isccfg/tokens.h>.
  *
  * Requires:
@@ -589,7 +590,7 @@ typedef isc_result_t(pluginlist_cb_t)(const cfg_obj_t	*config,
 				      const cfg_obj_t	*obj,
 				      cfg_aclconfctx_t	*aclctx,
 				      const char	*plugin_path,
-				      const char *const *parameters,
+				      const cfg_token_t *parameters,
 				      void		*callback_data);
 /*%<
  * Function prototype for the callback used with cfg_pluginlist_foreach().
diff --git a/lib/isccfg/include/isccfg/grammar.h b/lib/isccfg/include/isccfg/grammar.h
index 74b73991fc..e367707187 100644
--- a/lib/isccfg/include/isccfg/grammar.h
+++ b/lib/isccfg/include/isccfg/grammar.h
@@ -228,7 +228,7 @@ struct cfg_obj {
 		uint32_t	   uint32;
 		uint64_t	   uint64;
 		char		  *string; /*%< null terminated */
-		char		 **tokens; /*%< see <isccfg/tokens.h> */
+		cfg_token_t	  *tokens; /*%< see <isccfg/tokens.h> */
 		bool		   boolean;
 		cfg_map_t	  *map;
 		cfg_list_t	  *list;
@@ -510,12 +510,13 @@ cfg_print_cstr(cfg_printer_t *pctx, const char *s);
 /*%< Print the null-terminated string 's' */

 void
-cfg_print_tokens(cfg_printer_t *pctx, const char *const *tokens,
-		 const char *const *end);
+cfg_print_tokens(cfg_printer_t *pctx, const cfg_token_t *tokens,
+		 const cfg_token_t *end, unsigned long line);
 /*%<
  * Print the token array 'tokens' (see <isccfg/tokens.h>) as a
- * bracketed block, stopping at 'end' or at the terminating NULL,
- * whichever comes first. 'end' may be NULL.
+ * bracketed block, stopping at 'end' or at the terminating CFG_TOKEN_EOF,
+ * whichever comes first. 'end' may be NULL. 'line' is the source line
+ * of the opening bracket.
  */

 isc_result_t
diff --git a/lib/isccfg/include/isccfg/tokens.h b/lib/isccfg/include/isccfg/tokens.h
index 62af5ef769..fd14475e26 100644
--- a/lib/isccfg/include/isccfg/tokens.h
+++ b/lib/isccfg/include/isccfg/tokens.h
@@ -19,13 +19,12 @@
  *
  * The contents of the "{ ... }" block of a "plugin" or "dyndb" statement
  * are lexed, but not parsed, by named. They are handed to the plugin or
- * driver as a NULL-terminated array of tokens, in which:
+ * driver as a CFG_TOKEN_EOF-terminated array of tokens, in which:
  *
  * \li	CFG_TOKEN_OPEN, CFG_TOKEN_CLOSE, CFG_TOKEN_END, CFG_TOKEN_SLASH
  *	and CFG_TOKEN_BANG stand for '{', '}', ';', '/' and '!' respectively;
- * \li	CFG_TOKEN_NEWLINE marks a line break in the original text, so
- *	that line numbers can be tracked;
- * \li	every other element preserves the token's original spelling,
+ * \li	CFG_TOKEN_LINE sets the absolute source line for following tokens;
+ * \li	CFG_TOKEN_STRING preserves the token's original spelling,
  *	including quotes and escapes. For example, "10.0.0.0/8" is three
  *	tokens: "10.0.0.0", CFG_TOKEN_SLASH and "8".
  *
@@ -41,30 +40,36 @@

 #include <isccfg/cfg.h>

-#define CFG_TOKEN_OPEN	  ((const char *)(uintptr_t)-1)
-#define CFG_TOKEN_NEWLINE ((const char *)(uintptr_t)-2)
-#define CFG_TOKEN_CLOSE	  ((const char *)(uintptr_t)-3)
-#define CFG_TOKEN_END	  ((const char *)(uintptr_t)-4)
-#define CFG_TOKEN_SLASH	  ((const char *)(uintptr_t)-5)
-#define CFG_TOKEN_BANG	  ((const char *)(uintptr_t)-6)
-
-/*%
- * True if the (non-NULL) token 't' is a string rather than one of the
- * CFG_TOKEN_* markers.
- */
-#define CFG_TOKEN_ISSTRING(t) ((uintptr_t)(t) < (uintptr_t)CFG_TOKEN_BANG)
+typedef enum {
+	CFG_TOKEN_EOF = 0,
+	CFG_TOKEN_STRING,
+	CFG_TOKEN_LINE,
+	CFG_TOKEN_OPEN,
+	CFG_TOKEN_CLOSE,
+	CFG_TOKEN_END,
+	CFG_TOKEN_SLASH,
+	CFG_TOKEN_BANG,
+} cfg_token_kind_t;
+
+struct cfg_token {
+	cfg_token_kind_t kind;
+	union {
+		const char   *text;
+		unsigned long line;
+	};
+};

 typedef struct cfg_tokenstring cfg_tokenstring_t;

 typedef struct cfg_tokens {
-	const char *const *next;
+	const cfg_token_t *next;
 	const char	  *file;
 	unsigned long	   line;
 	cfg_tokenstring_t *strings;
 } cfg_tokens_t;

 void
-cfg_tokens_init(cfg_tokens_t *tok, const char *const *tokens, const char *file,
+cfg_tokens_init(cfg_tokens_t *tok, const cfg_token_t *tokens, const char *file,
 		unsigned long line);
 /*%<
  * Initialize the cursor 'tok' to walk 'tokens'. 'file' and 'line' are
@@ -79,21 +84,21 @@ cfg_tokens_clear(cfg_tokens_t *tok);
  * with the cursor and all values returned by cfg_tokens_getstring().
  */

-const char *
+const cfg_token_t *
 cfg_tokens_peek(cfg_tokens_t *tok);
 /*%<
- * Return the next token without consuming it, skipping (and counting)
- * line breaks. Returns NULL at the end of the list.
+ * Return the next token without consuming it, skipping line markers and
+ * updating the source line. Returns NULL at the end of the list.
  */

-const char *
+const cfg_token_t *
 cfg_tokens_next(cfg_tokens_t *tok);
 /*%<
  * Like cfg_tokens_peek(), but consume the token.
  */

 isc_result_t
-cfg_tokens_expect(cfg_tokens_t *tok, const char *marker);
+cfg_tokens_expect(cfg_tokens_t *tok, cfg_token_kind_t marker);
 /*%<
  * Consume the next token, which must be 'marker' (one of CFG_TOKEN_OPEN,
  * CFG_TOKEN_CLOSE, CFG_TOKEN_END, CFG_TOKEN_SLASH or CFG_TOKEN_BANG).
diff --git a/lib/isccfg/parser.c b/lib/isccfg/parser.c
index 306e5dc449..79101e351d 100644
--- a/lib/isccfg/parser.c
+++ b/lib/isccfg/parser.c
@@ -241,18 +241,18 @@ static void
 copy_tokens(cfg_obj_t *to, const cfg_obj_t *from) {
 	size_t n = 0;

-	while (from->value.tokens[n] != NULL) {
+	while (from->value.tokens[n].kind != CFG_TOKEN_EOF) {
 		n++;
 	}
-	to->value.tokens = isc_mem_allocate(isc_g_mctx,
-					    (n + 1) * sizeof(char *));
-	for (size_t i = 0; i < n; i++) {
-		char *t = from->value.tokens[i];
-		to->value.tokens[i] = CFG_TOKEN_ISSTRING(t)
-					      ? isc_mem_strdup(isc_g_mctx, t)
-					      : t;
+	to->value.tokens = isc_mem_allocate(
+		isc_g_mctx, (n + 1) * sizeof(*to->value.tokens));
+	for (size_t i = 0; i <= n; i++) {
+		to->value.tokens[i] = from->value.tokens[i];
+		if (from->value.tokens[i].kind == CFG_TOKEN_STRING) {
+			to->value.tokens[i].text = isc_mem_strdup(
+				isc_g_mctx, from->value.tokens[i].text);
+		}
 	}
-	to->value.tokens[n] = NULL;
 }

 static void
@@ -1497,7 +1497,8 @@ cleanup:
 }

 static void
-tokens_append(cfg_obj_t *obj, size_t *countp, size_t *sizep, char *token) {
+tokens_append(cfg_obj_t *obj, size_t *countp, size_t *sizep,
+	      cfg_token_t token) {
 	if (*countp + 1 >= *sizep) {
 		*sizep = (*sizep == 0) ? 16 : *sizep * 2;
 		obj->value.tokens = isc_mem_reallocate(
@@ -1505,7 +1506,19 @@ tokens_append(cfg_obj_t *obj, size_t *countp, size_t *sizep, char *token) {
 			*sizep * sizeof(obj->value.tokens[0]));
 	}
 	obj->value.tokens[(*countp)++] = token;
-	obj->value.tokens[*countp] = NULL;
+	obj->value.tokens[*countp] = (cfg_token_t){ .kind = CFG_TOKEN_EOF };
+}
+
+static size_t
+count_newlines(const char *text, size_t length) {
+	size_t count = 0;
+
+	for (size_t i = 0; i < length; i++) {
+		if (text[i] == '\n') {
+			count++;
+		}
+	}
+	return count;
 }

 static isc_result_t
@@ -1525,37 +1538,53 @@ parse_btokens(cfg_parser_t *pctx, const cfg_type_t *type, cfg_obj_t **ret) {

 	/*
 	 * Create the object now, so that it carries the line number of
-	 * the opening bracket. Start with an empty, NULL-terminated array.
+	 * the opening bracket. Start with an empty, EOF-terminated array.
 	 */
 	cfg_obj_create(cfg_parser_currentfile(pctx), pctx->line, type, &obj);
 	obj->value.tokens = NULL;
-	tokens_append(obj, &count, &size, NULL);
+	tokens_append(obj, &count, &size,
+		      (cfg_token_t){ .kind = CFG_TOKEN_EOF });
 	count = 0;
+	unsigned long line = pctx->line;

 	for (;;) {
-		char *token = NULL;
+		cfg_token_t token = { 0 };
+		isc_region_t region = { 0 };
+
+		CHECK(cfg_gettoken(pctx, CFG_LEXOPT_QSTRING));
+		/* cfg_gettoken() closes the input source at EOF. */
+		if (pctx->token.type != isc_tokentype_eof) {
+			isc_lex_getlasttokentext(pctx->lexer, &pctx->token,
+						 &region);
+		}
+		const size_t next = pctx->line -
+				    count_newlines((const char *)region.base,
+						   region.length);
+
+		/* Set the starting line, including after multiline strings. */
+		if (next != line) {
+			tokens_append(obj, &count, &size,
+				      (cfg_token_t){ .kind = CFG_TOKEN_LINE,
+						     .line = next });
+			line = next;
+		}

-		CHECK(cfg_gettoken(pctx, CFG_LEXOPT_QSTRING | ISC_LEXOPT_EOL));
 		switch (pctx->token.type) {
-		case isc_tokentype_eol:
-			token = UNCONST(CFG_TOKEN_NEWLINE);
-			break;
 		case isc_tokentype_string:
 		case isc_tokentype_qstring: {
-			isc_region_t region;
-
-			isc_lex_getlasttokentext(pctx->lexer, &pctx->token,
-						 &region);
-			token = isc_mem_allocate(isc_g_mctx, region.length + 1);
-			memmove(token, region.base, region.length);
-			token[region.length] = '\0';
+			char *text = isc_mem_allocate(isc_g_mctx,
+						      region.length + 1);
+			memmove(text, region.base, region.length);
+			text[region.length] = '\0';
+			token = (cfg_token_t){ .kind = CFG_TOKEN_STRING,
+					       .text = text };
 			break;
 		}
 		case isc_tokentype_special:
 			switch (pctx->token.value.as_char) {
 			case '{':
 				depth++;
-				token = UNCONST(CFG_TOKEN_OPEN);
+				token.kind = CFG_TOKEN_OPEN;
 				break;
 			case '}':
 				if (depth == 0) {
@@ -1563,16 +1592,16 @@ parse_btokens(cfg_parser_t *pctx, const cfg_type_t *type, cfg_obj_t **ret) {
 					return ISC_R_SUCCESS;
 				}
 				depth--;
-				token = UNCONST(CFG_TOKEN_CLOSE);
+				token.kind = CFG_TOKEN_CLOSE;
 				break;
 			case ';':
-				token = UNCONST(CFG_TOKEN_END);
+				token.kind = CFG_TOKEN_END;
 				break;
 			case '/':
-				token = UNCONST(CFG_TOKEN_SLASH);
+				token.kind = CFG_TOKEN_SLASH;
 				break;
 			case '!':
-				token = UNCONST(CFG_TOKEN_BANG);
+				token.kind = CFG_TOKEN_BANG;
 				break;
 			default:
 				UNREACHABLE();
@@ -1598,53 +1627,57 @@ cleanup:
 }

 void
-cfg_print_tokens(cfg_printer_t *pctx, const char *const *tokens,
-		 const char *const *end) {
+cfg_print_tokens(cfg_printer_t *pctx, const cfg_token_t *tokens,
+		 const cfg_token_t *end, unsigned long line) {
 	bool oneline = (pctx->flags & CFG_PRINTER_ONELINE) != 0;
 	bool newline = false;

 	/*
-	 * Line breaks are reproduced from the CFG_TOKEN_NEWLINE markers,
-	 * so the output has the same line structure as the input; the
-	 * indentation is derived from the bracket depth.
+	 * Preserve source line spacing for ACL reconstruction and derive
+	 * indentation from the bracket depth.
 	 */
 	pctx->indent++;
 	cfg_print_cstr(pctx, "{");
-	for (; tokens != end && *tokens != NULL; tokens++) {
-		const char *t = *tokens;
+	for (; tokens != end && tokens->kind != CFG_TOKEN_EOF; tokens++) {
+		const cfg_token_t *t = tokens;

-		if (t == CFG_TOKEN_NEWLINE) {
+		if (t->kind == CFG_TOKEN_LINE) {
 			if (!oneline) {
-				cfg_print_cstr(pctx, "\n");
-				newline = true;
+				while (line < t->line) {
+					cfg_print_cstr(pctx, "\n");
+					newline = true;
+					line++;
+				}
 			}
+			line = t->line;
 			continue;
 		}

-		if (t == CFG_TOKEN_CLOSE) {
+		if (t->kind == CFG_TOKEN_CLOSE) {
 			pctx->indent--;
 		}

 		if (newline) {
 			cfg_print_indent(pctx);
 			newline = false;
-		} else if (t != CFG_TOKEN_END) {
+		} else if (t->kind != CFG_TOKEN_END) {
 			cfg_print_cstr(pctx, " ");
 		}

-		if (t == CFG_TOKEN_OPEN) {
+		if (t->kind == CFG_TOKEN_OPEN) {
 			cfg_print_cstr(pctx, "{");
 			pctx->indent++;
-		} else if (t == CFG_TOKEN_CLOSE) {
+		} else if (t->kind == CFG_TOKEN_CLOSE) {
 			cfg_print_cstr(pctx, "}");
-		} else if (t == CFG_TOKEN_END) {
+		} else if (t->kind == CFG_TOKEN_END) {
 			cfg_print_cstr(pctx, ";");
-		} else if (t == CFG_TOKEN_SLASH) {
+		} else if (t->kind == CFG_TOKEN_SLASH) {
 			cfg_print_cstr(pctx, "/");
-		} else if (t == CFG_TOKEN_BANG) {
+		} else if (t->kind == CFG_TOKEN_BANG) {
 			cfg_print_cstr(pctx, "!");
 		} else {
-			cfg_print_cstr(pctx, t);
+			cfg_print_cstr(pctx, t->text);
+			line += count_newlines(t->text, strlen(t->text));
 		}
 	}

@@ -1659,7 +1692,7 @@ cfg_print_tokens(cfg_printer_t *pctx, const char *const *tokens,

 static void
 print_btokens(cfg_printer_t *pctx, const cfg_obj_t *obj) {
-	cfg_print_tokens(pctx, cfg_obj_astokens(obj), NULL);
+	cfg_print_tokens(pctx, cfg_obj_astokens(obj), NULL, obj->line);
 }

 static void
@@ -1673,11 +1706,11 @@ cfg_obj_istokens(const cfg_obj_t *obj) {
 	return obj->type->rep == &cfg_rep_tokens;
 }

-const char *const *
+const cfg_token_t *
 cfg_obj_astokens(const cfg_obj_t *obj) {
 	REQUIRE(VALID_CFGOBJ(obj));
 	REQUIRE(obj->type->rep == &cfg_rep_tokens);
-	return (const char *const *)obj->value.tokens;
+	return obj->value.tokens;
 }

 bool
@@ -1845,9 +1878,12 @@ free_tokens(cfg_obj_t *obj) {
 	if (obj->value.tokens == NULL) {
 		return;
 	}
-	for (char **t = obj->value.tokens; *t != NULL; t++) {
-		if (CFG_TOKEN_ISSTRING(*t)) {
-			isc_mem_free(isc_g_mctx, *t);
+	for (cfg_token_t *t = obj->value.tokens; t->kind != CFG_TOKEN_EOF; t++)
+	{
+		if (t->kind == CFG_TOKEN_STRING) {
+			char *text = UNCONST(t->text);
+			isc_mem_free(isc_g_mctx, text);
+			t->text = NULL;
 		}
 	}
 	isc_mem_free(isc_g_mctx, obj->value.tokens);
@@ -4340,7 +4376,7 @@ cfg_pluginlist_foreach(const cfg_obj_t *config, const cfg_obj_t *list,
 		const cfg_obj_t *plugin = cfg_listelt_value(element);
 		const cfg_obj_t *obj;
 		const char *type, *library;
-		const char *const *parameters = NULL;
+		const cfg_token_t *parameters = NULL;

 		/* Get the path to the plugin module. */
 		obj = cfg_tuple_get(plugin, "type");
diff --git a/lib/isccfg/tokens.c b/lib/isccfg/tokens.c
index 37f125dc38..73238d7131 100644
--- a/lib/isccfg/tokens.c
+++ b/lib/isccfg/tokens.c
@@ -30,10 +30,10 @@ struct cfg_tokenstring {
 	cfg_tokenstring_t *next;
 };

-static const char *const empty[] = { NULL };
+static const cfg_token_t empty[] = { { .kind = CFG_TOKEN_EOF } };

 void
-cfg_tokens_init(cfg_tokens_t *tok, const char *const *tokens, const char *file,
+cfg_tokens_init(cfg_tokens_t *tok, const cfg_token_t *tokens, const char *file,
 		unsigned long line) {
 	REQUIRE(tok != NULL);

@@ -55,20 +55,20 @@ cfg_tokens_clear(cfg_tokens_t *tok) {
 	}
 }

-const char *
+const cfg_token_t *
 cfg_tokens_peek(cfg_tokens_t *tok) {
 	REQUIRE(tok != NULL);

-	while (*tok->next == CFG_TOKEN_NEWLINE) {
+	while (tok->next->kind == CFG_TOKEN_LINE) {
+		tok->line = tok->next->line;
 		tok->next++;
-		tok->line++;
 	}
-	return *tok->next;
+	return tok->next->kind == CFG_TOKEN_EOF ? NULL : tok->next;
 }

-const char *
+const cfg_token_t *
 cfg_tokens_next(cfg_tokens_t *tok) {
-	const char *token = cfg_tokens_peek(tok);
+	const cfg_token_t *token = cfg_tokens_peek(tok);

 	if (token != NULL) {
 		tok->next++;
@@ -97,7 +97,7 @@ cfg_tokens_log(const cfg_tokens_t *tok, int level, const char *fmt, ...) {
 }

 static const char *
-markername(const char *marker) {
+markername(cfg_token_kind_t marker) {
 	if (marker == CFG_TOKEN_OPEN) {
 		return "'{'";
 	} else if (marker == CFG_TOKEN_CLOSE) {
@@ -113,8 +113,8 @@ markername(const char *marker) {
 }

 isc_result_t
-cfg_tokens_expect(cfg_tokens_t *tok, const char *marker) {
-	const char *token = cfg_tokens_next(tok);
+cfg_tokens_expect(cfg_tokens_t *tok, cfg_token_kind_t marker) {
+	const cfg_token_t *token = cfg_tokens_next(tok);

 	if (token == NULL) {
 		cfg_tokens_log(tok, ISC_LOG_ERROR,
@@ -122,7 +122,7 @@ cfg_tokens_expect(cfg_tokens_t *tok, const char *marker) {
 			       markername(marker));
 		return ISC_R_UNEXPECTEDEND;
 	}
-	if (token != marker) {
+	if (token->kind != marker) {
 		cfg_tokens_log(tok, ISC_LOG_ERROR, "expected %s",
 			       markername(marker));
 		return ISC_R_UNEXPECTEDTOKEN;
@@ -132,7 +132,7 @@ cfg_tokens_expect(cfg_tokens_t *tok, const char *marker) {

 isc_result_t
 cfg_tokens_getstring(cfg_tokens_t *tok, const char **strp) {
-	const char *token = NULL;
+	const cfg_token_t *token = NULL;

 	REQUIRE(strp != NULL);

@@ -142,16 +142,16 @@ cfg_tokens_getstring(cfg_tokens_t *tok, const char **strp) {
 			       "expected string, found end of input");
 		return ISC_R_UNEXPECTEDEND;
 	}
-	if (!CFG_TOKEN_ISSTRING(token)) {
+	if (token->kind != CFG_TOKEN_STRING) {
 		cfg_tokens_log(tok, ISC_LOG_ERROR, "expected string");
 		return ISC_R_UNEXPECTEDTOKEN;
 	}
-	if (token[0] == '"') {
+	if (token->text[0] == '"') {
 		cfg_obj_t *obj = NULL;
 		isc_buffer_t buffer;
-		size_t length = strlen(token);
+		size_t length = strlen(token->text);

-		isc_buffer_constinit(&buffer, token, length);
+		isc_buffer_constinit(&buffer, token->text, length);
 		isc_buffer_add(&buffer, length);
 		RETERR(cfg_parse_buffer(&buffer, tok->file, tok->line,
 					&cfg_type_qstring, 0, &obj));
@@ -163,7 +163,7 @@ cfg_tokens_getstring(cfg_tokens_t *tok, const char **strp) {
 		tok->strings = string;
 		*strp = cfg_obj_asstring(obj);
 	} else {
-		*strp = token;
+		*strp = token->text;
 	}
 	return ISC_R_SUCCESS;
 }
@@ -176,7 +176,7 @@ print_tobuffer(void *closure, const char *text, int textlen) {
 isc_result_t
 cfg_tokens_getaml(cfg_tokens_t *tok, cfg_obj_t **objp) {
 	isc_result_t result;
-	const char *const *end = NULL;
+	const cfg_token_t *end = NULL;
 	unsigned long line;
 	unsigned int depth = 0;
 	isc_buffer_t *b = NULL;
@@ -189,13 +189,13 @@ cfg_tokens_getaml(cfg_tokens_t *tok, cfg_obj_t **objp) {

 	/* Find the matching closing bracket. */
 	for (end = tok->next;; end++) {
-		if (*end == NULL) {
+		if (end->kind == CFG_TOKEN_EOF) {
 			cfg_tokens_log(tok, ISC_LOG_ERROR,
 				       "expected '}', found end of input");
 			return ISC_R_UNEXPECTEDEND;
-		} else if (*end == CFG_TOKEN_OPEN) {
+		} else if (end->kind == CFG_TOKEN_OPEN) {
 			depth++;
-		} else if (*end == CFG_TOKEN_CLOSE) {
+		} else if (end->kind == CFG_TOKEN_CLOSE) {
 			if (depth == 0) {
 				break;
 			}
@@ -211,15 +211,15 @@ cfg_tokens_getaml(cfg_tokens_t *tok, cfg_obj_t **objp) {
 	 */
 	isc_buffer_allocate(isc_g_mctx, &b, 256);
 	pctx = (cfg_printer_t){ .f = print_tobuffer, .closure = b };
-	cfg_print_tokens(&pctx, tok->next, end);
+	cfg_print_tokens(&pctx, tok->next, end, line);

 	result = cfg_parse_buffer(b, tok->file, line, &cfg_type_bracketed_aml,
 				  0, objp);
 	isc_buffer_free(&b);

 	for (; tok->next != end; tok->next++) {
-		if (*tok->next == CFG_TOKEN_NEWLINE) {
-			tok->line++;
+		if (tok->next->kind == CFG_TOKEN_LINE) {
+			tok->line = tok->next->line;
 		}
 	}
 	tok->next++;
diff --git a/lib/ns/hooks.c b/lib/ns/hooks.c
index 53dbabe312..efaef4227e 100644
--- a/lib/ns/hooks.c
+++ b/lib/ns/hooks.c
@@ -211,7 +211,7 @@ unload_plugin(ns_plugin_t **pluginp) {
 }

 isc_result_t
-ns_plugin_register(const char *modpath, const char *const *parameters,
+ns_plugin_register(const char *modpath, const cfg_token_t *parameters,
 		   const void *cfg, const char *cfg_file,
 		   unsigned long cfg_line, isc_mem_t *mctx, void *aclctx,
 		   ns_hook_data_t *hookdata) {
@@ -248,7 +248,7 @@ cleanup:
 }

 isc_result_t
-ns_plugin_check(const char *modpath, const char *const *parameters,
+ns_plugin_check(const char *modpath, const cfg_token_t *parameters,
 		const void *cfg, const char *cfg_file, unsigned long cfg_line,
 		isc_mem_t *mctx, void *aclctx, const ns_pluginctx_t *ctx) {
 	isc_result_t result;
diff --git a/lib/ns/include/ns/hooks.h b/lib/ns/include/ns/hooks.h
index c530f23e28..837f4c331c 100644
--- a/lib/ns/include/ns/hooks.h
+++ b/lib/ns/include/ns/hooks.h
@@ -26,6 +26,8 @@

 #include <ns/client.h>
 #include <ns/query.h>
+
+typedef struct cfg_token cfg_token_t;
 /*
  * "Hooks" are a mechanism to call a defined function or set of functions once
  * a certain place in code is reached.  Hook actions can inspect and alter the
@@ -515,12 +517,12 @@ typedef struct ns_hook_data {
  * as well; if not, set NS_PLUGIN_AGE to 0.
  */
 #ifndef NS_PLUGIN_VERSION
-#define NS_PLUGIN_VERSION 5
+#define NS_PLUGIN_VERSION 6
 #define NS_PLUGIN_AGE	  0
 #endif /* ifndef NS_PLUGIN_VERSION */

 typedef isc_result_t
-ns_plugin_register_t(const char *const *parameters, const void *cfg,
+ns_plugin_register_t(const cfg_token_t *parameters, const void *cfg,
 		     const char *file, unsigned long line, isc_mem_t *mctx,
 		     void *aclctx, ns_hooktable_t *hooktable,
 		     const ns_pluginctx_t *ctx, void **instp);
@@ -549,7 +551,7 @@ ns_plugin_destroy_t(void **instp);
  */

 typedef isc_result_t
-ns_plugin_check_t(const char *const *parameters, const void *cfg,
+ns_plugin_check_t(const cfg_token_t *parameters, const void *cfg,
 		  const char *file, unsigned long line, isc_mem_t *mctx,
 		  void *aclctx, const ns_pluginctx_t *ctx);
 /*%<
@@ -597,7 +599,7 @@ ns_plugin_expandpath(const char *src, char *dst, size_t dstsize);
  */

 isc_result_t
-ns_plugin_register(const char *modpath, const char *const *parameters,
+ns_plugin_register(const char *modpath, const cfg_token_t *parameters,
 		   const void *cfg, const char *cfg_file,
 		   unsigned long cfg_line, isc_mem_t *mctx, void *aclctx,
 		   ns_hook_data_t *hookdata);
@@ -617,7 +619,7 @@ ns_plugin_register(const char *modpath, const char *const *parameters,
  */

 isc_result_t
-ns_plugin_check(const char *modpath, const char *const *parameters,
+ns_plugin_check(const char *modpath, const cfg_token_t *parameters,
 		const void *cfg, const char *cfg_file, unsigned long cfg_line,
 		isc_mem_t *mctx, void *aclctx, const ns_pluginctx_t *ctx);
 /*%<
diff --git a/tests/isccfg/parser_test.c b/tests/isccfg/parser_test.c
index ae8f8886de..41066d38a8 100644
--- a/tests/isccfg/parser_test.c
+++ b/tests/isccfg/parser_test.c
@@ -60,30 +60,51 @@ append(void *arg, const char *str, int len) {

 /* Preserve lexical spelling, including quoted punctuation and escapes. */
 ISC_RUN_TEST_IMPL(tokens_roundtrip) {
-	const char *spelling[] = {
-		"plain",     "\"plain\"",     "\"\"",	       "\"a\\ b\"",
-		"\"a\\;b\"", "\"a\\#b\"",     "\"a\\/b\"",     "\"a\\!b\"",
-		"\"a\\{b\"", "\"a\\}b\"",     "\"a\\\tb\"",    "\"a\\\"b\"",
-		"tail\\",    "tail\\\\",      "\"tail\\\\\"",  "\"a\\\\\\\"b\"",
-		"\"/\"",     "\"/\"",	      "\"/\"",	       "\"*\"",
-		"\"!\"",     CFG_TOKEN_SLASH, CFG_TOKEN_BANG,  CFG_TOKEN_OPEN,
-		"nested",    CFG_TOKEN_END,   CFG_TOKEN_CLOSE, CFG_TOKEN_END,
-		NULL
+	const cfg_token_t spelling[] = {
+		{ .kind = CFG_TOKEN_STRING, .text = "plain" },
+		{ .kind = CFG_TOKEN_STRING, .text = "\"plain\"" },
+		{ .kind = CFG_TOKEN_STRING, .text = "\"\"" },
+		{ .kind = CFG_TOKEN_STRING, .text = "\"a\\ b\"" },
+		{ .kind = CFG_TOKEN_STRING, .text = "\"a\\;b\"" },
+		{ .kind = CFG_TOKEN_STRING, .text = "\"a\\#b\"" },
+		{ .kind = CFG_TOKEN_STRING, .text = "\"a\\/b\"" },
+		{ .kind = CFG_TOKEN_STRING, .text = "\"a\\!b\"" },
+		{ .kind = CFG_TOKEN_STRING, .text = "\"a\\{b\"" },
+		{ .kind = CFG_TOKEN_STRING, .text = "\"a\\}b\"" },
+		{ .kind = CFG_TOKEN_STRING, .text = "\"a\\\tb\"" },
+		{ .kind = CFG_TOKEN_STRING, .text = "\"a\\\"b\"" },
+		{ .kind = CFG_TOKEN_STRING, .text = "tail\\" },
+		{ .kind = CFG_TOKEN_STRING, .text = "tail\\\\" },
+		{ .kind = CFG_TOKEN_STRING, .text = "\"tail\\\\\"" },
+		{ .kind = CFG_TOKEN_STRING, .text = "\"a\\\\\\\"b\"" },
+		{ .kind = CFG_TOKEN_STRING, .text = "\"/\"" },
+		{ .kind = CFG_TOKEN_STRING, .text = "\"/\"" },
+		{ .kind = CFG_TOKEN_STRING, .text = "\"/\"" },
+		{ .kind = CFG_TOKEN_STRING, .text = "\"*\"" },
+		{ .kind = CFG_TOKEN_STRING, .text = "\"!\"" },
+		{ .kind = CFG_TOKEN_SLASH },
+		{ .kind = CFG_TOKEN_BANG },
+		{ .kind = CFG_TOKEN_OPEN },
+		{ .kind = CFG_TOKEN_STRING, .text = "nested" },
+		{ .kind = CFG_TOKEN_END },
+		{ .kind = CFG_TOKEN_CLOSE },
+		{ .kind = CFG_TOKEN_END },
+		{ .kind = CFG_TOKEN_EOF }
 	};
 	unsigned int flags[] = { 0, CFG_PRINTER_ONELINE };
 	char input[1024] = "{ ";

-	for (const char *const *t = spelling; *t != NULL; t++) {
-		const char *text = *t;
-		if (text == CFG_TOKEN_OPEN) {
+	for (const cfg_token_t *t = spelling; t->kind != CFG_TOKEN_EOF; t++) {
+		const char *text = t->text;
+		if (t->kind == CFG_TOKEN_OPEN) {
 			text = "{";
-		} else if (text == CFG_TOKEN_CLOSE) {
+		} else if (t->kind == CFG_TOKEN_CLOSE) {
 			text = "}";
-		} else if (text == CFG_TOKEN_END) {
+		} else if (t->kind == CFG_TOKEN_END) {
 			text = ";";
-		} else if (text == CFG_TOKEN_SLASH) {
+		} else if (t->kind == CFG_TOKEN_SLASH) {
 			text = "/";
-		} else if (text == CFG_TOKEN_BANG) {
+		} else if (t->kind == CFG_TOKEN_BANG) {
 			text = "!";
 		}
 		strlcat(input, text, sizeof(input));
@@ -110,35 +131,155 @@ ISC_RUN_TEST_IMPL(tokens_roundtrip) {
 						  &copy),
 				 ISC_R_SUCCESS);

-		const char *const *original = cfg_obj_astokens(obj);
-		const char *const *reparsed = cfg_obj_astokens(copy);
+		const cfg_token_t *original = cfg_obj_astokens(obj);
+		const cfg_token_t *reparsed = cfg_obj_astokens(copy);
 		for (size_t j = 0; j < ARRAY_SIZE(spelling); j++) {
-			if (spelling[j] != NULL &&
-			    CFG_TOKEN_ISSTRING(spelling[j]))
-			{
-				assert_non_null(original[j]);
-				assert_non_null(reparsed[j]);
-				assert_true(CFG_TOKEN_ISSTRING(original[j]));
-				assert_true(CFG_TOKEN_ISSTRING(reparsed[j]));
-				assert_string_equal(original[j], spelling[j]);
-				assert_string_equal(reparsed[j], spelling[j]);
-			} else {
-				assert_ptr_equal(original[j], spelling[j]);
-				assert_ptr_equal(reparsed[j], spelling[j]);
+			assert_int_equal(original[j].kind, spelling[j].kind);
+			assert_int_equal(reparsed[j].kind, spelling[j].kind);
+			if (spelling[j].kind == CFG_TOKEN_STRING) {
+				assert_string_equal(original[j].text,
+						    spelling[j].text);
+				assert_string_equal(reparsed[j].text,
+						    spelling[j].text);
 			}
 		}
+
 		cfg_obj_detach(&obj);
 		cfg_obj_detach(&copy);
 	}
 }

+/* Preserve skipped lines without duplicating newlines inside strings. */
+ISC_RUN_TEST_IMPL(tokens_newlines) {
+	const char *input = "{\r\n\n/* comment\ncontinued */ plain;\n"
+			    "// comment\n# comment\n\"first\nsecond\";\n"
+			    "{ nested;\n}; /* trailing\ncomment */\n}";
+	const cfg_token_t expected[] = {
+		{ .kind = CFG_TOKEN_LINE, .line = 103 },
+		{ .kind = CFG_TOKEN_STRING, .text = "plain" },
+		{ .kind = CFG_TOKEN_END },
+		{ .kind = CFG_TOKEN_LINE, .line = 106 },
+		{ .kind = CFG_TOKEN_STRING, .text = "\"first\nsecond\"" },
+		{ .kind = CFG_TOKEN_LINE, .line = 107 },
+		{ .kind = CFG_TOKEN_END },
+		{ .kind = CFG_TOKEN_LINE, .line = 108 },
+		{ .kind = CFG_TOKEN_OPEN },
+		{ .kind = CFG_TOKEN_STRING, .text = "nested" },
+		{ .kind = CFG_TOKEN_END },
+		{ .kind = CFG_TOKEN_LINE, .line = 109 },
+		{ .kind = CFG_TOKEN_CLOSE },
+		{ .kind = CFG_TOKEN_END },
+		{ .kind = CFG_TOKEN_LINE, .line = 111 },
+		{ .kind = CFG_TOKEN_EOF },
+	};
+	char printed[1024] = "";
+
+	for (size_t pass = 0; pass < 2; pass++) {
+		cfg_obj_t *obj = NULL;
+		isc_buffer_t b;
+
+		isc_buffer_constinit(&b, input, strlen(input));
+		isc_buffer_add(&b, strlen(input));
+		assert_int_equal(cfg_parse_buffer(&b, "newlines", 100,
+						  &cfg_type_bracketed_tokens, 0,
+						  &obj),
+				 ISC_R_SUCCESS);
+
+		const cfg_token_t *tokens = cfg_obj_astokens(obj);
+		for (size_t i = 0; i < ARRAY_SIZE(expected); i++) {
+			assert_int_equal(tokens[i].kind, expected[i].kind);
+			if (expected[i].kind == CFG_TOKEN_STRING) {
+				assert_string_equal(tokens[i].text,
+						    expected[i].text);
+			} else if (expected[i].kind == CFG_TOKEN_LINE) {
+				assert_int_equal(tokens[i].line,
+						 expected[i].line);
+			}
+		}
+
+		if (pass == 0) {
+			cfg_printx(obj, 0, append, printed);
+			input = printed;
+		}
+		cfg_obj_detach(&obj);
+	}
+}
+
+/* Absolute line markers locate tokens after multiline strings and ACLs. */
+ISC_RUN_TEST_IMPL(tokens_cursor_lines) {
+	const char *input = "{\n\"first\nsecond\nthird\";\n"
+			    "{ \"acl\nname\"; any;\n}\n{ any; }\n}";
+	isc_buffer_t b;
+	cfg_obj_t *tokens = NULL, *copy = NULL;
+
+	isc_buffer_constinit(&b, input, strlen(input));
+	isc_buffer_add(&b, strlen(input));
+	assert_int_equal(cfg_parse_buffer(&b, "lines", 100,
+					  &cfg_type_bracketed_tokens, 0,
+					  &tokens),
+			 ISC_R_SUCCESS);
+	cfg_obj_clone(tokens, &copy);
+	cfg_obj_detach(&tokens);
+
+	for (size_t pass = 0; pass < 2; pass++) {
+		cfg_tokens_t tok;
+		cfg_obj_t *aml = NULL;
+		const char *value = NULL;
+
+		cfg_tokens_init(&tok, cfg_obj_astokens(copy), "lines", 100);
+		for (size_t i = 0; i < 2; i++) {
+			const cfg_token_t *token = cfg_tokens_peek(&tok);
+			assert_non_null(token);
+			assert_int_equal(token->kind, CFG_TOKEN_STRING);
+			assert_string_equal(token->text,
+					    "\"first\nsecond\nthird\"");
+			assert_int_equal(tok.line, 101);
+		}
+		if (pass == 0) {
+			assert_string_equal(cfg_tokens_next(&tok)->text,
+					    "\"first\nsecond\nthird\"");
+		} else {
+			assert_int_equal(cfg_tokens_getstring(&tok, &value),
+					 ISC_R_SUCCESS);
+			assert_string_equal(value, "first\nsecond\nthird");
+		}
+		assert_int_equal(tok.line, 101);
+		assert_int_equal(cfg_tokens_expect(&tok, CFG_TOKEN_END),
+				 ISC_R_SUCCESS);
+		assert_int_equal(tok.line, 103);
+		assert_int_equal(cfg_tokens_getaml(&tok, &aml), ISC_R_SUCCESS);
+		assert_int_equal(cfg_obj_line(aml), 104);
+		const cfg_listelt_t *second =
+			cfg_list_next(cfg_list_first(aml));
+		assert_non_null(second);
+		assert_int_equal(cfg_obj_line(cfg_listelt_value(second)), 105);
+		assert_int_equal(tok.line, 106);
+		cfg_obj_detach(&aml);
+		assert_int_equal(cfg_tokens_getaml(&tok, &aml), ISC_R_SUCCESS);
+		assert_int_equal(cfg_obj_line(aml), 107);
+		assert_int_equal(tok.line, 107);
+		cfg_obj_detach(&aml);
+		assert_null(cfg_tokens_next(&tok));
+		assert_int_equal(tok.line, 108);
+		cfg_tokens_clear(&tok);
+	}
+	cfg_obj_detach(&copy);
+}
+
 /* Decoding leaves lexical tokens intact and previous results valid. */
 ISC_RUN_TEST_IMPL(tokens_strings) {
-	const char *tokens[] = { "\"dynamic-\"", "\"\"",
-				 "\"a\\\"b\"",	 "\"a\\ b\"",
-				 "\"tail\\\\\"", "\"a\\\\\\\"b\"",
-				 "a\\\"b",	 "\"a\nb\"",
-				 "plain",	 NULL };
+	const cfg_token_t tokens[] = {
+		{ .kind = CFG_TOKEN_STRING, .text = "\"dynamic-\"" },
+		{ .kind = CFG_TOKEN_STRING, .text = "\"\"" },
+		{ .kind = CFG_TOKEN_STRING, .text = "\"a\\\"b\"" },
+		{ .kind = CFG_TOKEN_STRING, .text = "\"a\\ b\"" },
+		{ .kind = CFG_TOKEN_STRING, .text = "\"tail\\\\\"" },
+		{ .kind = CFG_TOKEN_STRING, .text = "\"a\\\\\\\"b\"" },
+		{ .kind = CFG_TOKEN_STRING, .text = "a\\\"b" },
+		{ .kind = CFG_TOKEN_STRING, .text = "\"a\nb\"" },
+		{ .kind = CFG_TOKEN_STRING, .text = "plain" },
+		{ .kind = CFG_TOKEN_EOF },
+	};
 	const char *expected[] = { "dynamic-", "",	   "a\"b",
 				   "a\\ b",    "tail\\\\", "a\\\\\"b",
 				   "a\\\"b",   "a\nb",	   "plain" };
@@ -147,7 +288,7 @@ ISC_RUN_TEST_IMPL(tokens_strings) {

 	cfg_tokens_init(&tok, tokens, "strings", 1);
 	for (size_t i = 0; i < ARRAY_SIZE(expected); i++) {
-		assert_ptr_equal(cfg_tokens_peek(&tok), tokens[i]);
+		assert_ptr_equal(cfg_tokens_peek(&tok), &tokens[i]);
 		assert_int_equal(cfg_tokens_getstring(&tok, &values[i]),
 				 ISC_R_SUCCESS);
 	}
@@ -155,7 +296,7 @@ ISC_RUN_TEST_IMPL(tokens_strings) {
 	for (size_t i = 0; i < ARRAY_SIZE(expected); i++) {
 		assert_string_equal(values[i], expected[i]);
 	}
-	assert_string_equal(tokens[0], "\"dynamic-\"");
+	assert_string_equal(tokens[0].text, "\"dynamic-\"");
 	cfg_tokens_clear(&tok);
 }

@@ -500,6 +641,8 @@ ISC_RUN_TEST_IMPL(cfg_map_findclause_empty) {
 ISC_TEST_LIST_START

 ISC_TEST_ENTRY(tokens_roundtrip)
+ISC_TEST_ENTRY(tokens_newlines)
+ISC_TEST_ENTRY(tokens_cursor_lines)
 ISC_TEST_ENTRY(tokens_strings)
 ISC_TEST_ENTRY(tokens_aml)
 ISC_TEST_ENTRY(addzoneconf)